diff --git a/.archive/.flake8 b/.archive/.flake8 new file mode 100644 index 0000000000..f855799a35 --- /dev/null +++ b/.archive/.flake8 @@ -0,0 +1,5 @@ +[flake8] +max-line-length = 88 +select = B,BLK,C90,E,F,I,W +max-complexity = 15 +extend-ignore = E203 diff --git a/.gitattributes b/.archive/.gitattributes similarity index 100% rename from .gitattributes rename to .archive/.gitattributes diff --git a/.github/CODEOWNERS b/.archive/.github/CODEOWNERS similarity index 100% rename from .github/CODEOWNERS rename to .archive/.github/CODEOWNERS diff --git a/.github/workflows/continuous-integration-workflow.yml b/.archive/.github/workflows/continuous-integration-workflow.yml similarity index 100% rename from .github/workflows/continuous-integration-workflow.yml rename to .archive/.github/workflows/continuous-integration-workflow.yml diff --git a/.archive/.gitignore b/.archive/.gitignore new file mode 100644 index 0000000000..e359288dee --- /dev/null +++ b/.archive/.gitignore @@ -0,0 +1,18 @@ +# IDE Settings +/.vscode + +# lisa runtime folder +/runtime/* + +# python cache +__pycache__ + +# you can have your own command alias on Windows +lisa.cmd + +# it's auto generated by poetry +lisa.egg-info + +# code coverage result +.coverage +htmlcov diff --git a/.archive/CODE_OF_CONDUCT.md b/.archive/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..f9ba8cf65f --- /dev/null +++ b/.archive/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/.archive/CONTRIBUTING.md b/.archive/CONTRIBUTING.md new file mode 100644 index 0000000000..353e2c6af3 --- /dev/null +++ b/.archive/CONTRIBUTING.md @@ -0,0 +1,284 @@ +# Contributing Guidelines + +This document describes the existing developer tooling we have in place (and what to +expect of it), as well as our design and development philosophy. + +## Naming Conventions + +Naming conventions are not automatically enforced, so please read the [naming +conventions](https://www.python.org/dev/peps/pep-0008/#naming-conventions) +section of PEP 8, which describes what each of the different styles means. A +short summary of the most important parts: + +* Modules (and hence files) should have short, all-lowercase names. +* Class (and exception) names should normally use the `CapWords` convention + (also known as `CamelCase`). +* Function and variable names should be lowercase, with words separated by + underscores as necessary to improve readability (also known as `snake_case`). +* To avoid collisions with the standard library, an underscore can be appended, + such as `id_`. +* Always use `self` for the first argument to instance methods. +* Always use `cls` for the first argument to class methods. +* One leading underscore like `_data` is for non-public methods and instance + variables. And it can be used by sub-classes. If it won't be used in + sub-classes, use like `__data`. +* If there is a pair of `get_x` and `set_x` methods, they should instead be a + proper property, which is easy to do with the built-in `@property` decorator. +* Constants should be `CAPITALIZED_SNAKE_CASE`. +* When importing a function, try to avoid renaming it with `import as` because + it introduces cognitive overhead to track yet another name. + +When in doubt, adhere to existing conventions, or check the style guide. + +## Automated Tooling + +If you have ran LISAv3 already, then you have installed and used the `poetry` +tool. [Poetry][] is a [PEP 518][] compliant and cross-platform build system +which handles our Python dependencies and environment. + +This project’s dependencies are found in the [`pyproject.toml`](pyproject.toml) +file. This is similar to but more powerful than the familiar `requirements.txt`. +With [PEP 518][] and [PEP 621][]. + +[Poetry]: https://python-poetry.org/docs/ +[PEP 518]: https://www.python.org/dev/peps/pep-0518/ +[PEP 621]: https://www.python.org/dev/peps/pep-0621/ + +### Metadata + +The first section, `tool.poetry`, defines the project’s metadata (name, version, +description, authors, and license) which will be embedded in the final built +package. + +The chosen version follows [Semantic Versioning][], with the [Python specific +pre-release versioning suffix][pre-release] ‘.dev1’. Since this is “LISAv3” it +seemed appropriate to set our version to ‘3.0.0.dev1’, that is, “the first +development release of LISAv3.” + +[Semantic Versioning]: https://semver.org/ +[pre-release]: https://packaging.python.org/guides/distributing-packages-using-setuptools/#choosing-a-versioning-scheme + +### Package Dependencies + +The next section, `tool.poetry.dependencies`, is where `poetry add +` records our required packages. + +Poetry automatically creates and manages [isolated +environments](https://python-poetry.org/docs/managing-environments/). + +From the documentation: + +> Poetry will first check if it’s currently running inside a virtual +> environment. If it is, it will use it directly without creating a new one. But +> if it’s not, it will use one that it has already created or create a brand new +> one for you. + +On Linux, your initial run of `poetry install` will cause Poetry to +automatically setup a new [virtualenv][] using [pyenv][]. If you are developing +on Windows, you will want to setup your own, perhaps using [Conda][]. + +[virtualenv]: https://docs.python-guide.org/dev/virtualenvs/ +[pyenv]: https://github.com/pyenv/pyenv +[Conda]: https://docs.conda.io/en/latest/ + +* python: We pinned Python to version 3.8 so everyone uses the same version. + +* psutil: TODO @squirrelsc will document + +* pyyaml: TODO @squirrelsc will document + +* retry: TODO @squirrelsc will document + +* paramiko: TODO @squirrelsc will document + +* spurplus: TODO @squirrelsc will document + +* dataclasses-json: TODO @squirrelsc will document (brings in `usjon` which + requires `gcc` and `libpython`) + +* portalocker: TODO @squirrelsc will document + +* azure-*: TODO @squirrelsc will document + +### Developer Dependencies + +Similar to the previous section, `tool.poetry.dev-dependencies` is where `poetry +add --dev ` records our _developer_ packages. These are not +necessary for LISAv3 to execute, but are used by developers to automatically +adhere to our coding standards. + +* [Black](https://github.com/psf/black), the opinionated code formatter which + settles all debates as to how our Python files should be formatted. It follows + [PEP 8](https://www.python.org/dev/peps/pep-0008/), the official Python style + guide, and where ambiguous makes the decision for us. + +* [Flake8](https://flake8.pycqa.org/en/latest/) (and integrations), the semantic + analyzer, used to coordinate most of the other tools. + +* [isort](https://timothycrosley.github.io/isort/), the `import` sorter, which + automatically splits imports into the expected, alphabetized sections. + +* [mypy](http://mypy-lang.org/), the static type checker, which coupled with + type annotations allows us to avoid the pitfalls of Python being a dynamically + typed language. + +* [python-language-server](https://github.com/palantir/python-language-server) + (and integrations), the de facto LSP server. While Microsoft is developing + their own LSP servers, they do not integrate with the existing ecosystem of + tools, and their latest tool, Pyright, simply does not support + `pyproject.toml`. Since pyls is used far more widely, and supports every + editor, we use it. + +* [rope](https://github.com/python-rope/rope), to provide completions and + renaming support to pyls. + +With these packages installed and a correctly setup editor (see the readme and +feel free to reach out to us), your code should automatically follow all the +standards which we could automate. + +The final sections, `tool.black`, `tool.isort`, `build-system`, and the +`.flake8` file (Flake8 does not yet support `pyproject.toml`) configure the +tools per their recommendations. + +## Type Annotations + +We are using [mypy][] to enforce static type checking of our Python code. This +may surprise you as Python is not a statically typed language. While dynamic +typing can be useful, for a complex tool such as LISA it is more likely to +introduce bugs that are found only at runtime (which the user experiences as a +crash). For more information on why we (and others) do this, see [Dropbox’s +journey to type checking 4 million lines of Python][dropbox]. [PEP 484][] and +[PEP 526][] (among others) introduced and defined [type hints][] for the Python +language. You can probably figuring out the syntax based on the surrounding +code, but you can also see this [Intro to Using Python Type Hints][intro] and +mypy’s [cheat sheet][]. + +[mypy]: http://mypy-lang.org/ +[dropbox]: https://dropbox.tech/application/our-journey-to-type-checking-4-million-lines-of-python +[PEP 484]: https://www.python.org/dev/peps/pep-0484/ +[PEP 526]: https://www.python.org/dev/peps/pep-0526/ +[type hints]: https://docs.python.org/3/library/typing.html +[intro]: https://kishstats.com/python/2019/01/07/python-type-hinting.html +[cheat sheet]: https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html + +## Runbook schema + +Some plugins like Platform need follow this section to extend runbook schema. Runbook is the configurations of LISA runs. Every LISA run need a runbook. + +The runbook uses [dataclass](https://docs.python.org/3/library/dataclasses.html) to define, [dataclass-json](https://github.com/lidatong/dataclasses-json/) to deserialize, and [marshmallow](https://marshmallow.readthedocs.io/en/3.0/api_reference.html) to validate the schema. + +See more examples in [schema.py](lisa/schema.py), if you need to extend runbook schema. + +## Committing Guidelines + +A best practice when using [Git](https://git-scm.com/book/en/v2) is to create a +series of independent and well-documented commits. Each commit should “do one +thing” and do it correctly. If a mistake is made (you need to fix a bug or +adjust formatting), you should amend it (or use an [interactive +rebase](https://thoughtbot.com/blog/git-interactive-rebase-squash-amend-rewriting-history) +to edit it). If you’re using Emacs, the [Magit](https://magit.vc/) package makes +all of this easy. Some of the reasons for making each commit polished is that it +aids immensely in future debugging. It lets us use tools like [`git +bisect`](https://git-scm.com/docs/git-bisect) to automatically find bugs, and +understand why prior code was written. Although some of it has gone out of date, +see this otherwise great essay on [Git best +practices](http://sethrobertson.github.io/GitBestPractices/). For how Git works, +read [Git from the Bottom +Up](https://jwiegley.github.io/git-from-the-bottom-up/). + +For writing your commit messages, see this modification of [Tim Pope’s +example](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html): + +> Capitalized, short (72 chars or less) summary +> +> More detailed explanatory text, if necessary. Wrap it to about 72 +> characters or so. In some contexts, the first line is treated as the +> subject of an email and the rest of the text as the body. The blank line +> separating the summary from the body is critical (unless you omit the +> body entirely); tools like rebase can get confused if you run the two +> together. +> +> Write your commit message in the imperative: “Fix bug” and not “Fixed +> bug” or “Fixes bug.” This convention matches up with commit messages +> generated by commands like git merge and git revert. +> +> Further paragraphs come after blank lines. +> +> * Bullet points are okay, too +> +> * Typically a hyphen or asterisk is used for the bullet, followed by a +> single space, with blank lines in between, but conventions vary here +> +> * Use a hanging indent + +You should also feel free to use Markdown in the commit messages, as our project +is hosted on GitHub which renders it (and Markdown is human readable). + +## Design Patterns + +The most important goal we are attempting to accomplish with LISAv3 is for it to +be “simple, clean, and with a low maintenance cost.” + +We should use caution when using Object Oriented Design, because when it is used +without critical analysis, it creates unmaintainable code. A great talk on this +subject is [Stop Writing Classes](https://www.youtube.com/watch?v=o9pEzgHorH0), +by Jack Diederich. As he says, “classes are great but they are also overused.” + +This [Python Design Patterns](https://python-patterns.guide/) is a fantastic +collection of material for writing maintainable Python code. It specifically +details many of the common “Object Oriented” patterns from the Gang of Four book +(which, in fact, were patterns geared toward languages like C++, and no longer +apply to modern languages like Python), what lessons can be learned from them, +and how to apply them (or their modern alternatives) today. It also serves as an +easy-to-read guide to the Gang of Four book itself, as its principles still +serve us well today. + +Every time a developer chooses to use a design pattern, that person needs to +reason through and document why it was chosen, and what alternatives were +considered. We will recreate the problems with LISAv2 unless we take our time to +carefully create a well-designed and maintainable framework. + +Several popular patterns that actually _do not_ work well in Python are: + +* [The Abstract Factory Pattern](https://python-patterns.guide/gang-of-four/abstract-factory/) +* [The Factory Method Pattern](https://python-patterns.guide/gang-of-four/factory-method/) +* [The Prototype Pattern](https://python-patterns.guide/gang-of-four/prototype/) +* [The Singleton Pattern](https://python-patterns.guide/gang-of-four/singleton/) + +Conversely, patterns that are a natural fit to Python include: + +* [The Composite Pattern](https://python-patterns.guide/gang-of-four/composite/) +* [The Iterator Pattern](https://python-patterns.guide/gang-of-four/iterator/) + (caution: it is actually better to implement these with `yield`!) + +Finally, a high-level guide to all things Python is [The Hitchhiker’s Guide to +Python](https://docs.python-guide.org/). It covers just about everything in the +Python world. If you make it through even some of these guides, you will be well +on your way to being a “Pythonista” (a Python developer) writing “Pythonic” +(canonically correct Python) code left and right. + +### Async IO + +With Python 3.4, the Async IO pattern found in languages such as C# and Go is +available through the keywords `async` and `await`, along with the Python module +`asyncio`. Please read [Async IO in Python: A Complete +Walkthrough](https://realpython.com/async-io-python/) to understand at a high +level how asynchronous programming works. As of Python 3.7, One major “gotcha” +is that `asyncio.run(...)` should be used [exactly once in +`main`](https://docs.python.org/3/library/asyncio-task.html), it starts the +event loop. Everything else should be a coroutine or task which the event loop +schedules. + +## Future Sections + +Just a collection of reminders for the author to expand on later. + +* [unittest](https://docs.python.org/3/library/unittest.html) +* [doctest](https://docs.python.org/3/library/doctest.html) +* [subprocess](https://pymotw.com/3/subprocess/index.html) +* [GitHub Actions](https://github.com/LIS/LISAv2/actions) +* [ShellCheck](https://www.shellcheck.net/) +* [Governance](https://opensource.guide/leadership-and-governance/) +* [Maintenance Cost](https://web.archive.org/web/20120313070806/http://users.jyu.fi/~koskinen/smcosts.htm) +* Parallelism and multi-plexing +* Versioned inputs and outputs diff --git a/LICENSE-2.0.txt b/.archive/LICENSE-2.0.txt similarity index 100% rename from LICENSE-2.0.txt rename to .archive/LICENSE-2.0.txt diff --git a/Makefile b/.archive/Makefile similarity index 100% rename from Makefile rename to .archive/Makefile diff --git a/.archive/README.md b/.archive/README.md new file mode 100644 index 0000000000..babe1e1ada --- /dev/null +++ b/.archive/README.md @@ -0,0 +1,156 @@ +# Linux Integration Services Automation 3.0 (LISAv3) + +[![CI Workflow for LISAv3](https://github.com/LIS/LISAv2/workflows/CI%20Workflow%20for%20LISAv3/badge.svg?branch=main)](https://github.com/LIS/LISAv2/actions?query=workflow%3A%22CI+Workflow+for+LISAv3%22+event%3Apush+branch%3Amain) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![GitHub license](https://img.shields.io/github/license/LIS/LISAv2)](https://github.com/LIS/LISAv2/blob/main/LICENSE-2.0.txt) + +LISA is a Linux test automation framework with built-in test cases to verify the quality of +Linux distributions on multiple platforms (such as Azure, On-prem HyperV, and Linux bare metal). + +This version (v3) is a brand new implementation in Python 3, supports running on Windows and Linux distros. + +## Getting Started + +### Install Python 3 + +Install [Python 3.8](https://www.python.org/) from Linux distribution’s package +repositories, or [python.org](https://www.python.org/). We also need `gcc` and +the `libpython` headers for the `usjon` package. + +On Ubuntu 18.04 and 20.04: + +```bash +sudo apt install git python3.8 libpython3.8-dev python3-distutils python3-apt gcc +``` + +NOTE: If you are using WSL, installing Poetry on both Windows and Linux may +cause both platforms’ versions of Poetry to be on your path, as Windows binaries +are mapped into WSL’s `PATH`. This means that the Linux `poetry` binary _must_ +appear in your `PATH` before the Windows version, or this error will appear: + +> `/usr/bin/env: ‘python\r’: No such file or directory` + +### Install Poetry + +On Linux (or WSL): + +```bash +curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 - --preview --version 1.1.3 +source $HOME/.poetry/env +``` + +On Windows (in PowerShell): + +```powershell +(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - --preview --version 1.1.3 +# the path can be added to system, so it applies to every terminal. +$env:PATH += ";$env:USERPROFILE\.poetry\bin" +``` + +If you already have Poetry installed, you can update it like: + +```bash +poetry self update 1.1.3 +``` + +### Install Python packages + +Then use Poetry to install LISAv3's Python package dependencies: + +```bash +git clone -b main https://github.com/LIS/LISAv2.git +cd LISAv2 +poetry install +``` + +To obtain the path of the Poetry virtual environment setup for LISA (where the +isolated Python installation and packages are located), run: + +```bash +poetry env list --full-path +``` + +### Run LISAv3 + +Run LISAv3 using Poetry’s environment: + +```bash +poetry run python lisa/main.py +``` + +### Make + +We now also have a GNU Makefile that automates some tasks. Try: +```bash +# Install Python packages +make setup + +# Run LISAv3 +make run + +# Run unit tests +make test + +# Run syntactic, semantic, formatting and type checkers +make check + +# Generate coverage report (slow, reruns LISAv3 and tests) +make coverage + +# Print current Python virtualenv +make venv +``` + +### Editor Setup + +Install and enable [ShellCheck](https://github.com/koalaman/shellcheck) to find +bash errors locally. + +#### Visual Studio Code + +First, click the Python version in the bottom left, then enter the path emitted +by the command above. This will point Code to the Poetry virtual environment. + +Make sure below settings are in root level of `.vscode/settings.json` + +```json +{ + "python.analysis.typeCheckingMode": "strict", + "python.formatting.provider": "black", + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "python.linting.mypyEnabled": true, + "python.linting.pylintEnabled": false, + "editor.formatOnSave": true, + "python.linting.mypyArgs": [ + "--strict", + "--namespace-packages", + "--show-column-numbers", + ], + "python.sortImports.path": "isort", + "python.analysis.useLibraryCodeForTypes": false, + "python.analysis.autoImportCompletions": false, + "files.eol": "\n", +} +``` + +#### Emacs + +Use the [pyvenv](https://github.com/jorgenschaefer/pyvenv) package: + +```emacs-lisp +(use-package pyvenv + :ensure t + :hook (python-mode . pyvenv-tracking-mode)) +``` + +Then run `M-x add-dir-local-variable RET python-mode RET pyvenv-activate RET +` where the value is the path given by the command above. +This will create a `.dir-locals.el` file which looks like this: + +```emacs-lisp +;;; Directory Local Variables +;;; For more information see (info "(emacs) Directory Variables") + +((python-mode . ((pyvenv-activate . "~/.cache/pypoetry/virtualenvs/lisa-s7Q404Ij-py3.8")))) +``` diff --git a/examples/runbook/hello_world.yml b/.archive/examples/runbook/hello_world.yml similarity index 100% rename from examples/runbook/hello_world.yml rename to .archive/examples/runbook/hello_world.yml diff --git a/examples/runbook/hello_world_azure.yml b/.archive/examples/runbook/hello_world_azure.yml similarity index 100% rename from examples/runbook/hello_world_azure.yml rename to .archive/examples/runbook/hello_world_azure.yml diff --git a/examples/runbook/hello_world_remote.yml b/.archive/examples/runbook/hello_world_remote.yml similarity index 100% rename from examples/runbook/hello_world_remote.yml rename to .archive/examples/runbook/hello_world_remote.yml diff --git a/examples/testsuites/__init__.py b/.archive/examples/testsuites/__init__.py similarity index 100% rename from examples/testsuites/__init__.py rename to .archive/examples/testsuites/__init__.py diff --git a/examples/testsuites/helloworld.py b/.archive/examples/testsuites/helloworld.py similarity index 100% rename from examples/testsuites/helloworld.py rename to .archive/examples/testsuites/helloworld.py diff --git a/examples/testsuites/multinodes.py b/.archive/examples/testsuites/multinodes.py similarity index 100% rename from examples/testsuites/multinodes.py rename to .archive/examples/testsuites/multinodes.py diff --git a/examples/testsuites/scripts/echo.sh b/.archive/examples/testsuites/scripts/echo.sh similarity index 100% rename from examples/testsuites/scripts/echo.sh rename to .archive/examples/testsuites/scripts/echo.sh diff --git a/examples/testsuites/withscript.py b/.archive/examples/testsuites/withscript.py similarity index 100% rename from examples/testsuites/withscript.py rename to .archive/examples/testsuites/withscript.py diff --git a/lisa/__init__.py b/.archive/lisa/__init__.py similarity index 100% rename from lisa/__init__.py rename to .archive/lisa/__init__.py diff --git a/lisa/action.py b/.archive/lisa/action.py similarity index 100% rename from lisa/action.py rename to .archive/lisa/action.py diff --git a/lisa/commands.py b/.archive/lisa/commands.py similarity index 100% rename from lisa/commands.py rename to .archive/lisa/commands.py diff --git a/lisa/environment.py b/.archive/lisa/environment.py similarity index 100% rename from lisa/environment.py rename to .archive/lisa/environment.py diff --git a/lisa/executable.py b/.archive/lisa/executable.py similarity index 100% rename from lisa/executable.py rename to .archive/lisa/executable.py diff --git a/lisa/feature.py b/.archive/lisa/feature.py similarity index 100% rename from lisa/feature.py rename to .archive/lisa/feature.py diff --git a/lisa/features/__init__.py b/.archive/lisa/features/__init__.py similarity index 100% rename from lisa/features/__init__.py rename to .archive/lisa/features/__init__.py diff --git a/lisa/features/serial_console.py b/.archive/lisa/features/serial_console.py similarity index 100% rename from lisa/features/serial_console.py rename to .archive/lisa/features/serial_console.py diff --git a/lisa/features/startstop.py b/.archive/lisa/features/startstop.py similarity index 100% rename from lisa/features/startstop.py rename to .archive/lisa/features/startstop.py diff --git a/lisa/main.py b/.archive/lisa/main.py similarity index 100% rename from lisa/main.py rename to .archive/lisa/main.py diff --git a/lisa/node.py b/.archive/lisa/node.py similarity index 100% rename from lisa/node.py rename to .archive/lisa/node.py diff --git a/lisa/notifier.py b/.archive/lisa/notifier.py similarity index 100% rename from lisa/notifier.py rename to .archive/lisa/notifier.py diff --git a/lisa/notifiers/__init__.py b/.archive/lisa/notifiers/__init__.py similarity index 100% rename from lisa/notifiers/__init__.py rename to .archive/lisa/notifiers/__init__.py diff --git a/lisa/notifiers/console.py b/.archive/lisa/notifiers/console.py similarity index 100% rename from lisa/notifiers/console.py rename to .archive/lisa/notifiers/console.py diff --git a/lisa/operating_system.py b/.archive/lisa/operating_system.py similarity index 100% rename from lisa/operating_system.py rename to .archive/lisa/operating_system.py diff --git a/lisa/parameter_parser/__init__.py b/.archive/lisa/parameter_parser/__init__.py similarity index 100% rename from lisa/parameter_parser/__init__.py rename to .archive/lisa/parameter_parser/__init__.py diff --git a/lisa/parameter_parser/argparser.py b/.archive/lisa/parameter_parser/argparser.py similarity index 100% rename from lisa/parameter_parser/argparser.py rename to .archive/lisa/parameter_parser/argparser.py diff --git a/lisa/parameter_parser/runbook.py b/.archive/lisa/parameter_parser/runbook.py similarity index 100% rename from lisa/parameter_parser/runbook.py rename to .archive/lisa/parameter_parser/runbook.py diff --git a/lisa/platform_.py b/.archive/lisa/platform_.py similarity index 100% rename from lisa/platform_.py rename to .archive/lisa/platform_.py diff --git a/lisa/runner.py b/.archive/lisa/runner.py similarity index 100% rename from lisa/runner.py rename to .archive/lisa/runner.py diff --git a/lisa/schema.py b/.archive/lisa/schema.py similarity index 100% rename from lisa/schema.py rename to .archive/lisa/schema.py diff --git a/lisa/search_space.py b/.archive/lisa/search_space.py similarity index 100% rename from lisa/search_space.py rename to .archive/lisa/search_space.py diff --git a/lisa/secret.py b/.archive/lisa/secret.py similarity index 100% rename from lisa/secret.py rename to .archive/lisa/secret.py diff --git a/lisa/sut_orchestrator/__init__.py b/.archive/lisa/sut_orchestrator/__init__.py similarity index 100% rename from lisa/sut_orchestrator/__init__.py rename to .archive/lisa/sut_orchestrator/__init__.py diff --git a/lisa/sut_orchestrator/azure/__init__.py b/.archive/lisa/sut_orchestrator/azure/__init__.py similarity index 100% rename from lisa/sut_orchestrator/azure/__init__.py rename to .archive/lisa/sut_orchestrator/azure/__init__.py diff --git a/lisa/sut_orchestrator/azure/arm_template.json b/.archive/lisa/sut_orchestrator/azure/arm_template.json similarity index 100% rename from lisa/sut_orchestrator/azure/arm_template.json rename to .archive/lisa/sut_orchestrator/azure/arm_template.json diff --git a/lisa/sut_orchestrator/azure/common.py b/.archive/lisa/sut_orchestrator/azure/common.py similarity index 100% rename from lisa/sut_orchestrator/azure/common.py rename to .archive/lisa/sut_orchestrator/azure/common.py diff --git a/lisa/sut_orchestrator/azure/cred_wrapper.py b/.archive/lisa/sut_orchestrator/azure/cred_wrapper.py similarity index 100% rename from lisa/sut_orchestrator/azure/cred_wrapper.py rename to .archive/lisa/sut_orchestrator/azure/cred_wrapper.py diff --git a/lisa/sut_orchestrator/azure/features.py b/.archive/lisa/sut_orchestrator/azure/features.py similarity index 100% rename from lisa/sut_orchestrator/azure/features.py rename to .archive/lisa/sut_orchestrator/azure/features.py diff --git a/lisa/sut_orchestrator/azure/platform_.py b/.archive/lisa/sut_orchestrator/azure/platform_.py similarity index 100% rename from lisa/sut_orchestrator/azure/platform_.py rename to .archive/lisa/sut_orchestrator/azure/platform_.py diff --git a/lisa/sut_orchestrator/azure/tests/__init__.py b/.archive/lisa/sut_orchestrator/azure/tests/__init__.py similarity index 100% rename from lisa/sut_orchestrator/azure/tests/__init__.py rename to .archive/lisa/sut_orchestrator/azure/tests/__init__.py diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_australiaeast.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_australiaeast.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_australiaeast.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_australiaeast.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_brazilsouth.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_brazilsouth.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_brazilsouth.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_brazilsouth.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_eastus.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_eastus.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_eastus.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_eastus.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_eastus2.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_eastus2.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_eastus2.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_eastus2.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_northeurope.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_northeurope.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_northeurope.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_northeurope.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_notreal.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_notreal.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_notreal.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_notreal.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_southcentralus.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_southcentralus.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_southcentralus.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_southcentralus.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_southeastasia.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_southeastasia.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_southeastasia.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_southeastasia.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_uksouth.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_uksouth.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_uksouth.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_uksouth.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_westeurope.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_westeurope.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_westeurope.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_westeurope.json diff --git a/lisa/sut_orchestrator/azure/tests/azure_locations_westus2.json b/.archive/lisa/sut_orchestrator/azure/tests/azure_locations_westus2.json similarity index 100% rename from lisa/sut_orchestrator/azure/tests/azure_locations_westus2.json rename to .archive/lisa/sut_orchestrator/azure/tests/azure_locations_westus2.json diff --git a/lisa/sut_orchestrator/azure/tests/test_prepare.py b/.archive/lisa/sut_orchestrator/azure/tests/test_prepare.py similarity index 100% rename from lisa/sut_orchestrator/azure/tests/test_prepare.py rename to .archive/lisa/sut_orchestrator/azure/tests/test_prepare.py diff --git a/lisa/sut_orchestrator/azure/tools.py b/.archive/lisa/sut_orchestrator/azure/tools.py similarity index 100% rename from lisa/sut_orchestrator/azure/tools.py rename to .archive/lisa/sut_orchestrator/azure/tools.py diff --git a/lisa/sut_orchestrator/ready.py b/.archive/lisa/sut_orchestrator/ready.py similarity index 100% rename from lisa/sut_orchestrator/ready.py rename to .archive/lisa/sut_orchestrator/ready.py diff --git a/lisa/tests/__init__.py b/.archive/lisa/tests/__init__.py similarity index 100% rename from lisa/tests/__init__.py rename to .archive/lisa/tests/__init__.py diff --git a/lisa/tests/test_env_requirement.py b/.archive/lisa/tests/test_env_requirement.py similarity index 100% rename from lisa/tests/test_env_requirement.py rename to .archive/lisa/tests/test_env_requirement.py diff --git a/lisa/tests/test_environment.py b/.archive/lisa/tests/test_environment.py similarity index 100% rename from lisa/tests/test_environment.py rename to .archive/lisa/tests/test_environment.py diff --git a/lisa/tests/test_platform.py b/.archive/lisa/tests/test_platform.py similarity index 100% rename from lisa/tests/test_platform.py rename to .archive/lisa/tests/test_platform.py diff --git a/lisa/tests/test_runner.py b/.archive/lisa/tests/test_runner.py similarity index 100% rename from lisa/tests/test_runner.py rename to .archive/lisa/tests/test_runner.py diff --git a/lisa/tests/test_search_space.py b/.archive/lisa/tests/test_search_space.py similarity index 100% rename from lisa/tests/test_search_space.py rename to .archive/lisa/tests/test_search_space.py diff --git a/lisa/tests/test_secret.py b/.archive/lisa/tests/test_secret.py similarity index 100% rename from lisa/tests/test_secret.py rename to .archive/lisa/tests/test_secret.py diff --git a/lisa/tests/test_testselector.py b/.archive/lisa/tests/test_testselector.py similarity index 100% rename from lisa/tests/test_testselector.py rename to .archive/lisa/tests/test_testselector.py diff --git a/lisa/tests/test_testsuite.py b/.archive/lisa/tests/test_testsuite.py similarity index 100% rename from lisa/tests/test_testsuite.py rename to .archive/lisa/tests/test_testsuite.py diff --git a/lisa/tests/test_variable.py b/.archive/lisa/tests/test_variable.py similarity index 100% rename from lisa/tests/test_variable.py rename to .archive/lisa/tests/test_variable.py diff --git a/lisa/tests/variable_normal.yml b/.archive/lisa/tests/variable_normal.yml similarity index 100% rename from lisa/tests/variable_normal.yml rename to .archive/lisa/tests/variable_normal.yml diff --git a/lisa/tests/variable_secret.yml b/.archive/lisa/tests/variable_secret.yml similarity index 100% rename from lisa/tests/variable_secret.yml rename to .archive/lisa/tests/variable_secret.yml diff --git a/lisa/testselector.py b/.archive/lisa/testselector.py similarity index 100% rename from lisa/testselector.py rename to .archive/lisa/testselector.py diff --git a/lisa/testsuite.py b/.archive/lisa/testsuite.py similarity index 100% rename from lisa/testsuite.py rename to .archive/lisa/testsuite.py diff --git a/lisa/tools/__init__.py b/.archive/lisa/tools/__init__.py similarity index 100% rename from lisa/tools/__init__.py rename to .archive/lisa/tools/__init__.py diff --git a/lisa/tools/cat.py b/.archive/lisa/tools/cat.py similarity index 100% rename from lisa/tools/cat.py rename to .archive/lisa/tools/cat.py diff --git a/lisa/tools/date.py b/.archive/lisa/tools/date.py similarity index 100% rename from lisa/tools/date.py rename to .archive/lisa/tools/date.py diff --git a/lisa/tools/dmesg.py b/.archive/lisa/tools/dmesg.py similarity index 100% rename from lisa/tools/dmesg.py rename to .archive/lisa/tools/dmesg.py diff --git a/lisa/tools/echo.py b/.archive/lisa/tools/echo.py similarity index 100% rename from lisa/tools/echo.py rename to .archive/lisa/tools/echo.py diff --git a/lisa/tools/gcc.py b/.archive/lisa/tools/gcc.py similarity index 100% rename from lisa/tools/gcc.py rename to .archive/lisa/tools/gcc.py diff --git a/lisa/tools/git.py b/.archive/lisa/tools/git.py similarity index 100% rename from lisa/tools/git.py rename to .archive/lisa/tools/git.py diff --git a/lisa/tools/lscpu.py b/.archive/lisa/tools/lscpu.py similarity index 100% rename from lisa/tools/lscpu.py rename to .archive/lisa/tools/lscpu.py diff --git a/lisa/tools/make.py b/.archive/lisa/tools/make.py similarity index 100% rename from lisa/tools/make.py rename to .archive/lisa/tools/make.py diff --git a/lisa/tools/modinfo.py b/.archive/lisa/tools/modinfo.py similarity index 100% rename from lisa/tools/modinfo.py rename to .archive/lisa/tools/modinfo.py diff --git a/lisa/tools/ntttcp.py b/.archive/lisa/tools/ntttcp.py similarity index 100% rename from lisa/tools/ntttcp.py rename to .archive/lisa/tools/ntttcp.py diff --git a/lisa/tools/reboot.py b/.archive/lisa/tools/reboot.py similarity index 100% rename from lisa/tools/reboot.py rename to .archive/lisa/tools/reboot.py diff --git a/lisa/tools/uname.py b/.archive/lisa/tools/uname.py similarity index 100% rename from lisa/tools/uname.py rename to .archive/lisa/tools/uname.py diff --git a/lisa/tools/who.py b/.archive/lisa/tools/who.py similarity index 100% rename from lisa/tools/who.py rename to .archive/lisa/tools/who.py diff --git a/lisa/util/__init__.py b/.archive/lisa/util/__init__.py similarity index 100% rename from lisa/util/__init__.py rename to .archive/lisa/util/__init__.py diff --git a/lisa/util/constants.py b/.archive/lisa/util/constants.py similarity index 100% rename from lisa/util/constants.py rename to .archive/lisa/util/constants.py diff --git a/lisa/util/logger.py b/.archive/lisa/util/logger.py similarity index 100% rename from lisa/util/logger.py rename to .archive/lisa/util/logger.py diff --git a/lisa/util/module.py b/.archive/lisa/util/module.py similarity index 100% rename from lisa/util/module.py rename to .archive/lisa/util/module.py diff --git a/lisa/util/perf_timer.py b/.archive/lisa/util/perf_timer.py similarity index 100% rename from lisa/util/perf_timer.py rename to .archive/lisa/util/perf_timer.py diff --git a/lisa/util/process.py b/.archive/lisa/util/process.py similarity index 100% rename from lisa/util/process.py rename to .archive/lisa/util/process.py diff --git a/lisa/util/shell.py b/.archive/lisa/util/shell.py similarity index 100% rename from lisa/util/shell.py rename to .archive/lisa/util/shell.py diff --git a/lisa/util/subclasses.py b/.archive/lisa/util/subclasses.py similarity index 100% rename from lisa/util/subclasses.py rename to .archive/lisa/util/subclasses.py diff --git a/lisa/variable.py b/.archive/lisa/variable.py similarity index 100% rename from lisa/variable.py rename to .archive/lisa/variable.py diff --git a/.archive/poetry.lock b/.archive/poetry.lock new file mode 100644 index 0000000000..5930f5aa3d --- /dev/null +++ b/.archive/poetry.lock @@ -0,0 +1,1515 @@ +[[package]] +name = "adal" +version = "1.2.5" +description = "Note: This library is already replaced by MSAL Python, available here: https://pypi.org/project/msal/ .ADAL Python remains available here as a legacy. The ADAL for Python library makes it easy for python application to authenticate to Azure Active Directory (AAD) in order to access AAD protected web resources." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cryptography = ">=1.1.0" +PyJWT = ">=1.0.0" +python-dateutil = ">=2.1.0" +requests = ">=2.0.0" + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "asserts" +version = "0.11.1" +description = "Stand-alone Assertions" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "asttokens" +version = "2.0.4" +description = "Annotate AST trees with source code positions" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + +[[package]] +name = "attrs" +version = "20.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "azure-common" +version = "1.1.25" +description = "Microsoft Azure Client Library for Python (Common)" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "azure-core" +version = "1.8.2" +description = "Microsoft Azure Core Library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.18.4" +six = ">=1.6" + +[[package]] +name = "azure-identity" +version = "1.5.0b2" +description = "Microsoft Azure Identity Library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +azure-core = ">=1.0.0,<2.0.0" +cryptography = ">=2.1.4" +msal = ">=1.3.0,<1.6.0" +msal-extensions = ">=0.3.0,<0.4.0" +six = ">=1.6" + +[[package]] +name = "azure-mgmt-compute" +version = "17.0.0" +description = "Microsoft Azure Compute Management Client Library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +azure-common = ">=1.1,<2.0" +azure-mgmt-core = ">=1.2.0,<2.0.0" +msrest = ">=0.5.0" + +[[package]] +name = "azure-mgmt-core" +version = "1.2.1" +description = "Microsoft Azure Management Core Library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +azure-core = ">=1.8.2a,<2.0.0" + +[[package]] +name = "azure-mgmt-marketplaceordering" +version = "0.2.1" +description = "Microsoft Azure Market Place Ordering Client Library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +azure-common = ">=1.1,<2.0" +msrest = ">=0.5.0" +msrestazure = ">=0.4.32,<2.0.0" + +[[package]] +name = "azure-mgmt-network" +version = "16.0.0" +description = "Microsoft Azure Network Management Client Library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +azure-common = ">=1.1,<2.0" +azure-mgmt-core = ">=1.2.0,<2.0.0" +msrest = ">=0.5.0" + +[[package]] +name = "azure-mgmt-resource" +version = "15.0.0" +description = "Microsoft Azure Resource Management Client Library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +azure-common = ">=1.1,<2.0" +azure-mgmt-core = ">=1.2.0,<2.0.0" +msrest = ">=0.5.0" + +[[package]] +name = "azure-mgmt-storage" +version = "16.0.0" +description = "Microsoft Azure Storage Management Client Library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +azure-common = ">=1.1,<2.0" +azure-mgmt-core = ">=1.2.0,<2.0.0" +msrest = ">=0.5.0" + +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +name = "certifi" +version = "2020.6.20" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.14.3" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "chardet" +version = "3.0.4" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "5.3" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "cryptography" +version = "3.2.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.dependencies] +cffi = ">=1.8,<1.11.3 || >1.11.3" +six = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "dataclasses-json" +version = "0.5.2" +description = "Easily serialize dataclasses to and from JSON" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +marshmallow = ">=3.3.0,<4.0.0" +marshmallow-enum = ">=1.5.1,<2.0.0" +stringcase = "1.2.0" +typing-inspect = ">=0.4.0" + +[package.extras] +dev = ["pytest", "ipython", "mypy (>=0.710)", "hypothesis", "portray", "flake8", "simplejson"] + +[[package]] +name = "decorator" +version = "4.4.2" +description = "Decorators for Humans" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" + +[[package]] +name = "flake8" +version = "3.8.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[[package]] +name = "flake8-black" +version = "0.2.1" +description = "flake8 plugin to call black as a code style validator" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +black = "*" +flake8 = ">=3.0.0" + +[[package]] +name = "flake8-bugbear" +version = "20.1.4" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[[package]] +name = "flake8-isort" +version = "4.0.0" +description = "flake8 plugin that integrates isort ." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3.2.1,<4" +isort = ">=4.3.5,<6" +testfixtures = ">=6.8.0,<7" + +[package.extras] +test = ["pytest (>=4.0.2,<6)", "toml"] + +[[package]] +name = "icontract" +version = "2.3.6" +description = "Provide design-by-contract with informative violation messages." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +asttokens = ">=2,<3" + +[package.extras] +dev = ["mypy (==0.750)", "pylint (==2.3.1)", "yapf (==0.20.2)", "tox (>=3.0.0)", "pydocstyle (>=2.1.1,<3)", "coverage (>=4.5.1,<5)", "docutils (>=0.14,<1)", "pygments (>=2.2.0,<3)", "dpcontracts (==0.6.0)", "tabulate (>=0.8.7,<1)", "py-cpuinfo (>=5.0.0,<6)", "deal (==4.1.0)"] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "isodate" +version = "0.6.0" +description = "An ISO 8601 date/time/duration parser and formatter" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[[package]] +name = "isort" +version = "5.6.4" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] + +[[package]] +name = "jedi" +version = "0.17.2" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +parso = ">=0.7.0,<0.8.0" + +[package.extras] +qa = ["flake8 (==3.7.9)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] + +[[package]] +name = "marshmallow" +version = "3.9.0" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["pytest", "pytz", "simplejson", "mypy (==0.790)", "flake8 (==3.8.4)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] +docs = ["sphinx (==3.2.1)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.1)"] +lint = ["mypy (==0.790)", "flake8 (==3.8.4)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)"] +tests = ["pytest", "pytz", "simplejson"] + +[[package]] +name = "marshmallow-enum" +version = "1.5.1" +description = "Enum field for Marshmallow" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +marshmallow = ">=2.0.0" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "msal" +version = "1.5.1" +description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +PyJWT = {version = ">=1.0.0,<2", extras = ["crypto"]} +requests = ">=2.0.0,<3" + +[[package]] +name = "msal-extensions" +version = "0.3.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +msal = ">=0.4.1,<2.0.0" +portalocker = [ + {version = ">=1.0,<2.0", markers = "platform_system != \"Windows\""}, + {version = ">=1.6,<2.0", markers = "platform_system == \"Windows\""}, +] + +[[package]] +name = "msrest" +version = "0.6.19" +description = "AutoRest swagger generator Python client runtime." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +certifi = ">=2017.4.17" +isodate = ">=0.6.0" +requests = ">=2.16,<3.0" +requests-oauthlib = ">=0.5.0" + +[package.extras] +async = ["aiohttp (>=3.0)", "aiodns"] + +[[package]] +name = "msrestazure" +version = "0.6.4" +description = "AutoRest swagger generator Python client runtime. Azure-specific module." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +adal = ">=0.6.0,<2.0.0" +msrest = ">=0.6.0,<2.0.0" +six = "*" + +[[package]] +name = "mypy" +version = "0.782" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "oauthlib" +version = "3.1.0" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +rsa = ["cryptography"] +signals = ["blinker"] +signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] + +[[package]] +name = "paramiko" +version = "2.7.2" +description = "SSH2 protocol library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" + +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + +[[package]] +name = "parso" +version = "0.7.1" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +testing = ["docopt", "pytest (>=3.0.7)"] + +[[package]] +name = "pathspec" +version = "0.8.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "portalocker" +version = "1.7.1" +description = "Wraps the portalocker recipe for easy usage" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pywin32 = {version = "!=226", markers = "platform_system == \"Windows\""} + +[package.extras] +docs = ["sphinx (>=1.7.1)"] +tests = ["pytest (>=4.6.9)", "pytest-cov (>=2.8.1)", "sphinx (>=1.8.5)", "pytest-flake8 (>=1.0.5)"] + +[[package]] +name = "psutil" +version = "5.7.3" +description = "Cross-platform lib for process and system monitoring in Python." +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] + +[[package]] +name = "py" +version = "1.9.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycodestyle" +version = "2.6.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyflakes" +version = "2.2.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyjwt" +version = "1.7.1" +description = "JSON Web Token implementation in Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cryptography = {version = ">=1.4", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=1.4)"] +flake8 = ["flake8", "flake8-import-order", "pep8-naming"] +test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"] + +[[package]] +name = "pyls-black" +version = "0.4.6" +description = "Black plugin for the Python Language Server" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +black = ">=19.3b0" +python-language-server = "*" +toml = "*" + +[package.extras] +dev = ["isort (>=5.0)", "flake8", "pytest", "mypy"] + +[[package]] +name = "pyls-isort" +version = "0.2.0" +description = "Isort plugin for python-language-server" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +isort = "*" +python-language-server = "*" + +[[package]] +name = "pyls-mypy" +version = "0.1.8" +description = "Mypy linter for the Python Language Server" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +mypy = "*" +python-language-server = "*" + +[package.extras] +test = ["tox", "versioneer", "pytest", "pytest-cov", "coverage"] + +[[package]] +name = "pynacl" +version = "1.4.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +cffi = ">=1.4.1" +six = "*" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] + +[[package]] +name = "python-dateutil" +version = "2.8.1" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-jsonrpc-server" +version = "0.4.0" +description = "JSON RPC 2.0 server library" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ujson = ">=3.0.0" + +[package.extras] +test = ["versioneer", "pylint", "pycodestyle", "pyflakes", "pytest", "mock", "pytest-cov", "coverage"] + +[[package]] +name = "python-language-server" +version = "0.35.1" +description = "Python Language Server for the Language Server Protocol" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +jedi = ">=0.17.0,<0.18.0" +pluggy = "*" +python-jsonrpc-server = ">=0.4.0" + +[package.extras] +all = ["autopep8", "flake8 (>=3.8.0)", "mccabe (>=0.6.0,<0.7.0)", "pycodestyle (>=2.6.0,<2.7.0)", "pydocstyle (>=2.0.0)", "pyflakes (>=2.2.0,<2.3.0)", "pylint (>=2.5.0)", "rope (>=0.10.5)", "yapf"] +autopep8 = ["autopep8"] +flake8 = ["flake8 (>=3.8.0)"] +mccabe = ["mccabe (>=0.6.0,<0.7.0)"] +pycodestyle = ["pycodestyle (>=2.6.0,<2.7.0)"] +pydocstyle = ["pydocstyle (>=2.0.0)"] +pyflakes = ["pyflakes (>=2.2.0,<2.3.0)"] +pylint = ["pylint (>=2.5.0)"] +rope = ["rope (>0.10.5)"] +test = ["versioneer", "pylint (>=2.5.0)", "pytest", "mock", "pytest-cov", "coverage", "numpy", "pandas", "matplotlib", "flaky", "pyqt5"] +yapf = ["yapf"] + +[[package]] +name = "pywin32" +version = "228" +description = "Python for Window Extensions" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "regex" +version = "2020.10.28" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.24.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "requests-oauthlib" +version = "1.3.0" +description = "OAuthlib authentication support for Requests." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "retry" +version = "0.9.2" +description = "Easy to use retry decorator." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +decorator = ">=3.4.2" +py = ">=1.4.26,<2.0.0" + +[[package]] +name = "rope" +version = "0.17.0" +description = "a python refactoring library..." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +dev = ["pytest"] + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "spur" +version = "0.3.20" +description = "Run commands and manipulate files locally or over SSH using the same interface" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +paramiko = ">=1.13.1,<3" + +[[package]] +name = "spurplus" +version = "2.3.4" +description = "Manage remote machines and file operations over SSH." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +icontract = ">=2.0.1,<3" +spur = "0.3.20" +temppathlib = ">=1.0.3,<2" +typing_extensions = ">=3.6.2.1" + +[package.extras] +dev = ["mypy (==0.790)", "pylint (==2.6.0)", "yapf (==0.20.2)", "tox (>=3.0.0)", "coverage (>=4.5.1,<5)", "pydocstyle (>=2.1.1,<3)"] + +[[package]] +name = "stringcase" +version = "1.2.0" +description = "String case converter." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "temppathlib" +version = "1.0.4" +description = "Wrap tempfile to give you pathlib.Path." +category = "main" +optional = false +python-versions = "*" + +[package.extras] +dev = ["mypy (==0.790)", "pylint (==2.6.0)", "yapf (==0.20.2)", "tox (>=3,<4)", "coverage (>=5,<6)", "pydocstyle (>=5,<6)", "twine"] + +[[package]] +name = "testfixtures" +version = "6.15.0" +description = "A collection of helpers and mock objects for unit tests and doc tests." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +build = ["setuptools-git", "wheel", "twine"] +docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] +test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typed-ast" +version = "1.4.1" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "typing-inspect" +version = "0.6.0" +description = "Runtime inspection utilities for typing module." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "ujson" +version = "4.0.1" +description = "Ultra fast JSON encoder and decoder for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.25.11" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "a44f707696fdb38afd23be8723eccd02e6aa375323aafaf2a995b586901a2ac8" + +[metadata.files] +adal = [ + {file = "adal-1.2.5-py2.py3-none-any.whl", hash = "sha256:7492aff8f0ba7dd4e1c477303295c645141540fff34c3ca6de0a0b0e6c1c122a"}, + {file = "adal-1.2.5.tar.gz", hash = "sha256:8003ba03ef04170195b3eddda8a5ab43649ef2c5f0287023d515affb1ccfcfc3"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +asserts = [ + {file = "asserts-0.11.1-py2.py3-none-any.whl", hash = "sha256:dfe3a45a311c727f516e9375eac85e5f5dd1df846b60ae52f559d8534b24df5d"}, +] +asttokens = [ + {file = "asttokens-2.0.4-py2.py3-none-any.whl", hash = "sha256:766d3352908730efb20b95ae22db0f1cb1bedb67c6071fcffb5c236ea673f2f7"}, + {file = "asttokens-2.0.4.tar.gz", hash = "sha256:a42e57e28f2ac1c85ed9b1f84109401427e5c63c04f61d15b8842b027eec5128"}, +] +attrs = [ + {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, + {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, +] +azure-common = [ + {file = "azure-common-1.1.25.zip", hash = "sha256:ce0f1013e6d0e9faebaf3188cc069f4892fc60a6ec552e3f817c1a2f92835054"}, + {file = "azure_common-1.1.25-py2.py3-none-any.whl", hash = "sha256:fd02e4256dc9cdd2d4422bc795bdca2ef302f7a86148b154fbf4ea1f09da400a"}, +] +azure-core = [ + {file = "azure-core-1.8.2.zip", hash = "sha256:621b53271f7988b766f8a7d7f7a2c44241e3d2c1d8db13e68089d6da6241748e"}, + {file = "azure_core-1.8.2-py2.py3-none-any.whl", hash = "sha256:be23d411e19874f375c2ef0327c452be10b1e9a1023ac6afe334598f2920136b"}, +] +azure-identity = [ + {file = "azure-identity-1.5.0b2.zip", hash = "sha256:095873d61c6a4474831794b6232f156c255ff883cdd14195dc3d2c5f823559ae"}, + {file = "azure_identity-1.5.0b2-py2.py3-none-any.whl", hash = "sha256:ea415bf5b76ea160f311d8a89c0b97200c48d74046ec4e393883351dbbf01056"}, +] +azure-mgmt-compute = [ + {file = "azure-mgmt-compute-17.0.0.zip", hash = "sha256:c7350b404e5d10a548ceddb034394c8fad6c852ce33a3d3b211065813c1da404"}, + {file = "azure_mgmt_compute-17.0.0-py2.py3-none-any.whl", hash = "sha256:d84e048bb56448d94e6827213321c08a006c39e58c698aee6e4e83961d50680a"}, +] +azure-mgmt-core = [ + {file = "azure-mgmt-core-1.2.1.zip", hash = "sha256:a3906fa77edfedfcc3229dc3b69489d5ed63b107c7eacbc50092e6cbfbfd83f0"}, + {file = "azure_mgmt_core-1.2.1-py2.py3-none-any.whl", hash = "sha256:bd4503a2d81b86780f15936af2e4244c1345062f4c2422f0b377b56cb80d7796"}, +] +azure-mgmt-marketplaceordering = [ + {file = "azure-mgmt-marketplaceordering-0.2.1.zip", hash = "sha256:dc765cde7ec03efe456438c85c6207c2f77775a8ce8a7adb19b0df5c5dc513c2"}, + {file = "azure_mgmt_marketplaceordering-0.2.1-py2.py3-none-any.whl", hash = "sha256:12d595f3dbda90de7cbc08ace99b925124ce675219b32bb3fde90e36d357c095"}, +] +azure-mgmt-network = [ + {file = "azure-mgmt-network-16.0.0.zip", hash = "sha256:6159a8c44590cc58841690c27c7d4acb0cd9ad0a1e5178c1d35e0f48e3c3c0e9"}, + {file = "azure_mgmt_network-16.0.0-py2.py3-none-any.whl", hash = "sha256:c0e8358e9d530790dbf3efef6b31bce26e664de5096cbd84c62845067da815d1"}, +] +azure-mgmt-resource = [ + {file = "azure-mgmt-resource-15.0.0.zip", hash = "sha256:80ecb69aa21152b924edf481e4b26c641f11aa264120bc322a14284811df9c14"}, + {file = "azure_mgmt_resource-15.0.0-py2.py3-none-any.whl", hash = "sha256:15bb46ef4b197426ce7ce8080a490c997111f3f3efe9f728cf8dc420982d54a2"}, +] +azure-mgmt-storage = [ + {file = "azure-mgmt-storage-16.0.0.zip", hash = "sha256:2f9d714d9722b1ef4bac6563676612e6e795c4e90f6f3cd323616fdadb0a99e5"}, + {file = "azure_mgmt_storage-16.0.0-py2.py3-none-any.whl", hash = "sha256:a819e421d50c0b58416b551d3e9e9a9cf6029714cf977ffaaee86a37572e7113"}, +] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] +black = [ + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +cffi = [ + {file = "cffi-1.14.3-2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc"}, + {file = "cffi-1.14.3-2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768"}, + {file = "cffi-1.14.3-2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d"}, + {file = "cffi-1.14.3-2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1"}, + {file = "cffi-1.14.3-2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca"}, + {file = "cffi-1.14.3-2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a"}, + {file = "cffi-1.14.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c"}, + {file = "cffi-1.14.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730"}, + {file = "cffi-1.14.3-cp27-cp27m-win32.whl", hash = "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d"}, + {file = "cffi-1.14.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05"}, + {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b"}, + {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171"}, + {file = "cffi-1.14.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f"}, + {file = "cffi-1.14.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4"}, + {file = "cffi-1.14.3-cp35-cp35m-win32.whl", hash = "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d"}, + {file = "cffi-1.14.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808"}, + {file = "cffi-1.14.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537"}, + {file = "cffi-1.14.3-cp36-cp36m-win32.whl", hash = "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0"}, + {file = "cffi-1.14.3-cp36-cp36m-win_amd64.whl", hash = "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579"}, + {file = "cffi-1.14.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394"}, + {file = "cffi-1.14.3-cp37-cp37m-win32.whl", hash = "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc"}, + {file = "cffi-1.14.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828"}, + {file = "cffi-1.14.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9"}, + {file = "cffi-1.14.3-cp38-cp38-win32.whl", hash = "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522"}, + {file = "cffi-1.14.3-cp38-cp38-win_amd64.whl", hash = "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15"}, + {file = "cffi-1.14.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d"}, + {file = "cffi-1.14.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c"}, + {file = "cffi-1.14.3-cp39-cp39-win32.whl", hash = "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b"}, + {file = "cffi-1.14.3-cp39-cp39-win_amd64.whl", hash = "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3"}, + {file = "cffi-1.14.3.tar.gz", hash = "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +coverage = [ + {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"}, + {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"}, + {file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"}, + {file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"}, + {file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"}, + {file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"}, + {file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"}, + {file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"}, + {file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"}, + {file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"}, + {file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"}, + {file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"}, + {file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"}, + {file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"}, + {file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"}, + {file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"}, + {file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"}, + {file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"}, + {file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"}, + {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"}, +] +cryptography = [ + {file = "cryptography-3.2.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7"}, + {file = "cryptography-3.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d"}, + {file = "cryptography-3.2.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33"}, + {file = "cryptography-3.2.1-cp27-cp27m-win32.whl", hash = "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297"}, + {file = "cryptography-3.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4"}, + {file = "cryptography-3.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8"}, + {file = "cryptography-3.2.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e"}, + {file = "cryptography-3.2.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b"}, + {file = "cryptography-3.2.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df"}, + {file = "cryptography-3.2.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77"}, + {file = "cryptography-3.2.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7"}, + {file = "cryptography-3.2.1-cp35-cp35m-win32.whl", hash = "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b"}, + {file = "cryptography-3.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7"}, + {file = "cryptography-3.2.1-cp36-abi3-win32.whl", hash = "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f"}, + {file = "cryptography-3.2.1-cp36-abi3-win_amd64.whl", hash = "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538"}, + {file = "cryptography-3.2.1-cp36-cp36m-win32.whl", hash = "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b"}, + {file = "cryptography-3.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13"}, + {file = "cryptography-3.2.1-cp37-cp37m-win32.whl", hash = "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e"}, + {file = "cryptography-3.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb"}, + {file = "cryptography-3.2.1-cp38-cp38-win32.whl", hash = "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b"}, + {file = "cryptography-3.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851"}, + {file = "cryptography-3.2.1.tar.gz", hash = "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3"}, +] +dataclasses-json = [ + {file = "dataclasses-json-0.5.2.tar.gz", hash = "sha256:56ec931959ede74b5dedf65cf20772e6a79764d20c404794cce0111c88c085ff"}, + {file = "dataclasses_json-0.5.2-py3-none-any.whl", hash = "sha256:b746c48d9d8e884e2a0ffa59c6220a1b21f94d4f9f12c839da0a8a0efd36dc19"}, +] +decorator = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] +flake8 = [ + {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, + {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, +] +flake8-black = [ + {file = "flake8-black-0.2.1.tar.gz", hash = "sha256:f26651bc10db786c03f4093414f7c9ea982ed8a244cec323c984feeffdf4c118"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-20.1.4.tar.gz", hash = "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162"}, + {file = "flake8_bugbear-20.1.4-py36.py37.py38-none-any.whl", hash = "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63"}, +] +flake8-isort = [ + {file = "flake8-isort-4.0.0.tar.gz", hash = "sha256:2b91300f4f1926b396c2c90185844eb1a3d5ec39ea6138832d119da0a208f4d9"}, + {file = "flake8_isort-4.0.0-py2.py3-none-any.whl", hash = "sha256:729cd6ef9ba3659512dee337687c05d79c78e1215fdf921ed67e5fe46cce2f3c"}, +] +icontract = [ + {file = "icontract-2.3.6.tar.gz", hash = "sha256:3248f092813cda5bf0cfef890f68a95fcc0ffe40fbf65c34a0bab3ff04d250f9"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +isodate = [ + {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, + {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, +] +isort = [ + {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, + {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, +] +jedi = [ + {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, + {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, +] +marshmallow = [ + {file = "marshmallow-3.9.0-py2.py3-none-any.whl", hash = "sha256:4bc31ab18133083b12893c61f2fc38b93c390d3fd6ae2ac61980b7dc936a1afa"}, + {file = "marshmallow-3.9.0.tar.gz", hash = "sha256:97ad6acaf727be986330969cff16040fce051510759ca709de9cd48093c55d04"}, +] +marshmallow-enum = [ + {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"}, + {file = "marshmallow_enum-1.5.1-py2.py3-none-any.whl", hash = "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +msal = [ + {file = "msal-1.5.1-py2.py3-none-any.whl", hash = "sha256:d84074a997e6fb2a47e22815dce376adcc8790838b6b1bf1fcea29378e2bf3eb"}, + {file = "msal-1.5.1.tar.gz", hash = "sha256:7efb0256c96a7b2eadab49ce29ecdb91352a91440c12a40bed44303724b62fda"}, +] +msal-extensions = [ + {file = "msal-extensions-0.3.0.tar.gz", hash = "sha256:5523dfa15da88297e90d2e73486c8ef875a17f61ea7b7e2953a300432c2e7861"}, + {file = "msal_extensions-0.3.0-py2.py3-none-any.whl", hash = "sha256:a530c2d620061822f2ced8e29da301bc928b146970df635c852907423e8ddddc"}, +] +msrest = [ + {file = "msrest-0.6.19-py2.py3-none-any.whl", hash = "sha256:87aa64948c3ef3dbf6f6956d2240493e68d714e4621b92b65b3c4d5808297929"}, + {file = "msrest-0.6.19.tar.gz", hash = "sha256:55f8c3940bc5dc609f8cf9fcd639444716cc212a943606756272e0d0017bbb5b"}, +] +msrestazure = [ + {file = "msrestazure-0.6.4-py2.py3-none-any.whl", hash = "sha256:3de50f56147ef529b31e099a982496690468ecef33f0544cb0fa0cfe1e1de5b9"}, + {file = "msrestazure-0.6.4.tar.gz", hash = "sha256:a06f0dabc9a6f5efe3b6add4bd8fb623aeadacf816b7a35b0f89107e0544d189"}, +] +mypy = [ + {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"}, + {file = "mypy-0.782-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e"}, + {file = "mypy-0.782-cp35-cp35m-win_amd64.whl", hash = "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d"}, + {file = "mypy-0.782-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd"}, + {file = "mypy-0.782-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"}, + {file = "mypy-0.782-cp36-cp36m-win_amd64.whl", hash = "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406"}, + {file = "mypy-0.782-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86"}, + {file = "mypy-0.782-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707"}, + {file = "mypy-0.782-cp37-cp37m-win_amd64.whl", hash = "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308"}, + {file = "mypy-0.782-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc"}, + {file = "mypy-0.782-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea"}, + {file = "mypy-0.782-cp38-cp38-win_amd64.whl", hash = "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b"}, + {file = "mypy-0.782-py3-none-any.whl", hash = "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d"}, + {file = "mypy-0.782.tar.gz", hash = "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +oauthlib = [ + {file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"}, + {file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"}, +] +paramiko = [ + {file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, + {file = "paramiko-2.7.2.tar.gz", hash = "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035"}, +] +parso = [ + {file = "parso-0.7.1-py2.py3-none-any.whl", hash = "sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea"}, + {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, +] +pathspec = [ + {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, + {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +portalocker = [ + {file = "portalocker-1.7.1-py2.py3-none-any.whl", hash = "sha256:34cb36c618d88bcd9079beb36dcdc1848a3e3d92ac4eac59055bdeafc39f9d4a"}, + {file = "portalocker-1.7.1.tar.gz", hash = "sha256:6d6f5de5a3e68c4dd65a98ec1babb26d28ccc5e770e07b672d65d5a35e4b2d8a"}, +] +psutil = [ + {file = "psutil-5.7.3-cp27-none-win32.whl", hash = "sha256:1cd6a0c9fb35ece2ccf2d1dd733c1e165b342604c67454fd56a4c12e0a106787"}, + {file = "psutil-5.7.3-cp27-none-win_amd64.whl", hash = "sha256:e02c31b2990dcd2431f4524b93491941df39f99619b0d312dfe1d4d530b08b4b"}, + {file = "psutil-5.7.3-cp35-cp35m-win32.whl", hash = "sha256:56c85120fa173a5d2ad1d15a0c6e0ae62b388bfb956bb036ac231fbdaf9e4c22"}, + {file = "psutil-5.7.3-cp35-cp35m-win_amd64.whl", hash = "sha256:fa38ac15dbf161ab1e941ff4ce39abd64b53fec5ddf60c23290daed2bc7d1157"}, + {file = "psutil-5.7.3-cp36-cp36m-win32.whl", hash = "sha256:01bc82813fbc3ea304914581954979e637bcc7084e59ac904d870d6eb8bb2bc7"}, + {file = "psutil-5.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:6a3e1fd2800ca45083d976b5478a2402dd62afdfb719b30ca46cd28bb25a2eb4"}, + {file = "psutil-5.7.3-cp37-cp37m-win32.whl", hash = "sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7"}, + {file = "psutil-5.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:5d9106ff5ec2712e2f659ebbd112967f44e7d33f40ba40530c485cc5904360b8"}, + {file = "psutil-5.7.3-cp38-cp38-win32.whl", hash = "sha256:ade6af32eb80a536eff162d799e31b7ef92ddcda707c27bbd077238065018df4"}, + {file = "psutil-5.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:2cb55ef9591b03ef0104bedf67cc4edb38a3edf015cf8cf24007b99cb8497542"}, + {file = "psutil-5.7.3.tar.gz", hash = "sha256:af73f7bcebdc538eda9cc81d19db1db7bf26f103f91081d780bbacfcb620dee2"}, +] +py = [ + {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, + {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, +] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] +pyjwt = [ + {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, + {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"}, +] +pyls-black = [ + {file = "pyls-black-0.4.6.tar.gz", hash = "sha256:33700e5ed605636ea7ba39188a1362d2f8602f7301f8f2b8544773886f965663"}, + {file = "pyls_black-0.4.6-py3-none-any.whl", hash = "sha256:8f5fb8fed503588c10435d2d48e2c3751437f1bdb8116134b05a4591c4899940"}, +] +pyls-isort = [ + {file = "pyls-isort-0.2.0.tar.gz", hash = "sha256:a6c292332746d3dc690f2a3dcdb9a01d913b9ee8444defe3cbffcddb7e3874eb"}, +] +pyls-mypy = [ + {file = "pyls-mypy-0.1.8.tar.gz", hash = "sha256:3fd83028961f0ca9eb3048b7a01cf42a9e3d46d8ea4935c1424c33da22c3eb03"}, +] +pynacl = [ + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +python-jsonrpc-server = [ + {file = "python-jsonrpc-server-0.4.0.tar.gz", hash = "sha256:62c543e541f101ec5b57dc654efc212d2c2e3ea47ff6f54b2e7dcb36ecf20595"}, + {file = "python_jsonrpc_server-0.4.0-py3-none-any.whl", hash = "sha256:e5a908ff182e620aac07db5f57887eeb0afe33993008f57dc1b85b594cea250c"}, +] +python-language-server = [ + {file = "python-language-server-0.35.1.tar.gz", hash = "sha256:6e0c9a3b2ae98e0eb22e98ed6b3c4e190a6bf9e27af53efd2396da60cd92b221"}, + {file = "python_language_server-0.35.1-py2.py3-none-any.whl", hash = "sha256:7051090259e3e81c0cdb140de8e32b8f11219808cda4427e6faf61f9ff9a3bf4"}, +] +pywin32 = [ + {file = "pywin32-228-cp27-cp27m-win32.whl", hash = "sha256:37dc9935f6a383cc744315ae0c2882ba1768d9b06700a70f35dc1ce73cd4ba9c"}, + {file = "pywin32-228-cp27-cp27m-win_amd64.whl", hash = "sha256:11cb6610efc2f078c9e6d8f5d0f957620c333f4b23466931a247fb945ed35e89"}, + {file = "pywin32-228-cp35-cp35m-win32.whl", hash = "sha256:1f45db18af5d36195447b2cffacd182fe2d296849ba0aecdab24d3852fbf3f80"}, + {file = "pywin32-228-cp35-cp35m-win_amd64.whl", hash = "sha256:6e38c44097a834a4707c1b63efa9c2435f5a42afabff634a17f563bc478dfcc8"}, + {file = "pywin32-228-cp36-cp36m-win32.whl", hash = "sha256:ec16d44b49b5f34e99eb97cf270806fdc560dff6f84d281eb2fcb89a014a56a9"}, + {file = "pywin32-228-cp36-cp36m-win_amd64.whl", hash = "sha256:a60d795c6590a5b6baeacd16c583d91cce8038f959bd80c53bd9a68f40130f2d"}, + {file = "pywin32-228-cp37-cp37m-win32.whl", hash = "sha256:af40887b6fc200eafe4d7742c48417529a8702dcc1a60bf89eee152d1d11209f"}, + {file = "pywin32-228-cp37-cp37m-win_amd64.whl", hash = "sha256:00eaf43dbd05ba6a9b0080c77e161e0b7a601f9a3f660727a952e40140537de7"}, + {file = "pywin32-228-cp38-cp38-win32.whl", hash = "sha256:fa6ba028909cfc64ce9e24bcf22f588b14871980d9787f1e2002c99af8f1850c"}, + {file = "pywin32-228-cp38-cp38-win_amd64.whl", hash = "sha256:9b3466083f8271e1a5eb0329f4e0d61925d46b40b195a33413e0905dccb285e8"}, + {file = "pywin32-228-cp39-cp39-win32.whl", hash = "sha256:ed74b72d8059a6606f64842e7917aeee99159ebd6b8d6261c518d002837be298"}, + {file = "pywin32-228-cp39-cp39-win_amd64.whl", hash = "sha256:8319bafdcd90b7202c50d6014efdfe4fde9311b3ff15fd6f893a45c0868de203"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +regex = [ + {file = "regex-2020.10.28-cp27-cp27m-win32.whl", hash = "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504"}, + {file = "regex-2020.10.28-cp27-cp27m-win_amd64.whl", hash = "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd"}, + {file = "regex-2020.10.28-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26"}, + {file = "regex-2020.10.28-cp36-cp36m-win32.whl", hash = "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582"}, + {file = "regex-2020.10.28-cp36-cp36m-win_amd64.whl", hash = "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c"}, + {file = "regex-2020.10.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0"}, + {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c"}, + {file = "regex-2020.10.28-cp37-cp37m-win32.whl", hash = "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0"}, + {file = "regex-2020.10.28-cp37-cp37m-win_amd64.whl", hash = "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a"}, + {file = "regex-2020.10.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux1_i686.whl", hash = "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427"}, + {file = "regex-2020.10.28-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf"}, + {file = "regex-2020.10.28-cp38-cp38-win32.whl", hash = "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f"}, + {file = "regex-2020.10.28-cp38-cp38-win_amd64.whl", hash = "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de"}, + {file = "regex-2020.10.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d"}, + {file = "regex-2020.10.28-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b"}, + {file = "regex-2020.10.28-cp39-cp39-win32.whl", hash = "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0"}, + {file = "regex-2020.10.28-cp39-cp39-win_amd64.whl", hash = "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e"}, + {file = "regex-2020.10.28.tar.gz", hash = "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +requests-oauthlib = [ + {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, + {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, + {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, +] +retry = [ + {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, + {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, +] +rope = [ + {file = "rope-0.17.0.tar.gz", hash = "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +spur = [ + {file = "spur-0.3.20-py2.py3-none-any.whl", hash = "sha256:f9446f418f419c046f49543ca55b35eff2e96b13cf7e0cf8605d657e5fa530ee"}, + {file = "spur-0.3.20.tar.gz", hash = "sha256:f359e0573c0e4aaf8427494d6e67bc2bfac4f0c719e47f05e4750921b5804760"}, +] +spurplus = [ + {file = "spurplus-2.3.4.tar.gz", hash = "sha256:667a88c06d43f4008ded2ee8706c8f20808f138dbd777ea07b8d9be277470116"}, +] +stringcase = [ + {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, +] +temppathlib = [ + {file = "temppathlib-1.0.4.tar.gz", hash = "sha256:ecce37504cc2c7c996983d0c462311152bbc76102d1aa04119c12e6fdfe4180a"}, +] +testfixtures = [ + {file = "testfixtures-6.15.0-py2.py3-none-any.whl", hash = "sha256:e17f4f526fc90b0ac9bc7f8ca62b7dec17d9faf3d721f56bda4f0fd94d02f85a"}, + {file = "testfixtures-6.15.0.tar.gz", hash = "sha256:409f77cfbdad822d12a8ce5c4aa8fb4d0bb38073f4a5444fede3702716a2cec2"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typed-ast = [ + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +typing-inspect = [ + {file = "typing_inspect-0.6.0-py2-none-any.whl", hash = "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0"}, + {file = "typing_inspect-0.6.0-py3-none-any.whl", hash = "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f"}, + {file = "typing_inspect-0.6.0.tar.gz", hash = "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7"}, +] +ujson = [ + {file = "ujson-4.0.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:5fe1536465b1c86e32a47113abd3178001b7c2dcd61f95f336fe2febf4661e74"}, + {file = "ujson-4.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0f412c3f59b1ab0f40018235224ca0cf29232d0201ff5085618565a8a9c810ed"}, + {file = "ujson-4.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f12b0b4e235b35d49f15227b0a827e614c52dda903c58a8f5523936c233dfc7"}, + {file = "ujson-4.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7a1545ac2476db4cc1f0f236603ccbb50991fc1bba480cda1bc06348cc2a2bf0"}, + {file = "ujson-4.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:078808c385036cba73cad96f498310c61e9b5ae5ac9ea01e7c3996ece544b556"}, + {file = "ujson-4.0.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:4fe8c6112b732cba5a722f7cbe22f18d405f6f44415794a5b46473a477635233"}, + {file = "ujson-4.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:71703a269f074ff65b9d7746662e4b3e76a4af443e532218af1e8ce15d9b1e7b"}, + {file = "ujson-4.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b87379a3f8046d6d111762d81f3384bf38ab24b1535c841fe867a4a097d84523"}, + {file = "ujson-4.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a79bca47eafb31c74b38e68623bc9b2bb930cb48fab1af31c8f2cb68cf473421"}, + {file = "ujson-4.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:e7ab24942b2d57920d75b817b8eead293026db003247e26f99506bdad86c61b4"}, + {file = "ujson-4.0.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:51480048373cf97a6b97fcd70c3586ca0a31f27e22ab680fb14c1f22bedbf743"}, + {file = "ujson-4.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c604024bd853b5df6be7d933e934da8dd139e6159564db7c55b92a9937678093"}, + {file = "ujson-4.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:568bb3e7f035006147af4ce3a9ced7d126c92e1a8607c7b2266007b1c1162c53"}, + {file = "ujson-4.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:bd4c77aee3ffb920e2dbc21a9e0c7945a400557ce671cfd57dbd569f5ebc619d"}, + {file = "ujson-4.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:c354c1617b0a4378b6279d0cd511b769500cf3fa7c42e8e004cbbbb6b4c2a875"}, + {file = "ujson-4.0.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a5200a68f1dcf3ce275e1cefbcfa3914b70c2b5e2f71c2e31556aa1f7244c845"}, + {file = "ujson-4.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a618af22407baeadb3f046f81e7a5ee5e9f8b0b716d2b564f92276a54d26a823"}, + {file = "ujson-4.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:0a2e1b211714eb1ec0772a013ec9967f8f95f21c84e8f46382e9f8a32ae781fe"}, + {file = "ujson-4.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:2b2d9264ac76aeb11f590f7a1ccff0689ba1313adacbb6d38d3b15f21a392897"}, + {file = "ujson-4.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:f8a60928737a9a47e692fcd661ef2b5d75ba22c7c930025bd95e338f2a6e15bc"}, + {file = "ujson-4.0.1.tar.gz", hash = "sha256:26cf6241b36ff5ce4539ae687b6b02673109c5e3efc96148806a7873eaa229d3"}, +] +urllib3 = [ + {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, + {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, +] diff --git a/.archive/pyproject.toml b/.archive/pyproject.toml new file mode 100644 index 0000000000..afd0645a74 --- /dev/null +++ b/.archive/pyproject.toml @@ -0,0 +1,55 @@ +[tool.poetry] +name = "LISA" +version = "3.0.0.dev1" +description = "Linux Integration Services Automation" +authors = ["Andrew Schwartzmeyer ", "Chi Song "] +license = "Apache License 2.0" + +[tool.poetry.dependencies] +python = "^3.8" +psutil = "^5.7.2" +pyyaml = "^5.3.1" +retry = "^0.9.2" +paramiko = "^2.7.1" +spurplus = "^2.3.3" +dataclasses-json = "^0.5.2" +# portalocker for compatibility of Windows, trigger pywin32 installed +portalocker = "^1.7.1" +azure-identity = {version = "^1.4.0", allow-prereleases = true} +azure-mgmt-resource = {version = "^15.0.0-beta.1", allow-prereleases = true} +azure-mgmt-compute = {version = "^17.0.0-beta.1", allow-prereleases = true} +azure-mgmt-marketplaceordering = {version = "^0.2.1", allow-prereleases = true} +azure-mgmt-network = {version = "^16.0.0-beta.1", allow-prereleases = true} +azure-mgmt-storage = {version = "^16.0.0", allow-prereleases = true} +asserts = "^0.11.0" + +[tool.poetry.dev-dependencies] +black = "^20.8b1" +flake8 = "^3.8.3" +flake8-black = "^0.2.1" +flake8-bugbear = "^20.1.4" +flake8-isort = "^4.0.0" +isort = "^5.5.3" +mypy = "^0.782" +pyls-black = "^0.4.6" +pyls-isort = "^0.2.0" +pyls-mypy = "^0.1.8" +python-language-server = "^0.35.1" +rope = "^0.17.0" +coverage = "^5.3" + +[tool.black] +line-length = 88 +target-version = ['py38'] + +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 88 + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/testsuites/basic/provisioning.py b/.archive/testsuites/basic/provisioning.py similarity index 100% rename from testsuites/basic/provisioning.py rename to .archive/testsuites/basic/provisioning.py diff --git a/testsuites/runbooks/azure/p0.yml b/.archive/testsuites/runbooks/azure/p0.yml similarity index 100% rename from testsuites/runbooks/azure/p0.yml rename to .archive/testsuites/runbooks/azure/p0.yml diff --git a/testsuites/runbooks/azure/secret.yml b/.archive/testsuites/runbooks/azure/secret.yml similarity index 100% rename from testsuites/runbooks/azure/secret.yml rename to .archive/testsuites/runbooks/azure/secret.yml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..15e6a1f149 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +# Ignore parent project’s config +root = true diff --git a/.flake8 b/.flake8 index f855799a35..78708c2676 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,4 @@ max-line-length = 88 select = B,BLK,C90,E,F,I,W max-complexity = 15 -extend-ignore = E203 +extend-ignore = E203,E501 diff --git a/.github/workflows/ci-workflow.yaml b/.github/workflows/ci-workflow.yaml new file mode 100644 index 0000000000..a40cae5ffc --- /dev/null +++ b/.github/workflows/ci-workflow.yaml @@ -0,0 +1,62 @@ +name: LISA/Pytest CI Workflow + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + os: [ubuntu-20.04, windows-2019] + python-version: [3.7, 3.8, 3.9] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository to $GITHUB_WORKSPACE + uses: actions/checkout@v2 + + - name: Setup bootstrap Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry for Linux + if: runner.os == 'Linux' + run: | + curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python + echo "$HOME/.poetry/bin" >> $GITHUB_PATH + + - name: Install Poetry for Windows + if: runner.os == 'Windows' + run: | + (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python + echo "$env:USERPROFILE\.poetry\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Install Python dependencies + run: poetry install + + # TODO: There's a bug in Poetry where `poetry run lisa` tries + # to call the Bash script it generated instead of `lisa.cmd`, + # which of course fails on Windows. + - name: Run self tests + run: poetry run pytest --verbose --playbook=playbooks/test.yaml --setup-show selftests/ + + - name: Run semantic analysis + run: poetry run pytest --tb=auto --flake8 --mypy -m "flake8 or mypy" + + - name: Build documentation website + if: runner.os == 'Linux' && matrix.python-version == '3.9' + run: | + rm -rf docs/ + poetry run sphinx-build . docs/ + git config --local user.email 'lisval@microsoft.com' + git config --local user.name 'GitHub Action' + git add docs/ + git commit -m "Generated documentation" + + - name: Push to GitHub Pages branch + if: runner.os == 'Linux' && matrix.python-version == '3.9' + uses: ad-m/github-push-action@master + with: + branch: gh-pages + force: true + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index e359288dee..4f80922946 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,12 @@ -# IDE Settings -/.vscode - -# lisa runtime folder -/runtime/* - -# python cache -__pycache__ - -# you can have your own command alias on Windows -lisa.cmd - -# it's auto generated by poetry -lisa.egg-info - -# code coverage result -.coverage -htmlcov +*$py.class +*.py[cod] +*.so +.mypy_cache/ +.pytest_cache/ +/*.html +/*.xml +/assets/ +/_build/ +/modules/ +__pycache__/ +dist/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 353e2c6af3..a079aff1bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,36 +3,62 @@ This document describes the existing developer tooling we have in place (and what to expect of it), as well as our design and development philosophy. -## Naming Conventions +See [learnxinyminutes.com](https://learnxinyminutes.com/docs/python/) for a +Python crash course. -Naming conventions are not automatically enforced, so please read the [naming -conventions](https://www.python.org/dev/peps/pep-0008/#naming-conventions) -section of PEP 8, which describes what each of the different styles means. A -short summary of the most important parts: +## Pytest -* Modules (and hence files) should have short, all-lowercase names. -* Class (and exception) names should normally use the `CapWords` convention - (also known as `CamelCase`). -* Function and variable names should be lowercase, with words separated by - underscores as necessary to improve readability (also known as `snake_case`). -* To avoid collisions with the standard library, an underscore can be appended, - such as `id_`. -* Always use `self` for the first argument to instance methods. -* Always use `cls` for the first argument to class methods. -* One leading underscore like `_data` is for non-public methods and instance - variables. And it can be used by sub-classes. If it won't be used in - sub-classes, use like `__data`. -* If there is a pair of `get_x` and `set_x` methods, they should instead be a - proper property, which is easy to do with the built-in `@property` decorator. -* Constants should be `CAPITALIZED_SNAKE_CASE`. -* When importing a function, try to avoid renaming it with `import as` because - it introduces cognitive overhead to track yet another name. +Underneath the hood, `lisa` is just [Pytest][]! Refer to its +[documentation](https://docs.pytest.org/en/stable/contents.html). -When in doubt, adhere to existing conventions, or check the style guide. +[Pytest]: https://docs.pytest.org/en/stable/ + +Some useful CLI options are: + +* `--help` and `--verbose`, of course +* `--log-cli-level=DEBUG` to enable live output of all `DEBUG` and above logs +* `--collect-only` to show which tests would be selected (but don’t run them) +* `--setup-only` to show in which order fixtures and tests would be setup (but + don’t run them) +* `--flake8 --mypy -m "flake8 or mypy"` to run the semantic analysis tools + +## YAML Schema + +The registered playbook schema can be generated using `--print-schema`: + +```bash +lisa --print-schema=playbooks/schema.json +``` + +This will create a file `playbooks/schema.json` with the [JSON +Schema](https://json-schema.org/) for all the registered schemata (including +those added in any local plugins). Note that this file is committed to the repo +for the public schema, but you can generate your own (or update it) with the +above command. + +Using the LSP [yaml-language-server][] you can setup almost any editor to lint +the playbook files against the schema. Either add as a comment to the top of the +playbook file: -## Automated Tooling +```yaml +# yaml-language-server: $schema=file:///path/to/playbooks/schema.json +``` -If you have ran LISAv3 already, then you have installed and used the `poetry` +Or set `yaml.schemas` as appropriate for your editor. Also ensure that +`yaml-language-server` is installed, which you can do via: + +```bash +npm install -g yaml-language-server +``` + +See [learnxinyminutes.com](https://learnxinyminutes.com/docs/yaml/) for a crash +course in YAML. + +[yaml-language-server]: https://github.com/redhat-developer/yaml-language-server + +## Poetry + +If you have ran `lisa` already, then you have installed and used the `poetry` tool. [Poetry][] is a [PEP 518][] compliant and cross-platform build system which handles our Python dependencies and environment. @@ -51,9 +77,9 @@ description, authors, and license) which will be embedded in the final built package. The chosen version follows [Semantic Versioning][], with the [Python specific -pre-release versioning suffix][pre-release] ‘.dev1’. Since this is “LISAv3” it +pre-release versioning suffix][pre-release] ‘.dev1’. Since this is “pytest-lisa” it seemed appropriate to set our version to ‘3.0.0.dev1’, that is, “the first -development release of LISAv3.” +development release of pytest-lisa.” [Semantic Versioning]: https://semver.org/ [pre-release]: https://packaging.python.org/guides/distributing-packages-using-setuptools/#choosing-a-versioning-scheme @@ -75,30 +101,19 @@ From the documentation: On Linux, your initial run of `poetry install` will cause Poetry to automatically setup a new [virtualenv][] using [pyenv][]. If you are developing -on Windows, you will want to setup your own, perhaps using [Conda][]. - -[virtualenv]: https://docs.python-guide.org/dev/virtualenvs/ -[pyenv]: https://github.com/pyenv/pyenv -[Conda]: https://docs.conda.io/en/latest/ - -* python: We pinned Python to version 3.8 so everyone uses the same version. - -* psutil: TODO @squirrelsc will document - -* pyyaml: TODO @squirrelsc will document - -* retry: TODO @squirrelsc will document +on Windows, you may need to setup your own, perhaps using [Conda][]. -* paramiko: TODO @squirrelsc will document +The path to the virtualenv used by Poetry can found with this command: -* spurplus: TODO @squirrelsc will document +```bash +poetry env list --full-path +``` -* dataclasses-json: TODO @squirrelsc will document (brings in `usjon` which - requires `gcc` and `libpython`) +Use it to configure your editor. -* portalocker: TODO @squirrelsc will document - -* azure-*: TODO @squirrelsc will document +[virtualenv]: https://docs.python-guide.org/dev/virtualenvs/ +[pyenv]: https://github.com/pyenv/pyenv +[Conda]: https://docs.conda.io/en/latest/ ### Developer Dependencies @@ -132,13 +147,96 @@ adhere to our coding standards. * [rope](https://github.com/python-rope/rope), to provide completions and renaming support to pyls. -With these packages installed and a correctly setup editor (see the readme and -feel free to reach out to us), your code should automatically follow all the +With these packages installed and a correctly setup editor (see below, and feel +free to reach out to us), your code should automatically follow all the standards which we could automate. -The final sections, `tool.black`, `tool.isort`, `build-system`, and the -`.flake8` file (Flake8 does not yet support `pyproject.toml`) configure the -tools per their recommendations. +The final sections, `tool.black`, `tool.isort`, `build-system`, and files +`mypy.ini` and `.flake8` configure the tools per their recommendations. + +## Editor Setup + +### Visual Studio Code + +First, click the Python version in the bottom left, then enter the path emitted +by the command above. This will point Code to the Poetry virtual environment. + +Make sure below settings are in root level of `.vscode/settings.json` + +```json +{ + "python.analysis.typeCheckingMode": "strict", + "python.formatting.provider": "black", + "python.linting.enabled": true, + "python.linting.flake8Enabled": true, + "python.linting.mypyEnabled": true, + "python.linting.pylintEnabled": false, + "editor.formatOnSave": true, + "python.linting.mypyArgs": [ + "--strict", + "--namespace-packages", + "--show-column-numbers", + ], + "python.sortImports.path": "isort", + "python.analysis.useLibraryCodeForTypes": false, + "python.analysis.autoImportCompletions": false, + "files.eol": "\n", +} +``` + +### Emacs + +I recommend using the [pyvenv][] package to have Emacs automatically use the +correct Python venv (setup by Poetry), and the [eglot][] package to provide LSP +support. The below expects you already have an `init.el` with [use-package][]. + +[pyvenv]: https://github.com/jorgenschaefer/pyvenv +[eglot]: https://github.com/joaotavora/eglot +[use-package]: https://github.com/jwiegley/use-package + +```emacs-lisp +(use-package pyvenv + :ensure t + :hook (python-mode . pyvenv-tracking-mode)) + +(use-package eglot + :hook + (python-mode . eglot-ensure) + (yaml-mode . eglot-ensure) + :custom + (eglot-auto-display-help-buffer t) + (eglot-autoshutdown t) + (eglot-confirm-server-initiated-edits nil) + :config + (add-to-list 'eglot-server-programs '(yaml-mode . ("yaml-language-server" "--stdio"))) + (defun eglot-format-buffer-on-save () + (if (and (project-current) (eglot-managed-p)) + (add-hook 'before-save-hook #'eglot-format-buffer nil 'local) + (remove-hook 'before-save-hook #'eglot-format-buffer 'local))) + (add-hook 'eglot-managed-mode-hook #'eglot-format-buffer-on-save)) +``` + +Then run `M-x add-dir-local-variable RET python-mode RET pyvenv-activate RET +` where the value is the path given by the command above. +This will create a `.dir-locals.el` with this variable set for Python. + +Refer to this `.dir-locals.el` for a complete setup: + +```emacs-lisp +;;; Directory Local Variables +;;; For more information see (info "(emacs) Directory Variables") + +((python-mode + . ((eglot-workspace-configuration ; an LSP implementation + ;; Use `flake8’ instead of the default, and disable noisy plugins. + . ((:pyls . (:configurationSources ["flake8"] :plugins (:pycodestyle (:enabled nil) :mccabe (:enabled nil)))))))) + (yaml-mode + . ((eglot-workspace-configuration + ;; Set the `playbooks/schema.json’ as the schema for playbooks. + . ((:yaml . (:schemas (:playbooks/schema.json "playbooks/*"))))))) + ;; Set `pyvenv’ to use the given venv for the whole project. + (nil . ((pyvenv-activate . "~/.cache/pypoetry/virtualenvs/")))) +``` ## Type Annotations @@ -161,13 +259,31 @@ mypy’s [cheat sheet][]. [intro]: https://kishstats.com/python/2019/01/07/python-type-hinting.html [cheat sheet]: https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html -## Runbook schema +## Python Naming Conventions -Some plugins like Platform need follow this section to extend runbook schema. Runbook is the configurations of LISA runs. Every LISA run need a runbook. +Naming conventions are not automatically enforced, so please read the [naming +conventions](https://www.python.org/dev/peps/pep-0008/#naming-conventions) +section of PEP 8, which describes what each of the different styles means. A +short summary of the most important parts: -The runbook uses [dataclass](https://docs.python.org/3/library/dataclasses.html) to define, [dataclass-json](https://github.com/lidatong/dataclasses-json/) to deserialize, and [marshmallow](https://marshmallow.readthedocs.io/en/3.0/api_reference.html) to validate the schema. +* Modules (and hence files) should have short, all-lowercase names. +* Class (and exception) names should normally use the `CapWords` convention + (also known as `CamelCase`). +* Function and variable names should be lowercase, with words separated by + underscores as necessary to improve readability (also known as `snake_case`). +* To avoid collisions with the standard library, an underscore can be appended, + such as `id_`. +* Always use `self` for the first argument to instance methods. +* Always use `cls` for the first argument to class methods. +* Use one leading underscore only for non-public methods and instance variables, + such as `_data`. Do not activate name mangling with `__` unless necessary. +* If there is a pair of `get_x` and `set_x` methods, they should instead be a + proper property, which is easy to do with the built-in `@property` decorator. +* Constants should be `CAPITALIZED_SNAKE_CASE`. +* When importing a function, try to avoid renaming it with `import as` because + it introduces cognitive overhead to track yet another name. -See more examples in [schema.py](lisa/schema.py), if you need to extend runbook schema. +When in doubt, adhere to existing conventions, or check the style guide. ## Committing Guidelines @@ -257,28 +373,40 @@ Python world. If you make it through even some of these guides, you will be well on your way to being a “Pythonista” (a Python developer) writing “Pythonic” (canonically correct Python) code left and right. -### Async IO +## Generating Documentation + +We use [Sphinx](https://www.sphinx-doc.org/en/master/index.html) to generate our +[documentation](https://microsoft.github.io/lisa/). To build it locally (and +check all the links), use: + +```bash +sphinx-build -b linkcheck . _build +``` + +## Publishing Packages + +Setup [PyPI](https://pypi.org/) with Poetry’s +[repositories](https://python-poetry.org/docs/repositories/): + +```bash +poetry config pypi-token.pypi my-token +``` + +Then for each package, build and publish it! Don’t forget to increment the +version number afterwards. You should carefully test the package on the [Test +PyPI](https://test.pypi.org/) instance first, as you cannot overwrite uploaded +packages. You can also bump the patch version if you made a small mistake. -With Python 3.4, the Async IO pattern found in languages such as C# and Go is -available through the keywords `async` and `await`, along with the Python module -`asyncio`. Please read [Async IO in Python: A Complete -Walkthrough](https://realpython.com/async-io-python/) to understand at a high -level how asynchronous programming works. As of Python 3.7, One major “gotcha” -is that `asyncio.run(...)` should be used [exactly once in -`main`](https://docs.python.org/3/library/asyncio-task.html), it starts the -event loop. Everything else should be a coroutine or task which the event loop -schedules. +```bash +poetry publish --build +``` ## Future Sections Just a collection of reminders for the author to expand on later. -* [unittest](https://docs.python.org/3/library/unittest.html) * [doctest](https://docs.python.org/3/library/doctest.html) * [subprocess](https://pymotw.com/3/subprocess/index.html) * [GitHub Actions](https://github.com/LIS/LISAv2/actions) * [ShellCheck](https://www.shellcheck.net/) * [Governance](https://opensource.guide/leadership-and-governance/) -* [Maintenance Cost](https://web.archive.org/web/20120313070806/http://users.jyu.fi/~koskinen/smcosts.htm) -* Parallelism and multi-plexing -* Versioned inputs and outputs diff --git a/DESIGN.rst b/DESIGN.rst new file mode 100644 index 0000000000..119dc906d1 --- /dev/null +++ b/DESIGN.rst @@ -0,0 +1,1070 @@ +Technical Specification Document +================================ + +This document outlines the technical specifications and design for +LISAv3 leveraging `Pytest `_ as +the test runner. + +Please see `PR #1107 `_ +for a working implementation and see the `documentation`_. + +.. _documentation: https://microsoft.github.io/lisa/. + +:Author: Andrew Schwartzmeyer (he/him) +:Version: 0.4 + +Why Pytest? +----------- + +Pytest is an `incredibly popular +`_ MIT licensed open +source Python testing framework. It has a thriving community and +plugin framework, with over 750 `plugins +`_. Instead of writing (and +therefore maintaining) yet another test framework, we will do more +with less by reusing Pytest and existing plugins. This allows us to +focus on our unique problems: organizing and understanding our tests, +deploying necessary resources (such as Azure, Hyper-V, or bare metal +machines, collectively known as “targets”), and analyzing our results. + +Most of Pytest itself is implemented via `built-in plugins +`_, providing us with +many useful and well-documented examples. Furthermore, when others +were confronted with a problem similar to our own they also chose to +use Pytest. + +`Labgrid`_ is an open source embedded board control library that +delegated the testing framework logic to Pytest in their `design +`_, +and `U-Boot `_, an embedded board +boot loader, similarly leveraged Pytest in their `tests +`_. KernelCI and +Avocado were also evaluated by the Labgrid developers at an `Embedded +Linux Conference `_ and both ruled out +for reasons similar to our own before they settled on Pytest. + +.. _Labgrid: https://github.com/labgrid-project/labgrid + +The `fundamental features `_ of Pytest +match our needs very well: + +- Automatic test discovery, no boiler-plate test code +- Useful information when a test fails (assertions are introspected) +- Test and fixture `parameterization`_ +- Modular setup/teardown via `fixtures`_ +- Incredibly customizable (as detailed above) + +.. _parameterization: https://docs.pytest.org/en/stable/parametrize.html +.. _fixtures: https://docs.pytest.org/en/stable/fixture.html + +All the logic for describing, discovering, running, skipping and +reporting results of the tests, as well as enabling and importing +users’ plugins is already written and maintained by the open source +community. This leaves us to focus on our hard and specific problems: +creating an abstraction to launch the necessary targets, organizing +and publishing our tests, and reporting test results upstream. Using +Pytest also allows us the space to abstract other commonalities in our +specific tests. In this way, LISAv3 could solve the difficulties we +have at hand without creating yet another test framework. + +By leveraging such a popular framework we maximize the ease of +adoption for developers to write tests, as they are likely already +familiar with Pytest, and if not, have a wealth of examples and +`resources`_ from which to draw. The environment will be one of +instant familiarity, thus providing developers a running start. + +.. _resources: https://docs.pytest.org/en/stable/example/index.html + +Finally, by reducing the amount of code we maintain, we drastically +increase our chances of receiving pull requests instead of bug reports +from users. This is important because despite our best efforts it is +practically guaranteed that as adoption of LISAv3 increases, users +will want changes to be made, and we need to empower them to do so +themselves. Using Pytest gives us the best chances for users to +understand and extend the framework, plugins, etc. with ease. + +What are we maintaining? +------------------------ + +We have three Pytest plugins, soon to be published on `PyPI +`_, supporting the framework: + +- `pytest-target`_ +- `pytest-lisa`_ +- `pytest-playbook`_ + +We will also maintain our set of public “LISA” tests, but these are +decoupled from the plugins and packages. + +The `pytest-target`_ plugin encapsulates all our logic for *how* and +*when* to deploy targets (local or cloud virtual machines, or bare +metal machines, and all the associated resources), run tests on the +specified targets, and delete the targets. This includes specifying +which features and resources each test needs and each given target +provides (such as number of cores, amount of RAM, and other hardware +like a GPU etc.), how to deploy and delete each target based on its +platform, and `parameterization`_ of the +:py:func:`~target.plugin.target` fixture based on YAML file input (the +“playbook”). In fact, some tests (like networking) require multiple +targets at once. This plugin will need to manage resources +intelligently, being able to optimize for both time and cost, and make +it easy for tests to request and use various resources. + +The `pytest-lisa`_ plugin encapsulates all our logic for how to +organize and select tests, as well as our opinions on displaying test +results. This includes the user modes, test metadata and inventory, +test selection based on criteria against that metadata, required and +pre-configured upstream plugins, and result notifiers. It will +similarly support YAML file playbook input. + +The `pytest-playbook`_ plugin encapsulates the shared common +functionality of registering component schemata (e.g. platform and +target parameters from `pytest-target`_ and selection criteria from +`pytest-lisa`_). It uses the `schema`_ library. + +.. _schema: https://pypi.org/project/schema/ + +We have striven to keep `pytest-lisa`_ and `pytest-target`_ from +depending on each other in order to keep their scope well-defined. +They both depend on `pytest-playbook`_, and the “LISA” project depends +on them both, but they are independent plugins. + +In the “LISA” repository of tests we may also maintain additional +`fixtures`_ for our tests’ unique requirements. Similarly, we and +others may have private test repositories which build upon the above +by defining new platform support and internal service integrations. +The built-in plugin discovery of Pytest (via ``conftest.py`` files) +enables us to satisfy one of our requirements to “support plugins to +orchestrate the test environment.” + +pytest-target +------------- + +How are targets provided and accessed? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First we need to define “target” as an instance of a +system-under-test. That is, given some environment requirements, such +an Azure image (URN) and size (SKU), a target would be a virtual +machine deployed by `pytest-target`_ with SSH access provided to the +requesting test. A target could optionally be pre-deployed and simply +connected. Some tests may request multiple targets as well. + +Pytest uses `fixtures`_, which are the primary way of setting up test +requirements. They replace less flexible alternatives like +setup/teardown functions. It is through fixtures that we implement +remote target setup/teardown. Our :py:func:`~target.plugin.target` +fixture returns a :py:class:`~target.target.Target` instance, which +currently provides: + +- Remote shell access via SSH using `Fabric`_ +- Data including hostname / IP address +- Cross-platform ping functionality with exponential back-off +- Uploading of local files to arbitrary remote destinations +- Downloading of remote file contents into local string variable +- Asynchronous remote command execution with promises + +.. _Fabric: https://www.fabfile.org/ + +The :py:class:`~target.azure.AzureCLI` subclass additionally provides: + +- An example of a working platform implementation +- Automatic provisioning of a parameterized Azure VM +- Allowing ICMP ping via Azure firewall rules +- Azure platform forced reboot by API +- Downloading boot diagnostics (serial console log) + +The :py:class:`~target.target.SSH` subclass is a simple implementation +which only connects to a given host. + +The :py:class:`~target.target.Target` class leverages `Fabric`_ which +is a popular high-level Python library for executing shell commands on +remote systems over SSH. Underneath the covers Fabric uses +`Paramiko`_, the most popular low-level Python SSH library. Fabric +does the heavy lifting of safely connecting and disconnecting from the +node, executing the shell command (synchronously or asynchronously), +reporting the exit status, gathering the ``stdout`` and ``stderr``, +providing ``stdin`` (or interactive auto-responses, similar to +``expect``), uploading and downloading files, and much more. In fact, +these APIs are all available and implemented for the local machine by +the underlying `Invoke`_ library, which is essentially a Python +``subprocess`` wrapper with “a powerful and clean feature set.” + +.. _Paramiko: https://docs.paramiko.org/en/stable/ +.. _Invoke: https://www.pyinvoke.org/ + +Other test specific requirements, such as installing software and +daemons, downloading files from remote storage, or checking the state +of our Bash test scripts, would similarly be implemented by methods on +:py:class:`~target.target.Target`, its subclasses, or via additional +fixtures and thus shared among tests. + +What’s the :py:class:`~target.target.Target` class? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In version 0.1 of this design document we detailed a planned refactor +of what was then called the ``Node`` class. This has since been +executed with just a few modifications (one being the rename to +:py:class:`~target.target.Target`, as ``Node`` was found to be an +overloaded term in the context of data centers). This class and its +subclasses are decoupled from Pytest, and are used via fixtures. Its +interface looks like this: + +.. code:: python + + from abc import ABC, abstractmethod + from schema import Schema + import fabric + + class Target(ABC): + + group: str + params: Dict[str, str] + features: List[str] + data: Dict[Any, Any] + number: int + locked: bool + name: str + host: str + conn: fabric.Connection # Provides run, sudo, get, put etc. + + def __init__(...): + ... + self.params = self.get_schema().validate(params) + self.name = f"{self.group}-{self.number}" + self.host = self.deploy() + self.conn = fabric.Connection(self.host, ...) + + @classmethod + @abstractmethod + def schema(cls) -> Mapping[Any, Any]: + """Must return a mapping for expected instance parameters.""" + ... + + @classmethod + def defaults(cls) -> Mapping[Any, Any]: + """Can return a mapping for default parameters.""" + ... + + @abstractmethod + def deploy(self) -> str: + """Must deploy the target resources and return the hostname.""" + ... + + @abstractmethod + def delete(self) -> None: + """Must delete the target's resources.""" + ... + +This class allows us to answer the next question. + +How are new platforms supported? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Platform support is implemented by subclassing +:py:class:`~target.target.Target` and implementing the abstract +methods in the above interface: + +- :py:meth:`~target.target.Target.schema`: Define the schema for the platform’s parameters +- :py:meth:`~target.target.Target.defaults`: Define defaults for those parameters +- :py:meth:`~target.target.Target.deploy`: Create an instance resource +- :py:meth:`~target.target.Target.delete`: Delete the instance and its resources + +Internally we use the ``__subclasses__`` attribute of +:py:class:`~target.target.Target` to automatically gather all the +available platforms and their parameter schemata from users’ own +``conftest.py`` files and other plugins. This enables the +:py:func:`~target.plugin.target` fixture to dynamically instantiate a +target from the gathered requirements and parameters. + +For example, the :py:class:`~target.azure.AzureCLI` subclass defines +its required parameters using the `schema`_ library like this: + +.. code:: python + + from schema import Optional, Schema + from target import Target + + class AzureCLI(Target): + ... + @classmethod + def schema(cls) -> Dict[Any, Any]: + return { + "image": str, + Optional("sku"): str, + Optional("location"): str, + } + +Simply through defining this subclass the user can now specify a set +of parameterized YAML targets in a playbook like this: + +.. code:: yaml + + platforms: + AzureCLI: + sku: Standard_DS2_v2 + + targets: + - name: Debian + platform: AzureCLI + image: Debian:debian-10:10:latest + + - name: Ubuntu + platform: AzureCLI + image: Canonical:UbuntuServer:18.04-LTS:latest + + +These targets are then used to parameterize the +:py:func:`~target.plugin.target` fixture in the +:py:func:`~target.plugin.pytest_generate_tests` hook (see below for +more details). + +This demonstrated how we can have platforms define their own schema +and register that schema automatically. The ``platforms`` key allows a +playbook to override the defaults in the platform implementation, +which are then eclipsed for each named target in the ``targets`` key. +This is accomplished through internal details in `pytest-target`_’s +hook implementation :py:func:`~target.plugin.pytest_playbook_schema` +using the `pytest-playbook`_ plugin, but for the users, it just works. + +How do we interact with Azure? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For :py:class:`~target.azure.AzureCLI`, we use the `Azure CLI`_ to +deploy a virtual machine. For Hyper-V (and other virtualization +platforms), we would like to use `libvirt`_, and for embedded / bare +metal environments we are evaluating `Labgrid`_. + +.. _Azure CLI: https://aka.ms/azureclidocs +.. _libvirt: https://libvirt.org/python.html + +If possible, we do not want to use the `Azure Python APIs +`_ directly because they are more +complicated (and less documented) than the `Azure CLI`_. With +`Invoke`_ (as discussed above), ``az`` becomes incredibly easy to work +with. The Azure CLI lead developer states that they have `feature +parity `_ and that the +CLI is more straightforward to use. Considering our +ease-of-maintenance requirement, this seems the apt choice, especially +since the Azure CLI supports deploying resources with `ARM templates +`_. + +If it later becomes necessary to use the Python APIs directly, that +is, of course, still doable (and we can reuse existing code doing it). +This implementation can coexist as simply another class, ``AzureAPI``. + +On the topic of “servicing” the `Azure CLI`_, its developers state +that “at command level, packages only upgrading the PATCH version +guarantee backward compatibility.” The tool is also intended to be +used in scripts, so servicing would amount to documenting the tested +version and having the Azure class check that it’s compatible before +using it (or warning and then trying its best). + +How are requirements examined? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :py:attr:`~target.target.TargetData.features` attribute is +currently a list of strings and (combined with the +:py:attr:`~target.target.TargetData.params` dictionary) is used to +demonstrate how we can test if an existing target instance +(representing a deployed machine) met a test’s requirements. It should +be updated with a ``Requirements`` class that represents all physical +attributes of the target. The :py:mod:`target.plugin` module defines a +``@pytest.mark.target`` `pytest-mark`_ which takes the features list +but should instead take instances of this ``Requirements`` class. Two +``Requirements`` should be comparable to determine if one set meets +(or exceeds) the other set. Existing code that does this can be reused +for this. + +.. _pytest-mark: https://docs.pytest.org/en/stable/mark.html + +How do we share common tasks? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Common tasks for targets like rebooting and pinging should be +implemented on the :py:class:`~target.target.Target` class, and +platform-specific tasks on the respective subclass. + +Methods available from :py:attr:`~target.target.Target.conn` include +``run()`` and ``sudo()`` which are used to easily run arbitrary +commands, and ``get()`` and ``put()`` to download and upload arbitrary +files. + +The :py:meth:`~target.target.Target.cat` method wraps ``get()`` and +returns the file as data in a string. This makes test code like this +possible: + +.. code:: python + + assert target.cat("state.txt") == "TestCompleted" + +A ``reboot()`` method should be added that first tries to use +``sudo("reboot", timeout=5)`` (with a short timeout to avoid a hung SSH +session). It should retry with an exponential back-off to see if the +machine has rebooted by checking either ``uptime`` or the existence of a +file created before the reboot. This is to avoid having to ``sleep()`` +and just guess the amount of time it takes to reboot. + +The :py:meth:`~target.target.Target.restart` method should “power +cycle” the machine using the platform’s API, and thus is in abstract +method as each platform needs to implement it differently. + +Other tools and shared logic should be implemented as necessary. A major +area of concern is the automatic and package-manager agnostic +installation of necessary tools, much of which has been implemented +previously and can be reused. + +How are targets requested and managed? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In version 0.3 of this design document we detailed how we used a +session-scoped ``pool()`` fixture to manage targets across an entire +test session. This has since been replaced with an enhanced disk-based +`cache`_, accessed through a context manager with an atomic file lock: + +.. _cache: https://docs.pytest.org/en/stable/cache.html + +.. code:: python + + from filelock import FileLock + + @contextmanager + def target_pool(config: Config) -> Generator[Dict[str, Any], None, None]: + """Exclusive access to the cached targets pool.""" + assert config.cache is not None + lock = Path(config.cache.makedir("target")) / "pool.lock" + with FileLock(str(lock)): + pool = config.cache.get("target/pool", {}) + yield pool + config.cache.set("target/pool", pool) + +Note that the cross-session `cache`_ is provided by Pytest, and very +easy to work with. The key maps to a file path, and the data stored +and read is JSON. So our targets are serializable: internally the +`data class`_ :py:attr:`~target.target.TargetData` implements the +methods :py:meth:`~target.target.TargetData.to_json` and +:py:meth:`~target.target.TargetData.from_json`, and the +:py:func:`~target.plugin.target` fixture creates new instances of +:py:class:`~target.target.Target` for the requesting test from either +a “fit” cached target (and so locks it) or deploys a new target. + +.. _data class: https://docs.python.org/3/library/dataclasses.html + +.. code:: python + + @pytest.fixture + def target(request: SubRequest) -> Iterator[Target]: + ... + with target_pool(request.config) as pool: + for name, json in pool.items(): + if fits(TargetData(**json)): + t = Target.from_json(json) + t.locked = True + pool[t.name] = t.to_json() + # Or... + cls = Target.get_platform(params["platform"]) + t = cls(group, params, features, {}, i) + pool[t.name] = t.to_json() + +Because all access to the cache (and so the target pool) is within the +scope of the context manager, the access is locked in such a way that +this works with multiple Pytest processes, as used by `pytest-xdist`_ +and as necessary for parallel CPU-bound tasks (like testing multiple +targets) given Python’s `Global Interpreter Lock`_. Platform +implementations can save arbitrary JSON-serializable data to the +class’s :py:attr:`~target.target.TargetData.data` attribute and it +will be returned when recreated from the cache. + +.. _Global Interpreter Lock: https://wiki.python.org/moin/GlobalInterpreterLock + +While currently an unordered dictionary, to support optimal scheduling +we will likely want to use a priority queue, where the priority of a +target represents its cost (whether in terms of time or money), +allowing us to provide either the fastest or the cheapest target to +each request. By using the `pytest_collection_modifyitems`_ hook to +sort (and so group) the tests by their requirements, the tests would +efficiently reuse targets. Except for the most recently used target, +targets not in use (unlocked) should be deallocated. + +.. _pytest_collection_modifyitems: https://docs.pytest.org/en/latest/reference.html#pytest.hookspec.pytest_collection_modifyitems + +With the ``--keep-targets`` CLI flag the targets won’t be deleted at +the end of a run, and without it they will be automatically deleted. +Regardless, they will always be cached to disk when they are created +so that the CLI flag ``--delete-targets`` can delete *all* allocated +targets, even after a test session is interrupted. + +The fixture is indirectly parameterized during setup with the +:py:func:`~target.plugin.pytest_generate_tests` hook. Test and fixture +`parameterization`_ is a huge feature of Pytest. When we parameterize +the :py:func:`~target.plugin.target` fixture for multiple targets +(e.g. “Ubuntu” and “Debian”), Pytest automatically creates a set of +tests for each target. So ``test_smoke`` turns into +``test_smoke[Ubuntu]`` and ``test_smoke[Debian]``. This allows us to +run a collection of tests against multiple targets with ease. These +targets are defined in a YAML file (thanks to `pytest-playbook`_) and +validated against the parameters collected from the previously +described platform subclasses. + +Finally, once the :py:func:`~target.plugin.target` fixture has +returned a working and sanity-checked environment to the requesting +test, the test is capable of examining any and all attributes of the +:py:class:`~target.target.Target` and quickly marking itself as +skipped, expected to fail, or failed before executing the body of the +test. Our static type checking enables developers to ensure that the +platform they requested supports all methods and fields they use by +annotating the test’s ``target`` parameter with the expected platform +type (or types). Ensuring the effectiveness of this type checking will +require us to carefully update our platform implementations, and not +rely on arbitrary objects of data. (For example, add an +``internal_address`` field to ``AzureCLI``, don’t just look up +``data["internal_address"]``.) + +pytest-lisa +----------- + +How are tests described? +~~~~~~~~~~~~~~~~~~~~~~~~ + +The built-in `pytest-mark`_ plugin already provides functionality for +adding metadata to tests, where we specifically want (and describe +using `schema`_ :py:data:`~lisa.lisa_schema`): + +- Platform: used to skip tests inapplicable to the current + system-under-test +- Category: our high-level test organization +- Area: feature being tested +- Priority: self-explanatory +- Tags: optional additional metadata for test organization +- Features: a set of required features (like “GPU”) +- Reuse: a boolean to indicate if a target is reusable after the test +- Count: number of targets the test needs + +We simply reuse this with minimal logic to enforce our required +metadata, with sane defaults , and to list statistics about our test +coverage. It looks like this: + +.. code:: python + + from lisa import LISA + + @LISA(platform="Azure", category="Functional", area="deploy", priority=0) + def test_smoke(target: AzureCLI, caplog: LogCaptureFixture) -> None: + """Check that an Azure Linux VM can be deployed and is responsive. + +This is a functional example. With this simple decorator, all test +`collection hooks`_ can introspect the metadata, enforce required +parameters and set defaults, select tests based on arbitrary criteria, +and list test coverage statistics (test inventory). We validate the +metadata in :py:func:`lisa.pytest_collection_modifyitems`. + +.. _collection hooks: https://docs.pytest.org/en/latest/reference.html#collection-hooks + +Note that Pytest leverages Python’s docstrings for built-in +documentation (and can even run tests discovered in such strings, like +doctest). Hence we do not have a separate field for the test’s +documentation. By following the best practice of using docstrings for +our modules, classes, and functions, we can automatically to generate +full `documentation`_ for each plugin and test (which you are likely +currently reading). + +This mark also does need to be repeated for each test, as marks can be +scoped to a module, and so one line could describe defaults for every +test in a file, with individual tests overriding parameters as needed. + +In the current implementation, we take a ``features: List[str]`` +argument that is used to prove the concept deploying (or reusing) a +target based on the test’s required and the target’s available sets of +features, and it is passed to ``@pytest.mark.target``. See `How are +requirements examined?`_ for more. Coupled with the test’s requested +:py:func:`~target.plugin.target` fixture being parameterized (see +discussion in `pytest-target`_) this demonstrates at least one way we +can satisfy our “test run planner/scheduler” requirement. + +Furthermore, we have a prototype `generator +`_ which parses +LISAv2 XML test descriptions and generates stubs with this mark filled +in correctly. + +How are tests selected? +~~~~~~~~~~~~~~~~~~~~~~~ + +Pytest already allows a user to specify which exact tests to run: + +- Listing folders on the CLI (see below on where tests should live) +- Specifying a name expression on the CLI (e.g. ``-k smoke and xdp``) +- Specifying a mark expression on the CLI (e.g. ``-m functional and + not slow``) + +We can also implement any other mechanism via the +`pytest_collection_modifyitems`_ hook. The existing implementation in :py:mod:`lisa` +supports gathering selection criteria from a YAML file: + +.. code-block:: yaml + + criteria: + # Select all Priority 0 tests. + - priority: 0 + # Run tests with 'smoke' in the name twice. + - name: smoke + times: 2 + # Exclude all tests in Area "xdp" + - area: xdp + exclude: true + +This criteria is validated against following `schema`_ defined in +:py:func:`lisa.pytest_playbook_schema`. + +The test collection is then modified using the Pytest hook in +:py:func:`lisa.pytest_collection_modifyitems`. Because this is simply +a Python list, we can also sort the tests according to our needs, such +as by priority. If the `pytest-target`_ plugin has already sorted by +requirements, that’s just fine, Python’s ``sorted()`` built-in is +guaranteed to be stable (meaning we can sort in multiple passes). + +Together, the CLI support and YAML playbook satisfy one of our “test +entrance” requirements. We also generate our own binary called +``lisa`` which simply delegates to Pytest. + +How are results reported? +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Parsing the results of a large test suite can be difficult. +Fortunately, because Pytest is a testing framework, there already +exists support for generating excellent reports. For developers, the +`HTML report`_ is easy to read: it is self-contained, holds all the +results and logs, and each test can be expanded and collapsed. Tests +which were rerun are recorded separately. For CI pipelines, Pytest has +integrated `JUnit +`_ +XML test report support. This is the standard method of reporting +results to CI servers like Jenkins and are natively parsed into the CI +system’s built-in test display page. Finally, Azure DevOps pipelines +are even supported with a community plugin `pytest-azurepipelines +`_ which enhances the +standard JUnit report for ADO. + +.. _HTML report: https://pypi.org/project/pytest-html/ + +One of our requirements is to support the lookup of previous tests’ +execution metrics, such as recorded performance metrics and duration, +so that performance tests can check regressions. This is the perfect +example of carrying a small fixture which provides access to our +internal database and is dynamically added to our tests when run +internally, and the tests can lookup and record whatever they need +through the fixture. + +However, we also have internal requirements to report test results +throughout the test life cycle to a database (the “result manager” and +“progress tracker”) to be consumed by other tools. In this sense, +LISAv3 (the composition of our published plugins, tests, and fixtures) +is simply a producer, and the consumers can parse the test results, +send emails, archive the collected logs, update a GUI display of test +progress, etc. Our repository’s ``conftest.py`` can implement the +necessary logic using Pytest’s ample `test running hooks +`_. +In particular, the hook `pytest_runtest_makereport +`_ +is called for each of the setup, call and teardown phases of a test. +As such it can used for precisely this purpose. + +How is setup, run, and cleanup handled? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pytest strives to require minimal boiler-plate code. Thus the classic +“xunit-style” of defining a class with setup and teardown functions in +addition to test functions is not recommended (nor necessary). +Generally Pytest expects `fixtures`_ to be used for dependency +injection (which is what setup/teardown functions usually do). For +users that really want the classic style, it is nonetheless fully +`supported `_ and +documented (and can be applied at the module, class, and method +scopes). Thus our “test runner” requirement is satisfied. + +How are tests timed out? +~~~~~~~~~~~~~~~~~~~~~~~~ + +The `pytest-timeout `_ +plugin provides integrated timeouts via ``@pytest.mark.timeout()``, a configuration file option, environment variable, and +CLI flag. The Fabric library provides timeouts in both the +configuration and per-command usage. These are already used to +satisfaction in the prototype. Additionally, Pytest has built-in +support for measuring the duration of each fixture’s setup and +teardown and each test (it’s simply the ``--durations`` and +``--durations-min`` flags). + +How are tests organized? +~~~~~~~~~~~~~~~~~~~~~~~~ + +That is, what does a folder of tests map to: a platform, feature, or +owner? + +In the author’s opinion it is likely to be both. Tests which are +common to a platform and written by our team are probably best placed +in a folder like ``tests/azure`` whereas tests for a particular +scenario which limits their image and SKU applicability should be in a +folder like ``tests/acc``. It’s going to depend on how often the tests +are run together. + +Because Pytest can run tests and ``conftest.py`` files from arbitrary +folders, maintaining sets of tests and plugins separately from the +base LISA repository is easy. Custom repositories with new tests, +plugins, fixtures, platform-specific support, etc. can simply be +cloned anywhere, and provided on the command-line to Pytest. + +Test authors should keep tests which share requirements and are +otherwise similar to a single module (Python file). Not only is this +well-organized, but because marks can be applied at the module level, +setting all the tests to be skipped or expected to fail (with the +built-in ``skip`` and ``xfail`` Pytest marks) becomes even easier. + +An open question is if we really want to bring every test from LISAv2 +directly over, or if we should carefully analyze our tests to craft a +new set of high-level scenarios. An interesting result of reorganizing +and rewriting the tests would be the ability to have test layers, +where the result of a high-level test dictates if the tests below it +should be skipped. If it passes, it implies the tests underneath it +would pass, and so skips them; but if it fails, the next test below it +runs and so on until a passing layer is found. + +How will we port LISAv2 tests? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Given the above, we still must decide if we want to put the +engineering effort into porting *every* LISAv2 test. However, the +prototype started by porting the ``LIS-DRIVER-VERSION-CHECK`` test, +proving that tests which exclusively use Bash scripts are trivially +portable. Unfortunately, most tests use an associated PowerShell +script which is tightly coupled to the LISAv2 framework. + +We believe that it is *possible* to port these tests without untoward +modifications. We would need to write a mock library that implements +(or stubs where appropriate) LISAv2 framework functionality such as +``Provision-VMsForLisa``, ``Copy-RemoteFiles``, ``Run-LinuxCmd``, +etc., and provides both the expected “global” objects and the test +function parameters ``AllVmData`` and ``CurrentTestData``. But it +wouldn’t be great. + +This work needs to be done regardless of the approach we take with our +framework (leveraging Pytest or writing our own), and it is not +inconsequential work. It needs to be thoroughly planned and executed, +and is certainly a ways off. The author’s personal opinion is that we +won’t want to port most LISAv2 tests, and instead create a new set of +well-documented, comprehensive, layered tests that cover our current +needs, instead of bringing along all these historical tests. + +How are tests and functions retried? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Testing remote targets is inherently flaky, so we take a two-pronged +approach to dealing with the flakiness. + +The `pytest-rerunfailures`_ plugin can be used to easily mark a test +itself as flaky. It has the nice feature of recording each rerun in +the produced report. It looks like this: + +.. _pytest-rerunfailures: https://pypi.org/project/pytest-rerunfailures/ + +.. code:: python + + @pytest.mark.flaky(reruns=5) + def test_something_flaky(...): + """This fails most of the time.""" + ... + +Note that there is an open `bug +`_ in +this plugin which can cause issues with fixtures using scopes other +than “function” but it can be worked around (and we mostly use +“function” scope anyway). + +The `Tenacity`_ library is used to retry flaky functions that are not +tests, such as downloading boot diagnostics or pinging a node. As the +“modern Python retry library” it has easy-to-use decorators to retry +functions (and context managers to use within functions), as well as +excellent wait and timeout support. The +:py:meth:`~target.target.Target.ping` method looks like this: + +.. _Tenacity: https://tenacity.readthedocs.io/en/latest/ + +.. code:: python + + from tenacity import retry, stop_after_attempt, wait_exponential + + class Target: + ... + @retry(reraise=True, wait=wait_exponential(), stop=stop_after_attempt(3)) + def ping(self, **kwargs: Any) -> Result: + """Ping the node from the local system in a cross-platform manner.""" + flag = "-c 1" if platform.system() == "Linux" else "-n 1" + return self.local(f"ping {flag} {self.host}", **kwargs) + +We can additionally list a test twice when modifying the items +collection, as implemented in the criteria proof-of-concept. However, +given the above abilities, this may not be desired. + +How are tests executed in parallel? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While our original list of goals stated that we want to run tests “in +parallel” we were not specific about what was meant, and the topic of +parallelism and concurrency is understandably complex. We certainly +don’t mean running two tests at once on the same target, as this would +undoubtedly lead to flaky tests. + +Assuming that we care about a set of tests passing on a particular +image and size combination, but not necessarily on a particular +deployed instance, then we can run tests concurrently by deploying +multiple “identical” targets and splitting the tests across them. The +tests would still run in isolation on each target. This sounds hard, +but actually it’s practically free with Pytest via `pytest-xdist`_. + +.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist + +The default `pytest-xdist`_ implementation simply takes the list of +tests and runs them in a round-robin fashion with the desired number +of executors. We’ve talked at length about being able to schedule +groups of tests to run in particular executors and using particular +targets. While there are many paths open to us, this plugin actually +provides a hook, `pytest_xdist_make_scheduler +`_ +that exists specifically to “implement custom tests distribution +logic.” We used this to create the :py:class:`~lisa.LISAScheduling` +custom scheduler. + +Figuring out the requirements of our test scheduler and designing the +best algorithm will require further discussion and design review. For +the purposes of moving forward, we are not blocked, as the eventual +implementation can be dropped in-place with minimal effort. + +What are the user modes? +~~~~~~~~~~~~~~~~~~~~~~~~ + +Because Pytest is incredibly `customizable`_, we may want to provide a +few sets of reasonable default configurations for some common +scenarios. We should add a flag like +``--lisa-mode=[dev,debug,ci,demo]`` to change the default options and +output of Pytest. Doing so is readily supported by Pytest via the +`pytest_addoption`_ and `pytest_configure`_ hooks. We call these the +provided “user modes.” Note that by “output” we mean not just logging +(because that implies the Python ``logger`` module, which Pytest +allows full control over) but also commands’ ``stdout`` and ``stderr`` +as well as Pytest-provided information. + +As the current implementation stands, we just have sane defaults in +our repository’s ``pytest.ini``, and users who install and use our +plugins or tests can edit their own ``pytests.ini`` + +.. _customizable: https://docs.pytest.org/en/stable/customize.html +.. _pytest_addoption: https://docs.pytest.org/en/latest/reference.html#pytest.hookspec.pytest_addoption +.. _pytest_configure: https://docs.pytest.org/en/latest/reference.html#pytest.hookspec.pytest_configure + +- The dev(eloper) mode is intended for use by test developers while + writing a new test. It is verbose, caches the deployed VMs between + runs, and generates a digestible `HTML report`_ report. + +- The debug mode is like dev mode but with all possible information + shown, and will open the Python debugger automatically on failures + (which is provided by Pytest with the ``--pdb`` flag). + +- The CI mode will be fairly quiet on the console, showing all test + results, but putting the full info output into the generated report + file (HTML for sharing with humans and `JUnit + `_ + for the associated CI environment, which presents as native test + results). + +- The demo mode will show the “executive summary” (a lot like CI, but + finely tuned for demos). + +pytest-playbook +--------------- + +This plugin is simple, but exciting. The module :py:mod:`playbook` +defines a hook :py:meth:`playbook.Hooks.pytest_playbook_schema` which +other plugins (as discussed above) can use to add schemata to the +final playbook. In :py:meth:`playbook.pytest_configure`, all the +schemata are gathered and then the file given by ``--playbook=`` +is read, validated, and made available at :py:data:`playbook.data`. It +uses the `PyYAML `_ +library, but can be extended to support other formats. Also “YAML +Schema” section in :doc:`contributing guidelines ` on +how to generate the `JSON Schema `_ for use +with editors or for manual review. + +This is leveraging Pytest’s existing parameterization technology to +achieve one of our “test entrance” goals of requesting environments +with a YAML playbook, and one of our “test parameter validation” goals +of validating platforms before executing tests so that we can fail +fast if a target has insufficient information to be setup. Parsing the +same parameters from a CLI can also be implemented. + +What does the “flow” of Pytest look like? +----------------------------------------- + +This is best described in Pythonic pseudo-code, where the context +manager encapsulates each scope and the for loop encapsulates +processing: + +.. code:: python + + pool_fixture: a session-scoped context manager + target_fixture: a function-scoped context manager + items: a collection of tests + targets: a collection of targets + criteria: a collection of test selection criteria + + def pytest_addoption(parser): + """Add CLI options etc.""" + parser.addoption("--playbook", type=Path) + + pytest_addoption(parser) # Pytest fills in parser. + + def pytest_configure(config): + """Setup the run's configuration.""" + targets = playbook.get_targets() + criteria = playbook.get_criteria() + + pytest_configure(config) # Pytest fills in config. + + # pytest_generate_tests(metafunc) does this: + for test_metafunc in metafuncs: + for target in targets: + # items is tests * targets in size + items.append(test_metafunc[target]) + + # pytest_collection_modifyitems(session, config, items) does this: + for test in items: + validate(test) + include_or_exclude(test, criteria) + + # finally, each executor/session does this: + session_items = items.split() # based on scheduler algorithm + with pool_fixture as pool: + # the fixture has setup a pool to track the deployed targets + for test_function in session_items: + with target_fixture as target: + # the fixture has found or deployed an appropriate target + test_function(target) + +What Else? +---------- + +There’s still a lot more to think about and design. A non-exhaustive +list of future topics (some touched on above): + +- Terminology table +- Tests inventory (generating statistics from metadata) +- Environment / multiple targets class design +- Feature/requirement requests (NICs in particular) +- Custom test scheduler algorithm +- Secret management + +What alternatives were tried? +----------------------------- + +These are notes from things tried that did not work out, and why. + +Writing Another Framework +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The author believes the above set of technical specifications clearly +describes how we can leverage Pytest for our needs. Furthermore, the +existing implementation proves this is a viable option. Therefore he +does not think we should write and maintain a *new* Python testing +framework. We should avoid falling for “not invented here” syndrome. +The alternative prototype which implements a whole new testing +framework required over five thousand lines of code, and the +Pytest-based prototype used less than two hundred (now barely six +hundred as a full fledged implementation with three separate Pytest +plugins, even after extensive feature additions and refactors), or +less than three percent. + +We do not want to take on the maintenance cost of yet another +framework, the maintenance cost of LISAv2 already caused this mess in +the first place. The work of prototyping said new framework was +valuable, as it provided insight into the eventual technical design of +LISAv3, as laid out in this document. + +Using Remote Capabilities of ``pytest-xdist`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With the `pytest-xdist `_ +plugin there already exists support for running a folder of tests on an +arbitrary remote host via SSH. + +The LISA tests could be written as Python code suitable for running on +the target test system, which means direct access to the system in the +test code itself (subprocesses are still available, without having to +use SSH within the test, but would become far less necessary), something +that is not possible with any current prototype. Where the +``pytest-xdist`` plugin copies the package of code to the target node +and runs it, the pytest-lisa plugin could instantiate that node (boot +the necessary image on a remote machine or launch a new Hyper-V or Azure +VM, etc.) for the tests. + +However, this use of pytest-dist requires full Python support on the +target machines, and drastically changes how developers write tests. +Furthermore, it would not support running local commands against the +remote node (like ping) or running the test across a reboot of the node. +Thus we do not want to use this functionality of ``pytest-xdist``. That +said, ``pytest-xdist`` will still be useful for running tests +concurrently, as described above. + +Using Paramiko Instead of Fabric +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Paramiko library is less complex (smaller library footprint) than +Fabric, as the latter wraps the former, but it is a bit more difficult +to use, and doesn’t support reading existing SSH config files, nor does +it support “ProxyJump” which we use heavily. Fabric instead provides a +clean high-level interface for existing shell commands, handling all the +connection abstractions for us. + +Using Paramiko looked like this: + +.. code:: python + + from pathlib import Path + from typing import List + + from paramiko import SSHClient + + import pytest + + @pytest.fixture + def node() -> SSHClient: + with SSHClient() as client: + client.load_system_host_keys() + client.connect(hostname="...") + yield client + + + def test_lis_version(node: SSHClient) -> None: + with node.open_sftp() as sftp: + for f in ["utils.sh", "LIS-VERSION-CHECK.sh"]: + sftp.put(LINUX_SCRIPTS / f, f) + _, stdout, stderr = node.exec_command("./LIS-VERSION-CHECK.sh") + sftp.get("state.txt", "state.txt") + with Path("state.txt").open as f: + assert f.readline() == "TestCompleted" + +It is more verbose than necessary when compared to Fabric. + +StringIO +~~~~~~~~ + +For ``Node.cat()`` it would seem we could use ``StringIO`` like so: + +.. code:: python + + from io import StringIO + + with StringIO() as result: + node.get("state.txt", result) + assert result.getvalue().strip() == "TestCompleted" + +However, the data returned by Paramiko is in bytes, which in Python 3 +are not equivalent to strings, hence the existing implementation which +uses ``BytesIO`` and decodes the bytes to a string. diff --git a/LICENSE-DOCS.md b/LICENSE-DOCS.md new file mode 100644 index 0000000000..9912bb9c01 --- /dev/null +++ b/LICENSE-DOCS.md @@ -0,0 +1,395 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..b2f52a2bad --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index babe1e1ada..44f8f6ad79 100644 --- a/README.md +++ b/README.md @@ -1,156 +1,48 @@ -# Linux Integration Services Automation 3.0 (LISAv3) - -[![CI Workflow for LISAv3](https://github.com/LIS/LISAv2/workflows/CI%20Workflow%20for%20LISAv3/badge.svg?branch=main)](https://github.com/LIS/LISAv2/actions?query=workflow%3A%22CI+Workflow+for+LISAv3%22+event%3Apush+branch%3Amain) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![GitHub license](https://img.shields.io/github/license/LIS/LISAv2)](https://github.com/LIS/LISAv2/blob/main/LICENSE-2.0.txt) - -LISA is a Linux test automation framework with built-in test cases to verify the quality of -Linux distributions on multiple platforms (such as Azure, On-prem HyperV, and Linux bare metal). - -This version (v3) is a brand new implementation in Python 3, supports running on Windows and Linux distros. - -## Getting Started - -### Install Python 3 - -Install [Python 3.8](https://www.python.org/) from Linux distribution’s package -repositories, or [python.org](https://www.python.org/). We also need `gcc` and -the `libpython` headers for the `usjon` package. - -On Ubuntu 18.04 and 20.04: - -```bash -sudo apt install git python3.8 libpython3.8-dev python3-distutils python3-apt gcc -``` - -NOTE: If you are using WSL, installing Poetry on both Windows and Linux may -cause both platforms’ versions of Poetry to be on your path, as Windows binaries -are mapped into WSL’s `PATH`. This means that the Linux `poetry` binary _must_ -appear in your `PATH` before the Windows version, or this error will appear: - -> `/usr/bin/env: ‘python\r’: No such file or directory` - -### Install Poetry - -On Linux (or WSL): - -```bash -curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 - --preview --version 1.1.3 -source $HOME/.poetry/env -``` - -On Windows (in PowerShell): - -```powershell -(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - --preview --version 1.1.3 -# the path can be added to system, so it applies to every terminal. -$env:PATH += ";$env:USERPROFILE\.poetry\bin" -``` - -If you already have Poetry installed, you can update it like: - -```bash -poetry self update 1.1.3 -``` - -### Install Python packages - -Then use Poetry to install LISAv3's Python package dependencies: - -```bash -git clone -b main https://github.com/LIS/LISAv2.git -cd LISAv2 -poetry install -``` - -To obtain the path of the Poetry virtual environment setup for LISA (where the -isolated Python installation and packages are located), run: - -```bash -poetry env list --full-path -``` - -### Run LISAv3 - -Run LISAv3 using Poetry’s environment: - -```bash -poetry run python lisa/main.py -``` - -### Make - -We now also have a GNU Makefile that automates some tasks. Try: -```bash -# Install Python packages -make setup - -# Run LISAv3 -make run - -# Run unit tests -make test - -# Run syntactic, semantic, formatting and type checkers -make check - -# Generate coverage report (slow, reruns LISAv3 and tests) -make coverage - -# Print current Python virtualenv -make venv -``` - -### Editor Setup - -Install and enable [ShellCheck](https://github.com/koalaman/shellcheck) to find -bash errors locally. - -#### Visual Studio Code - -First, click the Python version in the bottom left, then enter the path emitted -by the command above. This will point Code to the Poetry virtual environment. - -Make sure below settings are in root level of `.vscode/settings.json` - -```json -{ - "python.analysis.typeCheckingMode": "strict", - "python.formatting.provider": "black", - "python.linting.enabled": true, - "python.linting.flake8Enabled": true, - "python.linting.mypyEnabled": true, - "python.linting.pylintEnabled": false, - "editor.formatOnSave": true, - "python.linting.mypyArgs": [ - "--strict", - "--namespace-packages", - "--show-column-numbers", - ], - "python.sortImports.path": "isort", - "python.analysis.useLibraryCodeForTypes": false, - "python.analysis.autoImportCompletions": false, - "files.eol": "\n", -} -``` - -#### Emacs - -Use the [pyvenv](https://github.com/jorgenschaefer/pyvenv) package: - -```emacs-lisp -(use-package pyvenv - :ensure t - :hook (python-mode . pyvenv-tracking-mode)) -``` - -Then run `M-x add-dir-local-variable RET python-mode RET pyvenv-activate RET -` where the value is the path given by the command above. -This will create a `.dir-locals.el` file which looks like this: - -```emacs-lisp -;;; Directory Local Variables -;;; For more information see (info "(emacs) Directory Variables") - -((python-mode . ((pyvenv-activate . "~/.cache/pypoetry/virtualenvs/lisa-s7Q404Ij-py3.8")))) -``` +# Linux Integration Services Automation + +LISA is a Linux test automation framework with built-in test cases to verify the +quality of Linux distributions on multiple platforms (such as Azure, Hyper-V, +and bare metal). + +See the [documentation](https://microsoft.github.io/lisa) for details. + +## Contributor License Agreement + +This project welcomes contributions and suggestions. Most contributions require +you to agree to a Contributor License Agreement (CLA) declaring that you have +the right to, and actually do, grant us the rights to use your contribution. For +details, visit . + +When you submit a pull request, a CLA bot will automatically determine whether +you need to provide a CLA and decorate the PR appropriately (e.g., status check, +comment). Simply follow the instructions provided by the bot. You will only need +to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of +Conduct](https://opensource.microsoft.com/codeofconduct/). For more information +see the [Code of Conduct +FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact + with any additional questions or comments. + +## Legal Notices + +Microsoft and any contributors grant you a license to the Microsoft +documentation and other content in this repository under the [Creative Commons +Attribution 4.0 International Public +License](https://creativecommons.org/licenses/by/4.0/legalcode), see the +[LICENSE-DOCS](LICENSE-DOCS.md) file, and grant you a license to any code in the +repository under the [MIT License](https://opensource.org/licenses/MIT), see the +[LICENSE](LICENSE.md) file. + +Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services +referenced in the documentation may be either trademarks or registered +trademarks of Microsoft in the United States and/or other countries. The +licenses for this project do not grant you rights to use any Microsoft names, +logos, or trademarks. Microsoft's general trademark guidelines can be found at +. + +Privacy information can be found at + +Microsoft and any contributors reserve all other rights, whether under their +respective copyrights, patents, or trademarks, whether by implication, estoppel +or otherwise. diff --git a/USAGE.rst b/USAGE.rst new file mode 100644 index 0000000000..eef66afe70 --- /dev/null +++ b/USAGE.rst @@ -0,0 +1,449 @@ +How to Use Pytest and LISA +========================== + +LISA is supported on almost any Linux or Windows installation provided +Python 3.7 (released in 2018) or newer is available and SSH can be +used to connect to the remote targets under test. The local SSH +configuration is respected so ``ProxyJump`` can be used. + +Install Python 3.7+ +------------------- + +Install Python 3.7 or newer from your Linux distribution’s package +repositories, or `python.org `_. + +On Ubuntu 20.04 and up, just run ``apt install python-is-python3``. + +Below that Ubuntu version, the ``python3`` package is out-of-date, so +use something like a `PPA`_ or `pyenv`_. + +.. _PPA: https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa +.. _pyenv: https://github.com/pyenv/pyenv + +Install Poetry +-------------- + +`Poetry `_ is our preferred tool for +Python dependency management and packaging. We’ll use it to +automatically setup a virtual environment and install everything. + +On Linux (or WSL) +~~~~~~~~~~~~~~~~~ + +.. code:: bash + + curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - + source $HOME/.poetry/env + +If you are using WSL, installing Poetry on both Windows and Linux may +cause both platforms’ versions of Poetry to be on your path, as Windows +binaries are mapped into WSL’s ``PATH``. This means that the Linux +``poetry`` binary *must* appear in your ``PATH`` before the Windows +version, or this error will appear: + +:: + + `/usr/bin/env: ‘python\r’: No such file or directory` + +Adjust your ``PATH`` appropriately to fix it. + +On Windows (in PowerShell) +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: powershell + + (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - + $env:PATH += ";$env:USERPROFILE\.poetry\bin" + +Clone LISA and ``cd`` into the Git repo +--------------------------------------- + +.. code:: bash + + git clone -b andschwa/pytest https://github.com/microsoft/lisa.git + cd lisa + +Install Python dependencies +--------------------------- + +Now we’ll use ``poetry`` to install all the necessary packages. Note +that we have a number of developer dependencies specified to make your +life easier when :doc:`contributing `, but you can +exclude these (and their potential additional requirements) with the +flag ``--no-dev``. Once installed, we use ``poetry shell`` to enter a +sub-shell with the Python virtual environment setup. + +.. code:: bash + + # Install the Python packages + poetry install + + # Enter the virtual environment + poetry shell + +Use LISA +-------- + +Under the covers ``lisa`` is just ``pytest``! Run ``lisa --help`` for +all available options, and refer to the Pytest `usage`_ documentation. +LISA is generally run with a :py:mod:`playbook` which is a `YAML file +`_ specifying a list of +remote targets, their parameters, and optionally a set of test +selection criteria. The ``demo.yaml`` looks like: + +.. _usage: https://docs.pytest.org/en/stable/usage.html + +.. code:: yaml + + platforms: + AzureCLI: + sku: Standard_DS2_v2 + + targets: + - name: Debian + platform: AzureCLI + image: credativ:Debian:9:9.0.201706190 + + - name: Ubuntu + platform: AzureCLI + image: Canonical:UbuntuServer:18.04-LTS:latest + + criteria: + - module: test_smoke_b + +The ``platforms`` key is used to set default parameters for +targets using that platform; in this case, the SKU is set to +``Standard_DS2_v2``. + +The ``targets`` key defines a number of targets on which the selected +tests will run. Here we’re asking for two targets using the same +:py:class:`~target.azure.AzureCLI` platform, both will use the same +default for the SKU, but different images. The ``name`` is just a +user-provided friendly name that is appended to the parameterized +tests and will show up in test results. + +The ``criteria`` key can be used to select a tests instead of using +Pytest’s CLI test selection interface. In this case we’re selecting +all tests from the module (Python file) named ``test_smoke_b``, one of +the examples of an Azure VM smoke test, and it looks like this: + +.. code:: python + + from __future__ import annotations # For type checking. + + import typing + + if typing.TYPE_CHECKING: + from target import AzureCLI + from _pytest.logging import LogCaptureFixture + from pathlib import Path + + import logging + import socket + import time + + from invoke.runners import CommandTimedOut, UnexpectedExit # type: ignore + from paramiko import SSHException # type: ignore + + from lisa import LISA + + + @LISA(platform="Azure", category="Functional", area="deploy", priority=0) + def test_smoke(target: AzureCLI, caplog: LogCaptureFixture, tmp_path: Path) -> None: + """Check that an Azure Linux VM can be deployed and is responsive. + + This example uses exactly one function for the entire test, which + means we have to catch failures that don't fail the test, and + instead emit warnings. It works, and it's closer to how LISAv2 + would have implemented it, but it's less Pythonic. For a more + "modern" example, see `test_smoke_a.py`. + + 1. Deploy the VM (via `target` fixture). + 2. Ping the VM. + 3. Connect to the VM via SSH. + 4. Attempt to reboot via SSH, otherwise use the platform. + 5. Fetch the serial console logs AKA boot diagnostics. + + SSH failures DO NOT fail this test. + + """ + # Capture INFO and above logs for this test. + caplog.set_level(logging.INFO) + + logging.info("Pinging before reboot...") + ping1 = target.ping() + + ssh_errors = (TimeoutError, CommandTimedOut, SSHException, socket.error) + + try: + logging.info("SSHing before reboot...") + target.conn.open() + except ssh_errors as e: + logging.warning(f"SSH before reboot failed: '{e}'") + + reboot_exit = 0 + try: + logging.info("Rebooting...") + # If this succeeds, we should expect the exit code to be -1 + reboot_exit = target.conn.sudo("reboot", timeout=5).exited + except ssh_errors as e: + logging.warning(f"SSH failed, using platform to reboot: '{e}'") + target.platform_restart() + except UnexpectedExit: + # TODO: How do we differentiate reboot working and the SSH + # connection disconnecting for other reasons? + if reboot_exit != -1: + logging.warning("While SSH worked, 'reboot' command failed") + + # TODO: We should check something more concrete here instead of + # sleeping an arbitrary amount of time. + logging.info("Sleeping for 10 seconds after reboot...") + time.sleep(10) + + logging.info("Pinging after reboot...") + ping2 = target.ping() + + try: + logging.info("SSHing after reboot...") + target.conn.open() + except ssh_errors as e: + logging.warning(f"SSH after reboot failed: '{e}'") + + logging.info("Retrieving boot diagnostics...") + path = tmp_path / "diagnostics.txt" + try: + # NOTE: It’s actually more interesting to emit the downloaded + # boot diagnostics to `stdout` as they’re then captured in the + # HTML report, but this is to demo using `tmp_path`. + diagnostics = target.get_boot_diagnostics(hide=True) + path.write_text(diagnostics.stdout) + except UnexpectedExit: + logging.warning("Retrieving boot diagnostics failed.") + else: + logging.info(f"See '{path}' for boot diagnostics.") + + # NOTE: The test criteria is to fail only if ping fails. + assert ping1.ok, f"Pinging {target.host} before reboot failed" + assert ping2.ok, f"Pinging {target.host} after reboot failed" + + +Enable Azure +~~~~~~~~~~~~ + +Before running this demo, we will need to set up the `Azure CLI +`_ because this platform uses it. Install +it if you do not already have, then ensure it is logged in with your +choice of authentication, and set a default subscription, which will +be used to deploy the resources. + +.. code:: bash + + # Install Azure CLI, make sure `az` is in your `PATH` + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + + # Login and set subscription + az login + az account set -s + +Run the Demo +~~~~~~~~~~~~ + +Now we can run the demo! + +.. code:: bash + + # Run a demo which deploys Azure resources + lisa --playbook=playbooks/demo.yaml --keep-targets --html=demo.html + +This will sequentially deploy the two requested targets and run the +smoke test against them, printing the stdout, stderr, and logging of +all tests after they complete (see below for how to change this +behavior). The ``--keep-targets`` flag comes from the :py:mod:`target` +plugin and instructs it to cache the deployed targets between test +runs. Delete them by running ``lisa --delete-targets``. The +``--html=demo.html`` flag will cause an easy-to-read HTML report to be +written to ``demo.html``. + +It should look very similar to this slightly redacted example: + +:: + + $ lisa --playbook=playbooks/demo.yaml --keep-targets --html=demo.html + =========================== test session starts ========================== + collected 40 items / 38 deselected / 2 selected + + testsuites/test_smoke_b.py F. [100%] + + ================================ FAILURES ================================ + _______________________ test_smoke[Target=Debian] ________________________ + testsuites/test_smoke_b.py:93: in test_smoke + assert ping2.ok, f"Pinging {target.host} after reboot failed" + E AssertionError: Pinging 40.123.27.161 after reboot failed + E assert False + E + where False = .ok + ------------------------- Captured stdout setup -------------------------- + az vm create -g pytest-d6056453-a28c-4fec-8225-7c7aab02c84a-rg -n pytest-d6056453-a28c-4fec-8225-7c7aab02c84a-0 --image credativ:Debian:9:9.0.201706190 --size Standard_DS2_v2 --boot-diagnostics-storage pytestbootdiag --generate-ssh-keys + { + "fqdns": "", + "id": "/subscriptions/<...>/resourceGroups/pytest-d6056453-a28c-4fec-8225-7c7aab02c84a-rg/providers/Microsoft.Compute/virtualMachines/pytest-d6056453-a28c-4fec-8225-7c7aab02c84a-0", + "location": "eastus2", + "macAddress": "00-0D-3A-DE-07-17", + "powerState": "VM running", + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "<...>", + "resourceGroup": "pytest-d6056453-a28c-4fec-8225-7c7aab02c84a-rg", + "zones": "" + } + -------------------------- Captured stdout call -------------------------- + ping -c 1 40.123.27.161 + PING 40.123.27.161 (40.123.27.161) 56(84) bytes of data. + + --- 40.123.27.161 ping statistics --- + 1 packets transmitted, 0 received, 100% packet loss, time 0ms + ... + ping -c 1 40.123.27.161 + PING 40.123.27.161 (40.123.27.161) 56(84) bytes of data. + 64 bytes from 40.123.27.161: icmp_seq=1 ttl=43 time=85.6 ms + + --- 40.123.27.161 ping statistics --- + 1 packets transmitted, 1 received, 0% packet loss, time 0ms + rtt min/avg/max/mdev = 85.562/85.562/85.562/0.000 ms + ping -c 1 40.123.27.161 + PING 40.123.27.161 (40.123.27.161) 56(84) bytes of data. + + --- 40.123.27.161 ping statistics --- + 1 packets transmitted, 0 received, 100% packet loss, time 0ms + ... + ping -c 1 40.123.27.161 + PING 40.123.27.161 (40.123.27.161) 56(84) bytes of data. + + --- 40.123.27.161 ping statistics --- + 1 packets transmitted, 0 received, 100% packet loss, time 0ms + + --------------------------- Captured log call ---------------------------- + 2021-01-20 17:14:56 INFO Pinging before reboot... + 2021-01-20 17:15:51 INFO SSHing before reboot... + 2021-01-20 17:15:52 INFO Connected (version 2.0, client OpenSSH_7.4p1) + 2021-01-20 17:15:53 INFO Authentication (publickey) successful! + 2021-01-20 17:15:53 INFO Rebooting... + 2021-01-20 17:15:53 WARNING While SSH worked, 'reboot' command failed + 2021-01-20 17:15:53 INFO Sleeping for 10 seconds after reboot... + 2021-01-20 17:16:03 INFO Pinging after reboot... + 2021-01-20 17:17:08 INFO SSHing after reboot... + 2021-01-20 17:19:16 ERROR Secsh channel 1 open FAILED: Connection timed out: Connect failed + 2021-01-20 17:19:16 WARNING SSH after reboot failed: 'ChannelException(2, 'Connect failed')' + 2021-01-20 17:19:16 INFO Retrieving boot diagnostics... + 2021-01-20 17:19:20 INFO See '/tmp/pytest-of-andschwa/pytest-181/test_smoke_Target_Debian_0/diagnostics.txt' for boot diagnostics. + ================================= PASSES ================================= + _______________________ test_smoke[Target=Ubuntu] ________________________ + ------------------------- Captured stdout setup -------------------------- + az vm create -g pytest-8f173841-d702-432e-bd32-f09a984bd3ab-rg -n pytest-8f173841-d702-432e-bd32-f09a984bd3ab-0 --image Canonical:UbuntuServer:18.04-LTS:latest --size Standard_DS2_v2 --boot-diagnostics-storage pytestbootdiag --generate-ssh-keys + { + "fqdns": "", + "id": "/subscriptions/<..>/resourceGroups/pytest-8f173841-d702-432e-bd32-f09a984bd3ab-rg/providers/Microsoft.Compute/virtualMachines/pytest-8f173841-d702-432e-bd32-f09a984bd3ab-0", + "location": "eastus2", + "macAddress": "00-0D-3A-7C-85-59", + "powerState": "VM running", + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "<...>", + "resourceGroup": "pytest-8f173841-d702-432e-bd32-f09a984bd3ab-rg", + "zones": "" + } + -------------------------- Captured stdout call -------------------------- + ping -c 1 137.116.51.62 + PING 137.116.51.62 (137.116.51.62) 56(84) bytes of data. + + --- 137.116.51.62 ping statistics --- + 1 packets transmitted, 0 received, 100% packet loss, time 0ms + ... + ping -c 1 137.116.51.62 + PING 137.116.51.62 (137.116.51.62) 56(84) bytes of data. + 64 bytes from 137.116.51.62: icmp_seq=1 ttl=42 time=84.0 ms + + --- 137.116.51.62 ping statistics --- + 1 packets transmitted, 1 received, 0% packet loss, time 0ms + rtt min/avg/max/mdev = 84.004/84.004/84.004/0.000 ms + --------------------------- Captured log call ---------------------------- + 2021-01-20 17:20:26 INFO Pinging before reboot... + 2021-01-20 17:21:21 INFO SSHing before reboot... + 2021-01-20 17:21:21 INFO Connected (version 2.0, client OpenSSH_7.6p1) + 2021-01-20 17:21:22 INFO Authentication (publickey) successful! + 2021-01-20 17:21:22 INFO Rebooting... + 2021-01-20 17:21:24 WARNING While SSH worked, 'reboot' command failed + 2021-01-20 17:21:24 INFO Sleeping for 10 seconds after reboot... + 2021-01-20 17:21:34 INFO Pinging after reboot... + 2021-01-20 17:21:45 INFO SSHing after reboot... + 2021-01-20 17:21:46 INFO Connected (version 2.0, client OpenSSH_7.6p1) + 2021-01-20 17:21:46 INFO Authentication (publickey) successful! + 2021-01-20 17:21:46 INFO Retrieving boot diagnostics... + 2021-01-20 17:21:50 INFO See '/tmp/pytest-of-andschwa/pytest-181/test_smoke_Target_Ubuntu_0/diagnostics.txt' for boot diagnostics. + ----- generated html file: file:///home/andschwa/src/lisa/demo.html ------ + ======================== short test summary info ========================= + PASSED testsuites/test_smoke_b.py::test_smoke[Target=Ubuntu] + FAILED testsuites/test_smoke_b.py::test_smoke[Target=Debian] - AssertionError: Pinging 40.123.27.161 after reboot failed + ========= 1 failed, 1 passed, 38 deselected in 541.93s (0:09:01) ========= + +Settings +-------- + +Our opinionated `usage`_ settings are in ``pytest.ini``. Adjust them +(or override them on the CLI) as you see fit! They include: + +``--no-header`` + + For more succinct display, we suppress the default Pytest header + with the platform, root directory, plugins, and timeout + information. + +``--tb=short`` + + Since we’re generally testing commands on remote systems, we don’t + care about the full Python trace when a test fails, so we set the + `traceback printing + `_ + to be short. + +``-rA`` + + We want the status (and captured logs) of *all* tests printed in + the final summary, but Pytest defaults to failed and errored tests + with ``fE``, hence our use of ``A``. + +``timeout = 1200`` + + Since we run our tests on remote machines which may hang, we use + `pytest-timeout `_ to + cancel any tests that exceed 20 minutes. Note that the + :py:class:`target` class also has a “timeout” configuration for + individual commands using `Invoke `_. + +Suggestions +~~~~~~~~~~~ + +Test developers may wish to run with the flags: + +``--capture=tee-sys`` + + This will `capture + `_ all writes to + ``sys.stdout`` and ``sys.stderr``, but also pass them to ``sys`` + such that they’re printed *live* (useful when writing tests, but + annoying when running tests). + +``log_cli=true`` + + Pytest can emit captured `logs + `_ live too. Add + this to ``pytest.ini`` (and adjust the level and format as + desired). + +``--tb=auto`` + + To show the full traceback instead of just a line. + +``--html=path/to/report.html`` + + We include `pytest-html + `_ as a dependency + so users can generate HTML reports with all captured stdout, + stderr, traceback, and logs. diff --git a/conf.py b/conf.py new file mode 100644 index 0000000000..bc8872f14b --- /dev/null +++ b/conf.py @@ -0,0 +1,98 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# type: ignore +"""Configuration file for the Sphinx documentation builder. + +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" +import importlib.metadata +from datetime import datetime + +metadata = importlib.metadata.metadata("LISA") + +project = metadata["Name"].upper() +copyright = f"{datetime.now().year} Microsoft Corporation" +author = metadata["Author"] +version = metadata["Version"] +release = version + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "recommonmark", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.githubpages", + "sphinx.ext.linkcode", + "sphinx.ext.viewcode", + "sphinx.ext.todo", +] + +# Setup autodoc default options. +autodoc_member_order = "bysource" +autodoc_default_options = { + "members": True, + "member-order": "bysource", + "private-members": False, + "special-members": "__init__", + "ignore-module-all": True, + "undoc-members": True, + "show-inheritance": True, +} + +# Scan all found documents for autosummary directives, and generate +# stub pages for each, instead of using `sphinx-autogen` directly. +autosummary_generate = True + + +def linkcode_resolve(domain, info): + """Configure linkcode extension.""" + if domain != "py": + return None + module = info["module"] + if not module: + return None + + # Map to subfolders. + if module.startswith("lisa"): + folder = "pytest-lisa" + elif module.startswith("target"): + folder = "pytest-target" + elif module.startswith("playbook"): + folder = "pytest-playbook" + else: + folder = "" + + filename = module.replace(".", "/") + # NOTE: Note `metadata["Project-Url"]` because we need the base. + url = "https://github.com/microsoft/lisa" + # TODO: Update this branch to `main` branch after PR is merged. + branch = "andschwa/pytest" + return f"{url}/blob/{branch}/{folder}/{filename}.py" + + +# Generate TODO list. +todo_include_todos = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", ".archive", ".pytest_cache", "dist"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000000..ba780e5725 --- /dev/null +++ b/conftest.py @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""LISA tests' specific configurations go here. + +This file is essentially the staging ground for contributions to +`pytest-lisa`, the plugin (and package). Anything that is reusable and +stable should be sent upstream. + +""" +from __future__ import annotations + +import logging +import typing + +if typing.TYPE_CHECKING: + from pytest_html.plugin import HTMLReport # type: ignore + + +def pytest_html_report_title(report: HTMLReport) -> None: + """pytest-html hook to set the HTML report title.""" + report.title = "LISAv3 Results" + + +def pytest_configure() -> None: + """Pytest hook for plugin configuration.""" + # Flake8 is noisy so we make it quieter here. + logging.getLogger("flake8").setLevel(logging.WARNING) diff --git a/index.rst b/index.rst new file mode 100644 index 0000000000..fc0994f38e --- /dev/null +++ b/index.rst @@ -0,0 +1,57 @@ +Linux Integration Services Automation +===================================== + +|LISA/Pytest CI Workflow| |Code Style: black| + +LISA is a Linux test automation framework with built-in test cases to +verify the quality of Linux distributions on multiple platforms (such +as Azure, Hyper-V, and bare metal). It is an opinionated collection of +custom `Pytest `_ plugins, +configurations, and tests. See the :doc:`technical specification +document ` for details, and the `GitHub repository`_ for +sources. + +.. _GitHub repository: https://github.com/microsoft/lisa/tree/andschwa/pytest + +.. toctree:: + :maxdepth: 3 + :caption: Documentation + :hidden: + + Usage + Design + Contributing + Code of Conduct + +Getting Started +--------------- + +See the :doc:`usage document ` for how to setup the +requirements, run tests, and write new tests. + +Python Modules +-------------- + +See the :doc:`technical specification document ` for design +details, and see the below table for auto-generated API documentation +of the framework. + +.. autosummary:: + :toctree: modules + :caption: API + :recursive: + + lisa + target + playbook + +Contributing +------------ + +See the :doc:`contributing guidelines ` for developer +information! + +.. |LISA/Pytest CI Workflow| image:: https://github.com/microsoft/lisa/workflows/LISA/Pytest%20CI%20Workflow/badge.svg?branch=andschwa%2Fpytest + :target: https://github.com/microsoft/lisa/actions?query=workflow%3A%22LISA%2FPytest+CI+Workflow%22 +.. |Code Style: black| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000000..6b513808fb --- /dev/null +++ b/mypy.ini @@ -0,0 +1,20 @@ +[mypy] +namespace_packages = True +pretty = True + +warn_unused_configs = True +disallow_any_generics = True +disallow_subclassing_any = False +disallow_untyped_calls = True +disallow_untyped_defs = True +disallow_incomplete_defs = True +check_untyped_defs = True +disallow_untyped_decorators = False +no_implicit_optional = True +warn_redundant_casts = True +warn_unused_ignores = True +warn_return_any = True +no_implicit_reexport = True +strict_equality = True + +warn_unreachable = True diff --git a/playbooks/criteria.yaml b/playbooks/criteria.yaml new file mode 100644 index 0000000000..3e86bc538d --- /dev/null +++ b/playbooks/criteria.yaml @@ -0,0 +1,10 @@ +# NOTE: This is an adjusted proof-of-concept ask from Chi. +criteria: + # Select all Priority 0 tests. + - priority: 0 + # Run tests with 'smoke' in the name twice. + - name: smoke + times: 2 + # Exclude all tests in Area "xdp" + - area: xdp + exclude: true diff --git a/playbooks/demo.yaml b/playbooks/demo.yaml new file mode 100644 index 0000000000..74eca3e6f3 --- /dev/null +++ b/playbooks/demo.yaml @@ -0,0 +1,15 @@ +platforms: + AzureCLI: + sku: Standard_DS2_v2 + +targets: + - name: Debian + platform: AzureCLI + image: credativ:Debian:9:9.0.201706190 + + - name: Ubuntu + platform: AzureCLI + image: Canonical:UbuntuServer:18.04-LTS:latest + +criteria: + - module: test_smoke_b diff --git a/playbooks/schema.json b/playbooks/schema.json new file mode 100644 index 0000000000..630b374c79 --- /dev/null +++ b/playbooks/schema.json @@ -0,0 +1,195 @@ +{ + "type": "object", + "properties": { + "criteria": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "default": null + }, + "module": { + "type": "string", + "default": null + }, + "area": { + "type": "string", + "default": null + }, + "category": { + "type": "string", + "default": null + }, + "priority": { + "type": "integer", + "default": null + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "default": "" + }, + "times": { + "type": "integer", + "default": 1 + }, + "exclude": { + "type": "boolean", + "default": false + } + }, + "required": [], + "additionalProperties": false + }, + "default": "" + }, + "platforms": { + "type": "object", + "properties": { + "SSH": { + "$ref": "#/definitions/SSH_Defaults", + "default": { + "host": "localhost" + } + }, + "AzureCLI": { + "$ref": "#/definitions/AzureCLI_Defaults", + "default": { + "location": "eastus2", + "sku": "Standard_DS1_v2", + "networking": "", + "image": "UbuntuLTS" + } + } + }, + "required": [], + "additionalProperties": false, + "default": { + "AzureCLI": { + "location": "eastus2", + "sku": "Standard_DS1_v2", + "networking": "", + "image": "UbuntuLTS" + }, + "SSH": { + "host": "localhost" + } + } + }, + "targets": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/SSH_Schema" + }, + { + "$ref": "#/definitions/AzureCLI_Schema" + } + ] + }, + "default": [ + { + "name": "Default", + "platform": "SSH" + } + ] + } + }, + "required": [], + "additionalProperties": false, + "$id": "schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "SSH_Defaults": { + "type": "object", + "properties": { + "host": { + "type": "string", + "default": "localhost" + } + }, + "required": [], + "additionalProperties": false + }, + "AzureCLI_Defaults": { + "type": "object", + "properties": { + "image": { + "type": "string", + "default": "UbuntuLTS" + }, + "sku": { + "type": "string", + "default": "Standard_DS1_v2" + }, + "location": { + "type": "string", + "default": "eastus2" + }, + "networking": { + "type": "string", + "default": "" + } + }, + "required": [], + "additionalProperties": false + }, + "SSH_Schema": { + "type": "object", + "properties": { + "name": { + "description": "A friendly name for the target.", + "type": "string" + }, + "platform": { + "description": "The class name of the platform implementation.", + "const": "SSH" + }, + "host": { + "type": "string" + } + }, + "required": [ + "name", + "platform" + ], + "additionalProperties": false + }, + "AzureCLI_Schema": { + "type": "object", + "properties": { + "name": { + "description": "A friendly name for the target.", + "type": "string" + }, + "platform": { + "description": "The class name of the platform implementation.", + "const": "AzureCLI" + }, + "image": { + "type": "string" + }, + "sku": { + "type": "string" + }, + "location": { + "type": "string" + }, + "networking": { + "type": "string" + } + }, + "required": [ + "name", + "platform", + "image" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/playbooks/test.yaml b/playbooks/test.yaml new file mode 100644 index 0000000000..5c32b14290 --- /dev/null +++ b/playbooks/test.yaml @@ -0,0 +1,5 @@ +targets: + - name: Local Tests + platform: SSH + - name: Setup Plan + platform: Custom diff --git a/poetry.lock b/poetry.lock index 5930f5aa3d..43548f04e5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,16 +1,18 @@ [[package]] -name = "adal" -version = "1.2.5" -description = "Note: This library is already replaced by MSAL Python, available here: https://pypi.org/project/msal/ .ADAL Python remains available here as a legacy. The ADAL for Python library makes it easy for python application to authenticate to Azure Active Directory (AAD) in order to access AAD protected web resources." -category = "main" +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" optional = false python-versions = "*" -[package.dependencies] -cryptography = ">=1.1.0" -PyJWT = ">=1.0.0" -python-dateutil = ">=2.1.0" -requests = ">=2.0.0" +[[package]] +name = "apipkg" +version = "1.5" +description = "apipkg: namespace control and lazy-import mechanism" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "appdirs" @@ -21,151 +23,37 @@ optional = false python-versions = "*" [[package]] -name = "asserts" -version = "0.11.1" -description = "Stand-alone Assertions" -category = "main" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "asttokens" -version = "2.0.4" -description = "Annotate AST trees with source code positions" +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." category = "main" optional = false -python-versions = "*" - -[package.dependencies] -six = "*" - -[package.extras] -test = ["astroid", "pytest"] +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "20.2.0" +version = "20.3.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] [[package]] -name = "azure-common" -version = "1.1.25" -description = "Microsoft Azure Client Library for Python (Common)" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "azure-core" -version = "1.8.2" -description = "Microsoft Azure Core Library for Python" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -requests = ">=2.18.4" -six = ">=1.6" - -[[package]] -name = "azure-identity" -version = "1.5.0b2" -description = "Microsoft Azure Identity Library for Python" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -azure-core = ">=1.0.0,<2.0.0" -cryptography = ">=2.1.4" -msal = ">=1.3.0,<1.6.0" -msal-extensions = ">=0.3.0,<0.4.0" -six = ">=1.6" - -[[package]] -name = "azure-mgmt-compute" -version = "17.0.0" -description = "Microsoft Azure Compute Management Client Library for Python" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.2.0,<2.0.0" -msrest = ">=0.5.0" - -[[package]] -name = "azure-mgmt-core" -version = "1.2.1" -description = "Microsoft Azure Management Core Library for Python" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -azure-core = ">=1.8.2a,<2.0.0" - -[[package]] -name = "azure-mgmt-marketplaceordering" -version = "0.2.1" -description = "Microsoft Azure Market Place Ordering Client Library for Python" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -azure-common = ">=1.1,<2.0" -msrest = ">=0.5.0" -msrestazure = ">=0.4.32,<2.0.0" - -[[package]] -name = "azure-mgmt-network" -version = "16.0.0" -description = "Microsoft Azure Network Management Client Library for Python" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.2.0,<2.0.0" -msrest = ">=0.5.0" - -[[package]] -name = "azure-mgmt-resource" -version = "15.0.0" -description = "Microsoft Azure Resource Management Client Library for Python" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.2.0,<2.0.0" -msrest = ">=0.5.0" - -[[package]] -name = "azure-mgmt-storage" -version = "16.0.0" -description = "Microsoft Azure Storage Management Client Library for Python" -category = "main" +name = "babel" +version = "2.9.0" +description = "Internationalization utilities" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] -azure-common = ">=1.1,<2.0" -azure-mgmt-core = ">=1.2.0,<2.0.0" -msrest = ">=0.5.0" +pytz = ">=2015.7" [[package]] name = "bcrypt" @@ -207,15 +95,15 @@ d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] [[package]] name = "certifi" -version = "2020.6.20" +version = "2020.12.5" description = "Python package for providing Mozilla's CA Bundle." -category = "main" +category = "dev" optional = false python-versions = "*" [[package]] name = "cffi" -version = "1.14.3" +version = "1.14.4" description = "Foreign Function Interface for Python calling C code." category = "main" optional = false @@ -226,11 +114,11 @@ pycparser = "*" [[package]] name = "chardet" -version = "3.0.4" +version = "4.0.0" description = "Universal encoding detector for Python 2 and 3" -category = "main" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "click" @@ -241,26 +129,42 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] -name = "coverage" -version = "5.3" -description = "Code coverage measurement for Python" +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = "*" [package.extras] -toml = ["toml"] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "contextlib2" +version = "0.6.0.post1" +description = "Backports and enhancements for the contextlib module" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "cryptography" -version = "3.2.1" +version = "3.3.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" [package.dependencies] -cffi = ">=1.8,<1.11.3 || >1.11.3" +cffi = ">=1.12" six = ">=1.4.1" [package.extras] @@ -271,29 +175,50 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] -name = "dataclasses-json" -version = "0.5.2" -description = "Easily serialize dataclasses to and from JSON" +name = "docutils" +version = "0.16" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "execnet" +version = "1.7.1" +description = "execnet: rapid multi-Python deployment" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] -marshmallow = ">=3.3.0,<4.0.0" -marshmallow-enum = ">=1.5.1,<2.0.0" -stringcase = "1.2.0" -typing-inspect = ">=0.4.0" +apipkg = ">=1.4" [package.extras] -dev = ["pytest", "ipython", "mypy (>=0.710)", "hypothesis", "portray", "flake8", "simplejson"] +testing = ["pre-commit"] [[package]] -name = "decorator" -version = "4.4.2" -description = "Decorators for Humans" +name = "fabric" +version = "2.5.0" +description = "High level SSH command execution" category = "main" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" +python-versions = "*" + +[package.dependencies] +invoke = ">=1.3,<2.0" +paramiko = ">=2.4" + +[package.extras] +pytest = ["mock (>=2.0.0,<3.0)", "pytest (>=3.2.5,<4.0)"] +testing = ["mock (>=2.0.0,<3.0)"] + +[[package]] +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." +category = "main" +optional = false +python-versions = "*" [[package]] name = "flake8" @@ -304,6 +229,7 @@ optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.6.0a1,<2.7.0" pyflakes = ">=2.2.0,<2.3.0" @@ -322,7 +248,7 @@ flake8 = ">=3.0.0" [[package]] name = "flake8-bugbear" -version = "20.1.4" +version = "20.11.1" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." category = "dev" optional = false @@ -332,6 +258,9 @@ python-versions = ">=3.6" attrs = ">=19.2.0" flake8 = ">=3.0.0" +[package.extras] +dev = ["coverage", "black", "hypothesis", "hypothesmith"] + [[package]] name = "flake8-isort" version = "4.0.0" @@ -349,41 +278,56 @@ testfixtures = ">=6.8.0,<7" test = ["pytest (>=4.0.2,<6)", "toml"] [[package]] -name = "icontract" -version = "2.3.6" -description = "Provide design-by-contract with informative violation messages." +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "imagesize" +version = "1.2.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "importlib-metadata" +version = "3.4.0" +description = "Read metadata from Python packages" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [package.dependencies] -asttokens = ">=2,<3" +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" [package.extras] -dev = ["mypy (==0.750)", "pylint (==2.3.1)", "yapf (==0.20.2)", "tox (>=3.0.0)", "pydocstyle (>=2.1.1,<3)", "coverage (>=4.5.1,<5)", "docutils (>=0.14,<1)", "pygments (>=2.2.0,<3)", "dpcontracts (==0.6.0)", "tabulate (>=0.8.7,<1)", "py-cpuinfo (>=5.0.0,<6)", "deal (==4.1.0)"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] -name = "idna" -version = "2.10" -description = "Internationalized Domain Names in Applications (IDNA)" +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "*" [[package]] -name = "isodate" -version = "0.6.0" -description = "An ISO 8601 date/time/duration parser and formatter" +name = "invoke" +version = "1.5.0" +description = "Pythonic task execution" category = "main" optional = false python-versions = "*" -[package.dependencies] -six = "*" - [[package]] name = "isort" -version = "5.6.4" +version = "5.7.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -410,29 +354,26 @@ qa = ["flake8 (==3.7.9)"] testing = ["Django (<3.1)", "colorama", "docopt", "pytest (>=3.9.0,<5.0.0)"] [[package]] -name = "marshmallow" -version = "3.9.0" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -category = "main" +name = "jinja2" +version = "2.11.2" +description = "A very fast and expressive template engine." +category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +MarkupSafe = ">=0.23" [package.extras] -dev = ["pytest", "pytz", "simplejson", "mypy (==0.790)", "flake8 (==3.8.4)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] -docs = ["sphinx (==3.2.1)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.1)"] -lint = ["mypy (==0.790)", "flake8 (==3.8.4)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)"] -tests = ["pytest", "pytz", "simplejson"] +i18n = ["Babel (>=0.8)"] [[package]] -name = "marshmallow-enum" -version = "1.5.1" -description = "Enum field for Marshmallow" -category = "main" +name = "markupsafe" +version = "1.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" optional = false -python-versions = "*" - -[package.dependencies] -marshmallow = ">=2.0.0" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" [[package]] name = "mccabe" @@ -442,63 +383,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "msal" -version = "1.5.1" -description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -PyJWT = {version = ">=1.0.0,<2", extras = ["crypto"]} -requests = ">=2.0.0,<3" - -[[package]] -name = "msal-extensions" -version = "0.3.0" -description = "" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -msal = ">=0.4.1,<2.0.0" -portalocker = [ - {version = ">=1.0,<2.0", markers = "platform_system != \"Windows\""}, - {version = ">=1.6,<2.0", markers = "platform_system == \"Windows\""}, -] - -[[package]] -name = "msrest" -version = "0.6.19" -description = "AutoRest swagger generator Python client runtime." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -certifi = ">=2017.4.17" -isodate = ">=0.6.0" -requests = ">=2.16,<3.0" -requests-oauthlib = ">=0.5.0" - -[package.extras] -async = ["aiohttp (>=3.0)", "aiodns"] - -[[package]] -name = "msrestazure" -version = "0.6.4" -description = "AutoRest swagger generator Python client runtime. Azure-specific module." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -adal = ">=0.6.0,<2.0.0" -msrest = ">=0.6.0,<2.0.0" -six = "*" - [[package]] name = "mypy" version = "0.782" @@ -519,22 +403,20 @@ dmypy = ["psutil (>=4.0)"] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "main" +category = "dev" optional = false python-versions = "*" [[package]] -name = "oauthlib" -version = "3.1.0" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +name = "packaging" +version = "20.8" +description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[package.extras] -rsa = ["cryptography"] -signals = ["blinker"] -signedtoken = ["cryptography", "pyjwt (>=1.0.0)"] +[package.dependencies] +pyparsing = ">=2.0.2" [[package]] name = "paramiko" @@ -568,7 +450,7 @@ testing = ["docopt", "pytest (>=3.0.7)"] [[package]] name = "pathspec" -version = "0.8.0" +version = "0.8.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -578,42 +460,19 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.extras] -dev = ["pre-commit", "tox"] - -[[package]] -name = "portalocker" -version = "1.7.1" -description = "Wraps the portalocker recipe for easy usage" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] -pywin32 = {version = "!=226", markers = "platform_system == \"Windows\""} - -[package.extras] -docs = ["sphinx (>=1.7.1)"] -tests = ["pytest (>=4.6.9)", "pytest-cov (>=2.8.1)", "sphinx (>=1.8.5)", "pytest-flake8 (>=1.0.5)"] - -[[package]] -name = "psutil" -version = "5.7.3" -description = "Cross-platform lib for process and system monitoring in Python." -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] +dev = ["pre-commit", "tox"] [[package]] name = "py" -version = "1.9.0" +version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "main" optional = false @@ -644,20 +503,12 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "pyjwt" -version = "1.7.1" -description = "JSON Web Token implementation in Python" -category = "main" +name = "pygments" +version = "2.7.3" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" optional = false -python-versions = "*" - -[package.dependencies] -cryptography = {version = ">=1.4", optional = true, markers = "extra == \"crypto\""} - -[package.extras] -crypto = ["cryptography (>=1.4)"] -flake8 = ["flake8", "flake8-import-order", "pep8-naming"] -test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"] +python-versions = ">=3.5" [[package]] name = "pyls-black" @@ -719,15 +570,194 @@ docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] [[package]] -name = "python-dateutil" -version = "2.8.1" -description = "Extensions to the standard Python datetime module" +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.1" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-flake8" +version = "1.0.7" +description = "pytest plugin to check FLAKE8 requirements" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3.5" +pytest = ">=3.5" + +[[package]] +name = "pytest-forked" +version = "1.3.0" +description = "run tests in isolated forked subprocesses" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +py = "*" +pytest = ">=3.10" + +[[package]] +name = "pytest-html" +version = "2.1.1" +description = "pytest plugin for generating HTML reports" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=5.0" +pytest-metadata = "*" + +[[package]] +name = "pytest-lisa" +version = "0.1.0" +description = "Pytest plugin for Linux Integration Services Automation (LISA)." +category = "main" +optional = false +python-versions = "^3.7" +develop = true + +[package.dependencies] +pytest = "^6.1.2" +pytest-playbook = "0.1.0" +pytest-xdist = "^2.1.0" +schema = "0.7.2" + +[package.source] +type = "directory" +url = "pytest-lisa" + +[[package]] +name = "pytest-metadata" +version = "1.11.0" +description = "pytest plugin for test session metadata" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +pytest = ">=2.9.0" + +[[package]] +name = "pytest-mypy" +version = "0.7.0" +description = "Mypy static type checker plugin for Pytest" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +filelock = ">=3.0" +mypy = [ + {version = ">=0.500", markers = "python_version < \"3.8\""}, + {version = ">=0.700", markers = "python_version >= \"3.8\""}, +] +pytest = ">=3.5" + +[[package]] +name = "pytest-playbook" +version = "0.1.0" +description = "Pytest plugin for reading playbooks." +category = "main" +optional = false +python-versions = "^3.7" +develop = true + +[package.dependencies] +pytest = "^6.1.2" +PyYAML = "^5.3.1" +schema = "0.7.2" + +[package.source] +type = "directory" +url = "pytest-playbook" + +[[package]] +name = "pytest-rerunfailures" +version = "9.1.1" +description = "pytest plugin to re-run tests to eliminate flaky failures" category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +python-versions = ">=3.5" [package.dependencies] -six = ">=1.5" +pytest = ">=5.0" + +[[package]] +name = "pytest-target" +version = "0.1.0" +description = "Pytest plugin for remote target orchestration." +category = "main" +optional = false +python-versions = "^3.7" +develop = true + +[package.dependencies] +fabric = "^2.5.0" +filelock = "^3.0.12" +invoke = "^1.4.1" +pytest = "^6.1.2" +pytest-playbook = "0.1.0" +tenacity = "^6.2.0" + +[package.source] +type = "directory" +url = "pytest-target" + +[[package]] +name = "pytest-timeout" +version = "1.4.2" +description = "py.test plugin to abort hanging tests" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pytest = ">=3.6.0" + +[[package]] +name = "pytest-xdist" +version = "2.2.0" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.0.0" +pytest-forked = "*" + +[package.extras] +psutil = ["psutil (>=3.0)"] +testing = ["filelock"] [[package]] name = "python-jsonrpc-server" @@ -770,10 +800,10 @@ test = ["versioneer", "pylint (>=2.5.0)", "pytest", "mock", "pytest-cov", "cover yapf = ["yapf"] [[package]] -name = "pywin32" -version = "228" -description = "Python for Window Extensions" -category = "main" +name = "pytz" +version = "2020.5" +description = "World timezone definitions, modern and historical" +category = "dev" optional = false python-versions = "*" @@ -785,9 +815,22 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "recommonmark" +version = "0.7.1" +description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +commonmark = ">=0.8.1" +docutils = ">=0.11" +sphinx = ">=1.3.1" + [[package]] name = "regex" -version = "2020.10.28" +version = "2020.11.13" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -795,59 +838,43 @@ python-versions = "*" [[package]] name = "requests" -version = "2.24.0" +version = "2.25.1" description = "Python HTTP for Humans." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" +chardet = ">=3.0.2,<5" idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +urllib3 = ">=1.21.1,<1.27" [package.extras] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] [[package]] -name = "requests-oauthlib" -version = "1.3.0" -description = "OAuthlib authentication support for Requests." -category = "main" +name = "rope" +version = "0.18.0" +description = "a python refactoring library..." +category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -oauthlib = ">=3.0.0" -requests = ">=2.0.0" +python-versions = "*" [package.extras] -rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +dev = ["pytest"] [[package]] -name = "retry" -version = "0.9.2" -description = "Easy to use retry decorator." +name = "schema" +version = "0.7.2" +description = "Simple data validation library" category = "main" optional = false python-versions = "*" [package.dependencies] -decorator = ">=3.4.2" -py = ">=1.4.26,<2.0.0" - -[[package]] -name = "rope" -version = "0.17.0" -description = "a python refactoring library..." -category = "dev" -optional = false -python-versions = "*" - -[package.extras] -dev = ["pytest"] +contextlib2 = ">=0.5.5" [[package]] name = "six" @@ -858,55 +885,132 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] -name = "spur" -version = "0.3.20" -description = "Run commands and manipulate files locally or over SSH using the same interface" -category = "main" +name = "snowballstemmer" +version = "2.0.0" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +category = "dev" optional = false python-versions = "*" +[[package]] +name = "sphinx" +version = "3.4.3" +description = "Python documentation generator" +category = "dev" +optional = false +python-versions = ">=3.5" + [package.dependencies] -paramiko = ">=1.13.1,<3" +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.12" +imagesize = "*" +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.790)", "docutils-stubs"] +test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] [[package]] -name = "spurplus" -version = "2.3.4" -description = "Manage remote machines and file operations over SSH." -category = "main" +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" -[package.dependencies] -icontract = ">=2.0.1,<3" -spur = "0.3.20" -temppathlib = ">=1.0.3,<2" -typing_extensions = ">=3.6.2.1" +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "dev" +optional = false +python-versions = ">=3.5" [package.extras] -dev = ["mypy (==0.790)", "pylint (==2.6.0)", "yapf (==0.20.2)", "tox (>=3.0.0)", "coverage (>=4.5.1,<5)", "pydocstyle (>=2.1.1,<3)"] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] [[package]] -name = "stringcase" -version = "1.2.0" -description = "String case converter." -category = "main" +name = "sphinxcontrib-htmlhelp" +version = "1.0.3" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] [[package]] -name = "temppathlib" -version = "1.0.4" -description = "Wrap tempfile to give you pathlib.Path." +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.4" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "tenacity" +version = "6.3.1" +description = "Retry code until it succeeds" category = "main" optional = false python-versions = "*" +[package.dependencies] +six = ">=1.9.0" + [package.extras] -dev = ["mypy (==0.790)", "pylint (==2.6.0)", "yapf (==0.20.2)", "tox (>=3,<4)", "coverage (>=5,<6)", "pydocstyle (>=5,<6)", "twine"] +doc = ["reno", "sphinx", "tornado (>=4.5)"] [[package]] name = "testfixtures" -version = "6.15.0" +version = "6.17.0" description = "A collection of helpers and mock objects for unit tests and doc tests." category = "dev" optional = false @@ -921,13 +1025,13 @@ test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybi name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" +category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typed-ast" -version = "1.4.1" +version = "1.4.2" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false @@ -941,18 +1045,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "typing-inspect" -version = "0.6.0" -description = "Runtime inspection utilities for typing module." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -mypy-extensions = ">=0.3.0" -typing-extensions = ">=3.7.4" - [[package]] name = "ujson" version = "4.0.1" @@ -963,9 +1055,9 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.25.11" +version = "1.26.2" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" @@ -974,66 +1066,47 @@ brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "zipp" +version = "3.4.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + [metadata] lock-version = "1.1" -python-versions = "^3.8" -content-hash = "a44f707696fdb38afd23be8723eccd02e6aa375323aafaf2a995b586901a2ac8" +python-versions = "^3.7" +content-hash = "01f1cdbe9414f75f0366106f0b049986cde6d756a9e9614835b56c0abec3ee0f" [metadata.files] -adal = [ - {file = "adal-1.2.5-py2.py3-none-any.whl", hash = "sha256:7492aff8f0ba7dd4e1c477303295c645141540fff34c3ca6de0a0b0e6c1c122a"}, - {file = "adal-1.2.5.tar.gz", hash = "sha256:8003ba03ef04170195b3eddda8a5ab43649ef2c5f0287023d515affb1ccfcfc3"}, +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +apipkg = [ + {file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"}, + {file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"}, ] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] -asserts = [ - {file = "asserts-0.11.1-py2.py3-none-any.whl", hash = "sha256:dfe3a45a311c727f516e9375eac85e5f5dd1df846b60ae52f559d8534b24df5d"}, -] -asttokens = [ - {file = "asttokens-2.0.4-py2.py3-none-any.whl", hash = "sha256:766d3352908730efb20b95ae22db0f1cb1bedb67c6071fcffb5c236ea673f2f7"}, - {file = "asttokens-2.0.4.tar.gz", hash = "sha256:a42e57e28f2ac1c85ed9b1f84109401427e5c63c04f61d15b8842b027eec5128"}, +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, - {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, -] -azure-common = [ - {file = "azure-common-1.1.25.zip", hash = "sha256:ce0f1013e6d0e9faebaf3188cc069f4892fc60a6ec552e3f817c1a2f92835054"}, - {file = "azure_common-1.1.25-py2.py3-none-any.whl", hash = "sha256:fd02e4256dc9cdd2d4422bc795bdca2ef302f7a86148b154fbf4ea1f09da400a"}, -] -azure-core = [ - {file = "azure-core-1.8.2.zip", hash = "sha256:621b53271f7988b766f8a7d7f7a2c44241e3d2c1d8db13e68089d6da6241748e"}, - {file = "azure_core-1.8.2-py2.py3-none-any.whl", hash = "sha256:be23d411e19874f375c2ef0327c452be10b1e9a1023ac6afe334598f2920136b"}, -] -azure-identity = [ - {file = "azure-identity-1.5.0b2.zip", hash = "sha256:095873d61c6a4474831794b6232f156c255ff883cdd14195dc3d2c5f823559ae"}, - {file = "azure_identity-1.5.0b2-py2.py3-none-any.whl", hash = "sha256:ea415bf5b76ea160f311d8a89c0b97200c48d74046ec4e393883351dbbf01056"}, -] -azure-mgmt-compute = [ - {file = "azure-mgmt-compute-17.0.0.zip", hash = "sha256:c7350b404e5d10a548ceddb034394c8fad6c852ce33a3d3b211065813c1da404"}, - {file = "azure_mgmt_compute-17.0.0-py2.py3-none-any.whl", hash = "sha256:d84e048bb56448d94e6827213321c08a006c39e58c698aee6e4e83961d50680a"}, -] -azure-mgmt-core = [ - {file = "azure-mgmt-core-1.2.1.zip", hash = "sha256:a3906fa77edfedfcc3229dc3b69489d5ed63b107c7eacbc50092e6cbfbfd83f0"}, - {file = "azure_mgmt_core-1.2.1-py2.py3-none-any.whl", hash = "sha256:bd4503a2d81b86780f15936af2e4244c1345062f4c2422f0b377b56cb80d7796"}, + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, ] -azure-mgmt-marketplaceordering = [ - {file = "azure-mgmt-marketplaceordering-0.2.1.zip", hash = "sha256:dc765cde7ec03efe456438c85c6207c2f77775a8ce8a7adb19b0df5c5dc513c2"}, - {file = "azure_mgmt_marketplaceordering-0.2.1-py2.py3-none-any.whl", hash = "sha256:12d595f3dbda90de7cbc08ace99b925124ce675219b32bb3fde90e36d357c095"}, -] -azure-mgmt-network = [ - {file = "azure-mgmt-network-16.0.0.zip", hash = "sha256:6159a8c44590cc58841690c27c7d4acb0cd9ad0a1e5178c1d35e0f48e3c3c0e9"}, - {file = "azure_mgmt_network-16.0.0-py2.py3-none-any.whl", hash = "sha256:c0e8358e9d530790dbf3efef6b31bce26e664de5096cbd84c62845067da815d1"}, -] -azure-mgmt-resource = [ - {file = "azure-mgmt-resource-15.0.0.zip", hash = "sha256:80ecb69aa21152b924edf481e4b26c641f11aa264120bc322a14284811df9c14"}, - {file = "azure_mgmt_resource-15.0.0-py2.py3-none-any.whl", hash = "sha256:15bb46ef4b197426ce7ce8080a490c997111f3f3efe9f728cf8dc420982d54a2"}, -] -azure-mgmt-storage = [ - {file = "azure-mgmt-storage-16.0.0.zip", hash = "sha256:2f9d714d9722b1ef4bac6563676612e6e795c4e90f6f3cd323616fdadb0a99e5"}, - {file = "azure_mgmt_storage-16.0.0-py2.py3-none-any.whl", hash = "sha256:a819e421d50c0b58416b551d3e9e9a9cf6029714cf977ffaaee86a37572e7113"}, +babel = [ + {file = "Babel-2.9.0-py2.py3-none-any.whl", hash = "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5"}, + {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"}, ] bcrypt = [ {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, @@ -1048,122 +1121,98 @@ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] certifi = [ - {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, - {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, ] cffi = [ - {file = "cffi-1.14.3-2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3eeeb0405fd145e714f7633a5173318bd88d8bbfc3dd0a5751f8c4f70ae629bc"}, - {file = "cffi-1.14.3-2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:cb763ceceae04803adcc4e2d80d611ef201c73da32d8f2722e9d0ab0c7f10768"}, - {file = "cffi-1.14.3-2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f60519595eaca110f248e5017363d751b12782a6f2bd6a7041cba275215f5d"}, - {file = "cffi-1.14.3-2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c53af463f4a40de78c58b8b2710ade243c81cbca641e34debf3396a9640d6ec1"}, - {file = "cffi-1.14.3-2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:33c6cdc071ba5cd6d96769c8969a0531be2d08c2628a0143a10a7dcffa9719ca"}, - {file = "cffi-1.14.3-2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c11579638288e53fc94ad60022ff1b67865363e730ee41ad5e6f0a17188b327a"}, - {file = "cffi-1.14.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3cb3e1b9ec43256c4e0f8d2837267a70b0e1ca8c4f456685508ae6106b1f504c"}, - {file = "cffi-1.14.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f0620511387790860b249b9241c2f13c3a80e21a73e0b861a2df24e9d6f56730"}, - {file = "cffi-1.14.3-cp27-cp27m-win32.whl", hash = "sha256:005f2bfe11b6745d726dbb07ace4d53f057de66e336ff92d61b8c7e9c8f4777d"}, - {file = "cffi-1.14.3-cp27-cp27m-win_amd64.whl", hash = "sha256:2f9674623ca39c9ebe38afa3da402e9326c245f0f5ceff0623dccdac15023e05"}, - {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:09e96138280241bd355cd585148dec04dbbedb4f46128f340d696eaafc82dd7b"}, - {file = "cffi-1.14.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3363e77a6176afb8823b6e06db78c46dbc4c7813b00a41300a4873b6ba63b171"}, - {file = "cffi-1.14.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0ef488305fdce2580c8b2708f22d7785ae222d9825d3094ab073e22e93dfe51f"}, - {file = "cffi-1.14.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:0b1ad452cc824665ddc682400b62c9e4f5b64736a2ba99110712fdee5f2505c4"}, - {file = "cffi-1.14.3-cp35-cp35m-win32.whl", hash = "sha256:85ba797e1de5b48aa5a8427b6ba62cf69607c18c5d4eb747604b7302f1ec382d"}, - {file = "cffi-1.14.3-cp35-cp35m-win_amd64.whl", hash = "sha256:e66399cf0fc07de4dce4f588fc25bfe84a6d1285cc544e67987d22663393926d"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:15f351bed09897fbda218e4db5a3d5c06328862f6198d4fb385f3e14e19decb3"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4d7c26bfc1ea9f92084a1d75e11999e97b62d63128bcc90c3624d07813c52808"}, - {file = "cffi-1.14.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:23e5d2040367322824605bc29ae8ee9175200b92cb5483ac7d466927a9b3d537"}, - {file = "cffi-1.14.3-cp36-cp36m-win32.whl", hash = "sha256:a624fae282e81ad2e4871bdb767e2c914d0539708c0f078b5b355258293c98b0"}, - {file = "cffi-1.14.3-cp36-cp36m-win_amd64.whl", hash = "sha256:de31b5164d44ef4943db155b3e8e17929707cac1e5bd2f363e67a56e3af4af6e"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f92cdecb618e5fa4658aeb97d5eb3d2f47aa94ac6477c6daf0f306c5a3b9e6b1"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:22399ff4870fb4c7ef19fff6eeb20a8bbf15571913c181c78cb361024d574579"}, - {file = "cffi-1.14.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f4eae045e6ab2bb54ca279733fe4eb85f1effda392666308250714e01907f394"}, - {file = "cffi-1.14.3-cp37-cp37m-win32.whl", hash = "sha256:b0358e6fefc74a16f745afa366acc89f979040e0cbc4eec55ab26ad1f6a9bfbc"}, - {file = "cffi-1.14.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6642f15ad963b5092d65aed022d033c77763515fdc07095208f15d3563003869"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2791f68edc5749024b4722500e86303a10d342527e1e3bcac47f35fbd25b764e"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:529c4ed2e10437c205f38f3691a68be66c39197d01062618c55f74294a4a4828"}, - {file = "cffi-1.14.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0f1e499e4000c4c347a124fa6a27d37608ced4fe9f7d45070563b7c4c370c9"}, - {file = "cffi-1.14.3-cp38-cp38-win32.whl", hash = "sha256:3b8eaf915ddc0709779889c472e553f0d3e8b7bdf62dab764c8921b09bf94522"}, - {file = "cffi-1.14.3-cp38-cp38-win_amd64.whl", hash = "sha256:bbd2f4dfee1079f76943767fce837ade3087b578aeb9f69aec7857d5bf25db15"}, - {file = "cffi-1.14.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:cc75f58cdaf043fe6a7a6c04b3b5a0e694c6a9e24050967747251fb80d7bce0d"}, - {file = "cffi-1.14.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:bf39a9e19ce7298f1bd6a9758fa99707e9e5b1ebe5e90f2c3913a47bc548747c"}, - {file = "cffi-1.14.3-cp39-cp39-win32.whl", hash = "sha256:d80998ed59176e8cba74028762fbd9b9153b9afc71ea118e63bbf5d4d0f9552b"}, - {file = "cffi-1.14.3-cp39-cp39-win_amd64.whl", hash = "sha256:c150eaa3dadbb2b5339675b88d4573c1be3cb6f2c33a6c83387e10cc0bf05bd3"}, - {file = "cffi-1.14.3.tar.gz", hash = "sha256:f92f789e4f9241cd262ad7a555ca2c648a98178a953af117ef7fad46aa1d5591"}, + {file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"}, + {file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"}, + {file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"}, + {file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"}, + {file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"}, + {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"}, + {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"}, + {file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"}, + {file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"}, + {file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"}, + {file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"}, + {file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"}, + {file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"}, + {file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"}, + {file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"}, + {file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01"}, + {file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"}, + {file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"}, + {file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e"}, + {file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"}, + {file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"}, + {file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"}, + {file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"}, + {file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"}, + {file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"}, + {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"}, + {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"}, ] chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] -coverage = [ - {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"}, - {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"}, - {file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"}, - {file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"}, - {file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"}, - {file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"}, - {file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"}, - {file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"}, - {file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"}, - {file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"}, - {file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"}, - {file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"}, - {file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"}, - {file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"}, - {file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"}, - {file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"}, - {file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"}, - {file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"}, - {file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"}, - {file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"}, - {file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"}, - {file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"}, - {file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"}, - {file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"}, - {file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"}, - {file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"}, - {file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"}, - {file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"}, - {file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"}, - {file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"}, - {file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"}, - {file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"}, - {file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"}, - {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"}, +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] +contextlib2 = [ + {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, + {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, ] cryptography = [ - {file = "cryptography-3.2.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7"}, - {file = "cryptography-3.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d"}, - {file = "cryptography-3.2.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33"}, - {file = "cryptography-3.2.1-cp27-cp27m-win32.whl", hash = "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297"}, - {file = "cryptography-3.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4"}, - {file = "cryptography-3.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8"}, - {file = "cryptography-3.2.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e"}, - {file = "cryptography-3.2.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b"}, - {file = "cryptography-3.2.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df"}, - {file = "cryptography-3.2.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77"}, - {file = "cryptography-3.2.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7"}, - {file = "cryptography-3.2.1-cp35-cp35m-win32.whl", hash = "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b"}, - {file = "cryptography-3.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7"}, - {file = "cryptography-3.2.1-cp36-abi3-win32.whl", hash = "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f"}, - {file = "cryptography-3.2.1-cp36-abi3-win_amd64.whl", hash = "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538"}, - {file = "cryptography-3.2.1-cp36-cp36m-win32.whl", hash = "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b"}, - {file = "cryptography-3.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13"}, - {file = "cryptography-3.2.1-cp37-cp37m-win32.whl", hash = "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e"}, - {file = "cryptography-3.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb"}, - {file = "cryptography-3.2.1-cp38-cp38-win32.whl", hash = "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b"}, - {file = "cryptography-3.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851"}, - {file = "cryptography-3.2.1.tar.gz", hash = "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3"}, -] -dataclasses-json = [ - {file = "dataclasses-json-0.5.2.tar.gz", hash = "sha256:56ec931959ede74b5dedf65cf20772e6a79764d20c404794cce0111c88c085ff"}, - {file = "dataclasses_json-0.5.2-py3-none-any.whl", hash = "sha256:b746c48d9d8e884e2a0ffa59c6220a1b21f94d4f9f12c839da0a8a0efd36dc19"}, -] -decorator = [ - {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, - {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, + {file = "cryptography-3.3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030"}, + {file = "cryptography-3.3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0"}, + {file = "cryptography-3.3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812"}, + {file = "cryptography-3.3.1-cp27-cp27m-win32.whl", hash = "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e"}, + {file = "cryptography-3.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901"}, + {file = "cryptography-3.3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d"}, + {file = "cryptography-3.3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5"}, + {file = "cryptography-3.3.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"}, + {file = "cryptography-3.3.1-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244"}, + {file = "cryptography-3.3.1-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c"}, + {file = "cryptography-3.3.1-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c"}, + {file = "cryptography-3.3.1-cp36-abi3-win32.whl", hash = "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a"}, + {file = "cryptography-3.3.1-cp36-abi3-win_amd64.whl", hash = "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7"}, + {file = "cryptography-3.3.1.tar.gz", hash = "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6"}, +] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] +execnet = [ + {file = "execnet-1.7.1-py2.py3-none-any.whl", hash = "sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547"}, + {file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"}, +] +fabric = [ + {file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"}, + {file = "fabric-2.5.0.tar.gz", hash = "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] flake8 = [ {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, @@ -1173,60 +1222,85 @@ flake8-black = [ {file = "flake8-black-0.2.1.tar.gz", hash = "sha256:f26651bc10db786c03f4093414f7c9ea982ed8a244cec323c984feeffdf4c118"}, ] flake8-bugbear = [ - {file = "flake8-bugbear-20.1.4.tar.gz", hash = "sha256:bd02e4b009fb153fe6072c31c52aeab5b133d508095befb2ffcf3b41c4823162"}, - {file = "flake8_bugbear-20.1.4-py36.py37.py38-none-any.whl", hash = "sha256:a3ddc03ec28ba2296fc6f89444d1c946a6b76460f859795b35b77d4920a51b63"}, + {file = "flake8-bugbear-20.11.1.tar.gz", hash = "sha256:528020129fea2dea33a466b9d64ab650aa3e5f9ffc788b70ea4bc6cf18283538"}, + {file = "flake8_bugbear-20.11.1-py36.py37.py38-none-any.whl", hash = "sha256:f35b8135ece7a014bc0aee5b5d485334ac30a6da48494998cc1fabf7ec70d703"}, ] flake8-isort = [ {file = "flake8-isort-4.0.0.tar.gz", hash = "sha256:2b91300f4f1926b396c2c90185844eb1a3d5ec39ea6138832d119da0a208f4d9"}, {file = "flake8_isort-4.0.0-py2.py3-none-any.whl", hash = "sha256:729cd6ef9ba3659512dee337687c05d79c78e1215fdf921ed67e5fe46cce2f3c"}, ] -icontract = [ - {file = "icontract-2.3.6.tar.gz", hash = "sha256:3248f092813cda5bf0cfef890f68a95fcc0ffe40fbf65c34a0bab3ff04d250f9"}, -] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] -isodate = [ - {file = "isodate-0.6.0-py2.py3-none-any.whl", hash = "sha256:aa4d33c06640f5352aca96e4b81afd8ab3b47337cc12089822d6f322ac772c81"}, - {file = "isodate-0.6.0.tar.gz", hash = "sha256:2e364a3d5759479cdb2d37cce6b9376ea504db2ff90252a2e5b7cc89cc9ff2d8"}, +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] +importlib-metadata = [ + {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, + {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +invoke = [ + {file = "invoke-1.5.0-py2-none-any.whl", hash = "sha256:da7c2d0be71be83ffd6337e078ef9643f41240024d6b2659e7b46e0b251e339f"}, + {file = "invoke-1.5.0-py3-none-any.whl", hash = "sha256:7e44d98a7dc00c91c79bac9e3007276965d2c96884b3c22077a9f04042bd6d90"}, + {file = "invoke-1.5.0.tar.gz", hash = "sha256:f0c560075b5fb29ba14dad44a7185514e94970d1b9d57dcd3723bec5fed92650"}, ] isort = [ - {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, - {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, + {file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"}, + {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"}, ] jedi = [ {file = "jedi-0.17.2-py2.py3-none-any.whl", hash = "sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"}, {file = "jedi-0.17.2.tar.gz", hash = "sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20"}, ] -marshmallow = [ - {file = "marshmallow-3.9.0-py2.py3-none-any.whl", hash = "sha256:4bc31ab18133083b12893c61f2fc38b93c390d3fd6ae2ac61980b7dc936a1afa"}, - {file = "marshmallow-3.9.0.tar.gz", hash = "sha256:97ad6acaf727be986330969cff16040fce051510759ca709de9cd48093c55d04"}, -] -marshmallow-enum = [ - {file = "marshmallow-enum-1.5.1.tar.gz", hash = "sha256:38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58"}, - {file = "marshmallow_enum-1.5.1-py2.py3-none-any.whl", hash = "sha256:57161ab3dbfde4f57adeb12090f39592e992b9c86d206d02f6bd03ebec60f072"}, +jinja2 = [ + {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, + {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, +] +markupsafe = [ + {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, + {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, + {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, + {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, + {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] -msal = [ - {file = "msal-1.5.1-py2.py3-none-any.whl", hash = "sha256:d84074a997e6fb2a47e22815dce376adcc8790838b6b1bf1fcea29378e2bf3eb"}, - {file = "msal-1.5.1.tar.gz", hash = "sha256:7efb0256c96a7b2eadab49ce29ecdb91352a91440c12a40bed44303724b62fda"}, -] -msal-extensions = [ - {file = "msal-extensions-0.3.0.tar.gz", hash = "sha256:5523dfa15da88297e90d2e73486c8ef875a17f61ea7b7e2953a300432c2e7861"}, - {file = "msal_extensions-0.3.0-py2.py3-none-any.whl", hash = "sha256:a530c2d620061822f2ced8e29da301bc928b146970df635c852907423e8ddddc"}, -] -msrest = [ - {file = "msrest-0.6.19-py2.py3-none-any.whl", hash = "sha256:87aa64948c3ef3dbf6f6956d2240493e68d714e4621b92b65b3c4d5808297929"}, - {file = "msrest-0.6.19.tar.gz", hash = "sha256:55f8c3940bc5dc609f8cf9fcd639444716cc212a943606756272e0d0017bbb5b"}, -] -msrestazure = [ - {file = "msrestazure-0.6.4-py2.py3-none-any.whl", hash = "sha256:3de50f56147ef529b31e099a982496690468ecef33f0544cb0fa0cfe1e1de5b9"}, - {file = "msrestazure-0.6.4.tar.gz", hash = "sha256:a06f0dabc9a6f5efe3b6add4bd8fb623aeadacf816b7a35b0f89107e0544d189"}, -] mypy = [ {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"}, {file = "mypy-0.782-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e"}, @@ -1247,9 +1321,9 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] -oauthlib = [ - {file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"}, - {file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"}, +packaging = [ + {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, + {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, ] paramiko = [ {file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, @@ -1260,33 +1334,16 @@ parso = [ {file = "parso-0.7.1.tar.gz", hash = "sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"}, ] pathspec = [ - {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, - {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, + {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, + {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] -portalocker = [ - {file = "portalocker-1.7.1-py2.py3-none-any.whl", hash = "sha256:34cb36c618d88bcd9079beb36dcdc1848a3e3d92ac4eac59055bdeafc39f9d4a"}, - {file = "portalocker-1.7.1.tar.gz", hash = "sha256:6d6f5de5a3e68c4dd65a98ec1babb26d28ccc5e770e07b672d65d5a35e4b2d8a"}, -] -psutil = [ - {file = "psutil-5.7.3-cp27-none-win32.whl", hash = "sha256:1cd6a0c9fb35ece2ccf2d1dd733c1e165b342604c67454fd56a4c12e0a106787"}, - {file = "psutil-5.7.3-cp27-none-win_amd64.whl", hash = "sha256:e02c31b2990dcd2431f4524b93491941df39f99619b0d312dfe1d4d530b08b4b"}, - {file = "psutil-5.7.3-cp35-cp35m-win32.whl", hash = "sha256:56c85120fa173a5d2ad1d15a0c6e0ae62b388bfb956bb036ac231fbdaf9e4c22"}, - {file = "psutil-5.7.3-cp35-cp35m-win_amd64.whl", hash = "sha256:fa38ac15dbf161ab1e941ff4ce39abd64b53fec5ddf60c23290daed2bc7d1157"}, - {file = "psutil-5.7.3-cp36-cp36m-win32.whl", hash = "sha256:01bc82813fbc3ea304914581954979e637bcc7084e59ac904d870d6eb8bb2bc7"}, - {file = "psutil-5.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:6a3e1fd2800ca45083d976b5478a2402dd62afdfb719b30ca46cd28bb25a2eb4"}, - {file = "psutil-5.7.3-cp37-cp37m-win32.whl", hash = "sha256:fbcac492cb082fa38d88587d75feb90785d05d7e12d4565cbf1ecc727aff71b7"}, - {file = "psutil-5.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:5d9106ff5ec2712e2f659ebbd112967f44e7d33f40ba40530c485cc5904360b8"}, - {file = "psutil-5.7.3-cp38-cp38-win32.whl", hash = "sha256:ade6af32eb80a536eff162d799e31b7ef92ddcda707c27bbd077238065018df4"}, - {file = "psutil-5.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:2cb55ef9591b03ef0104bedf67cc4edb38a3edf015cf8cf24007b99cb8497542"}, - {file = "psutil-5.7.3.tar.gz", hash = "sha256:af73f7bcebdc538eda9cc81d19db1db7bf26f103f91081d780bbacfcb620dee2"}, -] py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] pycodestyle = [ {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, @@ -1300,9 +1357,9 @@ pyflakes = [ {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, ] -pyjwt = [ - {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, - {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"}, +pygments = [ + {file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"}, + {file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"}, ] pyls-black = [ {file = "pyls-black-0.4.6.tar.gz", hash = "sha256:33700e5ed605636ea7ba39188a1362d2f8602f7301f8f2b8544773886f965663"}, @@ -1322,6 +1379,8 @@ pynacl = [ {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, @@ -1332,9 +1391,48 @@ pynacl = [ {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, ] -python-dateutil = [ - {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, - {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, + {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, +] +pytest-flake8 = [ + {file = "pytest-flake8-1.0.7.tar.gz", hash = "sha256:f0259761a903563f33d6f099914afef339c085085e643bee8343eb323b32dd6b"}, + {file = "pytest_flake8-1.0.7-py2.py3-none-any.whl", hash = "sha256:c28cf23e7d359753c896745fd4ba859495d02e16c84bac36caa8b1eec58f5bc1"}, +] +pytest-forked = [ + {file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"}, + {file = "pytest_forked-1.3.0-py2.py3-none-any.whl", hash = "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"}, +] +pytest-html = [ + {file = "pytest-html-2.1.1.tar.gz", hash = "sha256:6a4ac391e105e391208e3eb9bd294a60dd336447fd8e1acddff3a6de7f4e57c5"}, + {file = "pytest_html-2.1.1-py2.py3-none-any.whl", hash = "sha256:9e4817e8be8ddde62e8653c8934d0f296b605da3d2277a052f762c56a8b32df2"}, +] +pytest-lisa = [] +pytest-metadata = [ + {file = "pytest-metadata-1.11.0.tar.gz", hash = "sha256:71b506d49d34e539cc3cfdb7ce2c5f072bea5c953320002c95968e0238f8ecf1"}, + {file = "pytest_metadata-1.11.0-py2.py3-none-any.whl", hash = "sha256:576055b8336dd4a9006dd2a47615f76f2f8c30ab12b1b1c039d99e834583523f"}, +] +pytest-mypy = [ + {file = "pytest-mypy-0.7.0.tar.gz", hash = "sha256:5a667d9a2b66bf98b3a494411f221923a6e2c3eafbe771104951aaec8985673d"}, + {file = "pytest_mypy-0.7.0-py3-none-any.whl", hash = "sha256:e0505ace48d2b19fe686366fce6b4a2ac0d090423736bb6aa2e39554d18974b7"}, +] +pytest-playbook = [] +pytest-rerunfailures = [ + {file = "pytest-rerunfailures-9.1.1.tar.gz", hash = "sha256:1cb11a17fc121b3918414eb5eaf314ee325f2e693ac7cb3f6abf7560790827f2"}, + {file = "pytest_rerunfailures-9.1.1-py3-none-any.whl", hash = "sha256:2eb7d0ad651761fbe80e064b0fd415cf6730cdbc53c16a145fd84b66143e609f"}, +] +pytest-target = [] +pytest-timeout = [ + {file = "pytest-timeout-1.4.2.tar.gz", hash = "sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76"}, + {file = "pytest_timeout-1.4.2-py2.py3-none-any.whl", hash = "sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063"}, +] +pytest-xdist = [ + {file = "pytest-xdist-2.2.0.tar.gz", hash = "sha256:1d8edbb1a45e8e1f8e44b1260583107fc23f8bc8da6d18cb331ff61d41258ecf"}, + {file = "pytest_xdist-2.2.0-py3-none-any.whl", hash = "sha256:f127e11e84ad37cc1de1088cb2990f3c354630d428af3f71282de589c5bb779b"}, ] python-jsonrpc-server = [ {file = "python-jsonrpc-server-0.4.0.tar.gz", hash = "sha256:62c543e541f101ec5b57dc654efc212d2c2e3ea47ff6f54b2e7dcb36ecf20595"}, @@ -1344,19 +1442,9 @@ python-language-server = [ {file = "python-language-server-0.35.1.tar.gz", hash = "sha256:6e0c9a3b2ae98e0eb22e98ed6b3c4e190a6bf9e27af53efd2396da60cd92b221"}, {file = "python_language_server-0.35.1-py2.py3-none-any.whl", hash = "sha256:7051090259e3e81c0cdb140de8e32b8f11219808cda4427e6faf61f9ff9a3bf4"}, ] -pywin32 = [ - {file = "pywin32-228-cp27-cp27m-win32.whl", hash = "sha256:37dc9935f6a383cc744315ae0c2882ba1768d9b06700a70f35dc1ce73cd4ba9c"}, - {file = "pywin32-228-cp27-cp27m-win_amd64.whl", hash = "sha256:11cb6610efc2f078c9e6d8f5d0f957620c333f4b23466931a247fb945ed35e89"}, - {file = "pywin32-228-cp35-cp35m-win32.whl", hash = "sha256:1f45db18af5d36195447b2cffacd182fe2d296849ba0aecdab24d3852fbf3f80"}, - {file = "pywin32-228-cp35-cp35m-win_amd64.whl", hash = "sha256:6e38c44097a834a4707c1b63efa9c2435f5a42afabff634a17f563bc478dfcc8"}, - {file = "pywin32-228-cp36-cp36m-win32.whl", hash = "sha256:ec16d44b49b5f34e99eb97cf270806fdc560dff6f84d281eb2fcb89a014a56a9"}, - {file = "pywin32-228-cp36-cp36m-win_amd64.whl", hash = "sha256:a60d795c6590a5b6baeacd16c583d91cce8038f959bd80c53bd9a68f40130f2d"}, - {file = "pywin32-228-cp37-cp37m-win32.whl", hash = "sha256:af40887b6fc200eafe4d7742c48417529a8702dcc1a60bf89eee152d1d11209f"}, - {file = "pywin32-228-cp37-cp37m-win_amd64.whl", hash = "sha256:00eaf43dbd05ba6a9b0080c77e161e0b7a601f9a3f660727a952e40140537de7"}, - {file = "pywin32-228-cp38-cp38-win32.whl", hash = "sha256:fa6ba028909cfc64ce9e24bcf22f588b14871980d9787f1e2002c99af8f1850c"}, - {file = "pywin32-228-cp38-cp38-win_amd64.whl", hash = "sha256:9b3466083f8271e1a5eb0329f4e0d61925d46b40b195a33413e0905dccb285e8"}, - {file = "pywin32-228-cp39-cp39-win32.whl", hash = "sha256:ed74b72d8059a6606f64842e7917aeee99159ebd6b8d6261c518d002837be298"}, - {file = "pywin32-228-cp39-cp39-win_amd64.whl", hash = "sha256:8319bafdcd90b7202c50d6014efdfe4fde9311b3ff15fd6f893a45c0868de203"}, +pytz = [ + {file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"}, + {file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"}, ] pyyaml = [ {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, @@ -1369,123 +1457,153 @@ pyyaml = [ {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, + {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] +recommonmark = [ + {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, + {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, +] regex = [ - {file = "regex-2020.10.28-cp27-cp27m-win32.whl", hash = "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504"}, - {file = "regex-2020.10.28-cp27-cp27m-win_amd64.whl", hash = "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e"}, - {file = "regex-2020.10.28-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789"}, - {file = "regex-2020.10.28-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd"}, - {file = "regex-2020.10.28-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd"}, - {file = "regex-2020.10.28-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26"}, - {file = "regex-2020.10.28-cp36-cp36m-win32.whl", hash = "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582"}, - {file = "regex-2020.10.28-cp36-cp36m-win_amd64.whl", hash = "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c"}, - {file = "regex-2020.10.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64"}, - {file = "regex-2020.10.28-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c"}, - {file = "regex-2020.10.28-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625"}, - {file = "regex-2020.10.28-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12"}, - {file = "regex-2020.10.28-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520"}, - {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9"}, - {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0"}, - {file = "regex-2020.10.28-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c"}, - {file = "regex-2020.10.28-cp37-cp37m-win32.whl", hash = "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0"}, - {file = "regex-2020.10.28-cp37-cp37m-win_amd64.whl", hash = "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a"}, - {file = "regex-2020.10.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f"}, - {file = "regex-2020.10.28-cp38-cp38-manylinux1_i686.whl", hash = "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b"}, - {file = "regex-2020.10.28-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53"}, - {file = "regex-2020.10.28-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5"}, - {file = "regex-2020.10.28-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa"}, - {file = "regex-2020.10.28-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898"}, - {file = "regex-2020.10.28-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427"}, - {file = "regex-2020.10.28-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf"}, - {file = "regex-2020.10.28-cp38-cp38-win32.whl", hash = "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f"}, - {file = "regex-2020.10.28-cp38-cp38-win_amd64.whl", hash = "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de"}, - {file = "regex-2020.10.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de"}, - {file = "regex-2020.10.28-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786"}, - {file = "regex-2020.10.28-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c"}, - {file = "regex-2020.10.28-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb"}, - {file = "regex-2020.10.28-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d"}, - {file = "regex-2020.10.28-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740"}, - {file = "regex-2020.10.28-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d"}, - {file = "regex-2020.10.28-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b"}, - {file = "regex-2020.10.28-cp39-cp39-win32.whl", hash = "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0"}, - {file = "regex-2020.10.28-cp39-cp39-win_amd64.whl", hash = "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e"}, - {file = "regex-2020.10.28.tar.gz", hash = "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b"}, + {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"}, + {file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"}, + {file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"}, + {file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"}, + {file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"}, + {file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"}, + {file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"}, + {file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"}, + {file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"}, + {file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"}, + {file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"}, + {file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"}, + {file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"}, + {file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"}, + {file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"}, + {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, + {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, ] requests = [ - {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, - {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, -] -requests-oauthlib = [ - {file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"}, - {file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"}, - {file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"}, -] -retry = [ - {file = "retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606"}, - {file = "retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4"}, + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, ] rope = [ - {file = "rope-0.17.0.tar.gz", hash = "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1"}, + {file = "rope-0.18.0.tar.gz", hash = "sha256:786b5c38c530d4846aa68a42604f61b4e69a493390e3ca11b88df0fbfdc3ed04"}, +] +schema = [ + {file = "schema-0.7.2-py2.py3-none-any.whl", hash = "sha256:3a03c2e2b22e6a331ae73750ab1da46916da6ca861b16e6f073ac1d1eba43b71"}, + {file = "schema-0.7.2.tar.gz", hash = "sha256:b536f2375b49fdf56f36279addae98bd86a8afbd58b3c32ce363c464bed5fc1c"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] -spur = [ - {file = "spur-0.3.20-py2.py3-none-any.whl", hash = "sha256:f9446f418f419c046f49543ca55b35eff2e96b13cf7e0cf8605d657e5fa530ee"}, - {file = "spur-0.3.20.tar.gz", hash = "sha256:f359e0573c0e4aaf8427494d6e67bc2bfac4f0c719e47f05e4750921b5804760"}, +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, ] -spurplus = [ - {file = "spurplus-2.3.4.tar.gz", hash = "sha256:667a88c06d43f4008ded2ee8706c8f20808f138dbd777ea07b8d9be277470116"}, +sphinx = [ + {file = "Sphinx-3.4.3-py3-none-any.whl", hash = "sha256:c314c857e7cd47c856d2c5adff514ac2e6495f8b8e0f886a8a37e9305dfea0d8"}, + {file = "Sphinx-3.4.3.tar.gz", hash = "sha256:41cad293f954f7d37f803d97eb184158cfd90f51195131e94875bc07cd08b93c"}, ] -stringcase = [ - {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, ] -temppathlib = [ - {file = "temppathlib-1.0.4.tar.gz", hash = "sha256:ecce37504cc2c7c996983d0c462311152bbc76102d1aa04119c12e6fdfe4180a"}, +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, + {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, + {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, +] +tenacity = [ + {file = "tenacity-6.3.1-py2.py3-none-any.whl", hash = "sha256:baed357d9f35ec64264d8a4bbf004c35058fad8795c5b0d8a7dc77ecdcbb8f39"}, + {file = "tenacity-6.3.1.tar.gz", hash = "sha256:e14d191fb0a309b563904bbc336582efe2037de437e543b38da749769b544d7f"}, ] testfixtures = [ - {file = "testfixtures-6.15.0-py2.py3-none-any.whl", hash = "sha256:e17f4f526fc90b0ac9bc7f8ca62b7dec17d9faf3d721f56bda4f0fd94d02f85a"}, - {file = "testfixtures-6.15.0.tar.gz", hash = "sha256:409f77cfbdad822d12a8ce5c4aa8fb4d0bb38073f4a5444fede3702716a2cec2"}, + {file = "testfixtures-6.17.0-py2.py3-none-any.whl", hash = "sha256:ebcc3e024d47bb58a60cdc678604151baa0c920ae2814004c89ac9066de31b2c"}, + {file = "testfixtures-6.17.0.tar.gz", hash = "sha256:fa7c170df68ca6367eda061e9ec339ae3e6d3679c31e04033f83ef97a7d7d0ce"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, + {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, + {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, + {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, + {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, + {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, + {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, + {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, + {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, + {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, + {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, + {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, + {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, + {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, + {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, + {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, + {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, + {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, ] typing-extensions = [ {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] -typing-inspect = [ - {file = "typing_inspect-0.6.0-py2-none-any.whl", hash = "sha256:de08f50a22955ddec353876df7b2545994d6df08a2f45d54ac8c05e530372ca0"}, - {file = "typing_inspect-0.6.0-py3-none-any.whl", hash = "sha256:3b98390df4d999a28cf5b35d8b333425af5da2ece8a4ea9e98f71e7591347b4f"}, - {file = "typing_inspect-0.6.0.tar.gz", hash = "sha256:8f1b1dd25908dbfd81d3bebc218011531e7ab614ba6e5bf7826d887c834afab7"}, -] ujson = [ {file = "ujson-4.0.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:5fe1536465b1c86e32a47113abd3178001b7c2dcd61f95f336fe2febf4661e74"}, {file = "ujson-4.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0f412c3f59b1ab0f40018235224ca0cf29232d0201ff5085618565a8a9c810ed"}, @@ -1510,6 +1628,10 @@ ujson = [ {file = "ujson-4.0.1.tar.gz", hash = "sha256:26cf6241b36ff5ce4539ae687b6b02673109c5e3efc96148806a7873eaa229d3"}, ] urllib3 = [ - {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, - {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, + {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, + {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, +] +zipp = [ + {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, + {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, ] diff --git a/pyproject.toml b/pyproject.toml index afd0645a74..0e08e2f332 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,42 +1,51 @@ [tool.poetry] name = "LISA" -version = "3.0.0.dev1" -description = "Linux Integration Services Automation" -authors = ["Andrew Schwartzmeyer ", "Chi Song "] -license = "Apache License 2.0" +version = "0.1.0" +description = "Linux Integration Services Automation (LISA)" +license = "MIT" +authors = ["Andrew Schwartzmeyer "] +readme = "README.md" +homepage = "https://microsoft.github.io/lisa" +repository = "https://github.com/microsoft/lisa/tree/andschwa/pytest" +classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: Pytest", + "Intended Audience :: Developers", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Topic :: Software Development :: Testing", + "Topic :: Utilities" +] +include = [".md", "playbooks/*.yaml", "playbooks/schema.json"] +packages = [{include = "*.py"}, {include = "testsuites"}] [tool.poetry.dependencies] -python = "^3.8" -psutil = "^5.7.2" -pyyaml = "^5.3.1" -retry = "^0.9.2" -paramiko = "^2.7.1" -spurplus = "^2.3.3" -dataclasses-json = "^0.5.2" -# portalocker for compatibility of Windows, trigger pywin32 installed -portalocker = "^1.7.1" -azure-identity = {version = "^1.4.0", allow-prereleases = true} -azure-mgmt-resource = {version = "^15.0.0-beta.1", allow-prereleases = true} -azure-mgmt-compute = {version = "^17.0.0-beta.1", allow-prereleases = true} -azure-mgmt-marketplaceordering = {version = "^0.2.1", allow-prereleases = true} -azure-mgmt-network = {version = "^16.0.0-beta.1", allow-prereleases = true} -azure-mgmt-storage = {version = "^16.0.0", allow-prereleases = true} -asserts = "^0.11.0" +python = "^3.7" +pytest = "^6.1.1" +pytest-timeout = "^1.4.2" +pytest-html = "^2.1.1" +pytest-rerunfailures = "^9.1.1" +pytest-playbook = {path = "pytest-playbook", develop = true} +pytest-target = {path = "pytest-target", develop = true} +pytest-lisa = {path = "pytest-lisa", develop = true} [tool.poetry.dev-dependencies] black = "^20.8b1" -flake8 = "^3.8.3" +flake8 = "^3.8.4" flake8-black = "^0.2.1" flake8-bugbear = "^20.1.4" flake8-isort = "^4.0.0" -isort = "^5.5.3" +isort = "^5.6.1" mypy = "^0.782" +python-language-server = "^0.35.1" pyls-black = "^0.4.6" pyls-isort = "^0.2.0" pyls-mypy = "^0.1.8" -python-language-server = "^0.35.1" -rope = "^0.17.0" -coverage = "^5.3" +rope = "^0.18.0" +pytest-flake8 = "^1.0.6" +pytest-mypy = "^0.7.0" +Sphinx = "^3.4.2" +recommonmark = "^0.7.1" [tool.black] line-length = 88 @@ -51,5 +60,5 @@ ensure_newline_before_comments = true line_length = 88 [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pytest-lisa/README.md b/pytest-lisa/README.md new file mode 100644 index 0000000000..8d9261dfbd --- /dev/null +++ b/pytest-lisa/README.md @@ -0,0 +1,9 @@ +# pytest-lisa + +Pytest plugin for organizing tests. Supports the LISAv3 framework. + +See the [documentation][] for more information, and the LISAv3 [`README.md`][] +for notices. + +[documentation]: https://microsoft.github.io/lisa/modules/lisa.html +[`README.md`]: https://github.com/microsoft/lisa/blob/andschwa/pytest/README.md diff --git a/pytest-lisa/lisa.py b/pytest-lisa/lisa.py new file mode 100644 index 0000000000..7aa4ed3d0d --- /dev/null +++ b/pytest-lisa/lisa.py @@ -0,0 +1,346 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""A plugin for organizing, analyzing, and selecting tests. + +This plugin provides the mark ``pytest.mark.lisa``, aliased as +:py:func:`LISA`, for marking up tests metadata beyond that which +Pytest provides by default. See the :py:data:`lisa_schema` for the +expected metadata input. + +Tests can be selected through a ``playbook.yaml`` file using the +criteria schema (using the `pytest-playbook`_ plugin). For example: + +.. _pytest-playbook: https://microsoft.github.io/lisa/modules/playbook.html + +.. code-block:: yaml + + criteria: + # Select all Priority 0 tests. + - priority: 0 + # Run tests with 'smoke' in the name twice. + - name: smoke + times: 2 + # Exclude all tests in Area "xdp" + - area: xdp + exclude: true + +.. TODO:: + + * Review the ``criteria`` schema. + * Provide test metadata statistics via a command-line flag. + * Assert every test has a LISA marker. + +""" +from __future__ import annotations + +import logging +import re +import sys +import typing + +import playbook +import py +import pytest +from schema import And, Literal, Optional, Or, Schema, SchemaError # type: ignore + +# TODO: Importing `xdist` here causes a `PytestAssertRewriteWarning` +# to be thrown, which we ignore for now. +from xdist.scheduler.loadscope import LoadScopeScheduling # type: ignore + +if typing.TYPE_CHECKING: + from typing import Any, Dict, List + + from _pytest.config import Config + from _pytest.mark.structures import Mark + from pytest import Item, Session + +LISA = pytest.mark.lisa +"""Alias for the Pytest mark ``lisa``.""" + + +def main() -> None: + """Wrapper function so we can have a ``lisa`` binary.""" + sys.exit(pytest.main()) + + +def pytest_configure(config: Config) -> None: + """Pytest `configure hook`_ to perform initial configuration. + + .. _configure hook: https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_configure + + We're registering our custom marker so that it passes + ``--strict-markers``. + + """ + config.addinivalue_line( + "markers", + ( + "lisa(platform, category, area, priority, tags): " + "Annotate a test with metadata." + ), + ) + + +def pytest_playbook_schema(schema: Dict[Any, Any]) -> None: + """:py:meth:`~playbook.Hooks.pytest_playbook_schema` hook to update the playbook schema.""" + # TODO: We also want to support a ‘targets’ list that confines a + # test selection to only the given targets. + criteria_schema = Schema( + { + # TODO: Should any/all of the strings be regex comparisons? + Optional( + "name", description="Substring match of test name.", default=None + ): str, + Optional( + "module", + description="Substring match of test file (Python module).", + default=None, + ): str, + Optional( + "area", + description="Case-folded equality comparison of test's area.", + default=None, + ): str, + Optional( + "category", + description="Case-folded equality comparison of test's category.", + default=None, + ): str, + Optional( + "priority", + # TODO: Should this instead be a range comparison? + description="Equality comparison of test's priority.", + default=None, + ): int, + Optional( + "tags", description="Subset comparison of test's tags.", default=list + ): [str], + Optional( + "times", + description="Number of times to run the matched tests.", + default=1, + ): int, + Optional( + "exclude", + description="Exclude the matched tests instead.", + default=False, + ): bool, + } + ) + schema.update({Optional("criteria", default=list): [criteria_schema]}) + + +lisa_schema = Schema( + { + Literal("platform", description="The test's intended platform."): str, + Literal("category", description="The kind of test this is."): Or( + "Functional", "Performance", "Stress", "Community", "Longhaul" + ), + Literal("area", description="The test's area (or 'feature')."): str, + Literal( + "priority", description="The test's priority with 0 being the highest." + ): Or(0, 1, 2, 3), + Optional( + "tags", + description="An arbitrary set of tags used for selection.", + default=[], + ): [str], + # TODO: Consider just making users set this manually. + Optional( + "features", + description="A set of required features, passed to `pytest.mark.target`.", + default=[], + ): [str], + Optional( + "reuse", + description="Set to false if the target is made unusable.", + default=True, + ): bool, + Optional( + "count", description="Number of targets this test needs.", default=1 + ): And(int, lambda n: 0 < n < 10), + } +) +"""Schema used to validate a test's metadata.""" + + +def validate_mark(mark: Mark) -> None: + """Validate each test's :py:func:`LISA` parameters.""" + assert not mark.args, "LISA marker cannot have positional arguments!" + mark.kwargs.update(lisa_schema.validate(mark.kwargs)) # type: ignore + + +def pytest_collection_modifyitems( + session: Session, config: Config, items: List[Item] +) -> None: + """Pytest `collection modifyitems hook`_ for modifying the selected items (tests). + + .. _collection modifyitems hook: https://docs.pytest.org/en/latest/reference.html#pytest.hookspec.pytest_collection_modifyitems + + First we validate all the :py:func:`LISA` marks on the collected + tests. Then we parse the given ``criteria`` in the playbook to + include or exclude tests. We do not care if the ``platform`` + mismatches because we intend a multiplicative effect where all + selected tests in a playbook are run on all the targets. + + """ + # TODO: The ‘Item’ object has a ‘user_properties’ attribute which + # is a list of tuples and could be used to hold the validated + # marker data, simplifying later usage. + + # Validate all LISA marks. + for item in items: + try: + mark = item.get_closest_marker("lisa") + # TODO: `assert mark, "LISA marker is missing!"` but not + # all tests will have it, such as static analysis tests. + if not mark: + continue + validate_mark(mark) + # Forward args to `pytest.mark.target` so LISA users don’t + # need to use two marks, but keep them decoupled. + # + # NOTE: The module and class scoped target fixtures won’t + # work with this, because the mark will need to be applied + # at that scope, and this applies on the function. + kw = mark.kwargs + item.add_marker( + pytest.mark.target( + features=kw["features"], reuse=kw["reuse"], count=kw["count"] + ) + ) + except (SchemaError, AssertionError) as e: + pytest.exit(f"Error validating test '{item.name}' metadata: {e}") + + # Optionally select tests based on a playbook. + included: List[Item] = [] + excluded: List[Item] = [] + + def select(item: Item, times: int, exclude: bool) -> None: + """Includes or excludes the item as appropriate.""" + if exclude: + logging.debug(f"Excluding '{item}'") + excluded.append(item) + else: + logging.debug(f"Including '{item}' {times} times") + for _ in range(times - included.count(item)): + included.append(item) + + for c in playbook.data.get("criteria", []): + for item in items: + mark = item.get_closest_marker("lisa") + if not mark: + # Not all tests will have the LISA marker, such as + # static analysis tests. + continue + i = mark.kwargs + if any( + [ + c["name"] and c["name"] in item.name, + # NOTE: `Item` does have a `module` field, though it’s untyped. + c["module"] and c["module"] in item.module.__name__, # type: ignore + c["area"] and c["area"].casefold() == i["area"].casefold(), + c["category"] + and c["category"].casefold() == i["category"].casefold(), + # Priority of 0 is falsy so explicitly check against None. + c["priority"] is not None and c["priority"] == i["priority"], + c["tags"] and set(c["tags"]) <= set(i["tags"]), + ] + ): + select(item, c["times"], c["exclude"]) + # Handle edge case of no items selected for inclusion. + if not included: + included = items + # Properly report deselected items. + collected = [i for i in included if i not in excluded] + deselected = [i for i in items if i not in collected] + config.hook.pytest_deselected(items=deselected) + items[:] = collected + + +class LISAScheduling(LoadScopeScheduling): + """Implement load scheduling across nodes, but grouping by target parameter. + + This algorithm ensures that all tests which share the same set of + parameters (namely the target) will run on the same executor as a + single work-unit. + + .. TODO:: + + This essentially confines the targets and one target won't be + spun up multiple times when run in parallel, so we should make + this scheduler optional, as an alternative scenario is to spin + up multiple near-identical instances of a target in order to + run tests in parallel. + + This is modeled after the built-in ``LoadFileScheduling``, which + also simply subclasses ``LoadScopeScheduling``. See + ``_split_scope`` for the important part. Note that we can extend + this to implement any kind of scheduling algorithm we want. + + """ + + def __init__(self, config: Config, log=None): # type: ignore + super().__init__(config, log) + if log is None: + self.log = py.log.Producer("lisasched") + else: + self.log = log.lisasched + + # NOTE: Needs to handle whitespace, so can’t be `\w+`. + _regex = re.compile(r"\[Target=([^\[\]]+)\]") + + def _split_scope(self, nodeid: str) -> str: + """Determine the scope (grouping) of a `nodeid`. + + Example of a parameterized test's `nodeid`: + + * ``example/test_module.py::test_function[Target=A]`` + * ``example/test_module.py::test_function[A][Target=B]`` + * ``example/test_module.py::test_function_extra[A][B][Target=C]`` + + `LoadScopeScheduling` uses ``nodeid.rsplit("::", 1)[0]``, or + the first ``::`` from the right, to split by scope, such that + classes will be grouped, then modules. ``LoadFileScheduling`` + uses ``nodeid.split("::", 1)[0]``, or the first ``::`` from + the left, to instead split only by modules (Python files). + + We opportunistically find the "Target" parameter and use it as + the scope. If the target parameter is missing then we simply + fallback to the algorithm of `LoadScopeScheduling`. So the + above would map into the scopes: 'A', 'B', and 'C'. + + >>> class Config: + ... def getoption(self, option): + ... return False + ... def getvalue(self, value): + ... return ["popen"] + >>> s = LISAScheduling(Config()) + >>> s._split_scope("example/test_module.py::test_function[Target=A][B][C]") + 'A' + >>> s._split_scope("example/test_module.py::test_function[A][Target=B][C]") + 'B' + >>> s._split_scope("example/test_module.py::test_function[A][B][Target=C]") + 'C' + >>> s._split_scope("example/test_module.py::test_function") + 'example/test_module.py' + >>> s._split_scope("example/test_module.py::test_class::test_function") + 'example/test_module.py::test_class' + + """ + search = self._regex.search(nodeid) + if search: + scope = search.group(1) + if self.config.getoption("verbose"): + self.log(f"Split nodeid '{nodeid}' into scope '{scope}'") + return scope + return super()._split_scope(nodeid) # type: ignore + + +def pytest_xdist_make_scheduler(config: Config) -> LISAScheduling: + """pytest-xdist `make scheduler hook`_ for implementing a custom scheduler. + + .. _make scheduler hook: https://github.com/pytest-dev/pytest-xdist/blob/master/OVERVIEW.md + + """ + return LISAScheduling(config) diff --git a/pytest-lisa/poetry.lock b/pytest-lisa/poetry.lock new file mode 100644 index 0000000000..4a3d0ed6e5 --- /dev/null +++ b/pytest-lisa/poetry.lock @@ -0,0 +1,338 @@ +[[package]] +name = "apipkg" +version = "1.5" +description = "apipkg: namespace control and lazy-import mechanism" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "20.3.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "contextlib2" +version = "0.6.0.post1" +description = "Backports and enhancements for the contextlib module" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "execnet" +version = "1.7.1" +description = "execnet: rapid multi-Python deployment" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +apipkg = ">=1.4" + +[package.extras] +testing = ["pre-commit"] + +[[package]] +name = "importlib-metadata" +version = "3.4.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "20.8" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.1" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-forked" +version = "1.3.0" +description = "run tests in isolated forked subprocesses" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +py = "*" +pytest = ">=3.10" + +[[package]] +name = "pytest-playbook" +version = "0.1.0" +description = "Pytest plugin for reading playbooks." +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +pytest = ">=6.1.2,<7.0.0" +PyYAML = ">=5.3.1,<6.0.0" +schema = "0.7.2" + +[[package]] +name = "pytest-xdist" +version = "2.2.0" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.0.0" +pytest-forked = "*" + +[package.extras] +psutil = ["psutil (>=3.0)"] +testing = ["filelock"] + +[[package]] +name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "schema" +version = "0.7.2" +description = "Simple data validation library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +contextlib2 = ">=0.5.5" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.4.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "f265436364a83d94f8fb710e4d992f88e631a07004e6bce037888eb0991288bc" + +[metadata.files] +apipkg = [ + {file = "apipkg-1.5-py2.py3-none-any.whl", hash = "sha256:58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"}, + {file = "apipkg-1.5.tar.gz", hash = "sha256:37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +contextlib2 = [ + {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, + {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, +] +execnet = [ + {file = "execnet-1.7.1-py2.py3-none-any.whl", hash = "sha256:d4efd397930c46415f62f8a31388d6be4f27a91d7550eb79bc64a756e0056547"}, + {file = "execnet-1.7.1.tar.gz", hash = "sha256:cacb9df31c9680ec5f95553976c4da484d407e85e41c83cb812aa014f0eddc50"}, +] +importlib-metadata = [ + {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, + {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +packaging = [ + {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, + {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, + {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, +] +pytest-forked = [ + {file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"}, + {file = "pytest_forked-1.3.0-py2.py3-none-any.whl", hash = "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"}, +] +pytest-playbook = [ + {file = "pytest-playbook-0.1.0.tar.gz", hash = "sha256:c9ce7446e9301646ee3fdb74a3d7cbdee4a83e775126b9062d0cbd6178ef36ec"}, + {file = "pytest_playbook-0.1.0-py3-none-any.whl", hash = "sha256:c2fb714d4347d89d7b031822eed279c956275f41f34927cb94685f04670bd01d"}, +] +pytest-xdist = [ + {file = "pytest-xdist-2.2.0.tar.gz", hash = "sha256:1d8edbb1a45e8e1f8e44b1260583107fc23f8bc8da6d18cb331ff61d41258ecf"}, + {file = "pytest_xdist-2.2.0-py3-none-any.whl", hash = "sha256:f127e11e84ad37cc1de1088cb2990f3c354630d428af3f71282de589c5bb779b"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, + {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +schema = [ + {file = "schema-0.7.2-py2.py3-none-any.whl", hash = "sha256:3a03c2e2b22e6a331ae73750ab1da46916da6ca861b16e6f073ac1d1eba43b71"}, + {file = "schema-0.7.2.tar.gz", hash = "sha256:b536f2375b49fdf56f36279addae98bd86a8afbd58b3c32ce363c464bed5fc1c"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +zipp = [ + {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, + {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, +] diff --git a/pytest-lisa/pyproject.toml b/pytest-lisa/pyproject.toml new file mode 100644 index 0000000000..33269571b0 --- /dev/null +++ b/pytest-lisa/pyproject.toml @@ -0,0 +1,38 @@ +[tool.poetry] +name = "pytest-lisa" +version = "0.1.0" +description = "Pytest plugin for organizing tests." +license = "MIT" +authors = ["Andrew Schwartzmeyer "] +readme = "README.md" +homepage = "https://microsoft.github.io/lisa" +repository = "https://github.com/microsoft/lisa/tree/andschwa/pytest/pytest-lisa" +classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: Pytest", + "Intended Audience :: Developers", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Topic :: Software Development :: Testing", + "Topic :: Utilities" +] +packages = [{include = "lisa.py"}] + +[tool.poetry.dependencies] +python = "^3.7" +pytest = "^6.1.2" +pytest-playbook = "^0.1.0" +pytest-xdist = "^2.1.0" +schema = "0.7.2" + +[tool.poetry.dev-dependencies] + +[tool.poetry.plugins] +pytest11 = {lisa = "lisa"} + +[tool.poetry.scripts] +lisa = "lisa:main" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pytest-playbook/README.md b/pytest-playbook/README.md new file mode 100644 index 0000000000..8f921de114 --- /dev/null +++ b/pytest-playbook/README.md @@ -0,0 +1,9 @@ +# pytest-playbook + +Pytest plugin for reading playbooks. Supports the LISAv3 framework. + +See the [documentation][] for more information, and the LISAv3 [`README.md`][] +for notices. + +[documentation]: https://microsoft.github.io/lisa/modules/playbook.html +[`README.md`]: https://github.com/microsoft/lisa/blob/andschwa/pytest/README.md diff --git a/pytest-playbook/playbook.py b/pytest-playbook/playbook.py new file mode 100644 index 0000000000..ecd7269f32 --- /dev/null +++ b/pytest-playbook/playbook.py @@ -0,0 +1,142 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""A plugin for creating, validating, and reading a playbook. + +Use the :py:meth:`~Hooks.pytest_playbook_schema` hook to modify the +`schema`_ dictionary representing the data expected to be read and +validated from a ``playbook.yaml`` `YAML`_ file, the path to which is +provided by the user with the command-line flag +``--playbook=``. + +.. _schema: https://github.com/keleshev/schema +.. _YAML: https://pyyaml.org/wiki/PyYAMLDocumentation + +This module's :py:data:`data` attribute will hold the read and +validated data after all :py:func:`pytest_configure` hooks have run. +Use it in your ``conftest.py`` (or Pytest plugin) like so: + +.. code-block:: python + + import playbook + from schema import Schema + + def pytest_playbook_schema(schema): + schema["targets"] = Schema({"name": str, "platform": str, "cpus": int}) + + def pytest_sessionstart(session): + for target in playbook.data["targets"]: + print(target["name"]) + +Remember not to use ``from playbook import data`` because then the +attribute will not contain the shared data. Instead use ``import +playbook`` and reference :py:data:`playbook.data`. + +All registered schema can be printed to a `JSON Schema`_ file with +``--print-schema=``. + +.. _JSON Schema: https://json-schema.org/ + +""" + +from __future__ import annotations + +import json +import typing +import warnings +from pathlib import Path + +import yaml # TODO: Optionally load yaml. +from schema import Schema, SchemaError # type: ignore + +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader # type: ignore + +import pytest + +if typing.TYPE_CHECKING: + from typing import Any, Dict, Optional + + from _pytest.config import Config, PytestPluginManager + from _pytest.config.argparsing import Parser + +data: Dict[Any, Any] = dict() +"""This global is the data read from the given playbook.""" + + +class Hooks: + """Provides the hook specifications.""" + + @pytest.hookspec + def pytest_playbook_schema(self, schema: Dict[Any, Any], config: Config) -> None: + """Update the Playbook's schema dict. + + :param schema: mutable dict passed to ``schema.validate()`` in :py:func:`pytest_configure`. + :param config: optional, allows access to Pytest ``Config`` object if given. + + """ + + +# Now provide the hook implementations. +def pytest_addhooks(pluginmanager: PytestPluginManager) -> None: + """Pytest `addhooks hook`_ to register our hooks. + + .. _addhooks hook: https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_addhooks + + """ + pluginmanager.add_hookspecs(Hooks) + + +def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None: + """Pytest `addoption hook`_ to add our CLI options. + + .. _addoption hook: https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_addoption + + """ + group = parser.getgroup("playbook") + group.addoption("--playbook", type=Path, help="Path to playbook.") + group.addoption( + "--print-schema", + type=Path, + help="Print the JSON schema of the playbook to the given path.", + ) + + +# TODO: See if this works without ‘trylast’. +@pytest.hookimpl(trylast=True) +def pytest_configure(config: Config) -> None: + """Pytest `configure hook`_ to configure our plugin. + + .. _configure hook: https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_configure + + This is set to be tried last so that all other plugins and + ``conftest.py`` files have been loaded and defined their + :py:meth:`~Hooks.pytest_playbook_schema` hooks. + + """ + schema_dict: Dict[Any, Any] = dict() + config.hook.pytest_playbook_schema(schema=schema_dict, config=config) + schema = Schema(schema_dict) + + json_schema: Optional[Path] = config.getoption("print_schema") + if json_schema: + with json_schema.open("w") as f: + json.dump(schema.json_schema(json_schema.name), f, indent=2) + pytest.exit(f"Printed schema to {json_schema}!", pytest.ExitCode.OK) + + global data + + path: Optional[Path] = config.getoption("playbook") + if not path or not path.is_file(): + warnings.warn("No playbook was specified, using defaults...") + data = schema.validate({}) + else: + try: + with path.open() as f: + data = yaml.load(f, Loader=Loader) + data = schema.validate(data) + except (yaml.YAMLError, SchemaError, OSError) as e: + pytest.exit( + f"Error loading playbook '{path}': {e}", pytest.ExitCode.USAGE_ERROR + ) diff --git a/pytest-playbook/poetry.lock b/pytest-playbook/poetry.lock new file mode 100644 index 0000000000..597abb748e --- /dev/null +++ b/pytest-playbook/poetry.lock @@ -0,0 +1,254 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "20.3.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "contextlib2" +version = "0.6.0.post1" +description = "Backports and enhancements for the contextlib module" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "importlib-metadata" +version = "3.3.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "20.8" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.1" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "schema" +version = "0.7.2" +description = "Simple data validation library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +contextlib2 = ">=0.5.5" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.4.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "42f6d26da539ab20702f2f8e6a8efae0a1f0cae1bee4d4f3896c98995cb8d3f5" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +contextlib2 = [ + {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, + {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, +] +importlib-metadata = [ + {file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"}, + {file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +packaging = [ + {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, + {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, + {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, + {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +schema = [ + {file = "schema-0.7.2-py2.py3-none-any.whl", hash = "sha256:3a03c2e2b22e6a331ae73750ab1da46916da6ca861b16e6f073ac1d1eba43b71"}, + {file = "schema-0.7.2.tar.gz", hash = "sha256:b536f2375b49fdf56f36279addae98bd86a8afbd58b3c32ce363c464bed5fc1c"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +zipp = [ + {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, + {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, +] diff --git a/pytest-playbook/pyproject.toml b/pytest-playbook/pyproject.toml new file mode 100644 index 0000000000..45dddc9ea0 --- /dev/null +++ b/pytest-playbook/pyproject.toml @@ -0,0 +1,34 @@ +[tool.poetry] +name = "pytest-playbook" +version = "0.1.1" +description = "Pytest plugin for reading playbooks." +license = "MIT" +authors = ["Andrew Schwartzmeyer "] +readme = "README.md" +homepage = "https://microsoft.github.io/lisa" +repository = "https://github.com/microsoft/lisa/tree/andschwa/pytest/pytest-playbook" +classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: Pytest", + "Intended Audience :: Developers", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Topic :: Software Development :: Testing", + "Topic :: Utilities" +] +packages = [{include = "playbook.py"}] + +[tool.poetry.dependencies] +python = "^3.7" +pytest = "^6.1.2" +schema = "0.7.2" +PyYAML = "^5.3.1" + +[tool.poetry.dev-dependencies] + +[tool.poetry.plugins] +pytest11 = {playbook = "playbook"} + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pytest-target/README.md b/pytest-target/README.md new file mode 100644 index 0000000000..3acaa76ec2 --- /dev/null +++ b/pytest-target/README.md @@ -0,0 +1,9 @@ +# pytest-target + +Pytest plugin for remote target orchestration. Supports the LISAv3 framework. + +See the [documentation][] for more information, and the LISAv3 [`README.md`][] +for notices. + +[documentation]: https://microsoft.github.io/lisa/modules/target.html +[`README.md`]: https://github.com/microsoft/lisa/blob/andschwa/pytest/README.md diff --git a/pytest-target/poetry.lock b/pytest-target/poetry.lock new file mode 100644 index 0000000000..0990516cb3 --- /dev/null +++ b/pytest-target/poetry.lock @@ -0,0 +1,526 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "20.3.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "cffi" +version = "1.14.4" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "contextlib2" +version = "0.6.0.post1" +description = "Backports and enhancements for the contextlib module" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "cryptography" +version = "3.3.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" + +[package.dependencies] +cffi = ">=1.12" +six = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "fabric" +version = "2.5.0" +description = "High level SSH command execution" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +invoke = ">=1.3,<2.0" +paramiko = ">=2.4" + +[package.extras] +pytest = ["mock (>=2.0.0,<3.0)", "pytest (>=3.2.5,<4.0)"] +testing = ["mock (>=2.0.0,<3.0)"] + +[[package]] +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "importlib-metadata" +version = "3.3.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "invoke" +version = "1.5.0" +description = "Pythonic task execution" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "20.8" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "paramiko" +version = "2.7.2" +description = "SSH2 protocol library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = ">=3.1.3" +cryptography = ">=2.5" +pynacl = ">=1.0.1" + +[package.extras] +all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=1.3)"] + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pynacl" +version = "1.4.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +cffi = ">=1.4.1" +six = "*" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.1" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-playbook" +version = "0.1.0" +description = "Pytest plugin for reading playbooks." +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +pytest = ">=6.1.2,<7.0.0" +PyYAML = ">=5.3.1,<6.0.0" +schema = "0.7.2" + +[[package]] +name = "pyyaml" +version = "5.3.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "schema" +version = "0.7.2" +description = "Simple data validation library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +contextlib2 = ">=0.5.5" + +[[package]] +name = "six" +version = "1.15.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tenacity" +version = "6.3.1" +description = "Retry code until it succeeds" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "typing-extensions" +version = "3.7.4.3" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "zipp" +version = "3.4.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "2cfd10a84e947de5a1483d7ae41f1213f0bff74c1318688aafaa2751aa1313c6" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, + {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, +] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] +cffi = [ + {file = "cffi-1.14.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775"}, + {file = "cffi-1.14.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06"}, + {file = "cffi-1.14.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26"}, + {file = "cffi-1.14.4-cp27-cp27m-win32.whl", hash = "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c"}, + {file = "cffi-1.14.4-cp27-cp27m-win_amd64.whl", hash = "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b"}, + {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d"}, + {file = "cffi-1.14.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca"}, + {file = "cffi-1.14.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698"}, + {file = "cffi-1.14.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b"}, + {file = "cffi-1.14.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293"}, + {file = "cffi-1.14.4-cp35-cp35m-win32.whl", hash = "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2"}, + {file = "cffi-1.14.4-cp35-cp35m-win_amd64.whl", hash = "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7"}, + {file = "cffi-1.14.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec"}, + {file = "cffi-1.14.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b"}, + {file = "cffi-1.14.4-cp36-cp36m-win32.whl", hash = "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668"}, + {file = "cffi-1.14.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009"}, + {file = "cffi-1.14.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03"}, + {file = "cffi-1.14.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01"}, + {file = "cffi-1.14.4-cp37-cp37m-win32.whl", hash = "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e"}, + {file = "cffi-1.14.4-cp37-cp37m-win_amd64.whl", hash = "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35"}, + {file = "cffi-1.14.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53"}, + {file = "cffi-1.14.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e"}, + {file = "cffi-1.14.4-cp38-cp38-win32.whl", hash = "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d"}, + {file = "cffi-1.14.4-cp38-cp38-win_amd64.whl", hash = "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375"}, + {file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"}, + {file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"}, + {file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"}, + {file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"}, + {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"}, + {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +contextlib2 = [ + {file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"}, + {file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"}, +] +cryptography = [ + {file = "cryptography-3.3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030"}, + {file = "cryptography-3.3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0"}, + {file = "cryptography-3.3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812"}, + {file = "cryptography-3.3.1-cp27-cp27m-win32.whl", hash = "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e"}, + {file = "cryptography-3.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901"}, + {file = "cryptography-3.3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d"}, + {file = "cryptography-3.3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5"}, + {file = "cryptography-3.3.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"}, + {file = "cryptography-3.3.1-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244"}, + {file = "cryptography-3.3.1-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c"}, + {file = "cryptography-3.3.1-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c"}, + {file = "cryptography-3.3.1-cp36-abi3-win32.whl", hash = "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a"}, + {file = "cryptography-3.3.1-cp36-abi3-win_amd64.whl", hash = "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7"}, + {file = "cryptography-3.3.1.tar.gz", hash = "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6"}, +] +fabric = [ + {file = "fabric-2.5.0-py2.py3-none-any.whl", hash = "sha256:160331934ea60036604928e792fa8e9f813266b098ef5562aa82b88527740389"}, + {file = "fabric-2.5.0.tar.gz", hash = "sha256:24842d7d51556adcabd885ac3cf5e1df73fc622a1708bf3667bf5927576cdfa6"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +importlib-metadata = [ + {file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"}, + {file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +invoke = [ + {file = "invoke-1.5.0-py2-none-any.whl", hash = "sha256:da7c2d0be71be83ffd6337e078ef9643f41240024d6b2659e7b46e0b251e339f"}, + {file = "invoke-1.5.0-py3-none-any.whl", hash = "sha256:7e44d98a7dc00c91c79bac9e3007276965d2c96884b3c22077a9f04042bd6d90"}, + {file = "invoke-1.5.0.tar.gz", hash = "sha256:f0c560075b5fb29ba14dad44a7185514e94970d1b9d57dcd3723bec5fed92650"}, +] +packaging = [ + {file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"}, + {file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"}, +] +paramiko = [ + {file = "paramiko-2.7.2-py2.py3-none-any.whl", hash = "sha256:4f3e316fef2ac628b05097a637af35685183111d4bc1b5979bd397c2ab7b5898"}, + {file = "paramiko-2.7.2.tar.gz", hash = "sha256:7f36f4ba2c0d81d219f4595e35f70d56cc94f9ac40a6acdf51d6ca210ce65035"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] +pynacl = [ + {file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"}, + {file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"}, + {file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"}, + {file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"}, + {file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"}, + {file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"}, + {file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"}, + {file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"}, + {file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"}, + {file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"}, + {file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.1-py3-none-any.whl", hash = "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8"}, + {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, +] +pytest-playbook = [ + {file = "pytest-playbook-0.1.0.tar.gz", hash = "sha256:c9ce7446e9301646ee3fdb74a3d7cbdee4a83e775126b9062d0cbd6178ef36ec"}, + {file = "pytest_playbook-0.1.0-py3-none-any.whl", hash = "sha256:c2fb714d4347d89d7b031822eed279c956275f41f34927cb94685f04670bd01d"}, +] +pyyaml = [ + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, + {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, +] +schema = [ + {file = "schema-0.7.2-py2.py3-none-any.whl", hash = "sha256:3a03c2e2b22e6a331ae73750ab1da46916da6ca861b16e6f073ac1d1eba43b71"}, + {file = "schema-0.7.2.tar.gz", hash = "sha256:b536f2375b49fdf56f36279addae98bd86a8afbd58b3c32ce363c464bed5fc1c"}, +] +six = [ + {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, + {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, +] +tenacity = [ + {file = "tenacity-6.3.1-py2.py3-none-any.whl", hash = "sha256:baed357d9f35ec64264d8a4bbf004c35058fad8795c5b0d8a7dc77ecdcbb8f39"}, + {file = "tenacity-6.3.1.tar.gz", hash = "sha256:e14d191fb0a309b563904bbc336582efe2037de437e543b38da749769b544d7f"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +typing-extensions = [ + {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, + {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, + {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, +] +zipp = [ + {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, + {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, +] diff --git a/pytest-target/pyproject.toml b/pytest-target/pyproject.toml new file mode 100644 index 0000000000..b971971540 --- /dev/null +++ b/pytest-target/pyproject.toml @@ -0,0 +1,37 @@ +[tool.poetry] +name = "pytest-target" +version = "0.1.0" +description = "Pytest plugin for remote target orchestration." +license = "MIT" +authors = ["Andrew Schwartzmeyer "] +readme = "README.md" +homepage = "https://microsoft.github.io/lisa" +repository = "https://github.com/microsoft/lisa/tree/andschwa/pytest/pytest-target" +classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: Pytest", + "Intended Audience :: Developers", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Topic :: Software Development :: Testing", + "Topic :: Utilities" +] +packages = [{include = "target"}] + +[tool.poetry.dependencies] +python = "^3.7" +pytest = "^6.1.2" +fabric = "^2.5.0" +filelock = "^3.0.12" +invoke = "^1.4.1" +tenacity = "^6.2.0" +pytest-playbook = "^0.1.0" + +[tool.poetry.dev-dependencies] + +[tool.poetry.plugins] +pytest11 = {target = "target.plugin"} + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pytest-target/target/__init__.py b/pytest-target/target/__init__.py new file mode 100644 index 0000000000..6e28cad54a --- /dev/null +++ b/pytest-target/target/__init__.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""A plugin for creating, using, and managing remote targets. + +The abstract base :py:class:`~target.target.Target` class provides an +interface for adding platform-specific support through sub-classes. A +usable reference implementation is the +:py:class:`~target.azure.AzureCLI` class. A class for just connecting +over SSH is the :py:class:`~target.target.SSH` Sub-classes can be +implemented in a ``conftest.py`` file and will be found automatically. + +Tests can request access to a target through the function-scoped +`target` Pytest fixture, which returns an instance based on the +targets listed in a `playbook.yaml` file. The fixture is parameterized +across the list of provided targets. For example: + +.. code-block:: yaml + + platforms: + AzureCLI: + sku: Standard_DS2_v2 + + targets: + - name: Debian + platform: AzureCLI + image: Debian:debian-10:10:latest + + - name: Ubuntu + platform: AzureCLI + image: Canonical:UbuntuServer:18.04-LTS:latest + +Will run all selected tests against each target. The pool of targets +can be cached between runs with ``--keep-targets``. + +""" +import pytest + +# Provide common types in the package's namespace. +from target.azure import AzureCLI +from target.target import SSH, Target + +# NOTE: This is mostly to avoid “imported but not used.” +__all__ = ["AzureCLI", "Target", "SSH"] + +# See https://docs.pytest.org/en/stable/writing_plugins.html#assertion-rewriting +pytest.register_assert_rewrite("pytest_target.azure", "pytest_target.target") diff --git a/pytest-target/target/azure.py b/pytest-target/target/azure.py new file mode 100644 index 0000000000..0a4231b2b9 --- /dev/null +++ b/pytest-target/target/azure.py @@ -0,0 +1,194 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Provides an ``Azure(Target)`` implementation using the Azure CLI.""" +from __future__ import annotations + +import json +import logging +import typing + +import invoke # type: ignore +from invoke.runners import Result # type: ignore +from schema import Optional # type: ignore +from target.target import Target +from tenacity import retry, stop_after_attempt, wait_exponential # type: ignore + +if typing.TYPE_CHECKING: + from typing import Any, Dict + + +class AzureCLI(Target): + """Implements Azure-specific target methods. + + This implementation uses the Azure CLI `az` to automate creating + VMs based on the given parameters. + + """ + + # Custom instance attribute(s). + internal_address: str + """Internal IP address of this target.""" + + @classmethod + def schema(cls) -> Dict[Any, Any]: + return { + # TODO: Maybe validate as URN or path etc. + "image": str, + Optional("sku"): str, + Optional("location"): str, + Optional("networking"): str, + } + + @classmethod + def defaults(cls) -> Dict[Any, Any]: + return { + Optional("image", default="UbuntuLTS"): str, + Optional("sku", default="Standard_DS1_v2"): str, + Optional("location", default="eastus2"): str, + Optional("networking", default=""): str, + } + + @classmethod + def _local(cls, *args: Any, **kwargs: Any) -> Result: + """A quiet version of `local()`.""" + # TODO: Consider adding this to the superclass. + config = Target._config.copy() + config["run"]["hide"] = True + context = invoke.Context(config=invoke.Config(overrides=config)) + return context.run(*args, **kwargs) + + # A class attribute because it’s defined. + _az_ok = False + + @classmethod + def check_az_cli(cls) -> None: + """Assert that the `az` CLI is installed and logged in.""" + if cls._az_ok: # Shortcut if we already checked. + return + # E.g. on Ubuntu: `curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash` + assert cls._local("az --version", warn=True), "Please install the `az` CLI!" + # TODO: Login with service principal (az login) and set + # default subscription (az account set -s) using secrets. + account: Result = cls._local("az account show") + assert account.ok, "Please `az login`!" + sub = json.loads(account.stdout) + assert sub["isDefault"], "Please `az account set -s `!" + logging.info( + f"Using account '{sub['user']['name']}' with subscription '{sub['name']}'" + ) + cls._az_ok = True + + def create_boot_storage(self, location: str) -> str: + """Create a separate resource group and storage account for boot diagnostics.""" + # TODO: Use a different account per user. + account = "pytestbootdiag" + # This command always exits with 0 but returns a string. + if self._local("az group exists -n pytest-lisa").stdout.strip() == "false": + self._local(f"az group create -n pytest-lisa --location {location}") + if not self._local( + f"az storage account show -g pytest-lisa -n {account}", warn=True + ): + self._local(f"az storage account create -g pytest-lisa -n {account}") + return account + + def allow_ping(self) -> None: + """Create NSG rules to enable ICMP ping. + + ICMP ping is disallowed by the Azure load balancer by default, but + there’s strong debate about if this is necessary, and our tests + like to check if the host is up using ping, so we create inbound + and outbound rules in the VM's network security group to allow it. + + """ + try: + for d in ["Inbound", "Outbound"]: + self.local( + f"az network nsg rule create " + f"--name allow{d}ICMP --resource-group {self.group}-rg " + f"--nsg-name {self.name}NSG --priority 150 " + f"--access Allow --direction '{d}' --protocol Icmp " + "--source-port-ranges '*' --destination-port-ranges '*'", + hide=True, + ) + except Exception as e: + logging.warning( + f"Failed creating ICMP allow rules in '{self.name}NSG': {e}" + ) + + def parse_data(self) -> str: + self.internal_address = self.data["privateIpAddress"] + return typing.cast(str, self.data["publicIpAddress"]) + + def deploy(self) -> str: + """Given deployment info, deploy a new VM.""" + if self.data: # Shortcut if refreshing from cache. + return self.parse_data() + + AzureCLI.check_az_cli() + + image = self.params["image"] + sku = self.params["sku"] + location = self.params["location"] + networking = self.params["networking"] + + logging.info( + "Deploying VM...\n" + f" Group: '{self.group}-rg'\n" + f" Region: '{location}'\n" + f" Image: '{image}'\n" + f" SKU: '{sku}'" + ) + + boot_storage = self.create_boot_storage(location) + + self._local(f"az group create -n {self.group}-rg --location {location}") + + # TODO: Accept EULA terms when necessary. Like: + # + # local.run(f"az vm image terms accept --urn {vm_image}") + # + # However, this command fails unless the terms exist and have yet + # to be accepted. + + vm_command = [ + "az vm create", + f"-g {self.group}-rg", + f"-n {self.name}", + f"--image {image}", + f"--size {sku}", + f"--boot-diagnostics-storage {boot_storage}", + "--generate-ssh-keys", + ] + # TODO: Support setting up to NICs. + if networking == "SRIOV": + vm_command.append("--accelerated-networking true") + + self.data = json.loads(self.local(" ".join(vm_command)).stdout) + self.allow_ping() + # TODO: Enable auto-shutdown 4 hours from deployment. + return self.parse_data() + + def delete(self) -> None: + """Delete the entire allocated resource group.""" + # TODO: Delete VM '{self.name}'. Only if it was + # the last VM then delete the entire resource group. + logging.debug(f"Deleting resource group '{self.group}-rg'") + try: + self.local(f"az group delete -n {self.group}-rg --yes --no-wait") + except Exception as e: + logging.warning(f"Failed deleting resource group '{self.group}-rg': {e}") + + @retry(reraise=True, wait=wait_exponential(), stop=stop_after_attempt(3)) + def get_boot_diagnostics(self, **kwargs: Any) -> Result: + """Gets the serial console logs.""" + # NOTE: Some images can cause the `az` CLI to crash because + # their logs aren’t UTF-8 encoded. I’ve filed a bug: + # https://github.com/Azure/azure-cli/issues/15590 + return self.local( + f"az vm boot-diagnostics get-boot-log -n {self.name} -g {self.group}-rg", + **kwargs, + ) + + def platform_restart(self) -> Result: + """Should this use `--force` and redeploy?""" + return self.local(f"az vm restart -n {self.name} -g {self.group}-rg") diff --git a/pytest-target/target/plugin.py b/pytest-target/target/plugin.py new file mode 100644 index 0000000000..aa196116f9 --- /dev/null +++ b/pytest-target/target/plugin.py @@ -0,0 +1,343 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Provides and parameterizes the :py:func:`.target` fixture(s). + +.. TODO:: + + * Deallocate targets when switching to a new target. + * Use richer feature/requirements comparison for targets. + +""" +from __future__ import annotations + +import logging +import typing +import warnings +from contextlib import contextmanager +from pathlib import Path +from uuid import uuid4 + +import playbook +import pytest +from filelock import FileLock # type: ignore + +# See https://pypi.org/project/schema/ +from schema import Optional, Or, Schema # type: ignore +from target.target import SSH, Target, TargetData + +if typing.TYPE_CHECKING: + from typing import Any, Dict, Generator, Iterator, List + + from _pytest.config import Config + from _pytest.config.argparsing import Parser + from _pytest.fixtures import SubRequest + from _pytest.mark.structures import Mark + from _pytest.python import Metafunc + from pytest import Session + + +def pytest_addoption(parser: Parser) -> None: + """Pytest `addoption hook`_ to add our CLI options. + + .. _addoption hook: https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_addoption + + """ + group = parser.getgroup("target") + group.addoption( + "--keep-targets", action="store_true", help="Keeps targets between runs." + ) + group.addoption( + "--delete-targets", action="store_true", help="Deletes all cached targets." + ) + + +def pytest_playbook_schema(schema: Dict[Any, Any]) -> None: + """:py:mod:`playbook` hook to update the playbook schema. + + This adds ``platforms`` and ``targets`` keys to the playbook + schema, with their nested schemata accumulated from each + platform's implementations of + :py:meth:`~target.target.Target.defaults` and + :py:meth:`~target.target.Target.schema`. We do this by iterating + over the subclasses of :py:class:`~target.target.Target`, a handy + feature of Python that lets us automatically discover users' + implementations, even if they're defined in a local + ``conftest.py`` Pytest configuration file. + + """ + classes = Target.__subclasses__() + + # The platforms schema is a set of optional mappings of each + # platform’s name to defaults for its provided schema. + platforms_schema = dict(cls.get_defaults() for cls in classes) + default_platforms = Schema(platforms_schema).validate({}) + schema.update( + { + Optional( + "platforms", + default=default_platforms, + description="A set of objects with default values for each platform.", + ): platforms_schema + } + ) + + # The targets schema is a list of ‘any of’ the platforms’ + # reference schemata. + targets_schema = [Or(*(cls.get_schema() for cls in classes))] + default_target = { + "name": "Default", + "platform": "SSH", + **Schema(SSH.schema()).validate({}), # Fill in the defaults + } + schema.update( + { + Optional( + "targets", + default=[default_target], + description="A list of targets with which to parameterize the tests.", + ): targets_schema + } + ) + + +@contextmanager +def target_pool(config: Config) -> Generator[Dict[str, Any], None, None]: + """Exclusive access to the cached targets pool. + + This handles access to the Pytest cache of serialized targets. The + cache is a dict of ``{target.name: target.to_json()}``. We use a + file lock to provide exclusive access even if Pytest is being run + in parallel with `pytest-xdist`_. Entries have a ``locked`` + property and must only be modified during a session when locked by + that session. Locking means setting ``locked`` to ``True`` and + updating the entry before exiting this context manager. + + .. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist + + """ + # TODO: Handle edge case where cache plugin is disabled. + assert config.cache is not None + lock = Path(config.cache.makedir("target")) / "pool.lock" + with FileLock(str(lock)): + pool = config.cache.get("target/pool", {}) + yield pool + config.cache.set("target/pool", pool) + + +def delete_targets(config: Config) -> None: + """Deletes all cached targets.""" + with target_pool(config) as pool: + for name, json in pool.items(): + try: + Target.from_json(json).delete() + except Exception as e: + warnings.warn(f"Failed to delete '{name}': {e}") + pool.clear() + + +def pytest_configure(config: Config) -> None: + """Pytest `configure hook`_ to perform initial configuration. + + .. _configure hook: https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_configure + + We're registering our custom marker so that it passes + ``--strict-markers``. + + """ + config.addinivalue_line( + "markers", + "target(platform, features, reuse, count): Specify target requirements.", + ) + + if config.getoption("delete_targets"): + logging.info("Deleting all cached targets!") + delete_targets(config) + pytest.exit("Deleted all cached targets!", pytest.ExitCode.OK) + + +def pytest_unconfigure(config: Config) -> None: + """Pytest `unconfigure hook`_ to perform teardown. + + .. _unconfigure hook: https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_unconfigure + + """ + if not config.getoption("keep_targets"): + # TODO: Ignore `--help` or other times tests weren’t run. + logging.info("Deleting targets! Pass `--keep-targets` to prevent this.") + delete_targets(config) + + +def get_target(request: SubRequest) -> Target: + """Common case of getting one ``Target``.""" + marker = request.node.get_closest_marker("target") + count = marker.kwargs.get("count", 1) + assert count == 1, "Use `targets` fixture with `count` instead!" + return get_targets(request).pop() + + +def get_targets(request: SubRequest) -> List[Target]: + """This function gets or creates N ``Target`` instances. + + 1. Unpack request into params, required features, and count + 2. Setup fitness criteria for target(s) + 3. Find or create necessary targets + 4. Return targets + + """ + params: Dict[Any, Any] = request.param + mark: Optional[Mark] = request.node.get_closest_marker("target") + assert mark is not None + features = mark.kwargs.get("features", []) + count = mark.kwargs.get("count", 1) + + targets: List[Target] = [] + with target_pool(request.config) as pool: + + def fits(t: TargetData) -> bool: + """Checks if a given ``Target`` fits the current search criteria. + + Converting the cached JSON to a ``TargetData`` instance is + cheap and lets us use typed fields here. + + """ + # TODO: Implement full feature comparison, etc. and not + # just proof-of-concept string set comparison. + logging.debug(f"Checking fit of {t}...") + return ( + not t.locked + and params == t.params + and set(features) <= set(t.features) + and count <= sum(t.group == x["group"] for x in pool.values()) + ) + + # TODO: If `t` is not already in use, deallocate the previous + # target, and ensure the tests have been sorted (and so grouped) + # by their requirements. + logging.debug(f"Looking for {count} target(s) which fit: {params}...") + for i in range(count): + for name, json in pool.items(): + if fits(TargetData(**json)): + logging.debug(f"Found fit target '{i}'!") + t = Target.from_json(json) + assert name == t.name # Sanity check. + t.locked = True + pool[t.name] = t.to_json() + targets.append(t) + break # Continue outer counting loop... + if targets: + assert len(targets) == count + else: + group = f"pytest-{uuid4()}" + for i in range(count): + logging.info(f"Instantiating target '{group}-{i}': {params}...") + t = Target.from_json( + { + "group": group, + "params": params, + "features": features, + "data": {}, + "number": i, + "locked": True, + } + ) + with target_pool(request.config) as pool: + pool[t.name] = t.to_json() + targets.append(t) + return targets + + +def cleanup_target(t: Target, request: SubRequest) -> None: + """This is called by fixtures after they're done with a :py:class:`~target.target.Target`.""" + t.conn.close() + mark: Optional[Mark] = request.node.get_closest_marker("target") + assert mark is not None + with target_pool(request.config) as pool: + if mark.kwargs.get("reuse", True): + t.locked = False + pool[t.name] = t.to_json() + else: + logging.info(f"Deleting target '{t.group}/{t.number}'...") + t.delete() + del pool[t.name] + + +@pytest.fixture +def target(request: SubRequest) -> Iterator[Target]: + """This fixture provides a connected :py:class:`~target.target.Target` for each test. + + It is parametrized indirectly in :py:func:`pytest_generate_tests`. + + """ + t = get_target(request) + yield t + cleanup_target(t, request) + + +@pytest.fixture +def targets(request: SubRequest) -> Iterator[List[Target]]: + """This fixture is the same as :py:func:`.target` but gets a ``Target`` list. + + For example, use ``pytest.mark.target(count=2)`` to get a list of + two targets with the same parameters, in the same group. + + """ + ts = get_targets(request) + yield ts + for t in ts: + cleanup_target(t, request) + + +@pytest.fixture(scope="class") +def c_target(request: SubRequest) -> Iterator[Target]: + """This fixture is the same as :py:func:`.target` but shared across a class.""" + t = get_target(request) + yield t + cleanup_target(t, request) + + +@pytest.fixture(scope="module") +def m_target(request: SubRequest) -> Iterator[Target]: + """This fixture is the same as :py:func:`.target` but shared across a module.""" + t = get_target(request) + yield t + cleanup_target(t, request) + + +target_params: Dict[str, Dict[str, Any]] = {} + + +def pytest_sessionstart(session: Session) -> None: + """Pytest `sessionstart hook`_ to setup the session. + + .. _sessionstart hook: https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_sessionstart + + Gather the targets from the playbook. + + First collect any user supplied defaults from the ``platforms`` + key in the playbook, which will default to the given ``defaults`` + implemented for each platform. Copy the defaults and then + overwrite with the target's specific parameters. + + """ + platform_defaults = playbook.data.get("platforms", {}) + for t in playbook.data.get("targets", []): + params = platform_defaults.get(t["platform"], {}).copy() + params.update(t) + target_params["Target=" + t["name"]] = params + + +def pytest_generate_tests(metafunc: Metafunc) -> None: + """Pytest `generate_tests hook`_ to indirectly parameterize :py:func:`.target`. + + .. _generate_tests hook: https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_generate_tests + + This takes the given targets (probably from the playbook) and + transforms them into parameters for all the tests using the + :py:func:`.target` fixture. Since this hook is run for each test, + so we gather the targets in :py:func:`pytest_sessionstart`. + + """ + assert target_params, "This should not be empty!" + for f in "target", "targets", "m_target", "c_target": + if f in metafunc.fixturenames: + metafunc.parametrize(f, target_params.values(), True, target_params.keys()) diff --git a/pytest-target/target/target.py b/pytest-target/target/target.py new file mode 100644 index 0000000000..dc2f39646a --- /dev/null +++ b/pytest-target/target/target.py @@ -0,0 +1,367 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +"""Provides the abstract base :py:class:`~target.target.Target` class. + +This abstract base class provides the building blocks for any platform +support that could be required. Users simply define a subclass with +the abstract methods implemented to deploy or delete the target +appropriately. + +""" +from __future__ import annotations + +import dataclasses +import platform +import typing +import warnings +from abc import ABCMeta, abstractmethod +from io import BytesIO + +import fabric # type: ignore +import invoke # type: ignore +from invoke.runners import Result # type: ignore +from schema import Literal, Optional, Schema # type: ignore +from tenacity import ( # type: ignore + retry, + retry_if_result, + stop_after_attempt, + wait_exponential, +) + +if typing.TYPE_CHECKING: + from typing import Any, Dict, List, Mapping, Tuple, Type + + +@dataclasses.dataclass +class TargetData: + """This class holds serializable data for a :py:class:`Target`. + + This is an internal detail. It is separated out so we can easily + serialize to and from JSON in order to enable caching. By + decoupling these we prevent users from having to understand the + semantics of a ``dataclass``, and fields added to subclasses don't + interfere with serialization. + + .. TODO:: + + Consider using more from ``dataclasses``, such as ``field()`` + and ``__post_init__()``. + + """ + + group: str + params: Dict[str, str] + features: List[str] + data: Dict[Any, Any] + number: int + locked: bool + + def to_json(self) -> Dict[str, Any]: + """Returns a JSON-serializable representation of the ``Target``.""" + return dataclasses.asdict(self) + + @staticmethod + def from_json(json: Dict[str, Any]) -> Target: + """Instantiates the correct ``Target`` subclass given the JSON representation.""" + cls = Target.get_platform(json["params"]["platform"]) + return cls(**json) + + +class Target(TargetData, metaclass=ABCMeta): + """This class represents a remote Linux target. + + As a partially abstract base class, it is meant to be subclassed + to provide platform support. So ``Target`` as a class maps to the + concept of a Linux target machine reachable via SSH (through + :py:attr:`conn`, an instance of `Fabric.Connection`_). Each + subclass of ``Target`` provides the necessary implementation to + instantiate an actual Linux target, by deploying it on that + platform. Each _instance_ of a platform-specific subclass of + ``Target`` maps to an actual Linux target that has been deployed + on that platform. + + .. _Fabric.Connection: https://docs.fabfile.org/en/stable/api/connection.html + + """ + + # Typed instance attributes (not class attributes) in addition to + # those inherited from the dataclass `TargetData`. These exist + # here and not on the superclass because they shouldn’t be cached. + name: str + host: str + conn: fabric.Connection + """Used for SSH access, see `Fabric.Connection`_""" + + # Setup a sane configuration for local and remote commands. Note + # that the defaults between Fabric and Invoke are different, so we + # use their Config classes explicitly later. + _config = { + "run": { + # Show each command as its run. + "echo": True, + # Disable stdin forwarding. + "in_stream": False, + # Don’t let remote commands take longer than twenty minutes + # (unless later overridden). This is to prevent hangs. + "command_timeout": 1200, + } + } + + def __init__( + self, + group: str, + params: Dict[Any, Any], + features: List[str], + data: Dict[Any, Any], + number: int = 0, + locked: bool = True, + ): + """Creates and deploys an instance of :py:class:`Target`. + + :param group: is a unique ID for the group of associated resources + :param params: is the input parameters conforming to `schema()` + :param features: is set of arbitrary feature requirements + :param data: is the cached data for the target + :param number: is the numerical ID of this target in its group + :param locked: is the state of the target's availability + + Subclass implementations of ``Target`` do not need to (and + should not) override :py:meth:`__init__` as it is setup such + that all platform-specific setup logic can be encoded in + :py:meth:`deploy` instead, which this calls. + + """ + self.group = group + self.params = self.get_schema().validate(params) + self.features = features + self.data = data + self.number = number + self.locked = locked + self.name = f"{self.group}-{self.number}" + + try: + self.host = self.deploy() + except Exception as e: + warnings.warn(f"Failed to deploy '{self.name}': {e}") + + fabric_config = self._config.copy() + fabric_config["run"]["env"] = { # type: ignore + # Set PATH since it’s not a login shell. + "PATH": "/sbin:/usr/sbin:/usr/local/sbin:/bin:/usr/bin:/usr/local/bin" + } + self.conn = fabric.Connection( + self.host, + config=fabric.Config(overrides=fabric_config), + inline_ssh_env=True, + ) + + # NOTE: This ought to be a property, but the combination of + # @classmethod, @property, and @abstractmethod is only supported + # in Python 3.9 and up. + @classmethod + @abstractmethod + def schema(cls) -> Mapping[Any, Any]: + """Must return a mapping for expected instance parameters. + + The items in this mapping are added to the playbook schema, so + they may contain objects from the `schema`_ library. Each + target in the playbook will have ``name`` and ``platform`` + keys in addition to those specified here (they're merged). + Parameters should generally be ``schema.Optional``. If the + parameter should have a shared but mutable default value, set + it in ``defaults``. + + .. _schema: https://github.com/keleshev/schema + + """ + ... + + @classmethod + def defaults(cls) -> Mapping[Any, Any]: + """Can return a mapping for default parameters. + + If specified, it must contain only ``schema.Optional`` + elements, where the names and types match those in + :py:meth:`schema`, but with a set default value, and those in + :py:meth:`schema` should not contain default values. This is + used a base for each target. + + """ + return {} + + @abstractmethod + def deploy(self) -> str: + """Must deploy the target resources and return the hostname. + + Subclass implementations can treat this like ``__init__`` with + :py:meth:`schema` defining the input ``params``. + + Data which should be cached must be saved to :py:attr:`data`. + + If :py:attr:`data` is populated then implementations should + assume they're refreshing a cached target. + + """ + ... + + @abstractmethod + def delete(self) -> None: + """Must delete the target's resources. + + If this is the last target in its group then implementations + should delete the group resource too. + + """ + ... + + # Internal details follow: + + _platform_description = "The class name of the platform implementation." + + @classmethod + def get_defaults(cls) -> Tuple[Optional, Schema]: + """Returns a tuple of "platform key" / "defaults value" pairs. + + This is an internal detail, used when generating the + playbook's schema. Subclasses should not override this. + + The key is an optional literal, the name of the subclass for + the platform, with a default value of the validated + :py:meth:`defaults` schema when given no input (hence they + must all be optional). The value is reference schema + definition generated from the :py:meth:`defaults` dict. + + When generating the playbook's schema all the platforms' + tuples are mapped into a single dict. + + .. TODO:: + + Assert that the set of key names in each ``defaults()`` is + a subset of the key names in the corresponding + ``schema()``. + + """ + return ( + Optional( + cls.__name__, + default=Schema(cls.defaults()).validate({}), + description=cls._platform_description, + ), + Schema(cls.defaults(), name=f"{cls.__name__}_Defaults", as_reference=True), + ) + + @classmethod + def get_schema(cls) -> Schema: + """Returns a reference schema definition for the class parameters. + + This is an internal detail, used when generating the + playbook's schema. Subclasses should not override this. + + We generate the whole definition by combining the values of + :py:meth:`schema` (which is defined by each platform's + implementation) with two required keys: + + * ``name``: A friendly name for the target. + * ``platform``: The name of the subclass for the platform. + + When generating the playbook's schema all the platforms' + schemata are mapped into an 'any of' schema. + + .. TODO:: + + Perhaps elevate ‘name’ to the key, with the nested schema + as the value. + + """ + return Schema( + { + # We’re adding ‘name’ and ‘platform’ keys. + Literal("name", description="A friendly name for the target."): str, + Literal( + "platform", description=cls._platform_description + ): cls.__name__, + # Unpack the rest of the schema’s items. + **cls.schema(), + }, + name=f"{cls.__name__}_Schema", + as_reference=True, + ) + + @staticmethod + def get_platform(platform: str) -> Type[Target]: + """Returns the :py:class:`Target` subclass for the named platform.""" + cls: typing.Optional[typing.Type[Target]] = next( + (x for x in Target.__subclasses__() if x.__name__ == platform), + None, + ) + assert cls, f"Platform implementation not found for '{platform}'" + return cls + + # Platform-agnostic functionality should be added here: + + _local_context = invoke.Context(config=invoke.Config(overrides=_config)) + + @classmethod + def local(cls, *args: Any, **kwargs: Any) -> Result: + """This patches Fabric's ``local()`` function to ignore SSH environment.""" + return Target._local_context.run(*args, **kwargs) + + @retry( + retry=retry_if_result(lambda result: result.failed), + retry_error_callback=(lambda retry_state: retry_state.outcome.result()), + wait=wait_exponential(), + stop=stop_after_attempt(5), + ) + def ping(self, **kwargs: Any) -> Result: + """Ping the node from the local system in a cross-platform manner. + + This is setup such that it retries five times when the exit + code is nonzero, with an exponential backoff. Since we want to + return the command's result regardless of failure, we suppress + `Invoke`_'s exception with ``warn=True`` and `Tenacity`_'s exception + with ``retry_error_callback=...``. + + .. _Invoke: https://www.pyinvoke.org/ + .. _Tenacity: https://tenacity.readthedocs.io/en/latest/ + + """ + flag = "-c 1" if platform.system() == "Linux" else "-n 1" + return self.local(f"ping {flag} {self.host}", warn=True, **kwargs) + + def cat(self, path: str) -> str: + """Gets the value of a remote file without a temporary file.""" + with BytesIO() as buf: + self.conn.get(path, buf) + return buf.getvalue().decode("utf-8").strip() + + +class SSH(Target): + """This platform simply connects to existing targets. + + It does not deploy nor delete the target. The default ``host`` is + ``localhost`` so this can be used for testing against the user's + system (if SSH is enabled). + + """ + + @classmethod + def schema(cls) -> Dict[Any, Any]: + """Takes a ``host`` parameter.""" + return { + Optional("host", description="The address of the destination target."): str + } + + @classmethod + def defaults(cls) -> Dict[Any, Any]: + """Defaults to ``localhost``.""" + return { + Optional( + "host", default="localhost", description="The default value for host." + ): str + } + + def deploy(self) -> str: + return self.params["host"] + + def delete(self) -> None: + pass diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000..e87ecdec1f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,16 @@ +[pytest] +addopts = + --strict-markers + --self-contained-html + --no-header + --tb=short + -rA +log_format = %(asctime)s %(levelname)s %(message)s +log_date_format = %Y-%m-%d %H:%M:%S +render_collapsed = true +junit_logging = all +junit_suite_name = LISAv3 +junit_duration_report = call +timeout = 1200 +filterwarnings = + ignore:Module already imported so cannot be rewritten diff --git a/selftests/__init__.py b/selftests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/selftests/conftest.py b/selftests/conftest.py new file mode 100644 index 0000000000..94a42e6165 --- /dev/null +++ b/selftests/conftest.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + from typing import Any, Mapping + +from target import Target + + +class Custom(Target): + @classmethod + def schema(cls) -> Mapping[Any, Any]: + return {} + + def deploy(self) -> str: + return "localhost" + + def delete(self) -> None: + pass diff --git a/selftests/setup_plan/__init__.py b/selftests/setup_plan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/selftests/setup_plan/test_plan_A.py b/selftests/setup_plan/test_plan_A.py new file mode 100644 index 0000000000..6d27842635 --- /dev/null +++ b/selftests/setup_plan/test_plan_A.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import functools +import typing + +if typing.TYPE_CHECKING: + from target import Target + +import lisa + +LISA = functools.partial( + lisa.LISA, platform="Custom", category="Functional", area="self-test", priority=1 +) + + +@LISA(features=["xdp"]) +def test_xdp_a(target: Target) -> None: + pass + + +@LISA(features=["gpu"]) +def test_gpu_a(target: Target) -> None: + pass + + +@LISA(features=["rdma"]) +def test_rdma_a(target: Target) -> None: + pass diff --git a/selftests/setup_plan/test_plan_B.py b/selftests/setup_plan/test_plan_B.py new file mode 100644 index 0000000000..bce121d133 --- /dev/null +++ b/selftests/setup_plan/test_plan_B.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import functools +import typing + +if typing.TYPE_CHECKING: + from target import Target + +import lisa + +LISA = functools.partial( + lisa.LISA, platform="Custom", category="Functional", area="self-test", priority=1 +) + + +@LISA(features=["xdp"]) +def test_xdp_b(target: Target) -> None: + pass + + +@LISA(features=["gpu"]) +def test_gpu_b(target: Target) -> None: + pass + + +@LISA(features=["rdma"]) +def test_rdma_b(target: Target) -> None: + pass diff --git a/selftests/setup_plan/test_plan_C.py b/selftests/setup_plan/test_plan_C.py new file mode 100644 index 0000000000..35c1684f1e --- /dev/null +++ b/selftests/setup_plan/test_plan_C.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import functools +import typing + +if typing.TYPE_CHECKING: + from target import Target + +import lisa + +LISA = functools.partial( + lisa.LISA, platform="Custom", category="Functional", area="self-test", priority=1 +) + + +@LISA(features=["xdp"]) +def test_xdp_c(target: Target) -> None: + pass + + +@LISA(features=["gpu"]) +def test_gpu_c(target: Target) -> None: + pass + + +@LISA(features=["rdma"]) +def test_rdma_c(target: Target) -> None: + pass diff --git a/selftests/test_basic.py b/selftests/test_basic.py new file mode 100644 index 0000000000..de768e15a9 --- /dev/null +++ b/selftests/test_basic.py @@ -0,0 +1,23 @@ +"""These tests are meant to run in a CI environment.""" +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + from target import SSH + from typing import List + +from lisa import LISA + + +@LISA(platform="SSH", category="Functional", area="self-test", priority=1) +def test_basic(target: SSH) -> None: + """Basic test which creates a `Target` connection to 'localhost'.""" + target.local("echo Hello World") + + +@LISA(platform="SSH", category="Functional", area="self-test", priority=1, count=3) +def test_basic_multiple(targets: List[SSH]) -> None: + """Basic test which asks for 3 unique targets in 1 group.""" + assert len({t.group for t in targets}) == 1 + assert len({t.number for t in targets}) == 3 diff --git a/testsuites/test_lis.py b/testsuites/test_lis.py new file mode 100644 index 0000000000..401bb691f7 --- /dev/null +++ b/testsuites/test_lis.py @@ -0,0 +1,24 @@ +"""Runs 'LIS-Tests.xml' using Pytest.""" +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + from target import AzureCLI + +import pytest + +from lisa import LISA + + +@LISA(platform="Azure", category="Functional", priority=0, area="LIS_DEPLOY") +@pytest.mark.skip(reason="Scripts missing") +def test_lis_driver_version(target: AzureCLI) -> None: + """Checks that the installed drivers have the correct version.""" + # TODO: Include “utils.sh” automatically? Or something... + for f in ["utils.sh", "LIS-VERSION-CHECK.sh"]: + target.conn.put(f) + target.conn.run(f"chmod +x {f}") + target.conn.sudo("yum install -y bc") + target.conn.run("./LIS-VERSION-CHECK.sh") + assert target.cat("state.txt") == "TestCompleted" diff --git a/testsuites/test_smoke_a.py b/testsuites/test_smoke_a.py new file mode 100644 index 0000000000..fd827b9e02 --- /dev/null +++ b/testsuites/test_smoke_a.py @@ -0,0 +1,79 @@ +"""Check that an Azure Linux VM can be deployed and is responsive. + +This example uses multiple tests with a module-scoped target fixture. +It's a more Pythonic approach, and since Pytest automatically groups +tests by fixture scopes, these run for each parameter of the target in +order as we would expect. This results in the "smoke test" actually +being a module of multiple unit tests. Another similar alternative is +to use a class and the class-scoped target fixture. See +`test_smoke_b.py` for the single-test approach. + +""" +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + from target import AzureCLI + +import socket +import time + +import pytest +from invoke.runners import CommandTimedOut, UnexpectedExit # type: ignore +from paramiko import SSHException # type: ignore + +from lisa import LISA + +pytestmark = [ + LISA( + platform="Azure", + category="Functional", + area="deploy", + priority=0, + ), + pytest.mark.target, +] + + +def test_first_ping(m_target: AzureCLI) -> None: + """"Pinging before reboot...""" + assert m_target.ping(), f"Pinging {m_target.host} before reboot failed" + + +def test_first_ssh(m_target: AzureCLI) -> None: + """SSHing before reboot...""" + assert m_target.conn.open(), f"SSH {m_target.host} before reboot failed" + + +def test_reboot(m_target: AzureCLI) -> None: + """Rebooting...""" + reboot_exit = 0 + try: + # If this succeeds, we should expect the exit code to be -1 + reboot_exit = m_target.conn.sudo("reboot", timeout=5).exited + except (TimeoutError, CommandTimedOut, SSHException, socket.error) as e: + print(f"SSH failed, using platform to reboot: '{e}'") + m_target.platform_restart() + except UnexpectedExit: + # TODO: How do we differentiate reboot working and the SSH + # connection disconnecting for other reasons? + assert reboot_exit == -1, "While SSH worked, 'reboot' command failed" + finally: + print("Sleeping for 10 seconds after reboot...") + time.sleep(10) + + +def test_second_ping(m_target: AzureCLI) -> None: + """Pinging after reboot...""" + assert m_target.ping(), f"Pinging {m_target.host} after reboot failed" + + +def test_second_ssh(m_target: AzureCLI) -> None: + """SSHing after reboot...""" + assert m_target.conn.open(), f"SSH {m_target.host} after reboot failed" + + +def test_boot_diagnostics(m_target: AzureCLI) -> None: + """Retrieving boot diagnostics...""" + m_target.get_boot_diagnostics() diff --git a/testsuites/test_smoke_b.py b/testsuites/test_smoke_b.py new file mode 100644 index 0000000000..7a6c5220c4 --- /dev/null +++ b/testsuites/test_smoke_b.py @@ -0,0 +1,96 @@ +from __future__ import annotations # For type checking. + +import typing + +if typing.TYPE_CHECKING: + from target import AzureCLI + from _pytest.logging import LogCaptureFixture + from pathlib import Path + +import logging +import socket +import time + +from invoke.runners import CommandTimedOut, UnexpectedExit # type: ignore +from paramiko import SSHException # type: ignore + +from lisa import LISA + + +@LISA(platform="Azure", category="Functional", area="deploy", priority=0) +def test_smoke(target: AzureCLI, caplog: LogCaptureFixture, tmp_path: Path) -> None: + """Check that an Azure Linux VM can be deployed and is responsive. + + This example uses exactly one function for the entire test, which + means we have to catch failures that don't fail the test, and + instead emit warnings. It works, and it's closer to how LISAv2 + would have implemented it, but it's less Pythonic. For a more + "modern" example, see `test_smoke_a.py`. + + 1. Deploy the VM (via `target` fixture). + 2. Ping the VM. + 3. Connect to the VM via SSH. + 4. Attempt to reboot via SSH, otherwise use the platform. + 5. Fetch the serial console logs AKA boot diagnostics. + + SSH failures DO NOT fail this test. + + """ + # Capture INFO and above logs for this test. + caplog.set_level(logging.INFO) + + logging.info("Pinging before reboot...") + ping1 = target.ping() + + ssh_errors = (TimeoutError, CommandTimedOut, SSHException, socket.error) + + try: + logging.info("SSHing before reboot...") + target.conn.open() + except ssh_errors as e: + logging.warning(f"SSH before reboot failed: '{e}'") + + reboot_exit = 0 + try: + logging.info("Rebooting...") + # If this succeeds, we should expect the exit code to be -1 + reboot_exit = target.conn.sudo("reboot", timeout=5).exited + except ssh_errors as e: + logging.warning(f"SSH failed, using platform to reboot: '{e}'") + target.platform_restart() + except UnexpectedExit: + # TODO: How do we differentiate reboot working and the SSH + # connection disconnecting for other reasons? + if reboot_exit != -1: + logging.warning("While SSH worked, 'reboot' command failed") + + # TODO: We should check something more concrete here instead of + # sleeping an arbitrary amount of time. + logging.info("Sleeping for 10 seconds after reboot...") + time.sleep(10) + + logging.info("Pinging after reboot...") + ping2 = target.ping() + + try: + logging.info("SSHing after reboot...") + target.conn.open() + except ssh_errors as e: + logging.warning(f"SSH after reboot failed: '{e}'") + + logging.info("Retrieving boot diagnostics...") + path = tmp_path / "diagnostics.txt" + try: + # NOTE: It’s actually more interesting to emit the downloaded + # boot diagnostics to `stdout` as they’re then captured in the + # HTML report, but this is to demo using `tmp_path`. + diagnostics = target.get_boot_diagnostics(hide=True) + path.write_text(diagnostics.stdout) + except UnexpectedExit: + logging.warning("Retrieving boot diagnostics failed.") + else: + logging.info(f"See '{path}' for boot diagnostics.") + + # NOTE: The test criteria is to fail only if ping fails. + assert ping1.ok, f"Pinging {target.host} before reboot failed" + assert ping2.ok, f"Pinging {target.host} after reboot failed" diff --git a/testsuites/test_xdp.py b/testsuites/test_xdp.py new file mode 100644 index 0000000000..c110bf0be9 --- /dev/null +++ b/testsuites/test_xdp.py @@ -0,0 +1,41 @@ +"""Runs 'FunctionalTests-XDP.xml' using Pytest.""" +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + from target import AzureCLI + +import pytest + +from lisa import LISA + + +@LISA( + platform="Azure", + category="Functional", + area="XDP", + tags=["xdp", "network", "hv_netvsc", "sriov"], + priority=0, +) +# TODO: This example is pending an update. +# setup="OneVM2NIC", +# networking="SRIOV", +# vm_image="Canonical:0001-com-ubuntu-server-focal:20_04-lts:latest", +# vm_size="Standard_DS4_v2", +@pytest.mark.skip(reason="Not Finished") +def test_verify_xdp_compliance(target: AzureCLI) -> None: + for f in [ + "utils.sh", + "XDPDumpSetup.sh", + "XDPUtils.sh", + "enable_passwordless_root.sh", + "enable_root.sh", + ]: + target.conn.put(f) + target.conn.run(f"chmod +x {f}") + target.conn.run("./enable_root.sh") + target.conn.run("./enable_passwordless_root.sh") + synth_interface = target.conn.run("source XDPUtils.sh ; get_extra_synth_nic").stdout + target.conn.run(f"./XDPDumpSetup.sh {target.internal_address} {synth_interface}") + assert target.cat("state.txt") == "TestCompleted"