mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b532b7d59 | ||
|
|
e78c4455de | ||
|
|
8e5f4a8f35 | ||
|
|
cb3159e586 | ||
|
|
7b301b09a1 | ||
|
|
55b16166c8 | ||
|
|
c70d0bd14d | ||
|
|
b99a0cd6ca | ||
|
|
d4170b2ec0 | ||
|
|
a3181bb509 | ||
|
|
4d6b90502b | ||
|
|
947427f4d9 | ||
|
|
0482a1a88b | ||
|
|
b470459987 | ||
|
|
81a010674e | ||
|
|
d946a1328e | ||
|
|
ad7d39da20 | ||
|
|
8432de5980 | ||
|
|
c73764156e | ||
|
|
df60df5242 | ||
|
|
504485ed6e | ||
|
|
1e989bde2c | ||
|
|
7e2248bbe5 | ||
|
|
a2d80b2403 | ||
|
|
76e7c76ae5 | ||
|
|
6cada6dc12 | ||
|
|
89d4461908 | ||
|
|
a124d7e866 | ||
|
|
628be2ae49 | ||
|
|
7a71e06946 | ||
|
|
91e54d8c27 | ||
|
|
071b2b8f81 | ||
|
|
0b2b59a601 | ||
|
|
65dc1cd08f | ||
|
|
1b103297b7 | ||
|
|
887c73b3c6 | ||
|
|
660c17f654 | ||
|
|
49da2eef02 | ||
|
|
aff65d42bb | ||
|
|
8968405770 | ||
|
|
b6e8ba6758 | ||
|
|
e307a62dc0 | ||
|
|
639eeed43b | ||
|
|
91f12395f1 | ||
|
|
e7daeb1c3e | ||
|
|
d3569035c8 | ||
|
|
daee52e176 | ||
|
|
a8415b01d0 | ||
|
|
77f56090ff | ||
|
|
1d3102f29b | ||
|
|
a0cbcaa3c8 | ||
|
|
ab780879d1 | ||
|
|
bfad82a2e3 | ||
|
|
e5eec59cb8 | ||
|
|
2a4b83060e | ||
|
|
9e2ebe9fcc | ||
|
|
2cef130191 | ||
|
|
17b9b1571e | ||
|
|
37c8ef3406 | ||
|
|
b7aea414e6 | ||
|
|
126a64618b | ||
|
|
8bdf9b1138 | ||
|
|
9cb242c8a5 | ||
|
|
11802426de | ||
|
|
b376044c45 | ||
|
|
7b3b4b373b | ||
|
|
512bfc4305 | ||
|
|
54ee8c0d77 | ||
|
|
063eb6904d | ||
|
|
3e183d62ed | ||
|
|
e815bbe211 | ||
|
|
2374c72105 | ||
|
|
5cf1421c61 | ||
|
|
a6a2feea62 | ||
|
|
6c1d00c2d9 | ||
|
|
f72456f67c | ||
|
|
2c556f58d3 | ||
|
|
460d08057c | ||
|
|
cc6deb24c8 | ||
|
|
e681030c72 | ||
|
|
7a51548214 | ||
|
|
8e99fd0091 | ||
|
|
7e5a586f85 | ||
|
|
c76a84b7b1 | ||
|
|
26008f1559 | ||
|
|
92d680ff94 | ||
|
|
c69c46d08c | ||
|
|
6477ade557 | ||
|
|
78dd045d29 | ||
|
|
2ddbbdf5a4 | ||
|
|
17a565023b | ||
|
|
b5d5728bf8 | ||
|
|
055641a51e | ||
|
|
fc509d4e4a | ||
|
|
a67fea2235 | ||
|
|
f062b52e05 | ||
|
|
25166f3e02 | ||
|
|
a79ea15827 | ||
|
|
8db9a2f70f | ||
|
|
ff2f2e433f | ||
|
|
3fc9f323a7 | ||
|
|
bf0aa5e8aa | ||
|
|
3b3866323e | ||
|
|
3c3cbee7fd | ||
|
|
4a95020155 | ||
|
|
3a429a869e | ||
|
|
99465b25b5 | ||
|
|
fcdad4d229 | ||
|
|
2e8b463795 | ||
|
|
206e00ccbe | ||
|
|
f13443477d | ||
|
|
9837747eef | ||
|
|
cf5dfc517a | ||
|
|
c134d4ab8e | ||
|
|
7a0b3e9377 | ||
|
|
d6f8e5a507 | ||
|
|
2ca1d6c7c1 | ||
|
|
f7fbac1eed | ||
|
|
7c1e339650 | ||
|
|
b7c0c41262 | ||
|
|
9180a718c1 | ||
|
|
306456d453 | ||
|
|
95421a5605 | ||
|
|
31635b8312 | ||
|
|
45ac64ddde | ||
|
|
c7b22b467c | ||
|
|
ae83e3c787 | ||
|
|
f68c4e9a4b | ||
|
|
2182c4508d | ||
|
|
eaab253e2b | ||
|
|
905c416ffc | ||
|
|
a6063307b5 | ||
|
|
704191b15a | ||
|
|
5625a94365 | ||
|
|
634894d3b1 | ||
|
|
0ce4e769f1 | ||
|
|
2c3655f7ff | ||
|
|
52bdd90840 | ||
|
|
fcc7240617 | ||
|
|
ecd98bdf7d | ||
|
|
e525d9bedb | ||
|
|
27b2a5cd6d | ||
|
|
8518fe639f | ||
|
|
496599518a | ||
|
|
116ad512e4 | ||
|
|
6a7b55fdf7 | ||
|
|
ed62ddb373 | ||
|
|
38b1a41f2b | ||
|
|
6f0736142b | ||
|
|
737d7cda4b | ||
|
|
75745a70b4 | ||
|
|
9ff18502b6 | ||
|
|
44a4fb59a7 | ||
|
|
6543683c0c | ||
|
|
8fee26c3d2 | ||
|
|
7c8994b538 | ||
|
|
9dfe88e2b1 | ||
|
|
d0eda1d538 | ||
|
|
896acedb07 | ||
|
|
fd7dcae8aa | ||
|
|
a4763593c7 | ||
|
|
cc2d2495c0 | ||
|
|
9b30ef474a | ||
|
|
f95e6340e0 | ||
|
|
c3ac4f84bf | ||
|
|
61729ff66a | ||
|
|
3dfcb82e5c | ||
|
|
340a158595 | ||
|
|
4b965c3f9f | ||
|
|
b5220129e7 | ||
|
|
7a4886386f | ||
|
|
ae143505e4 | ||
|
|
b9b2e5bbd9 | ||
|
|
66420439c9 | ||
|
|
ed1d8e2b8f | ||
|
|
0b07354361 | ||
|
|
fa200c9b4c | ||
|
|
1aaf836340 | ||
|
|
f51e96fe6d | ||
|
|
afefc0ddc1 | ||
|
|
bd82af2e1e | ||
|
|
34f20bf69f | ||
|
|
2c339a7fb3 | ||
|
|
ec4d61e2b1 | ||
|
|
94eec50d27 | ||
|
|
dd32ff6206 | ||
|
|
52a9c58cc0 | ||
|
|
01855b7dcf | ||
|
|
38f10884a7 | ||
|
|
9e6d2163df | ||
|
|
dbfe88510f | ||
|
|
c04eb40f90 | ||
|
|
c2668110b7 | ||
|
|
80272a0770 | ||
|
|
5a6e363c16 | ||
|
|
ed593493df | ||
|
|
3130a9a75f | ||
|
|
eeb516e7d3 | ||
|
|
c0368e1756 | ||
|
|
e19824fd2e | ||
|
|
02681f2e65 | ||
|
|
b37524603a | ||
|
|
ff3b23c1b6 | ||
|
|
4e005411fe | ||
|
|
1f7b802f79 | ||
|
|
7c2bd340db | ||
|
|
9e0788aa3a | ||
|
|
e1cf62a583 | ||
|
|
ce4cee184e | ||
|
|
db52c75221 | ||
|
|
bd74172633 | ||
|
|
e5b4946309 | ||
|
|
3918db945a | ||
|
|
25b5d1d522 | ||
|
|
164efe089f | ||
|
|
4e82eb4978 | ||
|
|
16bb31a284 | ||
|
|
d7a917e2ab | ||
|
|
8dfcb0a36d | ||
|
|
67cbdf6de8 | ||
|
|
f12091002f | ||
|
|
570f4345a0 | ||
|
|
425d0701c9 | ||
|
|
99a11f7056 | ||
|
|
8ab46c1549 | ||
|
|
8b663f2719 | ||
|
|
c0db513cf0 | ||
|
|
bf1bc9625d | ||
|
|
686c15dab9 | ||
|
|
10aa833f80 | ||
|
|
9f5af04963 | ||
|
|
ac5d72967a | ||
|
|
bcc2f3b389 | ||
|
|
3d0ee0d202 | ||
|
|
6b8c87b58a | ||
|
|
fee1d02791 | ||
|
|
d766c01d2d | ||
|
|
dc56c4c9b0 | ||
|
|
7ece26d58a | ||
|
|
f26664a569 | ||
|
|
4b0c65093a | ||
|
|
12fc8c4e7d | ||
|
|
a6cb4eb72e | ||
|
|
4df5a1321d | ||
|
|
22deca3a6a | ||
|
|
dc4292c602 | ||
|
|
56f90eb3c9 | ||
|
|
1c474e8f30 | ||
|
|
24654fe16c | ||
|
|
44820e74a3 | ||
|
|
c16908e9fc | ||
|
|
33dbdd20af | ||
|
|
69d7bdd0c0 | ||
|
|
b9f4398826 | ||
|
|
a6e7d43668 | ||
|
|
e2ee69ddb2 | ||
|
|
e8448345f1 | ||
|
|
acab008c65 | ||
|
|
8c74775a4e | ||
|
|
9b31a4b463 | ||
|
|
9f022c576c | ||
|
|
316029a9d2 | ||
|
|
34fe643cee | ||
|
|
6a01cc530e | ||
|
|
81475bee09 | ||
|
|
a167dff019 | ||
|
|
cffd587952 | ||
|
|
a0ae9d3f45 | ||
|
|
e74f8f9e11 | ||
|
|
e45ca1e830 | ||
|
|
46c734eb7e | ||
|
|
c17f7f03bd | ||
|
|
7113568c60 | ||
|
|
6143146974 | ||
|
|
15ada9a1c3 | ||
|
|
217eb13495 | ||
|
|
20d1f7a87d | ||
|
|
9c59689aa7 | ||
|
|
d47e3c40b8 | ||
|
|
d8be68436f | ||
|
|
f6adc49089 | ||
|
|
a4c0ae9c73 | ||
|
|
8b3020aaf3 | ||
|
|
182a22b8f6 | ||
|
|
892b235d0b | ||
|
|
da93e9a76c | ||
|
|
bd93d85d47 | ||
|
|
eed6a8483c | ||
|
|
954de0cae4 | ||
|
|
909941308b | ||
|
|
c59f935cc5 | ||
|
|
03b789e206 | ||
|
|
cacc2f14cb | ||
|
|
0d71194741 | ||
|
|
683d22c42b | ||
|
|
c687d1bf21 | ||
|
|
fbb98a87d1 | ||
|
|
f42beb4433 | ||
|
|
de8ab044f8 | ||
|
|
3c76885577 | ||
|
|
c347d9f84b | ||
|
|
efc4787cb0 | ||
|
|
be3ec9dc59 | ||
|
|
685a2d5579 | ||
|
|
c336507e51 | ||
|
|
d448eb5fc1 | ||
|
|
3dcd0107d2 | ||
|
|
2773e99afc | ||
|
|
9a1a3a05f5 | ||
|
|
286fab4aab | ||
|
|
20f69a121e | ||
|
|
b994c529a2 | ||
|
|
f106a68424 | ||
|
|
66c2e8cbd8 | ||
|
|
735b563ce4 | ||
|
|
604e0178f7 | ||
|
|
f210e41d5a | ||
|
|
e07e79da52 | ||
|
|
1284842f70 | ||
|
|
0aa063a1e7 | ||
|
|
87c5cb7ebf | ||
|
|
59570c08a4 | ||
|
|
eafe686dd6 | ||
|
|
d056d8a4c5 | ||
|
|
165d95be56 | ||
|
|
ea1c54f48e | ||
|
|
31f67cd3de | ||
|
|
ee87e2b260 | ||
|
|
ae61143956 | ||
|
|
1253e6cc46 | ||
|
|
19f2186c8b | ||
|
|
2e21c92c4f | ||
|
|
4262e26a15 | ||
|
|
830c116b4e | ||
|
|
1663b97eaa | ||
|
|
15e10a1d81 | ||
|
|
4bc6b5699a | ||
|
|
8e003e5022 | ||
|
|
af649af462 | ||
|
|
957d8d179b | ||
|
|
e7b265a178 | ||
|
|
422511f66d | ||
|
|
6826d8ac35 | ||
|
|
bdea9847b5 | ||
|
|
8e6ea87b76 | ||
|
|
036ac8e7c2 | ||
|
|
3306d2789b | ||
|
|
b094839210 | ||
|
|
b9c04a247a | ||
|
|
ef689c5593 | ||
|
|
7b16a377e5 | ||
|
|
793f97bdd5 | ||
|
|
d30dbd50bc | ||
|
|
e6db6ffc89 | ||
|
|
9ac2f49b57 | ||
|
|
9b2adfd3cc | ||
|
|
6dd3f1e053 | ||
|
|
677da2b22c | ||
|
|
a3ee993739 | ||
|
|
95f67445de |
@@ -1 +1,4 @@
|
||||
node: $Format:%H$
|
||||
node-date: $Format:%cI$
|
||||
describe-name: $Format:%(describe:tags=true)$
|
||||
ref-names: $Format:%D$
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
* @iterative/dvc
|
||||
@@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: DVCorg
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
os: [ubuntu-18.04]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.9.1
|
||||
uses: styfle/cancel-workflow-action@0.10.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/setup-python@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
- uses: iterative/dvc-bench@main
|
||||
|
||||
+52
-112
@@ -6,152 +6,92 @@ on:
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deb:
|
||||
runs-on: ubuntu-18.04
|
||||
binary:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- {pkg: "deb", os: "ubuntu-18.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-18.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-10.15", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
|
||||
|
||||
name: ${{ matrix.pkg }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
setup.cfg
|
||||
scripts/build-requirements.txt
|
||||
|
||||
- name: Set up Ruby 2.6
|
||||
uses: ruby/setup-ruby@v1
|
||||
if: matrix.pkg != 'exe'
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
- name: Build deb
|
||||
run: python scripts/build.py deb
|
||||
- name: Publish deb
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/fpm/dvc_${{ github.event.release.tag_name }}_amd64.deb
|
||||
asset_name: dvc_${{ github.event.release.tag_name }}_amd64.deb
|
||||
asset_content_type: binary/octet-stream
|
||||
rpm:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
- name: Build rpm
|
||||
run: python scripts/build.py rpm
|
||||
- name: Publish rpm
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
|
||||
asset_content_type: binary/octet-stream
|
||||
osxpkg:
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
run: |
|
||||
# available lxml (webdav dependency) wheels are built with an old
|
||||
# SDK, which breaks osxpkg notarization. Building from scratch
|
||||
# instead.
|
||||
pip install .[all] --no-binary lxml
|
||||
pip install -r scripts/build-requirements.txt
|
||||
gem install --no-document fpm
|
||||
- name: Build
|
||||
run: python scripts/build.py osxpkg
|
||||
- name: Publish
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}.pkg
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}.pkg
|
||||
asset_content_type: binary/octet-stream
|
||||
exe:
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: 3.9
|
||||
ruby-version: '2.6'
|
||||
|
||||
- name: Install fpm
|
||||
if: matrix.pkg != 'exe'
|
||||
run: gem install --no-document fpm
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install -U wheel pip setuptools
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
|
||||
- name: Pull images
|
||||
run: |
|
||||
dvc pull
|
||||
- name: Build
|
||||
run: python scripts\build.py exe
|
||||
shell: cmd
|
||||
- name: Publish
|
||||
run: dvc pull
|
||||
|
||||
- name: Build ${{ matrix.pkg }}
|
||||
run: python scripts/build.py ${{ matrix.pkg }}
|
||||
|
||||
- name: Publish ${{ matrix.pkg }}
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/innosetup/dvc-${{ github.event.release.tag_name }}.exe
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}.exe
|
||||
asset_path: scripts/${{ matrix.pkg == 'exe' && 'innosetup' || 'fpm' }}/${{ matrix.asset }}
|
||||
asset_name: ${{ matrix.asset }}
|
||||
asset_content_type: binary/octet-stream
|
||||
|
||||
pip:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: pip
|
||||
cache-dependency-path: setup.cfg
|
||||
|
||||
- name: Install
|
||||
run: pip install ".[all,tests]"
|
||||
run: |
|
||||
pip install -U wheel pip build setuptools
|
||||
pip install ".[all,tests]"
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
pip install -U pip
|
||||
./scripts/build_package.sh
|
||||
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
@@ -17,14 +17,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.9.1
|
||||
uses: styfle/cancel-workflow-action@0.10.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install requirements
|
||||
@@ -43,12 +43,15 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.8", "3.9", "3.10"]
|
||||
include:
|
||||
- {os: ubuntu-latest, pyv: "3.11-dev"}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
- name: get pip cache dir
|
||||
@@ -68,6 +71,8 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install -e ".[dev]"
|
||||
env:
|
||||
AIOHTTP_NO_EXTENSIONS: ${{ matrix.pyv == '3.11-dev' && '1' }}
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
run: >-
|
||||
|
||||
@@ -6,9 +6,9 @@ repos:
|
||||
- id: black
|
||||
language_version: python3
|
||||
repo: https://github.com/ambv/black
|
||||
rev: 22.3.0
|
||||
rev: 22.6.0
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.2.0
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
|
||||
+124
-145
@@ -3,106 +3,114 @@
|
||||
`Website <https://dvc.org>`_
|
||||
• `Docs <https://dvc.org/doc>`_
|
||||
• `Blog <http://blog.dataversioncontrol.com>`_
|
||||
• `Twitter <https://twitter.com/DVCorg>`_
|
||||
• `Chat (Community & Support) <https://dvc.org/chat>`_
|
||||
• `Tutorial <https://dvc.org/doc/get-started>`_
|
||||
• `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
|
||||
• `Related Technologies <https://dvc.org/doc/user-guide/related-technologies>`_
|
||||
• `How DVC works`_
|
||||
• `VS Code Extension`_
|
||||
• `Installation`_
|
||||
• `Contributing`_
|
||||
• `Community and Support`_
|
||||
|
||||
|CI| |Maintainability| |Coverage| |Donate| |DOI|
|
||||
|CI| |Python Version| |Coverage| |VS Code| |DOI|
|
||||
|
||||
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
|
||||
|
||||
|
|
||||
|
||||
**Data Version Control** or **DVC** helps you develop reproducible machine
|
||||
learning projects:
|
||||
**Data Version Control** or **DVC** is a command line tool and `VS Code Extension`_ to help you develop reproducible machine learning projects:
|
||||
|
||||
#. Version your data and models.
|
||||
#. **Version** your data and models.
|
||||
Store them in your cloud storage but keep their version info in your Git repo.
|
||||
|
||||
#. Connect them to your code and parameters to form a pipeline.
|
||||
#. **Iterate** fast with lightweight pipelines.
|
||||
When you make changes, only run the steps impacted by those changes.
|
||||
|
||||
#. Modify data, code, or parameters and run your modified pipeline.
|
||||
#. **Track** experiments in your local Git repo (no servers needed).
|
||||
|
||||
#. Iterate and track each experiment.
|
||||
#. **Compare** any data, code, parameters, model, or performance plots.
|
||||
|
||||
#. Share, compare, and reconstruct experiments.
|
||||
|
||||
**Why DVC?**
|
||||
|
||||
#. **Store** data and models (big) in your cloud storage, but keep their version
|
||||
info (small) in your Git repo.
|
||||
|
||||
#. **Iterate** fast with straightforward YAML pipelines. When you make changes,
|
||||
only run the steps impacted by those changes.
|
||||
|
||||
#. **Track** experiments in your local Git repo (no servers needed) and choose
|
||||
which to keep and share.
|
||||
|
||||
#. **Compare** any data, code, parameters, metrics, or plots and analyze however
|
||||
you want.
|
||||
|
||||
#. **Reconstruct** anyone's experiment with a single command.
|
||||
|
||||
.. contents:: **Contents**
|
||||
:backlinks: none
|
||||
|
||||
How DVC works
|
||||
=============
|
||||
|
||||
We encourage you to read our `Get Started <https://dvc.org/doc/get-started>`_ guide to better understand what DVC
|
||||
is and how it can fit your scenarios.
|
||||
|
||||
The easiest (but not perfect!) *analogy* to describe it: DVC is Git (or Git-LFS to be precise) & Makefiles
|
||||
made right and tailored specifically for ML and Data Science scenarios.
|
||||
|
||||
#. ``Git/Git-LFS`` part - DVC helps store and share data artifacts and models, connecting them with a Git repository.
|
||||
#. ``Makefile``\ s part - DVC describes how one data or model artifact was built from other data and code.
|
||||
|
||||
DVC usually runs along with Git. Git is used as usual to store and version code (including DVC meta-files). DVC helps
|
||||
to store data and model files seamlessly out of Git, while preserving almost the same user experience as if they
|
||||
were stored in Git itself. To store and share the data cache, DVC supports multiple remotes - any cloud (S3, Azure,
|
||||
Google Cloud, etc) or any on-premise network storage (via SSH, for example).
|
||||
|
||||
|Flowchart|
|
||||
|
||||
The DVC pipelines (computational graph) feature connects code and data together. It is possible to explicitly
|
||||
specify all steps required to produce a model: input dependencies including data, commands to run,
|
||||
and output information to be saved. See the quick start section below or
|
||||
the `Get Started <https://dvc.org/doc/get-started>`_ tutorial to learn more.
|
||||
#. **Share** experiments and automatically reproduce anyone's experiment.
|
||||
|
||||
Quick start
|
||||
===========
|
||||
|
||||
Please read `Get Started <https://dvc.org/doc/get-started>`_ guide for a full version. Common workflow commands include:
|
||||
Please read our `Command Reference <https://dvc.org/doc/command-reference>`_ for a complete list.
|
||||
|
||||
+-----------------------------------+----------------------------------------------------------------------------+
|
||||
| Step | Command |
|
||||
+===================================+============================================================================+
|
||||
| Track data | | ``$ git add train.py`` |
|
||||
| | | ``$ dvc add images.zip`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------+
|
||||
| Connect code and data by commands | | ``$ dvc run -n prepare -d images.zip -o images/ unzip -q images.zip`` |
|
||||
| | | ``$ dvc run -n train -d images/ -d train.py -o model.p python train.py`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------+
|
||||
| Make changes and reproduce | | ``$ vi train.py`` |
|
||||
| | | ``$ dvc repro model.p.dvc`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------+
|
||||
| Share code | | ``$ git add .`` |
|
||||
| | | ``$ git commit -m 'The baseline model'`` |
|
||||
| | | ``$ git push`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------+
|
||||
| Share data and ML models | | ``$ dvc remote add myremote -d s3://mybucket/image_cnn`` |
|
||||
| | | ``$ dvc push`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------+
|
||||
A common CLI workflow includes:
|
||||
|
||||
|
||||
+-----------------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| Task | Terminal |
|
||||
+===================================+====================================================================================================+
|
||||
| Track data | | ``$ git add train.py params.yaml`` |
|
||||
| | | ``$ dvc add images/`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| Connect code and data | | ``$ dvc stage add -n featurize -d images/ -o features/ python featurize.py`` |
|
||||
| | | ``$ dvc stage add -n train -d features/ -d train.py -o model.p -M metrics.json python train.py`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| Make changes and experiment | | ``$ dvc exp run -n exp-baseline`` |
|
||||
| | | ``$ vi train.py`` |
|
||||
| | | ``$ dvc exp run -n exp-code-change`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| Compare and select experiments | | ``$ dvc exp show`` |
|
||||
| | | ``$ dvc exp apply exp-baseline`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| Share code | | ``$ git add .`` |
|
||||
| | | ``$ git commit -m 'The baseline model'`` |
|
||||
| | | ``$ git push`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| Share data and ML models | | ``$ dvc remote add myremote -d s3://mybucket/image_cnn`` |
|
||||
| | | ``$ dvc push`` |
|
||||
+-----------------------------------+----------------------------------------------------------------------------------------------------+
|
||||
|
||||
How DVC works
|
||||
=============
|
||||
|
||||
We encourage you to read our `Get Started
|
||||
<https://dvc.org/doc/get-started>`_ docs to better understand what DVC
|
||||
does and how it can fit your scenarios.
|
||||
|
||||
The closest *analogies* to describe the main DVC features are these:
|
||||
|
||||
#. **Git for data**: Store and share data artifacts (like Git-LFS but without a server) and models, connecting them with a Git repository. Data management meets GitOps!
|
||||
#. **Makefiles** for ML: Describes how data or model artifacts are built from other data and code in a standard format. Now you can version your data pipelines with Git.
|
||||
#. Local **experiment tracking**: Turn your machine into an ML experiment management platform, and collaborate with others using existing Git hosting (Github, Gitlab, etc.).
|
||||
|
||||
Git is employed as usual to store and version code (including DVC meta-files as placeholders for data).
|
||||
DVC `stores data and model files <https://dvc.org/doc/start/data-management>`_ seamlessly in a cache outside of Git, while preserving almost the same user experience as if they were in the repo.
|
||||
To share and back up the *data cache*, DVC supports multiple remote storage platforms - any cloud (S3, Azure, Google Cloud, etc.) or on-premise network storage (via SSH, for example).
|
||||
|
||||
|Flowchart|
|
||||
|
||||
`DVC pipelines <https://dvc.org/doc/start/data-management/pipelines>`_ (computational graphs) connect code and data together.
|
||||
They specify all steps required to produce a model: input dependencies including code, data, commands to run; and output information to be saved.
|
||||
|
||||
Last but not least, `DVC Experiment Versioning <https://dvc.org/doc/start/experiments>`_ lets you prepare and run a large number of experiments.
|
||||
Their results can be filtered and compared based on hyperparameters and metrics, and visualized with multiple plots.
|
||||
|
||||
.. _`VS Code Extension`:
|
||||
|
||||
Visual Studio Code Extension
|
||||
============================
|
||||
|
||||
|VS Code|
|
||||
|
||||
To use DVC as a GUI right from your VS Code IDE, install the `DVC Extension <https://marketplace.visualstudio.com/items?itemName=Iterative.dvc>`_ from the Marketplace.
|
||||
It currently features experiment tracking and data management, and more features (data pipeline support, etc.) are coming soon!
|
||||
|
||||
|VS Code Extension Overview|
|
||||
|
||||
Note: You'll have to install core DVC on your system separately (as detailed
|
||||
below). The Extension will guide you if needed.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
There are four options to install DVC: ``pip``, Homebrew, Conda (Anaconda) or an OS-specific package.
|
||||
There are several ways to install DVC: in VS Code; using ``snap``, ``choco``, ``brew``, ``conda``, ``pip``; or with an OS-specific package.
|
||||
Full instructions are `available here <https://dvc.org/doc/get-started/install>`_.
|
||||
|
||||
Snap (Snapcraft/Linux)
|
||||
----------------------
|
||||
Snapcraft (Linux)
|
||||
-----------------
|
||||
|
||||
|Snap|
|
||||
|
||||
@@ -111,11 +119,10 @@ Snap (Snapcraft/Linux)
|
||||
snap install dvc --classic
|
||||
|
||||
This corresponds to the latest tagged release.
|
||||
Add ``--beta`` for the latest tagged release candidate,
|
||||
or ``--edge`` for the latest ``main`` version.
|
||||
Add ``--beta`` for the latest tagged release candidate, or ``--edge`` for the latest ``main`` version.
|
||||
|
||||
Choco (Chocolatey/Windows)
|
||||
--------------------------
|
||||
Chocolatey (Windows)
|
||||
--------------------
|
||||
|
||||
|Choco|
|
||||
|
||||
@@ -123,8 +130,8 @@ Choco (Chocolatey/Windows)
|
||||
|
||||
choco install dvc
|
||||
|
||||
Brew (Homebrew/Mac OS)
|
||||
----------------------
|
||||
Brew (mac OS)
|
||||
-------------
|
||||
|
||||
|Brew|
|
||||
|
||||
@@ -132,8 +139,8 @@ Brew (Homebrew/Mac OS)
|
||||
|
||||
brew install dvc
|
||||
|
||||
Conda (Anaconda)
|
||||
----------------
|
||||
Anaconda (Any platform)
|
||||
-----------------------
|
||||
|
||||
|Conda|
|
||||
|
||||
@@ -142,11 +149,10 @@ Conda (Anaconda)
|
||||
conda install -c conda-forge mamba # installs much faster than conda
|
||||
mamba install -c conda-forge dvc
|
||||
|
||||
Depending on the remote storage type you plan to use to keep and share your data, you might need to
|
||||
install optional dependencies: `dvc-s3`, `dvc-azure`, `dvc-gdrive`, `dvc-gs`, `dvc-oss`, `dvc-ssh`.
|
||||
Depending on the remote storage type you plan to use to keep and share your data, you might need to install optional dependencies: `dvc-s3`, `dvc-azure`, `dvc-gdrive`, `dvc-gs`, `dvc-oss`, `dvc-ssh`.
|
||||
|
||||
pip (PyPI)
|
||||
----------
|
||||
PyPI (Python)
|
||||
-------------
|
||||
|
||||
|PyPI|
|
||||
|
||||
@@ -154,10 +160,8 @@ pip (PyPI)
|
||||
|
||||
pip install dvc
|
||||
|
||||
Depending on the remote storage type you plan to use to keep and share your data, you might need to specify
|
||||
one of the optional dependencies: ``s3``, ``gs``, ``azure``, ``oss``, ``ssh``. Or ``all`` to include them all.
|
||||
The command should look like this: ``pip install dvc[s3]`` (in this case AWS S3 dependencies such as ``boto3``
|
||||
will be installed automatically).
|
||||
Depending on the remote storage type you plan to use to keep and share your data, you might need to specify one of the optional dependencies: ``s3``, ``gs``, ``azure``, ``oss``, ``ssh``. Or ``all`` to include them all.
|
||||
The command should look like this: ``pip install 'dvc[s3]'`` (in this case AWS S3 dependencies such as ``boto3`` will be installed automatically).
|
||||
|
||||
To install the development version, run:
|
||||
|
||||
@@ -165,13 +169,13 @@ To install the development version, run:
|
||||
|
||||
pip install git+git://github.com/iterative/dvc
|
||||
|
||||
Package
|
||||
-------
|
||||
Package (Platform-specific)
|
||||
---------------------------
|
||||
|
||||
|Packages|
|
||||
|
||||
Self-contained packages for Linux, Windows, and Mac are available. The latest version of the packages
|
||||
can be found on the GitHub `releases page <https://github.com/iterative/dvc/releases>`_.
|
||||
Self-contained packages for Linux, Windows, and Mac are available.
|
||||
The latest version of the packages can be found on the GitHub `releases page <https://github.com/iterative/dvc/releases>`_.
|
||||
|
||||
Ubuntu / Debian (deb)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -191,64 +195,32 @@ Fedora / CentOS (rpm)
|
||||
sudo yum update
|
||||
sudo yum install dvc
|
||||
|
||||
Comparison to related technologies
|
||||
==================================
|
||||
|
||||
#. Data Engineering tools such as `AirFlow <https://airflow.apache.org/>`_,
|
||||
`Luigi <https://github.com/spotify/luigi>`_, and others - in DVC data,
|
||||
model and ML pipelines represent a single ML project focused on data
|
||||
scientists' experience. Data engineering tools orchestrate multiple data
|
||||
projects and focus on efficient execution. A DVC project can be used from
|
||||
existing data pipelines as a single execution step.
|
||||
|
||||
#. `Git-annex <https://git-annex.branchable.com/>`_ - DVC uses the idea of storing the content of large files (which should
|
||||
not be in a Git repository) in a local key-value store, and uses file hardlinks/symlinks instead of
|
||||
copying/duplicating files.
|
||||
|
||||
#. `Git-LFS <https://git-lfs.github.com/>`_ - DVC is compatible with many
|
||||
remote storage services (S3, Google Cloud, Azure, SSH, etc). DVC also
|
||||
uses reflinks or hardlinks to avoid copy operations on checkouts; thus
|
||||
handling large data files much more efficiently.
|
||||
|
||||
#. Makefile (and analogues including ad-hoc scripts) - DVC tracks
|
||||
dependencies (in a directed acyclic graph).
|
||||
|
||||
#. `Workflow Management Systems <https://en.wikipedia.org/wiki/Workflow_management_system>`_ - DVC is a workflow
|
||||
management system designed specifically to manage machine learning experiments. DVC is built on top of Git.
|
||||
|
||||
#. `DAGsHub <https://dagshub.com/>`_ - online service to host DVC
|
||||
projects. It provides a useful UI around DVC repositories and integrates
|
||||
other tools.
|
||||
|
||||
#. `DVC Studio <https://studio.iterative.ai/>`_ - official online
|
||||
platform for DVC projects. It can be used to manage data and models, run
|
||||
and track experiments, and visualize and share results. Also, it
|
||||
integrates with `CML (CI/CD for ML) <https://cml.dev/>`__ for training
|
||||
models in the cloud or Kubernetes.
|
||||
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
|Maintainability| |Donate|
|
||||
|Maintainability|
|
||||
|
||||
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more
|
||||
details. Thanks to all our contributors!
|
||||
Contributions are welcome!
|
||||
Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more details.
|
||||
Thanks to all our contributors!
|
||||
|
||||
|Contribs|
|
||||
|
||||
Mailing List
|
||||
============
|
||||
Community and Support
|
||||
=====================
|
||||
|
||||
Want to stay up to date? Want to help improve DVC by participating in our occasional polls? Subscribe to our `mailing list <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_. No spam, really low traffic.
|
||||
* `Twitter <https://twitter.com/DVCorg>`_
|
||||
* `Forum <https://discuss.dvc.org/>`_
|
||||
* `Discord Chat <https://dvc.org/chat>`_
|
||||
* `Email <mailto:[email protected]>`_
|
||||
* `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This project is distributed under the Apache license version 2.0 (see the LICENSE file in the project root).
|
||||
|
||||
By submitting a pull request to this project, you agree to license your contribution under the Apache license version
|
||||
2.0 to this project.
|
||||
By submitting a pull request to this project, you agree to license your contribution under the Apache license version 2.0 to this project.
|
||||
|
||||
Citation
|
||||
========
|
||||
@@ -265,6 +237,9 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://dvc.org
|
||||
:alt: DVC logo
|
||||
|
||||
.. |VS Code Extension Overview| image:: https://raw.githubusercontent.com/iterative/vscode-dvc/main/extension/docs/overview.gif
|
||||
:alt: DVC Extension for VS Code
|
||||
|
||||
.. |CI| image:: https://github.com/iterative/dvc/workflows/Tests/badge.svg?branch=main
|
||||
:target: https://github.com/iterative/dvc/actions
|
||||
:alt: GHA Tests
|
||||
@@ -273,14 +248,14 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://codeclimate.com/github/iterative/dvc
|
||||
:alt: Code Climate
|
||||
|
||||
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/dvc
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: Python Version
|
||||
|
||||
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/main/graph/badge.svg
|
||||
:target: https://codecov.io/gh/iterative/dvc
|
||||
:alt: Codecov
|
||||
|
||||
.. |Donate| image:: https://img.shields.io/badge/patreon-donate-green.svg?logo=patreon
|
||||
:target: https://www.patreon.com/DVCorg/overview
|
||||
:alt: Donate
|
||||
|
||||
.. |Snap| image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
|
||||
:target: https://snapcraft.io/dvc
|
||||
:alt: Snapcraft
|
||||
@@ -316,3 +291,7 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
.. |Contribs| image:: https://contrib.rocks/image?repo=iterative/dvc
|
||||
:target: https://github.com/iterative/dvc/graphs/contributors
|
||||
:alt: Contributors
|
||||
|
||||
.. |VS Code| image:: https://vsmarketplacebadge.apphb.com/version/Iterative.dvc.svg
|
||||
:target: https://marketplace.visualstudio.com/items?itemName=Iterative.dvc
|
||||
:alt: VS Code Extension
|
||||
|
||||
+2
-2
@@ -50,7 +50,7 @@ def is_enabled():
|
||||
Config(validate=False).get("core", {}).get("analytics", "true")
|
||||
)
|
||||
|
||||
logger.debug("Analytics is {}abled.".format("en" if enabled else "dis"))
|
||||
logger.debug("Analytics is %sabled.", "en" if enabled else "dis")
|
||||
|
||||
return enabled
|
||||
|
||||
@@ -187,4 +187,4 @@ def _find_or_create_user_id():
|
||||
return user_id
|
||||
|
||||
except LockError:
|
||||
logger.debug(f"Failed to acquire '{lockfile}'")
|
||||
logger.debug("Failed to acquire '%s'", lockfile)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from dataclasses import asdict, dataclass, field, fields
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
|
||||
from funcy import compact
|
||||
|
||||
|
||||
@dataclass
|
||||
class Annotation:
|
||||
PARAM_DESC: ClassVar[str] = "desc"
|
||||
PARAM_TYPE: ClassVar[str] = "type"
|
||||
PARAM_LABELS: ClassVar[str] = "labels"
|
||||
PARAM_META: ClassVar[str] = "meta"
|
||||
|
||||
desc: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def update(self, **kwargs) -> "Annotation":
|
||||
for attr, value in kwargs.items():
|
||||
if value and hasattr(self, attr):
|
||||
setattr(self, attr, value)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
ANNOTATION_FIELDS = [field.name for field in fields(Annotation)]
|
||||
ANNOTATION_SCHEMA = {
|
||||
Annotation.PARAM_DESC: str,
|
||||
Annotation.PARAM_TYPE: str,
|
||||
Annotation.PARAM_LABELS: [str],
|
||||
Annotation.PARAM_META: object,
|
||||
}
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
import os
|
||||
from contextlib import _GeneratorContextManager as GCM
|
||||
|
||||
from funcy import reraise
|
||||
|
||||
from dvc.exceptions import OutputNotFoundError, PathMissingError
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def get_url(path, repo=None, rev=None, remote=None):
|
||||
"""
|
||||
Returns the URL to the storage location of a data file or directory tracked
|
||||
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
|
||||
supplied. The default remote is tried unless a remote argument is supplied.
|
||||
|
||||
Raises OutputNotFoundError if the file is not tracked by DVC.
|
||||
|
||||
NOTE: This function does not check for the actual existence of the file or
|
||||
directory in the remote storage.
|
||||
"""
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
fs_path = _repo.dvcfs.from_os_path(path)
|
||||
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
||||
info = _repo.dvcfs.info(fs_path)
|
||||
|
||||
dvc_info = info.get("dvc_info")
|
||||
if not dvc_info:
|
||||
raise OutputNotFoundError(path, repo)
|
||||
|
||||
dvc_repo = info["repo"]
|
||||
md5 = dvc_info["md5"]
|
||||
|
||||
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
|
||||
|
||||
|
||||
def open( # noqa, pylint: disable=redefined-builtin
|
||||
path, repo=None, rev=None, remote=None, mode="r", encoding=None
|
||||
):
|
||||
"""
|
||||
Open file in the supplied path tracked in a repo (both DVC projects and
|
||||
plain Git repos are supported). For Git repos, HEAD is used unless a rev
|
||||
argument is supplied. The default remote is tried unless a remote argument
|
||||
is supplied. It may only be used as a context manager:
|
||||
|
||||
with dvc.api.open(
|
||||
'path/to/file',
|
||||
repo='https://example.com/url/to/repo'
|
||||
) as fd:
|
||||
# ... Handle file object fd
|
||||
"""
|
||||
args = (path,)
|
||||
kwargs = {
|
||||
"repo": repo,
|
||||
"remote": remote,
|
||||
"rev": rev,
|
||||
"mode": mode,
|
||||
"encoding": encoding,
|
||||
}
|
||||
return _OpenContextManager(_open, args, kwargs)
|
||||
|
||||
|
||||
class _OpenContextManager(GCM):
|
||||
def __init__(
|
||||
self, func, args, kwds
|
||||
): # pylint: disable=super-init-not-called
|
||||
self.gen = func(*args, **kwds)
|
||||
self.func, self.args, self.kwds = func, args, kwds
|
||||
|
||||
def __getattr__(self, name):
|
||||
raise AttributeError(
|
||||
"dvc.api.open() should be used in a with statement."
|
||||
)
|
||||
|
||||
|
||||
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
with _repo.open_by_relpath(
|
||||
path, remote=remote, mode=mode, encoding=encoding
|
||||
) as fd:
|
||||
yield fd
|
||||
|
||||
|
||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
"""
|
||||
Returns the contents of a tracked file (by DVC or Git). For Git repos, HEAD
|
||||
is used unless a rev argument is supplied. The default remote is tried
|
||||
unless a remote argument is supplied.
|
||||
"""
|
||||
with open(
|
||||
path, repo=repo, rev=rev, remote=remote, mode=mode, encoding=encoding
|
||||
) as fd:
|
||||
return fd.read()
|
||||
|
||||
|
||||
def make_checkpoint():
|
||||
"""
|
||||
Signal DVC to create a checkpoint experiment.
|
||||
|
||||
If the current process is being run from DVC, this function will block
|
||||
until DVC has finished creating the checkpoint. Otherwise, this function
|
||||
will return immediately.
|
||||
"""
|
||||
import builtins
|
||||
from time import sleep
|
||||
|
||||
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
|
||||
from dvc.stage.monitor import CheckpointTask
|
||||
|
||||
if os.getenv(DVC_CHECKPOINT) is None:
|
||||
return
|
||||
|
||||
root_dir = os.getenv(DVC_ROOT, Repo.find_root())
|
||||
signal_file = os.path.join(
|
||||
root_dir, Repo.DVC_DIR, "tmp", CheckpointTask.SIGNAL_FILE
|
||||
)
|
||||
|
||||
with builtins.open(signal_file, "w", encoding="utf-8") as fobj:
|
||||
# NOTE: force flushing/writing empty file to disk, otherwise when
|
||||
# run in certain contexts (pytest) file may not actually be written
|
||||
fobj.write("")
|
||||
fobj.flush()
|
||||
os.fsync(fobj.fileno())
|
||||
while os.path.exists(signal_file):
|
||||
sleep(0.1)
|
||||
@@ -0,0 +1,9 @@
|
||||
from .data import ( # noqa, pylint: disable=redefined-builtin
|
||||
get_url,
|
||||
open,
|
||||
read,
|
||||
)
|
||||
from .experiments import make_checkpoint
|
||||
from .params import params_show
|
||||
|
||||
__all__ = ["get_url", "make_checkpoint", "open", "params_show", "read"]
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
from contextlib import _GeneratorContextManager as GCM
|
||||
from typing import Optional
|
||||
|
||||
from funcy import reraise
|
||||
|
||||
from dvc.exceptions import OutputNotFoundError, PathMissingError
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def get_url(path, repo=None, rev=None, remote=None):
|
||||
"""
|
||||
Returns the URL to the storage location of a data file or directory tracked
|
||||
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
|
||||
supplied. The default remote is tried unless a remote argument is supplied.
|
||||
|
||||
Raises OutputNotFoundError if the file is not tracked by DVC.
|
||||
|
||||
NOTE: This function does not check for the actual existence of the file or
|
||||
directory in the remote storage.
|
||||
"""
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
fs_path = _repo.dvcfs.from_os_path(path)
|
||||
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
||||
info = _repo.dvcfs.info(fs_path)
|
||||
|
||||
dvc_info = info.get("dvc_info")
|
||||
if not dvc_info:
|
||||
raise OutputNotFoundError(path, repo)
|
||||
|
||||
dvc_repo = info["repo"]
|
||||
md5 = dvc_info["md5"]
|
||||
|
||||
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
|
||||
|
||||
|
||||
class _OpenContextManager(GCM):
|
||||
def __init__(
|
||||
self, func, args, kwds
|
||||
): # pylint: disable=super-init-not-called
|
||||
self.gen = func(*args, **kwds)
|
||||
self.func, self.args, self.kwds = func, args, kwds
|
||||
|
||||
def __getattr__(self, name):
|
||||
raise AttributeError(
|
||||
"dvc.api.open() should be used in a with statement."
|
||||
)
|
||||
|
||||
|
||||
def open( # noqa, pylint: disable=redefined-builtin
|
||||
path: str,
|
||||
repo: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
remote: Optional[str] = None,
|
||||
mode: str = "r",
|
||||
encoding: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Opens a file tracked in a DVC project.
|
||||
|
||||
This function may only be used as a context manager (using the `with`
|
||||
keyword, as shown in the examples).
|
||||
|
||||
This function makes a direct connection to the remote storage, so the file
|
||||
contents can be streamed. Your code can process the data buffer as it's
|
||||
streamed, which optimizes memory usage.
|
||||
|
||||
Note:
|
||||
Use dvc.api.read() to load the complete file contents
|
||||
in a single function call, no context manager involved.
|
||||
Neither function utilizes disc space.
|
||||
|
||||
Args:
|
||||
path (str): location and file name of the target to open,
|
||||
relative to the root of `repo`.
|
||||
repo (str, optional): location of the DVC project or Git Repo.
|
||||
Defaults to the current DVC project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
rev (str, optional): Any `Git revision`_ such as a branch or tag name,
|
||||
a commit hash or a dvc experiment name.
|
||||
Defaults to HEAD.
|
||||
If `repo` is not a Git repo, this option is ignored.
|
||||
remote (str, optional): Name of the `DVC remote`_ used to form the
|
||||
returned URL string.
|
||||
Defaults to the `default remote`_ of `repo`.
|
||||
For local projects, the cache is tried before the default remote.
|
||||
mode (str, optional): Specifies the mode in which the file is opened.
|
||||
Defaults to "r" (read).
|
||||
Mirrors the namesake parameter in builtin `open()`_.
|
||||
Only reading `mode` is supported.
|
||||
encoding(str, optional): `Codec`_ used to decode the file contents.
|
||||
Defaults to None.
|
||||
This should only be used in text mode.
|
||||
Mirrors the namesake parameter in builtin `open()`_.
|
||||
|
||||
Returns:
|
||||
_OpenContextManager: A context manager that generatse a corresponding
|
||||
`file object`_.
|
||||
The exact type of file object depends on the mode used.
|
||||
For more details, please refer to Python's `open()`_ built-in,
|
||||
which is used under the hood.
|
||||
|
||||
Raises:
|
||||
AttributeError: If this method is not used as a context manager.
|
||||
ValueError: If non-read `mode` is used.
|
||||
|
||||
Examples:
|
||||
|
||||
- Use data or models from a DVC repository.
|
||||
|
||||
Any file tracked in a DVC project (and stored remotely) can be
|
||||
processed directly in your Python code with this API.
|
||||
For example, an XML file tracked in a public DVC repo on GitHub can be
|
||||
processed like this:
|
||||
|
||||
>>> from xml.sax import parse
|
||||
>>> import dvc.api
|
||||
>>> from mymodule import mySAXHandler
|
||||
|
||||
>>> with dvc.api.open(
|
||||
... 'get-started/data.xml',
|
||||
... repo='https://github.com/iterative/dataset-registry'
|
||||
... ) as fd:
|
||||
... parse(fd, mySAXHandler)
|
||||
|
||||
We use a SAX XML parser here because dvc.api.open() is able to stream
|
||||
the data from remote storage.
|
||||
The mySAXHandler object should handle the event-driven parsing of the
|
||||
document in this case.
|
||||
This increases the performance of the code (minimizing memory usage),
|
||||
and is typically faster than loading the whole data into memory.
|
||||
|
||||
- Accessing private repos
|
||||
|
||||
This is just a matter of using the right repo argument, for example an
|
||||
SSH URL (requires that the credentials are configured locally):
|
||||
|
||||
>>> import dvc.api
|
||||
|
||||
>>> with dvc.api.open(
|
||||
... 'features.dat',
|
||||
... repo='[email protected]:path/to/repo.git'
|
||||
... ) as fd:
|
||||
... # ... Process 'features'
|
||||
... pass
|
||||
|
||||
- Use different versions of data
|
||||
|
||||
Any git revision (see `rev`) can be accessed programmatically.
|
||||
For example, if your DVC repo has tagged releases of a CSV dataset:
|
||||
|
||||
>>> import csv
|
||||
>>> import dvc.api
|
||||
>>> with dvc.api.open(
|
||||
... 'clean.csv',
|
||||
... rev='v1.1.0'
|
||||
... ) as fd:
|
||||
... reader = csv.reader(fd)
|
||||
... # ... Process 'clean' data from version 1.1.0
|
||||
|
||||
.. _Git revision:
|
||||
https://git-scm.com/docs/revisions
|
||||
|
||||
.. _DVC remote:
|
||||
https://dvc.org/doc/command-reference/remote
|
||||
|
||||
.. _default remote:
|
||||
https://dvc.org/doc/command-reference/remote/default
|
||||
|
||||
.. _open():
|
||||
https://docs.python.org/3/library/functions.html#open
|
||||
|
||||
.. _Codec:
|
||||
https://docs.python.org/3/library/codecs.html#standard-encodings
|
||||
|
||||
.. _file object:
|
||||
https://docs.python.org/3/glossary.html#term-file-object
|
||||
|
||||
"""
|
||||
if "r" not in mode:
|
||||
raise ValueError("Only reading `mode` is supported.")
|
||||
|
||||
args = (path,)
|
||||
kwargs = {
|
||||
"repo": repo,
|
||||
"remote": remote,
|
||||
"rev": rev,
|
||||
"mode": mode,
|
||||
"encoding": encoding,
|
||||
}
|
||||
return _OpenContextManager(_open, args, kwargs)
|
||||
|
||||
|
||||
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
with _repo.open_by_relpath(
|
||||
path, remote=remote, mode=mode, encoding=encoding
|
||||
) as fd:
|
||||
yield fd
|
||||
|
||||
|
||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
"""
|
||||
Returns the contents of a tracked file (by DVC or Git). For Git repos, HEAD
|
||||
is used unless a rev argument is supplied. The default remote is tried
|
||||
unless a remote argument is supplied.
|
||||
"""
|
||||
with open(
|
||||
path, repo=repo, rev=rev, remote=remote, mode=mode, encoding=encoding
|
||||
) as fd:
|
||||
return fd.read()
|
||||
@@ -0,0 +1,33 @@
|
||||
import builtins
|
||||
import os
|
||||
from time import sleep
|
||||
|
||||
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage.monitor import CheckpointTask
|
||||
|
||||
|
||||
def make_checkpoint():
|
||||
"""
|
||||
Signal DVC to create a checkpoint experiment.
|
||||
|
||||
If the current process is being run from DVC, this function will block
|
||||
until DVC has finished creating the checkpoint. Otherwise, this function
|
||||
will return immediately.
|
||||
"""
|
||||
if os.getenv(DVC_CHECKPOINT) is None:
|
||||
return
|
||||
|
||||
root_dir = os.getenv(DVC_ROOT, Repo.find_root())
|
||||
signal_file = os.path.join(
|
||||
root_dir, Repo.DVC_DIR, "tmp", CheckpointTask.SIGNAL_FILE
|
||||
)
|
||||
|
||||
with builtins.open(signal_file, "w", encoding="utf-8") as fobj:
|
||||
# NOTE: force flushing/writing empty file to disk, otherwise when
|
||||
# run in certain contexts (pytest) file may not actually be written
|
||||
fobj.write("")
|
||||
fobj.flush()
|
||||
os.fsync(fobj.fileno())
|
||||
while os.path.exists(signal_file):
|
||||
sleep(0.1)
|
||||
@@ -0,0 +1,278 @@
|
||||
from collections import Counter
|
||||
from typing import Dict, Iterable, Optional, Union
|
||||
|
||||
from funcy import first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def params_show(
|
||||
*targets: str,
|
||||
repo: Optional[str] = None,
|
||||
stages: Optional[Union[str, Iterable[str]]] = None,
|
||||
rev: Optional[str] = None,
|
||||
deps: bool = False,
|
||||
) -> Dict:
|
||||
"""Get parameters tracked in `repo`.
|
||||
|
||||
Without arguments, this function will retrieve all params from all tracked
|
||||
parameter files, for the current working tree.
|
||||
|
||||
See the options below to restrict the parameters retrieved.
|
||||
|
||||
Args:
|
||||
*targets (str, optional): Names of the parameter files to retrieve
|
||||
params from. For example, "params.py, myparams.toml".
|
||||
If no `targets` are provided, all parameter files tracked in `dvc.yaml`
|
||||
will be used.
|
||||
Note that targets don't necessarily have to be defined in `dvc.yaml`.
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
stages (Union[str, Iterable[str]], optional): Name or names of the
|
||||
stages to retrieve parameters from.
|
||||
Defaults to `None`.
|
||||
If `None`, all parameters from all stages will be retrieved.
|
||||
If this method is called from a different location to the one where
|
||||
the `dvc.yaml` is found, the relative path to the `dvc.yaml` must
|
||||
be provided as a prefix with the syntax `{relpath}:{stage}`.
|
||||
For example: `subdir/dvc.yaml:stage-0` or `../dvc.yaml:stage-1`.
|
||||
rev (str, optional): Name of the `Git revision`_ to retrieve parameters
|
||||
from.
|
||||
Defaults to `None`.
|
||||
An example of git revision can be a branch or tag name, a commit
|
||||
hash or a dvc experiment name.
|
||||
If `repo` is not a Git repo, this option is ignored.
|
||||
If `None`, the current working tree will be used.
|
||||
deps (bool, optional): Whether to retrieve only parameters that are
|
||||
stage dependencies or not.
|
||||
Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
|
||||
Raises:
|
||||
DvcException: If no params are found in `repo`.
|
||||
|
||||
Examples:
|
||||
|
||||
- No arguments.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> params = dvc.api.params_show()
|
||||
>>> print(json.dumps(params, indent=4))
|
||||
{
|
||||
"prepare": {
|
||||
"split": 0.2,
|
||||
"seed": 20170428
|
||||
},
|
||||
"featurize": {
|
||||
"max_features": 200,
|
||||
"ngrams": 2
|
||||
},
|
||||
"train": {
|
||||
"seed": 20170428,
|
||||
"n_est": 50,
|
||||
"min_split": 0.01
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Filtering with `stages`.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
`stages` can a single string:
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> params = dvc.api.params_show(stages="prepare")
|
||||
>>> print(json.dumps(params, indent=4))
|
||||
{
|
||||
"prepare": {
|
||||
"split": 0.2,
|
||||
"seed": 20170428
|
||||
}
|
||||
}
|
||||
|
||||
Or an iterable of strings:
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> params = dvc.api.params_show(stages=["prepare", "train"])
|
||||
>>> print(json.dumps(params, indent=4))
|
||||
{
|
||||
"prepare": {
|
||||
"split": 0.2,
|
||||
"seed": 20170428
|
||||
},
|
||||
"train": {
|
||||
"seed": 20170428,
|
||||
"n_est": 50,
|
||||
"min_split": 0.01
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Using `rev`.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> params = dvc.api.params_show(rev="tune-hyperparams")
|
||||
>>> print(json.dumps(params, indent=4))
|
||||
{
|
||||
"prepare": {
|
||||
"split": 0.2,
|
||||
"seed": 20170428
|
||||
},
|
||||
"featurize": {
|
||||
"max_features": 200,
|
||||
"ngrams": 2
|
||||
},
|
||||
"train": {
|
||||
"seed": 20170428,
|
||||
"n_est": 100,
|
||||
"min_split": 8
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Using `targets`.
|
||||
|
||||
Working on `multi-params-files` folder of
|
||||
https://github.com/iterative/pipeline-conifguration
|
||||
|
||||
You can pass a single target:
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> params = dvc.api.params_show("params.yaml")
|
||||
>>> print(json.dumps(params, indent=4))
|
||||
{
|
||||
"run_mode": "prod",
|
||||
"configs": {
|
||||
"dev": "configs/params_dev.yaml",
|
||||
"test": "configs/params_test.yaml",
|
||||
"prod": "configs/params_prod.yaml"
|
||||
},
|
||||
"evaluate": {
|
||||
"dataset": "micro",
|
||||
"size": 5000,
|
||||
"metrics": ["f1", "roc-auc"],
|
||||
"metrics_file": "reports/metrics.json",
|
||||
"plots_cm": "reports/plot_confusion_matrix.png"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Or multiple targets:
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> params = dvc.api.params_show(
|
||||
... "configs/params_dev.yaml", "configs/params_prod.yaml")
|
||||
>>> print(json.dumps(params, indent=4))
|
||||
{
|
||||
"configs/params_prod.yaml:run_mode": "prod",
|
||||
"configs/params_prod.yaml:config_file": "configs/params_prod.yaml",
|
||||
"configs/params_prod.yaml:data_load": {
|
||||
"dataset": "large",
|
||||
"sampling": {
|
||||
"enable": true,
|
||||
"size": 50000
|
||||
}
|
||||
},
|
||||
"configs/params_prod.yaml:train": {
|
||||
"epochs": 1000
|
||||
},
|
||||
"configs/params_dev.yaml:run_mode": "dev",
|
||||
"configs/params_dev.yaml:config_file": "configs/params_dev.yaml",
|
||||
"configs/params_dev.yaml:data_load": {
|
||||
"dataset": "development",
|
||||
"sampling": {
|
||||
"enable": true,
|
||||
"size": 1000
|
||||
}
|
||||
},
|
||||
"configs/params_dev.yaml:train": {
|
||||
"epochs": 10
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Git URL as `repo`.
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> params = dvc.api.params_show(
|
||||
... repo="https://github.com/iterative/demo-fashion-mnist")
|
||||
{
|
||||
"train": {
|
||||
"batch_size": 128,
|
||||
"hidden_units": 64,
|
||||
"dropout": 0.4,
|
||||
"num_epochs": 10,
|
||||
"lr": 0.001,
|
||||
"conv_activation": "relu"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.. _Git revision:
|
||||
https://git-scm.com/docs/revisions
|
||||
|
||||
"""
|
||||
if isinstance(stages, str):
|
||||
stages = [stages]
|
||||
|
||||
def _onerror_raise(result: Dict, exception: Exception, *args, **kwargs):
|
||||
raise exception
|
||||
|
||||
def _postprocess(params):
|
||||
processed = {}
|
||||
for rev, rev_data in params.items():
|
||||
processed[rev] = {}
|
||||
|
||||
counts = Counter()
|
||||
for file_data in rev_data["data"].values():
|
||||
for k in file_data["data"]:
|
||||
counts[k] += 1
|
||||
|
||||
for file_name, file_data in rev_data["data"].items():
|
||||
to_merge = {
|
||||
(k if counts[k] == 1 else f"{file_name}:{k}"): v
|
||||
for k, v in file_data["data"].items()
|
||||
}
|
||||
processed[rev] = {**processed[rev], **to_merge}
|
||||
|
||||
if "workspace" in processed:
|
||||
del processed["workspace"]
|
||||
|
||||
if not processed:
|
||||
raise DvcException("No params found")
|
||||
|
||||
return processed[first(processed)]
|
||||
|
||||
with Repo.open(repo) as _repo:
|
||||
params = _repo.params.show(
|
||||
revs=rev if rev is None else [rev],
|
||||
targets=targets,
|
||||
deps=deps,
|
||||
onerror=_onerror_raise,
|
||||
stages=stages,
|
||||
)
|
||||
|
||||
return _postprocess(params)
|
||||
+11
-2
@@ -107,7 +107,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
)
|
||||
return 251
|
||||
|
||||
from dvc_objects.cache import DiskError
|
||||
from dvc_data.hashfile.cache import DiskError
|
||||
|
||||
if isinstance(exc, DiskError):
|
||||
from dvc.utils import relpath
|
||||
@@ -121,7 +121,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
)
|
||||
return None
|
||||
|
||||
from dvc_data.stage import IgnoreInCollectedDirError
|
||||
from dvc_data.hashfile.build import IgnoreInCollectedDirError
|
||||
|
||||
if isinstance(exc, IgnoreInCollectedDirError):
|
||||
logger.exception("")
|
||||
@@ -189,6 +189,15 @@ def main(argv=None): # noqa: C901
|
||||
except KeyboardInterrupt:
|
||||
logger.exception("interrupted by the user")
|
||||
ret = 252
|
||||
except BrokenPipeError:
|
||||
import os
|
||||
|
||||
# Python flushes standard streams on exit; redirect remaining output
|
||||
# to devnull to avoid another BrokenPipeError at shutdown
|
||||
# See: https://docs.python.org/3/library/signal.html#note-on-sigpipe
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, sys.stdout.fileno())
|
||||
ret = 141 # 128 + 13 (SIGPIPE)
|
||||
except NotDvcRepoError:
|
||||
logger.exception("")
|
||||
ret = 253
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from argparse import _AppendAction
|
||||
|
||||
|
||||
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
from funcy import ldistinct
|
||||
|
||||
items = getattr(namespace, self.dest) or []
|
||||
items.extend(map(str.strip, values.split(",")))
|
||||
setattr(namespace, self.dest, ldistinct(items))
|
||||
|
||||
|
||||
class KeyValueArgs(_AppendAction):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
items = getattr(namespace, self.dest) or {}
|
||||
for value in filter(bool, values):
|
||||
key, _, value = value.partition("=")
|
||||
items[key.strip()] = value
|
||||
|
||||
setattr(namespace, self.dest, items)
|
||||
+1
-1
@@ -13,7 +13,7 @@ class CmdBase(ABC):
|
||||
|
||||
os.chdir(args.cd)
|
||||
|
||||
self.repo = Repo(uninitialized=self.UNINITIALIZED)
|
||||
self.repo: "Repo" = Repo(uninitialized=self.UNINITIALIZED)
|
||||
self.config = self.repo.config
|
||||
self.args = args
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from dvc.commands import (
|
||||
config,
|
||||
daemon,
|
||||
dag,
|
||||
data,
|
||||
data_sync,
|
||||
destroy,
|
||||
diff,
|
||||
@@ -33,6 +34,7 @@ from dvc.commands import (
|
||||
move,
|
||||
params,
|
||||
plots,
|
||||
queue,
|
||||
remote,
|
||||
remove,
|
||||
repro,
|
||||
@@ -50,6 +52,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
COMMANDS = [
|
||||
init,
|
||||
queue,
|
||||
get,
|
||||
get_url,
|
||||
destroy,
|
||||
@@ -86,6 +89,7 @@ COMMANDS = [
|
||||
experiments,
|
||||
check_ignore,
|
||||
machine,
|
||||
data,
|
||||
]
|
||||
|
||||
|
||||
|
||||
+36
-9
@@ -2,12 +2,43 @@ import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import KeyValueArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help="User description of the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--meta",
|
||||
metavar="key=value",
|
||||
nargs=1,
|
||||
action=KeyValueArgs,
|
||||
help="Custom metadata to add to the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--label",
|
||||
dest="labels",
|
||||
type=str,
|
||||
action="append",
|
||||
metavar="<str>",
|
||||
help="Label for the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
type=str,
|
||||
metavar="<str>",
|
||||
help="Type of the data.",
|
||||
)
|
||||
|
||||
|
||||
class CmdAdd(CmdBase):
|
||||
def run(self):
|
||||
from dvc.exceptions import (
|
||||
@@ -28,6 +59,9 @@ class CmdAdd(CmdBase):
|
||||
glob=self.args.glob,
|
||||
desc=self.args.desc,
|
||||
out=self.args.out,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
jobs=self.args.jobs,
|
||||
@@ -109,15 +143,8 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
)
|
||||
|
||||
_add_annotating_args(parser)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
|
||||
+20
-4
@@ -1,11 +1,15 @@
|
||||
import argparse
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from networkx import DiGraph
|
||||
|
||||
def _show_ascii(G):
|
||||
|
||||
def _show_ascii(G: "DiGraph"):
|
||||
from dvc.dagascii import draw
|
||||
from dvc.repo.graph import get_pipelines
|
||||
|
||||
@@ -18,12 +22,24 @@ def _show_ascii(G):
|
||||
return "\n".join(ret)
|
||||
|
||||
|
||||
def _show_dot(G):
|
||||
def _quote_label(node):
|
||||
label = str(node)
|
||||
# Node names should not contain ":" unless they are quoted with "".
|
||||
# See: https://github.com/pydot/pydot/issues/258.
|
||||
if label[0] != '"' and label[-1] != '"':
|
||||
return f'"{label}"'
|
||||
return label
|
||||
|
||||
|
||||
def _show_dot(G: "DiGraph"):
|
||||
import io
|
||||
|
||||
import networkx as nx
|
||||
from networkx.drawing.nx_pydot import write_dot
|
||||
|
||||
dot_file = io.StringIO()
|
||||
|
||||
nx.relabel_nodes(G, _quote_label, copy=False)
|
||||
write_dot(G.reverse(), dot_file)
|
||||
return dot_file.getvalue()
|
||||
|
||||
@@ -70,9 +86,9 @@ def _collect_targets(repo, target, outs):
|
||||
targets.extend([str(out) for out in stage.outs])
|
||||
continue
|
||||
|
||||
for out in outs_trie.itervalues(
|
||||
for out in outs_trie.itervalues( # noqa: B301
|
||||
prefix=repo.fs.path.parts(path)
|
||||
): # noqa: B301
|
||||
):
|
||||
targets.extend(str(out))
|
||||
|
||||
return targets
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
import argparse
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
|
||||
|
||||
from funcy import chunks, compact, log_durations
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import CommaSeparatedArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo.data import Status as DataStatus
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdDataStatus(CmdBase):
|
||||
COLORS = {
|
||||
"not_in_cache": "red",
|
||||
"committed": "green",
|
||||
"uncommitted": "yellow",
|
||||
"untracked": "cyan",
|
||||
}
|
||||
LABELS = {
|
||||
"not_in_cache": "Not in cache",
|
||||
"committed": "DVC committed changes",
|
||||
"uncommitted": "DVC uncommitted changes",
|
||||
"untracked": "Untracked files",
|
||||
"unchanged": "DVC unchanged files",
|
||||
}
|
||||
HINTS = {
|
||||
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
|
||||
"committed": (
|
||||
"git commit the corresponding dvc files to update the repo",
|
||||
),
|
||||
"uncommitted": (
|
||||
'use "dvc commit <file>..." to track changes',
|
||||
'use "dvc checkout <file>..." to discard changes',
|
||||
),
|
||||
"untracked": (
|
||||
'use "git add <file> ..." or '
|
||||
'"dvc add <file>..." to commit to git or to dvc',
|
||||
),
|
||||
"git_dirty": (
|
||||
"there are {}changes not tracked by dvc, "
|
||||
'use "git status" to see',
|
||||
),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _process_status(status: "DataStatus"):
|
||||
"""Flatten stage status, and filter empty stage status contents."""
|
||||
for stage, stage_status in status.items():
|
||||
items = stage_status
|
||||
if isinstance(stage_status, dict):
|
||||
items = {
|
||||
file: state
|
||||
for state, files in stage_status.items()
|
||||
for file in files
|
||||
}
|
||||
if not items:
|
||||
continue
|
||||
yield stage, items
|
||||
|
||||
@classmethod
|
||||
def _show_status(cls, status: "DataStatus") -> int:
|
||||
git_info = status.pop("git") # type: ignore[misc]
|
||||
result = dict(cls._process_status(status))
|
||||
if not result:
|
||||
no_changes = "No changes"
|
||||
if git_info.get("is_empty", False):
|
||||
no_changes += " in an empty git repo"
|
||||
ui.write(f"{no_changes}.")
|
||||
|
||||
for idx, (stage, stage_status) in enumerate(result.items()):
|
||||
if idx:
|
||||
ui.write()
|
||||
|
||||
label = cls.LABELS.get(stage, stage.capitalize() + " files")
|
||||
header = f"{label}:"
|
||||
color = cls.COLORS.get(stage, None)
|
||||
|
||||
ui.write(header)
|
||||
if hints := cls.HINTS.get(stage):
|
||||
for hint in hints:
|
||||
ui.write(f" ({hint})")
|
||||
|
||||
if isinstance(stage_status, dict):
|
||||
items = [
|
||||
": ".join([state, file])
|
||||
for file, state in stage_status.items()
|
||||
]
|
||||
else:
|
||||
items = stage_status
|
||||
|
||||
tabs = "\t".expandtabs(8)
|
||||
for chunk in chunks(1000, items):
|
||||
out = "\n".join(tabs + item for item in chunk)
|
||||
ui.write(colorize(out, color))
|
||||
|
||||
if (hints := cls.HINTS.get("git_dirty")) and git_info.get("is_dirty"):
|
||||
for hint in hints:
|
||||
message = hint.format("other " if result else "")
|
||||
ui.write(f"[blue]({message})[/]", styled=True)
|
||||
return 0
|
||||
|
||||
def run(self) -> int:
|
||||
with log_durations(logger.trace, "in data_status"): # type: ignore
|
||||
status = self.repo.data_status(
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
)
|
||||
|
||||
if not self.args.unchanged:
|
||||
status.pop("unchanged") # type: ignore[misc]
|
||||
if self.args.untracked_files == "no":
|
||||
status.pop("untracked")
|
||||
if self.args.json:
|
||||
status.pop("git") # type: ignore[misc]
|
||||
ui.write_json(compact(status))
|
||||
return 0
|
||||
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",
|
||||
parents=[parent_parser],
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
data_subparsers = data_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use `dvc data CMD --help` to display command-specific help.",
|
||||
)
|
||||
fix_subparsers(data_subparsers)
|
||||
|
||||
DATA_STATUS_HELP = (
|
||||
"Show changes between the last git commit, "
|
||||
"the dvcfiles and the workspace."
|
||||
)
|
||||
data_status_parser = data_subparsers.add_parser(
|
||||
"status",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DATA_STATUS_HELP, "data/status"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=DATA_STATUS_HELP,
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
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",
|
||||
default=False,
|
||||
help="Show granular file-level info for DVC-tracked directories.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--unchanged",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show unmodified DVC-tracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--untracked-files",
|
||||
choices=["no", "all"],
|
||||
default="no",
|
||||
const="all",
|
||||
nargs="?",
|
||||
help="Show untracked files.",
|
||||
)
|
||||
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,
|
||||
help=DATA_LS_HELP,
|
||||
)
|
||||
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)
|
||||
@@ -43,7 +43,8 @@ class CmdDataPull(CmdDataBase):
|
||||
)
|
||||
self.log_summary(stats)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
self.log_summary(getattr(exc, "stats", {}))
|
||||
if stats := getattr(exc, "stats", {}):
|
||||
self.log_summary(stats)
|
||||
logger.exception("failed to pull data from the cloud")
|
||||
return 1
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from dvc.commands.experiments import (
|
||||
ls,
|
||||
pull,
|
||||
push,
|
||||
queue_worker,
|
||||
remove,
|
||||
run,
|
||||
show,
|
||||
@@ -30,6 +31,7 @@ SUB_COMMANDS = [
|
||||
ls,
|
||||
pull,
|
||||
push,
|
||||
queue_worker,
|
||||
remove,
|
||||
run,
|
||||
show,
|
||||
|
||||
@@ -29,6 +29,6 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
"experiment", help="Experiment to be promoted."
|
||||
)
|
||||
experiments_branch_parser.add_argument(
|
||||
"branch", help="Git branch name to use."
|
||||
"branch", help="Name for the new Git branch"
|
||||
)
|
||||
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
|
||||
|
||||
@@ -27,7 +27,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
elif diff:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
precision = self.args.precision or DEFAULT_PRECISION
|
||||
|
||||
@@ -12,8 +12,8 @@ logger = logging.getLogger(__name__)
|
||||
def _raise_error_if_all_disabled(**kwargs):
|
||||
if not any(kwargs.values()):
|
||||
raise InvalidArgumentError(
|
||||
"Either of `-w|--workspace`, `-a|--all-branches`, `-T|--all-tags` "
|
||||
"or `--all-commits` needs to be set."
|
||||
"Either of `-w|--workspace`, `-a|--all-branches`, `-T|--all-tags`,"
|
||||
" `--all-commits` or `--date` needs to be set."
|
||||
)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class CmdExperimentsGC(CmdBase):
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -31,13 +32,21 @@ class CmdExperimentsGC(CmdBase):
|
||||
msg += "the workspace"
|
||||
if self.args.all_commits:
|
||||
msg += " and all git commits"
|
||||
elif self.args.all_branches and self.args.all_tags:
|
||||
msg += " and all git branches and tags"
|
||||
elif self.args.all_branches:
|
||||
msg += " and all git branches"
|
||||
elif self.args.all_tags:
|
||||
msg += " and all git tags"
|
||||
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 += (
|
||||
" and all git commits before date "
|
||||
f"{self.args.commit_date}"
|
||||
)
|
||||
msg += " of the current repo."
|
||||
|
||||
if self.args.queued:
|
||||
msg += " Run queued experiments will be preserved."
|
||||
else:
|
||||
@@ -54,6 +63,7 @@ class CmdExperimentsGC(CmdBase):
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -110,6 +120,17 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
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",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdQueueWorker(CmdBase):
|
||||
"""Run the exp queue worker."""
|
||||
|
||||
def run(self):
|
||||
self.repo.experiments.celery_queue.worker.start(self.args.name)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
QUEUE_WORKER_HELP = "Run the exp queue worker."
|
||||
parser = experiments_subparsers.add_parser(
|
||||
"queue-worker",
|
||||
parents=[parent_parser],
|
||||
description=QUEUE_WORKER_HELP,
|
||||
add_help=False,
|
||||
)
|
||||
parser.add_argument("name", help="Celery worker name.")
|
||||
parser.set_defaults(func=CmdQueueWorker)
|
||||
@@ -10,23 +10,22 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsRemove(CmdBase):
|
||||
def raise_error_if_all_disabled(self):
|
||||
def check_arguments(self):
|
||||
if not any(
|
||||
[
|
||||
self.args.experiment,
|
||||
self.args.all_commits,
|
||||
self.args.rev,
|
||||
self.args.queue,
|
||||
]
|
||||
):
|
||||
) ^ bool(self.args.experiment):
|
||||
raise InvalidArgumentError(
|
||||
"Either provide an `experiment` argument, or use the "
|
||||
"`--rev` or `--all-commits` flag."
|
||||
"`--rev` or `--all-commits` or `--queue` flag."
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
self.raise_error_if_all_disabled()
|
||||
self.check_arguments()
|
||||
|
||||
removed_list = self.repo.experiments.remove(
|
||||
exp_names=self.args.experiment,
|
||||
|
||||
@@ -138,7 +138,7 @@ def _collect_rows(
|
||||
row_dict["Experiment"] = exp.get("name", "")
|
||||
row_dict["rev"] = name_rev
|
||||
row_dict["typ"] = typ
|
||||
row_dict["Created"] = _format_time(
|
||||
row_dict["Created"] = format_time(
|
||||
exp.get("timestamp"), fill_value, iso
|
||||
)
|
||||
row_dict["parent"] = parent
|
||||
@@ -218,7 +218,7 @@ def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
|
||||
return ret
|
||||
|
||||
|
||||
def _format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
|
||||
def format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
|
||||
if datetime_obj is None:
|
||||
return fill_value
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class CmdFreezeBase(CmdBase):
|
||||
try:
|
||||
func(target)
|
||||
except DvcException:
|
||||
logger.exception(f"failed to {name} '{target}'")
|
||||
logger.exception("failed to %s '%s'", name, target)
|
||||
ret = 1
|
||||
return ret
|
||||
|
||||
|
||||
+62
-10
@@ -11,29 +11,46 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class CmdGC(CmdBase):
|
||||
def run(self):
|
||||
from dvc.repo.gc import _raise_error_if_all_disabled
|
||||
from dvc.repo.gc import _validate_args
|
||||
|
||||
_raise_error_if_all_disabled(
|
||||
_validate_args(
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
all_experiments=self.args.all_experiments,
|
||||
commit_date=self.args.commit_date,
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
)
|
||||
|
||||
if self.args.rev:
|
||||
self.args.num = self.args.num or 1
|
||||
|
||||
msg = "This will remove all cache except items used in "
|
||||
|
||||
msg += "the workspace"
|
||||
if self.args.all_commits:
|
||||
msg += " and all git commits"
|
||||
elif self.args.all_branches and self.args.all_tags:
|
||||
msg += " and all git branches and tags"
|
||||
elif self.args.all_branches:
|
||||
msg += " and all git branches"
|
||||
elif self.args.all_tags:
|
||||
msg += " and all git tags"
|
||||
elif self.args.all_experiments:
|
||||
msg += " and all experiments"
|
||||
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 += (
|
||||
" and all git commits before date "
|
||||
f"{self.args.commit_date}"
|
||||
)
|
||||
if self.args.rev:
|
||||
msg += (
|
||||
f" and last {self.args.num} commits from {self.args.rev}"
|
||||
)
|
||||
|
||||
if self.args.all_experiments:
|
||||
msg += " and all experiments"
|
||||
if self.args.repos:
|
||||
msg += " of the current and the following repos:"
|
||||
|
||||
@@ -53,12 +70,15 @@ class CmdGC(CmdBase):
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
all_experiments=self.args.all_experiments,
|
||||
commit_date=self.args.commit_date,
|
||||
cloud=self.args.cloud,
|
||||
remote=self.args.remote,
|
||||
force=self.args.force,
|
||||
jobs=self.args.jobs,
|
||||
repos=self.args.repos,
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -83,6 +103,26 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Keep data files used in the current workspace.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"--rev",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Keep data files used in the specified <commit>.",
|
||||
metavar="<commit>",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-n",
|
||||
"--num",
|
||||
type=int,
|
||||
dest="num",
|
||||
metavar="<num>",
|
||||
help=(
|
||||
"Keep data files used in the last `num` commits "
|
||||
"starting from the `--rev` <commit>. "
|
||||
"Only used if `--rev` is also provided. "
|
||||
"Defaults to `1`."
|
||||
),
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
@@ -104,6 +144,18 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Keep data files for all Git commits.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"--date",
|
||||
type=str,
|
||||
dest="commit_date",
|
||||
metavar="<YYYY-MM-DD>",
|
||||
default=None,
|
||||
help=(
|
||||
"Keep cached data referenced in the commits after ( inclusive )"
|
||||
" a certain time. Date must match the extended ISO 8601 format "
|
||||
"(YYYY-MM-DD)."
|
||||
),
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"--all-experiments",
|
||||
action="store_true",
|
||||
|
||||
@@ -18,7 +18,7 @@ class CmdGetUrl(CmdBaseNoRepo):
|
||||
Repo.get_url(self.args.url, out=self.args.out, jobs=self.args.jobs)
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception(f"failed to get '{self.args.url}'")
|
||||
logger.exception("failed to get '%s'", self.args.url)
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
+17
-10
@@ -19,7 +19,11 @@ class CmdImport(CmdBase):
|
||||
fname=self.args.file,
|
||||
rev=self.args.rev,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -33,6 +37,8 @@ class CmdImport(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download file or directory tracked by DVC or by Git "
|
||||
"into the workspace, and track it."
|
||||
@@ -69,20 +75,19 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create .dvc file without actually downloading it.",
|
||||
help="Only create .dvc file without actually importing target data.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Create .dvc file including target data hash value(s)"
|
||||
" but do not actually download the file(s).",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-j",
|
||||
@@ -94,4 +99,6 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImport)
|
||||
|
||||
+17
-11
@@ -17,9 +17,13 @@ class CmdImportUrl(CmdBase):
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -34,6 +38,8 @@ class CmdImportUrl(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download or copy file from URL and take it under DVC control."
|
||||
)
|
||||
@@ -66,11 +72,19 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create .dvc file without actually downloading it.",
|
||||
help="Only create .dvc file without actually importing target data.",
|
||||
)
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Create .dvc file including target data hash value(s)"
|
||||
" but do not actually download the file(s).",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--to-remote",
|
||||
@@ -94,13 +108,5 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help=(
|
||||
"User description of the data (optional). "
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
)
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.set_defaults(func=CmdImportUrl)
|
||||
|
||||
@@ -42,7 +42,7 @@ class CmdList(CmdBaseNoRepo):
|
||||
ui.write("\n".join(entries))
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception(f"failed to list '{self.args.url}'")
|
||||
logger.exception("failed to list '%s'", self.args.url)
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
+144
-75
@@ -24,6 +24,60 @@ def _show_json(renderers, split=False):
|
||||
ui.write_json(result)
|
||||
|
||||
|
||||
def _adjust_vega_renderers(renderers):
|
||||
from dvc.render import VERSION_FIELD
|
||||
from dvc_render import VegaRenderer
|
||||
|
||||
for r in renderers:
|
||||
if isinstance(r, VegaRenderer):
|
||||
if _data_versions_count(r) > 1:
|
||||
summary = _summarize_version_infos(r)
|
||||
for dp in r.datapoints:
|
||||
vi = dp.pop(VERSION_FIELD, {})
|
||||
keys = list(vi.keys())
|
||||
for key in keys:
|
||||
if not (len(summary.get(key, set())) > 1):
|
||||
vi.pop(key)
|
||||
if vi:
|
||||
dp["rev"] = "::".join(vi.values())
|
||||
else:
|
||||
for dp in r.datapoints:
|
||||
dp.pop(VERSION_FIELD, {})
|
||||
|
||||
|
||||
def _summarize_version_infos(renderer):
|
||||
from collections import defaultdict
|
||||
|
||||
from dvc.render import VERSION_FIELD
|
||||
|
||||
result = defaultdict(set)
|
||||
|
||||
for dp in renderer.datapoints:
|
||||
for key, value in dp.get(VERSION_FIELD, {}).items():
|
||||
result[key].add(value)
|
||||
return dict(result)
|
||||
|
||||
|
||||
def _data_versions_count(renderer):
|
||||
from itertools import product
|
||||
|
||||
summary = _summarize_version_infos(renderer)
|
||||
x = product(summary.get("filename", {None}), summary.get("field", {None}))
|
||||
return len(set(x))
|
||||
|
||||
|
||||
def _filter_unhandled_renderers(renderers):
|
||||
# filtering out renderers currently unhandled by vscode extension
|
||||
from dvc_render import VegaRenderer
|
||||
|
||||
def _is_json_viable(r):
|
||||
return not (
|
||||
isinstance(r, VegaRenderer) and _data_versions_count(r) > 1
|
||||
)
|
||||
|
||||
return list(filter(_is_json_viable, renderers))
|
||||
|
||||
|
||||
class CmdPlots(CmdBase):
|
||||
def _func(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
@@ -35,10 +89,28 @@ 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):
|
||||
config_files = None
|
||||
if self.args.from_config:
|
||||
config_files = {self.args.from_config}
|
||||
return config_files
|
||||
|
||||
def _html_template_path(self):
|
||||
html_template_path = self.args.html_template
|
||||
if not html_template_path:
|
||||
html_template_path = self.repo.config.get("plots", {}).get(
|
||||
"html_template", None
|
||||
)
|
||||
if html_template_path and not os.path.isabs(html_template_path):
|
||||
html_template_path = os.path.join(
|
||||
self.repo.dvc_dir, html_template_path
|
||||
)
|
||||
return html_template_path
|
||||
|
||||
def run(self):
|
||||
from pathlib import Path
|
||||
|
||||
from dvc.render.match import match_renderers
|
||||
from dvc.render.match import match_defs_renderers
|
||||
from dvc_render import render_html
|
||||
|
||||
if self.args.show_vega:
|
||||
@@ -58,9 +130,10 @@ class CmdPlots(CmdBase):
|
||||
return 1
|
||||
|
||||
try:
|
||||
|
||||
plots_data = self._func(
|
||||
targets=self.args.targets, props=self._props()
|
||||
targets=self.args.targets,
|
||||
props=self._props(),
|
||||
config_files=self._config_files(),
|
||||
)
|
||||
|
||||
if not plots_data:
|
||||
@@ -69,56 +142,51 @@ class CmdPlots(CmdBase):
|
||||
"visualization file will not be created."
|
||||
)
|
||||
|
||||
out: str = self.args.out or "dvc_plots"
|
||||
out: str = self.args.out or self.repo.config.get("plots", {}).get(
|
||||
"out_dir", "dvc_plots"
|
||||
)
|
||||
|
||||
renderers_out = (
|
||||
out if self.args.json else os.path.join(out, "static")
|
||||
)
|
||||
renderers = match_renderers(
|
||||
plots_data=plots_data,
|
||||
|
||||
renderers = match_defs_renderers(
|
||||
data=plots_data,
|
||||
out=renderers_out,
|
||||
templates_dir=self.repo.plots.templates_dir,
|
||||
)
|
||||
if self.args.json:
|
||||
renderers = _filter_unhandled_renderers(renderers)
|
||||
_show_json(renderers, self.args.split)
|
||||
return 0
|
||||
|
||||
_adjust_vega_renderers(renderers)
|
||||
|
||||
if self.args.show_vega:
|
||||
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
|
||||
if renderer:
|
||||
ui.write_json(json.loads(renderer.get_filled_template()))
|
||||
return 0
|
||||
if self.args.json:
|
||||
_show_json(renderers, self.args.split)
|
||||
return 0
|
||||
|
||||
html_template_path = self.args.html_template
|
||||
if not html_template_path:
|
||||
html_template_path = self.repo.config.get("plots", {}).get(
|
||||
"html_template", None
|
||||
)
|
||||
if html_template_path and not os.path.isabs(
|
||||
html_template_path
|
||||
):
|
||||
html_template_path = os.path.join(
|
||||
self.repo.dvc_dir, html_template_path
|
||||
)
|
||||
|
||||
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
|
||||
|
||||
render_html(
|
||||
renderers=renderers,
|
||||
output_file=output_file,
|
||||
template_path=html_template_path,
|
||||
)
|
||||
if renderers:
|
||||
render_html(
|
||||
renderers=renderers,
|
||||
output_file=output_file,
|
||||
template_path=self._html_template_path(),
|
||||
)
|
||||
|
||||
ui.write(output_file.as_uri())
|
||||
auto_open = self.repo.config["plots"].get("auto_open", False)
|
||||
if self.args.open or auto_open:
|
||||
if not auto_open:
|
||||
ui.write(
|
||||
"To enable auto opening, you can run:\n"
|
||||
"\n"
|
||||
"\tdvc config plots.auto_open true"
|
||||
)
|
||||
return ui.open_browser(output_file)
|
||||
ui.write(output_file.as_uri())
|
||||
auto_open = self.repo.config["plots"].get("auto_open", False)
|
||||
if self.args.open or auto_open:
|
||||
if not auto_open:
|
||||
ui.write(
|
||||
"To enable auto opening, you can run:\n"
|
||||
"\n"
|
||||
"\tdvc config plots.auto_open true"
|
||||
)
|
||||
return ui.open_browser(output_file)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -155,29 +223,22 @@ class CmdPlotsModify(CmdPlots):
|
||||
|
||||
|
||||
class CmdPlotsTemplates(CmdBase):
|
||||
TEMPLATES_CHOICES = [
|
||||
"simple",
|
||||
"linear",
|
||||
"confusion",
|
||||
"confusion_normalized",
|
||||
"scatter",
|
||||
"smooth",
|
||||
]
|
||||
|
||||
def run(self):
|
||||
from dvc_render.vega_templates import dump_templates
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc_render.vega_templates import TEMPLATES
|
||||
|
||||
try:
|
||||
out = (
|
||||
os.path.join(os.getcwd(), self.args.out)
|
||||
if self.args.out
|
||||
else self.repo.plots.templates_dir
|
||||
)
|
||||
target = self.args.template
|
||||
if target:
|
||||
for template in TEMPLATES:
|
||||
if target == template.DEFAULT_NAME:
|
||||
ui.write_json(template.DEFAULT_CONTENT)
|
||||
return 0
|
||||
raise InvalidArgumentError(f"Unexpected template: {target}.")
|
||||
|
||||
targets = [self.args.target] if self.args.target else None
|
||||
dump_templates(output=out, targets=targets)
|
||||
templates_path = os.path.relpath(out, os.getcwd())
|
||||
ui.write(f"Templates have been written into '{templates_path}'.")
|
||||
else:
|
||||
for template in TEMPLATES:
|
||||
ui.write(template.DEFAULT_NAME)
|
||||
|
||||
return 0
|
||||
except DvcException:
|
||||
@@ -186,10 +247,7 @@ class CmdPlotsTemplates(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
PLOTS_HELP = (
|
||||
"Commands to visualize and compare plot metrics in structured files "
|
||||
"(JSON, YAML, CSV, TSV)."
|
||||
)
|
||||
PLOTS_HELP = "Commands to visualize and compare plot data."
|
||||
|
||||
plots_parser = subparsers.add_parser(
|
||||
"plots",
|
||||
@@ -205,7 +263,10 @@ def add_parser(subparsers, parent_parser):
|
||||
|
||||
fix_subparsers(plots_subparsers)
|
||||
|
||||
SHOW_HELP = "Generate plots from metrics files."
|
||||
SHOW_HELP = (
|
||||
"Generate plots from target files or from `plots`"
|
||||
" definitions in `dvc.yaml`."
|
||||
)
|
||||
plots_show_parser = plots_subparsers.add_parser(
|
||||
"show",
|
||||
parents=[parent_parser],
|
||||
@@ -216,9 +277,10 @@ def add_parser(subparsers, parent_parser):
|
||||
plots_show_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Files to visualize (supports any file, "
|
||||
"even when not found as `plots` in `dvc.yaml`). "
|
||||
"Shows all plots by default.",
|
||||
help=(
|
||||
"Plots files or plot IDs from `dvc.yaml` to visualize. "
|
||||
"Shows all plots by default."
|
||||
),
|
||||
).complete = completion.FILE
|
||||
_add_props_arguments(plots_show_parser)
|
||||
_add_output_argument(plots_show_parser)
|
||||
@@ -226,8 +288,8 @@ def add_parser(subparsers, parent_parser):
|
||||
plots_show_parser.set_defaults(func=CmdPlotsShow)
|
||||
|
||||
PLOTS_DIFF_HELP = (
|
||||
"Show multiple versions of plot metrics "
|
||||
"by plotting them in a single image."
|
||||
"Show multiple versions of a plot by overlaying them "
|
||||
"in a single image."
|
||||
)
|
||||
plots_diff_parser = plots_subparsers.add_parser(
|
||||
"diff",
|
||||
@@ -240,8 +302,8 @@ def add_parser(subparsers, parent_parser):
|
||||
"--targets",
|
||||
nargs="*",
|
||||
help=(
|
||||
"Specific plots file(s) to visualize "
|
||||
"(even if not found as `plots` in `dvc.yaml`). "
|
||||
"Specific plots to visualize. "
|
||||
"Accepts any file path or plot name from `dvc.yaml` file. "
|
||||
"Shows all tracked plots by default."
|
||||
),
|
||||
metavar="<paths>",
|
||||
@@ -263,7 +325,7 @@ def add_parser(subparsers, parent_parser):
|
||||
|
||||
PLOTS_MODIFY_HELP = (
|
||||
"Modify display properties of data-series plots "
|
||||
"(has no effect on image-type plots)."
|
||||
"defined in stages (has no effect on image plots)."
|
||||
)
|
||||
plots_modify_parser = plots_subparsers.add_parser(
|
||||
"modify",
|
||||
@@ -273,7 +335,8 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_modify_parser.add_argument(
|
||||
"target", help="Metric file to set properties to"
|
||||
"target",
|
||||
help="Plots file to set properties for (defined at the stage level).",
|
||||
).complete = completion.FILE
|
||||
_add_props_arguments(plots_modify_parser)
|
||||
plots_modify_parser.add_argument(
|
||||
@@ -285,8 +348,7 @@ def add_parser(subparsers, parent_parser):
|
||||
plots_modify_parser.set_defaults(func=CmdPlotsModify)
|
||||
|
||||
TEMPLATES_HELP = (
|
||||
"Write built-in plots templates to a directory "
|
||||
"(.dvc/plots by default)."
|
||||
"List built-in plots templates or show JSON specification for one."
|
||||
)
|
||||
plots_templates_parser = plots_subparsers.add_parser(
|
||||
"templates",
|
||||
@@ -296,13 +358,14 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_templates_parser.add_argument(
|
||||
"target",
|
||||
"template",
|
||||
default=None,
|
||||
nargs="?",
|
||||
choices=CmdPlotsTemplates.TEMPLATES_CHOICES,
|
||||
help="Template to write. Writes all templates by default.",
|
||||
help=(
|
||||
"Template for which to show JSON specification. "
|
||||
"List all template names by default."
|
||||
),
|
||||
)
|
||||
_add_output_argument(plots_templates_parser, typ="templates")
|
||||
plots_templates_parser.set_defaults(func=CmdPlotsTemplates)
|
||||
|
||||
|
||||
@@ -383,3 +446,9 @@ 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,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import argparse
|
||||
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.commands.queue import kill, logs, remove, start, status, stop
|
||||
|
||||
SUB_COMMANDS = [
|
||||
start,
|
||||
stop,
|
||||
status,
|
||||
logs,
|
||||
remove,
|
||||
kill,
|
||||
]
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
QUEUE_HELP = "Commands to manage experiments queue."
|
||||
|
||||
queue_parser = subparsers.add_parser(
|
||||
"queue",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(QUEUE_HELP, "queue"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=QUEUE_HELP,
|
||||
)
|
||||
|
||||
queue_subparsers = queue_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use `dvc queue CMD --help` to display " "command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(queue_subparsers)
|
||||
for cmd in SUB_COMMANDS:
|
||||
cmd.add_parser(queue_subparsers, parent_parser)
|
||||
@@ -0,0 +1,34 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdQueueKill(CmdBase):
|
||||
"""Kill exp task in queue."""
|
||||
|
||||
def run(self):
|
||||
self.repo.experiments.celery_queue.kill(revs=self.args.task)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(queue_subparsers, parent_parser):
|
||||
QUEUE_KILL_HELP = "Kill actively running experiment queue tasks."
|
||||
queue_kill_parser = queue_subparsers.add_parser(
|
||||
"kill",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(QUEUE_KILL_HELP, "queue/kill"),
|
||||
help=QUEUE_KILL_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
queue_kill_parser.add_argument(
|
||||
"task",
|
||||
nargs="*",
|
||||
help="Tasks in queue to kill.",
|
||||
metavar="<task>",
|
||||
)
|
||||
queue_kill_parser.set_defaults(func=CmdQueueKill)
|
||||
@@ -0,0 +1,56 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdQueueLogs(CmdBase):
|
||||
"""Show output logs for a queued experiment."""
|
||||
|
||||
def run(self):
|
||||
self.repo.experiments.celery_queue.logs(
|
||||
rev=self.args.task,
|
||||
encoding=self.args.encoding,
|
||||
follow=self.args.follow,
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(queue_subparsers, parent_parser):
|
||||
QUEUE_LOGS_HELP = (
|
||||
"Show output logs for running and completed experiment queue tasks."
|
||||
)
|
||||
queue_logs_parser = queue_subparsers.add_parser(
|
||||
"logs",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(QUEUE_LOGS_HELP, "queue/logs"),
|
||||
help=QUEUE_LOGS_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
queue_logs_parser.add_argument(
|
||||
"-e",
|
||||
"--encoding",
|
||||
help=(
|
||||
"Text encoding for log output. Defaults to system locale encoding."
|
||||
),
|
||||
metavar="<encoding>",
|
||||
)
|
||||
queue_logs_parser.add_argument(
|
||||
"-f",
|
||||
"--follow",
|
||||
help=(
|
||||
"Attach to task and follow additional live output. Only "
|
||||
"applicable if the task is still running."
|
||||
),
|
||||
action="store_true",
|
||||
)
|
||||
queue_logs_parser.add_argument(
|
||||
"task",
|
||||
help="Task to show.",
|
||||
metavar="<task>",
|
||||
)
|
||||
queue_logs_parser.set_defaults(func=CmdQueueLogs)
|
||||
@@ -0,0 +1,95 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdQueueRemove(CmdBase):
|
||||
"""Remove exp in queue."""
|
||||
|
||||
def check_arguments(self):
|
||||
clear_flag = any(
|
||||
[
|
||||
self.args.all,
|
||||
self.args.queued,
|
||||
self.args.failed,
|
||||
self.args.success,
|
||||
]
|
||||
)
|
||||
if not (clear_flag ^ bool(self.args.task)):
|
||||
raise InvalidArgumentError(
|
||||
"Either provide an `tasks` argument, or use the "
|
||||
"`--all`, `--queued`, `--failed`, `--success` flag."
|
||||
)
|
||||
|
||||
def run(self):
|
||||
|
||||
self.check_arguments()
|
||||
|
||||
if self.args.all:
|
||||
self.args.queued = True
|
||||
self.args.failed = True
|
||||
self.args.success = True
|
||||
|
||||
if self.args.queued or self.args.failed or self.args.success:
|
||||
removed_list = self.repo.experiments.celery_queue.clear(
|
||||
success=self.args.success,
|
||||
queued=self.args.queued,
|
||||
failed=self.args.failed,
|
||||
)
|
||||
else:
|
||||
removed_list = self.repo.experiments.celery_queue.remove(
|
||||
revs=self.args.task,
|
||||
)
|
||||
|
||||
if removed_list:
|
||||
removed = ", ".join(removed_list)
|
||||
ui.write(f"Removed tasks in queue: {removed}")
|
||||
else:
|
||||
ui.write(f"No tasks found named {self.args.task}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(queue_subparsers, parent_parser):
|
||||
|
||||
QUEUE_REMOVE_HELP = "Remove queued and completed tasks from the queue."
|
||||
queue_remove_parser = queue_subparsers.add_parser(
|
||||
"remove",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(QUEUE_REMOVE_HELP, "queue/remove"),
|
||||
help=QUEUE_REMOVE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
queue_remove_parser.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
help="Remove all queued and completed tasks from the queue.",
|
||||
)
|
||||
queue_remove_parser.add_argument(
|
||||
"--queued",
|
||||
action="store_true",
|
||||
help="Remove all queued tasks from the queue.",
|
||||
)
|
||||
queue_remove_parser.add_argument(
|
||||
"--success",
|
||||
action="store_true",
|
||||
help="Remove all successful tasks from the queue.",
|
||||
)
|
||||
queue_remove_parser.add_argument(
|
||||
"--failed",
|
||||
action="store_true",
|
||||
help="Remove all failed tasks from the queue.",
|
||||
)
|
||||
queue_remove_parser.add_argument(
|
||||
"task",
|
||||
nargs="*",
|
||||
help="Tasks to remove.",
|
||||
metavar="<task>",
|
||||
)
|
||||
queue_remove_parser.set_defaults(func=CmdQueueRemove)
|
||||
@@ -0,0 +1,48 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdQueueStart(CmdBase):
|
||||
"""Start exp queue workers."""
|
||||
|
||||
def run(self):
|
||||
started = self.repo.experiments.celery_queue.start_workers(
|
||||
self.args.jobs
|
||||
)
|
||||
|
||||
suffix = "s" if started > 1 else ""
|
||||
ui.write(
|
||||
f"Started '{started}' new experiments task queue worker{suffix}."
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(queue_subparsers, parent_parser):
|
||||
|
||||
QUEUE_START_HELP = "Start the experiments task queue worker."
|
||||
queue_start_parser = queue_subparsers.add_parser(
|
||||
"start",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(QUEUE_START_HELP, "queue/start"),
|
||||
help=QUEUE_START_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
queue_start_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
type=int,
|
||||
default=1,
|
||||
help=(
|
||||
"Maximum number of concurrent queue workers to start. "
|
||||
"Defaults to 1."
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
queue_start_parser.set_defaults(func=CmdQueueStart)
|
||||
@@ -0,0 +1,62 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import List, Mapping, Optional
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.compare import TabularData
|
||||
from dvc.ui import ui
|
||||
|
||||
from ..experiments.show import format_time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdQueueStatus(CmdBase):
|
||||
"""Show queue task and worker status."""
|
||||
|
||||
def run(self):
|
||||
result: List[
|
||||
Mapping[str, Optional[str]]
|
||||
] = self.repo.experiments.celery_queue.status()
|
||||
if result:
|
||||
all_headers = ["Task", "Name", "Created", "Status"]
|
||||
td = TabularData(all_headers)
|
||||
for exp in result:
|
||||
created = format_time(exp.get("timestamp"))
|
||||
td.append(
|
||||
[
|
||||
exp["rev"][:7],
|
||||
exp.get("name", ""),
|
||||
created,
|
||||
exp["status"],
|
||||
]
|
||||
)
|
||||
td.render()
|
||||
else:
|
||||
ui.write("No experiment tasks in the queue.")
|
||||
ui.write()
|
||||
|
||||
worker_status = self.repo.experiments.celery_queue.worker_status()
|
||||
active_count = len(
|
||||
[name for name, task in worker_status.items() if task]
|
||||
)
|
||||
idle_count = len(worker_status) - active_count
|
||||
|
||||
ui.write(f"Worker status: {active_count} active, {idle_count} idle")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(queue_subparsers, parent_parser):
|
||||
QUEUE_STATUS_HELP = (
|
||||
"Show the status of experiments queue tasks and workers."
|
||||
)
|
||||
queue_status_parser = queue_subparsers.add_parser(
|
||||
"status",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(QUEUE_STATUS_HELP, "queue/status"),
|
||||
help=QUEUE_STATUS_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
queue_status_parser.set_defaults(func=CmdQueueStatus)
|
||||
@@ -0,0 +1,43 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdQueueStop(CmdBase):
|
||||
"""Stop exp queue workers."""
|
||||
|
||||
def run(self):
|
||||
self.repo.experiments.celery_queue.shutdown(kill=self.args.kill)
|
||||
|
||||
if self.args.kill:
|
||||
ui.write(
|
||||
"All running tasks in the queue have been killed."
|
||||
"Queue workers are stopping."
|
||||
)
|
||||
else:
|
||||
ui.write("Queue workers will stop after running tasks finish.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(queue_subparsers, parent_parser):
|
||||
|
||||
QUEUE_STOP_HELP = "Stop all experiments task queue workers."
|
||||
queue_stop_parser = queue_subparsers.add_parser(
|
||||
"stop",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(QUEUE_STOP_HELP, "queue/stop"),
|
||||
help=QUEUE_STOP_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
queue_stop_parser.add_argument(
|
||||
"--kill",
|
||||
action="store_true",
|
||||
help="Kill all running tasks before stopping the queue workers.",
|
||||
)
|
||||
queue_stop_parser.set_defaults(func=CmdQueueStop)
|
||||
@@ -16,9 +16,7 @@ class CmdRepro(CmdBase):
|
||||
if len(stages) == 0:
|
||||
ui.write(CmdDataStatus.UP_TO_DATE_MSG)
|
||||
else:
|
||||
ui.write(
|
||||
"Use `dvc push` to send your updates to " "remote storage."
|
||||
)
|
||||
ui.write("Use `dvc push` to send your updates to remote storage.")
|
||||
|
||||
if self.args.metrics:
|
||||
from dvc.compare import show_metrics
|
||||
|
||||
@@ -76,7 +76,7 @@ class CmdStageList(CmdBase):
|
||||
# removing duplicates while maintaining order
|
||||
collected = chain.from_iterable(
|
||||
self.repo.stage.collect(
|
||||
target=target, recursive=self.args.recursive, accept_group=True
|
||||
target=target, recursive=self.args.recursive
|
||||
)
|
||||
for target in self.args.targets
|
||||
)
|
||||
|
||||
+12
-1
@@ -18,6 +18,7 @@ class CmdUpdate(CmdBase):
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
to_remote=self.args.to_remote,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
jobs=self.args.jobs,
|
||||
)
|
||||
@@ -28,7 +29,10 @@ class CmdUpdate(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
UPDATE_HELP = "Update data artifacts imported from other DVC repositories."
|
||||
UPDATE_HELP = (
|
||||
"Update data artifact imported (via dvc import or dvc import-url) "
|
||||
"from an external DVC repository or URL."
|
||||
)
|
||||
update_parser = subparsers.add_parser(
|
||||
"update",
|
||||
parents=[parent_parser],
|
||||
@@ -52,6 +56,13 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Update all stages in the specified directory.",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--no-download",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Update .dvc file hash value(s)"
|
||||
" but do not download the file(s).",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--to-remote",
|
||||
action="store_true",
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@ class Config(dict):
|
||||
filename = self.files[level]
|
||||
fs = self._get_fs(level)
|
||||
|
||||
logger.debug(f"Writing '{filename}'.")
|
||||
logger.debug("Writing '%s'.", filename)
|
||||
|
||||
fs.makedirs(os.path.dirname(filename))
|
||||
|
||||
|
||||
@@ -78,7 +78,9 @@ REMOTE_COMMON = {
|
||||
"url": str,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
"jobs": All(Coerce(int), Range(1)),
|
||||
Optional("worktree"): Bool,
|
||||
Optional("no_traverse"): Bool, # obsoleted
|
||||
Optional("version_aware"): Bool,
|
||||
}
|
||||
LOCAL_COMMON = {
|
||||
"type": supported_cache_type,
|
||||
@@ -152,6 +154,8 @@ SCHEMA = {
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"sse": str,
|
||||
"sse_kms_key_id": str,
|
||||
"sse_customer_algorithm": str,
|
||||
"sse_customer_key": str,
|
||||
"acl": str,
|
||||
"grant_read": str,
|
||||
"grant_read_acp": str,
|
||||
@@ -166,6 +170,7 @@ SCHEMA = {
|
||||
"gs": {
|
||||
"projectname": str,
|
||||
"credentialpath": str,
|
||||
"endpointurl": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
@@ -217,6 +222,7 @@ SCHEMA = {
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gdrive": {
|
||||
"profile": str,
|
||||
"gdrive_use_service_account": Bool,
|
||||
"gdrive_client_id": str,
|
||||
"gdrive_client_secret": str,
|
||||
@@ -224,6 +230,7 @@ SCHEMA = {
|
||||
"gdrive_service_account_user_email": str,
|
||||
"gdrive_service_account_json_file_path": str,
|
||||
Optional("gdrive_trash_only", default=False): Bool,
|
||||
Optional("gdrive_acknowledge_abuse", default=False): Bool,
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
@@ -269,6 +276,7 @@ SCHEMA = {
|
||||
"plots": {
|
||||
"html_template": str,
|
||||
Optional("auto_open", default=False): Bool,
|
||||
"out_dir": str,
|
||||
},
|
||||
"exp": {
|
||||
"code": str,
|
||||
@@ -279,4 +287,13 @@ SCHEMA = {
|
||||
"plots": str,
|
||||
"live": str,
|
||||
},
|
||||
"parsing": {
|
||||
"bool": All(Lower, Choices("store_true", "boolean_optional")),
|
||||
"list": All(Lower, Choices("nargs", "append")),
|
||||
},
|
||||
"hydra": {
|
||||
Optional("enabled", default=False): Bool,
|
||||
"config_dir": str,
|
||||
"config_name": str,
|
||||
},
|
||||
}
|
||||
|
||||
+7
-4
@@ -6,6 +6,7 @@ import os
|
||||
import platform
|
||||
import sys
|
||||
from subprocess import Popen
|
||||
from typing import List
|
||||
|
||||
from dvc.env import DVC_DAEMON
|
||||
from dvc.utils import fix_env, is_binary
|
||||
@@ -81,7 +82,7 @@ def _spawn_posix(cmd, env):
|
||||
|
||||
|
||||
def _spawn(cmd, env):
|
||||
logger.debug(f"Trying to spawn '{cmd}'")
|
||||
logger.debug("Trying to spawn '%s'", cmd)
|
||||
|
||||
if os.name == "nt":
|
||||
_spawn_windows(cmd, env)
|
||||
@@ -90,7 +91,7 @@ def _spawn(cmd, env):
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
logger.debug(f"Spawned '{cmd}'")
|
||||
logger.debug("Spawned '%s'", cmd)
|
||||
|
||||
|
||||
def daemon(args):
|
||||
@@ -99,12 +100,14 @@ def daemon(args):
|
||||
Args:
|
||||
args (list): list of arguments to append to `dvc daemon` command.
|
||||
"""
|
||||
daemonize(["daemon", "-q"] + args)
|
||||
|
||||
|
||||
def daemonize(cmd: List[str]):
|
||||
if os.environ.get(DVC_DAEMON):
|
||||
logger.debug("skipping launching a new daemon.")
|
||||
return
|
||||
|
||||
cmd = ["daemon", "-q"] + args
|
||||
|
||||
env = fix_env()
|
||||
if not is_binary():
|
||||
file_path = os.path.abspath(inspect.stack()[0][1])
|
||||
|
||||
+48
-22
@@ -3,16 +3,37 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
from dvc_data.db import get_index
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc_data.hashfile.db import get_index
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.status import CompareStatusResult
|
||||
from dvc_objects.db import ObjectDB
|
||||
from dvc_objects.hash_info import HashInfo
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, path, fs, **config):
|
||||
self.path = path
|
||||
self.fs = fs
|
||||
|
||||
self.worktree = config.pop("worktree", False)
|
||||
self.config = config
|
||||
|
||||
@cached_property
|
||||
def odb(self):
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
path = self.path
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(path, ".dvc", "cache")
|
||||
|
||||
return get_odb(self.fs, path, **self.config)
|
||||
|
||||
|
||||
class DataCloud:
|
||||
"""Class that manages dvc remotes.
|
||||
|
||||
@@ -27,18 +48,23 @@ class DataCloud:
|
||||
def __init__(self, repo):
|
||||
self.repo = repo
|
||||
|
||||
def get_remote_odb(
|
||||
def get_remote(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "ObjectDB":
|
||||
) -> "Remote":
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
if not name:
|
||||
name = self.repo.config["core"].get("remote")
|
||||
|
||||
if name:
|
||||
return self._init_odb(name)
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
fs = cls(**config)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return Remote(fs_path, fs, **config)
|
||||
|
||||
if bool(self.repo.config["remote"]):
|
||||
error_msg = (
|
||||
@@ -55,13 +81,13 @@ class DataCloud:
|
||||
|
||||
raise NoRemoteError(error_msg)
|
||||
|
||||
def _init_odb(self, name):
|
||||
from dvc.fs import get_cloud_fs
|
||||
from dvc_data.db import get_odb
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return get_odb(cls(**config), fs_path, **config)
|
||||
def get_remote_odb(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "HashFileDB":
|
||||
remote = self.get_remote(name=name, command=command)
|
||||
return remote.odb
|
||||
|
||||
def _log_missing(self, status: "CompareStatusResult"):
|
||||
if status.missing:
|
||||
@@ -76,13 +102,13 @@ class DataCloud:
|
||||
|
||||
def transfer(
|
||||
self,
|
||||
src_odb: "ObjectDB",
|
||||
dest_odb: "ObjectDB",
|
||||
src_odb: "HashFileDB",
|
||||
dest_odb: "HashFileDB",
|
||||
objs: Iterable["HashInfo"],
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.exceptions import FileTransferError
|
||||
from dvc_data.transfer import TransferError, transfer
|
||||
from dvc_data.hashfile.transfer import TransferError, transfer
|
||||
|
||||
try:
|
||||
return transfer(src_odb, dest_odb, objs, **kwargs)
|
||||
@@ -94,7 +120,7 @@ class DataCloud:
|
||||
objs: Iterable["HashInfo"],
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
odb: Optional["ObjectDB"] = None,
|
||||
odb: Optional["HashFileDB"] = None,
|
||||
):
|
||||
"""Push data items in a cloud-agnostic way.
|
||||
|
||||
@@ -121,7 +147,7 @@ class DataCloud:
|
||||
objs: Iterable["HashInfo"],
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
odb: Optional["ObjectDB"] = None,
|
||||
odb: Optional["HashFileDB"] = None,
|
||||
):
|
||||
"""Pull data items in a cloud-agnostic way.
|
||||
|
||||
@@ -149,7 +175,7 @@ class DataCloud:
|
||||
objs: Iterable["HashInfo"],
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
odb: Optional["ObjectDB"] = None,
|
||||
odb: Optional["HashFileDB"] = None,
|
||||
log_missing: bool = True,
|
||||
):
|
||||
"""Check status of data items in a cloud-agnostic way.
|
||||
@@ -164,7 +190,7 @@ class DataCloud:
|
||||
log_missing: log warning messages if file doesn't exist
|
||||
neither in cache, neither in cloud.
|
||||
"""
|
||||
from dvc_data.status import compare_status
|
||||
from dvc_data.hashfile.status import compare_status
|
||||
|
||||
if not odb:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
@@ -180,5 +206,5 @@ class DataCloud:
|
||||
|
||||
def get_url_for(self, remote, checksum):
|
||||
odb = self.get_remote_odb(remote)
|
||||
path = odb.hash_to_path(checksum)
|
||||
path = odb.oid_to_path(checksum)
|
||||
return odb.fs.unstrip_protocol(path)
|
||||
|
||||
+8
-10
@@ -8,8 +8,8 @@ import dpath.util
|
||||
from voluptuous import Any
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils.serialize import LOADERS, ParseError
|
||||
from dvc_objects.hash_info import HashInfo
|
||||
from dvc.utils.serialize import ParseError, load_path
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
@@ -39,12 +39,13 @@ class ParamsDependency(Dependency):
|
||||
|
||||
def __init__(self, stage, path, params=None, repo=None):
|
||||
self.params = list(params) if params else []
|
||||
info = (
|
||||
{self.PARAM_PARAMS: params} if isinstance(params, dict) else None
|
||||
)
|
||||
hash_info = HashInfo()
|
||||
if isinstance(params, dict):
|
||||
hash_info = HashInfo(self.PARAM_PARAMS, params)
|
||||
repo = repo or stage.repo
|
||||
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
|
||||
super().__init__(stage, path, info=info, repo=repo)
|
||||
super().__init__(stage, path, repo=repo)
|
||||
self.hash_info = hash_info
|
||||
|
||||
def dumpd(self):
|
||||
ret = super().dumpd()
|
||||
@@ -139,12 +140,9 @@ class ParamsDependency(Dependency):
|
||||
)
|
||||
|
||||
def read_file(self):
|
||||
_, ext = os.path.splitext(self.fs_path)
|
||||
loader = LOADERS[ext]
|
||||
|
||||
self.validate_filepath()
|
||||
try:
|
||||
return loader(self.fs_path, fs=self.repo.fs)
|
||||
return load_path(self.fs_path, self.repo.fs)
|
||||
except ParseError as exc:
|
||||
raise BadParamFileError(
|
||||
f"Unable to read parameters from '{self}'"
|
||||
|
||||
+22
-24
@@ -10,9 +10,9 @@ from dvc.prompt import confirm
|
||||
from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.file import HashFile
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_objects.db import ObjectDB
|
||||
from dvc_objects.file import HashFile
|
||||
from dvc_objects.hash_info import HashInfo
|
||||
|
||||
|
||||
class RepoDependency(Dependency):
|
||||
@@ -31,7 +31,7 @@ class RepoDependency(Dependency):
|
||||
|
||||
def __init__(self, def_repo, stage, *args, **kwargs):
|
||||
self.def_repo = def_repo
|
||||
self._staged_objs: Dict[str, "HashFile"] = {}
|
||||
self._objs: Dict[str, "HashFile"] = {}
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
def _parse_path(self, fs, fs_path):
|
||||
@@ -63,7 +63,7 @@ class RepoDependency(Dependency):
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc_data.checkout import checkout
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
||||
@@ -103,14 +103,12 @@ class RepoDependency(Dependency):
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputOrStageError, PathMissingError
|
||||
from dvc.utils import as_posix
|
||||
from dvc_data.stage import stage
|
||||
from dvc_data.tree import Tree, TreeError
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.tree import Tree, TreeError
|
||||
|
||||
local_odb = self.repo.odb.local
|
||||
locked = kwargs.pop("locked", True)
|
||||
with self._make_repo(
|
||||
locked=locked, cache_dir=local_odb.cache_dir
|
||||
) as repo:
|
||||
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:
|
||||
@@ -133,7 +131,7 @@ class RepoDependency(Dependency):
|
||||
pass
|
||||
|
||||
try:
|
||||
staging, _, staged_obj = stage(
|
||||
object_store, _, obj = build(
|
||||
local_odb,
|
||||
as_posix(self.def_path),
|
||||
repo.dvcfs,
|
||||
@@ -143,31 +141,31 @@ class RepoDependency(Dependency):
|
||||
raise PathMissingError(
|
||||
self.def_path, self.def_repo[self.PARAM_URL]
|
||||
) from exc
|
||||
staging = copy(staging)
|
||||
staging.read_only = True
|
||||
object_store = copy(object_store)
|
||||
object_store.read_only = True
|
||||
|
||||
self._staged_objs[rev] = staged_obj
|
||||
used_obj_ids[staging].add(staged_obj.hash_info)
|
||||
if isinstance(staged_obj, Tree):
|
||||
used_obj_ids[staging].update(oid for _, _, oid in staged_obj)
|
||||
return used_obj_ids, staged_obj
|
||||
self._objs[rev] = obj
|
||||
used_obj_ids[object_store].add(obj.hash_info)
|
||||
if isinstance(obj, Tree):
|
||||
used_obj_ids[object_store].update(oid for _, _, oid in obj)
|
||||
return used_obj_ids, obj
|
||||
|
||||
def _check_circular_import(self, odb, obj_ids):
|
||||
from dvc.exceptions import CircularImportError
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
from dvc_data.db.reference import ReferenceObjectDB
|
||||
from dvc_data.tree import Tree
|
||||
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
if not isinstance(odb, ReferenceObjectDB):
|
||||
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(oid) for _, _, oid in tree)
|
||||
yield from (odb.get(hi.value) for _, _, hi in tree)
|
||||
else:
|
||||
yield odb.get(hash_info)
|
||||
yield odb.get(hash_info.value)
|
||||
|
||||
checked_urls = set()
|
||||
for obj in iter_objs():
|
||||
@@ -190,8 +188,8 @@ class RepoDependency(Dependency):
|
||||
def get_obj(self, filter_info=None, **kwargs):
|
||||
locked = kwargs.get("locked", True)
|
||||
rev = self._get_rev(locked=locked)
|
||||
if rev in self._staged_objs:
|
||||
return self._staged_objs[rev]
|
||||
if rev in self._objs:
|
||||
return self._objs[rev]
|
||||
_, obj = self._get_used_and_obj(
|
||||
obj_only=True, filter_info=filter_info, **kwargs
|
||||
)
|
||||
|
||||
+6
-3
@@ -197,7 +197,7 @@ class SingleStageFile(FileMixin):
|
||||
assert not isinstance(stage, PipelineStage)
|
||||
if self.verify:
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
logger.debug(f"Saving information to '{relpath(self.path)}'.")
|
||||
logger.debug("Saving information to '%s'.", relpath(self.path))
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage))
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
@@ -243,9 +243,11 @@ class PipelineFile(FileMixin):
|
||||
self._lockfile.dump(stage)
|
||||
|
||||
@staticmethod
|
||||
def _check_if_parametrized(stage):
|
||||
def _check_if_parametrized(stage, action: str = "dump") -> None:
|
||||
if stage.raw_data.parametrized:
|
||||
raise ParametrizedDumpError(f"cannot dump a parametrized {stage}")
|
||||
raise ParametrizedDumpError(
|
||||
f"cannot {action} a parametrized {stage}"
|
||||
)
|
||||
|
||||
def _dump_pipeline_file(self, stage):
|
||||
self._check_if_parametrized(stage)
|
||||
@@ -291,6 +293,7 @@ class PipelineFile(FileMixin):
|
||||
self._lockfile.remove()
|
||||
|
||||
def remove_stage(self, stage):
|
||||
self._check_if_parametrized(stage, "remove")
|
||||
self._lockfile.remove_stage(stage)
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
@@ -145,7 +145,7 @@ def _get_remote_config(url):
|
||||
name = "auto-generated-upstream"
|
||||
return {
|
||||
"core": {"remote": name},
|
||||
"remote": {name: {"url": repo.odb.local.cache_dir}},
|
||||
"remote": {name: {"url": repo.odb.local.path}},
|
||||
}
|
||||
|
||||
# Use original remote to make sure that we are using correct url,
|
||||
|
||||
+20
-28
@@ -1,26 +1,18 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from dvc_objects.fs import utils # noqa: F401
|
||||
from dvc_objects.fs import ( # noqa: F401
|
||||
FS_MAP,
|
||||
AzureFileSystem,
|
||||
GDriveFileSystem,
|
||||
GSFileSystem,
|
||||
HDFSFileSystem,
|
||||
HTTPFileSystem,
|
||||
HTTPSFileSystem,
|
||||
LocalFileSystem,
|
||||
MemoryFileSystem,
|
||||
OSSFileSystem,
|
||||
S3FileSystem,
|
||||
Schemes,
|
||||
SSHFileSystem,
|
||||
WebDAVFileSystem,
|
||||
WebDAVSFileSystem,
|
||||
WebHDFSFileSystem,
|
||||
generic,
|
||||
get_fs_cls,
|
||||
known_implementations,
|
||||
localfs,
|
||||
registry,
|
||||
system,
|
||||
)
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401
|
||||
@@ -29,18 +21,26 @@ from dvc_objects.fs.errors import ( # noqa: F401
|
||||
ConfigError,
|
||||
RemoteMissingDepsError,
|
||||
)
|
||||
from dvc_objects.fs.implementations.azure import AzureAuthError # noqa: F401
|
||||
from dvc_objects.fs.implementations.gdrive import GDriveAuthError # noqa: F401
|
||||
from dvc_objects.fs.implementations.local import localfs # noqa: F401
|
||||
from dvc_objects.fs.implementations.ssh import ( # noqa: F401
|
||||
DEFAULT_PORT as DEFAULT_SSH_PORT,
|
||||
)
|
||||
from dvc_objects.fs.path import Path # noqa: F401
|
||||
|
||||
from .data import DataFileSystem # noqa: F401
|
||||
from .dvc import DvcFileSystem # noqa: F401
|
||||
from .git import GitFileSystem # noqa: F401
|
||||
|
||||
known_implementations.update(
|
||||
{
|
||||
"dvc": {
|
||||
"class": "dvc.fs.dvc.DvcFileSystem",
|
||||
"err": "dvc is supported, but requires 'dvc' to be installed",
|
||||
},
|
||||
"git": {
|
||||
"class": "dvc.fs.git.GitFileSystem",
|
||||
"err": "git is supported, but requires 'dvc' to be installed",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# pylint: enable=unused-import
|
||||
|
||||
|
||||
@@ -104,11 +104,6 @@ def get_cloud_fs(repo, **kwargs):
|
||||
except Invalid as exc:
|
||||
raise RepoConfigError(str(exc)) from None
|
||||
|
||||
if "jobs" not in remote_conf:
|
||||
jobs = core_config.get("jobs")
|
||||
if jobs:
|
||||
remote_conf["jobs"] = jobs
|
||||
|
||||
if "checksum_jobs" not in remote_conf:
|
||||
checksum_jobs = core_config.get("checksum_jobs")
|
||||
if checksum_jobs:
|
||||
@@ -116,11 +111,8 @@ def get_cloud_fs(repo, **kwargs):
|
||||
|
||||
cls = get_fs_cls(remote_conf)
|
||||
|
||||
if cls == GDriveFileSystem and repo:
|
||||
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
|
||||
|
||||
url = remote_conf.pop("url")
|
||||
if issubclass(cls, WebDAVFileSystem):
|
||||
if cls.protocol in ["webdav", "webdavs"]:
|
||||
# For WebDAVFileSystem, provided url is the base path itself, so it
|
||||
# should be treated as being a root path.
|
||||
fs_path = cls.root_marker
|
||||
|
||||
+8
-173
@@ -1,183 +1,16 @@
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import typing
|
||||
|
||||
from fsspec import AbstractFileSystem
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.utils import as_posix
|
||||
from dvc_data.fs import DataFileSystem as _DataFileSystem
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from dvc.types import AnyPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _DataFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
"""DVC repo fs.
|
||||
|
||||
Args:
|
||||
repo: DVC repo.
|
||||
"""
|
||||
|
||||
root_marker = "/"
|
||||
|
||||
def __init__(self, workspace=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.repo = kwargs["repo"]
|
||||
self.workspace = workspace or "repo"
|
||||
|
||||
@cached_property
|
||||
def path(self):
|
||||
from . import Path
|
||||
|
||||
def _getcwd():
|
||||
return self.root_marker
|
||||
|
||||
return Path(self.sep, getcwd=_getcwd)
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_key(self, path):
|
||||
if self.workspace != "repo":
|
||||
from . import get_cloud_fs
|
||||
|
||||
cls, kwargs, fs_path = get_cloud_fs(None, url=path)
|
||||
fs = cls(**kwargs)
|
||||
return (self.workspace, *fs.path.parts(fs_path))
|
||||
|
||||
path = self.path.abspath(path)
|
||||
if path == self.root_marker:
|
||||
return (self.workspace,)
|
||||
|
||||
key = self.path.relparts(path, self.root_marker)
|
||||
if key == (".") or key == (""):
|
||||
key = ()
|
||||
|
||||
return (self.workspace, *key)
|
||||
|
||||
def _get_fs_path(self, path: "AnyPath", remote=None):
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
info = self.info(path)
|
||||
if info["type"] == "directory":
|
||||
raise IsADirectoryError
|
||||
|
||||
value = info.get("md5")
|
||||
if not value:
|
||||
raise FileNotFoundError
|
||||
|
||||
cache_path = self.repo.odb.local.hash_to_path(value)
|
||||
|
||||
if self.repo.odb.local.fs.exists(cache_path):
|
||||
return self.repo.odb.local.fs, cache_path
|
||||
|
||||
try:
|
||||
remote_odb = self.repo.cloud.get_remote_odb(remote)
|
||||
except NoRemoteError as exc:
|
||||
raise FileNotFoundError from exc
|
||||
remote_fs_path = remote_odb.hash_to_path(value)
|
||||
return remote_odb.fs, remote_fs_path
|
||||
|
||||
def open( # type: ignore
|
||||
self, path: str, mode="r", encoding=None, **kwargs
|
||||
): # pylint: disable=arguments-renamed, arguments-differ
|
||||
fs, fspath = self._get_fs_path(path, **kwargs)
|
||||
return fs.open(fspath, mode=mode, encoding=encoding)
|
||||
|
||||
def ls(self, path, detail=True, **kwargs):
|
||||
info = self.info(path)
|
||||
if info["type"] != "directory":
|
||||
return [info] if detail else [path]
|
||||
|
||||
root_key = self._get_key(path)
|
||||
try:
|
||||
entries = [
|
||||
self.sep.join((path, name)) if path else name
|
||||
for name in self.repo.index.tree.ls(prefix=root_key)
|
||||
]
|
||||
except KeyError as exc:
|
||||
raise FileNotFoundError from exc
|
||||
|
||||
if not detail:
|
||||
return entries
|
||||
|
||||
return [self.info(epath) for epath in entries]
|
||||
|
||||
def isdvc(self, path, recursive=False, strict=True):
|
||||
try:
|
||||
info = self.info(path)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
recurse = recursive or not strict
|
||||
return bool(info.get("outs") if recurse else info.get("isout"))
|
||||
|
||||
def info(self, path, **kwargs):
|
||||
from dvc_objects.meta import Meta
|
||||
|
||||
key = self._get_key(path)
|
||||
|
||||
try:
|
||||
outs = list(self.repo.index.tree.iteritems(key))
|
||||
except KeyError as exc:
|
||||
raise FileNotFoundError from exc
|
||||
|
||||
ret = {
|
||||
"type": "file",
|
||||
"size": 0,
|
||||
"isexec": False,
|
||||
"isdvc": False,
|
||||
"outs": outs,
|
||||
"name": path,
|
||||
}
|
||||
|
||||
if len(outs) > 1 and outs[0][0] != key:
|
||||
shortest = self.repo.index.tree.shortest_prefix(key)
|
||||
if shortest:
|
||||
assert shortest[1][1].isdir
|
||||
if len(shortest[0]) <= len(key):
|
||||
ret["isdvc"] = True
|
||||
|
||||
ret["type"] = "directory"
|
||||
return ret
|
||||
|
||||
item_key, (meta, hash_info) = outs[0]
|
||||
|
||||
meta = meta or Meta()
|
||||
|
||||
if key != item_key:
|
||||
assert item_key[: len(key)] == key
|
||||
ret["type"] = "directory"
|
||||
return ret
|
||||
|
||||
ret["size"] = meta.size
|
||||
ret["isexec"] = meta.isexec
|
||||
ret[hash_info.name] = hash_info.value
|
||||
ret["isdvc"] = True
|
||||
ret["isout"] = True
|
||||
ret["meta"] = meta
|
||||
if hash_info and hash_info.isdir:
|
||||
ret["type"] = "directory"
|
||||
return ret
|
||||
|
||||
def get_file( # pylint: disable=arguments-differ
|
||||
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
fs, path = self._get_fs_path(rpath)
|
||||
fs.get_file(path, lpath, callback=callback, **kwargs)
|
||||
|
||||
def checksum(self, path):
|
||||
info = self.info(path)
|
||||
md5 = info.get("md5")
|
||||
if md5:
|
||||
return md5
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DataFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
|
||||
@@ -194,6 +27,8 @@ class DataFileSystem(FileSystem):
|
||||
def isdvc(self, path, **kwargs):
|
||||
return self.fs.isdvc(path, **kwargs)
|
||||
|
||||
@property
|
||||
def repo(self):
|
||||
return self.fs.repo
|
||||
def from_os_path(self, path):
|
||||
if os.path.isabs(path):
|
||||
path = os.path.splitdrive(path)[1]
|
||||
|
||||
return as_posix(path)
|
||||
|
||||
+115
-203
@@ -4,8 +4,7 @@ import os
|
||||
import posixpath
|
||||
import threading
|
||||
from contextlib import suppress
|
||||
from itertools import takewhile
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Tuple, Type, Union
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple, Type, Union
|
||||
|
||||
from fsspec.spec import AbstractFileSystem
|
||||
from funcy import cached_property, wrap_prop, wrap_with
|
||||
@@ -22,29 +21,19 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RepoFactory = Union[Callable[[str], "Repo"], Type["Repo"]]
|
||||
Key = Tuple[str, ...]
|
||||
|
||||
|
||||
def as_posix(path: str) -> str:
|
||||
return path.replace(ntpath.sep, posixpath.sep)
|
||||
|
||||
|
||||
def _wrap_walk(dvc_fs, *args, **kwargs):
|
||||
for root, dnames, fnames in dvc_fs.walk(*args, **kwargs):
|
||||
yield dvc_fs.path.join(dvc_fs.repo.root_dir, root), dnames, fnames
|
||||
# NOT the same as dvc.dvcfile.is_dvc_file()!
|
||||
def _is_dvc_file(fname):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.ignore import DvcIgnore
|
||||
|
||||
|
||||
def _ls(fs, path):
|
||||
dnames = []
|
||||
fnames = []
|
||||
|
||||
for entry in fs.ls(path, detail=True):
|
||||
name = fs.path.name(entry["name"])
|
||||
if entry["type"] == "directory":
|
||||
dnames.append(name)
|
||||
else:
|
||||
fnames.append(name)
|
||||
|
||||
return dnames, fnames
|
||||
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
|
||||
|
||||
def _merge_info(repo, fs_info, dvc_info):
|
||||
@@ -70,6 +59,10 @@ def _merge_info(repo, fs_info, dvc_info):
|
||||
return ret
|
||||
|
||||
|
||||
def _get_dvc_path(dvc_fs, subkey):
|
||||
return dvc_fs.path.join(*subkey) if subkey else ""
|
||||
|
||||
|
||||
class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
"""DVC + git-tracked files fs.
|
||||
|
||||
@@ -82,28 +75,25 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
root_marker = "/"
|
||||
|
||||
PARAM_REPO_URL = "repo_url"
|
||||
PARAM_REPO_ROOT = "repo_root"
|
||||
PARAM_REV = "rev"
|
||||
PARAM_CACHE_DIR = "cache_dir"
|
||||
PARAM_CACHE_TYPES = "cache_types"
|
||||
PARAM_SUBREPOS = "subrepos"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
repo: Optional["Repo"] = None,
|
||||
subrepos=False,
|
||||
repo_factory: RepoFactory = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
subrepos: bool = False,
|
||||
repo_factory: Optional[RepoFactory] = None,
|
||||
**repo_kwargs: Any,
|
||||
) -> None:
|
||||
from pygtrie import Trie
|
||||
|
||||
super().__init__()
|
||||
if repo is None:
|
||||
repo, repo_factory = self._repo_from_fs_config(
|
||||
subrepos=subrepos, **kwargs
|
||||
repo = self._make_repo(
|
||||
url=url, rev=rev, subrepos=subrepos, **repo_kwargs
|
||||
)
|
||||
assert repo
|
||||
# pylint: disable=protected-access
|
||||
repo_factory = repo._fs_conf["repo_factory"]
|
||||
|
||||
if not repo_factory:
|
||||
from dvc.repo import Repo
|
||||
@@ -135,82 +125,37 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
"""Keep a datafs instance of each repo."""
|
||||
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self._datafss[key] = DataFileSystem(repo=repo)
|
||||
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
|
||||
|
||||
def _get_key(self, path):
|
||||
def _get_key(self, path) -> Key:
|
||||
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
|
||||
if parts == (".",):
|
||||
parts = ()
|
||||
return parts
|
||||
|
||||
def _get_key_from_relative(self, path) -> Key:
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
parts = parts[1:]
|
||||
return parts
|
||||
|
||||
def _from_key(self, parts: Key) -> str:
|
||||
return self.repo.fs.path.join(self.repo.root_dir, *parts)
|
||||
|
||||
@property
|
||||
def repo_url(self):
|
||||
if self.repo is None:
|
||||
return None
|
||||
return self.repo.url
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return {
|
||||
self.PARAM_REPO_URL: self.repo_url,
|
||||
self.PARAM_REPO_ROOT: self.repo.root_dir,
|
||||
self.PARAM_REV: getattr(self.repo.fs, "rev", None),
|
||||
self.PARAM_CACHE_DIR: os.path.abspath(
|
||||
self.repo.odb.local.cache_dir
|
||||
),
|
||||
self.PARAM_CACHE_TYPES: self.repo.odb.local.cache_types,
|
||||
self.PARAM_SUBREPOS: self._traverse_subrepos,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _repo_from_fs_config(
|
||||
cls, **config
|
||||
) -> Tuple["Repo", Optional["RepoFactory"]]:
|
||||
from dvc.external_repo import erepo_factory, external_repo
|
||||
def _make_repo(cls, **kwargs) -> "Repo":
|
||||
from dvc.repo import Repo
|
||||
|
||||
url = config.get(cls.PARAM_REPO_URL)
|
||||
root = config.get(cls.PARAM_REPO_ROOT)
|
||||
assert url or root
|
||||
with Repo.open(uninitialized=True, **kwargs) as repo:
|
||||
return repo
|
||||
|
||||
def _open(*args, **kwargs):
|
||||
# NOTE: if original repo was an erepo (and has a URL),
|
||||
# we cannot use Repo.open() since it will skip erepo
|
||||
# cache/remote setup for local URLs
|
||||
if url is None:
|
||||
return Repo.open(*args, **kwargs)
|
||||
return external_repo(*args, **kwargs)
|
||||
|
||||
cache_dir = config.get(cls.PARAM_CACHE_DIR)
|
||||
cache_config = (
|
||||
{}
|
||||
if not cache_dir
|
||||
else {
|
||||
"cache": {
|
||||
"dir": cache_dir,
|
||||
"type": config.get(cls.PARAM_CACHE_TYPES),
|
||||
}
|
||||
}
|
||||
)
|
||||
repo_kwargs: dict = {
|
||||
"rev": config.get(cls.PARAM_REV),
|
||||
"subrepos": config.get(cls.PARAM_SUBREPOS, False),
|
||||
"uninitialized": True,
|
||||
}
|
||||
factory: Optional["RepoFactory"] = None
|
||||
if url is None:
|
||||
repo_kwargs["config"] = cache_config
|
||||
else:
|
||||
repo_kwargs["cache_dir"] = cache_dir
|
||||
factory = erepo_factory(url, root, cache_config)
|
||||
|
||||
with _open(
|
||||
url if url else root,
|
||||
**repo_kwargs,
|
||||
) as repo:
|
||||
return repo, factory
|
||||
|
||||
def _get_repo(self, path: str) -> "Repo":
|
||||
def _get_repo(self, key: Key) -> "Repo":
|
||||
"""Returns repo that the path falls in, using prefix.
|
||||
|
||||
If the path is already tracked/collected, it just returns the repo.
|
||||
@@ -218,33 +163,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
Otherwise, it collects the repos that might be in the path's parents
|
||||
and then returns the appropriate one.
|
||||
"""
|
||||
if not self.repo.fs.path.isin_or_eq(path, self.repo.root_dir):
|
||||
# outside of repo
|
||||
return self.repo
|
||||
|
||||
key = self._get_key(path)
|
||||
repo = self._subrepos_trie.get(key)
|
||||
if repo:
|
||||
return repo
|
||||
|
||||
prefix_key, repo = self._subrepos_trie.longest_prefix(key)
|
||||
prefix = self.repo.fs.path.join(
|
||||
self.repo.root_dir,
|
||||
*prefix_key, # pylint: disable=not-an-iterable
|
||||
)
|
||||
|
||||
parents = (parent for parent in self.repo.fs.path.parents(path))
|
||||
dirs = [path] + list(takewhile(lambda p: p != prefix, parents))
|
||||
dirs.reverse()
|
||||
self._update(dirs, starting_repo=repo)
|
||||
dir_keys = (key[:i] for i in range(len(prefix_key) + 1, len(key) + 1))
|
||||
self._update(dir_keys, starting_repo=repo)
|
||||
return self._subrepos_trie.get(key) or self.repo
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _update(self, dirs, starting_repo):
|
||||
def _update(self, dir_keys, starting_repo):
|
||||
"""Checks for subrepo in directories and updates them."""
|
||||
repo = starting_repo
|
||||
for d in dirs:
|
||||
key = self._get_key(d)
|
||||
for key in dir_keys:
|
||||
d = self._from_key(key)
|
||||
if self._is_dvc_repo(d):
|
||||
repo = self.repo_factory(
|
||||
d,
|
||||
@@ -252,7 +185,9 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
scm=self.repo.scm,
|
||||
repo_factory=self.repo_factory,
|
||||
)
|
||||
self._datafss[key] = DataFileSystem(repo=repo)
|
||||
self._datafss[key] = DataFileSystem(
|
||||
index=repo.index.data["repo"]
|
||||
)
|
||||
self._subrepos_trie[key] = repo
|
||||
|
||||
def _is_dvc_repo(self, dir_path):
|
||||
@@ -265,41 +200,23 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
repo_path = self.repo.fs.path.join(dir_path, Repo.DVC_DIR)
|
||||
return self.repo.fs.isdir(repo_path)
|
||||
|
||||
def _get_fs_pair(
|
||||
self, path
|
||||
) -> Tuple[
|
||||
Optional[FileSystem],
|
||||
Optional[str],
|
||||
Optional[DataFileSystem],
|
||||
Optional[str],
|
||||
]:
|
||||
def _get_subrepo_info(
|
||||
self, key: Key
|
||||
) -> Tuple["Repo", Optional[DataFileSystem], Key]:
|
||||
"""
|
||||
Returns a pair of fss based on repo the path falls in, using prefix.
|
||||
Returns information about the subrepo the key is part of.
|
||||
"""
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
parts = parts[1:]
|
||||
|
||||
fs_path = self.repo.fs.path.join(self.repo.root_dir, *parts)
|
||||
repo = self._get_repo(fs_path)
|
||||
fs = repo.fs
|
||||
|
||||
repo_parts = fs.path.relparts(repo.root_dir, self.repo.root_dir)
|
||||
if repo_parts[0] == os.curdir:
|
||||
repo_parts = repo_parts[1:]
|
||||
|
||||
dvc_parts = parts[len(repo_parts) :]
|
||||
if dvc_parts and dvc_parts[0] == os.curdir:
|
||||
dvc_parts = dvc_parts[1:]
|
||||
|
||||
key = self._get_key(repo.root_dir)
|
||||
dvc_fs = self._datafss.get(key)
|
||||
if dvc_fs:
|
||||
dvc_path = dvc_fs.path.join(*dvc_parts) if dvc_parts else ""
|
||||
repo = self._get_repo(key)
|
||||
repo_key: Key
|
||||
if repo is self.repo:
|
||||
repo_key = ()
|
||||
subkey = key
|
||||
else:
|
||||
dvc_path = None
|
||||
repo_key = self._get_key(repo.root_dir)
|
||||
subkey = key[len(repo_key) :]
|
||||
|
||||
return fs, fs_path, dvc_fs, dvc_path
|
||||
dvc_fs = self._datafss.get(repo_key)
|
||||
return repo, dvc_fs, subkey
|
||||
|
||||
def open(
|
||||
self, path, mode="r", encoding="utf-8", **kwargs
|
||||
@@ -307,37 +224,43 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
if "b" in mode:
|
||||
encoding = None
|
||||
|
||||
fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
key = self._get_key_from_relative(path)
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
return fs.open(fs_path, mode=mode, encoding=encoding)
|
||||
return self.repo.fs.open(fs_path, mode=mode, encoding=encoding)
|
||||
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.open(dvc_path, mode=mode, encoding=encoding, **kwargs)
|
||||
|
||||
def isdvc(self, path, **kwargs):
|
||||
_, _, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
key = self._get_key_from_relative(path)
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
|
||||
|
||||
def ls( # pylint: disable=arguments-differ
|
||||
self, path, detail=True, dvc_only=False, **kwargs
|
||||
):
|
||||
fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
repo = dvc_fs.repo if dvc_fs else self.repo
|
||||
dvcignore = repo.dvcignore
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
names = set()
|
||||
if dvc_fs:
|
||||
with suppress(FileNotFoundError):
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
for entry in dvc_fs.ls(dvc_path, detail=False):
|
||||
names.add(dvc_fs.path.name(entry))
|
||||
|
||||
if not dvc_only and fs:
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
if not dvc_only:
|
||||
fs = self.repo.fs
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
for entry in dvcignore.ls(
|
||||
for entry in repo.dvcignore.ls(
|
||||
fs, fs_path, detail=False, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
names.add(fs.path.name(entry))
|
||||
@@ -345,26 +268,21 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
pass
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
|
||||
def _func(fname):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
from dvc.ignore import DvcIgnore
|
||||
|
||||
if dvcfiles:
|
||||
return True
|
||||
|
||||
return not (
|
||||
is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
)
|
||||
|
||||
names = filter(_func, names)
|
||||
if not dvcfiles:
|
||||
names = (name for name in names if not _is_dvc_file(name))
|
||||
|
||||
infos = []
|
||||
paths = []
|
||||
for name in names:
|
||||
entry_path = self.path.join(path, name)
|
||||
entry_key = key + (name,)
|
||||
try:
|
||||
info = self.info(entry_path, ignore_subrepos=ignore_subrepos)
|
||||
info = self._info(
|
||||
entry_key,
|
||||
entry_path,
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
check_ignored=False,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
infos.append(info)
|
||||
@@ -378,46 +296,52 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
def get_file( # pylint: disable=arguments-differ
|
||||
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(rpath)
|
||||
|
||||
if fs:
|
||||
try:
|
||||
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
|
||||
return
|
||||
except FileNotFoundError:
|
||||
if not dvc_fs:
|
||||
raise
|
||||
key = self._get_key_from_relative(rpath)
|
||||
fs_path = self._from_key(key)
|
||||
fs = self.repo.fs
|
||||
try:
|
||||
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
|
||||
return
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_fs.get_file(dvc_path, lpath, callback=callback, **kwargs)
|
||||
|
||||
def info(self, path, **kwargs):
|
||||
fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
repo = dvc_fs.repo if dvc_fs else self.repo
|
||||
dvcignore = repo.dvcignore
|
||||
key = self._get_key_from_relative(path)
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info(self, key, path, ignore_subrepos=True, check_ignored=True):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_info = None
|
||||
if dvc_fs:
|
||||
try:
|
||||
dvc_info = dvc_fs.info(dvc_path)
|
||||
dvc_info = dvc_fs.fs.index.info(subkey)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_info["name"] = dvc_path
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
fs_info = None
|
||||
if fs:
|
||||
try:
|
||||
fs_info = fs.info(fs_path)
|
||||
if dvcignore.is_ignored(
|
||||
fs, fs_path, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_info = None
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
if not dvc_info:
|
||||
raise
|
||||
fs = self.repo.fs
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
fs_info = fs.info(fs_path)
|
||||
if check_ignored and repo.dvcignore.is_ignored(
|
||||
fs, fs_path, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_info = None
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
if not dvc_info:
|
||||
raise
|
||||
|
||||
# NOTE: if some parent in fs_path turns out to be a file, it means
|
||||
# that the whole repofs branch doesn't exist.
|
||||
if fs and not fs_info and dvc_info:
|
||||
if dvc_info and not fs_info:
|
||||
for parent in fs.path.parents(fs_path):
|
||||
try:
|
||||
if fs.info(parent)["type"] != "directory":
|
||||
@@ -433,14 +357,6 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
def checksum(self, path):
|
||||
fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
|
||||
|
||||
try:
|
||||
return fs.checksum(fs_path)
|
||||
except FileNotFoundError:
|
||||
return dvc_fs.checksum(dvc_path)
|
||||
|
||||
|
||||
class DvcFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
@@ -469,10 +385,6 @@ class DvcFileSystem(FileSystem):
|
||||
def repo_url(self):
|
||||
return self.fs.repo_url
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return self.fs.config
|
||||
|
||||
def from_os_path(self, path):
|
||||
if os.path.isabs(path):
|
||||
path = os.path.relpath(path, self.repo.root_dir)
|
||||
|
||||
@@ -15,6 +15,7 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
|
||||
"""Proxies the repo file access methods to Git objects"""
|
||||
|
||||
protocol = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
+35
-6
@@ -8,12 +8,20 @@ import psutil
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.fs import FS_MAP, generic, get_fs_cls, get_fs_config
|
||||
from dvc.fs import Schemes, generic, get_fs_cls, get_fs_config, registry
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import SCMError
|
||||
from dvc.utils import error_link
|
||||
from dvc.utils.pkg import PKG
|
||||
|
||||
SUBPROJECTS = (
|
||||
"dvc_data",
|
||||
"dvc_objects",
|
||||
"dvc_render",
|
||||
"dvc_task",
|
||||
"dvclive",
|
||||
"scmrepo",
|
||||
)
|
||||
package = "" if PKG is None else f"({PKG})"
|
||||
|
||||
|
||||
@@ -23,6 +31,7 @@ def get_dvc_info():
|
||||
"---------------------------------",
|
||||
f"Platform: Python {platform.python_version()} on "
|
||||
f"{platform.platform()}",
|
||||
f"Subprojects:{_get_subprojects()}",
|
||||
f"Supports:{_get_supported_remotes()}",
|
||||
]
|
||||
|
||||
@@ -32,9 +41,9 @@ 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.cache_dir):
|
||||
if os.path.exists(repo.odb.local.path):
|
||||
info.append(f"Cache types: {_get_linktype_support_info(repo)}")
|
||||
fs_type = get_fs_type(repo.odb.local.cache_dir)
|
||||
fs_type = get_fs_type(repo.odb.local.path)
|
||||
info.append(f"Cache directory: {fs_type}")
|
||||
else:
|
||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||
@@ -58,7 +67,7 @@ def _get_caches(cache):
|
||||
caches = (
|
||||
cache_type
|
||||
for cache_type, cache_instance in cache.by_scheme()
|
||||
if cache_instance
|
||||
if cache_instance and cache_type != "repo"
|
||||
)
|
||||
|
||||
# Caches will be always non-empty including the local cache
|
||||
@@ -80,7 +89,7 @@ def _get_linktype_support_info(repo):
|
||||
links = generic.test_links(
|
||||
["reflink", "hardlink", "symlink"],
|
||||
odb.fs,
|
||||
odb.fs_path,
|
||||
odb.path,
|
||||
repo.fs,
|
||||
repo.root_dir,
|
||||
)
|
||||
@@ -88,9 +97,29 @@ def _get_linktype_support_info(repo):
|
||||
return ", ".join(links)
|
||||
|
||||
|
||||
def _get_subprojects():
|
||||
subprojects = []
|
||||
for subproject in SUBPROJECTS:
|
||||
try:
|
||||
version = importlib_metadata.version(subproject)
|
||||
subprojects.append(f"{subproject} = {version}")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return "\n\t" + "\n\t".join(subprojects)
|
||||
|
||||
|
||||
def _get_supported_remotes():
|
||||
supported_remotes = []
|
||||
for scheme, fs_cls in FS_MAP.items():
|
||||
for scheme in registry:
|
||||
if scheme in [Schemes.LOCAL, Schemes.MEMORY, "dvc", "git"]:
|
||||
continue
|
||||
|
||||
try:
|
||||
fs_cls = registry[scheme]
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
if not fs_cls.get_missing_deps():
|
||||
dependencies = []
|
||||
for requirement in fs_cls.REQUIRES:
|
||||
|
||||
+11
@@ -70,6 +70,8 @@ class LockNoop(LockBase):
|
||||
self._lock = True
|
||||
|
||||
def unlock(self):
|
||||
if not self.is_locked:
|
||||
raise DvcException("Unlock called on an unlocked lock")
|
||||
self._lock = False
|
||||
|
||||
@property
|
||||
@@ -93,6 +95,7 @@ class Lock(LockBase):
|
||||
super().__init__(lockfile)
|
||||
self._friendly = friendly
|
||||
self._lock = None
|
||||
self._lock_failed = False
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
@@ -100,6 +103,7 @@ class Lock(LockBase):
|
||||
|
||||
def _do_lock(self):
|
||||
try:
|
||||
self._lock_failed = False
|
||||
with Tqdm(
|
||||
bar_format="{desc}",
|
||||
disable=not self._friendly,
|
||||
@@ -111,6 +115,7 @@ class Lock(LockBase):
|
||||
):
|
||||
self._lock = zc.lockfile.LockFile(self._lockfile)
|
||||
except zc.lockfile.LockError:
|
||||
self._lock_failed = True
|
||||
raise LockError(FAILED_TO_LOCK_MESSAGE)
|
||||
|
||||
def lock(self):
|
||||
@@ -120,6 +125,12 @@ class Lock(LockBase):
|
||||
lock_retry()
|
||||
|
||||
def unlock(self):
|
||||
if self._lock_failed:
|
||||
assert self._lock is None
|
||||
return
|
||||
|
||||
if not self.is_locked:
|
||||
raise DvcException("Unlock called on an unlocked lock")
|
||||
self._lock.close()
|
||||
self._lock = None
|
||||
|
||||
|
||||
+1
-1
@@ -163,7 +163,7 @@ class LoggerHandler(logging.StreamHandler):
|
||||
msg, file=self.stream, end=getattr(self, "terminator", "\n")
|
||||
)
|
||||
self.flush()
|
||||
except RecursionError:
|
||||
except (BrokenPipeError, RecursionError):
|
||||
raise
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
self.handleError(record)
|
||||
|
||||
@@ -3,7 +3,8 @@ from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import SSHFileSystem
|
||||
from dvc_ssh import SSHFileSystem
|
||||
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ from contextlib import contextmanager
|
||||
from functools import partial, partialmethod
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
from dvc_ssh import DEFAULT_PORT, SSHFileSystem
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.fs import DEFAULT_SSH_PORT, SSHFileSystem
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
from .base import BaseMachineBackend
|
||||
@@ -68,7 +69,7 @@ class TerraformBackend(BaseMachineBackend):
|
||||
resource = tpi.default_resource(name)
|
||||
return {
|
||||
"host": resource["instance_ip"],
|
||||
"port": DEFAULT_SSH_PORT,
|
||||
"port": DEFAULT_PORT,
|
||||
"username": "ubuntu",
|
||||
"fs_factory": partial(_sshfs, dict(resource)),
|
||||
}
|
||||
|
||||
+14
-7
@@ -1,8 +1,8 @@
|
||||
from dvc.fs import Schemes
|
||||
from dvc_data.db import get_odb
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
|
||||
def _get_odb(repo, settings):
|
||||
def _get_odb(repo, settings, fs=None):
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
if not settings:
|
||||
@@ -10,7 +10,8 @@ def _get_odb(repo, settings):
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(repo, **settings)
|
||||
config["tmp_dir"] = repo.tmp_dir
|
||||
return get_odb(cls(**config), fs_path, state=repo.state, **config)
|
||||
fs = fs or cls(**config)
|
||||
return get_odb(fs, fs_path, state=repo.state, **config)
|
||||
|
||||
|
||||
class ODBManager:
|
||||
@@ -24,7 +25,7 @@ class ODBManager:
|
||||
]
|
||||
|
||||
def __init__(self, repo):
|
||||
self.repo = repo
|
||||
self._repo = repo
|
||||
self.config = config = repo.config["cache"]
|
||||
self._odb = {}
|
||||
|
||||
@@ -42,13 +43,19 @@ class ODBManager:
|
||||
if opt in config:
|
||||
settings[str(opt)] = config.get(opt)
|
||||
|
||||
self._odb[Schemes.LOCAL] = _get_odb(repo, settings)
|
||||
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)
|
||||
self._odb[scheme] = _get_odb(self._repo, settings)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self._odb and name in self.CLOUD_SCHEMES:
|
||||
|
||||
+123
-84
@@ -17,25 +17,19 @@ from dvc.exceptions import (
|
||||
MergeError,
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc_data import check as ocheck
|
||||
from dvc_data import load as oload
|
||||
from dvc_data.checkout import checkout
|
||||
from dvc_data.stage import stage as ostage
|
||||
from dvc_data.transfer import transfer as otransfer
|
||||
from dvc_data.tree import Tree
|
||||
from dvc_data.hashfile import Tree
|
||||
from dvc_data.hashfile import check as ocheck
|
||||
from dvc_data.hashfile import load as oload
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.istextfile import istextfile
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.hashfile.transfer import transfer as otransfer
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
from dvc_objects.hash_info import HashInfo
|
||||
from dvc_objects.istextfile import istextfile
|
||||
from dvc_objects.meta import Meta
|
||||
|
||||
from .fs import (
|
||||
HDFSFileSystem,
|
||||
LocalFileSystem,
|
||||
RemoteMissingDepsError,
|
||||
S3FileSystem,
|
||||
Schemes,
|
||||
get_cloud_fs,
|
||||
)
|
||||
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
|
||||
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
|
||||
from .utils import relpath
|
||||
from .utils.fs import path_isin
|
||||
|
||||
@@ -65,10 +59,12 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
|
||||
#
|
||||
# so when a few types of outputs share the same name, we only need
|
||||
# specify it once.
|
||||
HDFS_PARAM_CHECKSUM = "checksum"
|
||||
S3_PARAM_CHECKSUM = "etag"
|
||||
CHECKSUMS_SCHEMA = {
|
||||
LocalFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
HDFSFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3FileSystem.PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
|
||||
HDFS_PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3_PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
|
||||
}
|
||||
|
||||
|
||||
@@ -85,9 +81,9 @@ def loadd_from(stage, d_list):
|
||||
plot = d.pop(Output.PARAM_PLOT, False)
|
||||
persist = d.pop(Output.PARAM_PERSIST, False)
|
||||
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
|
||||
desc = d.pop(Output.PARAM_DESC, False)
|
||||
live = d.pop(Output.PARAM_LIVE, False)
|
||||
remote = d.pop(Output.PARAM_REMOTE, None)
|
||||
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
|
||||
ret.append(
|
||||
_get(
|
||||
stage,
|
||||
@@ -98,9 +94,9 @@ def loadd_from(stage, d_list):
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
desc=desc,
|
||||
live=live,
|
||||
remote=remote,
|
||||
**annot,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
@@ -192,6 +188,7 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
Output.PARAM_REMOTE,
|
||||
*ANNOTATION_FIELDS,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -257,7 +254,6 @@ class Output:
|
||||
PARAM_PLOT_TITLE = "title"
|
||||
PARAM_PLOT_HEADER = "header"
|
||||
PARAM_PERSIST = "persist"
|
||||
PARAM_DESC = "desc"
|
||||
PARAM_LIVE = "live"
|
||||
PARAM_LIVE_SUMMARY = "summary"
|
||||
PARAM_LIVE_HTML = "html"
|
||||
@@ -289,11 +285,25 @@ class Output:
|
||||
checkpoint=False,
|
||||
live=False,
|
||||
desc=None,
|
||||
type=None, # pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
meta=None,
|
||||
remote=None,
|
||||
repo=None,
|
||||
):
|
||||
self.annot = Annotation(
|
||||
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
||||
)
|
||||
self.repo = stage.repo if not repo and stage else repo
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(self.repo, url=path)
|
||||
meta = Meta.from_dict(info or {})
|
||||
# NOTE: when version_aware is not passed into get_cloud_fs, it will be
|
||||
# set based on whether or not path is versioned
|
||||
fs_kwargs = {"version_aware": True} if meta.version_id else {}
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(
|
||||
self.repo,
|
||||
url=path,
|
||||
**fs_kwargs,
|
||||
)
|
||||
self.fs = fs_cls(**fs_config)
|
||||
|
||||
if (
|
||||
@@ -325,38 +335,52 @@ class Output:
|
||||
# By resolved path, which contains actual location,
|
||||
# should be absolute and don't contain remote:// refs.
|
||||
self.stage = stage
|
||||
self.meta = Meta.from_dict(info)
|
||||
self.hash_info = HashInfo.from_dict(info)
|
||||
self.meta = meta
|
||||
self.use_cache = False if self.IS_DEPENDENCY else cache
|
||||
self.metric = False if self.IS_DEPENDENCY else metric
|
||||
self.plot = False if self.IS_DEPENDENCY else plot
|
||||
self.persist = persist
|
||||
self.checkpoint = checkpoint
|
||||
self.live = live
|
||||
self.desc = desc
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj = None
|
||||
|
||||
self.remote = remote
|
||||
|
||||
def _parse_path(self, fs, fs_path):
|
||||
if fs.protocol != "local":
|
||||
return fs_path
|
||||
if self.fs.version_aware:
|
||||
_, version_id = self.fs.path.coalesce_version(
|
||||
self.def_path, self.meta.version_id
|
||||
)
|
||||
self.meta.version_id = version_id
|
||||
|
||||
if self.is_in_repo:
|
||||
self.hash_name = "md5"
|
||||
else:
|
||||
self.hash_name = self.fs.PARAM_CHECKSUM
|
||||
|
||||
self.hash_info = HashInfo(
|
||||
name=self.hash_name,
|
||||
value=getattr(self.meta, self.hash_name, None),
|
||||
)
|
||||
|
||||
def _parse_path(self, fs, fs_path):
|
||||
parsed = urlparse(self.def_path)
|
||||
if parsed.scheme != "remote":
|
||||
if (
|
||||
parsed.scheme != "remote"
|
||||
and self.stage
|
||||
and self.stage.repo.fs == fs
|
||||
and not fs.path.isabs(fs_path)
|
||||
):
|
||||
# NOTE: we can path either from command line or .dvc file,
|
||||
# so we should expect both posix and windows style paths.
|
||||
# paths accepts both, i.e. / works everywhere, \ only on win.
|
||||
#
|
||||
# FIXME: if we have Windows path containing / or posix one with \
|
||||
# then we have #2059 bug and can't really handle that.
|
||||
if self.stage and not os.path.isabs(fs_path):
|
||||
fs_path = fs.path.join(self.stage.wdir, fs_path)
|
||||
fs_path = fs.path.join(self.stage.wdir, fs_path)
|
||||
|
||||
abs_p = fs.path.abspath(fs.path.normpath(fs_path))
|
||||
return abs_p
|
||||
return fs.path.abspath(fs.path.normpath(fs_path))
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_name}: '{def_path}'".format(
|
||||
@@ -380,15 +404,17 @@ class Output:
|
||||
|
||||
return self.fs.path.relpath(self.fs_path, self.repo.root_dir)
|
||||
|
||||
def clear(self):
|
||||
self.hash_info = HashInfo.from_dict({})
|
||||
self.meta = Meta.from_dict({})
|
||||
self.obj = None
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
return self.fs.protocol
|
||||
|
||||
@property
|
||||
def is_in_repo(self):
|
||||
if self.fs.protocol != "local":
|
||||
return False
|
||||
|
||||
if urlparse(self.def_path).scheme == "remote":
|
||||
return False
|
||||
|
||||
@@ -409,7 +435,8 @@ class Output:
|
||||
|
||||
@property
|
||||
def odb(self):
|
||||
odb = getattr(self.repo.odb, self.protocol)
|
||||
odb_name = "repo" if self.is_in_repo else self.protocol
|
||||
odb = getattr(self.repo.odb, odb_name)
|
||||
if self.use_cache and odb is None:
|
||||
raise RemoteCacheRequiredError(self.fs.protocol, self.fs_path)
|
||||
return odb
|
||||
@@ -417,25 +444,31 @@ class Output:
|
||||
@property
|
||||
def cache_path(self):
|
||||
return self.odb.fs.unstrip_protocol(
|
||||
self.odb.hash_to_path(self.hash_info.value)
|
||||
self.odb.oid_to_path(self.hash_info.value)
|
||||
)
|
||||
|
||||
def get_hash(self):
|
||||
_, hash_info = self._get_hash_meta()
|
||||
return hash_info
|
||||
|
||||
def _get_hash_meta(self):
|
||||
if self.use_cache:
|
||||
odb = self.odb
|
||||
name = self.odb.fs.PARAM_CHECKSUM
|
||||
else:
|
||||
odb = self.repo.odb.local
|
||||
name = self.fs.PARAM_CHECKSUM
|
||||
_, _, obj = ostage(
|
||||
_, meta, obj = build(
|
||||
odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
name,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=not self.use_cache,
|
||||
)
|
||||
return obj.hash_info
|
||||
return meta, obj.hash_info
|
||||
|
||||
def get_meta(self) -> Meta:
|
||||
meta, _ = self._get_hash_meta()
|
||||
return meta
|
||||
|
||||
@property
|
||||
def is_dir_checksum(self):
|
||||
@@ -471,6 +504,11 @@ class Output:
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return True
|
||||
|
||||
def changed_meta(self) -> bool:
|
||||
if self.fs.version_aware and self.meta.version_id:
|
||||
return self.meta.version_id == self.get_meta().version_id
|
||||
return False
|
||||
|
||||
def workspace_status(self):
|
||||
if not self.exists:
|
||||
return {str(self): "deleted"}
|
||||
@@ -541,19 +579,19 @@ class Output:
|
||||
raise self.IsNotFileOrDirError(self)
|
||||
|
||||
if self.is_empty:
|
||||
logger.warning(f"'{self}' is empty.")
|
||||
logger.warning("'%s' is empty.", self)
|
||||
|
||||
self.ignore()
|
||||
|
||||
if self.metric or self.plot:
|
||||
if self.metric:
|
||||
self.verify_metric()
|
||||
|
||||
if not self.use_cache:
|
||||
_, self.meta, obj = ostage(
|
||||
_, self.meta, obj = build(
|
||||
self.repo.odb.local,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=True,
|
||||
)
|
||||
@@ -566,11 +604,11 @@ class Output:
|
||||
|
||||
assert not self.IS_DEPENDENCY
|
||||
|
||||
_, self.meta, self.obj = ostage(
|
||||
_, self.meta, self.obj = build(
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
self.hash_info = self.obj.hash_info
|
||||
@@ -580,9 +618,10 @@ class Output:
|
||||
self.odb.set_exec(self.fs_path)
|
||||
|
||||
def _checkout(self, *args, **kwargs):
|
||||
from dvc_data.checkout import CheckoutError as _CheckoutError
|
||||
from dvc_data.checkout import LinkError, PromptError
|
||||
from dvc_data.hashfile.checkout import CheckoutError as _CheckoutError
|
||||
from dvc_data.hashfile.checkout import LinkError, PromptError
|
||||
|
||||
kwargs.setdefault("ignore", self.dvcignore)
|
||||
try:
|
||||
return checkout(*args, **kwargs)
|
||||
except PromptError as exc:
|
||||
@@ -607,11 +646,11 @@ class Output:
|
||||
if granular:
|
||||
obj = self._commit_granular_dir(filter_info)
|
||||
else:
|
||||
staging, _, obj = ostage(
|
||||
staging, _, obj = build(
|
||||
self.odb,
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
otransfer(
|
||||
@@ -627,7 +666,6 @@ class Output:
|
||||
obj,
|
||||
self.odb,
|
||||
relink=True,
|
||||
ignore=self.dvcignore,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
)
|
||||
@@ -637,15 +675,15 @@ class Output:
|
||||
prefix = self.fs.path.parts(
|
||||
self.fs.path.relpath(filter_info, self.fs_path)
|
||||
)
|
||||
staging, _, save_obj = ostage(
|
||||
staging, _, save_obj = build(
|
||||
self.odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
save_obj = save_obj.filter(prefix)
|
||||
checkout_obj = save_obj.get(self.odb, prefix)
|
||||
checkout_obj = save_obj.get_obj(self.odb, prefix)
|
||||
otransfer(
|
||||
staging,
|
||||
self.odb,
|
||||
@@ -670,9 +708,7 @@ class Output:
|
||||
if self.IS_DEPENDENCY:
|
||||
return ret
|
||||
|
||||
if self.desc:
|
||||
ret[self.PARAM_DESC] = self.desc
|
||||
|
||||
ret.update(self.annot.to_dict())
|
||||
if not self.use_cache:
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
|
||||
@@ -708,22 +744,21 @@ class Output:
|
||||
raise DvcException(
|
||||
f"verify metric is not supported for {self.protocol}"
|
||||
)
|
||||
|
||||
if not self.metric or self.plot:
|
||||
if not self.metric:
|
||||
return
|
||||
|
||||
if not os.path.exists(self.fs_path):
|
||||
return
|
||||
|
||||
name = "metrics" if self.metric else "plot"
|
||||
if os.path.isdir(self.fs_path):
|
||||
msg = "directory '%s' cannot be used as %s."
|
||||
logger.debug(msg, str(self), name)
|
||||
logger.debug(msg, str(self), "metrics")
|
||||
return
|
||||
|
||||
if not istextfile(self.fs_path, self.fs):
|
||||
msg = "binary file '{}' cannot be used as {}."
|
||||
raise DvcException(msg.format(self.fs_path, name))
|
||||
raise DvcException(
|
||||
f"binary file '{self.fs_path}' cannot be used as metrics."
|
||||
)
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc.fs.callbacks import Callback
|
||||
@@ -740,7 +775,7 @@ class Output:
|
||||
elif self.hash_info:
|
||||
try:
|
||||
obj = oload(self.odb, self.hash_info)
|
||||
except FileNotFoundError:
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
@@ -748,7 +783,7 @@ class Output:
|
||||
fs_path = self.fs.path
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
prefix = fs_path.relparts(filter_info, self.fs_path)
|
||||
obj = obj.get(self.odb, prefix)
|
||||
obj = obj.get_obj(self.odb, prefix)
|
||||
|
||||
return obj
|
||||
|
||||
@@ -843,13 +878,12 @@ class Output:
|
||||
|
||||
upload = not (update and from_fs.isdir(from_info))
|
||||
jobs = jobs or min((from_fs.jobs, odb.fs.jobs))
|
||||
staging, self.meta, obj = ostage(
|
||||
staging, self.meta, obj = build(
|
||||
odb,
|
||||
from_info,
|
||||
from_fs,
|
||||
"md5",
|
||||
upload=upload,
|
||||
jobs=jobs,
|
||||
no_progress_bar=no_progress_bar,
|
||||
)
|
||||
otransfer(
|
||||
@@ -885,7 +919,7 @@ class Output:
|
||||
if not self.is_dir_checksum:
|
||||
raise DvcException("cannot get dir cache for file checksum")
|
||||
|
||||
obj = self.odb.get(self.hash_info)
|
||||
obj = self.odb.get(self.hash_info.value)
|
||||
try:
|
||||
ocheck(self.odb, obj)
|
||||
except FileNotFoundError:
|
||||
@@ -913,10 +947,10 @@ class Output:
|
||||
except RemoteMissingDepsError: # pylint: disable=try-except-raise
|
||||
raise
|
||||
except DvcException:
|
||||
logger.debug(f"failed to pull cache for '{self}'")
|
||||
logger.debug("failed to pull cache for '%s'", self)
|
||||
|
||||
try:
|
||||
ocheck(self.odb, self.odb.get(self.hash_info))
|
||||
ocheck(self.odb, self.odb.get(self.hash_info.value))
|
||||
except FileNotFoundError:
|
||||
msg = (
|
||||
"Missing cache for directory '{}'. "
|
||||
@@ -973,7 +1007,7 @@ class Output:
|
||||
else:
|
||||
obj = self.get_obj(filter_info=kwargs.get("filter_info"))
|
||||
if not obj:
|
||||
obj = self.odb.get(self.hash_info)
|
||||
obj = self.odb.get(self.hash_info.value)
|
||||
|
||||
if not obj:
|
||||
return {}
|
||||
@@ -1024,7 +1058,7 @@ class Output:
|
||||
other = out.dumpd()
|
||||
|
||||
ignored = [
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
self.hash_name,
|
||||
Meta.PARAM_SIZE,
|
||||
Meta.PARAM_NFILES,
|
||||
]
|
||||
@@ -1044,8 +1078,8 @@ class Output:
|
||||
)
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
from dvc_data.tree import MergeError as TreeMergeError
|
||||
from dvc_data.tree import du, merge
|
||||
from dvc_data.hashfile.tree import MergeError as TreeMergeError
|
||||
from dvc_data.hashfile.tree import du, merge
|
||||
|
||||
assert other
|
||||
|
||||
@@ -1065,7 +1099,7 @@ class Output:
|
||||
except TreeMergeError as exc:
|
||||
raise MergeError(str(exc)) from exc
|
||||
|
||||
self.odb.add(merged.fs_path, merged.fs, merged.hash_info)
|
||||
self.odb.add(merged.path, merged.fs, merged.oid)
|
||||
|
||||
self.hash_info = merged.hash_info
|
||||
self.meta = Meta(
|
||||
@@ -1087,24 +1121,29 @@ class Output:
|
||||
|
||||
@property
|
||||
def is_plot(self) -> bool:
|
||||
return bool(self.plot)
|
||||
return bool(self.plot) or bool(self.live)
|
||||
|
||||
|
||||
META_SCHEMA = {
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
Meta.PARAM_VERSION_ID: str,
|
||||
}
|
||||
|
||||
ARTIFACT_SCHEMA = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required(Output.PARAM_PATH): str,
|
||||
Output.PARAM_PLOT: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Meta.PARAM_ISEXEC: bool,
|
||||
}
|
||||
|
||||
SCHEMA = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**ANNOTATION_SCHEMA,
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
|
||||
Output.PARAM_DESC: str,
|
||||
Output.PARAM_REMOTE: str,
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ class EntryDefinition:
|
||||
) -> DictStr:
|
||||
try:
|
||||
return context.resolve(
|
||||
value, skip_interpolation_checks=skip_checks
|
||||
value, skip_interpolation_checks=skip_checks, key=key
|
||||
)
|
||||
except (ParseError, KeyNotInContext) as exc:
|
||||
format_and_raise(
|
||||
|
||||
+11
-12
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence
|
||||
@@ -18,6 +17,7 @@ from dvc.parsing.interpolate import (
|
||||
normalize_key,
|
||||
recurse,
|
||||
str_interpolate,
|
||||
validate_value,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -159,7 +159,7 @@ class Value(Node):
|
||||
PRIMITIVES = (int, float, str, bytes, bool)
|
||||
|
||||
|
||||
class Container(Node, ABC):
|
||||
class Container(Node, ABC): # noqa: B024
|
||||
meta: Meta
|
||||
data: Union[list, dict]
|
||||
_key_transform = staticmethod(identity)
|
||||
@@ -356,17 +356,14 @@ class Context(CtxDict):
|
||||
def load_from(
|
||||
cls, fs, path: str, select_keys: List[str] = None
|
||||
) -> "Context":
|
||||
from dvc.utils.serialize import LOADERS
|
||||
from dvc.utils.serialize import load_path
|
||||
|
||||
if not fs.exists(path):
|
||||
raise ParamsLoadError(f"'{path}' does not exist")
|
||||
if fs.isdir(path):
|
||||
raise ParamsLoadError(f"'{path}' is a directory")
|
||||
|
||||
_, ext = os.path.splitext(path)
|
||||
loader = LOADERS[ext]
|
||||
|
||||
data = loader(path, fs=fs)
|
||||
data = load_path(path, fs)
|
||||
if not isinstance(data, Mapping):
|
||||
typ = type(data).__name__
|
||||
raise ParamsLoadError(
|
||||
@@ -506,7 +503,7 @@ class Context(CtxDict):
|
||||
self.data.pop(key, None)
|
||||
|
||||
def resolve(
|
||||
self, src, unwrap=True, skip_interpolation_checks=False
|
||||
self, src, unwrap=True, skip_interpolation_checks=False, key=None
|
||||
) -> Any:
|
||||
"""Recursively resolves interpolation and returns resolved data.
|
||||
|
||||
@@ -522,10 +519,10 @@ class Context(CtxDict):
|
||||
{'lst': [1, 2, 3]}
|
||||
"""
|
||||
func = recurse(self.resolve_str)
|
||||
return func(src, unwrap, skip_interpolation_checks)
|
||||
return func(src, unwrap, skip_interpolation_checks, key)
|
||||
|
||||
def resolve_str(
|
||||
self, src: str, unwrap=True, skip_interpolation_checks=False
|
||||
self, src: str, unwrap=True, skip_interpolation_checks=False, key=None
|
||||
) -> str:
|
||||
"""Resolves interpolated string to it's original value,
|
||||
or in case of multiple interpolations, a combined string.
|
||||
@@ -543,10 +540,12 @@ class Context(CtxDict):
|
||||
expr = get_expression(
|
||||
matches[0], skip_checks=skip_interpolation_checks
|
||||
)
|
||||
return self.select(expr, unwrap=unwrap)
|
||||
value = self.select(expr, unwrap=unwrap)
|
||||
validate_value(value, key)
|
||||
return value
|
||||
# but not "${num} days"
|
||||
return str_interpolate(
|
||||
src, matches, self, skip_checks=skip_interpolation_checks
|
||||
src, matches, self, skip_checks=skip_interpolation_checks, key=key
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import re
|
||||
import typing
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Iterable, Mapping
|
||||
from functools import singledispatch
|
||||
|
||||
from funcy import memoize, rpartial
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils.flatten import flatten
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import List, Match
|
||||
@@ -80,6 +81,45 @@ def _(obj: bool):
|
||||
return "true" if obj else "false"
|
||||
|
||||
|
||||
@to_str.register(dict)
|
||||
def _(obj: dict):
|
||||
from dvc.config import Config
|
||||
|
||||
config = Config().get("parsing", {})
|
||||
|
||||
result = ""
|
||||
for k, v in flatten(obj).items():
|
||||
|
||||
if isinstance(v, bool):
|
||||
if v:
|
||||
result += f"--{k} "
|
||||
else:
|
||||
if config.get("bool", "store_true") == "boolean_optional":
|
||||
result += f"--no-{k} "
|
||||
|
||||
elif isinstance(v, str):
|
||||
result += f"--{k} '{v}' "
|
||||
|
||||
elif isinstance(v, Iterable):
|
||||
for n, i in enumerate(v):
|
||||
if isinstance(i, str):
|
||||
i = f"'{i}'"
|
||||
elif isinstance(i, Iterable):
|
||||
raise ParseError(
|
||||
f"Cannot interpolate nested iterable in '{k}'"
|
||||
)
|
||||
|
||||
if config.get("list", "nargs") == "append":
|
||||
result += f"--{k} {i} "
|
||||
else:
|
||||
result += f"{i} " if n > 0 else f"--{k} {i} "
|
||||
|
||||
else:
|
||||
result += f"--{k} {v} "
|
||||
|
||||
return result.rstrip()
|
||||
|
||||
|
||||
def _format_exc_msg(exc: "ParseException"):
|
||||
from pyparsing import ParseException
|
||||
|
||||
@@ -148,23 +188,33 @@ def get_expression(match: "Match", skip_checks: bool = False):
|
||||
return inner if skip_checks else parse_expr(inner)
|
||||
|
||||
|
||||
def validate_value(value, key):
|
||||
from .context import PRIMITIVES
|
||||
|
||||
not_primitive = value is not None and not isinstance(value, PRIMITIVES)
|
||||
not_foreach = key is not None and "foreach" not in key
|
||||
if not_primitive and not_foreach:
|
||||
if isinstance(value, dict):
|
||||
if key == "cmd":
|
||||
return True
|
||||
raise ParseError(
|
||||
f"Cannot interpolate data of type '{type(value).__name__}'"
|
||||
)
|
||||
|
||||
|
||||
def str_interpolate(
|
||||
template: str,
|
||||
matches: "List[Match]",
|
||||
context: "Context",
|
||||
skip_checks: bool = False,
|
||||
key=None,
|
||||
):
|
||||
from .context import PRIMITIVES
|
||||
|
||||
index, buf = 0, ""
|
||||
for match in matches:
|
||||
start, end = match.span(0)
|
||||
expr = get_expression(match, skip_checks=skip_checks)
|
||||
value = context.select(expr, unwrap=True)
|
||||
if value is not None and not isinstance(value, PRIMITIVES):
|
||||
raise ParseError(
|
||||
f"Cannot interpolate data of type '{type(value).__name__}'"
|
||||
)
|
||||
validate_value(value, key)
|
||||
buf += template[index:start] + to_str(value)
|
||||
index = end
|
||||
buf += template[index:]
|
||||
|
||||
+1
-1
@@ -52,5 +52,5 @@ def password(statement):
|
||||
Returns:
|
||||
str: password entered by the user.
|
||||
"""
|
||||
logger.info(f"{statement}: ")
|
||||
logger.info("%s: ", statement)
|
||||
return getpass("")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
INDEX_FIELD = "step"
|
||||
REVISION_FIELD = "rev"
|
||||
FILENAME_FIELD = "filename"
|
||||
VERSION_FIELD = "dvc_data_version_info"
|
||||
REVISIONS_KEY = "revisions"
|
||||
TYPE_KEY = "type"
|
||||
SRC_FIELD = "src"
|
||||
|
||||
+2
-16
@@ -3,8 +3,8 @@ from collections import defaultdict
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from dvc.render import REVISION_FIELD, REVISIONS_KEY, SRC_FIELD, TYPE_KEY
|
||||
from dvc.render.image_converter import ImageConverter
|
||||
from dvc.render.vega_converter import VegaConverter
|
||||
from dvc.render.converter.image import ImageConverter
|
||||
from dvc.render.converter.vega import VegaConverter
|
||||
|
||||
|
||||
def _get_converter(
|
||||
@@ -20,20 +20,6 @@ def _get_converter(
|
||||
raise ValueError(f"Invalid renderer class {renderer_class}")
|
||||
|
||||
|
||||
def to_datapoints(renderer_class, data: Dict, props: Dict):
|
||||
converter = _get_converter(renderer_class, props)
|
||||
datapoints: List[Dict] = []
|
||||
final_props: Dict = {}
|
||||
for revision, rev_data in data.items():
|
||||
for filename, file_data in rev_data.get("data", {}).items():
|
||||
if "data" in file_data:
|
||||
processed, final_props = converter.convert(
|
||||
file_data.get("data"), revision, filename
|
||||
)
|
||||
datapoints.extend(processed)
|
||||
return datapoints, final_props
|
||||
|
||||
|
||||
def _group_by_rev(datapoints):
|
||||
grouped = defaultdict(list)
|
||||
for datapoint in datapoints:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class Converter:
|
||||
def __init__(self, plot_properties: Optional[Dict] = None):
|
||||
self.plot_properties = plot_properties or {}
|
||||
|
||||
def convert(self, data, revision: str, filename: str, **kwargs):
|
||||
raise NotImplementedError
|
||||
@@ -1,17 +1,16 @@
|
||||
import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, List, Tuple
|
||||
|
||||
from dvc.render import FILENAME_FIELD, REVISION_FIELD, SRC_FIELD
|
||||
|
||||
from . import Converter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
class ImageConverter:
|
||||
def __init__(self, plot_properties: Optional[Dict] = None):
|
||||
self.plot_properties = plot_properties or {}
|
||||
|
||||
class ImageConverter(Converter):
|
||||
@staticmethod
|
||||
def _write_image(
|
||||
path: "StrPath",
|
||||
@@ -20,7 +19,8 @@ class ImageConverter:
|
||||
image_data: bytes,
|
||||
) -> "StrPath":
|
||||
img_path = os.path.join(
|
||||
path, f"{revision}_{filename.replace(os.sep, '_')}"
|
||||
path,
|
||||
f"{revision}_{filename}".replace(os.sep, "_").replace("/", "_"),
|
||||
)
|
||||
with open(img_path, "wb") as fd:
|
||||
fd.write(image_data)
|
||||
@@ -35,7 +35,7 @@ class ImageConverter:
|
||||
return f"data:image;base64,{base64_str}"
|
||||
|
||||
def convert(
|
||||
self, data: bytes, revision, filename
|
||||
self, data, revision: str, filename: str, **kwargs
|
||||
) -> Tuple[List[Dict], Dict]:
|
||||
"""
|
||||
Convert the DVC Plots content to DVC Render datapoints.
|
||||
@@ -0,0 +1,316 @@
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from typing import Dict, Iterable, List, Optional, Set, Union
|
||||
|
||||
from funcy import first, project
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.render import (
|
||||
FILENAME_FIELD,
|
||||
INDEX_FIELD,
|
||||
REVISION_FIELD,
|
||||
VERSION_FIELD,
|
||||
)
|
||||
|
||||
from . import Converter
|
||||
|
||||
|
||||
class FieldsNotFoundError(DvcException):
|
||||
def __init__(self, expected_fields, found_fields):
|
||||
expected_str = ", ".join(expected_fields)
|
||||
found_str = ", ".join(found_fields)
|
||||
super().__init__(
|
||||
f"Could not find all provided fields ('{expected_str}') "
|
||||
f"in data fields ('{found_str}')."
|
||||
)
|
||||
|
||||
|
||||
class PlotDataStructureError(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"Plot data extraction failed. Please see "
|
||||
"https://man.dvc.org/plots for supported data formats."
|
||||
)
|
||||
|
||||
|
||||
def _filter_fields(
|
||||
datapoints: List[Dict], fields: Set, **kwargs
|
||||
) -> List[Dict]:
|
||||
if not fields:
|
||||
return datapoints
|
||||
assert isinstance(fields, set)
|
||||
|
||||
new_data = []
|
||||
for data_point in datapoints:
|
||||
keys = set(data_point.keys())
|
||||
if not fields <= keys:
|
||||
raise FieldsNotFoundError(fields, keys)
|
||||
|
||||
new_data.append(project(data_point, fields))
|
||||
|
||||
return new_data
|
||||
|
||||
|
||||
def _lists(dictionary: Dict):
|
||||
for _, value in dictionary.items():
|
||||
if isinstance(value, dict):
|
||||
yield from _lists(value)
|
||||
elif isinstance(value, list):
|
||||
yield value
|
||||
|
||||
|
||||
def _find_first_list(
|
||||
data: Union[Dict, List], fields: Set, **kwargs
|
||||
) -> List[Dict]:
|
||||
fields = fields or set()
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
for lst in _lists(data):
|
||||
if (
|
||||
all(isinstance(dp, dict) for dp in lst)
|
||||
# if fields is empty, it will match any set
|
||||
and set(first(lst).keys()) & fields == fields
|
||||
):
|
||||
return lst
|
||||
|
||||
raise PlotDataStructureError()
|
||||
|
||||
|
||||
def _append_index(datapoints: List[Dict], **kwargs) -> List[Dict]:
|
||||
if INDEX_FIELD in first(datapoints).keys():
|
||||
return datapoints
|
||||
|
||||
for index, data_point in enumerate(datapoints):
|
||||
data_point[INDEX_FIELD] = index
|
||||
return datapoints
|
||||
|
||||
|
||||
class VegaConverter(Converter):
|
||||
"""
|
||||
Class that takes care of converting unspecified data blob
|
||||
(Dict or List[Dict]) into datapoints (List[Dict]).
|
||||
If some properties that are required by Template class are missing
|
||||
('x', 'y') it will attempt to fill in the blanks.
|
||||
"""
|
||||
|
||||
def __init__(self, plot_properties: Optional[Dict] = None):
|
||||
super().__init__(plot_properties)
|
||||
self.inferred_properties: Dict = {}
|
||||
|
||||
self.steps = []
|
||||
|
||||
self._infer_x()
|
||||
self._infer_fields()
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"find_data",
|
||||
partial(
|
||||
_find_first_list,
|
||||
fields=self.inferred_properties.get("fields", set())
|
||||
- {INDEX_FIELD},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if not self.plot_properties.get("x", None):
|
||||
self.steps.append(("append_index", partial(_append_index)))
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"filter_fields",
|
||||
partial(
|
||||
_filter_fields,
|
||||
fields=self.inferred_properties.get("fields", set()),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"generate_y",
|
||||
partial(
|
||||
self._generate_y_values,
|
||||
y_values=self.plot_properties.get("y", None),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def _infer_x(self):
|
||||
if not self.plot_properties.get("x", None):
|
||||
self.inferred_properties["x"] = INDEX_FIELD
|
||||
|
||||
def skip_step(self, name: str):
|
||||
self.steps = [(_name, fn) for _name, fn in self.steps if _name != name]
|
||||
|
||||
def _infer_fields(self):
|
||||
fields = self.plot_properties.get("fields", set())
|
||||
if fields:
|
||||
fields = {
|
||||
*fields,
|
||||
self.plot_properties.get("x", None),
|
||||
self.plot_properties.get("y", None),
|
||||
self.inferred_properties.get("x", None),
|
||||
} - {None}
|
||||
self.inferred_properties["fields"] = fields
|
||||
|
||||
def _infer_y(self, datapoints: List[Dict]):
|
||||
if "y" not in self.plot_properties:
|
||||
data_fields = list(first(datapoints))
|
||||
skip = (
|
||||
REVISION_FIELD,
|
||||
self.plot_properties.get("x", None)
|
||||
or self.inferred_properties.get("x"),
|
||||
FILENAME_FIELD,
|
||||
VERSION_FIELD,
|
||||
)
|
||||
inferred_y = first(
|
||||
f for f in reversed(data_fields) if f not in skip
|
||||
)
|
||||
if "y" in self.inferred_properties:
|
||||
previous_y = self.inferred_properties["y"]
|
||||
if previous_y != inferred_y:
|
||||
raise DvcException(
|
||||
f"Inferred y ('{inferred_y}' value does not match"
|
||||
f"previously matched one ('f{previous_y}')."
|
||||
)
|
||||
else:
|
||||
self.inferred_properties["y"] = inferred_y
|
||||
|
||||
def convert(
|
||||
self,
|
||||
data,
|
||||
revision: str,
|
||||
filename: str,
|
||||
skip: List = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Convert the data. Fill necessary fields ('x', 'y') and return both
|
||||
generated datapoints and updated properties.
|
||||
"""
|
||||
if not skip:
|
||||
skip = []
|
||||
|
||||
processed = deepcopy(data)
|
||||
|
||||
for step_name, step in self.steps:
|
||||
|
||||
if step_name not in skip:
|
||||
processed = step( # type: ignore
|
||||
processed,
|
||||
revision=revision,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
self._infer_y(processed) # type: ignore
|
||||
return processed, {**self.plot_properties, **self.inferred_properties}
|
||||
|
||||
def _generate_y_values( # noqa: C901
|
||||
self,
|
||||
datapoints: List[Dict],
|
||||
y_values: Optional[Union[str, List, Dict]],
|
||||
revision: str,
|
||||
filename: str,
|
||||
**kwargs,
|
||||
) -> List[Dict]:
|
||||
|
||||
result = []
|
||||
properties_update = {}
|
||||
|
||||
def _add_version_info(datapoint, version_info):
|
||||
tmp = datapoint.copy()
|
||||
tmp[VERSION_FIELD] = version_info
|
||||
tmp[REVISION_FIELD] = version_info["revision"]
|
||||
return tmp
|
||||
|
||||
def _version_info(revision, filename=None, field=None):
|
||||
res = {"revision": revision}
|
||||
if filename is not None:
|
||||
res["filename"] = filename
|
||||
if field is not None:
|
||||
res["field"] = field
|
||||
return res
|
||||
|
||||
def _generate_y(datapoint, field):
|
||||
tmp = datapoint.copy()
|
||||
tmp["dvc_inferred_y_value"] = datapoint[field]
|
||||
tmp = _add_version_info(
|
||||
tmp, _version_info(revision, filename, field)
|
||||
)
|
||||
if (
|
||||
"y_label" not in properties_update
|
||||
and "y_label" not in self.plot_properties
|
||||
):
|
||||
properties_update["y_label"] = "y"
|
||||
|
||||
properties_update["y"] = "dvc_inferred_y_value"
|
||||
|
||||
return tmp
|
||||
|
||||
if not y_values:
|
||||
for dp in datapoints:
|
||||
result.append(
|
||||
_add_version_info(dp, _version_info(revision, filename))
|
||||
)
|
||||
|
||||
if isinstance(y_values, str):
|
||||
for datapoint in datapoints:
|
||||
result.append(
|
||||
_add_version_info(
|
||||
datapoint, _version_info(revision, filename, y_values)
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(y_values, list):
|
||||
for datapoint in datapoints:
|
||||
for y_val in y_values:
|
||||
if y_val in datapoint:
|
||||
result.append(_generate_y(datapoint, y_val))
|
||||
|
||||
if isinstance(y_values, dict):
|
||||
|
||||
def _to_set(values: Iterable):
|
||||
result = set()
|
||||
for val in values:
|
||||
if isinstance(val, list):
|
||||
for elem in val:
|
||||
result.add(elem)
|
||||
else:
|
||||
result.add(val)
|
||||
|
||||
return result
|
||||
|
||||
all_fields = _to_set(y_values.values())
|
||||
if (
|
||||
all([isinstance(field, str) for field in all_fields])
|
||||
and len(all_fields) == 1
|
||||
):
|
||||
# if we use the same field from all files,
|
||||
# we dont have to generate it
|
||||
for datapoint in datapoints:
|
||||
result.append(
|
||||
_add_version_info(
|
||||
datapoint, _version_info(revision, filename)
|
||||
)
|
||||
)
|
||||
properties_update.update({"y": all_fields.pop()})
|
||||
else:
|
||||
for def_filename, val in y_values.items():
|
||||
if isinstance(val, str):
|
||||
fields = [val]
|
||||
if isinstance(val, list):
|
||||
fields = val
|
||||
for datapoint in datapoints:
|
||||
for field in fields:
|
||||
if field in datapoint and def_filename in filename:
|
||||
result.append(_generate_y(datapoint, field))
|
||||
|
||||
self.inferred_properties = {
|
||||
**self.inferred_properties,
|
||||
**properties_update,
|
||||
}
|
||||
|
||||
return result
|
||||
+92
-40
@@ -1,58 +1,110 @@
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
import dpath.options
|
||||
import dpath.util
|
||||
from funcy import last
|
||||
|
||||
from dvc_render import RENDERERS
|
||||
from dvc.repo.plots import infer_data_sources
|
||||
from dvc.utils.plots import get_plot_id
|
||||
|
||||
from .convert import to_datapoints
|
||||
from .convert import _get_converter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
def group_by_filename(plots_data: Dict) -> Dict:
|
||||
grouped: Dict[str, Dict] = {}
|
||||
|
||||
for revision in plots_data.keys():
|
||||
data = plots_data[revision].get("data", {})
|
||||
for file in data.keys():
|
||||
content = data.get(file)
|
||||
if content:
|
||||
dpath.util.new(
|
||||
grouped, [file, revision, "data", file], content
|
||||
)
|
||||
|
||||
return grouped
|
||||
dpath.options.ALLOW_EMPTY_STRING_KEYS = True
|
||||
|
||||
|
||||
def squash_plots_properties(data: Dict) -> Dict:
|
||||
resolved: Dict[str, str] = {}
|
||||
for rev_data in data.values():
|
||||
for file_data in rev_data.get("data", {}).values():
|
||||
props = file_data.get("props", {})
|
||||
resolved = {**resolved, **props}
|
||||
def _squash_plots_properties(data: List) -> Dict:
|
||||
configs = [last(group) for group in data]
|
||||
resolved: Dict = {}
|
||||
for config in reversed(configs):
|
||||
resolved = {**resolved, **config}
|
||||
return resolved
|
||||
|
||||
|
||||
def match_renderers(
|
||||
plots_data,
|
||||
out: Optional["StrPath"] = None,
|
||||
class PlotsData:
|
||||
def __init__(self, data: Dict):
|
||||
self.data = data
|
||||
|
||||
def group_definitions(self):
|
||||
groups = defaultdict(list)
|
||||
for rev, rev_content in self.data.items():
|
||||
for config_file, config_file_content in (
|
||||
rev_content.get("definitions", {}).get("data", {}).items()
|
||||
):
|
||||
for plot_id, plot_definition in config_file_content.get(
|
||||
"data", {}
|
||||
).items():
|
||||
full_id = get_plot_id(plot_id, config_file)
|
||||
groups[full_id].append((rev, plot_id, plot_definition))
|
||||
return dict(groups)
|
||||
|
||||
def get_definition_data(self, target_files, rev):
|
||||
result = []
|
||||
for file in target_files:
|
||||
file_content = (
|
||||
self.data.get(rev, {})
|
||||
.get("sources", {})
|
||||
.get("data", {})
|
||||
.get(file, {})
|
||||
.get("data", {})
|
||||
)
|
||||
if file_content:
|
||||
result.append((file, file_content))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def match_defs_renderers(
|
||||
data,
|
||||
out=None,
|
||||
templates_dir: Optional["StrPath"] = None,
|
||||
):
|
||||
|
||||
from dvc_render import ImageRenderer, VegaRenderer
|
||||
|
||||
plots_data = PlotsData(data)
|
||||
renderers = []
|
||||
for filename, group in group_by_filename(plots_data).items():
|
||||
plot_properties = squash_plots_properties(group)
|
||||
for renderer_class in RENDERERS:
|
||||
if renderer_class.matches(filename, plot_properties):
|
||||
if out is not None:
|
||||
plot_properties["out"] = out
|
||||
if templates_dir is not None:
|
||||
plot_properties["template_dir"] = templates_dir
|
||||
datapoints, plot_properties = to_datapoints(
|
||||
renderer_class, group, plot_properties
|
||||
renderer_cls = None
|
||||
for plot_id, group in plots_data.group_definitions().items():
|
||||
plot_datapoints: List[Dict] = []
|
||||
props = _squash_plots_properties(group)
|
||||
final_props: Dict = {}
|
||||
|
||||
if out is not None:
|
||||
props["out"] = out
|
||||
if templates_dir is not None:
|
||||
props["template_dir"] = templates_dir
|
||||
|
||||
for rev, inner_id, plot_definition in group:
|
||||
plot_sources = infer_data_sources(inner_id, plot_definition)
|
||||
definitions_data = plots_data.get_definition_data(
|
||||
plot_sources, rev
|
||||
)
|
||||
|
||||
if ImageRenderer.matches(inner_id, None):
|
||||
renderer_cls = ImageRenderer
|
||||
renderer_id = inner_id
|
||||
else:
|
||||
renderer_cls = VegaRenderer
|
||||
renderer_id = plot_id
|
||||
|
||||
converter = _get_converter(renderer_cls, props)
|
||||
|
||||
for filename, plot_data in definitions_data:
|
||||
dps, final_props = converter.convert(
|
||||
revision=rev,
|
||||
filename=filename,
|
||||
data=plot_data,
|
||||
)
|
||||
renderers.append(
|
||||
renderer_class(datapoints, filename, **plot_properties)
|
||||
)
|
||||
break
|
||||
plot_datapoints.extend(dps)
|
||||
|
||||
if "title" not in final_props:
|
||||
final_props["title"] = renderer_id
|
||||
if renderer_cls is not None:
|
||||
renderers.append(
|
||||
renderer_cls(plot_datapoints, renderer_id, **final_props)
|
||||
)
|
||||
return renderers
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from typing import Dict, List, Optional, Set, Union
|
||||
|
||||
from funcy import first, project
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.render import FILENAME_FIELD, INDEX_FIELD, REVISION_FIELD
|
||||
|
||||
|
||||
class FieldsNotFoundError(DvcException):
|
||||
def __init__(self, expected_fields, found_fields):
|
||||
expected_str = ", ".join(expected_fields)
|
||||
found_str = ", ".join(found_fields)
|
||||
super().__init__(
|
||||
f"Could not find all provided fields ('{expected_str}') "
|
||||
f"in data fields ('{found_str}')."
|
||||
)
|
||||
|
||||
|
||||
class PlotDataStructureError(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"Plot data extraction failed. Please see "
|
||||
"https://man.dvc.org/plots for supported data formats."
|
||||
)
|
||||
|
||||
|
||||
def _filter_fields(datapoints: List[Dict], fields: Set) -> List[Dict]:
|
||||
if not fields:
|
||||
return datapoints
|
||||
assert isinstance(fields, set)
|
||||
|
||||
new_data = []
|
||||
for data_point in datapoints:
|
||||
keys = set(data_point.keys())
|
||||
if not fields <= keys:
|
||||
raise FieldsNotFoundError(fields, keys)
|
||||
|
||||
new_data.append(project(data_point, fields))
|
||||
|
||||
return new_data
|
||||
|
||||
|
||||
def _lists(dictionary: Dict):
|
||||
for _, value in dictionary.items():
|
||||
if isinstance(value, dict):
|
||||
yield from _lists(value)
|
||||
elif isinstance(value, list):
|
||||
yield value
|
||||
|
||||
|
||||
def _find_first_list(data: Union[Dict, List], fields: Set) -> List[Dict]:
|
||||
fields = fields or set()
|
||||
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
for lst in _lists(data):
|
||||
if (
|
||||
all(isinstance(dp, dict) for dp in lst)
|
||||
# if fields is empty, it will match any set
|
||||
and set(first(lst).keys()) & fields == fields
|
||||
):
|
||||
return lst
|
||||
|
||||
raise PlotDataStructureError()
|
||||
|
||||
|
||||
def _append_index(datapoints: List[Dict]) -> List[Dict]:
|
||||
if INDEX_FIELD in first(datapoints).keys():
|
||||
return datapoints
|
||||
|
||||
for index, data_point in enumerate(datapoints):
|
||||
data_point[INDEX_FIELD] = index
|
||||
return datapoints
|
||||
|
||||
|
||||
class VegaConverter:
|
||||
"""
|
||||
Class that takes care of converting unspecified data blob
|
||||
(Dict or List[Dict]) into datapoints (List[Dict]).
|
||||
If some properties that are required by Template class are missing
|
||||
('x', 'y') it will attempt to fill in the blanks.
|
||||
"""
|
||||
|
||||
def __init__(self, plot_properties: Optional[Dict] = None):
|
||||
plot_properties = plot_properties or {}
|
||||
self.props = deepcopy(plot_properties)
|
||||
self.inferred_props: Dict = {}
|
||||
|
||||
self.steps = []
|
||||
|
||||
self._infer_x()
|
||||
self._infer_fields()
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"find_data",
|
||||
partial(
|
||||
_find_first_list,
|
||||
fields=self.inferred_props.get("fields", set())
|
||||
- {INDEX_FIELD},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if not self.props.get("x", None):
|
||||
self.steps.append(("append_index", partial(_append_index)))
|
||||
|
||||
self.steps.append(
|
||||
(
|
||||
"filter_fields",
|
||||
partial(
|
||||
_filter_fields,
|
||||
fields=self.inferred_props.get("fields", set()),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def _infer_x(self):
|
||||
if not self.props.get("x", None):
|
||||
self.inferred_props["x"] = INDEX_FIELD
|
||||
|
||||
def skip_step(self, name: str):
|
||||
self.steps = [(_name, fn) for _name, fn in self.steps if _name != name]
|
||||
|
||||
def _infer_fields(self):
|
||||
fields = self.props.get("fields", set())
|
||||
if fields:
|
||||
fields = {
|
||||
*fields,
|
||||
self.props.get("x", None),
|
||||
self.props.get("y", None),
|
||||
self.inferred_props.get("x", None),
|
||||
} - {None}
|
||||
self.inferred_props["fields"] = fields
|
||||
|
||||
def _infer_y(self, datapoints: List[Dict]):
|
||||
if "y" not in self.props:
|
||||
data_fields = list(first(datapoints))
|
||||
skip = (
|
||||
REVISION_FIELD,
|
||||
self.props.get("x", None) or self.inferred_props.get("x"),
|
||||
)
|
||||
inferred_y = first(
|
||||
f for f in reversed(data_fields) if f not in skip
|
||||
)
|
||||
if "y" in self.inferred_props:
|
||||
previous_y = self.inferred_props["y"]
|
||||
if previous_y != inferred_y:
|
||||
raise DvcException(
|
||||
f"Inferred y ('{inferred_y}' value does not match"
|
||||
f"previously matched one ('f{previous_y}')."
|
||||
)
|
||||
else:
|
||||
self.inferred_props["y"] = inferred_y
|
||||
|
||||
def convert(
|
||||
self,
|
||||
data: Dict,
|
||||
revision: Optional[str] = None,
|
||||
filename: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Convert the data. Fill necessary fields ('x', 'y') and return both
|
||||
generated datapoints and updated properties.
|
||||
"""
|
||||
processed = deepcopy(data)
|
||||
|
||||
for _, step in self.steps:
|
||||
processed = step(processed) # type: ignore
|
||||
|
||||
self._infer_y(processed) # type: ignore
|
||||
|
||||
if revision:
|
||||
for datapoint in processed:
|
||||
datapoint[REVISION_FIELD] = revision
|
||||
if filename:
|
||||
for datapoint in processed:
|
||||
datapoint[FILENAME_FIELD] = filename
|
||||
|
||||
return processed, {**self.props, **self.inferred_props}
|
||||
+93
-52
@@ -3,7 +3,7 @@ import os
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Set
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
from dvc.repo.scm_context import SCMContext
|
||||
from dvc.scm import Base
|
||||
from dvc_objects.file import HashFile
|
||||
from dvc.stage import Stage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,28 +54,30 @@ def locked(f):
|
||||
class Repo:
|
||||
DVC_DIR = ".dvc"
|
||||
|
||||
from dvc.repo.add import add
|
||||
from dvc.repo.checkout import checkout
|
||||
from dvc.repo.commit import commit
|
||||
from dvc.repo.add import add # type: ignore[misc]
|
||||
from dvc.repo.checkout import checkout # type: ignore[misc]
|
||||
from dvc.repo.commit import commit # type: ignore[misc]
|
||||
from dvc.repo.destroy import destroy # type: ignore[misc]
|
||||
from dvc.repo.diff import diff
|
||||
from dvc.repo.fetch import fetch
|
||||
from dvc.repo.diff import diff # type: ignore[misc]
|
||||
from dvc.repo.fetch import fetch # type: ignore[misc]
|
||||
from dvc.repo.freeze import freeze, unfreeze # type: ignore[misc]
|
||||
from dvc.repo.gc import gc
|
||||
from dvc.repo.gc import gc # type: ignore[misc]
|
||||
from dvc.repo.get import get as _get # type: ignore[misc]
|
||||
from dvc.repo.get_url import get_url as _get_url # type: ignore[misc]
|
||||
from dvc.repo.imp import imp # type: ignore[misc]
|
||||
from dvc.repo.imp_url import imp_url
|
||||
from dvc.repo.imp_url import imp_url # type: ignore[misc]
|
||||
from dvc.repo.install import install # type: ignore[misc]
|
||||
from dvc.repo.ls import ls as _ls # type: ignore[misc]
|
||||
from dvc.repo.move import move
|
||||
from dvc.repo.pull import pull
|
||||
from dvc.repo.push import push
|
||||
from dvc.repo.remove import remove
|
||||
from dvc.repo.reproduce import reproduce
|
||||
from dvc.repo.run import run
|
||||
from dvc.repo.status import status
|
||||
from dvc.repo.update import update
|
||||
from dvc.repo.move import move # type: ignore[misc]
|
||||
from dvc.repo.pull import pull # type: ignore[misc]
|
||||
from dvc.repo.push import push # type: ignore[misc]
|
||||
from dvc.repo.remove import remove # type: ignore[misc]
|
||||
from dvc.repo.reproduce import reproduce # type: ignore[misc]
|
||||
from dvc.repo.run import run # type: ignore[misc]
|
||||
from dvc.repo.status import status # type: ignore[misc]
|
||||
from dvc.repo.update import update # type: ignore[misc]
|
||||
|
||||
from .data import status as data_status # type: ignore[misc]
|
||||
|
||||
ls = staticmethod(_ls)
|
||||
get = staticmethod(_get)
|
||||
@@ -127,19 +129,17 @@ class Repo:
|
||||
|
||||
import hashlib
|
||||
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
root_dir_hash = hashlib.sha224(
|
||||
self.root_dir.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
db_dir = os.path.join(
|
||||
db_dir = self.fs.path.join(
|
||||
base_db_dir,
|
||||
self.DVC_DIR,
|
||||
f"{os.path.basename(self.root_dir)}-{root_dir_hash[0:7]}",
|
||||
f"{self.fs.path.name(self.root_dir)}-{root_dir_hash[0:7]}",
|
||||
)
|
||||
|
||||
makedirs(db_dir, exist_ok=True)
|
||||
self.fs.makedirs(db_dir, exist_ok=True)
|
||||
return db_dir
|
||||
|
||||
def __init__(
|
||||
@@ -156,7 +156,7 @@ class Repo:
|
||||
):
|
||||
from dvc.config import Config
|
||||
from dvc.data_cloud import DataCloud
|
||||
from dvc.fs import GitFileSystem, localfs
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem, localfs
|
||||
from dvc.lock import LockNoop, make_lock
|
||||
from dvc.odbmgr import ODBManager
|
||||
from dvc.repo.metrics import Metrics
|
||||
@@ -165,7 +165,7 @@ class Repo:
|
||||
from dvc.repo.stage import StageLoad
|
||||
from dvc.scm import SCM
|
||||
from dvc.stage.cache import StageCache
|
||||
from dvc_objects.state import State, StateNoop
|
||||
from dvc_data.hashfile.state import State, StateNoop
|
||||
|
||||
self.url = url
|
||||
self._fs_conf = {"repo_factory": repo_factory}
|
||||
@@ -199,18 +199,23 @@ class Repo:
|
||||
self.odb = ODBManager(self)
|
||||
self.tmp_dir = None
|
||||
else:
|
||||
from dvc.utils.fs import makedirs
|
||||
self.fs.makedirs(self.tmp_dir, exist_ok=True)
|
||||
|
||||
makedirs(self.tmp_dir, exist_ok=True)
|
||||
self.lock = make_lock(
|
||||
os.path.join(self.tmp_dir, "lock"),
|
||||
tmp_dir=self.tmp_dir,
|
||||
hardlink_lock=self.config["core"].get("hardlink_lock", False),
|
||||
friendly=True,
|
||||
)
|
||||
if isinstance(self.fs, LocalFileSystem):
|
||||
self.lock = make_lock(
|
||||
self.fs.path.join(self.tmp_dir, "lock"),
|
||||
tmp_dir=self.tmp_dir,
|
||||
hardlink_lock=self.config["core"].get(
|
||||
"hardlink_lock", False
|
||||
),
|
||||
friendly=True,
|
||||
)
|
||||
state_db_dir = self._get_database_dir("state")
|
||||
self.state = State(self.root_dir, state_db_dir, self.dvcignore)
|
||||
else:
|
||||
self.lock = LockNoop()
|
||||
self.state = StateNoop()
|
||||
|
||||
state_db_dir = self._get_database_dir("state")
|
||||
self.state = State(self.root_dir, state_db_dir, self.dvcignore)
|
||||
self.odb = ODBManager(self)
|
||||
|
||||
self.stage_cache = StageCache(self)
|
||||
@@ -363,13 +368,13 @@ class Repo:
|
||||
)
|
||||
|
||||
def unprotect(self, target):
|
||||
return self.odb.local.unprotect(target)
|
||||
return self.odb.repo.unprotect(target)
|
||||
|
||||
def _ignore(self):
|
||||
flist = [self.config.files["local"], self.tmp_dir]
|
||||
|
||||
if path_isin(self.odb.local.cache_dir, self.root_dir):
|
||||
flist += [self.odb.local.cache_dir]
|
||||
if path_isin(self.odb.repo.path, self.root_dir):
|
||||
flist += [self.odb.repo.path]
|
||||
|
||||
for file in flist:
|
||||
self.scm_context.ignore(file)
|
||||
@@ -387,12 +392,14 @@ class Repo:
|
||||
all_tags=False,
|
||||
all_commits=False,
|
||||
all_experiments=False,
|
||||
commit_date: Optional[str] = None,
|
||||
remote=None,
|
||||
force=False,
|
||||
jobs=None,
|
||||
recursive=False,
|
||||
used_run_cache=None,
|
||||
revs=None,
|
||||
num=1,
|
||||
):
|
||||
"""Get the stages related to the given target and collect
|
||||
the `info` of its outputs.
|
||||
@@ -411,21 +418,14 @@ class Repo:
|
||||
"""
|
||||
used = defaultdict(set)
|
||||
|
||||
def _add_suffix(objs: Set["HashFile"], suffix: str) -> None:
|
||||
from itertools import chain
|
||||
|
||||
from dvc_data import iterobjs
|
||||
|
||||
for obj in chain.from_iterable(map(iterobjs, objs)):
|
||||
if obj.name is not None:
|
||||
obj.name += suffix
|
||||
|
||||
for branch in self.brancher(
|
||||
for _ in self.brancher(
|
||||
revs=revs,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
all_experiments=all_experiments,
|
||||
commit_date=commit_date,
|
||||
num=num,
|
||||
):
|
||||
for odb, objs in self.index.used_objs(
|
||||
targets,
|
||||
@@ -435,8 +435,6 @@ class Repo:
|
||||
recursive=recursive,
|
||||
with_deps=with_deps,
|
||||
).items():
|
||||
if branch:
|
||||
_add_suffix(objs, f" ({branch})")
|
||||
used[odb].update(objs)
|
||||
|
||||
if used_run_cache:
|
||||
@@ -447,6 +445,44 @@ class Repo:
|
||||
|
||||
return used
|
||||
|
||||
def partial_imports(
|
||||
self,
|
||||
targets=None,
|
||||
all_branches=False,
|
||||
all_tags=False,
|
||||
all_commits=False,
|
||||
all_experiments=False,
|
||||
commit_date: Optional[str] = None,
|
||||
recursive=False,
|
||||
revs=None,
|
||||
num=1,
|
||||
) -> List["Stage"]:
|
||||
"""Get the stages related to the given target and collect dependencies
|
||||
which are missing outputs.
|
||||
|
||||
This is useful to retrieve files which have been imported to the repo
|
||||
using --no-download.
|
||||
|
||||
Returns:
|
||||
A list of partially imported stages
|
||||
"""
|
||||
from itertools import chain
|
||||
|
||||
partial_imports = chain.from_iterable(
|
||||
self.index.partial_imports(targets, recursive=recursive)
|
||||
for _ in self.brancher(
|
||||
revs=revs,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
all_experiments=all_experiments,
|
||||
commit_date=commit_date,
|
||||
num=num,
|
||||
)
|
||||
)
|
||||
|
||||
return list(partial_imports)
|
||||
|
||||
@property
|
||||
def stages(self): # obsolete, only for backward-compatibility
|
||||
return self.index.stages
|
||||
@@ -486,7 +522,7 @@ class Repo:
|
||||
def datafs(self):
|
||||
from dvc.fs.data import DataFileSystem
|
||||
|
||||
return DataFileSystem(repo=self)
|
||||
return DataFileSystem(index=self.index.data["repo"])
|
||||
|
||||
@cached_property
|
||||
def dvcfs(self):
|
||||
@@ -507,18 +543,23 @@ class Repo:
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(repo=self, workspace="local")
|
||||
fs = DataFileSystem(index=self.index.data["local"])
|
||||
fs_path = path
|
||||
else:
|
||||
fs = DvcFileSystem(repo=self, subrepos=True)
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
try:
|
||||
if remote:
|
||||
remote_odb = self.cloud.get_remote_odb(name=remote)
|
||||
oid = fs.info(fs_path)["dvc_info"]["md5"]
|
||||
fs = remote_odb.fs
|
||||
fs_path = remote_odb.oid_to_path(oid)
|
||||
|
||||
with fs.open(
|
||||
fs_path,
|
||||
mode=mode,
|
||||
encoding=encoding,
|
||||
remote=remote,
|
||||
) as fobj:
|
||||
yield fobj
|
||||
except FileNotFoundError as exc:
|
||||
|
||||
+3
-2
@@ -264,6 +264,7 @@ def create_stages(
|
||||
outs=[out],
|
||||
external=external,
|
||||
)
|
||||
if kwargs.get("desc"):
|
||||
stage.outs[0].desc = kwargs["desc"]
|
||||
|
||||
out_obj = stage.outs[0]
|
||||
out_obj.annot.update(**kwargs)
|
||||
yield stage
|
||||
|
||||
+12
-1
@@ -1,3 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from dvc.scm import iter_revs
|
||||
|
||||
|
||||
@@ -8,7 +10,9 @@ def brancher( # noqa: E302
|
||||
all_tags=False,
|
||||
all_commits=False,
|
||||
all_experiments=False,
|
||||
commit_date: Optional[str] = None,
|
||||
sha_only=False,
|
||||
num=1,
|
||||
):
|
||||
"""Generator that iterates over specified revisions.
|
||||
|
||||
@@ -17,6 +21,9 @@ def brancher( # noqa: E302
|
||||
all_branches (bool): iterate over all available branches.
|
||||
all_commits (bool): iterate over all commits.
|
||||
all_tags (bool): iterate over all available tags.
|
||||
commit_date (str): Keep experiments from the commits after(include)
|
||||
a certain date. Date must match the extended
|
||||
ISO 8601 format (YYYY-MM-DD).
|
||||
sha_only (bool): only return git SHA for a revision.
|
||||
|
||||
Yields:
|
||||
@@ -62,6 +69,8 @@ def brancher( # noqa: E302
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
all_experiments=all_experiments,
|
||||
commit_date=commit_date,
|
||||
num=num,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -72,7 +81,9 @@ def brancher( # noqa: E302
|
||||
self.root_dir = self.fs.path.join("/", *repo_root_parts)
|
||||
|
||||
if cwd_parts:
|
||||
cwd = self.fs.path.join("/", *cwd_parts)
|
||||
cwd = self.fs.path.join( # type: ignore[unreachable]
|
||||
"/", *cwd_parts
|
||||
)
|
||||
self.fs.path.chdir(cwd)
|
||||
|
||||
# ignore revs that don't contain repo root
|
||||
|
||||
+7
-5
@@ -51,8 +51,8 @@ def _collect_paths(
|
||||
return target_paths
|
||||
|
||||
|
||||
def _filter_duplicates(
|
||||
outs: Outputs, fs_paths: StrPaths
|
||||
def _filter_outs(
|
||||
outs: Outputs, fs_paths: StrPaths, duplicates=False
|
||||
) -> Tuple[Outputs, StrPaths]:
|
||||
res_outs: Outputs = []
|
||||
fs_res_paths = fs_paths
|
||||
@@ -61,8 +61,9 @@ def _filter_duplicates(
|
||||
fs_path = out.repo.dvcfs.from_os_path(out.fs_path)
|
||||
if fs_path in fs_paths:
|
||||
res_outs.append(out)
|
||||
# MUTATING THE SAME LIST!!
|
||||
fs_res_paths.remove(fs_path)
|
||||
if not duplicates:
|
||||
# MUTATING THE SAME LIST!!
|
||||
fs_res_paths.remove(fs_path)
|
||||
|
||||
return res_outs, fs_res_paths
|
||||
|
||||
@@ -74,6 +75,7 @@ def collect(
|
||||
output_filter: FilterFn = None,
|
||||
rev: str = None,
|
||||
recursive: bool = False,
|
||||
duplicates: bool = False,
|
||||
) -> Tuple[Outputs, StrPaths]:
|
||||
assert targets or output_filter
|
||||
|
||||
@@ -85,4 +87,4 @@ def collect(
|
||||
|
||||
target_paths = _collect_paths(repo, targets, recursive=recursive, rev=rev)
|
||||
|
||||
return _filter_duplicates(outs, target_paths)
|
||||
return _filter_outs(outs, target_paths, duplicates=duplicates)
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
cast,
|
||||
)
|
||||
|
||||
from dvc.fs.git import GitFileSystem
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.base import Base
|
||||
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
|
||||
|
||||
def posixpath_to_os_path(path: str) -> str:
|
||||
return path.replace(posixpath.sep, os.path.sep)
|
||||
|
||||
|
||||
def _in_cache(obj: "HashInfo", cache: "HashFileDB") -> bool:
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
assert obj.value
|
||||
try:
|
||||
cache.check(obj.value)
|
||||
return True
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return False
|
||||
|
||||
|
||||
def _shallow_diff(
|
||||
root: str,
|
||||
old: Optional["HashInfo"],
|
||||
new: Optional["HashInfo"],
|
||||
cache: "HashFileDB",
|
||||
) -> Dict[str, List[str]]:
|
||||
d = {}
|
||||
|
||||
old_root = new_root = root
|
||||
if old and old.isdir:
|
||||
old_root = os.path.sep.join([root, ""])
|
||||
if new and new.isdir:
|
||||
new_root = os.path.sep.join([root, ""])
|
||||
|
||||
if old and not _in_cache(old, cache):
|
||||
d["not_in_cache"] = [old_root]
|
||||
|
||||
if not old and not new:
|
||||
return d
|
||||
if not new:
|
||||
return {"deleted": [old_root], **d}
|
||||
if not old:
|
||||
return {"added": [new_root], **d}
|
||||
if old != new:
|
||||
return {"modified": [new_root], **d}
|
||||
return {"unchanged": [new_root], **d}
|
||||
|
||||
|
||||
def _granular_diff(
|
||||
root: str,
|
||||
old_obj: Optional["HashFile"],
|
||||
new_obj: Optional["HashFile"],
|
||||
cache: "HashFileDB",
|
||||
) -> Dict[str, List[str]]:
|
||||
from dvc_data.hashfile.diff import ROOT
|
||||
from dvc_data.hashfile.diff import diff as odiff
|
||||
|
||||
def path_join(root: str, *paths: str, isdir: bool = False) -> str:
|
||||
if not isdir and paths == ROOT:
|
||||
return root
|
||||
return os.path.sep.join([root, *paths])
|
||||
|
||||
diff_data = odiff(old_obj, new_obj, cache)
|
||||
|
||||
output: Dict[str, List[str]] = defaultdict(list)
|
||||
for state in ("added", "deleted", "modified", "unchanged"):
|
||||
items = getattr(diff_data, state)
|
||||
for item in items: # pylint: disable=not-an-iterable
|
||||
entry = item.old if state == "deleted" else item.new
|
||||
isdir = entry.oid.isdir if entry.oid else False
|
||||
|
||||
path = path_join(root, *entry.key, isdir=isdir)
|
||||
output[state].append(path)
|
||||
if not item.old.in_cache and state != "added":
|
||||
output["not_in_cache"].append(path)
|
||||
return dict(output)
|
||||
|
||||
|
||||
def _get_obj_items(root: str, obj: Optional["HashFile"]) -> List[str]:
|
||||
if not obj:
|
||||
return []
|
||||
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
sep = os.path.sep
|
||||
if isinstance(obj, Tree):
|
||||
return [sep.join([root, *key]) for key, _, _ in obj]
|
||||
return [root]
|
||||
|
||||
|
||||
def _diff(
|
||||
root: str,
|
||||
old_oid: Optional["HashInfo"],
|
||||
old_obj: Optional["HashFile"],
|
||||
new_oid: Optional["HashInfo"],
|
||||
new_obj: Optional["HashFile"],
|
||||
odb: "HashFileDB",
|
||||
granular: bool = False,
|
||||
) -> Dict[str, List[str]]:
|
||||
if not granular:
|
||||
return _shallow_diff(root, old_oid, new_oid, odb)
|
||||
if (old_oid and not old_obj) or (new_oid and not new_obj):
|
||||
# we don't have enough information to give full details
|
||||
unknown = _get_obj_items(root, new_obj)
|
||||
shallow_diff = _shallow_diff(root, old_oid, new_oid, odb)
|
||||
return {**shallow_diff, "unknown": unknown}
|
||||
return _granular_diff(root, old_obj, new_obj, odb)
|
||||
|
||||
|
||||
class GitInfo(TypedDict, total=False):
|
||||
staged: Dict[str, List[str]]
|
||||
unstaged: Dict[str, List[str]]
|
||||
untracked: List[str]
|
||||
is_empty: bool
|
||||
is_dirty: bool
|
||||
|
||||
|
||||
def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.scm import NoSCM
|
||||
|
||||
if isinstance(scm, NoSCM):
|
||||
return {}
|
||||
|
||||
try:
|
||||
scm.get_rev()
|
||||
except SCMError:
|
||||
empty_repo = True
|
||||
else:
|
||||
empty_repo = False
|
||||
|
||||
staged, unstaged, untracked = scm.status(untracked_files=untracked_files)
|
||||
if os.name == "nt":
|
||||
untracked = [posixpath_to_os_path(path) for path in untracked]
|
||||
# NOTE: order is important here.
|
||||
return GitInfo(
|
||||
staged=staged,
|
||||
unstaged=unstaged,
|
||||
untracked=untracked,
|
||||
is_empty=empty_repo,
|
||||
is_dirty=any([staged, unstaged, untracked]),
|
||||
)
|
||||
|
||||
|
||||
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
unstaged_diff = defaultdict(list)
|
||||
for out in repo.index.outs:
|
||||
out = cast("Output", out)
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
try:
|
||||
_, _, new = build(
|
||||
out.odb,
|
||||
out.fs_path,
|
||||
out.fs,
|
||||
out.fs.PARAM_CHECKSUM,
|
||||
ignore=out.dvcignore,
|
||||
dry_run=True,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
new = None
|
||||
|
||||
cache = repo.odb.repo
|
||||
root = str(out)
|
||||
old = out.get_obj()
|
||||
|
||||
with ui.status(f"Calculating diff for {root} between index/workspace"):
|
||||
d = _diff(
|
||||
root,
|
||||
out.hash_info,
|
||||
old,
|
||||
new.hash_info if new else None,
|
||||
new,
|
||||
cache,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
for state, items in d.items():
|
||||
if not items:
|
||||
continue
|
||||
unstaged_diff[state].extend(items)
|
||||
return unstaged_diff
|
||||
|
||||
|
||||
def _diff_head_to_index(
|
||||
repo: "Repo", head: str = "HEAD", **kwargs: Any
|
||||
) -> Dict[str, List[str]]:
|
||||
# we need to store objects from index and the HEAD to diff later
|
||||
objs: Dict[str, Dict[str, Tuple["HashFile", "HashInfo"]]]
|
||||
objs = defaultdict(dict)
|
||||
|
||||
staged_diff = defaultdict(list)
|
||||
for rev in repo.brancher(revs=[head]):
|
||||
for out in repo.index.outs:
|
||||
out = cast("Output", out)
|
||||
if not out.use_cache:
|
||||
continue
|
||||
|
||||
root = str(out)
|
||||
if isinstance(out.fs, GitFileSystem):
|
||||
root = posixpath_to_os_path(root)
|
||||
typ = "index" if rev == "workspace" else head
|
||||
objs[root][typ] = (out.get_obj(), out.hash_info)
|
||||
|
||||
cache = repo.odb.repo
|
||||
for root, obj_d in objs.items():
|
||||
old_obj, old_oid = obj_d.get(head, (None, None))
|
||||
new_obj, new_oid = obj_d.get("index", (None, None))
|
||||
with ui.status(f"Calculating diff for {root} between head/index"):
|
||||
d = _diff(
|
||||
root, old_oid, old_obj, new_oid, new_obj, cache, **kwargs
|
||||
)
|
||||
|
||||
for state, items in d.items():
|
||||
if not items:
|
||||
continue
|
||||
staged_diff[state].extend(items)
|
||||
|
||||
return staged_diff
|
||||
|
||||
|
||||
class Status(TypedDict):
|
||||
not_in_cache: List[str]
|
||||
committed: Dict[str, List[str]]
|
||||
uncommitted: Dict[str, List[str]]
|
||||
untracked: List[str]
|
||||
unchanged: List[str]
|
||||
git: GitInfo
|
||||
|
||||
|
||||
def _transform_git_paths_to_dvc(
|
||||
repo: "Repo", files: Iterable[str]
|
||||
) -> List[str]:
|
||||
"""Transform files rel. to Git root to DVC root, and drop outside files."""
|
||||
rel = repo.fs.path.relpath(repo.root_dir, repo.scm.root_dir).rstrip("/")
|
||||
|
||||
# if we have repo root in a different location than scm's root,
|
||||
# i.e. subdir repo, all git_paths need to be transformed rel. to the DVC
|
||||
# repo root and anything outside need to be filtered out.
|
||||
if rel not in (os.curdir, ""):
|
||||
prefix = rel + os.sep
|
||||
length = len(prefix)
|
||||
files = (file[length:] for file in files if file.startswith(prefix))
|
||||
|
||||
start = repo.fs.path.relpath(repo.fs.path.getcwd(), repo.root_dir)
|
||||
if start in (os.curdir, ""):
|
||||
return list(files)
|
||||
# we need to convert repo relative paths to curdir relative.
|
||||
return [repo.fs.path.relpath(file, start) for file in files]
|
||||
|
||||
|
||||
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
from dvc.scm import NoSCMError, SCMError
|
||||
|
||||
head = kwargs.pop("head", "HEAD")
|
||||
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
|
||||
not_in_cache = uncommitted_diff.pop("not_in_cache", [])
|
||||
unchanged = set(uncommitted_diff.pop("unchanged", []))
|
||||
|
||||
try:
|
||||
committed_diff = _diff_head_to_index(repo, head=head, **kwargs)
|
||||
except (SCMError, NoSCMError):
|
||||
committed_diff = {}
|
||||
else:
|
||||
# we don't care about not-in-cache between the head and the index.
|
||||
committed_diff.pop("not_in_cache", None)
|
||||
unchanged &= set(committed_diff.pop("unchanged", []))
|
||||
|
||||
git_info = _git_info(repo.scm, untracked_files=untracked_files)
|
||||
untracked = git_info.get("untracked", [])
|
||||
untracked = _transform_git_paths_to_dvc(repo, untracked)
|
||||
# order matters here
|
||||
return Status(
|
||||
not_in_cache=not_in_cache,
|
||||
committed=committed_diff,
|
||||
uncommitted=uncommitted_diff,
|
||||
untracked=untracked,
|
||||
unchanged=list(unchanged),
|
||||
git=git_info,
|
||||
)
|
||||
|
||||
|
||||
def ls(
|
||||
repo: "Repo",
|
||||
targets: List[Optional[str]] = None,
|
||||
recursive: bool = False,
|
||||
) -> Iterator[Dict[str, Any]]:
|
||||
targets = targets or [None]
|
||||
pairs = chain.from_iterable(
|
||||
repo.stage.collect_granular(target, recursive=recursive)
|
||||
for target in targets
|
||||
)
|
||||
for stage, filter_info in pairs:
|
||||
for out in stage.filter_outs(filter_info):
|
||||
yield {"path": str(out), **out.annot.to_dict()}
|
||||
+13
-9
@@ -5,6 +5,7 @@ from typing import Dict, List
|
||||
|
||||
from dvc.exceptions import PathMissingError
|
||||
from dvc.repo import locked
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,6 +26,8 @@ def diff(self, a_rev="HEAD", b_rev=None, targets=None):
|
||||
from dvc.fs.dvc import DvcFileSystem
|
||||
|
||||
dvcfs = DvcFileSystem(repo=self)
|
||||
targets = ensure_list(targets)
|
||||
targets = [dvcfs.from_os_path(target) for target in targets]
|
||||
|
||||
b_rev = b_rev if b_rev else "workspace"
|
||||
results = {}
|
||||
@@ -36,7 +39,7 @@ def diff(self, a_rev="HEAD", b_rev=None, targets=None):
|
||||
continue
|
||||
|
||||
targets_paths = None
|
||||
if targets is not None:
|
||||
if targets:
|
||||
# convert targets to paths, and capture any missing targets
|
||||
targets_paths, missing_targets[rev] = _targets_to_paths(
|
||||
dvcfs, targets
|
||||
@@ -44,7 +47,7 @@ def diff(self, a_rev="HEAD", b_rev=None, targets=None):
|
||||
|
||||
results[rev] = _paths_checksums(self, targets_paths)
|
||||
|
||||
if targets is not None:
|
||||
if targets:
|
||||
# check for overlapping missing targets between a_rev and b_rev
|
||||
for target in set(missing_targets[a_rev]) & set(
|
||||
missing_targets[b_rev]
|
||||
@@ -112,7 +115,7 @@ def _paths_checksums(repo, targets):
|
||||
|
||||
def _output_paths(repo, targets):
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc_data.stage import stage as ostage
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
on_working_fs = isinstance(repo.fs, LocalFileSystem)
|
||||
|
||||
@@ -136,10 +139,10 @@ def _output_paths(repo, targets):
|
||||
)
|
||||
|
||||
if on_working_fs:
|
||||
_, _, obj = ostage(
|
||||
repo.odb.local,
|
||||
_, _, obj = build(
|
||||
repo.odb.repo,
|
||||
output.fs_path,
|
||||
repo.odb.local.fs,
|
||||
repo.odb.repo.fs,
|
||||
"md5",
|
||||
dry_run=True,
|
||||
ignore=output.dvcignore,
|
||||
@@ -183,11 +186,12 @@ def _filter_missing(dvcfs, paths):
|
||||
fs_path = dvcfs.from_os_path(path)
|
||||
try:
|
||||
info = dvcfs.info(fs_path)
|
||||
dvc_info = info.get("dvc_info")
|
||||
dvc_info = info.get("dvc_info", {})
|
||||
entry = dvc_info.get("entry")
|
||||
if (
|
||||
dvc_info
|
||||
entry
|
||||
and info["type"] == "directory"
|
||||
and not dvc_info["meta"].obj
|
||||
and not entry.odb.exists(entry.hash_info.value)
|
||||
):
|
||||
yield path
|
||||
except FileNotFoundError:
|
||||
|
||||
+191
-497
@@ -1,74 +1,43 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from functools import wraps
|
||||
from typing import Dict, Iterable, List, Mapping, Optional, Type
|
||||
import time
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
from funcy import cached_property, first
|
||||
|
||||
from dvc.dependency.param import MissingParamsError
|
||||
from dvc.env import DVCLIVE_RESUME
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import relpath
|
||||
|
||||
from .base import (
|
||||
from .exceptions import (
|
||||
BaselineMismatchError,
|
||||
ExperimentExistsError,
|
||||
InvalidExpRefError,
|
||||
MultipleBranchError,
|
||||
)
|
||||
from .executor.base import BaseExecutor, ExecutorInfo
|
||||
from .queue.base import BaseStashQueue, QueueEntry
|
||||
from .queue.celery import LocalCeleryQueue
|
||||
from .queue.tempdir import TempDirQueue
|
||||
from .queue.workspace import WorkspaceQueue
|
||||
from .refs import (
|
||||
CELERY_FAILED_STASH,
|
||||
CELERY_STASH,
|
||||
EXEC_APPLY,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_NAMESPACE,
|
||||
EXPS_STASH,
|
||||
BaselineMismatchError,
|
||||
ExperimentExistsError,
|
||||
WORKSPACE_STASH,
|
||||
ExpRefInfo,
|
||||
ExpStashEntry,
|
||||
InvalidExpRefError,
|
||||
MultipleBranchError,
|
||||
)
|
||||
from .executor.base import (
|
||||
EXEC_PID_DIR,
|
||||
EXEC_TMP_DIR,
|
||||
BaseExecutor,
|
||||
ExecutorInfo,
|
||||
)
|
||||
from .executor.manager.base import BaseExecutorManager
|
||||
from .executor.manager.local import (
|
||||
TempDirExecutorManager,
|
||||
WorkspaceExecutorManager,
|
||||
)
|
||||
from .executor.manager.ssh import SSHExecutorManager
|
||||
from .utils import exp_refs_by_rev
|
||||
from .stash import ExpStashEntry
|
||||
from .utils import exp_refs_by_rev, scm_locked, unlocked_repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def scm_locked(f):
|
||||
# Lock the experiments workspace so that we don't try to perform two
|
||||
# different sequences of git operations at once
|
||||
@wraps(f)
|
||||
def wrapper(exp, *args, **kwargs):
|
||||
from dvc.scm import map_scm_exception
|
||||
|
||||
with map_scm_exception(), exp.scm_lock:
|
||||
return f(exp, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def unlocked_repo(f):
|
||||
@wraps(f)
|
||||
def wrapper(exp, *args, **kwargs):
|
||||
exp.repo.lock.unlock()
|
||||
exp.repo._reset() # pylint: disable=protected-access
|
||||
try:
|
||||
ret = f(exp, *args, **kwargs)
|
||||
finally:
|
||||
exp.repo.lock.lock()
|
||||
return ret
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Experiments:
|
||||
"""Class that manages experiments in a DVC repo.
|
||||
|
||||
@@ -76,13 +45,6 @@ class Experiments:
|
||||
repo (dvc.repo.Repo): repo instance that these experiments belong to.
|
||||
"""
|
||||
|
||||
STASH_EXPERIMENT_FORMAT = "dvc-exp:{rev}:{baseline_rev}:{name}"
|
||||
STASH_EXPERIMENT_RE = re.compile(
|
||||
r"(?:commit: )"
|
||||
r"dvc-exp:(?P<rev>[0-9a-f]+):(?P<baseline_rev>[0-9a-f]+)"
|
||||
r":(?P<name>[^~^:\\?\[\]*]*)"
|
||||
r"(:(?P<branch>.+))?$"
|
||||
)
|
||||
BRANCH_RE = re.compile(
|
||||
r"^(?P<baseline_rev>[a-f0-9]{7})-(?P<exp_sha>[a-f0-9]+)"
|
||||
r"(?P<checkpoint>-checkpoint)?$"
|
||||
@@ -115,289 +77,34 @@ class Experiments:
|
||||
return os.path.join(self.repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
|
||||
|
||||
@cached_property
|
||||
def stash(self):
|
||||
from scmrepo.git import Stash
|
||||
def workspace_queue(self) -> WorkspaceQueue:
|
||||
return WorkspaceQueue(self.repo, WORKSPACE_STASH)
|
||||
|
||||
return Stash(self.scm, EXPS_STASH)
|
||||
@cached_property
|
||||
def tempdir_queue(self) -> TempDirQueue:
|
||||
# NOTE: tempdir and workspace stash is shared since both
|
||||
# implementations immediately push -> pop (queue length is only 0 or 1)
|
||||
return TempDirQueue(self.repo, WORKSPACE_STASH)
|
||||
|
||||
@cached_property
|
||||
def celery_queue(self) -> LocalCeleryQueue:
|
||||
return LocalCeleryQueue(self.repo, CELERY_STASH, CELERY_FAILED_STASH)
|
||||
|
||||
@property
|
||||
def stash_revs(self) -> Dict[str, ExpStashEntry]:
|
||||
revs = {}
|
||||
for i, entry in enumerate(self.stash):
|
||||
msg = entry.message.decode("utf-8").strip()
|
||||
m = self.STASH_EXPERIMENT_RE.match(msg)
|
||||
if m:
|
||||
revs[entry.new_sha.decode("utf-8")] = ExpStashEntry(
|
||||
i,
|
||||
m.group("rev"),
|
||||
m.group("baseline_rev"),
|
||||
m.group("branch"),
|
||||
m.group("name"),
|
||||
)
|
||||
for queue in (self.workspace_queue, self.celery_queue):
|
||||
revs.update(queue.stash.stash_revs)
|
||||
return revs
|
||||
|
||||
def _stash_exp(
|
||||
self,
|
||||
*args,
|
||||
params: Optional[dict] = None,
|
||||
resume_rev: Optional[str] = None,
|
||||
baseline_rev: Optional[str] = None,
|
||||
branch: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Stash changes from the workspace as an experiment.
|
||||
|
||||
Args:
|
||||
params: Optional dictionary of parameter values to be used.
|
||||
Values take priority over any parameters specified in the
|
||||
user's workspace.
|
||||
resume_rev: Optional checkpoint resume rev.
|
||||
baseline_rev: Optional baseline rev for this experiment, defaults
|
||||
to the current SCM rev.
|
||||
branch: Optional experiment branch name. If specified, the
|
||||
experiment will be added to `branch` instead of creating
|
||||
a new branch.
|
||||
name: Optional experiment name. If specified this will be used as
|
||||
the human-readable name in the experiment branch ref. Has no
|
||||
effect of branch is specified.
|
||||
"""
|
||||
with self.scm.detach_head(client="dvc") as orig_head:
|
||||
stash_head = orig_head
|
||||
if baseline_rev is None:
|
||||
baseline_rev = orig_head
|
||||
|
||||
with self.scm.stash_workspace() as workspace:
|
||||
try:
|
||||
if workspace:
|
||||
self.stash.apply(workspace)
|
||||
|
||||
if resume_rev:
|
||||
# move HEAD to the resume rev so that the stashed diff
|
||||
# only contains changes relative to resume rev
|
||||
stash_head = resume_rev
|
||||
self.scm.set_ref(
|
||||
"HEAD",
|
||||
resume_rev,
|
||||
message=f"dvc: resume from HEAD {resume_rev[:7]}",
|
||||
)
|
||||
self.scm.reset()
|
||||
|
||||
# update experiment params from command line
|
||||
if params:
|
||||
self._update_params(params)
|
||||
|
||||
# DVC commit data deps to preserve state across workspace
|
||||
# & tempdir runs
|
||||
self._stash_commit_deps(*args, **kwargs)
|
||||
|
||||
if resume_rev:
|
||||
if branch:
|
||||
branch_name = ExpRefInfo.from_ref(branch).name
|
||||
else:
|
||||
branch_name = f"{resume_rev[:7]}"
|
||||
if self.scm.is_dirty(untracked_files=False):
|
||||
logger.info(
|
||||
"Modified checkpoint experiment based on "
|
||||
"'%s' will be created",
|
||||
branch_name,
|
||||
)
|
||||
branch = None
|
||||
elif (
|
||||
not branch
|
||||
or self.scm.get_ref(branch) != resume_rev
|
||||
):
|
||||
msg = [
|
||||
(
|
||||
"Nothing to do for unchanged checkpoint "
|
||||
f"'{resume_rev[:7]}'. "
|
||||
)
|
||||
]
|
||||
if branch:
|
||||
msg.append(
|
||||
"To resume from the head of this "
|
||||
"experiment, use "
|
||||
f"'dvc exp apply {branch_name}'."
|
||||
)
|
||||
else:
|
||||
names = [
|
||||
ref_info.name
|
||||
for ref_info in exp_refs_by_rev(
|
||||
self.scm, resume_rev
|
||||
)
|
||||
]
|
||||
if len(names) > 3:
|
||||
names[3:] = [
|
||||
f"... ({len(names) - 3} more)"
|
||||
]
|
||||
msg.append(
|
||||
"To resume an experiment containing this "
|
||||
"checkpoint, apply one of these heads:\n"
|
||||
"\t{}".format(", ".join(names))
|
||||
)
|
||||
raise DvcException("".join(msg))
|
||||
else:
|
||||
logger.info(
|
||||
"Existing checkpoint experiment '%s' will be "
|
||||
"resumed",
|
||||
branch_name,
|
||||
)
|
||||
if name:
|
||||
logger.warning(
|
||||
"Ignoring option '--name %s' for resumed "
|
||||
"experiment. Existing experiment name will"
|
||||
"be preserved instead.",
|
||||
name,
|
||||
)
|
||||
|
||||
# save additional repro command line arguments
|
||||
run_env = {DVCLIVE_RESUME: "1"} if resume_rev else {}
|
||||
self._pack_args(*args, run_env=run_env, **kwargs)
|
||||
|
||||
# save experiment as a stash commit
|
||||
msg = self._stash_msg(
|
||||
stash_head,
|
||||
baseline_rev=baseline_rev,
|
||||
branch=branch,
|
||||
name=name,
|
||||
)
|
||||
stash_rev = self.stash.push(message=msg)
|
||||
logger.debug(
|
||||
(
|
||||
"Stashed experiment '%s' with baseline '%s' "
|
||||
"for future execution."
|
||||
),
|
||||
stash_rev[:7],
|
||||
baseline_rev[:7],
|
||||
)
|
||||
finally:
|
||||
if resume_rev:
|
||||
# NOTE: this set_ref + reset() is equivalent to
|
||||
# `git reset orig_head` (our SCM reset() only operates
|
||||
# on HEAD rather than any arbitrary commit)
|
||||
self.scm.set_ref(
|
||||
"HEAD", orig_head, message="dvc: restore HEAD"
|
||||
)
|
||||
self.scm.reset()
|
||||
# Revert any of our changes before prior unstashing
|
||||
self.scm.reset(hard=True)
|
||||
|
||||
return stash_rev
|
||||
|
||||
def _stash_commit_deps(self, *args, **kwargs):
|
||||
if len(args):
|
||||
targets = args[0]
|
||||
else:
|
||||
targets = kwargs.get("targets")
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
elif not targets:
|
||||
targets = [None]
|
||||
for target in targets:
|
||||
self.repo.commit(
|
||||
target,
|
||||
with_deps=True,
|
||||
recursive=kwargs.get("recursive", False),
|
||||
force=True,
|
||||
allow_missing=True,
|
||||
data_only=True,
|
||||
)
|
||||
|
||||
def _stash_msg(
|
||||
self,
|
||||
rev: str,
|
||||
baseline_rev: str,
|
||||
branch: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
if not baseline_rev:
|
||||
baseline_rev = rev
|
||||
msg = self.STASH_EXPERIMENT_FORMAT.format(
|
||||
rev=rev, baseline_rev=baseline_rev, name=name if name else ""
|
||||
)
|
||||
if branch:
|
||||
return f"{msg}:{branch}"
|
||||
return msg
|
||||
|
||||
def _pack_args(self, *args, **kwargs):
|
||||
import pickle
|
||||
|
||||
if os.path.exists(self.args_file) and self.scm.is_tracked(
|
||||
self.args_file
|
||||
):
|
||||
logger.warning(
|
||||
(
|
||||
"Temporary DVC file '.dvc/tmp/%s' exists and was "
|
||||
"likely committed to Git by mistake. It should be removed "
|
||||
"with:\n"
|
||||
"\tgit rm .dvc/tmp/%s"
|
||||
),
|
||||
BaseExecutor.PACKED_ARGS_FILE,
|
||||
BaseExecutor.PACKED_ARGS_FILE,
|
||||
)
|
||||
with open(self.args_file, "rb") as fobj:
|
||||
try:
|
||||
data = pickle.load(fobj)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
data = {}
|
||||
extra = int(data.get("extra", 0)) + 1
|
||||
else:
|
||||
extra = None
|
||||
BaseExecutor.pack_repro_args(
|
||||
self.args_file, *args, extra=extra, **kwargs
|
||||
)
|
||||
self.scm.add(self.args_file)
|
||||
|
||||
def _format_new_params_msg(self, new_params, config_path):
|
||||
"""Format an error message for when new parameters are identified"""
|
||||
new_param_count = len(new_params)
|
||||
pluralise = "s are" if new_param_count > 1 else " is"
|
||||
param_list = ", ".join(new_params)
|
||||
return (
|
||||
f"{new_param_count} parameter{pluralise} missing "
|
||||
f"from '{config_path}': {param_list}"
|
||||
)
|
||||
|
||||
def _update_params(self, params: dict):
|
||||
"""Update experiment params files with the specified values."""
|
||||
from dvc.utils.collections import NewParamsFound, merge_params
|
||||
from dvc.utils.serialize import MODIFIERS
|
||||
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
|
||||
for path in params:
|
||||
suffix = self.repo.fs.path.suffix(path).lower()
|
||||
modify_data = MODIFIERS[suffix]
|
||||
with modify_data(path, fs=self.repo.fs) as data:
|
||||
try:
|
||||
merge_params(data, params[path], allow_new=False)
|
||||
except NewParamsFound as e:
|
||||
msg = self._format_new_params_msg(e.new_params, path)
|
||||
raise MissingParamsError(msg)
|
||||
|
||||
# Force params file changes to be staged in git
|
||||
# Otherwise in certain situations the changes to params file may be
|
||||
# ignored when we `git stash` them since mtime is used to determine
|
||||
# whether the file is dirty
|
||||
self.scm.add(list(params.keys()))
|
||||
|
||||
def reproduce_one(
|
||||
self,
|
||||
queue: bool = False,
|
||||
tmp_dir: bool = False,
|
||||
checkpoint_resume: Optional[str] = None,
|
||||
reset: bool = False,
|
||||
machine: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Reproduce and checkout a single experiment."""
|
||||
if queue and not checkpoint_resume:
|
||||
reset = True
|
||||
|
||||
if reset:
|
||||
self.reset_checkpoints()
|
||||
|
||||
if not (queue or tmp_dir or machine):
|
||||
"""Reproduce and checkout a single (standalone) experiment."""
|
||||
if not (tmp_dir or machine):
|
||||
staged, _, _ = self.scm.status(untracked_files="no")
|
||||
if staged:
|
||||
logger.warning(
|
||||
@@ -406,6 +113,31 @@ class Experiments:
|
||||
)
|
||||
self.scm.reset()
|
||||
|
||||
exp_queue: BaseStashQueue = (
|
||||
self.tempdir_queue if tmp_dir else self.workspace_queue
|
||||
)
|
||||
self.queue_one(exp_queue, **kwargs)
|
||||
results = self._reproduce_queue(exp_queue)
|
||||
exp_rev = first(results)
|
||||
if exp_rev is not None:
|
||||
self._log_reproduced(results, tmp_dir=tmp_dir)
|
||||
return results
|
||||
|
||||
def queue_one(
|
||||
self,
|
||||
queue: BaseStashQueue,
|
||||
checkpoint_resume: Optional[str] = None,
|
||||
reset: bool = False,
|
||||
**kwargs,
|
||||
) -> QueueEntry:
|
||||
"""Queue a single experiment."""
|
||||
if reset:
|
||||
self.reset_checkpoints()
|
||||
|
||||
if kwargs.pop("machine", None) is not None:
|
||||
# TODO: decide how to handle queued remote execution
|
||||
raise NotImplementedError
|
||||
|
||||
if checkpoint_resume:
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
@@ -421,30 +153,12 @@ class Experiments:
|
||||
else:
|
||||
checkpoint_resume = self._workspace_resume_rev()
|
||||
|
||||
stash_rev = self.new(
|
||||
checkpoint_resume=checkpoint_resume, reset=reset, **kwargs
|
||||
return self.new(
|
||||
queue,
|
||||
checkpoint_resume=checkpoint_resume,
|
||||
reset=reset,
|
||||
**kwargs,
|
||||
)
|
||||
if queue:
|
||||
logger.info(
|
||||
"Queued experiment '%s' for future execution.", stash_rev[:7]
|
||||
)
|
||||
return [stash_rev]
|
||||
if tmp_dir or queue:
|
||||
manager_cls: Type = TempDirExecutorManager
|
||||
elif machine:
|
||||
manager_cls = SSHExecutorManager
|
||||
else:
|
||||
manager_cls = WorkspaceExecutorManager
|
||||
results = self._reproduce_revs(
|
||||
revs=[stash_rev],
|
||||
keep_stash=False,
|
||||
manager_cls=manager_cls,
|
||||
machine=machine,
|
||||
)
|
||||
exp_rev = first(results)
|
||||
if exp_rev is not None:
|
||||
self._log_reproduced(results, tmp_dir=tmp_dir)
|
||||
return results
|
||||
|
||||
def _workspace_resume_rev(self) -> Optional[str]:
|
||||
last_checkpoint = self._get_last_checkpoint()
|
||||
@@ -453,10 +167,46 @@ class Experiments:
|
||||
return last_applied
|
||||
return None
|
||||
|
||||
def reproduce_queued(self, **kwargs):
|
||||
results = self._reproduce_revs(**kwargs)
|
||||
def reproduce_celery(
|
||||
self, entries: Optional[Iterable[QueueEntry]] = None, **kwargs
|
||||
) -> Dict[str, str]:
|
||||
results: Dict[str, str] = {}
|
||||
if entries is None:
|
||||
entries = list(self.celery_queue.iter_queued())
|
||||
if not entries:
|
||||
return results
|
||||
|
||||
# TODO: re-enable --jobs concurrency
|
||||
self.celery_queue.spawn_worker()
|
||||
failed = []
|
||||
try:
|
||||
ui.write(
|
||||
"Following logs for all queued experiments. Use Ctrl+C to "
|
||||
"stop following logs (experiment execution will continue).\n"
|
||||
)
|
||||
for entry in entries:
|
||||
# wait for task execution to start
|
||||
while not self.celery_queue.proc.get(entry.stash_rev):
|
||||
time.sleep(1)
|
||||
self.celery_queue.follow(entry)
|
||||
# wait for task collection to complete
|
||||
result = self.celery_queue.get_result(entry)
|
||||
if result is None or result.exp_hash is None:
|
||||
name = entry.name or entry.stash_rev[:7]
|
||||
failed.append(name)
|
||||
elif result.ref_info:
|
||||
exp_rev = self.scm.get_ref(str(result.ref_info))
|
||||
results[exp_rev] = result.exp_hash
|
||||
except KeyboardInterrupt:
|
||||
ui.write(
|
||||
"Experiment(s) are still executing in the background. To "
|
||||
"abort execution use 'dvc queue kill' or 'dvc queue stop'."
|
||||
)
|
||||
if failed:
|
||||
names = ", ".join(name for name in failed)
|
||||
ui.error(f"Failed to reproduce experiment(s) '{names}'")
|
||||
if results:
|
||||
self._log_reproduced(results, tmp_dir=True)
|
||||
self._log_reproduced((rev for rev in results), True)
|
||||
return results
|
||||
|
||||
def _log_reproduced(self, revs: Iterable[str], tmp_dir: bool = False):
|
||||
@@ -464,18 +214,16 @@ class Experiments:
|
||||
for rev in revs:
|
||||
name = self.get_exact_name(rev)
|
||||
names.append(name if name else rev[:7])
|
||||
logger.info("\nReproduced experiment(s): %s", ", ".join(names))
|
||||
ui.write("\nRan experiment(s): {}".format(", ".join(names)))
|
||||
if tmp_dir:
|
||||
logger.info(
|
||||
ui.write(
|
||||
"To apply the results of an experiment to your workspace "
|
||||
"run:\n\n"
|
||||
"\tdvc exp apply <exp>"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Experiment results have been applied to your workspace."
|
||||
)
|
||||
logger.info(
|
||||
ui.write("Experiment results have been applied to your workspace.")
|
||||
ui.write(
|
||||
"\nTo promote an experiment to a Git branch run:\n\n"
|
||||
"\tdvc exp branch <exp> <branch>\n"
|
||||
)
|
||||
@@ -492,15 +240,20 @@ class Experiments:
|
||||
raise ExperimentExistsError(exp_ref.name)
|
||||
|
||||
@scm_locked
|
||||
def new(self, *args, checkpoint_resume: Optional[str] = None, **kwargs):
|
||||
"""Create a new experiment.
|
||||
def new(
|
||||
self,
|
||||
queue: BaseStashQueue,
|
||||
*args,
|
||||
checkpoint_resume: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> QueueEntry:
|
||||
"""Create and enqueue a new experiment.
|
||||
|
||||
Experiment will be reproduced and checked out into the user's
|
||||
workspace.
|
||||
Experiment will be derived from the current workspace.
|
||||
"""
|
||||
if checkpoint_resume is not None:
|
||||
return self._resume_checkpoint(
|
||||
*args, resume_rev=checkpoint_resume, **kwargs
|
||||
queue, *args, resume_rev=checkpoint_resume, **kwargs
|
||||
)
|
||||
|
||||
name = kwargs.get("name", None)
|
||||
@@ -513,16 +266,16 @@ class Experiments:
|
||||
if not (kwargs.get("force", False) or kwargs.get("reset", False)):
|
||||
raise err
|
||||
|
||||
return self._stash_exp(*args, **kwargs)
|
||||
return queue.put(*args, **kwargs)
|
||||
|
||||
def _resume_checkpoint(
|
||||
self, *args, resume_rev: Optional[str] = None, **kwargs
|
||||
):
|
||||
"""Resume an existing (checkpoint) experiment.
|
||||
|
||||
Experiment will be reproduced and checked out into the user's
|
||||
workspace.
|
||||
"""
|
||||
self,
|
||||
queue: BaseStashQueue,
|
||||
*args,
|
||||
resume_rev: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> QueueEntry:
|
||||
"""Create and queue a resumed checkpoint experiment."""
|
||||
assert resume_rev
|
||||
|
||||
branch: Optional[str] = None
|
||||
@@ -553,7 +306,7 @@ class Experiments:
|
||||
resume_rev[:7],
|
||||
baseline_rev,
|
||||
)
|
||||
return self._stash_exp(
|
||||
return queue.put(
|
||||
*args,
|
||||
resume_rev=resume_rev,
|
||||
baseline_rev=baseline_rev,
|
||||
@@ -589,90 +342,25 @@ class Experiments:
|
||||
self.scm.remove_ref(EXEC_CHECKPOINT)
|
||||
self.scm.remove_ref(EXEC_APPLY)
|
||||
|
||||
@scm_locked
|
||||
def _reproduce_revs(
|
||||
self,
|
||||
revs: Optional[Iterable] = None,
|
||||
keep_stash: Optional[bool] = True,
|
||||
manager_cls: Type = TempDirExecutorManager,
|
||||
machine: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Mapping[str, str]:
|
||||
"""Reproduce the specified experiments.
|
||||
@unlocked_repo
|
||||
def _reproduce_queue(
|
||||
self, queue: BaseStashQueue, **kwargs
|
||||
) -> Dict[str, str]:
|
||||
"""Reproduce queued experiments.
|
||||
|
||||
Args:
|
||||
revs: If revs is not specified, all stashed experiments will be
|
||||
reproduced.
|
||||
keep_stash: If True, stashed experiments will be preserved if they
|
||||
fail to reproduce successfully.
|
||||
Arguments:
|
||||
queue: Experiment queue.
|
||||
|
||||
Returns:
|
||||
dict mapping successfully reproduced experiment revs to their
|
||||
hashes.
|
||||
results.
|
||||
"""
|
||||
stash_revs = self.stash_revs
|
||||
exec_results = queue.reproduce()
|
||||
|
||||
# to_run contains mapping of:
|
||||
# input_rev: (stash_index, rev, baseline_rev)
|
||||
# where input_rev contains the changes to execute (usually a stash
|
||||
# commit), rev is the original SCM commit to be checked out, and
|
||||
# baseline_rev is the experiment baseline.
|
||||
if revs is None:
|
||||
to_run = dict(stash_revs)
|
||||
else:
|
||||
to_run = {
|
||||
rev: stash_revs[rev]
|
||||
if rev in stash_revs
|
||||
else ExpStashEntry(None, rev, rev, None, None)
|
||||
for rev in revs
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
"Reproducing experiment revs '%s'",
|
||||
", ".join(rev[:7] for rev in to_run),
|
||||
)
|
||||
|
||||
manager = manager_cls.from_stash_entries(
|
||||
self.scm,
|
||||
os.path.join(self.repo.tmp_dir, EXEC_TMP_DIR),
|
||||
self.repo,
|
||||
to_run,
|
||||
machine_name=machine,
|
||||
)
|
||||
try:
|
||||
exec_results = {}
|
||||
exec_results.update(self._executors_repro(manager, **kwargs))
|
||||
finally:
|
||||
# only drop successfully run stashed experiments
|
||||
to_drop: List[int] = [
|
||||
entry.stash_index
|
||||
for rev, entry in to_run.items()
|
||||
if (
|
||||
entry.stash_index is not None
|
||||
and (not keep_stash or rev in exec_results)
|
||||
)
|
||||
]
|
||||
for index in sorted(to_drop, reverse=True):
|
||||
self.stash.drop(index)
|
||||
|
||||
result: Dict[str, str] = {}
|
||||
results: Dict[str, str] = {}
|
||||
for _, exp_result in exec_results.items():
|
||||
result.update(exp_result)
|
||||
return result
|
||||
|
||||
@unlocked_repo
|
||||
def _executors_repro(
|
||||
self,
|
||||
manager: "BaseExecutorManager",
|
||||
**kwargs,
|
||||
) -> Dict[str, Dict[str, str]]:
|
||||
"""Run dvc repro for the specified BaseExecutors in parallel.
|
||||
|
||||
Returns:
|
||||
dict mapping stash revs to the successfully executed experiments
|
||||
for each stash rev.
|
||||
"""
|
||||
return manager.exec_queue(self.repo, **kwargs)
|
||||
results.update(exp_result)
|
||||
return results
|
||||
|
||||
def check_baseline(self, exp_rev):
|
||||
baseline_sha = self.repo.scm.get_rev()
|
||||
@@ -689,7 +377,6 @@ class Experiments:
|
||||
return exp_baseline
|
||||
raise BaselineMismatchError(exp_baseline, baseline_sha)
|
||||
|
||||
@scm_locked
|
||||
def get_baseline(self, rev):
|
||||
"""Return the baseline rev for an experiment rev."""
|
||||
return self._get_baseline(rev)
|
||||
@@ -742,61 +429,68 @@ class Experiments:
|
||||
return self.stash_revs[rev].name
|
||||
return None
|
||||
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, int]:
|
||||
def get_running_exps(self, fetch_refs: bool = True) -> Dict[str, Any]:
|
||||
"""Return info for running experiments."""
|
||||
result = {}
|
||||
infofile = self.workspace_queue.get_infofile_path("workspace")
|
||||
result.update(
|
||||
self._fetch_running_exp("workspace", infofile, fetch_refs)
|
||||
)
|
||||
for queue in (self.tempdir_queue, self.celery_queue):
|
||||
for entry in queue.iter_active():
|
||||
infofile = queue.get_infofile_path(entry.stash_rev)
|
||||
result.update(
|
||||
self._fetch_running_exp(
|
||||
entry.stash_rev, infofile, fetch_refs
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def _fetch_running_exp(
|
||||
self, rev: str, infofile: str, fetch_refs: bool
|
||||
) -> Dict[str, Any]:
|
||||
from dvc.scm import InvalidRemoteSCMRepo
|
||||
from dvc.utils.serialize import load_json
|
||||
|
||||
from .executor.local import TempDirExecutor
|
||||
|
||||
result = {}
|
||||
pid_dir = os.path.join(
|
||||
self.repo.tmp_dir,
|
||||
EXEC_TMP_DIR,
|
||||
EXEC_PID_DIR,
|
||||
)
|
||||
for fname in self.repo.fs.find(pid_dir):
|
||||
rev, ext = os.path.splitext(os.path.basename(fname))
|
||||
if ext != BaseExecutor.INFOFILE_EXT:
|
||||
continue
|
||||
|
||||
try:
|
||||
info = ExecutorInfo.from_dict(load_json(fname))
|
||||
if info.result is not None:
|
||||
continue
|
||||
if rev == "workspace":
|
||||
# If we are appending to a checkpoint branch in a workspace
|
||||
# run, show the latest checkpoint as running.
|
||||
last_rev = self.scm.get_ref(EXEC_BRANCH)
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
else:
|
||||
result[rev] = info.asdict()
|
||||
result: Dict[str, Any] = {}
|
||||
try:
|
||||
info = ExecutorInfo.from_dict(load_json(infofile))
|
||||
except OSError:
|
||||
return result
|
||||
if info.result is None:
|
||||
if rev == "workspace":
|
||||
# If we are appending to a checkpoint branch in a workspace
|
||||
# run, show the latest checkpoint as running.
|
||||
last_rev = self.scm.get_ref(EXEC_BRANCH)
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
else:
|
||||
result[rev] = info.asdict()
|
||||
if info.git_url and fetch_refs:
|
||||
else:
|
||||
result[rev] = info.asdict()
|
||||
if info.git_url and fetch_refs:
|
||||
|
||||
def on_diverged(_ref: str, _checkpoint: bool):
|
||||
return False
|
||||
def on_diverged(_ref: str, _checkpoint: bool):
|
||||
return False
|
||||
|
||||
executor = TempDirExecutor.from_info(info)
|
||||
try:
|
||||
for ref in executor.fetch_exps(
|
||||
self.scm,
|
||||
on_diverged=on_diverged,
|
||||
):
|
||||
logger.debug(
|
||||
"Updated running experiment '%s'.", ref
|
||||
)
|
||||
last_rev = self.scm.get_ref(ref)
|
||||
result[rev]["last"] = last_rev
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
except InvalidRemoteSCMRepo:
|
||||
# ignore stale info files
|
||||
del result[rev]
|
||||
except OSError:
|
||||
pass
|
||||
executor = TempDirExecutor.from_info(info)
|
||||
try:
|
||||
for ref in executor.fetch_exps(
|
||||
self.scm,
|
||||
on_diverged=on_diverged,
|
||||
):
|
||||
logger.debug(
|
||||
"Updated running experiment '%s'.", ref
|
||||
)
|
||||
last_rev = self.scm.get_ref(ref)
|
||||
result[rev]["last"] = last_rev
|
||||
if last_rev:
|
||||
result[last_rev] = info.asdict()
|
||||
except InvalidRemoteSCMRepo:
|
||||
# ignore stale info files
|
||||
del result[rev]
|
||||
return result
|
||||
|
||||
def apply(self, *args, **kwargs):
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
import logging
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
from .base import (
|
||||
EXEC_APPLY,
|
||||
from .exceptions import (
|
||||
ApplyConflictError,
|
||||
BaselineMismatchError,
|
||||
InvalidExpRevError,
|
||||
)
|
||||
from .executor.base import BaseExecutor
|
||||
from .refs import EXEC_APPLY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def apply(repo, rev, force=True, **kwargs):
|
||||
from scmrepo.exceptions import MergeConflictError
|
||||
def apply(repo: "Repo", rev: str, force: bool = True, **kwargs):
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
|
||||
from dvc.repo.checkout import checkout as dvc_checkout
|
||||
from dvc.scm import GitMergeError, RevError, resolve_rev
|
||||
@@ -38,37 +45,14 @@ def apply(repo, rev, force=True, **kwargs):
|
||||
):
|
||||
raise InvalidExpRevError(exp_rev)
|
||||
|
||||
# Note that we don't use stash_workspace() here since we need finer control
|
||||
# over the merge behavior when we unstash everything
|
||||
if repo.scm.is_dirty(untracked_files=True):
|
||||
logger.debug("Stashing workspace")
|
||||
workspace = repo.scm.stash.push(include_untracked=True)
|
||||
else:
|
||||
workspace = None
|
||||
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
|
||||
try:
|
||||
repo.scm.merge(exp_rev, commit=False, squash=True)
|
||||
except _SCMError as exc:
|
||||
raise GitMergeError(str(exc), scm=repo.scm)
|
||||
|
||||
if workspace:
|
||||
# NOTE: we don't use scmrepo's stash_workspace() here since we need
|
||||
# finer control over the merge behavior when we unstash everything
|
||||
with _apply_workspace(repo, rev, force):
|
||||
try:
|
||||
repo.scm.stash.apply(workspace)
|
||||
except MergeConflictError as exc:
|
||||
# Applied experiment conflicts with user's workspace changes
|
||||
if force:
|
||||
# prefer applied experiment changes over prior stashed changes
|
||||
repo.scm.checkout_index(ours=True)
|
||||
else:
|
||||
# revert applied changes and restore user's workspace
|
||||
repo.scm.reset(hard=True)
|
||||
repo.scm.stash.pop()
|
||||
raise ApplyConflictError(rev) from exc
|
||||
repo.scm.merge(exp_rev, commit=False, squash=True)
|
||||
except _SCMError as exc:
|
||||
raise ApplyConflictError(rev) from exc
|
||||
repo.scm.stash.drop()
|
||||
raise GitMergeError(str(exc), scm=repo.scm)
|
||||
|
||||
repo.scm.reset()
|
||||
|
||||
if stash_rev:
|
||||
@@ -84,3 +68,44 @@ def apply(repo, rev, force=True, **kwargs):
|
||||
"workspace.",
|
||||
rev,
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _apply_workspace(repo: "Repo", rev: str, force: bool):
|
||||
from scmrepo.exceptions import MergeConflictError
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
|
||||
if repo.scm.is_dirty(untracked_files=True):
|
||||
logger.debug("Stashing workspace")
|
||||
stash_rev: Optional[str] = repo.scm.stash.push(include_untracked=True)
|
||||
else:
|
||||
stash_rev = None
|
||||
try:
|
||||
yield
|
||||
except Exception: # pylint: disable=broad-except
|
||||
if stash_rev:
|
||||
_clean_and_pop(repo.scm)
|
||||
raise
|
||||
if not stash_rev:
|
||||
return
|
||||
|
||||
try:
|
||||
repo.scm.reset()
|
||||
repo.scm.stash.apply(stash_rev, skip_conflicts=force)
|
||||
repo.scm.stash.drop()
|
||||
except (MergeConflictError, _SCMError) as exc:
|
||||
_clean_and_pop(repo.scm)
|
||||
raise ApplyConflictError(rev) from exc
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_clean_and_pop(repo.scm)
|
||||
raise
|
||||
|
||||
|
||||
def _clean_and_pop(scm: "Git"):
|
||||
"""Revert any changes and pop the last stash entry."""
|
||||
scm.reset(hard=True)
|
||||
if scm.is_dirty(untracked_files=True):
|
||||
# drop any changes to untracked files before popping stash
|
||||
scm.stash.push(include_untracked=True)
|
||||
scm.stash.drop()
|
||||
scm.stash.pop()
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
|
||||
# Experiment refs are stored according baseline git SHA:
|
||||
# refs/exps/01/234abcd.../<exp_name>
|
||||
EXPS_NAMESPACE = "refs/exps"
|
||||
EXPS_STASH = f"{EXPS_NAMESPACE}/stash"
|
||||
EXEC_NAMESPACE = f"{EXPS_NAMESPACE}/exec"
|
||||
EXEC_APPLY = f"{EXEC_NAMESPACE}/EXEC_APPLY"
|
||||
EXEC_CHECKPOINT = f"{EXEC_NAMESPACE}/EXEC_CHECKPOINT"
|
||||
EXEC_BRANCH = f"{EXEC_NAMESPACE}/EXEC_BRANCH"
|
||||
EXEC_BASELINE = f"{EXEC_NAMESPACE}/EXEC_BASELINE"
|
||||
EXEC_HEAD = f"{EXEC_NAMESPACE}/EXEC_HEAD"
|
||||
EXEC_MERGE = f"{EXEC_NAMESPACE}/EXEC_MERGE"
|
||||
|
||||
|
||||
class UnchangedExperimentError(DvcException):
|
||||
def __init__(self, rev):
|
||||
super().__init__(f"Experiment unchanged from '{rev[:7]}'.")
|
||||
self.rev = rev
|
||||
|
||||
|
||||
class BaselineMismatchError(DvcException):
|
||||
def __init__(self, rev, expected):
|
||||
if hasattr(rev, "hexsha"):
|
||||
rev = rev.hexsha
|
||||
rev_str = f"{rev[:7]}" if rev is not None else "invalid commit"
|
||||
super().__init__(
|
||||
f"Experiment derived from '{rev_str}', expected '{expected[:7]}'."
|
||||
)
|
||||
self.rev = rev
|
||||
self.expected_rev = expected
|
||||
|
||||
|
||||
class ExperimentExistsError(DvcException):
|
||||
def __init__(self, name: str):
|
||||
msg = (
|
||||
"Experiment conflicts with existing experiment "
|
||||
f"'{name}'. To overwrite the existing experiment run:\n\n"
|
||||
"\tdvc exp run -f ...\n\n"
|
||||
"To run this experiment with a different name run:\n\n"
|
||||
f"\tdvc exp run -n <new_name> ...\n"
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.name = name
|
||||
|
||||
|
||||
class CheckpointExistsError(DvcException):
|
||||
def __init__(self, name: str):
|
||||
msg = (
|
||||
"Reproduced checkpoint experiment conflicts with existing "
|
||||
f"experiment '{name}'. To restart (and overwrite) the existing "
|
||||
"experiment run:\n\n"
|
||||
"\tdvc exp run -f ...\n\n"
|
||||
"To resume the existing experiment, run:\n\n"
|
||||
f"\tdvc exp apply {name}\n"
|
||||
f"\tdvc exp run\n"
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.name = name
|
||||
|
||||
|
||||
class InvalidExpRefError(DvcException):
|
||||
def __init__(self, ref):
|
||||
super().__init__(f"'{ref}' is not a valid experiment refname.")
|
||||
self.ref = ref
|
||||
|
||||
|
||||
class InvalidExpRevError(InvalidArgumentError):
|
||||
def __init__(self, rev):
|
||||
super().__init__(
|
||||
f"'{rev}' does not appear to be an experiment commit."
|
||||
)
|
||||
|
||||
|
||||
class MultipleBranchError(DvcException):
|
||||
def __init__(self, rev, ref_infos):
|
||||
super().__init__(
|
||||
f"Ambiguous commit '{rev[:7]}' belongs to multiple experiment "
|
||||
"branches."
|
||||
)
|
||||
self.rev = rev
|
||||
self.ref_infos = ref_infos
|
||||
|
||||
|
||||
class ApplyConflictError(InvalidArgumentError):
|
||||
def __init__(self, name):
|
||||
super().__init__(
|
||||
f"Experiment '{name}' cannot be applied to because your current "
|
||||
"workspace contains changes which would be overwritten. Either "
|
||||
"'git stash' your current changes before applying this "
|
||||
"experiment, or re-run this command with '--force' to overwrite "
|
||||
"your current changes."
|
||||
)
|
||||
self.name = name
|
||||
|
||||
|
||||
class ExpRefInfo:
|
||||
|
||||
namespace = EXPS_NAMESPACE
|
||||
|
||||
def __init__(
|
||||
self, baseline_sha: Optional[str] = None, name: Optional[str] = None
|
||||
):
|
||||
self.baseline_sha = baseline_sha
|
||||
self.name: str = name if name else ""
|
||||
|
||||
def __str__(self):
|
||||
return "/".join(self.parts)
|
||||
|
||||
def __repr__(self):
|
||||
baseline = f"'{self.baseline_sha}'" if self.baseline_sha else "None"
|
||||
name = f"'{self.name}'" if self.name else "None"
|
||||
return f"ExpRefInfo(baseline_sha={baseline}, name={name})"
|
||||
|
||||
@property
|
||||
def parts(self):
|
||||
return (
|
||||
(self.namespace,)
|
||||
+ (
|
||||
(self.baseline_sha[:2], self.baseline_sha[2:])
|
||||
if self.baseline_sha
|
||||
else ()
|
||||
)
|
||||
+ ((self.name,) if self.name else ())
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_ref(cls, ref: str):
|
||||
try:
|
||||
parts = ref.split("/")
|
||||
if (
|
||||
len(parts) < 2
|
||||
or len(parts) == 3
|
||||
or len(parts) > 5
|
||||
or "/".join(parts[:2]) != EXPS_NAMESPACE
|
||||
):
|
||||
raise InvalidExpRefError(ref)
|
||||
except ValueError:
|
||||
raise InvalidExpRefError(ref)
|
||||
baseline_sha = parts[2] + parts[3] if len(parts) >= 4 else None
|
||||
name = parts[4] if len(parts) == 5 else None
|
||||
return cls(baseline_sha, name)
|
||||
|
||||
|
||||
class ExpStashEntry(NamedTuple):
|
||||
"""Experiment stash entry.
|
||||
|
||||
stash_index: EXPS_STASH index for this entry. Can be None if this commit
|
||||
is not pushed onto the stash ref.
|
||||
head_rev: HEAD Git commit to be checked out for this experiment.
|
||||
baseline_rev: Experiment baseline commit.
|
||||
branch: Optional exp (checkpoint) branch name for this experiment.
|
||||
"""
|
||||
|
||||
stash_index: Optional[int]
|
||||
head_rev: str
|
||||
baseline_rev: str
|
||||
branch: Optional[str]
|
||||
name: Optional[str]
|
||||
@@ -5,7 +5,7 @@ from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import RevError
|
||||
|
||||
from .base import InvalidExpRevError
|
||||
from .exceptions import InvalidExpRevError
|
||||
from .utils import exp_refs_by_rev
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,15 +1,97 @@
|
||||
from typing import Collection, Iterable
|
||||
from typing import TYPE_CHECKING, Collection, Iterable
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
|
||||
from .base import ExpRefInfo
|
||||
if TYPE_CHECKING:
|
||||
from .refs import ExpRefInfo
|
||||
|
||||
|
||||
class UnchangedExperimentError(DvcException):
|
||||
def __init__(self, rev):
|
||||
super().__init__(f"Experiment unchanged from '{rev[:7]}'.")
|
||||
self.rev = rev
|
||||
|
||||
|
||||
class BaselineMismatchError(DvcException):
|
||||
def __init__(self, rev, expected):
|
||||
if hasattr(rev, "hexsha"):
|
||||
rev = rev.hexsha
|
||||
rev_str = f"{rev[:7]}" if rev is not None else "invalid commit"
|
||||
super().__init__(
|
||||
f"Experiment derived from '{rev_str}', expected '{expected[:7]}'."
|
||||
)
|
||||
self.rev = rev
|
||||
self.expected_rev = expected
|
||||
|
||||
|
||||
class ExperimentExistsError(DvcException):
|
||||
def __init__(self, name: str):
|
||||
msg = (
|
||||
"Experiment conflicts with existing experiment "
|
||||
f"'{name}'. To overwrite the existing experiment run:\n\n"
|
||||
"\tdvc exp run -f ...\n\n"
|
||||
"To run this experiment with a different name run:\n\n"
|
||||
f"\tdvc exp run -n <new_name> ...\n"
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.name = name
|
||||
|
||||
|
||||
class CheckpointExistsError(DvcException):
|
||||
def __init__(self, name: str):
|
||||
msg = (
|
||||
"Reproduced checkpoint experiment conflicts with existing "
|
||||
f"experiment '{name}'. To restart (and overwrite) the existing "
|
||||
"experiment run:\n\n"
|
||||
"\tdvc exp run -f ...\n\n"
|
||||
"To resume the existing experiment, run:\n\n"
|
||||
f"\tdvc exp apply {name}\n"
|
||||
f"\tdvc exp run\n"
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.name = name
|
||||
|
||||
|
||||
class InvalidExpRefError(DvcException):
|
||||
def __init__(self, ref):
|
||||
super().__init__(f"'{ref}' is not a valid experiment refname.")
|
||||
self.ref = ref
|
||||
|
||||
|
||||
class InvalidExpRevError(InvalidArgumentError):
|
||||
def __init__(self, rev):
|
||||
super().__init__(
|
||||
f"'{rev}' does not appear to be an experiment commit."
|
||||
)
|
||||
|
||||
|
||||
class MultipleBranchError(DvcException):
|
||||
def __init__(self, rev, ref_infos):
|
||||
super().__init__(
|
||||
f"Ambiguous commit '{rev[:7]}' belongs to multiple experiment "
|
||||
"branches."
|
||||
)
|
||||
self.rev = rev
|
||||
self.ref_infos = ref_infos
|
||||
|
||||
|
||||
class ApplyConflictError(InvalidArgumentError):
|
||||
def __init__(self, name):
|
||||
super().__init__(
|
||||
f"Experiment '{name}' cannot be applied to because your current "
|
||||
"workspace contains changes which would be overwritten. Either "
|
||||
"'git stash' your current changes before applying this "
|
||||
"experiment, or re-run this command with '--force' to overwrite "
|
||||
"your current changes."
|
||||
)
|
||||
self.name = name
|
||||
|
||||
|
||||
class AmbiguousExpRefInfo(InvalidArgumentError):
|
||||
def __init__(
|
||||
self,
|
||||
exp_name: str,
|
||||
exp_ref_list: Iterable[ExpRefInfo],
|
||||
exp_ref_list: Iterable["ExpRefInfo"],
|
||||
):
|
||||
msg = [
|
||||
(
|
||||
@@ -23,21 +105,31 @@ class AmbiguousExpRefInfo(InvalidArgumentError):
|
||||
|
||||
|
||||
class UnresolvedExpNamesError(InvalidArgumentError):
|
||||
NAME = "experiment name"
|
||||
|
||||
def __init__(
|
||||
self, unresolved_list: Collection[str], *args, git_remote: str = None
|
||||
):
|
||||
unresolved_names = ";".join(unresolved_list)
|
||||
unresolved_names = "; ".join(unresolved_list)
|
||||
if not git_remote:
|
||||
if len(unresolved_names) > 1:
|
||||
if len(unresolved_list) > 1:
|
||||
super().__init__(
|
||||
f"'{unresolved_names}' are not valid experiment names"
|
||||
f"'{unresolved_names}' are not valid {self.NAME}s"
|
||||
)
|
||||
else:
|
||||
super().__init__(
|
||||
f"'{unresolved_names}' is not a valid experiment name"
|
||||
f"'{unresolved_names}' is not a valid {self.NAME}"
|
||||
)
|
||||
else:
|
||||
super().__init__(
|
||||
f"Experiment '{unresolved_names}' does not exist "
|
||||
f"in '{git_remote}'"
|
||||
)
|
||||
|
||||
|
||||
class UnresolvedQueueExpNamesError(UnresolvedExpNamesError):
|
||||
NAME = "queued experiment name"
|
||||
|
||||
|
||||
class ExpQueueEmptyError(DvcException):
|
||||
pass
|
||||
|
||||
@@ -22,24 +22,27 @@ from typing import (
|
||||
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.stage.serialize import to_lockfile
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import dict_sha256, env2bool, relpath
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
from ..base import (
|
||||
from ..exceptions import (
|
||||
CheckpointExistsError,
|
||||
ExperimentExistsError,
|
||||
UnchangedExperimentError,
|
||||
)
|
||||
from ..refs import (
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_NAMESPACE,
|
||||
EXPS_STASH,
|
||||
CheckpointExistsError,
|
||||
ExperimentExistsError,
|
||||
ExpRefInfo,
|
||||
UnchangedExperimentError,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from queue import Queue
|
||||
|
||||
from scmrepo.git import Git
|
||||
|
||||
@@ -70,6 +73,7 @@ class ExecutorInfo:
|
||||
dvc_dir: str
|
||||
name: Optional[str] = None
|
||||
wdir: Optional[str] = None
|
||||
collected: bool = False
|
||||
result_hash: Optional[str] = None
|
||||
result_ref: Optional[str] = None
|
||||
result_force: bool = False
|
||||
@@ -99,6 +103,12 @@ class ExecutorInfo:
|
||||
with modify_json(filename) as d:
|
||||
d.update(self.asdict())
|
||||
|
||||
@classmethod
|
||||
def load_json(cls, filename: str) -> "ExecutorInfo":
|
||||
from dvc.utils.serialize import load_json
|
||||
|
||||
return cls.from_dict(load_json(filename))
|
||||
|
||||
|
||||
_T = TypeVar("_T", bound="BaseExecutor")
|
||||
|
||||
@@ -348,10 +358,11 @@ class BaseExecutor(ABC):
|
||||
|
||||
if git_remote == dvc.root_dir:
|
||||
logger.warning(
|
||||
f"'{git_remote}' points to the current Git repo, experiment "
|
||||
"'%s' points to the current Git repo, experiment "
|
||||
"Git refs will not be pushed. But DVC cache and run cache "
|
||||
"will automatically be pushed to the default DVC remote "
|
||||
"(if any) on each experiment commit."
|
||||
"(if any) on each experiment commit.",
|
||||
git_remote,
|
||||
)
|
||||
try:
|
||||
dvc.scm.validate_git_remote(git_remote)
|
||||
@@ -603,7 +614,9 @@ class BaseExecutor(ABC):
|
||||
except BaseException as exc: # pylint: disable=broad-except
|
||||
logger.warning(
|
||||
"Something went wrong while auto pushing experiment "
|
||||
f"to the remote '{git_remote}': {exc}"
|
||||
"to the remote '%s': %s",
|
||||
git_remote,
|
||||
exc,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -625,7 +638,7 @@ class BaseExecutor(ABC):
|
||||
if env2bool(DVC_EXP_AUTO_PUSH):
|
||||
git_remote = os.getenv(DVC_EXP_GIT_REMOTE)
|
||||
cls._auto_push(dvc, scm, git_remote)
|
||||
logger.info("Checkpoint experiment iteration '%s'.", exp_rev[:7])
|
||||
ui.write(f"Checkpoint experiment iteration '{exp_rev[:7]}'.")
|
||||
except UnchangedExperimentError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ from funcy import cached_property
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
|
||||
from dvc.scm import SCM, GitMergeError
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.fs import makedirs, remove
|
||||
|
||||
from ..base import (
|
||||
from ..refs import (
|
||||
EXEC_APPLY,
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
@@ -26,7 +26,8 @@ if TYPE_CHECKING:
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
from ..base import ExpRefInfo, ExpStashEntry
|
||||
from ..refs import ExpRefInfo
|
||||
from ..stash import ExpStashEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,7 +65,7 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
# suggestions) that are not applicable outside of workspace runs
|
||||
WARN_UNTRACKED = True
|
||||
QUIET = True
|
||||
DEFAULT_LOCATION = "temp"
|
||||
DEFAULT_LOCATION = "tempdir"
|
||||
|
||||
def init_git(self, scm: "Git", branch: Optional[str] = None):
|
||||
from dulwich.repo import Repo as DulwichRepo
|
||||
@@ -89,6 +90,11 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
head = EXEC_BRANCH if branch else EXEC_HEAD
|
||||
self.scm.checkout(head, detach=True)
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
|
||||
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
|
||||
if scm.get_ref(ref):
|
||||
scm.remove_ref(ref)
|
||||
|
||||
try:
|
||||
self.scm.merge(merge_rev, squash=True, commit=False)
|
||||
except _SCMError as exc:
|
||||
@@ -106,7 +112,7 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
|
||||
def init_cache(self, repo: "Repo", rev: str, run_cache: bool = True):
|
||||
"""Initialize DVC cache."""
|
||||
self._config(repo.odb.local.cache_dir)
|
||||
self._config(repo.odb.repo.path)
|
||||
|
||||
def cleanup(self):
|
||||
super().cleanup()
|
||||
@@ -119,11 +125,16 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
repo: "Repo",
|
||||
stash_rev: str,
|
||||
entry: "ExpStashEntry",
|
||||
wdir: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
tmp_dir = mkdtemp(dir=os.path.join(repo.tmp_dir, EXEC_TMP_DIR))
|
||||
parent_dir: str = wdir or os.path.join(repo.tmp_dir, EXEC_TMP_DIR)
|
||||
makedirs(parent_dir, exist_ok=True)
|
||||
tmp_dir = mkdtemp(dir=parent_dir)
|
||||
try:
|
||||
executor = cls._from_stash_entry(repo, stash_rev, entry, tmp_dir)
|
||||
executor = cls._from_stash_entry(
|
||||
repo, stash_rev, entry, tmp_dir, **kwargs
|
||||
)
|
||||
logger.debug("Init temp dir executor in '%s'", tmp_dir)
|
||||
return executor
|
||||
except Exception:
|
||||
@@ -146,7 +157,9 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
**kwargs,
|
||||
):
|
||||
root_dir = repo.scm.root_dir
|
||||
executor = cls._from_stash_entry(repo, stash_rev, entry, root_dir)
|
||||
executor = cls._from_stash_entry(
|
||||
repo, stash_rev, entry, root_dir, **kwargs
|
||||
)
|
||||
logger.debug("Init workspace executor in '%s'", root_dir)
|
||||
return executor
|
||||
|
||||
@@ -174,6 +187,7 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
def cleanup(self):
|
||||
with self._detach_stack:
|
||||
self.scm.remove_ref(EXEC_BASELINE)
|
||||
self.scm.remove_ref(EXEC_MERGE)
|
||||
if self.scm.get_ref(EXEC_BRANCH):
|
||||
self.scm.remove_ref(EXEC_BRANCH)
|
||||
checkpoint = self.scm.get_ref(EXEC_CHECKPOINT)
|
||||
|
||||
@@ -7,15 +7,9 @@ from typing import TYPE_CHECKING, Deque, Dict, Generator, Optional, Tuple, Type
|
||||
|
||||
from dvc.proc.manager import ProcessManager
|
||||
|
||||
from ...base import (
|
||||
EXEC_BASELINE,
|
||||
EXEC_HEAD,
|
||||
EXEC_MERGE,
|
||||
CheckpointExistsError,
|
||||
ExperimentExistsError,
|
||||
ExpRefInfo,
|
||||
ExpStashEntry,
|
||||
)
|
||||
from ...exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
from ...refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
|
||||
from ...stash import ExpStashEntry
|
||||
from ..base import EXEC_PID_DIR, BaseExecutor
|
||||
from ..local import TempDirExecutor, WorkspaceExecutor
|
||||
|
||||
@@ -27,7 +21,7 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseExecutorManager(ABC, Mapping):
|
||||
class BaseExecutorManager(ABC, Mapping): # noqa: B024
|
||||
"""Manages executors for a collection of experiments to be run."""
|
||||
|
||||
EXECUTOR_CLS: Type = BaseExecutor
|
||||
|
||||
@@ -2,13 +2,8 @@ import logging
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from ...base import (
|
||||
EXEC_BASELINE,
|
||||
EXEC_BRANCH,
|
||||
EXEC_HEAD,
|
||||
EXEC_MERGE,
|
||||
ExpStashEntry,
|
||||
)
|
||||
from ...refs import EXEC_BASELINE, EXEC_BRANCH, EXEC_HEAD, EXEC_MERGE
|
||||
from ...stash import ExpStashEntry
|
||||
from ..local import TempDirExecutor, WorkspaceExecutor
|
||||
from .base import BaseExecutorManager
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import posixpath
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Generator, Optional, Tuple
|
||||
|
||||
from ...base import ExpStashEntry
|
||||
from ...stash import ExpStashEntry
|
||||
from ..base import BaseExecutor
|
||||
from ..ssh import SSHExecutor, _sshfs
|
||||
from .base import BaseExecutorManager
|
||||
|
||||
@@ -5,21 +5,20 @@ import sys
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional
|
||||
|
||||
from dvc_ssh import SSHFileSystem
|
||||
from funcy import first
|
||||
|
||||
from dvc.fs import SSHFileSystem
|
||||
from dvc.repo.experiments.base import (
|
||||
from ..refs import (
|
||||
EXEC_BRANCH,
|
||||
EXEC_CHECKPOINT,
|
||||
EXEC_HEAD,
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
)
|
||||
|
||||
from .base import BaseExecutor, ExecutorInfo, ExecutorResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from multiprocessing import Queue
|
||||
from queue import Queue
|
||||
|
||||
from scmrepo.git import Git
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ def gc(
|
||||
all_tags: Optional[bool] = False,
|
||||
all_commits: Optional[bool] = False,
|
||||
workspace: Optional[bool] = False,
|
||||
commit_date: Optional[str] = None,
|
||||
queued: Optional[bool] = False,
|
||||
):
|
||||
keep_revs = set(
|
||||
@@ -22,6 +23,7 @@ def gc(
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
commit_date=commit_date,
|
||||
sha_only=True,
|
||||
)
|
||||
)
|
||||
@@ -40,11 +42,12 @@ def gc(
|
||||
removed = len(to_remove)
|
||||
|
||||
delete_stashes = []
|
||||
for _, entry in repo.experiments.stash_revs.items():
|
||||
stash = repo.experiments.celery_queue.stash
|
||||
for stash_rev, entry in stash.stash_revs.items():
|
||||
if not queued or entry.baseline_rev not in keep_revs:
|
||||
delete_stashes.append(entry.stash_index)
|
||||
for index in sorted(delete_stashes, reverse=True):
|
||||
repo.experiments.stash.drop(index)
|
||||
delete_stashes.append(stash_rev)
|
||||
if delete_stashes:
|
||||
repo.experiments.celery_queue.remove(delete_stashes)
|
||||
removed += len(delete_stashes)
|
||||
|
||||
return removed
|
||||
|
||||
@@ -253,18 +253,25 @@ def init(
|
||||
live_metrics = f"{live_path}.json" if live_path else None
|
||||
live_plots = os.path.join(live_path, "scalars") if live_path else None
|
||||
|
||||
if type == "checkpoint":
|
||||
outs_key = "checkpoints"
|
||||
metrics_key = "metrics_persist_no_cache"
|
||||
plots_key = "plots_persist_no_cache"
|
||||
else:
|
||||
outs_key = "outs"
|
||||
metrics_key = "metrics_no_cache"
|
||||
plots_key = "plots_no_cache"
|
||||
|
||||
stage = repo.stage.create(
|
||||
name=name,
|
||||
cmd=context["cmd"],
|
||||
deps=compact([context.get("code"), context.get("data")]),
|
||||
params=[{params: None}] if params else None,
|
||||
metrics_no_cache=compact([context.get("metrics"), live_metrics]),
|
||||
plots_no_cache=compact([context.get("plots"), live_plots]),
|
||||
force=force,
|
||||
**{
|
||||
"checkpoints"
|
||||
if type == "checkpoint"
|
||||
else "outs": compact([models])
|
||||
outs_key: compact([models]),
|
||||
metrics_key: compact([context.get("metrics"), live_metrics]),
|
||||
plots_key: compact([context.get("plots"), live_plots]),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import TqdmGit, iter_revs
|
||||
from dvc.ui import ui
|
||||
|
||||
from .base import ExpRefInfo
|
||||
from .exceptions import UnresolvedExpNamesError
|
||||
from .refs import ExpRefInfo
|
||||
from .utils import exp_commits, exp_refs, exp_refs_by_baseline, resolve_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -82,7 +82,7 @@ def _pull(
|
||||
force: bool,
|
||||
) -> Mapping[SyncStatus, List["ExpRefInfo"]]:
|
||||
refspec_list = [f"{exp_ref}:{exp_ref}" for exp_ref in refs]
|
||||
logger.debug(f"git pull experiment '{git_remote}' -> '{refspec_list}'")
|
||||
logger.debug("git pull experiment '%s' -> '%s'", git_remote, refspec_list)
|
||||
|
||||
with TqdmGit(desc="Fetching git refs") as pbar:
|
||||
results: Mapping[str, SyncStatus] = repo.scm.fetch_refspecs(
|
||||
@@ -113,7 +113,7 @@ def _pull_cache(
|
||||
if isinstance(refs, ExpRefInfo):
|
||||
refs = [refs]
|
||||
revs = list(exp_commits(repo.scm, refs))
|
||||
logger.debug(f"dvc fetch experiment '{refs}'")
|
||||
logger.debug("dvc fetch experiment '%s'", refs)
|
||||
repo.fetch(
|
||||
jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs, odb=odb
|
||||
)
|
||||
|
||||
@@ -9,8 +9,8 @@ from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import TqdmGit, iter_revs
|
||||
from dvc.ui import ui
|
||||
|
||||
from .base import ExpRefInfo
|
||||
from .exceptions import UnresolvedExpNamesError
|
||||
from .refs import ExpRefInfo
|
||||
from .utils import exp_commits, exp_refs, exp_refs_by_baseline, resolve_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -85,7 +85,7 @@ def _push(
|
||||
from ...scm import GitAuthError
|
||||
|
||||
refspec_list = [f"{exp_ref}:{exp_ref}" for exp_ref in refs]
|
||||
logger.debug(f"git push experiment '{refs}' -> '{git_remote}'")
|
||||
logger.debug("git push experiment '%s' -> '%s'", refspec_list, git_remote)
|
||||
|
||||
with TqdmGit(desc="Pushing git refs") as pbar:
|
||||
try:
|
||||
@@ -118,5 +118,5 @@ def _push_cache(
|
||||
if isinstance(refs, ExpRefInfo):
|
||||
refs = [refs]
|
||||
revs = list(exp_commits(repo.scm, refs))
|
||||
logger.debug(f"dvc push experiment '{refs}'")
|
||||
logger.debug("dvc push experiment '%s'", refs)
|
||||
repo.push(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs)
|
||||
|
||||
@@ -0,0 +1,692 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Collection,
|
||||
Dict,
|
||||
Generator,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.dependency import ParamsDependency
|
||||
from dvc.env import DVCLIVE_RESUME
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
from ..exceptions import CheckpointExistsError, ExperimentExistsError
|
||||
from ..executor.base import (
|
||||
EXEC_PID_DIR,
|
||||
EXEC_TMP_DIR,
|
||||
BaseExecutor,
|
||||
ExecutorResult,
|
||||
)
|
||||
from ..executor.local import WorkspaceExecutor
|
||||
from ..refs import EXEC_BASELINE, EXEC_HEAD, EXEC_MERGE, ExpRefInfo
|
||||
from ..stash import ExpStash, ExpStashEntry
|
||||
from ..utils import exp_refs_by_rev, scm_locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments import Experiments
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class QueueEntry:
|
||||
dvc_root: str
|
||||
scm_root: str
|
||||
stash_ref: str
|
||||
stash_rev: str
|
||||
baseline_rev: str
|
||||
branch: Optional[str]
|
||||
name: Optional[str]
|
||||
head_rev: Optional[str] = None
|
||||
|
||||
def __eq__(self, other: object):
|
||||
return (
|
||||
isinstance(other, QueueEntry)
|
||||
and self.dvc_root == other.dvc_root
|
||||
and self.scm_root == other.scm_root
|
||||
and self.stash_ref == other.stash_ref
|
||||
and self.stash_rev == other.stash_rev
|
||||
)
|
||||
|
||||
def asdict(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "QueueEntry":
|
||||
return cls(**d)
|
||||
|
||||
|
||||
class QueueGetResult(NamedTuple):
|
||||
entry: QueueEntry
|
||||
executor: BaseExecutor
|
||||
|
||||
|
||||
class QueueDoneResult(NamedTuple):
|
||||
entry: QueueEntry
|
||||
result: Optional[ExecutorResult]
|
||||
|
||||
|
||||
class BaseStashQueue(ABC):
|
||||
"""Naive Git-stash based experiment queue.
|
||||
|
||||
Maps queued experiments to (Git) stash reflog entries.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, repo: "Repo", ref: str, failed_ref: Optional[str] = None
|
||||
):
|
||||
"""Construct a queue.
|
||||
|
||||
Arguments:
|
||||
scm: Git SCM instance for this queue.
|
||||
ref: Git stash ref for this queue.
|
||||
failed_ref: Failed run Git stash ref for this queue.
|
||||
"""
|
||||
self.repo = repo
|
||||
self.ref = ref
|
||||
self.failed_ref = failed_ref
|
||||
|
||||
@property
|
||||
def scm(self) -> "Git":
|
||||
return self.repo.scm
|
||||
|
||||
@cached_property
|
||||
def stash(self) -> ExpStash:
|
||||
return ExpStash(self.scm, self.ref)
|
||||
|
||||
@cached_property
|
||||
def failed_stash(self) -> Optional[ExpStash]:
|
||||
return ExpStash(self.scm, self.failed_ref) if self.failed_ref else None
|
||||
|
||||
@cached_property
|
||||
def pid_dir(self) -> str:
|
||||
return os.path.join(self.repo.tmp_dir, EXEC_TMP_DIR, EXEC_PID_DIR)
|
||||
|
||||
@cached_property
|
||||
def args_file(self):
|
||||
return os.path.join(self.repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
|
||||
|
||||
@abstractmethod
|
||||
def put(self, *args, **kwargs) -> QueueEntry:
|
||||
"""Stash an experiment and add it to the queue."""
|
||||
|
||||
@abstractmethod
|
||||
def get(self) -> QueueGetResult:
|
||||
"""Pop and return the first item in the queue."""
|
||||
|
||||
def remove(
|
||||
self,
|
||||
revs: Collection[str],
|
||||
all_: bool = False,
|
||||
queued: bool = False,
|
||||
**kwargs,
|
||||
) -> List[str]:
|
||||
"""Remove the specified entries from the queue.
|
||||
|
||||
Arguments:
|
||||
revs: Stash revisions or queued exp names to be removed.
|
||||
queued: Remove all queued tasks.
|
||||
all: Remove all tasks.
|
||||
|
||||
Returns:
|
||||
Revisions (or names) which were removed.
|
||||
"""
|
||||
|
||||
if all_ or queued:
|
||||
return self.clear()
|
||||
|
||||
name_to_remove: List[str] = []
|
||||
entry_to_remove: List[ExpStashEntry] = []
|
||||
queue_entries = self.match_queue_entry_by_name(
|
||||
revs, self.iter_queued()
|
||||
)
|
||||
for name, entry in queue_entries.items():
|
||||
if entry:
|
||||
entry_to_remove.append(self.stash.stash_revs[entry.stash_rev])
|
||||
name_to_remove.append(name)
|
||||
|
||||
self.stash.remove_revs(entry_to_remove)
|
||||
return name_to_remove
|
||||
|
||||
def clear(self, **kwargs) -> List[str]:
|
||||
"""Remove all entries from the queue."""
|
||||
stash_revs = self.stash.stash_revs
|
||||
name_to_remove = list(stash_revs)
|
||||
self.stash.remove_revs(list(stash_revs.values()))
|
||||
|
||||
return name_to_remove
|
||||
|
||||
def status(self) -> List[Dict[str, Any]]:
|
||||
"""Show the status of exp tasks in queue"""
|
||||
from datetime import datetime
|
||||
|
||||
result: List[Dict[str, Optional[str]]] = []
|
||||
|
||||
def _get_timestamp(rev: str) -> datetime:
|
||||
commit = self.scm.resolve_commit(rev)
|
||||
return datetime.fromtimestamp(commit.commit_time)
|
||||
|
||||
def _format_entry(
|
||||
entry: QueueEntry,
|
||||
exp_result: Optional[ExecutorResult] = None,
|
||||
status: str = "Unknown",
|
||||
) -> Dict[str, Any]:
|
||||
name = entry.name
|
||||
if not name and exp_result and exp_result.ref_info:
|
||||
name = exp_result.ref_info.name
|
||||
# NOTE: We fallback to Unknown status for experiments
|
||||
# generated in prior (incompatible) DVC versions
|
||||
return {
|
||||
"rev": entry.stash_rev,
|
||||
"name": name,
|
||||
"timestamp": _get_timestamp(entry.stash_rev),
|
||||
"status": status,
|
||||
}
|
||||
|
||||
result.extend(
|
||||
_format_entry(queue_entry, status="Running")
|
||||
for queue_entry in self.iter_active()
|
||||
)
|
||||
result.extend(
|
||||
_format_entry(queue_entry, status="Queued")
|
||||
for queue_entry in self.iter_queued()
|
||||
)
|
||||
result.extend(
|
||||
_format_entry(queue_entry, status="Failed")
|
||||
for queue_entry, _ in self.iter_failed()
|
||||
)
|
||||
result.extend(
|
||||
_format_entry(queue_entry, exp_result=exp_result, status="Success")
|
||||
for queue_entry, exp_result in self.iter_success()
|
||||
)
|
||||
return result
|
||||
|
||||
@abstractmethod
|
||||
def iter_queued(self) -> Generator[QueueEntry, None, None]:
|
||||
"""Iterate over items in the queue."""
|
||||
|
||||
@abstractmethod
|
||||
def iter_active(self) -> Generator[QueueEntry, None, None]:
|
||||
"""Iterate over items which are being actively processed."""
|
||||
|
||||
@abstractmethod
|
||||
def iter_done(self) -> Generator[QueueDoneResult, None, None]:
|
||||
"""Iterate over items which been processed."""
|
||||
|
||||
@abstractmethod
|
||||
def iter_success(self) -> Generator[QueueDoneResult, None, None]:
|
||||
"""Iterate over items which been success."""
|
||||
|
||||
@abstractmethod
|
||||
def iter_failed(self) -> Generator[QueueDoneResult, None, None]:
|
||||
"""Iterate over items which been failed."""
|
||||
|
||||
@abstractmethod
|
||||
def reproduce(self) -> Mapping[str, Mapping[str, str]]:
|
||||
"""Reproduce queued experiments sequentially."""
|
||||
|
||||
@abstractmethod
|
||||
def get_result(self, entry: QueueEntry) -> Optional[ExecutorResult]:
|
||||
"""Return result of the specified item.
|
||||
|
||||
This method blocks until the specified item has been collected.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def kill(self, revs: str) -> None:
|
||||
"""Kill the specified running entries in the queue.
|
||||
|
||||
Arguments:
|
||||
revs: Stash revs or running exp name to be killed.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def shutdown(self, kill: bool = False):
|
||||
"""Shutdown the queue worker.
|
||||
|
||||
Arguments:
|
||||
kill: If True, the any active experiments will be killed and the
|
||||
worker will shutdown immediately. If False, the worker will
|
||||
finish any active experiments before shutting down.
|
||||
"""
|
||||
|
||||
def logs(
|
||||
self,
|
||||
rev: str,
|
||||
encoding: Optional[str] = None,
|
||||
follow: bool = False,
|
||||
):
|
||||
"""Print redirected output logs for an exp process.
|
||||
|
||||
Args:
|
||||
rev: Stash rev or exp name.
|
||||
encoding: Text encoding for redirected output. Defaults to
|
||||
`locale.getpreferredencoding()`.
|
||||
follow: Attach to running exp process and follow additional
|
||||
output.
|
||||
"""
|
||||
|
||||
def _stash_exp(
|
||||
self,
|
||||
*args,
|
||||
params: Optional[Dict[str, List[str]]] = None,
|
||||
resume_rev: Optional[str] = None,
|
||||
baseline_rev: Optional[str] = None,
|
||||
branch: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> QueueEntry:
|
||||
"""Stash changes from the workspace as an experiment.
|
||||
|
||||
Args:
|
||||
params: Dict mapping paths to `Hydra Override`_ patterns,
|
||||
provided via `exp run --set-param`.
|
||||
resume_rev: Optional checkpoint resume rev.
|
||||
baseline_rev: Optional baseline rev for this experiment, defaults
|
||||
to the current SCM rev.
|
||||
branch: Optional experiment branch name. If specified, the
|
||||
experiment will be added to `branch` instead of creating
|
||||
a new branch.
|
||||
name: Optional experiment name. If specified this will be used as
|
||||
the human-readable name in the experiment branch ref. Has no
|
||||
effect of branch is specified.
|
||||
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/next/advanced/override_grammar/basic/
|
||||
"""
|
||||
with self.scm.detach_head(client="dvc") as orig_head:
|
||||
stash_head = orig_head
|
||||
if baseline_rev is None:
|
||||
baseline_rev = orig_head
|
||||
|
||||
with self.scm.stash_workspace() as workspace:
|
||||
try:
|
||||
if workspace:
|
||||
self.stash.apply(workspace)
|
||||
|
||||
if resume_rev:
|
||||
# move HEAD to the resume rev so that the stashed diff
|
||||
# only contains changes relative to resume rev
|
||||
stash_head = resume_rev
|
||||
self.scm.set_ref(
|
||||
"HEAD",
|
||||
resume_rev,
|
||||
message=f"dvc: resume from HEAD {resume_rev[:7]}",
|
||||
)
|
||||
self.scm.reset()
|
||||
|
||||
# update experiment params from command line
|
||||
if params:
|
||||
self._update_params(params)
|
||||
|
||||
# DVC commit data deps to preserve state across workspace
|
||||
# & tempdir runs
|
||||
self._stash_commit_deps(*args, **kwargs)
|
||||
|
||||
if resume_rev:
|
||||
if branch:
|
||||
branch_name = ExpRefInfo.from_ref(branch).name
|
||||
else:
|
||||
branch_name = f"{resume_rev[:7]}"
|
||||
if self.scm.is_dirty(untracked_files=False):
|
||||
ui.write(
|
||||
"Modified checkpoint experiment based on "
|
||||
f"'{branch_name}' will be created",
|
||||
)
|
||||
branch = None
|
||||
elif (
|
||||
not branch
|
||||
or self.scm.get_ref(branch) != resume_rev
|
||||
):
|
||||
err_msg = [
|
||||
(
|
||||
"Nothing to do for unchanged checkpoint "
|
||||
f"'{resume_rev[:7]}'. "
|
||||
)
|
||||
]
|
||||
if branch:
|
||||
err_msg.append(
|
||||
"To resume from the head of this "
|
||||
"experiment, use "
|
||||
f"'dvc exp apply {branch_name}'."
|
||||
)
|
||||
else:
|
||||
names = [
|
||||
ref_info.name
|
||||
for ref_info in exp_refs_by_rev(
|
||||
self.scm, resume_rev
|
||||
)
|
||||
]
|
||||
if len(names) > 3:
|
||||
names[3:] = [
|
||||
f"... ({len(names) - 3} more)"
|
||||
]
|
||||
err_msg.append(
|
||||
"To resume an experiment containing this "
|
||||
"checkpoint, apply one of these heads:\n"
|
||||
"\t{}".format(", ".join(names))
|
||||
)
|
||||
raise DvcException("".join(err_msg))
|
||||
else:
|
||||
ui.write(
|
||||
"Existing checkpoint experiment "
|
||||
f"'{branch_name}' will be resumed"
|
||||
)
|
||||
if name:
|
||||
logger.warning(
|
||||
"Ignoring option '--name %s' for resumed "
|
||||
"experiment. Existing experiment name will"
|
||||
"be preserved instead.",
|
||||
name,
|
||||
)
|
||||
|
||||
# save additional repro command line arguments
|
||||
run_env = {DVCLIVE_RESUME: "1"} if resume_rev else {}
|
||||
self._pack_args(*args, run_env=run_env, **kwargs)
|
||||
|
||||
# save experiment as a stash commit
|
||||
msg = self._stash_msg(
|
||||
stash_head,
|
||||
baseline_rev=baseline_rev,
|
||||
branch=branch,
|
||||
name=name,
|
||||
)
|
||||
stash_rev = self.stash.push(message=msg)
|
||||
logger.debug(
|
||||
(
|
||||
"Stashed experiment '%s' with baseline '%s' "
|
||||
"for future execution."
|
||||
),
|
||||
stash_rev[:7],
|
||||
baseline_rev[:7],
|
||||
)
|
||||
finally:
|
||||
if resume_rev:
|
||||
# NOTE: this set_ref + reset() is equivalent to
|
||||
# `git reset orig_head` (our SCM reset() only operates
|
||||
# on HEAD rather than any arbitrary commit)
|
||||
self.scm.set_ref(
|
||||
"HEAD", orig_head, message="dvc: restore HEAD"
|
||||
)
|
||||
self.scm.reset()
|
||||
# Revert any of our changes before prior unstashing
|
||||
self.scm.reset(hard=True)
|
||||
|
||||
return QueueEntry(
|
||||
self.repo.root_dir,
|
||||
self.scm.root_dir,
|
||||
self.ref,
|
||||
stash_rev,
|
||||
baseline_rev,
|
||||
branch,
|
||||
name,
|
||||
stash_head,
|
||||
)
|
||||
|
||||
def _stash_commit_deps(self, *args, **kwargs):
|
||||
if len(args):
|
||||
targets = args[0]
|
||||
else:
|
||||
targets = kwargs.get("targets")
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
elif not targets:
|
||||
targets = [None]
|
||||
for target in targets:
|
||||
self.repo.commit(
|
||||
target,
|
||||
with_deps=True,
|
||||
recursive=kwargs.get("recursive", False),
|
||||
force=True,
|
||||
allow_missing=True,
|
||||
data_only=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _stash_msg(
|
||||
rev: str,
|
||||
baseline_rev: str,
|
||||
branch: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
) -> str:
|
||||
if not baseline_rev:
|
||||
baseline_rev = rev
|
||||
msg = ExpStash.format_message(rev, baseline_rev, name)
|
||||
if branch:
|
||||
return f"{msg}:{branch}"
|
||||
return msg
|
||||
|
||||
def _pack_args(self, *args, **kwargs) -> None:
|
||||
import pickle
|
||||
|
||||
if os.path.exists(self.args_file) and self.scm.is_tracked(
|
||||
self.args_file
|
||||
):
|
||||
logger.warning(
|
||||
(
|
||||
"Temporary DVC file '.dvc/tmp/%s' exists and was "
|
||||
"likely committed to Git by mistake. It should be removed "
|
||||
"with:\n"
|
||||
"\tgit rm .dvc/tmp/%s"
|
||||
),
|
||||
BaseExecutor.PACKED_ARGS_FILE,
|
||||
BaseExecutor.PACKED_ARGS_FILE,
|
||||
)
|
||||
with open(self.args_file, "rb") as fobj:
|
||||
try:
|
||||
data = pickle.load(fobj)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
data = {}
|
||||
extra = int(data.get("extra", 0)) + 1
|
||||
else:
|
||||
extra = None
|
||||
BaseExecutor.pack_repro_args(
|
||||
self.args_file, *args, extra=extra, **kwargs
|
||||
)
|
||||
self.scm.add(self.args_file)
|
||||
|
||||
@staticmethod
|
||||
def _format_new_params_msg(new_params, config_path):
|
||||
"""Format an error message for when new parameters are identified"""
|
||||
new_param_count = len(new_params)
|
||||
pluralise = "s are" if new_param_count > 1 else " is"
|
||||
param_list = ", ".join(new_params)
|
||||
return (
|
||||
f"{new_param_count} parameter{pluralise} missing "
|
||||
f"from '{config_path}': {param_list}"
|
||||
)
|
||||
|
||||
def _update_params(self, params: Dict[str, List[str]]):
|
||||
"""Update param files with the provided `Hydra Override`_ patterns.
|
||||
|
||||
Args:
|
||||
params: Dict mapping paths to `Hydra Override`_ patterns,
|
||||
provided via `exp run --set-param`.
|
||||
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/advanced/override_grammar/basic/
|
||||
"""
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
|
||||
try:
|
||||
from dvc.utils.hydra import apply_overrides, compose_and_dump
|
||||
except ValueError:
|
||||
if sys.version_info >= (3, 11):
|
||||
raise DvcException(
|
||||
"--set-param is not supported in Python >= 3.11"
|
||||
)
|
||||
raise
|
||||
|
||||
hydra_config = self.repo.config.get("hydra", {})
|
||||
hydra_enabled = hydra_config.get("enabled", False)
|
||||
hydra_output_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
for path, overrides in params.items():
|
||||
if hydra_enabled and path == hydra_output_file:
|
||||
config_dir = os.path.join(
|
||||
self.repo.root_dir, hydra_config.get("config_dir", "conf")
|
||||
)
|
||||
config_name = hydra_config.get("config_name", "config")
|
||||
compose_and_dump(
|
||||
path,
|
||||
config_dir,
|
||||
config_name,
|
||||
overrides,
|
||||
)
|
||||
else:
|
||||
apply_overrides(path, overrides)
|
||||
|
||||
# Force params file changes to be staged in git
|
||||
# Otherwise in certain situations the changes to params file may be
|
||||
# ignored when we `git stash` them since mtime is used to determine
|
||||
# whether the file is dirty
|
||||
self.scm.add(list(params.keys()))
|
||||
|
||||
@staticmethod
|
||||
@scm_locked
|
||||
def setup_executor(
|
||||
exp: "Experiments",
|
||||
queue_entry: QueueEntry,
|
||||
executor_cls: Type[BaseExecutor] = WorkspaceExecutor,
|
||||
**kwargs,
|
||||
) -> BaseExecutor:
|
||||
scm = exp.scm
|
||||
stash = ExpStash(scm, queue_entry.stash_ref)
|
||||
stash_rev = queue_entry.stash_rev
|
||||
stash_entry = stash.stash_revs.get(
|
||||
stash_rev,
|
||||
ExpStashEntry(None, stash_rev, stash_rev, None, None),
|
||||
)
|
||||
if stash_entry.stash_index is not None:
|
||||
stash.drop(stash_entry.stash_index)
|
||||
|
||||
scm.set_ref(EXEC_HEAD, stash_entry.head_rev)
|
||||
scm.set_ref(EXEC_MERGE, stash_rev)
|
||||
scm.set_ref(EXEC_BASELINE, stash_entry.baseline_rev)
|
||||
|
||||
# Executor will be initialized with an empty git repo that
|
||||
# we populate by pushing:
|
||||
# EXEC_HEAD - the base commit for this experiment
|
||||
# EXEC_MERGE - the unmerged changes (from our stash)
|
||||
# to be reproduced
|
||||
# EXEC_BASELINE - the baseline commit for this experiment
|
||||
return executor_cls.from_stash_entry(
|
||||
exp.repo, stash_rev, stash_entry, **kwargs
|
||||
)
|
||||
|
||||
def get_infofile_path(self, name: str) -> str:
|
||||
return os.path.join(
|
||||
self.pid_dir,
|
||||
name,
|
||||
f"{name}{BaseExecutor.INFOFILE_EXT}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@scm_locked
|
||||
def collect_executor(
|
||||
exp: "Experiments",
|
||||
executor: BaseExecutor,
|
||||
exec_result: ExecutorResult,
|
||||
) -> Dict[str, str]:
|
||||
results = {}
|
||||
|
||||
def on_diverged(ref: str, checkpoint: bool):
|
||||
ref_info = ExpRefInfo.from_ref(ref)
|
||||
if checkpoint:
|
||||
raise CheckpointExistsError(ref_info.name)
|
||||
raise ExperimentExistsError(ref_info.name)
|
||||
|
||||
for ref in executor.fetch_exps(
|
||||
exp.scm,
|
||||
force=exec_result.force,
|
||||
on_diverged=on_diverged,
|
||||
):
|
||||
exp_rev = exp.scm.get_ref(ref)
|
||||
if exp_rev:
|
||||
assert exec_result.exp_hash
|
||||
logger.debug("Collected experiment '%s'.", exp_rev[:7])
|
||||
results[exp_rev] = exec_result.exp_hash
|
||||
|
||||
if exec_result.ref_info is not None:
|
||||
executor.collect_cache(exp.repo, exec_result.ref_info)
|
||||
|
||||
return results
|
||||
|
||||
def match_queue_entry_by_name(
|
||||
self,
|
||||
exp_names: Collection[str],
|
||||
*entries: Iterable[Union[QueueEntry, QueueDoneResult]],
|
||||
) -> Dict[str, Optional[QueueEntry]]:
|
||||
from funcy import concat
|
||||
|
||||
entry_name_dict: Dict[str, QueueEntry] = {}
|
||||
entry_rev_dict: Dict[str, QueueEntry] = {}
|
||||
for entry in concat(*entries):
|
||||
if isinstance(entry, QueueDoneResult):
|
||||
queue_entry: QueueEntry = entry.entry
|
||||
if (
|
||||
entry.result is not None
|
||||
and entry.result.ref_info is not None
|
||||
):
|
||||
name: Optional[str] = entry.result.ref_info.name
|
||||
else:
|
||||
name = queue_entry.name
|
||||
else:
|
||||
queue_entry = entry
|
||||
name = queue_entry.name
|
||||
if name:
|
||||
entry_name_dict[name] = queue_entry
|
||||
entry_rev_dict[queue_entry.stash_rev] = queue_entry
|
||||
|
||||
result: Dict[str, Optional[QueueEntry]] = {}
|
||||
for exp_name in exp_names:
|
||||
result[exp_name] = None
|
||||
if exp_name in entry_name_dict:
|
||||
result[exp_name] = entry_name_dict[exp_name]
|
||||
continue
|
||||
if self.scm.is_sha(exp_name):
|
||||
for rev in entry_rev_dict:
|
||||
if rev.startswith(exp_name.lower()):
|
||||
result[exp_name] = entry_rev_dict[rev]
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def stash_failed(self, entry: QueueEntry) -> None:
|
||||
"""Add an entry to the failed exp stash.
|
||||
|
||||
Arguments:
|
||||
entry: Failed queue entry to add. ``entry.stash_rev`` must be a
|
||||
valid Git stash commit.
|
||||
"""
|
||||
if self.failed_stash is not None:
|
||||
assert entry.head_rev
|
||||
logger.debug("Stashing failed exp '%s'", entry.stash_rev[:7])
|
||||
msg = self.failed_stash.format_message(
|
||||
entry.head_rev,
|
||||
baseline_rev=entry.baseline_rev,
|
||||
name=entry.name,
|
||||
branch=entry.branch,
|
||||
)
|
||||
self.scm.set_ref(
|
||||
self.failed_stash.ref,
|
||||
entry.stash_rev,
|
||||
message=f"commit: {msg}",
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user