commit fb7fec7ea619318b0e0830c0f9260335c0d4e173 Author: Daniel Tomlinson Date: Sun Nov 21 13:58:52 2021 +0000 feat: initial commit diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..47dfca3 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,10 @@ +[report] +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError + +[run] +source=tembo diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml new file mode 100644 index 0000000..6021bae --- /dev/null +++ b/.github/workflows/analysis.yml @@ -0,0 +1,62 @@ +name: analysis + +on: + push: + branches: + - main + +jobs: + analysis: + runs-on: ubuntu-latest + steps: + # Checkout repo & install Python + - name: Check out repository + uses: actions/checkout@v2 + with: + # disable shallow clones for better sonarcloud accuracy + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + # Install & configure Poetry + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + # Load cached venv + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + # Install dependencies if no cache + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + # Install package if needed + - name: Install package + run: poetry install --no-interaction + # Run tests + - name: Run tests + run: | + source .venv/bin/activate + pytest -v --cov --junitxml=pytest.xml --suppress-tests-failed-exit-code + coverage xml -i + sed -i 's/\/home\/runner\/work\/tembo-core\/tembo-core\///g' coverage.xml + sed -i 's/\/home\/runner\/work\/tembo-core\/tembo-core\///g' pytest.xml + # Build docs + - name: Build docs + run: | + source .venv/bin/activate + coverage html + mkdocs gh-deploy + # Upload to sonarcloud + - name: SonarCloud Scan + uses: sonarsource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..cdf92b2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,63 @@ +name: tests + +on: + pull_request: + types: + - opened + - synchronize + - reopened + +jobs: + tests: + runs-on: ubuntu-latest + steps: + # Checkout repo & install Python + - name: Check out repository + uses: actions/checkout@v2 + with: + # disable shallow clones for better sonarcloud accuracy + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + # Install & configure Poetry + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + # Load cached venv + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + # Install dependencies if no cache + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + # Install package if needed + - name: Install package + run: poetry install --no-interaction + # Run tests + - name: Run tests + run: | + source .venv/bin/activate + pytest -v --cov --junitxml=pytest.xml + coverage xml -i + sed -i 's/\/home\/runner\/work\/tembo-core\/tembo-core\///g' coverage.xml + sed -i 's/\/home\/runner\/work\/tembo-core\/tembo-core\///g' pytest.xml + # Run linting + - name: Run prospector linting + run: | + source .venv/bin/activate + prospector "./tembo" + # Upload to sonarcloud + - name: SonarCloud Scan + uses: sonarsource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e38fc2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,145 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# custom +.vscode/ +**/__pycache__ +**/.DS_Store +.python-version +codecov diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..826a83f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + + +## [0.1.0](https://github.com/tembo-pages/tembo-core/releases/tag/0.1.0) - 2021-11-20 + +[Compare with first commit](https://github.com/tembo-pages/tembo-core/compare/8884a942c5c2a2815a1bbc75fb106555402d2055...0.1.0) + +### Features +- update duties ([e2b4ef9](https://github.com/tembo-pages/tembo-core/commit/e2b4ef9f91c484d0d26ee5afcd308b6470f46370) by Daniel Tomlinson). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9447d91 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at [INSERT EMAIL ADDRESS]. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c57b892 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2021 Daniel Tomlinson + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e0a386 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Tembo + + + +A simple folder organiser for your work notes. + +![](https://img.shields.io/codecov/c/github/tembo-pages/tembo-core?style=flat-square) + +![Sonar Coverage](https://img.shields.io/sonar/coverage/tembo-pages_tembo-core?server=https%3A%2F%2Fsonarcloud.io&style=flat-square) +![Sonar Tests](https://img.shields.io/sonar/tests/tembo-pages_tembo-core?compact_message&failed_label=failed&passed_label=passed&server=https%3A%2F%2Fsonarcloud.io&skipped_label=skipped&style=flat-square) +![Sonar Tech Debt](https://img.shields.io/sonar/tech_debt/tembo-pages_tembo-core?server=https%3A%2F%2Fsonarcloud.io&style=flat-square) + +## config.yml + +```yaml +# time tokens: https://strftime.org +tembo: + base_path: ~/tembo + # template_path: ~/tembo/templates + scopes: + - name: scratchpad + example: tembo new scratchpad + path: "scratchpad/{d:%B_%Y}" + filename: "{d:%B_%W}" + extension: md + template_filename: scratchpad.md.tpl + - name: wtodo + example: tembo new wtodo | directory is month_year, filename is month_week-of-year + path: "wtodo/{d:%B_%Y}" + filename: "week_{d:%W}" + extension: todo + template_filename: weekly.todo.tpl + - name: meeting + example: tembo new meeting $meeting_title + path: "meetings/{d:%B_%y}" + filename: "{d:%a_%d_%m_%y}-{input0}" + extension: md + template_filename: meeting.md.tpl + - name: knowledge + example: tembo new knowledge $project $filename + path: "knowledge/{input0}" + filename: "{input1}" + extension: md + template_filename: knowledge.md.tpl + logging: + level: INFO + path: ~/tembo/.logs +``` + +## templates + +### knowledge + +``` +--- +created: {d:%d-%m-%Y} +--- + +# {input0} - {input1}.md +``` + +### meeting + +``` +--- +created: {d:%d-%m-%Y} +--- + +# {d:%A %d %B %Y} - {input0} + +## People + +Head: + +Attendees: + +## Actions + + +## Notes + +``` + +### scratchpad + +``` +--- +created: {d:%d-%m-%Y} +--- + +# Scratchpad - Week {d:%W} - {d:%B-%y} +``` + +### wtodo + +``` +--- +created: {d:%d-%m-%Y} +--- + +Weekly TODO | Week {d:%W} {d:%B}-{d:%Y} + +Work: + +Documentation: +``` diff --git a/TODO.todo b/TODO.todo new file mode 100644 index 0000000..cbc904e --- /dev/null +++ b/TODO.todo @@ -0,0 +1,152 @@ +Priority: + ✔ Version duty @done(21-11-09 23:54) + ✔ Duty to `poetry build` and extract + copy `setup.py` to root @done(21-11-10 22:14) + ✘ Duty to run tests in isolation @cancelled(21-11-10 22:30) + ✔ Docstrings @done(21-11-15 21:37) + ✔ Update trilium with latest docstrings (documenting __init__ at class level etc) @done(21-11-15 21:37) + Make sure the gist is updated for prospector with the right ignores + ✔ Go over Panaetius @done(21-11-15 21:37) + ✔ Update docstrings with latest @done(21-11-15 21:37) + ✔ Write basic README.md page with 2 uses @done(21-11-15 21:37) + Script and Module usage + ✔ Build and publish latest version @done(21-11-15 21:37) + ☐ Write Tembo documentation with mkdocs + ✔ Document duties in Trilium and create a gist for common duties @done(21-11-15 21:37) + ☐ Document writing documentation in Trilium with example to Trilium + ☐ Integrate sonarcloud with CI (github actions?) + +Documentation: + Docstrings: + ✔ Use Duty to write module docstrings @done(21-11-15 21:37) + ✔ Use Duty to add Class docstrings @done(21-11-15 21:37) + ✔ Document these in Trilium and rewrite the docstrings notes @done(21-11-15 21:37) + ☐ Add the comment on Reddit (artie buco?) about imports in a module + + ✔ Add the CLI initialisation boilerplate to trilium @done(21-11-10 22:50) + ✔ _version, base command, __init__.py etc @done(21-11-10 22:49) + ☐ Write documentation using `mkdocs` + ✔ Create a boilerplate `duties.py` for common tasks for future projects. Put in a gist. @done(21-11-15 21:37) + ☐ Look at how to use github actions + Use for an example + ☐ Build the docs using a github action. + +Functionality: + ✔ Use the python runner Duty @done(21-11-15 21:37) + + ✔ Run tests @done(21-11-10 22:54) + ✔ Update poetry @done(21-11-10 22:54) + ☐ Build docs + ✔ Document using Duty @done(21-11-15 21:37) + ✔ Duty for auto insert version from `poetry version`. @done(21-11-09 23:53) + Need to decide what file to place `__version__` in. + +VSCode: + PyInstaller: + ☐ Document build error: + PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.8.11 mac + PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.8.11 linux + ☐ Freeze a click app: + ☐ If python 3.9 can be used with Pyinstaller, rewrite the code to use the latest Python features + dict.update -> |= + walrus := + +VSCode: + ☐ Look at + +Logging: + Documentation: + Tembo: + ☐ Document creating new Tembo config + ☐ ~/tembo needs creating + ☐ ~/tembo/.config + ☐ ~/tembo/.templates + ☐ ~/tembo/logs + ☐ Document how to overwrite these with ENV vars + ☐ have a git repo with all the above already configured and walk user through + clone the repo, delete .git, git init, configure and add git origin + +Archive: + ✘ test logs: @cancelled(21-11-09 23:15) @project(Priority) + document this + ✔ Write the tests @done(21-11-07 15:36) @project(Priority) + ✔ documented poetry with extras (panaetius `pyproject.toml`) @done(21-11-09 22:29) @project(Documentation) + ✔ Document using `__main__.py` and `cli.py` @done(21-11-07 15:21) @project(Documentation.Docstrings) + Use Duty as an example + ✔ Document regex usage @done(21-11-09 22:39) @project(Documentation) + ✔ Document how to use pytest to read a logging message @done(21-11-09 22:57) @project(Documentation) + + caplog as fixture + reading `caplog.records[0].message` + see `_old_test_pages.py` + ✔ Document testing value of an exception raised @done(21-11-09 22:50) @project(Documentation) + When you use `with pytest.raises` you can use `.value` to access the attributes + reading `.value.code` + reading `str(.value)` + ✔ Document working with exceptions @done(21-11-09 22:17) @project(Documentation) + ✔ General pattern - raise exceptions in codebase, catch them in the CLI. @done(21-11-09 22:16) @project(Documentation) + Allows people to use via an API and handle the exceptions themselves. + You can use python builtins but custom exceptions are better for internal control + ✔ Capturing exceptions in the CLI. @done(21-11-09 22:16) @project(Documentation) + Access the message of the exception with `.args[0]`. + use `raise SystemExit(1) from exception` in order to gracefully exit + ✔ Adding custom args to an exception @done(21-11-09 22:17) @project(Documentation) + Overwrite `__init__`, access them in pytest with `.value.$args` + Access them in a try,except with `raise $excpetion as $name; $name.$arg` + ✔ Document capturing stdout @done(21-11-09 22:59) @project(Documentation) + Use `capsys` + `assert capsys.readouterr().out` + A new line may be inserted if using `click.echo()` + + ✔ Document using datadir with a module rather than a shared one. Link to tembo as an example. @done(21-11-09 22:59) @project(Documentation) + ✘ Can prospector ignore tests dir? document this in the gist if so @cancelled(21-11-09 23:08) @project(Documentation) + ✔ Redo the documentation on a CLI, reorganise and inocropoate all the new tembo layouts @done(21-11-09 23:08) @project(Documentation) + ✔ Document importing in inidivudal tests using `importlib.reload` @done(21-11-09 23:13) @project(Documentation.Testing) + Globally import the module + Use `importlib.reload(module)` in each test instead of explicitly importing the module. + This is because the import is cached. + + ✔ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? @done(21-11-09 22:17) @project(Functionality) + Define a format: [TEMBO:$datetime] $message 🐘 - document this in general python for CLI + ✔ Refactor the tembo new command so the cli is split out into manageable methods @done(21-11-07 15:35) @project(Functionality) + ✘ Use the complicated CLI example so the tembo new has its own module to define functions in @cancelled(21-11-07 15:35) @project(Functionality) + ✔ Replace all logger errors with exceptions, move logger messages to the cli. @done(21-11-07 15:35) @project(Functionality) + ✔ How to pass a successful save notification back to the CLI? Return a bool? Or is there some other way? @done(21-11-07 15:35) @project(Functionality) + ✘ Replace pendulum with datetime @cancelled(21-11-07 15:35) @project(Functionality) + ✔ Make options a property on the class, add to abstract @done(21-10-30 19:31) @project(Functionality) + ✘ Make all internal tembo logs be debug @cancelled(21-11-09 22:20) @project(Logging) + ✘ User can enable them with the config @cancelled(21-11-09 22:20) @project(Logging) + ✔ Write tests! @2d @done(21-11-07 15:36) @project(Tests) + Use coverage as going along to make sure all bases are covered in the testing + ✔ Document the python/logging/typing in Trilium @done(21-10-25 14:33) @project(Priority) + ✔ Update typing annotations to include generics instead @done(21-10-25 22:38) @project(Priority) + https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes + ✔ Move any `tembo.CONFIG` calls out of `pages.py` and ensure these are passed in from the cli. @done(21-10-28 19:44) @project(Functionality) + ✔ Make `config scope` a dict in `cli.py`. @done(21-10-28 19:44) @project(Functionality) + ✔ Make example optional @done(21-10-29 00:15) @project(Functionality) + ✔ Add the `--example` output to the miscounted token message so the user knows the correct command to use. @done(21-10-29 00:15) @project(Functionality) + ✔ Page options dataclass @done(21-10-28 20:09) @project(Functionality) + ✔ Make user_input optional @important @done(21-10-30 03:20) @project(Functionality) + ✔ Look at `_convert_to_path()` and see if it can be rewritten to make it clearer when there isn't a base path. @done(21-10-30 02:14) @project(Functionality) + ✔ Replace scoped page creator inputs so that the whole class uses the options dict rather than the variables passed around. @done(21-10-30 03:20) @project(Functionality) + ✔ Go through code TODOs @done(21-10-25 05:52) @project(Priority) + ✔ Check code order and make sure things are where they should be @done(21-10-25 13:31) @project(Priority) + ✔ Add version option @done(21-10-25 13:40) @project(Functionality) + ✘ Document usage of Panaetius in a module @cancelled(21-10-25 13:31) @project(Logging.Documentation) + ✘ Uses `strftime` tokens: @cancelled(21-10-25 13:32) @project(Logging.Documentation) + ✔ Document latest typing. @done(21-10-25 14:14) @project(Logging.Documentation) + ✔ Using from `__future__` with `|` @done(21-10-25 13:48) @project(Logging.Documentation) + ✔ `using Tuple[str, ...]` @done(21-10-25 13:49) @project(Logging.Documentation) + ✔ `Sequence` vs `Collection` @done(21-10-25 13:55) @project(Logging.Documentation) + ✔ Document how to do docstrings in python. Don't document `__init__` do it in class. @done(21-10-25 13:57) @project(Logging.Documentation) + ✔ Document using jinja2 briefly and link to Tembo (link to ) @done(21-10-25 14:21) @project(Logging.Documentation) + ✔ How to raise + debug an exception? @done(21-10-25 14:32) @project(Logging.Documentation.Logging) + ✔ Document how to raise a logger.critical instead of exception @done(21-10-25 14:32) @project(Logging.Documentation.Logging) + ✔ tokens @done(21-10-25 05:35) @project(Bug) + ✔ Handle case where there are no scopes in the config and command is invoked. @done(21-10-25 04:32) @project(Functionality) + ✔ Have an `--example` flag to `new` that prints an example given in the `config.yml` @done(21-10-25 04:55) @project(Functionality) + ✔ Should be a `tembo new --list` to list all possible names. @done(21-10-25 05:28) @project(Functionality) + ✘ When template not found, raise a Tembo error @cancelled(21-10-25 05:29) @project(Functionality) + ✔ Convert spaces to underscores in filepath @done(21-10-25 05:35) @project(Functionality) + ✘ Add update notification? @cancelled(21-10-25 05:29) @project(Functionality) + ✔ `TEMBO_CONFIG` should follow same pattern as other env vars and be a python string when read in @done(21-10-24 05:31) @project(Functionality) + ✘ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens @cancelled(21-10-24 05:32) @project(Logging.Documentation) diff --git a/assets/tembo_logo.png b/assets/tembo_logo.png new file mode 100644 index 0000000..f65ca98 Binary files /dev/null and b/assets/tembo_logo.png differ diff --git a/docs/assets/tembo_doc_logo.png b/docs/assets/tembo_doc_logo.png new file mode 100644 index 0000000..99bf9e7 Binary files /dev/null and b/docs/assets/tembo_doc_logo.png differ diff --git a/docs/cli_reference/main.md b/docs/cli_reference/main.md new file mode 100644 index 0000000..89ff963 --- /dev/null +++ b/docs/cli_reference/main.md @@ -0,0 +1,7 @@ +# CLI Reference + +::: mkdocs-click + :module: tembo.cli.cli + :command: main + :prog_name: tembo + :style: table diff --git a/docs/css/extra.css b/docs/css/extra.css new file mode 100644 index 0000000..79293ef --- /dev/null +++ b/docs/css/extra.css @@ -0,0 +1,4 @@ +:root { + --md-primary-bg-color: #ee0f0f; + --md-primary-bg-color--light: #ee0f0f; +} diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css new file mode 100644 index 0000000..42c7741 --- /dev/null +++ b/docs/css/mkdocstrings.css @@ -0,0 +1,6 @@ +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: 4px solid rgba(230, 230, 230); + margin-bottom: 80px; +} diff --git a/docs/gen_reference.py b/docs/gen_reference.py new file mode 100644 index 0000000..0c4654c --- /dev/null +++ b/docs/gen_reference.py @@ -0,0 +1,28 @@ +"""Generate the code reference pages and navigation.""" + +from pathlib import Path + +import mkdocs_gen_files + +PACKAGE_NAME = "tembo" + +nav = mkdocs_gen_files.Nav() + +for path in sorted(Path(PACKAGE_NAME).glob("**/*.py")): + module_path = path.relative_to(PACKAGE_NAME).with_suffix("") + doc_path = path.relative_to(PACKAGE_NAME).with_suffix(".md") + full_doc_path = Path("code_reference", doc_path) + + parts = list(module_path.parts) + parts[-1] = f"{parts[-1]}.py" + nav[parts] = doc_path + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + code_ident = ".".join(module_path.parts) + print("::: " + PACKAGE_NAME + "." + code_ident, file=fd) + + mkdocs_gen_files.set_edit_path(full_doc_path, path) + + +with mkdocs_gen_files.open("code_reference/SUMMARY.md", "w") as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/home/changelog.md b/docs/home/changelog.md new file mode 100644 index 0000000..786b75d --- /dev/null +++ b/docs/home/changelog.md @@ -0,0 +1 @@ +--8<-- "CHANGELOG.md" diff --git a/docs/home/license.md b/docs/home/license.md new file mode 100644 index 0000000..616b86c --- /dev/null +++ b/docs/home/license.md @@ -0,0 +1,3 @@ +``` +--8<-- "LICENSE.md" +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..22773e7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,8 @@ +# Tembo + +```python +from tembo import Success +``` + +v0.0.8 + diff --git a/duties.py b/duties.py new file mode 100644 index 0000000..904dfdb --- /dev/null +++ b/duties.py @@ -0,0 +1,449 @@ +from __future__ import annotations + +import importlib +import os +import pathlib +import re +import shutil +import sys +from io import StringIO +from typing import List, Optional, Pattern +from urllib.request import urlopen + +from duty import duty + +PACKAGE_NAME = "tembo" + + +@duty(post=["export"]) +def update_deps(ctx, dry: bool = False): + """ + Update the dependencies using Poetry. + + Args: + ctx: The context instance (passed automatically). + dry (bool, optional) = If True will update the `poetry.lock` without updating the + dependencies themselves. Defaults to False. + + Example: + `duty update_deps dry=False` + """ + dry_run = "--dry-run" if dry else "" + ctx.run( + ["poetry", "update", dry_run], + title=f"Updating poetry deps {dry_run}", + ) + + +@duty +def test(ctx): + """ + Run tests using pytest. + + Args: + ctx: The context instance (passed automatically). + """ + pytest_results = ctx.run(["pytest", "-v"], pty=True) + print(pytest_results) + + +@duty +def coverage(ctx): + """ + Generate a coverage report and save to XML and HTML. + + Args: + ctx: The context instance (passed automatically). + + Example: + `duty coverage` + """ + ctx.run(["coverage", "run", "--source", PACKAGE_NAME, "-m", "pytest"]) + res = ctx.run(["coverage", "report"], pty=True) + print(res) + ctx.run(["coverage", "html"]) + ctx.run(["coverage", "xml"]) + + +@duty +def version(ctx, bump: str = "patch"): + """ + Bump the version using Poetry and update _version.py. + + Args: + ctx: The context instance (passed automatically). + bump (str, optional) = poetry version flag. Available options are: + patch, minor, major, prepatch, preminor, premajor, prerelease. + Defaults to patch. + + Example: + `duty version bump=major` + """ + + # bump with poetry + result = ctx.run(["poetry", "version", bump]) + new_version = re.search(r"(?:.*)(?:\s)(\d+\.\d+\.\d+)$", result) + print(new_version.group(0)) + + # update _version.py + version_file = pathlib.Path(PACKAGE_NAME) / "_version.py" + with version_file.open("w", encoding="utf-8") as version_file: + version_file.write( + f'"""Module containing the version of {PACKAGE_NAME}."""\n\n' + f'__version__ = "{new_version.group(1)}"\n' + ) + print(f"Bumped _version.py to {new_version.group(1)}") + + +@duty +def build(ctx): + """ + Build with poetry and extract the setup.py and copy to project root. + + Args: + ctx: The context instance (passed automatically). + + Example: + `duty build` + """ + + repo_root = pathlib.Path(".") + + # build with poetry + result = ctx.run(["poetry", "build"]) + print(result) + + # extract the setup.py from the tar + extracted_tar = re.search(r"(?:.*)(?:Built\s)(.*)", result) + tar_file = pathlib.Path(f"./dist/{extracted_tar.group(1)}") + shutil.unpack_archive(tar_file, tar_file.parents[0]) + + # copy setup.py to repo root + extracted_path = tar_file.parents[0] / os.path.splitext(tar_file.stem)[0] + setup_py = extracted_path / "setup.py" + shutil.copyfile(setup_py, (repo_root / "setup.py")) + + # cleanup + shutil.rmtree(extracted_path) + + +@duty +def export(ctx): + """ + Export the dependencies to a requirements.txt file. + + Args: + ctx: The context instance (passed automatically). + + Example: + `duty export` + """ + requirements_content = ctx.run( + [ + "poetry", + "export", + "-f", + "requirements.txt", + "--without-hashes", + ] + ) + requirements_dev_content = ctx.run( + [ + "poetry", + "export", + "-f", + "requirements.txt", + "--without-hashes", + "--dev", + ] + ) + + requirements = pathlib.Path(".") / "requirements.txt" + requirements_dev = pathlib.Path(".") / "requirements_dev.txt" + + with requirements.open("w", encoding="utf-8") as req: + req.write(requirements_content) + + with requirements_dev.open("w", encoding="utf-8") as req: + req.write(requirements_dev_content) + + +@duty +def publish(ctx, password: str): + """ + Publish the package to pypi.org. + + Args: + ctx: The context instance (passed automatically). + password (str): pypi.org password. + + Example: + `duty publish password=$my_password` + """ + dist_dir = pathlib.Path(".") / "dist" + rm_result = rm_tree(dist_dir) + print(rm_result) + + publish_result = ctx.run(["poetry", "publish", "-u", "dtomlinson", "-p", password, "--build"]) + print(publish_result) + + +@duty(silent=True) +def clean(ctx): + """ + Delete temporary files. + + Args: + ctx: The context instance (passed automatically). + """ + ctx.run("rm -rf .mypy_cache") + ctx.run("rm -rf .pytest_cache") + ctx.run("rm -rf tests/.pytest_cache") + ctx.run("rm -rf build") + ctx.run("rm -rf dist") + ctx.run("rm -rf pip-wheel-metadata") + ctx.run("rm -rf site") + ctx.run("rm -rf coverage.xml") + ctx.run("rm -rf pytest.xml") + ctx.run("rm -rf htmlcov") + ctx.run("find . -iname '.coverage*' -not -name .coveragerc | xargs rm -rf") + ctx.run("find . -type d -name __pycache__ | xargs rm -rf") + ctx.run("find . -name '*.rej' -delete") + + +@duty +def format(ctx): + """ + Format code using Black and isort. + + Args: + ctx: The context instance (passed automatically). + """ + res = ctx.run(["black", "--line-length=99", PACKAGE_NAME], pty=True, title="Running Black") + print(res) + + res = ctx.run(["isort", PACKAGE_NAME]) + print(res) + + +@duty(pre=["check_code_quality", "check_types", "check_docs", "check_dependencies"]) +def check(ctx): + """ + Check the code quality, check types, check documentation builds and check dependencies for vulnerabilities. + + Args: + ctx: The context instance (passed automatically). + """ + + +@duty +def check_code_quality(ctx): + """ + Check the code quality using prospector. + + Args: + ctx: The context instance (passed automatically). + """ + ctx.run(["prospector", PACKAGE_NAME], pty=True, title="Checking code quality with prospector") + + +@duty +def check_types(ctx): + """ + Check the types using mypy. + + Args: + ctx: The context instance (passed automatically). + """ + ctx.run(["mypy", PACKAGE_NAME], pty=True, title="Checking types with MyPy") + + +@duty +def check_docs(ctx): + """ + Check the documentation builds successfully. + + Args: + ctx: The context instance (passed automatically). + """ + ctx.run(["mkdocs", "build"], title="Building documentation") + + +@duty +def check_dependencies(ctx): + """ + Check dependencies with safety for vulnerabilities. + + Args: + ctx: The context instance (passed automatically). + """ + for module in sys.modules: + if module.startswith("safety.") or module == "safety": + del sys.modules[module] + + importlib.invalidate_caches() + + from safety import safety + from safety.formatter import report + from safety.util import read_requirements + + requirements = ctx.run( + "poetry export --dev --without-hashes", + title="Exporting dependencies as requirements", + allow_overrides=False, + ) + + def check_vulns(): + packages = list(read_requirements(StringIO(requirements))) + vulns = safety.check(packages=packages, ignore_ids="41002", key="", db_mirror="", cached=False, proxy={}) + output_report = report(vulns=vulns, full=True, checked_packages=len(packages)) + print(vulns) + if vulns: + print(output_report) + + ctx.run( + check_vulns, + stdin=requirements, + title="Checking dependencies", + pty=True, + ) + + +def _latest(lines: List[str], regex: Pattern) -> Optional[str]: + for line in lines: + match = regex.search(line) + if match: + return match.groupdict()["version"] + return None + + +def _unreleased(versions, last_release): + for index, version in enumerate(versions): + if version.tag == last_release: + return versions[:index] + return versions + + +def update_changelog( + inplace_file: str, + marker: str, + version_regex: str, + commit_style: str, +) -> None: + """ + Update the given changelog file in place. + Arguments: + inplace_file: The file to update in-place. + marker: The line after which to insert new contents. + version_regex: A regular expression to find currently documented versions in the file. + template_url: The URL to the Jinja template used to render contents. + commit_style: The style of commit messages to parse. + """ + from git_changelog.build import Changelog + from jinja2.sandbox import SandboxedEnvironment + + env = SandboxedEnvironment(autoescape=False) + template = env.from_string(changelog_template()) + changelog = Changelog(".", style=commit_style) + + if len(changelog.versions_list) == 1: + last_version = changelog.versions_list[0] + if last_version.planned_tag is None: + planned_tag = "0.1.0" + last_version.tag = planned_tag + last_version.url += planned_tag + last_version.compare_url = last_version.compare_url.replace("HEAD", planned_tag) + + with open(inplace_file, "r") as changelog_file: + lines = changelog_file.read().splitlines() + + last_released = _latest(lines, re.compile(version_regex)) + if last_released: + changelog.versions_list = _unreleased(changelog.versions_list, last_released) + rendered = template.render(changelog=changelog, inplace=True) + lines[lines.index(marker)] = rendered + + with open(inplace_file, "w") as changelog_file: # noqa: WPS440 + changelog_file.write("\n".join(lines).rstrip("\n") + "\n") + + +@duty +def changelog(ctx): + """ + Update the changelog in-place with latest commits. + Arguments: + ctx: The context instance (passed automatically). + """ + ctx.run( + update_changelog, + kwargs={ + "inplace_file": "CHANGELOG.md", + "marker": "", + "version_regex": r"^## \[v?(?P[^\]]+)", + "commit_style": "angular", + }, + title="Updating changelog", + pty=True, + ) + + +def rm_tree(directory: pathlib.Path): + """ + Recursively delete a directory and all its contents. + + Args: + directory (pathlib.Path): The directory to delete. + """ + for child in directory.glob("*"): + if child.is_file(): + child.unlink() + else: + rm_tree(child) + directory.rmdir() + + +def changelog_template() -> str: + return """ +{% if not inplace -%} +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +{% endif %} +{% macro render_commit(commit) -%} +- {{ commit.style.subject|default(commit.subject) }} ([{{ commit.hash|truncate(7, True, '') }}]({{ commit.url }}) by {{ commit.author_name }}). +{%- if commit.text_refs.issues_not_in_subject %} References: {% for issue in commit.text_refs.issues_not_in_subject -%} +{% if issue.url %}[{{ issue.ref }}]({{ issue.url }}){%else %}{{ issue.ref }}{% endif %}{% if not loop.last %}, {% endif -%} +{%- endfor -%}{%- endif -%} +{%- endmacro -%} + +{%- macro render_section(section) -%} +### {{ section.type or "Misc" }} +{% for commit in section.commits|sort(attribute='author_date',reverse=true)|unique(attribute='subject') -%} +{{ render_commit(commit) }} +{% endfor %} +{%- endmacro -%} + +{%- macro render_version(version) -%} +{%- if version.tag or version.planned_tag -%} +## [{{ version.tag or version.planned_tag }}]({{ version.url }}){% if version.date %} - {{ version.date }}{% endif %} + +[Compare with {{ version.previous_version.tag|default("first commit") }}]({{ version.compare_url }}) +{%- else -%} +## Unrealeased + +[Compare with latest]({{ version.compare_url }}) +{%- endif %} + +{% for type, section in version.sections_dict|dictsort -%} +{%- if type and type in changelog.style.DEFAULT_RENDER -%} +{{ render_section(section) }} +{% endif -%} +{%- endfor -%} +{%- endmacro -%} + +{% for version in changelog.versions_list -%} +{{ render_version(version) }} +{%- endfor -%} + """ diff --git a/mkdocs.yaml b/mkdocs.yaml new file mode 100644 index 0000000..3b5b93e --- /dev/null +++ b/mkdocs.yaml @@ -0,0 +1,107 @@ +# Project Information + +# Repository +site_name: Tembo +site_url: https://tembo-pages.github.io/tembo-core/ +site_description: "Tembo: A simple folder organiser for your work notes." +site_author: Daniel Tomlinson +repo_url: https://github.com/tembo-pages/tembo-core +repo_name: tembo-pages/tembo-core + +# Preview Controls +# set use_directory_urls false if browsing locally +# use_directory_urls: false + +# Page Tree +nav: + - Home: + - Overview: index.md + - Changelog: home/changelog.md + - License: home/license.md + - Code Reference: code_reference/ + - CLI Reference: + - tembo: cli_reference/main.md + - Development: + - Coverage report: coverage.md + +# Theme +theme: + name: material + logo: assets/tembo_doc_logo.png + icon: + repo: fontawesome/brands/github + features: + - navigation.tabs + - navigation.top + - navigation.instant + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: deep orange + accent: orange + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: deep orange + accent: orange + toggle: + icon: material/weather-night + name: Switch to light mode + +# Extensions - see https://squidfunk.github.io/mkdocs-material/setup/extensions/?h= for all options +markdown_extensions: + - admonition + - codehilite: + guess_lang: true + - toc: + # sets the character used to bookmark the title + permalink: "¤" + - pymdownx.highlight: + # show title, linenums + # auto_title: true + # linenums: true + # linenums_style: pymdownx-inline + - pymdownx.details + - pymdownx.inlinehilite + - pymdownx.superfences + - pymdownx.snippets + - pymdownx.arithmatex: + generic: true + - mkdocs-click + +# Plugins +plugins: + - search: + lang: en + - mkdocstrings: + handlers: + python: + import: + - https://docs.python.org/3/objects.inv + - gen-files: + scripts: + - docs/gen_reference.py + - literate-nav: + nav_file: SUMMARY.md + - coverage + +# Customisation +extra_javascript: + - https://polyfill.io/v3/polyfill.min.js?features=es6 + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js + +# CSS +extra_css: + - css/mkdocstrings.css + # - css/extra.css + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/tembo-pages/tembo-core + - icon: fontawesome/solid/paper-plane + link: mailto:dtomlinson@panaetius.co.uk + - icon: fontawesome/brands/twitter + link: https://twitter.com/dmot7291 diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..3d1cd43 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = true diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..1853f0b --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1763 @@ +[[package]] +name = "altgraph" +version = "0.17.2" +description = "Python graph (network) package" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "ansimarkup" +version = "1.5.0" +description = "Produce colored terminal text with an xml-like markup" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +colorama = "*" + +[package.extras] +devel = ["bumpversion (>=0.5.2)", "check-manifest (>=0.35)", "readme-renderer (>=16.0)", "flake8", "pep8-naming"] +tests = ["tox (>=2.6.0)", "pytest (>=3.0.3)", "pytest-cov (>=2.3.1)"] + +[[package]] +name = "astroid" +version = "2.8.5" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = ">=1.11,<1.14" + +[[package]] +name = "astunparse" +version = "1.6.3" +description = "An AST unparser for Python" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.6.1,<2.0" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +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"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "bandit" +version = "1.7.1" +description = "Security oriented static analyser for python code." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} +GitPython = ">=1.0.1" +PyYAML = ">=5.3.1" +stevedore = ">=1.20.0" + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.0.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.1.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "dodgy" +version = "0.2.1" +description = "Dodgy: Searches for dodgy looking lines in Python code" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "dparse" +version = "0.5.1" +description = "A parser for Python dependency files" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +packaging = "*" +pyyaml = "*" +toml = "*" + +[package.extras] +pipenv = ["pipenv"] + +[[package]] +name = "duty" +version = "0.7.0" +description = "A simple task runner." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +failprint = ">=0.8,<1.0" + +[[package]] +name = "failprint" +version = "0.8.0" +description = "Run a command, print its output only if it fails." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +ansimarkup = ">=1.4,<2.0" +jinja2 = ">=2.11,<4" +ptyprocess = {version = ">=0.6,<1.0", markers = "sys_platform != \"win32\""} + +[[package]] +name = "flake8" +version = "2.3.0" +description = "the modular source code checker: pep8, pyflakes and co" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +mccabe = ">=0.2.1" +pep8 = ">=1.5.7" +pyflakes = ">=0.8.1" + +[[package]] +name = "flake8-polyfill" +version = "1.0.2" +description = "Polyfill package for Flake8 plugins" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[[package]] +name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "ghp-import" +version = "2.0.2" +description = "Copy your docs directly to the gh-pages branch." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["twine", "markdown", "flake8", "wheel"] + +[[package]] +name = "git-changelog" +version = "0.5.0" +description = "Automatic Changelog generator using Jinja2 templates." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +Jinja2 = ">=2.10,<4" +semver = ">=2.13,<3.0" + +[[package]] +name = "gitdb" +version = "4.0.9" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.24" +description = "GitPython is a python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +gitdb = ">=4.0.1,<5" +typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.8.2" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=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)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "jinja2" +version = "3.0.3" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.6.0" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[[package]] +name = "macholib" +version = "1.15.2" +description = "Mach-O header analysis and editing" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +altgraph = ">=0.15" + +[[package]] +name = "markdown" +version = "3.3.6" +description = "Python implementation of Markdown." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mkdocs" +version = "1.2.3" +description = "Project documentation with Markdown." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +click = ">=3.3" +ghp-import = ">=1.0" +importlib-metadata = ">=3.10" +Jinja2 = ">=2.10.1" +Markdown = ">=3.2.1" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +PyYAML = ">=3.10" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "0.3.0" +description = "Automatically link across pages in MkDocs." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +Markdown = ">=3.3,<4.0" +mkdocs = ">=1.1,<2.0" + +[[package]] +name = "mkdocs-click" +version = "0.4.0" +description = "An MkDocs extension to generate documentation for Click command line applications" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=7,<9" +markdown = ">=3.0.0,<4.0.0" + +[[package]] +name = "mkdocs-coverage" +version = "0.2.4" +description = "MkDocs plugin to integrate your coverage HTML report into your site." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +mkdocs = ">=1.1,<2.0" + +[[package]] +name = "mkdocs-gen-files" +version = "0.3.3" +description = "MkDocs plugin to programmatically generate documentation pages during the build" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +mkdocs = ">=1.0.3,<2.0.0" + +[[package]] +name = "mkdocs-literate-nav" +version = "0.4.0" +description = "MkDocs plugin to specify the navigation in Markdown instead of YAML" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +mkdocs = ">=1.0.3,<2.0.0" + +[[package]] +name = "mkdocs-material" +version = "7.3.6" +description = "A Material Design theme for MkDocs" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +jinja2 = ">=2.11.1" +markdown = ">=3.2" +mkdocs = ">=1.2.3" +mkdocs-material-extensions = ">=1.0" +pygments = ">=2.10" +pymdown-extensions = ">=9.0" + +[[package]] +name = "mkdocs-material-extensions" +version = "1.0.3" +description = "Extension pack for Python Markdown." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mkdocstrings" +version = "0.16.2" +description = "Automatic documentation from sources, for MkDocs." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Jinja2 = ">=2.11.1,<4.0" +Markdown = ">=3.3,<4.0" +MarkupSafe = ">=1.1,<3.0" +mkdocs = ">=1.2,<2.0" +mkdocs-autorefs = ">=0.1,<0.4" +pymdown-extensions = ">=6.3,<10.0" +pytkdocs = ">=0.2.0,<0.13.0" + +[[package]] +name = "mypy" +version = "0.910" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +toml = "*" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<1.5.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "panaetius" +version = "2.3.2" +description = "Python module to gracefully handle a .config file/environment variables for scripts, with built in masking for sensitive options. Provides a Splunk friendly formatted logger instance." +category = "main" +optional = false +python-versions = "^3.7" +develop = true + +[package.dependencies] +PyYAML = "^6.0" +toml = "^0.10.0" + +[package.source] +type = "directory" +url = "../../panaetius" + +[[package]] +name = "pbr" +version = "5.8.0" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "pefile" +version = "2021.9.3" +description = "Python PE parsing module" +category = "dev" +optional = false +python-versions = ">=3.6.0" + +[package.dependencies] +future = "*" + +[[package]] +name = "pendulum" +version = "2.1.2" +description = "Python datetimes made easy" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +python-dateutil = ">=2.6,<3.0" +pytzdata = ">=2020.1" + +[[package]] +name = "pep8" +version = "1.7.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pep8-naming" +version = "0.10.0" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8-polyfill = ">=1.0.2,<2" + +[[package]] +name = "platformdirs" +version = "2.4.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prospector" +version = "1.5.1" +description = "" +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.dependencies] +bandit = {version = ">=1.5.1", optional = true, markers = "extra == \"with_bandit\" or extra == \"with_everything\""} +dodgy = ">=0.2.1,<0.3.0" +mccabe = ">=0.6.0,<0.7.0" +mypy = {version = ">=0.600", optional = true, markers = "extra == \"with_mypy\" or extra == \"with_everything\""} +pep8-naming = ">=0.3.3,<=0.10.0" +pycodestyle = ">=2.6.0,<2.9.0" +pydocstyle = ">=2.0.0" +pyflakes = ">=2.2.0,<2.4.0" +pylint = ">=2.8.3,<3" +pylint-celery = "0.3" +pylint-django = ">=2.4.4,<3.0.0" +pylint-flask = "0.6" +pylint-plugin-utils = ">=0.6,<0.7" +PyYAML = "*" +requirements-detector = ">=0.7,<0.8" +setoptconf-tmp = ">=0.3.1,<0.4.0" +toml = ">=0.10.2,<0.11.0" + +[package.extras] +with_bandit = ["bandit (>=1.5.1)"] +with_everything = ["bandit (>=1.5.1)", "frosted (>=1.4.1)", "mypy (>=0.600)", "pyroma (>=2.4)", "vulture (>=1.5)"] +with_frosted = ["frosted (>=1.4.1)"] +with_mypy = ["mypy (>=0.600)"] +with_pyroma = ["pyroma (>=2.4)"] +with_vulture = ["vulture (>=1.5)"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycodestyle" +version = "2.8.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pydocstyle" +version = "6.1.1" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +snowballstemmer = "*" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.10.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "pyinstaller" +version = "4.5.1" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2020.6" +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} + +[package.extras] +encryption = ["tinyaes (>=1.0.0)"] +hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2021.3" +description = "Community maintained hooks for PyInstaller" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pylint" +version = "2.11.1" +description = "python code static checker" +category = "dev" +optional = false +python-versions = "~=3.6" + +[package.dependencies] +astroid = ">=2.8.0,<2.9" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.7" +platformdirs = ">=2.2.0" +toml = ">=0.7.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[[package]] +name = "pylint-celery" +version = "0.3" +description = "pylint-celery is a Pylint plugin to aid Pylint in recognising and understandingerrors caused when using the Celery library" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +astroid = ">=1.0" +pylint = ">=1.0" +pylint-plugin-utils = ">=0.2.1" + +[[package]] +name = "pylint-django" +version = "2.4.4" +description = "A Pylint plugin to help Pylint understand the Django web framework" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pylint = ">=2.0" +pylint-plugin-utils = ">=0.5" + +[package.extras] +for_tests = ["django-tables2", "factory-boy", "coverage", "pytest"] +with_django = ["django"] + +[[package]] +name = "pylint-flask" +version = "0.6" +description = "pylint-flask is a Pylint plugin to aid Pylint in recognizing and understanding errors caused when using Flask" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pylint-plugin-utils = ">=0.2.1" + +[[package]] +name = "pylint-plugin-utils" +version = "0.6" +description = "Utilities and helpers for writing Pylint plugins" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pylint = ">=1.7" + +[[package]] +name = "pymdown-extensions" +version = "9.1" +description = "Extension pack for Python Markdown." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Markdown = ">=3.2" + +[[package]] +name = "pyparsing" +version = "3.0.6" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-custom-exit-code" +version = "0.3.0" +description = "Exit pytest test session with custom exit code in different scenarios" +category = "dev" +optional = false +python-versions = ">2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytest = ">=4.0.2" + +[[package]] +name = "pytest-datadir" +version = "1.3.1" +description = "pytest plugin for test data directories and files" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pytest = ">=2.7.0" + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytkdocs" +version = "0.12.0" +description = "Load Python objects documentation." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +astunparse = {version = ">=1.6,<2.0", markers = "python_version < \"3.9\""} + +[package.extras] +numpy-style = ["docstring_parser (>=0.7,<1.0)"] + +[[package]] +name = "pytzdata" +version = "2020.1" +description = "The Olson timezone database for Python." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pywin32-ctypes" +version = "0.2.0" +description = "" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "requirements-detector" +version = "0.7" +description = "Python tool to find and list requirements of a Python project" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +astroid = ">=1.4" + +[[package]] +name = "safety" +version = "1.10.3" +description = "Checks installed dependencies for known vulnerabilities." +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +Click = ">=6.0" +dparse = ">=0.5.1" +packaging = "*" +requests = "*" + +[[package]] +name = "semver" +version = "2.13.0" +description = "Python helper for Semantic Versioning (http://semver.org/)" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "setoptconf-tmp" +version = "0.3.1" +description = "A module for retrieving program settings from various sources in a consistant method." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +yaml = ["pyyaml"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "stevedore" +version = "3.5.0" +description = "Manage dynamic plugins for Python applications" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pbr = ">=2.0.0,<2.1.0 || >2.1.0" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "1.2.2" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.0.0" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "watchdog" +version = "2.1.6" +description = "Filesystem events monitoring" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wrapt" +version = "1.13.3" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +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"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "0126a464628e466ebffe16877a2a727a10a2a298c90f504274f264b5ab8b4fcd" + +[metadata.files] +altgraph = [ + {file = "altgraph-0.17.2-py2.py3-none-any.whl", hash = "sha256:743628f2ac6a7c26f5d9223c91ed8ecbba535f506f4b6f558885a8a56a105857"}, + {file = "altgraph-0.17.2.tar.gz", hash = "sha256:ebf2269361b47d97b3b88e696439f6e4cbc607c17c51feb1754f90fb79839158"}, +] +ansimarkup = [ + {file = "ansimarkup-1.5.0-py2.py3-none-any.whl", hash = "sha256:3146ca74af5f69e48a9c3d41b31085c0d6378f803edeb364856d37c11a684acf"}, + {file = "ansimarkup-1.5.0.tar.gz", hash = "sha256:96c65d75bbed07d3dcbda8dbede8c2252c984f90d0ca07434b88a6bbf345fad3"}, +] +astroid = [ + {file = "astroid-2.8.5-py3-none-any.whl", hash = "sha256:abc423a1e85bc1553954a14f2053473d2b7f8baf32eae62a328be24f436b5107"}, + {file = "astroid-2.8.5.tar.gz", hash = "sha256:11f7356737b624c42e21e71fe85eea6875cb94c03c82ac76bd535a0ff10b0f25"}, +] +astunparse = [ + {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, + {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +bandit = [ + {file = "bandit-1.7.1-py3-none-any.whl", hash = "sha256:f5acd838e59c038a159b5c621cf0f8270b279e884eadd7b782d7491c02add0d4"}, + {file = "bandit-1.7.1.tar.gz", hash = "sha256:a81b00b5436e6880fa8ad6799bc830e02032047713cbb143a12939ac67eb756c"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, + {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, +] +click = [ + {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, + {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-6.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:675adb3b3380967806b3cbb9c5b00ceb29b1c472692100a338730c1d3e59c8b9"}, + {file = "coverage-6.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95a58336aa111af54baa451c33266a8774780242cab3704b7698d5e514840758"}, + {file = "coverage-6.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0a595a781f8e186580ff8e3352dd4953b1944289bec7705377c80c7e36c4d6c"}, + {file = "coverage-6.1.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d3c5f49ce6af61154060640ad3b3281dbc46e2e0ef2fe78414d7f8a324f0b649"}, + {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:310c40bed6b626fd1f463e5a83dba19a61c4eb74e1ac0d07d454ebbdf9047e9d"}, + {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a4d48e42e17d3de212f9af44f81ab73b9378a4b2b8413fd708d0d9023f2bbde4"}, + {file = "coverage-6.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ffa545230ca2ad921ad066bf8fd627e7be43716b6e0fcf8e32af1b8188ccb0ab"}, + {file = "coverage-6.1.2-cp310-cp310-win32.whl", hash = "sha256:cd2d11a59afa5001ff28073ceca24ae4c506da4355aba30d1e7dd2bd0d2206dc"}, + {file = "coverage-6.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:96129e41405887a53a9cc564f960d7f853cc63d178f3a182fdd302e4cab2745b"}, + {file = "coverage-6.1.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1de9c6f5039ee2b1860b7bad2c7bc3651fbeb9368e4c4d93e98a76358cdcb052"}, + {file = "coverage-6.1.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:80cb70264e9a1d04b519cdba3cd0dc42847bf8e982a4d55c769b9b0ee7cdce1e"}, + {file = "coverage-6.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba6125d4e55c0b8e913dad27b22722eac7abdcb1f3eab1bd090eee9105660266"}, + {file = "coverage-6.1.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8492d37acdc07a6eac6489f6c1954026f2260a85a4c2bb1e343fe3d35f5ee21a"}, + {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66af99c7f7b64d050d37e795baadf515b4561124f25aae6e1baa482438ecc388"}, + {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ebcc03e1acef4ff44f37f3c61df478d6e469a573aa688e5a162f85d7e4c3860d"}, + {file = "coverage-6.1.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d44a8136eebbf544ad91fef5bd2b20ef0c9b459c65a833c923d9aa4546b204"}, + {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c18725f3cffe96732ef96f3de1939d81215fd6d7d64900dcc4acfe514ea4fcbf"}, + {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c8e9c4bcaaaa932be581b3d8b88b677489975f845f7714efc8cce77568b6711c"}, + {file = "coverage-6.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:06d009e8a29483cbc0520665bc46035ffe9ae0e7484a49f9782c2a716e37d0a0"}, + {file = "coverage-6.1.2-cp36-cp36m-win32.whl", hash = "sha256:e5432d9c329b11c27be45ee5f62cf20a33065d482c8dec1941d6670622a6fb8f"}, + {file = "coverage-6.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:82fdcb64bf08aa5db881db061d96db102c77397a570fbc112e21c48a4d9cb31b"}, + {file = "coverage-6.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:94f558f8555e79c48c422045f252ef41eb43becdd945e9c775b45ebfc0cbd78f"}, + {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046647b96969fda1ae0605f61288635209dd69dcd27ba3ec0bf5148bc157f954"}, + {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cc799916b618ec9fd00135e576424165691fec4f70d7dc12cfaef09268a2478c"}, + {file = "coverage-6.1.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62646d98cf0381ffda301a816d6ac6c35fc97aa81b09c4c52d66a15c4bef9d7c"}, + {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:27a3df08a855522dfef8b8635f58bab81341b2fb5f447819bc252da3aa4cf44c"}, + {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:610c0ba11da8de3a753dc4b1f71894f9f9debfdde6559599f303286e70aeb0c2"}, + {file = "coverage-6.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:35b246ae3a2c042dc8f410c94bcb9754b18179cdb81ff9477a9089dbc9ecc186"}, + {file = "coverage-6.1.2-cp37-cp37m-win32.whl", hash = "sha256:0cde7d9fe2fb55ff68ebe7fb319ef188e9b88e0a3d1c9c5db7dd829cd93d2193"}, + {file = "coverage-6.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:958ac66272ff20e63d818627216e3d7412fdf68a2d25787b89a5c6f1eb7fdd93"}, + {file = "coverage-6.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a300b39c3d5905686c75a369d2a66e68fd01472ea42e16b38c948bd02b29e5bd"}, + {file = "coverage-6.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3855d5d26292539861f5ced2ed042fc2aa33a12f80e487053aed3bcb6ced13"}, + {file = "coverage-6.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:586d38dfc7da4a87f5816b203ff06dd7c1bb5b16211ccaa0e9788a8da2b93696"}, + {file = "coverage-6.1.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a34fccb45f7b2d890183a263578d60a392a1a218fdc12f5bce1477a6a68d4373"}, + {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bc1ee1318f703bc6c971da700d74466e9b86e0c443eb85983fb2a1bd20447263"}, + {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3f546f48d5d80a90a266769aa613bc0719cb3e9c2ef3529d53f463996dd15a9d"}, + {file = "coverage-6.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd92ece726055e80d4e3f01fff3b91f54b18c9c357c48fcf6119e87e2461a091"}, + {file = "coverage-6.1.2-cp38-cp38-win32.whl", hash = "sha256:24ed38ec86754c4d5a706fbd5b52b057c3df87901a8610d7e5642a08ec07087e"}, + {file = "coverage-6.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:97ef6e9119bd39d60ef7b9cd5deea2b34869c9f0b9777450a7e3759c1ab09b9b"}, + {file = "coverage-6.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e5a8c947a2a89c56655ecbb789458a3a8e3b0cbf4c04250331df8f647b3de59"}, + {file = "coverage-6.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a39590d1e6acf6a3c435c5d233f72f5d43b585f5be834cff1f21fec4afda225"}, + {file = "coverage-6.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d2c2e3ce7b8cc932a2f918186964bd44de8c84e2f9ef72dc616f5bb8be22e71"}, + {file = "coverage-6.1.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3348865798c077c695cae00da0924136bb5cc501f236cfd6b6d9f7a3c94e0ec4"}, + {file = "coverage-6.1.2-cp39-cp39-win32.whl", hash = "sha256:fae3fe111670e51f1ebbc475823899524e3459ea2db2cb88279bbfb2a0b8a3de"}, + {file = "coverage-6.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:af45eea024c0e3a25462fade161afab4f0d9d9e0d5a5d53e86149f74f0a35ecc"}, + {file = "coverage-6.1.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:eab14fdd410500dae50fd14ccc332e65543e7b39f6fc076fe90603a0e5d2f929"}, + {file = "coverage-6.1.2.tar.gz", hash = "sha256:d9a635114b88c0ab462e0355472d00a180a5fbfd8511e7f18e4ac32652e7d972"}, +] +dodgy = [ + {file = "dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"}, + {file = "dodgy-0.2.1.tar.gz", hash = "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a"}, +] +dparse = [ + {file = "dparse-0.5.1-py3-none-any.whl", hash = "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"}, + {file = "dparse-0.5.1.tar.gz", hash = "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367"}, +] +duty = [ + {file = "duty-0.7.0-py3-none-any.whl", hash = "sha256:45068baf1639f16464aa40e9d8f698f0ae09408368fe53a34e9bfe6993dfd743"}, + {file = "duty-0.7.0.tar.gz", hash = "sha256:5ebfd4640ab41e3058f1d8433f74228d60c9a808def1784e65319ef1899a9d15"}, +] +failprint = [ + {file = "failprint-0.8.0-py3-none-any.whl", hash = "sha256:a8215a7aca5ce687116b995cd3a9667180f222ab88c4328a5007d2fa0b5c0f78"}, + {file = "failprint-0.8.0.tar.gz", hash = "sha256:4633b52f9395bf042ad996c96cd7819a94b2021833030dd1eb692ebbd86b89a1"}, +] +flake8 = [ + {file = "flake8-2.3.0-py2.py3-none-any.whl", hash = "sha256:c99cc9716d6655d9c8bcb1e77632b8615bf0abd282d7abd9f5c2148cad7fc669"}, + {file = "flake8-2.3.0.tar.gz", hash = "sha256:5ee1a43ccd0716d6061521eec6937c983efa027793013e572712c4da55c7c83e"}, +] +flake8-polyfill = [ + {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, + {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, +] +future = [ + {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, +] +ghp-import = [ + {file = "ghp-import-2.0.2.tar.gz", hash = "sha256:947b3771f11be850c852c64b561c600fdddf794bab363060854c1ee7ad05e071"}, + {file = "ghp_import-2.0.2-py3-none-any.whl", hash = "sha256:5f8962b30b20652cdffa9c5a9812f7de6bcb56ec475acac579807719bf242c46"}, +] +git-changelog = [ + {file = "git-changelog-0.5.0.tar.gz", hash = "sha256:6a1b43d21edb2b42e6c21250de97761602ff01deb8f139c84ba1b11cc591bd92"}, + {file = "git_changelog-0.5.0-py3-none-any.whl", hash = "sha256:ca7fd3371dd6918c6c19202bbccadc59030da48a5fe350601f15fc5c36c1431a"}, +] +gitdb = [ + {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, + {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, +] +gitpython = [ + {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"}, + {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.8.2-py3-none-any.whl", hash = "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100"}, + {file = "importlib_metadata-4.8.2.tar.gz", hash = "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +jinja2 = [ + {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, + {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"}, + {file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"}, + {file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"}, + {file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"}, + {file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, + {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, +] +macholib = [ + {file = "macholib-1.15.2-py2.py3-none-any.whl", hash = "sha256:885613dd02d3e26dbd2b541eb4cc4ce611b841f827c0958ab98656e478b9e6f6"}, + {file = "macholib-1.15.2.tar.gz", hash = "sha256:1542c41da3600509f91c165cb897e7e54c0e74008bd8da5da7ebbee519d593d2"}, +] +markdown = [ + {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, + {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, +] +markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, + {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mergedeep = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] +mkdocs = [ + {file = "mkdocs-1.2.3-py3-none-any.whl", hash = "sha256:a1fa8c2d0c1305d7fc2b9d9f607c71778572a8b110fb26642aa00296c9e6d072"}, + {file = "mkdocs-1.2.3.tar.gz", hash = "sha256:89f5a094764381cda656af4298727c9f53dc3e602983087e1fe96ea1df24f4c1"}, +] +mkdocs-autorefs = [ + {file = "mkdocs-autorefs-0.3.0.tar.gz", hash = "sha256:2f89556eb2107d72e3aff41b04dcaaf1125d407a33b8027fbc982137d248d37d"}, + {file = "mkdocs_autorefs-0.3.0-py3-none-any.whl", hash = "sha256:261875003e49b5d708993fd2792a69d624cbc8cf7de49e96c81d3d9825977ca4"}, +] +mkdocs-click = [ + {file = "mkdocs_click-0.4.0-py3-none-any.whl", hash = "sha256:3b54c65bd1e6e2b600da71d77705e911d15d86bc2c2b341ff5d7f76b9fe1505b"}, + {file = "mkdocs_click-0.4.0.tar.gz", hash = "sha256:b34be84cde57850733fb1b32db37b472620ac2c3e97db4abbe11dbd6b98124f2"}, +] +mkdocs-coverage = [ + {file = "mkdocs-coverage-0.2.4.tar.gz", hash = "sha256:d0535b9ecf0a436fcbda3c8d7d58209d05441d79fa9388e12de99e50631f9b0a"}, + {file = "mkdocs_coverage-0.2.4-py3-none-any.whl", hash = "sha256:7caf6953c8aa10d4386e8536e2e0d3e2f0b4f8a3066f94417bd216ef6e4baa37"}, +] +mkdocs-gen-files = [ + {file = "mkdocs-gen-files-0.3.3.tar.gz", hash = "sha256:0bfe82ecb62b3d2064349808c898063ad955a77804436e0a54fa029414c893bb"}, + {file = "mkdocs_gen_files-0.3.3-py3-none-any.whl", hash = "sha256:bcfbaa496c5fc8164a9a243963e444c8750c619e18cd54e217549586c9132461"}, +] +mkdocs-literate-nav = [ + {file = "mkdocs-literate-nav-0.4.0.tar.gz", hash = "sha256:29bf383170b80200d16f0d8528f4925ae96982677c8a98f84af70343a3b38bcf"}, + {file = "mkdocs_literate_nav-0.4.0-py3-none-any.whl", hash = "sha256:2012ac97bc2316890ac35deabd653fd3c6a7434923d572de965c5b6c80f09537"}, +] +mkdocs-material = [ + {file = "mkdocs-material-7.3.6.tar.gz", hash = "sha256:1b1dbd8ef2508b358d93af55a5c5db3f141c95667fad802301ec621c40c7c217"}, + {file = "mkdocs_material-7.3.6-py2.py3-none-any.whl", hash = "sha256:1b6b3e9e09f922c2d7f1160fe15c8f43d4adc0d6fb81aa6ff0cbc7ef5b78ec75"}, +] +mkdocs-material-extensions = [ + {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, + {file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"}, +] +mkdocstrings = [ + {file = "mkdocstrings-0.16.2-py3-none-any.whl", hash = "sha256:671fba8a6c7a8455562aae0a3fa85979fbcef261daec5b2bac4dd1479acc14df"}, + {file = "mkdocstrings-0.16.2.tar.gz", hash = "sha256:3d8a86c283dfa21818d5b9579aa4e750eea6b5c127b43ad8b00cebbfb7f9634e"}, +] +mypy = [ + {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, + {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, + {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, + {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, + {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, + {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, + {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, + {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, + {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, + {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, + {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, + {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, + {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, + {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, + {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, + {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, + {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, + {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, + {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, + {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, + {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, + {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, + {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +panaetius = [] +pbr = [ + {file = "pbr-5.8.0-py2.py3-none-any.whl", hash = "sha256:176e8560eaf61e127817ef93d8a844803abb27a4d4637f0ff3bb783129be2e0a"}, + {file = "pbr-5.8.0.tar.gz", hash = "sha256:672d8ebee84921862110f23fcec2acea191ef58543d34dfe9ef3d9f13c31cddf"}, +] +pefile = [ + {file = "pefile-2021.9.3.tar.gz", hash = "sha256:344a49e40a94e10849f0fe34dddc80f773a12b40675bf2f7be4b8be578bdd94a"}, +] +pendulum = [ + {file = "pendulum-2.1.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe"}, + {file = "pendulum-2.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739"}, + {file = "pendulum-2.1.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394"}, + {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0"}, + {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3"}, + {file = "pendulum-2.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b"}, + {file = "pendulum-2.1.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360"}, + {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0"}, + {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087"}, + {file = "pendulum-2.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db"}, + {file = "pendulum-2.1.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002"}, + {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5"}, + {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b"}, + {file = "pendulum-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b"}, + {file = "pendulum-2.1.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116"}, + {file = "pendulum-2.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052"}, + {file = "pendulum-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be"}, + {file = "pendulum-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269"}, + {file = "pendulum-2.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a"}, + {file = "pendulum-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7"}, + {file = "pendulum-2.1.2.tar.gz", hash = "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207"}, +] +pep8 = [ + {file = "pep8-1.7.1-py2.py3-none-any.whl", hash = "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee"}, + {file = "pep8-1.7.1.tar.gz", hash = "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374"}, +] +pep8-naming = [ + {file = "pep8-naming-0.10.0.tar.gz", hash = "sha256:f3b4a5f9dd72b991bf7d8e2a341d2e1aa3a884a769b5aaac4f56825c1763bf3a"}, + {file = "pep8_naming-0.10.0-py2.py3-none-any.whl", hash = "sha256:5d9f1056cb9427ce344e98d1a7f5665710e2f20f748438e308995852cfa24164"}, +] +platformdirs = [ + {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, + {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +prospector = [ + {file = "prospector-1.5.1-py3-none-any.whl", hash = "sha256:47f8ff3fd36ae276967eb392ca20b300a7bdea66c0d0252250a4d89a6c03ab15"}, + {file = "prospector-1.5.1.tar.gz", hash = "sha256:851c2892cd615cfee91fd27cfaf7a5061d14daf2853aa8f012e927b98f919578"}, +] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycodestyle = [ + {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, + {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pygments = [ + {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, + {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, +] +pyinstaller = [ + {file = "pyinstaller-4.5.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:ecc2baadeeefd2b6fbf39d13c65d4aa603afdda1c6aaaebc4577ba72893fee9e"}, + {file = "pyinstaller-4.5.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4d848cd782ee0893d7ad9fe2bfe535206a79f0b6760cecc5f2add831258b9322"}, + {file = "pyinstaller-4.5.1-py3-none-manylinux2014_i686.whl", hash = "sha256:8f747b190e6ad30e2d2fd5da9a64636f61aac8c038c0b7f685efa92c782ea14f"}, + {file = "pyinstaller-4.5.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c587da8f521a7ce1b9efb4e3d0117cd63c92dc6cedff24590aeef89372f53012"}, + {file = "pyinstaller-4.5.1-py3-none-win32.whl", hash = "sha256:fed9f5e4802769a416a8f2ca171c6be961d1861cc05a0b71d20dfe05423137e9"}, + {file = "pyinstaller-4.5.1-py3-none-win_amd64.whl", hash = "sha256:aae456205c68355f9597411090576bb31b614e53976b4c102d072bbe5db8392a"}, + {file = "pyinstaller-4.5.1.tar.gz", hash = "sha256:30733baaf8971902286a0ddf77e5499ac5f7bf8e7c39163e83d4f8c696ef265e"}, +] +pyinstaller-hooks-contrib = [ + {file = "pyinstaller-hooks-contrib-2021.3.tar.gz", hash = "sha256:169b09802a19f83593114821d6ba0416a05c7071ef0ca394f7bfb7e2c0c916c8"}, + {file = "pyinstaller_hooks_contrib-2021.3-py2.py3-none-any.whl", hash = "sha256:a52bc3834281266bbf77239cfc9521923336ca622f44f90924546ed6c6d3ad5e"}, +] +pylint = [ + {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, + {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, +] +pylint-celery = [ + {file = "pylint-celery-0.3.tar.gz", hash = "sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb"}, +] +pylint-django = [ + {file = "pylint-django-2.4.4.tar.gz", hash = "sha256:f63f717169b0c2e4e19c28f1c32c28290647330184fcb7427805ae9b6994f3fc"}, + {file = "pylint_django-2.4.4-py3-none-any.whl", hash = "sha256:aff49d9602a39c027b4ed7521a041438893205918f405800063b7ff692b7371b"}, +] +pylint-flask = [ + {file = "pylint-flask-0.6.tar.gz", hash = "sha256:f4d97de2216bf7bfce07c9c08b166e978fe9f2725de2a50a9845a97de7e31517"}, +] +pylint-plugin-utils = [ + {file = "pylint-plugin-utils-0.6.tar.gz", hash = "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a"}, + {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, +] +pymdown-extensions = [ + {file = "pymdown-extensions-9.1.tar.gz", hash = "sha256:74247f2c80f1d9e3c7242abe1c16317da36c6f26c7ad4b8a7f457f0ec20f0365"}, + {file = "pymdown_extensions-9.1-py3-none-any.whl", hash = "sha256:b03e66f91f33af4a6e7a0e20c740313522995f69a03d86316b1449766c473d0e"}, +] +pyparsing = [ + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pytest-custom-exit-code = [ + {file = "pytest-custom_exit_code-0.3.0.tar.gz", hash = "sha256:51ffff0ee2c1ddcc1242e2ddb2a5fd02482717e33a2326ef330e3aa430244635"}, + {file = "pytest_custom_exit_code-0.3.0-py3-none-any.whl", hash = "sha256:6e0ce6e57ce3a583cb7e5023f7d1021e19dfec22be41d9ad345bae2fc61caf3b"}, +] +pytest-datadir = [ + {file = "pytest-datadir-1.3.1.tar.gz", hash = "sha256:d3af1e738df87515ee509d6135780f25a15959766d9c2b2dbe02bf4fb979cb18"}, + {file = "pytest_datadir-1.3.1-py2.py3-none-any.whl", hash = "sha256:1847ed0efe0bc54cac40ab3fba6d651c2f03d18dd01f2a582979604d32e7621e"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pytkdocs = [ + {file = "pytkdocs-0.12.0-py3-none-any.whl", hash = "sha256:12cb4180d5eafc7819dba91142948aa7b85ad0a3ad0e956db1cdc6d6c5d0ef56"}, + {file = "pytkdocs-0.12.0.tar.gz", hash = "sha256:746905493ff79482ebc90816b8c397c096727a1da8214a0ccff662a8412e91b3"}, +] +pytzdata = [ + {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, + {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, +] +pywin32-ctypes = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +pyyaml-env-tag = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] +requirements-detector = [ + {file = "requirements-detector-0.7.tar.gz", hash = "sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"}, +] +safety = [ + {file = "safety-1.10.3-py2.py3-none-any.whl", hash = "sha256:5f802ad5df5614f9622d8d71fedec2757099705c2356f862847c58c6dfe13e84"}, + {file = "safety-1.10.3.tar.gz", hash = "sha256:30e394d02a20ac49b7f65292d19d38fa927a8f9582cdfd3ad1adbbc66c641ad5"}, +] +semver = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, +] +setoptconf-tmp = [ + {file = "setoptconf-tmp-0.3.1.tar.gz", hash = "sha256:e0480addd11347ba52f762f3c4d8afa3e10ad0affbc53e3ffddc0ca5f27d5778"}, + {file = "setoptconf_tmp-0.3.1-py3-none-any.whl", hash = "sha256:76035d5cd1593d38b9056ae12d460eca3aaa34ad05c315b69145e138ba80a745"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +smmap = [ + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] +stevedore = [ + {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"}, + {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, + {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, +] +typing-extensions = [ + {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, + {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"}, +] +urllib3 = [ + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, +] +watchdog = [ + {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3"}, + {file = "watchdog-2.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b"}, + {file = "watchdog-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542"}, + {file = "watchdog-2.1.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669"}, + {file = "watchdog-2.1.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660"}, + {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3"}, + {file = "watchdog-2.1.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04"}, + {file = "watchdog-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b"}, + {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604"}, + {file = "watchdog-2.1.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6"}, + {file = "watchdog-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9"}, + {file = "watchdog-2.1.6-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_armv7l.whl", hash = "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_i686.whl", hash = "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_s390x.whl", hash = "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2"}, + {file = "watchdog-2.1.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15"}, + {file = "watchdog-2.1.6-py3-none-win32.whl", hash = "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d"}, + {file = "watchdog-2.1.6-py3-none-win_amd64.whl", hash = "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5"}, + {file = "watchdog-2.1.6-py3-none-win_ia64.whl", hash = "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923"}, + {file = "watchdog-2.1.6.tar.gz", hash = "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7"}, +] +wrapt = [ + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, +] +zipp = [ + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, +] diff --git a/prospector.yaml b/prospector.yaml new file mode 100644 index 0000000..5a0a6d3 --- /dev/null +++ b/prospector.yaml @@ -0,0 +1,125 @@ +output-format: vscode +doc-warnings: true +strictness: none + +ignore-patterns: + - (^|/)\..+ + +# https://pylint.pycqa.org/en/latest/technical_reference/features.html +pylint: + run: true + disable: + # disables TODO warnings + - fixme +# !doc docstrings + - missing-module-docstring + - missing-class-docstring + - missing-function-docstring +# ! doc end of docstrings + # disables warnings about abstract methods not overridden + - abstract-method + # used when an ancestor class method has an __init__ method which is not called by a derived class. + - super-init-not-called + # either all return statements in a function should return an expression, or none of them should. + # - inconsistent-return-statements + # Used when an expression that is not a function call is assigned to nothing. Probably something else was intended. + # - expression-not-assigned + # Used when a line is longer than a given number of characters. + # - line-too-long + enable: + options: + max-locals: 15 + max-returns: 6 + max-branches: 12 + max-statements: 50 + max-parents: 7 + max-attributes: 20 + min-public-methods: 0 + max-public-methods: 25 + max-module-lines: 1000 + max-line-length: 99 + max-args: 8 + +mccabe: + run: true + options: + max-complexity: 10 + +# https://pep8.readthedocs.io/en/release-1.7.x/intro.html#error-codes +pep8: + run: true + options: + max-line-length: 99 + single-line-if-stmt: n + disable: + # line too long + - E501 + +pyroma: + run: false + disable: + - PYR19 + - PYR16 + +# https://pep257.readthedocs.io/en/latest/error_codes.html +# http://www.pydocstyle.org/en/6.1.1/error_codes.html +pep257: + disable: +# !doc docstrings + # # Missing docstring in public package + # - D104 + # # Missing docstring in __init__ + # - D107 + # # Missing docstring in public module + # - D100 + # # Missing docstring in public class + # - D101 + # # Missing docstring in public method + # - D102 + # # Missing docstring in public function + # - D103 + # # Missing docstring in magic method + # - D105 + # # One-line docstring should fit on one line with quotes + # - D200 + # # No blank lines allowed after function docstring + # - D202 + # # Multi-line docstring summary should start at the second line + # - D213 + # # First word of the docstring should not be This + # - D404 + # DEFAULT IGNORES + # 1 blank line required before class docstring + - D203 + # Multi-line docstring summary should start at the first line + - D212 +# !doc end of docstrings + # Section name should end with a newline + - D406 + # Missing dashed underline after section + - D407 + # Missing blank line after last section + - D413 + +# https://flake8.pycqa.org/en/latest/user/error-codes.html +pyflakes: + disable: + # module imported but unused + # - F401 + +dodgy: + run: true + +bandit: + run: true + # options: + # ignore assert warning + # - B101 + +mypy: + run: true + options: + # https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-type-hints-for-third-party-library + ignore-missing-imports: true + # https://mypy.readthedocs.io/en/stable/running_mypy.html#following-imports + follow-imports: normal diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2e71aa4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[tool.poetry] +name = "tembo" +description = "A simple folder organiser for your work notes." +version = "0.0.8" +license = "ISC" +authors = ["dtomlinson "] +readme = "./README.md" +homepage = "https://tembo-pages.github.io/tembo-core/" +repository = "https://github.com/tembo-pages/tembo-core/" +documentation = "https://tembo-pages.github.io/tembo-core/" +keywords = ["notes", "organisation", "work"] + +[tool.poetry.dependencies] +python = "^3.8" +click = "^8.0.3" +pendulum = "^2.1.2" +Jinja2 = "^3.0.2" +# panaetius = "^2.3.2" +panaetius = { path = "../../panaetius", develop = true } + + +[tool.poetry.dev-dependencies] +pytest = "^6.2.5" +pytest-cov = "^3.0.0" +pytest-datadir = "^1.3.1" +pytest-custom-exit-code = "^0.3.0" +coverage = "^6.0.2" +prospector = { extras = ["with_bandit", "with_mypy"], version = "^1.5.1" } +duty = "^0.7.0" +pyinstaller = "^4.5.1" +isort = "^5.10.0" +mkdocs = "^1.2.3" +mkdocs-material = "^7.3.6" +mkdocstrings = "^0.16.2" +mkdocs-gen-files = "^0.3.3" +mkdocs-literate-nav = "^0.4.0" +mkdocs-coverage = "^0.2.4" +mkdocs-click = "^0.4.0" +mypy = "^0.910" +safety = "^1.10.3" +git-changelog = "^0.5.0" +Jinja2 = "^3.0.3" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +"tembo" = "tembo.cli.cli:main" + +[tool.black] +line-length = 120 + +[tool.isort] +line_length = 120 +multi_line_output = 3 +force_single_line = false +balanced_wrapping = true +default_section = "THIRDPARTY" +known_first_party = "duty" +include_trailing_comma = true diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cdc06cb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +click==8.0.3; python_version >= "3.6" +colorama==0.4.4; python_version >= "3.6" and python_full_version < "3.0.0" and platform_system == "Windows" or platform_system == "Windows" and python_version >= "3.6" and python_full_version >= "3.5.0" +jinja2==3.0.3; python_version >= "3.6" +markupsafe==2.0.1; python_version >= "3.6" +panaetius @ /home/dtomlinson/git-repos/python/panaetius; python_version >= "3.7" and python_version < "4.0" +pendulum==2.1.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +python-dateutil==2.8.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +pytzdata==2020.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +pyyaml==6.0; python_version >= "3.7" and python_version < "4.0" +six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +toml==0.10.2; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.3.0" diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..c7eec7c --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,96 @@ +altgraph==0.17.2; sys_platform == "darwin" and python_version >= "3.6" +ansimarkup==1.5.0; python_version >= "3.6" +astroid==2.8.5; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" +astunparse==1.6.3; python_full_version >= "3.6.1" and python_version >= "3.6" and python_version < "3.9" +atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0" +attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +bandit==1.7.1; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.5" +certifi==2021.10.8; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5" +charset-normalizer==2.0.7; python_full_version >= "3.6.0" and python_version >= "3.5" +click==8.0.3; python_version >= "3.6" +colorama==0.4.4; platform_system == "Windows" and python_version >= "3.6" and python_full_version >= "3.6.1" and sys_platform == "win32" and python_version < "4.0" +coverage==6.1.2; python_version >= "3.6" +dodgy==0.2.1; python_full_version >= "3.6.1" and python_version < "4.0" +dparse==0.5.1; python_version >= "3.5" +duty==0.7.0; python_version >= "3.6" +failprint==0.8.0; python_version >= "3.6" +flake8-polyfill==1.0.2; python_full_version >= "3.6.1" and python_version < "4.0" +flake8==2.3.0; python_full_version >= "3.6.1" and python_version < "4.0" +future==0.18.2; sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.6.0" +ghp-import==2.0.2; python_version >= "3.6" +git-changelog==0.5.0; python_full_version >= "3.6.2" +gitdb==4.0.9; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.7" +gitpython==3.1.24; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.7" +idna==3.3; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5" +importlib-metadata==4.8.2; python_version < "3.10" and python_version >= "3.6" +iniconfig==1.1.1; python_version >= "3.6" +isort==5.10.1; python_full_version >= "3.6.1" and python_version < "4.0" +jinja2==3.0.3; python_version >= "3.6" +lazy-object-proxy==1.6.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" +macholib==1.15.2; sys_platform == "darwin" and python_version >= "3.6" +markdown==3.3.6; python_version >= "3.7" and python_version < "4.0" +markupsafe==2.0.1; python_version >= "3.6" +mccabe==0.6.1; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6" +mergedeep==1.3.4; python_version >= "3.6" +mkdocs-autorefs==0.3.0; python_version >= "3.6" and python_version < "4.0" +mkdocs-click==0.4.0; python_version >= "3.7" +mkdocs-coverage==0.2.4; python_full_version >= "3.6.1" +mkdocs-gen-files==0.3.3; python_version >= "3.7" and python_version < "4.0" +mkdocs-literate-nav==0.4.0; python_version >= "3.6" and python_version < "4.0" +mkdocs-material-extensions==1.0.3; python_version >= "3.6" +mkdocs-material==7.3.6 +mkdocs==1.2.3; python_version >= "3.6" +mkdocstrings==0.16.2; python_version >= "3.6" +mypy-extensions==0.4.3; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.5" +mypy==0.910; python_version >= "3.5" +packaging==21.3; python_version >= "3.6" +panaetius @ /home/dtomlinson/git-repos/python/panaetius; python_version >= "3.7" and python_version < "4.0" +pbr==5.8.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6" +pefile==2021.9.3; sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.6.0" +pendulum==2.1.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +pep8-naming==0.10.0; python_full_version >= "3.6.1" and python_version < "4.0" +pep8==1.7.1; python_full_version >= "3.6.1" and python_version < "4.0" +platformdirs==2.4.0; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" +pluggy==1.0.0; python_version >= "3.6" +prospector==1.5.1; python_full_version >= "3.6.1" and python_version < "4.0" +ptyprocess==0.7.0; sys_platform != "win32" and python_version >= "3.6" +py==1.11.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +pycodestyle==2.8.0; python_full_version >= "3.6.1" and python_version < "4.0" +pydocstyle==6.1.1; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6" +pyflakes==2.3.1; python_full_version >= "3.6.1" and python_version < "4.0" +pygments==2.10.0; python_version >= "3.5" +pyinstaller-hooks-contrib==2021.3; python_version >= "3.6" +pyinstaller==4.5.1; python_version >= "3.6" +pylint-celery==0.3; python_full_version >= "3.6.1" and python_version < "4.0" +pylint-django==2.4.4; python_full_version >= "3.6.1" and python_version < "4.0" +pylint-flask==0.6; python_full_version >= "3.6.1" and python_version < "4.0" +pylint-plugin-utils==0.6; python_full_version >= "3.6.1" and python_version < "4.0" +pylint==2.11.1; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" +pymdown-extensions==9.1; python_version >= "3.6" +pyparsing==3.0.6; python_version >= "3.6" +pytest-cov==3.0.0; python_version >= "3.6" +pytest-custom-exit-code==0.3.0; (python_version > "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") +pytest-datadir==1.3.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") +pytest==6.2.5; python_version >= "3.6" +python-dateutil==2.8.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +pytkdocs==0.12.0; python_full_version >= "3.6.1" and python_version >= "3.6" +pytzdata==2020.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +pywin32-ctypes==0.2.0; sys_platform == "win32" and python_version >= "3.6" +pyyaml-env-tag==0.1; python_version >= "3.6" +pyyaml==6.0; python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.6.1" +requests==2.26.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.5" +requirements-detector==0.7; python_full_version >= "3.6.1" and python_version < "4.0" +safety==1.10.3; python_version >= "3.5" +semver==2.13.0; python_full_version >= "3.6.2" +setoptconf-tmp==0.3.1; python_full_version >= "3.6.1" and python_version < "4.0" +six==1.16.0; python_full_version >= "3.6.1" and python_version >= "3.6" and python_version < "3.9" +smmap==5.0.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.7" +snowballstemmer==2.2.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6" +stevedore==3.5.0; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6" +toml==0.10.2; python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.6.1" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.5") +tomli==1.2.2; python_version >= "3.6" +typing-extensions==4.0.0; python_full_version >= "3.6.1" and python_version < "3.10" and python_version >= "3.7" +urllib3==1.26.7; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.5" +watchdog==2.1.6; python_version >= "3.6" +wrapt==1.13.3; python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.1" +zipp==3.6.0; python_version >= "3.6" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..35ca0e1 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from setuptools import setup + +packages = \ +['tembo', 'tembo.cli', 'tembo.journal', 'tembo.utils'] + +package_data = \ +{'': ['*']} + +install_requires = \ +['Jinja2>=3.0.2,<4.0.0', + 'click>=8.0.3,<9.0.0', + 'panaetius>=2.3.2,<3.0.0', + 'pendulum>=2.1.2,<3.0.0'] + +entry_points = \ +{'console_scripts': ['tembo = tembo.cli.cli:main']} + +setup_kwargs = { + 'name': 'tembo', + 'version': '0.0.8', + 'description': 'A simple folder organiser for your work notes.', + 'long_description': '# Tembo\n\n\n\nA simple folder organiser for your work notes.\n\n![](https://img.shields.io/codecov/c/github/tembo-pages/tembo-core?style=flat-square)\n\n![Sonar Coverage](https://img.shields.io/sonar/coverage/tembo-pages_tembo-core?server=https%3A%2F%2Fsonarcloud.io&style=flat-square)\n![Sonar Tests](https://img.shields.io/sonar/tests/tembo-pages_tembo-core?compact_message&failed_label=failed&passed_label=passed&server=https%3A%2F%2Fsonarcloud.io&skipped_label=skipped&style=flat-square)\n![Sonar Tech Debt](https://img.shields.io/sonar/tech_debt/tembo-pages_tembo-core?server=https%3A%2F%2Fsonarcloud.io&style=flat-square)\n\n## config.yml\n\n```yaml\n# time tokens: https://strftime.org\ntembo:\n base_path: ~/tembo\n # template_path: ~/tembo/templates\n scopes:\n - name: scratchpad\n example: tembo new scratchpad\n path: "scratchpad/{d:%B_%Y}"\n filename: "{d:%B_%W}"\n extension: md\n template_filename: scratchpad.md.tpl\n - name: wtodo\n example: tembo new wtodo | directory is month_year, filename is month_week-of-year\n path: "wtodo/{d:%B_%Y}"\n filename: "week_{d:%W}"\n extension: todo\n template_filename: weekly.todo.tpl\n - name: meeting\n example: tembo new meeting $meeting_title\n path: "meetings/{d:%B_%y}"\n filename: "{d:%a_%d_%m_%y}-{input0}"\n extension: md\n template_filename: meeting.md.tpl\n - name: knowledge\n example: tembo new knowledge $project $filename\n path: "knowledge/{input0}"\n filename: "{input1}"\n extension: md\n template_filename: knowledge.md.tpl\n logging:\n level: INFO\n path: ~/tembo/.logs\n```\n\n## templates\n\n###\xa0knowledge\n\n```\n---\ncreated: {d:%d-%m-%Y}\n---\n\n# {input0} - {input1}.md\n```\n\n### meeting\n\n```\n---\ncreated: {d:%d-%m-%Y}\n---\n\n# {d:%A %d %B %Y} - {input0}\n\n## People\n\nHead:\n\nAttendees:\n\n## Actions\n\n\n## Notes\n\n```\n\n### scratchpad\n\n```\n---\ncreated: {d:%d-%m-%Y}\n---\n\n# Scratchpad - Week {d:%W} - {d:%B-%y}\n```\n\n### wtodo\n\n```\n---\ncreated: {d:%d-%m-%Y}\n---\n\nWeekly TODO | Week {d:%W} {d:%B}-{d:%Y}\n\nWork:\n\nDocumentation:\n```\n', + 'author': 'dtomlinson', + 'author_email': 'dtomlinson@panaetius.co.uk', + 'maintainer': None, + 'maintainer_email': None, + 'url': 'https://tembo-pages.github.io/tembo-core/', + 'packages': packages, + 'package_data': package_data, + 'install_requires': install_requires, + 'entry_points': entry_points, + 'python_requires': '>=3.8,<4.0', +} + + +setup(**setup_kwargs) diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..e6d61ed --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,9 @@ +sonar.organization=tembo-pages +sonar.projectKey=tembo-pages_tembo-core + +sonar.sources=tembo +sonar.tests=tests + +sonar.python.version=3 +sonar.python.coverage.reportPaths=coverage.xml +sonar.python.xunit.reportPath=pytest.xml diff --git a/tembo/__init__.py b/tembo/__init__.py new file mode 100644 index 0000000..c2d3f48 --- /dev/null +++ b/tembo/__init__.py @@ -0,0 +1,10 @@ +""" +Tembo package. + +A simple folder organiser for your work notes. +""" + +# flake8: noqa + +from . import exceptions +from .journal.pages import PageCreatorOptions, ScopedPageCreator diff --git a/tembo/__main__.py b/tembo/__main__.py new file mode 100644 index 0000000..34480e9 --- /dev/null +++ b/tembo/__main__.py @@ -0,0 +1,12 @@ +""" +Entrypoint module. + +Used when using `python -m tembo` to invoke the CLI. +""" + +import sys + +from tembo.cli.cli import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tembo/_version.py b/tembo/_version.py new file mode 100644 index 0000000..7c2d128 --- /dev/null +++ b/tembo/_version.py @@ -0,0 +1,3 @@ +"""Module containing the version of tembo.""" + +__version__ = "0.0.8" diff --git a/tembo/cli/__init__.py b/tembo/cli/__init__.py new file mode 100644 index 0000000..f05174b --- /dev/null +++ b/tembo/cli/__init__.py @@ -0,0 +1,31 @@ +"""Subpackage that contains the CLI application.""" + +import os +from typing import Any + +import panaetius +from panaetius.exceptions import LoggingDirectoryDoesNotExistException + +if (config_path := os.environ.get("TEMBO_CONFIG")) is not None: + CONFIG: Any = panaetius.Config("tembo", config_path, skip_header_init=True) +else: + CONFIG = panaetius.Config("tembo", "~/tembo/.config", skip_header_init=True) + + +panaetius.set_config(CONFIG, "base_path", "~/tembo") +panaetius.set_config(CONFIG, "template_path", "~/tembo/.templates") +panaetius.set_config(CONFIG, "scopes", {}) +panaetius.set_config(CONFIG, "logging.level", "DEBUG") +panaetius.set_config(CONFIG, "logging.path") + +try: + logger = panaetius.set_logger( + CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level) + ) +except LoggingDirectoryDoesNotExistException: + _LOGGING_PATH = CONFIG.logging_path + CONFIG.logging_path = "" + logger = panaetius.set_logger( + CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level) + ) + logger.warning("Logging directory %s does not exist", _LOGGING_PATH) diff --git a/tembo/cli/cli.py b/tembo/cli/cli.py new file mode 100644 index 0000000..52946a0 --- /dev/null +++ b/tembo/cli/cli.py @@ -0,0 +1,210 @@ +"""Submodule which contains the CLI implementation using Click.""" + +from __future__ import annotations + +import pathlib +from typing import Collection + +import click + +import tembo.cli +from tembo import exceptions +from tembo._version import __version__ +from tembo.journal import pages +from tembo.utils import Success + +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + + +@click.group(context_settings=CONTEXT_SETTINGS, options_metavar="") +@click.version_option( + __version__, + "-v", + "--version", + prog_name="Tembo", + message=f"Tembo v{__version__} 🐘", +) +def main(): + """Tembo - an organiser for work notes.""" + + +@click.command(options_metavar="", name="list") +def list_all(): + """List all scopes defined in the config.yml.""" + _all_scopes = [user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes] + _all_scopes_joined = "', '".join(_all_scopes) + cli_message(f"{len(_all_scopes)} names found in config.yml: '{_all_scopes_joined}'") + raise SystemExit(0) + + +@click.command(options_metavar="") +@click.argument("scope", metavar="") +@click.argument( + "inputs", + nargs=-1, + metavar="", +) +@click.option( + "--dry-run", + is_flag=True, + default=False, + help="Show the full path of the page to be created without actually saving the page to disk " + "and exit.", +) +@click.option( + "--example", + is_flag=True, + default=False, + help="Show the example command in the config.yml if it exists and exit.", +) +def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool): # noqa + """ + Create a new page. + + \b + `` The name of the scope in the config.yml. + \b + `` Any input token values that are defined in the config.yml for this scope. + Accepts multiple inputs separated by a space. + + \b + Example: + `tembo new meeting my_presentation` + """ + # check that the name exists in the config.yml + try: + _new_verify_name_exists(scope) + except ( + exceptions.ScopeNotFound, + exceptions.EmptyConfigYML, + exceptions.MissingConfigYML, + ) as tembo_exception: + cli_message(tembo_exception.args[0]) + raise SystemExit(1) from tembo_exception + + # get the scope configuration from the config.yml + try: + config_scope = _new_get_config_scope(scope) + except exceptions.MandatoryKeyNotFound as mandatory_key_not_found: + cli_message(mandatory_key_not_found.args[0]) + raise SystemExit(1) from mandatory_key_not_found + + # if --example flag, return the example to the user + _new_show_example(example, config_scope) + + # if the name is in the config.yml, create the scoped page + scoped_page = _new_create_scoped_page(config_scope, inputs) + + if dry_run: + cli_message(f"{scoped_page.path} will be created") + raise SystemExit(0) + + try: + result = scoped_page.save_to_disk() + if isinstance(result, Success): + cli_message(f"Saved {result.message} to disk") + raise SystemExit(0) + except exceptions.ScopedPageAlreadyExists as scoped_page_already_exists: + cli_message(f"File {scoped_page_already_exists}") + raise SystemExit(0) from scoped_page_already_exists + + +def _new_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> pages.Page: + page_creator_options = pages.PageCreatorOptions( + base_path=tembo.cli.CONFIG.base_path, + template_path=tembo.cli.CONFIG.template_path, + page_path=config_scope["path"], + filename=config_scope["filename"], + extension=config_scope["extension"], + name=config_scope["name"], + example=config_scope["example"], + user_input=inputs, + template_filename=config_scope["template_filename"], + ) + try: + return pages.ScopedPageCreator(page_creator_options).create_page() + except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: + cli_message(base_path_does_not_exist_error.args[0]) + raise SystemExit(1) from base_path_does_not_exist_error + except exceptions.TemplateFileNotFoundError as template_file_not_found_error: + cli_message(template_file_not_found_error.args[0]) + raise SystemExit(1) from template_file_not_found_error + except exceptions.MismatchedTokenError as mismatched_token_error: + if config_scope["example"] is not None: + cli_message( + f"Your tembo config.yml/template specifies {mismatched_token_error.expected}" + + f" input tokens, you gave {mismatched_token_error.given}. " + + f'Example: {config_scope["example"]}' + ) + raise SystemExit(1) from mismatched_token_error + cli_message( + f"Your tembo config.yml/template specifies {mismatched_token_error.expected}" + + f" input tokens, you gave {mismatched_token_error.given}" + ) + + raise SystemExit(1) from mismatched_token_error + + +def _new_verify_name_exists(scope: str) -> None: + _name_found = scope in [user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes] + if _name_found: + return + if len(tembo.cli.CONFIG.scopes) > 0: + # if the name is missing in the config.yml, raise error + raise exceptions.ScopeNotFound(f"Scope {scope} not found in config.yml") + # raise error if no config.yml found + if pathlib.Path(tembo.cli.CONFIG.config_path).exists(): + raise exceptions.EmptyConfigYML( + f"Config.yml found in {tembo.cli.CONFIG.config_path} is empty" + ) + raise exceptions.MissingConfigYML(f"No config.yml found in {tembo.cli.CONFIG.config_path}") + + +def _new_get_config_scope(scope: str) -> dict: + config_scope = {} + optional_keys = ["example", "template_filename"] + for option in [ + "name", + "path", + "filename", + "extension", + "example", + "template_filename", + ]: + try: + config_scope.update( + { + option: str(user_scope[option]) + for user_scope in tembo.cli.CONFIG.scopes + if user_scope["name"] == scope + } + ) + except KeyError as key_error: + if key_error.args[0] in optional_keys: + config_scope.update({key_error.args[0]: None}) + continue + raise exceptions.MandatoryKeyNotFound(f"Key {key_error} not found in config.yml") + return config_scope + + +def _new_show_example(example: bool, config_scope: dict) -> None: + if example: + if isinstance(config_scope["example"], str): + cli_message(f'Example for {config_scope["name"]}: {config_scope["example"]}') + else: + cli_message("No example in config.yml") + raise SystemExit(0) + + +def cli_message(message: str) -> None: + """ + Relay a message to the user using the CLI. + + Args: + message (str): THe message to be displayed. + """ + click.echo(f"[TEMBO] {message} 🐘") + + +main.add_command(new) +main.add_command(list_all) diff --git a/tembo/exceptions.py b/tembo/exceptions.py new file mode 100644 index 0000000..54cf2e0 --- /dev/null +++ b/tembo/exceptions.py @@ -0,0 +1,51 @@ +"""Module containing custom exceptions.""" + + +class MismatchedTokenError(Exception): + """ + Raised when the number of input tokens does not match the user config. + + Attributes: + expected (int): number of input tokens in the user config. + given (int): number of input tokens passed in. + """ + + def __init__(self, expected: int, given: int) -> None: + """ + Initialise the exception. + + Args: + expected (int): number of input tokens in the user config. + given (int): number of input tokens passed in. + """ + self.expected = expected + self.given = given + super().__init__() + + +class BasePathDoesNotExistError(Exception): + """Raised if the base path does not exist.""" + + +class TemplateFileNotFoundError(Exception): + """Raised if the template file does not exist.""" + + +class ScopedPageAlreadyExists(Exception): + """Raised if the scoped page file already exists.""" + + +class MissingConfigYML(Exception): + """Raised if the config.yml file is missing.""" + + +class EmptyConfigYML(Exception): + """Raised if the config.yml file is empty.""" + + +class ScopeNotFound(Exception): + """Raised if the scope does not exist in the config.yml.""" + + +class MandatoryKeyNotFound(Exception): + """Raised if a mandatory key is not found in the config.yml.""" diff --git a/tembo/journal/__init__.py b/tembo/journal/__init__.py new file mode 100644 index 0000000..0fd835e --- /dev/null +++ b/tembo/journal/__init__.py @@ -0,0 +1,5 @@ +"""Subpackage containing the logic to create Tembo journals & pages.""" + +# flake8: noqa + +from tembo.journal import pages diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py new file mode 100644 index 0000000..73760fa --- /dev/null +++ b/tembo/journal/pages.py @@ -0,0 +1,480 @@ +"""Submodule containing the factories & page objects to create Tembo pages.""" + +from __future__ import annotations + +import pathlib +import re +from abc import ABCMeta, abstractmethod +from dataclasses import dataclass +from typing import Collection, Optional + +import jinja2 +import pendulum +from jinja2.exceptions import TemplateNotFound + +import tembo.utils +from tembo import exceptions + + +@dataclass +class PageCreatorOptions: + """ + Options [dataclass][dataclasses.dataclass] to create a Page. + + This is passed to an implemented instance of [PageCreator][tembo.journal.pages.PageCreator] + + Attributes: + base_path (str): The base path. + page_path (str): The path of the page relative to the base path. + filename (str): The filename of the page. + extension (str): The extension of the page. + name (str): The name of the scope. + user_input (Collection[str] | None, optional): User input tokens. + example (str | None, optional): User example command. + template_path (str | None, optional): The path which contains the templates. This should + be the full path and not relative to the base path. + template_filename (str | None, optional): The template filename with extension relative + to the template path. + """ + + base_path: str + page_path: str + filename: str + extension: str + name: str + user_input: Optional[Collection[str]] = None + example: Optional[str] = None + template_path: Optional[str] = None + template_filename: Optional[str] = None + + +class PageCreator: + """ + A PageCreator factory base class. + + This factory should implement methods to create [Page][tembo.journal.pages.Page] objects. + + !!! abstract + This factory is an abstract base class and should be implemented for each + [Page][tembo.journal.pages.Page] type. + + The private methods + + - `_check_base_path_exists()` + - `_convert_base_path_to_path()` + - `_load_template()` + + are not abstract and are shared between all [Page][tembo.journal.pages.Page] types. + """ + + @abstractmethod + def __init__(self, options: PageCreatorOptions) -> None: + """ + When implemented this should initialise the `PageCreator` factory. + + Args: + options (PageCreatorOptions): An instance of + [PageCreatorOptions][tembo.journal.pages.PageCreatorOptions] + + !!! abstract + This method is abstract and should be implemented for each + [Page][tembo.journal.pages.Page] type. + """ + raise NotImplementedError + + @property + @abstractmethod + def options(self) -> PageCreatorOptions: + """ + When implemented this should return the `PageCreatorOptions` on the class. + + Returns: + PageCreatorOptions: the instance of + [PageCreatorOptions][tembo.journal.pages.PageCreatorOptions] set on the class. + + !!! abstract + This method is abstract and should be implemented for each + [Page][tembo.journal.pages.Page] type. + """ + raise NotImplementedError + + @abstractmethod + def create_page(self) -> Page: + """ + When implemented this should create a `Page` object. + + Returns: + Page: an implemented instance of [Page][tembo.journal.pages.Page] such as + [ScopedPage][tembo.journal.pages.ScopedPage]. + + !!! abstract + This method is abstract and should be implemented for each + [Page][tembo.journal.pages.Page] type. + """ + raise NotImplementedError + + def _check_base_path_exists(self) -> None: + """ + Check that the base path exists. + + Raises: + exceptions.BasePathDoesNotExistError: raised if the base path does not exist. + """ + if not pathlib.Path(self.options.base_path).expanduser().exists(): + raise exceptions.BasePathDoesNotExistError( + f"Tembo base path of {self.options.base_path} does not exist." + ) + + def _convert_base_path_to_path(self) -> pathlib.Path: + """ + Convert the `base_path` from a `str` to a `pathlib.Path` object. + + Returns: + pathlib.Path: the `base_path` as a `pathlib.Path` object. + """ + path_to_file = ( + pathlib.Path(self.options.base_path).expanduser() + / pathlib.Path(self.options.page_path.replace(" ", "_")).expanduser() + / self.options.filename.replace(" ", "_") + ) + # check for existing `.` in the extension + extension = ( + self.options.extension[1:] + if self.options.extension[0] == "." + else self.options.extension + ) + # return path with a file + return path_to_file.with_suffix(f".{extension}") + + def _load_template(self) -> str: + """ + Load the template file. + + Raises: + exceptions.TemplateFileNotFoundError: raised if the template file is specified but + not found. + + Returns: + str: the contents of the template file. + """ + if self.options.template_filename is None: + return "" + if self.options.template_path is not None: + converted_template_path = pathlib.Path(self.options.template_path).expanduser() + else: + converted_template_path = ( + pathlib.Path(self.options.base_path).expanduser() / ".templates" + ) + + file_loader = jinja2.FileSystemLoader(converted_template_path) + env = jinja2.Environment(loader=file_loader, autoescape=True) + + try: + loaded_template = env.get_template(self.options.template_filename) + except TemplateNotFound as template_not_found: + _template_file = f"{converted_template_path}/{template_not_found.args[0]}" + raise exceptions.TemplateFileNotFoundError( + f"Template file {_template_file} does not exist." + ) from template_not_found + return loaded_template.render() + + +class ScopedPageCreator(PageCreator): + """ + Factory to create a scoped page. + + Attributes: + base_path (str): base path of tembo. + page_path (str): path of the page relative to the base path. + filename (str): filename relative to the page path. + extension (str): extension of file. + """ + + def __init__(self, options: PageCreatorOptions) -> None: + """ + Initialise a `ScopedPageCreator` factory. + + Args: + options (PageCreatorOptions): An instance of + [PageCreatorOptions][tembo.journal.pages.PageCreatorOptions]. + """ + self._all_input_tokens: list[str] = [] + self._options = options + + @property + def options(self) -> PageCreatorOptions: + """ + Return the `PageCreatorOptions` instance set on the factory. + + Returns: + PageCreatorOptions: + An instance of [PageCreatorOptions][tembo.journal.pages.PageCreatorOptions]. + """ + return self._options + + def create_page(self) -> Page: + """ + Create a [ScopedPage][tembo.journal.pages.ScopedPage] object. + + This method will + + - Check the `base_path` exists + - Verify the input tokens match the number defined in the `config.yml` + - Substitue the input tokens in the filepath + - Load the template contents and substitue the input tokens + + Raises: + exceptions.MismatchedTokenError: Raises + [MismatchedTokenError][tembo.exceptions.MismatchedTokenError] if the number of + input tokens does not match the number of unique input tokens defined. + exceptions.BasePathDoesNotExistError: Raises + [BasePathDoesNotExistError][tembo.exceptions.BasePathDoesNotExistError] if the + base path does not exist. + exceptions.TemplateFileNotFoundError: Raises + [TemplateFileNotFoundError][tembo.exceptions.TemplateFileNotFoundError] if the + template file is specified but not found. + + + Returns: + Page: A [ScopedPage][tembo.journal.pages.ScopedPage] object using the + `PageCreatorOptions`. + """ + try: + self._check_base_path_exists() + except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: + raise base_path_does_not_exist_error + self._all_input_tokens = self._get_input_tokens() + try: + self._verify_input_tokens() + except exceptions.MismatchedTokenError as mismatched_token_error: + raise mismatched_token_error + + path = self._convert_base_path_to_path() + path = pathlib.Path(self._substitute_tokens(str(path))) + + try: + template_contents = self._load_template() + except exceptions.TemplateFileNotFoundError as template_not_found_error: + raise template_not_found_error + if self.options.template_filename is not None: + template_contents = self._substitute_tokens(template_contents) + + return ScopedPage(path, template_contents) + + def _get_input_tokens(self) -> list[str]: + """Get the input tokens from the path & user template.""" + path = str( + pathlib.Path( + self.options.base_path, + self.options.page_path, + self.options.filename, + ) + .expanduser() + .with_suffix(f".{self.options.extension}") + ) + template_contents = self._load_template() + # get the input tokens from both the path and the template + all_input_tokens = [] + for tokenified_string in (path, template_contents): + all_input_tokens.extend(re.findall(r"(\{input\d*\})", tokenified_string)) + return sorted(list(set(all_input_tokens))) + + def _verify_input_tokens(self) -> None: + """ + Verify the input tokens. + + The number of input tokens should match the number of unique input tokens defined in the + path and the user's template. + + Raises: + exceptions.MismatchedTokenError: Raises + [MismatchedTokenError][tembo.exceptions.MismatchedTokenError] if the number of + input tokens does not match the number of unique input tokens defined. + """ + if len(self._all_input_tokens) > 0 and self.options.user_input is None: + raise exceptions.MismatchedTokenError(expected=len(self._all_input_tokens), given=0) + if self.options.user_input is None: + return + if len(self._all_input_tokens) != len(self.options.user_input): + raise exceptions.MismatchedTokenError( + expected=len(self._all_input_tokens), + given=len(self.options.user_input), + ) + + def _substitute_tokens(self, tokenified_string: str) -> str: + """For a tokened string, substitute input, name and date tokens.""" + tokenified_string = self.__substitute_input_tokens(tokenified_string) + tokenified_string = self.__substitute_name_tokens(tokenified_string) + tokenified_string = self.__substitute_date_tokens(tokenified_string) + return tokenified_string + + def __substitute_input_tokens(self, tokenified_string: str) -> str: + """ + Substitue the input tokens in a `str` with the user input. + + Args: + tokenified_string (str): a string with input tokens. + + Returns: + str: the string with the input tokens replaced by the user input. + + Examples: + A `user_input` of `("monthly_meeting",)` with a `tokenified_string` of + `/meetings/{input0}/` results in a string of `/meetings/monthly_meeting/` + """ + if self.options.user_input is not None: + for input_value, extracted_token in zip( + self.options.user_input, self._all_input_tokens + ): + tokenified_string = tokenified_string.replace( + extracted_token, input_value.replace(" ", "_") + ) + return tokenified_string + + def __substitute_name_tokens(self, tokenified_string: str) -> str: + """Find any `{name}` tokens and substitute for the name value in a `str`.""" + name_extraction = re.findall(r"(\{name\})", tokenified_string) + for extracted_input in name_extraction: + tokenified_string = tokenified_string.replace(extracted_input, self.options.name) + return tokenified_string + + @staticmethod + def __substitute_date_tokens(tokenified_string: str) -> str: + """Find any {d:%d-%M-%Y} tokens in a `str`.""" + # extract the full token string + date_extraction_token = re.findall(r"(\{d\:[^}]*\})", tokenified_string) + for extracted_token in date_extraction_token: + # extract the inner %d-%M-%Y only + strftime_value = re.match(r"\{d\:([^\}]*)\}", extracted_token) + if strftime_value is not None: + strftime_value = strftime_value.group(1) + if isinstance(strftime_value, str): + tokenified_string = tokenified_string.replace( + extracted_token, pendulum.now().strftime(strftime_value) + ) + return tokenified_string + + +class Page(metaclass=ABCMeta): + """ + Abstract Page class. + + This interface is used to define a `Page` object. + + A `Page` represents a note/page that will be saved to disk. + + !!! abstract + This object is an abstract base class and should be implemented for each `Page` type. + """ + + @abstractmethod + def __init__(self, path: pathlib.Path, page_content: str) -> None: + """ + When implemented this should initalise a Page object. + + Args: + path (pathlib.Path): the full path of the page including the filename as a + [Path][pathlib.Path]. + page_content (str): the contents of the page. + + !!! abstract + This method is abstract and should be implemented for each `Page` type. + """ + raise NotImplementedError + + @property + @abstractmethod + def path(self) -> pathlib.Path: + """ + When implemented this should return the full path of the page including the filename. + + Returns: + pathlib.Path: the path as a [Path][pathlib.Path] object. + + !!! abstract + This property is abstract and should be implemented for each `Page` type. + """ + raise NotImplementedError + + @abstractmethod + def save_to_disk(self) -> tembo.utils.Success: + """ + When implemented this should save the page to disk. + + Returns: + tembo.utils.Success: A Tembo [Success][tembo.utils.__init__.Success] object. + + !!! abstract + This method is abstract and should be implemented for each `Page` type. + """ + raise NotImplementedError + + +class ScopedPage(Page): + """ + A page that uses substitute tokens. + + Attributes: + path (pathlib.Path): a [Path][pathlib.Path] object of the page's filepath including the + filename. + page_content (str): the content of the page from the template. + """ + + def __init__(self, path: pathlib.Path, page_content: str) -> None: + """ + Initalise a scoped page object. + + Args: + path (pathlib.Path): a [Path][pathlib.Path] object of the page's filepath including + the filename. + page_content (str): the content of the page from the template. + """ + self._path = path + self.page_content = page_content + + def __str__(self) -> str: + """ + Return a `str` representation of a `ScopedPage`. + + Examples: + ``` + >>> str(ScopedPage(Path("/home/bob/tembo/meetings/my_meeting_0.md"), "")) + ScopedPage("/home/bob/tembo/meetings/my_meeting_0.md") + ``` + + Returns: + str: The `ScopedPage` as a `str`. + """ + return f'ScopedPage("{self.path}")' + + @property + def path(self) -> pathlib.Path: + """ + Return the full path of the page. + + Returns: + pathlib.path: The full path of the page as a [Path][pathlib.Path] object. + """ + return self._path + + def save_to_disk(self) -> tembo.utils.Success: + """ + Save the scoped page to disk and write the `page_content`. + + Raises: + exceptions.ScopedPageAlreadyExists: If the page already exists a + [ScopedPageAlreadyExists][tembo.exceptions.ScopedPageAlreadyExists] exception + is raised. + + Returns: + tembo.utils.Success: A [Success][tembo.utils.__init__.Success] with the path of the + ScopedPage as the message. + """ + # create the parent directories + scoped_page_file = pathlib.Path(self.path) + scoped_page_file.parents[0].mkdir(parents=True, exist_ok=True) + if scoped_page_file.exists(): + raise exceptions.ScopedPageAlreadyExists(f"{self.path} already exists") + with scoped_page_file.open("w", encoding="utf-8") as scoped_page: + scoped_page.write(self.page_content) + return tembo.utils.Success(str(self.path)) diff --git a/tembo/utils/__init__.py b/tembo/utils/__init__.py new file mode 100644 index 0000000..d4a75bb --- /dev/null +++ b/tembo/utils/__init__.py @@ -0,0 +1,18 @@ +"""Subpackage containing utility objects.""" + +from dataclasses import dataclass + + +@dataclass +class Success: + """ + A Tembo success object. + + This is returned from [Page][tembo.journal.pages.ScopedPage] methods such as + [save_to_disk()][tembo.journal.pages.ScopedPage.save_to_disk] + + Attributes: + message (str): A success message. + """ + + message: str diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cli/data/config/empty/config.yml b/tests/test_cli/data/config/empty/config.yml new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/tests/test_cli/data/config/empty/config.yml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/test_cli/data/config/missing_keys/config.yml b/tests/test_cli/data/config/missing_keys/config.yml new file mode 100644 index 0000000..6f6a2f8 --- /dev/null +++ b/tests/test_cli/data/config/missing_keys/config.yml @@ -0,0 +1,5 @@ +tembo: + scopes: + - name: some_scope + path: "some_scope" + extension: md diff --git a/tests/test_cli/data/config/missing_template/config.yml b/tests/test_cli/data/config/missing_template/config.yml new file mode 100644 index 0000000..abb09f7 --- /dev/null +++ b/tests/test_cli/data/config/missing_template/config.yml @@ -0,0 +1,8 @@ +tembo: + scopes: + - name: some_scope + example: tembo new some_scope + path: some_scope + filename: "{name}" + extension: md + template_filename: some_nonexistent_template.md.tpl diff --git a/tests/test_cli/data/config/optional_keys/config.yml b/tests/test_cli/data/config/optional_keys/config.yml new file mode 100644 index 0000000..2af3099 --- /dev/null +++ b/tests/test_cli/data/config/optional_keys/config.yml @@ -0,0 +1,6 @@ +tembo: + scopes: + - name: some_scope + path: "some_scope" + filename: "{name}" + extension: md diff --git a/tests/test_cli/data/config/success/config.yml b/tests/test_cli/data/config/success/config.yml new file mode 100644 index 0000000..6db5753 --- /dev/null +++ b/tests/test_cli/data/config/success/config.yml @@ -0,0 +1,16 @@ +tembo: + scopes: + - name: some_scope + example: tembo new some_scope + path: "some_scope" + filename: "{name}" + extension: md + - name: some_scope_no_example + path: "some_scope" + filename: "{name}" + extension: md + - name: another_some_scope + example: tembo new another_some_scope + path: "another_some_scope" + filename: "{name}" + extension: md diff --git a/tests/test_cli/data/some_scope/some_scope.md b/tests/test_cli/data/some_scope/some_scope.md new file mode 100644 index 0000000..ce7e948 --- /dev/null +++ b/tests/test_cli/data/some_scope/some_scope.md @@ -0,0 +1 @@ +already exists diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py new file mode 100644 index 0000000..00fe9f0 --- /dev/null +++ b/tests/test_cli/test_cli.py @@ -0,0 +1,303 @@ +import importlib +import os +import pathlib + +import pytest + +import tembo.cli +from tembo.cli.cli import new, list_all + + +def test_new_dry_run(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + scope = "some_scope" + dry_run = "--dry-run" + + # act + with pytest.raises(SystemExit) as system_exit: + new([scope, dry_run]) + + # assert + assert system_exit.value.code == 0 + assert ( + capsys.readouterr().out + == f"[TEMBO] {tmpdir}/some_scope/some_scope.md will be created 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_success(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + scoped_page_file = pathlib.Path(tmpdir / "some_scope" / "some_scope").with_suffix( + ".md" + ) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope"]) + + # assert + assert scoped_page_file.exists() + assert system_exit.value.code == 0 + assert capsys.readouterr().out == f"[TEMBO] Saved {scoped_page_file} to disk 🐘\n" + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_success_already_exists(shared_datadir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(shared_datadir) + importlib.reload(tembo.cli) + scoped_page_file = pathlib.Path( + shared_datadir / "some_scope" / "some_scope" + ).with_suffix(".md") + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope"]) + + # assert + assert scoped_page_file.exists() + assert system_exit.value.code == 0 + assert ( + capsys.readouterr().out == f"[TEMBO] File {scoped_page_file} already exists 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_scope_not_found(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + scoped_page_file = pathlib.Path(tmpdir / "some_scope" / "some_scope").with_suffix( + ".md" + ) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_nonexistent_scope"]) + + # assert + assert not scoped_page_file.exists() + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == "[TEMBO] Scope some_nonexistent_scope not found in config.yml 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_empty_config(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "empty") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_nonexistent_scope"]) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f"[TEMBO] Config.yml found in {shared_datadir}/config/empty is empty 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_missing_config(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_nonexistent_scope"]) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f"[TEMBO] No config.yml found in {shared_datadir}/config/missing 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_missing_mandatory_key(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing_keys") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope"]) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out == "[TEMBO] Key 'filename' not found in config.yml 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +@pytest.mark.parametrize( + "path,message", + [ + ("success", "[TEMBO] Example for some_scope: tembo new some_scope 🐘\n"), + ("optional_keys", "[TEMBO] No example in config.yml 🐘\n"), + ], +) +def test_new_show_example(path, message, shared_datadir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / path) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope", "--example"]) + + # assert + assert system_exit.value.code == 0 + assert capsys.readouterr().out == message + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +def test_new_base_path_does_not_exist(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir / "nonexistent" / "path") + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope"]) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f"[TEMBO] Tembo base path of {tmpdir}/nonexistent/path does not exist. 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_template_file_does_not_exist(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing_template") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + os.environ["TEMBO_TEMPLATE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope"]) + + # assert + assert ( + capsys.readouterr().out + == f"[TEMBO] Template file {tmpdir}/some_nonexistent_template.md.tpl does not exist. 🐘\n" + ) + assert system_exit.value.code == 1 + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_TEMPLATE_PATH"] + + +def test_new_mismatched_tokens_with_example(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope", "input0", "input1"]) + + # assert + assert system_exit.value.code == 1 + assert capsys.readouterr().out == "[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 2. Example: tembo new some_scope 🐘\n" + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_new_mismatched_tokens_without_example(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + # act + with pytest.raises(SystemExit) as system_exit: + new(["some_scope_no_example", "input0", "input1"]) + + # assert + assert system_exit.value.code == 1 + assert capsys.readouterr().out == "[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 2 🐘\n" + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +def test_list_all_success(shared_datadir, tmpdir, capsys): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + scoped_page_file = pathlib.Path(tmpdir / "some_scope" / "some_scope").with_suffix( + ".md" + ) + + # act + with pytest.raises(SystemExit) as system_exit: + list_all([]) + + # assert + assert system_exit.value.code == 0 + assert ( + capsys.readouterr().out + == "[TEMBO] 3 names found in config.yml: 'some_scope', 'some_scope_no_example', 'another_some_scope' 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py new file mode 100644 index 0000000..399c6c8 --- /dev/null +++ b/tests/test_journal/test_pages.py @@ -0,0 +1,388 @@ +from datetime import date +import pathlib + +import pytest + +from tembo import PageCreatorOptions, ScopedPageCreator +from tembo import exceptions +from tembo.utils import Success + + +DATE_TODAY = date.today().strftime("%d-%m-%Y") + + +def test_create_page_base_path_does_not_exist(tmpdir): + # arrange + base_path = str(tmpdir / "nonexistent" / "path") + options = PageCreatorOptions( + base_path=base_path, + page_path="", + filename="", + extension="", + name="", + user_input=None, + example=None, + template_filename=None, + template_path=None, + ) + + # act + with pytest.raises( + exceptions.BasePathDoesNotExistError + ) as base_path_does_not_exist_error: + scoped_page = ScopedPageCreator(options).create_page() + + # assert + assert ( + str(base_path_does_not_exist_error.value) + == f"Tembo base path of {base_path} does not exist." + ) + + +@pytest.mark.parametrize("template_path", [(None), ("/nonexistent/path")]) +def test_create_page_template_file_does_not_exist(template_path, tmpdir): + # arrange + options = PageCreatorOptions( + base_path=str(tmpdir), + page_path="some_path", + filename="some_filename", + extension="some_extension", + name="some_name", + user_input=None, + example=None, + template_filename="template.md.tpl", + template_path=template_path, + ) + + # act + with pytest.raises( + exceptions.TemplateFileNotFoundError + ) as template_file_not_found_error: + scoped_page = ScopedPageCreator(options).create_page() + + # assert + if template_path is None: + assert str(template_file_not_found_error.value) == ( + f"Template file {options.base_path}/.templates/{options.template_filename} does not exist." + ) + else: + assert str(template_file_not_found_error.value) == ( + f"Template file {template_path}/{options.template_filename} does not exist." + ) + + +def test_create_page_already_exists(datadir): + # arrange + options = PageCreatorOptions( + base_path=str(datadir), + page_path="does_exist", + filename="some_note", + extension="md", + name="some_name", + user_input=None, + example=None, + template_filename=None, + template_path=None, + ) + scoped_page_file = ( + pathlib.Path(options.base_path) / options.page_path / options.filename + ).with_suffix(f".{options.extension}") + + # act + scoped_page = ScopedPageCreator(options).create_page() + with pytest.raises(exceptions.ScopedPageAlreadyExists) as page_already_exists: + result = scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert str(page_already_exists.value) == f"{scoped_page_file} already exists" + with scoped_page_file.open("r", encoding="utf-8") as scoped_page_contents: + assert scoped_page_contents.readlines() == ["this file already exists\n"] + + +def test_create_page_without_template(tmpdir): + # arrange + options = PageCreatorOptions( + base_path=str(tmpdir), + page_path="some_path", + filename="some_filename", + extension="some_extension", + name="some_name", + user_input=None, + example=None, + template_filename=None, + template_path=None, + ) + scoped_page_file = ( + pathlib.Path(options.base_path) / options.page_path / options.filename + ).with_suffix(f".{options.extension}") + + # act + scoped_page = ScopedPageCreator(options).create_page() + result = scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) + with scoped_page_file.open("r", encoding="utf-8") as scoped_page_contents: + assert scoped_page_contents.readlines() == [] + + +def test_create_page_with_template(datadir, caplog): + # arrange + options = PageCreatorOptions( + base_path=str(datadir), + page_path="some_path", + filename="some_note", + extension="md", + name="some_name", + user_input=None, + example=None, + template_filename="some_template_no_tokens.md.tpl", + template_path=None, + ) + scoped_page_file = ( + pathlib.Path(options.base_path) / options.page_path / options.filename + ).with_suffix(f".{options.extension}") + + # act + scoped_page = ScopedPageCreator(options).create_page() + result = scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) + with scoped_page_file.open("r", encoding="utf-8") as scoped_page_contents: + assert scoped_page_contents.readlines() == [ + "scoped page file\n", + "\n", + "no tokens", + ] + + +@pytest.mark.parametrize( + "user_input,template_filename,page_contents", + [ + (None, "some_template_date_tokens.md.tpl", f"some date token: {DATE_TODAY}"), + ( + ("first_input", "second_input"), + "some_template_input_tokens.md.tpl", + "some input tokens second_input first_input", + ), + (None, "some_template_name_tokens.md.tpl", "some name token some_name"), + ], +) +def test_create_tokened_page_tokens_in_template( + datadir, caplog, user_input, template_filename, page_contents +): + # arrange + options = PageCreatorOptions( + base_path=str(datadir), + page_path="some_path", + filename="some_note", + extension="md", + name="some_name", + user_input=user_input, + example=None, + template_filename=template_filename, + template_path=None, + ) + scoped_page_file = ( + pathlib.Path(options.base_path) / options.page_path / options.filename + ).with_suffix(f".{options.extension}") + + # act + scoped_page = ScopedPageCreator(options).create_page() + result = scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) + + with scoped_page_file.open("r", encoding="utf-8") as scoped_page_contents: + assert scoped_page_contents.readline() == page_contents + + +@pytest.mark.parametrize( + "user_input,filename,tokened_filename", + [ + (None, "date_token_{d:%d-%m-%Y}", f"date_token_{DATE_TODAY}"), + (None, "name_token_{name}", "name_token_some_name"), + ( + ("first_input", "second input"), + "input_token_{input1}_{input0}", + "input_token_second_input_first_input", + ), + ], +) +def test_create_tokened_page_tokens_in_filename( + datadir, caplog, user_input, filename, tokened_filename +): + # arrange + options = PageCreatorOptions( + base_path=str(datadir), + page_path="some_path", + filename=filename, + extension="md", + name="some_name", + user_input=user_input, + example=None, + template_filename=None, + template_path=None, + ) + scoped_page_file = ( + pathlib.Path(options.base_path) / options.page_path / tokened_filename + ).with_suffix(f".{options.extension}") + + # act + scoped_page = ScopedPageCreator(options).create_page() + result = scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) + + +def test_create_tokened_page_input_tokens_preserve_order(datadir, caplog): + # arrange + tokened_filename = "input_token_fourth_input_first_input" + options = PageCreatorOptions( + base_path=str(datadir), + page_path="some_path", + filename="input_token_{input3}_{input0}", + extension="md", + name="some_name", + user_input=("first_input", "second_input", "third_input", "fourth_input"), + example=None, + template_filename="some_template_input_tokens_preserve_order.md.tpl", + template_path=None, + ) + scoped_page_file = ( + pathlib.Path(options.base_path) / options.page_path / tokened_filename + ).with_suffix(f".{options.extension}") + + # act + scoped_page = ScopedPageCreator(options).create_page() + result = scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) + with scoped_page_file.open(mode="r", encoding="utf-8") as scoped_page_contents: + assert scoped_page_contents.readline() == "third_input second_input" + + +@pytest.mark.parametrize( + "user_input,expected,given", + [ + (None, 3, 0), + (("first_input", "second_input"), 3, 2), + (("first_input", "second_input", "third_input", "fourth_input"), 3, 4), + ], +) +def test_create_page_mismatched_tokens(tmpdir, user_input, expected, given): + # arrange + options = PageCreatorOptions( + base_path=str(tmpdir), + page_path="some_path", + filename="input_token_{input0}_{input1}_{input2}", + extension="md", + name="some_name", + user_input=user_input, + example=None, + template_filename=None, + template_path=None, + ) + + # act + with pytest.raises(exceptions.MismatchedTokenError) as mismatched_token_error: + scoped_page = ScopedPageCreator(options).create_page() + + # assert + assert mismatched_token_error.value.expected == expected + assert mismatched_token_error.value.given == given + + +def test_create_page_spaces_in_path(tmpdir, caplog): + # arrange + options = PageCreatorOptions( + base_path=str(tmpdir), + page_path="some path with a space", + filename="some filename with a space", + extension="md", + name="some_name", + user_input=None, + example=None, + template_filename=None, + template_path=None, + ) + scoped_page_file = ( + pathlib.Path(options.base_path) + / options.page_path.replace(" ", "_") + / options.filename.replace(" ", "_") + ).with_suffix(f".{options.extension}") + + # act + scoped_page = ScopedPageCreator(options).create_page() + result = scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) + + +def test_create_page_dot_in_extension(tmpdir, caplog): + # arrange + options = PageCreatorOptions( + base_path=str(tmpdir), + page_path="some_path", + filename="some_filename", + extension=".md", + name="some_name", + user_input=None, + example=None, + template_filename=None, + template_path=None, + ) + scoped_page_file = ( + pathlib.Path(options.base_path) / options.page_path / options.filename + ).with_suffix(f".{options.extension[1:]}") + + # act + scoped_page = ScopedPageCreator(options).create_page() + result = scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) + + +def test_create_page_str_representation(tmpdir): + # arrange + options = PageCreatorOptions( + base_path=str(tmpdir), + page_path="some_path", + filename="some_filename", + extension="md", + name="some_name", + user_input=None, + example=None, + template_filename=None, + template_path=None, + ) + scoped_page_file = ( + pathlib.Path(options.base_path) / options.page_path / options.filename + ).with_suffix(f".{options.extension}") + + # act + scoped_page = ScopedPageCreator(options).create_page() + + # assert + assert str(scoped_page) == f"ScopedPage(\"{scoped_page_file}\")" diff --git a/tests/test_journal/test_pages/.templates/some_template_date_tokens.md.tpl b/tests/test_journal/test_pages/.templates/some_template_date_tokens.md.tpl new file mode 100644 index 0000000..da47289 --- /dev/null +++ b/tests/test_journal/test_pages/.templates/some_template_date_tokens.md.tpl @@ -0,0 +1 @@ +some date token: {d:%d-%m-%Y} diff --git a/tests/test_journal/test_pages/.templates/some_template_input_tokens.md.tpl b/tests/test_journal/test_pages/.templates/some_template_input_tokens.md.tpl new file mode 100644 index 0000000..18bcc20 --- /dev/null +++ b/tests/test_journal/test_pages/.templates/some_template_input_tokens.md.tpl @@ -0,0 +1 @@ +some input tokens {input1} {input0} diff --git a/tests/test_journal/test_pages/.templates/some_template_input_tokens_preserve_order.md.tpl b/tests/test_journal/test_pages/.templates/some_template_input_tokens_preserve_order.md.tpl new file mode 100644 index 0000000..7d43d68 --- /dev/null +++ b/tests/test_journal/test_pages/.templates/some_template_input_tokens_preserve_order.md.tpl @@ -0,0 +1 @@ +{input2} {input1} diff --git a/tests/test_journal/test_pages/.templates/some_template_name_tokens.md.tpl b/tests/test_journal/test_pages/.templates/some_template_name_tokens.md.tpl new file mode 100644 index 0000000..77ecade --- /dev/null +++ b/tests/test_journal/test_pages/.templates/some_template_name_tokens.md.tpl @@ -0,0 +1 @@ +some name token {name} diff --git a/tests/test_journal/test_pages/.templates/some_template_no_tokens.md.tpl b/tests/test_journal/test_pages/.templates/some_template_no_tokens.md.tpl new file mode 100644 index 0000000..6a2f5e1 --- /dev/null +++ b/tests/test_journal/test_pages/.templates/some_template_no_tokens.md.tpl @@ -0,0 +1,3 @@ +scoped page file + +no tokens diff --git a/tests/test_journal/test_pages/does_exist/some_note.md b/tests/test_journal/test_pages/does_exist/some_note.md new file mode 100644 index 0000000..cc0459b --- /dev/null +++ b/tests/test_journal/test_pages/does_exist/some_note.md @@ -0,0 +1 @@ +this file already exists