mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bccbbc1d94 | ||
|
|
d736b31d8b | ||
|
|
68c1cc095c | ||
|
|
c94d28594d | ||
|
|
d3d429ef7f | ||
|
|
6ac88104da | ||
|
|
d87cdba6d8 | ||
|
|
b25ef5cdfb | ||
|
|
435407185f | ||
|
|
0b192af6f4 | ||
|
|
c6b602a167 | ||
|
|
1d5de9c5ba | ||
|
|
0e5674ffea | ||
|
|
684fc32e36 | ||
|
|
81f37be9d6 | ||
|
|
3dfdd577e7 | ||
|
|
bfb6a32b51 | ||
|
|
33de761560 | ||
|
|
ff38aed927 | ||
|
|
b62f17ffaf | ||
|
|
a0c781a676 | ||
|
|
6ac7dfc008 | ||
|
|
b0372918ea | ||
|
|
4b0fa4d559 | ||
|
|
a24c389674 | ||
|
|
f386f7c082 | ||
|
|
eadd0da394 | ||
|
|
06673dd212 | ||
|
|
5f0b1edb40 | ||
|
|
05701e26db | ||
|
|
109288e1cd | ||
|
|
9e6f44bc1c | ||
|
|
68b24307f9 | ||
|
|
ea8a4a3db4 | ||
|
|
08ae68caf4 | ||
|
|
4400fb4090 | ||
|
|
28848c5407 | ||
|
|
b13184fcec | ||
|
|
ee5fbc72ab | ||
|
|
d7fd68a1ae | ||
|
|
59d6d5ea98 | ||
|
|
6fc0a6ff98 | ||
|
|
b4b5db4b4e | ||
|
|
23e4d376c9 | ||
|
|
089a90c969 | ||
|
|
198daec421 | ||
|
|
04eb6a9f00 | ||
|
|
0467f6a036 | ||
|
|
dc3d9c9677 | ||
|
|
fcf92b2755 | ||
|
|
c806275167 | ||
|
|
bd1add0b95 | ||
|
|
e36f0e9d85 | ||
|
|
bc1b8f3494 | ||
|
|
9169d273e3 | ||
|
|
bb7512687c | ||
|
|
334f297ad3 | ||
|
|
ace3ba0ef9 | ||
|
|
959d149381 | ||
|
|
7a65d67e7f | ||
|
|
e9be6779ea | ||
|
|
876d123f04 | ||
|
|
2bfe403a28 | ||
|
|
9bef123182 | ||
|
|
e98ca36f39 | ||
|
|
b9bf73f043 | ||
|
|
7628fe9d44 | ||
|
|
d415a05460 | ||
|
|
f5699ce16b | ||
|
|
baeedead3a | ||
|
|
5630db55ad | ||
|
|
f1bd47ce0e | ||
|
|
74a13a4d9a | ||
|
|
b7732a591d | ||
|
|
d57bb23c93 | ||
|
|
28402dedf1 | ||
|
|
df60f5d9bf | ||
|
|
7ea73b2596 | ||
|
|
aa2e830511 | ||
|
|
6fdd6673f3 | ||
|
|
fac5f18dea | ||
|
|
d8068430e7 | ||
|
|
25b1ef9971 | ||
|
|
df563e43c1 | ||
|
|
3ba6b1f0bf | ||
|
|
6a9fbaf3d2 | ||
|
|
a036eaf817 | ||
|
|
665185b9e7 | ||
|
|
43e281521e | ||
|
|
73b46a5789 | ||
|
|
8124396218 | ||
|
|
e95ae5613d | ||
|
|
ca4dd08e37 | ||
|
|
0ebc253a43 | ||
|
|
6bfce5a5db | ||
|
|
5fa33a06bc | ||
|
|
6a75f3c327 | ||
|
|
d7587556c9 | ||
|
|
bb80eab77a | ||
|
|
ab3f8fb1a4 | ||
|
|
6edca79b79 | ||
|
|
2a888970e8 | ||
|
|
73fdfc8059 | ||
|
|
8dc11417d2 | ||
|
|
b02855fb93 | ||
|
|
af255a46b4 | ||
|
|
2748492f6b | ||
|
|
c69d3188a6 | ||
|
|
9b9c04e633 | ||
|
|
350e0fb1f7 | ||
|
|
f14358bc72 | ||
|
|
705bc6ec82 | ||
|
|
61a02e6f56 | ||
|
|
8e5226ece0 | ||
|
|
2a1af0b880 | ||
|
|
014d149512 | ||
|
|
77ae4aa92e | ||
|
|
588471a811 | ||
|
|
e59c19e752 | ||
|
|
b0a92414fa | ||
|
|
dae500d7c7 | ||
|
|
a01bc2918e | ||
|
|
a33bbc97d5 | ||
|
|
ba2e8bdac7 | ||
|
|
4d6e045989 | ||
|
|
00eea4560e | ||
|
|
6e02dddabd | ||
|
|
b550c5e625 | ||
|
|
b36861ada9 | ||
|
|
d482e26c99 | ||
|
|
a8ad59ae4e | ||
|
|
76cc649f5e | ||
|
|
d52e1aa17b | ||
|
|
65f61acffa | ||
|
|
fdc689954f | ||
|
|
076c48532d | ||
|
|
e4ea463c73 | ||
|
|
87d8269365 | ||
|
|
a753ee215f | ||
|
|
014a38fae2 | ||
|
|
8913f5a6fe | ||
|
|
4ce9825c2b | ||
|
|
e9c5b5fa84 | ||
|
|
fa54c1af7c | ||
|
|
9fbbfd07a6 | ||
|
|
929de7c651 | ||
|
|
49af6e1435 | ||
|
|
a6bfc7db4e | ||
|
|
872a0d663e | ||
|
|
c01583f3dd | ||
|
|
e0d41c7012 | ||
|
|
628b992442 | ||
|
|
99e7f52bbd | ||
|
|
b2a275ee9e | ||
|
|
7e423b668f | ||
|
|
2c3956bdb0 | ||
|
|
a48a19ce31 | ||
|
|
1de983f0de | ||
|
|
eb20a8e049 | ||
|
|
78e25747a4 | ||
|
|
240f0ebdc3 | ||
|
|
69f78d3bb6 | ||
|
|
740fe504c9 | ||
|
|
7bb84286f5 | ||
|
|
3891e81bdd | ||
|
|
bb62833897 | ||
|
|
fb703150ce | ||
|
|
33d4ead8b6 | ||
|
|
76b1f8c907 | ||
|
|
d7152e360e | ||
|
|
ab69b88a21 | ||
|
|
43a8eab2e0 | ||
|
|
15f35e827e | ||
|
|
4d17c171f1 | ||
|
|
c27005a8b7 | ||
|
|
ecd371f8ae | ||
|
|
bda1388bff | ||
|
|
c02bd7f318 | ||
|
|
fe1d805c80 | ||
|
|
f9b0de883f | ||
|
|
1cad8ea7fd | ||
|
|
08a643e005 | ||
|
|
97a8b6978c | ||
|
|
b44099367e | ||
|
|
29b3dc1f9e | ||
|
|
6e73bd0b5b | ||
|
|
fa5a9abdeb | ||
|
|
30775f7b31 | ||
|
|
e07aca9fd3 | ||
|
|
d2265f36ba | ||
|
|
d548618291 | ||
|
|
48ff332f4a | ||
|
|
97f5ffc3c6 | ||
|
|
f6849d2f06 | ||
|
|
a172240536 | ||
|
|
129a4fbb92 | ||
|
|
c101514677 | ||
|
|
5e883bc00c | ||
|
|
4b720e5820 | ||
|
|
c0054fa57e | ||
|
|
6049494cef | ||
|
|
120a5879bb | ||
|
|
c8e39523c3 | ||
|
|
7648e715f8 | ||
|
|
ca5ad6457f | ||
|
|
de848d9598 | ||
|
|
4330f1a70f | ||
|
|
144e21b552 | ||
|
|
7945428e5e | ||
|
|
6df70d388e | ||
|
|
4ca2158b9d | ||
|
|
f87e78d754 | ||
|
|
5899440b46 | ||
|
|
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 |
@@ -1,10 +0,0 @@
|
||||
comment: false
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 90
|
||||
threshold: 10%
|
||||
patch:
|
||||
default:
|
||||
threshold: 25%
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = dvc
|
||||
omit =
|
||||
*__main__.py
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
if __name__ == .__main__.:
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
raise NotImplementedError
|
||||
# pragma: no cover
|
||||
# pragma: no cover
|
||||
\.\.\.$
|
||||
@@ -1,4 +1,3 @@
|
||||
/tests
|
||||
/dvc
|
||||
/.github
|
||||
/bin
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[flake8]
|
||||
ignore=
|
||||
E203, # Whitespace before ':'
|
||||
E266, # Too many leading '#' for block comment
|
||||
W503, # Line break occurred before a binary operator
|
||||
P1, # unindexed parameters in the str.format, see:
|
||||
# https://pypi.org/project/flake8-string-format/
|
||||
max_line_length = 79
|
||||
max-complexity = 15
|
||||
select = B,C,E,F,W,T4,B902,T,P
|
||||
show_source = true
|
||||
count = true
|
||||
@@ -8,7 +8,7 @@ about: Create a bug report to help us improve DVC
|
||||
<!--
|
||||
## Issue name
|
||||
|
||||
Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug.
|
||||
Issue names must follow the pattern `command: description` where the command is the dvc command that you are trying to run. The description should describe the consequence of the bug.
|
||||
|
||||
Example: `repro: doesn't detect input changes`
|
||||
-->
|
||||
|
||||
@@ -2,4 +2,3 @@
|
||||
name: "\U0001F680 Feature Request"
|
||||
about: Suggest an idea for this project
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
codecov:
|
||||
notify:
|
||||
wait_for_ci: true
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 2%
|
||||
patch: off
|
||||
github_checks:
|
||||
annotations: false
|
||||
@@ -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! 🎉
|
||||
@@ -0,0 +1,16 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels: ["skip-changelog"]
|
||||
categories:
|
||||
- title: 🛠 Breaking Changes
|
||||
labels: ["breaking-change"]
|
||||
- title: 🚀 New Features and Enhancements
|
||||
labels: ["feature", "enhancement", "ui"]
|
||||
- title: ⚡ Optimizations
|
||||
labels: ["optimize", "performance"]
|
||||
- title: 🐛 Bug Fixes
|
||||
labels: ["bugfix", "bug"]
|
||||
- title: 🔨 Maintenance
|
||||
labels: ["maintenance", "refactoring", "chore", "build", "ci"]
|
||||
- title: Other Changes
|
||||
labels: ["*"]
|
||||
@@ -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.11"
|
||||
- uses: iterative/dvc-bench@main
|
||||
with:
|
||||
pytest_options: "-k 'test_init or test_help'"
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
- cron: '32 19 * * 2'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['python']
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
@@ -7,8 +7,13 @@ on:
|
||||
types: [released, prereleased]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
permissions:
|
||||
contents: write # for actions/upload-release-asset to upload release asset
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -24,13 +29,13 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.9
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.10"
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
setup.cfg
|
||||
pyproject.toml
|
||||
scripts/build-requirements.txt
|
||||
|
||||
- name: Set up Ruby 2.6
|
||||
@@ -45,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install -U wheel pip setuptools
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
|
||||
@@ -77,21 +82,31 @@ jobs:
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: pip
|
||||
cache-dependency-path: setup.cfg
|
||||
|
||||
- name: Install
|
||||
run: pip install --upgrade pip wheel
|
||||
|
||||
- name: Force version for Test PyPI uploads
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
||||
run: |
|
||||
pip install -U wheel pip build setuptools
|
||||
pip install ".[all,tests]"
|
||||
pip install setuptools_scm
|
||||
echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
pip install -U pip
|
||||
./scripts/build_package.sh
|
||||
run: ./scripts/build_package.sh
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.version }}
|
||||
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.TEST_PYPI_TOKEN }}
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
skip_existing: true
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
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
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
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
|
||||
@@ -27,13 +27,12 @@ jobs:
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: pip
|
||||
cache-dependency-path: pyproject.toml
|
||||
- name: Install requirements
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install collective.checkdocs==0.2
|
||||
pip install ".[dev]" pre-commit
|
||||
- name: Check README
|
||||
run: python setup.py checkdocs
|
||||
pip install --upgrade pip wheel
|
||||
pip install ".[dev]"
|
||||
- uses: pre-commit/[email protected]
|
||||
tests:
|
||||
timeout-minutes: 45
|
||||
@@ -41,13 +40,16 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.8", "3.9", "3.10"]
|
||||
pytest-filter:
|
||||
os: [ubuntu-20.04, windows-latest]
|
||||
pyv: ["3.8", "3.9", "3.10", "3.11"]
|
||||
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"}
|
||||
- {os: macos-latest, pyv: "3.8"}
|
||||
- {os: macos-latest, pyv: "3.9"}
|
||||
- {os: macos-latest, pyv: "3.10"}
|
||||
- {os: macos-latest, pyv: "3.11"}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -57,38 +59,34 @@ 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: pyproject.toml
|
||||
- name: install
|
||||
if: steps.cache.pip-cache-dir.cache-hit != 'true'
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install --upgrade pip wheel
|
||||
pip install -e ".[dev]"
|
||||
env:
|
||||
AIOHTTP_NO_EXTENSIONS: ${{ matrix.pyv == '3.11-dev' && '1' }}
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
run: pytest -nauto -ra --durations 100 --cov=dvc --cov-report=xml --cov-report=term -k "${{ matrix.pytest-filter }}"
|
||||
timeout-minutes: 30
|
||||
run: >
|
||||
pytest -nauto --timeout=300 --durations=100
|
||||
--cov --cov-report=xml --cov-report=term
|
||||
-k "${{ matrix.pytest-filter }}"
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
check:
|
||||
if: always()
|
||||
needs: [lint, tests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
notify:
|
||||
if: github.ref == 'refs/heads/main' && failure()
|
||||
needs:
|
||||
- lint
|
||||
- tests
|
||||
needs: [lint, tests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Notification
|
||||
|
||||
+43
-28
@@ -1,39 +1,62 @@
|
||||
default_language_version:
|
||||
python: python3
|
||||
|
||||
ci:
|
||||
skip: [mypy, pylint, dvc-pre-commit]
|
||||
|
||||
repos:
|
||||
- hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
repo: https://github.com/ambv/black
|
||||
rev: 22.6.0
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
exclude: "tests/func/utils/test_strict_yaml.py"
|
||||
args: ['--assume-in-merge']
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
- --ignore-words-list
|
||||
- ba,datas,fo,uptodate,cachable
|
||||
repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.1.0
|
||||
- hooks:
|
||||
- id: isort
|
||||
language_version: python3
|
||||
repo: https://github.com/timothycrosley/isort
|
||||
- id: debug-statements
|
||||
exclude: "dvc/_debug.py"
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/lovesegfault/beautysh
|
||||
rev: v6.2.1
|
||||
hooks:
|
||||
- id: beautysh
|
||||
args: ["-i", "2"]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
- --py38-plus
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.10.1
|
||||
- hooks:
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.2
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies: ["tomli"]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 5.0.4
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
additional_dependencies:
|
||||
- flake8-broken-line
|
||||
- flake8-bugbear
|
||||
- flake8-comprehensions
|
||||
- flake8-debugger
|
||||
- flake8-string-format
|
||||
repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.9.2
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: mypy
|
||||
@@ -47,14 +70,6 @@ repos:
|
||||
entry: pylint
|
||||
language: system
|
||||
types: [python]
|
||||
- hooks:
|
||||
- args:
|
||||
- -i
|
||||
- "2"
|
||||
id: beautysh
|
||||
language_version: python3
|
||||
repo: https://github.com/lovesegfault/beautysh
|
||||
rev: v6.2.1
|
||||
- hooks:
|
||||
- args:
|
||||
- git-hook
|
||||
|
||||
@@ -74,4 +74,3 @@ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.ht
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
include LICENSE
|
||||
recursive-include requirements *.txt
|
||||
recursive-include dvc/repo/plots/templates *.json
|
||||
+7
-3
@@ -13,7 +13,7 @@
|
||||
|
||||
|CI| |Python Version| |Coverage| |VS Code| |DOI|
|
||||
|
||||
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
|
||||
|PyPI| |PyPI Downloads| |Packages| |Brew| |Conda| |Choco| |Snap|
|
||||
|
||||
|
|
||||
|
||||
@@ -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.
|
||||
@@ -276,6 +276,10 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: PyPI
|
||||
|
||||
.. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/dvc.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: PyPI Downloads
|
||||
|
||||
.. |Packages| image:: https://img.shields.io/github/v/release/iterative/dvc?label=deb|pkg|rpm|exe&logo=GitHub
|
||||
:target: https://github.com/iterative/dvc/releases/latest
|
||||
:alt: deb|pkg|rpm|exe
|
||||
@@ -292,6 +296,6 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://github.com/iterative/dvc/graphs/contributors
|
||||
:alt: Contributors
|
||||
|
||||
.. |VS Code| image:: https://vsmarketplacebadge.apphb.com/version/Iterative.dvc.svg
|
||||
.. |VS Code| image:: https://img.shields.io/visual-studio-marketplace/v/Iterative.dvc?color=blue&label=VSCode&logo=visualstudiocode&logoColor=blue
|
||||
:target: https://marketplace.visualstudio.com/items?itemName=Iterative.dvc
|
||||
:alt: VS Code Extension
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo OFF
|
||||
set PYTHONPATH=%DVC_HOME%
|
||||
python -m dvc %*
|
||||
+1
-1
@@ -4,6 +4,6 @@ DVC
|
||||
Make your data science projects reproducible and shareable.
|
||||
"""
|
||||
import dvc.logger
|
||||
from dvc.version import __version__ # noqa: F401
|
||||
from dvc.version import __version__, version_tuple # noqa: F401
|
||||
|
||||
dvc.logger.setup()
|
||||
|
||||
+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)
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
from dvc.fs.dvc import _DvcFileSystem as DvcFileSystem
|
||||
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .data import open # pylint: disable=redefined-builtin
|
||||
from .data import get_url, read
|
||||
@@ -11,5 +11,5 @@ __all__ = [
|
||||
"open",
|
||||
"params_show",
|
||||
"read",
|
||||
"DvcFileSystem",
|
||||
"DVCFileSystem",
|
||||
]
|
||||
|
||||
+5
-16
@@ -2,8 +2,8 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.commands import (
|
||||
add,
|
||||
cache,
|
||||
@@ -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,
|
||||
@@ -126,17 +128,6 @@ class DvcParser(argparse.ArgumentParser):
|
||||
return args
|
||||
|
||||
|
||||
class VersionAction(argparse.Action): # pragma: no cover
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Shows DVC version and exits."""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
from dvc import __version__
|
||||
|
||||
print(__version__)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def get_parent_parser():
|
||||
"""Create instances of a parser containing common arguments shared among
|
||||
all the commands.
|
||||
@@ -184,13 +175,11 @@ def get_main_parser():
|
||||
help="Show this help message and exit.",
|
||||
)
|
||||
|
||||
# NOTE: On some python versions action='version' prints to stderr
|
||||
# instead of stdout https://bugs.python.org/issue18920
|
||||
parser.add_argument(
|
||||
"-V",
|
||||
"--version",
|
||||
action=VersionAction,
|
||||
nargs=0,
|
||||
action="version",
|
||||
version=__version__,
|
||||
help="Show program's version.",
|
||||
)
|
||||
|
||||
|
||||
+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
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -8,7 +8,11 @@ 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
|
||||
|
||||
@@ -251,7 +255,7 @@ def add_parser(subparsers, parent_parser):
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DATA_LS_HELP, "data/ls"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=DATA_LS_HELP,
|
||||
add_help=False,
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--md",
|
||||
@@ -289,5 +293,5 @@ def add_parser(subparsers, parent_parser):
|
||||
".dvc files, or stage names."
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
|
||||
data_ls_parser.set_defaults(func=CmdDataLs)
|
||||
hide_subparsers_from_help(data_subparsers)
|
||||
|
||||
@@ -120,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
|
||||
|
||||
@@ -195,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,
|
||||
@@ -18,6 +18,7 @@ from dvc.commands.experiments import (
|
||||
queue_worker,
|
||||
remove,
|
||||
run,
|
||||
save,
|
||||
show,
|
||||
)
|
||||
|
||||
@@ -34,6 +35,7 @@ SUB_COMMANDS = [
|
||||
queue_worker,
|
||||
remove,
|
||||
run,
|
||||
save,
|
||||
show,
|
||||
]
|
||||
|
||||
@@ -59,7 +61,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(
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,18 +17,13 @@ class CmdExperimentsList(CmdBase):
|
||||
num=self.args.num,
|
||||
git_remote=self.args.git_remote,
|
||||
)
|
||||
|
||||
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]
|
||||
if not name_only:
|
||||
print(f"{name}:")
|
||||
ui.write(f"{baseline}:")
|
||||
for exp_name in exps[baseline]:
|
||||
indent = "" if name_only else "\t"
|
||||
print(f"{indent}{exp_name}")
|
||||
ui.write(f"{indent}{exp_name}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Replace local experiment already exists.",
|
||||
help="Replace local experiment if it already exists.",
|
||||
)
|
||||
experiments_pull_parser.add_argument(
|
||||
"--no-cache",
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsSave(CmdBase):
|
||||
def run(self):
|
||||
|
||||
try:
|
||||
ref = self.repo.experiments.save(
|
||||
name=self.args.name,
|
||||
force=self.args.force,
|
||||
include_untracked=self.args.include_untracked,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to save experiment")
|
||||
return 1
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json({"ref": ref})
|
||||
else:
|
||||
name = self.repo.experiments.get_exact_name([ref])[ref]
|
||||
ui.write(f"Experiment has been saved as: {name}")
|
||||
ui.write(
|
||||
"\nTo promote an experiment to a Git branch run:\n\n"
|
||||
"\tdvc exp branch <exp> <branch>\n"
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_SAVE_HELP = "Save current workspace as a dvc experiment."
|
||||
save_parser = experiments_subparsers.add_parser(
|
||||
"save",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_SAVE_HELP, "exp/save"),
|
||||
help=EXPERIMENTS_SAVE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Replace experiment if it already exists.",
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"-n",
|
||||
"--name",
|
||||
default=None,
|
||||
help=(
|
||||
"Human-readable experiment name. If not specified, a name will "
|
||||
"be auto-generated."
|
||||
),
|
||||
metavar="<name>",
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"-I",
|
||||
"--include-untracked",
|
||||
action="append",
|
||||
default=[],
|
||||
help="List of untracked paths to include in the experiment.",
|
||||
metavar="<path>",
|
||||
)
|
||||
save_parser.set_defaults(func=CmdExperimentsSave)
|
||||
@@ -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",
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.scm import CloneError
|
||||
|
||||
from ..cli.utils import append_doc_link
|
||||
|
||||
@@ -44,6 +45,9 @@ class CmdGet(CmdBaseNoRepo):
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
return 0
|
||||
except CloneError:
|
||||
logger.exception(f"failed to get '{self.args.path}'")
|
||||
return 1
|
||||
except DvcException:
|
||||
logger.exception(
|
||||
"failed to get '{}' from '{}'".format(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -5,6 +5,7 @@ from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.scm import CloneError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,6 +27,9 @@ class CmdImport(CmdBase):
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
except CloneError:
|
||||
logger.exception(f"failed to import '{self.args.path}'")
|
||||
return 1
|
||||
except DvcException:
|
||||
logger.exception(
|
||||
"failed to import '{}' from '{}'.".format(
|
||||
|
||||
+22
-12
@@ -25,6 +25,7 @@ class CmdImportUrl(CmdBase):
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
version_aware=self.args.version_aware,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception(
|
||||
@@ -72,6 +73,18 @@ 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(
|
||||
"--to-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Download it directly to the remote",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-r",
|
||||
"--remote",
|
||||
help="Remote storage to download to",
|
||||
metavar="<name>",
|
||||
)
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
@@ -86,18 +99,6 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Create .dvc file including target data hash value(s)"
|
||||
" but do not actually download the file(s).",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--to-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Download it directly to the remote",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-r",
|
||||
"--remote",
|
||||
help="Remote storage to download to",
|
||||
metavar="<name>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
@@ -108,5 +109,14 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--version-aware",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Import using cloud versioning. Implied if the URL contains a "
|
||||
"version ID."
|
||||
),
|
||||
)
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImportUrl)
|
||||
|
||||
@@ -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)
|
||||
+2
-15
@@ -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:
|
||||
|
||||
@@ -11,13 +11,18 @@ class CmdQueueKill(CmdBase):
|
||||
"""Kill exp task in queue."""
|
||||
|
||||
def run(self):
|
||||
self.repo.experiments.celery_queue.kill(revs=self.args.task)
|
||||
self.repo.experiments.celery_queue.kill(
|
||||
revs=self.args.task, force=self.args.force
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(queue_subparsers, parent_parser):
|
||||
QUEUE_KILL_HELP = "Kill actively running experiment queue tasks."
|
||||
QUEUE_KILL_HELP = (
|
||||
"Gracefully interrupt running experiment queue tasks "
|
||||
"(equivalent to Ctrl-C)"
|
||||
)
|
||||
queue_kill_parser = queue_subparsers.add_parser(
|
||||
"kill",
|
||||
parents=[parent_parser],
|
||||
@@ -25,6 +30,13 @@ def add_parser(queue_subparsers, parent_parser):
|
||||
help=QUEUE_KILL_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
queue_kill_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Forcefully and immediately kill running experiment queue tasks",
|
||||
)
|
||||
queue_kill_parser.add_argument(
|
||||
"task",
|
||||
nargs="*",
|
||||
|
||||
+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",
|
||||
|
||||
@@ -238,26 +238,6 @@ def _add_common_args(parser):
|
||||
help="Declare output plot file (do not put into DVC cache).",
|
||||
metavar="<path>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live", help="Declare output as dvclive.", metavar="<path>"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live-no-cache",
|
||||
help="Declare output as dvclive (do not put into DVC cache).",
|
||||
metavar="<path>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live-no-summary",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live-no-html",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Signal dvclive logger to not produce training report.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--wdir",
|
||||
|
||||
@@ -31,7 +31,8 @@ class CmdUpdate(CmdBase):
|
||||
def add_parser(subparsers, parent_parser):
|
||||
UPDATE_HELP = (
|
||||
"Update data artifact imported (via dvc import or dvc import-url) "
|
||||
"from an external DVC repository or URL."
|
||||
"from an external DVC repository or URL, or update data from a "
|
||||
"DVC worktree remote."
|
||||
)
|
||||
update_parser = subparsers.add_parser(
|
||||
"update",
|
||||
@@ -60,7 +61,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Update .dvc file hash value(s)"
|
||||
help="Update .dvc file git revision/hash value(s)"
|
||||
" but do not download the file(s).",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
|
||||
+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()))
|
||||
|
||||
@@ -97,6 +97,8 @@ HTTP_COMMON = {
|
||||
"ask_password": Bool,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"method": str,
|
||||
"connect_timeout": All(Coerce(float), Range(0, min_included=True)),
|
||||
"read_timeout": All(Coerce(float), Range(0, min_included=True)),
|
||||
Optional("verify", default=False): Bool,
|
||||
}
|
||||
WEBDAV_COMMON = {
|
||||
@@ -180,6 +182,8 @@ SCHEMA = {
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
"passphrase": str,
|
||||
"ask_passphrase": Bool,
|
||||
"keyfile": str,
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
|
||||
+19
-11
@@ -5,12 +5,14 @@ from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.config import NoRemoteError, RemoteConfigError
|
||||
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.hashfile.status import CompareStatusResult
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,6 +24,14 @@ class Remote:
|
||||
|
||||
self.worktree = config.pop("worktree", False)
|
||||
self.config = config
|
||||
if self.worktree:
|
||||
version_aware = self.config.get("version_aware")
|
||||
if version_aware is False:
|
||||
raise RemoteConfigError(
|
||||
"worktree remotes require version_aware cloud"
|
||||
)
|
||||
if version_aware is None:
|
||||
self.fs.version_aware = True
|
||||
|
||||
@cached_property
|
||||
def odb(self):
|
||||
@@ -53,8 +63,6 @@ class DataCloud:
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "Remote":
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
if not name:
|
||||
name = self.repo.config["core"].get("remote")
|
||||
|
||||
@@ -87,6 +95,10 @@ class DataCloud:
|
||||
command: str = "<command>",
|
||||
) -> "HashFileDB":
|
||||
remote = self.get_remote(name=name, command=command)
|
||||
if remote.fs.version_aware or remote.worktree:
|
||||
raise NoRemoteError(
|
||||
f"'{command}' is unsupported for cloud versioned remotes"
|
||||
)
|
||||
return remote.odb
|
||||
|
||||
def _log_missing(self, status: "CompareStatusResult"):
|
||||
@@ -106,14 +118,10 @@ class DataCloud:
|
||||
dest_odb: "HashFileDB",
|
||||
objs: Iterable["HashInfo"],
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.exceptions import FileTransferError
|
||||
from dvc_data.hashfile.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,
|
||||
@@ -121,7 +129,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:
|
||||
@@ -148,7 +156,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:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections import defaultdict
|
||||
from typing import Any, Mapping
|
||||
|
||||
from dvc.output import ARTIFACT_SCHEMA, Output
|
||||
from dvc.output import ARTIFACT_SCHEMA, DIR_FILES_SCHEMA, Output
|
||||
|
||||
from .base import Dependency
|
||||
from .param import ParamsDependency
|
||||
@@ -15,10 +15,11 @@ SCHEMA: Mapping[str, Any] = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**RepoDependency.REPO_SCHEMA,
|
||||
**ParamsDependency.PARAM_SCHEMA,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
|
||||
|
||||
def _get(stage, p, info, fs_config=None):
|
||||
def _get(stage, p, info, **kwargs):
|
||||
if info and info.get(RepoDependency.PARAM_REPO):
|
||||
repo = info.pop(RepoDependency.PARAM_REPO)
|
||||
return RepoDependency(repo, stage, p, info)
|
||||
@@ -27,14 +28,15 @@ def _get(stage, p, info, fs_config=None):
|
||||
params = info.pop(ParamsDependency.PARAM_PARAMS)
|
||||
return ParamsDependency(stage, p, params)
|
||||
|
||||
return Dependency(stage, p, info, fs_config=fs_config)
|
||||
return Dependency(stage, p, info, **kwargs)
|
||||
|
||||
|
||||
def loadd_from(stage, d_list):
|
||||
ret = []
|
||||
for d in d_list:
|
||||
p = d.pop(Output.PARAM_PATH, None)
|
||||
ret.append(_get(stage, p, d))
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
ret.append(_get(stage, p, d, files=files))
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
+37
-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,39 @@ 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
|
||||
)
|
||||
|
||||
def dumpd(self, **kwargs):
|
||||
if self.fs.version_aware:
|
||||
kwargs["with_files"] = True
|
||||
return super().dumpd(**kwargs)
|
||||
|
||||
+21
-7
@@ -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):
|
||||
@@ -94,12 +96,12 @@ 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
|
||||
@@ -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,14 +147,16 @@ 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.fs.dvc import DVCFileSystem
|
||||
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+58
-13
@@ -1,7 +1,18 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
|
||||
@@ -172,7 +183,7 @@ class FileMixin:
|
||||
def dump(self, stage, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -180,6 +191,10 @@ class SingleStageFile(FileMixin):
|
||||
from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA
|
||||
from dvc.stage.loader import SingleStageLoader as LOADER
|
||||
|
||||
metrics: List[str] = []
|
||||
plots: Dict[str, Any] = {}
|
||||
params: List[str] = []
|
||||
|
||||
@property
|
||||
def stage(self):
|
||||
data, raw = self._load()
|
||||
@@ -204,12 +219,12 @@ class SingleStageFile(FileMixin):
|
||||
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)
|
||||
|
||||
|
||||
@@ -223,6 +238,11 @@ class PipelineFile(FileMixin):
|
||||
def _lockfile(self):
|
||||
return Lockfile(self.repo, os.path.splitext(self.path)[0] + ".lock")
|
||||
|
||||
def _reset(self):
|
||||
self.__dict__.pop("contents", None)
|
||||
self.__dict__.pop("lockfile_contents", None)
|
||||
self.__dict__.pop("resolver", None)
|
||||
|
||||
def dump(
|
||||
self, stage, update_pipeline=True, update_lock=True, **kwargs
|
||||
): # pylint: disable=arguments-differ
|
||||
@@ -237,10 +257,10 @@ 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, action: str = "dump") -> None:
|
||||
@@ -278,11 +298,36 @@ class PipelineFile(FileMixin):
|
||||
"PipelineFile has multiple stages. Please specify it's name."
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def contents(self):
|
||||
return self._load()[0]
|
||||
|
||||
@cached_property
|
||||
def lockfile_contents(self):
|
||||
return self._lockfile.load()
|
||||
|
||||
@cached_property
|
||||
def resolver(self):
|
||||
from .parsing import DataResolver
|
||||
|
||||
wdir = self.repo.fs.path.parent(self.path)
|
||||
return DataResolver(self.repo, wdir, self.contents)
|
||||
|
||||
@property
|
||||
def stages(self):
|
||||
data, _ = self._load()
|
||||
lockfile_data = self._lockfile.load()
|
||||
return self.LOADER(self, data, lockfile_data)
|
||||
return self.LOADER(self, self.contents, self.lockfile_contents)
|
||||
|
||||
@property
|
||||
def metrics(self):
|
||||
return self.contents.get("metrics", [])
|
||||
|
||||
@property
|
||||
def plots(self):
|
||||
return self.contents.get("plots", {})
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
return self.contents.get("params", [])
|
||||
|
||||
def remove(self, force=False):
|
||||
if not force:
|
||||
@@ -310,7 +355,7 @@ class PipelineFile(FileMixin):
|
||||
else:
|
||||
super().remove()
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -366,7 +411,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)
|
||||
@@ -411,7 +456,7 @@ class Lockfile(FileMixin):
|
||||
else:
|
||||
self.remove()
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
+2
-3
@@ -2,11 +2,10 @@ DVC_CHECKPOINT = "DVC_CHECKPOINT"
|
||||
DVC_DAEMON = "DVC_DAEMON"
|
||||
DVC_PAGER = "DVC_PAGER"
|
||||
DVC_ROOT = "DVC_ROOT"
|
||||
DVCLIVE_PATH = "DVCLIVE_PATH"
|
||||
DVCLIVE_SUMMARY = "DVCLIVE_SUMMARY"
|
||||
DVCLIVE_HTML = "DVCLIVE_HTML"
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
|
||||
DVC_EXP_NAME = "DVC_EXP_NAME"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
|
||||
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
|
||||
|
||||
+15
-3
@@ -132,10 +132,17 @@ class NotDvcRepoError(DvcException):
|
||||
class CyclicGraphError(DvcException):
|
||||
def __init__(self, stages):
|
||||
assert isinstance(stages, list)
|
||||
msg = "Pipeline has a cycle involving: {}.".format(
|
||||
", ".join(s.addressing for s in stages)
|
||||
stage_part = "stage" if len(stages) == 1 else "stages"
|
||||
msg = (
|
||||
"Same item(s) are defined as both a dependency and an output "
|
||||
"in {stage_part}: {stage}."
|
||||
)
|
||||
super().__init__(
|
||||
msg.format(
|
||||
stage_part=stage_part,
|
||||
stage=", ".join(s.addressing for s in stages),
|
||||
)
|
||||
)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class ConfirmRemoveError(DvcException):
|
||||
@@ -278,6 +285,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)
|
||||
|
||||
+58
-54
@@ -1,3 +1,4 @@
|
||||
import errno
|
||||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
@@ -9,9 +10,7 @@ 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.utils.fs import makedirs
|
||||
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
|
||||
@@ -64,16 +63,8 @@ def _get_dvc_path(dvc_fs, subkey):
|
||||
return dvc_fs.path.join(*subkey) if subkey else ""
|
||||
|
||||
|
||||
class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
"""DVC + git-tracked files fs.
|
||||
|
||||
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 = "/"
|
||||
|
||||
def __init__(
|
||||
@@ -85,6 +76,41 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
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__()
|
||||
@@ -222,6 +248,9 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
def _open(
|
||||
self, path, mode="rb", **kwargs
|
||||
): # pylint: disable=arguments-renamed, arguments-differ
|
||||
if mode != "rb":
|
||||
raise OSError(errno.EROFS, os.strerror(errno.EROFS))
|
||||
|
||||
key = self._get_key_from_relative(path)
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
@@ -234,7 +263,8 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.open(dvc_path, mode=mode)
|
||||
|
||||
def isdvc(self, path, **kwargs):
|
||||
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)
|
||||
@@ -246,43 +276,38 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
names = set()
|
||||
dvc_infos = {}
|
||||
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))
|
||||
for info in dvc_fs.ls(dvc_path, detail=True):
|
||||
dvc_infos[dvc_fs.path.name(info["name"])] = info
|
||||
|
||||
fs_infos = {}
|
||||
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 repo.dvcignore.ls(
|
||||
fs, fs_path, detail=False, ignore_subrepos=ignore_subrepos
|
||||
for info in repo.dvcignore.ls(
|
||||
fs, fs_path, detail=True, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
names.add(fs.path.name(entry))
|
||||
fs_infos[fs.path.name(info["name"])] = info
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
pass
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
if not dvcfiles:
|
||||
names = (name for name in names if not _is_dvc_file(name))
|
||||
|
||||
infos = []
|
||||
paths = []
|
||||
names = set(dvc_infos.keys()) | set(fs_infos.keys())
|
||||
for name in names:
|
||||
entry_path = self.path.join(path, name)
|
||||
entry_key = key + (name,)
|
||||
try:
|
||||
info = self._info(
|
||||
entry_key,
|
||||
entry_path,
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
check_ignored=False,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
if not dvcfiles and _is_dvc_file(name):
|
||||
continue
|
||||
|
||||
entry_path = self.path.join(path, name)
|
||||
info = _merge_info(repo, fs_infos.get(name), dvc_infos.get(name))
|
||||
info["name"] = entry_path
|
||||
infos.append(info)
|
||||
paths.append(entry_path)
|
||||
|
||||
@@ -291,27 +316,6 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
return infos
|
||||
|
||||
def get_file( # pylint: disable=arguments-differ
|
||||
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
key = self._get_key_from_relative(rpath)
|
||||
fs_path = self._from_key(key)
|
||||
fs = self.repo.fs
|
||||
|
||||
if self.isdir(rpath):
|
||||
makedirs(lpath, exist_ok=True)
|
||||
return None
|
||||
|
||||
try:
|
||||
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
|
||||
return
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_fs.get_file(dvc_path, lpath, callback=callback, **kwargs)
|
||||
|
||||
def info(self, path, **kwargs):
|
||||
key = self._get_key_from_relative(path)
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
@@ -361,7 +365,7 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
return info
|
||||
|
||||
|
||||
class DvcFileSystem(FileSystem):
|
||||
class DVCFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
@@ -371,7 +375,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)
|
||||
|
||||
+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)
|
||||
|
||||
+1
-1
@@ -76,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"]
|
||||
)
|
||||
|
||||
|
||||
+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()
|
||||
|
||||
@@ -26,7 +26,7 @@ class BaseMachineBackend(ABC):
|
||||
) -> Iterator[dict]:
|
||||
"""Iterate over status of all instances of the specified machine."""
|
||||
|
||||
def close(self):
|
||||
def close(self): # noqa: B027
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
||||
+130
-78
@@ -26,10 +26,12 @@ from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.istextfile import istextfile
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.hashfile.transfer import transfer as otransfer
|
||||
from dvc_data.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
|
||||
|
||||
@@ -38,6 +40,8 @@ if TYPE_CHECKING:
|
||||
from dvc_data.index import DataIndexKey
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
from .fs.callbacks import Callback
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -83,10 +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)
|
||||
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)
|
||||
push = d.pop(Output.PARAM_PUSH, True)
|
||||
ret.append(
|
||||
_get(
|
||||
stage,
|
||||
@@ -97,10 +101,10 @@ def loadd_from(stage, d_list):
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
live=live,
|
||||
remote=remote,
|
||||
**annot,
|
||||
files=files,
|
||||
push=push,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
@@ -114,8 +118,8 @@ def loads_from(
|
||||
plot=False,
|
||||
persist=False,
|
||||
checkpoint=False,
|
||||
live=False,
|
||||
remote=None,
|
||||
push=True,
|
||||
):
|
||||
return [
|
||||
_get(
|
||||
@@ -127,8 +131,8 @@ def loads_from(
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
live=live,
|
||||
remote=remote,
|
||||
push=push,
|
||||
)
|
||||
for s in s_list
|
||||
]
|
||||
@@ -162,29 +166,21 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
stage.PARAM_OUTS,
|
||||
stage.PARAM_METRICS,
|
||||
stage.PARAM_PLOTS,
|
||||
stage.PARAM_LIVE,
|
||||
):
|
||||
raise ValueError(f"'{typ}' key is not allowed for pipeline files.")
|
||||
|
||||
metric = typ == stage.PARAM_METRICS
|
||||
plot = typ == stage.PARAM_PLOTS
|
||||
live = typ == stage.PARAM_LIVE
|
||||
if live:
|
||||
# `live` is single object
|
||||
data = [data]
|
||||
|
||||
d = _merge_data(data)
|
||||
|
||||
for path, flags in d.items():
|
||||
plt_d, live_d = {}, {}
|
||||
plt_d = {}
|
||||
if plot:
|
||||
from dvc.schema import PLOT_PROPS
|
||||
|
||||
plt_d, flags = _split_dict(flags, keys=PLOT_PROPS.keys())
|
||||
if live:
|
||||
from dvc.schema import LIVE_PROPS
|
||||
|
||||
live_d, flags = _split_dict(flags, keys=LIVE_PROPS.keys())
|
||||
extra = project(
|
||||
flags,
|
||||
[
|
||||
@@ -192,6 +188,7 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
Output.PARAM_REMOTE,
|
||||
Output.PARAM_PUSH,
|
||||
*ANNOTATION_FIELDS,
|
||||
],
|
||||
)
|
||||
@@ -202,7 +199,6 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
info={},
|
||||
plot=plt_d or plot,
|
||||
metric=metric,
|
||||
live=live_d or live,
|
||||
**extra,
|
||||
)
|
||||
|
||||
@@ -259,10 +255,8 @@ class Output:
|
||||
PARAM_PLOT_TITLE = "title"
|
||||
PARAM_PLOT_HEADER = "header"
|
||||
PARAM_PERSIST = "persist"
|
||||
PARAM_LIVE = "live"
|
||||
PARAM_LIVE_SUMMARY = "summary"
|
||||
PARAM_LIVE_HTML = "html"
|
||||
PARAM_REMOTE = "remote"
|
||||
PARAM_PUSH = "push"
|
||||
|
||||
METRIC_SCHEMA = Any(
|
||||
None,
|
||||
@@ -288,7 +282,6 @@ class Output:
|
||||
plot=False,
|
||||
persist=False,
|
||||
checkpoint=False,
|
||||
live=False,
|
||||
desc=None,
|
||||
type=None, # pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
@@ -297,6 +290,7 @@ class Output:
|
||||
repo=None,
|
||||
fs_config=None,
|
||||
files: List[Dict[str, Any]] = None,
|
||||
push: bool = True,
|
||||
):
|
||||
self.annot = Annotation(
|
||||
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
||||
@@ -351,7 +345,7 @@ class Output:
|
||||
self.plot = False if self.IS_DEPENDENCY else plot
|
||||
self.persist = persist
|
||||
self.checkpoint = checkpoint
|
||||
self.live = live
|
||||
self.can_push = push
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj: Optional["HashFile"] = None
|
||||
@@ -373,8 +367,12 @@ class Output:
|
||||
name=self.hash_name,
|
||||
value=getattr(self.meta, self.hash_name, None),
|
||||
)
|
||||
if self.hash_info and self.hash_info.isdir:
|
||||
if self.meta.nfiles or self.hash_info and self.hash_info.isdir:
|
||||
self.meta.isdir = True
|
||||
if not self.hash_info and self.hash_name != "md5":
|
||||
md5 = getattr(self.meta, "md5", None)
|
||||
if md5:
|
||||
self.hash_info = HashInfo("md5", md5)
|
||||
|
||||
def _parse_path(self, fs, fs_path):
|
||||
parsed = urlparse(self.def_path)
|
||||
@@ -511,6 +509,36 @@ class Output:
|
||||
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
|
||||
|
||||
if self.files and not self.obj:
|
||||
self.obj = self.get_obj()
|
||||
|
||||
entry = DataIndexEntry(
|
||||
meta=self.meta,
|
||||
obj=self.obj,
|
||||
hash_info=self.hash_info,
|
||||
odb=self.odb,
|
||||
cache=self.odb,
|
||||
remote=remote,
|
||||
)
|
||||
if self.stage.is_import and not self.stage.is_repo_import:
|
||||
dep = self.stage.deps[0]
|
||||
entry.fs = dep.fs
|
||||
entry.path = dep.fs_path
|
||||
entry.meta = dep.meta
|
||||
if not entry.obj:
|
||||
if not dep.obj and dep.files:
|
||||
dep.obj = dep.get_obj()
|
||||
entry.obj = dep.obj
|
||||
return entry
|
||||
|
||||
def changed_checksum(self):
|
||||
return self.hash_info != self.get_hash()
|
||||
|
||||
@@ -530,7 +558,7 @@ class Output:
|
||||
|
||||
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 self.meta.version_id != self.get_meta().version_id
|
||||
return False
|
||||
|
||||
def workspace_status(self):
|
||||
@@ -610,8 +638,16 @@ class Output:
|
||||
if self.metric:
|
||||
self.verify_metric()
|
||||
|
||||
if not self.use_cache:
|
||||
_, self.meta, obj = build(
|
||||
if self.use_cache:
|
||||
_, self.meta, self.obj = build(
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
else:
|
||||
_, self.meta, self.obj = build(
|
||||
self.repo.odb.local,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
@@ -619,23 +655,11 @@ class Output:
|
||||
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
|
||||
)
|
||||
return
|
||||
|
||||
assert not self.IS_DEPENDENCY
|
||||
|
||||
_, self.meta, self.obj = build(
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.files = None
|
||||
|
||||
@@ -733,41 +757,38 @@ class Output:
|
||||
|
||||
ret[self.PARAM_PATH] = path
|
||||
|
||||
if self.IS_DEPENDENCY:
|
||||
return ret
|
||||
if not self.IS_DEPENDENCY:
|
||||
ret.update(self.annot.to_dict())
|
||||
if not self.use_cache:
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
|
||||
ret.update(self.annot.to_dict())
|
||||
if not self.use_cache:
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
if isinstance(self.metric, dict):
|
||||
if (
|
||||
self.PARAM_METRIC_XPATH in self.metric
|
||||
and not self.metric[self.PARAM_METRIC_XPATH]
|
||||
):
|
||||
del self.metric[self.PARAM_METRIC_XPATH]
|
||||
|
||||
if isinstance(self.metric, dict):
|
||||
if (
|
||||
self.PARAM_METRIC_XPATH in self.metric
|
||||
and not self.metric[self.PARAM_METRIC_XPATH]
|
||||
):
|
||||
del self.metric[self.PARAM_METRIC_XPATH]
|
||||
if self.metric:
|
||||
ret[self.PARAM_METRIC] = self.metric
|
||||
|
||||
if self.metric:
|
||||
ret[self.PARAM_METRIC] = self.metric
|
||||
if self.plot:
|
||||
ret[self.PARAM_PLOT] = self.plot
|
||||
|
||||
if self.plot:
|
||||
ret[self.PARAM_PLOT] = self.plot
|
||||
if self.persist:
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
|
||||
if self.persist:
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
if self.checkpoint:
|
||||
ret[self.PARAM_CHECKPOINT] = self.checkpoint
|
||||
|
||||
if self.checkpoint:
|
||||
ret[self.PARAM_CHECKPOINT] = self.checkpoint
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
|
||||
if self.live:
|
||||
ret[self.PARAM_LIVE] = self.live
|
||||
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
if not self.can_push:
|
||||
ret[self.PARAM_PUSH] = self.can_push
|
||||
|
||||
if (
|
||||
self.use_cache
|
||||
and self.is_in_repo
|
||||
(not self.IS_DEPENDENCY or self.stage.is_import)
|
||||
and self.hash_info.isdir
|
||||
and (kwargs.get("with_files") or self.files is not None)
|
||||
):
|
||||
@@ -800,15 +821,6 @@ 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: Optional[str] = None, **kwargs
|
||||
) -> Optional["HashFile"]:
|
||||
@@ -828,6 +840,7 @@ class Output:
|
||||
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)
|
||||
@@ -838,7 +851,7 @@ class Output:
|
||||
def checkout(
|
||||
self,
|
||||
force=False,
|
||||
progress_callback=None,
|
||||
progress_callback: "Callback" = DEFAULT_CALLBACK,
|
||||
relink=False,
|
||||
filter_info=None,
|
||||
allow_missing=False,
|
||||
@@ -846,9 +859,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
|
||||
|
||||
@@ -1030,9 +1043,15 @@ class Output:
|
||||
if not self.use_cache:
|
||||
return {}
|
||||
|
||||
push: bool = kwargs.pop("push", False)
|
||||
if self.stage.is_repo_import:
|
||||
if push:
|
||||
return {}
|
||||
return self.get_used_external(**kwargs)
|
||||
|
||||
if push and not self.can_push:
|
||||
return {}
|
||||
|
||||
if not self.hash_info:
|
||||
msg = (
|
||||
"Output '{}'({}) is missing version info. "
|
||||
@@ -1127,7 +1146,7 @@ class Output:
|
||||
"unable to auto-merge outputs that are not directories"
|
||||
)
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
from dvc_data.hashfile.tree import MergeError as TreeMergeError
|
||||
from dvc_data.hashfile.tree import du, merge
|
||||
|
||||
@@ -1144,7 +1163,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
|
||||
@@ -1168,11 +1191,39 @@ class Output:
|
||||
|
||||
@property
|
||||
def is_metric(self) -> bool:
|
||||
return bool(self.metric) or bool(self.live)
|
||||
return bool(self.metric)
|
||||
|
||||
@property
|
||||
def is_plot(self) -> bool:
|
||||
return bool(self.plot) or bool(self.live)
|
||||
return bool(self.plot)
|
||||
|
||||
def restore_fields(self, other: "Output"):
|
||||
"""Restore attributes that need to be preserved when serialized."""
|
||||
self.annot = other.annot
|
||||
self.remote = other.remote
|
||||
self.can_push = other.can_push
|
||||
|
||||
def merge_version_meta(self, other: "Output"):
|
||||
"""Merge version meta for files which are unchanged from other."""
|
||||
if not self.hash_info:
|
||||
return
|
||||
if self.hash_info.isdir:
|
||||
return self._merge_dir_version_meta(other)
|
||||
if self.hash_info != other.hash_info:
|
||||
return
|
||||
self.meta = other.meta
|
||||
|
||||
def _merge_dir_version_meta(self, other: "Output"):
|
||||
from dvc_data.hashfile.tree import update_meta
|
||||
|
||||
if not self.obj or not other.hash_info.isdir:
|
||||
return
|
||||
other_obj = other.obj if other.obj is not None else other.get_obj()
|
||||
assert isinstance(self.obj, Tree) and isinstance(other_obj, Tree)
|
||||
updated = update_meta(self.obj, other_obj)
|
||||
assert updated.hash_info == self.obj.hash_info
|
||||
self.obj = updated
|
||||
self.files = updated.as_list(with_meta=True)
|
||||
|
||||
|
||||
META_SCHEMA = {
|
||||
@@ -1203,5 +1254,6 @@ SCHEMA = {
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
|
||||
Output.PARAM_REMOTE: str,
|
||||
Output.PARAM_PUSH: bool,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ from .interpolate import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import NoReturn
|
||||
from typing import NoReturn
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import re
|
||||
import typing
|
||||
from collections.abc import Iterable, Mapping
|
||||
@@ -9,10 +10,9 @@ from dvc.exceptions import DvcException
|
||||
from dvc.utils.flatten import flatten
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import List, Match
|
||||
from typing import List, Match, NoReturn
|
||||
|
||||
from pyparsing import ParseException
|
||||
from typing_extensions import NoReturn
|
||||
|
||||
from .context import Context
|
||||
|
||||
@@ -71,6 +71,16 @@ def embrace(s: str):
|
||||
return BRACE_OPEN + s + BRACE_CLOSE
|
||||
|
||||
|
||||
def escape_str(value):
|
||||
if os.name == "nt":
|
||||
from subprocess import list2cmdline
|
||||
|
||||
return list2cmdline([value])
|
||||
from shlex import quote
|
||||
|
||||
return quote(value)
|
||||
|
||||
|
||||
@singledispatch
|
||||
def to_str(obj) -> str:
|
||||
return str(obj)
|
||||
@@ -98,12 +108,12 @@ def _(obj: dict):
|
||||
result += f"--no-{k} "
|
||||
|
||||
elif isinstance(v, str):
|
||||
result += f"--{k} '{v}' "
|
||||
result += f"--{k} {escape_str(v)} "
|
||||
|
||||
elif isinstance(v, Iterable):
|
||||
for n, i in enumerate(v):
|
||||
if isinstance(i, str):
|
||||
i = f"'{i}'"
|
||||
i = escape_str(i)
|
||||
elif isinstance(i, Iterable):
|
||||
raise ParseError(
|
||||
f"Cannot interpolate nested iterable in '{k}'"
|
||||
|
||||
+11
-5
@@ -2,20 +2,26 @@ 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
|
||||
|
||||
|
||||
def _get_converter(
|
||||
renderer_class, props
|
||||
renderer_class, renderer_id, props, data
|
||||
) -> Union[VegaConverter, ImageConverter]:
|
||||
from dvc_render import ImageRenderer, VegaRenderer
|
||||
|
||||
if renderer_class.TYPE == VegaRenderer.TYPE:
|
||||
return VegaConverter(props)
|
||||
return VegaConverter(renderer_id, data, props)
|
||||
if renderer_class.TYPE == ImageRenderer.TYPE:
|
||||
return ImageConverter(props)
|
||||
return ImageConverter(renderer_id, data, props)
|
||||
|
||||
raise ValueError(f"Invalid renderer class {renderer_class}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
from typing import Dict, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
class Converter:
|
||||
def __init__(self, plot_properties: Optional[Dict] = None):
|
||||
self.plot_properties = plot_properties or {}
|
||||
def __init__(
|
||||
self,
|
||||
plot_id: str,
|
||||
data: Dict[str, Any] = None,
|
||||
properties: Optional[Dict] = None,
|
||||
):
|
||||
self.plot_id = plot_id
|
||||
self.properties = properties or {}
|
||||
self.data = data or {}
|
||||
|
||||
def convert(self, data, revision: str, filename: str, **kwargs):
|
||||
def convert(self) -> Tuple[List[Tuple[str, str, Any]], Dict]:
|
||||
raise NotImplementedError
|
||||
|
||||
def flat_datapoints(self, revision: str) -> Tuple[List[Dict], Dict]:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, List, Tuple
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
|
||||
|
||||
from dvc.render import FILENAME_FIELD, REVISION_FIELD, SRC_FIELD
|
||||
|
||||
@@ -34,25 +34,33 @@ class ImageConverter(Converter):
|
||||
base64_str = base64.b64encode(image_data).decode()
|
||||
return f"data:image;base64,{base64_str}"
|
||||
|
||||
def convert(
|
||||
self, data, revision: str, filename: str, **kwargs
|
||||
) -> Tuple[List[Dict], Dict]:
|
||||
def convert(self) -> Tuple[List[Tuple[str, str, Any]], Dict]:
|
||||
datas = []
|
||||
for filename, image_data in self.data.items():
|
||||
datas.append((filename, "", image_data))
|
||||
return datas, self.properties
|
||||
|
||||
def flat_datapoints(self, revision: str) -> Tuple[List[Dict], Dict]:
|
||||
"""
|
||||
Convert the DVC Plots content to DVC Render datapoints.
|
||||
Return both generated datapoints and updated properties.
|
||||
"""
|
||||
path = self.plot_properties.get("out")
|
||||
if path:
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
src = self._write_image(
|
||||
os.path.abspath(path), revision, filename, data
|
||||
)
|
||||
else:
|
||||
src = self._encode_image(data)
|
||||
datapoint = {
|
||||
REVISION_FIELD: revision,
|
||||
FILENAME_FIELD: filename,
|
||||
SRC_FIELD: src,
|
||||
}
|
||||
return [datapoint], self.plot_properties
|
||||
path = self.properties.get("out")
|
||||
datapoints = []
|
||||
datas, properties = self.convert()
|
||||
for filename, _, image_data in datas:
|
||||
if path:
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
src = self._write_image(
|
||||
os.path.abspath(path), revision, filename, image_data
|
||||
)
|
||||
else:
|
||||
src = self._encode_image(image_data)
|
||||
datapoint = {
|
||||
REVISION_FIELD: revision,
|
||||
FILENAME_FIELD: filename,
|
||||
SRC_FIELD: src,
|
||||
}
|
||||
datapoints.append(datapoint)
|
||||
return datapoints, properties
|
||||
|
||||
+277
-259
@@ -1,90 +1,100 @@
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from typing import Dict, Iterable, List, Optional, Set, Union
|
||||
from typing import Any, Dict, Iterable, List, Tuple, Union
|
||||
|
||||
from funcy import first, project
|
||||
from funcy import first, last
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.render import (
|
||||
FILENAME_FIELD,
|
||||
INDEX_FIELD,
|
||||
REVISION_FIELD,
|
||||
VERSION_FIELD,
|
||||
)
|
||||
from dvc.render import FILENAME_FIELD, INDEX_FIELD, VERSION_FIELD
|
||||
|
||||
from . import Converter
|
||||
|
||||
|
||||
class FieldsNotFoundError(DvcException):
|
||||
def __init__(self, expected_fields, found_fields):
|
||||
expected_str = ", ".join(expected_fields)
|
||||
class FieldNotFoundError(DvcException):
|
||||
def __init__(self, expected_field, found_fields):
|
||||
found_str = ", ".join(found_fields)
|
||||
super().__init__(
|
||||
f"Could not find all provided fields ('{expected_str}') "
|
||||
f"Could not find provided field ('{expected_field}') "
|
||||
f"in data fields ('{found_str}')."
|
||||
)
|
||||
|
||||
|
||||
class PlotDataStructureError(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"Plot data extraction failed. Please see "
|
||||
"https://man.dvc.org/plots for supported data formats."
|
||||
)
|
||||
def _lists(blob: Union[Dict, List]) -> Iterable[List]:
|
||||
if isinstance(blob, list):
|
||||
yield blob
|
||||
else:
|
||||
for _, value in blob.items():
|
||||
if isinstance(value, dict):
|
||||
yield from _lists(value)
|
||||
elif isinstance(value, list):
|
||||
yield value
|
||||
|
||||
|
||||
def _filter_fields(
|
||||
datapoints: List[Dict], fields: Set, **kwargs
|
||||
) -> List[Dict]:
|
||||
if not fields:
|
||||
return datapoints
|
||||
assert isinstance(fields, set)
|
||||
|
||||
new_data = []
|
||||
for data_point in datapoints:
|
||||
keys = set(data_point.keys())
|
||||
if not fields <= keys:
|
||||
raise FieldsNotFoundError(fields, keys)
|
||||
|
||||
new_data.append(project(data_point, fields))
|
||||
|
||||
return new_data
|
||||
def _file_field(*args):
|
||||
for axis_def in args:
|
||||
if axis_def is not None:
|
||||
for file, val in axis_def.items():
|
||||
if isinstance(val, str):
|
||||
yield file, val
|
||||
elif isinstance(val, list):
|
||||
for field in val:
|
||||
yield file, field
|
||||
|
||||
|
||||
def _lists(dictionary: Dict):
|
||||
for _, value in dictionary.items():
|
||||
if isinstance(value, dict):
|
||||
yield from _lists(value)
|
||||
elif isinstance(value, list):
|
||||
yield value
|
||||
def _find(
|
||||
filename: str,
|
||||
field: str,
|
||||
data_series: List[Tuple[str, str, Any]],
|
||||
):
|
||||
for data_file, data_field, data in data_series:
|
||||
if data_file == filename and data_field == field:
|
||||
return data_file, data_field, data
|
||||
return None
|
||||
|
||||
|
||||
def _find_first_list(
|
||||
data: Union[Dict, List], fields: Set, **kwargs
|
||||
) -> List[Dict]:
|
||||
fields = fields or set()
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
for lst in _lists(data):
|
||||
if (
|
||||
all(isinstance(dp, dict) for dp in lst)
|
||||
# if fields is empty, it will match any set
|
||||
and set(first(lst).keys()) & fields == fields
|
||||
):
|
||||
return lst
|
||||
|
||||
raise PlotDataStructureError()
|
||||
def _verify_field(file2datapoints: Dict[str, List], filename: str, field: str):
|
||||
if filename in file2datapoints:
|
||||
datapoint = first(file2datapoints[filename])
|
||||
if field not in datapoint:
|
||||
raise FieldNotFoundError(field, datapoint.keys())
|
||||
return
|
||||
|
||||
|
||||
def _append_index(datapoints: List[Dict], **kwargs) -> List[Dict]:
|
||||
if INDEX_FIELD in first(datapoints).keys():
|
||||
return datapoints
|
||||
def _get_xs(properties: Dict, file2datapoints: Dict[str, List[Dict]]):
|
||||
x = properties.get("x", None)
|
||||
if x is not None and isinstance(x, dict):
|
||||
for filename, field in _file_field(x):
|
||||
_verify_field(file2datapoints, filename, field)
|
||||
yield filename, field
|
||||
|
||||
for index, data_point in enumerate(datapoints):
|
||||
data_point[INDEX_FIELD] = index
|
||||
return datapoints
|
||||
|
||||
def _get_ys(properties, file2datapoints: Dict[str, List[Dict]]):
|
||||
y = properties.get("y", None)
|
||||
if y is not None:
|
||||
for filename, field in _file_field(y):
|
||||
_verify_field(file2datapoints, filename, field)
|
||||
yield filename, field
|
||||
|
||||
|
||||
def _is_datapoints(lst: List[Dict]):
|
||||
"""
|
||||
check if dict keys match, datapoints with different keys mgiht lead
|
||||
to unexpected behavior
|
||||
"""
|
||||
|
||||
return all(isinstance(item, dict) for item in lst) and set(
|
||||
first(lst).keys()
|
||||
) == {key for keys in lst for key in keys}
|
||||
|
||||
|
||||
def get_datapoints(file_content: Dict):
|
||||
result: List[Dict[str, Any]] = []
|
||||
for lst in _lists(file_content):
|
||||
if _is_datapoints(lst):
|
||||
for index, datapoint in enumerate(lst):
|
||||
if len(result) <= index:
|
||||
result.append({})
|
||||
result[index].update(datapoint)
|
||||
return result
|
||||
|
||||
|
||||
class VegaConverter(Converter):
|
||||
@@ -95,222 +105,230 @@ class VegaConverter(Converter):
|
||||
('x', 'y') it will attempt to fill in the blanks.
|
||||
"""
|
||||
|
||||
def __init__(self, plot_properties: Optional[Dict] = None):
|
||||
super().__init__(plot_properties)
|
||||
def __init__(
|
||||
self, plot_id: str, data: Dict = None, properties: Dict = None
|
||||
):
|
||||
super().__init__(plot_id, data, properties)
|
||||
self.plot_id = plot_id
|
||||
self.inferred_properties: Dict = {}
|
||||
|
||||
self.steps = []
|
||||
# TODO we should be handling that in `convert`,
|
||||
# to avoid stateful `self.inferred_properties`
|
||||
self._infer_x_y()
|
||||
|
||||
self._infer_x()
|
||||
self._infer_fields()
|
||||
def _infer_y_from_data(self):
|
||||
if self.plot_id in self.data:
|
||||
for lst in _lists(self.data[self.plot_id]):
|
||||
if all(isinstance(item, dict) for item in lst):
|
||||
datapoint = first(lst)
|
||||
field = last(datapoint.keys())
|
||||
self.inferred_properties["y"] = {self.plot_id: field}
|
||||
break
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"find_data",
|
||||
partial(
|
||||
_find_first_list,
|
||||
fields=self.inferred_properties.get("fields", set())
|
||||
- {INDEX_FIELD},
|
||||
),
|
||||
)
|
||||
)
|
||||
def _infer_x_y(self):
|
||||
x = self.properties.get("x", None)
|
||||
y = self.properties.get("y", None)
|
||||
|
||||
if not self.plot_properties.get("x", None):
|
||||
self.steps.append(("append_index", partial(_append_index)))
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"filter_fields",
|
||||
partial(
|
||||
_filter_fields,
|
||||
fields=self.inferred_properties.get("fields", set()),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"generate_y",
|
||||
partial(
|
||||
self._generate_y_values,
|
||||
y_values=self.plot_properties.get("y", None),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def _infer_x(self):
|
||||
if not self.plot_properties.get("x", None):
|
||||
self.inferred_properties["x"] = INDEX_FIELD
|
||||
|
||||
def skip_step(self, name: str):
|
||||
self.steps = [(_name, fn) for _name, fn in self.steps if _name != name]
|
||||
|
||||
def _infer_fields(self):
|
||||
fields = self.plot_properties.get("fields", set())
|
||||
if fields:
|
||||
fields = {
|
||||
*fields,
|
||||
self.plot_properties.get("x", None),
|
||||
self.plot_properties.get("y", None),
|
||||
self.inferred_properties.get("x", None),
|
||||
} - {None}
|
||||
self.inferred_properties["fields"] = fields
|
||||
|
||||
def _infer_y(self, datapoints: List[Dict]):
|
||||
if "y" not in self.plot_properties:
|
||||
data_fields = list(first(datapoints))
|
||||
skip = (
|
||||
REVISION_FIELD,
|
||||
self.plot_properties.get("x", None)
|
||||
or self.inferred_properties.get("x"),
|
||||
FILENAME_FIELD,
|
||||
VERSION_FIELD,
|
||||
)
|
||||
inferred_y = first(
|
||||
f for f in reversed(data_fields) if f not in skip
|
||||
)
|
||||
if "y" in self.inferred_properties:
|
||||
previous_y = self.inferred_properties["y"]
|
||||
if previous_y != inferred_y:
|
||||
raise DvcException(
|
||||
f"Inferred y ('{inferred_y}' value does not match"
|
||||
f"previously matched one ('f{previous_y}')."
|
||||
)
|
||||
# Infer x.
|
||||
if isinstance(x, str):
|
||||
self.inferred_properties["x"] = {}
|
||||
# If multiple y files, duplicate x for each file.
|
||||
if isinstance(y, dict):
|
||||
for file, fields in y.items():
|
||||
# Duplicate x for each y.
|
||||
if isinstance(fields, list):
|
||||
self.inferred_properties["x"][file] = [x] * len(fields)
|
||||
else:
|
||||
self.inferred_properties["x"][file] = x
|
||||
# Otherwise use plot ID as file.
|
||||
else:
|
||||
self.inferred_properties["y"] = inferred_y
|
||||
self.inferred_properties["x"][self.plot_id] = x
|
||||
|
||||
# Infer y.
|
||||
if y is None:
|
||||
self._infer_y_from_data()
|
||||
# If y files not provided, use plot ID as file.
|
||||
elif not isinstance(y, dict):
|
||||
self.inferred_properties["y"] = {self.plot_id: y}
|
||||
|
||||
def _find_datapoints(self):
|
||||
result = {}
|
||||
for file, content in self.data.items():
|
||||
result[file] = get_datapoints(content)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def infer_y_label(properties):
|
||||
y_label = properties.get("y_label", None)
|
||||
if y_label is None:
|
||||
y = properties.get("y", None)
|
||||
if isinstance(y, dict):
|
||||
fields = {field for _, field in _file_field(y)}
|
||||
if len(fields) == 1:
|
||||
y_label = first(fields)
|
||||
else:
|
||||
y_label = "y"
|
||||
elif isinstance(y, list):
|
||||
y_label = "y"
|
||||
elif isinstance(y, str):
|
||||
y_label = y
|
||||
|
||||
return y_label
|
||||
|
||||
@staticmethod
|
||||
def infer_x_label(properties):
|
||||
x_label = properties.get("x_label", None)
|
||||
if x_label is None:
|
||||
x = properties.get("x", None)
|
||||
if isinstance(x, dict):
|
||||
fields = {field for _, field in _file_field(x)}
|
||||
if len(fields) == 1:
|
||||
x_label = first(fields)
|
||||
else:
|
||||
x_label = "x"
|
||||
else:
|
||||
x_label = INDEX_FIELD
|
||||
return x_label
|
||||
|
||||
def flat_datapoints(self, revision):
|
||||
|
||||
file2datapoints, properties = self.convert()
|
||||
|
||||
props_update = {}
|
||||
|
||||
xs = list(_get_xs(properties, file2datapoints))
|
||||
|
||||
# assign "step" if no x provided
|
||||
if not xs:
|
||||
x_file, x_field = (
|
||||
None,
|
||||
INDEX_FIELD,
|
||||
)
|
||||
else:
|
||||
x_file, x_field = xs[0]
|
||||
props_update["x"] = x_field
|
||||
|
||||
ys = list(_get_ys(properties, file2datapoints))
|
||||
|
||||
num_xs = len(xs)
|
||||
num_ys = len(ys)
|
||||
if num_xs > 1 and num_xs != num_ys:
|
||||
raise DvcException(
|
||||
f"Cannot have different number of x and y data sources. Found "
|
||||
f"{num_xs} x and {num_ys} y data sources."
|
||||
)
|
||||
|
||||
all_datapoints = []
|
||||
all_y_fields = {y_field for _, y_field in ys}
|
||||
|
||||
# override to unified y field name if there are different y fields
|
||||
if len(all_y_fields) > 1:
|
||||
props_update["y"] = "dvc_inferred_y_value"
|
||||
else:
|
||||
props_update["y"] = first(all_y_fields)
|
||||
|
||||
for i, (y_file, y_field) in enumerate(ys):
|
||||
if num_xs > 1:
|
||||
x_file, x_field = xs[i]
|
||||
datapoints = deepcopy(file2datapoints.get(y_file, []))
|
||||
|
||||
if props_update.get("y", None) == "dvc_inferred_y_value":
|
||||
_update_from_field(
|
||||
datapoints,
|
||||
field="dvc_inferred_y_value",
|
||||
source_field=y_field,
|
||||
)
|
||||
|
||||
if x_field == INDEX_FIELD and x_file is None:
|
||||
_update_from_index(datapoints, INDEX_FIELD)
|
||||
else:
|
||||
x_datapoints = file2datapoints.get(x_file, [])
|
||||
try:
|
||||
_update_from_field(
|
||||
datapoints,
|
||||
field=x_field,
|
||||
source_datapoints=x_datapoints,
|
||||
)
|
||||
except IndexError:
|
||||
raise DvcException(
|
||||
f"Cannot join '{x_field}' from '{x_file}' and "
|
||||
f"'{y_field}' from '{y_file}'. "
|
||||
"They have to have same length."
|
||||
)
|
||||
|
||||
_update_all(
|
||||
datapoints,
|
||||
update_dict={
|
||||
VERSION_FIELD: {
|
||||
"revision": revision,
|
||||
FILENAME_FIELD: y_file,
|
||||
"field": y_field,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
all_datapoints.extend(datapoints)
|
||||
|
||||
if not all_datapoints:
|
||||
return [], {}
|
||||
|
||||
properties = {**properties, **props_update}
|
||||
|
||||
return all_datapoints, properties
|
||||
|
||||
def convert(
|
||||
self,
|
||||
data,
|
||||
revision: str,
|
||||
filename: str,
|
||||
skip: List = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Convert the data. Fill necessary fields ('x', 'y') and return both
|
||||
generated datapoints and updated properties.
|
||||
generated datapoints and updated properties. If `x` is not provided,
|
||||
leave it as None, fronteds should handle it.
|
||||
|
||||
NOTE: Studio uses this method.
|
||||
The only thing studio FE handles is filling `x` and `y`.
|
||||
`x/y_label` should be filled here.
|
||||
|
||||
Datapoints are not stripped according to config, because users
|
||||
might be utilizing other fields in their custom plots.
|
||||
"""
|
||||
if not skip:
|
||||
skip = []
|
||||
datapoints = self._find_datapoints()
|
||||
properties = {**self.properties, **self.inferred_properties}
|
||||
|
||||
processed = deepcopy(data)
|
||||
properties["y_label"] = self.infer_y_label(properties)
|
||||
properties["x_label"] = self.infer_x_label(properties)
|
||||
|
||||
for step_name, step in self.steps:
|
||||
return datapoints, properties
|
||||
|
||||
if step_name not in skip:
|
||||
processed = step( # type: ignore
|
||||
processed,
|
||||
revision=revision,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
self._infer_y(processed) # type: ignore
|
||||
return processed, {**self.plot_properties, **self.inferred_properties}
|
||||
def _update_from_field(
|
||||
target_datapoints: List[Dict],
|
||||
field: str,
|
||||
source_datapoints: List[Dict] = None,
|
||||
source_field: str = None,
|
||||
):
|
||||
if source_datapoints is None:
|
||||
source_datapoints = target_datapoints
|
||||
if source_field is None:
|
||||
source_field = field
|
||||
|
||||
def _generate_y_values( # noqa: C901
|
||||
self,
|
||||
datapoints: List[Dict],
|
||||
y_values: Optional[Union[str, List, Dict]],
|
||||
revision: str,
|
||||
filename: str,
|
||||
**kwargs,
|
||||
) -> List[Dict]:
|
||||
if len(source_datapoints) != len(target_datapoints):
|
||||
raise IndexError(
|
||||
"Source and target datapoints must have the same length"
|
||||
)
|
||||
|
||||
result = []
|
||||
properties_update = {}
|
||||
for index, datapoint in enumerate(target_datapoints):
|
||||
source_datapoint = source_datapoints[index]
|
||||
if source_field in source_datapoint:
|
||||
datapoint[field] = source_datapoint[source_field]
|
||||
|
||||
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):
|
||||
res = {"revision": revision}
|
||||
if filename is not None:
|
||||
res["filename"] = filename
|
||||
if field is not None:
|
||||
res["field"] = field
|
||||
return res
|
||||
def _update_from_index(datapoints: List[Dict], new_field: str):
|
||||
for index, datapoint in enumerate(datapoints):
|
||||
datapoint[new_field] = index
|
||||
|
||||
def _generate_y(datapoint, field):
|
||||
tmp = datapoint.copy()
|
||||
tmp["dvc_inferred_y_value"] = datapoint[field]
|
||||
tmp = _add_version_info(
|
||||
tmp, _version_info(revision, filename, field)
|
||||
)
|
||||
if (
|
||||
"y_label" not in properties_update
|
||||
and "y_label" not in self.plot_properties
|
||||
):
|
||||
properties_update["y_label"] = "y"
|
||||
|
||||
properties_update["y"] = "dvc_inferred_y_value"
|
||||
def _update_all(datapoints: List[Dict], update_dict: Dict):
|
||||
|
||||
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(
|
||||
_add_version_info(
|
||||
datapoint, _version_info(revision, filename, y_values)
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(y_values, list):
|
||||
for datapoint in datapoints:
|
||||
for y_val in y_values:
|
||||
if y_val in datapoint:
|
||||
result.append(_generate_y(datapoint, y_val))
|
||||
|
||||
if isinstance(y_values, dict):
|
||||
|
||||
def _to_set(values: Iterable):
|
||||
result = set()
|
||||
for val in values:
|
||||
if isinstance(val, list):
|
||||
for elem in val:
|
||||
result.add(elem)
|
||||
else:
|
||||
result.add(val)
|
||||
|
||||
return result
|
||||
|
||||
all_fields = _to_set(y_values.values())
|
||||
if (
|
||||
all([isinstance(field, str) for field in all_fields])
|
||||
and len(all_fields) == 1
|
||||
):
|
||||
# if we use the same field from all files,
|
||||
# we dont have to generate it
|
||||
for datapoint in datapoints:
|
||||
result.append(
|
||||
_add_version_info(
|
||||
datapoint, _version_info(revision, filename)
|
||||
)
|
||||
)
|
||||
properties_update.update({"y": all_fields.pop()})
|
||||
else:
|
||||
for def_filename, val in y_values.items():
|
||||
if isinstance(val, str):
|
||||
fields = [val]
|
||||
if isinstance(val, list):
|
||||
fields = val
|
||||
for datapoint in datapoints:
|
||||
for field in fields:
|
||||
if field in datapoint and def_filename in filename:
|
||||
result.append(_generate_y(datapoint, field))
|
||||
|
||||
self.inferred_properties = {
|
||||
**self.inferred_properties,
|
||||
**properties_update,
|
||||
}
|
||||
|
||||
return result
|
||||
for datapoint in datapoints:
|
||||
datapoint.update(update_dict)
|
||||
|
||||
+9
-10
@@ -42,7 +42,7 @@ class PlotsData:
|
||||
return dict(groups)
|
||||
|
||||
def get_definition_data(self, target_files, rev):
|
||||
result = []
|
||||
result = {}
|
||||
for file in target_files:
|
||||
file_content = (
|
||||
self.data.get(rev, {})
|
||||
@@ -52,7 +52,7 @@ class PlotsData:
|
||||
.get("data", {})
|
||||
)
|
||||
if file_content:
|
||||
result.append((file, file_content))
|
||||
result[file] = file_content
|
||||
|
||||
return result
|
||||
|
||||
@@ -91,15 +91,14 @@ def match_defs_renderers(
|
||||
renderer_cls = VegaRenderer
|
||||
renderer_id = plot_id
|
||||
|
||||
converter = _get_converter(renderer_cls, props)
|
||||
converter = _get_converter(
|
||||
renderer_cls, inner_id, props, definitions_data
|
||||
)
|
||||
|
||||
for filename, plot_data in definitions_data:
|
||||
dps, final_props = converter.convert(
|
||||
revision=rev,
|
||||
filename=filename,
|
||||
data=plot_data,
|
||||
)
|
||||
plot_datapoints.extend(dps)
|
||||
dps, rev_props = converter.flat_datapoints(rev)
|
||||
if not final_props and rev_props:
|
||||
final_props = rev_props
|
||||
plot_datapoints.extend(dps)
|
||||
|
||||
if "title" not in final_props:
|
||||
final_props["title"] = renderer_id
|
||||
|
||||
+18
-48
@@ -3,7 +3,7 @@ import os
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
@@ -18,7 +18,6 @@ if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
from dvc.repo.scm_context import SCMContext
|
||||
from dvc.scm import Base
|
||||
from dvc.stage import Stage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -68,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]
|
||||
@@ -80,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)
|
||||
|
||||
@@ -187,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)
|
||||
@@ -268,7 +269,7 @@ class Repo:
|
||||
except SCMError:
|
||||
if self._uninitialized:
|
||||
# might not be a git/dvc repo at all
|
||||
# used in `params/metrics/plots/live` targets
|
||||
# used in `params/metrics/plots` targets
|
||||
return SCM(self.root_dir, no_scm=True)
|
||||
raise
|
||||
|
||||
@@ -400,6 +401,7 @@ class Repo:
|
||||
used_run_cache=None,
|
||||
revs=None,
|
||||
num=1,
|
||||
push: bool = False,
|
||||
):
|
||||
"""Get the stages related to the given target and collect
|
||||
the `info` of its outputs.
|
||||
@@ -434,6 +436,7 @@ class Repo:
|
||||
jobs=jobs,
|
||||
recursive=recursive,
|
||||
with_deps=with_deps,
|
||||
push=push,
|
||||
).items():
|
||||
used[odb].update(objs)
|
||||
|
||||
@@ -445,44 +448,6 @@ class Repo:
|
||||
|
||||
return used
|
||||
|
||||
def partial_imports(
|
||||
self,
|
||||
targets=None,
|
||||
all_branches=False,
|
||||
all_tags=False,
|
||||
all_commits=False,
|
||||
all_experiments=False,
|
||||
commit_date: Optional[str] = None,
|
||||
recursive=False,
|
||||
revs=None,
|
||||
num=1,
|
||||
) -> List["Stage"]:
|
||||
"""Get the stages related to the given target and collect dependencies
|
||||
which are missing outputs.
|
||||
|
||||
This is useful to retrieve files which have been imported to the repo
|
||||
using --no-download.
|
||||
|
||||
Returns:
|
||||
A list of partially imported stages
|
||||
"""
|
||||
from itertools import chain
|
||||
|
||||
partial_imports = chain.from_iterable(
|
||||
self.index.partial_imports(targets, recursive=recursive)
|
||||
for _ in self.brancher(
|
||||
revs=revs,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
all_experiments=all_experiments,
|
||||
commit_date=commit_date,
|
||||
num=num,
|
||||
)
|
||||
)
|
||||
|
||||
return list(partial_imports)
|
||||
|
||||
@property
|
||||
def stages(self): # obsolete, only for backward-compatibility
|
||||
return self.index.stages
|
||||
@@ -526,9 +491,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
|
||||
)
|
||||
|
||||
@@ -540,13 +505,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:
|
||||
@@ -571,11 +536,16 @@ class Repo:
|
||||
self.scm.close()
|
||||
self.state.close()
|
||||
|
||||
def _reset_cached_indecies(self):
|
||||
self.__dict__.pop("index", None)
|
||||
self.__dict__.pop("dvcignore", None)
|
||||
self.__dict__.pop("dvcfs", None)
|
||||
self.__dict__.pop("datafs", None)
|
||||
|
||||
def _reset(self):
|
||||
self.state.close()
|
||||
self.scm._reset() # pylint: disable=protected-access
|
||||
self.__dict__.pop("index", None)
|
||||
self.__dict__.pop("dvcignore", None)
|
||||
self._reset_cached_indecies()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@ def add( # noqa: C901
|
||||
stage.transfer(source, to_remote=to_remote, odb=odb, **kwargs)
|
||||
else:
|
||||
try:
|
||||
stage.save()
|
||||
stage.save(merge_versioned=True)
|
||||
if not no_commit:
|
||||
stage.commit()
|
||||
except CacheLinkError:
|
||||
|
||||
@@ -77,6 +77,7 @@ def brancher( # noqa: E302
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
for sha, names in found_revs.items():
|
||||
self._reset_cached_indecies() # pylint: disable=W0212
|
||||
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 = []
|
||||
|
||||
+6
-3
@@ -56,10 +56,13 @@ def commit(
|
||||
]
|
||||
for stage_info in stages_info:
|
||||
stage = stage_info.stage
|
||||
changes = stage.changed_entries()
|
||||
if any(changes):
|
||||
prompt_to_commit(stage, changes, force=force)
|
||||
if force:
|
||||
stage.save(allow_missing=allow_missing)
|
||||
else:
|
||||
changes = stage.changed_entries()
|
||||
if any(changes):
|
||||
prompt_to_commit(stage, changes, force=force)
|
||||
stage.save(allow_missing=allow_missing)
|
||||
stage.commit(
|
||||
filter_info=stage_info.filter_info, allow_missing=allow_missing
|
||||
)
|
||||
|
||||
+5
-3
@@ -12,6 +12,7 @@ from typing import (
|
||||
Optional,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
@@ -19,10 +20,9 @@ from dvc.fs.git import GitFileSystem
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.base import Base
|
||||
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git, NoSCM
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
@@ -140,7 +140,9 @@ class GitInfo(TypedDict, total=False):
|
||||
is_dirty: bool
|
||||
|
||||
|
||||
def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
def _git_info(
|
||||
scm: Union["Git", "NoSCM"], untracked_files: str = "all"
|
||||
) -> GitInfo:
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.scm import NoSCM
|
||||
|
||||
+2
-2
@@ -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]
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ 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
|
||||
from funcy import cached_property, chain, first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
@@ -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,
|
||||
@@ -33,7 +32,7 @@ from .refs import (
|
||||
ExpRefInfo,
|
||||
)
|
||||
from .stash import ExpStashEntry
|
||||
from .utils import exp_refs_by_rev, scm_locked, unlocked_repo
|
||||
from .utils import check_ref_format, exp_refs_by_rev, unlocked_repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,18 +50,12 @@ class Experiments:
|
||||
)
|
||||
|
||||
def __init__(self, repo):
|
||||
from dvc.lock import make_lock
|
||||
from dvc.scm import NoSCMError
|
||||
|
||||
if repo.config["core"].get("no_scm", False):
|
||||
raise NoSCMError
|
||||
|
||||
self.repo = repo
|
||||
self.scm_lock = make_lock(
|
||||
os.path.join(self.repo.tmp_dir, "exp_scm_lock"),
|
||||
tmp_dir=self.repo.tmp_dir,
|
||||
hardlink_lock=repo.config["core"].get("hardlink_lock", False),
|
||||
)
|
||||
|
||||
@property
|
||||
def scm(self):
|
||||
@@ -172,12 +165,22 @@ class Experiments:
|
||||
) -> Dict[str, str]:
|
||||
results: Dict[str, str] = {}
|
||||
if entries is None:
|
||||
entries = list(self.celery_queue.iter_queued())
|
||||
entries = list(
|
||||
chain(
|
||||
self.celery_queue.iter_active(),
|
||||
self.celery_queue.iter_queued(),
|
||||
)
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"reproduce all these entries '%s'",
|
||||
entries,
|
||||
)
|
||||
|
||||
if not entries:
|
||||
return results
|
||||
|
||||
# TODO: re-enable --jobs concurrency
|
||||
self.celery_queue.spawn_worker()
|
||||
self.celery_queue.start_workers(count=kwargs.get("jobs", 1))
|
||||
failed = []
|
||||
try:
|
||||
ui.write(
|
||||
@@ -190,7 +193,10 @@ class Experiments:
|
||||
time.sleep(1)
|
||||
self.celery_queue.follow(entry)
|
||||
# wait for task collection to complete
|
||||
result = self.celery_queue.get_result(entry)
|
||||
try:
|
||||
result = self.celery_queue.get_result(entry)
|
||||
except FileNotFoundError:
|
||||
result = None
|
||||
if result is None or result.exp_hash is None:
|
||||
name = entry.name or entry.stash_rev[:7]
|
||||
failed.append(name)
|
||||
@@ -211,8 +217,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:
|
||||
@@ -228,18 +235,6 @@ class Experiments:
|
||||
"\tdvc exp branch <exp> <branch>\n"
|
||||
)
|
||||
|
||||
def _validate_new_ref(self, exp_ref: ExpRefInfo):
|
||||
from .utils import check_ref_format
|
||||
|
||||
if not exp_ref.name:
|
||||
return
|
||||
|
||||
check_ref_format(self.scm, exp_ref)
|
||||
|
||||
if self.scm.get_ref(str(exp_ref)):
|
||||
raise ExperimentExistsError(exp_ref.name)
|
||||
|
||||
@scm_locked
|
||||
def new(
|
||||
self,
|
||||
queue: BaseStashQueue,
|
||||
@@ -258,13 +253,13 @@ class Experiments:
|
||||
|
||||
name = kwargs.get("name", None)
|
||||
baseline_sha = kwargs.get("baseline_rev") or self.repo.scm.get_rev()
|
||||
exp_ref = ExpRefInfo(baseline_sha=baseline_sha, name=name)
|
||||
|
||||
try:
|
||||
self._validate_new_ref(exp_ref)
|
||||
except ExperimentExistsError as err:
|
||||
if not (kwargs.get("force", False) or kwargs.get("reset", False)):
|
||||
raise err
|
||||
if name:
|
||||
exp_ref = ExpRefInfo(baseline_sha=baseline_sha, name=name)
|
||||
check_ref_format(self.scm, exp_ref)
|
||||
force = kwargs.get("force", False)
|
||||
if self.scm.get_ref(str(exp_ref)) and not force:
|
||||
raise ExperimentExistsError(exp_ref.name)
|
||||
|
||||
return queue.put(*args, **kwargs)
|
||||
|
||||
@@ -411,86 +406,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):
|
||||
@@ -518,6 +468,11 @@ class Experiments:
|
||||
|
||||
return run(self.repo, *args, **kwargs)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
from dvc.repo.experiments.save import save
|
||||
|
||||
return save(self.repo, *args, **kwargs)
|
||||
|
||||
def gc(self, *args, **kwargs):
|
||||
from dvc.repo.experiments.gc import gc
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@ from .executor.base import BaseExecutor
|
||||
from .refs import EXEC_APPLY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo import Git
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments import Experiments
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,20 +32,33 @@ def apply(repo: "Repo", rev: str, force: bool = True, **kwargs):
|
||||
from dvc.repo.checkout import checkout as dvc_checkout
|
||||
from dvc.scm import GitMergeError, RevError, resolve_rev
|
||||
|
||||
exps = repo.experiments
|
||||
exps: "Experiments" = repo.experiments
|
||||
|
||||
is_stash: bool = False
|
||||
|
||||
try:
|
||||
exp_rev = resolve_rev(repo.scm, rev)
|
||||
exps.check_baseline(exp_rev)
|
||||
except (RevError, BaselineMismatchError) as exc:
|
||||
except RevError as exc:
|
||||
(
|
||||
exp_ref_info,
|
||||
queue_entry,
|
||||
) = exps.celery_queue.get_ref_and_entry_by_names(rev)[rev]
|
||||
baseline_sha = repo.scm.get_rev()
|
||||
if exp_ref_info:
|
||||
if baseline_sha != exp_ref_info.baseline_sha:
|
||||
raise InvalidExpRevError(rev)
|
||||
exp_rev = repo.scm.get_ref(str(exp_ref_info))
|
||||
elif queue_entry:
|
||||
if queue_entry.baseline_rev != baseline_sha:
|
||||
raise InvalidExpRevError(rev)
|
||||
exp_rev = queue_entry.stash_rev
|
||||
is_stash = True
|
||||
else:
|
||||
raise InvalidExpRevError(rev) from exc
|
||||
except BaselineMismatchError as exc:
|
||||
raise InvalidExpRevError(rev) from exc
|
||||
|
||||
stash_rev = exp_rev in exps.stash_revs
|
||||
if not stash_rev and not exps.get_branch_by_rev(
|
||||
exp_rev, allow_multiple=True
|
||||
):
|
||||
raise InvalidExpRevError(exp_rev)
|
||||
|
||||
# NOTE: we don't use scmrepo's stash_workspace() here since we need
|
||||
# finer control over the merge behavior when we unstash everything
|
||||
with _apply_workspace(repo, rev, force):
|
||||
@@ -55,7 +69,7 @@ def apply(repo: "Repo", rev: str, force: bool = True, **kwargs):
|
||||
|
||||
repo.scm.reset()
|
||||
|
||||
if stash_rev:
|
||||
if is_stash:
|
||||
args_path = os.path.join(repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
|
||||
if os.path.exists(args_path):
|
||||
remove(args_path)
|
||||
|
||||
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
from dvc.repo.experiments.show import _collect_experiment_commit
|
||||
from dvc.repo.experiments.show import collect_experiment_commit
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
if repo.scm.no_commits:
|
||||
@@ -15,15 +15,15 @@ def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
|
||||
if a_rev:
|
||||
rev = resolve_rev(repo.scm, a_rev)
|
||||
old = _collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
old = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
old = _collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
|
||||
old = collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
|
||||
|
||||
if b_rev:
|
||||
rev = resolve_rev(repo.scm, b_rev)
|
||||
new = _collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
new = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
new = _collect_experiment_commit(
|
||||
new = collect_experiment_commit(
|
||||
repo, "workspace", param_deps=param_deps
|
||||
)
|
||||
|
||||
|
||||
@@ -6,12 +6,6 @@ if TYPE_CHECKING:
|
||||
from .refs import ExpRefInfo
|
||||
|
||||
|
||||
class UnchangedExperimentError(DvcException):
|
||||
def __init__(self, rev):
|
||||
super().__init__(f"Experiment unchanged from '{rev[:7]}'.")
|
||||
self.rev = rev
|
||||
|
||||
|
||||
class BaselineMismatchError(DvcException):
|
||||
def __init__(self, rev, expected):
|
||||
if hasattr(rev, "hexsha"):
|
||||
@@ -25,13 +19,13 @@ class BaselineMismatchError(DvcException):
|
||||
|
||||
|
||||
class ExperimentExistsError(DvcException):
|
||||
def __init__(self, name: str):
|
||||
def __init__(self, name: str, command: str = "run"):
|
||||
msg = (
|
||||
"Experiment conflicts with existing experiment "
|
||||
f"'{name}'. To overwrite the existing experiment run:\n\n"
|
||||
"\tdvc exp run -f ...\n\n"
|
||||
"To run this experiment with a different name run:\n\n"
|
||||
f"\tdvc exp run -n <new_name> ...\n"
|
||||
f"\tdvc exp {command} -f ...\n\n"
|
||||
f"To {command} this experiment with a different name run:\n\n"
|
||||
f"\tdvc exp {command} -n <new_name> ...\n"
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.name = name
|
||||
|
||||
@@ -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,
|
||||
@@ -11,6 +12,7 @@ from typing import (
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Tuple,
|
||||
@@ -19,6 +21,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
|
||||
@@ -26,20 +30,8 @@ from dvc.ui import ui
|
||||
from dvc.utils import dict_sha256, env2bool, relpath
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
from ..exceptions import (
|
||||
CheckpointExistsError,
|
||||
ExperimentExistsError,
|
||||
UnchangedExperimentError,
|
||||
)
|
||||
from ..refs import (
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_NAMESPACE,
|
||||
EXPS_STASH,
|
||||
ExpRefInfo,
|
||||
)
|
||||
from ..exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
from ..refs import EXEC_BASELINE, EXEC_BRANCH, EXEC_CHECKPOINT, ExpRefInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from queue import Queue
|
||||
@@ -47,23 +39,28 @@ 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__)
|
||||
|
||||
|
||||
EXEC_TMP_DIR = "exps"
|
||||
EXEC_PID_DIR = "run"
|
||||
|
||||
|
||||
class ExecutorResult(NamedTuple):
|
||||
exp_hash: Optional[str]
|
||||
ref_info: Optional["ExpRefInfo"]
|
||||
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 +70,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 +136,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 +150,18 @@ 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,
|
||||
repo: "Repo",
|
||||
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 +201,7 @@ class BaseExecutor(ABC):
|
||||
dvc_dir=self.dvc_dir,
|
||||
name=self.name,
|
||||
wdir=self.wdir,
|
||||
status=self.status,
|
||||
**result_dict,
|
||||
)
|
||||
|
||||
@@ -212,6 +223,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 +234,6 @@ class BaseExecutor(ABC):
|
||||
def from_stash_entry(
|
||||
cls: Type[_T],
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
**kwargs,
|
||||
) -> _T:
|
||||
@@ -232,7 +243,6 @@ class BaseExecutor(ABC):
|
||||
def _from_stash_entry(
|
||||
cls: Type[_T],
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
root_dir: str,
|
||||
**kwargs,
|
||||
@@ -241,12 +251,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 +268,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
|
||||
@@ -290,6 +303,7 @@ class BaseExecutor(ABC):
|
||||
def fetch_exps(
|
||||
self,
|
||||
dest_scm: "Git",
|
||||
refs: List[str],
|
||||
force: bool = False,
|
||||
on_diverged: Callable[[str, bool], None] = None,
|
||||
**kwargs,
|
||||
@@ -298,56 +312,54 @@ class BaseExecutor(ABC):
|
||||
|
||||
Args:
|
||||
dest_scm: Destination Git instance.
|
||||
refs: reference names to be fetched from the remotes.
|
||||
force: If True, diverged refs will be overwritten
|
||||
on_diverged: Callback in the form on_diverged(ref, is_checkpoint)
|
||||
to be called when an experiment ref has diverged.
|
||||
|
||||
Extra kwargs will be passed into the remote git client.
|
||||
"""
|
||||
from ..utils import iter_remote_refs
|
||||
|
||||
refs = []
|
||||
has_checkpoint = False
|
||||
for ref in iter_remote_refs(
|
||||
dest_scm,
|
||||
self.git_url,
|
||||
base=EXPS_NAMESPACE,
|
||||
**kwargs,
|
||||
):
|
||||
if ref == EXEC_CHECKPOINT:
|
||||
has_checkpoint = True
|
||||
elif not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH:
|
||||
refs.append(ref)
|
||||
if EXEC_CHECKPOINT in refs:
|
||||
refs.remove(EXEC_CHECKPOINT)
|
||||
has_checkpoint = True
|
||||
else:
|
||||
has_checkpoint = False
|
||||
|
||||
def on_diverged_ref(orig_ref: str, new_rev: str):
|
||||
if force:
|
||||
logger.debug("Replacing existing experiment '%s'", orig_ref)
|
||||
return True
|
||||
|
||||
if on_diverged:
|
||||
return on_diverged(orig_ref, has_checkpoint)
|
||||
|
||||
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 +423,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:
|
||||
@@ -482,7 +492,6 @@ class BaseExecutor(ABC):
|
||||
info,
|
||||
stages,
|
||||
exp_hash,
|
||||
checkpoint_reset,
|
||||
auto_push,
|
||||
git_remote,
|
||||
repro_force,
|
||||
@@ -491,9 +500,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
|
||||
@@ -506,29 +512,20 @@ class BaseExecutor(ABC):
|
||||
info,
|
||||
stages,
|
||||
exp_hash,
|
||||
checkpoint_reset,
|
||||
auto_push,
|
||||
git_remote,
|
||||
repro_force,
|
||||
) -> Tuple[Optional[str], Optional["ExpRefInfo"], bool]:
|
||||
try:
|
||||
is_checkpoint = any(stage.is_checkpoint for stage in stages)
|
||||
if is_checkpoint and checkpoint_reset:
|
||||
# For reset checkpoint stages, we need to force
|
||||
# overwriting existing checkpoint refs even though
|
||||
# repro may not have actually been run with --force
|
||||
repro_force = True
|
||||
cls.commit(
|
||||
dvc.scm,
|
||||
exp_hash,
|
||||
exp_name=info.name,
|
||||
force=repro_force,
|
||||
checkpoint=is_checkpoint,
|
||||
)
|
||||
if auto_push:
|
||||
cls._auto_push(dvc, dvc.scm, git_remote)
|
||||
except UnchangedExperimentError:
|
||||
pass
|
||||
is_checkpoint = any(stage.is_checkpoint for stage in stages)
|
||||
cls.commit(
|
||||
dvc.scm,
|
||||
exp_hash,
|
||||
exp_name=info.name,
|
||||
force=repro_force,
|
||||
checkpoint=is_checkpoint,
|
||||
)
|
||||
if auto_push:
|
||||
cls._auto_push(dvc, dvc.scm, git_remote)
|
||||
ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
|
||||
exp_ref: Optional["ExpRefInfo"] = (
|
||||
ExpRefInfo.from_ref(ref) if ref else None
|
||||
@@ -550,6 +547,7 @@ class BaseExecutor(ABC):
|
||||
def _repro_dvc(
|
||||
cls,
|
||||
info: "ExecutorInfo",
|
||||
infofile: str = None,
|
||||
log_errors: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -557,6 +555,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,17 +569,23 @@ class BaseExecutor(ABC):
|
||||
try:
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
info.status = TaskStatus.SUCCESS
|
||||
except CheckpointKilledError:
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except DvcException:
|
||||
if log_errors:
|
||||
logger.exception("")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except Exception:
|
||||
if log_errors:
|
||||
logger.exception("unexpected error")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
finally:
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
dvc.close()
|
||||
os.chdir(old_cwd)
|
||||
|
||||
@@ -629,18 +636,15 @@ class BaseExecutor(ABC):
|
||||
unchanged: Iterable["PipelineStage"],
|
||||
stages: Iterable["PipelineStage"],
|
||||
):
|
||||
try:
|
||||
exp_hash = cls.hash_exp(list(stages) + list(unchanged))
|
||||
exp_rev = cls.commit(
|
||||
scm, exp_hash, exp_name=name, force=force, checkpoint=True
|
||||
)
|
||||
exp_hash = cls.hash_exp(list(stages) + list(unchanged))
|
||||
exp_rev = cls.commit(
|
||||
scm, exp_hash, exp_name=name, force=force, checkpoint=True
|
||||
)
|
||||
|
||||
if env2bool(DVC_EXP_AUTO_PUSH):
|
||||
git_remote = os.getenv(DVC_EXP_GIT_REMOTE)
|
||||
cls._auto_push(dvc, scm, git_remote)
|
||||
ui.write(f"Checkpoint experiment iteration '{exp_rev[:7]}'.")
|
||||
except UnchangedExperimentError:
|
||||
pass
|
||||
if env2bool(DVC_EXP_AUTO_PUSH):
|
||||
git_remote = os.getenv(DVC_EXP_GIT_REMOTE)
|
||||
cls._auto_push(dvc, scm, git_remote)
|
||||
ui.write(f"Checkpoint experiment iteration '{exp_rev[:7]}'.")
|
||||
|
||||
@classmethod
|
||||
def commit(
|
||||
@@ -655,7 +659,6 @@ class BaseExecutor(ABC):
|
||||
rev = scm.get_rev()
|
||||
if not scm.is_dirty(untracked_files=False):
|
||||
logger.debug("No changes to commit")
|
||||
raise UnchangedExperimentError(rev)
|
||||
|
||||
check_conflict = False
|
||||
branch = scm.get_ref(EXEC_BRANCH, follow=False)
|
||||
@@ -714,3 +717,14 @@ class BaseExecutor(ABC):
|
||||
disable_other_loggers()
|
||||
if level is not None:
|
||||
dvc_logger.setLevel(level)
|
||||
|
||||
@contextmanager
|
||||
def set_temp_refs(self, scm: "Git", temp_dict: Dict[str, str]):
|
||||
try:
|
||||
for ref, rev in temp_dict.items():
|
||||
scm.set_ref(ref, rev)
|
||||
yield
|
||||
finally:
|
||||
for ref in temp_dict:
|
||||
if scm.get_ref(ref):
|
||||
scm.remove_ref(ref)
|
||||
|
||||
@@ -4,9 +4,11 @@ from contextlib import ExitStack
|
||||
from tempfile import mkdtemp
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from funcy import cached_property
|
||||
from funcy import cached_property, retry
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
from shortuuid import uuid
|
||||
|
||||
from dvc.lock import LockError
|
||||
from dvc.scm import SCM, GitMergeError
|
||||
from dvc.utils.fs import makedirs, remove
|
||||
|
||||
@@ -18,15 +20,17 @@ from ..refs import (
|
||||
EXEC_HEAD,
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_TEMP,
|
||||
ExpRefInfo,
|
||||
)
|
||||
from .base import EXEC_TMP_DIR, BaseExecutor
|
||||
from ..utils import EXEC_TMP_DIR, get_exp_rwlock
|
||||
from .base import BaseExecutor, TaskStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
from ..refs import ExpRefInfo
|
||||
from ..stash import ExpStashEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -46,10 +50,10 @@ class BaseLocalExecutor(BaseExecutor):
|
||||
def scm(self):
|
||||
return SCM(self.root_dir)
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
def cleanup(self, infofile: str):
|
||||
self.scm.close()
|
||||
del self.scm
|
||||
super().cleanup(infofile)
|
||||
|
||||
def collect_cache(
|
||||
self, repo: "Repo", exp_ref: "ExpRefInfo", run_cache: bool = True
|
||||
@@ -67,23 +71,62 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
QUIET = True
|
||||
DEFAULT_LOCATION = "tempdir"
|
||||
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
@retry(180, errors=LockError, timeout=1)
|
||||
def init_git(
|
||||
self,
|
||||
repo: "Repo",
|
||||
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)
|
||||
if branch:
|
||||
push_refspec(scm, self.git_url, branch, branch)
|
||||
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
|
||||
elif self.scm.get_ref(EXEC_BRANCH):
|
||||
self.scm.remove_ref(EXEC_BRANCH)
|
||||
self.status = TaskStatus.PREPARING
|
||||
if infofile:
|
||||
self.info.dump_json(infofile)
|
||||
|
||||
if self.scm.get_ref(EXEC_CHECKPOINT):
|
||||
self.scm.remove_ref(EXEC_CHECKPOINT)
|
||||
temp_head = f"{EXPS_TEMP}/head-{uuid()}"
|
||||
temp_merge = f"{EXPS_TEMP}/merge-{uuid()}"
|
||||
temp_baseline = f"{EXPS_TEMP}/baseline-{uuid()}"
|
||||
|
||||
temp_ref_dict = {
|
||||
temp_head: entry.head_rev,
|
||||
temp_merge: stash_rev,
|
||||
temp_baseline: entry.baseline_rev,
|
||||
}
|
||||
with get_exp_rwlock(
|
||||
repo, writes=[temp_head, temp_merge, temp_baseline]
|
||||
), self.set_temp_refs(scm, temp_ref_dict):
|
||||
# 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
|
||||
refspec = [
|
||||
(temp_head, EXEC_HEAD),
|
||||
(temp_merge, EXEC_MERGE),
|
||||
(temp_baseline, EXEC_BASELINE),
|
||||
]
|
||||
|
||||
if branch:
|
||||
refspec.append((branch, branch))
|
||||
with get_exp_rwlock(repo, reads=[branch]):
|
||||
push_refspec(scm, self.git_url, refspec)
|
||||
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
|
||||
else:
|
||||
push_refspec(scm, self.git_url, refspec)
|
||||
if self.scm.get_ref(EXEC_BRANCH):
|
||||
self.scm.remove_ref(EXEC_BRANCH)
|
||||
|
||||
if self.scm.get_ref(EXEC_CHECKPOINT):
|
||||
self.scm.remove_ref(EXEC_CHECKPOINT)
|
||||
|
||||
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
|
||||
# committing
|
||||
@@ -91,10 +134,6 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
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 +153,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 +162,6 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
def from_stash_entry(
|
||||
cls,
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
wdir: Optional[str] = None,
|
||||
**kwargs,
|
||||
@@ -132,9 +170,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,39 +188,57 @@ 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):
|
||||
self._detach_stack.enter_context(
|
||||
self.scm.detach_head(
|
||||
self.scm.get_ref(EXEC_HEAD),
|
||||
force=True,
|
||||
client="dvc",
|
||||
@retry(180, errors=LockError, timeout=1)
|
||||
def init_git(
|
||||
self,
|
||||
repo: "Repo",
|
||||
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)
|
||||
|
||||
with get_exp_rwlock(repo, writes=[EXEC_NAMESPACE]):
|
||||
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),
|
||||
force=True,
|
||||
client="dvc",
|
||||
)
|
||||
)
|
||||
)
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
try:
|
||||
self.scm.merge(merge_rev, squash=True, commit=False)
|
||||
except _SCMError as exc:
|
||||
raise GitMergeError(str(exc), scm=self.scm)
|
||||
if branch:
|
||||
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
|
||||
elif scm.get_ref(EXEC_BRANCH):
|
||||
self.scm.remove_ref(EXEC_BRANCH)
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
try:
|
||||
self.scm.merge(merge_rev, squash=True, commit=False)
|
||||
except _SCMError as exc:
|
||||
raise GitMergeError(str(exc), scm=self.scm)
|
||||
if branch:
|
||||
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
|
||||
elif scm.get_ref(EXEC_BRANCH):
|
||||
self.scm.remove_ref(EXEC_BRANCH)
|
||||
|
||||
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 +247,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): # noqa: B024
|
||||
"""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()
|
||||
@@ -9,13 +9,14 @@ from dvc_ssh import SSHFileSystem
|
||||
from funcy import first
|
||||
|
||||
from ..refs import (
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_HEAD,
|
||||
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 +24,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 +76,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 +118,21 @@ class SSHExecutor(BaseExecutor):
|
||||
}
|
||||
return kwargs
|
||||
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
def init_git(
|
||||
self,
|
||||
repo: "Repo",
|
||||
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,10 +148,19 @@ 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)
|
||||
|
||||
ref_dict = {
|
||||
EXEC_HEAD: entry.head_rev,
|
||||
EXEC_MERGE: stash_rev,
|
||||
EXEC_BASELINE: entry.baseline_rev,
|
||||
}
|
||||
with self.set_temp_refs(scm, ref_dict):
|
||||
exec_namespace = f"{EXEC_NAMESPACE}/"
|
||||
refspec = [(exec_namespace, exec_namespace)]
|
||||
push_refspec(scm, self.git_url, refspec, **kwargs)
|
||||
|
||||
if branch:
|
||||
push_refspec(scm, self.git_url, branch, branch, **kwargs)
|
||||
push_refspec(scm, self.git_url, [(branch, branch)], **kwargs)
|
||||
self._ssh_cmd(fs, f"git symbolic-ref {EXEC_BRANCH} {branch}")
|
||||
else:
|
||||
self._ssh_cmd(
|
||||
|
||||
@@ -248,11 +248,6 @@ def init(
|
||||
except MissingParamsFile:
|
||||
pass
|
||||
|
||||
models = context.get("models")
|
||||
live_path = context.pop("live", None)
|
||||
live_metrics = f"{live_path}.json" if live_path else None
|
||||
live_plots = os.path.join(live_path, "scalars") if live_path else None
|
||||
|
||||
if type == "checkpoint":
|
||||
outs_key = "checkpoints"
|
||||
metrics_key = "metrics_persist_no_cache"
|
||||
@@ -262,6 +257,13 @@ def init(
|
||||
metrics_key = "metrics_no_cache"
|
||||
plots_key = "plots_no_cache"
|
||||
|
||||
models = [context.get("models")]
|
||||
metrics = [context.get("metrics")]
|
||||
plots = [context.get("plots")]
|
||||
if live_path := context.pop("live", None):
|
||||
metrics.append(os.path.join(live_path, "metrics.json"))
|
||||
plots.append(os.path.join(live_path, "plots"))
|
||||
|
||||
stage = repo.stage.create(
|
||||
name=name,
|
||||
cmd=context["cmd"],
|
||||
@@ -269,9 +271,9 @@ def init(
|
||||
params=[{params: None}] if params else None,
|
||||
force=force,
|
||||
**{
|
||||
outs_key: compact([models]),
|
||||
metrics_key: compact([context.get("metrics"), live_metrics]),
|
||||
plots_key: compact([context.get("plots"), live_plots]),
|
||||
outs_key: compact(models),
|
||||
metrics_key: compact(metrics),
|
||||
plots_key: compact(plots),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
+17
-14
@@ -6,7 +6,7 @@ from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import iter_revs
|
||||
from dvc.types import Optional
|
||||
|
||||
from .utils import exp_refs, exp_refs_by_baseline
|
||||
from .utils import exp_refs_by_baseline
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,19 +20,22 @@ def ls(
|
||||
num: int = 1,
|
||||
git_remote: Optional[str] = None,
|
||||
):
|
||||
results = defaultdict(list)
|
||||
if all_commits:
|
||||
gen = exp_refs(repo.scm, git_remote)
|
||||
for info in gen:
|
||||
results[info.baseline_sha].append(info.name)
|
||||
return results
|
||||
|
||||
rev = rev or "HEAD"
|
||||
|
||||
revs = iter_revs(repo.scm, [rev], num)
|
||||
rev_set = set(revs.keys())
|
||||
rev_set = None
|
||||
if not all_commits:
|
||||
revs = iter_revs(repo.scm, [rev or "HEAD"], num)
|
||||
rev_set = set(revs.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
|
||||
for rev, ref_info_list in ref_info_dict.items():
|
||||
results[rev] = [ref_info.name for ref_info in ref_info_list]
|
||||
|
||||
tags = repo.scm.describe(ref_info_dict.keys())
|
||||
remained = {baseline for baseline, tag in tags.items() if tag is None}
|
||||
base = "refs/heads"
|
||||
ref_heads = repo.scm.describe(remained, base=base)
|
||||
|
||||
results = defaultdict(list)
|
||||
for baseline in ref_info_dict:
|
||||
name = baseline[:7]
|
||||
if tags[baseline] or ref_heads[baseline]:
|
||||
name = tags[baseline] or ref_heads[baseline][len(base) + 1 :]
|
||||
results[name] = [info.name for info in ref_info_dict[baseline]]
|
||||
|
||||
return results
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import (
|
||||
@@ -18,24 +17,27 @@ 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.env import DVC_EXP_BASELINE_REV, DVC_EXP_NAME, DVCLIVE_RESUME
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.lock import LockError
|
||||
from dvc.ui import ui
|
||||
|
||||
from ..exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
from ..executor.base import (
|
||||
from ..executor.base import BaseExecutor, ExecutorResult
|
||||
from ..executor.local import WorkspaceExecutor
|
||||
from ..refs import ExpRefInfo
|
||||
from ..stash import ExpStash, ExpStashEntry
|
||||
from ..utils import (
|
||||
EXEC_PID_DIR,
|
||||
EXEC_TMP_DIR,
|
||||
BaseExecutor,
|
||||
ExecutorResult,
|
||||
exp_refs_by_rev,
|
||||
get_exp_rwlock,
|
||||
get_random_exp_name,
|
||||
)
|
||||
from ..executor.local import WorkspaceExecutor
|
||||
from ..refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
|
||||
from ..stash import ExpStash, ExpStashEntry
|
||||
from ..utils import exp_refs_by_rev, scm_locked
|
||||
from .utils import get_remote_executor_refs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
@@ -84,6 +86,11 @@ class QueueDoneResult(NamedTuple):
|
||||
result: Optional[ExecutorResult]
|
||||
|
||||
|
||||
class ExpRefAndQueueEntry(NamedTuple):
|
||||
exp_ref_info: Optional["ExpRefInfo"]
|
||||
queue_entry: Optional["QueueEntry"]
|
||||
|
||||
|
||||
class BaseStashQueue(ABC):
|
||||
"""Naive Git-stash based experiment queue.
|
||||
|
||||
@@ -268,6 +275,7 @@ class BaseStashQueue(ABC):
|
||||
finish any active experiments before shutting down.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def logs(
|
||||
self,
|
||||
rev: str,
|
||||
@@ -284,7 +292,7 @@ class BaseStashQueue(ABC):
|
||||
output.
|
||||
"""
|
||||
|
||||
def _stash_exp(
|
||||
def _stash_exp( # noqa: C901
|
||||
self,
|
||||
*args,
|
||||
params: Optional[Dict[str, List[str]]] = None,
|
||||
@@ -399,7 +407,14 @@ class BaseStashQueue(ABC):
|
||||
)
|
||||
|
||||
# save additional repro command line arguments
|
||||
run_env = {DVCLIVE_RESUME: "1"} if resume_rev else {}
|
||||
run_env = {
|
||||
DVC_EXP_BASELINE_REV: baseline_rev,
|
||||
}
|
||||
if not name:
|
||||
name = get_random_exp_name(self.scm, baseline_rev)
|
||||
run_env[DVC_EXP_NAME] = name
|
||||
if resume_rev:
|
||||
run_env[DVCLIVE_RESUME] = "1"
|
||||
self._pack_args(*args, run_env=run_env, **kwargs)
|
||||
|
||||
# save experiment as a stash commit
|
||||
@@ -524,16 +539,9 @@ class BaseStashQueue(ABC):
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/advanced/override_grammar/basic/
|
||||
"""
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
from dvc.utils.hydra import apply_overrides, compose_and_dump
|
||||
|
||||
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
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
|
||||
hydra_config = self.repo.config.get("hydra", {})
|
||||
hydra_enabled = hydra_config.get("enabled", False)
|
||||
@@ -560,36 +568,50 @@ class BaseStashQueue(ABC):
|
||||
self.scm.add(list(params.keys()))
|
||||
|
||||
@staticmethod
|
||||
@scm_locked
|
||||
def setup_executor(
|
||||
@retry(180, errors=LockError, timeout=1)
|
||||
def get_stash_entry(
|
||||
exp: "Experiments",
|
||||
queue_entry: QueueEntry,
|
||||
) -> "ExpStashEntry":
|
||||
stash = ExpStash(exp.scm, queue_entry.stash_ref)
|
||||
stash_rev = queue_entry.stash_rev
|
||||
with get_exp_rwlock(exp.repo, writes=[queue_entry.stash_ref]):
|
||||
stash_entry = stash.stash_revs.get(
|
||||
stash_rev,
|
||||
ExpStashEntry(None, stash_rev, stash_rev, None, None),
|
||||
)
|
||||
if stash_entry.stash_index is not None:
|
||||
stash.drop(stash_entry.stash_index)
|
||||
return stash_entry
|
||||
|
||||
@classmethod
|
||||
def init_executor(
|
||||
cls,
|
||||
exp: "Experiments",
|
||||
queue_entry: QueueEntry,
|
||||
executor_cls: Type[BaseExecutor] = WorkspaceExecutor,
|
||||
**kwargs,
|
||||
) -> BaseExecutor:
|
||||
scm = exp.scm
|
||||
stash = ExpStash(scm, queue_entry.stash_ref)
|
||||
stash_entry = cls.get_stash_entry(exp, queue_entry)
|
||||
|
||||
executor = executor_cls.from_stash_entry(
|
||||
exp.repo, stash_entry, **kwargs
|
||||
)
|
||||
|
||||
stash_rev = queue_entry.stash_rev
|
||||
stash_entry = stash.stash_revs.get(
|
||||
infofile = exp.celery_queue.get_infofile_path(stash_rev)
|
||||
executor.init_git(
|
||||
exp.repo,
|
||||
exp.repo.scm,
|
||||
stash_rev,
|
||||
ExpStashEntry(None, stash_rev, stash_rev, None, None),
|
||||
stash_entry,
|
||||
infofile,
|
||||
branch=stash_entry.branch,
|
||||
)
|
||||
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.init_cache(exp.repo, stash_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
|
||||
)
|
||||
return executor
|
||||
|
||||
def get_infofile_path(self, name: str) -> str:
|
||||
return os.path.join(
|
||||
@@ -599,8 +621,8 @@ class BaseStashQueue(ABC):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@scm_locked
|
||||
def collect_executor(
|
||||
@retry(180, errors=LockError, timeout=1)
|
||||
def collect_git(
|
||||
exp: "Experiments",
|
||||
executor: BaseExecutor,
|
||||
exec_result: ExecutorResult,
|
||||
@@ -613,16 +635,31 @@ class BaseStashQueue(ABC):
|
||||
raise CheckpointExistsError(ref_info.name)
|
||||
raise ExperimentExistsError(ref_info.name)
|
||||
|
||||
for ref in executor.fetch_exps(
|
||||
exp.scm,
|
||||
force=exec_result.force,
|
||||
on_diverged=on_diverged,
|
||||
):
|
||||
exp_rev = exp.scm.get_ref(ref)
|
||||
if exp_rev:
|
||||
assert exec_result.exp_hash
|
||||
logger.debug("Collected experiment '%s'.", exp_rev[:7])
|
||||
results[exp_rev] = exec_result.exp_hash
|
||||
refs = get_remote_executor_refs(exp.scm, executor.git_url)
|
||||
|
||||
with get_exp_rwlock(exp.repo, writes=refs):
|
||||
for ref in executor.fetch_exps(
|
||||
exp.scm,
|
||||
refs,
|
||||
force=exec_result.force,
|
||||
on_diverged=on_diverged,
|
||||
):
|
||||
exp_rev = exp.scm.get_ref(ref)
|
||||
if exp_rev:
|
||||
assert exec_result.exp_hash
|
||||
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)
|
||||
@@ -690,3 +727,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.
|
||||
"""
|
||||
|
||||
@@ -12,10 +12,11 @@ from typing import (
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Set,
|
||||
Union,
|
||||
)
|
||||
|
||||
from celery.result import AsyncResult
|
||||
from funcy import cached_property
|
||||
from funcy import cached_property, first
|
||||
from kombu.message import Message
|
||||
|
||||
from dvc.daemon import daemonize
|
||||
@@ -23,15 +24,27 @@ from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
from ..exceptions import UnresolvedQueueExpNamesError
|
||||
from ..executor.base import EXEC_TMP_DIR, ExecutorInfo, ExecutorResult
|
||||
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
|
||||
from ..executor.base import ExecutorInfo, ExecutorResult
|
||||
from ..refs import CELERY_STASH
|
||||
from ..utils import EXEC_TMP_DIR, get_exp_rwlock
|
||||
from .base import (
|
||||
BaseStashQueue,
|
||||
ExpRefAndQueueEntry,
|
||||
QueueDoneResult,
|
||||
QueueEntry,
|
||||
QueueGetResult,
|
||||
)
|
||||
from .exceptions import CannotKillTasksError
|
||||
from .tasks import run_exp
|
||||
from .utils import fetch_running_exp_from_temp_dir
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_task.app import FSApp
|
||||
from dvc_task.proc.manager import ProcessManager
|
||||
from dvc_task.worker import TemporaryWorker
|
||||
|
||||
from ..refs import ExpRefInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -53,8 +66,6 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
|
||||
CELERY_DIR = "celery"
|
||||
|
||||
_shutdown_task_ids: Set[str] = set()
|
||||
|
||||
@cached_property
|
||||
def wdir(self) -> str:
|
||||
return os.path.join(self.repo.tmp_dir, EXEC_TMP_DIR, self.CELERY_DIR)
|
||||
@@ -112,7 +123,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
def spawn_worker(self, num: int = 1):
|
||||
def _spawn_worker(self, num: int = 1):
|
||||
"""spawn one single worker to process to queued tasks.
|
||||
|
||||
Argument:
|
||||
@@ -155,14 +166,15 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
if node_name in active_worker:
|
||||
logger.debug(f"Exp queue worker {node_name} already exist")
|
||||
continue
|
||||
self.spawn_worker(num)
|
||||
self._spawn_worker(num)
|
||||
started += 1
|
||||
|
||||
return started
|
||||
|
||||
def put(self, *args, **kwargs) -> QueueEntry:
|
||||
"""Stash an experiment and add it to the queue."""
|
||||
entry = self._stash_exp(*args, **kwargs)
|
||||
with get_exp_rwlock(self.repo, writes=["workspace", CELERY_STASH]):
|
||||
entry = self._stash_exp(*args, **kwargs)
|
||||
self.celery.signature(run_exp.s(entry.asdict())).delay()
|
||||
return entry
|
||||
|
||||
@@ -181,6 +193,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
continue
|
||||
args, kwargs, _embed = msg.decode()
|
||||
entry_dict = kwargs.get("entry_dict", args[0])
|
||||
logger.debug("Found queued task %s", entry_dict["stash_rev"])
|
||||
yield _MessageEntry(msg, QueueEntry.from_dict(entry_dict))
|
||||
|
||||
def _iter_processed(self) -> Generator[_MessageEntry, None, None]:
|
||||
@@ -197,6 +210,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug("Found active task %s", entry.stash_rev)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def _iter_done_tasks(self) -> Generator[_TaskEntry, None, None]:
|
||||
@@ -205,6 +219,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if result.ready():
|
||||
logger.debug("Found done task %s", entry.stash_rev)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def iter_active(self) -> Generator[QueueEntry, None, None]:
|
||||
@@ -237,76 +252,136 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
def reproduce(self) -> Mapping[str, Mapping[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_result(
|
||||
def _load_info(self, rev: str) -> ExecutorInfo:
|
||||
infofile = self.get_infofile_path(rev)
|
||||
return ExecutorInfo.load_json(infofile)
|
||||
|
||||
def _get_done_result(
|
||||
self, entry: QueueEntry, timeout: Optional[float] = None
|
||||
) -> Optional[ExecutorResult]:
|
||||
) -> Optional["ExecutorResult"]:
|
||||
from celery.exceptions import TimeoutError as _CeleryTimeout
|
||||
|
||||
def _load_info(rev: str) -> ExecutorInfo:
|
||||
infofile = self.get_infofile_path(rev)
|
||||
return ExecutorInfo.load_json(infofile)
|
||||
|
||||
def _load_collected(rev: str) -> Optional[ExecutorResult]:
|
||||
executor_info = _load_info(rev)
|
||||
if executor_info.collected:
|
||||
for msg, processed_entry in self._iter_processed():
|
||||
if entry.stash_rev == processed_entry.stash_rev:
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug(
|
||||
"Waiting for exp task '%s' to complete", result.id
|
||||
)
|
||||
try:
|
||||
result.get(timeout=timeout)
|
||||
except _CeleryTimeout as exc:
|
||||
raise DvcException(
|
||||
"Timed out waiting for exp to finish."
|
||||
) from exc
|
||||
executor_info = self._load_info(entry.stash_rev)
|
||||
return executor_info.result
|
||||
raise FileNotFoundError
|
||||
raise FileNotFoundError
|
||||
|
||||
def get_result(
|
||||
self, entry: QueueEntry, timeout: Optional[float] = None
|
||||
) -> Optional["ExecutorResult"]:
|
||||
|
||||
try:
|
||||
return _load_collected(entry.stash_rev)
|
||||
return self._get_done_result(entry, timeout)
|
||||
except FileNotFoundError:
|
||||
# Infofile will not be created until execution begins
|
||||
pass
|
||||
|
||||
for queue_entry in self.iter_queued():
|
||||
if entry.stash_rev == queue_entry.stash_rev:
|
||||
raise DvcException("Experiment has not been started.")
|
||||
for result, active_entry in self._iter_active_tasks():
|
||||
if entry.stash_rev == active_entry.stash_rev:
|
||||
logger.debug(
|
||||
"Waiting for exp task '%s' to complete", result.id
|
||||
)
|
||||
try:
|
||||
result.get(timeout=timeout)
|
||||
except _CeleryTimeout as exc:
|
||||
raise DvcException(
|
||||
"Timed out waiting for exp to finish."
|
||||
) from exc
|
||||
executor_info = _load_info(entry.stash_rev)
|
||||
return executor_info.result
|
||||
|
||||
# NOTE: It's possible for an exp to complete while iterating through
|
||||
# other queued and active tasks, in which case the exp will get moved
|
||||
# out of the active task list, and needs to be loaded here.
|
||||
return _load_collected(entry.stash_rev)
|
||||
return self._get_done_result(entry, timeout)
|
||||
|
||||
def kill(self, revs: Collection[str]) -> None:
|
||||
to_kill: Set[QueueEntry] = set()
|
||||
def _get_running_task_ids(self) -> Set[str]:
|
||||
running_task_ids: Set[str] = set()
|
||||
active_workers = self.worker_status()
|
||||
for _, tasks in active_workers.items():
|
||||
task = first(tasks)
|
||||
if task:
|
||||
running_task_ids.add(task["id"])
|
||||
return running_task_ids
|
||||
|
||||
def _try_to_kill_tasks(
|
||||
self, to_kill: Dict[QueueEntry, str], force: bool
|
||||
) -> Dict[QueueEntry, str]:
|
||||
fail_to_kill_entries: Dict[QueueEntry, str] = {}
|
||||
for queue_entry, rev in to_kill.items():
|
||||
try:
|
||||
if force:
|
||||
self.proc.kill(queue_entry.stash_rev)
|
||||
else:
|
||||
self.proc.interrupt(queue_entry.stash_rev)
|
||||
logger.debug(f"Task {rev} had been killed.")
|
||||
except ProcessLookupError:
|
||||
fail_to_kill_entries[queue_entry] = rev
|
||||
return fail_to_kill_entries
|
||||
|
||||
def _mark_inactive_tasks_failure(self, remained_entries):
|
||||
remained_revs: List[str] = []
|
||||
running_ids = self._get_running_task_ids()
|
||||
logger.debug(f"Current running tasks ids: {running_ids}.")
|
||||
for msg, entry in self._iter_processed():
|
||||
if entry not in remained_entries:
|
||||
continue
|
||||
task_id = msg.headers["id"]
|
||||
if task_id in running_ids:
|
||||
remained_revs.append(remained_entries[entry])
|
||||
else:
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug(
|
||||
f"Task id {task_id} rev {remained_entries[entry]} "
|
||||
"marked as failure."
|
||||
)
|
||||
self.celery.backend.mark_as_failure(task_id, None)
|
||||
|
||||
if remained_revs:
|
||||
raise CannotKillTasksError(remained_revs)
|
||||
|
||||
def _kill_entries(self, entries: Dict[QueueEntry, str], force: bool):
|
||||
logger.debug(
|
||||
"Found active tasks: '%s' to kill",
|
||||
list(entries.values()),
|
||||
)
|
||||
inactive_entries: Dict[QueueEntry, str] = self._try_to_kill_tasks(
|
||||
entries, force
|
||||
)
|
||||
|
||||
if inactive_entries:
|
||||
self._mark_inactive_tasks_failure(inactive_entries)
|
||||
|
||||
def kill(self, revs: Collection[str], force: bool = False) -> None:
|
||||
name_dict: Dict[
|
||||
str, Optional[QueueEntry]
|
||||
] = self.match_queue_entry_by_name(set(revs), self.iter_active())
|
||||
|
||||
missing_rev: List[str] = []
|
||||
missing_revs: List[str] = []
|
||||
to_kill: Dict[QueueEntry, str] = {}
|
||||
for rev, queue_entry in name_dict.items():
|
||||
if queue_entry is None:
|
||||
missing_rev.append(rev)
|
||||
missing_revs.append(rev)
|
||||
else:
|
||||
to_kill.add(queue_entry)
|
||||
to_kill[queue_entry] = rev
|
||||
|
||||
if missing_rev:
|
||||
raise UnresolvedQueueExpNamesError(missing_rev)
|
||||
if missing_revs:
|
||||
raise UnresolvedQueueExpNamesError(missing_revs)
|
||||
|
||||
for queue_entry in to_kill:
|
||||
self.proc.kill(queue_entry.stash_rev)
|
||||
if to_kill:
|
||||
self._kill_entries(to_kill, force)
|
||||
|
||||
def shutdown(self, kill: bool = False):
|
||||
self.celery.control.shutdown()
|
||||
if kill:
|
||||
for _, task_entry in self._iter_active_tasks():
|
||||
try:
|
||||
self.proc.kill(task_entry.stash_rev)
|
||||
except ProcessLookupError:
|
||||
continue
|
||||
to_kill: Dict[QueueEntry, str] = {}
|
||||
for entry in self.iter_active():
|
||||
to_kill[entry] = entry.name or entry.stash_rev
|
||||
if to_kill:
|
||||
self._kill_entries(to_kill, True)
|
||||
|
||||
def follow(
|
||||
self,
|
||||
@@ -353,10 +428,11 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
) as fobj:
|
||||
ui.write(fobj.read())
|
||||
|
||||
def worker_status(self) -> Dict:
|
||||
def worker_status(self) -> Dict[str, List[Dict]]:
|
||||
"""Return the current active celery worker"""
|
||||
status = self.celery.control.inspect().active() or {}
|
||||
logger.debug(f"Worker status: {status}")
|
||||
|
||||
return status
|
||||
|
||||
def clear(self, *args, **kwargs):
|
||||
@@ -368,3 +444,46 @@ 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
|
||||
|
||||
def get_ref_and_entry_by_names(
|
||||
self,
|
||||
exp_names: Union[str, List[str]],
|
||||
git_remote: Optional[str] = None,
|
||||
) -> Dict[str, ExpRefAndQueueEntry]:
|
||||
"""Find finished ExpRefInfo or queued or failed QueueEntry by name"""
|
||||
from ..utils import resolve_name
|
||||
|
||||
if isinstance(exp_names, str):
|
||||
exp_names = [exp_names]
|
||||
results: Dict[str, ExpRefAndQueueEntry] = {}
|
||||
|
||||
exp_ref_match: Dict[str, Optional["ExpRefInfo"]] = resolve_name(
|
||||
self.scm, exp_names, git_remote
|
||||
)
|
||||
if not git_remote:
|
||||
queue_entry_match: Dict[
|
||||
str, Optional["QueueEntry"]
|
||||
] = self.match_queue_entry_by_name(
|
||||
exp_names, self.iter_queued(), self.iter_done()
|
||||
)
|
||||
|
||||
for exp_name in exp_names:
|
||||
exp_ref = exp_ref_match[exp_name]
|
||||
queue_entry = None if git_remote else queue_entry_match[exp_name]
|
||||
results[exp_name] = ExpRefAndQueueEntry(exp_ref, queue_entry)
|
||||
return results
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from typing import Collection
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class CannotKillTasksError(DvcException):
|
||||
def __init__(self, revs: Collection[str]):
|
||||
rev_str = ",".join(revs)
|
||||
super().__init__(
|
||||
f"Task '{rev_str}' is initializing, please wait a few seconds "
|
||||
"until the experiments start running to retry the kill operation."
|
||||
)
|
||||
@@ -1,19 +1,21 @@
|
||||
from typing import Any, Dict
|
||||
from typing import TYPE_CHECKING, 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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..executor.base import BaseExecutor
|
||||
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
@shared_task
|
||||
def setup_exp(entry_dict: Dict[str, Any]) -> str:
|
||||
def setup_exp(entry_dict: Dict[str, Any]) -> "BaseExecutor":
|
||||
"""Setup an experiment.
|
||||
|
||||
Arguments:
|
||||
@@ -28,7 +30,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 +38,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 +81,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 +106,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)()
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Generator, Optional
|
||||
|
||||
from funcy import cached_property, first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
from ..exceptions import ExpQueueEmptyError
|
||||
from ..executor.base import (
|
||||
EXEC_PID_DIR,
|
||||
EXEC_TMP_DIR,
|
||||
BaseExecutor,
|
||||
ExecutorInfo,
|
||||
ExecutorResult,
|
||||
TaskStatus,
|
||||
)
|
||||
from ..executor.local import TempDirExecutor
|
||||
from ..utils import EXEC_PID_DIR, EXEC_TMP_DIR
|
||||
from .base import BaseStashQueue, QueueEntry, QueueGetResult
|
||||
from .utils import fetch_running_exp_from_temp_dir
|
||||
from .workspace import WorkspaceQueue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -60,7 +64,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,7 +79,7 @@ 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(
|
||||
if executor_info.status <= TaskStatus.SUCCESS and os.path.exists(
|
||||
executor_info.root_dir
|
||||
):
|
||||
yield QueueEntry(
|
||||
@@ -89,6 +93,51 @@ class TempDirQueue(WorkspaceQueue):
|
||||
None,
|
||||
)
|
||||
|
||||
def _reproduce_entry(
|
||||
self, entry: QueueEntry, executor: BaseExecutor
|
||||
) -> Dict[str, Dict[str, str]]:
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
results: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
exec_name = self._EXEC_NAME or entry.stash_rev
|
||||
infofile = self.get_infofile_path(exec_name)
|
||||
try:
|
||||
rev = entry.stash_rev
|
||||
exec_result = executor.reproduce(
|
||||
info=executor.info,
|
||||
rev=rev,
|
||||
infofile=infofile,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
log_errors=True,
|
||||
)
|
||||
if not exec_result.exp_hash:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
)
|
||||
if exec_result.ref_info:
|
||||
results[rev].update(
|
||||
self.collect_executor(
|
||||
self.repo.experiments, executor, exec_result
|
||||
)
|
||||
)
|
||||
except CheckpointKilledError:
|
||||
results[rev].update(
|
||||
self.collect_executor(
|
||||
self.repo.experiments, executor, exec_result
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
) from exc
|
||||
finally:
|
||||
executor.cleanup(infofile)
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def collect_executor(
|
||||
exp: "Experiments",
|
||||
@@ -96,3 +145,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,86 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from ..executor.base import ExecutorInfo, TaskStatus
|
||||
from ..refs import EXEC_CHECKPOINT, EXEC_NAMESPACE, EXPS_NAMESPACE, EXPS_STASH
|
||||
from ..utils import get_exp_rwlock, iter_remote_refs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from .base import BaseStashQueue
|
||||
|
||||
|
||||
def get_remote_executor_refs(scm: "Git", remote_url: str) -> List[str]:
|
||||
"""Get result list refs from a remote repository
|
||||
|
||||
Args:
|
||||
remote_url : remote executor's url
|
||||
"""
|
||||
refs = []
|
||||
for ref in iter_remote_refs(
|
||||
scm,
|
||||
remote_url,
|
||||
base=EXPS_NAMESPACE,
|
||||
):
|
||||
if ref == EXEC_CHECKPOINT or (
|
||||
not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH
|
||||
):
|
||||
refs.append(ref)
|
||||
return refs
|
||||
|
||||
|
||||
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.RUNNING:
|
||||
result[rev] = info.asdict()
|
||||
if info.git_url and fetch_refs and info.status > TaskStatus.PREPARING:
|
||||
|
||||
def on_diverged(_ref: str, _checkpoint: bool):
|
||||
return True
|
||||
|
||||
executor = TempDirExecutor.from_info(info)
|
||||
try:
|
||||
refs = get_remote_executor_refs(queue.scm, executor.git_url)
|
||||
with get_exp_rwlock(queue.repo, writes=refs):
|
||||
for ref in executor.fetch_exps(
|
||||
queue.scm,
|
||||
refs,
|
||||
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, SCMError):
|
||||
# ignore stale info files
|
||||
del result[rev]
|
||||
return result
|
||||
@@ -1,17 +1,28 @@
|
||||
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 ..refs import EXEC_BRANCH, WORKSPACE_STASH
|
||||
from ..utils import get_exp_rwlock, to_studio_params
|
||||
from .base import BaseStashQueue, QueueDoneResult, QueueEntry, QueueGetResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -24,7 +35,8 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
_EXEC_NAME: Optional[str] = "workspace"
|
||||
|
||||
def put(self, *args, **kwargs) -> QueueEntry:
|
||||
return self._stash_exp(*args, **kwargs)
|
||||
with get_exp_rwlock(self.repo, writes=["workspace", WORKSPACE_STASH]):
|
||||
return self._stash_exp(*args, **kwargs)
|
||||
|
||||
def get(self) -> QueueGetResult:
|
||||
revs = self.stash.stash_revs
|
||||
@@ -41,7 +53,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]:
|
||||
@@ -84,6 +96,8 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
def _reproduce_entry(
|
||||
self, entry: QueueEntry, executor: BaseExecutor
|
||||
) -> Dict[str, Dict[str, str]]:
|
||||
from dvc_studio_client.post_live_metrics import post_live_metrics
|
||||
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
results: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
@@ -91,6 +105,13 @@ class WorkspaceQueue(BaseStashQueue):
|
||||
infofile = self.get_infofile_path(exec_name)
|
||||
try:
|
||||
rev = entry.stash_rev
|
||||
post_live_metrics(
|
||||
"start",
|
||||
executor.info.baseline_rev,
|
||||
executor.info.name,
|
||||
"dvc",
|
||||
params=to_studio_params(self.repo.params.show()),
|
||||
)
|
||||
exec_result = executor.reproduce(
|
||||
info=executor.info,
|
||||
rev=rev,
|
||||
@@ -118,9 +139,14 @@ 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()
|
||||
post_live_metrics(
|
||||
"done",
|
||||
executor.info.baseline_rev,
|
||||
executor.info.name,
|
||||
"dvc",
|
||||
experiment_rev=first(results.get(rev, {})),
|
||||
)
|
||||
executor.cleanup(infofile)
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
@@ -135,6 +161,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 +180,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
|
||||
|
||||
@@ -16,6 +16,7 @@ EXEC_BRANCH = f"{EXEC_NAMESPACE}/EXEC_BRANCH"
|
||||
EXEC_BASELINE = f"{EXEC_NAMESPACE}/EXEC_BASELINE"
|
||||
EXEC_HEAD = f"{EXEC_NAMESPACE}/EXEC_HEAD"
|
||||
EXEC_MERGE = f"{EXEC_NAMESPACE}/EXEC_MERGE"
|
||||
EXPS_TEMP = f"{EXPS_NAMESPACE}/temp"
|
||||
STASHES = {WORKSPACE_STASH, CELERY_STASH}
|
||||
|
||||
|
||||
@@ -23,9 +24,7 @@ class ExpRefInfo:
|
||||
|
||||
namespace = EXPS_NAMESPACE
|
||||
|
||||
def __init__(
|
||||
self, baseline_sha: Optional[str] = None, name: Optional[str] = None
|
||||
):
|
||||
def __init__(self, baseline_sha: str, name: Optional[str] = None):
|
||||
self.baseline_sha = baseline_sha
|
||||
self.name: str = name if name else ""
|
||||
|
||||
@@ -33,7 +32,7 @@ class ExpRefInfo:
|
||||
return "/".join(self.parts)
|
||||
|
||||
def __repr__(self):
|
||||
baseline = f"'{self.baseline_sha}'" if self.baseline_sha else "None"
|
||||
baseline = f"'{self.baseline_sha}'"
|
||||
name = f"'{self.name}'" if self.name else "None"
|
||||
return f"ExpRefInfo(baseline_sha={baseline}, name={name})"
|
||||
|
||||
@@ -41,11 +40,7 @@ class ExpRefInfo:
|
||||
def parts(self):
|
||||
return (
|
||||
(self.namespace,)
|
||||
+ (
|
||||
(self.baseline_sha[:2], self.baseline_sha[2:])
|
||||
if self.baseline_sha
|
||||
else ()
|
||||
)
|
||||
+ ((self.baseline_sha[:2], self.baseline_sha[2:]))
|
||||
+ ((self.name,) if self.name else ())
|
||||
)
|
||||
|
||||
@@ -54,14 +49,13 @@ class ExpRefInfo:
|
||||
try:
|
||||
parts = ref.split("/")
|
||||
if (
|
||||
len(parts) < 2
|
||||
or len(parts) == 3
|
||||
len(parts) < 4
|
||||
or len(parts) > 5
|
||||
or "/".join(parts[:2]) != EXPS_NAMESPACE
|
||||
):
|
||||
raise InvalidExpRefError(ref)
|
||||
except ValueError:
|
||||
raise InvalidExpRefError(ref)
|
||||
baseline_sha = parts[2] + parts[3] if len(parts) >= 4 else None
|
||||
baseline_sha = parts[2] + parts[3]
|
||||
name = parts[4] if len(parts) == 5 else None
|
||||
return cls(baseline_sha, name)
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Union
|
||||
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import iter_revs
|
||||
|
||||
from .exceptions import UnresolvedExpNamesError
|
||||
from .queue.base import QueueEntry
|
||||
from .refs import ExpRefInfo
|
||||
from .utils import exp_refs, exp_refs_by_baseline, push_refspec
|
||||
|
||||
@@ -22,58 +15,12 @@ if TYPE_CHECKING:
|
||||
from dvc.repo.experiments.queue.celery import LocalCeleryQueue
|
||||
from dvc.scm import Git
|
||||
|
||||
from .queue.base import QueueEntry
|
||||
from .queue.base import ExpRefAndQueueEntry
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExpRefAndQueueEntry(NamedTuple):
|
||||
exp_ref_list: List["ExpRefInfo"]
|
||||
queue_entry_list: List["QueueEntry"]
|
||||
removed: List["str"]
|
||||
|
||||
|
||||
def _get_ref_and_entry_by_names(
|
||||
exp_names: Union[str, List[str]],
|
||||
scm: "Git",
|
||||
celery_queue: "LocalCeleryQueue",
|
||||
git_remote: Optional[str],
|
||||
) -> ExpRefAndQueueEntry:
|
||||
from .utils import resolve_name
|
||||
|
||||
exp_ref_list: List["ExpRefInfo"] = []
|
||||
queue_entry_list: List["QueueEntry"] = []
|
||||
removed: List[str] = []
|
||||
if isinstance(exp_names, str):
|
||||
exp_names = [exp_names]
|
||||
exp_ref_match: Dict[str, Optional["ExpRefInfo"]] = resolve_name(
|
||||
scm, exp_names, git_remote
|
||||
)
|
||||
if not git_remote:
|
||||
queue_entry_match: Dict[
|
||||
str, Optional["QueueEntry"]
|
||||
] = celery_queue.match_queue_entry_by_name(
|
||||
exp_names, celery_queue.iter_queued(), celery_queue.iter_done()
|
||||
)
|
||||
|
||||
remained = []
|
||||
for exp_name in exp_names:
|
||||
exp_ref = exp_ref_match[exp_name]
|
||||
queue_entry = None if git_remote else queue_entry_match[exp_name]
|
||||
if exp_ref or queue_entry:
|
||||
if exp_ref:
|
||||
exp_ref_list.append(exp_ref)
|
||||
if queue_entry:
|
||||
queue_entry_list.append(queue_entry)
|
||||
removed.append(exp_name)
|
||||
else:
|
||||
remained.append(exp_name)
|
||||
if remained:
|
||||
raise UnresolvedExpNamesError(remained)
|
||||
return ExpRefAndQueueEntry(exp_ref_list, queue_entry_list, removed)
|
||||
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def remove(
|
||||
@@ -103,12 +50,22 @@ def remove(
|
||||
exp_ref_list: List["ExpRefInfo"] = []
|
||||
queue_entry_list: List["QueueEntry"] = []
|
||||
if exp_names:
|
||||
result = _get_ref_and_entry_by_names(
|
||||
exp_names, repo.scm, celery_queue, git_remote
|
||||
)
|
||||
removed.extend(result.removed)
|
||||
exp_ref_list.extend(result.exp_ref_list)
|
||||
queue_entry_list.extend(result.queue_entry_list)
|
||||
results: Dict[
|
||||
str, "ExpRefAndQueueEntry"
|
||||
] = celery_queue.get_ref_and_entry_by_names(exp_names, git_remote)
|
||||
remained: List[str] = []
|
||||
for name, result in results.items():
|
||||
if not result.exp_ref_info and not result.queue_entry:
|
||||
remained.append(name)
|
||||
continue
|
||||
removed.append(name)
|
||||
if result.exp_ref_info:
|
||||
exp_ref_list.append(result.exp_ref_info)
|
||||
if result.queue_entry:
|
||||
queue_entry_list.append(result.queue_entry)
|
||||
|
||||
if remained:
|
||||
raise UnresolvedExpNamesError(remained)
|
||||
elif rev:
|
||||
exp_ref_dict = _resolve_exp_by_baseline(repo, rev, num, git_remote)
|
||||
removed.extend(exp_ref_dict.keys())
|
||||
@@ -152,8 +109,7 @@ def _remove_commited_exps(
|
||||
push_refspec(
|
||||
scm,
|
||||
remote,
|
||||
None,
|
||||
str(ref_info),
|
||||
[(None, str(ref_info))],
|
||||
progress=pbar.update_git,
|
||||
)
|
||||
else:
|
||||
|
||||
+18
-15
@@ -28,14 +28,24 @@ 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)
|
||||
return repo.experiments.reproduce_celery(jobs=jobs)
|
||||
|
||||
hydra_sweep = None
|
||||
if params:
|
||||
from dvc.utils.hydra import to_hydra_overrides
|
||||
|
||||
path_overrides = to_path_overrides(params)
|
||||
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`"
|
||||
)
|
||||
else:
|
||||
path_overrides = {}
|
||||
|
||||
@@ -45,23 +55,16 @@ def run(
|
||||
# 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:
|
||||
if kwargs.get("name"):
|
||||
raise InvalidArgumentError(
|
||||
"Sweep overrides can't be used alongside `--name`"
|
||||
)
|
||||
from dvc.utils.hydra import get_hydra_sweeps
|
||||
|
||||
sweeps = get_hydra_sweeps(path_overrides)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user