mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3cbf319e0d | ||
|
|
477a6e177f | ||
|
|
f854fc6365 | ||
|
|
1b90683e02 | ||
|
|
d762372d55 | ||
|
|
f3d304580a | ||
|
|
0e0d46e232 | ||
|
|
95ae018ed8 | ||
|
|
c29e721823 | ||
|
|
a9c7032d50 | ||
|
|
ecb767d622 | ||
|
|
fa45a9bbba | ||
|
|
f50ae589ba | ||
|
|
611c114f01 | ||
|
|
fdc18d5483 | ||
|
|
3b532b7d59 | ||
|
|
e78c4455de | ||
|
|
8e5f4a8f35 | ||
|
|
cb3159e586 | ||
|
|
7b301b09a1 | ||
|
|
55b16166c8 | ||
|
|
c70d0bd14d | ||
|
|
b99a0cd6ca | ||
|
|
d4170b2ec0 | ||
|
|
a3181bb509 | ||
|
|
4d6b90502b | ||
|
|
947427f4d9 | ||
|
|
0482a1a88b | ||
|
|
b470459987 | ||
|
|
81a010674e | ||
|
|
d946a1328e | ||
|
|
ad7d39da20 | ||
|
|
8432de5980 | ||
|
|
c73764156e | ||
|
|
df60df5242 | ||
|
|
504485ed6e | ||
|
|
1e989bde2c | ||
|
|
7e2248bbe5 | ||
|
|
a2d80b2403 | ||
|
|
76e7c76ae5 | ||
|
|
6cada6dc12 | ||
|
|
89d4461908 | ||
|
|
a124d7e866 | ||
|
|
628be2ae49 | ||
|
|
7a71e06946 | ||
|
|
91e54d8c27 | ||
|
|
071b2b8f81 | ||
|
|
0b2b59a601 | ||
|
|
65dc1cd08f | ||
|
|
1b103297b7 | ||
|
|
887c73b3c6 | ||
|
|
660c17f654 | ||
|
|
49da2eef02 | ||
|
|
aff65d42bb | ||
|
|
8968405770 | ||
|
|
b6e8ba6758 | ||
|
|
e307a62dc0 | ||
|
|
639eeed43b | ||
|
|
91f12395f1 | ||
|
|
e7daeb1c3e | ||
|
|
d3569035c8 | ||
|
|
daee52e176 | ||
|
|
a8415b01d0 | ||
|
|
77f56090ff | ||
|
|
1d3102f29b | ||
|
|
a0cbcaa3c8 | ||
|
|
ab780879d1 | ||
|
|
bfad82a2e3 | ||
|
|
e5eec59cb8 | ||
|
|
2a4b83060e | ||
|
|
9e2ebe9fcc | ||
|
|
2cef130191 | ||
|
|
17b9b1571e | ||
|
|
37c8ef3406 | ||
|
|
b7aea414e6 | ||
|
|
126a64618b | ||
|
|
8bdf9b1138 | ||
|
|
9cb242c8a5 | ||
|
|
11802426de | ||
|
|
b376044c45 | ||
|
|
7b3b4b373b | ||
|
|
512bfc4305 | ||
|
|
54ee8c0d77 | ||
|
|
063eb6904d | ||
|
|
3e183d62ed | ||
|
|
e815bbe211 | ||
|
|
2374c72105 | ||
|
|
5cf1421c61 | ||
|
|
a6a2feea62 | ||
|
|
6c1d00c2d9 | ||
|
|
f72456f67c | ||
|
|
2c556f58d3 | ||
|
|
460d08057c | ||
|
|
cc6deb24c8 | ||
|
|
e681030c72 | ||
|
|
7a51548214 | ||
|
|
8e99fd0091 | ||
|
|
7e5a586f85 | ||
|
|
c76a84b7b1 | ||
|
|
26008f1559 | ||
|
|
92d680ff94 | ||
|
|
c69c46d08c | ||
|
|
6477ade557 | ||
|
|
78dd045d29 | ||
|
|
2ddbbdf5a4 | ||
|
|
17a565023b | ||
|
|
b5d5728bf8 | ||
|
|
055641a51e | ||
|
|
fc509d4e4a | ||
|
|
a67fea2235 | ||
|
|
f062b52e05 | ||
|
|
25166f3e02 | ||
|
|
a79ea15827 | ||
|
|
8db9a2f70f | ||
|
|
ff2f2e433f | ||
|
|
3fc9f323a7 | ||
|
|
bf0aa5e8aa | ||
|
|
3b3866323e | ||
|
|
3c3cbee7fd | ||
|
|
4a95020155 | ||
|
|
3a429a869e | ||
|
|
99465b25b5 | ||
|
|
fcdad4d229 | ||
|
|
2e8b463795 | ||
|
|
206e00ccbe | ||
|
|
f13443477d | ||
|
|
9837747eef | ||
|
|
cf5dfc517a | ||
|
|
c134d4ab8e | ||
|
|
7a0b3e9377 | ||
|
|
d6f8e5a507 | ||
|
|
2ca1d6c7c1 | ||
|
|
f7fbac1eed | ||
|
|
7c1e339650 | ||
|
|
b7c0c41262 | ||
|
|
9180a718c1 | ||
|
|
306456d453 | ||
|
|
95421a5605 | ||
|
|
31635b8312 |
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
* @iterative/dvc
|
||||
@@ -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
|
||||
+70
-115
@@ -6,152 +6,107 @@ on:
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deb:
|
||||
runs-on: ubuntu-18.04
|
||||
binary:
|
||||
permissions:
|
||||
contents: write # for actions/upload-release-asset to upload release asset
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- {pkg: "deb", os: "ubuntu-18.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-18.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-10.15", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
|
||||
|
||||
name: ${{ matrix.pkg }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
python-version: "3.10"
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
pyproject.toml
|
||||
scripts/build-requirements.txt
|
||||
|
||||
- name: Set up Ruby 2.6
|
||||
uses: ruby/setup-ruby@v1
|
||||
if: matrix.pkg != 'exe'
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
- name: Build deb
|
||||
run: python scripts/build.py deb
|
||||
- name: Publish deb
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/fpm/dvc_${{ github.event.release.tag_name }}_amd64.deb
|
||||
asset_name: dvc_${{ github.event.release.tag_name }}_amd64.deb
|
||||
asset_content_type: binary/octet-stream
|
||||
rpm:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
- name: Build rpm
|
||||
run: python scripts/build.py rpm
|
||||
- name: Publish rpm
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
|
||||
asset_content_type: binary/octet-stream
|
||||
osxpkg:
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
run: |
|
||||
# available lxml (webdav dependency) wheels are built with an old
|
||||
# SDK, which breaks osxpkg notarization. Building from scratch
|
||||
# instead.
|
||||
pip install .[all] --no-binary lxml
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
- name: Build
|
||||
run: python scripts/build.py osxpkg
|
||||
- name: Publish
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}.pkg
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}.pkg
|
||||
asset_content_type: binary/octet-stream
|
||||
exe:
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
ruby-version: '2.6'
|
||||
|
||||
- name: Install fpm
|
||||
if: matrix.pkg != 'exe'
|
||||
run: gem install --no-document fpm
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
|
||||
- name: Pull images
|
||||
run: |
|
||||
dvc pull
|
||||
- name: Build
|
||||
run: python scripts\build.py exe
|
||||
shell: cmd
|
||||
- name: Publish
|
||||
run: dvc pull
|
||||
|
||||
- name: Build ${{ matrix.pkg }}
|
||||
run: python scripts/build.py ${{ matrix.pkg }}
|
||||
|
||||
- name: Publish ${{ matrix.pkg }}
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/innosetup/dvc-${{ github.event.release.tag_name }}.exe
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}.exe
|
||||
asset_path: scripts/${{ matrix.pkg == 'exe' && 'innosetup' || 'fpm' }}/${{ matrix.asset }}
|
||||
asset_name: ${{ matrix.asset }}
|
||||
asset_content_type: binary/octet-stream
|
||||
|
||||
pip:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install
|
||||
run: pip install ".[all,tests]"
|
||||
- name: Build packages
|
||||
run: pip install --upgrade pip wheel
|
||||
|
||||
- name: Force version for Test PyPI uploads
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
||||
run: |
|
||||
pip install -U pip
|
||||
./scripts/build_package.sh
|
||||
pip install setuptools_scm
|
||||
echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
||||
|
||||
- name: Build packages
|
||||
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@master
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
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,30 @@
|
||||
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 +32,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,8 +45,17 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.8", "3.9", "3.10"]
|
||||
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: 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
|
||||
with:
|
||||
@@ -51,39 +64,31 @@ 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]"
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
run: >-
|
||||
python -m tests -n=auto
|
||||
--cov-report=xml --cov-report=term
|
||||
${{ env.extra_test_args }}
|
||||
timeout-minutes: 20
|
||||
run: pytest -nauto --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
|
||||
+12
-4
@@ -11,9 +11,9 @@
|
||||
• `Contributing`_
|
||||
• `Community and Support`_
|
||||
|
||||
|CI| |Maintainability| |Coverage| |VS Code| |DOI|
|
||||
|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.
|
||||
@@ -248,6 +248,10 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://codeclimate.com/github/iterative/dvc
|
||||
:alt: Code Climate
|
||||
|
||||
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/dvc
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: Python Version
|
||||
|
||||
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/main/graph/badge.svg
|
||||
:target: https://codecov.io/gh/iterative/dvc
|
||||
:alt: Codecov
|
||||
@@ -272,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
|
||||
@@ -288,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)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from dataclasses import asdict, dataclass, field, fields
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
|
||||
from funcy import compact
|
||||
|
||||
|
||||
@dataclass
|
||||
class Annotation:
|
||||
PARAM_DESC: ClassVar[str] = "desc"
|
||||
PARAM_TYPE: ClassVar[str] = "type"
|
||||
PARAM_LABELS: ClassVar[str] = "labels"
|
||||
PARAM_META: ClassVar[str] = "meta"
|
||||
|
||||
desc: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def update(self, **kwargs) -> "Annotation":
|
||||
for attr, value in kwargs.items():
|
||||
if value and hasattr(self, attr):
|
||||
setattr(self, attr, value)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
ANNOTATION_FIELDS = [field.name for field in fields(Annotation)]
|
||||
ANNOTATION_SCHEMA = {
|
||||
Annotation.PARAM_DESC: str,
|
||||
Annotation.PARAM_TYPE: str,
|
||||
Annotation.PARAM_LABELS: [str],
|
||||
Annotation.PARAM_META: object,
|
||||
}
|
||||
+12
-6
@@ -1,9 +1,15 @@
|
||||
from .data import ( # noqa, pylint: disable=redefined-builtin
|
||||
get_url,
|
||||
open,
|
||||
read,
|
||||
)
|
||||
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .data import open # pylint: disable=redefined-builtin
|
||||
from .data import get_url, read
|
||||
from .experiments import make_checkpoint
|
||||
from .params import params_show
|
||||
|
||||
__all__ = ["get_url", "make_checkpoint", "open", "params_show", "read"]
|
||||
__all__ = [
|
||||
"get_url",
|
||||
"make_checkpoint",
|
||||
"open",
|
||||
"params_show",
|
||||
"read",
|
||||
"DVCFileSystem",
|
||||
]
|
||||
|
||||
@@ -37,6 +37,10 @@ def params_show(
|
||||
stages to retrieve parameters from.
|
||||
Defaults to `None`.
|
||||
If `None`, all parameters from all stages will be retrieved.
|
||||
If this method is called from a different location to the one where
|
||||
the `dvc.yaml` is found, the relative path to the `dvc.yaml` must
|
||||
be provided as a prefix with the syntax `{relpath}:{stage}`.
|
||||
For example: `subdir/dvc.yaml:stage-0` or `../dvc.yaml:stage-1`.
|
||||
rev (str, optional): Name of the `Git revision`_ to retrieve parameters
|
||||
from.
|
||||
Defaults to `None`.
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
)
|
||||
return None
|
||||
|
||||
from dvc_data.build import IgnoreInCollectedDirError
|
||||
from dvc_data.hashfile.build import IgnoreInCollectedDirError
|
||||
|
||||
if isinstance(exc, IgnoreInCollectedDirError):
|
||||
logger.exception("")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from argparse import _AppendAction
|
||||
|
||||
|
||||
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
from funcy import ldistinct
|
||||
|
||||
items = getattr(namespace, self.dest) or []
|
||||
items.extend(map(str.strip, values.split(",")))
|
||||
setattr(namespace, self.dest, ldistinct(items))
|
||||
|
||||
|
||||
class KeyValueArgs(_AppendAction):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
items = getattr(namespace, self.dest) or {}
|
||||
for value in filter(bool, values):
|
||||
key, _, value = value.partition("=")
|
||||
items[key.strip()] = value
|
||||
|
||||
setattr(namespace, self.dest, items)
|
||||
+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
|
||||
]
|
||||
|
||||
+36
-9
@@ -2,12 +2,43 @@ import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import KeyValueArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help="User description of the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--meta",
|
||||
metavar="key=value",
|
||||
nargs=1,
|
||||
action=KeyValueArgs,
|
||||
help="Custom metadata to add to the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--label",
|
||||
dest="labels",
|
||||
type=str,
|
||||
action="append",
|
||||
metavar="<str>",
|
||||
help="Label for the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
type=str,
|
||||
metavar="<str>",
|
||||
help="Type of the data.",
|
||||
)
|
||||
|
||||
|
||||
class CmdAdd(CmdBase):
|
||||
def run(self):
|
||||
from dvc.exceptions import (
|
||||
@@ -28,6 +59,9 @@ class CmdAdd(CmdBase):
|
||||
glob=self.args.glob,
|
||||
desc=self.args.desc,
|
||||
out=self.args.out,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
jobs=self.args.jobs,
|
||||
@@ -109,15 +143,8 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
)
|
||||
|
||||
_add_annotating_args(parser)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
|
||||
@@ -110,7 +110,7 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
".dvc files and stage names."
|
||||
),
|
||||
).complete = completion.DVC_FILE
|
||||
checkout_parser.set_defaults(func=CmdCheckout)
|
||||
|
||||
@@ -70,7 +70,9 @@ def add_parser(subparsers, parent_parser):
|
||||
commit_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="stages or .dvc files to commit. Optional. "
|
||||
"(Finds all DVC files in the workspace by default.)",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files and stage names."
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
commit_parser.set_defaults(func=CmdCommit)
|
||||
|
||||
+1
-1
@@ -204,7 +204,7 @@ def add_parser(subparsers, parent_parser):
|
||||
dag_parser.add_argument(
|
||||
"target",
|
||||
nargs="?",
|
||||
help="Stage or output to show pipeline for (optional). "
|
||||
help="Stage name or output to show pipeline for. "
|
||||
"Finds all stages in the workspace by default.",
|
||||
)
|
||||
dag_parser.set_defaults(func=CmdDAG)
|
||||
|
||||
+137
-21
@@ -1,11 +1,18 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
|
||||
|
||||
from funcy import chunks, compact, log_durations
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import CommaSeparatedArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.cli.utils import (
|
||||
append_doc_link,
|
||||
fix_subparsers,
|
||||
hide_subparsers_from_help,
|
||||
)
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
@@ -31,15 +38,22 @@ class CmdDataStatus(CmdBase):
|
||||
"unchanged": "DVC unchanged files",
|
||||
}
|
||||
HINTS = {
|
||||
"not_in_cache": 'use "dvc pull <file>..." '
|
||||
"to update your local storage",
|
||||
"committed": "git commit the corresponding dvc files "
|
||||
"to update the repo",
|
||||
"uncommitted": 'use "dvc commit <file>..." to track changes',
|
||||
"untracked": 'use "git add <file> ..." or '
|
||||
'dvc add <file>..." to commit to git or to dvc',
|
||||
"git_dirty": "there are {}changes not tracked by dvc, "
|
||||
'use "git status" to see',
|
||||
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
|
||||
"committed": (
|
||||
"git commit the corresponding dvc files to update the repo",
|
||||
),
|
||||
"uncommitted": (
|
||||
'use "dvc commit <file>..." to track changes',
|
||||
'use "dvc checkout <file>..." to discard changes',
|
||||
),
|
||||
"untracked": (
|
||||
'use "git add <file> ..." or '
|
||||
'"dvc add <file>..." to commit to git or to dvc',
|
||||
),
|
||||
"git_dirty": (
|
||||
"there are {}changes not tracked by dvc, "
|
||||
'use "git status" to see',
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -76,8 +90,9 @@ class CmdDataStatus(CmdBase):
|
||||
color = cls.COLORS.get(stage, None)
|
||||
|
||||
ui.write(header)
|
||||
if hint := cls.HINTS.get(stage):
|
||||
ui.write(f" ({hint})")
|
||||
if hints := cls.HINTS.get(stage):
|
||||
for hint in hints:
|
||||
ui.write(f" ({hint})")
|
||||
|
||||
if isinstance(stage_status, dict):
|
||||
items = [
|
||||
@@ -92,9 +107,10 @@ class CmdDataStatus(CmdBase):
|
||||
out = "\n".join(tabs + item for item in chunk)
|
||||
ui.write(colorize(out, color))
|
||||
|
||||
if (hint := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
|
||||
message = hint.format("other " if result else "")
|
||||
ui.write(f"[blue]({message})[/]", styled=True)
|
||||
if (hints := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
|
||||
for hint in hints:
|
||||
message = hint.format("other " if result else "")
|
||||
ui.write(f"[blue]({message})[/]", styled=True)
|
||||
return 0
|
||||
|
||||
def run(self) -> int:
|
||||
@@ -102,7 +118,6 @@ class CmdDataStatus(CmdBase):
|
||||
status = self.repo.data_status(
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
with_dirs=self.args.with_dirs,
|
||||
)
|
||||
|
||||
if not self.args.unchanged:
|
||||
@@ -116,6 +131,65 @@ class CmdDataStatus(CmdBase):
|
||||
return self._show_status(status)
|
||||
|
||||
|
||||
class CmdDataLs(CmdBase):
|
||||
@staticmethod
|
||||
def _show_table(
|
||||
d: Iterable[Dict[str, Any]],
|
||||
filter_types: Set[str],
|
||||
filter_labels: Set[str],
|
||||
markdown: bool = False,
|
||||
) -> None:
|
||||
from rich.style import Style
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
td = TabularData(
|
||||
columns=["Path", "Type", "Labels", "Description"], fill_value="-"
|
||||
)
|
||||
for entry in sorted(d, key=itemgetter("path")):
|
||||
typ = entry.get("type", "")
|
||||
desc = entry.get("desc", "-")
|
||||
labels = entry.get("labels", [])
|
||||
|
||||
if filter_types and typ not in filter_types:
|
||||
continue
|
||||
if filter_labels and filter_labels.isdisjoint(labels):
|
||||
continue
|
||||
|
||||
rich_label = ui.rich_text()
|
||||
for index, label in enumerate(labels):
|
||||
if index:
|
||||
rich_label.append(",")
|
||||
style = Style(bold=label in filter_labels, color="green")
|
||||
rich_label.append(label, style=style)
|
||||
|
||||
if markdown and desc:
|
||||
desc = desc.partition("\n")[0]
|
||||
|
||||
path = ui.rich_text(entry["path"], style="cyan")
|
||||
type_style = Style(bold=typ in filter_types, color="yellow")
|
||||
typ = ui.rich_text(entry.get("type", ""), style=type_style)
|
||||
td.append([path, typ or "-", rich_label or "-", desc])
|
||||
|
||||
td.render(markdown=markdown, rich_table=True)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo.data import ls
|
||||
|
||||
filter_labels = set(self.args.labels)
|
||||
filter_types = set(self.args.type)
|
||||
d = ls(
|
||||
self.repo, targets=self.args.targets, recursive=self.args.recursive
|
||||
)
|
||||
self._show_table(
|
||||
d,
|
||||
filter_labels=filter_labels,
|
||||
filter_types=filter_types,
|
||||
markdown=self.args.markdown,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
data_parser = subparsers.add_parser(
|
||||
"data",
|
||||
@@ -172,10 +246,52 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--with-dirs",
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
DATA_LS_HELP = "List data tracked by DVC with its metadata."
|
||||
data_ls_parser = data_subparsers.add_parser(
|
||||
"ls",
|
||||
aliases=["list"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DATA_LS_HELP, "data/ls"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
add_help=False,
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
dest="markdown",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
data_ls_parser.add_argument(
|
||||
"--type",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of type to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--labels",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of labels to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Recursively list from the specified directories.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"targets",
|
||||
default=None,
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
data_ls_parser.set_defaults(func=CmdDataLs)
|
||||
hide_subparsers_from_help(data_subparsers)
|
||||
|
||||
@@ -43,7 +43,8 @@ class CmdDataPull(CmdDataBase):
|
||||
)
|
||||
self.log_summary(stats)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
self.log_summary(getattr(exc, "stats", {}))
|
||||
if stats := getattr(exc, "stats", {}):
|
||||
self.log_summary(stats)
|
||||
logger.exception("failed to pull data from the cloud")
|
||||
return 1
|
||||
|
||||
@@ -119,7 +120,7 @@ def shared_parent_parser():
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
".dvc files and stage names."
|
||||
),
|
||||
).complete = completion.DVC_FILE
|
||||
|
||||
@@ -194,7 +195,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
"--glob",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Pull cache for targets matching shell-style wildcards.",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
pull_parser.set_defaults(func=CmdDataPull)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import argparse
|
||||
|
||||
from dvc.cli.utils import (
|
||||
append_doc_link,
|
||||
fix_plumbing_subparsers,
|
||||
fix_subparsers,
|
||||
hide_subparsers_from_help,
|
||||
)
|
||||
from dvc.commands.experiments import (
|
||||
apply,
|
||||
@@ -59,7 +59,7 @@ def add_parser(subparsers, parent_parser):
|
||||
fix_subparsers(experiments_subparsers)
|
||||
for cmd in SUB_COMMANDS:
|
||||
cmd.add_parser(experiments_subparsers, parent_parser)
|
||||
fix_plumbing_subparsers(experiments_subparsers)
|
||||
hide_subparsers_from_help(experiments_subparsers)
|
||||
|
||||
|
||||
def add_rev_selection_flags(
|
||||
|
||||
@@ -27,7 +27,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
elif diff:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
precision = self.args.precision or DEFAULT_PRECISION
|
||||
|
||||
@@ -16,13 +16,15 @@ class CmdExperimentsList(CmdBase):
|
||||
num=self.args.num,
|
||||
git_remote=self.args.git_remote,
|
||||
)
|
||||
tags = self.repo.scm.describe(exps)
|
||||
remained = {baseline for baseline, tag in tags.items() if tag is None}
|
||||
base = "refs/heads"
|
||||
ref_heads = self.repo.scm.describe(remained, base=base)
|
||||
|
||||
for baseline in exps:
|
||||
tag = self.repo.scm.describe(baseline)
|
||||
if not tag:
|
||||
branch = self.repo.scm.describe(baseline, base="refs/heads")
|
||||
if branch:
|
||||
tag = branch.split("/")[-1]
|
||||
name = tag if tag else baseline[:7]
|
||||
name = baseline[:7]
|
||||
if tags[baseline] or ref_heads[baseline]:
|
||||
name = tags[baseline] or ref_heads[baseline][len(base) + 1 :]
|
||||
if not name_only:
|
||||
print(f"{name}:")
|
||||
for exp_name in exps[baseline]:
|
||||
|
||||
@@ -95,11 +95,8 @@ def _collect_rows(
|
||||
|
||||
exp = results.get("data", {})
|
||||
|
||||
if exp.get("running"):
|
||||
state = "Running"
|
||||
elif exp.get("queued"):
|
||||
state = "Queued"
|
||||
else:
|
||||
state = exp.get("status")
|
||||
if state == "Success":
|
||||
state = fill_value
|
||||
|
||||
is_baseline = rev == "baseline"
|
||||
@@ -476,6 +473,8 @@ class CmdExperimentsShow(CmdBase):
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
hide_queued=self.args.hide_queued,
|
||||
hide_failed=self.args.hide_failed,
|
||||
revs=self.args.rev,
|
||||
num=self.args.num,
|
||||
sha_only=self.args.sha,
|
||||
@@ -594,6 +593,18 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Always show git commit SHAs instead of branch/tag names.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--hide-failed",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Hide failed experiments in the table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--hide-queued",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Hide queued experiments in the table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
|
||||
@@ -74,7 +74,7 @@ class CmdMergeDriver(CmdHookBase):
|
||||
our = Dvcfile(dvc, self.args.our, verify=False)
|
||||
their = Dvcfile(dvc, self.args.their, verify=False)
|
||||
|
||||
our.merge(ancestor, their)
|
||||
our.merge(ancestor, their, allowed=["add", "remove", "change"])
|
||||
|
||||
return 0
|
||||
finally:
|
||||
|
||||
+17
-10
@@ -19,7 +19,11 @@ class CmdImport(CmdBase):
|
||||
fname=self.args.file,
|
||||
rev=self.args.rev,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -33,6 +37,8 @@ class CmdImport(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download file or directory tracked by DVC or by Git "
|
||||
"into the workspace, and track it."
|
||||
@@ -69,20 +75,19 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create .dvc file without actually downloading it.",
|
||||
help="Only create .dvc file without actually importing target data.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Create .dvc file including target data hash value(s)"
|
||||
" but do not actually download the file(s).",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-j",
|
||||
@@ -94,4 +99,6 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImport)
|
||||
|
||||
+23
-7
@@ -17,10 +17,15 @@ class CmdImportUrl(CmdBase):
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
version_aware=self.args.version_aware,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception(
|
||||
@@ -34,6 +39,8 @@ class CmdImportUrl(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download or copy file from URL and take it under DVC control."
|
||||
)
|
||||
@@ -66,11 +73,19 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create .dvc file without actually downloading it.",
|
||||
help="Only create .dvc file without actually importing target data.",
|
||||
)
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Create .dvc file including target data hash value(s)"
|
||||
" but do not actually download the file(s).",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--to-remote",
|
||||
@@ -95,12 +110,13 @@ def add_parser(subparsers, parent_parser):
|
||||
metavar="<number>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
"--version-aware",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
"Import using cloud versioning. Implied if the URL contains a "
|
||||
"version ID."
|
||||
),
|
||||
)
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImportUrl)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
from .ls import _prettify
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdListUrl(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
from dvc.repo import Repo
|
||||
|
||||
entries = Repo.ls_url(self.args.url, recursive=self.args.recursive)
|
||||
if entries:
|
||||
entries = _prettify(entries, with_color=True)
|
||||
ui.write("\n".join(entries))
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
LS_HELP = "List directory contents from URL."
|
||||
lsurl_parser = subparsers.add_parser(
|
||||
"list-url",
|
||||
aliases=["ls-url"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(LS_HELP, "list-url"),
|
||||
help=LS_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
lsurl_parser.add_argument(
|
||||
"url", help="See `dvc import-url -h` for full list of supported URLs."
|
||||
)
|
||||
lsurl_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
help="Recursively list files.",
|
||||
)
|
||||
lsurl_parser.set_defaults(func=CmdListUrl)
|
||||
+25
-46
@@ -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
|
||||
@@ -155,17 +144,16 @@ class CmdPlots(CmdBase):
|
||||
out=renderers_out,
|
||||
templates_dir=self.repo.plots.templates_dir,
|
||||
)
|
||||
if self.args.json:
|
||||
_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:
|
||||
ui.write_json(json.loads(renderer.get_filled_template()))
|
||||
return 0
|
||||
if self.args.json:
|
||||
renderers = _filter_unhandled_renderers(renderers)
|
||||
_show_json(renderers, self.args.split)
|
||||
return 0
|
||||
|
||||
_adjust_vega_renderers(renderers)
|
||||
|
||||
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
|
||||
|
||||
@@ -222,31 +210,22 @@ class CmdPlotsModify(CmdPlots):
|
||||
|
||||
|
||||
class CmdPlotsTemplates(CmdBase):
|
||||
TEMPLATES_CHOICES = [
|
||||
"simple",
|
||||
"linear",
|
||||
"confusion",
|
||||
"confusion_normalized",
|
||||
"scatter",
|
||||
"smooth",
|
||||
"bar_horizontal_sorted",
|
||||
"bar_horizontal",
|
||||
]
|
||||
|
||||
def run(self):
|
||||
from dvc_render.vega_templates import dump_templates
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc_render.vega_templates import TEMPLATES
|
||||
|
||||
try:
|
||||
out = (
|
||||
os.path.join(os.getcwd(), self.args.out)
|
||||
if self.args.out
|
||||
else self.repo.plots.templates_dir
|
||||
)
|
||||
target = self.args.template
|
||||
if target:
|
||||
for template in TEMPLATES:
|
||||
if target == template.DEFAULT_NAME:
|
||||
ui.write_json(template.DEFAULT_CONTENT)
|
||||
return 0
|
||||
raise InvalidArgumentError(f"Unexpected template: {target}.")
|
||||
|
||||
targets = [self.args.target] if self.args.target else None
|
||||
dump_templates(output=out, targets=targets)
|
||||
templates_path = os.path.relpath(out, os.getcwd())
|
||||
ui.write(f"Templates have been written into '{templates_path}'.")
|
||||
else:
|
||||
for template in TEMPLATES:
|
||||
ui.write(template.DEFAULT_NAME)
|
||||
|
||||
return 0
|
||||
except DvcException:
|
||||
@@ -356,8 +335,7 @@ def add_parser(subparsers, parent_parser):
|
||||
plots_modify_parser.set_defaults(func=CmdPlotsModify)
|
||||
|
||||
TEMPLATES_HELP = (
|
||||
"Write built-in plots templates to a directory "
|
||||
"(.dvc/plots by default)."
|
||||
"List built-in plots templates or show JSON specification for one."
|
||||
)
|
||||
plots_templates_parser = plots_subparsers.add_parser(
|
||||
"templates",
|
||||
@@ -367,13 +345,14 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_templates_parser.add_argument(
|
||||
"target",
|
||||
"template",
|
||||
default=None,
|
||||
nargs="?",
|
||||
choices=CmdPlotsTemplates.TEMPLATES_CHOICES,
|
||||
help="Template to write. Writes all templates by default.",
|
||||
help=(
|
||||
"Template for which to show JSON specification. "
|
||||
"List all template names by default."
|
||||
),
|
||||
)
|
||||
_add_output_argument(plots_templates_parser, typ="templates")
|
||||
plots_templates_parser.set_defaults(func=CmdPlotsTemplates)
|
||||
|
||||
|
||||
|
||||
+10
-1
@@ -55,7 +55,16 @@ def add_arguments(repro_parser):
|
||||
repro_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Stages to reproduce. 'dvc.yaml' by default.",
|
||||
help=(
|
||||
"""\
|
||||
Stages to reproduce. 'dvc.yaml' by default.
|
||||
The targets can be path to a dvc.yaml file or `.dvc` file,
|
||||
or a stage name from dvc.yaml file from
|
||||
current working directory. To run a stage from dvc.yaml
|
||||
from other directories, the target must be a path followed by colon `:`
|
||||
and then the stage name name.
|
||||
"""
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
repro_parser.add_argument(
|
||||
"-f",
|
||||
|
||||
+1
-21
@@ -76,7 +76,7 @@ class CmdStageList(CmdBase):
|
||||
# removing duplicates while maintaining order
|
||||
collected = chain.from_iterable(
|
||||
self.repo.stage.collect(
|
||||
target=target, recursive=self.args.recursive, accept_group=True
|
||||
target=target, recursive=self.args.recursive
|
||||
)
|
||||
for target in self.args.targets
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
@@ -18,6 +18,7 @@ class CmdUpdate(CmdBase):
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
to_remote=self.args.to_remote,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
@@ -55,6 +56,13 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Update all stages in the specified directory.",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Update .dvc file git revision/hash value(s)"
|
||||
" but do not download the file(s).",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--to-remote",
|
||||
action="store_true",
|
||||
|
||||
+4
-1
@@ -176,7 +176,10 @@ class Config(dict):
|
||||
|
||||
if fs.exists(filename):
|
||||
with fs.open(filename) as fobj:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
try:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
else:
|
||||
conf_obj = ConfigObj()
|
||||
return _parse_named(_lower_keys(conf_obj.dict()))
|
||||
|
||||
@@ -78,7 +78,9 @@ REMOTE_COMMON = {
|
||||
"url": str,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
"jobs": All(Coerce(int), Range(1)),
|
||||
Optional("worktree"): Bool,
|
||||
Optional("no_traverse"): Bool, # obsoleted
|
||||
Optional("version_aware"): Bool,
|
||||
}
|
||||
LOCAL_COMMON = {
|
||||
"type": supported_cache_type,
|
||||
@@ -168,6 +170,7 @@ SCHEMA = {
|
||||
"gs": {
|
||||
"projectname": str,
|
||||
"credentialpath": str,
|
||||
"endpointurl": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
@@ -177,6 +180,8 @@ SCHEMA = {
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
"passphrase": str,
|
||||
"ask_passphrase": Bool,
|
||||
"keyfile": str,
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
@@ -288,4 +293,9 @@ SCHEMA = {
|
||||
"bool": All(Lower, Choices("store_true", "boolean_optional")),
|
||||
"list": All(Lower, Choices("nargs", "append")),
|
||||
},
|
||||
"hydra": {
|
||||
Optional("enabled", default=False): Bool,
|
||||
"config_dir": str,
|
||||
"config_name": str,
|
||||
},
|
||||
}
|
||||
|
||||
+45
-22
@@ -3,16 +3,38 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
from dvc_data.db import get_index
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc_data.hashfile.db import get_index
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.status import CompareStatusResult
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, path, fs, **config):
|
||||
self.path = path
|
||||
self.fs = fs
|
||||
|
||||
self.worktree = config.pop("worktree", False)
|
||||
self.config = config
|
||||
|
||||
@cached_property
|
||||
def odb(self):
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
path = self.path
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(path, ".dvc", "cache")
|
||||
|
||||
return get_odb(self.fs, path, **self.config)
|
||||
|
||||
|
||||
class DataCloud:
|
||||
"""Class that manages dvc remotes.
|
||||
|
||||
@@ -27,18 +49,23 @@ class DataCloud:
|
||||
def __init__(self, repo):
|
||||
self.repo = repo
|
||||
|
||||
def get_remote_odb(
|
||||
def get_remote(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "HashFileDB":
|
||||
) -> "Remote":
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
if not name:
|
||||
name = self.repo.config["core"].get("remote")
|
||||
|
||||
if name:
|
||||
return self._init_odb(name)
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
fs = cls(**config)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return Remote(fs_path, fs, **config)
|
||||
|
||||
if bool(self.repo.config["remote"]):
|
||||
error_msg = (
|
||||
@@ -55,13 +82,13 @@ class DataCloud:
|
||||
|
||||
raise NoRemoteError(error_msg)
|
||||
|
||||
def _init_odb(self, name):
|
||||
from dvc.fs import get_cloud_fs
|
||||
from dvc_data.db import get_odb
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return get_odb(cls(**config), fs_path, **config)
|
||||
def get_remote_odb(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "HashFileDB":
|
||||
remote = self.get_remote(name=name, command=command)
|
||||
return remote.odb
|
||||
|
||||
def _log_missing(self, status: "CompareStatusResult"):
|
||||
if status.missing:
|
||||
@@ -80,14 +107,10 @@ class DataCloud:
|
||||
dest_odb: "HashFileDB",
|
||||
objs: Iterable["HashInfo"],
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.exceptions import FileTransferError
|
||||
from dvc_data.transfer import TransferError, transfer
|
||||
) -> "TransferResult":
|
||||
from dvc_data.hashfile.transfer import transfer
|
||||
|
||||
try:
|
||||
return transfer(src_odb, dest_odb, objs, **kwargs)
|
||||
except TransferError as exc:
|
||||
raise FileTransferError(exc.fails) from exc
|
||||
return transfer(src_odb, dest_odb, objs, **kwargs)
|
||||
|
||||
def push(
|
||||
self,
|
||||
@@ -95,7 +118,7 @@ class DataCloud:
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
odb: Optional["HashFileDB"] = None,
|
||||
):
|
||||
) -> "TransferResult":
|
||||
"""Push data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
@@ -122,7 +145,7 @@ class DataCloud:
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
odb: Optional["HashFileDB"] = None,
|
||||
):
|
||||
) -> "TransferResult":
|
||||
"""Pull data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
@@ -164,7 +187,7 @@ class DataCloud:
|
||||
log_missing: log warning messages if file doesn't exist
|
||||
neither in cache, neither in cloud.
|
||||
"""
|
||||
from dvc_data.status import compare_status
|
||||
from dvc_data.hashfile.status import compare_status
|
||||
|
||||
if not odb:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
|
||||
@@ -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):
|
||||
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,21 +28,30 @@ def _get(stage, p, info):
|
||||
params = info.pop(ParamsDependency.PARAM_PARAMS)
|
||||
return ParamsDependency(stage, p, params)
|
||||
|
||||
return Dependency(stage, p, info)
|
||||
return Dependency(stage, p, info, **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
|
||||
|
||||
|
||||
def loads_from(stage, s_list, erepo=None):
|
||||
def loads_from(stage, s_list, erepo=None, fs_config=None):
|
||||
assert isinstance(s_list, list)
|
||||
info = {RepoDependency.PARAM_REPO: erepo} if erepo else {}
|
||||
return [_get(stage, s, info.copy()) for s in s_list]
|
||||
return [
|
||||
_get(
|
||||
stage,
|
||||
s,
|
||||
info.copy(),
|
||||
fs_config=fs_config,
|
||||
)
|
||||
for s in s_list
|
||||
]
|
||||
|
||||
|
||||
def _merge_params(s_list):
|
||||
|
||||
+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)
|
||||
|
||||
+8
-10
@@ -8,7 +8,7 @@ import dpath.util
|
||||
from voluptuous import Any
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils.serialize import LOADERS, ParseError
|
||||
from dvc.utils.serialize import ParseError, load_path
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
|
||||
from .base import Dependency
|
||||
@@ -39,14 +39,15 @@ class ParamsDependency(Dependency):
|
||||
|
||||
def __init__(self, stage, path, params=None, repo=None):
|
||||
self.params = list(params) if params else []
|
||||
info = (
|
||||
{self.PARAM_PARAMS: params} if isinstance(params, dict) else None
|
||||
)
|
||||
hash_info = HashInfo()
|
||||
if isinstance(params, dict):
|
||||
hash_info = HashInfo(self.PARAM_PARAMS, params)
|
||||
repo = repo or stage.repo
|
||||
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
|
||||
super().__init__(stage, path, info=info, repo=repo)
|
||||
super().__init__(stage, path, repo=repo)
|
||||
self.hash_info = hash_info
|
||||
|
||||
def dumpd(self):
|
||||
def dumpd(self, **kwargs):
|
||||
ret = super().dumpd()
|
||||
if not self.hash_info:
|
||||
ret[self.PARAM_PARAMS] = self.params or {}
|
||||
@@ -139,12 +140,9 @@ class ParamsDependency(Dependency):
|
||||
)
|
||||
|
||||
def read_file(self):
|
||||
_, ext = os.path.splitext(self.fs_path)
|
||||
loader = LOADERS[ext]
|
||||
|
||||
self.validate_filepath()
|
||||
try:
|
||||
return loader(self.fs_path, fs=self.repo.fs)
|
||||
return load_path(self.fs_path, self.repo.fs)
|
||||
except ParseError as exc:
|
||||
raise BadParamFileError(
|
||||
f"Unable to read parameters from '{self}'"
|
||||
|
||||
+27
-13
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
|
||||
from voluptuous import Required
|
||||
|
||||
from dvc.prompt import confirm
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
@@ -32,6 +33,7 @@ class RepoDependency(Dependency):
|
||||
def __init__(self, def_repo, stage, *args, **kwargs):
|
||||
self.def_repo = def_repo
|
||||
self._objs: Dict[str, "HashFile"] = {}
|
||||
self._meta: Dict[str, Meta] = {}
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
def _parse_path(self, fs, fs_path):
|
||||
@@ -59,11 +61,11 @@ class RepoDependency(Dependency):
|
||||
def save(self):
|
||||
pass
|
||||
|
||||
def dumpd(self):
|
||||
def dumpd(self, **kwargs):
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc_data.checkout import checkout
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
||||
@@ -94,17 +96,17 @@ class RepoDependency(Dependency):
|
||||
def get_used_objs(
|
||||
self, **kwargs
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
|
||||
used, _ = self._get_used_and_obj(**kwargs)
|
||||
used, _, _ = self._get_used_and_obj(**kwargs)
|
||||
return used
|
||||
|
||||
def _get_used_and_obj(
|
||||
self, obj_only=False, **kwargs
|
||||
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "HashFile"]:
|
||||
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], Meta, "HashFile"]:
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputOrStageError, PathMissingError
|
||||
from dvc.utils import as_posix
|
||||
from dvc_data.build import build
|
||||
from dvc_data.objects.tree import Tree, TreeError
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.tree import Tree, TreeError
|
||||
|
||||
local_odb = self.repo.odb.local
|
||||
locked = kwargs.pop("locked", True)
|
||||
@@ -131,7 +133,7 @@ class RepoDependency(Dependency):
|
||||
pass
|
||||
|
||||
try:
|
||||
object_store, _, obj = build(
|
||||
object_store, meta, obj = build(
|
||||
local_odb,
|
||||
as_posix(self.def_path),
|
||||
repo.dvcfs,
|
||||
@@ -145,16 +147,18 @@ class RepoDependency(Dependency):
|
||||
object_store.read_only = True
|
||||
|
||||
self._objs[rev] = obj
|
||||
self._meta[rev] = meta
|
||||
|
||||
used_obj_ids[object_store].add(obj.hash_info)
|
||||
if isinstance(obj, Tree):
|
||||
used_obj_ids[object_store].update(oid for _, _, oid in obj)
|
||||
return used_obj_ids, obj
|
||||
return used_obj_ids, meta, obj
|
||||
|
||||
def _check_circular_import(self, odb, obj_ids):
|
||||
from dvc.exceptions import CircularImportError
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc_data.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.objects.tree import Tree
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
if not isinstance(odb, ReferenceHashFileDB):
|
||||
return
|
||||
@@ -169,7 +173,7 @@ class RepoDependency(Dependency):
|
||||
|
||||
checked_urls = set()
|
||||
for obj in iter_objs():
|
||||
if not isinstance(obj.fs, DvcFileSystem):
|
||||
if not isinstance(obj.fs, DVCFileSystem):
|
||||
continue
|
||||
if (
|
||||
obj.fs.repo_url in checked_urls
|
||||
@@ -190,11 +194,21 @@ class RepoDependency(Dependency):
|
||||
rev = self._get_rev(locked=locked)
|
||||
if rev in self._objs:
|
||||
return self._objs[rev]
|
||||
_, obj = self._get_used_and_obj(
|
||||
_, _, obj = self._get_used_and_obj(
|
||||
obj_only=True, filter_info=filter_info, **kwargs
|
||||
)
|
||||
return obj
|
||||
|
||||
def get_meta(self, filter_info=None, **kwargs):
|
||||
locked = kwargs.get("locked", True)
|
||||
rev = self._get_rev(locked=locked)
|
||||
if rev in self._meta:
|
||||
return self._meta[rev]
|
||||
_, meta, _ = self._get_used_and_obj(
|
||||
obj_only=True, filter_info=filter_info, **kwargs
|
||||
)
|
||||
return meta
|
||||
|
||||
def _make_repo(self, locked=True, **kwargs):
|
||||
from dvc.external_repo import external_repo
|
||||
|
||||
|
||||
+15
-12
@@ -172,7 +172,7 @@ class FileMixin:
|
||||
def dump(self, stage, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -198,18 +198,18 @@ class SingleStageFile(FileMixin):
|
||||
if self.verify:
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
logger.debug("Saving information to '%s'.", relpath(self.path))
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage))
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def remove_stage(self, stage): # pylint: disable=unused-argument
|
||||
self.remove()
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
assert isinstance(ancestor, SingleStageFile)
|
||||
assert isinstance(other, SingleStageFile)
|
||||
|
||||
stage = self.stage
|
||||
stage.merge(ancestor.stage, other.stage)
|
||||
stage.merge(ancestor.stage, other.stage, allowed=allowed)
|
||||
self.dump(stage)
|
||||
|
||||
|
||||
@@ -237,15 +237,17 @@ class PipelineFile(FileMixin):
|
||||
self._dump_pipeline_file(stage)
|
||||
|
||||
if update_lock:
|
||||
self._dump_lockfile(stage)
|
||||
self._dump_lockfile(stage, **kwargs)
|
||||
|
||||
def _dump_lockfile(self, stage):
|
||||
self._lockfile.dump(stage)
|
||||
def _dump_lockfile(self, stage, **kwargs):
|
||||
self._lockfile.dump(stage, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _check_if_parametrized(stage):
|
||||
def _check_if_parametrized(stage, action: str = "dump") -> None:
|
||||
if stage.raw_data.parametrized:
|
||||
raise ParametrizedDumpError(f"cannot dump a parametrized {stage}")
|
||||
raise ParametrizedDumpError(
|
||||
f"cannot {action} a parametrized {stage}"
|
||||
)
|
||||
|
||||
def _dump_pipeline_file(self, stage):
|
||||
self._check_if_parametrized(stage)
|
||||
@@ -291,6 +293,7 @@ class PipelineFile(FileMixin):
|
||||
self._lockfile.remove()
|
||||
|
||||
def remove_stage(self, stage):
|
||||
self._check_if_parametrized(stage, "remove")
|
||||
self._lockfile.remove_stage(stage)
|
||||
if not self.exists():
|
||||
return
|
||||
@@ -307,7 +310,7 @@ class PipelineFile(FileMixin):
|
||||
else:
|
||||
super().remove()
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -363,7 +366,7 @@ class Lockfile(FileMixin):
|
||||
return {SCHEMA_KWD: version}
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
stage_data = serialize.to_lockfile(stage)
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
version = LOCKFILE_VERSION.from_dict(data)
|
||||
@@ -408,7 +411,7 @@ class Lockfile(FileMixin):
|
||||
else:
|
||||
self.remove()
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@ 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_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
|
||||
+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):
|
||||
|
||||
|
||||
+51
-32
@@ -1,26 +1,21 @@
|
||||
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
|
||||
FS_MAP,
|
||||
AzureFileSystem,
|
||||
GDriveFileSystem,
|
||||
GSFileSystem,
|
||||
HDFSFileSystem,
|
||||
HTTPFileSystem,
|
||||
HTTPSFileSystem,
|
||||
LocalFileSystem,
|
||||
MemoryFileSystem,
|
||||
OSSFileSystem,
|
||||
S3FileSystem,
|
||||
Schemes,
|
||||
SSHFileSystem,
|
||||
WebDAVFileSystem,
|
||||
WebDAVSFileSystem,
|
||||
WebHDFSFileSystem,
|
||||
generic,
|
||||
get_fs_cls,
|
||||
known_implementations,
|
||||
localfs,
|
||||
registry,
|
||||
system,
|
||||
)
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401
|
||||
@@ -29,21 +24,47 @@ from dvc_objects.fs.errors import ( # noqa: F401
|
||||
ConfigError,
|
||||
RemoteMissingDepsError,
|
||||
)
|
||||
from dvc_objects.fs.implementations.azure import AzureAuthError # noqa: F401
|
||||
from dvc_objects.fs.implementations.local import localfs # noqa: F401
|
||||
from dvc_objects.fs.implementations.ssh import ( # noqa: F401
|
||||
DEFAULT_PORT as DEFAULT_SSH_PORT,
|
||||
)
|
||||
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",
|
||||
"err": "dvc is supported, but requires 'dvc' to be installed",
|
||||
},
|
||||
"git": {
|
||||
"class": "dvc.fs.git.GitFileSystem",
|
||||
"err": "git is supported, but requires 'dvc' to be installed",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# 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:
|
||||
@@ -54,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:
|
||||
@@ -83,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:
|
||||
@@ -110,11 +132,8 @@ def get_cloud_fs(repo, **kwargs):
|
||||
|
||||
cls = get_fs_cls(remote_conf)
|
||||
|
||||
if cls == GDriveFileSystem and repo:
|
||||
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
|
||||
|
||||
url = remote_conf.pop("url")
|
||||
if issubclass(cls, WebDAVFileSystem):
|
||||
if cls.protocol in ["webdav", "webdavs"]:
|
||||
# For WebDAVFileSystem, provided url is the base path itself, so it
|
||||
# should be treated as being a root path.
|
||||
fs_path = cls.root_marker
|
||||
|
||||
+17
-5
@@ -1,6 +1,6 @@
|
||||
# pylint: disable=unused-import
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
@@ -24,6 +24,7 @@ class RichCallback(Callback):
|
||||
bytes: bool = False, # pylint: disable=redefined-builtin
|
||||
unit: str = None,
|
||||
disable: bool = False,
|
||||
transient: bool = True,
|
||||
) -> None:
|
||||
self._progress = progress
|
||||
self.disable = disable
|
||||
@@ -35,6 +36,7 @@ class RichCallback(Callback):
|
||||
"visible": False,
|
||||
"progress_type": None if bytes else "summary",
|
||||
}
|
||||
self._transient = transient
|
||||
self._stack = ExitStack()
|
||||
super().__init__(size=size, value=value)
|
||||
|
||||
@@ -47,7 +49,7 @@ class RichCallback(Callback):
|
||||
return self._progress
|
||||
|
||||
progress = RichTransferProgress(
|
||||
transient=True,
|
||||
transient=self._transient,
|
||||
disable=self.disable,
|
||||
console=ui.error_console,
|
||||
)
|
||||
@@ -61,7 +63,8 @@ class RichCallback(Callback):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
self.progress.clear_task(self.task)
|
||||
if self._transient:
|
||||
self.progress.clear_task(self.task)
|
||||
self._stack.close()
|
||||
|
||||
def call(self, hook_name=None, **kwargs):
|
||||
@@ -72,8 +75,17 @@ class RichCallback(Callback):
|
||||
visible=not self.disable,
|
||||
)
|
||||
|
||||
def branch(self, path_1, path_2, kwargs, child: Optional[Callback] = None):
|
||||
def branch(
|
||||
self,
|
||||
path_1: Union[str, BinaryIO],
|
||||
path_2: str,
|
||||
kwargs: Dict[str, Any],
|
||||
child: "Callback" = None,
|
||||
):
|
||||
child = child or RichCallback(
|
||||
progress=self.progress, desc=path_1, bytes=True
|
||||
progress=self.progress,
|
||||
desc=path_1 if isinstance(path_1, str) else path_2,
|
||||
bytes=True,
|
||||
transient=self._transient,
|
||||
)
|
||||
return super().branch(path_1, path_2, kwargs, child=child)
|
||||
|
||||
+144
-219
@@ -1,17 +1,16 @@
|
||||
import errno
|
||||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import threading
|
||||
from contextlib import suppress
|
||||
from itertools import takewhile
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Tuple, Type, Union
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple, Type, Union
|
||||
|
||||
from fsspec.spec import AbstractFileSystem
|
||||
from funcy import cached_property, wrap_prop, wrap_with
|
||||
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
from .data import DataFileSystem
|
||||
@@ -22,29 +21,19 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RepoFactory = Union[Callable[[str], "Repo"], Type["Repo"]]
|
||||
Key = Tuple[str, ...]
|
||||
|
||||
|
||||
def as_posix(path: str) -> str:
|
||||
return path.replace(ntpath.sep, posixpath.sep)
|
||||
|
||||
|
||||
def _wrap_walk(dvc_fs, *args, **kwargs):
|
||||
for root, dnames, fnames in dvc_fs.walk(*args, **kwargs):
|
||||
yield dvc_fs.path.join(dvc_fs.repo.root_dir, root), dnames, fnames
|
||||
# NOT the same as dvc.dvcfile.is_dvc_file()!
|
||||
def _is_dvc_file(fname):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.ignore import DvcIgnore
|
||||
|
||||
|
||||
def _ls(fs, path):
|
||||
dnames = []
|
||||
fnames = []
|
||||
|
||||
for entry in fs.ls(path, detail=True):
|
||||
name = fs.path.name(entry["name"])
|
||||
if entry["type"] == "directory":
|
||||
dnames.append(name)
|
||||
else:
|
||||
fnames.append(name)
|
||||
|
||||
return dnames, fnames
|
||||
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
|
||||
|
||||
def _merge_info(repo, fs_info, dvc_info):
|
||||
@@ -70,40 +59,68 @@ def _merge_info(repo, fs_info, dvc_info):
|
||||
return ret
|
||||
|
||||
|
||||
class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
"""DVC + git-tracked files fs.
|
||||
def _get_dvc_path(dvc_fs, subkey):
|
||||
return dvc_fs.path.join(*subkey) if subkey else ""
|
||||
|
||||
Args:
|
||||
repo: DVC or git repo.
|
||||
subrepos: traverse to subrepos (by default, it ignores subrepos)
|
||||
repo_factory: A function to initialize subrepo with, default is Repo.
|
||||
kwargs: Additional keyword arguments passed to the `DataFileSystem()`.
|
||||
"""
|
||||
|
||||
class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
cachable = False
|
||||
root_marker = "/"
|
||||
|
||||
PARAM_REPO_URL = "repo_url"
|
||||
PARAM_REPO_ROOT = "repo_root"
|
||||
PARAM_REV = "rev"
|
||||
PARAM_CACHE_DIR = "cache_dir"
|
||||
PARAM_CACHE_TYPES = "cache_types"
|
||||
PARAM_SUBREPOS = "subrepos"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
repo: Optional["Repo"] = None,
|
||||
subrepos=False,
|
||||
repo_factory: RepoFactory = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
subrepos: bool = False,
|
||||
repo_factory: Optional[RepoFactory] = None,
|
||||
**repo_kwargs: Any,
|
||||
) -> None:
|
||||
"""DVC + git-tracked files fs.
|
||||
|
||||
Args:
|
||||
path (str, optional): URL or path to a DVC/Git repository.
|
||||
Defaults to a DVC repository in the current working directory.
|
||||
Both HTTP and SSH protocols are supported for remote Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
rev (str, optional): Any Git revision such as a branch or tag name,
|
||||
a commit hash or a dvc experiment name.
|
||||
Defaults to the default branch in case of remote repositories.
|
||||
In case of a local repository, if rev is unspecified, it will
|
||||
default to the working directory.
|
||||
If the repo is not a Git repo, this option is ignored.
|
||||
repo (:obj:`Repo`, optional): `Repo` instance.
|
||||
subrepos (bool): traverse to subrepos.
|
||||
By default, it ignores subrepos.
|
||||
repo_factory (callable): A function to initialize subrepo with.
|
||||
The default is `Repo`.
|
||||
|
||||
Examples:
|
||||
- Opening a filesystem from repo in current working directory
|
||||
|
||||
>>> fs = DVCFileSystem()
|
||||
|
||||
- Opening a filesystem from local repository
|
||||
|
||||
>>> fs = DVCFileSystem("path/to/local/repository")
|
||||
|
||||
- Opening a remote repository
|
||||
|
||||
>>> fs = DVCFileSystem(
|
||||
... "https://github.com/iterative/example-get-started",
|
||||
... rev="main",
|
||||
... )
|
||||
"""
|
||||
from pygtrie import Trie
|
||||
|
||||
super().__init__()
|
||||
if repo is None:
|
||||
repo, repo_factory = self._repo_from_fs_config(
|
||||
subrepos=subrepos, **kwargs
|
||||
repo = self._make_repo(
|
||||
url=url, rev=rev, subrepos=subrepos, **repo_kwargs
|
||||
)
|
||||
assert repo
|
||||
# pylint: disable=protected-access
|
||||
repo_factory = repo._fs_conf["repo_factory"]
|
||||
|
||||
if not repo_factory:
|
||||
from dvc.repo import Repo
|
||||
@@ -137,78 +154,35 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
|
||||
|
||||
def _get_key(self, path):
|
||||
def _get_key(self, path) -> Key:
|
||||
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
|
||||
if parts == (".",):
|
||||
parts = ()
|
||||
return parts
|
||||
|
||||
def _get_key_from_relative(self, path) -> Key:
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
parts = parts[1:]
|
||||
return parts
|
||||
|
||||
def _from_key(self, parts: Key) -> str:
|
||||
return self.repo.fs.path.join(self.repo.root_dir, *parts)
|
||||
|
||||
@property
|
||||
def repo_url(self):
|
||||
if self.repo is None:
|
||||
return None
|
||||
return self.repo.url
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return {
|
||||
self.PARAM_REPO_URL: self.repo_url,
|
||||
self.PARAM_REPO_ROOT: self.repo.root_dir,
|
||||
self.PARAM_REV: getattr(self.repo.fs, "rev", None),
|
||||
self.PARAM_CACHE_DIR: os.path.abspath(self.repo.odb.local.path),
|
||||
self.PARAM_CACHE_TYPES: self.repo.odb.local.cache_types,
|
||||
self.PARAM_SUBREPOS: self._traverse_subrepos,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _repo_from_fs_config(
|
||||
cls, **config
|
||||
) -> Tuple["Repo", Optional["RepoFactory"]]:
|
||||
from dvc.external_repo import erepo_factory, external_repo
|
||||
def _make_repo(cls, **kwargs) -> "Repo":
|
||||
from dvc.repo import Repo
|
||||
|
||||
url = config.get(cls.PARAM_REPO_URL)
|
||||
root = config.get(cls.PARAM_REPO_ROOT)
|
||||
assert url or root
|
||||
with Repo.open(uninitialized=True, **kwargs) as repo:
|
||||
return repo
|
||||
|
||||
def _open(*args, **kwargs):
|
||||
# NOTE: if original repo was an erepo (and has a URL),
|
||||
# we cannot use Repo.open() since it will skip erepo
|
||||
# cache/remote setup for local URLs
|
||||
if url is None:
|
||||
return Repo.open(*args, **kwargs)
|
||||
return external_repo(*args, **kwargs)
|
||||
|
||||
cache_dir = config.get(cls.PARAM_CACHE_DIR)
|
||||
cache_config = (
|
||||
{}
|
||||
if not cache_dir
|
||||
else {
|
||||
"cache": {
|
||||
"dir": cache_dir,
|
||||
"type": config.get(cls.PARAM_CACHE_TYPES),
|
||||
}
|
||||
}
|
||||
)
|
||||
repo_kwargs: dict = {
|
||||
"rev": config.get(cls.PARAM_REV),
|
||||
"subrepos": config.get(cls.PARAM_SUBREPOS, False),
|
||||
"uninitialized": True,
|
||||
}
|
||||
factory: Optional["RepoFactory"] = None
|
||||
if url is None:
|
||||
repo_kwargs["config"] = cache_config
|
||||
else:
|
||||
repo_kwargs["cache_dir"] = cache_dir
|
||||
factory = erepo_factory(url, root, cache_config)
|
||||
|
||||
with _open(
|
||||
url if url else root,
|
||||
**repo_kwargs,
|
||||
) as repo:
|
||||
return repo, factory
|
||||
|
||||
def _get_repo(self, path: str) -> "Repo":
|
||||
def _get_repo(self, key: Key) -> "Repo":
|
||||
"""Returns repo that the path falls in, using prefix.
|
||||
|
||||
If the path is already tracked/collected, it just returns the repo.
|
||||
@@ -216,33 +190,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
Otherwise, it collects the repos that might be in the path's parents
|
||||
and then returns the appropriate one.
|
||||
"""
|
||||
if not self.repo.fs.path.isin_or_eq(path, self.repo.root_dir):
|
||||
# outside of repo
|
||||
return self.repo
|
||||
|
||||
key = self._get_key(path)
|
||||
repo = self._subrepos_trie.get(key)
|
||||
if repo:
|
||||
return repo
|
||||
|
||||
prefix_key, repo = self._subrepos_trie.longest_prefix(key)
|
||||
prefix = self.repo.fs.path.join(
|
||||
self.repo.root_dir,
|
||||
*prefix_key, # pylint: disable=not-an-iterable
|
||||
)
|
||||
|
||||
parents = (parent for parent in self.repo.fs.path.parents(path))
|
||||
dirs = [path] + list(takewhile(lambda p: p != prefix, parents))
|
||||
dirs.reverse()
|
||||
self._update(dirs, starting_repo=repo)
|
||||
dir_keys = (key[:i] for i in range(len(prefix_key) + 1, len(key) + 1))
|
||||
self._update(dir_keys, starting_repo=repo)
|
||||
return self._subrepos_trie.get(key) or self.repo
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _update(self, dirs, starting_repo):
|
||||
def _update(self, dir_keys, starting_repo):
|
||||
"""Checks for subrepo in directories and updates them."""
|
||||
repo = starting_repo
|
||||
for d in dirs:
|
||||
key = self._get_key(d)
|
||||
for key in dir_keys:
|
||||
d = self._from_key(key)
|
||||
if self._is_dvc_repo(d):
|
||||
repo = self.repo_factory(
|
||||
d,
|
||||
@@ -265,79 +227,68 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
repo_path = self.repo.fs.path.join(dir_path, Repo.DVC_DIR)
|
||||
return self.repo.fs.isdir(repo_path)
|
||||
|
||||
def _get_fs_pair(
|
||||
self, path
|
||||
) -> Tuple[
|
||||
Optional["Repo"],
|
||||
Optional[FileSystem],
|
||||
Optional[str],
|
||||
Optional[DataFileSystem],
|
||||
Optional[str],
|
||||
]:
|
||||
def _get_subrepo_info(
|
||||
self, key: Key
|
||||
) -> Tuple["Repo", Optional[DataFileSystem], Key]:
|
||||
"""
|
||||
Returns a pair of fss based on repo the path falls in, using prefix.
|
||||
Returns information about the subrepo the key is part of.
|
||||
"""
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
parts = parts[1:]
|
||||
|
||||
fs_path = self.repo.fs.path.join(self.repo.root_dir, *parts)
|
||||
repo = self._get_repo(fs_path)
|
||||
fs = repo.fs
|
||||
|
||||
repo_parts = fs.path.relparts(repo.root_dir, self.repo.root_dir)
|
||||
if repo_parts[0] == os.curdir:
|
||||
repo_parts = repo_parts[1:]
|
||||
|
||||
dvc_parts = parts[len(repo_parts) :]
|
||||
if dvc_parts and dvc_parts[0] == os.curdir:
|
||||
dvc_parts = dvc_parts[1:]
|
||||
|
||||
key = self._get_key(repo.root_dir)
|
||||
dvc_fs = self._datafss.get(key)
|
||||
if dvc_fs:
|
||||
dvc_path = dvc_fs.path.join(*dvc_parts) if dvc_parts else ""
|
||||
repo = self._get_repo(key)
|
||||
repo_key: Key
|
||||
if repo is self.repo:
|
||||
repo_key = ()
|
||||
subkey = key
|
||||
else:
|
||||
dvc_path = None
|
||||
repo_key = self._get_key(repo.root_dir)
|
||||
subkey = key[len(repo_key) :]
|
||||
|
||||
return repo, fs, fs_path, dvc_fs, dvc_path
|
||||
dvc_fs = self._datafss.get(repo_key)
|
||||
return repo, dvc_fs, subkey
|
||||
|
||||
def open(
|
||||
self, path, mode="r", encoding="utf-8", **kwargs
|
||||
def _open(
|
||||
self, path, mode="rb", **kwargs
|
||||
): # pylint: disable=arguments-renamed, arguments-differ
|
||||
if "b" in mode:
|
||||
encoding = None
|
||||
if mode != "rb":
|
||||
raise OSError(errno.EROFS, os.strerror(errno.EROFS))
|
||||
|
||||
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
key = self._get_key_from_relative(path)
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
return fs.open(fs_path, mode=mode, encoding=encoding)
|
||||
return self.repo.fs.open(fs_path, mode=mode)
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
return dvc_fs.open(dvc_path, mode=mode, encoding=encoding, **kwargs)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.open(dvc_path, mode=mode)
|
||||
|
||||
def isdvc(self, path, **kwargs):
|
||||
_, _, _, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
"""Is this entry dvc-tracked?"""
|
||||
key = self._get_key_from_relative(path)
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
|
||||
|
||||
def ls( # pylint: disable=arguments-differ
|
||||
self, path, detail=True, dvc_only=False, **kwargs
|
||||
):
|
||||
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
dvcignore = repo.dvcignore
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
names = set()
|
||||
if dvc_fs:
|
||||
with suppress(FileNotFoundError):
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
for entry in dvc_fs.ls(dvc_path, detail=False):
|
||||
names.add(dvc_fs.path.name(entry))
|
||||
|
||||
if not dvc_only and fs:
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
if not dvc_only:
|
||||
fs = self.repo.fs
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
for entry in dvcignore.ls(
|
||||
for entry in repo.dvcignore.ls(
|
||||
fs, fs_path, detail=False, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
names.add(fs.path.name(entry))
|
||||
@@ -345,26 +296,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
pass
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
|
||||
def _func(fname):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.ignore import DvcIgnore
|
||||
|
||||
if dvcfiles:
|
||||
return True
|
||||
|
||||
return not (
|
||||
is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
)
|
||||
|
||||
names = filter(_func, names)
|
||||
if not dvcfiles:
|
||||
names = (name for name in names if not _is_dvc_file(name))
|
||||
|
||||
infos = []
|
||||
paths = []
|
||||
for name in names:
|
||||
entry_path = self.path.join(path, name)
|
||||
entry_key = key + (name,)
|
||||
try:
|
||||
info = self.info(entry_path, ignore_subrepos=ignore_subrepos)
|
||||
info = self._info(
|
||||
entry_key,
|
||||
entry_path,
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
check_ignored=False,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
infos.append(info)
|
||||
@@ -375,48 +321,39 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
return infos
|
||||
|
||||
def get_file( # pylint: disable=arguments-differ
|
||||
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(rpath)
|
||||
|
||||
if fs:
|
||||
try:
|
||||
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
|
||||
return
|
||||
except FileNotFoundError:
|
||||
if not dvc_fs:
|
||||
raise
|
||||
dvc_fs.get_file(dvc_path, lpath, callback=callback, **kwargs)
|
||||
|
||||
def info(self, path, **kwargs):
|
||||
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
dvcignore = repo.dvcignore
|
||||
key = self._get_key_from_relative(path)
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info(self, key, path, ignore_subrepos=True, check_ignored=True):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_info = None
|
||||
if dvc_fs:
|
||||
try:
|
||||
dvc_info = dvc_fs.info(dvc_path)
|
||||
dvc_info = dvc_fs.fs.index.info(subkey)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_info["name"] = dvc_path
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
fs_info = None
|
||||
if fs:
|
||||
try:
|
||||
fs_info = fs.info(fs_path)
|
||||
if dvcignore.is_ignored(
|
||||
fs, fs_path, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_info = None
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
if not dvc_info:
|
||||
raise
|
||||
fs = self.repo.fs
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
fs_info = fs.info(fs_path)
|
||||
if check_ignored and repo.dvcignore.is_ignored(
|
||||
fs, fs_path, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_info = None
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
if not dvc_info:
|
||||
raise
|
||||
|
||||
# NOTE: if some parent in fs_path turns out to be a file, it means
|
||||
# that the whole repofs branch doesn't exist.
|
||||
if fs and not fs_info and dvc_info:
|
||||
if dvc_info and not fs_info:
|
||||
for parent in fs.path.parents(fs_path):
|
||||
try:
|
||||
if fs.info(parent)["type"] != "directory":
|
||||
@@ -432,16 +369,8 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
def checksum(self, path):
|
||||
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
try:
|
||||
return fs.checksum(fs_path)
|
||||
except FileNotFoundError:
|
||||
return dvc_fs.checksum(dvc_path)
|
||||
|
||||
|
||||
class DvcFileSystem(FileSystem):
|
||||
class DVCFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
@@ -451,7 +380,7 @@ class DvcFileSystem(FileSystem):
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self):
|
||||
return _DvcFileSystem(**self.fs_args)
|
||||
return _DVCFileSystem(**self.fs_args)
|
||||
|
||||
def isdvc(self, path, **kwargs):
|
||||
return self.fs.isdvc(path, **kwargs)
|
||||
@@ -468,10 +397,6 @@ class DvcFileSystem(FileSystem):
|
||||
def repo_url(self):
|
||||
return self.fs.repo_url
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.fs.config
|
||||
|
||||
def from_os_path(self, path):
|
||||
if os.path.isabs(path):
|
||||
path = os.path.relpath(path, self.repo.root_dir)
|
||||
|
||||
@@ -15,6 +15,7 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
|
||||
"""Proxies the repo file access methods to Git objects"""
|
||||
|
||||
protocol = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
+16
-3
@@ -300,12 +300,25 @@ class DvcIgnoreFilter:
|
||||
return [fs_dict[name] for name in chain(dirs, nondirs)]
|
||||
|
||||
def walk(self, fs: FileSystem, path: AnyFSPath, **kwargs):
|
||||
detail = kwargs.get("detail", False)
|
||||
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
|
||||
if fs.protocol == Schemes.LOCAL:
|
||||
for root, dirs, files in fs.walk(path, **kwargs):
|
||||
dirs[:], files[:] = self(
|
||||
root, dirs, files, ignore_subrepos=ignore_subrepos
|
||||
)
|
||||
if detail:
|
||||
all_dnames = set(dirs.keys())
|
||||
all_fnames = set(files.keys())
|
||||
dnames, fnames = self(
|
||||
root,
|
||||
all_dnames,
|
||||
all_fnames,
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
)
|
||||
list(map(dirs.pop, all_dnames - set(dnames)))
|
||||
list(map(files.pop, all_fnames - set(fnames)))
|
||||
else:
|
||||
dirs[:], files[:] = self(
|
||||
root, dirs, files, ignore_subrepos=ignore_subrepos
|
||||
)
|
||||
yield root, dirs, files
|
||||
else:
|
||||
yield from fs.walk(path, **kwargs)
|
||||
|
||||
+32
-3
@@ -8,12 +8,20 @@ import psutil
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.fs import FS_MAP, generic, get_fs_cls, get_fs_config
|
||||
from dvc.fs import Schemes, generic, get_fs_cls, get_fs_config, registry
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import SCMError
|
||||
from dvc.utils import error_link
|
||||
from dvc.utils.pkg import PKG
|
||||
|
||||
SUBPROJECTS = (
|
||||
"dvc_data",
|
||||
"dvc_objects",
|
||||
"dvc_render",
|
||||
"dvc_task",
|
||||
"dvclive",
|
||||
"scmrepo",
|
||||
)
|
||||
package = "" if PKG is None else f"({PKG})"
|
||||
|
||||
|
||||
@@ -23,6 +31,7 @@ def get_dvc_info():
|
||||
"---------------------------------",
|
||||
f"Platform: Python {platform.python_version()} on "
|
||||
f"{platform.platform()}",
|
||||
f"Subprojects:{_get_subprojects()}",
|
||||
f"Supports:{_get_supported_remotes()}",
|
||||
]
|
||||
|
||||
@@ -67,7 +76,7 @@ def _get_caches(cache):
|
||||
|
||||
def _get_remotes(config):
|
||||
schemes = (
|
||||
get_fs_cls(get_fs_config(None, config, name=remote)).protocol
|
||||
get_fs_cls(get_fs_config(config, name=remote)).protocol
|
||||
for remote in config["remote"]
|
||||
)
|
||||
|
||||
@@ -88,9 +97,29 @@ def _get_linktype_support_info(repo):
|
||||
return ", ".join(links)
|
||||
|
||||
|
||||
def _get_subprojects():
|
||||
subprojects = []
|
||||
for subproject in SUBPROJECTS:
|
||||
try:
|
||||
version = importlib_metadata.version(subproject)
|
||||
subprojects.append(f"{subproject} = {version}")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return "\n\t" + "\n\t".join(subprojects)
|
||||
|
||||
|
||||
def _get_supported_remotes():
|
||||
supported_remotes = []
|
||||
for scheme, fs_cls in FS_MAP.items():
|
||||
for scheme in registry:
|
||||
if scheme in [Schemes.LOCAL, Schemes.MEMORY, "dvc", "git"]:
|
||||
continue
|
||||
|
||||
try:
|
||||
fs_cls = registry[scheme]
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
if not fs_cls.get_missing_deps():
|
||||
dependencies = []
|
||||
for requirement in fs_cls.REQUIRES:
|
||||
|
||||
+11
@@ -70,6 +70,8 @@ class LockNoop(LockBase):
|
||||
self._lock = True
|
||||
|
||||
def unlock(self):
|
||||
if not self.is_locked:
|
||||
raise DvcException("Unlock called on an unlocked lock")
|
||||
self._lock = False
|
||||
|
||||
@property
|
||||
@@ -93,6 +95,7 @@ class Lock(LockBase):
|
||||
super().__init__(lockfile)
|
||||
self._friendly = friendly
|
||||
self._lock = None
|
||||
self._lock_failed = False
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
@@ -100,6 +103,7 @@ class Lock(LockBase):
|
||||
|
||||
def _do_lock(self):
|
||||
try:
|
||||
self._lock_failed = False
|
||||
with Tqdm(
|
||||
bar_format="{desc}",
|
||||
disable=not self._friendly,
|
||||
@@ -111,6 +115,7 @@ class Lock(LockBase):
|
||||
):
|
||||
self._lock = zc.lockfile.LockFile(self._lockfile)
|
||||
except zc.lockfile.LockError:
|
||||
self._lock_failed = True
|
||||
raise LockError(FAILED_TO_LOCK_MESSAGE)
|
||||
|
||||
def lock(self):
|
||||
@@ -120,6 +125,12 @@ class Lock(LockBase):
|
||||
lock_retry()
|
||||
|
||||
def unlock(self):
|
||||
if self._lock_failed:
|
||||
assert self._lock is None
|
||||
return
|
||||
|
||||
if not self.is_locked:
|
||||
raise DvcException("Unlock called on an unlocked lock")
|
||||
self._lock.close()
|
||||
self._lock = None
|
||||
|
||||
|
||||
+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()
|
||||
|
||||
@@ -3,7 +3,8 @@ from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import SSHFileSystem
|
||||
from dvc_ssh import SSHFileSystem
|
||||
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
@@ -25,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
|
||||
|
||||
@@ -3,8 +3,9 @@ from contextlib import contextmanager
|
||||
from functools import partial, partialmethod
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
from dvc_ssh import DEFAULT_PORT, SSHFileSystem
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.fs import DEFAULT_SSH_PORT, SSHFileSystem
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
from .base import BaseMachineBackend
|
||||
@@ -68,7 +69,7 @@ class TerraformBackend(BaseMachineBackend):
|
||||
resource = tpi.default_resource(name)
|
||||
return {
|
||||
"host": resource["instance_ip"],
|
||||
"port": DEFAULT_SSH_PORT,
|
||||
"port": DEFAULT_PORT,
|
||||
"username": "ubuntu",
|
||||
"fs_factory": partial(_sshfs, dict(resource)),
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.db import get_odb
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
|
||||
def _get_odb(repo, settings, fs=None):
|
||||
|
||||
+277
-124
@@ -1,10 +1,10 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set, Type
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import collecting, project
|
||||
from funcy import cached_property, collecting, project
|
||||
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
|
||||
|
||||
from dvc import prompt
|
||||
@@ -17,31 +17,31 @@ from dvc.exceptions import (
|
||||
MergeError,
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc_data import Tree
|
||||
from dvc_data import check as ocheck
|
||||
from dvc_data import load as oload
|
||||
from dvc_data.build import build
|
||||
from dvc_data.checkout import checkout
|
||||
from dvc_data.hashfile import Tree
|
||||
from dvc_data.hashfile import check as ocheck
|
||||
from dvc_data.hashfile import load as oload
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.istextfile import istextfile
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.transfer import transfer as otransfer
|
||||
from dvc_data.hashfile.transfer import transfer as otransfer
|
||||
from dvc_data.index import DataIndexEntry
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
from .fs import (
|
||||
HDFSFileSystem,
|
||||
LocalFileSystem,
|
||||
RemoteMissingDepsError,
|
||||
S3FileSystem,
|
||||
Schemes,
|
||||
get_cloud_fs,
|
||||
)
|
||||
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
|
||||
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
|
||||
from .fs.callbacks import DEFAULT_CALLBACK
|
||||
from .utils import relpath
|
||||
from .utils.fs import path_isin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_data.index import DataIndexKey
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
from .fs.callbacks import Callback
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -65,10 +65,12 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
|
||||
#
|
||||
# so when a few types of outputs share the same name, we only need
|
||||
# specify it once.
|
||||
HDFS_PARAM_CHECKSUM = "checksum"
|
||||
S3_PARAM_CHECKSUM = "etag"
|
||||
CHECKSUMS_SCHEMA = {
|
||||
LocalFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
HDFSFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3FileSystem.PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
|
||||
HDFS_PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3_PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
|
||||
}
|
||||
|
||||
|
||||
@@ -85,9 +87,10 @@ def loadd_from(stage, d_list):
|
||||
plot = d.pop(Output.PARAM_PLOT, False)
|
||||
persist = d.pop(Output.PARAM_PERSIST, False)
|
||||
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
|
||||
desc = d.pop(Output.PARAM_DESC, False)
|
||||
live = d.pop(Output.PARAM_LIVE, False)
|
||||
remote = d.pop(Output.PARAM_REMOTE, None)
|
||||
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
push = d.pop(Output.PARAM_PUSH, True)
|
||||
ret.append(
|
||||
_get(
|
||||
stage,
|
||||
@@ -98,9 +101,10 @@ def loadd_from(stage, d_list):
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
desc=desc,
|
||||
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,8 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
Output.PARAM_REMOTE,
|
||||
Output.PARAM_PUSH,
|
||||
*ANNOTATION_FIELDS,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -201,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,
|
||||
)
|
||||
|
||||
@@ -245,6 +242,7 @@ class Output:
|
||||
PARAM_PATH = "path"
|
||||
PARAM_CACHE = "cache"
|
||||
PARAM_CHECKPOINT = "checkpoint"
|
||||
PARAM_FILES = "files"
|
||||
PARAM_METRIC = "metric"
|
||||
PARAM_METRIC_TYPE = "type"
|
||||
PARAM_METRIC_XPATH = "xpath"
|
||||
@@ -257,11 +255,8 @@ class Output:
|
||||
PARAM_PLOT_TITLE = "title"
|
||||
PARAM_PLOT_HEADER = "header"
|
||||
PARAM_PERSIST = "persist"
|
||||
PARAM_DESC = "desc"
|
||||
PARAM_LIVE = "live"
|
||||
PARAM_LIVE_SUMMARY = "summary"
|
||||
PARAM_LIVE_HTML = "html"
|
||||
PARAM_REMOTE = "remote"
|
||||
PARAM_PUSH = "push"
|
||||
|
||||
METRIC_SCHEMA = Any(
|
||||
None,
|
||||
@@ -287,13 +282,31 @@ class Output:
|
||||
plot=False,
|
||||
persist=False,
|
||||
checkpoint=False,
|
||||
live=False,
|
||||
desc=None,
|
||||
type=None, # pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
meta=None,
|
||||
remote=None,
|
||||
repo=None,
|
||||
fs_config=None,
|
||||
files: List[Dict[str, Any]] = None,
|
||||
push: bool = True,
|
||||
):
|
||||
self.annot = Annotation(
|
||||
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
||||
)
|
||||
self.repo = stage.repo if not repo and stage else repo
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(self.repo, url=path)
|
||||
meta = Meta.from_dict(info or {})
|
||||
# NOTE: when version_aware is not passed into get_cloud_fs, it will be
|
||||
# set based on whether or not path is versioned
|
||||
fs_kwargs = {"version_aware": True} if meta.version_id else {}
|
||||
|
||||
if fs_config is not None:
|
||||
fs_kwargs.update(**fs_config)
|
||||
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(
|
||||
self.repo, url=path, **fs_kwargs
|
||||
)
|
||||
self.fs = fs_cls(**fs_config)
|
||||
|
||||
if (
|
||||
@@ -325,21 +338,42 @@ class Output:
|
||||
# By resolved path, which contains actual location,
|
||||
# should be absolute and don't contain remote:// refs.
|
||||
self.stage = stage
|
||||
self.meta = Meta.from_dict(info)
|
||||
self.hash_info = HashInfo.from_dict(info)
|
||||
self.meta = meta
|
||||
self.files = files
|
||||
self.use_cache = False if self.IS_DEPENDENCY else cache
|
||||
self.metric = False if self.IS_DEPENDENCY else metric
|
||||
self.plot = False if self.IS_DEPENDENCY else plot
|
||||
self.persist = persist
|
||||
self.checkpoint = checkpoint
|
||||
self.live = live
|
||||
self.desc = desc
|
||||
self.can_push = push
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj = None
|
||||
self.obj: Optional["HashFile"] = None
|
||||
|
||||
self.remote = remote
|
||||
|
||||
if self.fs.version_aware:
|
||||
_, version_id = self.fs.path.coalesce_version(
|
||||
self.def_path, self.meta.version_id
|
||||
)
|
||||
self.meta.version_id = version_id
|
||||
|
||||
if self.is_in_repo:
|
||||
self.hash_name = "md5"
|
||||
else:
|
||||
self.hash_name = self.fs.PARAM_CHECKSUM
|
||||
|
||||
self.hash_info = HashInfo(
|
||||
name=self.hash_name,
|
||||
value=getattr(self.meta, self.hash_name, None),
|
||||
)
|
||||
if self.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)
|
||||
if (
|
||||
@@ -380,6 +414,12 @@ class Output:
|
||||
|
||||
return self.fs.path.relpath(self.fs_path, self.repo.root_dir)
|
||||
|
||||
def clear(self):
|
||||
self.hash_info = HashInfo.from_dict({})
|
||||
self.meta = Meta.from_dict({})
|
||||
self.obj = None
|
||||
self.files = None
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
return self.fs.protocol
|
||||
@@ -419,21 +459,27 @@ class Output:
|
||||
)
|
||||
|
||||
def get_hash(self):
|
||||
_, hash_info = self._get_hash_meta()
|
||||
return hash_info
|
||||
|
||||
def _get_hash_meta(self):
|
||||
if self.use_cache:
|
||||
odb = self.odb
|
||||
name = self.odb.fs.PARAM_CHECKSUM
|
||||
else:
|
||||
odb = self.repo.odb.local
|
||||
name = self.fs.PARAM_CHECKSUM
|
||||
_, _, obj = build(
|
||||
_, meta, obj = build(
|
||||
odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
name,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=not self.use_cache,
|
||||
)
|
||||
return obj.hash_info
|
||||
return meta, obj.hash_info
|
||||
|
||||
def get_meta(self) -> Meta:
|
||||
meta, _ = self._get_hash_meta()
|
||||
return meta
|
||||
|
||||
@property
|
||||
def is_dir_checksum(self):
|
||||
@@ -452,6 +498,47 @@ class Output:
|
||||
|
||||
return self.fs.exists(self.fs_path)
|
||||
|
||||
@cached_property
|
||||
def index_key(self) -> Tuple[str, "DataIndexKey"]:
|
||||
if self.is_in_repo:
|
||||
workspace = "repo"
|
||||
key = self.repo.fs.path.relparts(self.fs_path, self.repo.root_dir)
|
||||
else:
|
||||
workspace = self.fs.protocol
|
||||
no_drive = self.fs.path.flavour.splitdrive(self.fs_path)[1]
|
||||
key = self.fs.path.parts(no_drive)[1:]
|
||||
return workspace, key
|
||||
|
||||
def get_entry(self) -> "DataIndexEntry":
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
try:
|
||||
remote = self.repo.cloud.get_remote_odb(self.remote)
|
||||
except NoRemoteError:
|
||||
remote = None
|
||||
|
||||
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()
|
||||
|
||||
@@ -469,6 +556,11 @@ class Output:
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return True
|
||||
|
||||
def changed_meta(self) -> bool:
|
||||
if self.fs.version_aware and self.meta.version_id:
|
||||
return self.meta.version_id != self.get_meta().version_id
|
||||
return False
|
||||
|
||||
def workspace_status(self):
|
||||
if not self.exists:
|
||||
return {str(self): "deleted"}
|
||||
@@ -546,40 +638,38 @@ 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,
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=True,
|
||||
)
|
||||
self.hash_info = obj.hash_info
|
||||
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.odb.fs.PARAM_CHECKSUM,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.files = None
|
||||
|
||||
def set_exec(self):
|
||||
if self.isfile() and self.meta.isexec:
|
||||
self.odb.set_exec(self.fs_path)
|
||||
|
||||
def _checkout(self, *args, **kwargs):
|
||||
from dvc_data.checkout import CheckoutError as _CheckoutError
|
||||
from dvc_data.checkout import LinkError, PromptError
|
||||
from dvc_data.hashfile.checkout import CheckoutError as _CheckoutError
|
||||
from dvc_data.hashfile.checkout import LinkError, PromptError
|
||||
|
||||
kwargs.setdefault("ignore", self.dvcignore)
|
||||
try:
|
||||
@@ -610,7 +700,7 @@ class Output:
|
||||
self.odb,
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
otransfer(
|
||||
@@ -639,7 +729,7 @@ class Output:
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
save_obj = save_obj.filter(prefix)
|
||||
@@ -653,8 +743,10 @@ class Output:
|
||||
)
|
||||
return checkout_obj
|
||||
|
||||
def dumpd(self):
|
||||
ret = {**self.hash_info.to_dict(), **self.meta.to_dict()}
|
||||
def dumpd(self, **kwargs):
|
||||
meta = self.meta.to_dict()
|
||||
meta.pop("isdir", None)
|
||||
ret = {**self.hash_info.to_dict(), **meta}
|
||||
|
||||
if self.is_in_repo:
|
||||
path = self.fs.path.as_posix(
|
||||
@@ -665,39 +757,46 @@ 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
|
||||
|
||||
if self.desc:
|
||||
ret[self.PARAM_DESC] = self.desc
|
||||
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 not self.use_cache:
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
if self.metric:
|
||||
ret[self.PARAM_METRIC] = self.metric
|
||||
|
||||
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.plot:
|
||||
ret[self.PARAM_PLOT] = self.plot
|
||||
|
||||
if self.metric:
|
||||
ret[self.PARAM_METRIC] = self.metric
|
||||
if self.persist:
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
|
||||
if self.plot:
|
||||
ret[self.PARAM_PLOT] = self.plot
|
||||
if self.checkpoint:
|
||||
ret[self.PARAM_CHECKPOINT] = self.checkpoint
|
||||
|
||||
if self.persist:
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
|
||||
if self.checkpoint:
|
||||
ret[self.PARAM_CHECKPOINT] = self.checkpoint
|
||||
if not self.can_push:
|
||||
ret[self.PARAM_PUSH] = self.can_push
|
||||
|
||||
if self.live:
|
||||
ret[self.PARAM_LIVE] = self.live
|
||||
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
if (
|
||||
(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)
|
||||
):
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
else:
|
||||
obj = self.get_obj()
|
||||
ret[self.PARAM_FILES] = obj.as_list(with_meta=True)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -722,26 +821,26 @@ class Output:
|
||||
f"binary file '{self.fs_path}' cannot be used as metrics."
|
||||
)
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc.fs.callbacks import Callback
|
||||
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Downloading {self.fs.path.name(self.fs_path)}",
|
||||
unit="files",
|
||||
) as cb:
|
||||
self.fs.get(self.fs_path, to.fs_path, batch_size=jobs, callback=cb)
|
||||
|
||||
def get_obj(self, filter_info=None, **kwargs):
|
||||
def get_obj(
|
||||
self, filter_info: Optional[str] = None, **kwargs
|
||||
) -> Optional["HashFile"]:
|
||||
obj: Optional["HashFile"] = None
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
elif self.hash_info:
|
||||
try:
|
||||
obj = oload(self.odb, self.hash_info)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
if self.files:
|
||||
tree = Tree.from_list(self.files, hash_name=self.hash_name)
|
||||
tree.digest()
|
||||
obj = tree
|
||||
else:
|
||||
try:
|
||||
obj = oload(self.odb, self.hash_info)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
assert obj
|
||||
fs_path = self.fs.path
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
prefix = fs_path.relparts(filter_info, self.fs_path)
|
||||
@@ -752,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,
|
||||
@@ -760,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
|
||||
|
||||
@@ -858,6 +957,7 @@ class Output:
|
||||
)
|
||||
|
||||
self.hash_info = obj.hash_info
|
||||
self.files = None
|
||||
return obj
|
||||
|
||||
def get_files_number(self, filter_info=None):
|
||||
@@ -928,6 +1028,7 @@ class Output:
|
||||
|
||||
obj = self.get_obj()
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
assert obj
|
||||
prefix = self.fs.path.parts(
|
||||
self.fs.path.relpath(filter_info, self.fs_path)
|
||||
)
|
||||
@@ -942,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. "
|
||||
@@ -1020,7 +1127,7 @@ class Output:
|
||||
other = out.dumpd()
|
||||
|
||||
ignored = [
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
Meta.PARAM_SIZE,
|
||||
Meta.PARAM_NFILES,
|
||||
]
|
||||
@@ -1039,9 +1146,9 @@ class Output:
|
||||
"unable to auto-merge outputs that are not directories"
|
||||
)
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
from dvc_data.objects.tree import MergeError as TreeMergeError
|
||||
from dvc_data.objects.tree import du, merge
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
from dvc_data.hashfile.tree import MergeError as TreeMergeError
|
||||
from dvc_data.hashfile.tree import du, merge
|
||||
|
||||
assert other
|
||||
|
||||
@@ -1056,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
|
||||
@@ -1064,6 +1175,7 @@ class Output:
|
||||
self.odb.add(merged.path, merged.fs, merged.oid)
|
||||
|
||||
self.hash_info = merged.hash_info
|
||||
self.files = None
|
||||
self.meta = Meta(
|
||||
size=du(self.odb, merged),
|
||||
nfiles=len(merged),
|
||||
@@ -1079,28 +1191,69 @@ 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 = {
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
Meta.PARAM_VERSION_ID: str,
|
||||
}
|
||||
|
||||
ARTIFACT_SCHEMA = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required(Output.PARAM_PATH): str,
|
||||
Output.PARAM_PLOT: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
}
|
||||
|
||||
DIR_FILES_SCHEMA: Dict[str, Any] = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required(Tree.PARAM_RELPATH): str,
|
||||
}
|
||||
|
||||
SCHEMA = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**ANNOTATION_SCHEMA,
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
|
||||
Output.PARAM_DESC: str,
|
||||
Output.PARAM_REMOTE: str,
|
||||
Output.PARAM_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,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence
|
||||
@@ -160,7 +159,7 @@ class Value(Node):
|
||||
PRIMITIVES = (int, float, str, bytes, bool)
|
||||
|
||||
|
||||
class Container(Node, ABC):
|
||||
class Container(Node, ABC): # noqa: B024
|
||||
meta: Meta
|
||||
data: Union[list, dict]
|
||||
_key_transform = staticmethod(identity)
|
||||
@@ -357,17 +356,14 @@ class Context(CtxDict):
|
||||
def load_from(
|
||||
cls, fs, path: str, select_keys: List[str] = None
|
||||
) -> "Context":
|
||||
from dvc.utils.serialize import LOADERS
|
||||
from dvc.utils.serialize import load_path
|
||||
|
||||
if not fs.exists(path):
|
||||
raise ParamsLoadError(f"'{path}' does not exist")
|
||||
if fs.isdir(path):
|
||||
raise ParamsLoadError(f"'{path}' is a directory")
|
||||
|
||||
_, ext = os.path.splitext(path)
|
||||
loader = LOADERS[ext]
|
||||
|
||||
data = loader(path, fs=fs)
|
||||
data = load_path(path, fs)
|
||||
if not isinstance(data, Mapping):
|
||||
typ = type(data).__name__
|
||||
raise ParamsLoadError(
|
||||
|
||||
@@ -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
|
||||
|
||||
+263
-259
@@ -1,90 +1,101 @@
|
||||
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_x(properties: Dict, file2datapoints: Dict[str, List[Dict]]):
|
||||
x = properties.get("x", None)
|
||||
if x is not None and isinstance(x, dict):
|
||||
filename, field = first(_file_field(x))
|
||||
_verify_field(file2datapoints, filename, field)
|
||||
return filename, field
|
||||
return None
|
||||
|
||||
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 +106,215 @@ 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):
|
||||
def _infer_files(from_name, to_name):
|
||||
from_value = self.properties.get(from_name, None)
|
||||
to_value = self.properties.get(to_name, 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}')."
|
||||
)
|
||||
if isinstance(to_value, str):
|
||||
self.inferred_properties[to_name] = {}
|
||||
if isinstance(from_value, dict):
|
||||
for file in from_value.keys():
|
||||
self.inferred_properties[to_name][file] = to_value
|
||||
else:
|
||||
self.inferred_properties[to_name][self.plot_id] = to_value
|
||||
else:
|
||||
self.inferred_properties["y"] = inferred_y
|
||||
self.inferred_properties[to_name] = to_value
|
||||
|
||||
_infer_files("y", "x")
|
||||
_infer_files("x", "y")
|
||||
|
||||
if self.inferred_properties.get("y", None) is None:
|
||||
self._infer_y_from_data()
|
||||
|
||||
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 = {}
|
||||
|
||||
x = _get_x(properties, file2datapoints)
|
||||
|
||||
# assign "step" if no x provided
|
||||
if not x:
|
||||
x_file, x_field = (
|
||||
None,
|
||||
INDEX_FIELD,
|
||||
)
|
||||
else:
|
||||
x_file, x_field = x
|
||||
props_update["x"] = x_field
|
||||
|
||||
ys = list(_get_ys(properties, file2datapoints))
|
||||
|
||||
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 y_file, y_field in ys:
|
||||
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 "
|
||||
"'{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
|
||||
|
||||
+10
-6
@@ -67,6 +67,7 @@ class Repo:
|
||||
from dvc.repo.imp_url import imp_url # type: ignore[misc]
|
||||
from dvc.repo.install import install # type: ignore[misc]
|
||||
from dvc.repo.ls import ls as _ls # type: ignore[misc]
|
||||
from dvc.repo.ls_url import ls_url as _ls_url # type: ignore[misc]
|
||||
from dvc.repo.move import move # type: ignore[misc]
|
||||
from dvc.repo.pull import pull # type: ignore[misc]
|
||||
from dvc.repo.push import push # type: ignore[misc]
|
||||
@@ -79,6 +80,7 @@ class Repo:
|
||||
from .data import status as data_status # type: ignore[misc]
|
||||
|
||||
ls = staticmethod(_ls)
|
||||
ls_url = staticmethod(_ls_url)
|
||||
get = staticmethod(_get)
|
||||
get_url = staticmethod(_get_url)
|
||||
|
||||
@@ -186,7 +188,7 @@ class Repo:
|
||||
self.config = Config(self.dvc_dir, fs=self.fs, config=config)
|
||||
self._uninitialized = uninitialized
|
||||
|
||||
# used by DvcFileSystem to determine if it should traverse subrepos
|
||||
# used by DVCFileSystem to determine if it should traverse subrepos
|
||||
self.subrepos = subrepos
|
||||
|
||||
self.cloud = DataCloud(self)
|
||||
@@ -267,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
|
||||
|
||||
@@ -399,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.
|
||||
@@ -433,6 +436,7 @@ class Repo:
|
||||
jobs=jobs,
|
||||
recursive=recursive,
|
||||
with_deps=with_deps,
|
||||
push=push,
|
||||
).items():
|
||||
used[odb].update(objs)
|
||||
|
||||
@@ -487,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
|
||||
)
|
||||
|
||||
@@ -501,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:
|
||||
|
||||
+4
-3
@@ -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:
|
||||
@@ -264,6 +264,7 @@ def create_stages(
|
||||
outs=[out],
|
||||
external=external,
|
||||
)
|
||||
if kwargs.get("desc"):
|
||||
stage.outs[0].desc = kwargs["desc"]
|
||||
|
||||
out_obj = stage.outs[0]
|
||||
out_obj.annot.update(**kwargs)
|
||||
yield stage
|
||||
|
||||
@@ -77,6 +77,7 @@ def brancher( # noqa: E302
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
for sha, names in found_revs.items():
|
||||
self.__dict__.pop("index", None)
|
||||
self.fs = GitFileSystem(scm=scm, rev=sha)
|
||||
self.root_dir = self.fs.path.join("/", *repo_root_parts)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from dvc.exceptions import (
|
||||
CheckoutErrorSuggestGit,
|
||||
NoOutputOrStageError,
|
||||
)
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils import relpath
|
||||
|
||||
from . import locked
|
||||
@@ -80,6 +79,7 @@ def checkout(
|
||||
allow_missing=False,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.fs.callbacks import Callback
|
||||
|
||||
stats = {"added": [], "deleted": [], "modified": [], "failed": []}
|
||||
if not targets:
|
||||
@@ -91,13 +91,16 @@ def checkout(
|
||||
|
||||
pairs = _collect_pairs(self, targets, with_deps, recursive)
|
||||
total = get_all_files_numbers(pairs)
|
||||
with Tqdm(
|
||||
total=total, unit="file", desc="Checkout", disable=total == 0
|
||||
) as pbar:
|
||||
with Callback.as_tqdm_callback(
|
||||
unit="file",
|
||||
desc="Checkout",
|
||||
disable=total == 0,
|
||||
) as cb:
|
||||
cb.set_size(total)
|
||||
for stage, filter_info in pairs:
|
||||
result = stage.checkout(
|
||||
force=force,
|
||||
progress_callback=pbar.update_msg,
|
||||
progress_callback=cb,
|
||||
relink=relink,
|
||||
filter_info=filter_info,
|
||||
allow_missing=allow_missing,
|
||||
|
||||
+2
-2
@@ -30,9 +30,9 @@ def _collect_paths(
|
||||
recursive: bool = False,
|
||||
rev: str = None,
|
||||
) -> StrPaths:
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
fs = DvcFileSystem(repo=repo)
|
||||
fs = DVCFileSystem(repo=repo)
|
||||
fs_paths = [fs.from_os_path(target) for target in targets]
|
||||
|
||||
target_paths: StrPaths = []
|
||||
|
||||
+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
|
||||
)
|
||||
|
||||
+141
-78
@@ -1,28 +1,43 @@
|
||||
import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict, cast
|
||||
from itertools import chain
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
|
||||
def posixpath_to_os_path(path: str) -> str:
|
||||
return path.replace(posixpath.sep, os.path.sep)
|
||||
|
||||
|
||||
def _in_cache(obj: "HashInfo", cache: "HashFileDB") -> bool:
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
if not obj:
|
||||
return False
|
||||
if not obj.hash_info.value:
|
||||
return False
|
||||
|
||||
assert obj.value
|
||||
try:
|
||||
cache.check(obj.hash_info.value)
|
||||
cache.check(obj.value)
|
||||
return True
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return False
|
||||
@@ -30,31 +45,30 @@ def _in_cache(obj: Optional["HashFile"], cache: "HashFileDB") -> bool:
|
||||
|
||||
def _shallow_diff(
|
||||
root: str,
|
||||
old_obj: Optional["HashFile"],
|
||||
new_obj: Optional["HashFile"],
|
||||
old: Optional["HashInfo"],
|
||||
new: Optional["HashInfo"],
|
||||
cache: "HashFileDB",
|
||||
) -> Dict[str, List[str]]:
|
||||
# TODO: add support for shallow diff in dvc-data
|
||||
# TODO: we may want to recursively do in_cache check
|
||||
d = {}
|
||||
|
||||
from dvc_data.objects.tree import Tree
|
||||
old_root = new_root = root
|
||||
if old and old.isdir:
|
||||
old_root = os.path.sep.join([root, ""])
|
||||
if new and new.isdir:
|
||||
new_root = os.path.sep.join([root, ""])
|
||||
|
||||
if isinstance(new_obj, Tree):
|
||||
root = os.path.sep.join([root, ""])
|
||||
if old and not _in_cache(old, cache):
|
||||
d["not_in_cache"] = [old_root]
|
||||
|
||||
if not _in_cache(old_obj, cache):
|
||||
d["not_in_cache"] = [root]
|
||||
|
||||
if old_obj is None and new_obj is None:
|
||||
if not old and not new:
|
||||
return d
|
||||
if old_obj is None:
|
||||
return {"added": [root], **d}
|
||||
if new_obj is None:
|
||||
return {"deleted": [root], **d}
|
||||
if old_obj.hash_info != new_obj.hash_info:
|
||||
return {"modified": [root], **d}
|
||||
return {"unchanged": [root], **d}
|
||||
if not new:
|
||||
return {"deleted": [old_root], **d}
|
||||
if not old:
|
||||
return {"added": [new_root], **d}
|
||||
if old != new:
|
||||
return {"modified": [new_root], **d}
|
||||
return {"unchanged": [new_root], **d}
|
||||
|
||||
|
||||
def _granular_diff(
|
||||
@@ -62,55 +76,60 @@ def _granular_diff(
|
||||
old_obj: Optional["HashFile"],
|
||||
new_obj: Optional["HashFile"],
|
||||
cache: "HashFileDB",
|
||||
with_dirs: bool = False,
|
||||
) -> Dict[str, List[str]]:
|
||||
from dvc_data.diff import ROOT
|
||||
from dvc_data.diff import diff as odiff
|
||||
from dvc_data.objects.tree import Tree
|
||||
from dvc_data.hashfile.diff import ROOT
|
||||
from dvc_data.hashfile.diff import diff as odiff
|
||||
|
||||
def path_join(root: str, *paths: str) -> str:
|
||||
if not isinstance(new_obj, Tree):
|
||||
def path_join(root: str, *paths: str, isdir: bool = False) -> str:
|
||||
if not isdir and paths == ROOT:
|
||||
return root
|
||||
return os.path.sep.join([root, *paths])
|
||||
|
||||
diff_data = odiff(old_obj, new_obj, cache)
|
||||
drop_root = not with_dirs and isinstance(new_obj, Tree)
|
||||
|
||||
output: Dict[str, List[str]] = defaultdict(list)
|
||||
for state in ("added", "deleted", "modified", "unchanged"):
|
||||
items = getattr(diff_data, state)
|
||||
output[state].extend(
|
||||
path_join(root, *item.new.key)
|
||||
for item in items
|
||||
if not (drop_root and item.new.key == ROOT)
|
||||
)
|
||||
# TODO: PERF: diff is checking not_in_cache for each even if we only
|
||||
# need it for the index.
|
||||
# BUG: not_in_cache file also shows up as modified in staged and
|
||||
# unstaged. We currently don't know if it is really modified.
|
||||
output["not_in_cache"].extend(
|
||||
path_join(root, *item.new.key)
|
||||
for item in items
|
||||
if not item.old.in_cache
|
||||
and not (drop_root and item.new.key == ROOT)
|
||||
and state != "added"
|
||||
)
|
||||
return output
|
||||
for item in items: # pylint: disable=not-an-iterable
|
||||
entry = item.old if state == "deleted" else item.new
|
||||
isdir = entry.oid.isdir if entry.oid else False
|
||||
|
||||
path = path_join(root, *entry.key, isdir=isdir)
|
||||
output[state].append(path)
|
||||
if not item.old.in_cache and state != "added":
|
||||
output["not_in_cache"].append(path)
|
||||
return dict(output)
|
||||
|
||||
|
||||
def _get_obj_items(root: str, obj: Optional["HashFile"]) -> List[str]:
|
||||
if not obj:
|
||||
return []
|
||||
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
sep = os.path.sep
|
||||
if isinstance(obj, Tree):
|
||||
return [sep.join([root, *key]) for key, _, _ in obj]
|
||||
return [root]
|
||||
|
||||
|
||||
def _diff(
|
||||
root: str,
|
||||
old_oid: Optional["HashInfo"],
|
||||
old_obj: Optional["HashFile"],
|
||||
new_oid: Optional["HashInfo"],
|
||||
new_obj: Optional["HashFile"],
|
||||
cache: "HashFileDB",
|
||||
odb: "HashFileDB",
|
||||
granular: bool = False,
|
||||
with_dirs: bool = False,
|
||||
) -> Dict[str, List[str]]:
|
||||
if granular:
|
||||
return _granular_diff(
|
||||
root, old_obj, new_obj, cache, with_dirs=with_dirs
|
||||
)
|
||||
return _shallow_diff(root, old_obj, new_obj, cache)
|
||||
if not granular:
|
||||
return _shallow_diff(root, old_oid, new_oid, odb)
|
||||
if (old_oid and not old_obj) or (new_oid and not new_obj):
|
||||
# we don't have enough information to give full details
|
||||
unknown = _get_obj_items(root, new_obj)
|
||||
shallow_diff = _shallow_diff(root, old_oid, new_oid, odb)
|
||||
return {**shallow_diff, "unknown": unknown}
|
||||
return _granular_diff(root, old_obj, new_obj, odb)
|
||||
|
||||
|
||||
class GitInfo(TypedDict, total=False):
|
||||
@@ -121,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
|
||||
@@ -137,6 +158,8 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
empty_repo = False
|
||||
|
||||
staged, unstaged, untracked = scm.status(untracked_files=untracked_files)
|
||||
if os.name == "nt":
|
||||
untracked = [posixpath_to_os_path(path) for path in untracked]
|
||||
# NOTE: order is important here.
|
||||
return GitInfo(
|
||||
staged=staged,
|
||||
@@ -148,7 +171,7 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
|
||||
|
||||
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
from dvc_data.build import build
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
unstaged_diff = defaultdict(list)
|
||||
for out in repo.index.outs:
|
||||
@@ -171,8 +194,18 @@ def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
cache = repo.odb.repo
|
||||
root = str(out)
|
||||
old = out.get_obj()
|
||||
|
||||
with ui.status(f"Calculating diff for {root} between index/workspace"):
|
||||
d = _diff(root, old, new, cache, **kwargs)
|
||||
d = _diff(
|
||||
root,
|
||||
out.hash_info,
|
||||
old,
|
||||
new.hash_info if new else None,
|
||||
new,
|
||||
cache,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
for state, items in d.items():
|
||||
if not items:
|
||||
continue
|
||||
@@ -184,7 +217,9 @@ def _diff_head_to_index(
|
||||
repo: "Repo", head: str = "HEAD", **kwargs: Any
|
||||
) -> Dict[str, List[str]]:
|
||||
# we need to store objects from index and the HEAD to diff later
|
||||
objs: Dict[str, Dict[str, "HashFile"]] = defaultdict(dict)
|
||||
objs: Dict[str, Dict[str, Tuple["HashFile", "HashInfo"]]]
|
||||
objs = defaultdict(dict)
|
||||
|
||||
staged_diff = defaultdict(list)
|
||||
for rev in repo.brancher(revs=[head]):
|
||||
for out in repo.index.outs:
|
||||
@@ -193,15 +228,20 @@ def _diff_head_to_index(
|
||||
continue
|
||||
|
||||
root = str(out)
|
||||
if isinstance(out.fs, GitFileSystem):
|
||||
root = posixpath_to_os_path(root)
|
||||
typ = "index" if rev == "workspace" else head
|
||||
objs[root][typ] = out.get_obj()
|
||||
objs[root][typ] = (out.get_obj(), out.hash_info)
|
||||
|
||||
cache = repo.odb.repo
|
||||
for root, obj_d in objs.items():
|
||||
old = obj_d.get(head, None)
|
||||
new = obj_d.get("index", None)
|
||||
old_obj, old_oid = obj_d.get(head, (None, None))
|
||||
new_obj, new_oid = obj_d.get("index", (None, None))
|
||||
with ui.status(f"Calculating diff for {root} between head/index"):
|
||||
d = _diff(root, old, new, cache, **kwargs)
|
||||
d = _diff(
|
||||
root, old_oid, old_obj, new_oid, new_obj, cache, **kwargs
|
||||
)
|
||||
|
||||
for state, items in d.items():
|
||||
if not items:
|
||||
continue
|
||||
@@ -212,28 +252,36 @@ def _diff_head_to_index(
|
||||
|
||||
class Status(TypedDict):
|
||||
not_in_cache: List[str]
|
||||
committed: Dict[str, Any]
|
||||
uncommitted: Dict[str, Any]
|
||||
committed: Dict[str, List[str]]
|
||||
uncommitted: Dict[str, List[str]]
|
||||
untracked: List[str]
|
||||
unchanged: List[str]
|
||||
git: GitInfo
|
||||
|
||||
|
||||
def _transform_git_paths_to_dvc(repo: "Repo", files: List[str]):
|
||||
def _transform_git_paths_to_dvc(
|
||||
repo: "Repo", files: Iterable[str]
|
||||
) -> List[str]:
|
||||
"""Transform files rel. to Git root to DVC root, and drop outside files."""
|
||||
rel = repo.fs.path.relpath(repo.root_dir, repo.scm.root_dir).rstrip("/")
|
||||
if rel in (os.curdir, ""):
|
||||
return files
|
||||
|
||||
prefix = rel + os.sep
|
||||
length = len(prefix)
|
||||
return [file[length:] for file in files if file.startswith(prefix)]
|
||||
# if we have repo root in a different location than scm's root,
|
||||
# i.e. subdir repo, all git_paths need to be transformed rel. to the DVC
|
||||
# repo root and anything outside need to be filtered out.
|
||||
if rel not in (os.curdir, ""):
|
||||
prefix = rel + os.sep
|
||||
length = len(prefix)
|
||||
files = (file[length:] for file in files if file.startswith(prefix))
|
||||
|
||||
start = repo.fs.path.relpath(repo.fs.path.getcwd(), repo.root_dir)
|
||||
if start in (os.curdir, ""):
|
||||
return list(files)
|
||||
# we need to convert repo relative paths to curdir relative.
|
||||
return [repo.fs.path.relpath(file, start) for file in files]
|
||||
|
||||
|
||||
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.scm import NoSCMError
|
||||
from dvc.scm import NoSCMError, SCMError
|
||||
|
||||
head = kwargs.pop("head", "HEAD")
|
||||
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
|
||||
@@ -261,3 +309,18 @@ def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
unchanged=list(unchanged),
|
||||
git=git_info,
|
||||
)
|
||||
|
||||
|
||||
def ls(
|
||||
repo: "Repo",
|
||||
targets: List[Optional[str]] = None,
|
||||
recursive: bool = False,
|
||||
) -> Iterator[Dict[str, Any]]:
|
||||
targets = targets or [None]
|
||||
pairs = chain.from_iterable(
|
||||
repo.stage.collect_granular(target, recursive=recursive)
|
||||
for target in targets
|
||||
)
|
||||
for stage, filter_info in pairs:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
yield {"path": str(out), **out.annot.to_dict()}
|
||||
|
||||
+3
-3
@@ -23,9 +23,9 @@ def diff(self, a_rev="HEAD", b_rev=None, targets=None):
|
||||
if self.scm.no_commits:
|
||||
return {}
|
||||
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
dvcfs = DvcFileSystem(repo=self)
|
||||
dvcfs = DVCFileSystem(repo=self)
|
||||
targets = ensure_list(targets)
|
||||
targets = [dvcfs.from_os_path(target) for target in targets]
|
||||
|
||||
@@ -115,7 +115,7 @@ def _paths_checksums(repo, targets):
|
||||
|
||||
def _output_paths(repo, targets):
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc_data.build import build
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
on_working_fs = isinstance(repo.fs, LocalFileSystem)
|
||||
|
||||
|
||||
@@ -2,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,
|
||||
@@ -172,7 +171,18 @@ 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
|
||||
|
||||
@@ -190,7 +200,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,10 +224,11 @@ 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("\nReproduced experiment(s): {}".format(", ".join(names)))
|
||||
ui.write("\nRan experiment(s): {}".format(", ".join(names)))
|
||||
if tmp_dir:
|
||||
ui.write(
|
||||
"To apply the results of an experiment to your workspace "
|
||||
@@ -411,86 +425,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):
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import pickle
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import IntEnum
|
||||
from functools import partial
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -19,6 +20,8 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.stage.serialize import to_lockfile
|
||||
@@ -35,6 +38,8 @@ from ..refs import (
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_HEAD,
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_NAMESPACE,
|
||||
EXPS_STASH,
|
||||
@@ -47,10 +52,9 @@ if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments.stash import ExpStashEntry
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
from ..base import ExpStashEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -64,6 +68,16 @@ class ExecutorResult(NamedTuple):
|
||||
force: bool
|
||||
|
||||
|
||||
class TaskStatus(IntEnum):
|
||||
PENDING = 0
|
||||
PREPARING = 1
|
||||
RUNNING = 2
|
||||
SUCCESS = 3
|
||||
FAILED = 4
|
||||
CANCELED = 5
|
||||
FINISHED = 6
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutorInfo:
|
||||
git_url: str
|
||||
@@ -73,13 +87,16 @@ class ExecutorInfo:
|
||||
dvc_dir: str
|
||||
name: Optional[str] = None
|
||||
wdir: Optional[str] = None
|
||||
collected: bool = False
|
||||
result_hash: Optional[str] = None
|
||||
result_ref: Optional[str] = None
|
||||
result_force: bool = False
|
||||
status: TaskStatus = TaskStatus.PENDING
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
if "collected" in d:
|
||||
if d.pop("collected"):
|
||||
d["status"] = TaskStatus.FINISHED
|
||||
return cls(**d)
|
||||
|
||||
def asdict(self):
|
||||
@@ -136,6 +153,7 @@ class BaseExecutor(ABC):
|
||||
root_dir: str,
|
||||
dvc_dir: str,
|
||||
baseline_rev: str,
|
||||
status: TaskStatus,
|
||||
wdir: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
location: Optional[str] = None,
|
||||
@@ -149,9 +167,17 @@ class BaseExecutor(ABC):
|
||||
self.baseline_rev = baseline_rev
|
||||
self.location: str = location or self.DEFAULT_LOCATION
|
||||
self.result = result
|
||||
self.status = status
|
||||
|
||||
@abstractmethod
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
def init_git(
|
||||
self,
|
||||
scm: "Git",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
infofile: Optional[str],
|
||||
branch: Optional[str] = None,
|
||||
):
|
||||
"""Init git repo and populate it using exp refs from the specified
|
||||
SCM instance.
|
||||
"""
|
||||
@@ -191,6 +217,7 @@ class BaseExecutor(ABC):
|
||||
dvc_dir=self.dvc_dir,
|
||||
name=self.name,
|
||||
wdir=self.wdir,
|
||||
status=self.status,
|
||||
**result_dict,
|
||||
)
|
||||
|
||||
@@ -212,6 +239,7 @@ class BaseExecutor(ABC):
|
||||
root_dir=info.root_dir,
|
||||
dvc_dir=info.dvc_dir,
|
||||
baseline_rev=info.baseline_rev,
|
||||
status=info.status,
|
||||
name=info.name,
|
||||
wdir=info.wdir,
|
||||
result=result,
|
||||
@@ -222,7 +250,6 @@ class BaseExecutor(ABC):
|
||||
def from_stash_entry(
|
||||
cls: Type[_T],
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
**kwargs,
|
||||
) -> _T:
|
||||
@@ -232,7 +259,6 @@ class BaseExecutor(ABC):
|
||||
def _from_stash_entry(
|
||||
cls: Type[_T],
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
root_dir: str,
|
||||
**kwargs,
|
||||
@@ -241,12 +267,11 @@ class BaseExecutor(ABC):
|
||||
root_dir=root_dir,
|
||||
dvc_dir=relpath(repo.dvc_dir, repo.scm.root_dir),
|
||||
baseline_rev=entry.baseline_rev,
|
||||
status=TaskStatus.PREPARING,
|
||||
name=entry.name,
|
||||
wdir=relpath(os.getcwd(), repo.scm.root_dir),
|
||||
**kwargs,
|
||||
)
|
||||
executor.init_git(repo.scm, branch=entry.branch)
|
||||
executor.init_cache(repo, stash_rev)
|
||||
return executor
|
||||
|
||||
@staticmethod
|
||||
@@ -259,8 +284,12 @@ class BaseExecutor(ABC):
|
||||
exp_data.update(to_lockfile(stage))
|
||||
return dict_sha256(exp_data)
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
def cleanup(self, infofile: str):
|
||||
if infofile is not None:
|
||||
info = ExecutorInfo.load_json(infofile)
|
||||
if info.status < TaskStatus.FAILED:
|
||||
info.status = TaskStatus.FINISHED
|
||||
info.dump_json(infofile)
|
||||
|
||||
# TODO: come up with better way to stash repro arguments
|
||||
@staticmethod
|
||||
@@ -324,30 +353,35 @@ class BaseExecutor(ABC):
|
||||
logger.debug("Replacing existing experiment '%s'", orig_ref)
|
||||
return True
|
||||
|
||||
if on_diverged:
|
||||
return on_diverged(orig_ref, has_checkpoint)
|
||||
|
||||
self._raise_ref_conflict(
|
||||
dest_scm, orig_ref, new_rev, has_checkpoint
|
||||
)
|
||||
if on_diverged:
|
||||
on_diverged(orig_ref, has_checkpoint)
|
||||
logger.debug("Reproduced existing experiment '%s'", orig_ref)
|
||||
return False
|
||||
|
||||
# fetch experiments
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
[f"{ref}:{ref}" for ref in refs],
|
||||
on_diverged=on_diverged_ref,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
# update last run checkpoint (if it exists)
|
||||
if has_checkpoint:
|
||||
try:
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
[f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}"],
|
||||
force=True,
|
||||
[f"{ref}:{ref}" for ref in refs],
|
||||
on_diverged=on_diverged_ref,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
# update last run checkpoint (if it exists)
|
||||
if has_checkpoint:
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
[f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}"],
|
||||
force=True,
|
||||
**kwargs,
|
||||
)
|
||||
except SCMError:
|
||||
pass
|
||||
|
||||
return refs
|
||||
|
||||
@classmethod
|
||||
@@ -411,11 +445,9 @@ class BaseExecutor(ABC):
|
||||
exp_ref: Optional["ExpRefInfo"] = None
|
||||
repro_force: bool = False
|
||||
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
|
||||
with cls._repro_dvc(
|
||||
info,
|
||||
infofile,
|
||||
log_errors=log_errors,
|
||||
**kwargs,
|
||||
) as dvc:
|
||||
@@ -491,9 +523,6 @@ class BaseExecutor(ABC):
|
||||
info.result_ref = ref
|
||||
info.result_force = repro_force
|
||||
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
|
||||
# ideally we would return stages here like a normal repro() call, but
|
||||
# stages is not currently picklable and cannot be returned across
|
||||
# multiprocessing calls
|
||||
@@ -550,6 +579,7 @@ class BaseExecutor(ABC):
|
||||
def _repro_dvc(
|
||||
cls,
|
||||
info: "ExecutorInfo",
|
||||
infofile: str = None,
|
||||
log_errors: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -557,6 +587,9 @@ class BaseExecutor(ABC):
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
|
||||
info.status = TaskStatus.RUNNING
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
if cls.QUIET:
|
||||
dvc.scm_context.quiet = cls.QUIET
|
||||
old_cwd = os.getcwd()
|
||||
@@ -568,17 +601,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)
|
||||
|
||||
@@ -714,3 +753,23 @@ class BaseExecutor(ABC):
|
||||
disable_other_loggers()
|
||||
if level is not None:
|
||||
dvc_logger.setLevel(level)
|
||||
|
||||
@contextmanager
|
||||
def set_exec_refs(
|
||||
self, scm: "Git", stash_rev: str, entry: "ExpStashEntry"
|
||||
):
|
||||
try:
|
||||
# Executor will be initialized with an empty git repo that
|
||||
# we populate by pushing:
|
||||
# EXEC_HEAD - the base commit for this experiment
|
||||
# EXEC_MERGE - the unmerged changes (from our stash)
|
||||
# to be reproduced
|
||||
# EXEC_BASELINE - the baseline commit for this experiment
|
||||
scm.set_ref(EXEC_HEAD, entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
|
||||
yield
|
||||
finally:
|
||||
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
|
||||
if scm.get_ref(ref):
|
||||
scm.remove_ref(ref)
|
||||
|
||||
@@ -19,7 +19,7 @@ from ..refs import (
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
)
|
||||
from .base import EXEC_TMP_DIR, BaseExecutor
|
||||
from .base import EXEC_TMP_DIR, BaseExecutor, TaskStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
@@ -46,10 +46,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,15 +67,28 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
QUIET = True
|
||||
DEFAULT_LOCATION = "tempdir"
|
||||
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
def init_git(
|
||||
self,
|
||||
scm: "Git",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
infofile: Optional[str],
|
||||
branch: Optional[str] = None,
|
||||
):
|
||||
from dulwich.repo import Repo as DulwichRepo
|
||||
|
||||
from ..utils import push_refspec
|
||||
|
||||
DulwichRepo.init(os.fspath(self.root_dir))
|
||||
|
||||
refspec = f"{EXEC_NAMESPACE}/"
|
||||
push_refspec(scm, self.git_url, refspec, refspec)
|
||||
self.status = TaskStatus.PREPARING
|
||||
if infofile:
|
||||
self.info.dump_json(infofile)
|
||||
|
||||
with self.set_exec_refs(scm, stash_rev, entry):
|
||||
refspec = f"{EXEC_NAMESPACE}/"
|
||||
push_refspec(scm, self.git_url, refspec, refspec)
|
||||
|
||||
if branch:
|
||||
push_refspec(scm, self.git_url, branch, branch)
|
||||
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
|
||||
@@ -84,17 +97,12 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
|
||||
if self.scm.get_ref(EXEC_CHECKPOINT):
|
||||
self.scm.remove_ref(EXEC_CHECKPOINT)
|
||||
|
||||
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
|
||||
# committing
|
||||
head = EXEC_BRANCH if branch else EXEC_HEAD
|
||||
self.scm.checkout(head, detach=True)
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
|
||||
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
|
||||
if scm.get_ref(ref):
|
||||
scm.remove_ref(ref)
|
||||
|
||||
try:
|
||||
self.scm.merge(merge_rev, squash=True, commit=False)
|
||||
except _SCMError as exc:
|
||||
@@ -114,8 +122,8 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
"""Initialize DVC cache."""
|
||||
self._config(repo.odb.repo.path)
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
def cleanup(self, infofile: str):
|
||||
super().cleanup(infofile)
|
||||
logger.debug("Removing tmpdir '%s'", self.root_dir)
|
||||
remove(self.root_dir)
|
||||
|
||||
@@ -123,7 +131,6 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
def from_stash_entry(
|
||||
cls,
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
wdir: Optional[str] = None,
|
||||
**kwargs,
|
||||
@@ -132,9 +139,7 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
makedirs(parent_dir, exist_ok=True)
|
||||
tmp_dir = mkdtemp(dir=parent_dir)
|
||||
try:
|
||||
executor = cls._from_stash_entry(
|
||||
repo, stash_rev, entry, tmp_dir, **kwargs
|
||||
)
|
||||
executor = cls._from_stash_entry(repo, entry, tmp_dir, **kwargs)
|
||||
logger.debug("Init temp dir executor in '%s'", tmp_dir)
|
||||
return executor
|
||||
except Exception:
|
||||
@@ -152,18 +157,31 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
def from_stash_entry(
|
||||
cls,
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
**kwargs,
|
||||
):
|
||||
root_dir = repo.scm.root_dir
|
||||
executor = cls._from_stash_entry(
|
||||
repo, stash_rev, entry, root_dir, **kwargs
|
||||
executor: "WorkspaceExecutor" = cls._from_stash_entry(
|
||||
repo, entry, root_dir, **kwargs
|
||||
)
|
||||
logger.debug("Init workspace executor in '%s'", root_dir)
|
||||
return executor
|
||||
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
def init_git(
|
||||
self,
|
||||
scm: "Git",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
infofile: Optional[str],
|
||||
branch: Optional[str] = None,
|
||||
):
|
||||
self.status = TaskStatus.PREPARING
|
||||
if infofile:
|
||||
self.info.dump_json(infofile)
|
||||
|
||||
scm.set_ref(EXEC_HEAD, entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
|
||||
self._detach_stack.enter_context(
|
||||
self.scm.detach_head(
|
||||
self.scm.get_ref(EXEC_HEAD),
|
||||
@@ -184,7 +202,9 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
def init_cache(self, repo: "Repo", rev: str, run_cache: bool = True):
|
||||
pass
|
||||
|
||||
def cleanup(self):
|
||||
def cleanup(self, infofile: str):
|
||||
super().cleanup(infofile)
|
||||
remove(os.path.dirname(infofile))
|
||||
with self._detach_stack:
|
||||
self.scm.remove_ref(EXEC_BASELINE)
|
||||
self.scm.remove_ref(EXEC_MERGE)
|
||||
@@ -193,4 +213,3 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
|
||||
if checkpoint and checkpoint != self._orig_checkpoint:
|
||||
self.scm.set_ref(EXEC_APPLY, checkpoint)
|
||||
super().cleanup()
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from abc import ABC
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Deque, Dict, Generator, Optional, Tuple, Type
|
||||
|
||||
from dvc.proc.manager import ProcessManager
|
||||
|
||||
from ...exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
from ...refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
|
||||
from ...stash import ExpStashEntry
|
||||
from ..base import EXEC_PID_DIR, BaseExecutor
|
||||
from ..local import TempDirExecutor, WorkspaceExecutor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseExecutorManager(ABC, Mapping):
|
||||
"""Manages executors for a collection of experiments to be run."""
|
||||
|
||||
EXECUTOR_CLS: Type = BaseExecutor
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
self.scm = scm
|
||||
makedirs(wdir, exist_ok=True)
|
||||
self.wdir = wdir
|
||||
self.proc = ProcessManager(self.pid_dir)
|
||||
self._attached: Dict[str, "BaseExecutor"] = {}
|
||||
self._detached: Dict[str, "BaseExecutor"] = dict(self._load_infos())
|
||||
self._queue: Deque[Tuple[str, "BaseExecutor"]] = deque()
|
||||
|
||||
def __getitem__(self, key: str) -> "BaseExecutor":
|
||||
try:
|
||||
return self._attached[key]
|
||||
except KeyError:
|
||||
pass
|
||||
return self._detached[key]
|
||||
|
||||
def __iter__(self):
|
||||
yield from self._attached
|
||||
yield from self._detached
|
||||
|
||||
def __len__(self):
|
||||
return len(self._attached) + len(self._detached)
|
||||
|
||||
@property
|
||||
def pid_dir(self) -> str:
|
||||
return os.path.join(self.wdir, EXEC_PID_DIR)
|
||||
|
||||
def enqueue(self, rev: str, executor: "BaseExecutor"):
|
||||
assert rev not in self
|
||||
self._queue.append((rev, executor))
|
||||
|
||||
def _load_infos(self) -> Generator[Tuple[str, "BaseExecutor"], None, None]:
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ..base import ExecutorInfo
|
||||
from ..ssh import SSHExecutor
|
||||
|
||||
def make_executor(info: "ExecutorInfo"):
|
||||
if info.git_url:
|
||||
scheme = urlparse(info.git_url).scheme
|
||||
if scheme == "file":
|
||||
cls: Type = TempDirExecutor
|
||||
elif scheme == "ssh":
|
||||
cls = SSHExecutor
|
||||
else:
|
||||
raise NotImplementedError
|
||||
else:
|
||||
cls = WorkspaceExecutor
|
||||
return cls.from_info(info)
|
||||
|
||||
for name in self.proc:
|
||||
infofile = self.get_infofile_path(name)
|
||||
try:
|
||||
with open(infofile, encoding="utf-8") as fobj:
|
||||
info = ExecutorInfo.from_dict(json.load(fobj))
|
||||
yield name, make_executor(info)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
def get_infofile_path(self, name: str) -> str:
|
||||
return os.path.join(
|
||||
self.pid_dir,
|
||||
name,
|
||||
f"{name}{BaseExecutor.INFOFILE_EXT}",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_stash_entries(
|
||||
cls,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
repo: "Repo",
|
||||
to_run: Dict[str, ExpStashEntry],
|
||||
**kwargs,
|
||||
):
|
||||
manager = cls(scm, wdir)
|
||||
manager._enqueue_stash_entries(scm, repo, to_run, **kwargs)
|
||||
return manager
|
||||
|
||||
def _enqueue_stash_entries(
|
||||
self,
|
||||
scm: "Git",
|
||||
repo: "Repo",
|
||||
to_run: Dict[str, ExpStashEntry],
|
||||
**kwargs,
|
||||
):
|
||||
try:
|
||||
for stash_rev, entry in to_run.items():
|
||||
scm.set_ref(EXEC_HEAD, entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
|
||||
|
||||
# Executor will be initialized with an empty git repo that
|
||||
# we populate by pushing:
|
||||
# EXEC_HEAD - the base commit for this experiment
|
||||
# EXEC_MERGE - the unmerged changes (from our stash)
|
||||
# to be reproduced
|
||||
# EXEC_BASELINE - the baseline commit for this experiment
|
||||
executor = self.EXECUTOR_CLS.from_stash_entry(
|
||||
repo,
|
||||
stash_rev,
|
||||
entry,
|
||||
**kwargs,
|
||||
)
|
||||
self.enqueue(stash_rev, executor)
|
||||
finally:
|
||||
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
|
||||
scm.remove_ref(ref)
|
||||
|
||||
def exec_queue(
|
||||
self, repo: "Repo", jobs: Optional[int] = 1, detach: bool = False
|
||||
):
|
||||
"""Run dvc repro for queued executors in parallel."""
|
||||
if detach:
|
||||
raise NotImplementedError
|
||||
# TODO use ProcessManager.spawn() to support detached runs
|
||||
return self._exec_attached(repo, jobs=jobs)
|
||||
|
||||
def _exec_attached(self, repo: "Repo", jobs: Optional[int] = 1):
|
||||
import signal
|
||||
from concurrent.futures import (
|
||||
CancelledError,
|
||||
ProcessPoolExecutor,
|
||||
wait,
|
||||
)
|
||||
from multiprocessing import Manager
|
||||
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
result: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
|
||||
manager = Manager()
|
||||
pid_q = manager.Queue()
|
||||
|
||||
with ProcessPoolExecutor(max_workers=jobs) as workers:
|
||||
futures = {}
|
||||
while self._queue:
|
||||
rev, executor = self._queue.popleft()
|
||||
infofile = self.get_infofile_path(rev)
|
||||
future = workers.submit(
|
||||
executor.reproduce,
|
||||
info=executor.info,
|
||||
rev=rev,
|
||||
queue=pid_q,
|
||||
infofile=infofile,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
)
|
||||
futures[future] = (rev, executor)
|
||||
self._attached[rev] = executor
|
||||
|
||||
try:
|
||||
wait(futures)
|
||||
except KeyboardInterrupt:
|
||||
# forward SIGINT to any running executor processes and
|
||||
# cancel any remaining futures
|
||||
workers.shutdown(wait=False)
|
||||
pids = {}
|
||||
for future, (rev, _) in futures.items():
|
||||
if future.running():
|
||||
# if future has already been started by the scheduler
|
||||
# we still have to wait until it tells us its PID
|
||||
while rev not in pids:
|
||||
rev, pid = pid_q.get()
|
||||
pids[rev] = pid
|
||||
os.kill(pids[rev], signal.SIGINT)
|
||||
elif not future.done():
|
||||
future.cancel()
|
||||
|
||||
for future, (rev, executor) in futures.items():
|
||||
rev, executor = futures[future]
|
||||
|
||||
try:
|
||||
exc = future.exception()
|
||||
if exc is None:
|
||||
exec_result = future.result()
|
||||
result[rev].update(
|
||||
self._collect_executor(repo, executor, exec_result)
|
||||
)
|
||||
elif not isinstance(exc, CheckpointKilledError):
|
||||
logger.error(
|
||||
"Failed to reproduce experiment '%s'", rev[:7]
|
||||
)
|
||||
except CancelledError:
|
||||
logger.error(
|
||||
"Cancelled before attempting to reproduce experiment "
|
||||
"'%s'",
|
||||
rev[:7],
|
||||
)
|
||||
finally:
|
||||
self.cleanup_executor(rev, executor)
|
||||
|
||||
return result
|
||||
|
||||
def _collect_executor(self, repo, executor, exec_result) -> Dict[str, str]:
|
||||
# NOTE: GitPython Repo instances cannot be re-used
|
||||
# after process has received SIGINT or SIGTERM, so we
|
||||
# need this hack to re-instantiate git instances after
|
||||
# checkpoint runs. See:
|
||||
# https://github.com/gitpython-developers/GitPython/issues/427
|
||||
# del self.repo.scm
|
||||
|
||||
results = {}
|
||||
|
||||
def on_diverged(ref: str, checkpoint: bool):
|
||||
ref_info = ExpRefInfo.from_ref(ref)
|
||||
if checkpoint:
|
||||
raise CheckpointExistsError(ref_info.name)
|
||||
raise ExperimentExistsError(ref_info.name)
|
||||
|
||||
for ref in executor.fetch_exps(
|
||||
self.scm,
|
||||
force=exec_result.force,
|
||||
on_diverged=on_diverged,
|
||||
):
|
||||
exp_rev = self.scm.get_ref(ref)
|
||||
if exp_rev:
|
||||
logger.debug("Collected experiment '%s'.", exp_rev[:7])
|
||||
results[exp_rev] = exec_result.exp_hash
|
||||
|
||||
if exec_result.ref_info is not None:
|
||||
executor.collect_cache(repo, exec_result.ref_info)
|
||||
|
||||
return results
|
||||
|
||||
def cleanup_executor(self, rev: str, executor: "BaseExecutor"):
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
executor.cleanup()
|
||||
try:
|
||||
self.proc.remove(rev)
|
||||
except KeyError:
|
||||
pass
|
||||
remove(os.path.join(self.pid_dir, rev))
|
||||
@@ -1,107 +0,0 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from ...refs import EXEC_BASELINE, EXEC_BRANCH, EXEC_HEAD, EXEC_MERGE
|
||||
from ...stash import ExpStashEntry
|
||||
from ..local import TempDirExecutor, WorkspaceExecutor
|
||||
from .base import BaseExecutorManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TempDirExecutorManager(BaseExecutorManager):
|
||||
EXECUTOR_CLS = TempDirExecutor
|
||||
|
||||
|
||||
class WorkspaceExecutorManager(BaseExecutorManager):
|
||||
EXECUTOR_CLS = WorkspaceExecutor
|
||||
|
||||
@classmethod
|
||||
def from_stash_entries(
|
||||
cls,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
repo: "Repo",
|
||||
to_run: Dict[str, ExpStashEntry],
|
||||
**kwargs,
|
||||
):
|
||||
manager = cls(scm, wdir)
|
||||
try:
|
||||
assert len(to_run) == 1
|
||||
for stash_rev, entry in to_run.items():
|
||||
scm.set_ref(EXEC_HEAD, entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
|
||||
|
||||
executor = cls.EXECUTOR_CLS.from_stash_entry(
|
||||
repo,
|
||||
stash_rev,
|
||||
entry,
|
||||
**kwargs,
|
||||
)
|
||||
manager.enqueue(stash_rev, executor)
|
||||
finally:
|
||||
for ref in (EXEC_MERGE,):
|
||||
scm.remove_ref(ref)
|
||||
return manager
|
||||
|
||||
def _collect_executor(self, repo, executor, exec_result) -> Dict[str, str]:
|
||||
results = {}
|
||||
exp_rev = self.scm.get_ref(EXEC_BRANCH)
|
||||
if exp_rev:
|
||||
logger.debug("Collected experiment '%s'.", exp_rev[:7])
|
||||
results[exp_rev] = exec_result.exp_hash
|
||||
return results
|
||||
|
||||
def exec_queue(
|
||||
self, repo: "Repo", jobs: Optional[int] = 1, detach: bool = False
|
||||
):
|
||||
"""Run a single WorkspaceExecutor.
|
||||
|
||||
Workspace execution is done within the main DVC process
|
||||
(rather than in multiprocessing context)
|
||||
"""
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
assert len(self._queue) == 1
|
||||
assert not detach
|
||||
result: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
rev, executor = self._queue.popleft()
|
||||
|
||||
exec_name = "workspace"
|
||||
infofile = self.get_infofile_path(exec_name)
|
||||
try:
|
||||
exec_result = executor.reproduce(
|
||||
info=executor.info,
|
||||
rev=rev,
|
||||
infofile=infofile,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
log_errors=not isinstance(executor, WorkspaceExecutor),
|
||||
)
|
||||
if not exec_result.exp_hash:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
)
|
||||
if exec_result.ref_info:
|
||||
result[rev].update(
|
||||
self._collect_executor(repo, executor, exec_result)
|
||||
)
|
||||
except CheckpointKilledError:
|
||||
# Checkpoint errors have already been logged
|
||||
return {}
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
) from exc
|
||||
finally:
|
||||
self.cleanup_executor(exec_name, executor)
|
||||
return result
|
||||
@@ -1,109 +0,0 @@
|
||||
import logging
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Generator, Optional, Tuple
|
||||
|
||||
from ...stash import ExpStashEntry
|
||||
from ..base import BaseExecutor
|
||||
from ..ssh import SSHExecutor, _sshfs
|
||||
from .base import BaseExecutorManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SSHExecutorManager(BaseExecutorManager):
|
||||
EXECUTOR_CLS = SSHExecutor
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
username: Optional[str] = None,
|
||||
fs_factory: Optional[Callable] = None,
|
||||
**kwargs,
|
||||
):
|
||||
assert host
|
||||
super().__init__(scm, wdir, **kwargs)
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.username = username
|
||||
self._fs_factory = fs_factory
|
||||
|
||||
def _load_infos(self) -> Generator[Tuple[str, "BaseExecutor"], None, None]:
|
||||
# TODO: load existing infos using sshfs
|
||||
yield from []
|
||||
|
||||
@classmethod
|
||||
def from_stash_entries(
|
||||
cls,
|
||||
scm: "Git",
|
||||
wdir: str,
|
||||
repo: "Repo",
|
||||
to_run: Dict[str, ExpStashEntry],
|
||||
**kwargs,
|
||||
):
|
||||
machine_name: Optional[str] = kwargs.get("machine_name", None)
|
||||
manager = cls(
|
||||
scm, wdir, **repo.machine.get_executor_kwargs(machine_name)
|
||||
)
|
||||
manager._enqueue_stash_entries(scm, repo, to_run, **kwargs)
|
||||
return manager
|
||||
|
||||
def sshfs(self):
|
||||
return _sshfs(self._fs_factory, host=self.host, port=self.port)
|
||||
|
||||
def get_infofile_path(self, name: str) -> str:
|
||||
return f"{name}{BaseExecutor.INFOFILE_EXT}"
|
||||
|
||||
def _exec_attached(self, repo: "Repo", jobs: Optional[int] = 1):
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
assert len(self._queue) == 1
|
||||
result: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
rev, executor = self._queue.popleft()
|
||||
info = executor.info
|
||||
infofile = posixpath.join(
|
||||
info.root_dir,
|
||||
info.dvc_dir,
|
||||
"tmp",
|
||||
self.get_infofile_path(rev),
|
||||
)
|
||||
try:
|
||||
exec_result = executor.reproduce(
|
||||
info=executor.info,
|
||||
rev=rev,
|
||||
infofile=infofile,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
fs_factory=self._fs_factory,
|
||||
)
|
||||
if not exec_result.exp_hash:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
)
|
||||
if exec_result.ref_info:
|
||||
result[rev].update(
|
||||
self._collect_executor(repo, executor, exec_result)
|
||||
)
|
||||
except CheckpointKilledError:
|
||||
# Checkpoint errors have already been logged
|
||||
return {}
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise DvcException(
|
||||
f"Failed to reproduce experiment '{rev[:7]}'"
|
||||
) from exc
|
||||
finally:
|
||||
self.cleanup_executor(rev, executor)
|
||||
return result
|
||||
|
||||
def cleanup_executor(self, rev: str, executor: "BaseExecutor"):
|
||||
executor.cleanup()
|
||||
@@ -5,10 +5,9 @@ import sys
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional
|
||||
|
||||
from dvc_ssh import SSHFileSystem
|
||||
from funcy import first
|
||||
|
||||
from dvc.fs import SSHFileSystem
|
||||
|
||||
from ..refs import (
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
@@ -16,7 +15,7 @@ from ..refs import (
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
)
|
||||
from .base import BaseExecutor, ExecutorInfo, ExecutorResult
|
||||
from .base import BaseExecutor, ExecutorInfo, ExecutorResult, TaskStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from queue import Queue
|
||||
@@ -24,8 +23,8 @@ if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
from ..base import ExpRefInfo, ExpStashEntry
|
||||
from dvc.repo.experiments.refs import ExpRefInfo
|
||||
from dvc.repo.experiments.stash import ExpStashEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -76,14 +75,12 @@ class SSHExecutor(BaseExecutor):
|
||||
def from_stash_entry(
|
||||
cls,
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
**kwargs,
|
||||
):
|
||||
machine_name: Optional[str] = kwargs.pop("machine_name", None)
|
||||
executor = cls._from_stash_entry(
|
||||
repo,
|
||||
stash_rev,
|
||||
entry,
|
||||
cls.gen_dirname(entry.name),
|
||||
location=machine_name,
|
||||
@@ -120,9 +117,20 @@ class SSHExecutor(BaseExecutor):
|
||||
}
|
||||
return kwargs
|
||||
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
def init_git(
|
||||
self,
|
||||
scm: "Git",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
infofile: Optional[str],
|
||||
branch: Optional[str] = None,
|
||||
):
|
||||
from ..utils import push_refspec
|
||||
|
||||
self.status = TaskStatus.PREPARING
|
||||
if infofile:
|
||||
self.info.dump_json(infofile)
|
||||
|
||||
with self.sshfs() as fs:
|
||||
fs.makedirs(self.root_dir)
|
||||
self._ssh_cmd(fs, "git init .")
|
||||
@@ -138,8 +146,11 @@ class SSHExecutor(BaseExecutor):
|
||||
# TODO: support multiple client key retries in git backends
|
||||
# (see https://github.com/iterative/dvc/issues/6508)
|
||||
kwargs = self._git_client_args(fs)
|
||||
refspec = f"{EXEC_NAMESPACE}/"
|
||||
push_refspec(scm, self.git_url, refspec, refspec, **kwargs)
|
||||
|
||||
with self.set_exec_refs(scm, stash_rev, entry):
|
||||
refspec = f"{EXEC_NAMESPACE}/"
|
||||
push_refspec(scm, self.git_url, refspec, refspec, **kwargs)
|
||||
|
||||
if branch:
|
||||
push_refspec(scm, self.git_url, branch, branch, **kwargs)
|
||||
self._ssh_cmd(fs, f"git symbolic-ref {EXEC_BRANCH} {branch}")
|
||||
|
||||
@@ -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,11 +17,12 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import cached_property
|
||||
from funcy import cached_property, retry
|
||||
|
||||
from dvc.dependency.param import MissingParamsError
|
||||
from dvc.dependency import ParamsDependency
|
||||
from dvc.env import DVCLIVE_RESUME
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.lock import LockError
|
||||
from dvc.ui import ui
|
||||
|
||||
from ..exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
@@ -32,7 +33,7 @@ from ..executor.base import (
|
||||
ExecutorResult,
|
||||
)
|
||||
from ..executor.local import WorkspaceExecutor
|
||||
from ..refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
|
||||
from ..refs import ExpRefInfo
|
||||
from ..stash import ExpStash, ExpStashEntry
|
||||
from ..utils import exp_refs_by_rev, scm_locked
|
||||
|
||||
@@ -185,9 +186,12 @@ class BaseStashQueue(ABC):
|
||||
|
||||
def _format_entry(
|
||||
entry: QueueEntry,
|
||||
exp_result: Optional[ExecutorResult] = None,
|
||||
status: str = "Unknown",
|
||||
) -> Dict[str, Any]:
|
||||
name = entry.name
|
||||
if not name and exp_result and exp_result.ref_info:
|
||||
name = exp_result.ref_info.name
|
||||
# NOTE: We fallback to Unknown status for experiments
|
||||
# generated in prior (incompatible) DVC versions
|
||||
return {
|
||||
@@ -210,8 +214,8 @@ class BaseStashQueue(ABC):
|
||||
for queue_entry, _ in self.iter_failed()
|
||||
)
|
||||
result.extend(
|
||||
_format_entry(queue_entry, status="Success")
|
||||
for queue_entry, _ in self.iter_success()
|
||||
_format_entry(queue_entry, exp_result=exp_result, status="Success")
|
||||
for queue_entry, exp_result in self.iter_success()
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -264,6 +268,7 @@ class BaseStashQueue(ABC):
|
||||
finish any active experiments before shutting down.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def logs(
|
||||
self,
|
||||
rev: str,
|
||||
@@ -283,7 +288,7 @@ class BaseStashQueue(ABC):
|
||||
def _stash_exp(
|
||||
self,
|
||||
*args,
|
||||
params: Optional[dict] = None,
|
||||
params: Optional[Dict[str, List[str]]] = None,
|
||||
resume_rev: Optional[str] = None,
|
||||
baseline_rev: Optional[str] = None,
|
||||
branch: Optional[str] = None,
|
||||
@@ -292,10 +297,9 @@ class BaseStashQueue(ABC):
|
||||
) -> QueueEntry:
|
||||
"""Stash changes from the workspace as an experiment.
|
||||
|
||||
Arguments:
|
||||
params: Optional dictionary of parameter values to be used.
|
||||
Values take priority over any parameters specified in the
|
||||
user's workspace.
|
||||
Args:
|
||||
params: Dict mapping paths to `Hydra Override`_ patterns,
|
||||
provided via `exp run --set-param`.
|
||||
resume_rev: Optional checkpoint resume rev.
|
||||
baseline_rev: Optional baseline rev for this experiment, defaults
|
||||
to the current SCM rev.
|
||||
@@ -305,6 +309,9 @@ class BaseStashQueue(ABC):
|
||||
name: Optional experiment name. If specified this will be used as
|
||||
the human-readable name in the experiment branch ref. Has no
|
||||
effect of branch is specified.
|
||||
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/next/advanced/override_grammar/basic/
|
||||
"""
|
||||
with self.scm.detach_head(client="dvc") as orig_head:
|
||||
stash_head = orig_head
|
||||
@@ -508,22 +515,37 @@ class BaseStashQueue(ABC):
|
||||
f"from '{config_path}': {param_list}"
|
||||
)
|
||||
|
||||
def _update_params(self, params: dict):
|
||||
"""Update experiment params files with the specified values."""
|
||||
from dvc.utils.collections import NewParamsFound, merge_params
|
||||
from dvc.utils.serialize import MODIFIERS
|
||||
def _update_params(self, params: Dict[str, List[str]]):
|
||||
"""Update param files with the provided `Hydra Override`_ patterns.
|
||||
|
||||
Args:
|
||||
params: Dict mapping paths to `Hydra Override`_ patterns,
|
||||
provided via `exp run --set-param`.
|
||||
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/advanced/override_grammar/basic/
|
||||
"""
|
||||
from dvc.utils.hydra import apply_overrides, compose_and_dump
|
||||
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
|
||||
for path in params:
|
||||
suffix = self.repo.fs.path.suffix(path).lower()
|
||||
modify_data = MODIFIERS[suffix]
|
||||
with modify_data(path, fs=self.repo.fs) as data:
|
||||
try:
|
||||
merge_params(data, params[path], allow_new=False)
|
||||
except NewParamsFound as e:
|
||||
msg = self._format_new_params_msg(e.new_params, path)
|
||||
raise MissingParamsError(msg)
|
||||
hydra_config = self.repo.config.get("hydra", {})
|
||||
hydra_enabled = hydra_config.get("enabled", False)
|
||||
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
for path, overrides in params.items():
|
||||
if hydra_enabled and path == hydra_output_file:
|
||||
config_dir = os.path.join(
|
||||
self.repo.root_dir, hydra_config.get("config_dir", "conf")
|
||||
)
|
||||
config_name = hydra_config.get("config_name", "config")
|
||||
compose_and_dump(
|
||||
path,
|
||||
config_dir,
|
||||
config_name,
|
||||
overrides,
|
||||
)
|
||||
else:
|
||||
apply_overrides(path, overrides)
|
||||
|
||||
# Force params file changes to be staged in git
|
||||
# Otherwise in certain situations the changes to params file may be
|
||||
@@ -532,8 +554,9 @@ class BaseStashQueue(ABC):
|
||||
self.scm.add(list(params.keys()))
|
||||
|
||||
@staticmethod
|
||||
@retry(180, errors=LockError, timeout=1)
|
||||
@scm_locked
|
||||
def setup_executor(
|
||||
def init_executor(
|
||||
exp: "Experiments",
|
||||
queue_entry: QueueEntry,
|
||||
executor_cls: Type[BaseExecutor] = WorkspaceExecutor,
|
||||
@@ -548,21 +571,23 @@ class BaseStashQueue(ABC):
|
||||
)
|
||||
if stash_entry.stash_index is not None:
|
||||
stash.drop(stash_entry.stash_index)
|
||||
|
||||
scm.set_ref(EXEC_HEAD, stash_entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, stash_entry.baseline_rev)
|
||||
|
||||
# Executor will be initialized with an empty git repo that
|
||||
# we populate by pushing:
|
||||
# EXEC_HEAD - the base commit for this experiment
|
||||
# EXEC_MERGE - the unmerged changes (from our stash)
|
||||
# to be reproduced
|
||||
# EXEC_BASELINE - the baseline commit for this experiment
|
||||
return executor_cls.from_stash_entry(
|
||||
exp.repo, stash_rev, stash_entry, **kwargs
|
||||
executor = executor_cls.from_stash_entry(
|
||||
exp.repo, stash_entry, **kwargs
|
||||
)
|
||||
|
||||
infofile = exp.celery_queue.get_infofile_path(stash_rev)
|
||||
executor.init_git(
|
||||
exp.repo.scm,
|
||||
stash_rev,
|
||||
stash_entry,
|
||||
infofile,
|
||||
branch=stash_entry.branch,
|
||||
)
|
||||
|
||||
executor.init_cache(exp.repo, stash_rev)
|
||||
|
||||
return executor
|
||||
|
||||
def get_infofile_path(self, name: str) -> str:
|
||||
return os.path.join(
|
||||
self.pid_dir,
|
||||
@@ -571,8 +596,9 @@ class BaseStashQueue(ABC):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@retry(180, errors=LockError, timeout=1)
|
||||
@scm_locked
|
||||
def collect_executor(
|
||||
def collect_git(
|
||||
exp: "Experiments",
|
||||
executor: BaseExecutor,
|
||||
exec_result: ExecutorResult,
|
||||
@@ -596,6 +622,17 @@ class BaseStashQueue(ABC):
|
||||
logger.debug("Collected experiment '%s'.", exp_rev[:7])
|
||||
results[exp_rev] = exec_result.exp_hash
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def collect_executor(
|
||||
cls,
|
||||
exp: "Experiments",
|
||||
executor: BaseExecutor,
|
||||
exec_result: ExecutorResult,
|
||||
) -> Dict[str, str]:
|
||||
results = cls.collect_git(exp, executor, exec_result)
|
||||
|
||||
if exec_result.ref_info is not None:
|
||||
executor.collect_cache(exp.repo, exec_result.ref_info)
|
||||
|
||||
@@ -662,3 +699,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.
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,8 @@ from typing import (
|
||||
Set,
|
||||
)
|
||||
|
||||
from funcy import cached_property
|
||||
from celery.result import AsyncResult
|
||||
from funcy import cached_property, first
|
||||
from kombu.message import Message
|
||||
|
||||
from dvc.daemon import daemonize
|
||||
@@ -23,9 +24,10 @@ from dvc.ui import ui
|
||||
|
||||
from ..exceptions import UnresolvedQueueExpNamesError
|
||||
from ..executor.base import EXEC_TMP_DIR, ExecutorInfo, ExecutorResult
|
||||
from ..stash import ExpStashEntry
|
||||
from .base import BaseStashQueue, 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
|
||||
@@ -41,7 +43,7 @@ class _MessageEntry(NamedTuple):
|
||||
|
||||
|
||||
class _TaskEntry(NamedTuple):
|
||||
task_id: str
|
||||
async_result: AsyncResult
|
||||
entry: QueueEntry
|
||||
|
||||
|
||||
@@ -181,6 +183,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]:
|
||||
@@ -192,30 +195,39 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
yield _MessageEntry(msg, QueueEntry.from_dict(entry_dict))
|
||||
|
||||
def _iter_active_tasks(self) -> Generator[_TaskEntry, None, None]:
|
||||
from celery.result import AsyncResult
|
||||
|
||||
for msg, entry in self._iter_processed():
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
yield _TaskEntry(task_id, entry)
|
||||
logger.debug("Found active task %s", entry.stash_rev)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def _iter_done_tasks(self) -> Generator[_TaskEntry, None, None]:
|
||||
from celery.result import AsyncResult
|
||||
|
||||
for msg, entry in self._iter_processed():
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if result.ready():
|
||||
yield _TaskEntry(task_id, entry)
|
||||
logger.debug("Found done task %s", entry.stash_rev)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def iter_active(self) -> Generator[QueueEntry, None, None]:
|
||||
for _, entry in self._iter_active_tasks():
|
||||
yield entry
|
||||
|
||||
def iter_done(self) -> Generator[QueueDoneResult, None, None]:
|
||||
for _, entry in self._iter_done_tasks():
|
||||
yield QueueDoneResult(entry, self.get_result(entry))
|
||||
for result, entry in self._iter_done_tasks():
|
||||
try:
|
||||
exp_result = self.get_result(entry)
|
||||
except FileNotFoundError:
|
||||
if result.status == "SUCCESS":
|
||||
raise DvcException(
|
||||
f"Invalid experiment '{entry.stash_rev[:7]}'."
|
||||
)
|
||||
elif result.status == "FAILURE":
|
||||
exp_result = None
|
||||
yield QueueDoneResult(entry, exp_result)
|
||||
|
||||
def iter_success(self) -> Generator[QueueDoneResult, None, None]:
|
||||
for queue_entry, exp_result in self.iter_done():
|
||||
@@ -223,84 +235,122 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
yield QueueDoneResult(queue_entry, exp_result)
|
||||
|
||||
def iter_failed(self) -> Generator[QueueDoneResult, None, None]:
|
||||
failed_revs: Dict[str, ExpStashEntry] = (
|
||||
dict(self.failed_stash.stash_revs)
|
||||
if self.failed_stash is not None
|
||||
else {}
|
||||
)
|
||||
|
||||
for queue_entry, exp_result in self.iter_done():
|
||||
if exp_result is None and queue_entry.stash_rev in failed_revs:
|
||||
if exp_result is None:
|
||||
yield QueueDoneResult(queue_entry, exp_result)
|
||||
|
||||
def reproduce(self) -> Mapping[str, Mapping[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
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"]:
|
||||
from celery.exceptions import TimeoutError as _CeleryTimeout
|
||||
|
||||
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
|
||||
|
||||
def get_result(
|
||||
self, entry: QueueEntry, timeout: Optional[float] = None
|
||||
) -> Optional[ExecutorResult]:
|
||||
from celery.exceptions import TimeoutError as _CeleryTimeout
|
||||
from celery.result import AsyncResult
|
||||
|
||||
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:
|
||||
return executor_info.result
|
||||
raise FileNotFoundError
|
||||
) -> 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 task_id, active_entry in self._iter_active_tasks():
|
||||
if entry.stash_rev == active_entry.stash_rev:
|
||||
logger.debug("Waiting for exp task '%s' to complete", task_id)
|
||||
try:
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
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.
|
||||
try:
|
||||
return _load_collected(entry.stash_rev)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
raise DvcException(f"Invalid experiment '{entry.stash_rev[:7]}'.")
|
||||
return self._get_done_result(entry, timeout)
|
||||
|
||||
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]
|
||||
) -> Dict[QueueEntry, str]:
|
||||
fail_to_kill_entries: Dict[QueueEntry, str] = {}
|
||||
for queue_entry, rev in to_kill.items():
|
||||
try:
|
||||
self.proc.kill(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(self, revs: Collection[str]) -> None:
|
||||
to_kill: Set[QueueEntry] = set()
|
||||
name_dict: Dict[
|
||||
str, Optional[QueueEntry]
|
||||
] = self.match_queue_entry_by_name(set(revs), self.iter_active())
|
||||
|
||||
missing_rev: List[str] = []
|
||||
to_kill: Dict[QueueEntry, str] = {}
|
||||
missing_revs: List[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_revs:
|
||||
raise UnresolvedQueueExpNamesError(missing_revs)
|
||||
|
||||
if missing_rev:
|
||||
raise UnresolvedQueueExpNamesError(missing_rev)
|
||||
fail_to_kill_entries: Dict[QueueEntry, str] = self._try_to_kill_tasks(
|
||||
to_kill
|
||||
)
|
||||
|
||||
for queue_entry in to_kill:
|
||||
self.proc.kill(queue_entry.stash_rev)
|
||||
if fail_to_kill_entries:
|
||||
self._mark_inactive_tasks_failure(fail_to_kill_entries)
|
||||
|
||||
def shutdown(self, kill: bool = False):
|
||||
self.celery.control.shutdown()
|
||||
@@ -356,10 +406,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):
|
||||
@@ -371,3 +422,18 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
from .remove import celery_remove
|
||||
|
||||
return celery_remove(self, *args, **kwargs)
|
||||
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Dict]:
|
||||
"""Get the execution info of the currently running experiments
|
||||
|
||||
Args:
|
||||
fetch_ref (bool): fetch completed checkpoints or not.
|
||||
"""
|
||||
result: Dict[str, Dict] = {}
|
||||
for entry in self.iter_active():
|
||||
result.update(
|
||||
fetch_running_exp_from_temp_dir(
|
||||
self, entry.stash_rev, fetch_refs
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
@@ -3,8 +3,6 @@ from typing import Any, Dict
|
||||
from celery import shared_task
|
||||
from celery.utils.log import get_task_logger
|
||||
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
from ..executor.base import ExecutorInfo
|
||||
from ..executor.local import TempDirExecutor
|
||||
from .base import BaseStashQueue, QueueEntry
|
||||
@@ -13,7 +11,7 @@ logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
@shared_task
|
||||
def setup_exp(entry_dict: Dict[str, Any]) -> str:
|
||||
def setup_exp(entry_dict: Dict[str, Any]) -> TempDirExecutor:
|
||||
"""Setup an experiment.
|
||||
|
||||
Arguments:
|
||||
@@ -28,7 +26,7 @@ def setup_exp(entry_dict: Dict[str, Any]) -> str:
|
||||
repo = Repo(entry.dvc_root)
|
||||
# TODO: split executor.init_cache into separate subtask - we can release
|
||||
# exp.scm_lock before DVC push
|
||||
executor = BaseStashQueue.setup_executor(
|
||||
executor = BaseStashQueue.init_executor(
|
||||
repo.experiments,
|
||||
entry,
|
||||
TempDirExecutor,
|
||||
@@ -36,7 +34,7 @@ def setup_exp(entry_dict: Dict[str, Any]) -> str:
|
||||
)
|
||||
infofile = repo.experiments.celery_queue.get_infofile_path(entry.stash_rev)
|
||||
executor.info.dump_json(infofile)
|
||||
return executor.root_dir
|
||||
return executor
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -79,25 +77,14 @@ def collect_exp(
|
||||
|
||||
|
||||
@shared_task
|
||||
def cleanup_exp( # pylint: disable=unused-argument
|
||||
tmp_dir: str, entry_dict: Dict[str, Any]
|
||||
) -> None:
|
||||
def cleanup_exp(executor: TempDirExecutor, infofile: str) -> None:
|
||||
"""Cleanup after an experiment.
|
||||
|
||||
Arguments:
|
||||
tmp_dir: Temp directory to be removed.
|
||||
entry_dict: Serialized QueueEntry for this experiment.
|
||||
"""
|
||||
remove(tmp_dir)
|
||||
|
||||
|
||||
def _set_collected(infofile: str):
|
||||
try:
|
||||
executor_info = ExecutorInfo.load_json(infofile)
|
||||
executor_info.collected = True
|
||||
executor_info.dump_json(infofile)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
executor.cleanup(infofile)
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -115,11 +102,10 @@ def run_exp(entry_dict: Dict[str, Any]) -> None:
|
||||
repo = Repo(entry.dvc_root)
|
||||
queue = repo.experiments.celery_queue
|
||||
infofile = queue.get_infofile_path(entry.stash_rev)
|
||||
root_dir = setup_exp.s(entry_dict)()
|
||||
executor = setup_exp.s(entry_dict)()
|
||||
try:
|
||||
cmd = ["dvc", "exp", "exec-run", "--infofile", infofile]
|
||||
proc_dict = queue.proc.run_signature(cmd, name=entry.stash_rev)()
|
||||
collect_exp.s(proc_dict, entry_dict)()
|
||||
finally:
|
||||
cleanup_exp.s(root_dir, entry_dict)()
|
||||
_set_collected(infofile)
|
||||
cleanup_exp.s(executor, infofile)()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user