mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-10 15:28:34 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8be68436f | ||
|
|
f6adc49089 | ||
|
|
a4c0ae9c73 | ||
|
|
8b3020aaf3 | ||
|
|
182a22b8f6 | ||
|
|
892b235d0b | ||
|
|
da93e9a76c | ||
|
|
bd93d85d47 | ||
|
|
eed6a8483c | ||
|
|
954de0cae4 | ||
|
|
909941308b | ||
|
|
c59f935cc5 | ||
|
|
03b789e206 | ||
|
|
cacc2f14cb | ||
|
|
0d71194741 | ||
|
|
683d22c42b | ||
|
|
c687d1bf21 | ||
|
|
fbb98a87d1 | ||
|
|
f42beb4433 | ||
|
|
de8ab044f8 | ||
|
|
3c76885577 | ||
|
|
c347d9f84b | ||
|
|
efc4787cb0 | ||
|
|
be3ec9dc59 | ||
|
|
685a2d5579 | ||
|
|
c336507e51 | ||
|
|
d448eb5fc1 | ||
|
|
3dcd0107d2 | ||
|
|
2773e99afc | ||
|
|
9a1a3a05f5 | ||
|
|
286fab4aab | ||
|
|
20f69a121e | ||
|
|
b994c529a2 | ||
|
|
f106a68424 | ||
|
|
66c2e8cbd8 | ||
|
|
735b563ce4 | ||
|
|
604e0178f7 | ||
|
|
f210e41d5a | ||
|
|
e07e79da52 | ||
|
|
1284842f70 | ||
|
|
0aa063a1e7 | ||
|
|
87c5cb7ebf | ||
|
|
59570c08a4 | ||
|
|
eafe686dd6 | ||
|
|
d056d8a4c5 | ||
|
|
165d95be56 | ||
|
|
ea1c54f48e | ||
|
|
31f67cd3de | ||
|
|
ee87e2b260 | ||
|
|
ae61143956 | ||
|
|
1253e6cc46 | ||
|
|
19f2186c8b | ||
|
|
2e21c92c4f | ||
|
|
4262e26a15 | ||
|
|
830c116b4e | ||
|
|
1663b97eaa | ||
|
|
15e10a1d81 | ||
|
|
4bc6b5699a | ||
|
|
8e003e5022 | ||
|
|
af649af462 | ||
|
|
957d8d179b | ||
|
|
e7b265a178 | ||
|
|
422511f66d | ||
|
|
6826d8ac35 | ||
|
|
bdea9847b5 | ||
|
|
8e6ea87b76 | ||
|
|
036ac8e7c2 | ||
|
|
3306d2789b | ||
|
|
b094839210 | ||
|
|
b9c04a247a | ||
|
|
ef689c5593 | ||
|
|
7b16a377e5 | ||
|
|
793f97bdd5 | ||
|
|
d30dbd50bc | ||
|
|
e6db6ffc89 | ||
|
|
9ac2f49b57 | ||
|
|
9b2adfd3cc | ||
|
|
6dd3f1e053 | ||
|
|
677da2b22c | ||
|
|
a3ee993739 | ||
|
|
95f67445de | ||
|
|
c9a3cb803c | ||
|
|
41ecd2e204 | ||
|
|
33b3afa2f5 | ||
|
|
e849162018 | ||
|
|
c14f96391a | ||
|
|
88d3582d40 | ||
|
|
8fa0e40669 | ||
|
|
a3d6b1299a | ||
|
|
20c7b0e3e6 | ||
|
|
6794dd28f7 | ||
|
|
1cc803d12b | ||
|
|
2b6aa01392 | ||
|
|
906c87fdef | ||
|
|
52f02d238d | ||
|
|
3d791778de | ||
|
|
1e77be3ae3 | ||
|
|
844f77b4f9 | ||
|
|
34ef1807be | ||
|
|
dae0000bcd | ||
|
|
109d0e4e28 | ||
|
|
68b0d61c9e | ||
|
|
942a080567 | ||
|
|
60cbcec399 | ||
|
|
0c5a18513c | ||
|
|
b5c28dd9f9 | ||
|
|
84439e6a04 | ||
|
|
75fa487406 | ||
|
|
0ecd47b333 | ||
|
|
9710cafd38 | ||
|
|
e34767ca90 | ||
|
|
9a2feaa4ed | ||
|
|
7bc98e3fb3 | ||
|
|
5e561c1dfd | ||
|
|
572b6dfca9 | ||
|
|
37921d618d | ||
|
|
95c6cea36d | ||
|
|
455260b29c | ||
|
|
82dc67d890 | ||
|
|
2057c471bb | ||
|
|
f23d31af64 | ||
|
|
cf5d3830c5 | ||
|
|
32cd6c2c03 | ||
|
|
54917e3255 | ||
|
|
e8a439df8d | ||
|
|
71a4b4823a | ||
|
|
01cc97f38a | ||
|
|
3872f79dad | ||
|
|
48176b80f6 | ||
|
|
8c9a9e9a1f | ||
|
|
120eb09c4d | ||
|
|
bf7892012d | ||
|
|
36605eea4c | ||
|
|
01e5f9eadd | ||
|
|
752e10a834 | ||
|
|
092eda183c | ||
|
|
d82e094b13 | ||
|
|
570e3a5359 | ||
|
|
b8e770747f | ||
|
|
1999ecf0d3 | ||
|
|
1ff5f30839 | ||
|
|
c6ae91c907 | ||
|
|
dc1d3e880a | ||
|
|
3f5757a0a5 | ||
|
|
7a795f79db | ||
|
|
b3fb0dfdad | ||
|
|
ee29888e18 | ||
|
|
11eaa87849 | ||
|
|
b4faa5616f | ||
|
|
4d6df989f2 | ||
|
|
e585c52187 | ||
|
|
4add88f627 | ||
|
|
aac2fc0ca0 | ||
|
|
005d56eedf | ||
|
|
b6d25825c8 | ||
|
|
799dca8b67 | ||
|
|
3faf413288 | ||
|
|
a3261637de | ||
|
|
b8d738220d | ||
|
|
fb36db450f | ||
|
|
946ceb288d | ||
|
|
8d99ce06aa | ||
|
|
9b3873c4f0 | ||
|
|
e177f041c8 | ||
|
|
a80a85e6bd | ||
|
|
14c9348f93 | ||
|
|
7a1f133f29 | ||
|
|
45a01c6a30 | ||
|
|
09afd2a0cd | ||
|
|
d92c6fed67 | ||
|
|
f10dbef0cf | ||
|
|
8fb2f65d4b | ||
|
|
477f266dc5 | ||
|
|
b14a972cd0 | ||
|
|
562e61235b | ||
|
|
9bd9ac4a37 | ||
|
|
f89f4ac152 | ||
|
|
54dd3ae90c | ||
|
|
35d2963069 | ||
|
|
1f5f2f5c97 | ||
|
|
c64db917f5 | ||
|
|
9d476aab8b | ||
|
|
5067e1f5bc | ||
|
|
8a0c5680e0 | ||
|
|
61699d1d66 | ||
|
|
94c776182e | ||
|
|
e3d3500db8 | ||
|
|
d0e05c4b66 | ||
|
|
58d960a47c | ||
|
|
dd187df667 | ||
|
|
b71ea349d2 | ||
|
|
c81d841fd7 | ||
|
|
127a9c74b2 | ||
|
|
e30d2fdda5 | ||
|
|
ae4f56c5ad | ||
|
|
8cdcf9b8bb | ||
|
|
55a4532f1d | ||
|
|
43b9cab01b | ||
|
|
36d53bd403 | ||
|
|
46fac33ba5 | ||
|
|
8b68e4fa75 | ||
|
|
4f1fdf0b0c | ||
|
|
0d48598e4b | ||
|
|
5d64f6504f | ||
|
|
3d54039614 | ||
|
|
cda855b97a | ||
|
|
bea7d56bd5 | ||
|
|
10bcc9971d | ||
|
|
700b648da9 | ||
|
|
f84f9cf184 | ||
|
|
b53ebc1a5e | ||
|
|
31e8eed402 | ||
|
|
112ea1d448 | ||
|
|
f39e35445a | ||
|
|
7211bd02ed | ||
|
|
83795ff3df | ||
|
|
8ead29805f | ||
|
|
fecc81e951 | ||
|
|
7599c6e20d | ||
|
|
68a3cd9a13 | ||
|
|
a394498067 | ||
|
|
7708a8e292 | ||
|
|
23e62a9ff7 | ||
|
|
fed0bfb2b3 | ||
|
|
f22da81999 | ||
|
|
22a988b113 | ||
|
|
77a8e870ff | ||
|
|
f20d6a7048 | ||
|
|
6ffba62b75 | ||
|
|
5c561572b0 | ||
|
|
444bf27e6f | ||
|
|
ba82faee53 | ||
|
|
72d57f13bc | ||
|
|
02b2ab6804 | ||
|
|
fd440df5b3 | ||
|
|
cc8a0ce134 | ||
|
|
5c1e7be909 | ||
|
|
179e080c2f | ||
|
|
f3a7ec7f48 | ||
|
|
c0580c740b | ||
|
|
b4dcadbc9c | ||
|
|
55219e9089 | ||
|
|
70b57d4703 | ||
|
|
5eb16ab05e | ||
|
|
517f45d45f | ||
|
|
53c7f44480 | ||
|
|
9d75e8261b | ||
|
|
b3b0ec1fba | ||
|
|
0a3f5cbdd5 | ||
|
|
bdd21fd481 | ||
|
|
394118ef3d | ||
|
|
f500c66aef | ||
|
|
318235f072 | ||
|
|
ab395dfb7a | ||
|
|
dd9bc116a3 | ||
|
|
0a3dd7a97f | ||
|
|
5da72fc8ff | ||
|
|
d8dac6f50d | ||
|
|
36c27ed17f | ||
|
|
43f5faa652 | ||
|
|
968f9b38bf | ||
|
|
aab7f8af4d | ||
|
|
79c87ba119 | ||
|
|
aa5cfc46ed | ||
|
|
69acbe33c9 | ||
|
|
7dd0cffd49 | ||
|
|
a11aac37ea | ||
|
|
6de1ddadf5 | ||
|
|
7804e6550e | ||
|
|
65d935e5c7 | ||
|
|
44af5b3b7e | ||
|
|
03f553c54b | ||
|
|
1e0a4a26eb | ||
|
|
ee0a47f166 | ||
|
|
fa7b371c74 | ||
|
|
117b22d58a | ||
|
|
bfb1b74923 | ||
|
|
d600eeca99 | ||
|
|
0470e164cc | ||
|
|
0ab86c8fae | ||
|
|
067442f348 | ||
|
|
b58ebff5db | ||
|
|
565a813e2a | ||
|
|
dc0bc0c535 | ||
|
|
65397517ca | ||
|
|
1a3a222239 | ||
|
|
5ff1d76826 | ||
|
|
582c4afc38 | ||
|
|
b1284cb4af | ||
|
|
7d0d6a38d7 | ||
|
|
700654cb1e | ||
|
|
5a7790dcd5 | ||
|
|
ead7724824 | ||
|
|
ddd6c97f71 | ||
|
|
b71a0f30fb | ||
|
|
9930551458 | ||
|
|
3322980ff6 | ||
|
|
23a86c9a15 | ||
|
|
57419273ea | ||
|
|
879c9bd7d6 | ||
|
|
d09d39538a | ||
|
|
b684b2b25a | ||
|
|
7de443d8bc | ||
|
|
d008054ea2 | ||
|
|
b92bc3e01a | ||
|
|
c08aa4a93b | ||
|
|
81eee05995 | ||
|
|
325ead5e8b | ||
|
|
5cf59e4ee6 | ||
|
|
290a593322 | ||
|
|
19dcba9db6 | ||
|
|
ab54b5bdfc | ||
|
|
2e21be992d | ||
|
|
9d96ddfbe7 | ||
|
|
a3bab48f41 | ||
|
|
a13f2770ef | ||
|
|
079714772c | ||
|
|
cd65400d83 | ||
|
|
0d06a98c67 | ||
|
|
3fc8dcfc59 | ||
|
|
e80e725fe4 | ||
|
|
6a6955e27c | ||
|
|
efc021da73 | ||
|
|
244637e299 | ||
|
|
d033345860 | ||
|
|
de390419a6 | ||
|
|
d80c43135f | ||
|
|
5249207ceb | ||
|
|
2ebf9cb9b9 | ||
|
|
17899bc109 | ||
|
|
380c65134e | ||
|
|
ab024a47b3 | ||
|
|
fc3f3ca8d7 | ||
|
|
6bb3794f52 | ||
|
|
a2e25cf449 | ||
|
|
59b8498905 | ||
|
|
a378e017e1 | ||
|
|
4c71aadc21 | ||
|
|
3b4d76df42 | ||
|
|
6c04d0efd1 | ||
|
|
f41651c97c | ||
|
|
fcddf080da | ||
|
|
82b0a3088f | ||
|
|
30fa3fc41b | ||
|
|
d8fa55bafa | ||
|
|
220c633497 | ||
|
|
3767b03784 | ||
|
|
c0f63efe99 | ||
|
|
0b18efb7b7 | ||
|
|
497eac952c | ||
|
|
6eb5c31dd7 | ||
|
|
69ce69e427 | ||
|
|
f8467cf375 | ||
|
|
2735fbac71 | ||
|
|
795fe24a5f | ||
|
|
d1f7cf4540 | ||
|
|
d5b809d705 | ||
|
|
4db9d19387 | ||
|
|
31a3461465 | ||
|
|
e6d52ac594 | ||
|
|
a78d63cf58 | ||
|
|
d89555cd31 | ||
|
|
b6fa1178e7 | ||
|
|
e430693fc8 | ||
|
|
e60ab00f51 | ||
|
|
cb584c6a53 | ||
|
|
9b5ccb7554 | ||
|
|
27b090709b | ||
|
|
48641fb41f | ||
|
|
8497edab93 | ||
|
|
dc13179759 | ||
|
|
37cfb8e8af | ||
|
|
7bc14a5ddb | ||
|
|
22ebfd1e2f | ||
|
|
d213ccb686 | ||
|
|
0ce1f982b0 | ||
|
|
a4b3da6acc | ||
|
|
b58a01cfd5 | ||
|
|
0562b4fd5f | ||
|
|
95731e9a37 | ||
|
|
aca6d229da | ||
|
|
60e502d361 | ||
|
|
8c2f8095da | ||
|
|
5e80fcce4f | ||
|
|
1a1e28282d | ||
|
|
e3b97cb3a7 | ||
|
|
d882e21b9b | ||
|
|
3276c76a02 | ||
|
|
758dc43575 | ||
|
|
f1dc9e50a7 | ||
|
|
2f15f30b0e | ||
|
|
47d10c4a8b | ||
|
|
822b501069 | ||
|
|
943481f0a3 | ||
|
|
b30385885b | ||
|
|
d4861d535a | ||
|
|
646097a735 | ||
|
|
43953ace53 | ||
|
|
255e6a1aec | ||
|
|
73fee513b9 | ||
|
|
b0f65cbd17 | ||
|
|
672f079cc6 | ||
|
|
446516d788 | ||
|
|
1b3e1251e6 | ||
|
|
4268b5a339 | ||
|
|
f5baf990cf | ||
|
|
69ff760e4b | ||
|
|
4ba504be3e | ||
|
|
377b0bc689 | ||
|
|
89f2d42df8 | ||
|
|
57263f15fa | ||
|
|
9bb6494e55 | ||
|
|
13238e9716 | ||
|
|
85181e396f | ||
|
|
4c3c492b90 | ||
|
|
474812195e | ||
|
|
c83612be71 | ||
|
|
161e7892ba | ||
|
|
0a0e3c3f9c | ||
|
|
ac2feb2340 | ||
|
|
dd5d999644 | ||
|
|
3c7695b39d | ||
|
|
367e5d1429 | ||
|
|
fb16cd06e3 | ||
|
|
39eee9b5a6 | ||
|
|
84da78a62b | ||
|
|
4b9d39c901 | ||
|
|
5b8bf52eab | ||
|
|
b673ac24d0 | ||
|
|
87abeaddc2 | ||
|
|
a3dece8236 | ||
|
|
221adf246b | ||
|
|
8681086bfb | ||
|
|
6c3b1370a3 | ||
|
|
9a01d98876 | ||
|
|
e190433249 | ||
|
|
f64018b75e | ||
|
|
92f714e5ca | ||
|
|
634246f81c | ||
|
|
05b5e9dc29 | ||
|
|
a3144482ba | ||
|
|
44a1146915 | ||
|
|
ec3600eb8e | ||
|
|
fe4fefd880 | ||
|
|
b6a2402d27 | ||
|
|
d90fe54df4 | ||
|
|
9e590edb70 | ||
|
|
6bb4889022 | ||
|
|
0a4958bafd | ||
|
|
5a9f861921 | ||
|
|
fc1ab7d3a1 | ||
|
|
760c8fceb3 | ||
|
|
d8e2c14927 | ||
|
|
a73accd071 | ||
|
|
69ac43f131 | ||
|
|
9ee428ef17 | ||
|
|
0e0e1725e6 | ||
|
|
a1609cfb10 | ||
|
|
175ffaeb22 | ||
|
|
025e15d500 | ||
|
|
52a234c5b8 | ||
|
|
8ec1af1cf4 | ||
|
|
35d364a527 | ||
|
|
9a5614d326 | ||
|
|
12a737d905 | ||
|
|
51a6116b92 | ||
|
|
3566b1941c | ||
|
|
ceeda6d307 | ||
|
|
98d7f785e9 | ||
|
|
04e93da0bd | ||
|
|
72aaca4343 | ||
|
|
60cb8322d9 | ||
|
|
b0b9a74207 | ||
|
|
32fcc73078 | ||
|
|
e3596178d4 | ||
|
|
ba40d91de6 | ||
|
|
02cfbcfc32 | ||
|
|
96acb64fec | ||
|
|
1507a6ed99 | ||
|
|
6034f0e36a | ||
|
|
6df1ee87ab | ||
|
|
a6263cffc8 | ||
|
|
8ca3a7b684 | ||
|
|
4d5be46ab4 | ||
|
|
5ee82d4000 | ||
|
|
a5b7c5ea19 | ||
|
|
19b3c82698 | ||
|
|
9bd60f62d7 | ||
|
|
970cbfa64e | ||
|
|
0bf56fe29b | ||
|
|
30d6839a5f | ||
|
|
e24de3a0f3 | ||
|
|
25ef812ecf | ||
|
|
fa819b0b70 | ||
|
|
37f8010b42 | ||
|
|
8319cc50c6 | ||
|
|
98c22e3277 | ||
|
|
3ca8e26169 | ||
|
|
0deab0118a | ||
|
|
72af8b8605 | ||
|
|
ba4955738f | ||
|
|
602713e57b |
@@ -0,0 +1,4 @@
|
|||||||
|
/tests
|
||||||
|
/dvc
|
||||||
|
/.github
|
||||||
|
/bin
|
||||||
@@ -1 +1,4 @@
|
|||||||
|
node: $Format:%H$
|
||||||
|
node-date: $Format:%cI$
|
||||||
|
describe-name: $Format:%(describe:tags=true)$
|
||||||
ref-names: $Format:%D$
|
ref-names: $Format:%D$
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
patreon: DVCorg
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
pull_request_rules:
|
pull_request_rules:
|
||||||
- name: backport patches to 1.11
|
- name: backport patches to 1.11
|
||||||
conditions:
|
conditions:
|
||||||
- base=master
|
- base=main
|
||||||
- label=backport-1.11
|
- label=backport-1.11
|
||||||
actions:
|
actions:
|
||||||
backport:
|
backport:
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ jobs:
|
|||||||
os: [ubuntu-18.04]
|
os: [ubuntu-18.04]
|
||||||
steps:
|
steps:
|
||||||
- name: Cancel Previous Runs
|
- name: Cancel Previous Runs
|
||||||
uses: styfle/cancel-workflow-action@0.9.1
|
uses: styfle/cancel-workflow-action@0.10.0
|
||||||
with:
|
with:
|
||||||
access_token: ${{ github.token }}
|
access_token: ${{ github.token }}
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.8
|
||||||
- uses: iterative/dvc-bench@master
|
- uses: iterative/dvc-bench@main
|
||||||
with:
|
with:
|
||||||
pytest_options: "-k 'test_init or test_help'"
|
pytest_options: "-k 'test_init or test_help'"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Build packages
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- main
|
||||||
release:
|
release:
|
||||||
types: [released, prereleased]
|
types: [released, prereleased]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -10,11 +10,11 @@ jobs:
|
|||||||
deb:
|
deb:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2.2.2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
- name: Set up Ruby 2.4
|
- name: Set up Ruby 2.4
|
||||||
@@ -41,11 +41,11 @@ jobs:
|
|||||||
rpm:
|
rpm:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2.2.2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
- name: Set up Ruby 2.4
|
- name: Set up Ruby 2.4
|
||||||
@@ -72,11 +72,11 @@ jobs:
|
|||||||
osxpkg:
|
osxpkg:
|
||||||
runs-on: macos-10.15
|
runs-on: macos-10.15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2.2.2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
- name: Set up Ruby 2.4
|
- name: Set up Ruby 2.4
|
||||||
@@ -106,11 +106,11 @@ jobs:
|
|||||||
exe:
|
exe:
|
||||||
runs-on: windows-2019
|
runs-on: windows-2019
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Python 3.9
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2.2.2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: 3.9
|
||||||
- name: Install deps
|
- name: Install deps
|
||||||
@@ -136,13 +136,13 @@ jobs:
|
|||||||
pip:
|
pip:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-18.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.8
|
||||||
uses: actions/setup-python@v2.2.2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.8
|
||||||
- name: Install
|
- name: Install
|
||||||
run: pip install ".[all,tests]"
|
run: pip install ".[all,tests]"
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
|
|||||||
@@ -17,23 +17,24 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Cancel Previous Runs
|
- name: Cancel Previous Runs
|
||||||
uses: styfle/cancel-workflow-action@0.9.1
|
uses: styfle/cancel-workflow-action@0.10.0
|
||||||
with:
|
with:
|
||||||
access_token: ${{ github.token }}
|
access_token: ${{ github.token }}
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.8
|
||||||
uses: actions/setup-python@v2.2.2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.8
|
||||||
- name: Install requirements
|
- name: Install requirements
|
||||||
run: |
|
run: |
|
||||||
pip install --upgrade pip setuptools wheel
|
pip install --upgrade pip setuptools wheel
|
||||||
|
pip install collective.checkdocs==0.2
|
||||||
pip install ".[dev]" pre-commit
|
pip install ".[dev]" pre-commit
|
||||||
- name: Check README
|
- name: Check README
|
||||||
run: python setup.py checkdocs
|
run: python setup.py checkdocs
|
||||||
- uses: pre-commit/action@v2.0.3
|
- uses: pre-commit/action@v3.0.0
|
||||||
tests:
|
tests:
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -41,13 +42,13 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||||
pyv: ["3.7", "3.8", "3.9", "3.10"]
|
pyv: ["3.8", "3.9", "3.10"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2.2.2
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.pyv }}
|
python-version: ${{ matrix.pyv }}
|
||||||
- name: get pip cache dir
|
- name: get pip cache dir
|
||||||
@@ -56,7 +57,7 @@ jobs:
|
|||||||
echo "::set-output name=dir::$(pip cache dir)"
|
echo "::set-output name=dir::$(pip cache dir)"
|
||||||
- name: set pip cache
|
- name: set pip cache
|
||||||
id: pip-cache
|
id: pip-cache
|
||||||
uses: actions/cache@v2.1.7
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.pip-cache-dir.outputs.dir }}
|
path: ${{ steps.pip-cache-dir.outputs.dir }}
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}
|
||||||
@@ -67,10 +68,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
pip install --upgrade pip setuptools wheel
|
pip install --upgrade pip setuptools wheel
|
||||||
pip install -e ".[dev]"
|
pip install -e ".[dev]"
|
||||||
- name: setup git
|
|
||||||
run: |
|
|
||||||
git config --global user.email "[email protected]"
|
|
||||||
git config --global user.name "DVC Tester"
|
|
||||||
- name: run tests
|
- name: run tests
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
run: >-
|
run: >-
|
||||||
@@ -78,12 +75,12 @@ jobs:
|
|||||||
--cov-report=xml --cov-report=term
|
--cov-report=xml --cov-report=term
|
||||||
${{ env.extra_test_args }}
|
${{ env.extra_test_args }}
|
||||||
- name: upload coverage report
|
- name: upload coverage report
|
||||||
uses: codecov/codecov-action@v2.1.0
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
file: ./coverage.xml
|
file: ./coverage.xml
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
notify:
|
notify:
|
||||||
if: github.ref == 'refs/heads/master' && failure()
|
if: github.ref == 'refs/heads/main' && failure()
|
||||||
needs:
|
needs:
|
||||||
- lint
|
- lint
|
||||||
- tests
|
- tests
|
||||||
@@ -94,6 +91,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||||
SLACK_COLOR: ${{ job.status }}
|
SLACK_COLOR: ${{ job.status }}
|
||||||
SLACK_MESSAGE: 'CI Failed on master :boom:'
|
SLACK_MESSAGE: 'CI Failed on main :boom:'
|
||||||
SLACK_TITLE: CI Status
|
SLACK_TITLE: CI Status
|
||||||
SLACK_USERNAME: DVC-CI
|
SLACK_USERNAME: DVC-CI
|
||||||
|
|||||||
+13
-3
@@ -1,10 +1,13 @@
|
|||||||
|
__pycache__/
|
||||||
neatlynx.conf
|
neatlynx.conf
|
||||||
neatlynx/__pycache__
|
|
||||||
.idea
|
.idea
|
||||||
/cache
|
/cache
|
||||||
*.pyc
|
*.pyc
|
||||||
.env/
|
|
||||||
.env2.7/
|
.env*/
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
.dvc.conf.lock
|
.dvc.conf.lock
|
||||||
@@ -26,6 +29,7 @@ innosetup/config.ini
|
|||||||
|
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
@@ -40,3 +44,9 @@ scripts/ci/gcp-creds.json
|
|||||||
|
|
||||||
*~
|
*~
|
||||||
/dvc/_dvc_version.py
|
/dvc/_dvc_version.py
|
||||||
|
|
||||||
|
.mypy_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.nox/
|
||||||
|
.tox/
|
||||||
|
htmlcov/
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ repos:
|
|||||||
- id: black
|
- id: black
|
||||||
language_version: python3
|
language_version: python3
|
||||||
repo: https://github.com/ambv/black
|
repo: https://github.com/ambv/black
|
||||||
rev: 21.12b0
|
rev: 22.6.0
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.0.1
|
rev: v4.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
|||||||
+45
-32
@@ -2,37 +2,32 @@
|
|||||||
|
|
||||||
`Website <https://dvc.org>`_
|
`Website <https://dvc.org>`_
|
||||||
• `Docs <https://dvc.org/doc>`_
|
• `Docs <https://dvc.org/doc>`_
|
||||||
|
• `VS Code Extension <https://marketplace.visualstudio.com/items?itemName=Iterative.dvc>`_
|
||||||
• `Blog <http://blog.dataversioncontrol.com>`_
|
• `Blog <http://blog.dataversioncontrol.com>`_
|
||||||
• `Twitter <https://twitter.com/DVCorg>`_
|
• `Twitter <https://twitter.com/DVCorg>`_
|
||||||
• `Chat (Community & Support) <https://dvc.org/chat>`_
|
• `Chat (Community & Support) <https://dvc.org/chat>`_
|
||||||
• `Tutorial <https://dvc.org/doc/get-started>`_
|
• `Tutorial <https://dvc.org/doc/get-started>`_
|
||||||
• `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
|
• `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
|
||||||
|
|
||||||
|CI| |Maintainability| |Coverage| |Donate| |DOI|
|
|CI| |Maintainability| |Coverage| |VS Code| |DOI|
|
||||||
|
|
||||||
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
|
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|
||||||
**Data Version Control** or **DVC** is an **open-source** tool for data science and machine
|
**Data Version Control** or **DVC** is a command line tool and `VS Code Extension <https://marketplace.visualstudio.com/items?itemName=Iterative.dvc>`_ to help you develop reproducible machine learning projects:
|
||||||
learning projects. Key features:
|
|
||||||
|
|
||||||
#. Simple **command line** Git-like experience. Does not require installing and maintaining
|
#. **Version** your data and models. Store them in your cloud storage but keep
|
||||||
any databases. Does not depend on any proprietary online services.
|
their version info in your Git repo.
|
||||||
|
|
||||||
#. Management and versioning of **datasets** and **machine learning
|
#. **Iterate** fast with lightweight pipelines. When you make changes, only run
|
||||||
models**. Data can be saved in S3, Google cloud, Azure, Alibaba
|
the steps impacted by those changes.
|
||||||
cloud, SSH server, HDFS, or even local HDD RAID.
|
|
||||||
|
|
||||||
#. Makes projects **reproducible** and **shareable**; helping to answer questions about how
|
#. **Track** experiments in your local Git repo (no servers needed).
|
||||||
a model was built.
|
|
||||||
|
|
||||||
#. Helps manage experiments with Git tags/branches and **metrics** tracking.
|
#. **Compare** any data, code, parameters, model, or performance plots
|
||||||
|
|
||||||
**DVC** aims to replace spreadsheet and document sharing tools (such as Excel or Google Docs)
|
#. **Share** experiments and automatically reproduce anyone's experiment.
|
||||||
frequently used as both knowledge repositories and team ledgers.
|
|
||||||
DVC also replaces both ad-hoc scripts to track, move, and deploy different model versions
|
|
||||||
and ad-hoc data file suffixes and prefixes.
|
|
||||||
|
|
||||||
.. contents:: **Contents**
|
.. contents:: **Contents**
|
||||||
:backlinks: none
|
:backlinks: none
|
||||||
@@ -58,7 +53,7 @@ Google Cloud, etc) or any on-premise network storage (via SSH, for example).
|
|||||||
|
|
||||||
The DVC pipelines (computational graph) feature connects code and data together. It is possible to explicitly
|
The DVC pipelines (computational graph) feature connects code and data together. It is possible to explicitly
|
||||||
specify all steps required to produce a model: input dependencies including data, commands to run,
|
specify all steps required to produce a model: input dependencies including data, commands to run,
|
||||||
and output information to be saved. See the quick start section below or
|
and output information to be saved. See the quick start sections below or
|
||||||
the `Get Started <https://dvc.org/doc/get-started>`_ tutorial to learn more.
|
the `Get Started <https://dvc.org/doc/get-started>`_ tutorial to learn more.
|
||||||
|
|
||||||
Quick start
|
Quick start
|
||||||
@@ -86,14 +81,27 @@ Please read `Get Started <https://dvc.org/doc/get-started>`_ guide for a full ve
|
|||||||
| | | ``$ dvc push`` |
|
| | | ``$ dvc push`` |
|
||||||
+-----------------------------------+----------------------------------------------------------------------------+
|
+-----------------------------------+----------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
Visual Studio Code Extension
|
||||||
|
============================
|
||||||
|
|
||||||
|
|VS Code|
|
||||||
|
|
||||||
|
To get use DVC as a GUI right from your VS Code IDE, install the `DVC Extension <https://marketplace.visualstudio.com/items?itemName=Iterative.dvc>`_ from the Marketplace.
|
||||||
|
It currently features experiment tracking and data management, and more features (data pipeline support, etc.) are coming soon!
|
||||||
|
|
||||||
|
|VS Code Extension Overview|
|
||||||
|
|
||||||
|
Note: You'll have to install core DVC on your system separately (as detailed
|
||||||
|
below). The Extension will guide you if needed.
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
============
|
============
|
||||||
|
|
||||||
There are four options to install DVC: ``pip``, Homebrew, Conda (Anaconda) or an OS-specific package.
|
There are several ways to install DVC: in VS Code; using ``snap``, ``choco``, ``brew``, ``conda``, ``pip``; or with an OS-specific package.
|
||||||
Full instructions are `available here <https://dvc.org/doc/get-started/install>`_.
|
Full instructions are `available here <https://dvc.org/doc/get-started/install>`_.
|
||||||
|
|
||||||
Snap (Snapcraft/Linux)
|
Snapcraft (Linux)
|
||||||
----------------------
|
-----------------
|
||||||
|
|
||||||
|Snap|
|
|Snap|
|
||||||
|
|
||||||
@@ -103,7 +111,7 @@ Snap (Snapcraft/Linux)
|
|||||||
|
|
||||||
This corresponds to the latest tagged release.
|
This corresponds to the latest tagged release.
|
||||||
Add ``--beta`` for the latest tagged release candidate,
|
Add ``--beta`` for the latest tagged release candidate,
|
||||||
or ``--edge`` for the latest ``master`` version.
|
or ``--edge`` for the latest ``main`` version.
|
||||||
|
|
||||||
Choco (Chocolatey/Windows)
|
Choco (Chocolatey/Windows)
|
||||||
--------------------------
|
--------------------------
|
||||||
@@ -169,22 +177,24 @@ Ubuntu / Debian (deb)
|
|||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo wget https://dvc.org/deb/dvc.list -O /etc/apt/sources.list.d/dvc.list
|
sudo wget https://dvc.org/deb/dvc.list -O /etc/apt/sources.list.d/dvc.list
|
||||||
sudo apt-get update
|
wget -qO - https://dvc.org/deb/iterative.asc | sudo apt-key add -
|
||||||
sudo apt-get install dvc
|
sudo apt update
|
||||||
|
sudo apt install dvc
|
||||||
|
|
||||||
Fedora / CentOS (rpm)
|
Fedora / CentOS (rpm)
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo wget https://dvc.org/rpm/dvc.repo -O /etc/yum.repos.d/dvc.repo
|
sudo wget https://dvc.org/rpm/dvc.repo -O /etc/yum.repos.d/dvc.repo
|
||||||
|
sudo rpm --import https://dvc.org/rpm/iterative.asc
|
||||||
sudo yum update
|
sudo yum update
|
||||||
sudo yum install dvc
|
sudo yum install dvc
|
||||||
|
|
||||||
Comparison to related technologies
|
Comparison to related technologies
|
||||||
==================================
|
==================================
|
||||||
|
|
||||||
#. Data Engineering tools such as `AirFlow <https://airflow.apache.org/>`,
|
#. Data Engineering tools such as `AirFlow <https://airflow.apache.org/>`_,
|
||||||
`Luigi <https://github.com/spotify/luigi>`, and others - in DVC data,
|
`Luigi <https://github.com/spotify/luigi>`_, and others - in DVC data,
|
||||||
model and ML pipelines represent a single ML project focused on data
|
model and ML pipelines represent a single ML project focused on data
|
||||||
scientists' experience. Data engineering tools orchestrate multiple data
|
scientists' experience. Data engineering tools orchestrate multiple data
|
||||||
projects and focus on efficient execution. A DVC project can be used from
|
projects and focus on efficient execution. A DVC project can be used from
|
||||||
@@ -212,14 +222,14 @@ Comparison to related technologies
|
|||||||
#. `DVC Studio <https://studio.iterative.ai/>`_ - official online
|
#. `DVC Studio <https://studio.iterative.ai/>`_ - official online
|
||||||
platform for DVC projects. It can be used to manage data and models, run
|
platform for DVC projects. It can be used to manage data and models, run
|
||||||
and track experiments, and visualize and share results. Also, it
|
and track experiments, and visualize and share results. Also, it
|
||||||
integrates with `CML (CI/CD for ML) <https://cml.dev/>` for training
|
integrates with `CML (CI/CD for ML) <https://cml.dev/>`__ for training
|
||||||
models in the cloud or Kubernetes.
|
models in the cloud or Kubernetes.
|
||||||
|
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
============
|
============
|
||||||
|
|
||||||
|Maintainability| |Donate|
|
|Maintainability|
|
||||||
|
|
||||||
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more
|
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more
|
||||||
details. Thanks to all our contributors!
|
details. Thanks to all our contributors!
|
||||||
@@ -254,7 +264,10 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
|||||||
:target: https://dvc.org
|
:target: https://dvc.org
|
||||||
:alt: DVC logo
|
:alt: DVC logo
|
||||||
|
|
||||||
.. |CI| image:: https://github.com/iterative/dvc/workflows/Tests/badge.svg?branch=master
|
.. |VS Code Extension Overview| image:: https://raw.githubusercontent.com/iterative/vscode-dvc/main/extension/docs/overview.gif
|
||||||
|
:alt: DVC Extension for VS Code
|
||||||
|
|
||||||
|
.. |CI| image:: https://github.com/iterative/dvc/workflows/Tests/badge.svg?branch=main
|
||||||
:target: https://github.com/iterative/dvc/actions
|
:target: https://github.com/iterative/dvc/actions
|
||||||
:alt: GHA Tests
|
:alt: GHA Tests
|
||||||
|
|
||||||
@@ -262,14 +275,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
|
:target: https://codeclimate.com/github/iterative/dvc
|
||||||
:alt: Code Climate
|
:alt: Code Climate
|
||||||
|
|
||||||
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/master/graph/badge.svg
|
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/main/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/iterative/dvc
|
:target: https://codecov.io/gh/iterative/dvc
|
||||||
:alt: Codecov
|
:alt: Codecov
|
||||||
|
|
||||||
.. |Donate| image:: https://img.shields.io/badge/patreon-donate-green.svg?logo=patreon
|
|
||||||
:target: https://www.patreon.com/DVCorg/overview
|
|
||||||
:alt: Donate
|
|
||||||
|
|
||||||
.. |Snap| image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
|
.. |Snap| image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
|
||||||
:target: https://snapcraft.io/dvc
|
:target: https://snapcraft.io/dvc
|
||||||
:alt: Snapcraft
|
:alt: Snapcraft
|
||||||
@@ -305,3 +314,7 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
|||||||
.. |Contribs| image:: https://contrib.rocks/image?repo=iterative/dvc
|
.. |Contribs| image:: https://contrib.rocks/image?repo=iterative/dvc
|
||||||
:target: https://github.com/iterative/dvc/graphs/contributors
|
:target: https://github.com/iterative/dvc/graphs/contributors
|
||||||
:alt: Contributors
|
:alt: Contributors
|
||||||
|
|
||||||
|
.. |VS Code| image:: https://vsmarketplacebadge.apphb.com/version/Iterative.dvc.svg
|
||||||
|
:target: https://marketplace.visualstudio.com/items?itemName=Iterative.dvc
|
||||||
|
:alt: VS Code Extension
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
"""Main entry point for DVC command line tool."""
|
"""Main entry point for DVC command line tool."""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from dvc.main import main
|
from dvc.cli import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main(sys.argv[1:]))
|
sys.exit(main(sys.argv[1:]))
|
||||||
|
|||||||
+34
-2
@@ -1,10 +1,32 @@
|
|||||||
from contextlib import ExitStack, contextmanager
|
from contextlib import ExitStack, contextmanager
|
||||||
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Callable, Union
|
from typing import TYPE_CHECKING, Callable, Union
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def viztracer_profile(
|
||||||
|
path: Union[Callable[[], str], str],
|
||||||
|
max_stack_depth: int = -1,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
import viztracer # pylint: disable=import-error
|
||||||
|
except ImportError:
|
||||||
|
print("Failed to run profiler, viztracer is not installed")
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
tracer = viztracer.VizTracer(max_stack_depth=max_stack_depth)
|
||||||
|
|
||||||
|
tracer.start()
|
||||||
|
yield
|
||||||
|
tracer.stop()
|
||||||
|
|
||||||
|
tracer.save(path() if callable(path) else path)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def yappi_profile(
|
def yappi_profile(
|
||||||
path: Union[Callable[[], str], str] = None, wall_clock: bool = True
|
path: Union[Callable[[], str], str] = None, wall_clock: bool = True
|
||||||
@@ -100,12 +122,18 @@ def debugtools(args: "Namespace" = None, **kwargs):
|
|||||||
if kw.get("instrument") or kw.get("instrument_open"):
|
if kw.get("instrument") or kw.get("instrument_open"):
|
||||||
stack.enter_context(instrument(kw.get("instrument_open", False)))
|
stack.enter_context(instrument(kw.get("instrument_open", False)))
|
||||||
if kw.get("yappi"):
|
if kw.get("yappi"):
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
output = "callgrind.dvc-{0:%Y%m%d}_{0:%H%M%S}.out"
|
output = "callgrind.dvc-{0:%Y%m%d}_{0:%H%M%S}.out"
|
||||||
stack.enter_context(
|
stack.enter_context(
|
||||||
yappi_profile(path=lambda: output.format(datetime.now()))
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@@ -118,6 +146,10 @@ def add_debugging_flags(parser):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--yappi", action="store_true", default=False, help=SUPPRESS
|
"--yappi", action="store_true", default=False, help=SUPPRESS
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--viztracer", action="store_true", default=False, help=SUPPRESS
|
||||||
|
)
|
||||||
|
parser.add_argument("--viztracer-depth", type=int, help=SUPPRESS)
|
||||||
parser.add_argument("--cprofile-dump", help=SUPPRESS)
|
parser.add_argument("--cprofile-dump", help=SUPPRESS)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--pdb", action="store_true", default=False, help=SUPPRESS
|
"--pdb", action="store_true", default=False, help=SUPPRESS
|
||||||
|
|||||||
+2
-2
@@ -50,7 +50,7 @@ def is_enabled():
|
|||||||
Config(validate=False).get("core", {}).get("analytics", "true")
|
Config(validate=False).get("core", {}).get("analytics", "true")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("Analytics is {}abled.".format("en" if enabled else "dis"))
|
logger.debug("Analytics is %sabled.", "en" if enabled else "dis")
|
||||||
|
|
||||||
return enabled
|
return enabled
|
||||||
|
|
||||||
@@ -187,4 +187,4 @@ def _find_or_create_user_id():
|
|||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
except LockError:
|
except LockError:
|
||||||
logger.debug(f"Failed to acquire '{lockfile}'")
|
logger.debug("Failed to acquire '%s'", lockfile)
|
||||||
|
|||||||
-122
@@ -1,122 +0,0 @@
|
|||||||
import os
|
|
||||||
from contextlib import _GeneratorContextManager as GCM
|
|
||||||
|
|
||||||
from funcy import reraise
|
|
||||||
|
|
||||||
from dvc.exceptions import OutputNotFoundError, PathMissingError
|
|
||||||
from dvc.repo import Repo
|
|
||||||
|
|
||||||
|
|
||||||
def get_url(path, repo=None, rev=None, remote=None):
|
|
||||||
"""
|
|
||||||
Returns the URL to the storage location of a data file or directory tracked
|
|
||||||
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
|
|
||||||
supplied. The default remote is tried unless a remote argument is supplied.
|
|
||||||
|
|
||||||
Raises OutputNotFoundError if the file is not tracked by DVC.
|
|
||||||
|
|
||||||
NOTE: This function does not check for the actual existence of the file or
|
|
||||||
directory in the remote storage.
|
|
||||||
"""
|
|
||||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
|
||||||
fs_path = _repo.fs.path.join(_repo.root_dir, path)
|
|
||||||
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
|
||||||
metadata = _repo.repo_fs.metadata(fs_path)
|
|
||||||
|
|
||||||
if not metadata.is_dvc:
|
|
||||||
raise OutputNotFoundError(path, repo)
|
|
||||||
|
|
||||||
cloud = metadata.repo.cloud
|
|
||||||
md5 = metadata.repo.dvcfs.info(fs_path)["md5"]
|
|
||||||
return cloud.get_url_for(remote, checksum=md5)
|
|
||||||
|
|
||||||
|
|
||||||
def open( # noqa, pylint: disable=redefined-builtin
|
|
||||||
path, repo=None, rev=None, remote=None, mode="r", encoding=None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Open file in the supplied path tracked in a repo (both DVC projects and
|
|
||||||
plain Git repos are supported). For Git repos, HEAD is used unless a rev
|
|
||||||
argument is supplied. The default remote is tried unless a remote argument
|
|
||||||
is supplied. It may only be used as a context manager:
|
|
||||||
|
|
||||||
with dvc.api.open(
|
|
||||||
'path/to/file',
|
|
||||||
repo='https://example.com/url/to/repo'
|
|
||||||
) as fd:
|
|
||||||
# ... Handle file object fd
|
|
||||||
"""
|
|
||||||
args = (path,)
|
|
||||||
kwargs = {
|
|
||||||
"repo": repo,
|
|
||||||
"remote": remote,
|
|
||||||
"rev": rev,
|
|
||||||
"mode": mode,
|
|
||||||
"encoding": encoding,
|
|
||||||
}
|
|
||||||
return _OpenContextManager(_open, args, kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class _OpenContextManager(GCM):
|
|
||||||
def __init__(
|
|
||||||
self, func, args, kwds
|
|
||||||
): # pylint: disable=super-init-not-called
|
|
||||||
self.gen = func(*args, **kwds)
|
|
||||||
self.func, self.args, self.kwds = func, args, kwds
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
raise AttributeError(
|
|
||||||
"dvc.api.open() should be used in a with statement."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
|
||||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
|
||||||
with _repo.open_by_relpath(
|
|
||||||
path, remote=remote, mode=mode, encoding=encoding
|
|
||||||
) as fd:
|
|
||||||
yield fd
|
|
||||||
|
|
||||||
|
|
||||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
|
||||||
"""
|
|
||||||
Returns the contents of a tracked file (by DVC or Git). For Git repos, HEAD
|
|
||||||
is used unless a rev argument is supplied. The default remote is tried
|
|
||||||
unless a remote argument is supplied.
|
|
||||||
"""
|
|
||||||
with open(
|
|
||||||
path, repo=repo, rev=rev, remote=remote, mode=mode, encoding=encoding
|
|
||||||
) as fd:
|
|
||||||
return fd.read()
|
|
||||||
|
|
||||||
|
|
||||||
def make_checkpoint():
|
|
||||||
"""
|
|
||||||
Signal DVC to create a checkpoint experiment.
|
|
||||||
|
|
||||||
If the current process is being run from DVC, this function will block
|
|
||||||
until DVC has finished creating the checkpoint. Otherwise, this function
|
|
||||||
will return immediately.
|
|
||||||
"""
|
|
||||||
import builtins
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
|
|
||||||
from dvc.stage.monitor import CheckpointTask
|
|
||||||
|
|
||||||
if os.getenv(DVC_CHECKPOINT) is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
root_dir = os.getenv(DVC_ROOT, Repo.find_root())
|
|
||||||
signal_file = os.path.join(
|
|
||||||
root_dir, Repo.DVC_DIR, "tmp", CheckpointTask.SIGNAL_FILE
|
|
||||||
)
|
|
||||||
|
|
||||||
with builtins.open(signal_file, "w", encoding="utf-8") as fobj:
|
|
||||||
# NOTE: force flushing/writing empty file to disk, otherwise when
|
|
||||||
# run in certain contexts (pytest) file may not actually be written
|
|
||||||
fobj.write("")
|
|
||||||
fobj.flush()
|
|
||||||
os.fsync(fobj.fileno())
|
|
||||||
while os.path.exists(signal_file):
|
|
||||||
sleep(0.1)
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from .data import ( # noqa, pylint: disable=redefined-builtin
|
||||||
|
get_url,
|
||||||
|
open,
|
||||||
|
read,
|
||||||
|
)
|
||||||
|
from .experiments import make_checkpoint
|
||||||
|
from .params import params_show
|
||||||
|
|
||||||
|
__all__ = ["get_url", "make_checkpoint", "open", "params_show", "read"]
|
||||||
+213
@@ -0,0 +1,213 @@
|
|||||||
|
from contextlib import _GeneratorContextManager as GCM
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from funcy import reraise
|
||||||
|
|
||||||
|
from dvc.exceptions import OutputNotFoundError, PathMissingError
|
||||||
|
from dvc.repo import Repo
|
||||||
|
|
||||||
|
|
||||||
|
def get_url(path, repo=None, rev=None, remote=None):
|
||||||
|
"""
|
||||||
|
Returns the URL to the storage location of a data file or directory tracked
|
||||||
|
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
|
||||||
|
supplied. The default remote is tried unless a remote argument is supplied.
|
||||||
|
|
||||||
|
Raises OutputNotFoundError if the file is not tracked by DVC.
|
||||||
|
|
||||||
|
NOTE: This function does not check for the actual existence of the file or
|
||||||
|
directory in the remote storage.
|
||||||
|
"""
|
||||||
|
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||||
|
fs_path = _repo.dvcfs.from_os_path(path)
|
||||||
|
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
||||||
|
info = _repo.dvcfs.info(fs_path)
|
||||||
|
|
||||||
|
dvc_info = info.get("dvc_info")
|
||||||
|
if not dvc_info:
|
||||||
|
raise OutputNotFoundError(path, repo)
|
||||||
|
|
||||||
|
dvc_repo = info["repo"]
|
||||||
|
md5 = dvc_info["md5"]
|
||||||
|
|
||||||
|
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
|
||||||
|
|
||||||
|
|
||||||
|
class _OpenContextManager(GCM):
|
||||||
|
def __init__(
|
||||||
|
self, func, args, kwds
|
||||||
|
): # pylint: disable=super-init-not-called
|
||||||
|
self.gen = func(*args, **kwds)
|
||||||
|
self.func, self.args, self.kwds = func, args, kwds
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
raise AttributeError(
|
||||||
|
"dvc.api.open() should be used in a with statement."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def open( # noqa, pylint: disable=redefined-builtin
|
||||||
|
path: str,
|
||||||
|
repo: Optional[str] = None,
|
||||||
|
rev: Optional[str] = None,
|
||||||
|
remote: Optional[str] = None,
|
||||||
|
mode: str = "r",
|
||||||
|
encoding: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Opens a file tracked in a DVC project.
|
||||||
|
|
||||||
|
This function may only be used as a context manager (using the `with`
|
||||||
|
keyword, as shown in the examples).
|
||||||
|
|
||||||
|
This function makes a direct connection to the remote storage, so the file
|
||||||
|
contents can be streamed. Your code can process the data buffer as it's
|
||||||
|
streamed, which optimizes memory usage.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Use dvc.api.read() to load the complete file contents
|
||||||
|
in a single function call, no context manager involved.
|
||||||
|
Neither function utilizes disc space.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): location and file name of the target to open,
|
||||||
|
relative to the root of `repo`.
|
||||||
|
repo (str, optional): location of the DVC project or Git Repo.
|
||||||
|
Defaults to the current DVC project (found by walking up from the
|
||||||
|
current working directory tree).
|
||||||
|
It can be a URL or a file system path.
|
||||||
|
Both HTTP and SSH protocols are supported for online Git repos
|
||||||
|
(e.g. [user@]server:project.git).
|
||||||
|
rev (str, optional): Any `Git revision`_ such as a branch or tag name,
|
||||||
|
a commit hash or a dvc experiment name.
|
||||||
|
Defaults to HEAD.
|
||||||
|
If `repo` is not a Git repo, this option is ignored.
|
||||||
|
remote (str, optional): Name of the `DVC remote`_ used to form the
|
||||||
|
returned URL string.
|
||||||
|
Defaults to the `default remote`_ of `repo`.
|
||||||
|
For local projects, the cache is tried before the default remote.
|
||||||
|
mode (str, optional): Specifies the mode in which the file is opened.
|
||||||
|
Defaults to "r" (read).
|
||||||
|
Mirrors the namesake parameter in builtin `open()`_.
|
||||||
|
Only reading `mode` is supported.
|
||||||
|
encoding(str, optional): `Codec`_ used to decode the file contents.
|
||||||
|
Defaults to None.
|
||||||
|
This should only be used in text mode.
|
||||||
|
Mirrors the namesake parameter in builtin `open()`_.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_OpenContextManager: A context manager that generatse a corresponding
|
||||||
|
`file object`_.
|
||||||
|
The exact type of file object depends on the mode used.
|
||||||
|
For more details, please refer to Python's `open()`_ built-in,
|
||||||
|
which is used under the hood.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: If this method is not used as a context manager.
|
||||||
|
ValueError: If non-read `mode` is used.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- Use data or models from a DVC repository.
|
||||||
|
|
||||||
|
Any file tracked in a DVC project (and stored remotely) can be
|
||||||
|
processed directly in your Python code with this API.
|
||||||
|
For example, an XML file tracked in a public DVC repo on GitHub can be
|
||||||
|
processed like this:
|
||||||
|
|
||||||
|
>>> from xml.sax import parse
|
||||||
|
>>> import dvc.api
|
||||||
|
>>> from mymodule import mySAXHandler
|
||||||
|
|
||||||
|
>>> with dvc.api.open(
|
||||||
|
... 'get-started/data.xml',
|
||||||
|
... repo='https://github.com/iterative/dataset-registry'
|
||||||
|
... ) as fd:
|
||||||
|
... parse(fd, mySAXHandler)
|
||||||
|
|
||||||
|
We use a SAX XML parser here because dvc.api.open() is able to stream
|
||||||
|
the data from remote storage.
|
||||||
|
The mySAXHandler object should handle the event-driven parsing of the
|
||||||
|
document in this case.
|
||||||
|
This increases the performance of the code (minimizing memory usage),
|
||||||
|
and is typically faster than loading the whole data into memory.
|
||||||
|
|
||||||
|
- Accessing private repos
|
||||||
|
|
||||||
|
This is just a matter of using the right repo argument, for example an
|
||||||
|
SSH URL (requires that the credentials are configured locally):
|
||||||
|
|
||||||
|
>>> import dvc.api
|
||||||
|
|
||||||
|
>>> with dvc.api.open(
|
||||||
|
... 'features.dat',
|
||||||
|
... repo='[email protected]:path/to/repo.git'
|
||||||
|
... ) as fd:
|
||||||
|
... # ... Process 'features'
|
||||||
|
... pass
|
||||||
|
|
||||||
|
- Use different versions of data
|
||||||
|
|
||||||
|
Any git revision (see `rev`) can be accessed programmatically.
|
||||||
|
For example, if your DVC repo has tagged releases of a CSV dataset:
|
||||||
|
|
||||||
|
>>> import csv
|
||||||
|
>>> import dvc.api
|
||||||
|
>>> with dvc.api.open(
|
||||||
|
... 'clean.csv',
|
||||||
|
... rev='v1.1.0'
|
||||||
|
... ) as fd:
|
||||||
|
... reader = csv.reader(fd)
|
||||||
|
... # ... Process 'clean' data from version 1.1.0
|
||||||
|
|
||||||
|
.. _Git revision:
|
||||||
|
https://git-scm.com/docs/revisions
|
||||||
|
|
||||||
|
.. _DVC remote:
|
||||||
|
https://dvc.org/doc/command-reference/remote
|
||||||
|
|
||||||
|
.. _default remote:
|
||||||
|
https://dvc.org/doc/command-reference/remote/default
|
||||||
|
|
||||||
|
.. _open():
|
||||||
|
https://docs.python.org/3/library/functions.html#open
|
||||||
|
|
||||||
|
.. _Codec:
|
||||||
|
https://docs.python.org/3/library/codecs.html#standard-encodings
|
||||||
|
|
||||||
|
.. _file object:
|
||||||
|
https://docs.python.org/3/glossary.html#term-file-object
|
||||||
|
|
||||||
|
"""
|
||||||
|
if "r" not in mode:
|
||||||
|
raise ValueError("Only reading `mode` is supported.")
|
||||||
|
|
||||||
|
args = (path,)
|
||||||
|
kwargs = {
|
||||||
|
"repo": repo,
|
||||||
|
"remote": remote,
|
||||||
|
"rev": rev,
|
||||||
|
"mode": mode,
|
||||||
|
"encoding": encoding,
|
||||||
|
}
|
||||||
|
return _OpenContextManager(_open, args, kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||||
|
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||||
|
with _repo.open_by_relpath(
|
||||||
|
path, remote=remote, mode=mode, encoding=encoding
|
||||||
|
) as fd:
|
||||||
|
yield fd
|
||||||
|
|
||||||
|
|
||||||
|
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||||
|
"""
|
||||||
|
Returns the contents of a tracked file (by DVC or Git). For Git repos, HEAD
|
||||||
|
is used unless a rev argument is supplied. The default remote is tried
|
||||||
|
unless a remote argument is supplied.
|
||||||
|
"""
|
||||||
|
with open(
|
||||||
|
path, repo=repo, rev=rev, remote=remote, mode=mode, encoding=encoding
|
||||||
|
) as fd:
|
||||||
|
return fd.read()
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import builtins
|
||||||
|
import os
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
|
||||||
|
from dvc.repo import Repo
|
||||||
|
from dvc.stage.monitor import CheckpointTask
|
||||||
|
|
||||||
|
|
||||||
|
def make_checkpoint():
|
||||||
|
"""
|
||||||
|
Signal DVC to create a checkpoint experiment.
|
||||||
|
|
||||||
|
If the current process is being run from DVC, this function will block
|
||||||
|
until DVC has finished creating the checkpoint. Otherwise, this function
|
||||||
|
will return immediately.
|
||||||
|
"""
|
||||||
|
if os.getenv(DVC_CHECKPOINT) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
root_dir = os.getenv(DVC_ROOT, Repo.find_root())
|
||||||
|
signal_file = os.path.join(
|
||||||
|
root_dir, Repo.DVC_DIR, "tmp", CheckpointTask.SIGNAL_FILE
|
||||||
|
)
|
||||||
|
|
||||||
|
with builtins.open(signal_file, "w", encoding="utf-8") as fobj:
|
||||||
|
# NOTE: force flushing/writing empty file to disk, otherwise when
|
||||||
|
# run in certain contexts (pytest) file may not actually be written
|
||||||
|
fobj.write("")
|
||||||
|
fobj.flush()
|
||||||
|
os.fsync(fobj.fileno())
|
||||||
|
while os.path.exists(signal_file):
|
||||||
|
sleep(0.1)
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
from collections import Counter
|
||||||
|
from typing import Dict, Iterable, Optional, Union
|
||||||
|
|
||||||
|
from funcy import first
|
||||||
|
|
||||||
|
from dvc.exceptions import DvcException
|
||||||
|
from dvc.repo import Repo
|
||||||
|
|
||||||
|
|
||||||
|
def params_show(
|
||||||
|
*targets: str,
|
||||||
|
repo: Optional[str] = None,
|
||||||
|
stages: Optional[Union[str, Iterable[str]]] = None,
|
||||||
|
rev: Optional[str] = None,
|
||||||
|
deps: bool = False,
|
||||||
|
) -> Dict:
|
||||||
|
"""Get parameters tracked in `repo`.
|
||||||
|
|
||||||
|
Without arguments, this function will retrieve all params from all tracked
|
||||||
|
parameter files, for the current working tree.
|
||||||
|
|
||||||
|
See the options below to restrict the parameters retrieved.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*targets (str, optional): Names of the parameter files to retrieve
|
||||||
|
params from. For example, "params.py, myparams.toml".
|
||||||
|
If no `targets` are provided, all parameter files tracked in `dvc.yaml`
|
||||||
|
will be used.
|
||||||
|
Note that targets don't necessarily have to be defined in `dvc.yaml`.
|
||||||
|
repo (str, optional): location of the DVC repository.
|
||||||
|
Defaults to the current project (found by walking up from the
|
||||||
|
current working directory tree).
|
||||||
|
It can be a URL or a file system path.
|
||||||
|
Both HTTP and SSH protocols are supported for online Git repos
|
||||||
|
(e.g. [user@]server:project.git).
|
||||||
|
stages (Union[str, Iterable[str]], optional): Name or names of the
|
||||||
|
stages to retrieve parameters from.
|
||||||
|
Defaults to `None`.
|
||||||
|
If `None`, all parameters from all stages will be retrieved.
|
||||||
|
rev (str, optional): Name of the `Git revision`_ to retrieve parameters
|
||||||
|
from.
|
||||||
|
Defaults to `None`.
|
||||||
|
An example of git revision can be a branch or tag name, a commit
|
||||||
|
hash or a dvc experiment name.
|
||||||
|
If `repo` is not a Git repo, this option is ignored.
|
||||||
|
If `None`, the current working tree will be used.
|
||||||
|
deps (bool, optional): Whether to retrieve only parameters that are
|
||||||
|
stage dependencies or not.
|
||||||
|
Defaults to `False`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: See Examples below.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DvcException: If no params are found in `repo`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- No arguments.
|
||||||
|
|
||||||
|
Working on https://github.com/iterative/example-get-started
|
||||||
|
|
||||||
|
>>> import json
|
||||||
|
>>> import dvc.api
|
||||||
|
>>> params = dvc.api.params_show()
|
||||||
|
>>> print(json.dumps(params, indent=4))
|
||||||
|
{
|
||||||
|
"prepare": {
|
||||||
|
"split": 0.2,
|
||||||
|
"seed": 20170428
|
||||||
|
},
|
||||||
|
"featurize": {
|
||||||
|
"max_features": 200,
|
||||||
|
"ngrams": 2
|
||||||
|
},
|
||||||
|
"train": {
|
||||||
|
"seed": 20170428,
|
||||||
|
"n_est": 50,
|
||||||
|
"min_split": 0.01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- Filtering with `stages`.
|
||||||
|
|
||||||
|
Working on https://github.com/iterative/example-get-started
|
||||||
|
|
||||||
|
`stages` can a single string:
|
||||||
|
|
||||||
|
>>> import json
|
||||||
|
>>> import dvc.api
|
||||||
|
>>> params = dvc.api.params_show(stages="prepare")
|
||||||
|
>>> print(json.dumps(params, indent=4))
|
||||||
|
{
|
||||||
|
"prepare": {
|
||||||
|
"split": 0.2,
|
||||||
|
"seed": 20170428
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Or an iterable of strings:
|
||||||
|
|
||||||
|
>>> import json
|
||||||
|
>>> import dvc.api
|
||||||
|
>>> params = dvc.api.params_show(stages=["prepare", "train"])
|
||||||
|
>>> print(json.dumps(params, indent=4))
|
||||||
|
{
|
||||||
|
"prepare": {
|
||||||
|
"split": 0.2,
|
||||||
|
"seed": 20170428
|
||||||
|
},
|
||||||
|
"train": {
|
||||||
|
"seed": 20170428,
|
||||||
|
"n_est": 50,
|
||||||
|
"min_split": 0.01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- Using `rev`.
|
||||||
|
|
||||||
|
Working on https://github.com/iterative/example-get-started
|
||||||
|
|
||||||
|
>>> import json
|
||||||
|
>>> import dvc.api
|
||||||
|
>>> params = dvc.api.params_show(rev="tune-hyperparams")
|
||||||
|
>>> print(json.dumps(params, indent=4))
|
||||||
|
{
|
||||||
|
"prepare": {
|
||||||
|
"split": 0.2,
|
||||||
|
"seed": 20170428
|
||||||
|
},
|
||||||
|
"featurize": {
|
||||||
|
"max_features": 200,
|
||||||
|
"ngrams": 2
|
||||||
|
},
|
||||||
|
"train": {
|
||||||
|
"seed": 20170428,
|
||||||
|
"n_est": 100,
|
||||||
|
"min_split": 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- Using `targets`.
|
||||||
|
|
||||||
|
Working on `multi-params-files` folder of
|
||||||
|
https://github.com/iterative/pipeline-conifguration
|
||||||
|
|
||||||
|
You can pass a single target:
|
||||||
|
|
||||||
|
>>> import json
|
||||||
|
>>> import dvc.api
|
||||||
|
>>> params = dvc.api.params_show("params.yaml")
|
||||||
|
>>> print(json.dumps(params, indent=4))
|
||||||
|
{
|
||||||
|
"run_mode": "prod",
|
||||||
|
"configs": {
|
||||||
|
"dev": "configs/params_dev.yaml",
|
||||||
|
"test": "configs/params_test.yaml",
|
||||||
|
"prod": "configs/params_prod.yaml"
|
||||||
|
},
|
||||||
|
"evaluate": {
|
||||||
|
"dataset": "micro",
|
||||||
|
"size": 5000,
|
||||||
|
"metrics": ["f1", "roc-auc"],
|
||||||
|
"metrics_file": "reports/metrics.json",
|
||||||
|
"plots_cm": "reports/plot_confusion_matrix.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Or multiple targets:
|
||||||
|
|
||||||
|
>>> import json
|
||||||
|
>>> import dvc.api
|
||||||
|
>>> params = dvc.api.params_show(
|
||||||
|
... "configs/params_dev.yaml", "configs/params_prod.yaml")
|
||||||
|
>>> print(json.dumps(params, indent=4))
|
||||||
|
{
|
||||||
|
"configs/params_prod.yaml:run_mode": "prod",
|
||||||
|
"configs/params_prod.yaml:config_file": "configs/params_prod.yaml",
|
||||||
|
"configs/params_prod.yaml:data_load": {
|
||||||
|
"dataset": "large",
|
||||||
|
"sampling": {
|
||||||
|
"enable": true,
|
||||||
|
"size": 50000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configs/params_prod.yaml:train": {
|
||||||
|
"epochs": 1000
|
||||||
|
},
|
||||||
|
"configs/params_dev.yaml:run_mode": "dev",
|
||||||
|
"configs/params_dev.yaml:config_file": "configs/params_dev.yaml",
|
||||||
|
"configs/params_dev.yaml:data_load": {
|
||||||
|
"dataset": "development",
|
||||||
|
"sampling": {
|
||||||
|
"enable": true,
|
||||||
|
"size": 1000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configs/params_dev.yaml:train": {
|
||||||
|
"epochs": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- Git URL as `repo`.
|
||||||
|
|
||||||
|
>>> import json
|
||||||
|
>>> import dvc.api
|
||||||
|
>>> params = dvc.api.params_show(
|
||||||
|
... repo="https://github.com/iterative/demo-fashion-mnist")
|
||||||
|
{
|
||||||
|
"train": {
|
||||||
|
"batch_size": 128,
|
||||||
|
"hidden_units": 64,
|
||||||
|
"dropout": 0.4,
|
||||||
|
"num_epochs": 10,
|
||||||
|
"lr": 0.001,
|
||||||
|
"conv_activation": "relu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.. _Git revision:
|
||||||
|
https://git-scm.com/docs/revisions
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(stages, str):
|
||||||
|
stages = [stages]
|
||||||
|
|
||||||
|
def _onerror_raise(result: Dict, exception: Exception, *args, **kwargs):
|
||||||
|
raise exception
|
||||||
|
|
||||||
|
def _postprocess(params):
|
||||||
|
processed = {}
|
||||||
|
for rev, rev_data in params.items():
|
||||||
|
processed[rev] = {}
|
||||||
|
|
||||||
|
counts = Counter()
|
||||||
|
for file_data in rev_data["data"].values():
|
||||||
|
for k in file_data["data"]:
|
||||||
|
counts[k] += 1
|
||||||
|
|
||||||
|
for file_name, file_data in rev_data["data"].items():
|
||||||
|
to_merge = {
|
||||||
|
(k if counts[k] == 1 else f"{file_name}:{k}"): v
|
||||||
|
for k, v in file_data["data"].items()
|
||||||
|
}
|
||||||
|
processed[rev] = {**processed[rev], **to_merge}
|
||||||
|
|
||||||
|
if "workspace" in processed:
|
||||||
|
del processed["workspace"]
|
||||||
|
|
||||||
|
if not processed:
|
||||||
|
raise DvcException("No params found")
|
||||||
|
|
||||||
|
return processed[first(processed)]
|
||||||
|
|
||||||
|
with Repo.open(repo) as _repo:
|
||||||
|
params = _repo.params.show(
|
||||||
|
revs=rev if rev is None else [rev],
|
||||||
|
targets=targets,
|
||||||
|
deps=deps,
|
||||||
|
onerror=_onerror_raise,
|
||||||
|
stages=stages,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _postprocess(params)
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
"""This module provides an entrypoint to the dvc cli and parsing utils."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Workaround for CPython bug. See [1] and [2] for more info.
|
||||||
|
# [1] https://github.com/aws/aws-cli/blob/1.16.277/awscli/clidriver.py#L55
|
||||||
|
# [2] https://bugs.python.org/issue29288
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
"".encode("idna")
|
||||||
|
|
||||||
|
logger = logging.getLogger("dvc")
|
||||||
|
|
||||||
|
|
||||||
|
class DvcParserError(Exception):
|
||||||
|
"""Base class for CLI parser errors."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("parser error")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv=None):
|
||||||
|
"""Parses CLI arguments.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
argv: optional list of arguments to parse. sys.argv is used by default.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DvcParserError: raised for argument parsing errors.
|
||||||
|
"""
|
||||||
|
from .parser import get_main_parser
|
||||||
|
|
||||||
|
parser = get_main_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
args.parser = parser
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def _log_unknown_exceptions() -> None:
|
||||||
|
from dvc.info import get_dvc_info
|
||||||
|
from dvc.logger import FOOTER
|
||||||
|
|
||||||
|
logger.exception("unexpected error")
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("Version info for developers:\n%s", get_dvc_info())
|
||||||
|
logger.info(FOOTER)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||||
|
"""Try to log some known exceptions, that are not DVCExceptions."""
|
||||||
|
from dvc.utils import error_link, format_link
|
||||||
|
|
||||||
|
if isinstance(exc, OSError):
|
||||||
|
import errno
|
||||||
|
|
||||||
|
if exc.errno == errno.EMFILE:
|
||||||
|
logger.exception(
|
||||||
|
"too many open files, please visit "
|
||||||
|
"{} to see how to handle this "
|
||||||
|
"problem".format(error_link("many-files")),
|
||||||
|
extra={"tb_only": True},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_log_unknown_exceptions()
|
||||||
|
return None
|
||||||
|
|
||||||
|
from dvc.fs import AuthError, ConfigError, RemoteMissingDepsError
|
||||||
|
|
||||||
|
if isinstance(exc, RemoteMissingDepsError):
|
||||||
|
from dvc.utils.pkg import PKG
|
||||||
|
|
||||||
|
proto = exc.protocol
|
||||||
|
by_pkg = {
|
||||||
|
"pip": f"pip install 'dvc[{proto}]'",
|
||||||
|
"conda": f"conda install -c conda-forge dvc-{proto}",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = by_pkg.get(PKG)
|
||||||
|
if cmd:
|
||||||
|
link = format_link("https://dvc.org/doc/install")
|
||||||
|
hint = (
|
||||||
|
f"To install dvc with those dependencies, run:\n"
|
||||||
|
"\n"
|
||||||
|
f"\t{cmd}\n"
|
||||||
|
"\n"
|
||||||
|
f"See {link} for more info."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
link = format_link("https://github.com/iterative/dvc/issues")
|
||||||
|
hint = f"\nPlease report this bug to {link}. Thank you!"
|
||||||
|
|
||||||
|
logger.exception(
|
||||||
|
f"URL '{exc.url}' is supported but requires these missing "
|
||||||
|
f"dependencies: {exc.missing_deps}. {hint}",
|
||||||
|
extra={"tb_only": True},
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(exc, (AuthError, ConfigError)):
|
||||||
|
link = format_link("https://man.dvc.org/remote/modify")
|
||||||
|
logger.exception("configuration error")
|
||||||
|
logger.exception(
|
||||||
|
f"{exc!s}\nLearn more about configuration settings at {link}.",
|
||||||
|
extra={"tb_only": True},
|
||||||
|
)
|
||||||
|
return 251
|
||||||
|
|
||||||
|
from dvc_data.hashfile.cache import DiskError
|
||||||
|
|
||||||
|
if isinstance(exc, DiskError):
|
||||||
|
from dvc.utils import relpath
|
||||||
|
|
||||||
|
directory = relpath(exc.directory)
|
||||||
|
logger.exception(
|
||||||
|
f"Could not open pickled '{exc.type}' cache.\n"
|
||||||
|
f"Remove the '{directory}' directory and then retry this command."
|
||||||
|
f"\nSee {error_link('pickle')} for more information.",
|
||||||
|
extra={"tb_only": True},
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
from dvc_data.build import IgnoreInCollectedDirError
|
||||||
|
|
||||||
|
if isinstance(exc, IgnoreInCollectedDirError):
|
||||||
|
logger.exception("")
|
||||||
|
return None
|
||||||
|
|
||||||
|
_log_unknown_exceptions()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None): # noqa: C901
|
||||||
|
"""Main entry point for dvc CLI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
argv: optional list of arguments to parse. sys.argv is used by default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: command's return code.
|
||||||
|
"""
|
||||||
|
from dvc._debug import debugtools
|
||||||
|
from dvc.config import ConfigError
|
||||||
|
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||||
|
from dvc.logger import disable_other_loggers, set_loggers_level
|
||||||
|
|
||||||
|
# NOTE: stderr/stdout may be closed if we are running from dvc.daemon.
|
||||||
|
# On Linux we directly call cli.main after double forking and closing
|
||||||
|
# the copied parent's standard file descriptors. If we make any logging
|
||||||
|
# calls in this state it will cause an exception due to writing to a closed
|
||||||
|
# file descriptor.
|
||||||
|
if sys.stderr.closed: # pylint: disable=using-constant-test
|
||||||
|
logging.disable()
|
||||||
|
elif sys.stdout.closed: # pylint: disable=using-constant-test
|
||||||
|
logging.disable(logging.INFO)
|
||||||
|
|
||||||
|
args = None
|
||||||
|
disable_other_loggers()
|
||||||
|
|
||||||
|
outerLogLevel = logger.level
|
||||||
|
try:
|
||||||
|
args = parse_args(argv)
|
||||||
|
|
||||||
|
level = None
|
||||||
|
if args.quiet:
|
||||||
|
level = logging.CRITICAL
|
||||||
|
elif args.verbose == 1:
|
||||||
|
level = logging.DEBUG
|
||||||
|
elif args.verbose > 1:
|
||||||
|
level = logging.TRACE
|
||||||
|
|
||||||
|
if level is not None:
|
||||||
|
set_loggers_level(level)
|
||||||
|
|
||||||
|
logger.trace(args)
|
||||||
|
|
||||||
|
if not sys.stdout.closed and not args.quiet:
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
ui.enable()
|
||||||
|
|
||||||
|
with debugtools(args):
|
||||||
|
cmd = args.func(args)
|
||||||
|
ret = cmd.do_run()
|
||||||
|
except ConfigError:
|
||||||
|
logger.exception("configuration error")
|
||||||
|
ret = 251
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.exception("interrupted by the user")
|
||||||
|
ret = 252
|
||||||
|
except NotDvcRepoError:
|
||||||
|
logger.exception("")
|
||||||
|
ret = 253
|
||||||
|
except DvcException:
|
||||||
|
ret = 255
|
||||||
|
logger.exception("")
|
||||||
|
except DvcParserError:
|
||||||
|
ret = 254
|
||||||
|
except Exception as exc: # noqa, pylint: disable=broad-except
|
||||||
|
# pylint: disable=no-member
|
||||||
|
ret = _log_exceptions(exc) or 255
|
||||||
|
|
||||||
|
try:
|
||||||
|
from dvc import analytics
|
||||||
|
|
||||||
|
if analytics.is_enabled():
|
||||||
|
analytics.collect_and_send_report(args, ret)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
finally:
|
||||||
|
logger.setLevel(outerLogLevel)
|
||||||
|
|
||||||
|
from dvc.external_repo import clean_repos
|
||||||
|
|
||||||
|
# Remove cached repos in the end of the call, these are anonymous
|
||||||
|
# so won't be reused by any other subsequent run anyway.
|
||||||
|
clean_repos()
|
||||||
@@ -5,29 +5,6 @@ from abc import ABC, abstractmethod
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def fix_subparsers(subparsers):
|
|
||||||
"""Workaround for bug in Python 3. See more info at:
|
|
||||||
https://bugs.python.org/issue16308
|
|
||||||
https://github.com/iterative/dvc/issues/769
|
|
||||||
|
|
||||||
Args:
|
|
||||||
subparsers: subparsers to fix.
|
|
||||||
"""
|
|
||||||
subparsers.required = True
|
|
||||||
subparsers.dest = "cmd"
|
|
||||||
|
|
||||||
|
|
||||||
def append_doc_link(help_message, path):
|
|
||||||
from dvc.utils import format_link
|
|
||||||
|
|
||||||
if not path:
|
|
||||||
return help_message
|
|
||||||
doc_base = "https://man.dvc.org/"
|
|
||||||
return "{message}\nDocumentation: {link}".format(
|
|
||||||
message=help_message, link=format_link(doc_base + path)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdBase(ABC):
|
class CmdBase(ABC):
|
||||||
UNINITIALIZED = False
|
UNINITIALIZED = False
|
||||||
|
|
||||||
@@ -1,17 +1,6 @@
|
|||||||
import argparse
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import shtab
|
import shtab
|
||||||
|
|
||||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
BASH_PREAMBLE = """
|
||||||
from dvc.ui import ui
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
FILE = shtab.FILE
|
|
||||||
DIR = shtab.DIRECTORY
|
|
||||||
|
|
||||||
PREAMBLE = {
|
|
||||||
"bash": """
|
|
||||||
# $1=COMP_WORDS[1]
|
# $1=COMP_WORDS[1]
|
||||||
_dvc_compgen_DVCFiles() {
|
_dvc_compgen_DVCFiles() {
|
||||||
compgen -d -S '/' -- $1 # recurse into subdirs
|
compgen -d -S '/' -- $1 # recurse into subdirs
|
||||||
@@ -30,11 +19,12 @@ _dvc_compgen_stages_and_files() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_dvc_compgen_exps() {
|
_dvc_compgen_exps() {
|
||||||
local _dvc_exps=($(dvc exp list -q --all --names-only))
|
local _dvc_exps=($(dvc exp list -q --all-commits --names-only))
|
||||||
compgen -W "${_dvc_exps[*]}" -- $1
|
compgen -W "${_dvc_exps[*]}" -- $1
|
||||||
}
|
}
|
||||||
""",
|
"""
|
||||||
"zsh": """
|
|
||||||
|
ZSH_PREAMBLE = """
|
||||||
_dvc_compadd_DVCFiles() {
|
_dvc_compadd_DVCFiles() {
|
||||||
_files -g '(*?.dvc|Dvcfile|dvc.yaml)'
|
_files -g '(*?.dvc|Dvcfile|dvc.yaml)'
|
||||||
}
|
}
|
||||||
@@ -63,49 +53,21 @@ _dvc_compadd_stages_and_files() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_dvc_compadd_exps() {
|
_dvc_compadd_exps() {
|
||||||
_describe 'experiments' "($(dvc exp list -q -a --names-only))"
|
_describe 'experiments' "($(dvc exp list -q --all-commits --names-only))"
|
||||||
}
|
}
|
||||||
""",
|
"""
|
||||||
|
|
||||||
|
PREAMBLE = {
|
||||||
|
"bash": BASH_PREAMBLE,
|
||||||
|
"zsh": ZSH_PREAMBLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FILE = shtab.FILE
|
||||||
|
DIR = shtab.DIRECTORY
|
||||||
DVC_FILE = {"bash": "_dvc_compgen_DVCFiles", "zsh": "_dvc_compadd_DVCFiles"}
|
DVC_FILE = {"bash": "_dvc_compgen_DVCFiles", "zsh": "_dvc_compadd_DVCFiles"}
|
||||||
|
|
||||||
STAGE = {"bash": "_dvc_compgen_stages", "zsh": "_dvc_compadd_stages"}
|
STAGE = {"bash": "_dvc_compgen_stages", "zsh": "_dvc_compadd_stages"}
|
||||||
|
|
||||||
DVCFILES_AND_STAGE = {
|
DVCFILES_AND_STAGE = {
|
||||||
"bash": "_dvc_compgen_stages_and_files",
|
"bash": "_dvc_compgen_stages_and_files",
|
||||||
"zsh": "_dvc_compadd_stages_and_files",
|
"zsh": "_dvc_compadd_stages_and_files",
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
|
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
|
||||||
|
|
||||||
|
|
||||||
class CmdCompletion(CmdBaseNoRepo):
|
|
||||||
def run(self):
|
|
||||||
from dvc.cli import get_main_parser
|
|
||||||
|
|
||||||
parser = get_main_parser()
|
|
||||||
shell = self.args.shell
|
|
||||||
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
|
|
||||||
ui.write(script, force=True)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
|
||||||
COMPLETION_HELP = "Generate shell tab completion."
|
|
||||||
COMPLETION_DESCRIPTION = "Prints out shell tab completion scripts."
|
|
||||||
completion_parser = subparsers.add_parser(
|
|
||||||
"completion",
|
|
||||||
parents=[parent_parser],
|
|
||||||
description=append_doc_link(COMPLETION_DESCRIPTION, "completion"),
|
|
||||||
help=COMPLETION_HELP,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
completion_parser.add_argument(
|
|
||||||
"-s",
|
|
||||||
"--shell",
|
|
||||||
help="Shell syntax for completions.",
|
|
||||||
default="bash",
|
|
||||||
choices=["bash", "zsh"],
|
|
||||||
)
|
|
||||||
completion_parser.set_defaults(func=CmdCompletion)
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
"""DVC command line interface"""
|
"""Main parser for the dvc cli."""
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ._debug import add_debugging_flags
|
from dvc.commands import (
|
||||||
from .command import (
|
|
||||||
add,
|
add,
|
||||||
cache,
|
cache,
|
||||||
check_ignore,
|
check_ignore,
|
||||||
@@ -28,7 +27,6 @@ from .command import (
|
|||||||
imp_url,
|
imp_url,
|
||||||
init,
|
init,
|
||||||
install,
|
install,
|
||||||
live,
|
|
||||||
ls,
|
ls,
|
||||||
machine,
|
machine,
|
||||||
metrics,
|
metrics,
|
||||||
@@ -45,8 +43,8 @@ from .command import (
|
|||||||
update,
|
update,
|
||||||
version,
|
version,
|
||||||
)
|
)
|
||||||
from .command.base import fix_subparsers
|
|
||||||
from .exceptions import DvcParserError
|
from . import DvcParserError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -87,7 +85,6 @@ COMMANDS = [
|
|||||||
stage,
|
stage,
|
||||||
experiments,
|
experiments,
|
||||||
check_ignore,
|
check_ignore,
|
||||||
live,
|
|
||||||
machine,
|
machine,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -96,7 +93,7 @@ def _find_parser(parser, cmd_cls):
|
|||||||
defaults = parser._defaults # pylint: disable=protected-access
|
defaults = parser._defaults # pylint: disable=protected-access
|
||||||
if not cmd_cls or cmd_cls == defaults.get("func"):
|
if not cmd_cls or cmd_cls == defaults.get("func"):
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
raise DvcParserError()
|
raise DvcParserError
|
||||||
|
|
||||||
actions = parser._actions # pylint: disable=protected-access
|
actions = parser._actions # pylint: disable=protected-access
|
||||||
for action in actions:
|
for action in actions:
|
||||||
@@ -143,6 +140,8 @@ def get_parent_parser():
|
|||||||
When overwriting `-q` or `-v`, you need to instantiate a new object
|
When overwriting `-q` or `-v`, you need to instantiate a new object
|
||||||
in order to prevent some weird behavior.
|
in order to prevent some weird behavior.
|
||||||
"""
|
"""
|
||||||
|
from dvc._debug import add_debugging_flags
|
||||||
|
|
||||||
parent_parser = argparse.ArgumentParser(add_help=False)
|
parent_parser = argparse.ArgumentParser(add_help=False)
|
||||||
add_debugging_flags(parent_parser)
|
add_debugging_flags(parent_parser)
|
||||||
log_level_group = parent_parser.add_mutually_exclusive_group()
|
log_level_group = parent_parser.add_mutually_exclusive_group()
|
||||||
@@ -207,23 +206,11 @@ def get_main_parser():
|
|||||||
help="Use `dvc COMMAND --help` for command-specific help.",
|
help="Use `dvc COMMAND --help` for command-specific help.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .utils import fix_subparsers
|
||||||
|
|
||||||
fix_subparsers(subparsers)
|
fix_subparsers(subparsers)
|
||||||
|
|
||||||
for cmd in COMMANDS:
|
for cmd in COMMANDS:
|
||||||
cmd.add_parser(subparsers, parent_parser)
|
cmd.add_parser(subparsers, parent_parser)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def parse_args(argv=None):
|
|
||||||
"""Parses CLI arguments.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: optional list of arguments to parse. sys.argv is used by default.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
dvc.exceptions.DvcParserError: raised for argument parsing errors.
|
|
||||||
"""
|
|
||||||
parser = get_main_parser()
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
return args
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
def fix_subparsers(subparsers):
|
||||||
|
"""Workaround for bug in Python 3. See more info at:
|
||||||
|
https://bugs.python.org/issue16308
|
||||||
|
https://github.com/iterative/dvc/issues/769
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subparsers: subparsers to fix.
|
||||||
|
"""
|
||||||
|
subparsers.required = True
|
||||||
|
subparsers.dest = "cmd"
|
||||||
|
|
||||||
|
|
||||||
|
def append_doc_link(help_message, path):
|
||||||
|
from dvc.utils import format_link
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
return help_message
|
||||||
|
doc_base = "https://man.dvc.org/"
|
||||||
|
return "{message}\nDocumentation: {link}".format(
|
||||||
|
message=help_message, link=format_link(doc_base + path)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fix_plumbing_subparsers(subparsers):
|
||||||
|
# metavar needs to be explicitly set in order to hide plumbing subcommands
|
||||||
|
# from the 'positional arguments' choices list
|
||||||
|
# see: https://bugs.python.org/issue22848
|
||||||
|
cmds = [
|
||||||
|
cmd for cmd, parser in subparsers.choices.items() if parser.add_help
|
||||||
|
]
|
||||||
|
subparsers.metavar = "{{{}}}".format(",".join(cmds))
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,93 +0,0 @@
|
|||||||
import argparse
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from dvc.command import completion
|
|
||||||
from dvc.command.base import CmdBase, fix_subparsers
|
|
||||||
from dvc.ui import ui
|
|
||||||
|
|
||||||
|
|
||||||
class CmdLive(CmdBase):
|
|
||||||
UNINITIALIZED = True
|
|
||||||
|
|
||||||
def _run(self, target, revs=None):
|
|
||||||
metrics, plots = self.repo.live.show(target=target, revs=revs)
|
|
||||||
|
|
||||||
if plots:
|
|
||||||
html_path = Path.cwd() / (self.args.target + "_html")
|
|
||||||
from dvc.render.utils import render
|
|
||||||
|
|
||||||
index_path = render(self.repo, plots, metrics, html_path)
|
|
||||||
ui.write(index_path.as_uri())
|
|
||||||
return 0
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
class CmdLiveShow(CmdLive):
|
|
||||||
def run(self):
|
|
||||||
return self._run(self.args.target)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdLiveDiff(CmdLive):
|
|
||||||
def run(self):
|
|
||||||
return self._run(self.args.target, self.args.revs)
|
|
||||||
|
|
||||||
|
|
||||||
def shared_parent_parser():
|
|
||||||
parent_parser = argparse.ArgumentParser(add_help=False)
|
|
||||||
parent_parser.add_argument(
|
|
||||||
"target", help="Logs dir to produce summary from"
|
|
||||||
).complete = completion.DIR
|
|
||||||
parent_parser.add_argument(
|
|
||||||
"-o",
|
|
||||||
"--out",
|
|
||||||
default=None,
|
|
||||||
help="Destination path to save plots to",
|
|
||||||
metavar="<path>",
|
|
||||||
).complete = completion.DIR
|
|
||||||
return parent_parser
|
|
||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
|
||||||
LIVE_DESCRIPTION = (
|
|
||||||
"Commands to visualize and compare dvclive-produced logs."
|
|
||||||
)
|
|
||||||
live_parser = subparsers.add_parser(
|
|
||||||
"live",
|
|
||||||
parents=[parent_parser],
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
description=LIVE_DESCRIPTION,
|
|
||||||
)
|
|
||||||
live_subparsers = live_parser.add_subparsers(
|
|
||||||
dest="cmd",
|
|
||||||
help="Use `dvc live CMD --help` to display command-specific help.",
|
|
||||||
)
|
|
||||||
|
|
||||||
fix_subparsers(live_subparsers)
|
|
||||||
|
|
||||||
SHOW_HELP = "Visualize dvclive directory content."
|
|
||||||
live_show_parser = live_subparsers.add_parser(
|
|
||||||
"show",
|
|
||||||
parents=[parent_parser, shared_parent_parser()],
|
|
||||||
help=SHOW_HELP,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
live_show_parser.set_defaults(func=CmdLiveShow)
|
|
||||||
|
|
||||||
DIFF_HELP = (
|
|
||||||
"Show multiple versions of dvclive data, "
|
|
||||||
"by plotting it in single view."
|
|
||||||
)
|
|
||||||
live_diff_parser = live_subparsers.add_parser(
|
|
||||||
"diff",
|
|
||||||
parents=[parent_parser, shared_parent_parser()],
|
|
||||||
help=DIFF_HELP,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
live_diff_parser.add_argument(
|
|
||||||
"--revs",
|
|
||||||
nargs="*",
|
|
||||||
default=None,
|
|
||||||
help="Git revision (e.g. SHA, branch, tag)",
|
|
||||||
metavar="<commit>",
|
|
||||||
)
|
|
||||||
live_diff_parser.set_defaults(func=CmdLiveDiff)
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from dvc.command import completion
|
|
||||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
|
||||||
from dvc.exceptions import DvcException
|
|
||||||
from dvc.render.utils import find_vega, render
|
|
||||||
from dvc.ui import ui
|
|
||||||
from dvc.utils import format_link
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdPlots(CmdBase):
|
|
||||||
def _func(self, *args, **kwargs):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _props(self):
|
|
||||||
from dvc.schema import PLOT_PROPS
|
|
||||||
|
|
||||||
# Pass only props specified by user, to not shadow ones from plot def
|
|
||||||
props = {p: getattr(self.args, p) for p in PLOT_PROPS}
|
|
||||||
return {k: v for k, v in props.items() if v is not None}
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
if self.args.show_vega:
|
|
||||||
if not self.args.targets:
|
|
||||||
logger.error("please specify a target for `--show-vega`")
|
|
||||||
return 1
|
|
||||||
if len(self.args.targets) > 1:
|
|
||||||
logger.error(
|
|
||||||
"you can only specify one target for `--show-vega`"
|
|
||||||
)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
plots_data = self._func(
|
|
||||||
targets=self.args.targets, props=self._props()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not plots_data:
|
|
||||||
ui.error_write(
|
|
||||||
"No plots were loaded, "
|
|
||||||
"visualization file will not be created."
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.args.show_vega:
|
|
||||||
target = self.args.targets[0]
|
|
||||||
plot_json = find_vega(self.repo, plots_data, target)
|
|
||||||
if plot_json:
|
|
||||||
ui.write(plot_json)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
rel: str = self.args.out or "dvc_plots"
|
|
||||||
path: Path = (Path.cwd() / rel).resolve()
|
|
||||||
index_path = render(
|
|
||||||
self.repo,
|
|
||||||
plots_data,
|
|
||||||
path=path,
|
|
||||||
html_template_path=self.args.html_template,
|
|
||||||
)
|
|
||||||
|
|
||||||
ui.write(index_path.as_uri())
|
|
||||||
|
|
||||||
if self.args.open:
|
|
||||||
return ui.open_browser(index_path)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
except DvcException:
|
|
||||||
logger.exception("")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
class CmdPlotsShow(CmdPlots):
|
|
||||||
UNINITIALIZED = True
|
|
||||||
|
|
||||||
def _func(self, *args, **kwargs):
|
|
||||||
return self.repo.plots.show(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdPlotsDiff(CmdPlots):
|
|
||||||
UNINITIALIZED = True
|
|
||||||
|
|
||||||
def _func(self, *args, **kwargs):
|
|
||||||
return self.repo.plots.diff(
|
|
||||||
*args,
|
|
||||||
revs=self.args.revisions,
|
|
||||||
experiment=self.args.experiment,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdPlotsModify(CmdPlots):
|
|
||||||
def run(self):
|
|
||||||
self.repo.plots.modify(
|
|
||||||
self.args.target, props=self._props(), unset=self.args.unset
|
|
||||||
)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
|
||||||
PLOTS_HELP = (
|
|
||||||
"Commands to visualize and compare plot metrics in structured files "
|
|
||||||
"(JSON, YAML, CSV, TSV)."
|
|
||||||
)
|
|
||||||
|
|
||||||
plots_parser = subparsers.add_parser(
|
|
||||||
"plots",
|
|
||||||
parents=[parent_parser],
|
|
||||||
description=append_doc_link(PLOTS_HELP, "plots"),
|
|
||||||
help=PLOTS_HELP,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
plots_subparsers = plots_parser.add_subparsers(
|
|
||||||
dest="cmd",
|
|
||||||
help="Use `dvc plots CMD --help` to display command-specific help.",
|
|
||||||
)
|
|
||||||
|
|
||||||
fix_subparsers(plots_subparsers)
|
|
||||||
|
|
||||||
SHOW_HELP = "Generate plots from metrics files."
|
|
||||||
plots_show_parser = plots_subparsers.add_parser(
|
|
||||||
"show",
|
|
||||||
parents=[parent_parser],
|
|
||||||
description=append_doc_link(SHOW_HELP, "plots/show"),
|
|
||||||
help=SHOW_HELP,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
plots_show_parser.add_argument(
|
|
||||||
"targets",
|
|
||||||
nargs="*",
|
|
||||||
help="Files to visualize (supports any file, "
|
|
||||||
"even when not found as `plots` in `dvc.yaml`). "
|
|
||||||
"Shows all plots by default.",
|
|
||||||
).complete = completion.FILE
|
|
||||||
_add_props_arguments(plots_show_parser)
|
|
||||||
_add_output_arguments(plots_show_parser)
|
|
||||||
plots_show_parser.set_defaults(func=CmdPlotsShow)
|
|
||||||
|
|
||||||
PLOTS_DIFF_HELP = (
|
|
||||||
"Show multiple versions of plot metrics "
|
|
||||||
"by plotting them in a single image."
|
|
||||||
)
|
|
||||||
plots_diff_parser = plots_subparsers.add_parser(
|
|
||||||
"diff",
|
|
||||||
parents=[parent_parser],
|
|
||||||
description=append_doc_link(PLOTS_DIFF_HELP, "plots/diff"),
|
|
||||||
help=PLOTS_DIFF_HELP,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
plots_diff_parser.add_argument(
|
|
||||||
"--targets",
|
|
||||||
nargs="*",
|
|
||||||
help=(
|
|
||||||
"Specific plots file(s) to visualize "
|
|
||||||
"(even if not found as `plots` in `dvc.yaml`). "
|
|
||||||
"Shows all tracked plots by default."
|
|
||||||
),
|
|
||||||
metavar="<paths>",
|
|
||||||
).complete = completion.FILE
|
|
||||||
plots_diff_parser.add_argument(
|
|
||||||
"-e",
|
|
||||||
"--experiment",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help=argparse.SUPPRESS,
|
|
||||||
)
|
|
||||||
plots_diff_parser.add_argument(
|
|
||||||
"revisions", nargs="*", default=None, help="Git commits to plot from"
|
|
||||||
)
|
|
||||||
_add_props_arguments(plots_diff_parser)
|
|
||||||
_add_output_arguments(plots_diff_parser)
|
|
||||||
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
|
|
||||||
|
|
||||||
PLOTS_MODIFY_HELP = (
|
|
||||||
"Modify display properties of data-series plots "
|
|
||||||
"(has no effect on image-type plots)."
|
|
||||||
)
|
|
||||||
plots_modify_parser = plots_subparsers.add_parser(
|
|
||||||
"modify",
|
|
||||||
parents=[parent_parser],
|
|
||||||
description=append_doc_link(PLOTS_MODIFY_HELP, "plots/modify"),
|
|
||||||
help=PLOTS_MODIFY_HELP,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
plots_modify_parser.add_argument(
|
|
||||||
"target", help="Metric file to set properties to"
|
|
||||||
).complete = completion.FILE
|
|
||||||
_add_props_arguments(plots_modify_parser)
|
|
||||||
plots_modify_parser.add_argument(
|
|
||||||
"--unset",
|
|
||||||
nargs="*",
|
|
||||||
metavar="<property>",
|
|
||||||
help="Unset one or more display properties.",
|
|
||||||
)
|
|
||||||
plots_modify_parser.set_defaults(func=CmdPlotsModify)
|
|
||||||
|
|
||||||
|
|
||||||
def _add_props_arguments(parser):
|
|
||||||
parser.add_argument(
|
|
||||||
"-t",
|
|
||||||
"--template",
|
|
||||||
nargs="?",
|
|
||||||
default=None,
|
|
||||||
help=(
|
|
||||||
"Special JSON or HTML schema file to inject with the data. "
|
|
||||||
"See {}".format(
|
|
||||||
format_link("https://man.dvc.org/plots#plot-templates")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
metavar="<path>",
|
|
||||||
).complete = completion.FILE
|
|
||||||
parser.add_argument(
|
|
||||||
"-x", default=None, help="Field name for X axis.", metavar="<field>"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-y", default=None, help="Field name for Y axis.", metavar="<field>"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--no-header",
|
|
||||||
action="store_false",
|
|
||||||
dest="header",
|
|
||||||
default=None, # Use default None to distinguish when it's not used
|
|
||||||
help="Provided CSV or TSV datafile does not have a header.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--title", default=None, metavar="<text>", help="Plot title."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--x-label", default=None, help="X axis label", metavar="<text>"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--y-label", default=None, help="Y axis label", metavar="<text>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _add_output_arguments(parser):
|
|
||||||
parser.add_argument(
|
|
||||||
"-o",
|
|
||||||
"--out",
|
|
||||||
default=None,
|
|
||||||
help="Destination path to save plots to",
|
|
||||||
metavar="<path>",
|
|
||||||
).complete = completion.DIR
|
|
||||||
parser.add_argument(
|
|
||||||
"--show-vega",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Show output in Vega format.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--open",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Open plot file directly in the browser.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--html-template",
|
|
||||||
default=None,
|
|
||||||
help="Custom HTML template for VEGA visualization.",
|
|
||||||
metavar="<path>",
|
|
||||||
)
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -101,9 +102,10 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"--jobs",
|
"--jobs",
|
||||||
type=int,
|
type=int,
|
||||||
help=(
|
help=(
|
||||||
"Number of jobs to run simultaneously. "
|
"Only used along with '--to-remote'. "
|
||||||
|
"Number of jobs to run simultaneously "
|
||||||
|
"when pushing data to remote."
|
||||||
"The default value is 4 * cpu_count(). "
|
"The default value is 4 * cpu_count(). "
|
||||||
"For SSH remotes, the default is 4. "
|
|
||||||
),
|
),
|
||||||
metavar="<number>",
|
metavar="<number>",
|
||||||
)
|
)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import append_doc_link, fix_subparsers
|
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||||
from dvc.command.config import CmdConfig
|
from dvc.commands.config import CmdConfig
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ class CmdCacheDir(CmdConfig):
|
|||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
from dvc.command.config import parent_config_parser
|
from dvc.commands.config import parent_config_parser
|
||||||
|
|
||||||
CACHE_HELP = "Manage cache settings."
|
CACHE_HELP = "Manage cache settings."
|
||||||
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.exceptions import CheckoutError
|
from dvc.exceptions import CheckoutError
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
@@ -54,6 +55,10 @@ class CmdCheckout(CmdBase):
|
|||||||
|
|
||||||
if exc:
|
if exc:
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
|
if self.args.relink:
|
||||||
|
msg = "Relinked successfully"
|
||||||
|
ui.write(msg)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
|
from dvc.cli.completion import PREAMBLE
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdCompletion(CmdBaseNoRepo):
|
||||||
|
def run(self):
|
||||||
|
import shtab
|
||||||
|
|
||||||
|
shell = self.args.shell
|
||||||
|
parser = self.args.parser
|
||||||
|
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
|
||||||
|
ui.write(script, force=True)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(subparsers, parent_parser):
|
||||||
|
COMPLETION_HELP = "Generate shell tab completion."
|
||||||
|
COMPLETION_DESCRIPTION = "Prints out shell tab completion scripts."
|
||||||
|
completion_parser = subparsers.add_parser(
|
||||||
|
"completion",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(COMPLETION_DESCRIPTION, "completion"),
|
||||||
|
help=COMPLETION_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
completion_parser.add_argument(
|
||||||
|
"-s",
|
||||||
|
"--shell",
|
||||||
|
help="Shell syntax for completions.",
|
||||||
|
default="bash",
|
||||||
|
choices=["bash", "zsh"],
|
||||||
|
)
|
||||||
|
completion_parser.set_defaults(func=CmdCompletion)
|
||||||
@@ -2,7 +2,8 @@ import argparse
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBaseNoRepo, fix_subparsers
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
|
from dvc.cli.utils import fix_subparsers
|
||||||
|
|
||||||
|
|
||||||
class CmdDaemonBase(CmdBaseNoRepo):
|
class CmdDaemonBase(CmdBaseNoRepo):
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from networkx import DiGraph
|
||||||
|
|
||||||
def _show_ascii(G):
|
|
||||||
|
def _show_ascii(G: "DiGraph"):
|
||||||
from dvc.dagascii import draw
|
from dvc.dagascii import draw
|
||||||
from dvc.repo.graph import get_pipelines
|
from dvc.repo.graph import get_pipelines
|
||||||
|
|
||||||
@@ -17,16 +22,54 @@ def _show_ascii(G):
|
|||||||
return "\n".join(ret)
|
return "\n".join(ret)
|
||||||
|
|
||||||
|
|
||||||
def _show_dot(G):
|
def _quote_label(node):
|
||||||
|
label = str(node)
|
||||||
|
# Node names should not contain ":" unless they are quoted with "".
|
||||||
|
# See: https://github.com/pydot/pydot/issues/258.
|
||||||
|
if label[0] != '"' and label[-1] != '"':
|
||||||
|
return f'"{label}"'
|
||||||
|
return label
|
||||||
|
|
||||||
|
|
||||||
|
def _show_dot(G: "DiGraph"):
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
import networkx as nx
|
||||||
from networkx.drawing.nx_pydot import write_dot
|
from networkx.drawing.nx_pydot import write_dot
|
||||||
|
|
||||||
dot_file = io.StringIO()
|
dot_file = io.StringIO()
|
||||||
write_dot(G, dot_file)
|
|
||||||
|
nx.relabel_nodes(G, _quote_label, copy=False)
|
||||||
|
write_dot(G.reverse(), dot_file)
|
||||||
return dot_file.getvalue()
|
return dot_file.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _show_mermaid(G, markdown: bool = False):
|
||||||
|
from dvc.repo.graph import get_pipelines
|
||||||
|
|
||||||
|
pipelines = get_pipelines(G)
|
||||||
|
|
||||||
|
graph = "flowchart TD"
|
||||||
|
|
||||||
|
total_nodes = 0
|
||||||
|
for pipeline in pipelines:
|
||||||
|
node_ids = {}
|
||||||
|
nodes = sorted(str(x) for x in pipeline.nodes)
|
||||||
|
for node in nodes:
|
||||||
|
total_nodes += 1
|
||||||
|
node_id = f"node{total_nodes}"
|
||||||
|
graph += f'\n\t{node_id}["{node}"]'
|
||||||
|
node_ids[node] = node_id
|
||||||
|
edges = sorted((str(a), str(b)) for b, a in pipeline.edges)
|
||||||
|
for a, b in edges:
|
||||||
|
graph += f"\n\t{node_ids[str(a)]}-->{node_ids[str(b)]}"
|
||||||
|
|
||||||
|
if markdown:
|
||||||
|
return f"```mermaid\n{graph}\n```"
|
||||||
|
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
def _collect_targets(repo, target, outs):
|
def _collect_targets(repo, target, outs):
|
||||||
if not target:
|
if not target:
|
||||||
return []
|
return []
|
||||||
@@ -43,9 +86,9 @@ def _collect_targets(repo, target, outs):
|
|||||||
targets.extend([str(out) for out in stage.outs])
|
targets.extend([str(out) for out in stage.outs])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for out in outs_trie.itervalues(
|
for out in outs_trie.itervalues( # noqa: B301
|
||||||
prefix=repo.fs.path.parts(path)
|
prefix=repo.fs.path.parts(path)
|
||||||
): # noqa: B301
|
):
|
||||||
targets.extend(str(out))
|
targets.extend(str(out))
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
@@ -104,6 +147,8 @@ class CmdDAG(CmdBase):
|
|||||||
|
|
||||||
if self.args.dot:
|
if self.args.dot:
|
||||||
ui.write(_show_dot(G))
|
ui.write(_show_dot(G))
|
||||||
|
elif self.args.mermaid or self.args.markdown:
|
||||||
|
ui.write(_show_mermaid(G, self.args.markdown))
|
||||||
else:
|
else:
|
||||||
with ui.pager():
|
with ui.pager():
|
||||||
ui.write(_show_ascii(G))
|
ui.write(_show_ascii(G))
|
||||||
@@ -126,6 +171,20 @@ def add_parser(subparsers, parent_parser):
|
|||||||
default=False,
|
default=False,
|
||||||
help="Print DAG with .dot format.",
|
help="Print DAG with .dot format.",
|
||||||
)
|
)
|
||||||
|
dag_parser.add_argument(
|
||||||
|
"--mermaid",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Print DAG with mermaid format.",
|
||||||
|
)
|
||||||
|
dag_parser.add_argument(
|
||||||
|
"--md",
|
||||||
|
"--show-md",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
dest="markdown",
|
||||||
|
help="Print DAG with mermaid format wrapped in Markdown block.",
|
||||||
|
)
|
||||||
dag_parser.add_argument(
|
dag_parser.add_argument(
|
||||||
"--full",
|
"--full",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ class CmdDataBase(CmdBase):
|
|||||||
|
|
||||||
class CmdDataPull(CmdDataBase):
|
class CmdDataPull(CmdDataBase):
|
||||||
def log_summary(self, stats):
|
def log_summary(self, stats):
|
||||||
from dvc.command.checkout import log_changes
|
from dvc.commands.checkout import log_changes
|
||||||
|
|
||||||
log_changes(stats)
|
log_changes(stats)
|
||||||
super().log_summary(stats)
|
super().log_summary(stats)
|
||||||
@@ -97,7 +98,7 @@ class CmdDataFetch(CmdDataBase):
|
|||||||
|
|
||||||
|
|
||||||
def shared_parent_parser():
|
def shared_parent_parser():
|
||||||
from dvc.cli import get_parent_parser
|
from dvc.cli.parser import get_parent_parser
|
||||||
|
|
||||||
# Parent parser used in pull/push/status
|
# Parent parser used in pull/push/status
|
||||||
parent_parser = argparse.ArgumentParser(
|
parent_parser = argparse.ArgumentParser(
|
||||||
@@ -110,7 +111,6 @@ def shared_parent_parser():
|
|||||||
help=(
|
help=(
|
||||||
"Number of jobs to run simultaneously. "
|
"Number of jobs to run simultaneously. "
|
||||||
"The default value is 4 * cpu_count(). "
|
"The default value is 4 * cpu_count(). "
|
||||||
"For SSH remotes, the default is 4. "
|
|
||||||
),
|
),
|
||||||
metavar="<number>",
|
metavar="<number>",
|
||||||
)
|
)
|
||||||
@@ -127,7 +127,7 @@ def shared_parent_parser():
|
|||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, _parent_parser):
|
def add_parser(subparsers, _parent_parser):
|
||||||
from dvc.command.status import CmdDataStatus
|
from dvc.commands.status import CmdDataStatus
|
||||||
|
|
||||||
# Pull
|
# Pull
|
||||||
PULL_HELP = "Download tracked files or directories from remote storage."
|
PULL_HELP = "Download tracked files or directories from remote storage."
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -2,8 +2,9 @@ import argparse
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -127,7 +128,7 @@ class CmdDiff(CmdBase):
|
|||||||
show_hash = self.args.show_hash
|
show_hash = self.args.show_hash
|
||||||
hide_missing = self.args.b_rev or self.args.hide_missing
|
hide_missing = self.args.b_rev or self.args.hide_missing
|
||||||
if hide_missing:
|
if hide_missing:
|
||||||
del diff["not in cache"]
|
diff.pop("not in cache", None)
|
||||||
|
|
||||||
for key, entries in diff.items():
|
for key, entries in diff.items():
|
||||||
entries = sorted(
|
entries = sorted(
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import argparse
|
||||||
|
|
||||||
|
from dvc.cli.utils import (
|
||||||
|
append_doc_link,
|
||||||
|
fix_plumbing_subparsers,
|
||||||
|
fix_subparsers,
|
||||||
|
)
|
||||||
|
from dvc.commands.experiments import (
|
||||||
|
apply,
|
||||||
|
branch,
|
||||||
|
diff,
|
||||||
|
exec_run,
|
||||||
|
gc,
|
||||||
|
init,
|
||||||
|
ls,
|
||||||
|
pull,
|
||||||
|
push,
|
||||||
|
remove,
|
||||||
|
run,
|
||||||
|
show,
|
||||||
|
)
|
||||||
|
|
||||||
|
SUB_COMMANDS = [
|
||||||
|
apply,
|
||||||
|
branch,
|
||||||
|
diff,
|
||||||
|
exec_run,
|
||||||
|
gc,
|
||||||
|
init,
|
||||||
|
ls,
|
||||||
|
pull,
|
||||||
|
push,
|
||||||
|
remove,
|
||||||
|
run,
|
||||||
|
show,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(subparsers, parent_parser):
|
||||||
|
EXPERIMENTS_HELP = "Commands to run and compare experiments."
|
||||||
|
|
||||||
|
experiments_parser = subparsers.add_parser(
|
||||||
|
"experiments",
|
||||||
|
parents=[parent_parser],
|
||||||
|
aliases=["exp"],
|
||||||
|
description=append_doc_link(EXPERIMENTS_HELP, "exp"),
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
help=EXPERIMENTS_HELP,
|
||||||
|
)
|
||||||
|
|
||||||
|
experiments_subparsers = experiments_parser.add_subparsers(
|
||||||
|
dest="cmd",
|
||||||
|
help="Use `dvc experiments CMD --help` to display "
|
||||||
|
"command-specific help.",
|
||||||
|
)
|
||||||
|
|
||||||
|
fix_subparsers(experiments_subparsers)
|
||||||
|
for cmd in SUB_COMMANDS:
|
||||||
|
cmd.add_parser(experiments_subparsers, parent_parser)
|
||||||
|
fix_plumbing_subparsers(experiments_subparsers)
|
||||||
|
|
||||||
|
|
||||||
|
def add_rev_selection_flags(
|
||||||
|
experiments_subcmd_parser, command: str, default: bool = True
|
||||||
|
):
|
||||||
|
experiments_subcmd_parser.add_argument(
|
||||||
|
"-A",
|
||||||
|
"--all-commits",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=(
|
||||||
|
f"{command} all experiments in the repository "
|
||||||
|
"(overrides `--rev` and `--num`)."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
default_msg = " (HEAD by default)" if default else ""
|
||||||
|
msg = (
|
||||||
|
f"{command} experiments derived from the specified `<commit>` as "
|
||||||
|
f"baseline{default_msg}."
|
||||||
|
)
|
||||||
|
experiments_subcmd_parser.add_argument(
|
||||||
|
"--rev",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help=msg,
|
||||||
|
metavar="<commit>",
|
||||||
|
)
|
||||||
|
experiments_subcmd_parser.add_argument(
|
||||||
|
"-n",
|
||||||
|
"--num",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
dest="num",
|
||||||
|
metavar="<num>",
|
||||||
|
help=(
|
||||||
|
f"{command} experiments from the last `num` commits "
|
||||||
|
"(first parents) starting from the `--rev` baseline. "
|
||||||
|
"Give a negative value to include all first-parent commits "
|
||||||
|
"(similar to `git log -n`)."
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli import completion
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsApply(CmdBase):
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
self.repo.experiments.apply(
|
||||||
|
self.args.experiment, force=self.args.force
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
|
||||||
|
EXPERIMENTS_APPLY_HELP = (
|
||||||
|
"Apply the changes from an experiment to your workspace."
|
||||||
|
)
|
||||||
|
experiments_apply_parser = experiments_subparsers.add_parser(
|
||||||
|
"apply",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_APPLY_HELP, "exp/apply"),
|
||||||
|
help=EXPERIMENTS_APPLY_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
experiments_apply_parser.add_argument(
|
||||||
|
"--no-force",
|
||||||
|
action="store_false",
|
||||||
|
dest="force",
|
||||||
|
help="Fail if this command would overwrite conflicting changes.",
|
||||||
|
)
|
||||||
|
experiments_apply_parser.add_argument(
|
||||||
|
"experiment", help="Experiment to be applied."
|
||||||
|
).complete = completion.EXPERIMENT
|
||||||
|
experiments_apply_parser.set_defaults(func=CmdExperimentsApply)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsBranch(CmdBase):
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
self.repo.experiments.branch(self.args.experiment, self.args.branch)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
|
||||||
|
EXPERIMENTS_BRANCH_HELP = "Promote an experiment to a Git branch."
|
||||||
|
experiments_branch_parser = experiments_subparsers.add_parser(
|
||||||
|
"branch",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_BRANCH_HELP, "exp/branch"),
|
||||||
|
help=EXPERIMENTS_BRANCH_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
experiments_branch_parser.add_argument(
|
||||||
|
"experiment", help="Experiment to be promoted."
|
||||||
|
)
|
||||||
|
experiments_branch_parser.add_argument(
|
||||||
|
"branch", help="Git branch name to use."
|
||||||
|
)
|
||||||
|
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli import completion
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.commands.metrics import DEFAULT_PRECISION
|
||||||
|
from dvc.exceptions import DvcException
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsDiff(CmdBase):
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
diff = self.repo.experiments.diff(
|
||||||
|
a_rev=self.args.a_rev,
|
||||||
|
b_rev=self.args.b_rev,
|
||||||
|
all=self.args.all,
|
||||||
|
param_deps=self.args.param_deps,
|
||||||
|
)
|
||||||
|
except DvcException:
|
||||||
|
logger.exception("failed to show experiments diff")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if self.args.json:
|
||||||
|
ui.write_json(diff)
|
||||||
|
else:
|
||||||
|
from dvc.compare import show_diff
|
||||||
|
|
||||||
|
precision = self.args.precision or DEFAULT_PRECISION
|
||||||
|
diffs = [("metrics", "Metric"), ("params", "Param")]
|
||||||
|
for idx, (key, title) in enumerate(diffs):
|
||||||
|
if idx:
|
||||||
|
# we are printing tables even in `--quiet` mode
|
||||||
|
# so we should also be printing the "table" separator
|
||||||
|
ui.write(force=True)
|
||||||
|
|
||||||
|
show_diff(
|
||||||
|
diff[key],
|
||||||
|
title=title,
|
||||||
|
markdown=self.args.markdown,
|
||||||
|
no_path=self.args.no_path,
|
||||||
|
on_empty_diff="diff not supported",
|
||||||
|
precision=precision if key == "metrics" else None,
|
||||||
|
a_rev=self.args.a_rev,
|
||||||
|
b_rev=self.args.b_rev,
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
|
||||||
|
EXPERIMENTS_DIFF_HELP = (
|
||||||
|
"Show changes between experiments in the DVC repository."
|
||||||
|
)
|
||||||
|
|
||||||
|
experiments_diff_parser = experiments_subparsers.add_parser(
|
||||||
|
"diff",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_DIFF_HELP, "exp/diff"),
|
||||||
|
help=EXPERIMENTS_DIFF_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
experiments_diff_parser.add_argument(
|
||||||
|
"a_rev", nargs="?", help="Old experiment to compare (defaults to HEAD)"
|
||||||
|
).complete = completion.EXPERIMENT
|
||||||
|
experiments_diff_parser.add_argument(
|
||||||
|
"b_rev",
|
||||||
|
nargs="?",
|
||||||
|
help="New experiment to compare (defaults to the current workspace)",
|
||||||
|
).complete = completion.EXPERIMENT
|
||||||
|
experiments_diff_parser.add_argument(
|
||||||
|
"--all",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Show unchanged metrics/params as well.",
|
||||||
|
)
|
||||||
|
experiments_diff_parser.add_argument(
|
||||||
|
"--param-deps",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Show only params that are stage dependencies.",
|
||||||
|
)
|
||||||
|
experiments_diff_parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
"--show-json",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Show output in JSON format.",
|
||||||
|
)
|
||||||
|
experiments_diff_parser.add_argument(
|
||||||
|
"--md",
|
||||||
|
"--show-md",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
dest="markdown",
|
||||||
|
help="Show tabulated output in the Markdown format (GFM).",
|
||||||
|
)
|
||||||
|
experiments_diff_parser.add_argument(
|
||||||
|
"--no-path",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Don't show metric/param path.",
|
||||||
|
)
|
||||||
|
experiments_diff_parser.add_argument(
|
||||||
|
"--precision",
|
||||||
|
type=int,
|
||||||
|
help=(
|
||||||
|
"Round metrics/params to `n` digits precision after the decimal "
|
||||||
|
f"point. Rounds to {DEFAULT_PRECISION} digits by default."
|
||||||
|
),
|
||||||
|
metavar="<n>",
|
||||||
|
)
|
||||||
|
experiments_diff_parser.set_defaults(func=CmdExperimentsDiff)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExecutorRun(CmdBaseNoRepo):
|
||||||
|
"""Run an experiment executor."""
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
from dvc.repo.experiments.executor.base import (
|
||||||
|
BaseExecutor,
|
||||||
|
ExecutorInfo,
|
||||||
|
)
|
||||||
|
from dvc.utils.serialize import load_json
|
||||||
|
|
||||||
|
info = ExecutorInfo.from_dict(load_json(self.args.infofile))
|
||||||
|
BaseExecutor.reproduce(
|
||||||
|
info=info,
|
||||||
|
rev="",
|
||||||
|
queue=None,
|
||||||
|
log_level=logger.getEffectiveLevel(),
|
||||||
|
infofile=self.args.infofile,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
EXEC_RUN_HELP = "Run an experiment executor."
|
||||||
|
exec_run_parser = experiments_subparsers.add_parser(
|
||||||
|
"exec-run",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=EXEC_RUN_HELP,
|
||||||
|
add_help=False,
|
||||||
|
)
|
||||||
|
exec_run_parser.add_argument(
|
||||||
|
"--infofile",
|
||||||
|
help="Path to executor info file",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
exec_run_parser.set_defaults(func=CmdExecutorRun)
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.exceptions import InvalidArgumentError
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_error_if_all_disabled(**kwargs):
|
||||||
|
if not any(kwargs.values()):
|
||||||
|
raise InvalidArgumentError(
|
||||||
|
"Either of `-w|--workspace`, `-a|--all-branches`, `-T|--all-tags` "
|
||||||
|
"or `--all-commits` needs to be set."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsGC(CmdBase):
|
||||||
|
def run(self):
|
||||||
|
_raise_error_if_all_disabled(
|
||||||
|
all_branches=self.args.all_branches,
|
||||||
|
all_tags=self.args.all_tags,
|
||||||
|
all_commits=self.args.all_commits,
|
||||||
|
workspace=self.args.workspace,
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = "This will remove all experiments except those derived from "
|
||||||
|
|
||||||
|
msg += "the workspace"
|
||||||
|
if self.args.all_commits:
|
||||||
|
msg += " and all git commits"
|
||||||
|
elif self.args.all_branches and self.args.all_tags:
|
||||||
|
msg += " and all git branches and tags"
|
||||||
|
elif self.args.all_branches:
|
||||||
|
msg += " and all git branches"
|
||||||
|
elif self.args.all_tags:
|
||||||
|
msg += " and all git tags"
|
||||||
|
msg += " of the current repo."
|
||||||
|
if self.args.queued:
|
||||||
|
msg += " Run queued experiments will be preserved."
|
||||||
|
else:
|
||||||
|
msg += " Run queued experiments will be removed."
|
||||||
|
|
||||||
|
logger.warning(msg)
|
||||||
|
|
||||||
|
msg = "Are you sure you want to proceed?"
|
||||||
|
if not self.args.force and not ui.confirm(msg):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
removed = self.repo.experiments.gc(
|
||||||
|
all_branches=self.args.all_branches,
|
||||||
|
all_tags=self.args.all_tags,
|
||||||
|
all_commits=self.args.all_commits,
|
||||||
|
workspace=self.args.workspace,
|
||||||
|
queued=self.args.queued,
|
||||||
|
)
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
ui.write(
|
||||||
|
f"Removed {removed} experiments.",
|
||||||
|
"To remove unused cache files",
|
||||||
|
"use 'dvc gc'.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ui.write("No experiments to remove.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
|
||||||
|
EXPERIMENTS_GC_HELP = "Garbage collect unneeded experiments."
|
||||||
|
EXPERIMENTS_GC_DESCRIPTION = (
|
||||||
|
"Removes all experiments which are not derived from the specified"
|
||||||
|
"Git revisions."
|
||||||
|
)
|
||||||
|
experiments_gc_parser = experiments_subparsers.add_parser(
|
||||||
|
"gc",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_GC_DESCRIPTION, "exp/gc"),
|
||||||
|
help=EXPERIMENTS_GC_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
experiments_gc_parser.add_argument(
|
||||||
|
"-w",
|
||||||
|
"--workspace",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Keep experiments derived from the current workspace.",
|
||||||
|
)
|
||||||
|
experiments_gc_parser.add_argument(
|
||||||
|
"-a",
|
||||||
|
"--all-branches",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Keep experiments derived from the tips of all Git branches.",
|
||||||
|
)
|
||||||
|
experiments_gc_parser.add_argument(
|
||||||
|
"-T",
|
||||||
|
"--all-tags",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Keep experiments derived from all Git tags.",
|
||||||
|
)
|
||||||
|
experiments_gc_parser.add_argument(
|
||||||
|
"-A",
|
||||||
|
"--all-commits",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Keep experiments derived from all Git commits.",
|
||||||
|
)
|
||||||
|
experiments_gc_parser.add_argument(
|
||||||
|
"--queued",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=(
|
||||||
|
"Keep queued experiments (experiments run queue will be cleared "
|
||||||
|
"by default)."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
experiments_gc_parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Force garbage collection - automatically agree to all prompts.",
|
||||||
|
)
|
||||||
|
experiments_gc_parser.set_defaults(func=CmdExperimentsGC)
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, List
|
||||||
|
|
||||||
|
from funcy import compact
|
||||||
|
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.exceptions import InvalidArgumentError
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dvc.dependency import Dependency
|
||||||
|
from dvc.stage import PipelineStage
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsInit(CmdBase):
|
||||||
|
DEFAULT_NAME = "train"
|
||||||
|
CODE = "src"
|
||||||
|
DATA = "data"
|
||||||
|
MODELS = "models"
|
||||||
|
DEFAULT_METRICS = "metrics.json"
|
||||||
|
DEFAULT_PARAMS = "params.yaml"
|
||||||
|
PLOTS = "plots"
|
||||||
|
DEFAULTS = {
|
||||||
|
"code": CODE,
|
||||||
|
"data": DATA,
|
||||||
|
"models": MODELS,
|
||||||
|
"metrics": DEFAULT_METRICS,
|
||||||
|
"params": DEFAULT_PARAMS,
|
||||||
|
"plots": PLOTS,
|
||||||
|
}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
from dvc.commands.stage import parse_cmd
|
||||||
|
|
||||||
|
cmd = parse_cmd(self.args.command)
|
||||||
|
if not self.args.interactive and not cmd:
|
||||||
|
raise InvalidArgumentError("command is not specified")
|
||||||
|
|
||||||
|
from dvc.repo.experiments.init import init
|
||||||
|
|
||||||
|
defaults = {}
|
||||||
|
if not self.args.explicit:
|
||||||
|
config = self.repo.config["exp"]
|
||||||
|
defaults.update({**self.DEFAULTS, **config})
|
||||||
|
|
||||||
|
cli_args = compact(
|
||||||
|
{
|
||||||
|
"cmd": cmd,
|
||||||
|
"code": self.args.code,
|
||||||
|
"data": self.args.data,
|
||||||
|
"models": self.args.models,
|
||||||
|
"metrics": self.args.metrics,
|
||||||
|
"params": self.args.params,
|
||||||
|
"plots": self.args.plots,
|
||||||
|
"live": self.args.live,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
initialized_stage, initialized_deps, initialized_out_dirs = init(
|
||||||
|
self.repo,
|
||||||
|
name=self.args.name,
|
||||||
|
type=self.args.type,
|
||||||
|
defaults=defaults,
|
||||||
|
overrides=cli_args,
|
||||||
|
interactive=self.args.interactive,
|
||||||
|
force=self.args.force,
|
||||||
|
)
|
||||||
|
self._post_init_display(
|
||||||
|
initialized_stage, initialized_deps, initialized_out_dirs
|
||||||
|
)
|
||||||
|
if self.args.run:
|
||||||
|
self.repo.experiments.run(targets=[initialized_stage.addressing])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _post_init_display(
|
||||||
|
self,
|
||||||
|
stage: "PipelineStage",
|
||||||
|
new_deps: List["Dependency"],
|
||||||
|
new_out_dirs: List[str],
|
||||||
|
) -> None:
|
||||||
|
from dvc.utils import humanize
|
||||||
|
|
||||||
|
path_fmt = "[green]{}[/green]".format
|
||||||
|
if new_deps:
|
||||||
|
deps_paths = humanize.join(map(path_fmt, new_deps))
|
||||||
|
ui.write(f"Creating dependencies: {deps_paths}", styled=True)
|
||||||
|
|
||||||
|
if new_out_dirs:
|
||||||
|
out_dirs_paths = humanize.join(map(path_fmt, new_out_dirs))
|
||||||
|
ui.write(
|
||||||
|
f"Creating output directories: {out_dirs_paths}", styled=True
|
||||||
|
)
|
||||||
|
|
||||||
|
ui.write(
|
||||||
|
f"Creating [b]{self.args.name}[/b] stage in [green]dvc.yaml[/]",
|
||||||
|
styled=True,
|
||||||
|
)
|
||||||
|
if stage.outs or not self.args.run:
|
||||||
|
# separate the above status-like messages with help/tips section
|
||||||
|
ui.write(styled=True)
|
||||||
|
|
||||||
|
if stage.outs:
|
||||||
|
outs_paths = humanize.join(map(path_fmt, stage.outs))
|
||||||
|
tips = f"Ensure your experiment command creates {outs_paths}."
|
||||||
|
ui.write(tips, styled=True)
|
||||||
|
|
||||||
|
if not self.args.run:
|
||||||
|
ui.write(
|
||||||
|
'You can now run your experiment using [b]"dvc exp run"[/].',
|
||||||
|
styled=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# separate between `exp.run` output and `dvc exp init` output
|
||||||
|
ui.write(styled=True)
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
|
||||||
|
EXPERIMENTS_INIT_HELP = "Quickly setup any project to use experiments."
|
||||||
|
experiments_init_parser = experiments_subparsers.add_parser(
|
||||||
|
"init",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_INIT_HELP, "exp/init"),
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
help=EXPERIMENTS_INIT_HELP,
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"command",
|
||||||
|
nargs=argparse.REMAINDER,
|
||||||
|
help="Command to execute.",
|
||||||
|
metavar="command",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--run",
|
||||||
|
action="store_true",
|
||||||
|
help="Run the experiment after initializing it",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--interactive",
|
||||||
|
"-i",
|
||||||
|
action="store_true",
|
||||||
|
help="Prompt for values that are not provided",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Overwrite existing stage",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--explicit",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Only use the path values explicitly provided",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--name",
|
||||||
|
"-n",
|
||||||
|
help="Name of the stage to create (default: %(default)s)",
|
||||||
|
default=CmdExperimentsInit.DEFAULT_NAME,
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--code",
|
||||||
|
help="Path to the source file or directory "
|
||||||
|
"which your experiments depend"
|
||||||
|
f" (default: {CmdExperimentsInit.CODE})",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--data",
|
||||||
|
help="Path to the data file or directory "
|
||||||
|
"which your experiments depend"
|
||||||
|
f" (default: {CmdExperimentsInit.DATA})",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--models",
|
||||||
|
help="Path to the model file or directory for your experiments"
|
||||||
|
f" (default: {CmdExperimentsInit.MODELS})",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--params",
|
||||||
|
help="Path to the parameters file for your experiments"
|
||||||
|
f" (default: {CmdExperimentsInit.DEFAULT_PARAMS})",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--metrics",
|
||||||
|
help="Path to the metrics file for your experiments"
|
||||||
|
f" (default: {CmdExperimentsInit.DEFAULT_METRICS})",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--plots",
|
||||||
|
help="Path to the plots file or directory for your experiments"
|
||||||
|
f" (default: {CmdExperimentsInit.PLOTS})",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--live",
|
||||||
|
help="Path to log dvclive outputs for your experiments",
|
||||||
|
)
|
||||||
|
experiments_init_parser.add_argument(
|
||||||
|
"--type",
|
||||||
|
choices=["default", "checkpoint"],
|
||||||
|
default="default",
|
||||||
|
help="Select type of stage to create (default: %(default)s)",
|
||||||
|
)
|
||||||
|
experiments_init_parser.set_defaults(func=CmdExperimentsInit)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsList(CmdBase):
|
||||||
|
def run(self):
|
||||||
|
name_only = self.args.name_only
|
||||||
|
exps = self.repo.experiments.ls(
|
||||||
|
all_commits=self.args.all_commits,
|
||||||
|
rev=self.args.rev,
|
||||||
|
num=self.args.num,
|
||||||
|
git_remote=self.args.git_remote,
|
||||||
|
)
|
||||||
|
for baseline in exps:
|
||||||
|
tag = self.repo.scm.describe(baseline)
|
||||||
|
if not tag:
|
||||||
|
branch = self.repo.scm.describe(baseline, base="refs/heads")
|
||||||
|
if branch:
|
||||||
|
tag = branch.split("/")[-1]
|
||||||
|
name = tag if tag else baseline[:7]
|
||||||
|
if not name_only:
|
||||||
|
print(f"{name}:")
|
||||||
|
for exp_name in exps[baseline]:
|
||||||
|
indent = "" if name_only else "\t"
|
||||||
|
print(f"{indent}{exp_name}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
from . import add_rev_selection_flags
|
||||||
|
|
||||||
|
EXPERIMENTS_LIST_HELP = "List local and remote experiments."
|
||||||
|
experiments_list_parser = experiments_subparsers.add_parser(
|
||||||
|
"list",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_LIST_HELP, "exp/list"),
|
||||||
|
help=EXPERIMENTS_LIST_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
add_rev_selection_flags(experiments_list_parser, "List")
|
||||||
|
experiments_list_parser.add_argument(
|
||||||
|
"--name-only",
|
||||||
|
"--names-only",
|
||||||
|
action="store_true",
|
||||||
|
help="Only output experiment names (without parent commits).",
|
||||||
|
)
|
||||||
|
experiments_list_parser.add_argument(
|
||||||
|
"git_remote",
|
||||||
|
nargs="?",
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Optional Git remote name or Git URL. "
|
||||||
|
"If provided, experiments from the specified Git repository "
|
||||||
|
" will be listed instead of local ones."
|
||||||
|
),
|
||||||
|
metavar="<git_remote>",
|
||||||
|
)
|
||||||
|
experiments_list_parser.set_defaults(func=CmdExperimentsList)
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.exceptions import InvalidArgumentError
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsPull(CmdBase):
|
||||||
|
def raise_error_if_all_disabled(self):
|
||||||
|
if not any(
|
||||||
|
[self.args.experiment, self.args.all_commits, self.args.rev]
|
||||||
|
):
|
||||||
|
raise InvalidArgumentError(
|
||||||
|
"Either provide an `experiment` argument, or use the "
|
||||||
|
"`--rev` or `--all-commits` flag."
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.raise_error_if_all_disabled()
|
||||||
|
|
||||||
|
pulled_exps = self.repo.experiments.pull(
|
||||||
|
self.args.git_remote,
|
||||||
|
self.args.experiment,
|
||||||
|
all_commits=self.args.all_commits,
|
||||||
|
rev=self.args.rev,
|
||||||
|
num=self.args.num,
|
||||||
|
force=self.args.force,
|
||||||
|
pull_cache=self.args.pull_cache,
|
||||||
|
dvc_remote=self.args.dvc_remote,
|
||||||
|
jobs=self.args.jobs,
|
||||||
|
run_cache=self.args.run_cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
if pulled_exps:
|
||||||
|
ui.write(
|
||||||
|
f"Pulled experiment '{pulled_exps}'",
|
||||||
|
f"from Git remote '{self.args.git_remote}'.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ui.write("No experiments to pull.")
|
||||||
|
if not self.args.pull_cache:
|
||||||
|
ui.write(
|
||||||
|
"To pull cached outputs for this experiment"
|
||||||
|
"from DVC remote storage,"
|
||||||
|
"re-run this command without '--no-cache'."
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
from . import add_rev_selection_flags
|
||||||
|
|
||||||
|
EXPERIMENTS_PULL_HELP = "Pull an experiment from a Git remote."
|
||||||
|
experiments_pull_parser = experiments_subparsers.add_parser(
|
||||||
|
"pull",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_PULL_HELP, "exp/pull"),
|
||||||
|
help=EXPERIMENTS_PULL_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
add_rev_selection_flags(experiments_pull_parser, "Pull", False)
|
||||||
|
experiments_pull_parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Replace local experiment already exists.",
|
||||||
|
)
|
||||||
|
experiments_pull_parser.add_argument(
|
||||||
|
"--no-cache",
|
||||||
|
action="store_false",
|
||||||
|
dest="pull_cache",
|
||||||
|
help=(
|
||||||
|
"Do not pull cached outputs for this experiment from DVC remote "
|
||||||
|
"storage."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
experiments_pull_parser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--remote",
|
||||||
|
dest="dvc_remote",
|
||||||
|
metavar="<name>",
|
||||||
|
help="Name of the DVC remote to use when pulling cached outputs.",
|
||||||
|
)
|
||||||
|
experiments_pull_parser.add_argument(
|
||||||
|
"-j",
|
||||||
|
"--jobs",
|
||||||
|
type=int,
|
||||||
|
metavar="<number>",
|
||||||
|
help=(
|
||||||
|
"Number of jobs to run simultaneously when pulling from DVC "
|
||||||
|
"remote storage."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
experiments_pull_parser.add_argument(
|
||||||
|
"--run-cache",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Pull run history for all stages.",
|
||||||
|
)
|
||||||
|
experiments_pull_parser.add_argument(
|
||||||
|
"git_remote",
|
||||||
|
help="Git remote name or Git URL.",
|
||||||
|
metavar="<git_remote>",
|
||||||
|
)
|
||||||
|
experiments_pull_parser.add_argument(
|
||||||
|
"experiment",
|
||||||
|
nargs="*",
|
||||||
|
default=None,
|
||||||
|
help="Experiments to pull.",
|
||||||
|
metavar="<experiment>",
|
||||||
|
)
|
||||||
|
experiments_pull_parser.set_defaults(func=CmdExperimentsPull)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli import completion
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.exceptions import InvalidArgumentError
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsPush(CmdBase):
|
||||||
|
def raise_error_if_all_disabled(self):
|
||||||
|
if not any(
|
||||||
|
[self.args.experiment, self.args.all_commits, self.args.rev]
|
||||||
|
):
|
||||||
|
raise InvalidArgumentError(
|
||||||
|
"Either provide an `experiment` argument, or use the "
|
||||||
|
"`--rev` or `--all-commits` flag."
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
self.raise_error_if_all_disabled()
|
||||||
|
|
||||||
|
pushed_exps = self.repo.experiments.push(
|
||||||
|
self.args.git_remote,
|
||||||
|
self.args.experiment,
|
||||||
|
all_commits=self.args.all_commits,
|
||||||
|
rev=self.args.rev,
|
||||||
|
num=self.args.num,
|
||||||
|
force=self.args.force,
|
||||||
|
push_cache=self.args.push_cache,
|
||||||
|
dvc_remote=self.args.dvc_remote,
|
||||||
|
jobs=self.args.jobs,
|
||||||
|
run_cache=self.args.run_cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
if pushed_exps:
|
||||||
|
ui.write(
|
||||||
|
f"Pushed experiment '{pushed_exps}'"
|
||||||
|
f"to Git remote '{self.args.git_remote}'."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ui.write("No experiments to push.")
|
||||||
|
if not self.args.push_cache:
|
||||||
|
ui.write(
|
||||||
|
"To push cached outputs",
|
||||||
|
"for this experiment to DVC remote storage,"
|
||||||
|
"re-run this command without '--no-cache'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
from . import add_rev_selection_flags
|
||||||
|
|
||||||
|
EXPERIMENTS_PUSH_HELP = "Push a local experiment to a Git remote."
|
||||||
|
experiments_push_parser = experiments_subparsers.add_parser(
|
||||||
|
"push",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_PUSH_HELP, "exp/push"),
|
||||||
|
help=EXPERIMENTS_PUSH_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
add_rev_selection_flags(experiments_push_parser, "Push", False)
|
||||||
|
experiments_push_parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Replace experiment in the Git remote if it already exists.",
|
||||||
|
)
|
||||||
|
experiments_push_parser.add_argument(
|
||||||
|
"--no-cache",
|
||||||
|
action="store_false",
|
||||||
|
dest="push_cache",
|
||||||
|
help=(
|
||||||
|
"Do not push cached outputs for this experiment to DVC remote "
|
||||||
|
"storage."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
experiments_push_parser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--remote",
|
||||||
|
dest="dvc_remote",
|
||||||
|
metavar="<name>",
|
||||||
|
help="Name of the DVC remote to use when pushing cached outputs.",
|
||||||
|
)
|
||||||
|
experiments_push_parser.add_argument(
|
||||||
|
"-j",
|
||||||
|
"--jobs",
|
||||||
|
type=int,
|
||||||
|
metavar="<number>",
|
||||||
|
help=(
|
||||||
|
"Number of jobs to run simultaneously when pushing to DVC remote "
|
||||||
|
"storage."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
experiments_push_parser.add_argument(
|
||||||
|
"--run-cache",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Push run history for all stages.",
|
||||||
|
)
|
||||||
|
experiments_push_parser.add_argument(
|
||||||
|
"git_remote",
|
||||||
|
help="Git remote name or Git URL.",
|
||||||
|
metavar="<git_remote>",
|
||||||
|
)
|
||||||
|
experiments_push_parser.add_argument(
|
||||||
|
"experiment",
|
||||||
|
nargs="*",
|
||||||
|
default=None,
|
||||||
|
help="Experiments to push.",
|
||||||
|
metavar="<experiment>",
|
||||||
|
).complete = completion.EXPERIMENT
|
||||||
|
experiments_push_parser.set_defaults(func=CmdExperimentsPush)
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.exceptions import InvalidArgumentError
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsRemove(CmdBase):
|
||||||
|
def raise_error_if_all_disabled(self):
|
||||||
|
if not any(
|
||||||
|
[
|
||||||
|
self.args.experiment,
|
||||||
|
self.args.all_commits,
|
||||||
|
self.args.rev,
|
||||||
|
self.args.queue,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
raise InvalidArgumentError(
|
||||||
|
"Either provide an `experiment` argument, or use the "
|
||||||
|
"`--rev` or `--all-commits` flag."
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
self.raise_error_if_all_disabled()
|
||||||
|
|
||||||
|
removed_list = self.repo.experiments.remove(
|
||||||
|
exp_names=self.args.experiment,
|
||||||
|
all_commits=self.args.all_commits,
|
||||||
|
rev=self.args.rev,
|
||||||
|
num=self.args.num,
|
||||||
|
queue=self.args.queue,
|
||||||
|
git_remote=self.args.git_remote,
|
||||||
|
)
|
||||||
|
removed = ",".join(removed_list)
|
||||||
|
ui.write(f"Removed experiments: {removed}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
from . import add_rev_selection_flags
|
||||||
|
|
||||||
|
EXPERIMENTS_REMOVE_HELP = "Remove experiments."
|
||||||
|
experiments_remove_parser = experiments_subparsers.add_parser(
|
||||||
|
"remove",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_REMOVE_HELP, "exp/remove"),
|
||||||
|
help=EXPERIMENTS_REMOVE_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
remove_group = experiments_remove_parser.add_mutually_exclusive_group()
|
||||||
|
add_rev_selection_flags(experiments_remove_parser, "Remove", False)
|
||||||
|
remove_group.add_argument(
|
||||||
|
"--queue", action="store_true", help="Remove all queued experiments."
|
||||||
|
)
|
||||||
|
remove_group.add_argument(
|
||||||
|
"-g",
|
||||||
|
"--git-remote",
|
||||||
|
metavar="<git_remote>",
|
||||||
|
help="Name or URL of the Git remote to remove the experiment from",
|
||||||
|
)
|
||||||
|
experiments_remove_parser.add_argument(
|
||||||
|
"experiment",
|
||||||
|
nargs="*",
|
||||||
|
help="Experiments to remove.",
|
||||||
|
metavar="<experiment>",
|
||||||
|
)
|
||||||
|
experiments_remove_parser.set_defaults(func=CmdExperimentsRemove)
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli import completion
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.commands.repro import CmdRepro
|
||||||
|
from dvc.commands.repro import add_arguments as add_repro_arguments
|
||||||
|
from dvc.exceptions import InvalidArgumentError
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsRun(CmdRepro):
|
||||||
|
def run(self):
|
||||||
|
from dvc.compare import show_metrics
|
||||||
|
|
||||||
|
if self.args.checkpoint_resume:
|
||||||
|
if self.args.reset:
|
||||||
|
raise InvalidArgumentError(
|
||||||
|
"--reset and --rev are mutually exclusive."
|
||||||
|
)
|
||||||
|
if not (self.args.queue or self.args.tmp_dir):
|
||||||
|
raise InvalidArgumentError(
|
||||||
|
"--rev can only be used in conjunction with "
|
||||||
|
"--queue or --temp."
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.args.reset:
|
||||||
|
ui.write("Any existing checkpoints will be reset and re-run.")
|
||||||
|
|
||||||
|
results = self.repo.experiments.run(
|
||||||
|
name=self.args.name,
|
||||||
|
queue=self.args.queue,
|
||||||
|
run_all=self.args.run_all,
|
||||||
|
jobs=self.args.jobs,
|
||||||
|
params=self.args.set_param,
|
||||||
|
checkpoint_resume=self.args.checkpoint_resume,
|
||||||
|
reset=self.args.reset,
|
||||||
|
tmp_dir=self.args.tmp_dir,
|
||||||
|
machine=self.args.machine,
|
||||||
|
**self._common_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.args.metrics and results:
|
||||||
|
metrics = self.repo.metrics.show(revs=list(results))
|
||||||
|
metrics.pop("workspace", None)
|
||||||
|
show_metrics(metrics)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
|
||||||
|
EXPERIMENTS_RUN_HELP = "Run or resume an experiment."
|
||||||
|
experiments_run_parser = experiments_subparsers.add_parser(
|
||||||
|
"run",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_RUN_HELP, "exp/run"),
|
||||||
|
help=EXPERIMENTS_RUN_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
_add_run_common(experiments_run_parser)
|
||||||
|
experiments_run_parser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--rev",
|
||||||
|
type=str,
|
||||||
|
dest="checkpoint_resume",
|
||||||
|
help=(
|
||||||
|
"Continue the specified checkpoint experiment. Can only be used "
|
||||||
|
"in conjunction with --queue or --temp."
|
||||||
|
),
|
||||||
|
metavar="<experiment_rev>",
|
||||||
|
).complete = completion.EXPERIMENT
|
||||||
|
experiments_run_parser.add_argument(
|
||||||
|
"--reset",
|
||||||
|
action="store_true",
|
||||||
|
help="Reset existing checkpoints and restart the experiment.",
|
||||||
|
)
|
||||||
|
experiments_run_parser.set_defaults(func=CmdExperimentsRun)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_run_common(parser):
|
||||||
|
"""Add common args for 'exp run'."""
|
||||||
|
# inherit arguments from `dvc repro`
|
||||||
|
add_repro_arguments(parser)
|
||||||
|
parser.add_argument(
|
||||||
|
"-n",
|
||||||
|
"--name",
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Human-readable experiment name. If not specified, a name will "
|
||||||
|
"be auto-generated."
|
||||||
|
),
|
||||||
|
metavar="<name>",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-S",
|
||||||
|
"--set-param",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Use the specified param value when reproducing pipelines.",
|
||||||
|
metavar="[<filename>:]<param_name>=<param_value>",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--queue",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Stage this experiment in the run queue for future execution.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--run-all",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Execute all experiments in the run queue. Implies --temp.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-j",
|
||||||
|
"--jobs",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Run the specified number of experiments at a time in parallel.",
|
||||||
|
metavar="<number>",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--temp",
|
||||||
|
action="store_true",
|
||||||
|
dest="tmp_dir",
|
||||||
|
help=(
|
||||||
|
"Run this experiment in a separate temporary directory instead of "
|
||||||
|
"your workspace."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--machine",
|
||||||
|
default=None,
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
# help=(
|
||||||
|
# "Run this experiment on the specified 'dvc machine' instance."
|
||||||
|
# )
|
||||||
|
# metavar="<name>",
|
||||||
|
)
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from collections import Counter, OrderedDict, defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from funcy import lmap
|
||||||
|
|
||||||
|
from dvc.cli import completion
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.commands.metrics import DEFAULT_PRECISION
|
||||||
|
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||||
|
from dvc.ui import ui
|
||||||
|
from dvc.utils.flatten import flatten
|
||||||
|
from dvc.utils.serialize import encode_exception
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dvc.compare import TabularData
|
||||||
|
from dvc.ui import RichText
|
||||||
|
|
||||||
|
FILL_VALUE = "-"
|
||||||
|
FILL_VALUE_ERRORED = "!"
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _update_names(names, items):
|
||||||
|
for name, item in items:
|
||||||
|
item = item.get("data", {})
|
||||||
|
if isinstance(item, dict):
|
||||||
|
item = flatten(item)
|
||||||
|
names[name].update({key: None for key in item})
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_names(all_experiments, **kwargs):
|
||||||
|
metric_names = defaultdict(dict)
|
||||||
|
param_names = defaultdict(dict)
|
||||||
|
deps_names = set()
|
||||||
|
|
||||||
|
for _, experiments in all_experiments.items():
|
||||||
|
for exp_data in experiments.values():
|
||||||
|
exp = exp_data.get("data", {})
|
||||||
|
_update_names(metric_names, exp.get("metrics", {}).items())
|
||||||
|
_update_names(param_names, exp.get("params", {}).items())
|
||||||
|
for dep_name in exp.get("deps", {}):
|
||||||
|
deps_names.add(dep_name)
|
||||||
|
|
||||||
|
return metric_names, param_names, sorted(deps_names)
|
||||||
|
|
||||||
|
|
||||||
|
experiment_types = {
|
||||||
|
"checkpoint_tip": "│ ╓",
|
||||||
|
"checkpoint_commit": "│ ╟",
|
||||||
|
"checkpoint_base": "├─╨",
|
||||||
|
"branch_commit": "├──",
|
||||||
|
"branch_base": "└──",
|
||||||
|
"baseline": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_rows(
|
||||||
|
base_rev,
|
||||||
|
experiments,
|
||||||
|
all_headers,
|
||||||
|
metric_headers,
|
||||||
|
param_headers,
|
||||||
|
metric_names,
|
||||||
|
param_names,
|
||||||
|
deps_names,
|
||||||
|
precision=DEFAULT_PRECISION,
|
||||||
|
sort_by=None,
|
||||||
|
sort_order=None,
|
||||||
|
fill_value=FILL_VALUE,
|
||||||
|
iso=False,
|
||||||
|
):
|
||||||
|
from scmrepo.git import Git
|
||||||
|
|
||||||
|
if sort_by:
|
||||||
|
sort_path, sort_name, sort_type = _sort_column(
|
||||||
|
sort_by, metric_names, param_names
|
||||||
|
)
|
||||||
|
reverse = sort_order == "desc"
|
||||||
|
experiments = _sort_exp(
|
||||||
|
experiments, sort_path, sort_name, sort_type, reverse
|
||||||
|
)
|
||||||
|
|
||||||
|
new_checkpoint = True
|
||||||
|
for i, (rev, results) in enumerate(experiments.items()):
|
||||||
|
fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value
|
||||||
|
row_dict = {k: fill_value for k in all_headers}
|
||||||
|
|
||||||
|
exp = results.get("data", {})
|
||||||
|
|
||||||
|
if exp.get("running"):
|
||||||
|
state = "Running"
|
||||||
|
elif exp.get("queued"):
|
||||||
|
state = "Queued"
|
||||||
|
else:
|
||||||
|
state = fill_value
|
||||||
|
|
||||||
|
is_baseline = rev == "baseline"
|
||||||
|
|
||||||
|
if is_baseline:
|
||||||
|
name_rev = base_rev[:7] if Git.is_sha(base_rev) else base_rev
|
||||||
|
else:
|
||||||
|
name_rev = rev[:7]
|
||||||
|
|
||||||
|
tip = exp.get("checkpoint_tip")
|
||||||
|
parent_rev = exp.get("checkpoint_parent", "")
|
||||||
|
parent_exp = experiments.get(parent_rev, {}).get("data", {})
|
||||||
|
parent_tip = parent_exp.get("checkpoint_tip")
|
||||||
|
|
||||||
|
parent = ""
|
||||||
|
if is_baseline:
|
||||||
|
typ = "baseline"
|
||||||
|
elif tip:
|
||||||
|
if tip == parent_tip:
|
||||||
|
typ = (
|
||||||
|
"checkpoint_tip" if new_checkpoint else "checkpoint_commit"
|
||||||
|
)
|
||||||
|
elif parent_rev == base_rev:
|
||||||
|
typ = "checkpoint_base"
|
||||||
|
else:
|
||||||
|
typ = "checkpoint_commit"
|
||||||
|
parent = parent_rev[:7]
|
||||||
|
elif i < len(experiments) - 1:
|
||||||
|
typ = "branch_commit"
|
||||||
|
else:
|
||||||
|
typ = "branch_base"
|
||||||
|
|
||||||
|
if not is_baseline:
|
||||||
|
new_checkpoint = not (tip and tip == parent_tip)
|
||||||
|
|
||||||
|
row_dict["Experiment"] = exp.get("name", "")
|
||||||
|
row_dict["rev"] = name_rev
|
||||||
|
row_dict["typ"] = typ
|
||||||
|
row_dict["Created"] = _format_time(
|
||||||
|
exp.get("timestamp"), fill_value, iso
|
||||||
|
)
|
||||||
|
row_dict["parent"] = parent
|
||||||
|
row_dict["State"] = state
|
||||||
|
row_dict["Executor"] = exp.get("executor", fill_value)
|
||||||
|
|
||||||
|
_extend_row(
|
||||||
|
row_dict,
|
||||||
|
metric_names,
|
||||||
|
metric_headers,
|
||||||
|
exp.get("metrics", {}).items(),
|
||||||
|
precision,
|
||||||
|
fill_value=fill_value,
|
||||||
|
)
|
||||||
|
_extend_row(
|
||||||
|
row_dict,
|
||||||
|
param_names,
|
||||||
|
param_headers,
|
||||||
|
exp.get("params", {}).items(),
|
||||||
|
precision,
|
||||||
|
fill_value=fill_value,
|
||||||
|
)
|
||||||
|
for dep in deps_names:
|
||||||
|
hash_info = exp.get("deps", {}).get(dep, {}).get("hash")
|
||||||
|
if hash_info is not None:
|
||||||
|
hash_info = hash_info[:7]
|
||||||
|
row_dict[dep] = hash_info
|
||||||
|
yield list(row_dict.values())
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_column(sort_by, metric_names, param_names):
|
||||||
|
path, _, sort_name = sort_by.rpartition(":")
|
||||||
|
matches = set()
|
||||||
|
|
||||||
|
if path:
|
||||||
|
if path in metric_names and sort_name in metric_names[path]:
|
||||||
|
matches.add((path, sort_name, "metrics"))
|
||||||
|
if path in param_names and sort_name in param_names[path]:
|
||||||
|
matches.add((path, sort_name, "params"))
|
||||||
|
else:
|
||||||
|
for path in metric_names:
|
||||||
|
if sort_name in metric_names[path]:
|
||||||
|
matches.add((path, sort_name, "metrics"))
|
||||||
|
for path in param_names:
|
||||||
|
if sort_name in param_names[path]:
|
||||||
|
matches.add((path, sort_name, "params"))
|
||||||
|
|
||||||
|
if len(matches) == 1:
|
||||||
|
return matches.pop()
|
||||||
|
if len(matches) > 1:
|
||||||
|
raise InvalidArgumentError(
|
||||||
|
"Ambiguous sort column '{}' matched '{}'".format(
|
||||||
|
sort_by,
|
||||||
|
", ".join([f"{path}:{name}" for path, name, _ in matches]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise InvalidArgumentError(f"Unknown sort column '{sort_by}'")
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
|
||||||
|
def _sort(item):
|
||||||
|
rev, exp = item
|
||||||
|
exp_data = exp.get("data", {})
|
||||||
|
tip = exp_data.get("checkpoint_tip")
|
||||||
|
if tip and tip != rev:
|
||||||
|
# Sort checkpoint experiments by tip commit
|
||||||
|
return _sort((tip, experiments[tip]))
|
||||||
|
data = exp_data.get(typ, {}).get(sort_path, {}).get("data", {})
|
||||||
|
val = flatten(data).get(sort_name)
|
||||||
|
return val is None, val
|
||||||
|
|
||||||
|
ret = OrderedDict()
|
||||||
|
if "baseline" in experiments:
|
||||||
|
ret["baseline"] = experiments.pop("baseline")
|
||||||
|
|
||||||
|
ret.update(sorted(experiments.items(), key=_sort, reverse=reverse))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def _format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
|
||||||
|
if datetime_obj is None:
|
||||||
|
return fill_value
|
||||||
|
|
||||||
|
if iso:
|
||||||
|
return datetime_obj.isoformat()
|
||||||
|
|
||||||
|
if datetime_obj.date() == date.today():
|
||||||
|
fmt = "%I:%M %p"
|
||||||
|
else:
|
||||||
|
fmt = "%b %d, %Y"
|
||||||
|
return datetime_obj.strftime(fmt)
|
||||||
|
|
||||||
|
|
||||||
|
def _extend_row(row, names, headers, items, precision, fill_value=FILL_VALUE):
|
||||||
|
from dvc.compare import _format_field, with_value
|
||||||
|
|
||||||
|
for fname, data in items:
|
||||||
|
item = data.get("data", {})
|
||||||
|
item = flatten(item) if isinstance(item, dict) else {fname: item}
|
||||||
|
for name in names[fname]:
|
||||||
|
value = with_value(
|
||||||
|
item.get(name),
|
||||||
|
FILL_VALUE_ERRORED if data.get("error", None) else fill_value,
|
||||||
|
)
|
||||||
|
# wrap field data in ui.rich_text, otherwise rich may
|
||||||
|
# interpret unescaped braces from list/dict types as rich
|
||||||
|
# markup tags
|
||||||
|
value = ui.rich_text(str(_format_field(value, precision)))
|
||||||
|
if name in headers:
|
||||||
|
row[name] = value
|
||||||
|
else:
|
||||||
|
row[f"{fname}:{name}"] = value
|
||||||
|
|
||||||
|
|
||||||
|
def experiments_table(
|
||||||
|
all_experiments,
|
||||||
|
headers,
|
||||||
|
metric_headers,
|
||||||
|
metric_names,
|
||||||
|
param_headers,
|
||||||
|
param_names,
|
||||||
|
deps_names,
|
||||||
|
sort_by=None,
|
||||||
|
sort_order=None,
|
||||||
|
precision=DEFAULT_PRECISION,
|
||||||
|
fill_value=FILL_VALUE,
|
||||||
|
iso=False,
|
||||||
|
) -> "TabularData":
|
||||||
|
from funcy import lconcat
|
||||||
|
|
||||||
|
from dvc.compare import TabularData
|
||||||
|
|
||||||
|
all_headers = lconcat(headers, metric_headers, param_headers, deps_names)
|
||||||
|
td = TabularData(all_headers, fill_value=fill_value)
|
||||||
|
for base_rev, experiments in all_experiments.items():
|
||||||
|
rows = _collect_rows(
|
||||||
|
base_rev,
|
||||||
|
experiments,
|
||||||
|
all_headers,
|
||||||
|
metric_headers,
|
||||||
|
param_headers,
|
||||||
|
metric_names,
|
||||||
|
param_names,
|
||||||
|
deps_names,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_order=sort_order,
|
||||||
|
precision=precision,
|
||||||
|
fill_value=fill_value,
|
||||||
|
iso=iso,
|
||||||
|
)
|
||||||
|
td.extend(rows)
|
||||||
|
|
||||||
|
return td
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_exp_id(kwargs) -> "RichText":
|
||||||
|
exp_name = kwargs["Experiment"]
|
||||||
|
rev = kwargs["rev"]
|
||||||
|
typ = kwargs.get("typ", "baseline")
|
||||||
|
|
||||||
|
if typ == "baseline" or not exp_name:
|
||||||
|
text = ui.rich_text(exp_name or rev)
|
||||||
|
else:
|
||||||
|
text = ui.rich_text.assemble(rev, " [", (exp_name, "bold"), "]")
|
||||||
|
|
||||||
|
parent = kwargs.get("parent")
|
||||||
|
suff = f" ({parent})" if parent else ""
|
||||||
|
text.append(suff)
|
||||||
|
|
||||||
|
tree = experiment_types[typ]
|
||||||
|
pref = f"{tree} " if tree else ""
|
||||||
|
return ui.rich_text(pref) + text
|
||||||
|
|
||||||
|
|
||||||
|
def baseline_styler(typ):
|
||||||
|
return {"style": "bold"} if typ == "baseline" else {}
|
||||||
|
|
||||||
|
|
||||||
|
def show_experiments(
|
||||||
|
all_experiments,
|
||||||
|
keep=None,
|
||||||
|
drop=None,
|
||||||
|
pager=True,
|
||||||
|
csv=False,
|
||||||
|
markdown=False,
|
||||||
|
pcp=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
from funcy.seqs import flatten as flatten_list
|
||||||
|
|
||||||
|
metric_names, param_names, deps_names = _collect_names(all_experiments)
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
"Experiment",
|
||||||
|
"rev",
|
||||||
|
"typ",
|
||||||
|
"Created",
|
||||||
|
"parent",
|
||||||
|
"State",
|
||||||
|
"Executor",
|
||||||
|
]
|
||||||
|
|
||||||
|
names = {**metric_names, **param_names}
|
||||||
|
counter = Counter(flatten_list([list(a.keys()) for a in names.values()]))
|
||||||
|
counter.update(headers)
|
||||||
|
metric_headers = _normalize_headers(metric_names, counter)
|
||||||
|
param_headers = _normalize_headers(param_names, counter)
|
||||||
|
|
||||||
|
td = experiments_table(
|
||||||
|
all_experiments,
|
||||||
|
headers,
|
||||||
|
metric_headers,
|
||||||
|
metric_names,
|
||||||
|
param_headers,
|
||||||
|
param_names,
|
||||||
|
deps_names,
|
||||||
|
kwargs.get("sort_by"),
|
||||||
|
kwargs.get("sort_order"),
|
||||||
|
kwargs.get("precision"),
|
||||||
|
kwargs.get("fill_value"),
|
||||||
|
kwargs.get("iso"),
|
||||||
|
)
|
||||||
|
if keep:
|
||||||
|
for col in td.keys():
|
||||||
|
if re.match(keep, col):
|
||||||
|
td.protect(col)
|
||||||
|
|
||||||
|
for col in ("State", "Executor"):
|
||||||
|
if td.is_empty(col):
|
||||||
|
td.drop(col)
|
||||||
|
|
||||||
|
row_styles = lmap(baseline_styler, td.column("typ"))
|
||||||
|
|
||||||
|
if not csv:
|
||||||
|
merge_headers = ["Experiment", "rev", "typ", "parent"]
|
||||||
|
td.column("Experiment")[:] = map(
|
||||||
|
prepare_exp_id, td.as_dict(merge_headers)
|
||||||
|
)
|
||||||
|
td.drop(*merge_headers[1:])
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"metrics": metric_headers,
|
||||||
|
"params": param_headers,
|
||||||
|
"deps": deps_names,
|
||||||
|
}
|
||||||
|
styles = {
|
||||||
|
"Experiment": {"no_wrap": True, "header_style": "black on grey93"},
|
||||||
|
"Created": {"header_style": "black on grey93"},
|
||||||
|
"State": {"header_style": "black on grey93"},
|
||||||
|
"Executor": {"header_style": "black on grey93"},
|
||||||
|
}
|
||||||
|
header_bg_colors = {
|
||||||
|
"metrics": "cornsilk1",
|
||||||
|
"params": "light_cyan1",
|
||||||
|
"deps": "plum2",
|
||||||
|
}
|
||||||
|
styles.update(
|
||||||
|
{
|
||||||
|
header: {
|
||||||
|
"justify": "right" if typ == "metrics" else "left",
|
||||||
|
"header_style": f"black on {header_bg_colors[typ]}",
|
||||||
|
"collapse": idx != 0,
|
||||||
|
"no_wrap": typ == "metrics",
|
||||||
|
}
|
||||||
|
for typ, hs in headers.items()
|
||||||
|
for idx, header in enumerate(hs)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if kwargs.get("only_changed", False) or pcp:
|
||||||
|
td.drop_duplicates("cols", ignore_empty=False)
|
||||||
|
|
||||||
|
cols_to_drop = set()
|
||||||
|
if drop is not None:
|
||||||
|
cols_to_drop = {col for col in td.keys() if re.match(drop, col)}
|
||||||
|
if pcp:
|
||||||
|
cols_to_drop.add("Created")
|
||||||
|
td.drop(*cols_to_drop)
|
||||||
|
|
||||||
|
if pcp:
|
||||||
|
subset = {x for x in td.keys() if x != "Experiment"}
|
||||||
|
td.dropna(
|
||||||
|
"rows",
|
||||||
|
how="all",
|
||||||
|
subset=subset,
|
||||||
|
)
|
||||||
|
td.drop_duplicates("rows", subset=subset)
|
||||||
|
if "Experiment" in td:
|
||||||
|
td.column("Experiment")[:] = [
|
||||||
|
# remove tree characters
|
||||||
|
str(x).encode("ascii", "ignore").strip().decode()
|
||||||
|
for x in td.column("Experiment")
|
||||||
|
]
|
||||||
|
out = kwargs.get("out") or "dvc_plots"
|
||||||
|
output_file = os.path.join(out, "index.html")
|
||||||
|
ui.write(
|
||||||
|
td.to_parallel_coordinates(
|
||||||
|
output_path=os.path.abspath(output_file),
|
||||||
|
color_by=kwargs.get("sort_by") or "Experiment",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if kwargs.get("open"):
|
||||||
|
return ui.open_browser(output_file)
|
||||||
|
|
||||||
|
else:
|
||||||
|
td.render(
|
||||||
|
pager=pager,
|
||||||
|
borders="horizontals",
|
||||||
|
rich_table=True,
|
||||||
|
header_styles=styles,
|
||||||
|
row_styles=row_styles,
|
||||||
|
csv=csv,
|
||||||
|
markdown=markdown,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_headers(names, count):
|
||||||
|
return [
|
||||||
|
name if count[name] == 1 else f"{path}:{name}"
|
||||||
|
for path in names
|
||||||
|
for name in names[path]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _format_json(item):
|
||||||
|
if isinstance(item, (date, datetime)):
|
||||||
|
return item.isoformat()
|
||||||
|
return encode_exception(item)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdExperimentsShow(CmdBase):
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
all_experiments = self.repo.experiments.show(
|
||||||
|
all_branches=self.args.all_branches,
|
||||||
|
all_tags=self.args.all_tags,
|
||||||
|
all_commits=self.args.all_commits,
|
||||||
|
revs=self.args.rev,
|
||||||
|
num=self.args.num,
|
||||||
|
sha_only=self.args.sha,
|
||||||
|
param_deps=self.args.param_deps,
|
||||||
|
fetch_running=self.args.fetch_running,
|
||||||
|
)
|
||||||
|
except DvcException:
|
||||||
|
logger.exception("failed to show experiments")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if self.args.json:
|
||||||
|
ui.write_json(all_experiments, default=_format_json)
|
||||||
|
else:
|
||||||
|
precision = (
|
||||||
|
self.args.precision or None
|
||||||
|
if self.args.csv
|
||||||
|
else DEFAULT_PRECISION
|
||||||
|
)
|
||||||
|
fill_value = "" if self.args.csv else FILL_VALUE
|
||||||
|
iso = True if self.args.csv else False
|
||||||
|
|
||||||
|
show_experiments(
|
||||||
|
all_experiments,
|
||||||
|
keep=self.args.keep,
|
||||||
|
drop=self.args.drop,
|
||||||
|
sort_by=self.args.sort_by,
|
||||||
|
sort_order=self.args.sort_order,
|
||||||
|
precision=precision,
|
||||||
|
fill_value=fill_value,
|
||||||
|
iso=iso,
|
||||||
|
pager=not self.args.no_pager,
|
||||||
|
csv=self.args.csv,
|
||||||
|
markdown=self.args.markdown,
|
||||||
|
only_changed=self.args.only_changed,
|
||||||
|
pcp=self.args.pcp,
|
||||||
|
out=self.args.out,
|
||||||
|
open=self.args.open,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
|
from . import add_rev_selection_flags
|
||||||
|
|
||||||
|
EXPERIMENTS_SHOW_HELP = "Print experiments."
|
||||||
|
experiments_show_parser = experiments_subparsers.add_parser(
|
||||||
|
"show",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(EXPERIMENTS_SHOW_HELP, "exp/show"),
|
||||||
|
help=EXPERIMENTS_SHOW_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
add_rev_selection_flags(experiments_show_parser, "Show")
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"-a",
|
||||||
|
"--all-branches",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Show experiments derived from the tip of all Git branches.",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"-T",
|
||||||
|
"--all-tags",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Show experiments derived from all Git tags.",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--no-pager",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Do not pipe output into a pager.",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--only-changed",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=(
|
||||||
|
"Only show metrics/params with values varying "
|
||||||
|
"across the selected experiments."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--drop",
|
||||||
|
help="Remove the columns matching the specified regex pattern.",
|
||||||
|
metavar="<regex_pattern>",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--keep",
|
||||||
|
help="Preserve the columns matching the specified regex pattern.",
|
||||||
|
metavar="<regex_pattern>",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--param-deps",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Show only params that are stage dependencies.",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--sort-by",
|
||||||
|
help="Sort related experiments by the specified metric or param.",
|
||||||
|
metavar="<metric/param>",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--sort-order",
|
||||||
|
help=(
|
||||||
|
"Sort order to use with --sort-by."
|
||||||
|
" Defaults to ascending ('asc')."
|
||||||
|
),
|
||||||
|
choices=("asc", "desc"),
|
||||||
|
default="asc",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--sha",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Always show git commit SHAs instead of branch/tag names.",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
"--show-json",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Print output in JSON format instead of a human-readable table.",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--csv",
|
||||||
|
"--show-csv",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Print output in csv format instead of a human-readable table.",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--md",
|
||||||
|
"--show-md",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
dest="markdown",
|
||||||
|
help="Show tabulated output in the Markdown format (GFM).",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--precision",
|
||||||
|
type=int,
|
||||||
|
help=(
|
||||||
|
"Round metrics/params to `n` digits precision after the decimal "
|
||||||
|
f"point. Rounds to {DEFAULT_PRECISION} digits by default."
|
||||||
|
),
|
||||||
|
metavar="<n>",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--parallel-coordinates-plot",
|
||||||
|
"--pcp",
|
||||||
|
dest="pcp",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Generate a Parallel Coordinates Plot from the tabulated output.",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--out",
|
||||||
|
default=None,
|
||||||
|
help="Destination folder to save the Parallel Coordinates Plot to",
|
||||||
|
metavar="<path>",
|
||||||
|
).complete = completion.DIR
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--open",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Open the Parallel Coordinates Plot directly in the browser.",
|
||||||
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--no-fetch",
|
||||||
|
dest="fetch_running",
|
||||||
|
action="store_false",
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
experiments_show_parser.set_defaults(func=CmdExperimentsShow)
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -15,7 +16,7 @@ class CmdFreezeBase(CmdBase):
|
|||||||
try:
|
try:
|
||||||
func(target)
|
func(target)
|
||||||
except DvcException:
|
except DvcException:
|
||||||
logger.exception(f"failed to {name} '{target}'")
|
logger.exception("failed to %s '%s'", name, target)
|
||||||
ret = 1
|
ret = 1
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -2,7 +2,8 @@ import argparse
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -136,7 +137,6 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=(
|
help=(
|
||||||
"Number of jobs to run simultaneously. "
|
"Number of jobs to run simultaneously. "
|
||||||
"The default value is 4 * cpu_count(). "
|
"The default value is 4 * cpu_count(). "
|
||||||
"For SSH remotes, the default is 4. "
|
|
||||||
),
|
),
|
||||||
metavar="<number>",
|
metavar="<number>",
|
||||||
)
|
)
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli import completion
|
||||||
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
from . import completion
|
from ..cli.utils import append_doc_link
|
||||||
from .base import CmdBaseNoRepo, append_doc_link
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -93,7 +94,6 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=(
|
help=(
|
||||||
"Number of jobs to run simultaneously. "
|
"Number of jobs to run simultaneously. "
|
||||||
"The default value is 4 * cpu_count(). "
|
"The default value is 4 * cpu_count(). "
|
||||||
"For SSH remotes, the default is 4. "
|
|
||||||
),
|
),
|
||||||
metavar="<number>",
|
metavar="<number>",
|
||||||
)
|
)
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from dvc.cli import completion
|
||||||
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
from . import completion
|
from ..cli.utils import append_doc_link
|
||||||
from .base import CmdBaseNoRepo, append_doc_link
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ class CmdGetUrl(CmdBaseNoRepo):
|
|||||||
Repo.get_url(self.args.url, out=self.args.out, jobs=self.args.jobs)
|
Repo.get_url(self.args.url, out=self.args.out, jobs=self.args.jobs)
|
||||||
return 0
|
return 0
|
||||||
except DvcException:
|
except DvcException:
|
||||||
logger.exception(f"failed to get '{self.args.url}'")
|
logger.exception("failed to get '%s'", self.args.url)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
@@ -43,7 +44,6 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=(
|
help=(
|
||||||
"Number of jobs to run simultaneously. "
|
"Number of jobs to run simultaneously. "
|
||||||
"The default value is 4 * cpu_count(). "
|
"The default value is 4 * cpu_count(). "
|
||||||
"For SSH remotes, the default is 4. "
|
|
||||||
),
|
),
|
||||||
metavar="<number>",
|
metavar="<number>",
|
||||||
)
|
)
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from dvc.command.base import CmdBaseNoRepo, fix_subparsers
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
|
from dvc.cli.utils import fix_subparsers
|
||||||
from dvc.exceptions import NotDvcRepoError
|
from dvc.exceptions import NotDvcRepoError
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -25,7 +26,7 @@ class CmdHookBase(CmdBaseNoRepo):
|
|||||||
|
|
||||||
class CmdPreCommit(CmdHookBase):
|
class CmdPreCommit(CmdHookBase):
|
||||||
def _run(self):
|
def _run(self):
|
||||||
from dvc.main import main
|
from dvc.cli import main
|
||||||
|
|
||||||
return main(["status"])
|
return main(["status"])
|
||||||
|
|
||||||
@@ -49,14 +50,14 @@ class CmdPostCheckout(CmdHookBase):
|
|||||||
if os.path.isdir(os.path.join(".git", "rebase-merge")):
|
if os.path.isdir(os.path.join(".git", "rebase-merge")):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
from dvc.main import main
|
from dvc.cli import main
|
||||||
|
|
||||||
return main(["checkout"])
|
return main(["checkout"])
|
||||||
|
|
||||||
|
|
||||||
class CmdPrePush(CmdHookBase):
|
class CmdPrePush(CmdHookBase):
|
||||||
def _run(self):
|
def _run(self):
|
||||||
from dvc.main import main
|
from dvc.cli import main
|
||||||
|
|
||||||
return main(["push"])
|
return main(["push"])
|
||||||
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -90,7 +91,6 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=(
|
help=(
|
||||||
"Number of jobs to run simultaneously. "
|
"Number of jobs to run simultaneously. "
|
||||||
"The default value is 4 * cpu_count(). "
|
"The default value is 4 * cpu_count(). "
|
||||||
"For SSH remotes, the default is 4. "
|
|
||||||
),
|
),
|
||||||
metavar="<number>",
|
metavar="<number>",
|
||||||
)
|
)
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -90,7 +91,6 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=(
|
help=(
|
||||||
"Number of jobs to run simultaneously. "
|
"Number of jobs to run simultaneously. "
|
||||||
"The default value is 4 * cpu_count(). "
|
"The default value is 4 * cpu_count(). "
|
||||||
"For SSH remotes, the default is 4. "
|
|
||||||
),
|
),
|
||||||
metavar="<number>",
|
metavar="<number>",
|
||||||
)
|
)
|
||||||
@@ -4,7 +4,8 @@ import logging
|
|||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
from dvc import analytics
|
from dvc import analytics
|
||||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.utils import boxify
|
from dvc.utils import boxify
|
||||||
from dvc.utils import format_link as fmt_link
|
from dvc.utils import format_link as fmt_link
|
||||||
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
from dvc.command.ls.ls_colors import LsColors
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.commands.ls.ls_colors import LsColors
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ class CmdList(CmdBaseNoRepo):
|
|||||||
ui.write("\n".join(entries))
|
ui.write("\n".join(entries))
|
||||||
return 0
|
return 0
|
||||||
except DvcException:
|
except DvcException:
|
||||||
logger.exception(f"failed to list '{self.args.url}'")
|
logger.exception("failed to list '%s'", self.args.url)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
from dvc.cli.command import CmdBase
|
||||||
from dvc.command.config import CmdConfig
|
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||||
|
from dvc.commands.config import CmdConfig
|
||||||
from dvc.compare import TabularData
|
from dvc.compare import TabularData
|
||||||
from dvc.config import ConfigError
|
from dvc.config import ConfigError
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
@@ -241,6 +242,7 @@ class CmdMachineStatus(CmdBase):
|
|||||||
"instance_hdd_size",
|
"instance_hdd_size",
|
||||||
"instance_gpu",
|
"instance_gpu",
|
||||||
]
|
]
|
||||||
|
FILL_VALUE = "-"
|
||||||
|
|
||||||
def _add_row(
|
def _add_row(
|
||||||
self,
|
self,
|
||||||
@@ -250,7 +252,11 @@ class CmdMachineStatus(CmdBase):
|
|||||||
):
|
):
|
||||||
|
|
||||||
if not all_status:
|
if not all_status:
|
||||||
row = [name, None, "offline"]
|
row = [
|
||||||
|
name,
|
||||||
|
self.FILL_VALUE,
|
||||||
|
"offline",
|
||||||
|
] # back to `None` after #7167
|
||||||
td.append(row)
|
td.append(row)
|
||||||
for i, status in enumerate(all_status, start=1):
|
for i, status in enumerate(all_status, start=1):
|
||||||
row = [name, f"num_{i}", "running" if status else "offline"]
|
row = [name, f"num_{i}", "running" if status else "offline"]
|
||||||
@@ -264,7 +270,7 @@ class CmdMachineStatus(CmdBase):
|
|||||||
raise MachineDisabledError
|
raise MachineDisabledError
|
||||||
|
|
||||||
td = TabularData(
|
td = TabularData(
|
||||||
self.INSTANCE_FIELD + self.SHOWN_FIELD, fill_value="-"
|
self.INSTANCE_FIELD + self.SHOWN_FIELD, fill_value=self.FILL_VALUE
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.args.name:
|
if self.args.name:
|
||||||
@@ -304,7 +310,7 @@ class CmdMachineSsh(CmdBase):
|
|||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
from dvc.command.config import parent_config_parser
|
from dvc.commands.config import parent_config_parser
|
||||||
|
|
||||||
machine_HELP = "Set up and manage cloud machines."
|
machine_HELP = "Set up and manage cloud machines."
|
||||||
machine_parser = subparsers.add_parser(
|
machine_parser = subparsers.add_parser(
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
from dvc.utils.serialize import encode_exception
|
from dvc.utils.serialize import encode_exception
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
@@ -0,0 +1,458 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from funcy import first
|
||||||
|
|
||||||
|
from dvc.cli import completion
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||||
|
from dvc.exceptions import DvcException
|
||||||
|
from dvc.ui import ui
|
||||||
|
from dvc.utils import format_link
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _show_json(renderers, split=False):
|
||||||
|
from dvc.render.convert import to_json
|
||||||
|
|
||||||
|
result = {
|
||||||
|
renderer.name: to_json(renderer, split) for renderer in renderers
|
||||||
|
}
|
||||||
|
ui.write_json(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _adjust_vega_renderers(renderers):
|
||||||
|
from dvc.render import VERSION_FIELD
|
||||||
|
from dvc_render import VegaRenderer
|
||||||
|
|
||||||
|
for r in renderers:
|
||||||
|
if isinstance(r, VegaRenderer):
|
||||||
|
if _data_versions_count(r) > 1:
|
||||||
|
summary = _summarize_version_infos(r)
|
||||||
|
for dp in r.datapoints:
|
||||||
|
vi = dp.pop(VERSION_FIELD, {})
|
||||||
|
keys = list(vi.keys())
|
||||||
|
for key in keys:
|
||||||
|
if not (len(summary.get(key, set())) > 1):
|
||||||
|
vi.pop(key)
|
||||||
|
if vi:
|
||||||
|
dp["rev"] = "::".join(vi.values())
|
||||||
|
else:
|
||||||
|
for dp in r.datapoints:
|
||||||
|
dp.pop(VERSION_FIELD, {})
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize_version_infos(renderer):
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from dvc.render import VERSION_FIELD
|
||||||
|
|
||||||
|
result = defaultdict(set)
|
||||||
|
|
||||||
|
for dp in renderer.datapoints:
|
||||||
|
for key, value in dp.get(VERSION_FIELD, {}).items():
|
||||||
|
result[key].add(value)
|
||||||
|
return dict(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _data_versions_count(renderer):
|
||||||
|
from itertools import product
|
||||||
|
|
||||||
|
summary = _summarize_version_infos(renderer)
|
||||||
|
x = product(summary.get("filename", {None}), summary.get("field", {None}))
|
||||||
|
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
|
||||||
|
|
||||||
|
def _props(self):
|
||||||
|
from dvc.schema import PLOT_PROPS
|
||||||
|
|
||||||
|
# Pass only props specified by user, to not shadow ones from plot def
|
||||||
|
props = {p: getattr(self.args, p) for p in PLOT_PROPS}
|
||||||
|
return {k: v for k, v in props.items() if v is not None}
|
||||||
|
|
||||||
|
def _config_files(self):
|
||||||
|
config_files = None
|
||||||
|
if self.args.from_config:
|
||||||
|
config_files = {self.args.from_config}
|
||||||
|
return config_files
|
||||||
|
|
||||||
|
def _html_template_path(self):
|
||||||
|
html_template_path = self.args.html_template
|
||||||
|
if not html_template_path:
|
||||||
|
html_template_path = self.repo.config.get("plots", {}).get(
|
||||||
|
"html_template", None
|
||||||
|
)
|
||||||
|
if html_template_path and not os.path.isabs(html_template_path):
|
||||||
|
html_template_path = os.path.join(
|
||||||
|
self.repo.dvc_dir, html_template_path
|
||||||
|
)
|
||||||
|
return html_template_path
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dvc.render.match import match_defs_renderers
|
||||||
|
from dvc_render import render_html
|
||||||
|
|
||||||
|
if self.args.show_vega:
|
||||||
|
if not self.args.targets:
|
||||||
|
logger.error("please specify a target for `--show-vega`")
|
||||||
|
return 1
|
||||||
|
if len(self.args.targets) > 1:
|
||||||
|
logger.error(
|
||||||
|
"you can only specify one target for `--show-vega`"
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
if self.args.json:
|
||||||
|
logger.error(
|
||||||
|
"'--show-vega' and '--json' are mutually exclusive "
|
||||||
|
"options."
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
plots_data = self._func(
|
||||||
|
targets=self.args.targets,
|
||||||
|
props=self._props(),
|
||||||
|
config_files=self._config_files(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not plots_data:
|
||||||
|
ui.error_write(
|
||||||
|
"No plots were loaded, "
|
||||||
|
"visualization file will not be created."
|
||||||
|
)
|
||||||
|
|
||||||
|
out: str = self.args.out or self.repo.config.get("plots", {}).get(
|
||||||
|
"out_dir", "dvc_plots"
|
||||||
|
)
|
||||||
|
|
||||||
|
renderers_out = (
|
||||||
|
out if self.args.json else os.path.join(out, "static")
|
||||||
|
)
|
||||||
|
|
||||||
|
renderers = match_defs_renderers(
|
||||||
|
data=plots_data,
|
||||||
|
out=renderers_out,
|
||||||
|
templates_dir=self.repo.plots.templates_dir,
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
|
||||||
|
if renderers:
|
||||||
|
render_html(
|
||||||
|
renderers=renderers,
|
||||||
|
output_file=output_file,
|
||||||
|
template_path=self._html_template_path(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ui.write(output_file.as_uri())
|
||||||
|
auto_open = self.repo.config["plots"].get("auto_open", False)
|
||||||
|
if self.args.open or auto_open:
|
||||||
|
if not auto_open:
|
||||||
|
ui.write(
|
||||||
|
"To enable auto opening, you can run:\n"
|
||||||
|
"\n"
|
||||||
|
"\tdvc config plots.auto_open true"
|
||||||
|
)
|
||||||
|
return ui.open_browser(output_file)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
except DvcException:
|
||||||
|
logger.exception("")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class CmdPlotsShow(CmdPlots):
|
||||||
|
UNINITIALIZED = True
|
||||||
|
|
||||||
|
def _func(self, *args, **kwargs):
|
||||||
|
return self.repo.plots.show(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdPlotsDiff(CmdPlots):
|
||||||
|
UNINITIALIZED = True
|
||||||
|
|
||||||
|
def _func(self, *args, **kwargs):
|
||||||
|
return self.repo.plots.diff(
|
||||||
|
*args,
|
||||||
|
revs=self.args.revisions,
|
||||||
|
experiment=self.args.experiment,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdPlotsModify(CmdPlots):
|
||||||
|
def run(self):
|
||||||
|
self.repo.plots.modify(
|
||||||
|
self.args.target, props=self._props(), unset=self.args.unset
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class CmdPlotsTemplates(CmdBase):
|
||||||
|
TEMPLATES_CHOICES = [
|
||||||
|
"simple",
|
||||||
|
"linear",
|
||||||
|
"confusion",
|
||||||
|
"confusion_normalized",
|
||||||
|
"scatter",
|
||||||
|
"smooth",
|
||||||
|
]
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
from dvc_render.vega_templates import dump_templates
|
||||||
|
|
||||||
|
try:
|
||||||
|
out = (
|
||||||
|
os.path.join(os.getcwd(), self.args.out)
|
||||||
|
if self.args.out
|
||||||
|
else self.repo.plots.templates_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
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}'.")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
except DvcException:
|
||||||
|
logger.exception("")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(subparsers, parent_parser):
|
||||||
|
PLOTS_HELP = "Commands to visualize and compare plot data."
|
||||||
|
|
||||||
|
plots_parser = subparsers.add_parser(
|
||||||
|
"plots",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(PLOTS_HELP, "plots"),
|
||||||
|
help=PLOTS_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
plots_subparsers = plots_parser.add_subparsers(
|
||||||
|
dest="cmd",
|
||||||
|
help="Use `dvc plots CMD --help` to display command-specific help.",
|
||||||
|
)
|
||||||
|
|
||||||
|
fix_subparsers(plots_subparsers)
|
||||||
|
|
||||||
|
SHOW_HELP = (
|
||||||
|
"Generate plots from target files or plots definitions from "
|
||||||
|
"`dvc.yaml` file."
|
||||||
|
)
|
||||||
|
plots_show_parser = plots_subparsers.add_parser(
|
||||||
|
"show",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(SHOW_HELP, "plots/show"),
|
||||||
|
help=SHOW_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
plots_show_parser.add_argument(
|
||||||
|
"targets",
|
||||||
|
nargs="*",
|
||||||
|
help="Plots to visualize. Supports any file path, or plot name "
|
||||||
|
"defined in `dvc.yaml`. "
|
||||||
|
"Shows all plots by default.",
|
||||||
|
).complete = completion.FILE
|
||||||
|
_add_props_arguments(plots_show_parser)
|
||||||
|
_add_output_argument(plots_show_parser)
|
||||||
|
_add_ui_arguments(plots_show_parser)
|
||||||
|
plots_show_parser.set_defaults(func=CmdPlotsShow)
|
||||||
|
|
||||||
|
PLOTS_DIFF_HELP = (
|
||||||
|
"Show multiple versions of plot data "
|
||||||
|
"by plotting them in a single image."
|
||||||
|
)
|
||||||
|
plots_diff_parser = plots_subparsers.add_parser(
|
||||||
|
"diff",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(PLOTS_DIFF_HELP, "plots/diff"),
|
||||||
|
help=PLOTS_DIFF_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
plots_diff_parser.add_argument(
|
||||||
|
"--targets",
|
||||||
|
nargs="*",
|
||||||
|
help=(
|
||||||
|
"Specific plots to visualize. "
|
||||||
|
"Accepts any file path or plot name from `dvc.yaml` file. "
|
||||||
|
"Shows all tracked plots by default."
|
||||||
|
),
|
||||||
|
metavar="<paths>",
|
||||||
|
).complete = completion.FILE
|
||||||
|
plots_diff_parser.add_argument(
|
||||||
|
"-e",
|
||||||
|
"--experiment",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
plots_diff_parser.add_argument(
|
||||||
|
"revisions", nargs="*", default=None, help="Git commits to plot from"
|
||||||
|
)
|
||||||
|
_add_props_arguments(plots_diff_parser)
|
||||||
|
_add_output_argument(plots_diff_parser)
|
||||||
|
_add_ui_arguments(plots_diff_parser)
|
||||||
|
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
|
||||||
|
|
||||||
|
PLOTS_MODIFY_HELP = (
|
||||||
|
"Modify display properties of data-series plot outputs "
|
||||||
|
"(has no effect on image-type plots)."
|
||||||
|
)
|
||||||
|
plots_modify_parser = plots_subparsers.add_parser(
|
||||||
|
"modify",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(PLOTS_MODIFY_HELP, "plots/modify"),
|
||||||
|
help=PLOTS_MODIFY_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
plots_modify_parser.add_argument(
|
||||||
|
"target", help="Plot output to set properties to"
|
||||||
|
).complete = completion.FILE
|
||||||
|
_add_props_arguments(plots_modify_parser)
|
||||||
|
plots_modify_parser.add_argument(
|
||||||
|
"--unset",
|
||||||
|
nargs="*",
|
||||||
|
metavar="<property>",
|
||||||
|
help="Unset one or more display properties.",
|
||||||
|
)
|
||||||
|
plots_modify_parser.set_defaults(func=CmdPlotsModify)
|
||||||
|
|
||||||
|
TEMPLATES_HELP = (
|
||||||
|
"Write built-in plots templates to a directory "
|
||||||
|
"(.dvc/plots by default)."
|
||||||
|
)
|
||||||
|
plots_templates_parser = plots_subparsers.add_parser(
|
||||||
|
"templates",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(TEMPLATES_HELP, "plots/templates"),
|
||||||
|
help=TEMPLATES_HELP,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
plots_templates_parser.add_argument(
|
||||||
|
"target",
|
||||||
|
default=None,
|
||||||
|
nargs="?",
|
||||||
|
choices=CmdPlotsTemplates.TEMPLATES_CHOICES,
|
||||||
|
help="Template to write. Writes all templates by default.",
|
||||||
|
)
|
||||||
|
_add_output_argument(plots_templates_parser, typ="templates")
|
||||||
|
plots_templates_parser.set_defaults(func=CmdPlotsTemplates)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_props_arguments(parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"-t",
|
||||||
|
"--template",
|
||||||
|
nargs="?",
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Special JSON or HTML schema file to inject with the data. "
|
||||||
|
"See {}".format(
|
||||||
|
format_link("https://man.dvc.org/plots#plot-templates")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
metavar="<path>",
|
||||||
|
).complete = completion.FILE
|
||||||
|
parser.add_argument(
|
||||||
|
"-x", default=None, help="Field name for X axis.", metavar="<field>"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-y", default=None, help="Field name for Y axis.", metavar="<field>"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-header",
|
||||||
|
action="store_false",
|
||||||
|
dest="header",
|
||||||
|
default=None, # Use default None to distinguish when it's not used
|
||||||
|
help="Provided CSV or TSV datafile does not have a header.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--title", default=None, metavar="<text>", help="Plot title."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--x-label", default=None, help="X axis label", metavar="<text>"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--y-label", default=None, help="Y axis label", metavar="<text>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_output_argument(parser, typ="plots"):
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--out",
|
||||||
|
default=None,
|
||||||
|
help=f"Directory to save {typ} to.",
|
||||||
|
metavar="<path>",
|
||||||
|
).complete = completion.DIR
|
||||||
|
|
||||||
|
|
||||||
|
def _add_ui_arguments(parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--show-vega",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Show output in Vega format.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
"--show-json",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--split", action="store_true", default=False, help=argparse.SUPPRESS
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--open",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Open plot file directly in the browser.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--html-template",
|
||||||
|
default=None,
|
||||||
|
help="Custom HTML template for VEGA visualization.",
|
||||||
|
metavar="<path>",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--from-config",
|
||||||
|
default=None,
|
||||||
|
metavar="<path>",
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from dvc.command.base import append_doc_link, fix_subparsers
|
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||||
from dvc.command.config import CmdConfig
|
from dvc.commands.config import CmdConfig
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
from dvc.utils import format_link
|
from dvc.utils import format_link
|
||||||
|
|
||||||
@@ -113,8 +113,8 @@ class CmdRemoteDefault(CmdRemote):
|
|||||||
class CmdRemoteList(CmdRemote):
|
class CmdRemoteList(CmdRemote):
|
||||||
def run(self):
|
def run(self):
|
||||||
conf = self.config.read(self.args.level)
|
conf = self.config.read(self.args.level)
|
||||||
for name, conf in conf["remote"].items():
|
for name, remote_conf in conf["remote"].items():
|
||||||
ui.write(name, conf["url"], sep="\t")
|
ui.write(name, remote_conf["url"], sep="\t")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ class CmdRemoteRename(CmdRemote):
|
|||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
from dvc.command.config import parent_config_parser
|
from dvc.commands.config import parent_config_parser
|
||||||
|
|
||||||
REMOTE_HELP = "Set up and manage data remotes."
|
REMOTE_HELP = "Set up and manage data remotes."
|
||||||
remote_parser = subparsers.add_parser(
|
remote_parser = subparsers.add_parser(
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
from dvc.command.status import CmdDataStatus
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.commands.status import CmdDataStatus
|
||||||
|
|
||||||
|
|
||||||
class CmdRepro(CmdBase):
|
class CmdRepro(CmdBase):
|
||||||
def run(self):
|
def run(self):
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
stages = self.repo.reproduce(**self._repro_kwargs)
|
stages = self.repo.reproduce(
|
||||||
|
**self._common_kwargs, **self._repro_kwargs
|
||||||
|
)
|
||||||
if len(stages) == 0:
|
if len(stages) == 0:
|
||||||
ui.write(CmdDataStatus.UP_TO_DATE_MSG)
|
ui.write(CmdDataStatus.UP_TO_DATE_MSG)
|
||||||
else:
|
else:
|
||||||
ui.write(
|
ui.write("Use `dvc push` to send your updates to remote storage.")
|
||||||
"Use `dvc push` to send your updates to " "remote storage."
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.args.metrics:
|
if self.args.metrics:
|
||||||
from dvc.compare import show_metrics
|
from dvc.compare import show_metrics
|
||||||
@@ -26,7 +27,7 @@ class CmdRepro(CmdBase):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _repro_kwargs(self):
|
def _common_kwargs(self):
|
||||||
return {
|
return {
|
||||||
"targets": self.args.targets,
|
"targets": self.args.targets,
|
||||||
"single_item": self.args.single_item,
|
"single_item": self.args.single_item,
|
||||||
@@ -35,12 +36,17 @@ class CmdRepro(CmdBase):
|
|||||||
"interactive": self.args.interactive,
|
"interactive": self.args.interactive,
|
||||||
"pipeline": self.args.pipeline,
|
"pipeline": self.args.pipeline,
|
||||||
"all_pipelines": self.args.all_pipelines,
|
"all_pipelines": self.args.all_pipelines,
|
||||||
"run_cache": not self.args.no_run_cache,
|
|
||||||
"no_commit": self.args.no_commit,
|
|
||||||
"downstream": self.args.downstream,
|
"downstream": self.args.downstream,
|
||||||
"recursive": self.args.recursive,
|
"recursive": self.args.recursive,
|
||||||
"force_downstream": self.args.force_downstream,
|
"force_downstream": self.args.force_downstream,
|
||||||
"pull": self.args.pull,
|
"pull": self.args.pull,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _repro_kwargs(self):
|
||||||
|
return {
|
||||||
|
"run_cache": not self.args.no_run_cache,
|
||||||
|
"no_commit": self.args.no_commit,
|
||||||
"glob": self.args.glob,
|
"glob": self.args.glob,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +64,13 @@ def add_arguments(repro_parser):
|
|||||||
default=False,
|
default=False,
|
||||||
help="Reproduce even if dependencies were not changed.",
|
help="Reproduce even if dependencies were not changed.",
|
||||||
)
|
)
|
||||||
|
repro_parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--interactive",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Ask for confirmation before reproducing each stage.",
|
||||||
|
)
|
||||||
repro_parser.add_argument(
|
repro_parser.add_argument(
|
||||||
"-s",
|
"-s",
|
||||||
"--single-item",
|
"--single-item",
|
||||||
@@ -66,27 +79,6 @@ def add_arguments(repro_parser):
|
|||||||
help="Reproduce only single data item without recursive dependencies "
|
help="Reproduce only single data item without recursive dependencies "
|
||||||
"check.",
|
"check.",
|
||||||
)
|
)
|
||||||
repro_parser.add_argument(
|
|
||||||
"-m",
|
|
||||||
"--metrics",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Show metrics after reproduction.",
|
|
||||||
)
|
|
||||||
repro_parser.add_argument(
|
|
||||||
"--dry",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Only print the commands that would be executed without "
|
|
||||||
"actually executing.",
|
|
||||||
)
|
|
||||||
repro_parser.add_argument(
|
|
||||||
"-i",
|
|
||||||
"--interactive",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Ask for confirmation before reproducing each stage.",
|
|
||||||
)
|
|
||||||
repro_parser.add_argument(
|
repro_parser.add_argument(
|
||||||
"-p",
|
"-p",
|
||||||
"--pipeline",
|
"--pipeline",
|
||||||
@@ -110,13 +102,17 @@ def add_arguments(repro_parser):
|
|||||||
help="Reproduce all stages in the specified directory.",
|
help="Reproduce all stages in the specified directory.",
|
||||||
)
|
)
|
||||||
repro_parser.add_argument(
|
repro_parser.add_argument(
|
||||||
"--no-run-cache",
|
"-m",
|
||||||
|
"--metrics",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help=(
|
help="Show metrics after reproduction.",
|
||||||
"Execute stage commands even if they have already been run with "
|
)
|
||||||
"the same command/dependencies/outputs/etc before."
|
repro_parser.add_argument(
|
||||||
),
|
"--downstream",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Start from the specified stages when reproducing pipelines.",
|
||||||
)
|
)
|
||||||
repro_parser.add_argument(
|
repro_parser.add_argument(
|
||||||
"--force-downstream",
|
"--force-downstream",
|
||||||
@@ -125,18 +121,6 @@ def add_arguments(repro_parser):
|
|||||||
help="Reproduce all descendants of a changed stage even if their "
|
help="Reproduce all descendants of a changed stage even if their "
|
||||||
"direct dependencies didn't change.",
|
"direct dependencies didn't change.",
|
||||||
)
|
)
|
||||||
repro_parser.add_argument(
|
|
||||||
"--no-commit",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Don't put files/directories into cache.",
|
|
||||||
)
|
|
||||||
repro_parser.add_argument(
|
|
||||||
"--downstream",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Start from the specified stages when reproducing pipelines.",
|
|
||||||
)
|
|
||||||
repro_parser.add_argument(
|
repro_parser.add_argument(
|
||||||
"--pull",
|
"--pull",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -147,10 +131,11 @@ def add_arguments(repro_parser):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
repro_parser.add_argument(
|
repro_parser.add_argument(
|
||||||
"--glob",
|
"--dry",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="Allows targets containing shell-style wildcards.",
|
help="Only print the commands that would be executed without "
|
||||||
|
"actually executing.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -165,5 +150,28 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=REPRO_HELP,
|
help=REPRO_HELP,
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
|
# repro/exp run shared args
|
||||||
add_arguments(repro_parser)
|
add_arguments(repro_parser)
|
||||||
|
# repro only args
|
||||||
|
repro_parser.add_argument(
|
||||||
|
"--glob",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Allows targets containing shell-style wildcards.",
|
||||||
|
)
|
||||||
|
repro_parser.add_argument(
|
||||||
|
"--no-commit",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Don't put files/directories into cache.",
|
||||||
|
)
|
||||||
|
repro_parser.add_argument(
|
||||||
|
"--no-run-cache",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=(
|
||||||
|
"Execute stage commands even if they have already been run with "
|
||||||
|
"the same command/dependencies/outputs/etc before."
|
||||||
|
),
|
||||||
|
)
|
||||||
repro_parser.set_defaults(func=CmdRepro)
|
repro_parser.set_defaults(func=CmdRepro)
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.utils import relpath
|
from dvc.utils import relpath
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
from dvc.command.stage import parse_cmd
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.commands.stage import parse_cmd
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -52,7 +53,7 @@ class CmdRun(CmdBase):
|
|||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
from dvc.command.stage import _add_common_args
|
from dvc.commands.stage import _add_common_args
|
||||||
|
|
||||||
RUN_HELP = (
|
RUN_HELP = (
|
||||||
"Generate a dvc.yaml file from a command and execute the command."
|
"Generate a dvc.yaml file from a command and execute the command."
|
||||||
@@ -68,12 +69,25 @@ def add_parser(subparsers, parent_parser):
|
|||||||
run_parser.add_argument(
|
run_parser.add_argument(
|
||||||
"--file", metavar="<filename>", help=argparse.SUPPRESS
|
"--file", metavar="<filename>", help=argparse.SUPPRESS
|
||||||
)
|
)
|
||||||
|
run_parser.add_argument(
|
||||||
|
"--single-stage",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
_add_common_args(run_parser)
|
||||||
run_parser.add_argument(
|
run_parser.add_argument(
|
||||||
"--no-exec",
|
"--no-exec",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="Only create dvc.yaml without actually running it.",
|
help="Only create dvc.yaml without actually running it.",
|
||||||
)
|
)
|
||||||
|
run_parser.add_argument(
|
||||||
|
"--no-commit",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Don't put files/directories into cache.",
|
||||||
|
)
|
||||||
run_parser.add_argument(
|
run_parser.add_argument(
|
||||||
"--no-run-cache",
|
"--no-run-cache",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -83,17 +97,4 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"with the same command/dependencies/outputs/etc before."
|
"with the same command/dependencies/outputs/etc before."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
run_parser.add_argument(
|
|
||||||
"--no-commit",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Don't put files/directories into cache.",
|
|
||||||
)
|
|
||||||
run_parser.add_argument(
|
|
||||||
"--single-stage",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help=argparse.SUPPRESS,
|
|
||||||
)
|
|
||||||
_add_common_args(run_parser)
|
|
||||||
run_parser.set_defaults(func=CmdRun)
|
run_parser.set_defaults(func=CmdRun)
|
||||||
@@ -3,8 +3,9 @@ import logging
|
|||||||
from itertools import chain, filterfalse
|
from itertools import chain, filterfalse
|
||||||
from typing import TYPE_CHECKING, Dict, Iterable, List
|
from typing import TYPE_CHECKING, Dict, Iterable, List
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||||
from dvc.utils.cli_parse import parse_params
|
from dvc.utils.cli_parse import parse_params
|
||||||
from dvc.utils.humanize import truncate_text
|
from dvc.utils.humanize import truncate_text
|
||||||
|
|
||||||
@@ -93,9 +94,7 @@ class CmdStageList(CmdBase):
|
|||||||
self.repo.stage_collection_error_handler = log_error
|
self.repo.stage_collection_error_handler = log_error
|
||||||
|
|
||||||
stages = self._get_stages()
|
stages = self._get_stages()
|
||||||
names_only = self.args.names_only
|
data = prepare_stages_data(stages, description=not self.args.name_only)
|
||||||
|
|
||||||
data = prepare_stages_data(stages, description=not names_only)
|
|
||||||
ui.table(data.items())
|
ui.table(data.items())
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -138,6 +137,13 @@ class CmdStageAdd(CmdBase):
|
|||||||
|
|
||||||
|
|
||||||
def _add_common_args(parser):
|
def _add_common_args(parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Overwrite existing stage",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-d",
|
"-d",
|
||||||
"--deps",
|
"--deps",
|
||||||
@@ -146,6 +152,14 @@ def _add_common_args(parser):
|
|||||||
help="Declare dependencies for reproducible cmd.",
|
help="Declare dependencies for reproducible cmd.",
|
||||||
metavar="<path>",
|
metavar="<path>",
|
||||||
).complete = completion.FILE
|
).complete = completion.FILE
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--params",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Declare parameter to use as additional dependency.",
|
||||||
|
metavar="[<filename>:]<params_list>",
|
||||||
|
).complete = completion.FILE
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-o",
|
"-o",
|
||||||
"--outs",
|
"--outs",
|
||||||
@@ -164,13 +178,36 @@ def _add_common_args(parser):
|
|||||||
metavar="<filename>",
|
metavar="<filename>",
|
||||||
).complete = completion.FILE
|
).complete = completion.FILE
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p",
|
"-c",
|
||||||
"--params",
|
"--checkpoints",
|
||||||
action="append",
|
action="append",
|
||||||
default=[],
|
default=[],
|
||||||
help="Declare parameter to use as additional dependency.",
|
help="Declare checkpoint output file or directory for 'dvc exp run'. "
|
||||||
metavar="[<filename>:]<params_list>",
|
"Not compatible with 'dvc repro'.",
|
||||||
|
metavar="<filename>",
|
||||||
).complete = completion.FILE
|
).complete = completion.FILE
|
||||||
|
parser.add_argument(
|
||||||
|
"--external",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Allow outputs that are outside of the DVC repository.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--outs-persist",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Declare output file or directory that will not be "
|
||||||
|
"removed upon repro.",
|
||||||
|
metavar="<filename>",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--outs-persist-no-cache",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Declare output file or directory that will not be "
|
||||||
|
"removed upon repro (do not put into DVC cache).",
|
||||||
|
metavar="<filename>",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-m",
|
"-m",
|
||||||
"--metrics",
|
"--metrics",
|
||||||
@@ -213,7 +250,7 @@ def _add_common_args(parser):
|
|||||||
"--live-no-summary",
|
"--live-no-summary",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="Signal dvclive logger to not dump latest metrics file.",
|
help=argparse.SUPPRESS,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--live-no-html",
|
"--live-no-html",
|
||||||
@@ -227,50 +264,12 @@ def _add_common_args(parser):
|
|||||||
help="Directory within your repo to run your command in.",
|
help="Directory within your repo to run your command in.",
|
||||||
metavar="<path>",
|
metavar="<path>",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"-f",
|
|
||||||
"--force",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Overwrite existing stage",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--outs-persist",
|
|
||||||
action="append",
|
|
||||||
default=[],
|
|
||||||
help="Declare output file or directory that will not be "
|
|
||||||
"removed upon repro.",
|
|
||||||
metavar="<filename>",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--outs-persist-no-cache",
|
|
||||||
action="append",
|
|
||||||
default=[],
|
|
||||||
help="Declare output file or directory that will not be "
|
|
||||||
"removed upon repro (do not put into DVC cache).",
|
|
||||||
metavar="<filename>",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-c",
|
|
||||||
"--checkpoints",
|
|
||||||
action="append",
|
|
||||||
default=[],
|
|
||||||
help="Declare checkpoint output file or directory for 'dvc exp run'. "
|
|
||||||
"Not compatible with 'dvc repro'.",
|
|
||||||
metavar="<filename>",
|
|
||||||
).complete = completion.FILE
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--always-changed",
|
"--always-changed",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="Always consider this DVC-file as changed.",
|
help="Always consider this DVC-file as changed.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--external",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Allow outputs that are outside of the DVC repository.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--desc",
|
"--desc",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -357,6 +356,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help="List all stages inside the specified directory.",
|
help="List all stages inside the specified directory.",
|
||||||
)
|
)
|
||||||
stage_list_parser.add_argument(
|
stage_list_parser.add_argument(
|
||||||
|
"--name-only",
|
||||||
"--names-only",
|
"--names-only",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command.data_sync import CmdDataBase
|
from dvc.commands.data_sync import CmdDataBase
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
from dvc.utils import format_link
|
from dvc.utils import format_link
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command import completion
|
from dvc.cli import completion
|
||||||
from dvc.command.base import CmdBase, append_doc_link
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -27,7 +28,10 @@ class CmdUpdate(CmdBase):
|
|||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
UPDATE_HELP = "Update data artifacts imported from other DVC repositories."
|
UPDATE_HELP = (
|
||||||
|
"Update data artifact imported (via dvc import or dvc import-url) "
|
||||||
|
"from an external DVC repository or URL."
|
||||||
|
)
|
||||||
update_parser = subparsers.add_parser(
|
update_parser = subparsers.add_parser(
|
||||||
"update",
|
"update",
|
||||||
parents=[parent_parser],
|
parents=[parent_parser],
|
||||||
@@ -70,7 +74,6 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=(
|
help=(
|
||||||
"Number of jobs to run simultaneously. "
|
"Number of jobs to run simultaneously. "
|
||||||
"The default value is 4 * cpu_count(). "
|
"The default value is 4 * cpu_count(). "
|
||||||
"For SSH remotes, the default is 4. "
|
|
||||||
),
|
),
|
||||||
metavar="<number>",
|
metavar="<number>",
|
||||||
)
|
)
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
+63
-9
@@ -11,6 +11,7 @@ from typing import (
|
|||||||
List,
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
MutableSequence,
|
MutableSequence,
|
||||||
|
Optional,
|
||||||
Sequence,
|
Sequence,
|
||||||
Set,
|
Set,
|
||||||
Tuple,
|
Tuple,
|
||||||
@@ -21,6 +22,7 @@ from typing import (
|
|||||||
from funcy import reraise
|
from funcy import reraise
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from dvc.types import StrPath
|
||||||
from dvc.ui.table import CellT
|
from dvc.ui.table import CellT
|
||||||
|
|
||||||
|
|
||||||
@@ -37,11 +39,21 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
self._columns: Dict[str, Column] = {name: Column() for name in columns}
|
self._columns: Dict[str, Column] = {name: Column() for name in columns}
|
||||||
self._keys: List[str] = list(columns)
|
self._keys: List[str] = list(columns)
|
||||||
self._fill_value = fill_value
|
self._fill_value = fill_value
|
||||||
|
self._protected: Set[str] = set()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def columns(self) -> List[Column]:
|
def columns(self) -> List[Column]:
|
||||||
return list(map(self.column, self.keys()))
|
return list(map(self.column, self.keys()))
|
||||||
|
|
||||||
|
def is_protected(self, col_name) -> bool:
|
||||||
|
return col_name in self._protected
|
||||||
|
|
||||||
|
def protect(self, *col_names: str):
|
||||||
|
self._protected.update(col_names)
|
||||||
|
|
||||||
|
def unprotect(self, *col_names: str):
|
||||||
|
self._protected = self._protected.difference(col_names)
|
||||||
|
|
||||||
def column(self, name: str) -> Column:
|
def column(self, name: str) -> Column:
|
||||||
return self._columns[name]
|
return self._columns[name]
|
||||||
|
|
||||||
@@ -125,9 +137,10 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
return len(self.columns), len(self)
|
return len(self.columns), len(self)
|
||||||
|
|
||||||
def drop(self, *col_names: str) -> None:
|
def drop(self, *col_names: str) -> None:
|
||||||
for col_name in col_names:
|
for col in col_names:
|
||||||
self._keys.remove(col_name)
|
if not self.is_protected(col):
|
||||||
self._columns.pop(col_name)
|
self._keys.remove(col)
|
||||||
|
self._columns.pop(col)
|
||||||
|
|
||||||
def rename(self, from_col_name: str, to_col_name: str) -> None:
|
def rename(self, from_col_name: str, to_col_name: str) -> None:
|
||||||
self._columns[to_col_name] = self._columns.pop(from_col_name)
|
self._columns[to_col_name] = self._columns.pop(from_col_name)
|
||||||
@@ -153,6 +166,24 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
writer.writerow(row)
|
writer.writerow(row)
|
||||||
return buff.getvalue()
|
return buff.getvalue()
|
||||||
|
|
||||||
|
def to_parallel_coordinates(
|
||||||
|
self, output_path: "StrPath", color_by: str = None
|
||||||
|
) -> "StrPath":
|
||||||
|
from dvc_render.html import render_html
|
||||||
|
from dvc_render.plotly import ParallelCoordinatesRenderer
|
||||||
|
|
||||||
|
render_html(
|
||||||
|
renderers=[
|
||||||
|
ParallelCoordinatesRenderer(
|
||||||
|
self.as_dict(),
|
||||||
|
color_by=color_by,
|
||||||
|
fill_value=self._fill_value,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
output_file=output_path,
|
||||||
|
)
|
||||||
|
return output_path
|
||||||
|
|
||||||
def add_column(self, name: str) -> None:
|
def add_column(self, name: str) -> None:
|
||||||
self._columns[name] = Column([self._fill_value] * len(self))
|
self._columns[name] = Column([self._fill_value] * len(self))
|
||||||
self._keys.append(name)
|
self._keys.append(name)
|
||||||
@@ -184,7 +215,12 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
{k: self._columns[k][i] for k in keys} for i in range(len(self))
|
{k: self._columns[k][i] for k in keys} for i in range(len(self))
|
||||||
]
|
]
|
||||||
|
|
||||||
def dropna(self, axis: str = "rows", how="any"):
|
def dropna(
|
||||||
|
self,
|
||||||
|
axis: str = "rows",
|
||||||
|
how="any",
|
||||||
|
subset: Optional[Iterable[str]] = None,
|
||||||
|
):
|
||||||
if axis not in ["rows", "cols"]:
|
if axis not in ["rows", "cols"]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid 'axis' value {axis}."
|
f"Invalid 'axis' value {axis}."
|
||||||
@@ -196,13 +232,15 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
match_line: Set = set()
|
match_line: Set = set()
|
||||||
match = True
|
match_any = True
|
||||||
if how == "all":
|
if how == "all":
|
||||||
match = False
|
match_any = False
|
||||||
|
|
||||||
for n_row, row in enumerate(self):
|
for n_row, row in enumerate(self):
|
||||||
for n_col, col in enumerate(row):
|
for n_col, col in enumerate(row):
|
||||||
if (col == self._fill_value) is match:
|
if subset and self.keys()[n_col] not in subset:
|
||||||
|
continue
|
||||||
|
if (col == self._fill_value) is match_any:
|
||||||
if axis == "rows":
|
if axis == "rows":
|
||||||
match_line.add(n_row)
|
match_line.add(n_row)
|
||||||
break
|
break
|
||||||
@@ -229,7 +267,12 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
else:
|
else:
|
||||||
self.drop(*to_drop)
|
self.drop(*to_drop)
|
||||||
|
|
||||||
def drop_duplicates(self, axis: str = "rows"):
|
def drop_duplicates(
|
||||||
|
self,
|
||||||
|
axis: str = "rows",
|
||||||
|
subset: Optional[Iterable[str]] = None,
|
||||||
|
ignore_empty: bool = True,
|
||||||
|
):
|
||||||
if axis not in ["rows", "cols"]:
|
if axis not in ["rows", "cols"]:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid 'axis' value {axis}."
|
f"Invalid 'axis' value {axis}."
|
||||||
@@ -239,8 +282,12 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
if axis == "cols":
|
if axis == "cols":
|
||||||
cols_to_drop: List[str] = []
|
cols_to_drop: List[str] = []
|
||||||
for n_col, col in enumerate(self.columns):
|
for n_col, col in enumerate(self.columns):
|
||||||
|
if subset and self.keys()[n_col] not in subset:
|
||||||
|
continue
|
||||||
# Cast to str because Text is not hashable error
|
# Cast to str because Text is not hashable error
|
||||||
unique_vals = {str(x) for x in col if x != self._fill_value}
|
unique_vals = {str(x) for x in col}
|
||||||
|
if ignore_empty and self._fill_value in unique_vals:
|
||||||
|
unique_vals -= {self._fill_value}
|
||||||
if len(unique_vals) == 1:
|
if len(unique_vals) == 1:
|
||||||
cols_to_drop.append(self.keys()[n_col])
|
cols_to_drop.append(self.keys()[n_col])
|
||||||
self.drop(*cols_to_drop)
|
self.drop(*cols_to_drop)
|
||||||
@@ -249,6 +296,13 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
unique_rows = []
|
unique_rows = []
|
||||||
rows_to_drop: List[int] = []
|
rows_to_drop: List[int] = []
|
||||||
for n_row, row in enumerate(self):
|
for n_row, row in enumerate(self):
|
||||||
|
if subset:
|
||||||
|
row = [
|
||||||
|
col
|
||||||
|
for n_col, col in enumerate(row)
|
||||||
|
if self.keys()[n_col] in subset
|
||||||
|
]
|
||||||
|
|
||||||
tuple_row = tuple(row)
|
tuple_row = tuple(row)
|
||||||
if tuple_row in unique_rows:
|
if tuple_row in unique_rows:
|
||||||
rows_to_drop.append(n_row)
|
rows_to_drop.append(n_row)
|
||||||
|
|||||||
+15
-11
@@ -86,22 +86,21 @@ class Config(dict):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self, dvc_dir=None, validate=True, fs=None, config=None
|
self, dvc_dir=None, validate=True, fs=None, config=None
|
||||||
): # pylint: disable=super-init-not-called
|
): # pylint: disable=super-init-not-called
|
||||||
from dvc.fs.local import LocalFileSystem
|
from dvc.fs import LocalFileSystem
|
||||||
|
|
||||||
self.dvc_dir = dvc_dir
|
self.dvc_dir = dvc_dir
|
||||||
|
self.wfs = LocalFileSystem()
|
||||||
|
self.fs = fs or self.wfs
|
||||||
|
|
||||||
if not dvc_dir:
|
if not dvc_dir:
|
||||||
try:
|
try:
|
||||||
from dvc.repo import Repo
|
from dvc.repo import Repo
|
||||||
|
|
||||||
self.dvc_dir = os.path.join(Repo.find_dvc_dir())
|
self.dvc_dir = Repo.find_dvc_dir()
|
||||||
except NotDvcRepoError:
|
except NotDvcRepoError:
|
||||||
self.dvc_dir = None
|
self.dvc_dir = None
|
||||||
else:
|
else:
|
||||||
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
|
self.dvc_dir = self.fs.path.abspath(self.fs.path.realpath(dvc_dir))
|
||||||
|
|
||||||
self.wfs = LocalFileSystem(url=self.dvc_dir)
|
|
||||||
self.fs = fs or self.wfs
|
|
||||||
|
|
||||||
self.load(validate=validate, config=config)
|
self.load(validate=validate, config=config)
|
||||||
|
|
||||||
@@ -124,8 +123,8 @@ class Config(dict):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.dvc_dir is not None:
|
if self.dvc_dir is not None:
|
||||||
files["repo"] = os.path.join(self.dvc_dir, self.CONFIG)
|
files["repo"] = self.fs.path.join(self.dvc_dir, self.CONFIG)
|
||||||
files["local"] = os.path.join(self.dvc_dir, self.CONFIG_LOCAL)
|
files["local"] = self.fs.path.join(self.dvc_dir, self.CONFIG_LOCAL)
|
||||||
|
|
||||||
return files
|
return files
|
||||||
|
|
||||||
@@ -188,7 +187,7 @@ class Config(dict):
|
|||||||
filename = self.files[level]
|
filename = self.files[level]
|
||||||
fs = self._get_fs(level)
|
fs = self._get_fs(level)
|
||||||
|
|
||||||
logger.debug(f"Writing '{filename}'.")
|
logger.debug("Writing '%s'.", filename)
|
||||||
|
|
||||||
fs.makedirs(os.path.dirname(filename))
|
fs.makedirs(os.path.dirname(filename))
|
||||||
|
|
||||||
@@ -228,7 +227,7 @@ class Config(dict):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_relpath(conf_dir, path):
|
def _to_relpath(conf_dir, path):
|
||||||
from dvc.fs.local import localfs
|
from dvc.fs import localfs
|
||||||
from dvc.utils import relpath
|
from dvc.utils import relpath
|
||||||
|
|
||||||
from .config_schema import RelPath
|
from .config_schema import RelPath
|
||||||
@@ -266,7 +265,12 @@ class Config(dict):
|
|||||||
"key_path": func,
|
"key_path": func,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"machine": {str: {"startup_script": func}},
|
"machine": {
|
||||||
|
str: {
|
||||||
|
"startup_script": func,
|
||||||
|
"setup_script": func,
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return Schema(dirs_schema, extra=ALLOW_EXTRA)(conf)
|
return Schema(dirs_schema, extra=ALLOW_EXTRA)(conf)
|
||||||
|
|
||||||
|
|||||||
+22
-2
@@ -79,13 +79,13 @@ REMOTE_COMMON = {
|
|||||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||||
"jobs": All(Coerce(int), Range(1)),
|
"jobs": All(Coerce(int), Range(1)),
|
||||||
Optional("no_traverse"): Bool, # obsoleted
|
Optional("no_traverse"): Bool, # obsoleted
|
||||||
"verify": Bool,
|
|
||||||
}
|
}
|
||||||
LOCAL_COMMON = {
|
LOCAL_COMMON = {
|
||||||
"type": supported_cache_type,
|
"type": supported_cache_type,
|
||||||
Optional("protected", default=False): Bool, # obsoleted
|
Optional("protected", default=False): Bool, # obsoleted
|
||||||
"shared": All(Lower, Choices("group")),
|
"shared": All(Lower, Choices("group")),
|
||||||
Optional("slow_link_warning", default=True): Bool,
|
Optional("slow_link_warning", default=True): Bool,
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
}
|
}
|
||||||
HTTP_COMMON = {
|
HTTP_COMMON = {
|
||||||
"auth": All(Lower, Choices("basic", "digest", "custom")),
|
"auth": All(Lower, Choices("basic", "digest", "custom")),
|
||||||
@@ -95,6 +95,7 @@ HTTP_COMMON = {
|
|||||||
"ask_password": Bool,
|
"ask_password": Bool,
|
||||||
"ssl_verify": Any(Bool, str),
|
"ssl_verify": Any(Bool, str),
|
||||||
"method": str,
|
"method": str,
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
}
|
}
|
||||||
WEBDAV_COMMON = {
|
WEBDAV_COMMON = {
|
||||||
"user": str,
|
"user": str,
|
||||||
@@ -105,6 +106,7 @@ WEBDAV_COMMON = {
|
|||||||
"key_path": str,
|
"key_path": str,
|
||||||
"timeout": Coerce(int),
|
"timeout": Coerce(int),
|
||||||
"ssl_verify": Any(Bool, str),
|
"ssl_verify": Any(Bool, str),
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
SCHEMA = {
|
SCHEMA = {
|
||||||
@@ -150,6 +152,8 @@ SCHEMA = {
|
|||||||
"ssl_verify": Any(Bool, str),
|
"ssl_verify": Any(Bool, str),
|
||||||
"sse": str,
|
"sse": str,
|
||||||
"sse_kms_key_id": str,
|
"sse_kms_key_id": str,
|
||||||
|
"sse_customer_algorithm": str,
|
||||||
|
"sse_customer_key": str,
|
||||||
"acl": str,
|
"acl": str,
|
||||||
"grant_read": str,
|
"grant_read": str,
|
||||||
"grant_read_acp": str,
|
"grant_read_acp": str,
|
||||||
@@ -158,11 +162,13 @@ SCHEMA = {
|
|||||||
"cache_regions": bool,
|
"cache_regions": bool,
|
||||||
"read_timeout": Coerce(int),
|
"read_timeout": Coerce(int),
|
||||||
"connect_timeout": Coerce(int),
|
"connect_timeout": Coerce(int),
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
**REMOTE_COMMON,
|
**REMOTE_COMMON,
|
||||||
},
|
},
|
||||||
"gs": {
|
"gs": {
|
||||||
"projectname": str,
|
"projectname": str,
|
||||||
"credentialpath": str,
|
"credentialpath": str,
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
**REMOTE_COMMON,
|
**REMOTE_COMMON,
|
||||||
},
|
},
|
||||||
"ssh": {
|
"ssh": {
|
||||||
@@ -175,6 +181,7 @@ SCHEMA = {
|
|||||||
"timeout": Coerce(int),
|
"timeout": Coerce(int),
|
||||||
"gss_auth": Bool,
|
"gss_auth": Bool,
|
||||||
"allow_agent": Bool,
|
"allow_agent": Bool,
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
**REMOTE_COMMON,
|
**REMOTE_COMMON,
|
||||||
},
|
},
|
||||||
"hdfs": {"user": str, "kerb_ticket": str, **REMOTE_COMMON},
|
"hdfs": {"user": str, "kerb_ticket": str, **REMOTE_COMMON},
|
||||||
@@ -185,6 +192,7 @@ SCHEMA = {
|
|||||||
"ssl_verify": Any(Bool, str),
|
"ssl_verify": Any(Bool, str),
|
||||||
"token": str,
|
"token": str,
|
||||||
"use_https": Bool,
|
"use_https": Bool,
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
**REMOTE_COMMON,
|
**REMOTE_COMMON,
|
||||||
},
|
},
|
||||||
"azure": {
|
"azure": {
|
||||||
@@ -200,12 +208,14 @@ SCHEMA = {
|
|||||||
"exclude_visual_studio_code_credential": Bool,
|
"exclude_visual_studio_code_credential": Bool,
|
||||||
"exclude_shared_token_cache_credential": Bool,
|
"exclude_shared_token_cache_credential": Bool,
|
||||||
"exclude_managed_identity_credential": Bool,
|
"exclude_managed_identity_credential": Bool,
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
**REMOTE_COMMON,
|
**REMOTE_COMMON,
|
||||||
},
|
},
|
||||||
"oss": {
|
"oss": {
|
||||||
"oss_key_id": str,
|
"oss_key_id": str,
|
||||||
"oss_key_secret": str,
|
"oss_key_secret": str,
|
||||||
"oss_endpoint": str,
|
"oss_endpoint": str,
|
||||||
|
Optional("verify", default=True): Bool,
|
||||||
**REMOTE_COMMON,
|
**REMOTE_COMMON,
|
||||||
},
|
},
|
||||||
"gdrive": {
|
"gdrive": {
|
||||||
@@ -216,6 +226,7 @@ SCHEMA = {
|
|||||||
"gdrive_service_account_user_email": str,
|
"gdrive_service_account_user_email": str,
|
||||||
"gdrive_service_account_json_file_path": str,
|
"gdrive_service_account_json_file_path": str,
|
||||||
Optional("gdrive_trash_only", default=False): Bool,
|
Optional("gdrive_trash_only", default=False): Bool,
|
||||||
|
Optional("verify", default=True): Bool,
|
||||||
**REMOTE_COMMON,
|
**REMOTE_COMMON,
|
||||||
},
|
},
|
||||||
"http": {**HTTP_COMMON, **REMOTE_COMMON},
|
"http": {**HTTP_COMMON, **REMOTE_COMMON},
|
||||||
@@ -248,6 +259,7 @@ SCHEMA = {
|
|||||||
"instance_gpu": Lower,
|
"instance_gpu": Lower,
|
||||||
"ssh_private": str,
|
"ssh_private": str,
|
||||||
"startup_script": str,
|
"startup_script": str,
|
||||||
|
"setup_script": str,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# section for experimental features
|
# section for experimental features
|
||||||
@@ -256,7 +268,11 @@ SCHEMA = {
|
|||||||
# enabled by default. It's of no use, kept for backward compatibility.
|
# enabled by default. It's of no use, kept for backward compatibility.
|
||||||
Optional("parametrization", default=True): Bool,
|
Optional("parametrization", default=True): Bool,
|
||||||
},
|
},
|
||||||
"plots": {"html_template": str},
|
"plots": {
|
||||||
|
"html_template": str,
|
||||||
|
Optional("auto_open", default=False): Bool,
|
||||||
|
"out_dir": str,
|
||||||
|
},
|
||||||
"exp": {
|
"exp": {
|
||||||
"code": str,
|
"code": str,
|
||||||
"data": str,
|
"data": str,
|
||||||
@@ -266,4 +282,8 @@ SCHEMA = {
|
|||||||
"plots": str,
|
"plots": str,
|
||||||
"live": str,
|
"live": str,
|
||||||
},
|
},
|
||||||
|
"parsing": {
|
||||||
|
"bool": All(Lower, Choices("store_true", "boolean_optional")),
|
||||||
|
"list": All(Lower, Choices("nargs", "append")),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -42,7 +42,7 @@ def _spawn_windows(cmd, env):
|
|||||||
|
|
||||||
|
|
||||||
def _spawn_posix(cmd, env):
|
def _spawn_posix(cmd, env):
|
||||||
from dvc.main import main
|
from dvc.cli import main
|
||||||
|
|
||||||
# NOTE: using os._exit instead of sys.exit, because dvc built
|
# NOTE: using os._exit instead of sys.exit, because dvc built
|
||||||
# with PyInstaller has trouble with SystemExit exception and throws
|
# with PyInstaller has trouble with SystemExit exception and throws
|
||||||
@@ -81,7 +81,7 @@ def _spawn_posix(cmd, env):
|
|||||||
|
|
||||||
|
|
||||||
def _spawn(cmd, env):
|
def _spawn(cmd, env):
|
||||||
logger.debug(f"Trying to spawn '{cmd}'")
|
logger.debug("Trying to spawn '%s'", cmd)
|
||||||
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
_spawn_windows(cmd, env)
|
_spawn_windows(cmd, env)
|
||||||
@@ -90,7 +90,7 @@ def _spawn(cmd, env):
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
logger.debug(f"Spawned '{cmd}'")
|
logger.debug("Spawned '%s'", cmd)
|
||||||
|
|
||||||
|
|
||||||
def daemon(args):
|
def daemon(args):
|
||||||
@@ -106,8 +106,9 @@ def daemon(args):
|
|||||||
cmd = ["daemon", "-q"] + args
|
cmd = ["daemon", "-q"] + args
|
||||||
|
|
||||||
env = fix_env()
|
env = fix_env()
|
||||||
file_path = os.path.abspath(inspect.stack()[0][1])
|
if not is_binary():
|
||||||
env["PYTHONPATH"] = os.path.dirname(os.path.dirname(file_path))
|
file_path = os.path.abspath(inspect.stack()[0][1])
|
||||||
|
env["PYTHONPATH"] = os.path.dirname(os.path.dirname(file_path))
|
||||||
env[DVC_DAEMON] = "1"
|
env[DVC_DAEMON] = "1"
|
||||||
|
|
||||||
_spawn(cmd, env)
|
_spawn(cmd, env)
|
||||||
|
|||||||
+43
-20
@@ -3,11 +3,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Iterable, Optional
|
from typing import TYPE_CHECKING, Iterable, Optional
|
||||||
|
|
||||||
from dvc.objects.db import get_index
|
from dvc_data.db import get_index
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dvc.hash_info import HashInfo
|
from dvc_data.hashfile.db import HashFileDB
|
||||||
from dvc.objects.db.base import ObjectDB
|
from dvc_data.hashfile.hash_info import HashInfo
|
||||||
|
from dvc_data.status import CompareStatusResult
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ class DataCloud:
|
|||||||
self,
|
self,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
command: str = "<command>",
|
command: str = "<command>",
|
||||||
) -> "ObjectDB":
|
) -> "HashFileDB":
|
||||||
from dvc.config import NoRemoteError
|
from dvc.config import NoRemoteError
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
@@ -56,18 +57,44 @@ class DataCloud:
|
|||||||
|
|
||||||
def _init_odb(self, name):
|
def _init_odb(self, name):
|
||||||
from dvc.fs import get_cloud_fs
|
from dvc.fs import get_cloud_fs
|
||||||
from dvc.objects.db import get_odb
|
from dvc_data.db import get_odb
|
||||||
|
|
||||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||||
config["tmp_dir"] = self.repo.index_db_dir
|
config["tmp_dir"] = self.repo.index_db_dir
|
||||||
return get_odb(cls(**config), fs_path, **config)
|
return get_odb(cls(**config), fs_path, **config)
|
||||||
|
|
||||||
|
def _log_missing(self, status: "CompareStatusResult"):
|
||||||
|
if status.missing:
|
||||||
|
missing_desc = "\n".join(
|
||||||
|
f"name: {hash_info.obj_name}, {hash_info}"
|
||||||
|
for hash_info in status.missing
|
||||||
|
)
|
||||||
|
logger.warning(
|
||||||
|
"Some of the cache files do not exist neither locally "
|
||||||
|
f"nor on remote. Missing cache files:\n{missing_desc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def transfer(
|
||||||
|
self,
|
||||||
|
src_odb: "HashFileDB",
|
||||||
|
dest_odb: "HashFileDB",
|
||||||
|
objs: Iterable["HashInfo"],
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
from dvc.exceptions import FileTransferError
|
||||||
|
from dvc_data.transfer import TransferError, transfer
|
||||||
|
|
||||||
|
try:
|
||||||
|
return transfer(src_odb, dest_odb, objs, **kwargs)
|
||||||
|
except TransferError as exc:
|
||||||
|
raise FileTransferError(exc.fails) from exc
|
||||||
|
|
||||||
def push(
|
def push(
|
||||||
self,
|
self,
|
||||||
objs: Iterable["HashInfo"],
|
objs: Iterable["HashInfo"],
|
||||||
jobs: Optional[int] = None,
|
jobs: Optional[int] = None,
|
||||||
remote: Optional[str] = None,
|
remote: Optional[str] = None,
|
||||||
odb: Optional["ObjectDB"] = None,
|
odb: Optional["HashFileDB"] = None,
|
||||||
):
|
):
|
||||||
"""Push data items in a cloud-agnostic way.
|
"""Push data items in a cloud-agnostic way.
|
||||||
|
|
||||||
@@ -78,17 +105,15 @@ class DataCloud:
|
|||||||
By default remote from core.remote config option is used.
|
By default remote from core.remote config option is used.
|
||||||
odb: optional ODB to push to. Overrides remote.
|
odb: optional ODB to push to. Overrides remote.
|
||||||
"""
|
"""
|
||||||
from dvc.objects.transfer import transfer
|
odb = odb or self.get_remote_odb(remote, "push")
|
||||||
|
return self.transfer(
|
||||||
if not odb:
|
|
||||||
odb = self.get_remote_odb(remote, "push")
|
|
||||||
return transfer(
|
|
||||||
self.repo.odb.local,
|
self.repo.odb.local,
|
||||||
odb,
|
odb,
|
||||||
objs,
|
objs,
|
||||||
jobs=jobs,
|
jobs=jobs,
|
||||||
dest_index=get_index(odb),
|
dest_index=get_index(odb),
|
||||||
cache_odb=self.repo.odb.local,
|
cache_odb=self.repo.odb.local,
|
||||||
|
validate_status=self._log_missing,
|
||||||
)
|
)
|
||||||
|
|
||||||
def pull(
|
def pull(
|
||||||
@@ -96,7 +121,7 @@ class DataCloud:
|
|||||||
objs: Iterable["HashInfo"],
|
objs: Iterable["HashInfo"],
|
||||||
jobs: Optional[int] = None,
|
jobs: Optional[int] = None,
|
||||||
remote: Optional[str] = None,
|
remote: Optional[str] = None,
|
||||||
odb: Optional["ObjectDB"] = None,
|
odb: Optional["HashFileDB"] = None,
|
||||||
):
|
):
|
||||||
"""Pull data items in a cloud-agnostic way.
|
"""Pull data items in a cloud-agnostic way.
|
||||||
|
|
||||||
@@ -107,11 +132,8 @@ class DataCloud:
|
|||||||
By default remote from core.remote config option is used.
|
By default remote from core.remote config option is used.
|
||||||
odb: optional ODB to pull from. Overrides remote.
|
odb: optional ODB to pull from. Overrides remote.
|
||||||
"""
|
"""
|
||||||
from dvc.objects.transfer import transfer
|
odb = odb or self.get_remote_odb(remote, "pull")
|
||||||
|
return self.transfer(
|
||||||
if not odb:
|
|
||||||
odb = self.get_remote_odb(remote, "pull")
|
|
||||||
return transfer(
|
|
||||||
odb,
|
odb,
|
||||||
self.repo.odb.local,
|
self.repo.odb.local,
|
||||||
objs,
|
objs,
|
||||||
@@ -119,6 +141,7 @@ class DataCloud:
|
|||||||
src_index=get_index(odb),
|
src_index=get_index(odb),
|
||||||
cache_odb=self.repo.odb.local,
|
cache_odb=self.repo.odb.local,
|
||||||
verify=odb.verify,
|
verify=odb.verify,
|
||||||
|
validate_status=self._log_missing,
|
||||||
)
|
)
|
||||||
|
|
||||||
def status(
|
def status(
|
||||||
@@ -126,7 +149,7 @@ class DataCloud:
|
|||||||
objs: Iterable["HashInfo"],
|
objs: Iterable["HashInfo"],
|
||||||
jobs: Optional[int] = None,
|
jobs: Optional[int] = None,
|
||||||
remote: Optional[str] = None,
|
remote: Optional[str] = None,
|
||||||
odb: Optional["ObjectDB"] = None,
|
odb: Optional["HashFileDB"] = None,
|
||||||
log_missing: bool = True,
|
log_missing: bool = True,
|
||||||
):
|
):
|
||||||
"""Check status of data items in a cloud-agnostic way.
|
"""Check status of data items in a cloud-agnostic way.
|
||||||
@@ -141,7 +164,7 @@ class DataCloud:
|
|||||||
log_missing: log warning messages if file doesn't exist
|
log_missing: log warning messages if file doesn't exist
|
||||||
neither in cache, neither in cloud.
|
neither in cache, neither in cloud.
|
||||||
"""
|
"""
|
||||||
from dvc.objects.status import compare_status
|
from dvc_data.status import compare_status
|
||||||
|
|
||||||
if not odb:
|
if not odb:
|
||||||
odb = self.get_remote_odb(remote, "status")
|
odb = self.get_remote_odb(remote, "status")
|
||||||
@@ -157,5 +180,5 @@ class DataCloud:
|
|||||||
|
|
||||||
def get_url_for(self, remote, checksum):
|
def get_url_for(self, remote, checksum):
|
||||||
odb = self.get_remote_odb(remote)
|
odb = self.get_remote_odb(remote)
|
||||||
path = odb.hash_to_path(checksum)
|
path = odb.oid_to_path(checksum)
|
||||||
return odb.fs.unstrip_protocol(path)
|
return odb.fs.unstrip_protocol(path)
|
||||||
|
|||||||
@@ -47,16 +47,29 @@ def loads_from(stage, s_list, erepo=None):
|
|||||||
def _merge_params(s_list):
|
def _merge_params(s_list):
|
||||||
d = defaultdict(list)
|
d = defaultdict(list)
|
||||||
default_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
default_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||||
|
|
||||||
|
# figure out completely tracked params file, and ignore specific keys
|
||||||
|
wholly_tracked = set()
|
||||||
|
for key in s_list:
|
||||||
|
if not isinstance(key, dict):
|
||||||
|
continue
|
||||||
|
wholly_tracked.update(k for k, params in key.items() if not params)
|
||||||
|
|
||||||
for key in s_list:
|
for key in s_list:
|
||||||
if isinstance(key, str):
|
if isinstance(key, str):
|
||||||
d[default_file].append(key)
|
if default_file not in wholly_tracked:
|
||||||
|
d[default_file].append(key)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not isinstance(key, dict):
|
if not isinstance(key, dict):
|
||||||
msg = "Only list of str/dict is supported. Got: "
|
msg = "Only list of str/dict is supported. Got: "
|
||||||
msg += f"'{type(key).__name__}'."
|
msg += f"'{type(key).__name__}'."
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
for k, params in key.items():
|
for k, params in key.items():
|
||||||
|
if k in wholly_tracked:
|
||||||
|
d[k] = []
|
||||||
|
continue
|
||||||
if not isinstance(params, list):
|
if not isinstance(params, list):
|
||||||
msg = "Expected list of params for custom params file "
|
msg = "Expected list of params for custom params file "
|
||||||
msg += f"'{k}', got '{type(params).__name__}'."
|
msg += f"'{k}', got '{type(params).__name__}'."
|
||||||
|
|||||||
+78
-59
@@ -1,13 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import typing
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
import dpath.util
|
import dpath.util
|
||||||
from voluptuous import Any
|
from voluptuous import Any
|
||||||
|
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
from dvc.hash_info import HashInfo
|
|
||||||
from dvc.utils.serialize import LOADERS, ParseError
|
from dvc.utils.serialize import LOADERS, ParseError
|
||||||
|
from dvc_data.hashfile.hash_info import HashInfo
|
||||||
|
|
||||||
from .base import Dependency
|
from .base import Dependency
|
||||||
|
|
||||||
@@ -18,6 +20,14 @@ class MissingParamsError(DvcException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingParamsFile(DvcException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ParamsIsADirectoryError(DvcException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BadParamFileError(DvcException):
|
class BadParamFileError(DvcException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -27,51 +37,83 @@ class ParamsDependency(Dependency):
|
|||||||
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
|
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
|
||||||
DEFAULT_PARAMS_FILE = "params.yaml"
|
DEFAULT_PARAMS_FILE = "params.yaml"
|
||||||
|
|
||||||
def __init__(self, stage, path, params):
|
def __init__(self, stage, path, params=None, repo=None):
|
||||||
info = {}
|
self.params = list(params) if params else []
|
||||||
self.params = []
|
info = (
|
||||||
if params:
|
{self.PARAM_PARAMS: params} if isinstance(params, dict) else None
|
||||||
if isinstance(params, list):
|
|
||||||
self.params = params
|
|
||||||
else:
|
|
||||||
assert isinstance(params, dict)
|
|
||||||
self.params = list(params.keys())
|
|
||||||
info = {self.PARAM_PARAMS: params}
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
stage,
|
|
||||||
path
|
|
||||||
or os.path.join(stage.repo.root_dir, self.DEFAULT_PARAMS_FILE),
|
|
||||||
info=info,
|
|
||||||
)
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
def dumpd(self):
|
def dumpd(self):
|
||||||
ret = super().dumpd()
|
ret = super().dumpd()
|
||||||
if not self.hash_info:
|
if not self.hash_info:
|
||||||
ret[self.PARAM_PARAMS] = self.params
|
ret[self.PARAM_PARAMS] = self.params or {}
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def fill_values(self, values=None):
|
def fill_values(self, values=None):
|
||||||
"""Load params values dynamically."""
|
"""Load params values dynamically."""
|
||||||
if not values:
|
if values is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
info = {}
|
info = {}
|
||||||
|
if not self.params:
|
||||||
|
info.update(values)
|
||||||
for param in self.params:
|
for param in self.params:
|
||||||
if param in values:
|
if param in values:
|
||||||
info[param] = values[param]
|
info[param] = values[param]
|
||||||
self.hash_info = HashInfo(self.PARAM_PARAMS, info)
|
self.hash_info = HashInfo(self.PARAM_PARAMS, info)
|
||||||
|
|
||||||
def workspace_status(self):
|
def read_params(
|
||||||
status = super().workspace_status()
|
self, flatten: bool = True, **kwargs: typing.Any
|
||||||
|
) -> Dict[str, typing.Any]:
|
||||||
|
try:
|
||||||
|
config = self.read_file()
|
||||||
|
except MissingParamsFile:
|
||||||
|
config = {}
|
||||||
|
|
||||||
if status.get(str(self)) == "deleted":
|
if not self.params:
|
||||||
return status
|
return config
|
||||||
|
|
||||||
|
ret = {}
|
||||||
|
if flatten:
|
||||||
|
for param in self.params:
|
||||||
|
try:
|
||||||
|
ret[param] = dpath.util.get(config, param, separator=".")
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
return ret
|
||||||
|
|
||||||
|
from dpath.util import merge
|
||||||
|
|
||||||
|
for param in self.params:
|
||||||
|
merge(
|
||||||
|
ret,
|
||||||
|
dpath.util.search(config, param, separator="."),
|
||||||
|
separator=".",
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def workspace_status(self):
|
||||||
|
if not self.exists:
|
||||||
|
return {str(self): "deleted"}
|
||||||
|
if self.hash_info.value is None:
|
||||||
|
return {str(self): "new"}
|
||||||
|
|
||||||
|
from funcy import ldistinct
|
||||||
|
|
||||||
status = defaultdict(dict)
|
status = defaultdict(dict)
|
||||||
info = self.hash_info.value if self.hash_info else {}
|
info = self.hash_info.value if self.hash_info else {}
|
||||||
actual = self.read_params()
|
actual = self.read_params()
|
||||||
for param in self.params:
|
|
||||||
if param not in actual.keys():
|
# NOTE: we want to preserve the order of params as specified in the
|
||||||
|
# status. In case of tracking the whole file, the order is top-level
|
||||||
|
# keys in the file and then the keys in the `info` from `dvc.lock`
|
||||||
|
# (which are alphabetically sorted).
|
||||||
|
params = self.params or ldistinct([*actual.keys(), *info.keys()])
|
||||||
|
for param in params:
|
||||||
|
if param not in actual:
|
||||||
st = "deleted"
|
st = "deleted"
|
||||||
elif param not in info:
|
elif param not in info:
|
||||||
st = "new"
|
st = "new"
|
||||||
@@ -88,12 +130,19 @@ class ParamsDependency(Dependency):
|
|||||||
def status(self):
|
def status(self):
|
||||||
return self.workspace_status()
|
return self.workspace_status()
|
||||||
|
|
||||||
def _read(self):
|
def validate_filepath(self):
|
||||||
if not self.exists:
|
if not self.exists:
|
||||||
return {}
|
raise MissingParamsFile(f"Parameters file '{self}' does not exist")
|
||||||
|
if self.isdir():
|
||||||
|
raise ParamsIsADirectoryError(
|
||||||
|
f"'{self}' is a directory, expected a parameters file"
|
||||||
|
)
|
||||||
|
|
||||||
suffix = self.repo.fs.path.suffix(self.fs_path).lower()
|
def read_file(self):
|
||||||
loader = LOADERS[suffix]
|
_, ext = os.path.splitext(self.fs_path)
|
||||||
|
loader = LOADERS[ext]
|
||||||
|
|
||||||
|
self.validate_filepath()
|
||||||
try:
|
try:
|
||||||
return loader(self.fs_path, fs=self.repo.fs)
|
return loader(self.fs_path, fs=self.repo.fs)
|
||||||
except ParseError as exc:
|
except ParseError as exc:
|
||||||
@@ -101,29 +150,6 @@ class ParamsDependency(Dependency):
|
|||||||
f"Unable to read parameters from '{self}'"
|
f"Unable to read parameters from '{self}'"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
def read_params_d(self, **kwargs):
|
|
||||||
config = self._read()
|
|
||||||
|
|
||||||
ret = {}
|
|
||||||
for param in self.params:
|
|
||||||
dpath.util.merge(
|
|
||||||
ret,
|
|
||||||
dpath.util.search(config, param, separator="."),
|
|
||||||
separator=".",
|
|
||||||
)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def read_params(self):
|
|
||||||
config = self._read()
|
|
||||||
|
|
||||||
ret = {}
|
|
||||||
for param in self.params:
|
|
||||||
try:
|
|
||||||
ret[param] = dpath.util.get(config, param, separator=".")
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def get_hash(self):
|
def get_hash(self):
|
||||||
info = self.read_params()
|
info = self.read_params()
|
||||||
|
|
||||||
@@ -144,12 +170,5 @@ class ParamsDependency(Dependency):
|
|||||||
if not self.isfile and not self.isdir:
|
if not self.isfile and not self.isdir:
|
||||||
raise self.IsNotFileOrDirError(self)
|
raise self.IsNotFileOrDirError(self)
|
||||||
|
|
||||||
if self.is_empty:
|
|
||||||
logger.warning(f"'{self}' is empty.")
|
|
||||||
|
|
||||||
self.ignore()
|
self.ignore()
|
||||||
|
|
||||||
if self.metric or self.plot:
|
|
||||||
self.verify_metric()
|
|
||||||
|
|
||||||
self.hash_info = self.get_hash()
|
self.hash_info = self.get_hash()
|
||||||
|
|||||||
+36
-33
@@ -5,12 +5,14 @@ from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
|
|||||||
|
|
||||||
from voluptuous import Required
|
from voluptuous import Required
|
||||||
|
|
||||||
|
from dvc.prompt import confirm
|
||||||
|
|
||||||
from .base import Dependency
|
from .base import Dependency
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dvc.hash_info import HashInfo
|
from dvc_data.hashfile.file import HashFile
|
||||||
from dvc.objects.db.base import ObjectDB
|
from dvc_data.hashfile.hash_info import HashInfo
|
||||||
from dvc.objects.file import HashFile
|
from dvc_objects.db import ObjectDB
|
||||||
|
|
||||||
|
|
||||||
class RepoDependency(Dependency):
|
class RepoDependency(Dependency):
|
||||||
@@ -29,7 +31,7 @@ class RepoDependency(Dependency):
|
|||||||
|
|
||||||
def __init__(self, def_repo, stage, *args, **kwargs):
|
def __init__(self, def_repo, stage, *args, **kwargs):
|
||||||
self.def_repo = def_repo
|
self.def_repo = def_repo
|
||||||
self._staged_objs: Dict[str, "HashFile"] = {}
|
self._objs: Dict[str, "HashFile"] = {}
|
||||||
super().__init__(stage, *args, **kwargs)
|
super().__init__(stage, *args, **kwargs)
|
||||||
|
|
||||||
def _parse_path(self, fs, fs_path):
|
def _parse_path(self, fs, fs_path):
|
||||||
@@ -61,7 +63,7 @@ class RepoDependency(Dependency):
|
|||||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||||
|
|
||||||
def download(self, to, jobs=None):
|
def download(self, to, jobs=None):
|
||||||
from dvc.objects.checkout import checkout
|
from dvc_data.checkout import checkout
|
||||||
|
|
||||||
for odb, objs in self.get_used_objs().items():
|
for odb, objs in self.get_used_objs().items():
|
||||||
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
||||||
@@ -72,8 +74,9 @@ class RepoDependency(Dependency):
|
|||||||
to.fs,
|
to.fs,
|
||||||
obj,
|
obj,
|
||||||
self.repo.odb.local,
|
self.repo.odb.local,
|
||||||
dvcignore=None,
|
ignore=None,
|
||||||
state=self.repo.state,
|
state=self.repo.state,
|
||||||
|
prompt=confirm,
|
||||||
)
|
)
|
||||||
|
|
||||||
def update(self, rev=None):
|
def update(self, rev=None):
|
||||||
@@ -99,8 +102,9 @@ class RepoDependency(Dependency):
|
|||||||
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "HashFile"]:
|
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "HashFile"]:
|
||||||
from dvc.config import NoRemoteError
|
from dvc.config import NoRemoteError
|
||||||
from dvc.exceptions import NoOutputOrStageError, PathMissingError
|
from dvc.exceptions import NoOutputOrStageError, PathMissingError
|
||||||
from dvc.objects.stage import stage
|
from dvc.utils import as_posix
|
||||||
from dvc.objects.tree import Tree
|
from dvc_data.build import build
|
||||||
|
from dvc_data.objects.tree import Tree, TreeError
|
||||||
|
|
||||||
local_odb = self.repo.odb.local
|
local_odb = self.repo.odb.local
|
||||||
locked = kwargs.pop("locked", True)
|
locked = kwargs.pop("locked", True)
|
||||||
@@ -112,11 +116,10 @@ class RepoDependency(Dependency):
|
|||||||
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||||
self.def_repo[self.PARAM_REV_LOCK] = rev
|
self.def_repo[self.PARAM_REV_LOCK] = rev
|
||||||
|
|
||||||
path = os.path.abspath(os.path.join(repo.root_dir, self.def_path))
|
|
||||||
if not obj_only:
|
if not obj_only:
|
||||||
try:
|
try:
|
||||||
for odb, obj_ids in repo.used_objs(
|
for odb, obj_ids in repo.used_objs(
|
||||||
[path],
|
[os.path.join(repo.root_dir, self.def_path)],
|
||||||
force=True,
|
force=True,
|
||||||
jobs=kwargs.get("jobs"),
|
jobs=kwargs.get("jobs"),
|
||||||
recursive=True,
|
recursive=True,
|
||||||
@@ -130,65 +133,65 @@ class RepoDependency(Dependency):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
staging, _, staged_obj = stage(
|
object_store, _, obj = build(
|
||||||
local_odb,
|
local_odb,
|
||||||
path,
|
as_posix(self.def_path),
|
||||||
repo.repo_fs,
|
repo.dvcfs,
|
||||||
local_odb.fs.PARAM_CHECKSUM,
|
local_odb.fs.PARAM_CHECKSUM,
|
||||||
)
|
)
|
||||||
except FileNotFoundError as exc:
|
except (FileNotFoundError, TreeError) as exc:
|
||||||
raise PathMissingError(
|
raise PathMissingError(
|
||||||
self.def_path, self.def_repo[self.PARAM_URL]
|
self.def_path, self.def_repo[self.PARAM_URL]
|
||||||
) from exc
|
) from exc
|
||||||
staging = copy(staging)
|
object_store = copy(object_store)
|
||||||
staging.read_only = True
|
object_store.read_only = True
|
||||||
|
|
||||||
self._staged_objs[rev] = staged_obj
|
self._objs[rev] = obj
|
||||||
used_obj_ids[staging].add(staged_obj.hash_info)
|
used_obj_ids[object_store].add(obj.hash_info)
|
||||||
if isinstance(staged_obj, Tree):
|
if isinstance(obj, Tree):
|
||||||
used_obj_ids[staging].update(oid for _, _, oid in staged_obj)
|
used_obj_ids[object_store].update(oid for _, _, oid in obj)
|
||||||
return used_obj_ids, staged_obj
|
return used_obj_ids, obj
|
||||||
|
|
||||||
def _check_circular_import(self, odb, obj_ids):
|
def _check_circular_import(self, odb, obj_ids):
|
||||||
from dvc.exceptions import CircularImportError
|
from dvc.exceptions import CircularImportError
|
||||||
from dvc.fs.repo import RepoFileSystem
|
from dvc.fs.dvc import DvcFileSystem
|
||||||
from dvc.objects.db.reference import ReferenceObjectDB
|
from dvc_data.db.reference import ReferenceHashFileDB
|
||||||
from dvc.objects.tree import Tree
|
from dvc_data.objects.tree import Tree
|
||||||
|
|
||||||
if not isinstance(odb, ReferenceObjectDB):
|
if not isinstance(odb, ReferenceHashFileDB):
|
||||||
return
|
return
|
||||||
|
|
||||||
def iter_objs():
|
def iter_objs():
|
||||||
for hash_info in obj_ids:
|
for hash_info in obj_ids:
|
||||||
if hash_info.isdir:
|
if hash_info.isdir:
|
||||||
tree = Tree.load(odb, hash_info)
|
tree = Tree.load(odb, hash_info)
|
||||||
yield from (odb.get(oid) for _, _, oid in tree)
|
yield from (odb.get(hi.value) for _, _, hi in tree)
|
||||||
else:
|
else:
|
||||||
yield odb.get(hash_info)
|
yield odb.get(hash_info.value)
|
||||||
|
|
||||||
checked_urls = set()
|
checked_urls = set()
|
||||||
for obj in iter_objs():
|
for obj in iter_objs():
|
||||||
if not isinstance(obj.fs, RepoFileSystem):
|
if not isinstance(obj.fs, DvcFileSystem):
|
||||||
continue
|
continue
|
||||||
if (
|
if (
|
||||||
obj.fs.repo_url in checked_urls
|
obj.fs.repo_url in checked_urls
|
||||||
or obj.fs.root_dir in checked_urls
|
or obj.fs.repo.root_dir in checked_urls
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
self_url = self.repo.url or self.repo.root_dir
|
self_url = self.repo.url or self.repo.root_dir
|
||||||
if (
|
if (
|
||||||
obj.fs.repo_url is not None
|
obj.fs.repo_url is not None
|
||||||
and obj.fs.repo_url == self_url
|
and obj.fs.repo_url == self_url
|
||||||
or obj.fs.root_dir == self.repo.root_dir
|
or obj.fs.repo.root_dir == self.repo.root_dir
|
||||||
):
|
):
|
||||||
raise CircularImportError(self, obj.fs.repo_url, self_url)
|
raise CircularImportError(self, obj.fs.repo_url, self_url)
|
||||||
checked_urls.update([obj.fs.repo_url, obj.fs.root_dir])
|
checked_urls.update([obj.fs.repo_url, obj.fs.repo.root_dir])
|
||||||
|
|
||||||
def get_obj(self, filter_info=None, **kwargs):
|
def get_obj(self, filter_info=None, **kwargs):
|
||||||
locked = kwargs.get("locked", True)
|
locked = kwargs.get("locked", True)
|
||||||
rev = self._get_rev(locked=locked)
|
rev = self._get_rev(locked=locked)
|
||||||
if rev in self._staged_objs:
|
if rev in self._objs:
|
||||||
return self._staged_objs[rev]
|
return self._objs[rev]
|
||||||
_, obj = self._get_used_and_obj(
|
_, obj = self._get_used_and_obj(
|
||||||
obj_only=True, filter_info=filter_info, **kwargs
|
obj_only=True, filter_info=filter_info, **kwargs
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -59,7 +59,7 @@ def is_lock_file(path):
|
|||||||
|
|
||||||
|
|
||||||
def is_git_ignored(repo, path):
|
def is_git_ignored(repo, path):
|
||||||
from dvc.fs.local import LocalFileSystem
|
from dvc.fs import LocalFileSystem
|
||||||
from dvc.scm import NoSCMError
|
from dvc.scm import NoSCMError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -197,7 +197,7 @@ class SingleStageFile(FileMixin):
|
|||||||
assert not isinstance(stage, PipelineStage)
|
assert not isinstance(stage, PipelineStage)
|
||||||
if self.verify:
|
if self.verify:
|
||||||
check_dvcfile_path(self.repo, self.path)
|
check_dvcfile_path(self.repo, self.path)
|
||||||
logger.debug(f"Saving information to '{relpath(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))
|
||||||
self.repo.scm_context.track_file(self.relpath)
|
self.repo.scm_context.track_file(self.relpath)
|
||||||
|
|
||||||
|
|||||||
+4
-20
@@ -1,4 +1,5 @@
|
|||||||
"""Exceptions raised by the dvc."""
|
"""Exceptions raised by the dvc."""
|
||||||
|
from dvc.utils import format_link
|
||||||
|
|
||||||
|
|
||||||
class DvcException(Exception):
|
class DvcException(Exception):
|
||||||
@@ -128,13 +129,6 @@ class NotDvcRepoError(DvcException):
|
|||||||
"""Thrown if a directory is not a DVC repo"""
|
"""Thrown if a directory is not a DVC repo"""
|
||||||
|
|
||||||
|
|
||||||
class DvcParserError(DvcException):
|
|
||||||
"""Base class for CLI parser errors."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__("parser error")
|
|
||||||
|
|
||||||
|
|
||||||
class CyclicGraphError(DvcException):
|
class CyclicGraphError(DvcException):
|
||||||
def __init__(self, stages):
|
def __init__(self, stages):
|
||||||
assert isinstance(stages, list)
|
assert isinstance(stages, list)
|
||||||
@@ -157,9 +151,9 @@ class InitError(DvcException):
|
|||||||
|
|
||||||
|
|
||||||
class ReproductionError(DvcException):
|
class ReproductionError(DvcException):
|
||||||
def __init__(self, dvc_file_name):
|
def __init__(self, name):
|
||||||
self.path = dvc_file_name
|
self.name = name
|
||||||
super().__init__(f"failed to reproduce '{dvc_file_name}'")
|
super().__init__(f"failed to reproduce '{name}'")
|
||||||
|
|
||||||
|
|
||||||
class BadMetricError(DvcException):
|
class BadMetricError(DvcException):
|
||||||
@@ -208,14 +202,6 @@ class FileMissingError(DvcException):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DvcIgnoreInCollectedDirError(DvcException):
|
|
||||||
def __init__(self, ignore_dirname):
|
|
||||||
super().__init__(
|
|
||||||
".dvcignore file should not be in collected dir path: "
|
|
||||||
"'{}'".format(ignore_dirname)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FileTransferError(DvcException):
|
class FileTransferError(DvcException):
|
||||||
_METHOD = "transfer"
|
_METHOD = "transfer"
|
||||||
|
|
||||||
@@ -294,7 +280,6 @@ class PathMissingError(DvcException):
|
|||||||
|
|
||||||
class RemoteCacheRequiredError(DvcException):
|
class RemoteCacheRequiredError(DvcException):
|
||||||
def __init__(self, scheme, fs_path):
|
def __init__(self, scheme, fs_path):
|
||||||
from dvc.utils import format_link
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
(
|
(
|
||||||
@@ -330,7 +315,6 @@ class MergeError(DvcException):
|
|||||||
|
|
||||||
|
|
||||||
class CacheLinkError(DvcException):
|
class CacheLinkError(DvcException):
|
||||||
from dvc.utils import format_link
|
|
||||||
|
|
||||||
SUPPORT_LINK = "See {} for more information.".format(
|
SUPPORT_LINK = "See {} for more information.".format(
|
||||||
format_link(
|
format_link(
|
||||||
|
|||||||
+53
-27
@@ -3,7 +3,7 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Dict
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
from funcy import retry, wrap_with
|
from funcy import retry, wrap_with
|
||||||
|
|
||||||
@@ -19,6 +19,9 @@ from dvc.repo import Repo
|
|||||||
from dvc.scm import CloneError, map_scm_exception
|
from dvc.scm import CloneError, map_scm_exception
|
||||||
from dvc.utils import relpath
|
from dvc.utils import relpath
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from scmrepo import Git
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -30,6 +33,7 @@ def external_repo(
|
|||||||
from scmrepo.git import Git
|
from scmrepo.git import Git
|
||||||
|
|
||||||
from dvc.config import NoRemoteError
|
from dvc.config import NoRemoteError
|
||||||
|
from dvc.fs import GitFileSystem
|
||||||
|
|
||||||
logger.debug("Creating external repo %s@%s", url, rev)
|
logger.debug("Creating external repo %s@%s", url, rev)
|
||||||
path = _cached_clone(url, rev, for_write=for_write)
|
path = _cached_clone(url, rev, for_write=for_write)
|
||||||
@@ -45,14 +49,22 @@ def external_repo(
|
|||||||
config = _get_remote_config(url) if os.path.isdir(url) else {}
|
config = _get_remote_config(url) if os.path.isdir(url) else {}
|
||||||
config.update(cache_config)
|
config.update(cache_config)
|
||||||
|
|
||||||
root_dir = path if for_write else os.path.realpath(path)
|
if for_write:
|
||||||
|
scm = None
|
||||||
|
root_dir = path
|
||||||
|
fs = None
|
||||||
|
else:
|
||||||
|
scm = Git(path)
|
||||||
|
fs = GitFileSystem(scm=scm, rev=rev)
|
||||||
|
root_dir = "/"
|
||||||
|
|
||||||
repo_kwargs = dict(
|
repo_kwargs = dict(
|
||||||
root_dir=root_dir,
|
root_dir=root_dir,
|
||||||
url=url,
|
url=url,
|
||||||
scm=None if for_write else Git(root_dir),
|
fs=fs,
|
||||||
rev=None if for_write else rev,
|
|
||||||
config=config,
|
config=config,
|
||||||
repo_factory=erepo_factory(url, cache_config),
|
repo_factory=erepo_factory(url, root_dir, cache_config),
|
||||||
|
scm=scm,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,14 +94,16 @@ def external_repo(
|
|||||||
_remove(path)
|
_remove(path)
|
||||||
|
|
||||||
|
|
||||||
def erepo_factory(url, cache_config, *args, **kwargs):
|
def erepo_factory(url, root_dir, cache_config):
|
||||||
def make_repo(path, **_kwargs):
|
from dvc.fs import localfs
|
||||||
|
|
||||||
|
def make_repo(path, fs=None, **_kwargs):
|
||||||
_config = cache_config.copy()
|
_config = cache_config.copy()
|
||||||
if os.path.isdir(url):
|
if os.path.isdir(url):
|
||||||
rel = os.path.relpath(path, _kwargs["scm"].root_dir)
|
fs = fs or localfs
|
||||||
repo_path = os.path.join(url, rel)
|
repo_path = os.path.join(url, *fs.path.relparts(path, root_dir))
|
||||||
_config.update(_get_remote_config(repo_path))
|
_config.update(_get_remote_config(repo_path))
|
||||||
return Repo(path, config=_config, **_kwargs)
|
return Repo(path, fs=fs, config=_config, **_kwargs)
|
||||||
|
|
||||||
return make_repo
|
return make_repo
|
||||||
|
|
||||||
@@ -194,12 +208,12 @@ def _clone_default_branch(url, rev, for_write=False):
|
|||||||
# given repo URL it is easier/safer for us to work with
|
# given repo URL it is easier/safer for us to work with
|
||||||
# full clones in this case.
|
# full clones in this case.
|
||||||
logger.debug("erepo: unshallowing clone for '%s'", url)
|
logger.debug("erepo: unshallowing clone for '%s'", url)
|
||||||
_unshallow(git)
|
_pull(git, unshallow=True)
|
||||||
shallow = False
|
shallow = False
|
||||||
CLONES[url] = (clone_path, shallow)
|
CLONES[url] = (clone_path, shallow)
|
||||||
else:
|
else:
|
||||||
logger.debug("erepo: git pull '%s'", url)
|
logger.debug("erepo: git pull '%s'", url)
|
||||||
git.pull()
|
_pull(git)
|
||||||
else:
|
else:
|
||||||
from dvc.scm import clone
|
from dvc.scm import clone
|
||||||
|
|
||||||
@@ -210,12 +224,17 @@ def _clone_default_branch(url, rev, for_write=False):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
git = clone(url, clone_path, shallow_branch=rev)
|
git = clone(url, clone_path, shallow_branch=rev)
|
||||||
shallow = True
|
shallow = os.path.exists(
|
||||||
logger.debug(
|
os.path.join(clone_path, Git.GIT_DIR, "shallow")
|
||||||
"erepo: using shallow clone for branch '%s'", rev
|
|
||||||
)
|
)
|
||||||
|
if shallow:
|
||||||
|
logger.debug(
|
||||||
|
"erepo: using shallow clone for branch '%s'", rev
|
||||||
|
)
|
||||||
except CloneError:
|
except CloneError:
|
||||||
pass
|
git_dir = os.path.join(clone_path, ".git")
|
||||||
|
if os.path.exists(git_dir):
|
||||||
|
_remove(git_dir)
|
||||||
if not git:
|
if not git:
|
||||||
git = clone(url, clone_path)
|
git = clone(url, clone_path)
|
||||||
shallow = False
|
shallow = False
|
||||||
@@ -227,17 +246,24 @@ def _clone_default_branch(url, rev, for_write=False):
|
|||||||
return clone_path, shallow
|
return clone_path, shallow
|
||||||
|
|
||||||
|
|
||||||
def _unshallow(git):
|
def _pull(git: "Git", unshallow: bool = False):
|
||||||
if git.gitpython.repo.head.is_detached:
|
from dvc.repo.experiments.utils import fetch_all_exps
|
||||||
# If this is a detached head (i.e. we shallow cloned a tag) switch to
|
|
||||||
# the default branch
|
git.fetch(unshallow=unshallow)
|
||||||
origin_refs = git.gitpython.repo.remotes["origin"].refs
|
_merge_upstream(git)
|
||||||
ref = origin_refs["HEAD"].reference
|
fetch_all_exps(git, "origin")
|
||||||
branch_name = ref.name.split("/")[-1]
|
|
||||||
branch = git.gitpython.repo.create_head(branch_name, ref)
|
|
||||||
branch.set_tracking_branch(ref)
|
def _merge_upstream(git: "Git"):
|
||||||
branch.checkout()
|
from scmrepo.exceptions import SCMError
|
||||||
git.pull(unshallow=True)
|
|
||||||
|
try:
|
||||||
|
branch = git.active_branch()
|
||||||
|
upstream = f"refs/remotes/origin/{branch}"
|
||||||
|
if git.get_ref(upstream):
|
||||||
|
git.merge(upstream)
|
||||||
|
except SCMError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _git_checkout(repo_path, rev):
|
def _git_checkout(repo_path, rev):
|
||||||
|
|||||||
+48
-42
@@ -1,41 +1,47 @@
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from ..scheme import Schemes
|
# pylint: disable=unused-import
|
||||||
from . import utils # noqa: F401
|
from dvc_objects.fs import utils # noqa: F401
|
||||||
from .azure import AzureFileSystem
|
from dvc_objects.fs import ( # noqa: F401
|
||||||
from .gdrive import GDriveFileSystem
|
FS_MAP,
|
||||||
from .gs import GSFileSystem
|
AzureFileSystem,
|
||||||
from .hdfs import HDFSFileSystem
|
GDriveFileSystem,
|
||||||
from .http import HTTPFileSystem
|
GSFileSystem,
|
||||||
from .https import HTTPSFileSystem
|
HDFSFileSystem,
|
||||||
from .local import LocalFileSystem
|
HTTPFileSystem,
|
||||||
from .oss import OSSFileSystem
|
HTTPSFileSystem,
|
||||||
from .s3 import S3FileSystem
|
LocalFileSystem,
|
||||||
from .ssh import SSHFileSystem
|
MemoryFileSystem,
|
||||||
from .webdav import WebDAVFileSystem, WebDAVSFileSystem
|
OSSFileSystem,
|
||||||
from .webhdfs import WebHDFSFileSystem
|
S3FileSystem,
|
||||||
|
Schemes,
|
||||||
|
SSHFileSystem,
|
||||||
|
WebDAVFileSystem,
|
||||||
|
WebDAVSFileSystem,
|
||||||
|
WebHDFSFileSystem,
|
||||||
|
generic,
|
||||||
|
get_fs_cls,
|
||||||
|
system,
|
||||||
|
)
|
||||||
|
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401
|
||||||
|
from dvc_objects.fs.errors import ( # noqa: F401
|
||||||
|
AuthError,
|
||||||
|
ConfigError,
|
||||||
|
RemoteMissingDepsError,
|
||||||
|
)
|
||||||
|
from dvc_objects.fs.implementations.azure import AzureAuthError # noqa: F401
|
||||||
|
from dvc_objects.fs.implementations.gdrive import GDriveAuthError # 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
|
||||||
|
|
||||||
FS_MAP = {
|
from .data import DataFileSystem # noqa: F401
|
||||||
Schemes.AZURE: AzureFileSystem,
|
from .dvc import DvcFileSystem # noqa: F401
|
||||||
Schemes.GDRIVE: GDriveFileSystem,
|
from .git import GitFileSystem # noqa: F401
|
||||||
Schemes.GS: GSFileSystem,
|
|
||||||
Schemes.HDFS: HDFSFileSystem,
|
|
||||||
Schemes.WEBHDFS: WebHDFSFileSystem,
|
|
||||||
Schemes.HTTP: HTTPFileSystem,
|
|
||||||
Schemes.HTTPS: HTTPSFileSystem,
|
|
||||||
Schemes.S3: S3FileSystem,
|
|
||||||
Schemes.SSH: SSHFileSystem,
|
|
||||||
Schemes.OSS: OSSFileSystem,
|
|
||||||
Schemes.WEBDAV: WebDAVFileSystem,
|
|
||||||
Schemes.WEBDAVS: WebDAVSFileSystem,
|
|
||||||
# NOTE: LocalFileSystem is the default
|
|
||||||
}
|
|
||||||
|
|
||||||
|
# pylint: enable=unused-import
|
||||||
def get_fs_cls(remote_conf, scheme=None):
|
|
||||||
if not scheme:
|
|
||||||
scheme = urlparse(remote_conf["url"]).scheme
|
|
||||||
return FS_MAP.get(scheme, LocalFileSystem)
|
|
||||||
|
|
||||||
|
|
||||||
def get_fs_config(repo, config, **kwargs):
|
def get_fs_config(repo, config, **kwargs):
|
||||||
@@ -86,7 +92,7 @@ def _resolve_remote_refs(repo, config, remote_conf):
|
|||||||
|
|
||||||
|
|
||||||
def get_cloud_fs(repo, **kwargs):
|
def get_cloud_fs(repo, **kwargs):
|
||||||
from dvc.config import ConfigError
|
from dvc.config import ConfigError as RepoConfigError
|
||||||
from dvc.config_schema import SCHEMA, Invalid
|
from dvc.config_schema import SCHEMA, Invalid
|
||||||
|
|
||||||
repo_config = repo.config if repo else {}
|
repo_config = repo.config if repo else {}
|
||||||
@@ -96,12 +102,7 @@ def get_cloud_fs(repo, **kwargs):
|
|||||||
try:
|
try:
|
||||||
remote_conf = SCHEMA["remote"][str](remote_conf)
|
remote_conf = SCHEMA["remote"][str](remote_conf)
|
||||||
except Invalid as exc:
|
except Invalid as exc:
|
||||||
raise ConfigError(str(exc)) from None
|
raise RepoConfigError(str(exc)) from None
|
||||||
|
|
||||||
if "jobs" not in remote_conf:
|
|
||||||
jobs = core_config.get("jobs")
|
|
||||||
if jobs:
|
|
||||||
remote_conf["jobs"] = jobs
|
|
||||||
|
|
||||||
if "checksum_jobs" not in remote_conf:
|
if "checksum_jobs" not in remote_conf:
|
||||||
checksum_jobs = core_config.get("checksum_jobs")
|
checksum_jobs = core_config.get("checksum_jobs")
|
||||||
@@ -114,7 +115,12 @@ def get_cloud_fs(repo, **kwargs):
|
|||||||
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
|
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
|
||||||
|
|
||||||
url = remote_conf.pop("url")
|
url = remote_conf.pop("url")
|
||||||
fs_path = cls._strip_protocol(url) # pylint:disable=protected-access
|
if issubclass(cls, WebDAVFileSystem):
|
||||||
|
# For WebDAVFileSystem, provided url is the base path itself, so it
|
||||||
|
# should be treated as being a root path.
|
||||||
|
fs_path = cls.root_marker
|
||||||
|
else:
|
||||||
|
fs_path = cls._strip_protocol(url) # pylint:disable=protected-access
|
||||||
|
|
||||||
extras = cls._get_kwargs_from_urls(url) # pylint:disable=protected-access
|
extras = cls._get_kwargs_from_urls(url) # pylint:disable=protected-access
|
||||||
conf = {**extras, **remote_conf} # remote config takes priority
|
conf = {**extras, **remote_conf} # remote config takes priority
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import TYPE_CHECKING, List
|
|
||||||
|
|
||||||
from dvc.output import Output
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from dvc.repo import Repo
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Metadata:
|
|
||||||
"""
|
|
||||||
Container for storing metadata for a given path, similar in spirit to
|
|
||||||
`os.stat_result`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# required field
|
|
||||||
fs_path: str
|
|
||||||
repo: "Repo"
|
|
||||||
|
|
||||||
# computed fields
|
|
||||||
is_output: bool = field(init=False, default=False) # is it an output?
|
|
||||||
# is the path part of an output?
|
|
||||||
part_of_output: bool = field(init=False, default=False)
|
|
||||||
# does the path contain outputs?
|
|
||||||
contains_outputs: bool = field(init=False, default=False)
|
|
||||||
# is the path tracked by dvc? equivalent to (is_output or part_of_output)
|
|
||||||
is_dvc: bool = field(init=False, default=False)
|
|
||||||
# does dvc has any output in that path or inside it? equiv. to bool(outs)
|
|
||||||
output_exists: bool = field(init=False)
|
|
||||||
|
|
||||||
# optional fields
|
|
||||||
isdir: bool = False # is it a directory?
|
|
||||||
is_exec: bool = False # is it an executable?
|
|
||||||
outs: List[Output] = field(default_factory=list) # list of outputs
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
self.output_exists = bool(self.outs)
|
|
||||||
|
|
||||||
if not self.output_exists:
|
|
||||||
return
|
|
||||||
|
|
||||||
# it can contain multiple outputs
|
|
||||||
if len(self.outs) > 1:
|
|
||||||
self.contains_outputs = True
|
|
||||||
else:
|
|
||||||
out = self.outs[0]
|
|
||||||
# or, the path itself could be an output
|
|
||||||
self.is_output = self.fs_path == out.fs_path
|
|
||||||
|
|
||||||
# or, a directory must have an output somewhere deep inside it
|
|
||||||
if not self.is_output:
|
|
||||||
self.contains_outputs = self.repo.fs.path.isin(
|
|
||||||
out.fs_path, self.fs_path
|
|
||||||
)
|
|
||||||
|
|
||||||
# or, the path could be a part of an output, i.e. inside of an output
|
|
||||||
self.part_of_output = not self.is_output and not self.contains_outputs
|
|
||||||
|
|
||||||
self.is_dvc = self.is_output or self.part_of_output
|
|
||||||
# if it contains outputs, it must be a dir
|
|
||||||
self.isdir = self.contains_outputs
|
|
||||||
|
|
||||||
@property
|
|
||||||
def isfile(self):
|
|
||||||
# self.isdir might require additional computation, especially if it's
|
|
||||||
# a dir that's inside a dvc-tracked dir
|
|
||||||
# since, `Metadata()` does not support being used as null (i.e. when
|
|
||||||
# path does not exist), `isfile` becomes `isdir`'s complement.
|
|
||||||
return not self.isdir
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Metadata: {self.fs_path}"
|
|
||||||
-160
@@ -1,160 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from fsspec.asyn import fsspec_loop
|
|
||||||
from fsspec.utils import infer_storage_options
|
|
||||||
from funcy import cached_property, memoize, wrap_prop
|
|
||||||
|
|
||||||
from dvc.exceptions import DvcException
|
|
||||||
from dvc.scheme import Schemes
|
|
||||||
from dvc.utils import format_link
|
|
||||||
|
|
||||||
from .fsspec_wrapper import CallbackMixin, ObjectFSWrapper
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
_DEFAULT_CREDS_STEPS = (
|
|
||||||
"https://azuresdkdocs.blob.core.windows.net/$web/python/"
|
|
||||||
"azure-identity/1.4.0/azure.identity.html#azure.identity"
|
|
||||||
".DefaultAzureCredential"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AzureAuthError(DvcException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@memoize
|
|
||||||
def _az_config():
|
|
||||||
# NOTE: ideally we would've used get_default_cli().config from
|
|
||||||
# azure.cli.core, but azure-cli-core has a lot of conflicts with other
|
|
||||||
# dependencies. So instead we are just use knack directly
|
|
||||||
from knack.config import CLIConfig
|
|
||||||
|
|
||||||
config_dir = os.getenv(
|
|
||||||
"AZURE_CONFIG_DIR", os.path.expanduser(os.path.join("~", ".azure"))
|
|
||||||
)
|
|
||||||
return CLIConfig(config_dir=config_dir, config_env_var_prefix="AZURE")
|
|
||||||
|
|
||||||
|
|
||||||
# pylint:disable=abstract-method
|
|
||||||
class AzureFileSystem(CallbackMixin, ObjectFSWrapper):
|
|
||||||
scheme = Schemes.AZURE
|
|
||||||
PARAM_CHECKSUM = "etag"
|
|
||||||
REQUIRES = {
|
|
||||||
"adlfs": "adlfs",
|
|
||||||
"knack": "knack",
|
|
||||||
"azure-identity": "azure.identity",
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _strip_protocol(cls, path: str):
|
|
||||||
opts = infer_storage_options(path)
|
|
||||||
if opts.get("host"):
|
|
||||||
return "{host}{path}".format(**opts)
|
|
||||||
|
|
||||||
return _az_config().get("storage", "container_name", None)
|
|
||||||
|
|
||||||
def unstrip_protocol(self, path: str) -> str:
|
|
||||||
return "azure://" + path.lstrip("/")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_kwargs_from_urls(urlpath):
|
|
||||||
ops = infer_storage_options(urlpath)
|
|
||||||
if "host" in ops:
|
|
||||||
return {"bucket": ops["host"]}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _prepare_credentials(self, **config):
|
|
||||||
from azure.identity.aio import DefaultAzureCredential
|
|
||||||
|
|
||||||
# Disable spam from failed cred types for DefaultAzureCredential
|
|
||||||
logging.getLogger("azure.identity.aio").setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
login_info = {}
|
|
||||||
login_info["connection_string"] = config.get(
|
|
||||||
"connection_string",
|
|
||||||
_az_config().get("storage", "connection_string", None),
|
|
||||||
)
|
|
||||||
login_info["account_name"] = config.get(
|
|
||||||
"account_name", _az_config().get("storage", "account", None)
|
|
||||||
)
|
|
||||||
login_info["account_key"] = config.get(
|
|
||||||
"account_key", _az_config().get("storage", "key", None)
|
|
||||||
)
|
|
||||||
login_info["sas_token"] = config.get(
|
|
||||||
"sas_token", _az_config().get("storage", "sas_token", None)
|
|
||||||
)
|
|
||||||
login_info["tenant_id"] = config.get("tenant_id")
|
|
||||||
login_info["client_id"] = config.get("client_id")
|
|
||||||
login_info["client_secret"] = config.get("client_secret")
|
|
||||||
|
|
||||||
if not (login_info["account_name"] or login_info["connection_string"]):
|
|
||||||
raise AzureAuthError(
|
|
||||||
"Authentication to Azure Blob Storage requires either "
|
|
||||||
"account_name or connection_string.\nLearn more about "
|
|
||||||
"configuration settings at "
|
|
||||||
+ format_link("https://man.dvc.org/remote/modify")
|
|
||||||
)
|
|
||||||
|
|
||||||
any_secondary = any(
|
|
||||||
value for key, value in login_info.items() if key != "account_name"
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
login_info["account_name"]
|
|
||||||
and not any_secondary
|
|
||||||
and not config.get("allow_anonymous_login", False)
|
|
||||||
):
|
|
||||||
with fsspec_loop():
|
|
||||||
login_info["credential"] = DefaultAzureCredential(
|
|
||||||
exclude_interactive_browser_credential=False,
|
|
||||||
exclude_environment_credential=config.get(
|
|
||||||
"exclude_environment_credential", False
|
|
||||||
),
|
|
||||||
exclude_visual_studio_code_credential=config.get(
|
|
||||||
"exclude_visual_studio_code_credential", False
|
|
||||||
),
|
|
||||||
exclude_shared_token_cache_credential=config.get(
|
|
||||||
"exclude_shared_token_cache_credential", False
|
|
||||||
),
|
|
||||||
exclude_managed_identity_credential=config.get(
|
|
||||||
"exclude_managed_identity_credential", False
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for login_method, required_keys in [ # noqa
|
|
||||||
("connection string", ["connection_string"]),
|
|
||||||
(
|
|
||||||
"AD service principal",
|
|
||||||
["tenant_id", "client_id", "client_secret"],
|
|
||||||
),
|
|
||||||
("account key", ["account_name", "account_key"]),
|
|
||||||
("SAS token", ["account_name", "sas_token"]),
|
|
||||||
(
|
|
||||||
f"default credentials ({_DEFAULT_CREDS_STEPS})",
|
|
||||||
["account_name", "credential"],
|
|
||||||
),
|
|
||||||
("anonymous login", ["account_name"]),
|
|
||||||
]:
|
|
||||||
if all(login_info.get(key) is not None for key in required_keys):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
login_method = None
|
|
||||||
|
|
||||||
self.login_method = login_method
|
|
||||||
return login_info
|
|
||||||
|
|
||||||
@wrap_prop(threading.Lock())
|
|
||||||
@cached_property
|
|
||||||
def fs(self):
|
|
||||||
from adlfs import AzureBlobFileSystem
|
|
||||||
from azure.core.exceptions import AzureError
|
|
||||||
|
|
||||||
try:
|
|
||||||
return AzureBlobFileSystem(**self.fs_args)
|
|
||||||
except (ValueError, AzureError) as e:
|
|
||||||
raise AzureAuthError(
|
|
||||||
f"Authentication to Azure Blob Storage via {self.login_method}"
|
|
||||||
" failed.\nLearn more about configuration settings at"
|
|
||||||
f" {format_link('https://man.dvc.org/remote/modify')}"
|
|
||||||
) from e
|
|
||||||
-376
@@ -1,376 +0,0 @@
|
|||||||
import contextlib
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
from functools import partialmethod
|
|
||||||
from multiprocessing import cpu_count
|
|
||||||
from typing import ClassVar, Dict, Optional
|
|
||||||
|
|
||||||
from funcy import cached_property
|
|
||||||
from tqdm.utils import CallbackIOWrapper
|
|
||||||
|
|
||||||
from dvc.exceptions import DvcException
|
|
||||||
from dvc.progress import DEFAULT_CALLBACK, FsspecCallback
|
|
||||||
from dvc.ui import ui
|
|
||||||
from dvc.utils import tmp_fname
|
|
||||||
from dvc.utils.fs import makedirs, move
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteCmdError(DvcException):
|
|
||||||
def __init__(self, remote, cmd, ret, err):
|
|
||||||
super().__init__(
|
|
||||||
"{remote} command '{cmd}' finished with non-zero return code"
|
|
||||||
" {ret}': {err}".format(remote=remote, cmd=cmd, ret=ret, err=err)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteActionNotImplemented(DvcException):
|
|
||||||
def __init__(self, action, scheme):
|
|
||||||
m = f"{action} is not supported for {scheme} remotes"
|
|
||||||
super().__init__(m)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteMissingDepsError(DvcException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FileSystem:
|
|
||||||
sep = "/"
|
|
||||||
|
|
||||||
scheme = "base"
|
|
||||||
REQUIRES: ClassVar[Dict[str, str]] = {}
|
|
||||||
_JOBS = 4 * cpu_count()
|
|
||||||
|
|
||||||
HASH_JOBS = max(1, min(4, cpu_count() // 2))
|
|
||||||
LIST_OBJECT_PAGE_SIZE = 1000
|
|
||||||
TRAVERSE_WEIGHT_MULTIPLIER = 5
|
|
||||||
TRAVERSE_PREFIX_LEN = 3
|
|
||||||
TRAVERSE_THRESHOLD_SIZE = 500000
|
|
||||||
CAN_TRAVERSE = True
|
|
||||||
|
|
||||||
# Needed for some providers, and http open()
|
|
||||||
CHUNK_SIZE = 64 * 1024 * 1024 # 64 MiB
|
|
||||||
|
|
||||||
PARAM_CHECKSUM: ClassVar[Optional[str]] = None
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
self._check_requires(**kwargs)
|
|
||||||
|
|
||||||
self.jobs = kwargs.get("jobs") or self._JOBS
|
|
||||||
self.hash_jobs = kwargs.get("checksum_jobs") or self.HASH_JOBS
|
|
||||||
self._config = kwargs
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self):
|
|
||||||
return self._config
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def path(self):
|
|
||||||
from .path import Path
|
|
||||||
|
|
||||||
return Path(self.sep)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _strip_protocol(cls, path: str):
|
|
||||||
return path
|
|
||||||
|
|
||||||
def unstrip_protocol(self, path):
|
|
||||||
return path
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_kwargs_from_urls(urlpath): # pylint:disable=unused-argument
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_missing_deps(cls):
|
|
||||||
import importlib
|
|
||||||
|
|
||||||
missing = []
|
|
||||||
for package, module in cls.REQUIRES.items():
|
|
||||||
try:
|
|
||||||
importlib.import_module(module)
|
|
||||||
except ImportError:
|
|
||||||
missing.append(package)
|
|
||||||
|
|
||||||
return missing
|
|
||||||
|
|
||||||
def _check_requires(self, **kwargs):
|
|
||||||
from ..scheme import Schemes
|
|
||||||
from ..utils import format_link
|
|
||||||
from ..utils.pkg import PKG
|
|
||||||
|
|
||||||
missing = self.get_missing_deps()
|
|
||||||
if not missing:
|
|
||||||
return
|
|
||||||
|
|
||||||
url = kwargs.get("url", f"{self.scheme}://")
|
|
||||||
|
|
||||||
scheme = self.scheme
|
|
||||||
if scheme == Schemes.WEBDAVS:
|
|
||||||
scheme = Schemes.WEBDAV
|
|
||||||
|
|
||||||
by_pkg = {
|
|
||||||
"pip": f"pip install 'dvc[{scheme}]'",
|
|
||||||
"conda": f"conda install -c conda-forge dvc-{scheme}",
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd = by_pkg.get(PKG)
|
|
||||||
if cmd:
|
|
||||||
link = format_link("https://dvc.org/doc/install")
|
|
||||||
hint = (
|
|
||||||
f"To install dvc with those dependencies, run:\n"
|
|
||||||
"\n"
|
|
||||||
f"\t{cmd}\n"
|
|
||||||
"\n"
|
|
||||||
f"See {link} for more info."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
link = format_link("https://github.com/iterative/dvc/issues")
|
|
||||||
hint = f"Please report this bug to {link}. Thank you!"
|
|
||||||
|
|
||||||
raise RemoteMissingDepsError(
|
|
||||||
f"URL '{url}' is supported but requires these missing "
|
|
||||||
f"dependencies: {missing}. {hint}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def checksum(self, path) -> str:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def open(self, path, mode: str = "r", encoding: str = None, **kwargs):
|
|
||||||
raise RemoteActionNotImplemented("open", self.scheme)
|
|
||||||
|
|
||||||
def exists(self, path) -> bool:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
|
|
||||||
def isdir(self, path):
|
|
||||||
"""Optional: Overwrite only if the remote has a way to distinguish
|
|
||||||
between a directory and a file.
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def isfile(self, path):
|
|
||||||
"""Optional: Overwrite only if the remote has a way to distinguish
|
|
||||||
between a directory and a file.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def isexec(self, path):
|
|
||||||
"""Optional: Overwrite only if the remote has a way to distinguish
|
|
||||||
between executable and non-executable file.
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def iscopy(self, path):
|
|
||||||
"""Check if this file is an independent copy."""
|
|
||||||
return False # We can't be sure by default
|
|
||||||
|
|
||||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
|
||||||
"""Return a generator with (root, dirs, files)."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def find(self, path, prefix=None):
|
|
||||||
"""Return a generator with `str`s to all the files.
|
|
||||||
|
|
||||||
Optional kwargs:
|
|
||||||
prefix (bool): If true `path` will be treated as a prefix
|
|
||||||
rather than directory path.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def is_empty(self, path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def info(self, path):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def getsize(self, path):
|
|
||||||
return self.info(path).get("size")
|
|
||||||
|
|
||||||
def remove(self, path):
|
|
||||||
raise RemoteActionNotImplemented("remove", self.scheme)
|
|
||||||
|
|
||||||
def makedirs(self, path, **kwargs):
|
|
||||||
"""Optional: Implement only if the remote needs to create
|
|
||||||
directories before copying/linking/moving data
|
|
||||||
"""
|
|
||||||
|
|
||||||
def move(self, from_info, to_info):
|
|
||||||
self.copy(from_info, to_info)
|
|
||||||
self.remove(from_info)
|
|
||||||
|
|
||||||
def copy(self, from_info, to_info):
|
|
||||||
raise RemoteActionNotImplemented("copy", self.scheme)
|
|
||||||
|
|
||||||
def symlink(self, from_info, to_info):
|
|
||||||
raise RemoteActionNotImplemented("symlink", self.scheme)
|
|
||||||
|
|
||||||
def hardlink(self, from_info, to_info):
|
|
||||||
raise RemoteActionNotImplemented("hardlink", self.scheme)
|
|
||||||
|
|
||||||
def reflink(self, from_info, to_info):
|
|
||||||
raise RemoteActionNotImplemented("reflink", self.scheme)
|
|
||||||
|
|
||||||
# pylint: enable=unused-argument
|
|
||||||
|
|
||||||
def upload(
|
|
||||||
self,
|
|
||||||
from_info,
|
|
||||||
to_info,
|
|
||||||
total=None,
|
|
||||||
desc=None,
|
|
||||||
callback=None,
|
|
||||||
no_progress_bar=False,
|
|
||||||
**pbar_args,
|
|
||||||
):
|
|
||||||
is_file_obj = hasattr(from_info, "read")
|
|
||||||
method = "upload_fobj" if is_file_obj else "put_file"
|
|
||||||
if not hasattr(self, method):
|
|
||||||
raise RemoteActionNotImplemented(method, self.scheme)
|
|
||||||
|
|
||||||
if not is_file_obj:
|
|
||||||
from .local import localfs
|
|
||||||
|
|
||||||
desc = desc or localfs.path.name(from_info)
|
|
||||||
|
|
||||||
stack = contextlib.ExitStack()
|
|
||||||
if not callback:
|
|
||||||
pbar = ui.progress(
|
|
||||||
desc=desc,
|
|
||||||
disable=no_progress_bar,
|
|
||||||
bytes=True,
|
|
||||||
total=total or -1,
|
|
||||||
**pbar_args,
|
|
||||||
)
|
|
||||||
stack.enter_context(pbar)
|
|
||||||
callback = pbar.as_callback()
|
|
||||||
if total:
|
|
||||||
callback.set_size(total)
|
|
||||||
|
|
||||||
with stack:
|
|
||||||
if is_file_obj:
|
|
||||||
wrapped = CallbackIOWrapper(
|
|
||||||
callback.relative_update, from_info
|
|
||||||
)
|
|
||||||
# `size` is used to provide hints to the WebdavFileSystem
|
|
||||||
# for legacy servers.
|
|
||||||
# pylint: disable=no-member
|
|
||||||
return self.upload_fobj(wrapped, to_info, size=total)
|
|
||||||
|
|
||||||
logger.debug("Uploading '%s' to '%s'", from_info, to_info)
|
|
||||||
# pylint: disable=no-member
|
|
||||||
return self.put_file(
|
|
||||||
os.fspath(from_info), to_info, callback=callback
|
|
||||||
)
|
|
||||||
|
|
||||||
def download(
|
|
||||||
self,
|
|
||||||
from_info,
|
|
||||||
to_info,
|
|
||||||
name=None,
|
|
||||||
callback=None,
|
|
||||||
no_progress_bar=False,
|
|
||||||
jobs=None,
|
|
||||||
_only_file=False,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
from .local import localfs
|
|
||||||
|
|
||||||
if not hasattr(self, "get_file"):
|
|
||||||
raise RemoteActionNotImplemented("get_file", self.scheme)
|
|
||||||
|
|
||||||
download_dir = not _only_file and self.isdir(from_info)
|
|
||||||
|
|
||||||
desc = name or localfs.path.name(to_info)
|
|
||||||
stack = contextlib.ExitStack()
|
|
||||||
if not callback:
|
|
||||||
pbar_kwargs = {"unit": "Files"} if download_dir else {}
|
|
||||||
pbar = ui.progress(
|
|
||||||
total=-1,
|
|
||||||
desc="Downloading directory" if download_dir else desc,
|
|
||||||
bytes=not download_dir,
|
|
||||||
disable=no_progress_bar,
|
|
||||||
**pbar_kwargs,
|
|
||||||
)
|
|
||||||
stack.enter_context(pbar)
|
|
||||||
callback = pbar.as_callback()
|
|
||||||
|
|
||||||
with stack:
|
|
||||||
if download_dir:
|
|
||||||
return self._download_dir(
|
|
||||||
from_info, to_info, callback=callback, jobs=jobs, **kwargs
|
|
||||||
)
|
|
||||||
return self._download_file(from_info, to_info, callback=callback)
|
|
||||||
|
|
||||||
download_file = partialmethod(download, _only_file=True)
|
|
||||||
|
|
||||||
def _download_dir(
|
|
||||||
self,
|
|
||||||
from_info,
|
|
||||||
to_info,
|
|
||||||
callback=DEFAULT_CALLBACK,
|
|
||||||
jobs=None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
from .local import localfs
|
|
||||||
|
|
||||||
from_infos = list(self.find(from_info, **kwargs))
|
|
||||||
if not from_infos:
|
|
||||||
makedirs(to_info, exist_ok=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
to_infos = (
|
|
||||||
localfs.path.join(to_info, *self.path.relparts(info, from_info))
|
|
||||||
for info in from_infos
|
|
||||||
)
|
|
||||||
callback.set_size(len(from_infos))
|
|
||||||
|
|
||||||
download_files = FsspecCallback.wrap_fn(callback, self._download_file)
|
|
||||||
max_workers = jobs or self.jobs
|
|
||||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
||||||
futures = [
|
|
||||||
executor.submit(download_files, from_info, to_info)
|
|
||||||
for from_info, to_info in zip(from_infos, to_infos)
|
|
||||||
]
|
|
||||||
|
|
||||||
# NOTE: unlike pulling/fetching cache, where we need to
|
|
||||||
# download everything we can, not raising an error here might
|
|
||||||
# turn very ugly, as the user might think that he has
|
|
||||||
# downloaded a complete directory, while having a partial one,
|
|
||||||
# which might cause unexpected results in his pipeline.
|
|
||||||
for future in as_completed(futures):
|
|
||||||
# NOTE: executor won't let us raise until all futures that
|
|
||||||
# it has are finished, so we need to cancel them ourselves
|
|
||||||
# before re-raising.
|
|
||||||
exc = future.exception()
|
|
||||||
if exc:
|
|
||||||
for entry in futures:
|
|
||||||
entry.cancel()
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
def _download_file(
|
|
||||||
self,
|
|
||||||
from_info,
|
|
||||||
to_info,
|
|
||||||
callback=DEFAULT_CALLBACK,
|
|
||||||
):
|
|
||||||
from .local import localfs
|
|
||||||
|
|
||||||
makedirs(localfs.path.parent(to_info), exist_ok=True)
|
|
||||||
tmp_file = tmp_fname(to_info)
|
|
||||||
|
|
||||||
logger.debug("Downloading '%s' to '%s'", from_info, to_info)
|
|
||||||
try:
|
|
||||||
# noqa, pylint: disable=no-member
|
|
||||||
self.get_file(from_info, tmp_file, callback=callback)
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
# do we need to rollback makedirs for previously not-existing
|
|
||||||
# directories?
|
|
||||||
with contextlib.suppress(FileNotFoundError):
|
|
||||||
os.unlink(tmp_file)
|
|
||||||
raise
|
|
||||||
|
|
||||||
move(tmp_file, to_info)
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# pylint: disable=unused-import
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from funcy import cached_property
|
||||||
|
|
||||||
|
from dvc_objects.fs.callbacks import ( # noqa: F401
|
||||||
|
DEFAULT_CALLBACK,
|
||||||
|
Callback,
|
||||||
|
TqdmCallback,
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dvc.ui._rich_progress import RichTransferProgress
|
||||||
|
|
||||||
|
|
||||||
|
class RichCallback(Callback):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
size: Optional[int] = None,
|
||||||
|
value: int = 0,
|
||||||
|
progress: "RichTransferProgress" = None,
|
||||||
|
desc: str = None,
|
||||||
|
bytes: bool = False, # pylint: disable=redefined-builtin
|
||||||
|
unit: str = None,
|
||||||
|
disable: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self._progress = progress
|
||||||
|
self.disable = disable
|
||||||
|
self._task_kwargs = {
|
||||||
|
"description": desc or "",
|
||||||
|
"bytes": bytes,
|
||||||
|
"unit": unit,
|
||||||
|
"total": size or 0,
|
||||||
|
"visible": False,
|
||||||
|
"progress_type": None if bytes else "summary",
|
||||||
|
}
|
||||||
|
self._stack = ExitStack()
|
||||||
|
super().__init__(size=size, value=value)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def progress(self):
|
||||||
|
from dvc.ui import ui
|
||||||
|
from dvc.ui._rich_progress import RichTransferProgress
|
||||||
|
|
||||||
|
if self._progress is not None:
|
||||||
|
return self._progress
|
||||||
|
|
||||||
|
progress = RichTransferProgress(
|
||||||
|
transient=True,
|
||||||
|
disable=self.disable,
|
||||||
|
console=ui.error_console,
|
||||||
|
)
|
||||||
|
return self._stack.enter_context(progress)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def task(self):
|
||||||
|
return self.progress.add_task(**self._task_kwargs)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.progress.clear_task(self.task)
|
||||||
|
self._stack.close()
|
||||||
|
|
||||||
|
def call(self, hook_name=None, **kwargs):
|
||||||
|
self.progress.update(
|
||||||
|
self.task,
|
||||||
|
completed=self.value,
|
||||||
|
total=self.size,
|
||||||
|
visible=not self.disable,
|
||||||
|
)
|
||||||
|
|
||||||
|
def branch(self, path_1, path_2, kwargs, child: Optional[Callback] = None):
|
||||||
|
child = child or RichCallback(
|
||||||
|
progress=self.progress, desc=path_1, bytes=True
|
||||||
|
)
|
||||||
|
return super().branch(path_1, path_2, kwargs, child=child)
|
||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from fsspec import AbstractFileSystem
|
||||||
|
from funcy import cached_property, wrap_prop
|
||||||
|
|
||||||
|
from dvc_objects.fs.base import FileSystem
|
||||||
|
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from dvc.types import AnyPath
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class _DataFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||||
|
"""DVC repo fs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo: DVC repo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
root_marker = "/"
|
||||||
|
|
||||||
|
def __init__(self, workspace=None, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.repo = kwargs["repo"]
|
||||||
|
self.workspace = workspace or "repo"
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def path(self):
|
||||||
|
from . import Path
|
||||||
|
|
||||||
|
def _getcwd():
|
||||||
|
return self.root_marker
|
||||||
|
|
||||||
|
return Path(self.sep, getcwd=_getcwd)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _get_key(self, path):
|
||||||
|
if self.workspace != "repo":
|
||||||
|
from . import get_cloud_fs
|
||||||
|
|
||||||
|
cls, kwargs, fs_path = get_cloud_fs(None, url=path)
|
||||||
|
fs = cls(**kwargs)
|
||||||
|
return (self.workspace, *fs.path.parts(fs_path))
|
||||||
|
|
||||||
|
path = self.path.abspath(path)
|
||||||
|
if path == self.root_marker:
|
||||||
|
return (self.workspace,)
|
||||||
|
|
||||||
|
key = self.path.relparts(path, self.root_marker)
|
||||||
|
if key == (".") or key == (""):
|
||||||
|
key = ()
|
||||||
|
|
||||||
|
return (self.workspace, *key)
|
||||||
|
|
||||||
|
def _get_fs_path(self, path: "AnyPath", remote=None):
|
||||||
|
from dvc.config import NoRemoteError
|
||||||
|
|
||||||
|
info = self.info(path)
|
||||||
|
if info["type"] == "directory":
|
||||||
|
raise IsADirectoryError
|
||||||
|
|
||||||
|
value = info.get("md5")
|
||||||
|
if not value:
|
||||||
|
raise FileNotFoundError
|
||||||
|
|
||||||
|
cache_path = self.repo.odb.local.oid_to_path(value)
|
||||||
|
|
||||||
|
if self.repo.odb.local.fs.exists(cache_path):
|
||||||
|
return self.repo.odb.local.fs, cache_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
remote_odb = self.repo.cloud.get_remote_odb(remote)
|
||||||
|
except NoRemoteError as exc:
|
||||||
|
raise FileNotFoundError from exc
|
||||||
|
remote_fs_path = remote_odb.oid_to_path(value)
|
||||||
|
return remote_odb.fs, remote_fs_path
|
||||||
|
|
||||||
|
def open( # type: ignore
|
||||||
|
self, path: str, mode="r", encoding=None, **kwargs
|
||||||
|
): # pylint: disable=arguments-renamed, arguments-differ
|
||||||
|
fs, fspath = self._get_fs_path(path, **kwargs)
|
||||||
|
return fs.open(fspath, mode=mode, encoding=encoding)
|
||||||
|
|
||||||
|
def ls(self, path, detail=True, **kwargs):
|
||||||
|
info = self.info(path)
|
||||||
|
if info["type"] != "directory":
|
||||||
|
return [info] if detail else [path]
|
||||||
|
|
||||||
|
root_key = self._get_key(path)
|
||||||
|
try:
|
||||||
|
entries = [
|
||||||
|
self.sep.join((path, name)) if path else name
|
||||||
|
for name in self.repo.index.tree.ls(prefix=root_key)
|
||||||
|
]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise FileNotFoundError from exc
|
||||||
|
|
||||||
|
if not detail:
|
||||||
|
return entries
|
||||||
|
|
||||||
|
return [self.info(epath) for epath in entries]
|
||||||
|
|
||||||
|
def isdvc(self, path, recursive=False, strict=True):
|
||||||
|
try:
|
||||||
|
info = self.info(path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
recurse = recursive or not strict
|
||||||
|
return bool(info.get("outs") if recurse else info.get("isout"))
|
||||||
|
|
||||||
|
def info(self, path, **kwargs):
|
||||||
|
from dvc_data.hashfile.meta import Meta
|
||||||
|
|
||||||
|
key = self._get_key(path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
outs = list(self.repo.index.tree.iteritems(key)) # noqa: B301
|
||||||
|
except KeyError as exc:
|
||||||
|
raise FileNotFoundError from exc
|
||||||
|
|
||||||
|
ret = {
|
||||||
|
"type": "file",
|
||||||
|
"size": 0,
|
||||||
|
"isexec": False,
|
||||||
|
"isdvc": False,
|
||||||
|
"outs": outs,
|
||||||
|
"name": path,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outs) > 1 and outs[0][0] != key:
|
||||||
|
shortest = self.repo.index.tree.shortest_prefix(key)
|
||||||
|
if shortest:
|
||||||
|
assert shortest[1][1].isdir
|
||||||
|
if len(shortest[0]) <= len(key):
|
||||||
|
ret["isdvc"] = True
|
||||||
|
|
||||||
|
ret["type"] = "directory"
|
||||||
|
return ret
|
||||||
|
|
||||||
|
item_key, (meta, hash_info) = outs[0]
|
||||||
|
|
||||||
|
meta = meta or Meta()
|
||||||
|
|
||||||
|
if key != item_key:
|
||||||
|
assert item_key[: len(key)] == key
|
||||||
|
ret["type"] = "directory"
|
||||||
|
return ret
|
||||||
|
|
||||||
|
ret["size"] = meta.size
|
||||||
|
ret["isexec"] = meta.isexec
|
||||||
|
ret[hash_info.name] = hash_info.value
|
||||||
|
ret["isdvc"] = True
|
||||||
|
ret["isout"] = True
|
||||||
|
ret["meta"] = meta
|
||||||
|
if hash_info and hash_info.isdir:
|
||||||
|
ret["type"] = "directory"
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def get_file( # pylint: disable=arguments-differ
|
||||||
|
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
|
||||||
|
):
|
||||||
|
fs, path = self._get_fs_path(rpath)
|
||||||
|
fs.get_file(path, lpath, callback=callback, **kwargs)
|
||||||
|
|
||||||
|
def checksum(self, path):
|
||||||
|
info = self.info(path)
|
||||||
|
md5 = info.get("md5")
|
||||||
|
if md5:
|
||||||
|
return md5
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class DataFileSystem(FileSystem):
|
||||||
|
protocol = "local"
|
||||||
|
|
||||||
|
PARAM_CHECKSUM = "md5"
|
||||||
|
|
||||||
|
def _prepare_credentials(self, **config):
|
||||||
|
return config
|
||||||
|
|
||||||
|
@wrap_prop(threading.Lock())
|
||||||
|
@cached_property
|
||||||
|
def fs(self):
|
||||||
|
return _DataFileSystem(**self.fs_args)
|
||||||
|
|
||||||
|
def isdvc(self, path, **kwargs):
|
||||||
|
return self.fs.isdvc(path, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def repo(self):
|
||||||
|
return self.fs.repo
|
||||||
+448
-243
@@ -1,275 +1,480 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import ntpath
|
||||||
import os
|
import os
|
||||||
import typing
|
import posixpath
|
||||||
|
import threading
|
||||||
|
from contextlib import suppress
|
||||||
|
from itertools import takewhile
|
||||||
|
from typing import TYPE_CHECKING, Callable, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
from dvc.exceptions import OutputNotFoundError
|
from fsspec.spec import AbstractFileSystem
|
||||||
from dvc.utils import relpath
|
from funcy import cached_property, wrap_prop, wrap_with
|
||||||
|
|
||||||
from ..progress import DEFAULT_CALLBACK
|
from dvc_objects.fs.base import FileSystem
|
||||||
from ._metadata import Metadata
|
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK
|
||||||
from .base import FileSystem
|
from dvc_objects.fs.path import Path
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
from .data import DataFileSystem
|
||||||
from dvc.output import Output
|
|
||||||
from dvc.types import AnyPath
|
if TYPE_CHECKING:
|
||||||
|
from dvc.repo import Repo
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
RepoFactory = Union[Callable[[str], "Repo"], Type["Repo"]]
|
||||||
|
|
||||||
class DvcFileSystem(FileSystem): # pylint:disable=abstract-method
|
|
||||||
"""DVC repo fs.
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_info(repo, fs_info, dvc_info):
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
ret = {"repo": repo}
|
||||||
|
|
||||||
|
if dvc_info:
|
||||||
|
ret["dvc_info"] = dvc_info
|
||||||
|
ret["type"] = dvc_info["type"]
|
||||||
|
ret["size"] = dvc_info["size"]
|
||||||
|
if not fs_info and "md5" in dvc_info:
|
||||||
|
ret["md5"] = dvc_info["md5"]
|
||||||
|
|
||||||
|
if fs_info:
|
||||||
|
ret["type"] = fs_info["type"]
|
||||||
|
ret["size"] = fs_info["size"]
|
||||||
|
isexec = False
|
||||||
|
if fs_info["type"] == "file":
|
||||||
|
isexec = utils.is_exec(fs_info["mode"])
|
||||||
|
ret["isexec"] = isexec
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||||
|
"""DVC + git-tracked files fs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repo: DVC repo.
|
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()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sep = os.sep
|
root_marker = "/"
|
||||||
|
|
||||||
scheme = "local"
|
PARAM_REPO_URL = "repo_url"
|
||||||
PARAM_CHECKSUM = "md5"
|
PARAM_REPO_ROOT = "repo_root"
|
||||||
|
PARAM_REV = "rev"
|
||||||
|
PARAM_CACHE_DIR = "cache_dir"
|
||||||
|
PARAM_CACHE_TYPES = "cache_types"
|
||||||
|
PARAM_SUBREPOS = "subrepos"
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(
|
||||||
super().__init__(**kwargs)
|
self,
|
||||||
self.repo = kwargs["repo"]
|
repo: Optional["Repo"] = None,
|
||||||
|
subrepos=False,
|
||||||
|
repo_factory: RepoFactory = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
from pygtrie import Trie
|
||||||
|
|
||||||
|
if repo is None:
|
||||||
|
repo, repo_factory = self._repo_from_fs_config(
|
||||||
|
subrepos=subrepos, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
if not repo_factory:
|
||||||
|
from dvc.repo import Repo
|
||||||
|
|
||||||
|
self.repo_factory: RepoFactory = Repo
|
||||||
|
else:
|
||||||
|
self.repo_factory = repo_factory
|
||||||
|
|
||||||
|
def _getcwd():
|
||||||
|
relparts = ()
|
||||||
|
if repo.fs.path.isin(repo.fs.path.getcwd(), repo.root_dir):
|
||||||
|
relparts = repo.fs.path.relparts(
|
||||||
|
repo.fs.path.getcwd(), repo.root_dir
|
||||||
|
)
|
||||||
|
return self.root_marker + self.sep.join(relparts)
|
||||||
|
|
||||||
|
self.path = Path(self.sep, getcwd=_getcwd)
|
||||||
|
self.repo = repo
|
||||||
|
self.hash_jobs = repo.fs.hash_jobs
|
||||||
|
self._traverse_subrepos = subrepos
|
||||||
|
|
||||||
|
self._subrepos_trie = Trie()
|
||||||
|
"""Keeps track of each and every path with the corresponding repo."""
|
||||||
|
|
||||||
|
key = self._get_key(self.repo.root_dir)
|
||||||
|
self._subrepos_trie[key] = repo
|
||||||
|
|
||||||
|
self._datafss = {}
|
||||||
|
"""Keep a datafs instance of each repo."""
|
||||||
|
|
||||||
|
if hasattr(repo, "dvc_dir"):
|
||||||
|
self._datafss[key] = DataFileSystem(repo=repo)
|
||||||
|
|
||||||
|
def _get_key(self, path):
|
||||||
|
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
|
||||||
|
if parts == (".",):
|
||||||
|
parts = ()
|
||||||
|
return parts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def repo_url(self):
|
||||||
|
if self.repo is None:
|
||||||
|
return None
|
||||||
|
return self.repo.url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
raise NotImplementedError
|
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.cache_dir
|
||||||
|
),
|
||||||
|
self.PARAM_CACHE_TYPES: self.repo.odb.local.cache_types,
|
||||||
|
self.PARAM_SUBREPOS: self._traverse_subrepos,
|
||||||
|
}
|
||||||
|
|
||||||
def _find_outs(self, path, *args, **kwargs):
|
@classmethod
|
||||||
outs = self.repo.find_outs_by_path(path, *args, **kwargs)
|
def _repo_from_fs_config(
|
||||||
|
cls, **config
|
||||||
|
) -> Tuple["Repo", Optional["RepoFactory"]]:
|
||||||
|
from dvc.external_repo import erepo_factory, external_repo
|
||||||
|
from dvc.repo import Repo
|
||||||
|
|
||||||
def _is_cached(out):
|
url = config.get(cls.PARAM_REPO_URL)
|
||||||
return out.use_cache
|
root = config.get(cls.PARAM_REPO_ROOT)
|
||||||
|
assert url or root
|
||||||
|
|
||||||
outs = list(filter(_is_cached, outs))
|
def _open(*args, **kwargs):
|
||||||
if not outs:
|
# NOTE: if original repo was an erepo (and has a URL),
|
||||||
raise OutputNotFoundError(path, self.repo)
|
# 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)
|
||||||
|
|
||||||
return outs
|
cache_dir = config.get(cls.PARAM_CACHE_DIR)
|
||||||
|
cache_config = (
|
||||||
def _get_granular_hash(self, path: "AnyPath", out: "Output", remote=None):
|
{}
|
||||||
# NOTE: use string paths here for performance reasons
|
if not cache_dir
|
||||||
key = tuple(relpath(path, out.fs_path).split(os.sep))
|
else {
|
||||||
out.get_dir_cache(remote=remote)
|
"cache": {
|
||||||
if out.obj is None:
|
"dir": cache_dir,
|
||||||
raise FileNotFoundError
|
"type": config.get(cls.PARAM_CACHE_TYPES),
|
||||||
(_, oid) = out.obj.trie.get(key) or (None, None)
|
}
|
||||||
if oid:
|
}
|
||||||
return oid
|
)
|
||||||
raise FileNotFoundError
|
repo_kwargs: dict = {
|
||||||
|
"rev": config.get(cls.PARAM_REV),
|
||||||
def _get_fs_path(self, path: "AnyPath", remote=None):
|
"subrepos": config.get(cls.PARAM_SUBREPOS, False),
|
||||||
try:
|
"uninitialized": True,
|
||||||
outs = self._find_outs(path, strict=False)
|
}
|
||||||
except OutputNotFoundError as exc:
|
factory: Optional["RepoFactory"] = None
|
||||||
raise FileNotFoundError from exc
|
if url is None:
|
||||||
|
repo_kwargs["config"] = cache_config
|
||||||
if len(outs) != 1 or (
|
|
||||||
outs[0].is_dir_checksum and path == outs[0].fs_path
|
|
||||||
):
|
|
||||||
raise IsADirectoryError
|
|
||||||
|
|
||||||
out = outs[0]
|
|
||||||
|
|
||||||
if not out.hash_info:
|
|
||||||
raise FileNotFoundError
|
|
||||||
|
|
||||||
if out.changed_cache(filter_info=path):
|
|
||||||
from dvc.config import NoRemoteError
|
|
||||||
|
|
||||||
try:
|
|
||||||
remote_odb = self.repo.cloud.get_remote_odb(remote)
|
|
||||||
except NoRemoteError as exc:
|
|
||||||
raise FileNotFoundError from exc
|
|
||||||
if out.is_dir_checksum:
|
|
||||||
checksum = self._get_granular_hash(path, out).value
|
|
||||||
else:
|
|
||||||
checksum = out.hash_info.value
|
|
||||||
remote_fs_path = remote_odb.hash_to_path(checksum)
|
|
||||||
return remote_odb.fs, remote_fs_path
|
|
||||||
|
|
||||||
if out.is_dir_checksum:
|
|
||||||
checksum = self._get_granular_hash(path, out).value
|
|
||||||
cache_path = out.odb.fs.unstrip_protocol(
|
|
||||||
out.odb.hash_to_path(checksum)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
cache_path = out.cache_path
|
repo_kwargs["cache_dir"] = cache_dir
|
||||||
return out.odb.fs, cache_path
|
factory = erepo_factory(url, root, cache_config)
|
||||||
|
|
||||||
def open( # type: ignore
|
with _open(
|
||||||
self, path: str, mode="r", encoding=None, **kwargs
|
url if url else root,
|
||||||
): # pylint: disable=arguments-renamed
|
**repo_kwargs,
|
||||||
fs, fspath = self._get_fs_path(path, **kwargs)
|
) as repo:
|
||||||
return fs.open(fspath, mode=mode, encoding=encoding)
|
return repo, factory
|
||||||
|
|
||||||
def exists(self, path): # pylint: disable=arguments-renamed
|
def _get_repo(self, path: str) -> "Repo":
|
||||||
try:
|
"""Returns repo that the path falls in, using prefix.
|
||||||
self.metadata(path)
|
|
||||||
return True
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def isdir(self, path): # pylint: disable=arguments-renamed
|
If the path is already tracked/collected, it just returns the repo.
|
||||||
try:
|
|
||||||
meta = self.metadata(path)
|
|
||||||
return meta.isdir
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def check_isdir(self, path, outs):
|
Otherwise, it collects the repos that might be in the path's parents
|
||||||
if len(outs) != 1:
|
and then returns the appropriate one.
|
||||||
return True
|
"""
|
||||||
|
if not self.repo.fs.path.isin_or_eq(path, self.repo.root_dir):
|
||||||
|
# outside of repo
|
||||||
|
return self.repo
|
||||||
|
|
||||||
out = outs[0]
|
key = self._get_key(path)
|
||||||
if not out.is_dir_checksum:
|
repo = self._subrepos_trie.get(key)
|
||||||
return out.fs_path != path
|
if repo:
|
||||||
if out.fs_path == path:
|
return repo
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
prefix_key, repo = self._subrepos_trie.longest_prefix(key)
|
||||||
self._get_granular_hash(path, out)
|
prefix = self.repo.fs.path.join(
|
||||||
return False
|
self.repo.root_dir,
|
||||||
except FileNotFoundError:
|
*prefix_key, # pylint: disable=not-an-iterable
|
||||||
return True
|
|
||||||
|
|
||||||
def isfile(self, path): # pylint: disable=arguments-renamed
|
|
||||||
try:
|
|
||||||
meta = self.metadata(path)
|
|
||||||
return meta.isfile
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _fetch_dir(self, out, **kwargs):
|
|
||||||
# pull dir cache if needed
|
|
||||||
out.get_dir_cache(**kwargs)
|
|
||||||
|
|
||||||
if not out.obj:
|
|
||||||
raise FileNotFoundError
|
|
||||||
|
|
||||||
def _add_dir(self, trie, out, **kwargs):
|
|
||||||
self._fetch_dir(out, **kwargs)
|
|
||||||
|
|
||||||
base = out.fs.path.parts(out.fs_path)
|
|
||||||
for key, _, _ in out.obj: # noqa: B301
|
|
||||||
trie[base + key] = None
|
|
||||||
|
|
||||||
def _walk(self, root, trie, topdown=True, **kwargs):
|
|
||||||
dirs = set()
|
|
||||||
files = []
|
|
||||||
|
|
||||||
root_parts = self.path.parts(root)
|
|
||||||
out = trie.get(root_parts)
|
|
||||||
if out and out.is_dir_checksum:
|
|
||||||
self._add_dir(trie, out, **kwargs)
|
|
||||||
|
|
||||||
root_len = len(root_parts)
|
|
||||||
try:
|
|
||||||
for key, out in trie.iteritems(prefix=root_parts): # noqa: B301
|
|
||||||
if key == root_parts:
|
|
||||||
continue
|
|
||||||
|
|
||||||
name = key[root_len]
|
|
||||||
if len(key) > root_len + 1 or (out and out.is_dir_checksum):
|
|
||||||
dirs.add(name)
|
|
||||||
continue
|
|
||||||
|
|
||||||
files.append(name)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert topdown
|
|
||||||
dirs = list(dirs)
|
|
||||||
yield root, dirs, files
|
|
||||||
|
|
||||||
for dname in dirs:
|
|
||||||
yield from self._walk(self.path.join(root, dname), trie)
|
|
||||||
|
|
||||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
|
||||||
from pygtrie import Trie
|
|
||||||
|
|
||||||
assert topdown
|
|
||||||
root = os.path.abspath(top)
|
|
||||||
try:
|
|
||||||
meta = self.metadata(root)
|
|
||||||
except FileNotFoundError:
|
|
||||||
if onerror is not None:
|
|
||||||
onerror(FileNotFoundError(top))
|
|
||||||
return
|
|
||||||
|
|
||||||
if not meta.isdir:
|
|
||||||
if onerror is not None:
|
|
||||||
onerror(NotADirectoryError(top))
|
|
||||||
return
|
|
||||||
|
|
||||||
trie = Trie()
|
|
||||||
for out in meta.outs:
|
|
||||||
trie[out.fs.path.parts(out.fs_path)] = out
|
|
||||||
|
|
||||||
if out.is_dir_checksum and self.path.isin_or_eq(root, out.fs_path):
|
|
||||||
self._add_dir(trie, out, **kwargs)
|
|
||||||
|
|
||||||
yield from self._walk(root, trie, topdown=topdown, **kwargs)
|
|
||||||
|
|
||||||
def find(self, path, prefix=None):
|
|
||||||
for root, _, files in self.walk(path):
|
|
||||||
for fname in files:
|
|
||||||
# NOTE: os.path.join is ~5.5 times slower
|
|
||||||
yield f"{root}{os.sep}{fname}"
|
|
||||||
|
|
||||||
def isdvc(self, path, recursive=False, strict=True):
|
|
||||||
try:
|
|
||||||
meta = self.metadata(path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
recurse = recursive or not strict
|
|
||||||
return meta.output_exists if recurse else meta.is_output
|
|
||||||
|
|
||||||
def isexec(self, path): # pylint: disable=unused-argument
|
|
||||||
return False
|
|
||||||
|
|
||||||
def metadata(self, fs_path):
|
|
||||||
abspath = os.path.abspath(fs_path)
|
|
||||||
|
|
||||||
try:
|
|
||||||
outs = self._find_outs(abspath, strict=False, recursive=True)
|
|
||||||
except OutputNotFoundError as exc:
|
|
||||||
raise FileNotFoundError from exc
|
|
||||||
|
|
||||||
meta = Metadata(fs_path=abspath, outs=outs, repo=self.repo)
|
|
||||||
meta.isdir = meta.isdir or self.check_isdir(meta.fs_path, meta.outs)
|
|
||||||
return meta
|
|
||||||
|
|
||||||
def info(self, path):
|
|
||||||
meta = self.metadata(path)
|
|
||||||
ret = {"type": "directory" if meta.isdir else "file"}
|
|
||||||
if meta.is_output and len(meta.outs) == 1 and meta.outs[0].hash_info:
|
|
||||||
out = meta.outs[0]
|
|
||||||
ret["size"] = out.meta.size
|
|
||||||
ret[out.hash_info.name] = out.hash_info.value
|
|
||||||
elif meta.part_of_output:
|
|
||||||
(out,) = meta.outs
|
|
||||||
key = self.path.parts(self.path.relpath(path, out.fs_path))
|
|
||||||
(obj_meta, oid) = out.obj.trie.get(key) or (None, None)
|
|
||||||
if oid:
|
|
||||||
ret["size"] = obj_meta.size if obj_meta else 0
|
|
||||||
ret[oid.name] = oid.value
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def get_file(
|
|
||||||
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
|
|
||||||
):
|
|
||||||
fs, path = self._get_fs_path(from_info)
|
|
||||||
fs.get_file( # pylint: disable=protected-access
|
|
||||||
path, to_file, callback=callback, **kwargs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
return self._subrepos_trie.get(key) or self.repo
|
||||||
|
|
||||||
|
@wrap_with(threading.Lock())
|
||||||
|
def _update(self, dirs, starting_repo):
|
||||||
|
"""Checks for subrepo in directories and updates them."""
|
||||||
|
repo = starting_repo
|
||||||
|
for d in dirs:
|
||||||
|
key = self._get_key(d)
|
||||||
|
if self._is_dvc_repo(d):
|
||||||
|
repo = self.repo_factory(
|
||||||
|
d,
|
||||||
|
fs=self.repo.fs,
|
||||||
|
scm=self.repo.scm,
|
||||||
|
repo_factory=self.repo_factory,
|
||||||
|
)
|
||||||
|
self._datafss[key] = DataFileSystem(repo=repo)
|
||||||
|
self._subrepos_trie[key] = repo
|
||||||
|
|
||||||
|
def _is_dvc_repo(self, dir_path):
|
||||||
|
"""Check if the directory is a dvc repo."""
|
||||||
|
if not self._traverse_subrepos:
|
||||||
|
return False
|
||||||
|
|
||||||
|
from dvc.repo import Repo
|
||||||
|
|
||||||
|
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[FileSystem],
|
||||||
|
Optional[str],
|
||||||
|
Optional[DataFileSystem],
|
||||||
|
Optional[str],
|
||||||
|
]:
|
||||||
|
"""
|
||||||
|
Returns a pair of fss based on repo the path falls in, using prefix.
|
||||||
|
"""
|
||||||
|
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 ""
|
||||||
|
else:
|
||||||
|
dvc_path = None
|
||||||
|
|
||||||
|
return fs, fs_path, dvc_fs, dvc_path
|
||||||
|
|
||||||
|
def open(
|
||||||
|
self, path, mode="r", encoding="utf-8", **kwargs
|
||||||
|
): # pylint: disable=arguments-renamed, arguments-differ
|
||||||
|
if "b" in mode:
|
||||||
|
encoding = None
|
||||||
|
|
||||||
|
fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||||
|
try:
|
||||||
|
return fs.open(fs_path, mode=mode, encoding=encoding)
|
||||||
|
except FileNotFoundError:
|
||||||
|
if not dvc_fs:
|
||||||
|
raise
|
||||||
|
|
||||||
|
return dvc_fs.open(dvc_path, mode=mode, encoding=encoding, **kwargs)
|
||||||
|
|
||||||
|
def isdvc(self, path, **kwargs):
|
||||||
|
_, _, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||||
|
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
|
||||||
|
):
|
||||||
|
fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||||
|
|
||||||
|
repo = dvc_fs.repo if dvc_fs else self.repo
|
||||||
|
dvcignore = repo.dvcignore
|
||||||
|
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||||
|
|
||||||
|
names = set()
|
||||||
|
if dvc_fs:
|
||||||
|
with suppress(FileNotFoundError):
|
||||||
|
for entry in dvc_fs.ls(dvc_path, detail=False):
|
||||||
|
names.add(dvc_fs.path.name(entry))
|
||||||
|
|
||||||
|
if not dvc_only and fs:
|
||||||
|
try:
|
||||||
|
for entry in dvcignore.ls(
|
||||||
|
fs, fs_path, detail=False, ignore_subrepos=ignore_subrepos
|
||||||
|
):
|
||||||
|
names.add(fs.path.name(entry))
|
||||||
|
except (FileNotFoundError, NotADirectoryError):
|
||||||
|
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)
|
||||||
|
|
||||||
|
infos = []
|
||||||
|
paths = []
|
||||||
|
for name in names:
|
||||||
|
entry_path = self.path.join(path, name)
|
||||||
|
try:
|
||||||
|
info = self.info(entry_path, ignore_subrepos=ignore_subrepos)
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
infos.append(info)
|
||||||
|
paths.append(entry_path)
|
||||||
|
|
||||||
|
if not detail:
|
||||||
|
return paths
|
||||||
|
|
||||||
|
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):
|
||||||
|
fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||||
|
|
||||||
|
repo = dvc_fs.repo if dvc_fs else self.repo
|
||||||
|
dvcignore = repo.dvcignore
|
||||||
|
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||||
|
|
||||||
|
dvc_info = None
|
||||||
|
if dvc_fs:
|
||||||
|
try:
|
||||||
|
dvc_info = dvc_fs.info(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
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
for parent in fs.path.parents(fs_path):
|
||||||
|
try:
|
||||||
|
if fs.info(parent)["type"] != "directory":
|
||||||
|
dvc_info = None
|
||||||
|
break
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not dvc_info and not fs_info:
|
||||||
|
raise FileNotFoundError
|
||||||
|
|
||||||
|
info = _merge_info(repo, fs_info, dvc_info)
|
||||||
|
info["name"] = path
|
||||||
|
return info
|
||||||
|
|
||||||
def checksum(self, path):
|
def checksum(self, path):
|
||||||
info = self.info(path)
|
fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||||
md5 = info.get("md5")
|
|
||||||
if md5:
|
try:
|
||||||
return md5
|
return fs.checksum(fs_path)
|
||||||
raise NotImplementedError
|
except FileNotFoundError:
|
||||||
|
return dvc_fs.checksum(dvc_path)
|
||||||
|
|
||||||
|
|
||||||
|
class DvcFileSystem(FileSystem):
|
||||||
|
protocol = "local"
|
||||||
|
PARAM_CHECKSUM = "md5"
|
||||||
|
|
||||||
|
def _prepare_credentials(self, **config):
|
||||||
|
return config
|
||||||
|
|
||||||
|
@wrap_prop(threading.Lock())
|
||||||
|
@cached_property
|
||||||
|
def fs(self):
|
||||||
|
return _DvcFileSystem(**self.fs_args)
|
||||||
|
|
||||||
|
def isdvc(self, path, **kwargs):
|
||||||
|
return self.fs.isdvc(path, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self): # pylint: disable=invalid-overridden-method
|
||||||
|
return self.fs.path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def repo(self):
|
||||||
|
return self.fs.repo
|
||||||
|
|
||||||
|
@property
|
||||||
|
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)
|
||||||
|
|
||||||
|
return as_posix(path)
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
import os
|
|
||||||
import shutil
|
|
||||||
from typing import IO, TYPE_CHECKING, Any, Dict, Iterator, Optional, overload
|
|
||||||
|
|
||||||
from funcy import cached_property
|
|
||||||
from tqdm.utils import CallbackIOWrapper
|
|
||||||
|
|
||||||
from dvc.progress import DEFAULT_CALLBACK
|
|
||||||
|
|
||||||
from .base import FileSystem
|
|
||||||
|
|
||||||
FSPath = str
|
|
||||||
AnyFSPath = str
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
|
|
||||||
# An info() entry, might evolve to a TypedDict
|
|
||||||
# in the future (e.g for properly type 'size' etc).
|
|
||||||
Entry = Dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-member
|
|
||||||
class FSSpecWrapper(FileSystem):
|
|
||||||
TRAVERSE_PREFIX_LEN = 2
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.fs_args = {"skip_instance_cache": True}
|
|
||||||
self.fs_args.update(self._prepare_credentials(**kwargs))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_kwargs_from_urls(urlpath: str) -> "Dict[str, Any]":
|
|
||||||
from fsspec.utils import infer_storage_options
|
|
||||||
|
|
||||||
options = infer_storage_options(urlpath)
|
|
||||||
options.pop("path", None)
|
|
||||||
options.pop("protocol", None)
|
|
||||||
return options
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def fs(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _prepare_credentials(
|
|
||||||
self, **config: Dict[str, Any] # pylint: disable=unused-argument
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""Prepare the arguments for authentication to the
|
|
||||||
host filesystem"""
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _isdir(self, path: AnyFSPath) -> bool:
|
|
||||||
return self.fs.isdir(path)
|
|
||||||
|
|
||||||
def isdir(self, path: AnyFSPath) -> bool:
|
|
||||||
try:
|
|
||||||
return self._isdir(path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def isfile(self, path: AnyFSPath) -> bool:
|
|
||||||
try:
|
|
||||||
return not self._isdir(path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_empty(self, path: AnyFSPath) -> bool:
|
|
||||||
entry = self.info(path)
|
|
||||||
if entry["type"] == "directory":
|
|
||||||
return not self.fs.ls(path)
|
|
||||||
return entry["size"] == 0
|
|
||||||
|
|
||||||
def open(
|
|
||||||
self,
|
|
||||||
path: AnyFSPath,
|
|
||||||
mode: str = "r",
|
|
||||||
encoding: Optional[str] = None,
|
|
||||||
**kwargs,
|
|
||||||
) -> "IO": # pylint: disable=arguments-differ
|
|
||||||
return self.fs.open(path, mode=mode)
|
|
||||||
|
|
||||||
def checksum(self, path: AnyFSPath) -> str:
|
|
||||||
return self.fs.checksum(path)
|
|
||||||
|
|
||||||
def copy(self, from_info: AnyFSPath, to_info: AnyFSPath) -> None:
|
|
||||||
self.makedirs(self.path.parent(to_info))
|
|
||||||
self.fs.copy(from_info, to_info)
|
|
||||||
|
|
||||||
def exists(self, path: AnyFSPath) -> bool:
|
|
||||||
return self.fs.exists(path)
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def ls(
|
|
||||||
self, path: AnyFSPath, detail: "Literal[True]"
|
|
||||||
) -> "Iterator[Entry]":
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def ls(self, path: AnyFSPath, detail: "Literal[False]") -> Iterator[str]:
|
|
||||||
...
|
|
||||||
|
|
||||||
def ls(self, path, detail=False):
|
|
||||||
yield from self.fs.ls(path, detail=detail)
|
|
||||||
|
|
||||||
def find(self, path, prefix=None):
|
|
||||||
yield from self.fs.find(path)
|
|
||||||
|
|
||||||
def move(self, from_info: AnyFSPath, to_info: AnyFSPath) -> None:
|
|
||||||
self.fs.move(from_info, to_info)
|
|
||||||
|
|
||||||
def remove(self, path: AnyFSPath) -> None:
|
|
||||||
self.fs.rm_file(path)
|
|
||||||
|
|
||||||
def info(self, path: AnyFSPath) -> "Entry":
|
|
||||||
return self.fs.info(path)
|
|
||||||
|
|
||||||
def makedirs(self, path: AnyFSPath, **kwargs) -> None:
|
|
||||||
self.fs.makedirs(path, exist_ok=kwargs.pop("exist_ok", True))
|
|
||||||
|
|
||||||
def put_file(
|
|
||||||
self,
|
|
||||||
from_file: AnyFSPath,
|
|
||||||
to_info: AnyFSPath,
|
|
||||||
callback: Any = DEFAULT_CALLBACK,
|
|
||||||
**kwargs,
|
|
||||||
) -> None:
|
|
||||||
self.fs.put_file(from_file, to_info, callback=callback, **kwargs)
|
|
||||||
self.fs.invalidate_cache(self.path.parent(to_info))
|
|
||||||
|
|
||||||
def get_file(
|
|
||||||
self,
|
|
||||||
from_info: AnyFSPath,
|
|
||||||
to_info: AnyFSPath,
|
|
||||||
callback: Any = DEFAULT_CALLBACK,
|
|
||||||
**kwargs,
|
|
||||||
) -> None:
|
|
||||||
self.fs.get_file(from_info, to_info, callback=callback, **kwargs)
|
|
||||||
|
|
||||||
def upload_fobj(self, fobj: IO, to_info: AnyFSPath, **kwargs) -> None:
|
|
||||||
self.makedirs(self.path.parent(to_info))
|
|
||||||
with self.open(to_info, "wb") as fdest:
|
|
||||||
shutil.copyfileobj(
|
|
||||||
fobj, fdest, length=getattr(fdest, "blocksize", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=abstract-method
|
|
||||||
class ObjectFSWrapper(FSSpecWrapper):
|
|
||||||
TRAVERSE_PREFIX_LEN = 3
|
|
||||||
|
|
||||||
def makedirs(self, path: AnyFSPath, **kwargs) -> None:
|
|
||||||
# For object storages make this method a no-op. The original
|
|
||||||
# fs.makedirs() method will only check if the bucket exists
|
|
||||||
# and create if it doesn't though we don't want to support
|
|
||||||
# that behavior, and the check will cost some time so we'll
|
|
||||||
# simply ignore all mkdir()/makedirs() calls.
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _isdir(self, path: AnyFSPath) -> bool:
|
|
||||||
# Directory in object storages are interpreted differently
|
|
||||||
# among different fsspec providers, so this logic is a temporary
|
|
||||||
# measure for us to adapt as of now. It checks whether it is a
|
|
||||||
# directory (as in a prefix with contents) or whether it is an empty
|
|
||||||
# file where it's name ends with a forward slash
|
|
||||||
|
|
||||||
entry = self.info(path)
|
|
||||||
return entry["type"] == "directory" or (
|
|
||||||
entry["size"] == 0
|
|
||||||
and entry["type"] == "file"
|
|
||||||
and entry["name"].endswith("/")
|
|
||||||
)
|
|
||||||
|
|
||||||
def find(self, path, prefix=None):
|
|
||||||
if prefix:
|
|
||||||
with_prefix = self.path.parent(path)
|
|
||||||
files = self.fs.find(with_prefix, prefix=self.path.parts(path)[-1])
|
|
||||||
else:
|
|
||||||
with_prefix = path
|
|
||||||
files = self.fs.find(path)
|
|
||||||
|
|
||||||
# When calling find() on a file, it returns the same file in a list.
|
|
||||||
# For object-based storages, the same behavior applies to empty
|
|
||||||
# directories since they are represented as files. This condition
|
|
||||||
# checks whether we should yield an empty list (if it is an empty
|
|
||||||
# directory) or just yield the file itself.
|
|
||||||
if len(files) == 1 and files[0] == with_prefix and self.isdir(path):
|
|
||||||
return None
|
|
||||||
|
|
||||||
yield from files
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=arguments-differ
|
|
||||||
class NoDirectoriesMixin:
|
|
||||||
def isdir(self, *args, **kwargs):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def isfile(self, *args, **kwargs):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def find(self, *args, **kwargs):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def walk(self, *args, **kwargs):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def ls(self, *args, **kwargs):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class CallbackMixin:
|
|
||||||
"""Provides callback support for the filesystem that don't support yet."""
|
|
||||||
|
|
||||||
def put_file(
|
|
||||||
self,
|
|
||||||
from_file,
|
|
||||||
to_info,
|
|
||||||
callback=DEFAULT_CALLBACK,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Add compatibility support for Callback."""
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
self.makedirs(self.path.parent(to_info))
|
|
||||||
size = os.path.getsize(from_file)
|
|
||||||
with open(from_file, "rb") as fobj:
|
|
||||||
callback.set_size(size)
|
|
||||||
wrapped = CallbackIOWrapper(callback.relative_update, fobj)
|
|
||||||
self.upload_fobj(wrapped, to_info)
|
|
||||||
self.fs.invalidate_cache(self.path.parent(to_info))
|
|
||||||
|
|
||||||
def get_file(
|
|
||||||
self,
|
|
||||||
from_info,
|
|
||||||
to_info,
|
|
||||||
callback=DEFAULT_CALLBACK,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
total: int = self.getsize(from_info)
|
|
||||||
if total:
|
|
||||||
callback.set_size(total)
|
|
||||||
|
|
||||||
with self.open(from_info, "rb") as fobj, open(to_info, "wb") as fdest:
|
|
||||||
wrapped = CallbackIOWrapper(callback.relative_update, fobj)
|
|
||||||
shutil.copyfileobj(wrapped, fdest, length=fobj.blocksize)
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from funcy import cached_property, wrap_prop
|
|
||||||
|
|
||||||
from dvc.exceptions import DvcException
|
|
||||||
from dvc.scheme import Schemes
|
|
||||||
from dvc.utils import format_link, tmp_fname
|
|
||||||
|
|
||||||
from .fsspec_wrapper import FSSpecWrapper
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"
|
|
||||||
|
|
||||||
|
|
||||||
class GDriveAuthError(DvcException):
|
|
||||||
def __init__(self, cred_location):
|
|
||||||
|
|
||||||
if cred_location:
|
|
||||||
message = (
|
|
||||||
"GDrive remote auth failed with credentials in '{}'.\n"
|
|
||||||
"Backup first, remove or fix them, and run DVC again.\n"
|
|
||||||
"It should do auth again and refresh the credentials.\n\n"
|
|
||||||
"Details:".format(cred_location)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
message = "Failed to authenticate GDrive remote"
|
|
||||||
|
|
||||||
super().__init__(message)
|
|
||||||
|
|
||||||
|
|
||||||
class GDriveFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
|
||||||
scheme = Schemes.GDRIVE
|
|
||||||
PARAM_CHECKSUM = "checksum"
|
|
||||||
REQUIRES = {"pydrive2": "pydrive2"}
|
|
||||||
# Always prefer traverse for GDrive since API usage quotas are a concern.
|
|
||||||
TRAVERSE_WEIGHT_MULTIPLIER = 1
|
|
||||||
TRAVERSE_PREFIX_LEN = 2
|
|
||||||
|
|
||||||
GDRIVE_CREDENTIALS_DATA = "GDRIVE_CREDENTIALS_DATA"
|
|
||||||
DEFAULT_USER_CREDENTIALS_FILE = "gdrive-user-credentials.json"
|
|
||||||
|
|
||||||
DEFAULT_GDRIVE_CLIENT_ID = "710796635688-iivsgbgsb6uv1fap6635dhvuei09o66c.apps.googleusercontent.com" # noqa: E501
|
|
||||||
DEFAULT_GDRIVE_CLIENT_SECRET = "a1Fz59uTpVNeG_VGuSKDLJXv"
|
|
||||||
|
|
||||||
def __init__(self, **config):
|
|
||||||
from fsspec.utils import infer_storage_options
|
|
||||||
|
|
||||||
super().__init__(**config)
|
|
||||||
|
|
||||||
self.url = config["url"]
|
|
||||||
opts = infer_storage_options(self.url)
|
|
||||||
|
|
||||||
if not opts["host"]:
|
|
||||||
raise DvcException(
|
|
||||||
"Empty GDrive URL '{}'. Learn more at {}".format(
|
|
||||||
config["url"],
|
|
||||||
format_link("https://man.dvc.org/remote/add"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self._bucket = opts["host"]
|
|
||||||
self._path = opts["path"].lstrip("/")
|
|
||||||
self._trash_only = config.get("gdrive_trash_only")
|
|
||||||
self._use_service_account = config.get("gdrive_use_service_account")
|
|
||||||
self._service_account_user_email = config.get(
|
|
||||||
"gdrive_service_account_user_email"
|
|
||||||
)
|
|
||||||
self._service_account_json_file_path = config.get(
|
|
||||||
"gdrive_service_account_json_file_path"
|
|
||||||
)
|
|
||||||
self._client_id = config.get("gdrive_client_id")
|
|
||||||
self._client_secret = config.get("gdrive_client_secret")
|
|
||||||
self._validate_config()
|
|
||||||
|
|
||||||
tmp_dir = config["gdrive_credentials_tmp_dir"]
|
|
||||||
assert tmp_dir
|
|
||||||
|
|
||||||
self._gdrive_service_credentials_path = tmp_fname(
|
|
||||||
os.path.join(tmp_dir, "")
|
|
||||||
)
|
|
||||||
self._gdrive_user_credentials_path = (
|
|
||||||
tmp_fname(os.path.join(tmp_dir, ""))
|
|
||||||
if os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA)
|
|
||||||
else config.get(
|
|
||||||
"gdrive_user_credentials_file",
|
|
||||||
os.path.join(tmp_dir, self.DEFAULT_USER_CREDENTIALS_FILE),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _strip_protocol(cls, path):
|
|
||||||
from fsspec.utils import infer_storage_options
|
|
||||||
|
|
||||||
opts = infer_storage_options(path)
|
|
||||||
|
|
||||||
return "{host}{path}".format(**opts)
|
|
||||||
|
|
||||||
def unstrip_protocol(self, path):
|
|
||||||
return f"gdrive://{path}"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_kwargs_from_urls(urlpath):
|
|
||||||
return {"url": urlpath}
|
|
||||||
|
|
||||||
def _validate_config(self):
|
|
||||||
# Validate Service Account configuration
|
|
||||||
if (
|
|
||||||
self._use_service_account
|
|
||||||
and not self._service_account_json_file_path
|
|
||||||
):
|
|
||||||
raise DvcException(
|
|
||||||
"To use service account, set "
|
|
||||||
"`gdrive_service_account_json_file_path`, and optionally"
|
|
||||||
"`gdrive_service_account_user_email` in DVC config\n"
|
|
||||||
"Learn more at {}".format(
|
|
||||||
format_link("https://man.dvc.org/remote/modify")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate OAuth 2.0 Client ID configuration
|
|
||||||
if not self._use_service_account:
|
|
||||||
if bool(self._client_id) != bool(self._client_secret):
|
|
||||||
raise DvcException(
|
|
||||||
"Please specify GDrive's client ID and secret in "
|
|
||||||
"DVC config or omit both to use the defaults.\n"
|
|
||||||
"Learn more at {}".format(
|
|
||||||
format_link("https://man.dvc.org/remote/modify")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def credentials_location(self):
|
|
||||||
"""
|
|
||||||
Helper to determine where will GDrive remote read credentials from.
|
|
||||||
Useful for tests, exception messages, etc. Returns either env variable
|
|
||||||
name if it's set or actual path to the credentials file.
|
|
||||||
"""
|
|
||||||
if os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA):
|
|
||||||
return GDriveFileSystem.GDRIVE_CREDENTIALS_DATA
|
|
||||||
if os.path.exists(self._gdrive_user_credentials_path):
|
|
||||||
return self._gdrive_user_credentials_path
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _validate_credentials(auth, settings):
|
|
||||||
"""
|
|
||||||
Detects discrepancy in DVC config and cached credentials file.
|
|
||||||
Usually happens when a second remote is added and it is using
|
|
||||||
the same credentials default file. Or when someones decides to change
|
|
||||||
DVC config client id or secret but forgets to remove the cached
|
|
||||||
credentials file.
|
|
||||||
"""
|
|
||||||
if not os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA):
|
|
||||||
if (
|
|
||||||
settings["client_config"]["client_id"]
|
|
||||||
!= auth.credentials.client_id
|
|
||||||
or settings["client_config"]["client_secret"]
|
|
||||||
!= auth.credentials.client_secret
|
|
||||||
):
|
|
||||||
logger.warning(
|
|
||||||
"Client ID and secret configured do not match the "
|
|
||||||
"actual ones used\nto access the remote. Do you "
|
|
||||||
"use multiple GDrive remotes and forgot to\nset "
|
|
||||||
"`gdrive_user_credentials_file` for one or more of them? "
|
|
||||||
"Learn more at\n{}.\n".format(
|
|
||||||
format_link("https://man.dvc.org/remote/modify")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@wrap_prop(threading.RLock())
|
|
||||||
@cached_property
|
|
||||||
def fs(self):
|
|
||||||
from pydrive2.auth import GoogleAuth
|
|
||||||
from pydrive2.fs import GDriveFileSystem as _GDriveFileSystem
|
|
||||||
|
|
||||||
temporary_save_path = self._gdrive_user_credentials_path
|
|
||||||
is_credentials_temp = os.getenv(
|
|
||||||
GDriveFileSystem.GDRIVE_CREDENTIALS_DATA
|
|
||||||
)
|
|
||||||
if self._use_service_account:
|
|
||||||
temporary_save_path = self._gdrive_service_credentials_path
|
|
||||||
|
|
||||||
if is_credentials_temp:
|
|
||||||
with open(temporary_save_path, "w", encoding="utf-8") as cred_file:
|
|
||||||
cred_file.write(
|
|
||||||
os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA)
|
|
||||||
)
|
|
||||||
|
|
||||||
auth_settings = {
|
|
||||||
"client_config_backend": "settings",
|
|
||||||
"save_credentials": True,
|
|
||||||
"save_credentials_backend": "file",
|
|
||||||
"save_credentials_file": self._gdrive_user_credentials_path,
|
|
||||||
"get_refresh_token": True,
|
|
||||||
"oauth_scope": [
|
|
||||||
"https://www.googleapis.com/auth/drive",
|
|
||||||
"https://www.googleapis.com/auth/drive.appdata",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
if self._use_service_account:
|
|
||||||
auth_settings["service_config"] = {
|
|
||||||
"client_user_email": self._service_account_user_email,
|
|
||||||
"client_json_file_path": self._service_account_json_file_path,
|
|
||||||
}
|
|
||||||
if is_credentials_temp:
|
|
||||||
auth_settings["service_config"][
|
|
||||||
"client_json_file_path"
|
|
||||||
] = temporary_save_path
|
|
||||||
|
|
||||||
else:
|
|
||||||
auth_settings["client_config"] = {
|
|
||||||
"client_id": self._client_id or self.DEFAULT_GDRIVE_CLIENT_ID,
|
|
||||||
"client_secret": self._client_secret
|
|
||||||
or self.DEFAULT_GDRIVE_CLIENT_SECRET,
|
|
||||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
||||||
"token_uri": "https://oauth2.googleapis.com/token",
|
|
||||||
"revoke_uri": "https://oauth2.googleapis.com/revoke",
|
|
||||||
"redirect_uri": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
GoogleAuth.DEFAULT_SETTINGS.update(auth_settings)
|
|
||||||
|
|
||||||
# Pass non existent settings path to force DEFAULT_SETTINGS loadings
|
|
||||||
gauth = GoogleAuth(settings_file="")
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.debug(
|
|
||||||
"GDrive remote auth with config '{}'.".format(
|
|
||||||
GoogleAuth.DEFAULT_SETTINGS
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if self._use_service_account:
|
|
||||||
gauth.ServiceAuth()
|
|
||||||
else:
|
|
||||||
gauth.CommandLineAuth()
|
|
||||||
GDriveFileSystem._validate_credentials(gauth, auth_settings)
|
|
||||||
|
|
||||||
# Handle AuthenticationError, RefreshError and other auth failures
|
|
||||||
# It's hard to come up with a narrow exception, since PyDrive throws
|
|
||||||
# a lot of different errors - broken credentials file, refresh token
|
|
||||||
# expired, flow failed, etc.
|
|
||||||
except Exception as exc:
|
|
||||||
raise GDriveAuthError(self.credentials_location) from exc
|
|
||||||
finally:
|
|
||||||
if is_credentials_temp:
|
|
||||||
os.remove(temporary_save_path)
|
|
||||||
|
|
||||||
return _GDriveFileSystem(
|
|
||||||
f"{self._bucket}/{self._path}",
|
|
||||||
gauth,
|
|
||||||
trash_only=self._trash_only,
|
|
||||||
)
|
|
||||||
|
|
||||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
|
||||||
self.makedirs(os.path.dirname(to_info))
|
|
||||||
return self.fs.upload_fobj(fobj, to_info, **kwargs)
|
|
||||||
+10
-25
@@ -1,10 +1,9 @@
|
|||||||
import os
|
|
||||||
import threading
|
import threading
|
||||||
from typing import TYPE_CHECKING, Any, Callable
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from funcy import cached_property, wrap_prop
|
from funcy import cached_property, wrap_prop
|
||||||
|
|
||||||
from .fsspec_wrapper import AnyFSPath, FSSpecWrapper
|
from . import FileSystem
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from scmrepo.fs import GitFileSystem as FsspecGitFileSystem
|
from scmrepo.fs import GitFileSystem as FsspecGitFileSystem
|
||||||
@@ -12,11 +11,10 @@ if TYPE_CHECKING:
|
|||||||
from scmrepo.git.objects import GitTrie
|
from scmrepo.git.objects import GitTrie
|
||||||
|
|
||||||
|
|
||||||
class GitFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
class GitFileSystem(FileSystem): # pylint:disable=abstract-method
|
||||||
"""Proxies the repo file access methods to Git objects"""
|
"""Proxies the repo file access methods to Git objects"""
|
||||||
|
|
||||||
sep = os.sep
|
protocol = "local"
|
||||||
scheme = "local"
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -47,26 +45,13 @@ class GitFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
|||||||
|
|
||||||
return FsspecGitFileSystem(**self.fs_args)
|
return FsspecGitFileSystem(**self.fs_args)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def path(self):
|
||||||
|
return self.fs.path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rev(self) -> str:
|
def rev(self) -> str:
|
||||||
return self.fs.rev
|
return self.fs.rev
|
||||||
|
|
||||||
def isexec(self, path: AnyFSPath) -> bool:
|
def ls(self, path, detail=True, **kwargs):
|
||||||
from dvc.utils import is_exec
|
return self.fs.ls(path, detail=detail, **kwargs) or []
|
||||||
|
|
||||||
try:
|
|
||||||
return is_exec(self.info(path)["mode"])
|
|
||||||
except FileNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def isfile(self, path: AnyFSPath) -> bool:
|
|
||||||
return self.fs.isfile(path)
|
|
||||||
|
|
||||||
def walk(
|
|
||||||
self,
|
|
||||||
top: AnyFSPath,
|
|
||||||
topdown: bool = True,
|
|
||||||
onerror: Callable[[OSError], None] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
):
|
|
||||||
return self.fs.walk(top, topdown=topdown, onerror=onerror, **kwargs)
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import threading
|
|
||||||
|
|
||||||
from funcy import cached_property, wrap_prop
|
|
||||||
|
|
||||||
from dvc.scheme import Schemes
|
|
||||||
|
|
||||||
# pylint:disable=abstract-method
|
|
||||||
from .fsspec_wrapper import CallbackMixin, ObjectFSWrapper
|
|
||||||
|
|
||||||
|
|
||||||
class GSFileSystem(CallbackMixin, ObjectFSWrapper):
|
|
||||||
scheme = Schemes.GS
|
|
||||||
REQUIRES = {"gcsfs": "gcsfs"}
|
|
||||||
PARAM_CHECKSUM = "etag"
|
|
||||||
|
|
||||||
def _prepare_credentials(self, **config):
|
|
||||||
login_info = {"consistency": None}
|
|
||||||
login_info["project"] = config.get("projectname")
|
|
||||||
login_info["token"] = config.get("credentialpath")
|
|
||||||
return login_info
|
|
||||||
|
|
||||||
@wrap_prop(threading.Lock())
|
|
||||||
@cached_property
|
|
||||||
def fs(self):
|
|
||||||
from gcsfs import GCSFileSystem
|
|
||||||
|
|
||||||
return GCSFileSystem(**self.fs_args)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user