diff --git a/poetry.lock b/poetry.lock index 6a79068..b24646b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -94,6 +94,21 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "loguru" +version = "0.6.0" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["sphinx-rtd-theme (>=0.4.3)", "sphinx-autobuild (>=0.7.1)", "Sphinx (>=4.1.1)", "isort (>=5.1.1)", "black (>=19.10b0)", "pytest-cov (>=2.7.1)", "pytest (>=4.6.2)", "tox (>=3.9.0)", "flake8 (>=3.7.7)", "docutils (==0.16)", "colorama (>=0.3.4)"] + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -248,6 +263,17 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + [[package]] name = "zipp" version = "3.8.1" @@ -263,7 +289,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "c44b5e718866e498815a184650d98fd5e637b07246375d1e9fc4298a0125f0d6" +content-hash = "6ca131347c47d90f6093f09ad7a6025b55535855e382770019f20e60c07b6c15" [metadata.files] attrs = [ @@ -363,6 +389,10 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +loguru = [ + {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, + {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, +] 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"}, @@ -440,6 +470,10 @@ typing-extensions = [ {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] +win32-setctime = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] zipp = [ {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, diff --git a/pyproject.toml b/pyproject.toml index e94519a..ef3e178 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ authors = ["Jacob Hinkle "] python = "^3.7" click = "^8.1.3" colorama = "^0.4.5" +loguru = "^0.6.0" [tool.poetry.dev-dependencies] black = "^22.8.0" diff --git a/src/nancy/__init__.py b/src/nancy/__init__.py index 05526ff..9674bf1 100644 --- a/src/nancy/__init__.py +++ b/src/nancy/__init__.py @@ -1,50 +1,4 @@ -import sqlite3 +import sys from . import store from .version import __version__ - - -def save_data( - destination_mapping, - desc=f"Provenance-tracking with nancy v{__version__}", -): - """ - A command-line interface to compute and save a set of outputs. - - Ex: - - .. code: - save_data({ - "scores.csv": scores, - "checkpoints": checkpoint_dir, - "plots/learning_curve.pdf": learning_curve_plot, - "plots/test_roc.pdf": test_auc_plot, - " - }, - """ - import argparse - import os - import sys - - parser = argparse.ArgumentParser(description=desc) - parser.add_argument( - "--out_dir", - "-o", - required=True, - help="Directory (will be created) in which to save all outputs", - ) - args = parser.parse_args() - - if os.path.exists(args.out_dir): - print( - f"Output directory {args.out_dir} exists. Refusing to overwrite.", - file=sys.stderr, - ) - sys.exit(1) - - # Create the output directory, back up the in-memory db to this location, an - # switch to it. - os.makedirs(args.out_dir, exist_ok=True) - persist_db(os.path.join(args.out_dir, "nancy.db")) - - # Now we save the provided roots, and record their locations in _conn.data diff --git a/src/nancy/cli/__init__.py b/src/nancy/cli/__init__.py index 50a9fdd..ddae9d2 100644 --- a/src/nancy/cli/__init__.py +++ b/src/nancy/cli/__init__.py @@ -1,4 +1,5 @@ import click +from loguru import logger from ..version import __version__ @@ -30,8 +31,17 @@ def version(): @click.group( cls=AliasedGroup, help=f"Composable provenance tracking for scientific data") -def main(): - pass +@click.option( + "-L", "--log_level", + # https://loguru.readthedocs.io/en/stable/api/logger.html#levels + type=click.Choice(['CRITICAL', 'ERROR', 'WARNING', 'SUCCESS', 'INFO', 'DEBUG', 'TRACE']), + default='SUCCESS', + help='If given, print all output including debugging info.', +) +def main(log_level): + import sys + logger.remove() + logger.add(sys.stderr, level=log_level) #main.add_command(freeze) #main.add_command(thaw) main.add_command(diff.diff_cli, name='diff') diff --git a/src/nancy/cli/diff.py b/src/nancy/cli/diff.py index ac177bb..3516d99 100644 --- a/src/nancy/cli/diff.py +++ b/src/nancy/cli/diff.py @@ -1,4 +1,5 @@ import click +from loguru import logger from .. import db, fs, store @@ -91,6 +92,7 @@ def diff(store, filedir_path, show_hashes=False, use_color=True): help='If given, do not print any color output.', ) def diff_cli(path, show_hashes, no_color): +@logger.catch """Detect and describe changes to PATH PATH is a path to a file or directory inside an existing nancy store @@ -116,4 +118,5 @@ def diff_cli(path, show_hashes, no_color): except FileNotFoundError as e: print(str(e), file=sys.stderr) sys.exit(1) + logger.success("Computed diff") diff --git a/src/nancy/cli/record.py b/src/nancy/cli/record.py index 8bd1781..d93061e 100644 --- a/src/nancy/cli/record.py +++ b/src/nancy/cli/record.py @@ -1,4 +1,5 @@ import click +from loguru import logger from .. import store @@ -10,6 +11,7 @@ import sys def record(directory, message, show_diff=True, show_hashes=False, use_color=True, +@logger.catch skip_confirm=False): """Unwrapped record command""" if not os.path.isdir(directory): @@ -26,12 +28,12 @@ def record(directory, message, show_diff=True, show_hashes=False, use_color=True if show_diff: print_diff(fsdiff, show_hashes=show_hashes, use_color=use_color) - print('Message:', message) + logger.info('Recording with message:', message) if skip_confirm or confirm("Record the values above into the database?"): s.record(fsdiff, message=message) else: - print("Cancelled!") + logger.info("Cancelled!") sys.exit(1) diff --git a/src/nancy/fs.py b/src/nancy/fs.py index 28a925c..460716b 100644 --- a/src/nancy/fs.py +++ b/src/nancy/fs.py @@ -1,6 +1,7 @@ """Interaction with the filesystem and with file database entries""" from dataclasses import dataclass +from loguru import logger import hashlib import operator import os @@ -201,6 +202,7 @@ class FSEntry: @classmethod def from_db_index(cls, cursor, root_id=None, root_row=None): + @logger.catch """Given id of an entry in filedir, recursively fill this object""" fields = 'id, filename, filetype, frozen, deleted' if root_row is None: diff --git a/src/nancy/store.py b/src/nancy/store.py index 977dd97..5297d24 100644 --- a/src/nancy/store.py +++ b/src/nancy/store.py @@ -1,5 +1,7 @@ """Utilities for creating new stores and linking between them.""" +from loguru import logger + from . import db, environment, fs, machine import datetime @@ -22,6 +24,7 @@ class Program: def set_start_time(self, t): self.start_time = t + @logger.catch def __enter__(self): if self._evaluated: raise RuntimeError("Cannot re-enter a Program context") @@ -68,7 +71,7 @@ class Program: cur.connection.commit() self._evaluated = True # prevent re-running elapsed = end_time - self.start_time - print(f"Program [{self.id}] {self.name} (message:{self.message}) ran in {elapsed} seconds.") + logger.success(f"Program [{self.id}] {self.name} (message:{self.message}) ran in {elapsed} seconds.") class Store: @@ -178,7 +181,9 @@ class Store: def fs_entries(self, shallow=False): """Return recursive structure containing FSEntry objects from db""" root_id = self.filedir_root_index() + logger.debug('root_id={}', root_id) if root_id is None: + logger.trace("Empty root") return fs.FSEntry.empty_root() else: return fs.FSEntry.from_db_index(cur, root_id=root_id) @@ -197,9 +202,12 @@ class Store: # provided os.path.relpath(os.path.realpath(filepath), os.path.realpath(self.path)), ) + logger.trace("DIFF") recorded = self.path_to_fsentry(filepath) + logger.debug("CURRENT: \n{}", str(current)) + logger.debug("RECORDED: \n{}", str(recorded)) return fs.FSDiff.compute(recorded, current) def record(self, diff, message=None):