Compare commits

..
6 Commits
Author SHA1 Message Date
Ruslan Kuprieiev 31479f02ab dvc: bump to 2.7.2 2021-09-08 20:40:32 +03:00
Batuhan Taskaya b4e0c75a30 http: pass prepared filesystem arguments (#6565) 2021-09-08 20:40:01 +03:00
Saugat Pachhai 378486dbf5 Update setup.py (#6557) 2021-09-08 15:13:07 +05:45
GaoandPeter Rowlands 6106b1341b Show csv format of experiments (#6468)
* Show csv format of experiments

fix #5989 and #5446 

1. add --show-csv to dvc exp show
2. add a new functional test for csv format
3. add a new unit test for show_experiements
4. solve name duplication in table common headers (Created,Parent, etc... ), metrics files and param files.

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
2021-09-08 16:59:01 +08:00
Casper da Costa-Luis 73c493500b remove tpi.base dependency (#6554)
* remove tpi.base dependency

* potential future-proof constructor
2021-09-08 06:09:52 +00:00
Casper da Costa-Luis 820d89606c add contrib avatars (#6552) 2021-09-08 11:19:49 +05:45
11 changed files with 334 additions and 52 deletions
+7 -1
View File
@@ -222,7 +222,9 @@ Contributing
|Maintainability| |Donate|
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more
details.
details. Thanks to all our contributors!
|Contribs|
Mailing List
============
@@ -299,3 +301,7 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
.. |Flowchart| image:: https://dvc.org/img/flow.gif
:target: https://dvc.org/img/flow.gif
:alt: how_dvc_works
.. |Contribs| image:: https://contrib.rocks/image?repo=iterative/dvc
:target: https://github.com/iterative/dvc/graphs/contributors
:alt: Contributors
+69 -30
View File
@@ -141,6 +141,8 @@ def _collect_rows(
precision=DEFAULT_PRECISION,
sort_by=None,
sort_order=None,
fill_value=FILL_VALUE,
iso=False,
):
from dvc.scm.git import Git
@@ -161,8 +163,8 @@ def _collect_rows(
elif exp.get("queued"):
state = "Queued"
else:
state = FILL_VALUE
executor = exp.get("executor", FILL_VALUE)
state = fill_value
executor = exp.get("executor", fill_value)
is_baseline = rev == "baseline"
if is_baseline:
@@ -202,12 +204,12 @@ def _collect_rows(
exp_name,
name_rev,
typ,
_format_time(exp.get("timestamp")),
_format_time(exp.get("timestamp"), fill_value, iso),
parent,
state,
executor,
]
fill_value = FILL_VALUE_ERRORED if results.get("error") else FILL_VALUE
fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value
_extend_row(
row,
metric_names,
@@ -274,14 +276,18 @@ def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
return ret
def _format_time(timestamp):
if timestamp is None:
return FILL_VALUE
if timestamp.date() == date.today():
def _format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
if datetime_obj is None:
return fill_value
if iso:
return datetime_obj.isoformat()
if datetime_obj.date() == date.today():
fmt = "%I:%M %p"
else:
fmt = "%b %d, %Y"
return timestamp.strftime(fmt)
return datetime_obj.strftime(fmt)
def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
@@ -320,6 +326,7 @@ def _parse_filter_list(param_list):
def experiments_table(
all_experiments,
headers,
metric_headers,
metric_names,
param_headers,
@@ -327,22 +334,15 @@ def experiments_table(
sort_by=None,
sort_order=None,
precision=DEFAULT_PRECISION,
fill_value=FILL_VALUE,
iso=False,
) -> "TabularData":
from funcy import lconcat
from dvc.compare import TabularData
headers = [
"Experiment",
"rev",
"typ",
"Created",
"parent",
"State",
"Executor",
]
td = TabularData(
lconcat(headers, metric_headers, param_headers), fill_value=FILL_VALUE
lconcat(headers, metric_headers, param_headers), fill_value=fill_value
)
for base_rev, experiments in all_experiments.items():
rows = _collect_rows(
@@ -353,6 +353,8 @@ def experiments_table(
sort_by=sort_by,
sort_order=sort_order,
precision=precision,
fill_value=fill_value,
iso=iso,
)
td.extend(rows)
@@ -385,8 +387,10 @@ def baseline_styler(typ):
def show_experiments(
all_experiments, pager=True, no_timestamp=False, **kwargs
all_experiments, pager=True, no_timestamp=False, show_csv=False, **kwargs
):
from funcy.seqs import flatten as flatten_list
include_metrics = _parse_filter_list(kwargs.pop("include_metrics", []))
exclude_metrics = _parse_filter_list(kwargs.pop("exclude_metrics", []))
include_params = _parse_filter_list(kwargs.pop("include_params", []))
@@ -399,11 +403,26 @@ def show_experiments(
include_params=include_params,
exclude_params=exclude_params,
)
metric_headers = _normalize_headers(metric_names)
param_headers = _normalize_headers(param_names)
headers = [
"Experiment",
"rev",
"typ",
"Created",
"parent",
"State",
"Executor",
]
names = {**metric_names, **param_names}
counter = Counter(flatten_list([list(a.keys()) for a in names.values()]))
counter.update(headers)
metric_headers = _normalize_headers(metric_names, counter)
param_headers = _normalize_headers(param_names, counter)
td = experiments_table(
all_experiments,
headers,
metric_headers,
metric_names,
param_headers,
@@ -411,6 +430,8 @@ def show_experiments(
kwargs.get("sort_by"),
kwargs.get("sort_order"),
kwargs.get("precision"),
kwargs.get("fill_value"),
kwargs.get("iso"),
)
if no_timestamp:
@@ -422,9 +443,12 @@ def show_experiments(
row_styles = lmap(baseline_styler, td.column("typ"))
merge_headers = ["Experiment", "rev", "typ", "parent"]
td.column("Experiment")[:] = map(prepare_exp_id, td.as_dict(merge_headers))
td.drop(*merge_headers[1:])
if not show_csv:
merge_headers = ["Experiment", "rev", "typ", "parent"]
td.column("Experiment")[:] = map(
prepare_exp_id, td.as_dict(merge_headers)
)
td.drop(*merge_headers[1:])
headers = {"metrics": metric_headers, "params": param_headers}
styles = {
@@ -453,13 +477,11 @@ def show_experiments(
rich_table=True,
header_styles=styles,
row_styles=row_styles,
show_csv=show_csv,
)
def _normalize_headers(names):
count = Counter(
name for path in names for name in names[path] for path in names
)
def _normalize_headers(names, count):
return [
name if count[name] == 1 else f"{path}:{name}"
for path in names
@@ -493,6 +515,14 @@ class CmdExperimentsShow(CmdBase):
ui.write(json.dumps(all_experiments, default=_format_json))
else:
precision = (
self.args.precision or None
if self.args.show_csv
else DEFAULT_PRECISION
)
fill_value = "" if self.args.show_csv else FILL_VALUE
iso = True if self.args.show_csv else False
show_experiments(
all_experiments,
include_metrics=self.args.include_metrics,
@@ -502,8 +532,11 @@ class CmdExperimentsShow(CmdBase):
no_timestamp=self.args.no_timestamp,
sort_by=self.args.sort_by,
sort_order=self.args.sort_order,
precision=self.args.precision or DEFAULT_PRECISION,
precision=precision,
fill_value=fill_value,
iso=iso,
pager=not self.args.no_pager,
show_csv=self.args.show_csv,
)
return 0
@@ -883,6 +916,12 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Print output in JSON format instead of a human-readable table.",
)
experiments_show_parser.add_argument(
"--show-csv",
action="store_true",
default=False,
help="Print output in csv format instead of a human-readable table.",
)
experiments_show_parser.add_argument(
"--precision",
type=int,
+4 -1
View File
@@ -168,7 +168,10 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
def render(self, **kwargs: Any):
from dvc.ui import ui
ui.table(self, headers=self.keys(), **kwargs)
if kwargs.pop("show_csv", False):
ui.write(self.to_csv(), end="")
else:
ui.table(self, headers=self.keys(), **kwargs)
def as_dict(
self, cols: Iterable[str] = None
+17 -4
View File
@@ -50,7 +50,7 @@ class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
from dvc.config import ConfigError
credentials = {}
client_args = credentials.setdefault("client_args", {})
client_kwargs = credentials.setdefault("client_kwargs", {})
if config.get("auth"):
user = config.get("user")
@@ -68,7 +68,7 @@ class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
"'user' and 'password'"
)
client_args["auth"] = aiohttp.BasicAuth(user, password)
client_kwargs["auth"] = aiohttp.BasicAuth(user, password)
elif auth_method == "custom":
if custom_auth_header is None or password is None:
raise ConfigError(
@@ -83,7 +83,7 @@ class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
if "ssl_verify" in config:
with fsspec_loop():
client_args["connector"] = aiohttp.TCPConnector(
client_kwargs["connector"] = aiohttp.TCPConnector(
ssl=make_context(config["ssl_verify"])
)
@@ -92,6 +92,7 @@ class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
return credentials
async def get_client(self, **kwargs):
import aiohttp
from aiohttp_retry import ExponentialRetry, RetryClient
kwargs["retry_options"] = ExponentialRetry(
@@ -100,6 +101,18 @@ class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
max_timeout=self.REQUEST_TIMEOUT,
)
# The default timeout for the aiohttp is 300 seconds
# which is too low for DVC's interactions (especially
# on the read) when dealing with large data blobs. We
# unlimit the total time to read, and only limit the
# time that is spent when connecting to the remote server.
kwargs["timeout"] = aiohttp.ClientTimeout(
total=None,
connect=self.REQUEST_TIMEOUT,
sock_connect=self.REQUEST_TIMEOUT,
sock_read=None,
)
return RetryClient(**kwargs)
@cached_property
@@ -108,7 +121,7 @@ class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
HTTPFileSystem as _HTTPFileSystem,
)
return _HTTPFileSystem(timeout=self.REQUEST_TIMEOUT)
return _HTTPFileSystem(**self.fs_args)
def _entry_hook(self, entry):
entry["checksum"] = entry.get("ETag") or entry.get("Content-MD5")
+27 -4
View File
@@ -1,15 +1,38 @@
from abc import abstractmethod
from abc import ABC, abstractmethod
from contextlib import contextmanager
from typing import TYPE_CHECKING, Iterator, Optional
from tpi import base
if TYPE_CHECKING:
from dvc.fs.ssh import SSHFileSystem
from dvc.repo.experiments.executor.base import BaseExecutor
from dvc.types import StrPath
class BaseMachineBackend(base.BaseMachineBackend):
class BaseMachineBackend(ABC):
def __init__(self, tmp_dir: "StrPath", **kwargs):
raise NotImplementedError
@abstractmethod
def create(self, name: Optional[str] = None, **config):
"""Create and start an instance of the specified machine."""
@abstractmethod
def destroy(self, name: Optional[str] = None, **config):
"""Stop and destroy all instances of the specified machine."""
@abstractmethod
def instances(
self, name: Optional[str] = None, **config
) -> Iterator[dict]:
"""Iterate over status of all instances of the specified machine."""
def close(self):
pass
@abstractmethod
def run_shell(self, name: Optional[str] = None, **config):
"""Spawn an interactive SSH shell for the specified machine."""
@abstractmethod
def get_executor(
self, name: Optional[str] = None, **config
+7 -1
View File
@@ -5,11 +5,17 @@ from tpi import TerraformProviderIterative, terraform
from dvc.fs.ssh import SSHFileSystem
from .base import BaseMachineBackend
if TYPE_CHECKING:
from dvc.repo.experiments.executor.base import BaseExecutor
from dvc.types import StrPath
class TerraformBackend(terraform.TerraformBackend):
class TerraformBackend(terraform.TerraformBackend, BaseMachineBackend):
def __init__(self, tmp_dir: "StrPath", **kwargs):
super().__init__(tmp_dir)
def get_executor(
self, name: Optional[str] = None, **config
) -> "BaseExecutor":
+1 -1
View File
@@ -6,7 +6,7 @@
import os
import subprocess
_BASE_VERSION = "2.7.1"
_BASE_VERSION = "2.7.2"
def _generate_version(base_version):
+1 -1
View File
@@ -107,7 +107,7 @@ ssh = ["sshfs>=2021.8.1"]
hdfs = ["pyarrow>=2.0.0; python_version < '3.10'"]
webhdfs = ["hdfs==2.5.8"]
webdav = ["webdav4>=0.9.0"]
webdav = ["webdav4>=0.9.1"]
# gssapi should not be included in all_remotes, because it doesn't have wheels
# for linux and mac, so it will fail to compile if user doesn't have all the
# requirements, including kerberos itself. Once all the wheels are available,
+68 -4
View File
@@ -9,6 +9,7 @@ from dvc.exceptions import InvalidArgumentError
from dvc.main import main
from dvc.repo.experiments.base import EXPS_STASH, ExpRefInfo
from dvc.repo.experiments.executor.base import BaseExecutor, ExecutorInfo
from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.utils.fs import makedirs
from dvc.utils.serialize import YAMLFileCorruptedError, dump_yaml
from tests.func.test_repro_multistage import COPY_SCRIPT
@@ -269,6 +270,10 @@ def test_show_filter(
included,
excluded,
):
from contextlib import contextmanager
from dvc.ui import ui
capsys.readouterr()
div = "│" if os.name == "nt" else "┃"
@@ -312,13 +317,30 @@ def test_show_filter(
if e_params is not None:
command.append(f"--exclude-params={e_params}")
assert main(command) == 0
@contextmanager
def console_with(console, width):
console_options = console.options
original = console_options.max_width
con_width = console._width
try:
console_options.max_width = width
console._width = width
yield
finally:
console_options.max_width = original
console._width = con_width
with console_with(ui.rich_console, 255):
assert main(command) == 0
cap = capsys.readouterr()
for i in included:
assert f"{div} {i} {div}" in cap.out
assert f"{div} params.yaml:{i} {div}" in cap.out
assert f"{div} metrics.yaml:{i} {div}" in cap.out
for e in excluded:
assert f"{div} {e} {div}" not in cap.out
assert f"{div} params.yaml:{e} {div}" not in cap.out
assert f"{div} metrics.yaml:{e} {div}" not in cap.out
def test_show_multiple_commits(tmp_dir, scm, dvc, exp_stage):
@@ -412,7 +434,6 @@ def test_show_running_checkpoint(
):
from dvc.repo.experiments.base import EXEC_BRANCH
from dvc.repo.experiments.executor.local import TempDirExecutor
from dvc.repo.experiments.utils import exp_refs_by_rev
baseline_rev = scm.get_rev()
dvc.experiments.run(
@@ -471,3 +492,46 @@ def test_show_with_broken_repo(tmp_dir, scm, dvc, exp_stage, caplog):
paths = ["workspace", "baseline", "error"]
assert isinstance(get_in(result, paths), YAMLFileCorruptedError)
def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
baseline_rev = scm.get_rev()
def _get_rev_isotimestamp(rev):
return datetime.fromtimestamp(
scm.gitpython.repo.rev_parse(rev).committed_date
).isoformat()
result1 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
rev1 = first(result1)
ref_info1 = first(exp_refs_by_rev(scm, rev1))
result2 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
rev2 = first(result2)
ref_info2 = first(exp_refs_by_rev(scm, rev2))
capsys.readouterr()
assert main(["exp", "show", "--show-csv"]) == 0
cap = capsys.readouterr()
assert (
"Experiment,rev,typ,Created,parent,metrics.yaml:foo,params.yaml:foo"
in cap.out
)
assert ",workspace,baseline,,,3,3" in cap.out
assert (
"master,{},baseline,{},,1,1".format(
baseline_rev[:7], _get_rev_isotimestamp(baseline_rev)
)
in cap.out
)
assert (
"{},{},branch_base,{},,2,2".format(
ref_info1.name, rev1[:7], _get_rev_isotimestamp(rev1)
)
in cap.out
)
assert (
"{},{},branch_commit,{},,3,3".format(
ref_info2.name, rev2[:7], _get_rev_isotimestamp(rev2)
)
in cap.out
)
+128
View File
@@ -1,3 +1,5 @@
from datetime import datetime
import pytest
from dvc.cli import parse_args
@@ -12,6 +14,7 @@ from dvc.command.experiments import (
CmdExperimentsRemove,
CmdExperimentsRun,
CmdExperimentsShow,
show_experiments,
)
from dvc.exceptions import InvalidArgumentError
@@ -277,3 +280,128 @@ def test_experiments_remove(dvc, scm, mocker, queue, clear_all, remote):
clear_all=clear_all,
remote="myremote" if remote else None,
)
all_experiments = {
"workspace": {
"baseline": {
"data": {
"timestamp": None,
"params": {
"params.yaml": {
"data": {
"featurize": {"max_features": 3000, "ngrams": 1},
"parent": 20170428,
"train": {
"n_est": 100,
"min_split": 36,
},
}
}
},
"queued": False,
"running": False,
"executor": None,
"metrics": {
"scores.json": {
"data": {
"featurize": {"max_features": 3000, "ngrams": 1},
"avg_prec": 0.5843640011189556,
"roc_auc": 0.9544670443829399,
}
}
},
}
}
},
"b05eecc666734e899f79af228ff49a7ae5a18cc0": {
"baseline": {
"data": {
"timestamp": datetime(2021, 8, 2, 16, 48, 14),
"params": {
"params.yaml": {
"data": {
"featurize": {"max_features": 3000, "ngrams": 1},
"parent": 20170428,
"train": {
"n_est": 100,
"min_split": 2,
},
}
}
},
"queued": False,
"running": False,
"executor": None,
"metrics": {
"scores.json": {
"data": {
"featurize": {"max_features": 3000, "ngrams": 1},
"avg_prec": 0.5325162867864254,
"roc_auc": 0.9106964878520005,
}
}
},
"name": "master",
}
},
"ae99936461d6c3092934160f8beafe66a294f98d": {
"data": {
"timestamp": datetime(2021, 8, 31, 14, 56, 55),
"params": {
"params.yaml": {
"data": {
"featurize": {"max_features": 3000, "ngrams": 1},
"parent": 20170428,
"train": {
"n_est": 100,
"min_split": 36,
},
}
}
},
"queued": True,
"running": True,
"executor": None,
"metrics": {
"scores.json": {
"data": {
"featurize": {"max_features": 3000, "ngrams": 1},
"avg_prec": 0.5843640011189556,
"roc_auc": 0.9544670443829399,
}
}
},
"name": "exp-44136",
}
},
},
}
def test_show_experiments(capsys):
show_experiments(
all_experiments, precision=None, fill_value="", iso=True, show_csv=True
)
cap = capsys.readouterr()
assert (
"Experiment,rev,typ,Created,parent,State,scores.json:"
"featurize.max_features,scores.json:featurize.ngrams,"
"avg_prec,roc_auc,params.yaml:featurize.max_features,"
"params.yaml:featurize.ngrams,params.yaml:parent,"
"train.n_est,train.min_split" in cap.out
)
assert (
",workspace,baseline,,,,3000,1,0.5843640011189556,0.9544670443829399,"
"3000,1,20170428,100,36" in cap.out
)
assert (
"master,b05eecc,baseline,2021-08-02T16:48:14,,,3000,1,"
"0.5325162867864254,0.9106964878520005,3000,1,20170428,100,2"
in cap.out
)
assert (
"exp-44136,ae99936,branch_base,2021-08-31T14:56:55,,Running,"
"3000,1,0.5843640011189556,0.9544670443829399,3000,1,20170428,100,36"
in cap.out
)
+5 -5
View File
@@ -23,7 +23,7 @@ def test_public_auth_method(dvc):
fs = HTTPFileSystem(**config)
assert "auth" not in fs.fs_args["client_args"]
assert "auth" not in fs.fs_args["client_kwargs"]
assert "headers" not in fs.fs_args
@@ -40,8 +40,8 @@ def test_basic_auth_method(dvc):
fs = HTTPFileSystem(**config)
assert fs.fs_args["client_args"]["auth"].login == user
assert fs.fs_args["client_args"]["auth"].password == password
assert fs.fs_args["client_kwargs"]["auth"].login == user
assert fs.fs_args["client_kwargs"]["auth"].password == password
def test_custom_auth_method(dvc):
@@ -70,7 +70,7 @@ def test_ssl_verify_disable(dvc):
}
fs = HTTPFileSystem(**config)
assert not fs.fs_args["client_args"]["connector"]._ssl
assert not fs.fs_args["client_kwargs"]["connector"]._ssl
@patch("ssl.SSLContext.load_verify_locations")
@@ -84,7 +84,7 @@ def test_ssl_verify_custom_cert(dvc, mocker):
fs = HTTPFileSystem(**config)
assert isinstance(
fs.fs_args["client_args"]["connector"]._ssl, ssl.SSLContext
fs.fs_args["client_kwargs"]["connector"]._ssl, ssl.SSLContext
)