From 3a709547deebb6dbec99b91529c0cda8168b8ce6 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Wed, 20 Oct 2021 02:20:40 +0100 Subject: [PATCH 01/74] adding initial scaffolding --- .python-version | 1 + README.md | 2 + dev.todo | 2 + poetry.lock | 822 ++++++++++++++++++ prospector.yaml | 121 +++ pyproject.toml | 10 +- tembo/__init__.py | 13 +- tembo/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 372 bytes tembo/__pycache__/cli.cpython-38.pyc | Bin 0 -> 815 bytes tembo/cli.py | 27 + tests/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 151 bytes .../test_tembo.cpython-38-pytest-6.2.5.pyc | Bin 0 -> 153 bytes tests/test_tembo.py | 5 - 13 files changed, 995 insertions(+), 8 deletions(-) create mode 100644 .python-version create mode 100644 dev.todo create mode 100644 poetry.lock create mode 100644 prospector.yaml create mode 100644 tembo/__pycache__/__init__.cpython-38.pyc create mode 100644 tembo/__pycache__/cli.cpython-38.pyc create mode 100644 tembo/cli.py create mode 100644 tests/__pycache__/__init__.cpython-38.pyc create mode 100644 tests/__pycache__/test_tembo.cpython-38-pytest-6.2.5.pyc diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..89a1ad7 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.8.12 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/dev.todo b/dev.todo new file mode 100644 index 0000000..99c54ad --- /dev/null +++ b/dev.todo @@ -0,0 +1,2 @@ +Functionality: + ☐ Handle case where there are no scopes in the config and command is invoked. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..d6b1bbf --- /dev/null +++ b/poetry.lock @@ -0,0 +1,822 @@ +[[package]] +name = "astroid" +version = "2.8.3" +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 = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "bandit" +version = "1.7.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 = "dodgy" +version = "0.2.1" +description = "Dodgy: Searches for dodgy looking lines in Python code" +category = "dev" +optional = false +python-versions = "*" + +[[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 = "gitdb" +version = "4.0.7" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +smmap = ">=3.0.1,<5" + +[[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 = "dev" +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 = "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 = "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.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "panaetius" +version = "2.0.0" +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,<4.0" + +[package.dependencies] +PyYAML = ">=6.0,<7.0" +toml = ">=0.10.0,<0.11.0" + +[[package]] +name = "pbr" +version = "5.6.0" +description = "Python Build Reasonableness" +category = "dev" +optional = false +python-versions = ">=2.6" + +[[package]] +name = "pep8" +version = "1.7.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pep8-naming" +version = "0.10.0" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8-polyfill = ">=1.0.2,<2" + +[[package]] +name = "platformdirs" +version = "2.4.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prospector" +version = "1.5.1" +description = "" +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.dependencies] +bandit = {version = ">=1.5.1", optional = true, markers = "extra == \"with_bandit\" or extra == \"with_everything\""} +dodgy = ">=0.2.1,<0.3.0" +mccabe = ">=0.6.0,<0.7.0" +mypy = {version = ">=0.600", optional = true, markers = "extra == \"with_mypy\" or extra == \"with_everything\""} +pep8-naming = ">=0.3.3,<=0.10.0" +pycodestyle = ">=2.6.0,<2.9.0" +pydocstyle = ">=2.0.0" +pyflakes = ">=2.2.0,<2.4.0" +pylint = ">=2.8.3,<3" +pylint-celery = "0.3" +pylint-django = ">=2.4.4,<3.0.0" +pylint-flask = "0.6" +pylint-plugin-utils = ">=0.6,<0.7" +PyYAML = "*" +requirements-detector = ">=0.7,<0.8" +setoptconf-tmp = ">=0.3.1,<0.4.0" +toml = ">=0.10.2,<0.11.0" + +[package.extras] +with_bandit = ["bandit (>=1.5.1)"] +with_everything = ["bandit (>=1.5.1)", "frosted (>=1.4.1)", "mypy (>=0.600)", "pyroma (>=2.4)", "vulture (>=1.5)"] +with_frosted = ["frosted (>=1.4.1)"] +with_mypy = ["mypy (>=0.600)"] +with_pyroma = ["pyroma (>=2.4)"] +with_vulture = ["vulture (>=1.5)"] + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[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 = "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 = "dev" +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 = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "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 = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "smmap" +version = "4.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[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 = "e78ad97b70cef33d30b021e87c29165311c054f96276396093a29aaa7335df4d" + +[metadata.files] +astroid = [ + {file = "astroid-2.8.3-py3-none-any.whl", hash = "sha256:f9d66e3a4a0e5b52819b2ff41ac2b179df9d180697db71c92beb33a60c661794"}, + {file = "astroid-2.8.3.tar.gz", hash = "sha256:0e361da0744d5011d4f5d57e64473ba9b7ab4da1e2d45d6631ebd67dd28c3cce"}, +] +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"}, +] +dodgy = [ + {file = "dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6"}, + {file = "dodgy-0.2.1.tar.gz", hash = "sha256:28323cbfc9352139fdd3d316fa17f325cc0e9ac74438cbba51d70f9b48f86c3a"}, +] +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"}, +] +gitdb = [ + {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, + {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, +] +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"}, +] +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"}, +] +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.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, + {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, +] +panaetius = [ + {file = "panaetius-2.0.0-py3-none-any.whl", hash = "sha256:ccd3893c285b1ba4bf46cf24d45214a23d469e31b0f78962dba6fdd5c42a29dc"}, + {file = "panaetius-2.0.0.tar.gz", hash = "sha256:53fbf197ccad264838b0efa5528372be33ac8cc9e3923b51fd5c8c73c0de610c"}, +] +pbr = [ + {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, + {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, +] +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"}, +] +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"}, +] +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"}, +] +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-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, + {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, +] +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..b10f57b --- /dev/null +++ b/prospector.yaml @@ -0,0 +1,121 @@ +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 + # One-line docstring should fit on one line with quotes + - D200 + # 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..81bd1e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,17 @@ description = "" authors = ["dtomlinson "] [tool.poetry.dependencies] -python = "^3.7" +python = "^3.8" +panaetius = "^2.0" +click = "^8.0.3" [tool.poetry.dev-dependencies] -pytest = "^5.2" +pytest = "^6.2.5" +prospector = {extras = ["with_bandit", "with_mypy"], version = "^1.5.1"} [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +"tembo" = "tembo.cli:run" diff --git a/tembo/__init__.py b/tembo/__init__.py index b794fd4..c76f4a2 100644 --- a/tembo/__init__.py +++ b/tembo/__init__.py @@ -1 +1,12 @@ -__version__ = '0.1.0' +import os + +import panaetius + +if config_path := os.environ.get("TEMBO_CONFIG") is not None: + CONFIG = panaetius.Config("tembo", config_path) +else: + CONFIG = panaetius.Config("tembo") + + +panaetius.set_config(CONFIG, "base_path", "~/tembo") +panaetius.set_config(CONFIG, "scopes", {}) diff --git a/tembo/__pycache__/__init__.cpython-38.pyc b/tembo/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f16c65d7f3770ff304a436cade97aecb157843b4 GIT binary patch literal 372 zcmYk2y-ou$49Da9$bHauV&nxnz)8#%LR5elXb0#LA);$zO8SwUE-4ZWJWFrnm9jGN z3QVNE4p{Qv{#kZndD89X#M|>ymnT5T_YVGB?BUY8dGaENfE8hF1O^C;U>k~{0RhV_ zA}p%Uk!)iTV$=kTs=TEJ81-3#R74_fLLdjClI%#p1L-F}{s%H*oyY6@t1%yrM>lu3 zF1EOyYL`t*gS;y3>qqigv^8pGx4II0FnEf?oS@@`!;<9P z3^@o=1w7#4C*To}KaxAhLlIU{wYc!q1! z!K|Q+N^#?r7-fF{Y;yYQIn`p0-BGKky%JQiT2M9jYhNiXr4KrN3nk~}Vsu+CJ2BVy z8)l5)4&~ub=z6~c{f*nmah{{>I>g?z`&ez_!A85*hk@?nlxHiHdFB)U)z>`-53%wC zTp0gn(*f+m0~mlFxZ}M%M!}0~Pv6E1tyYcKI`0*#u9@U{%JgDY3u*m{9d`f9{)+Ud WR(!P-$L?i}BTj=f%pglMNZ}6@`@kCj literal 0 HcmV?d00001 diff --git a/tembo/cli.py b/tembo/cli.py new file mode 100644 index 0000000..46aa3cc --- /dev/null +++ b/tembo/cli.py @@ -0,0 +1,27 @@ +import click + +import tembo + + +@click.group(options_metavar="") +def run(): + """ + Tembo - an organiser for work notes. + """ + print(tembo.CONFIG.base_path) + # print(tembo.CONFIG.scopes) + + +@run.command(options_metavar="") +@click.argument("scope") +def new(scope): + """ + Create a new note. + + SCOPE refers to the name of the scope in the Tembo config.yml. + """ + for user_scope in tembo.CONFIG.scopes: + print(f"passed in scope: {scope}") + print(f'config scope: {user_scope["name"]}') + if user_scope["name"] == scope: + print(True) diff --git a/tests/__pycache__/__init__.cpython-38.pyc b/tests/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..317d9d6aaf71c8b397ae2f32e1fe62869844b8e8 GIT binary patch literal 151 zcmWIL<>g`kg8%yYi6Hthh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6v8KeRZts8~Ox zBtJJNGp{&5Pd`1gM7JolAir3@peR2pHMyi%za%v`DIZ7{1KIKMnR%Hd@$q^EmA5!- Ra`RJ4b5iXV1 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_tembo.cpython-38-pytest-6.2.5.pyc b/tests/__pycache__/test_tembo.cpython-38-pytest-6.2.5.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6bfba605c4f36b2dac1d7e1d38aa0f7acf251d64 GIT binary patch literal 153 zcmWIL<>g`kg6{16L=gQLL?8o3AjbiSi&=m~3PUi1CZpdUy_=eln*3}!R+Fac(8z8LFFwDo80`A O(wtN~kh!0Mm;nH>btP8- literal 0 HcmV?d00001 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' From aa2545aa5a737debcb8e9d2a1bb2cd9c2ab62b6d Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Wed, 20 Oct 2021 03:23:21 +0100 Subject: [PATCH 02/74] adding python .gitignore --- .gitignore | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fdf691 --- /dev/null +++ b/.gitignore @@ -0,0 +1,140 @@ +# 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/ + + From 066d715c8540720a4ad278bba22b1ffcee078dfb Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Wed, 20 Oct 2021 03:24:00 +0100 Subject: [PATCH 03/74] updating prospector.yaml --- prospector.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prospector.yaml b/prospector.yaml index b10f57b..465e96e 100644 --- a/prospector.yaml +++ b/prospector.yaml @@ -80,6 +80,8 @@ pep257: - D103 # 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 From 03d9d563b4ee681b0ad8d49b28ab120202155f11 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Wed, 20 Oct 2021 03:24:09 +0100 Subject: [PATCH 04/74] adding logging --- tembo/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tembo/__init__.py b/tembo/__init__.py index c76f4a2..0425bbc 100644 --- a/tembo/__init__.py +++ b/tembo/__init__.py @@ -10,3 +10,9 @@ else: panaetius.set_config(CONFIG, "base_path", "~/tembo") panaetius.set_config(CONFIG, "scopes", {}) +panaetius.set_config(CONFIG, "logging.level", "DEBUG") +panaetius.set_config(CONFIG, "logging.path") + +logger = panaetius.set_logger( + CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level) +) From 51473e11d21e80b170def2867919b03ed3425085 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Wed, 20 Oct 2021 03:24:15 +0100 Subject: [PATCH 05/74] fleshing out initial cli --- tembo/cli.py | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/tembo/cli.py b/tembo/cli.py index 46aa3cc..11e55ae 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -2,8 +2,10 @@ import click import tembo +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) -@click.group(options_metavar="") + +@click.group(context_settings=CONTEXT_SETTINGS, options_metavar="") def run(): """ Tembo - an organiser for work notes. @@ -12,16 +14,40 @@ def run(): # print(tembo.CONFIG.scopes) -@run.command(options_metavar="") -@click.argument("scope") -def new(scope): +@click.command(options_metavar="") +@click.argument("scope", metavar="") +@click.argument( + "inputs", + nargs=-1, + metavar="", +) +def new(scope, inputs): """ Create a new note. - SCOPE refers to the name of the scope in the Tembo config.yml. + The name of the scope in the Tembo config.yml. + + Any input tokens needed in the Tembo config.yml. + + Example: tembo new meeting my_presentation """ + for user_scope in tembo.CONFIG.scopes: print(f"passed in scope: {scope}") print(f'config scope: {user_scope["name"]}') if user_scope["name"] == scope: print(True) + + click.echo(inputs) + + +# TODO: decide on a date format to pass in +@click.command(options_metavar="") +@click.option("--scope", "-s", help="The name of the scope in the Tembo config.yml.") +@click.option("--date", "-d", help="") +def archive(scope, date): + print(f"{scope}, {date}") + + +run.add_command(new) +run.add_command(archive) From 5474c3c27343f8ef076fe4daa6fa9c0f0f104611 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Thu, 21 Oct 2021 00:22:20 +0100 Subject: [PATCH 06/74] adding initial Page scaffolding --- .DS_Store | Bin 6148 -> 0 bytes .vscode/settings.json | 18 ++++++++ dev.todo | 3 ++ poetry.lock | 6 +-- tembo/__pycache__/__init__.cpython-38.pyc | Bin 372 -> 524 bytes tembo/__pycache__/cli.cpython-38.pyc | Bin 815 -> 1445 bytes tembo/journal/__init__.py | 1 + tembo/journal/pages.py | 51 ++++++++++++++++++++++ 8 files changed, 76 insertions(+), 3 deletions(-) delete mode 100644 .DS_Store create mode 100644 .vscode/settings.json create mode 100644 tembo/journal/__init__.py create mode 100644 tembo/journal/pages.py diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index a119bd2ba36eec67533f2655c2d16b0bdd6d92c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK&2G~`5T0$*VuOGjkje$g7j99Mv<0b$N+!669uNp4H~?zxIBJY+uN6B)B?S52 z16R243LJO@E=W88j|1QS6vaskH$Z47n*G+hGqc`ry*pVVV!d(HC2A6p1r^x1h~f+p z{X&;yMYmi6G8v{U%h4< zPKsRb^)F$b#6{WZd}AxCjkWa)mTg=1W&625Si;?o^w>J6k*WhD$-Gkj&@K{6S0qjEJ3zjEmS{CPgNgw9EBsd!Rs|@Hp zV2Lxph+)7mU>KNZK%5T_RG_P|QYeoO6!HoHY@k^R>LTR`A4j9Bu~LX02ve>=<;wIG zgDH2kJKE0GSSeKQ#PsEZ>5-Yfp)ff*;ycoun5)o~h5^Gsoq<*L*cADH@BI6J-N`%| z1`GrL6$7l{`CboG(z$hMaw69{&_k#YVOJ@XE+}+5mKBkTuS2Dv&k+@%tFcmu7Kr&H NAZaj#Vc?H4@B?<7vNiw! diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..80eea8e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "workbench.colorCustomizations": { + "editorGroup.border": "#d68b62", + "panel.border": "#d68b62", + "sash.hoverBorder": "#d68b62", + "sideBar.border": "#d68b62", + "statusBar.background": "#cc6d39", + "statusBar.foreground": "#15202b", + "statusBarItem.hoverBackground": "#a7572b", + "statusBarItem.remoteBackground": "#cc6d39", + "statusBarItem.remoteForeground": "#15202b", + "titleBar.activeBackground": "#cc6d39", + "titleBar.activeForeground": "#15202b", + "titleBar.inactiveBackground": "#cc6d3999", + "titleBar.inactiveForeground": "#15202b99" + }, + "peacock.color": "#CC6D39" +} diff --git a/dev.todo b/dev.todo index 99c54ad..35b7cac 100644 --- a/dev.todo +++ b/dev.todo @@ -1,2 +1,5 @@ Functionality: ☐ Handle case where there are no scopes in the config and command is invoked. + +Documentation: + ☐ Document usage of Panaetius in a module? diff --git a/poetry.lock b/poetry.lock index d6b1bbf..cd73ef9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -198,7 +198,7 @@ pyparsing = ">=2.0.2" [[package]] name = "panaetius" -version = "2.0.0" +version = "2.1.0" 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 @@ -638,8 +638,8 @@ packaging = [ {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] panaetius = [ - {file = "panaetius-2.0.0-py3-none-any.whl", hash = "sha256:ccd3893c285b1ba4bf46cf24d45214a23d469e31b0f78962dba6fdd5c42a29dc"}, - {file = "panaetius-2.0.0.tar.gz", hash = "sha256:53fbf197ccad264838b0efa5528372be33ac8cc9e3923b51fd5c8c73c0de610c"}, + {file = "panaetius-2.1.0-py3-none-any.whl", hash = "sha256:ab524a2a2aac650d684ffbf601659f463d1cf8d2ca26e31df4a294ed9a22e03d"}, + {file = "panaetius-2.1.0.tar.gz", hash = "sha256:0ac2efd70b055c71ad200427d19f6d4297242f321a5f50e32cc50e93ddfbffeb"}, ] pbr = [ {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, diff --git a/tembo/__pycache__/__init__.cpython-38.pyc b/tembo/__pycache__/__init__.cpython-38.pyc index f16c65d7f3770ff304a436cade97aecb157843b4..f08dc10e85b14fb71ba90e073c44c1de0aa8a00c 100644 GIT binary patch literal 524 zcmYk3y-ve05P)qbb$*&MG4Ki5pi4F)*K~I+&yyk|H&aEAJ(4VvOpkDnPG# zxymvjvx6L#kh9MD@N|5+ivFYPqCT$wvyvvOAJs%@78I#W!H9*fJ&;RLDp@-j7}lLC z`5>~UBco>xSlhlS)L@b}fi?cR6_hL*iFuL3bwj0Nr(v```aq>;u~9#Rc&wq+F*jFE$;)@)P?e8MJXO&NUvWs^}=WPX6)E{(f$r?zbjhl#;l`R-Bw)kXo$C5ye@Mn3tGZ zl37}Oiz$Dy1*44WEjH);ytK^pC^m?WD6ZnvlK5n>*h+>XPN11ZoRb$Y1_FyUA{`+w9K9#_zL_*vIUy)z~NO)Av3x z58mwX=qHSklVU3|e1?+G-a_7J+K|zr2}h#^U)2dt_w75AwJD{@L|$>-^p84KA15T; zE2J>|xydv)rWAAC-q?Q4DsJ+pxhibf>x+PL0$cCFVUWQ~vh-fQOM^7yn8KRz>WQQy znhPq`JQtjOHY`Q)v6R)ipjh*BgJc}C$*AGUKl-bp|)qSD#!I@>#4PfY~r>86sSI& z3078X16|0PH@zeGK2C7k+t%NPrKs0Nzx@kV6CHILVAlpBS97g)z`t-IL9ok%9l&-V zeG7s?O91Hs`u_e4VmyYg&};M~v5FVbq#QdRh(OD(dsg*71vyt+|toy+U(c4)?HMW z-63s;T~1xLhqU!=3A1SuDwtcOEVOOtjJ2{@w*fS`xJZ3oEM(#-JLhTuM;fnzDVa0< zhd|15p!EQx7w+7BsW z>0*m<6uAR}L6#Ii4Gh%irU^l#3C#073M934Cd8H2`Znml>*>LyF3P8T1cn{KePshD z17*u}(1B-Jm~;>3Yj*nGEIT`$%^po3p0&YT$#v}pBJCBjs&c{72-v-@pj!@+H0=Ht zT+nnUXDr(qUdzM{se?B`o-^^R(Js2I&&4obnk!H;k2JmfzS@9^?A~pN;NRZfu zcxo|51u2tE@_Uq?^zUGdF;_hGra1*Kz6@SbmZY#4p@GB(MCBjq1qXxjMkVXzS6SE8 zBaDj(AA^7Z9TcCqJ+iJLN&vnV^iF(}WIdIjP5p_5ebOCL$1sN!Q??7I>$jM5i9>jT zXEsIzir5e(U;fWbqan1I7KD1X>RL#j7zTs$;)|^;Hg*n=_G(&PC~azaR?CaJw^}^# zwUMVe&D8chlfV%(+Omn5E|ZDFzES$j0eZTWEic(}-c>gXgMjtOnW!T%@7-%O!nKVZm(r*XTbyy JF%%&R`~_2!Vq*XR 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..d2702d2 --- /dev/null +++ b/tembo/journal/pages.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod + + +class PageCreator: + @abstractmethod + def create_page(self, path, user_input, raw_entry_input) -> Page: + pass + + +class ScopedPageCreator(PageCreator): + def create_page( + self, path: str, user_input, raw_entry_input: dict | None = None + ) -> Page: + pass + + +class Page(metaclass=ABCMeta): + @abstractmethod + def __init__(self) -> None: + pass + + @abstractmethod + def save_to_disk(self) -> None: + pass + + +class ScopedPage(Page): + """A Page that uses substitute tokens.""" + + def __init__(self, path: str, raw_entry_input: dict): + self.path = path + self._raw_entry_input = raw_entry_input + self._squashed_content: dict = {} + self._page_content: str = "" + + def save_to_disk(self) -> None: + # create the file/folder if it doesnt exist + pass + + def _convert_to_path(self): + # take a path str and convert to pathlib + # substitute the tokens in + pass + + def squash_raw_entry_input(self) -> None: + pass + + def substitute_tokens(self) -> None: + pass From 77e070bf697189cdc59efa9dc48c5bf40dc9e167 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 22 Oct 2021 05:21:05 +0100 Subject: [PATCH 07/74] adding base path exception --- tembo/exceptions.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tembo/exceptions.py diff --git a/tembo/exceptions.py b/tembo/exceptions.py new file mode 100644 index 0000000..07776a4 --- /dev/null +++ b/tembo/exceptions.py @@ -0,0 +1,5 @@ +"""Tembo exceptions.""" + + +class BasePathDoesNotExistError(Exception): + pass From 34e6cb4136712df8f746eea346cafe1984b676c1 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 22 Oct 2021 05:21:39 +0100 Subject: [PATCH 08/74] adding latest --- tembo/journal/pages.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index d2702d2..9439b8e 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -1,19 +1,37 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod +import pathlib + +from tembo.exceptions import BasePathDoesNotExistError # noqa +from tembo import logger class PageCreator: @abstractmethod - def create_page(self, path, user_input, raw_entry_input) -> Page: + def create_page(self, base_path: str, page_path: str, user_input: str) -> Page: pass + @staticmethod + def _convert_to_path(base_path: str, page_path: str) -> pathlib.Path: + if not pathlib.Path(base_path).expanduser().exists(): + logger.debug("base path of %s does not exist", base_path, exec_info=1) + raise BasePathDoesNotExistError( + f"Your base path of {base_path} does not exist." + ) + return pathlib.Path(base_path).expanduser() / pathlib.Path(page_path) + class ScopedPageCreator(PageCreator): + def __init__(self, raw_entry_input: str) -> None: + self.raw_entry_input = raw_entry_input + def create_page( - self, path: str, user_input, raw_entry_input: dict | None = None + self, base_path: str, page_path: str, user_input: str | None ) -> Page: - pass + path = self._convert_to_path(base_path, page_path) + # substitute tokens in path + # substitute tokens in raw_entry_input class Page(metaclass=ABCMeta): From d9f7125f780a988a4aa0b9daec396f4878d64894 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 22 Oct 2021 05:30:14 +0100 Subject: [PATCH 09/74] updating .gitignore --- .gitignore | 2 +- .vscode/settings.json | 18 ------------------ tembo/__pycache__/__init__.cpython-38.pyc | Bin 524 -> 0 bytes tembo/__pycache__/cli.cpython-38.pyc | Bin 1445 -> 0 bytes 4 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 tembo/__pycache__/__init__.cpython-38.pyc delete mode 100644 tembo/__pycache__/cli.cpython-38.pyc diff --git a/.gitignore b/.gitignore index 4fdf691..2dc658c 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,4 @@ dmypy.json # Cython debug symbols cython_debug/ - +.vscode/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 80eea8e..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "workbench.colorCustomizations": { - "editorGroup.border": "#d68b62", - "panel.border": "#d68b62", - "sash.hoverBorder": "#d68b62", - "sideBar.border": "#d68b62", - "statusBar.background": "#cc6d39", - "statusBar.foreground": "#15202b", - "statusBarItem.hoverBackground": "#a7572b", - "statusBarItem.remoteBackground": "#cc6d39", - "statusBarItem.remoteForeground": "#15202b", - "titleBar.activeBackground": "#cc6d39", - "titleBar.activeForeground": "#15202b", - "titleBar.inactiveBackground": "#cc6d3999", - "titleBar.inactiveForeground": "#15202b99" - }, - "peacock.color": "#CC6D39" -} diff --git a/tembo/__pycache__/__init__.cpython-38.pyc b/tembo/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index f08dc10e85b14fb71ba90e073c44c1de0aa8a00c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 524 zcmYk3y-ve05P)qbb$*&MG4Ki5pi4F)*K~I+&yyk|H&aEAJ(4VvOpkDnPG# zxymvjvx6L#kh9MD@N|5+ivFYPqCT$wvyvvOAJs%@78I#W!H9*fJ&;RLDp@-j7}lLC z`5>~UBco>xSlhlS)L@b}fi?cR6_hL*iFuL3bwj0Nr(v```aq>;u~9#Rc&wq+F*jFE$;)@)P?e8MJXO&NUvWs^}=FyUA{`+w9K9#_zL_*vIUy)z~NO)Av3x z58mwX=qHSklVU3|e1?+G-a_7J+K|zr2}h#^U)2dt_w75AwJD{@L|$>-^p84KA15T; zE2J>|xydv)rWAAC-q?Q4DsJ+pxhibf>x+PL0$cCFVUWQ~vh-fQOM^7yn8KRz>WQQy znhPq`JQtjOHY`Q)v6R)ipjh*BgJc}C$*AGUKl-bp|)qSD#!I@>#4PfY~r>86sSI& z3078X16|0PH@zeGK2C7k+t%NPrKs0Nzx@kV6CHILVAlpBS97g)z`t-IL9ok%9l&-V zeG7s?O91Hs`u_e4VmyYg&};M~v5FVbq#QdRh(OD(dsg*71vyt+|toy+U(c4)?HMW z-63s;T~1xLhqU!=3A1SuDwtcOEVOOtjJ2{@w*fS`xJZ3oEM(#-JLhTuM;fnzDVa0< zhd|15p!EQx7w+7BsW z>0*m<6uAR}L6#Ii4Gh%irU^l#3C#073M934Cd8H2`Znml>*>LyF3P8T1cn{KePshD z17*u}(1B-Jm~;>3Yj*nGEIT`$%^po3p0&YT$#v}pBJCBjs&c{72-v-@pj!@+H0=Ht zT+nnUXDr(qUdzM{se?B Date: Fri, 22 Oct 2021 23:28:05 +0100 Subject: [PATCH 10/74] adding jinja2 to requirements --- .python-version => .pythonversion | 0 TODO.todo | 11 ++ dev.todo | 5 - poetry.lock | 169 +++++++++++++++++++++++++++++- pyproject.toml | 2 + 5 files changed, 177 insertions(+), 10 deletions(-) rename .python-version => .pythonversion (100%) create mode 100644 TODO.todo delete mode 100644 dev.todo diff --git a/.python-version b/.pythonversion similarity index 100% rename from .python-version rename to .pythonversion diff --git a/TODO.todo b/TODO.todo new file mode 100644 index 0000000..eb0e90a --- /dev/null +++ b/TODO.todo @@ -0,0 +1,11 @@ +Functionality: + ☐ Handle case where there are no scopes in the config and command is invoked. + ☐ Have an `--example` flag to `new` that prints an example given in the `config.yml` + ☐ Should be a `tembo new --list` to list all possible names. + +Logging: + ☐ How to raise + debug an exception? + +Documentation: + ☐ Document usage of Panaetius in a module? + ☐ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens diff --git a/dev.todo b/dev.todo deleted file mode 100644 index 35b7cac..0000000 --- a/dev.todo +++ /dev/null @@ -1,5 +0,0 @@ -Functionality: - ☐ Handle case where there are no scopes in the config and command is invoked. - -Documentation: - ☐ Document usage of Panaetius in a module? diff --git a/poetry.lock b/poetry.lock index cd73ef9..769531a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -144,6 +144,20 @@ 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" @@ -152,6 +166,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +[[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" @@ -198,7 +220,7 @@ pyparsing = ">=2.0.2" [[package]] name = "panaetius" -version = "2.1.0" +version = "2.2.0" 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 @@ -216,6 +238,18 @@ category = "dev" optional = false python-versions = ">=2.6" +[[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" @@ -429,6 +463,25 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[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 = "pyyaml" version = "6.0" @@ -463,7 +516,7 @@ yaml = ["pyyaml"] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" @@ -521,7 +574,7 @@ 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 = "e78ad97b70cef33d30b021e87c29165311c054f96276396093a29aaa7335df4d" +content-hash = "3811535320f246d17fe1c47b4a749724381fb56d5baea0e507008712f955c1b3" [metadata.files] astroid = [ @@ -576,6 +629,10 @@ 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"}, @@ -600,6 +657,77 @@ lazy-object-proxy = [ {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"}, ] +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"}, @@ -638,13 +766,36 @@ packaging = [ {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] panaetius = [ - {file = "panaetius-2.1.0-py3-none-any.whl", hash = "sha256:ab524a2a2aac650d684ffbf601659f463d1cf8d2ca26e31df4a294ed9a22e03d"}, - {file = "panaetius-2.1.0.tar.gz", hash = "sha256:0ac2efd70b055c71ad200427d19f6d4297242f321a5f50e32cc50e93ddfbffeb"}, + {file = "panaetius-2.2.0-py3-none-any.whl", hash = "sha256:55baae01ce7c1fbbea7a9d667585907e5881c859b64bbbcd747db208bcc777d8"}, + {file = "panaetius-2.2.0.tar.gz", hash = "sha256:a144debf21a13b23213f138782fd27267cb8b335a2243f86790b1bd5dcbc6da3"}, ] pbr = [ {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, {file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"}, ] +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"}, @@ -707,6 +858,14 @@ pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] +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"}, +] 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"}, diff --git a/pyproject.toml b/pyproject.toml index 81bd1e7..13d7d30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,8 @@ authors = ["dtomlinson "] python = "^3.8" panaetius = "^2.0" click = "^8.0.3" +pendulum = "^2.1.2" +Jinja2 = "^3.0.2" [tool.poetry.dev-dependencies] pytest = "^6.2.5" From 903755dd1e836cd8c40c13767efbf681ee284b43 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 22 Oct 2021 23:28:14 +0100 Subject: [PATCH 11/74] adding latest pages --- tembo/cli.py | 21 ++++-- tembo/exceptions.py | 4 + tembo/journal/pages.py | 167 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 166 insertions(+), 26 deletions(-) diff --git a/tembo/cli.py b/tembo/cli.py index 11e55ae..701ae72 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -32,13 +32,22 @@ def new(scope, inputs): Example: tembo new meeting my_presentation """ - for user_scope in tembo.CONFIG.scopes: - print(f"passed in scope: {scope}") - print(f'config scope: {user_scope["name"]}') - if user_scope["name"] == scope: - print(True) + # for user_scope in tembo.CONFIG.scopes: + # print(f"passed in scope: {scope}") + # print(f'config scope: {user_scope["name"]}') + # if user_scope["name"] == scope: + # print(True) - click.echo(inputs) + # TODO: write check for if titles is missing + from panaetius.utilities import Squash + + for user_scope in tembo.CONFIG.scopes: + print(Squash({"titles": user_scope["titles"]}).as_dict) + + # click.echo(inputs) + # click.echo(type(inputs)) + + # if len(inputs) == 0, pass None is as user_input # TODO: decide on a date format to pass in diff --git a/tembo/exceptions.py b/tembo/exceptions.py index 07776a4..185cb68 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -3,3 +3,7 @@ class BasePathDoesNotExistError(Exception): pass + + +class MismatchingNumberOfInputTokensError(Exception): + pass diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 9439b8e..8a796a1 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -2,24 +2,44 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod import pathlib +import re -from tembo.exceptions import BasePathDoesNotExistError # noqa +import pendulum + +from tembo.exceptions import ( + BasePathDoesNotExistError, + MismatchingNumberOfInputTokensError, +) # noqa from tembo import logger class PageCreator: @abstractmethod - def create_page(self, base_path: str, page_path: str, user_input: str) -> Page: + def create_page( + self, + base_path: str, + page_path: str, + filename: str, + extension: str, + name: str, + user_input: tuple[str, ...], + ) -> Page: pass @staticmethod - def _convert_to_path(base_path: str, page_path: str) -> pathlib.Path: + def _convert_to_path( + base_path: str, page_path: str, filename: str, extension: str + ) -> pathlib.Path: if not pathlib.Path(base_path).expanduser().exists(): - logger.debug("base path of %s does not exist", base_path, exec_info=1) + logger.debug("base path of %s does not exist", base_path) raise BasePathDoesNotExistError( f"Your base path of {base_path} does not exist." ) - return pathlib.Path(base_path).expanduser() / pathlib.Path(page_path) + path_to_file = ( + pathlib.Path(base_path).expanduser() / pathlib.Path(page_path) / filename + ) + extension = extension[1:] if extension[0] == "." else extension + return path_to_file.with_suffix(f".{extension}") class ScopedPageCreator(PageCreator): @@ -27,16 +47,84 @@ class ScopedPageCreator(PageCreator): self.raw_entry_input = raw_entry_input def create_page( - self, base_path: str, page_path: str, user_input: str | None + self, + base_path: str, + page_path: str, + filename: str, + extension: str, + name: str, + user_input: tuple[str, ...] | None, ) -> Page: - path = self._convert_to_path(base_path, page_path) + path = self._convert_to_path(base_path, page_path, filename, extension) + path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) + return path + # return ScopedPage(path, "") # substitute tokens in path # substitute tokens in raw_entry_input + def _substitute_tokens( + self, tokenified_string: str, user_input: tuple[str, ...] | None, name: str + ) -> str: + tokenified_string = self.__substitute_input_tokens( + tokenified_string, user_input + ) + tokenified_string = self.__substitute_name_tokens(tokenified_string, name) + tokenified_string = self.__substitute_date_tokens(tokenified_string) + return tokenified_string + + def __substitute_name_tokens( # noqa + self, tokenified_string: str, name: str + ) -> str: + # find any {name} tokens and substitute for the name value + name_extraction = re.findall(r"(\{name\d*\})", tokenified_string) + for extracted_input in name_extraction: + tokenified_string = tokenified_string.replace(extracted_input, name) + return tokenified_string + + def __substitute_input_tokens( # noqa + self, tokenified_string: str, user_input: tuple[str, ...] | None + ) -> str: + # find {inputN} tokens in string + input_extraction = re.findall(r"(\{input\d*\})", tokenified_string) + if user_input is None: + # if there's no user input, but the regex matches, raise error + if len(input_extraction) > 0: + raise MismatchingNumberOfInputTokensError( + f"Your config specifies {len(input_extraction)} input tokens, " + f"you gave 0", + ) + # if there aren't any tokens in the string, return it + return tokenified_string + # if there is user input, check the number of tokens matches what's passed in + if len(input_extraction) != len(user_input): + logger.debug("MismatchingNumberOfInputTokensError") + raise MismatchingNumberOfInputTokensError( + f"Your config specifies {len(input_extraction)} input tokens, " + f"you gave {len(user_input)}", + ) + # substitute each token with the user's input + for extracted_input, input_value in zip(input_extraction, user_input): + tokenified_string = tokenified_string.replace(extracted_input, input_value) + return tokenified_string + + def __substitute_date_tokens(self, tokenified_string: str) -> str: # noqa + # find any {d:DD-MM-YYYY} tokens + date_extraction_token = re.findall(r"(\{d\:[^}]*\})", tokenified_string) + for extracted_token in date_extraction_token: + # extract the inner DD-MM-YYYY only + strftime_value = re.match(r"\{d\:([^\}]*)\}", extracted_token) + if strftime_value: + strftime_value = strftime_value.group(1) + # replace {d:DD-MM-YYYY} with todays format of DD-MM-YYYY + tokenified_string = tokenified_string.replace( + extracted_token, pendulum.now().format(strftime_value) + ) + return tokenified_string + class Page(metaclass=ABCMeta): @abstractmethod - def __init__(self) -> None: + def __init__(self, path: pathlib.Path, page_content: str) -> None: pass @abstractmethod @@ -47,23 +135,62 @@ class Page(metaclass=ABCMeta): class ScopedPage(Page): """A Page that uses substitute tokens.""" - def __init__(self, path: str, raw_entry_input: dict): + def __init__(self, path: pathlib.Path, page_content: str): self.path = path - self._raw_entry_input = raw_entry_input - self._squashed_content: dict = {} - self._page_content: str = "" + self.page_content = page_content def save_to_disk(self) -> None: # create the file/folder if it doesnt exist pass - def _convert_to_path(self): - # take a path str and convert to pathlib - # substitute the tokens in - pass - def squash_raw_entry_input(self) -> None: - pass +if __name__ == "__main__": + c = ScopedPageCreator("") + # # raises error + # # print(c._substitute_tokens("scratchpad/{input0}-{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file.md", None)) + # print( + # c._substitute_tokens( + # "scratchpad/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file-{input0}.md", ("last",) + # ) + # ) - def substitute_tokens(self) -> None: - pass + print( + c.create_page( + "~/tembo", + "{name}", + "{input0}-{input1}-file", + "md", + "scratchpad", + ("first", "second"), + ) + ) + print( + c.create_page( + "~/tembo", + "{name}/{d:MMMM-YY}", + "{input0}-{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file", + "md", + "scratchpad", + ("first",), + ) + ) + print( + c.create_page( + "~/tembo", + "{name}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", + "file", + "md", + "scratchpad", + None, + ) + ) + print( + c.create_page( + "~/tembo", + "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", + "file-{input0}-{name}", + ".md", + "meeting", + ("last",), + ) + ) From 36d489e7289c3b99b1ad2bacc91c0a8e6e5a4dde Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 23 Oct 2021 01:47:51 +0100 Subject: [PATCH 12/74] updating TODO --- TODO.todo | 4 + tembo/__init__.py | 1 + tembo/cli.py | 6 +- tembo/journal/pages.py | 198 ++++++++++++++++++++++++++++------------- 4 files changed, 144 insertions(+), 65 deletions(-) diff --git a/TODO.todo b/TODO.todo index eb0e90a..4b64cd6 100644 --- a/TODO.todo +++ b/TODO.todo @@ -9,3 +9,7 @@ Logging: Documentation: ☐ Document usage of Panaetius in a module? ☐ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens + ☐ Document latest typing. + ☐ Using from `__future__` with `|` + ☐ `using Tuple[str, ...]` + ☐ `Sequence` vs `Collection` diff --git a/tembo/__init__.py b/tembo/__init__.py index 0425bbc..5e73856 100644 --- a/tembo/__init__.py +++ b/tembo/__init__.py @@ -9,6 +9,7 @@ else: 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") diff --git a/tembo/cli.py b/tembo/cli.py index 701ae72..4053e4c 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -38,11 +38,7 @@ def new(scope, inputs): # if user_scope["name"] == scope: # print(True) - # TODO: write check for if titles is missing - from panaetius.utilities import Squash - - for user_scope in tembo.CONFIG.scopes: - print(Squash({"titles": user_scope["titles"]}).as_dict) + # TODO: if user_scope["template"], check for a template in the templates dir. If it doesn't exist raise MissingTemplateError # click.echo(inputs) # click.echo(type(inputs)) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 8a796a1..7abb124 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -4,13 +4,14 @@ from abc import ABCMeta, abstractmethod import pathlib import re +import jinja2 import pendulum from tembo.exceptions import ( BasePathDoesNotExistError, MismatchingNumberOfInputTokensError, ) # noqa -from tembo import logger +from tembo import logger, CONFIG class PageCreator: @@ -23,6 +24,7 @@ class PageCreator: extension: str, name: str, user_input: tuple[str, ...], + template_file: str | None = None, ) -> Page: pass @@ -30,21 +32,48 @@ class PageCreator: def _convert_to_path( base_path: str, page_path: str, filename: str, extension: str ) -> pathlib.Path: + # check if Tembo base path exists if not pathlib.Path(base_path).expanduser().exists(): logger.debug("base path of %s does not exist", base_path) raise BasePathDoesNotExistError( f"Your base path of {base_path} does not exist." ) path_to_file = ( - pathlib.Path(base_path).expanduser() / pathlib.Path(page_path) / filename + pathlib.Path(base_path).expanduser() + / pathlib.Path(page_path).expanduser() + / filename ) - extension = extension[1:] if extension[0] == "." else extension + try: + # check for existing `.` in filename extension + extension = extension[1:] if extension[0] == "." else extension + except IndexError: + # return paths without a file + return path_to_file + # return path with a file return path_to_file.with_suffix(f".{extension}") + def _load_template(self, base_path: str, template_file: str) -> str: + if CONFIG.template_path is not None: + # check for overriden template_path + template_path = self._convert_to_path("", CONFIG.template_path, "", "") + else: + # default template_path is base_path / templates + template_path = self._convert_to_path(base_path, "templates", "", "") + print(template_path, template_file) + # load the template folder + file_loader = jinja2.FileSystemLoader(template_path) + env = jinja2.Environment(loader=file_loader, autoescape=True) + # load the template contents + loaded_template = env.get_template(template_file) + return loaded_template.render() + class ScopedPageCreator(PageCreator): - def __init__(self, raw_entry_input: str) -> None: - self.raw_entry_input = raw_entry_input + def __init__(self) -> None: + self.base_path = "" + self.page_path = "" + self.filename = "" + self.extension = "" def create_page( self, @@ -54,17 +83,34 @@ class ScopedPageCreator(PageCreator): extension: str, name: str, user_input: tuple[str, ...] | None, + template_file: str | None = None, ) -> Page: - path = self._convert_to_path(base_path, page_path, filename, extension) + self.base_path = base_path + self.page_path = page_path + self.filename = filename + self.extension = extension + # get the path of the scoped page + path = self._convert_to_path( + self.base_path, self.page_path, self.filename, self.extension + ) + # substitute tokens in the filepath path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) - return path - # return ScopedPage(path, "") - # substitute tokens in path - # substitute tokens in raw_entry_input + # get the template file + if template_file is not None: + # load the template file contents and substitute tokens + template_contents = self._load_template(self.base_path, template_file) + template_contents = self._substitute_tokens( + template_contents, user_input, name + ) + # print(template_contents) + else: + template_contents = "" + return ScopedPage(path, template_contents) def _substitute_tokens( self, tokenified_string: str, user_input: tuple[str, ...] | None, name: str ) -> str: + # for a tokened string, substitute input, name and date tokens tokenified_string = self.__substitute_input_tokens( tokenified_string, user_input ) @@ -72,17 +118,17 @@ class ScopedPageCreator(PageCreator): tokenified_string = self.__substitute_date_tokens(tokenified_string) return tokenified_string - def __substitute_name_tokens( # noqa - self, tokenified_string: str, name: str - ) -> str: + @staticmethod + def __substitute_name_tokens(tokenified_string: str, name: str) -> str: # find any {name} tokens and substitute for the name value name_extraction = re.findall(r"(\{name\d*\})", tokenified_string) for extracted_input in name_extraction: tokenified_string = tokenified_string.replace(extracted_input, name) return tokenified_string - def __substitute_input_tokens( # noqa - self, tokenified_string: str, user_input: tuple[str, ...] | None + @staticmethod + def __substitute_input_tokens( + tokenified_string: str, user_input: tuple[str, ...] | None ) -> str: # find {inputN} tokens in string input_extraction = re.findall(r"(\{input\d*\})", tokenified_string) @@ -96,18 +142,22 @@ class ScopedPageCreator(PageCreator): # if there aren't any tokens in the string, return it return tokenified_string # if there is user input, check the number of tokens matches what's passed in - if len(input_extraction) != len(user_input): + if len(input_extraction) != len(user_input) and len(input_extraction) > 0: + # if there are input matches and they don't equal the number of input + # tokens, raise error logger.debug("MismatchingNumberOfInputTokensError") raise MismatchingNumberOfInputTokensError( f"Your config specifies {len(input_extraction)} input tokens, " f"you gave {len(user_input)}", ) - # substitute each token with the user's input + # if the length of both matches and tokens match, or there arent any input + # matches, then substitute each token with the user's input for extracted_input, input_value in zip(input_extraction, user_input): tokenified_string = tokenified_string.replace(extracted_input, input_value) return tokenified_string - def __substitute_date_tokens(self, tokenified_string: str) -> str: # noqa + @staticmethod + def __substitute_date_tokens(tokenified_string: str) -> str: # find any {d:DD-MM-YYYY} tokens date_extraction_token = re.findall(r"(\{d\:[^}]*\})", tokenified_string) for extracted_token in date_extraction_token: @@ -140,12 +190,18 @@ class ScopedPage(Page): self.page_content = page_content def save_to_disk(self) -> None: - # create the file/folder if it doesnt exist - pass + scoped_note_file = pathlib.Path(self.path) + # create the parent directories + scoped_note_file.parents[0].mkdir(parents=True, exist_ok=True) + + if not scoped_note_file.exists(): + with scoped_note_file.open("w", encoding="utf-8") as scoped_page: + scoped_page.write(self.page_content) + logger.info("The file %s already exists - skipping.", str(scoped_note_file)) if __name__ == "__main__": - c = ScopedPageCreator("") + c = ScopedPageCreator() # # raises error # # print(c._substitute_tokens("scratchpad/{input0}-{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file.md", None)) # print( @@ -154,43 +210,65 @@ if __name__ == "__main__": # ) # ) - print( - c.create_page( - "~/tembo", - "{name}", - "{input0}-{input1}-file", - "md", - "scratchpad", - ("first", "second"), - ) - ) - print( - c.create_page( - "~/tembo", - "{name}/{d:MMMM-YY}", - "{input0}-{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file", - "md", - "scratchpad", - ("first",), - ) - ) - print( - c.create_page( - "~/tembo", - "{name}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", - "file", - "md", - "scratchpad", - None, - ) - ) - print( - c.create_page( - "~/tembo", - "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", - "file-{input0}-{name}", - ".md", - "meeting", - ("last",), - ) + # print( + # c.create_page( + # "~/tembo", + # "{name}", + # "{input0}-{input1}-file", + # "md", + # "scratchpad", + # ("first", "second"), + # ) + # ) + # print( + # c.create_page( + # "~/tembo", + # "{name}/{d:MMMM-YY}", + # "{input0}-{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file", + # "md", + # "scratchpad", + # ("first",), + # ) + # ) + # print( + # c.create_page( + # "~/tembo", + # "{name}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", + # "file", + # "md", + # "scratchpad", + # None, + # ) + # ) + # print( + # c.create_page( + # "~/tembo", + # "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", + # "file-{input0}-{name}", + # ".md", + # "meeting", + # ("last",), + # "scratchpad.md.tpl", + # ) + # ) + test_page_with_template = c.create_page( + "~/tembo", + "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", + "file-{input0}-{name}", + ".md", + "meeting", + ("last",), + "scratchpad.md.tpl", ) + test_page_with_template.save_to_disk() + # print( + # c.create_page( + # "~/tembo", + # "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", + # "file-{input0}-{name}", + # ".md", + # "meeting", + # ("last",), + # "scratchpad_templates/scratchpad.md.tpl", + # ) + # ) From b67cd57b3fa9cae543409712e10d1ac7d10b6f19 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 23 Oct 2021 03:03:30 +0100 Subject: [PATCH 13/74] adding latest to cli --- TODO.todo | 9 ++++++- prospector.yaml | 2 ++ tembo/cli.py | 36 +++++++++++--------------- tembo/exceptions.py | 8 ------ tembo/journal/pages.py | 58 ++++++++++++++++++++++-------------------- 5 files changed, 55 insertions(+), 58 deletions(-) diff --git a/TODO.todo b/TODO.todo index 4b64cd6..f0a4d9d 100644 --- a/TODO.todo +++ b/TODO.todo @@ -3,13 +3,20 @@ Functionality: ☐ Have an `--example` flag to `new` that prints an example given in the `config.yml` ☐ Should be a `tembo new --list` to list all possible names. +VSCode: + ☐ Look at + Logging: ☐ How to raise + debug an exception? + ☐ Document how to raise a logger.critical instead of exception + in a try, except you can just do logger.critical(exec_info=1) to print the stack Documentation: - ☐ Document usage of Panaetius in a module? + ☐ Document usage of Panaetius in a module + Using the logger, initialising with the config path etc ☐ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens ☐ Document latest typing. ☐ Using from `__future__` with `|` ☐ `using Tuple[str, ...]` ☐ `Sequence` vs `Collection` + ☐ Document using jinja2 briefly and link to Tembo (link to ) diff --git a/prospector.yaml b/prospector.yaml index 465e96e..407555f 100644 --- a/prospector.yaml +++ b/prospector.yaml @@ -78,6 +78,8 @@ pep257: - 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 diff --git a/tembo/cli.py b/tembo/cli.py index 4053e4c..88cb88e 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -1,6 +1,8 @@ import click import tembo +from tembo.journal import pages + CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -32,27 +34,19 @@ def new(scope, inputs): Example: tembo new meeting my_presentation """ - # for user_scope in tembo.CONFIG.scopes: - # print(f"passed in scope: {scope}") - # print(f'config scope: {user_scope["name"]}') - # if user_scope["name"] == scope: - # print(True) - - # TODO: if user_scope["template"], check for a template in the templates dir. If it doesn't exist raise MissingTemplateError - - # click.echo(inputs) - # click.echo(type(inputs)) - - # if len(inputs) == 0, pass None is as user_input - - -# TODO: decide on a date format to pass in -@click.command(options_metavar="") -@click.option("--scope", "-s", help="The name of the scope in the Tembo config.yml.") -@click.option("--date", "-d", help="") -def archive(scope, date): - print(f"{scope}, {date}") + for user_scope in tembo.CONFIG.scopes: + if user_scope["name"] == scope: + scoped_page = pages.ScopedPageCreator().create_page( + base_path=str(tembo.CONFIG.base_path), + page_path=str(user_scope["path"]), + filename=str(user_scope["filename"]), + extension=str(user_scope["extension"]), + name=str(user_scope["name"]), + user_input=inputs, + template_filename=str(user_scope["template_filename"]), + ) + scoped_page.save_to_disk() + tembo.logger.info("Saved %s to disk", scoped_page) run.add_command(new) -run.add_command(archive) diff --git a/tembo/exceptions.py b/tembo/exceptions.py index 185cb68..c8c6ad5 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -1,9 +1 @@ """Tembo exceptions.""" - - -class BasePathDoesNotExistError(Exception): - pass - - -class MismatchingNumberOfInputTokensError(Exception): - pass diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 7abb124..74c85e5 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -7,10 +7,6 @@ import re import jinja2 import pendulum -from tembo.exceptions import ( - BasePathDoesNotExistError, - MismatchingNumberOfInputTokensError, -) # noqa from tembo import logger, CONFIG @@ -24,7 +20,7 @@ class PageCreator: extension: str, name: str, user_input: tuple[str, ...], - template_file: str | None = None, + template_filename: str | None = None, ) -> Page: pass @@ -34,10 +30,8 @@ class PageCreator: ) -> pathlib.Path: # check if Tembo base path exists if not pathlib.Path(base_path).expanduser().exists(): - logger.debug("base path of %s does not exist", base_path) - raise BasePathDoesNotExistError( - f"Your base path of {base_path} does not exist." - ) + logger.critical("base path of %s does not exist - exiting", base_path) + raise SystemExit(1) path_to_file = ( pathlib.Path(base_path).expanduser() / pathlib.Path(page_path).expanduser() @@ -52,19 +46,19 @@ class PageCreator: # return path with a file return path_to_file.with_suffix(f".{extension}") - def _load_template(self, base_path: str, template_file: str) -> str: + def _load_template(self, base_path: str, template_filename: str) -> str: if CONFIG.template_path is not None: # check for overriden template_path template_path = self._convert_to_path("", CONFIG.template_path, "", "") else: # default template_path is base_path / templates template_path = self._convert_to_path(base_path, "templates", "", "") - print(template_path, template_file) + print(template_path, template_filename) # load the template folder file_loader = jinja2.FileSystemLoader(template_path) env = jinja2.Environment(loader=file_loader, autoescape=True) # load the template contents - loaded_template = env.get_template(template_file) + loaded_template = env.get_template(template_filename) return loaded_template.render() @@ -83,7 +77,7 @@ class ScopedPageCreator(PageCreator): extension: str, name: str, user_input: tuple[str, ...] | None, - template_file: str | None = None, + template_filename: str | None = None, ) -> Page: self.base_path = base_path self.page_path = page_path @@ -96,9 +90,9 @@ class ScopedPageCreator(PageCreator): # substitute tokens in the filepath path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) # get the template file - if template_file is not None: + if template_filename is not None: # load the template file contents and substitute tokens - template_contents = self._load_template(self.base_path, template_file) + template_contents = self._load_template(self.base_path, template_filename) template_contents = self._substitute_tokens( template_contents, user_input, name ) @@ -135,23 +129,26 @@ class ScopedPageCreator(PageCreator): if user_input is None: # if there's no user input, but the regex matches, raise error if len(input_extraction) > 0: - raise MismatchingNumberOfInputTokensError( - f"Your config specifies {len(input_extraction)} input tokens, " - f"you gave 0", + logger.critical( + "Your config/template specifies %s input tokens, you gave 0 " + "- exiting", + len(input_extraction), ) - # if there aren't any tokens in the string, return it + raise SystemExit(1) + # if there aren't any tokens in the string, return the string return tokenified_string - # if there is user input, check the number of tokens matches what's passed in + # if there is user input, check the number of tokens match what's passed in if len(input_extraction) != len(user_input) and len(input_extraction) > 0: # if there are input matches and they don't equal the number of input # tokens, raise error - logger.debug("MismatchingNumberOfInputTokensError") - raise MismatchingNumberOfInputTokensError( - f"Your config specifies {len(input_extraction)} input tokens, " - f"you gave {len(user_input)}", + logger.critical( + "Your config specifies %s input tokens, you gave %s - exiting", + len(input_extraction), + len(user_input), ) - # if the length of both matches and tokens match, or there arent any input - # matches, then substitute each token with the user's input + raise SystemExit(1) + # if the length of user input matches and number of tokens match, or there are + # no input matches, then substitute each token with the user's input for extracted_input, input_value in zip(input_extraction, user_input): tokenified_string = tokenified_string.replace(extracted_input, input_value) return tokenified_string @@ -165,7 +162,7 @@ class ScopedPageCreator(PageCreator): strftime_value = re.match(r"\{d\:([^\}]*)\}", extracted_token) if strftime_value: strftime_value = strftime_value.group(1) - # replace {d:DD-MM-YYYY} with todays format of DD-MM-YYYY + # replace {d:DD-MM-YYYY} with todays date formatted as DD-MM-YYYY tokenified_string = tokenified_string.replace( extracted_token, pendulum.now().format(strftime_value) ) @@ -189,6 +186,9 @@ class ScopedPage(Page): self.path = path self.page_content = page_content + def __str__(self): + return f"ScopedPage({self.path})" + def save_to_disk(self) -> None: scoped_note_file = pathlib.Path(self.path) # create the parent directories @@ -197,7 +197,8 @@ class ScopedPage(Page): if not scoped_note_file.exists(): with scoped_note_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) - logger.info("The file %s already exists - skipping.", str(scoped_note_file)) + else: + logger.info("The file %s already exists - skipping.", str(scoped_note_file)) if __name__ == "__main__": @@ -260,6 +261,7 @@ if __name__ == "__main__": ("last",), "scratchpad.md.tpl", ) + print(test_page_with_template) test_page_with_template.save_to_disk() # print( # c.create_page( From 560991325c4da2fb110c2e479b84f31d9c380651 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 23 Oct 2021 21:11:20 +0100 Subject: [PATCH 14/74] updating todo --- TODO.todo | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/TODO.todo b/TODO.todo index f0a4d9d..c1e8a91 100644 --- a/TODO.todo +++ b/TODO.todo @@ -2,6 +2,7 @@ Functionality: ☐ Handle case where there are no scopes in the config and command is invoked. ☐ Have an `--example` flag to `new` that prints an example given in the `config.yml` ☐ Should be a `tembo new --list` to list all possible names. + ☐ `TEMBO_CONFIG` should follow same pattern as other env vars and be a python string when read in VSCode: ☐ Look at @@ -19,4 +20,12 @@ Documentation: ☐ Using from `__future__` with `|` ☐ `using Tuple[str, ...]` ☐ `Sequence` vs `Collection` + ☐ Document how to do docstrings in python. Don't document `__init__` do it in class. + Should update the default gist to hide the `__init__` messages ☐ Document using jinja2 briefly and link to Tembo (link to ) + Tembo: + ☐ Document creating new Tembo config + ☐ ~/tembo needs creating + ☐ ~/tembo/.config + ☐ ~/tembo/.templates + ☐ Document how to overwrite these with ENV vars From 4e6efa326ed9d4a77f432a439295843fa49a9059 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 23 Oct 2021 21:12:04 +0100 Subject: [PATCH 15/74] changing default config location default config is ~/tembo/.config --- tembo/__init__.py | 6 +++--- tembo/cli.py | 12 +++++++++--- tembo/journal/pages.py | 29 ++++++++++++++--------------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/tembo/__init__.py b/tembo/__init__.py index 5e73856..a9fc698 100644 --- a/tembo/__init__.py +++ b/tembo/__init__.py @@ -2,14 +2,14 @@ import os import panaetius -if config_path := os.environ.get("TEMBO_CONFIG") is not None: +if (config_path := os.environ.get("TEMBO_CONFIG")) is not None: CONFIG = panaetius.Config("tembo", config_path) else: - CONFIG = panaetius.Config("tembo") + 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, "template_path", "~/tembo/.templates") panaetius.set_config(CONFIG, "scopes", {}) panaetius.set_config(CONFIG, "logging.level", "DEBUG") panaetius.set_config(CONFIG, "logging.path") diff --git a/tembo/cli.py b/tembo/cli.py index 88cb88e..4275486 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -12,8 +12,6 @@ def run(): """ Tembo - an organiser for work notes. """ - print(tembo.CONFIG.base_path) - # print(tembo.CONFIG.scopes) @click.command(options_metavar="") @@ -33,7 +31,6 @@ def new(scope, inputs): Example: tembo new meeting my_presentation """ - for user_scope in tembo.CONFIG.scopes: if user_scope["name"] == scope: scoped_page = pages.ScopedPageCreator().create_page( @@ -47,6 +44,15 @@ def new(scope, inputs): ) scoped_page.save_to_disk() tembo.logger.info("Saved %s to disk", scoped_page) + raise SystemExit(0) + tembo.logger.critical( + "No config.yml found in %s - exiting", tembo.CONFIG.config_path + ) + raise SystemExit(1) run.add_command(new) + + +if __name__ == "__main__": + new(["scratchpad"], ()) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 74c85e5..cc346c1 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -19,7 +19,7 @@ class PageCreator: filename: str, extension: str, name: str, - user_input: tuple[str, ...], + user_input: tuple[str, ...] | None, template_filename: str | None = None, ) -> Page: pass @@ -53,7 +53,6 @@ class PageCreator: else: # default template_path is base_path / templates template_path = self._convert_to_path(base_path, "templates", "", "") - print(template_path, template_filename) # load the template folder file_loader = jinja2.FileSystemLoader(template_path) env = jinja2.Environment(loader=file_loader, autoescape=True) @@ -96,7 +95,6 @@ class ScopedPageCreator(PageCreator): template_contents = self._substitute_tokens( template_contents, user_input, name ) - # print(template_contents) else: template_contents = "" return ScopedPage(path, template_contents) @@ -138,34 +136,34 @@ class ScopedPageCreator(PageCreator): # if there aren't any tokens in the string, return the string return tokenified_string # if there is user input, check the number of tokens match what's passed in - if len(input_extraction) != len(user_input) and len(input_extraction) > 0: + if len(input_extraction) > 0 and len(input_extraction) != len(user_input): # if there are input matches and they don't equal the number of input # tokens, raise error logger.critical( - "Your config specifies %s input tokens, you gave %s - exiting", + "Your config/template specifies %s input tokens, you gave %s - exiting", len(input_extraction), len(user_input), ) raise SystemExit(1) - # if the length of user input matches and number of tokens match, or there are - # no input matches, then substitute each token with the user's input + # if the length of both the input matches and the number of tokens match then + # substitute each token with the user's input for extracted_input, input_value in zip(input_extraction, user_input): tokenified_string = tokenified_string.replace(extracted_input, input_value) return tokenified_string @staticmethod def __substitute_date_tokens(tokenified_string: str) -> str: - # find any {d:DD-MM-YYYY} tokens + # find any {d:%d-%M-%Y} tokens date_extraction_token = re.findall(r"(\{d\:[^}]*\})", tokenified_string) for extracted_token in date_extraction_token: - # extract the inner DD-MM-YYYY only + # extract the inner %d-%M-%Y only strftime_value = re.match(r"\{d\:([^\}]*)\}", extracted_token) - if strftime_value: + if strftime_value is not None: strftime_value = strftime_value.group(1) - # replace {d:DD-MM-YYYY} with todays date formatted as DD-MM-YYYY - tokenified_string = tokenified_string.replace( - extracted_token, pendulum.now().format(strftime_value) - ) + if isinstance(strftime_value, str): + tokenified_string = tokenified_string.replace( + extracted_token, pendulum.now().strftime(strftime_value) + ) return tokenified_string @@ -198,7 +196,8 @@ class ScopedPage(Page): with scoped_note_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) else: - logger.info("The file %s already exists - skipping.", str(scoped_note_file)) + logger.info("%s already exists - skipping.", str(self)) + raise SystemExit(0) if __name__ == "__main__": From 65f6a7658b29e12646894015d2f9f70cbff6336f Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 01:21:24 +0100 Subject: [PATCH 16/74] updating panaetius --- poetry.lock | 37 ++++++++++++++++++++----------------- pyproject.toml | 5 +++-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/poetry.lock b/poetry.lock index 769531a..a72745f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -101,14 +101,14 @@ flake8 = "*" [[package]] name = "gitdb" -version = "4.0.7" +version = "4.0.8" description = "Git Object Database" category = "dev" optional = false -python-versions = ">=3.4" +python-versions = ">=3.6" [package.dependencies] -smmap = ">=3.0.1,<5" +smmap = ">=3.0.1,<6" [[package]] name = "gitpython" @@ -220,7 +220,7 @@ pyparsing = ">=2.0.2" [[package]] name = "panaetius" -version = "2.2.0" +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 @@ -436,11 +436,14 @@ pylint = ">=1.7" [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.0" description = "Python parsing module" category = "dev" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" @@ -522,11 +525,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "smmap" -version = "4.0.0" +version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [[package]] name = "snowballstemmer" @@ -574,7 +577,7 @@ 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 = "3811535320f246d17fe1c47b4a749724381fb56d5baea0e507008712f955c1b3" +content-hash = "cf725126812e7aa05eb881de12b273d64ba32c24f0d75ac0e1eb8943f7ad496c" [metadata.files] astroid = [ @@ -614,8 +617,8 @@ flake8-polyfill = [ {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] gitdb = [ - {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, - {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, + {file = "gitdb-4.0.8-py3-none-any.whl", hash = "sha256:6875cbaed01f1b750394f372607803768fc7dad7c58c7ceb5f5917e980d779b2"}, + {file = "gitdb-4.0.8.tar.gz", hash = "sha256:858966a9310649cb24a387c101429bb5a1110068a312517722b0281077e78bc6"}, ] gitpython = [ {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"}, @@ -766,8 +769,8 @@ packaging = [ {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] panaetius = [ - {file = "panaetius-2.2.0-py3-none-any.whl", hash = "sha256:55baae01ce7c1fbbea7a9d667585907e5881c859b64bbbcd747db208bcc777d8"}, - {file = "panaetius-2.2.0.tar.gz", hash = "sha256:a144debf21a13b23213f138782fd27267cb8b335a2243f86790b1bd5dcbc6da3"}, + {file = "panaetius-2.2.2-py3-none-any.whl", hash = "sha256:f353c9893f0de38d90658713d968fec095beaf6834b9f5f086dab13bd292cdf1"}, + {file = "panaetius-2.2.2.tar.gz", hash = "sha256:f0a5b9cba12d91a3e21799a5f3bf1ce19b9c40e7cadad0ce2bacc654160e8694"}, ] pbr = [ {file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"}, @@ -851,8 +854,8 @@ pylint-plugin-utils = [ {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"}, + {file = "pyparsing-3.0.0-py3-none-any.whl", hash = "sha256:d487599e9fb0dc36bee6b5c183c6fc5bd372ce667736f3d430ab7d842a54a35a"}, + {file = "pyparsing-3.0.0.tar.gz", hash = "sha256:001cad8d467e7a9248ef9fd513f5c0d39afcbcb9a43684101853bd0ab962e479"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -913,8 +916,8 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] smmap = [ - {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, - {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, + {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"}, diff --git a/pyproject.toml b/pyproject.toml index 13d7d30..c66217f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,14 +6,15 @@ authors = ["dtomlinson "] [tool.poetry.dependencies] python = "^3.8" -panaetius = "^2.0" click = "^8.0.3" pendulum = "^2.1.2" Jinja2 = "^3.0.2" +# panaetius = { path = "../panaetius", develop = true } +panaetius = "^2.2.2" [tool.poetry.dev-dependencies] pytest = "^6.2.5" -prospector = {extras = ["with_bandit", "with_mypy"], version = "^1.5.1"} +prospector = { extras = ["with_bandit", "with_mypy"], version = "^1.5.1" } [build-system] requires = ["poetry-core>=1.0.0"] From 118a85e6fccffb33d844b82400623689f2d66317 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 01:21:54 +0100 Subject: [PATCH 17/74] adding --dry-run --- tembo/cli.py | 7 ++++--- tembo/journal/pages.py | 31 +++++++++++++++++-------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/tembo/cli.py b/tembo/cli.py index 4275486..40529bf 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -21,7 +21,8 @@ def run(): nargs=-1, metavar="", ) -def new(scope, inputs): +@click.option("--dry-run", is_flag=True, default=False) +def new(scope, inputs, dry_run): """ Create a new note. @@ -42,8 +43,8 @@ def new(scope, inputs): user_input=inputs, template_filename=str(user_scope["template_filename"]), ) - scoped_page.save_to_disk() - tembo.logger.info("Saved %s to disk", scoped_page) + scoped_page.save_to_disk(dry_run=dry_run) + tembo.logger.info("Saved %s to disk", scoped_page.path) raise SystemExit(0) tembo.logger.critical( "No config.yml found in %s - exiting", tembo.CONFIG.config_path diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index cc346c1..b32019b 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -21,8 +21,9 @@ class PageCreator: name: str, user_input: tuple[str, ...] | None, template_filename: str | None = None, + dry_run: bool = False ) -> Page: - pass + raise NotImplementedError @staticmethod def _convert_to_path( @@ -30,7 +31,7 @@ class PageCreator: ) -> pathlib.Path: # check if Tembo base path exists if not pathlib.Path(base_path).expanduser().exists(): - logger.critical("base path of %s does not exist - exiting", base_path) + logger.critical("Tembo base path of %s does not exist - exiting", base_path) raise SystemExit(1) path_to_file = ( pathlib.Path(base_path).expanduser() @@ -52,7 +53,7 @@ class PageCreator: template_path = self._convert_to_path("", CONFIG.template_path, "", "") else: # default template_path is base_path / templates - template_path = self._convert_to_path(base_path, "templates", "", "") + template_path = self._convert_to_path(base_path, ".templates", "", "") # load the template folder file_loader = jinja2.FileSystemLoader(template_path) env = jinja2.Environment(loader=file_loader, autoescape=True) @@ -113,7 +114,7 @@ class ScopedPageCreator(PageCreator): @staticmethod def __substitute_name_tokens(tokenified_string: str, name: str) -> str: # find any {name} tokens and substitute for the name value - name_extraction = re.findall(r"(\{name\d*\})", tokenified_string) + name_extraction = re.findall(r"(\{name\})", tokenified_string) for extracted_input in name_extraction: tokenified_string = tokenified_string.replace(extracted_input, name) return tokenified_string @@ -135,7 +136,7 @@ class ScopedPageCreator(PageCreator): raise SystemExit(1) # if there aren't any tokens in the string, return the string return tokenified_string - # if there is user input, check the number of tokens match what's passed in + # if there is user input, check the number of tokens match the number passed in if len(input_extraction) > 0 and len(input_extraction) != len(user_input): # if there are input matches and they don't equal the number of input # tokens, raise error @@ -169,34 +170,36 @@ class ScopedPageCreator(PageCreator): class Page(metaclass=ABCMeta): @abstractmethod - def __init__(self, path: pathlib.Path, page_content: str) -> None: - pass + def __init__(self, path: pathlib.Path, page_content: str, dry_run: bool) -> None: + raise NotImplementedError @abstractmethod - def save_to_disk(self) -> None: - pass + def save_to_disk(self, dry_run: bool) -> None: + raise NotImplementedError class ScopedPage(Page): """A Page that uses substitute tokens.""" - def __init__(self, path: pathlib.Path, page_content: str): + def __init__(self, path: pathlib.Path, page_content: str) -> None: self.path = path self.page_content = page_content - def __str__(self): + def __str__(self) -> str: return f"ScopedPage({self.path})" - def save_to_disk(self) -> None: + def save_to_disk(self, dry_run: bool = False) -> None: scoped_note_file = pathlib.Path(self.path) # create the parent directories scoped_note_file.parents[0].mkdir(parents=True, exist_ok=True) - + if dry_run: + logger.info("%s will be created", self.path) + raise SystemExit(0) if not scoped_note_file.exists(): with scoped_note_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) else: - logger.info("%s already exists - skipping.", str(self)) + logger.info("%s already exists - skipping.", self.path) raise SystemExit(0) From 0262e760c948d37af30185f744f9c20debcb1ae3 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 06:00:40 +0100 Subject: [PATCH 18/74] update todo --- TODO.todo | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/TODO.todo b/TODO.todo index c1e8a91..1287bf5 100644 --- a/TODO.todo +++ b/TODO.todo @@ -2,7 +2,7 @@ Functionality: ☐ Handle case where there are no scopes in the config and command is invoked. ☐ Have an `--example` flag to `new` that prints an example given in the `config.yml` ☐ Should be a `tembo new --list` to list all possible names. - ☐ `TEMBO_CONFIG` should follow same pattern as other env vars and be a python string when read in + ✔ `TEMBO_CONFIG` should follow same pattern as other env vars and be a python string when read in @done(21-10-24 05:31) VSCode: ☐ Look at @@ -15,7 +15,8 @@ Logging: Documentation: ☐ Document usage of Panaetius in a module Using the logger, initialising with the config path etc - ☐ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens + ✘ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens @cancelled(21-10-24 05:32) + ☐ Uses `strftime` tokens: ☐ Document latest typing. ☐ Using from `__future__` with `|` ☐ `using Tuple[str, ...]` @@ -28,4 +29,5 @@ Documentation: ☐ ~/tembo needs creating ☐ ~/tembo/.config ☐ ~/tembo/.templates + ☐ ~/tembo/logs ☐ Document how to overwrite these with ENV vars From 67c2a0235201b22e6178d20722411edf5e0a40b6 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 06:00:47 +0100 Subject: [PATCH 19/74] update panaetius --- poetry.lock | 18 ++++++++++-------- pyproject.toml | 3 +-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/poetry.lock b/poetry.lock index a72745f..3c77c43 100644 --- a/poetry.lock +++ b/poetry.lock @@ -224,11 +224,16 @@ 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,<4.0" +python-versions = "^3.7" +develop = true [package.dependencies] -PyYAML = ">=6.0,<7.0" -toml = ">=0.10.0,<0.11.0" +PyYAML = "^6.0" +toml = "^0.10.0" + +[package.source] +type = "directory" +url = "../panaetius" [[package]] name = "pbr" @@ -577,7 +582,7 @@ 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 = "cf725126812e7aa05eb881de12b273d64ba32c24f0d75ac0e1eb8943f7ad496c" +content-hash = "a1db17ea6defe48d6d640c87c94f57997dd769ae5d5bdc448dee53e57cb9ffd8" [metadata.files] astroid = [ @@ -768,10 +773,7 @@ packaging = [ {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, ] -panaetius = [ - {file = "panaetius-2.2.2-py3-none-any.whl", hash = "sha256:f353c9893f0de38d90658713d968fec095beaf6834b9f5f086dab13bd292cdf1"}, - {file = "panaetius-2.2.2.tar.gz", hash = "sha256:f0a5b9cba12d91a3e21799a5f3bf1ce19b9c40e7cadad0ce2bacc654160e8694"}, -] +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"}, diff --git a/pyproject.toml b/pyproject.toml index c66217f..b97e44f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,7 @@ python = "^3.8" click = "^8.0.3" pendulum = "^2.1.2" Jinja2 = "^3.0.2" -# panaetius = { path = "../panaetius", develop = true } -panaetius = "^2.2.2" +panaetius = { path = "../panaetius", develop = true } [tool.poetry.dev-dependencies] pytest = "^6.2.5" From 70129585a86a80aa1cb334b6269579356a484ba3 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 06:01:17 +0100 Subject: [PATCH 20/74] updating typing --- tembo/journal/pages.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index b32019b..ba4a6e9 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod import pathlib import re +from typing import Tuple import jinja2 import pendulum @@ -19,9 +20,8 @@ class PageCreator: filename: str, extension: str, name: str, - user_input: tuple[str, ...] | None, + user_input: Tuple[str, ...] | Tuple[()], template_filename: str | None = None, - dry_run: bool = False ) -> Page: raise NotImplementedError @@ -76,7 +76,7 @@ class ScopedPageCreator(PageCreator): filename: str, extension: str, name: str, - user_input: tuple[str, ...] | None, + user_input: Tuple[str, ...] | Tuple[()], template_filename: str | None = None, ) -> Page: self.base_path = base_path @@ -101,7 +101,10 @@ class ScopedPageCreator(PageCreator): return ScopedPage(path, template_contents) def _substitute_tokens( - self, tokenified_string: str, user_input: tuple[str, ...] | None, name: str + self, + tokenified_string: str, + user_input: Tuple[str, ...] | Tuple[()], + name: str, ) -> str: # for a tokened string, substitute input, name and date tokens tokenified_string = self.__substitute_input_tokens( @@ -121,11 +124,11 @@ class ScopedPageCreator(PageCreator): @staticmethod def __substitute_input_tokens( - tokenified_string: str, user_input: tuple[str, ...] | None + tokenified_string: str, user_input: Tuple[str, ...] | Tuple[()] ) -> str: # find {inputN} tokens in string input_extraction = re.findall(r"(\{input\d*\})", tokenified_string) - if user_input is None: + if len(user_input) == 0: # if there's no user input, but the regex matches, raise error if len(input_extraction) > 0: logger.critical( @@ -137,7 +140,11 @@ class ScopedPageCreator(PageCreator): # if there aren't any tokens in the string, return the string return tokenified_string # if there is user input, check the number of tokens match the number passed in - if len(input_extraction) > 0 and len(input_extraction) != len(user_input): + if ( + len(input_extraction) > 0 + and len(input_extraction) != len(user_input) + and len(user_input) > 0 + ): # if there are input matches and they don't equal the number of input # tokens, raise error logger.critical( @@ -148,8 +155,11 @@ class ScopedPageCreator(PageCreator): raise SystemExit(1) # if the length of both the input matches and the number of tokens match then # substitute each token with the user's input - for extracted_input, input_value in zip(input_extraction, user_input): - tokenified_string = tokenified_string.replace(extracted_input, input_value) + if len(user_input) > 0: + for extracted_input, input_value in zip(input_extraction, user_input): + tokenified_string = tokenified_string.replace( + extracted_input, input_value + ) return tokenified_string @staticmethod @@ -170,7 +180,7 @@ class ScopedPageCreator(PageCreator): class Page(metaclass=ABCMeta): @abstractmethod - def __init__(self, path: pathlib.Path, page_content: str, dry_run: bool) -> None: + def __init__(self, path: pathlib.Path, page_content: str) -> None: raise NotImplementedError @abstractmethod @@ -189,12 +199,12 @@ class ScopedPage(Page): return f"ScopedPage({self.path})" def save_to_disk(self, dry_run: bool = False) -> None: - scoped_note_file = pathlib.Path(self.path) - # create the parent directories - scoped_note_file.parents[0].mkdir(parents=True, exist_ok=True) if dry_run: logger.info("%s will be created", self.path) raise SystemExit(0) + # create the parent directories + scoped_note_file = pathlib.Path(self.path) + scoped_note_file.parents[0].mkdir(parents=True, exist_ok=True) if not scoped_note_file.exists(): with scoped_note_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) @@ -264,7 +274,7 @@ if __name__ == "__main__": "scratchpad.md.tpl", ) print(test_page_with_template) - test_page_with_template.save_to_disk() + test_page_with_template.save_to_disk(False) # print( # c.create_page( # "~/tembo", From c85a0068101c9c793e4ee5423a9374a51ad63df3 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 06:01:39 +0100 Subject: [PATCH 21/74] add command not found error --- tembo/cli.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/tembo/cli.py b/tembo/cli.py index 40529bf..b26acec 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -32,20 +32,25 @@ def new(scope, inputs, dry_run): Example: tembo new meeting my_presentation """ - for user_scope in tembo.CONFIG.scopes: - if user_scope["name"] == scope: - scoped_page = pages.ScopedPageCreator().create_page( - base_path=str(tembo.CONFIG.base_path), - page_path=str(user_scope["path"]), - filename=str(user_scope["filename"]), - extension=str(user_scope["extension"]), - name=str(user_scope["name"]), - user_input=inputs, - template_filename=str(user_scope["template_filename"]), - ) - scoped_page.save_to_disk(dry_run=dry_run) - tembo.logger.info("Saved %s to disk", scoped_page.path) - raise SystemExit(0) + _name_found = scope in [user_scope["name"] for user_scope in tembo.CONFIG.scopes] + if _name_found: + for user_scope in tembo.CONFIG.scopes: + if user_scope["name"] == scope: + scoped_page = pages.ScopedPageCreator().create_page( + base_path=str(tembo.CONFIG.base_path), + page_path=str(user_scope["path"]), + filename=str(user_scope["filename"]), + extension=str(user_scope["extension"]), + name=str(user_scope["name"]), + user_input=inputs, + template_filename=str(user_scope["template_filename"]) + ) + scoped_page.save_to_disk(dry_run=dry_run) + tembo.logger.info("Saved %s to disk", scoped_page.path) + raise SystemExit(0) + if not _name_found and len(tembo.CONFIG.scopes) > 0: + tembo.logger.warning("Command %s not found in config.yml - exiting", scope) + raise SystemExit(0) tembo.logger.critical( "No config.yml found in %s - exiting", tembo.CONFIG.config_path ) @@ -56,4 +61,6 @@ run.add_command(new) if __name__ == "__main__": - new(["scratchpad"], ()) + # BUG: fix this bug where input tokens are mismatched + new(["meeting", "robs presentation", "meeting on gcp"]) + # new(["meeting", "robs presentation"]) From b629266752020dbb676f2f35681315cb7c5ef55f Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 06:01:55 +0100 Subject: [PATCH 22/74] add logging file not found warning --- tembo/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tembo/__init__.py b/tembo/__init__.py index a9fc698..cd93fcd 100644 --- a/tembo/__init__.py +++ b/tembo/__init__.py @@ -1,6 +1,7 @@ import os import panaetius +from panaetius.exceptions import LoggingDirectoryDoesNotExistException if (config_path := os.environ.get("TEMBO_CONFIG")) is not None: CONFIG = panaetius.Config("tembo", config_path) @@ -14,6 +15,14 @@ panaetius.set_config(CONFIG, "scopes", {}) panaetius.set_config(CONFIG, "logging.level", "DEBUG") panaetius.set_config(CONFIG, "logging.path") -logger = panaetius.set_logger( - CONFIG, panaetius.SimpleLogger(logging_level=CONFIG.logging_level) -) +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) From 0c03f327469465009d10399a877e48766cf3ee45 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 07:06:40 +0100 Subject: [PATCH 23/74] adding latest --- tembo/exceptions.py | 4 +++ tembo/journal/pages.py | 79 +++++++++++++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/tembo/exceptions.py b/tembo/exceptions.py index c8c6ad5..a71ea99 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -1 +1,5 @@ """Tembo exceptions.""" + + +class MismatchedTokenError(Exception): + pass diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index ba4a6e9..ebf8bbd 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -9,6 +9,7 @@ import jinja2 import pendulum from tembo import logger, CONFIG +from tembo.exceptions import MismatchedTokenError class PageCreator: @@ -83,23 +84,66 @@ class ScopedPageCreator(PageCreator): self.page_path = page_path self.filename = filename self.extension = extension + path_error: MismatchedTokenError | None = None + template_error: MismatchedTokenError | None = None + # get the path of the scoped page path = self._convert_to_path( self.base_path, self.page_path, self.filename, self.extension ) # substitute tokens in the filepath - path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) + try: + path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) + except MismatchedTokenError as mismatched_path_error: + path_error = mismatched_path_error # get the template file if template_filename is not None: # load the template file contents and substitute tokens template_contents = self._load_template(self.base_path, template_filename) - template_contents = self._substitute_tokens( - template_contents, user_input, name - ) + try: + template_contents = self._substitute_tokens( + template_contents, user_input, name + ) + except MismatchedTokenError as mismatched_template_error: + template_error = mismatched_template_error else: template_contents = "" + if path_error is not None or template_error is not None: + # self.__mismatched_token_error(path_error, template_error) + if path_error.args[0] > template_error.args[0]: + print("path_token_count > template_token_count") + elif template_error.args[0] > path_error.args[0]: + print("template_token_count > path_token_count") return ScopedPage(path, template_contents) + @staticmethod + def __mismatched_token_error( + path_error: MismatchedTokenError | None = None, + template_error: MismatchedTokenError | None = None, + ) -> None: + if isinstance(path_error, MismatchedTokenError): + path_token_count = path_error.args[0] + # logger.critical( + # "Your config specifies %s input tokens, you gave %s " "- exiting", + # path_error.args[0], + # path_error.args[1], + # ) + # raise SystemExit(1) + if isinstance(template_error, MismatchedTokenError): + template_token_count = template_error.args[0] + # logger.critical( + # "Your template specifies %s input tokens, you gave %s " "- exiting", + # template_error.args[0], + # template_error.args[1], + # ) + # raise SystemExit(1) + if path_token_count > template_token_count: + print("path_token_count > template_token_count") + elif template_token_count > path_token_count: + print("template_token_count > path_token_count") + raise SystemExit(1) + + # TODO: change the annotation to include the error def _substitute_tokens( self, tokenified_string: str, @@ -107,11 +151,14 @@ class ScopedPageCreator(PageCreator): name: str, ) -> str: # for a tokened string, substitute input, name and date tokens - tokenified_string = self.__substitute_input_tokens( - tokenified_string, user_input - ) - tokenified_string = self.__substitute_name_tokens(tokenified_string, name) - tokenified_string = self.__substitute_date_tokens(tokenified_string) + try: + tokenified_string = self.__substitute_input_tokens( + tokenified_string, user_input + ) + tokenified_string = self.__substitute_name_tokens(tokenified_string, name) + tokenified_string = self.__substitute_date_tokens(tokenified_string) + except MismatchedTokenError as mismatched_token_error: + raise mismatched_token_error return tokenified_string @staticmethod @@ -131,12 +178,7 @@ class ScopedPageCreator(PageCreator): if len(user_input) == 0: # if there's no user input, but the regex matches, raise error if len(input_extraction) > 0: - logger.critical( - "Your config/template specifies %s input tokens, you gave 0 " - "- exiting", - len(input_extraction), - ) - raise SystemExit(1) + raise MismatchedTokenError(len(input_extraction), 0) # if there aren't any tokens in the string, return the string return tokenified_string # if there is user input, check the number of tokens match the number passed in @@ -147,12 +189,7 @@ class ScopedPageCreator(PageCreator): ): # if there are input matches and they don't equal the number of input # tokens, raise error - logger.critical( - "Your config/template specifies %s input tokens, you gave %s - exiting", - len(input_extraction), - len(user_input), - ) - raise SystemExit(1) + raise MismatchedTokenError(len(input_extraction), len(user_input)) # if the length of both the input matches and the number of tokens match then # substitute each token with the user's input if len(user_input) > 0: From 5fb70300fb523cdac0ee3d860726437b38f9ea85 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 18:28:36 +0100 Subject: [PATCH 24/74] adding latest --- tembo/journal/pages.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index ebf8bbd..72238a6 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -69,6 +69,9 @@ class ScopedPageCreator(PageCreator): self.page_path = "" self.filename = "" self.extension = "" + self.path_error: MismatchedTokenError | None = None + self.template_error: MismatchedTokenError | None = None + def create_page( self, @@ -84,8 +87,6 @@ class ScopedPageCreator(PageCreator): self.page_path = page_path self.filename = filename self.extension = extension - path_error: MismatchedTokenError | None = None - template_error: MismatchedTokenError | None = None # get the path of the scoped page path = self._convert_to_path( @@ -95,7 +96,7 @@ class ScopedPageCreator(PageCreator): try: path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) except MismatchedTokenError as mismatched_path_error: - path_error = mismatched_path_error + self.path_error = mismatched_path_error # get the template file if template_filename is not None: # load the template file contents and substitute tokens @@ -105,14 +106,14 @@ class ScopedPageCreator(PageCreator): template_contents, user_input, name ) except MismatchedTokenError as mismatched_template_error: - template_error = mismatched_template_error + self.template_error = mismatched_template_error else: template_contents = "" - if path_error is not None or template_error is not None: + if self.path_error is not None or self.template_error is not None: # self.__mismatched_token_error(path_error, template_error) - if path_error.args[0] > template_error.args[0]: + if self.path_error.args[0] > self.template_error.args[0]: print("path_token_count > template_token_count") - elif template_error.args[0] > path_error.args[0]: + elif self.template_error.args[0] > self.path_error.args[0]: print("template_token_count > path_token_count") return ScopedPage(path, template_contents) From 6b2b0a3b5eeadf3337f9fc8dc3485820bbaf49a4 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 18:28:18 +0100 Subject: [PATCH 25/74] Auto stash before merge of "bugfix/token_mismatch" and "origin/bugfix/token_mismatch" --- .pythonversion => .python-version | 0 tembo/journal/pages.py | 69 +++++++++--------- tests/__pycache__/__init__.cpython-38.pyc | Bin 151 -> 148 bytes .../test_tembo.cpython-38-pytest-6.2.5.pyc | Bin 153 -> 150 bytes 4 files changed, 36 insertions(+), 33 deletions(-) rename .pythonversion => .python-version (100%) diff --git a/.pythonversion b/.python-version similarity index 100% rename from .pythonversion rename to .python-version diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 72238a6..ae5b796 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod import pathlib import re -from typing import Tuple +from typing import Tuple, Literal import jinja2 import pendulum @@ -69,8 +69,8 @@ class ScopedPageCreator(PageCreator): self.page_path = "" self.filename = "" self.extension = "" - self.path_error: MismatchedTokenError | None = None - self.template_error: MismatchedTokenError | None = None + self.path_date_tokens: Tuple[int, int] = (0, 0) + self.template_date_tokens: Tuple[int, int] = (0, 0) def create_page( @@ -94,7 +94,9 @@ class ScopedPageCreator(PageCreator): ) # substitute tokens in the filepath try: - path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) + path = pathlib.Path( + self._substitute_tokens(str(path), user_input, name, "path") + ) except MismatchedTokenError as mismatched_path_error: self.path_error = mismatched_path_error # get the template file @@ -103,7 +105,7 @@ class ScopedPageCreator(PageCreator): template_contents = self._load_template(self.base_path, template_filename) try: template_contents = self._substitute_tokens( - template_contents, user_input, name + template_contents, user_input, name, "template" ) except MismatchedTokenError as mismatched_template_error: self.template_error = mismatched_template_error @@ -117,32 +119,32 @@ class ScopedPageCreator(PageCreator): print("template_token_count > path_token_count") return ScopedPage(path, template_contents) - @staticmethod - def __mismatched_token_error( - path_error: MismatchedTokenError | None = None, - template_error: MismatchedTokenError | None = None, - ) -> None: - if isinstance(path_error, MismatchedTokenError): - path_token_count = path_error.args[0] - # logger.critical( - # "Your config specifies %s input tokens, you gave %s " "- exiting", - # path_error.args[0], - # path_error.args[1], - # ) - # raise SystemExit(1) - if isinstance(template_error, MismatchedTokenError): - template_token_count = template_error.args[0] - # logger.critical( - # "Your template specifies %s input tokens, you gave %s " "- exiting", - # template_error.args[0], - # template_error.args[1], - # ) - # raise SystemExit(1) - if path_token_count > template_token_count: - print("path_token_count > template_token_count") - elif template_token_count > path_token_count: - print("template_token_count > path_token_count") - raise SystemExit(1) + # @staticmethod + # def __mismatched_token_error( + # path_error: MismatchedTokenError | None = None, + # template_error: MismatchedTokenError | None = None, + # ) -> None: + # if isinstance(path_error, MismatchedTokenError): + # path_token_count = path_error.args[0] + # # logger.critical( + # # "Your config specifies %s input tokens, you gave %s " "- exiting", + # # path_error.args[0], + # # path_error.args[1], + # # ) + # # raise SystemExit(1) + # if isinstance(template_error, MismatchedTokenError): + # template_token_count = template_error.args[0] + # # logger.critical( + # # "Your template specifies %s input tokens, you gave %s " "- exiting", + # # template_error.args[0], + # # template_error.args[1], + # # ) + # # raise SystemExit(1) + # if path_token_count > template_token_count: + # print("path_token_count > template_token_count") + # elif template_token_count > path_token_count: + # print("template_token_count > path_token_count") + # raise SystemExit(1) # TODO: change the annotation to include the error def _substitute_tokens( @@ -150,6 +152,7 @@ class ScopedPageCreator(PageCreator): tokenified_string: str, user_input: Tuple[str, ...] | Tuple[()], name: str, + token_type: Literal["path", "template"], ) -> str: # for a tokened string, substitute input, name and date tokens try: @@ -170,9 +173,9 @@ class ScopedPageCreator(PageCreator): tokenified_string = tokenified_string.replace(extracted_input, name) return tokenified_string - @staticmethod + # @staticmethod def __substitute_input_tokens( - tokenified_string: str, user_input: Tuple[str, ...] | Tuple[()] + self, tokenified_string: str, user_input: Tuple[str, ...] | Tuple[()] ) -> str: # find {inputN} tokens in string input_extraction = re.findall(r"(\{input\d*\})", tokenified_string) diff --git a/tests/__pycache__/__init__.cpython-38.pyc b/tests/__pycache__/__init__.cpython-38.pyc index 317d9d6aaf71c8b397ae2f32e1fe62869844b8e8..b550fe0f02174128a0407f6693b2cad3bd046cc2 100644 GIT binary patch delta 38 scmbQvIE9frl$V!_0SF%Tl}_ZgVYAfF$j?ok=qJuvS(1^TH!(*W0JlX7+W-In delta 41 vcmbQjIGvF@l$V!_0SNx<=TGFeVYkr_Elw>ep6DyiUX-7enp{#mF;g1=(-#ZI diff --git a/tests/__pycache__/test_tembo.cpython-38-pytest-6.2.5.pyc b/tests/__pycache__/test_tembo.cpython-38-pytest-6.2.5.pyc index 6bfba605c4f36b2dac1d7e1d38aa0f7acf251d64..91d170683e0a5178f624bc17758ff960e5cf6636 100644 GIT binary patch delta 38 scmbQqIE|4zl$V!_0SJWpODA&MuvzP8TEXl~vo0y{u0HenW6aWAK delta 41 vcmbQnIFpe(l$V!_0SLOY^CxoKu-oZ}7N-^!PxKXMFUrqKO)e>(n5hc@&4&w? From b1fda1fa8bdc7772186bf03de401b13497f3cf51 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 21:56:04 +0100 Subject: [PATCH 26/74] adding latest --- TODO.todo | 4 + tembo/cli.py | 3 +- tembo/journal/pages.py | 164 ++++++++++++++++++++++------------------- 3 files changed, 96 insertions(+), 75 deletions(-) diff --git a/TODO.todo b/TODO.todo index 1287bf5..c956e6e 100644 --- a/TODO.todo +++ b/TODO.todo @@ -2,6 +2,10 @@ Functionality: ☐ Handle case where there are no scopes in the config and command is invoked. ☐ Have an `--example` flag to `new` that prints an example given in the `config.yml` ☐ Should be a `tembo new --list` to list all possible names. + ☐ When template not found, raise a Tembo error + currently raises jinja2.exceptions.TemplateNotFound on line 62, in _load_template + ☐ Add update notification? + check pypi for latest version and compare to current ✔ `TEMBO_CONFIG` should follow same pattern as other env vars and be a python string when read in @done(21-10-24 05:31) VSCode: diff --git a/tembo/cli.py b/tembo/cli.py index b26acec..1978be1 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -62,5 +62,6 @@ run.add_command(new) if __name__ == "__main__": # BUG: fix this bug where input tokens are mismatched - new(["meeting", "robs presentation", "meeting on gcp"]) + # new(["meeting", "robs presentation", "meeting on gcp"]) + new(["meeting", "a", "b", "c"]) # new(["meeting", "robs presentation"]) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index ae5b796..4398af0 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -69,9 +69,9 @@ class ScopedPageCreator(PageCreator): self.page_path = "" self.filename = "" self.extension = "" - self.path_date_tokens: Tuple[int, int] = (0, 0) - self.template_date_tokens: Tuple[int, int] = (0, 0) - + # TODO: rename these to input tokens + more sensible + self.path_input_tokens: Tuple[int, int] = (0, 0) + self.template_input_tokens: Tuple[int, int] = (0, 0) def create_page( self, @@ -92,77 +92,84 @@ class ScopedPageCreator(PageCreator): path = self._convert_to_path( self.base_path, self.page_path, self.filename, self.extension ) + # substitute tokens in the filepath - try: - path = pathlib.Path( - self._substitute_tokens(str(path), user_input, name, "path") - ) - except MismatchedTokenError as mismatched_path_error: - self.path_error = mismatched_path_error + path = pathlib.Path( + self._substitute_tokens(str(path), user_input, name, "path") + ) + if sum(self.path_input_tokens) > 0: + _highest_input_token_in_path = max(self.path_input_tokens) + else: + _highest_input_token_in_path = 0 + # get the template file if template_filename is not None: # load the template file contents and substitute tokens template_contents = self._load_template(self.base_path, template_filename) - try: - template_contents = self._substitute_tokens( - template_contents, user_input, name, "template" - ) - except MismatchedTokenError as mismatched_template_error: - self.template_error = mismatched_template_error + template_contents = self._substitute_tokens( + template_contents, user_input, name, "template" + ) + if sum(self.template_input_tokens) > 0: + _highest_input_token_in_template = max(self.template_input_tokens) + else: + _highest_input_token_in_template = 0 else: template_contents = "" - if self.path_error is not None or self.template_error is not None: - # self.__mismatched_token_error(path_error, template_error) - if self.path_error.args[0] > self.template_error.args[0]: - print("path_token_count > template_token_count") - elif self.template_error.args[0] > self.path_error.args[0]: - print("template_token_count > path_token_count") + + self.__check_input_token_mismatch( + _highest_input_token_in_path, _highest_input_token_in_template + ) return ScopedPage(path, template_contents) - # @staticmethod - # def __mismatched_token_error( - # path_error: MismatchedTokenError | None = None, - # template_error: MismatchedTokenError | None = None, - # ) -> None: - # if isinstance(path_error, MismatchedTokenError): - # path_token_count = path_error.args[0] - # # logger.critical( - # # "Your config specifies %s input tokens, you gave %s " "- exiting", - # # path_error.args[0], - # # path_error.args[1], - # # ) - # # raise SystemExit(1) - # if isinstance(template_error, MismatchedTokenError): - # template_token_count = template_error.args[0] - # # logger.critical( - # # "Your template specifies %s input tokens, you gave %s " "- exiting", - # # template_error.args[0], - # # template_error.args[1], - # # ) - # # raise SystemExit(1) - # if path_token_count > template_token_count: - # print("path_token_count > template_token_count") - # elif template_token_count > path_token_count: - # print("template_token_count > path_token_count") - # raise SystemExit(1) + def __check_input_token_mismatch( + self, _highest_input_token_in_path: int, _highest_input_token_in_template: int + ) -> None: + _highest_input_token_count = max( + _highest_input_token_in_path, _highest_input_token_in_template + ) + if _highest_input_token_in_path < _highest_input_token_count: + logger.critical( + "Your config/template specifies %s input tokens, you gave %s " + "- exiting", + _highest_input_token_count, + self.path_input_tokens[0], + ) + raise SystemExit(1) + if _highest_input_token_in_path > _highest_input_token_count: + logger.warning( + "Your config/template specifies %s input tokens, you gave %s", + _highest_input_token_count, + self.path_input_tokens[0], + ) + if _highest_input_token_in_template < _highest_input_token_count: + logger.critical( + "Your config/template specifies %s input tokens, you gave %s " + "- exiting", + _highest_input_token_count, + self.template_input_tokens[0], + ) + raise SystemExit(1) + if _highest_input_token_in_template > _highest_input_token_count: + logger.warning( + "Your config/template specifies %s input tokens, you gave %s", + _highest_input_token_count, + self.template_input_tokens[0], + ) - # TODO: change the annotation to include the error def _substitute_tokens( self, tokenified_string: str, user_input: Tuple[str, ...] | Tuple[()], name: str, - token_type: Literal["path", "template"], + input_token_type: Literal["path", "template"], ) -> str: - # for a tokened string, substitute input, name and date tokens - try: - tokenified_string = self.__substitute_input_tokens( - tokenified_string, user_input - ) - tokenified_string = self.__substitute_name_tokens(tokenified_string, name) - tokenified_string = self.__substitute_date_tokens(tokenified_string) - except MismatchedTokenError as mismatched_token_error: - raise mismatched_token_error + """For a tokened string, substitute input, name and date tokens.""" + + tokenified_string = self.__substitute_input_tokens( + tokenified_string, user_input, input_token_type + ) + tokenified_string = self.__substitute_name_tokens(tokenified_string, name) + tokenified_string = self.__substitute_date_tokens(tokenified_string) return tokenified_string @staticmethod @@ -175,28 +182,35 @@ class ScopedPageCreator(PageCreator): # @staticmethod def __substitute_input_tokens( - self, tokenified_string: str, user_input: Tuple[str, ...] | Tuple[()] + self, + tokenified_string: str, + user_input: Tuple[str, ...] | Tuple[()], + input_token_type: Literal["path", "template"], ) -> str: - # find {inputN} tokens in string + """Find `{inputN}` tokens in string.""" + input_extraction = re.findall(r"(\{input\d*\})", tokenified_string) + # if there is no user input if len(user_input) == 0: - # if there's no user input, but the regex matches, raise error if len(input_extraction) > 0: - raise MismatchedTokenError(len(input_extraction), 0) + # if the regex matches, save the number of input tokens found + if input_token_type == "path": # noqa: bandit 105 + # TODO: change this to a dict instead of tuple + self.path_input_tokens = (len(input_extraction), 0) + if input_token_type == "template": # noqa: bandit 105 + self.template_input_tokens = (len(input_extraction), 0) # if there aren't any tokens in the string, return the string return tokenified_string - # if there is user input, check the number of tokens match the number passed in - if ( - len(input_extraction) > 0 - and len(input_extraction) != len(user_input) - and len(user_input) > 0 - ): - # if there are input matches and they don't equal the number of input - # tokens, raise error - raise MismatchedTokenError(len(input_extraction), len(user_input)) - # if the length of both the input matches and the number of tokens match then - # substitute each token with the user's input + + # if there is user input if len(user_input) > 0: + # save the number of input tokens, and the number of user inputs + if input_token_type == "path": # noqa: bandit 105 + self.path_input_tokens = (len(input_extraction), len(user_input)) + elif input_token_type == "template": # noqa: bandit 105 + self.template_input_tokens = (len(input_extraction), len(user_input)) + + # sbustitute the input token for the user's input for extracted_input, input_value in zip(input_extraction, user_input): tokenified_string = tokenified_string.replace( extracted_input, input_value @@ -205,7 +219,9 @@ class ScopedPageCreator(PageCreator): @staticmethod def __substitute_date_tokens(tokenified_string: str) -> str: - # find any {d:%d-%M-%Y} tokens + """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 From 2d91c9d62d2284f19265c957e62d20d7bca3d9ef Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 24 Oct 2021 22:34:58 +0100 Subject: [PATCH 27/74] adding latest --- TODO.todo | 40 +++++++++++++++---------- tembo/journal/pages.py | 68 ++++++++++++++++-------------------------- 2 files changed, 50 insertions(+), 58 deletions(-) diff --git a/TODO.todo b/TODO.todo index c956e6e..cbe844e 100644 --- a/TODO.todo +++ b/TODO.todo @@ -8,30 +8,40 @@ Functionality: check pypi for latest version and compare to current ✔ `TEMBO_CONFIG` should follow same pattern as other env vars and be a python string when read in @done(21-10-24 05:31) +Bug: + ☐ tokens + Say we have input0 and input3 in file path + and we have input1 and input2 in template + it only recognises this as 2 inputs total, not four. + passing in tembo new meeting a b will work and input0=input1, input3=input2 + VSCode: ☐ Look at Logging: - ☐ How to raise + debug an exception? - ☐ Document how to raise a logger.critical instead of exception - in a try, except you can just do logger.critical(exec_info=1) to print the stack -Documentation: - ☐ Document usage of Panaetius in a module - Using the logger, initialising with the config path etc - ✘ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens @cancelled(21-10-24 05:32) - ☐ Uses `strftime` tokens: - ☐ Document latest typing. + Documentation: + ☐ Document usage of Panaetius in a module + Using the logger, initialising with the config path etc + ✘ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens @cancelled(21-10-24 05:32) + ☐ Uses `strftime` tokens: + ☐ Document latest typing. ☐ Using from `__future__` with `|` ☐ `using Tuple[str, ...]` ☐ `Sequence` vs `Collection` - ☐ Document how to do docstrings in python. Don't document `__init__` do it in class. - Should update the default gist to hide the `__init__` messages - ☐ Document using jinja2 briefly and link to Tembo (link to ) - Tembo: - ☐ Document creating new Tembo config + ☐ Document how to do docstrings in python. Don't document `__init__` do it in class. + Should update the default gist to hide the `__init__` messages + ☐ Document using jinja2 briefly and link to Tembo (link to ) + + Logging: + ☐ How to raise + debug an exception? + ☐ Document how to raise a logger.critical instead of exception + in a try, except you can just do logger.critical(exec_info=1) to print the stack + + Tembo: + ☐ Document creating new Tembo config ☐ ~/tembo needs creating ☐ ~/tembo/.config ☐ ~/tembo/.templates ☐ ~/tembo/logs - ☐ Document how to overwrite these with ENV vars + ☐ Document how to overwrite these with ENV vars diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 4398af0..f27afd1 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -9,7 +9,6 @@ import jinja2 import pendulum from tembo import logger, CONFIG -from tembo.exceptions import MismatchedTokenError class PageCreator: @@ -70,8 +69,8 @@ class ScopedPageCreator(PageCreator): self.filename = "" self.extension = "" # TODO: rename these to input tokens + more sensible - self.path_input_tokens: Tuple[int, int] = (0, 0) - self.template_input_tokens: Tuple[int, int] = (0, 0) + self.path_input_token_counts = {"config": 0, "user": 0} + self.template_input_token_counts = {"config": 0, "user": 0} def create_page( self, @@ -97,10 +96,6 @@ class ScopedPageCreator(PageCreator): path = pathlib.Path( self._substitute_tokens(str(path), user_input, name, "path") ) - if sum(self.path_input_tokens) > 0: - _highest_input_token_in_path = max(self.path_input_tokens) - else: - _highest_input_token_in_path = 0 # get the template file if template_filename is not None: @@ -109,51 +104,34 @@ class ScopedPageCreator(PageCreator): template_contents = self._substitute_tokens( template_contents, user_input, name, "template" ) - if sum(self.template_input_tokens) > 0: - _highest_input_token_in_template = max(self.template_input_tokens) - else: - _highest_input_token_in_template = 0 else: template_contents = "" - self.__check_input_token_mismatch( - _highest_input_token_in_path, _highest_input_token_in_template - ) + self.__check_input_token_mismatch() return ScopedPage(path, template_contents) - def __check_input_token_mismatch( - self, _highest_input_token_in_path: int, _highest_input_token_in_template: int - ) -> None: - _highest_input_token_count = max( - _highest_input_token_in_path, _highest_input_token_in_template + def __check_input_token_mismatch(self) -> None: + _max_config_input_token_count = max( + self.path_input_token_counts["config"], + self.template_input_token_counts["config"], ) - if _highest_input_token_in_path < _highest_input_token_count: + _max_user_input_token_count = max( + self.path_input_token_counts["user"], + self.template_input_token_counts["user"], + ) + if _max_user_input_token_count < _max_config_input_token_count: logger.critical( "Your config/template specifies %s input tokens, you gave %s " "- exiting", - _highest_input_token_count, - self.path_input_tokens[0], + _max_config_input_token_count, + _max_user_input_token_count, ) raise SystemExit(1) - if _highest_input_token_in_path > _highest_input_token_count: + if _max_user_input_token_count > _max_config_input_token_count: logger.warning( "Your config/template specifies %s input tokens, you gave %s", - _highest_input_token_count, - self.path_input_tokens[0], - ) - if _highest_input_token_in_template < _highest_input_token_count: - logger.critical( - "Your config/template specifies %s input tokens, you gave %s " - "- exiting", - _highest_input_token_count, - self.template_input_tokens[0], - ) - raise SystemExit(1) - if _highest_input_token_in_template > _highest_input_token_count: - logger.warning( - "Your config/template specifies %s input tokens, you gave %s", - _highest_input_token_count, - self.template_input_tokens[0], + _max_config_input_token_count, + _max_user_input_token_count, ) def _substitute_tokens( @@ -196,9 +174,11 @@ class ScopedPageCreator(PageCreator): # if the regex matches, save the number of input tokens found if input_token_type == "path": # noqa: bandit 105 # TODO: change this to a dict instead of tuple - self.path_input_tokens = (len(input_extraction), 0) + self.path_input_token_counts["config"] = len(input_extraction) + self.path_input_token_counts["user"] = 0 if input_token_type == "template": # noqa: bandit 105 - self.template_input_tokens = (len(input_extraction), 0) + self.template_input_token_counts["config"] = len(input_extraction) + self.template_input_token_counts["user"] = 0 # if there aren't any tokens in the string, return the string return tokenified_string @@ -206,9 +186,11 @@ class ScopedPageCreator(PageCreator): if len(user_input) > 0: # save the number of input tokens, and the number of user inputs if input_token_type == "path": # noqa: bandit 105 - self.path_input_tokens = (len(input_extraction), len(user_input)) + self.path_input_token_counts["config"] = len(input_extraction) + self.path_input_token_counts["user"] = len(user_input) elif input_token_type == "template": # noqa: bandit 105 - self.template_input_tokens = (len(input_extraction), len(user_input)) + self.template_input_token_counts["config"] = len(input_extraction) + self.template_input_token_counts["user"] = len(user_input) # sbustitute the input token for the user's input for extracted_input, input_value in zip(input_extraction, user_input): From a19866e3d8e465fab6c5e8e2c812f8951bb3f19e Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 02:48:16 +0100 Subject: [PATCH 28/74] updating .gitgnore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2dc658c..34243a4 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,6 @@ dmypy.json # Cython debug symbols cython_debug/ +# custom .vscode/ +**/__pycache__ From 87c6e99eed63208b3f81e3bcc96cb6ba2d7c4ade Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 02:51:12 +0100 Subject: [PATCH 29/74] removing __pycache__ --- tests/__pycache__/__init__.cpython-38.pyc | Bin 148 -> 0 bytes .../test_tembo.cpython-38-pytest-6.2.5.pyc | Bin 150 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/__pycache__/__init__.cpython-38.pyc delete mode 100644 tests/__pycache__/test_tembo.cpython-38-pytest-6.2.5.pyc diff --git a/tests/__pycache__/__init__.cpython-38.pyc b/tests/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index b550fe0f02174128a0407f6693b2cad3bd046cc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 148 zcmWIL<>g`kf=7L&i6Hthh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6vKKO;XkRX?R9 zKQ|{cuQ)$XKRvTVwg`k0-^rWL=gQLL?8o3AjbiSi&=m~3PUi1CZpd Date: Mon, 25 Oct 2021 02:51:22 +0100 Subject: [PATCH 30/74] updating todo --- TODO.todo | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.todo b/TODO.todo index cbe844e..cfa36ec 100644 --- a/TODO.todo +++ b/TODO.todo @@ -3,6 +3,7 @@ Functionality: ☐ Have an `--example` flag to `new` that prints an example given in the `config.yml` ☐ Should be a `tembo new --list` to list all possible names. ☐ When template not found, raise a Tembo error + ☐ Convert spaces to underscores in filepath currently raises jinja2.exceptions.TemplateNotFound on line 62, in _load_template ☐ Add update notification? check pypi for latest version and compare to current From d5879f711fffa5dc23a4717b2329fa6c3951d2c9 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 03:46:11 +0100 Subject: [PATCH 31/74] adding latest --- tembo/cli.py | 3 +-- tembo/journal/pages.py | 52 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/tembo/cli.py b/tembo/cli.py index 1978be1..e5810e6 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -46,7 +46,6 @@ def new(scope, inputs, dry_run): template_filename=str(user_scope["template_filename"]) ) scoped_page.save_to_disk(dry_run=dry_run) - tembo.logger.info("Saved %s to disk", scoped_page.path) raise SystemExit(0) if not _name_found and len(tembo.CONFIG.scopes) > 0: tembo.logger.warning("Command %s not found in config.yml - exiting", scope) @@ -63,5 +62,5 @@ run.add_command(new) if __name__ == "__main__": # BUG: fix this bug where input tokens are mismatched # new(["meeting", "robs presentation", "meeting on gcp"]) - new(["meeting", "a", "b", "c"]) + new(["meeting", "a", "b", "c", "d"]) # new(["meeting", "robs presentation"]) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index f27afd1..98a8379 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -68,6 +68,7 @@ class ScopedPageCreator(PageCreator): self.page_path = "" self.filename = "" self.extension = "" + self._all_input_tokens: list[str] = [] # TODO: rename these to input tokens + more sensible self.path_input_token_counts = {"config": 0, "user": 0} self.template_input_token_counts = {"config": 0, "user": 0} @@ -87,6 +88,11 @@ class ScopedPageCreator(PageCreator): self.filename = filename self.extension = extension + # verify the user input matches the number of input tokens in the + # config/templates + self._all_input_tokens = self.__get_input_tokens(template_filename) + self.__verify_input_tokens(user_input) + # get the path of the scoped page path = self._convert_to_path( self.base_path, self.page_path, self.filename, self.extension @@ -110,6 +116,40 @@ class ScopedPageCreator(PageCreator): self.__check_input_token_mismatch() return ScopedPage(path, template_contents) + def __get_input_tokens(self, template_filename: str | None) -> list[str]: + path = str( + pathlib.Path( + self.base_path, self.page_path, self.filename, self.extension + ).expanduser() + ) + if template_filename is not None: + template_contents = self._load_template(self.base_path, template_filename) + else: + template_contents = "" + # 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, user_input: Tuple[str, ...] | Tuple[()]) -> None: + if len(self._all_input_tokens) != len(user_input): + logger.critical( + "Your config/template specifies %s input tokens, you gave %s", + len(self._all_input_tokens), + len(user_input), + ) + raise SystemExit(1) + + def __substitute_input_tokens__( + self, + tokenified_string: str, + user_input: Tuple[str, ...] | Tuple[()], + ) -> str: + for input_value, extracted_token in zip(user_input, self._all_input_tokens): + tokenified_string = tokenified_string.replace(extracted_token, input_value) + return tokenified_string + def __check_input_token_mismatch(self) -> None: _max_config_input_token_count = max( self.path_input_token_counts["config"], @@ -142,9 +182,12 @@ class ScopedPageCreator(PageCreator): input_token_type: Literal["path", "template"], ) -> str: """For a tokened string, substitute input, name and date tokens.""" - - tokenified_string = self.__substitute_input_tokens( - tokenified_string, user_input, input_token_type + # TODO: fn to get tokens from file and template + # tokenified_string = self.__substitute_input_tokens( + # tokenified_string, user_input, input_token_type + # ) + tokenified_string = self.__substitute_input_tokens__( + tokenified_string, user_input ) tokenified_string = self.__substitute_name_tokens(tokenified_string, name) tokenified_string = self.__substitute_date_tokens(tokenified_string) @@ -247,9 +290,10 @@ class ScopedPage(Page): if not scoped_note_file.exists(): with scoped_note_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) + logger.info("Saved %s to disk", self.path) else: logger.info("%s already exists - skipping.", self.path) - raise SystemExit(0) + raise SystemExit(0) if __name__ == "__main__": From 794e6bf8fa89ed453d68bb9274ff0edfb4e2814a Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 04:24:28 +0100 Subject: [PATCH 32/74] adding latest --- TODO.todo | 2 + tembo/journal/pages.py | 124 +++++++++-------------------------------- 2 files changed, 29 insertions(+), 97 deletions(-) diff --git a/TODO.todo b/TODO.todo index cfa36ec..0f5c56b 100644 --- a/TODO.todo +++ b/TODO.todo @@ -46,3 +46,5 @@ Logging: ☐ ~/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 diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 98a8379..8609377 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod import pathlib import re -from typing import Tuple, Literal +from typing import Tuple import jinja2 import pendulum @@ -69,9 +69,6 @@ class ScopedPageCreator(PageCreator): self.filename = "" self.extension = "" self._all_input_tokens: list[str] = [] - # TODO: rename these to input tokens + more sensible - self.path_input_token_counts = {"config": 0, "user": 0} - self.template_input_token_counts = {"config": 0, "user": 0} def create_page( self, @@ -88,10 +85,10 @@ class ScopedPageCreator(PageCreator): self.filename = filename self.extension = extension - # verify the user input matches the number of input tokens in the + # verify the user input length matches the number of input tokens in the # config/templates - self._all_input_tokens = self.__get_input_tokens(template_filename) - self.__verify_input_tokens(user_input) + self._all_input_tokens = self._get_input_tokens(template_filename) + self._verify_input_tokens(user_input) # get the path of the scoped page path = self._convert_to_path( @@ -100,7 +97,7 @@ class ScopedPageCreator(PageCreator): # substitute tokens in the filepath path = pathlib.Path( - self._substitute_tokens(str(path), user_input, name, "path") + self._substitute_tokens(str(path), user_input, name) ) # get the template file @@ -108,15 +105,14 @@ class ScopedPageCreator(PageCreator): # load the template file contents and substitute tokens template_contents = self._load_template(self.base_path, template_filename) template_contents = self._substitute_tokens( - template_contents, user_input, name, "template" + template_contents, user_input, name ) else: template_contents = "" - self.__check_input_token_mismatch() return ScopedPage(path, template_contents) - def __get_input_tokens(self, template_filename: str | None) -> list[str]: + def _get_input_tokens(self, template_filename: str | None) -> list[str]: path = str( pathlib.Path( self.base_path, self.page_path, self.filename, self.extension @@ -132,7 +128,7 @@ class ScopedPageCreator(PageCreator): all_input_tokens.extend(re.findall(r"(\{input\d*\})", tokenified_string)) return sorted(all_input_tokens) - def __verify_input_tokens(self, user_input: Tuple[str, ...] | Tuple[()]) -> None: + def _verify_input_tokens(self, user_input: Tuple[str, ...] | Tuple[()]) -> None: if len(self._all_input_tokens) != len(user_input): logger.critical( "Your config/template specifies %s input tokens, you gave %s", @@ -141,7 +137,25 @@ class ScopedPageCreator(PageCreator): ) raise SystemExit(1) - def __substitute_input_tokens__( + def _substitute_tokens( + self, + tokenified_string: str, + user_input: Tuple[str, ...] | Tuple[()], + name: str, + ) -> str: + """For a tokened string, substitute input, name and date tokens.""" + # TODO: fn to get tokens from file and template + # tokenified_string = self.__substitute_input_tokens( + # tokenified_string, user_input, input_token_type + # ) + tokenified_string = self.__substitute_input_tokens( + tokenified_string, user_input + ) + tokenified_string = self.__substitute_name_tokens(tokenified_string, name) + tokenified_string = self.__substitute_date_tokens(tokenified_string) + return tokenified_string + + def __substitute_input_tokens( self, tokenified_string: str, user_input: Tuple[str, ...] | Tuple[()], @@ -150,49 +164,6 @@ class ScopedPageCreator(PageCreator): tokenified_string = tokenified_string.replace(extracted_token, input_value) return tokenified_string - def __check_input_token_mismatch(self) -> None: - _max_config_input_token_count = max( - self.path_input_token_counts["config"], - self.template_input_token_counts["config"], - ) - _max_user_input_token_count = max( - self.path_input_token_counts["user"], - self.template_input_token_counts["user"], - ) - if _max_user_input_token_count < _max_config_input_token_count: - logger.critical( - "Your config/template specifies %s input tokens, you gave %s " - "- exiting", - _max_config_input_token_count, - _max_user_input_token_count, - ) - raise SystemExit(1) - if _max_user_input_token_count > _max_config_input_token_count: - logger.warning( - "Your config/template specifies %s input tokens, you gave %s", - _max_config_input_token_count, - _max_user_input_token_count, - ) - - def _substitute_tokens( - self, - tokenified_string: str, - user_input: Tuple[str, ...] | Tuple[()], - name: str, - input_token_type: Literal["path", "template"], - ) -> str: - """For a tokened string, substitute input, name and date tokens.""" - # TODO: fn to get tokens from file and template - # tokenified_string = self.__substitute_input_tokens( - # tokenified_string, user_input, input_token_type - # ) - tokenified_string = self.__substitute_input_tokens__( - tokenified_string, user_input - ) - tokenified_string = self.__substitute_name_tokens(tokenified_string, name) - tokenified_string = self.__substitute_date_tokens(tokenified_string) - return tokenified_string - @staticmethod def __substitute_name_tokens(tokenified_string: str, name: str) -> str: # find any {name} tokens and substitute for the name value @@ -201,47 +172,6 @@ class ScopedPageCreator(PageCreator): tokenified_string = tokenified_string.replace(extracted_input, name) return tokenified_string - # @staticmethod - def __substitute_input_tokens( - self, - tokenified_string: str, - user_input: Tuple[str, ...] | Tuple[()], - input_token_type: Literal["path", "template"], - ) -> str: - """Find `{inputN}` tokens in string.""" - - input_extraction = re.findall(r"(\{input\d*\})", tokenified_string) - # if there is no user input - if len(user_input) == 0: - if len(input_extraction) > 0: - # if the regex matches, save the number of input tokens found - if input_token_type == "path": # noqa: bandit 105 - # TODO: change this to a dict instead of tuple - self.path_input_token_counts["config"] = len(input_extraction) - self.path_input_token_counts["user"] = 0 - if input_token_type == "template": # noqa: bandit 105 - self.template_input_token_counts["config"] = len(input_extraction) - self.template_input_token_counts["user"] = 0 - # if there aren't any tokens in the string, return the string - return tokenified_string - - # if there is user input - if len(user_input) > 0: - # save the number of input tokens, and the number of user inputs - if input_token_type == "path": # noqa: bandit 105 - self.path_input_token_counts["config"] = len(input_extraction) - self.path_input_token_counts["user"] = len(user_input) - elif input_token_type == "template": # noqa: bandit 105 - self.template_input_token_counts["config"] = len(input_extraction) - self.template_input_token_counts["user"] = len(user_input) - - # sbustitute the input token for the user's input - for extracted_input, input_value in zip(input_extraction, user_input): - tokenified_string = tokenified_string.replace( - extracted_input, input_value - ) - return tokenified_string - @staticmethod def __substitute_date_tokens(tokenified_string: str) -> str: """Find any {d:%d-%M-%Y} tokens.""" From f91f83874d1c7b9f0d2b78f5b5e7f3f47723309a Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 05:38:40 +0100 Subject: [PATCH 33/74] adding latest working --- TODO.todo | 38 ++++++++++++-------- tembo/cli.py | 79 +++++++++++++++++++++++++++++++++--------- tembo/journal/pages.py | 40 +++++++++++---------- 3 files changed, 107 insertions(+), 50 deletions(-) diff --git a/TODO.todo b/TODO.todo index 0f5c56b..0952f16 100644 --- a/TODO.todo +++ b/TODO.todo @@ -1,20 +1,18 @@ +Priority: + ☐ Go through code TODOs + ☐ Check code order and make sure things are where they should be + ☐ Document the python/logging/typing in Trilium + ☐ Write the tests + Functionality: - ☐ Handle case where there are no scopes in the config and command is invoked. - ☐ Have an `--example` flag to `new` that prints an example given in the `config.yml` - ☐ Should be a `tembo new --list` to list all possible names. - ☐ When template not found, raise a Tembo error - ☐ Convert spaces to underscores in filepath - currently raises jinja2.exceptions.TemplateNotFound on line 62, in _load_template - ☐ Add update notification? - check pypi for latest version and compare to current - ✔ `TEMBO_CONFIG` should follow same pattern as other env vars and be a python string when read in @done(21-10-24 05:31) + ☐ a Bug: - ☐ tokens - Say we have input0 and input3 in file path - and we have input1 and input2 in template - it only recognises this as 2 inputs total, not four. - passing in tembo new meeting a b will work and input0=input1, input3=input2 + ☐ a + +Tests: + ☐ Write tests! @2d + Use coverage as going along to make sure all bases are covered in the testing VSCode: ☐ Look at @@ -24,7 +22,6 @@ Logging: Documentation: ☐ Document usage of Panaetius in a module Using the logger, initialising with the config path etc - ✘ Uses Pendulum tokens: https://pendulum.eustace.io/docs/#tokens @cancelled(21-10-24 05:32) ☐ Uses `strftime` tokens: ☐ Document latest typing. ☐ Using from `__future__` with `|` @@ -48,3 +45,14 @@ Logging: ☐ 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: + ✔ 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/tembo/cli.py b/tembo/cli.py index e5810e6..1cc3de7 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -14,6 +14,17 @@ def run(): """ +@click.command(options_metavar="") +def list_all(): + r"""List all names for "tembo new ".""" + _all_scopes = [user_scope["name"] for user_scope in tembo.CONFIG.scopes] + tembo.logger.info( + "%s names found in config.yml: '%s'", len(_all_scopes), "', '".join(_all_scopes) + ) + raise SystemExit(0) + + +# TODO: organise this so all flags are at the top and in order @click.command(options_metavar="") @click.argument("scope", metavar="") @click.argument( @@ -22,9 +33,10 @@ def run(): metavar="", ) @click.option("--dry-run", is_flag=True, default=False) -def new(scope, inputs, dry_run): +@click.option("--example", is_flag=True, default=False) +def new(scope, inputs, dry_run, example): """ - Create a new note. + Create a new page. The name of the scope in the Tembo config.yml. @@ -32,21 +44,53 @@ def new(scope, inputs, dry_run): Example: tembo new meeting my_presentation """ - _name_found = scope in [user_scope["name"] for user_scope in tembo.CONFIG.scopes] + try: + _name_found = scope in [ + user_scope["name"] for user_scope in tembo.CONFIG.scopes + ] + except TypeError as type_error: + tembo.logger.critical("No scopes found in config.yml - exiting") + raise SystemExit(1) from type_error + + try: + config_scope = [ + ( + str(user_scope["name"]), + user_scope.get("example"), + str(user_scope["path"]), + str(user_scope["filename"]), + str(user_scope["extension"]), + user_scope.get("template_filename"), + ) + for user_scope in tembo.CONFIG.scopes + if user_scope["name"] == scope + ] + except KeyError as key_error: + tembo.logger.critical("Key %s not found in config.yml - exiting", key_error) + raise SystemExit(1) from key_error + + if example: + tembo.logger.info( + "Example for 'tembo new %s': %s", + config_scope[0][0], + config_scope[0][1] + if isinstance(config_scope[0][1], str) + else "No example in config.yml", + ) + raise SystemExit(0) + if _name_found: - for user_scope in tembo.CONFIG.scopes: - if user_scope["name"] == scope: - scoped_page = pages.ScopedPageCreator().create_page( - base_path=str(tembo.CONFIG.base_path), - page_path=str(user_scope["path"]), - filename=str(user_scope["filename"]), - extension=str(user_scope["extension"]), - name=str(user_scope["name"]), - user_input=inputs, - template_filename=str(user_scope["template_filename"]) - ) - scoped_page.save_to_disk(dry_run=dry_run) - raise SystemExit(0) + scoped_page = pages.ScopedPageCreator().create_page( + base_path=str(tembo.CONFIG.base_path), + page_path=config_scope[0][2], + filename=config_scope[0][3], + extension=config_scope[0][4], + name=config_scope[0][0], + user_input=inputs, + template_filename=config_scope[0][5], + ) + scoped_page.save_to_disk(dry_run=dry_run) + raise SystemExit(0) if not _name_found and len(tembo.CONFIG.scopes) > 0: tembo.logger.warning("Command %s not found in config.yml - exiting", scope) raise SystemExit(0) @@ -57,10 +101,11 @@ def new(scope, inputs, dry_run): run.add_command(new) +run.add_command(list_all) if __name__ == "__main__": # BUG: fix this bug where input tokens are mismatched # new(["meeting", "robs presentation", "meeting on gcp"]) - new(["meeting", "a", "b", "c", "d"]) + new(["meeting", "a", "b", "c", "d", "--example"]) # new(["meeting", "robs presentation"]) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 8609377..eeb42bc 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -8,7 +8,7 @@ from typing import Tuple import jinja2 import pendulum -from tembo import logger, CONFIG +import tembo class PageCreator: @@ -31,26 +31,30 @@ class PageCreator: ) -> pathlib.Path: # check if Tembo base path exists if not pathlib.Path(base_path).expanduser().exists(): - logger.critical("Tembo base path of %s does not exist - exiting", base_path) + tembo.logger.critical( + "Tembo base path of %s does not exist - exiting", base_path + ) raise SystemExit(1) path_to_file = ( pathlib.Path(base_path).expanduser() - / pathlib.Path(page_path).expanduser() - / filename + / pathlib.Path(page_path.replace(" ", "_")).expanduser() + / filename.replace(" ", "_") ) try: - # check for existing `.` in filename extension + # check for existing `.` in the extension extension = extension[1:] if extension[0] == "." else extension except IndexError: - # return paths without a file + # IndexError means the path is not a file, just a path return path_to_file # return path with a file return path_to_file.with_suffix(f".{extension}") def _load_template(self, base_path: str, template_filename: str) -> str: - if CONFIG.template_path is not None: + if tembo.CONFIG.template_path is not None: # check for overriden template_path - template_path = self._convert_to_path("", CONFIG.template_path, "", "") + template_path = self._convert_to_path( + "", tembo.CONFIG.template_path, "", "" + ) else: # default template_path is base_path / templates template_path = self._convert_to_path(base_path, ".templates", "", "") @@ -86,7 +90,7 @@ class ScopedPageCreator(PageCreator): self.extension = extension # verify the user input length matches the number of input tokens in the - # config/templates + # tembo.config/templates self._all_input_tokens = self._get_input_tokens(template_filename) self._verify_input_tokens(user_input) @@ -96,9 +100,7 @@ class ScopedPageCreator(PageCreator): ) # substitute tokens in the filepath - path = pathlib.Path( - self._substitute_tokens(str(path), user_input, name) - ) + path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) # get the template file if template_filename is not None: @@ -130,8 +132,8 @@ class ScopedPageCreator(PageCreator): def _verify_input_tokens(self, user_input: Tuple[str, ...] | Tuple[()]) -> None: if len(self._all_input_tokens) != len(user_input): - logger.critical( - "Your config/template specifies %s input tokens, you gave %s", + tembo.logger.critical( + "Your tembo.config/template specifies %s input tokens, you gave %s", len(self._all_input_tokens), len(user_input), ) @@ -161,7 +163,9 @@ class ScopedPageCreator(PageCreator): user_input: Tuple[str, ...] | Tuple[()], ) -> str: for input_value, extracted_token in zip(user_input, self._all_input_tokens): - tokenified_string = tokenified_string.replace(extracted_token, input_value) + tokenified_string = tokenified_string.replace( + extracted_token, input_value.replace(" ", "_") + ) return tokenified_string @staticmethod @@ -212,7 +216,7 @@ class ScopedPage(Page): def save_to_disk(self, dry_run: bool = False) -> None: if dry_run: - logger.info("%s will be created", self.path) + tembo.logger.info("%s will be created", self.path) raise SystemExit(0) # create the parent directories scoped_note_file = pathlib.Path(self.path) @@ -220,9 +224,9 @@ class ScopedPage(Page): if not scoped_note_file.exists(): with scoped_note_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) - logger.info("Saved %s to disk", self.path) + tembo.logger.info("Saved %s to disk", self.path) else: - logger.info("%s already exists - skipping.", self.path) + tembo.logger.info("%s already exists - skipping.", self.path) raise SystemExit(0) From 1df6e3f2eade49b4589f1736afd07f0e963dc257 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 06:04:32 +0100 Subject: [PATCH 34/74] adding latest --- TODO.todo | 5 ++--- tembo/cli.py | 13 ++++++++++--- tembo/journal/pages.py | 17 +++++++++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/TODO.todo b/TODO.todo index 0952f16..2e2e531 100644 --- a/TODO.todo +++ b/TODO.todo @@ -1,14 +1,13 @@ Priority: - ☐ Go through code TODOs + ✔ Go through code TODOs @done(21-10-25 05:52) ☐ Check code order and make sure things are where they should be ☐ Document the python/logging/typing in Trilium ☐ Write the tests Functionality: - ☐ a + ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages Bug: - ☐ a Tests: ☐ Write tests! @2d diff --git a/tembo/cli.py b/tembo/cli.py index 1cc3de7..f37f6f8 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -16,7 +16,7 @@ def run(): @click.command(options_metavar="") def list_all(): - r"""List all names for "tembo new ".""" + """List all names for 'tembo new '.""" _all_scopes = [user_scope["name"] for user_scope in tembo.CONFIG.scopes] tembo.logger.info( "%s names found in config.yml: '%s'", len(_all_scopes), "', '".join(_all_scopes) @@ -24,7 +24,6 @@ def list_all(): raise SystemExit(0) -# TODO: organise this so all flags are at the top and in order @click.command(options_metavar="") @click.argument("scope", metavar="") @click.argument( @@ -44,14 +43,17 @@ def new(scope, inputs, dry_run, example): Example: tembo new meeting my_presentation """ + # get the name from the tembo config.yml try: _name_found = scope in [ user_scope["name"] for user_scope in tembo.CONFIG.scopes ] except TypeError as type_error: + # raise error if no scopes are defined tembo.logger.critical("No scopes found in config.yml - exiting") raise SystemExit(1) from type_error + # get the scope information from the tembo config.yml try: config_scope = [ ( @@ -66,9 +68,11 @@ def new(scope, inputs, dry_run, example): if user_scope["name"] == scope ] except KeyError as key_error: + # raise error if any non optional keys are missing tembo.logger.critical("Key %s not found in config.yml - exiting", key_error) raise SystemExit(1) from key_error + # print the example to the user if example: tembo.logger.info( "Example for 'tembo new %s': %s", @@ -79,6 +83,7 @@ def new(scope, inputs, dry_run, example): ) raise SystemExit(0) + # if the name is in the config.yml, create the scoped page if _name_found: scoped_page = pages.ScopedPageCreator().create_page( base_path=str(tembo.CONFIG.base_path), @@ -92,8 +97,11 @@ def new(scope, inputs, dry_run, example): scoped_page.save_to_disk(dry_run=dry_run) raise SystemExit(0) if not _name_found and len(tembo.CONFIG.scopes) > 0: + # if the name is missing in the config.yml, raise error tembo.logger.warning("Command %s not found in config.yml - exiting", scope) raise SystemExit(0) + + # raise error if no config.yml found tembo.logger.critical( "No config.yml found in %s - exiting", tembo.CONFIG.config_path ) @@ -105,7 +113,6 @@ run.add_command(list_all) if __name__ == "__main__": - # BUG: fix this bug where input tokens are mismatched # new(["meeting", "robs presentation", "meeting on gcp"]) new(["meeting", "a", "b", "c", "d", "--example"]) # new(["meeting", "robs presentation"]) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index eeb42bc..db0620f 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -6,6 +6,7 @@ import re from typing import Tuple import jinja2 +from jinja2.exceptions import TemplateNotFound import pendulum import tembo @@ -62,7 +63,14 @@ class PageCreator: file_loader = jinja2.FileSystemLoader(template_path) env = jinja2.Environment(loader=file_loader, autoescape=True) # load the template contents - loaded_template = env.get_template(template_filename) + try: + loaded_template = env.get_template(template_filename) + except TemplateNotFound as template_not_found: + tembo.logger.critical( + "Template file %s not found - exiting", + str(template_path) + "/" + str(template_not_found.message), + ) + raise SystemExit(1) from template_not_found return loaded_template.render() @@ -90,7 +98,7 @@ class ScopedPageCreator(PageCreator): self.extension = extension # verify the user input length matches the number of input tokens in the - # tembo.config/templates + # tembo config/templates self._all_input_tokens = self._get_input_tokens(template_filename) self._verify_input_tokens(user_input) @@ -146,10 +154,6 @@ class ScopedPageCreator(PageCreator): name: str, ) -> str: """For a tokened string, substitute input, name and date tokens.""" - # TODO: fn to get tokens from file and template - # tokenified_string = self.__substitute_input_tokens( - # tokenified_string, user_input, input_token_type - # ) tokenified_string = self.__substitute_input_tokens( tokenified_string, user_input ) @@ -163,6 +167,7 @@ class ScopedPageCreator(PageCreator): user_input: Tuple[str, ...] | Tuple[()], ) -> str: for input_value, extracted_token in zip(user_input, self._all_input_tokens): + # REVIEW: test this for spaces in the filename/input token tokenified_string = tokenified_string.replace( extracted_token, input_value.replace(" ", "_") ) From 7da751dc1708f674a9700cba55e1515d20d75d22 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 14:33:49 +0100 Subject: [PATCH 35/74] adding version option --- tembo/__init__.py | 2 ++ tembo/cli.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/tembo/__init__.py b/tembo/__init__.py index cd93fcd..c688a8d 100644 --- a/tembo/__init__.py +++ b/tembo/__init__.py @@ -3,6 +3,8 @@ 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) else: diff --git a/tembo/cli.py b/tembo/cli.py index f37f6f8..8740d44 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -8,6 +8,13 @@ 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. From d5831d1b757130444ba6cbcdf7b729f52f4e7d1e Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 14:33:57 +0100 Subject: [PATCH 36/74] updating lock file --- poetry.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3c77c43..304ec56 100644 --- a/poetry.lock +++ b/poetry.lock @@ -101,7 +101,7 @@ flake8 = "*" [[package]] name = "gitdb" -version = "4.0.8" +version = "4.0.9" description = "Git Object Database" category = "dev" optional = false @@ -441,7 +441,7 @@ pylint = ">=1.7" [[package]] name = "pyparsing" -version = "3.0.0" +version = "3.0.1" description = "Python parsing module" category = "dev" optional = false @@ -622,8 +622,8 @@ flake8-polyfill = [ {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, ] gitdb = [ - {file = "gitdb-4.0.8-py3-none-any.whl", hash = "sha256:6875cbaed01f1b750394f372607803768fc7dad7c58c7ceb5f5917e980d779b2"}, - {file = "gitdb-4.0.8.tar.gz", hash = "sha256:858966a9310649cb24a387c101429bb5a1110068a312517722b0281077e78bc6"}, + {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"}, @@ -856,8 +856,8 @@ pylint-plugin-utils = [ {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, ] pyparsing = [ - {file = "pyparsing-3.0.0-py3-none-any.whl", hash = "sha256:d487599e9fb0dc36bee6b5c183c6fc5bd372ce667736f3d430ab7d842a54a35a"}, - {file = "pyparsing-3.0.0.tar.gz", hash = "sha256:001cad8d467e7a9248ef9fd513f5c0d39afcbcb9a43684101853bd0ab962e479"}, + {file = "pyparsing-3.0.1-py3-none-any.whl", hash = "sha256:fd93fc45c47893c300bd98f5dd1b41c0e783eaeb727e7cea210dcc09d64ce7c3"}, + {file = "pyparsing-3.0.1.tar.gz", hash = "sha256:84196357aa3566d64ad123d7a3c67b0e597a115c4934b097580e5ce220b91531"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, From 5348fb72e41fa9bc176e74d7b434cf3de095ca4f Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 14:34:15 +0100 Subject: [PATCH 37/74] moving get template logic into own method --- tembo/journal/pages.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index db0620f..8fbf21f 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -51,13 +51,13 @@ class PageCreator: return path_to_file.with_suffix(f".{extension}") def _load_template(self, base_path: str, template_filename: str) -> str: + # check for overriden template_path if tembo.CONFIG.template_path is not None: - # check for overriden template_path template_path = self._convert_to_path( "", tembo.CONFIG.template_path, "", "" ) else: - # default template_path is base_path / templates + # default template_path is base_path / .templates template_path = self._convert_to_path(base_path, ".templates", "", "") # load the template folder file_loader = jinja2.FileSystemLoader(template_path) @@ -75,6 +75,15 @@ class PageCreator: 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) -> None: self.base_path = "" self.page_path = "" @@ -111,15 +120,12 @@ class ScopedPageCreator(PageCreator): path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) # get the template file + template_contents = self._get_template_contents(template_filename) + # substitute tokens in template_contents if template_filename is not None: - # load the template file contents and substitute tokens - template_contents = self._load_template(self.base_path, template_filename) template_contents = self._substitute_tokens( template_contents, user_input, name ) - else: - template_contents = "" - return ScopedPage(path, template_contents) def _get_input_tokens(self, template_filename: str | None) -> list[str]: @@ -128,10 +134,7 @@ class ScopedPageCreator(PageCreator): self.base_path, self.page_path, self.filename, self.extension ).expanduser() ) - if template_filename is not None: - template_contents = self._load_template(self.base_path, template_filename) - else: - template_contents = "" + template_contents = self._get_template_contents(template_filename) # get the input tokens from both the path and the template all_input_tokens = [] for tokenified_string in (path, template_contents): @@ -147,6 +150,13 @@ class ScopedPageCreator(PageCreator): ) raise SystemExit(1) + def _get_template_contents(self, template_filename: str | None) -> str: + return ( + self._load_template(self.base_path, template_filename) + if template_filename is not None + else "" + ) + def _substitute_tokens( self, tokenified_string: str, From 293a4a1329a8708ee71565f5d6dc4886279a68d4 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 14:34:20 +0100 Subject: [PATCH 38/74] updating TODO --- TODO.todo | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/TODO.todo b/TODO.todo index 2e2e531..a6483f4 100644 --- a/TODO.todo +++ b/TODO.todo @@ -1,13 +1,10 @@ Priority: - ✔ Go through code TODOs @done(21-10-25 05:52) - ☐ Check code order and make sure things are where they should be - ☐ Document the python/logging/typing in Trilium + ✔ Document the python/logging/typing in Trilium @done(21-10-25 14:33) ☐ Write the tests + ☐ Docstrings Functionality: - ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages - -Bug: + ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? Tests: ☐ Write tests! @2d @@ -19,21 +16,7 @@ VSCode: Logging: Documentation: - ☐ Document usage of Panaetius in a module - Using the logger, initialising with the config path etc - ☐ Uses `strftime` tokens: - ☐ Document latest typing. - ☐ Using from `__future__` with `|` - ☐ `using Tuple[str, ...]` - ☐ `Sequence` vs `Collection` - ☐ Document how to do docstrings in python. Don't document `__init__` do it in class. - Should update the default gist to hide the `__init__` messages - ☐ Document using jinja2 briefly and link to Tembo (link to ) - - Logging: - ☐ How to raise + debug an exception? - ☐ Document how to raise a logger.critical instead of exception - in a try, except you can just do logger.critical(exec_info=1) to print the stack + ☐ a Tembo: ☐ Document creating new Tembo config @@ -46,6 +29,19 @@ Logging: clone the repo, delete .git, git init, configure and add git origin Archive: + ✔ 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) From 557eec305dcc555dbcc9f80d8d88d93c60ad00fd Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 25 Oct 2021 22:52:48 +0100 Subject: [PATCH 39/74] updating docstrings --- TODO.todo | 6 ++++-- tembo/journal/pages.py | 47 +++++++++++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/TODO.todo b/TODO.todo index a6483f4..1473de3 100644 --- a/TODO.todo +++ b/TODO.todo @@ -1,5 +1,7 @@ Priority: ✔ Document the python/logging/typing in Trilium @done(21-10-25 14:33) + ☐ Update typing annotations to include generics instead + https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes ☐ Write the tests ☐ Docstrings @@ -8,7 +10,7 @@ Functionality: Tests: ☐ Write tests! @2d - Use coverage as going along to make sure all bases are covered in the testing + Use coverage as going along to make sure all bases are covered in the testing VSCode: ☐ Look at @@ -26,7 +28,7 @@ Logging: ☐ ~/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 + clone the repo, delete .git, git init, configure and add git origin Archive: ✔ Go through code TODOs @done(21-10-25 05:52) @project(Priority) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 8fbf21f..96f5a4a 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod import pathlib import re -from typing import Tuple +from typing import Collection import jinja2 from jinja2.exceptions import TemplateNotFound @@ -21,7 +21,7 @@ class PageCreator: filename: str, extension: str, name: str, - user_input: Tuple[str, ...] | Tuple[()], + user_input: Collection[str], template_filename: str | None = None, ) -> Page: raise NotImplementedError @@ -80,8 +80,8 @@ class ScopedPageCreator(PageCreator): 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 + filename (str): filename relative to the page path. + extension (str): extension of file. """ def __init__(self) -> None: @@ -98,7 +98,7 @@ class ScopedPageCreator(PageCreator): filename: str, extension: str, name: str, - user_input: Tuple[str, ...] | Tuple[()], + user_input: Collection[str], template_filename: str | None = None, ) -> Page: self.base_path = base_path @@ -141,7 +141,7 @@ class ScopedPageCreator(PageCreator): all_input_tokens.extend(re.findall(r"(\{input\d*\})", tokenified_string)) return sorted(all_input_tokens) - def _verify_input_tokens(self, user_input: Tuple[str, ...] | Tuple[()]) -> None: + def _verify_input_tokens(self, user_input: Collection[str]) -> None: if len(self._all_input_tokens) != len(user_input): tembo.logger.critical( "Your tembo.config/template specifies %s input tokens, you gave %s", @@ -160,7 +160,7 @@ class ScopedPageCreator(PageCreator): def _substitute_tokens( self, tokenified_string: str, - user_input: Tuple[str, ...] | Tuple[()], + user_input: Collection[str], name: str, ) -> str: """For a tokened string, substitute input, name and date tokens.""" @@ -174,7 +174,7 @@ class ScopedPageCreator(PageCreator): def __substitute_input_tokens( self, tokenified_string: str, - user_input: Tuple[str, ...] | Tuple[()], + user_input: Collection[str], ) -> str: for input_value, extracted_token in zip(user_input, self._all_input_tokens): # REVIEW: test this for spaces in the filename/input token @@ -185,7 +185,8 @@ class ScopedPageCreator(PageCreator): @staticmethod def __substitute_name_tokens(tokenified_string: str, name: str) -> str: - # find any {name} tokens and substitute for the name value + """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, name) @@ -220,9 +221,20 @@ class Page(metaclass=ABCMeta): class ScopedPage(Page): - """A Page that uses substitute tokens.""" + """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 @@ -230,6 +242,21 @@ class ScopedPage(Page): return f"ScopedPage({self.path})" def save_to_disk(self, dry_run: bool = False) -> None: + """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. + """ if dry_run: tembo.logger.info("%s will be created", self.path) raise SystemExit(0) From 5a63aef81e420dcc8eb0642419a1c153ba3b6eb5 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Tue, 26 Oct 2021 00:04:04 +0100 Subject: [PATCH 40/74] adding duty --- TODO.todo | 17 +++++++++++- duties.py | 16 ++++++++++++ poetry.lock | 71 +++++++++++++++++++++++++++++++++++++++++++++++--- pyproject.toml | 1 + 4 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 duties.py diff --git a/TODO.todo b/TODO.todo index 1473de3..e3f56b2 100644 --- a/TODO.todo +++ b/TODO.todo @@ -1,12 +1,27 @@ Priority: ✔ Document the python/logging/typing in Trilium @done(21-10-25 14:33) - ☐ Update typing annotations to include generics instead + ✔ Update typing annotations to include generics instead @done(21-10-25 22:38) https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes ☐ Write the tests + ☐ test logs: + document this + ☐ ☐ Docstrings +Documentation: + ☐ Write documentation using `mkdocs` + ☐ Look at how to use github actions + Use for an example + ☐ Build the docs using a github action. + Functionality: ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? + ☐ Use the python runner + + ☐ Run tests + ☐ Update poetry + ☐ Build docs + ☐ Document using Duty Tests: ☐ Write tests! @2d diff --git a/duties.py b/duties.py new file mode 100644 index 0000000..62525c7 --- /dev/null +++ b/duties.py @@ -0,0 +1,16 @@ +from duty import duty + + +@duty +def test(ctx): + ctx.run(["echo", "test"], title="test command") + + +@duty +def update_deps(ctx, dry: bool = True): + # 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}", + ) diff --git a/poetry.lock b/poetry.lock index 304ec56..51968c7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,21 @@ +[[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.3" +version = "2.8.4" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -75,6 +90,30 @@ 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" @@ -333,6 +372,14 @@ 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" @@ -582,12 +629,16 @@ 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 = "a1db17ea6defe48d6d640c87c94f57997dd769ae5d5bdc448dee53e57cb9ffd8" +content-hash = "0a1cc3ae7d658d6838c81fd0258ad92e67340c519b3d0910835453bac0fec906" [metadata.files] +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.3-py3-none-any.whl", hash = "sha256:f9d66e3a4a0e5b52819b2ff41ac2b179df9d180697db71c92beb33a60c661794"}, - {file = "astroid-2.8.3.tar.gz", hash = "sha256:0e361da0744d5011d4f5d57e64473ba9b7ab4da1e2d45d6631ebd67dd28c3cce"}, + {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"}, @@ -613,6 +664,14 @@ 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"}, @@ -821,6 +880,10 @@ 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"}, diff --git a/pyproject.toml b/pyproject.toml index b97e44f..8211cce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ panaetius = { path = "../panaetius", develop = true } [tool.poetry.dev-dependencies] pytest = "^6.2.5" prospector = { extras = ["with_bandit", "with_mypy"], version = "^1.5.1" } +duty = "^0.7.0" [build-system] requires = ["poetry-core>=1.0.0"] From a0afe628797492582af6c1ed70f95ea3b7f5cd6e Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Thu, 28 Oct 2021 20:10:58 +0100 Subject: [PATCH 41/74] add PyInstaller --- .python-version | 2 +- poetry.lock | 106 +++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/.python-version b/.python-version index 89a1ad7..73bb444 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.8.12 +3.8.11 diff --git a/poetry.lock b/poetry.lock index 51968c7..389d4c6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[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" @@ -138,6 +146,14 @@ 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" @@ -205,6 +221,17 @@ 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" @@ -282,6 +309,17 @@ 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" @@ -418,6 +456,33 @@ 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" @@ -537,6 +602,14 @@ 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" @@ -629,9 +702,13 @@ 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 = "0a1cc3ae7d658d6838c81fd0258ad92e67340c519b3d0910835453bac0fec906" +content-hash = "d34e6da39ccdc2c892d1e9c263384b3b0a3ea59aaf0efb57c0b7d52d23635b56" [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"}, @@ -680,6 +757,9 @@ 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"}, @@ -724,6 +804,10 @@ lazy-object-proxy = [ {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"}, @@ -837,6 +921,9 @@ 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"}, @@ -900,6 +987,19 @@ 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"}, @@ -934,6 +1034,10 @@ 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"}, diff --git a/pyproject.toml b/pyproject.toml index 8211cce..64759d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ panaetius = { path = "../panaetius", develop = true } pytest = "^6.2.5" prospector = { extras = ["with_bandit", "with_mypy"], version = "^1.5.1" } duty = "^0.7.0" +pyinstaller = "^4.5.1" [build-system] requires = ["poetry-core>=1.0.0"] From 4077ec1760639bc9b7c7a3caece5af8d8766b80e Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Thu, 28 Oct 2021 20:11:27 +0100 Subject: [PATCH 42/74] adding options class and tidying up --- TODO.todo | 23 ++++++-- tembo/cli.py | 55 +++++++++++-------- tembo/journal/pages.py | 91 ++++++++++++++++++-------------- tests/test_journal/test_pages.py | 0 4 files changed, 101 insertions(+), 68 deletions(-) create mode 100644 tests/test_journal/test_pages.py diff --git a/TODO.todo b/TODO.todo index e3f56b2..831fb56 100644 --- a/TODO.todo +++ b/TODO.todo @@ -5,24 +5,41 @@ 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 + ☐ Write documentation using `mkdocs` ☐ Look at how to use github actions Use for an example ☐ Build the docs using a github action. Functionality: + ✔ Move any `tembo.CONFIG` calls out of `pages.py` and ensure these are passed in from the cli. @done(21-10-28 19:44) + ✔ Make `config scope` a dict in `cli.py`. @done(21-10-28 19:44) + ☐ Add the `--example` output to the miscounted token message so the user knows the correct command to use. + ✔ Page options dataclass @done(21-10-28 20:09) ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? - ☐ Use the python runner + ☐ Use the python runner Duty ☐ Run tests ☐ Update poetry ☐ Build docs ☐ Document using Duty +VSCode: + PyInstaller: + ☐ Document build error: + PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.8.11 + ☐ Freeze a click app: + Tests: ☐ Write tests! @2d Use coverage as going along to make sure all bases are covered in the testing @@ -33,8 +50,6 @@ VSCode: Logging: Documentation: - ☐ a - Tembo: ☐ Document creating new Tembo config ☐ ~/tembo needs creating diff --git a/tembo/cli.py b/tembo/cli.py index 8740d44..3a9b071 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -1,9 +1,10 @@ +import sys + import click import tembo from tembo.journal import pages - CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -62,18 +63,19 @@ def new(scope, inputs, dry_run, example): # get the scope information from the tembo config.yml try: - config_scope = [ - ( - str(user_scope["name"]), - user_scope.get("example"), - str(user_scope["path"]), - str(user_scope["filename"]), - str(user_scope["extension"]), - user_scope.get("template_filename"), - ) + config_scope = { + option: str(user_scope[option]) + for option in [ + "name", + "example", + "path", + "filename", + "extension", + "template_filename", + ] for user_scope in tembo.CONFIG.scopes if user_scope["name"] == scope - ] + } except KeyError as key_error: # raise error if any non optional keys are missing tembo.logger.critical("Key %s not found in config.yml - exiting", key_error) @@ -83,24 +85,26 @@ def new(scope, inputs, dry_run, example): if example: tembo.logger.info( "Example for 'tembo new %s': %s", - config_scope[0][0], - config_scope[0][1] + config_scope["name"], + config_scope["example"] if isinstance(config_scope[0][1], str) else "No example in config.yml", ) raise SystemExit(0) # if the name is in the config.yml, create the scoped page + page_creator_options = pages.PageCreatorOptions( + base_path=tembo.CONFIG.base_path, + page_path=config_scope["path"], + filename=config_scope["filename"], + extension=config_scope["extension"], + name=config_scope["name"], + user_input=inputs, + template_filename=config_scope["template_filename"], + template_path=tembo.CONFIG.template_path, + ) if _name_found: - scoped_page = pages.ScopedPageCreator().create_page( - base_path=str(tembo.CONFIG.base_path), - page_path=config_scope[0][2], - filename=config_scope[0][3], - extension=config_scope[0][4], - name=config_scope[0][0], - user_input=inputs, - template_filename=config_scope[0][5], - ) + scoped_page = pages.ScopedPageCreator().create_page(page_creator_options) scoped_page.save_to_disk(dry_run=dry_run) raise SystemExit(0) if not _name_found and len(tembo.CONFIG.scopes) > 0: @@ -121,5 +125,10 @@ run.add_command(list_all) if __name__ == "__main__": # new(["meeting", "robs presentation", "meeting on gcp"]) - new(["meeting", "a", "b", "c", "d", "--example"]) + new(["meeting", "a", "b", "c", "d"]) # new(["meeting", "robs presentation"]) + + # pyinstaller + # if getattr(sys, "frozen", False): + # run(sys.argv[1:]) + # run(sys.argv[1:]) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 96f5a4a..20bb3e6 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -1,6 +1,7 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod +from dataclasses import dataclass import pathlib import re from typing import Collection @@ -12,18 +13,21 @@ import pendulum import tembo +@dataclass +class PageCreatorOptions: + base_path: str + page_path: str + filename: str + extension: str + name: str + user_input: Collection[str] + template_filename: str | None = None + template_path: str | None = None + + class PageCreator: @abstractmethod - def create_page( - self, - base_path: str, - page_path: str, - filename: str, - extension: str, - name: str, - user_input: Collection[str], - template_filename: str | None = None, - ) -> Page: + def create_page(self, options: PageCreatorOptions) -> Page: raise NotImplementedError @staticmethod @@ -50,17 +54,19 @@ class PageCreator: # return path with a file return path_to_file.with_suffix(f".{extension}") - def _load_template(self, base_path: str, template_filename: str) -> str: + def _load_template( + self, base_path: str, template_filename: str, template_path: str | None + ) -> str: # check for overriden template_path - if tembo.CONFIG.template_path is not None: - template_path = self._convert_to_path( - "", tembo.CONFIG.template_path, "", "" - ) + if template_path is not None: + converted_template_path = self._convert_to_path("", template_path, "", "") else: # default template_path is base_path / .templates - template_path = self._convert_to_path(base_path, ".templates", "", "") + converted_template_path = self._convert_to_path( + base_path, ".templates", "", "" + ) # load the template folder - file_loader = jinja2.FileSystemLoader(template_path) + file_loader = jinja2.FileSystemLoader(converted_template_path) env = jinja2.Environment(loader=file_loader, autoescape=True) # load the template contents try: @@ -91,25 +97,18 @@ class ScopedPageCreator(PageCreator): self.extension = "" self._all_input_tokens: list[str] = [] - def create_page( - self, - base_path: str, - page_path: str, - filename: str, - extension: str, - name: str, - user_input: Collection[str], - template_filename: str | None = None, - ) -> Page: - self.base_path = base_path - self.page_path = page_path - self.filename = filename - self.extension = extension + def create_page(self, options: PageCreatorOptions) -> Page: + self.base_path = options.base_path + self.page_path = options.page_path + self.filename = options.filename + self.extension = options.extension # verify the user input length matches the number of input tokens in the # tembo config/templates - self._all_input_tokens = self._get_input_tokens(template_filename) - self._verify_input_tokens(user_input) + self._all_input_tokens = self._get_input_tokens( + options.template_filename, options.template_path + ) + self._verify_input_tokens(options.user_input) # get the path of the scoped page path = self._convert_to_path( @@ -117,24 +116,32 @@ class ScopedPageCreator(PageCreator): ) # substitute tokens in the filepath - path = pathlib.Path(self._substitute_tokens(str(path), user_input, name)) + path = pathlib.Path( + self._substitute_tokens(str(path), options.user_input, options.name) + ) # get the template file - template_contents = self._get_template_contents(template_filename) + template_contents = self._get_template_contents( + options.template_filename, options.template_path + ) # substitute tokens in template_contents - if template_filename is not None: + if options.template_filename is not None: template_contents = self._substitute_tokens( - template_contents, user_input, name + template_contents, options.user_input, options.name ) return ScopedPage(path, template_contents) - def _get_input_tokens(self, template_filename: str | None) -> list[str]: + def _get_input_tokens( + self, template_filename: str | None, template_path: str | None + ) -> list[str]: path = str( pathlib.Path( self.base_path, self.page_path, self.filename, self.extension ).expanduser() ) - template_contents = self._get_template_contents(template_filename) + template_contents = self._get_template_contents( + template_filename, template_path + ) # get the input tokens from both the path and the template all_input_tokens = [] for tokenified_string in (path, template_contents): @@ -150,9 +157,11 @@ class ScopedPageCreator(PageCreator): ) raise SystemExit(1) - def _get_template_contents(self, template_filename: str | None) -> str: + def _get_template_contents( + self, template_filename: str | None, template_path: str | None + ) -> str: return ( - self._load_template(self.base_path, template_filename) + self._load_template(self.base_path, template_filename, template_path) if template_filename is not None else "" ) diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py new file mode 100644 index 0000000..e69de29 From 1fccf89d608b4bc1180f22404377879cc1493fde Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 29 Oct 2021 00:15:54 +0100 Subject: [PATCH 43/74] adding example and template_file as optional in config --- TODO.todo | 6 ++++- tembo/cli.py | 47 +++++++++++++++++++++----------------- tembo/journal/pages.py | 51 +++++++++++++++++++++++++----------------- 3 files changed, 63 insertions(+), 41 deletions(-) diff --git a/TODO.todo b/TODO.todo index 831fb56..773c760 100644 --- a/TODO.todo +++ b/TODO.todo @@ -24,7 +24,8 @@ Documentation: Functionality: ✔ Move any `tembo.CONFIG` calls out of `pages.py` and ensure these are passed in from the cli. @done(21-10-28 19:44) ✔ Make `config scope` a dict in `cli.py`. @done(21-10-28 19:44) - ☐ Add the `--example` output to the miscounted token message so the user knows the correct command to use. + ✔ Make example optional @done(21-10-29 00:15) + ✔ Add the `--example` output to the miscounted token message so the user knows the correct command to use. @done(21-10-29 00:15) ✔ Page options dataclass @done(21-10-28 20:09) ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? ☐ Use the python runner Duty @@ -39,6 +40,9 @@ VSCode: ☐ Document build error: PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.8.11 ☐ 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 diff --git a/tembo/cli.py b/tembo/cli.py index 3a9b071..f1685e9 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -62,32 +62,38 @@ def new(scope, inputs, dry_run, example): raise SystemExit(1) from type_error # get the scope information from the tembo config.yml - try: - config_scope = { - option: str(user_scope[option]) - for option in [ - "name", - "example", - "path", - "filename", - "extension", - "template_filename", - ] - for user_scope in tembo.CONFIG.scopes - if user_scope["name"] == scope - } - except KeyError as key_error: - # raise error if any non optional keys are missing - tembo.logger.critical("Key %s not found in config.yml - exiting", key_error) - raise SystemExit(1) from key_error - + config_scope = {} + for option in [ + "name", + "example", + "path", + "filename", + "extension", + "template_filename", + ]: + try: + config_scope.update( + { + option: str(user_scope[option]) + for user_scope in tembo.CONFIG.scopes + if user_scope["name"] == scope + } + ) + except KeyError as key_error: + if key_error.args[0] in ["example", "template_filename"]: + config_scope.update({key_error.args[0]: None}) + continue + tembo.logger.critical( + "Key %s not found in config. yml - exiting", key_error + ) + raise SystemExit(1) from key_error # print the example to the user if example: tembo.logger.info( "Example for 'tembo new %s': %s", config_scope["name"], config_scope["example"] - if isinstance(config_scope[0][1], str) + if isinstance(config_scope["example"], str) else "No example in config.yml", ) raise SystemExit(0) @@ -99,6 +105,7 @@ def new(scope, inputs, dry_run, example): 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"], template_path=tembo.CONFIG.template_path, diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 20bb3e6..5774942 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -20,9 +20,10 @@ class PageCreatorOptions: filename: str extension: str name: str + example: str | None user_input: Collection[str] - template_filename: str | None = None - template_path: str | None = None + template_filename: str | None + template_path: str | None class PageCreator: @@ -108,7 +109,7 @@ class ScopedPageCreator(PageCreator): self._all_input_tokens = self._get_input_tokens( options.template_filename, options.template_path ) - self._verify_input_tokens(options.user_input) + self._verify_input_tokens(options.user_input, options.example) # get the path of the scoped page path = self._convert_to_path( @@ -148,13 +149,23 @@ class ScopedPageCreator(PageCreator): all_input_tokens.extend(re.findall(r"(\{input\d*\})", tokenified_string)) return sorted(all_input_tokens) - def _verify_input_tokens(self, user_input: Collection[str]) -> None: + def _verify_input_tokens( + self, user_input: Collection[str], example: str | None + ) -> None: if len(self._all_input_tokens) != len(user_input): - tembo.logger.critical( - "Your tembo.config/template specifies %s input tokens, you gave %s", - len(self._all_input_tokens), - len(user_input), - ) + if example is not None: + tembo.logger.critical( + "Your tembo.config/template specifies %s input tokens, you gave %s. Example command: %s", + len(self._all_input_tokens), + len(user_input), + example, + ) + else: + tembo.logger.critical( + "Your tembo.config/template specifies %s input tokens, you gave %s.", + len(self._all_input_tokens), + len(user_input), + ) raise SystemExit(1) def _get_template_contents( @@ -332,17 +343,17 @@ if __name__ == "__main__": # "scratchpad.md.tpl", # ) # ) - test_page_with_template = c.create_page( - "~/tembo", - "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", - "file-{input0}-{name}", - ".md", - "meeting", - ("last",), - "scratchpad.md.tpl", - ) - print(test_page_with_template) - test_page_with_template.save_to_disk(False) + # test_page_with_template = c.create_page( + # "~/tembo", + # "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", + # "file-{input0}-{name}", + # ".md", + # "meeting", + # ("last",), + # "scratchpad.md.tpl", + # ) + # print(test_page_with_template) + # test_page_with_template.save_to_disk(False) # print( # c.create_page( # "~/tembo", From 883b38c8001fb9af710ce48a2e3ebb6d0af74669 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 29 Oct 2021 00:40:14 +0100 Subject: [PATCH 44/74] adding initial tests --- TODO.todo | 4 ++++ poetry.lock | 33 +++++++++++++++++++++++--------- pyproject.toml | 1 + tembo/cli.py | 3 +-- tembo/journal/pages.py | 2 +- tests/test_journal/test_pages.py | 31 ++++++++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 12 deletions(-) diff --git a/TODO.todo b/TODO.todo index 773c760..afa3906 100644 --- a/TODO.todo +++ b/TODO.todo @@ -21,6 +21,9 @@ Documentation: Use for an example ☐ Build the docs using a github action. + ☐ Document how to use pytest to read a logging message + + Functionality: ✔ Move any `tembo.CONFIG` calls out of `pages.py` and ensure these are passed in from the cli. @done(21-10-28 19:44) ✔ Make `config scope` a dict in `cli.py`. @done(21-10-28 19:44) @@ -28,6 +31,7 @@ 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) ✔ Page options dataclass @done(21-10-28 20:09) ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? + ☐ Replace scoped page creator inputs so that the whole class uses the options dict rather than the variables passed around. ☐ Use the python runner Duty ☐ Run tests diff --git a/poetry.lock b/poetry.lock index 389d4c6..1e1bc16 100644 --- a/poetry.lock +++ b/poetry.lock @@ -38,7 +38,7 @@ wrapt = ">=1.11,<1.14" name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -46,7 +46,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -181,7 +181,7 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\" name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -277,7 +277,7 @@ python-versions = "*" name = "packaging" version = "21.0" description = "Core utilities for Python packages" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -367,7 +367,7 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -422,7 +422,7 @@ python-versions = "*" name = "py" version = "1.10.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -555,7 +555,7 @@ pylint = ">=1.7" name = "pyparsing" version = "3.0.1" description = "Python parsing module" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -566,7 +566,7 @@ diagrams = ["jinja2", "railroad-diagrams"] name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -583,6 +583,17 @@ 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" @@ -702,7 +713,7 @@ 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 = "d34e6da39ccdc2c892d1e9c263384b3b0a3ea59aaf0efb57c0b7d52d23635b56" +content-hash = "21a4ccbcf449e8ef748f7fffef2b07f245e58df5dc5c119591758f907c3c1b65" [metadata.files] altgraph = [ @@ -1026,6 +1037,10 @@ 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"}, diff --git a/pyproject.toml b/pyproject.toml index 64759d9..2080f55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ 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 = "^6.2.5" diff --git a/tembo/cli.py b/tembo/cli.py index f1685e9..973eccc 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -1,10 +1,9 @@ -import sys - import click import tembo from tembo.journal import pages + CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 5774942..c987e6e 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -6,9 +6,9 @@ import pathlib import re from typing import Collection +import pendulum import jinja2 from jinja2.exceptions import TemplateNotFound -import pendulum import tembo diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index e69de29..89c5d25 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -0,0 +1,31 @@ +import pytest + +from tembo.journal.pages import PageCreator + + +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 = "some_extension" + + # act + with pytest.raises(SystemExit) as system_exit: + PageCreator._convert_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([]) +def test_page_creator_convert_to_path(shared_datadir): + # arrange + + # act + + # assert + pass From 1333e555a8923b73e645e5de5ef005f18b279f30 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 29 Oct 2021 17:45:35 +0100 Subject: [PATCH 45/74] adding coverage --- poetry.lock | 48 +++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 1e1bc16..c83e9e4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -90,6 +90,17 @@ 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" @@ -713,7 +724,7 @@ 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 = "21a4ccbcf449e8ef748f7fffef2b07f245e58df5dc5c119591758f907c3c1b65" +content-hash = "a44f31719364047d4a223e74f8b31a8cb20ec7940bb82428e038648d70c71e0e" [metadata.files] altgraph = [ @@ -748,6 +759,41 @@ 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"}, diff --git a/pyproject.toml b/pyproject.toml index 2080f55..c3e2c86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ 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"] From 3795f51161dd03b0933a323dc040e710a0bc3860 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 29 Oct 2021 17:45:40 +0100 Subject: [PATCH 46/74] adding coverage duty --- duties.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/duties.py b/duties.py index 62525c7..a813bbd 100644 --- a/duties.py +++ b/duties.py @@ -8,9 +8,24 @@ def test(ctx): @duty def update_deps(ctx, dry: bool = True): - # duty update_deps dry=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"]) From 5b668deb13979f74e9a32f7cd69fc2c276bcebc8 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 29 Oct 2021 17:45:57 +0100 Subject: [PATCH 47/74] updating tests --- tests/test_journal/test_pages.py | 48 ++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index 89c5d25..8ec416d 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -1,3 +1,5 @@ +import pathlib + import pytest from tembo.journal.pages import PageCreator @@ -8,24 +10,58 @@ def test_page_creator_convert_to_path_missing_base_path(caplog): base_path = "/some/nonexistent/path" page_path = "some_page" filename = "some_filename" - extension = "some_extension" + extension = "ex" # act with pytest.raises(SystemExit) as system_exit: PageCreator._convert_to_path( - base_path=base_path, page_path=page_path, filename=filename, extension=extension + 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" + assert ( + caplog.records[0].message + == "Tembo base path of /some/nonexistent/path does not exist - exiting" + ) -# @pytest.mark.parametrize([]) -def test_page_creator_convert_to_path(shared_datadir): + +@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}") + ) # act + converted_path = PageCreator._convert_to_path( + tmpdir, page_path, filename, extension + ) # assert - pass + 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") + + # act + converted_path = PageCreator._convert_to_path("", "/some/path", "", "") + + # assert + assert str(full_path).replace(" ", "_") == str(converted_path) From d82c41f2ff3d920d752298190487555f3655ad11 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Fri, 29 Oct 2021 17:47:10 +0100 Subject: [PATCH 48/74] updating dependencies --- duties.py | 2 +- poetry.lock | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/duties.py b/duties.py index a813bbd..4f23051 100644 --- a/duties.py +++ b/duties.py @@ -7,7 +7,7 @@ def test(ctx): @duty -def update_deps(ctx, dry: bool = True): +def update_deps(ctx, dry: bool = False): """Update the dependencies using Poetry. Example: diff --git a/poetry.lock b/poetry.lock index c83e9e4..e9a558f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -286,14 +286,14 @@ python-versions = "*" [[package]] name = "packaging" -version = "21.0" +version = "21.2" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3" [[package]] name = "panaetius" @@ -564,14 +564,11 @@ pylint = ">=1.7" [[package]] name = "pyparsing" -version = "3.0.1" +version = "2.4.7" description = "Python parsing module" category = "main" optional = false -python-versions = ">=3.6" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "pytest" @@ -970,8 +967,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, + {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, ] panaetius = [] pbr = [ @@ -1076,8 +1073,8 @@ pylint-plugin-utils = [ {file = "pylint_plugin_utils-0.6-py3-none-any.whl", hash = "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a"}, ] pyparsing = [ - {file = "pyparsing-3.0.1-py3-none-any.whl", hash = "sha256:fd93fc45c47893c300bd98f5dd1b41c0e783eaeb727e7cea210dcc09d64ce7c3"}, - {file = "pyparsing-3.0.1.tar.gz", hash = "sha256:84196357aa3566d64ad123d7a3c67b0e597a115c4934b097580e5ce220b91531"}, + {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"}, From be55e20e4dda5c92c53e36ce0d655f7518b9e4dc Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 30 Oct 2021 01:17:08 +0100 Subject: [PATCH 49/74] adding latest tests --- .coveragerc | 7 ++ TODO.todo | 4 ++ tests/test_journal/test_pages.py | 65 ++++++++++++++++++- .../.templates/some_template.md.tpl | 1 + 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 .coveragerc create mode 100644 tests/test_journal/test_pages/.templates/some_template.md.tpl 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/TODO.todo b/TODO.todo index afa3906..645aacd 100644 --- a/TODO.todo +++ b/TODO.todo @@ -24,6 +24,8 @@ Documentation: ☐ Document how to use pytest to read a logging message + ☐ Document using datadir with a module rather than a shared one. Link to tembo as an example. + Functionality: ✔ Move any `tembo.CONFIG` calls out of `pages.py` and ensure these are passed in from the cli. @done(21-10-28 19:44) ✔ Make `config scope` a dict in `cli.py`. @done(21-10-28 19:44) @@ -31,6 +33,8 @@ 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) ✔ Page options dataclass @done(21-10-28 20:09) ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? + ☐ Look at `_convert_to_path()` and see if it can be rewritten to make it clearer when there isn't a base path. + Currently checks to see if base_path is not None but this is never the case as a string must be passed in and if there isn't a base_path we pass in an empty string. ☐ Replace scoped page creator inputs so that the whole class uses the options dict rather than the variables passed around. ☐ Use the python runner Duty diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index 8ec416d..66bc168 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -1,8 +1,9 @@ import pathlib import pytest +import jinja2 -from tembo.journal.pages import PageCreator +from tembo.journal.pages import PageCreator, ScopedPageCreator def test_page_creator_convert_to_path_missing_base_path(caplog): @@ -46,10 +47,11 @@ def test_page_creator_convert_to_path_full_path_to_file( / pathlib.Path(page_path) / pathlib.Path(filename).with_suffix(f".{extension}") ) + base_path = tmpdir # act converted_path = PageCreator._convert_to_path( - tmpdir, page_path, filename, extension + base_path, page_path, filename, extension ) # assert @@ -59,9 +61,66 @@ def test_page_creator_convert_to_path_full_path_to_file( 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_to_path("", "/some/path", "", "") + converted_path = PageCreator._convert_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/.templates/some_template.md.tpl b/tests/test_journal/test_pages/.templates/some_template.md.tpl new file mode 100644 index 0000000..1a53169 --- /dev/null +++ b/tests/test_journal/test_pages/.templates/some_template.md.tpl @@ -0,0 +1 @@ +template contents From 1f3fa4100d2576f88a3f4c870e1c993d409560d7 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 30 Oct 2021 02:12:36 +0100 Subject: [PATCH 50/74] adding latest tests --- TODO.todo | 3 +- tembo/cli.py | 2 + tembo/journal/pages.py | 26 +++--- tests/test_journal/old_test_pages.py | 126 +++++++++++++++++++++++++++ tests/test_journal/test_pages.py | 121 +------------------------ 5 files changed, 147 insertions(+), 131 deletions(-) create mode 100644 tests/test_journal/old_test_pages.py diff --git a/TODO.todo b/TODO.todo index 645aacd..da7030b 100644 --- a/TODO.todo +++ b/TODO.todo @@ -46,7 +46,8 @@ Functionality: VSCode: PyInstaller: ☐ Document build error: - PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.8.11 + 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 -> |= diff --git a/tembo/cli.py b/tembo/cli.py index 973eccc..a1f8313 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -50,6 +50,7 @@ def new(scope, inputs, dry_run, example): Example: tembo new meeting my_presentation """ + # get the name from the tembo config.yml try: _name_found = scope in [ @@ -86,6 +87,7 @@ def new(scope, inputs, dry_run, example): "Key %s not found in config. yml - exiting", key_error ) raise SystemExit(1) from key_error + # print the example to the user if example: tembo.logger.info( diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index c987e6e..d9f3ea6 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -20,10 +20,10 @@ class PageCreatorOptions: filename: str extension: str name: str - example: str | None user_input: Collection[str] - template_filename: str | None - template_path: str | None + example: str | None = None + template_filename: str | None = None + template_path: str | None = None class PageCreator: @@ -60,7 +60,7 @@ class PageCreator: ) -> str: # check for overriden template_path if template_path is not None: - converted_template_path = self._convert_to_path("", template_path, "", "") + converted_template_path = pathlib.Path(template_path).expanduser() else: # default template_path is base_path / .templates converted_template_path = self._convert_to_path( @@ -132,6 +132,15 @@ class ScopedPageCreator(PageCreator): ) return ScopedPage(path, template_contents) + def _get_template_contents( + self, template_filename: str | None, template_path: str | None + ) -> str: + return ( + self._load_template(self.base_path, template_filename, template_path) + if template_filename is not None + else "" + ) + def _get_input_tokens( self, template_filename: str | None, template_path: str | None ) -> list[str]: @@ -168,15 +177,6 @@ class ScopedPageCreator(PageCreator): ) raise SystemExit(1) - def _get_template_contents( - self, template_filename: str | None, template_path: str | None - ) -> str: - return ( - self._load_template(self.base_path, template_filename, template_path) - if template_filename is not None - else "" - ) - def _substitute_tokens( self, tokenified_string: str, diff --git a/tests/test_journal/old_test_pages.py b/tests/test_journal/old_test_pages.py new file mode 100644 index 0000000..03c62d6 --- /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_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_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_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 index 66bc168..abc14c5 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -1,126 +1,13 @@ -import pathlib - import pytest -import jinja2 -from tembo.journal.pages import PageCreator, ScopedPageCreator +from tembo.journal.pages import PageCreatorOptions -def test_page_creator_convert_to_path_missing_base_path(caplog): +def test_scoped_page_creator_create_page_missing_base_path(): # arrange - base_path = "/some/nonexistent/path" - page_path = "some_page" - filename = "some_filename" - extension = "ex" + options = PageCreatorOptions() # act - with pytest.raises(SystemExit) as system_exit: - PageCreator._convert_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_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_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" - ) + pass From 651f94e51c5e5038edf14127abec3edca19a1cf5 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 30 Oct 2021 02:13:42 +0100 Subject: [PATCH 51/74] adding testing notes --- dev/notes/test.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 dev/notes/test.md diff --git a/dev/notes/test.md b/dev/notes/test.md new file mode 100644 index 0000000..1c065d0 --- /dev/null +++ b/dev/notes/test.md @@ -0,0 +1,30 @@ +# testing notes + +optional: +- template_path +- example +- template_filename + +required: +- base_path +- page_path +- filename +- extension +- name +- user_input - should be optional + + + +- page with a template +- page without a template +- page using date tokens +- page using input tokens +- page using name tokens + + +- user input does not match number of input tokens + + + + +- user input does match number of input tokensE From 903fbf6b69a1bd73608b4b5b6a9977ca476d3c20 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 30 Oct 2021 03:07:44 +0100 Subject: [PATCH 52/74] adding latest --- TODO.todo | 5 ++- dev/notes/test.md | 20 ++++------ tembo/journal/pages.py | 85 ++++++++++++++++++++++++++++-------------- 3 files changed, 69 insertions(+), 41 deletions(-) diff --git a/TODO.todo b/TODO.todo index da7030b..8b543aa 100644 --- a/TODO.todo +++ b/TODO.todo @@ -25,6 +25,7 @@ Documentation: ☐ 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 Functionality: ✔ Move any `tembo.CONFIG` calls out of `pages.py` and ensure these are passed in from the cli. @done(21-10-28 19:44) @@ -32,9 +33,9 @@ Functionality: ✔ Make example optional @done(21-10-29 00:15) ✔ Add the `--example` output to the miscounted token message so the user knows the correct command to use. @done(21-10-29 00:15) ✔ Page options dataclass @done(21-10-28 20:09) + ☐ Make user_input optional @important ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? - ☐ Look at `_convert_to_path()` and see if it can be rewritten to make it clearer when there isn't a base path. - Currently checks to see if base_path is not None but this is never the case as a string must be passed in and if there isn't a base_path we pass in an empty string. + ✔ 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) ☐ Replace scoped page creator inputs so that the whole class uses the options dict rather than the variables passed around. ☐ Use the python runner Duty diff --git a/dev/notes/test.md b/dev/notes/test.md index 1c065d0..607a988 100644 --- a/dev/notes/test.md +++ b/dev/notes/test.md @@ -4,6 +4,7 @@ optional: - template_path - example - template_filename +- user_input required: - base_path @@ -11,20 +12,15 @@ required: - filename - extension - name -- user_input - should be optional - - - -- page with a template -- page without a template -- page using date tokens -- page using input tokens -- page using name tokens +- page using/not using input tokens +- page with/without a template - user input does not match number of input tokens +- user input is None +- the given base path does not exist +- page using/not using date tokens +- page using/not using name tokens - - -- user input does match number of input tokensE +- path/page filenames can contain spaces and they are converted diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index d9f3ea6..8562d9d 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -13,14 +13,29 @@ from jinja2.exceptions import TemplateNotFound import tembo +# 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] + user_input: Collection[str] | None = None example: str | None = None template_filename: str | None = None template_path: str | None = None @@ -92,51 +107,48 @@ class ScopedPageCreator(PageCreator): """ def __init__(self) -> None: - self.base_path = "" - self.page_path = "" - self.filename = "" - self.extension = "" + # self.base_path = "" + # self.page_path = "" + # self.filename = "" + # self.extension = "" self._all_input_tokens: list[str] = [] + self.options: PageCreatorOptions def create_page(self, options: PageCreatorOptions) -> Page: - self.base_path = options.base_path - self.page_path = options.page_path - self.filename = options.filename - self.extension = options.extension + self.options = options - # verify the user input length matches the number of input tokens in the - # tembo config/templates self._all_input_tokens = self._get_input_tokens( options.template_filename, options.template_path ) self._verify_input_tokens(options.user_input, options.example) - # get the path of the scoped page path = self._convert_to_path( - self.base_path, self.page_path, self.filename, self.extension + self.options.base_path, + self.options.page_path, + self.options.filename, + self.options.extension, ) - - # substitute tokens in the filepath path = pathlib.Path( self._substitute_tokens(str(path), options.user_input, options.name) ) - # get the template file template_contents = self._get_template_contents( options.template_filename, options.template_path ) - # substitute tokens in template_contents if options.template_filename is not None: template_contents = self._substitute_tokens( template_contents, options.user_input, options.name ) + return ScopedPage(path, template_contents) def _get_template_contents( self, template_filename: str | None, template_path: str | None ) -> str: return ( - self._load_template(self.base_path, template_filename, template_path) + self._load_template( + self.options.base_path, template_filename, template_path + ) if template_filename is not None else "" ) @@ -146,7 +158,10 @@ class ScopedPageCreator(PageCreator): ) -> list[str]: path = str( pathlib.Path( - self.base_path, self.page_path, self.filename, self.extension + self.options.base_path, + self.options.page_path, + self.options.filename, + self.options.extension, ).expanduser() ) template_contents = self._get_template_contents( @@ -159,8 +174,23 @@ class ScopedPageCreator(PageCreator): return sorted(all_input_tokens) def _verify_input_tokens( - self, user_input: Collection[str], example: str | None + self, user_input: Collection[str] | None, example: str | None ) -> None: + if len(self._all_input_tokens) > 0 and user_input is None: + if example is not None: + tembo.logger.critical( + "Your tembo.config/template specifies %s input tokens, you gave 0. Example command: %s", + len(self._all_input_tokens), + example, + ) + else: + tembo.logger.critical( + "Your tembo.config/template specifies %s input tokens, you gave 0.", + len(self._all_input_tokens), + ) + raise SystemExit(1) + if user_input is None: + return if len(self._all_input_tokens) != len(user_input): if example is not None: tembo.logger.critical( @@ -176,11 +206,12 @@ class ScopedPageCreator(PageCreator): len(user_input), ) raise SystemExit(1) + return def _substitute_tokens( self, tokenified_string: str, - user_input: Collection[str], + user_input: Collection[str] | None, name: str, ) -> str: """For a tokened string, substitute input, name and date tokens.""" @@ -194,13 +225,13 @@ class ScopedPageCreator(PageCreator): def __substitute_input_tokens( self, tokenified_string: str, - user_input: Collection[str], + user_input: Collection[str] | None, ) -> str: - for input_value, extracted_token in zip(user_input, self._all_input_tokens): - # REVIEW: test this for spaces in the filename/input token - tokenified_string = tokenified_string.replace( - extracted_token, input_value.replace(" ", "_") - ) + if user_input is not None: + for input_value, extracted_token in zip(user_input, self._all_input_tokens): + tokenified_string = tokenified_string.replace( + extracted_token, input_value.replace(" ", "_") + ) return tokenified_string @staticmethod From a6ec5a9f35dd07b7e40e826a6deac63649caea64 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 30 Oct 2021 03:21:21 +0100 Subject: [PATCH 53/74] replacing all calls to self.options --- TODO.todo | 23 ++++---- tembo/journal/pages.py | 118 +++++++++++++++++------------------------ 2 files changed, 60 insertions(+), 81 deletions(-) diff --git a/TODO.todo b/TODO.todo index 8b543aa..efe3316 100644 --- a/TODO.todo +++ b/TODO.todo @@ -1,7 +1,4 @@ Priority: - ✔ Document the python/logging/typing in Trilium @done(21-10-25 14:33) - ✔ Update typing annotations to include generics instead @done(21-10-25 22:38) - https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes ☐ Write the tests ☐ test logs: document this @@ -28,15 +25,8 @@ Documentation: ☐ Can prospector ignore tests dir? document this in the gist if so Functionality: - ✔ Move any `tembo.CONFIG` calls out of `pages.py` and ensure these are passed in from the cli. @done(21-10-28 19:44) - ✔ Make `config scope` a dict in `cli.py`. @done(21-10-28 19:44) - ✔ Make example optional @done(21-10-29 00:15) - ✔ Add the `--example` output to the miscounted token message so the user knows the correct command to use. @done(21-10-29 00:15) - ✔ Page options dataclass @done(21-10-28 20:09) - ☐ Make user_input optional @important ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? - ✔ 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) - ☐ Replace scoped page creator inputs so that the whole class uses the options dict rather than the variables passed around. + ☐ Make options a property on the class, add to abstract ☐ Use the python runner Duty ☐ Run tests @@ -75,6 +65,17 @@ Logging: 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) diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 8562d9d..a305d36 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -42,28 +42,32 @@ class PageCreatorOptions: class PageCreator: + def __init__(self, options: PageCreatorOptions) -> None: + raise NotImplementedError + @abstractmethod def create_page(self, options: PageCreatorOptions) -> Page: raise NotImplementedError - @staticmethod - def _convert_to_path( - base_path: str, page_path: str, filename: str, extension: str - ) -> pathlib.Path: + def _convert_to_path(self) -> pathlib.Path: # check if Tembo base path exists - if not pathlib.Path(base_path).expanduser().exists(): + if not pathlib.Path(self.options.base_path).expanduser().exists(): tembo.logger.critical( - "Tembo base path of %s does not exist - exiting", base_path + "Tembo base path of %s does not exist - exiting", self.options.base_path ) raise SystemExit(1) path_to_file = ( - pathlib.Path(base_path).expanduser() - / pathlib.Path(page_path.replace(" ", "_")).expanduser() - / filename.replace(" ", "_") + pathlib.Path(self.options.base_path).expanduser() + / pathlib.Path(self.options.page_path.replace(" ", "_")).expanduser() + / self.options.filename.replace(" ", "_") ) try: # check for existing `.` in the extension - extension = extension[1:] if extension[0] == "." else extension + extension = ( + self.options.extension[1:] + if self.options.extension[0] == "." + else self.options.extension + ) except IndexError: # IndexError means the path is not a file, just a path return path_to_file @@ -107,20 +111,14 @@ class ScopedPageCreator(PageCreator): """ def __init__(self) -> None: - # self.base_path = "" - # self.page_path = "" - # self.filename = "" - # self.extension = "" self._all_input_tokens: list[str] = [] self.options: PageCreatorOptions def create_page(self, options: PageCreatorOptions) -> Page: self.options = options - self._all_input_tokens = self._get_input_tokens( - options.template_filename, options.template_path - ) - self._verify_input_tokens(options.user_input, options.example) + self._all_input_tokens = self._get_input_tokens() + self._verify_input_tokens() path = self._convert_to_path( self.options.base_path, @@ -128,34 +126,26 @@ class ScopedPageCreator(PageCreator): self.options.filename, self.options.extension, ) - path = pathlib.Path( - self._substitute_tokens(str(path), options.user_input, options.name) - ) + path = pathlib.Path(self._substitute_tokens(str(path))) - template_contents = self._get_template_contents( - options.template_filename, options.template_path - ) + template_contents = self._get_template_contents() if options.template_filename is not None: - template_contents = self._substitute_tokens( - template_contents, options.user_input, options.name - ) + template_contents = self._substitute_tokens(template_contents) return ScopedPage(path, template_contents) - def _get_template_contents( - self, template_filename: str | None, template_path: str | None - ) -> str: + def _get_template_contents(self) -> str: return ( self._load_template( - self.options.base_path, template_filename, template_path + self.options.base_path, + self.options.template_filename, + self.options.template_path, ) - if template_filename is not None + if self.options.template_filename is not None else "" ) - def _get_input_tokens( - self, template_filename: str | None, template_path: str | None - ) -> list[str]: + def _get_input_tokens(self) -> list[str]: path = str( pathlib.Path( self.options.base_path, @@ -164,24 +154,20 @@ class ScopedPageCreator(PageCreator): self.options.extension, ).expanduser() ) - template_contents = self._get_template_contents( - template_filename, template_path - ) + template_contents = self._get_template_contents() # 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, user_input: Collection[str] | None, example: str | None - ) -> None: - if len(self._all_input_tokens) > 0 and user_input is None: - if example is not None: + def _verify_input_tokens(self) -> None: + if len(self._all_input_tokens) > 0 and self.options.user_input is None: + if self.options.example is not None: tembo.logger.critical( "Your tembo.config/template specifies %s input tokens, you gave 0. Example command: %s", len(self._all_input_tokens), - example, + self.options.example, ) else: tembo.logger.critical( @@ -189,58 +175,50 @@ class ScopedPageCreator(PageCreator): len(self._all_input_tokens), ) raise SystemExit(1) - if user_input is None: + if self.options.user_input is None: return - if len(self._all_input_tokens) != len(user_input): - if example is not None: + if len(self._all_input_tokens) != len(self.options.user_input): + if self.options.example is not None: tembo.logger.critical( "Your tembo.config/template specifies %s input tokens, you gave %s. Example command: %s", len(self._all_input_tokens), - len(user_input), - example, + len(self.options.user_input), + self.options.example, ) else: tembo.logger.critical( "Your tembo.config/template specifies %s input tokens, you gave %s.", len(self._all_input_tokens), - len(user_input), + len(self.options.user_input), ) raise SystemExit(1) return - def _substitute_tokens( - self, - tokenified_string: str, - user_input: Collection[str] | None, - name: str, - ) -> str: + 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, user_input - ) - tokenified_string = self.__substitute_name_tokens(tokenified_string, name) + 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, - user_input: Collection[str] | None, - ) -> str: - if user_input is not None: - for input_value, extracted_token in zip(user_input, self._all_input_tokens): + 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 - @staticmethod - def __substitute_name_tokens(tokenified_string: str, name: str) -> str: + 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, name) + tokenified_string = tokenified_string.replace( + extracted_input, self.options.name + ) return tokenified_string @staticmethod From 86ec115c9f990e88e72fc7367c7166f0c013d757 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 30 Oct 2021 20:24:56 +0100 Subject: [PATCH 54/74] adding latest tests --- TODO.todo | 5 ++- dev/notes/test.md | 39 +++++++++++++++-- tembo/exceptions.py | 4 ++ tembo/journal/pages.py | 62 ++++++++++++---------------- tests/test_journal/old_test_pages.py | 6 +-- tests/test_journal/test_pages.py | 26 ++++++++++-- 6 files changed, 94 insertions(+), 48 deletions(-) diff --git a/TODO.todo b/TODO.todo index efe3316..f7c5ce0 100644 --- a/TODO.todo +++ b/TODO.todo @@ -26,7 +26,10 @@ Documentation: Functionality: ☐ Replace loggers with `click.echo` for command outputs. Keep logging messages for actual logging messages? - ☐ Make options a property on the class, add to abstract + ☐ 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. + ✔ Make options a property on the class, add to abstract @done(21-10-30 19:31) ☐ Use the python runner Duty ☐ Run tests diff --git a/dev/notes/test.md b/dev/notes/test.md index 607a988..4a1a293 100644 --- a/dev/notes/test.md +++ b/dev/notes/test.md @@ -1,10 +1,10 @@ # testing notes optional: -- template_path +- user_input - example - template_filename -- user_input +- template_path required: - base_path @@ -14,13 +14,44 @@ required: - name -- page using/not using input tokens - page with/without a template -- user input does not match number of input tokens - user input is None - the given base path does not exist +- page using/not using input tokens +- user input does not match number of input tokens + - no user input + - mismatched user input + - with/without example - page using/not using date tokens - page using/not using name tokens - path/page filenames can contain spaces and they are converted + + + +@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 diff --git a/tembo/exceptions.py b/tembo/exceptions.py index a71ea99..9746128 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -3,3 +3,7 @@ class MismatchedTokenError(Exception): pass + + +class BasePathDoesNotExistError(Exception): + pass diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index a305d36..4870b47 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -45,11 +45,16 @@ class PageCreator: def __init__(self, options: PageCreatorOptions) -> None: raise NotImplementedError + @property + @abstractmethod + def options(self) -> PageCreatorOptions: + raise NotImplementedError + @abstractmethod def create_page(self, options: PageCreatorOptions) -> Page: raise NotImplementedError - def _convert_to_path(self) -> pathlib.Path: + def _convert_base_path_to_path(self) -> pathlib.Path: # check if Tembo base path exists if not pathlib.Path(self.options.base_path).expanduser().exists(): tembo.logger.critical( @@ -74,27 +79,24 @@ class PageCreator: # return path with a file return path_to_file.with_suffix(f".{extension}") - def _load_template( - self, base_path: str, template_filename: str, template_path: str | None - ) -> str: - # check for overriden template_path - if template_path is not None: - converted_template_path = pathlib.Path(template_path).expanduser() + 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: - # default template_path is base_path / .templates - converted_template_path = self._convert_to_path( - base_path, ".templates", "", "" - ) - # load the template folder + converted_template_path = pathlib.Path() + file_loader = jinja2.FileSystemLoader(converted_template_path) env = jinja2.Environment(loader=file_loader, autoescape=True) - # load the template contents try: - loaded_template = env.get_template(template_filename) + loaded_template = env.get_template(self.options.template_filename) except TemplateNotFound as template_not_found: tembo.logger.critical( "Template file %s not found - exiting", - str(template_path) + "/" + str(template_not_found.message), + str(self.options.template_path) + "/" + str(template_not_found.message), ) raise SystemExit(1) from template_not_found return loaded_template.render() @@ -112,39 +114,27 @@ class ScopedPageCreator(PageCreator): def __init__(self) -> None: self._all_input_tokens: list[str] = [] - self.options: PageCreatorOptions + self._options: PageCreatorOptions + + @property + def options(self) -> PageCreatorOptions: + return self._options def create_page(self, options: PageCreatorOptions) -> Page: - self.options = options + self._options = options self._all_input_tokens = self._get_input_tokens() self._verify_input_tokens() - path = self._convert_to_path( - self.options.base_path, - self.options.page_path, - self.options.filename, - self.options.extension, - ) + path = self._convert_base_path_to_path() path = pathlib.Path(self._substitute_tokens(str(path))) - template_contents = self._get_template_contents() + template_contents = self._load_template() if options.template_filename is not None: template_contents = self._substitute_tokens(template_contents) return ScopedPage(path, template_contents) - def _get_template_contents(self) -> str: - return ( - self._load_template( - self.options.base_path, - self.options.template_filename, - self.options.template_path, - ) - if self.options.template_filename is not None - else "" - ) - def _get_input_tokens(self) -> list[str]: path = str( pathlib.Path( @@ -154,7 +144,7 @@ class ScopedPageCreator(PageCreator): self.options.extension, ).expanduser() ) - template_contents = self._get_template_contents() + 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): diff --git a/tests/test_journal/old_test_pages.py b/tests/test_journal/old_test_pages.py index 03c62d6..7805193 100644 --- a/tests/test_journal/old_test_pages.py +++ b/tests/test_journal/old_test_pages.py @@ -15,7 +15,7 @@ def test_page_creator_convert_to_path_missing_base_path(caplog): # act with pytest.raises(SystemExit) as system_exit: - PageCreator._convert_to_path( + PageCreator._convert_base_path_to_path( base_path=base_path, page_path=page_path, filename=filename, @@ -50,7 +50,7 @@ def test_page_creator_convert_to_path_full_path_to_file( base_path = tmpdir # act - converted_path = PageCreator._convert_to_path( + converted_path = PageCreator._convert_base_path_to_path( base_path, page_path, filename, extension ) @@ -67,7 +67,7 @@ def test_page_creator_convert_to_path_full_path_no_file(tmpdir): extension = "" # act - converted_path = PageCreator._convert_to_path( + converted_path = PageCreator._convert_base_path_to_path( base_path, page_path, filename, extension ) diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index abc14c5..d751287 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -1,13 +1,31 @@ import pytest -from tembo.journal.pages import PageCreatorOptions +from tembo.journal.pages import PageCreatorOptions, ScopedPageCreator -def test_scoped_page_creator_create_page_missing_base_path(): +def test_create_page_base_path_does_not_exist(tmpdir, caplog): # arrange - options = PageCreatorOptions() + 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(SystemExit) as system_exit: + scoped_page_creator = ScopedPageCreator().create_page(options) # assert - pass + assert system_exit.value.code == 1 + assert ( + caplog.records[0].message + == f"Tembo base path of {base_path} does not exist - exiting" + ) + assert caplog.records[0].levelname == "CRITICAL" From 413f783475265e6b24f3c694c18168e29183a1c7 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sat, 30 Oct 2021 23:39:52 +0100 Subject: [PATCH 55/74] adding latest --- TODO.todo | 16 ++++++++++++++++ tembo/cli.py | 11 ++++++++++- tembo/exceptions.py | 6 +++++- tembo/journal/pages.py | 17 +++++++++-------- tests/test_journal/test_pages.py | 11 +++++------ 5 files changed, 45 insertions(+), 16 deletions(-) diff --git a/TODO.todo b/TODO.todo index f7c5ce0..ef1f800 100644 --- a/TODO.todo +++ b/TODO.todo @@ -20,6 +20,22 @@ Documentation: ☐ 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 ☐ 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 diff --git a/tembo/cli.py b/tembo/cli.py index a1f8313..c2e3f5e 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -2,6 +2,7 @@ import click import tembo from tembo.journal import pages +from tembo import exceptions CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -112,7 +113,15 @@ def new(scope, inputs, dry_run, example): template_path=tembo.CONFIG.template_path, ) if _name_found: - scoped_page = pages.ScopedPageCreator().create_page(page_creator_options) + try: + scoped_page = pages.ScopedPageCreator().create_page(page_creator_options) + except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: + tembo.logger.critical(base_path_does_not_exist_error) + raise SystemExit(1) from base_path_does_not_exist_error + except exceptions.TemplateFileNotFoundError as template_file_not_found_error: + tembo.logger.critical(template_file_not_found_error.args[0]) + raise SystemExit(1) from template_file_not_found_error + scoped_page.save_to_disk(dry_run=dry_run) raise SystemExit(0) if not _name_found and len(tembo.CONFIG.scopes) > 0: diff --git a/tembo/exceptions.py b/tembo/exceptions.py index 9746128..d4b0252 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -6,4 +6,8 @@ class MismatchedTokenError(Exception): class BasePathDoesNotExistError(Exception): - pass + """Raised if the base path does not exist.""" + + +class TemplateFileNotFoundError(Exception): + """Raised if the template file does not exist.""" diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 4870b47..b566c6b 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -11,6 +11,7 @@ import jinja2 from jinja2.exceptions import TemplateNotFound import tembo +from tembo import exceptions # TODO: flesh this out with details for the optional args @@ -57,10 +58,9 @@ class PageCreator: def _convert_base_path_to_path(self) -> pathlib.Path: # check if Tembo base path exists if not pathlib.Path(self.options.base_path).expanduser().exists(): - tembo.logger.critical( - "Tembo base path of %s does not exist - exiting", self.options.base_path + raise exceptions.BasePathDoesNotExistError( + f"Tembo base path of {self.options.base_path} does not exist." ) - raise SystemExit(1) path_to_file = ( pathlib.Path(self.options.base_path).expanduser() / pathlib.Path(self.options.page_path.replace(" ", "_")).expanduser() @@ -74,6 +74,7 @@ class PageCreator: else self.options.extension ) except IndexError: + # REVIEW: can this be removed now this is not called anywhere else? # IndexError means the path is not a file, just a path return path_to_file # return path with a file @@ -91,14 +92,14 @@ class PageCreator: 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: - tembo.logger.critical( - "Template file %s not found - exiting", - str(self.options.template_path) + "/" + str(template_not_found.message), - ) - raise SystemExit(1) from 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() diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index d751287..16b4d54 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -1,9 +1,10 @@ import pytest from tembo.journal.pages import PageCreatorOptions, ScopedPageCreator +from tembo.exceptions import BasePathDoesNotExistError -def test_create_page_base_path_does_not_exist(tmpdir, caplog): +def test_create_page_base_path_does_not_exist(tmpdir): # arrange base_path = str(tmpdir / "nonexistent" / "path") options = PageCreatorOptions( @@ -19,13 +20,11 @@ def test_create_page_base_path_does_not_exist(tmpdir, caplog): ) # act - with pytest.raises(SystemExit) as system_exit: + with pytest.raises(BasePathDoesNotExistError) as base_path_does_not_exist_error: scoped_page_creator = ScopedPageCreator().create_page(options) # assert - assert system_exit.value.code == 1 assert ( - caplog.records[0].message - == f"Tembo base path of {base_path} does not exist - exiting" + str(base_path_does_not_exist_error.value) + == f"Tembo base path of {base_path} does not exist." ) - assert caplog.records[0].levelname == "CRITICAL" From 526dd733b51aa4304be5aff63e400334d5786c6b Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 00:41:01 +0000 Subject: [PATCH 56/74] adding latest --- TODO.todo | 8 + dev/notes/test.md | 40 +--- tembo/cli.py | 12 +- tembo/exceptions.py | 4 + tembo/journal/pages.py | 23 +- tests/test_journal/test_pages.py | 219 +++++++++++++++++- .../.templates/some_template.md.tpl | 1 - .../some_template_date_tokens.md.tpl | 1 + .../some_template_input_tokens.md.tpl | 1 + .../some_template_name_tokens.md.tpl | 1 + .../.templates/some_template_no_tokens.md.tpl | 3 + .../test_pages/does_exist/some_note.md | 1 + 12 files changed, 270 insertions(+), 44 deletions(-) delete mode 100644 tests/test_journal/test_pages/.templates/some_template.md.tpl create mode 100644 tests/test_journal/test_pages/.templates/some_template_date_tokens.md.tpl create mode 100644 tests/test_journal/test_pages/.templates/some_template_input_tokens.md.tpl create mode 100644 tests/test_journal/test_pages/.templates/some_template_name_tokens.md.tpl create mode 100644 tests/test_journal/test_pages/.templates/some_template_no_tokens.md.tpl create mode 100644 tests/test_journal/test_pages/does_exist/some_note.md diff --git a/TODO.todo b/TODO.todo index ef1f800..1fdcd36 100644 --- a/TODO.todo +++ b/TODO.todo @@ -39,12 +39,16 @@ Documentation: ☐ 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 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 @@ -53,6 +57,10 @@ Functionality: ☐ Build docs ☐ Document using Duty +Logging: + ☐ Make all internal tembo logs be debug + ☐ User can enable them with the config + VSCode: PyInstaller: ☐ Document build error: diff --git a/dev/notes/test.md b/dev/notes/test.md index 4a1a293..1681544 100644 --- a/dev/notes/test.md +++ b/dev/notes/test.md @@ -1,22 +1,25 @@ # testing notes +## options + optional: + - user_input - example - template_filename - template_path required: + - base_path - page_path - filename - extension - name +## tests to write -- page with/without a template - user input is None -- the given base path does not exist - page using/not using input tokens - user input does not match number of input tokens - no user input @@ -24,34 +27,13 @@ required: - with/without example - page using/not using date tokens - page using/not using name tokens - +- dry run - path/page filenames can contain spaces and they are converted +## tests done - -@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 +- 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/tembo/cli.py b/tembo/cli.py index c2e3f5e..361243d 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -122,8 +122,12 @@ def new(scope, inputs, dry_run, example): tembo.logger.critical(template_file_not_found_error.args[0]) raise SystemExit(1) from template_file_not_found_error - scoped_page.save_to_disk(dry_run=dry_run) - raise SystemExit(0) + try: + scoped_page.save_to_disk(dry_run=dry_run) + 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 if not _name_found and len(tembo.CONFIG.scopes) > 0: # if the name is missing in the config.yml, raise error tembo.logger.warning("Command %s not found in config.yml - exiting", scope) @@ -136,6 +140,10 @@ def new(scope, inputs, dry_run, example): raise SystemExit(1) +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 index d4b0252..6d9fb1b 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -11,3 +11,7 @@ class BasePathDoesNotExistError(Exception): class TemplateFileNotFoundError(Exception): """Raised if the template file does not exist.""" + + +class ScopedPageAlreadyExists(Exception): + """Raised if the scoped page file already exists.""" diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index b566c6b..bfd0ba1 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -74,7 +74,8 @@ class PageCreator: else self.options.extension ) except IndexError: - # REVIEW: can this be removed now this is not called anywhere else? + # REVIEW: try putting a . in the config yaml and see what error is raised + # this is no longer generic it just gets the full path to the file. # IndexError means the path is not a file, just a path return path_to_file # return path with a file @@ -88,7 +89,9 @@ class PageCreator: self.options.template_path ).expanduser() else: - converted_template_path = pathlib.Path() + 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) @@ -277,19 +280,21 @@ class ScopedPage(Page): 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. if dry_run: tembo.logger.info("%s will be created", self.path) raise SystemExit(0) # create the parent directories scoped_note_file = pathlib.Path(self.path) scoped_note_file.parents[0].mkdir(parents=True, exist_ok=True) - if not scoped_note_file.exists(): - with scoped_note_file.open("w", encoding="utf-8") as scoped_page: - scoped_page.write(self.page_content) - tembo.logger.info("Saved %s to disk", self.path) - else: - tembo.logger.info("%s already exists - skipping.", self.path) - raise SystemExit(0) + if scoped_note_file.exists(): + raise exceptions.ScopedPageAlreadyExists(f"{self.path} already exists") + + with scoped_note_file.open("w", encoding="utf-8") as scoped_page: + scoped_page.write(self.page_content) + # TODO: pass this back somehow + tembo.logger.info("Saved %s to disk", self.path) if __name__ == "__main__": diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index 16b4d54..ef2005d 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -1,7 +1,13 @@ +from datetime import date +import pathlib + import pytest from tembo.journal.pages import PageCreatorOptions, ScopedPageCreator -from tembo.exceptions import BasePathDoesNotExistError +from tembo import exceptions + + +DATE_TODAY = date.today().strftime("%d-%m-%Y") def test_create_page_base_path_does_not_exist(tmpdir): @@ -20,11 +26,218 @@ def test_create_page_base_path_does_not_exist(tmpdir): ) # act - with pytest.raises(BasePathDoesNotExistError) as base_path_does_not_exist_error: - scoped_page_creator = ScopedPageCreator().create_page(options) + with pytest.raises( + exceptions.BasePathDoesNotExistError + ) as base_path_does_not_exist_error: + scoped_page = ScopedPageCreator().create_page(options) # 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="", + filename="", + extension="", + 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().create_page(options) + + # 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().create_page(options) + with pytest.raises(exceptions.ScopedPageAlreadyExists) as page_already_exists: + 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, caplog): + # 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, + ) + # TODO: copy this pattern creation into the other tests + scoped_page_file = ( + pathlib.Path(options.base_path) / options.page_path / options.filename + ).with_suffix(f".{options.extension}") + + # act + scoped_page = ScopedPageCreator().create_page(options) + scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + 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().create_page(options) + scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + 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().create_page(options) + scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + + 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().create_page(options) + scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" diff --git a/tests/test_journal/test_pages/.templates/some_template.md.tpl b/tests/test_journal/test_pages/.templates/some_template.md.tpl deleted file mode 100644 index 1a53169..0000000 --- a/tests/test_journal/test_pages/.templates/some_template.md.tpl +++ /dev/null @@ -1 +0,0 @@ -template contents 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_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 From bfc34e9414d4e2821b2f456ac1d7272469c8f33c Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 01:34:41 +0000 Subject: [PATCH 57/74] adding latest --- dev/notes/test.md | 12 +- tembo/journal/pages.py | 104 ++-------------- tests/test_journal/test_pages.py | 115 +++++++++++++++++- ...emplate_input_tokens_preserve_order.md.tpl | 1 + 4 files changed, 131 insertions(+), 101 deletions(-) create mode 100644 tests/test_journal/test_pages/.templates/some_template_input_tokens_preserve_order.md.tpl diff --git a/dev/notes/test.md b/dev/notes/test.md index 1681544..7566c82 100644 --- a/dev/notes/test.md +++ b/dev/notes/test.md @@ -19,20 +19,20 @@ required: ## tests to write -- user input is None -- page using/not using input tokens - user input does not match number of input tokens - no user input - mismatched user input - with/without example -- page using/not using date tokens -- page using/not using name tokens -- dry run -- path/page filenames can contain spaces and they are converted +- 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 diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index bfd0ba1..59ea5e2 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -55,29 +55,24 @@ class PageCreator: def create_page(self, options: PageCreatorOptions) -> Page: raise NotImplementedError - def _convert_base_path_to_path(self) -> pathlib.Path: - # check if Tembo base path exists + 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(" ", "_") ) - try: - # check for existing `.` in the extension - extension = ( - self.options.extension[1:] - if self.options.extension[0] == "." - else self.options.extension - ) - except IndexError: - # REVIEW: try putting a . in the config yaml and see what error is raised - # this is no longer generic it just gets the full path to the file. - # IndexError means the path is not a file, just a path - return path_to_file + # 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}") @@ -126,6 +121,7 @@ class ScopedPageCreator(PageCreator): def create_page(self, options: PageCreatorOptions) -> Page: self._options = options + self._check_base_path_exists() self._all_input_tokens = self._get_input_tokens() self._verify_input_tokens() @@ -145,8 +141,9 @@ class ScopedPageCreator(PageCreator): self.options.base_path, self.options.page_path, self.options.filename, - self.options.extension, - ).expanduser() + ) + .expanduser() + .with_suffix(f".{self.options.extension}") ) template_contents = self._load_template() # get the input tokens from both the path and the template @@ -295,78 +292,3 @@ class ScopedPage(Page): scoped_page.write(self.page_content) # TODO: pass this back somehow tembo.logger.info("Saved %s to disk", self.path) - - -if __name__ == "__main__": - c = ScopedPageCreator() - # # raises error - # # print(c._substitute_tokens("scratchpad/{input0}-{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file.md", None)) - # print( - # c._substitute_tokens( - # "scratchpad/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file-{input0}.md", ("last",) - # ) - # ) - - # print( - # c.create_page( - # "~/tembo", - # "{name}", - # "{input0}-{input1}-file", - # "md", - # "scratchpad", - # ("first", "second"), - # ) - # ) - # print( - # c.create_page( - # "~/tembo", - # "{name}/{d:MMMM-YY}", - # "{input0}-{d:DD-MM-YYYY}-{d:dddd}-{d:A}-file", - # "md", - # "scratchpad", - # ("first",), - # ) - # ) - # print( - # c.create_page( - # "~/tembo", - # "{name}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", - # "file", - # "md", - # "scratchpad", - # None, - # ) - # ) - # print( - # c.create_page( - # "~/tembo", - # "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", - # "file-{input0}-{name}", - # ".md", - # "meeting", - # ("last",), - # "scratchpad.md.tpl", - # ) - # ) - # test_page_with_template = c.create_page( - # "~/tembo", - # "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", - # "file-{input0}-{name}", - # ".md", - # "meeting", - # ("last",), - # "scratchpad.md.tpl", - # ) - # print(test_page_with_template) - # test_page_with_template.save_to_disk(False) - # print( - # c.create_page( - # "~/tembo", - # "{name}/{d:A}/{d:DD-MM-YYYY}-{d:dddd}-{d:A}", - # "file-{input0}-{name}", - # ".md", - # "meeting", - # ("last",), - # "scratchpad_templates/scratchpad.md.tpl", - # ) - # ) diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index ef2005d..11a35f5 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -43,10 +43,10 @@ def test_create_page_template_file_does_not_exist(template_path, tmpdir): # arrange options = PageCreatorOptions( base_path=str(tmpdir), - page_path="", - filename="", - extension="", - name="", + page_path="some_path", + filename="some_filename", + extension="some_extension", + name="some_name", user_input=None, example=None, template_filename="template.md.tpl", @@ -241,3 +241,110 @@ def test_create_tokened_page_tokens_in_filename( # assert assert scoped_page_file.exists() assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + + +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().create_page(options) + scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + with scoped_page_file.open(mode="r", encoding="utf-8") as scoped_page_contents: + assert scoped_page_contents.readline() == "third_input second_input" + + +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().create_page(options) + scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + + +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().create_page(options) + scoped_page.save_to_disk() + + # assert + assert scoped_page_file.exists() + assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + + +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().create_page(options) + + # assert + assert str(scoped_page) == f"ScopedPage({scoped_page_file})" 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} From f2bca8f2e156d59650caea77e38916b0ac99bea6 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 02:13:34 +0000 Subject: [PATCH 58/74] adding latest --- TODO.todo | 1 + tembo/journal/pages.py | 12 ++++++------ tests/test_journal/test_pages.py | 22 +++++++++++----------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/TODO.todo b/TODO.todo index 1fdcd36..d2f7949 100644 --- a/TODO.todo +++ b/TODO.todo @@ -13,6 +13,7 @@ Documentation: ☐ Document using `__main__.py` and `cli.py` Use Duty as an example + ☐ Document regex usage ☐ Write documentation using `mkdocs` ☐ Look at how to use github actions Use for an example diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 59ea5e2..cb887eb 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -43,6 +43,7 @@ class PageCreatorOptions: class PageCreator: + @abstractmethod def __init__(self, options: PageCreatorOptions) -> None: raise NotImplementedError @@ -52,7 +53,7 @@ class PageCreator: raise NotImplementedError @abstractmethod - def create_page(self, options: PageCreatorOptions) -> Page: + def create_page(self) -> Page: raise NotImplementedError def _check_base_path_exists(self) -> None: @@ -111,16 +112,15 @@ class ScopedPageCreator(PageCreator): extension (str): extension of file. """ - def __init__(self) -> None: + def __init__(self, options: PageCreatorOptions) -> None: self._all_input_tokens: list[str] = [] - self._options: PageCreatorOptions + self._options = options @property def options(self) -> PageCreatorOptions: return self._options - def create_page(self, options: PageCreatorOptions) -> Page: - self._options = options + def create_page(self) -> Page: self._check_base_path_exists() self._all_input_tokens = self._get_input_tokens() @@ -130,7 +130,7 @@ class ScopedPageCreator(PageCreator): path = pathlib.Path(self._substitute_tokens(str(path))) template_contents = self._load_template() - if options.template_filename is not None: + if self.options.template_filename is not None: template_contents = self._substitute_tokens(template_contents) return ScopedPage(path, template_contents) diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index 11a35f5..6a73852 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -29,7 +29,7 @@ def test_create_page_base_path_does_not_exist(tmpdir): with pytest.raises( exceptions.BasePathDoesNotExistError ) as base_path_does_not_exist_error: - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() # assert assert ( @@ -57,7 +57,7 @@ def test_create_page_template_file_does_not_exist(template_path, tmpdir): with pytest.raises( exceptions.TemplateFileNotFoundError ) as template_file_not_found_error: - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() # assert if template_path is None: @@ -88,7 +88,7 @@ def test_create_page_already_exists(datadir): ).with_suffix(f".{options.extension}") # act - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() with pytest.raises(exceptions.ScopedPageAlreadyExists) as page_already_exists: scoped_page.save_to_disk() @@ -118,7 +118,7 @@ def test_create_page_without_template(tmpdir, caplog): ).with_suffix(f".{options.extension}") # act - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() scoped_page.save_to_disk() # assert @@ -146,7 +146,7 @@ def test_create_page_with_template(datadir, caplog): ).with_suffix(f".{options.extension}") # act - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() scoped_page.save_to_disk() # assert @@ -192,7 +192,7 @@ def test_create_tokened_page_tokens_in_template( ).with_suffix(f".{options.extension}") # act - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() scoped_page.save_to_disk() # assert @@ -235,7 +235,7 @@ def test_create_tokened_page_tokens_in_filename( ).with_suffix(f".{options.extension}") # act - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() scoped_page.save_to_disk() # assert @@ -262,7 +262,7 @@ def test_create_tokened_page_input_tokens_preserve_order(datadir, caplog): ).with_suffix(f".{options.extension}") # act - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() scoped_page.save_to_disk() # assert @@ -292,7 +292,7 @@ def test_create_page_spaces_in_path(tmpdir, caplog): ).with_suffix(f".{options.extension}") # act - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() scoped_page.save_to_disk() # assert @@ -318,7 +318,7 @@ def test_create_page_dot_in_extension(tmpdir, caplog): ).with_suffix(f".{options.extension[1:]}") # act - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() scoped_page.save_to_disk() # assert @@ -344,7 +344,7 @@ def test_create_page_str_representation(tmpdir): ).with_suffix(f".{options.extension}") # act - scoped_page = ScopedPageCreator().create_page(options) + scoped_page = ScopedPageCreator(options).create_page() # assert assert str(scoped_page) == f"ScopedPage({scoped_page_file})" From 9b277b29313f32a809e8b873f441bd055d5afb15 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 03:23:46 +0000 Subject: [PATCH 59/74] updating cli.py --- tembo/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tembo/cli.py b/tembo/cli.py index 361243d..8b8d9af 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -114,7 +114,7 @@ def new(scope, inputs, dry_run, example): ) if _name_found: try: - scoped_page = pages.ScopedPageCreator().create_page(page_creator_options) + scoped_page = pages.ScopedPageCreator(page_creator_options).create_page() except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: tembo.logger.critical(base_path_does_not_exist_error) raise SystemExit(1) from base_path_does_not_exist_error From a4df50f77af320ad160a0ee5c20f6f29e36dceee Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 14:11:20 +0000 Subject: [PATCH 60/74] adding latest --- TODO.todo | 4 +++ tembo/cli.py | 23 +++++++++++++-- tembo/exceptions.py | 5 +++- tembo/journal/pages.py | 48 +++++++++----------------------- tests/test_journal/test_pages.py | 31 +++++++++++++++++++++ 5 files changed, 73 insertions(+), 38 deletions(-) diff --git a/TODO.todo b/TODO.todo index d2f7949..e6f1e79 100644 --- a/TODO.todo +++ b/TODO.todo @@ -15,6 +15,7 @@ Documentation: ☐ 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. @@ -37,6 +38,9 @@ Documentation: ☐ 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 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 diff --git a/tembo/cli.py b/tembo/cli.py index 8b8d9af..0d866e0 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -121,9 +121,28 @@ def new(scope, inputs, dry_run, example): except exceptions.TemplateFileNotFoundError as template_file_not_found_error: tembo.logger.critical(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: + tembo.logger.critical( + "Your tembo config.yml/template specifies %s input tokens, you gave %s. Example: %s", + mismatched_token_error.expected, + mismatched_token_error.given, + config_scope["example"], + ) + raise SystemExit(1) from mismatched_token_error + tembo.logger.critical( + "Your tembo config.yml/template specifies %s input tokens, you gave %s", + mismatched_token_error.expected, + mismatched_token_error.given, + ) + raise SystemExit(1) from mismatched_token_error + + if dry_run: + click.echo(cli_message(f"{scoped_page.path} will be created")) + raise SystemExit(0) try: - scoped_page.save_to_disk(dry_run=dry_run) + scoped_page.save_to_disk() raise SystemExit(0) except exceptions.ScopedPageAlreadyExists as scoped_page_already_exists: cli_message(f"File {scoped_page_already_exists}") @@ -150,7 +169,7 @@ run.add_command(list_all) if __name__ == "__main__": # new(["meeting", "robs presentation", "meeting on gcp"]) - new(["meeting", "a", "b", "c", "d"]) + new(["meeting", "a", "b", "c", "d", "e"]) # new(["meeting", "robs presentation"]) # pyinstaller diff --git a/tembo/exceptions.py b/tembo/exceptions.py index 6d9fb1b..581fcb0 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -2,7 +2,10 @@ class MismatchedTokenError(Exception): - pass + def __init__(self, expected: int, given: int) -> None: + self.expected = expected + self.given = given + super().__init__() class BasePathDoesNotExistError(Exception): diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index cb887eb..c7783f9 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -154,35 +154,16 @@ class ScopedPageCreator(PageCreator): def _verify_input_tokens(self) -> None: if len(self._all_input_tokens) > 0 and self.options.user_input is None: - if self.options.example is not None: - tembo.logger.critical( - "Your tembo.config/template specifies %s input tokens, you gave 0. Example command: %s", - len(self._all_input_tokens), - self.options.example, - ) - else: - tembo.logger.critical( - "Your tembo.config/template specifies %s input tokens, you gave 0.", - len(self._all_input_tokens), - ) - raise SystemExit(1) + 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): - if self.options.example is not None: - tembo.logger.critical( - "Your tembo.config/template specifies %s input tokens, you gave %s. Example command: %s", - len(self._all_input_tokens), - len(self.options.user_input), - self.options.example, - ) - else: - tembo.logger.critical( - "Your tembo.config/template specifies %s input tokens, you gave %s.", - len(self._all_input_tokens), - len(self.options.user_input), - ) - raise SystemExit(1) + raise exceptions.MismatchedTokenError( + expected=len(self._all_input_tokens), + given=len(self.options.user_input), + ) return def _substitute_tokens(self, tokenified_string: str) -> str: @@ -236,7 +217,7 @@ class Page(metaclass=ABCMeta): raise NotImplementedError @abstractmethod - def save_to_disk(self, dry_run: bool) -> None: + def save_to_disk(self) -> None: raise NotImplementedError @@ -261,7 +242,7 @@ class ScopedPage(Page): def __str__(self) -> str: return f"ScopedPage({self.path})" - def save_to_disk(self, dry_run: bool = False) -> None: + def save_to_disk(self) -> None: """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 @@ -279,16 +260,13 @@ class ScopedPage(Page): """ # TODO: move this functionality to the CLI so the page is created and the message # returned to the user from the CLI. - if dry_run: - tembo.logger.info("%s will be created", self.path) - raise SystemExit(0) # create the parent directories - scoped_note_file = pathlib.Path(self.path) - scoped_note_file.parents[0].mkdir(parents=True, exist_ok=True) - if scoped_note_file.exists(): + 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_note_file.open("w", encoding="utf-8") as scoped_page: + with scoped_page_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) # TODO: pass this back somehow tembo.logger.info("Saved %s to disk", self.path) diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index 6a73852..7cf101a 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -272,6 +272,37 @@ def test_create_tokened_page_input_tokens_preserve_order(datadir, caplog): 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( From 41f007c9a5f3c573b89066c3c86b68b3431dac17 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 14:20:58 +0000 Subject: [PATCH 61/74] adding latest --- tembo/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tembo/cli.py b/tembo/cli.py index 0d866e0..fcab713 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -149,7 +149,7 @@ def new(scope, inputs, dry_run, example): raise SystemExit(0) from scoped_page_already_exists if not _name_found and len(tembo.CONFIG.scopes) > 0: # if the name is missing in the config.yml, raise error - tembo.logger.warning("Command %s not found in config.yml - exiting", scope) + click.echo(cli_message(f"Command {scope} not found in config.yml.")) raise SystemExit(0) # raise error if no config.yml found From 23eead307a5af56f73eea649921a0ebeb89defeb Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 19:37:12 +0000 Subject: [PATCH 62/74] adding latest to CLI --- tembo/cli.py | 107 ++++++++++++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/tembo/cli.py b/tembo/cli.py index fcab713..bf38f2b 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -1,3 +1,5 @@ +import pathlib + import click import tembo @@ -22,9 +24,9 @@ def run(): """ -@click.command(options_metavar="") +@click.command(options_metavar="", name="list") def list_all(): - """List all names for 'tembo new '.""" + """List all scopes defined in the config.yml""" _all_scopes = [user_scope["name"] for user_scope in tembo.CONFIG.scopes] tembo.logger.info( "%s names found in config.yml: '%s'", len(_all_scopes), "', '".join(_all_scopes) @@ -42,25 +44,21 @@ def list_all(): @click.option("--dry-run", is_flag=True, default=False) @click.option("--example", is_flag=True, default=False) def new(scope, inputs, dry_run, example): - """ + r""" Create a new page. - The name of the scope in the Tembo config.yml. + \n + The name of the scope in the config.yml. - Any input tokens needed in the Tembo 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 """ # get the name from the tembo config.yml - try: - _name_found = scope in [ - user_scope["name"] for user_scope in tembo.CONFIG.scopes - ] - except TypeError as type_error: - # raise error if no scopes are defined - tembo.logger.critical("No scopes found in config.yml - exiting") - raise SystemExit(1) from type_error + _cli_verify_name_exists(scope) # get the scope information from the tembo config.yml config_scope = {} @@ -112,50 +110,61 @@ def new(scope, inputs, dry_run, example): template_filename=config_scope["template_filename"], template_path=tembo.CONFIG.template_path, ) - if _name_found: - try: - scoped_page = pages.ScopedPageCreator(page_creator_options).create_page() - except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: - tembo.logger.critical(base_path_does_not_exist_error) - raise SystemExit(1) from base_path_does_not_exist_error - except exceptions.TemplateFileNotFoundError as template_file_not_found_error: - tembo.logger.critical(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: - tembo.logger.critical( - "Your tembo config.yml/template specifies %s input tokens, you gave %s. Example: %s", - mismatched_token_error.expected, - mismatched_token_error.given, - config_scope["example"], - ) - raise SystemExit(1) from mismatched_token_error + try: + scoped_page = pages.ScopedPageCreator(page_creator_options).create_page() + except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: + tembo.logger.critical(base_path_does_not_exist_error) + raise SystemExit(1) from base_path_does_not_exist_error + except exceptions.TemplateFileNotFoundError as template_file_not_found_error: + tembo.logger.critical(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: tembo.logger.critical( - "Your tembo config.yml/template specifies %s input tokens, you gave %s", + "Your tembo config.yml/template specifies %s input tokens, you gave %s. Example: %s", mismatched_token_error.expected, mismatched_token_error.given, + config_scope["example"], ) raise SystemExit(1) from mismatched_token_error + tembo.logger.critical( + "Your tembo config.yml/template specifies %s input tokens, you gave %s", + mismatched_token_error.expected, + mismatched_token_error.given, + ) + raise SystemExit(1) from mismatched_token_error - if dry_run: - click.echo(cli_message(f"{scoped_page.path} will be created")) - raise SystemExit(0) - - try: - scoped_page.save_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 - if not _name_found and len(tembo.CONFIG.scopes) > 0: - # if the name is missing in the config.yml, raise error - click.echo(cli_message(f"Command {scope} not found in config.yml.")) + if dry_run: + click.echo(cli_message(f"{scoped_page.path} will be created")) raise SystemExit(0) + try: + scoped_page.save_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 _cli_verify_name_exists(scope: str) -> None: + _name_found = scope in [ + user_scope["name"] for user_scope in tembo.CONFIG.scopes + ] + if _name_found: + return + if len(tembo.CONFIG.scopes) > 0: + # if the name is missing in the config.yml, raise error + cli_message(f"Command {scope} not found in config.yml.") + raise SystemExit(0) # raise error if no config.yml found - tembo.logger.critical( - "No config.yml found in %s - exiting", tembo.CONFIG.config_path - ) + if pathlib.Path(tembo.CONFIG.config_path).exists(): + tembo.logger.critical( + "Config.yml found in %s is empty - exiting", tembo.CONFIG.config_path + ) + else: + tembo.logger.critical( + "No config.yml found in %s - exiting", tembo.CONFIG.config_path + ) raise SystemExit(1) @@ -169,7 +178,7 @@ run.add_command(list_all) if __name__ == "__main__": # new(["meeting", "robs presentation", "meeting on gcp"]) - new(["meeting", "a", "b", "c", "d", "e"]) + new(["meeting", "a", "b", "c", "d"]) # new(["meeting", "robs presentation"]) # pyinstaller From b114fa94bd2ae6432559ebdc5ee5906e5de474bb Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 19:58:32 +0000 Subject: [PATCH 63/74] adding latest to CLI --- tembo/cli.py | 55 ++++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/tembo/cli.py b/tembo/cli.py index bf38f2b..ac42cb7 100644 --- a/tembo/cli.py +++ b/tembo/cli.py @@ -61,31 +61,7 @@ def new(scope, inputs, dry_run, example): _cli_verify_name_exists(scope) # get the scope information from the tembo config.yml - config_scope = {} - for option in [ - "name", - "example", - "path", - "filename", - "extension", - "template_filename", - ]: - try: - config_scope.update( - { - option: str(user_scope[option]) - for user_scope in tembo.CONFIG.scopes - if user_scope["name"] == scope - } - ) - except KeyError as key_error: - if key_error.args[0] in ["example", "template_filename"]: - config_scope.update({key_error.args[0]: None}) - continue - tembo.logger.critical( - "Key %s not found in config. yml - exiting", key_error - ) - raise SystemExit(1) from key_error + config_scope = _cli_get_config_scope(scope) # print the example to the user if example: @@ -168,6 +144,35 @@ def _cli_verify_name_exists(scope: str) -> None: raise SystemExit(1) +def _cli_get_config_scope(scope: str) -> dict: + config_scope = {} + for option in [ + "name", + "example", + "path", + "filename", + "extension", + "template_filename", + ]: + try: + config_scope.update( + { + option: str(user_scope[option]) + for user_scope in tembo.CONFIG.scopes + if user_scope["name"] == scope + } + ) + except KeyError as key_error: + if key_error.args[0] in ["example", "template_filename"]: + config_scope.update({key_error.args[0]: None}) + continue + tembo.logger.critical( + "Key %s not found in config. yml - exiting", key_error + ) + raise SystemExit(1) from key_error + return config_scope + + def cli_message(message: str) -> None: click.echo(f"[TEMBO] {message} 🐘") From 4e20fdc2d12e47ac3d048fc36255cda737fda76f Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 20:21:30 +0000 Subject: [PATCH 64/74] adding latest to CLI --- tembo/{ => journal/cli}/cli.py | 114 ++++++++++++++++++--------------- tembo/journal/cli/new.py | 0 tembo/journal/pages.py | 11 +++- 3 files changed, 72 insertions(+), 53 deletions(-) rename tembo/{ => journal/cli}/cli.py (86%) create mode 100644 tembo/journal/cli/new.py diff --git a/tembo/cli.py b/tembo/journal/cli/cli.py similarity index 86% rename from tembo/cli.py rename to tembo/journal/cli/cli.py index ac42cb7..52561d1 100644 --- a/tembo/cli.py +++ b/tembo/journal/cli/cli.py @@ -1,4 +1,7 @@ +from __future__ import annotations + import pathlib +from typing import Collection import click @@ -26,7 +29,7 @@ def run(): @click.command(options_metavar="", name="list") def list_all(): - """List all scopes defined in the config.yml""" + """List all scopes defined in the config.yml.""" _all_scopes = [user_scope["name"] for user_scope in tembo.CONFIG.scopes] tembo.logger.info( "%s names found in config.yml: '%s'", len(_all_scopes), "', '".join(_all_scopes) @@ -43,7 +46,7 @@ def list_all(): ) @click.option("--dry-run", is_flag=True, default=False) @click.option("--example", is_flag=True, default=False) -def new(scope, inputs, dry_run, example): +def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool): r""" Create a new page. @@ -57,61 +60,20 @@ def new(scope, inputs, dry_run, example): Example: tembo new meeting my_presentation """ - # get the name from the tembo config.yml + # check that the name exists in the config.yml _cli_verify_name_exists(scope) - # get the scope information from the tembo config.yml + # get the scope configuration from the config.yml config_scope = _cli_get_config_scope(scope) - # print the example to the user - if example: - tembo.logger.info( - "Example for 'tembo new %s': %s", - config_scope["name"], - config_scope["example"] - if isinstance(config_scope["example"], str) - else "No example in config.yml", - ) - raise SystemExit(0) + # if --example flag, return the example to the user + _cli_show_example(example, config_scope) # if the name is in the config.yml, create the scoped page - page_creator_options = pages.PageCreatorOptions( - base_path=tembo.CONFIG.base_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"], - template_path=tembo.CONFIG.template_path, - ) - try: - scoped_page = pages.ScopedPageCreator(page_creator_options).create_page() - except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: - tembo.logger.critical(base_path_does_not_exist_error) - raise SystemExit(1) from base_path_does_not_exist_error - except exceptions.TemplateFileNotFoundError as template_file_not_found_error: - tembo.logger.critical(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: - tembo.logger.critical( - "Your tembo config.yml/template specifies %s input tokens, you gave %s. Example: %s", - mismatched_token_error.expected, - mismatched_token_error.given, - config_scope["example"], - ) - raise SystemExit(1) from mismatched_token_error - tembo.logger.critical( - "Your tembo config.yml/template specifies %s input tokens, you gave %s", - mismatched_token_error.expected, - mismatched_token_error.given, - ) - raise SystemExit(1) from mismatched_token_error + scoped_page = _cli_create_scoped_page(config_scope, inputs) if dry_run: - click.echo(cli_message(f"{scoped_page.path} will be created")) + cli_message(f"{scoped_page.path} will be created") raise SystemExit(0) try: @@ -123,9 +85,7 @@ def new(scope, inputs, dry_run, example): def _cli_verify_name_exists(scope: str) -> None: - _name_found = scope in [ - user_scope["name"] for user_scope in tembo.CONFIG.scopes - ] + _name_found = scope in [user_scope["name"] for user_scope in tembo.CONFIG.scopes] if _name_found: return if len(tembo.CONFIG.scopes) > 0: @@ -173,6 +133,56 @@ def _cli_get_config_scope(scope: str) -> dict: return config_scope +def _cli_show_example(example: bool, config_scope: dict) -> None: + if example: + tembo.logger.info( + "Example for 'tembo new %s': %s", + config_scope["name"], + config_scope["example"] + if isinstance(config_scope["example"], str) + else "No example in config.yml", + ) + raise SystemExit(0) + + +def _cli_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> pages.Page: + page_creator_options = pages.PageCreatorOptions( + base_path=tembo.CONFIG.base_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"], + template_path=tembo.CONFIG.template_path, + ) + try: + return pages.ScopedPageCreator(page_creator_options).create_page() + except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: + tembo.logger.critical(base_path_does_not_exist_error) + raise SystemExit(1) from base_path_does_not_exist_error + except exceptions.TemplateFileNotFoundError as template_file_not_found_error: + tembo.logger.critical(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: + tembo.logger.critical( + "Your tembo config.yml/template specifies %s input tokens, you gave %s. Example: %s", + mismatched_token_error.expected, + mismatched_token_error.given, + config_scope["example"], + ) + raise SystemExit(1) from mismatched_token_error + tembo.logger.critical( + "Your tembo config.yml/template specifies %s input tokens, you gave %s", + mismatched_token_error.expected, + mismatched_token_error.given, + ) + raise SystemExit(1) from mismatched_token_error + + + def cli_message(message: str) -> None: click.echo(f"[TEMBO] {message} 🐘") diff --git a/tembo/journal/cli/new.py b/tembo/journal/cli/new.py new file mode 100644 index 0000000..e69de29 diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index c7783f9..536b124 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -216,6 +216,11 @@ class Page(metaclass=ABCMeta): 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) -> None: raise NotImplementedError @@ -236,12 +241,16 @@ class ScopedPage(Page): 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._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) -> None: """Save the scoped page to disk and write the `page_content`. From 779e99434f1a913cb51ce8180543d2566214a2e2 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Mon, 1 Nov 2021 20:36:16 +0000 Subject: [PATCH 65/74] adding latest to CLI --- pyproject.toml | 2 +- tembo/__init__.py | 29 +--------------------- tembo/cli/__init__.py | 30 +++++++++++++++++++++++ tembo/{journal => }/cli/cli.py | 41 ++++++++++++++++---------------- tembo/journal/cli/new.py | 0 tembo/journal/pages.py | 2 +- tests/test_journal/test_pages.py | 2 +- 7 files changed, 54 insertions(+), 52 deletions(-) create mode 100644 tembo/cli/__init__.py rename tembo/{journal => }/cli/cli.py (85%) delete mode 100644 tembo/journal/cli/new.py diff --git a/pyproject.toml b/pyproject.toml index c3e2c86..c2626b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,4 +24,4 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] -"tembo" = "tembo.cli:run" +"tembo" = "tembo.cli.cli:run" diff --git a/tembo/__init__.py b/tembo/__init__.py index c688a8d..4cd6b98 100644 --- a/tembo/__init__.py +++ b/tembo/__init__.py @@ -1,30 +1,3 @@ -import os - -import panaetius -from panaetius.exceptions import LoggingDirectoryDoesNotExistException +from .journal.pages import ScopedPageCreator, PageCreatorOptions __version__ = "0.1.0" - -if (config_path := os.environ.get("TEMBO_CONFIG")) is not None: - CONFIG = panaetius.Config("tembo", config_path) -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/__init__.py b/tembo/cli/__init__.py new file mode 100644 index 0000000..c688a8d --- /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) +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/journal/cli/cli.py b/tembo/cli/cli.py similarity index 85% rename from tembo/journal/cli/cli.py rename to tembo/cli/cli.py index 52561d1..8a59c5d 100644 --- a/tembo/journal/cli/cli.py +++ b/tembo/cli/cli.py @@ -5,7 +5,7 @@ from typing import Collection import click -import tembo +import tembo.cli from tembo.journal import pages from tembo import exceptions @@ -30,8 +30,8 @@ def run(): @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.CONFIG.scopes] - tembo.logger.info( + _all_scopes = [user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes] + tembo.cli.logger.info( "%s names found in config.yml: '%s'", len(_all_scopes), "', '".join(_all_scopes) ) raise SystemExit(0) @@ -85,21 +85,21 @@ def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool): def _cli_verify_name_exists(scope: str) -> None: - _name_found = scope in [user_scope["name"] for user_scope in tembo.CONFIG.scopes] + _name_found = scope in [user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes] if _name_found: return - if len(tembo.CONFIG.scopes) > 0: + if len(tembo.cli.CONFIG.scopes) > 0: # if the name is missing in the config.yml, raise error cli_message(f"Command {scope} not found in config.yml.") raise SystemExit(0) # raise error if no config.yml found - if pathlib.Path(tembo.CONFIG.config_path).exists(): - tembo.logger.critical( - "Config.yml found in %s is empty - exiting", tembo.CONFIG.config_path + if pathlib.Path(tembo.cli.CONFIG.config_path).exists(): + tembo.cli.logger.critical( + "Config.yml found in %s is empty - exiting", tembo.cli.CONFIG.config_path ) else: - tembo.logger.critical( - "No config.yml found in %s - exiting", tembo.CONFIG.config_path + tembo.cli.logger.critical( + "No config.yml found in %s - exiting", tembo.cli.CONFIG.config_path ) raise SystemExit(1) @@ -118,7 +118,7 @@ def _cli_get_config_scope(scope: str) -> dict: config_scope.update( { option: str(user_scope[option]) - for user_scope in tembo.CONFIG.scopes + for user_scope in tembo.cli.CONFIG.scopes if user_scope["name"] == scope } ) @@ -126,7 +126,7 @@ def _cli_get_config_scope(scope: str) -> dict: if key_error.args[0] in ["example", "template_filename"]: config_scope.update({key_error.args[0]: None}) continue - tembo.logger.critical( + tembo.cli.logger.critical( "Key %s not found in config. yml - exiting", key_error ) raise SystemExit(1) from key_error @@ -135,7 +135,7 @@ def _cli_get_config_scope(scope: str) -> dict: def _cli_show_example(example: bool, config_scope: dict) -> None: if example: - tembo.logger.info( + tembo.cli.logger.info( "Example for 'tembo new %s': %s", config_scope["name"], config_scope["example"] @@ -147,7 +147,7 @@ def _cli_show_example(example: bool, config_scope: dict) -> None: def _cli_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> pages.Page: page_creator_options = pages.PageCreatorOptions( - base_path=tembo.CONFIG.base_path, + base_path=tembo.cli.CONFIG.base_path, page_path=config_scope["path"], filename=config_scope["filename"], extension=config_scope["extension"], @@ -155,26 +155,26 @@ def _cli_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> page example=config_scope["example"], user_input=inputs, template_filename=config_scope["template_filename"], - template_path=tembo.CONFIG.template_path, + template_path=tembo.cli.CONFIG.template_path, ) try: return pages.ScopedPageCreator(page_creator_options).create_page() except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: - tembo.logger.critical(base_path_does_not_exist_error) + tembo.cli.logger.critical(base_path_does_not_exist_error) raise SystemExit(1) from base_path_does_not_exist_error except exceptions.TemplateFileNotFoundError as template_file_not_found_error: - tembo.logger.critical(template_file_not_found_error.args[0]) + tembo.cli.logger.critical(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: - tembo.logger.critical( + tembo.cli.logger.critical( "Your tembo config.yml/template specifies %s input tokens, you gave %s. Example: %s", mismatched_token_error.expected, mismatched_token_error.given, config_scope["example"], ) raise SystemExit(1) from mismatched_token_error - tembo.logger.critical( + tembo.cli.logger.critical( "Your tembo config.yml/template specifies %s input tokens, you gave %s", mismatched_token_error.expected, mismatched_token_error.given, @@ -182,7 +182,6 @@ def _cli_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> page raise SystemExit(1) from mismatched_token_error - def cli_message(message: str) -> None: click.echo(f"[TEMBO] {message} 🐘") @@ -193,7 +192,7 @@ run.add_command(list_all) if __name__ == "__main__": # new(["meeting", "robs presentation", "meeting on gcp"]) - new(["meeting", "a", "b", "c", "d"]) + new(["meeting", "a", "b", "c", "d"]) # noqa # new(["meeting", "robs presentation"]) # pyinstaller diff --git a/tembo/journal/cli/new.py b/tembo/journal/cli/new.py deleted file mode 100644 index e69de29..0000000 diff --git a/tembo/journal/pages.py b/tembo/journal/pages.py index 536b124..417f969 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -278,4 +278,4 @@ class ScopedPage(Page): with scoped_page_file.open("w", encoding="utf-8") as scoped_page: scoped_page.write(self.page_content) # TODO: pass this back somehow - tembo.logger.info("Saved %s to disk", self.path) + tembo.cli.cli.logger.info("Saved %s to disk", self.path) diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index 7cf101a..7164fcc 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -3,7 +3,7 @@ import pathlib import pytest -from tembo.journal.pages import PageCreatorOptions, ScopedPageCreator +from tembo import PageCreatorOptions, ScopedPageCreator from tembo import exceptions From 1cc68834f0d85c91a302200024a5288586c5ace2 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Wed, 3 Nov 2021 13:46:58 +0000 Subject: [PATCH 66/74] adding latest --- tembo/cli/__init__.py | 2 +- tembo/cli/cli.py | 65 ++++++++++++------- tembo/exceptions.py | 16 +++++ tembo/journal/pages.py | 11 ++-- tembo/utils/__init__.py | 12 ++++ tests/test_cli/__init__.py | 0 tests/test_cli/data/config/success/config.yml | 7 ++ tests/test_cli/test_cli.py | 50 ++++++++++++++ tests/test_journal/test_pages.py | 43 +++++++----- 9 files changed, 156 insertions(+), 50 deletions(-) create mode 100644 tembo/utils/__init__.py create mode 100644 tests/test_cli/__init__.py create mode 100644 tests/test_cli/data/config/success/config.yml create mode 100644 tests/test_cli/test_cli.py diff --git a/tembo/cli/__init__.py b/tembo/cli/__init__.py index c688a8d..f4076cb 100644 --- a/tembo/cli/__init__.py +++ b/tembo/cli/__init__.py @@ -6,7 +6,7 @@ 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) + CONFIG = panaetius.Config("tembo", config_path, skip_header_init=True) else: CONFIG = panaetius.Config("tembo", "~/tembo/.config", skip_header_init=True) diff --git a/tembo/cli/cli.py b/tembo/cli/cli.py index 8a59c5d..2520823 100644 --- a/tembo/cli/cli.py +++ b/tembo/cli/cli.py @@ -7,6 +7,7 @@ import click import tembo.cli from tembo.journal import pages +from tembo.utils import Success from tembo import exceptions @@ -61,57 +62,71 @@ def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool): """ # check that the name exists in the config.yml - _cli_verify_name_exists(scope) + try: + _new_verify_name_exists(scope) + except ( + exceptions.ScopeNotFound, + exceptions.EmptyConfigYML, + exceptions.MissingConfigYML, + ) as tembo_exception: + cli_message(tembo_exception.args[0]) + raise SystemExit(0) from tembo_exception # get the scope configuration from the config.yml - config_scope = _cli_get_config_scope(scope) + 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 - _cli_show_example(example, config_scope) + _new_show_example(example, config_scope) # if the name is in the config.yml, create the scoped page - scoped_page = _cli_create_scoped_page(config_scope, inputs) + 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: - scoped_page.save_to_disk() + 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 _cli_verify_name_exists(scope: str) -> None: - _name_found = scope in [user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes] +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 - cli_message(f"Command {scope} not found in config.yml.") - raise SystemExit(0) + raise exceptions.ScopeNotFound(f"Command {scope} not found in config.yml") # raise error if no config.yml found if pathlib.Path(tembo.cli.CONFIG.config_path).exists(): - tembo.cli.logger.critical( - "Config.yml found in %s is empty - exiting", tembo.cli.CONFIG.config_path + raise exceptions.EmptyConfigYML( + f"Config.yml found in {tembo.cli.CONFIG.config_path} is empty" ) - else: - tembo.cli.logger.critical( - "No config.yml found in %s - exiting", tembo.cli.CONFIG.config_path - ) - raise SystemExit(1) + raise exceptions.MissingConfigYML( + f"No config.yml found in {tembo.cli.CONFIG.config_path}" + ) -def _cli_get_config_scope(scope: str) -> dict: +def _new_get_config_scope(scope: str) -> dict: config_scope = {} + optional_keys = ["example", "template_filename"] for option in [ "name", - "example", "path", "filename", "extension", + "example", "template_filename", ]: try: @@ -123,17 +138,19 @@ def _cli_get_config_scope(scope: str) -> dict: } ) except KeyError as key_error: - if key_error.args[0] in ["example", "template_filename"]: + if key_error.args[0] in optional_keys: config_scope.update({key_error.args[0]: None}) continue tembo.cli.logger.critical( "Key %s not found in config. yml - exiting", key_error ) - raise SystemExit(1) from key_error + raise exceptions.MandatoryKeyNotFound( + f"Key {key_error} not found in config.yml" + ) return config_scope -def _cli_show_example(example: bool, config_scope: dict) -> None: +def _new_show_example(example: bool, config_scope: dict) -> None: if example: tembo.cli.logger.info( "Example for 'tembo new %s': %s", @@ -145,9 +162,10 @@ def _cli_show_example(example: bool, config_scope: dict) -> None: raise SystemExit(0) -def _cli_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> pages.Page: +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"], @@ -155,7 +173,6 @@ def _cli_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> page example=config_scope["example"], user_input=inputs, template_filename=config_scope["template_filename"], - template_path=tembo.cli.CONFIG.template_path, ) try: return pages.ScopedPageCreator(page_creator_options).create_page() @@ -191,9 +208,7 @@ run.add_command(list_all) if __name__ == "__main__": - # new(["meeting", "robs presentation", "meeting on gcp"]) new(["meeting", "a", "b", "c", "d"]) # noqa - # new(["meeting", "robs presentation"]) # pyinstaller # if getattr(sys, "frozen", False): diff --git a/tembo/exceptions.py b/tembo/exceptions.py index 581fcb0..df6a885 100644 --- a/tembo/exceptions.py +++ b/tembo/exceptions.py @@ -18,3 +18,19 @@ class TemplateFileNotFoundError(Exception): 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/pages.py b/tembo/journal/pages.py index 417f969..4810cdf 100644 --- a/tembo/journal/pages.py +++ b/tembo/journal/pages.py @@ -12,6 +12,7 @@ from jinja2.exceptions import TemplateNotFound import tembo from tembo import exceptions +import tembo.utils # TODO: flesh this out with details for the optional args @@ -222,7 +223,7 @@ class Page(metaclass=ABCMeta): raise NotImplementedError @abstractmethod - def save_to_disk(self) -> None: + def save_to_disk(self) -> tembo.utils.Success: raise NotImplementedError @@ -245,13 +246,13 @@ class ScopedPage(Page): self.page_content = page_content def __str__(self) -> str: - return f"ScopedPage({self.path})" + return f"ScopedPage(\"{self.path}\")" @property def path(self) -> pathlib.Path: return self._path - def save_to_disk(self) -> None: + 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 @@ -274,8 +275,6 @@ class ScopedPage(Page): 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) - # TODO: pass this back somehow - tembo.cli.cli.logger.info("Saved %s to disk", self.path) + 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/success/config.yml b/tests/test_cli/data/config/success/config.yml new file mode 100644 index 0000000..8830957 --- /dev/null +++ b/tests/test_cli/data/config/success/config.yml @@ -0,0 +1,7 @@ +tembo: + scopes: + - name: some_scope + example: tembo new some_scope + path: "some_scope" + filename: "{name}" + extension: md diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py new file mode 100644 index 0000000..0eb5fd9 --- /dev/null +++ b/tests/test_cli/test_cli.py @@ -0,0 +1,50 @@ +import os + +import pytest + +import tembo.exceptions + + +def test_cli_page_is_saved_success(): + pass + + +def test_new_verify_name_exists_success(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + import tembo.cli + from tembo.cli.cli import _new_verify_name_exists + + c = tembo.cli.CONFIG + + # act + verified_name = _new_verify_name_exists("some_scope") + + # assert + assert verified_name is None + + +def test_new_verify_name_exists_scope_not_found(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + import tembo.cli + from tembo.cli.cli import _new_verify_name_exists + + c = tembo.cli.CONFIG + + # act + with pytest.raises(tembo.exceptions.ScopeNotFound) as scope_not_found: + _new_verify_name_exists("some_missing_scope") + + # assert + assert str(scope_not_found.value) == "Command some_missing_scope not found in config.yml" + + +def test_new_get_config_scope(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + import tembo.cli + + # act + + # assert diff --git a/tests/test_journal/test_pages.py b/tests/test_journal/test_pages.py index 7164fcc..399c6c8 100644 --- a/tests/test_journal/test_pages.py +++ b/tests/test_journal/test_pages.py @@ -5,6 +5,7 @@ import pytest from tembo import PageCreatorOptions, ScopedPageCreator from tembo import exceptions +from tembo.utils import Success DATE_TODAY = date.today().strftime("%d-%m-%Y") @@ -90,7 +91,7 @@ def test_create_page_already_exists(datadir): # act scoped_page = ScopedPageCreator(options).create_page() with pytest.raises(exceptions.ScopedPageAlreadyExists) as page_already_exists: - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() @@ -99,7 +100,7 @@ def test_create_page_already_exists(datadir): assert scoped_page_contents.readlines() == ["this file already exists\n"] -def test_create_page_without_template(tmpdir, caplog): +def test_create_page_without_template(tmpdir): # arrange options = PageCreatorOptions( base_path=str(tmpdir), @@ -112,18 +113,18 @@ def test_create_page_without_template(tmpdir, caplog): template_filename=None, template_path=None, ) - # TODO: copy this pattern creation into the other tests 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() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + 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() == [] @@ -147,11 +148,12 @@ def test_create_page_with_template(datadir, caplog): # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + 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", @@ -193,11 +195,12 @@ def test_create_tokened_page_tokens_in_template( # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + 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 @@ -236,11 +239,12 @@ def test_create_tokened_page_tokens_in_filename( # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) def test_create_tokened_page_input_tokens_preserve_order(datadir, caplog): @@ -263,11 +267,12 @@ def test_create_tokened_page_input_tokens_preserve_order(datadir, caplog): # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + 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" @@ -324,11 +329,12 @@ def test_create_page_spaces_in_path(tmpdir, caplog): # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) def test_create_page_dot_in_extension(tmpdir, caplog): @@ -350,11 +356,12 @@ def test_create_page_dot_in_extension(tmpdir, caplog): # act scoped_page = ScopedPageCreator(options).create_page() - scoped_page.save_to_disk() + result = scoped_page.save_to_disk() # assert assert scoped_page_file.exists() - assert caplog.records[0].message == f"Saved {scoped_page_file} to disk" + assert isinstance(result, Success) + assert result.message == str(scoped_page_file) def test_create_page_str_representation(tmpdir): @@ -378,4 +385,4 @@ def test_create_page_str_representation(tmpdir): scoped_page = ScopedPageCreator(options).create_page() # assert - assert str(scoped_page) == f"ScopedPage({scoped_page_file})" + assert str(scoped_page) == f"ScopedPage(\"{scoped_page_file}\")" From bfb4d7fd8f83a4e05013e7bce1af8948c2d79913 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Wed, 3 Nov 2021 14:01:48 +0000 Subject: [PATCH 67/74] adding latest --- tembo/__init__.py | 1 + tests/test_cli/data/config/empty/config.yml | 1 + tests/test_cli/test_cli.py | 59 ++++++++++++++++++--- 3 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 tests/test_cli/data/config/empty/config.yml diff --git a/tembo/__init__.py b/tembo/__init__.py index 4cd6b98..d15ffc9 100644 --- a/tembo/__init__.py +++ b/tembo/__init__.py @@ -1,3 +1,4 @@ from .journal.pages import ScopedPageCreator, PageCreatorOptions +from . import exceptions __version__ = "0.1.0" 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/test_cli.py b/tests/test_cli/test_cli.py index 0eb5fd9..ba4539f 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -15,14 +15,15 @@ def test_new_verify_name_exists_success(shared_datadir): import tembo.cli from tembo.cli.cli import _new_verify_name_exists - c = tembo.cli.CONFIG - # act verified_name = _new_verify_name_exists("some_scope") # assert assert verified_name is None + # cleanup + del os.environ["TEMBO_CONFIG"] + def test_new_verify_name_exists_scope_not_found(shared_datadir): # arrange @@ -30,21 +31,65 @@ def test_new_verify_name_exists_scope_not_found(shared_datadir): import tembo.cli from tembo.cli.cli import _new_verify_name_exists - c = tembo.cli.CONFIG - # act with pytest.raises(tembo.exceptions.ScopeNotFound) as scope_not_found: _new_verify_name_exists("some_missing_scope") # assert - assert str(scope_not_found.value) == "Command some_missing_scope not found in config.yml" + assert ( + str(scope_not_found.value) + == "Command some_missing_scope not found in config.yml" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] -def test_new_get_config_scope(shared_datadir): +def test_new_verify_name_exists_empty_config(shared_datadir): # arrange - os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "empty") import tembo.cli + from tembo.cli.cli import _new_verify_name_exists # act + with pytest.raises(tembo.exceptions.EmptyConfigYML) as empty_config_yml: + _new_verify_name_exists("some_missing_scope") # assert + assert ( + str(empty_config_yml.value) + == f'Config.yml found in {os.environ["TEMBO_CONFIG"]} is empty' + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +def test_new_verify_name_exists_missing_config(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing") + import tembo.cli + from tembo.cli.cli import _new_verify_name_exists + + # act + with pytest.raises(tembo.exceptions.MissingConfigYML) as missing_config_yml: + _new_verify_name_exists("some_missing_scope") + + # assert + assert ( + str(missing_config_yml.value) + == f'No config.yml found in {os.environ["TEMBO_CONFIG"]}' + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + + +# def test_new_get_config_scope(shared_datadir): +# # arrange +# os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") +# import tembo.cli + +# # act + +# # assert From 544c90eeed94ca7a1aef97b73b9a15b3fe0fc006 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Wed, 3 Nov 2021 20:48:40 +0000 Subject: [PATCH 68/74] adding latest --- TODO.todo | 7 +++ tembo/cli/cli.py | 3 -- .../data/config/missing_keys/config.yml | 5 ++ .../data/config/optional_keys/config.yml | 6 +++ tests/test_cli/test_cli.py | 51 +++++++++++++++---- 5 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 tests/test_cli/data/config/missing_keys/config.yml create mode 100644 tests/test_cli/data/config/optional_keys/config.yml diff --git a/TODO.todo b/TODO.todo index e6f1e79..1ec3271 100644 --- a/TODO.todo +++ b/TODO.todo @@ -46,6 +46,13 @@ Documentation: ☐ 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 diff --git a/tembo/cli/cli.py b/tembo/cli/cli.py index 2520823..2ce4c1d 100644 --- a/tembo/cli/cli.py +++ b/tembo/cli/cli.py @@ -141,9 +141,6 @@ def _new_get_config_scope(scope: str) -> dict: if key_error.args[0] in optional_keys: config_scope.update({key_error.args[0]: None}) continue - tembo.cli.logger.critical( - "Key %s not found in config. yml - exiting", key_error - ) raise exceptions.MandatoryKeyNotFound( f"Key {key_error} not found in config.yml" ) 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/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/test_cli.py b/tests/test_cli/test_cli.py index ba4539f..2d56814 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -1,8 +1,11 @@ +import importlib import os import pytest import tembo.exceptions +import tembo.cli +from tembo.cli.cli import _new_verify_name_exists, _new_get_config_scope def test_cli_page_is_saved_success(): @@ -12,8 +15,7 @@ def test_cli_page_is_saved_success(): def test_new_verify_name_exists_success(shared_datadir): # arrange os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") - import tembo.cli - from tembo.cli.cli import _new_verify_name_exists + importlib.reload(tembo.cli) # act verified_name = _new_verify_name_exists("some_scope") @@ -28,7 +30,7 @@ def test_new_verify_name_exists_success(shared_datadir): def test_new_verify_name_exists_scope_not_found(shared_datadir): # arrange os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") - import tembo.cli + importlib.reload(tembo.cli) from tembo.cli.cli import _new_verify_name_exists # act @@ -48,7 +50,7 @@ def test_new_verify_name_exists_scope_not_found(shared_datadir): def test_new_verify_name_exists_empty_config(shared_datadir): # arrange os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "empty") - import tembo.cli + importlib.reload(tembo.cli) from tembo.cli.cli import _new_verify_name_exists # act @@ -68,7 +70,7 @@ def test_new_verify_name_exists_empty_config(shared_datadir): def test_new_verify_name_exists_missing_config(shared_datadir): # arrange os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing") - import tembo.cli + importlib.reload(tembo.cli) from tembo.cli.cli import _new_verify_name_exists # act @@ -85,11 +87,38 @@ def test_new_verify_name_exists_missing_config(shared_datadir): del os.environ["TEMBO_CONFIG"] -# def test_new_get_config_scope(shared_datadir): -# # arrange -# os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") -# import tembo.cli +def test_new_get_config_scope_success(shared_datadir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "optional_keys") + importlib.reload(tembo.cli) -# # act + # act + config_scope = _new_get_config_scope("some_scope") -# # assert + # assert + assert config_scope == { + "name": "some_scope", + "path": "some_scope", + "filename": "{name}", + "extension": "md", + "example": None, + "template_filename": None, + } + + +def test_new_get_config_scope_key_not_found(shared_datadir): + # arrange + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing_keys") + importlib.reload(tembo.cli) + + # act + with pytest.raises( + tembo.exceptions.MandatoryKeyNotFound + ) as mandatory_key_not_found: + config_scope = _new_get_config_scope("some_scope") + + # assert + assert ( + str(mandatory_key_not_found.value) == "Key 'filename' not found in config.yml" + ) From 83db380fecfddf29b6e8635c88112ed2ef21d62c Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Thu, 4 Nov 2021 00:01:38 +0000 Subject: [PATCH 69/74] adding latest --- TODO.todo | 7 +++++++ tembo/cli/cli.py | 13 ++++++------- tests/test_cli/test_cli.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/TODO.todo b/TODO.todo index 1ec3271..05ddf5f 100644 --- a/TODO.todo +++ b/TODO.todo @@ -42,6 +42,11 @@ Documentation: Overwrite `__init__`, access them in pytest with `.value.$args` Access them in a try,except with `raise $excpetion as $name; $name.$arg` + ☐ Document capturing stdout + 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 @@ -68,6 +73,8 @@ Functionality: ☐ 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 diff --git a/tembo/cli/cli.py b/tembo/cli/cli.py index 2ce4c1d..b2e47a8 100644 --- a/tembo/cli/cli.py +++ b/tembo/cli/cli.py @@ -149,13 +149,12 @@ def _new_get_config_scope(scope: str) -> dict: def _new_show_example(example: bool, config_scope: dict) -> None: if example: - tembo.cli.logger.info( - "Example for 'tembo new %s': %s", - config_scope["name"], - config_scope["example"] - if isinstance(config_scope["example"], str) - else "No example in config.yml", - ) + 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) diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py index 2d56814..039e12f 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -5,7 +5,11 @@ import pytest import tembo.exceptions import tembo.cli -from tembo.cli.cli import _new_verify_name_exists, _new_get_config_scope +from tembo.cli.cli import ( + _new_verify_name_exists, + _new_get_config_scope, + _new_show_example, +) def test_cli_page_is_saved_success(): @@ -107,7 +111,6 @@ def test_new_get_config_scope_success(shared_datadir): def test_new_get_config_scope_key_not_found(shared_datadir): - # arrange # arrange os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing_keys") importlib.reload(tembo.cli) @@ -122,3 +125,25 @@ def test_new_get_config_scope_key_not_found(shared_datadir): assert ( str(mandatory_key_not_found.value) == "Key 'filename' not found in config.yml" ) + + +@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) + config_scope = _new_get_config_scope("some_scope") + + # act + with pytest.raises(SystemExit) as system_exit: + _new_show_example(True, config_scope) + + # assert + assert capsys.readouterr().out == message + assert system_exit.value.code == 0 From 37657563a09581cb5af79166634de94b796882e5 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Thu, 4 Nov 2021 08:39:00 +0000 Subject: [PATCH 70/74] adding latest --- TODO.todo | 1 + tembo/cli/cli.py | 18 +++---- tests/test_cli/test_cli.py | 96 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 11 deletions(-) diff --git a/TODO.todo b/TODO.todo index 05ddf5f..2ad76f5 100644 --- a/TODO.todo +++ b/TODO.todo @@ -46,6 +46,7 @@ Documentation: Use `capsys` `assert capsys.readouterr().out` A new line may be inserted if using `click.echo()` + ☐ Document using datadir with a module rather than a shared one. Link to tembo as an example. ☐ Can prospector ignore tests dir? document this in the gist if so diff --git a/tembo/cli/cli.py b/tembo/cli/cli.py index b2e47a8..0014fea 100644 --- a/tembo/cli/cli.py +++ b/tembo/cli/cli.py @@ -173,25 +173,21 @@ def _new_create_scoped_page(config_scope: dict, inputs: Collection[str]) -> page try: return pages.ScopedPageCreator(page_creator_options).create_page() except exceptions.BasePathDoesNotExistError as base_path_does_not_exist_error: - tembo.cli.logger.critical(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: - tembo.cli.logger.critical(template_file_not_found_error.args[0]) + 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: - tembo.cli.logger.critical( - "Your tembo config.yml/template specifies %s input tokens, you gave %s. Example: %s", - mismatched_token_error.expected, - mismatched_token_error.given, - config_scope["example"], + 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 - tembo.cli.logger.critical( - "Your tembo config.yml/template specifies %s input tokens, you gave %s", - mismatched_token_error.expected, - mismatched_token_error.given, + 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 diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py index 039e12f..de08919 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -1,5 +1,6 @@ import importlib import os +import pathlib import pytest @@ -9,6 +10,7 @@ from tembo.cli.cli import ( _new_verify_name_exists, _new_get_config_scope, _new_show_example, + _new_create_scoped_page, ) @@ -147,3 +149,97 @@ def test_new_show_example(path, message, shared_datadir, capsys): # assert assert capsys.readouterr().out == message assert system_exit.value.code == 0 + + +def test_new_create_scoped_page_success(shared_datadir, tmpdir): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + config_scope = _new_get_config_scope("some_scope") + inputs = () + scoped_page_file = pathlib.Path(tmpdir / "some_scope" / "some_scope").with_suffix( + ".md" + ) + + # act + scoped_page = _new_create_scoped_page(config_scope, inputs) + + # assert + assert scoped_page.path == scoped_page_file + assert scoped_page.page_content == "" + + +def test_new_create_scoped_page_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) + + config_scope = _new_get_config_scope("some_scope") + inputs = () + + # act + with pytest.raises(SystemExit) as system_exit: + _new_create_scoped_page(config_scope, inputs) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f'[TEMBO] Tembo base path of {os.environ["TEMBO_BASE_PATH"]} does not exist. 🐘\n' + ) + + +def test_new_create_scoped_page_template_file_does_not_exist( + shared_datadir, tmpdir, capsys +): + # arrange + os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") + os.environ["TEMBO_BASE_PATH"] = str(tmpdir) + os.environ["TEMBO_TEMPLATE_PATH"] = str(tmpdir) + importlib.reload(tembo.cli) + + config_scope = _new_get_config_scope("some_scope") + config_scope["template_filename"] = "some_nonexistent_template.md.tpl" + inputs = () + + # act + with pytest.raises(SystemExit) as system_exit: + _new_create_scoped_page(config_scope, inputs) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f'[TEMBO] Template file {os.environ["TEMBO_TEMPLATE_PATH"]}/{config_scope["template_filename"]} does not exist. 🐘\n' + ) + + +@pytest.mark.parametrize("example", [(True,), (False,)]) +def test_new_create_scoped_page_mismatched_token( + 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) + + config_scope = _new_get_config_scope("some_scope") + inputs = ("some_input",) + if not example: + config_scope["example"] = None + + # act + with pytest.raises(SystemExit) as system_exit: + _new_create_scoped_page(config_scope, inputs) + + # assert + assert system_exit.value.code == 1 + assert ( + capsys.readouterr().out + == f'[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1. Example: tembo new some_scope 🐘\n' + ) From 240ba6f9737674c1306308ca140f6011fabfc3a9 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Thu, 4 Nov 2021 09:08:05 +0000 Subject: [PATCH 71/74] adding latest --- tests/test_cli/test_cli.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py index de08919..b4f14a9 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -230,7 +230,7 @@ def test_new_create_scoped_page_mismatched_token( config_scope = _new_get_config_scope("some_scope") inputs = ("some_input",) - if not example: + if not example[0]: config_scope["example"] = None # act @@ -239,7 +239,13 @@ def test_new_create_scoped_page_mismatched_token( # assert assert system_exit.value.code == 1 - assert ( - capsys.readouterr().out - == f'[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1. Example: tembo new some_scope 🐘\n' - ) + if not example[0]: + assert ( + capsys.readouterr().out + == f'[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1 🐘\n' + ) + else: + assert ( + capsys.readouterr().out + == f'[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1. Example: tembo new some_scope 🐘\n' + ) From 70a62ab70a3dbc0b7e94c98d4cd9ef2aeb6c1d80 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Thu, 4 Nov 2021 22:33:27 +0000 Subject: [PATCH 72/74] adding latest tests + refactored cli --- tembo/cli/cli.py | 86 +++---- tests/test_cli/data/config/success/config.yml | 5 + tests/test_cli/data/some_scope/some_scope.md | 1 + tests/test_cli/test_cli.py | 236 +++++++++++++++++- 4 files changed, 267 insertions(+), 61 deletions(-) create mode 100644 tests/test_cli/data/some_scope/some_scope.md diff --git a/tembo/cli/cli.py b/tembo/cli/cli.py index 0014fea..e0df85b 100644 --- a/tembo/cli/cli.py +++ b/tembo/cli/cli.py @@ -32,9 +32,8 @@ def run(): def list_all(): """List all scopes defined in the config.yml.""" _all_scopes = [user_scope["name"] for user_scope in tembo.cli.CONFIG.scopes] - tembo.cli.logger.info( - "%s names found in config.yml: '%s'", len(_all_scopes), "', '".join(_all_scopes) - ) + _all_scopes_joined = "', '".join(_all_scopes) + cli_message(f"{len(_all_scopes)} names found in config.yml: '{_all_scopes_joined}'") raise SystemExit(0) @@ -70,7 +69,7 @@ def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool): exceptions.MissingConfigYML, ) as tembo_exception: cli_message(tembo_exception.args[0]) - raise SystemExit(0) from tembo_exception + raise SystemExit(1) from tembo_exception # get the scope configuration from the config.yml try: @@ -83,7 +82,7 @@ def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool): _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) + scoped_page = new_create_scoped_page(config_scope, inputs) if dry_run: cli_message(f"{scoped_page.path} will be created") @@ -99,6 +98,39 @@ def new(scope: str, inputs: Collection[str], dry_run: bool, example: bool): 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 @@ -107,7 +139,7 @@ def _new_verify_name_exists(scope: str) -> None: return if len(tembo.cli.CONFIG.scopes) > 0: # if the name is missing in the config.yml, raise error - raise exceptions.ScopeNotFound(f"Command {scope} not found in config.yml") + 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( @@ -158,51 +190,9 @@ def _new_show_example(example: bool, config_scope: dict) -> None: raise SystemExit(0) -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 cli_message(message: str) -> None: click.echo(f"[TEMBO] {message} 🐘") run.add_command(new) run.add_command(list_all) - - -if __name__ == "__main__": - new(["meeting", "a", "b", "c", "d"]) # noqa - - # pyinstaller - # if getattr(sys, "frozen", False): - # run(sys.argv[1:]) - # run(sys.argv[1:]) diff --git a/tests/test_cli/data/config/success/config.yml b/tests/test_cli/data/config/success/config.yml index 8830957..0550bbe 100644 --- a/tests/test_cli/data/config/success/config.yml +++ b/tests/test_cli/data/config/success/config.yml @@ -5,3 +5,8 @@ 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 index b4f14a9..a89346a 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -10,14 +10,12 @@ from tembo.cli.cli import ( _new_verify_name_exists, _new_get_config_scope, _new_show_example, - _new_create_scoped_page, + new_create_scoped_page, + new, + list_all, ) -def test_cli_page_is_saved_success(): - pass - - def test_new_verify_name_exists_success(shared_datadir): # arrange os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") @@ -45,8 +43,7 @@ def test_new_verify_name_exists_scope_not_found(shared_datadir): # assert assert ( - str(scope_not_found.value) - == "Command some_missing_scope not found in config.yml" + str(scope_not_found.value) == "Scope some_missing_scope not found in config.yml" ) # cleanup @@ -111,6 +108,9 @@ def test_new_get_config_scope_success(shared_datadir): "template_filename": None, } + # cleanup + del os.environ["TEMBO_CONFIG"] + def test_new_get_config_scope_key_not_found(shared_datadir): # arrange @@ -128,6 +128,9 @@ def test_new_get_config_scope_key_not_found(shared_datadir): str(mandatory_key_not_found.value) == "Key 'filename' not found in config.yml" ) + # cleanup + del os.environ["TEMBO_CONFIG"] + @pytest.mark.parametrize( "path,message", @@ -150,6 +153,9 @@ def test_new_show_example(path, message, shared_datadir, capsys): assert capsys.readouterr().out == message assert system_exit.value.code == 0 + # cleanup + del os.environ["TEMBO_CONFIG"] + def test_new_create_scoped_page_success(shared_datadir, tmpdir): # arrange @@ -164,12 +170,16 @@ def test_new_create_scoped_page_success(shared_datadir, tmpdir): ) # act - scoped_page = _new_create_scoped_page(config_scope, inputs) + scoped_page = new_create_scoped_page(config_scope, inputs) # assert assert scoped_page.path == scoped_page_file assert scoped_page.page_content == "" + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + def test_new_create_scoped_page_base_path_does_not_exist( shared_datadir, tmpdir, capsys @@ -184,7 +194,7 @@ def test_new_create_scoped_page_base_path_does_not_exist( # act with pytest.raises(SystemExit) as system_exit: - _new_create_scoped_page(config_scope, inputs) + new_create_scoped_page(config_scope, inputs) # assert assert system_exit.value.code == 1 @@ -193,6 +203,10 @@ def test_new_create_scoped_page_base_path_does_not_exist( == f'[TEMBO] Tembo base path of {os.environ["TEMBO_BASE_PATH"]} does not exist. 🐘\n' ) + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + def test_new_create_scoped_page_template_file_does_not_exist( shared_datadir, tmpdir, capsys @@ -209,7 +223,7 @@ def test_new_create_scoped_page_template_file_does_not_exist( # act with pytest.raises(SystemExit) as system_exit: - _new_create_scoped_page(config_scope, inputs) + new_create_scoped_page(config_scope, inputs) # assert assert system_exit.value.code == 1 @@ -218,6 +232,10 @@ def test_new_create_scoped_page_template_file_does_not_exist( == f'[TEMBO] Template file {os.environ["TEMBO_TEMPLATE_PATH"]}/{config_scope["template_filename"]} does not exist. 🐘\n' ) + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + @pytest.mark.parametrize("example", [(True,), (False,)]) def test_new_create_scoped_page_mismatched_token( @@ -235,17 +253,209 @@ def test_new_create_scoped_page_mismatched_token( # act with pytest.raises(SystemExit) as system_exit: - _new_create_scoped_page(config_scope, inputs) + new_create_scoped_page(config_scope, inputs) # assert assert system_exit.value.code == 1 if not example[0]: assert ( capsys.readouterr().out - == f'[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1 🐘\n' + == f"[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1 🐘\n" ) else: assert ( capsys.readouterr().out - == f'[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1. Example: tembo new some_scope 🐘\n' + == f"[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1. Example: tembo new some_scope 🐘\n" ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] + + +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 + == f"[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 == f"[TEMBO] Key 'filename' not found in config.yml 🐘\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 + == f"[TEMBO] 2 names found in config.yml: 'some_scope', 'another_some_scope' 🐘\n" + ) + + # cleanup + del os.environ["TEMBO_CONFIG"] + del os.environ["TEMBO_BASE_PATH"] From 52461cb794e46c9f063c82e77f4722d8da98c0f8 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Thu, 4 Nov 2021 22:47:28 +0000 Subject: [PATCH 73/74] adding latest tests --- tests/test_cli/test_cli.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py index a89346a..f4846c1 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -35,7 +35,6 @@ def test_new_verify_name_exists_scope_not_found(shared_datadir): # arrange os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") importlib.reload(tembo.cli) - from tembo.cli.cli import _new_verify_name_exists # act with pytest.raises(tembo.exceptions.ScopeNotFound) as scope_not_found: @@ -54,7 +53,6 @@ def test_new_verify_name_exists_empty_config(shared_datadir): # arrange os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "empty") importlib.reload(tembo.cli) - from tembo.cli.cli import _new_verify_name_exists # act with pytest.raises(tembo.exceptions.EmptyConfigYML) as empty_config_yml: @@ -74,7 +72,6 @@ def test_new_verify_name_exists_missing_config(shared_datadir): # arrange os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing") importlib.reload(tembo.cli) - from tembo.cli.cli import _new_verify_name_exists # act with pytest.raises(tembo.exceptions.MissingConfigYML) as missing_config_yml: From 78fdf54da3e4b2800dea00f76c73b33d01876103 Mon Sep 17 00:00:00 2001 From: Daniel Tomlinson Date: Sun, 7 Nov 2021 14:47:56 +0000 Subject: [PATCH 74/74] adding latest tests --- .../data/config/missing_template/config.yml | 8 + tests/test_cli/data/config/success/config.yml | 4 + tests/test_cli/test_cli.py | 377 ++++++------------ 3 files changed, 123 insertions(+), 266 deletions(-) create mode 100644 tests/test_cli/data/config/missing_template/config.yml 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/success/config.yml b/tests/test_cli/data/config/success/config.yml index 0550bbe..6db5753 100644 --- a/tests/test_cli/data/config/success/config.yml +++ b/tests/test_cli/data/config/success/config.yml @@ -5,6 +5,10 @@ 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" diff --git a/tests/test_cli/test_cli.py b/tests/test_cli/test_cli.py index f4846c1..010e6cf 100644 --- a/tests/test_cli/test_cli.py +++ b/tests/test_cli/test_cli.py @@ -4,270 +4,8 @@ import pathlib import pytest -import tembo.exceptions import tembo.cli -from tembo.cli.cli import ( - _new_verify_name_exists, - _new_get_config_scope, - _new_show_example, - new_create_scoped_page, - new, - list_all, -) - - -def test_new_verify_name_exists_success(shared_datadir): - # arrange - os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") - importlib.reload(tembo.cli) - - # act - verified_name = _new_verify_name_exists("some_scope") - - # assert - assert verified_name is None - - # cleanup - del os.environ["TEMBO_CONFIG"] - - -def test_new_verify_name_exists_scope_not_found(shared_datadir): - # arrange - os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") - importlib.reload(tembo.cli) - - # act - with pytest.raises(tembo.exceptions.ScopeNotFound) as scope_not_found: - _new_verify_name_exists("some_missing_scope") - - # assert - assert ( - str(scope_not_found.value) == "Scope some_missing_scope not found in config.yml" - ) - - # cleanup - del os.environ["TEMBO_CONFIG"] - - -def test_new_verify_name_exists_empty_config(shared_datadir): - # arrange - os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "empty") - importlib.reload(tembo.cli) - - # act - with pytest.raises(tembo.exceptions.EmptyConfigYML) as empty_config_yml: - _new_verify_name_exists("some_missing_scope") - - # assert - assert ( - str(empty_config_yml.value) - == f'Config.yml found in {os.environ["TEMBO_CONFIG"]} is empty' - ) - - # cleanup - del os.environ["TEMBO_CONFIG"] - - -def test_new_verify_name_exists_missing_config(shared_datadir): - # arrange - os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing") - importlib.reload(tembo.cli) - - # act - with pytest.raises(tembo.exceptions.MissingConfigYML) as missing_config_yml: - _new_verify_name_exists("some_missing_scope") - - # assert - assert ( - str(missing_config_yml.value) - == f'No config.yml found in {os.environ["TEMBO_CONFIG"]}' - ) - - # cleanup - del os.environ["TEMBO_CONFIG"] - - -def test_new_get_config_scope_success(shared_datadir): - # arrange - os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "optional_keys") - importlib.reload(tembo.cli) - - # act - config_scope = _new_get_config_scope("some_scope") - - # assert - assert config_scope == { - "name": "some_scope", - "path": "some_scope", - "filename": "{name}", - "extension": "md", - "example": None, - "template_filename": None, - } - - # cleanup - del os.environ["TEMBO_CONFIG"] - - -def test_new_get_config_scope_key_not_found(shared_datadir): - # arrange - os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "missing_keys") - importlib.reload(tembo.cli) - - # act - with pytest.raises( - tembo.exceptions.MandatoryKeyNotFound - ) as mandatory_key_not_found: - config_scope = _new_get_config_scope("some_scope") - - # assert - assert ( - str(mandatory_key_not_found.value) == "Key 'filename' not found in config.yml" - ) - - # cleanup - del os.environ["TEMBO_CONFIG"] - - -@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) - config_scope = _new_get_config_scope("some_scope") - - # act - with pytest.raises(SystemExit) as system_exit: - _new_show_example(True, config_scope) - - # assert - assert capsys.readouterr().out == message - assert system_exit.value.code == 0 - - # cleanup - del os.environ["TEMBO_CONFIG"] - - -def test_new_create_scoped_page_success(shared_datadir, tmpdir): - # arrange - os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") - os.environ["TEMBO_BASE_PATH"] = str(tmpdir) - importlib.reload(tembo.cli) - - config_scope = _new_get_config_scope("some_scope") - inputs = () - scoped_page_file = pathlib.Path(tmpdir / "some_scope" / "some_scope").with_suffix( - ".md" - ) - - # act - scoped_page = new_create_scoped_page(config_scope, inputs) - - # assert - assert scoped_page.path == scoped_page_file - assert scoped_page.page_content == "" - - # cleanup - del os.environ["TEMBO_CONFIG"] - del os.environ["TEMBO_BASE_PATH"] - - -def test_new_create_scoped_page_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) - - config_scope = _new_get_config_scope("some_scope") - inputs = () - - # act - with pytest.raises(SystemExit) as system_exit: - new_create_scoped_page(config_scope, inputs) - - # assert - assert system_exit.value.code == 1 - assert ( - capsys.readouterr().out - == f'[TEMBO] Tembo base path of {os.environ["TEMBO_BASE_PATH"]} does not exist. 🐘\n' - ) - - # cleanup - del os.environ["TEMBO_CONFIG"] - del os.environ["TEMBO_BASE_PATH"] - - -def test_new_create_scoped_page_template_file_does_not_exist( - shared_datadir, tmpdir, capsys -): - # arrange - os.environ["TEMBO_CONFIG"] = str(shared_datadir / "config" / "success") - os.environ["TEMBO_BASE_PATH"] = str(tmpdir) - os.environ["TEMBO_TEMPLATE_PATH"] = str(tmpdir) - importlib.reload(tembo.cli) - - config_scope = _new_get_config_scope("some_scope") - config_scope["template_filename"] = "some_nonexistent_template.md.tpl" - inputs = () - - # act - with pytest.raises(SystemExit) as system_exit: - new_create_scoped_page(config_scope, inputs) - - # assert - assert system_exit.value.code == 1 - assert ( - capsys.readouterr().out - == f'[TEMBO] Template file {os.environ["TEMBO_TEMPLATE_PATH"]}/{config_scope["template_filename"]} does not exist. 🐘\n' - ) - - # cleanup - del os.environ["TEMBO_CONFIG"] - del os.environ["TEMBO_BASE_PATH"] - - -@pytest.mark.parametrize("example", [(True,), (False,)]) -def test_new_create_scoped_page_mismatched_token( - 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) - - config_scope = _new_get_config_scope("some_scope") - inputs = ("some_input",) - if not example[0]: - config_scope["example"] = None - - # act - with pytest.raises(SystemExit) as system_exit: - new_create_scoped_page(config_scope, inputs) - - # assert - assert system_exit.value.code == 1 - if not example[0]: - assert ( - capsys.readouterr().out - == f"[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1 🐘\n" - ) - else: - assert ( - capsys.readouterr().out - == f"[TEMBO] Your tembo config.yml/template specifies 0 input tokens, you gave 1. Example: tembo new some_scope 🐘\n" - ) - - # cleanup - del os.environ["TEMBO_CONFIG"] - del os.environ["TEMBO_BASE_PATH"] +from tembo.cli.cli import new, list_all def test_new_dry_run(shared_datadir, tmpdir, capsys): @@ -360,7 +98,7 @@ def test_new_scope_not_found(shared_datadir, tmpdir, capsys): assert system_exit.value.code == 1 assert ( capsys.readouterr().out - == f"[TEMBO] Scope some_nonexistent_scope not found in config.yml 🐘\n" + == "[TEMBO] Scope some_nonexistent_scope not found in config.yml 🐘\n" ) # cleanup @@ -425,7 +163,7 @@ def test_new_missing_mandatory_key(shared_datadir, tmpdir, capsys): # assert assert system_exit.value.code == 1 assert ( - capsys.readouterr().out == f"[TEMBO] Key 'filename' not found in config.yml 🐘\n" + capsys.readouterr().out == "[TEMBO] Key 'filename' not found in config.yml 🐘\n" ) # cleanup @@ -433,6 +171,113 @@ def test_new_missing_mandatory_key(shared_datadir, tmpdir, capsys): 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") @@ -450,7 +295,7 @@ def test_list_all_success(shared_datadir, tmpdir, capsys): assert system_exit.value.code == 0 assert ( capsys.readouterr().out - == f"[TEMBO] 2 names found in config.yml: 'some_scope', 'another_some_scope' 🐘\n" + == "[TEMBO] 3 names found in config.yml: 'some_scope', 'some_scope_no_example', 'another_some_scope' 🐘\n" ) # cleanup