mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ee4f137f2 | ||
|
|
a750a75016 | ||
|
|
cf976f17ae | ||
|
|
7041fa5f42 | ||
|
|
d898877175 | ||
|
|
1ca1d78b38 | ||
|
|
da6ebdbf8c | ||
|
|
e995492095 | ||
|
|
276d499c9b | ||
|
|
fd4696cc50 | ||
|
|
90b9193250 | ||
|
|
77e35145a9 | ||
|
|
49c7c55ba3 | ||
|
|
f28d5ee9e0 | ||
|
|
621fe07a3b | ||
|
|
8323923ce9 | ||
|
|
04b50c5cd0 | ||
|
|
b20a59d748 | ||
|
|
bf2f08da09 | ||
|
|
1835c8c6e9 | ||
|
|
7e1bb07f10 | ||
|
|
92dc1f31f5 | ||
|
|
e16c0c8b2f | ||
|
|
33e7645603 | ||
|
|
c827654fe7 | ||
|
|
b8c666b1b7 | ||
|
|
fd73b389c6 | ||
|
|
0433565f1f | ||
|
|
b5449503f0 | ||
|
|
fa14507bc8 | ||
|
|
21458796ed | ||
|
|
a6e49a72de | ||
|
|
97348cfba7 | ||
|
|
c5e3d42899 | ||
|
|
c6c2ce130a | ||
|
|
be975ad9eb | ||
|
|
d0aa1ce776 | ||
|
|
5ec4fa473c | ||
|
|
5fd54679c7 | ||
|
|
5c1487e2ec | ||
|
|
30517f6676 | ||
|
|
bbb77360e9 | ||
|
|
1af150a5e9 | ||
|
|
cf59d801ce | ||
|
|
b733dea4c2 | ||
|
|
2ed677057d | ||
|
|
0597311439 | ||
|
|
fc8ec6aa95 | ||
|
|
3dca69b678 | ||
|
|
4c0bb8d316 | ||
|
|
1cb371d4d7 | ||
|
|
c96795dcfb | ||
|
|
8f768addf9 | ||
|
|
af1fb722c8 | ||
|
|
8b1c1a31f3 | ||
|
|
262419ef35 | ||
|
|
265fc581c0 | ||
|
|
80f414c19d | ||
|
|
2cd33e10ac | ||
|
|
603f5fdba1 | ||
|
|
528eab0418 | ||
|
|
ed664143b4 | ||
|
|
21a2504bcb | ||
|
|
d0291e8039 | ||
|
|
358c5e0c39 | ||
|
|
9d7fba5c61 | ||
|
|
a1e339b4fa | ||
|
|
6c58d438b3 | ||
|
|
0c709b6ec1 | ||
|
|
0aeb7a8662 | ||
|
|
46f3157f43 | ||
|
|
36fd69be21 | ||
|
|
3759535d43 | ||
|
|
2c4365513e | ||
|
|
26152292aa | ||
|
|
3e0f2bafbb | ||
|
|
78637713cf | ||
|
|
f9104275f3 | ||
|
|
bce3b5d2a1 | ||
|
|
6ace5ed380 | ||
|
|
6543580100 | ||
|
|
a1b891ce01 | ||
|
|
3a85326df3 | ||
|
|
355ebc72eb | ||
|
|
5a6f834088 | ||
|
|
e98bf38d4c | ||
|
|
192c9cc577 | ||
|
|
0e38525d8d | ||
|
|
8acd943d52 | ||
|
|
79c88008f8 | ||
|
|
9c9ca39fb4 | ||
|
|
89109fafcf | ||
|
|
aa19e24670 | ||
|
|
629223cbc2 | ||
|
|
96449f157c | ||
|
|
8cb2646023 | ||
|
|
25313f5d82 | ||
|
|
182728d674 | ||
|
|
f245567613 | ||
|
|
4163c8f57d | ||
|
|
6532258e4a | ||
|
|
eb45cda5d0 | ||
|
|
abd4f4a2a8 | ||
|
|
f41e535857 | ||
|
|
1989c0fe98 | ||
|
|
3ddd4b87fc | ||
|
|
e089ee0365 | ||
|
|
02f7fcae73 | ||
|
|
cdba65f27e | ||
|
|
3959c51373 | ||
|
|
90aaf5a296 | ||
|
|
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 |
@@ -1,12 +0,0 @@
|
||||
/state
|
||||
/lock
|
||||
/config.local
|
||||
/updater
|
||||
/updater.lock
|
||||
/state-journal
|
||||
/state-wal
|
||||
/cache
|
||||
/pkg
|
||||
/repos
|
||||
/tmp
|
||||
/experiments
|
||||
@@ -1,4 +0,0 @@
|
||||
['remote "dvc"']
|
||||
url = https://remote.dvc.org/dvc
|
||||
[core]
|
||||
remote = dvc
|
||||
@@ -0,0 +1,10 @@
|
||||
pull_request_rules:
|
||||
- name: backport patches to 2.x
|
||||
conditions:
|
||||
- base=main
|
||||
- label=backport
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- "2.x"
|
||||
title: ({{ destination_branch }}) {{ title }}
|
||||
@@ -2,7 +2,7 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ["main", "2.x"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
|
||||
@@ -11,68 +11,10 @@ permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
permissions:
|
||||
contents: write # for actions/upload-release-asset to upload release asset
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- {pkg: "deb", os: "ubuntu-20.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-20.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-11", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
|
||||
|
||||
name: ${{ matrix.pkg }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
pyproject.toml
|
||||
scripts/build-requirements.txt
|
||||
|
||||
- name: Set up Ruby 2.6
|
||||
uses: ruby/setup-ruby@v1
|
||||
if: matrix.pkg != 'exe'
|
||||
with:
|
||||
ruby-version: '2.6'
|
||||
|
||||
- name: Install fpm
|
||||
if: matrix.pkg != 'exe'
|
||||
run: gem install --no-document fpm
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
|
||||
- name: Pull images
|
||||
run: dvc pull
|
||||
|
||||
- name: Build ${{ matrix.pkg }}
|
||||
run: python scripts/build.py ${{ matrix.pkg }}
|
||||
|
||||
- name: Publish ${{ matrix.pkg }}
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/${{ matrix.pkg == 'exe' && 'innosetup' || 'fpm' }}/${{ matrix.asset }}
|
||||
asset_name: ${{ matrix.asset }}
|
||||
asset_content_type: binary/octet-stream
|
||||
|
||||
pip:
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -100,13 +42,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
|
||||
|
||||
@@ -4,6 +4,7 @@ DVC
|
||||
Make your data science projects reproducible and shareable.
|
||||
"""
|
||||
import dvc.logger
|
||||
from dvc.build import PKG # noqa: F401
|
||||
from dvc.version import __version__, version_tuple # noqa: F401
|
||||
|
||||
dvc.logger.setup()
|
||||
|
||||
+14
-8
@@ -17,20 +17,26 @@ class Annotation:
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def update(self, **kwargs) -> "Annotation":
|
||||
for attr, value in kwargs.items():
|
||||
if value and hasattr(self, attr):
|
||||
setattr(self, attr, value)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
@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
-2
@@ -2,13 +2,17 @@ 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
|
||||
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",
|
||||
"params_show",
|
||||
"metrics_show",
|
||||
|
||||
+24
-5
@@ -73,6 +73,7 @@ def open( # noqa, pylint: disable=redefined-builtin
|
||||
remote: Optional[str] = None,
|
||||
mode: str = "r",
|
||||
encoding: Optional[str] = None,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
Opens a file tracked in a DVC project.
|
||||
@@ -114,6 +115,8 @@ def open( # noqa, pylint: disable=redefined-builtin
|
||||
Defaults to None.
|
||||
This should only be used in text mode.
|
||||
Mirrors the namesake parameter in builtin `open()`_.
|
||||
config(dict, optional): config to be passed to the DVC repository.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
_OpenContextManager: A context manager that generatse a corresponding
|
||||
@@ -209,14 +212,24 @@ def open( # noqa, pylint: disable=redefined-builtin
|
||||
"rev": rev,
|
||||
"mode": mode,
|
||||
"encoding": encoding,
|
||||
"config": config,
|
||||
}
|
||||
return _OpenContextManager(_open, args, kwargs)
|
||||
|
||||
|
||||
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
repo_kwargs: Dict[str, Any] = {"subrepos": True, "uninitialized": True}
|
||||
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None, config=None):
|
||||
if remote:
|
||||
repo_kwargs["config"] = {"core": {"remote": remote}}
|
||||
if config is not None:
|
||||
raise ValueError(
|
||||
"can't specify both `remote` and `config` at the same time"
|
||||
)
|
||||
config = {"core": {"remote": remote}}
|
||||
|
||||
repo_kwargs: Dict[str, Any] = {
|
||||
"subrepos": True,
|
||||
"uninitialized": True,
|
||||
"config": config,
|
||||
}
|
||||
|
||||
with Repo.open(repo, rev=rev, **repo_kwargs) as _repo:
|
||||
with _wrap_exceptions(_repo, path):
|
||||
@@ -251,13 +264,19 @@ def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
|
||||
|
||||
|
||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None, config=None):
|
||||
"""
|
||||
Returns the contents of a tracked file (by DVC or Git). For Git repos, HEAD
|
||||
is used unless a rev argument is supplied. The default remote is tried
|
||||
unless a remote argument is supplied.
|
||||
"""
|
||||
with open(
|
||||
path, repo=repo, rev=rev, remote=remote, mode=mode, encoding=encoding
|
||||
path,
|
||||
repo=repo,
|
||||
rev=rev,
|
||||
remote=remote,
|
||||
mode=mode,
|
||||
encoding=encoding,
|
||||
config=config,
|
||||
) as fd:
|
||||
return fd.read()
|
||||
|
||||
+81
-32
@@ -1,37 +1,9 @@
|
||||
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.stage.monitor import CheckpointTask
|
||||
|
||||
|
||||
def make_checkpoint():
|
||||
"""
|
||||
Signal DVC to create a checkpoint experiment.
|
||||
|
||||
If the current process is being run from DVC, this function will block
|
||||
until DVC has finished creating the checkpoint. Otherwise, this function
|
||||
will return immediately.
|
||||
"""
|
||||
if os.getenv(DVC_CHECKPOINT) is None:
|
||||
return
|
||||
|
||||
root_dir = os.getenv(DVC_ROOT, Repo.find_root())
|
||||
signal_file = os.path.join(
|
||||
root_dir, Repo.DVC_DIR, "tmp", CheckpointTask.SIGNAL_FILE
|
||||
)
|
||||
|
||||
with builtins.open(signal_file, "w", encoding="utf-8") as fobj:
|
||||
# NOTE: force flushing/writing empty file to disk, otherwise when
|
||||
# run in certain contexts (pytest) file may not actually be written
|
||||
fobj.write("")
|
||||
fobj.flush()
|
||||
os.fsync(fobj.fileno())
|
||||
while os.path.exists(signal_file):
|
||||
sleep(0.1)
|
||||
from dvc.repo.experiments.show import tabulate
|
||||
|
||||
|
||||
def exp_save(
|
||||
@@ -69,3 +41,80 @@ 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,
|
||||
config: Optional[Dict] = None,
|
||||
) -> 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`.
|
||||
config (dict, optional): config to be passed through to DVC project.
|
||||
Defaults to `None`.
|
||||
|
||||
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, config=config) 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()
|
||||
+8
-2
@@ -41,6 +41,7 @@ def metrics_show(
|
||||
*targets: str,
|
||||
repo: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
config: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
"""Get metrics tracked in `repo`.
|
||||
|
||||
@@ -69,6 +70,8 @@ def metrics_show(
|
||||
hash or a dvc experiment name.
|
||||
If `repo` is not a Git repo, this option is ignored.
|
||||
If `None`, the current working tree will be used.
|
||||
config (dict, optional): config to be passed through to DVC project.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
@@ -136,7 +139,7 @@ def metrics_show(
|
||||
https://git-scm.com/docs/revisions
|
||||
"""
|
||||
|
||||
with Repo.open(repo) as _repo:
|
||||
with Repo.open(repo, config=config) as _repo:
|
||||
metrics = _repo.metrics.show(
|
||||
targets=targets,
|
||||
revs=rev if rev is None else [rev],
|
||||
@@ -157,6 +160,7 @@ def params_show(
|
||||
stages: Optional[Union[str, Iterable[str]]] = None,
|
||||
rev: Optional[str] = None,
|
||||
deps: bool = False,
|
||||
config: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
"""Get parameters tracked in `repo`.
|
||||
|
||||
@@ -195,6 +199,8 @@ def params_show(
|
||||
deps (bool, optional): Whether to retrieve only parameters that are
|
||||
stage dependencies or not.
|
||||
Defaults to `False`.
|
||||
config (dict, optional): config to be passed through to DVC project.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
@@ -379,7 +385,7 @@ def params_show(
|
||||
if isinstance(stages, str):
|
||||
stages = [stages]
|
||||
|
||||
with Repo.open(repo) as _repo:
|
||||
with Repo.open(repo, config=config) as _repo:
|
||||
params = _repo.params.show(
|
||||
revs=rev if rev is None else [rev],
|
||||
targets=targets,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
# file is created during dvc build
|
||||
# pylint:disable=unused-import
|
||||
from . import _build # type: ignore[attr-defined, import]
|
||||
|
||||
PKG: Optional[str] = _build.PKG # type: ignore[assignment]
|
||||
except ImportError:
|
||||
PKG = None # type: ignore[assignment]
|
||||
+75
-14
@@ -1,10 +1,24 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
LEGACY_HASH_NAMES = {"md5-dos2unix", "params"}
|
||||
|
||||
|
||||
def _get_odb(repo, settings, fs=None):
|
||||
def _get_odb(
|
||||
repo,
|
||||
settings,
|
||||
fs=None,
|
||||
prefix: Optional[Tuple[str, ...]] = None,
|
||||
hash_name: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
if not settings:
|
||||
@@ -12,18 +26,16 @@ def _get_odb(repo, settings, fs=None):
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(repo, **settings)
|
||||
fs = fs or cls(**config)
|
||||
if prefix:
|
||||
fs_path = fs.path.join(fs_path, *prefix)
|
||||
if hash_name:
|
||||
config["hash_name"] = hash_name
|
||||
return get_odb(fs, fs_path, state=repo.state, **config)
|
||||
|
||||
|
||||
class CacheManager:
|
||||
CACHE_DIR = "cache"
|
||||
CLOUD_SCHEMES = [
|
||||
Schemes.S3,
|
||||
Schemes.GS,
|
||||
Schemes.SSH,
|
||||
Schemes.HDFS,
|
||||
Schemes.WEBHDFS,
|
||||
]
|
||||
FILES_DIR = "files"
|
||||
|
||||
def __init__(self, repo):
|
||||
self._repo = repo
|
||||
@@ -53,25 +65,74 @@ class CacheManager:
|
||||
if not isinstance(repo.fs, GitFileSystem):
|
||||
kwargs["fs"] = repo.fs
|
||||
|
||||
odb = _get_odb(repo, settings, **kwargs)
|
||||
odb = _get_odb(
|
||||
repo,
|
||||
settings,
|
||||
prefix=(self.FILES_DIR, DEFAULT_ALGORITHM),
|
||||
**kwargs,
|
||||
)
|
||||
self._odb["repo"] = odb
|
||||
self._odb[Schemes.LOCAL] = odb
|
||||
legacy_odb = _get_odb(repo, settings, hash_name="md5-dos2unix", **kwargs)
|
||||
self._odb["legacy"] = legacy_odb
|
||||
|
||||
def _init_odb(self, schemes):
|
||||
for scheme in schemes:
|
||||
remote = self.config.get(scheme)
|
||||
settings = {"name": remote} if remote else None
|
||||
self._odb[scheme] = _get_odb(self._repo, settings)
|
||||
self._odb[scheme] = _get_odb(
|
||||
self._repo,
|
||||
settings,
|
||||
prefix=(self.FILES_DIR, DEFAULT_ALGORITHM),
|
||||
)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self._odb and name in self.CLOUD_SCHEMES:
|
||||
self._init_odb([name])
|
||||
|
||||
try:
|
||||
return self._odb[name]
|
||||
except KeyError as exc:
|
||||
raise AttributeError from exc
|
||||
|
||||
def by_scheme(self):
|
||||
self._init_odb(self.CLOUD_SCHEMES)
|
||||
yield from self._odb.items()
|
||||
|
||||
@property
|
||||
def local_cache_dir(self) -> str:
|
||||
"""Return base local cache directory without any prefixes.
|
||||
|
||||
(i.e. `dvc cache dir`).
|
||||
"""
|
||||
return self.legacy.path
|
||||
|
||||
|
||||
def migrate_2_to_3(repo: "Repo", dry: bool = False):
|
||||
"""Migrate legacy 2.x objects to 3.x cache.
|
||||
|
||||
Legacy 'md5-dos2unix' objects will be re-hashed with 'md5', added to 3.x cache,
|
||||
and then a link from the legacy 2.x location to the 3.x location will be created.
|
||||
"""
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
from dvc.ui import ui
|
||||
from dvc_data.hashfile.db.migrate import migrate, prepare
|
||||
|
||||
src = repo.cache.legacy
|
||||
dest = repo.cache.local
|
||||
if dry:
|
||||
oids = list(src._list_oids()) # pylint: disable=protected-access
|
||||
ui.write(
|
||||
f"{len(oids)} files will be re-hashed and migrated to the DVC 3.0 cache "
|
||||
"location."
|
||||
)
|
||||
return
|
||||
|
||||
with TqdmCallback(
|
||||
desc="Computing DVC 3.0 hashes",
|
||||
unit="files",
|
||||
) as cb:
|
||||
migration = prepare(src, dest, callback=cb)
|
||||
|
||||
with TqdmCallback(
|
||||
desc="Migrating to DVC 3.0 cache",
|
||||
unit="files",
|
||||
) as cb:
|
||||
count = migrate(migration, callback=cb)
|
||||
ui.write(f"Migrated {count} files to DVC 3.0 cache location.")
|
||||
|
||||
+5
-6
@@ -75,7 +75,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
from dvc.fs import AuthError, ConfigError, RemoteMissingDepsError
|
||||
|
||||
if isinstance(exc, RemoteMissingDepsError):
|
||||
from dvc.utils.pkg import PKG
|
||||
from dvc import PKG
|
||||
|
||||
proto = exc.protocol
|
||||
by_pkg = {
|
||||
@@ -83,9 +83,9 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
"conda": f"conda install -c conda-forge dvc-{proto}",
|
||||
}
|
||||
|
||||
cmd = by_pkg.get(PKG)
|
||||
if cmd:
|
||||
if PKG in by_pkg:
|
||||
link = format_link("https://dvc.org/doc/install")
|
||||
cmd = by_pkg.get(PKG)
|
||||
hint = (
|
||||
"To install dvc with those dependencies, run:\n"
|
||||
"\n"
|
||||
@@ -190,8 +190,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
if level and level <= logging.DEBUG:
|
||||
from platform import platform, python_implementation, python_version
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.utils.pkg import PKG
|
||||
from dvc import PKG, __version__
|
||||
|
||||
pyv = " ".join([python_implementation(), python_version()])
|
||||
pkg = f" ({PKG})" if PKG else ""
|
||||
@@ -245,7 +244,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.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from argparse import _AppendAction
|
||||
|
||||
|
||||
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
|
||||
def __call__(self, parser, namespace, values, option_string=None): # noqa: ARG002
|
||||
from funcy import ldistinct
|
||||
|
||||
items = getattr(namespace, self.dest) or []
|
||||
items.extend(map(str.strip, values.split(",")))
|
||||
setattr(namespace, self.dest, ldistinct(items))
|
||||
|
||||
|
||||
class KeyValueArgs(_AppendAction):
|
||||
def __call__(self, parser, namespace, values, option_string=None): # noqa: ARG002
|
||||
items = getattr(namespace, self.dest) or {}
|
||||
for value in filter(bool, values):
|
||||
key, _, value = value.partition("=")
|
||||
items[key.strip()] = value
|
||||
|
||||
setattr(namespace, self.dest, items)
|
||||
@@ -41,7 +41,6 @@ from dvc.commands import (
|
||||
remove,
|
||||
repro,
|
||||
root,
|
||||
run,
|
||||
stage,
|
||||
unprotect,
|
||||
update,
|
||||
@@ -62,7 +61,6 @@ COMMANDS = [
|
||||
remove,
|
||||
move,
|
||||
unprotect,
|
||||
run,
|
||||
repro,
|
||||
data_sync,
|
||||
gc,
|
||||
|
||||
+44
-65
@@ -2,68 +2,61 @@ import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import KeyValueArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help="User description of the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--meta",
|
||||
metavar="key=value",
|
||||
nargs=1,
|
||||
action=KeyValueArgs,
|
||||
help="Custom metadata to add to the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--label",
|
||||
dest="labels",
|
||||
type=str,
|
||||
action="append",
|
||||
metavar="<str>",
|
||||
help="Label for the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
type=str,
|
||||
metavar="<str>",
|
||||
help="Type of the data.",
|
||||
)
|
||||
|
||||
|
||||
class CmdAdd(CmdBase):
|
||||
def validate_args(self) -> None:
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
args = self.args
|
||||
invalid_opt = None
|
||||
|
||||
if args.to_remote or args.out:
|
||||
message = "{option} can't be used with "
|
||||
message += "--to-remote" if args.to_remote else "--out"
|
||||
if len(args.targets) != 1:
|
||||
invalid_opt = "multiple targets"
|
||||
elif args.glob:
|
||||
invalid_opt = "--glob option"
|
||||
elif args.no_commit:
|
||||
invalid_opt = "--no-commit option"
|
||||
else:
|
||||
message = "{option} can't be used without --to-remote"
|
||||
if args.remote:
|
||||
invalid_opt = "--remote"
|
||||
elif args.remote_jobs:
|
||||
invalid_opt = "--remote-jobs"
|
||||
|
||||
if invalid_opt is not None:
|
||||
raise InvalidArgumentError(message.format(option=invalid_opt))
|
||||
|
||||
def run(self):
|
||||
from dvc.exceptions import DvcException, RecursiveAddingWhileUsingFilename
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
|
||||
try:
|
||||
if len(self.args.targets) > 1 and self.args.file:
|
||||
raise RecursiveAddingWhileUsingFilename()
|
||||
self.validate_args()
|
||||
except InvalidArgumentError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
try:
|
||||
self.repo.add(
|
||||
self.args.targets,
|
||||
recursive=self.args.recursive,
|
||||
no_commit=self.args.no_commit,
|
||||
fname=self.args.file,
|
||||
external=self.args.external,
|
||||
glob=self.args.glob,
|
||||
desc=self.args.desc,
|
||||
out=self.args.out,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
jobs=self.args.jobs,
|
||||
remote_jobs=self.args.remote_jobs,
|
||||
force=self.args.force,
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
@@ -80,36 +73,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help=ADD_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Recursively add files under directory targets.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-commit",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Don't put files/directories into cache.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--external",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allow targets that are outside of the DVC repository.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--glob",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allows targets containing shell-style wildcards.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file",
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
@@ -129,8 +104,7 @@ def add_parser(subparsers, parent_parser):
|
||||
metavar="<name>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
"--remote-jobs",
|
||||
type=int,
|
||||
help=(
|
||||
"Only used along with '--to-remote'. "
|
||||
@@ -140,8 +114,13 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(parser)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
|
||||
+29
-2
@@ -2,6 +2,7 @@ import argparse
|
||||
import os
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.commands.config import CmdConfig
|
||||
from dvc.ui import ui
|
||||
@@ -36,6 +37,14 @@ class CmdCacheDir(CmdConfig):
|
||||
return 0
|
||||
|
||||
|
||||
class CmdCacheMigrate(CmdBase):
|
||||
def run(self):
|
||||
from dvc.cachemgr import migrate_2_to_3
|
||||
|
||||
migrate_2_to_3(self.repo, dry=self.args.dry)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from dvc.commands.config import parent_config_parser
|
||||
|
||||
@@ -54,8 +63,6 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Use `dvc cache CMD --help` for command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(cache_subparsers)
|
||||
|
||||
parent_cache_config_parser = argparse.ArgumentParser(
|
||||
add_help=False, parents=[parent_config_parser]
|
||||
)
|
||||
@@ -86,3 +93,23 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
).complete = completion.DIR
|
||||
cache_dir_parser.set_defaults(func=CmdCacheDir)
|
||||
|
||||
CACHE_MIGRATE_HELP = "Migrate cached files to the DVC 3.0 cache location."
|
||||
cache_migrate_parser = cache_subparsers.add_parser(
|
||||
"migrate",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(CACHE_HELP, "cache/migrate"),
|
||||
help=CACHE_MIGRATE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
cache_migrate_parser.add_argument(
|
||||
"--dry",
|
||||
help=(
|
||||
"Only print actions which would be taken without actually migrating "
|
||||
"any data."
|
||||
),
|
||||
action="store_true",
|
||||
)
|
||||
cache_migrate_parser.set_defaults(func=CmdCacheMigrate)
|
||||
|
||||
fix_subparsers(cache_subparsers)
|
||||
|
||||
@@ -176,7 +176,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
dag_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
+11
-119
@@ -1,14 +1,11 @@
|
||||
import argparse
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from funcy import chunks, compact, log_durations
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import CommaSeparatedArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers, hide_subparsers_from_help
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
@@ -117,6 +114,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,
|
||||
)
|
||||
|
||||
@@ -131,63 +129,6 @@ class CmdDataStatus(CmdBase):
|
||||
return self._show_status(status)
|
||||
|
||||
|
||||
class CmdDataLs(CmdBase):
|
||||
@staticmethod
|
||||
def _show_table(
|
||||
d: Iterable[Dict[str, Any]],
|
||||
filter_types: Set[str],
|
||||
filter_labels: Set[str],
|
||||
markdown: bool = False,
|
||||
) -> None:
|
||||
from rich.style import Style
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
td = TabularData(
|
||||
columns=["Path", "Type", "Labels", "Description"], fill_value="-"
|
||||
)
|
||||
for entry in sorted(d, key=itemgetter("path")):
|
||||
typ = entry.get("type", "")
|
||||
desc = entry.get("desc", "-")
|
||||
labels = entry.get("labels", [])
|
||||
|
||||
if filter_types and typ not in filter_types:
|
||||
continue
|
||||
if filter_labels and filter_labels.isdisjoint(labels):
|
||||
continue
|
||||
|
||||
rich_label = ui.rich_text()
|
||||
for index, label in enumerate(labels):
|
||||
if index:
|
||||
rich_label.append(",")
|
||||
style = Style(bold=label in filter_labels, color="green")
|
||||
rich_label.append(label, style=style)
|
||||
|
||||
if markdown and desc:
|
||||
desc = desc.partition("\n")[0]
|
||||
|
||||
path = ui.rich_text(entry["path"], style="cyan")
|
||||
type_style = Style(bold=typ in filter_types, color="yellow")
|
||||
typ = ui.rich_text(entry.get("type", ""), style=type_style)
|
||||
td.append([path, typ or "-", rich_label or "-", desc])
|
||||
|
||||
td.render(markdown=markdown, rich_table=True)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo.data import ls
|
||||
|
||||
filter_labels = set(self.args.labels)
|
||||
filter_types = set(self.args.type)
|
||||
d = ls(self.repo, targets=self.args.targets, recursive=self.args.recursive)
|
||||
self._show_table(
|
||||
d,
|
||||
filter_labels=filter_labels,
|
||||
filter_types=filter_types,
|
||||
markdown=self.args.markdown,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
data_parser = subparsers.add_parser(
|
||||
"data",
|
||||
@@ -216,13 +157,6 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="json",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--granular",
|
||||
action="store_true",
|
||||
@@ -244,57 +178,15 @@ 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)
|
||||
|
||||
DATA_LS_HELP = "List data tracked by DVC with its metadata."
|
||||
data_ls_parser = data_subparsers.add_parser(
|
||||
"ls",
|
||||
aliases=["list"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DATA_LS_HELP, "data/ls"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
add_help=False,
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
dest="markdown",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--type",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of type to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--labels",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of labels to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Recursively list from the specified directories.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"targets",
|
||||
default=None,
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
data_ls_parser.set_defaults(func=CmdDataLs)
|
||||
hide_subparsers_from_help(data_subparsers)
|
||||
|
||||
@@ -403,7 +403,6 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show status in JSON format.",
|
||||
|
||||
@@ -184,7 +184,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
help="Format the output into a JSON",
|
||||
action="store_true",
|
||||
default=False,
|
||||
@@ -197,7 +196,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
action="store_true",
|
||||
dest="markdown",
|
||||
|
||||
@@ -7,8 +7,6 @@ from dvc.commands.experiments import (
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
init,
|
||||
ls,
|
||||
pull,
|
||||
push,
|
||||
@@ -25,8 +23,6 @@ SUB_COMMANDS = [
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
init,
|
||||
ls,
|
||||
pull,
|
||||
push,
|
||||
@@ -82,6 +78,7 @@ def add_rev_selection_flags(
|
||||
experiments_subcmd_parser.add_argument(
|
||||
"--rev",
|
||||
type=str,
|
||||
action="append",
|
||||
default=None,
|
||||
help=msg,
|
||||
metavar="<commit>",
|
||||
|
||||
@@ -83,14 +83,12 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _raise_error_if_all_disabled(**kwargs):
|
||||
if not any(kwargs.values()):
|
||||
raise InvalidArgumentError(
|
||||
"Either of `-w|--workspace`, `-a|--all-branches`, `-T|--all-tags`,"
|
||||
" `--all-commits` or `--date` needs to be set."
|
||||
)
|
||||
|
||||
|
||||
class CmdExperimentsGC(CmdBase):
|
||||
def run(self):
|
||||
_raise_error_if_all_disabled(
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
commit_date=self.args.commit_date,
|
||||
workspace=self.args.workspace,
|
||||
)
|
||||
|
||||
msg = "This will remove all experiments except those derived from "
|
||||
|
||||
msg += "the workspace"
|
||||
if self.args.all_commits:
|
||||
msg += " and all git commits"
|
||||
else:
|
||||
if self.args.all_branches and self.args.all_tags:
|
||||
msg += " and all git branches and tags"
|
||||
elif self.args.all_branches:
|
||||
msg += " and all git branches"
|
||||
elif self.args.all_tags:
|
||||
msg += " and all git tags"
|
||||
|
||||
if self.args.commit_date:
|
||||
msg += f" and all git commits before date {self.args.commit_date}"
|
||||
msg += " of the current repo."
|
||||
|
||||
if self.args.queued:
|
||||
msg += " Run queued experiments will be preserved."
|
||||
else:
|
||||
msg += " Run queued experiments will be removed."
|
||||
|
||||
logger.warning(msg)
|
||||
|
||||
msg = "Are you sure you want to proceed?"
|
||||
if not self.args.force and not ui.confirm(msg):
|
||||
return 1
|
||||
|
||||
removed = self.repo.experiments.gc(
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
workspace=self.args.workspace,
|
||||
commit_date=self.args.commit_date,
|
||||
queued=self.args.queued,
|
||||
)
|
||||
|
||||
if removed:
|
||||
ui.write(
|
||||
f"Removed {removed} experiments.",
|
||||
"To remove unused cache files",
|
||||
"use 'dvc gc'.",
|
||||
)
|
||||
else:
|
||||
ui.write("No experiments to remove.")
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_GC_HELP = "Garbage collect unneeded experiments."
|
||||
EXPERIMENTS_GC_DESCRIPTION = (
|
||||
"Removes all experiments which are not derived"
|
||||
" from the specified Git revisions."
|
||||
)
|
||||
experiments_gc_parser = experiments_subparsers.add_parser(
|
||||
"gc",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_GC_DESCRIPTION, "exp/gc"),
|
||||
help=EXPERIMENTS_GC_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-w",
|
||||
"--workspace",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep experiments derived from the current workspace.",
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep experiments derived from the tips of all Git branches.",
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-T",
|
||||
"--all-tags",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep experiments derived from all Git tags.",
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-A",
|
||||
"--all-commits",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep experiments derived from all Git commits.",
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"--date",
|
||||
type=str,
|
||||
dest="commit_date",
|
||||
metavar="<YYYY-MM-DD>",
|
||||
default=None,
|
||||
help=(
|
||||
"Keep experiments from the commits after (inclusive) a certain "
|
||||
"date. Date must match the extended ISO 8601 format (YYYY-MM-DD)."
|
||||
),
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"--queued",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Keep queued experiments (experiments run queue will be cleared "
|
||||
"by default)."
|
||||
),
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Force garbage collection - automatically agree to all prompts.",
|
||||
)
|
||||
experiments_gc_parser.set_defaults(func=CmdExperimentsGC)
|
||||
@@ -1,219 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from funcy import compact
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.dependency import Dependency
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsInit(CmdBase):
|
||||
DEFAULT_NAME = "train"
|
||||
CODE = "src"
|
||||
DATA = "data"
|
||||
MODELS = "models"
|
||||
DEFAULT_METRICS = "metrics.json"
|
||||
DEFAULT_PARAMS = "params.yaml"
|
||||
PLOTS = "plots"
|
||||
DEFAULTS = {
|
||||
"code": CODE,
|
||||
"data": DATA,
|
||||
"models": MODELS,
|
||||
"metrics": DEFAULT_METRICS,
|
||||
"params": DEFAULT_PARAMS,
|
||||
"plots": PLOTS,
|
||||
}
|
||||
|
||||
def run(self):
|
||||
from dvc.commands.stage import parse_cmd
|
||||
|
||||
cmd = parse_cmd(self.args.command)
|
||||
if not self.args.interactive and not cmd:
|
||||
raise InvalidArgumentError("command is not specified")
|
||||
|
||||
from dvc.repo.experiments.init import init
|
||||
|
||||
defaults = {}
|
||||
if not self.args.explicit:
|
||||
config = self.repo.config["exp"]
|
||||
defaults.update({**self.DEFAULTS, **config})
|
||||
|
||||
cli_args = compact(
|
||||
{
|
||||
"cmd": cmd,
|
||||
"code": self.args.code,
|
||||
"data": self.args.data,
|
||||
"models": self.args.models,
|
||||
"metrics": self.args.metrics,
|
||||
"params": self.args.params,
|
||||
"plots": self.args.plots,
|
||||
"live": self.args.live,
|
||||
}
|
||||
)
|
||||
|
||||
initialized_stage, initialized_deps, initialized_out_dirs = init(
|
||||
self.repo,
|
||||
name=self.args.name,
|
||||
type=self.args.type,
|
||||
defaults=defaults,
|
||||
overrides=cli_args,
|
||||
interactive=self.args.interactive,
|
||||
force=self.args.force,
|
||||
)
|
||||
self._post_init_display(
|
||||
initialized_stage, initialized_deps, initialized_out_dirs
|
||||
)
|
||||
if self.args.run:
|
||||
self.repo.experiments.run(targets=[initialized_stage.addressing])
|
||||
return 0
|
||||
|
||||
def _post_init_display(
|
||||
self,
|
||||
stage: "PipelineStage",
|
||||
new_deps: List["Dependency"],
|
||||
new_out_dirs: List[str],
|
||||
) -> None:
|
||||
from dvc.utils import humanize
|
||||
|
||||
path_fmt = "[green]{}[/green]".format
|
||||
if new_deps:
|
||||
deps_paths = humanize.join(map(path_fmt, new_deps))
|
||||
ui.write(f"Creating dependencies: {deps_paths}", styled=True)
|
||||
|
||||
if new_out_dirs:
|
||||
out_dirs_paths = humanize.join(map(path_fmt, new_out_dirs))
|
||||
ui.write(f"Creating output directories: {out_dirs_paths}", styled=True)
|
||||
|
||||
ui.write(
|
||||
f"Creating [b]{self.args.name}[/b] stage in [green]dvc.yaml[/]",
|
||||
styled=True,
|
||||
)
|
||||
if stage.outs or not self.args.run:
|
||||
# separate the above status-like messages with help/tips section
|
||||
ui.write(styled=True)
|
||||
|
||||
if stage.outs:
|
||||
outs_paths = humanize.join(map(path_fmt, stage.outs))
|
||||
tips = f"Ensure your experiment command creates {outs_paths}."
|
||||
ui.write(tips, styled=True)
|
||||
|
||||
if not self.args.run:
|
||||
ui.write(
|
||||
'You can now run your experiment using [b]"dvc exp run"[/].',
|
||||
styled=True,
|
||||
)
|
||||
else:
|
||||
# separate between `exp.run` output and `dvc exp init` output
|
||||
ui.write(styled=True)
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_INIT_HELP = "Quickly setup any project to use experiments."
|
||||
experiments_init_parser = experiments_subparsers.add_parser(
|
||||
"init",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_INIT_HELP, "exp/init"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=EXPERIMENTS_INIT_HELP,
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"command",
|
||||
nargs=argparse.REMAINDER,
|
||||
help="Command to execute.",
|
||||
metavar="command",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--run",
|
||||
action="store_true",
|
||||
help="Run the experiment after initializing it",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--interactive",
|
||||
"-i",
|
||||
action="store_true",
|
||||
help="Prompt for values that are not provided",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Overwrite existing stage",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--explicit",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only use the path values explicitly provided",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--name",
|
||||
"-n",
|
||||
help="Name of the stage to create (default: %(default)s)",
|
||||
default=CmdExperimentsInit.DEFAULT_NAME,
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--code",
|
||||
help=(
|
||||
"Path to the source file or directory "
|
||||
"which your experiments depend"
|
||||
f" (default: {CmdExperimentsInit.CODE})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--data",
|
||||
help=(
|
||||
"Path to the data file or directory "
|
||||
"which your experiments depend"
|
||||
f" (default: {CmdExperimentsInit.DATA})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--models",
|
||||
help=(
|
||||
"Path to the model file or directory for your experiments"
|
||||
f" (default: {CmdExperimentsInit.MODELS})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--params",
|
||||
help=(
|
||||
"Path to the parameters file for your experiments"
|
||||
f" (default: {CmdExperimentsInit.DEFAULT_PARAMS})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--metrics",
|
||||
help=(
|
||||
"Path to the metrics file for your experiments"
|
||||
f" (default: {CmdExperimentsInit.DEFAULT_METRICS})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--plots",
|
||||
help=(
|
||||
"Path to the plots file or directory for your experiments"
|
||||
f" (default: {CmdExperimentsInit.PLOTS})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--live",
|
||||
help="Path to log dvclive outputs for your experiments",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--type",
|
||||
choices=["default", "checkpoint"],
|
||||
default="default",
|
||||
help="Select type of stage to create (default: %(default)s)",
|
||||
)
|
||||
experiments_init_parser.set_defaults(func=CmdExperimentsInit)
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -11,19 +12,36 @@ logger = logging.getLogger(__name__)
|
||||
class CmdExperimentsList(CmdBase):
|
||||
def run(self):
|
||||
name_only = self.args.name_only
|
||||
sha_only = self.args.sha_only
|
||||
git_remote = self.args.git_remote
|
||||
if sha_only and git_remote:
|
||||
raise InvalidArgumentError("--sha-only not supported with git_remote.")
|
||||
exps = self.repo.experiments.ls(
|
||||
all_commits=self.args.all_commits,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
git_remote=self.args.git_remote,
|
||||
git_remote=git_remote,
|
||||
)
|
||||
|
||||
for baseline in exps:
|
||||
if not name_only:
|
||||
ui.write(f"{baseline}:")
|
||||
for exp_name in exps[baseline]:
|
||||
indent = "" if name_only else "\t"
|
||||
ui.write(f"{indent}{exp_name}")
|
||||
if not (name_only or sha_only):
|
||||
tag_base = "refs/tags/"
|
||||
branch_base = "refs/heads/"
|
||||
name = baseline[:7]
|
||||
if baseline.startswith(tag_base):
|
||||
name = baseline[len(tag_base) :]
|
||||
elif baseline.startswith(branch_base):
|
||||
name = baseline[len(branch_base) :]
|
||||
ui.write(f"{name}:")
|
||||
for exp_name, rev in exps[baseline]:
|
||||
if name_only:
|
||||
ui.write(exp_name)
|
||||
elif sha_only:
|
||||
ui.write(rev)
|
||||
elif rev:
|
||||
ui.write(f"\t{rev[:7]} [{exp_name}]")
|
||||
else:
|
||||
ui.write(f"\t{exp_name}")
|
||||
|
||||
return 0
|
||||
|
||||
@@ -40,11 +58,18 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
add_rev_selection_flags(experiments_list_parser, "List")
|
||||
experiments_list_parser.add_argument(
|
||||
display_group = experiments_list_parser.add_mutually_exclusive_group()
|
||||
display_group.add_argument(
|
||||
"--name-only",
|
||||
"--names-only",
|
||||
action="store_true",
|
||||
help="Only output experiment names (without parent commits).",
|
||||
help="Only output experiment names (without SHAs or parent commits).",
|
||||
)
|
||||
display_group.add_argument(
|
||||
"--sha-only",
|
||||
"--shas-only",
|
||||
action="store_true",
|
||||
help="Only output experiment commit SHAs (without names or parent commits).",
|
||||
)
|
||||
experiments_list_parser.add_argument(
|
||||
"git_remote",
|
||||
|
||||
@@ -3,23 +3,13 @@ import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsPull(CmdBase):
|
||||
def raise_error_if_all_disabled(self):
|
||||
if not any([self.args.experiment, self.args.all_commits, self.args.rev]):
|
||||
raise InvalidArgumentError(
|
||||
"Either provide an `experiment` argument, or use the "
|
||||
"`--rev` or `--all-commits` flag."
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.raise_error_if_all_disabled()
|
||||
|
||||
pulled_exps = self.repo.experiments.pull(
|
||||
self.args.git_remote,
|
||||
self.args.experiment,
|
||||
@@ -61,7 +51,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help=EXPERIMENTS_PULL_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
add_rev_selection_flags(experiments_pull_parser, "Pull", False)
|
||||
add_rev_selection_flags(experiments_pull_parser, "Pull", True)
|
||||
experiments_pull_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
|
||||
@@ -5,20 +5,12 @@ from typing import Any, Dict
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsPush(CmdBase):
|
||||
def raise_error_if_all_disabled(self):
|
||||
if not any([self.args.experiment, self.args.all_commits, self.args.rev]):
|
||||
raise InvalidArgumentError(
|
||||
"Either provide an `experiment` argument, or use the "
|
||||
"`--rev` or `--all-commits` flag."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_result(result: Dict[str, Any], remote: str):
|
||||
from dvc.utils import humanize
|
||||
@@ -52,13 +44,13 @@ 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
|
||||
|
||||
self.raise_error_if_all_disabled()
|
||||
|
||||
try:
|
||||
result = self.repo.experiments.push(
|
||||
self.args.git_remote,
|
||||
@@ -100,7 +92,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help=EXPERIMENTS_PUSH_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
add_rev_selection_flags(experiments_push_parser, "Push", False)
|
||||
add_rev_selection_flags(experiments_push_parser, "Push", True)
|
||||
experiments_push_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
|
||||
@@ -1,50 +1,28 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.repro import CmdRepro
|
||||
from dvc.commands.repro import add_arguments as add_repro_arguments
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsRun(CmdRepro):
|
||||
def run(self):
|
||||
from dvc.compare import show_metrics
|
||||
|
||||
if self.args.checkpoint_resume:
|
||||
if self.args.reset:
|
||||
raise InvalidArgumentError("--reset and --rev are mutually exclusive.")
|
||||
if not (self.args.queue or self.args.tmp_dir):
|
||||
raise InvalidArgumentError(
|
||||
"--rev can only be used in conjunction with --queue or --temp."
|
||||
)
|
||||
|
||||
if self.args.reset:
|
||||
ui.write("Any existing checkpoints will be reset and re-run.")
|
||||
|
||||
results = self.repo.experiments.run(
|
||||
self.repo.experiments.run(
|
||||
name=self.args.name,
|
||||
queue=self.args.queue,
|
||||
run_all=self.args.run_all,
|
||||
jobs=self.args.jobs,
|
||||
params=self.args.set_param,
|
||||
checkpoint_resume=self.args.checkpoint_resume,
|
||||
reset=self.args.reset,
|
||||
tmp_dir=self.args.tmp_dir,
|
||||
machine=self.args.machine,
|
||||
copy_paths=self.args.copy_paths,
|
||||
message=self.args.message,
|
||||
**self._common_kwargs,
|
||||
)
|
||||
|
||||
if self.args.metrics and results:
|
||||
metrics = self.repo.metrics.show(revs=list(results))
|
||||
metrics.pop("workspace", None)
|
||||
show_metrics(metrics)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -58,22 +36,6 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
_add_run_common(experiments_run_parser)
|
||||
experiments_run_parser.add_argument(
|
||||
"-r",
|
||||
"--rev",
|
||||
type=str,
|
||||
dest="checkpoint_resume",
|
||||
help=(
|
||||
"Continue the specified checkpoint experiment. Can only be used "
|
||||
"in conjunction with --queue or --temp."
|
||||
),
|
||||
metavar="<experiment_rev>",
|
||||
).complete = completion.EXPERIMENT
|
||||
experiments_run_parser.add_argument(
|
||||
"--reset",
|
||||
action="store_true",
|
||||
help="Reset existing checkpoints and restart the experiment.",
|
||||
)
|
||||
experiments_run_parser.set_defaults(func=CmdExperimentsRun)
|
||||
|
||||
|
||||
@@ -147,3 +109,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")
|
||||
@@ -52,7 +53,6 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
@@ -75,4 +75,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)
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
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
|
||||
|
||||
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,263 +24,13 @@ 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": "│ ╟",
|
||||
"checkpoint_base": "├─╨",
|
||||
"branch_commit": "├──",
|
||||
"branch_base": "└──",
|
||||
"baseline": "",
|
||||
}
|
||||
|
||||
|
||||
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"]
|
||||
@@ -308,50 +54,16 @@ def baseline_styler(typ):
|
||||
return {"style": "bold"} if typ == "baseline" else {}
|
||||
|
||||
|
||||
def show_experiments( # noqa: C901, PLR0912
|
||||
all_experiments,
|
||||
def show_experiments( # noqa: C901
|
||||
td: "TabularData",
|
||||
headers: Dict[str, Iterable[str]],
|
||||
keep=None,
|
||||
drop=None,
|
||||
pager=True,
|
||||
csv=False,
|
||||
markdown=False,
|
||||
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 +80,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"},
|
||||
@@ -397,51 +104,23 @@ def show_experiments( # noqa: C901, PLR0912
|
||||
}
|
||||
)
|
||||
|
||||
if kwargs.get("only_changed", False) or pcp:
|
||||
if kwargs.get("only_changed", False):
|
||||
td.drop_duplicates("cols", ignore_empty=False)
|
||||
|
||||
cols_to_drop = set()
|
||||
if drop is not None:
|
||||
cols_to_drop = {col for col in td.keys() if re.match(drop, col)} # noqa: SIM118
|
||||
if pcp:
|
||||
cols_to_drop.add("Created")
|
||||
td.drop(*cols_to_drop)
|
||||
|
||||
if pcp:
|
||||
subset = {x for x in td.keys() if x != "Experiment"} # noqa: SIM118
|
||||
td.dropna(
|
||||
"rows",
|
||||
how="all",
|
||||
subset=subset,
|
||||
)
|
||||
td.drop_duplicates("rows", subset=subset)
|
||||
if "Experiment" in td:
|
||||
td.column("Experiment")[:] = [
|
||||
# remove tree characters
|
||||
str(x).encode("ascii", "ignore").strip().decode()
|
||||
for x in td.column("Experiment")
|
||||
]
|
||||
out = kwargs.get("out") or "dvc_plots"
|
||||
output_file = os.path.join(out, "index.html")
|
||||
ui.write(
|
||||
td.to_parallel_coordinates(
|
||||
output_path=os.path.abspath(output_file),
|
||||
color_by=kwargs.get("sort_by") or "Experiment",
|
||||
)
|
||||
)
|
||||
if kwargs.get("open"):
|
||||
return ui.open_browser(output_file)
|
||||
|
||||
else:
|
||||
td.render(
|
||||
pager=pager,
|
||||
borders="horizontals",
|
||||
rich_table=True,
|
||||
header_styles=styles,
|
||||
row_styles=row_styles,
|
||||
csv=csv,
|
||||
markdown=markdown,
|
||||
)
|
||||
td.render(
|
||||
pager=pager,
|
||||
borders="horizontals",
|
||||
rich_table=True,
|
||||
header_styles=styles,
|
||||
row_styles=row_styles,
|
||||
csv=csv,
|
||||
markdown=markdown,
|
||||
)
|
||||
|
||||
|
||||
def _normalize_headers(names, count):
|
||||
@@ -453,19 +132,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,30 +160,33 @@ 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,
|
||||
only_changed=self.args.only_changed,
|
||||
pcp=self.args.pcp,
|
||||
out=self.args.out,
|
||||
open=self.args.open,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -599,21 +279,18 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print output in JSON format instead of a human-readable table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--csv",
|
||||
"--show-csv",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print output in csv format instead of a human-readable table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
@@ -628,27 +305,6 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
),
|
||||
metavar="<n>",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--parallel-coordinates-plot",
|
||||
"--pcp",
|
||||
dest="pcp",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Generate a Parallel Coordinates Plot from the tabulated output.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
default=None,
|
||||
help="Destination folder to save the Parallel Coordinates Plot to",
|
||||
metavar="<path>",
|
||||
).complete = completion.DIR
|
||||
experiments_show_parser.add_argument(
|
||||
"--open",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Open the Parallel Coordinates Plot directly in the browser.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--no-fetch",
|
||||
dest="fetch_running",
|
||||
|
||||
+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)
|
||||
|
||||
@@ -18,14 +18,9 @@ class CmdImport(CmdBase):
|
||||
self.args.url,
|
||||
self.args.path,
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
rev=self.args.rev,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
except CloneError:
|
||||
@@ -42,8 +37,6 @@ class CmdImport(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download file or directory tracked by DVC or by Git "
|
||||
"into the workspace, and track it."
|
||||
@@ -75,11 +68,6 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--file",
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
@@ -106,6 +94,4 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImport)
|
||||
|
||||
+8
-13
@@ -15,16 +15,12 @@ class CmdImportUrl(CmdBase):
|
||||
self.repo.imp_url(
|
||||
self.args.url,
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
version_aware=self.args.version_aware,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -40,8 +36,6 @@ class CmdImportUrl(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = "Download or copy file from URL and take it under DVC control."
|
||||
|
||||
import_parser = subparsers.add_parser(
|
||||
@@ -69,11 +63,6 @@ def add_parser(subparsers, parent_parser):
|
||||
import_parser.add_argument(
|
||||
"out", nargs="?", help="Destination path to put files to."
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"--file",
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"--to-remote",
|
||||
action="store_true",
|
||||
@@ -112,11 +101,17 @@ 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",
|
||||
default=False,
|
||||
help="Import using cloud versioning. Implied if the URL contains a version ID.",
|
||||
)
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImportUrl)
|
||||
|
||||
@@ -71,7 +71,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
|
||||
@@ -140,14 +140,12 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
@@ -223,14 +221,12 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
@@ -105,14 +105,12 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
@@ -430,7 +430,6 @@ def _add_ui_arguments(parser):
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
|
||||
@@ -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)
|
||||
|
||||
+7
-13
@@ -16,12 +16,6 @@ class CmdRepro(CmdBase):
|
||||
else:
|
||||
ui.write("Use `dvc push` to send your updates to remote storage.")
|
||||
|
||||
if self.args.metrics:
|
||||
from dvc.compare import show_metrics
|
||||
|
||||
metrics = self.repo.metrics.show()
|
||||
show_metrics(metrics)
|
||||
|
||||
return 0
|
||||
|
||||
@property
|
||||
@@ -38,6 +32,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
|
||||
@@ -104,13 +99,6 @@ and then the stage name name.
|
||||
default=False,
|
||||
help="Reproduce all stages in the specified directory.",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"-m",
|
||||
"--metrics",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show metrics after reproduction.",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--downstream",
|
||||
action="store_true",
|
||||
@@ -135,6 +123,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",
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.stage import parse_cmd
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdRun(CmdBase):
|
||||
def run(self):
|
||||
if not any(
|
||||
[
|
||||
self.args.deps,
|
||||
self.args.outs,
|
||||
self.args.outs_no_cache,
|
||||
self.args.metrics,
|
||||
self.args.metrics_no_cache,
|
||||
self.args.plots,
|
||||
self.args.plots_no_cache,
|
||||
self.args.outs_persist,
|
||||
self.args.outs_persist_no_cache,
|
||||
self.args.checkpoints,
|
||||
self.args.params,
|
||||
self.args.command,
|
||||
]
|
||||
): # pragma: no cover
|
||||
logger.error(
|
||||
"too few arguments. Specify at least one: `-d`, `-o`, `-O`, "
|
||||
"`-m`, `-M`, `-p`, `--plots`, `--plots-no-cache`, "
|
||||
"`--outs-persist`, `--outs-persist-no-cache`, `command`."
|
||||
)
|
||||
return 1
|
||||
|
||||
kwargs = vars(self.args)
|
||||
kwargs.update(
|
||||
{
|
||||
"cmd": parse_cmd(self.args.command),
|
||||
"fname": kwargs.pop("file"),
|
||||
"no_exec": self.args.no_exec or bool(self.args.checkpoints),
|
||||
"run_cache": not kwargs.pop("no_run_cache"),
|
||||
}
|
||||
)
|
||||
try:
|
||||
self.repo.run(**kwargs)
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from dvc.commands.stage import _add_common_args
|
||||
|
||||
RUN_HELP = "Generate a dvc.yaml file from a command and execute the command."
|
||||
run_parser = subparsers.add_parser(
|
||||
"run",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(RUN_HELP, "run"),
|
||||
help=RUN_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
run_parser.add_argument("-n", "--name", help="Stage name.")
|
||||
run_parser.add_argument("--file", metavar="<filename>", help=argparse.SUPPRESS)
|
||||
run_parser.add_argument(
|
||||
"--single-stage",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
_add_common_args(run_parser)
|
||||
run_parser.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create dvc.yaml without actually running it.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--no-commit",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Don't put files/directories into cache.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--no-run-cache",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Execute the command even if this stage has already been run "
|
||||
"with the same command/dependencies/outputs/etc before."
|
||||
),
|
||||
)
|
||||
run_parser.set_defaults(func=CmdRun)
|
||||
+30
-12
@@ -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
|
||||
|
||||
|
||||
@@ -172,17 +195,6 @@ def _add_common_args(parser):
|
||||
help="Declare output file or directory (do not put into DVC cache).",
|
||||
metavar="<filename>",
|
||||
).complete = completion.FILE
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--checkpoints",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"Declare checkpoint output file or directory for 'dvc exp run'. "
|
||||
"Not compatible with 'dvc repro'."
|
||||
),
|
||||
metavar="<filename>",
|
||||
).complete = completion.FILE
|
||||
parser.add_argument(
|
||||
"--external",
|
||||
action="store_true",
|
||||
@@ -257,6 +269,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
-20
@@ -22,7 +22,6 @@ from typing import (
|
||||
from funcy import reraise
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
from dvc.ui.table import CellT
|
||||
|
||||
|
||||
@@ -35,7 +34,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
|
||||
@@ -161,24 +160,6 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
writer.writerow(row)
|
||||
return buff.getvalue()
|
||||
|
||||
def to_parallel_coordinates(
|
||||
self, output_path: "StrPath", color_by: Optional[str] = None
|
||||
) -> "StrPath":
|
||||
from dvc_render.html import render_html
|
||||
from dvc_render.plotly import ParallelCoordinatesRenderer
|
||||
|
||||
render_html(
|
||||
renderers=[
|
||||
ParallelCoordinatesRenderer(
|
||||
self.as_dict(),
|
||||
color_by=color_by,
|
||||
fill_value=self._fill_value,
|
||||
)
|
||||
],
|
||||
output_file=output_path,
|
||||
)
|
||||
return output_path
|
||||
|
||||
def add_column(self, name: str) -> None:
|
||||
self._columns[name] = Column([self._fill_value] * len(self))
|
||||
self._keys.append(name)
|
||||
|
||||
+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]:
|
||||
|
||||
+13
-10
@@ -154,13 +154,13 @@ SCHEMA = {
|
||||
"machine": Lower,
|
||||
},
|
||||
"cache": {
|
||||
"local": str,
|
||||
"s3": str,
|
||||
"gs": str,
|
||||
"hdfs": str,
|
||||
"webhdfs": str,
|
||||
"ssh": str,
|
||||
"azure": str,
|
||||
"local": str, # obsoleted
|
||||
"s3": str, # obsoleted
|
||||
"gs": str, # obsoleted
|
||||
"hdfs": str, # obsoleted
|
||||
"webhdfs": str, # obsoleted
|
||||
"ssh": str, # obsoleted
|
||||
"azure": str, # obsoleted
|
||||
# This is for default local cache
|
||||
"dir": str,
|
||||
**LOCAL_COMMON,
|
||||
@@ -302,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": {
|
||||
@@ -330,4 +327,10 @@ SCHEMA = {
|
||||
"config_dir": str,
|
||||
"config_name": str,
|
||||
},
|
||||
"studio": {
|
||||
"token": str,
|
||||
"url": str,
|
||||
"repo_url": str,
|
||||
Optional("offline", default=False): Bool,
|
||||
},
|
||||
}
|
||||
|
||||
+131
-14
@@ -1,18 +1,18 @@
|
||||
"""Manages dvc remotes that user can use with push/pull/status commands."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, Set, Tuple
|
||||
|
||||
from dvc.config import NoRemoteError, RemoteConfigError
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_data.hashfile.db import get_index
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,13 +29,40 @@ class Remote:
|
||||
|
||||
@cached_property
|
||||
def odb(self) -> "HashFileDB":
|
||||
from dvc.cachemgr import CacheManager
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
|
||||
|
||||
path = self.path
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(path, ".dvc", "cache")
|
||||
path = self.fs.path.join(
|
||||
path, ".dvc", CacheManager.FILES_DIR, DEFAULT_ALGORITHM
|
||||
)
|
||||
else:
|
||||
path = self.fs.path.join(path, CacheManager.FILES_DIR, DEFAULT_ALGORITHM)
|
||||
return get_odb(self.fs, path, hash_name=DEFAULT_ALGORITHM, **self.config)
|
||||
|
||||
return get_odb(self.fs, path, hash_name="md5", **self.config)
|
||||
@cached_property
|
||||
def legacy_odb(self) -> "HashFileDB":
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
path = self.path
|
||||
return get_odb(self.fs, path, hash_name="md5-dos2unix", **self.config)
|
||||
|
||||
|
||||
def _split_legacy_hash_infos(
|
||||
hash_infos: Iterable["HashInfo"],
|
||||
) -> Tuple[Set["HashInfo"], Set["HashInfo"]]:
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
legacy = set()
|
||||
default = set()
|
||||
for hi in hash_infos:
|
||||
if hi.name in LEGACY_HASH_NAMES:
|
||||
legacy.add(hi)
|
||||
else:
|
||||
default.add(hi)
|
||||
return legacy, default
|
||||
|
||||
|
||||
class DataCloud:
|
||||
@@ -101,12 +128,17 @@ class DataCloud:
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
hash_name: str = "md5",
|
||||
) -> "HashFileDB":
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
remote = self.get_remote(name=name, command=command)
|
||||
if remote.fs.version_aware or remote.worktree:
|
||||
raise NoRemoteError(
|
||||
f"'{command}' is unsupported for cloud versioned remotes"
|
||||
)
|
||||
if hash_name in LEGACY_HASH_NAMES:
|
||||
return remote.legacy_odb
|
||||
return remote.odb
|
||||
|
||||
def _log_missing(self, status: "CompareStatusResult"):
|
||||
@@ -150,14 +182,40 @@ class DataCloud:
|
||||
By default remote from core.remote config option is used.
|
||||
odb: optional ODB to push to. Overrides remote.
|
||||
"""
|
||||
odb = odb or self.get_remote_odb(remote, "push")
|
||||
if odb is not None:
|
||||
return self._push(objs, jobs=jobs, odb=odb)
|
||||
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
|
||||
result = TransferResult(set(), set())
|
||||
if legacy_objs:
|
||||
odb = self.get_remote_odb(remote, "push", hash_name="md5-dos2unix")
|
||||
t, f = self._push(legacy_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
if default_objs:
|
||||
odb = self.get_remote_odb(remote, "push")
|
||||
t, f = self._push(default_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
return result
|
||||
|
||||
def _push(
|
||||
self,
|
||||
objs: Iterable["HashInfo"],
|
||||
*,
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
) -> "TransferResult":
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
return self.transfer(
|
||||
self.repo.cache.local,
|
||||
cache,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=self.repo.cache.local,
|
||||
cache_odb=cache,
|
||||
validate_status=self._log_missing,
|
||||
)
|
||||
|
||||
@@ -177,14 +235,41 @@ class DataCloud:
|
||||
By default remote from core.remote config option is used.
|
||||
odb: optional ODB to pull from. Overrides remote.
|
||||
"""
|
||||
odb = odb or self.get_remote_odb(remote, "pull")
|
||||
if odb is not None:
|
||||
return self._pull(objs, jobs=jobs, odb=odb)
|
||||
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
|
||||
result = TransferResult(set(), set())
|
||||
if legacy_objs:
|
||||
odb = self.get_remote_odb(remote, "pull", hash_name="md5-dos2unix")
|
||||
assert odb.hash_name == "md5-dos2unix"
|
||||
t, f = self._pull(legacy_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
if default_objs:
|
||||
odb = self.get_remote_odb(remote, "pull")
|
||||
t, f = self._pull(default_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
return result
|
||||
|
||||
def _pull(
|
||||
self,
|
||||
objs: Iterable["HashInfo"],
|
||||
*,
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
) -> "TransferResult":
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
return self.transfer(
|
||||
odb,
|
||||
self.repo.cache.local,
|
||||
cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
src_index=get_index(odb),
|
||||
cache_odb=self.repo.cache.local,
|
||||
cache_odb=cache,
|
||||
verify=odb.verify,
|
||||
validate_status=self._log_missing,
|
||||
)
|
||||
@@ -206,17 +291,49 @@ class DataCloud:
|
||||
is used.
|
||||
odb: optional ODB to check status from. Overrides remote.
|
||||
"""
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
|
||||
if odb is not None:
|
||||
return self._status(objs, jobs=jobs, odb=odb)
|
||||
result = CompareStatusResult(set(), set(), set(), set())
|
||||
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
|
||||
if legacy_objs:
|
||||
odb = self.get_remote_odb(remote, "status", hash_name="md5-dos2unix")
|
||||
assert odb.hash_name == "md5-dos2unix"
|
||||
o, m, n, d = self._status(legacy_objs, jobs=jobs, odb=odb)
|
||||
result.ok.update(o)
|
||||
result.missing.update(m)
|
||||
result.new.update(n)
|
||||
result.deleted.update(d)
|
||||
if default_objs:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
o, m, n, d = self._status(default_objs, jobs=jobs, odb=odb)
|
||||
result.ok.update(o)
|
||||
result.missing.update(m)
|
||||
result.new.update(n)
|
||||
result.deleted.update(d)
|
||||
return result
|
||||
|
||||
def _status(
|
||||
self,
|
||||
objs: Iterable["HashInfo"],
|
||||
*,
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
):
|
||||
from dvc_data.hashfile.status import compare_status
|
||||
|
||||
if not odb:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
return compare_status(
|
||||
self.repo.cache.local,
|
||||
cache,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=self.repo.cache.local,
|
||||
cache_odb=cache,
|
||||
)
|
||||
|
||||
def get_url_for(self, remote, checksum):
|
||||
|
||||
@@ -14,7 +14,6 @@ from .repo import RepoDependency
|
||||
SCHEMA: Mapping[str, Any] = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**RepoDependency.REPO_SCHEMA,
|
||||
**ParamsDependency.PARAM_SCHEMA,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
|
||||
@@ -36,7 +35,8 @@ def loadd_from(stage, d_list):
|
||||
for d in d_list:
|
||||
p = d.pop(Output.PARAM_PATH, None)
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
ret.append(_get(stage, p, d, files=files))
|
||||
hash_name = d.pop(Output.PARAM_HASH, None)
|
||||
ret.append(_get(stage, p, d, files=files, hash_name=hash_name))
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@ import logging
|
||||
import os
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
from typing import Any, Dict
|
||||
|
||||
import dpath
|
||||
from voluptuous import Any
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils.serialize import ParseError, load_path
|
||||
@@ -34,7 +33,6 @@ class BadParamFileError(DvcException):
|
||||
|
||||
class ParamsDependency(Dependency):
|
||||
PARAM_PARAMS = "params"
|
||||
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
|
||||
DEFAULT_PARAMS_FILE = "params.yaml"
|
||||
|
||||
def __init__(self, stage, path, params=None, repo=None):
|
||||
@@ -48,6 +46,7 @@ class ParamsDependency(Dependency):
|
||||
repo = repo or stage.repo
|
||||
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
|
||||
super().__init__(stage, path, repo=repo)
|
||||
self.hash_name = self.PARAM_PARAMS
|
||||
self.hash_info = hash_info
|
||||
|
||||
def dumpd(self, **kwargs):
|
||||
|
||||
@@ -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_cache_dir
|
||||
|
||||
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
|
||||
)
|
||||
+5
-48
@@ -14,7 +14,6 @@ from typing import (
|
||||
)
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
|
||||
from dvc.stage import serialize
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileBadNameError,
|
||||
@@ -36,7 +35,6 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
_T = TypeVar("_T")
|
||||
|
||||
DVC_FILE = "Dvcfile"
|
||||
DVC_FILE_SUFFIX = ".dvc"
|
||||
PROJECT_FILE = "dvc.yaml"
|
||||
LOCK_FILE = "dvc.lock"
|
||||
@@ -56,10 +54,7 @@ class ParametrizedDumpError(DvcException):
|
||||
|
||||
|
||||
def is_valid_filename(path):
|
||||
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [
|
||||
DVC_FILE,
|
||||
PROJECT_FILE,
|
||||
]
|
||||
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) == PROJECT_FILE
|
||||
|
||||
|
||||
def is_dvc_file(path):
|
||||
@@ -358,36 +353,8 @@ class ProjectFile(FileMixin):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def get_lockfile_schema(d):
|
||||
from dvc.schema import COMPILED_LOCKFILE_V1_SCHEMA, COMPILED_LOCKFILE_V2_SCHEMA
|
||||
|
||||
schema = {
|
||||
LOCKFILE_VERSION.V1: COMPILED_LOCKFILE_V1_SCHEMA,
|
||||
LOCKFILE_VERSION.V2: COMPILED_LOCKFILE_V2_SCHEMA,
|
||||
}
|
||||
|
||||
version = LOCKFILE_VERSION.from_dict(d)
|
||||
return schema[version]
|
||||
|
||||
|
||||
def migrate_lock_v1_to_v2(d, version_info):
|
||||
stages = dict(d)
|
||||
|
||||
for key in stages:
|
||||
d.pop(key)
|
||||
|
||||
# forcing order, meta should always be at the top
|
||||
d.update(version_info)
|
||||
d["stages"] = stages
|
||||
|
||||
|
||||
def lockfile_schema(data: _T) -> _T:
|
||||
schema = get_lockfile_schema(data)
|
||||
return schema(data)
|
||||
|
||||
|
||||
class Lockfile(FileMixin):
|
||||
SCHEMA = staticmethod(lockfile_schema) # type: ignore[assignment]
|
||||
from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA
|
||||
|
||||
def _verify_filename(self):
|
||||
pass # lockfile path is hardcoded, so no need to verify here
|
||||
@@ -401,21 +368,12 @@ class Lockfile(FileMixin):
|
||||
self._check_gitignored()
|
||||
return {}, ""
|
||||
|
||||
@property
|
||||
def latest_version_info(self):
|
||||
version = LOCKFILE_VERSION.V2.value # pylint:disable=no-member
|
||||
return {SCHEMA_KWD: version}
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
version = LOCKFILE_VERSION.from_dict(data)
|
||||
if version == LOCKFILE_VERSION.V1:
|
||||
logger.info("Migrating lock file '%s' from v1 to v2", self.relpath)
|
||||
migrate_lock_v1_to_v2(data, self.latest_version_info)
|
||||
elif not data:
|
||||
data.update(self.latest_version_info)
|
||||
if not data:
|
||||
data.update({"schema": "2.0"})
|
||||
# order is important, meta should always be at the top
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
|
||||
@@ -436,8 +394,7 @@ class Lockfile(FileMixin):
|
||||
return
|
||||
|
||||
d, _ = self._load_yaml(round_trip=True)
|
||||
version = LOCKFILE_VERSION.from_dict(d)
|
||||
data = d if version == LOCKFILE_VERSION.V1 else d.get("stages", {})
|
||||
data = d.get("stages", {})
|
||||
if stage.name not in data:
|
||||
return
|
||||
|
||||
|
||||
+13
-8
@@ -1,12 +1,17 @@
|
||||
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"
|
||||
|
||||
+13
-20
@@ -1,4 +1,5 @@
|
||||
"""Exceptions raised by the dvc."""
|
||||
import errno
|
||||
from typing import Dict, List
|
||||
|
||||
from dvc.utils import format_link
|
||||
@@ -181,11 +182,6 @@ class BadMetricError(DvcException):
|
||||
)
|
||||
|
||||
|
||||
class RecursiveAddingWhileUsingFilename(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__("cannot use `fname` with multiple targets or `-R|--recursive`")
|
||||
|
||||
|
||||
class OverlappingOutputPathsError(DvcException):
|
||||
def __init__(self, parent, overlapping_out, message):
|
||||
self.parent = parent
|
||||
@@ -206,6 +202,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
|
||||
@@ -291,21 +299,6 @@ class URLMissingError(DvcException):
|
||||
super().__init__(f"The path '{url}' does not exist")
|
||||
|
||||
|
||||
class RemoteCacheRequiredError(DvcException):
|
||||
def __init__(self, scheme, fs_path):
|
||||
super().__init__(
|
||||
(
|
||||
"Current operation was unsuccessful because '{}' requires "
|
||||
"existing cache on '{}' remote. See {} for information on how "
|
||||
"to set up remote cache."
|
||||
).format(
|
||||
fs_path,
|
||||
scheme,
|
||||
format_link("https://man.dvc.org/config#cache"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class IsADirectoryError(DvcException): # noqa,pylint:disable=redefined-builtin
|
||||
"""Raised when a file operation is requested on a directory."""
|
||||
|
||||
|
||||
+55
-5
@@ -2,12 +2,9 @@
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_objects.fs.callbacks import ( # noqa: F401
|
||||
DEFAULT_CALLBACK,
|
||||
Callback,
|
||||
TqdmCallback,
|
||||
)
|
||||
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.progress import TaskID
|
||||
@@ -15,6 +12,59 @@ if TYPE_CHECKING:
|
||||
from dvc.ui._rich_progress import RichTransferProgress
|
||||
|
||||
|
||||
# NOTE: this is very similar to dvc-objects.fs.callbacks.TqdmCallback,
|
||||
# but it works with our own Tqdm instance.
|
||||
class TqdmCallback(Callback):
|
||||
def __init__(
|
||||
self,
|
||||
size: Optional[int] = None,
|
||||
value: int = 0,
|
||||
progress_bar: Optional["Tqdm"] = None,
|
||||
**tqdm_kwargs,
|
||||
):
|
||||
tqdm_kwargs["total"] = size or -1
|
||||
self._tqdm_kwargs = tqdm_kwargs
|
||||
self._progress_bar = progress_bar
|
||||
self._stack = ExitStack()
|
||||
super().__init__(size=size, value=value)
|
||||
|
||||
@cached_property
|
||||
def progress_bar(self):
|
||||
progress_bar = (
|
||||
self._progress_bar
|
||||
if self._progress_bar is not None
|
||||
else Tqdm(**self._tqdm_kwargs)
|
||||
)
|
||||
return self._stack.enter_context(progress_bar)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
self._stack.close()
|
||||
|
||||
def set_size(self, size):
|
||||
# Tqdm tries to be smart when to refresh,
|
||||
# so we try to force it to re-render.
|
||||
super().set_size(size)
|
||||
self.progress_bar.refresh()
|
||||
|
||||
def call(self, hook_name=None, **kwargs): # noqa: ARG002
|
||||
self.progress_bar.update_to(self.value, total=self.size)
|
||||
|
||||
def branch(
|
||||
self,
|
||||
path_1: "Union[str, BinaryIO]",
|
||||
path_2: str,
|
||||
kwargs: Dict[str, Any],
|
||||
child: Optional[Callback] = None,
|
||||
):
|
||||
child = child or TqdmCallback(
|
||||
bytes=True, desc=path_1 if isinstance(path_1, str) else path_2
|
||||
)
|
||||
return super().branch(path_1, path_2, kwargs, child=child)
|
||||
|
||||
|
||||
class RichCallback(Callback):
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
+14
-7
@@ -38,12 +38,17 @@ def _is_dvc_file(fname):
|
||||
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
|
||||
|
||||
def _merge_info(repo, fs_info, dvc_info):
|
||||
def _merge_info(repo, key, fs_info, dvc_info):
|
||||
from . import utils
|
||||
|
||||
ret = {"repo": repo}
|
||||
|
||||
if dvc_info:
|
||||
dvc_info["isout"] = any(
|
||||
(len(out_key) <= len(key) and key[: len(out_key)] == out_key)
|
||||
for out_key in repo.index.data_keys["repo"]
|
||||
)
|
||||
dvc_info["isdvc"] = dvc_info["isout"]
|
||||
ret["dvc_info"] = dvc_info
|
||||
ret["type"] = dvc_info["type"]
|
||||
ret["size"] = dvc_info["size"]
|
||||
@@ -263,10 +268,10 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
"""Is this entry dvc-tracked?"""
|
||||
key = self._get_key_from_relative(path)
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
|
||||
try:
|
||||
return self.info(path).get("dvc_info", {}).get("isout", False)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def ls( # pylint: disable=arguments-differ # noqa: C901
|
||||
self, path, detail=True, dvc_only=False, **kwargs
|
||||
@@ -314,7 +319,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
continue
|
||||
|
||||
entry_path = self.path.join(path, name)
|
||||
info = _merge_info(repo, fs_infos.get(name), dvc_infos.get(name))
|
||||
info = _merge_info(
|
||||
repo, (*subkey, name), fs_infos.get(name), dvc_infos.get(name)
|
||||
)
|
||||
info["name"] = entry_path
|
||||
infos.append(info)
|
||||
paths.append(entry_path)
|
||||
@@ -370,7 +377,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
if not dvc_info and not fs_info:
|
||||
raise FileNotFoundError
|
||||
|
||||
info = _merge_info(repo, fs_info, dvc_info)
|
||||
info = _merge_info(repo, subkey, fs_info, dvc_info)
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
|
||||
+20
-9
@@ -6,13 +6,12 @@ import platform
|
||||
|
||||
import psutil
|
||||
|
||||
from dvc import __version__
|
||||
from dvc import PKG, __version__
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.fs import Schemes, generic, get_fs_cls, get_fs_config, registry
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import SCMError
|
||||
from dvc.utils import error_link
|
||||
from dvc.utils.pkg import PKG
|
||||
|
||||
SUBPROJECTS = (
|
||||
"dvc_data",
|
||||
@@ -32,6 +31,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 +42,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 +51,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}")
|
||||
@@ -67,7 +67,7 @@ def _get_caches(cache):
|
||||
caches = (
|
||||
cache_type
|
||||
for cache_type, cache_instance in cache.by_scheme()
|
||||
if cache_instance and cache_type != "repo"
|
||||
if cache_instance and cache_type not in ("repo", "legacy")
|
||||
)
|
||||
|
||||
# Caches will be always non-empty including the local cache
|
||||
@@ -136,7 +136,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 +166,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"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# TODO: remove in 3.0, kept for backward compatibility
|
||||
from .cli import main # noqa: F401, pylint: disable=unused-import
|
||||
+290
-82
@@ -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
|
||||
@@ -18,33 +19,32 @@ from dvc.exceptions import (
|
||||
ConfirmRemoveError,
|
||||
DvcException,
|
||||
MergeError,
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc.utils.objects import cached_property
|
||||
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 import DEFAULT_ALGORITHM
|
||||
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__)
|
||||
@@ -64,7 +64,7 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
|
||||
|
||||
# NOTE: currently there are only 3 possible checksum names:
|
||||
#
|
||||
# 1) md5 (LOCAL, SSH);
|
||||
# 1) md5 (LOCAL, SSH) (actually DVC 2.x md5-dos2unix)
|
||||
# 2) etag (S3, GS, OSS, AZURE, HTTP);
|
||||
# 3) checksum (HDFS);
|
||||
#
|
||||
@@ -73,7 +73,7 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
|
||||
HDFS_PARAM_CHECKSUM = "checksum"
|
||||
S3_PARAM_CHECKSUM = "etag"
|
||||
CHECKSUMS_SCHEMA = {
|
||||
LocalFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
"md5": CHECKSUM_SCHEMA, # DVC 2.x md5-dos2unix
|
||||
HDFS_PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3_PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
|
||||
}
|
||||
@@ -91,11 +91,11 @@ def loadd_from(stage, d_list):
|
||||
metric = d.pop(Output.PARAM_METRIC, False)
|
||||
plot = d.pop(Output.PARAM_PLOT, False)
|
||||
persist = d.pop(Output.PARAM_PERSIST, False)
|
||||
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
|
||||
remote = d.pop(Output.PARAM_REMOTE, None)
|
||||
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
push = d.pop(Output.PARAM_PUSH, True)
|
||||
hash_name = d.pop(Output.PARAM_HASH, None)
|
||||
ret.append(
|
||||
_get(
|
||||
stage,
|
||||
@@ -105,11 +105,11 @@ def loadd_from(stage, d_list):
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
remote=remote,
|
||||
**annot,
|
||||
files=files,
|
||||
push=push,
|
||||
hash_name=hash_name,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
@@ -122,7 +122,6 @@ def loads_from(
|
||||
metric=False,
|
||||
plot=False,
|
||||
persist=False,
|
||||
checkpoint=False,
|
||||
remote=None,
|
||||
push=True,
|
||||
):
|
||||
@@ -135,7 +134,6 @@ def loads_from(
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
remote=remote,
|
||||
push=push,
|
||||
)
|
||||
@@ -191,7 +189,6 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
[
|
||||
Output.PARAM_CACHE,
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
Output.PARAM_REMOTE,
|
||||
Output.PARAM_PUSH,
|
||||
*ANNOTATION_FIELDS,
|
||||
@@ -232,17 +229,29 @@ def merge_file_meta_from_cloud(entry: Dict) -> Dict:
|
||||
return entry
|
||||
|
||||
|
||||
def _serialize_tree_obj_to_files(obj: "Tree") -> List[Dict[str, Any]]:
|
||||
def _serialize_tree_obj_to_files(obj: Tree) -> List[Dict[str, Any]]:
|
||||
key = obj.PARAM_RELPATH
|
||||
return sorted(
|
||||
(
|
||||
{key: posixpath.sep.join(parts), **hi.to_dict(), **meta.to_dict()}
|
||||
{
|
||||
key: posixpath.sep.join(parts),
|
||||
**_serialize_hi_to_dict(hi),
|
||||
**meta.to_dict(),
|
||||
}
|
||||
for parts, meta, hi in obj
|
||||
),
|
||||
key=itemgetter(key),
|
||||
)
|
||||
|
||||
|
||||
def _serialize_hi_to_dict(hash_info: Optional[HashInfo]) -> Dict[str, Any]:
|
||||
if hash_info:
|
||||
if hash_info.name == "md5-dos2unix":
|
||||
return {"md5": hash_info.value}
|
||||
return hash_info.to_dict()
|
||||
return {}
|
||||
|
||||
|
||||
class OutputDoesNotExistError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = f"output '{path}' does not exist"
|
||||
@@ -276,12 +285,16 @@ 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
|
||||
|
||||
PARAM_PATH = "path"
|
||||
PARAM_CACHE = "cache"
|
||||
PARAM_CHECKPOINT = "checkpoint"
|
||||
PARAM_FILES = "files"
|
||||
PARAM_METRIC = "metric"
|
||||
PARAM_METRIC_TYPE = "type"
|
||||
@@ -298,15 +311,7 @@ class Output:
|
||||
PARAM_REMOTE = "remote"
|
||||
PARAM_PUSH = "push"
|
||||
PARAM_CLOUD = "cloud"
|
||||
|
||||
METRIC_SCHEMA = Any(
|
||||
None,
|
||||
bool,
|
||||
{
|
||||
PARAM_METRIC_TYPE: Any(str, None),
|
||||
PARAM_METRIC_XPATH: Any(str, None),
|
||||
},
|
||||
)
|
||||
PARAM_HASH = "hash"
|
||||
|
||||
DoesNotExistError: Type[DvcException] = OutputDoesNotExistError
|
||||
IsNotFileOrDirError: Type[DvcException] = OutputIsNotFileOrDirError
|
||||
@@ -322,7 +327,6 @@ class Output:
|
||||
metric=False,
|
||||
plot=False,
|
||||
persist=False,
|
||||
checkpoint=False,
|
||||
desc=None,
|
||||
type=None, # noqa: A002, pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
@@ -332,6 +336,7 @@ class Output:
|
||||
fs_config=None,
|
||||
files: Optional[List[Dict[str, Any]]] = None,
|
||||
push: bool = True,
|
||||
hash_name: Optional[str] = DEFAULT_ALGORITHM,
|
||||
):
|
||||
self.annot = Annotation(
|
||||
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
||||
@@ -389,7 +394,6 @@ class Output:
|
||||
self.metric = False if self.IS_DEPENDENCY else metric
|
||||
self.plot = False if self.IS_DEPENDENCY else plot
|
||||
self.persist = persist
|
||||
self.checkpoint = checkpoint
|
||||
self.can_push = push
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
@@ -404,21 +408,32 @@ class Output:
|
||||
)
|
||||
self.meta.version_id = version_id
|
||||
|
||||
if self.is_in_repo:
|
||||
self.hash_name = "md5"
|
||||
else:
|
||||
self.hash_name = self.fs.PARAM_CHECKSUM
|
||||
|
||||
self.hash_info = HashInfo(
|
||||
name=self.hash_name,
|
||||
value=getattr(self.meta, self.hash_name, None),
|
||||
)
|
||||
self.hash_name, self.hash_info = self._compute_hash_info_from_meta(hash_name)
|
||||
self._compute_meta_hash_info_from_files()
|
||||
|
||||
def _compute_hash_info_from_meta(
|
||||
self, hash_name: Optional[str]
|
||||
) -> Tuple[str, HashInfo]:
|
||||
if self.is_in_repo:
|
||||
if hash_name is None:
|
||||
# Legacy 2.x output, use "md5-dos2unix" but read "md5" from
|
||||
# file meta
|
||||
hash_name = "md5-dos2unix"
|
||||
meta_name = "md5"
|
||||
else:
|
||||
meta_name = hash_name
|
||||
else:
|
||||
hash_name = meta_name = self.fs.PARAM_CHECKSUM
|
||||
assert hash_name
|
||||
|
||||
hash_info = HashInfo(
|
||||
name=hash_name,
|
||||
value=getattr(self.meta, meta_name, None),
|
||||
)
|
||||
return hash_name, hash_info
|
||||
|
||||
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")
|
||||
@@ -431,7 +446,7 @@ class Output:
|
||||
self.meta.remote = first(f.get("remote") for f in self.files)
|
||||
elif self.meta.nfiles or self.hash_info and self.hash_info.isdir:
|
||||
self.meta.isdir = True
|
||||
if not self.hash_info and self.hash_name != "md5":
|
||||
if not self.hash_info and self.hash_name not in ("md5", "md5-dos2unix"):
|
||||
md5 = getattr(self.meta, "md5", None)
|
||||
if md5:
|
||||
self.hash_info = HashInfo("md5", md5)
|
||||
@@ -495,7 +510,7 @@ class Output:
|
||||
return False
|
||||
|
||||
return self.repo and self.fs.path.isin(
|
||||
self.fs.path.realpath(self.fs_path),
|
||||
self.fs_path,
|
||||
self.repo.root_dir,
|
||||
)
|
||||
|
||||
@@ -508,11 +523,19 @@ class Output:
|
||||
|
||||
@property
|
||||
def cache(self):
|
||||
odb_name = "repo" if self.is_in_repo else self.protocol
|
||||
odb = getattr(self.repo.cache, odb_name)
|
||||
if self.use_cache and odb is None:
|
||||
raise RemoteCacheRequiredError(self.fs.protocol, self.fs_path)
|
||||
return odb
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
assert self.is_in_repo
|
||||
odb_name = "legacy" if self.hash_name in LEGACY_HASH_NAMES else "repo"
|
||||
return getattr(self.repo.cache, odb_name)
|
||||
|
||||
@property
|
||||
def local_cache(self):
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
if self.hash_name in LEGACY_HASH_NAMES:
|
||||
return self.repo.cache.legacy
|
||||
return self.repo.cache.local
|
||||
|
||||
@property
|
||||
def cache_path(self):
|
||||
@@ -528,7 +551,7 @@ class Output:
|
||||
if self.use_cache:
|
||||
odb = self.cache
|
||||
else:
|
||||
odb = self.repo.cache.local
|
||||
odb = self.local_cache
|
||||
_, meta, obj = build(
|
||||
odb,
|
||||
self.fs_path,
|
||||
@@ -669,6 +692,7 @@ class Output:
|
||||
if self.metric:
|
||||
self.verify_metric()
|
||||
|
||||
self._update_legacy_hash_name()
|
||||
if self.use_cache:
|
||||
_, self.meta, self.obj = build(
|
||||
self.cache,
|
||||
@@ -679,7 +703,7 @@ class Output:
|
||||
)
|
||||
else:
|
||||
_, self.meta, self.obj = build(
|
||||
self.repo.cache.local,
|
||||
self.local_cache,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
@@ -692,6 +716,10 @@ class Output:
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.files = None
|
||||
|
||||
def _update_legacy_hash_name(self):
|
||||
if self.hash_name == "md5-dos2unix" and self.changed_checksum():
|
||||
self.hash_name = "md5"
|
||||
|
||||
def set_exec(self) -> None:
|
||||
if self.isfile() and self.meta.isexec:
|
||||
self.cache.set_exec(self.fs_path)
|
||||
@@ -741,15 +769,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"]:
|
||||
@@ -775,6 +806,8 @@ class Output:
|
||||
return checkout_obj
|
||||
|
||||
def dumpd(self, **kwargs): # noqa: C901, PLR0912
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
ret: Dict[str, Any] = {}
|
||||
with_files = (
|
||||
(not self.IS_DEPENDENCY or self.stage.is_import)
|
||||
@@ -785,7 +818,12 @@ class Output:
|
||||
if not with_files:
|
||||
meta_d = self.meta.to_dict()
|
||||
meta_d.pop("isdir", None)
|
||||
ret.update(self.hash_info.to_dict())
|
||||
if self.hash_name in LEGACY_HASH_NAMES:
|
||||
# 2.x checksums get serialized with file meta
|
||||
name = "md5" if self.hash_name == "md5-dos2unix" else self.hash_name
|
||||
ret.update({name: self.hash_info.value})
|
||||
else:
|
||||
ret.update(self.hash_info.to_dict())
|
||||
ret.update(split_file_meta_from_cloud(meta_d))
|
||||
|
||||
if self.is_in_repo:
|
||||
@@ -793,6 +831,9 @@ class Output:
|
||||
else:
|
||||
path = self.def_path
|
||||
|
||||
if self.hash_name not in LEGACY_HASH_NAMES:
|
||||
ret[self.PARAM_HASH] = "md5"
|
||||
|
||||
ret[self.PARAM_PATH] = path
|
||||
|
||||
if not self.IS_DEPENDENCY:
|
||||
@@ -816,9 +857,6 @@ class Output:
|
||||
if self.persist:
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
|
||||
if self.checkpoint:
|
||||
ret[self.PARAM_CHECKPOINT] = self.checkpoint
|
||||
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
|
||||
@@ -888,12 +926,18 @@ class Output:
|
||||
relink: bool = False,
|
||||
filter_info: Optional[str] = None,
|
||||
allow_missing: bool = False,
|
||||
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)
|
||||
@@ -901,11 +945,6 @@ class Output:
|
||||
# backward compatibility
|
||||
return None
|
||||
|
||||
if self.checkpoint and checkpoint_reset:
|
||||
if self.exists:
|
||||
self.remove()
|
||||
return None
|
||||
|
||||
added = not self.exists
|
||||
|
||||
try:
|
||||
@@ -915,21 +954,24 @@ class Output:
|
||||
obj,
|
||||
self.cache,
|
||||
force=force,
|
||||
progress_callback=progress_callback,
|
||||
progress_callback=callback,
|
||||
relink=relink,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
**kwargs,
|
||||
)
|
||||
except CheckoutError:
|
||||
if allow_missing or self.checkpoint:
|
||||
if allow_missing:
|
||||
return None
|
||||
raise
|
||||
self.set_exec()
|
||||
return added, False if added else modified
|
||||
|
||||
def remove(self, ignore_remove=False):
|
||||
self.fs.remove(self.fs_path, recursive=True)
|
||||
try:
|
||||
self.fs.remove(self.fs_path, recursive=True)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if self.protocol != Schemes.LOCAL:
|
||||
return
|
||||
|
||||
@@ -974,7 +1016,7 @@ class Output:
|
||||
odb,
|
||||
from_info,
|
||||
from_fs,
|
||||
"md5",
|
||||
DEFAULT_ALGORITHM,
|
||||
upload=upload,
|
||||
no_progress_bar=no_progress_bar,
|
||||
)
|
||||
@@ -1008,7 +1050,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 +1064,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
|
||||
@@ -1112,7 +1157,9 @@ class Output:
|
||||
return {}
|
||||
|
||||
if self.remote:
|
||||
remote = self.repo.cloud.get_remote_odb(name=self.remote)
|
||||
remote = self.repo.cloud.get_remote_odb(
|
||||
name=self.remote, hash_name=self.hash_name
|
||||
)
|
||||
else:
|
||||
remote = None
|
||||
|
||||
@@ -1151,13 +1198,14 @@ class Output:
|
||||
self.hash_name,
|
||||
Meta.PARAM_SIZE,
|
||||
Meta.PARAM_NFILES,
|
||||
Output.PARAM_HASH,
|
||||
]
|
||||
|
||||
for opt in ignored:
|
||||
my.pop(opt, None)
|
||||
other.pop(opt, None)
|
||||
|
||||
if my != other:
|
||||
if my != other or self.hash_name != out.hash_name:
|
||||
raise MergeError("unable to auto-merge outputs with different options")
|
||||
|
||||
if not out.is_dir_checksum:
|
||||
@@ -1165,7 +1213,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 +1246,168 @@ 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
|
||||
self._update_legacy_hash_name()
|
||||
cache = self.cache if self.use_cache else self.local_cache
|
||||
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
|
||||
@@ -1257,10 +1467,9 @@ ARTIFACT_SCHEMA = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required(Output.PARAM_PATH): str,
|
||||
Output.PARAM_PLOT: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
Output.PARAM_CLOUD: CLOUD_SCHEMA,
|
||||
Output.PARAM_HASH: str,
|
||||
}
|
||||
|
||||
DIR_FILES_SCHEMA: Dict[str, Any] = {
|
||||
@@ -1274,7 +1483,6 @@ SCHEMA = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**ANNOTATION_SCHEMA,
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
|
||||
Output.PARAM_REMOTE: str,
|
||||
Output.PARAM_PUSH: bool,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import enum
|
||||
from collections.abc import Mapping
|
||||
|
||||
from voluptuous import validators
|
||||
|
||||
SCHEMA_KWD = "schema"
|
||||
META_KWD = "meta"
|
||||
|
||||
|
||||
def lockfile_version_schema(value):
|
||||
expected = [LOCKFILE_VERSION.V2.value] # pylint: disable=no-member
|
||||
msg = f"invalid schema version {value}, expected one of {expected}"
|
||||
return validators.Any(*expected, msg=msg)(value)
|
||||
|
||||
|
||||
class VersionEnum(str, enum.Enum):
|
||||
@classmethod
|
||||
def all_versions(cls):
|
||||
return [v.value for v in cls]
|
||||
|
||||
|
||||
class LOCKFILE_VERSION(VersionEnum): # noqa: N801
|
||||
V1 = "1.0"
|
||||
V2 = "2.0"
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
# 1) if it's empty or or is not a dict, use the latest one (V2).
|
||||
# 2) use the `schema` identifier if it exists and is a supported
|
||||
# version
|
||||
# 3) if it's not in any of the supported version, use the latest one
|
||||
# 4) if there's no identifier, it's a V1
|
||||
if not data or not isinstance(data, Mapping):
|
||||
return cls(cls.V2)
|
||||
|
||||
version = data.get(SCHEMA_KWD)
|
||||
if version:
|
||||
return cls(version if version in cls.all_versions() else cls.V2)
|
||||
return cls(cls.V1)
|
||||
@@ -2,12 +2,16 @@
|
||||
import logging
|
||||
import sys
|
||||
from threading import RLock
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from dvc.env import DVC_IGNORE_ISATTY
|
||||
from dvc.utils import env2bool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
tqdm.set_lock(RLock())
|
||||
|
||||
@@ -158,3 +162,8 @@ class Tqdm(tqdm):
|
||||
d["ncols_desc"] = d["ncols_info"] = 1
|
||||
d["prefix"] = ""
|
||||
return d
|
||||
|
||||
def as_callback(self) -> "TqdmCallback":
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
|
||||
return TqdmCallback(progress_bar=self)
|
||||
|
||||
+14
-24
@@ -8,7 +8,6 @@ from typing import (
|
||||
Callable,
|
||||
ContextManager,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
@@ -287,18 +286,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"]:
|
||||
@@ -377,6 +367,14 @@ class Repo:
|
||||
|
||||
return self._data_index
|
||||
|
||||
def drop_data_index(self) -> None:
|
||||
try:
|
||||
self.data_index.delete_node(("tree",))
|
||||
except KeyError:
|
||||
pass
|
||||
self.data_index.commit()
|
||||
self._reset()
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}: '{self.root_dir}'"
|
||||
|
||||
@@ -430,8 +428,8 @@ class Repo:
|
||||
flist = [self.config.files["local"]]
|
||||
if tmp_dir := self.tmp_dir:
|
||||
flist.append(tmp_dir)
|
||||
if path_isin(self.cache.repo.path, self.root_dir):
|
||||
flist.append(self.cache.repo.path)
|
||||
if path_isin(self.cache.legacy.path, self.root_dir):
|
||||
flist.append(self.cache.legacy.path)
|
||||
|
||||
for file in flist:
|
||||
self.scm_context.ignore(file)
|
||||
@@ -509,12 +507,6 @@ class Repo:
|
||||
|
||||
return used
|
||||
|
||||
@property
|
||||
def stages(
|
||||
self,
|
||||
) -> List["Stage"]: # obsolete, only for backward-compatibility
|
||||
return self.index.stages
|
||||
|
||||
def find_outs_by_path(self, path, outs=None, recursive=False, strict=True):
|
||||
# using `outs_graph` to ensure graph checks are run
|
||||
outs = outs or self.index.outs_graph
|
||||
@@ -563,12 +555,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 = ()
|
||||
|
||||
+153
-184
@@ -1,96 +1,120 @@
|
||||
import logging
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from itertools import tee
|
||||
from typing import TYPE_CHECKING, Any, Iterator, List, Optional
|
||||
|
||||
import colorama
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc.exceptions import (
|
||||
CacheLinkError,
|
||||
InvalidArgumentError,
|
||||
DvcException,
|
||||
OutputDuplicationError,
|
||||
OutputNotFoundError,
|
||||
OverlappingOutputPathsError,
|
||||
RecursiveAddingWhileUsingFilename,
|
||||
)
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import LARGE_DIR_SIZE, glob_targets, resolve_output, resolve_paths
|
||||
from dvc.utils.collections import ensure_list, validate
|
||||
from dvc.utils import glob_targets, resolve_output, resolve_paths
|
||||
|
||||
from . import locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage import Stage
|
||||
from dvc.types import TargetType
|
||||
|
||||
Stages = List["Stage"]
|
||||
logger = logging.getLogger(__name__)
|
||||
from dvc.types import StrOrBytesPath
|
||||
|
||||
|
||||
OVERLAPPING_OUTPUT_FMT = (
|
||||
class StageInfo(NamedTuple):
|
||||
stage: "Stage"
|
||||
output_exists: bool
|
||||
|
||||
|
||||
def find_targets(
|
||||
targets: Union["StrOrBytesPath", Iterator["StrOrBytesPath"]], glob: bool = False
|
||||
) -> List[str]:
|
||||
if isinstance(targets, (str, bytes, os.PathLike)):
|
||||
targets_list = [os.fsdecode(targets)]
|
||||
else:
|
||||
targets_list = [os.fsdecode(target) for target in targets]
|
||||
return glob_targets(targets_list, glob=glob)
|
||||
|
||||
|
||||
PIPELINE_TRACKED_UPDATE_FMT = (
|
||||
"cannot update {out!r}: overlaps with an output of {stage} in '{path}'.\n"
|
||||
"Run the pipeline or use 'dvc commit' to force update it."
|
||||
)
|
||||
|
||||
|
||||
def get_or_create_stage(
|
||||
repo: "Repo",
|
||||
target: str,
|
||||
out: Optional[str] = None,
|
||||
to_remote: bool = False,
|
||||
force: bool = False,
|
||||
) -> StageInfo:
|
||||
if out:
|
||||
target = resolve_output(target, out, force=force)
|
||||
path, wdir, out = resolve_paths(repo, target, always_local=to_remote and not out)
|
||||
|
||||
try:
|
||||
(out_obj,) = repo.find_outs_by_path(target, strict=False)
|
||||
stage = out_obj.stage
|
||||
if not stage.is_data_source:
|
||||
msg = PIPELINE_TRACKED_UPDATE_FMT.format(
|
||||
out=out, stage=stage, path=stage.relpath
|
||||
)
|
||||
raise DvcException(msg)
|
||||
return StageInfo(stage, output_exists=True)
|
||||
except OutputNotFoundError:
|
||||
stage = repo.stage.create(
|
||||
single_stage=True,
|
||||
validate=False,
|
||||
fname=path,
|
||||
wdir=wdir,
|
||||
outs=[out],
|
||||
force=force,
|
||||
)
|
||||
return StageInfo(stage, output_exists=False)
|
||||
|
||||
|
||||
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}'"
|
||||
)
|
||||
|
||||
|
||||
def check_recursive_and_fname(args):
|
||||
if args.recursive and args.fname:
|
||||
raise RecursiveAddingWhileUsingFilename()
|
||||
|
||||
|
||||
def transform_targets(args):
|
||||
from funcy import count_reps
|
||||
|
||||
counts = count_reps(ensure_list(args.targets))
|
||||
dupes = [key for key, count in counts.items() if count > 1]
|
||||
if dupes:
|
||||
msg = ", ".join(f"[b]{key}[/]" for key in dupes)
|
||||
ui.error_write(f"ignoring duplicated targets: {msg}", styled=True)
|
||||
args.targets = list(counts)
|
||||
|
||||
|
||||
def check_arg_combinations(args):
|
||||
kwargs = args.kwargs
|
||||
invalid_opt = None
|
||||
to_remote = args.to_remote
|
||||
to_cache = kwargs.get("out") and not to_remote
|
||||
|
||||
if to_remote or to_cache:
|
||||
message = "{option} can't be used with "
|
||||
message += "--to-remote" if to_remote else "-o"
|
||||
if len(args.targets) != 1:
|
||||
invalid_opt = "multiple targets"
|
||||
elif args.no_commit:
|
||||
invalid_opt = "--no-commit option"
|
||||
elif args.recursive:
|
||||
invalid_opt = "--recursive option"
|
||||
elif kwargs.get("external"):
|
||||
invalid_opt = "--external option"
|
||||
else:
|
||||
message = "{option} can't be used without --to-remote"
|
||||
if kwargs.get("remote"):
|
||||
invalid_opt = "--remote"
|
||||
elif kwargs.get("jobs"):
|
||||
invalid_opt = "--jobs"
|
||||
|
||||
if invalid_opt is not None:
|
||||
raise InvalidArgumentError(message.format(option=invalid_opt))
|
||||
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}'"
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def translate_graph_error(stages: Stages) -> Iterator[None]:
|
||||
def translate_graph_error(stages: List["Stage"]) -> 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,18 +124,21 @@ def translate_graph_error(stages: Stages) -> Iterator[None]:
|
||||
)
|
||||
|
||||
|
||||
def progress_iter(stages: Stages) -> Iterator["Stage"]:
|
||||
def progress_iter(stages: Dict[str, StageInfo]) -> Iterator[Tuple[str, StageInfo]]:
|
||||
total = len(stages)
|
||||
desc = "Adding..."
|
||||
with ui.progress(stages, total=total, desc=desc, unit="file", leave=True) as pbar:
|
||||
with ui.progress(
|
||||
stages.items(), total=total, desc=desc, unit="file", leave=True
|
||||
) as pbar:
|
||||
if total == 1:
|
||||
pbar.bar_format = desc
|
||||
pbar.refresh()
|
||||
|
||||
for stage in pbar:
|
||||
for item, stage_info in pbar:
|
||||
if total > 1:
|
||||
pbar.set_msg(f"{stage.outs[0]}")
|
||||
yield stage
|
||||
pbar.set_msg(str(stage_info.stage.outs[0]))
|
||||
pbar.refresh()
|
||||
yield item, stage_info
|
||||
if total == 1: # restore bar format for stats
|
||||
# pylint: disable=no-member
|
||||
pbar.bar_format = pbar.BAR_FMT_DEFAULT
|
||||
@@ -138,133 +165,75 @@ def warn_link_failures() -> Iterator[List[str]]:
|
||||
ui.error_write(msg)
|
||||
|
||||
|
||||
VALIDATORS = (
|
||||
check_recursive_and_fname,
|
||||
transform_targets,
|
||||
check_arg_combinations,
|
||||
)
|
||||
def _add_transfer(
|
||||
stage: "Stage",
|
||||
source: str,
|
||||
remote: Optional[str] = None,
|
||||
to_remote: bool = False,
|
||||
jobs: Optional[int] = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
odb = None
|
||||
if to_remote:
|
||||
odb = stage.repo.cloud.get_remote_odb(remote, "add")
|
||||
stage.transfer(source, odb=odb, to_remote=to_remote, jobs=jobs, force=force)
|
||||
stage.dump()
|
||||
|
||||
|
||||
def _add(stage: "Stage", source: Optional[str] = None, no_commit: bool = False) -> None:
|
||||
out = stage.outs[0]
|
||||
path = out.fs.path.abspath(source) if source else None
|
||||
try:
|
||||
stage.add_outs(path, no_commit=no_commit)
|
||||
except CacheLinkError:
|
||||
stage.dump()
|
||||
raise
|
||||
stage.dump()
|
||||
|
||||
|
||||
@validate(*VALIDATORS)
|
||||
@locked
|
||||
@scm_context
|
||||
def add(
|
||||
repo: "Repo",
|
||||
targets: "TargetType",
|
||||
recursive: bool = False,
|
||||
targets: Union["StrOrBytesPath", Iterator["StrOrBytesPath"]],
|
||||
no_commit: bool = False,
|
||||
fname: Optional[str] = None,
|
||||
glob: bool = False,
|
||||
out: Optional[str] = None,
|
||||
remote: Optional[str] = None,
|
||||
to_remote: bool = False,
|
||||
**kwargs: Any,
|
||||
):
|
||||
to_cache = bool(kwargs.get("out")) and not to_remote
|
||||
transfer = to_remote or to_cache
|
||||
remote_jobs: Optional[int] = None,
|
||||
force: bool = False,
|
||||
) -> List["Stage"]:
|
||||
add_targets = find_targets(targets, glob=glob)
|
||||
if not add_targets:
|
||||
return []
|
||||
|
||||
glob = kwargs.get("glob", False)
|
||||
add_targets = collect_targets(repo, targets, recursive, glob)
|
||||
# pass one for creating stages, other one is used for iterating here
|
||||
add_targets, sources = tee(add_targets)
|
||||
|
||||
# collect targets and build stages as we go
|
||||
desc = "Collecting targets"
|
||||
stages_it = create_stages(repo, add_targets, fname, transfer, **kwargs)
|
||||
stages = list(ui.progress(stages_it, desc=desc, unit="file"))
|
||||
msg = "Collecting stages from the workspace"
|
||||
with translate_graph_error(stages), 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_with_targets = {
|
||||
target: get_or_create_stage(
|
||||
repo,
|
||||
target,
|
||||
out=out,
|
||||
to_remote=to_remote,
|
||||
force=force,
|
||||
)
|
||||
for target in add_targets
|
||||
}
|
||||
|
||||
odb = None
|
||||
if to_remote:
|
||||
odb = repo.cloud.get_remote_odb(kwargs.get("remote"), "add")
|
||||
stages = [stage for stage, _ in stages_with_targets.values()]
|
||||
msg = "Collecting stages from the workspace"
|
||||
with translate_graph_error(stages), ui.status(msg) as st:
|
||||
repo.check_graph(stages=stages, callback=lambda: st.update("Checking graph"))
|
||||
|
||||
if to_remote or out:
|
||||
assert len(stages_with_targets) == 1, "multiple targets are unsupported"
|
||||
(source, (stage, _)) = next(iter(stages_with_targets.items()))
|
||||
_add_transfer(stage, source, remote, to_remote, jobs=remote_jobs, force=force)
|
||||
return [stage]
|
||||
|
||||
with warn_link_failures() as link_failures:
|
||||
for stage, source in zip(progress_iter(stages), sources):
|
||||
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()
|
||||
except CacheLinkError:
|
||||
link_failures.append(str(stage.relpath))
|
||||
stage.dump()
|
||||
for source, (stage, output_exists) in progress_iter(stages_with_targets):
|
||||
try:
|
||||
_add(stage, source if output_exists else None, no_commit=no_commit)
|
||||
except CacheLinkError:
|
||||
link_failures.append(stage.relpath)
|
||||
return stages
|
||||
|
||||
|
||||
LARGE_DIR_RECURSIVE_ADD_WARNING = (
|
||||
"You are adding a large directory '{target}' recursively.\n"
|
||||
"Consider tracking it as a whole instead with "
|
||||
"`{cyan}dvc add {target}{nc}`."
|
||||
)
|
||||
|
||||
|
||||
def collect_targets(
|
||||
repo: "Repo",
|
||||
targets: "TargetType",
|
||||
recursive: bool = False,
|
||||
glob: bool = False,
|
||||
) -> Iterator[str]:
|
||||
for target in glob_targets(ensure_list(targets), glob=glob):
|
||||
expanded_targets = _find_all_targets(repo, target, recursive=recursive)
|
||||
for index, path in enumerate(expanded_targets):
|
||||
if index == LARGE_DIR_SIZE:
|
||||
msg = LARGE_DIR_RECURSIVE_ADD_WARNING.format(
|
||||
cyan=colorama.Fore.CYAN,
|
||||
nc=colorama.Style.RESET_ALL,
|
||||
target=target,
|
||||
)
|
||||
ui.error_write(msg)
|
||||
yield path
|
||||
|
||||
|
||||
def _find_all_targets(
|
||||
repo: "Repo", target: str, recursive: bool = False
|
||||
) -> Iterator[str]:
|
||||
from dvc.dvcfile import is_dvc_file
|
||||
|
||||
if os.path.isdir(target) and recursive:
|
||||
files = repo.dvcignore.find(repo.fs, target)
|
||||
yield from (
|
||||
path
|
||||
for path in files
|
||||
if not repo.is_dvc_internal(path)
|
||||
if not is_dvc_file(path)
|
||||
if not repo.scm.belongs_to_scm(path)
|
||||
if not repo.scm.is_tracked(path)
|
||||
)
|
||||
else:
|
||||
yield target
|
||||
|
||||
|
||||
def create_stages(
|
||||
repo: "Repo",
|
||||
targets: Iterator[str],
|
||||
fname: Optional[str] = None,
|
||||
transfer: bool = False,
|
||||
external: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Iterator["Stage"]:
|
||||
for target in targets:
|
||||
if kwargs.get("out"):
|
||||
target = resolve_output(target, kwargs["out"])
|
||||
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,
|
||||
)
|
||||
|
||||
out_obj = stage.outs[0]
|
||||
out_obj.annot.update(**kwargs)
|
||||
yield stage
|
||||
|
||||
+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)
|
||||
|
||||
+148
-50
@@ -1,15 +1,21 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, List, Set
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
from dvc.exceptions import CheckoutError, CheckoutErrorSuggestGit, NoOutputOrStageError
|
||||
from dvc.exceptions import (
|
||||
CheckoutError,
|
||||
CheckoutErrorSuggestGit,
|
||||
DvcException,
|
||||
NoOutputOrStageError,
|
||||
)
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import relpath
|
||||
|
||||
from . import locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Repo
|
||||
from .stage import StageInfo
|
||||
from dvc_data.index import BaseDataIndex, DataIndexEntry
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,37 +36,68 @@ def _remove_unused_links(repo):
|
||||
return ret
|
||||
|
||||
|
||||
def get_all_files_numbers(pairs):
|
||||
return sum(stage.get_all_files_number(filter_info) for stage, filter_info in pairs)
|
||||
def _build_out_changes(index, changes):
|
||||
from dvc_data.index.checkout import MODIFY
|
||||
|
||||
out_keys = []
|
||||
for out in index.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
ws, key = out.index_key
|
||||
if ws != "repo":
|
||||
continue
|
||||
|
||||
out_keys.append(key)
|
||||
|
||||
out_changes = {}
|
||||
for objects in changes.values():
|
||||
for change in objects:
|
||||
for out_key in out_keys:
|
||||
if (
|
||||
len(out_key) > len(change.key)
|
||||
or change.key[: len(out_key)] != out_key
|
||||
):
|
||||
continue
|
||||
|
||||
if change.key == out_key:
|
||||
out_changes[out_key] = change.typ
|
||||
elif not out_changes.get(out_key):
|
||||
out_changes[out_key] = MODIFY
|
||||
break
|
||||
|
||||
return out_changes
|
||||
|
||||
|
||||
def _collect_pairs(
|
||||
self: "Repo", targets, with_deps: bool, recursive: bool
|
||||
) -> Set["StageInfo"]:
|
||||
from dvc.stage.exceptions import StageFileBadNameError, StageFileDoesNotExistError
|
||||
|
||||
pairs: Set["StageInfo"] = set()
|
||||
for target in targets:
|
||||
def _check_can_delete(
|
||||
entries: List["DataIndexEntry"],
|
||||
index: "BaseDataIndex",
|
||||
path: str,
|
||||
fs: "FileSystem",
|
||||
):
|
||||
entry_paths = []
|
||||
for entry in entries:
|
||||
try:
|
||||
pairs.update(
|
||||
self.stage.collect_granular(
|
||||
target, with_deps=with_deps, recursive=recursive
|
||||
)
|
||||
)
|
||||
except (
|
||||
StageFileDoesNotExistError,
|
||||
StageFileBadNameError,
|
||||
NoOutputOrStageError,
|
||||
) as exc:
|
||||
if not target:
|
||||
raise
|
||||
raise CheckoutErrorSuggestGit(target) from exc
|
||||
cache_fs, cache_path = index.storage_map.get_cache(entry)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return pairs
|
||||
if cache_fs.exists(cache_path):
|
||||
continue
|
||||
|
||||
entry_paths.append(fs.path.join(path, *entry.key))
|
||||
|
||||
if not entry_paths:
|
||||
return
|
||||
|
||||
raise DvcException(
|
||||
"Can't remove the following unsaved files without confirmation. "
|
||||
"Use `--force` to force.\n" + "\n".join(entry_paths)
|
||||
)
|
||||
|
||||
|
||||
@locked
|
||||
def checkout(
|
||||
def checkout( # noqa: C901
|
||||
self,
|
||||
targets=None,
|
||||
with_deps=False,
|
||||
@@ -70,13 +107,14 @@ def checkout(
|
||||
allow_missing=False,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.fs.callbacks import Callback
|
||||
from dvc.repo.index import build_data_index
|
||||
from dvc.stage.exceptions import StageFileBadNameError, StageFileDoesNotExistError
|
||||
from dvc_data.index.checkout import ADD, DELETE, MODIFY, apply, compare
|
||||
|
||||
stats: Dict[str, List[str]] = {
|
||||
"added": [],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
"failed": [],
|
||||
}
|
||||
if not targets:
|
||||
targets = [None]
|
||||
@@ -85,28 +123,88 @@ def checkout(
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
|
||||
pairs = _collect_pairs(self, targets, with_deps, recursive)
|
||||
total = get_all_files_numbers(pairs)
|
||||
with Callback.as_tqdm_callback(
|
||||
def onerror(target, exc):
|
||||
if target and isinstance(
|
||||
exc,
|
||||
(
|
||||
StageFileDoesNotExistError,
|
||||
StageFileBadNameError,
|
||||
NoOutputOrStageError,
|
||||
),
|
||||
):
|
||||
raise CheckoutErrorSuggestGit(target) from exc
|
||||
raise # pylint: disable=misplaced-bare-raise
|
||||
|
||||
view = self.index.targets_view(
|
||||
targets,
|
||||
recursive=recursive,
|
||||
with_deps=with_deps,
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
with ui.progress(
|
||||
unit="entry",
|
||||
desc="Building data index",
|
||||
) as pb:
|
||||
old = build_data_index(
|
||||
view,
|
||||
self.root_dir,
|
||||
self.fs,
|
||||
compute_hash=True,
|
||||
callback=pb.as_callback(),
|
||||
)
|
||||
|
||||
new = view.data["repo"]
|
||||
|
||||
with ui.progress(
|
||||
desc="Comparing indexes",
|
||||
unit="entry",
|
||||
) as pb:
|
||||
diff = compare(old, new, relink=relink, delete=True, callback=pb.as_callback())
|
||||
|
||||
if not force:
|
||||
_check_can_delete(diff.files_delete, new, self.root_dir, self.fs)
|
||||
|
||||
failed = set()
|
||||
out_paths = [out.fs_path for out in view.outs if out.use_cache and out.is_in_repo]
|
||||
|
||||
def checkout_onerror(src_path, dest_path, _exc):
|
||||
logger.debug(
|
||||
"failed to create '%s' from '%s'", dest_path, src_path, exc_info=True
|
||||
)
|
||||
|
||||
for out_path in out_paths:
|
||||
if self.fs.path.isin_or_eq(dest_path, out_path):
|
||||
failed.add(out_path)
|
||||
|
||||
with ui.progress(
|
||||
unit="file",
|
||||
desc="Checkout",
|
||||
disable=total == 0,
|
||||
) as cb:
|
||||
cb.set_size(total)
|
||||
for stage, filter_info in pairs:
|
||||
result = stage.checkout(
|
||||
force=force,
|
||||
progress_callback=cb,
|
||||
relink=relink,
|
||||
filter_info=filter_info,
|
||||
allow_missing=allow_missing,
|
||||
**kwargs,
|
||||
)
|
||||
for key, items in result.items():
|
||||
stats[key].extend(_fspath_dir(path) for path in items)
|
||||
) as pb:
|
||||
apply(
|
||||
diff,
|
||||
self.root_dir,
|
||||
self.fs,
|
||||
callback=pb.as_callback(),
|
||||
update_meta=False,
|
||||
onerror=checkout_onerror,
|
||||
state=self.state,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if stats.get("failed"):
|
||||
raise CheckoutError(stats["failed"], stats)
|
||||
out_changes = _build_out_changes(view, diff.changes)
|
||||
|
||||
typ_map = {ADD: "added", DELETE: "deleted", MODIFY: "modified"}
|
||||
for key, typ in out_changes.items():
|
||||
out_path = self.fs.path.join(self.root_dir, *key)
|
||||
|
||||
if out_path in failed:
|
||||
self.fs.remove(out_path, recursive=True)
|
||||
else:
|
||||
self.state.save_link(out_path, self.fs)
|
||||
stats[typ_map[typ]].append(_fspath_dir(out_path))
|
||||
|
||||
if failed and not allow_missing:
|
||||
raise CheckoutError([relpath(out_path) for out_path in failed], stats)
|
||||
|
||||
del stats["failed"]
|
||||
return stats
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ def prompt_to_commit(stage, changes, force=False):
|
||||
@locked
|
||||
def commit(
|
||||
self,
|
||||
target,
|
||||
target=None,
|
||||
with_deps=False,
|
||||
recursive=False,
|
||||
force=False,
|
||||
|
||||
+26
-36
@@ -1,21 +1,12 @@
|
||||
import os
|
||||
import posixpath
|
||||
from itertools import chain
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, TypedDict, Union
|
||||
|
||||
from dvc.fs.callbacks import DEFAULT_CALLBACK
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.callbacks import Callback
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git, NoSCM
|
||||
from dvc_data.index import DataIndex
|
||||
@@ -61,6 +52,7 @@ def _diff(
|
||||
not_in_cache: bool = False,
|
||||
not_in_remote: bool = False,
|
||||
remote_refresh: bool = False,
|
||||
callback: "Callback" = DEFAULT_CALLBACK,
|
||||
) -> Dict[str, List[str]]:
|
||||
from dvc_data.index import StorageError
|
||||
from dvc_data.index.diff import UNCHANGED, UNKNOWN, diff
|
||||
@@ -81,13 +73,12 @@ def _diff(
|
||||
shallow=not granular,
|
||||
hash_only=True,
|
||||
with_unknown=True,
|
||||
callback=callback,
|
||||
):
|
||||
if (
|
||||
change.typ == UNCHANGED
|
||||
and change.old
|
||||
and change.new
|
||||
and not change.old.hash_info
|
||||
and not change.new.hash_info
|
||||
and (not change.old or not change.old.hash_info)
|
||||
and (not change.new or not change.new.hash_info)
|
||||
):
|
||||
# NOTE: emulating previous behaviour
|
||||
continue
|
||||
@@ -162,17 +153,27 @@ def _git_info(scm: Union["Git", "NoSCM"], untracked_files: str = "all") -> GitIn
|
||||
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
from .index import build_data_index
|
||||
|
||||
with ui.status("Building workspace index"):
|
||||
with ui.progress(
|
||||
desc="Building workspace index",
|
||||
unit="entry",
|
||||
) as pb:
|
||||
workspace = build_data_index(
|
||||
repo.index, repo.root_dir, repo.fs, compute_hash=True
|
||||
repo.index,
|
||||
repo.root_dir,
|
||||
repo.fs,
|
||||
compute_hash=True,
|
||||
callback=pb.as_callback(),
|
||||
)
|
||||
|
||||
with ui.status("Calculating diff between index/workspace"):
|
||||
with ui.progress(
|
||||
desc="Calculating diff between index/workspace",
|
||||
unit="entry",
|
||||
) as pb:
|
||||
return _diff(
|
||||
repo.index.data["repo"],
|
||||
workspace,
|
||||
not_in_cache=True,
|
||||
not_in_remote=True,
|
||||
callback=pb.as_callback(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -185,8 +186,11 @@ def _diff_head_to_index(
|
||||
with repo.switch(head):
|
||||
head_index = repo.index.data["repo"]
|
||||
|
||||
with ui.status("Calculating diff between head/index"):
|
||||
return _diff(head_index, index, **kwargs)
|
||||
with ui.progress(
|
||||
desc="Calculating diff between head/index",
|
||||
unit="entry",
|
||||
) as pb:
|
||||
return _diff(head_index, index, callback=pb.as_callback(), **kwargs)
|
||||
|
||||
|
||||
class Status(TypedDict):
|
||||
@@ -252,17 +256,3 @@ def status(
|
||||
unchanged=list(unchanged),
|
||||
git=git_info,
|
||||
)
|
||||
|
||||
|
||||
def ls(
|
||||
repo: "Repo",
|
||||
targets: Optional[List[Optional[str]]] = None,
|
||||
recursive: bool = False,
|
||||
) -> Iterator[Dict[str, Any]]:
|
||||
targets = targets or [None]
|
||||
pairs = chain.from_iterable(
|
||||
repo.stage.collect_granular(target, recursive=recursive) for target in targets
|
||||
)
|
||||
for stage, filter_info in pairs:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
yield {"path": str(out), **out.annot.to_dict()}
|
||||
|
||||
+7
-2
@@ -156,8 +156,13 @@ def diff(
|
||||
for target in old_missing & new_missing:
|
||||
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), target)
|
||||
|
||||
old = indexes[a_rev]
|
||||
new = indexes[b_rev]
|
||||
if len(indexes.keys()) == 1:
|
||||
# both a_rev and b_rev point to the same sha, nothing to compare
|
||||
old = None
|
||||
new = None
|
||||
else:
|
||||
old = indexes[a_rev]
|
||||
new = indexes[b_rev]
|
||||
|
||||
with ui.status("Calculating diff"):
|
||||
return _diff(old, new, with_missing=with_missing)
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
|
||||
|
||||
from funcy import chain, first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.objects import cached_property
|
||||
@@ -22,7 +21,6 @@ from .refs import (
|
||||
CELERY_FAILED_STASH,
|
||||
CELERY_STASH,
|
||||
EXEC_APPLY,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_NAMESPACE,
|
||||
WORKSPACE_STASH,
|
||||
@@ -48,10 +46,7 @@ class Experiments:
|
||||
repo (dvc.repo.Repo): repo instance that these experiments belong to.
|
||||
"""
|
||||
|
||||
BRANCH_RE = re.compile(
|
||||
r"^(?P<baseline_rev>[a-f0-9]{7})-(?P<exp_sha>[a-f0-9]+)"
|
||||
r"(?P<checkpoint>-checkpoint)?$"
|
||||
)
|
||||
BRANCH_RE = re.compile(r"^(?P<baseline_rev>[a-f0-9]{7})-(?P<exp_sha>[a-f0-9]+)")
|
||||
|
||||
def __init__(self, repo):
|
||||
from dvc.scm import NoSCMError
|
||||
@@ -119,6 +114,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 +122,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)
|
||||
@@ -135,47 +133,18 @@ class Experiments:
|
||||
def queue_one(
|
||||
self,
|
||||
queue: "BaseStashQueue",
|
||||
checkpoint_resume: Optional[str] = None,
|
||||
reset: bool = False,
|
||||
**kwargs,
|
||||
) -> "QueueEntry":
|
||||
"""Queue a single experiment."""
|
||||
if reset:
|
||||
self.reset_checkpoints()
|
||||
|
||||
if kwargs.pop("machine", None) is not None:
|
||||
# TODO: decide how to handle queued remote execution
|
||||
raise NotImplementedError
|
||||
|
||||
if checkpoint_resume:
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
resume_rev = resolve_rev(self.scm, checkpoint_resume)
|
||||
try:
|
||||
self.check_baseline(resume_rev)
|
||||
checkpoint_resume = resume_rev
|
||||
except BaselineMismatchError as exc:
|
||||
raise DvcException(
|
||||
f"Cannot resume from '{checkpoint_resume}' as it is not "
|
||||
"derived from your current workspace."
|
||||
) from exc
|
||||
else:
|
||||
checkpoint_resume = self._workspace_resume_rev()
|
||||
|
||||
return self.new(
|
||||
queue,
|
||||
checkpoint_resume=checkpoint_resume,
|
||||
reset=reset,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _workspace_resume_rev(self) -> Optional[str]:
|
||||
last_checkpoint = self._get_last_checkpoint()
|
||||
last_applied = self._get_last_applied()
|
||||
if last_checkpoint and last_applied:
|
||||
return last_applied
|
||||
return None
|
||||
|
||||
def reproduce_celery( # noqa: C901
|
||||
self, entries: Optional[Iterable["QueueEntry"]] = None, **kwargs
|
||||
) -> Dict[str, str]:
|
||||
@@ -254,17 +223,12 @@ class Experiments:
|
||||
self,
|
||||
queue: "BaseStashQueue",
|
||||
*args,
|
||||
checkpoint_resume: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> "QueueEntry":
|
||||
"""Create and enqueue a new experiment.
|
||||
|
||||
Experiment will be derived from the current workspace.
|
||||
"""
|
||||
if checkpoint_resume is not None:
|
||||
return self._resume_checkpoint(
|
||||
queue, *args, resume_rev=checkpoint_resume, **kwargs
|
||||
)
|
||||
|
||||
name = kwargs.get("name", None)
|
||||
baseline_sha = kwargs.get("baseline_rev") or self.repo.scm.get_rev()
|
||||
@@ -278,59 +242,6 @@ class Experiments:
|
||||
|
||||
return queue.put(*args, **kwargs)
|
||||
|
||||
def _resume_checkpoint(
|
||||
self,
|
||||
queue: "BaseStashQueue",
|
||||
*args,
|
||||
resume_rev: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> "QueueEntry":
|
||||
"""Create and queue a resumed checkpoint experiment."""
|
||||
assert resume_rev
|
||||
|
||||
branch: Optional[str] = None
|
||||
try:
|
||||
allow_multiple = bool(kwargs.get("params", None))
|
||||
branch = self.get_branch_by_rev(resume_rev, allow_multiple=allow_multiple)
|
||||
if not branch:
|
||||
raise DvcException(
|
||||
f"Could not find checkpoint experiment '{resume_rev[:7]}'"
|
||||
)
|
||||
baseline_rev = self._get_baseline(branch)
|
||||
except MultipleBranchError as exc:
|
||||
baselines = {
|
||||
info.baseline_sha for info in exc.ref_infos if info.baseline_sha
|
||||
}
|
||||
if len(baselines) == 1:
|
||||
baseline_rev = baselines.pop()
|
||||
else:
|
||||
raise
|
||||
|
||||
logger.debug(
|
||||
"Checkpoint run from '%s' with baseline '%s'",
|
||||
resume_rev[:7],
|
||||
baseline_rev,
|
||||
)
|
||||
return queue.put(
|
||||
*args,
|
||||
resume_rev=resume_rev,
|
||||
baseline_rev=baseline_rev,
|
||||
branch=branch,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _get_last_checkpoint(self) -> Optional[str]:
|
||||
try:
|
||||
last_checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
|
||||
if last_checkpoint:
|
||||
self.check_baseline(last_checkpoint)
|
||||
return last_checkpoint
|
||||
except BaselineMismatchError:
|
||||
# If HEAD has moved since the the last checkpoint run,
|
||||
# the specified checkpoint is no longer relevant
|
||||
self.scm.remove_ref(EXEC_CHECKPOINT)
|
||||
return None
|
||||
|
||||
def _get_last_applied(self) -> Optional[str]:
|
||||
try:
|
||||
last_applied = self.scm.get_ref(EXEC_APPLY)
|
||||
@@ -343,13 +254,13 @@ class Experiments:
|
||||
self.scm.remove_ref(EXEC_APPLY)
|
||||
return None
|
||||
|
||||
def reset_checkpoints(self):
|
||||
self.scm.remove_ref(EXEC_CHECKPOINT)
|
||||
self.scm.remove_ref(EXEC_APPLY)
|
||||
|
||||
@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 +271,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 +353,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
|
||||
|
||||
@@ -483,11 +383,6 @@ class Experiments:
|
||||
|
||||
return save(self.repo, *args, **kwargs)
|
||||
|
||||
def gc(self, *args, **kwargs):
|
||||
from dvc.repo.experiments.gc import gc
|
||||
|
||||
return gc(self.repo, *args, **kwargs)
|
||||
|
||||
def push(self, *args, **kwargs):
|
||||
from dvc.repo.experiments.push import push
|
||||
|
||||
|
||||
@@ -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,382 @@
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Collection,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
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, rev)."""
|
||||
|
||||
def _head_timestamp(exp_range: "ExpRange") -> Tuple[datetime, str]:
|
||||
head_exp = first(exp_range.revs)
|
||||
if head_exp and head_exp.data and head_exp.data.timestamp:
|
||||
return head_exp.data.timestamp, head_exp.rev
|
||||
|
||||
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"]
|
||||
|
||||
@@ -31,21 +31,6 @@ class ExperimentExistsError(DvcException):
|
||||
self.name = name
|
||||
|
||||
|
||||
class CheckpointExistsError(DvcException):
|
||||
def __init__(self, name: str):
|
||||
msg = (
|
||||
"Reproduced checkpoint experiment conflicts with existing "
|
||||
f"experiment '{name}'. To restart (and overwrite) the existing "
|
||||
"experiment run:\n\n"
|
||||
"\tdvc exp run -f ...\n\n"
|
||||
"To resume the existing experiment, run:\n\n"
|
||||
f"\tdvc exp apply {name}\n"
|
||||
"\tdvc exp run\n"
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.name = name
|
||||
|
||||
|
||||
class InvalidExpRefError(DvcException):
|
||||
def __init__(self, ref):
|
||||
super().__init__(f"'{ref}' is not a valid experiment refname.")
|
||||
@@ -114,3 +99,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"
|
||||
)
|
||||
|
||||
@@ -6,13 +6,14 @@ from abc import ABC, abstractmethod
|
||||
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,
|
||||
@@ -27,20 +28,15 @@ from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo.experiments.exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
from dvc.repo.experiments.refs import (
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
ExpRefInfo,
|
||||
)
|
||||
from dvc.repo.experiments.exceptions import ExperimentExistsError
|
||||
from dvc.repo.experiments.refs import EXEC_BASELINE, EXEC_BRANCH, ExpRefInfo
|
||||
from dvc.repo.experiments.utils import to_studio_params
|
||||
from dvc.repo.metrics.show import _collect_top_level_metrics
|
||||
from dvc.repo.params.show import _collect_top_level_params
|
||||
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 +255,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 +287,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 +303,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
|
||||
@@ -370,7 +374,7 @@ class BaseExecutor(ABC):
|
||||
dest_scm: "Git",
|
||||
refs: List[str],
|
||||
force: bool = False,
|
||||
on_diverged: Optional[Callable[[str, bool], None]] = None,
|
||||
on_diverged: Optional[Callable[[str], None]] = None,
|
||||
**kwargs,
|
||||
) -> Iterable[str]:
|
||||
"""Fetch reproduced experiment refs into the specified SCM.
|
||||
@@ -379,41 +383,32 @@ class BaseExecutor(ABC):
|
||||
dest_scm: Destination Git instance.
|
||||
refs: reference names to be fetched from the remotes.
|
||||
force: If True, diverged refs will be overwritten
|
||||
on_diverged: Callback in the form on_diverged(ref, is_checkpoint)
|
||||
on_diverged: Callback in the form on_diverged(ref)
|
||||
to be called when an experiment ref has diverged.
|
||||
|
||||
Extra kwargs will be passed into the remote git client.
|
||||
"""
|
||||
|
||||
if EXEC_CHECKPOINT in refs:
|
||||
refs.remove(EXEC_CHECKPOINT)
|
||||
has_checkpoint = True
|
||||
else:
|
||||
has_checkpoint = False
|
||||
|
||||
def on_diverged_ref(orig_ref: str, new_rev: str):
|
||||
if force:
|
||||
logger.debug("Replacing existing experiment '%s'", orig_ref)
|
||||
return True
|
||||
|
||||
if on_diverged:
|
||||
return on_diverged(orig_ref, has_checkpoint)
|
||||
return on_diverged(orig_ref)
|
||||
|
||||
self._raise_ref_conflict(dest_scm, orig_ref, new_rev, has_checkpoint)
|
||||
self._raise_ref_conflict(dest_scm, orig_ref, new_rev)
|
||||
logger.debug("Reproduced existing experiment '%s'", orig_ref)
|
||||
return False
|
||||
|
||||
# fetch experiments
|
||||
try:
|
||||
refspecs = [f"{ref}:{ref}" for ref in refs]
|
||||
# update last run checkpoint (if it exists)
|
||||
if has_checkpoint:
|
||||
refspecs.append(f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}")
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
refspecs,
|
||||
on_diverged=on_diverged_ref,
|
||||
force=force or has_checkpoint,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
except SCMError:
|
||||
@@ -453,6 +448,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.
|
||||
@@ -465,6 +461,7 @@ class BaseExecutor(ABC):
|
||||
from dvc.repo.checkout import checkout as dvc_checkout
|
||||
from dvc.repo.reproduce import reproduce as dvc_reproduce
|
||||
from dvc.stage import PipelineStage
|
||||
from dvc.ui import ui
|
||||
|
||||
auto_push = env2bool(DVC_EXP_AUTO_PUSH)
|
||||
git_remote = os.getenv(DVC_EXP_GIT_REMOTE, None)
|
||||
@@ -485,11 +482,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:
|
||||
@@ -508,56 +509,36 @@ class BaseExecutor(ABC):
|
||||
|
||||
repro_dry = kwargs.get("dry")
|
||||
|
||||
# NOTE: checkpoint outs are handled as a special type of persist
|
||||
# out:
|
||||
#
|
||||
# - checkpoint out may not yet exist if this is the first time this
|
||||
# experiment has been run, this is not an error condition for
|
||||
# experiments
|
||||
# - if experiment was run with --reset, the checkpoint out will be
|
||||
# removed at the start of the experiment (regardless of any
|
||||
# dvc.lock entry for the checkpoint out)
|
||||
# - if run without --reset, the checkpoint out will be checked out
|
||||
# using any hash present in dvc.lock (or removed if no entry
|
||||
# exists in dvc.lock)
|
||||
checkpoint_reset: bool = kwargs.pop("reset", False)
|
||||
if not repro_dry:
|
||||
dvc_checkout(
|
||||
dvc,
|
||||
targets=targets,
|
||||
with_deps=targets is not None,
|
||||
force=True,
|
||||
quiet=True,
|
||||
allow_missing=True,
|
||||
checkpoint_reset=checkpoint_reset,
|
||||
recursive=kwargs.get("recursive", False),
|
||||
)
|
||||
|
||||
checkpoint_func = partial(
|
||||
cls.checkpoint_callback,
|
||||
dvc,
|
||||
dvc.scm,
|
||||
info.name,
|
||||
repro_force or checkpoint_reset,
|
||||
)
|
||||
stages = dvc_reproduce(
|
||||
dvc,
|
||||
*args,
|
||||
on_unchanged=filter_pipeline,
|
||||
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:
|
||||
ref, exp_ref, repro_force = cls._repro_commit(
|
||||
dvc,
|
||||
info,
|
||||
stages,
|
||||
exp_hash,
|
||||
auto_push,
|
||||
git_remote,
|
||||
repro_force,
|
||||
message=message,
|
||||
)
|
||||
info.result_hash = exp_hash
|
||||
info.result_ref = ref
|
||||
@@ -573,19 +554,18 @@ class BaseExecutor(ABC):
|
||||
cls,
|
||||
dvc,
|
||||
info,
|
||||
stages,
|
||||
exp_hash,
|
||||
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(
|
||||
dvc.scm,
|
||||
exp_hash,
|
||||
exp_name=info.name,
|
||||
force=repro_force,
|
||||
checkpoint=is_checkpoint,
|
||||
message=message,
|
||||
)
|
||||
if auto_push:
|
||||
cls._auto_push(dvc, dvc.scm, git_remote)
|
||||
@@ -613,12 +593,12 @@ 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
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
with Repo(os.path.join(info.root_dir, info.dvc_dir)) as dvc:
|
||||
info.status = TaskStatus.RUNNING
|
||||
@@ -636,20 +616,27 @@ 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
|
||||
info.status = TaskStatus.SUCCESS
|
||||
except CheckpointKilledError:
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except DvcException:
|
||||
if log_errors:
|
||||
logger.exception("")
|
||||
@@ -664,10 +651,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:
|
||||
@@ -713,24 +701,6 @@ class BaseExecutor(ABC):
|
||||
exc,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def checkpoint_callback(
|
||||
cls,
|
||||
dvc: "Repo",
|
||||
scm: "Git",
|
||||
name: Optional[str],
|
||||
force: bool,
|
||||
unchanged: Iterable["PipelineStage"],
|
||||
stages: Iterable["PipelineStage"],
|
||||
):
|
||||
exp_hash = cls.hash_exp(list(stages) + list(unchanged))
|
||||
exp_rev = cls.commit(scm, exp_hash, exp_name=name, force=force, checkpoint=True)
|
||||
|
||||
if env2bool(DVC_EXP_AUTO_PUSH):
|
||||
git_remote = os.getenv(DVC_EXP_GIT_REMOTE)
|
||||
cls._auto_push(dvc, scm, git_remote)
|
||||
ui.write(f"Checkpoint experiment iteration '{exp_rev[:7]}'.")
|
||||
|
||||
@classmethod
|
||||
def commit(
|
||||
cls,
|
||||
@@ -738,7 +708,7 @@ class BaseExecutor(ABC):
|
||||
exp_hash: str,
|
||||
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,26 +738,24 @@ 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)
|
||||
new_rev = cls._raise_ref_conflict(scm, branch, new_rev)
|
||||
else:
|
||||
scm.set_ref(branch, new_rev, old_ref=old_ref)
|
||||
scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
|
||||
if checkpoint:
|
||||
scm.set_ref(EXEC_CHECKPOINT, new_rev)
|
||||
|
||||
return new_rev
|
||||
|
||||
@staticmethod
|
||||
def _raise_ref_conflict(scm, ref, new_rev, checkpoint):
|
||||
def _raise_ref_conflict(scm, ref, new_rev):
|
||||
# If this commit is a duplicate of the existing commit at 'ref', return
|
||||
# the existing commit. Otherwise, error out and require user to re-run
|
||||
# with --force as needed
|
||||
orig_rev = scm.get_ref(ref)
|
||||
if scm.diff(orig_rev, new_rev):
|
||||
if checkpoint:
|
||||
raise CheckpointExistsError(ref)
|
||||
raise ExperimentExistsError(ref)
|
||||
return orig_rev
|
||||
|
||||
|
||||
@@ -10,14 +10,12 @@ from shortuuid import uuid
|
||||
|
||||
from dvc.lock import LockError
|
||||
from dvc.repo.experiments.refs import (
|
||||
EXEC_APPLY,
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
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 +88,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,
|
||||
@@ -124,9 +122,6 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
if self.scm.get_ref(EXEC_BRANCH):
|
||||
self.scm.remove_ref(EXEC_BRANCH)
|
||||
|
||||
if self.scm.get_ref(EXEC_CHECKPOINT):
|
||||
self.scm.remove_ref(EXEC_CHECKPOINT)
|
||||
|
||||
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
|
||||
# committing
|
||||
assert isinstance(self.scm, Git)
|
||||
@@ -157,7 +152,7 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
self, repo: "Repo", rev: str, run_cache: bool = True # noqa: ARG002
|
||||
):
|
||||
"""Initialize DVC cache."""
|
||||
self._update_config({"cache": {"dir": repo.cache.repo.path}})
|
||||
self._update_config({"cache": {"dir": repo.cache.local_cache_dir}})
|
||||
|
||||
def cleanup(self, infofile: Optional[str] = None):
|
||||
super().cleanup(infofile)
|
||||
@@ -189,7 +184,6 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._detach_stack = ExitStack()
|
||||
self._orig_checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
|
||||
|
||||
@classmethod
|
||||
def from_stash_entry(
|
||||
@@ -251,6 +245,3 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
self.scm.remove_ref(EXEC_MERGE)
|
||||
if self.scm.get_ref(EXEC_BRANCH):
|
||||
self.scm.remove_ref(EXEC_BRANCH)
|
||||
checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
|
||||
if checkpoint and checkpoint != self._orig_checkpoint:
|
||||
self.scm.set_ref(EXEC_APPLY, checkpoint)
|
||||
|
||||
@@ -11,7 +11,6 @@ from funcy import first
|
||||
from dvc.repo.experiments.refs import (
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_HEAD,
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
@@ -162,7 +161,6 @@ class SSHExecutor(BaseExecutor):
|
||||
self._ssh_cmd(fs, f"git symbolic-ref {EXEC_BRANCH} {branch}")
|
||||
else:
|
||||
self._ssh_cmd(fs, f"git symbolic-ref -d {EXEC_BRANCH}", check=False)
|
||||
self._ssh_cmd(fs, f"git update-ref -d {EXEC_CHECKPOINT}", check=False)
|
||||
|
||||
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
|
||||
# committing
|
||||
@@ -212,8 +210,7 @@ class SSHExecutor(BaseExecutor):
|
||||
"""Collect DVC cache."""
|
||||
from dvc.repo.experiments.pull import _pull_cache
|
||||
|
||||
with self.get_odb() as odb:
|
||||
_pull_cache(repo, exp_ref, run_cache=run_cache, odb=odb)
|
||||
_pull_cache(repo, exp_ref, run_cache=run_cache)
|
||||
|
||||
@contextmanager
|
||||
def get_odb(self):
|
||||
@@ -243,6 +240,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,53 +0,0 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from dvc.repo import locked
|
||||
|
||||
from .utils import exp_refs, remove_exp_refs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@locked
|
||||
def gc(
|
||||
repo,
|
||||
all_branches: Optional[bool] = False,
|
||||
all_tags: Optional[bool] = False,
|
||||
all_commits: Optional[bool] = False,
|
||||
workspace: Optional[bool] = False,
|
||||
commit_date: Optional[str] = None,
|
||||
queued: Optional[bool] = False,
|
||||
):
|
||||
keep_revs = set(
|
||||
repo.brancher(
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
commit_date=commit_date,
|
||||
sha_only=True,
|
||||
)
|
||||
)
|
||||
if workspace:
|
||||
keep_revs.add(repo.scm.get_rev())
|
||||
|
||||
if not keep_revs:
|
||||
return 0
|
||||
|
||||
to_remove = [
|
||||
ref_info
|
||||
for ref_info in exp_refs(repo.scm)
|
||||
if ref_info.baseline_sha not in keep_revs
|
||||
]
|
||||
remove_exp_refs(repo.scm, to_remove)
|
||||
removed = len(to_remove)
|
||||
|
||||
delete_stashes = []
|
||||
stash = repo.experiments.celery_queue.stash
|
||||
for stash_rev, entry in stash.stash_revs.items():
|
||||
if not queued or entry.baseline_rev not in keep_revs:
|
||||
delete_stashes.append(stash_rev)
|
||||
if delete_stashes:
|
||||
repo.experiments.celery_queue.remove(delete_stashes)
|
||||
removed += len(delete_stashes)
|
||||
|
||||
return removed
|
||||
@@ -1,284 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
TextIO,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import compact, lremove, lsplit
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.dependency import Dependency
|
||||
from dvc.dvcfile import ProjectFile, SingleStageFile
|
||||
from dvc.repo import Repo
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
PROMPTS = {
|
||||
"cmd": "[b]Command[/b] to execute",
|
||||
"code": "Path to a [b]code[/b] file/directory",
|
||||
"data": "Path to a [b]data[/b] file/directory",
|
||||
"models": "Path to a [b]model[/b] file/directory",
|
||||
"params": "Path to a [b]parameters[/b] file",
|
||||
"metrics": "Path to a [b]metrics[/b] file",
|
||||
"plots": "Path to a [b]plots[/b] file/directory",
|
||||
}
|
||||
|
||||
|
||||
def _prompts(
|
||||
keys: Iterable[str],
|
||||
defaults: Optional[Dict[str, str]] = None,
|
||||
validator: Optional[Callable[[str, str], Union[str, Tuple[str, str]]]] = None,
|
||||
allow_omission: bool = True,
|
||||
stream: Optional[TextIO] = None,
|
||||
) -> Dict[str, Optional[str]]:
|
||||
from dvc.ui.prompt import Prompt
|
||||
|
||||
defaults = defaults or {}
|
||||
return {
|
||||
key: Prompt.prompt_(
|
||||
PROMPTS[key],
|
||||
console=ui.error_console,
|
||||
default=defaults.get(key),
|
||||
validator=partial(validator, key) if validator else None,
|
||||
allow_omission=allow_omission,
|
||||
stream=stream,
|
||||
)
|
||||
for key in keys
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
def init_interactive(
|
||||
defaults: Dict[str, str],
|
||||
provided: Dict[str, str],
|
||||
validator: Optional[Callable[[str, str], Union[str, Tuple[str, str]]]] = None,
|
||||
stream: Optional[TextIO] = None,
|
||||
) -> Dict[str, str]:
|
||||
command_prompts = lremove(provided.keys(), ["cmd"])
|
||||
dependencies_prompts = lremove(provided.keys(), ["code", "data", "params"])
|
||||
output_keys = ["models"]
|
||||
if "live" not in provided:
|
||||
output_keys.extend(["metrics", "plots"])
|
||||
outputs_prompts = lremove(provided.keys(), output_keys)
|
||||
|
||||
ret: Dict[str, str] = {}
|
||||
if "cmd" in provided:
|
||||
ret["cmd"] = provided["cmd"]
|
||||
|
||||
for heading, prompts, allow_omission in (
|
||||
("", command_prompts, False),
|
||||
("Enter experiment dependencies.", dependencies_prompts, True),
|
||||
("Enter experiment outputs.", outputs_prompts, True),
|
||||
):
|
||||
if prompts and heading:
|
||||
ui.error_write(heading, styled=True)
|
||||
response = _prompts(
|
||||
prompts,
|
||||
defaults=defaults,
|
||||
allow_omission=allow_omission,
|
||||
validator=validator,
|
||||
stream=stream,
|
||||
)
|
||||
ret.update(compact(response))
|
||||
if prompts:
|
||||
ui.error_write(styled=True)
|
||||
return ret
|
||||
|
||||
|
||||
def _check_stage_exists(
|
||||
dvcfile: Union["ProjectFile", "SingleStageFile"],
|
||||
name: str,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
if not force and dvcfile.exists() and name in dvcfile.stages:
|
||||
from dvc.stage.exceptions import DuplicateStageName
|
||||
|
||||
hint = "Use '--force' to overwrite."
|
||||
raise DuplicateStageName(f"Stage '{name}' already exists in 'dvc.yaml'. {hint}")
|
||||
|
||||
|
||||
def validate_prompts(repo: "Repo", key: str, value: str) -> Union[Any, Tuple[Any, str]]:
|
||||
from dvc.ui.prompt import InvalidResponse
|
||||
|
||||
msg_format = "[yellow]'{0}' does not exist, the {1} will be created.[/]"
|
||||
if key == "params":
|
||||
from dvc.dependency.param import (
|
||||
MissingParamsFile,
|
||||
ParamsDependency,
|
||||
ParamsIsADirectoryError,
|
||||
)
|
||||
|
||||
assert isinstance(value, str)
|
||||
try:
|
||||
ParamsDependency(None, value, repo=repo).validate_filepath()
|
||||
except MissingParamsFile:
|
||||
return value, msg_format.format(value, "file")
|
||||
except ParamsIsADirectoryError:
|
||||
raise InvalidResponse( # noqa: B904
|
||||
f"[prompt.invalid]'{value}' is a directory. "
|
||||
"Please retry with an existing parameters file."
|
||||
)
|
||||
elif key in ("code", "data") and not os.path.exists(value):
|
||||
typ = "file" if is_file(value) else "directory"
|
||||
return value, msg_format.format(value, typ)
|
||||
return value
|
||||
|
||||
|
||||
def is_file(path: str) -> bool:
|
||||
_, ext = os.path.splitext(path)
|
||||
return bool(ext)
|
||||
|
||||
|
||||
def init_deps(stage: PipelineStage) -> List["Dependency"]:
|
||||
from funcy import rpartial
|
||||
|
||||
from dvc.dependency import ParamsDependency
|
||||
from dvc.fs import localfs
|
||||
|
||||
new_deps = [dep for dep in stage.deps if not dep.exists]
|
||||
params, deps = lsplit(rpartial(isinstance, ParamsDependency), new_deps)
|
||||
|
||||
# always create a file for params, detect file/folder based on extension
|
||||
# for other dependencies
|
||||
dirs = [dep.fs_path for dep in deps if not is_file(dep.fs_path)]
|
||||
files = [dep.fs_path for dep in deps + params if is_file(dep.fs_path)]
|
||||
for path in dirs:
|
||||
localfs.makedirs(path)
|
||||
for path in files:
|
||||
localfs.makedirs(localfs.path.parent(path), exist_ok=True)
|
||||
localfs.touch(path)
|
||||
|
||||
return new_deps
|
||||
|
||||
|
||||
def init_out_dirs(stage: PipelineStage) -> List[str]:
|
||||
from dvc.fs import localfs
|
||||
|
||||
new_dirs = []
|
||||
|
||||
# create dirs for outputs
|
||||
for out in stage.outs:
|
||||
path = out.def_path
|
||||
if is_file(path):
|
||||
path = localfs.path.parent(path)
|
||||
if path and not localfs.exists(path):
|
||||
localfs.makedirs(path)
|
||||
new_dirs.append(path)
|
||||
|
||||
return new_dirs
|
||||
|
||||
|
||||
def init(
|
||||
repo: "Repo",
|
||||
name: str = "train",
|
||||
type: str = "default", # noqa: A002, pylint: disable=redefined-builtin
|
||||
defaults: Optional[Dict[str, str]] = None,
|
||||
overrides: Optional[Dict[str, str]] = None,
|
||||
interactive: bool = False,
|
||||
force: bool = False,
|
||||
stream: Optional[TextIO] = None,
|
||||
) -> Tuple[PipelineStage, List["Dependency"], List[str]]:
|
||||
from dvc.dvcfile import PROJECT_FILE, load_file
|
||||
|
||||
dvcfile = load_file(repo, PROJECT_FILE)
|
||||
_check_stage_exists(dvcfile, name, force=force)
|
||||
|
||||
defaults = defaults.copy() if defaults else {}
|
||||
overrides = overrides.copy() if overrides else {}
|
||||
|
||||
if interactive:
|
||||
defaults = init_interactive(
|
||||
validator=partial(validate_prompts, repo),
|
||||
defaults=defaults,
|
||||
provided=overrides,
|
||||
stream=stream,
|
||||
)
|
||||
elif "live" in overrides:
|
||||
# suppress `metrics`/`plots` if live is selected.
|
||||
defaults.pop("metrics", None)
|
||||
defaults.pop("plots", None)
|
||||
else:
|
||||
defaults.pop("live", None) # suppress live otherwise
|
||||
|
||||
context: Dict[str, str] = {**defaults, **overrides}
|
||||
assert "cmd" in context
|
||||
|
||||
params = context.get("params")
|
||||
if params:
|
||||
from dvc.dependency.param import (
|
||||
MissingParamsFile,
|
||||
ParamsDependency,
|
||||
ParamsIsADirectoryError,
|
||||
)
|
||||
|
||||
try:
|
||||
ParamsDependency(None, params, repo=repo).validate_filepath()
|
||||
except ParamsIsADirectoryError as exc:
|
||||
raise DvcException(f"{exc}.") # noqa: B904 # swallow cause for display
|
||||
except MissingParamsFile:
|
||||
pass
|
||||
|
||||
if type == "checkpoint":
|
||||
outs_key = "checkpoints"
|
||||
metrics_key = "metrics_persist_no_cache"
|
||||
plots_key = "plots_persist_no_cache"
|
||||
else:
|
||||
outs_key = "outs"
|
||||
metrics_key = "metrics_no_cache"
|
||||
plots_key = "plots_no_cache"
|
||||
|
||||
models = [context.get("models")]
|
||||
metrics = [context.get("metrics")]
|
||||
plots = [context.get("plots")]
|
||||
if live_path := context.pop("live", None):
|
||||
metrics.append(os.path.join(live_path, "metrics.json"))
|
||||
plots.append(os.path.join(live_path, "plots"))
|
||||
|
||||
stage = repo.stage.create(
|
||||
name=name,
|
||||
cmd=context["cmd"],
|
||||
deps=compact([context.get("code"), context.get("data")]),
|
||||
params=[{params: None}] if params else None,
|
||||
force=force,
|
||||
**{
|
||||
outs_key: compact(models),
|
||||
metrics_key: compact(metrics),
|
||||
plots_key: compact(plots),
|
||||
},
|
||||
)
|
||||
assert isinstance(stage, PipelineStage)
|
||||
|
||||
with _disable_logging(), repo.scm_context(autostage=True, quiet=True):
|
||||
stage.dump(update_lock=False)
|
||||
initialized_out_dirs = init_out_dirs(stage)
|
||||
stage.ignore_outs()
|
||||
initialized_deps = init_deps(stage)
|
||||
if params:
|
||||
repo.scm_context.track_file(params)
|
||||
|
||||
return stage, initialized_deps, initialized_out_dirs
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import Optional
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
@@ -15,14 +15,21 @@ 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,
|
||||
):
|
||||
) -> Dict[str, List[Tuple[str, Optional[str]]]]:
|
||||
"""List experiments.
|
||||
|
||||
Returns a dict mapping baseline revs to a list of (exp_name, exp_sha) tuples.
|
||||
"""
|
||||
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)
|
||||
|
||||
@@ -33,9 +40,14 @@ def ls(
|
||||
|
||||
results = defaultdict(list)
|
||||
for baseline in ref_info_dict:
|
||||
name = baseline[:7]
|
||||
name = baseline
|
||||
if tags[baseline] or ref_heads[baseline]:
|
||||
name = tags[baseline] or ref_heads[baseline][len(base) + 1 :]
|
||||
results[name] = [info.name for info in ref_info_dict[baseline]]
|
||||
name = tags[baseline] or ref_heads[baseline]
|
||||
for info in ref_info_dict[baseline]:
|
||||
if git_remote:
|
||||
exp_rev = None
|
||||
else:
|
||||
exp_rev = repo.scm.get_ref(str(info))
|
||||
results[name].append((info.name, exp_rev))
|
||||
|
||||
return results
|
||||
|
||||
@@ -21,9 +21,9 @@ logger = logging.getLogger(__name__)
|
||||
def pull( # noqa: C901
|
||||
repo,
|
||||
git_remote: str,
|
||||
exp_names: Union[Iterable[str], str],
|
||||
exp_names: Optional[Union[Iterable[str], str]] = None,
|
||||
all_commits=False,
|
||||
rev: Optional[str] = None,
|
||||
rev: Optional[Union[List[str], str]] = None,
|
||||
num=1,
|
||||
force: bool = False,
|
||||
pull_cache: bool = False,
|
||||
@@ -32,28 +32,30 @@ def pull( # noqa: C901
|
||||
exp_ref_set: Set["ExpRefInfo"] = set()
|
||||
if all_commits:
|
||||
exp_ref_set.update(exp_refs(repo.scm, git_remote))
|
||||
elif exp_names:
|
||||
if isinstance(exp_names, str):
|
||||
exp_names = [exp_names]
|
||||
exp_ref_dict = resolve_name(repo.scm, exp_names, git_remote)
|
||||
|
||||
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)
|
||||
|
||||
else:
|
||||
if exp_names:
|
||||
if isinstance(exp_names, str):
|
||||
exp_names = [exp_names]
|
||||
exp_ref_dict = resolve_name(repo.scm, exp_names, git_remote)
|
||||
|
||||
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, git_remote)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
exp_ref_set.update(ref_info_list)
|
||||
rev = rev or "HEAD"
|
||||
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():
|
||||
exp_ref_set.update(ref_info_list)
|
||||
|
||||
pull_result = _pull(repo, git_remote, exp_ref_set, force)
|
||||
|
||||
@@ -105,10 +107,9 @@ def _pull_cache(
|
||||
dvc_remote=None,
|
||||
jobs=None,
|
||||
run_cache=False,
|
||||
odb=None,
|
||||
):
|
||||
if isinstance(refs, ExpRefInfo):
|
||||
refs = [refs]
|
||||
revs = list(exp_commits(repo.scm, refs))
|
||||
logger.debug("dvc fetch experiment '%s'", refs)
|
||||
repo.fetch(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs, odb=odb)
|
||||
repo.fetch(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs)
|
||||
|
||||
@@ -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: Optional[Union[List[str], str]] = None,
|
||||
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,13 @@ def push( # noqa: C901
|
||||
assert isinstance(repo.scm, Git)
|
||||
if all_commits:
|
||||
exp_ref_set.update(exp_refs(repo.scm))
|
||||
|
||||
if exp_names:
|
||||
exp_ref_set.update(exp_refs_from_names(repo.scm, ensure_list(exp_names)))
|
||||
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)
|
||||
rev = rev or "HEAD"
|
||||
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 +128,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,15 +17,13 @@ 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
|
||||
from dvc.env import DVC_EXP_BASELINE_REV, DVC_EXP_NAME, DVCLIVE_RESUME
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.env import DVC_EXP_BASELINE_REV, DVC_EXP_NAME
|
||||
from dvc.lock import LockError
|
||||
from dvc.repo.experiments.exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
from dvc.repo.experiments.exceptions import ExperimentExistsError
|
||||
from dvc.repo.experiments.executor.base import BaseExecutor
|
||||
from dvc.repo.experiments.executor.local import WorkspaceExecutor
|
||||
from dvc.repo.experiments.refs import ExpRefInfo
|
||||
@@ -33,12 +31,11 @@ from dvc.repo.experiments.stash import ExpStash, ExpStashEntry
|
||||
from dvc.repo.experiments.utils import (
|
||||
EXEC_PID_DIR,
|
||||
EXEC_TMP_DIR,
|
||||
exp_refs_by_rev,
|
||||
get_exp_rwlock,
|
||||
get_random_exp_name,
|
||||
)
|
||||
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 +43,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 +251,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."""
|
||||
|
||||
@@ -299,11 +297,10 @@ class BaseStashQueue(ABC):
|
||||
output.
|
||||
"""
|
||||
|
||||
def _stash_exp( # noqa: PLR0915, C901
|
||||
def _stash_exp( # noqa: C901
|
||||
self,
|
||||
*args,
|
||||
params: Optional[Dict[str, List[str]]] = None,
|
||||
resume_rev: Optional[str] = None,
|
||||
baseline_rev: Optional[str] = None,
|
||||
branch: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
@@ -314,7 +311,6 @@ class BaseStashQueue(ABC):
|
||||
Args:
|
||||
params: Dict mapping paths to `Hydra Override`_ patterns,
|
||||
provided via `exp run --set-param`.
|
||||
resume_rev: Optional checkpoint resume rev.
|
||||
baseline_rev: Optional baseline rev for this experiment, defaults
|
||||
to the current SCM rev.
|
||||
branch: Optional experiment branch name. If specified, the
|
||||
@@ -337,17 +333,6 @@ class BaseStashQueue(ABC):
|
||||
if workspace:
|
||||
self.stash.apply(workspace)
|
||||
|
||||
if resume_rev:
|
||||
# move HEAD to the resume rev so that the stashed diff
|
||||
# only contains changes relative to resume rev
|
||||
stash_head = resume_rev
|
||||
self.scm.set_ref(
|
||||
"HEAD",
|
||||
resume_rev,
|
||||
message=f"dvc: resume from HEAD {resume_rev[:7]}",
|
||||
)
|
||||
self.scm.reset()
|
||||
|
||||
# update experiment params from command line
|
||||
if params:
|
||||
self._update_params(params)
|
||||
@@ -356,60 +341,6 @@ class BaseStashQueue(ABC):
|
||||
# & tempdir runs
|
||||
self._stash_commit_deps(*args, **kwargs)
|
||||
|
||||
if resume_rev:
|
||||
if branch:
|
||||
branch_name = ExpRefInfo.from_ref(branch).name
|
||||
else:
|
||||
branch_name = f"{resume_rev[:7]}"
|
||||
if self.scm.is_dirty(untracked_files=False):
|
||||
ui.write(
|
||||
(
|
||||
"Modified checkpoint experiment based on "
|
||||
f"'{branch_name}' will be created"
|
||||
),
|
||||
)
|
||||
branch = None
|
||||
elif not branch or self.scm.get_ref(branch) != resume_rev:
|
||||
err_msg = [
|
||||
"Nothing to do for unchanged checkpoint "
|
||||
f"'{resume_rev[:7]}'. "
|
||||
]
|
||||
if branch:
|
||||
err_msg.append(
|
||||
"To resume from the head of this "
|
||||
"experiment, use "
|
||||
f"'dvc exp apply {branch_name}'."
|
||||
)
|
||||
else:
|
||||
names = [
|
||||
ref_info.name
|
||||
for ref_info in exp_refs_by_rev(
|
||||
self.scm, resume_rev
|
||||
)
|
||||
]
|
||||
if len(names) > 3:
|
||||
names[3:] = [f"... ({len(names) - 3} more)"]
|
||||
err_msg.append(
|
||||
"To resume an experiment containing this "
|
||||
"checkpoint, apply one of these heads:\n"
|
||||
"\t{}".format(", ".join(names))
|
||||
)
|
||||
raise DvcException("".join(err_msg))
|
||||
else:
|
||||
ui.write(
|
||||
"Existing checkpoint experiment "
|
||||
f"'{branch_name}' will be resumed"
|
||||
)
|
||||
if name:
|
||||
logger.warning(
|
||||
(
|
||||
"Ignoring option '--name %s' for resumed "
|
||||
"experiment. Existing experiment name will"
|
||||
"be preserved instead."
|
||||
),
|
||||
name,
|
||||
)
|
||||
|
||||
# save additional repro command line arguments
|
||||
run_env = {
|
||||
DVC_EXP_BASELINE_REV: baseline_rev,
|
||||
@@ -417,14 +348,13 @@ class BaseStashQueue(ABC):
|
||||
if not name:
|
||||
name = get_random_exp_name(self.scm, baseline_rev)
|
||||
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,
|
||||
@@ -443,12 +373,6 @@ class BaseStashQueue(ABC):
|
||||
baseline_rev[:7],
|
||||
)
|
||||
finally:
|
||||
if resume_rev:
|
||||
# NOTE: this set_ref + reset() is equivalent to
|
||||
# `git reset orig_head` (our SCM reset() only operates
|
||||
# on HEAD rather than any arbitrary commit)
|
||||
self.scm.set_ref("HEAD", orig_head, message="dvc: restore HEAD")
|
||||
self.scm.reset()
|
||||
# Revert any of our changes before prior unstashing
|
||||
self.scm.reset(hard=True)
|
||||
|
||||
@@ -631,10 +555,8 @@ class BaseStashQueue(ABC):
|
||||
) -> Dict[str, str]:
|
||||
results = {}
|
||||
|
||||
def on_diverged(ref: str, checkpoint: bool):
|
||||
def on_diverged(ref: str):
|
||||
ref_info = ExpRefInfo.from_ref(ref)
|
||||
if checkpoint:
|
||||
raise CheckpointExistsError(ref_info.name)
|
||||
raise ExperimentExistsError(ref_info.name)
|
||||
|
||||
refs = get_remote_executor_refs(exp.scm, executor.git_url)
|
||||
@@ -728,9 +650,79 @@ 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.exceptions import DvcException
|
||||
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,10 +97,9 @@ 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
|
||||
|
||||
results: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
exec_name = self._EXEC_NAME or entry.stash_rev
|
||||
infofile = self.get_infofile_path(exec_name)
|
||||
@@ -112,6 +112,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]}'")
|
||||
@@ -119,12 +120,6 @@ class TempDirQueue(WorkspaceQueue):
|
||||
results[rev].update(
|
||||
self.collect_executor(self.repo.experiments, executor, exec_result)
|
||||
)
|
||||
except CheckpointKilledError:
|
||||
results[rev].update(
|
||||
self.collect_executor(self.repo.experiments, executor, exec_result)
|
||||
)
|
||||
|
||||
return results
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
@@ -141,10 +136,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
|
||||
|
||||
@@ -4,12 +4,7 @@ from typing import TYPE_CHECKING, Dict, List
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.repo.experiments.executor.base import ExecutorInfo, TaskStatus
|
||||
from dvc.repo.experiments.refs import (
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_NAMESPACE,
|
||||
EXPS_STASH,
|
||||
)
|
||||
from dvc.repo.experiments.refs import EXEC_NAMESPACE, EXPS_NAMESPACE, EXPS_STASH
|
||||
from dvc.repo.experiments.utils import get_exp_rwlock, iter_remote_refs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -33,9 +28,7 @@ def get_remote_executor_refs(scm: "Git", remote_url: str) -> List[str]:
|
||||
remote_url,
|
||||
base=EXPS_NAMESPACE,
|
||||
):
|
||||
if ref == EXEC_CHECKPOINT or (
|
||||
not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH
|
||||
):
|
||||
if not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH:
|
||||
refs.append(ref)
|
||||
return refs
|
||||
|
||||
@@ -67,7 +60,7 @@ def fetch_running_exp_from_temp_dir(
|
||||
result[rev] = info.asdict()
|
||||
if info.git_url and fetch_refs and info.status > TaskStatus.PREPARING:
|
||||
|
||||
def on_diverged(_ref: str, _checkpoint: bool):
|
||||
def on_diverged(_ref: str):
|
||||
return True
|
||||
|
||||
executor = TempDirExecutor.from_info(info)
|
||||
|
||||
@@ -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
|
||||
@@ -100,10 +101,14 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
self, entry: QueueEntry, executor: "BaseExecutor", **kwargs
|
||||
) -> 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 +118,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]}'")
|
||||
@@ -120,17 +126,35 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
results[rev].update(
|
||||
self.collect_executor(self.repo.experiments, executor, exec_result)
|
||||
)
|
||||
except CheckpointKilledError:
|
||||
# Checkpoint errors have already been logged
|
||||
return {}
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
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 +187,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,18 +10,23 @@ 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"
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -85,9 +87,6 @@ def run( # noqa: C901, PLR0912
|
||||
else:
|
||||
sweeps = [path_overrides]
|
||||
|
||||
if not kwargs.get("checkpoint_resume", None):
|
||||
kwargs["reset"] = True
|
||||
|
||||
for idx, sweep_overrides in enumerate(sweeps):
|
||||
if hydra_sweep and name_prefix is not None:
|
||||
kwargs["name"] = f"{name_prefix}-{idx+1}"
|
||||
@@ -96,6 +95,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))
|
||||
params = {k: dict(v) for k, v in params.items()}
|
||||
metrics = dict(
|
||||
_gather_metrics(
|
||||
repo,
|
||||
targets=None,
|
||||
rev=rev[:7],
|
||||
recursive=False,
|
||||
onerror=onerror_collect,
|
||||
)
|
||||
)
|
||||
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,
|
||||
@@ -113,6 +109,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 +120,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 +128,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 +161,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]
|
||||
]
|
||||
|
||||
@@ -21,7 +21,7 @@ class ExpStashEntry(NamedTuple):
|
||||
is not pushed onto the stash ref.
|
||||
head_rev: HEAD Git commit to be checked out for this experiment.
|
||||
baseline_rev: Experiment baseline commit.
|
||||
branch: Optional exp (checkpoint) branch name for this experiment.
|
||||
branch: Optional branch name for this experiment.
|
||||
name: Optional exp name.
|
||||
"""
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user