mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4c0946f65 | ||
|
|
e35fb76dac | ||
|
|
722839dda6 | ||
|
|
d5533348f7 | ||
|
|
5d2ca11557 | ||
|
|
009b238807 | ||
|
|
43b371c43a | ||
|
|
d1160a28e0 | ||
|
|
bc86fa0643 | ||
|
|
4067a86926 | ||
|
|
9d1d51caa7 | ||
|
|
761422638f | ||
|
|
d01e2355d4 | ||
|
|
aa94bfd66b | ||
|
|
b9b9f91084 | ||
|
|
8a382f51fd | ||
|
|
b235487ecc | ||
|
|
502b3df6c9 | ||
|
|
5c2fd67e6d | ||
|
|
ed34c0f0bd | ||
|
|
7c60dfd3c5 | ||
|
|
225814fdde | ||
|
|
ca0ad239f7 | ||
|
|
f57ac609e9 | ||
|
|
cc4f505f99 | ||
|
|
4d011718a6 | ||
|
|
9c89841637 | ||
|
|
92cae74512 | ||
|
|
73dad4780f | ||
|
|
65ba9ba527 | ||
|
|
7806afd4ad | ||
|
|
a9d0ac2ba9 | ||
|
|
41df96fcdb | ||
|
|
53ec0fa41d | ||
|
|
41194b7975 | ||
|
|
ea5f7d40c1 | ||
|
|
eb06351062 | ||
|
|
ca3704997b | ||
|
|
f1764bdc77 | ||
|
|
eb212a7ba3 | ||
|
|
4c8f539c56 | ||
|
|
45aefc8423 | ||
|
|
67a747a356 | ||
|
|
a5dda1b8ad | ||
|
|
dd4035bc05 | ||
|
|
f69b3533e4 | ||
|
|
1b2291ca83 | ||
|
|
4de4f0fd65 | ||
|
|
2ef2caafd3 | ||
|
|
adcefbec66 | ||
|
|
2102ba8465 | ||
|
|
d736d0a3ef | ||
|
|
0d1f1f6436 | ||
|
|
63de16ced1 | ||
|
|
2f6c308c84 | ||
|
|
c8d180d2b0 | ||
|
|
d69972d199 | ||
|
|
eb1ed40228 | ||
|
|
120289128e | ||
|
|
a607e7b3d7 | ||
|
|
86e199c6f2 | ||
|
|
8661abf224 | ||
|
|
541def388b | ||
|
|
58eb145b6a | ||
|
|
16465e0b63 | ||
|
|
6614efd35a | ||
|
|
2becfd9344 | ||
|
|
b7b6642428 | ||
|
|
fea2e6bb30 | ||
|
|
bfa5b1a0c3 | ||
|
|
24d9a05d69 | ||
|
|
1f31a9c696 | ||
|
|
097e48fcc9 | ||
|
|
453a8f9516 | ||
|
|
9f59d8652e | ||
|
|
b1834f34e7 | ||
|
|
536273ab1e | ||
|
|
5cff4891fd | ||
|
|
e29da6eae1 | ||
|
|
046ebb05dd | ||
|
|
36ec25ffd7 | ||
|
|
6e6d9253d6 | ||
|
|
910c232077 | ||
|
|
c90947f33c | ||
|
|
4ee4f137f2 | ||
|
|
a750a75016 | ||
|
|
cf976f17ae | ||
|
|
7041fa5f42 | ||
|
|
d898877175 | ||
|
|
1ca1d78b38 | ||
|
|
da6ebdbf8c | ||
|
|
e995492095 | ||
|
|
276d499c9b | ||
|
|
fd4696cc50 | ||
|
|
90b9193250 | ||
|
|
77e35145a9 | ||
|
|
49c7c55ba3 | ||
|
|
f28d5ee9e0 | ||
|
|
621fe07a3b | ||
|
|
8323923ce9 | ||
|
|
04b50c5cd0 | ||
|
|
b20a59d748 | ||
|
|
bf2f08da09 | ||
|
|
1835c8c6e9 | ||
|
|
7e1bb07f10 | ||
|
|
92dc1f31f5 | ||
|
|
e16c0c8b2f | ||
|
|
33e7645603 | ||
|
|
c827654fe7 | ||
|
|
b8c666b1b7 | ||
|
|
fd73b389c6 | ||
|
|
0433565f1f | ||
|
|
b5449503f0 | ||
|
|
fa14507bc8 | ||
|
|
21458796ed | ||
|
|
a6e49a72de | ||
|
|
97348cfba7 | ||
|
|
c5e3d42899 | ||
|
|
c6c2ce130a | ||
|
|
be975ad9eb | ||
|
|
d0aa1ce776 | ||
|
|
5ec4fa473c | ||
|
|
5fd54679c7 | ||
|
|
5c1487e2ec | ||
|
|
30517f6676 | ||
|
|
bbb77360e9 | ||
|
|
1af150a5e9 | ||
|
|
cf59d801ce | ||
|
|
b733dea4c2 | ||
|
|
2ed677057d | ||
|
|
0597311439 | ||
|
|
fc8ec6aa95 | ||
|
|
3dca69b678 | ||
|
|
4c0bb8d316 | ||
|
|
1cb371d4d7 | ||
|
|
c96795dcfb | ||
|
|
8f768addf9 | ||
|
|
af1fb722c8 | ||
|
|
8b1c1a31f3 | ||
|
|
262419ef35 | ||
|
|
265fc581c0 | ||
|
|
80f414c19d | ||
|
|
2cd33e10ac | ||
|
|
603f5fdba1 | ||
|
|
528eab0418 | ||
|
|
ed664143b4 | ||
|
|
21a2504bcb | ||
|
|
d0291e8039 | ||
|
|
358c5e0c39 | ||
|
|
9d7fba5c61 | ||
|
|
a1e339b4fa | ||
|
|
6c58d438b3 | ||
|
|
0c709b6ec1 | ||
|
|
0aeb7a8662 | ||
|
|
46f3157f43 | ||
|
|
36fd69be21 | ||
|
|
3759535d43 | ||
|
|
2c4365513e | ||
|
|
26152292aa | ||
|
|
3e0f2bafbb | ||
|
|
78637713cf | ||
|
|
f9104275f3 | ||
|
|
bce3b5d2a1 | ||
|
|
6ace5ed380 | ||
|
|
6543580100 | ||
|
|
a1b891ce01 | ||
|
|
3a85326df3 | ||
|
|
355ebc72eb | ||
|
|
5a6f834088 | ||
|
|
e98bf38d4c | ||
|
|
192c9cc577 | ||
|
|
0e38525d8d | ||
|
|
8acd943d52 | ||
|
|
79c88008f8 | ||
|
|
9c9ca39fb4 | ||
|
|
89109fafcf | ||
|
|
aa19e24670 | ||
|
|
629223cbc2 | ||
|
|
96449f157c | ||
|
|
8cb2646023 | ||
|
|
25313f5d82 | ||
|
|
182728d674 | ||
|
|
f245567613 | ||
|
|
4163c8f57d | ||
|
|
6532258e4a | ||
|
|
eb45cda5d0 | ||
|
|
abd4f4a2a8 | ||
|
|
f41e535857 | ||
|
|
1989c0fe98 | ||
|
|
3ddd4b87fc | ||
|
|
e089ee0365 | ||
|
|
02f7fcae73 | ||
|
|
cdba65f27e | ||
|
|
3959c51373 | ||
|
|
90aaf5a296 | ||
|
|
45c5c83699 | ||
|
|
ecb71c8263 | ||
|
|
e6e3912da0 | ||
|
|
7f6890ff25 | ||
|
|
99daf5d113 | ||
|
|
83ffb8803e | ||
|
|
ace8464acb | ||
|
|
0dbdfd2b9b | ||
|
|
6d4251798a | ||
|
|
11d031935d | ||
|
|
64beb1578a | ||
|
|
f888544554 | ||
|
|
1e538ed87b | ||
|
|
e69deaa132 | ||
|
|
be0a1a4a12 | ||
|
|
004f49744e | ||
|
|
45181504ea | ||
|
|
915dd5062e | ||
|
|
d08c85f7d1 | ||
|
|
2342099bd8 | ||
|
|
06d8e3c639 | ||
|
|
195e67c0c1 | ||
|
|
c75a5583b3 | ||
|
|
981baa8c30 | ||
|
|
aa517bc90e | ||
|
|
c74dc20b97 | ||
|
|
1ba3944944 | ||
|
|
8620b6e5af | ||
|
|
6b6084a848 | ||
|
|
967dd5ea2b | ||
|
|
ad39bf14ed | ||
|
|
01416df5a4 | ||
|
|
eb592e7e33 | ||
|
|
2db07b047e | ||
|
|
8ecc66ae9a | ||
|
|
dfa5f4d9a0 | ||
|
|
75952c4e36 | ||
|
|
31b0b8de73 | ||
|
|
fae47e6122 | ||
|
|
ac3eef439f | ||
|
|
b044bcce5e | ||
|
|
a251cffdb5 | ||
|
|
37858be02f | ||
|
|
9b7240e9e0 | ||
|
|
e362438f03 | ||
|
|
6c79f6bbfc | ||
|
|
58cd1cbc09 | ||
|
|
96e3b48afd | ||
|
|
1f03c40dbe | ||
|
|
e93e8f05a3 | ||
|
|
0f1326e547 | ||
|
|
2a04263f02 | ||
|
|
348446050e | ||
|
|
7e1036b30d | ||
|
|
a15dbc8e03 | ||
|
|
4e02197245 | ||
|
|
4ab7ef06e5 | ||
|
|
dae2faf36d | ||
|
|
58f10a4661 | ||
|
|
64e787fa02 | ||
|
|
c7e2ec9707 | ||
|
|
a4cc198ac7 | ||
|
|
d203bf9024 | ||
|
|
baeec8ec2d | ||
|
|
9c689a3b51 | ||
|
|
e1bf418e72 | ||
|
|
b6d8c07d4d | ||
|
|
b814531c84 | ||
|
|
3b3cd0a0b2 | ||
|
|
b2101a85ad | ||
|
|
ba760c56e5 | ||
|
|
787c72ea18 | ||
|
|
a2b7a59322 | ||
|
|
ec090c5680 | ||
|
|
58f74ae543 | ||
|
|
2e8bcbf719 | ||
|
|
10f523430b | ||
|
|
5563a595d5 | ||
|
|
dd3deb6cbd | ||
|
|
7841020b2b | ||
|
|
1c394986db | ||
|
|
870935ebdc | ||
|
|
1be82fa56d | ||
|
|
ce0f6c187e | ||
|
|
559ec392f6 | ||
|
|
c71a4082e6 | ||
|
|
ce269ffa58 | ||
|
|
61d0893d7c | ||
|
|
eef5fb9b37 | ||
|
|
f03d555747 | ||
|
|
a91993616f | ||
|
|
3c570a2177 | ||
|
|
3312a90949 | ||
|
|
32bebc0246 | ||
|
|
35648134f7 | ||
|
|
df09e522ec | ||
|
|
40f7f34889 | ||
|
|
cf52efb8c3 | ||
|
|
8be71be540 | ||
|
|
071a5b7ba4 | ||
|
|
ed6cf093a8 | ||
|
|
7c3c47b4a2 | ||
|
|
3a2259821b | ||
|
|
649ee67e4b | ||
|
|
7cdb3b0ae2 | ||
|
|
ecdc44e010 | ||
|
|
24e10e0278 | ||
|
|
7c8e6e3156 | ||
|
|
d2812fa48f | ||
|
|
086d51d9de | ||
|
|
1d8b2e5fa6 | ||
|
|
894f530d22 | ||
|
|
1b9d40960f | ||
|
|
696fabc745 | ||
|
|
780dc72301 | ||
|
|
1ca3ef14a3 | ||
|
|
a6c89cac8d | ||
|
|
593f1064cf | ||
|
|
35bb261b89 | ||
|
|
fe6d543c1f | ||
|
|
4351d32de4 | ||
|
|
efc2d7ad3d | ||
|
|
7c65739e7e | ||
|
|
cc4816f652 | ||
|
|
faa1bdfbce | ||
|
|
006b6f3513 | ||
|
|
1bc4c1dceb | ||
|
|
531b2110cf | ||
|
|
c1917ed751 | ||
|
|
43195a806d | ||
|
|
d41512c85c | ||
|
|
e9fb0b92aa | ||
|
|
5485275b92 | ||
|
|
1f80f2f2ed | ||
|
|
6b6dc01ac4 | ||
|
|
91d3ff0f69 | ||
|
|
701f48197e | ||
|
|
da3bbccf66 | ||
|
|
40484e8d57 | ||
|
|
48b56c83dc | ||
|
|
f8bbf20b77 | ||
|
|
cd2c4d0caf | ||
|
|
9b329d01bf | ||
|
|
5c442cb867 | ||
|
|
2d5c15bee1 | ||
|
|
7868c1301c | ||
|
|
dfe76c54d7 | ||
|
|
392c15e1d4 | ||
|
|
121d56b739 | ||
|
|
66406e6c99 | ||
|
|
8399bf612a | ||
|
|
6c1b786564 | ||
|
|
d1c5fcc1b8 | ||
|
|
4ee3271b0c | ||
|
|
dae8515933 | ||
|
|
f02dd83e53 | ||
|
|
a2066d75f9 | ||
|
|
283d8245e1 | ||
|
|
c52b12ac2a | ||
|
|
440cb9d2bc | ||
|
|
2d820cccad | ||
|
|
30c3e3e4d4 | ||
|
|
58029e9719 | ||
|
|
990e834f1d | ||
|
|
33a2449c91 | ||
|
|
e2ce95e17e | ||
|
|
53f9953cd1 | ||
|
|
e6419b939d | ||
|
|
37ccc5e8d8 | ||
|
|
8312eb9b42 | ||
|
|
c8745dd580 | ||
|
|
1cf8b5e582 | ||
|
|
12955da3c3 | ||
|
|
e73cc9b40a | ||
|
|
b56f6b69dd | ||
|
|
54a6709d99 | ||
|
|
e4a5a2aec5 | ||
|
|
16ecd53b71 | ||
|
|
612f3039e0 | ||
|
|
17f523bf86 | ||
|
|
a0bf2d997a | ||
|
|
8ed9b911db | ||
|
|
ef22b1d77e | ||
|
|
aaa825a9a8 | ||
|
|
2a40a55bb4 | ||
|
|
3aa9f51e36 | ||
|
|
1cfdc22549 | ||
|
|
2b889baa5e | ||
|
|
8589aa4001 | ||
|
|
8cbc4c43f1 | ||
|
|
417c327143 | ||
|
|
204eebaf15 | ||
|
|
3a6fd75d98 | ||
|
|
351b952d5f | ||
|
|
fe7db32311 | ||
|
|
0bd34c8977 | ||
|
|
48a63ca588 | ||
|
|
4459d6ac57 | ||
|
|
08a50ee36e | ||
|
|
f1ea64c4ef | ||
|
|
7ede45af2d | ||
|
|
51db1765ee | ||
|
|
9101fce761 | ||
|
|
3f842a205f | ||
|
|
77e900fed2 | ||
|
|
dde456c923 | ||
|
|
cb9d3529af | ||
|
|
876c10fcf5 | ||
|
|
93eea0cd42 | ||
|
|
25fd274090 | ||
|
|
ddfa607437 | ||
|
|
c5a3566e49 | ||
|
|
e7d4f5b24e | ||
|
|
1d2892efac | ||
|
|
83835afa69 | ||
|
|
4c5d052d8c | ||
|
|
e4d514c990 | ||
|
|
baeecd1972 | ||
|
|
dbd098506c | ||
|
|
4779c248f8 | ||
|
|
2cf3a12e46 | ||
|
|
78fb5275e5 | ||
|
|
ccbe2de89e | ||
|
|
54e39e4abd | ||
|
|
59a4c4f67c | ||
|
|
1a7ae16f69 | ||
|
|
286f4fda3e | ||
|
|
d2c63bffcb | ||
|
|
2b41305c51 | ||
|
|
546becddaf | ||
|
|
c6015a05a3 | ||
|
|
c0754ee5b3 | ||
|
|
4db2e89938 | ||
|
|
4a6f3ed64a | ||
|
|
9f0c4350bd | ||
|
|
a91528c3c9 | ||
|
|
b80d84e689 | ||
|
|
2a2c02c184 | ||
|
|
35b648bbcf | ||
|
|
96ac321dd5 | ||
|
|
50fd8ca15d | ||
|
|
184cc35cf0 | ||
|
|
81da91a973 | ||
|
|
ad5b91e51a | ||
|
|
391cf80e7b | ||
|
|
8d13486048 | ||
|
|
37761a2627 | ||
|
|
968a8fde99 | ||
|
|
3043b6588f | ||
|
|
47208533a2 | ||
|
|
09ccd9c7e4 | ||
|
|
cd9dd0a312 | ||
|
|
6fbbbeccc5 | ||
|
|
c34cbf63af | ||
|
|
b76aad8d56 | ||
|
|
68bf095eae | ||
|
|
ea6e01d0e0 | ||
|
|
19038b32bd | ||
|
|
648edd71eb | ||
|
|
0c95cde712 | ||
|
|
e218431f64 | ||
|
|
d3182e0dd9 | ||
|
|
e040e875dc | ||
|
|
303da84b18 | ||
|
|
03bfdf2663 | ||
|
|
8edaef0103 | ||
|
|
fd8a8c2729 | ||
|
|
30337bf0ba | ||
|
|
5426ca6f79 | ||
|
|
a89c25700f | ||
|
|
0e0bb8ad76 | ||
|
|
1de37556d5 | ||
|
|
098685b8f3 | ||
|
|
8f549c8a8d | ||
|
|
e56d97bb28 | ||
|
|
07b72c20e9 | ||
|
|
592f251481 | ||
|
|
890c0020ed | ||
|
|
7ce0b1cba2 | ||
|
|
b8a9c37af5 | ||
|
|
1af63acae5 | ||
|
|
d68599a5f7 | ||
|
|
7ccf35b47e | ||
|
|
4f49023a08 | ||
|
|
d7ea7c358d | ||
|
|
3f0da8f6cc | ||
|
|
b7b7eff4a3 | ||
|
|
ddeb32ed33 | ||
|
|
c7be921f44 | ||
|
|
c95c3f0f7a | ||
|
|
be824ede0d | ||
|
|
cbe46cccd8 | ||
|
|
49b5157abe | ||
|
|
6f87c14bf9 | ||
|
|
0f80d16ba7 | ||
|
|
4b23b4f8ae | ||
|
|
446871d84f | ||
|
|
99bbe15980 | ||
|
|
320771551b | ||
|
|
1676d1f344 | ||
|
|
c8ae2895f0 | ||
|
|
ed1ab7fa75 | ||
|
|
73497dae9e | ||
|
|
eff775c760 | ||
|
|
e73d662686 | ||
|
|
86a6fb104a | ||
|
|
3b097dcccd | ||
|
|
e910dac0dc | ||
|
|
17a71d6011 | ||
|
|
5a6ccc8027 | ||
|
|
6dccfcb3f9 | ||
|
|
2b0c365f35 | ||
|
|
611e2f4311 | ||
|
|
561a375b48 | ||
|
|
e51345e276 | ||
|
|
00e61e18ae | ||
|
|
5490fc00d1 | ||
|
|
f1043676da | ||
|
|
f7ebcbb974 | ||
|
|
c637b963e0 | ||
|
|
9156bb80db | ||
|
|
58295ca0ab | ||
|
|
4176007493 | ||
|
|
9d7883f529 | ||
|
|
93c556acdd | ||
|
|
676b1e47e9 | ||
|
|
0b141ac08a | ||
|
|
b4db1e06a1 | ||
|
|
0d0fff6ddc | ||
|
|
6c8c26f741 | ||
|
|
f68a12558a | ||
|
|
a8a5b29fb7 | ||
|
|
037e2e54f0 | ||
|
|
ae80889477 | ||
|
|
e1acab5113 | ||
|
|
d84196db62 | ||
|
|
80c2815e67 | ||
|
|
e42a01a3dd |
+1
-10
@@ -1,12 +1,3 @@
|
||||
/state
|
||||
/lock
|
||||
/config.local
|
||||
/updater
|
||||
/updater.lock
|
||||
/state-journal
|
||||
/state-wal
|
||||
/cache
|
||||
/pkg
|
||||
/repos
|
||||
/tmp
|
||||
/experiments
|
||||
/cache
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
['remote "dvc"']
|
||||
url = https://remote.dvc.org/dvc
|
||||
[core]
|
||||
remote = dvc
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
[flake8]
|
||||
ignore=
|
||||
max_line_length = 88
|
||||
extend-ignore=
|
||||
# Whitespace before ':'
|
||||
E203
|
||||
# Too many leading '#' for block comment
|
||||
E266
|
||||
# Line break occurred before a binary operator
|
||||
W503
|
||||
# unindexed parameters in the str.format, see:
|
||||
# https://pypi.org/project/flake8-string-format/
|
||||
P1
|
||||
# use `!r` instead of surrounding by quotes
|
||||
B028
|
||||
max_line_length = 88
|
||||
max-complexity = 10
|
||||
select = B,C,E,F,W,T4,B902,T,P,TYP
|
||||
show_source = true
|
||||
count = true
|
||||
min_python_version = 3.8.0
|
||||
per-file-ignores =
|
||||
dvc/testing/benchmarks/conftest.py:F401
|
||||
dvc/testing/conftest.py:F401
|
||||
dvc/testing/plugin.py:F401
|
||||
tests/conftest.py:F401
|
||||
tests/func/experiments/executor/test_ssh.py:F401
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: Epic/Story Issue Template
|
||||
about: Template for "top" level issues - Epics (>2 weeks) / Stories (<2 weeks)
|
||||
title: 'Epic: New Feature'
|
||||
labels: epic
|
||||
assignees:
|
||||
|
||||
---
|
||||
|
||||
## Summary / Background
|
||||
What do you want to achieve, why? business context
|
||||
...
|
||||
|
||||
## Scope
|
||||
|
||||
What will be impacted and what won't be?
|
||||
What needs implementation and what is invariant?
|
||||
e.g.
|
||||
- user should be able to run workflow X from UI
|
||||
- enable workflow Y, Z from CLI
|
||||
|
||||
## Assumptions
|
||||
Product / UX assumptions as well as technical assumptions / limitations
|
||||
e.g.
|
||||
* Support only Python Runtime
|
||||
* Focus on DVC experiments only
|
||||
* Deployment environments don't change often and can be picked up from shared configuration
|
||||
|
||||
## Open Questions
|
||||
e.g.
|
||||
- How should access control work for shared artifacts (workflow X)
|
||||
- Python runtime assumption - is it really valid? in light of <...>
|
||||
|
||||
## Blockers / Dependencies
|
||||
List issues or other conditions / blockers
|
||||
|
||||
## General Approach
|
||||
Invocation example:
|
||||
```shell
|
||||
$ mapper-run task.tar.gz --ray-cluster <ip>:<port>
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### Must have (p1)
|
||||
- [ ] subissue2
|
||||
- [ ] step 2
|
||||
- info
|
||||
- info
|
||||
|
||||
### Optional / followup (p2)
|
||||
- [ ] ⌛ step 3 wip
|
||||
- [ ] step 4
|
||||
|
||||
## Timelines
|
||||
|
||||
Put your estimations here. Update once certainty changes
|
||||
- end of week (Feb 3) for prototype with workflows X, Y
|
||||
- Feb 15 - MVP in production
|
||||
- Low priority followups can be done later
|
||||
@@ -0,0 +1,10 @@
|
||||
pull_request_rules:
|
||||
- name: backport patches to 2.x
|
||||
conditions:
|
||||
- base=main
|
||||
- label=backport
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- "2.x"
|
||||
title: ({{ destination_branch }}) {{ title }}
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Build package
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
pip:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- run: python -m pip install --upgrade pip setuptools_scm build twine
|
||||
- name: Force version for Test PyPI upload
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
||||
run: echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
||||
|
||||
- run: |
|
||||
echo 'PKG = "pip"'>dvc/_build.py
|
||||
python -m build
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.version }}
|
||||
- run: twine check --strict dist/*
|
||||
|
||||
- name: Publish to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
skip-existing: true
|
||||
@@ -2,7 +2,7 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ["main", "2.x"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
name: Build packages
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
permissions:
|
||||
contents: write # for actions/upload-release-asset to upload release asset
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- {pkg: "deb", os: "ubuntu-18.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-18.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-10.15", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
|
||||
|
||||
name: ${{ matrix.pkg }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
pyproject.toml
|
||||
scripts/build-requirements.txt
|
||||
|
||||
- name: Set up Ruby 2.6
|
||||
uses: ruby/setup-ruby@v1
|
||||
if: matrix.pkg != 'exe'
|
||||
with:
|
||||
ruby-version: '2.6'
|
||||
|
||||
- name: Install fpm
|
||||
if: matrix.pkg != 'exe'
|
||||
run: gem install --no-document fpm
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
|
||||
- name: Pull images
|
||||
run: dvc pull
|
||||
|
||||
- name: Build ${{ matrix.pkg }}
|
||||
run: python scripts/build.py ${{ matrix.pkg }}
|
||||
|
||||
- name: Publish ${{ matrix.pkg }}
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/${{ matrix.pkg == 'exe' && 'innosetup' || 'fpm' }}/${{ matrix.asset }}
|
||||
asset_name: ${{ matrix.asset }}
|
||||
asset_content_type: binary/octet-stream
|
||||
|
||||
pip:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install
|
||||
run: pip install --upgrade pip wheel
|
||||
|
||||
- name: Force version for Test PyPI uploads
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
||||
run: |
|
||||
pip install setuptools_scm
|
||||
echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
||||
|
||||
- name: Build packages
|
||||
run: ./scripts/build_package.sh
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.version }}
|
||||
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.TEST_PYPI_TOKEN }}
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
skip_existing: true
|
||||
@@ -2,7 +2,7 @@ name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, 2.x]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '5 1 * * *' # every day at 01:05
|
||||
@@ -17,7 +17,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 50
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -41,9 +41,11 @@ jobs:
|
||||
pip install -e ".[dev]"
|
||||
- uses: pre-commit/[email protected]
|
||||
- name: run tests
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
PYTHONUTF8: 1
|
||||
run: >
|
||||
pytest -nauto --timeout=300 --durations=100
|
||||
pytest -n=logical --timeout=300 --durations=100
|
||||
--cov --cov-report=xml --cov-report=term
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
+13
-19
@@ -23,44 +23,38 @@ repos:
|
||||
- id: mixed-line-ending
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/lovesegfault/beautysh
|
||||
rev: v6.2.1
|
||||
hooks:
|
||||
- id: beautysh
|
||||
args: ["-i", "2"]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
- --py38-plus
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.238'
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.0.282'
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.2
|
||||
rev: v2.2.5
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies: ["tomli"]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-bugbear
|
||||
- flake8-comprehensions
|
||||
- flake8-debugger
|
||||
- flake8-string-format
|
||||
- flake8-typing-imports
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: '1.7.5'
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: ["-c", "pyproject.toml", "--quiet", "--format=custom"]
|
||||
files: ^dvc/
|
||||
additional_dependencies: [".[toml]"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: mypy
|
||||
|
||||
+2
-2
@@ -280,8 +280,8 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: PyPI Downloads
|
||||
|
||||
.. |Packages| image:: https://img.shields.io/github/v/release/iterative/dvc?label=deb|pkg|rpm|exe&logo=GitHub
|
||||
:target: https://github.com/iterative/dvc/releases/latest
|
||||
.. |Packages| image:: https://img.shields.io/badge/deb|pkg|rpm|exe-blue
|
||||
:target: https://dvc.org/doc/install
|
||||
:alt: deb|pkg|rpm|exe
|
||||
|
||||
.. |DOI| image:: https://img.shields.io/badge/DOI-10.5281/zenodo.3677553-blue.svg
|
||||
|
||||
@@ -4,6 +4,7 @@ DVC
|
||||
Make your data science projects reproducible and shareable.
|
||||
"""
|
||||
import dvc.logger
|
||||
from dvc.build import PKG # noqa: F401
|
||||
from dvc.version import __version__, version_tuple # noqa: F401
|
||||
|
||||
dvc.logger.setup()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
|
||||
|
||||
def get_hook_dirs():
|
||||
return [os.path.dirname(__file__)]
|
||||
@@ -1 +1,2 @@
|
||||
# ruff: noqa: N999
|
||||
hiddenimports = ["win32timezone"]
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
# pylint: disable=import-error
|
||||
from PyInstaller.utils.hooks import collect_submodules, is_module_or_submodule
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
from PyInstaller.utils.hooks import copy_metadata # pylint:disable=import-error
|
||||
|
||||
# needed for `dvc doctor` to show dep versions
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
from PyInstaller.utils.hooks import copy_metadata # pylint:disable=import-error
|
||||
|
||||
datas = copy_metadata("flatten-dict")
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
# pylint: disable=import-error
|
||||
from PyInstaller.utils.hooks import collect_submodules
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
# ruff: noqa: N999
|
||||
hiddenimports = ["fsspec.implementations.memory"]
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
from PyInstaller.utils.hooks import copy_metadata # pylint:disable=import-error
|
||||
|
||||
datas = copy_metadata("pydrive2")
|
||||
+66
-15
@@ -198,33 +198,84 @@ def debugtools(args: Optional["Namespace"] = None, **kwargs):
|
||||
def add_debugging_flags(parser):
|
||||
from argparse import SUPPRESS
|
||||
|
||||
parser.add_argument("--cprofile", action="store_true", default=False, help=SUPPRESS)
|
||||
parser.add_argument("--yappi", action="store_true", default=False, help=SUPPRESS)
|
||||
# For detailed info see:
|
||||
# https://github.com/iterative/dvc/wiki/Debugging,-Profiling-and-Benchmarking-DVC
|
||||
args, _ = parser.parse_known_args()
|
||||
verbose = args.verbose
|
||||
|
||||
def debug_help(msg):
|
||||
if verbose:
|
||||
return msg
|
||||
return SUPPRESS
|
||||
|
||||
parser = parser.add_argument_group("debug options")
|
||||
|
||||
parser.add_argument(
|
||||
"--cprofile",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Generate cprofile data for tools like snakeviz / tuna"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cprofile-dump", help=debug_help("Location to dump cprofile file")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yappi",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help(
|
||||
"Generate a callgrind file for use with tools like "
|
||||
"kcachegrind / qcachegrind"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yappi-separate-threads",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=SUPPRESS,
|
||||
help=debug_help("Generate one callgrind file per thread"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--viztracer", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument("--viztracer-depth", type=int, help=SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--viztracer-async", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument("--cprofile-dump", help=SUPPRESS)
|
||||
parser.add_argument("--pdb", action="store_true", default=False, help=SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--instrument", action="store_true", default=False, help=SUPPRESS
|
||||
"--viztracer",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Generate a viztracer file for use with vizviewer"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instrument-open", action="store_true", default=False, help=SUPPRESS
|
||||
"--viztracer-depth",
|
||||
type=int,
|
||||
help=debug_help("Set viztracer maximum stack depth"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--viztracer-async",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Treat async tasks as threads"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pdb",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Drop into the pdb/ipdb debugger on any exception"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instrument",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Use pyinstrument CLI profiler"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instrument-open",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Use pyinstrument web profiler"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show-stack",
|
||||
"--ss",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=SUPPRESS,
|
||||
help=debug_help(
|
||||
r"Use Ctrl+T on macOS or Ctrl+\ on Linux to print the stack "
|
||||
"frame currently executing. Unavailable on Windows."
|
||||
),
|
||||
)
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ def is_enabled():
|
||||
enabled = not os.getenv(DVC_NO_ANALYTICS)
|
||||
if enabled:
|
||||
enabled = to_bool(
|
||||
Config(validate=False).get("core", {}).get("analytics", "true")
|
||||
Config.from_cwd(validate=False).get("core", {}).get("analytics", "true")
|
||||
)
|
||||
|
||||
logger.debug("Analytics is %sabled.", "en" if enabled else "dis")
|
||||
|
||||
+22
-5
@@ -2,6 +2,7 @@ from dataclasses import asdict, dataclass, field, fields
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
|
||||
from funcy import compact
|
||||
from voluptuous import Required
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -16,11 +17,23 @@ class Annotation:
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def update(self, **kwargs) -> "Annotation":
|
||||
for attr, value in kwargs.items():
|
||||
if value and hasattr(self, attr):
|
||||
setattr(self, attr, value)
|
||||
return self
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Artifact:
|
||||
PARAM_PATH: ClassVar[str] = "path"
|
||||
PARAM_DESC: ClassVar[str] = "desc"
|
||||
PARAM_TYPE: ClassVar[str] = "type"
|
||||
PARAM_LABELS: ClassVar[str] = "labels"
|
||||
PARAM_META: ClassVar[str] = "meta"
|
||||
|
||||
path: str
|
||||
desc: Optional[str] = None
|
||||
type: Optional[str] = None # noqa: A003
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
@@ -33,3 +46,7 @@ ANNOTATION_SCHEMA = {
|
||||
Annotation.PARAM_LABELS: [str],
|
||||
Annotation.PARAM_META: object,
|
||||
}
|
||||
ARTIFACT_SCHEMA = {
|
||||
Required(Artifact.PARAM_PATH): str,
|
||||
**ANNOTATION_SCHEMA, # type: ignore[arg-type]
|
||||
}
|
||||
|
||||
+9
-3
@@ -2,14 +2,20 @@ from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .data import open # pylint: disable=redefined-builtin
|
||||
from .data import get_url, read
|
||||
from .experiments import make_checkpoint
|
||||
from .params import params_show
|
||||
from .experiments import exp_save, exp_show
|
||||
from .scm import all_branches, all_commits, all_tags
|
||||
from .show import metrics_show, params_show
|
||||
|
||||
__all__ = [
|
||||
"all_branches",
|
||||
"all_commits",
|
||||
"all_tags",
|
||||
"exp_save",
|
||||
"exp_show",
|
||||
"get_url",
|
||||
"make_checkpoint",
|
||||
"open",
|
||||
"params_show",
|
||||
"metrics_show",
|
||||
"read",
|
||||
"DVCFileSystem",
|
||||
]
|
||||
|
||||
+112
-23
@@ -1,12 +1,30 @@
|
||||
from contextlib import _GeneratorContextManager as GCM
|
||||
from typing import Optional
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from funcy import reraise
|
||||
|
||||
from dvc.exceptions import OutputNotFoundError, PathMissingError
|
||||
from dvc.exceptions import FileMissingError, OutputNotFoundError, PathMissingError
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _wrap_exceptions(repo, url):
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputInExternalRepoError, NoRemoteInExternalRepoError
|
||||
|
||||
try:
|
||||
yield
|
||||
except NoRemoteError as exc:
|
||||
raise NoRemoteInExternalRepoError(url) from exc
|
||||
except OutputNotFoundError as exc:
|
||||
if exc.repo is repo:
|
||||
raise NoOutputInExternalRepoError(exc.output, repo.root_dir, url) from exc
|
||||
raise
|
||||
except FileMissingError as exc:
|
||||
raise PathMissingError(exc.path, url) from exc
|
||||
|
||||
|
||||
def get_url(path, repo=None, rev=None, remote=None):
|
||||
"""
|
||||
Returns the URL to the storage location of a data file or directory tracked
|
||||
@@ -18,19 +36,22 @@ def get_url(path, repo=None, rev=None, remote=None):
|
||||
NOTE: This function does not check for the actual existence of the file or
|
||||
directory in the remote storage.
|
||||
"""
|
||||
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)
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc_data.index import StorageKeyError
|
||||
|
||||
dvc_info = info.get("dvc_info")
|
||||
if not dvc_info:
|
||||
raise OutputNotFoundError(path, repo)
|
||||
|
||||
dvc_repo = info["repo"] # pylint: disable=unsubscriptable-object
|
||||
md5 = dvc_info["md5"]
|
||||
|
||||
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
|
||||
repo_kwargs: Dict[str, Any] = {}
|
||||
if remote:
|
||||
repo_kwargs["config"] = {"core": {"remote": remote}}
|
||||
with Repo.open(
|
||||
repo, rev=rev, subrepos=True, uninitialized=True, **repo_kwargs
|
||||
) as _repo:
|
||||
index, entry = _repo.get_data_index_entry(path)
|
||||
with reraise(
|
||||
(StorageKeyError, ValueError),
|
||||
NoRemoteError(f"no remote specified in {_repo}"),
|
||||
):
|
||||
remote_fs, remote_path = index.storage_map.get_remote(entry)
|
||||
return remote_fs.unstrip_protocol(remote_path)
|
||||
|
||||
|
||||
class _OpenContextManager(GCM):
|
||||
@@ -46,13 +67,15 @@ class _OpenContextManager(GCM):
|
||||
raise AttributeError("dvc.api.open() should be used in a with statement.")
|
||||
|
||||
|
||||
def open( # noqa, pylint: disable=redefined-builtin
|
||||
def open( # noqa: A001, 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,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
remote_config: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
Opens a file tracked in a DVC project.
|
||||
@@ -94,6 +117,11 @@ def open( # noqa, pylint: disable=redefined-builtin
|
||||
Defaults to None.
|
||||
This should only be used in text mode.
|
||||
Mirrors the namesake parameter in builtin `open()`_.
|
||||
config(dict, optional): config to be passed to the DVC repository.
|
||||
Defaults to None.
|
||||
remote_config(dict, optional): remote config to be passed to the DVC
|
||||
repository.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
_OpenContextManager: A context manager that generatse a corresponding
|
||||
@@ -189,25 +217,86 @@ def open( # noqa, pylint: disable=redefined-builtin
|
||||
"rev": rev,
|
||||
"mode": mode,
|
||||
"encoding": encoding,
|
||||
"config": config,
|
||||
"remote_config": remote_config,
|
||||
}
|
||||
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 _open(
|
||||
path,
|
||||
repo=None,
|
||||
rev=None,
|
||||
remote=None,
|
||||
mode="r",
|
||||
encoding=None,
|
||||
config=None,
|
||||
remote_config=None,
|
||||
):
|
||||
repo_kwargs: Dict[str, Any] = {
|
||||
"subrepos": True,
|
||||
"uninitialized": True,
|
||||
"remote": remote,
|
||||
"config": config,
|
||||
"remote_config": remote_config,
|
||||
}
|
||||
|
||||
with Repo.open(repo, rev=rev, **repo_kwargs) as _repo:
|
||||
with _wrap_exceptions(_repo, path):
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
|
||||
from dvc.fs.data import DataFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=_repo.index.data["local"])
|
||||
fs_path = path
|
||||
else:
|
||||
fs = DVCFileSystem(repo=_repo, subrepos=True)
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
try:
|
||||
with fs.open(
|
||||
fs_path,
|
||||
mode=mode,
|
||||
encoding=encoding,
|
||||
) as fobj:
|
||||
yield fobj
|
||||
except FileNotFoundError as exc:
|
||||
raise FileMissingError(path) from exc
|
||||
except IsADirectoryError as exc:
|
||||
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
|
||||
|
||||
|
||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
def read(
|
||||
path,
|
||||
repo=None,
|
||||
rev=None,
|
||||
remote=None,
|
||||
mode="r",
|
||||
encoding=None,
|
||||
config=None,
|
||||
remote_config=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
|
||||
path,
|
||||
repo=repo,
|
||||
rev=rev,
|
||||
remote=remote,
|
||||
mode=mode,
|
||||
encoding=encoding,
|
||||
config=config,
|
||||
remote_config=remote_config,
|
||||
) as fd:
|
||||
return fd.read()
|
||||
|
||||
+111
-24
@@ -1,33 +1,120 @@
|
||||
import builtins
|
||||
import os
|
||||
from time import sleep
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage.monitor import CheckpointTask
|
||||
from dvc.repo.experiments.show import tabulate
|
||||
|
||||
|
||||
def make_checkpoint():
|
||||
def exp_save(
|
||||
name: Optional[str] = None,
|
||||
force: bool = False,
|
||||
include_untracked: Optional[List[str]] = None,
|
||||
):
|
||||
"""
|
||||
Signal DVC to create a checkpoint experiment.
|
||||
Create a new DVC experiment using `exp save`.
|
||||
|
||||
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.
|
||||
See https://dvc.org/doc/command-reference/exp/save.
|
||||
|
||||
Args:
|
||||
name (str, optional): specify a name for this experiment.
|
||||
If `None`, a default one will be generated, such as `urban-sign`.
|
||||
Defaults to `None`.
|
||||
force (bool): overwrite the experiment if an experiment with the same
|
||||
name already exists.
|
||||
Defaults to `False`.
|
||||
include_untracked (List[str], optional): specify untracked file(s) to
|
||||
be included in the saved experiment.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
str: The `Git revision`_ of the created experiment.
|
||||
|
||||
Raises:
|
||||
ExperimentExistsError: If an experiment with `name` already exists and
|
||||
`force=False`.
|
||||
|
||||
.. _Git revision:
|
||||
https://git-scm.com/docs/revisions
|
||||
"""
|
||||
if os.getenv(DVC_CHECKPOINT) is None:
|
||||
return
|
||||
with Repo() as repo:
|
||||
return repo.experiments.save(
|
||||
name=name, force=force, include_untracked=include_untracked
|
||||
)
|
||||
|
||||
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)
|
||||
def _postprocess(exp_rows):
|
||||
for exp_row in exp_rows:
|
||||
for k, v in exp_row.items():
|
||||
if isinstance(v, Text):
|
||||
v_str = str(v)
|
||||
try:
|
||||
exp_row[k] = float(v_str)
|
||||
except ValueError:
|
||||
exp_row[k] = v_str
|
||||
|
||||
if not exp_row[k]:
|
||||
exp_row[k] = None
|
||||
|
||||
return exp_rows
|
||||
|
||||
|
||||
def exp_show(
|
||||
repo: Optional[str] = None,
|
||||
revs: Optional[Union[str, List[str]]] = None,
|
||||
num: int = 1,
|
||||
param_deps: bool = False,
|
||||
force: bool = False,
|
||||
config: Optional[Dict] = None,
|
||||
) -> List[Dict]:
|
||||
"""Get DVC experiments tracked in `repo`.
|
||||
|
||||
Without arguments, this function will retrieve all experiments derived from
|
||||
the Git `HEAD`.
|
||||
|
||||
See the options below to customize the experiments retrieved.
|
||||
|
||||
Args:
|
||||
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).
|
||||
revs (Union[str, List[str]], optional): Git revision(s) (e.g. branch,
|
||||
tag, SHA commit) to use as a reference point to start listing
|
||||
experiments.
|
||||
Defaults to `None`, which will use `HEAD` as starting point.
|
||||
num (int, optional): show experiments from the last `num` commits
|
||||
(first parents) starting from the `revs` baseline.
|
||||
Give a negative value to include all first-parent commits (similar
|
||||
to `git log -n`).
|
||||
Defaults to 1.
|
||||
param_deps (bool, optional): include only parameters that are stage
|
||||
dependencies.
|
||||
Defaults to `False`.
|
||||
force (bool, optional): force re-collection of experiments instead of
|
||||
loading from internal experiments cache.
|
||||
DVC caches `exp_show` data for completed experiments to improve
|
||||
performance of subsequent calls.
|
||||
When `force` is specified, DVC will reload all experiment data and
|
||||
ignore any previously cached results.
|
||||
Defaults to `False`.
|
||||
config (dict, optional): config to be passed through to DVC project.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
List[Dict]: Each item in the list will contain a dictionary with
|
||||
the info for an individual experiment.
|
||||
See Examples below.
|
||||
"""
|
||||
with Repo.open(repo, config=config) as _repo:
|
||||
experiments = _repo.experiments.show(
|
||||
revs=revs,
|
||||
num=num,
|
||||
param_deps=param_deps,
|
||||
force=force,
|
||||
)
|
||||
td, _ = tabulate(experiments, fill_value=None)
|
||||
|
||||
return _postprocess(td.as_dict())
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def all_branches(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git branches in a DVC repository.
|
||||
|
||||
Args:
|
||||
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).
|
||||
Returns:
|
||||
List[str]: Names of the Git branches.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_branches()
|
||||
|
||||
|
||||
def all_commits(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git commits in a DVC repository.
|
||||
|
||||
Args:
|
||||
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).
|
||||
Returns:
|
||||
List[str]: SHAs of the Git commits.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_all_commits()
|
||||
|
||||
|
||||
def all_tags(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git tags in a DVC repository.
|
||||
|
||||
Args:
|
||||
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).
|
||||
Returns:
|
||||
List[str]: Names of the Git tags.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_tags()
|
||||
@@ -4,16 +4,163 @@ from typing import Dict, Iterable, Optional, Union
|
||||
|
||||
from funcy import first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def _onerror_raise(exception: Exception, *args, **kwargs):
|
||||
raise exception
|
||||
|
||||
|
||||
def _postprocess(results):
|
||||
processed: Dict[str, Dict] = {}
|
||||
for rev, rev_data in results.items():
|
||||
if not rev_data:
|
||||
continue
|
||||
|
||||
processed[rev] = {}
|
||||
|
||||
counts: typing.Counter[str] = 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"]
|
||||
|
||||
return processed
|
||||
|
||||
|
||||
def metrics_show(
|
||||
*targets: str,
|
||||
repo: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
config: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
"""Get metrics tracked in `repo`.
|
||||
|
||||
Without arguments, this function will retrieve all metrics from all tracked
|
||||
metric files, for the current working tree.
|
||||
|
||||
See the options below to restrict the metrics retrieved.
|
||||
|
||||
Args:
|
||||
*targets (str, optional): Names of the metric files to retrieve
|
||||
metrics from. For example, "classifier_eval.json,
|
||||
clustering_eval.json".
|
||||
If no `targets` are provided, all metric 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).
|
||||
rev (str, optional): Name of the `Git revision`_ to retrieve metrics
|
||||
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.
|
||||
config (dict, optional): config to be passed through to DVC project.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
|
||||
Examples:
|
||||
|
||||
- No arguments.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show()
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"avg_prec": 0.9249974999612706,
|
||||
"roc_auc": 0.9460213440787918
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Using `rev`.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show(rev="tune-hyperparams")
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"avg_prec": 0.9268792615819422,
|
||||
"roc_auc": 0.945093365854111
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Using `targets`.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show("evaluation.json")
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"avg_prec": 0.9249974999612706,
|
||||
"roc_auc": 0.9460213440787918
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Git URL as `repo`.
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show(
|
||||
... repo="https://github.com/iterative/demo-fashion-mnist")
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"loss": 0.25284987688064575,
|
||||
"accuracy": 0.9071000218391418
|
||||
}
|
||||
|
||||
|
||||
.. _Git revision:
|
||||
https://git-scm.com/docs/revisions
|
||||
"""
|
||||
|
||||
with Repo.open(repo, config=config) as _repo:
|
||||
metrics = _repo.metrics.show(
|
||||
targets=targets,
|
||||
revs=rev if rev is None else [rev],
|
||||
onerror=_onerror_raise,
|
||||
)
|
||||
|
||||
metrics = _postprocess(metrics)
|
||||
|
||||
if not metrics:
|
||||
return {}
|
||||
|
||||
return metrics[first(metrics)]
|
||||
|
||||
|
||||
def params_show(
|
||||
*targets: str,
|
||||
repo: Optional[str] = None,
|
||||
stages: Optional[Union[str, Iterable[str]]] = None,
|
||||
rev: Optional[str] = None,
|
||||
deps: bool = False,
|
||||
config: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
"""Get parameters tracked in `repo`.
|
||||
|
||||
@@ -52,13 +199,12 @@ def params_show(
|
||||
deps (bool, optional): Whether to retrieve only parameters that are
|
||||
stage dependencies or not.
|
||||
Defaults to `False`.
|
||||
config (dict, optional): config to be passed through to DVC project.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
|
||||
Raises:
|
||||
DvcException: If no params are found in `repo`.
|
||||
|
||||
Examples:
|
||||
|
||||
- No arguments.
|
||||
@@ -239,35 +385,7 @@ def params_show(
|
||||
if isinstance(stages, str):
|
||||
stages = [stages]
|
||||
|
||||
def _onerror_raise(_result: Dict, exception: Exception, *args, **kwargs):
|
||||
raise exception
|
||||
|
||||
def _postprocess(params):
|
||||
processed: Dict[str, Dict] = {}
|
||||
for rev, rev_data in params.items():
|
||||
processed[rev] = {}
|
||||
|
||||
counts: typing.Counter[str] = 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:
|
||||
with Repo.open(repo, config=config) as _repo:
|
||||
params = _repo.params.show(
|
||||
revs=rev if rev is None else [rev],
|
||||
targets=targets,
|
||||
@@ -276,4 +394,9 @@ def params_show(
|
||||
stages=stages,
|
||||
)
|
||||
|
||||
return _postprocess(params)
|
||||
params = _postprocess(params)
|
||||
|
||||
if not params:
|
||||
return {}
|
||||
|
||||
return params[first(params)]
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
# file is created during dvc build
|
||||
# pylint:disable=unused-import
|
||||
from . import _build # type: ignore[attr-defined, import]
|
||||
|
||||
PKG: Optional[str] = _build.PKG # type: ignore[assignment]
|
||||
except ImportError:
|
||||
PKG = None # type: ignore[assignment]
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
LEGACY_HASH_NAMES = {"md5-dos2unix", "params"}
|
||||
|
||||
|
||||
def _get_odb(
|
||||
repo,
|
||||
settings,
|
||||
fs=None,
|
||||
prefix: Optional[Tuple[str, ...]] = None,
|
||||
hash_name: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
if not settings:
|
||||
return None
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(repo.config, **settings)
|
||||
fs = fs or cls(**config)
|
||||
if prefix:
|
||||
fs_path = fs.path.join(fs_path, *prefix)
|
||||
if hash_name:
|
||||
config["hash_name"] = hash_name
|
||||
return get_odb(fs, fs_path, state=repo.state, **config)
|
||||
|
||||
|
||||
class CacheManager:
|
||||
CACHE_DIR = "cache"
|
||||
FILES_DIR = "files"
|
||||
|
||||
def __init__(self, repo):
|
||||
self._repo = repo
|
||||
self.config = config = repo.config["cache"]
|
||||
self._odb = {}
|
||||
|
||||
default = None
|
||||
if repo and repo.local_dvc_dir:
|
||||
default = os.path.join(repo.local_dvc_dir, self.CACHE_DIR)
|
||||
|
||||
local = config.get("local")
|
||||
|
||||
if local:
|
||||
settings = {"name": local}
|
||||
elif "dir" not in config and not default:
|
||||
settings = None
|
||||
else:
|
||||
from dvc.config_schema import LOCAL_COMMON
|
||||
|
||||
url = config.get("dir") or default
|
||||
settings = {"url": url}
|
||||
for opt in LOCAL_COMMON:
|
||||
if opt in config:
|
||||
settings[str(opt)] = config.get(opt)
|
||||
|
||||
kwargs = {}
|
||||
if not isinstance(repo.fs, GitFileSystem):
|
||||
kwargs["fs"] = repo.fs
|
||||
|
||||
odb = _get_odb(
|
||||
repo,
|
||||
settings,
|
||||
prefix=(self.FILES_DIR, DEFAULT_ALGORITHM),
|
||||
**kwargs,
|
||||
)
|
||||
self._odb["repo"] = odb
|
||||
self._odb[Schemes.LOCAL] = odb
|
||||
legacy_odb = _get_odb(repo, settings, hash_name="md5-dos2unix", **kwargs)
|
||||
self._odb["legacy"] = legacy_odb
|
||||
|
||||
def _init_odb(self, schemes):
|
||||
for scheme in schemes:
|
||||
remote = self.config.get(scheme)
|
||||
settings = {"name": remote} if remote else None
|
||||
self._odb[scheme] = _get_odb(
|
||||
self._repo,
|
||||
settings,
|
||||
prefix=(self.FILES_DIR, DEFAULT_ALGORITHM),
|
||||
)
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self._odb[name]
|
||||
except KeyError as exc:
|
||||
raise AttributeError from exc
|
||||
|
||||
def by_scheme(self):
|
||||
yield from self._odb.items()
|
||||
|
||||
@property
|
||||
def local_cache_dir(self) -> str:
|
||||
"""Return base local cache directory without any prefixes.
|
||||
|
||||
(i.e. `dvc cache dir`).
|
||||
"""
|
||||
return self.legacy.path
|
||||
|
||||
|
||||
def migrate_2_to_3(repo: "Repo", dry: bool = False):
|
||||
"""Migrate legacy 2.x objects to 3.x cache.
|
||||
|
||||
Legacy 'md5-dos2unix' objects will be re-hashed with 'md5', added to 3.x cache,
|
||||
and then a link from the legacy 2.x location to the 3.x location will be created.
|
||||
"""
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
from dvc.ui import ui
|
||||
from dvc_data.hashfile.db.migrate import migrate, prepare
|
||||
|
||||
src = repo.cache.legacy
|
||||
dest = repo.cache.local
|
||||
if dry:
|
||||
oids = list(src._list_oids()) # pylint: disable=protected-access
|
||||
ui.write(
|
||||
f"{len(oids)} files will be re-hashed and migrated to the DVC 3.0 cache "
|
||||
"location."
|
||||
)
|
||||
return
|
||||
|
||||
with TqdmCallback(
|
||||
desc="Computing DVC 3.0 hashes",
|
||||
unit="files",
|
||||
) as cb:
|
||||
migration = prepare(src, dest, callback=cb)
|
||||
|
||||
with TqdmCallback(
|
||||
desc="Migrating to DVC 3.0 cache",
|
||||
unit="files",
|
||||
) as cb:
|
||||
count = migrate(migration, callback=cb)
|
||||
ui.write(f"Migrated {count} files to DVC 3.0 cache location.")
|
||||
+18
-15
@@ -39,12 +39,17 @@ def parse_args(argv=None):
|
||||
|
||||
def _log_unknown_exceptions() -> None:
|
||||
from dvc.info import get_dvc_info
|
||||
from dvc.logger import FOOTER
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
logger.exception("unexpected error")
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("Version info for developers:\n%s", get_dvc_info())
|
||||
logger.info(FOOTER)
|
||||
|
||||
q = colorize("Having any troubles?", "yellow")
|
||||
link = colorize("https://dvc.org/support", "blue")
|
||||
footer = f"\n{q} Hit us up at {link}, we are always happy to help!"
|
||||
ui.error_write(footer)
|
||||
|
||||
|
||||
def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
@@ -70,7 +75,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
from dvc.fs import AuthError, ConfigError, RemoteMissingDepsError
|
||||
|
||||
if isinstance(exc, RemoteMissingDepsError):
|
||||
from dvc.utils.pkg import PKG
|
||||
from dvc import PKG
|
||||
|
||||
proto = exc.protocol
|
||||
by_pkg = {
|
||||
@@ -78,9 +83,9 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
"conda": f"conda install -c conda-forge dvc-{proto}",
|
||||
}
|
||||
|
||||
cmd = by_pkg.get(PKG)
|
||||
if cmd:
|
||||
if PKG in by_pkg:
|
||||
link = format_link("https://dvc.org/doc/install")
|
||||
cmd = by_pkg.get(PKG)
|
||||
hint = (
|
||||
"To install dvc with those dependencies, run:\n"
|
||||
"\n"
|
||||
@@ -141,7 +146,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
def main(argv=None): # noqa: C901
|
||||
def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
"""Main entry point for dvc CLI.
|
||||
|
||||
Args:
|
||||
@@ -153,7 +158,7 @@ def main(argv=None): # noqa: C901
|
||||
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
|
||||
from dvc.logger import 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
|
||||
@@ -166,9 +171,8 @@ def main(argv=None): # noqa: C901
|
||||
logging.disable(logging.INFO)
|
||||
|
||||
args = None
|
||||
disable_other_loggers()
|
||||
|
||||
outerLogLevel = logger.level
|
||||
outer_log_level = logger.level
|
||||
try:
|
||||
args = parse_args(argv)
|
||||
|
||||
@@ -186,10 +190,9 @@ def main(argv=None): # noqa: C901
|
||||
if level and level <= logging.DEBUG:
|
||||
from platform import platform, python_implementation, python_version
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.utils.pkg import PKG
|
||||
from dvc import PKG, __version__
|
||||
|
||||
pyv = " ".join([python_implementation(), python_version()])
|
||||
pyv = f"{python_implementation()} {python_version()}"
|
||||
pkg = f" ({PKG})" if PKG else ""
|
||||
logger.debug("v%s%s, %s on %s", __version__, pkg, pyv, platform())
|
||||
logger.debug("command: %s", " ".join(argv or sys.argv))
|
||||
@@ -227,7 +230,7 @@ def main(argv=None): # noqa: C901
|
||||
logger.exception("")
|
||||
except DvcParserError:
|
||||
ret = 254
|
||||
except Exception as exc: # noqa, pylint: disable=broad-except
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
# pylint: disable=no-member
|
||||
ret = _log_exceptions(exc) or 255
|
||||
|
||||
@@ -239,9 +242,9 @@ def main(argv=None): # noqa: C901
|
||||
|
||||
return ret
|
||||
finally:
|
||||
logger.setLevel(outerLogLevel)
|
||||
logger.setLevel(outer_log_level)
|
||||
|
||||
from dvc.external_repo import clean_repos
|
||||
from dvc.repo.open_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.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from argparse import _AppendAction
|
||||
|
||||
|
||||
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
|
||||
def __call__(self, parser, namespace, values, option_string=None): # noqa: ARG002
|
||||
from funcy import ldistinct
|
||||
|
||||
items = getattr(namespace, self.dest) or []
|
||||
items.extend(map(str.strip, values.split(",")))
|
||||
setattr(namespace, self.dest, ldistinct(items))
|
||||
|
||||
|
||||
class KeyValueArgs(_AppendAction):
|
||||
def __call__(self, parser, namespace, values, option_string=None): # noqa: ARG002
|
||||
items = getattr(namespace, self.dest) or {}
|
||||
for value in filter(bool, values):
|
||||
key, _, value = value.partition("=")
|
||||
items[key.strip()] = value
|
||||
|
||||
setattr(namespace, self.dest, items)
|
||||
+3
-3
@@ -2,6 +2,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
from functools import lru_cache
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.commands import (
|
||||
@@ -40,7 +41,6 @@ from dvc.commands import (
|
||||
remove,
|
||||
repro,
|
||||
root,
|
||||
run,
|
||||
stage,
|
||||
unprotect,
|
||||
update,
|
||||
@@ -61,7 +61,6 @@ COMMANDS = [
|
||||
remove,
|
||||
move,
|
||||
unprotect,
|
||||
run,
|
||||
repro,
|
||||
data_sync,
|
||||
gc,
|
||||
@@ -138,7 +137,6 @@ def get_parent_parser():
|
||||
from dvc._debug import add_debugging_flags
|
||||
|
||||
parent_parser = argparse.ArgumentParser(add_help=False)
|
||||
add_debugging_flags(parent_parser)
|
||||
log_level_group = parent_parser.add_mutually_exclusive_group()
|
||||
log_level_group.add_argument(
|
||||
"-q", "--quiet", action="count", default=0, help="Be quiet."
|
||||
@@ -146,10 +144,12 @@ def get_parent_parser():
|
||||
log_level_group.add_argument(
|
||||
"-v", "--verbose", action="count", default=0, help="Be verbose."
|
||||
)
|
||||
add_debugging_flags(parent_parser)
|
||||
|
||||
return parent_parser
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_main_parser():
|
||||
parent_parser = get_parent_parser()
|
||||
|
||||
|
||||
+25
-3
@@ -1,3 +1,27 @@
|
||||
import argparse
|
||||
|
||||
|
||||
class DictAction(argparse.Action):
|
||||
def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
|
||||
d = getattr(args, self.dest) or {}
|
||||
|
||||
if isinstance(values, list):
|
||||
kvs = values
|
||||
else:
|
||||
kvs = [values]
|
||||
|
||||
for kv in kvs:
|
||||
key, value = kv.split("=")
|
||||
if not value:
|
||||
raise argparse.ArgumentError(
|
||||
self,
|
||||
f'Could not parse argument "{values}" as k1=v1 k2=v2 ... format',
|
||||
)
|
||||
d[key] = value
|
||||
|
||||
setattr(args, self.dest, d)
|
||||
|
||||
|
||||
def fix_subparsers(subparsers):
|
||||
"""Workaround for bug in Python 3. See more info at:
|
||||
https://bugs.python.org/issue16308
|
||||
@@ -16,9 +40,7 @@ def append_doc_link(help_message, path):
|
||||
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)
|
||||
)
|
||||
return f"{help_message}\nDocumentation: {format_link(doc_base + path)}"
|
||||
|
||||
|
||||
def hide_subparsers_from_help(subparsers):
|
||||
|
||||
+44
-65
@@ -2,68 +2,61 @@ import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import KeyValueArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help="User description of the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--meta",
|
||||
metavar="key=value",
|
||||
nargs=1,
|
||||
action=KeyValueArgs,
|
||||
help="Custom metadata to add to the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--label",
|
||||
dest="labels",
|
||||
type=str,
|
||||
action="append",
|
||||
metavar="<str>",
|
||||
help="Label for the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
type=str,
|
||||
metavar="<str>",
|
||||
help="Type of the data.",
|
||||
)
|
||||
|
||||
|
||||
class CmdAdd(CmdBase):
|
||||
def validate_args(self) -> None:
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
args = self.args
|
||||
invalid_opt = None
|
||||
|
||||
if args.to_remote or args.out:
|
||||
message = "{option} can't be used with "
|
||||
message += "--to-remote" if args.to_remote else "--out"
|
||||
if len(args.targets) != 1:
|
||||
invalid_opt = "multiple targets"
|
||||
elif args.glob:
|
||||
invalid_opt = "--glob option"
|
||||
elif args.no_commit:
|
||||
invalid_opt = "--no-commit option"
|
||||
else:
|
||||
message = "{option} can't be used without --to-remote"
|
||||
if args.remote:
|
||||
invalid_opt = "--remote"
|
||||
elif args.remote_jobs:
|
||||
invalid_opt = "--remote-jobs"
|
||||
|
||||
if invalid_opt is not None:
|
||||
raise InvalidArgumentError(message.format(option=invalid_opt))
|
||||
|
||||
def run(self):
|
||||
from dvc.exceptions import DvcException, RecursiveAddingWhileUsingFilename
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
|
||||
try:
|
||||
if len(self.args.targets) > 1 and self.args.file:
|
||||
raise RecursiveAddingWhileUsingFilename()
|
||||
self.validate_args()
|
||||
except InvalidArgumentError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
try:
|
||||
self.repo.add(
|
||||
self.args.targets,
|
||||
recursive=self.args.recursive,
|
||||
no_commit=self.args.no_commit,
|
||||
fname=self.args.file,
|
||||
external=self.args.external,
|
||||
glob=self.args.glob,
|
||||
desc=self.args.desc,
|
||||
out=self.args.out,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
jobs=self.args.jobs,
|
||||
remote_jobs=self.args.remote_jobs,
|
||||
force=self.args.force,
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
@@ -80,36 +73,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help=ADD_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Recursively add files under directory targets.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-commit",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Don't put files/directories into cache.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--external",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allow targets that are outside of the DVC repository.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--glob",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allows targets containing shell-style wildcards.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file",
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
@@ -129,8 +104,7 @@ def add_parser(subparsers, parent_parser):
|
||||
metavar="<name>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
"--remote-jobs",
|
||||
type=int,
|
||||
help=(
|
||||
"Only used along with '--to-remote'. "
|
||||
@@ -140,8 +114,13 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(parser)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
|
||||
+40
-4
@@ -1,6 +1,8 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.commands.config import CmdConfig
|
||||
from dvc.ui import ui
|
||||
@@ -9,13 +11,21 @@ from dvc.ui import ui
|
||||
class CmdCacheDir(CmdConfig):
|
||||
def run(self):
|
||||
if self.args.value is None and not self.args.unset:
|
||||
from dvc.config import ConfigError
|
||||
|
||||
if self.args.level:
|
||||
conf = self.config.read(level=self.args.level)
|
||||
else:
|
||||
# Use merged config with default values
|
||||
conf = self.config
|
||||
self._check(conf, False, "cache", "dir")
|
||||
ui.write(conf["cache"]["dir"])
|
||||
try:
|
||||
self._check(conf, False, "cache", "dir")
|
||||
path = conf["cache"]["dir"]
|
||||
except ConfigError:
|
||||
if not self.config.dvc_dir or self.args.level:
|
||||
raise
|
||||
path = os.path.join(self.config.dvc_dir, "cache")
|
||||
ui.write(path)
|
||||
return 0
|
||||
with self.config.edit(level=self.args.level) as conf:
|
||||
if self.args.unset:
|
||||
@@ -27,6 +37,14 @@ class CmdCacheDir(CmdConfig):
|
||||
return 0
|
||||
|
||||
|
||||
class CmdCacheMigrate(CmdBase):
|
||||
def run(self):
|
||||
from dvc.cachemgr import migrate_2_to_3
|
||||
|
||||
migrate_2_to_3(self.repo, dry=self.args.dry)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from dvc.commands.config import parent_config_parser
|
||||
|
||||
@@ -45,8 +63,6 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Use `dvc cache CMD --help` for command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(cache_subparsers)
|
||||
|
||||
parent_cache_config_parser = argparse.ArgumentParser(
|
||||
add_help=False, parents=[parent_config_parser]
|
||||
)
|
||||
@@ -77,3 +93,23 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
).complete = completion.DIR
|
||||
cache_dir_parser.set_defaults(func=CmdCacheDir)
|
||||
|
||||
CACHE_MIGRATE_HELP = "Migrate cached files to the DVC 3.0 cache location."
|
||||
cache_migrate_parser = cache_subparsers.add_parser(
|
||||
"migrate",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(CACHE_HELP, "cache/migrate"),
|
||||
help=CACHE_MIGRATE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
cache_migrate_parser.add_argument(
|
||||
"--dry",
|
||||
help=(
|
||||
"Only print actions which would be taken without actually migrating "
|
||||
"any data."
|
||||
),
|
||||
action="store_true",
|
||||
)
|
||||
cache_migrate_parser.set_defaults(func=CmdCacheMigrate)
|
||||
|
||||
fix_subparsers(cache_subparsers)
|
||||
|
||||
@@ -39,7 +39,7 @@ class CmdCheckIgnore(CmdBase):
|
||||
target = input()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
break
|
||||
if target == "":
|
||||
if not target:
|
||||
break
|
||||
if not self._check_one_file(target):
|
||||
ret = 0
|
||||
|
||||
@@ -47,7 +47,10 @@ def add_parser(subparsers, parent_parser):
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Commit even if hash value for dependencies/outputs changed.",
|
||||
help=(
|
||||
"Commit data even if hash values for dependencies or "
|
||||
"outputs did not change."
|
||||
),
|
||||
)
|
||||
commit_parser.add_argument(
|
||||
"-d",
|
||||
|
||||
@@ -9,13 +9,16 @@ from dvc.ui import ui
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SUPPORTED_SHELLS = ["bash", "zsh"]
|
||||
|
||||
|
||||
class CmdCompletion(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
import shtab
|
||||
|
||||
shell = self.args.shell
|
||||
parser = self.args.parser
|
||||
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
|
||||
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE) # nosec B604
|
||||
ui.write(script, force=True)
|
||||
return 0
|
||||
|
||||
@@ -35,6 +38,6 @@ def add_parser(subparsers, parent_parser):
|
||||
"--shell",
|
||||
help="Shell syntax for completions.",
|
||||
default="bash",
|
||||
choices=["bash", "zsh"],
|
||||
choices=SUPPORTED_SHELLS,
|
||||
)
|
||||
completion_parser.set_defaults(func=CmdCompletion)
|
||||
|
||||
@@ -32,7 +32,7 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
|
||||
super().__init__(args)
|
||||
|
||||
self.config = Config(validate=False)
|
||||
self.config = Config.from_cwd(validate=False)
|
||||
|
||||
def run(self):
|
||||
if self.args.show_origin and (self.args.value or self.args.unset):
|
||||
|
||||
+23
-25
@@ -9,11 +9,11 @@ if TYPE_CHECKING:
|
||||
from networkx import DiGraph
|
||||
|
||||
|
||||
def _show_ascii(G: "DiGraph"):
|
||||
def _show_ascii(graph: "DiGraph"):
|
||||
from dvc.dagascii import draw
|
||||
from dvc.repo.graph import get_pipelines
|
||||
|
||||
pipelines = get_pipelines(G)
|
||||
pipelines = get_pipelines(graph)
|
||||
|
||||
ret = []
|
||||
for pipeline in pipelines:
|
||||
@@ -31,7 +31,7 @@ def _quote_label(node):
|
||||
return label
|
||||
|
||||
|
||||
def _show_dot(G: "DiGraph"):
|
||||
def _show_dot(graph: "DiGraph"):
|
||||
import io
|
||||
|
||||
import networkx as nx
|
||||
@@ -39,15 +39,15 @@ def _show_dot(G: "DiGraph"):
|
||||
|
||||
dot_file = io.StringIO()
|
||||
|
||||
nx.relabel_nodes(G, _quote_label, copy=False)
|
||||
write_dot(G.reverse(), dot_file)
|
||||
nx.relabel_nodes(graph, _quote_label, copy=False)
|
||||
write_dot(graph.reverse(), dot_file)
|
||||
return dot_file.getvalue()
|
||||
|
||||
|
||||
def _show_mermaid(G, markdown: bool = False):
|
||||
def _show_mermaid(graph, markdown: bool = False):
|
||||
from dvc.repo.graph import get_pipelines
|
||||
|
||||
pipelines = get_pipelines(G)
|
||||
pipelines = get_pipelines(graph)
|
||||
|
||||
graph = "flowchart TD"
|
||||
|
||||
@@ -100,43 +100,42 @@ def _transform(index, outs):
|
||||
def _relabel(node) -> str:
|
||||
return node.addressing if isinstance(node, Stage) else str(node)
|
||||
|
||||
G = index.outs_graph if outs else index.graph
|
||||
return nx.relabel_nodes(G, _relabel, copy=True)
|
||||
graph = index.outs_graph if outs else index.graph
|
||||
return nx.relabel_nodes(graph, _relabel, copy=True)
|
||||
|
||||
|
||||
def _filter(G, targets, full):
|
||||
def _filter(graph, targets, full):
|
||||
import networkx as nx
|
||||
|
||||
if not targets:
|
||||
return G
|
||||
return graph
|
||||
|
||||
H = G.copy()
|
||||
new_graph = graph.copy()
|
||||
if not full:
|
||||
descendants = set()
|
||||
for target in targets:
|
||||
descendants.update(nx.descendants(G, target))
|
||||
descendants.update(nx.descendants(graph, target))
|
||||
descendants.add(target)
|
||||
H.remove_nodes_from(set(G.nodes()) - descendants)
|
||||
new_graph.remove_nodes_from(set(graph.nodes()) - descendants)
|
||||
|
||||
undirected = H.to_undirected()
|
||||
undirected = new_graph.to_undirected()
|
||||
connected = set()
|
||||
for target in targets:
|
||||
connected.update(nx.node_connected_component(undirected, target))
|
||||
|
||||
H.remove_nodes_from(set(H.nodes()) - connected)
|
||||
|
||||
return H
|
||||
new_graph.remove_nodes_from(set(new_graph.nodes()) - connected)
|
||||
return new_graph
|
||||
|
||||
|
||||
def _build(repo, target=None, full=False, outs=False):
|
||||
targets = _collect_targets(repo, target, outs)
|
||||
G = _transform(repo.index, outs)
|
||||
return _filter(G, targets, full)
|
||||
graph = _transform(repo.index, outs)
|
||||
return _filter(graph, targets, full)
|
||||
|
||||
|
||||
class CmdDAG(CmdBase):
|
||||
def run(self):
|
||||
G = _build(
|
||||
graph = _build(
|
||||
self.repo,
|
||||
target=self.args.target,
|
||||
full=self.args.full,
|
||||
@@ -144,12 +143,12 @@ class CmdDAG(CmdBase):
|
||||
)
|
||||
|
||||
if self.args.dot:
|
||||
ui.write(_show_dot(G))
|
||||
ui.write(_show_dot(graph))
|
||||
elif self.args.mermaid or self.args.markdown:
|
||||
ui.write(_show_mermaid(G, self.args.markdown))
|
||||
ui.write(_show_mermaid(graph, self.args.markdown))
|
||||
else:
|
||||
with ui.pager():
|
||||
ui.write(_show_ascii(G))
|
||||
ui.write(_show_ascii(graph))
|
||||
|
||||
return 0
|
||||
|
||||
@@ -177,7 +176,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
dag_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
+20
-120
@@ -1,14 +1,11 @@
|
||||
import argparse
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from funcy import chunks, compact, log_durations
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import CommaSeparatedArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers, hide_subparsers_from_help
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
@@ -21,12 +18,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class CmdDataStatus(CmdBase):
|
||||
COLORS = {
|
||||
"not_in_remote": "red",
|
||||
"not_in_cache": "red",
|
||||
"committed": "green",
|
||||
"uncommitted": "yellow",
|
||||
"untracked": "cyan",
|
||||
}
|
||||
LABELS = {
|
||||
"not_in_remote": "Not in remote",
|
||||
"not_in_cache": "Not in cache",
|
||||
"committed": "DVC committed changes",
|
||||
"uncommitted": "DVC uncommitted changes",
|
||||
@@ -34,6 +33,7 @@ class CmdDataStatus(CmdBase):
|
||||
"unchanged": "DVC unchanged files",
|
||||
}
|
||||
HINTS = {
|
||||
"not_in_remote": ('use "dvc push <file>..." to upload files',),
|
||||
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
|
||||
"committed": ("git commit the corresponding dvc files to update the repo",),
|
||||
"uncommitted": (
|
||||
@@ -90,9 +90,7 @@ class CmdDataStatus(CmdBase):
|
||||
ui.write(f" ({hint})")
|
||||
|
||||
if isinstance(stage_status, dict):
|
||||
items = [
|
||||
": ".join([state, file]) for file, state in stage_status.items()
|
||||
]
|
||||
items = [f"{state}: {file}" for file, state in stage_status.items()]
|
||||
else:
|
||||
items = stage_status
|
||||
|
||||
@@ -114,6 +112,8 @@ class CmdDataStatus(CmdBase):
|
||||
status = self.repo.data_status(
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
remote_refresh=self.args.remote_refresh,
|
||||
)
|
||||
|
||||
if not self.args.unchanged:
|
||||
@@ -127,63 +127,6 @@ class CmdDataStatus(CmdBase):
|
||||
return self._show_status(status)
|
||||
|
||||
|
||||
class CmdDataLs(CmdBase):
|
||||
@staticmethod
|
||||
def _show_table(
|
||||
d: Iterable[Dict[str, Any]],
|
||||
filter_types: Set[str],
|
||||
filter_labels: Set[str],
|
||||
markdown: bool = False,
|
||||
) -> None:
|
||||
from rich.style import Style
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
td = TabularData(
|
||||
columns=["Path", "Type", "Labels", "Description"], fill_value="-"
|
||||
)
|
||||
for entry in sorted(d, key=itemgetter("path")):
|
||||
typ = entry.get("type", "")
|
||||
desc = entry.get("desc", "-")
|
||||
labels = entry.get("labels", [])
|
||||
|
||||
if filter_types and typ not in filter_types:
|
||||
continue
|
||||
if filter_labels and filter_labels.isdisjoint(labels):
|
||||
continue
|
||||
|
||||
rich_label = ui.rich_text()
|
||||
for index, label in enumerate(labels):
|
||||
if index:
|
||||
rich_label.append(",")
|
||||
style = Style(bold=label in filter_labels, color="green")
|
||||
rich_label.append(label, style=style)
|
||||
|
||||
if markdown and desc:
|
||||
desc = desc.partition("\n")[0]
|
||||
|
||||
path = ui.rich_text(entry["path"], style="cyan")
|
||||
type_style = Style(bold=typ in filter_types, color="yellow")
|
||||
typ = ui.rich_text(entry.get("type", ""), style=type_style)
|
||||
td.append([path, typ or "-", rich_label or "-", desc])
|
||||
|
||||
td.render(markdown=markdown, rich_table=True)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo.data import ls
|
||||
|
||||
filter_labels = set(self.args.labels)
|
||||
filter_types = set(self.args.type)
|
||||
d = ls(self.repo, targets=self.args.targets, recursive=self.args.recursive)
|
||||
self._show_table(
|
||||
d,
|
||||
filter_labels=filter_labels,
|
||||
filter_types=filter_types,
|
||||
markdown=self.args.markdown,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
data_parser = subparsers.add_parser(
|
||||
"data",
|
||||
@@ -212,13 +155,6 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="json",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--granular",
|
||||
action="store_true",
|
||||
@@ -239,52 +175,16 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show files not in remote.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--no-remote-refresh",
|
||||
dest="remote_refresh",
|
||||
action="store_false",
|
||||
help="Use cached remote index (don't check remote).",
|
||||
)
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
DATA_LS_HELP = "List data tracked by DVC with its metadata."
|
||||
data_ls_parser = data_subparsers.add_parser(
|
||||
"ls",
|
||||
aliases=["list"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DATA_LS_HELP, "data/ls"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
add_help=False,
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
dest="markdown",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--type",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of type to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--labels",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of labels to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Recursively list from the specified directories.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"targets",
|
||||
default=None,
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
data_ls_parser.set_defaults(func=CmdDataLs)
|
||||
hide_subparsers_from_help(data_subparsers)
|
||||
|
||||
@@ -40,6 +40,7 @@ class CmdDataPull(CmdDataBase):
|
||||
recursive=self.args.recursive,
|
||||
run_cache=self.args.run_cache,
|
||||
glob=self.args.glob,
|
||||
allow_missing=self.args.allow_missing,
|
||||
)
|
||||
self.log_summary(stats)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
@@ -90,6 +91,8 @@ class CmdDataFetch(CmdDataBase):
|
||||
with_deps=self.args.with_deps,
|
||||
recursive=self.args.recursive,
|
||||
run_cache=self.args.run_cache,
|
||||
max_size=self.args.max_size,
|
||||
types=self.args.types,
|
||||
)
|
||||
self.log_summary({"fetched": processed_files_count})
|
||||
except DvcException:
|
||||
@@ -197,6 +200,12 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"--allow-missing",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Ignore errors if some of the files or directories are missing.",
|
||||
)
|
||||
pull_parser.set_defaults(func=CmdDataPull)
|
||||
|
||||
# Push
|
||||
@@ -315,6 +324,22 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"--max-size",
|
||||
type=int,
|
||||
help="Fetch data files/directories that are each below specified size (bytes).",
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"--type",
|
||||
dest="types",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"Only fetch data files/directories that are of a particular "
|
||||
"type (metrics, plots)."
|
||||
),
|
||||
choices=["metrics", "plots"],
|
||||
)
|
||||
fetch_parser.set_defaults(func=CmdDataFetch)
|
||||
|
||||
# Status
|
||||
@@ -396,7 +421,6 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show status in JSON format.",
|
||||
|
||||
@@ -21,7 +21,7 @@ class CmdDestroy(CmdBase):
|
||||
)
|
||||
|
||||
if not self.args.force and not ui.confirm(statement):
|
||||
raise DvcException(
|
||||
raise DvcException( # noqa: TRY301
|
||||
"cannot destroy without a confirmation from the user."
|
||||
" Use `-f` to force."
|
||||
)
|
||||
|
||||
@@ -184,7 +184,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
help="Format the output into a JSON",
|
||||
action="store_true",
|
||||
default=False,
|
||||
@@ -197,7 +196,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
action="store_true",
|
||||
dest="markdown",
|
||||
|
||||
@@ -4,10 +4,9 @@ from dvc.cli.utils import append_doc_link, fix_subparsers, hide_subparsers_from_
|
||||
from dvc.commands.experiments import (
|
||||
apply,
|
||||
branch,
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
init,
|
||||
ls,
|
||||
pull,
|
||||
push,
|
||||
@@ -21,10 +20,9 @@ from dvc.commands.experiments import (
|
||||
SUB_COMMANDS = [
|
||||
apply,
|
||||
branch,
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
init,
|
||||
ls,
|
||||
pull,
|
||||
push,
|
||||
@@ -80,6 +78,7 @@ def add_rev_selection_flags(
|
||||
experiments_subcmd_parser.add_argument(
|
||||
"--rev",
|
||||
type=str,
|
||||
action="append",
|
||||
default=None,
|
||||
help=msg,
|
||||
metavar="<commit>",
|
||||
|
||||
@@ -4,13 +4,21 @@ import logging
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsApply(CmdBase):
|
||||
def run(self):
|
||||
self.repo.experiments.apply(self.args.experiment, force=self.args.force)
|
||||
if not self.args.force:
|
||||
ui.write(
|
||||
"The --no-force option is deprecated and will be removed in a future"
|
||||
" DVC release. To revert the result of 'exp apply', run:\n"
|
||||
"\n\tgit reset --hard\n"
|
||||
"\tgit stash apply refs/exps/apply/stash\n"
|
||||
)
|
||||
self.repo.experiments.apply(self.args.experiment)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -26,5 +26,13 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
experiments_branch_parser.add_argument(
|
||||
"experiment", help="Experiment to be promoted."
|
||||
)
|
||||
experiments_branch_parser.add_argument("branch", help="Name for the new Git branch")
|
||||
experiments_branch_parser.add_argument(
|
||||
"branch",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help=(
|
||||
"Optional name for the new Git branch. "
|
||||
"Defaults to '{experiment-name}-branch'."
|
||||
),
|
||||
)
|
||||
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsClean(CmdBase):
|
||||
def run(self):
|
||||
self.repo.experiments.clean()
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_CLEAN_HELP = "Cleanup experiments temporary internal files."
|
||||
experiments_clean_parser = experiments_subparsers.add_parser(
|
||||
"clean",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_CLEAN_HELP, "exp/clean"),
|
||||
help=EXPERIMENTS_CLEAN_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
experiments_clean_parser.set_defaults(func=CmdExperimentsClean)
|
||||
@@ -52,7 +52,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_DIFF_HELP = "Show changes between experiments in the DVC repository."
|
||||
EXPERIMENTS_DIFF_HELP = "Show changes between experiments."
|
||||
|
||||
experiments_diff_parser = experiments_subparsers.add_parser(
|
||||
"diff",
|
||||
@@ -83,14 +83,12 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
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",
|
||||
|
||||
@@ -19,6 +19,8 @@ class CmdExecutorRun(CmdBaseNoRepo):
|
||||
queue=None,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
infofile=self.args.infofile,
|
||||
copy_paths=self.args.copy_paths,
|
||||
message=self.args.message,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -36,4 +38,21 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help="Path to executor info file",
|
||||
default=None,
|
||||
)
|
||||
exec_run_parser.add_argument(
|
||||
"-C",
|
||||
"--copy-paths",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"List of ignored or untracked paths to copy into the temp directory."
|
||||
" Only used if `--temp` or `--queue` is specified."
|
||||
),
|
||||
)
|
||||
exec_run_parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
exec_run_parser.set_defaults(func=CmdExecutorRun)
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
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`,"
|
||||
" `--all-commits` or `--date` 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,
|
||||
commit_date=self.args.commit_date,
|
||||
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"
|
||||
else:
|
||||
if 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"
|
||||
|
||||
if self.args.commit_date:
|
||||
msg += f" and all git commits before date {self.args.commit_date}"
|
||||
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,
|
||||
commit_date=self.args.commit_date,
|
||||
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 specifiedGit 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(
|
||||
"--date",
|
||||
type=str,
|
||||
dest="commit_date",
|
||||
metavar="<YYYY-MM-DD>",
|
||||
default=None,
|
||||
help=(
|
||||
"Keep experiments from the commits after (inclusive) a certain "
|
||||
"date. Date must match the extended ISO 8601 format (YYYY-MM-DD)."
|
||||
),
|
||||
)
|
||||
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)
|
||||
@@ -1,219 +0,0 @@
|
||||
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)
|
||||
@@ -3,6 +3,7 @@ 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__)
|
||||
@@ -11,19 +12,36 @@ logger = logging.getLogger(__name__)
|
||||
class CmdExperimentsList(CmdBase):
|
||||
def run(self):
|
||||
name_only = self.args.name_only
|
||||
sha_only = self.args.sha_only
|
||||
git_remote = self.args.git_remote
|
||||
if sha_only and git_remote:
|
||||
raise InvalidArgumentError("--sha-only not supported with git_remote.")
|
||||
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,
|
||||
git_remote=git_remote,
|
||||
)
|
||||
|
||||
for baseline in exps:
|
||||
if not name_only:
|
||||
ui.write(f"{baseline}:")
|
||||
for exp_name in exps[baseline]:
|
||||
indent = "" if name_only else "\t"
|
||||
ui.write(f"{indent}{exp_name}")
|
||||
if not (name_only or sha_only):
|
||||
tag_base = "refs/tags/"
|
||||
branch_base = "refs/heads/"
|
||||
name = baseline[:7]
|
||||
if baseline.startswith(tag_base):
|
||||
name = baseline[len(tag_base) :]
|
||||
elif baseline.startswith(branch_base):
|
||||
name = baseline[len(branch_base) :]
|
||||
ui.write(f"{name}:")
|
||||
for exp_name, rev in exps[baseline]:
|
||||
if name_only:
|
||||
ui.write(exp_name)
|
||||
elif sha_only:
|
||||
ui.write(rev)
|
||||
elif rev:
|
||||
ui.write(f"\t{rev[:7]} [{exp_name}]")
|
||||
else:
|
||||
ui.write(f"\t{exp_name}")
|
||||
|
||||
return 0
|
||||
|
||||
@@ -40,11 +58,18 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
add_rev_selection_flags(experiments_list_parser, "List")
|
||||
experiments_list_parser.add_argument(
|
||||
display_group = experiments_list_parser.add_mutually_exclusive_group()
|
||||
display_group.add_argument(
|
||||
"--name-only",
|
||||
"--names-only",
|
||||
action="store_true",
|
||||
help="Only output experiment names (without parent commits).",
|
||||
help="Only output experiment names (without SHAs or parent commits).",
|
||||
)
|
||||
display_group.add_argument(
|
||||
"--sha-only",
|
||||
"--shas-only",
|
||||
action="store_true",
|
||||
help="Only output experiment commit SHAs (without names or parent commits).",
|
||||
)
|
||||
experiments_list_parser.add_argument(
|
||||
"git_remote",
|
||||
|
||||
@@ -3,23 +3,13 @@ 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,
|
||||
@@ -61,7 +51,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help=EXPERIMENTS_PULL_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
add_rev_selection_flags(experiments_pull_parser, "Pull", False)
|
||||
add_rev_selection_flags(experiments_pull_parser, "Pull", True)
|
||||
experiments_pull_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
|
||||
@@ -1,46 +1,74 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
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."
|
||||
@staticmethod
|
||||
def log_result(result: Dict[str, Any], remote: str):
|
||||
from dvc.utils import humanize
|
||||
|
||||
def join_exps(exps):
|
||||
return humanize.join([f"[bold]{e}[/]" for e in exps])
|
||||
|
||||
if diverged_exps := result.get("diverged"):
|
||||
exps = join_exps(diverged_exps)
|
||||
ui.error_write(
|
||||
f"[yellow]Local experiment {exps} has diverged "
|
||||
"from remote experiment with the same name.\n"
|
||||
"To override the remote experiment re-run with '--force'.",
|
||||
styled=True,
|
||||
)
|
||||
if uptodate_exps := result.get("up_to_date"):
|
||||
exps = join_exps(uptodate_exps)
|
||||
verb = "are" if len(uptodate_exps) > 1 else "is"
|
||||
ui.write(
|
||||
f"Experiment {exps} {verb} up to date on Git remote {remote!r}.",
|
||||
styled=True,
|
||||
)
|
||||
if pushed_exps := result.get("success"):
|
||||
exps = join_exps(pushed_exps)
|
||||
ui.write(f"Pushed experiment {exps} to Git remote {remote!r}.", styled=True)
|
||||
if not uptodate_exps and not pushed_exps:
|
||||
ui.write("No experiments to push.")
|
||||
|
||||
if uploaded := result.get("uploaded"):
|
||||
stats = {"uploaded": uploaded}
|
||||
ui.write(humanize.get_summary(stats.items()))
|
||||
|
||||
if project_url := result.get("url"):
|
||||
ui.rich_print(
|
||||
"View your experiments at", project_url, style="yellow", soft_wrap=True
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.raise_error_if_all_disabled()
|
||||
from dvc.repo.experiments.push import UploadError
|
||||
|
||||
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}'."
|
||||
try:
|
||||
result = 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,
|
||||
)
|
||||
else:
|
||||
ui.write("No experiments to push.")
|
||||
except UploadError as e:
|
||||
self.log_result(e.result, self.args.git_remote)
|
||||
raise
|
||||
|
||||
self.log_result(result, self.args.git_remote)
|
||||
if not self.args.push_cache:
|
||||
ui.write(
|
||||
"To push cached outputs",
|
||||
@@ -64,7 +92,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help=EXPERIMENTS_PUSH_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
add_rev_selection_flags(experiments_push_parser, "Push", False)
|
||||
add_rev_selection_flags(experiments_push_parser, "Push", True)
|
||||
experiments_push_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
|
||||
@@ -9,7 +9,9 @@ class CmdQueueWorker(CmdBase):
|
||||
"""Run the exp queue worker."""
|
||||
|
||||
def run(self):
|
||||
self.repo.experiments.celery_queue.worker.start(self.args.name)
|
||||
self.repo.experiments.celery_queue.worker.start(
|
||||
self.args.name, fsapp_clean=self.args.clean
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -22,4 +24,9 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
add_help=False,
|
||||
)
|
||||
parser.add_argument("name", help="Celery worker name.")
|
||||
parser.add_argument(
|
||||
"--clean",
|
||||
action="store_true",
|
||||
help="Automatically cleanup celery broker on shutdown.",
|
||||
)
|
||||
parser.set_defaults(func=CmdQueueWorker)
|
||||
|
||||
@@ -24,9 +24,11 @@ class CmdExperimentsRemove(CmdBase):
|
||||
)
|
||||
|
||||
def run(self):
|
||||
from dvc.utils import humanize
|
||||
|
||||
self.check_arguments()
|
||||
|
||||
removed_list = self.repo.experiments.remove(
|
||||
removed = self.repo.experiments.remove(
|
||||
exp_names=self.args.experiment,
|
||||
all_commits=self.args.all_commits,
|
||||
rev=self.args.rev,
|
||||
@@ -34,8 +36,10 @@ class CmdExperimentsRemove(CmdBase):
|
||||
queue=self.args.queue,
|
||||
git_remote=self.args.git_remote,
|
||||
)
|
||||
removed = ",".join(removed_list)
|
||||
ui.write(f"Removed experiments: {removed}")
|
||||
if removed:
|
||||
ui.write(f"Removed experiments: {humanize.join(map(repr, removed))}")
|
||||
else:
|
||||
ui.write("No experiments to remove.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -1,49 +1,28 @@
|
||||
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(
|
||||
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,
|
||||
copy_paths=self.args.copy_paths,
|
||||
message=self.args.message,
|
||||
**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
|
||||
|
||||
|
||||
@@ -57,22 +36,6 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
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)
|
||||
|
||||
|
||||
@@ -136,3 +99,20 @@ def _add_run_common(parser):
|
||||
# )
|
||||
# metavar="<name>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-C",
|
||||
"--copy-paths",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"List of ignored or untracked paths to copy into the temp directory."
|
||||
" Only used if `--temp` or `--queue` is specified."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ class CmdExperimentsSave(CmdBase):
|
||||
name=self.args.name,
|
||||
force=self.args.force,
|
||||
include_untracked=self.args.include_untracked,
|
||||
message=self.args.message,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to save experiment")
|
||||
@@ -35,7 +36,7 @@ class CmdExperimentsSave(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_SAVE_HELP = "Save current workspace as a dvc experiment."
|
||||
EXPERIMENTS_SAVE_HELP = "Save current workspace as an experiment."
|
||||
save_parser = experiments_subparsers.add_parser(
|
||||
"save",
|
||||
parents=[parent_parser],
|
||||
@@ -52,7 +53,6 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
@@ -75,4 +75,11 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help="List of untracked paths to include in the experiment.",
|
||||
metavar="<path>",
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
save_parser.set_defaults(func=CmdExperimentsSave)
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING, Any, Dict, Set
|
||||
from typing import TYPE_CHECKING, Dict, Iterable
|
||||
|
||||
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.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.flatten import flatten
|
||||
from dvc.utils.serialize import encode_exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -28,263 +24,13 @@ 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: Dict[str, Dict[str, Any]] = defaultdict(dict)
|
||||
param_names: Dict[str, Dict[str, Any]] = defaultdict(dict)
|
||||
deps_names: Set[str] = 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( # noqa: C901
|
||||
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 dvc.scm 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", {})
|
||||
|
||||
state = exp.get("status")
|
||||
if state == "Success":
|
||||
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"]
|
||||
@@ -309,49 +55,15 @@ def baseline_styler(typ):
|
||||
|
||||
|
||||
def show_experiments( # noqa: C901
|
||||
all_experiments,
|
||||
td: "TabularData",
|
||||
headers: Dict[str, Iterable[str]],
|
||||
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_list = [
|
||||
"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_list)
|
||||
metric_headers = _normalize_headers(metric_names, counter)
|
||||
param_headers = _normalize_headers(param_names, counter)
|
||||
|
||||
td = experiments_table(
|
||||
all_experiments,
|
||||
headers_list,
|
||||
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(): # noqa: SIM118
|
||||
if re.match(keep, col):
|
||||
@@ -368,11 +80,6 @@ def show_experiments( # noqa: C901
|
||||
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"},
|
||||
@@ -397,51 +104,23 @@ def show_experiments( # noqa: C901
|
||||
}
|
||||
)
|
||||
|
||||
if kwargs.get("only_changed", False) or pcp:
|
||||
if kwargs.get("only_changed", False):
|
||||
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)} # noqa: SIM118
|
||||
if pcp:
|
||||
cols_to_drop.add("Created")
|
||||
td.drop(*cols_to_drop)
|
||||
|
||||
if pcp:
|
||||
subset = {x for x in td.keys() if x != "Experiment"} # noqa: SIM118
|
||||
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,
|
||||
)
|
||||
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):
|
||||
@@ -460,8 +139,10 @@ def _format_json(item):
|
||||
|
||||
class CmdExperimentsShow(CmdBase):
|
||||
def run(self):
|
||||
from dvc.repo.experiments.show import tabulate
|
||||
|
||||
try:
|
||||
all_experiments = self.repo.experiments.show(
|
||||
exps = self.repo.experiments.show(
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
@@ -472,36 +153,40 @@ class CmdExperimentsShow(CmdBase):
|
||||
sha_only=self.args.sha,
|
||||
param_deps=self.args.param_deps,
|
||||
fetch_running=self.args.fetch_running,
|
||||
force=self.args.force,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to show experiments")
|
||||
return 1
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(all_experiments, default=_format_json)
|
||||
ui.write_json([exp.dumpd() for exp in exps], 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 = self.args.csv
|
||||
td, headers = tabulate(
|
||||
exps,
|
||||
precision=precision,
|
||||
fill_value=fill_value,
|
||||
iso=iso,
|
||||
sort_by=self.args.sort_by,
|
||||
sort_order=self.args.sort_order,
|
||||
)
|
||||
|
||||
show_experiments(
|
||||
all_experiments,
|
||||
td,
|
||||
headers,
|
||||
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
|
||||
|
||||
@@ -594,21 +279,18 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
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",
|
||||
@@ -623,31 +305,16 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
),
|
||||
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.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Force re-collection of experiments instead of loading from exp cache.",
|
||||
)
|
||||
experiments_show_parser.set_defaults(func=CmdExperimentsShow)
|
||||
|
||||
+15
-2
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdGC(CmdBase):
|
||||
def run(self): # noqa: C901
|
||||
def run(self): # noqa: C901, PLR0912
|
||||
from dvc.repo.gc import _validate_args
|
||||
|
||||
_validate_args(
|
||||
@@ -22,6 +22,8 @@ class CmdGC(CmdBase):
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
cloud=self.args.cloud,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
)
|
||||
|
||||
if self.args.rev:
|
||||
@@ -46,6 +48,10 @@ class CmdGC(CmdBase):
|
||||
|
||||
if self.args.all_experiments:
|
||||
msg += " and all experiments"
|
||||
|
||||
if self.args.not_in_remote:
|
||||
msg += " that are present in the DVC remote"
|
||||
|
||||
if self.args.repos:
|
||||
msg += " of the current and the following repos:"
|
||||
|
||||
@@ -74,6 +80,7 @@ class CmdGC(CmdBase):
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -157,12 +164,18 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Keep data files for all experiments.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep data files that are not present in the remote.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-c",
|
||||
"--cloud",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Collect garbage in remote repository.",
|
||||
help="Collect garbage in remote storage in addition to local cache.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-r",
|
||||
|
||||
+38
-8
@@ -3,9 +3,8 @@ import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.scm import CloneError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,12 +14,8 @@ class CmdGet(CmdBaseNoRepo):
|
||||
from dvc.api import get_url
|
||||
from dvc.ui import ui
|
||||
|
||||
try:
|
||||
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
|
||||
ui.write(url, force=True)
|
||||
except DvcException:
|
||||
logger.exception("failed to show URL")
|
||||
return 1
|
||||
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
|
||||
ui.write(url, force=True)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -32,6 +27,7 @@ class CmdGet(CmdBaseNoRepo):
|
||||
|
||||
def _get_file_from_repo(self):
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import CloneError
|
||||
|
||||
try:
|
||||
Repo.get(
|
||||
@@ -40,6 +36,10 @@ class CmdGet(CmdBaseNoRepo):
|
||||
out=self.args.out,
|
||||
rev=self.args.rev,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
return 0
|
||||
except CloneError:
|
||||
@@ -98,4 +98,34 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
help=(
|
||||
"Path to a config file that will be merged with the config "
|
||||
"in the target repository."
|
||||
),
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help=(
|
||||
"Remote config options to merge with a remote's config (default or one "
|
||||
"specified by '--remote') in the target repository."
|
||||
),
|
||||
)
|
||||
get_parser.set_defaults(func=CmdGet)
|
||||
|
||||
+13
-1
@@ -14,7 +14,12 @@ class CmdGetUrl(CmdBaseNoRepo):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
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,
|
||||
force=self.args.force,
|
||||
)
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception("failed to get '%s'", self.args.url)
|
||||
@@ -46,4 +51,11 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
get_parser.set_defaults(func=CmdGetUrl)
|
||||
|
||||
+29
-16
@@ -3,29 +3,28 @@ import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.scm import CloneError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdImport(CmdBase):
|
||||
def run(self):
|
||||
from dvc.scm import CloneError
|
||||
|
||||
try:
|
||||
self.repo.imp(
|
||||
self.args.url,
|
||||
self.args.path,
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
rev=self.args.rev,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
except CloneError:
|
||||
logger.exception("failed to import '%s'", self.args.path)
|
||||
@@ -41,8 +40,6 @@ class CmdImport(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download file or directory tracked by DVC or by Git "
|
||||
"into the workspace, and track it."
|
||||
@@ -74,11 +71,6 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--file",
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
@@ -105,6 +97,27 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
help=(
|
||||
"Path to a config file that will be merged with the config "
|
||||
"in the target repository."
|
||||
),
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help=(
|
||||
"Remote config options to merge with a remote's config (default or one "
|
||||
"specified by '--remote') in the target repository."
|
||||
),
|
||||
)
|
||||
import_parser.set_defaults(func=CmdImport)
|
||||
|
||||
+8
-13
@@ -15,16 +15,12 @@ class CmdImportUrl(CmdBase):
|
||||
self.repo.imp_url(
|
||||
self.args.url,
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
version_aware=self.args.version_aware,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -40,8 +36,6 @@ class CmdImportUrl(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = "Download or copy file from URL and take it under DVC control."
|
||||
|
||||
import_parser = subparsers.add_parser(
|
||||
@@ -69,11 +63,6 @@ def add_parser(subparsers, parent_parser):
|
||||
import_parser.add_argument(
|
||||
"out", nargs="?", help="Destination path to put files to."
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"--file",
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"--to-remote",
|
||||
action="store_true",
|
||||
@@ -112,11 +101,17 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--version-aware",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Import using cloud versioning. Implied if the URL contains a version ID.",
|
||||
)
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImportUrl)
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.commands.ls.ls_colors import LsColors
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
@@ -34,6 +34,9 @@ class CmdList(CmdBaseNoRepo):
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
dvc_only=self.args.dvc_only,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
if self.args.json:
|
||||
ui.write_json(entries)
|
||||
@@ -71,7 +74,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
@@ -81,6 +83,29 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
help=(
|
||||
"Path to a config file that will be merged with the config "
|
||||
"in the target repository."
|
||||
),
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help=(
|
||||
"Remote config options to merge with a remote's config (default or one "
|
||||
"specified by '--remote') in the target repository."
|
||||
),
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
|
||||
@@ -302,7 +302,7 @@ class CmdMachineSsh(CmdBase):
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
def add_parser(subparsers, parent_parser): # noqa: PLR0915
|
||||
from dvc.commands.config import parent_config_parser
|
||||
|
||||
machine_HELP = "Set up and manage cloud machines."
|
||||
|
||||
@@ -140,14 +140,12 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
@@ -223,14 +221,12 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
@@ -105,14 +105,12 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
+56
-25
@@ -1,9 +1,9 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
from funcy import first
|
||||
from funcy import compact, first, get_in
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
@@ -12,14 +12,51 @@ from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.render.match import RendererWithErrors
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _show_json(renderers, split=False):
|
||||
def _show_json(
|
||||
renderers_with_errors: List["RendererWithErrors"],
|
||||
split=False,
|
||||
errors: Optional[Dict[str, Exception]] = None,
|
||||
):
|
||||
from dvc.render.convert import to_json
|
||||
from dvc.utils.serialize import encode_exception
|
||||
|
||||
result = {renderer.name: to_json(renderer, split) for renderer in renderers}
|
||||
ui.write_json(result)
|
||||
all_errors: List[Dict] = []
|
||||
data = {}
|
||||
|
||||
for renderer, src_errors, def_errors in renderers_with_errors:
|
||||
name = renderer.name
|
||||
data[name] = to_json(renderer, split)
|
||||
all_errors.extend(
|
||||
{
|
||||
"name": name,
|
||||
"rev": rev,
|
||||
"source": source,
|
||||
**encode_exception(e),
|
||||
}
|
||||
for rev, per_rev_src_errors in src_errors.items()
|
||||
for source, e in per_rev_src_errors.items()
|
||||
)
|
||||
all_errors.extend(
|
||||
{
|
||||
"name": name,
|
||||
"rev": rev,
|
||||
**encode_exception(e),
|
||||
}
|
||||
for rev, e in def_errors.items()
|
||||
)
|
||||
|
||||
# these errors are not tied to any renderers
|
||||
errors = errors or {}
|
||||
all_errors.extend({"rev": rev, **encode_exception(e)} for rev, e in errors.items())
|
||||
|
||||
ui.write_json(compact({"errors": all_errors, "data": data}), highlight=False)
|
||||
|
||||
|
||||
def _adjust_vega_renderers(renderers):
|
||||
@@ -76,10 +113,6 @@ class CmdPlots(CmdBase):
|
||||
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):
|
||||
if self.args.from_config:
|
||||
return {self.args.from_config}
|
||||
|
||||
def _html_template_path(self):
|
||||
html_template_path = self.args.html_template
|
||||
if not html_template_path:
|
||||
@@ -89,9 +122,9 @@ class CmdPlots(CmdBase):
|
||||
if html_template_path and not os.path.isabs(html_template_path):
|
||||
assert self.repo.dvc_dir
|
||||
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
|
||||
return html_template_path # noqa: RET504
|
||||
return html_template_path
|
||||
|
||||
def run(self) -> int: # noqa: C901
|
||||
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
|
||||
from pathlib import Path
|
||||
|
||||
from dvc.render.match import match_defs_renderers
|
||||
@@ -114,10 +147,9 @@ class CmdPlots(CmdBase):
|
||||
plots_data = self._func(
|
||||
targets=self.args.targets,
|
||||
props=self._props(),
|
||||
config_files=self._config_files(),
|
||||
)
|
||||
|
||||
if not plots_data:
|
||||
if not plots_data and not self.args.json:
|
||||
ui.error_write(
|
||||
"No plots were loaded, visualization file will not be created."
|
||||
)
|
||||
@@ -127,21 +159,27 @@ class CmdPlots(CmdBase):
|
||||
)
|
||||
|
||||
renderers_out = out if self.args.json else os.path.join(out, "static")
|
||||
|
||||
renderers = match_defs_renderers(
|
||||
renderers_with_errors = match_defs_renderers(
|
||||
data=plots_data,
|
||||
out=renderers_out,
|
||||
templates_dir=self.repo.plots.templates_dir,
|
||||
)
|
||||
if self.args.json:
|
||||
_show_json(renderers, self.args.split)
|
||||
errors = compact(
|
||||
{
|
||||
rev: get_in(data, ["definitions", "error"])
|
||||
for rev, data in plots_data.items()
|
||||
}
|
||||
)
|
||||
_show_json(renderers_with_errors, self.args.split, errors=errors)
|
||||
return 0
|
||||
|
||||
renderers = [r.renderer for r in renderers_with_errors]
|
||||
_adjust_vega_renderers(renderers)
|
||||
if self.args.show_vega:
|
||||
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
|
||||
if renderer:
|
||||
ui.write_json(json.loads(renderer.get_filled_template()))
|
||||
ui.write_json(renderer.get_filled_template(as_string=False))
|
||||
return 0
|
||||
|
||||
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
|
||||
@@ -150,7 +188,7 @@ class CmdPlots(CmdBase):
|
||||
render_html(
|
||||
renderers=renderers,
|
||||
output_file=output_file,
|
||||
template_path=self._html_template_path(),
|
||||
html_template=self._html_template_path(),
|
||||
)
|
||||
|
||||
ui.write(output_file.as_uri())
|
||||
@@ -392,7 +430,6 @@ def _add_ui_arguments(parser):
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
@@ -412,9 +449,3 @@ def _add_ui_arguments(parser):
|
||||
help="Custom HTML template for VEGA visualization.",
|
||||
metavar="<path>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--from-config",
|
||||
default=None,
|
||||
metavar="<path>",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.experiments.show import format_time
|
||||
from dvc.compare import TabularData
|
||||
from dvc.ui import ui
|
||||
|
||||
@@ -15,9 +13,9 @@ class CmdQueueStatus(CmdBase):
|
||||
"""Show queue task and worker status."""
|
||||
|
||||
def run(self) -> int:
|
||||
result: List[
|
||||
Dict[str, Optional[str]]
|
||||
] = self.repo.experiments.celery_queue.status()
|
||||
from dvc.repo.experiments.show import format_time
|
||||
|
||||
result = self.repo.experiments.celery_queue.status()
|
||||
if result:
|
||||
all_headers = ["Task", "Name", "Created", "Status"]
|
||||
td = TabularData(all_headers)
|
||||
|
||||
@@ -127,9 +127,7 @@ class CmdRemoteRename(CmdRemote):
|
||||
all_config = self.config.load_config_to_level(None)
|
||||
if self.args.new in all_config.get("remote", {}):
|
||||
raise ConfigError(
|
||||
"Rename failed. Remote name '{}' already exists.".format(
|
||||
{self.args.new}
|
||||
)
|
||||
f"Rename failed. Remote name {self.args.new!r} already exists."
|
||||
)
|
||||
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
|
||||
+28
-13
@@ -16,12 +16,6 @@ class CmdRepro(CmdBase):
|
||||
else:
|
||||
ui.write("Use `dvc push` to send your updates to remote storage.")
|
||||
|
||||
if self.args.metrics:
|
||||
from dvc.compare import show_metrics
|
||||
|
||||
metrics = self.repo.metrics.show()
|
||||
show_metrics(metrics)
|
||||
|
||||
return 0
|
||||
|
||||
@property
|
||||
@@ -38,6 +32,8 @@ class CmdRepro(CmdBase):
|
||||
"recursive": self.args.recursive,
|
||||
"force_downstream": self.args.force_downstream,
|
||||
"pull": self.args.pull,
|
||||
"allow_missing": self.args.allow_missing,
|
||||
"on_error": self.args.on_error,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -104,13 +100,6 @@ and then the stage name name.
|
||||
default=False,
|
||||
help="Reproduce all stages in the specified directory.",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"-m",
|
||||
"--metrics",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show metrics after reproduction.",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--downstream",
|
||||
action="store_true",
|
||||
@@ -135,6 +124,12 @@ and then the stage name name.
|
||||
"from the run-cache."
|
||||
),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--allow-missing",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=("Skip stages with missing data but no other changes."),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--dry",
|
||||
action="store_true",
|
||||
@@ -143,6 +138,26 @@ and then the stage name name.
|
||||
"Only print the commands that would be executed without actually executing."
|
||||
),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"-k",
|
||||
"--keep-going",
|
||||
action="store_const",
|
||||
default="fail",
|
||||
const="keep-going",
|
||||
dest="on_error",
|
||||
help=(
|
||||
"Continue executing, skipping stages having dependencies "
|
||||
"on the failed stages"
|
||||
),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--ignore-errors",
|
||||
action="store_const",
|
||||
default="fail",
|
||||
const="ignore",
|
||||
dest="on_error",
|
||||
help="Ignore errors from stages.",
|
||||
)
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.stage import parse_cmd
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdRun(CmdBase):
|
||||
def run(self):
|
||||
if not any(
|
||||
[
|
||||
self.args.deps,
|
||||
self.args.outs,
|
||||
self.args.outs_no_cache,
|
||||
self.args.metrics,
|
||||
self.args.metrics_no_cache,
|
||||
self.args.plots,
|
||||
self.args.plots_no_cache,
|
||||
self.args.outs_persist,
|
||||
self.args.outs_persist_no_cache,
|
||||
self.args.checkpoints,
|
||||
self.args.params,
|
||||
self.args.command,
|
||||
]
|
||||
): # pragma: no cover
|
||||
logger.error(
|
||||
"too few arguments. Specify at least one: `-d`, `-o`, `-O`, "
|
||||
"`-m`, `-M`, `-p`, `--plots`, `--plots-no-cache`, "
|
||||
"`--outs-persist`, `--outs-persist-no-cache`, `command`."
|
||||
)
|
||||
return 1
|
||||
|
||||
kwargs = vars(self.args)
|
||||
kwargs.update(
|
||||
{
|
||||
"cmd": parse_cmd(self.args.command),
|
||||
"fname": kwargs.pop("file"),
|
||||
"no_exec": self.args.no_exec or bool(self.args.checkpoints),
|
||||
"run_cache": not kwargs.pop("no_run_cache"),
|
||||
}
|
||||
)
|
||||
try:
|
||||
self.repo.run(**kwargs)
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from dvc.commands.stage import _add_common_args
|
||||
|
||||
RUN_HELP = "Generate a dvc.yaml file from a command and execute the command."
|
||||
run_parser = subparsers.add_parser(
|
||||
"run",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(RUN_HELP, "run"),
|
||||
help=RUN_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
run_parser.add_argument("-n", "--name", help="Stage name.")
|
||||
run_parser.add_argument("--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(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
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(
|
||||
"--no-run-cache",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Execute the command even if this stage has already been run "
|
||||
"with the same command/dependencies/outputs/etc before."
|
||||
),
|
||||
)
|
||||
run_parser.set_defaults(func=CmdRun)
|
||||
+30
-18
@@ -1,5 +1,6 @@
|
||||
import argparse
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from itertools import chain, filterfalse
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List
|
||||
|
||||
@@ -119,8 +120,22 @@ def parse_cmd(commands: List[str]) -> str:
|
||||
return " ".join(map(quote_argument, commands))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _disable_logging(highest_level=logging.CRITICAL):
|
||||
previous_level = logging.root.manager.disable
|
||||
|
||||
logging.disable(highest_level)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logging.disable(previous_level)
|
||||
|
||||
|
||||
class CmdStageAdd(CmdBase):
|
||||
def run(self):
|
||||
from dvc.repo import lock_repo
|
||||
|
||||
kwargs = vars(self.args)
|
||||
kwargs.update(
|
||||
{
|
||||
@@ -128,7 +143,15 @@ class CmdStageAdd(CmdBase):
|
||||
"params": parse_params(self.args.params),
|
||||
}
|
||||
)
|
||||
self.repo.stage.add(**kwargs)
|
||||
|
||||
with self.repo.scm_context, lock_repo(self.repo):
|
||||
with _disable_logging(logging.INFO):
|
||||
stage = self.repo.stage.add(**kwargs)
|
||||
logger.info("Added stage %r in %r", stage.addressing, stage.relpath)
|
||||
if self.args.run:
|
||||
stage.run()
|
||||
stage.dump(update_pipeline=False)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -172,23 +195,6 @@ def _add_common_args(parser):
|
||||
help="Declare output file or directory (do not put into DVC cache).",
|
||||
metavar="<filename>",
|
||||
).complete = completion.FILE
|
||||
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(
|
||||
"--external",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allow outputs that are outside of the DVC repository.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--outs-persist",
|
||||
action="append",
|
||||
@@ -257,6 +263,12 @@ def _add_common_args(parser):
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Execute the stage after generating it.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"command",
|
||||
nargs=argparse.REMAINDER,
|
||||
|
||||
@@ -31,8 +31,7 @@ class CmdUpdate(CmdBase):
|
||||
def add_parser(subparsers, parent_parser):
|
||||
UPDATE_HELP = (
|
||||
"Update data artifact imported (via dvc import or dvc import-url) "
|
||||
"from an external DVC repository or URL, or update data from a "
|
||||
"DVC worktree remote."
|
||||
"from an external DVC repository or URL."
|
||||
)
|
||||
update_parser = subparsers.add_parser(
|
||||
"update",
|
||||
|
||||
+7
-27
@@ -22,7 +22,6 @@ from typing import (
|
||||
from funcy import reraise
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
from dvc.ui.table import CellT
|
||||
|
||||
|
||||
@@ -35,7 +34,7 @@ def with_value(value, default):
|
||||
|
||||
|
||||
class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
def __init__(self, columns: Sequence[str], fill_value: str = ""):
|
||||
def __init__(self, columns: Sequence[str], fill_value: Optional[str] = ""):
|
||||
self._columns: Dict[str, Column] = {name: Column() for name in columns}
|
||||
self._keys: List[str] = list(columns)
|
||||
self._fill_value = fill_value
|
||||
@@ -58,7 +57,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
return self._columns[name]
|
||||
|
||||
def items(self) -> ItemsView[str, Column]:
|
||||
projection = {k: self.column(k) for k in self.keys()} # noqa: SIM118
|
||||
projection = {k: self.column(k) for k in self.keys()}
|
||||
return projection.items()
|
||||
|
||||
def keys(self) -> List[str]:
|
||||
@@ -161,24 +160,6 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
writer.writerow(row)
|
||||
return buff.getvalue()
|
||||
|
||||
def to_parallel_coordinates(
|
||||
self, output_path: "StrPath", color_by: Optional[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:
|
||||
self._columns[name] = Column([self._fill_value] * len(self))
|
||||
self._keys.append(name)
|
||||
@@ -190,8 +171,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
self.add_column(key)
|
||||
|
||||
row: List["CellT"] = [
|
||||
with_value(d.get(key), self._fill_value)
|
||||
for key in self.keys() # noqa: SIM118
|
||||
with_value(d.get(key), self._fill_value) for key in self.keys()
|
||||
]
|
||||
self.append(row)
|
||||
|
||||
@@ -209,7 +189,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
keys = self.keys() if cols is None else set(cols)
|
||||
return [{k: self._columns[k][i] for k in keys} for i in range(len(self))]
|
||||
|
||||
def dropna( # noqa: C901
|
||||
def dropna( # noqa: C901, PLR0912
|
||||
self,
|
||||
axis: str = "rows",
|
||||
how="any",
|
||||
@@ -246,7 +226,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
to_drop -= match_line
|
||||
|
||||
if axis == "rows":
|
||||
for name in self.keys(): # noqa: SIM118
|
||||
for name in self.keys():
|
||||
self._columns[name] = Column(
|
||||
[x for n, x in enumerate(self._columns[name]) if n not in to_drop]
|
||||
)
|
||||
@@ -294,7 +274,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
else:
|
||||
unique_rows.append(tuple_row)
|
||||
|
||||
for name in self.keys(): # noqa: SIM118
|
||||
for name in self.keys():
|
||||
self._columns[name] = Column(
|
||||
[
|
||||
x
|
||||
@@ -371,7 +351,7 @@ def diff_table(
|
||||
return td
|
||||
|
||||
|
||||
def show_diff(
|
||||
def show_diff( # noqa: PLR0913
|
||||
diff,
|
||||
title: str,
|
||||
old: bool = True,
|
||||
|
||||
+59
-32
@@ -79,9 +79,6 @@ class Config(dict):
|
||||
ConfigError: thrown if config has an invalid format.
|
||||
"""
|
||||
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
SYSTEM_LEVELS = ("system", "global")
|
||||
REPO_LEVELS = ("repo", "local")
|
||||
# In the order they shadow each other
|
||||
@@ -96,6 +93,8 @@ class Config(dict):
|
||||
validate: bool = True,
|
||||
fs: Optional["FileSystem"] = None,
|
||||
config: Optional["DictStrAny"] = None,
|
||||
remote: Optional[str] = None,
|
||||
remote_config: Optional["DictStrAny"] = None,
|
||||
): # pylint: disable=super-init-not-called
|
||||
from dvc.fs import LocalFileSystem
|
||||
|
||||
@@ -103,28 +102,34 @@ class Config(dict):
|
||||
self.wfs = LocalFileSystem()
|
||||
self.fs = fs or self.wfs
|
||||
|
||||
if not dvc_dir:
|
||||
try:
|
||||
from dvc.repo import Repo
|
||||
if dvc_dir:
|
||||
self.dvc_dir = self.fs.path.abspath(dvc_dir)
|
||||
|
||||
self.dvc_dir = Repo.find_dvc_dir()
|
||||
except NotDvcRepoError:
|
||||
self.dvc_dir = None
|
||||
else:
|
||||
self.dvc_dir = self.fs.path.abspath(self.fs.path.realpath(dvc_dir))
|
||||
self.load(
|
||||
validate=validate, config=config, remote=remote, remote_config=remote_config
|
||||
)
|
||||
|
||||
self.load(validate=validate, config=config)
|
||||
@classmethod
|
||||
def from_cwd(cls, fs: Optional["FileSystem"] = None, **kwargs):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
dvc_dir = Repo.find_dvc_dir(fs=fs)
|
||||
except NotDvcRepoError:
|
||||
dvc_dir = None
|
||||
|
||||
return cls(dvc_dir=dvc_dir, fs=fs, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_dir(cls, level):
|
||||
from appdirs import site_config_dir, user_config_dir
|
||||
from dvc.dirs import global_config_dir, system_config_dir
|
||||
|
||||
assert level in ("global", "system")
|
||||
|
||||
if level == "global":
|
||||
return user_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
return global_config_dir()
|
||||
if level == "system":
|
||||
return site_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
return system_config_dir()
|
||||
|
||||
@cached_property
|
||||
def files(self) -> Dict[str, str]:
|
||||
@@ -153,7 +158,13 @@ class Config(dict):
|
||||
with open(config_file, "w+", encoding="utf-8"):
|
||||
return Config(dvc_dir)
|
||||
|
||||
def load(self, validate: bool = True, config: Optional["DictStrAny"] = None):
|
||||
def load(
|
||||
self,
|
||||
validate: bool = True,
|
||||
config: Optional["DictStrAny"] = None,
|
||||
remote: Optional[str] = None,
|
||||
remote_config: Optional["DictStrAny"] = None,
|
||||
):
|
||||
"""Loads config from all the config files.
|
||||
|
||||
Raises:
|
||||
@@ -168,34 +179,50 @@ class Config(dict):
|
||||
conf = self.validate(conf)
|
||||
|
||||
self.clear()
|
||||
self.update(conf)
|
||||
|
||||
# Add resolved default cache.dir
|
||||
if not self["cache"].get("dir") and self.dvc_dir:
|
||||
self["cache"]["dir"] = os.path.join(self.dvc_dir, "cache")
|
||||
if remote:
|
||||
conf["core"]["remote"] = remote
|
||||
|
||||
if remote_config:
|
||||
remote = remote or conf["core"].get("remote")
|
||||
if not remote:
|
||||
raise ValueError("Missing remote name")
|
||||
|
||||
merge(conf, {"remote": {remote: remote_config}})
|
||||
|
||||
self.update(conf)
|
||||
|
||||
def _get_fs(self, level):
|
||||
# NOTE: this might be a Gitfs, which doesn't see things outside of
|
||||
# the repo.
|
||||
return self.fs if level == "repo" else self.wfs
|
||||
|
||||
def _load_config(self, level):
|
||||
@staticmethod
|
||||
def load_file(path, fs=None) -> dict:
|
||||
from configobj import ConfigObj, ConfigObjError
|
||||
|
||||
from dvc.fs import localfs
|
||||
|
||||
fs = fs or localfs
|
||||
|
||||
with fs.open(path) as fobj:
|
||||
try:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
except ConfigObjError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
|
||||
return _parse_named(_lower_keys(conf_obj.dict()))
|
||||
|
||||
def _load_config(self, level):
|
||||
filename = self.files[level]
|
||||
fs = self._get_fs(level)
|
||||
|
||||
if fs.exists(filename):
|
||||
with fs.open(filename) as fobj:
|
||||
try:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
except ConfigObjError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
else:
|
||||
conf_obj = ConfigObj()
|
||||
return _parse_named(_lower_keys(conf_obj.dict()))
|
||||
try:
|
||||
return self.load_file(filename, fs=fs)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
def _save_config(self, level, conf_dict):
|
||||
from configobj import ConfigObj
|
||||
|
||||
+68
-21
@@ -1,8 +1,22 @@
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import walk_values
|
||||
from voluptuous import All, Any, Coerce, Invalid, Lower, Optional, Range, Schema
|
||||
from funcy import once, walk_values
|
||||
from voluptuous import (
|
||||
REMOVE_EXTRA,
|
||||
All,
|
||||
Any,
|
||||
Coerce,
|
||||
Exclusive,
|
||||
Invalid,
|
||||
Lower,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Bool = All(
|
||||
Lower,
|
||||
@@ -30,7 +44,7 @@ def supported_cache_type(types):
|
||||
return types
|
||||
|
||||
|
||||
def Choices(*choices):
|
||||
def Choices(*choices): # noqa: N802
|
||||
"""Checks that value belongs to the specified set of values
|
||||
|
||||
Args:
|
||||
@@ -40,7 +54,7 @@ def Choices(*choices):
|
||||
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))
|
||||
|
||||
|
||||
def ByUrl(mapping):
|
||||
def ByUrl(mapping): # noqa: N802
|
||||
schemas = walk_values(Schema, mapping)
|
||||
|
||||
def validate(data):
|
||||
@@ -49,7 +63,9 @@ def ByUrl(mapping):
|
||||
|
||||
parsed = urlparse(data["url"])
|
||||
# Windows absolute paths should really have scheme == "" (local)
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.netloc == "":
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
|
||||
return schemas[""](data)
|
||||
if not parsed.netloc:
|
||||
return schemas[""](data)
|
||||
if parsed.scheme not in schemas:
|
||||
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
|
||||
@@ -60,7 +76,28 @@ def ByUrl(mapping):
|
||||
|
||||
|
||||
class RelPath(str):
|
||||
pass
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class FeatureSchema(Schema):
|
||||
def __init__(self, schema, required=False):
|
||||
super().__init__(schema, required=required, extra=REMOVE_EXTRA)
|
||||
|
||||
@staticmethod
|
||||
@once
|
||||
def _log_deprecated(keys):
|
||||
# only run this once per session
|
||||
message = "%s config option%s unsupported"
|
||||
paths = ", ".join(f"'feature.{key}'" for key in keys)
|
||||
pluralize = " is" if len(keys) == 1 else "s are"
|
||||
logger.warning(message, paths, pluralize)
|
||||
|
||||
def __call__(self, data):
|
||||
ret = super().__call__(data)
|
||||
extra_keys = data.keys() - ret.keys()
|
||||
if extra_keys:
|
||||
self._log_deprecated(sorted(extra_keys))
|
||||
return ret
|
||||
|
||||
|
||||
REMOTE_COMMON = {
|
||||
@@ -114,16 +151,17 @@ SCHEMA = {
|
||||
Optional("autostage", default=False): Bool,
|
||||
Optional("experiments"): Bool, # obsoleted
|
||||
Optional("check_update", default=True): Bool,
|
||||
"site_cache_dir": str,
|
||||
"machine": Lower,
|
||||
},
|
||||
"cache": {
|
||||
"local": str,
|
||||
"s3": str,
|
||||
"gs": str,
|
||||
"hdfs": str,
|
||||
"webhdfs": str,
|
||||
"ssh": str,
|
||||
"azure": str,
|
||||
"local": str, # obsoleted
|
||||
"s3": str, # obsoleted
|
||||
"gs": str, # obsoleted
|
||||
"hdfs": str, # obsoleted
|
||||
"webhdfs": str, # obsoleted
|
||||
"ssh": str, # obsoleted
|
||||
"azure": str, # obsoleted
|
||||
# This is for default local cache
|
||||
"dir": str,
|
||||
**LOCAL_COMMON,
|
||||
@@ -178,6 +216,7 @@ SCHEMA = {
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
"allow_agent": Bool,
|
||||
"max_sessions": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
@@ -237,12 +276,12 @@ SCHEMA = {
|
||||
)
|
||||
},
|
||||
"state": {
|
||||
"dir": str,
|
||||
"dir": str, # obsoleted
|
||||
"row_limit": All(Coerce(int), Range(1)), # obsoleted
|
||||
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
|
||||
},
|
||||
"index": {
|
||||
"dir": str,
|
||||
"dir": str, # obsoleted
|
||||
},
|
||||
"machine": {
|
||||
str: {
|
||||
@@ -260,11 +299,12 @@ SCHEMA = {
|
||||
},
|
||||
},
|
||||
# section for experimental features
|
||||
"feature": {
|
||||
Optional("machine", default=False): Bool,
|
||||
# enabled by default. It's of no use, kept for backward compatibility.
|
||||
Optional("parametrization", default=True): Bool,
|
||||
},
|
||||
# only specified keys are validated, others get logged and then ignored/removed
|
||||
"feature": FeatureSchema(
|
||||
{
|
||||
Optional("machine", default=False): Bool,
|
||||
},
|
||||
),
|
||||
"plots": {
|
||||
"html_template": str,
|
||||
Optional("auto_open", default=False): Bool,
|
||||
@@ -285,7 +325,14 @@ SCHEMA = {
|
||||
},
|
||||
"hydra": {
|
||||
Optional("enabled", default=False): Bool,
|
||||
"config_dir": str,
|
||||
Exclusive("config_dir", "config_source"): str,
|
||||
Exclusive("config_module", "config_source"): str,
|
||||
"config_name": str,
|
||||
},
|
||||
"studio": {
|
||||
"token": str,
|
||||
"url": str,
|
||||
"repo_url": str,
|
||||
Optional("offline", default=False): Bool,
|
||||
},
|
||||
}
|
||||
|
||||
+25
-5
@@ -5,7 +5,7 @@ import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from subprocess import Popen
|
||||
from subprocess import Popen # nosec B404
|
||||
from typing import List
|
||||
|
||||
from dvc.env import DVC_DAEMON
|
||||
@@ -14,19 +14,28 @@ from dvc.utils import fix_env, is_binary
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs):
|
||||
def _suppress_resource_warning(popen: Popen):
|
||||
"""Sets the returncode to avoid ResourceWarning when popen is garbage collected."""
|
||||
# only use for daemon processes.
|
||||
# See https://bugs.python.org/issue38890.
|
||||
popen.returncode = 0
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs) -> Popen:
|
||||
prefix = [sys.executable]
|
||||
if not is_binary():
|
||||
main_entrypoint = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)), "__main__.py"
|
||||
)
|
||||
prefix += [main_entrypoint]
|
||||
return Popen(prefix + cmd, close_fds=True, shell=False, **kwargs)
|
||||
return Popen(
|
||||
prefix + cmd, close_fds=True, shell=False, **kwargs # nosec B603 # noqa: S603
|
||||
)
|
||||
|
||||
|
||||
def _spawn_windows(cmd, env):
|
||||
if sys.platform == "win32":
|
||||
from subprocess import (
|
||||
from subprocess import ( # nosec B404
|
||||
CREATE_NEW_PROCESS_GROUP,
|
||||
CREATE_NO_WINDOW,
|
||||
STARTF_USESHOWWINDOW,
|
||||
@@ -40,12 +49,22 @@ def _spawn_windows(cmd, env):
|
||||
startupinfo = STARTUPINFO()
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
|
||||
_popen(cmd, env=env, creationflags=creationflags, startupinfo=startupinfo)
|
||||
popen = _popen(
|
||||
cmd, env=env, creationflags=creationflags, startupinfo=startupinfo
|
||||
)
|
||||
_suppress_resource_warning(popen)
|
||||
|
||||
|
||||
def _spawn_posix(cmd, env):
|
||||
from dvc.cli import main
|
||||
|
||||
# `fork` will copy buffers, so we need to flush them before forking.
|
||||
# Otherwise, we will get duplicated outputs.
|
||||
if sys.stdout and not sys.stdout.closed:
|
||||
sys.stdout.flush()
|
||||
if sys.stderr and not sys.stderr.closed:
|
||||
sys.stderr.flush()
|
||||
|
||||
# NOTE: using os._exit instead of sys.exit, because dvc built
|
||||
# with PyInstaller has trouble with SystemExit exception and throws
|
||||
# errors such as "[26338] Failed to execute script __main__"
|
||||
@@ -72,6 +91,7 @@ def _spawn_posix(cmd, env):
|
||||
sys.stdin.close()
|
||||
sys.stdout.close()
|
||||
sys.stderr.close()
|
||||
os.closerange(0, 3)
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
# workaround for MacOS bug
|
||||
|
||||
+27
-3
@@ -80,7 +80,7 @@ class AsciiCanvas:
|
||||
|
||||
self.canvas[y][x] = char
|
||||
|
||||
def line(self, x0, y0, x1, y1, char): # noqa: C901
|
||||
def line(self, x0, y0, x1, y1, char): # noqa: C901, PLR0912
|
||||
"""Create a line on ASCII canvas.
|
||||
|
||||
Args:
|
||||
@@ -209,12 +209,36 @@ def draw(vertices, edges):
|
||||
Args:
|
||||
vertices (list): list of graph vertices.
|
||||
edges (list): list of graph edges.
|
||||
|
||||
Returns:
|
||||
str: ASCII representation
|
||||
|
||||
Example:
|
||||
>>> from dvc.dagascii import draw
|
||||
>>> vertices = [1, 2, 3, 4]
|
||||
>>> edges = [(1, 2), (2, 3), (2, 4), (1, 4)]
|
||||
>>> print(draw(vertices, edges))
|
||||
+---+ +---+
|
||||
| 3 | | 4 |
|
||||
+---+ *+---+
|
||||
* ** *
|
||||
* ** *
|
||||
* * *
|
||||
+---+ *
|
||||
| 2 | *
|
||||
+---+ *
|
||||
* *
|
||||
* *
|
||||
**
|
||||
+---+
|
||||
| 1 |
|
||||
+---+
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
# NOTE: coordinates might me negative, so we need to shift
|
||||
# everything to the positive plane before we actually draw it.
|
||||
Xs = [] # pylint: disable=invalid-name
|
||||
Ys = [] # pylint: disable=invalid-name
|
||||
Xs = [] # noqa: N806, pylint: disable=invalid-name
|
||||
Ys = [] # noqa: N806, pylint: disable=invalid-name
|
||||
|
||||
sug = _build_sugiyama_layout(vertices, edges)
|
||||
|
||||
|
||||
+169
-36
@@ -1,40 +1,69 @@
|
||||
"""Manages dvc remotes that user can use with push/pull/status commands."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, Set, Tuple
|
||||
|
||||
from dvc.config import NoRemoteError, RemoteConfigError
|
||||
from dvc.fs.callbacks import Callback
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_data.hashfile.db import get_index
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, name: str, path: str, fs: "FileSystem", **config):
|
||||
def __init__(self, name: str, path: str, fs: "FileSystem", *, index=None, **config):
|
||||
self.path = path
|
||||
self.fs = fs
|
||||
self.name = name
|
||||
self.index = index
|
||||
|
||||
self.worktree: bool = config.pop("worktree", False)
|
||||
self.config = config
|
||||
|
||||
@cached_property
|
||||
def odb(self) -> "HashFileDB":
|
||||
from dvc.cachemgr import CacheManager
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
|
||||
|
||||
path = self.path
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(path, ".dvc", "cache")
|
||||
path = self.fs.path.join(
|
||||
path, ".dvc", CacheManager.FILES_DIR, DEFAULT_ALGORITHM
|
||||
)
|
||||
else:
|
||||
path = self.fs.path.join(path, CacheManager.FILES_DIR, DEFAULT_ALGORITHM)
|
||||
return get_odb(self.fs, path, hash_name=DEFAULT_ALGORITHM, **self.config)
|
||||
|
||||
return get_odb(self.fs, path, **self.config)
|
||||
@cached_property
|
||||
def legacy_odb(self) -> "HashFileDB":
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
path = self.path
|
||||
return get_odb(self.fs, path, hash_name="md5-dos2unix", **self.config)
|
||||
|
||||
|
||||
def _split_legacy_hash_infos(
|
||||
hash_infos: Iterable["HashInfo"],
|
||||
) -> Tuple[Set["HashInfo"], Set["HashInfo"]]:
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
legacy = set()
|
||||
default = set()
|
||||
for hi in hash_infos:
|
||||
if hi.name in LEGACY_HASH_NAMES:
|
||||
legacy.add(hi)
|
||||
else:
|
||||
default.add(hi)
|
||||
return legacy, default
|
||||
|
||||
|
||||
class DataCloud:
|
||||
@@ -62,7 +91,7 @@ class DataCloud:
|
||||
if name:
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
cls, config, fs_path = get_cloud_fs(self.repo.config, name=name)
|
||||
|
||||
if config.get("worktree"):
|
||||
version_aware = config.get("version_aware")
|
||||
@@ -74,19 +103,23 @@ class DataCloud:
|
||||
config["version_aware"] = True
|
||||
|
||||
fs = cls(**config)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return Remote(name, fs_path, fs, **config)
|
||||
config["tmp_dir"] = self.repo.site_cache_dir
|
||||
if self.repo.data_index is not None:
|
||||
index = self.repo.data_index.view(("remote", name))
|
||||
else:
|
||||
index = None
|
||||
return Remote(name, fs_path, fs, index=index, **config)
|
||||
|
||||
if bool(self.repo.config["remote"]):
|
||||
error_msg = (
|
||||
"no remote specified. Setup default remote with\n"
|
||||
f"no remote specified in {self.repo}. Setup default remote with\n"
|
||||
" dvc remote default <remote name>\n"
|
||||
"or use:\n"
|
||||
" dvc {} -r <remote name>".format(command)
|
||||
f" dvc {command} -r <remote name>"
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
"no remote specified. Create a default remote with\n"
|
||||
f"no remote specified in {self.repo}. Create a default remote with\n"
|
||||
" dvc remote add -d <remote name> <remote url>"
|
||||
)
|
||||
|
||||
@@ -96,12 +129,17 @@ class DataCloud:
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
hash_name: str = "md5",
|
||||
) -> "HashFileDB":
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
remote = self.get_remote(name=name, command=command)
|
||||
if remote.fs.version_aware or remote.worktree:
|
||||
raise NoRemoteError(
|
||||
f"'{command}' is unsupported for cloud versioned remotes"
|
||||
)
|
||||
if hash_name in LEGACY_HASH_NAMES:
|
||||
return remote.legacy_odb
|
||||
return remote.odb
|
||||
|
||||
def _log_missing(self, status: "CompareStatusResult"):
|
||||
@@ -145,16 +183,47 @@ class DataCloud:
|
||||
By default remote from core.remote config option is used.
|
||||
odb: optional ODB to push to. Overrides remote.
|
||||
"""
|
||||
odb = odb or self.get_remote_odb(remote, "push")
|
||||
return self.transfer(
|
||||
self.repo.odb.local,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=self.repo.odb.local,
|
||||
validate_status=self._log_missing,
|
||||
)
|
||||
if odb is not None:
|
||||
return self._push(objs, jobs=jobs, odb=odb)
|
||||
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
|
||||
result = TransferResult(set(), set())
|
||||
if legacy_objs:
|
||||
odb = self.get_remote_odb(remote, "push", hash_name="md5-dos2unix")
|
||||
t, f = self._push(legacy_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
if default_objs:
|
||||
odb = self.get_remote_odb(remote, "push")
|
||||
t, f = self._push(default_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
return result
|
||||
|
||||
def _push(
|
||||
self,
|
||||
objs: Iterable["HashInfo"],
|
||||
*,
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
) -> "TransferResult":
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Pushing to {odb.fs.unstrip_protocol(odb.path)}",
|
||||
unit="file",
|
||||
) as cb:
|
||||
return self.transfer(
|
||||
cache,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=cache,
|
||||
validate_status=self._log_missing,
|
||||
callback=cb,
|
||||
)
|
||||
|
||||
def pull(
|
||||
self,
|
||||
@@ -172,17 +241,49 @@ class DataCloud:
|
||||
By default remote from core.remote config option is used.
|
||||
odb: optional ODB to pull from. Overrides remote.
|
||||
"""
|
||||
odb = odb or self.get_remote_odb(remote, "pull")
|
||||
return self.transfer(
|
||||
odb,
|
||||
self.repo.odb.local,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
src_index=get_index(odb),
|
||||
cache_odb=self.repo.odb.local,
|
||||
verify=odb.verify,
|
||||
validate_status=self._log_missing,
|
||||
)
|
||||
if odb is not None:
|
||||
return self._pull(objs, jobs=jobs, odb=odb)
|
||||
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
|
||||
result = TransferResult(set(), set())
|
||||
if legacy_objs:
|
||||
odb = self.get_remote_odb(remote, "pull", hash_name="md5-dos2unix")
|
||||
assert odb.hash_name == "md5-dos2unix"
|
||||
t, f = self._pull(legacy_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
if default_objs:
|
||||
odb = self.get_remote_odb(remote, "pull")
|
||||
t, f = self._pull(default_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
return result
|
||||
|
||||
def _pull(
|
||||
self,
|
||||
objs: Iterable["HashInfo"],
|
||||
*,
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
) -> "TransferResult":
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Fetching from {odb.fs.unstrip_protocol(odb.path)}",
|
||||
unit="file",
|
||||
) as cb:
|
||||
return self.transfer(
|
||||
odb,
|
||||
cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
src_index=get_index(odb),
|
||||
cache_odb=cache,
|
||||
verify=odb.verify,
|
||||
validate_status=self._log_missing,
|
||||
callback=cb,
|
||||
)
|
||||
|
||||
def status(
|
||||
self,
|
||||
@@ -201,17 +302,49 @@ class DataCloud:
|
||||
is used.
|
||||
odb: optional ODB to check status from. Overrides remote.
|
||||
"""
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
|
||||
if odb is not None:
|
||||
return self._status(objs, jobs=jobs, odb=odb)
|
||||
result = CompareStatusResult(set(), set(), set(), set())
|
||||
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
|
||||
if legacy_objs:
|
||||
odb = self.get_remote_odb(remote, "status", hash_name="md5-dos2unix")
|
||||
assert odb.hash_name == "md5-dos2unix"
|
||||
o, m, n, d = self._status(legacy_objs, jobs=jobs, odb=odb)
|
||||
result.ok.update(o)
|
||||
result.missing.update(m)
|
||||
result.new.update(n)
|
||||
result.deleted.update(d)
|
||||
if default_objs:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
o, m, n, d = self._status(default_objs, jobs=jobs, odb=odb)
|
||||
result.ok.update(o)
|
||||
result.missing.update(m)
|
||||
result.new.update(n)
|
||||
result.deleted.update(d)
|
||||
return result
|
||||
|
||||
def _status(
|
||||
self,
|
||||
objs: Iterable["HashInfo"],
|
||||
*,
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
):
|
||||
from dvc_data.hashfile.status import compare_status
|
||||
|
||||
if not odb:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
return compare_status(
|
||||
self.repo.odb.local,
|
||||
cache,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=self.repo.odb.local,
|
||||
cache_odb=cache,
|
||||
)
|
||||
|
||||
def get_url_for(self, remote, checksum):
|
||||
|
||||
@@ -14,7 +14,6 @@ from .repo import RepoDependency
|
||||
SCHEMA: Mapping[str, Any] = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**RepoDependency.REPO_SCHEMA,
|
||||
**ParamsDependency.PARAM_SCHEMA,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
}
|
||||
|
||||
@@ -36,7 +35,8 @@ def loadd_from(stage, d_list):
|
||||
for d in d_list:
|
||||
p = d.pop(Output.PARAM_PATH, None)
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
ret.append(_get(stage, p, d, files=files))
|
||||
hash_name = d.pop(Output.PARAM_HASH, None)
|
||||
ret.append(_get(stage, p, d, files=files, hash_name=hash_name))
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class Dependency(Output):
|
||||
self.fs_path = self.fs.path.version_path(self.fs_path, self.meta.version_id)
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
fs_download(self.fs, self.fs_path, to, jobs=jobs)
|
||||
fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
|
||||
|
||||
def save(self):
|
||||
super().save()
|
||||
|
||||
@@ -2,10 +2,9 @@ import logging
|
||||
import os
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from typing import Dict, cast
|
||||
from typing import Any, Dict
|
||||
|
||||
import dpath.util
|
||||
from voluptuous import Any
|
||||
import dpath
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils.serialize import ParseError, load_path
|
||||
@@ -34,7 +33,6 @@ class BadParamFileError(DvcException):
|
||||
|
||||
class ParamsDependency(Dependency):
|
||||
PARAM_PARAMS = "params"
|
||||
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
|
||||
DEFAULT_PARAMS_FILE = "params.yaml"
|
||||
|
||||
def __init__(self, stage, path, params=None, repo=None):
|
||||
@@ -48,6 +46,7 @@ class ParamsDependency(Dependency):
|
||||
repo = repo or stage.repo
|
||||
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
|
||||
super().__init__(stage, path, repo=repo)
|
||||
self.hash_name = self.PARAM_PARAMS
|
||||
self.hash_info = hash_info
|
||||
|
||||
def dumpd(self, **kwargs):
|
||||
@@ -87,17 +86,17 @@ class ParamsDependency(Dependency):
|
||||
if flatten:
|
||||
for param in self.params:
|
||||
try:
|
||||
ret[param] = dpath.util.get(config, param, separator=".")
|
||||
ret[param] = dpath.get(config, param, separator=".")
|
||||
except KeyError:
|
||||
continue
|
||||
return ret
|
||||
|
||||
from dpath.util import merge
|
||||
from dpath import merge
|
||||
|
||||
for param in self.params:
|
||||
merge(
|
||||
ret,
|
||||
dpath.util.search(config, param, separator="."),
|
||||
dpath.search(config, param, separator="."),
|
||||
separator=".",
|
||||
)
|
||||
return ret
|
||||
@@ -111,7 +110,8 @@ class ParamsDependency(Dependency):
|
||||
from funcy import ldistinct
|
||||
|
||||
status: Dict[str, Any] = defaultdict(dict)
|
||||
info = cast(dict, self.hash_info.value) if self.hash_info else {}
|
||||
assert isinstance(self.hash_info.value, dict)
|
||||
info = self.hash_info.value if self.hash_info else {}
|
||||
actual = self.read_params()
|
||||
|
||||
# NOTE: we want to preserve the order of params as specified in the
|
||||
|
||||
+63
-149
@@ -1,24 +1,14 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Union
|
||||
|
||||
from voluptuous import Required
|
||||
from voluptuous import Any, Required
|
||||
|
||||
from dvc.prompt import confirm
|
||||
from dvc.utils import as_posix
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import ContextManager
|
||||
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc.fs import DVCFileSystem
|
||||
from dvc.stage import Stage
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
|
||||
class RepoDependency(Dependency):
|
||||
@@ -26,21 +16,26 @@ class RepoDependency(Dependency):
|
||||
PARAM_URL = "url"
|
||||
PARAM_REV = "rev"
|
||||
PARAM_REV_LOCK = "rev_lock"
|
||||
PARAM_CONFIG = "config"
|
||||
PARAM_REMOTE = "remote"
|
||||
|
||||
REPO_SCHEMA = {
|
||||
PARAM_REPO: {
|
||||
Required(PARAM_URL): str,
|
||||
PARAM_REV: str,
|
||||
PARAM_REV_LOCK: str,
|
||||
PARAM_CONFIG: str,
|
||||
PARAM_REMOTE: Any(str, dict),
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, def_repo: Dict[str, str], stage: "Stage", *args, **kwargs):
|
||||
self.def_repo = def_repo
|
||||
self._objs: Dict[str, "HashFile"] = {}
|
||||
self._meta: Dict[str, "Meta"] = {}
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
self.fs = self._make_fs()
|
||||
self.fs_path = as_posix(self.def_path)
|
||||
|
||||
def _parse_path(self, fs, fs_path): # noqa: ARG002
|
||||
return None
|
||||
|
||||
@@ -52,8 +47,8 @@ class RepoDependency(Dependency):
|
||||
return f"{self.def_path} ({self.def_repo[self.PARAM_URL]})"
|
||||
|
||||
def workspace_status(self):
|
||||
current = self.get_obj(locked=True).hash_info
|
||||
updated = self.get_obj(locked=False).hash_info
|
||||
current = self._make_fs(locked=True).repo.get_rev()
|
||||
updated = self._make_fs(locked=False).repo.get_rev()
|
||||
|
||||
if current != updated:
|
||||
return {str(self): "update available"}
|
||||
@@ -64,33 +59,39 @@ class RepoDependency(Dependency):
|
||||
return self.workspace_status()
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
rev = self.fs.repo.get_rev()
|
||||
if self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = rev
|
||||
|
||||
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
repo = {self.PARAM_URL: self.def_repo[self.PARAM_URL]}
|
||||
|
||||
def download(self, to: "Output", jobs: Optional[int] = None):
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
rev = self.def_repo.get(self.PARAM_REV)
|
||||
if rev:
|
||||
repo[self.PARAM_REV] = self.def_repo[self.PARAM_REV]
|
||||
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
||||
rev_lock = self.def_repo.get(self.PARAM_REV_LOCK)
|
||||
if rev_lock:
|
||||
repo[self.PARAM_REV_LOCK] = rev_lock
|
||||
|
||||
obj = self.get_obj()
|
||||
checkout(
|
||||
to.fs_path,
|
||||
to.fs,
|
||||
obj,
|
||||
self.repo.odb.local,
|
||||
ignore=None,
|
||||
state=self.repo.state,
|
||||
prompt=confirm,
|
||||
)
|
||||
config = self.def_repo.get(self.PARAM_CONFIG)
|
||||
if config:
|
||||
repo[self.PARAM_CONFIG] = config
|
||||
|
||||
remote = self.def_repo.get(self.PARAM_REMOTE)
|
||||
if remote:
|
||||
repo[self.PARAM_REMOTE] = remote
|
||||
|
||||
return {
|
||||
self.PARAM_PATH: self.def_path,
|
||||
self.PARAM_REPO: repo,
|
||||
}
|
||||
|
||||
def update(self, rev: Optional[str] = None):
|
||||
if rev:
|
||||
self.def_repo[self.PARAM_REV] = rev
|
||||
with self._make_repo(locked=False) as repo:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
|
||||
self.fs = self._make_fs(rev=rev, locked=False)
|
||||
self.def_repo[self.PARAM_REV_LOCK] = self.fs.repo.get_rev()
|
||||
|
||||
def changed_checksum(self) -> bool:
|
||||
# From current repo point of view what describes RepoDependency is its
|
||||
@@ -98,124 +99,37 @@ class RepoDependency(Dependency):
|
||||
# immutable, hence its impossible for checksum to change.
|
||||
return False
|
||||
|
||||
def get_used_objs(self, **kwargs) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
|
||||
used, _, _ = self._get_used_and_obj(**kwargs)
|
||||
return used
|
||||
def _make_fs(
|
||||
self, rev: Optional[str] = None, locked: bool = True
|
||||
) -> "DVCFileSystem":
|
||||
from dvc.config import Config
|
||||
from dvc.fs import DVCFileSystem
|
||||
|
||||
def _get_used_and_obj(
|
||||
self, obj_only: bool = False, **kwargs
|
||||
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "Meta", "HashFile"]:
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputOrStageError, PathMissingError
|
||||
from dvc.utils import as_posix
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.tree import Tree, TreeError
|
||||
rem = self.def_repo.get("remote")
|
||||
if isinstance(rem, dict):
|
||||
remote = None # type: ignore[unreachable]
|
||||
remote_config = rem
|
||||
else:
|
||||
remote = rem
|
||||
remote_config = None
|
||||
|
||||
local_odb = self.repo.odb.local
|
||||
locked = kwargs.pop("locked", True)
|
||||
with self._make_repo(locked=locked, cache_dir=local_odb.path) as repo:
|
||||
used_obj_ids = defaultdict(set)
|
||||
rev = repo.get_rev()
|
||||
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = rev
|
||||
conf = self.def_repo.get("config")
|
||||
if conf:
|
||||
config = Config.load_file(conf)
|
||||
else:
|
||||
config = {}
|
||||
|
||||
if not obj_only:
|
||||
try:
|
||||
for odb, obj_ids in repo.used_objs(
|
||||
[os.path.join(repo.root_dir, self.def_path)],
|
||||
force=True,
|
||||
jobs=kwargs.get("jobs"),
|
||||
recursive=True,
|
||||
).items():
|
||||
if odb is None:
|
||||
odb = repo.cloud.get_remote_odb()
|
||||
odb.read_only = True
|
||||
self._check_circular_import(odb, obj_ids)
|
||||
used_obj_ids[odb].update(obj_ids)
|
||||
except (NoRemoteError, NoOutputOrStageError):
|
||||
pass # noqa: S110
|
||||
config["cache"] = self.repo.config["cache"]
|
||||
config["cache"]["dir"] = self.repo.cache.local_cache_dir
|
||||
|
||||
try:
|
||||
object_store, meta, obj = build(
|
||||
local_odb,
|
||||
as_posix(self.def_path),
|
||||
repo.dvcfs,
|
||||
local_odb.fs.PARAM_CHECKSUM,
|
||||
)
|
||||
except (FileNotFoundError, TreeError) as exc:
|
||||
raise PathMissingError(
|
||||
self.def_path, self.def_repo[self.PARAM_URL]
|
||||
) from exc
|
||||
object_store = copy(object_store)
|
||||
object_store.read_only = True
|
||||
|
||||
self._objs[rev] = obj
|
||||
self._meta[rev] = meta
|
||||
|
||||
used_obj_ids[object_store].add(obj.hash_info)
|
||||
if isinstance(obj, Tree):
|
||||
used_obj_ids[object_store].update(oid for _, _, oid in obj)
|
||||
return used_obj_ids, meta, obj
|
||||
|
||||
def _check_circular_import(self, odb: "ObjectDB", obj_ids: Set["HashInfo"]) -> None:
|
||||
from dvc.exceptions import CircularImportError
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
if not isinstance(odb, ReferenceHashFileDB):
|
||||
return
|
||||
|
||||
def iter_objs():
|
||||
for hash_info in obj_ids:
|
||||
if hash_info.isdir:
|
||||
tree = Tree.load(odb, hash_info)
|
||||
yield from (odb.get(hi.value) for _, _, hi in tree)
|
||||
else:
|
||||
assert hash_info.value
|
||||
yield odb.get(hash_info.value)
|
||||
|
||||
checked_urls = set()
|
||||
for obj in iter_objs():
|
||||
if not isinstance(obj.fs, DVCFileSystem):
|
||||
continue
|
||||
if obj.fs.repo_url in checked_urls or obj.fs.repo.root_dir in checked_urls:
|
||||
continue
|
||||
self_url = self.repo.url or self.repo.root_dir
|
||||
if (
|
||||
obj.fs.repo_url is not None
|
||||
and obj.fs.repo_url == self_url
|
||||
or obj.fs.repo.root_dir == self.repo.root_dir
|
||||
):
|
||||
raise CircularImportError(self, obj.fs.repo_url, self_url)
|
||||
checked_urls.update([obj.fs.repo_url, obj.fs.repo.root_dir])
|
||||
|
||||
def get_obj(self, filter_info=None, **kwargs):
|
||||
locked = kwargs.get("locked", True)
|
||||
rev = self._get_rev(locked=locked)
|
||||
if rev in self._objs:
|
||||
return self._objs[rev]
|
||||
_, _, obj = self._get_used_and_obj(
|
||||
obj_only=True, filter_info=filter_info, **kwargs
|
||||
return DVCFileSystem(
|
||||
url=self.def_repo[self.PARAM_URL],
|
||||
rev=rev or self._get_rev(locked=locked),
|
||||
subrepos=True,
|
||||
config=config,
|
||||
remote=remote,
|
||||
remote_config=remote_config,
|
||||
)
|
||||
return obj
|
||||
|
||||
def get_meta(self, filter_info=None, **kwargs):
|
||||
locked = kwargs.get("locked", True)
|
||||
rev = self._get_rev(locked=locked)
|
||||
if rev in self._meta:
|
||||
return self._meta[rev]
|
||||
_, meta, _ = self._get_used_and_obj(
|
||||
obj_only=True, filter_info=filter_info, **kwargs
|
||||
)
|
||||
return meta
|
||||
|
||||
def _make_repo(self, locked: bool = True, **kwargs) -> "ContextManager[Repo]":
|
||||
from dvc.external_repo import external_repo
|
||||
|
||||
d = self.def_repo
|
||||
rev = self._get_rev(locked=locked)
|
||||
return external_repo(d[self.PARAM_URL], rev=rev, **kwargs)
|
||||
|
||||
def _get_rev(self, locked: bool = True):
|
||||
d = self.def_repo
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
|
||||
import platformdirs
|
||||
|
||||
from . import env
|
||||
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
|
||||
def system_config_dir():
|
||||
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.site_config_dir(
|
||||
APPNAME, APPAUTHOR
|
||||
)
|
||||
|
||||
|
||||
def global_config_dir():
|
||||
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.user_config_dir(
|
||||
APPNAME, APPAUTHOR
|
||||
)
|
||||
|
||||
|
||||
def site_cache_dir():
|
||||
return os.getenv(env.DVC_SITE_CACHE_DIR) or platformdirs.site_cache_dir(
|
||||
APPNAME, APPAUTHOR, opinion=True
|
||||
)
|
||||
+15
-56
@@ -14,7 +14,6 @@ from typing import (
|
||||
)
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
|
||||
from dvc.stage import serialize
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileBadNameError,
|
||||
@@ -36,7 +35,6 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
_T = TypeVar("_T")
|
||||
|
||||
DVC_FILE = "Dvcfile"
|
||||
DVC_FILE_SUFFIX = ".dvc"
|
||||
PROJECT_FILE = "dvc.yaml"
|
||||
LOCK_FILE = "dvc.lock"
|
||||
@@ -56,10 +54,7 @@ class ParametrizedDumpError(DvcException):
|
||||
|
||||
|
||||
def is_valid_filename(path):
|
||||
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [
|
||||
DVC_FILE,
|
||||
PROJECT_FILE,
|
||||
]
|
||||
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) == PROJECT_FILE
|
||||
|
||||
|
||||
def is_dvc_file(path):
|
||||
@@ -102,9 +97,7 @@ class FileMixin:
|
||||
self.verify = verify
|
||||
|
||||
def __repr__(self):
|
||||
return "{}: {}".format(
|
||||
self.__class__.__name__, relpath(self.path, self.repo.root_dir)
|
||||
)
|
||||
return f"{self.__class__.__name__}: {relpath(self.path, self.repo.root_dir)}"
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.path)
|
||||
@@ -187,11 +180,12 @@ class FileMixin:
|
||||
|
||||
class SingleStageFile(FileMixin):
|
||||
from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA
|
||||
from dvc.stage.loader import SingleStageLoader as LOADER
|
||||
from dvc.stage.loader import SingleStageLoader as LOADER # noqa: N814
|
||||
|
||||
metrics: List[str] = []
|
||||
plots: Any = {}
|
||||
params: List[str] = []
|
||||
artifacts: Dict[str, Optional[Dict[str, Any]]] = {}
|
||||
|
||||
@property
|
||||
def stage(self) -> "Stage":
|
||||
@@ -231,7 +225,7 @@ class ProjectFile(FileMixin):
|
||||
"""Abstraction for pipelines file, .yaml + .lock combined."""
|
||||
|
||||
from dvc.schema import COMPILED_MULTI_STAGE_SCHEMA as SCHEMA
|
||||
from dvc.stage.loader import StageLoader as LOADER
|
||||
from dvc.stage.loader import StageLoader as LOADER # noqa: N814
|
||||
|
||||
@property
|
||||
def _lockfile(self):
|
||||
@@ -323,6 +317,10 @@ class ProjectFile(FileMixin):
|
||||
def params(self) -> List[str]:
|
||||
return self.contents.get("params", [])
|
||||
|
||||
@property
|
||||
def artifacts(self) -> Dict[str, Optional[Dict[str, Any]]]:
|
||||
return self.contents.get("artifacts", {})
|
||||
|
||||
def remove(self, force=False):
|
||||
if not force:
|
||||
logger.warning("Cannot remove pipeline file.")
|
||||
@@ -353,36 +351,8 @@ class ProjectFile(FileMixin):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def get_lockfile_schema(d):
|
||||
from dvc.schema import COMPILED_LOCKFILE_V1_SCHEMA, COMPILED_LOCKFILE_V2_SCHEMA
|
||||
|
||||
schema = {
|
||||
LOCKFILE_VERSION.V1: COMPILED_LOCKFILE_V1_SCHEMA,
|
||||
LOCKFILE_VERSION.V2: COMPILED_LOCKFILE_V2_SCHEMA,
|
||||
}
|
||||
|
||||
version = LOCKFILE_VERSION.from_dict(d)
|
||||
return schema[version]
|
||||
|
||||
|
||||
def migrate_lock_v1_to_v2(d, version_info):
|
||||
stages = {k: v for k, v in d.items()}
|
||||
|
||||
for key in stages:
|
||||
d.pop(key)
|
||||
|
||||
# forcing order, meta should always be at the top
|
||||
d.update(version_info)
|
||||
d["stages"] = stages
|
||||
|
||||
|
||||
def lockfile_schema(data: _T) -> _T:
|
||||
schema = get_lockfile_schema(data)
|
||||
return schema(data)
|
||||
|
||||
|
||||
class Lockfile(FileMixin):
|
||||
SCHEMA = staticmethod(lockfile_schema) # type: ignore[assignment]
|
||||
from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA
|
||||
|
||||
def _verify_filename(self):
|
||||
pass # lockfile path is hardcoded, so no need to verify here
|
||||
@@ -396,24 +366,14 @@ class Lockfile(FileMixin):
|
||||
self._check_gitignored()
|
||||
return {}, ""
|
||||
|
||||
@property
|
||||
def latest_version_info(self):
|
||||
version = LOCKFILE_VERSION.V2.value # pylint:disable=no-member
|
||||
return {SCHEMA_KWD: version}
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
version = LOCKFILE_VERSION.from_dict(data)
|
||||
if version == LOCKFILE_VERSION.V1:
|
||||
logger.info("Migrating lock file '%s' from v1 to v2", self.relpath)
|
||||
migrate_lock_v1_to_v2(data, self.latest_version_info)
|
||||
else:
|
||||
if not data:
|
||||
data.update(self.latest_version_info)
|
||||
# order is important, meta should always be at the top
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
if not data:
|
||||
data.update({"schema": "2.0"})
|
||||
# order is important, meta should always be at the top
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
|
||||
data["stages"] = data.get("stages", {})
|
||||
modified = data["stages"].get(stage.name, {}) != stage_data.get(
|
||||
@@ -432,8 +392,7 @@ class Lockfile(FileMixin):
|
||||
return
|
||||
|
||||
d, _ = self._load_yaml(round_trip=True)
|
||||
version = LOCKFILE_VERSION.from_dict(d)
|
||||
data = d if version == LOCKFILE_VERSION.V1 else d.get("stages", {})
|
||||
data = d.get("stages", {})
|
||||
if stage.name not in data:
|
||||
return
|
||||
|
||||
|
||||
+14
-8
@@ -1,11 +1,17 @@
|
||||
DVC_CHECKPOINT = "DVC_CHECKPOINT"
|
||||
DVC_DAEMON = "DVC_DAEMON"
|
||||
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
|
||||
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
DVC_EXP_NAME = "DVC_EXP_NAME"
|
||||
DVC_GLOBAL_CONFIG_DIR = "DVC_GLOBAL_CONFIG_DIR"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
|
||||
DVC_PAGER = "DVC_PAGER"
|
||||
DVC_ROOT = "DVC_ROOT"
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
|
||||
DVC_EXP_NAME = "DVC_EXP_NAME"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
|
||||
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
|
||||
DVC_SHOW_TRACEBACK = "DVC_SHOW_TRACEBACK"
|
||||
DVC_SITE_CACHE_DIR = "DVC_SITE_CACHE_DIR"
|
||||
DVC_STUDIO_OFFLINE = "DVC_STUDIO_OFFLINE"
|
||||
DVC_STUDIO_REPO_URL = "DVC_STUDIO_REPO_URL"
|
||||
DVC_STUDIO_TOKEN = "DVC_STUDIO_TOKEN" # noqa: S105 # nosec B105
|
||||
DVC_STUDIO_URL = "DVC_STUDIO_URL"
|
||||
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
|
||||
|
||||
+35
-47
@@ -1,4 +1,5 @@
|
||||
"""Exceptions raised by the dvc."""
|
||||
import errno
|
||||
from typing import Dict, List
|
||||
|
||||
from dvc.utils import format_link
|
||||
@@ -16,6 +17,10 @@ class DvcException(Exception):
|
||||
class InvalidArgumentError(ValueError, DvcException):
|
||||
"""Thrown if arguments are invalid."""
|
||||
|
||||
def __init__(self, msg, *args):
|
||||
self.msg = msg
|
||||
super().__init__(msg, *args)
|
||||
|
||||
|
||||
class OutputDuplicationError(DvcException):
|
||||
"""Thrown if a file/directory is specified as an output in more than one
|
||||
@@ -31,14 +36,19 @@ class OutputDuplicationError(DvcException):
|
||||
|
||||
assert isinstance(output, str)
|
||||
assert all(hasattr(stage, "relpath") for stage in stages)
|
||||
msg = ""
|
||||
stage_names = [s.addressing for s in stages]
|
||||
stages_str = " ".join(stage_names)
|
||||
if len(stages) == 1:
|
||||
msg = "output '{}' is already specified in {}.".format(
|
||||
output, first(stages)
|
||||
)
|
||||
stage_name = first(stages)
|
||||
msg = f"output '{output}' is already specified in {stage_name}."
|
||||
else:
|
||||
msg = "output '{}' is already specified in stages:\n{}".format(
|
||||
output, "\n".join(f"\t- {s.addressing}" for s in stages)
|
||||
output, "\n".join(f"\t- {s}" for s in stage_names)
|
||||
)
|
||||
msg += (
|
||||
f"\nUse `dvc remove {stages_str}` to stop tracking the overlapping output."
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.stages = stages
|
||||
self.output = output
|
||||
@@ -57,12 +67,14 @@ class OutputNotFoundError(DvcException):
|
||||
self.output = output
|
||||
self.repo = repo
|
||||
super().__init__(
|
||||
"Unable to find DVC file with output '{path}'".format(
|
||||
path=relpath(self.output)
|
||||
)
|
||||
f"Unable to find DVC file with output {relpath(self.output)!r}"
|
||||
)
|
||||
|
||||
|
||||
class StageNotFoundError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class StagePathAsOutputError(DvcException):
|
||||
"""Thrown if directory that stage is going to be saved in is specified as
|
||||
an output of another stage.
|
||||
@@ -74,11 +86,7 @@ class StagePathAsOutputError(DvcException):
|
||||
|
||||
def __init__(self, stage, output):
|
||||
assert isinstance(output, str)
|
||||
super().__init__(
|
||||
"{stage} is within an output '{output}' of another stage".format(
|
||||
stage=stage, output=output
|
||||
)
|
||||
)
|
||||
super().__init__(f"{stage} is within an output {output!r} of another stage")
|
||||
|
||||
|
||||
class CircularDependencyError(DvcException):
|
||||
@@ -150,9 +158,7 @@ class CyclicGraphError(DvcException):
|
||||
class ConfirmRemoveError(DvcException):
|
||||
def __init__(self, path):
|
||||
super().__init__(
|
||||
"unable to remove '{}' without a confirmation. Use `-f` to force.".format(
|
||||
path
|
||||
)
|
||||
f"unable to remove {path!r} without a confirmation. Use `-f` to force."
|
||||
)
|
||||
|
||||
|
||||
@@ -161,9 +167,7 @@ class InitError(DvcException):
|
||||
|
||||
|
||||
class ReproductionError(DvcException):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
super().__init__(f"failed to reproduce '{name}'")
|
||||
pass
|
||||
|
||||
|
||||
class BadMetricError(DvcException):
|
||||
@@ -176,11 +180,6 @@ class BadMetricError(DvcException):
|
||||
)
|
||||
|
||||
|
||||
class RecursiveAddingWhileUsingFilename(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__("cannot use `fname` with multiple targets or `-R|--recursive`")
|
||||
|
||||
|
||||
class OverlappingOutputPathsError(DvcException):
|
||||
def __init__(self, parent, overlapping_out, message):
|
||||
self.parent = parent
|
||||
@@ -201,6 +200,18 @@ class ETagMismatchError(DvcException):
|
||||
)
|
||||
|
||||
|
||||
class FileExistsLocallyError(FileExistsError, DvcException):
|
||||
def __init__(self, path, hint=None):
|
||||
import os.path
|
||||
|
||||
self.path = path
|
||||
hint = "" if hint is None else f". {hint}"
|
||||
path_typ = "directory" if os.path.isdir(path) else "file"
|
||||
msg = f"The {path_typ} '{path}' already exists locally{hint}"
|
||||
super().__init__(msg)
|
||||
self.errno = errno.EEXIST
|
||||
|
||||
|
||||
class FileMissingError(DvcException):
|
||||
def __init__(self, path, hint=None):
|
||||
self.path = path
|
||||
@@ -286,22 +297,7 @@ class URLMissingError(DvcException):
|
||||
super().__init__(f"The path '{url}' does not exist")
|
||||
|
||||
|
||||
class RemoteCacheRequiredError(DvcException):
|
||||
def __init__(self, scheme, fs_path):
|
||||
super().__init__(
|
||||
(
|
||||
"Current operation was unsuccessful because '{}' requires "
|
||||
"existing cache on '{}' remote. See {} for information on how "
|
||||
"to set up remote cache."
|
||||
).format(
|
||||
fs_path,
|
||||
scheme,
|
||||
format_link("https://man.dvc.org/config#cache"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class IsADirectoryError(DvcException): # noqa,pylint:disable=redefined-builtin
|
||||
class IsADirectoryError(DvcException): # noqa: A001,pylint:disable=redefined-builtin
|
||||
"""Raised when a file operation is requested on a directory."""
|
||||
|
||||
|
||||
@@ -333,14 +329,6 @@ class CacheLinkError(DvcException):
|
||||
self.fs_paths = fs_paths
|
||||
|
||||
|
||||
class CircularImportError(DvcException):
|
||||
def __init__(self, dep, a, b):
|
||||
super().__init__(
|
||||
f"'{dep}' contains invalid circular import. "
|
||||
f"DVC repo '{a}' already imports from '{b}'."
|
||||
)
|
||||
|
||||
|
||||
class PrettyDvcException(DvcException):
|
||||
def __pretty_exc__(self, **kwargs):
|
||||
"""Print prettier exception message."""
|
||||
|
||||
+37
-13
@@ -1,3 +1,4 @@
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
||||
@@ -6,7 +7,6 @@ from dvc.config import ConfigError as RepoConfigError
|
||||
from dvc.config_schema import SCHEMA, Invalid
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from dvc_objects.fs import utils # noqa: F401
|
||||
from dvc_objects.fs import ( # noqa: F401
|
||||
LocalFileSystem,
|
||||
MemoryFileSystem,
|
||||
@@ -17,8 +17,9 @@ from dvc_objects.fs import ( # noqa: F401
|
||||
localfs,
|
||||
registry,
|
||||
system,
|
||||
utils,
|
||||
)
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401, TCH001
|
||||
from dvc_objects.fs.errors import ( # noqa: F401
|
||||
AuthError,
|
||||
ConfigError,
|
||||
@@ -48,18 +49,45 @@ known_implementations.update(
|
||||
# pylint: enable=unused-import
|
||||
|
||||
|
||||
def download(fs, fs_path, to, jobs=None):
|
||||
def download(fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None):
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Downloading {fs.path.name(fs_path)}",
|
||||
unit="files",
|
||||
) as cb:
|
||||
fs.get(fs_path, to.fs_path, batch_size=jobs, callback=cb)
|
||||
# NOTE: We use dvc-objects generic.copy over fs.get since it makes file
|
||||
# download atomic and avoids fsspec glob/regex path expansion.
|
||||
if fs.isdir(fs_path):
|
||||
from_infos = [
|
||||
path
|
||||
for path in fs.find(fs_path)
|
||||
if not path.endswith(fs.path.flavour.sep)
|
||||
]
|
||||
if not from_infos:
|
||||
return localfs.makedirs(to, exist_ok=True)
|
||||
to_infos = [
|
||||
localfs.path.join(to, *fs.path.relparts(info, fs_path))
|
||||
for info in from_infos
|
||||
]
|
||||
else:
|
||||
from_infos = [fs_path]
|
||||
to_infos = [to]
|
||||
|
||||
cb.set_size(len(from_infos))
|
||||
jobs = jobs or fs.jobs
|
||||
generic.copy(
|
||||
fs,
|
||||
from_infos,
|
||||
localfs,
|
||||
to_infos,
|
||||
callback=cb,
|
||||
batch_size=jobs,
|
||||
)
|
||||
|
||||
|
||||
def parse_external_url(url, config=None):
|
||||
remote_config = dict(config) if config else {}
|
||||
remote_config["url"] = url
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(None, **remote_config)
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs({}, **remote_config)
|
||||
fs = fs_cls(**fs_config)
|
||||
return fs, fs_path
|
||||
|
||||
@@ -105,23 +133,19 @@ def _resolve_remote_refs(config, remote_conf):
|
||||
return remote_conf
|
||||
|
||||
base = get_fs_config(config, name=parsed.netloc)
|
||||
cls, _, _ = _get_cloud_fs(config, **base)
|
||||
cls, _, _ = get_cloud_fs(config, **base)
|
||||
relpath = parsed.path.lstrip("/").replace("/", cls.sep)
|
||||
url = cls.sep.join((base["url"], relpath))
|
||||
return {**base, **remote_conf, "url": url}
|
||||
|
||||
|
||||
def get_cloud_fs(repo, **kwargs):
|
||||
repo_config = repo.config if repo else {}
|
||||
return _get_cloud_fs(repo_config, **kwargs)
|
||||
|
||||
|
||||
def _get_cloud_fs(repo_config, **kwargs):
|
||||
def get_cloud_fs(repo_config, **kwargs):
|
||||
repo_config = repo_config or {}
|
||||
core_config = repo_config.get("core", {})
|
||||
|
||||
remote_conf = get_fs_config(repo_config, **kwargs)
|
||||
try:
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf)
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf) # type: ignore[index]
|
||||
except Invalid as exc:
|
||||
raise RepoConfigError(str(exc)) from None
|
||||
|
||||
|
||||
+58
-7
@@ -1,13 +1,10 @@
|
||||
# pylint: disable=unused-import
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_objects.fs.callbacks import ( # noqa: F401
|
||||
DEFAULT_CALLBACK,
|
||||
Callback,
|
||||
TqdmCallback,
|
||||
)
|
||||
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.progress import TaskID
|
||||
@@ -15,6 +12,59 @@ if TYPE_CHECKING:
|
||||
from dvc.ui._rich_progress import RichTransferProgress
|
||||
|
||||
|
||||
# NOTE: this is very similar to dvc-objects.fs.callbacks.TqdmCallback,
|
||||
# but it works with our own Tqdm instance.
|
||||
class TqdmCallback(Callback):
|
||||
def __init__(
|
||||
self,
|
||||
size: Optional[int] = None,
|
||||
value: int = 0,
|
||||
progress_bar: Optional["Tqdm"] = None,
|
||||
**tqdm_kwargs,
|
||||
):
|
||||
tqdm_kwargs["total"] = size or -1
|
||||
self._tqdm_kwargs = tqdm_kwargs
|
||||
self._progress_bar = progress_bar
|
||||
self._stack = ExitStack()
|
||||
super().__init__(size=size, value=value)
|
||||
|
||||
@cached_property
|
||||
def progress_bar(self):
|
||||
progress_bar = (
|
||||
self._progress_bar
|
||||
if self._progress_bar is not None
|
||||
else Tqdm(**self._tqdm_kwargs)
|
||||
)
|
||||
return self._stack.enter_context(progress_bar)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
self._stack.close()
|
||||
|
||||
def set_size(self, size):
|
||||
# Tqdm tries to be smart when to refresh,
|
||||
# so we try to force it to re-render.
|
||||
super().set_size(size)
|
||||
self.progress_bar.refresh()
|
||||
|
||||
def call(self, hook_name=None, **kwargs): # noqa: ARG002
|
||||
self.progress_bar.update_to(self.value, total=self.size)
|
||||
|
||||
def branch(
|
||||
self,
|
||||
path_1: "Union[str, BinaryIO]",
|
||||
path_2: str,
|
||||
kwargs: Dict[str, Any],
|
||||
child: Optional[Callback] = None,
|
||||
):
|
||||
child = child or TqdmCallback(
|
||||
bytes=True, desc=path_1 if isinstance(path_1, str) else path_2
|
||||
)
|
||||
return super().branch(path_1, path_2, kwargs, child=child)
|
||||
|
||||
|
||||
class RichCallback(Callback):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -54,7 +104,8 @@ class RichCallback(Callback):
|
||||
disable=self.disable,
|
||||
console=ui.error_console,
|
||||
)
|
||||
return cast(RichTransferProgress, self._stack.enter_context(progress))
|
||||
self._stack.enter_context(progress)
|
||||
return progress
|
||||
|
||||
@cached_property
|
||||
def task(self) -> "TaskID":
|
||||
|
||||
+64
-17
@@ -5,7 +5,7 @@ import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import threading
|
||||
from contextlib import suppress
|
||||
from contextlib import ExitStack, suppress
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Union
|
||||
|
||||
from fsspec.spec import AbstractFileSystem
|
||||
@@ -38,17 +38,24 @@ def _is_dvc_file(fname):
|
||||
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
|
||||
|
||||
def _merge_info(repo, fs_info, dvc_info):
|
||||
def _merge_info(repo, key, fs_info, dvc_info):
|
||||
from . import utils
|
||||
|
||||
ret = {"repo": repo}
|
||||
|
||||
if dvc_info:
|
||||
dvc_info["isout"] = any(
|
||||
(len(out_key) <= len(key) and key[: len(out_key)] == out_key)
|
||||
for out_key in repo.index.data_keys["repo"]
|
||||
)
|
||||
dvc_info["isdvc"] = dvc_info["isout"]
|
||||
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 not fs_info and "md5-dos2unix" in dvc_info:
|
||||
ret["md5-dos2unix"] = dvc_info["md5-dos2unix"]
|
||||
|
||||
if fs_info:
|
||||
ret["type"] = fs_info["type"]
|
||||
@@ -116,11 +123,13 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
from pygtrie import Trie
|
||||
|
||||
super().__init__()
|
||||
self._repo_stack = ExitStack()
|
||||
if repo is None:
|
||||
repo = self._make_repo(url=url, rev=rev, subrepos=subrepos, **repo_kwargs)
|
||||
assert repo is not None
|
||||
# pylint: disable=protected-access
|
||||
repo_factory = repo._fs_conf["repo_factory"]
|
||||
self._repo_stack.enter_context(repo)
|
||||
|
||||
if not repo_factory:
|
||||
from dvc.repo import Repo
|
||||
@@ -209,6 +218,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
scm=self.repo.scm,
|
||||
repo_factory=self.repo_factory,
|
||||
)
|
||||
self._repo_stack.enter_context(repo)
|
||||
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
|
||||
self._subrepos_trie[key] = repo
|
||||
|
||||
@@ -251,33 +261,39 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
try:
|
||||
return self.repo.fs.open(fs_path, mode=mode)
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.open(dvc_path, mode=mode)
|
||||
kw = {}
|
||||
if kwargs.get("cache_remote_stream", False):
|
||||
kw["cache_odb"] = repo.cache.local
|
||||
return dvc_fs.open(dvc_path, mode=mode, **kw)
|
||||
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
"""Is this entry dvc-tracked?"""
|
||||
key = self._get_key_from_relative(path)
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
|
||||
try:
|
||||
return self.info(path).get("dvc_info", {}).get("isout", False)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def ls( # pylint: disable=arguments-differ
|
||||
def ls( # pylint: disable=arguments-differ # noqa: C901
|
||||
self, path, detail=True, dvc_only=False, **kwargs
|
||||
):
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_exists = False
|
||||
dvc_infos = {}
|
||||
if dvc_fs:
|
||||
with suppress(FileNotFoundError):
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
with suppress(FileNotFoundError, NotADirectoryError):
|
||||
for info in dvc_fs.ls(dvc_path, detail=True):
|
||||
dvc_infos[dvc_fs.path.name(info["name"])] = info
|
||||
dvc_exists = bool(dvc_infos) or dvc_fs.exists(dvc_path)
|
||||
|
||||
fs_exists = False
|
||||
fs_infos = {}
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
if not dvc_only:
|
||||
@@ -289,19 +305,28 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
):
|
||||
fs_infos[fs.path.name(info["name"])] = info
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
pass # noqa: S110
|
||||
pass
|
||||
|
||||
fs_exists = bool(fs_infos) or fs.exists(fs_path)
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
|
||||
infos = []
|
||||
paths = []
|
||||
names = set(dvc_infos.keys()) | set(fs_infos.keys())
|
||||
|
||||
if not names and (dvc_exists or fs_exists):
|
||||
# broken symlink or TreeError
|
||||
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
||||
|
||||
for name in names:
|
||||
if not dvcfiles and _is_dvc_file(name):
|
||||
continue
|
||||
|
||||
entry_path = self.path.join(path, name)
|
||||
info = _merge_info(repo, fs_infos.get(name), dvc_infos.get(name))
|
||||
info = _merge_info(
|
||||
repo, (*subkey, name), fs_infos.get(name), dvc_infos.get(name)
|
||||
)
|
||||
info["name"] = entry_path
|
||||
infos.append(info)
|
||||
paths.append(entry_path)
|
||||
@@ -316,7 +341,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info(self, key, path, ignore_subrepos=True, check_ignored=True): # noqa: C901
|
||||
def _info( # noqa: C901, PLR0912
|
||||
self, key, path, ignore_subrepos=True, check_ignored=True
|
||||
):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_info = None
|
||||
@@ -325,7 +352,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
dvc_info = dvc_fs.fs.index.info(subkey)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_info["name"] = dvc_path
|
||||
except FileNotFoundError:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
fs_info = None
|
||||
@@ -355,10 +382,26 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
if not dvc_info and not fs_info:
|
||||
raise FileNotFoundError
|
||||
|
||||
info = _merge_info(repo, fs_info, dvc_info)
|
||||
info = _merge_info(repo, subkey, fs_info, dvc_info)
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
def get_file(self, rpath, lpath, **kwargs): # pylint: disable=arguments-differ
|
||||
key = self._get_key_from_relative(rpath)
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
return self.repo.fs.get_file(fs_path, lpath, **kwargs)
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
|
||||
|
||||
def close(self):
|
||||
self._repo_stack.close()
|
||||
|
||||
|
||||
class DVCFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
@@ -369,7 +412,7 @@ class DVCFileSystem(FileSystem):
|
||||
|
||||
@functools.cached_property
|
||||
# pylint: disable-next=invalid-overridden-method
|
||||
def fs(self) -> "DVCFileSystem":
|
||||
def fs(self) -> "_DVCFileSystem":
|
||||
return _DVCFileSystem(**self.fs_args)
|
||||
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
@@ -392,3 +435,7 @@ class DVCFileSystem(FileSystem):
|
||||
path = os.path.relpath(path, self.repo.root_dir)
|
||||
|
||||
return as_posix(path)
|
||||
|
||||
def close(self):
|
||||
if "fs" in self.__dict__:
|
||||
self.fs.close()
|
||||
|
||||
+25
-14
@@ -6,20 +6,18 @@ import platform
|
||||
|
||||
import psutil
|
||||
|
||||
from dvc import __version__
|
||||
from dvc import PKG, __version__
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.fs import Schemes, generic, get_fs_cls, get_fs_config, registry
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import SCMError
|
||||
from dvc.utils import error_link
|
||||
from dvc.utils.pkg import PKG
|
||||
|
||||
SUBPROJECTS = (
|
||||
"dvc_data",
|
||||
"dvc_objects",
|
||||
"dvc_render",
|
||||
"dvc_task",
|
||||
"dvclive",
|
||||
"scmrepo",
|
||||
)
|
||||
package = "" if PKG is None else f" ({PKG})"
|
||||
@@ -33,6 +31,7 @@ def get_dvc_info():
|
||||
f"Platform: Python {platform.python_version()} on {platform.platform()}",
|
||||
f"Subprojects:{_get_subprojects()}",
|
||||
f"Supports:{_get_supported_remotes()}",
|
||||
f"Config:{_get_config_dirs()}",
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -41,20 +40,21 @@ def get_dvc_info():
|
||||
# can't auto-create it, as it might cause issues if the user
|
||||
# later decides to enable shared cache mode with
|
||||
# `dvc config cache.shared group`.
|
||||
if os.path.exists(repo.odb.local.path):
|
||||
if os.path.exists(repo.cache.local.path):
|
||||
info.append(f"Cache types: {_get_linktype_support_info(repo)}")
|
||||
fs_type = get_fs_type(repo.odb.local.path)
|
||||
fs_type = _get_fs_type(repo.cache.local.path)
|
||||
info.append(f"Cache directory: {fs_type}")
|
||||
else:
|
||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||
|
||||
info.append(f"Caches: {_get_caches(repo.odb)}")
|
||||
info.append(f"Caches: {_get_caches(repo.cache)}")
|
||||
info.append(f"Remotes: {_get_remotes(repo.config)}")
|
||||
|
||||
root_directory = repo.root_dir
|
||||
fs_root = get_fs_type(os.path.abspath(root_directory))
|
||||
fs_root = _get_fs_type(os.path.abspath(root_directory))
|
||||
info.append(f"Workspace directory: {fs_root}")
|
||||
info.append(f"Repo: {_get_dvc_repo_info(repo)}")
|
||||
info.append(f"Repo.site_cache_dir: {repo.site_cache_dir}")
|
||||
except NotDvcRepoError:
|
||||
pass
|
||||
except SCMError:
|
||||
@@ -67,7 +67,7 @@ def _get_caches(cache):
|
||||
caches = (
|
||||
cache_type
|
||||
for cache_type, cache_instance in cache.by_scheme()
|
||||
if cache_instance and cache_type != "repo"
|
||||
if cache_instance and cache_type not in ("repo", "legacy")
|
||||
)
|
||||
|
||||
# Caches will be always non-empty including the local cache
|
||||
@@ -84,7 +84,7 @@ def _get_remotes(config):
|
||||
|
||||
|
||||
def _get_linktype_support_info(repo):
|
||||
odb = repo.odb.local
|
||||
odb = repo.cache.local
|
||||
|
||||
links = generic.test_links(
|
||||
["reflink", "hardlink", "symlink"],
|
||||
@@ -136,10 +136,21 @@ def _get_supported_remotes():
|
||||
return "\n\t" + ",\n\t".join(supported_remotes)
|
||||
|
||||
|
||||
def get_fs_type(path):
|
||||
def _get_config_dirs():
|
||||
from dvc.config import Config
|
||||
|
||||
dirs = [
|
||||
f"Global: {Config.get_dir('global')}",
|
||||
f"System: {Config.get_dir('system')}",
|
||||
]
|
||||
|
||||
return "\n\t" + "\n\t".join(dirs)
|
||||
|
||||
|
||||
def _get_fs_type(path):
|
||||
partition = {}
|
||||
for part in psutil.disk_partitions(all=True):
|
||||
if part.fstype != "":
|
||||
if part.fstype:
|
||||
try:
|
||||
mountpoint = pathlib.Path(part.mountpoint).resolve()
|
||||
partition[mountpoint] = part.fstype + " on " + part.device
|
||||
@@ -155,11 +166,11 @@ def get_fs_type(path):
|
||||
return ("unknown", "none")
|
||||
|
||||
|
||||
def _get_dvc_repo_info(self):
|
||||
if self.config.get("core", {}).get("no_scm", False):
|
||||
def _get_dvc_repo_info(repo):
|
||||
if repo.config.get("core", {}).get("no_scm", False):
|
||||
return "dvc (no_scm)"
|
||||
|
||||
if self.root_dir != self.scm.root_dir:
|
||||
if repo.root_dir != repo.scm.root_dir:
|
||||
return "dvc (subdir), git"
|
||||
|
||||
return "dvc, git"
|
||||
|
||||
+3
-1
@@ -187,7 +187,9 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
||||
|
||||
if self._tmp_dir is not None:
|
||||
# Under Windows file path length is limited so we hash it
|
||||
hasher = hashlib.md5(self._claimfile.encode()) # noqa: S324
|
||||
hasher = hashlib.md5( # nosec B324, B303 # noqa: S324
|
||||
self._claimfile.encode()
|
||||
)
|
||||
filename = hasher.hexdigest()
|
||||
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
|
||||
|
||||
|
||||
+109
-173
@@ -1,77 +1,70 @@
|
||||
"""Manages logging configuration for DVC repo."""
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
|
||||
import colorama
|
||||
|
||||
from dvc.env import DVC_SHOW_TRACEBACK
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
FOOTER = (
|
||||
"\n{yellow}Having any troubles?{nc}"
|
||||
" Hit us up at {blue}https://dvc.org/support{nc},"
|
||||
" we are always happy to help!"
|
||||
).format(
|
||||
blue=colorama.Fore.BLUE,
|
||||
nc=colorama.Fore.RESET,
|
||||
yellow=colorama.Fore.YELLOW,
|
||||
)
|
||||
|
||||
|
||||
def addLoggingLevel(levelName, levelNum, methodName=None):
|
||||
def add_logging_level(level_name, level_num, method_name=None):
|
||||
"""
|
||||
Adds a new logging level to the `logging` module and the
|
||||
currently configured logging class.
|
||||
|
||||
Uses the existing numeric levelNum if already defined.
|
||||
Uses the existing numeric level_num if already defined.
|
||||
|
||||
Based on https://stackoverflow.com/questions/2183233
|
||||
"""
|
||||
if methodName is None:
|
||||
methodName = levelName.lower()
|
||||
if method_name is None:
|
||||
method_name = level_name.lower()
|
||||
|
||||
# If the level name is already defined as a top-level `logging`
|
||||
# constant, then adopt the existing numeric level.
|
||||
if hasattr(logging, levelName):
|
||||
existingLevelNum = getattr(logging, levelName)
|
||||
assert isinstance(existingLevelNum, int)
|
||||
levelNum = existingLevelNum
|
||||
if hasattr(logging, level_name):
|
||||
existing_level_num = getattr(logging, level_name)
|
||||
assert isinstance(existing_level_num, int)
|
||||
level_num = existing_level_num
|
||||
|
||||
def logForLevel(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(levelNum):
|
||||
def log_for_level(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(level_num):
|
||||
# pylint: disable=protected-access
|
||||
self._log(levelNum, message, args, **kwargs)
|
||||
self._log(level_num, message, args, **kwargs)
|
||||
|
||||
def logToRoot(message, *args, **kwargs):
|
||||
logging.log(levelNum, message, *args, **kwargs)
|
||||
def log_to_root(message, *args, **kwargs):
|
||||
logging.log(level_num, message, *args, **kwargs)
|
||||
|
||||
# getLevelName resolves the numeric log level if already defined,
|
||||
# otherwise returns a string
|
||||
if not isinstance(logging.getLevelName(levelName), int):
|
||||
logging.addLevelName(levelNum, levelName)
|
||||
if not isinstance(logging.getLevelName(level_name), int):
|
||||
logging.addLevelName(level_num, level_name)
|
||||
|
||||
if not hasattr(logging, levelName):
|
||||
setattr(logging, levelName, levelNum)
|
||||
if not hasattr(logging, level_name):
|
||||
setattr(logging, level_name, level_num)
|
||||
|
||||
if not hasattr(logging.getLoggerClass(), methodName):
|
||||
setattr(logging.getLoggerClass(), methodName, logForLevel)
|
||||
if not hasattr(logging.getLoggerClass(), method_name):
|
||||
setattr(logging.getLoggerClass(), method_name, log_for_level)
|
||||
|
||||
if not hasattr(logging, methodName):
|
||||
setattr(logging, methodName, logToRoot)
|
||||
if not hasattr(logging, method_name):
|
||||
setattr(logging, method_name, log_to_root)
|
||||
|
||||
|
||||
class LoggingException(Exception):
|
||||
def __init__(self, record):
|
||||
msg = f"failed to log {str(record)}"
|
||||
msg = f"failed to log {record!s}"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
def excludeFilter(level):
|
||||
class ExcludeLevelFilter(logging.Filter):
|
||||
def filter(self, record): # noqa: A003
|
||||
return record.levelno < level
|
||||
def exclude_filter(level: int):
|
||||
def filter_fn(record: "logging.LogRecord") -> bool:
|
||||
return record.levelno < level
|
||||
|
||||
return ExcludeLevelFilter
|
||||
return filter_fn
|
||||
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
@@ -86,7 +79,8 @@ class ColorFormatter(logging.Formatter):
|
||||
exception cause, just the message and the traceback.
|
||||
"""
|
||||
|
||||
color_code = {
|
||||
reset = colorama.Fore.RESET
|
||||
color_codes = {
|
||||
"TRACE": colorama.Fore.GREEN,
|
||||
"DEBUG": colorama.Fore.BLUE,
|
||||
"WARNING": colorama.Fore.YELLOW,
|
||||
@@ -94,51 +88,53 @@ class ColorFormatter(logging.Formatter):
|
||||
"CRITICAL": colorama.Fore.RED,
|
||||
}
|
||||
|
||||
def format(self, record): # noqa: A003
|
||||
def __init__(self, log_colors: bool = True, show_traceback: bool = False) -> None:
|
||||
super().__init__()
|
||||
self.log_colors = log_colors
|
||||
self.show_traceback = show_traceback
|
||||
|
||||
def format(self, record) -> str: # noqa: A003, C901
|
||||
record.message = record.getMessage()
|
||||
msg = self.formatMessage(record)
|
||||
|
||||
if record.levelname == "INFO":
|
||||
if record.levelno == logging.INFO:
|
||||
return msg
|
||||
|
||||
if record.exc_info:
|
||||
if getattr(record, "tb_only", False):
|
||||
cause = ""
|
||||
else:
|
||||
cause = ": ".join(_iter_causes(record.exc_info[1]))
|
||||
ei = record.exc_info
|
||||
if ei:
|
||||
cause = ""
|
||||
if not getattr(record, "tb_only", False):
|
||||
cause = ": ".join(_iter_causes(ei[1]))
|
||||
sep = " - " if msg and cause else ""
|
||||
msg = msg + sep + cause
|
||||
|
||||
msg = "{message}{separator}{cause}".format(
|
||||
message=msg or "",
|
||||
separator=" - " if msg and cause else "",
|
||||
cause=cause,
|
||||
)
|
||||
asctime = ""
|
||||
verbose = _is_verbose()
|
||||
if verbose:
|
||||
asctime = self.formatTime(record, self.datefmt)
|
||||
if verbose or self.show_traceback:
|
||||
if ei and not record.exc_text:
|
||||
record.exc_text = self.formatException(ei)
|
||||
if record.exc_text:
|
||||
if msg[-1:] != "\n":
|
||||
msg = msg + "\n"
|
||||
msg = msg + record.exc_text + "\n"
|
||||
if record.stack_info:
|
||||
if msg[-1:] != "\n":
|
||||
msg = msg + "\n"
|
||||
msg = msg + self.formatStack(record.stack_info) + "\n"
|
||||
|
||||
if _is_verbose():
|
||||
msg += _stack_trace(record.exc_info)
|
||||
|
||||
return "{asctime}{color}{levelname}{nc}: {msg}".format(
|
||||
asctime=self.formatTime(record, self.datefmt),
|
||||
color=self.color_code[record.levelname],
|
||||
nc=colorama.Fore.RESET,
|
||||
levelname=record.levelname,
|
||||
msg=msg,
|
||||
)
|
||||
|
||||
def formatTime(self, record, datefmt=None):
|
||||
# only show if current level is set to DEBUG
|
||||
# also, skip INFO as it is used for UI
|
||||
if not _is_verbose() or record.levelno == logging.INFO:
|
||||
return ""
|
||||
|
||||
return "{green}{date}{nc} ".format(
|
||||
green=colorama.Fore.GREEN,
|
||||
date=super().formatTime(record, datefmt),
|
||||
nc=colorama.Fore.RESET,
|
||||
)
|
||||
level = record.levelname
|
||||
if self.log_colors:
|
||||
color = self.color_codes[level]
|
||||
if asctime:
|
||||
asctime = color + asctime + self.reset
|
||||
level = color + level + self.reset
|
||||
return asctime + (" " if asctime else "") + level + ": " + msg
|
||||
|
||||
|
||||
class LoggerHandler(logging.StreamHandler):
|
||||
def handleError(self, record):
|
||||
def handleError(self, record): # noqa: N802
|
||||
super().handleError(record)
|
||||
raise LoggingException(record)
|
||||
|
||||
@@ -155,15 +151,16 @@ class LoggerHandler(logging.StreamHandler):
|
||||
self.emit_pretty_exception(exc, verbose=_is_verbose())
|
||||
if not _is_verbose():
|
||||
return
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
pass # noqa: S110
|
||||
# pylint: disable-next=broad-except
|
||||
except Exception: # noqa: BLE001, S110 # nosec B110
|
||||
pass
|
||||
|
||||
msg = self.format(record)
|
||||
Tqdm.write(msg, file=self.stream, end=getattr(self, "terminator", "\n"))
|
||||
self.flush()
|
||||
except (BrokenPipeError, RecursionError):
|
||||
raise
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
except Exception: # noqa: BLE001, pylint: disable=broad-except
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
@@ -179,31 +176,47 @@ def _iter_causes(exc):
|
||||
exc = exc.__cause__
|
||||
|
||||
|
||||
def _stack_trace(exc_info):
|
||||
import traceback
|
||||
|
||||
return "\n{red}{line}{nc}\n{trace}{red}{line}{nc}".format(
|
||||
red=colorama.Fore.RED,
|
||||
line="-" * 60,
|
||||
trace="".join(traceback.format_exception(*exc_info)),
|
||||
nc=colorama.Fore.RESET,
|
||||
)
|
||||
|
||||
|
||||
def disable_other_loggers():
|
||||
logging.captureWarnings(True)
|
||||
loggerDict = logging.root.manager.loggerDict # pylint: disable=no-member
|
||||
for logger_name, logger in loggerDict.items():
|
||||
if logger_name != "dvc" and not logger_name.startswith("dvc."):
|
||||
logger.disabled = True # type: ignore[union-attr]
|
||||
|
||||
|
||||
def set_loggers_level(level: int = logging.INFO) -> None:
|
||||
for name in ["dvc", "dvc_objects", "dvc_data"]:
|
||||
logging.getLogger(name).setLevel(level)
|
||||
|
||||
|
||||
def setup(level: int = logging.INFO) -> None:
|
||||
def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
|
||||
colorama.init()
|
||||
|
||||
formatter = ColorFormatter(log_colors=log_colors and sys.stdout.isatty())
|
||||
|
||||
console_info = LoggerHandler(sys.stdout)
|
||||
console_info.setLevel(logging.INFO)
|
||||
console_info.setFormatter(formatter)
|
||||
console_info.addFilter(exclude_filter(logging.WARNING))
|
||||
|
||||
console_debug = LoggerHandler(sys.stdout)
|
||||
console_debug.setLevel(logging.DEBUG)
|
||||
console_debug.setFormatter(formatter)
|
||||
console_debug.addFilter(exclude_filter(logging.INFO))
|
||||
|
||||
add_logging_level("TRACE", logging.DEBUG - 5)
|
||||
|
||||
console_trace = LoggerHandler(sys.stdout)
|
||||
console_trace.setLevel(logging.TRACE) # type: ignore[attr-defined]
|
||||
console_trace.setFormatter(formatter)
|
||||
console_trace.addFilter(exclude_filter(logging.DEBUG))
|
||||
|
||||
show_traceback = bool(os.environ.get(DVC_SHOW_TRACEBACK))
|
||||
err_formatter = ColorFormatter(
|
||||
log_colors=log_colors and sys.stderr.isatty(), show_traceback=show_traceback
|
||||
)
|
||||
console_errors = LoggerHandler(sys.stderr)
|
||||
console_errors.setLevel(logging.WARNING)
|
||||
console_errors.setFormatter(err_formatter)
|
||||
|
||||
for name in ["dvc", "dvc_objects", "dvc_data"]:
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
for handler in [console_info, console_debug, console_trace, console_errors]:
|
||||
logger.addHandler(handler)
|
||||
|
||||
if level >= logging.DEBUG:
|
||||
# Unclosed session errors for asyncio/aiohttp are only available
|
||||
# on the tracing mode for extensive debug purposes. They are really
|
||||
@@ -214,80 +227,3 @@ def setup(level: int = logging.INFO) -> None:
|
||||
# action.
|
||||
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("aiohttp").setLevel(logging.CRITICAL)
|
||||
|
||||
addLoggingLevel("TRACE", logging.DEBUG - 5)
|
||||
logging.config.dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"filters": {
|
||||
"exclude_errors": {"()": excludeFilter(logging.WARNING)},
|
||||
"exclude_info": {"()": excludeFilter(logging.INFO)},
|
||||
"exclude_debug": {"()": excludeFilter(logging.DEBUG)},
|
||||
},
|
||||
"formatters": {"color": {"()": ColorFormatter}},
|
||||
"handlers": {
|
||||
"console_info": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "INFO",
|
||||
"formatter": "color",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["exclude_errors"],
|
||||
},
|
||||
"console_debug": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "color",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["exclude_info"],
|
||||
},
|
||||
"console_trace": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "TRACE",
|
||||
"formatter": "color",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["exclude_debug"],
|
||||
},
|
||||
"console_errors": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "WARNING",
|
||||
"formatter": "color",
|
||||
"stream": "ext://sys.stderr",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"dvc": {
|
||||
"level": level,
|
||||
"handlers": [
|
||||
"console_info",
|
||||
"console_debug",
|
||||
"console_trace",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
"dvc_objects": {
|
||||
"level": level,
|
||||
"handlers": [
|
||||
"console_info",
|
||||
"console_debug",
|
||||
"console_trace",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
"dvc_data": {
|
||||
"level": level,
|
||||
"handlers": [
|
||||
"console_info",
|
||||
"console_debug",
|
||||
"console_trace",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
},
|
||||
"disable_existing_loggers": False,
|
||||
}
|
||||
)
|
||||
|
||||
# NOTE: colorama init needs to go after logging config to avoid potential
|
||||
# conflicts with libs that modify logging (like tensorflow/abseil)
|
||||
# https://github.com/iterative/dvc/issues/8387
|
||||
colorama.init()
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# TODO: remove in 3.0, kept for backward compatibility
|
||||
from .cli import main # noqa: F401, pylint: disable=unused-import
|
||||
@@ -1,71 +0,0 @@
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
|
||||
def _get_odb(repo, settings, fs=None):
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
if not settings:
|
||||
return None
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(repo, **settings)
|
||||
config["tmp_dir"] = repo.tmp_dir
|
||||
fs = fs or cls(**config)
|
||||
return get_odb(fs, fs_path, state=repo.state, **config)
|
||||
|
||||
|
||||
class ODBManager:
|
||||
CACHE_DIR = "cache"
|
||||
CLOUD_SCHEMES = [
|
||||
Schemes.S3,
|
||||
Schemes.GS,
|
||||
Schemes.SSH,
|
||||
Schemes.HDFS,
|
||||
Schemes.WEBHDFS,
|
||||
]
|
||||
|
||||
def __init__(self, repo):
|
||||
self._repo = repo
|
||||
self.config = config = repo.config["cache"]
|
||||
self._odb = {}
|
||||
|
||||
local = config.get("local")
|
||||
|
||||
if local:
|
||||
settings = {"name": local}
|
||||
elif "dir" not in config:
|
||||
settings = None
|
||||
else:
|
||||
from dvc.config_schema import LOCAL_COMMON
|
||||
|
||||
settings = {"url": config["dir"]}
|
||||
for opt in LOCAL_COMMON:
|
||||
if opt in config:
|
||||
settings[str(opt)] = config.get(opt)
|
||||
|
||||
kwargs = {}
|
||||
if not isinstance(repo.fs, GitFileSystem):
|
||||
kwargs["fs"] = repo.fs
|
||||
|
||||
odb = _get_odb(repo, settings, **kwargs)
|
||||
self._odb["repo"] = odb
|
||||
self._odb[Schemes.LOCAL] = odb
|
||||
|
||||
def _init_odb(self, schemes):
|
||||
for scheme in schemes:
|
||||
remote = self.config.get(scheme)
|
||||
settings = {"name": remote} if remote else None
|
||||
self._odb[scheme] = _get_odb(self._repo, settings)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self._odb and name in self.CLOUD_SCHEMES:
|
||||
self._init_odb([name])
|
||||
|
||||
try:
|
||||
return self._odb[name]
|
||||
except KeyError as exc:
|
||||
raise AttributeError from exc
|
||||
|
||||
def by_scheme(self):
|
||||
self._init_odb(self.CLOUD_SCHEMES)
|
||||
yield from self._odb.items()
|
||||
+441
-180
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user