├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── license-checker.yaml │ ├── lint.yaml │ └── pytest.yml ├── .gitignore ├── .mypy.ini ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── _images │ ├── Installation.gif │ ├── No Display Usage.gif │ ├── Usage.gif │ └── demo.png ├── example ├── division_by_zero_error.py ├── hello_world.py ├── print_int_str_error.py └── runs_with_error.py ├── poetry.lock ├── pyproject.toml ├── tests ├── MANUAL_TEST.md └── test_system.py ├── tox.ini └── wtpython ├── __init__.py ├── __main__.py ├── backends ├── __init__.py ├── cache.py ├── search_engine.py ├── stackoverflow.py └── trace.py ├── displays ├── __init__.py ├── no_display.py └── textual_display.py ├── exceptions.py ├── formatters.py └── settings.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Traceback** 21 | If possible, paste your traceback here 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | 2 | # To get started with Dependabot version updates, you'll need to specify which 3 | # package ecosystems to update and where the package manifests are located. 4 | # Please see the documentation for all configuration options: 5 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "pip" # See documentation for possible values 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "daily" 13 | 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | # Check for updates to GitHub Actions every weekday 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /.github/workflows/license-checker.yaml: -------------------------------------------------------------------------------- 1 | name: MIT license compatibility checker 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'pyproject.toml' 7 | - '.github/workflows/license-checker.yaml' 8 | pull_request: 9 | paths: 10 | - 'pyproject.toml' 11 | - '.github/workflows/license-checker.yaml' 12 | workflow_dispatch: 13 | 14 | concurrency: liccheck-${{ github.sha }} 15 | 16 | jobs: 17 | 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | env: 23 | # Set an environment variable to select pip's cache directory for us to actually cache between runs. 24 | PIP_CACHE_DIR: /tmp/pip-cache-dir 25 | # The Python version your project uses. Feel free to change this if required. 26 | PYTHON_VERSION: 3.9 27 | steps: 28 | # Checks out the repository in the current folder. 29 | - uses: actions/checkout@v2 30 | - uses: actions/setup-python@v2 31 | with: 32 | python-version: ${{ env.PYTHON_VERSION }} 33 | 34 | - name: Install Poetry 35 | uses: abatilo/actions-poetry@v2.1.3 36 | 37 | 38 | - name: Install dependencies 39 | run: | 40 | pip install liccheck wheel 41 | poetry install 42 | - name: Check for compatibility with MIT license 43 | run: | 44 | poetry export --format requirements.txt --output requirements.txt 45 | pip install -r requirements.txt 46 | cat requirements.txt 47 | liccheck -r requirements.txt 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Action Workflow enforcing our code style. 2 | 3 | name: Lint, Build & Deploy 4 | 5 | # Trigger the workflow on both push (to the main repository) 6 | # and pull requests (against the main repository, but from any repo). 7 | on: [push, pull_request] 8 | 9 | # Brand new concurrency setting! This ensures that not more than one run can be triggered for the same commit. 10 | # It is useful for pull requests coming from the main repository since both triggers will match. 11 | concurrency: lint-${{ github.sha }} 12 | 13 | jobs: 14 | lint: 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | python_version: [3.7, 3.8, 3.9] 19 | runs-on: ubuntu-latest 20 | 21 | env: 22 | # Set an environment variable to select pip's cache directory for us to actually cache between runs. 23 | PIP_CACHE_DIR: /tmp/pip-cache-dir 24 | 25 | steps: 26 | # Checks out the repository in the current folder. 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-python@v2 29 | with: 30 | python-version: ${{ matrix.python_version }} 31 | 32 | - name: Install dependencies 33 | run: | 34 | pip install wheel pre-commit 35 | 36 | - name: Install poetry 37 | uses: abatilo/actions-poetry@v2.1.3 38 | 39 | - name: Install poetry dependencies 40 | run: | 41 | poetry install 42 | 43 | - name: Run pre-commit hooks 44 | run: | 45 | pre-commit run --all-files 46 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: pytest 5 | 6 | on: 7 | push: 8 | paths: 9 | - '*.py' 10 | - '.github/workflows/pytest.yml' 11 | pull_request: 12 | paths: 13 | - '*.py' 14 | - '.github/workflows/pytest.yml' 15 | 16 | concurrency: test-${{ github.sha }} 17 | 18 | jobs: 19 | 20 | test: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | env: 25 | # Set an environment variable to select pip's cache directory for us to actually cache between runs. 26 | PIP_CACHE_DIR: /tmp/pip-cache-dir 27 | # The Python version your project uses. Feel free to change this if required. 28 | PYTHON_VERSION: 3.9 29 | steps: 30 | # Checks out the repository in the current folder. 31 | - uses: actions/checkout@v2 32 | - uses: actions/setup-python@v2 33 | with: 34 | python-version: ${{ env.PYTHON_VERSION }} 35 | 36 | - name: Install Poetry 37 | uses: abatilo/actions-poetry@v2.1.3 38 | 39 | - name: Install dependencies 40 | run: | 41 | pip install pytest wheel 42 | poetry install 43 | poetry export --format requirements.txt --output requirements.txt 44 | pip install -r requirements.txt 45 | 46 | - name: Run pytest 47 | run: | 48 | python -m pytest 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files generated by the interpreter 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Environment specific 6 | .venv 7 | venv 8 | .env 9 | env 10 | 11 | # Unittest reports 12 | .coverage* 13 | 14 | # Logs 15 | *.log 16 | 17 | # PyEnv version selector 18 | .python-version 19 | 20 | # Built objects 21 | *.so 22 | dist/ 23 | build/ 24 | 25 | # IDEs 26 | # PyCharm 27 | .idea/ 28 | # VSCode 29 | .vscode/ 30 | # MacOS 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.9 3 | 4 | [mypy-parse.*] 5 | ignore_missing_imports = True 6 | 7 | [mypy-markdownify.*] 8 | ignore_missing_imports = True 9 | 10 | [mypy-textual.*] 11 | ignore_missing_imports = True 12 | 13 | [mypy-pyperclip.*] 14 | ignore_missing_imports = True 15 | 16 | [mypy-requests.*] 17 | ignore_missing_imports = True 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ## Pre-commit setup 2 | # See https://github.com/python-discord/code-jam-template/tree/main#pre-commit-run-linting-before-committing 3 | 4 | # Make sure to edit the `additional_dependencies` list if you want to add plugins 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.0.1 9 | hooks: 10 | - id: check-toml 11 | - id: check-yaml 12 | - id: end-of-file-fixer 13 | - id: trailing-whitespace 14 | args: [--markdown-linebreak-ext=md] 15 | 16 | - repo: https://github.com/pre-commit/pygrep-hooks 17 | rev: v1.9.0 18 | hooks: 19 | - id: python-check-blanket-noqa 20 | 21 | - repo: https://github.com/pre-commit/mirrors-isort 22 | rev: v5.9.3 23 | hooks: 24 | - id: isort 25 | 26 | - repo: https://github.com/pycqa/flake8 27 | rev: 3.9.2 28 | hooks: 29 | - id: flake8 30 | additional_dependencies: 31 | - flake8-annotations~=2.0 32 | - flake8-bandit~=2.1 33 | - flake8-docstrings~=1.5 34 | - flake8-isort~=4.0 35 | 36 | - repo: https://github.com/pre-commit/mirrors-mypy 37 | rev: 'v0.910' 38 | hooks: 39 | - id: mypy 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions are welcome! 4 | 5 | For typos and minor corrections, please feel free to submit a PR. For technical changes, please speak with the core team first. We'd hate to turn down your awesome work. 6 | 7 | Note: Due to a [limitation in textual](https://github.com/willmcgugan/textual/issues/14), Windows is not supported, but fear not, this works in [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). 8 | 9 | 1. Fork the repository to get started. 10 | 11 | 2. Clone the repo and enter the `wtpython` folder. 12 | 13 | ```sh 14 | git clone https://github.com//wtpython.git 15 | cd wtpython 16 | ``` 17 | 18 | 3. Create and activate a virtual environment. 19 | 20 | ```sh 21 | python -m venv .venv --prompt template 22 | source .venv/bin/activate 23 | ``` 24 | 25 | 4. Upgrade pip and install [Poetry](https://python-poetry.org/docs/master/#installation). Please note that using an older version of Poetry or the old installer might not work as well. 26 | ```sh 27 | python -m pip install --upgrade pip 28 | # macOS / Linux / Bash 29 | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python - 30 | # Powershell 31 | (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - 32 | # Brew 33 | brew install poetry 34 | ``` 35 | 36 | 5. Install the package in editable mode. 37 | 38 | ```sh 39 | poetry install 40 | ``` 41 | 42 | If you add dependencies to the project, you'll have to run this command again to install the new dependencies. Make sure to pin the version in `pyproject.toml`. This software is under a MIT license, so all dependencies must respect this. There is an automated test that will block contributions that violate MIT. 43 | 44 | 6. Install [pre-commit](https://pre-commit.com/). 45 | 46 | ```sh 47 | pre-commit install 48 | ``` 49 | 50 | The `.pre-commit-config.yaml` file is configured to perform the following tasks on each commit: 51 | 52 | - Validate yaml files 53 | - Validate toml files 54 | - Ensure a single new line on each file 55 | - Ensure trailing white spaces are removed 56 | - Ensure `noqa` annotations have specific codes 57 | - Ensure your Python imports are sorted consistently 58 | - Check for errors with flake8 59 | 60 | 7. Create a new branch in your repo. Using a branch other than main will help you when syncing a fork later. 61 | 62 | ```sh 63 | git checkout -b 64 | ``` 65 | 66 | 8. Make some awesome changes! 67 | 68 | We're excited to see what you can do. 🤩 69 | 70 | 9. Commit your changes. 71 | 72 | ```sh 73 | git add . 74 | git commit -m "" 75 | ``` 76 | 77 | 10. Push your changes up to your repository. 78 | 79 | ```sh 80 | git push --set-upstream origin 81 | ``` 82 | 83 | 11. Open a Pull Request. 84 | Please provide a good description to help us understand what you've done. We are open to your suggestions and hope you'll be open to feedback if necessary. 85 | 86 | 12. Celebrate! You've just made an open-source contribution, 🎉 and you deserve a gold star! ⭐ 87 | 88 | #### Syncing a fork 89 | 90 | If you didn't commit to your main branch then you can [sync a fork with the web UI](https://docs.github.com/en/github/collaborating-with-pull-requests/working-with-forks/syncing-a-fork#syncing-a-fork-from-the-web-ui). 91 | 92 | If you did commit to the main branch then you can [merge the changes in](https://docs.github.com/en/github/collaborating-with-pull-requests/working-with-forks/syncing-a-fork#syncing-a-fork-from-the-command-line). 93 | 94 | If you want to replace your main branch with the upstream one do the following: 95 | 96 | 1. Fetch upstream. 97 | 98 | ```sh 99 | git fetch upstream 100 | ``` 101 | 102 | 2. Checkout the main branch. 103 | 104 | ```sh 105 | git checkout main 106 | ``` 107 | 108 | 3. Reset your main branch using upstream. 109 | 110 | ```sh 111 | git reset --hard upstream/main 112 | ``` 113 | 114 | 4. Force your main branch to your origin repo. 115 | 116 | ```sh 117 | git push origin master --force 118 | ``` 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 what-the-python 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PyPI - License](https://img.shields.io/pypi/l/wtpython) 2 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wtpython) 3 | ![PyPI - Status](https://img.shields.io/pypi/status/wtpython) 4 | ![PyPI](https://img.shields.io/pypi/v/wtpython) 5 | ![PyPI - Wheel](https://img.shields.io/pypi/wheel/wtpython) 6 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/wtpython) 7 | [![Discord](https://img.shields.io/discord/862056949066891284.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/DqdKBeUTmw) 8 | 9 | ![Logo](https://avatars.githubusercontent.com/u/87154160?s=200&v=4) 10 | 11 | # What the Python?! 12 | 13 | Find solutions to your Python errors without leaving your IDE or terminal! 14 | 15 | Ever have Python throw an error traceback longer than a CVS receipt 🧾? Is it dizzying to read through a bunch of cryptic lines trying to figure out exactly what caused the error? Do you look at the traceback and go, "WHAT THE ....."?😕 16 | 17 | What the Python (`wtpython`) is a simple terminal user interface that allows you to explore relevant answers on StackOverflow without leaving your terminal or IDE. When you get an error, all you have to do is swap `python` for `wtpython`. When your code hits an error, you'll see a textual interface for exploring relevant answers allowing you to stay focused and ship faster! 🚀 18 | 19 | `wtpython` is styled using [Rich](https://rich.readthedocs.io/en/stable/) and the interface is developed using [Textual](https://github.com/willmcgugan/textual). 20 | 21 | Like what you see? Feel free to share `#wtpython` on social media! 22 | 23 | ## Installation 24 | 25 | This project is hosted on [PyPI](https://pypi.org/project/wtpython/), allowing you to install it with pip. 26 | 27 | ``` 28 | pip install wtpython 29 | ``` 30 | 31 | ## Usage 32 | 33 | When you're coding and running your script or application, then all of a sudden, you see an error and think to yourself "what the...?" 34 | 35 | ``` 36 | $ python example/division_by_zero_error.py 37 | Traceback (most recent call last): 38 | File "/home/cohan/github/what-the-python/wtpython/example/division_by_zero_error.py", line 1, in 39 | 1 / 0 40 | ZeroDivisionError: division by zero 41 | ``` 42 | 43 | All you have to do is jump to the beginning of the line and change `python` to `wtpython` and the magic will happen. 44 | 45 | ``` 46 | $ wtpython example/division_by_zero_error.py 47 | # Magic! 🎩 48 | ``` 49 | 50 | ![usage](https://raw.githubusercontent.com/what-the-python/wtpython/main/docs/_images/Usage.gif) 51 | 52 | If you want results but don't want to go into the interface, just pass the `-n` flag to see the Rich formatted traceback and links to the most relevant questions on StackOverflow. 53 | 54 | ![no-display-usage](https://raw.githubusercontent.com/what-the-python/wtpython/main/docs/_images/No%20Display%20Usage.gif) 55 | 56 | If you want, you can always run `wtpython` in place of `python`. `wtpython` is designed to allow your code to function normally and only acts when your code hits an error. **`wtpython` will even allow you to pass arguments to your own script!** If our code hits an error, please [let us know](https://github.com/what-the-python/wtpython/issues). 57 | 58 | ### Command Line Options 59 | 60 | All command line options should be passed before your script and arguments passed to your script. 61 | 62 | Flag | Action 63 | ---|--- 64 | `-n` or `--no-display` | Do not enter the interactive session, just print the error and give me the links! 65 | `-c` or `--copy-error` | Add the error message to your clipboard so you can look for answers yourself (it's okay, we understand). 66 | `--clear-cache` | `wtpython` will cache results of each error message for up to a day. This helps prevent you from getting throttled by the StackOverflow API. 67 | 68 | ### Interface Hotkeys 69 | 70 | Key | Action 71 | ---|--- 72 | s| Toggle the sidebar (questions list) 73 | t| View the traceback 74 | , k| View previous question 75 | , j| View next question 76 | d| Open question in your browser 77 | f| Search for answers on Google 78 | q, ctrl+c | Quit the interface. 79 | i | Report an issue with `wtpython` 80 | 81 | ## Roadmap 82 | 83 | This project is still in the early phases, but we have big plans going forward. We hope to tackle: 84 | 85 | - Windows support (without WSL) 86 | - Jupyter integration 87 | - More interactive interface 88 | - User configuration settings 89 | - Much much more! 90 | 91 | ## Feedback / Support 92 | 93 | If you have any feedback, please [create an issue](https://github.com/what-the-python/wtpython/issues) or [start a discussion](https://github.com/what-the-python/wtpython/discussions). There is also a Discord server which you can join with this [invite](https://discord.gg/DqdKBeUTmw). 94 | 95 | ## Contributing 96 | 97 | See [CONTRIBUTING.md](https://github.com/what-the-python/wtpython/blob/main/CONTRIBUTING.md) 98 | -------------------------------------------------------------------------------- /docs/_images/Installation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagishVela/wtpython/b3d4a919a9e8b263d37d07cc1836ba0bf34ca20a/docs/_images/Installation.gif -------------------------------------------------------------------------------- /docs/_images/No Display Usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagishVela/wtpython/b3d4a919a9e8b263d37d07cc1836ba0bf34ca20a/docs/_images/No Display Usage.gif -------------------------------------------------------------------------------- /docs/_images/Usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagishVela/wtpython/b3d4a919a9e8b263d37d07cc1836ba0bf34ca20a/docs/_images/Usage.gif -------------------------------------------------------------------------------- /docs/_images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VagishVela/wtpython/b3d4a919a9e8b263d37d07cc1836ba0bf34ca20a/docs/_images/demo.png -------------------------------------------------------------------------------- /example/division_by_zero_error.py: -------------------------------------------------------------------------------- 1 | """Example file that raises ZeroDivisionError.""" 2 | 1 / 0 3 | -------------------------------------------------------------------------------- /example/hello_world.py: -------------------------------------------------------------------------------- 1 | """This script should run with no errors. 2 | 3 | This is to demonstrate what happens when the code executes correctly. 4 | """ 5 | print("Hello World!") 6 | -------------------------------------------------------------------------------- /example/print_int_str_error.py: -------------------------------------------------------------------------------- 1 | """Example to raise TypeError.""" 2 | num = 1 3 | print(num + ' one') # type: ignore 4 | -------------------------------------------------------------------------------- /example/runs_with_error.py: -------------------------------------------------------------------------------- 1 | """Example to show WTPython runs your code normally until an Exception is raised.""" 2 | import sys 3 | import time 4 | 5 | import requests 6 | 7 | print(sys.argv) 8 | 9 | for i in range(3): 10 | print(i) 11 | time.sleep(0.1) 12 | 13 | requests.get('badurl') 14 | 15 | for i in range(3): 16 | print(i) 17 | time.sleep(0.1) 18 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "appdirs" 3 | version = "1.4.4" 4 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 5 | category = "main" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "atomicwrites" 11 | version = "1.4.0" 12 | description = "Atomic file writes." 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | 17 | [[package]] 18 | name = "attrs" 19 | version = "21.2.0" 20 | description = "Classes Without Boilerplate" 21 | category = "main" 22 | optional = false 23 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 24 | 25 | [package.extras] 26 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] 27 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 28 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] 29 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] 30 | 31 | [[package]] 32 | name = "backports.entry-points-selectable" 33 | version = "1.1.0" 34 | description = "Compatibility shim providing selectable entry points for older implementations" 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=2.7" 38 | 39 | [package.dependencies] 40 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 41 | 42 | [package.extras] 43 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 44 | testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] 45 | 46 | [[package]] 47 | name = "bandit" 48 | version = "1.7.0" 49 | description = "Security oriented static analyser for python code." 50 | category = "dev" 51 | optional = false 52 | python-versions = ">=3.5" 53 | 54 | [package.dependencies] 55 | colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} 56 | GitPython = ">=1.0.1" 57 | PyYAML = ">=5.3.1" 58 | six = ">=1.10.0" 59 | stevedore = ">=1.20.0" 60 | 61 | [[package]] 62 | name = "beautifulsoup4" 63 | version = "4.10.0" 64 | description = "Screen-scraping library" 65 | category = "main" 66 | optional = false 67 | python-versions = ">3.0.0" 68 | 69 | [package.dependencies] 70 | soupsieve = ">1.2" 71 | 72 | [package.extras] 73 | html5lib = ["html5lib"] 74 | lxml = ["lxml"] 75 | 76 | [[package]] 77 | name = "cattrs" 78 | version = "1.8.0" 79 | description = "Composable complex class support for attrs and dataclasses." 80 | category = "main" 81 | optional = false 82 | python-versions = ">=3.7,<4.0" 83 | 84 | [package.dependencies] 85 | attrs = ">=20" 86 | 87 | [[package]] 88 | name = "certifi" 89 | version = "2021.5.30" 90 | description = "Python package for providing Mozilla's CA Bundle." 91 | category = "main" 92 | optional = false 93 | python-versions = "*" 94 | 95 | [[package]] 96 | name = "cfgv" 97 | version = "3.3.1" 98 | description = "Validate configuration and produce human readable error messages." 99 | category = "dev" 100 | optional = false 101 | python-versions = ">=3.6.1" 102 | 103 | [[package]] 104 | name = "charset-normalizer" 105 | version = "2.0.4" 106 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 107 | category = "main" 108 | optional = false 109 | python-versions = ">=3.5.0" 110 | 111 | [package.extras] 112 | unicode_backport = ["unicodedata2"] 113 | 114 | [[package]] 115 | name = "colorama" 116 | version = "0.4.4" 117 | description = "Cross-platform colored terminal text." 118 | category = "main" 119 | optional = false 120 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 121 | 122 | [[package]] 123 | name = "commonmark" 124 | version = "0.9.1" 125 | description = "Python parser for the CommonMark Markdown spec" 126 | category = "main" 127 | optional = false 128 | python-versions = "*" 129 | 130 | [package.extras] 131 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 132 | 133 | [[package]] 134 | name = "coverage" 135 | version = "5.5" 136 | description = "Code coverage measurement for Python" 137 | category = "dev" 138 | optional = false 139 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 140 | 141 | [package.extras] 142 | toml = ["toml"] 143 | 144 | [[package]] 145 | name = "distlib" 146 | version = "0.3.2" 147 | description = "Distribution utilities" 148 | category = "dev" 149 | optional = false 150 | python-versions = "*" 151 | 152 | [[package]] 153 | name = "filelock" 154 | version = "3.0.12" 155 | description = "A platform independent file lock." 156 | category = "dev" 157 | optional = false 158 | python-versions = "*" 159 | 160 | [[package]] 161 | name = "flake8" 162 | version = "3.9.2" 163 | description = "the modular source code checker: pep8 pyflakes and co" 164 | category = "dev" 165 | optional = false 166 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 167 | 168 | [package.dependencies] 169 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 170 | mccabe = ">=0.6.0,<0.7.0" 171 | pycodestyle = ">=2.7.0,<2.8.0" 172 | pyflakes = ">=2.3.0,<2.4.0" 173 | 174 | [[package]] 175 | name = "flake8-annotations" 176 | version = "2.6.2" 177 | description = "Flake8 Type Annotation Checks" 178 | category = "dev" 179 | optional = false 180 | python-versions = ">=3.6.1,<4.0.0" 181 | 182 | [package.dependencies] 183 | flake8 = ">=3.7,<4.0" 184 | typed-ast = {version = ">=1.4,<2.0", markers = "python_version < \"3.8\""} 185 | 186 | [[package]] 187 | name = "flake8-bandit" 188 | version = "2.1.2" 189 | description = "Automated security testing with bandit and flake8." 190 | category = "dev" 191 | optional = false 192 | python-versions = "*" 193 | 194 | [package.dependencies] 195 | bandit = "*" 196 | flake8 = "*" 197 | flake8-polyfill = "*" 198 | pycodestyle = "*" 199 | 200 | [[package]] 201 | name = "flake8-docstrings" 202 | version = "1.6.0" 203 | description = "Extension for flake8 which uses pydocstyle to check docstrings" 204 | category = "dev" 205 | optional = false 206 | python-versions = "*" 207 | 208 | [package.dependencies] 209 | flake8 = ">=3" 210 | pydocstyle = ">=2.1" 211 | 212 | [[package]] 213 | name = "flake8-isort" 214 | version = "4.0.0" 215 | description = "flake8 plugin that integrates isort ." 216 | category = "dev" 217 | optional = false 218 | python-versions = "*" 219 | 220 | [package.dependencies] 221 | flake8 = ">=3.2.1,<4" 222 | isort = ">=4.3.5,<6" 223 | testfixtures = ">=6.8.0,<7" 224 | 225 | [package.extras] 226 | test = ["pytest (>=4.0.2,<6)", "toml"] 227 | 228 | [[package]] 229 | name = "flake8-polyfill" 230 | version = "1.0.2" 231 | description = "Polyfill package for Flake8 plugins" 232 | category = "dev" 233 | optional = false 234 | python-versions = "*" 235 | 236 | [package.dependencies] 237 | flake8 = "*" 238 | 239 | [[package]] 240 | name = "gitdb" 241 | version = "4.0.7" 242 | description = "Git Object Database" 243 | category = "dev" 244 | optional = false 245 | python-versions = ">=3.4" 246 | 247 | [package.dependencies] 248 | smmap = ">=3.0.1,<5" 249 | 250 | [[package]] 251 | name = "gitpython" 252 | version = "3.1.23" 253 | description = "GitPython is a python library used to interact with Git repositories" 254 | category = "dev" 255 | optional = false 256 | python-versions = ">=3.7" 257 | 258 | [package.dependencies] 259 | gitdb = ">=4.0.1,<5" 260 | typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} 261 | 262 | [[package]] 263 | name = "identify" 264 | version = "2.2.14" 265 | description = "File identification library for Python" 266 | category = "dev" 267 | optional = false 268 | python-versions = ">=3.6.1" 269 | 270 | [package.extras] 271 | license = ["editdistance-s"] 272 | 273 | [[package]] 274 | name = "idna" 275 | version = "3.2" 276 | description = "Internationalized Domain Names in Applications (IDNA)" 277 | category = "main" 278 | optional = false 279 | python-versions = ">=3.5" 280 | 281 | [[package]] 282 | name = "importlib-metadata" 283 | version = "4.8.1" 284 | description = "Read metadata from Python packages" 285 | category = "dev" 286 | optional = false 287 | python-versions = ">=3.6" 288 | 289 | [package.dependencies] 290 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 291 | zipp = ">=0.5" 292 | 293 | [package.extras] 294 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 295 | perf = ["ipython"] 296 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 297 | 298 | [[package]] 299 | name = "iniconfig" 300 | version = "1.1.1" 301 | description = "iniconfig: brain-dead simple config-ini parsing" 302 | category = "dev" 303 | optional = false 304 | python-versions = "*" 305 | 306 | [[package]] 307 | name = "isort" 308 | version = "5.9.3" 309 | description = "A Python utility / library to sort Python imports." 310 | category = "dev" 311 | optional = false 312 | python-versions = ">=3.6.1,<4.0" 313 | 314 | [package.extras] 315 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 316 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 317 | colors = ["colorama (>=0.4.3,<0.5.0)"] 318 | plugins = ["setuptools"] 319 | 320 | [[package]] 321 | name = "markdownify" 322 | version = "0.9.4" 323 | description = "Convert HTML to markdown." 324 | category = "main" 325 | optional = false 326 | python-versions = "*" 327 | 328 | [package.dependencies] 329 | beautifulsoup4 = ">=4.9,<5" 330 | six = ">=1.15,<2" 331 | 332 | [[package]] 333 | name = "mccabe" 334 | version = "0.6.1" 335 | description = "McCabe checker, plugin for flake8" 336 | category = "dev" 337 | optional = false 338 | python-versions = "*" 339 | 340 | [[package]] 341 | name = "nodeenv" 342 | version = "1.6.0" 343 | description = "Node.js virtual environment builder" 344 | category = "dev" 345 | optional = false 346 | python-versions = "*" 347 | 348 | [[package]] 349 | name = "packaging" 350 | version = "21.0" 351 | description = "Core utilities for Python packages" 352 | category = "dev" 353 | optional = false 354 | python-versions = ">=3.6" 355 | 356 | [package.dependencies] 357 | pyparsing = ">=2.0.2" 358 | 359 | [[package]] 360 | name = "parse" 361 | version = "1.19.0" 362 | description = "parse() is the opposite of format()" 363 | category = "main" 364 | optional = false 365 | python-versions = "*" 366 | 367 | [[package]] 368 | name = "pbr" 369 | version = "5.6.0" 370 | description = "Python Build Reasonableness" 371 | category = "dev" 372 | optional = false 373 | python-versions = ">=2.6" 374 | 375 | [[package]] 376 | name = "platformdirs" 377 | version = "2.3.0" 378 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 379 | category = "dev" 380 | optional = false 381 | python-versions = ">=3.6" 382 | 383 | [package.extras] 384 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 385 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 386 | 387 | [[package]] 388 | name = "pluggy" 389 | version = "1.0.0" 390 | description = "plugin and hook calling mechanisms for python" 391 | category = "dev" 392 | optional = false 393 | python-versions = ">=3.6" 394 | 395 | [package.dependencies] 396 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 397 | 398 | [package.extras] 399 | dev = ["pre-commit", "tox"] 400 | testing = ["pytest", "pytest-benchmark"] 401 | 402 | [[package]] 403 | name = "pre-commit" 404 | version = "2.15.0" 405 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 406 | category = "dev" 407 | optional = false 408 | python-versions = ">=3.6.1" 409 | 410 | [package.dependencies] 411 | cfgv = ">=2.0.0" 412 | identify = ">=1.0.0" 413 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 414 | nodeenv = ">=0.11.1" 415 | pyyaml = ">=5.1" 416 | toml = "*" 417 | virtualenv = ">=20.0.8" 418 | 419 | [[package]] 420 | name = "py" 421 | version = "1.10.0" 422 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 423 | category = "dev" 424 | optional = false 425 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 426 | 427 | [[package]] 428 | name = "pycodestyle" 429 | version = "2.7.0" 430 | description = "Python style guide checker" 431 | category = "dev" 432 | optional = false 433 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 434 | 435 | [[package]] 436 | name = "pydocstyle" 437 | version = "6.1.1" 438 | description = "Python docstring style checker" 439 | category = "dev" 440 | optional = false 441 | python-versions = ">=3.6" 442 | 443 | [package.dependencies] 444 | snowballstemmer = "*" 445 | 446 | [package.extras] 447 | toml = ["toml"] 448 | 449 | [[package]] 450 | name = "pyflakes" 451 | version = "2.3.1" 452 | description = "passive checker of Python programs" 453 | category = "dev" 454 | optional = false 455 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 456 | 457 | [[package]] 458 | name = "pygments" 459 | version = "2.10.0" 460 | description = "Pygments is a syntax highlighting package written in Python." 461 | category = "main" 462 | optional = false 463 | python-versions = ">=3.5" 464 | 465 | [[package]] 466 | name = "pyparsing" 467 | version = "2.4.7" 468 | description = "Python parsing module" 469 | category = "dev" 470 | optional = false 471 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 472 | 473 | [[package]] 474 | name = "pyperclip" 475 | version = "1.8.2" 476 | description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" 477 | category = "main" 478 | optional = false 479 | python-versions = "*" 480 | 481 | [[package]] 482 | name = "pytest" 483 | version = "6.2.5" 484 | description = "pytest: simple powerful testing with Python" 485 | category = "dev" 486 | optional = false 487 | python-versions = ">=3.6" 488 | 489 | [package.dependencies] 490 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 491 | attrs = ">=19.2.0" 492 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 493 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 494 | iniconfig = "*" 495 | packaging = "*" 496 | pluggy = ">=0.12,<2.0" 497 | py = ">=1.8.2" 498 | toml = "*" 499 | 500 | [package.extras] 501 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 502 | 503 | [[package]] 504 | name = "pytest-cov" 505 | version = "2.12.1" 506 | description = "Pytest plugin for measuring coverage." 507 | category = "dev" 508 | optional = false 509 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 510 | 511 | [package.dependencies] 512 | coverage = ">=5.2.1" 513 | pytest = ">=4.6" 514 | toml = "*" 515 | 516 | [package.extras] 517 | testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] 518 | 519 | [[package]] 520 | name = "pytest-datadir" 521 | version = "1.3.1" 522 | description = "pytest plugin for test data directories and files" 523 | category = "dev" 524 | optional = false 525 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 526 | 527 | [package.dependencies] 528 | pytest = ">=2.7.0" 529 | 530 | [[package]] 531 | name = "pytest-randomly" 532 | version = "3.10.1" 533 | description = "Pytest plugin to randomly order tests and control random.seed." 534 | category = "dev" 535 | optional = false 536 | python-versions = ">=3.6" 537 | 538 | [package.dependencies] 539 | importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} 540 | pytest = "*" 541 | 542 | [[package]] 543 | name = "pyyaml" 544 | version = "5.4.1" 545 | description = "YAML parser and emitter for Python" 546 | category = "dev" 547 | optional = false 548 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 549 | 550 | [[package]] 551 | name = "requests" 552 | version = "2.26.0" 553 | description = "Python HTTP for Humans." 554 | category = "main" 555 | optional = false 556 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 557 | 558 | [package.dependencies] 559 | certifi = ">=2017.4.17" 560 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 561 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 562 | urllib3 = ">=1.21.1,<1.27" 563 | 564 | [package.extras] 565 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 566 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 567 | 568 | [[package]] 569 | name = "requests-cache" 570 | version = "0.8.0" 571 | description = "A transparent persistent cache for the requests library" 572 | category = "main" 573 | optional = false 574 | python-versions = ">=3.7,<4.0" 575 | 576 | [package.dependencies] 577 | appdirs = ">=1.4.4,<2.0.0" 578 | attrs = ">=21.2,<22.0" 579 | cattrs = ">=1.8,<2.0" 580 | requests = ">=2.22,<3.0" 581 | url-normalize = ">=1.4,<2.0" 582 | urllib3 = ">=1.25.5,<2.0.0" 583 | 584 | [package.extras] 585 | dynamodb = ["boto3 (>=1.15,<2.0)", "botocore (>=1.18,<2.0)"] 586 | all = ["boto3 (>=1.15,<2.0)", "botocore (>=1.18,<2.0)", "pymongo (>=3.0,<4.0)", "redis (>=3.0,<4.0)", "itsdangerous (>=2.0,<3.0)", "pyyaml (>=5.4)", "ujson (>=4.0)"] 587 | mongodb = ["pymongo (>=3.0,<4.0)"] 588 | redis = ["redis (>=3.0,<4.0)"] 589 | bson = ["bson (>=0.5)"] 590 | security = ["itsdangerous (>=2.0,<3.0)"] 591 | yaml = ["pyyaml (>=5.4)"] 592 | json = ["ujson (>=4.0)"] 593 | docs = ["furo (>=2021.8.11-beta.42)", "linkify-it-py (>=1.0.1,<2.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx (==4.1.2)", "sphinx-autodoc-typehints (>=1.11,<2.0)", "sphinx-automodapi (>=0.13,<0.14)", "sphinx-copybutton (>=0.3,<0.5)", "sphinx-inline-tabs (>=2021.4.11-beta.9,<2022.0.0)", "sphinx-panels (>=0.6,<0.7)", "sphinxcontrib-apidoc (>=0.3,<0.4)"] 594 | 595 | [[package]] 596 | name = "rich" 597 | version = "10.9.0" 598 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 599 | category = "main" 600 | optional = false 601 | python-versions = ">=3.6,<4.0" 602 | 603 | [package.dependencies] 604 | colorama = ">=0.4.0,<0.5.0" 605 | commonmark = ">=0.9.0,<0.10.0" 606 | pygments = ">=2.6.0,<3.0.0" 607 | typing-extensions = {version = ">=3.7.4,<4.0.0", markers = "python_version < \"3.8\""} 608 | 609 | [package.extras] 610 | jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] 611 | 612 | [[package]] 613 | name = "six" 614 | version = "1.16.0" 615 | description = "Python 2 and 3 compatibility utilities" 616 | category = "main" 617 | optional = false 618 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 619 | 620 | [[package]] 621 | name = "smmap" 622 | version = "4.0.0" 623 | description = "A pure Python implementation of a sliding window memory map manager" 624 | category = "dev" 625 | optional = false 626 | python-versions = ">=3.5" 627 | 628 | [[package]] 629 | name = "snowballstemmer" 630 | version = "2.1.0" 631 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 632 | category = "dev" 633 | optional = false 634 | python-versions = "*" 635 | 636 | [[package]] 637 | name = "soupsieve" 638 | version = "2.2.1" 639 | description = "A modern CSS selector implementation for Beautiful Soup." 640 | category = "main" 641 | optional = false 642 | python-versions = ">=3.6" 643 | 644 | [[package]] 645 | name = "stevedore" 646 | version = "3.4.0" 647 | description = "Manage dynamic plugins for Python applications" 648 | category = "dev" 649 | optional = false 650 | python-versions = ">=3.6" 651 | 652 | [package.dependencies] 653 | importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} 654 | pbr = ">=2.0.0,<2.1.0 || >2.1.0" 655 | 656 | [[package]] 657 | name = "testfixtures" 658 | version = "6.18.1" 659 | description = "A collection of helpers and mock objects for unit tests and doc tests." 660 | category = "dev" 661 | optional = false 662 | python-versions = "*" 663 | 664 | [package.extras] 665 | build = ["setuptools-git", "wheel", "twine"] 666 | docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] 667 | test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] 668 | 669 | [[package]] 670 | name = "textual" 671 | version = "0.1.11" 672 | description = "Text User Interface using Rich" 673 | category = "main" 674 | optional = false 675 | python-versions = ">=3.7,<4.0" 676 | 677 | [package.dependencies] 678 | rich = ">=10.7.0,<11.0.0" 679 | typing-extensions = {version = ">=3.10.0,<4.0.0", markers = "python_version < \"3.8\""} 680 | 681 | [[package]] 682 | name = "toml" 683 | version = "0.10.2" 684 | description = "Python Library for Tom's Obvious, Minimal Language" 685 | category = "main" 686 | optional = false 687 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 688 | 689 | [[package]] 690 | name = "typed-ast" 691 | version = "1.4.3" 692 | description = "a fork of Python 2 and 3 ast modules with type comment support" 693 | category = "dev" 694 | optional = false 695 | python-versions = "*" 696 | 697 | [[package]] 698 | name = "typing-extensions" 699 | version = "3.10.0.2" 700 | description = "Backported and Experimental Type Hints for Python 3.5+" 701 | category = "main" 702 | optional = false 703 | python-versions = "*" 704 | 705 | [[package]] 706 | name = "url-normalize" 707 | version = "1.4.3" 708 | description = "URL normalization for Python" 709 | category = "main" 710 | optional = false 711 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 712 | 713 | [package.dependencies] 714 | six = "*" 715 | 716 | [[package]] 717 | name = "urllib3" 718 | version = "1.26.6" 719 | description = "HTTP library with thread-safe connection pooling, file post, and more." 720 | category = "main" 721 | optional = false 722 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 723 | 724 | [package.extras] 725 | brotli = ["brotlipy (>=0.6.0)"] 726 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 727 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 728 | 729 | [[package]] 730 | name = "virtualenv" 731 | version = "20.7.2" 732 | description = "Virtual Python Environment builder" 733 | category = "dev" 734 | optional = false 735 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 736 | 737 | [package.dependencies] 738 | "backports.entry-points-selectable" = ">=1.0.4" 739 | distlib = ">=0.3.1,<1" 740 | filelock = ">=3.0.0,<4" 741 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 742 | platformdirs = ">=2,<3" 743 | six = ">=1.9.0,<2" 744 | 745 | [package.extras] 746 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] 747 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 748 | 749 | [[package]] 750 | name = "zipp" 751 | version = "3.5.0" 752 | description = "Backport of pathlib-compatible object wrapper for zip files" 753 | category = "dev" 754 | optional = false 755 | python-versions = ">=3.6" 756 | 757 | [package.extras] 758 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 759 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 760 | 761 | [extras] 762 | dev = [] 763 | test = [] 764 | 765 | [metadata] 766 | lock-version = "1.1" 767 | python-versions = ">=3.7,<4.0" 768 | content-hash = "1b1b63432a53187d327ebbb3c382fc1da2c330d3665a4874d0b3b388f6278cbf" 769 | 770 | [metadata.files] 771 | appdirs = [ 772 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 773 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 774 | ] 775 | atomicwrites = [ 776 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 777 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 778 | ] 779 | attrs = [ 780 | {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, 781 | {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, 782 | ] 783 | "backports.entry-points-selectable" = [ 784 | {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, 785 | {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, 786 | ] 787 | bandit = [ 788 | {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, 789 | {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, 790 | ] 791 | beautifulsoup4 = [ 792 | {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, 793 | {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, 794 | ] 795 | cattrs = [ 796 | {file = "cattrs-1.8.0-py3-none-any.whl", hash = "sha256:901fb2040529ae8fc9d93f48a2cdf7de3e983312ffb2a164ffa4e9847f253af1"}, 797 | {file = "cattrs-1.8.0.tar.gz", hash = "sha256:5c121ab06a7cac494813c228721a7feb5a6423b17316eeaebf13f5a03e5b0d53"}, 798 | ] 799 | certifi = [ 800 | {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"}, 801 | {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"}, 802 | ] 803 | cfgv = [ 804 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 805 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 806 | ] 807 | charset-normalizer = [ 808 | {file = "charset-normalizer-2.0.4.tar.gz", hash = "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"}, 809 | {file = "charset_normalizer-2.0.4-py3-none-any.whl", hash = "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b"}, 810 | ] 811 | colorama = [ 812 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 813 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 814 | ] 815 | commonmark = [ 816 | {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, 817 | {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, 818 | ] 819 | coverage = [ 820 | {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, 821 | {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, 822 | {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, 823 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, 824 | {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, 825 | {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, 826 | {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, 827 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, 828 | {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, 829 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, 830 | {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, 831 | {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, 832 | {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, 833 | {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, 834 | {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, 835 | {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, 836 | {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, 837 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, 838 | {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, 839 | {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, 840 | {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, 841 | {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, 842 | {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, 843 | {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, 844 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, 845 | {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, 846 | {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, 847 | {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, 848 | {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, 849 | {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, 850 | {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, 851 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, 852 | {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, 853 | {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, 854 | {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, 855 | {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, 856 | {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, 857 | {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, 858 | {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, 859 | {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, 860 | {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, 861 | {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, 862 | {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, 863 | {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, 864 | {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, 865 | {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, 866 | {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, 867 | {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, 868 | {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, 869 | {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, 870 | {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, 871 | {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, 872 | ] 873 | distlib = [ 874 | {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, 875 | {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, 876 | ] 877 | filelock = [ 878 | {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, 879 | {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, 880 | ] 881 | flake8 = [ 882 | {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, 883 | {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, 884 | ] 885 | flake8-annotations = [ 886 | {file = "flake8-annotations-2.6.2.tar.gz", hash = "sha256:0d6cd2e770b5095f09689c9d84cc054c51b929c41a68969ea1beb4b825cac515"}, 887 | {file = "flake8_annotations-2.6.2-py3-none-any.whl", hash = "sha256:d10c4638231f8a50c0a597c4efce42bd7b7d85df4f620a0ddaca526138936a4f"}, 888 | ] 889 | flake8-bandit = [ 890 | {file = "flake8_bandit-2.1.2.tar.gz", hash = "sha256:687fc8da2e4a239b206af2e54a90093572a60d0954f3054e23690739b0b0de3b"}, 891 | ] 892 | flake8-docstrings = [ 893 | {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, 894 | {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, 895 | ] 896 | flake8-isort = [ 897 | {file = "flake8-isort-4.0.0.tar.gz", hash = "sha256:2b91300f4f1926b396c2c90185844eb1a3d5ec39ea6138832d119da0a208f4d9"}, 898 | {file = "flake8_isort-4.0.0-py2.py3-none-any.whl", hash = "sha256:729cd6ef9ba3659512dee337687c05d79c78e1215fdf921ed67e5fe46cce2f3c"}, 899 | ] 900 | flake8-polyfill = [ 901 | {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, 902 | {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, 903 | ] 904 | gitdb = [ 905 | {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, 906 | {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, 907 | ] 908 | gitpython = [ 909 | {file = "GitPython-3.1.23-py3-none-any.whl", hash = "sha256:de2e2aff068097b23d6dca5daf588078fd8996a4218f6ffa704a662c2b54f9ac"}, 910 | {file = "GitPython-3.1.23.tar.gz", hash = "sha256:aaae7a3bfdf0a6db30dc1f3aeae47b71cd326d86b936fe2e158aa925fdf1471c"}, 911 | ] 912 | identify = [ 913 | {file = "identify-2.2.14-py2.py3-none-any.whl", hash = "sha256:113a76a6ba614d2a3dd408b3504446bcfac0370da5995aa6a17fd7c6dffde02d"}, 914 | {file = "identify-2.2.14.tar.gz", hash = "sha256:32f465f3c48083f345ad29a9df8419a4ce0674bf4a8c3245191d65c83634bdbf"}, 915 | ] 916 | idna = [ 917 | {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, 918 | {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, 919 | ] 920 | importlib-metadata = [ 921 | {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, 922 | {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, 923 | ] 924 | iniconfig = [ 925 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 926 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 927 | ] 928 | isort = [ 929 | {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, 930 | {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, 931 | ] 932 | markdownify = [ 933 | {file = "markdownify-0.9.4-py3-none-any.whl", hash = "sha256:153c9e739544a5dc086dbc55709c5019532f81052fbdf0dc937f7835009110dd"}, 934 | {file = "markdownify-0.9.4.tar.gz", hash = "sha256:a32d7d6b0801f81d12a44533093a5681b402c6b7e9a7049512d26697517eb795"}, 935 | ] 936 | mccabe = [ 937 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 938 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 939 | ] 940 | nodeenv = [ 941 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 942 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 943 | ] 944 | packaging = [ 945 | {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, 946 | {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, 947 | ] 948 | parse = [ 949 | {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"}, 950 | ] 951 | pbr = [ 952 | {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, 953 | {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, 954 | ] 955 | platformdirs = [ 956 | {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, 957 | {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, 958 | ] 959 | pluggy = [ 960 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 961 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 962 | ] 963 | pre-commit = [ 964 | {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, 965 | {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, 966 | ] 967 | py = [ 968 | {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, 969 | {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, 970 | ] 971 | pycodestyle = [ 972 | {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, 973 | {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, 974 | ] 975 | pydocstyle = [ 976 | {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, 977 | {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, 978 | ] 979 | pyflakes = [ 980 | {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, 981 | {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, 982 | ] 983 | pygments = [ 984 | {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, 985 | {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, 986 | ] 987 | pyparsing = [ 988 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 989 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 990 | ] 991 | pyperclip = [ 992 | {file = "pyperclip-1.8.2.tar.gz", hash = "sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57"}, 993 | ] 994 | pytest = [ 995 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 996 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 997 | ] 998 | pytest-cov = [ 999 | {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, 1000 | {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, 1001 | ] 1002 | pytest-datadir = [ 1003 | {file = "pytest-datadir-1.3.1.tar.gz", hash = "sha256:d3af1e738df87515ee509d6135780f25a15959766d9c2b2dbe02bf4fb979cb18"}, 1004 | {file = "pytest_datadir-1.3.1-py2.py3-none-any.whl", hash = "sha256:1847ed0efe0bc54cac40ab3fba6d651c2f03d18dd01f2a582979604d32e7621e"}, 1005 | ] 1006 | pytest-randomly = [ 1007 | {file = "pytest-randomly-3.10.1.tar.gz", hash = "sha256:d4ef5dbf27e542e6a4e4cec7a20ef3f1b906bce21fa340ca5657b5326ef23a64"}, 1008 | {file = "pytest_randomly-3.10.1-py3-none-any.whl", hash = "sha256:d28d490e3a743bdd64c5bc87c5fc182eac966ba6432c6bb6b224e32e76527e9e"}, 1009 | ] 1010 | pyyaml = [ 1011 | {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, 1012 | {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, 1013 | {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, 1014 | {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, 1015 | {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, 1016 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, 1017 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, 1018 | {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, 1019 | {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, 1020 | {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, 1021 | {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, 1022 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, 1023 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, 1024 | {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, 1025 | {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, 1026 | {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, 1027 | {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, 1028 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, 1029 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, 1030 | {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, 1031 | {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, 1032 | {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, 1033 | {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, 1034 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, 1035 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, 1036 | {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, 1037 | {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, 1038 | {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, 1039 | {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, 1040 | ] 1041 | requests = [ 1042 | {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, 1043 | {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, 1044 | ] 1045 | requests-cache = [ 1046 | {file = "requests-cache-0.8.0.tar.gz", hash = "sha256:2f80b2a43d6bb886558181133d9b74db12f1eed42c190b53d8e98ab62a0d2231"}, 1047 | {file = "requests_cache-0.8.0-py3-none-any.whl", hash = "sha256:35d0f6a7d6f43932515200dff4a6bfbace0b2fedfe0d031069514cb77ec5d088"}, 1048 | ] 1049 | rich = [ 1050 | {file = "rich-10.9.0-py3-none-any.whl", hash = "sha256:2c84d9b3459c16bf413fe0f9644c7ae1791971e0bb944dfae56e7c7634b187ab"}, 1051 | {file = "rich-10.9.0.tar.gz", hash = "sha256:ba285f1c519519490034284e6a9d2e6e3f16dc7690f2de3d9140737d81304d22"}, 1052 | ] 1053 | six = [ 1054 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1055 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1056 | ] 1057 | smmap = [ 1058 | {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, 1059 | {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, 1060 | ] 1061 | snowballstemmer = [ 1062 | {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, 1063 | {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, 1064 | ] 1065 | soupsieve = [ 1066 | {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"}, 1067 | {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"}, 1068 | ] 1069 | stevedore = [ 1070 | {file = "stevedore-3.4.0-py3-none-any.whl", hash = "sha256:920ce6259f0b2498aaa4545989536a27e4e4607b8318802d7ddc3a533d3d069e"}, 1071 | {file = "stevedore-3.4.0.tar.gz", hash = "sha256:59b58edb7f57b11897f150475e7bc0c39c5381f0b8e3fa9f5c20ce6c89ec4aa1"}, 1072 | ] 1073 | testfixtures = [ 1074 | {file = "testfixtures-6.18.1-py2.py3-none-any.whl", hash = "sha256:486be7b01eb71326029811878a3317b7e7994324621c0ec633c8e24499d8d5b3"}, 1075 | {file = "testfixtures-6.18.1.tar.gz", hash = "sha256:0a6422737f6d89b45cdef1e2df5576f52ad0f507956002ce1020daa9f44211d6"}, 1076 | ] 1077 | textual = [ 1078 | {file = "textual-0.1.11-py3-none-any.whl", hash = "sha256:8fc16b751c722cd7a258a5173f08cd32253f4cf165a73e71ad109e0e03546ef3"}, 1079 | {file = "textual-0.1.11.tar.gz", hash = "sha256:51cb6d6cfd1554a3c18f069fc3d63f98329bac39712de6193099a75f2b3ed03c"}, 1080 | ] 1081 | toml = [ 1082 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1083 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1084 | ] 1085 | typed-ast = [ 1086 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, 1087 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, 1088 | {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, 1089 | {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, 1090 | {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, 1091 | {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, 1092 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, 1093 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, 1094 | {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, 1095 | {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, 1096 | {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, 1097 | {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, 1098 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, 1099 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, 1100 | {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, 1101 | {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, 1102 | {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, 1103 | {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, 1104 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, 1105 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, 1106 | {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, 1107 | {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, 1108 | {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, 1109 | {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, 1110 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, 1111 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, 1112 | {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, 1113 | {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, 1114 | {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, 1115 | {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, 1116 | ] 1117 | typing-extensions = [ 1118 | {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, 1119 | {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, 1120 | {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, 1121 | ] 1122 | url-normalize = [ 1123 | {file = "url-normalize-1.4.3.tar.gz", hash = "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2"}, 1124 | {file = "url_normalize-1.4.3-py2.py3-none-any.whl", hash = "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed"}, 1125 | ] 1126 | urllib3 = [ 1127 | {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"}, 1128 | {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"}, 1129 | ] 1130 | virtualenv = [ 1131 | {file = "virtualenv-20.7.2-py2.py3-none-any.whl", hash = "sha256:e4670891b3a03eb071748c569a87cceaefbf643c5bac46d996c5a45c34aa0f06"}, 1132 | {file = "virtualenv-20.7.2.tar.gz", hash = "sha256:9ef4e8ee4710826e98ff3075c9a4739e2cb1040de6a2a8d35db0055840dc96a0"}, 1133 | ] 1134 | zipp = [ 1135 | {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"}, 1136 | {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"}, 1137 | ] 1138 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "wtpython" 3 | version = "0.1.0" 4 | description = "A TUI that interactively helps you solve errors that might arise in your code." 5 | authors = ["wtpython "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://pypi.org/project/wtpython/" 9 | repository = "https://github.com/what-the-python/wtpython" 10 | classifiers = [ 11 | "Development Status :: 3 - Alpha", 12 | "License :: OSI Approved :: MIT License", 13 | "Environment :: Console", 14 | "Intended Audience :: Developers", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3 :: Only", 17 | "Programming Language :: Python :: 3.7", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: Implementation :: CPython", 22 | ] 23 | 24 | [tool.poetry.dependencies] 25 | python = ">=3.7,<4.0" # Poetry's dependency resolver requires me to put the <4.0 part for some reason 26 | parse = "1.19.0" 27 | requests = "2.26.0" 28 | textual = "0.1.11" 29 | toml = "0.10.2" 30 | rich = "10.9.0" 31 | pyperclip = "1.8.2" 32 | requests-cache = "0.8.0" 33 | markdownify = "0.9.4" 34 | 35 | [tool.poetry.dev-dependencies] 36 | flake8 = "~=3.7" 37 | flake8-annotations = "~=2.0" 38 | flake8-bandit = "~=2.1" 39 | flake8-docstrings = "~=1.5" 40 | flake8-isort = "~=4.0" 41 | isort = "~=5.9" 42 | pytest = ">=6.2.4" 43 | pytest-cov = ">=2.12.1" 44 | pytest-datadir = ">=1.3.1" 45 | pytest-randomly = ">=3.8.0" 46 | pre-commit = ">=2.13.0" 47 | 48 | # Install dev and test dependencies with the -e option of poetry install 49 | [tool.poetry.extras] 50 | test = [ 51 | "flake8~=3.7", 52 | "flake8-annotations~=2.0", 53 | "flake8-bandit~=2.1", 54 | "flake8-docstrings~=1.5", 55 | "flake8-isort~=4.0", 56 | "isort~=5.9", 57 | "pytest >= 6.2.4", 58 | "pytest-cov >= 2.12.1", 59 | "pytest-datadir >= 1.3.1", 60 | "pytest-randomly >= 3.8.0", 61 | ] 62 | dev = [ 63 | "pre-commit >= 2.13.0", 64 | ] 65 | 66 | [tool.poetry.urls] 67 | "Bug Tracker" = "https://github.com/what-the-python/wtpython/issues" 68 | 69 | [tool.poetry.scripts] 70 | wtpython = 'wtpython.__main__:main' 71 | 72 | [build-system] 73 | requires = ["poetry-core>=1.0.0"] 74 | build-backend = "poetry.core.masonry.api" 75 | 76 | [tool.coverage.run] 77 | source = ["src"] 78 | 79 | [tool.coverage.report] 80 | show_missing = true 81 | 82 | [tool.liccheck] 83 | authorized_licenses = [ 84 | "bsd", 85 | "new bsd", 86 | "bsd license", 87 | "new bsd license", 88 | "simplified bsd", 89 | "apache", 90 | "apache 2.0", 91 | "apache software", 92 | "apache software license", 93 | "gnu lgpl", 94 | "lgpl with exceptions or zpl", 95 | "GNU Library or Lesser General Public License (LGPL)", 96 | "isc license", 97 | "isc license (iscl)", 98 | "mit", 99 | "mit license", 100 | "MPL 2.0", 101 | "Mozilla Public License 2.0 (MPL 2.0)", 102 | "python software foundation license", 103 | "Python Software Foundation", 104 | "public domain", 105 | "zpl 2.1" 106 | ] 107 | unauthorized_licenses = [ 108 | "gpl v3" 109 | ] 110 | 111 | [tool.liccheck.authorized_packages] 112 | requests-cache = "0.8.0" 113 | flake8-isort = "~=4.0" 114 | -------------------------------------------------------------------------------- /tests/MANUAL_TEST.md: -------------------------------------------------------------------------------- 1 | # Manually Test the Features of WTPython 2 | 3 | * Check if it can run with `wtpython example/runs_with_error.py` 4 | - Check if pressing `q` and `ctrl + c` exits the app 5 | - Check if pressing `left`, `right`, `k`, and `j` change the question 6 | - Check if pressing `s` toggles the sidebar 7 | - Check if pressing `t` toggles the traceback 8 | - Check if pressing `d` opens the current question in the browser 9 | - Check if pressing `f` opens google 10 | - Check if pressing `i` opens the issue tracker 11 | * Check if no display mode works with `wtpython -n example/runs_with_error.py` 12 | * Check if copy mode works with `wtpython -c example/runs_with_error.py` 13 | * Check if copy and no display mode works with`wtpython -c -n example/runs_with_error.py` 14 | -------------------------------------------------------------------------------- /tests/test_system.py: -------------------------------------------------------------------------------- 1 | """Tests for the core of WTPython.""" 2 | import sys 3 | from contextlib import contextmanager 4 | from typing import Iterator 5 | 6 | import pytest 7 | 8 | import wtpython 9 | from wtpython.__main__ import parse_arguments 10 | 11 | 12 | @contextmanager 13 | def update_argv(args: list) -> Iterator[None]: 14 | """Update sys.argv.""" 15 | orig, sys.argv = sys.argv, args 16 | yield 17 | sys.argv = orig 18 | 19 | 20 | def test_version() -> None: 21 | """Test Version is Correct.""" 22 | assert wtpython.__version__ == "0.1" 23 | 24 | 25 | def test_default_parse_args() -> None: 26 | """Simple test to demonstrate how to test the arg parser.""" 27 | args = ['wtpython', __file__] 28 | with update_argv(args): 29 | parsed = parse_arguments() 30 | 31 | assert parsed.get('no-display') is None 32 | assert parsed.get('copy_error') is False 33 | assert parsed.get('clear_cache') is False 34 | assert parsed.get('args') == [__file__] 35 | 36 | 37 | @pytest.mark.parametrize("args", [ 38 | ['wtpython', '--no-display', __file__], 39 | ['wtpython', '-n', __file__], 40 | ]) 41 | def test_no_display_option(args: list) -> None: 42 | """Test that the no-display option is set.""" 43 | with update_argv(args): 44 | parsed = parse_arguments() 45 | assert parsed.get('no_display') is True 46 | 47 | 48 | @pytest.mark.parametrize("args", [ 49 | ['wtpython', '--copy-error', __file__], 50 | ['wtpython', '-c', __file__], 51 | ]) 52 | def test_copy_error_option(args: list) -> None: 53 | """Test that the copy-error option is set.""" 54 | with update_argv(args): 55 | parsed = parse_arguments() 56 | assert parsed.get('copy_error') is True 57 | 58 | 59 | @pytest.mark.parametrize("args", [ 60 | ['wtpython', '--clear-cache', __file__], 61 | ]) 62 | def test_clear_cache_option(args: list) -> None: 63 | """Test that the clear-cache option is set.""" 64 | with update_argv(args): 65 | parsed = parse_arguments() 66 | assert parsed.get('clear_cache') is True 67 | 68 | 69 | @pytest.mark.parametrize("args", [ 70 | ['wtpython'], 71 | ['wtpython', '-c'], 72 | ['wtpython', '-n'], 73 | ]) 74 | def test_exit_on_no_script(args: list) -> None: 75 | """Test script exits if no script is provided.""" 76 | with update_argv(args): 77 | with pytest.raises(SystemExit): 78 | parse_arguments() 79 | 80 | 81 | def test_exit_on_non_existent_file() -> None: 82 | """Test script exits when file does not exist.""" 83 | args = ['wtpython', 'non-existent-file.py'] 84 | with update_argv(args): 85 | with pytest.raises(SystemExit): 86 | parse_arguments() 87 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Flake8 and ISort configuration 2 | 3 | [flake8] 4 | # Increase the line length. This breaks PEP8 but it is way easier to work with. 5 | # The original reason for this limit was a standard vim terminal is only 79 characters, 6 | # but this doesn't really apply anymore. 7 | max-line-length=119 8 | # Don't lint the venv or the CPython cache. 9 | exclude=.venv,__pycache__ 10 | # Ignore some of the most obnoxious linting errors. 11 | ignore= 12 | # Missing Docstrings 13 | D105, # Missing docstring in magic method 14 | D107, # Missing docstring in __init__ 15 | # Docstring Quotes 16 | D301, # Use r"" if any backslashes in a docstring 17 | D302, # Deprecated: Use u"" for Unicode docstrings 18 | # Type Annotations 19 | ANN002, # Missing type annotation for *args 20 | ANN003, # Missing type annotation for **kwargs 21 | ANN101, # Missing type annotation for self in method 22 | ANN102, # Missing type annotation for cls in classmethod 23 | # Testing 24 | S101 # Use of Assert 25 | 26 | [isort] 27 | # Select the 5th style (Hanging grid grouped) to handle longer import. 28 | # This choice is mostly arbitrary and can be changed at your will. 29 | # 30 | # Example of this style: 31 | # from third_party import ( 32 | # lib1, lib2, lib3, lib4, 33 | # lib5, ... 34 | # ) 35 | multi_line_output=5 36 | -------------------------------------------------------------------------------- /wtpython/__init__.py: -------------------------------------------------------------------------------- 1 | """wtpython.""" 2 | __version__ = "0.1" 3 | -------------------------------------------------------------------------------- /wtpython/__main__.py: -------------------------------------------------------------------------------- 1 | """Main wtpython file. 2 | 3 | This file parses the command line arguments, runs your program, 4 | and then displays solutions for errors if they occur. 5 | """ 6 | from __future__ import annotations 7 | 8 | import argparse 9 | import os.path 10 | import runpy 11 | import sys 12 | import textwrap 13 | from typing import Optional 14 | 15 | import pyperclip 16 | from rich import print 17 | 18 | from wtpython.backends import SearchEngine, StackOverflow, Trace 19 | from wtpython.displays import TextualDisplay, dump_info 20 | from wtpython.displays.textual_display import store_results_in_module 21 | 22 | 23 | def run(args: list[str]) -> Optional[Trace]: 24 | """Execute desired program. 25 | 26 | This will set sys.argv as the desired program would receive it and executes the script. 27 | If there are no errors the program will function just like using Python but formatted with Rich. 28 | If there are errors this will return the exception object. 29 | 30 | Args: 31 | args: The arguments to pass to Python. args[0] should be the script to run. 32 | 33 | Returns: 34 | The exception object if there are errors, otherwise None. 35 | """ 36 | stashed, sys.argv = sys.argv, args 37 | exc = None 38 | try: 39 | runpy.run_path(args[0], run_name="__main__") 40 | except Exception as e: 41 | exc = Trace(e) 42 | finally: 43 | sys.argv = stashed 44 | return exc 45 | 46 | 47 | def parse_arguments() -> dict: 48 | """Parse sys.argv arguments. 49 | 50 | Args: 51 | None 52 | 53 | Returns: 54 | A dictionary of arguments. 55 | """ 56 | parser = argparse.ArgumentParser( 57 | formatter_class=argparse.RawDescriptionHelpFormatter, 58 | epilog=textwrap.dedent( 59 | """ 60 | Additional information: 61 | wtpython acts as a substitute for Python. Simply add `wt` to the beginning 62 | of the line and call your program with all the appropriate arguments: 63 | $ wtpython [OPTIONS] """ 64 | ), 65 | ) 66 | 67 | parser.add_argument( 68 | "-n", 69 | "--no-display", 70 | action="store_true", 71 | default=False, 72 | help="Run without display", 73 | ) 74 | parser.add_argument( 75 | "-c", 76 | "--copy-error", 77 | action="store_true", 78 | default=False, 79 | help="Copy error to clipboard", 80 | ) 81 | parser.add_argument( 82 | "--clear-cache", 83 | action="store_true", 84 | default=False, 85 | help="Clear StackOverflow cache", 86 | ) 87 | parser.add_argument( 88 | "args", 89 | nargs="*", 90 | help="Arguments normally passed to wtpython", 91 | ) 92 | 93 | opts = vars(parser.parse_args()) 94 | 95 | if not opts["args"]: 96 | parser.error("Please specify a script to run") 97 | sys.exit(1) 98 | 99 | if not os.path.isfile(opts["args"][0]): 100 | parser.error(f"{opts['args'][0]} is not a file") 101 | sys.exit(1) 102 | 103 | return opts 104 | 105 | 106 | def main() -> None: 107 | """Run the application. 108 | 109 | Args: 110 | None 111 | 112 | Returns: 113 | None 114 | """ 115 | opts: dict = parse_arguments() 116 | trace: Optional[Trace] = run(opts["args"]) 117 | 118 | if trace is None: # No exceptions were raised by user's program 119 | return 120 | 121 | engine = SearchEngine(trace) 122 | so = StackOverflow.from_trace(trace=trace, clear_cache=opts["clear_cache"]) 123 | 124 | print(trace.rich_traceback) 125 | 126 | if opts["copy_error"]: 127 | pyperclip.copy(trace.error) 128 | 129 | if opts["no_display"]: 130 | dump_info( 131 | so_results=so, 132 | search_engine=engine, 133 | ) 134 | else: 135 | store_results_in_module( 136 | trace=trace, 137 | so_results=so, 138 | search_engine=engine, 139 | ) 140 | try: 141 | TextualDisplay().run() 142 | except Exception as e: 143 | print(e) 144 | -------------------------------------------------------------------------------- /wtpython/backends/__init__.py: -------------------------------------------------------------------------------- 1 | """Backends for managing data and formatting text.""" 2 | from .search_engine import SearchEngine # noqa: F401 3 | from .stackoverflow import StackOverflow # noqa: F401 4 | from .trace import Trace # noqa: F401 5 | -------------------------------------------------------------------------------- /wtpython/backends/cache.py: -------------------------------------------------------------------------------- 1 | """Tools for caching requests to external APIs. 2 | 3 | Caching is used to reduce the number of requests to external APIs. 4 | """ 5 | from requests_cache import CachedSession 6 | from requests_cache.backends import FileCache 7 | 8 | from wtpython.settings import REQUEST_CACHE_DURATION, REQUEST_CACHE_LOCATION 9 | 10 | 11 | class CachedResponse: 12 | """Class for caching web queries. 13 | 14 | This class should be extended by any class that makes web queries. 15 | The `cache_key` should be defined to avoid caching conflicts. 16 | """ 17 | 18 | cache_key = 'wtpython' 19 | 20 | def __init__(self, clear_cache: bool = False) -> None: 21 | """Initialize the session and cache. 22 | 23 | Args: 24 | clear_cache: If True, clear the cache. 25 | 26 | Returns: 27 | None 28 | """ 29 | self.session = CachedSession( 30 | self.cache_key, 31 | backend=FileCache(REQUEST_CACHE_LOCATION), 32 | expire_after=REQUEST_CACHE_DURATION, 33 | ) 34 | if clear_cache: 35 | self.session.cache.clear() 36 | 37 | def __del__(self) -> None: 38 | """Close the session on exit.""" 39 | self.session.close() 40 | -------------------------------------------------------------------------------- /wtpython/backends/search_engine.py: -------------------------------------------------------------------------------- 1 | """Manages data relevant to search engines.""" 2 | from urllib.parse import urlencode 3 | 4 | from wtpython.settings import SEARCH_ENGINE 5 | 6 | from .trace import Trace 7 | 8 | 9 | class SearchEngine: 10 | """Class for handling urls for search engines.""" 11 | 12 | def __init__(self, trace: Trace, engine: str = SEARCH_ENGINE) -> None: 13 | """Search engine object. 14 | 15 | Args: 16 | trace: wtpython trace object. 17 | engine: search engine to use. 18 | 19 | Returns: 20 | SearchEngine object. 21 | """ 22 | self.query = trace.error 23 | self.engine = engine 24 | 25 | @property 26 | def url(self) -> str: 27 | """Url formatted for desired search engine.""" 28 | endpoints = { 29 | "Google": "https://www.google.com/search?", 30 | "DuckDuckGo": "https://duckduckgo.com/?", 31 | "Yahoo": "https://search.yahoo.com/search?", 32 | } 33 | 34 | params = {"q": f"python {self.query}"} 35 | return endpoints[self.engine] + urlencode(params) 36 | -------------------------------------------------------------------------------- /wtpython/backends/stackoverflow.py: -------------------------------------------------------------------------------- 1 | """Manages data from StackOverflow.""" 2 | from __future__ import annotations 3 | 4 | import html 5 | from textwrap import dedent 6 | from typing import Optional 7 | 8 | from rich.text import Text 9 | 10 | from wtpython.exceptions import SearchError 11 | from wtpython.formatters import PythonCodeConverter, rich_link 12 | from wtpython.settings import SO_MAX_RESULTS 13 | 14 | from .cache import CachedResponse 15 | from .trace import Trace 16 | 17 | 18 | class StackOverflowAnswer: 19 | """Class for visualizing StackOverflow answers. 20 | 21 | This class should only be called by the `StackOverflowQuestion` class. 22 | Answers should only be visible in the main TUI window, so `sidebar` and 23 | `no_display` are not implemented. 24 | """ 25 | 26 | def __init__(self, data: dict) -> None: 27 | """Store the json for the answer. 28 | 29 | Args: 30 | data: The json data for the answer provided by the API. 31 | """ 32 | self.data = data 33 | 34 | @property 35 | def answer_accepted(self) -> str: 36 | """Indicate if the answer is accepted.""" 37 | return ' ✔️ ' if self.data['is_accepted'] else '' 38 | 39 | def display(self) -> str: 40 | """Render information for display mode.""" 41 | converter = PythonCodeConverter() 42 | 43 | text = '\n'.join([ 44 | "---", 45 | f"### Answer: Score {self.data['score']}{self.answer_accepted}", 46 | "---", 47 | converter.convert(self.data['body']), 48 | ]) 49 | return text.lstrip() 50 | 51 | 52 | class StackOverflowQuestion: 53 | """Class for visualizing StackOverflow questions. 54 | 55 | This class should only be called by the `StackOverflow` class. 56 | This handles the display of questions and associated answers. 57 | """ 58 | 59 | def __init__(self, ix: int, data: dict) -> None: 60 | """Store the json for the question. 61 | 62 | Args: 63 | ix: The index of the question in the list of questions. 64 | data: The json data for the question provided by the API. 65 | 66 | Returns: 67 | None 68 | """ 69 | self.ix = ix 70 | self.data = data 71 | self.answers: list[StackOverflowAnswer] = [] 72 | 73 | @property 74 | def num_answers(self) -> str: 75 | """Human readable string indicating the number of answers.""" 76 | if self.data['answer_count'] == 1: 77 | return '1 Answer' 78 | else: 79 | return f'{self.data["answer_count"]} Answers' 80 | 81 | @property 82 | def answer_accepted(self) -> str: 83 | """Indicate if the question has an accepted answer.""" 84 | return ' ✔️ ' if self.data['is_answered'] else '' 85 | 86 | @property 87 | def url(self) -> str: 88 | """Return url for the question.""" 89 | return self.data['link'] 90 | 91 | @property 92 | def title(self) -> str: 93 | """Unescaped HTML title.""" 94 | return html.unescape(self.data['title']) 95 | 96 | def sidebar(self, ix: int, highlighted: Optional[int]) -> Text: 97 | """Render information for sidebar mode. 98 | 99 | Args: 100 | ix: The index of the active question. Used to determine highlighting. 101 | highlighted: The index of the hovered question. Used to determine highlighting. 102 | 103 | Returns: 104 | A rich text object for the sidebar. 105 | """ 106 | color = 'yellow' if ix == self.ix else ('grey' if highlighted == self.ix else 'white') 107 | 108 | text = Text.assemble( 109 | (f"#{self.ix + 1} ", color), 110 | (f"Score {self.data['score']}", f"{color} bold"), 111 | (f"{self.answer_accepted} - {self.title}", color), 112 | ) 113 | return text 114 | 115 | def display(self) -> str: 116 | """Render information for display mode.""" 117 | converter = PythonCodeConverter() 118 | 119 | text = '\n'.join([ 120 | "---", 121 | f"## Score {self.data['score']} | {self.title}", 122 | "---", 123 | converter.convert(self.data['body']) 124 | ]) 125 | for answer in self.answers: 126 | text += answer.display() 127 | 128 | return text 129 | 130 | def no_display(self) -> str: 131 | """Render information for no-display mode.""" 132 | return dedent(f""" 133 | Score {self.data['score']} | {self.data['title']} 134 | {rich_link(self.data['link'])} {self.num_answers} {self.answer_accepted} 135 | """).lstrip() 136 | 137 | 138 | class StackOverflow(CachedResponse): 139 | """Manage results from StackOverflow. 140 | 141 | This class can be instantiated by passing a query to the constructor 142 | or the classmethod `from_trace` will accept a trace object. 143 | 144 | This class is responsible for the API calls and managing the results. 145 | The StackOverflowQuestion and StackOverflowAnswer classes should not 146 | be called outside of this class. 147 | 148 | The main public functions of this class are: 149 | sidebar: renders information for textual sidebar. Items with self.index 150 | will be highlighted. 151 | display: renders information for textual scroll view. This will display 152 | the question and all answers for the question with self.index. 153 | no_display: renders information for no-display mode. Dumps all questions. 154 | 155 | Filter: https://api.stackexchange.com/docs/filters 156 | Create a filter: https://api.stackexchange.com/docs/create-filter 157 | This filter returns the question or answer body in addition to meta data. 158 | """ 159 | 160 | api = "https://api.stackexchange.com/2.3" 161 | cache_key = 'stackoverflow' 162 | sidebar_title = "Questions" 163 | default_params: dict[str, str] = { 164 | "site": "stackoverflow", 165 | "filter": "!6VvPDzQ)xXOrL", 166 | "order": "desc", 167 | } 168 | 169 | def __init__(self, query: str = '', clear_cache: bool = False) -> None: 170 | """Search StackOverflow API for the defined query. 171 | 172 | self.index is used to track the current question. Initialization 173 | will search for questions and fetch the associated answers. 174 | 175 | Args: 176 | query: The query to search for. 177 | clear_cache: If True, clear the cache before searching. 178 | 179 | Returns: 180 | StackOverflow object. 181 | """ 182 | super().__init__(clear_cache=clear_cache) 183 | self._query = query 184 | self.index = 0 185 | self.highlighted = None 186 | self.questions = [StackOverflowQuestion(ix, item) for ix, item in enumerate(self._get_questions())] 187 | self._get_answers() 188 | 189 | def __len__(self) -> int: 190 | """Return the number of questions found.""" 191 | return len(self.questions) 192 | 193 | def __bool__(self) -> bool: 194 | """Return whether the query has results.""" 195 | return bool(self.questions) 196 | 197 | @classmethod 198 | def from_trace(cls, trace: Trace, clear_cache: bool = False) -> StackOverflow: 199 | """Initialize from traceback. 200 | 201 | Will search for questions based on the full error message. If no results, 202 | this will fall back on just the error type. If no results, this will raise 203 | a SearchError. 204 | 205 | Args: 206 | trace: The wtpython Trace object. 207 | clear_cache: If True, clear the cache before searching. 208 | 209 | Returns: 210 | StackOverflow object. 211 | """ 212 | for query in [trace.error, trace.etype]: 213 | instance = cls(query, clear_cache) 214 | if instance: 215 | return instance 216 | 217 | raise SearchError(f"No StackOverflow results for {trace.error}") 218 | 219 | def _get_questions(self) -> dict: 220 | """Get StackOverflow questions. 221 | 222 | https://api.stackexchange.com/docs/advanced-search 223 | defaults is a list of items to include with every request. 224 | """ 225 | endpoint = f"{StackOverflow.api}/search/advanced" 226 | defaults = ['python'] 227 | params = { 228 | "q": ' '.join([*defaults, self._query]), 229 | "answers": 1, 230 | "pagesize": SO_MAX_RESULTS, 231 | **StackOverflow.default_params, 232 | } 233 | response = self.session.get(endpoint, params=params) 234 | return response.json()['items'] 235 | 236 | def _get_answers(self) -> None: 237 | """Get answers for this question. 238 | 239 | https://api.stackexchange.com/docs/answers-on-questions 240 | The answers for all questions are fetched with one api call. Answers 241 | are assigned to the questions based on the asssociated question id. 242 | """ 243 | question_ids = ";".join([ 244 | str(q.data['question_id']) 245 | for q in self.questions 246 | ]) 247 | endpoint = f"{StackOverflow.api}/questions/{question_ids}/answers" 248 | params = { 249 | "sort": "activity", 250 | **StackOverflow.default_params, 251 | } 252 | response = self.session.get(endpoint, params=params) 253 | answers = response.json()['items'] 254 | 255 | for question in self.questions: 256 | question.answers = [ 257 | StackOverflowAnswer(answer) 258 | for answer in answers 259 | if answer['question_id'] == question.data['question_id'] 260 | ] 261 | 262 | @property 263 | def active_url(self) -> str: 264 | """Return the url for the current question.""" 265 | if self.questions: 266 | return self.questions[self.index].url 267 | return "https://stackoverflow.com/search?q={self._query}" 268 | 269 | def sidebar(self) -> list[Text]: 270 | """Render information for sidebar mode. 271 | 272 | consolodate sidebar displays for all objects. ix is used to determine 273 | if the item is the current one. 274 | """ 275 | return [q.sidebar(self.index, self.highlighted) for q in self.questions] 276 | 277 | def display(self) -> str: 278 | """Render information for display mode. 279 | 280 | Get the display for the current item. 281 | """ 282 | if self.questions: 283 | return self.questions[self.index].display() 284 | return "Sorry, we could not find any results." 285 | 286 | def no_display(self) -> str: 287 | """Render information for no-display mode.""" 288 | return "\n".join([q.no_display() for q in self.questions]) 289 | -------------------------------------------------------------------------------- /wtpython/backends/trace.py: -------------------------------------------------------------------------------- 1 | """Manages information related to the traceback object.""" 2 | import traceback 3 | from pathlib import Path 4 | from types import TracebackType 5 | from typing import Optional 6 | 7 | from rich.traceback import Traceback 8 | 9 | 10 | class Trace: 11 | """Class for handling the formatting and display of tracebacks.""" 12 | 13 | def __init__(self, exc: Exception) -> None: 14 | """Store key parts to traceback. 15 | 16 | Args: 17 | exc: The exception to be formatted. 18 | """ 19 | self._etype = type(exc) 20 | self._value = exc 21 | self._tb = Trace.trim_exception_traceback(exc.__traceback__) 22 | 23 | @staticmethod 24 | def trim_exception_traceback(tb: Optional[TracebackType]) -> Optional[TracebackType]: 25 | """ 26 | Trims the traceback to remove extra frames. 27 | 28 | Because of the way we are currently running the code, any traceback 29 | created during the execution of the application will include the 30 | stack frames of this application. This function removes all the stack 31 | frames from the beginning of the traceback until we stop seeing `runpy`. 32 | 33 | Args: 34 | tb: The traceback to trim. 35 | 36 | Returns: 37 | The trimmed traceback. 38 | """ 39 | seen_runpy = False 40 | while tb is not None: 41 | cur = tb.tb_frame 42 | filename = Path(cur.f_code.co_filename).name 43 | if filename == "runpy.py": 44 | seen_runpy = True 45 | elif seen_runpy and filename != "runpy.py": 46 | break 47 | tb = tb.tb_next 48 | 49 | return tb 50 | 51 | @property 52 | def etype(self) -> str: 53 | """Error Type.""" 54 | return self._etype.__name__ 55 | 56 | @property 57 | def error(self) -> str: 58 | """Error type and value.""" 59 | _error = "".join(traceback.format_exception_only(self._etype, self._value)).strip() 60 | error_lines = _error.split("\n") 61 | if len(error_lines) > 1: 62 | _error = error_lines[-1] 63 | return _error 64 | 65 | @property 66 | def traceback(self) -> str: 67 | """Full traceback.""" 68 | return "".join(traceback.format_exception(self._etype, self._value, self._tb)) 69 | 70 | @property 71 | def rich_traceback(self) -> Traceback: 72 | """Rich formatted traceback.""" 73 | return Traceback.from_exception(self._etype, self._value, self._tb) 74 | -------------------------------------------------------------------------------- /wtpython/displays/__init__.py: -------------------------------------------------------------------------------- 1 | """Modes for displaying information to the user.""" 2 | from .no_display import dump_info # noqa: F401 3 | from .textual_display import TextualDisplay # noqa: F401 4 | -------------------------------------------------------------------------------- /wtpython/displays/no_display.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module works to dump the information for the no-display option. 3 | 4 | Each data source should have its own function while `dump_info` will 5 | control the order in which they are displayed. 6 | """ 7 | from rich import print 8 | from rich.markdown import HorizontalRule 9 | 10 | from wtpython.backends import SearchEngine, StackOverflow 11 | from wtpython.settings import SEARCH_ENGINE 12 | 13 | 14 | def _header(txt: str) -> str: 15 | """Format header for section. 16 | 17 | Args: 18 | txt: Text to display in header. 19 | 20 | Returns: 21 | Formatted header. 22 | """ 23 | print(HorizontalRule()) 24 | return f"[yellow]{txt}:[/]\n" 25 | 26 | 27 | def _stackoverflow(so: StackOverflow) -> None: 28 | """Dump Stackoverflow questions list. 29 | 30 | Args: 31 | so: Stackoverflow object. 32 | 33 | Returns: 34 | None 35 | """ 36 | print(_header("Stack Overflow Results")) 37 | print(so.no_display()) 38 | 39 | 40 | def _searchengine(search_engine: SearchEngine) -> None: 41 | """Dump url for search engine. 42 | 43 | Args: 44 | search_engine: SearchEngine object. 45 | 46 | Returns: 47 | None 48 | """ 49 | print(_header(f"Search on {SEARCH_ENGINE}")) 50 | print(search_engine.url) 51 | 52 | 53 | def dump_info(so_results: StackOverflow, search_engine: SearchEngine) -> None: 54 | """Dump information for no-display mode. 55 | 56 | The traceback message is dumped before display vs. no-display is evaluated. 57 | 58 | Args: 59 | so_results: Stackoverflow object. 60 | search_engine: SearchEngine object. 61 | 62 | Returns: 63 | None 64 | """ 65 | _stackoverflow(so_results) 66 | _searchengine(search_engine) 67 | print() 68 | -------------------------------------------------------------------------------- /wtpython/displays/textual_display.py: -------------------------------------------------------------------------------- 1 | """TUI using Textual.""" 2 | from __future__ import annotations 3 | 4 | import webbrowser 5 | from typing import Optional 6 | 7 | from rich.console import Console, RenderableType 8 | from rich.markdown import Markdown 9 | from rich.panel import Panel 10 | from rich.text import Text 11 | from textual import events 12 | from textual.app import App 13 | from textual.geometry import Size 14 | from textual.views import DockView 15 | from textual.widget import Reactive, Widget 16 | from textual.widgets import Footer, Header, ScrollView 17 | 18 | from wtpython.backends import SearchEngine, StackOverflow, Trace 19 | from wtpython.settings import APP_NAME, GH_ISSUES 20 | 21 | TRACE: Trace = Trace(Exception()) 22 | SO_RESULTS: StackOverflow = StackOverflow("python") 23 | SEARCH: SearchEngine = SearchEngine(Trace(Exception())) 24 | 25 | 26 | def store_results_in_module( 27 | trace: Trace, so_results: StackOverflow, search_engine: SearchEngine 28 | ) -> None: 29 | """Error with passing values to display; this is our temporary solution. 30 | 31 | Display inherits App and somewhere in the App.__init__ flow values are 32 | overwritten. These global variables are used in lieu of passing to 33 | display for now. 34 | 35 | These values are used in `Display.on_startup` 36 | """ 37 | global SO_RESULTS, TRACE, SEARCH 38 | SO_RESULTS = so_results 39 | TRACE = trace 40 | SEARCH = search_engine 41 | 42 | 43 | class Sidebar(Widget): 44 | """Sidebar widget to display list of questions.""" 45 | 46 | index: Reactive[int] = Reactive(0) 47 | page: Reactive[int] = Reactive(0) 48 | highlighted: Reactive[Optional[int]] = Reactive(None) 49 | 50 | def __init__( 51 | self, 52 | name: Optional[str], 53 | so: StackOverflow, 54 | ) -> None: 55 | self.so: StackOverflow = so 56 | super().__init__(name=name) 57 | self._text: Optional[Panel] = None 58 | self.pages: Optional[list[Text]] = None 59 | self.pages_index: dict[int, int] = {} 60 | 61 | @staticmethod 62 | def check_overflow(contents: list[Text], console: Console, size: Size) -> bool: 63 | """ 64 | Check if the sidebar is overflowing or not. 65 | 66 | It renders the sidebar, then checks if the controls are visible. 67 | Used to generate pages. 68 | Args: 69 | contents: List of sidebar entries 70 | console: App console 71 | size: Console size 72 | Returns: 73 | Returns whether the sidebar is overflowing or not 74 | """ 75 | page = Text(end="") 76 | for content in contents: 77 | page.append_text(content) 78 | page.append_text(Text("\n\n")) 79 | page.append_text(Text("<- Prev Page 0/0 Next ->")) 80 | 81 | panel = Panel(page, title="Questions") 82 | 83 | output = panel.__rich_console__(console, console.options.update_dimensions(size[0], size[1])) 84 | output = list(output) 85 | output = [i.text for i in output] 86 | 87 | output = "".join(output) 88 | 89 | return "<- Prev Page 0/0 Next ->" not in output 90 | 91 | @staticmethod 92 | def get_height(text: Text, console: Console, size: Size) -> int: 93 | """Get the height of the side panel with the sidebar entries. 94 | 95 | Used for figuring out the location of the page controls 96 | """ 97 | panel = Panel(text) 98 | 99 | output = panel.__rich_console__(console, console.options.update_dimensions(size[0], size[1] + 50)) 100 | output = list(output) 101 | output = [i.text for i in output] 102 | output = "".join(output).split("\n") 103 | 104 | output = output[1:-2] 105 | 106 | output.reverse() 107 | blank_line = output[0] 108 | output2 = output.copy() # output2 is used because mutating output while in a loop has weird results 109 | 110 | for i in output: 111 | if i == blank_line: 112 | output2.pop(0) 113 | else: 114 | break 115 | 116 | output2.reverse() 117 | 118 | return len(output2) 119 | 120 | async def watch_page(self, value: Optional[int]) -> None: 121 | """If page changes, regenerate the text.""" 122 | self._text = None 123 | 124 | async def watch_index(self, value: Optional[int]) -> None: 125 | """If index changes, regenerates the text.""" 126 | self._text = None 127 | self.so.index = self.index 128 | self.page = self.pages_index[self.index] 129 | 130 | async def watch_highlighted(self, value: Optional[int]) -> None: 131 | """If highlight key changes we need to regenerate the text.""" 132 | self._text = None 133 | self.so.highlighted = self.highlighted 134 | 135 | async def on_mouse_move(self, event: events.MouseMove) -> None: 136 | """Store any key we are moving over.""" 137 | self.highlighted = event.style.meta.get("index") 138 | 139 | async def on_leave(self, event: events.Leave) -> None: 140 | """Clear any highlights when the mouse leaves the widget.""" 141 | self.highlighted = None 142 | 143 | async def on_resize(self, event: events.Resize) -> None: 144 | """Update the pages on resize.""" 145 | self._text = None 146 | 147 | def update_pages(self) -> None: 148 | """Update the pages and the pages index.""" 149 | current_page = Text(end="") 150 | current_page_container: list[int] = [] 151 | current_page_contents: list[Text] = [] 152 | pages_index: dict[int, int] = {} 153 | pages: list[Text] = [] 154 | on_next = None 155 | length = 0 156 | 157 | for i, item_text in enumerate(self.so.sidebar()): 158 | if on_next is not None: 159 | current_page_contents.append(on_next[0]) 160 | current_page_container.append(on_next[1]) 161 | on_next = None 162 | 163 | current_page_contents.append(item_text) 164 | current_page.append_text(item_text) 165 | current_page.append_text(Text("\n\n")) 166 | current_page_container.append(i) 167 | 168 | overflow = len(current_page_contents) != 1 and \ 169 | self.check_overflow(current_page_contents, self.app.console, self.size) 170 | 171 | if overflow: 172 | page = Text(end="") 173 | on_next = (current_page_contents.pop(), current_page_container.pop()) 174 | 175 | for index, i in enumerate(current_page_contents): 176 | index += length 177 | i.apply_meta({"@click": f"app.set_index({index})", "index": index}) # type: ignore 178 | page.append_text(i) 179 | page.append_text(Text("\n\n")) 180 | length += len(current_page_contents) 181 | for i in current_page_container: 182 | pages_index[i] = len(pages) 183 | pages.append(page) 184 | current_page_contents = [] 185 | current_page_container = [] 186 | current_page = Text(end="") 187 | 188 | if on_next is not None: 189 | current_page_contents.append(on_next[0]) 190 | current_page_container.append(on_next[1]) 191 | if len(current_page_contents) != 0: 192 | page = Text(end="") 193 | for index, i in enumerate(current_page_contents): 194 | index += length 195 | i.apply_meta({"@click": f"app.set_index({index})", "index": index}) # type: ignore 196 | page.append_text(i) 197 | page.append_text(Text("\n\n")) 198 | for i in current_page_container: 199 | pages_index[i] = len(pages) 200 | pages.append(page) 201 | 202 | self.pages_index = pages_index 203 | self.pages = pages 204 | 205 | def render(self) -> RenderableType: 206 | """Render the panel.""" 207 | if self._text is None: 208 | self.update_pages() 209 | try: 210 | page = self.pages[self.page] # type: ignore 211 | except IndexError: 212 | page = self.pages[len(self.pages) - 1] # type: ignore 213 | 214 | if len(self.pages) > 1: # type: ignore 215 | height = self.get_height(page, self.app.console, self.size) 216 | 217 | extra_lines = self.size.height - height - 4 218 | 219 | page.append_text( 220 | Text( 221 | "\n" * extra_lines 222 | ) 223 | ) 224 | width = self.size.width - 2 225 | 226 | page.append_text( 227 | Text( 228 | " " * ( 229 | (width - len(f"<- Prev Page {self.page}/{len(self.pages)} Next ->")) // 2 # type: ignore 230 | ) 231 | ) 232 | ) 233 | 234 | page.append_text( 235 | Text.assemble( 236 | ( 237 | "<- Prev", 238 | ("grey" if self.highlighted == -1 else "white") if self.page > 0 else "#4f4f4f" 239 | ), 240 | meta={"@click": ("app.prev_page" if self.page > 0 else "app.bell"), "index": -1} 241 | ) 242 | ) 243 | 244 | page.append_text( 245 | Text.assemble( 246 | (f" Page {self.page + 1}/{len(self.pages)} ", "yellow") # type: ignore 247 | ) 248 | ) 249 | 250 | page.append_text( 251 | Text.assemble( 252 | ( 253 | "Next ->", 254 | ("grey" if self.highlighted == -2 else "white") 255 | if self.page + 1 < len(self.pages) else "#4f4f4f" # type: ignore 256 | ), 257 | meta={ 258 | "@click": ( 259 | "app.next_page" if self.page + 1 < len(self.pages) else "app.bell" # type: ignore 260 | ), 261 | "index": -2 262 | } 263 | ) 264 | ) 265 | self._text = Panel(page, title=self.so.sidebar_title) 266 | 267 | return self._text 268 | 269 | 270 | class TextualDisplay(App): 271 | """wtpython application.""" 272 | 273 | async def on_load(self, event: events.Load) -> None: 274 | """Key bindings.""" 275 | await self.bind("q", "quit", show=False) 276 | await self.bind( 277 | "ctrl+c", "quit", description="Quit", key_display="ctrl+c", show=True 278 | ) 279 | 280 | await self.bind("left", "prev_question", description="Prev", key_display="←") 281 | await self.bind("right", "next_question", description="Next", key_display="→") 282 | 283 | await self.bind("s", "view.toggle('sidebar')", description="Sidebar") 284 | await self.bind("t", "show_traceback", description="Toggle Traceback") 285 | 286 | await self.bind("d", "open_browser", description="Open Browser") 287 | await self.bind("f", "open_search_engine", description="Search Engine") 288 | await self.bind("i", "report_issue", description="Report Issue") 289 | 290 | # Vim shortcuts... 291 | await self.bind("k", "prev_question", show=False) 292 | await self.bind("j", "next_question", show=False) 293 | 294 | def create_body_text(self) -> RenderableType: 295 | """Generate the text to display in the scroll view.""" 296 | if self.viewing_traceback: 297 | return TRACE.rich_traceback 298 | 299 | SO_RESULTS.index = self.index 300 | output = Markdown(SO_RESULTS.display(), inline_code_lexer="python") 301 | return output 302 | 303 | async def update_body(self) -> None: 304 | """Update the scroll view body.""" 305 | await self.body.update(self.create_body_text()) 306 | self.body.y = 0 307 | self.body.target_y = 0 308 | 309 | async def action_set_index(self, index: int) -> None: 310 | """Set question index.""" 311 | self.sidebar.index = index 312 | SO_RESULTS.index = index 313 | self.index = index 314 | 315 | await self.update_body() 316 | 317 | async def action_next_question(self) -> None: 318 | """Go to the next question.""" 319 | if self.index + 1 < len(SO_RESULTS): 320 | self.viewing_traceback: bool = False 321 | self.index += 1 322 | await self.update_body() 323 | self.sidebar.index = self.index 324 | SO_RESULTS.index = self.index 325 | 326 | async def action_prev_question(self) -> None: 327 | """Go to the previous question.""" 328 | if self.index: 329 | self.viewing_traceback = False 330 | self.index -= 1 331 | await self.update_body() 332 | self.sidebar.index = self.index 333 | SO_RESULTS.index = self.index 334 | 335 | async def action_next_page(self) -> None: 336 | """Go to the next page.""" 337 | self.sidebar.page += 1 338 | 339 | async def action_prev_page(self) -> None: 340 | """Go to the previous page.""" 341 | self.sidebar.page -= 1 342 | 343 | async def action_open_browser(self) -> None: 344 | """Open the question in the browser.""" 345 | webbrowser.open(SO_RESULTS.active_url) 346 | 347 | async def action_report_issue(self) -> None: 348 | """Take user to submit new issue on Github.""" 349 | webbrowser.open(GH_ISSUES) 350 | 351 | async def action_open_search_engine(self) -> None: 352 | """Open the browser with search engine results.""" 353 | webbrowser.open(SEARCH.url) 354 | 355 | async def action_show_traceback(self) -> None: 356 | """Show the traceback.""" 357 | self.viewing_traceback = not self.viewing_traceback 358 | await self.update_body() 359 | 360 | async def on_mount(self, event: events.Mount) -> None: 361 | """Execute main program.""" 362 | self.title = f"{APP_NAME} | {TRACE.error}" 363 | 364 | view = await self.push_view(DockView()) 365 | self.index = 0 366 | self.viewing_traceback = False 367 | header = Header() 368 | footer = Footer() 369 | self.sidebar: Sidebar = Sidebar("sidebar", SO_RESULTS) 370 | self.body: ScrollView = ScrollView(self.create_body_text()) 371 | 372 | await view.dock(header, edge="top") 373 | await view.dock(footer, edge="bottom") 374 | await view.dock(self.sidebar, edge="left", size=35) 375 | await view.dock(self.body, edge="right") 376 | -------------------------------------------------------------------------------- /wtpython/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom exceptions for wtpython.""" 2 | from rich import print 3 | from rich.markdown import HorizontalRule 4 | 5 | from wtpython.formatters import rich_link 6 | from wtpython.settings import GH_ISSUES 7 | 8 | 9 | class WTPythonError(Exception): 10 | """Generic error for wtpython. 11 | 12 | For any internal application error this will surround the traceback 13 | with horizontal rules and print a message to the user. 14 | """ 15 | 16 | def __init__(self, *args) -> None: 17 | print(HorizontalRule()) 18 | super().__init__(*args) 19 | 20 | def __del__(self) -> None: 21 | print(HorizontalRule()) 22 | print("[red]We're terribly sorry, but wtpython has encountered an issue.") 23 | print( 24 | "[bold][green]Please let us know by by opening a new issue at:[/]" 25 | f"{rich_link(GH_ISSUES)}" 26 | ) 27 | print("Please include the information between the horizontal rules above.") 28 | 29 | 30 | class SearchError(WTPythonError): 31 | """Custom error for searching for external data.""" 32 | -------------------------------------------------------------------------------- /wtpython/formatters.py: -------------------------------------------------------------------------------- 1 | """Utility classes for formatting text.""" 2 | from typing import Any, Optional 3 | 4 | from markdownify import MarkdownConverter 5 | 6 | 7 | def rich_link(url: str, text: Optional[str] = None) -> str: 8 | """ 9 | Create a link to a URL. 10 | 11 | Args: 12 | url: The URL to link to. 13 | text: The text to display for the link. Defaults to the URL. 14 | 15 | Returns: 16 | The URL formatted with Rich's link syntax. 17 | """ 18 | if text is None: 19 | text = url 20 | return f"[link={url}]{text}[/link]" 21 | 22 | 23 | class PythonCodeConverter(MarkdownConverter): 24 | """Overrides MarkdownConverter to ensure Python syntax highlighting.""" 25 | 26 | def convert_pre(self, el: Any, text: str, convert_as_inline: bool) -> str: 27 | """Add Python syntax to all
 elements.
28 | 
29 |         Args:
30 |             el: The element to convert.
31 |             text: The text to convert.
32 |             convert_as_inline: Whether to convert the element as inline.
33 | 
34 |         Returns:
35 |             text of 
 element primed for Python syntax highlighting.
36 |         """
37 |         if not text:
38 |             return ""
39 |         return "\n```py\n%s\n```\n" % text
40 | 


--------------------------------------------------------------------------------
/wtpython/settings.py:
--------------------------------------------------------------------------------
 1 | """Default settings for wtpython."""
 2 | from pathlib import Path
 3 | 
 4 | BASE_DIR = Path(__file__).resolve().parent
 5 | APP_NAME = "WTPython"
 6 | GH_ISSUES = "https://github.com/what-the-python/wtpython/issues"
 7 | 
 8 | SO_MAX_RESULTS = 10
 9 | SEARCH_ENGINE = 'Google'
10 | 
11 | REQUEST_CACHE_LOCATION = Path.home() / Path(".wtpython_cache")
12 | REQUEST_CACHE_DURATION = 60 * 60 * 24   # One day (in seconds)
13 | 


--------------------------------------------------------------------------------