diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index a119bd2..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..ff6415d
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,7 @@
+[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
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..34243a4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,142 @@
+# 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__
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..73bb444
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.8.11
diff --git a/README.md b/README.md
index 0050759..ee336e9 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
# Tembo
+
+A simple folder organiser for your work notes.
diff --git a/TODO.todo b/TODO.todo
new file mode 100644
index 0000000..2ad76f5
--- /dev/null
+++ b/TODO.todo
@@ -0,0 +1,147 @@
+Priority:
+ ☐ Write the tests
+ ☐ test logs:
+ document this
+ ☐ Docstrings
+
+Documentation:
+ Docstrings:
+ ☐ Use Duty to write module docstrings
+ ☐ Use Duty to add Class docstrings
+ ☐ Document these in Trilium and rewrite the docstrings notes
+ ☐ Add the comment on Reddit (artie buco?) about imports in a module
+ ☐ Document using `__main__.py` and `cli.py`
+ Use Duty as an example
+
+ ☐ Document regex usage
+ ☐ Write documentation using `mkdocs`
+ ☐ Create a boilerplate `duties.py` for common tasks for future projects. Put in a gist.
+ ☐ Look at how to use github actions
+ Use for an example
+ ☐ Build the docs using a github action.
+
+ ☐ Document how to use pytest to read a logging message
+
+ - caplog as fixture
+ - reading `caplog.records[0].message`
+ see `_old_test_pages.py`
+
+ ☐ Document testing value of an exception raised
+ When you use `with pytest.raises` you can use `.value` to access the attributes
+ reading `.value.code`
+ reading `str(.value)`
+
+ ☐ Document working with exceptions
+ ☐ General pattern - raise exceptions in codebase, catch them in the CLI.
+ 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.
+ 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
+ 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
+ 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.
+ ☐ Can prospector ignore tests dir? document this in the gist if so
+ ☐ Redo the documentation on a CLI, reorganise and inocropoate all the new tembo layouts
+
+ Testing:
+ ☐ Document importing in inidivudal tests using `importlib.reload`
+ Globally import the module
+ Use `importlib.reload(module)` in each test instead of explicitly importing the module.
+ This is because the import is cached.
+
+
+Functionality:
+ ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages?
+ 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
+ ☐ Use the complicated CLI example so the tembo new has its own module to define functions in
+ ☐ Replace all logger errors with exceptions, move logger messages to the cli.
+ ☐ How to pass a successful save notification back to the CLI? Return a bool? Or is there some other way?
+ ☐ Replace pendulum with datetime
+ ✔ Make options a property on the class, add to abstract @done(21-10-30 19:31)
+ ☐ Use the python runner Duty
+
+ ☐ Run tests
+ ☐ Update poetry
+ ☐ Build docs
+ ☐ Document using Duty
+ ☐ Duty for auto insert version from `poetry version`.
+ Need to decide what file to place `__version__` in.
+
+Logging:
+ ☐ Make all internal tembo logs be debug
+ ☐ User can enable them with the config
+
+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 :=
+
+Tests:
+ ☐ Write tests! @2d
+ Use coverage as going along to make sure all bases are covered in the testing
+
+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:
+ ✔ 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/dev/notes/test.md b/dev/notes/test.md
new file mode 100644
index 0000000..7566c82
--- /dev/null
+++ b/dev/notes/test.md
@@ -0,0 +1,39 @@
+# testing notes
+
+## options
+
+optional:
+
+- user_input
+- example
+- template_filename
+- template_path
+
+required:
+
+- base_path
+- page_path
+- filename
+- extension
+- name
+
+## tests to write
+
+- user input does not match number of input tokens
+ - no user input
+ - mismatched user input
+ - with/without example
+
+- dry run
+
+## tests done
+
+- path/page filenames can contain spaces and they are converted
+- user input is None
+- page using/not using input tokens
+- page using/not using date tokens
+- page using/not using name tokens
+- page with/without a template
+- the given base path does not exist
+- the given template file does not exist
+- page already exists
diff --git a/duties.py b/duties.py
new file mode 100644
index 0000000..4f23051
--- /dev/null
+++ b/duties.py
@@ -0,0 +1,31 @@
+from duty import duty
+
+
+@duty
+def test(ctx):
+ ctx.run(["echo", "test"], title="test command")
+
+
+@duty
+def update_deps(ctx, dry: bool = False):
+ """Update the dependencies using Poetry.
+
+ 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 coverage(ctx):
+ """Generate a coverage HTML report.
+
+ Example:
+ `duty coverage`
+ """
+ ctx.run(["coverage", "run", "--source", "tembo", "-m", "pytest"])
+ ctx.run(["coverage", "html"])
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..e9a558f
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,1211 @@
+[[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.4"
+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 = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "21.2.0"
+description = "Classes Without Boilerplate"
+category = "main"
+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.0"
+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"
+six = ">=1.10.0"
+stevedore = ">=1.20.0"
+
+[[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.0.2"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[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 = "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 = "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 = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "isort"
+version = "5.9.3"
+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.2"
+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 = "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 = "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.2"
+description = "Core utilities for Python packages"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3"
+
+[[package]]
+name = "panaetius"
+version = "2.2.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.6.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 = "main"
+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.10.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "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 = "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 = "pyparsing"
+version = "2.4.7"
+description = "Python parsing module"
+category = "main"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "pytest"
+version = "6.2.5"
+description = "pytest: simple powerful testing with Python"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+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-datadir"
+version = "1.3.1"
+description = "pytest plugin for test data directories and files"
+category = "main"
+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 = "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 = "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 = "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.1.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 = "typing-extensions"
+version = "3.10.0.2"
+description = "Backported and Experimental Type Hints for Python 3.5+"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "wrapt"
+version = "1.13.2"
+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"
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.8"
+content-hash = "a44f31719364047d4a223e74f8b31a8cb20ec7940bb82428e038648d70c71e0e"
+
+[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.4-py3-none-any.whl", hash = "sha256:0755c998e7117078dcb7d0bda621391dd2a85da48052d948c7411ab187325346"},
+ {file = "astroid-2.8.4.tar.gz", hash = "sha256:1e83a69fd51b013ebf5912d26b9338d6643a55fec2f20c787792680610eed4a2"},
+]
+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.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"},
+ {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"},
+]
+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.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"},
+ {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"},
+ {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"},
+ {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"},
+ {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"},
+ {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"},
+ {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"},
+ {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"},
+ {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"},
+ {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"},
+ {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"},
+ {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"},
+ {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"},
+ {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"},
+ {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"},
+ {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"},
+ {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"},
+ {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"},
+ {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"},
+ {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"},
+ {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"},
+ {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"},
+ {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"},
+ {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"},
+ {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"},
+ {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"},
+ {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"},
+ {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"},
+ {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"},
+ {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"},
+ {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"},
+ {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"},
+ {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"},
+]
+dodgy = [
+ {file = "dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"},
+ {file = "dodgy-0.2.1.tar.gz", hash = "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a"},
+]
+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"},
+]
+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"},
+]
+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.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"},
+ {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"},
+]
+jinja2 = [
+ {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"},
+ {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"},
+]
+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"},
+]
+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-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
+ {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
+ {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
+ {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-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
+ {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
+ {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
+ {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-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
+ {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
+ {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
+ {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-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
+ {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
+ {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
+ {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-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
+ {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
+ {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
+ {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"},
+]
+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.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"},
+ {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"},
+]
+panaetius = []
+pbr = [
+ {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"},
+ {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"},
+]
+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.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
+ {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
+]
+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"},
+]
+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"},
+]
+pyparsing = [
+ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
+ {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
+]
+pytest = [
+ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
+ {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+]
+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"},
+]
+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"},
+]
+requirements-detector = [
+ {file = "requirements-detector-0.7.tar.gz", hash = "sha256:0d1e13e61ed243f9c3c86e6cbb19980bcb3a0e0619cde2ec1f3af70fdbee6f7b"},
+]
+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.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
+ {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
+]
+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"},
+]
+typing-extensions = [
+ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
+ {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
+ {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
+]
+wrapt = [
+ {file = "wrapt-1.13.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3de7b4d3066cc610054e7aa2c005645e308df2f92be730aae3a47d42e910566a"},
+ {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8164069f775c698d15582bf6320a4f308c50d048c1c10cf7d7a341feaccf5df7"},
+ {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9adee1891253670575028279de8365c3a02d3489a74a66d774c321472939a0b1"},
+ {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a70d876c9aba12d3bd7f8f1b05b419322c6789beb717044eea2c8690d35cb91b"},
+ {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3f87042623530bcffea038f824b63084180513c21e2e977291a9a7e65a66f13b"},
+ {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e634136f700a21e1fcead0c137f433dde928979538c14907640607d43537d468"},
+ {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3e33c138d1e3620b1e0cc6fd21e46c266393ed5dae0d595b7ed5a6b73ed57aa0"},
+ {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:283e402e5357e104ac1e3fba5791220648e9af6fb14ad7d9cc059091af2b31d2"},
+ {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ccb34ce599cab7f36a4c90318697ead18312c67a9a76327b3f4f902af8f68ea1"},
+ {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:fbad5ba74c46517e6488149514b2e2348d40df88cd6b52a83855b7a8bf04723f"},
+ {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:724ed2bc9c91a2b9026e5adce310fa60c6e7c8760b03391445730b9789b9d108"},
+ {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83f2793ec6f3ef513ad8d5b9586f5ee6081cad132e6eae2ecb7eac1cc3decae0"},
+ {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0473d1558b93e314e84313cc611f6c86be779369f9d3734302bf185a4d2625b1"},
+ {file = "wrapt-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:15eee0e6fd07f48af2f66d0e6f2ff1916ffe9732d464d5e2390695296872cad9"},
+ {file = "wrapt-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bc85d17d90201afd88e3d25421da805e4e135012b5d1f149e4de2981394b2a52"},
+ {file = "wrapt-1.13.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6ee5f8734820c21b9b8bf705e99faba87f21566d20626568eeb0d62cbeaf23c"},
+ {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:53c6706a1bcfb6436f1625511b95b812798a6d2ccc51359cd791e33722b5ea32"},
+ {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fbe6aebc9559fed7ea27de51c2bf5c25ba2a4156cf0017556f72883f2496ee9a"},
+ {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:0582180566e7a13030f896c2f1ac6a56134ab5f3c3f4c5538086f758b1caf3f2"},
+ {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bff0a59387a0a2951cb869251257b6553663329a1b5525b5226cab8c88dcbe7e"},
+ {file = "wrapt-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:df3eae297a5f1594d1feb790338120f717dac1fa7d6feed7b411f87e0f2401c7"},
+ {file = "wrapt-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1eb657ed84f4d3e6ad648483c8a80a0cf0a78922ef94caa87d327e2e1ad49b48"},
+ {file = "wrapt-1.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0cdedf681db878416c05e1831ec69691b0e6577ac7dca9d4f815632e3549580"},
+ {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:87ee3c73bdfb4367b26c57259995935501829f00c7b3eed373e2ad19ec21e4e4"},
+ {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3e0d16eedc242d01a6f8cf0623e9cdc3b869329da3f97a15961d8864111d8cf0"},
+ {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8318088860968c07e741537030b1abdd8908ee2c71fbe4facdaade624a09e006"},
+ {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d90520616fce71c05dedeac3a0fe9991605f0acacd276e5f821842e454485a70"},
+ {file = "wrapt-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:22142afab65daffc95863d78effcbd31c19a8003eca73de59f321ee77f73cadb"},
+ {file = "wrapt-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d0d717e10f952df7ea41200c507cc7e24458f4c45b56c36ad418d2e79dacd1d4"},
+ {file = "wrapt-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:593cb049ce1c391e0288523b30426c4430b26e74c7e6f6e2844bd99ac7ecc831"},
+ {file = "wrapt-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8860c8011a6961a651b1b9f46fdbc589ab63b0a50d645f7d92659618a3655867"},
+ {file = "wrapt-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ada5e29e59e2feb710589ca1c79fd989b1dd94d27079dc1d199ec954a6ecc724"},
+ {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:fdede980273aeca591ad354608778365a3a310e0ecdd7a3587b38bc5be9b1808"},
+ {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:af9480de8e63c5f959a092047aaf3d7077422ded84695b3398f5d49254af3e90"},
+ {file = "wrapt-1.13.2-cp38-cp38-win32.whl", hash = "sha256:c65e623ea7556e39c4f0818200a046cbba7575a6b570ff36122c276fdd30ab0a"},
+ {file = "wrapt-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:b20703356cae1799080d0ad15085dc3213c1ac3f45e95afb9f12769b98231528"},
+ {file = "wrapt-1.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c5c4cf188b5643a97e87e2110bbd4f5bc491d54a5b90633837b34d5df6a03fe"},
+ {file = "wrapt-1.13.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:82223f72eba6f63eafca87a0f614495ae5aa0126fe54947e2b8c023969e9f2d7"},
+ {file = "wrapt-1.13.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:81a4cf257263b299263472d669692785f9c647e7dca01c18286b8f116dbf6b38"},
+ {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:728e2d9b7a99dd955d3426f237b940fc74017c4a39b125fec913f575619ddfe9"},
+ {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7574de567dcd4858a2ffdf403088d6df8738b0e1eabea220553abf7c9048f59e"},
+ {file = "wrapt-1.13.2-cp39-cp39-win32.whl", hash = "sha256:c7ac2c7a8e34bd06710605b21dd1f3576764443d68e069d2afba9b116014d072"},
+ {file = "wrapt-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e6d1a8eeef415d7fb29fe017de0e48f45e45efd2d1bfda28fc50b7b330859ef"},
+ {file = "wrapt-1.13.2.tar.gz", hash = "sha256:dca56cc5963a5fd7c2aa8607017753f534ee514e09103a6c55d2db70b50e7447"},
+]
diff --git a/prospector.yaml b/prospector.yaml
new file mode 100644
index 0000000..407555f
--- /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: 88
+ 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: 88
+ 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
index edcc417..c2626b3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,11 +5,23 @@ description = ""
authors = ["dtomlinson "]
[tool.poetry.dependencies]
-python = "^3.7"
+python = "^3.8"
+click = "^8.0.3"
+pendulum = "^2.1.2"
+Jinja2 = "^3.0.2"
+panaetius = { path = "../panaetius", develop = true }
+pytest-datadir = "^1.3.1"
[tool.poetry.dev-dependencies]
-pytest = "^5.2"
+pytest = "^6.2.5"
+prospector = { extras = ["with_bandit", "with_mypy"], version = "^1.5.1" }
+duty = "^0.7.0"
+pyinstaller = "^4.5.1"
+coverage = "^6.0.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
+
+[tool.poetry.scripts]
+"tembo" = "tembo.cli.cli:run"
diff --git a/tembo/__init__.py b/tembo/__init__.py
index b794fd4..d15ffc9 100644
--- a/tembo/__init__.py
+++ b/tembo/__init__.py
@@ -1 +1,4 @@
-__version__ = '0.1.0'
+from .journal.pages import ScopedPageCreator, PageCreatorOptions
+from . import exceptions
+
+__version__ = "0.1.0"
diff --git a/tembo/cli/__init__.py b/tembo/cli/__init__.py
new file mode 100644
index 0000000..f4076cb
--- /dev/null
+++ b/tembo/cli/__init__.py
@@ -0,0 +1,30 @@
+import os
+
+import panaetius
+from panaetius.exceptions import LoggingDirectoryDoesNotExistException
+
+__version__ = "0.1.0"
+
+if (config_path := os.environ.get("TEMBO_CONFIG")) is not None:
+ CONFIG = 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..e0df85b
--- /dev/null
+++ b/tembo/cli/cli.py
@@ -0,0 +1,198 @@
+from __future__ import annotations
+
+import pathlib
+from typing import Collection
+
+import click
+
+import tembo.cli
+from tembo.journal import pages
+from tembo.utils import Success
+from tembo import exceptions
+
+
+CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
+
+
+@click.group(context_settings=CONTEXT_SETTINGS, options_metavar="")
+@click.version_option(
+ tembo.__version__,
+ "-v",
+ "--version",
+ prog_name="Tembo",
+ message=f"Tembo v{tembo.__version__} 🐘",
+)
+def run():
+ """
+ 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)
+@click.option("--example", is_flag=True, default=False)
+def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool):
+ r"""
+ Create a new page.
+
+ \n
+ The name of the scope in the config.yml.
+
+ \n
+ Any input token values that are defined in the config.yml for this scope.
+ Accepts multiple inputs separated by a space.
+
+ 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} input tokens, you gave {mismatched_token_error.given}. Example: {config_scope["example"]}'
+ )
+ raise SystemExit(1) from mismatched_token_error
+ cli_message(
+ f"Your tembo config.yml/template specifies {mismatched_token_error.expected} 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:
+ click.echo(f"[TEMBO] {message} 🐘")
+
+
+run.add_command(new)
+run.add_command(list_all)
diff --git a/tembo/exceptions.py b/tembo/exceptions.py
new file mode 100644
index 0000000..df6a885
--- /dev/null
+++ b/tembo/exceptions.py
@@ -0,0 +1,36 @@
+"""Tembo exceptions."""
+
+
+class MismatchedTokenError(Exception):
+ def __init__(self, expected: int, given: int) -> None:
+ 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..2235318
--- /dev/null
+++ b/tembo/journal/__init__.py
@@ -0,0 +1 @@
+from tembo.journal import pages
diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py
new file mode 100644
index 0000000..4810cdf
--- /dev/null
+++ b/tembo/journal/pages.py
@@ -0,0 +1,280 @@
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from dataclasses import dataclass
+import pathlib
+import re
+from typing import Collection
+
+import pendulum
+import jinja2
+from jinja2.exceptions import TemplateNotFound
+
+import tembo
+from tembo import exceptions
+import tembo.utils
+
+
+# TODO: flesh this out with details for the optional args
+@dataclass
+class PageCreatorOptions:
+ """Options dataclass to create a Page.
+
+ Attributes:
+ base_path (str):
+ page_path (str):
+ filename (str):
+ extension (str):
+ name (str):
+ user_input (Collection[str] | None, optional):
+ example (str | None, optional):
+ template_filename (str | None, optional):
+ template_path (str | None, optional):
+ """
+
+ base_path: str
+ page_path: str
+ filename: str
+ extension: str
+ name: str
+ user_input: Collection[str] | None = None
+ example: str | None = None
+ template_filename: str | None = None
+ template_path: str | None = None
+
+
+class PageCreator:
+ @abstractmethod
+ def __init__(self, options: PageCreatorOptions) -> None:
+ raise NotImplementedError
+
+ @property
+ @abstractmethod
+ def options(self) -> PageCreatorOptions:
+ raise NotImplementedError
+
+ @abstractmethod
+ def create_page(self) -> Page:
+ raise NotImplementedError
+
+ def _check_base_path_exists(self) -> None:
+ 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:
+ 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:
+ 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:
+ self._all_input_tokens: list[str] = []
+ self._options = options
+
+ @property
+ def options(self) -> PageCreatorOptions:
+ return self._options
+
+ def create_page(self) -> Page:
+ self._check_base_path_exists()
+
+ self._all_input_tokens = self._get_input_tokens()
+ self._verify_input_tokens()
+
+ path = self._convert_base_path_to_path()
+ path = pathlib.Path(self._substitute_tokens(str(path)))
+
+ template_contents = self._load_template()
+ 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]:
+ 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(all_input_tokens)
+
+ def _verify_input_tokens(self) -> None:
+ 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),
+ )
+ return
+
+ 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:
+ 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."""
+
+ 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."""
+
+ # 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):
+ @abstractmethod
+ def __init__(self, path: pathlib.Path, page_content: str) -> None:
+ raise NotImplementedError
+
+ @property
+ @abstractmethod
+ def path(self) -> pathlib.Path:
+ raise NotImplementedError
+
+ @abstractmethod
+ def save_to_disk(self) -> tembo.utils.Success:
+ raise NotImplementedError
+
+
+class ScopedPage(Page):
+ """A page that uses substitute tokens.
+
+ Attributes:
+ path (pathlib.Path): a `Path` object of the page's filepath.
+ page_content (str): the content of the page from the template.
+ """
+
+ def __init__(self, path: pathlib.Path, page_content: str) -> None:
+ """Create a scoped page object.
+
+ Args:
+ path (pathlib.Path): a `pathlib.Path` object of the page's filepath.
+ page_content (str): the content of the page from the template.
+ """
+ self._path = path
+ self.page_content = page_content
+
+ def __str__(self) -> str:
+ return f"ScopedPage(\"{self.path}\")"
+
+ @property
+ def path(self) -> pathlib.Path:
+ return self._path
+
+ def save_to_disk(self) -> tembo.utils.Success:
+ """Save the scoped page to disk and write the `page_content`.
+
+ If the page already exists a message will be logged to stdout and no file
+ will be saved.
+
+ If `dry_run=True` a message will be logged to stdout and no file will be saved.
+
+ Args:
+ dry_run (bool, optional): If `True` will log the `path` to stdout and not
+ save the page to disk. Defaults to False.
+
+ Raises:
+ SystemExit: Exit code 0 if dry run is `True`, page is successfully saved
+ or if page already exists.
+ """
+ # TODO: move this functionality to the CLI so the page is created and the message
+ # returned to the user from the CLI.
+ # 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..2ab70fe
--- /dev/null
+++ b/tembo/utils/__init__.py
@@ -0,0 +1,12 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class Success:
+ """Success message.
+
+ Attributes:
+ message (str): A success message.
+ """
+
+ message: str
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..010e6cf
--- /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
+ 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
+ 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/old_test_pages.py b/tests/test_journal/old_test_pages.py
new file mode 100644
index 0000000..7805193
--- /dev/null
+++ b/tests/test_journal/old_test_pages.py
@@ -0,0 +1,126 @@
+import pathlib
+
+import pytest
+import jinja2
+
+from tembo.journal.pages import PageCreator, ScopedPageCreator
+
+
+def test_page_creator_convert_to_path_missing_base_path(caplog):
+ # arrange
+ base_path = "/some/nonexistent/path"
+ page_path = "some_page"
+ filename = "some_filename"
+ extension = "ex"
+
+ # act
+ with pytest.raises(SystemExit) as system_exit:
+ PageCreator._convert_base_path_to_path(
+ base_path=base_path,
+ page_path=page_path,
+ filename=filename,
+ extension=extension,
+ )
+
+ # assert
+ assert system_exit.value.code == 1
+ assert caplog.records[0].levelname == "CRITICAL"
+ assert (
+ caplog.records[0].message
+ == "Tembo base path of /some/nonexistent/path does not exist - exiting"
+ )
+
+
+@pytest.mark.parametrize(
+ "page_path,filename,extension",
+ [
+ ("some_pagepath", "some_filename", "ex"),
+ ("some pagepath", "some filename", "ex"),
+ ],
+)
+def test_page_creator_convert_to_path_full_path_to_file(
+ page_path, filename, extension, tmpdir
+):
+ # arrange
+ path_to_file = (
+ pathlib.Path(tmpdir)
+ / pathlib.Path(page_path)
+ / pathlib.Path(filename).with_suffix(f".{extension}")
+ )
+ base_path = tmpdir
+
+ # act
+ converted_path = PageCreator._convert_base_path_to_path(
+ base_path, page_path, filename, extension
+ )
+
+ # assert
+ assert str(path_to_file).replace(" ", "_") == str(converted_path)
+
+
+def test_page_creator_convert_to_path_full_path_no_file(tmpdir):
+ # arrange
+ full_path = pathlib.Path("/some/path")
+ base_path = ""
+ page_path = "/some/path"
+ filename = ""
+ extension = ""
+
+ # act
+ converted_path = PageCreator._convert_base_path_to_path(
+ base_path, page_path, filename, extension
+ )
+
+ # assert
+ assert str(full_path).replace(" ", "_") == str(converted_path)
+
+
+def test_page_creator_load_template_with_base_path_success(datadir):
+ # arrange
+ # default template_path would be datadir/.templates
+ base_path = str(datadir)
+ template_filename = "some_template.md.tpl"
+
+ # act
+ template_contents = ScopedPageCreator()._load_template(
+ base_path, template_filename, None
+ )
+
+ # assert
+ assert template_contents == "template contents"
+
+
+def test_page_creator_load_template_overriden_template_path_success(datadir):
+ # arrange
+ base_path = str(datadir)
+ template_filename = "some_template.md.tpl"
+ template_path = str(datadir / ".templates")
+
+ # act
+ # we explicitly pass in the template_path to override the default
+ template_contents = ScopedPageCreator()._load_template(
+ base_path, template_filename, template_path
+ )
+
+ # assert
+ assert template_contents == "template contents"
+
+
+def test_page_creator_load_template_missing_template_file(datadir, caplog):
+ # arrange
+ base_path = str(datadir)
+ template_filename = "some_nonexistent_template.md.tpl"
+ template_path = str(datadir / ".templates")
+
+ # act
+ with pytest.raises(SystemExit) as system_exit:
+ template_contents = ScopedPageCreator()._load_template(
+ base_path, template_filename, template_path
+ )
+
+ # assert
+ assert system_exit.value.code == 1
+ assert caplog.records[0].message == (
+ f"Template file {template_path}/some_nonexistent_template.md.tpl not found "
+ "- exiting"
+ )
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
diff --git a/tests/test_tembo.py b/tests/test_tembo.py
index e36a0f6..e69de29 100644
--- a/tests/test_tembo.py
+++ b/tests/test_tembo.py
@@ -1,5 +0,0 @@
-from tembo import __version__
-
-
-def test_version():
- assert __version__ == '0.1.0'