mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 23:09:28 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a5fa998ad | ||
|
|
cbc8911863 | ||
|
|
4a5307603f | ||
|
|
6d783b8d97 | ||
|
|
0e0299e39d | ||
|
|
1652e6af7b | ||
|
|
e655844712 | ||
|
|
98cd479876 | ||
|
|
45c9b2c9f6 | ||
|
|
a9ae450a64 | ||
|
|
60e161004d | ||
|
|
45c5c83699 | ||
|
|
ecb71c8263 | ||
|
|
e6e3912da0 | ||
|
|
7f6890ff25 | ||
|
|
99daf5d113 | ||
|
|
83ffb8803e | ||
|
|
ace8464acb | ||
|
|
0dbdfd2b9b | ||
|
|
6d4251798a | ||
|
|
11d031935d | ||
|
|
64beb1578a | ||
|
|
f888544554 | ||
|
|
1e538ed87b | ||
|
|
e69deaa132 | ||
|
|
be0a1a4a12 | ||
|
|
004f49744e | ||
|
|
45181504ea | ||
|
|
915dd5062e | ||
|
|
d08c85f7d1 | ||
|
|
2342099bd8 | ||
|
|
06d8e3c639 | ||
|
|
195e67c0c1 | ||
|
|
c75a5583b3 | ||
|
|
981baa8c30 | ||
|
|
aa517bc90e | ||
|
|
c74dc20b97 | ||
|
|
1ba3944944 | ||
|
|
8620b6e5af | ||
|
|
6b6084a848 | ||
|
|
967dd5ea2b | ||
|
|
ad39bf14ed | ||
|
|
01416df5a4 | ||
|
|
eb592e7e33 | ||
|
|
2db07b047e | ||
|
|
8ecc66ae9a | ||
|
|
dfa5f4d9a0 | ||
|
|
75952c4e36 | ||
|
|
31b0b8de73 | ||
|
|
fae47e6122 | ||
|
|
ac3eef439f | ||
|
|
b044bcce5e | ||
|
|
a251cffdb5 | ||
|
|
37858be02f | ||
|
|
9b7240e9e0 | ||
|
|
e362438f03 | ||
|
|
6c79f6bbfc | ||
|
|
58cd1cbc09 | ||
|
|
96e3b48afd | ||
|
|
1f03c40dbe | ||
|
|
e93e8f05a3 | ||
|
|
0f1326e547 | ||
|
|
2a04263f02 | ||
|
|
348446050e | ||
|
|
7e1036b30d | ||
|
|
a15dbc8e03 | ||
|
|
4e02197245 | ||
|
|
4ab7ef06e5 | ||
|
|
dae2faf36d | ||
|
|
58f10a4661 | ||
|
|
64e787fa02 | ||
|
|
c7e2ec9707 | ||
|
|
a4cc198ac7 | ||
|
|
d203bf9024 | ||
|
|
baeec8ec2d | ||
|
|
9c689a3b51 | ||
|
|
e1bf418e72 | ||
|
|
b6d8c07d4d | ||
|
|
b814531c84 | ||
|
|
3b3cd0a0b2 | ||
|
|
b2101a85ad | ||
|
|
ba760c56e5 | ||
|
|
787c72ea18 | ||
|
|
a2b7a59322 | ||
|
|
ec090c5680 | ||
|
|
58f74ae543 | ||
|
|
2e8bcbf719 | ||
|
|
10f523430b | ||
|
|
5563a595d5 | ||
|
|
dd3deb6cbd | ||
|
|
7841020b2b | ||
|
|
1c394986db | ||
|
|
870935ebdc | ||
|
|
1be82fa56d | ||
|
|
ce0f6c187e | ||
|
|
559ec392f6 | ||
|
|
c71a4082e6 | ||
|
|
ce269ffa58 | ||
|
|
61d0893d7c | ||
|
|
eef5fb9b37 | ||
|
|
f03d555747 |
@@ -0,0 +1,9 @@
|
||||
pull_request_rules:
|
||||
- name: backport patches to 2.x
|
||||
conditions:
|
||||
- base=main
|
||||
- label=backport
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- "2.x"
|
||||
@@ -2,7 +2,7 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ["main", "2.x"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
|
||||
@@ -73,6 +73,8 @@ jobs:
|
||||
|
||||
pip:
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -100,13 +102,10 @@ jobs:
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.TEST_PYPI_TOKEN }}
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
skip_existing: true
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, 2.x]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '5 1 * * *' # every day at 01:05
|
||||
@@ -42,6 +42,8 @@ jobs:
|
||||
- uses: pre-commit/[email protected]
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
PYTHONUTF8: 1
|
||||
run: >
|
||||
pytest -n=logical --timeout=300 --durations=100
|
||||
--cov --cov-report=xml --cov-report=term
|
||||
|
||||
@@ -43,7 +43,7 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.260'
|
||||
rev: 'v0.0.261'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
|
||||
+14
-2
@@ -28,9 +28,21 @@ class Annotation:
|
||||
|
||||
|
||||
@dataclass
|
||||
class Artifact(Annotation):
|
||||
class Artifact:
|
||||
PARAM_PATH: ClassVar[str] = "path"
|
||||
path: Optional[str] = None
|
||||
PARAM_DESC: ClassVar[str] = "desc"
|
||||
PARAM_TYPE: ClassVar[str] = "type"
|
||||
PARAM_LABELS: ClassVar[str] = "labels"
|
||||
PARAM_META: ClassVar[str] = "meta"
|
||||
|
||||
path: str
|
||||
desc: Optional[str] = None
|
||||
type: Optional[str] = None # noqa: A003
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
ANNOTATION_FIELDS = [field.name for field in fields(Annotation)]
|
||||
|
||||
+6
-1
@@ -2,11 +2,16 @@ from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .data import open # pylint: disable=redefined-builtin
|
||||
from .data import get_url, read
|
||||
from .experiments import exp_save, make_checkpoint
|
||||
from .experiments import exp_save, exp_show, make_checkpoint
|
||||
from .scm import all_branches, all_commits, all_tags
|
||||
from .show import metrics_show, params_show
|
||||
|
||||
__all__ = [
|
||||
"all_branches",
|
||||
"all_commits",
|
||||
"all_tags",
|
||||
"exp_save",
|
||||
"exp_show",
|
||||
"get_url",
|
||||
"make_checkpoint",
|
||||
"open",
|
||||
|
||||
+78
-1
@@ -1,10 +1,13 @@
|
||||
import builtins
|
||||
import os
|
||||
from time import sleep
|
||||
from typing import List, Optional
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments.show import tabulate
|
||||
from dvc.stage.monitor import CheckpointTask
|
||||
|
||||
|
||||
@@ -69,3 +72,77 @@ def exp_save(
|
||||
return repo.experiments.save(
|
||||
name=name, force=force, include_untracked=include_untracked
|
||||
)
|
||||
|
||||
|
||||
def _postprocess(exp_rows):
|
||||
for exp_row in exp_rows:
|
||||
for k, v in exp_row.items():
|
||||
if isinstance(v, Text):
|
||||
v_str = str(v)
|
||||
try:
|
||||
exp_row[k] = float(v_str)
|
||||
except ValueError:
|
||||
exp_row[k] = v_str
|
||||
|
||||
if not exp_row[k]:
|
||||
exp_row[k] = None
|
||||
|
||||
return exp_rows
|
||||
|
||||
|
||||
def exp_show(
|
||||
repo: Optional[str] = None,
|
||||
revs: Optional[Union[str, List[str]]] = None,
|
||||
num: int = 1,
|
||||
param_deps: bool = False,
|
||||
force: bool = False,
|
||||
) -> List[Dict]:
|
||||
"""Get DVC experiments tracked in `repo`.
|
||||
|
||||
Without arguments, this function will retrieve all experiments derived from
|
||||
the Git `HEAD`.
|
||||
|
||||
See the options below to customize the experiments retrieved.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
revs (Union[str, List[str]], optional): Git revision(s) (e.g. branch,
|
||||
tag, SHA commit) to use as a reference point to start listing
|
||||
experiments.
|
||||
Defaults to `None`, which will use `HEAD` as starting point.
|
||||
num (int, optional): show experiments from the last `num` commits
|
||||
(first parents) starting from the `revs` baseline.
|
||||
Give a negative value to include all first-parent commits (similar
|
||||
to `git log -n`).
|
||||
Defaults to 1.
|
||||
param_deps (bool, optional): include only parameters that are stage
|
||||
dependencies.
|
||||
Defaults to `False`.
|
||||
force (bool, optional): force re-collection of experiments instead of
|
||||
loading from internal experiments cache.
|
||||
DVC caches `exp_show` data for completed experiments to improve
|
||||
performance of subsequent calls.
|
||||
When `force` is specified, DVC will reload all experiment data and
|
||||
ignore any previously cached results.
|
||||
Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
List[Dict]: Each item in the list will contain a dictionary with
|
||||
the info for an individual experiment.
|
||||
See Examples below.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
experiments = _repo.experiments.show(
|
||||
revs=revs,
|
||||
num=num,
|
||||
param_deps=param_deps,
|
||||
force=force,
|
||||
)
|
||||
td, _ = tabulate(experiments, fill_value=None)
|
||||
|
||||
return _postprocess(td.as_dict())
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def all_branches(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git branches in a DVC repository.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
Returns:
|
||||
List[str]: Names of the Git branches.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_branches()
|
||||
|
||||
|
||||
def all_commits(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git commits in a DVC repository.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
Returns:
|
||||
List[str]: SHAs of the Git commits.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_all_commits()
|
||||
|
||||
|
||||
def all_tags(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git tags in a DVC repository.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
Returns:
|
||||
List[str]: Names of the Git tags.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_tags()
|
||||
+1
-1
@@ -245,7 +245,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
finally:
|
||||
logger.setLevel(outer_log_level)
|
||||
|
||||
from dvc.external_repo import clean_repos
|
||||
from dvc.repo.open_repo import clean_repos
|
||||
|
||||
# Remove cached repos in the end of the call, these are anonymous
|
||||
# so won't be reused by any other subsequent run anyway.
|
||||
|
||||
+11
-1
@@ -62,8 +62,11 @@ class CmdAdd(CmdBase):
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
@@ -140,6 +143,13 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
|
||||
_add_annotating_args(parser)
|
||||
parser.add_argument(
|
||||
|
||||
@@ -117,6 +117,7 @@ class CmdDataStatus(CmdBase):
|
||||
status = self.repo.data_status(
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
remote_refresh=self.args.remote_refresh,
|
||||
)
|
||||
|
||||
@@ -244,10 +245,16 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--remote-refresh",
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Refresh remote index.",
|
||||
help="Show files not in remote.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--no-remote-refresh",
|
||||
dest="remote_refresh",
|
||||
action="store_false",
|
||||
help="Use cached remote index (don't check remote).",
|
||||
)
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ def add_rev_selection_flags(
|
||||
experiments_subcmd_parser.add_argument(
|
||||
"--rev",
|
||||
type=str,
|
||||
action="append",
|
||||
default=None,
|
||||
help=msg,
|
||||
metavar="<commit>",
|
||||
|
||||
@@ -20,6 +20,7 @@ class CmdExecutorRun(CmdBaseNoRepo):
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
infofile=self.args.infofile,
|
||||
copy_paths=self.args.copy_paths,
|
||||
message=self.args.message,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -47,4 +48,11 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
" Only used if `--temp` or `--queue` is specified."
|
||||
),
|
||||
)
|
||||
exec_run_parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
exec_run_parser.set_defaults(func=CmdExecutorRun)
|
||||
|
||||
@@ -52,7 +52,9 @@ class CmdExperimentsPush(CmdBase):
|
||||
ui.write(humanize.get_summary(stats.items()))
|
||||
|
||||
if project_url := result.get("url"):
|
||||
ui.write("[yellow]View your experiments at", project_url, styled=True)
|
||||
ui.rich_print(
|
||||
"View your experiments at", project_url, style="yellow", soft_wrap=True
|
||||
)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo.experiments.push import UploadError
|
||||
|
||||
@@ -37,6 +37,7 @@ class CmdExperimentsRun(CmdRepro):
|
||||
tmp_dir=self.args.tmp_dir,
|
||||
machine=self.args.machine,
|
||||
copy_paths=self.args.copy_paths,
|
||||
message=self.args.message,
|
||||
**self._common_kwargs,
|
||||
)
|
||||
|
||||
@@ -147,3 +148,10 @@ def _add_run_common(parser):
|
||||
" Only used if `--temp` or `--queue` is specified."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ class CmdExperimentsSave(CmdBase):
|
||||
name=self.args.name,
|
||||
force=self.args.force,
|
||||
include_untracked=self.args.include_untracked,
|
||||
message=self.args.message,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to save experiment")
|
||||
@@ -75,4 +76,11 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help="List of untracked paths to include in the experiment.",
|
||||
metavar="<path>",
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
save_parser.set_defaults(func=CmdExperimentsSave)
|
||||
|
||||
@@ -2,9 +2,8 @@ import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING, Any, Dict, Set
|
||||
from typing import TYPE_CHECKING, Dict, Iterable
|
||||
|
||||
from funcy import lmap
|
||||
|
||||
@@ -12,9 +11,8 @@ from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.metrics import DEFAULT_PRECISION
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.flatten import flatten
|
||||
from dvc.utils.serialize import encode_exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -28,30 +26,6 @@ FILL_VALUE_ERRORED = "!"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _update_names(names, items):
|
||||
for name, item in items:
|
||||
item = item.get("data", {})
|
||||
if isinstance(item, dict):
|
||||
item = flatten(item)
|
||||
names[name].update({key: None for key in item})
|
||||
|
||||
|
||||
def _collect_names(all_experiments, **kwargs):
|
||||
metric_names: Dict[str, Dict[str, Any]] = defaultdict(dict)
|
||||
param_names: Dict[str, Dict[str, Any]] = defaultdict(dict)
|
||||
deps_names: Set[str] = set()
|
||||
|
||||
for _, experiments in all_experiments.items():
|
||||
for exp_data in experiments.values():
|
||||
exp = exp_data.get("data", {})
|
||||
_update_names(metric_names, exp.get("metrics", {}).items())
|
||||
_update_names(param_names, exp.get("params", {}).items())
|
||||
for dep_name in exp.get("deps", {}):
|
||||
deps_names.add(dep_name)
|
||||
|
||||
return metric_names, param_names, sorted(deps_names)
|
||||
|
||||
|
||||
experiment_types = {
|
||||
"checkpoint_tip": "│ ╓",
|
||||
"checkpoint_commit": "│ ╟",
|
||||
@@ -62,229 +36,6 @@ experiment_types = {
|
||||
}
|
||||
|
||||
|
||||
def _collect_rows( # noqa: C901, PLR0912, PLR0913, PLR0915
|
||||
base_rev,
|
||||
experiments,
|
||||
all_headers,
|
||||
metric_headers,
|
||||
param_headers,
|
||||
metric_names,
|
||||
param_names,
|
||||
deps_names,
|
||||
precision=DEFAULT_PRECISION,
|
||||
sort_by=None,
|
||||
sort_order=None,
|
||||
fill_value=FILL_VALUE,
|
||||
iso=False,
|
||||
):
|
||||
from dvc.scm import Git
|
||||
|
||||
if sort_by:
|
||||
sort_path, sort_name, sort_type = _sort_column(
|
||||
sort_by, metric_names, param_names
|
||||
)
|
||||
reverse = sort_order == "desc"
|
||||
experiments = _sort_exp(experiments, sort_path, sort_name, sort_type, reverse)
|
||||
|
||||
new_checkpoint = True
|
||||
for i, (rev, results) in enumerate(experiments.items()):
|
||||
fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value
|
||||
row_dict = {k: fill_value for k in all_headers}
|
||||
|
||||
exp = results.get("data", {})
|
||||
|
||||
state = exp.get("status")
|
||||
if state == "Success":
|
||||
state = fill_value
|
||||
|
||||
is_baseline = rev == "baseline"
|
||||
|
||||
if is_baseline:
|
||||
name_rev = base_rev[:7] if Git.is_sha(base_rev) else base_rev
|
||||
else:
|
||||
name_rev = rev[:7]
|
||||
|
||||
tip = exp.get("checkpoint_tip")
|
||||
parent_rev = exp.get("checkpoint_parent", "")
|
||||
parent_exp = experiments.get(parent_rev, {}).get("data", {})
|
||||
parent_tip = parent_exp.get("checkpoint_tip")
|
||||
|
||||
parent = ""
|
||||
if is_baseline:
|
||||
typ = "baseline"
|
||||
elif tip:
|
||||
if tip == parent_tip:
|
||||
typ = "checkpoint_tip" if new_checkpoint else "checkpoint_commit"
|
||||
elif parent_rev == base_rev:
|
||||
typ = "checkpoint_base"
|
||||
else:
|
||||
typ = "checkpoint_commit"
|
||||
parent = parent_rev[:7]
|
||||
elif i < len(experiments) - 1:
|
||||
typ = "branch_commit"
|
||||
else:
|
||||
typ = "branch_base"
|
||||
|
||||
if not is_baseline:
|
||||
new_checkpoint = not (tip and tip == parent_tip)
|
||||
|
||||
row_dict["Experiment"] = exp.get("name", "")
|
||||
row_dict["rev"] = name_rev
|
||||
row_dict["typ"] = typ
|
||||
row_dict["Created"] = format_time(exp.get("timestamp"), fill_value, iso)
|
||||
row_dict["parent"] = parent
|
||||
row_dict["State"] = state
|
||||
row_dict["Executor"] = exp.get("executor", fill_value)
|
||||
|
||||
_extend_row(
|
||||
row_dict,
|
||||
metric_names,
|
||||
metric_headers,
|
||||
exp.get("metrics", {}).items(),
|
||||
precision,
|
||||
fill_value=fill_value,
|
||||
)
|
||||
_extend_row(
|
||||
row_dict,
|
||||
param_names,
|
||||
param_headers,
|
||||
exp.get("params", {}).items(),
|
||||
precision,
|
||||
fill_value=fill_value,
|
||||
)
|
||||
for dep in deps_names:
|
||||
hash_info = exp.get("deps", {}).get(dep, {}).get("hash")
|
||||
if hash_info is not None:
|
||||
hash_info = hash_info[:7]
|
||||
row_dict[dep] = hash_info
|
||||
yield list(row_dict.values())
|
||||
|
||||
|
||||
def _sort_column(sort_by, metric_names, param_names):
|
||||
path, _, sort_name = sort_by.rpartition(":")
|
||||
matches = set()
|
||||
|
||||
if path:
|
||||
if path in metric_names and sort_name in metric_names[path]:
|
||||
matches.add((path, sort_name, "metrics"))
|
||||
if path in param_names and sort_name in param_names[path]:
|
||||
matches.add((path, sort_name, "params"))
|
||||
else:
|
||||
for path in metric_names:
|
||||
if sort_name in metric_names[path]:
|
||||
matches.add((path, sort_name, "metrics"))
|
||||
for path in param_names:
|
||||
if sort_name in param_names[path]:
|
||||
matches.add((path, sort_name, "params"))
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches.pop()
|
||||
if len(matches) > 1:
|
||||
raise InvalidArgumentError(
|
||||
"Ambiguous sort column '{}' matched '{}'".format(
|
||||
sort_by,
|
||||
", ".join([f"{path}:{name}" for path, name, _ in matches]),
|
||||
)
|
||||
)
|
||||
raise InvalidArgumentError(f"Unknown sort column '{sort_by}'")
|
||||
|
||||
|
||||
def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
|
||||
def _sort(item):
|
||||
rev, exp = item
|
||||
exp_data = exp.get("data", {})
|
||||
tip = exp_data.get("checkpoint_tip")
|
||||
if tip and tip != rev:
|
||||
# Sort checkpoint experiments by tip commit
|
||||
return _sort((tip, experiments[tip]))
|
||||
data = exp_data.get(typ, {}).get(sort_path, {}).get("data", {})
|
||||
val = flatten(data).get(sort_name)
|
||||
return val is None, val
|
||||
|
||||
ret = OrderedDict()
|
||||
if "baseline" in experiments:
|
||||
ret["baseline"] = experiments.pop("baseline")
|
||||
|
||||
ret.update(sorted(experiments.items(), key=_sort, reverse=reverse))
|
||||
return ret
|
||||
|
||||
|
||||
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 datetime_obj.strftime(fmt)
|
||||
|
||||
|
||||
def _extend_row(row, names, headers, items, precision, fill_value=FILL_VALUE):
|
||||
from dvc.compare import _format_field, with_value
|
||||
|
||||
for fname, data in items:
|
||||
item = data.get("data", {})
|
||||
item = flatten(item) if isinstance(item, dict) else {fname: item}
|
||||
for name in names[fname]:
|
||||
value = with_value(
|
||||
item.get(name),
|
||||
FILL_VALUE_ERRORED if data.get("error", None) else fill_value,
|
||||
)
|
||||
# wrap field data in ui.rich_text, otherwise rich may
|
||||
# interpret unescaped braces from list/dict types as rich
|
||||
# markup tags
|
||||
value = ui.rich_text(str(_format_field(value, precision)))
|
||||
if name in headers:
|
||||
row[name] = value
|
||||
else:
|
||||
row[f"{fname}:{name}"] = value
|
||||
|
||||
|
||||
def experiments_table( # noqa: PLR0913
|
||||
all_experiments,
|
||||
headers,
|
||||
metric_headers,
|
||||
metric_names,
|
||||
param_headers,
|
||||
param_names,
|
||||
deps_names,
|
||||
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
|
||||
|
||||
all_headers = lconcat(headers, metric_headers, param_headers, deps_names)
|
||||
td = TabularData(all_headers, fill_value=fill_value)
|
||||
for base_rev, experiments in all_experiments.items():
|
||||
rows = _collect_rows(
|
||||
base_rev,
|
||||
experiments,
|
||||
all_headers,
|
||||
metric_headers,
|
||||
param_headers,
|
||||
metric_names,
|
||||
param_names,
|
||||
deps_names,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
precision=precision,
|
||||
fill_value=fill_value,
|
||||
iso=iso,
|
||||
)
|
||||
td.extend(rows)
|
||||
|
||||
return td
|
||||
|
||||
|
||||
def prepare_exp_id(kwargs) -> "RichText":
|
||||
exp_name = kwargs["Experiment"]
|
||||
rev = kwargs["rev"]
|
||||
@@ -309,7 +60,8 @@ def baseline_styler(typ):
|
||||
|
||||
|
||||
def show_experiments( # noqa: C901, PLR0912
|
||||
all_experiments,
|
||||
td: "TabularData",
|
||||
headers: Dict[str, Iterable[str]],
|
||||
keep=None,
|
||||
drop=None,
|
||||
pager=True,
|
||||
@@ -318,40 +70,6 @@ def show_experiments( # noqa: C901, PLR0912
|
||||
pcp=False,
|
||||
**kwargs,
|
||||
):
|
||||
from funcy.seqs import flatten as flatten_list
|
||||
|
||||
metric_names, param_names, deps_names = _collect_names(all_experiments)
|
||||
|
||||
headers_list = [
|
||||
"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_list)
|
||||
metric_headers = _normalize_headers(metric_names, counter)
|
||||
param_headers = _normalize_headers(param_names, counter)
|
||||
|
||||
td = experiments_table(
|
||||
all_experiments,
|
||||
headers_list,
|
||||
metric_headers,
|
||||
metric_names,
|
||||
param_headers,
|
||||
param_names,
|
||||
deps_names,
|
||||
kwargs.get("sort_by"),
|
||||
kwargs.get("sort_order"),
|
||||
kwargs.get("precision"),
|
||||
kwargs.get("fill_value"),
|
||||
kwargs.get("iso"),
|
||||
)
|
||||
if keep:
|
||||
for col in td.keys(): # noqa: SIM118
|
||||
if re.match(keep, col):
|
||||
@@ -368,11 +86,6 @@ def show_experiments( # noqa: C901, PLR0912
|
||||
td.column("Experiment")[:] = map(prepare_exp_id, td.as_dict(merge_headers))
|
||||
td.drop(*merge_headers[1:])
|
||||
|
||||
headers = {
|
||||
"metrics": metric_headers,
|
||||
"params": param_headers,
|
||||
"deps": deps_names,
|
||||
}
|
||||
styles = {
|
||||
"Experiment": {"no_wrap": True, "header_style": "black on grey93"},
|
||||
"Created": {"header_style": "black on grey93"},
|
||||
@@ -453,19 +166,17 @@ def _normalize_headers(names, count):
|
||||
|
||||
|
||||
def _format_json(item):
|
||||
from dvc.repo.experiments.show import _CachedError
|
||||
|
||||
if isinstance(item, (date, datetime)):
|
||||
return item.isoformat()
|
||||
if isinstance(item, _CachedError):
|
||||
return {"type": getattr(item, "typ", "_CachedError"), "msg": str(item)}
|
||||
return encode_exception(item)
|
||||
|
||||
|
||||
class CmdExperimentsShow(CmdBase):
|
||||
def run(self):
|
||||
from dvc.repo.experiments.show import tabulate
|
||||
|
||||
try:
|
||||
all_experiments = self.repo.experiments.show(
|
||||
exps = self.repo.experiments.show(
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
@@ -483,23 +194,29 @@ class CmdExperimentsShow(CmdBase):
|
||||
return 1
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(all_experiments, default=_format_json)
|
||||
ui.write_json([exp.dumpd() for exp in exps], default=_format_json)
|
||||
else:
|
||||
precision = (
|
||||
self.args.precision or None if self.args.csv else DEFAULT_PRECISION
|
||||
)
|
||||
fill_value = "" if self.args.csv else FILL_VALUE
|
||||
iso = self.args.csv
|
||||
td, headers = tabulate(
|
||||
exps,
|
||||
precision=precision,
|
||||
fill_value=fill_value,
|
||||
iso=iso,
|
||||
sort_by=self.args.sort_by,
|
||||
sort_order=self.args.sort_order,
|
||||
)
|
||||
|
||||
show_experiments(
|
||||
all_experiments,
|
||||
td,
|
||||
headers,
|
||||
keep=self.args.keep,
|
||||
drop=self.args.drop,
|
||||
sort_by=self.args.sort_by,
|
||||
sort_order=self.args.sort_order,
|
||||
precision=precision,
|
||||
fill_value=fill_value,
|
||||
iso=iso,
|
||||
pager=not self.args.no_pager,
|
||||
csv=self.args.csv,
|
||||
markdown=self.args.markdown,
|
||||
|
||||
+15
-2
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdGC(CmdBase):
|
||||
def run(self): # noqa: C901
|
||||
def run(self): # noqa: C901, PLR0912
|
||||
from dvc.repo.gc import _validate_args
|
||||
|
||||
_validate_args(
|
||||
@@ -22,6 +22,8 @@ class CmdGC(CmdBase):
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
cloud=self.args.cloud,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
)
|
||||
|
||||
if self.args.rev:
|
||||
@@ -46,6 +48,10 @@ class CmdGC(CmdBase):
|
||||
|
||||
if self.args.all_experiments:
|
||||
msg += " and all experiments"
|
||||
|
||||
if self.args.not_in_remote:
|
||||
msg += " that are present in the DVC remote"
|
||||
|
||||
if self.args.repos:
|
||||
msg += " of the current and the following repos:"
|
||||
|
||||
@@ -74,6 +80,7 @@ class CmdGC(CmdBase):
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -157,12 +164,18 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Keep data files for all experiments.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep data files that are not present in the remote.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-c",
|
||||
"--cloud",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Collect garbage in remote repository.",
|
||||
help="Collect garbage in remote storage in addition to local cache.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-r",
|
||||
|
||||
@@ -36,6 +36,7 @@ class CmdGet(CmdBaseNoRepo):
|
||||
out=self.args.out,
|
||||
rev=self.args.rev,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
)
|
||||
return 0
|
||||
except CloneError:
|
||||
@@ -94,4 +95,11 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
get_parser.set_defaults(func=CmdGet)
|
||||
|
||||
+13
-1
@@ -14,7 +14,12 @@ class CmdGetUrl(CmdBaseNoRepo):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
Repo.get_url(self.args.url, out=self.args.out, jobs=self.args.jobs)
|
||||
Repo.get_url(
|
||||
self.args.url,
|
||||
out=self.args.out,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
)
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception("failed to get '%s'", self.args.url)
|
||||
@@ -46,4 +51,11 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
get_parser.set_defaults(func=CmdGetUrl)
|
||||
|
||||
@@ -25,6 +25,7 @@ class CmdImportUrl(CmdBase):
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
version_aware=self.args.version_aware,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -112,6 +113,13 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--version-aware",
|
||||
action="store_true",
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.experiments.show import format_time
|
||||
from dvc.compare import TabularData
|
||||
from dvc.ui import ui
|
||||
|
||||
@@ -15,9 +13,9 @@ class CmdQueueStatus(CmdBase):
|
||||
"""Show queue task and worker status."""
|
||||
|
||||
def run(self) -> int:
|
||||
result: List[
|
||||
Dict[str, Optional[str]]
|
||||
] = self.repo.experiments.celery_queue.status()
|
||||
from dvc.repo.experiments.show import format_time
|
||||
|
||||
result = self.repo.experiments.celery_queue.status()
|
||||
if result:
|
||||
all_headers = ["Task", "Name", "Created", "Status"]
|
||||
td = TabularData(all_headers)
|
||||
|
||||
@@ -38,6 +38,7 @@ class CmdRepro(CmdBase):
|
||||
"recursive": self.args.recursive,
|
||||
"force_downstream": self.args.force_downstream,
|
||||
"pull": self.args.pull,
|
||||
"allow_missing": self.args.allow_missing,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -135,6 +136,12 @@ and then the stage name name.
|
||||
"from the run-cache."
|
||||
),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--allow-missing",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=("Skip stages with missing data but no other changes."),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--dry",
|
||||
action="store_true",
|
||||
|
||||
+30
-1
@@ -1,5 +1,6 @@
|
||||
import argparse
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from itertools import chain, filterfalse
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List
|
||||
|
||||
@@ -119,8 +120,22 @@ def parse_cmd(commands: List[str]) -> str:
|
||||
return " ".join(map(quote_argument, commands))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _disable_logging(highest_level=logging.CRITICAL):
|
||||
previous_level = logging.root.manager.disable
|
||||
|
||||
logging.disable(highest_level)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logging.disable(previous_level)
|
||||
|
||||
|
||||
class CmdStageAdd(CmdBase):
|
||||
def run(self):
|
||||
from dvc.repo import lock_repo
|
||||
|
||||
kwargs = vars(self.args)
|
||||
kwargs.update(
|
||||
{
|
||||
@@ -128,7 +143,15 @@ class CmdStageAdd(CmdBase):
|
||||
"params": parse_params(self.args.params),
|
||||
}
|
||||
)
|
||||
self.repo.stage.add(**kwargs)
|
||||
|
||||
with self.repo.scm_context, lock_repo(self.repo):
|
||||
with _disable_logging(logging.INFO):
|
||||
stage = self.repo.stage.add(**kwargs)
|
||||
logger.info("Added stage %r in %r", stage.addressing, stage.relpath)
|
||||
if self.args.run:
|
||||
stage.run()
|
||||
stage.dump(update_pipeline=False)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -257,6 +280,12 @@ def _add_common_args(parser):
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Execute the stage after generating it.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"command",
|
||||
nargs=argparse.REMAINDER,
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ def with_value(value, default):
|
||||
|
||||
|
||||
class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
def __init__(self, columns: Sequence[str], fill_value: str = ""):
|
||||
def __init__(self, columns: Sequence[str], fill_value: Optional[str] = ""):
|
||||
self._columns: Dict[str, Column] = {name: Column() for name in columns}
|
||||
self._keys: List[str] = list(columns)
|
||||
self._fill_value = fill_value
|
||||
|
||||
+3
-6
@@ -79,9 +79,6 @@ class Config(dict):
|
||||
ConfigError: thrown if config has an invalid format.
|
||||
"""
|
||||
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
SYSTEM_LEVELS = ("system", "global")
|
||||
REPO_LEVELS = ("repo", "local")
|
||||
# In the order they shadow each other
|
||||
@@ -121,14 +118,14 @@ class Config(dict):
|
||||
|
||||
@classmethod
|
||||
def get_dir(cls, level):
|
||||
from platformdirs import site_config_dir, user_config_dir
|
||||
from dvc.dirs import global_config_dir, system_config_dir
|
||||
|
||||
assert level in ("global", "system")
|
||||
|
||||
if level == "global":
|
||||
return user_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
return global_config_dir()
|
||||
if level == "system":
|
||||
return site_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
return system_config_dir()
|
||||
|
||||
@cached_property
|
||||
def files(self) -> Dict[str, str]:
|
||||
|
||||
@@ -215,6 +215,7 @@ SCHEMA = {
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
"allow_agent": Bool,
|
||||
"max_sessions": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
@@ -301,9 +302,6 @@ SCHEMA = {
|
||||
"feature": FeatureSchema(
|
||||
{
|
||||
Optional("machine", default=False): Bool,
|
||||
Optional("push_exp_to_studio", default=False): Bool,
|
||||
"studio_token": str,
|
||||
"studio_url": str,
|
||||
},
|
||||
),
|
||||
"plots": {
|
||||
@@ -329,4 +327,10 @@ SCHEMA = {
|
||||
"config_dir": str,
|
||||
"config_name": str,
|
||||
},
|
||||
"studio": {
|
||||
"token": str,
|
||||
"url": str,
|
||||
"repo_url": str,
|
||||
Optional("offline", default=False): Bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -79,10 +79,14 @@ class RepoDependency(Dependency):
|
||||
) -> "DVCFileSystem":
|
||||
from dvc.fs import DVCFileSystem
|
||||
|
||||
config = {"cache": self.repo.config["cache"]}
|
||||
config["cache"]["dir"] = self.repo.cache.local.path
|
||||
|
||||
return DVCFileSystem(
|
||||
url=self.def_repo[self.PARAM_URL],
|
||||
rev=rev or self._get_rev(locked=locked),
|
||||
subrepos=True,
|
||||
config=config,
|
||||
)
|
||||
|
||||
def _get_rev(self, locked: bool = True):
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
|
||||
import platformdirs
|
||||
|
||||
from . import env
|
||||
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
|
||||
def system_config_dir():
|
||||
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.site_config_dir(
|
||||
APPNAME, APPAUTHOR
|
||||
)
|
||||
|
||||
|
||||
def global_config_dir():
|
||||
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.user_config_dir(
|
||||
APPNAME, APPAUTHOR
|
||||
)
|
||||
|
||||
|
||||
def site_cache_dir():
|
||||
return os.getenv(env.DVC_SITE_CACHE_DIR) or platformdirs.site_cache_dir(
|
||||
APPNAME, APPAUTHOR, opinion=True
|
||||
)
|
||||
+14
-7
@@ -1,12 +1,19 @@
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_CHECKPOINT = "DVC_CHECKPOINT"
|
||||
DVC_DAEMON = "DVC_DAEMON"
|
||||
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
|
||||
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
DVC_EXP_NAME = "DVC_EXP_NAME"
|
||||
DVC_GLOBAL_CONFIG_DIR = "DVC_GLOBAL_CONFIG_DIR"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
|
||||
DVC_PAGER = "DVC_PAGER"
|
||||
DVC_ROOT = "DVC_ROOT"
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
|
||||
DVC_EXP_NAME = "DVC_EXP_NAME"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
|
||||
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
|
||||
DVC_SHOW_TRACEBACK = "DVC_SHOW_TRACEBACK"
|
||||
DVC_SITE_CACHE_DIR = "DVC_SITE_CACHE_DIR"
|
||||
DVC_STUDIO_OFFLINE = "DVC_STUDIO_OFFLINE"
|
||||
DVC_STUDIO_REPO_URL = "DVC_STUDIO_REPO_URL"
|
||||
DVC_STUDIO_TOKEN = "DVC_STUDIO_TOKEN" # noqa: S105 # nosec B105
|
||||
DVC_STUDIO_URL = "DVC_STUDIO_URL"
|
||||
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Exceptions raised by the dvc."""
|
||||
import errno
|
||||
from typing import Dict, List
|
||||
|
||||
from dvc.utils import format_link
|
||||
@@ -206,6 +207,18 @@ class ETagMismatchError(DvcException):
|
||||
)
|
||||
|
||||
|
||||
class FileExistsLocallyError(FileExistsError, DvcException):
|
||||
def __init__(self, path, hint=None):
|
||||
import os.path
|
||||
|
||||
self.path = path
|
||||
hint = "" if hint is None else f". {hint}"
|
||||
path_typ = "directory" if os.path.isdir(path) else "file"
|
||||
msg = f"The {path_typ} '{path}' already exists locally{hint}"
|
||||
super().__init__(msg)
|
||||
self.errno = errno.EEXIST
|
||||
|
||||
|
||||
class FileMissingError(DvcException):
|
||||
def __init__(self, path, hint=None):
|
||||
self.path = path
|
||||
|
||||
+18
-6
@@ -32,6 +32,7 @@ def get_dvc_info():
|
||||
f"Platform: Python {platform.python_version()} on {platform.platform()}",
|
||||
f"Subprojects:{_get_subprojects()}",
|
||||
f"Supports:{_get_supported_remotes()}",
|
||||
f"Config:{_get_config_dirs()}",
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -42,7 +43,7 @@ def get_dvc_info():
|
||||
# `dvc config cache.shared group`.
|
||||
if os.path.exists(repo.cache.local.path):
|
||||
info.append(f"Cache types: {_get_linktype_support_info(repo)}")
|
||||
fs_type = get_fs_type(repo.cache.local.path)
|
||||
fs_type = _get_fs_type(repo.cache.local.path)
|
||||
info.append(f"Cache directory: {fs_type}")
|
||||
else:
|
||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||
@@ -51,7 +52,7 @@ def get_dvc_info():
|
||||
info.append(f"Remotes: {_get_remotes(repo.config)}")
|
||||
|
||||
root_directory = repo.root_dir
|
||||
fs_root = get_fs_type(os.path.abspath(root_directory))
|
||||
fs_root = _get_fs_type(os.path.abspath(root_directory))
|
||||
info.append(f"Workspace directory: {fs_root}")
|
||||
info.append(f"Repo: {_get_dvc_repo_info(repo)}")
|
||||
info.append(f"Repo.site_cache_dir: {repo.site_cache_dir}")
|
||||
@@ -136,7 +137,18 @@ def _get_supported_remotes():
|
||||
return "\n\t" + ",\n\t".join(supported_remotes)
|
||||
|
||||
|
||||
def get_fs_type(path):
|
||||
def _get_config_dirs():
|
||||
from dvc.config import Config
|
||||
|
||||
dirs = [
|
||||
f"Global: {Config.get_dir('global')}",
|
||||
f"System: {Config.get_dir('system')}",
|
||||
]
|
||||
|
||||
return "\n\t" + "\n\t".join(dirs)
|
||||
|
||||
|
||||
def _get_fs_type(path):
|
||||
partition = {}
|
||||
for part in psutil.disk_partitions(all=True):
|
||||
if part.fstype:
|
||||
@@ -155,11 +167,11 @@ def get_fs_type(path):
|
||||
return ("unknown", "none")
|
||||
|
||||
|
||||
def _get_dvc_repo_info(self):
|
||||
if self.config.get("core", {}).get("no_scm", False):
|
||||
def _get_dvc_repo_info(repo):
|
||||
if repo.config.get("core", {}).get("no_scm", False):
|
||||
return "dvc (no_scm)"
|
||||
|
||||
if self.root_dir != self.scm.root_dir:
|
||||
if repo.root_dir != repo.scm.root_dir:
|
||||
return "dvc (subdir), git"
|
||||
|
||||
return "dvc, git"
|
||||
|
||||
+201
-24
@@ -1,10 +1,11 @@
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import collecting, first, project
|
||||
@@ -25,26 +26,25 @@ from dvc_data.hashfile import check as ocheck
|
||||
from dvc_data.hashfile import load as oload
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
from dvc_data.hashfile.db import HashFileDB, add_update_tree
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.istextfile import istextfile
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.hashfile.transfer import transfer as otransfer
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
from dvc_data.hashfile.tree import Tree, du
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
|
||||
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
|
||||
from .fs.callbacks import DEFAULT_CALLBACK
|
||||
from .fs.callbacks import DEFAULT_CALLBACK, Callback, TqdmCallback
|
||||
from .utils import relpath
|
||||
from .utils.fs import path_isin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_data.index import DataIndexKey
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
from .fs.callbacks import Callback
|
||||
from .ignore import DvcIgnoreFilter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -276,6 +276,11 @@ class OutputIsIgnoredError(DvcException):
|
||||
super().__init__(f"Path '{match.file}' is ignored by\n{lines}")
|
||||
|
||||
|
||||
class CheckoutCallback(TqdmCallback):
|
||||
# disable branching for checkouts
|
||||
branch = Callback.branch # type: ignore[assignment]
|
||||
|
||||
|
||||
class Output:
|
||||
IS_DEPENDENCY = False
|
||||
|
||||
@@ -417,8 +422,6 @@ class Output:
|
||||
|
||||
def _compute_meta_hash_info_from_files(self) -> None:
|
||||
if self.files:
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
|
||||
tree = Tree.from_list(self.files, hash_name=self.hash_name)
|
||||
tree.digest(with_meta=True)
|
||||
self.odb = HashFileDB(tree.fs, tree.path + ".odb")
|
||||
@@ -741,15 +744,18 @@ class Output:
|
||||
hardlink=hardlink,
|
||||
)
|
||||
if relink:
|
||||
self._checkout(
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
)
|
||||
rel = self.fs.path.relpath(filter_info or self.fs_path)
|
||||
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as cb:
|
||||
self._checkout(
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=cb,
|
||||
)
|
||||
self.set_exec()
|
||||
|
||||
def _commit_granular_dir(self, filter_info, hardlink) -> Optional["HashFile"]:
|
||||
@@ -891,9 +897,16 @@ class Output:
|
||||
checkpoint_reset: bool = False,
|
||||
**kwargs,
|
||||
) -> Optional[Tuple[bool, Optional[bool]]]:
|
||||
# callback passed act as a aggregate callback.
|
||||
# do not let checkout to call set_size and change progressbar.
|
||||
class CallbackProxy(Callback):
|
||||
def relative_update(self, inc: int = 1) -> None:
|
||||
progress_callback.relative_update(inc)
|
||||
return super().relative_update(inc)
|
||||
|
||||
callback = CallbackProxy()
|
||||
if not self.use_cache:
|
||||
if progress_callback != DEFAULT_CALLBACK:
|
||||
progress_callback.relative_update(self.get_files_number(filter_info))
|
||||
callback.relative_update(self.get_files_number(filter_info))
|
||||
return None
|
||||
|
||||
obj = self.get_obj(filter_info=filter_info)
|
||||
@@ -915,7 +928,7 @@ class Output:
|
||||
obj,
|
||||
self.cache,
|
||||
force=force,
|
||||
progress_callback=progress_callback,
|
||||
progress_callback=callback,
|
||||
relink=relink,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
@@ -1008,7 +1021,7 @@ class Output:
|
||||
if self.exists:
|
||||
self.cache.unprotect(self.fs_path)
|
||||
|
||||
def get_dir_cache(self, **kwargs):
|
||||
def get_dir_cache(self, **kwargs) -> Optional["Tree"]:
|
||||
if not self.is_dir_checksum:
|
||||
raise DvcException("cannot get dir cache for file checksum")
|
||||
|
||||
@@ -1022,14 +1035,17 @@ class Output:
|
||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||
|
||||
if self.obj:
|
||||
assert isinstance(self.obj, Tree)
|
||||
return self.obj
|
||||
|
||||
try:
|
||||
self.obj = oload(self.cache, self.hash_info)
|
||||
obj = oload(self.cache, self.hash_info)
|
||||
assert isinstance(obj, Tree)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
self.obj = None
|
||||
obj = None
|
||||
|
||||
return self.obj
|
||||
self.obj = obj
|
||||
return obj
|
||||
|
||||
def _collect_used_dir_cache(
|
||||
self, remote=None, force=False, jobs=None, filter_info=None
|
||||
@@ -1165,7 +1181,7 @@ class Output:
|
||||
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
from dvc_data.hashfile.tree import MergeError as TreeMergeError
|
||||
from dvc_data.hashfile.tree import du, merge
|
||||
from dvc_data.hashfile.tree import merge
|
||||
|
||||
assert other
|
||||
|
||||
@@ -1198,6 +1214,167 @@ class Output:
|
||||
nfiles=len(merged),
|
||||
)
|
||||
|
||||
def unstage(self, path: str) -> Tuple["Meta", "Tree"]:
|
||||
from pygtrie import Trie
|
||||
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
assert isinstance(self.fs.path, Path)
|
||||
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
|
||||
|
||||
if self.hash_info:
|
||||
tree = self.get_dir_cache()
|
||||
if tree is None:
|
||||
raise DvcException(f"could not read {self.hash_info.value!r}")
|
||||
else:
|
||||
tree = Tree()
|
||||
|
||||
trie = tree.as_trie()
|
||||
assert isinstance(trie, Trie)
|
||||
|
||||
try:
|
||||
del trie[rel_key:] # type: ignore[misc]
|
||||
except KeyError:
|
||||
raise FileNotFoundError( # noqa: B904
|
||||
errno.ENOENT,
|
||||
os.strerror(errno.ENOENT),
|
||||
self.fs.path.relpath(path),
|
||||
)
|
||||
|
||||
new = tree.from_trie(trie)
|
||||
new.digest()
|
||||
return Meta(nfiles=len(new), isdir=True), new
|
||||
|
||||
def apply(
|
||||
self,
|
||||
path: str,
|
||||
obj: Union["Tree", "HashFile"],
|
||||
meta: "Meta",
|
||||
) -> Tuple["Meta", "Tree"]:
|
||||
from pygtrie import Trie
|
||||
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
assert isinstance(self.fs.path, Path)
|
||||
append_only = True
|
||||
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
|
||||
|
||||
if self.hash_info:
|
||||
tree = self.get_dir_cache()
|
||||
if tree is None:
|
||||
raise DvcException(f"could not read {self.hash_info.value!r}")
|
||||
else:
|
||||
tree = Tree()
|
||||
|
||||
trie = tree.as_trie()
|
||||
assert isinstance(trie, Trie)
|
||||
|
||||
try:
|
||||
del trie[rel_key:] # type: ignore[misc]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
append_only = False
|
||||
|
||||
items = {}
|
||||
if isinstance(obj, Tree):
|
||||
items = {(*rel_key, *key): (m, o) for key, m, o in obj}
|
||||
else:
|
||||
items = {rel_key: (meta, obj.hash_info)}
|
||||
trie.update(items)
|
||||
|
||||
new = Tree.from_trie(trie)
|
||||
new.digest()
|
||||
|
||||
size = self.meta.size if self.meta and self.meta.size else None
|
||||
if append_only and size and meta.size is not None:
|
||||
# if files were only appended, we can sum to the existing size
|
||||
size += meta.size
|
||||
elif self.hash_info and self.hash_info == new.hash_info:
|
||||
# if hashes are same, sizes must have been the same
|
||||
size = self.meta.size
|
||||
else:
|
||||
size = None
|
||||
|
||||
meta = Meta(nfiles=len(new), size=size, isdir=True)
|
||||
return meta, new
|
||||
|
||||
def add( # noqa: C901
|
||||
self, path: Optional[str] = None, no_commit: bool = False, relink: bool = True
|
||||
) -> Optional["HashFile"]:
|
||||
path = path or self.fs_path
|
||||
if self.hash_info and not self.is_dir_checksum and self.fs_path != path:
|
||||
raise DvcException(
|
||||
f"Cannot modify '{self}' which is being tracked as a file"
|
||||
)
|
||||
|
||||
assert self.repo
|
||||
cache = self.cache if self.use_cache else self.repo.cache.local
|
||||
assert isinstance(cache, HashFileDB)
|
||||
|
||||
new: "HashFile"
|
||||
try:
|
||||
assert self.hash_name
|
||||
staging, meta, obj = build(
|
||||
cache,
|
||||
path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=not self.use_cache,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
if self.fs_path == path:
|
||||
raise self.DoesNotExistError(self) from exc
|
||||
if not self.is_dir_checksum:
|
||||
raise
|
||||
|
||||
meta, new = self.unstage(path)
|
||||
staging, obj = None, None
|
||||
else:
|
||||
assert obj
|
||||
assert staging
|
||||
if self.fs_path != path:
|
||||
meta, new = self.apply(path, obj, meta)
|
||||
add_update_tree(staging, new)
|
||||
else:
|
||||
new = obj
|
||||
|
||||
self.obj = new
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.meta = meta
|
||||
self.files = None
|
||||
self.ignore()
|
||||
|
||||
if no_commit or not self.use_cache:
|
||||
return obj
|
||||
|
||||
if isinstance(new, Tree):
|
||||
add_update_tree(cache, new)
|
||||
|
||||
if not obj:
|
||||
return obj
|
||||
|
||||
assert staging
|
||||
assert obj.hash_info
|
||||
otransfer(staging, self.cache, {obj.hash_info}, hardlink=relink, shallow=False)
|
||||
|
||||
if relink:
|
||||
rel = self.fs.path.relpath(path)
|
||||
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as callback:
|
||||
self._checkout(
|
||||
path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=callback,
|
||||
)
|
||||
self.set_exec()
|
||||
return obj
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
return self.fs_path
|
||||
|
||||
+4
-15
@@ -287,18 +287,9 @@ class Repo:
|
||||
|
||||
@staticmethod
|
||||
def open(url, *args, **kwargs): # noqa: A003
|
||||
if url is None:
|
||||
url = os.getcwd()
|
||||
from .open_repo import open_repo
|
||||
|
||||
if os.path.exists(url):
|
||||
try:
|
||||
return Repo(url, *args, **kwargs)
|
||||
except NotDvcRepoError:
|
||||
pass # fallthrough to external_repo
|
||||
|
||||
from dvc.external_repo import external_repo
|
||||
|
||||
return external_repo(url, *args, **kwargs)
|
||||
return open_repo(url, *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def scm(self) -> Union["Git", "NoSCM"]:
|
||||
@@ -563,12 +554,10 @@ class Repo:
|
||||
import getpass
|
||||
import hashlib
|
||||
|
||||
import platformdirs
|
||||
|
||||
from dvc.dirs import site_cache_dir
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
default = platformdirs.site_cache_dir("dvc", "iterative", opinion=True)
|
||||
cache_dir = self.config["core"].get("site_cache_dir") or default
|
||||
cache_dir = self.config["core"].get("site_cache_dir") or site_cache_dir()
|
||||
|
||||
if isinstance(self.fs, GitFileSystem):
|
||||
relparts = ()
|
||||
|
||||
+62
-29
@@ -2,14 +2,16 @@ import logging
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from itertools import tee
|
||||
from typing import TYPE_CHECKING, Any, Iterator, List, Optional
|
||||
from typing import TYPE_CHECKING, Any, Iterator, List, NamedTuple, Optional
|
||||
|
||||
import colorama
|
||||
|
||||
from dvc.exceptions import (
|
||||
CacheLinkError,
|
||||
DvcException,
|
||||
InvalidArgumentError,
|
||||
OutputDuplicationError,
|
||||
OutputNotFoundError,
|
||||
OverlappingOutputPathsError,
|
||||
RecursiveAddingWhileUsingFilename,
|
||||
)
|
||||
@@ -29,13 +31,25 @@ Stages = List["Stage"]
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
OVERLAPPING_OUTPUT_FMT = (
|
||||
class StageInfo(NamedTuple):
|
||||
stage: "Stage"
|
||||
output_exists: bool
|
||||
|
||||
|
||||
OVERLAPPING_CHILD_FMT = (
|
||||
"Cannot add '{out}', because it is overlapping with other "
|
||||
"DVC tracked output: '{parent}'.\n"
|
||||
"To include '{out}' in '{parent}', run "
|
||||
"'dvc commit {parent_stage}'"
|
||||
)
|
||||
|
||||
OVERLAPPING_PARENT_FMT = (
|
||||
"Cannot add '{parent}', because it is overlapping with other "
|
||||
"DVC tracked output: '{out}'.\n"
|
||||
"To include '{out}' in '{parent}', run "
|
||||
"'dvc remove {out_stage}' and then 'dvc add {parent}'"
|
||||
)
|
||||
|
||||
|
||||
def check_recursive_and_fname(args):
|
||||
if args.recursive and args.fname:
|
||||
@@ -86,11 +100,18 @@ def translate_graph_error(stages: Stages) -> Iterator[None]:
|
||||
try:
|
||||
yield
|
||||
except OverlappingOutputPathsError as exc:
|
||||
msg = OVERLAPPING_OUTPUT_FMT.format(
|
||||
out=exc.overlapping_out.fs_path,
|
||||
parent=exc.parent.fs_path,
|
||||
parent_stage=exc.parent.stage.addressing,
|
||||
)
|
||||
if exc.parent in [o for s in stages for o in s.outs]:
|
||||
msg = OVERLAPPING_PARENT_FMT.format(
|
||||
out=exc.overlapping_out,
|
||||
parent=exc.parent,
|
||||
out_stage=exc.overlapping_out.stage.addressing,
|
||||
)
|
||||
else:
|
||||
msg = OVERLAPPING_CHILD_FMT.format(
|
||||
out=exc.overlapping_out,
|
||||
parent=exc.parent,
|
||||
parent_stage=exc.parent.stage.addressing,
|
||||
)
|
||||
raise OverlappingOutputPathsError( # noqa: B904
|
||||
exc.parent, exc.overlapping_out, msg
|
||||
)
|
||||
@@ -100,7 +121,7 @@ def translate_graph_error(stages: Stages) -> Iterator[None]:
|
||||
)
|
||||
|
||||
|
||||
def progress_iter(stages: Stages) -> Iterator["Stage"]:
|
||||
def progress_iter(stages: List[StageInfo]) -> Iterator["StageInfo"]:
|
||||
total = len(stages)
|
||||
desc = "Adding..."
|
||||
with ui.progress(stages, total=total, desc=desc, unit="file", leave=True) as pbar:
|
||||
@@ -108,10 +129,10 @@ def progress_iter(stages: Stages) -> Iterator["Stage"]:
|
||||
pbar.bar_format = desc
|
||||
pbar.refresh()
|
||||
|
||||
for stage in pbar:
|
||||
for item in pbar:
|
||||
if total > 1:
|
||||
pbar.set_msg(f"{stage.outs[0]}")
|
||||
yield stage
|
||||
pbar.set_msg(f"{item.stage.outs[0]}")
|
||||
yield item
|
||||
if total == 1: # restore bar format for stats
|
||||
# pylint: disable=no-member
|
||||
pbar.bar_format = pbar.BAR_FMT_DEFAULT
|
||||
@@ -169,12 +190,14 @@ def add(
|
||||
desc = "Collecting targets"
|
||||
stages_it = create_stages(repo, add_targets, fname, transfer, **kwargs)
|
||||
stages = list(ui.progress(stages_it, desc=desc, unit="file"))
|
||||
|
||||
stages_list = [stage for stage, _ in stages]
|
||||
msg = "Collecting stages from the workspace"
|
||||
with translate_graph_error(stages), ui.status(msg) as status:
|
||||
with translate_graph_error(stages_list), ui.status(msg) as status:
|
||||
# remove existing stages that are to-be replaced with these
|
||||
# new stages for the graph checks.
|
||||
repo.check_graph(
|
||||
stages=stages, callback=lambda: status.update("Checking graph")
|
||||
stages=stages_list, callback=lambda: status.update("Checking graph")
|
||||
)
|
||||
|
||||
odb = None
|
||||
@@ -182,18 +205,18 @@ def add(
|
||||
odb = repo.cloud.get_remote_odb(kwargs.get("remote"), "add")
|
||||
|
||||
with warn_link_failures() as link_failures:
|
||||
for stage, source in zip(progress_iter(stages), sources):
|
||||
for (stage, output_exists), source in zip(progress_iter(stages), sources):
|
||||
out = stage.outs[0]
|
||||
if to_remote or to_cache:
|
||||
stage.transfer(source, to_remote=to_remote, odb=odb, **kwargs)
|
||||
else:
|
||||
try:
|
||||
stage.save()
|
||||
if not no_commit:
|
||||
stage.commit()
|
||||
path = out.fs.path.abspath(source) if output_exists else None
|
||||
stage.add_outs(path, no_commit=no_commit)
|
||||
except CacheLinkError:
|
||||
link_failures.append(str(stage.relpath))
|
||||
stage.dump()
|
||||
return stages
|
||||
return stages_list
|
||||
|
||||
|
||||
LARGE_DIR_RECURSIVE_ADD_WARNING = (
|
||||
@@ -247,24 +270,34 @@ def create_stages(
|
||||
fname: Optional[str] = None,
|
||||
transfer: bool = False,
|
||||
external: bool = False,
|
||||
force: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Iterator["Stage"]:
|
||||
) -> Iterator[StageInfo]:
|
||||
for target in targets:
|
||||
if kwargs.get("out"):
|
||||
target = resolve_output(target, kwargs["out"])
|
||||
target = resolve_output(target, kwargs["out"], force=force)
|
||||
path, wdir, out = resolve_paths(
|
||||
repo, target, always_local=transfer and not kwargs.get("out")
|
||||
)
|
||||
|
||||
stage = repo.stage.create(
|
||||
single_stage=True,
|
||||
validate=False,
|
||||
fname=fname or path,
|
||||
wdir=wdir,
|
||||
outs=[out],
|
||||
external=external,
|
||||
)
|
||||
try:
|
||||
(out_obj,) = repo.find_outs_by_path(target, strict=False)
|
||||
stage = out_obj.stage
|
||||
if not stage.is_data_source:
|
||||
raise DvcException(f"cannot update {out!r}: not a data source")
|
||||
output_exists = True
|
||||
except OutputNotFoundError:
|
||||
stage = repo.stage.create(
|
||||
single_stage=True,
|
||||
validate=False,
|
||||
fname=fname or path,
|
||||
wdir=wdir,
|
||||
outs=[out],
|
||||
external=external,
|
||||
force=force,
|
||||
)
|
||||
output_exists = False
|
||||
|
||||
out_obj = stage.outs[0]
|
||||
out_obj.annot.update(**kwargs)
|
||||
yield stage
|
||||
yield StageInfo(stage, output_exists)
|
||||
|
||||
+49
-29
@@ -1,19 +1,26 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dvc.annotations import Artifact
|
||||
from dvc.dvcfile import FileMixin
|
||||
from dvc.dvcfile import PROJECT_FILE
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.repo import Repo
|
||||
from dvc.utils import relpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.utils.serialize import modify_yaml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
NAME_RE = re.compile(r"^[a-z]([a-z0-9-]*[a-z0-9])?$")
|
||||
# Constants are taken from GTO.
|
||||
# When we make it a dependency, we can import them instead
|
||||
SEPARATOR_IN_NAME = ":"
|
||||
DIRNAME = r"[a-z0-9-_./]+"
|
||||
NAME = r"[a-z0-9]([a-z0-9-/]*[a-z0-9])?"
|
||||
NAME_RE = re.compile(f"^{NAME}$")
|
||||
FULLNAME = f"((?P<dirname>{DIRNAME}){SEPARATOR_IN_NAME})?(?P<name>{NAME})"
|
||||
FULLNAME_RE = re.compile(f"^{FULLNAME}$")
|
||||
|
||||
|
||||
def name_is_compatible(name: str) -> bool:
|
||||
@@ -22,22 +29,23 @@ def name_is_compatible(name: str) -> bool:
|
||||
|
||||
def check_name_format(name: str) -> None:
|
||||
if not name_is_compatible(name):
|
||||
logger.warning(
|
||||
"Can't use '%s' as artifact name (ID)."
|
||||
raise InvalidArgumentError(
|
||||
f"Can't use '{name}' as artifact name (ID)."
|
||||
" You can use letters and numbers, and use '-' as separator"
|
||||
" (but not at the start or end). The first character must be a letter.",
|
||||
name,
|
||||
" (but not at the start or end)."
|
||||
)
|
||||
|
||||
|
||||
class ArtifactsFile(FileMixin):
|
||||
from dvc.schema import SINGLE_ARTIFACT_SCHEMA as SCHEMA
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
def check_for_nested_dvc_repo(dvcfile: Path):
|
||||
if dvcfile.is_absolute():
|
||||
raise InvalidArgumentError("Use relative path to dvc.yaml.")
|
||||
path = dvcfile.parent
|
||||
while path.name:
|
||||
if (path / Repo.DVC_DIR).is_dir():
|
||||
raise InvalidArgumentError(
|
||||
f"Nested DVC repos like {path} are not supported."
|
||||
)
|
||||
path = path.parent
|
||||
|
||||
|
||||
class Artifacts:
|
||||
@@ -50,18 +58,30 @@ class Artifacts:
|
||||
dvcfile,
|
||||
dvcfile_artifacts,
|
||||
) in self.repo.index._artifacts.items(): # pylint: disable=protected-access
|
||||
# read the artifacts.yaml file if needed
|
||||
if isinstance(dvcfile_artifacts, str):
|
||||
dvcfile_artifacts = ArtifactsFile(
|
||||
self.repo,
|
||||
os.path.join(os.path.dirname(dvcfile), dvcfile_artifacts),
|
||||
verify=False,
|
||||
).load()
|
||||
if not dvcfile_artifacts:
|
||||
continue
|
||||
dvcyaml = relpath(dvcfile, self.repo.root_dir)
|
||||
artifacts[dvcyaml] = {}
|
||||
for name, value in dvcfile_artifacts.items():
|
||||
check_name_format(name)
|
||||
try:
|
||||
check_name_format(name)
|
||||
except InvalidArgumentError as e:
|
||||
logger.warning(e.msg)
|
||||
artifacts[dvcyaml][name] = Artifact(**value)
|
||||
return artifacts
|
||||
|
||||
def add(self, name: str, artifact: Artifact, dvcfile: Optional[str] = None):
|
||||
with self.repo.scm_context(quiet=True):
|
||||
check_name_format(name)
|
||||
dvcyaml = Path(dvcfile or PROJECT_FILE)
|
||||
check_for_nested_dvc_repo(
|
||||
dvcyaml.relative_to(self.repo.root_dir)
|
||||
if dvcyaml.is_absolute()
|
||||
else dvcyaml
|
||||
)
|
||||
|
||||
with modify_yaml(dvcyaml) as data:
|
||||
artifacts = data.setdefault("artifacts", {})
|
||||
artifacts.update({name: artifact.to_dict()})
|
||||
|
||||
self.repo.scm_context.track_file(dvcfile)
|
||||
|
||||
return artifacts.get(name)
|
||||
|
||||
@@ -172,7 +172,6 @@ def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
repo.index.data["repo"],
|
||||
workspace,
|
||||
not_in_cache=True,
|
||||
not_in_remote=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ class Experiments:
|
||||
self,
|
||||
tmp_dir: bool = False,
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Reproduce and checkout a single (standalone) experiment."""
|
||||
@@ -126,7 +127,9 @@ class Experiments:
|
||||
self.tempdir_queue if tmp_dir else self.workspace_queue
|
||||
)
|
||||
self.queue_one(exp_queue, **kwargs)
|
||||
results = self._reproduce_queue(exp_queue, copy_paths=copy_paths)
|
||||
results = self._reproduce_queue(
|
||||
exp_queue, copy_paths=copy_paths, message=message
|
||||
)
|
||||
exp_rev = first(results)
|
||||
if exp_rev is not None:
|
||||
self._log_reproduced(results, tmp_dir=tmp_dir)
|
||||
@@ -349,7 +352,11 @@ class Experiments:
|
||||
|
||||
@unlocked_repo
|
||||
def _reproduce_queue(
|
||||
self, queue: "BaseStashQueue", copy_paths: Optional[List[str]] = None, **kwargs
|
||||
self,
|
||||
queue: "BaseStashQueue",
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, str]:
|
||||
"""Reproduce queued experiments.
|
||||
|
||||
@@ -360,7 +367,7 @@ class Experiments:
|
||||
dict mapping successfully reproduced experiment revs to their
|
||||
results.
|
||||
"""
|
||||
exec_results = queue.reproduce(copy_paths=copy_paths)
|
||||
exec_results = queue.reproduce(copy_paths=copy_paths, message=message)
|
||||
|
||||
results: Dict[str, str] = {}
|
||||
for _, exp_result in exec_results.items():
|
||||
@@ -442,17 +449,6 @@ class Experiments:
|
||||
result[rev] = name
|
||||
return result
|
||||
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
"""Return info for running experiments."""
|
||||
result = {}
|
||||
for queue in (
|
||||
self.workspace_queue,
|
||||
self.tempdir_queue,
|
||||
self.celery_queue,
|
||||
):
|
||||
result.update(queue.get_running_exps(fetch_refs))
|
||||
return result
|
||||
|
||||
def apply(self, *args, **kwargs):
|
||||
from dvc.repo.experiments.apply import apply
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from typing import TYPE_CHECKING, Iterator, Tuple
|
||||
|
||||
from dvc.repo.experiments.exceptions import InvalidExpRevError
|
||||
from dvc.scm import RevError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
@contextmanager
|
||||
def switch_repo(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
) -> Iterator[Tuple["Repo", str]]:
|
||||
"""Return a repo instance (brancher) switched to rev.
|
||||
|
||||
If rev is the name of a running experiment, the returned instance will be
|
||||
the live repo wherever the experiment is running.
|
||||
|
||||
NOTE: This will not resolve git SHA's that only exist in queued exp workspaces
|
||||
(it will only match queued exp names).
|
||||
"""
|
||||
try:
|
||||
with repo.switch(rev):
|
||||
yield repo, rev
|
||||
return
|
||||
except RevError as exc:
|
||||
orig_exc = exc
|
||||
exps = repo.experiments
|
||||
|
||||
if rev == exps.workspace_queue.get_running_exp():
|
||||
yield repo, "workspace"
|
||||
return
|
||||
|
||||
for queue in (exps.tempdir_queue, exps.celery_queue):
|
||||
try:
|
||||
active_repo = queue.active_repo(rev)
|
||||
except InvalidExpRevError:
|
||||
continue
|
||||
stack = ExitStack()
|
||||
stack.enter_context(active_repo)
|
||||
stack.enter_context(active_repo.switch("workspace"))
|
||||
with stack:
|
||||
yield active_repo, rev
|
||||
return
|
||||
raise orig_exc
|
||||
@@ -0,0 +1,380 @@
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Collection,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import first
|
||||
from scmrepo.exceptions import SCMError as InnerSCMError
|
||||
|
||||
from dvc.scm import Git, SCMError, iter_revs
|
||||
|
||||
from .exceptions import InvalidExpRefError
|
||||
from .refs import EXEC_BRANCH, ExpRefInfo
|
||||
from .serialize import ExpRange, ExpState, SerializableError, SerializableExp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
from .cache import ExpCache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def collect_rev(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
param_deps: bool = False,
|
||||
force: bool = False,
|
||||
cache: Optional["ExpCache"] = None,
|
||||
**kwargs,
|
||||
) -> ExpState:
|
||||
"""Collect experiment state for the given revision.
|
||||
|
||||
Exp will be loaded from cache when available unless rev is 'workspace' or
|
||||
force is set.
|
||||
"""
|
||||
from dvc.fs import LocalFileSystem
|
||||
|
||||
cache = cache or repo.experiments.cache
|
||||
assert cache
|
||||
# TODO: support filtering serialized exp when param_deps is set
|
||||
if rev != "workspace" and not (force or param_deps):
|
||||
cached_exp = cache.get(rev)
|
||||
if cached_exp:
|
||||
if isinstance(cached_exp, SerializableError):
|
||||
return ExpState(rev=rev, error=cached_exp)
|
||||
return ExpState(rev=rev, data=cached_exp)
|
||||
if rev == "workspace" and isinstance(repo.fs, LocalFileSystem):
|
||||
orig_cwd: Optional[str] = os.getcwd()
|
||||
os.chdir(repo.root_dir)
|
||||
else:
|
||||
orig_cwd = None
|
||||
try:
|
||||
data = _collect_rev(
|
||||
repo,
|
||||
rev,
|
||||
param_deps=param_deps,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
if not (rev == "workspace" or param_deps or data.contains_error):
|
||||
cache.put(data, force=True)
|
||||
return ExpState(rev=rev, data=data)
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
logger.debug("", exc_info=True)
|
||||
error = SerializableError(str(exc), type(exc).__name__)
|
||||
return ExpState(rev=rev, error=error)
|
||||
finally:
|
||||
if orig_cwd:
|
||||
os.chdir(orig_cwd)
|
||||
|
||||
|
||||
def _collect_rev(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
param_deps: bool = False,
|
||||
**kwargs,
|
||||
) -> SerializableExp:
|
||||
with repo.switch(rev) as rev:
|
||||
if rev == "workspace":
|
||||
timestamp: Optional[datetime] = None
|
||||
else:
|
||||
commit = repo.scm.resolve_commit(rev)
|
||||
timestamp = datetime.fromtimestamp(commit.commit_time)
|
||||
|
||||
return SerializableExp.from_repo(
|
||||
repo,
|
||||
rev=rev,
|
||||
param_deps=param_deps,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
|
||||
def collect_branch(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
end_rev: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Iterator["ExpState"]:
|
||||
"""Iterate over exp states in a Git branch.
|
||||
|
||||
Git branch will be traversed in reverse, starting from rev.
|
||||
|
||||
Args:
|
||||
rev: Branch tip (head).
|
||||
end_rev: If specified, traversal will stop when end_rev is reached
|
||||
(exclusive, end_rev will not be collected).
|
||||
"""
|
||||
try:
|
||||
for branch_rev in repo.scm.branch_revs(rev, end_rev):
|
||||
yield collect_rev(repo, branch_rev, **kwargs)
|
||||
except (SCMError, InnerSCMError):
|
||||
pass
|
||||
|
||||
|
||||
def collect_exec_branch(
|
||||
repo: "Repo",
|
||||
baseline_rev: str,
|
||||
**kwargs,
|
||||
) -> Iterator["ExpState"]:
|
||||
"""Iterate over active experiment branch for the current executor."""
|
||||
last_rev = repo.scm.get_ref(EXEC_BRANCH) or repo.scm.get_rev()
|
||||
last_rev = repo.scm.get_rev()
|
||||
yield collect_rev(repo, "workspace", **kwargs)
|
||||
if last_rev != baseline_rev:
|
||||
yield from collect_branch(repo, last_rev, baseline_rev, **kwargs)
|
||||
|
||||
|
||||
def collect_queued(
|
||||
repo: "Repo",
|
||||
baseline_revs: Collection[str],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect queued experiments derived from the specified revisions.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of queued experiments.
|
||||
"""
|
||||
if not baseline_revs:
|
||||
return {}
|
||||
return repo.experiments.celery_queue.collect_queued_data(baseline_revs, **kwargs)
|
||||
|
||||
|
||||
def collect_active(
|
||||
repo: "Repo",
|
||||
baseline_revs: Collection[str],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect active (running) experiments derived from the specified revisions.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of active experiments.
|
||||
"""
|
||||
if not baseline_revs:
|
||||
return {}
|
||||
result: Dict[str, List["ExpRange"]] = {}
|
||||
exps = repo.experiments
|
||||
for queue in (exps.workspace_queue, exps.tempdir_queue, exps.celery_queue):
|
||||
for baseline, active_exps in queue.collect_active_data(
|
||||
baseline_revs, **kwargs
|
||||
).items():
|
||||
if baseline in result:
|
||||
result[baseline].extend(active_exps)
|
||||
else:
|
||||
result[baseline] = list(active_exps)
|
||||
return result
|
||||
|
||||
|
||||
def collect_failed(
|
||||
repo: "Repo",
|
||||
baseline_revs: Collection[str],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect failed experiments derived from the specified revisions.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of active experiments.
|
||||
"""
|
||||
if not baseline_revs:
|
||||
return {}
|
||||
return repo.experiments.celery_queue.collect_failed_data(baseline_revs, **kwargs)
|
||||
|
||||
|
||||
def collect_successful(
|
||||
repo: "Repo",
|
||||
baseline_revs: Collection[str],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect successful experiments derived from the specified revisions.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to successful experiments.
|
||||
"""
|
||||
result: Dict[str, List["ExpRange"]] = {}
|
||||
for baseline_rev in baseline_revs:
|
||||
result[baseline_rev] = list(_collect_baseline(repo, baseline_rev, **kwargs))
|
||||
return result
|
||||
|
||||
|
||||
def _collect_baseline(
|
||||
repo: "Repo",
|
||||
baseline_rev: str,
|
||||
**kwargs,
|
||||
) -> Iterator["ExpRange"]:
|
||||
"""Iterate over experiments derived from a baseline revision.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Yields:
|
||||
Tuple of (timestamp, exp_range).
|
||||
"""
|
||||
ref_info = ExpRefInfo(baseline_sha=baseline_rev)
|
||||
refs: Optional[Iterable[str]] = kwargs.get("refs")
|
||||
if refs:
|
||||
ref_it = (ref for ref in iter(refs) if ref.startswith(str(ref_info)))
|
||||
else:
|
||||
ref_it = repo.scm.iter_refs(base=str(ref_info))
|
||||
for ref in ref_it:
|
||||
try:
|
||||
ref_info = ExpRefInfo.from_ref(ref)
|
||||
exp_rev = repo.scm.get_ref(ref)
|
||||
if not exp_rev:
|
||||
continue
|
||||
except (InvalidExpRefError, SCMError, InnerSCMError):
|
||||
continue
|
||||
exps = list(collect_branch(repo, exp_rev, baseline_rev, **kwargs))
|
||||
if exps:
|
||||
exps[0].name = ref_info.name
|
||||
yield ExpRange(exps, name=ref_info.name)
|
||||
|
||||
|
||||
def collect(
|
||||
repo: "Repo",
|
||||
revs: Union[List[str], str, None] = None,
|
||||
all_branches: bool = False,
|
||||
all_tags: bool = False,
|
||||
all_commits: bool = False,
|
||||
num: int = 1,
|
||||
hide_queued: bool = False,
|
||||
hide_failed: bool = False,
|
||||
sha_only: bool = False,
|
||||
**kwargs,
|
||||
) -> List["ExpState"]:
|
||||
"""Collect baseline revisions and derived experiments."""
|
||||
assert isinstance(repo.scm, Git)
|
||||
if repo.scm.no_commits:
|
||||
return []
|
||||
if not any([revs, all_branches, all_tags, all_commits]):
|
||||
revs = ["HEAD"]
|
||||
if isinstance(revs, str):
|
||||
revs = [revs]
|
||||
cached_refs = list(repo.scm.iter_refs())
|
||||
baseline_revs = list(
|
||||
iter_revs(
|
||||
repo.scm,
|
||||
revs=revs,
|
||||
num=num,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
)
|
||||
)
|
||||
if sha_only:
|
||||
baseline_names: Dict[str, Optional[str]] = {}
|
||||
else:
|
||||
baseline_names = _describe(repo.scm, baseline_revs, refs=cached_refs)
|
||||
|
||||
workspace_data = collect_rev(repo, "workspace", **kwargs)
|
||||
result: List["ExpState"] = [workspace_data]
|
||||
queued = collect_queued(repo, baseline_revs, **kwargs) if not hide_queued else {}
|
||||
active = collect_active(repo, baseline_revs, **kwargs)
|
||||
failed = collect_failed(repo, baseline_revs, **kwargs) if not hide_failed else {}
|
||||
successful = collect_successful(repo, baseline_revs, **kwargs)
|
||||
|
||||
for baseline_rev in baseline_revs:
|
||||
baseline_data = collect_rev(repo, baseline_rev)
|
||||
experiments = list(
|
||||
itertools.chain.from_iterable(
|
||||
_sorted_ranges(collected.get(baseline_rev, []))
|
||||
for collected in (active, successful, queued, failed)
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
ExpState(
|
||||
rev=baseline_rev,
|
||||
name=baseline_names.get(baseline_rev),
|
||||
data=baseline_data.data,
|
||||
error=baseline_data.error,
|
||||
experiments=experiments if experiments else None,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _describe(
|
||||
scm: "Git",
|
||||
revs: Iterable[str],
|
||||
refs: Optional[Iterable[str]] = None,
|
||||
) -> Dict[str, Optional[str]]:
|
||||
"""Describe revisions using a tag, branch.
|
||||
|
||||
The first matching name will be returned for each rev. Names are preferred in this
|
||||
order:
|
||||
- current branch (if rev matches HEAD and HEAD is a branch)
|
||||
- tags
|
||||
- branches
|
||||
|
||||
Returns:
|
||||
Dict mapping revisions from revs to a name.
|
||||
"""
|
||||
|
||||
head_rev = scm.get_rev()
|
||||
head_ref = scm.get_ref("HEAD", follow=False)
|
||||
if head_ref and head_ref.startswith("refs/heads/"):
|
||||
head_branch = head_ref[len("refs/heads/") :]
|
||||
else:
|
||||
head_branch = None
|
||||
|
||||
tags = {}
|
||||
branches = {}
|
||||
ref_it = iter(refs) if refs else scm.iter_refs()
|
||||
for ref in ref_it:
|
||||
is_tag = ref.startswith("refs/tags/")
|
||||
is_branch = ref.startswith("refs/heads/")
|
||||
if not (is_tag or is_branch):
|
||||
continue
|
||||
rev = scm.get_ref(ref)
|
||||
if not rev:
|
||||
logger.debug("unresolved ref %s", ref)
|
||||
continue
|
||||
if is_tag and rev not in tags:
|
||||
tags[rev] = ref[len("refs/tags/") :]
|
||||
if is_branch and rev not in branches:
|
||||
branches[rev] = ref[len("refs/heads/") :]
|
||||
names: Dict[str, Optional[str]] = {}
|
||||
for rev in revs:
|
||||
if rev == head_rev and head_branch:
|
||||
names[rev] = head_branch
|
||||
else:
|
||||
names[rev] = tags.get(rev) or branches.get(rev)
|
||||
return names
|
||||
|
||||
|
||||
def _sorted_ranges(exp_ranges: Iterable["ExpRange"]) -> List["ExpRange"]:
|
||||
"""Return list of ExpRange sorted by timestamp."""
|
||||
|
||||
def _head_timestamp(exp_range: "ExpRange") -> datetime:
|
||||
head_exp = first(exp_range.revs)
|
||||
if head_exp and head_exp.data and head_exp.data.timestamp:
|
||||
return head_exp.data.timestamp
|
||||
return datetime.fromtimestamp(0)
|
||||
|
||||
return sorted(exp_ranges, key=_head_timestamp, reverse=True)
|
||||
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
from dvc.repo.experiments.show import collect_experiment_commit
|
||||
from dvc.repo.experiments.collect import collect_rev
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
if repo.scm.no_commits:
|
||||
@@ -15,22 +15,21 @@ def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
|
||||
if a_rev:
|
||||
rev = resolve_rev(repo.scm, a_rev)
|
||||
old = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
old = collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
|
||||
rev = resolve_rev(repo.scm, "HEAD")
|
||||
old = collect_rev(repo, rev, param_deps=param_deps)
|
||||
|
||||
if b_rev:
|
||||
rev = resolve_rev(repo.scm, b_rev)
|
||||
new = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
new = collect_experiment_commit(repo, "workspace", param_deps=param_deps)
|
||||
rev = "workspace"
|
||||
new = collect_rev(repo, rev, param_deps=param_deps)
|
||||
|
||||
with_unchanged = kwargs.pop("all", False)
|
||||
|
||||
return {
|
||||
key: _diff(
|
||||
format_dict(old.get("data", {}).get(key, {})),
|
||||
format_dict(new.get("data", {}).get(key, {})),
|
||||
format_dict(getattr(old.data, key, {})),
|
||||
format_dict(getattr(new.data, key, {})),
|
||||
with_unchanged=with_unchanged,
|
||||
)
|
||||
for key in ["metrics", "params"]
|
||||
|
||||
@@ -114,3 +114,10 @@ class UnresolvedRunningExpNamesError(UnresolvedExpNamesError):
|
||||
|
||||
class ExpQueueEmptyError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class ExpNotStartedError(DvcException):
|
||||
def __init__(self, name: str):
|
||||
super().__init__(
|
||||
f"Queued experiment '{name}' exists but has not started running yet"
|
||||
)
|
||||
|
||||
@@ -7,12 +7,14 @@ from contextlib import contextmanager
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import IntEnum
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
@@ -41,6 +43,7 @@ from dvc.stage.serialize import to_lockfile
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import dict_sha256, env2bool, relpath
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.studio import env_to_config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from queue import Queue
|
||||
@@ -259,12 +262,23 @@ class BaseExecutor(ABC):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_top_level_paths(cls, repo: "Repo") -> List["str"]:
|
||||
return list(
|
||||
chain(
|
||||
_collect_top_level_metrics(repo),
|
||||
_collect_top_level_params(repo),
|
||||
repo.index._plot_sources, # pylint: disable=protected-access
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def save(
|
||||
cls,
|
||||
info: "ExecutorInfo",
|
||||
force: bool = False,
|
||||
include_untracked: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
) -> ExecutorResult:
|
||||
from dvc.dvcfile import LOCK_FILE
|
||||
from dvc.repo import Repo
|
||||
@@ -280,11 +294,7 @@ class BaseExecutor(ABC):
|
||||
os.chdir(dvc.root_dir)
|
||||
|
||||
include_untracked = include_untracked or []
|
||||
include_untracked.extend(_collect_top_level_metrics(dvc))
|
||||
include_untracked.extend(_collect_top_level_params(dvc))
|
||||
include_untracked.extend(
|
||||
dvc.index._plot_sources # pylint: disable=protected-access
|
||||
)
|
||||
include_untracked.extend(cls._get_top_level_paths(dvc))
|
||||
# dvc repro automatically stages dvc.lock. Running redundant `git add`
|
||||
# on it causes an error when exiting the detached head context.
|
||||
if LOCK_FILE in dvc.scm.untracked_files():
|
||||
@@ -300,6 +310,7 @@ class BaseExecutor(ABC):
|
||||
exp_hash,
|
||||
exp_name=info.name,
|
||||
force=force,
|
||||
message=message,
|
||||
)
|
||||
ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
|
||||
exp_ref = ExpRefInfo.from_ref(ref) if ref else None
|
||||
@@ -453,6 +464,7 @@ class BaseExecutor(ABC):
|
||||
log_errors: bool = True,
|
||||
log_level: Optional[int] = None,
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> "ExecutorResult":
|
||||
"""Run dvc repro and return the result.
|
||||
@@ -485,11 +497,15 @@ class BaseExecutor(ABC):
|
||||
exp_ref: Optional["ExpRefInfo"] = None
|
||||
repro_force: bool = False
|
||||
|
||||
if info.name:
|
||||
ui.write(f"Reproducing experiment '{info.name}'")
|
||||
|
||||
with cls._repro_dvc(
|
||||
info,
|
||||
infofile,
|
||||
log_errors=log_errors,
|
||||
copy_paths=copy_paths,
|
||||
message=message,
|
||||
**kwargs,
|
||||
) as dvc:
|
||||
if auto_push:
|
||||
@@ -540,6 +556,7 @@ class BaseExecutor(ABC):
|
||||
info.name,
|
||||
repro_force or checkpoint_reset,
|
||||
)
|
||||
|
||||
stages = dvc_reproduce(
|
||||
dvc,
|
||||
*args,
|
||||
@@ -547,6 +564,9 @@ class BaseExecutor(ABC):
|
||||
checkpoint_func=checkpoint_func,
|
||||
**kwargs,
|
||||
)
|
||||
if paths := cls._get_top_level_paths(dvc):
|
||||
logger.debug("Staging top-level files: %s", paths)
|
||||
dvc.scm_context.add(paths)
|
||||
|
||||
exp_hash = cls.hash_exp(stages)
|
||||
if not repro_dry:
|
||||
@@ -558,6 +578,7 @@ class BaseExecutor(ABC):
|
||||
auto_push,
|
||||
git_remote,
|
||||
repro_force,
|
||||
message=message,
|
||||
)
|
||||
info.result_hash = exp_hash
|
||||
info.result_ref = ref
|
||||
@@ -578,6 +599,7 @@ class BaseExecutor(ABC):
|
||||
auto_push,
|
||||
git_remote,
|
||||
repro_force,
|
||||
message: Optional[str] = None,
|
||||
) -> Tuple[Optional[str], Optional["ExpRefInfo"], bool]:
|
||||
is_checkpoint = any(stage.is_checkpoint for stage in stages)
|
||||
cls.commit(
|
||||
@@ -586,6 +608,7 @@ class BaseExecutor(ABC):
|
||||
exp_name=info.name,
|
||||
force=repro_force,
|
||||
checkpoint=is_checkpoint,
|
||||
message=message,
|
||||
)
|
||||
if auto_push:
|
||||
cls._auto_push(dvc, dvc.scm, git_remote)
|
||||
@@ -613,8 +636,9 @@ class BaseExecutor(ABC):
|
||||
infofile: Optional[str] = None,
|
||||
log_errors: bool = True,
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
) -> Iterator["Repo"]:
|
||||
from dvc_studio_client.post_live_metrics import post_live_metrics
|
||||
|
||||
from dvc.repo import Repo
|
||||
@@ -636,13 +660,23 @@ class BaseExecutor(ABC):
|
||||
else:
|
||||
os.chdir(dvc.root_dir)
|
||||
|
||||
args_path = os.path.join(dvc.tmp_dir, cls.PACKED_ARGS_FILE)
|
||||
if os.path.exists(args_path):
|
||||
_, kwargs = cls.unpack_repro_args(args_path)
|
||||
dvc_studio_config = dvc.config.get("studio")
|
||||
# set missing config options using saved config
|
||||
# inferring repo url will fail if not set here
|
||||
run_env_config = env_to_config(kwargs.get("run_env", {}))
|
||||
dvc_studio_config = {**run_env_config, **dvc_studio_config}
|
||||
try:
|
||||
post_live_metrics(
|
||||
"start",
|
||||
info.baseline_rev,
|
||||
info.name,
|
||||
info.name, # type: ignore[arg-type]
|
||||
"dvc",
|
||||
params=to_studio_params(dvc.params.show()),
|
||||
dvc_studio_config=dvc_studio_config,
|
||||
message=message,
|
||||
)
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
@@ -664,10 +698,11 @@ class BaseExecutor(ABC):
|
||||
post_live_metrics(
|
||||
"done",
|
||||
info.baseline_rev,
|
||||
info.name,
|
||||
info.name, # type: ignore[arg-type]
|
||||
"dvc",
|
||||
experiment_rev=dvc.experiments.scm.get_ref(EXEC_BRANCH),
|
||||
metrics=get_in(dvc.metrics.show(), ["", "data"]),
|
||||
dvc_studio_config=dvc_studio_config,
|
||||
)
|
||||
|
||||
if infofile is not None:
|
||||
@@ -681,7 +716,7 @@ class BaseExecutor(ABC):
|
||||
args, kwargs = cls.unpack_repro_args(args_path)
|
||||
remove(args_path)
|
||||
# explicitly git rm/unstage the args file
|
||||
dvc.scm.add([args_path])
|
||||
dvc.scm.add([args_path], force=True)
|
||||
else:
|
||||
args = []
|
||||
kwargs = {}
|
||||
@@ -739,6 +774,7 @@ class BaseExecutor(ABC):
|
||||
exp_name: Optional[str] = None,
|
||||
force: bool = False,
|
||||
checkpoint: bool = False,
|
||||
message: Optional[str] = None,
|
||||
):
|
||||
"""Commit stages as an experiment and return the commit SHA."""
|
||||
rev = scm.get_rev()
|
||||
@@ -768,7 +804,8 @@ class BaseExecutor(ABC):
|
||||
logger.debug("Commit to new experiment branch '%s'", branch)
|
||||
|
||||
scm.add([], update=True)
|
||||
scm.commit(f"dvc: commit experiment {exp_hash}", no_verify=True)
|
||||
message = message or f"dvc: commit experiment {exp_hash}"
|
||||
scm.commit(message, no_verify=True)
|
||||
new_rev = scm.get_rev()
|
||||
if check_conflict:
|
||||
new_rev = cls._raise_ref_conflict(scm, branch, new_rev, checkpoint)
|
||||
|
||||
@@ -17,7 +17,7 @@ from dvc.repo.experiments.refs import (
|
||||
EXEC_HEAD,
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_TEMP,
|
||||
TEMP_NAMESPACE,
|
||||
)
|
||||
from dvc.repo.experiments.utils import EXEC_TMP_DIR, get_exp_rwlock
|
||||
from dvc.scm import SCM, Git
|
||||
@@ -90,9 +90,9 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
if infofile:
|
||||
self.info.dump_json(infofile)
|
||||
|
||||
temp_head = f"{EXPS_TEMP}/head-{uuid()}"
|
||||
temp_merge = f"{EXPS_TEMP}/merge-{uuid()}"
|
||||
temp_baseline = f"{EXPS_TEMP}/baseline-{uuid()}"
|
||||
temp_head = f"{TEMP_NAMESPACE}/head-{uuid()}"
|
||||
temp_merge = f"{TEMP_NAMESPACE}/merge-{uuid()}"
|
||||
temp_baseline = f"{TEMP_NAMESPACE}/baseline-{uuid()}"
|
||||
|
||||
temp_ref_dict = {
|
||||
temp_head: entry.head_rev,
|
||||
|
||||
@@ -243,6 +243,7 @@ class SSHExecutor(BaseExecutor):
|
||||
log_errors: bool = True,
|
||||
log_level: Optional[int] = None,
|
||||
copy_paths: Optional[List[str]] = None, # noqa: ARG003
|
||||
message: Optional[str] = None, # noqa: ARG003
|
||||
**kwargs,
|
||||
) -> "ExecutorResult":
|
||||
"""Reproduce an experiment on a remote machine over SSH.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import Optional
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
@@ -15,14 +15,17 @@ logger = logging.getLogger(__name__)
|
||||
@scm_context
|
||||
def ls(
|
||||
repo,
|
||||
rev: Optional[str] = None,
|
||||
rev: Optional[Union[List[str], str]] = None,
|
||||
all_commits: bool = False,
|
||||
num: int = 1,
|
||||
git_remote: Optional[str] = None,
|
||||
):
|
||||
rev_set = None
|
||||
if not all_commits:
|
||||
revs = iter_revs(repo.scm, [rev or "HEAD"], num)
|
||||
rev = rev or "HEAD"
|
||||
if isinstance(rev, str):
|
||||
rev = [rev]
|
||||
revs = iter_revs(repo.scm, rev, num)
|
||||
rev_set = set(revs.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ def pull( # noqa: C901
|
||||
git_remote: str,
|
||||
exp_names: Union[Iterable[str], str],
|
||||
all_commits=False,
|
||||
rev: Optional[str] = None,
|
||||
rev: Optional[Union[List[str], str]] = None,
|
||||
num=1,
|
||||
force: bool = False,
|
||||
pull_cache: bool = False,
|
||||
@@ -49,7 +49,9 @@ def pull( # noqa: C901
|
||||
raise UnresolvedExpNamesError(unresolved_exp_names)
|
||||
|
||||
if rev:
|
||||
rev_dict = iter_revs(repo.scm, [rev], num)
|
||||
if isinstance(rev, str):
|
||||
rev = [rev]
|
||||
rev_dict = iter_revs(repo.scm, rev, num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
|
||||
@@ -14,11 +14,13 @@ from typing import (
|
||||
from funcy import compact, group_by
|
||||
from scmrepo.git.backend.base import SyncStatus
|
||||
|
||||
from dvc.env import DVC_STUDIO_TOKEN, DVC_STUDIO_URL
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import Git, TqdmGit, iter_revs
|
||||
from dvc.utils import env2bool
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
from .exceptions import UnresolvedExpNamesError
|
||||
from .refs import ExpRefInfo
|
||||
@@ -39,21 +41,18 @@ class UploadError(DvcException):
|
||||
def notify_refs_to_studio(
|
||||
repo: "Repo", git_remote: str, **refs: List[str]
|
||||
) -> Optional[str]:
|
||||
config = repo.config["feature"]
|
||||
import os
|
||||
|
||||
config = repo.config["studio"]
|
||||
refs = compact(refs)
|
||||
if not refs or env2bool("DVC_TEST"):
|
||||
return None
|
||||
|
||||
if not (config.get("studio_token") or config["push_exp_to_studio"]):
|
||||
logger.debug(
|
||||
"Either feature.studio_token or feature.push_exp_to_studio config "
|
||||
"needs to be set."
|
||||
)
|
||||
return None
|
||||
|
||||
import os
|
||||
|
||||
token = os.environ.get("STUDIO_TOKEN") or config.get("studio_token")
|
||||
token = (
|
||||
os.environ.get(DVC_STUDIO_TOKEN)
|
||||
or os.environ.get("STUDIO_TOKEN")
|
||||
or config.get("token")
|
||||
)
|
||||
if not token:
|
||||
logger.debug("Studio token not found.")
|
||||
return None
|
||||
@@ -63,19 +62,44 @@ def notify_refs_to_studio(
|
||||
from dvc.utils import studio
|
||||
|
||||
_, repo_url = get_remote_repo(repo.scm.dulwich.repo, git_remote)
|
||||
studio_url = config.get("studio_url")
|
||||
studio_url = os.environ.get(DVC_STUDIO_URL) or config.get("url")
|
||||
d = studio.notify_refs(repo_url, token, base_url=studio_url, **refs)
|
||||
return d.get("url")
|
||||
|
||||
|
||||
def exp_refs_from_names(scm: "Git", exp_names: List[str]) -> Set["ExpRefInfo"]:
|
||||
exp_ref_set = set()
|
||||
exp_ref_dict = resolve_name(scm, exp_names)
|
||||
unresolved_exp_names = []
|
||||
for exp_name, exp_ref in exp_ref_dict.items():
|
||||
if exp_ref is None:
|
||||
unresolved_exp_names.append(exp_name)
|
||||
else:
|
||||
exp_ref_set.add(exp_ref)
|
||||
|
||||
if unresolved_exp_names:
|
||||
raise UnresolvedExpNamesError(unresolved_exp_names)
|
||||
return exp_ref_set
|
||||
|
||||
|
||||
def exp_refs_from_rev(scm: "Git", rev: List[str], num: int = 1) -> Set["ExpRefInfo"]:
|
||||
exp_ref_set = set()
|
||||
rev_dict = iter_revs(scm, rev, num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(scm, rev_set)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
exp_ref_set.update(ref_info_list)
|
||||
return exp_ref_set
|
||||
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def push( # noqa: C901
|
||||
def push(
|
||||
repo: "Repo",
|
||||
git_remote: str,
|
||||
exp_names: Union[Iterable[str], str],
|
||||
exp_names: Union[List[str], str],
|
||||
all_commits: bool = False,
|
||||
rev: Optional[str] = None,
|
||||
rev: Optional[Union[List[str], str]] = None,
|
||||
num: int = 1,
|
||||
force: bool = False,
|
||||
push_cache: bool = False,
|
||||
@@ -85,29 +109,12 @@ def push( # noqa: C901
|
||||
assert isinstance(repo.scm, Git)
|
||||
if all_commits:
|
||||
exp_ref_set.update(exp_refs(repo.scm))
|
||||
|
||||
else:
|
||||
if exp_names:
|
||||
if isinstance(exp_names, str):
|
||||
exp_names = [exp_names]
|
||||
exp_ref_dict = resolve_name(repo.scm, exp_names)
|
||||
|
||||
unresolved_exp_names = []
|
||||
for exp_name, exp_ref in exp_ref_dict.items():
|
||||
if exp_ref is None:
|
||||
unresolved_exp_names.append(exp_name)
|
||||
else:
|
||||
exp_ref_set.add(exp_ref)
|
||||
|
||||
if unresolved_exp_names:
|
||||
raise UnresolvedExpNamesError(unresolved_exp_names)
|
||||
|
||||
if rev:
|
||||
rev_dict = iter_revs(repo.scm, [rev], num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
exp_ref_set.update(ref_info_list)
|
||||
if exp_names:
|
||||
exp_ref_set.update(exp_refs_from_names(repo.scm, ensure_list(exp_names)))
|
||||
if rev:
|
||||
if isinstance(rev, str):
|
||||
rev = [rev]
|
||||
exp_ref_set.update(exp_refs_from_rev(repo.scm, rev, num=num))
|
||||
|
||||
push_result = _push(repo, git_remote, exp_ref_set, force)
|
||||
|
||||
@@ -120,15 +127,20 @@ def push( # noqa: C901
|
||||
pushed_refs_info = (
|
||||
push_result[SyncStatus.UP_TO_DATE] + push_result[SyncStatus.SUCCESS]
|
||||
)
|
||||
|
||||
e = None
|
||||
if push_cache:
|
||||
try:
|
||||
result["uploaded"] = _push_cache(repo, pushed_refs_info, **kwargs)
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
raise UploadError("failed to push cache", result) from exc
|
||||
e = exc
|
||||
|
||||
pushed_refs = [str(r) for r in pushed_refs_info]
|
||||
url = notify_refs_to_studio(repo, git_remote, pushed=pushed_refs)
|
||||
return {**result, "url": url}
|
||||
result["url"] = notify_refs_to_studio(repo, git_remote, pushed=pushed_refs)
|
||||
|
||||
if e:
|
||||
raise UploadError("failed to push cache", result) from e
|
||||
return result
|
||||
|
||||
|
||||
def _push(
|
||||
|
||||
@@ -17,8 +17,7 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc_studio_client.env import STUDIO_REPO_URL
|
||||
from dvc_studio_client.post_live_metrics import get_studio_repo_url
|
||||
from dvc_studio_client.post_live_metrics import get_studio_config
|
||||
from funcy import retry
|
||||
|
||||
from dvc.dependency import ParamsDependency
|
||||
@@ -39,6 +38,7 @@ from dvc.repo.experiments.utils import (
|
||||
)
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc.utils.studio import config_to_env
|
||||
|
||||
from .utils import get_remote_executor_refs
|
||||
|
||||
@@ -46,6 +46,7 @@ if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments import Experiments
|
||||
from dvc.repo.experiments.executor.base import ExecutorResult
|
||||
from dvc.repo.experiments.serialize import ExpRange
|
||||
from dvc.scm import Git
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -253,7 +254,7 @@ class BaseStashQueue(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def reproduce(
|
||||
self, copy_paths: Optional[List[str]] = None
|
||||
self, copy_paths: Optional[List[str]] = None, message: Optional[str] = None
|
||||
) -> Mapping[str, Mapping[str, str]]:
|
||||
"""Reproduce queued experiments sequentially."""
|
||||
|
||||
@@ -419,12 +420,13 @@ class BaseStashQueue(ABC):
|
||||
run_env[DVC_EXP_NAME] = name
|
||||
if resume_rev:
|
||||
run_env[DVCLIVE_RESUME] = "1"
|
||||
studio_repo_url = get_studio_repo_url()
|
||||
if studio_repo_url is not None:
|
||||
run_env[STUDIO_REPO_URL] = studio_repo_url
|
||||
|
||||
# save studio config to read later by dvc and dvclive
|
||||
studio_config = get_studio_config(
|
||||
dvc_studio_config=self.repo.config.get("studio")
|
||||
)
|
||||
run_env = {**config_to_env(studio_config), **run_env}
|
||||
self._pack_args(*args, run_env=run_env, **kwargs)
|
||||
|
||||
# save experiment as a stash commit
|
||||
msg = self._stash_msg(
|
||||
stash_head,
|
||||
@@ -520,7 +522,7 @@ class BaseStashQueue(ABC):
|
||||
else:
|
||||
extra = None
|
||||
BaseExecutor.pack_repro_args(self.args_file, *args, extra=extra, **kwargs)
|
||||
self.scm.add(self.args_file)
|
||||
self.scm.add(self.args_file, force=True)
|
||||
|
||||
@staticmethod
|
||||
def _format_new_params_msg(new_params, config_path):
|
||||
@@ -728,9 +730,78 @@ class BaseStashQueue(ABC):
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
"""Get the execution info of the currently running experiments
|
||||
def collect_active_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
fetch_refs: bool = False,
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect data for active (running) experiments.
|
||||
|
||||
Args:
|
||||
fetch_ref (bool): fetch completed checkpoints or not.
|
||||
baseline_revs: Optional resolved baseline Git SHAs. If set, only experiments
|
||||
derived from the specified revisions will be collected. Defaults to
|
||||
collecting all experiments.
|
||||
fetch_refs: Whether or not to fetch completed checkpoint commits from Git
|
||||
remote.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of active experiments.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def collect_queued_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect data for queued experiments.
|
||||
|
||||
Args:
|
||||
baseline_revs: Optional resolved baseline Git SHAs. If set, only experiments
|
||||
derived from the specified revisions will be collected. Defaults to
|
||||
collecting all experiments.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of queued experiments.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def collect_failed_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect data for failed experiments.
|
||||
|
||||
Args:
|
||||
baseline_revs: Optional resolved baseline Git SHAs. If set, only experiments
|
||||
derived from the specified revisions will be collected. Defaults to
|
||||
collecting all experiments.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of queued experiments.
|
||||
"""
|
||||
|
||||
def active_repo(self, name: str) -> "Repo":
|
||||
"""Return a Repo for the specified active experiment if it exists."""
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments.exceptions import (
|
||||
ExpNotStartedError,
|
||||
InvalidExpRevError,
|
||||
)
|
||||
from dvc.repo.experiments.executor.base import ExecutorInfo, TaskStatus
|
||||
|
||||
for entry in self.iter_active():
|
||||
if entry.name != name:
|
||||
continue
|
||||
infofile = self.get_infofile_path(entry.stash_rev)
|
||||
executor_info = ExecutorInfo.load_json(infofile)
|
||||
if executor_info.status < TaskStatus.RUNNING:
|
||||
raise ExpNotStartedError(name)
|
||||
dvc_root = os.path.join(executor_info.root_dir, executor_info.dvc_dir)
|
||||
try:
|
||||
return Repo(dvc_root)
|
||||
except (FileNotFoundError, DvcException) as exc:
|
||||
raise InvalidExpRevError(name) from exc
|
||||
raise InvalidExpRevError(name)
|
||||
|
||||
@@ -2,6 +2,7 @@ import hashlib
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Collection,
|
||||
@@ -40,6 +41,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from dvc.repo.experiments.executor.base import ExecutorResult
|
||||
from dvc.repo.experiments.refs import ExpRefInfo
|
||||
from dvc.repo.experiments.serialize import ExpRange
|
||||
from dvc_task.app import FSApp
|
||||
from dvc_task.proc.manager import ProcessManager
|
||||
from dvc_task.worker import TemporaryWorker
|
||||
@@ -123,6 +125,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
without_mingle=True,
|
||||
without_gossip=True,
|
||||
timeout=10,
|
||||
loglevel="debug" if logger.getEffectiveLevel() <= logging.DEBUG else "info",
|
||||
)
|
||||
|
||||
def _spawn_worker(self, num: int = 1):
|
||||
@@ -141,6 +144,8 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
if num == 1:
|
||||
# automatically run celery cleanup when primary worker shuts down
|
||||
cmd.append("--clean")
|
||||
if logger.getEffectiveLevel() <= logging.DEBUG:
|
||||
cmd.append("-v")
|
||||
name = f"dvc-exp-worker-{num}"
|
||||
|
||||
logger.debug("start a new worker: %s, node: %s", name, node_name)
|
||||
@@ -175,12 +180,18 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
return started
|
||||
|
||||
def put(
|
||||
self, *args, copy_paths: Optional[List[str]] = None, **kwargs
|
||||
self,
|
||||
*args,
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> QueueEntry:
|
||||
"""Stash an experiment and add it to the queue."""
|
||||
with get_exp_rwlock(self.repo, writes=["workspace", CELERY_STASH]):
|
||||
entry = self._stash_exp(*args, **kwargs)
|
||||
self.celery.signature(run_exp.s(entry.asdict(), copy_paths=copy_paths)).delay()
|
||||
self.celery.signature(
|
||||
run_exp.s(entry.asdict(), copy_paths=copy_paths, message=message)
|
||||
).delay()
|
||||
return entry
|
||||
|
||||
# NOTE: Queue consumption should not be done directly. Celery worker(s)
|
||||
@@ -198,7 +209,9 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
continue
|
||||
args, kwargs, _embed = msg.decode()
|
||||
entry_dict = kwargs.get("entry_dict", args[0])
|
||||
logger.debug("Found queued task %s", entry_dict["stash_rev"])
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"Found queued task %s", entry_dict["stash_rev"]
|
||||
)
|
||||
yield _MessageEntry(msg, QueueEntry.from_dict(entry_dict))
|
||||
|
||||
def _iter_processed(self) -> Generator[_MessageEntry, None, None]:
|
||||
@@ -214,7 +227,9 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug("Found active task %s", entry.stash_rev)
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"Found active task %s", entry.stash_rev
|
||||
)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def _iter_done_tasks(self) -> Generator[_TaskEntry, None, None]:
|
||||
@@ -222,7 +237,9 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if result.ready():
|
||||
logger.debug("Found done task %s", entry.stash_rev)
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"Found done task %s", entry.stash_rev
|
||||
)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def iter_active(self) -> Generator[QueueEntry, None, None]:
|
||||
@@ -253,7 +270,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
yield QueueDoneResult(queue_entry, exp_result)
|
||||
|
||||
def reproduce(
|
||||
self, copy_paths: Optional[List[str]] = None
|
||||
self, copy_paths: Optional[List[str]] = None, message: Optional[str] = None
|
||||
) -> Mapping[str, Mapping[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -472,19 +489,6 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
|
||||
return celery_remove(self, *args, **kwargs)
|
||||
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
"""Get the execution info of the currently running experiments
|
||||
|
||||
Args:
|
||||
fetch_ref (bool): fetch completed checkpoints or not.
|
||||
"""
|
||||
result: Dict[str, Dict] = {}
|
||||
for entry in self.iter_active():
|
||||
result.update(
|
||||
fetch_running_exp_from_temp_dir(self, entry.stash_rev, fetch_refs)
|
||||
)
|
||||
return result
|
||||
|
||||
def get_ref_and_entry_by_names(
|
||||
self,
|
||||
exp_names: Union[str, List[str]],
|
||||
@@ -512,3 +516,124 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
queue_entry = None if git_remote else queue_entry_match[exp_name]
|
||||
results[exp_name] = ExpRefAndQueueEntry(exp_ref, queue_entry)
|
||||
return results
|
||||
|
||||
def collect_active_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
fetch_refs: bool = False,
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments.collect import collect_exec_branch
|
||||
from dvc.repo.experiments.serialize import (
|
||||
ExpExecutor,
|
||||
ExpRange,
|
||||
LocalExpExecutor,
|
||||
)
|
||||
|
||||
result: Dict[str, List[ExpRange]] = defaultdict(list)
|
||||
for entry in self.iter_active():
|
||||
if baseline_revs and entry.baseline_rev not in baseline_revs:
|
||||
continue
|
||||
if fetch_refs:
|
||||
fetch_running_exp_from_temp_dir(self, entry.stash_rev, fetch_refs)
|
||||
proc_info = self.proc.get(entry.stash_rev)
|
||||
executor_info = self._load_info(entry.stash_rev)
|
||||
if proc_info:
|
||||
local_exec: Optional[LocalExpExecutor] = LocalExpExecutor(
|
||||
root=executor_info.root_dir,
|
||||
log=proc_info.stdout,
|
||||
pid=proc_info.pid,
|
||||
task_id=entry.stash_rev,
|
||||
)
|
||||
else:
|
||||
local_exec = None
|
||||
dvc_root = os.path.join(executor_info.root_dir, executor_info.dvc_dir)
|
||||
with Repo(dvc_root) as exec_repo:
|
||||
kwargs["cache"] = self.repo.experiments.cache
|
||||
exps = list(
|
||||
collect_exec_branch(exec_repo, executor_info.baseline_rev, **kwargs)
|
||||
)
|
||||
exps[0].rev = entry.stash_rev
|
||||
exps[0].name = entry.name
|
||||
result[entry.baseline_rev].append(
|
||||
ExpRange(
|
||||
exps,
|
||||
executor=ExpExecutor(
|
||||
"running",
|
||||
name=executor_info.location,
|
||||
local=local_exec,
|
||||
),
|
||||
name=entry.name,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def collect_queued_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
from dvc.repo.experiments.collect import collect_rev
|
||||
from dvc.repo.experiments.serialize import (
|
||||
ExpExecutor,
|
||||
ExpRange,
|
||||
LocalExpExecutor,
|
||||
)
|
||||
|
||||
result: Dict[str, List[ExpRange]] = defaultdict(list)
|
||||
for entry in self.iter_queued():
|
||||
if baseline_revs and entry.baseline_rev not in baseline_revs:
|
||||
continue
|
||||
exp = collect_rev(self.repo, entry.stash_rev, **kwargs)
|
||||
exp.name = entry.name
|
||||
local_exec: Optional[LocalExpExecutor] = LocalExpExecutor(
|
||||
task_id=entry.stash_rev,
|
||||
)
|
||||
result[entry.baseline_rev].append(
|
||||
ExpRange(
|
||||
[exp],
|
||||
executor=ExpExecutor("queued", name="dvc-task", local=local_exec),
|
||||
name=entry.name,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def collect_failed_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
from dvc.repo.experiments.collect import collect_rev
|
||||
from dvc.repo.experiments.serialize import (
|
||||
ExpExecutor,
|
||||
ExpRange,
|
||||
LocalExpExecutor,
|
||||
SerializableError,
|
||||
)
|
||||
|
||||
result: Dict[str, List[ExpRange]] = defaultdict(list)
|
||||
for entry, _ in self.iter_failed():
|
||||
if baseline_revs and entry.baseline_rev not in baseline_revs:
|
||||
continue
|
||||
proc_info = self.proc.get(entry.stash_rev)
|
||||
if proc_info:
|
||||
local_exec: Optional[LocalExpExecutor] = LocalExpExecutor(
|
||||
log=proc_info.stdout,
|
||||
pid=proc_info.pid,
|
||||
returncode=proc_info.returncode,
|
||||
task_id=entry.stash_rev,
|
||||
)
|
||||
else:
|
||||
local_exec = None
|
||||
exp = collect_rev(self.repo, entry.stash_rev, **kwargs)
|
||||
exp.name = entry.name
|
||||
exp.error = SerializableError("Experiment run failed")
|
||||
result[entry.baseline_rev].append(
|
||||
ExpRange(
|
||||
[exp],
|
||||
executor=ExpExecutor("failed", local=local_exec),
|
||||
name=entry.name,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -91,7 +91,11 @@ def cleanup_exp(executor: TempDirExecutor, infofile: str) -> None:
|
||||
|
||||
|
||||
@shared_task
|
||||
def run_exp(entry_dict: Dict[str, Any], copy_paths: Optional[List[str]] = None) -> None:
|
||||
def run_exp(
|
||||
entry_dict: Dict[str, Any],
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Run a full experiment.
|
||||
|
||||
Experiment subtasks are executed inline as one atomic operation.
|
||||
@@ -111,6 +115,8 @@ def run_exp(entry_dict: Dict[str, Any], copy_paths: Optional[List[str]] = None)
|
||||
if copy_paths:
|
||||
for path in copy_paths:
|
||||
cmd.extend(["--copy-paths", path])
|
||||
if message:
|
||||
cmd.extend(["--message", message])
|
||||
proc_dict = queue.proc.run_signature(cmd, name=entry.stash_rev)()
|
||||
collect_exp.s(proc_dict, entry_dict)()
|
||||
finally:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Generator, List, Optional
|
||||
from typing import TYPE_CHECKING, Collection, Dict, Generator, List, Optional
|
||||
|
||||
from funcy import first
|
||||
|
||||
@@ -19,6 +19,7 @@ from .workspace import WorkspaceQueue
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo.experiments import Experiments
|
||||
from dvc.repo.experiments.executor.base import BaseExecutor, ExecutorResult
|
||||
from dvc.repo.experiments.serialize import ExpRange
|
||||
from dvc_task.proc.manager import ProcessManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -96,6 +97,7 @@ class TempDirQueue(WorkspaceQueue):
|
||||
entry: QueueEntry,
|
||||
executor: "BaseExecutor",
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, str]]:
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
@@ -112,6 +114,7 @@ class TempDirQueue(WorkspaceQueue):
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
log_errors=True,
|
||||
copy_paths=copy_paths,
|
||||
message=message,
|
||||
)
|
||||
if not exec_result.exp_hash:
|
||||
raise DvcException(f"Failed to reproduce experiment '{rev[:7]}'")
|
||||
@@ -141,10 +144,53 @@ class TempDirQueue(WorkspaceQueue):
|
||||
) -> Dict[str, str]:
|
||||
return BaseStashQueue.collect_executor(exp, executor, exec_result)
|
||||
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
result: Dict[str, Dict] = {}
|
||||
def collect_active_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
fetch_refs: bool = False,
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments.collect import collect_exec_branch
|
||||
from dvc.repo.experiments.serialize import (
|
||||
ExpExecutor,
|
||||
ExpRange,
|
||||
LocalExpExecutor,
|
||||
)
|
||||
|
||||
result: Dict[str, List[ExpRange]] = defaultdict(list)
|
||||
for entry in self.iter_active():
|
||||
result.update(
|
||||
if baseline_revs and entry.baseline_rev not in baseline_revs:
|
||||
continue
|
||||
if fetch_refs:
|
||||
fetch_running_exp_from_temp_dir(self, entry.stash_rev, fetch_refs)
|
||||
proc_info = self.proc.get(entry.stash_rev)
|
||||
infofile = self.get_infofile_path(entry.stash_rev)
|
||||
executor_info = ExecutorInfo.load_json(infofile)
|
||||
if proc_info:
|
||||
local_exec: Optional[LocalExpExecutor] = LocalExpExecutor(
|
||||
root=executor_info.root_dir,
|
||||
log=proc_info.stdout,
|
||||
pid=proc_info.pid,
|
||||
)
|
||||
else:
|
||||
local_exec = None
|
||||
dvc_root = os.path.join(executor_info.root_dir, executor_info.dvc_dir)
|
||||
with Repo(dvc_root) as repo:
|
||||
exps = list(
|
||||
collect_exec_branch(repo, executor_info.baseline_rev, **kwargs)
|
||||
)
|
||||
exps[0].rev = entry.stash_rev
|
||||
exps[0].name = entry.name
|
||||
result[entry.baseline_rev].append(
|
||||
ExpRange(
|
||||
exps,
|
||||
executor=ExpExecutor(
|
||||
"running",
|
||||
name=executor_info.location,
|
||||
local=local_exec,
|
||||
),
|
||||
name=entry.name,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Collection, Dict, Generator, List, Optional
|
||||
|
||||
import psutil
|
||||
from funcy import first
|
||||
from voluptuous import Invalid
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.lock import make_lock
|
||||
from dvc.repo.experiments.exceptions import ExpQueueEmptyError
|
||||
from dvc.repo.experiments.executor.base import ExecutorInfo, TaskStatus
|
||||
from dvc.repo.experiments.executor.local import WorkspaceExecutor
|
||||
from dvc.repo.experiments.refs import EXEC_BRANCH, WORKSPACE_STASH
|
||||
from dvc.repo.experiments.utils import get_exp_rwlock
|
||||
from dvc.rwlock import RWLOCK_FILE, RWLOCK_LOCK, SCHEMA
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.serialize import load_json
|
||||
|
||||
from .base import BaseStashQueue, QueueEntry, QueueGetResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo.experiments import Experiments
|
||||
from dvc.repo.experiments.executor.base import BaseExecutor, ExecutorResult
|
||||
from dvc.repo.experiments.serialize import ExpRange
|
||||
|
||||
from .base import QueueDoneResult
|
||||
|
||||
@@ -83,14 +82,16 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
raise NotImplementedError
|
||||
|
||||
def reproduce(
|
||||
self, copy_paths: Optional[List[str]] = None
|
||||
self, copy_paths: Optional[List[str]] = None, message: Optional[str] = None
|
||||
) -> Dict[str, Dict[str, str]]:
|
||||
results: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
try:
|
||||
while True:
|
||||
entry, executor = self.get()
|
||||
results.update(
|
||||
self._reproduce_entry(entry, executor, copy_paths=copy_paths)
|
||||
self._reproduce_entry(
|
||||
entry, executor, copy_paths=copy_paths, message=message
|
||||
)
|
||||
)
|
||||
except ExpQueueEmptyError:
|
||||
pass
|
||||
@@ -101,9 +102,14 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
) -> Dict[str, Dict[str, str]]:
|
||||
kwargs.pop("copy_paths", None)
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
from dvc_task.proc.process import ProcessInfo
|
||||
|
||||
results: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
exec_name = self._EXEC_NAME or entry.stash_rev
|
||||
proc_info = ProcessInfo(os.getpid(), None, None, None, None)
|
||||
proc_info_path = self._proc_info_path(exec_name)
|
||||
os.makedirs(os.path.dirname(proc_info_path), exist_ok=True)
|
||||
proc_info.dump(proc_info_path)
|
||||
infofile = self.get_infofile_path(exec_name)
|
||||
try:
|
||||
rev = entry.stash_rev
|
||||
@@ -113,6 +119,7 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
infofile=infofile,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
log_errors=not isinstance(executor, WorkspaceExecutor),
|
||||
message=kwargs.get("message"),
|
||||
)
|
||||
if not exec_result.exp_hash:
|
||||
raise DvcException(f"Failed to reproduce experiment '{rev[:7]}'")
|
||||
@@ -129,8 +136,29 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
raise DvcException(f"Failed to reproduce experiment '{rev[:7]}'") from exc
|
||||
finally:
|
||||
executor.cleanup(infofile)
|
||||
remove(self._proc_info_path(exec_name))
|
||||
return results
|
||||
|
||||
def _proc_info_path(self, name: str) -> str:
|
||||
return os.path.join(self.pid_dir, name, f"{name}.json")
|
||||
|
||||
@property
|
||||
def _active_pid(self) -> Optional[int]:
|
||||
from dvc_task.proc.process import ProcessInfo
|
||||
|
||||
assert self._EXEC_NAME
|
||||
name = self._EXEC_NAME
|
||||
try:
|
||||
proc_info = ProcessInfo.load(self._proc_info_path(name))
|
||||
pid = proc_info.pid
|
||||
if psutil.pid_exists(pid):
|
||||
return pid
|
||||
logger.debug("Workspace exec PID '%d' no longer exists, removing.", pid)
|
||||
remove(self._proc_info_path(name))
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def collect_executor( # pylint: disable=unused-argument
|
||||
exp: "Experiments",
|
||||
@@ -163,87 +191,71 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
def check_rwlock(
|
||||
self,
|
||||
hardlink: bool = False,
|
||||
autocorrect: bool = False,
|
||||
) -> bool:
|
||||
"""Check and autocorrect the RWLock status for file paths.
|
||||
|
||||
Args:
|
||||
hardlink (bool): use hardlink lock to guard rwlock file when on
|
||||
edit.
|
||||
autocorrect (bool): autocorrect corrupted rwlock file.
|
||||
|
||||
Return:
|
||||
(bool): if the pid alive.
|
||||
"""
|
||||
path = self.repo.fs.path.join(self.repo.tmp_dir, RWLOCK_FILE)
|
||||
|
||||
rwlock_guard = make_lock(
|
||||
self.repo.fs.path.join(self.repo.tmp_dir, RWLOCK_LOCK),
|
||||
tmp_dir=self.repo.tmp_dir,
|
||||
hardlink_lock=hardlink,
|
||||
)
|
||||
with rwlock_guard:
|
||||
try:
|
||||
with self.repo.fs.open(path, encoding="utf-8") as fobj:
|
||||
lock: Dict[str, List[Dict]] = SCHEMA(json.load(fobj))
|
||||
file_path = first(lock["read"])
|
||||
if not file_path:
|
||||
return False
|
||||
lock_info = first(lock["read"][file_path])
|
||||
pid = int(lock_info["pid"])
|
||||
if psutil.pid_exists(pid):
|
||||
return True
|
||||
cmd = lock_info["cmd"]
|
||||
logger.warning(
|
||||
"Process '%s' with (Pid %s), in RWLock-file '%s' had been killed.",
|
||||
cmd,
|
||||
pid,
|
||||
relpath(path),
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(
|
||||
"Unable to read RWLock-file '%s'. JSON structure is corrupted",
|
||||
relpath(path),
|
||||
)
|
||||
except Invalid:
|
||||
logger.warning("RWLock-file '%s' format error.", relpath(path))
|
||||
if autocorrect:
|
||||
logger.warning("Delete corrupted RWLock-file '%s'", relpath(path))
|
||||
remove(path)
|
||||
return False
|
||||
|
||||
def get_running_exps(
|
||||
self,
|
||||
fetch_refs: bool = True, # noqa: ARG002
|
||||
) -> Dict[str, Dict]:
|
||||
from dvc.utils.serialize import load_json
|
||||
|
||||
def get_running_exp(self) -> Optional[str]:
|
||||
"""Return the name of the exp running in workspace (if it exists)."""
|
||||
assert self._EXEC_NAME
|
||||
result: Dict[str, Dict] = {}
|
||||
|
||||
if not self.check_rwlock(autocorrect=True):
|
||||
return result
|
||||
if self._active_pid is None:
|
||||
return None
|
||||
|
||||
infofile = self.get_infofile_path(self._EXEC_NAME)
|
||||
try:
|
||||
info = ExecutorInfo.from_dict(load_json(infofile))
|
||||
except OSError:
|
||||
return None
|
||||
return info.name
|
||||
|
||||
def collect_active_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
fetch_refs: bool = False, # noqa: ARG002
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
from dvc.repo.experiments.collect import collect_exec_branch
|
||||
from dvc.repo.experiments.serialize import (
|
||||
ExpExecutor,
|
||||
ExpRange,
|
||||
LocalExpExecutor,
|
||||
)
|
||||
|
||||
result: Dict[str, List[ExpRange]] = defaultdict(list)
|
||||
pid = self._active_pid
|
||||
if pid is None:
|
||||
return result
|
||||
|
||||
assert self._EXEC_NAME
|
||||
infofile = self.get_infofile_path(self._EXEC_NAME)
|
||||
try:
|
||||
info = ExecutorInfo.from_dict(load_json(infofile))
|
||||
except OSError:
|
||||
return result
|
||||
|
||||
if info.status < TaskStatus.FAILED:
|
||||
# If we are appending to a checkpoint branch in a workspace
|
||||
# run, show the latest checkpoint as running.
|
||||
if info.status == TaskStatus.SUCCESS:
|
||||
return result
|
||||
last_rev = self.scm.get_ref(EXEC_BRANCH)
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
else:
|
||||
result[self._EXEC_NAME] = info.asdict()
|
||||
if (
|
||||
(not baseline_revs or info.baseline_rev in baseline_revs)
|
||||
and info.status < TaskStatus.FAILED
|
||||
and info.status != TaskStatus.SUCCESS
|
||||
):
|
||||
local_exec = LocalExpExecutor(root=info.root_dir, pid=pid)
|
||||
exps = list(collect_exec_branch(self.repo, info.baseline_rev, **kwargs))
|
||||
exps[0].name = info.name
|
||||
result[info.baseline_rev] = [
|
||||
ExpRange(
|
||||
exps,
|
||||
executor=ExpExecutor("running", name="workspace", local=local_exec),
|
||||
name=info.name,
|
||||
)
|
||||
]
|
||||
return result
|
||||
|
||||
def collect_queued_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def collect_failed_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -10,8 +10,9 @@ WORKSPACE_STASH = EXPS_STASH
|
||||
APPLY_NAMESPACE = f"{EXPS_NAMESPACE}/apply"
|
||||
APPLY_HEAD = f"{APPLY_NAMESPACE}/ORIG_HEAD"
|
||||
APPLY_STASH = f"{APPLY_NAMESPACE}/stash"
|
||||
CELERY_STASH = f"{EXPS_NAMESPACE}/celery/stash"
|
||||
CELERY_FAILED_STASH = f"{EXPS_NAMESPACE}/celery/failed"
|
||||
CELERY_NAMESPACE = f"{EXPS_NAMESPACE}/celery"
|
||||
CELERY_STASH = f"{CELERY_NAMESPACE}/stash"
|
||||
CELERY_FAILED_STASH = f"{CELERY_NAMESPACE}/failed"
|
||||
EXEC_NAMESPACE = f"{EXPS_NAMESPACE}/exec"
|
||||
EXEC_APPLY = f"{EXEC_NAMESPACE}/EXEC_APPLY"
|
||||
EXEC_CHECKPOINT = f"{EXEC_NAMESPACE}/EXEC_CHECKPOINT"
|
||||
@@ -19,9 +20,14 @@ EXEC_BRANCH = f"{EXEC_NAMESPACE}/EXEC_BRANCH"
|
||||
EXEC_BASELINE = f"{EXEC_NAMESPACE}/EXEC_BASELINE"
|
||||
EXEC_HEAD = f"{EXEC_NAMESPACE}/EXEC_HEAD"
|
||||
EXEC_MERGE = f"{EXEC_NAMESPACE}/EXEC_MERGE"
|
||||
EXPS_TEMP = f"{EXPS_NAMESPACE}/temp"
|
||||
TEMP_NAMESPACE = f"{EXPS_NAMESPACE}/temp"
|
||||
STASHES = {WORKSPACE_STASH, CELERY_STASH}
|
||||
ITER_SKIP_NAMESPACES = {APPLY_NAMESPACE, EXEC_NAMESPACE}
|
||||
ITER_SKIP_NAMESPACES = {
|
||||
APPLY_NAMESPACE,
|
||||
CELERY_NAMESPACE,
|
||||
EXEC_NAMESPACE,
|
||||
TEMP_NAMESPACE,
|
||||
}
|
||||
|
||||
|
||||
class ExpRefInfo:
|
||||
|
||||
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
|
||||
def remove( # noqa: C901, PLR0912
|
||||
repo: "Repo",
|
||||
exp_names: Union[None, str, List[str]] = None,
|
||||
rev: Optional[str] = None,
|
||||
rev: Optional[Union[List[str], str]] = None,
|
||||
all_commits: bool = False,
|
||||
num: int = 1,
|
||||
queue: bool = False,
|
||||
@@ -60,6 +60,8 @@ def remove( # noqa: C901, PLR0912
|
||||
if remained:
|
||||
raise UnresolvedExpNamesError(remained, git_remote=git_remote)
|
||||
elif rev:
|
||||
if isinstance(rev, str):
|
||||
rev = [rev]
|
||||
exp_ref_dict = _resolve_exp_by_baseline(repo, rev, num, git_remote)
|
||||
removed.extend(exp_ref_dict.keys())
|
||||
exp_ref_list.extend(exp_ref_dict.values())
|
||||
@@ -85,14 +87,14 @@ def remove( # noqa: C901, PLR0912
|
||||
|
||||
def _resolve_exp_by_baseline(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
rev: List[str],
|
||||
num: int,
|
||||
git_remote: Optional[str] = None,
|
||||
) -> Dict[str, "ExpRefInfo"]:
|
||||
assert isinstance(repo.scm, Git)
|
||||
|
||||
commit_ref_dict: Dict[str, "ExpRefInfo"] = {}
|
||||
rev_dict = iter_revs(repo.scm, [rev], num)
|
||||
rev_dict = iter_revs(repo.scm, rev, num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
|
||||
@@ -20,6 +20,7 @@ def run( # noqa: C901, PLR0912
|
||||
tmp_dir: bool = False,
|
||||
queue: bool = False,
|
||||
copy_paths: Optional[Iterable[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, str]:
|
||||
"""Reproduce the specified targets as an experiment.
|
||||
@@ -74,6 +75,7 @@ def run( # noqa: C901, PLR0912
|
||||
params=path_overrides,
|
||||
tmp_dir=tmp_dir,
|
||||
copy_paths=copy_paths,
|
||||
message=message,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -96,6 +98,7 @@ def run( # noqa: C901, PLR0912
|
||||
targets=targets,
|
||||
params=sweep_overrides,
|
||||
copy_paths=copy_paths,
|
||||
message=message,
|
||||
**kwargs,
|
||||
)
|
||||
if sweep_overrides:
|
||||
|
||||
@@ -16,6 +16,7 @@ def save(
|
||||
name: Optional[str] = None,
|
||||
force: bool = False,
|
||||
include_untracked: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""Save the current workspace status as an experiment.
|
||||
|
||||
@@ -29,7 +30,10 @@ def save(
|
||||
|
||||
try:
|
||||
save_result = executor.save(
|
||||
executor.info, force=force, include_untracked=include_untracked
|
||||
executor.info,
|
||||
force=force,
|
||||
include_untracked=include_untracked,
|
||||
message=message,
|
||||
)
|
||||
result = queue.collect_executor(repo.experiments, executor, save_result)
|
||||
finally:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Literal, Optional
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo.metrics.show import _gather_metrics
|
||||
@@ -25,17 +25,15 @@ class _ISOEncoder(json.JSONEncoder):
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SerializableExp:
|
||||
"""Serializable experiment state."""
|
||||
"""Serializable experiment data."""
|
||||
|
||||
rev: str
|
||||
timestamp: Optional[datetime] = None
|
||||
params: Dict[str, Any] = field(default_factory=dict)
|
||||
metrics: Dict[str, Any] = field(default_factory=dict)
|
||||
deps: Dict[str, "_ExpDep"] = field(default_factory=dict)
|
||||
outs: Dict[str, "_ExpOut"] = field(default_factory=dict)
|
||||
status: Optional[str] = None
|
||||
executor: Optional[str] = None
|
||||
error: Optional["SerializableError"] = None
|
||||
deps: Dict[str, "ExpDep"] = field(default_factory=dict)
|
||||
outs: Dict[str, "ExpOut"] = field(default_factory=dict)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_repo(
|
||||
@@ -43,6 +41,7 @@ class SerializableExp:
|
||||
repo: "Repo",
|
||||
rev: Optional[str] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
param_deps: bool = False,
|
||||
**kwargs,
|
||||
) -> "SerializableExp":
|
||||
"""Returns a SerializableExp from the current repo state.
|
||||
@@ -57,29 +56,26 @@ class SerializableExp:
|
||||
|
||||
rev = rev or repo.get_rev()
|
||||
assert rev
|
||||
status: Optional[str] = kwargs.get("status")
|
||||
# NOTE: _gather_params/_gather_metrics return defaultdict which is not
|
||||
# supported in dataclasses.asdict() on all python releases
|
||||
# see https://bugs.python.org/issue35540
|
||||
params = dict(_gather_params(repo, onerror=onerror))
|
||||
if status and status.lower() in ("queued", "failed"):
|
||||
metrics: Dict[str, Any] = {}
|
||||
else:
|
||||
metrics = dict(
|
||||
_gather_metrics(
|
||||
repo,
|
||||
targets=None,
|
||||
rev=rev[:7],
|
||||
recursive=False,
|
||||
onerror=onerror_collect,
|
||||
)
|
||||
params = dict(_gather_params(repo, deps=param_deps, onerror=onerror))
|
||||
metrics = dict(
|
||||
_gather_metrics(
|
||||
repo,
|
||||
targets=None,
|
||||
rev=rev[:7],
|
||||
recursive=False,
|
||||
onerror=onerror_collect,
|
||||
)
|
||||
)
|
||||
meta = cls._gather_meta(repo)
|
||||
return cls(
|
||||
rev=rev,
|
||||
params=params,
|
||||
metrics=metrics,
|
||||
deps={
|
||||
relpath(dep.fs_path, repo.root_dir): _ExpDep(
|
||||
relpath(dep.fs_path, repo.root_dir): ExpDep(
|
||||
hash=dep.hash_info.value if dep.hash_info else None,
|
||||
size=dep.meta.size if dep.meta else None,
|
||||
nfiles=dep.meta.nfiles if dep.meta else None,
|
||||
@@ -88,7 +84,7 @@ class SerializableExp:
|
||||
if not isinstance(dep, (ParamsDependency, RepoDependency))
|
||||
},
|
||||
outs={
|
||||
relpath(out.fs_path, repo.root_dir): _ExpOut(
|
||||
relpath(out.fs_path, repo.root_dir): ExpOut(
|
||||
hash=out.hash_info.value if out.hash_info else None,
|
||||
size=out.meta.size if out.meta else None,
|
||||
nfiles=out.meta.nfiles if out.meta else None,
|
||||
@@ -98,9 +94,16 @@ class SerializableExp:
|
||||
for out in repo.index.outs
|
||||
if not (out.is_metric or out.is_plot)
|
||||
},
|
||||
meta=meta,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _gather_meta(repo: "Repo") -> Dict[str, Any]:
|
||||
return {
|
||||
"has_checkpoints": any(stage.is_checkpoint for stage in repo.index.stages)
|
||||
}
|
||||
|
||||
def dumpd(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
@@ -113,6 +116,10 @@ class SerializableExp:
|
||||
parsed = json.loads(data)
|
||||
if "timestamp" in parsed:
|
||||
parsed["timestamp"] = datetime.fromisoformat(parsed["timestamp"])
|
||||
if "deps" in parsed:
|
||||
parsed["deps"] = {k: ExpDep(**v) for k, v in parsed["deps"].items()}
|
||||
if "outs" in parsed:
|
||||
parsed["outs"] = {k: ExpOut(**v) for k, v in parsed["outs"].items()}
|
||||
return cls(**parsed)
|
||||
except (TypeError, json.JSONDecodeError) as exc:
|
||||
raise DeserializeError("failed to load SerializableExp") from exc
|
||||
@@ -120,8 +127,7 @@ class SerializableExp:
|
||||
@property
|
||||
def contains_error(self) -> bool:
|
||||
return (
|
||||
self.error is not None
|
||||
or self.params.get("error")
|
||||
self.params.get("error")
|
||||
or any(value.get("error") for value in self.params.values())
|
||||
or self.metrics.get("error")
|
||||
or any(value.get("error") for value in self.metrics.values())
|
||||
@@ -129,14 +135,14 @@ class SerializableExp:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _ExpDep:
|
||||
class ExpDep:
|
||||
hash: Optional[str] # noqa: A003
|
||||
size: Optional[int]
|
||||
nfiles: Optional[int]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _ExpOut:
|
||||
class ExpOut:
|
||||
hash: Optional[str] # noqa: A003
|
||||
size: Optional[int]
|
||||
nfiles: Optional[int]
|
||||
@@ -162,3 +168,52 @@ class SerializableError:
|
||||
return cls(**parsed)
|
||||
except (TypeError, json.JSONDecodeError) as exc:
|
||||
raise DeserializeError("failed to load SerializableError") from exc
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpState:
|
||||
"""Git/DVC experiment state."""
|
||||
|
||||
rev: str
|
||||
name: Optional[str] = None
|
||||
data: Optional[SerializableExp] = None
|
||||
error: Optional[SerializableError] = None
|
||||
experiments: Optional[List["ExpRange"]] = None
|
||||
|
||||
def dumpd(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpRange:
|
||||
revs: List["ExpState"]
|
||||
executor: Optional["ExpExecutor"] = None
|
||||
name: Optional[str] = None
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.revs)
|
||||
|
||||
def __iter__(self) -> Iterator["ExpState"]:
|
||||
return iter(self.revs)
|
||||
|
||||
def __getitem__(self, index: int) -> "ExpState":
|
||||
return self.revs[index]
|
||||
|
||||
def dumpd(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalExpExecutor:
|
||||
root: Optional[str] = None
|
||||
log: Optional[str] = None
|
||||
pid: Optional[int] = None
|
||||
returncode: Optional[int] = None
|
||||
task_id: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpExecutor:
|
||||
state: Literal["success", "queued", "running", "failed"]
|
||||
name: Optional[str] = None
|
||||
local: Optional[LocalExpExecutor] = None
|
||||
|
||||
+352
-479
@@ -1,517 +1,390 @@
|
||||
import logging
|
||||
from collections import OrderedDict, defaultdict
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from itertools import chain
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import date, datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Literal,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from scmrepo.exceptions import SCMError as InnerScmError
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.scm import Git
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.flatten import flatten
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.scm import Git, SCMError, iter_revs, resolve_rev
|
||||
|
||||
from .refs import ExpRefInfo
|
||||
from .serialize import SerializableError, SerializableExp
|
||||
from .collect import collect
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git.objects import GitCommit
|
||||
|
||||
from dvc.compare import TabularData
|
||||
from dvc.repo import Repo
|
||||
from dvc.ui.table import CellT
|
||||
|
||||
from .serialize import ExpRange, ExpState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExpStatus(Enum):
|
||||
Success = 0
|
||||
Queued = 1
|
||||
Running = 2
|
||||
Failed = 3
|
||||
|
||||
|
||||
class _CachedError(DvcException):
|
||||
def __init__(self, msg, typ, *args):
|
||||
super().__init__(msg, *args)
|
||||
self.typ = typ
|
||||
|
||||
|
||||
def _is_scm_error(collected_exp: Dict[str, Any]) -> bool:
|
||||
if "error" in collected_exp and (
|
||||
isinstance(collected_exp["error"], (_CachedError, SCMError, InnerScmError))
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _format_exp(exp: SerializableExp) -> Dict[str, Any]:
|
||||
# SerializableExp always includes error but we need to strip it from show
|
||||
# output when it is false-y to maintain compatibility w/tools that consume
|
||||
# json output and assume that "error" key presence means there was an error
|
||||
exp_dict = exp.dumpd()
|
||||
if "error" in exp_dict and not exp_dict["error"]:
|
||||
del exp_dict["error"]
|
||||
return {"data": exp_dict}
|
||||
|
||||
|
||||
def _format_error(error: SerializableError):
|
||||
msg = error.msg or "None"
|
||||
return {"data": {}, "error": _CachedError(msg, error.type)}
|
||||
|
||||
|
||||
def collect_experiment_commit(
|
||||
def show(
|
||||
repo: "Repo",
|
||||
exp_rev: str,
|
||||
status: ExpStatus = ExpStatus.Success,
|
||||
param_deps: bool = False,
|
||||
force: bool = False,
|
||||
revs: Union[List[str], str, None] = None,
|
||||
all_branches: bool = False,
|
||||
all_tags: bool = False,
|
||||
all_commits: bool = False,
|
||||
num: int = 1,
|
||||
hide_queued: bool = False,
|
||||
hide_failed: bool = False,
|
||||
sha_only: bool = False,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
cache = repo.experiments.cache
|
||||
# TODO: support filtering serialized exp when param_deps is set
|
||||
if exp_rev != "workspace" and not (force or param_deps):
|
||||
cached_exp = cache.get(exp_rev)
|
||||
if cached_exp:
|
||||
if status == ExpStatus.Running or (
|
||||
isinstance(cached_exp, SerializableExp)
|
||||
and cached_exp.status == ExpStatus.Running.name
|
||||
):
|
||||
# expire cached queued exp entry once we start running it
|
||||
cache.delete(exp_rev)
|
||||
elif isinstance(cached_exp, SerializableError):
|
||||
return _format_error(cached_exp)
|
||||
else:
|
||||
return _format_exp(cached_exp)
|
||||
try:
|
||||
exp = _collect_from_repo(
|
||||
repo,
|
||||
exp_rev,
|
||||
status=status,
|
||||
param_deps=param_deps,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
if not (
|
||||
exp_rev == "workspace"
|
||||
or status == ExpStatus.Running
|
||||
or param_deps
|
||||
or exp.contains_error
|
||||
):
|
||||
cache.put(exp, force=True)
|
||||
return _format_exp(exp)
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
logger.debug("", exc_info=True)
|
||||
error = SerializableError(str(exc), type(exc).__name__)
|
||||
return _format_error(error)
|
||||
|
||||
|
||||
def _collect_from_repo(
|
||||
repo: "Repo",
|
||||
exp_rev: str,
|
||||
status: ExpStatus = ExpStatus.Success,
|
||||
running: Optional[Dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> "SerializableExp":
|
||||
running = running or {}
|
||||
with repo.switch(exp_rev) as rev:
|
||||
if rev == "workspace":
|
||||
timestamp: Optional[datetime] = None
|
||||
else:
|
||||
commit = repo.scm.resolve_commit(rev)
|
||||
timestamp = datetime.fromtimestamp(commit.commit_time)
|
||||
|
||||
if status == ExpStatus.Running:
|
||||
executor: Optional[str] = running.get(exp_rev, {}).get("location", None)
|
||||
else:
|
||||
executor = None
|
||||
|
||||
if status == ExpStatus.Failed:
|
||||
error: Optional["SerializableError"] = SerializableError(
|
||||
"Experiment run failed."
|
||||
)
|
||||
else:
|
||||
error = None
|
||||
|
||||
return SerializableExp.from_repo(
|
||||
repo,
|
||||
rev=exp_rev,
|
||||
timestamp=timestamp,
|
||||
status=status.name,
|
||||
executor=executor,
|
||||
error=error,
|
||||
)
|
||||
|
||||
|
||||
def _collect_complete_experiment(
|
||||
repo: "Repo",
|
||||
baseline: str,
|
||||
exp_rev: str,
|
||||
running: Dict[str, Any],
|
||||
revs: List[str],
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
results: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
||||
|
||||
checkpoint = len(revs) > 1
|
||||
prev = ""
|
||||
|
||||
for rev in revs:
|
||||
status = ExpStatus.Running if rev in running else ExpStatus.Success
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
rev,
|
||||
status=status,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
return {}
|
||||
if checkpoint:
|
||||
exp = {"checkpoint_tip": exp_rev}
|
||||
if prev:
|
||||
results[prev]["data"][ # type: ignore[unreachable]
|
||||
"checkpoint_parent"
|
||||
] = rev
|
||||
if rev in results:
|
||||
results[rev]["data"].update(exp)
|
||||
results.move_to_end(rev)
|
||||
else:
|
||||
exp.update(collected_exp["data"])
|
||||
else:
|
||||
exp = collected_exp["data"]
|
||||
if rev not in results:
|
||||
results[rev] = {"data": exp}
|
||||
prev = rev
|
||||
if checkpoint and prev:
|
||||
results[prev]["data"]["checkpoint_parent"] = baseline
|
||||
return results
|
||||
|
||||
|
||||
def _collect_branch(
|
||||
repo: "Repo",
|
||||
baseline: str,
|
||||
running: Dict[str, Any],
|
||||
refs: Optional[Iterable[str]] = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
results: Dict[str, Dict[str, Any]] = OrderedDict()
|
||||
status = ExpStatus.Running if baseline in running else ExpStatus.Success
|
||||
results["baseline"] = collect_experiment_commit(
|
||||
) -> List["ExpState"]:
|
||||
return collect(
|
||||
repo,
|
||||
baseline,
|
||||
status=status,
|
||||
running=running,
|
||||
revs=revs,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
num=num,
|
||||
hide_queued=hide_queued,
|
||||
hide_failed=hide_failed,
|
||||
sha_only=sha_only,
|
||||
**kwargs,
|
||||
)
|
||||
if baseline == "workspace" or _is_scm_error(results["baseline"]):
|
||||
return results
|
||||
|
||||
ref_info = ExpRefInfo(baseline_sha=baseline)
|
||||
commits: List[Tuple[str, "GitCommit", str, List[str]]] = []
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
if refs:
|
||||
ref_it = (ref for ref in iter(refs) if ref.startswith(str(ref_info)))
|
||||
else:
|
||||
ref_it = repo.scm.iter_refs(base=str(ref_info))
|
||||
for ref in ref_it:
|
||||
try:
|
||||
commit = repo.scm.resolve_commit(ref)
|
||||
exp_rev = resolve_rev(repo.scm, ref)
|
||||
revs = list(repo.scm.branch_revs(exp_rev, baseline))
|
||||
except (SCMError, InnerScmError):
|
||||
continue
|
||||
commits.append((ref, commit, exp_rev, revs))
|
||||
def tabulate(
|
||||
baseline_states: Iterable["ExpState"],
|
||||
fill_value: Optional[str] = "-",
|
||||
error_value: str = "!",
|
||||
**kwargs,
|
||||
) -> Tuple["TabularData", Dict[str, Iterable[str]]]:
|
||||
"""Return table data for experiments.
|
||||
|
||||
for exp_ref, _, exp_rev, revs in sorted(
|
||||
commits, key=lambda x: x[1].commit_time, reverse=True
|
||||
):
|
||||
ref_info = ExpRefInfo.from_ref(exp_ref)
|
||||
assert ref_info.baseline_sha == baseline
|
||||
collected_exp = _collect_complete_experiment(
|
||||
repo,
|
||||
baseline=baseline,
|
||||
exp_rev=exp_rev,
|
||||
running=running,
|
||||
revs=revs,
|
||||
Returns:
|
||||
Tuple of (table_data, data_headers)
|
||||
"""
|
||||
from funcy import lconcat
|
||||
from funcy.seqs import flatten as flatten_list
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
data_names = _collect_names(baseline_states)
|
||||
metrics_names = data_names.metrics
|
||||
params_names = data_names.params
|
||||
deps_names = data_names.sorted_deps
|
||||
|
||||
headers = [
|
||||
"Experiment",
|
||||
"rev",
|
||||
"typ",
|
||||
"Created",
|
||||
"parent",
|
||||
"State",
|
||||
"Executor",
|
||||
]
|
||||
names = {**metrics_names, **params_names}
|
||||
counter = Counter(flatten_list([list(a.keys()) for a in names.values()]))
|
||||
counter.update(headers)
|
||||
metrics_headers = _normalize_headers(metrics_names, counter)
|
||||
params_headers = _normalize_headers(params_names, counter)
|
||||
|
||||
all_headers = lconcat(headers, metrics_headers, params_headers, deps_names)
|
||||
td = TabularData(all_headers, fill_value=fill_value)
|
||||
td.extend(
|
||||
_build_rows(
|
||||
baseline_states,
|
||||
all_headers=all_headers,
|
||||
metrics_headers=metrics_headers,
|
||||
params_headers=params_headers,
|
||||
metrics_names=metrics_names,
|
||||
params_names=params_names,
|
||||
deps_names=deps_names,
|
||||
fill_value=fill_value,
|
||||
error_value=error_value,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
results.update(collected_exp)
|
||||
return results
|
||||
)
|
||||
data_headers: Dict[str, Iterable[str]] = {
|
||||
"metrics": metrics_headers,
|
||||
"params": params_headers,
|
||||
"deps": deps_names,
|
||||
}
|
||||
return td, data_headers
|
||||
|
||||
|
||||
def get_branch_names(
|
||||
scm: "Git", baseline_set: Iterable[str], refs: Optional[Iterable[str]] = None
|
||||
) -> Dict[str, Optional[str]]:
|
||||
names: Dict[str, Optional[str]] = {}
|
||||
bases = [
|
||||
f"refs/exps/{baseline[:2]}/{baseline[2:]}/" for baseline in baseline_set
|
||||
] + ["refs/heads/", "refs/tags/"]
|
||||
ref_it = iter(refs) if refs else scm.iter_refs()
|
||||
for ref in ref_it:
|
||||
for base in bases:
|
||||
if ref.startswith(base):
|
||||
try:
|
||||
rev = scm.get_ref(ref)
|
||||
names[rev] = ref[len(base) :]
|
||||
except KeyError:
|
||||
logger.debug("unresolved ref %s", ref)
|
||||
break
|
||||
logger.debug("found refs for revs: %s", names)
|
||||
return names
|
||||
|
||||
|
||||
def update_names( # noqa: C901
|
||||
repo: "Repo",
|
||||
branch_names: Dict[str, Optional[str]],
|
||||
result: Dict[str, Dict[str, Any]],
|
||||
):
|
||||
rev_set = set()
|
||||
for baseline in result:
|
||||
for rev in result[baseline]:
|
||||
if rev == "baseline":
|
||||
rev = baseline
|
||||
if rev != "workspace":
|
||||
rev_set.add(rev)
|
||||
|
||||
if rev_set:
|
||||
rev_set.difference_update(branch_names.keys())
|
||||
|
||||
exact_name = repo.experiments.get_exact_name(rev_set)
|
||||
|
||||
for baseline, baseline_results in result.items():
|
||||
name_set: Set[str] = set()
|
||||
for rev, rev_result in baseline_results.items():
|
||||
name: Optional[str] = None
|
||||
if rev == "baseline":
|
||||
rev = baseline
|
||||
if rev == "workspace":
|
||||
continue
|
||||
name = branch_names.get(rev, None) or exact_name[rev]
|
||||
if name and name not in name_set:
|
||||
name_set.add(name)
|
||||
rev_result["data"]["name"] = name
|
||||
|
||||
|
||||
def _collect_active_experiment(
|
||||
repo: "Repo",
|
||||
found_revs: Dict[str, List[str]],
|
||||
running: Dict[str, Any],
|
||||
def _build_rows(
|
||||
baseline_states: Iterable["ExpState"],
|
||||
*,
|
||||
all_headers: Iterable[str],
|
||||
fill_value: Optional[str],
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[Literal["asc", "desc"]] = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
result: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
for entry in chain(
|
||||
repo.experiments.tempdir_queue.iter_active(),
|
||||
repo.experiments.celery_queue.iter_active(),
|
||||
):
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs and (
|
||||
stash_rev not in running or not running[stash_rev].get("last")
|
||||
) -> Iterator[Tuple["CellT", ...]]:
|
||||
for baseline in baseline_states:
|
||||
row: Dict[str, "CellT"] = {k: fill_value for k in all_headers}
|
||||
row["Experiment"] = ""
|
||||
if baseline.name:
|
||||
row["rev"] = baseline.name
|
||||
elif Git.is_sha(baseline.rev):
|
||||
row["rev"] = baseline.rev[:7]
|
||||
else:
|
||||
row["rev"] = baseline.rev
|
||||
row["typ"] = "baseline"
|
||||
row["parent"] = ""
|
||||
if baseline.data:
|
||||
row["Created"] = format_time(
|
||||
baseline.data.timestamp, fill_value=fill_value, **kwargs
|
||||
)
|
||||
row.update(
|
||||
_data_cells( # pylint: disable=missing-kwoa
|
||||
baseline, fill_value=fill_value, **kwargs
|
||||
)
|
||||
)
|
||||
yield tuple(row.values())
|
||||
if baseline.experiments:
|
||||
if sort_by:
|
||||
metrics_names: Mapping[str, Iterable[str]] = kwargs.get(
|
||||
"metrics_names", {}
|
||||
)
|
||||
params_names: Mapping[str, Iterable[str]] = kwargs.get(
|
||||
"params_names", {}
|
||||
)
|
||||
sort_path, sort_name, sort_type = _sort_column(
|
||||
sort_by, metrics_names, params_names
|
||||
)
|
||||
reverse = sort_order == "desc"
|
||||
experiments = _sort_exp(
|
||||
baseline.experiments, sort_path, sort_name, sort_type, reverse
|
||||
)
|
||||
else:
|
||||
experiments = baseline.experiments
|
||||
for i, child in enumerate(experiments):
|
||||
yield from _exp_range_rows(
|
||||
child,
|
||||
all_headers=all_headers,
|
||||
fill_value=fill_value,
|
||||
is_base=i == len(baseline.experiments) - 1,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _sort_column(
|
||||
sort_by: str,
|
||||
metric_names: Mapping[str, Iterable[str]],
|
||||
param_names: Mapping[str, Iterable[str]],
|
||||
) -> Tuple[str, str, str]:
|
||||
path, _, sort_name = sort_by.rpartition(":")
|
||||
matches: Set[Tuple[str, str, str]] = set()
|
||||
|
||||
if path:
|
||||
if path in metric_names and sort_name in metric_names[path]:
|
||||
matches.add((path, sort_name, "metrics"))
|
||||
if path in param_names and sort_name in param_names[path]:
|
||||
matches.add((path, sort_name, "params"))
|
||||
else:
|
||||
for path in metric_names:
|
||||
if sort_name in metric_names[path]:
|
||||
matches.add((path, sort_name, "metrics"))
|
||||
for path in param_names:
|
||||
if sort_name in param_names[path]:
|
||||
matches.add((path, sort_name, "params"))
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches.pop()
|
||||
if len(matches) > 1:
|
||||
raise InvalidArgumentError(
|
||||
"Ambiguous sort column '{}' matched '{}'".format(
|
||||
sort_by,
|
||||
", ".join([f"{path}:{name}" for path, name, _ in matches]),
|
||||
)
|
||||
)
|
||||
raise InvalidArgumentError(f"Unknown sort column '{sort_by}'")
|
||||
|
||||
|
||||
def _sort_exp(
|
||||
experiments: Iterable["ExpRange"],
|
||||
sort_path: str,
|
||||
sort_name: str,
|
||||
typ: str,
|
||||
reverse: bool,
|
||||
) -> List["ExpRange"]:
|
||||
from funcy import first
|
||||
|
||||
def _sort(exp_range: "ExpRange"):
|
||||
exp = first(exp_range.revs)
|
||||
if not exp:
|
||||
return True
|
||||
data = exp.data.dumpd().get(typ, {}).get(sort_path, {}).get("data", {})
|
||||
val = flatten(data).get(sort_name)
|
||||
return val is None, val
|
||||
|
||||
return sorted(experiments, key=_sort, reverse=reverse)
|
||||
|
||||
|
||||
def _exp_range_rows(
|
||||
exp_range: "ExpRange",
|
||||
*,
|
||||
all_headers: Iterable[str],
|
||||
fill_value: Optional[str],
|
||||
is_base: bool = False,
|
||||
**kwargs,
|
||||
) -> Iterator[Tuple["CellT", ...]]:
|
||||
for i, exp in enumerate(exp_range.revs):
|
||||
row: Dict[str, "CellT"] = {k: fill_value for k in all_headers}
|
||||
row["Experiment"] = exp.name or ""
|
||||
row["rev"] = exp.rev[:7] if Git.is_sha(exp.rev) else exp.rev
|
||||
if len(exp_range.revs) > 1:
|
||||
if i == 0:
|
||||
row["typ"] = "checkpoint_tip"
|
||||
elif i == len(exp_range.revs) - 1:
|
||||
row["typ"] = "checkpoint_base"
|
||||
else:
|
||||
row["typ"] = "checkpoint_commit"
|
||||
else:
|
||||
row["typ"] = "branch_base" if is_base else "branch_commit"
|
||||
row["parent"] = ""
|
||||
if exp_range.executor:
|
||||
row["State"] = exp_range.executor.state.capitalize()
|
||||
if exp_range.executor.name:
|
||||
row["Executor"] = exp_range.executor.name.capitalize()
|
||||
if exp.data:
|
||||
row["Created"] = format_time(
|
||||
exp.data.timestamp, fill_value=fill_value, **kwargs
|
||||
)
|
||||
row.update(
|
||||
_data_cells( # pylint: disable=missing-kwoa
|
||||
exp, fill_value=fill_value, **kwargs
|
||||
)
|
||||
)
|
||||
yield tuple(row.values())
|
||||
|
||||
|
||||
def _data_cells(
|
||||
exp: "ExpState",
|
||||
*,
|
||||
metrics_headers: Iterable[str],
|
||||
params_headers: Iterable[str],
|
||||
metrics_names: Mapping[str, Iterable[str]],
|
||||
params_names: Mapping[str, Iterable[str]],
|
||||
deps_names: Iterable[str],
|
||||
fill_value: Optional[str] = "-",
|
||||
error_value: str = "!",
|
||||
precision: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> Iterator[Tuple[str, "CellT"]]:
|
||||
def _d_cells(
|
||||
d: Mapping[str, Any],
|
||||
names: Mapping[str, Iterable[str]],
|
||||
headers: Iterable[str],
|
||||
) -> Iterator[Tuple[str, "CellT"]]:
|
||||
from dvc.compare import _format_field, with_value
|
||||
|
||||
for fname, data in d.items():
|
||||
item = data.get("data", {})
|
||||
item = flatten(item) if isinstance(item, dict) else {fname: item}
|
||||
for name in names[fname]:
|
||||
value = with_value(
|
||||
item.get(name),
|
||||
error_value if data.get("error") else fill_value,
|
||||
)
|
||||
# wrap field data in ui.rich_text, otherwise rich may
|
||||
# interpret unescaped braces from list/dict types as rich
|
||||
# markup tags
|
||||
value = ui.rich_text(str(_format_field(value, precision)))
|
||||
if name in headers:
|
||||
yield name, value
|
||||
else:
|
||||
yield f"{fname}:{name}", value
|
||||
|
||||
if not exp.data:
|
||||
return
|
||||
yield from _d_cells(exp.data.metrics, metrics_names, metrics_headers)
|
||||
yield from _d_cells(exp.data.params, params_names, params_headers)
|
||||
for name in deps_names:
|
||||
dep = exp.data.deps.get(name)
|
||||
if dep:
|
||||
yield name, dep.hash or fill_value
|
||||
|
||||
|
||||
def format_time(
|
||||
timestamp: Optional[datetime],
|
||||
fill_value: Optional[str] = "-",
|
||||
iso: bool = False,
|
||||
**kwargs,
|
||||
) -> Optional[str]:
|
||||
if not timestamp:
|
||||
return fill_value
|
||||
if iso:
|
||||
return timestamp.isoformat()
|
||||
if timestamp.date() == date.today():
|
||||
fmt = "%I:%M %p"
|
||||
else:
|
||||
fmt = "%b %d, %Y"
|
||||
return timestamp.strftime(fmt)
|
||||
|
||||
|
||||
class _DataNames(NamedTuple):
|
||||
# NOTE: we use nested dict instead of set for metrics/params names to
|
||||
# preserve key ordering
|
||||
metrics: Dict[str, Dict[str, Any]]
|
||||
params: Dict[str, Dict[str, Any]]
|
||||
deps: Set[str]
|
||||
|
||||
@property
|
||||
def sorted_deps(self):
|
||||
return sorted(self.deps)
|
||||
|
||||
def update(self, other: "_DataNames"):
|
||||
def _update_d(
|
||||
d: Dict[str, Dict[str, Any]], other_d: Mapping[str, Mapping[str, Any]]
|
||||
):
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Running,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
result[entry.baseline_rev][stash_rev] = collected_exp
|
||||
for k, v in other_d.items():
|
||||
if k in d:
|
||||
d[k].update(v)
|
||||
else:
|
||||
d[k] = dict(v)
|
||||
|
||||
_update_d(self.metrics, other.metrics)
|
||||
_update_d(self.params, other.params)
|
||||
self.deps.update(other.deps)
|
||||
|
||||
|
||||
def _collect_names(exp_states: Iterable["ExpState"]) -> _DataNames:
|
||||
result = _DataNames(defaultdict(dict), defaultdict(dict), set())
|
||||
|
||||
def _collect_d(result_d: Dict[str, Dict[str, Any]], data_d: Dict[str, Any]):
|
||||
for path, item in data_d.items():
|
||||
item = item.get("data", {})
|
||||
if isinstance(item, dict):
|
||||
item = flatten(item)
|
||||
result_d[path].update((key, None) for key in item)
|
||||
|
||||
for exp in exp_states:
|
||||
if exp.data:
|
||||
_collect_d(result.metrics, exp.data.metrics)
|
||||
_collect_d(result.params, exp.data.params)
|
||||
result.deps.update(exp.data.deps)
|
||||
if exp.experiments:
|
||||
for child in exp.experiments:
|
||||
result.update(_collect_names(child.revs))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _collect_queued_experiment(
|
||||
repo: "Repo",
|
||||
found_revs: Dict[str, List[str]],
|
||||
running: Dict[str, Any],
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
result: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
for entry in repo.experiments.celery_queue.iter_queued():
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs:
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Queued,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
result[entry.baseline_rev][stash_rev] = collected_exp
|
||||
return result
|
||||
|
||||
|
||||
def _collect_failed_experiment(
|
||||
repo: "Repo",
|
||||
found_revs: Dict[str, List[str]],
|
||||
running: Dict[str, Any],
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
result: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
for queue_done_result in repo.experiments.celery_queue.iter_failed():
|
||||
entry = queue_done_result.entry
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs:
|
||||
collected_exp = collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Failed,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if _is_scm_error(collected_exp):
|
||||
continue
|
||||
result[entry.baseline_rev][stash_rev] = collected_exp
|
||||
return result
|
||||
|
||||
|
||||
def update_new(
|
||||
to_dict: Dict[str, Dict[str, Any]], from_dict: Dict[str, Dict[str, Any]]
|
||||
):
|
||||
for baseline, experiments in from_dict.items():
|
||||
for rev, experiment in experiments.items():
|
||||
to_dict[baseline][rev] = to_dict[baseline].get(rev, experiment)
|
||||
|
||||
|
||||
def move_properties_to_head(result: Dict[str, Dict[str, Dict[str, Any]]]):
|
||||
for _, baseline_results in result.items():
|
||||
checkpoint: bool = False
|
||||
head: Dict[str, Any] = {}
|
||||
for rev, rev_data in baseline_results.items():
|
||||
if (
|
||||
"data" not in rev_data
|
||||
or rev_data["data"].get("checkpoint_tip", None) is None
|
||||
):
|
||||
checkpoint = False
|
||||
head = {}
|
||||
continue
|
||||
|
||||
rev_result: Dict[str, Any] = rev_data["data"]
|
||||
if (
|
||||
checkpoint is True
|
||||
and rev_result["checkpoint_tip"] == head["checkpoint_tip"]
|
||||
):
|
||||
if "name" in rev_result and "name" not in head:
|
||||
head["name"] = rev_result["name"]
|
||||
del rev_result["name"]
|
||||
if rev_result["executor"]:
|
||||
if not head["executor"]:
|
||||
head["executor"] = rev_result["executor"]
|
||||
rev_result["executor"] = None
|
||||
if rev_result["status"] == ExpStatus.Running.name:
|
||||
head["status"] = ExpStatus.Running.name
|
||||
rev_result["status"] = ExpStatus.Success.name
|
||||
elif rev_result["checkpoint_tip"] == rev:
|
||||
head = rev_result
|
||||
checkpoint = True
|
||||
|
||||
|
||||
def show( # noqa: PLR0913
|
||||
repo: "Repo",
|
||||
all_branches=False,
|
||||
all_tags=False,
|
||||
revs: Union[List[str], str, None] = None,
|
||||
all_commits=False,
|
||||
hide_queued=False,
|
||||
hide_failed=False,
|
||||
sha_only=False,
|
||||
num=1,
|
||||
param_deps=False,
|
||||
onerror: Optional[Callable] = None,
|
||||
fetch_running: bool = True,
|
||||
force: bool = False,
|
||||
):
|
||||
if repo.scm.no_commits:
|
||||
return {}
|
||||
|
||||
res: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
|
||||
if not any([revs, all_branches, all_tags, all_commits]):
|
||||
revs = ["HEAD"]
|
||||
if isinstance(revs, str):
|
||||
revs = [revs]
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
|
||||
found_revs: Dict[str, List[str]] = {"workspace": []}
|
||||
found_revs.update(
|
||||
iter_revs(repo.scm, revs, num, all_branches, all_tags, all_commits)
|
||||
)
|
||||
cached_refs = list(repo.scm.iter_refs())
|
||||
branch_names = get_branch_names(repo.scm, found_revs, refs=cached_refs)
|
||||
|
||||
running: Dict[str, Dict] = repo.experiments.get_running_exps(
|
||||
fetch_refs=fetch_running
|
||||
)
|
||||
|
||||
queued_experiment = (
|
||||
_collect_queued_experiment(
|
||||
repo,
|
||||
found_revs,
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
force=force,
|
||||
)
|
||||
if not hide_queued
|
||||
else {}
|
||||
)
|
||||
|
||||
active_experiment = _collect_active_experiment(
|
||||
repo,
|
||||
found_revs,
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
)
|
||||
|
||||
failed_experiments = (
|
||||
_collect_failed_experiment(
|
||||
repo,
|
||||
found_revs,
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
)
|
||||
if not hide_failed
|
||||
else {}
|
||||
)
|
||||
|
||||
for baseline in found_revs:
|
||||
res[baseline] = _collect_branch(
|
||||
repo,
|
||||
baseline,
|
||||
running=running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
force=force,
|
||||
refs=cached_refs,
|
||||
)
|
||||
|
||||
update_new(res, failed_experiments)
|
||||
|
||||
update_new(res, active_experiment)
|
||||
|
||||
update_new(res, queued_experiment)
|
||||
|
||||
if not sha_only:
|
||||
update_names(repo, branch_names, res)
|
||||
|
||||
move_properties_to_head(res)
|
||||
|
||||
return res
|
||||
def _normalize_headers(
|
||||
names: Mapping[str, Mapping[str, Any]], count: Mapping[str, int]
|
||||
) -> List[str]:
|
||||
return [
|
||||
name if count[name] == 1 else f"{path}:{name}"
|
||||
for path in names
|
||||
for name in names[path]
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
+53
-13
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
from typing import TYPE_CHECKING, List, Optional, Set, Tuple
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
@@ -7,11 +7,22 @@ from . import locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _validate_args(**kwargs):
|
||||
not_in_remote = kwargs.pop("not_in_remote", None)
|
||||
cloud = kwargs.pop("cloud", None)
|
||||
remote = kwargs.pop("remote", None)
|
||||
if remote and not (cloud or not_in_remote):
|
||||
raise InvalidArgumentError("`--remote` requires `--cloud` or `--not-in-remote`")
|
||||
if not_in_remote and cloud:
|
||||
raise InvalidArgumentError(
|
||||
"`--not-in-remote` and `--cloud` are mutually exclusive"
|
||||
)
|
||||
if not any(kwargs.values()):
|
||||
raise InvalidArgumentError(
|
||||
"Either of `-w|--workspace`, `-a|--all-branches`, `-T|--all-tags` "
|
||||
@@ -22,8 +33,22 @@ def _validate_args(**kwargs):
|
||||
raise InvalidArgumentError("`--num` can only be used alongside `--rev`")
|
||||
|
||||
|
||||
def _used_obj_ids_not_in_remote(repo, default_remote, jobs, remote_odb_to_obj_ids):
|
||||
used_obj_ids = set()
|
||||
remote_oids = set()
|
||||
for remote_odb, obj_ids in remote_odb_to_obj_ids:
|
||||
if remote_odb is None:
|
||||
remote_odb = repo.cloud.get_remote_odb(default_remote, "gc --not-in-remote")
|
||||
|
||||
remote_oids.update(
|
||||
remote_odb.list_oids_exists({x.value for x in obj_ids}, jobs=jobs)
|
||||
)
|
||||
used_obj_ids.update(obj_ids)
|
||||
return {obj for obj in used_obj_ids if obj.value not in remote_oids}
|
||||
|
||||
|
||||
@locked
|
||||
def gc( # noqa: PLR0913
|
||||
def gc( # noqa: PLR0912, PLR0913, C901
|
||||
self: "Repo",
|
||||
all_branches: bool = False,
|
||||
cloud: bool = False,
|
||||
@@ -39,6 +64,7 @@ def gc( # noqa: PLR0913
|
||||
commit_date: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
num: Optional[int] = None,
|
||||
not_in_remote: bool = False,
|
||||
):
|
||||
# require `workspace` to be true to come into effect.
|
||||
# assume `workspace` to be enabled if any of `all_tags`, `all_commits`,
|
||||
@@ -52,6 +78,8 @@ def gc( # noqa: PLR0913
|
||||
commit_date=commit_date,
|
||||
rev=rev,
|
||||
num=num,
|
||||
cloud=cloud,
|
||||
not_in_remote=not_in_remote,
|
||||
)
|
||||
|
||||
from contextlib import ExitStack
|
||||
@@ -64,13 +92,14 @@ def gc( # noqa: PLR0913
|
||||
repos = []
|
||||
all_repos = [Repo(path) for path in repos]
|
||||
|
||||
used_obj_ids = set()
|
||||
odb_to_obj_ids: List[Tuple[Optional["ObjectDB"], Set["HashInfo"]]] = []
|
||||
|
||||
with ExitStack() as stack:
|
||||
for repo in all_repos:
|
||||
stack.enter_context(repo.lock)
|
||||
|
||||
for repo in [*all_repos, self]:
|
||||
for obj_ids in repo.used_objs(
|
||||
for odb, obj_ids in repo.used_objs(
|
||||
all_branches=all_branches,
|
||||
with_deps=with_deps,
|
||||
all_tags=all_tags,
|
||||
@@ -82,13 +111,22 @@ def gc( # noqa: PLR0913
|
||||
jobs=jobs,
|
||||
revs=[rev] if rev else None,
|
||||
num=num or 1,
|
||||
).values():
|
||||
used_obj_ids.update(obj_ids)
|
||||
).items():
|
||||
odb_to_obj_ids.append((odb, obj_ids))
|
||||
|
||||
if not odb_to_obj_ids:
|
||||
odb_to_obj_ids = [(None, set())]
|
||||
|
||||
if not_in_remote:
|
||||
used_obj_ids = _used_obj_ids_not_in_remote(self, remote, jobs, odb_to_obj_ids)
|
||||
else:
|
||||
used_obj_ids = set()
|
||||
for _, obj_ids in odb_to_obj_ids:
|
||||
used_obj_ids.update(obj_ids)
|
||||
|
||||
for scheme, odb in self.cache.by_scheme():
|
||||
if not odb:
|
||||
continue
|
||||
|
||||
removed = ogc(odb, used_obj_ids, jobs=jobs)
|
||||
if not removed:
|
||||
logger.info("No unused '%s' cache to remove.", scheme)
|
||||
@@ -96,9 +134,11 @@ def gc( # noqa: PLR0913
|
||||
if not cloud:
|
||||
return
|
||||
|
||||
odb = self.cloud.get_remote_odb(remote, "gc -c")
|
||||
removed = ogc(odb, used_obj_ids, jobs=jobs)
|
||||
if removed:
|
||||
get_index(odb).clear()
|
||||
else:
|
||||
logger.info("No unused cache to remove from remote.")
|
||||
for remote_odb, obj_ids in odb_to_obj_ids:
|
||||
if remote_odb is None:
|
||||
remote_odb = repo.cloud.get_remote_odb(remote, "gc -c")
|
||||
removed = ogc(remote_odb, obj_ids, jobs=jobs)
|
||||
if removed:
|
||||
get_index(remote_odb).clear()
|
||||
else:
|
||||
logger.info("No unused cache to remove from remote.")
|
||||
|
||||
+27
-53
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Union
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils import resolve_output
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
@@ -20,64 +19,39 @@ class GetDVCFileError(DvcException):
|
||||
)
|
||||
|
||||
|
||||
def get(url, path, out=None, rev=None, jobs=None):
|
||||
import shortuuid
|
||||
|
||||
def get(url, path, out=None, rev=None, jobs=None, force=False):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.external_repo import external_repo
|
||||
from dvc.fs.callbacks import Callback
|
||||
from dvc.repo import Repo
|
||||
|
||||
out = resolve_output(path, out)
|
||||
out = resolve_output(path, out, force=force)
|
||||
|
||||
if is_valid_filename(out):
|
||||
raise GetDVCFileError()
|
||||
|
||||
# Creating a directory right beside the output to make sure that they
|
||||
# are on the same filesystem, so we could take the advantage of
|
||||
# reflink and/or hardlink. Not using tempfile.TemporaryDirectory
|
||||
# because it will create a symlink to tmpfs, which defeats the purpose
|
||||
# and won't work with reflink/hardlink.
|
||||
dpath = os.path.dirname(os.path.abspath(out))
|
||||
tmp_dir = os.path.join(dpath, "." + str(shortuuid.uuid()))
|
||||
with Repo.open(
|
||||
url=url,
|
||||
rev=rev,
|
||||
subrepos=True,
|
||||
uninitialized=True,
|
||||
) as repo:
|
||||
from dvc.fs.data import DataFileSystem
|
||||
|
||||
# Try any links possible to avoid data duplication.
|
||||
#
|
||||
# Not using symlink, because we need to remove cache after we
|
||||
# are done, and to make that work we would have to copy data
|
||||
# over anyway before removing the cache, so we might just copy
|
||||
# it right away.
|
||||
#
|
||||
# Also, we can't use theoretical "move" link type here, because
|
||||
# the same cache file might be used a few times in a directory.
|
||||
cache_types = ["reflink", "hardlink", "copy"]
|
||||
try:
|
||||
with external_repo(
|
||||
url=url,
|
||||
rev=rev,
|
||||
subrepos=True,
|
||||
uninitialized=True,
|
||||
cache_dir=tmp_dir,
|
||||
cache_types=cache_types,
|
||||
) as repo:
|
||||
from dvc.fs.data import DataFileSystem
|
||||
fs: Union[DataFileSystem, "DVCFileSystem"]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=repo.index.data["local"])
|
||||
fs_path = fs.from_os_path(path)
|
||||
else:
|
||||
fs = repo.dvcfs
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
fs: Union[DataFileSystem, "DVCFileSystem"]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=repo.index.data["local"])
|
||||
fs_path = fs.from_os_path(path)
|
||||
else:
|
||||
fs = repo.dvcfs
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Downloading {fs.path.name(path)}",
|
||||
unit="files",
|
||||
) as cb:
|
||||
fs.get(
|
||||
fs_path,
|
||||
os.path.abspath(out),
|
||||
batch_size=jobs,
|
||||
callback=cb,
|
||||
)
|
||||
finally:
|
||||
remove(tmp_dir)
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Downloading {fs.path.name(path)}",
|
||||
unit="files",
|
||||
) as cb:
|
||||
fs.get(
|
||||
fs_path,
|
||||
os.path.abspath(out),
|
||||
batch_size=jobs,
|
||||
callback=cb,
|
||||
)
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ from dvc.fs import download, parse_external_url
|
||||
from dvc.utils import resolve_output
|
||||
|
||||
|
||||
def get_url(url, out=None, *, config=None, jobs=None):
|
||||
out = resolve_output(url, out)
|
||||
def get_url(url, out=None, *, config=None, jobs=None, force=False):
|
||||
out = resolve_output(url, out, force=force)
|
||||
out = os.path.abspath(out)
|
||||
(out,) = output.loads_from(None, [out], use_cache=False)
|
||||
|
||||
|
||||
+5
-3
@@ -30,15 +30,17 @@ def imp_url( # noqa: C901, PLR0913
|
||||
labels=None,
|
||||
meta=None,
|
||||
jobs=None,
|
||||
force=False,
|
||||
fs_config=None,
|
||||
version_aware: bool = False,
|
||||
):
|
||||
out = resolve_output(url, out)
|
||||
out = resolve_output(url, out, force=force)
|
||||
path, wdir, out = resolve_paths(self, out, always_local=to_remote and not out)
|
||||
|
||||
if to_remote and (no_exec or no_download):
|
||||
if to_remote and (no_exec or no_download or version_aware):
|
||||
raise InvalidArgumentError(
|
||||
"--no-exec/--no-download cannot be combined with --to-remote"
|
||||
"--no-exec/--no-download/--version-aware cannot be combined with "
|
||||
"--to-remote"
|
||||
)
|
||||
|
||||
if not to_remote and remote:
|
||||
|
||||
@@ -47,7 +47,7 @@ def _collect_metrics(repo, targets, recursive):
|
||||
|
||||
|
||||
def _extract_metrics(metrics, path, rev):
|
||||
if isinstance(metrics, (int, float)):
|
||||
if isinstance(metrics, (int, float, str)):
|
||||
return metrics
|
||||
|
||||
if not isinstance(metrics, dict):
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import tempfile
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Collection, Dict, Iterator, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, Iterator, Optional, Tuple
|
||||
|
||||
from funcy import retry, wrap_with
|
||||
|
||||
@@ -13,21 +13,17 @@ from dvc.scm import CloneError, map_scm_exception
|
||||
from dvc.utils import relpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
from .scm import Git
|
||||
from dvc.scm import Git
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
@map_scm_exception()
|
||||
def external_repo(
|
||||
def _external_repo(
|
||||
url,
|
||||
rev: Optional[str] = None,
|
||||
for_write: bool = False,
|
||||
cache_dir: Optional["StrPath"] = None,
|
||||
cache_types: Optional[Collection[str]] = None,
|
||||
**kwargs,
|
||||
) -> Iterator["Repo"]:
|
||||
logger.debug("Creating external repo %s@%s", url, rev)
|
||||
@@ -37,12 +33,8 @@ def external_repo(
|
||||
# the tip of the default branch
|
||||
rev = rev or "refs/remotes/origin/HEAD"
|
||||
|
||||
cache_config = {
|
||||
"cache": {"dir": cache_dir or _get_cache_dir(url), "type": cache_types}
|
||||
}
|
||||
|
||||
config = _get_remote_config(url) if os.path.isdir(url) else {}
|
||||
config.update(cache_config)
|
||||
config.update({"cache": {"dir": _get_cache_dir(url)}})
|
||||
config.update(kwargs.pop("config", None) or {})
|
||||
|
||||
main_root = "/"
|
||||
@@ -55,7 +47,7 @@ def external_repo(
|
||||
root_dir=path,
|
||||
url=url,
|
||||
config=config,
|
||||
repo_factory=erepo_factory(url, main_root, cache_config),
|
||||
repo_factory=erepo_factory(url, main_root, {"cache": config["cache"]}),
|
||||
rev=rev,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -70,6 +62,22 @@ def external_repo(
|
||||
_remove(path)
|
||||
|
||||
|
||||
def open_repo(url, *args, **kwargs):
|
||||
if url is None:
|
||||
url = os.getcwd()
|
||||
|
||||
if os.path.exists(url):
|
||||
try:
|
||||
config = _get_remote_config(url)
|
||||
config.update(kwargs.get("config") or {})
|
||||
kwargs["config"] = config
|
||||
return Repo(url, *args, **kwargs)
|
||||
except NotDvcRepoError:
|
||||
pass # fallthrough to _external_repo
|
||||
|
||||
return _external_repo(url, *args, **kwargs)
|
||||
|
||||
|
||||
def erepo_factory(url, root_dir, cache_config):
|
||||
from dvc.fs import localfs
|
||||
|
||||
+33
-26
@@ -121,51 +121,58 @@ class Plots:
|
||||
|
||||
}
|
||||
"""
|
||||
from dvc.repo.experiments.brancher import switch_repo
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
targets = ensure_list(targets)
|
||||
targets = [self.repo.dvcfs.from_os_path(target) for target in targets]
|
||||
|
||||
for rev in self.repo.brancher(revs=revs):
|
||||
# .brancher() adds unwanted workspace
|
||||
if revs is not None and rev not in revs:
|
||||
continue
|
||||
rev = rev or "workspace"
|
||||
|
||||
res: Dict = {}
|
||||
definitions = _collect_definitions(
|
||||
self.repo,
|
||||
targets=targets,
|
||||
revision=rev,
|
||||
onerror=onerror,
|
||||
props=props,
|
||||
)
|
||||
if definitions:
|
||||
res[rev] = {"definitions": definitions}
|
||||
|
||||
data_targets = _get_data_targets(definitions)
|
||||
|
||||
res[rev]["sources"] = self._collect_data_sources(
|
||||
targets=data_targets,
|
||||
recursive=recursive,
|
||||
props=props,
|
||||
if revs is None:
|
||||
revs = ["workspace"]
|
||||
else:
|
||||
revs = list(revs)
|
||||
if "workspace" in revs:
|
||||
# reorder revs to match repo.brancher ordering
|
||||
revs.remove("workspace")
|
||||
revs = ["workspace"] + revs
|
||||
for rev in revs:
|
||||
with switch_repo(self.repo, rev) as (repo, _):
|
||||
res: Dict = {}
|
||||
definitions = _collect_definitions(
|
||||
repo,
|
||||
targets=targets,
|
||||
revision=rev,
|
||||
onerror=onerror,
|
||||
props=props,
|
||||
)
|
||||
yield res
|
||||
if definitions:
|
||||
res[rev] = {"definitions": definitions}
|
||||
|
||||
data_targets = _get_data_targets(definitions)
|
||||
|
||||
res[rev]["sources"] = self._collect_data_sources(
|
||||
repo,
|
||||
targets=data_targets,
|
||||
recursive=recursive,
|
||||
props=props,
|
||||
onerror=onerror,
|
||||
)
|
||||
yield res
|
||||
|
||||
@error_handler
|
||||
def _collect_data_sources(
|
||||
self,
|
||||
repo: "Repo",
|
||||
targets: Optional[List[str]] = None,
|
||||
recursive: bool = False,
|
||||
props: Optional[Dict] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
):
|
||||
fs = self.repo.dvcfs
|
||||
fs = repo.dvcfs
|
||||
|
||||
props = props or {}
|
||||
|
||||
plots = _collect_plots(self.repo, targets, recursive)
|
||||
plots = _collect_plots(repo, targets, recursive)
|
||||
res: Dict[str, Any] = {}
|
||||
for fs_path, rev_props in plots.items():
|
||||
joined_props = {**rev_props, **props}
|
||||
|
||||
+16
-18
@@ -3,9 +3,8 @@ from functools import partial
|
||||
from typing import TYPE_CHECKING, Iterator, List
|
||||
|
||||
from dvc.exceptions import DvcException, ReproductionError
|
||||
from dvc.repo.metrics.show import _collect_top_level_metrics
|
||||
from dvc.repo.params.show import _collect_top_level_params
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.stage.cache import RunCacheNotSupported
|
||||
from dvc.stage.exceptions import CheckpointKilledError
|
||||
|
||||
from . import locked
|
||||
@@ -76,20 +75,9 @@ def _track_stage(stage: "Stage") -> None:
|
||||
return context.track_changed_files()
|
||||
|
||||
|
||||
def _track_top_level(repo: "Repo") -> None:
|
||||
context = repo.scm_context
|
||||
for metric_file in _collect_top_level_metrics(repo):
|
||||
context.track_file(metric_file)
|
||||
for param_file in _collect_top_level_params(repo):
|
||||
context.track_file(param_file)
|
||||
for plot_file in repo.index._plot_sources: # pylint: disable=protected-access
|
||||
context.track_file(plot_file)
|
||||
context.track_changed_files()
|
||||
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def reproduce( # noqa: C901
|
||||
def reproduce( # noqa: C901, PLR0912
|
||||
self: "Repo",
|
||||
targets=None,
|
||||
recursive=False,
|
||||
@@ -138,13 +126,23 @@ def reproduce( # noqa: C901
|
||||
)
|
||||
)
|
||||
|
||||
result = _reproduce_stages(self.index.graph, list(stages), **kwargs)
|
||||
_track_top_level(self)
|
||||
return result
|
||||
if kwargs.get("pull", False) and kwargs.get("run_cache", True):
|
||||
logger.debug("Pulling run cache")
|
||||
try:
|
||||
self.stage_cache.pull(None)
|
||||
except RunCacheNotSupported as e:
|
||||
logger.warning("Failed to pull run cache: %s", e)
|
||||
|
||||
return _reproduce_stages(self.index.graph, list(stages), **kwargs)
|
||||
|
||||
|
||||
def _reproduce_stages( # noqa: C901
|
||||
graph, stages, downstream=False, single_item=False, on_unchanged=None, **kwargs
|
||||
graph,
|
||||
stages,
|
||||
downstream=False,
|
||||
single_item=False,
|
||||
on_unchanged=None,
|
||||
**kwargs,
|
||||
):
|
||||
r"""Derive the evaluation of the given node for the given graph.
|
||||
|
||||
|
||||
+9
-10
@@ -122,16 +122,15 @@ class StageLoad:
|
||||
force=force,
|
||||
**stage_data,
|
||||
)
|
||||
with self.repo.scm_context:
|
||||
stage.dump(update_lock=update_lock)
|
||||
try:
|
||||
stage.ignore_outs()
|
||||
except FileNotFoundError as exc:
|
||||
ui.warn(
|
||||
f"Could not create .gitignore entry in {exc.filename}."
|
||||
" DVC will attempt to create .gitignore entry again when"
|
||||
" the stage is run."
|
||||
)
|
||||
stage.dump(update_lock=update_lock)
|
||||
try:
|
||||
stage.ignore_outs()
|
||||
except FileNotFoundError as exc:
|
||||
ui.warn(
|
||||
f"Could not create .gitignore entry in {exc.filename}."
|
||||
" DVC will attempt to create .gitignore entry again when"
|
||||
" the stage is run."
|
||||
)
|
||||
|
||||
return stage
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ def push_worktree(
|
||||
) as cb:
|
||||
cb.set_size(total)
|
||||
try:
|
||||
pushed += checkout(
|
||||
stats = checkout(
|
||||
new_index,
|
||||
remote_obj.path,
|
||||
remote_obj.fs,
|
||||
@@ -184,6 +184,7 @@ def push_worktree(
|
||||
jobs=jobs,
|
||||
**diff_kwargs,
|
||||
)
|
||||
pushed += sum(len(changes) for changes in stats.values())
|
||||
except VersioningNotSupported:
|
||||
logger.exception("")
|
||||
raise DvcException(
|
||||
|
||||
+1
-2
@@ -116,7 +116,6 @@ PLOT_DEFINITION = {
|
||||
SINGLE_PLOT_SCHEMA = {str: Any(PLOT_DEFINITION, None)}
|
||||
ARTIFACTS = "artifacts"
|
||||
SINGLE_ARTIFACT_SCHEMA = Schema({str: ARTIFACT_SCHEMA})
|
||||
ARTIFACTS_SCHEMA = Any(str, SINGLE_ARTIFACT_SCHEMA)
|
||||
FOREACH_IN = {
|
||||
Required(FOREACH_KWD): Any(dict, list, str),
|
||||
Required(DO_KWD): STAGE_DEFINITION,
|
||||
@@ -130,7 +129,7 @@ MULTI_STAGE_SCHEMA = {
|
||||
VARS_KWD: VARS_SCHEMA,
|
||||
StageParams.PARAM_PARAMS: [str],
|
||||
StageParams.PARAM_METRICS: [str],
|
||||
ARTIFACTS: ARTIFACTS_SCHEMA,
|
||||
ARTIFACTS: SINGLE_ARTIFACT_SCHEMA,
|
||||
}
|
||||
|
||||
COMPILED_SINGLE_STAGE_SCHEMA = Schema(SINGLE_STAGE_SCHEMA)
|
||||
|
||||
+66
-31
@@ -319,20 +319,22 @@ class Stage(params.StageParams):
|
||||
env.update(current)
|
||||
return env
|
||||
|
||||
def changed_deps(self) -> bool:
|
||||
def changed_deps(self, allow_missing: bool = False) -> bool:
|
||||
if self.frozen:
|
||||
return False
|
||||
|
||||
if self.is_callback or self.always_changed or self.is_checkpoint:
|
||||
return True
|
||||
|
||||
return self._changed_deps()
|
||||
return self._changed_deps(allow_missing=allow_missing)
|
||||
|
||||
@rwlocked(read=["deps"])
|
||||
def _changed_deps(self) -> bool:
|
||||
def _changed_deps(self, allow_missing: bool = False) -> bool:
|
||||
for dep in self.deps:
|
||||
status = dep.status()
|
||||
if status:
|
||||
if allow_missing and status[str(dep)] == "deleted":
|
||||
continue
|
||||
logger.debug(
|
||||
"Dependency '%s' of %s changed because it is '%s'.",
|
||||
dep,
|
||||
@@ -343,10 +345,12 @@ class Stage(params.StageParams):
|
||||
return False
|
||||
|
||||
@rwlocked(read=["outs"])
|
||||
def changed_outs(self) -> bool:
|
||||
def changed_outs(self, allow_missing: bool = False) -> bool:
|
||||
for out in self.outs:
|
||||
status = out.status()
|
||||
if status:
|
||||
if allow_missing and status[str(out)] == "not in cache":
|
||||
continue
|
||||
logger.debug(
|
||||
"Output '%s' of %s changed because it is '%s'.",
|
||||
out,
|
||||
@@ -364,13 +368,13 @@ class Stage(params.StageParams):
|
||||
return changed
|
||||
|
||||
@rwlocked(read=["deps", "outs"])
|
||||
def changed(self) -> bool:
|
||||
def changed(self, allow_missing: bool = False) -> bool:
|
||||
is_changed = (
|
||||
# Short-circuit order: stage md5 is fast,
|
||||
# deps are expected to change
|
||||
self.changed_stage()
|
||||
or self.changed_deps()
|
||||
or self.changed_outs()
|
||||
or self.changed_deps(allow_missing=allow_missing)
|
||||
or self.changed_outs(allow_missing=allow_missing)
|
||||
)
|
||||
if is_changed:
|
||||
logger.debug("%s changed.", self)
|
||||
@@ -416,12 +420,15 @@ class Stage(params.StageParams):
|
||||
(out,) = self.outs
|
||||
out.transfer(source, odb=odb, jobs=kwargs.get("jobs"))
|
||||
if not to_remote:
|
||||
out.checkout()
|
||||
out.checkout(force=kwargs.get("force"))
|
||||
out.ignore()
|
||||
|
||||
@rwlocked(read=["deps"], write=["outs"])
|
||||
def reproduce(self, interactive=False, **kwargs) -> Optional["Stage"]:
|
||||
if not (kwargs.get("force", False) or self.changed()):
|
||||
if not (
|
||||
kwargs.get("force", False)
|
||||
or self.changed(kwargs.get("allow_missing", False))
|
||||
):
|
||||
if not isinstance(self, PipelineStage) and self.is_data_source:
|
||||
logger.info("'%s' didn't change, skipping", self.addressing)
|
||||
else:
|
||||
@@ -495,34 +502,34 @@ class Stage(params.StageParams):
|
||||
if not allow_missing:
|
||||
raise
|
||||
|
||||
def save_outs(self, allow_missing: bool = False):
|
||||
from dvc.output import OutputDoesNotExistError
|
||||
|
||||
def get_versioned_outs(self) -> Dict[str, "Output"]:
|
||||
from .exceptions import StageFileDoesNotExistError, StageNotFound
|
||||
|
||||
try:
|
||||
old = self.reload()
|
||||
old_outs = {out.def_path: out for out in old.outs}
|
||||
merge_versioned = any(
|
||||
(
|
||||
out.files is not None
|
||||
or (out.meta is not None and out.meta.version_id is not None)
|
||||
)
|
||||
for out in old_outs.values()
|
||||
)
|
||||
except (StageFileDoesNotExistError, StageNotFound):
|
||||
merge_versioned = False
|
||||
return {}
|
||||
|
||||
return {
|
||||
out.def_path: out
|
||||
for out in old.outs
|
||||
if out.files is not None
|
||||
or (out.meta is not None and out.meta.version_id is not None)
|
||||
}
|
||||
|
||||
def save_outs(self, allow_missing: bool = False):
|
||||
from dvc.output import OutputDoesNotExistError
|
||||
|
||||
old_versioned_outs = self.get_versioned_outs()
|
||||
for out in self.outs:
|
||||
try:
|
||||
out.save()
|
||||
except OutputDoesNotExistError:
|
||||
if not (allow_missing or out.checkpoint):
|
||||
raise
|
||||
if merge_versioned:
|
||||
old_out = old_outs.get(out.def_path)
|
||||
if old_out is not None:
|
||||
out.merge_version_meta(old_out)
|
||||
|
||||
if old_out := old_versioned_outs.get(out.def_path):
|
||||
out.merge_version_meta(old_out)
|
||||
|
||||
def ignore_outs(self) -> None:
|
||||
for out in self.outs:
|
||||
@@ -560,8 +567,31 @@ class Stage(params.StageParams):
|
||||
if link_failures:
|
||||
raise CacheLinkError(link_failures)
|
||||
|
||||
@rwlocked(write=["outs"])
|
||||
def add_outs( # noqa: C901
|
||||
self, filter_info=None, allow_missing: bool = False, **kwargs
|
||||
):
|
||||
from dvc.output import OutputDoesNotExistError
|
||||
|
||||
link_failures = []
|
||||
old_versioned_outs = self.get_versioned_outs()
|
||||
for out in self.filter_outs(filter_info):
|
||||
try:
|
||||
out.add(filter_info, **kwargs)
|
||||
except (FileNotFoundError, OutputDoesNotExistError):
|
||||
if not (allow_missing or out.checkpoint):
|
||||
raise
|
||||
except CacheLinkError:
|
||||
link_failures.append(filter_info or out.fs_path)
|
||||
|
||||
if old_out := old_versioned_outs.get(out.def_path):
|
||||
out.merge_version_meta(old_out)
|
||||
|
||||
if link_failures:
|
||||
raise CacheLinkError(link_failures)
|
||||
|
||||
@rwlocked(read=["deps", "outs"])
|
||||
def run(
|
||||
def run( # noqa: C901
|
||||
self,
|
||||
dry=False,
|
||||
no_commit=False,
|
||||
@@ -573,15 +603,20 @@ class Stage(params.StageParams):
|
||||
if (self.cmd or self.is_import) and not self.frozen and not dry:
|
||||
self.remove_outs(ignore_remove=False, force=False)
|
||||
|
||||
if (not self.frozen and self.is_import) or self.is_partial_import:
|
||||
if (
|
||||
self.is_import and (not self.frozen or kwargs.get("pull"))
|
||||
) or self.is_partial_import:
|
||||
self._sync_import(dry, force, kwargs.get("jobs", None), no_download)
|
||||
elif not self.frozen and self.cmd:
|
||||
self._run_stage(dry, force, **kwargs)
|
||||
else:
|
||||
self._run_stage(dry, force, allow_missing=allow_missing, **kwargs)
|
||||
elif kwargs.get("pull"):
|
||||
logger.info("Pulling data for %s", self)
|
||||
self.repo.pull(self.addressing, jobs=kwargs.get("jobs", None))
|
||||
self.checkout()
|
||||
elif not dry:
|
||||
args = ("outputs", "frozen ") if self.frozen else ("data sources", "")
|
||||
logger.info("Verifying %s in %s%s", *args, self)
|
||||
if not dry:
|
||||
self._check_missing_outputs()
|
||||
self._check_missing_outputs()
|
||||
|
||||
if not dry:
|
||||
if kwargs.get("checkpoint_func", None) or no_download:
|
||||
|
||||
+16
-1
@@ -133,10 +133,25 @@ def cmd_run(stage, dry=False, checkpoint_func=None, run_env=None):
|
||||
_run(stage, executable, cmd, checkpoint_func=checkpoint_func, **kwargs)
|
||||
|
||||
|
||||
def _pull_missing_deps(stage):
|
||||
for dep in stage.deps:
|
||||
if not dep.exists:
|
||||
stage.repo.pull(dep.def_path)
|
||||
|
||||
|
||||
def run_stage(
|
||||
stage, dry=False, force=False, checkpoint_func=None, run_env=None, **kwargs
|
||||
stage,
|
||||
dry=False,
|
||||
force=False,
|
||||
checkpoint_func=None,
|
||||
run_env=None,
|
||||
allow_missing: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
if not (force or checkpoint_func):
|
||||
if allow_missing and kwargs.get("pull") and not dry:
|
||||
_pull_missing_deps(stage)
|
||||
|
||||
from .cache import RunCacheNotFoundError
|
||||
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
DVC pytest plugin
|
||||
|
||||
dvc.testing.benchmarks
|
||||
======================
|
||||
|
||||
Benchmark test definitions are now part of ``dvc.testing``.
|
||||
For CLI usage and `bench.dvc.org <https://bench.dvc.org>`_ details see `dvc-bench <https://github.com/iterative/dvc-bench>`_.
|
||||
|
||||
``dvc.testing.benchmarks`` structure:
|
||||
|
||||
* cli: should be able to run these with any dvc (rpm, deb, pypi, snap, etc) (could be used in dvc-test repo too)
|
||||
|
||||
* commands: granular tests for individual commands. These should have a cached setup, so that we could use them during rapid development instead of our hand-written scripts. Every test could be run in a separate machine.
|
||||
* stories: multistage start-to-end benchmarks, useful for testing workflows (e.g. in documentation, see test_sharing inspired by `Storing and sharing <https://dvc.org/doc/start/data-management/data-versioning#storing-and-sharing>`_. Every full story could be run in a separate machine.
|
||||
|
||||
* api: for python api only.
|
||||
|
||||
* methods: granular tests for individual methods (e.g. ``api.open/read``). Same reasoning as in ``cli.commands``
|
||||
* stories: same as ``cli.stories`` but for our api. E.g. imagine using our api with pandas or smth like that.
|
||||
@@ -0,0 +1,3 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_add(bench_dvc, tmp_dir, dvc, dataset):
|
||||
bench_dvc("add", dataset)
|
||||
@@ -0,0 +1,39 @@
|
||||
# pylint: disable=unused-argument
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.fs import localfs
|
||||
from dvc_objects.fs import generic
|
||||
from dvc_objects.fs.utils import tmp_fname
|
||||
|
||||
|
||||
def _skip_unsupported_link(src, dest, link_type):
|
||||
src_test_file = os.path.join(src, tmp_fname())
|
||||
dest_test_file = os.path.join(dest, tmp_fname())
|
||||
if not generic.test_links(
|
||||
[link_type], localfs, src_test_file, localfs, dest_test_file
|
||||
):
|
||||
pytest.skip(f"{link_type} not supported")
|
||||
|
||||
|
||||
def generate_test(*, link_type="copy"):
|
||||
def _test_checkout_func(bench_dvc, tmp_dir, dvc, make_dataset):
|
||||
dataset = make_dataset(dvcfile=True, cache=True, files=False)
|
||||
|
||||
_skip_unsupported_link((tmp_dir / ".dvc" / "cache"), tmp_dir, link_type)
|
||||
|
||||
with dvc.config.edit() as conf:
|
||||
conf["cache"]["type"] = link_type
|
||||
|
||||
bench_dvc("checkout", dataset)
|
||||
bench_dvc("checkout", name="noop")
|
||||
(dataset / "new").write_text("new")
|
||||
bench_dvc("checkout", "--force", name="update")
|
||||
|
||||
return _test_checkout_func
|
||||
|
||||
|
||||
test_checkout_copy = generate_test(link_type="copy")
|
||||
test_checkout_symlink = generate_test(link_type="symlink")
|
||||
test_checkout_hardlink = generate_test(link_type="hardlink")
|
||||
@@ -0,0 +1,49 @@
|
||||
# pylint: disable=unused-argument
|
||||
from shutil import rmtree
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.requires(minversion=(2, 15, 0), reason="new command")
|
||||
|
||||
|
||||
def test_data_status(
|
||||
bench_dvc,
|
||||
tmp_dir,
|
||||
scm,
|
||||
dvc,
|
||||
make_dataset,
|
||||
):
|
||||
args = ("data", "status")
|
||||
dataset = make_dataset(cache=True, files=True, dvcfile=True, commit=False)
|
||||
rmtree(dvc.tmp_dir)
|
||||
|
||||
bench_dvc(*args, name="new")
|
||||
bench_dvc(*args, name="noop")
|
||||
|
||||
tmp_dir.scm_add(dataset.with_suffix(".dvc").name, commit="add dataset")
|
||||
|
||||
(dataset / "new").write_text("new")
|
||||
bench_dvc(*args, name="changed")
|
||||
bench_dvc(*args, name="changed-noop")
|
||||
|
||||
|
||||
def test_data_status_all_flags(bench_dvc, tmp_dir, scm, dvc, make_dataset):
|
||||
args = (
|
||||
"data",
|
||||
"status",
|
||||
"--granular",
|
||||
"--unchanged",
|
||||
"--untracked-files",
|
||||
"--json",
|
||||
)
|
||||
dataset = make_dataset(cache=True, files=True, dvcfile=True, commit=False)
|
||||
rmtree(dvc.tmp_dir)
|
||||
|
||||
bench_dvc(*args, name="new")
|
||||
bench_dvc(*args, name="noop")
|
||||
|
||||
tmp_dir.scm_add(dataset.with_suffix(".dvc").name, commit="add dataset")
|
||||
|
||||
(dataset / "new").write_text("new")
|
||||
bench_dvc(*args, name="changed")
|
||||
bench_dvc(*args, name="changed-noop")
|
||||
@@ -0,0 +1,9 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_diff(bench_dvc, tmp_dir, scm, dvc, make_dataset):
|
||||
dataset = make_dataset(cache=True, files=True, dvcfile=True, commit=True)
|
||||
bench_dvc("diff")
|
||||
bench_dvc("diff", name="noop")
|
||||
|
||||
(dataset / "new").write_text("new")
|
||||
bench_dvc("diff", name="changed")
|
||||
bench_dvc("diff", name="changed-noop")
|
||||
@@ -0,0 +1,14 @@
|
||||
# pylint: disable=unused-argument
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.requires(minversion=(2, 10, 0))
|
||||
def test_exp_show(make_project, monkeypatch, bench_dvc, dvc_bin):
|
||||
url = "https://github.com/efiop/lstm_seq2seq"
|
||||
rev = "dvc-bench"
|
||||
path = make_project(url, rev=rev)
|
||||
monkeypatch.chdir(path)
|
||||
|
||||
dvc_bin("exp", "pull", "-A", "--no-cache", "origin")
|
||||
|
||||
bench_dvc("exp", "show", "-A", "--no-pager")
|
||||
@@ -0,0 +1,4 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_gc(bench_dvc, tmp_dir, dvc, make_dataset):
|
||||
make_dataset(files=False, cache=True, dvcfile=False)
|
||||
bench_dvc("gc", "-f", "-w")
|
||||
@@ -0,0 +1,6 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_get(bench_dvc, tmp_dir, scm, dvc, make_dataset, remote):
|
||||
dataset = make_dataset(
|
||||
cache=False, files=False, dvcfile=True, commit=True, remote=True
|
||||
)
|
||||
bench_dvc("get", tmp_dir, dataset.name, "-o", "new")
|
||||
@@ -0,0 +1,6 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_get_url(bench_dvc, tmp_dir, scm, dvc, make_dataset):
|
||||
dataset = make_dataset(
|
||||
cache=False, files=True, dvcfile=False, commit=False, remote=False
|
||||
)
|
||||
bench_dvc("get-url", str(dataset), "new")
|
||||
@@ -0,0 +1,2 @@
|
||||
def test_help(bench_dvc):
|
||||
bench_dvc("--help", rounds=100)
|
||||
@@ -0,0 +1,6 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_import(bench_dvc, tmp_dir, scm, dvc, make_dataset, remote):
|
||||
dataset = make_dataset(
|
||||
cache=False, files=False, dvcfile=True, commit=True, remote=True
|
||||
)
|
||||
bench_dvc("import", tmp_dir, dataset.name, "-o", "new")
|
||||
@@ -0,0 +1,6 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_import_url(bench_dvc, tmp_dir, scm, dvc, make_dataset):
|
||||
dataset = make_dataset(
|
||||
cache=False, files=True, dvcfile=False, commit=False, remote=False
|
||||
)
|
||||
bench_dvc("import-url", str(dataset), "new")
|
||||
@@ -0,0 +1,14 @@
|
||||
# pylint: disable=unused-argument
|
||||
import shutil
|
||||
|
||||
|
||||
def test_init(bench_dvc, tmp_dir, scm):
|
||||
def _cleanup_dir():
|
||||
for item in tmp_dir.iterdir():
|
||||
if item.is_dir():
|
||||
if item.name != ".git":
|
||||
shutil.rmtree(item)
|
||||
else:
|
||||
item.unlink()
|
||||
|
||||
bench_dvc("init", setup=_cleanup_dir, rounds=100, warmup_rounds=1)
|
||||
@@ -0,0 +1,7 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_list(bench_dvc, tmp_dir, scm, dvc, make_dataset, remote):
|
||||
make_dataset(cache=False, files=False, dvcfile=True, commit=True, remote=True)
|
||||
bench_dvc("list", tmp_dir)
|
||||
bench_dvc("list", tmp_dir, "--dvc-only", name="dvc-only")
|
||||
bench_dvc("list", tmp_dir, "--recursive", name="recursive")
|
||||
bench_dvc("list", tmp_dir, "dataset", name="shallow")
|
||||
@@ -0,0 +1,20 @@
|
||||
import pytest
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.testing.benchmarks.fixtures import _pull
|
||||
|
||||
# pylint: disable=redefined-outer-name,unused-argument
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
||||
@pytest.mark.requires(minversion=(2, 34, 0), reason="top-level plots not supported")
|
||||
def test_plots(project, bench_dvc):
|
||||
with Repo() as dvc:
|
||||
_pull(dvc)
|
||||
|
||||
kwargs = {"rounds": 5, "iterations": 3, "warmup_rounds": 2}
|
||||
bench_dvc("plots", "show", name="show", **kwargs)
|
||||
bench_dvc("plots", "show", "--json", name="show-json", **kwargs)
|
||||
bench_dvc("plots", "diff", "HEAD", name="diff", **kwargs)
|
||||
bench_dvc("plots", "diff", "HEAD", "--json", name="diff-json", **kwargs)
|
||||
@@ -0,0 +1,4 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_pull(bench_dvc, tmp_dir, dvc, make_dataset, remote):
|
||||
make_dataset(cache=False, dvcfile=True, files=False, remote=True)
|
||||
bench_dvc("pull")
|
||||
@@ -0,0 +1,4 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_push(bench_dvc, tmp_dir, dvc, make_dataset, remote):
|
||||
make_dataset(cache=True, dvcfile=True, files=False)
|
||||
bench_dvc("push")
|
||||
@@ -0,0 +1,9 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_status(bench_dvc, tmp_dir, dvc, make_dataset):
|
||||
dataset = make_dataset(files=True, dvcfile=True, cache=True)
|
||||
bench_dvc("status")
|
||||
bench_dvc("status", name="noop")
|
||||
|
||||
(dataset / "new").write_text("new")
|
||||
bench_dvc("status", name="changed")
|
||||
bench_dvc("status", name="changed-noop")
|
||||
@@ -0,0 +1,9 @@
|
||||
# pylint: disable=unused-argument
|
||||
def test_update(bench_dvc, tmp_dir, dvc, make_dataset):
|
||||
dataset = make_dataset(
|
||||
cache=False, files=True, dvcfile=False, commit=False, remote=False
|
||||
)
|
||||
dvc.imp_url(str(dataset), "new")
|
||||
(dataset / "new").write_text("new")
|
||||
bench_dvc("update", "new.dvc")
|
||||
bench_dvc("update", "new.dvc", name="noop")
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Tests for modifications to an existing dataset.
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
# pylint: disable=unused-argument,unexpected-keyword-arg
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires 3.10 glob.glob")
|
||||
def test_partial_add(bench_dvc, tmp_dir, dvc, dataset, remote):
|
||||
random.seed(4231)
|
||||
# Move some files to create a partial dataset
|
||||
os.makedirs("partial-copy")
|
||||
for f in glob.glob("*", root_dir=dataset, recursive=True): # type: ignore[call-arg]
|
||||
if random.random() > 0.5: # noqa: S311 # nosec
|
||||
shutil.move(dataset / f, tmp_dir / "partial-copy" / f)
|
||||
|
||||
# Add/push partial dataset
|
||||
dvc.add(str(dataset))
|
||||
dvc.push()
|
||||
|
||||
# Add more files to the dataset
|
||||
shutil.copytree("partial-copy", dataset, dirs_exist_ok=True)
|
||||
|
||||
# Benchmark operations for adding files to a dataset
|
||||
bench_dvc("add", dataset, name="partial-add")
|
||||
bench_dvc("push", name="partial-add")
|
||||
bench_dvc("gc", "-f", "-w", name="noop")
|
||||
bench_dvc("gc", "-f", "-w", "-c", name="cloud-noop")
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires 3.10 glob.glob")
|
||||
def test_partial_remove(bench_dvc, tmp_dir, dvc, dataset, remote):
|
||||
random.seed(5232)
|
||||
# Add/push full dataset
|
||||
dvc.add(str(dataset))
|
||||
dvc.push()
|
||||
|
||||
# Remove some files
|
||||
for f in glob.glob("*", root_dir=dataset, recursive=True): # type: ignore[call-arg]
|
||||
if random.random() > 0.5: # noqa: S311 # nosec
|
||||
if os.path.isfile(dataset / f):
|
||||
os.remove(dataset / f)
|
||||
elif os.path.isdir(dataset / f):
|
||||
shutil.rmtree(dataset / f)
|
||||
|
||||
# Benchmark operations for removing files from dataset
|
||||
bench_dvc("add", dataset)
|
||||
bench_dvc("push")
|
||||
bench_dvc("gc", "-f", "-w")
|
||||
bench_dvc("gc", "-f", "-w", "-c", name="cloud")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user