Compare commits

..

No commits in common. "3beff36d6645094ddbec1f7eba0224934960445b" and "cf926658ee34eeca32ea095797cf2112e501ddac" have entirely different histories.

7 changed files with 39 additions and 220 deletions

View File

@ -5,7 +5,6 @@ from ..version import __version__
from . import diff
from . import record
from . import show
from typing import Optional
@ -52,5 +51,4 @@ def main(log_level: str) -> None:
main.add_command(diff.status, name="status")
main.add_command(record.record_cli, name="record")
main.add_command(show.show_cli, name="show")
main.add_command(version)

View File

@ -6,7 +6,6 @@ from ..store import find_store, Store
import os
import sys
from typing import Optional
import warnings
@ -39,8 +38,14 @@ def print_diff(
hashcolor = Fore.MAGENTA if use_color else ""
def _print_row(tag: str, entry: fs.FSEntry, level: int) -> None:
# Format relpath using filetype-based colors
if len(entry.versions) == 0:
print(Fore.RED + "NOVERSIONS" + Style.RESET_ALL + entry.sha256)
else:
ver = entry.versions[-1]
relpath = entry.relpath
# Format relpath using filetype-based colors
dname, fname = os.path.split(relpath)
if fname == "": # root directory leads to empty fname here
dirstr = filetypecolors["DIR"] + "<root>" + reset
@ -48,21 +53,6 @@ def print_diff(
dirstr = (
(filetypecolors["DIR"] + dname + "/" + reset) if dname != "" else ""
)
if len(entry.versions) == 0:
print(
Fore.RED
+ "NOVERSIONS"
+ Style.RESET_ALL
+ " "
+ ver.sha256
+ " "
+ dirstr
)
return
else:
ver: fs.FSEntryVersion = entry.versions[-1]
assert ver.filetype is not None
fname = filetypecolors.get(str(ver.filetype), "") + fname + reset
@ -72,6 +62,7 @@ def print_diff(
relpath = dirstr + fname
assert entry.sha256 is not None
hashchange = (
(hashcolor + ver.sha256.hex() + reset + " " + changetags[tag])
if show_hashes

View File

@ -1,135 +0,0 @@
import click
from loguru import logger
from .. import fs, store
from datetime import datetime
import os
import sys
from typing import Any, Optional, Union
import warnings
def print_recorded(
root: fs.FSEntry,
indent: int = 2,
indent_level: int = 0,
use_color: bool = True,
show_hashes: bool = False,
) -> None:
"""Pretty print an FSEntry object"""
if use_color:
try:
from colorama import init, Fore, Back, Style # NOQA
except ImportError:
warnings.warn("Could not import colorama library. Color output disabled.")
use_color = False
filetypecolors = dict(
DIR=Fore.BLUE if use_color else "",
REG="",
LNK=Fore.CYAN if use_color else "",
)
reset = Style.RESET_ALL if use_color else ""
hashcolor = Fore.MAGENTA if use_color else ""
def _print_row(entry: fs.FSEntry, level: int) -> None:
if len(entry.versions) == 0:
print(Fore.RED + "NOVERSIONS" + Style.RESET_ALL + entry.sha256)
else:
ver = entry.versions[-1]
relpath = entry.relpath
# Format relpath using filetype-based colors
dname, fname = os.path.split(relpath)
if fname == "": # root directory leads to empty fname here
dirstr = filetypecolors["DIR"] + "<root>" + reset
else:
dirstr = (
(filetypecolors["DIR"] + dname + "/" + reset) if dname != "" else ""
)
assert ver.filetype is not None
fname = filetypecolors.get(str(ver.filetype), "") + fname + reset
if ver.filetype == fs.FileType.LNK: # append symlink target
assert ver.symlink_target is not None
fname += " -> " + ver.symlink_target
relpath = dirstr + fname
print(
(hashcolor + ver.sha256.hex() + reset) if show_hashes else "",
ver.perms,
relpath,
)
for lev, d in root.flatten_tree():
_print_row(d, lev)
@logger.catch
def show(
store_path: Optional[Union[str, "os.PathLike[Any]"]] = None,
show_hashes: bool = False,
use_color: bool = True,
) -> None:
"""Unwrapped show command"""
start_time = datetime.now()
if store_path is None:
curdir = os.path.realpath(os.getcwd())
logger.info("Looking for store in {}", curdir)
store_path = store.find_store(curdir)
if store_path is None: # If no store found, assume we're creating here
logger.error(
"Could not find nancy.db in any directory containing {}", curdir
)
sys.exit(1)
logger.info("Found store at {}", store_path)
s = store.Store(store_path)
# then find a listing covering all the expected paths
recorded = s.fs_entries(shallow=True)
if recorded is None:
logger.warning(
f"No recorded entries in {store_path}. Have you run `nancy record` yet?"
)
else:
print_recorded(recorded, show_hashes=show_hashes, use_color=use_color)
@click.command()
@click.option(
"-H",
"--show-hashes",
is_flag=True,
help="If given, prepend each line in the diff with the new file hash (SHA256).",
)
@click.option(
"--no-color",
is_flag=True,
help="If given, do not print any color output.",
)
@click.option(
"-s",
"--store",
type=str,
default=None,
help="Top-level of store. If omitted, use closest common parent directory "
"of given paths. If given the path to a non-store directory, a new "
"store is initialized there.",
)
def show_cli(
show_hashes: bool,
no_color: bool,
store: str,
) -> None:
"""
Show what's currently recorded in a store.
"""
show(
show_hashes=show_hashes,
use_color=not no_color,
store_path=store,
)

View File

@ -91,7 +91,7 @@ class FSEntryVersion:
self.uuid,
self.filedir.sha256.hex(),
datetime.now().timestamp(),
self.filetype.name,
str(self.filetype),
False,
self.perms,
self.symlink_target,
@ -316,7 +316,7 @@ class FSEntry:
@classmethod
def from_db_key(
cls: Type[_FSEntryT],
cur: sqlite3.Cursor,
cursor: sqlite3.Cursor,
store: "Store",
root_key: Optional[str] = None,
root_row: Optional[
@ -327,11 +327,11 @@ class FSEntry:
"""Given key of an entry in filedir, recursively fill this object"""
if root_row is None:
assert root_key is not None
cur.execute(
cursor.execute(
"SELECT sha256, name, store FROM filedir WHERE sha256=?",
(root_key,),
)
root_row = cur.fetchone()
root_row = cursor.fetchone()
root_key, filename, store_key = root_row
assert store_key == store.uuid
@ -348,21 +348,21 @@ class FSEntry:
store=store,
)
cur.execute(
cursor.execute(
"SELECT sha256, name, store FROM filedir WHERE parent=?",
(root_key,),
)
rows = cur.fetchall()
rows = cursor.fetchall()
ob.children = [
cls.from_db_key(cur=cur, root_row=r, parent=ob, store=store) for r in rows
cls.from_db_key(cursor, root_row=r, parent=ob, store=store) for r in rows
]
# get all versions
cur.execute(
cursor.execute(
"SELECT * FROM filedir_version WHERE filedir=? ORDER BY recorded_time",
(root_key,),
)
matches = cur.fetchall()
matches = cursor.fetchall()
ob.versions = [FSEntryVersion.from_row(row, filedir=ob) for row in matches]
return ob

View File

@ -60,7 +60,7 @@ CREATE TABLE user(
-- on Windows: see https://stackoverflow.com/questions/21766954/how-to-get-windows-users-full-name-in-python
machine TEXT NOT NULL REFERENCES machine ON UPDATE CASCADE
);
CREATE INDEX FK_user_machine ON user (machine);
-- Stores and files (and directories)
-- These are the primary objects tracked by nancy.
@ -93,8 +93,6 @@ CREATE TABLE filedir (
parent TEXT REFERENCES filedir ON UPDATE CASCADE,
UNIQUE(store, name, parent)
);
CREATE INDEX FK_filedir_store ON filedir (store);
CREATE INDEX FK_filedir_parent ON filedir (parent);
-- Detect cross-store references
CREATE TRIGGER insert_filedir BEFORE INSERT ON filedir
BEGIN SELECT CASE
@ -142,8 +140,6 @@ CREATE TABLE filedir_version (
source_task TEXT REFERENCES task ON UPDATE CASCADE
);
CREATE INDEX FK_filedir_version_filedir ON filedir_version (filedir);
CREATE INDEX FK_filedir_version_source_task ON filedir_version (source_task);
-- Disallow UPDATING filedir_version. Instead, new version should be created.
-- One exception is during importing, in which case we can disable the trigger
INSERT INTO triggers VALUES('update_filedir_version', TRUE);
@ -203,42 +199,39 @@ CREATE TABLE program (
environment TEXT NOT NULL REFERENCES environment ON UPDATE CASCADE,
message TEXT NOT NULL -- user-defined message to help distinguish similar runs
);
CREATE INDEX FK_program_environment ON program (environment);
-- We try to track all python packages that impact execution by traversing a
-- copy of sys.modules. This is done once before a "program" and once after in
-- case some calling code winds up calling a previously-unloaded module.
CREATE TABLE package (
CREATE TABLE py_package (
sha256 TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
version TEXT,
UNIQUE (name, version)
);
-- A module describes any python module file containing decorated Functions.
-- A py_module describes any python module file containing decorated Functions.
-- Modules are tracked since they impact the global scope of function calls.
CREATE TABLE module(
CREATE TABLE py_module(
sha256 TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
code TEXT, -- code doesn't have to be included, but should be used to create sha256
package TEXT REFERENCES package ON UPDATE CASCADE
py_package TEXT REFERENCES py_package ON UPDATE CASCADE
);
CREATE INDEX FK_module_package ON module (package);
-- A func just describes a function, without reference to its arguments.
-- A py_function just describes a function, without reference to its arguments.
-- It can have inputs and outputs, which are described in the func_inputs and
-- func_outputs children tables.
CREATE TABLE func(
CREATE TABLE py_function(
sha256 TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
module TEXT NOT NULL REFERENCES module ON UPDATE CASCADE
py_module TEXT NOT NULL REFERENCES py_module ON UPDATE CASCADE
);
CREATE INDEX FK_func_module ON func (module);
CREATE TABLE func_input(
CREATE TABLE py_function_input(
uuid TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
typename TEXT NOT NULL,
func TEXT NOT NULL REFERENCES func ON UPDATE CASCADE,
py_function TEXT NOT NULL REFERENCES py_function ON UPDATE CASCADE,
position INTEGER,
posonly BOOL,
@ -246,17 +239,15 @@ CREATE TABLE func_input(
description TEXT
);
CREATE INDEX FK_func_input_func ON func_input (func);
CREATE TABLE func_output(
CREATE TABLE py_function_output(
uuid TEXT PRIMARY KEY NOT NULL,
name TEXT,
typename TEXT NOT NULL,
func TEXT NOT NULL REFERENCES func ON UPDATE CASCADE,
py_function TEXT NOT NULL REFERENCES py_function ON UPDATE CASCADE,
position INTEGER NOT NULL,
description TEXT
);
CREATE INDEX FK_func_output_func ON func_output (func);
-- Tasks are usually executed calls to Functions: they correspond to a
@ -267,11 +258,9 @@ CREATE TABLE task(
uuid TEXT PRIMARY KEY NOT NULL,
program TEXT NOT NULL REFERENCES program ON UPDATE CASCADE,
-- func is NULL for some built-in functionality like "RECORD" programs
func TEXT REFERENCES func ON UPDATE CASCADE
-- py_function is NULL for some built-in functionality like "RECORD" programs
py_function TEXT REFERENCES py_function ON UPDATE CASCADE
);
CREATE INDEX FK_task_program ON task (program);
CREATE INDEX FK_task_func ON task (func);
-- A datum is an object that is computed as the output of a task, given as a
-- literal value in a config file, or loaded from a file.
CREATE TABLE datum(
@ -291,14 +280,12 @@ CREATE TABLE datum(
typename TEXT NOT NULL -- string representation of the data type
);
CREATE INDEX FK_datum_task ON datum (task);
CREATE INDEX FK_datum_task_output ON datum (task_output);
-- A task_input records the version of a Datum that is passed to a function
CREATE TABLE task_input(
uuid TEXT PRIMARY KEY NOT NULL,
task TEXT NOT NULL REFERENCES task ON UPDATE CASCADE,
-- if this was a python function, reference which input
func_input TEXT REFERENCES func_input ON UPDATE CASCADE,
py_function_input TEXT REFERENCES py_function_input ON UPDATE CASCADE,
datum TEXT NOT NULL REFERENCES datum ON UPDATE CASCADE,
-- Data have versions to facilitate tracking non-const operations. If a datum
@ -306,5 +293,3 @@ CREATE TABLE task_input(
-- version
datum_version INTEGER NOT NULL
);
CREATE INDEX FK_task_input_func_input ON task_input (func_input);
CREATE INDEX FK_task_input_datum ON task_input (datum);

View File

@ -113,10 +113,7 @@ class Store:
if cur is None:
assert self.conn is not None
cur = self.conn.cursor()
cur.execute(
"SELECT sha256 FROM filedir WHERE store=? AND parent is NULL",
(self.uuid,),
)
cur.execute("SELECT sha256 FROM filedir WHERE store=1 AND parent is NULL")
row = cur.fetchone()
if row is None:
return None
@ -157,18 +154,18 @@ class Store:
fd_key, filetype = row
return fs.FSEntry.from_db_key(cur=cur, root_key=fd_key, store=self)
return fs.FSEntry.from_db_key(cur, root_key=fd_key, store=self)
def fs_entries(self, shallow: bool = False) -> Optional[fs.FSEntry]:
"""Return recursive structure containing FSEntry objects from db"""
assert self.conn is not None
cur = self.conn.cursor()
root_key = self.filedir_root_key(cur=cur)
logger.debug("root_key: {}", root_key)
root_key = self.filedir_root_key()
if root_key is None:
return None
else:
return fs.FSEntry.from_db_key(cur=cur, root_key=root_key, store=self)
assert self.conn is not None
return fs.FSEntry.from_db_key(
self.conn.cursor(), root_key=root_key, store=self
)
def new_program(self, name: str, message: str) -> program.Program:
p = program.Program(self, name, message)

View File

@ -37,23 +37,6 @@ def test_record(junk_dir: Path) -> None:
assert result.exit_code == 0
assert "ERROR" not in result.output
# Test with show-hashes
result = runner.invoke(
main,
[
"record",
"-s",
str(junk_dir),
"-m",
"This is just a test recording",
"--yes", # don't ask for confirmation
"--show-hashes",
],
)
print(result.output)
assert result.exit_code == 0
assert "ERROR" not in result.output
# test again with confirmation
result = runner.invoke(
main,