mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61ce52f259 | ||
|
|
475ef3ce65 | ||
|
|
22eb9051f6 | ||
|
|
9ef1d1d6e6 | ||
|
|
d5cc54ad35 | ||
|
|
47ece317a5 | ||
|
|
8f82a421a0 | ||
|
|
ed2ccaf54c | ||
|
|
23534063ca | ||
|
|
d56957416e | ||
|
|
269e12287c | ||
|
|
5413187c2d | ||
|
|
d56376c70a | ||
|
|
1d5d15644d | ||
|
|
a0e45cdd5d | ||
|
|
7fe316b8e8 | ||
|
|
3478a7e6a9 | ||
|
|
d1fa3d2c21 | ||
|
|
b594337a69 | ||
|
|
36f6f29ac8 | ||
|
|
bbaf57c5b2 | ||
|
|
61f8478885 | ||
|
|
4493da0f04 | ||
|
|
7c225206ab | ||
|
|
be5a54bfb4 | ||
|
|
d9ce9b4fef | ||
|
|
689988592f | ||
|
|
1ddfca856a | ||
|
|
e451ce7d0b | ||
|
|
de05659a93 | ||
|
|
e9244924d7 | ||
|
|
3bbf8fa7d2 | ||
|
|
ed87c308ba | ||
|
|
add28f73f1 | ||
|
|
2bc693fb4c | ||
|
|
ac57fa53fe | ||
|
|
0ca0d94cb2 | ||
|
|
c12deaa62c | ||
|
|
5606608867 | ||
|
|
6711b653a5 | ||
|
|
faef8eec2c | ||
|
|
11766c8839 | ||
|
|
1180a2f051 | ||
|
|
f59a99d442 | ||
|
|
d5f5e14183 | ||
|
|
10f60bed58 | ||
|
|
6a0d7f9c51 | ||
|
|
8640beb998 | ||
|
|
884182f6f2 | ||
|
|
7393ebdcb4 | ||
|
|
cf651c52a1 | ||
|
|
a3f20eb6f4 | ||
|
|
d60edcd88e | ||
|
|
a6cdb9efd2 | ||
|
|
4a558edc59 | ||
|
|
8ff92cc7b0 | ||
|
|
bd88000e2f | ||
|
|
4da4c69985 | ||
|
|
8a1c757497 | ||
|
|
22655fa46d | ||
|
|
103a0e2523 | ||
|
|
0d025e5943 | ||
|
|
b01c4f8b05 | ||
|
|
c27125d9c0 | ||
|
|
71734a3d09 | ||
|
|
fa7619dfee | ||
|
|
67aaf6a192 | ||
|
|
3adede8427 | ||
|
|
a686dded15 | ||
|
|
8b1e61d3c5 | ||
|
|
4594087786 | ||
|
|
5d69f57abf | ||
|
|
53c886e407 | ||
|
|
890fbf3a6e | ||
|
|
81abf2e412 | ||
|
|
3a9d0244b5 | ||
|
|
423166cb0f | ||
|
|
4904286307 | ||
|
|
e556c632b3 | ||
|
|
db0c6e9086 | ||
|
|
4177a5dc9e | ||
|
|
04af944e22 | ||
|
|
13b2ac3bd1 | ||
|
|
25ea5f35ec | ||
|
|
6bd14b21ef | ||
|
|
c2829f4ce8 | ||
|
|
d8a4ecd68f | ||
|
|
7fb9dcec12 | ||
|
|
f23bc71eea | ||
|
|
8b326137c6 | ||
|
|
90109bccd7 | ||
|
|
8c484645fa | ||
|
|
c039e9a6e7 | ||
|
|
10cfa0d5d7 | ||
|
|
ecab4a22fe | ||
|
|
4f354f5333 | ||
|
|
f6b1f01a5c | ||
|
|
6d17bcce93 | ||
|
|
8da04c7c81 | ||
|
|
c617769a1d | ||
|
|
69c3c83833 | ||
|
|
122a68398e | ||
|
|
748c015638 | ||
|
|
ba1ab86078 | ||
|
|
aaa467a96b | ||
|
|
ed15e59115 | ||
|
|
e63cb31a66 | ||
|
|
4c1f172132 | ||
|
|
d2a31eb660 | ||
|
|
625b735502 | ||
|
|
7e93fc8c4a | ||
|
|
853a918d5c | ||
|
|
b5f20f8467 | ||
|
|
2f6bc70b45 | ||
|
|
197f522175 | ||
|
|
35e6f082a4 | ||
|
|
4531fec2b7 | ||
|
|
391bb29be2 | ||
|
|
146bdbbe9b | ||
|
|
c1f659bdd7 | ||
|
|
0fe8f4e368 | ||
|
|
3cbf319e0d | ||
|
|
477a6e177f | ||
|
|
f854fc6365 | ||
|
|
1b90683e02 | ||
|
|
d762372d55 | ||
|
|
f3d304580a | ||
|
|
0e0d46e232 | ||
|
|
95ae018ed8 | ||
|
|
c29e721823 | ||
|
|
a9c7032d50 | ||
|
|
ecb767d622 | ||
|
|
fa45a9bbba | ||
|
|
f50ae589ba | ||
|
|
611c114f01 | ||
|
|
fdc18d5483 | ||
|
|
3b532b7d59 | ||
|
|
e78c4455de | ||
|
|
8e5f4a8f35 | ||
|
|
cb3159e586 | ||
|
|
7b301b09a1 | ||
|
|
55b16166c8 | ||
|
|
c70d0bd14d | ||
|
|
b99a0cd6ca | ||
|
|
d4170b2ec0 | ||
|
|
a3181bb509 | ||
|
|
4d6b90502b | ||
|
|
947427f4d9 | ||
|
|
0482a1a88b | ||
|
|
b470459987 | ||
|
|
81a010674e | ||
|
|
d946a1328e | ||
|
|
ad7d39da20 | ||
|
|
8432de5980 | ||
|
|
c73764156e | ||
|
|
df60df5242 | ||
|
|
504485ed6e | ||
|
|
1e989bde2c | ||
|
|
7e2248bbe5 | ||
|
|
a2d80b2403 | ||
|
|
76e7c76ae5 | ||
|
|
6cada6dc12 | ||
|
|
89d4461908 | ||
|
|
a124d7e866 | ||
|
|
628be2ae49 | ||
|
|
7a71e06946 | ||
|
|
91e54d8c27 | ||
|
|
071b2b8f81 | ||
|
|
0b2b59a601 | ||
|
|
65dc1cd08f | ||
|
|
1b103297b7 | ||
|
|
887c73b3c6 | ||
|
|
660c17f654 | ||
|
|
49da2eef02 | ||
|
|
aff65d42bb | ||
|
|
8968405770 | ||
|
|
b6e8ba6758 | ||
|
|
e307a62dc0 | ||
|
|
639eeed43b | ||
|
|
91f12395f1 | ||
|
|
e7daeb1c3e | ||
|
|
d3569035c8 | ||
|
|
daee52e176 | ||
|
|
a8415b01d0 | ||
|
|
77f56090ff | ||
|
|
1d3102f29b | ||
|
|
a0cbcaa3c8 | ||
|
|
ab780879d1 | ||
|
|
bfad82a2e3 | ||
|
|
e5eec59cb8 | ||
|
|
2a4b83060e | ||
|
|
9e2ebe9fcc | ||
|
|
2cef130191 | ||
|
|
17b9b1571e | ||
|
|
37c8ef3406 | ||
|
|
b7aea414e6 | ||
|
|
126a64618b | ||
|
|
8bdf9b1138 | ||
|
|
9cb242c8a5 | ||
|
|
11802426de | ||
|
|
b376044c45 | ||
|
|
7b3b4b373b | ||
|
|
512bfc4305 | ||
|
|
54ee8c0d77 | ||
|
|
063eb6904d | ||
|
|
3e183d62ed | ||
|
|
e815bbe211 | ||
|
|
2374c72105 | ||
|
|
5cf1421c61 | ||
|
|
a6a2feea62 | ||
|
|
6c1d00c2d9 | ||
|
|
f72456f67c | ||
|
|
2c556f58d3 | ||
|
|
460d08057c | ||
|
|
cc6deb24c8 | ||
|
|
e681030c72 | ||
|
|
7a51548214 | ||
|
|
8e99fd0091 | ||
|
|
7e5a586f85 | ||
|
|
c76a84b7b1 | ||
|
|
26008f1559 | ||
|
|
92d680ff94 | ||
|
|
c69c46d08c | ||
|
|
6477ade557 |
@@ -1 +0,0 @@
|
||||
* @iterative/dvc
|
||||
@@ -1,9 +0,0 @@
|
||||
pull_request_rules:
|
||||
- name: backport patches to 1.11
|
||||
conditions:
|
||||
- base=main
|
||||
- label=backport-1.11
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- "1.11"
|
||||
@@ -1,49 +0,0 @@
|
||||
# Config for https://github.com/apps/release-drafter
|
||||
name-template: "$RESOLVED_VERSION 🦉 "
|
||||
tag-template: "$RESOLVED_VERSION"
|
||||
categories:
|
||||
- title: "🚀 New Features and Enhancements "
|
||||
labels:
|
||||
- "feature"
|
||||
- "enhancement"
|
||||
- "ui"
|
||||
- title: "💣 Breaking Changes "
|
||||
labels:
|
||||
- "breaking-change"
|
||||
- title: "🏇 Optimizations "
|
||||
labels:
|
||||
- "optimize"
|
||||
- "performance"
|
||||
- title: "🐛 Bug Fixes "
|
||||
labels:
|
||||
- "bugfix"
|
||||
- "bug"
|
||||
- title: "🔨 Maintenance "
|
||||
labels:
|
||||
- "maintenance"
|
||||
- "refactoring"
|
||||
- "chore"
|
||||
- "build"
|
||||
- "ci"
|
||||
change-template: "- $TITLE (#$NUMBER) @$AUTHOR"
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- "major"
|
||||
minor:
|
||||
labels:
|
||||
- "minor"
|
||||
patch:
|
||||
labels:
|
||||
- "patch"
|
||||
default: patch
|
||||
exclude-labels:
|
||||
- "skip-changelog"
|
||||
template: |
|
||||
> Refer to https://dvc.org/doc/install for installation instructions.
|
||||
|
||||
## Changes
|
||||
|
||||
$CHANGES
|
||||
|
||||
Thanks again to $CONTRIBUTORS for the contributions! 🎉
|
||||
@@ -1,23 +1,18 @@
|
||||
name: benchmarks
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: run benchmarks
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: "3.10"
|
||||
- uses: iterative/dvc-bench@main
|
||||
with:
|
||||
pytest_options: "-k 'test_init or test_help'"
|
||||
|
||||
|
||||
@@ -35,15 +35,19 @@ jobs:
|
||||
|
||||
- name: Set up Ruby 2.6
|
||||
uses: ruby/setup-ruby@v1
|
||||
if: matrix.pkg != 'exe'
|
||||
with:
|
||||
ruby-version: '2.6'
|
||||
|
||||
- name: Install fpm
|
||||
if: matrix.pkg != 'exe'
|
||||
run: gem install --no-document fpm
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install -U wheel pip setuptools
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
|
||||
- name: Pull images
|
||||
run: dvc pull
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request: {}
|
||||
push: {}
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '5 1 * * *' # every day at 01:05
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
DVC_TEST: "true"
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
SHELL: /bin/bash
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
@@ -31,7 +33,7 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install collective.checkdocs==0.2
|
||||
pip install ".[dev]" pre-commit
|
||||
pip install ".[dev]"
|
||||
- name: Check README
|
||||
run: python setup.py checkdocs
|
||||
- uses: pre-commit/[email protected]
|
||||
@@ -43,6 +45,12 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.8", "3.9", "3.10"]
|
||||
pytest-filter:
|
||||
- "import or plot or live or experiment"
|
||||
- "not (import or plot or live or experiment)"
|
||||
include:
|
||||
- {os: ubuntu-latest, pyv: "3.11-dev"}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -51,18 +59,8 @@ jobs:
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
- name: get pip cache dir
|
||||
id: pip-cache-dir
|
||||
run: |
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
- name: set pip cache
|
||||
id: pip-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.pip-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
cache: pip
|
||||
cache-dependency-path: setup.cfg
|
||||
- name: install
|
||||
if: steps.cache.pip-cache-dir.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -70,10 +68,7 @@ jobs:
|
||||
pip install -e ".[dev]"
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
run: >-
|
||||
python -m tests -n=auto
|
||||
--cov-report=xml --cov-report=term
|
||||
${{ env.extra_test_args }}
|
||||
run: pytest -nauto -ra --durations 100 --cov=dvc --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
|
||||
@@ -6,7 +6,7 @@ repos:
|
||||
- id: black
|
||||
language_version: python3
|
||||
repo: https://github.com/ambv/black
|
||||
rev: 22.6.0
|
||||
rev: 22.10.0
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
@@ -16,9 +16,9 @@ repos:
|
||||
- id: codespell
|
||||
args:
|
||||
- --ignore-words-list
|
||||
- ba,datas,fo,uptodate,cachable
|
||||
- ba,datas,fo,uptodate,cachable,falsy
|
||||
repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.1.0
|
||||
rev: v2.2.2
|
||||
- hooks:
|
||||
- id: isort
|
||||
language_version: python3
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ To share and back up the *data cache*, DVC supports multiple remote storage plat
|
||||
|
||||
|Flowchart|
|
||||
|
||||
`DVC pipelines <https://dvc.org/doc/start/data-management/pipelines>`_ (computational graphs) connect code and data together.
|
||||
`DVC pipelines <https://dvc.org/doc/start/data-management/data-pipelines>`_ (computational graphs) connect code and data together.
|
||||
They specify all steps required to produce a model: input dependencies including code, data, commands to run; and output information to be saved.
|
||||
|
||||
Last but not least, `DVC Experiment Versioning <https://dvc.org/doc/start/experiments>`_ lets you prepare and run a large number of experiments.
|
||||
|
||||
+82
-23
@@ -1,16 +1,14 @@
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Callable, Union
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
from types import FrameType
|
||||
|
||||
|
||||
@contextmanager
|
||||
def viztracer_profile(
|
||||
path: Union[Callable[[], str], str],
|
||||
max_stack_depth: int = -1,
|
||||
):
|
||||
def viztracer_profile(path: Union[Callable[[], str], str], depth: int = -1):
|
||||
try:
|
||||
import viztracer # pylint: disable=import-error
|
||||
except ImportError:
|
||||
@@ -18,7 +16,7 @@ def viztracer_profile(
|
||||
yield
|
||||
return
|
||||
|
||||
tracer = viztracer.VizTracer(max_stack_depth=max_stack_depth)
|
||||
tracer = viztracer.VizTracer(max_stack_depth=depth)
|
||||
|
||||
tracer.start()
|
||||
yield
|
||||
@@ -29,7 +27,9 @@ def viztracer_profile(
|
||||
|
||||
@contextmanager
|
||||
def yappi_profile(
|
||||
path: Union[Callable[[], str], str] = None, wall_clock: bool = True
|
||||
path: Union[Callable[[], str], str] = None,
|
||||
wall_clock: Optional[bool] = True,
|
||||
separate_threads: Optional[bool] = False,
|
||||
):
|
||||
try:
|
||||
import yappi # pylint: disable=import-error
|
||||
@@ -44,14 +44,26 @@ def yappi_profile(
|
||||
yield
|
||||
yappi.stop()
|
||||
|
||||
# pylint:disable=no-member
|
||||
if path:
|
||||
stats = yappi.get_func_stats()
|
||||
fpath = path() if callable(path) else path
|
||||
stats.save(fpath, "callgrind")
|
||||
threads = yappi.get_thread_stats()
|
||||
stats = {}
|
||||
if separate_threads:
|
||||
for thread in threads:
|
||||
ctx_id = thread.id
|
||||
stats[ctx_id] = yappi.get_func_stats(ctx_id=ctx_id)
|
||||
else:
|
||||
yappi.get_func_stats().print_all()
|
||||
yappi.get_thread_stats().print_all()
|
||||
stats[None] = yappi.get_func_stats()
|
||||
|
||||
fpath = path() if callable(path) else path
|
||||
for ctx_id, st in stats.items():
|
||||
if fpath:
|
||||
out = f"{fpath}-{ctx_id}" if ctx_id is not None else fpath
|
||||
st.save(out, type="callgrind")
|
||||
else:
|
||||
if ctx_id is not None:
|
||||
print(f"\nThread {ctx_id}")
|
||||
st.print_all() # pylint:disable=no-member
|
||||
if ctx_id is None:
|
||||
threads.print_all() # pylint:disable=no-member
|
||||
|
||||
yappi.clear_stats()
|
||||
|
||||
@@ -109,6 +121,38 @@ def debug():
|
||||
raise # prevent from jumping ahead
|
||||
|
||||
|
||||
def _sigshow(_, frame: Optional["FrameType"]) -> None:
|
||||
from shutil import get_terminal_size
|
||||
from traceback import format_stack
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
lines = "\u2015" * get_terminal_size().columns
|
||||
ui.error_write(lines, "\n", *format_stack(frame), lines, sep="")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def show_stack():
|
||||
r"""Show stack trace on SIGQUIT (Ctrl-\) or SIGINFO (Ctrl-T on macOS)."""
|
||||
import signal
|
||||
|
||||
signal.signal(signal.SIGQUIT, _sigshow)
|
||||
try:
|
||||
signal.signal(signal.SIGINFO, _sigshow) # only available on macOS
|
||||
except AttributeError:
|
||||
pass
|
||||
yield
|
||||
|
||||
|
||||
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 func
|
||||
|
||||
|
||||
@contextmanager
|
||||
def debugtools(args: "Namespace" = None, **kwargs):
|
||||
kw = vars(args) if args else {}
|
||||
@@ -121,19 +165,21 @@ def debugtools(args: "Namespace" = None, **kwargs):
|
||||
stack.enter_context(profile(kw.get("cprofile_dump")))
|
||||
if kw.get("instrument") or kw.get("instrument_open"):
|
||||
stack.enter_context(instrument(kw.get("instrument_open", False)))
|
||||
if kw.get("show_stack", False):
|
||||
stack.enter_context(show_stack())
|
||||
if kw.get("yappi"):
|
||||
output = "callgrind.dvc-{0:%Y%m%d}_{0:%H%M%S}.out"
|
||||
path_func = _get_path_func("callgrind", "out")
|
||||
stack.enter_context(
|
||||
yappi_profile(path=lambda: output.format(datetime.now()))
|
||||
)
|
||||
if kw.get("viztracer") or kw.get("viztracer_depth"):
|
||||
output = "viztracer.dvc-{0:%Y%m%d}_{0:%H%M%S}.json"
|
||||
stack.enter_context(
|
||||
viztracer_profile(
|
||||
path=lambda: output.format(datetime.now()),
|
||||
max_stack_depth=kw.get("viztracer_depth") or -1,
|
||||
yappi_profile(
|
||||
path=path_func,
|
||||
separate_threads=kw.get("yappi_separate_threads"),
|
||||
)
|
||||
)
|
||||
if kw.get("viztracer") or kw.get("viztracer_depth"):
|
||||
path_func = _get_path_func("viztracer", "json")
|
||||
depth = kw.get("viztracer_depth") or -1
|
||||
prof = viztracer_profile(path=path_func, depth=depth)
|
||||
stack.enter_context(prof)
|
||||
yield
|
||||
|
||||
|
||||
@@ -146,6 +192,12 @@ def add_debugging_flags(parser):
|
||||
parser.add_argument(
|
||||
"--yappi", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yappi-separate-threads",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=SUPPRESS,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--viztracer", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
@@ -160,3 +212,10 @@ def add_debugging_flags(parser):
|
||||
parser.add_argument(
|
||||
"--instrument-open", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show-stack",
|
||||
"--ss",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=SUPPRESS,
|
||||
)
|
||||
|
||||
+3
-43
@@ -102,6 +102,8 @@ def _runtime_info():
|
||||
"""
|
||||
Gather information from the environment where DVC runs to fill a report.
|
||||
"""
|
||||
from iterative_telemetry import find_or_create_user_id
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.utils import is_binary
|
||||
|
||||
@@ -110,7 +112,7 @@ def _runtime_info():
|
||||
"is_binary": is_binary(),
|
||||
"scm_class": _scm_in_use(),
|
||||
"system_info": _system_info(),
|
||||
"user_id": _find_or_create_user_id(),
|
||||
"user_id": find_or_create_user_id(),
|
||||
}
|
||||
|
||||
|
||||
@@ -146,45 +148,3 @@ def _system_info():
|
||||
|
||||
# We don't collect data for any other system.
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _find_or_create_user_id():
|
||||
"""
|
||||
The user's ID is stored on a file under the global config directory.
|
||||
|
||||
The file should contain a JSON with a "user_id" key:
|
||||
|
||||
{"user_id": "16fd2706-8baf-433b-82eb-8c7fada847da"}
|
||||
|
||||
IDs are generated randomly with UUID.
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from dvc.config import Config
|
||||
from dvc.lock import Lock, LockError
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
config_dir = Config.get_dir("global")
|
||||
fname = os.path.join(config_dir, "user_id")
|
||||
lockfile = os.path.join(config_dir, "user_id.lock")
|
||||
|
||||
# Since the `fname` and `lockfile` are under the global config,
|
||||
# we need to make sure such directory exist already.
|
||||
makedirs(config_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
with Lock(lockfile):
|
||||
try:
|
||||
with open(fname, encoding="utf-8") as fobj:
|
||||
user_id = json.load(fobj)["user_id"]
|
||||
|
||||
except (FileNotFoundError, ValueError, KeyError):
|
||||
user_id = str(uuid.uuid4())
|
||||
|
||||
with open(fname, "w", encoding="utf-8") as fobj:
|
||||
json.dump({"user_id": user_id}, fobj)
|
||||
|
||||
return user_id
|
||||
|
||||
except LockError:
|
||||
logger.debug("Failed to acquire '%s'", lockfile)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from dataclasses import asdict, dataclass, field, fields
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
|
||||
from funcy import compact
|
||||
|
||||
|
||||
@dataclass
|
||||
class Annotation:
|
||||
PARAM_DESC: ClassVar[str] = "desc"
|
||||
PARAM_TYPE: ClassVar[str] = "type"
|
||||
PARAM_LABELS: ClassVar[str] = "labels"
|
||||
PARAM_META: ClassVar[str] = "meta"
|
||||
|
||||
desc: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def update(self, **kwargs) -> "Annotation":
|
||||
for attr, value in kwargs.items():
|
||||
if value and hasattr(self, attr):
|
||||
setattr(self, attr, value)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
ANNOTATION_FIELDS = [field.name for field in fields(Annotation)]
|
||||
ANNOTATION_SCHEMA = {
|
||||
Annotation.PARAM_DESC: str,
|
||||
Annotation.PARAM_TYPE: str,
|
||||
Annotation.PARAM_LABELS: [str],
|
||||
Annotation.PARAM_META: object,
|
||||
}
|
||||
+12
-6
@@ -1,9 +1,15 @@
|
||||
from .data import ( # noqa, pylint: disable=redefined-builtin
|
||||
get_url,
|
||||
open,
|
||||
read,
|
||||
)
|
||||
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .data import open # pylint: disable=redefined-builtin
|
||||
from .data import get_url, read
|
||||
from .experiments import make_checkpoint
|
||||
from .params import params_show
|
||||
|
||||
__all__ = ["get_url", "make_checkpoint", "open", "params_show", "read"]
|
||||
__all__ = [
|
||||
"get_url",
|
||||
"make_checkpoint",
|
||||
"open",
|
||||
"params_show",
|
||||
"read",
|
||||
"DVCFileSystem",
|
||||
]
|
||||
|
||||
@@ -37,6 +37,10 @@ def params_show(
|
||||
stages to retrieve parameters from.
|
||||
Defaults to `None`.
|
||||
If `None`, all parameters from all stages will be retrieved.
|
||||
If this method is called from a different location to the one where
|
||||
the `dvc.yaml` is found, the relative path to the `dvc.yaml` must
|
||||
be provided as a prefix with the syntax `{relpath}:{stage}`.
|
||||
For example: `subdir/dvc.yaml:stage-0` or `../dvc.yaml:stage-1`.
|
||||
rev (str, optional): Name of the `Git revision`_ to retrieve parameters
|
||||
from.
|
||||
Defaults to `None`.
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
)
|
||||
return None
|
||||
|
||||
from dvc_data.build import IgnoreInCollectedDirError
|
||||
from dvc_data.hashfile.build import IgnoreInCollectedDirError
|
||||
|
||||
if isinstance(exc, IgnoreInCollectedDirError):
|
||||
logger.exception("")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from argparse import _AppendAction
|
||||
|
||||
|
||||
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
from funcy import ldistinct
|
||||
|
||||
items = getattr(namespace, self.dest) or []
|
||||
items.extend(map(str.strip, values.split(",")))
|
||||
setattr(namespace, self.dest, ldistinct(items))
|
||||
|
||||
|
||||
class KeyValueArgs(_AppendAction):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
items = getattr(namespace, self.dest) or {}
|
||||
for value in filter(bool, values):
|
||||
key, _, value = value.partition("=")
|
||||
items[key.strip()] = value
|
||||
|
||||
setattr(namespace, self.dest, items)
|
||||
@@ -29,6 +29,7 @@ from dvc.commands import (
|
||||
init,
|
||||
install,
|
||||
ls,
|
||||
ls_url,
|
||||
machine,
|
||||
metrics,
|
||||
move,
|
||||
@@ -75,6 +76,7 @@ COMMANDS = [
|
||||
install,
|
||||
root,
|
||||
ls,
|
||||
ls_url,
|
||||
freeze,
|
||||
dag,
|
||||
daemon,
|
||||
|
||||
+5
-2
@@ -21,10 +21,13 @@ def append_doc_link(help_message, path):
|
||||
)
|
||||
|
||||
|
||||
def fix_plumbing_subparsers(subparsers):
|
||||
# metavar needs to be explicitly set in order to hide plumbing subcommands
|
||||
def hide_subparsers_from_help(subparsers):
|
||||
# metavar needs to be explicitly set in order to hide subcommands
|
||||
# from the 'positional arguments' choices list
|
||||
# see: https://bugs.python.org/issue22848
|
||||
# Need to set `add_help=False`, but avoid setting `help`
|
||||
# (not even to `argparse.SUPPPRESS`).
|
||||
# NOTE: The argument is the parent subparser, not the subcommand parser.
|
||||
cmds = [
|
||||
cmd for cmd, parser in subparsers.choices.items() if parser.add_help
|
||||
]
|
||||
|
||||
+36
-9
@@ -2,12 +2,43 @@ import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import KeyValueArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help="User description of the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--meta",
|
||||
metavar="key=value",
|
||||
nargs=1,
|
||||
action=KeyValueArgs,
|
||||
help="Custom metadata to add to the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--label",
|
||||
dest="labels",
|
||||
type=str,
|
||||
action="append",
|
||||
metavar="<str>",
|
||||
help="Label for the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
type=str,
|
||||
metavar="<str>",
|
||||
help="Type of the data.",
|
||||
)
|
||||
|
||||
|
||||
class CmdAdd(CmdBase):
|
||||
def run(self):
|
||||
from dvc.exceptions import (
|
||||
@@ -28,6 +59,9 @@ class CmdAdd(CmdBase):
|
||||
glob=self.args.glob,
|
||||
desc=self.args.desc,
|
||||
out=self.args.out,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
jobs=self.args.jobs,
|
||||
@@ -109,15 +143,8 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
)
|
||||
|
||||
_add_annotating_args(parser)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
|
||||
@@ -110,7 +110,7 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
".dvc files and stage names."
|
||||
),
|
||||
).complete = completion.DVC_FILE
|
||||
checkout_parser.set_defaults(func=CmdCheckout)
|
||||
|
||||
@@ -70,7 +70,9 @@ def add_parser(subparsers, parent_parser):
|
||||
commit_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="stages or .dvc files to commit. Optional. "
|
||||
"(Finds all DVC files in the workspace by default.)",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files and stage names."
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
commit_parser.set_defaults(func=CmdCommit)
|
||||
|
||||
+1
-1
@@ -204,7 +204,7 @@ def add_parser(subparsers, parent_parser):
|
||||
dag_parser.add_argument(
|
||||
"target",
|
||||
nargs="?",
|
||||
help="Stage or output to show pipeline for (optional). "
|
||||
help="Stage name or output to show pipeline for. "
|
||||
"Finds all stages in the workspace by default.",
|
||||
)
|
||||
dag_parser.set_defaults(func=CmdDAG)
|
||||
|
||||
+137
-21
@@ -1,11 +1,18 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
|
||||
|
||||
from funcy import chunks, compact, log_durations
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import CommaSeparatedArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.cli.utils import (
|
||||
append_doc_link,
|
||||
fix_subparsers,
|
||||
hide_subparsers_from_help,
|
||||
)
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
@@ -31,15 +38,22 @@ class CmdDataStatus(CmdBase):
|
||||
"unchanged": "DVC unchanged files",
|
||||
}
|
||||
HINTS = {
|
||||
"not_in_cache": 'use "dvc pull <file>..." '
|
||||
"to update your local storage",
|
||||
"committed": "git commit the corresponding dvc files "
|
||||
"to update the repo",
|
||||
"uncommitted": 'use "dvc commit <file>..." to track changes',
|
||||
"untracked": 'use "git add <file> ..." or '
|
||||
'dvc add <file>..." to commit to git or to dvc',
|
||||
"git_dirty": "there are {}changes not tracked by dvc, "
|
||||
'use "git status" to see',
|
||||
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
|
||||
"committed": (
|
||||
"git commit the corresponding dvc files to update the repo",
|
||||
),
|
||||
"uncommitted": (
|
||||
'use "dvc commit <file>..." to track changes',
|
||||
'use "dvc checkout <file>..." to discard changes',
|
||||
),
|
||||
"untracked": (
|
||||
'use "git add <file> ..." or '
|
||||
'"dvc add <file>..." to commit to git or to dvc',
|
||||
),
|
||||
"git_dirty": (
|
||||
"there are {}changes not tracked by dvc, "
|
||||
'use "git status" to see',
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -76,8 +90,9 @@ class CmdDataStatus(CmdBase):
|
||||
color = cls.COLORS.get(stage, None)
|
||||
|
||||
ui.write(header)
|
||||
if hint := cls.HINTS.get(stage):
|
||||
ui.write(f" ({hint})")
|
||||
if hints := cls.HINTS.get(stage):
|
||||
for hint in hints:
|
||||
ui.write(f" ({hint})")
|
||||
|
||||
if isinstance(stage_status, dict):
|
||||
items = [
|
||||
@@ -92,9 +107,10 @@ class CmdDataStatus(CmdBase):
|
||||
out = "\n".join(tabs + item for item in chunk)
|
||||
ui.write(colorize(out, color))
|
||||
|
||||
if (hint := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
|
||||
message = hint.format("other " if result else "")
|
||||
ui.write(f"[blue]({message})[/]", styled=True)
|
||||
if (hints := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
|
||||
for hint in hints:
|
||||
message = hint.format("other " if result else "")
|
||||
ui.write(f"[blue]({message})[/]", styled=True)
|
||||
return 0
|
||||
|
||||
def run(self) -> int:
|
||||
@@ -102,7 +118,6 @@ class CmdDataStatus(CmdBase):
|
||||
status = self.repo.data_status(
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
with_dirs=self.args.with_dirs,
|
||||
)
|
||||
|
||||
if not self.args.unchanged:
|
||||
@@ -116,6 +131,65 @@ class CmdDataStatus(CmdBase):
|
||||
return self._show_status(status)
|
||||
|
||||
|
||||
class CmdDataLs(CmdBase):
|
||||
@staticmethod
|
||||
def _show_table(
|
||||
d: Iterable[Dict[str, Any]],
|
||||
filter_types: Set[str],
|
||||
filter_labels: Set[str],
|
||||
markdown: bool = False,
|
||||
) -> None:
|
||||
from rich.style import Style
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
td = TabularData(
|
||||
columns=["Path", "Type", "Labels", "Description"], fill_value="-"
|
||||
)
|
||||
for entry in sorted(d, key=itemgetter("path")):
|
||||
typ = entry.get("type", "")
|
||||
desc = entry.get("desc", "-")
|
||||
labels = entry.get("labels", [])
|
||||
|
||||
if filter_types and typ not in filter_types:
|
||||
continue
|
||||
if filter_labels and filter_labels.isdisjoint(labels):
|
||||
continue
|
||||
|
||||
rich_label = ui.rich_text()
|
||||
for index, label in enumerate(labels):
|
||||
if index:
|
||||
rich_label.append(",")
|
||||
style = Style(bold=label in filter_labels, color="green")
|
||||
rich_label.append(label, style=style)
|
||||
|
||||
if markdown and desc:
|
||||
desc = desc.partition("\n")[0]
|
||||
|
||||
path = ui.rich_text(entry["path"], style="cyan")
|
||||
type_style = Style(bold=typ in filter_types, color="yellow")
|
||||
typ = ui.rich_text(entry.get("type", ""), style=type_style)
|
||||
td.append([path, typ or "-", rich_label or "-", desc])
|
||||
|
||||
td.render(markdown=markdown, rich_table=True)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo.data import ls
|
||||
|
||||
filter_labels = set(self.args.labels)
|
||||
filter_types = set(self.args.type)
|
||||
d = ls(
|
||||
self.repo, targets=self.args.targets, recursive=self.args.recursive
|
||||
)
|
||||
self._show_table(
|
||||
d,
|
||||
filter_labels=filter_labels,
|
||||
filter_types=filter_types,
|
||||
markdown=self.args.markdown,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
data_parser = subparsers.add_parser(
|
||||
"data",
|
||||
@@ -172,10 +246,52 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--with-dirs",
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
DATA_LS_HELP = "List data tracked by DVC with its metadata."
|
||||
data_ls_parser = data_subparsers.add_parser(
|
||||
"ls",
|
||||
aliases=["list"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DATA_LS_HELP, "data/ls"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
add_help=False,
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
dest="markdown",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
data_ls_parser.add_argument(
|
||||
"--type",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of type to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--labels",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of labels to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Recursively list from the specified directories.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"targets",
|
||||
default=None,
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
data_ls_parser.set_defaults(func=CmdDataLs)
|
||||
hide_subparsers_from_help(data_subparsers)
|
||||
|
||||
@@ -43,7 +43,8 @@ class CmdDataPull(CmdDataBase):
|
||||
)
|
||||
self.log_summary(stats)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
self.log_summary(getattr(exc, "stats", {}))
|
||||
if stats := getattr(exc, "stats", {}):
|
||||
self.log_summary(stats)
|
||||
logger.exception("failed to pull data from the cloud")
|
||||
return 1
|
||||
|
||||
@@ -119,7 +120,7 @@ def shared_parent_parser():
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
".dvc files and stage names."
|
||||
),
|
||||
).complete = completion.DVC_FILE
|
||||
|
||||
@@ -194,7 +195,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
"--glob",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Pull cache for targets matching shell-style wildcards.",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
pull_parser.set_defaults(func=CmdDataPull)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import argparse
|
||||
|
||||
from dvc.cli.utils import (
|
||||
append_doc_link,
|
||||
fix_plumbing_subparsers,
|
||||
fix_subparsers,
|
||||
hide_subparsers_from_help,
|
||||
)
|
||||
from dvc.commands.experiments import (
|
||||
apply,
|
||||
@@ -59,7 +59,7 @@ def add_parser(subparsers, parent_parser):
|
||||
fix_subparsers(experiments_subparsers)
|
||||
for cmd in SUB_COMMANDS:
|
||||
cmd.add_parser(experiments_subparsers, parent_parser)
|
||||
fix_plumbing_subparsers(experiments_subparsers)
|
||||
hide_subparsers_from_help(experiments_subparsers)
|
||||
|
||||
|
||||
def add_rev_selection_flags(
|
||||
|
||||
@@ -27,7 +27,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
elif diff:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
precision = self.args.precision or DEFAULT_PRECISION
|
||||
|
||||
@@ -16,13 +16,17 @@ class CmdExperimentsList(CmdBase):
|
||||
num=self.args.num,
|
||||
git_remote=self.args.git_remote,
|
||||
)
|
||||
tags = self.repo.scm.describe(exps)
|
||||
remained = {baseline for baseline, tag in tags.items() if tag is None}
|
||||
base = "refs/heads/"
|
||||
ref_heads = self.repo.scm.describe(remained, base=base)
|
||||
|
||||
for baseline in exps:
|
||||
tag = self.repo.scm.describe(baseline)
|
||||
if not tag:
|
||||
branch = self.repo.scm.describe(baseline, base="refs/heads")
|
||||
if branch:
|
||||
tag = branch.split("/")[-1]
|
||||
name = tag if tag else baseline[:7]
|
||||
name = (
|
||||
tags[baseline]
|
||||
or ref_heads[baseline][len(base) :]
|
||||
or baseline[:7]
|
||||
)
|
||||
if not name_only:
|
||||
print(f"{name}:")
|
||||
for exp_name in exps[baseline]:
|
||||
|
||||
@@ -95,11 +95,8 @@ def _collect_rows(
|
||||
|
||||
exp = results.get("data", {})
|
||||
|
||||
if exp.get("running"):
|
||||
state = "Running"
|
||||
elif exp.get("queued"):
|
||||
state = "Queued"
|
||||
else:
|
||||
state = exp.get("status")
|
||||
if state == "Success":
|
||||
state = fill_value
|
||||
|
||||
is_baseline = rev == "baseline"
|
||||
@@ -476,6 +473,8 @@ class CmdExperimentsShow(CmdBase):
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
hide_queued=self.args.hide_queued,
|
||||
hide_failed=self.args.hide_failed,
|
||||
revs=self.args.rev,
|
||||
num=self.args.num,
|
||||
sha_only=self.args.sha,
|
||||
@@ -594,6 +593,18 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Always show git commit SHAs instead of branch/tag names.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--hide-failed",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Hide failed experiments in the table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--hide-queued",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Hide queued experiments in the table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
|
||||
@@ -74,7 +74,7 @@ class CmdMergeDriver(CmdHookBase):
|
||||
our = Dvcfile(dvc, self.args.our, verify=False)
|
||||
their = Dvcfile(dvc, self.args.their, verify=False)
|
||||
|
||||
our.merge(ancestor, their)
|
||||
our.merge(ancestor, their, allowed=["add", "remove", "change"])
|
||||
|
||||
return 0
|
||||
finally:
|
||||
|
||||
+17
-10
@@ -19,7 +19,11 @@ class CmdImport(CmdBase):
|
||||
fname=self.args.file,
|
||||
rev=self.args.rev,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -33,6 +37,8 @@ class CmdImport(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download file or directory tracked by DVC or by Git "
|
||||
"into the workspace, and track it."
|
||||
@@ -69,20 +75,19 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create .dvc file without actually downloading it.",
|
||||
help="Only create .dvc file without actually importing target data.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Create .dvc file including target data hash value(s)"
|
||||
" but do not actually download the file(s).",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-j",
|
||||
@@ -94,4 +99,6 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImport)
|
||||
|
||||
+23
-7
@@ -17,10 +17,15 @@ class CmdImportUrl(CmdBase):
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
version_aware=self.args.version_aware,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception(
|
||||
@@ -34,6 +39,8 @@ class CmdImportUrl(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download or copy file from URL and take it under DVC control."
|
||||
)
|
||||
@@ -66,11 +73,19 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create .dvc file without actually downloading it.",
|
||||
help="Only create .dvc file without actually importing target data.",
|
||||
)
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Create .dvc file including target data hash value(s)"
|
||||
" but do not actually download the file(s).",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--to-remote",
|
||||
@@ -95,12 +110,13 @@ def add_parser(subparsers, parent_parser):
|
||||
metavar="<number>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
"--version-aware",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
"Import using cloud versioning. Implied if the URL contains a "
|
||||
"version ID."
|
||||
),
|
||||
)
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImportUrl)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
from .ls import _prettify
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdListUrl(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
from dvc.repo import Repo
|
||||
|
||||
entries = Repo.ls_url(self.args.url, recursive=self.args.recursive)
|
||||
if entries:
|
||||
entries = _prettify(entries, with_color=True)
|
||||
ui.write("\n".join(entries))
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
LS_HELP = "List directory contents from URL."
|
||||
lsurl_parser = subparsers.add_parser(
|
||||
"list-url",
|
||||
aliases=["ls-url"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(LS_HELP, "list-url"),
|
||||
help=LS_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
lsurl_parser.add_argument(
|
||||
"url", help="See `dvc import-url -h` for full list of supported URLs."
|
||||
)
|
||||
lsurl_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
help="Recursively list files.",
|
||||
)
|
||||
lsurl_parser.set_defaults(func=CmdListUrl)
|
||||
+20
-42
@@ -25,7 +25,7 @@ def _show_json(renderers, split=False):
|
||||
|
||||
|
||||
def _adjust_vega_renderers(renderers):
|
||||
from dvc.render import VERSION_FIELD
|
||||
from dvc.render import REVISION_FIELD, VERSION_FIELD
|
||||
from dvc_render import VegaRenderer
|
||||
|
||||
for r in renderers:
|
||||
@@ -42,6 +42,7 @@ def _adjust_vega_renderers(renderers):
|
||||
dp["rev"] = "::".join(vi.values())
|
||||
else:
|
||||
for dp in r.datapoints:
|
||||
dp[REVISION_FIELD] = dp[VERSION_FIELD]["revision"]
|
||||
dp.pop(VERSION_FIELD, {})
|
||||
|
||||
|
||||
@@ -66,18 +67,6 @@ def _data_versions_count(renderer):
|
||||
return len(set(x))
|
||||
|
||||
|
||||
def _filter_unhandled_renderers(renderers):
|
||||
# filtering out renderers currently unhandled by vscode extension
|
||||
from dvc_render import VegaRenderer
|
||||
|
||||
def _is_json_viable(r):
|
||||
return not (
|
||||
isinstance(r, VegaRenderer) and _data_versions_count(r) > 1
|
||||
)
|
||||
|
||||
return list(filter(_is_json_viable, renderers))
|
||||
|
||||
|
||||
class CmdPlots(CmdBase):
|
||||
def _func(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
@@ -156,12 +145,10 @@ class CmdPlots(CmdBase):
|
||||
templates_dir=self.repo.plots.templates_dir,
|
||||
)
|
||||
if self.args.json:
|
||||
renderers = _filter_unhandled_renderers(renderers)
|
||||
_show_json(renderers, self.args.split)
|
||||
return 0
|
||||
|
||||
_adjust_vega_renderers(renderers)
|
||||
|
||||
if self.args.show_vega:
|
||||
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
|
||||
if renderer:
|
||||
@@ -223,31 +210,22 @@ class CmdPlotsModify(CmdPlots):
|
||||
|
||||
|
||||
class CmdPlotsTemplates(CmdBase):
|
||||
TEMPLATES_CHOICES = [
|
||||
"simple",
|
||||
"linear",
|
||||
"confusion",
|
||||
"confusion_normalized",
|
||||
"scatter",
|
||||
"smooth",
|
||||
"bar_horizontal_sorted",
|
||||
"bar_horizontal",
|
||||
]
|
||||
|
||||
def run(self):
|
||||
from dvc_render.vega_templates import dump_templates
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc_render.vega_templates import TEMPLATES
|
||||
|
||||
try:
|
||||
out = (
|
||||
os.path.join(os.getcwd(), self.args.out)
|
||||
if self.args.out
|
||||
else self.repo.plots.templates_dir
|
||||
)
|
||||
target = self.args.template
|
||||
if target:
|
||||
for template in TEMPLATES:
|
||||
if target == template.DEFAULT_NAME:
|
||||
ui.write_json(template.DEFAULT_CONTENT)
|
||||
return 0
|
||||
raise InvalidArgumentError(f"Unexpected template: {target}.")
|
||||
|
||||
targets = [self.args.target] if self.args.target else None
|
||||
dump_templates(output=out, targets=targets)
|
||||
templates_path = os.path.relpath(out, os.getcwd())
|
||||
ui.write(f"Templates have been written into '{templates_path}'.")
|
||||
else:
|
||||
for template in TEMPLATES:
|
||||
ui.write(template.DEFAULT_NAME)
|
||||
|
||||
return 0
|
||||
except DvcException:
|
||||
@@ -357,8 +335,7 @@ def add_parser(subparsers, parent_parser):
|
||||
plots_modify_parser.set_defaults(func=CmdPlotsModify)
|
||||
|
||||
TEMPLATES_HELP = (
|
||||
"Write built-in plots templates to a directory "
|
||||
"(.dvc/plots by default)."
|
||||
"List built-in plots templates or show JSON specification for one."
|
||||
)
|
||||
plots_templates_parser = plots_subparsers.add_parser(
|
||||
"templates",
|
||||
@@ -368,13 +345,14 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_templates_parser.add_argument(
|
||||
"target",
|
||||
"template",
|
||||
default=None,
|
||||
nargs="?",
|
||||
choices=CmdPlotsTemplates.TEMPLATES_CHOICES,
|
||||
help="Template to write. Writes all templates by default.",
|
||||
help=(
|
||||
"Template for which to show JSON specification. "
|
||||
"List all template names by default."
|
||||
),
|
||||
)
|
||||
_add_output_argument(plots_templates_parser, typ="templates")
|
||||
plots_templates_parser.set_defaults(func=CmdPlotsTemplates)
|
||||
|
||||
|
||||
|
||||
+10
-1
@@ -55,7 +55,16 @@ def add_arguments(repro_parser):
|
||||
repro_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Stages to reproduce. 'dvc.yaml' by default.",
|
||||
help=(
|
||||
"""\
|
||||
Stages to reproduce. 'dvc.yaml' by default.
|
||||
The targets can be path to a dvc.yaml file or `.dvc` file,
|
||||
or a stage name from dvc.yaml file from
|
||||
current working directory. To run a stage from dvc.yaml
|
||||
from other directories, the target must be a path followed by colon `:`
|
||||
and then the stage name name.
|
||||
"""
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
repro_parser.add_argument(
|
||||
"-f",
|
||||
|
||||
@@ -76,7 +76,7 @@ class CmdStageList(CmdBase):
|
||||
# removing duplicates while maintaining order
|
||||
collected = chain.from_iterable(
|
||||
self.repo.stage.collect(
|
||||
target=target, recursive=self.args.recursive, accept_group=True
|
||||
target=target, recursive=self.args.recursive
|
||||
)
|
||||
for target in self.args.targets
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ class CmdUpdate(CmdBase):
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
to_remote=self.args.to_remote,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
@@ -55,6 +56,13 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Update all stages in the specified directory.",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Update .dvc file git revision/hash value(s)"
|
||||
" but do not download the file(s).",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--to-remote",
|
||||
action="store_true",
|
||||
|
||||
+4
-1
@@ -176,7 +176,10 @@ class Config(dict):
|
||||
|
||||
if fs.exists(filename):
|
||||
with fs.open(filename) as fobj:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
try:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
else:
|
||||
conf_obj = ConfigObj()
|
||||
return _parse_named(_lower_keys(conf_obj.dict()))
|
||||
|
||||
@@ -78,7 +78,9 @@ REMOTE_COMMON = {
|
||||
"url": str,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
"jobs": All(Coerce(int), Range(1)),
|
||||
Optional("worktree"): Bool,
|
||||
Optional("no_traverse"): Bool, # obsoleted
|
||||
Optional("version_aware"): Bool,
|
||||
}
|
||||
LOCAL_COMMON = {
|
||||
"type": supported_cache_type,
|
||||
@@ -178,6 +180,8 @@ SCHEMA = {
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
"passphrase": str,
|
||||
"ask_passphrase": Bool,
|
||||
"keyfile": str,
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
@@ -289,4 +293,9 @@ SCHEMA = {
|
||||
"bool": All(Lower, Choices("store_true", "boolean_optional")),
|
||||
"list": All(Lower, Choices("nargs", "append")),
|
||||
},
|
||||
"hydra": {
|
||||
Optional("enabled", default=False): Bool,
|
||||
"config_dir": str,
|
||||
"config_name": str,
|
||||
},
|
||||
}
|
||||
|
||||
+45
-22
@@ -3,16 +3,38 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
from dvc_data.db import get_index
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc_data.hashfile.db import get_index
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.status import CompareStatusResult
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, path, fs, **config):
|
||||
self.path = path
|
||||
self.fs = fs
|
||||
|
||||
self.worktree = config.pop("worktree", False)
|
||||
self.config = config
|
||||
|
||||
@cached_property
|
||||
def odb(self):
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
path = self.path
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(path, ".dvc", "cache")
|
||||
|
||||
return get_odb(self.fs, path, **self.config)
|
||||
|
||||
|
||||
class DataCloud:
|
||||
"""Class that manages dvc remotes.
|
||||
|
||||
@@ -27,18 +49,23 @@ class DataCloud:
|
||||
def __init__(self, repo):
|
||||
self.repo = repo
|
||||
|
||||
def get_remote_odb(
|
||||
def get_remote(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "HashFileDB":
|
||||
) -> "Remote":
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
if not name:
|
||||
name = self.repo.config["core"].get("remote")
|
||||
|
||||
if name:
|
||||
return self._init_odb(name)
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
fs = cls(**config)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return Remote(fs_path, fs, **config)
|
||||
|
||||
if bool(self.repo.config["remote"]):
|
||||
error_msg = (
|
||||
@@ -55,13 +82,13 @@ class DataCloud:
|
||||
|
||||
raise NoRemoteError(error_msg)
|
||||
|
||||
def _init_odb(self, name):
|
||||
from dvc.fs import get_cloud_fs
|
||||
from dvc_data.db import get_odb
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return get_odb(cls(**config), fs_path, **config)
|
||||
def get_remote_odb(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "HashFileDB":
|
||||
remote = self.get_remote(name=name, command=command)
|
||||
return remote.odb
|
||||
|
||||
def _log_missing(self, status: "CompareStatusResult"):
|
||||
if status.missing:
|
||||
@@ -80,14 +107,10 @@ class DataCloud:
|
||||
dest_odb: "HashFileDB",
|
||||
objs: Iterable["HashInfo"],
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.exceptions import FileTransferError
|
||||
from dvc_data.transfer import TransferError, transfer
|
||||
) -> "TransferResult":
|
||||
from dvc_data.hashfile.transfer import transfer
|
||||
|
||||
try:
|
||||
return transfer(src_odb, dest_odb, objs, **kwargs)
|
||||
except TransferError as exc:
|
||||
raise FileTransferError(exc.fails) from exc
|
||||
return transfer(src_odb, dest_odb, objs, **kwargs)
|
||||
|
||||
def push(
|
||||
self,
|
||||
@@ -95,7 +118,7 @@ class DataCloud:
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
odb: Optional["HashFileDB"] = None,
|
||||
):
|
||||
) -> "TransferResult":
|
||||
"""Push data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
@@ -122,7 +145,7 @@ class DataCloud:
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
odb: Optional["HashFileDB"] = None,
|
||||
):
|
||||
) -> "TransferResult":
|
||||
"""Pull data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
@@ -164,7 +187,7 @@ class DataCloud:
|
||||
log_missing: log warning messages if file doesn't exist
|
||||
neither in cache, neither in cloud.
|
||||
"""
|
||||
from dvc_data.status import compare_status
|
||||
from dvc_data.hashfile.status import compare_status
|
||||
|
||||
if not odb:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
|
||||
@@ -18,7 +18,7 @@ SCHEMA: Mapping[str, Any] = {
|
||||
}
|
||||
|
||||
|
||||
def _get(stage, p, info):
|
||||
def _get(stage, p, info, fs_config=None):
|
||||
if info and info.get(RepoDependency.PARAM_REPO):
|
||||
repo = info.pop(RepoDependency.PARAM_REPO)
|
||||
return RepoDependency(repo, stage, p, info)
|
||||
@@ -27,7 +27,7 @@ def _get(stage, p, info):
|
||||
params = info.pop(ParamsDependency.PARAM_PARAMS)
|
||||
return ParamsDependency(stage, p, params)
|
||||
|
||||
return Dependency(stage, p, info)
|
||||
return Dependency(stage, p, info, fs_config=fs_config)
|
||||
|
||||
|
||||
def loadd_from(stage, d_list):
|
||||
@@ -38,10 +38,18 @@ def loadd_from(stage, d_list):
|
||||
return ret
|
||||
|
||||
|
||||
def loads_from(stage, s_list, erepo=None):
|
||||
def loads_from(stage, s_list, erepo=None, fs_config=None):
|
||||
assert isinstance(s_list, list)
|
||||
info = {RepoDependency.PARAM_REPO: erepo} if erepo else {}
|
||||
return [_get(stage, s, info.copy()) for s in s_list]
|
||||
return [
|
||||
_get(
|
||||
stage,
|
||||
s,
|
||||
info.copy(),
|
||||
fs_config=fs_config,
|
||||
)
|
||||
for s in s_list
|
||||
]
|
||||
|
||||
|
||||
def _merge_params(s_list):
|
||||
|
||||
+32
-2
@@ -1,6 +1,7 @@
|
||||
from typing import Type
|
||||
from typing import Dict, Type
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.fs import download as fs_download
|
||||
from dvc.output import Output
|
||||
|
||||
|
||||
@@ -30,5 +31,34 @@ class Dependency(Output):
|
||||
) # type: Type[DvcException]
|
||||
IsStageFileError = DependencyIsStageFileError # type: Type[DvcException]
|
||||
|
||||
def workspace_status(self) -> Dict[str, str]:
|
||||
if self.fs.version_aware:
|
||||
old_fs_path = self.fs_path
|
||||
try:
|
||||
self.fs_path = self.fs.path.version_path(self.fs_path, None)
|
||||
if self.changed_meta():
|
||||
return {str(self): "update available"}
|
||||
finally:
|
||||
self.fs_path = old_fs_path
|
||||
return super().workspace_status()
|
||||
|
||||
def update(self, rev=None):
|
||||
pass
|
||||
if self.fs.version_aware:
|
||||
self.fs_path = self.fs.path.version_path(self.fs_path, rev)
|
||||
self.meta = self.get_meta()
|
||||
self.def_path = self.fs.path.version_path(
|
||||
self.def_path, self.meta.version_id
|
||||
)
|
||||
self.fs_path = self.fs.path.version_path(
|
||||
self.fs_path, self.meta.version_id
|
||||
)
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
fs_download(self.fs, self.fs_path, to, jobs=jobs)
|
||||
|
||||
def save(self):
|
||||
super().save()
|
||||
if self.fs.version_aware:
|
||||
self.fs_path = self.fs.path.version_path(
|
||||
self.fs_path, self.meta.version_id
|
||||
)
|
||||
|
||||
@@ -39,14 +39,15 @@ class ParamsDependency(Dependency):
|
||||
|
||||
def __init__(self, stage, path, params=None, repo=None):
|
||||
self.params = list(params) if params else []
|
||||
info = (
|
||||
{self.PARAM_PARAMS: params} if isinstance(params, dict) else None
|
||||
)
|
||||
hash_info = HashInfo()
|
||||
if isinstance(params, dict):
|
||||
hash_info = HashInfo(self.PARAM_PARAMS, params)
|
||||
repo = repo or stage.repo
|
||||
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
|
||||
super().__init__(stage, path, info=info, repo=repo)
|
||||
super().__init__(stage, path, repo=repo)
|
||||
self.hash_info = hash_info
|
||||
|
||||
def dumpd(self):
|
||||
def dumpd(self, **kwargs):
|
||||
ret = super().dumpd()
|
||||
if not self.hash_info:
|
||||
ret[self.PARAM_PARAMS] = self.params or {}
|
||||
|
||||
+27
-13
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
|
||||
from voluptuous import Required
|
||||
|
||||
from dvc.prompt import confirm
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
@@ -32,6 +33,7 @@ class RepoDependency(Dependency):
|
||||
def __init__(self, def_repo, stage, *args, **kwargs):
|
||||
self.def_repo = def_repo
|
||||
self._objs: Dict[str, "HashFile"] = {}
|
||||
self._meta: Dict[str, Meta] = {}
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
def _parse_path(self, fs, fs_path):
|
||||
@@ -59,11 +61,11 @@ class RepoDependency(Dependency):
|
||||
def save(self):
|
||||
pass
|
||||
|
||||
def dumpd(self):
|
||||
def dumpd(self, **kwargs):
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc_data.checkout import checkout
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
||||
@@ -94,17 +96,17 @@ class RepoDependency(Dependency):
|
||||
def get_used_objs(
|
||||
self, **kwargs
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
|
||||
used, _ = self._get_used_and_obj(**kwargs)
|
||||
used, _, _ = self._get_used_and_obj(**kwargs)
|
||||
return used
|
||||
|
||||
def _get_used_and_obj(
|
||||
self, obj_only=False, **kwargs
|
||||
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "HashFile"]:
|
||||
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], Meta, "HashFile"]:
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputOrStageError, PathMissingError
|
||||
from dvc.utils import as_posix
|
||||
from dvc_data.build import build
|
||||
from dvc_data.objects.tree import Tree, TreeError
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.tree import Tree, TreeError
|
||||
|
||||
local_odb = self.repo.odb.local
|
||||
locked = kwargs.pop("locked", True)
|
||||
@@ -131,7 +133,7 @@ class RepoDependency(Dependency):
|
||||
pass
|
||||
|
||||
try:
|
||||
object_store, _, obj = build(
|
||||
object_store, meta, obj = build(
|
||||
local_odb,
|
||||
as_posix(self.def_path),
|
||||
repo.dvcfs,
|
||||
@@ -145,16 +147,18 @@ class RepoDependency(Dependency):
|
||||
object_store.read_only = True
|
||||
|
||||
self._objs[rev] = obj
|
||||
self._meta[rev] = meta
|
||||
|
||||
used_obj_ids[object_store].add(obj.hash_info)
|
||||
if isinstance(obj, Tree):
|
||||
used_obj_ids[object_store].update(oid for _, _, oid in obj)
|
||||
return used_obj_ids, obj
|
||||
return used_obj_ids, meta, obj
|
||||
|
||||
def _check_circular_import(self, odb, obj_ids):
|
||||
from dvc.exceptions import CircularImportError
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc_data.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.objects.tree import Tree
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
if not isinstance(odb, ReferenceHashFileDB):
|
||||
return
|
||||
@@ -169,7 +173,7 @@ class RepoDependency(Dependency):
|
||||
|
||||
checked_urls = set()
|
||||
for obj in iter_objs():
|
||||
if not isinstance(obj.fs, DvcFileSystem):
|
||||
if not isinstance(obj.fs, DVCFileSystem):
|
||||
continue
|
||||
if (
|
||||
obj.fs.repo_url in checked_urls
|
||||
@@ -190,11 +194,21 @@ class RepoDependency(Dependency):
|
||||
rev = self._get_rev(locked=locked)
|
||||
if rev in self._objs:
|
||||
return self._objs[rev]
|
||||
_, obj = self._get_used_and_obj(
|
||||
_, _, obj = self._get_used_and_obj(
|
||||
obj_only=True, filter_info=filter_info, **kwargs
|
||||
)
|
||||
return obj
|
||||
|
||||
def get_meta(self, filter_info=None, **kwargs):
|
||||
locked = kwargs.get("locked", True)
|
||||
rev = self._get_rev(locked=locked)
|
||||
if rev in self._meta:
|
||||
return self._meta[rev]
|
||||
_, meta, _ = self._get_used_and_obj(
|
||||
obj_only=True, filter_info=filter_info, **kwargs
|
||||
)
|
||||
return meta
|
||||
|
||||
def _make_repo(self, locked=True, **kwargs):
|
||||
from dvc.external_repo import external_repo
|
||||
|
||||
|
||||
+15
-12
@@ -172,7 +172,7 @@ class FileMixin:
|
||||
def dump(self, stage, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -198,18 +198,18 @@ class SingleStageFile(FileMixin):
|
||||
if self.verify:
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
logger.debug("Saving information to '%s'.", relpath(self.path))
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage))
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def remove_stage(self, stage): # pylint: disable=unused-argument
|
||||
self.remove()
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
assert isinstance(ancestor, SingleStageFile)
|
||||
assert isinstance(other, SingleStageFile)
|
||||
|
||||
stage = self.stage
|
||||
stage.merge(ancestor.stage, other.stage)
|
||||
stage.merge(ancestor.stage, other.stage, allowed=allowed)
|
||||
self.dump(stage)
|
||||
|
||||
|
||||
@@ -237,15 +237,17 @@ class PipelineFile(FileMixin):
|
||||
self._dump_pipeline_file(stage)
|
||||
|
||||
if update_lock:
|
||||
self._dump_lockfile(stage)
|
||||
self._dump_lockfile(stage, **kwargs)
|
||||
|
||||
def _dump_lockfile(self, stage):
|
||||
self._lockfile.dump(stage)
|
||||
def _dump_lockfile(self, stage, **kwargs):
|
||||
self._lockfile.dump(stage, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _check_if_parametrized(stage):
|
||||
def _check_if_parametrized(stage, action: str = "dump") -> None:
|
||||
if stage.raw_data.parametrized:
|
||||
raise ParametrizedDumpError(f"cannot dump a parametrized {stage}")
|
||||
raise ParametrizedDumpError(
|
||||
f"cannot {action} a parametrized {stage}"
|
||||
)
|
||||
|
||||
def _dump_pipeline_file(self, stage):
|
||||
self._check_if_parametrized(stage)
|
||||
@@ -291,6 +293,7 @@ class PipelineFile(FileMixin):
|
||||
self._lockfile.remove()
|
||||
|
||||
def remove_stage(self, stage):
|
||||
self._check_if_parametrized(stage, "remove")
|
||||
self._lockfile.remove_stage(stage)
|
||||
if not self.exists():
|
||||
return
|
||||
@@ -307,7 +310,7 @@ class PipelineFile(FileMixin):
|
||||
else:
|
||||
super().remove()
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -363,7 +366,7 @@ class Lockfile(FileMixin):
|
||||
return {SCHEMA_KWD: version}
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
stage_data = serialize.to_lockfile(stage)
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
version = LOCKFILE_VERSION.from_dict(data)
|
||||
@@ -408,7 +411,7 @@ class Lockfile(FileMixin):
|
||||
else:
|
||||
self.remove()
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
@@ -278,6 +278,11 @@ class PathMissingError(DvcException):
|
||||
self.dvc_only = dvc_only
|
||||
|
||||
|
||||
class URLMissingError(DvcException):
|
||||
def __init__(self, url):
|
||||
super().__init__(f"The path '{url}' does not exist")
|
||||
|
||||
|
||||
class RemoteCacheRequiredError(DvcException):
|
||||
def __init__(self, scheme, fs_path):
|
||||
|
||||
|
||||
+32
-11
@@ -2,6 +2,9 @@ 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
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from dvc_objects.fs import utils # noqa: F401
|
||||
from dvc_objects.fs import ( # noqa: F401
|
||||
@@ -23,14 +26,15 @@ from dvc_objects.fs.errors import ( # noqa: F401
|
||||
)
|
||||
from dvc_objects.fs.path import Path # noqa: F401
|
||||
|
||||
from .callbacks import Callback
|
||||
from .data import DataFileSystem # noqa: F401
|
||||
from .dvc import DvcFileSystem # noqa: F401
|
||||
from .dvc import DVCFileSystem # noqa: F401
|
||||
from .git import GitFileSystem # noqa: F401
|
||||
|
||||
known_implementations.update(
|
||||
{
|
||||
"dvc": {
|
||||
"class": "dvc.fs.dvc.DvcFileSystem",
|
||||
"class": "dvc.fs.dvc.DVCFileSystem",
|
||||
"err": "dvc is supported, but requires 'dvc' to be installed",
|
||||
},
|
||||
"git": {
|
||||
@@ -44,7 +48,23 @@ known_implementations.update(
|
||||
# pylint: enable=unused-import
|
||||
|
||||
|
||||
def get_fs_config(repo, config, **kwargs):
|
||||
def download(fs, fs_path, to, jobs=None):
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Downloading {fs.path.name(fs_path)}",
|
||||
unit="files",
|
||||
) as cb:
|
||||
fs.get(fs_path, to.fs_path, batch_size=jobs, callback=cb)
|
||||
|
||||
|
||||
def parse_external_url(url, config=None):
|
||||
remote_config = dict(config) if config else {}
|
||||
remote_config["url"] = url
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(None, **remote_config)
|
||||
fs = fs_cls(**fs_config)
|
||||
return fs, fs_path
|
||||
|
||||
|
||||
def get_fs_config(config, **kwargs):
|
||||
name = kwargs.get("name")
|
||||
if name:
|
||||
try:
|
||||
@@ -55,10 +75,10 @@ def get_fs_config(repo, config, **kwargs):
|
||||
raise RemoteNotFoundError(f"remote '{name}' doesn't exist")
|
||||
else:
|
||||
remote_conf = kwargs
|
||||
return _resolve_remote_refs(repo, config, remote_conf)
|
||||
return _resolve_remote_refs(config, remote_conf)
|
||||
|
||||
|
||||
def _resolve_remote_refs(repo, config, remote_conf):
|
||||
def _resolve_remote_refs(config, remote_conf):
|
||||
# Support for cross referenced remotes.
|
||||
# This will merge the settings, shadowing base ref with remote_conf.
|
||||
# For example, having:
|
||||
@@ -84,21 +104,22 @@ def _resolve_remote_refs(repo, config, remote_conf):
|
||||
if parsed.scheme != "remote":
|
||||
return remote_conf
|
||||
|
||||
base = get_fs_config(repo, config, name=parsed.netloc)
|
||||
cls, _, _ = get_cloud_fs(repo, **base)
|
||||
base = get_fs_config(config, name=parsed.netloc)
|
||||
cls, _, _ = _get_cloud_fs(config, **base)
|
||||
relpath = parsed.path.lstrip("/").replace("/", cls.sep)
|
||||
url = cls.sep.join((base["url"], relpath))
|
||||
return {**base, **remote_conf, "url": url}
|
||||
|
||||
|
||||
def get_cloud_fs(repo, **kwargs):
|
||||
from dvc.config import ConfigError as RepoConfigError
|
||||
from dvc.config_schema import SCHEMA, Invalid
|
||||
|
||||
repo_config = repo.config if repo else {}
|
||||
return _get_cloud_fs(repo_config, **kwargs)
|
||||
|
||||
|
||||
def _get_cloud_fs(repo_config, **kwargs):
|
||||
core_config = repo_config.get("core", {})
|
||||
|
||||
remote_conf = get_fs_config(repo, repo_config, **kwargs)
|
||||
remote_conf = get_fs_config(repo_config, **kwargs)
|
||||
try:
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf)
|
||||
except Invalid as exc:
|
||||
|
||||
+17
-5
@@ -1,6 +1,6 @@
|
||||
# pylint: disable=unused-import
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
@@ -24,6 +24,7 @@ class RichCallback(Callback):
|
||||
bytes: bool = False, # pylint: disable=redefined-builtin
|
||||
unit: str = None,
|
||||
disable: bool = False,
|
||||
transient: bool = True,
|
||||
) -> None:
|
||||
self._progress = progress
|
||||
self.disable = disable
|
||||
@@ -35,6 +36,7 @@ class RichCallback(Callback):
|
||||
"visible": False,
|
||||
"progress_type": None if bytes else "summary",
|
||||
}
|
||||
self._transient = transient
|
||||
self._stack = ExitStack()
|
||||
super().__init__(size=size, value=value)
|
||||
|
||||
@@ -47,7 +49,7 @@ class RichCallback(Callback):
|
||||
return self._progress
|
||||
|
||||
progress = RichTransferProgress(
|
||||
transient=True,
|
||||
transient=self._transient,
|
||||
disable=self.disable,
|
||||
console=ui.error_console,
|
||||
)
|
||||
@@ -61,7 +63,8 @@ class RichCallback(Callback):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
self.progress.clear_task(self.task)
|
||||
if self._transient:
|
||||
self.progress.clear_task(self.task)
|
||||
self._stack.close()
|
||||
|
||||
def call(self, hook_name=None, **kwargs):
|
||||
@@ -72,8 +75,17 @@ class RichCallback(Callback):
|
||||
visible=not self.disable,
|
||||
)
|
||||
|
||||
def branch(self, path_1, path_2, kwargs, child: Optional[Callback] = None):
|
||||
def branch(
|
||||
self,
|
||||
path_1: Union[str, BinaryIO],
|
||||
path_2: str,
|
||||
kwargs: Dict[str, Any],
|
||||
child: "Callback" = None,
|
||||
):
|
||||
child = child or RichCallback(
|
||||
progress=self.progress, desc=path_1, bytes=True
|
||||
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)
|
||||
|
||||
+144
-219
@@ -1,17 +1,16 @@
|
||||
import errno
|
||||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import threading
|
||||
from contextlib import suppress
|
||||
from itertools import takewhile
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Tuple, Type, Union
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple, Type, Union
|
||||
|
||||
from fsspec.spec import AbstractFileSystem
|
||||
from funcy import cached_property, wrap_prop, wrap_with
|
||||
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
from .data import DataFileSystem
|
||||
@@ -22,29 +21,19 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RepoFactory = Union[Callable[[str], "Repo"], Type["Repo"]]
|
||||
Key = Tuple[str, ...]
|
||||
|
||||
|
||||
def as_posix(path: str) -> str:
|
||||
return path.replace(ntpath.sep, posixpath.sep)
|
||||
|
||||
|
||||
def _wrap_walk(dvc_fs, *args, **kwargs):
|
||||
for root, dnames, fnames in dvc_fs.walk(*args, **kwargs):
|
||||
yield dvc_fs.path.join(dvc_fs.repo.root_dir, root), dnames, fnames
|
||||
# NOT the same as dvc.dvcfile.is_dvc_file()!
|
||||
def _is_dvc_file(fname):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.ignore import DvcIgnore
|
||||
|
||||
|
||||
def _ls(fs, path):
|
||||
dnames = []
|
||||
fnames = []
|
||||
|
||||
for entry in fs.ls(path, detail=True):
|
||||
name = fs.path.name(entry["name"])
|
||||
if entry["type"] == "directory":
|
||||
dnames.append(name)
|
||||
else:
|
||||
fnames.append(name)
|
||||
|
||||
return dnames, fnames
|
||||
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
|
||||
|
||||
def _merge_info(repo, fs_info, dvc_info):
|
||||
@@ -70,40 +59,68 @@ def _merge_info(repo, fs_info, dvc_info):
|
||||
return ret
|
||||
|
||||
|
||||
class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
"""DVC + git-tracked files fs.
|
||||
def _get_dvc_path(dvc_fs, subkey):
|
||||
return dvc_fs.path.join(*subkey) if subkey else ""
|
||||
|
||||
Args:
|
||||
repo: DVC or git repo.
|
||||
subrepos: traverse to subrepos (by default, it ignores subrepos)
|
||||
repo_factory: A function to initialize subrepo with, default is Repo.
|
||||
kwargs: Additional keyword arguments passed to the `DataFileSystem()`.
|
||||
"""
|
||||
|
||||
class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
cachable = False
|
||||
root_marker = "/"
|
||||
|
||||
PARAM_REPO_URL = "repo_url"
|
||||
PARAM_REPO_ROOT = "repo_root"
|
||||
PARAM_REV = "rev"
|
||||
PARAM_CACHE_DIR = "cache_dir"
|
||||
PARAM_CACHE_TYPES = "cache_types"
|
||||
PARAM_SUBREPOS = "subrepos"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
repo: Optional["Repo"] = None,
|
||||
subrepos=False,
|
||||
repo_factory: RepoFactory = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
subrepos: bool = False,
|
||||
repo_factory: Optional[RepoFactory] = None,
|
||||
**repo_kwargs: Any,
|
||||
) -> None:
|
||||
"""DVC + git-tracked files fs.
|
||||
|
||||
Args:
|
||||
path (str, optional): URL or path to a DVC/Git repository.
|
||||
Defaults to a DVC repository in the current working directory.
|
||||
Both HTTP and SSH protocols are supported for remote 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 the default branch in case of remote repositories.
|
||||
In case of a local repository, if rev is unspecified, it will
|
||||
default to the working directory.
|
||||
If the repo is not a Git repo, this option is ignored.
|
||||
repo (:obj:`Repo`, optional): `Repo` instance.
|
||||
subrepos (bool): traverse to subrepos.
|
||||
By default, it ignores subrepos.
|
||||
repo_factory (callable): A function to initialize subrepo with.
|
||||
The default is `Repo`.
|
||||
|
||||
Examples:
|
||||
- Opening a filesystem from repo in current working directory
|
||||
|
||||
>>> fs = DVCFileSystem()
|
||||
|
||||
- Opening a filesystem from local repository
|
||||
|
||||
>>> fs = DVCFileSystem("path/to/local/repository")
|
||||
|
||||
- Opening a remote repository
|
||||
|
||||
>>> fs = DVCFileSystem(
|
||||
... "https://github.com/iterative/example-get-started",
|
||||
... rev="main",
|
||||
... )
|
||||
"""
|
||||
from pygtrie import Trie
|
||||
|
||||
super().__init__()
|
||||
if repo is None:
|
||||
repo, repo_factory = self._repo_from_fs_config(
|
||||
subrepos=subrepos, **kwargs
|
||||
repo = self._make_repo(
|
||||
url=url, rev=rev, subrepos=subrepos, **repo_kwargs
|
||||
)
|
||||
assert repo
|
||||
# pylint: disable=protected-access
|
||||
repo_factory = repo._fs_conf["repo_factory"]
|
||||
|
||||
if not repo_factory:
|
||||
from dvc.repo import Repo
|
||||
@@ -137,78 +154,35 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
|
||||
|
||||
def _get_key(self, path):
|
||||
def _get_key(self, path) -> Key:
|
||||
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
|
||||
if parts == (".",):
|
||||
parts = ()
|
||||
return parts
|
||||
|
||||
def _get_key_from_relative(self, path) -> Key:
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
parts = parts[1:]
|
||||
return parts
|
||||
|
||||
def _from_key(self, parts: Key) -> str:
|
||||
return self.repo.fs.path.join(self.repo.root_dir, *parts)
|
||||
|
||||
@property
|
||||
def repo_url(self):
|
||||
if self.repo is None:
|
||||
return None
|
||||
return self.repo.url
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return {
|
||||
self.PARAM_REPO_URL: self.repo_url,
|
||||
self.PARAM_REPO_ROOT: self.repo.root_dir,
|
||||
self.PARAM_REV: getattr(self.repo.fs, "rev", None),
|
||||
self.PARAM_CACHE_DIR: os.path.abspath(self.repo.odb.local.path),
|
||||
self.PARAM_CACHE_TYPES: self.repo.odb.local.cache_types,
|
||||
self.PARAM_SUBREPOS: self._traverse_subrepos,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _repo_from_fs_config(
|
||||
cls, **config
|
||||
) -> Tuple["Repo", Optional["RepoFactory"]]:
|
||||
from dvc.external_repo import erepo_factory, external_repo
|
||||
def _make_repo(cls, **kwargs) -> "Repo":
|
||||
from dvc.repo import Repo
|
||||
|
||||
url = config.get(cls.PARAM_REPO_URL)
|
||||
root = config.get(cls.PARAM_REPO_ROOT)
|
||||
assert url or root
|
||||
with Repo.open(uninitialized=True, **kwargs) as repo:
|
||||
return repo
|
||||
|
||||
def _open(*args, **kwargs):
|
||||
# NOTE: if original repo was an erepo (and has a URL),
|
||||
# we cannot use Repo.open() since it will skip erepo
|
||||
# cache/remote setup for local URLs
|
||||
if url is None:
|
||||
return Repo.open(*args, **kwargs)
|
||||
return external_repo(*args, **kwargs)
|
||||
|
||||
cache_dir = config.get(cls.PARAM_CACHE_DIR)
|
||||
cache_config = (
|
||||
{}
|
||||
if not cache_dir
|
||||
else {
|
||||
"cache": {
|
||||
"dir": cache_dir,
|
||||
"type": config.get(cls.PARAM_CACHE_TYPES),
|
||||
}
|
||||
}
|
||||
)
|
||||
repo_kwargs: dict = {
|
||||
"rev": config.get(cls.PARAM_REV),
|
||||
"subrepos": config.get(cls.PARAM_SUBREPOS, False),
|
||||
"uninitialized": True,
|
||||
}
|
||||
factory: Optional["RepoFactory"] = None
|
||||
if url is None:
|
||||
repo_kwargs["config"] = cache_config
|
||||
else:
|
||||
repo_kwargs["cache_dir"] = cache_dir
|
||||
factory = erepo_factory(url, root, cache_config)
|
||||
|
||||
with _open(
|
||||
url if url else root,
|
||||
**repo_kwargs,
|
||||
) as repo:
|
||||
return repo, factory
|
||||
|
||||
def _get_repo(self, path: str) -> "Repo":
|
||||
def _get_repo(self, key: Key) -> "Repo":
|
||||
"""Returns repo that the path falls in, using prefix.
|
||||
|
||||
If the path is already tracked/collected, it just returns the repo.
|
||||
@@ -216,33 +190,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
Otherwise, it collects the repos that might be in the path's parents
|
||||
and then returns the appropriate one.
|
||||
"""
|
||||
if not self.repo.fs.path.isin_or_eq(path, self.repo.root_dir):
|
||||
# outside of repo
|
||||
return self.repo
|
||||
|
||||
key = self._get_key(path)
|
||||
repo = self._subrepos_trie.get(key)
|
||||
if repo:
|
||||
return repo
|
||||
|
||||
prefix_key, repo = self._subrepos_trie.longest_prefix(key)
|
||||
prefix = self.repo.fs.path.join(
|
||||
self.repo.root_dir,
|
||||
*prefix_key, # pylint: disable=not-an-iterable
|
||||
)
|
||||
|
||||
parents = (parent for parent in self.repo.fs.path.parents(path))
|
||||
dirs = [path] + list(takewhile(lambda p: p != prefix, parents))
|
||||
dirs.reverse()
|
||||
self._update(dirs, starting_repo=repo)
|
||||
dir_keys = (key[:i] for i in range(len(prefix_key) + 1, len(key) + 1))
|
||||
self._update(dir_keys, starting_repo=repo)
|
||||
return self._subrepos_trie.get(key) or self.repo
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _update(self, dirs, starting_repo):
|
||||
def _update(self, dir_keys, starting_repo):
|
||||
"""Checks for subrepo in directories and updates them."""
|
||||
repo = starting_repo
|
||||
for d in dirs:
|
||||
key = self._get_key(d)
|
||||
for key in dir_keys:
|
||||
d = self._from_key(key)
|
||||
if self._is_dvc_repo(d):
|
||||
repo = self.repo_factory(
|
||||
d,
|
||||
@@ -265,79 +227,68 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
repo_path = self.repo.fs.path.join(dir_path, Repo.DVC_DIR)
|
||||
return self.repo.fs.isdir(repo_path)
|
||||
|
||||
def _get_fs_pair(
|
||||
self, path
|
||||
) -> Tuple[
|
||||
Optional["Repo"],
|
||||
Optional[FileSystem],
|
||||
Optional[str],
|
||||
Optional[DataFileSystem],
|
||||
Optional[str],
|
||||
]:
|
||||
def _get_subrepo_info(
|
||||
self, key: Key
|
||||
) -> Tuple["Repo", Optional[DataFileSystem], Key]:
|
||||
"""
|
||||
Returns a pair of fss based on repo the path falls in, using prefix.
|
||||
Returns information about the subrepo the key is part of.
|
||||
"""
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
parts = parts[1:]
|
||||
|
||||
fs_path = self.repo.fs.path.join(self.repo.root_dir, *parts)
|
||||
repo = self._get_repo(fs_path)
|
||||
fs = repo.fs
|
||||
|
||||
repo_parts = fs.path.relparts(repo.root_dir, self.repo.root_dir)
|
||||
if repo_parts[0] == os.curdir:
|
||||
repo_parts = repo_parts[1:]
|
||||
|
||||
dvc_parts = parts[len(repo_parts) :]
|
||||
if dvc_parts and dvc_parts[0] == os.curdir:
|
||||
dvc_parts = dvc_parts[1:]
|
||||
|
||||
key = self._get_key(repo.root_dir)
|
||||
dvc_fs = self._datafss.get(key)
|
||||
if dvc_fs:
|
||||
dvc_path = dvc_fs.path.join(*dvc_parts) if dvc_parts else ""
|
||||
repo = self._get_repo(key)
|
||||
repo_key: Key
|
||||
if repo is self.repo:
|
||||
repo_key = ()
|
||||
subkey = key
|
||||
else:
|
||||
dvc_path = None
|
||||
repo_key = self._get_key(repo.root_dir)
|
||||
subkey = key[len(repo_key) :]
|
||||
|
||||
return repo, fs, fs_path, dvc_fs, dvc_path
|
||||
dvc_fs = self._datafss.get(repo_key)
|
||||
return repo, dvc_fs, subkey
|
||||
|
||||
def open(
|
||||
self, path, mode="r", encoding="utf-8", **kwargs
|
||||
def _open(
|
||||
self, path, mode="rb", **kwargs
|
||||
): # pylint: disable=arguments-renamed, arguments-differ
|
||||
if "b" in mode:
|
||||
encoding = None
|
||||
if mode != "rb":
|
||||
raise OSError(errno.EROFS, os.strerror(errno.EROFS))
|
||||
|
||||
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
key = self._get_key_from_relative(path)
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
return fs.open(fs_path, mode=mode, encoding=encoding)
|
||||
return self.repo.fs.open(fs_path, mode=mode)
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
return dvc_fs.open(dvc_path, mode=mode, encoding=encoding, **kwargs)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.open(dvc_path, mode=mode)
|
||||
|
||||
def isdvc(self, path, **kwargs):
|
||||
_, _, _, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
"""Is this entry dvc-tracked?"""
|
||||
key = self._get_key_from_relative(path)
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
|
||||
|
||||
def ls( # pylint: disable=arguments-differ
|
||||
self, path, detail=True, dvc_only=False, **kwargs
|
||||
):
|
||||
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
dvcignore = repo.dvcignore
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
names = set()
|
||||
if dvc_fs:
|
||||
with suppress(FileNotFoundError):
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
for entry in dvc_fs.ls(dvc_path, detail=False):
|
||||
names.add(dvc_fs.path.name(entry))
|
||||
|
||||
if not dvc_only and fs:
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
if not dvc_only:
|
||||
fs = self.repo.fs
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
for entry in dvcignore.ls(
|
||||
for entry in repo.dvcignore.ls(
|
||||
fs, fs_path, detail=False, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
names.add(fs.path.name(entry))
|
||||
@@ -345,26 +296,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
pass
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
|
||||
def _func(fname):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.ignore import DvcIgnore
|
||||
|
||||
if dvcfiles:
|
||||
return True
|
||||
|
||||
return not (
|
||||
is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
)
|
||||
|
||||
names = filter(_func, names)
|
||||
if not dvcfiles:
|
||||
names = (name for name in names if not _is_dvc_file(name))
|
||||
|
||||
infos = []
|
||||
paths = []
|
||||
for name in names:
|
||||
entry_path = self.path.join(path, name)
|
||||
entry_key = key + (name,)
|
||||
try:
|
||||
info = self.info(entry_path, ignore_subrepos=ignore_subrepos)
|
||||
info = self._info(
|
||||
entry_key,
|
||||
entry_path,
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
check_ignored=False,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
infos.append(info)
|
||||
@@ -375,48 +321,39 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
return infos
|
||||
|
||||
def get_file( # pylint: disable=arguments-differ
|
||||
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(rpath)
|
||||
|
||||
if fs:
|
||||
try:
|
||||
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
|
||||
return
|
||||
except FileNotFoundError:
|
||||
if not dvc_fs:
|
||||
raise
|
||||
dvc_fs.get_file(dvc_path, lpath, callback=callback, **kwargs)
|
||||
|
||||
def info(self, path, **kwargs):
|
||||
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
dvcignore = repo.dvcignore
|
||||
key = self._get_key_from_relative(path)
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info(self, key, path, ignore_subrepos=True, check_ignored=True):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_info = None
|
||||
if dvc_fs:
|
||||
try:
|
||||
dvc_info = dvc_fs.info(dvc_path)
|
||||
dvc_info = dvc_fs.fs.index.info(subkey)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_info["name"] = dvc_path
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
fs_info = None
|
||||
if fs:
|
||||
try:
|
||||
fs_info = fs.info(fs_path)
|
||||
if dvcignore.is_ignored(
|
||||
fs, fs_path, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_info = None
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
if not dvc_info:
|
||||
raise
|
||||
fs = self.repo.fs
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
fs_info = fs.info(fs_path)
|
||||
if check_ignored and repo.dvcignore.is_ignored(
|
||||
fs, fs_path, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_info = None
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
if not dvc_info:
|
||||
raise
|
||||
|
||||
# NOTE: if some parent in fs_path turns out to be a file, it means
|
||||
# that the whole repofs branch doesn't exist.
|
||||
if fs and not fs_info and dvc_info:
|
||||
if dvc_info and not fs_info:
|
||||
for parent in fs.path.parents(fs_path):
|
||||
try:
|
||||
if fs.info(parent)["type"] != "directory":
|
||||
@@ -432,16 +369,8 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
def checksum(self, path):
|
||||
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
try:
|
||||
return fs.checksum(fs_path)
|
||||
except FileNotFoundError:
|
||||
return dvc_fs.checksum(dvc_path)
|
||||
|
||||
|
||||
class DvcFileSystem(FileSystem):
|
||||
class DVCFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
@@ -451,7 +380,7 @@ class DvcFileSystem(FileSystem):
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self):
|
||||
return _DvcFileSystem(**self.fs_args)
|
||||
return _DVCFileSystem(**self.fs_args)
|
||||
|
||||
def isdvc(self, path, **kwargs):
|
||||
return self.fs.isdvc(path, **kwargs)
|
||||
@@ -468,10 +397,6 @@ class DvcFileSystem(FileSystem):
|
||||
def repo_url(self):
|
||||
return self.fs.repo_url
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.fs.config
|
||||
|
||||
def from_os_path(self, path):
|
||||
if os.path.isabs(path):
|
||||
path = os.path.relpath(path, self.repo.root_dir)
|
||||
|
||||
@@ -15,6 +15,7 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
|
||||
"""Proxies the repo file access methods to Git objects"""
|
||||
|
||||
protocol = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
+16
-3
@@ -300,12 +300,25 @@ class DvcIgnoreFilter:
|
||||
return [fs_dict[name] for name in chain(dirs, nondirs)]
|
||||
|
||||
def walk(self, fs: FileSystem, path: AnyFSPath, **kwargs):
|
||||
detail = kwargs.get("detail", False)
|
||||
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
|
||||
if fs.protocol == Schemes.LOCAL:
|
||||
for root, dirs, files in fs.walk(path, **kwargs):
|
||||
dirs[:], files[:] = self(
|
||||
root, dirs, files, ignore_subrepos=ignore_subrepos
|
||||
)
|
||||
if detail:
|
||||
all_dnames = set(dirs.keys())
|
||||
all_fnames = set(files.keys())
|
||||
dnames, fnames = self(
|
||||
root,
|
||||
all_dnames,
|
||||
all_fnames,
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
)
|
||||
list(map(dirs.pop, all_dnames - set(dnames)))
|
||||
list(map(files.pop, all_fnames - set(fnames)))
|
||||
else:
|
||||
dirs[:], files[:] = self(
|
||||
root, dirs, files, ignore_subrepos=ignore_subrepos
|
||||
)
|
||||
yield root, dirs, files
|
||||
else:
|
||||
yield from fs.walk(path, **kwargs)
|
||||
|
||||
+22
-1
@@ -14,6 +14,14 @@ from dvc.scm import SCMError
|
||||
from dvc.utils import error_link
|
||||
from dvc.utils.pkg import PKG
|
||||
|
||||
SUBPROJECTS = (
|
||||
"dvc_data",
|
||||
"dvc_objects",
|
||||
"dvc_render",
|
||||
"dvc_task",
|
||||
"dvclive",
|
||||
"scmrepo",
|
||||
)
|
||||
package = "" if PKG is None else f"({PKG})"
|
||||
|
||||
|
||||
@@ -23,6 +31,7 @@ def get_dvc_info():
|
||||
"---------------------------------",
|
||||
f"Platform: Python {platform.python_version()} on "
|
||||
f"{platform.platform()}",
|
||||
f"Subprojects:{_get_subprojects()}",
|
||||
f"Supports:{_get_supported_remotes()}",
|
||||
]
|
||||
|
||||
@@ -67,7 +76,7 @@ def _get_caches(cache):
|
||||
|
||||
def _get_remotes(config):
|
||||
schemes = (
|
||||
get_fs_cls(get_fs_config(None, config, name=remote)).protocol
|
||||
get_fs_cls(get_fs_config(config, name=remote)).protocol
|
||||
for remote in config["remote"]
|
||||
)
|
||||
|
||||
@@ -88,6 +97,18 @@ def _get_linktype_support_info(repo):
|
||||
return ", ".join(links)
|
||||
|
||||
|
||||
def _get_subprojects():
|
||||
subprojects = []
|
||||
for subproject in SUBPROJECTS:
|
||||
try:
|
||||
version = importlib_metadata.version(subproject)
|
||||
subprojects.append(f"{subproject} = {version}")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return "\n\t" + "\n\t".join(subprojects)
|
||||
|
||||
|
||||
def _get_supported_remotes():
|
||||
supported_remotes = []
|
||||
for scheme in registry:
|
||||
|
||||
+5
-2
@@ -213,8 +213,6 @@ def set_loggers_level(level: int = logging.INFO) -> None:
|
||||
|
||||
|
||||
def setup(level: int = logging.INFO) -> None:
|
||||
colorama.init()
|
||||
|
||||
if level >= logging.DEBUG:
|
||||
# Unclosed session errors for asyncio/aiohttp are only available
|
||||
# on the tracing mode for extensive debug purposes. They are really
|
||||
@@ -297,3 +295,8 @@ def setup(level: int = logging.INFO) -> None:
|
||||
"disable_existing_loggers": False,
|
||||
}
|
||||
)
|
||||
|
||||
# NOTE: colorama init needs to go after logging config to avoid potential
|
||||
# conflicts with libs that modify logging (like tensorflow/abseil)
|
||||
# https://github.com/iterative/dvc/issues/8387
|
||||
colorama.init()
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.db import get_odb
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
|
||||
def _get_odb(repo, settings, fs=None):
|
||||
|
||||
+179
-59
@@ -1,10 +1,10 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set, Type
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import collecting, project
|
||||
from funcy import cached_property, collecting, project
|
||||
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
|
||||
|
||||
from dvc import prompt
|
||||
@@ -17,24 +17,31 @@ from dvc.exceptions import (
|
||||
MergeError,
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc_data import Tree
|
||||
from dvc_data import check as ocheck
|
||||
from dvc_data import load as oload
|
||||
from dvc_data.build import build
|
||||
from dvc_data.checkout import checkout
|
||||
from dvc_data.hashfile import Tree
|
||||
from dvc_data.hashfile import check as ocheck
|
||||
from dvc_data.hashfile import load as oload
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.istextfile import istextfile
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.transfer import transfer as otransfer
|
||||
from dvc_data.hashfile.transfer import transfer as otransfer
|
||||
from dvc_data.index import DataIndexEntry
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
|
||||
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
|
||||
from .fs.callbacks import DEFAULT_CALLBACK
|
||||
from .utils import relpath
|
||||
from .utils.fs import path_isin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_data.index import DataIndexKey
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
from .fs.callbacks import Callback
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -80,9 +87,10 @@ def loadd_from(stage, d_list):
|
||||
plot = d.pop(Output.PARAM_PLOT, False)
|
||||
persist = d.pop(Output.PARAM_PERSIST, False)
|
||||
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
|
||||
desc = d.pop(Output.PARAM_DESC, False)
|
||||
live = d.pop(Output.PARAM_LIVE, False)
|
||||
remote = d.pop(Output.PARAM_REMOTE, None)
|
||||
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
ret.append(
|
||||
_get(
|
||||
stage,
|
||||
@@ -93,9 +101,10 @@ def loadd_from(stage, d_list):
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
desc=desc,
|
||||
live=live,
|
||||
remote=remote,
|
||||
**annot,
|
||||
files=files,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
@@ -187,6 +196,7 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
Output.PARAM_REMOTE,
|
||||
*ANNOTATION_FIELDS,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -240,6 +250,7 @@ class Output:
|
||||
PARAM_PATH = "path"
|
||||
PARAM_CACHE = "cache"
|
||||
PARAM_CHECKPOINT = "checkpoint"
|
||||
PARAM_FILES = "files"
|
||||
PARAM_METRIC = "metric"
|
||||
PARAM_METRIC_TYPE = "type"
|
||||
PARAM_METRIC_XPATH = "xpath"
|
||||
@@ -252,7 +263,6 @@ class Output:
|
||||
PARAM_PLOT_TITLE = "title"
|
||||
PARAM_PLOT_HEADER = "header"
|
||||
PARAM_PERSIST = "persist"
|
||||
PARAM_DESC = "desc"
|
||||
PARAM_LIVE = "live"
|
||||
PARAM_LIVE_SUMMARY = "summary"
|
||||
PARAM_LIVE_HTML = "html"
|
||||
@@ -284,11 +294,29 @@ class Output:
|
||||
checkpoint=False,
|
||||
live=False,
|
||||
desc=None,
|
||||
type=None, # pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
meta=None,
|
||||
remote=None,
|
||||
repo=None,
|
||||
fs_config=None,
|
||||
files: List[Dict[str, Any]] = None,
|
||||
):
|
||||
self.annot = Annotation(
|
||||
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
||||
)
|
||||
self.repo = stage.repo if not repo and stage else repo
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(self.repo, url=path)
|
||||
meta = Meta.from_dict(info or {})
|
||||
# NOTE: when version_aware is not passed into get_cloud_fs, it will be
|
||||
# set based on whether or not path is versioned
|
||||
fs_kwargs = {"version_aware": True} if meta.version_id else {}
|
||||
|
||||
if fs_config is not None:
|
||||
fs_kwargs.update(**fs_config)
|
||||
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(
|
||||
self.repo, url=path, **fs_kwargs
|
||||
)
|
||||
self.fs = fs_cls(**fs_config)
|
||||
|
||||
if (
|
||||
@@ -320,21 +348,38 @@ class Output:
|
||||
# By resolved path, which contains actual location,
|
||||
# should be absolute and don't contain remote:// refs.
|
||||
self.stage = stage
|
||||
self.meta = Meta.from_dict(info)
|
||||
self.hash_info = HashInfo.from_dict(info)
|
||||
self.meta = meta
|
||||
self.files = files
|
||||
self.use_cache = False if self.IS_DEPENDENCY else cache
|
||||
self.metric = False if self.IS_DEPENDENCY else metric
|
||||
self.plot = False if self.IS_DEPENDENCY else plot
|
||||
self.persist = persist
|
||||
self.checkpoint = checkpoint
|
||||
self.live = live
|
||||
self.desc = desc
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj = None
|
||||
self.obj: Optional["HashFile"] = None
|
||||
|
||||
self.remote = remote
|
||||
|
||||
if self.fs.version_aware:
|
||||
_, version_id = self.fs.path.coalesce_version(
|
||||
self.def_path, self.meta.version_id
|
||||
)
|
||||
self.meta.version_id = version_id
|
||||
|
||||
if self.is_in_repo:
|
||||
self.hash_name = "md5"
|
||||
else:
|
||||
self.hash_name = self.fs.PARAM_CHECKSUM
|
||||
|
||||
self.hash_info = HashInfo(
|
||||
name=self.hash_name,
|
||||
value=getattr(self.meta, self.hash_name, None),
|
||||
)
|
||||
if self.hash_info and self.hash_info.isdir:
|
||||
self.meta.isdir = True
|
||||
|
||||
def _parse_path(self, fs, fs_path):
|
||||
parsed = urlparse(self.def_path)
|
||||
if (
|
||||
@@ -375,6 +420,12 @@ class Output:
|
||||
|
||||
return self.fs.path.relpath(self.fs_path, self.repo.root_dir)
|
||||
|
||||
def clear(self):
|
||||
self.hash_info = HashInfo.from_dict({})
|
||||
self.meta = Meta.from_dict({})
|
||||
self.obj = None
|
||||
self.files = None
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
return self.fs.protocol
|
||||
@@ -414,21 +465,27 @@ class Output:
|
||||
)
|
||||
|
||||
def get_hash(self):
|
||||
_, hash_info = self._get_hash_meta()
|
||||
return hash_info
|
||||
|
||||
def _get_hash_meta(self):
|
||||
if self.use_cache:
|
||||
odb = self.odb
|
||||
name = self.odb.fs.PARAM_CHECKSUM
|
||||
else:
|
||||
odb = self.repo.odb.local
|
||||
name = self.fs.PARAM_CHECKSUM
|
||||
_, _, obj = build(
|
||||
_, meta, obj = build(
|
||||
odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
name,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=not self.use_cache,
|
||||
)
|
||||
return obj.hash_info
|
||||
return meta, obj.hash_info
|
||||
|
||||
def get_meta(self) -> Meta:
|
||||
meta, _ = self._get_hash_meta()
|
||||
return meta
|
||||
|
||||
@property
|
||||
def is_dir_checksum(self):
|
||||
@@ -447,6 +504,34 @@ class Output:
|
||||
|
||||
return self.fs.exists(self.fs_path)
|
||||
|
||||
@cached_property
|
||||
def index_key(self) -> Tuple[str, "DataIndexKey"]:
|
||||
if self.is_in_repo:
|
||||
workspace = "repo"
|
||||
key = self.repo.fs.path.relparts(self.fs_path, self.repo.root_dir)
|
||||
else:
|
||||
workspace = self.fs.protocol
|
||||
no_drive = self.fs.path.flavour.splitdrive(self.fs_path)[1]
|
||||
key = self.fs.path.parts(no_drive)[1:]
|
||||
return workspace, key
|
||||
|
||||
def get_entry(self) -> "DataIndexEntry":
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
try:
|
||||
remote = self.repo.cloud.get_remote_odb(self.remote)
|
||||
except NoRemoteError:
|
||||
remote = None
|
||||
|
||||
return DataIndexEntry(
|
||||
meta=self.meta,
|
||||
obj=self.obj,
|
||||
hash_info=self.hash_info,
|
||||
odb=self.odb,
|
||||
cache=self.odb,
|
||||
remote=remote,
|
||||
)
|
||||
|
||||
def changed_checksum(self):
|
||||
return self.hash_info != self.get_hash()
|
||||
|
||||
@@ -464,6 +549,11 @@ class Output:
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return True
|
||||
|
||||
def changed_meta(self) -> bool:
|
||||
if self.fs.version_aware and self.meta.version_id:
|
||||
return self.meta.version_id != self.get_meta().version_id
|
||||
return False
|
||||
|
||||
def workspace_status(self):
|
||||
if not self.exists:
|
||||
return {str(self): "deleted"}
|
||||
@@ -546,11 +636,12 @@ class Output:
|
||||
self.repo.odb.local,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=True,
|
||||
)
|
||||
self.hash_info = obj.hash_info
|
||||
self.files = None
|
||||
if not self.IS_DEPENDENCY:
|
||||
logger.debug(
|
||||
"Output '%s' doesn't use cache. Skipping saving.", self
|
||||
@@ -563,18 +654,19 @@ class Output:
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.files = None
|
||||
|
||||
def set_exec(self):
|
||||
if self.isfile() and self.meta.isexec:
|
||||
self.odb.set_exec(self.fs_path)
|
||||
|
||||
def _checkout(self, *args, **kwargs):
|
||||
from dvc_data.checkout import CheckoutError as _CheckoutError
|
||||
from dvc_data.checkout import LinkError, PromptError
|
||||
from dvc_data.hashfile.checkout import CheckoutError as _CheckoutError
|
||||
from dvc_data.hashfile.checkout import LinkError, PromptError
|
||||
|
||||
kwargs.setdefault("ignore", self.dvcignore)
|
||||
try:
|
||||
@@ -605,7 +697,7 @@ class Output:
|
||||
self.odb,
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
otransfer(
|
||||
@@ -634,7 +726,7 @@ class Output:
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
save_obj = save_obj.filter(prefix)
|
||||
@@ -648,8 +740,10 @@ class Output:
|
||||
)
|
||||
return checkout_obj
|
||||
|
||||
def dumpd(self):
|
||||
ret = {**self.hash_info.to_dict(), **self.meta.to_dict()}
|
||||
def dumpd(self, **kwargs):
|
||||
meta = self.meta.to_dict()
|
||||
meta.pop("isdir", None)
|
||||
ret = {**self.hash_info.to_dict(), **meta}
|
||||
|
||||
if self.is_in_repo:
|
||||
path = self.fs.path.as_posix(
|
||||
@@ -663,9 +757,7 @@ class Output:
|
||||
if self.IS_DEPENDENCY:
|
||||
return ret
|
||||
|
||||
if self.desc:
|
||||
ret[self.PARAM_DESC] = self.desc
|
||||
|
||||
ret.update(self.annot.to_dict())
|
||||
if not self.use_cache:
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
|
||||
@@ -694,6 +786,15 @@ class Output:
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
|
||||
if self.hash_info.isdir and (
|
||||
kwargs.get("with_files") or self.files is not None
|
||||
):
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
else:
|
||||
obj = self.get_obj()
|
||||
ret[self.PARAM_FILES] = obj.as_list(with_meta=True)
|
||||
|
||||
return ret
|
||||
|
||||
def verify_metric(self):
|
||||
@@ -717,26 +818,26 @@ class Output:
|
||||
f"binary file '{self.fs_path}' cannot be used as metrics."
|
||||
)
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc.fs.callbacks import Callback
|
||||
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Downloading {self.fs.path.name(self.fs_path)}",
|
||||
unit="files",
|
||||
) as cb:
|
||||
self.fs.get(self.fs_path, to.fs_path, batch_size=jobs, callback=cb)
|
||||
|
||||
def get_obj(self, filter_info=None, **kwargs):
|
||||
def get_obj(
|
||||
self, filter_info: Optional[str] = None, **kwargs
|
||||
) -> Optional["HashFile"]:
|
||||
obj: Optional["HashFile"] = None
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
elif self.hash_info:
|
||||
try:
|
||||
obj = oload(self.odb, self.hash_info)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if self.files:
|
||||
tree = Tree.from_list(self.files, hash_name=self.hash_name)
|
||||
tree.digest()
|
||||
obj = tree
|
||||
else:
|
||||
try:
|
||||
obj = oload(self.odb, self.hash_info)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
assert obj
|
||||
fs_path = self.fs.path
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
prefix = fs_path.relparts(filter_info, self.fs_path)
|
||||
@@ -747,7 +848,7 @@ class Output:
|
||||
def checkout(
|
||||
self,
|
||||
force=False,
|
||||
progress_callback=None,
|
||||
progress_callback: "Callback" = DEFAULT_CALLBACK,
|
||||
relink=False,
|
||||
filter_info=None,
|
||||
allow_missing=False,
|
||||
@@ -755,9 +856,9 @@ class Output:
|
||||
**kwargs,
|
||||
):
|
||||
if not self.use_cache:
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
self.fs_path, self.get_files_number(filter_info)
|
||||
if progress_callback != DEFAULT_CALLBACK:
|
||||
progress_callback.relative_update(
|
||||
self.get_files_number(filter_info)
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -853,6 +954,7 @@ class Output:
|
||||
)
|
||||
|
||||
self.hash_info = obj.hash_info
|
||||
self.files = None
|
||||
return obj
|
||||
|
||||
def get_files_number(self, filter_info=None):
|
||||
@@ -923,6 +1025,7 @@ class Output:
|
||||
|
||||
obj = self.get_obj()
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
assert obj
|
||||
prefix = self.fs.path.parts(
|
||||
self.fs.path.relpath(filter_info, self.fs_path)
|
||||
)
|
||||
@@ -1015,7 +1118,7 @@ class Output:
|
||||
other = out.dumpd()
|
||||
|
||||
ignored = [
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
Meta.PARAM_SIZE,
|
||||
Meta.PARAM_NFILES,
|
||||
]
|
||||
@@ -1034,9 +1137,9 @@ class Output:
|
||||
"unable to auto-merge outputs that are not directories"
|
||||
)
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
from dvc_data.objects.tree import MergeError as TreeMergeError
|
||||
from dvc_data.objects.tree import du, merge
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
from dvc_data.hashfile.tree import MergeError as TreeMergeError
|
||||
from dvc_data.hashfile.tree import du, merge
|
||||
|
||||
assert other
|
||||
|
||||
@@ -1051,7 +1154,11 @@ class Output:
|
||||
|
||||
try:
|
||||
merged = merge(
|
||||
self.odb, ancestor_info, self.hash_info, other.hash_info
|
||||
self.odb,
|
||||
ancestor_info,
|
||||
self.hash_info,
|
||||
other.hash_info,
|
||||
allowed=allowed,
|
||||
)
|
||||
except TreeMergeError as exc:
|
||||
raise MergeError(str(exc)) from exc
|
||||
@@ -1059,6 +1166,7 @@ class Output:
|
||||
self.odb.add(merged.path, merged.fs, merged.oid)
|
||||
|
||||
self.hash_info = merged.hash_info
|
||||
self.files = None
|
||||
self.meta = Meta(
|
||||
size=du(self.odb, merged),
|
||||
nfiles=len(merged),
|
||||
@@ -1081,21 +1189,33 @@ class Output:
|
||||
return bool(self.plot) or bool(self.live)
|
||||
|
||||
|
||||
META_SCHEMA = {
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
Meta.PARAM_VERSION_ID: str,
|
||||
}
|
||||
|
||||
ARTIFACT_SCHEMA = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required(Output.PARAM_PATH): str,
|
||||
Output.PARAM_PLOT: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
}
|
||||
|
||||
DIR_FILES_SCHEMA: Dict[str, Any] = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required(Tree.PARAM_RELPATH): str,
|
||||
}
|
||||
|
||||
SCHEMA = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**ANNOTATION_SCHEMA,
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
|
||||
Output.PARAM_DESC: str,
|
||||
Output.PARAM_REMOTE: str,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ class Value(Node):
|
||||
PRIMITIVES = (int, float, str, bytes, bool)
|
||||
|
||||
|
||||
class Container(Node, ABC):
|
||||
class Container(Node, ABC): # noqa: B024
|
||||
meta: Meta
|
||||
data: Union[list, dict]
|
||||
_key_transform = staticmethod(identity)
|
||||
|
||||
@@ -2,7 +2,13 @@ import json
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from dvc.render import REVISION_FIELD, REVISIONS_KEY, SRC_FIELD, TYPE_KEY
|
||||
from dvc.render import (
|
||||
REVISION_FIELD,
|
||||
REVISIONS_KEY,
|
||||
SRC_FIELD,
|
||||
TYPE_KEY,
|
||||
VERSION_FIELD,
|
||||
)
|
||||
from dvc.render.converter.image import ImageConverter
|
||||
from dvc.render.converter.vega import VegaConverter
|
||||
|
||||
@@ -23,7 +29,7 @@ def _get_converter(
|
||||
def _group_by_rev(datapoints):
|
||||
grouped = defaultdict(list)
|
||||
for datapoint in datapoints:
|
||||
rev = datapoint.pop(REVISION_FIELD)
|
||||
rev = datapoint.get(VERSION_FIELD, {}).get("revision")
|
||||
grouped[rev].append(datapoint)
|
||||
return dict(grouped)
|
||||
|
||||
|
||||
@@ -127,13 +127,20 @@ class VegaConverter(Converter):
|
||||
),
|
||||
)
|
||||
)
|
||||
self.steps.append(
|
||||
(
|
||||
"infer_y",
|
||||
partial(
|
||||
self._infer_y,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"generate_y",
|
||||
partial(
|
||||
self._generate_y_values,
|
||||
y_values=self.plot_properties.get("y", None),
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -156,7 +163,7 @@ class VegaConverter(Converter):
|
||||
} - {None}
|
||||
self.inferred_properties["fields"] = fields
|
||||
|
||||
def _infer_y(self, datapoints: List[Dict]):
|
||||
def _infer_y(self, datapoints: List[Dict], **kwargs):
|
||||
if "y" not in self.plot_properties:
|
||||
data_fields = list(first(datapoints))
|
||||
skip = (
|
||||
@@ -178,6 +185,7 @@ class VegaConverter(Converter):
|
||||
)
|
||||
else:
|
||||
self.inferred_properties["y"] = inferred_y
|
||||
return datapoints
|
||||
|
||||
def convert(
|
||||
self,
|
||||
@@ -197,7 +205,6 @@ class VegaConverter(Converter):
|
||||
processed = deepcopy(data)
|
||||
|
||||
for step_name, step in self.steps:
|
||||
|
||||
if step_name not in skip:
|
||||
processed = step( # type: ignore
|
||||
processed,
|
||||
@@ -205,25 +212,28 @@ class VegaConverter(Converter):
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
self._infer_y(processed) # type: ignore
|
||||
return processed, {**self.plot_properties, **self.inferred_properties}
|
||||
|
||||
def _generate_y_values( # noqa: C901
|
||||
self,
|
||||
datapoints: List[Dict],
|
||||
y_values: Optional[Union[str, List, Dict]],
|
||||
revision: str,
|
||||
filename: str,
|
||||
**kwargs,
|
||||
) -> List[Dict]:
|
||||
|
||||
y_values = self.plot_properties.get(
|
||||
"y", None
|
||||
) or self.inferred_properties.get("y", None)
|
||||
|
||||
assert y_values is not None
|
||||
|
||||
result = []
|
||||
properties_update = {}
|
||||
|
||||
def _add_version_info(datapoint, version_info):
|
||||
tmp = datapoint.copy()
|
||||
tmp[VERSION_FIELD] = version_info
|
||||
tmp[REVISION_FIELD] = version_info["revision"]
|
||||
return tmp
|
||||
|
||||
def _version_info(revision, filename=None, field=None):
|
||||
@@ -250,12 +260,6 @@ class VegaConverter(Converter):
|
||||
|
||||
return tmp
|
||||
|
||||
if not y_values:
|
||||
for dp in datapoints:
|
||||
result.append(
|
||||
_add_version_info(dp, _version_info(revision, filename))
|
||||
)
|
||||
|
||||
if isinstance(y_values, str):
|
||||
for datapoint in datapoints:
|
||||
result.append(
|
||||
@@ -290,13 +294,15 @@ class VegaConverter(Converter):
|
||||
):
|
||||
# if we use the same field from all files,
|
||||
# we dont have to generate it
|
||||
y_field = all_fields.pop()
|
||||
for datapoint in datapoints:
|
||||
result.append(
|
||||
_add_version_info(
|
||||
datapoint, _version_info(revision, filename)
|
||||
datapoint,
|
||||
_version_info(revision, filename, y_field),
|
||||
)
|
||||
)
|
||||
properties_update.update({"y": all_fields.pop()})
|
||||
properties_update.update({"y": y_field})
|
||||
else:
|
||||
for def_filename, val in y_values.items():
|
||||
if isinstance(val, str):
|
||||
|
||||
@@ -67,6 +67,7 @@ class Repo:
|
||||
from dvc.repo.imp_url import imp_url # type: ignore[misc]
|
||||
from dvc.repo.install import install # type: ignore[misc]
|
||||
from dvc.repo.ls import ls as _ls # type: ignore[misc]
|
||||
from dvc.repo.ls_url import ls_url as _ls_url # type: ignore[misc]
|
||||
from dvc.repo.move import move # type: ignore[misc]
|
||||
from dvc.repo.pull import pull # type: ignore[misc]
|
||||
from dvc.repo.push import push # type: ignore[misc]
|
||||
@@ -79,6 +80,7 @@ class Repo:
|
||||
from .data import status as data_status # type: ignore[misc]
|
||||
|
||||
ls = staticmethod(_ls)
|
||||
ls_url = staticmethod(_ls_url)
|
||||
get = staticmethod(_get)
|
||||
get_url = staticmethod(_get_url)
|
||||
|
||||
@@ -186,7 +188,7 @@ class Repo:
|
||||
self.config = Config(self.dvc_dir, fs=self.fs, config=config)
|
||||
self._uninitialized = uninitialized
|
||||
|
||||
# used by DvcFileSystem to determine if it should traverse subrepos
|
||||
# used by DVCFileSystem to determine if it should traverse subrepos
|
||||
self.subrepos = subrepos
|
||||
|
||||
self.cloud = DataCloud(self)
|
||||
@@ -487,9 +489,9 @@ class Repo:
|
||||
|
||||
@cached_property
|
||||
def dvcfs(self):
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
return DvcFileSystem(
|
||||
return DVCFileSystem(
|
||||
repo=self, subrepos=self.subrepos, **self._fs_conf
|
||||
)
|
||||
|
||||
@@ -501,13 +503,13 @@ class Repo:
|
||||
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
|
||||
"""Opens a specified resource as a file descriptor"""
|
||||
from dvc.fs.data import DataFileSystem
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=self.index.data["local"])
|
||||
fs_path = path
|
||||
else:
|
||||
fs = DvcFileSystem(repo=self, subrepos=True)
|
||||
fs = DVCFileSystem(repo=self, subrepos=True)
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
try:
|
||||
|
||||
+3
-2
@@ -264,6 +264,7 @@ def create_stages(
|
||||
outs=[out],
|
||||
external=external,
|
||||
)
|
||||
if kwargs.get("desc"):
|
||||
stage.outs[0].desc = kwargs["desc"]
|
||||
|
||||
out_obj = stage.outs[0]
|
||||
out_obj.annot.update(**kwargs)
|
||||
yield stage
|
||||
|
||||
@@ -77,6 +77,7 @@ def brancher( # noqa: E302
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
for sha, names in found_revs.items():
|
||||
self.__dict__.pop("index", None)
|
||||
self.fs = GitFileSystem(scm=scm, rev=sha)
|
||||
self.root_dir = self.fs.path.join("/", *repo_root_parts)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from dvc.exceptions import (
|
||||
CheckoutErrorSuggestGit,
|
||||
NoOutputOrStageError,
|
||||
)
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils import relpath
|
||||
|
||||
from . import locked
|
||||
@@ -80,6 +79,7 @@ def checkout(
|
||||
allow_missing=False,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.fs.callbacks import Callback
|
||||
|
||||
stats = {"added": [], "deleted": [], "modified": [], "failed": []}
|
||||
if not targets:
|
||||
@@ -91,13 +91,16 @@ def checkout(
|
||||
|
||||
pairs = _collect_pairs(self, targets, with_deps, recursive)
|
||||
total = get_all_files_numbers(pairs)
|
||||
with Tqdm(
|
||||
total=total, unit="file", desc="Checkout", disable=total == 0
|
||||
) as pbar:
|
||||
with Callback.as_tqdm_callback(
|
||||
unit="file",
|
||||
desc="Checkout",
|
||||
disable=total == 0,
|
||||
) as cb:
|
||||
cb.set_size(total)
|
||||
for stage, filter_info in pairs:
|
||||
result = stage.checkout(
|
||||
force=force,
|
||||
progress_callback=pbar.update_msg,
|
||||
progress_callback=cb,
|
||||
relink=relink,
|
||||
filter_info=filter_info,
|
||||
allow_missing=allow_missing,
|
||||
|
||||
+2
-2
@@ -30,9 +30,9 @@ def _collect_paths(
|
||||
recursive: bool = False,
|
||||
rev: str = None,
|
||||
) -> StrPaths:
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
fs = DvcFileSystem(repo=repo)
|
||||
fs = DVCFileSystem(repo=repo)
|
||||
fs_paths = [fs.from_os_path(target) for target in targets]
|
||||
|
||||
target_paths: StrPaths = []
|
||||
|
||||
+136
-79
@@ -1,7 +1,21 @@
|
||||
import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict, cast
|
||||
from itertools import chain
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
cast,
|
||||
)
|
||||
|
||||
from dvc.fs.git import GitFileSystem
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -10,19 +24,20 @@ if TYPE_CHECKING:
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
|
||||
|
||||
def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
|
||||
def posixpath_to_os_path(path: str) -> str:
|
||||
return path.replace(posixpath.sep, os.path.sep)
|
||||
|
||||
|
||||
def _in_cache(obj: "HashInfo", cache: "HashFileDB") -> bool:
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
if not obj:
|
||||
return False
|
||||
if not obj.hash_info.value:
|
||||
return False
|
||||
|
||||
assert obj.value
|
||||
try:
|
||||
cache.check(obj.hash_info.value)
|
||||
cache.check(obj.value)
|
||||
return True
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return False
|
||||
@@ -30,31 +45,30 @@ def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
|
||||
|
||||
def _shallow_diff(
|
||||
root: str,
|
||||
old_obj: Optional["HashFile"],
|
||||
new_obj: Optional["HashFile"],
|
||||
old: Optional["HashInfo"],
|
||||
new: Optional["HashInfo"],
|
||||
cache: "HashFileDB",
|
||||
) -> Dict[str, List[str]]:
|
||||
# TODO: add support for shallow diff in dvc-data
|
||||
# TODO: we may want to recursively do in_cache check
|
||||
d = {}
|
||||
|
||||
from dvc_data.objects.tree import Tree
|
||||
old_root = new_root = root
|
||||
if old and old.isdir:
|
||||
old_root = os.path.sep.join([root, ""])
|
||||
if new and new.isdir:
|
||||
new_root = os.path.sep.join([root, ""])
|
||||
|
||||
if isinstance(new_obj, Tree):
|
||||
root = os.path.sep.join([root, ""])
|
||||
if old and not _in_cache(old, cache):
|
||||
d["not_in_cache"] = [old_root]
|
||||
|
||||
if not _in_cache(old_obj, cache):
|
||||
d["not_in_cache"] = [root]
|
||||
|
||||
if old_obj is None and new_obj is None:
|
||||
if not old and not new:
|
||||
return d
|
||||
if old_obj is None:
|
||||
return {"added": [root], **d}
|
||||
if new_obj is None:
|
||||
return {"deleted": [root], **d}
|
||||
if old_obj.hash_info != new_obj.hash_info:
|
||||
return {"modified": [root], **d}
|
||||
return {"unchanged": [root], **d}
|
||||
if not new:
|
||||
return {"deleted": [old_root], **d}
|
||||
if not old:
|
||||
return {"added": [new_root], **d}
|
||||
if old != new:
|
||||
return {"modified": [new_root], **d}
|
||||
return {"unchanged": [new_root], **d}
|
||||
|
||||
|
||||
def _granular_diff(
|
||||
@@ -62,19 +76,12 @@ def _granular_diff(
|
||||
old_obj: Optional["HashFile"],
|
||||
new_obj: Optional["HashFile"],
|
||||
cache: "HashFileDB",
|
||||
with_dirs: bool = False,
|
||||
) -> Dict[str, List[str]]:
|
||||
from dvc_data.diff import ROOT
|
||||
from dvc_data.diff import diff as odiff
|
||||
from dvc_data.objects.tree import Tree
|
||||
from dvc_data.hashfile.diff import ROOT
|
||||
from dvc_data.hashfile.diff import diff as odiff
|
||||
|
||||
drop_root = False
|
||||
trees = isinstance(old_obj, Tree) or isinstance(new_obj, Tree)
|
||||
if trees:
|
||||
drop_root = not with_dirs
|
||||
|
||||
def path_join(root: str, *paths: str) -> str:
|
||||
if not trees and paths == ROOT:
|
||||
def path_join(root: str, *paths: str, isdir: bool = False) -> str:
|
||||
if not isdir and paths == ROOT:
|
||||
return root
|
||||
return os.path.sep.join([root, *paths])
|
||||
|
||||
@@ -83,38 +90,46 @@ def _granular_diff(
|
||||
output: Dict[str, List[str]] = defaultdict(list)
|
||||
for state in ("added", "deleted", "modified", "unchanged"):
|
||||
items = getattr(diff_data, state)
|
||||
output[state].extend(
|
||||
path_join(root, *item.new.key)
|
||||
for item in items # pylint: disable=not-an-iterable
|
||||
if not (drop_root and item.new.key == ROOT)
|
||||
)
|
||||
# TODO: PERF: diff is checking not_in_cache for each even if we only
|
||||
# need it for the index.
|
||||
# BUG: not_in_cache file also shows up as modified in staged and
|
||||
# unstaged. We currently don't know if it is really modified.
|
||||
output["not_in_cache"].extend(
|
||||
path_join(root, *item.new.key)
|
||||
for item in items # pylint: disable=not-an-iterable
|
||||
if not item.old.in_cache
|
||||
and not (drop_root and item.new.key == ROOT)
|
||||
and state != "added"
|
||||
)
|
||||
return output
|
||||
for item in items: # pylint: disable=not-an-iterable
|
||||
entry = item.old if state == "deleted" else item.new
|
||||
isdir = entry.oid.isdir if entry.oid else False
|
||||
|
||||
path = path_join(root, *entry.key, isdir=isdir)
|
||||
output[state].append(path)
|
||||
if not item.old.in_cache and state != "added":
|
||||
output["not_in_cache"].append(path)
|
||||
return dict(output)
|
||||
|
||||
|
||||
def _get_obj_items(root: str, obj: Optional["HashFile"]) -> List[str]:
|
||||
if not obj:
|
||||
return []
|
||||
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
sep = os.path.sep
|
||||
if isinstance(obj, Tree):
|
||||
return [sep.join([root, *key]) for key, _, _ in obj]
|
||||
return [root]
|
||||
|
||||
|
||||
def _diff(
|
||||
root: str,
|
||||
old_oid: Optional["HashInfo"],
|
||||
old_obj: Optional["HashFile"],
|
||||
new_oid: Optional["HashInfo"],
|
||||
new_obj: Optional["HashFile"],
|
||||
cache: "HashFileDB",
|
||||
odb: "HashFileDB",
|
||||
granular: bool = False,
|
||||
with_dirs: bool = False,
|
||||
) -> Dict[str, List[str]]:
|
||||
if granular:
|
||||
return _granular_diff(
|
||||
root, old_obj, new_obj, cache, with_dirs=with_dirs
|
||||
)
|
||||
return _shallow_diff(root, old_obj, new_obj, cache)
|
||||
if not granular:
|
||||
return _shallow_diff(root, old_oid, new_oid, odb)
|
||||
if (old_oid and not old_obj) or (new_oid and not new_obj):
|
||||
# we don't have enough information to give full details
|
||||
unknown = _get_obj_items(root, new_obj)
|
||||
shallow_diff = _shallow_diff(root, old_oid, new_oid, odb)
|
||||
return {**shallow_diff, "unknown": unknown}
|
||||
return _granular_diff(root, old_obj, new_obj, odb)
|
||||
|
||||
|
||||
class GitInfo(TypedDict, total=False):
|
||||
@@ -141,6 +156,8 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
empty_repo = False
|
||||
|
||||
staged, unstaged, untracked = scm.status(untracked_files=untracked_files)
|
||||
if os.name == "nt":
|
||||
untracked = [posixpath_to_os_path(path) for path in untracked]
|
||||
# NOTE: order is important here.
|
||||
return GitInfo(
|
||||
staged=staged,
|
||||
@@ -152,7 +169,7 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
|
||||
|
||||
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
from dvc_data.build import build
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
unstaged_diff = defaultdict(list)
|
||||
for out in repo.index.outs:
|
||||
@@ -175,8 +192,18 @@ def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
cache = repo.odb.repo
|
||||
root = str(out)
|
||||
old = out.get_obj()
|
||||
|
||||
with ui.status(f"Calculating diff for {root} between index/workspace"):
|
||||
d = _diff(root, old, new, cache, **kwargs)
|
||||
d = _diff(
|
||||
root,
|
||||
out.hash_info,
|
||||
old,
|
||||
new.hash_info if new else None,
|
||||
new,
|
||||
cache,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
for state, items in d.items():
|
||||
if not items:
|
||||
continue
|
||||
@@ -188,7 +215,9 @@ def _diff_head_to_index(
|
||||
repo: "Repo", head: str = "HEAD", **kwargs: Any
|
||||
) -> Dict[str, List[str]]:
|
||||
# we need to store objects from index and the HEAD to diff later
|
||||
objs: Dict[str, Dict[str, "HashFile"]] = defaultdict(dict)
|
||||
objs: Dict[str, Dict[str, Tuple["HashFile", "HashInfo"]]]
|
||||
objs = defaultdict(dict)
|
||||
|
||||
staged_diff = defaultdict(list)
|
||||
for rev in repo.brancher(revs=[head]):
|
||||
for out in repo.index.outs:
|
||||
@@ -197,15 +226,20 @@ def _diff_head_to_index(
|
||||
continue
|
||||
|
||||
root = str(out)
|
||||
if isinstance(out.fs, GitFileSystem):
|
||||
root = posixpath_to_os_path(root)
|
||||
typ = "index" if rev == "workspace" else head
|
||||
objs[root][typ] = out.get_obj()
|
||||
objs[root][typ] = (out.get_obj(), out.hash_info)
|
||||
|
||||
cache = repo.odb.repo
|
||||
for root, obj_d in objs.items():
|
||||
old = obj_d.get(head, None)
|
||||
new = obj_d.get("index", None)
|
||||
old_obj, old_oid = obj_d.get(head, (None, None))
|
||||
new_obj, new_oid = obj_d.get("index", (None, None))
|
||||
with ui.status(f"Calculating diff for {root} between head/index"):
|
||||
d = _diff(root, old, new, cache, **kwargs)
|
||||
d = _diff(
|
||||
root, old_oid, old_obj, new_oid, new_obj, cache, **kwargs
|
||||
)
|
||||
|
||||
for state, items in d.items():
|
||||
if not items:
|
||||
continue
|
||||
@@ -216,28 +250,36 @@ def _diff_head_to_index(
|
||||
|
||||
class Status(TypedDict):
|
||||
not_in_cache: List[str]
|
||||
committed: Dict[str, Any]
|
||||
uncommitted: Dict[str, Any]
|
||||
committed: Dict[str, List[str]]
|
||||
uncommitted: Dict[str, List[str]]
|
||||
untracked: List[str]
|
||||
unchanged: List[str]
|
||||
git: GitInfo
|
||||
|
||||
|
||||
def _transform_git_paths_to_dvc(repo: "Repo", files: List[str]):
|
||||
def _transform_git_paths_to_dvc(
|
||||
repo: "Repo", files: Iterable[str]
|
||||
) -> List[str]:
|
||||
"""Transform files rel. to Git root to DVC root, and drop outside files."""
|
||||
rel = repo.fs.path.relpath(repo.root_dir, repo.scm.root_dir).rstrip("/")
|
||||
if rel in (os.curdir, ""):
|
||||
return files
|
||||
|
||||
prefix = rel + os.sep
|
||||
length = len(prefix)
|
||||
return [file[length:] for file in files if file.startswith(prefix)]
|
||||
# if we have repo root in a different location than scm's root,
|
||||
# i.e. subdir repo, all git_paths need to be transformed rel. to the DVC
|
||||
# repo root and anything outside need to be filtered out.
|
||||
if rel not in (os.curdir, ""):
|
||||
prefix = rel + os.sep
|
||||
length = len(prefix)
|
||||
files = (file[length:] for file in files if file.startswith(prefix))
|
||||
|
||||
start = repo.fs.path.relpath(repo.fs.path.getcwd(), repo.root_dir)
|
||||
if start in (os.curdir, ""):
|
||||
return list(files)
|
||||
# we need to convert repo relative paths to curdir relative.
|
||||
return [repo.fs.path.relpath(file, start) for file in files]
|
||||
|
||||
|
||||
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.scm import NoSCMError
|
||||
from dvc.scm import NoSCMError, SCMError
|
||||
|
||||
head = kwargs.pop("head", "HEAD")
|
||||
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
|
||||
@@ -265,3 +307,18 @@ def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
unchanged=list(unchanged),
|
||||
git=git_info,
|
||||
)
|
||||
|
||||
|
||||
def ls(
|
||||
repo: "Repo",
|
||||
targets: List[Optional[str]] = None,
|
||||
recursive: bool = False,
|
||||
) -> Iterator[Dict[str, Any]]:
|
||||
targets = targets or [None]
|
||||
pairs = chain.from_iterable(
|
||||
repo.stage.collect_granular(target, recursive=recursive)
|
||||
for target in targets
|
||||
)
|
||||
for stage, filter_info in pairs:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
yield {"path": str(out), **out.annot.to_dict()}
|
||||
|
||||
+3
-3
@@ -23,9 +23,9 @@ def diff(self, a_rev="HEAD", b_rev=None, targets=None):
|
||||
if self.scm.no_commits:
|
||||
return {}
|
||||
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
dvcfs = DvcFileSystem(repo=self)
|
||||
dvcfs = DVCFileSystem(repo=self)
|
||||
targets = ensure_list(targets)
|
||||
targets = [dvcfs.from_os_path(target) for target in targets]
|
||||
|
||||
@@ -115,7 +115,7 @@ def _paths_checksums(repo, targets):
|
||||
|
||||
def _output_paths(repo, targets):
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc_data.build import build
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
on_working_fs = isinstance(repo.fs, LocalFileSystem)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from funcy import cached_property, first
|
||||
|
||||
@@ -16,7 +16,7 @@ from .exceptions import (
|
||||
InvalidExpRefError,
|
||||
MultipleBranchError,
|
||||
)
|
||||
from .executor.base import BaseExecutor, ExecutorInfo
|
||||
from .executor.base import BaseExecutor
|
||||
from .queue.base import BaseStashQueue, QueueEntry
|
||||
from .queue.celery import LocalCeleryQueue
|
||||
from .queue.tempdir import TempDirQueue
|
||||
@@ -25,7 +25,6 @@ from .refs import (
|
||||
CELERY_FAILED_STASH,
|
||||
CELERY_STASH,
|
||||
EXEC_APPLY,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_NAMESPACE,
|
||||
@@ -211,8 +210,9 @@ class Experiments:
|
||||
|
||||
def _log_reproduced(self, revs: Iterable[str], tmp_dir: bool = False):
|
||||
names = []
|
||||
rev_names = self.get_exact_name(revs)
|
||||
for rev in revs:
|
||||
name = self.get_exact_name(rev)
|
||||
name = rev_names[rev]
|
||||
names.append(name if name else rev[:7])
|
||||
ui.write("\nRan experiment(s): {}".format(", ".join(names)))
|
||||
if tmp_dir:
|
||||
@@ -411,86 +411,41 @@ class Experiments:
|
||||
raise MultipleBranchError(rev, ref_infos)
|
||||
return str(ref_infos[0])
|
||||
|
||||
def get_exact_name(self, rev: str):
|
||||
def get_exact_name(self, revs: Iterable[str]) -> Dict[str, Optional[str]]:
|
||||
"""Returns preferred name for the specified revision.
|
||||
|
||||
Prefers tags, branches (heads), experiments in that orer.
|
||||
"""
|
||||
result: Dict[str, Optional[str]] = {}
|
||||
exclude = f"{EXEC_NAMESPACE}/*"
|
||||
ref = self.scm.describe(rev, base=EXPS_NAMESPACE, exclude=exclude)
|
||||
if ref:
|
||||
try:
|
||||
name = ExpRefInfo.from_ref(ref).name
|
||||
if name:
|
||||
return name
|
||||
except InvalidExpRefError:
|
||||
pass
|
||||
if rev in self.stash_revs:
|
||||
return self.stash_revs[rev].name
|
||||
return None
|
||||
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Any]:
|
||||
"""Return info for running experiments."""
|
||||
result = {}
|
||||
infofile = self.workspace_queue.get_infofile_path("workspace")
|
||||
result.update(
|
||||
self._fetch_running_exp("workspace", infofile, fetch_refs)
|
||||
ref_dict = self.scm.describe(
|
||||
revs, base=EXPS_NAMESPACE, exclude=exclude
|
||||
)
|
||||
for queue in (self.tempdir_queue, self.celery_queue):
|
||||
for entry in queue.iter_active():
|
||||
infofile = queue.get_infofile_path(entry.stash_rev)
|
||||
result.update(
|
||||
self._fetch_running_exp(
|
||||
entry.stash_rev, infofile, fetch_refs
|
||||
)
|
||||
)
|
||||
for rev in revs:
|
||||
name: Optional[str] = None
|
||||
ref = ref_dict[rev]
|
||||
if ref:
|
||||
try:
|
||||
name = ExpRefInfo.from_ref(ref).name
|
||||
except InvalidExpRefError:
|
||||
pass
|
||||
if not name:
|
||||
if rev in self.stash_revs:
|
||||
name = self.stash_revs[rev].name
|
||||
elif rev in self.celery_queue.failed_stash.stash_revs:
|
||||
name = self.celery_queue.failed_stash.stash_revs[rev].name
|
||||
result[rev] = name
|
||||
return result
|
||||
|
||||
def _fetch_running_exp(
|
||||
self, rev: str, infofile: str, fetch_refs: bool
|
||||
) -> Dict[str, Any]:
|
||||
from dvc.scm import InvalidRemoteSCMRepo
|
||||
from dvc.utils.serialize import load_json
|
||||
|
||||
from .executor.local import TempDirExecutor
|
||||
|
||||
result: Dict[str, Any] = {}
|
||||
try:
|
||||
info = ExecutorInfo.from_dict(load_json(infofile))
|
||||
except OSError:
|
||||
return result
|
||||
if info.result is None:
|
||||
if rev == "workspace":
|
||||
# If we are appending to a checkpoint branch in a workspace
|
||||
# run, show the latest checkpoint as running.
|
||||
last_rev = self.scm.get_ref(EXEC_BRANCH)
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
else:
|
||||
result[rev] = info.asdict()
|
||||
else:
|
||||
result[rev] = info.asdict()
|
||||
if info.git_url and fetch_refs:
|
||||
|
||||
def on_diverged(_ref: str, _checkpoint: bool):
|
||||
return False
|
||||
|
||||
executor = TempDirExecutor.from_info(info)
|
||||
try:
|
||||
for ref in executor.fetch_exps(
|
||||
self.scm,
|
||||
on_diverged=on_diverged,
|
||||
):
|
||||
logger.debug(
|
||||
"Updated running experiment '%s'.", ref
|
||||
)
|
||||
last_rev = self.scm.get_ref(ref)
|
||||
result[rev]["last"] = last_rev
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
except InvalidRemoteSCMRepo:
|
||||
# ignore stale info files
|
||||
del result[rev]
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
"""Return info for running experiments."""
|
||||
result = {}
|
||||
for queue in (
|
||||
self.workspace_queue,
|
||||
self.tempdir_queue,
|
||||
self.celery_queue,
|
||||
):
|
||||
result.update(queue.get_running_exps(fetch_refs))
|
||||
return result
|
||||
|
||||
def apply(self, *args, **kwargs):
|
||||
|
||||
@@ -4,6 +4,7 @@ import pickle
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import IntEnum
|
||||
from functools import partial
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -19,6 +20,8 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.stage.serialize import to_lockfile
|
||||
@@ -35,6 +38,8 @@ from ..refs import (
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_HEAD,
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_NAMESPACE,
|
||||
EXPS_STASH,
|
||||
@@ -47,10 +52,9 @@ if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments.stash import ExpStashEntry
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
from ..base import ExpStashEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -64,6 +68,16 @@ class ExecutorResult(NamedTuple):
|
||||
force: bool
|
||||
|
||||
|
||||
class TaskStatus(IntEnum):
|
||||
PENDING = 0
|
||||
PREPARING = 1
|
||||
RUNNING = 2
|
||||
SUCCESS = 3
|
||||
FAILED = 4
|
||||
CANCELED = 5
|
||||
FINISHED = 6
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutorInfo:
|
||||
git_url: str
|
||||
@@ -73,13 +87,16 @@ class ExecutorInfo:
|
||||
dvc_dir: str
|
||||
name: Optional[str] = None
|
||||
wdir: Optional[str] = None
|
||||
collected: bool = False
|
||||
result_hash: Optional[str] = None
|
||||
result_ref: Optional[str] = None
|
||||
result_force: bool = False
|
||||
status: TaskStatus = TaskStatus.PENDING
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
if "collected" in d:
|
||||
if d.pop("collected"):
|
||||
d["status"] = TaskStatus.FINISHED
|
||||
return cls(**d)
|
||||
|
||||
def asdict(self):
|
||||
@@ -136,6 +153,7 @@ class BaseExecutor(ABC):
|
||||
root_dir: str,
|
||||
dvc_dir: str,
|
||||
baseline_rev: str,
|
||||
status: TaskStatus,
|
||||
wdir: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
location: Optional[str] = None,
|
||||
@@ -149,9 +167,17 @@ class BaseExecutor(ABC):
|
||||
self.baseline_rev = baseline_rev
|
||||
self.location: str = location or self.DEFAULT_LOCATION
|
||||
self.result = result
|
||||
self.status = status
|
||||
|
||||
@abstractmethod
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
def init_git(
|
||||
self,
|
||||
scm: "Git",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
infofile: Optional[str],
|
||||
branch: Optional[str] = None,
|
||||
):
|
||||
"""Init git repo and populate it using exp refs from the specified
|
||||
SCM instance.
|
||||
"""
|
||||
@@ -191,6 +217,7 @@ class BaseExecutor(ABC):
|
||||
dvc_dir=self.dvc_dir,
|
||||
name=self.name,
|
||||
wdir=self.wdir,
|
||||
status=self.status,
|
||||
**result_dict,
|
||||
)
|
||||
|
||||
@@ -212,6 +239,7 @@ class BaseExecutor(ABC):
|
||||
root_dir=info.root_dir,
|
||||
dvc_dir=info.dvc_dir,
|
||||
baseline_rev=info.baseline_rev,
|
||||
status=info.status,
|
||||
name=info.name,
|
||||
wdir=info.wdir,
|
||||
result=result,
|
||||
@@ -222,7 +250,6 @@ class BaseExecutor(ABC):
|
||||
def from_stash_entry(
|
||||
cls: Type[_T],
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
**kwargs,
|
||||
) -> _T:
|
||||
@@ -232,7 +259,6 @@ class BaseExecutor(ABC):
|
||||
def _from_stash_entry(
|
||||
cls: Type[_T],
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
root_dir: str,
|
||||
**kwargs,
|
||||
@@ -241,12 +267,11 @@ class BaseExecutor(ABC):
|
||||
root_dir=root_dir,
|
||||
dvc_dir=relpath(repo.dvc_dir, repo.scm.root_dir),
|
||||
baseline_rev=entry.baseline_rev,
|
||||
status=TaskStatus.PREPARING,
|
||||
name=entry.name,
|
||||
wdir=relpath(os.getcwd(), repo.scm.root_dir),
|
||||
**kwargs,
|
||||
)
|
||||
executor.init_git(repo.scm, branch=entry.branch)
|
||||
executor.init_cache(repo, stash_rev)
|
||||
return executor
|
||||
|
||||
@staticmethod
|
||||
@@ -259,8 +284,12 @@ class BaseExecutor(ABC):
|
||||
exp_data.update(to_lockfile(stage))
|
||||
return dict_sha256(exp_data)
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
def cleanup(self, infofile: str):
|
||||
if infofile is not None:
|
||||
info = ExecutorInfo.load_json(infofile)
|
||||
if info.status < TaskStatus.FAILED:
|
||||
info.status = TaskStatus.FINISHED
|
||||
info.dump_json(infofile)
|
||||
|
||||
# TODO: come up with better way to stash repro arguments
|
||||
@staticmethod
|
||||
@@ -324,30 +353,35 @@ class BaseExecutor(ABC):
|
||||
logger.debug("Replacing existing experiment '%s'", orig_ref)
|
||||
return True
|
||||
|
||||
if on_diverged:
|
||||
return on_diverged(orig_ref, has_checkpoint)
|
||||
|
||||
self._raise_ref_conflict(
|
||||
dest_scm, orig_ref, new_rev, has_checkpoint
|
||||
)
|
||||
if on_diverged:
|
||||
on_diverged(orig_ref, has_checkpoint)
|
||||
logger.debug("Reproduced existing experiment '%s'", orig_ref)
|
||||
return False
|
||||
|
||||
# fetch experiments
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
[f"{ref}:{ref}" for ref in refs],
|
||||
on_diverged=on_diverged_ref,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
# update last run checkpoint (if it exists)
|
||||
if has_checkpoint:
|
||||
try:
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
[f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}"],
|
||||
force=True,
|
||||
[f"{ref}:{ref}" for ref in refs],
|
||||
on_diverged=on_diverged_ref,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
# update last run checkpoint (if it exists)
|
||||
if has_checkpoint:
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
[f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}"],
|
||||
force=True,
|
||||
**kwargs,
|
||||
)
|
||||
except SCMError:
|
||||
pass
|
||||
|
||||
return refs
|
||||
|
||||
@classmethod
|
||||
@@ -411,11 +445,9 @@ class BaseExecutor(ABC):
|
||||
exp_ref: Optional["ExpRefInfo"] = None
|
||||
repro_force: bool = False
|
||||
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
|
||||
with cls._repro_dvc(
|
||||
info,
|
||||
infofile,
|
||||
log_errors=log_errors,
|
||||
**kwargs,
|
||||
) as dvc:
|
||||
@@ -491,9 +523,6 @@ class BaseExecutor(ABC):
|
||||
info.result_ref = ref
|
||||
info.result_force = repro_force
|
||||
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
|
||||
# ideally we would return stages here like a normal repro() call, but
|
||||
# stages is not currently picklable and cannot be returned across
|
||||
# multiprocessing calls
|
||||
@@ -550,6 +579,7 @@ class BaseExecutor(ABC):
|
||||
def _repro_dvc(
|
||||
cls,
|
||||
info: "ExecutorInfo",
|
||||
infofile: str = None,
|
||||
log_errors: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -557,6 +587,9 @@ class BaseExecutor(ABC):
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
|
||||
info.status = TaskStatus.RUNNING
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
if cls.QUIET:
|
||||
dvc.scm_context.quiet = cls.QUIET
|
||||
old_cwd = os.getcwd()
|
||||
@@ -568,15 +601,28 @@ class BaseExecutor(ABC):
|
||||
try:
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
info.status = TaskStatus.SUCCESS
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
|
||||
except CheckpointKilledError:
|
||||
info.status = TaskStatus.FAILED
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
raise
|
||||
except DvcException:
|
||||
if log_errors:
|
||||
logger.exception("")
|
||||
info.status = TaskStatus.FAILED
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
raise
|
||||
except Exception:
|
||||
if log_errors:
|
||||
logger.exception("unexpected error")
|
||||
info.status = TaskStatus.FAILED
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
raise
|
||||
finally:
|
||||
dvc.close()
|
||||
@@ -714,3 +760,23 @@ class BaseExecutor(ABC):
|
||||
disable_other_loggers()
|
||||
if level is not None:
|
||||
dvc_logger.setLevel(level)
|
||||
|
||||
@contextmanager
|
||||
def set_exec_refs(
|
||||
self, scm: "Git", stash_rev: str, entry: "ExpStashEntry"
|
||||
):
|
||||
try:
|
||||
# Executor will be initialized with an empty git repo that
|
||||
# we populate by pushing:
|
||||
# EXEC_HEAD - the base commit for this experiment
|
||||
# EXEC_MERGE - the unmerged changes (from our stash)
|
||||
# to be reproduced
|
||||
# EXEC_BASELINE - the baseline commit for this experiment
|
||||
scm.set_ref(EXEC_HEAD, entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
|
||||
yield
|
||||
finally:
|
||||
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
|
||||
if scm.get_ref(ref):
|
||||
scm.remove_ref(ref)
|
||||
|
||||
@@ -19,7 +19,7 @@ from ..refs import (
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
)
|
||||
from .base import EXEC_TMP_DIR, BaseExecutor
|
||||
from .base import EXEC_TMP_DIR, BaseExecutor, TaskStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
@@ -46,8 +46,8 @@ class BaseLocalExecutor(BaseExecutor):
|
||||
def scm(self):
|
||||
return SCM(self.root_dir)
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
def cleanup(self, infofile: str):
|
||||
super().cleanup(infofile)
|
||||
self.scm.close()
|
||||
del self.scm
|
||||
|
||||
@@ -67,15 +67,28 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
QUIET = True
|
||||
DEFAULT_LOCATION = "tempdir"
|
||||
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
def init_git(
|
||||
self,
|
||||
scm: "Git",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
infofile: Optional[str],
|
||||
branch: Optional[str] = None,
|
||||
):
|
||||
from dulwich.repo import Repo as DulwichRepo
|
||||
|
||||
from ..utils import push_refspec
|
||||
|
||||
DulwichRepo.init(os.fspath(self.root_dir))
|
||||
|
||||
refspec = f"{EXEC_NAMESPACE}/"
|
||||
push_refspec(scm, self.git_url, refspec, refspec)
|
||||
self.status = TaskStatus.PREPARING
|
||||
if infofile:
|
||||
self.info.dump_json(infofile)
|
||||
|
||||
with self.set_exec_refs(scm, stash_rev, entry):
|
||||
refspec = f"{EXEC_NAMESPACE}/"
|
||||
push_refspec(scm, self.git_url, refspec, refspec)
|
||||
|
||||
if branch:
|
||||
push_refspec(scm, self.git_url, branch, branch)
|
||||
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
|
||||
@@ -84,17 +97,12 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
|
||||
if self.scm.get_ref(EXEC_CHECKPOINT):
|
||||
self.scm.remove_ref(EXEC_CHECKPOINT)
|
||||
|
||||
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
|
||||
# committing
|
||||
head = EXEC_BRANCH if branch else EXEC_HEAD
|
||||
self.scm.checkout(head, detach=True)
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
|
||||
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
|
||||
if scm.get_ref(ref):
|
||||
scm.remove_ref(ref)
|
||||
|
||||
try:
|
||||
self.scm.merge(merge_rev, squash=True, commit=False)
|
||||
except _SCMError as exc:
|
||||
@@ -114,8 +122,8 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
"""Initialize DVC cache."""
|
||||
self._config(repo.odb.repo.path)
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
def cleanup(self, infofile: str):
|
||||
super().cleanup(infofile)
|
||||
logger.debug("Removing tmpdir '%s'", self.root_dir)
|
||||
remove(self.root_dir)
|
||||
|
||||
@@ -123,7 +131,6 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
def from_stash_entry(
|
||||
cls,
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
wdir: Optional[str] = None,
|
||||
**kwargs,
|
||||
@@ -132,9 +139,7 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
makedirs(parent_dir, exist_ok=True)
|
||||
tmp_dir = mkdtemp(dir=parent_dir)
|
||||
try:
|
||||
executor = cls._from_stash_entry(
|
||||
repo, stash_rev, entry, tmp_dir, **kwargs
|
||||
)
|
||||
executor = cls._from_stash_entry(repo, entry, tmp_dir, **kwargs)
|
||||
logger.debug("Init temp dir executor in '%s'", tmp_dir)
|
||||
return executor
|
||||
except Exception:
|
||||
@@ -152,18 +157,31 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
def from_stash_entry(
|
||||
cls,
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
**kwargs,
|
||||
):
|
||||
root_dir = repo.scm.root_dir
|
||||
executor = cls._from_stash_entry(
|
||||
repo, stash_rev, entry, root_dir, **kwargs
|
||||
executor: "WorkspaceExecutor" = cls._from_stash_entry(
|
||||
repo, entry, root_dir, **kwargs
|
||||
)
|
||||
logger.debug("Init workspace executor in '%s'", root_dir)
|
||||
return executor
|
||||
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
def init_git(
|
||||
self,
|
||||
scm: "Git",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
infofile: Optional[str],
|
||||
branch: Optional[str] = None,
|
||||
):
|
||||
self.status = TaskStatus.PREPARING
|
||||
if infofile:
|
||||
self.info.dump_json(infofile)
|
||||
|
||||
scm.set_ref(EXEC_HEAD, entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
|
||||
self._detach_stack.enter_context(
|
||||
self.scm.detach_head(
|
||||
self.scm.get_ref(EXEC_HEAD),
|
||||
@@ -184,7 +202,9 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
def init_cache(self, repo: "Repo", rev: str, run_cache: bool = True):
|
||||
pass
|
||||
|
||||
def cleanup(self):
|
||||
def cleanup(self, infofile: str):
|
||||
super().cleanup(infofile)
|
||||
remove(os.path.dirname(infofile))
|
||||
with self._detach_stack:
|
||||
self.scm.remove_ref(EXEC_BASELINE)
|
||||
self.scm.remove_ref(EXEC_MERGE)
|
||||
@@ -193,4 +213,3 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
|
||||
if checkpoint and checkpoint != self._orig_checkpoint:
|
||||
self.scm.set_ref(EXEC_APPLY, checkpoint)
|
||||
super().cleanup()
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from abc import ABC
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Deque, Dict, Generator, Optional, Tuple, Type
|
||||
|
||||
from dvc.proc.manager import ProcessManager
|
||||
|
||||
from ...exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
from ...refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
|
||||
from ...stash import ExpStashEntry
|
||||
from ..base import EXEC_PID_DIR, BaseExecutor
|
||||
from ..local import TempDirExecutor, WorkspaceExecutor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseExecutorManager(ABC, Mapping):
|
||||
"""Manages executors for a collection of experiments to be run."""
|
||||
|
||||
EXECUTOR_CLS: Type = BaseExecutor
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
self.scm = scm
|
||||
makedirs(wdir, exist_ok=True)
|
||||
self.wdir = wdir
|
||||
self.proc = ProcessManager(self.pid_dir)
|
||||
self._attached: Dict[str, "BaseExecutor"] = {}
|
||||
self._detached: Dict[str, "BaseExecutor"] = dict(self._load_infos())
|
||||
self._queue: Deque[Tuple[str, "BaseExecutor"]] = deque()
|
||||
|
||||
def __getitem__(self, key: str) -> "BaseExecutor":
|
||||
try:
|
||||
return self._attached[key]
|
||||
except KeyError:
|
||||
pass
|
||||
return self._detached[key]
|
||||
|
||||
def __iter__(self):
|
||||
yield from self._attached
|
||||
yield from self._detached
|
||||
|
||||
def __len__(self):
|
||||
return len(self._attached) + len(self._detached)
|
||||
|
||||
@property
|
||||
def pid_dir(self) -> str:
|
||||
return os.path.join(self.wdir, EXEC_PID_DIR)
|
||||
|
||||
def enqueue(self, rev: str, executor: "BaseExecutor"):
|
||||
assert rev not in self
|
||||
self._queue.append((rev, executor))
|
||||
|
||||
def _load_infos(self) -> Generator[Tuple[str, "BaseExecutor"], None, None]:
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ..base import ExecutorInfo
|
||||
from ..ssh import SSHExecutor
|
||||
|
||||
def make_executor(info: "ExecutorInfo"):
|
||||
if info.git_url:
|
||||
scheme = urlparse(info.git_url).scheme
|
||||
if scheme == "file":
|
||||
cls: Type = TempDirExecutor
|
||||
elif scheme == "ssh":
|
||||
cls = SSHExecutor
|
||||
else:
|
||||
raise NotImplementedError
|
||||
else:
|
||||
cls = WorkspaceExecutor
|
||||
return cls.from_info(info)
|
||||
|
||||
for name in self.proc:
|
||||
infofile = self.get_infofile_path(name)
|
||||
try:
|
||||
with open(infofile, encoding="utf-8") as fobj:
|
||||
info = ExecutorInfo.from_dict(json.load(fobj))
|
||||
yield name, make_executor(info)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
def get_infofile_path(self, name: str) -> str:
|
||||
return os.path.join(
|
||||
self.pid_dir,
|
||||
name,
|
||||
f"{name}{BaseExecutor.INFOFILE_EXT}",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_stash_entries(
|
||||
cls,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
repo: "Repo",
|
||||
to_run: Dict[str, ExpStashEntry],
|
||||
**kwargs,
|
||||
):
|
||||
manager = cls(scm, wdir)
|
||||
manager._enqueue_stash_entries(scm, repo, to_run, **kwargs)
|
||||
return manager
|
||||
|
||||
def _enqueue_stash_entries(
|
||||
self,
|
||||
scm: "Git",
|
||||
repo: "Repo",
|
||||
to_run: Dict[str, ExpStashEntry],
|
||||
**kwargs,
|
||||
):
|
||||
try:
|
||||
for stash_rev, entry in to_run.items():
|
||||
scm.set_ref(EXEC_HEAD, entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
|
||||
|
||||
# Executor will be initialized with an empty git repo that
|
||||
# we populate by pushing:
|
||||
# EXEC_HEAD - the base commit for this experiment
|
||||
# EXEC_MERGE - the unmerged changes (from our stash)
|
||||
# to be reproduced
|
||||
# EXEC_BASELINE - the baseline commit for this experiment
|
||||
executor = self.EXECUTOR_CLS.from_stash_entry(
|
||||
repo,
|
||||
stash_rev,
|
||||
entry,
|
||||
**kwargs,
|
||||
)
|
||||
self.enqueue(stash_rev, executor)
|
||||
finally:
|
||||
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
|
||||
scm.remove_ref(ref)
|
||||
|
||||
def exec_queue(
|
||||
self, repo: "Repo", jobs: Optional[int] = 1, detach: bool = False
|
||||
):
|
||||
"""Run dvc repro for queued executors in parallel."""
|
||||
if detach:
|
||||
raise NotImplementedError
|
||||
# TODO use ProcessManager.spawn() to support detached runs
|
||||
return self._exec_attached(repo, jobs=jobs)
|
||||
|
||||
def _exec_attached(self, repo: "Repo", jobs: Optional[int] = 1):
|
||||
import signal
|
||||
from concurrent.futures import (
|
||||
CancelledError,
|
||||
ProcessPoolExecutor,
|
||||
wait,
|
||||
)
|
||||
from multiprocessing import Manager
|
||||
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
result: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
|
||||
manager = Manager()
|
||||
pid_q = manager.Queue()
|
||||
|
||||
with ProcessPoolExecutor(max_workers=jobs) as workers:
|
||||
futures = {}
|
||||
while self._queue:
|
||||
rev, executor = self._queue.popleft()
|
||||
infofile = self.get_infofile_path(rev)
|
||||
future = workers.submit(
|
||||
executor.reproduce,
|
||||
info=executor.info,
|
||||
rev=rev,
|
||||
queue=pid_q,
|
||||
infofile=infofile,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
)
|
||||
futures[future] = (rev, executor)
|
||||
self._attached[rev] = executor
|
||||
|
||||
try:
|
||||
wait(futures)
|
||||
except KeyboardInterrupt:
|
||||
# forward SIGINT to any running executor processes and
|
||||
# cancel any remaining futures
|
||||
workers.shutdown(wait=False)
|
||||
pids = {}
|
||||
for future, (rev, _) in futures.items():
|
||||
if future.running():
|
||||
# if future has already been started by the scheduler
|
||||
# we still have to wait until it tells us its PID
|
||||
while rev not in pids:
|
||||
rev, pid = pid_q.get()
|
||||
pids[rev] = pid
|
||||
os.kill(pids[rev], signal.SIGINT)
|
||||
elif not future.done():
|
||||
future.cancel()
|
||||
|
||||
for future, (rev, executor) in futures.items():
|
||||
rev, executor = futures[future]
|
||||
|
||||
try:
|
||||
exc = future.exception()
|
||||
if exc is None:
|
||||
exec_result = future.result()
|
||||
result[rev].update(
|
||||
self._collect_executor(repo, executor, exec_result)
|
||||
)
|
||||
elif not isinstance(exc, CheckpointKilledError):
|
||||
logger.error(
|
||||
"Failed to reproduce experiment '%s'", rev[:7]
|
||||
)
|
||||
except CancelledError:
|
||||
logger.error(
|
||||
"Cancelled before attempting to reproduce experiment "
|
||||
"'%s'",
|
||||
rev[:7],
|
||||
)
|
||||
finally:
|
||||
self.cleanup_executor(rev, executor)
|
||||
|
||||
return result
|
||||
|
||||
def _collect_executor(self, repo, executor, exec_result) -> Dict[str, str]:
|
||||
# NOTE: GitPython Repo instances cannot be re-used
|
||||
# after process has received SIGINT or SIGTERM, so we
|
||||
# need this hack to re-instantiate git instances after
|
||||
# checkpoint runs. See:
|
||||
# https://github.com/gitpython-developers/GitPython/issues/427
|
||||
# del self.repo.scm
|
||||
|
||||
results = {}
|
||||
|
||||
def on_diverged(ref: str, checkpoint: bool):
|
||||
ref_info = ExpRefInfo.from_ref(ref)
|
||||
if checkpoint:
|
||||
raise CheckpointExistsError(ref_info.name)
|
||||
raise ExperimentExistsError(ref_info.name)
|
||||
|
||||
for ref in executor.fetch_exps(
|
||||
self.scm,
|
||||
force=exec_result.force,
|
||||
on_diverged=on_diverged,
|
||||
):
|
||||
exp_rev = self.scm.get_ref(ref)
|
||||
if exp_rev:
|
||||
logger.debug("Collected experiment '%s'.", exp_rev[:7])
|
||||
results[exp_rev] = exec_result.exp_hash
|
||||
|
||||
if exec_result.ref_info is not None:
|
||||
executor.collect_cache(repo, exec_result.ref_info)
|
||||
|
||||
return results
|
||||
|
||||
def cleanup_executor(self, rev: str, executor: "BaseExecutor"):
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
executor.cleanup()
|
||||
try:
|
||||
self.proc.remove(rev)
|
||||
except KeyError:
|
||||
pass
|
||||
remove(os.path.join(self.pid_dir, rev))
|
||||
@@ -1,107 +0,0 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from ...refs import EXEC_BASELINE, EXEC_BRANCH, EXEC_HEAD, EXEC_MERGE
|
||||
from ...stash import ExpStashEntry
|
||||
from ..local import TempDirExecutor, WorkspaceExecutor
|
||||
from .base import BaseExecutorManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TempDirExecutorManager(BaseExecutorManager):
|
||||
EXECUTOR_CLS = TempDirExecutor
|
||||
|
||||
|
||||
class WorkspaceExecutorManager(BaseExecutorManager):
|
||||
EXECUTOR_CLS = WorkspaceExecutor
|
||||
|
||||
@classmethod
|
||||
def from_stash_entries(
|
||||
cls,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
repo: "Repo",
|
||||
to_run: Dict[str, ExpStashEntry],
|
||||
**kwargs,
|
||||
):
|
||||
manager = cls(scm, wdir)
|
||||
try:
|
||||
assert len(to_run) == 1
|
||||
for stash_rev, entry in to_run.items():
|
||||
scm.set_ref(EXEC_HEAD, entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
|
||||
|
||||
executor = cls.EXECUTOR_CLS.from_stash_entry(
|
||||
repo,
|
||||
stash_rev,
|
||||
entry,
|
||||
**kwargs,
|
||||
)
|
||||
manager.enqueue(stash_rev, executor)
|
||||
finally:
|
||||
for ref in (EXEC_MERGE,):
|
||||
scm.remove_ref(ref)
|
||||
return manager
|
||||
|
||||
def _collect_executor(self, repo, executor, exec_result) -> Dict[str, str]:
|
||||
results = {}
|
||||
exp_rev = self.scm.get_ref(EXEC_BRANCH)
|
||||
if exp_rev:
|
||||
logger.debug("Collected experiment '%s'.", exp_rev[:7])
|
||||
results[exp_rev] = exec_result.exp_hash
|
||||
return results
|
||||
|
||||
def exec_queue(
|
||||
self, repo: "Repo", jobs: Optional[int] = 1, detach: bool = False
|
||||
):
|
||||
"""Run a single WorkspaceExecutor.
|
||||
|
||||
Workspace execution is done within the main DVC process
|
||||
(rather than in multiprocessing context)
|
||||
"""
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
assert len(self._queue) == 1
|
||||
assert not detach
|
||||
result: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
rev, executor = self._queue.popleft()
|
||||
|
||||
exec_name = "workspace"
|
||||
infofile = self.get_infofile_path(exec_name)
|
||||
try:
|
||||
exec_result = executor.reproduce(
|
||||
info=executor.info,
|
||||
rev=rev,
|
||||
infofile=infofile,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
log_errors=not isinstance(executor, WorkspaceExecutor),
|
||||
)
|
||||
if not exec_result.exp_hash:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
)
|
||||
if exec_result.ref_info:
|
||||
result[rev].update(
|
||||
self._collect_executor(repo, executor, exec_result)
|
||||
)
|
||||
except CheckpointKilledError:
|
||||
# Checkpoint errors have already been logged
|
||||
return {}
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
) from exc
|
||||
finally:
|
||||
self.cleanup_executor(exec_name, executor)
|
||||
return result
|
||||
@@ -1,109 +0,0 @@
|
||||
import logging
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Generator, Optional, Tuple
|
||||
|
||||
from ...stash import ExpStashEntry
|
||||
from ..base import BaseExecutor
|
||||
from ..ssh import SSHExecutor, _sshfs
|
||||
from .base import BaseExecutorManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SSHExecutorManager(BaseExecutorManager):
|
||||
EXECUTOR_CLS = SSHExecutor
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
username: Optional[str] = None,
|
||||
fs_factory: Optional[Callable] = None,
|
||||
**kwargs,
|
||||
):
|
||||
assert host
|
||||
super().__init__(scm, wdir, **kwargs)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.username = username
|
||||
self._fs_factory = fs_factory
|
||||
|
||||
def _load_infos(self) -> Generator[Tuple[str, "BaseExecutor"], None, None]:
|
||||
# TODO: load existing infos using sshfs
|
||||
yield from []
|
||||
|
||||
@classmethod
|
||||
def from_stash_entries(
|
||||
cls,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
repo: "Repo",
|
||||
to_run: Dict[str, ExpStashEntry],
|
||||
**kwargs,
|
||||
):
|
||||
machine_name: Optional[str] = kwargs.get("machine_name", None)
|
||||
manager = cls(
|
||||
scm, wdir, **repo.machine.get_executor_kwargs(machine_name)
|
||||
)
|
||||
manager._enqueue_stash_entries(scm, repo, to_run, **kwargs)
|
||||
return manager
|
||||
|
||||
def sshfs(self):
|
||||
return _sshfs(self._fs_factory, host=self.host, port=self.port)
|
||||
|
||||
def get_infofile_path(self, name: str) -> str:
|
||||
return f"{name}{BaseExecutor.INFOFILE_EXT}"
|
||||
|
||||
def _exec_attached(self, repo: "Repo", jobs: Optional[int] = 1):
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
assert len(self._queue) == 1
|
||||
result: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
rev, executor = self._queue.popleft()
|
||||
info = executor.info
|
||||
infofile = posixpath.join(
|
||||
info.root_dir,
|
||||
info.dvc_dir,
|
||||
"tmp",
|
||||
self.get_infofile_path(rev),
|
||||
)
|
||||
try:
|
||||
exec_result = executor.reproduce(
|
||||
info=executor.info,
|
||||
rev=rev,
|
||||
infofile=infofile,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
fs_factory=self._fs_factory,
|
||||
)
|
||||
if not exec_result.exp_hash:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
)
|
||||
if exec_result.ref_info:
|
||||
result[rev].update(
|
||||
self._collect_executor(repo, executor, exec_result)
|
||||
)
|
||||
except CheckpointKilledError:
|
||||
# Checkpoint errors have already been logged
|
||||
return {}
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
) from exc
|
||||
finally:
|
||||
self.cleanup_executor(rev, executor)
|
||||
return result
|
||||
|
||||
def cleanup_executor(self, rev: str, executor: "BaseExecutor"):
|
||||
executor.cleanup()
|
||||
@@ -15,7 +15,7 @@ from ..refs import (
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
)
|
||||
from .base import BaseExecutor, ExecutorInfo, ExecutorResult
|
||||
from .base import BaseExecutor, ExecutorInfo, ExecutorResult, TaskStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from queue import Queue
|
||||
@@ -23,8 +23,8 @@ if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
from ..base import ExpRefInfo, ExpStashEntry
|
||||
from dvc.repo.experiments.refs import ExpRefInfo
|
||||
from dvc.repo.experiments.stash import ExpStashEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -75,14 +75,12 @@ class SSHExecutor(BaseExecutor):
|
||||
def from_stash_entry(
|
||||
cls,
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
**kwargs,
|
||||
):
|
||||
machine_name: Optional[str] = kwargs.pop("machine_name", None)
|
||||
executor = cls._from_stash_entry(
|
||||
repo,
|
||||
stash_rev,
|
||||
entry,
|
||||
cls.gen_dirname(entry.name),
|
||||
location=machine_name,
|
||||
@@ -119,9 +117,20 @@ class SSHExecutor(BaseExecutor):
|
||||
}
|
||||
return kwargs
|
||||
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
def init_git(
|
||||
self,
|
||||
scm: "Git",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
infofile: Optional[str],
|
||||
branch: Optional[str] = None,
|
||||
):
|
||||
from ..utils import push_refspec
|
||||
|
||||
self.status = TaskStatus.PREPARING
|
||||
if infofile:
|
||||
self.info.dump_json(infofile)
|
||||
|
||||
with self.sshfs() as fs:
|
||||
fs.makedirs(self.root_dir)
|
||||
self._ssh_cmd(fs, "git init .")
|
||||
@@ -137,8 +146,11 @@ class SSHExecutor(BaseExecutor):
|
||||
# TODO: support multiple client key retries in git backends
|
||||
# (see https://github.com/iterative/dvc/issues/6508)
|
||||
kwargs = self._git_client_args(fs)
|
||||
refspec = f"{EXEC_NAMESPACE}/"
|
||||
push_refspec(scm, self.git_url, refspec, refspec, **kwargs)
|
||||
|
||||
with self.set_exec_refs(scm, stash_rev, entry):
|
||||
refspec = f"{EXEC_NAMESPACE}/"
|
||||
push_refspec(scm, self.git_url, refspec, refspec, **kwargs)
|
||||
|
||||
if branch:
|
||||
push_refspec(scm, self.git_url, branch, branch, **kwargs)
|
||||
self._ssh_cmd(fs, f"git symbolic-ref {EXEC_BRANCH} {branch}")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import (
|
||||
@@ -17,10 +18,12 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import cached_property
|
||||
from funcy import cached_property, retry
|
||||
|
||||
from dvc.dependency import ParamsDependency
|
||||
from dvc.env import DVCLIVE_RESUME
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.lock import LockError
|
||||
from dvc.ui import ui
|
||||
|
||||
from ..exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
@@ -31,7 +34,7 @@ from ..executor.base import (
|
||||
ExecutorResult,
|
||||
)
|
||||
from ..executor.local import WorkspaceExecutor
|
||||
from ..refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
|
||||
from ..refs import ExpRefInfo
|
||||
from ..stash import ExpStash, ExpStashEntry
|
||||
from ..utils import exp_refs_by_rev, scm_locked
|
||||
|
||||
@@ -520,14 +523,36 @@ class BaseStashQueue(ABC):
|
||||
provided via `exp run --set-param`.
|
||||
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/next/advanced/override_grammar/basic/
|
||||
https://hydra.cc/docs/advanced/override_grammar/basic/
|
||||
"""
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
|
||||
from dvc.utils.hydra import apply_overrides
|
||||
try:
|
||||
from dvc.utils.hydra import apply_overrides, compose_and_dump
|
||||
except ValueError:
|
||||
if sys.version_info >= (3, 11):
|
||||
raise DvcException(
|
||||
"--set-param is not supported in Python >= 3.11"
|
||||
)
|
||||
raise
|
||||
|
||||
hydra_config = self.repo.config.get("hydra", {})
|
||||
hydra_enabled = hydra_config.get("enabled", False)
|
||||
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
for path, overrides in params.items():
|
||||
apply_overrides(path, overrides)
|
||||
if hydra_enabled and path == hydra_output_file:
|
||||
config_dir = os.path.join(
|
||||
self.repo.root_dir, hydra_config.get("config_dir", "conf")
|
||||
)
|
||||
config_name = hydra_config.get("config_name", "config")
|
||||
compose_and_dump(
|
||||
path,
|
||||
config_dir,
|
||||
config_name,
|
||||
overrides,
|
||||
)
|
||||
else:
|
||||
apply_overrides(path, overrides)
|
||||
|
||||
# Force params file changes to be staged in git
|
||||
# Otherwise in certain situations the changes to params file may be
|
||||
@@ -536,8 +561,9 @@ class BaseStashQueue(ABC):
|
||||
self.scm.add(list(params.keys()))
|
||||
|
||||
@staticmethod
|
||||
@retry(180, errors=LockError, timeout=1)
|
||||
@scm_locked
|
||||
def setup_executor(
|
||||
def init_executor(
|
||||
exp: "Experiments",
|
||||
queue_entry: QueueEntry,
|
||||
executor_cls: Type[BaseExecutor] = WorkspaceExecutor,
|
||||
@@ -552,21 +578,23 @@ class BaseStashQueue(ABC):
|
||||
)
|
||||
if stash_entry.stash_index is not None:
|
||||
stash.drop(stash_entry.stash_index)
|
||||
|
||||
scm.set_ref(EXEC_HEAD, stash_entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, stash_entry.baseline_rev)
|
||||
|
||||
# Executor will be initialized with an empty git repo that
|
||||
# we populate by pushing:
|
||||
# EXEC_HEAD - the base commit for this experiment
|
||||
# EXEC_MERGE - the unmerged changes (from our stash)
|
||||
# to be reproduced
|
||||
# EXEC_BASELINE - the baseline commit for this experiment
|
||||
return executor_cls.from_stash_entry(
|
||||
exp.repo, stash_rev, stash_entry, **kwargs
|
||||
executor = executor_cls.from_stash_entry(
|
||||
exp.repo, stash_entry, **kwargs
|
||||
)
|
||||
|
||||
infofile = exp.celery_queue.get_infofile_path(stash_rev)
|
||||
executor.init_git(
|
||||
exp.repo.scm,
|
||||
stash_rev,
|
||||
stash_entry,
|
||||
infofile,
|
||||
branch=stash_entry.branch,
|
||||
)
|
||||
|
||||
executor.init_cache(exp.repo, stash_rev)
|
||||
|
||||
return executor
|
||||
|
||||
def get_infofile_path(self, name: str) -> str:
|
||||
return os.path.join(
|
||||
self.pid_dir,
|
||||
@@ -575,8 +603,9 @@ class BaseStashQueue(ABC):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@retry(180, errors=LockError, timeout=1)
|
||||
@scm_locked
|
||||
def collect_executor(
|
||||
def collect_git(
|
||||
exp: "Experiments",
|
||||
executor: BaseExecutor,
|
||||
exec_result: ExecutorResult,
|
||||
@@ -600,6 +629,17 @@ class BaseStashQueue(ABC):
|
||||
logger.debug("Collected experiment '%s'.", exp_rev[:7])
|
||||
results[exp_rev] = exec_result.exp_hash
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def collect_executor(
|
||||
cls,
|
||||
exp: "Experiments",
|
||||
executor: BaseExecutor,
|
||||
exec_result: ExecutorResult,
|
||||
) -> Dict[str, str]:
|
||||
results = cls.collect_git(exp, executor, exec_result)
|
||||
|
||||
if exec_result.ref_info is not None:
|
||||
executor.collect_cache(exp.repo, exec_result.ref_info)
|
||||
|
||||
@@ -666,3 +706,11 @@ class BaseStashQueue(ABC):
|
||||
entry.stash_rev,
|
||||
message=f"commit: {msg}",
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
"""Get the execution info of the currently running experiments
|
||||
|
||||
Args:
|
||||
fetch_ref (bool): fetch completed checkpoints or not.
|
||||
"""
|
||||
|
||||
@@ -23,9 +23,15 @@ from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
from ..exceptions import UnresolvedQueueExpNamesError
|
||||
from ..executor.base import EXEC_TMP_DIR, ExecutorInfo, ExecutorResult
|
||||
from ..executor.base import (
|
||||
EXEC_TMP_DIR,
|
||||
ExecutorInfo,
|
||||
ExecutorResult,
|
||||
TaskStatus,
|
||||
)
|
||||
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
|
||||
from .tasks import run_exp
|
||||
from .utils import fetch_running_exp_from_temp_dir
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_task.app import FSApp
|
||||
@@ -248,7 +254,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
|
||||
def _load_collected(rev: str) -> Optional[ExecutorResult]:
|
||||
executor_info = _load_info(rev)
|
||||
if executor_info.collected:
|
||||
if executor_info.status > TaskStatus.SUCCESS:
|
||||
return executor_info.result
|
||||
raise FileNotFoundError
|
||||
|
||||
@@ -368,3 +374,18 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
from .remove import celery_remove
|
||||
|
||||
return celery_remove(self, *args, **kwargs)
|
||||
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
"""Get the execution info of the currently running experiments
|
||||
|
||||
Args:
|
||||
fetch_ref (bool): fetch completed checkpoints or not.
|
||||
"""
|
||||
result: Dict[str, Dict] = {}
|
||||
for entry in self.iter_active():
|
||||
result.update(
|
||||
fetch_running_exp_from_temp_dir(
|
||||
self, entry.stash_rev, fetch_refs
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -3,8 +3,6 @@ from typing import Any, Dict
|
||||
from celery import shared_task
|
||||
from celery.utils.log import get_task_logger
|
||||
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
from ..executor.base import ExecutorInfo
|
||||
from ..executor.local import TempDirExecutor
|
||||
from .base import BaseStashQueue, QueueEntry
|
||||
@@ -13,7 +11,7 @@ logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
@shared_task
|
||||
def setup_exp(entry_dict: Dict[str, Any]) -> str:
|
||||
def setup_exp(entry_dict: Dict[str, Any]) -> TempDirExecutor:
|
||||
"""Setup an experiment.
|
||||
|
||||
Arguments:
|
||||
@@ -28,7 +26,7 @@ def setup_exp(entry_dict: Dict[str, Any]) -> str:
|
||||
repo = Repo(entry.dvc_root)
|
||||
# TODO: split executor.init_cache into separate subtask - we can release
|
||||
# exp.scm_lock before DVC push
|
||||
executor = BaseStashQueue.setup_executor(
|
||||
executor = BaseStashQueue.init_executor(
|
||||
repo.experiments,
|
||||
entry,
|
||||
TempDirExecutor,
|
||||
@@ -36,7 +34,7 @@ def setup_exp(entry_dict: Dict[str, Any]) -> str:
|
||||
)
|
||||
infofile = repo.experiments.celery_queue.get_infofile_path(entry.stash_rev)
|
||||
executor.info.dump_json(infofile)
|
||||
return executor.root_dir
|
||||
return executor
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -79,25 +77,14 @@ def collect_exp(
|
||||
|
||||
|
||||
@shared_task
|
||||
def cleanup_exp( # pylint: disable=unused-argument
|
||||
tmp_dir: str, entry_dict: Dict[str, Any]
|
||||
) -> None:
|
||||
def cleanup_exp(executor: TempDirExecutor, infofile: str) -> None:
|
||||
"""Cleanup after an experiment.
|
||||
|
||||
Arguments:
|
||||
tmp_dir: Temp directory to be removed.
|
||||
entry_dict: Serialized QueueEntry for this experiment.
|
||||
"""
|
||||
remove(tmp_dir)
|
||||
|
||||
|
||||
def _set_collected(infofile: str):
|
||||
try:
|
||||
executor_info = ExecutorInfo.load_json(infofile)
|
||||
executor_info.collected = True
|
||||
executor_info.dump_json(infofile)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
executor.cleanup(infofile)
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -115,11 +102,10 @@ def run_exp(entry_dict: Dict[str, Any]) -> None:
|
||||
repo = Repo(entry.dvc_root)
|
||||
queue = repo.experiments.celery_queue
|
||||
infofile = queue.get_infofile_path(entry.stash_rev)
|
||||
root_dir = setup_exp.s(entry_dict)()
|
||||
executor = setup_exp.s(entry_dict)()
|
||||
try:
|
||||
cmd = ["dvc", "exp", "exec-run", "--infofile", infofile]
|
||||
proc_dict = queue.proc.run_signature(cmd, name=entry.stash_rev)()
|
||||
collect_exp.s(proc_dict, entry_dict)()
|
||||
finally:
|
||||
cleanup_exp.s(root_dir, entry_dict)()
|
||||
_set_collected(infofile)
|
||||
cleanup_exp.s(executor, infofile)()
|
||||
|
||||
@@ -11,9 +11,11 @@ from ..executor.base import (
|
||||
BaseExecutor,
|
||||
ExecutorInfo,
|
||||
ExecutorResult,
|
||||
TaskStatus,
|
||||
)
|
||||
from ..executor.local import TempDirExecutor
|
||||
from .base import BaseStashQueue, QueueEntry, QueueGetResult
|
||||
from .utils import fetch_running_exp_from_temp_dir
|
||||
from .workspace import WorkspaceQueue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -60,7 +62,7 @@ class TempDirQueue(WorkspaceQueue):
|
||||
stash_entry.name,
|
||||
stash_entry.head_rev,
|
||||
)
|
||||
executor = self.setup_executor(
|
||||
executor = self.init_executor(
|
||||
self.repo.experiments,
|
||||
entry,
|
||||
TempDirExecutor,
|
||||
@@ -75,8 +77,9 @@ class TempDirQueue(WorkspaceQueue):
|
||||
for stash_rev in self.proc:
|
||||
infofile = self.get_infofile_path(stash_rev)
|
||||
executor_info = ExecutorInfo.load_json(infofile)
|
||||
if not executor_info.collected and os.path.exists(
|
||||
executor_info.root_dir
|
||||
if (
|
||||
not executor_info.status <= TaskStatus.SUCCESS
|
||||
and os.path.exists(executor_info.root_dir)
|
||||
):
|
||||
yield QueueEntry(
|
||||
self.repo.root_dir,
|
||||
@@ -96,3 +99,13 @@ class TempDirQueue(WorkspaceQueue):
|
||||
exec_result: ExecutorResult,
|
||||
) -> Dict[str, str]:
|
||||
return BaseStashQueue.collect_executor(exp, executor, exec_result)
|
||||
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
result: Dict[str, Dict] = {}
|
||||
for entry in self.iter_active():
|
||||
result.update(
|
||||
fetch_running_exp_from_temp_dir(
|
||||
self, entry.stash_rev, fetch_refs
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from ..executor.base import ExecutorInfo, TaskStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .base import BaseStashQueue
|
||||
|
||||
|
||||
def fetch_running_exp_from_temp_dir(
|
||||
queue: "BaseStashQueue", rev: str, fetch_refs: bool
|
||||
) -> Dict[str, Dict]:
|
||||
"""Fetch status of running exps out of current working directory
|
||||
|
||||
Args:
|
||||
queue (BaseStashQueue):
|
||||
rev (str): stash revision of the experiment
|
||||
fetch_refs (bool): fetch running checkpoint results to local or not.
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict]: _description_
|
||||
"""
|
||||
from dvc.scm import InvalidRemoteSCMRepo
|
||||
from dvc.utils.serialize import load_json
|
||||
|
||||
from ..executor.local import TempDirExecutor
|
||||
|
||||
result: Dict[str, Dict] = {}
|
||||
infofile = queue.get_infofile_path(rev)
|
||||
try:
|
||||
info = ExecutorInfo.from_dict(load_json(infofile))
|
||||
except OSError:
|
||||
return result
|
||||
if info.status < TaskStatus.FAILED:
|
||||
result[rev] = info.asdict()
|
||||
if info.git_url and fetch_refs and info.status > TaskStatus.PREPARING:
|
||||
|
||||
def on_diverged(_ref: str, _checkpoint: bool):
|
||||
return False
|
||||
|
||||
executor = TempDirExecutor.from_info(info)
|
||||
try:
|
||||
for ref in executor.fetch_exps(
|
||||
queue.scm,
|
||||
on_diverged=on_diverged,
|
||||
):
|
||||
logger.debug("Updated running experiment '%s'.", ref)
|
||||
last_rev = queue.scm.get_ref(ref)
|
||||
result[rev]["last"] = last_rev
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
except InvalidRemoteSCMRepo:
|
||||
# ignore stale info files
|
||||
del result[rev]
|
||||
return result
|
||||
@@ -1,15 +1,25 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Collection, Dict, Generator, Optional
|
||||
from typing import TYPE_CHECKING, Collection, Dict, Generator, List, Optional
|
||||
|
||||
import psutil
|
||||
from funcy import first
|
||||
from voluptuous import Invalid
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.lock import make_lock
|
||||
from dvc.rwlock import RWLOCK_FILE, RWLOCK_LOCK, SCHEMA
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
from ..exceptions import ExpQueueEmptyError
|
||||
from ..executor.base import BaseExecutor, ExecutorResult
|
||||
from ..executor.base import (
|
||||
BaseExecutor,
|
||||
ExecutorInfo,
|
||||
ExecutorResult,
|
||||
TaskStatus,
|
||||
)
|
||||
from ..executor.local import WorkspaceExecutor
|
||||
from ..refs import EXEC_BRANCH
|
||||
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
|
||||
@@ -41,7 +51,7 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
stash_entry.name,
|
||||
stash_entry.head_rev,
|
||||
)
|
||||
executor = self.setup_executor(self.repo.experiments, entry)
|
||||
executor = self.init_executor(self.repo.experiments, entry)
|
||||
return QueueGetResult(entry, executor)
|
||||
|
||||
def iter_queued(self) -> Generator[QueueEntry, None, None]:
|
||||
@@ -118,9 +128,7 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
) from exc
|
||||
finally:
|
||||
if self._EXEC_NAME == exec_name:
|
||||
remove(os.path.join(self.pid_dir, exec_name))
|
||||
executor.cleanup()
|
||||
executor.cleanup(infofile)
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
@@ -135,6 +143,7 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
assert exec_result.exp_hash
|
||||
logger.debug("Collected experiment '%s'.", exp_rev[:7])
|
||||
results[exp_rev] = exec_result.exp_hash
|
||||
|
||||
return results
|
||||
|
||||
def get_result(self, entry: QueueEntry) -> Optional[ExecutorResult]:
|
||||
@@ -153,3 +162,89 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
follow: bool = False,
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
def check_rwlock(
|
||||
self,
|
||||
hardlink: bool = False,
|
||||
autocorrect: bool = False,
|
||||
) -> bool:
|
||||
"""Check and autocorrect the RWLock status for file paths.
|
||||
|
||||
Args:
|
||||
hardlink (bool): use hardlink lock to guard rwlock file when on
|
||||
edit.
|
||||
autocorrect (bool): autocorrect corrupted rwlock file.
|
||||
|
||||
Return:
|
||||
(bool): if the pid alive.
|
||||
"""
|
||||
path = self.repo.fs.path.join(self.repo.tmp_dir, RWLOCK_FILE)
|
||||
|
||||
rwlock_guard = make_lock(
|
||||
self.repo.fs.path.join(self.repo.tmp_dir, RWLOCK_LOCK),
|
||||
tmp_dir=self.repo.tmp_dir,
|
||||
hardlink_lock=hardlink,
|
||||
)
|
||||
with rwlock_guard:
|
||||
try:
|
||||
with self.repo.fs.open(path, encoding="utf-8") as fobj:
|
||||
lock: Dict[str, List[Dict]] = SCHEMA(json.load(fobj))
|
||||
file_path = first(lock["read"])
|
||||
if not file_path:
|
||||
return False
|
||||
lock_info = first(lock["read"][file_path])
|
||||
pid = int(lock_info["pid"])
|
||||
if psutil.pid_exists(pid):
|
||||
return True
|
||||
cmd = lock_info["cmd"]
|
||||
logger.warning(
|
||||
"Process '%s' with (Pid %s), in RWLock-file '%s'"
|
||||
" had been killed.",
|
||||
cmd,
|
||||
pid,
|
||||
relpath(path),
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
except json.JSONDecodeError:
|
||||
logger.warning(
|
||||
"Unable to read RWLock-file '%s'. JSON structure is"
|
||||
" corrupted",
|
||||
relpath(path),
|
||||
)
|
||||
except Invalid:
|
||||
logger.warning("RWLock-file '%s' format error.", relpath(path))
|
||||
if autocorrect:
|
||||
logger.warning(
|
||||
"Delete corrupted RWLock-file '%s'", relpath(path)
|
||||
)
|
||||
remove(path)
|
||||
return False
|
||||
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
from dvc.utils.serialize import load_json
|
||||
|
||||
assert self._EXEC_NAME
|
||||
result: Dict[str, Dict] = {}
|
||||
|
||||
if not self.check_rwlock(autocorrect=True):
|
||||
return result
|
||||
|
||||
infofile = self.get_infofile_path(self._EXEC_NAME)
|
||||
|
||||
try:
|
||||
info = ExecutorInfo.from_dict(load_json(infofile))
|
||||
except OSError:
|
||||
return result
|
||||
|
||||
if info.status < TaskStatus.FAILED:
|
||||
# If we are appending to a checkpoint branch in a workspace
|
||||
# run, show the latest checkpoint as running.
|
||||
if info.status == TaskStatus.SUCCESS:
|
||||
return result
|
||||
last_rev = self.scm.get_ref(EXEC_BRANCH)
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
else:
|
||||
result[self._EXEC_NAME] = info.asdict()
|
||||
return result
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from dvc.dependency.param import ParamsDependency
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.repo import locked
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.cli_parse import to_path_overrides
|
||||
@@ -26,26 +28,59 @@ def run(
|
||||
Returns a dict mapping new experiment SHAs to the results
|
||||
of `repro` for that experiment.
|
||||
"""
|
||||
from dvc.utils.hydra import to_hydra_overrides
|
||||
|
||||
if run_all:
|
||||
entries = list(repo.experiments.celery_queue.iter_queued())
|
||||
return repo.experiments.reproduce_celery(entries, jobs=jobs)
|
||||
|
||||
if params:
|
||||
params = to_path_overrides(params)
|
||||
path_overrides = to_path_overrides(params)
|
||||
else:
|
||||
path_overrides = {}
|
||||
|
||||
if queue:
|
||||
if not kwargs.get("checkpoint_resume", None):
|
||||
kwargs["reset"] = True
|
||||
hydra_enabled = repo.config.get("hydra", {}).get("enabled", False)
|
||||
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
if hydra_enabled and hydra_output_file not in path_overrides:
|
||||
# Force `_update_params` even if `--set-param` was not used
|
||||
path_overrides[hydra_output_file] = []
|
||||
|
||||
hydra_sweep = any(
|
||||
x.is_sweep_override()
|
||||
for param_file in path_overrides
|
||||
for x in to_hydra_overrides(path_overrides[param_file])
|
||||
)
|
||||
|
||||
if hydra_sweep and not queue:
|
||||
raise InvalidArgumentError(
|
||||
"Sweep overrides can't be used without `--queue`"
|
||||
)
|
||||
|
||||
if not queue:
|
||||
return repo.experiments.reproduce_one(
|
||||
targets=targets, params=path_overrides, tmp_dir=tmp_dir, **kwargs
|
||||
)
|
||||
|
||||
if hydra_sweep:
|
||||
from dvc.utils.hydra import get_hydra_sweeps
|
||||
|
||||
sweeps = get_hydra_sweeps(path_overrides)
|
||||
else:
|
||||
sweeps = [path_overrides]
|
||||
|
||||
if not kwargs.get("checkpoint_resume", None):
|
||||
kwargs["reset"] = True
|
||||
|
||||
for sweep_overrides in sweeps:
|
||||
queue_entry = repo.experiments.queue_one(
|
||||
repo.experiments.celery_queue,
|
||||
targets=targets,
|
||||
params=params,
|
||||
params=sweep_overrides,
|
||||
**kwargs,
|
||||
)
|
||||
if sweep_overrides:
|
||||
ui.write(f"Queueing with overrides '{sweep_overrides}'.")
|
||||
name = queue_entry.name or queue_entry.stash_rev[:7]
|
||||
ui.write(f"Queued experiment '{name}' for future execution.")
|
||||
return {}
|
||||
|
||||
return repo.experiments.reproduce_one(
|
||||
targets=targets, params=params, tmp_dir=tmp_dir, **kwargs
|
||||
)
|
||||
return {}
|
||||
|
||||
+205
-51
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from collections import OrderedDict, defaultdict
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
@@ -17,16 +18,21 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExpStatus(Enum):
|
||||
Success = 0
|
||||
Queued = 1
|
||||
Running = 2
|
||||
Failed = 3
|
||||
|
||||
|
||||
@error_handler
|
||||
def _collect_experiment_commit(
|
||||
repo,
|
||||
exp_rev,
|
||||
stash=False,
|
||||
sha_only=True,
|
||||
repo: "Repo",
|
||||
exp_rev: str,
|
||||
status: ExpStatus = ExpStatus.Success,
|
||||
param_deps=False,
|
||||
running=None,
|
||||
onerror: Optional[Callable] = None,
|
||||
is_baseline: bool = False,
|
||||
):
|
||||
from dvc.dependency import ParamsDependency, RepoDependency
|
||||
|
||||
@@ -67,37 +73,35 @@ def _collect_experiment_commit(
|
||||
if not (out.is_metric or out.is_plot)
|
||||
}
|
||||
|
||||
res["queued"] = stash
|
||||
if running is not None and exp_rev in running:
|
||||
res["running"] = True
|
||||
res["executor"] = running[exp_rev].get("location")
|
||||
res["status"] = status.name
|
||||
if status == ExpStatus.Running:
|
||||
res["executor"] = running.get(exp_rev, {}).get("location", None)
|
||||
else:
|
||||
res["running"] = False
|
||||
res["executor"] = None
|
||||
if not stash:
|
||||
|
||||
if status == ExpStatus.Failed:
|
||||
res["error"] = {
|
||||
"msg": "Experiment run failed.",
|
||||
"type": "",
|
||||
}
|
||||
|
||||
if status not in {ExpStatus.Queued, ExpStatus.Failed}:
|
||||
vals = _gather_metrics(
|
||||
repo, targets=None, rev=rev, recursive=False, onerror=onerror
|
||||
)
|
||||
res["metrics"] = vals
|
||||
|
||||
if not sha_only and rev != "workspace":
|
||||
name: Optional[str] = None
|
||||
if is_baseline:
|
||||
for refspec in ["refs/tags", "refs/heads"]:
|
||||
name = repo.scm.describe(rev, base=refspec)
|
||||
if name:
|
||||
break
|
||||
if not name:
|
||||
name = repo.experiments.get_exact_name(rev)
|
||||
if name:
|
||||
name = name.rsplit("/")[-1]
|
||||
res["name"] = name
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def _collect_experiment_branch(
|
||||
res, repo, branch, baseline, onerror: Optional[Callable] = None, **kwargs
|
||||
res,
|
||||
repo,
|
||||
branch,
|
||||
baseline,
|
||||
onerror: Optional[Callable] = None,
|
||||
running=None,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
@@ -105,8 +109,14 @@ def _collect_experiment_branch(
|
||||
prev = None
|
||||
revs = list(repo.scm.branch_revs(exp_rev, baseline))
|
||||
for rev in revs:
|
||||
status = ExpStatus.Running if rev in running else ExpStatus.Success
|
||||
collected_exp = _collect_experiment_commit(
|
||||
repo, rev, onerror=onerror, **kwargs
|
||||
repo,
|
||||
rev,
|
||||
onerror=onerror,
|
||||
status=status,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
if len(revs) > 1:
|
||||
exp = {"checkpoint_tip": exp_rev}
|
||||
@@ -129,12 +139,129 @@ def _collect_experiment_branch(
|
||||
return res
|
||||
|
||||
|
||||
def get_names(repo: "Repo", result: Dict[str, Dict[str, Any]]):
|
||||
|
||||
rev_set = set()
|
||||
baseline_set = set()
|
||||
for baseline in result:
|
||||
for rev in result[baseline]:
|
||||
if rev == "baseline":
|
||||
rev = baseline
|
||||
baseline_set.add(rev)
|
||||
if rev != "workspace":
|
||||
rev_set.add(rev)
|
||||
|
||||
names: Dict[str, Optional[str]] = {}
|
||||
for base in ("refs/tags/", "refs/heads/"):
|
||||
if rev_set:
|
||||
names.update(
|
||||
(rev, ref[len(base) :])
|
||||
for rev, ref in repo.scm.describe(
|
||||
baseline_set, base=base
|
||||
).items()
|
||||
if ref is not None
|
||||
)
|
||||
rev_set.difference_update(names.keys())
|
||||
|
||||
exact_name = repo.experiments.get_exact_name(rev_set)
|
||||
|
||||
for baseline, baseline_results in result.items():
|
||||
for rev, rev_result in baseline_results.items():
|
||||
name: Optional[str] = None
|
||||
if rev == "baseline":
|
||||
rev = baseline
|
||||
if rev == "workspace":
|
||||
continue
|
||||
name = names.get(rev, None)
|
||||
name = name or exact_name[rev]
|
||||
if name:
|
||||
rev_result["data"]["name"] = name
|
||||
|
||||
|
||||
# flake8: noqa: C901
|
||||
def _collect_active_experiment(
|
||||
repo: "Repo",
|
||||
found_revs: Dict[str, List[str]],
|
||||
running: Dict[str, Any],
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
result: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
for entry in chain(
|
||||
repo.experiments.tempdir_queue.iter_active(),
|
||||
repo.experiments.celery_queue.iter_active(),
|
||||
):
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs and (
|
||||
stash_rev not in running or not running[stash_rev].get("last")
|
||||
):
|
||||
result[entry.baseline_rev][stash_rev] = _collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Running,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _collect_queued_experiment(
|
||||
repo: "Repo",
|
||||
found_revs: Dict[str, List[str]],
|
||||
running: Dict[str, Any],
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
result: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
for entry in repo.experiments.celery_queue.iter_queued():
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs:
|
||||
result[entry.baseline_rev][stash_rev] = _collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Queued,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _collect_failed_experiment(
|
||||
repo: "Repo",
|
||||
found_revs: Dict[str, List[str]],
|
||||
running: Dict[str, Any],
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
result: Dict[str, Dict] = defaultdict(OrderedDict)
|
||||
for queue_done_result in repo.experiments.celery_queue.iter_failed():
|
||||
entry = queue_done_result.entry
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs:
|
||||
experiment = _collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
status=ExpStatus.Failed,
|
||||
running=running,
|
||||
**kwargs,
|
||||
)
|
||||
result[entry.baseline_rev][stash_rev] = experiment
|
||||
return result
|
||||
|
||||
|
||||
def update_new(
|
||||
to_dict: Dict[str, Dict[str, Any]], from_dict: Dict[str, Dict[str, Any]]
|
||||
):
|
||||
for baseline, experiments in from_dict.items():
|
||||
for rev, experiment in experiments.items():
|
||||
to_dict[baseline][rev] = to_dict[baseline].get(rev, experiment)
|
||||
|
||||
|
||||
def show(
|
||||
repo: "Repo",
|
||||
all_branches=False,
|
||||
all_tags=False,
|
||||
revs: Union[List[str], str, None] = None,
|
||||
all_commits=False,
|
||||
hide_queued=False,
|
||||
hide_failed=False,
|
||||
sha_only=False,
|
||||
num=1,
|
||||
param_deps=False,
|
||||
@@ -142,6 +269,9 @@ def show(
|
||||
fetch_running: bool = True,
|
||||
):
|
||||
|
||||
if repo.scm.no_commits:
|
||||
return {}
|
||||
|
||||
if onerror is None:
|
||||
onerror = onerror_collect
|
||||
|
||||
@@ -157,17 +287,52 @@ def show(
|
||||
iter_revs(repo.scm, revs, num, all_branches, all_tags, all_commits)
|
||||
)
|
||||
|
||||
running = repo.experiments.get_running_exps(fetch_refs=fetch_running)
|
||||
running: Dict[str, Dict] = repo.experiments.get_running_exps(
|
||||
fetch_refs=fetch_running
|
||||
)
|
||||
|
||||
queued_experiment = (
|
||||
_collect_queued_experiment(
|
||||
repo,
|
||||
found_revs,
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
)
|
||||
if not hide_queued
|
||||
else {}
|
||||
)
|
||||
|
||||
active_experiment = _collect_active_experiment(
|
||||
repo,
|
||||
found_revs,
|
||||
running,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
failed_experiments = (
|
||||
_collect_failed_experiment(
|
||||
repo,
|
||||
found_revs,
|
||||
running,
|
||||
sha_only=sha_only,
|
||||
param_deps=param_deps,
|
||||
onerror=onerror,
|
||||
)
|
||||
if not hide_failed
|
||||
else {}
|
||||
)
|
||||
|
||||
for rev in found_revs:
|
||||
status = ExpStatus.Running if rev in running else ExpStatus.Success
|
||||
res[rev]["baseline"] = _collect_experiment_commit(
|
||||
repo,
|
||||
rev,
|
||||
sha_only=sha_only,
|
||||
status=status,
|
||||
param_deps=param_deps,
|
||||
running=running,
|
||||
onerror=onerror,
|
||||
is_baseline=True,
|
||||
)
|
||||
|
||||
if rev == "workspace":
|
||||
@@ -188,31 +353,20 @@ def show(
|
||||
repo,
|
||||
exp_ref,
|
||||
rev,
|
||||
sha_only=sha_only,
|
||||
param_deps=param_deps,
|
||||
running=running,
|
||||
onerror=onerror,
|
||||
)
|
||||
|
||||
# collect standalone & celery experiments
|
||||
for entry in chain(
|
||||
repo.experiments.tempdir_queue.iter_active(),
|
||||
repo.experiments.celery_queue.iter_active(),
|
||||
repo.experiments.celery_queue.iter_queued(),
|
||||
):
|
||||
stash_rev = entry.stash_rev
|
||||
if entry.baseline_rev in found_revs:
|
||||
if stash_rev not in running or not running[stash_rev].get(
|
||||
"last"
|
||||
):
|
||||
experiment = _collect_experiment_commit(
|
||||
repo,
|
||||
stash_rev,
|
||||
sha_only=sha_only,
|
||||
stash=stash_rev not in running,
|
||||
param_deps=param_deps,
|
||||
running=running,
|
||||
onerror=onerror,
|
||||
)
|
||||
res[entry.baseline_rev][stash_rev] = experiment
|
||||
if not hide_failed:
|
||||
update_new(res, failed_experiments)
|
||||
|
||||
update_new(res, active_experiment)
|
||||
|
||||
if not hide_queued:
|
||||
update_new(res, queued_experiment)
|
||||
|
||||
if not sha_only:
|
||||
get_names(repo, res)
|
||||
|
||||
return res
|
||||
|
||||
+101
-40
@@ -1,17 +1,36 @@
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.exceptions import DownloadError, FileTransferError
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import DownloadError
|
||||
from dvc.fs import Schemes
|
||||
|
||||
from . import locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.types import TargetType
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
from dvc_objects.db.base import ObjectDB
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _fetch_worktree(repo, remote):
|
||||
from dvc_data.index import save
|
||||
|
||||
index = repo.index.data["repo"]
|
||||
for key, entry in index.iteritems():
|
||||
entry.fs = remote.fs
|
||||
entry.path = remote.fs.path.join(
|
||||
remote.path,
|
||||
*key,
|
||||
)
|
||||
save(index)
|
||||
return len(index)
|
||||
|
||||
|
||||
@locked
|
||||
def fetch(
|
||||
self,
|
||||
@@ -26,7 +45,7 @@ def fetch(
|
||||
run_cache=False,
|
||||
revs=None,
|
||||
odb: Optional["ObjectDB"] = None,
|
||||
):
|
||||
) -> int:
|
||||
"""Download data items from a cloud and imported repositories
|
||||
|
||||
Returns:
|
||||
@@ -39,71 +58,113 @@ def fetch(
|
||||
config.NoRemoteError: thrown when downloading only local files and no
|
||||
remote is configured
|
||||
"""
|
||||
from dvc.repo.imports import save_imports
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
|
||||
used = self.used_objs(
|
||||
targets,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
with_deps=with_deps,
|
||||
force=True,
|
||||
remote=remote,
|
||||
jobs=jobs,
|
||||
recursive=recursive,
|
||||
revs=revs,
|
||||
)
|
||||
with suppress(NoRemoteError):
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote.worktree:
|
||||
return _fetch_worktree(self, _remote)
|
||||
|
||||
downloaded = 0
|
||||
failed = 0
|
||||
failed_count = 0
|
||||
|
||||
try:
|
||||
if run_cache:
|
||||
self.stage_cache.pull(remote)
|
||||
except DownloadError as exc:
|
||||
failed += exc.amount
|
||||
failed_count += exc.amount
|
||||
|
||||
no_remote_msg: Optional[str] = None
|
||||
result = TransferResult(set(), set())
|
||||
try:
|
||||
d, f = _fetch(
|
||||
self,
|
||||
targets,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
with_deps=with_deps,
|
||||
force=True,
|
||||
remote=remote,
|
||||
jobs=jobs,
|
||||
recursive=recursive,
|
||||
revs=revs,
|
||||
odb=odb,
|
||||
)
|
||||
result.transferred.update(d)
|
||||
result.failed.update(f)
|
||||
except NoRemoteError as exc:
|
||||
no_remote_msg = str(exc)
|
||||
|
||||
for rev in self.brancher(
|
||||
revs=revs,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
):
|
||||
imported = save_imports(
|
||||
self,
|
||||
targets,
|
||||
unpartial=not rev or rev == "workspace",
|
||||
recursive=recursive,
|
||||
)
|
||||
result.transferred.update(imported)
|
||||
result.failed.difference_update(imported)
|
||||
|
||||
failed_count += len(result.failed)
|
||||
|
||||
if failed_count:
|
||||
if no_remote_msg:
|
||||
logger.error(no_remote_msg)
|
||||
raise DownloadError(failed_count)
|
||||
|
||||
return len(result.transferred)
|
||||
|
||||
|
||||
def _fetch(
|
||||
repo: "Repo",
|
||||
targets: "TargetType",
|
||||
remote: Optional[str] = None,
|
||||
jobs: Optional[int] = None,
|
||||
odb: Optional["ObjectDB"] = None,
|
||||
**kwargs,
|
||||
) -> "TransferResult":
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
result = TransferResult(set(), set())
|
||||
used = repo.used_objs(
|
||||
targets,
|
||||
remote=remote,
|
||||
jobs=jobs,
|
||||
**kwargs,
|
||||
)
|
||||
if odb:
|
||||
all_ids = set()
|
||||
for _odb, obj_ids in used.items():
|
||||
all_ids.update(obj_ids)
|
||||
d, f = _fetch(
|
||||
self,
|
||||
d, f = repo.cloud.pull(
|
||||
all_ids,
|
||||
jobs=jobs,
|
||||
remote=remote,
|
||||
odb=odb,
|
||||
)
|
||||
downloaded += d
|
||||
failed += f
|
||||
result.transferred.update(d)
|
||||
result.failed.update(f)
|
||||
else:
|
||||
for src_odb, obj_ids in sorted(
|
||||
used.items(),
|
||||
key=lambda item: item[0] is not None
|
||||
and item[0].fs.protocol == Schemes.MEMORY,
|
||||
):
|
||||
d, f = _fetch(
|
||||
self,
|
||||
d, f = repo.cloud.pull(
|
||||
obj_ids,
|
||||
jobs=jobs,
|
||||
remote=remote,
|
||||
odb=src_odb,
|
||||
)
|
||||
downloaded += d
|
||||
failed += f
|
||||
|
||||
if failed:
|
||||
raise DownloadError(failed)
|
||||
|
||||
return downloaded
|
||||
|
||||
|
||||
def _fetch(repo, obj_ids, **kwargs):
|
||||
downloaded = 0
|
||||
failed = 0
|
||||
try:
|
||||
downloaded += repo.cloud.pull(obj_ids, **kwargs)
|
||||
except FileTransferError as exc:
|
||||
failed += exc.amount
|
||||
return downloaded, failed
|
||||
result.transferred.update(d)
|
||||
result.failed.update(f)
|
||||
return result
|
||||
|
||||
+2
-2
@@ -60,8 +60,8 @@ def gc(
|
||||
from contextlib import ExitStack
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc_data.db import get_index
|
||||
from dvc_data.gc import gc as ogc
|
||||
from dvc_data.hashfile.db import get_index
|
||||
from dvc_data.hashfile.gc import gc as ogc
|
||||
|
||||
if not repos:
|
||||
repos = []
|
||||
|
||||
+10
-12
@@ -1,19 +1,17 @@
|
||||
import os
|
||||
|
||||
from dvc import output
|
||||
from dvc.exceptions import URLMissingError
|
||||
from dvc.fs import download, parse_external_url
|
||||
from dvc.utils import resolve_output
|
||||
|
||||
def get_url(url, out=None, jobs=None):
|
||||
from dvc import dependency, output
|
||||
from dvc.utils import resolve_output
|
||||
|
||||
def get_url(url, out=None, *, config=None, jobs=None):
|
||||
out = resolve_output(url, out)
|
||||
|
||||
if os.path.exists(url):
|
||||
url = os.path.abspath(url)
|
||||
|
||||
out = os.path.abspath(out)
|
||||
|
||||
(dep,) = dependency.loads_from(None, [url])
|
||||
(out,) = output.loads_from(None, [out], use_cache=False)
|
||||
if not dep.exists:
|
||||
raise dep.DoesNotExistError(dep)
|
||||
dep.download(out, jobs=jobs)
|
||||
|
||||
src_fs, src_path = parse_external_url(url, config)
|
||||
if not src_fs.exists(src_path):
|
||||
raise URLMissingError(url)
|
||||
download(src_fs, src_path, out, jobs=jobs)
|
||||
|
||||
+22
-7
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.utils import relpath, resolve_output, resolve_paths
|
||||
@@ -7,6 +8,9 @@ from dvc.utils.fs import path_isin
|
||||
from ..exceptions import InvalidArgumentError, OutputDuplicationError
|
||||
from . import locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.dvcfile import DVCFile
|
||||
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
@@ -17,11 +21,17 @@ def imp_url(
|
||||
fname=None,
|
||||
erepo=None,
|
||||
frozen=True,
|
||||
no_download=False,
|
||||
no_exec=False,
|
||||
remote=None,
|
||||
to_remote=False,
|
||||
desc=None,
|
||||
type=None, # pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
meta=None,
|
||||
jobs=None,
|
||||
fs_config=None,
|
||||
version_aware: bool = False,
|
||||
):
|
||||
from dvc.dvcfile import Dvcfile
|
||||
from dvc.stage import Stage, create_stage, restore_fields
|
||||
@@ -31,9 +41,9 @@ def imp_url(
|
||||
self, out, always_local=to_remote and not out
|
||||
)
|
||||
|
||||
if to_remote and no_exec:
|
||||
if to_remote and (no_exec or no_download):
|
||||
raise InvalidArgumentError(
|
||||
"--no-exec can't be combined with --to-remote"
|
||||
"--no-exec/--no-download cannot be combined with --to-remote"
|
||||
)
|
||||
|
||||
if not to_remote and remote:
|
||||
@@ -49,6 +59,11 @@ def imp_url(
|
||||
):
|
||||
url = relpath(url, wdir)
|
||||
|
||||
if version_aware:
|
||||
if fs_config is None:
|
||||
fs_config = {}
|
||||
fs_config["version_aware"] = True
|
||||
|
||||
stage = create_stage(
|
||||
Stage,
|
||||
self,
|
||||
@@ -57,13 +72,13 @@ def imp_url(
|
||||
deps=[url],
|
||||
outs=[out],
|
||||
erepo=erepo,
|
||||
fs_config=fs_config,
|
||||
)
|
||||
restore_fields(stage)
|
||||
|
||||
if desc:
|
||||
stage.outs[0].desc = desc
|
||||
|
||||
dvcfile = Dvcfile(self, stage.path)
|
||||
out_obj = stage.outs[0]
|
||||
out_obj.annot.update(desc=desc, type=type, labels=labels, meta=meta)
|
||||
dvcfile: "DVCFile" = Dvcfile(self, stage.path) # type: ignore
|
||||
dvcfile.remove()
|
||||
|
||||
try:
|
||||
@@ -80,7 +95,7 @@ def imp_url(
|
||||
stage.save_deps()
|
||||
stage.md5 = stage.compute_md5()
|
||||
else:
|
||||
stage.run(jobs=jobs)
|
||||
stage.run(jobs=jobs, no_download=no_download)
|
||||
|
||||
stage.frozen = frozen
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import logging
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import TYPE_CHECKING, List, Set, Tuple, Union
|
||||
|
||||
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 import HashInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def unfetched_view(
|
||||
index: "Index", targets: "TargetType", unpartial: bool = False, **kwargs
|
||||
) -> Tuple["IndexView", List["Dependency"]]:
|
||||
"""Return index view of imports which have not been fetched.
|
||||
|
||||
Returns:
|
||||
Tuple in the form (view, changed_deps) where changed_imports is a list
|
||||
of import dependencies that cannot be fetched due to changed data
|
||||
source.
|
||||
"""
|
||||
changed_deps: List["Dependency"] = []
|
||||
|
||||
def need_fetch(stage: "Stage") -> bool:
|
||||
if (
|
||||
not stage.is_import
|
||||
or stage.is_repo_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
|
||||
|
||||
return True
|
||||
|
||||
unfetched = index.targets_view(targets, filter_fn=need_fetch, **kwargs)
|
||||
return unfetched, changed_deps
|
||||
|
||||
|
||||
def partial_view(
|
||||
index: "Index", targets: "TargetType", **kwargs
|
||||
) -> "IndexView":
|
||||
return index.targets_view(
|
||||
targets,
|
||||
filter_fn=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.
|
||||
"""
|
||||
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:
|
||||
if out.stage.is_repo_import:
|
||||
dep = out.stage.deps[0]
|
||||
out.hash_info = dep.get_obj().hash_info
|
||||
out.meta = dep.get_meta()
|
||||
else:
|
||||
out.hash_info = entry.hash_info
|
||||
out.meta = entry.meta
|
||||
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.stage.exceptions import DataSourceChanged
|
||||
from dvc.utils.fs import makedirs
|
||||
from dvc_data.index import checkout, md5, save
|
||||
from dvc_objects.fs.callbacks import Callback
|
||||
|
||||
downloaded: Set["HashInfo"] = set()
|
||||
|
||||
unfetched, changed = unfetched_view(
|
||||
repo.index, targets, unpartial=unpartial, **kwargs
|
||||
)
|
||||
for dep in changed:
|
||||
logger.warning(str(DataSourceChanged(f"{dep.stage} ({dep})")))
|
||||
|
||||
data_view = unfetched.data["repo"]
|
||||
if len(data_view):
|
||||
cache = repo.odb.local
|
||||
if not cache.fs.exists(cache.path):
|
||||
makedirs(cache.path)
|
||||
with TemporaryDirectory(dir=cache.path) as tmpdir:
|
||||
with Callback.as_tqdm_callback(
|
||||
desc="Downloading imports from source",
|
||||
unit="files",
|
||||
) as cb:
|
||||
checkout(data_view, tmpdir, cache.fs, callback=cb)
|
||||
md5(data_view)
|
||||
save(data_view, odb=cache, hardlink=True)
|
||||
|
||||
downloaded.update(
|
||||
entry.hash_info
|
||||
for _, entry in data_view.iteritems()
|
||||
if not entry.meta.isdir and entry.hash_info is not None
|
||||
)
|
||||
|
||||
if unpartial:
|
||||
unpartial_imports(partial_view(repo.index, targets, **kwargs))
|
||||
|
||||
return downloaded
|
||||
+157
-40
@@ -1,13 +1,13 @@
|
||||
from contextlib import suppress
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
)
|
||||
|
||||
@@ -22,11 +22,11 @@ if TYPE_CHECKING:
|
||||
from dvc.dependency import Dependency, ParamsDependency
|
||||
from dvc.fs import FileSystem
|
||||
from dvc.output import Output
|
||||
from dvc.repo.stage import StageLoad
|
||||
from dvc.repo.stage import StageInfo, StageLoad
|
||||
from dvc.stage import Stage
|
||||
from dvc.types import StrPath, TargetType
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.index import DataIndex
|
||||
from dvc_data.index import DataIndex, DataIndexKey, DataIndexView
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ class Index:
|
||||
self.stage_collector: "StageLoad" = repo.stage
|
||||
if stages is not None:
|
||||
self.stages: List["Stage"] = stages
|
||||
self._collected_targets: Dict[int, List["StageInfo"]] = {}
|
||||
|
||||
@cached_property
|
||||
def stages(self) -> List["Stage"]: # pylint: disable=method-hidden
|
||||
@@ -113,6 +114,62 @@ class Index:
|
||||
|
||||
return self.filter(is_stage_inside_path)
|
||||
|
||||
@staticmethod
|
||||
def _hash_targets(
|
||||
targets: Iterable[Optional[str]],
|
||||
**kwargs: Any,
|
||||
) -> int:
|
||||
return hash(
|
||||
(
|
||||
frozenset(targets),
|
||||
kwargs.get("with_deps", False),
|
||||
kwargs.get("recursive", False),
|
||||
)
|
||||
)
|
||||
|
||||
def _collect_targets(
|
||||
self, targets: Optional["TargetType"], **kwargs: Any
|
||||
) -> List["StageInfo"]:
|
||||
from itertools import chain
|
||||
|
||||
from dvc.repo.stage import StageInfo
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
targets = ensure_list(targets)
|
||||
if not targets:
|
||||
return [StageInfo(stage) for stage in self.stages]
|
||||
targets_hash = self._hash_targets(targets, **kwargs)
|
||||
if targets_hash not in self._collected_targets:
|
||||
self._collected_targets[targets_hash] = list(
|
||||
chain.from_iterable(
|
||||
self.stage_collector.collect_granular(target, **kwargs)
|
||||
for target in targets
|
||||
)
|
||||
)
|
||||
return self._collected_targets[targets_hash]
|
||||
|
||||
def targets_view(
|
||||
self,
|
||||
targets: "TargetType",
|
||||
filter_fn: Optional[Callable[["Stage"], bool]] = None,
|
||||
**kwargs: Any,
|
||||
) -> "IndexView":
|
||||
"""Return read-only view of index for the specified targets.
|
||||
|
||||
Args:
|
||||
targets: Targets to collect
|
||||
filter_fn: Optional stage filter to be applied after collecting
|
||||
targets.
|
||||
|
||||
Additional kwargs will be passed into the stage collector.
|
||||
"""
|
||||
stage_infos = [
|
||||
stage_info
|
||||
for stage_info in self._collect_targets(targets, **kwargs)
|
||||
if not filter_fn or filter_fn(stage_info.stage)
|
||||
]
|
||||
return IndexView(self, stage_infos)
|
||||
|
||||
@property
|
||||
def outs(self) -> Iterator["Output"]:
|
||||
for stage in self:
|
||||
@@ -171,8 +228,7 @@ class Index:
|
||||
def data(self) -> "Dict[str, DataIndex]":
|
||||
from collections import defaultdict
|
||||
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc_data.index import DataIndex, DataIndexEntry
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
by_workspace: dict = defaultdict(DataIndex)
|
||||
|
||||
@@ -183,30 +239,19 @@ class Index:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
if out.is_in_repo:
|
||||
workspace = "repo"
|
||||
key = self.repo.fs.path.relparts(
|
||||
out.fs_path, self.repo.root_dir
|
||||
)
|
||||
else:
|
||||
workspace = out.fs.protocol
|
||||
no_drive = out.fs.path.flavour.splitdrive(out.fs_path)[1]
|
||||
key = out.fs.path.parts(no_drive)[1:]
|
||||
|
||||
try:
|
||||
remote = self.repo.cloud.get_remote_odb(out.remote)
|
||||
except NoRemoteError:
|
||||
remote = None
|
||||
|
||||
workspace, key = out.index_key
|
||||
data_index = by_workspace[workspace]
|
||||
|
||||
data_index[key] = DataIndexEntry(
|
||||
meta=out.meta,
|
||||
obj=out.obj,
|
||||
hash_info=out.hash_info,
|
||||
odb=out.odb,
|
||||
remote=remote,
|
||||
)
|
||||
if out.files:
|
||||
out.obj = out.get_obj()
|
||||
|
||||
entry = out.get_entry()
|
||||
entry.key = key
|
||||
if out.stage.is_import and not out.stage.is_repo_import:
|
||||
entry.fs = out.stage.deps[0].fs
|
||||
entry.path = out.stage.deps[0].fs_path
|
||||
entry.meta = out.stage.deps[0].meta
|
||||
data_index.add(entry)
|
||||
|
||||
return dict(by_workspace)
|
||||
|
||||
@@ -220,22 +265,11 @@ class Index:
|
||||
jobs: int = None,
|
||||
) -> "ObjectContainer":
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
used: "ObjectContainer" = defaultdict(set)
|
||||
collect_targets: Sequence[Optional[str]] = (None,)
|
||||
if targets:
|
||||
collect_targets = ensure_list(targets)
|
||||
|
||||
pairs = chain.from_iterable(
|
||||
self.stage_collector.collect_granular(
|
||||
target, recursive=recursive, with_deps=with_deps
|
||||
)
|
||||
for target in collect_targets
|
||||
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(
|
||||
remote=remote,
|
||||
@@ -311,6 +345,89 @@ class Index:
|
||||
return dict_md5(self.dumpd())
|
||||
|
||||
|
||||
class IndexView:
|
||||
"""Read-only view of Index.data using filtered stages."""
|
||||
|
||||
def __init__( # pylint: disable=redefined-outer-name
|
||||
self, index: Index, stage_infos: Iterable["StageInfo"]
|
||||
):
|
||||
self._index = index
|
||||
self._stage_infos = stage_infos
|
||||
# NOTE: stage_infos might have the same stage multiple times but with
|
||||
# different filter_info
|
||||
self._stages = list({stage for stage, _ in stage_infos})
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._stages)
|
||||
|
||||
def __contains__(self, stage: "Stage") -> bool:
|
||||
# as we are keeping stages inside a list, it might be slower.
|
||||
return stage in self._stages
|
||||
|
||||
def __iter__(self) -> Iterator["Stage"]:
|
||||
yield from self._stages
|
||||
|
||||
def __getitem__(self, item: str) -> "Stage":
|
||||
"""Get a stage by its addressing attribute."""
|
||||
for stage in self:
|
||||
if stage.addressing == item:
|
||||
return stage
|
||||
raise KeyError(f"{item} - available stages are {self.stages}")
|
||||
|
||||
@property
|
||||
def stages(self):
|
||||
return self._stages
|
||||
|
||||
@property
|
||||
def deps(self) -> Iterator["Dependency"]:
|
||||
for stage in self:
|
||||
yield from stage.deps
|
||||
|
||||
@property
|
||||
def outs(self) -> Iterator["Output"]:
|
||||
outs = set()
|
||||
for stage, filter_info in self._stage_infos:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
outs.add(out)
|
||||
yield from outs
|
||||
|
||||
@cached_property
|
||||
def _data_prefixes(self) -> Dict[str, Set["DataIndexKey"]]:
|
||||
from collections import defaultdict
|
||||
|
||||
prefixes: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
|
||||
for stage, filter_info in self._stage_infos:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
workspace, key = out.index_key
|
||||
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
|
||||
key = (
|
||||
*key,
|
||||
out.fs.path.relparts(filter_info, out.fs_path),
|
||||
)
|
||||
prefixes[workspace].add(key)
|
||||
prefixes[workspace].update(
|
||||
key[:i] for i in range(len(key), 0, -1)
|
||||
)
|
||||
return prefixes
|
||||
|
||||
@cached_property
|
||||
def data(self) -> "Dict[str, DataIndexView]":
|
||||
from functools import partial
|
||||
|
||||
from dvc_data.index import view
|
||||
|
||||
def key_filter(workspace: str, key: "DataIndexKey"):
|
||||
try:
|
||||
return key in self._data_prefixes[workspace]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return {
|
||||
workspace: view(index, partial(key_filter, workspace))
|
||||
for workspace, index in self._index.data.items()
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import logging
|
||||
|
||||
|
||||
+13
-14
@@ -1,11 +1,10 @@
|
||||
import os
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.exceptions import PathMissingError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
from . import Repo
|
||||
|
||||
@@ -56,7 +55,7 @@ def ls(
|
||||
def _ls(
|
||||
repo: "Repo", path: str, recursive: bool = None, dvc_only: bool = False
|
||||
):
|
||||
fs: "DvcFileSystem" = repo.dvcfs
|
||||
fs: "DVCFileSystem" = repo.dvcfs
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
try:
|
||||
@@ -66,15 +65,16 @@ def _ls(
|
||||
|
||||
infos = {}
|
||||
for root, dirs, files in fs.walk(
|
||||
fs_path, dvcfiles=True, dvc_only=dvc_only
|
||||
fs_path, dvcfiles=True, dvc_only=dvc_only, detail=True
|
||||
):
|
||||
entries = chain(files, dirs) if not recursive else files
|
||||
if not recursive:
|
||||
files.update(dirs)
|
||||
|
||||
for entry in entries:
|
||||
entry_fs_path = fs.path.join(root, entry)
|
||||
for name, entry in files.items():
|
||||
entry_fs_path = fs.path.join(root, name)
|
||||
relparts = fs.path.relparts(entry_fs_path, fs_path)
|
||||
name = os.path.join(*relparts)
|
||||
infos[name] = fs.info(entry_fs_path)
|
||||
infos[name] = entry
|
||||
|
||||
if not recursive:
|
||||
break
|
||||
@@ -85,11 +85,10 @@ def _ls(
|
||||
ret = {}
|
||||
for name, info in infos.items():
|
||||
dvc_info = info.get("dvc_info", {})
|
||||
if fs.isdvc(info["name"], recursive=True) or not dvc_only:
|
||||
ret[name] = {
|
||||
"isout": dvc_info.get("isout", False),
|
||||
"isdir": info["type"] == "directory",
|
||||
"isexec": info.get("isexec", False),
|
||||
}
|
||||
ret[name] = {
|
||||
"isout": dvc_info.get("isout", False),
|
||||
"isdir": info["type"] == "directory",
|
||||
"isexec": info.get("isexec", False),
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from dvc.exceptions import URLMissingError
|
||||
from dvc.fs import parse_external_url
|
||||
|
||||
|
||||
def ls_url(url, *, config=None, recursive=False):
|
||||
fs, fs_path = parse_external_url(url, config=config)
|
||||
try:
|
||||
info = fs.info(fs_path)
|
||||
except FileNotFoundError as exc:
|
||||
raise URLMissingError(url) from exc
|
||||
if info["type"] != "directory":
|
||||
return [{"path": info["name"], "isdir": False}]
|
||||
|
||||
ret = []
|
||||
for _, dirs, files in fs.walk(fs_path, detail=True):
|
||||
if not recursive:
|
||||
files.update(dirs)
|
||||
|
||||
for info in files.values():
|
||||
ls_info = {
|
||||
"path": fs.path.relpath(info["name"], fs_path),
|
||||
"isdir": info["type"] == "directory",
|
||||
}
|
||||
ret.append(ls_info)
|
||||
|
||||
if not recursive:
|
||||
break
|
||||
|
||||
return ret
|
||||
@@ -4,7 +4,7 @@ from typing import List
|
||||
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc.output import Output
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.collect import StrPaths, collect
|
||||
@@ -77,7 +77,7 @@ def _read_metric(path, fs, rev, **kwargs):
|
||||
|
||||
|
||||
def _read_metrics(repo, metrics, rev, onerror=None):
|
||||
fs = DvcFileSystem(repo=repo)
|
||||
fs = DVCFileSystem(repo=repo)
|
||||
|
||||
relpath = ""
|
||||
if repo.root_dir != repo.fs.path.getcwd():
|
||||
@@ -85,12 +85,14 @@ def _read_metrics(repo, metrics, rev, onerror=None):
|
||||
|
||||
res = {}
|
||||
for metric in metrics:
|
||||
rel_metric_path = os.path.join(relpath, *fs.path.parts(metric))
|
||||
if not fs.isfile(metric):
|
||||
continue
|
||||
if fs.isfile(rel_metric_path):
|
||||
metric = rel_metric_path
|
||||
else:
|
||||
continue
|
||||
|
||||
res[os.path.join(relpath, *fs.path.parts(metric))] = _read_metric(
|
||||
metric, fs, rev, onerror=onerror
|
||||
)
|
||||
res[rel_metric_path] = _read_metric(metric, fs, rev, onerror=onerror)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ def _collect_configs(
|
||||
default_params
|
||||
):
|
||||
fs_paths.append(default_params)
|
||||
|
||||
if targets and (deps or stages) and not params:
|
||||
# A target has been provided but it is not used in the stages
|
||||
fs_paths = []
|
||||
return params, fs_paths
|
||||
|
||||
|
||||
|
||||
+82
-43
@@ -100,7 +100,7 @@ class Plots:
|
||||
"filename":
|
||||
{
|
||||
"data_source": callable loading the data,
|
||||
"props": propreties for the file if it is
|
||||
"props": properties for the file if it is
|
||||
plots type output
|
||||
}
|
||||
}
|
||||
@@ -152,9 +152,9 @@ class Plots:
|
||||
props: Optional[Dict] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
):
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
fs = DvcFileSystem(repo=self.repo)
|
||||
fs = DVCFileSystem(repo=self.repo)
|
||||
|
||||
props = props or {}
|
||||
|
||||
@@ -386,30 +386,56 @@ def _collect_output_plots(
|
||||
return result
|
||||
|
||||
|
||||
def _adjust_definitions_to_cwd(fs, config_relpath, plots_definitions):
|
||||
result = defaultdict(dict)
|
||||
def _id_is_path(plot_props=None):
|
||||
if not plot_props:
|
||||
return True
|
||||
|
||||
config_dirname = fs.path.dirname(config_relpath)
|
||||
y_def = plot_props.get("y")
|
||||
return not isinstance(y_def, dict)
|
||||
|
||||
for plot_id, plot_def in plots_definitions.items():
|
||||
|
||||
y_def = plot_def.get("y", None) if plot_def else None
|
||||
if y_def is None or not isinstance(y_def, dict):
|
||||
# plot_id is filename
|
||||
new_plot_id = _normpath(fs.path.join(config_dirname, plot_id))
|
||||
result[new_plot_id] = plot_def or {}
|
||||
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.path.join(config_dir, filepath))] = val
|
||||
new_plot_props["y"] = new_y
|
||||
return new_plot_props
|
||||
|
||||
|
||||
def _resolve_definitions(
|
||||
fs, targets, props, config_path, definitions, onerror=None
|
||||
):
|
||||
config_dir = fs.path.dirname(config_path)
|
||||
result: Dict[str, Dict] = {}
|
||||
for plot_id, plot_props in definitions.items():
|
||||
if plot_props is None:
|
||||
plot_props = {}
|
||||
if _id_is_path(plot_props):
|
||||
data_path = _normpath(fs.path.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,
|
||||
)
|
||||
dpath.util.merge(
|
||||
result,
|
||||
unpacked,
|
||||
)
|
||||
else:
|
||||
new_plot_def = deepcopy(plot_def)
|
||||
old_y = new_plot_def.pop("y")
|
||||
new_y = {}
|
||||
for filepath, val in old_y.items():
|
||||
new_y[_normpath(fs.path.join(config_dirname, filepath))] = val
|
||||
new_plot_def["y"] = new_y
|
||||
result[plot_id] = new_plot_def
|
||||
return dict(result)
|
||||
if _matches(targets, config_path, plot_id):
|
||||
adjusted_props = _adjust_sources(fs, plot_props, config_dir)
|
||||
dpath.util.merge(
|
||||
result, {"data": {plot_id: {**adjusted_props, **props}}}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _collect_pipeline_files(repo, targets: List[str], props):
|
||||
def _collect_pipeline_files(repo, targets: List[str], props, onerror=None):
|
||||
from dvc.dvcfile import PipelineFile
|
||||
|
||||
result: Dict[str, Dict] = {}
|
||||
@@ -417,21 +443,23 @@ def _collect_pipeline_files(repo, targets: List[str], props):
|
||||
for dvcfile in dvcfiles:
|
||||
if isinstance(dvcfile, PipelineFile):
|
||||
dvcfile_path = _relpath(repo.dvcfs, dvcfile.path)
|
||||
dvcfile_defs = _adjust_definitions_to_cwd(
|
||||
repo.fs, dvcfile_path, dvcfile.load().get("plots", {})
|
||||
dvcfile_defs = dvcfile.load().get("plots", {})
|
||||
if isinstance(dvcfile_defs, list):
|
||||
dvcfile_defs = {
|
||||
k: v for elem in dvcfile_defs for k, v in elem.items()
|
||||
}
|
||||
resolved = _resolve_definitions(
|
||||
repo.dvcfs,
|
||||
targets,
|
||||
props,
|
||||
dvcfile_path,
|
||||
dvcfile_defs,
|
||||
onerror=onerror,
|
||||
)
|
||||
dpath.util.merge(
|
||||
result,
|
||||
{dvcfile_path: resolved},
|
||||
)
|
||||
for plot_id, plot_props in dvcfile_defs.items():
|
||||
if plot_props is None:
|
||||
plot_props = {}
|
||||
if _matches(targets, dvcfile_path, plot_id):
|
||||
dpath.util.merge(
|
||||
result,
|
||||
{
|
||||
dvcfile_path: {
|
||||
"data": {plot_id: {**plot_props, **props}}
|
||||
}
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -448,12 +476,15 @@ def _collect_definitions(
|
||||
result: Dict = defaultdict(dict)
|
||||
props = props or {}
|
||||
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
fs = DvcFileSystem(repo=repo)
|
||||
fs = DVCFileSystem(repo=repo)
|
||||
|
||||
if not config_files:
|
||||
dpath.util.merge(result, _collect_pipeline_files(repo, targets, props))
|
||||
dpath.util.merge(
|
||||
result,
|
||||
_collect_pipeline_files(repo, targets, props, onerror=onerror),
|
||||
)
|
||||
|
||||
if targets or (not targets and not config_files):
|
||||
dpath.util.merge(
|
||||
@@ -463,12 +494,20 @@ def _collect_definitions(
|
||||
|
||||
if config_files:
|
||||
for path in config_files:
|
||||
definitions = parse(fs, path)
|
||||
definitions = _adjust_definitions_to_cwd(
|
||||
repo.fs, path, definitions
|
||||
)
|
||||
definitions = parse(fs, path).get("data", {})
|
||||
if definitions:
|
||||
dpath.util.merge(result, {path: definitions})
|
||||
resolved = _resolve_definitions(
|
||||
repo.dvcfs,
|
||||
targets,
|
||||
props,
|
||||
path,
|
||||
definitions,
|
||||
onerror=onerror,
|
||||
)
|
||||
dpath.util.merge(
|
||||
result,
|
||||
{path: resolved},
|
||||
)
|
||||
|
||||
for target in targets:
|
||||
if not result or fs.exists(target):
|
||||
|
||||
@@ -12,6 +12,8 @@ def _revisions(repo, revs, experiment):
|
||||
|
||||
|
||||
def diff(repo, *args, revs=None, experiment=False, **kwargs):
|
||||
if repo.scm.no_commits:
|
||||
return {}
|
||||
return repo.plots.show(
|
||||
*args, revs=_revisions(repo, revs, experiment), **kwargs
|
||||
)
|
||||
|
||||
+57
-11
@@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.exceptions import FileTransferError, UploadError
|
||||
from dvc.exceptions import UploadError
|
||||
|
||||
from ..utils import glob_targets
|
||||
from . import locked
|
||||
@@ -9,6 +9,48 @@ if TYPE_CHECKING:
|
||||
from dvc_objects.db.base import ObjectDB
|
||||
|
||||
|
||||
def _push_worktree(repo, remote):
|
||||
from dvc_data.index import checkout
|
||||
from dvc_data.index.save import build_tree
|
||||
|
||||
index = repo.index.data["repo"]
|
||||
checkout(index, remote.path, remote.fs)
|
||||
|
||||
for stage in repo.index.stages:
|
||||
for out in stage.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
if not out.is_in_repo:
|
||||
continue
|
||||
|
||||
workspace, key = out.index_key
|
||||
index = repo.index.data[workspace]
|
||||
entry = index[key]
|
||||
if out.isdir():
|
||||
old_tree = out.get_obj()
|
||||
entry.hash_info = old_tree.hash_info
|
||||
entry.meta = out.meta
|
||||
for subkey, entry in index.iteritems(key):
|
||||
if entry.meta.isdir:
|
||||
continue
|
||||
fs_path = repo.fs.path.join(repo.root_dir, *subkey)
|
||||
_, hash_info = old_tree.get(
|
||||
repo.fs.path.relparts(fs_path, out.fs_path)
|
||||
)
|
||||
entry.hash_info = hash_info
|
||||
tree_meta, new_tree = build_tree(index, key)
|
||||
out.obj = new_tree
|
||||
out.hash_info = new_tree.hash_info
|
||||
out.meta = tree_meta
|
||||
else:
|
||||
out.hash_info = entry.hash_info
|
||||
out.meta = entry.meta
|
||||
stage.dvcfile.dump(stage, with_files=True, update_pipeline=False)
|
||||
|
||||
return len(index)
|
||||
|
||||
|
||||
@locked
|
||||
def push(
|
||||
self,
|
||||
@@ -26,6 +68,10 @@ def push(
|
||||
odb: Optional["ObjectDB"] = None,
|
||||
include_imports=False,
|
||||
):
|
||||
_remote = self.cloud.get_remote(name=remote)
|
||||
if _remote.worktree:
|
||||
return _push_worktree(self, _remote)
|
||||
|
||||
used_run_cache = (
|
||||
self.stage_cache.push(remote, odb=odb) if run_cache else []
|
||||
)
|
||||
@@ -56,18 +102,18 @@ def push(
|
||||
if not include_imports and dest_odb and dest_odb.read_only:
|
||||
continue
|
||||
all_ids.update(obj_ids)
|
||||
try:
|
||||
pushed += self.cloud.push(all_ids, jobs, remote=remote, odb=odb)
|
||||
except FileTransferError as exc:
|
||||
raise UploadError(exc.amount)
|
||||
result = self.cloud.push(all_ids, jobs, remote=remote, odb=odb)
|
||||
if result.failed:
|
||||
raise UploadError(len(result.failed))
|
||||
pushed += len(result.transferred)
|
||||
else:
|
||||
for dest_odb, obj_ids in used.items():
|
||||
if dest_odb and dest_odb.read_only:
|
||||
continue
|
||||
try:
|
||||
pushed += self.cloud.push(
|
||||
obj_ids, jobs, remote=remote, odb=odb or dest_odb
|
||||
)
|
||||
except FileTransferError as exc:
|
||||
raise UploadError(exc.amount)
|
||||
result = self.cloud.push(
|
||||
obj_ids, jobs, remote=remote, odb=odb or dest_odb
|
||||
)
|
||||
if result.failed:
|
||||
raise UploadError(len(result.failed))
|
||||
pushed += len(result.transferred)
|
||||
return pushed
|
||||
|
||||
+17
-1
@@ -1,6 +1,13 @@
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from dvc.dvcfile import DVC_FILE_SUFFIX
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileDoesNotExistError,
|
||||
StageFileIsNotDvcFileError,
|
||||
StageNotFound,
|
||||
)
|
||||
|
||||
from . import locked
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -11,7 +18,16 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@locked
|
||||
def remove(self: "Repo", target: str, outs: bool = False):
|
||||
stages = self.stage.from_target(target)
|
||||
try:
|
||||
stages = self.stage.from_target(target, accept_group=False)
|
||||
except (StageNotFound, StageFileDoesNotExistError) as e:
|
||||
# If the user specified a tracked file as a target instead of a stage,
|
||||
# e.g. `data.csv` instead of `data.csv.dvc`,
|
||||
# give a more helpful error message.
|
||||
if self.fs.exists(target + DVC_FILE_SUFFIX):
|
||||
raise StageFileIsNotDvcFileError(target) from e
|
||||
else:
|
||||
raise
|
||||
|
||||
for stage in stages:
|
||||
stage.remove(remove_outs=outs, force=outs)
|
||||
|
||||
@@ -102,7 +102,6 @@ def reproduce(
|
||||
from .graph import get_pipeline, get_pipelines
|
||||
|
||||
glob = kwargs.pop("glob", False)
|
||||
accept_group = not glob
|
||||
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
@@ -137,7 +136,6 @@ def reproduce(
|
||||
self.stage.collect(
|
||||
target,
|
||||
recursive=recursive,
|
||||
accept_group=accept_group,
|
||||
glob=glob,
|
||||
)
|
||||
)
|
||||
|
||||
+13
-25
@@ -82,7 +82,6 @@ def _collect_specific_target(
|
||||
target: str,
|
||||
with_deps: bool,
|
||||
recursive: bool,
|
||||
accept_group: bool,
|
||||
) -> Tuple[StageIter, "OptStr", "OptStr"]:
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
|
||||
@@ -98,13 +97,11 @@ def _collect_specific_target(
|
||||
msg = "Checking if stage '%s' is in '%s'"
|
||||
logger.debug(msg, target, PIPELINE_FILE)
|
||||
if not (recursive and loader.fs.isdir(target)):
|
||||
stages = _maybe_collect_from_dvc_yaml(
|
||||
loader, target, with_deps, accept_group=accept_group
|
||||
)
|
||||
stages = _maybe_collect_from_dvc_yaml(loader, target, with_deps)
|
||||
if stages:
|
||||
return stages, file, name
|
||||
elif not with_deps and is_valid_filename(file):
|
||||
stages = loader.load_all(file, name, accept_group=accept_group)
|
||||
stages = loader.load_all(file, name)
|
||||
return stages, file, name
|
||||
return [], file, name
|
||||
|
||||
@@ -209,7 +206,7 @@ class StageLoad:
|
||||
return stage
|
||||
|
||||
def from_target(
|
||||
self, target: str, accept_group: bool = False, glob: bool = False
|
||||
self, target: str, accept_group: bool = True, glob: bool = False
|
||||
) -> StageList:
|
||||
"""
|
||||
Returns a list of stage from the provided target.
|
||||
@@ -228,10 +225,9 @@ class StageLoad:
|
||||
path, name = parse_target(target)
|
||||
return self.load_one(path=path, name=name)
|
||||
|
||||
@staticmethod
|
||||
def _get_filepath(path: str = None, name: str = None) -> str:
|
||||
def _get_filepath(self, path: str = None, name: str = None) -> str:
|
||||
if path:
|
||||
return path
|
||||
return self.repo.fs.path.realpath(path)
|
||||
|
||||
path = PIPELINE_FILE
|
||||
logger.debug("Assuming '%s' to be a stage inside '%s'", name, path)
|
||||
@@ -250,15 +246,12 @@ class StageLoad:
|
||||
self,
|
||||
stages: "StageLoader",
|
||||
name: str = None,
|
||||
accept_group: bool = False,
|
||||
accept_group: bool = True,
|
||||
glob: bool = False,
|
||||
) -> Iterable[str]:
|
||||
|
||||
assert not (accept_group and glob)
|
||||
|
||||
if not name:
|
||||
return stages.keys()
|
||||
|
||||
if accept_group and stages.is_foreach_generated(name):
|
||||
return self._get_group_keys(stages, name)
|
||||
if glob:
|
||||
@@ -269,7 +262,7 @@ class StageLoad:
|
||||
self,
|
||||
path: str = None,
|
||||
name: str = None,
|
||||
accept_group: bool = False,
|
||||
accept_group: bool = True,
|
||||
glob: bool = False,
|
||||
) -> StageList:
|
||||
"""Load a list of stages from a file.
|
||||
@@ -338,7 +331,6 @@ class StageLoad:
|
||||
with_deps: bool = False,
|
||||
recursive: bool = False,
|
||||
graph: "DiGraph" = None,
|
||||
accept_group: bool = False,
|
||||
glob: bool = False,
|
||||
) -> StageIter:
|
||||
"""Collect list of stages from the provided target.
|
||||
@@ -347,15 +339,14 @@ class StageLoad:
|
||||
target: if not provided, all of the stages in the graph are
|
||||
returned.
|
||||
Target can be:
|
||||
- a stage name in the `dvc.yaml` file.
|
||||
- a foreach group name or a stage name in the `dvc.yaml` file.
|
||||
- a generated stage name from a foreach group.
|
||||
- a path to `dvc.yaml` or `.dvc` file.
|
||||
- in case of a stage to a dvc.yaml file in a different
|
||||
directory than current working directory, it can be a path
|
||||
to dvc.yaml file, followed by a colon `:`, followed by stage
|
||||
name (eg: `../dvc.yaml:build`).
|
||||
- in case of `recursive`, it can be a path to a directory.
|
||||
- in case of `accept_group`, it can be a group name of
|
||||
`foreach` generated stage.
|
||||
- in case of `glob`, it can be a wildcard pattern to match
|
||||
stages. Example: `build*` for stages in `dvc.yaml` file, or
|
||||
`../dvc.yaml:build*` for stages in dvc.yaml in a different
|
||||
@@ -367,8 +358,6 @@ class StageLoad:
|
||||
recursive: if true and if `target` is a directory, all of the
|
||||
stages inside that directory is returned.
|
||||
graph: graph to use. Defaults to `repo.graph`.
|
||||
accept_group: if true, all of the `foreach` generated stages of
|
||||
the specified target is returned.
|
||||
glob: Use `target` as a pattern to match stages in a file.
|
||||
"""
|
||||
if not target:
|
||||
@@ -380,7 +369,7 @@ class StageLoad:
|
||||
path = self.fs.path.abspath(target)
|
||||
return collect_inside_path(path, graph or self.graph)
|
||||
|
||||
stages = self.from_target(target, accept_group=accept_group, glob=glob)
|
||||
stages = self.from_target(target, glob=glob)
|
||||
if not with_deps:
|
||||
return stages
|
||||
|
||||
@@ -392,14 +381,14 @@ class StageLoad:
|
||||
with_deps: bool = False,
|
||||
recursive: bool = False,
|
||||
graph: "DiGraph" = None,
|
||||
accept_group: bool = False,
|
||||
) -> List[StageInfo]:
|
||||
"""Collects a list of (stage, filter_info) from the given target.
|
||||
|
||||
Priority is in the order of following in case of ambiguity:
|
||||
- .dvc file or .yaml file
|
||||
- dir if recursive and directory exists
|
||||
- stage_name
|
||||
- foreach_group_name or stage_name
|
||||
- generated stage name from a foreach group
|
||||
- output file
|
||||
|
||||
Args:
|
||||
@@ -418,7 +407,7 @@ class StageLoad:
|
||||
target = as_posix(target)
|
||||
|
||||
stages, file, _ = _collect_specific_target(
|
||||
self, target, with_deps, recursive, accept_group
|
||||
self, target, with_deps, recursive
|
||||
)
|
||||
if not stages:
|
||||
if not (recursive and self.fs.isdir(target)):
|
||||
@@ -440,7 +429,6 @@ class StageLoad:
|
||||
with_deps,
|
||||
recursive,
|
||||
graph,
|
||||
accept_group=accept_group,
|
||||
)
|
||||
except StageFileDoesNotExistError as exc:
|
||||
# collect() might try to use `target` as a stage name
|
||||
|
||||
+3
-1
@@ -14,7 +14,9 @@ def _joint_status(pairs):
|
||||
status_info = {}
|
||||
|
||||
for stage, filter_info in pairs:
|
||||
if stage.frozen and not stage.is_repo_import:
|
||||
if stage.frozen and not (
|
||||
stage.is_repo_import or stage.is_versioned_import
|
||||
):
|
||||
logger.warning(
|
||||
"%s is frozen. Its dependencies are"
|
||||
" not going to be shown in the status output.",
|
||||
|
||||
+13
-1
@@ -10,6 +10,7 @@ def update(
|
||||
rev=None,
|
||||
recursive=False,
|
||||
to_remote=False,
|
||||
no_download=False,
|
||||
remote=None,
|
||||
jobs=None,
|
||||
):
|
||||
@@ -21,6 +22,11 @@ def update(
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
|
||||
if to_remote and no_download:
|
||||
raise InvalidArgumentError(
|
||||
"--to-remote can't be used with --no-download"
|
||||
)
|
||||
|
||||
if not to_remote and remote:
|
||||
raise InvalidArgumentError(
|
||||
"--remote can't be used without --to-remote"
|
||||
@@ -31,7 +37,13 @@ def update(
|
||||
stages.update(self.stage.collect(target, recursive=recursive))
|
||||
|
||||
for stage in stages:
|
||||
stage.update(rev, to_remote=to_remote, remote=remote, jobs=jobs)
|
||||
stage.update(
|
||||
rev,
|
||||
to_remote=to_remote,
|
||||
remote=remote,
|
||||
no_download=no_download,
|
||||
jobs=jobs,
|
||||
)
|
||||
dvcfile = Dvcfile(self, stage.path)
|
||||
dvcfile.dump(stage)
|
||||
|
||||
|
||||
+9
-2
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
@@ -10,6 +11,9 @@ from .fs import localfs
|
||||
from .lock import make_lock
|
||||
from .utils import relpath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
INFO_SCHEMA = {Required("pid"): int, Required("cmd"): str}
|
||||
|
||||
SCHEMA = Schema(
|
||||
@@ -19,6 +23,9 @@ SCHEMA = Schema(
|
||||
}
|
||||
)
|
||||
|
||||
RWLOCK_FILE = "rwlock"
|
||||
RWLOCK_LOCK = "rwlock.lock"
|
||||
|
||||
|
||||
class RWLockFileCorruptedError(DvcException):
|
||||
def __init__(self, path):
|
||||
@@ -35,10 +42,10 @@ class RWLockFileFormatError(DvcException):
|
||||
|
||||
@contextmanager
|
||||
def _edit_rwlock(lock_dir, fs, hardlink):
|
||||
path = fs.path.join(lock_dir, "rwlock")
|
||||
path = fs.path.join(lock_dir, RWLOCK_FILE)
|
||||
|
||||
rwlock_guard = make_lock(
|
||||
fs.path.join(lock_dir, "rwlock.lock"),
|
||||
fs.path.join(lock_dir, RWLOCK_LOCK),
|
||||
tmp_dir=lock_dir,
|
||||
hardlink_lock=hardlink,
|
||||
)
|
||||
|
||||
+15
-8
@@ -3,11 +3,11 @@ from collections.abc import Mapping
|
||||
from voluptuous import Any, Optional, Required, Schema
|
||||
|
||||
from dvc import dependency, output
|
||||
from dvc.output import CHECKSUMS_SCHEMA, Output
|
||||
from dvc.annotations import ANNOTATION_SCHEMA
|
||||
from dvc.output import CHECKSUMS_SCHEMA, DIR_FILES_SCHEMA, META_SCHEMA, Output
|
||||
from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
|
||||
from dvc.parsing.versions import SCHEMA_KWD, lockfile_version_schema
|
||||
from dvc.stage.params import StageParams
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
|
||||
STAGES = "stages"
|
||||
SINGLE_STAGE_SCHEMA = {
|
||||
@@ -25,10 +25,9 @@ SINGLE_STAGE_SCHEMA = {
|
||||
|
||||
DATA_SCHEMA = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required("path"): str,
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
LOCK_FILE_STAGE_SCHEMA = {
|
||||
Required(StageParams.PARAM_CMD): Any(str, list),
|
||||
@@ -47,16 +46,15 @@ LOCKFILE_V2_SCHEMA = {
|
||||
|
||||
OUT_PSTAGE_DETAILED_SCHEMA = {
|
||||
str: {
|
||||
**ANNOTATION_SCHEMA, # type: ignore
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
Output.PARAM_DESC: str,
|
||||
Output.PARAM_REMOTE: str,
|
||||
}
|
||||
}
|
||||
|
||||
PLOTS = "plots"
|
||||
PLOTS_SCHEMA = dict
|
||||
PLOT_PROPS = {
|
||||
Output.PARAM_PLOT_TEMPLATE: str,
|
||||
Output.PARAM_PLOT_X: str,
|
||||
@@ -112,6 +110,15 @@ def either_or(primary, fallback, fallback_includes=None):
|
||||
return validator
|
||||
|
||||
|
||||
PLOT_DEFINITION = {
|
||||
Output.PARAM_PLOT_X: str,
|
||||
Output.PARAM_PLOT_Y: Any(str, [str], {str: Any(str, [str])}),
|
||||
Output.PARAM_PLOT_X_LABEL: str,
|
||||
Output.PARAM_PLOT_Y_LABEL: str,
|
||||
Output.PARAM_PLOT_TITLE: str,
|
||||
Output.PARAM_PLOT_TEMPLATE: str,
|
||||
}
|
||||
SINGLE_PLOT_SCHEMA = {str: Any(PLOT_DEFINITION, None)}
|
||||
FOREACH_IN = {
|
||||
Required(FOREACH_KWD): Any(dict, list, str),
|
||||
Required(DO_KWD): STAGE_DEFINITION,
|
||||
@@ -120,9 +127,9 @@ SINGLE_PIPELINE_STAGE_SCHEMA = {
|
||||
str: either_or(STAGE_DEFINITION, FOREACH_IN, [FOREACH_KWD, DO_KWD])
|
||||
}
|
||||
MULTI_STAGE_SCHEMA = {
|
||||
PLOTS: Any(SINGLE_PLOT_SCHEMA, [SINGLE_PLOT_SCHEMA]),
|
||||
STAGES: SINGLE_PIPELINE_STAGE_SCHEMA,
|
||||
VARS_KWD: VARS_SCHEMA,
|
||||
PLOTS: PLOTS_SCHEMA,
|
||||
}
|
||||
|
||||
COMPILED_SINGLE_STAGE_SCHEMA = Schema(SINGLE_STAGE_SCHEMA)
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ def resolve_rev(scm: "Git", rev: str) -> str:
|
||||
except InternalRevError as exc:
|
||||
# `scm` will only resolve git branch and tag names,
|
||||
# if rev is not a sha it may be an abbreviated experiment name
|
||||
if not rev.startswith("refs/"):
|
||||
if not (rev == "HEAD" or rev.startswith("refs/")):
|
||||
from dvc.repo.experiments.utils import (
|
||||
AmbiguousExpRefInfo,
|
||||
resolve_name,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user