mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bef0da4fb | ||
|
|
284d83f390 | ||
|
|
417e6d4b76 | ||
|
|
248ba3b6b6 | ||
|
|
8a91c2b303 | ||
|
|
544dad75e9 | ||
|
|
8f50f2509f | ||
|
|
cdc1857559 | ||
|
|
358dd163da | ||
|
|
aae1a93cb5 | ||
|
|
c9b7218659 | ||
|
|
88c4b9525d | ||
|
|
69d36eeb06 | ||
|
|
3201794553 | ||
|
|
4464f8b4b9 | ||
|
|
efc7e05893 | ||
|
|
a555c3d29a | ||
|
|
676645b1ca | ||
|
|
93c3100bad | ||
|
|
c74156288d | ||
|
|
0e38ccc031 | ||
|
|
b69b368ffe | ||
|
|
6cc80fdeaa | ||
|
|
8f8d49d023 | ||
|
|
83ec952da1 | ||
|
|
c0d7e2a8de | ||
|
|
9aad09164c | ||
|
|
1f7733fec1 | ||
|
|
856c902b65 | ||
|
|
ebb459f9e2 | ||
|
|
2e0b024efe | ||
|
|
135fa26515 | ||
|
|
ef507efa04 | ||
|
|
05db79e7cf | ||
|
|
0a813bbcbc | ||
|
|
e82356ba92 | ||
|
|
5bfacec974 | ||
|
|
1848074ac9 | ||
|
|
a7c0f6ef39 | ||
|
|
0a8a9deceb | ||
|
|
c5d98681c0 | ||
|
|
15e05e4b3e | ||
|
|
7df8b0c09b | ||
|
|
2e1faf2979 | ||
|
|
cbaeff2ea1 | ||
|
|
132e625ae8 | ||
|
|
8517cccff4 | ||
|
|
c52a25e011 | ||
|
|
c4678736b0 | ||
|
|
ec92cc14d1 | ||
|
|
e6048287cb | ||
|
|
72a353d8b9 | ||
|
|
dd2b515c97 | ||
|
|
c223cdaecd | ||
|
|
5379aa2348 | ||
|
|
3375a7c39f | ||
|
|
519c47f5fc | ||
|
|
6f6038888f | ||
|
|
e223f51e7e | ||
|
|
fb628bf9ea | ||
|
|
4f4e977e53 | ||
|
|
8b1f8847fe | ||
|
|
c62a0380e9 | ||
|
|
8d8939fb67 | ||
|
|
f7a54bf2d0 | ||
|
|
1a9f5d3aa8 | ||
|
|
017a510b83 | ||
|
|
a80d224c58 | ||
|
|
49709a4810 | ||
|
|
7df58ce404 | ||
|
|
827e58f305 | ||
|
|
8bc6ba7cd5 | ||
|
|
a96a446742 | ||
|
|
837c8e8de4 | ||
|
|
603da1ca1e | ||
|
|
377d0c9d58 | ||
|
|
6bdf41a217 | ||
|
|
1aeb8750b6 | ||
|
|
5aae934379 | ||
|
|
de1338caaf | ||
|
|
9957410998 | ||
|
|
cf9855af45 | ||
|
|
0001cc3a4b | ||
|
|
440e1491f0 | ||
|
|
b0384f8f51 | ||
|
|
cd1e228e55 | ||
|
|
3b2e031e60 | ||
|
|
e5ee82bc99 | ||
|
|
bc95b650a4 | ||
|
|
257ea5f4cb | ||
|
|
5f7acd4cbe | ||
|
|
13325ea89b | ||
|
|
fd71434df5 | ||
|
|
2ff4ff2daa | ||
|
|
0ee6d6eba8 | ||
|
|
7c34370464 | ||
|
|
b67b254287 | ||
|
|
212928a1fa | ||
|
|
5be23cfd4e | ||
|
|
4d3a631e60 | ||
|
|
491155cdb0 | ||
|
|
9bb4501bf0 | ||
|
|
4cb658f96c | ||
|
|
9f66897bf7 | ||
|
|
84a1750013 | ||
|
|
6cb37ac27b | ||
|
|
f38490c897 | ||
|
|
34d80fbfce | ||
|
|
89537a77b5 | ||
|
|
c72e1fb0b8 | ||
|
|
9fbe5d929d | ||
|
|
0635b0ca15 | ||
|
|
06abf73c22 | ||
|
|
5e2c441d28 | ||
|
|
6efd699267 | ||
|
|
f639ab50ab | ||
|
|
953ae56536 | ||
|
|
ac0397e536 | ||
|
|
b243ccab1f | ||
|
|
c31a5d9df9 | ||
|
|
44b78b81b9 | ||
|
|
d25cb79e0a | ||
|
|
79238fd2a3 | ||
|
|
2bf7bd990a | ||
|
|
68b9ecb95c | ||
|
|
594716ec79 | ||
|
|
84b2e685e2 |
@@ -1,10 +0,0 @@
|
||||
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", "2.x"]
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, 2.x]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '5 1 * * *' # every day at 01:05
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install -e ".[dev]"
|
||||
- uses: pre-commit/[email protected].0
|
||||
- uses: pre-commit/[email protected].1
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Notification
|
||||
uses: rtCamp/action-slack-notify@v2.2.1
|
||||
uses: rtCamp/action-slack-notify@v2.3.0
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
|
||||
@@ -6,7 +6,7 @@ ci:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
@@ -22,13 +22,13 @@ repos:
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.1.15'
|
||||
rev: 'v0.5.6'
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies: ["tomli"]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
cff-version: 1.2.0
|
||||
title: 'DVC: Data Version Control - Git for Data & Models"'
|
||||
message: >-
|
||||
If you use this software as part of a publication and wish
|
||||
to cite it, please use the metadata from this file.
|
||||
type: software
|
||||
authors:
|
||||
- name: The DVC team and contributors
|
||||
website: 'https://github.com/iterative'
|
||||
repository-code: 'https://github.com/iterative/dvc'
|
||||
url: 'https://dvc.org'
|
||||
abstract: >-
|
||||
DVC is a tool for data science that takes advantage of existing software engineering toolset.
|
||||
It helps machine learning teams manage large datasets, make projects reproducible, and collaborate better.
|
||||
keywords:
|
||||
- ai
|
||||
- collaboration
|
||||
- data-science
|
||||
- data-version-control
|
||||
- developer-tools
|
||||
- git
|
||||
- machine-learning
|
||||
- python
|
||||
- reproducibility
|
||||
license: Apache-2.0
|
||||
@@ -3,6 +3,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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Main entry point for DVC command line tool."""
|
||||
|
||||
import sys
|
||||
|
||||
from dvc.cli import main
|
||||
|
||||
+1
-1
@@ -162,7 +162,7 @@ def _get_path_func(tool: str, ext: str):
|
||||
fmt = f"{tool}.dvc-{{now:%Y%m%d}}_{{now:%H%M%S}}.{ext}"
|
||||
|
||||
def func(now: Optional["datetime"] = None) -> str:
|
||||
return fmt.format(now=now or datetime.now())
|
||||
return fmt.format(now=now or datetime.now()) # noqa: DTZ005
|
||||
|
||||
return func
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .artifacts import artifacts_show
|
||||
from .data import get_url, open, read
|
||||
from .dataset import get as get_dataset
|
||||
from .experiments import exp_save, exp_show
|
||||
from .scm import all_branches, all_commits, all_tags
|
||||
from .show import metrics_show, params_show
|
||||
@@ -14,6 +15,7 @@ __all__ = [
|
||||
"artifacts_show",
|
||||
"exp_save",
|
||||
"exp_show",
|
||||
"get_dataset",
|
||||
"get_url",
|
||||
"metrics_show",
|
||||
"open",
|
||||
|
||||
+13
-2
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
from dvc.repo import Repo
|
||||
@@ -36,12 +37,22 @@ def artifacts_show(
|
||||
if version and stage:
|
||||
raise ValueError("Artifact version and stage are mutually exclusive.")
|
||||
|
||||
from dvc.repo.artifacts import Artifacts
|
||||
from dvc.utils import as_posix
|
||||
|
||||
repo_kwargs: dict[str, Any] = {
|
||||
"subrepos": True,
|
||||
"uninitialized": True,
|
||||
}
|
||||
|
||||
dirname, _ = Artifacts.parse_path(name)
|
||||
with Repo.open(repo, **repo_kwargs) as _repo:
|
||||
rev = _repo.artifacts.get_rev(name, version=version, stage=stage)
|
||||
with _repo.switch(rev):
|
||||
path = _repo.artifacts.get_path(name)
|
||||
return {"rev": rev, "path": path}
|
||||
root = _repo.fs.root_marker
|
||||
_dirname = _repo.fs.join(root, dirname) if dirname else root
|
||||
with Repo(_dirname, fs=_repo.fs, scm=_repo.scm) as r:
|
||||
path = r.artifacts.get_path(name)
|
||||
path = _repo.fs.join(_repo.fs.root_marker, as_posix(path))
|
||||
parts = _repo.fs.relparts(path, _repo.root_dir)
|
||||
return {"rev": rev, "path": os.path.join(*parts)}
|
||||
|
||||
+42
-6
@@ -25,7 +25,14 @@ def _wrap_exceptions(repo, url):
|
||||
raise PathMissingError(exc.path, url) from exc
|
||||
|
||||
|
||||
def get_url(path, repo=None, rev=None, remote=None):
|
||||
def get_url(
|
||||
path: str,
|
||||
repo: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
remote: Optional[str] = None,
|
||||
config: Optional[dict[str, Any]] = None,
|
||||
remote_config: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
Returns the URL to the storage location of a data file or directory tracked
|
||||
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
|
||||
@@ -35,15 +42,44 @@ def get_url(path, repo=None, rev=None, remote=None):
|
||||
|
||||
NOTE: This function does not check for the actual existence of the file or
|
||||
directory in the remote storage.
|
||||
|
||||
Args:
|
||||
path (str): location and file name of the target, relative to the root
|
||||
of `repo`.
|
||||
repo (str, optional): location of the DVC project or Git Repo.
|
||||
Defaults to the current DVC 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).
|
||||
rev (str, optional): Any `Git revision`_ such as a branch or tag name,
|
||||
a commit hash or a dvc experiment name.
|
||||
Defaults to HEAD.
|
||||
If `repo` is not a Git repo, this option is ignored.
|
||||
remote (str, optional): Name of the `DVC remote`_ used to form the
|
||||
returned URL string.
|
||||
Defaults to the `default remote`_ of `repo`.
|
||||
For local projects, the cache is tried before the default remote.
|
||||
config(dict, optional): config to be passed to the DVC repository.
|
||||
Defaults to None.
|
||||
remote_config(dict, optional): remote config to be passed to the DVC
|
||||
repository.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: URL to the file or directory.
|
||||
"""
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc_data.index import StorageKeyError
|
||||
|
||||
repo_kwargs: dict[str, Any] = {}
|
||||
if remote:
|
||||
repo_kwargs["config"] = {"core": {"remote": remote}}
|
||||
with Repo.open(
|
||||
repo, rev=rev, subrepos=True, uninitialized=True, **repo_kwargs
|
||||
repo,
|
||||
rev=rev,
|
||||
subrepos=True,
|
||||
uninitialized=True,
|
||||
remote=remote,
|
||||
config=config,
|
||||
remote_config=remote_config,
|
||||
) as _repo:
|
||||
index, entry = _repo.get_data_index_entry(path)
|
||||
with reraise(
|
||||
@@ -249,7 +285,7 @@ def _open(
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
|
||||
fs: Union[FileSystem, DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=_repo.index.data["local"])
|
||||
fs_path = path
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from typing import Literal, TypedDict, Union
|
||||
|
||||
|
||||
class DVCXDataset(TypedDict):
|
||||
type: Literal["dvcx"]
|
||||
name: str
|
||||
version: int
|
||||
|
||||
|
||||
class DVCDataset(TypedDict):
|
||||
type: Literal["dvc"]
|
||||
url: str
|
||||
path: str
|
||||
sha: str
|
||||
|
||||
|
||||
class URLDataset(TypedDict):
|
||||
type: Literal["url"]
|
||||
files: list[str]
|
||||
path: str
|
||||
|
||||
|
||||
def get(name: str) -> Union[DVCXDataset, DVCDataset, URLDataset]:
|
||||
from difflib import get_close_matches
|
||||
|
||||
from dvc.fs import get_cloud_fs
|
||||
from dvc.repo import Repo, datasets
|
||||
|
||||
repo = Repo()
|
||||
try:
|
||||
dataset = repo.datasets[name]
|
||||
except datasets.DatasetNotFoundError as e:
|
||||
add_note = getattr(e, "add_note", lambda _: None)
|
||||
if matches := get_close_matches(name, repo.datasets):
|
||||
add_note(f"Did you mean: {matches[0]!r}?")
|
||||
raise
|
||||
|
||||
if dataset._invalidated:
|
||||
raise ValueError(f"dataset not in sync. Sync with 'dvc ds update {name}'.")
|
||||
if not dataset.lock:
|
||||
raise ValueError("missing lock information")
|
||||
if dataset.type == "dvc":
|
||||
return DVCDataset(
|
||||
type="dvc",
|
||||
url=dataset.lock.url,
|
||||
path=dataset.lock.path,
|
||||
sha=dataset.lock.rev_lock,
|
||||
)
|
||||
if dataset.type == "dvcx":
|
||||
return DVCXDataset(
|
||||
type="dvcx", name=dataset.name_version[0], version=dataset.lock.version
|
||||
)
|
||||
if dataset.type == "url":
|
||||
fs_cls, _, path = get_cloud_fs(repo.config, url=dataset.lock.url)
|
||||
assert fs_cls
|
||||
join_version = getattr(fs_cls, "join_version", lambda path, _: path)
|
||||
protocol = fs_cls.protocol
|
||||
versioned_path = join_version(path, dataset.lock.meta.version_id)
|
||||
versioned_path = f"{protocol}://{versioned_path}"
|
||||
files = [
|
||||
join_version(
|
||||
fs_cls.join(versioned_path, file.relpath), file.meta.version_id
|
||||
)
|
||||
for file in dataset.lock.files
|
||||
]
|
||||
return URLDataset(type="url", files=files, path=versioned_path)
|
||||
+1
-1
@@ -25,7 +25,7 @@ def _postprocess(results):
|
||||
(k if counts[k] == 1 else f"{file_name}:{k}"): v
|
||||
for k, v in file_data["data"].items()
|
||||
}
|
||||
processed[rev] = {**processed[rev], **to_merge}
|
||||
processed[rev] = processed[rev] | to_merge
|
||||
|
||||
if "workspace" in processed:
|
||||
del processed["workspace"]
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ class CmdBase(ABC):
|
||||
|
||||
os.chdir(args.cd)
|
||||
|
||||
self.repo: "Repo" = Repo(uninitialized=self.UNINITIALIZED)
|
||||
self.config: "Config" = self.repo.config
|
||||
self.repo: Repo = Repo(uninitialized=self.UNINITIALIZED)
|
||||
self.config: Config = self.repo.config
|
||||
self.args = args
|
||||
|
||||
def do_run(self):
|
||||
|
||||
+34
-31
@@ -1,4 +1,5 @@
|
||||
"""Main parser for the dvc cli."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from functools import lru_cache
|
||||
@@ -17,6 +18,7 @@ from dvc.commands import (
|
||||
dag,
|
||||
data,
|
||||
data_sync,
|
||||
dataset,
|
||||
destroy,
|
||||
diff,
|
||||
du,
|
||||
@@ -55,48 +57,49 @@ from . import DvcParserError, formatter
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
COMMANDS = [
|
||||
init,
|
||||
queue,
|
||||
add,
|
||||
artifacts,
|
||||
cache,
|
||||
check_ignore,
|
||||
checkout,
|
||||
commit,
|
||||
completion,
|
||||
config,
|
||||
daemon,
|
||||
dag,
|
||||
data,
|
||||
data_sync,
|
||||
dataset,
|
||||
destroy,
|
||||
diff,
|
||||
du,
|
||||
experiments,
|
||||
freeze,
|
||||
gc,
|
||||
get,
|
||||
get_url,
|
||||
destroy,
|
||||
add,
|
||||
remove,
|
||||
move,
|
||||
unprotect,
|
||||
repro,
|
||||
data_sync,
|
||||
gc,
|
||||
git_hook,
|
||||
imp,
|
||||
imp_db,
|
||||
imp_url,
|
||||
config,
|
||||
checkout,
|
||||
remote,
|
||||
cache,
|
||||
metrics,
|
||||
params,
|
||||
init,
|
||||
install,
|
||||
root,
|
||||
ls,
|
||||
ls_url,
|
||||
freeze,
|
||||
dag,
|
||||
daemon,
|
||||
commit,
|
||||
completion,
|
||||
diff,
|
||||
version,
|
||||
update,
|
||||
git_hook,
|
||||
metrics,
|
||||
move,
|
||||
params,
|
||||
plots,
|
||||
queue,
|
||||
remote,
|
||||
remove,
|
||||
repro,
|
||||
root,
|
||||
stage,
|
||||
experiments,
|
||||
check_ignore,
|
||||
data,
|
||||
artifacts,
|
||||
studio,
|
||||
du,
|
||||
unprotect,
|
||||
update,
|
||||
version,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class CmdAdd(CmdBase):
|
||||
to_remote=self.args.to_remote,
|
||||
remote_jobs=self.args.remote_jobs,
|
||||
force=self.args.force,
|
||||
relink=self.args.relink,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
logger.exception("")
|
||||
@@ -119,6 +120,13 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-relink",
|
||||
dest="relink",
|
||||
action="store_false",
|
||||
help="Don't recreate links from cache to workspace.",
|
||||
)
|
||||
parser.set_defaults(relink=True)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
|
||||
@@ -13,6 +13,9 @@ class CmdArtifactsGet(CmdBaseNoRepo):
|
||||
from dvc.scm import CloneError
|
||||
from dvc.ui import ui
|
||||
|
||||
if self.args.show_url:
|
||||
return self._show_url()
|
||||
|
||||
try:
|
||||
count, out = Artifacts.get(
|
||||
self.args.url,
|
||||
@@ -36,6 +39,28 @@ class CmdArtifactsGet(CmdBaseNoRepo):
|
||||
)
|
||||
return 1
|
||||
|
||||
def _show_url(self):
|
||||
from dvc.api import artifacts_show, get_url
|
||||
from dvc.ui import ui
|
||||
|
||||
artifact = artifacts_show(
|
||||
self.args.name,
|
||||
version=self.args.rev,
|
||||
stage=self.args.stage,
|
||||
repo=self.args.url,
|
||||
)
|
||||
|
||||
url = get_url(
|
||||
artifact["path"],
|
||||
repo=self.args.url,
|
||||
rev=artifact["rev"],
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
ui.write(url, force=True)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
ARTIFACTS_HELP = "DVC model registry artifact commands."
|
||||
@@ -84,6 +109,14 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Destination path to download artifact to",
|
||||
metavar="<path>",
|
||||
).complete = completion.DIR
|
||||
get_parser.add_argument(
|
||||
"--show-url",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Print the storage location (URL) the target data would be "
|
||||
"downloaded from, and exit."
|
||||
),
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
|
||||
@@ -20,6 +20,7 @@ class CmdCommit(CmdBase):
|
||||
with_deps=self.args.with_deps,
|
||||
recursive=self.args.recursive,
|
||||
force=self.args.force,
|
||||
relink=self.args.relink,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to commit%s", (" " + target) if target else "")
|
||||
@@ -64,6 +65,13 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Commit cache for subdirectories of the specified directory.",
|
||||
)
|
||||
commit_parser.add_argument(
|
||||
"--no-relink",
|
||||
dest="relink",
|
||||
action="store_false",
|
||||
help="Don't recreate links from cache to workspace.",
|
||||
)
|
||||
commit_parser.set_defaults(relink=True)
|
||||
commit_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
|
||||
@@ -194,7 +194,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
@@ -262,7 +262,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Push run history for all stages.",
|
||||
)
|
||||
@@ -324,7 +324,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
@@ -429,5 +429,11 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help="Show status in JSON format.",
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"--no-updates",
|
||||
dest="check_updates",
|
||||
action="store_false",
|
||||
help="Ignore updates to imported data.",
|
||||
)
|
||||
|
||||
status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.repo.datasets import Dataset, FileInfo
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
def diff_files(old: list["FileInfo"], new: list["FileInfo"]) -> dict[str, list[str]]:
|
||||
old_files = {d.relpath: d for d in old}
|
||||
new_files = {d.relpath: d for d in new}
|
||||
rest = old_files.keys() & new_files.keys()
|
||||
return {
|
||||
"added": list(new_files.keys() - old_files.keys()),
|
||||
"deleted": list(old_files.keys() - new_files.keys()),
|
||||
"modified": [p for p in rest if new_files[p] != old_files[p]],
|
||||
}
|
||||
|
||||
|
||||
class CmdDatasetAdd(CmdBase):
|
||||
@classmethod
|
||||
def display(cls, name: str, dataset: "Dataset", action: str = "Adding"):
|
||||
from dvc.ui import ui
|
||||
|
||||
assert dataset.lock
|
||||
|
||||
url = dataset.spec.url
|
||||
ver: str = ""
|
||||
if dataset.type == "dvcx":
|
||||
ver = f"v{dataset.lock.version}"
|
||||
if dataset.type == "dvc":
|
||||
if dataset.lock.path:
|
||||
url = f"{url}:/{dataset.lock.path.lstrip('/')}"
|
||||
if rev := dataset.lock.rev:
|
||||
ver = rev
|
||||
|
||||
ver_part: Optional[Text] = None
|
||||
if ver:
|
||||
ver_part = ui.rich_text.assemble(" @ ", (ver, "repr.number"))
|
||||
text = ui.rich_text.assemble("(", (url, "repr.url"), ver_part or "", ")")
|
||||
ui.write(action, ui.rich_text(name, "cyan"), text, styled=True)
|
||||
|
||||
def run(self):
|
||||
if not self.args.dvc and self.args.rev:
|
||||
raise DvcException("--rev can't be used without --dvc")
|
||||
if not self.args.dvc and self.args.path:
|
||||
raise DvcException("--path can't be used without --dvc")
|
||||
|
||||
d = vars(self.args)
|
||||
for key in ["dvc", "dvcx", "url"]:
|
||||
if url := d.pop(key, None):
|
||||
d.update({"type": key, "url": url})
|
||||
break
|
||||
|
||||
existing = self.repo.datasets.get(self.args.name)
|
||||
with self.repo.scm_context:
|
||||
if not self.args.force and existing:
|
||||
path = self.repo.fs.relpath(existing.manifest_path)
|
||||
raise DvcException(
|
||||
f"{self.args.name} already exists in {path}, "
|
||||
"use the --force to overwrite"
|
||||
)
|
||||
dataset = self.repo.datasets.add(**d)
|
||||
self.display(self.args.name, dataset)
|
||||
return 0
|
||||
|
||||
|
||||
class CmdDatasetUpdate(CmdBase):
|
||||
def display(self, name: str, dataset: "Dataset", new: "Dataset"):
|
||||
from dvc.commands.checkout import log_changes
|
||||
from dvc.ui import ui
|
||||
|
||||
action = "Updating"
|
||||
if not dataset.lock:
|
||||
return CmdDatasetAdd.display(name, new, action)
|
||||
if dataset == new:
|
||||
ui.write("[yellow]Nothing to update[/]", styled=True)
|
||||
return
|
||||
|
||||
assert new.lock
|
||||
|
||||
v: Optional[tuple[str, str]] = None
|
||||
if dataset.type == "dvcx":
|
||||
assert new.type == "dvcx"
|
||||
if new.lock.version < dataset.lock.version:
|
||||
action = "Downgrading"
|
||||
|
||||
v = (f"v{dataset.lock.version}", f"v{new.lock.version}")
|
||||
if dataset.type == "dvc":
|
||||
assert new.type == "dvc"
|
||||
v = (f"{dataset.lock.rev_lock[:9]}", f"{new.lock.rev_lock[:9]}")
|
||||
|
||||
if v:
|
||||
part = ui.rich_text.assemble(
|
||||
(v[0], "repr.number"),
|
||||
" -> ",
|
||||
(v[1], "repr.number"),
|
||||
)
|
||||
else:
|
||||
part = ui.rich_text(dataset.spec.url, "repr.url")
|
||||
changes = ui.rich_text.assemble("(", part, ")")
|
||||
ui.write(action, ui.rich_text(name, "cyan"), changes, styled=True)
|
||||
if dataset.type == "url":
|
||||
assert new.type == "url"
|
||||
stats = diff_files(dataset.lock.files, new.lock.files)
|
||||
log_changes(stats)
|
||||
|
||||
def run(self):
|
||||
from difflib import get_close_matches
|
||||
|
||||
from dvc.repo.datasets import DatasetNotFoundError
|
||||
from dvc.ui import ui
|
||||
|
||||
version = None
|
||||
if self.args.rev:
|
||||
try:
|
||||
version = int(self.args.rev.lstrip("v"))
|
||||
except ValueError:
|
||||
version = self.args.rev
|
||||
|
||||
d = vars(self.args) | {"version": version}
|
||||
with self.repo.scm_context:
|
||||
try:
|
||||
dataset, new = self.repo.datasets.update(**d)
|
||||
except DatasetNotFoundError:
|
||||
logger.exception("")
|
||||
if matches := get_close_matches(self.args.name, self.repo.datasets):
|
||||
ui.write(
|
||||
"did you mean?",
|
||||
ui.rich_text(matches[0], "cyan"),
|
||||
stderr=True,
|
||||
styled=True,
|
||||
)
|
||||
return 1
|
||||
self.display(self.args.name, dataset, new)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
ds_parser = subparsers.add_parser(
|
||||
"dataset",
|
||||
aliases=["ds"],
|
||||
parents=[parent_parser],
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
ds_subparsers = ds_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use `dvc dataset CMD --help` to display command-specific help.",
|
||||
required=True,
|
||||
)
|
||||
|
||||
dataset_add_help = "Add a dataset."
|
||||
ds_add_parser = ds_subparsers.add_parser(
|
||||
"add",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(dataset_add_help, "dataset/add"),
|
||||
formatter_class=formatter.RawTextHelpFormatter,
|
||||
help=dataset_add_help,
|
||||
)
|
||||
|
||||
url_exclusive_group = ds_add_parser.add_mutually_exclusive_group(required=True)
|
||||
url_exclusive_group.add_argument(
|
||||
"--dvcx", metavar="name", help="Name of the dvcx dataset to track"
|
||||
)
|
||||
url_exclusive_group.add_argument(
|
||||
"--dvc",
|
||||
help="Path or URL to a Git/DVC repository to track",
|
||||
metavar="url",
|
||||
)
|
||||
url_exclusive_group.add_argument(
|
||||
"--url",
|
||||
help="""\
|
||||
URL of a cloud-versioned remote to track. Supported URLs:
|
||||
|
||||
s3://bucket/key/path
|
||||
gs://bucket/path/to/file/or/dir
|
||||
azure://mycontainer/path
|
||||
remote://remote_name/path/to/file/or/dir (see `dvc remote`)
|
||||
""",
|
||||
)
|
||||
ds_add_parser.add_argument("name", help="Name of the dataset to add")
|
||||
ds_add_parser.add_argument(
|
||||
"--rev",
|
||||
help="Git revision, e.g. SHA, branch, tag (only applicable with --dvc)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
ds_add_parser.add_argument(
|
||||
"--path",
|
||||
help="Path to a file or a directory within a git repository "
|
||||
"(only applicable with --dvc)",
|
||||
)
|
||||
ds_add_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Overwrite existing dataset",
|
||||
)
|
||||
ds_add_parser.set_defaults(func=CmdDatasetAdd)
|
||||
|
||||
dataset_update_help = "Update a dataset."
|
||||
ds_update_parser = ds_subparsers.add_parser(
|
||||
"update",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(dataset_update_help, "dataset/update"),
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
help=dataset_update_help,
|
||||
)
|
||||
ds_update_parser.add_argument("name", help="Name of the dataset to update")
|
||||
ds_update_parser.add_argument(
|
||||
"--rev",
|
||||
"--version",
|
||||
nargs="?",
|
||||
help="DVCX dataset version or Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<version>",
|
||||
)
|
||||
ds_update_parser.set_defaults(func=CmdDatasetUpdate)
|
||||
@@ -1,3 +1,5 @@
|
||||
import argparse
|
||||
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
@@ -81,7 +83,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_pull_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Pull run history for all stages.",
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import argparse
|
||||
from typing import Any
|
||||
|
||||
from dvc.cli import completion, formatter
|
||||
@@ -120,7 +121,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_push_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Push run history for all stages.",
|
||||
)
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ class CmdGC(CmdBase):
|
||||
msg += " of the current and the following repos:"
|
||||
|
||||
for repo_path in self.args.repos:
|
||||
msg += "\n - %s" % os.path.abspath(repo_path)
|
||||
msg += f"\n - {os.path.abspath(repo_path)}"
|
||||
else:
|
||||
msg += " of the current repo."
|
||||
|
||||
|
||||
+7
-1
@@ -12,7 +12,13 @@ class CmdGet(CmdBaseNoRepo):
|
||||
from dvc.api import get_url
|
||||
from dvc.ui import ui
|
||||
|
||||
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
|
||||
url = get_url(
|
||||
self.args.path,
|
||||
repo=self.args.url,
|
||||
rev=self.args.rev,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
ui.write(url, force=True)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -57,6 +57,9 @@ class CmdList(CmdBaseNoRepo):
|
||||
elif entries:
|
||||
show_entries(entries, with_color=True, with_size=self.args.size)
|
||||
return 0
|
||||
except FileNotFoundError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
except DvcException:
|
||||
logger.exception("failed to list '%s'", self.args.url)
|
||||
return 1
|
||||
|
||||
@@ -72,7 +72,7 @@ class CmdPlots(CmdBase):
|
||||
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
|
||||
return html_template_path
|
||||
|
||||
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
|
||||
def run(self) -> int: # noqa: C901, PLR0911
|
||||
from pathlib import Path
|
||||
|
||||
from dvc.render.match import match_defs_renderers
|
||||
|
||||
@@ -117,10 +117,7 @@ and then the stage name name.
|
||||
"--pull",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Try automatically pulling missing cache for outputs restored "
|
||||
"from the run-cache."
|
||||
),
|
||||
help="Try automatically pulling missing data.",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--allow-missing",
|
||||
|
||||
@@ -68,7 +68,7 @@ def prepare_stages_data(
|
||||
class CmdStageList(CmdBase):
|
||||
def _get_stages(self) -> Iterable["Stage"]:
|
||||
if self.args.all:
|
||||
stages: list["Stage"] = self.repo.index.stages
|
||||
stages: list[Stage] = self.repo.index.stages
|
||||
logger.trace("%d no. of stages found", len(stages))
|
||||
return stages
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ class CmdDataStatus(CmdDataBase):
|
||||
all_commits=self.args.all_commits,
|
||||
with_deps=self.args.with_deps,
|
||||
recursive=self.args.recursive,
|
||||
check_updates=self.args.check_updates,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
|
||||
+37
-12
@@ -5,6 +5,7 @@ from funcy import get_in
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.config import CmdConfig
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
@@ -12,22 +13,35 @@ logger = logger.getChild(__name__)
|
||||
|
||||
class CmdStudioLogin(CmdConfig):
|
||||
def run(self):
|
||||
from dvc_studio_client.auth import StudioAuthError, get_access_token
|
||||
|
||||
from dvc.env import DVC_STUDIO_URL
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.studio import STUDIO_URL
|
||||
from dvc_studio_client.auth import StudioAuthError, get_access_token
|
||||
|
||||
studio = self.config.get("studio", {})
|
||||
name = self.args.name
|
||||
hostname = self.args.hostname or os.environ.get(DVC_STUDIO_URL) or STUDIO_URL
|
||||
hostname = (
|
||||
self.args.hostname
|
||||
or os.environ.get(DVC_STUDIO_URL)
|
||||
or studio.get("url")
|
||||
or STUDIO_URL
|
||||
)
|
||||
scopes = self.args.scopes
|
||||
|
||||
if studio.get("url", hostname) == hostname and "token" in studio:
|
||||
raise DvcException(
|
||||
"Token already exists. "
|
||||
"To login with a different token, "
|
||||
"logout using 'dvc studio logout'."
|
||||
)
|
||||
|
||||
open_browser = not self.args.no_open
|
||||
try:
|
||||
token_name, access_token = get_access_token(
|
||||
_, access_token = get_access_token(
|
||||
token_name=name,
|
||||
hostname=hostname,
|
||||
scopes=scopes,
|
||||
use_device_code=self.args.no_open,
|
||||
open_browser=open_browser,
|
||||
client_name="DVC",
|
||||
)
|
||||
except StudioAuthError as e:
|
||||
@@ -35,17 +49,26 @@ class CmdStudioLogin(CmdConfig):
|
||||
return 1
|
||||
|
||||
self.save_config(hostname, access_token)
|
||||
ui.write(
|
||||
"Authentication has been successfully completed."
|
||||
"The generated token will now be accessible as"
|
||||
f" {token_name} in the user's Studio profile."
|
||||
)
|
||||
|
||||
if not self.config["exp"].get("auto_push", True):
|
||||
from dvc.ui import ui
|
||||
|
||||
ui.warn(
|
||||
"exp.auto_push is disabled. \n"
|
||||
"Enable with 'dvc config exp.auto_push true' "
|
||||
"to automatically push experiments to Studio."
|
||||
)
|
||||
|
||||
config_path = self.config.files["global"]
|
||||
ui.write(f"Authentication complete. Saved token to {config_path}.")
|
||||
return 0
|
||||
|
||||
def save_config(self, hostname, token):
|
||||
with self.config.edit("global") as conf:
|
||||
conf["studio"]["token"] = token
|
||||
conf["studio"]["url"] = hostname
|
||||
if "auto_push" not in conf["exp"]:
|
||||
conf["exp"]["auto_push"] = True
|
||||
|
||||
|
||||
class CmdStudioLogout(CmdConfig):
|
||||
@@ -54,12 +77,14 @@ class CmdStudioLogout(CmdConfig):
|
||||
|
||||
with self.config.edit("global") as conf:
|
||||
if not get_in(conf, ["studio", "token"]):
|
||||
ui.error_write("Not logged in to Studio.")
|
||||
ui.error_write(
|
||||
"Not logged in to Studio. Log in with 'dvc studio login'."
|
||||
)
|
||||
return 1
|
||||
|
||||
del conf["studio"]["token"]
|
||||
|
||||
ui.write("Logged out from Studio")
|
||||
ui.write("Logged out from Studio (you can log back in with 'dvc studio login')")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+3
-5
@@ -88,12 +88,10 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
return list(it)
|
||||
|
||||
@overload
|
||||
def __setitem__(self, item: int, value: Sequence["CellT"]) -> None:
|
||||
...
|
||||
def __setitem__(self, item: int, value: Sequence["CellT"]) -> None: ...
|
||||
|
||||
@overload
|
||||
def __setitem__(self, item: slice, value: Iterable[Sequence["CellT"]]) -> None:
|
||||
...
|
||||
def __setitem__(self, item: slice, value: Iterable[Sequence["CellT"]]) -> None: ...
|
||||
|
||||
def __setitem__(self, item, value) -> None:
|
||||
it = value
|
||||
@@ -162,7 +160,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
if key not in keys:
|
||||
self.add_column(key)
|
||||
|
||||
row: list["CellT"] = [
|
||||
row: list[CellT] = [
|
||||
with_value(d.get(key), self._fill_value) for key in self.keys()
|
||||
]
|
||||
self.append(row)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""DVC config objects."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
|
||||
+14
-6
@@ -181,7 +181,7 @@ SCHEMA = {
|
||||
"remote": {
|
||||
str: ByUrl(
|
||||
{
|
||||
"": {**LOCAL_COMMON, **REMOTE_COMMON},
|
||||
"": LOCAL_COMMON | REMOTE_COMMON,
|
||||
"s3": {
|
||||
"region": str,
|
||||
"profile": str,
|
||||
@@ -193,6 +193,7 @@ SCHEMA = {
|
||||
"session_token": str,
|
||||
Optional("listobjects", default=False): Bool, # obsoleted
|
||||
Optional("use_ssl", default=True): Bool,
|
||||
Optional("allow_anonymous_login", default=False): Bool,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"sse": str,
|
||||
"sse_kms_key_id": str,
|
||||
@@ -232,7 +233,12 @@ SCHEMA = {
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"hdfs": {"user": str, "kerb_ticket": str, **REMOTE_COMMON},
|
||||
"hdfs": {
|
||||
"user": str,
|
||||
"kerb_ticket": str,
|
||||
"replication": int,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"webhdfs": {
|
||||
"kerberos": Bool,
|
||||
"kerberos_principal": str,
|
||||
@@ -285,10 +291,10 @@ SCHEMA = {
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"http": {**HTTP_COMMON, **REMOTE_COMMON},
|
||||
"https": {**HTTP_COMMON, **REMOTE_COMMON},
|
||||
"webdav": {**WEBDAV_COMMON, **REMOTE_COMMON},
|
||||
"webdavs": {**WEBDAV_COMMON, **REMOTE_COMMON},
|
||||
"http": HTTP_COMMON | REMOTE_COMMON,
|
||||
"https": HTTP_COMMON | REMOTE_COMMON,
|
||||
"webdav": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"webdavs": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"remote": {str: object}, # Any of the above options are valid
|
||||
}
|
||||
)
|
||||
@@ -336,6 +342,8 @@ SCHEMA = {
|
||||
"params": str,
|
||||
"plots": str,
|
||||
"live": str,
|
||||
"auto_push": Bool,
|
||||
"git_remote": str,
|
||||
},
|
||||
"parsing": {
|
||||
"bool": All(Lower, Choices("store_true", "boolean_optional")),
|
||||
|
||||
+3
-3
@@ -44,10 +44,10 @@ def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
|
||||
|
||||
startupinfo = STARTUPINFO()
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
popen = subprocess.Popen(
|
||||
popen = subprocess.Popen( # noqa: S603
|
||||
args,
|
||||
close_fds=True,
|
||||
shell=False, # noqa: S603
|
||||
shell=False,
|
||||
startupinfo=startupinfo,
|
||||
creationflags=creationflags,
|
||||
**kwargs,
|
||||
@@ -149,7 +149,7 @@ def _spawn(
|
||||
env: Optional[Mapping[str, str]] = None,
|
||||
output_file: Optional[str] = None,
|
||||
) -> int:
|
||||
file: "AbstractContextManager[Any]" = nullcontext()
|
||||
file: AbstractContextManager[Any] = nullcontext()
|
||||
kwargs = {}
|
||||
if output_file:
|
||||
file = open(output_file, "ab") # noqa: SIM115
|
||||
|
||||
+4
-4
@@ -5,9 +5,9 @@ from dataclasses import dataclass
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.engine import make_url as _make_url
|
||||
from sqlalchemy.exc import NoSuchModuleError
|
||||
from sqlalchemy import create_engine # type: ignore[import]
|
||||
from sqlalchemy.engine import make_url as _make_url # type: ignore[import]
|
||||
from sqlalchemy.exc import NoSuchModuleError # type: ignore[import]
|
||||
|
||||
from dvc import env
|
||||
from dvc.exceptions import DvcException
|
||||
@@ -17,7 +17,7 @@ from dvc.utils import env2bool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.engine import URL, Connectable, Engine
|
||||
from sqlalchemy.sql.expression import Selectable
|
||||
from sqlalchemy.sql.expression import Selectable # type: ignore[import]
|
||||
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
@@ -3,6 +3,7 @@ from urllib.parse import urlparse
|
||||
|
||||
from funcy import compact, merge
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
|
||||
from .db import AbstractDependency
|
||||
@@ -47,9 +48,15 @@ class DatasetDependency(AbstractDependency):
|
||||
)
|
||||
|
||||
def workspace_status(self):
|
||||
registered = self.repo.index.datasets.get(self.name, {})
|
||||
ds = self.repo.datasets[self.name]
|
||||
if not ds.lock:
|
||||
return {str(self): "not in sync"}
|
||||
|
||||
info: dict[str, Any] = self.hash_info.value if self.hash_info else {} # type: ignore[assignment]
|
||||
if info != registered:
|
||||
lock = self.repo.datasets._lock_from_info(info)
|
||||
if not lock:
|
||||
return {str(self): "new"}
|
||||
if lock != ds.lock:
|
||||
return {str(self): "modified"}
|
||||
return {}
|
||||
|
||||
@@ -57,7 +64,15 @@ class DatasetDependency(AbstractDependency):
|
||||
return self.workspace_status()
|
||||
|
||||
def get_hash(self):
|
||||
return HashInfo(self.PARAM_DATASET, self.repo.index.datasets.get(self.name, {}))
|
||||
ds = self.repo.datasets[self.name]
|
||||
if not ds.lock:
|
||||
if ds._invalidated:
|
||||
raise DvcException(
|
||||
"Dataset information is not in sync. "
|
||||
f"Run 'dvc ds update {self.name}' to sync."
|
||||
)
|
||||
raise DvcException("Dataset information missing from dvc.lock file")
|
||||
return HashInfo(self.PARAM_DATASET, ds.lock.to_dict()) # type: ignore[arg-type]
|
||||
|
||||
def save(self):
|
||||
self.hash_info = self.get_hash()
|
||||
|
||||
+14
-6
@@ -11,6 +11,7 @@ from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage import Stage
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
@@ -21,9 +22,12 @@ def download_progress(to: "Output") -> Iterator[Callable[[int], Any]]:
|
||||
from dvc.ui import ui
|
||||
from dvc.ui._rich_progress import DbDownloadProgress
|
||||
|
||||
with log_durations(logger.debug, f"Saving to {to}"), DbDownloadProgress(
|
||||
console=ui.error_console,
|
||||
) as progress:
|
||||
with (
|
||||
log_durations(logger.debug, f"Saving to {to}"),
|
||||
DbDownloadProgress(
|
||||
console=ui.error_console,
|
||||
) as progress,
|
||||
):
|
||||
task = progress.add_task("Saving", total=None, output=to)
|
||||
yield lambda n: progress.advance(task, advance=n)
|
||||
progress.update(task, description="Saved", total=0)
|
||||
@@ -33,13 +37,17 @@ class AbstractDependency(Dependency):
|
||||
"""Dependency without workspace/fs/fs_path"""
|
||||
|
||||
def __init__(self, stage: "Stage", info: dict[str, Any], *args, **kwargs):
|
||||
self.repo = stage.repo
|
||||
self.repo: Repo = stage.repo
|
||||
self.stage = stage
|
||||
self.fs = None
|
||||
self.fs_path = None
|
||||
self.def_path = None # type: ignore[assignment]
|
||||
self.info = info or {}
|
||||
|
||||
@property
|
||||
def is_in_repo(self):
|
||||
return False
|
||||
|
||||
|
||||
class DbDependency(AbstractDependency):
|
||||
PARAM_CONNECTION = "connection"
|
||||
@@ -66,8 +74,8 @@ class DbDependency(AbstractDependency):
|
||||
return self.db_info.get(self.PARAM_QUERY) or self.db_info.get(self.PARAM_TABLE)
|
||||
|
||||
def __repr__(self):
|
||||
return "{}:{}".format(
|
||||
self.__class__.__name__, "".join(f"{k}=={v}" for k, v in self.info.items())
|
||||
return "{}: {}".format(
|
||||
self.__class__.__name__, "".join(f"{k}={v}" for k, v in self.info.items())
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
+28
-1
@@ -3,12 +3,14 @@ from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from dvc.prompt import confirm
|
||||
from dvc.utils import as_posix
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import DVCFileSystem
|
||||
from dvc.output import Output
|
||||
from dvc.stage import Stage
|
||||
|
||||
|
||||
@@ -35,7 +37,7 @@ class RepoDependency(Dependency):
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
self.fs = self._make_fs()
|
||||
self.fs_path = as_posix(self.def_path)
|
||||
self.fs_path = as_posix(self.fs.normpath(self.def_path))
|
||||
|
||||
def _parse_path(self, fs, fs_path): # noqa: ARG002
|
||||
return None
|
||||
@@ -91,6 +93,31 @@ class RepoDependency(Dependency):
|
||||
self.PARAM_REPO: self._dump_def_repo(self.def_repo),
|
||||
}
|
||||
|
||||
def download(self, to: "Output", jobs: Optional[int] = None):
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.checkout import CheckoutError, checkout
|
||||
|
||||
try:
|
||||
repo = self._make_fs(locked=True).repo
|
||||
|
||||
_, _, obj = build(
|
||||
repo.cache.local,
|
||||
self.fs_path,
|
||||
repo.dvcfs,
|
||||
repo.cache.local.fs.PARAM_CHECKSUM,
|
||||
)
|
||||
checkout(
|
||||
to.fs_path,
|
||||
to.fs,
|
||||
obj,
|
||||
self.repo.cache.local,
|
||||
ignore=None,
|
||||
state=self.repo.state,
|
||||
prompt=confirm,
|
||||
)
|
||||
except CheckoutError:
|
||||
super().download(to=to, jobs=jobs)
|
||||
|
||||
def update(self, rev: Optional[str] = None):
|
||||
if rev:
|
||||
self.def_repo[self.PARAM_REV] = rev
|
||||
|
||||
+54
-12
@@ -68,10 +68,9 @@ def is_git_ignored(repo, path):
|
||||
def check_dvcfile_path(repo, path):
|
||||
if not is_valid_filename(path):
|
||||
raise StageFileBadNameError(
|
||||
"bad DVC file name '{}'. DVC files should be named "
|
||||
"'{}' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
|
||||
relpath(path), PROJECT_FILE, os.path.basename(path)
|
||||
)
|
||||
f"bad DVC file name '{relpath(path)}'. DVC files should be named "
|
||||
f"'{PROJECT_FILE}' or have a '.dvc' suffix "
|
||||
f"(e.g. '{os.path.basename(path)}.dvc')."
|
||||
)
|
||||
|
||||
if is_git_ignored(repo, path):
|
||||
@@ -172,6 +171,7 @@ class SingleStageFile(FileMixin):
|
||||
from dvc.stage.loader import SingleStageLoader as LOADER # noqa: N814
|
||||
|
||||
datasets: ClassVar[list[dict[str, Any]]] = []
|
||||
datasets_lock: ClassVar[list[dict[str, Any]]] = []
|
||||
metrics: ClassVar[list[str]] = []
|
||||
plots: ClassVar[Any] = {}
|
||||
params: ClassVar[list[str]] = []
|
||||
@@ -240,6 +240,26 @@ class ProjectFile(FileMixin):
|
||||
if update_lock:
|
||||
self._dump_lockfile(stage, **kwargs)
|
||||
|
||||
def dump_dataset(self, dataset):
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
parsed = self.datasets if data else []
|
||||
raw = data.setdefault("datasets", [])
|
||||
loc = next(
|
||||
(i for i, ds in enumerate(parsed) if ds["name"] == dataset["name"]),
|
||||
None,
|
||||
)
|
||||
if loc is not None:
|
||||
if raw[loc] != parsed[loc]:
|
||||
raise ParametrizedDumpError(
|
||||
"cannot update a parametrized dataset entry"
|
||||
)
|
||||
|
||||
apply_diff(dataset, raw[loc])
|
||||
raw[loc] = dataset
|
||||
else:
|
||||
raw.append(dataset)
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def _dump_lockfile(self, stage, **kwargs):
|
||||
self._lockfile.dump(stage, **kwargs)
|
||||
|
||||
@@ -293,24 +313,28 @@ class ProjectFile(FileMixin):
|
||||
return self.LOADER(self, self.contents, self.lockfile_contents)
|
||||
|
||||
@property
|
||||
def metrics(self) -> list[str]:
|
||||
return self.contents.get("metrics", [])
|
||||
def artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||
return self.resolver.resolve_artifacts()
|
||||
|
||||
@property
|
||||
def plots(self) -> Any:
|
||||
return self.contents.get("plots", {})
|
||||
def metrics(self) -> list[str]:
|
||||
return self.resolver.resolve_metrics()
|
||||
|
||||
@property
|
||||
def params(self) -> list[str]:
|
||||
return self.contents.get("params", [])
|
||||
return self.resolver.resolve_params()
|
||||
|
||||
@property
|
||||
def plots(self) -> list[Any]:
|
||||
return self.resolver.resolve_plots()
|
||||
|
||||
@property
|
||||
def datasets(self) -> list[dict[str, Any]]:
|
||||
return self.contents.get("datasets", [])
|
||||
return self.resolver.resolve_datasets()
|
||||
|
||||
@property
|
||||
def artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||
return self.contents.get("artifacts", {})
|
||||
def datasets_lock(self) -> list[dict[str, Any]]:
|
||||
return self.lockfile_contents.get("datasets", [])
|
||||
|
||||
def remove(self, force=False):
|
||||
if not force:
|
||||
@@ -357,6 +381,24 @@ class Lockfile(FileMixin):
|
||||
self._check_gitignored()
|
||||
return {}, ""
|
||||
|
||||
def dump_dataset(self, dataset: dict):
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
data.update({"schema": "2.0"})
|
||||
if not data:
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
|
||||
datasets: list[dict] = data.setdefault("datasets", [])
|
||||
loc = next(
|
||||
(i for i, ds in enumerate(datasets) if ds["name"] == dataset["name"]),
|
||||
None,
|
||||
)
|
||||
if loc is not None:
|
||||
datasets[loc] = dataset
|
||||
else:
|
||||
datasets.append(dataset)
|
||||
data.setdefault("stages", {})
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
|
||||
|
||||
@@ -19,3 +19,4 @@ DVC_STUDIO_URL = "DVC_STUDIO_URL"
|
||||
DVC_SQLALCHEMY_ECHO = "DVC_SQLALCHEMY_ECHO"
|
||||
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
|
||||
DVC_UPDATER_ENDPOINT = "DVC_UPDATER_ENDPOINT"
|
||||
DVC_STAGE = "DVC_STAGE"
|
||||
|
||||
+6
-6
@@ -1,4 +1,5 @@
|
||||
"""Exceptions raised by the dvc."""
|
||||
|
||||
import errno
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
@@ -132,11 +133,11 @@ class MoveNotDataSourceError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = (
|
||||
"move is not permitted for stages that are not data sources. "
|
||||
"You need to either move '{path}' to a new location and edit "
|
||||
"it by hand, or remove '{path}' and create a new one at the "
|
||||
f"You need to either move {path!r} to a new location and edit "
|
||||
f"it by hand, or remove {path!r} and create a new one at the "
|
||||
"desired location."
|
||||
)
|
||||
super().__init__(msg.format(path=path))
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class NotDvcRepoError(DvcException):
|
||||
@@ -270,9 +271,8 @@ class NoOutputInExternalRepoError(DvcException):
|
||||
from dvc.utils import relpath
|
||||
|
||||
super().__init__(
|
||||
"Output '{}' not found in target repository '{}'".format(
|
||||
relpath(path, external_repo_path), external_repo_url
|
||||
)
|
||||
f"Output {relpath(path, external_repo_path)!r} "
|
||||
f"not found in target repository '{external_repo_url}'"
|
||||
)
|
||||
|
||||
|
||||
|
||||
+11
-4
@@ -1,10 +1,10 @@
|
||||
import glob
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
||||
|
||||
from dvc.config import ConfigError as RepoConfigError
|
||||
from dvc.config_schema import SCHEMA, Invalid
|
||||
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from dvc_objects.fs import ( # noqa: F401
|
||||
@@ -70,7 +70,14 @@ def download(
|
||||
to_infos = [to]
|
||||
|
||||
if isinstance(fs, DVCFileSystem):
|
||||
lfs_prefetch(fs, from_infos)
|
||||
lfs_prefetch(
|
||||
fs,
|
||||
[
|
||||
f"{fs.normpath(glob.escape(fs_path))}/**"
|
||||
if fs.isdir(fs_path)
|
||||
else glob.escape(fs_path)
|
||||
],
|
||||
)
|
||||
cb.set_size(len(from_infos))
|
||||
jobs = jobs or fs.jobs
|
||||
generic.copy(fs, from_infos, localfs, to_infos, callback=cb, batch_size=jobs)
|
||||
@@ -158,5 +165,5 @@ def get_cloud_fs(repo_config, **kwargs):
|
||||
fs_path = cls._strip_protocol(url)
|
||||
|
||||
extras = cls._get_kwargs_from_urls(url)
|
||||
conf = {**extras, **remote_conf} # remote config takes priority
|
||||
conf = extras | remote_conf # remote config takes priority
|
||||
return cls, conf, fs_path
|
||||
|
||||
+18
-57
@@ -1,67 +1,38 @@
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Optional, Union
|
||||
from typing import TYPE_CHECKING, BinaryIO, Optional, Union
|
||||
|
||||
import fsspec
|
||||
from fsspec.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.progress import TaskID
|
||||
from tqdm import tqdm
|
||||
|
||||
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):
|
||||
class TqdmCallback(fsspec.callbacks.TqdmCallback):
|
||||
def __init__(
|
||||
self,
|
||||
size: Optional[int] = None,
|
||||
value: int = 0,
|
||||
progress_bar: Optional["Tqdm"] = None,
|
||||
progress_bar: Optional["tqdm"] = None,
|
||||
tqdm_cls: Optional[type["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)
|
||||
tqdm_kwargs.pop("total", None)
|
||||
super().__init__(
|
||||
tqdm_kwargs=tqdm_kwargs, tqdm_cls=tqdm_cls or Tqdm, size=size, value=value
|
||||
)
|
||||
return self._stack.enter_context(progress_bar)
|
||||
if progress_bar is not None:
|
||||
self.tqdm = 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)
|
||||
def branched(self, path_1: "Union[str, BinaryIO]", path_2: str, **kwargs):
|
||||
desc = path_1 if isinstance(path_1, str) else path_2
|
||||
return TqdmCallback(bytes=True, desc=desc)
|
||||
|
||||
|
||||
class RichCallback(Callback):
|
||||
@@ -110,9 +81,6 @@ class RichCallback(Callback):
|
||||
def task(self) -> "TaskID":
|
||||
return self.progress.add_task(**self._task_kwargs) # type: ignore[arg-type]
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
if self._transient:
|
||||
self.progress.clear_task(self.task)
|
||||
@@ -126,17 +94,10 @@ class RichCallback(Callback):
|
||||
visible=not self.disable,
|
||||
)
|
||||
|
||||
def branch(
|
||||
self,
|
||||
path_1: Union[str, BinaryIO],
|
||||
path_2: str,
|
||||
kwargs: dict[str, Any],
|
||||
child: Optional["Callback"] = None,
|
||||
):
|
||||
child = child or RichCallback(
|
||||
def branched(self, path_1: Union[str, BinaryIO], path_2: str, **kwargs):
|
||||
return RichCallback(
|
||||
progress=self.progress,
|
||||
desc=path_1 if isinstance(path_1, str) else path_2,
|
||||
bytes=True,
|
||||
transient=self._transient,
|
||||
)
|
||||
return super().branch(path_1, path_2, kwargs, child=child)
|
||||
|
||||
+25
-16
@@ -360,38 +360,47 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def ls(self, path, detail=True, dvc_only=False, **kwargs):
|
||||
def ls(self, path, detail=True, dvc_only=False, **kwargs): # noqa: C901, PLR0912
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_exists = False
|
||||
dvc_infos = {}
|
||||
dvc_info = {}
|
||||
if dvc_fs:
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
with suppress(FileNotFoundError, NotADirectoryError):
|
||||
for info in dvc_fs.ls(dvc_path, detail=True):
|
||||
dvc_infos[dvc_fs.name(info["name"])] = info
|
||||
dvc_exists = True
|
||||
with suppress(FileNotFoundError):
|
||||
dvc_info = dvc_fs.info(dvc_path)
|
||||
if dvc_info["type"] == "file":
|
||||
dvc_infos[""] = dvc_info
|
||||
else:
|
||||
for info in dvc_fs.ls(dvc_path, detail=True):
|
||||
dvc_infos[dvc_fs.name(info["name"])] = info
|
||||
|
||||
fs_exists = False
|
||||
fs_infos = {}
|
||||
fs_info = {}
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
if not dvc_only:
|
||||
fs = self.repo.fs
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
for info in repo.dvcignore.ls(
|
||||
fs, fs_path, detail=True, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_infos[fs.name(info["name"])] = info
|
||||
fs_exists = True
|
||||
fs_info = fs.info(fs_path)
|
||||
if fs_info["type"] == "file":
|
||||
fs_infos[""] = fs_info
|
||||
else:
|
||||
for info in repo.dvcignore.ls(
|
||||
fs, fs_path, detail=True, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_infos[fs.name(info["name"])] = info
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
pass
|
||||
|
||||
if not (dvc_exists or fs_exists):
|
||||
if not (fs_info or dvc_info):
|
||||
# broken symlink or TreeError
|
||||
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
||||
|
||||
if fs_info and dvc_info and dvc_info["type"] != fs_info["type"]:
|
||||
dvc_infos.clear() # invalidate dvc_info if file type differs
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
|
||||
infos = []
|
||||
@@ -402,7 +411,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
if not dvcfiles and _is_dvc_file(name):
|
||||
continue
|
||||
|
||||
entry_path = self.join(path, name)
|
||||
entry_path = self.join(path, name) if name else path
|
||||
info = _merge_info(
|
||||
repo, (*subkey, name), fs_infos.get(name), dvc_infos.get(name)
|
||||
)
|
||||
@@ -420,7 +429,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info( # noqa: C901, PLR0912
|
||||
def _info( # noqa: C901
|
||||
self, key, path, ignore_subrepos=True, check_ignored=True
|
||||
):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
@@ -459,7 +468,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
continue
|
||||
|
||||
if not dvc_info and not fs_info:
|
||||
raise FileNotFoundError
|
||||
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
||||
|
||||
info = _merge_info(repo, subkey, fs_info, dvc_info)
|
||||
info["name"] = path
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""UPath implementation for DVCFileSystem.
|
||||
|
||||
This provides a `pathlib.Path` like interface to
|
||||
work with DVCFileSystem.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> from upath import UPath
|
||||
|
||||
>>> local = UPath("dvc://path/to/local/repo")
|
||||
>>> https = UPath("dvc+https://github.com/iterative/example-get-started", rev="main")
|
||||
>>> ssh = UPath("dvc+ssh://[email protected]:iterative/example-get-started.git")
|
||||
"""
|
||||
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from upath import UPath
|
||||
|
||||
|
||||
class DVCPath(UPath):
|
||||
@classmethod
|
||||
def _transform_init_args(cls, args, protocol, storage_options):
|
||||
if not args:
|
||||
args = ("/",)
|
||||
elif (
|
||||
args
|
||||
and "url" not in storage_options
|
||||
and protocol in {"dvc+http", "dvc+https", "dvc+ssh"}
|
||||
):
|
||||
url, *rest = args
|
||||
url = urlsplit(str(url))
|
||||
proto = protocol.split("+")[1]
|
||||
if proto == "ssh":
|
||||
base_url = url.netloc + url.path
|
||||
else:
|
||||
base_url = url._replace(scheme=proto).geturl()
|
||||
storage_options["url"] = base_url
|
||||
# Assume the given path is a root url
|
||||
args = ("/", *rest)
|
||||
return super()._transform_init_args(args, "dvc", storage_options)
|
||||
|
||||
def __str__(self):
|
||||
s = super().__str__()
|
||||
if url := self.storage_options.get("url"):
|
||||
return s.replace("dvc://", f"dvc+{url}", 1)
|
||||
return s
|
||||
|
||||
def with_segments(self, *pathsegments):
|
||||
obj = super().with_segments(*pathsegments)
|
||||
# cache filesystem, as dvcfs does not cache filesystem
|
||||
# caveat: any joinpath operation will instantiate filesystem
|
||||
obj._fs_cached = self.fs
|
||||
return obj
|
||||
+7
-7
@@ -390,7 +390,7 @@ class Output:
|
||||
self.can_push = push
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj: Optional["HashFile"] = None
|
||||
self.obj: Optional[HashFile] = None
|
||||
|
||||
self.remote = remote
|
||||
|
||||
@@ -814,7 +814,7 @@ class Output:
|
||||
|
||||
ret: dict[str, Any] = {}
|
||||
with_files = (
|
||||
(not self.IS_DEPENDENCY or self.stage.is_import)
|
||||
(not self.IS_DEPENDENCY or kwargs.get("datasets") or self.stage.is_import)
|
||||
and self.hash_info.isdir
|
||||
and (kwargs.get("with_files") or self.files is not None)
|
||||
)
|
||||
@@ -902,7 +902,7 @@ class Output:
|
||||
def get_obj(
|
||||
self, filter_info: Optional[str] = None, **kwargs
|
||||
) -> Optional["HashFile"]:
|
||||
obj: Optional["HashFile"] = None
|
||||
obj: Optional[HashFile] = None
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
elif self.files:
|
||||
@@ -1158,7 +1158,7 @@ class Output:
|
||||
logger.warning(msg)
|
||||
return {}
|
||||
|
||||
obj: Optional["HashFile"]
|
||||
obj: Optional[HashFile]
|
||||
if self.is_dir_checksum:
|
||||
obj = self._collect_used_dir_cache(**kwargs)
|
||||
else:
|
||||
@@ -1353,7 +1353,7 @@ class Output:
|
||||
cache = self.cache if self.use_cache else self.local_cache
|
||||
assert isinstance(cache, HashFileDB)
|
||||
|
||||
new: "HashFile"
|
||||
new: HashFile
|
||||
try:
|
||||
assert self.hash_name
|
||||
staging, meta, obj = self._build(
|
||||
@@ -1365,7 +1365,7 @@ class Output:
|
||||
dry_run=not self.use_cache,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
if self.fs_path == path:
|
||||
if not self.exists:
|
||||
raise self.DoesNotExistError(self) from exc
|
||||
if not self.is_dir_checksum:
|
||||
raise
|
||||
@@ -1478,7 +1478,7 @@ META_SCHEMA = {
|
||||
Meta.PARAM_VERSION_ID: str,
|
||||
}
|
||||
|
||||
CLOUD_SCHEMA = vol.All({str: {**META_SCHEMA, **CHECKSUMS_SCHEMA}}, vol.Length(max=1))
|
||||
CLOUD_SCHEMA = vol.All({str: META_SCHEMA | CHECKSUMS_SCHEMA}, vol.Length(max=1))
|
||||
|
||||
ARTIFACT_SCHEMA: dict[Any, Any] = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
|
||||
+92
-3
@@ -21,6 +21,7 @@ from .context import (
|
||||
VarsAlreadyLoaded,
|
||||
)
|
||||
from .interpolate import (
|
||||
check_expression,
|
||||
check_recursive_parse_errors,
|
||||
is_interpolated_string,
|
||||
recurse,
|
||||
@@ -38,10 +39,16 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
STAGES_KWD = "stages"
|
||||
VARS_KWD = "vars"
|
||||
WDIR_KWD = "wdir"
|
||||
|
||||
ARTIFACTS_KWD = "artifacts"
|
||||
DATASETS_KWD = "datasets"
|
||||
METRICS_KWD = "metrics"
|
||||
PARAMS_KWD = "params"
|
||||
PLOTS_KWD = "plots"
|
||||
STAGES_KWD = "stages"
|
||||
|
||||
FOREACH_KWD = "foreach"
|
||||
MATRIX_KWD = "matrix"
|
||||
DO_KWD = "do"
|
||||
@@ -163,6 +170,27 @@ class DataResolver:
|
||||
for name, definition in stages_data.items()
|
||||
}
|
||||
|
||||
self.artifacts = [
|
||||
ArtifactDefinition(self, self.context, name, definition, ARTIFACTS_KWD)
|
||||
for name, definition in d.get(ARTIFACTS_KWD, {}).items()
|
||||
]
|
||||
self.datasets = [
|
||||
TopDefinition(self, self.context, str(i), definition, DATASETS_KWD)
|
||||
for i, definition in enumerate(d.get(DATASETS_KWD, []))
|
||||
]
|
||||
self.metrics = [
|
||||
TopDefinition(self, self.context, str(i), definition, METRICS_KWD)
|
||||
for i, definition in enumerate(d.get(METRICS_KWD, []))
|
||||
]
|
||||
self.params = [
|
||||
TopDefinition(self, self.context, str(i), definition, PARAMS_KWD)
|
||||
for i, definition in enumerate(d.get(PARAMS_KWD, []))
|
||||
]
|
||||
self.plots = [
|
||||
TopDefinition(self, self.context, str(i), definition, PLOTS_KWD)
|
||||
for i, definition in enumerate(d.get(PLOTS_KWD, []))
|
||||
]
|
||||
|
||||
def resolve_one(self, name: str):
|
||||
group, key = split_group_name(name)
|
||||
|
||||
@@ -186,6 +214,27 @@ class DataResolver:
|
||||
logger.trace("Resolved dvc.yaml:\n%s", data)
|
||||
return {STAGES_KWD: data}
|
||||
|
||||
# Top-level sections are eagerly evaluated, whereas stages are lazily evaluated,
|
||||
# one-by-one.
|
||||
|
||||
def resolve_artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||
d: dict[str, Optional[dict[str, Any]]] = {}
|
||||
for item in self.artifacts:
|
||||
d.update(item.resolve())
|
||||
return d
|
||||
|
||||
def resolve_datasets(self) -> list[dict[str, Any]]:
|
||||
return [item.resolve() for item in self.datasets]
|
||||
|
||||
def resolve_metrics(self) -> list[str]:
|
||||
return [item.resolve() for item in self.metrics]
|
||||
|
||||
def resolve_params(self) -> list[str]:
|
||||
return [item.resolve() for item in self.params]
|
||||
|
||||
def resolve_plots(self) -> list[Any]:
|
||||
return [item.resolve() for item in self.plots]
|
||||
|
||||
def has_key(self, key: str):
|
||||
return self._has_group_and_key(*split_group_name(key))
|
||||
|
||||
@@ -514,10 +563,10 @@ class MatrixDefinition:
|
||||
iterable = self.resolved_iterable
|
||||
assert isinstance(iterable, Mapping)
|
||||
|
||||
ret: dict[str, "DictStrAny"] = {}
|
||||
ret: dict[str, DictStrAny] = {}
|
||||
matrix = {key: enumerate(v) for key, v in iterable.items()}
|
||||
for combination in product(*matrix.values()):
|
||||
d: "DictStrAny" = {}
|
||||
d: DictStrAny = {}
|
||||
fragments: list[str] = []
|
||||
for k, (i, v) in zip(matrix.keys(), combination):
|
||||
d[k] = v
|
||||
@@ -565,3 +614,43 @@ class MatrixDefinition:
|
||||
return entry.resolve_stage(skip_checks=True)
|
||||
except ContextError as exc:
|
||||
format_and_raise(exc, f"stage '{generated}'", self.relpath)
|
||||
|
||||
|
||||
class TopDefinition:
|
||||
def __init__(
|
||||
self,
|
||||
resolver: DataResolver,
|
||||
context: Context,
|
||||
name: str,
|
||||
definition: "Any",
|
||||
where: str,
|
||||
):
|
||||
self.resolver = resolver
|
||||
self.context = context
|
||||
self.name = name
|
||||
self.definition = definition
|
||||
self.where = where
|
||||
self.relpath = self.resolver.relpath
|
||||
|
||||
def resolve(self):
|
||||
try:
|
||||
check_recursive_parse_errors(self.definition)
|
||||
return self.context.resolve(self.definition)
|
||||
except (ParseError, ContextError) as exc:
|
||||
format_and_raise(exc, f"'{self.where}.{self.name}'", self.relpath)
|
||||
|
||||
|
||||
class ArtifactDefinition(TopDefinition):
|
||||
def resolve(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||
try:
|
||||
check_expression(self.name)
|
||||
name = self.context.resolve(self.name)
|
||||
if not isinstance(name, str):
|
||||
typ = type(name).__name__
|
||||
raise ResolveError(
|
||||
f"failed to resolve '{self.where}.{self.name}'"
|
||||
f" in '{self.relpath}': expected str, got " + typ
|
||||
)
|
||||
except (ParseError, ContextError) as exc:
|
||||
format_and_raise(exc, f"'{self.where}.{self.name}'", self.relpath)
|
||||
return {name: super().resolve()}
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
"""Manages progress bars for DVC repo."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from threading import RLock
|
||||
@@ -40,7 +41,7 @@ class Tqdm(tqdm):
|
||||
"miniters": 1,
|
||||
}
|
||||
|
||||
def __init__( # noqa: PLR0913
|
||||
def __init__(
|
||||
self,
|
||||
iterable=None,
|
||||
disable=None,
|
||||
@@ -68,7 +69,7 @@ class Tqdm(tqdm):
|
||||
"""
|
||||
kwargs = kwargs.copy()
|
||||
if bytes:
|
||||
kwargs = {**self.BYTES_DEFAULTS, **kwargs}
|
||||
kwargs = self.BYTES_DEFAULTS | kwargs
|
||||
else:
|
||||
kwargs.setdefault("unit_scale", total > 999 if total else True)
|
||||
if file is None:
|
||||
|
||||
@@ -285,7 +285,7 @@ class VegaConverter(Converter):
|
||||
if not all_datapoints:
|
||||
return [], {}
|
||||
|
||||
properties = {**properties, **props_update}
|
||||
properties = properties | props_update
|
||||
|
||||
return all_datapoints, properties
|
||||
|
||||
@@ -298,7 +298,7 @@ class VegaConverter(Converter):
|
||||
inferred_properties = self._infer_x_y()
|
||||
|
||||
datapoints = self._find_datapoints()
|
||||
properties = {**self.properties, **inferred_properties}
|
||||
properties = self.properties | inferred_properties
|
||||
|
||||
properties["y_label"] = self.infer_y_label(properties)
|
||||
properties["x_label"] = self.infer_x_label(properties)
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ def _squash_plots_properties(data: list) -> dict:
|
||||
configs = [last(group) for group in data]
|
||||
resolved: dict = {}
|
||||
for config in reversed(configs):
|
||||
resolved = {**resolved, **config}
|
||||
resolved = resolved | config
|
||||
return resolved
|
||||
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ class Repo:
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem, localfs
|
||||
from dvc.lock import LockNoop, make_lock
|
||||
from dvc.repo.artifacts import Artifacts
|
||||
from dvc.repo.datasets import Datasets
|
||||
from dvc.repo.metrics import Metrics
|
||||
from dvc.repo.params import Params
|
||||
from dvc.repo.plots import Plots
|
||||
@@ -183,12 +184,12 @@ class Repo:
|
||||
# used by DVCFileSystem to determine if it should traverse subrepos
|
||||
self.subrepos = subrepos
|
||||
|
||||
self.cloud: "DataCloud" = DataCloud(self)
|
||||
self.stage: "StageLoad" = StageLoad(self)
|
||||
self.cloud: DataCloud = DataCloud(self)
|
||||
self.stage: StageLoad = StageLoad(self)
|
||||
|
||||
self.lock: "LockBase"
|
||||
self.lock: LockBase
|
||||
self.cache: CacheManager
|
||||
self.state: "StateBase"
|
||||
self.state: StateBase
|
||||
if isinstance(self.fs, GitFileSystem) or not self.dvc_dir:
|
||||
self.lock = LockNoop()
|
||||
self.state = StateNoop()
|
||||
@@ -220,6 +221,7 @@ class Repo:
|
||||
self.plots: Plots = Plots(self)
|
||||
self.params: Params = Params(self)
|
||||
self.artifacts: Artifacts = Artifacts(self)
|
||||
self.datasets: Datasets = Datasets(self)
|
||||
|
||||
self.stage_collection_error_handler: Optional[
|
||||
Callable[[str, Exception], None]
|
||||
@@ -549,11 +551,7 @@ class Repo:
|
||||
|
||||
if out.protocol == "local" and match(fs_path, out.fs_path):
|
||||
return True
|
||||
|
||||
if recursive and out.fs.isin(out.fs_path, fs_path):
|
||||
return True
|
||||
|
||||
return False
|
||||
return recursive and out.fs.isin(out.fs_path, fs_path)
|
||||
|
||||
matched = list(filter(func, outs))
|
||||
if not matched:
|
||||
@@ -653,6 +651,7 @@ class Repo:
|
||||
|
||||
def _reset(self):
|
||||
self.scm._reset()
|
||||
self.datasets._reset()
|
||||
self.state.close()
|
||||
if "dvcfs" in self.__dict__:
|
||||
self.dvcfs.close()
|
||||
|
||||
+14
-3
@@ -171,11 +171,16 @@ def _add_transfer(
|
||||
stage.dump()
|
||||
|
||||
|
||||
def _add(stage: "Stage", source: Optional[str] = None, no_commit: bool = False) -> None:
|
||||
def _add(
|
||||
stage: "Stage",
|
||||
source: Optional[str] = None,
|
||||
no_commit: bool = False,
|
||||
relink: bool = True,
|
||||
) -> None:
|
||||
out = stage.outs[0]
|
||||
path = out.fs.abspath(source) if source else None
|
||||
try:
|
||||
stage.add_outs(path, no_commit=no_commit)
|
||||
stage.add_outs(path, no_commit=no_commit, relink=relink)
|
||||
except CacheLinkError:
|
||||
stage.dump()
|
||||
raise
|
||||
@@ -194,6 +199,7 @@ def add(
|
||||
to_remote: bool = False,
|
||||
remote_jobs: Optional[int] = None,
|
||||
force: bool = False,
|
||||
relink: bool = True,
|
||||
) -> list["Stage"]:
|
||||
add_targets = find_targets(targets, glob=glob)
|
||||
if not add_targets:
|
||||
@@ -224,7 +230,12 @@ def add(
|
||||
with warn_link_failures() as link_failures:
|
||||
for source, (stage, output_exists) in progress_iter(stages_with_targets):
|
||||
try:
|
||||
_add(stage, source if output_exists else None, no_commit=no_commit)
|
||||
_add(
|
||||
stage,
|
||||
source if output_exists else None,
|
||||
no_commit=no_commit,
|
||||
relink=relink,
|
||||
)
|
||||
except CacheLinkError:
|
||||
link_failures.append(stage.relpath)
|
||||
return stages
|
||||
|
||||
+66
-24
@@ -12,7 +12,7 @@ from dvc.exceptions import (
|
||||
InvalidArgumentError,
|
||||
)
|
||||
from dvc.log import logger
|
||||
from dvc.utils import relpath, resolve_output
|
||||
from dvc.utils import as_posix, relpath, resolve_output
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc.utils.serialize import modify_yaml
|
||||
|
||||
@@ -99,7 +99,7 @@ class Artifacts:
|
||||
"""Read artifacts from dvc.yaml."""
|
||||
artifacts: dict[str, dict[str, Artifact]] = {}
|
||||
for dvcfile, dvcfile_artifacts in self.repo.index._artifacts.items():
|
||||
dvcyaml = relpath(dvcfile, self.repo.root_dir)
|
||||
dvcyaml = self.repo.fs.relpath(dvcfile, self.repo.root_dir)
|
||||
artifacts[dvcyaml] = {}
|
||||
for name, value in dvcfile_artifacts.items():
|
||||
try:
|
||||
@@ -137,18 +137,16 @@ class Artifacts:
|
||||
|
||||
assert not (version and stage)
|
||||
name = _reformat_name(name)
|
||||
tags: list["GitTag"] = find(
|
||||
name=name, version=version, stage=stage, scm=self.scm
|
||||
)
|
||||
tags: list[GitTag] = find(name=name, version=version, stage=stage, scm=self.scm)
|
||||
if not tags:
|
||||
raise ArtifactNotFoundError(name, version=version, stage=stage)
|
||||
if version or stage:
|
||||
return tags[-1].target
|
||||
gto_tags: list["GTOTag"] = sort_versions(parse_tag(tag) for tag in tags)
|
||||
gto_tags: list[GTOTag] = sort_versions(parse_tag(tag) for tag in tags)
|
||||
return gto_tags[0].tag.target
|
||||
|
||||
def get_path(self, name: str):
|
||||
"""Return repo fspath for the given artifact."""
|
||||
@classmethod
|
||||
def parse_path(cls, name: str) -> tuple[Optional[str], str]:
|
||||
from gto.constants import SEPARATOR_IN_NAME, fullname_re
|
||||
|
||||
name = _reformat_name(name)
|
||||
@@ -158,13 +156,37 @@ class Artifacts:
|
||||
dirname = m.group("dirname")
|
||||
if dirname:
|
||||
dirname = dirname.rstrip(SEPARATOR_IN_NAME)
|
||||
dvcyaml = os.path.join(dirname, PROJECT_FILE) if dirname else PROJECT_FILE
|
||||
artifact_name = m.group("name")
|
||||
|
||||
return dirname, m.group("name")
|
||||
|
||||
def get_path(self, name: str):
|
||||
"""Return fspath for the given artifact relative to the git root."""
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
dirname, artifact_name = self.parse_path(name)
|
||||
# `name`/`dirname` are expected to be a git root relative.
|
||||
# We convert it to dvc-root relative path so that we can read artifacts
|
||||
# from dvc.yaml file.
|
||||
# But we return dirname intact, as we want to return a git-root relative path.
|
||||
# This is useful when reading from `dvcfs` from remote.
|
||||
fs = self.repo.fs
|
||||
assert self.scm
|
||||
if isinstance(fs, GitFileSystem):
|
||||
scm_root = fs.root_marker
|
||||
else:
|
||||
scm_root = self.scm.root_dir
|
||||
|
||||
dirparts = posixpath.normpath(dirname).split(posixpath.sep) if dirname else ()
|
||||
abspath = fs.join(scm_root, *dirparts, PROJECT_FILE)
|
||||
rela = fs.relpath(abspath, self.repo.root_dir)
|
||||
try:
|
||||
artifact = self.read()[dvcyaml][artifact_name]
|
||||
artifact = self.read()[rela][artifact_name]
|
||||
except KeyError as exc:
|
||||
raise ArtifactNotFoundError(name) from exc
|
||||
return os.path.join(dirname, artifact.path) if dirname else artifact.path
|
||||
|
||||
path = posixpath.join(dirname or "", artifact.path)
|
||||
parts = posixpath.normpath(path).split(posixpath.sep)
|
||||
return os.path.join(*parts)
|
||||
|
||||
def download(
|
||||
self,
|
||||
@@ -177,15 +199,28 @@ class Artifacts:
|
||||
) -> tuple[int, str]:
|
||||
"""Download the specified artifact."""
|
||||
from dvc.fs import download as fs_download
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger.debug("Trying to download artifact '%s' via DVC", name)
|
||||
rev = self.get_rev(name, version=version, stage=stage)
|
||||
|
||||
dirname, _ = self.parse_path(name)
|
||||
with self.repo.switch(rev):
|
||||
path = self.get_path(name)
|
||||
root = self.repo.fs.root_marker
|
||||
_dirname = self.repo.fs.join(root, dirname) if dirname else root
|
||||
with Repo(_dirname, fs=self.repo.fs, scm=self.repo.scm) as r:
|
||||
path = r.artifacts.get_path(name)
|
||||
path = self.repo.fs.join(root, as_posix(path))
|
||||
path = self.repo.fs.relpath(path, self.repo.root_dir)
|
||||
# when the `repo` is a subrepo, the path `/subrepo/myart.pkl` for dvcfs
|
||||
# should be translated as `/myart.pkl`,
|
||||
# i.e. relative to the root of the subrepo
|
||||
path = self.repo.fs.join(root, path)
|
||||
path = self.repo.fs.normpath(path)
|
||||
|
||||
out = resolve_output(path, out, force=force)
|
||||
fs = self.repo.dvcfs
|
||||
fs_path = fs.from_os_path(path)
|
||||
count = fs_download(fs, fs_path, os.path.abspath(out), jobs=jobs)
|
||||
count = fs_download(fs, path, os.path.abspath(out), jobs=jobs)
|
||||
return count, out
|
||||
|
||||
@staticmethod
|
||||
@@ -200,10 +235,9 @@ class Artifacts:
|
||||
dvc_studio_config: Optional[dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> tuple[int, str]:
|
||||
from dvc_studio_client.model_registry import get_download_uris
|
||||
|
||||
from dvc.fs import HTTPFileSystem, generic, localfs
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
from dvc_studio_client.model_registry import get_download_uris
|
||||
|
||||
logger.debug("Trying to download artifact '%s' via studio", name)
|
||||
out = out or os.getcwd()
|
||||
@@ -231,7 +265,7 @@ class Artifacts:
|
||||
from_infos.append(url)
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
raise DvcException(
|
||||
f"Failed to download artifact '{name}' via Studio"
|
||||
) from exc
|
||||
@@ -249,7 +283,7 @@ class Artifacts:
|
||||
return len(to_infos), relpath(localfs.commonpath(to_infos))
|
||||
|
||||
@classmethod
|
||||
def get( # noqa: PLR0913
|
||||
def get(
|
||||
cls,
|
||||
url: str,
|
||||
name: str,
|
||||
@@ -278,6 +312,13 @@ class Artifacts:
|
||||
|
||||
name = _reformat_name(name)
|
||||
saved_exc: Optional[Exception] = None
|
||||
|
||||
local_dvc_studio_config = Config().get("studio", {})
|
||||
args_dvc_studio_config = {}
|
||||
if config and not isinstance(config, dict):
|
||||
config = Config.load_file(config)
|
||||
args_dvc_studio_config = config.get("studio", {})
|
||||
|
||||
try:
|
||||
logger.trace("Trying studio-only config")
|
||||
return cls._download_studio(
|
||||
@@ -288,14 +329,13 @@ class Artifacts:
|
||||
out=out,
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
dvc_studio_config=local_dvc_studio_config | args_dvc_studio_config,
|
||||
)
|
||||
except FileExistsLocallyError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
saved_exc = exc
|
||||
|
||||
if config and not isinstance(config, dict):
|
||||
config = Config.load_file(config)
|
||||
with Repo.open(
|
||||
url=url,
|
||||
subrepos=True,
|
||||
@@ -305,7 +345,7 @@ class Artifacts:
|
||||
remote_config=remote_config,
|
||||
) as repo:
|
||||
logger.trace("Trying repo [studio] config")
|
||||
dvc_studio_config = dict(repo.config.get("studio"))
|
||||
repo_dvc_studio_config = repo.config.get("studio", {})
|
||||
try:
|
||||
return cls._download_studio(
|
||||
url,
|
||||
@@ -315,7 +355,9 @@ class Artifacts:
|
||||
out=out,
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
dvc_studio_config=dvc_studio_config,
|
||||
dvc_studio_config=local_dvc_studio_config
|
||||
| repo_dvc_studio_config
|
||||
| args_dvc_studio_config,
|
||||
)
|
||||
except FileExistsLocallyError:
|
||||
raise
|
||||
@@ -333,7 +375,7 @@ class Artifacts:
|
||||
)
|
||||
except FileExistsLocallyError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
if saved_exc:
|
||||
logger.exception(str(saved_exc), exc_info=saved_exc.__cause__)
|
||||
raise DvcException(
|
||||
|
||||
@@ -125,7 +125,7 @@ def checkout( # noqa: C901
|
||||
(StageFileDoesNotExistError, StageFileBadNameError, NoOutputOrStageError),
|
||||
):
|
||||
raise CheckoutErrorSuggestGit(target) from exc
|
||||
raise
|
||||
raise # noqa: PLE0704
|
||||
|
||||
view = self.index.targets_view(
|
||||
targets, recursive=recursive, with_deps=with_deps, onerror=onerror
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
import os
|
||||
from collections.abc import Iterator, Mapping
|
||||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, Union, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from attrs import Attribute, AttrsInstance, asdict, evolve, field, fields, frozen
|
||||
from attrs.converters import default_if_none
|
||||
|
||||
from dvc.dvcfile import Lockfile, ProjectFile
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
from dvc.types import StrPath
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvcx.dataset import DatasetRecord, DatasetVersion # type: ignore[import]
|
||||
from typing_extensions import Self
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
def _get_dataset_record(name: str) -> "DatasetRecord":
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
try:
|
||||
from dvcx.catalog import get_catalog # type: ignore[import]
|
||||
|
||||
except ImportError as exc:
|
||||
raise DvcException("dvcx is not installed") from exc
|
||||
|
||||
catalog = get_catalog()
|
||||
return catalog.get_remote_dataset(name)
|
||||
|
||||
|
||||
def _get_dataset_info(
|
||||
name: str, record: Optional["DatasetRecord"] = None, version: Optional[int] = None
|
||||
) -> "DatasetVersion":
|
||||
record = record or _get_dataset_record(name)
|
||||
assert record
|
||||
v = record.latest_version if version is None else version
|
||||
assert v is not None
|
||||
return record.get_version(v)
|
||||
|
||||
|
||||
def default_str(v) -> str:
|
||||
return default_if_none("")(v)
|
||||
|
||||
|
||||
def to_datetime(d: Union[str, datetime]) -> datetime:
|
||||
return datetime.fromisoformat(d) if isinstance(d, str) else d
|
||||
|
||||
|
||||
def ensure(cls):
|
||||
def inner(v):
|
||||
return cls.from_dict(v) if isinstance(v, dict) else v
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
class SerDe:
|
||||
def to_dict(self: AttrsInstance) -> dict[str, Any]:
|
||||
def filter_defaults(attr: Attribute, v: Any):
|
||||
if attr.metadata.get("exclude_falsy", False) and not v:
|
||||
return False
|
||||
return attr.default != v
|
||||
|
||||
def value_serializer(_inst, _field, v):
|
||||
return v.isoformat() if isinstance(v, datetime) else v
|
||||
|
||||
return asdict(self, filter=filter_defaults, value_serializer=value_serializer)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type["Self"], d: dict[str, Any]) -> "Self":
|
||||
_fields = fields(cast("type[AttrsInstance]", cls))
|
||||
kwargs = {f.name: d[f.name] for f in _fields if f.name in d}
|
||||
return cls(**kwargs)
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class DatasetSpec(SerDe):
|
||||
name: str
|
||||
url: str
|
||||
type: Literal["dvc", "dvcx", "url"]
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class DVCDatasetSpec(DatasetSpec):
|
||||
type: Literal["dvc"]
|
||||
path: str = field(default="", converter=default_str)
|
||||
rev: Optional[str] = None
|
||||
|
||||
|
||||
@frozen(kw_only=True, order=True)
|
||||
class FileInfo(SerDe):
|
||||
relpath: str
|
||||
meta: Meta = field(order=False, converter=ensure(Meta)) # type: ignore[misc]
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class DVCDatasetLock(DVCDatasetSpec):
|
||||
rev_lock: str
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class DVCXDatasetLock(DatasetSpec):
|
||||
version: int
|
||||
created_at: datetime = field(converter=to_datetime)
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class URLDatasetLock(DatasetSpec):
|
||||
meta: Meta = field(converter=ensure(Meta)) # type: ignore[misc]
|
||||
files: list[FileInfo] = field(
|
||||
factory=list,
|
||||
converter=lambda f: sorted(map(ensure(FileInfo), f)),
|
||||
metadata={"exclude_falsy": True},
|
||||
)
|
||||
|
||||
|
||||
def to_spec(lock: "Lock") -> "Spec":
|
||||
cls = DVCDatasetSpec if lock.type == "dvc" else DatasetSpec
|
||||
return cls(**{f.name: getattr(lock, f.name) for f in fields(cls)})
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class DVCDataset:
|
||||
manifest_path: str
|
||||
spec: DVCDatasetSpec
|
||||
lock: Optional[DVCDatasetLock] = None
|
||||
_invalidated: bool = field(default=False, eq=False, repr=False)
|
||||
|
||||
type: ClassVar[Literal["dvc"]] = "dvc"
|
||||
|
||||
def update(self, repo, rev: Optional[str] = None, **kwargs) -> "Self":
|
||||
from dvc.dependency import RepoDependency
|
||||
|
||||
spec = self.spec
|
||||
if rev:
|
||||
spec = evolve(self.spec, rev=rev)
|
||||
|
||||
def_repo = {
|
||||
RepoDependency.PARAM_REV: spec.rev,
|
||||
RepoDependency.PARAM_URL: spec.url,
|
||||
}
|
||||
dep = RepoDependency(def_repo, None, spec.path, repo=repo) # type: ignore[arg-type]
|
||||
dep.save()
|
||||
d = dep.dumpd()
|
||||
|
||||
repo_info = d[RepoDependency.PARAM_REPO]
|
||||
assert isinstance(repo_info, dict)
|
||||
rev_lock = repo_info[RepoDependency.PARAM_REV_LOCK]
|
||||
lock = DVCDatasetLock(**spec.to_dict(), rev_lock=rev_lock)
|
||||
return evolve(self, spec=spec, lock=lock)
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class DVCXDataset:
|
||||
manifest_path: str
|
||||
spec: "DatasetSpec"
|
||||
lock: "Optional[DVCXDatasetLock]" = field(default=None)
|
||||
_invalidated: bool = field(default=False, eq=False, repr=False)
|
||||
|
||||
type: ClassVar[Literal["dvcx"]] = "dvcx"
|
||||
|
||||
@property
|
||||
def pinned(self) -> bool:
|
||||
return self.name_version[1] is not None
|
||||
|
||||
@property
|
||||
def name_version(self) -> tuple[str, Optional[int]]:
|
||||
url = urlparse(self.spec.url)
|
||||
path = url.netloc + url.path
|
||||
parts = path.split("@v")
|
||||
assert parts
|
||||
|
||||
name = parts[0]
|
||||
version = int(parts[1]) if len(parts) > 1 else None
|
||||
return name, version
|
||||
|
||||
def update(
|
||||
self,
|
||||
repo, # noqa: ARG002
|
||||
record: Optional["DatasetRecord"] = None,
|
||||
version: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> "Self":
|
||||
name, _version = self.name_version
|
||||
version = version if version is not None else _version
|
||||
version_info = _get_dataset_info(name, record=record, version=version)
|
||||
lock = DVCXDatasetLock(
|
||||
**self.spec.to_dict(),
|
||||
version=version_info.version,
|
||||
created_at=version_info.created_at,
|
||||
)
|
||||
return evolve(self, lock=lock)
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class URLDataset:
|
||||
manifest_path: str
|
||||
spec: "DatasetSpec"
|
||||
lock: "Optional[URLDatasetLock]" = None
|
||||
_invalidated: bool = field(default=False, eq=False, repr=False)
|
||||
|
||||
type: ClassVar[Literal["url"]] = "url"
|
||||
|
||||
def update(self, repo, **kwargs):
|
||||
from dvc.dependency import Dependency
|
||||
|
||||
dep = Dependency(
|
||||
None, self.spec.url, repo=repo, fs_config={"version_aware": True}
|
||||
)
|
||||
dep.save()
|
||||
d = dep.dumpd(datasets=True)
|
||||
files = [
|
||||
FileInfo(relpath=info["relpath"], meta=Meta.from_dict(info))
|
||||
for info in d.get("files", [])
|
||||
]
|
||||
lock = URLDatasetLock(**self.spec.to_dict(), meta=dep.meta, files=files)
|
||||
return evolve(self, lock=lock)
|
||||
|
||||
|
||||
Lock = Union[DVCDatasetLock, DVCXDatasetLock, URLDatasetLock]
|
||||
Spec = Union[DatasetSpec, DVCDatasetSpec]
|
||||
Dataset = Union[DVCDataset, DVCXDataset, URLDataset]
|
||||
|
||||
|
||||
class DatasetNotFoundError(DvcException, KeyError):
|
||||
def __init__(self, name, *args):
|
||||
self.name = name
|
||||
super().__init__("dataset not found", *args)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.msg
|
||||
|
||||
|
||||
class Datasets(Mapping[str, Dataset]):
|
||||
def __init__(self, repo: "Repo") -> None:
|
||||
self.repo: Repo = repo
|
||||
|
||||
def __repr__(self):
|
||||
return repr(dict(self))
|
||||
|
||||
def __rich_repr__(self):
|
||||
yield dict(self)
|
||||
|
||||
def __getitem__(self, name: str) -> Dataset:
|
||||
try:
|
||||
return self._datasets[name]
|
||||
except KeyError as exc:
|
||||
raise DatasetNotFoundError(name) from exc
|
||||
|
||||
def __setitem__(self, name: str, dataset: Dataset) -> None:
|
||||
self._datasets[name] = dataset
|
||||
|
||||
def __contains__(self, name: object) -> bool:
|
||||
return name in self._datasets
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self._datasets)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._datasets)
|
||||
|
||||
@cached_property
|
||||
def _spec(self) -> dict[str, tuple[str, dict[str, Any]]]:
|
||||
return {
|
||||
dataset["name"]: (path, dataset)
|
||||
for path, datasets in self.repo.index._datasets.items()
|
||||
for dataset in datasets
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def _lock(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||
datasets_lock = self.repo.index._datasets_lock
|
||||
|
||||
def find(path, name) -> Optional[dict[str, Any]]:
|
||||
# only look for `name` in the lock file next to the
|
||||
# corresponding `dvc.yaml` file
|
||||
lock = datasets_lock.get(path, [])
|
||||
return next((dataset for dataset in lock if dataset["name"] == name), None)
|
||||
|
||||
return {ds["name"]: find(path, name) for name, (path, ds) in self._spec.items()}
|
||||
|
||||
@cached_property
|
||||
def _datasets(self) -> dict[str, Dataset]:
|
||||
return {
|
||||
name: self._build_dataset(path, spec, self._lock[name])
|
||||
for name, (path, spec) in self._spec.items()
|
||||
}
|
||||
|
||||
def _reset(self) -> None:
|
||||
self.__dict__.pop("_spec", None)
|
||||
self.__dict__.pop("_lock", None)
|
||||
self.__dict__.pop("_datasets", None)
|
||||
|
||||
@staticmethod
|
||||
def _spec_from_info(spec: dict[str, Any]) -> Spec:
|
||||
typ = spec.get("type")
|
||||
if not typ:
|
||||
raise ValueError("type should be present in spec")
|
||||
if typ == "dvc":
|
||||
return DVCDatasetSpec.from_dict(spec)
|
||||
if typ in {"dvcx", "url"}:
|
||||
return DatasetSpec.from_dict(spec)
|
||||
raise ValueError(f"unknown dataset type: {spec.get('type', '')}")
|
||||
|
||||
@staticmethod
|
||||
def _lock_from_info(lock: Optional[dict[str, Any]]) -> Optional[Lock]:
|
||||
kl = {"dvc": DVCDatasetLock, "dvcx": DVCXDatasetLock, "url": URLDatasetLock}
|
||||
if lock and (cls := kl.get(lock.get("type", ""))): # type: ignore[assignment]
|
||||
return cls.from_dict(lock) # type: ignore[attr-defined]
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _build_dataset(
|
||||
cls,
|
||||
manifest_path: str,
|
||||
spec_data: dict[str, Any],
|
||||
lock_data: Optional[dict[str, Any]] = None,
|
||||
) -> Dataset:
|
||||
_invalidated = False
|
||||
spec = cls._spec_from_info(spec_data)
|
||||
lock = cls._lock_from_info(lock_data)
|
||||
# if dvc.lock and dvc.yaml file are not in sync, we invalidate the lock.
|
||||
if lock is not None and to_spec(lock) != spec:
|
||||
logger.debug(
|
||||
"invalidated lock data for %s in %s",
|
||||
spec.name,
|
||||
manifest_path,
|
||||
)
|
||||
_invalidated = True # signal is used during `dvc repro`/`dvc status`.
|
||||
lock = None
|
||||
|
||||
assert isinstance(spec, DatasetSpec)
|
||||
if spec.type == "dvc":
|
||||
assert lock is None or isinstance(lock, DVCDatasetLock)
|
||||
assert isinstance(spec, DVCDatasetSpec)
|
||||
return DVCDataset(
|
||||
manifest_path=manifest_path,
|
||||
spec=spec,
|
||||
lock=lock,
|
||||
invalidated=_invalidated,
|
||||
)
|
||||
if spec.type == "url":
|
||||
assert lock is None or isinstance(lock, URLDatasetLock)
|
||||
return URLDataset(
|
||||
manifest_path=manifest_path,
|
||||
spec=spec,
|
||||
lock=lock,
|
||||
invalidated=_invalidated,
|
||||
)
|
||||
if spec.type == "dvcx":
|
||||
assert lock is None or isinstance(lock, DVCXDatasetLock)
|
||||
return DVCXDataset(
|
||||
manifest_path=manifest_path,
|
||||
spec=spec,
|
||||
lock=lock,
|
||||
invalidated=_invalidated,
|
||||
)
|
||||
raise ValueError(f"unknown dataset type: {spec.type!r}")
|
||||
|
||||
def add(
|
||||
self,
|
||||
name: str,
|
||||
url: str,
|
||||
type: str, # noqa: A002
|
||||
manifest_path: StrPath = "dvc.yaml",
|
||||
**kwargs: Any,
|
||||
) -> Dataset:
|
||||
assert type in {"dvc", "dvcx", "url"}
|
||||
kwargs.update({"name": name, "url": url, "type": type})
|
||||
dataset = self._build_dataset(os.path.abspath(manifest_path), kwargs)
|
||||
dataset = dataset.update(self.repo)
|
||||
|
||||
self.dump(dataset)
|
||||
self[name] = dataset
|
||||
return dataset
|
||||
|
||||
def update(self, name, **kwargs) -> tuple[Dataset, Dataset]:
|
||||
dataset = self[name]
|
||||
version = kwargs.get("version")
|
||||
|
||||
if dataset.type == "url" and (version or kwargs.get("rev")):
|
||||
raise ValueError("cannot update version/revision for a url")
|
||||
if dataset.type == "dvcx" and version is not None:
|
||||
if not isinstance(version, int):
|
||||
raise TypeError(
|
||||
f"dvcx version has to be an integer, got {type(version).__name__!r}"
|
||||
)
|
||||
if version < 1:
|
||||
raise ValueError(f"dvcx version should be >=1, got {version}")
|
||||
|
||||
new = dataset.update(self.repo, **kwargs)
|
||||
|
||||
self.dump(new, old=dataset)
|
||||
self[name] = new
|
||||
return dataset, new
|
||||
|
||||
def _dump_spec(self, manifest_path: StrPath, spec: Spec) -> None:
|
||||
spec_data = spec.to_dict()
|
||||
assert spec_data.keys() & {"type", "name", "url"}
|
||||
project_file = ProjectFile(self.repo, manifest_path)
|
||||
project_file.dump_dataset(spec_data)
|
||||
|
||||
def _dump_lock(self, manifest_path: StrPath, lock: Lock) -> None:
|
||||
lock_data = lock.to_dict()
|
||||
assert lock_data.keys() & {"type", "name", "url"}
|
||||
lockfile = Lockfile(self.repo, Path(manifest_path).with_suffix(".lock"))
|
||||
lockfile.dump_dataset(lock_data)
|
||||
|
||||
def dump(self, dataset: Dataset, old: Optional[Dataset] = None) -> None:
|
||||
if not old or old.spec != dataset.spec:
|
||||
self._dump_spec(dataset.manifest_path, dataset.spec)
|
||||
if dataset.lock and (not old or old.lock != dataset.lock):
|
||||
self._dump_lock(dataset.manifest_path, dataset.lock)
|
||||
+1
-1
@@ -26,7 +26,7 @@ def _diff(old, new, data_keys, with_missing=False):
|
||||
from dvc_data.index.diff import ADD, DELETE, MODIFY, RENAME
|
||||
from dvc_data.index.diff import diff as idiff
|
||||
|
||||
ret: "dict[str, list[dict]]" = {
|
||||
ret: dict[str, list[dict]] = {
|
||||
"added": [],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
|
||||
@@ -119,7 +119,7 @@ class Experiments:
|
||||
**kwargs,
|
||||
):
|
||||
"""Reproduce and checkout a single (standalone) experiment."""
|
||||
exp_queue: "BaseStashQueue" = (
|
||||
exp_queue: BaseStashQueue = (
|
||||
self.tempdir_queue if tmp_dir else self.workspace_queue
|
||||
)
|
||||
self.queue_one(exp_queue, **kwargs)
|
||||
|
||||
@@ -25,7 +25,7 @@ def apply(repo: "Repo", rev: str, **kwargs):
|
||||
from dvc.repo.checkout import checkout as dvc_checkout
|
||||
from dvc.scm import RevError, resolve_rev
|
||||
|
||||
exps: "Experiments" = repo.experiments
|
||||
exps: Experiments = repo.experiments
|
||||
|
||||
is_stash: bool = False
|
||||
|
||||
@@ -57,7 +57,7 @@ def apply(repo: "Repo", rev: str, **kwargs):
|
||||
|
||||
|
||||
def _apply(repo: "Repo", rev: str, name: Optional[str] = None, is_stash: bool = False):
|
||||
exps: "Experiments" = repo.experiments
|
||||
exps: Experiments = repo.experiments
|
||||
|
||||
with exps.apply_stash.preserve_workspace(rev, name=name):
|
||||
with repo.scm.detach_head(rev, force=True):
|
||||
|
||||
@@ -69,16 +69,16 @@ def collect_rev(
|
||||
|
||||
def _collect_rev(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
revision: str,
|
||||
param_deps: bool = False,
|
||||
**kwargs,
|
||||
) -> SerializableExp:
|
||||
with repo.switch(rev) as rev:
|
||||
with repo.switch(revision) as rev:
|
||||
if rev == "workspace":
|
||||
timestamp: Optional[datetime] = None
|
||||
else:
|
||||
commit = repo.scm.resolve_commit(rev)
|
||||
timestamp = datetime.fromtimestamp(commit.commit_time)
|
||||
timestamp = datetime.fromtimestamp(commit.commit_time) # noqa: DTZ006
|
||||
|
||||
return SerializableExp.from_repo(
|
||||
repo,
|
||||
@@ -174,7 +174,7 @@ def collect_active(
|
||||
"""
|
||||
if not baseline_revs:
|
||||
return {}
|
||||
result: dict[str, list["ExpRange"]] = {}
|
||||
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(
|
||||
@@ -220,7 +220,7 @@ def collect_successful(
|
||||
Returns:
|
||||
Dict mapping baseline revision to successful experiments.
|
||||
"""
|
||||
result: dict[str, list["ExpRange"]] = {}
|
||||
result: dict[str, list[ExpRange]] = {}
|
||||
for baseline_rev in baseline_revs:
|
||||
result[baseline_rev] = list(_collect_baseline(repo, baseline_rev, **kwargs))
|
||||
return result
|
||||
@@ -304,7 +304,7 @@ def collect(
|
||||
)
|
||||
|
||||
workspace_data = collect_rev(repo, "workspace", **kwargs)
|
||||
result: list["ExpState"] = [workspace_data]
|
||||
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 {}
|
||||
@@ -338,6 +338,6 @@ def _sorted_ranges(exp_ranges: Iterable["ExpRange"]) -> list["ExpRange"]:
|
||||
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 datetime.fromtimestamp(0), "" # noqa: DTZ006
|
||||
|
||||
return sorted(exp_ranges, key=_head_timestamp, reverse=True)
|
||||
|
||||
@@ -23,7 +23,11 @@ from dvc.repo.params.show import _collect_top_level_params
|
||||
from dvc.stage.serialize import to_lockfile
|
||||
from dvc.utils import dict_sha256, env2bool, relpath
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.studio import env_to_config
|
||||
from dvc.utils.studio import (
|
||||
env_to_config,
|
||||
get_repo_url,
|
||||
get_subrepo_relpath,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from queue import Queue
|
||||
@@ -195,7 +199,7 @@ class BaseExecutor(ABC):
|
||||
@classmethod
|
||||
def from_info(cls, info: "ExecutorInfo") -> "Self":
|
||||
if info.result_hash:
|
||||
result: Optional["ExecutorResult"] = ExecutorResult(
|
||||
result: Optional[ExecutorResult] = ExecutorResult(
|
||||
info.result_hash,
|
||||
(ExpRefInfo.from_ref(info.result_ref) if info.result_ref else None),
|
||||
info.result_force,
|
||||
@@ -294,13 +298,16 @@ class BaseExecutor(ABC):
|
||||
exp_hash = cls.hash_exp(stages)
|
||||
if include_untracked:
|
||||
dvc.scm.add(include_untracked, force=True) # type: ignore[call-arg]
|
||||
cls.commit(
|
||||
dvc.scm, # type: ignore[arg-type]
|
||||
exp_hash,
|
||||
exp_name=info.name,
|
||||
force=force,
|
||||
message=message,
|
||||
)
|
||||
|
||||
with cls.auto_push(dvc):
|
||||
cls.commit(
|
||||
dvc.scm, # type: ignore[arg-type]
|
||||
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
|
||||
untracked = dvc.scm.untracked_files()
|
||||
@@ -457,16 +464,13 @@ class BaseExecutor(ABC):
|
||||
from dvc.repo.checkout import checkout as dvc_checkout
|
||||
from dvc.ui import ui
|
||||
|
||||
auto_push = env2bool(DVC_EXP_AUTO_PUSH)
|
||||
git_remote = os.getenv(DVC_EXP_GIT_REMOTE, None)
|
||||
|
||||
if queue is not None:
|
||||
queue.put((rev, os.getpid()))
|
||||
if log_errors and log_level is not None:
|
||||
cls._set_log_level(log_level)
|
||||
|
||||
exp_hash: Optional[str] = None
|
||||
exp_ref: Optional["ExpRefInfo"] = None
|
||||
exp_ref: Optional[ExpRefInfo] = None
|
||||
repro_force: bool = False
|
||||
|
||||
if info.name:
|
||||
@@ -480,9 +484,6 @@ class BaseExecutor(ABC):
|
||||
message=message,
|
||||
**kwargs,
|
||||
) as dvc:
|
||||
if auto_push:
|
||||
cls._validate_remotes(dvc, git_remote)
|
||||
|
||||
args, kwargs = cls._repro_args(dvc)
|
||||
if args:
|
||||
targets: Optional[Union[list, str]] = args[0]
|
||||
@@ -516,8 +517,6 @@ class BaseExecutor(ABC):
|
||||
dvc,
|
||||
info,
|
||||
exp_hash,
|
||||
auto_push,
|
||||
git_remote,
|
||||
repro_force,
|
||||
message=message,
|
||||
)
|
||||
@@ -547,22 +546,20 @@ class BaseExecutor(ABC):
|
||||
dvc,
|
||||
info,
|
||||
exp_hash,
|
||||
auto_push,
|
||||
git_remote,
|
||||
repro_force,
|
||||
message: Optional[str] = None,
|
||||
) -> tuple[Optional[str], Optional["ExpRefInfo"], bool]:
|
||||
cls.commit(
|
||||
dvc.scm,
|
||||
exp_hash,
|
||||
exp_name=info.name,
|
||||
force=repro_force,
|
||||
message=message,
|
||||
)
|
||||
if auto_push:
|
||||
cls._auto_push(dvc, dvc.scm, git_remote)
|
||||
with cls.auto_push(dvc):
|
||||
cls.commit(
|
||||
dvc.scm,
|
||||
exp_hash,
|
||||
exp_name=info.name,
|
||||
force=repro_force,
|
||||
message=message,
|
||||
)
|
||||
|
||||
ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
|
||||
exp_ref: Optional["ExpRefInfo"] = ExpRefInfo.from_ref(ref) if ref else None
|
||||
exp_ref: Optional[ExpRefInfo] = ExpRefInfo.from_ref(ref) if ref else None
|
||||
if cls.WARN_UNTRACKED:
|
||||
untracked = dvc.scm.untracked_files()
|
||||
if untracked:
|
||||
@@ -588,9 +585,8 @@ class BaseExecutor(ABC):
|
||||
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_studio_client.post_live_metrics import post_live_metrics
|
||||
|
||||
with Repo(os.path.join(info.root_dir, info.dvc_dir)) as dvc:
|
||||
info.status = TaskStatus.RUNNING
|
||||
@@ -614,7 +610,9 @@ class BaseExecutor(ABC):
|
||||
# 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}
|
||||
dvc_studio_config = run_env_config | dvc_studio_config
|
||||
# override studio repo url if exp git remote set
|
||||
repo_url = get_repo_url(dvc)
|
||||
try:
|
||||
post_live_metrics(
|
||||
"start",
|
||||
@@ -624,6 +622,8 @@ class BaseExecutor(ABC):
|
||||
params=to_studio_params(dvc.params.show()),
|
||||
dvc_studio_config=dvc_studio_config,
|
||||
message=message,
|
||||
subdir=get_subrepo_relpath(dvc),
|
||||
studio_repo_url=repo_url,
|
||||
)
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
@@ -649,6 +649,7 @@ class BaseExecutor(ABC):
|
||||
experiment_rev=dvc.experiments.scm.get_ref(EXEC_BRANCH),
|
||||
metrics=_gather_metrics(dvc, on_error="return"),
|
||||
dvc_studio_config=dvc_studio_config,
|
||||
studio_repo_url=repo_url,
|
||||
)
|
||||
|
||||
if infofile is not None:
|
||||
@@ -668,15 +669,46 @@ class BaseExecutor(ABC):
|
||||
kwargs = {}
|
||||
return args, kwargs
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def auto_push(cls, dvc: "Repo") -> Iterator[None]:
|
||||
exp_config = dvc.config.get("exp", {})
|
||||
auto_push = env2bool(DVC_EXP_AUTO_PUSH, exp_config.get("auto_push", False))
|
||||
if not auto_push:
|
||||
yield
|
||||
return
|
||||
|
||||
git_remote = os.getenv(
|
||||
DVC_EXP_GIT_REMOTE, exp_config.get("git_remote", "origin")
|
||||
)
|
||||
try:
|
||||
cls._validate_remotes(dvc, git_remote)
|
||||
except DvcException as exc:
|
||||
logger.warning("Failed to validate remotes. Disabling auto push: %s", exc)
|
||||
|
||||
yield
|
||||
return
|
||||
yield
|
||||
cls._auto_push(dvc, git_remote)
|
||||
|
||||
@staticmethod
|
||||
def _auto_push(
|
||||
dvc: "Repo",
|
||||
scm: "Git",
|
||||
git_remote: Optional[str],
|
||||
push_cache=True,
|
||||
run_cache=True,
|
||||
):
|
||||
branch = scm.get_ref(EXEC_BRANCH, follow=False)
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
branch = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
|
||||
link = format_link(
|
||||
"https://dvc.org/doc/user-guide/experiment-management/sharing-experiments"
|
||||
)
|
||||
ui.write(
|
||||
f"Pushing experiment to '{git_remote}'. Cancel with CTRL+C. "
|
||||
f"See {link} for more info."
|
||||
)
|
||||
try:
|
||||
dvc.experiments.push(
|
||||
git_remote,
|
||||
@@ -684,7 +716,7 @@ class BaseExecutor(ABC):
|
||||
push_cache=push_cache,
|
||||
run_cache=run_cache,
|
||||
)
|
||||
except BaseException as exc: # noqa: BLE001
|
||||
except DvcException as exc:
|
||||
logger.warning(
|
||||
(
|
||||
"Something went wrong while auto pushing experiment "
|
||||
@@ -704,6 +736,7 @@ class BaseExecutor(ABC):
|
||||
message: Optional[str] = None,
|
||||
):
|
||||
"""Commit stages as an experiment and return the commit SHA."""
|
||||
|
||||
rev = scm.get_rev()
|
||||
if not scm.is_dirty(untracked_files=False):
|
||||
logger.debug("No changes to commit")
|
||||
|
||||
@@ -96,9 +96,10 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
temp_merge: stash_rev,
|
||||
temp_baseline: entry.baseline_rev,
|
||||
}
|
||||
with get_exp_rwlock(
|
||||
repo, writes=[temp_head, temp_merge, temp_baseline]
|
||||
), self.set_temp_refs(scm, temp_ref_dict):
|
||||
with (
|
||||
get_exp_rwlock(repo, writes=[temp_head, temp_merge, temp_baseline]),
|
||||
self.set_temp_refs(scm, temp_ref_dict),
|
||||
):
|
||||
# Executor will be initialized with an empty git repo that
|
||||
# we populate by pushing:
|
||||
# EXEC_HEAD - the base commit for this experiment
|
||||
@@ -130,6 +131,8 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
|
||||
self.scm.stash.apply(merge_rev)
|
||||
self._update_config(repo.config.read("local"))
|
||||
local_git_config = os.path.join(repo.scm.root_dir, ".git", "config")
|
||||
self._update_git_config(ConfigObj(local_git_config, list_values=False))
|
||||
|
||||
def _update_config(self, update):
|
||||
local_config = os.path.join(self.root_dir, self.dvc_dir, "config.local")
|
||||
@@ -143,6 +146,18 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
with open(local_config, "wb") as fobj:
|
||||
conf_obj.write(fobj)
|
||||
|
||||
def _update_git_config(self, update):
|
||||
local_config = os.path.join(self.scm.root_dir, ".git", "config")
|
||||
logger.debug("Writing experiments local Git config '%s'", local_config)
|
||||
if os.path.exists(local_config):
|
||||
conf_obj = ConfigObj(local_config, list_values=False)
|
||||
conf_obj.merge(update)
|
||||
else:
|
||||
conf_obj = ConfigObj(update, list_values=False)
|
||||
if conf_obj:
|
||||
with open(local_config, "wb") as fobj:
|
||||
conf_obj.write(fobj)
|
||||
|
||||
def init_cache(
|
||||
self,
|
||||
repo: "Repo",
|
||||
@@ -186,7 +201,7 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
@classmethod
|
||||
def from_stash_entry(cls, repo: "Repo", entry: "ExpStashEntry", **kwargs):
|
||||
root_dir = repo.scm.root_dir
|
||||
executor: "WorkspaceExecutor" = cls._from_stash_entry(
|
||||
executor: WorkspaceExecutor = cls._from_stash_entry(
|
||||
repo, entry, root_dir, **kwargs
|
||||
)
|
||||
logger.debug("Init workspace executor in '%s'", root_dir)
|
||||
|
||||
@@ -30,7 +30,7 @@ def pull( # noqa: C901
|
||||
pull_cache: bool = False,
|
||||
**kwargs,
|
||||
) -> Iterable[str]:
|
||||
exp_ref_set: set["ExpRefInfo"] = set()
|
||||
exp_ref_set: set[ExpRefInfo] = set()
|
||||
if all_commits:
|
||||
exp_ref_set.update(exp_refs(repo.scm, git_remote))
|
||||
elif exp_names:
|
||||
@@ -97,7 +97,7 @@ def _pull(
|
||||
def group_result(refspec):
|
||||
return results[str(refspec)]
|
||||
|
||||
pull_result: Mapping[SyncStatus, list["ExpRefInfo"]] = group_by(group_result, refs)
|
||||
pull_result: Mapping[SyncStatus, list[ExpRefInfo]] = group_by(group_result, refs)
|
||||
|
||||
return pull_result
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ def push(
|
||||
push_cache: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
exp_ref_set: set["ExpRefInfo"] = set()
|
||||
exp_ref_set: set[ExpRefInfo] = set()
|
||||
assert isinstance(repo.scm, Git)
|
||||
if all_commits:
|
||||
exp_ref_set.update(exp_refs(repo.scm))
|
||||
@@ -162,7 +162,7 @@ def _push(
|
||||
def group_result(refspec):
|
||||
return results[str(refspec)]
|
||||
|
||||
pull_result: Mapping[SyncStatus, list["ExpRefInfo"]] = group_by(group_result, refs)
|
||||
pull_result: Mapping[SyncStatus, list[ExpRefInfo]] = group_by(group_result, refs)
|
||||
|
||||
return pull_result
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from collections.abc import Collection, Generator, Iterable, Mapping
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Union
|
||||
|
||||
from dvc_studio_client.post_live_metrics import get_studio_config
|
||||
from funcy import retry
|
||||
|
||||
from dvc.dependency import ParamsDependency
|
||||
@@ -24,6 +23,7 @@ from dvc.repo.experiments.utils import (
|
||||
)
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc.utils.studio import config_to_env
|
||||
from dvc_studio_client.post_live_metrics import get_studio_config
|
||||
|
||||
from .utils import get_remote_executor_refs
|
||||
|
||||
@@ -180,7 +180,7 @@ class BaseStashQueue(ABC):
|
||||
|
||||
def _get_timestamp(rev: str) -> datetime:
|
||||
commit = self.scm.resolve_commit(rev)
|
||||
return datetime.fromtimestamp(commit.commit_time)
|
||||
return datetime.fromtimestamp(commit.commit_time) # noqa: DTZ006
|
||||
|
||||
def _format_entry(
|
||||
entry: QueueEntry,
|
||||
@@ -337,7 +337,7 @@ class BaseStashQueue(ABC):
|
||||
studio_config = get_studio_config(
|
||||
dvc_studio_config=self.repo.config.get("studio")
|
||||
)
|
||||
run_env = {**config_to_env(studio_config), **run_env}
|
||||
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(
|
||||
|
||||
@@ -470,14 +470,14 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
exp_names = [exp_names]
|
||||
results: dict[str, ExpRefAndQueueEntry] = {}
|
||||
|
||||
exp_ref_match: dict[str, Optional["ExpRefInfo"]] = resolve_name(
|
||||
exp_ref_match: dict[str, Optional[ExpRefInfo]] = resolve_name(
|
||||
self.scm, exp_names, git_remote
|
||||
)
|
||||
if not git_remote:
|
||||
queue_entry_match: dict[
|
||||
str, Optional["QueueEntry"]
|
||||
] = self.match_queue_entry_by_name(
|
||||
exp_names, self.iter_queued(), self.iter_done()
|
||||
queue_entry_match: dict[str, Optional[QueueEntry]] = (
|
||||
self.match_queue_entry_by_name(
|
||||
exp_names, self.iter_queued(), self.iter_done()
|
||||
)
|
||||
)
|
||||
|
||||
for exp_name in exp_names:
|
||||
@@ -613,7 +613,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
"""Map exp refs to any available successful executors."""
|
||||
from dvc.repo.experiments.serialize import ExpExecutor, LocalExpExecutor
|
||||
|
||||
result: dict[str, "ExpExecutor"] = {}
|
||||
result: dict[str, ExpExecutor] = {}
|
||||
for entry, exec_result in self.iter_success():
|
||||
if baseline_revs and entry.baseline_rev not in baseline_revs:
|
||||
continue
|
||||
|
||||
@@ -21,11 +21,11 @@ def remove_tasks( # noqa: C901, PLR0912
|
||||
"""
|
||||
from celery.result import AsyncResult
|
||||
|
||||
stash_revs: dict[str, "ExpStashEntry"] = {}
|
||||
failed_stash_revs: list["ExpStashEntry"] = []
|
||||
done_entry_set: set["QueueEntry"] = set()
|
||||
stash_revs: dict[str, ExpStashEntry] = {}
|
||||
failed_stash_revs: list[ExpStashEntry] = []
|
||||
done_entry_set: set[QueueEntry] = set()
|
||||
stash_rev_all = celery_queue.stash.stash_revs
|
||||
failed_rev_all: dict[str, "ExpStashEntry"] = {}
|
||||
failed_rev_all: dict[str, ExpStashEntry] = {}
|
||||
if celery_queue.failed_stash:
|
||||
failed_rev_all = celery_queue.failed_stash.stash_revs
|
||||
for entry in queue_entries:
|
||||
@@ -90,17 +90,17 @@ def celery_clear(
|
||||
"""
|
||||
|
||||
removed: list[str] = []
|
||||
entry_list: list["QueueEntry"] = []
|
||||
entry_list: list[QueueEntry] = []
|
||||
if queued:
|
||||
queue_entries: list["QueueEntry"] = list(self.iter_queued())
|
||||
queue_entries: list[QueueEntry] = list(self.iter_queued())
|
||||
entry_list.extend(queue_entries)
|
||||
removed.extend(_get_names(queue_entries))
|
||||
if failed:
|
||||
failed_tasks: list["QueueDoneResult"] = list(self.iter_failed())
|
||||
failed_tasks: list[QueueDoneResult] = list(self.iter_failed())
|
||||
entry_list.extend([result.entry for result in failed_tasks])
|
||||
removed.extend(_get_names(failed_tasks))
|
||||
if success:
|
||||
success_tasks: list["QueueDoneResult"] = list(self.iter_success())
|
||||
success_tasks: list[QueueDoneResult] = list(self.iter_success())
|
||||
entry_list.extend([result.entry for result in success_tasks])
|
||||
removed.extend(_get_names(success_tasks))
|
||||
|
||||
@@ -125,7 +125,7 @@ def celery_remove(self: "LocalCeleryQueue", revs: Collection[str]) -> list[str]:
|
||||
|
||||
remained: list[str] = []
|
||||
removed: list[str] = []
|
||||
entry_to_remove: list["QueueEntry"] = []
|
||||
entry_to_remove: list[QueueEntry] = []
|
||||
for name, entry in match_results.items():
|
||||
if entry:
|
||||
entry_to_remove.append(entry)
|
||||
|
||||
@@ -125,7 +125,7 @@ class TempDirQueue(WorkspaceQueue):
|
||||
)
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
raise DvcException(f"Failed to reproduce experiment '{rev[:7]}'") from exc
|
||||
finally:
|
||||
executor.cleanup(infofile)
|
||||
|
||||
@@ -131,7 +131,7 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
)
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
raise DvcException(f"Failed to reproduce experiment '{rev[:7]}'") from exc
|
||||
finally:
|
||||
executor.cleanup(infofile)
|
||||
|
||||
@@ -34,19 +34,19 @@ def remove( # noqa: C901, PLR0912
|
||||
removed: list[str] = []
|
||||
if not any([exp_names, queue, all_commits, rev]):
|
||||
return removed
|
||||
celery_queue: "LocalCeleryQueue" = repo.experiments.celery_queue
|
||||
celery_queue: LocalCeleryQueue = repo.experiments.celery_queue
|
||||
|
||||
if queue:
|
||||
removed.extend(celery_queue.clear(queued=True))
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
|
||||
exp_ref_list: list["ExpRefInfo"] = []
|
||||
queue_entry_list: list["QueueEntry"] = []
|
||||
exp_ref_list: list[ExpRefInfo] = []
|
||||
queue_entry_list: list[QueueEntry] = []
|
||||
if exp_names:
|
||||
results: dict[
|
||||
str, "ExpRefAndQueueEntry"
|
||||
] = celery_queue.get_ref_and_entry_by_names(exp_names, git_remote)
|
||||
results: dict[str, ExpRefAndQueueEntry] = (
|
||||
celery_queue.get_ref_and_entry_by_names(exp_names, git_remote)
|
||||
)
|
||||
remained: list[str] = []
|
||||
for name, result in results.items():
|
||||
if not result.exp_ref_info and not result.queue_entry:
|
||||
@@ -94,7 +94,7 @@ def _resolve_exp_by_baseline(
|
||||
) -> dict[str, "ExpRefInfo"]:
|
||||
assert isinstance(repo.scm, Git)
|
||||
|
||||
commit_ref_dict: dict[str, "ExpRefInfo"] = {}
|
||||
commit_ref_dict: dict[str, ExpRefInfo] = {}
|
||||
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)
|
||||
|
||||
@@ -50,7 +50,12 @@ class SerializableExp:
|
||||
Params, metrics, deps, outs are filled via repo fs/index, all other fields
|
||||
should be passed via kwargs.
|
||||
"""
|
||||
from dvc.dependency import ParamsDependency, RepoDependency
|
||||
from dvc.dependency import (
|
||||
DatasetDependency,
|
||||
DbDependency,
|
||||
ParamsDependency,
|
||||
RepoDependency,
|
||||
)
|
||||
|
||||
rev = rev or repo.get_rev()
|
||||
assert rev
|
||||
@@ -68,7 +73,10 @@ class SerializableExp:
|
||||
nfiles=dep.meta.nfiles if dep.meta else None,
|
||||
)
|
||||
for dep in repo.index.deps
|
||||
if not isinstance(dep, (ParamsDependency, RepoDependency))
|
||||
if not isinstance(
|
||||
dep,
|
||||
(ParamsDependency, RepoDependency, DatasetDependency, DbDependency),
|
||||
)
|
||||
},
|
||||
outs={
|
||||
relpath(out.fs_path, repo.root_dir): ExpOut(
|
||||
|
||||
@@ -77,7 +77,7 @@ def tabulate(
|
||||
"State",
|
||||
"Executor",
|
||||
]
|
||||
names = {**metrics_names, **params_names}
|
||||
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)
|
||||
@@ -117,7 +117,7 @@ def _build_rows(
|
||||
**kwargs,
|
||||
) -> Iterator[tuple["CellT", ...]]:
|
||||
for baseline in baseline_states:
|
||||
row: dict[str, "CellT"] = dict.fromkeys(all_headers, fill_value)
|
||||
row: dict[str, CellT] = dict.fromkeys(all_headers, fill_value)
|
||||
row["Experiment"] = ""
|
||||
if baseline.name:
|
||||
row["rev"] = baseline.name
|
||||
@@ -232,7 +232,7 @@ def _exp_range_rows(
|
||||
logger.debug("Returning tip commit for legacy checkpoint exp")
|
||||
exp = first(exp_range.revs)
|
||||
if exp:
|
||||
row: dict[str, "CellT"] = dict.fromkeys(all_headers, fill_value)
|
||||
row: dict[str, CellT] = dict.fromkeys(all_headers, fill_value)
|
||||
row["Experiment"] = exp.name or ""
|
||||
row["rev"] = exp.rev[:7] if Git.is_sha(exp.rev) else exp.rev
|
||||
row["typ"] = "branch_base" if is_base else "branch_commit"
|
||||
@@ -306,7 +306,7 @@ def format_time(
|
||||
return fill_value
|
||||
if iso:
|
||||
return timestamp.isoformat()
|
||||
if timestamp.date() == date.today():
|
||||
if timestamp.date() == date.today(): # noqa: DTZ011
|
||||
fmt = "%I:%M %p"
|
||||
else:
|
||||
fmt = "%b %d, %Y"
|
||||
|
||||
@@ -199,7 +199,7 @@ def exp_commits(
|
||||
scm: "Git", ref_infos: Optional[Iterable[ExpRefInfo]] = None
|
||||
) -> Iterable[str]:
|
||||
"""Iterate over all experiment commits."""
|
||||
shas: set["str"] = set()
|
||||
shas: set[str] = set()
|
||||
refs = ref_infos if ref_infos else exp_refs(scm)
|
||||
for ref_info in refs:
|
||||
shas.update(scm.branch_revs(str(ref_info), ref_info.baseline_sha))
|
||||
@@ -287,7 +287,7 @@ def gen_random_name():
|
||||
# fmt: on
|
||||
# Use custom random generator to make sure that names are random even if
|
||||
# global random seed is set (common for ML pipelines).
|
||||
random_generator = random.Random()
|
||||
random_generator = random.Random() # noqa: S311
|
||||
adjective = random_generator.choice(ADJECTIVES)
|
||||
noun = random_generator.choice(NOUNS)
|
||||
return f"{adjective}-{noun}"
|
||||
|
||||
+6
-5
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.exceptions import DownloadError
|
||||
from dvc.log import logger
|
||||
from dvc.stage.cache import RunCacheNotSupported
|
||||
from dvc.ui import ui
|
||||
from dvc_data.index import DataIndex, FileStorage
|
||||
|
||||
@@ -49,14 +50,12 @@ def _collect_indexes( # noqa: PLR0913
|
||||
config["core"] = core
|
||||
|
||||
def stage_filter(stage: "Stage") -> bool:
|
||||
if push and stage.is_repo_import:
|
||||
return False
|
||||
return True
|
||||
return not (push and stage.is_repo_import)
|
||||
|
||||
def outs_filter(out: "Output") -> bool:
|
||||
if push and not out.can_push:
|
||||
return False
|
||||
return True
|
||||
return not (remote and out.remote and remote != out.remote)
|
||||
|
||||
for rev in repo.brancher(
|
||||
revs=revs,
|
||||
@@ -138,6 +137,8 @@ def fetch( # noqa: PLR0913
|
||||
try:
|
||||
if run_cache:
|
||||
self.stage_cache.pull(remote)
|
||||
except RunCacheNotSupported as e:
|
||||
logger.debug("failed to pull run cache: %s", e)
|
||||
except DownloadError as exc:
|
||||
failed_count += exc.amount
|
||||
|
||||
@@ -202,7 +203,7 @@ def fetch( # noqa: PLR0913
|
||||
|
||||
|
||||
def _log_unversioned(data: list["DataIndex"]) -> tuple[list["DataIndex"], int]:
|
||||
ret: list["DataIndex"] = []
|
||||
ret: list[DataIndex] = []
|
||||
unversioned: list[str] = []
|
||||
for fs_index in data:
|
||||
remote = fs_index.storage_map[()].remote
|
||||
|
||||
+2
-2
@@ -50,7 +50,7 @@ def _used_obj_ids_not_in_remote(
|
||||
|
||||
|
||||
@locked
|
||||
def gc( # noqa: PLR0913, C901
|
||||
def gc( # noqa: C901, PLR0912, PLR0913
|
||||
self: "Repo",
|
||||
all_branches: bool = False,
|
||||
cloud: bool = False,
|
||||
@@ -96,7 +96,7 @@ def gc( # noqa: PLR0913, C901
|
||||
repos = []
|
||||
all_repos = [Repo(path) for path in repos]
|
||||
|
||||
odb_to_obj_ids: "ObjectContainer" = {}
|
||||
odb_to_obj_ids: ObjectContainer = {}
|
||||
with ExitStack() as stack:
|
||||
for repo in all_repos:
|
||||
stack.enter_context(repo.lock)
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ def get(
|
||||
from dvc.fs import download
|
||||
from dvc.fs.data import DataFileSystem
|
||||
|
||||
fs: Union[DataFileSystem, "DVCFileSystem"]
|
||||
fs: Union[DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=repo.index.data["local"])
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ def check_acyclic(graph: "DiGraph") -> None:
|
||||
except nx.NetworkXNoCycle:
|
||||
return
|
||||
|
||||
stages: set["Stage"] = set()
|
||||
stages: set[Stage] = set()
|
||||
for from_node, to_node, _ in edges:
|
||||
stages.add(from_node)
|
||||
stages.add(to_node)
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import os
|
||||
from functools import partial
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from dvc.log import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.dependency.base import Dependency
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.index import Index, IndexView
|
||||
from dvc.stage import Stage
|
||||
from dvc.types import TargetType
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
def unfetched_view(
|
||||
index: "Index", targets: "TargetType", unpartial: bool = False, **kwargs
|
||||
) -> tuple["IndexView", "IndexView", list["Dependency"]]:
|
||||
"""Return index view of imports which have not been fetched.
|
||||
|
||||
Returns:
|
||||
Tuple in the form (legacy_view, view, changed_deps) where changed_imports is a
|
||||
list of import dependencies that cannot be fetched due to changed data source.
|
||||
"""
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
changed_deps: list["Dependency"] = []
|
||||
|
||||
def need_fetch(stage: "Stage", legacy: bool = False) -> bool:
|
||||
if not stage.is_import or (stage.is_partial_import and not unpartial):
|
||||
return False
|
||||
|
||||
out = stage.outs[0]
|
||||
if not out.changed_cache():
|
||||
return False
|
||||
|
||||
dep = stage.deps[0]
|
||||
if dep.changed_checksum():
|
||||
changed_deps.append(dep)
|
||||
return False
|
||||
|
||||
if out.hash_name in LEGACY_HASH_NAMES and legacy:
|
||||
return True
|
||||
if out.hash_name not in LEGACY_HASH_NAMES and not legacy:
|
||||
return True
|
||||
return False
|
||||
|
||||
legacy_unfetched = index.targets_view(
|
||||
targets,
|
||||
stage_filter=partial(need_fetch, legacy=True),
|
||||
**kwargs,
|
||||
)
|
||||
unfetched = index.targets_view(targets, stage_filter=need_fetch, **kwargs)
|
||||
return legacy_unfetched, unfetched, changed_deps
|
||||
|
||||
|
||||
def partial_view(index: "Index", targets: "TargetType", **kwargs) -> "IndexView":
|
||||
return index.targets_view(
|
||||
targets,
|
||||
stage_filter=lambda s: s.is_partial_import,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def unpartial_imports(index: Union["Index", "IndexView"]) -> int:
|
||||
"""Update any outs in the index which are no longer partial imports.
|
||||
|
||||
Returns:
|
||||
Total number of files which were unpartialed.
|
||||
"""
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
|
||||
updated = 0
|
||||
for out in index.outs:
|
||||
# we need to use view[key] here and since the out fields have not been
|
||||
# updated yet (out.get_entry() would return the partial-import state)
|
||||
workspace, key = out.index_key
|
||||
entry = index.data[workspace][key]
|
||||
if out.stage.is_partial_import:
|
||||
out.hash_info = entry.hash_info or HashInfo()
|
||||
out.meta = entry.meta or Meta()
|
||||
out.stage.md5 = out.stage.compute_md5()
|
||||
out.stage.dump()
|
||||
updated += out.meta.nfiles if out.meta.nfiles is not None else 1
|
||||
return updated
|
||||
|
||||
|
||||
def save_imports(
|
||||
repo: "Repo", targets: "TargetType", unpartial: bool = False, **kwargs
|
||||
) -> set["HashInfo"]:
|
||||
"""Save (download) imports from their original source location.
|
||||
|
||||
Imports which are already cached will not be downloaded.
|
||||
|
||||
Returns:
|
||||
Objects which were downloaded from source location.
|
||||
"""
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
from dvc.stage.exceptions import DataSourceChanged
|
||||
from dvc_data.index import md5, save
|
||||
from dvc_data.index.checkout import apply, compare
|
||||
|
||||
downloaded: set["HashInfo"] = set()
|
||||
|
||||
legacy_unfetched, unfetched, changed = unfetched_view(
|
||||
repo.index, targets, unpartial=unpartial, **kwargs
|
||||
)
|
||||
for dep in changed:
|
||||
logger.warning(str(DataSourceChanged(f"{dep.stage} ({dep})")))
|
||||
|
||||
for view, cache in [
|
||||
(legacy_unfetched, repo.cache.legacy),
|
||||
(unfetched, repo.cache.local),
|
||||
]:
|
||||
data_view = view.data["repo"]
|
||||
if len(data_view):
|
||||
if not cache.fs.exists(cache.path):
|
||||
os.makedirs(cache.path)
|
||||
with TemporaryDirectory(dir=cache.path) as tmpdir:
|
||||
with TqdmCallback(
|
||||
desc="Downloading imports from source",
|
||||
unit="files",
|
||||
) as cb:
|
||||
diff = compare(None, data_view)
|
||||
apply(diff, tmpdir, cache.fs, callback=cb, storage="remote")
|
||||
md5(data_view, name=cache.hash_name)
|
||||
save(data_view, odb=cache, hardlink=True)
|
||||
|
||||
downloaded.update(
|
||||
entry.hash_info
|
||||
for _, entry in data_view.iteritems()
|
||||
if entry.meta is not None
|
||||
and not entry.meta.isdir
|
||||
and entry.hash_info is not None
|
||||
)
|
||||
|
||||
if unpartial:
|
||||
unpartial_imports(partial_view(repo.index, targets, **kwargs))
|
||||
|
||||
return downloaded
|
||||
+38
-27
@@ -104,7 +104,7 @@ def collect_files(
|
||||
dirs[:] = [d for d in dirs if not is_out_or_ignored(root, d)]
|
||||
|
||||
|
||||
def _load_data_from_tree(index, prefix, ws, key, tree):
|
||||
def _load_data_from_tree(index, prefix, ws, key, tree, hash_name):
|
||||
from dvc_data.index import DataIndexEntry, Meta
|
||||
|
||||
parents = set()
|
||||
@@ -117,7 +117,7 @@ def _load_data_from_tree(index, prefix, ws, key, tree):
|
||||
index[(*prefix, ws, *fkey)] = DataIndexEntry(
|
||||
key=fkey,
|
||||
meta=ometa,
|
||||
hash_info=ohi,
|
||||
hash_info=ohi if (ohi and ohi.name == hash_name) else None,
|
||||
)
|
||||
|
||||
for parent in parents:
|
||||
@@ -151,7 +151,7 @@ def _load_data_from_outs(index, prefix, outs):
|
||||
tree = out.get_obj()
|
||||
|
||||
if tree is not None:
|
||||
_load_data_from_tree(index, prefix, ws, key, tree)
|
||||
_load_data_from_tree(index, prefix, ws, key, tree, out.hash_name)
|
||||
|
||||
entry = DataIndexEntry(
|
||||
key=key,
|
||||
@@ -193,16 +193,22 @@ def _load_storage_from_import(storage_map, key, out):
|
||||
return
|
||||
|
||||
dep = out.stage.deps[0]
|
||||
if not out.hash_info and (
|
||||
not dep.hash_info or dep.hash_info.name != storage_map[key].cache.odb.hash_name
|
||||
):
|
||||
# partial import
|
||||
if not out.hash_info or dep.fs.version_aware:
|
||||
if dep.meta and dep.meta.isdir:
|
||||
meta_token = dep.hash_info.value
|
||||
else:
|
||||
meta_token = tokenize(dep.meta.to_dict())
|
||||
|
||||
fs_cache = out.repo.cache.fs_cache
|
||||
storage_map.add_cache(
|
||||
FileStorage(
|
||||
key,
|
||||
fs_cache.fs,
|
||||
fs_cache.fs.join(fs_cache.path, dep.fs.protocol, tokenize(dep.fs_path)),
|
||||
fs_cache.fs.join(
|
||||
fs_cache.path,
|
||||
dep.fs.protocol,
|
||||
tokenize(dep.fs_path, meta_token),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -215,7 +221,9 @@ def _load_storage_from_out(storage_map, key, out):
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc_data.index import FileStorage, ObjectStorage
|
||||
|
||||
storage_map.add_cache(ObjectStorage(key, out.cache))
|
||||
if out.cache:
|
||||
storage_map.add_cache(ObjectStorage(key, out.cache))
|
||||
|
||||
try:
|
||||
remote = out.repo.cloud.get_remote(out.remote)
|
||||
if remote.fs.version_aware:
|
||||
@@ -282,6 +290,7 @@ class Index:
|
||||
params: Optional[dict[str, Any]] = None,
|
||||
artifacts: Optional[dict[str, Any]] = None,
|
||||
datasets: Optional[dict[str, list[dict[str, Any]]]] = None,
|
||||
datasets_lock: Optional[dict[str, list[dict[str, Any]]]] = None,
|
||||
) -> None:
|
||||
self.repo = repo
|
||||
self.stages = stages or []
|
||||
@@ -290,7 +299,8 @@ class Index:
|
||||
self._params = params or {}
|
||||
self._artifacts = artifacts or {}
|
||||
self._datasets: dict[str, list[dict[str, Any]]] = datasets or {}
|
||||
self._collected_targets: dict[int, list["StageInfo"]] = {}
|
||||
self._datasets_lock: dict[str, list[dict[str, Any]]] = datasets_lock or {}
|
||||
self._collected_targets: dict[int, list[StageInfo]] = {}
|
||||
|
||||
@cached_property
|
||||
def rev(self) -> Optional[str]:
|
||||
@@ -314,6 +324,7 @@ class Index:
|
||||
params = {}
|
||||
artifacts = {}
|
||||
datasets = {}
|
||||
datasets_lock = {}
|
||||
|
||||
onerror = onerror or repo.stage_collection_error_handler
|
||||
for _, idx in collect_files(repo, onerror=onerror):
|
||||
@@ -323,6 +334,7 @@ class Index:
|
||||
params.update(idx._params)
|
||||
artifacts.update(idx._artifacts)
|
||||
datasets.update(idx._datasets)
|
||||
datasets_lock.update(idx._datasets_lock)
|
||||
return cls(
|
||||
repo,
|
||||
stages=stages,
|
||||
@@ -331,6 +343,7 @@ class Index:
|
||||
params=params,
|
||||
artifacts=artifacts,
|
||||
datasets=datasets,
|
||||
datasets_lock=datasets_lock,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -346,6 +359,9 @@ class Index:
|
||||
params={path: dvcfile.params} if dvcfile.params else {},
|
||||
artifacts={path: dvcfile.artifacts} if dvcfile.artifacts else {},
|
||||
datasets={path: dvcfile.datasets} if dvcfile.datasets else {},
|
||||
datasets_lock={path: dvcfile.datasets_lock}
|
||||
if dvcfile.datasets_lock
|
||||
else {},
|
||||
)
|
||||
|
||||
def update(self, stages: Iterable["Stage"]) -> "Index":
|
||||
@@ -398,14 +414,9 @@ class Index:
|
||||
for stage in self.stages:
|
||||
yield from stage.outs
|
||||
|
||||
@cached_property
|
||||
def datasets(self) -> dict[str, dict[str, Any]]:
|
||||
datasets = chain.from_iterable(self._datasets.values())
|
||||
return {dataset["name"]: dataset for dataset in datasets}
|
||||
|
||||
@cached_property
|
||||
def out_data_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
by_workspace["local"] = set()
|
||||
@@ -459,7 +470,7 @@ class Index:
|
||||
|
||||
@cached_property
|
||||
def data_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
by_workspace["local"] = set()
|
||||
@@ -477,7 +488,7 @@ class Index:
|
||||
def metric_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
from .metrics.show import _collect_top_level_metrics
|
||||
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
|
||||
@@ -498,7 +509,7 @@ class Index:
|
||||
def param_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
from .params.show import _collect_top_level_params
|
||||
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
by_workspace["repo"] = set()
|
||||
|
||||
param_paths = _collect_top_level_params(self.repo)
|
||||
@@ -514,7 +525,7 @@ class Index:
|
||||
|
||||
@cached_property
|
||||
def plot_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
|
||||
@@ -537,7 +548,7 @@ class Index:
|
||||
|
||||
@cached_property
|
||||
def data(self) -> "dict[str, DataIndex]":
|
||||
prefix: "DataIndexKey"
|
||||
prefix: DataIndexKey
|
||||
loaded = False
|
||||
|
||||
index = self.repo.data_index
|
||||
@@ -589,7 +600,7 @@ class Index:
|
||||
if not onerror:
|
||||
|
||||
def onerror(_target, _exc):
|
||||
raise
|
||||
raise # noqa: PLE0704
|
||||
|
||||
targets = ensure_list(targets)
|
||||
if not targets:
|
||||
@@ -616,7 +627,7 @@ class Index:
|
||||
jobs: Optional[int] = None,
|
||||
push: bool = False,
|
||||
) -> "ObjectContainer":
|
||||
used: "ObjectContainer" = defaultdict(set)
|
||||
used: ObjectContainer = defaultdict(set)
|
||||
pairs = self.collect_targets(targets, recursive=recursive, with_deps=with_deps)
|
||||
for stage, filter_info in pairs:
|
||||
for odb, objs in stage.get_used_objs(
|
||||
@@ -737,7 +748,7 @@ class IndexView:
|
||||
|
||||
@cached_property
|
||||
def out_data_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
by_workspace["repo"] = set()
|
||||
by_workspace["local"] = set()
|
||||
@@ -753,7 +764,7 @@ class IndexView:
|
||||
|
||||
@cached_property
|
||||
def _data_prefixes(self) -> dict[str, "_DataPrefixes"]:
|
||||
prefixes: dict[str, "_DataPrefixes"] = defaultdict(
|
||||
prefixes: dict[str, _DataPrefixes] = defaultdict(
|
||||
lambda: _DataPrefixes(set(), set())
|
||||
)
|
||||
for out, filter_info in self._filtered_outs:
|
||||
@@ -770,7 +781,7 @@ class IndexView:
|
||||
|
||||
@cached_property
|
||||
def data_keys(self) -> dict[str, set["DataIndexKey"]]:
|
||||
ret: dict[str, set["DataIndexKey"]] = defaultdict(set)
|
||||
ret: dict[str, set[DataIndexKey]] = defaultdict(set)
|
||||
|
||||
for out, filter_info in self._filtered_outs:
|
||||
if not out.use_cache:
|
||||
@@ -800,7 +811,7 @@ class IndexView:
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
data: dict[str, Union["DataIndex", "DataIndexView"]] = {}
|
||||
data: dict[str, Union[DataIndex, DataIndexView]] = {}
|
||||
for workspace, data_index in self._index.data.items():
|
||||
if self.stages:
|
||||
data[workspace] = view(data_index, partial(key_filter, workspace))
|
||||
|
||||
+18
-18
@@ -77,30 +77,30 @@ def _ls(
|
||||
recursive: Optional[bool] = None,
|
||||
dvc_only: bool = False,
|
||||
):
|
||||
fs: "DVCFileSystem" = repo.dvcfs
|
||||
fs: DVCFileSystem = repo.dvcfs
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
fs_path = fs.info(fs_path)["name"]
|
||||
|
||||
infos = {}
|
||||
for root, dirs, files in fs.walk(
|
||||
fs_path, dvcfiles=True, dvc_only=dvc_only, detail=True
|
||||
):
|
||||
if not recursive:
|
||||
files.update(dirs)
|
||||
|
||||
parts = fs.relparts(root, fs_path)
|
||||
if parts == (".",):
|
||||
parts = ()
|
||||
|
||||
for name, entry in files.items():
|
||||
infos[os.path.join(*parts, name)] = entry
|
||||
|
||||
if not recursive:
|
||||
break
|
||||
|
||||
if not infos and fs.isfile(fs_path):
|
||||
if fs.isfile(fs_path):
|
||||
infos[os.path.basename(path)] = fs.info(fs_path)
|
||||
else:
|
||||
for root, dirs, files in fs.walk(
|
||||
fs_path, dvcfiles=True, dvc_only=dvc_only, detail=True
|
||||
):
|
||||
if not recursive:
|
||||
files.update(dirs)
|
||||
|
||||
parts = fs.relparts(root, fs_path)
|
||||
if parts == (".",):
|
||||
parts = ()
|
||||
|
||||
for name, entry in files.items():
|
||||
infos[os.path.join(*parts, name)] = entry
|
||||
|
||||
if not recursive:
|
||||
break
|
||||
|
||||
ret = {}
|
||||
for name, info in infos.items():
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Iterable, Iterator
|
||||
from itertools import chain
|
||||
@@ -8,7 +9,7 @@ from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.log import logger
|
||||
from dvc.scm import NoSCMError
|
||||
from dvc.utils import as_posix, expand_paths
|
||||
from dvc.utils import as_posix
|
||||
from dvc.utils.collections import ensure_list
|
||||
from dvc.utils.serialize import load_path
|
||||
|
||||
@@ -103,7 +104,7 @@ def _collect_metrics(
|
||||
paths = (fs.from_os_path(metric) for metric in metrics)
|
||||
# make paths absolute for DVCFileSystem
|
||||
repo_paths = (f"{fs.root_marker}{path}" for path in paths)
|
||||
return ldistinct(expand_paths(fs, repo_paths))
|
||||
return ldistinct(try_expand_paths(fs, repo_paths))
|
||||
|
||||
|
||||
class FileResult(TypedDict, total=False):
|
||||
@@ -116,6 +117,22 @@ class Result(TypedDict, total=False):
|
||||
error: Exception
|
||||
|
||||
|
||||
def try_expand_paths(fs: "FileSystem", paths: Iterable[str]) -> Iterator[str]:
|
||||
for path in paths:
|
||||
try:
|
||||
if fs.isdir(path):
|
||||
yield from fs.find(path)
|
||||
continue
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.debug(
|
||||
"failed to expand %r: %s",
|
||||
path,
|
||||
e,
|
||||
exc_info=logger.isEnabledFor(logging.TRACE), # type: ignore[attr-defined]
|
||||
)
|
||||
yield path
|
||||
|
||||
|
||||
def to_relpath(fs: "FileSystem", root_dir: str, d: Result) -> Result:
|
||||
relpath = fs.relpath
|
||||
cwd = fs.getcwd()
|
||||
@@ -207,7 +224,7 @@ def show(
|
||||
on_error=on_error,
|
||||
)
|
||||
res[rev] = Result(data=result)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
if on_error == "raise":
|
||||
raise
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ def _cached_clone(url, rev):
|
||||
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _clone_default_branch(url, rev): # noqa: PLR0912
|
||||
def _clone_default_branch(url, rev):
|
||||
"""Get or create a clean clone of the url.
|
||||
|
||||
The cloned is reactualized with git pull unless rev is a known sha.
|
||||
|
||||
@@ -6,9 +6,9 @@ from typing import TYPE_CHECKING, Any, Optional, Union
|
||||
|
||||
from dvc.dependency.param import ParamsDependency, read_param_file
|
||||
from dvc.log import logger
|
||||
from dvc.repo.metrics.show import FileResult, Result
|
||||
from dvc.repo.metrics.show import FileResult, Result, try_expand_paths
|
||||
from dvc.stage import PipelineStage
|
||||
from dvc.utils import as_posix, expand_paths
|
||||
from dvc.utils import as_posix
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -77,7 +77,7 @@ def _collect_params(
|
||||
path = fs.from_os_path(param)
|
||||
# make paths absolute for DVCFileSystem
|
||||
repo_path = f"{fs.root_marker}{path}"
|
||||
ret.update(dict.fromkeys(expand_paths(fs, [repo_path]), _params))
|
||||
ret.update(dict.fromkeys(try_expand_paths(fs, [repo_path]), _params))
|
||||
return ret
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ def show(
|
||||
on_error=on_error,
|
||||
)
|
||||
res[rev] = Result(data=params)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
if on_error == "raise":
|
||||
raise
|
||||
logger.warning("failed to load params in revision %r, %s", rev, str(exc))
|
||||
|
||||
+38
-14
@@ -171,7 +171,7 @@ class Plots:
|
||||
plots = _collect_plots(repo, targets, recursive)
|
||||
res: dict[str, Any] = {}
|
||||
for fs_path, rev_props in plots.items():
|
||||
joined_props = {**rev_props, **props}
|
||||
joined_props = rev_props | props
|
||||
res[fs_path] = {"props": joined_props}
|
||||
res[fs_path].update(
|
||||
{
|
||||
@@ -369,11 +369,9 @@ def _matches(targets, config_file, plot_id):
|
||||
return True
|
||||
|
||||
full_id = get_plot_id(plot_id, config_file)
|
||||
if any(
|
||||
return any(
|
||||
(re.match(target, plot_id) or re.match(target, full_id)) for target in targets
|
||||
):
|
||||
return True
|
||||
return False
|
||||
)
|
||||
|
||||
|
||||
def _normpath(path):
|
||||
@@ -405,7 +403,7 @@ def _collect_output_plots(repo, targets, props, onerror: Optional[Callable] = No
|
||||
unpacked = unpack_if_dir(
|
||||
fs,
|
||||
_normpath(fs.join(wdir_relpath, plot.def_path)),
|
||||
props={**plot_props, **props},
|
||||
props=plot_props | props,
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
@@ -423,11 +421,17 @@ def _id_is_path(plot_props=None):
|
||||
|
||||
def _adjust_sources(fs, plot_props, config_dir):
|
||||
new_plot_props = deepcopy(plot_props)
|
||||
old_y = new_plot_props.pop("y", {})
|
||||
new_y = {}
|
||||
for filepath, val in old_y.items():
|
||||
new_y[_normpath(fs.join(config_dir, filepath))] = val
|
||||
new_plot_props["y"] = new_y
|
||||
for axis in ["x", "y"]:
|
||||
x_is_inferred = axis == "x" and (
|
||||
axis not in new_plot_props or isinstance(new_plot_props[axis], str)
|
||||
)
|
||||
if x_is_inferred:
|
||||
continue
|
||||
old = new_plot_props.pop(axis, {})
|
||||
new = {}
|
||||
for filepath, val in old.items():
|
||||
new[_normpath(fs.join(config_dir, filepath))] = val
|
||||
new_plot_props[axis] = new
|
||||
return new_plot_props
|
||||
|
||||
|
||||
@@ -442,6 +446,10 @@ def _resolve_definitions(
|
||||
config_path = os.fspath(config_path)
|
||||
config_dir = fs.dirname(config_path)
|
||||
result: dict[str, dict] = {}
|
||||
|
||||
plot_ids_parents = [
|
||||
_normpath(fs.join(config_dir, plot_id)) for plot_id in definitions
|
||||
]
|
||||
for plot_id, plot_props in definitions.items():
|
||||
if plot_props is None:
|
||||
plot_props = {}
|
||||
@@ -449,16 +457,32 @@ def _resolve_definitions(
|
||||
data_path = _normpath(fs.join(config_dir, plot_id))
|
||||
if _matches(targets, config_path, plot_id):
|
||||
unpacked = unpack_if_dir(
|
||||
fs, data_path, props={**plot_props, **props}, onerror=onerror
|
||||
fs, data_path, props=plot_props | props, onerror=onerror
|
||||
)
|
||||
# use config for parent directory with most specific definition
|
||||
if unpacked.get("data"):
|
||||
unpacked["data"] = {
|
||||
k: v
|
||||
for k, v in unpacked["data"].items()
|
||||
if _closest_parent(fs, k, plot_ids_parents) == data_path
|
||||
}
|
||||
dpath.merge(result, unpacked)
|
||||
elif _matches(targets, config_path, plot_id):
|
||||
adjusted_props = _adjust_sources(fs, plot_props, config_dir)
|
||||
dpath.merge(result, {"data": {plot_id: {**adjusted_props, **props}}})
|
||||
dpath.merge(result, {"data": {plot_id: adjusted_props | props}})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _closest_parent(fs, path, parents):
|
||||
best_result = ""
|
||||
for parent in parents:
|
||||
common_path = fs.commonpath([path, parent])
|
||||
if len(common_path) > len(best_result):
|
||||
best_result = common_path
|
||||
return best_result
|
||||
|
||||
|
||||
def _collect_pipeline_files(repo, targets: list[str], props, onerror=None):
|
||||
result: dict[str, dict] = {}
|
||||
top_plots = repo.index._plots
|
||||
@@ -513,7 +537,7 @@ def unpack_if_dir(fs, path, props: dict[str, str], onerror: Optional[Callable] =
|
||||
|
||||
if "data" in unpacked:
|
||||
for subpath in unpacked["data"]:
|
||||
result["data"].update({subpath: props})
|
||||
result["data"].update({subpath: props.copy()})
|
||||
else:
|
||||
result.update(unpacked)
|
||||
|
||||
|
||||
+9
-2
@@ -1,10 +1,14 @@
|
||||
from contextlib import suppress
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError, UploadError
|
||||
from dvc.log import logger
|
||||
from dvc.stage.cache import RunCacheNotSupported
|
||||
from dvc.ui import ui
|
||||
|
||||
from . import locked
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
def _rebuild(idx, path, fs, cb):
|
||||
from dvc_data.index import DataIndex, DataIndexEntry, Meta
|
||||
@@ -95,8 +99,11 @@ def push( # noqa: PLR0913
|
||||
"Multiple rev push is unsupported for cloud versioned remotes"
|
||||
)
|
||||
|
||||
used_run_cache = self.stage_cache.push(remote) if run_cache else []
|
||||
transferred_count += len(used_run_cache)
|
||||
try:
|
||||
used_run_cache = self.stage_cache.push(remote) if run_cache else []
|
||||
transferred_count += len(used_run_cache)
|
||||
except RunCacheNotSupported as e:
|
||||
logger.debug("failed to push run cache: %s", e)
|
||||
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
|
||||
@@ -29,7 +29,7 @@ def collect_stages(
|
||||
recursive: bool = False,
|
||||
glob: bool = False,
|
||||
) -> list["Stage"]:
|
||||
stages: list["Stage"] = []
|
||||
stages: list[Stage] = []
|
||||
for target in targets:
|
||||
stages.extend(repo.stage.collect(target, recursive=recursive, glob=glob))
|
||||
return ldistinct(stages)
|
||||
@@ -166,10 +166,10 @@ def _reproduce(
|
||||
) -> list["Stage"]:
|
||||
assert on_error in ("fail", "keep-going", "ignore")
|
||||
|
||||
result: list["Stage"] = []
|
||||
failed: list["Stage"] = []
|
||||
to_skip: dict["Stage", "Stage"] = {}
|
||||
ret: Optional["Stage"] = None
|
||||
result: list[Stage] = []
|
||||
failed: list[Stage] = []
|
||||
to_skip: dict[Stage, Stage] = {}
|
||||
ret: Optional[Stage] = None
|
||||
|
||||
force_state = dict.fromkeys(stages, force)
|
||||
|
||||
@@ -228,7 +228,7 @@ def reproduce(
|
||||
if not kwargs.get("interactive", False):
|
||||
kwargs["interactive"] = self.config["core"].get("interactive", False)
|
||||
|
||||
stages: list["Stage"] = []
|
||||
stages: list[Stage] = []
|
||||
if not all_pipelines:
|
||||
targets_list = ensure_list(targets or PROJECT_FILE)
|
||||
stages = collect_stages(self, targets_list, recursive=recursive, glob=glob)
|
||||
|
||||
@@ -21,7 +21,7 @@ class SCMContext:
|
||||
def __init__(self, scm: "Base", config: Optional[dict[str, Any]] = None) -> None:
|
||||
from funcy import get_in
|
||||
|
||||
self.scm: "Base" = scm
|
||||
self.scm: Base = scm
|
||||
self.autostage: bool = get_in(
|
||||
config or {}, ["core", "autostage"], default=False
|
||||
)
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ def locked(f):
|
||||
|
||||
class StageLoad:
|
||||
def __init__(self, repo: "Repo") -> None:
|
||||
self.repo: "Repo" = repo
|
||||
self.repo: Repo = repo
|
||||
|
||||
@property
|
||||
def fs(self):
|
||||
|
||||
+17
-6
@@ -8,7 +8,7 @@ from . import locked
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
def _joint_status(pairs):
|
||||
def _joint_status(pairs, check_updates=True):
|
||||
status_info = {}
|
||||
|
||||
for stage, filter_info in pairs:
|
||||
@@ -20,19 +20,23 @@ def _joint_status(pairs):
|
||||
),
|
||||
stage,
|
||||
)
|
||||
status_info.update(stage.status(check_updates=True, filter_info=filter_info))
|
||||
status_info.update(
|
||||
stage.status(check_updates=check_updates, filter_info=filter_info)
|
||||
)
|
||||
|
||||
return status_info
|
||||
|
||||
|
||||
def _local_status(self, targets=None, with_deps=False, recursive=False):
|
||||
def _local_status(
|
||||
self, targets=None, with_deps=False, recursive=False, check_updates=True
|
||||
):
|
||||
targets = targets or [None]
|
||||
pairs = chain.from_iterable(
|
||||
self.stage.collect_granular(t, with_deps=with_deps, recursive=recursive)
|
||||
for t in targets
|
||||
)
|
||||
|
||||
return _joint_status(pairs)
|
||||
return _joint_status(pairs, check_updates=check_updates)
|
||||
|
||||
|
||||
def _cloud_status(
|
||||
@@ -100,7 +104,7 @@ def _cloud_status(
|
||||
|
||||
|
||||
@locked
|
||||
def status(
|
||||
def status( # noqa: PLR0913
|
||||
self,
|
||||
targets=None,
|
||||
jobs=None,
|
||||
@@ -111,6 +115,7 @@ def status(
|
||||
all_tags=False,
|
||||
all_commits=False,
|
||||
recursive=False,
|
||||
check_updates=True,
|
||||
):
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
@@ -138,4 +143,10 @@ def status(
|
||||
msg = "The following options are meaningless for local status: {}"
|
||||
raise InvalidArgumentError(msg.format(", ".join(ignored)))
|
||||
|
||||
return _local_status(self, targets, with_deps=with_deps, recursive=recursive)
|
||||
return _local_status(
|
||||
self,
|
||||
targets,
|
||||
with_deps=with_deps,
|
||||
recursive=recursive,
|
||||
check_updates=check_updates,
|
||||
)
|
||||
|
||||
+2
-6
@@ -25,15 +25,11 @@ def build_outs_trie(stages):
|
||||
overlapping = out
|
||||
if parent and overlapping:
|
||||
msg = (
|
||||
"The output paths:\n'{}'('{}')\n'{}'('{}')\n"
|
||||
f"The output paths:\n'{parent!s}'('{parent.stage.addressing}')\n"
|
||||
f"'{overlapping!s}'('{overlapping.stage.addressing}')\n"
|
||||
"overlap and are thus in the same tracked directory.\n"
|
||||
"To keep reproducibility, outputs should be in separate "
|
||||
"tracked directories or tracked individually."
|
||||
).format(
|
||||
str(parent),
|
||||
parent.stage.addressing,
|
||||
str(overlapping),
|
||||
overlapping.stage.addressing,
|
||||
)
|
||||
raise OverlappingOutputPathsError(parent, overlapping, msg)
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ def update( # noqa: C901
|
||||
raise InvalidArgumentError("--remote can't be used without --to-remote")
|
||||
|
||||
import_stages = set()
|
||||
other_stage_infos: list["StageInfo"] = []
|
||||
other_stage_infos: list[StageInfo] = []
|
||||
|
||||
for stage_info in self.index.collect_targets(targets, recursive=recursive):
|
||||
if stage_info.stage.is_import:
|
||||
|
||||
@@ -76,14 +76,10 @@ def worktree_view(
|
||||
"""
|
||||
|
||||
def stage_filter(stage: "Stage") -> bool:
|
||||
if push and stage.is_repo_import:
|
||||
return False
|
||||
return True
|
||||
return not (push and stage.is_repo_import)
|
||||
|
||||
def outs_filter(out: "Output") -> bool:
|
||||
if not out.is_in_repo or not out.use_cache or (push and not out.can_push):
|
||||
return False
|
||||
return True
|
||||
return out.is_in_repo and out.use_cache and (not push or out.can_push)
|
||||
|
||||
return index.targets_view(
|
||||
targets,
|
||||
@@ -170,7 +166,7 @@ def update_worktree_stages(repo: "Repo", stage_infos: Iterable["StageInfo"]):
|
||||
|
||||
view = IndexView(repo.index, stage_infos, outs_filter=outs_filter)
|
||||
local_index = view.data["repo"]
|
||||
remote_indexes: dict[str, tuple["Remote", "DataIndex"]] = {}
|
||||
remote_indexes: dict[str, tuple[Remote, DataIndex]] = {}
|
||||
for stage in view.stages:
|
||||
for out in stage.outs:
|
||||
_update_worktree_out(repo, out, local_index, remote_indexes)
|
||||
|
||||
+3
-2
@@ -47,6 +47,7 @@ LOCK_FILE_STAGE_SCHEMA = {
|
||||
LOCKFILE_STAGES_SCHEMA = {str: LOCK_FILE_STAGE_SCHEMA}
|
||||
LOCKFILE_SCHEMA = {
|
||||
vol.Required("schema"): vol.Equal("2.0", "invalid schema version"),
|
||||
"datasets": object,
|
||||
STAGES: LOCKFILE_STAGES_SCHEMA,
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ PLOT_PROPS = {
|
||||
Output.PARAM_PLOT_TITLE: str,
|
||||
Output.PARAM_PLOT_HEADER: bool,
|
||||
}
|
||||
PLOT_PROPS_SCHEMA = {**OUT_PSTAGE_DETAILED_SCHEMA[str], **PLOT_PROPS}
|
||||
PLOT_PROPS_SCHEMA = OUT_PSTAGE_DETAILED_SCHEMA[str] | PLOT_PROPS
|
||||
PLOT_PSTAGE_SCHEMA = {str: vol.Any(PLOT_PROPS_SCHEMA, [PLOT_PROPS_SCHEMA])}
|
||||
|
||||
PARAM_PSTAGE_NON_DEFAULT_SCHEMA = {str: [str]}
|
||||
@@ -128,7 +129,7 @@ DATASET_SCHEMA = vol.Schema(
|
||||
{vol.Required("type"): str, vol.Required("name"): str}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
MULTI_STAGE_SCHEMA = {
|
||||
"datasets": [DATASET_SCHEMA],
|
||||
"datasets": object,
|
||||
PLOTS: [vol.Any(str, SINGLE_PLOT_SCHEMA)],
|
||||
STAGES: SINGLE_PIPELINE_STAGE_SCHEMA,
|
||||
VARS_KWD: VARS_SCHEMA,
|
||||
|
||||
+7
-7
@@ -1,4 +1,5 @@
|
||||
"""Manages source control systems (e.g. Git)."""
|
||||
|
||||
import os
|
||||
from collections.abc import Iterator, Mapping
|
||||
from contextlib import contextmanager
|
||||
@@ -70,8 +71,7 @@ def SCM(
|
||||
*,
|
||||
search_parent_directories: bool = ...,
|
||||
no_scm: Literal[False] = ...,
|
||||
) -> "Git":
|
||||
...
|
||||
) -> "Git": ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -80,8 +80,7 @@ def SCM(
|
||||
*,
|
||||
search_parent_directories: bool = ...,
|
||||
no_scm: Literal[True],
|
||||
) -> "NoSCM":
|
||||
...
|
||||
) -> "NoSCM": ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -90,8 +89,7 @@ def SCM(
|
||||
*,
|
||||
search_parent_directories: bool = ...,
|
||||
no_scm: bool = ...,
|
||||
) -> Union["Git", "NoSCM"]:
|
||||
...
|
||||
) -> Union["Git", "NoSCM"]: ...
|
||||
|
||||
|
||||
def SCM(root_dir, *, search_parent_directories=True, no_scm=False):
|
||||
@@ -241,7 +239,9 @@ def iter_revs(
|
||||
if commit_date:
|
||||
from datetime import datetime
|
||||
|
||||
commit_datestamp = datetime.strptime(commit_date, "%Y-%m-%d").timestamp()
|
||||
commit_datestamp = (
|
||||
datetime.strptime(commit_date, "%Y-%m-%d").timestamp() # noqa: DTZ007
|
||||
)
|
||||
|
||||
def _time_filter(rev):
|
||||
try:
|
||||
|
||||
+21
-16
@@ -290,7 +290,7 @@ class Stage(params.StageParams):
|
||||
)
|
||||
|
||||
def short_description(self) -> Optional["str"]:
|
||||
desc: Optional["str"] = None
|
||||
desc: Optional[str] = None
|
||||
if self.desc:
|
||||
with suppress(ValueError):
|
||||
# try to use first non-empty line as a description
|
||||
@@ -339,7 +339,7 @@ class Stage(params.StageParams):
|
||||
for out in self.outs:
|
||||
status = out.status()
|
||||
if status:
|
||||
if allow_missing and status[str(out)] == "not in cache":
|
||||
if allow_missing and status[str(out)] in ["not in cache", "deleted"]:
|
||||
continue
|
||||
logger.debug(
|
||||
"Output '%s' of %s changed because it is '%s'.",
|
||||
@@ -418,17 +418,28 @@ class Stage(params.StageParams):
|
||||
|
||||
@rwlocked(read=["deps"], write=["outs"])
|
||||
def reproduce(self, interactive=False, **kwargs) -> Optional["Stage"]:
|
||||
if not (
|
||||
kwargs.get("force", False)
|
||||
or self.changed(
|
||||
kwargs.get("allow_missing", False), kwargs.pop("upstream", None)
|
||||
)
|
||||
):
|
||||
force = kwargs.get("force", False)
|
||||
allow_missing = kwargs.get("allow_missing", False)
|
||||
pull = kwargs.get("pull", False)
|
||||
upstream = kwargs.pop("upstream", None)
|
||||
if force:
|
||||
pass
|
||||
# Skip stages with missing data if otherwise unchanged
|
||||
elif not self.changed(allow_missing, upstream):
|
||||
if not isinstance(self, PipelineStage) and self.is_data_source:
|
||||
logger.info("'%s' didn't change, skipping", self.addressing)
|
||||
else:
|
||||
logger.info("Stage '%s' didn't change, skipping", self.addressing)
|
||||
return None
|
||||
# Pull stages with missing data if otherwise unchanged
|
||||
elif not self.changed(True, upstream) and pull:
|
||||
try:
|
||||
logger.info("Pulling data for %s", self)
|
||||
self.repo.pull(self.addressing, jobs=kwargs.get("jobs", None))
|
||||
self.checkout()
|
||||
return None
|
||||
except CheckoutError:
|
||||
logger.info("Unable to pull data for %s", self)
|
||||
|
||||
msg = f"Going to reproduce {self}. Are you sure you want to continue?"
|
||||
if interactive and not prompt.confirm(msg):
|
||||
@@ -597,16 +608,10 @@ class Stage(params.StageParams):
|
||||
if (self.cmd or self.is_import) and not self.frozen and not dry:
|
||||
self.remove_outs(ignore_remove=False, force=False)
|
||||
|
||||
if (
|
||||
self.is_import and (not self.frozen or kwargs.get("pull"))
|
||||
) or self.is_partial_import:
|
||||
if (self.is_import and not self.frozen) or self.is_partial_import:
|
||||
self._sync_import(dry, force, kwargs.get("jobs", None), no_download)
|
||||
elif not self.frozen and self.cmd:
|
||||
self._run_stage(dry, force, **kwargs)
|
||||
elif kwargs.get("pull"):
|
||||
logger.info("Pulling data for %s", self)
|
||||
self.repo.pull(self.addressing, jobs=kwargs.get("jobs", None))
|
||||
self.checkout()
|
||||
elif not dry:
|
||||
args = ("outputs", "frozen ") if self.frozen else ("data sources", "")
|
||||
logger.info("Verifying %s in %s%s", *args, self)
|
||||
@@ -653,7 +658,7 @@ class Stage(params.StageParams):
|
||||
def checkout(
|
||||
self, allow_missing: bool = False, **kwargs
|
||||
) -> dict[str, list["StrPath"]]:
|
||||
stats: dict[str, list["StrPath"]] = defaultdict(list)
|
||||
stats: dict[str, list[StrPath]] = defaultdict(list)
|
||||
if self.is_partial_import:
|
||||
return stats
|
||||
|
||||
|
||||
+3
-2
@@ -229,7 +229,7 @@ class StageCache:
|
||||
if not dry:
|
||||
cached_stage.checkout()
|
||||
|
||||
def transfer(self, from_odb, to_odb):
|
||||
def transfer(self, from_odb, to_odb, force=True):
|
||||
from dvc.fs import HTTPFileSystem, LocalFileSystem
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
|
||||
@@ -258,10 +258,11 @@ class StageCache:
|
||||
|
||||
dst = to_fs.join(to_odb.path, rel)
|
||||
key = to_fs.parent(dst)
|
||||
|
||||
# check if any build cache already exists for this key
|
||||
# TODO: check if MaxKeys=1 or something like that applies
|
||||
# or otherwise this will take a lot of time!
|
||||
if to_fs.exists(key) and first(to_fs.find(key)):
|
||||
if not force and to_fs.exists(key) and first(to_fs.find(key)):
|
||||
continue
|
||||
|
||||
src_name = from_fs.name(src)
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ class StageLoader(Mapping):
|
||||
from dvc.output import Output, merge_file_meta_from_cloud
|
||||
|
||||
assert isinstance(lock_data, dict)
|
||||
items: Iterable[tuple[str, "Output"]] = chain(
|
||||
items: Iterable[tuple[str, Output]] = chain(
|
||||
((StageParams.PARAM_DEPS, dep) for dep in stage.deps),
|
||||
((StageParams.PARAM_OUTS, out) for out in stage.outs),
|
||||
)
|
||||
|
||||
+61
-17
@@ -2,6 +2,9 @@ import os
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
from functools import cache
|
||||
|
||||
from packaging.version import InvalidVersion, Version
|
||||
|
||||
from dvc.log import logger
|
||||
from dvc.utils import fix_env
|
||||
@@ -12,25 +15,59 @@ from .exceptions import StageCmdFailedError
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
@cache
|
||||
def _fish_supports_no_config(executable) -> bool:
|
||||
"""
|
||||
Check if the fish shell supports the --no-config option.
|
||||
|
||||
Parameters:
|
||||
executable (str): The path to the fish shell executable.
|
||||
|
||||
Returns:
|
||||
bool: True if the fish version is greater than 3.3.0, False otherwise.
|
||||
"""
|
||||
try:
|
||||
output = subprocess.check_output( # noqa: S603
|
||||
[executable, "--version"],
|
||||
text=True,
|
||||
)
|
||||
version = Version(output.split(" ")[-1].strip())
|
||||
version_to_check = Version("3.3.0")
|
||||
return version >= version_to_check
|
||||
except (subprocess.CalledProcessError, IndexError, InvalidVersion):
|
||||
logger.trace("could not check fish version, defaulting to False")
|
||||
return False
|
||||
|
||||
|
||||
def _warn_if_fish(executable):
|
||||
if (
|
||||
executable is None
|
||||
or os.path.basename(executable) != "fish"
|
||||
or _fish_supports_no_config(executable)
|
||||
):
|
||||
return
|
||||
logger.warning(
|
||||
"DVC detected that you are using a version of fish shell below 3.3.0 "
|
||||
"Be aware that it might cause problems by overwriting "
|
||||
"your current environment variables with values defined "
|
||||
"in 'config.fish', which might affect your command. See "
|
||||
"https://github.com/iterative/dvc/issues/1307. "
|
||||
)
|
||||
|
||||
|
||||
def _make_cmd(executable, cmd):
|
||||
if executable is None:
|
||||
return cmd
|
||||
opts = {"zsh": ["--no-rcs"], "bash": ["--noprofile", "--norc"]}
|
||||
opts = {
|
||||
"zsh": ["--no-rcs"],
|
||||
"bash": ["--noprofile", "--norc"],
|
||||
"fish": [],
|
||||
}
|
||||
name = os.path.basename(executable).lower()
|
||||
return [executable, *opts.get(name, []), "-c", cmd]
|
||||
|
||||
|
||||
def warn_if_fish(executable):
|
||||
if executable is None or os.path.basename(os.path.realpath(executable)) != "fish":
|
||||
return
|
||||
|
||||
logger.warning(
|
||||
"DVC detected that you are using fish as your default "
|
||||
"shell. Be aware that it might cause problems by overwriting "
|
||||
"your current environment variables with values defined "
|
||||
"in '.fishrc', which might affect your command. See "
|
||||
"https://github.com/iterative/dvc/issues/1307. "
|
||||
)
|
||||
opt = opts.get(name, [])
|
||||
if name == "fish" and _fish_supports_no_config(executable):
|
||||
opt.append("--no-config")
|
||||
return [executable, *opt, "-c", cmd]
|
||||
|
||||
|
||||
def _enforce_cmd_list(cmd):
|
||||
@@ -39,7 +76,7 @@ def _enforce_cmd_list(cmd):
|
||||
|
||||
|
||||
def prepare_kwargs(stage, run_env=None):
|
||||
from dvc.env import DVC_ROOT
|
||||
from dvc.env import DVC_ROOT, DVC_STAGE
|
||||
|
||||
kwargs = {"cwd": stage.wdir, "env": fix_env(None), "close_fds": True}
|
||||
|
||||
@@ -48,6 +85,9 @@ def prepare_kwargs(stage, run_env=None):
|
||||
if DVC_ROOT not in kwargs["env"]:
|
||||
kwargs["env"][DVC_ROOT] = stage.repo.root_dir
|
||||
|
||||
# Create DVC_STAGE env variable for every command
|
||||
kwargs["env"][DVC_STAGE] = stage.addressing
|
||||
|
||||
# NOTE: when you specify `shell=True`, `Popen` [1] will default to
|
||||
# `/bin/sh` on *nix and will add ["/bin/sh", "-c"] to your command.
|
||||
# But we actually want to run the same shell that we are running
|
||||
@@ -103,7 +143,7 @@ def cmd_run(stage, dry=False, run_env=None):
|
||||
executable = get_executable()
|
||||
|
||||
if not dry:
|
||||
warn_if_fish(executable)
|
||||
_warn_if_fish(executable)
|
||||
|
||||
for cmd in commands:
|
||||
display_command(cmd)
|
||||
@@ -114,7 +154,11 @@ def cmd_run(stage, dry=False, run_env=None):
|
||||
|
||||
|
||||
def _pull_missing_deps(stage):
|
||||
from dvc.dependency import DatasetDependency, DbDependency
|
||||
|
||||
for dep in stage.deps:
|
||||
if isinstance(dep, (DatasetDependency, DbDependency)):
|
||||
continue
|
||||
if not dep.exists:
|
||||
stage.repo.pull(dep.def_path)
|
||||
|
||||
|
||||
+1
-3
@@ -92,9 +92,7 @@ def check_no_externals(stage):
|
||||
from dvc.utils import format_link
|
||||
|
||||
def _is_cached_external(out):
|
||||
if out.is_in_repo or not out.use_cache:
|
||||
return False
|
||||
return True
|
||||
return not out.is_in_repo and out.use_cache
|
||||
|
||||
outs = [str(out) for out in stage.outs if _is_cached_external(out)]
|
||||
if not outs:
|
||||
|
||||
@@ -33,9 +33,9 @@ class TestAPI:
|
||||
[
|
||||
{},
|
||||
{"url": "{path}"},
|
||||
{"url": "{path}", "rev": "master"},
|
||||
{"url": "{path}", "rev": "{default_branch}"},
|
||||
{"url": "file://{posixpath}"},
|
||||
{"url": "file://{posixpath}", "rev": "master"},
|
||||
{"url": "file://{posixpath}", "rev": "{default_branch}"},
|
||||
],
|
||||
ids=["current", "local", "local_rev", "git", "git_rev"],
|
||||
)
|
||||
@@ -61,6 +61,8 @@ class TestAPI:
|
||||
|
||||
if url := fs_kwargs.get("url"):
|
||||
fs_kwargs["url"] = url.format(path=tmp_dir, posixpath=tmp_dir.as_posix())
|
||||
if rev := fs_kwargs.get("rev"):
|
||||
fs_kwargs["rev"] = rev.format(default_branch=scm.active_branch())
|
||||
|
||||
fs = DVCFileSystem(**fs_kwargs)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.flaky(max_runs=3, min_passes=1)
|
||||
@pytest.mark.flaky(reruns=3)
|
||||
def test_get(bench_dvc, tmp_dir, scm, dvc, make_dataset, remote):
|
||||
dataset = make_dataset(
|
||||
cache=False, files=False, dvcfile=True, commit=True, remote=True
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user