mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bf19debb4 | ||
|
|
51dc78a135 | ||
|
|
784a5b5199 | ||
|
|
bf2af86397 | ||
|
|
08d37912d6 | ||
|
|
0e66aa2b6a | ||
|
|
f9340ed7b9 | ||
|
|
e6ddb31525 | ||
|
|
7f2ddf943f | ||
|
|
3d664f1dbf | ||
|
|
cdbed33386 | ||
|
|
e1599ae280 | ||
|
|
ee367dcb1b | ||
|
|
7e7d0e1dd8 | ||
|
|
d81a1bbf56 | ||
|
|
ba0834caa0 | ||
|
|
9a4578cc72 | ||
|
|
ccf461bc84 | ||
|
|
e9c7cd63a2 | ||
|
|
33b6c1d876 | ||
|
|
f2673acb53 | ||
|
|
7e52c233ae | ||
|
|
408d23f285 | ||
|
|
7fb8a9a427 | ||
|
|
784f5a5d63 | ||
|
|
1710e379f5 | ||
|
|
aa9636e2ae | ||
|
|
65d412bfa3 | ||
|
|
20606d480b | ||
|
|
e2aedb7e97 | ||
|
|
bacae48f96 | ||
|
|
bf119e70a9 | ||
|
|
3528c37b66 | ||
|
|
f47d21b0c2 | ||
|
|
7a9da0f4a1 | ||
|
|
8bfe8e3328 | ||
|
|
c290d5c008 | ||
|
|
3c2c1fe72a | ||
|
|
0602eca00e | ||
|
|
5b69a16ea7 | ||
|
|
ef260eaf2e | ||
|
|
88431b1c0a | ||
|
|
bd2dc9dfed | ||
|
|
428c094b8d | ||
|
|
fc955c1917 | ||
|
|
8cb7e4948d | ||
|
|
8e353f9fce | ||
|
|
a4e2bce5b2 | ||
|
|
736ca9a5e5 | ||
|
|
cae1b1b647 | ||
|
|
3783d65dde | ||
|
|
0342e4f1c3 | ||
|
|
fc9af7a976 | ||
|
|
fea59cfc52 | ||
|
|
31ffce4fba | ||
|
|
2da959acd1 | ||
|
|
72a42fce20 | ||
|
|
1cbe64face | ||
|
|
057b0112ea | ||
|
|
1805edbd32 | ||
|
|
1723abcd79 | ||
|
|
3081c60414 | ||
|
|
152991d878 | ||
|
|
8098fd109d | ||
|
|
f0c1ff0219 | ||
|
|
27c548575c | ||
|
|
79cc28501b | ||
|
|
a051b8b60f | ||
|
|
bee04c6d5f | ||
|
|
04e891cef9 | ||
|
|
f5dda2d3cc | ||
|
|
e9545c8b55 | ||
|
|
09cfd911bb | ||
|
|
4f61a63afd | ||
|
|
c71b07b996 | ||
|
|
f4c0946f65 | ||
|
|
e35fb76dac | ||
|
|
722839dda6 | ||
|
|
d5533348f7 | ||
|
|
5d2ca11557 | ||
|
|
009b238807 | ||
|
|
43b371c43a | ||
|
|
d1160a28e0 | ||
|
|
bc86fa0643 | ||
|
|
4067a86926 | ||
|
|
9d1d51caa7 | ||
|
|
761422638f | ||
|
|
d01e2355d4 | ||
|
|
aa94bfd66b | ||
|
|
b9b9f91084 | ||
|
|
8a382f51fd | ||
|
|
b235487ecc | ||
|
|
502b3df6c9 | ||
|
|
5c2fd67e6d | ||
|
|
ed34c0f0bd | ||
|
|
7c60dfd3c5 | ||
|
|
225814fdde | ||
|
|
ca0ad239f7 | ||
|
|
f57ac609e9 | ||
|
|
cc4f505f99 | ||
|
|
4d011718a6 | ||
|
|
9c89841637 | ||
|
|
92cae74512 | ||
|
|
73dad4780f | ||
|
|
65ba9ba527 | ||
|
|
7806afd4ad | ||
|
|
a9d0ac2ba9 | ||
|
|
41df96fcdb | ||
|
|
53ec0fa41d | ||
|
|
41194b7975 | ||
|
|
ea5f7d40c1 | ||
|
|
eb06351062 | ||
|
|
ca3704997b | ||
|
|
f1764bdc77 | ||
|
|
eb212a7ba3 | ||
|
|
4c8f539c56 | ||
|
|
45aefc8423 | ||
|
|
67a747a356 | ||
|
|
a5dda1b8ad | ||
|
|
dd4035bc05 | ||
|
|
f69b3533e4 | ||
|
|
1b2291ca83 | ||
|
|
4de4f0fd65 | ||
|
|
2ef2caafd3 | ||
|
|
adcefbec66 | ||
|
|
2102ba8465 | ||
|
|
d736d0a3ef | ||
|
|
0d1f1f6436 | ||
|
|
63de16ced1 | ||
|
|
2f6c308c84 | ||
|
|
c8d180d2b0 | ||
|
|
d69972d199 | ||
|
|
eb1ed40228 | ||
|
|
120289128e | ||
|
|
a607e7b3d7 | ||
|
|
86e199c6f2 | ||
|
|
8661abf224 | ||
|
|
541def388b | ||
|
|
58eb145b6a | ||
|
|
16465e0b63 | ||
|
|
6614efd35a | ||
|
|
2becfd9344 | ||
|
|
b7b6642428 | ||
|
|
fea2e6bb30 | ||
|
|
bfa5b1a0c3 | ||
|
|
24d9a05d69 | ||
|
|
1f31a9c696 | ||
|
|
097e48fcc9 | ||
|
|
453a8f9516 | ||
|
|
9f59d8652e | ||
|
|
b1834f34e7 | ||
|
|
536273ab1e | ||
|
|
5cff4891fd | ||
|
|
e29da6eae1 | ||
|
|
046ebb05dd | ||
|
|
36ec25ffd7 | ||
|
|
6e6d9253d6 | ||
|
|
910c232077 | ||
|
|
c90947f33c | ||
|
|
4ee4f137f2 | ||
|
|
a750a75016 | ||
|
|
cf976f17ae | ||
|
|
7041fa5f42 | ||
|
|
d898877175 | ||
|
|
1ca1d78b38 | ||
|
|
da6ebdbf8c | ||
|
|
e995492095 | ||
|
|
276d499c9b | ||
|
|
fd4696cc50 | ||
|
|
90b9193250 | ||
|
|
77e35145a9 | ||
|
|
49c7c55ba3 | ||
|
|
f28d5ee9e0 | ||
|
|
621fe07a3b | ||
|
|
8323923ce9 | ||
|
|
04b50c5cd0 | ||
|
|
b20a59d748 | ||
|
|
bf2f08da09 | ||
|
|
1835c8c6e9 | ||
|
|
7e1bb07f10 | ||
|
|
92dc1f31f5 | ||
|
|
e16c0c8b2f | ||
|
|
33e7645603 | ||
|
|
c827654fe7 | ||
|
|
b8c666b1b7 | ||
|
|
fd73b389c6 | ||
|
|
0433565f1f | ||
|
|
b5449503f0 | ||
|
|
fa14507bc8 | ||
|
|
21458796ed | ||
|
|
a6e49a72de | ||
|
|
97348cfba7 | ||
|
|
c5e3d42899 | ||
|
|
c6c2ce130a | ||
|
|
be975ad9eb | ||
|
|
d0aa1ce776 | ||
|
|
5ec4fa473c | ||
|
|
5fd54679c7 | ||
|
|
5c1487e2ec | ||
|
|
30517f6676 | ||
|
|
bbb77360e9 | ||
|
|
1af150a5e9 | ||
|
|
cf59d801ce | ||
|
|
b733dea4c2 | ||
|
|
2ed677057d | ||
|
|
0597311439 | ||
|
|
fc8ec6aa95 | ||
|
|
3dca69b678 | ||
|
|
4c0bb8d316 | ||
|
|
1cb371d4d7 | ||
|
|
c96795dcfb | ||
|
|
8f768addf9 | ||
|
|
af1fb722c8 | ||
|
|
8b1c1a31f3 | ||
|
|
262419ef35 | ||
|
|
265fc581c0 | ||
|
|
80f414c19d | ||
|
|
2cd33e10ac | ||
|
|
603f5fdba1 | ||
|
|
528eab0418 | ||
|
|
ed664143b4 | ||
|
|
21a2504bcb | ||
|
|
d0291e8039 | ||
|
|
358c5e0c39 | ||
|
|
9d7fba5c61 | ||
|
|
a1e339b4fa | ||
|
|
6c58d438b3 | ||
|
|
0c709b6ec1 | ||
|
|
0aeb7a8662 | ||
|
|
46f3157f43 | ||
|
|
36fd69be21 | ||
|
|
3759535d43 | ||
|
|
2c4365513e | ||
|
|
26152292aa | ||
|
|
3e0f2bafbb | ||
|
|
78637713cf | ||
|
|
f9104275f3 | ||
|
|
bce3b5d2a1 | ||
|
|
6ace5ed380 | ||
|
|
6543580100 | ||
|
|
a1b891ce01 | ||
|
|
3a85326df3 | ||
|
|
355ebc72eb | ||
|
|
5a6f834088 | ||
|
|
e98bf38d4c | ||
|
|
192c9cc577 | ||
|
|
0e38525d8d | ||
|
|
8acd943d52 | ||
|
|
79c88008f8 | ||
|
|
9c9ca39fb4 | ||
|
|
89109fafcf | ||
|
|
aa19e24670 | ||
|
|
629223cbc2 | ||
|
|
96449f157c | ||
|
|
8cb2646023 | ||
|
|
25313f5d82 | ||
|
|
182728d674 | ||
|
|
f245567613 | ||
|
|
4163c8f57d | ||
|
|
6532258e4a | ||
|
|
eb45cda5d0 | ||
|
|
abd4f4a2a8 | ||
|
|
f41e535857 | ||
|
|
1989c0fe98 | ||
|
|
3ddd4b87fc | ||
|
|
e089ee0365 | ||
|
|
02f7fcae73 | ||
|
|
cdba65f27e | ||
|
|
3959c51373 | ||
|
|
90aaf5a296 | ||
|
|
45c5c83699 | ||
|
|
ecb71c8263 | ||
|
|
e6e3912da0 | ||
|
|
7f6890ff25 | ||
|
|
99daf5d113 | ||
|
|
83ffb8803e | ||
|
|
ace8464acb | ||
|
|
0dbdfd2b9b | ||
|
|
6d4251798a | ||
|
|
11d031935d | ||
|
|
64beb1578a | ||
|
|
f888544554 | ||
|
|
1e538ed87b | ||
|
|
e69deaa132 | ||
|
|
be0a1a4a12 | ||
|
|
004f49744e | ||
|
|
45181504ea | ||
|
|
915dd5062e | ||
|
|
d08c85f7d1 | ||
|
|
2342099bd8 | ||
|
|
06d8e3c639 | ||
|
|
195e67c0c1 | ||
|
|
c75a5583b3 | ||
|
|
981baa8c30 | ||
|
|
aa517bc90e | ||
|
|
c74dc20b97 | ||
|
|
1ba3944944 | ||
|
|
8620b6e5af | ||
|
|
6b6084a848 | ||
|
|
967dd5ea2b | ||
|
|
ad39bf14ed | ||
|
|
01416df5a4 | ||
|
|
eb592e7e33 | ||
|
|
2db07b047e | ||
|
|
8ecc66ae9a | ||
|
|
dfa5f4d9a0 | ||
|
|
75952c4e36 | ||
|
|
31b0b8de73 | ||
|
|
fae47e6122 | ||
|
|
ac3eef439f | ||
|
|
b044bcce5e | ||
|
|
a251cffdb5 | ||
|
|
37858be02f | ||
|
|
9b7240e9e0 | ||
|
|
e362438f03 | ||
|
|
6c79f6bbfc | ||
|
|
58cd1cbc09 | ||
|
|
96e3b48afd | ||
|
|
1f03c40dbe | ||
|
|
e93e8f05a3 | ||
|
|
0f1326e547 | ||
|
|
2a04263f02 | ||
|
|
348446050e | ||
|
|
7e1036b30d | ||
|
|
a15dbc8e03 | ||
|
|
4e02197245 | ||
|
|
4ab7ef06e5 | ||
|
|
dae2faf36d | ||
|
|
58f10a4661 | ||
|
|
64e787fa02 | ||
|
|
c7e2ec9707 | ||
|
|
a4cc198ac7 | ||
|
|
d203bf9024 | ||
|
|
baeec8ec2d | ||
|
|
9c689a3b51 | ||
|
|
e1bf418e72 | ||
|
|
b6d8c07d4d | ||
|
|
b814531c84 | ||
|
|
3b3cd0a0b2 | ||
|
|
b2101a85ad | ||
|
|
ba760c56e5 |
+1
-10
@@ -1,12 +1,3 @@
|
||||
/state
|
||||
/lock
|
||||
/config.local
|
||||
/updater
|
||||
/updater.lock
|
||||
/state-journal
|
||||
/state-wal
|
||||
/cache
|
||||
/pkg
|
||||
/repos
|
||||
/tmp
|
||||
/experiments
|
||||
/cache
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
['remote "dvc"']
|
||||
url = https://remote.dvc.org/dvc
|
||||
[core]
|
||||
remote = dvc
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
[flake8]
|
||||
ignore=
|
||||
max_line_length = 88
|
||||
extend-ignore=
|
||||
# Whitespace before ':'
|
||||
E203
|
||||
# Too many leading '#' for block comment
|
||||
E266
|
||||
# Line break occurred before a binary operator
|
||||
W503
|
||||
# unindexed parameters in the str.format, see:
|
||||
# https://pypi.org/project/flake8-string-format/
|
||||
P1
|
||||
# use `!r` instead of surrounding by quotes
|
||||
B028
|
||||
max_line_length = 88
|
||||
max-complexity = 10
|
||||
select = B,C,E,F,W,T4,B902,T,P,TYP
|
||||
show_source = true
|
||||
count = true
|
||||
min_python_version = 3.8.0
|
||||
per-file-ignores =
|
||||
dvc/testing/benchmarks/conftest.py:F401
|
||||
dvc/testing/conftest.py:F401
|
||||
dvc/testing/plugin.py:F401
|
||||
tests/conftest.py:F401
|
||||
tests/func/experiments/executor/test_ssh.py:F401
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
pull_request_rules:
|
||||
- name: backport patches to 2.x
|
||||
conditions:
|
||||
- base=main
|
||||
- label=backport
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- "2.x"
|
||||
title: ({{ destination_branch }}) {{ title }}
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Build package
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
pip:
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- run: python -m pip install --upgrade pip setuptools_scm build twine
|
||||
- name: Force version for Test PyPI upload
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
||||
run: echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
||||
|
||||
- run: |
|
||||
echo 'PKG = "pip"'>dvc/_build.py
|
||||
python -m build
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.version }}
|
||||
- run: twine check --strict dist/*
|
||||
|
||||
- name: Publish to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
skip-existing: true
|
||||
@@ -2,7 +2,7 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ["main", "2.x"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
name: Build packages
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
binary:
|
||||
permissions:
|
||||
contents: write # for actions/upload-release-asset to upload release asset
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- {pkg: "deb", os: "ubuntu-20.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-20.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-11", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
|
||||
|
||||
name: ${{ matrix.pkg }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
pyproject.toml
|
||||
scripts/build-requirements.txt
|
||||
|
||||
- name: Set up Ruby 2.6
|
||||
uses: ruby/setup-ruby@v1
|
||||
if: matrix.pkg != 'exe'
|
||||
with:
|
||||
ruby-version: '2.6'
|
||||
|
||||
- name: Install fpm
|
||||
if: matrix.pkg != 'exe'
|
||||
run: gem install --no-document fpm
|
||||
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install --upgrade pip wheel setuptools
|
||||
pip install .[all]
|
||||
pip install -r scripts/build-requirements.txt
|
||||
|
||||
- name: Pull images
|
||||
run: dvc pull
|
||||
|
||||
- name: Build ${{ matrix.pkg }}
|
||||
run: python scripts/build.py ${{ matrix.pkg }}
|
||||
|
||||
- name: Publish ${{ matrix.pkg }}
|
||||
if: github.event_name == 'release'
|
||||
uses: actions/[email protected]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ github.event.release.upload_url }}
|
||||
asset_path: scripts/${{ matrix.pkg == 'exe' && 'innosetup' || 'fpm' }}/${{ matrix.asset }}
|
||||
asset_name: ${{ matrix.asset }}
|
||||
asset_content_type: binary/octet-stream
|
||||
|
||||
pip:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install
|
||||
run: pip install --upgrade pip wheel
|
||||
|
||||
- name: Force version for Test PyPI uploads
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
||||
run: |
|
||||
pip install setuptools_scm
|
||||
echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
||||
|
||||
- name: Build packages
|
||||
run: ./scripts/build_package.sh
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.version }}
|
||||
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.TEST_PYPI_TOKEN }}
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
skip_existing: true
|
||||
@@ -23,11 +23,11 @@ jobs:
|
||||
plugin: ["dvc-s3"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: dvc
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: iterative/${{ matrix.plugin }}
|
||||
ref: main
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, 2.x]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '5 1 * * *' # every day at 01:05
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
pyv: ["3.8", "3.9", "3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
@@ -42,6 +42,8 @@ jobs:
|
||||
- uses: pre-commit/[email protected]
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
PYTHONUTF8: 1
|
||||
run: >
|
||||
pytest -n=logical --timeout=300 --durations=100
|
||||
--cov --cov-report=xml --cov-report=term
|
||||
@@ -64,7 +66,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Notification
|
||||
uses: rtCamp/[email protected].0
|
||||
uses: rtCamp/[email protected].1
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
|
||||
+6
-19
@@ -23,42 +23,29 @@ repos:
|
||||
- id: mixed-line-ending
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/lovesegfault/beautysh
|
||||
rev: v6.2.1
|
||||
hooks:
|
||||
- id: beautysh
|
||||
args: ["-i", "2"]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
- --py38-plus
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
rev: 23.9.1
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.261'
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.0.288'
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.4
|
||||
rev: v2.2.5
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies: ["tomli"]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-bugbear
|
||||
- flake8-comprehensions
|
||||
- flake8-debugger
|
||||
- flake8-string-format
|
||||
- flake8-typing-imports
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
|
||||
+2
-2
@@ -280,8 +280,8 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: PyPI Downloads
|
||||
|
||||
.. |Packages| image:: https://img.shields.io/github/v/release/iterative/dvc?label=deb|pkg|rpm|exe&logo=GitHub
|
||||
:target: https://github.com/iterative/dvc/releases/latest
|
||||
.. |Packages| image:: https://img.shields.io/badge/deb|pkg|rpm|exe-blue
|
||||
:target: https://dvc.org/doc/install
|
||||
:alt: deb|pkg|rpm|exe
|
||||
|
||||
.. |DOI| image:: https://img.shields.io/badge/DOI-10.5281/zenodo.3677553-blue.svg
|
||||
|
||||
@@ -4,6 +4,7 @@ DVC
|
||||
Make your data science projects reproducible and shareable.
|
||||
"""
|
||||
import dvc.logger
|
||||
from dvc.build import PKG # noqa: F401
|
||||
from dvc.version import __version__, version_tuple # noqa: F401
|
||||
|
||||
dvc.logger.setup()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import os
|
||||
|
||||
|
||||
def get_hook_dirs():
|
||||
return [os.path.dirname(__file__)]
|
||||
@@ -1 +1,2 @@
|
||||
# ruff: noqa: N999
|
||||
hiddenimports = ["win32timezone"]
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
# pylint: disable=import-error
|
||||
from PyInstaller.utils.hooks import collect_submodules, is_module_or_submodule
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
from PyInstaller.utils.hooks import copy_metadata # pylint:disable=import-error
|
||||
|
||||
# needed for `dvc doctor` to show dep versions
|
||||
@@ -21,6 +22,7 @@ hiddenimports = [
|
||||
"dvc_hdfs",
|
||||
"dvc_oss",
|
||||
"dvc_s3",
|
||||
"dvc_ssh",
|
||||
"dvc_webdav",
|
||||
"dvc_webhdfs",
|
||||
# https://github.com/pypa/setuptools/issues/1963
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
from PyInstaller.utils.hooks import copy_metadata # pylint:disable=import-error
|
||||
|
||||
datas = copy_metadata("flatten-dict")
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
# pylint: disable=import-error
|
||||
from PyInstaller.utils.hooks import collect_submodules
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
# ruff: noqa: N999
|
||||
hiddenimports = ["fsspec.implementations.memory"]
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: N999
|
||||
from PyInstaller.utils.hooks import copy_metadata # pylint:disable=import-error
|
||||
|
||||
datas = copy_metadata("pydrive2")
|
||||
+66
-15
@@ -198,33 +198,84 @@ def debugtools(args: Optional["Namespace"] = None, **kwargs):
|
||||
def add_debugging_flags(parser):
|
||||
from argparse import SUPPRESS
|
||||
|
||||
parser.add_argument("--cprofile", action="store_true", default=False, help=SUPPRESS)
|
||||
parser.add_argument("--yappi", action="store_true", default=False, help=SUPPRESS)
|
||||
# For detailed info see:
|
||||
# https://github.com/iterative/dvc/wiki/Debugging,-Profiling-and-Benchmarking-DVC
|
||||
args, _ = parser.parse_known_args()
|
||||
verbose = args.verbose
|
||||
|
||||
def debug_help(msg):
|
||||
if verbose:
|
||||
return msg
|
||||
return SUPPRESS
|
||||
|
||||
parser = parser.add_argument_group("debug options")
|
||||
|
||||
parser.add_argument(
|
||||
"--cprofile",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Generate cprofile data for tools like snakeviz / tuna"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cprofile-dump", help=debug_help("Location to dump cprofile file")
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yappi",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help(
|
||||
"Generate a callgrind file for use with tools like "
|
||||
"kcachegrind / qcachegrind"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yappi-separate-threads",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=SUPPRESS,
|
||||
help=debug_help("Generate one callgrind file per thread"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--viztracer", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument("--viztracer-depth", type=int, help=SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--viztracer-async", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument("--cprofile-dump", help=SUPPRESS)
|
||||
parser.add_argument("--pdb", action="store_true", default=False, help=SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--instrument", action="store_true", default=False, help=SUPPRESS
|
||||
"--viztracer",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Generate a viztracer file for use with vizviewer"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instrument-open", action="store_true", default=False, help=SUPPRESS
|
||||
"--viztracer-depth",
|
||||
type=int,
|
||||
help=debug_help("Set viztracer maximum stack depth"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--viztracer-async",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Treat async tasks as threads"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pdb",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Drop into the pdb/ipdb debugger on any exception"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instrument",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Use pyinstrument CLI profiler"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instrument-open",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=debug_help("Use pyinstrument web profiler"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show-stack",
|
||||
"--ss",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=SUPPRESS,
|
||||
help=debug_help(
|
||||
r"Use Ctrl+T on macOS or Ctrl+\ on Linux to print the stack "
|
||||
"frame currently executing. Unavailable on Windows."
|
||||
),
|
||||
)
|
||||
|
||||
+14
-8
@@ -17,20 +17,26 @@ class Annotation:
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def update(self, **kwargs) -> "Annotation":
|
||||
for attr, value in kwargs.items():
|
||||
if value and hasattr(self, attr):
|
||||
setattr(self, attr, value)
|
||||
return self
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Artifact(Annotation):
|
||||
class Artifact:
|
||||
PARAM_PATH: ClassVar[str] = "path"
|
||||
path: Optional[str] = None
|
||||
PARAM_DESC: ClassVar[str] = "desc"
|
||||
PARAM_TYPE: ClassVar[str] = "type"
|
||||
PARAM_LABELS: ClassVar[str] = "labels"
|
||||
PARAM_META: ClassVar[str] = "meta"
|
||||
|
||||
path: str
|
||||
desc: Optional[str] = None
|
||||
type: Optional[str] = None # noqa: A003
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
ANNOTATION_FIELDS = [field.name for field in fields(Annotation)]
|
||||
|
||||
+6
-2
@@ -2,13 +2,17 @@ from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .data import open # pylint: disable=redefined-builtin
|
||||
from .data import get_url, read
|
||||
from .experiments import exp_save, make_checkpoint
|
||||
from .experiments import exp_save, exp_show
|
||||
from .scm import all_branches, all_commits, all_tags
|
||||
from .show import metrics_show, params_show
|
||||
|
||||
__all__ = [
|
||||
"all_branches",
|
||||
"all_commits",
|
||||
"all_tags",
|
||||
"exp_save",
|
||||
"exp_show",
|
||||
"get_url",
|
||||
"make_checkpoint",
|
||||
"open",
|
||||
"params_show",
|
||||
"metrics_show",
|
||||
|
||||
+60
-21
@@ -36,21 +36,22 @@ def get_url(path, repo=None, rev=None, remote=None):
|
||||
NOTE: This function does not check for the actual existence of the file or
|
||||
directory in the remote storage.
|
||||
"""
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
with _wrap_exceptions(_repo, path):
|
||||
fs_path = _repo.dvcfs.from_os_path(path)
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc_data.index import StorageKeyError
|
||||
|
||||
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"] # pylint: disable=unsubscriptable-object
|
||||
md5 = dvc_info["md5"]
|
||||
|
||||
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
|
||||
repo_kwargs: Dict[str, Any] = {}
|
||||
if remote:
|
||||
repo_kwargs["config"] = {"core": {"remote": remote}}
|
||||
with Repo.open(
|
||||
repo, rev=rev, subrepos=True, uninitialized=True, **repo_kwargs
|
||||
) as _repo:
|
||||
index, entry = _repo.get_data_index_entry(path)
|
||||
with reraise(
|
||||
(StorageKeyError, ValueError),
|
||||
NoRemoteError(f"no remote specified in {_repo}"),
|
||||
):
|
||||
remote_fs, remote_path = index.storage_map.get_remote(entry)
|
||||
return remote_fs.unstrip_protocol(remote_path)
|
||||
|
||||
|
||||
class _OpenContextManager(GCM):
|
||||
@@ -66,13 +67,15 @@ class _OpenContextManager(GCM):
|
||||
raise AttributeError("dvc.api.open() should be used in a with statement.")
|
||||
|
||||
|
||||
def open( # noqa, pylint: disable=redefined-builtin
|
||||
def open( # noqa: A001, pylint: disable=redefined-builtin
|
||||
path: str,
|
||||
repo: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
remote: Optional[str] = None,
|
||||
mode: str = "r",
|
||||
encoding: Optional[str] = None,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
remote_config: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
Opens a file tracked in a DVC project.
|
||||
@@ -114,6 +117,11 @@ def open( # noqa, pylint: disable=redefined-builtin
|
||||
Defaults to None.
|
||||
This should only be used in text mode.
|
||||
Mirrors the namesake parameter in builtin `open()`_.
|
||||
config(dict, optional): config to be passed to the DVC repository.
|
||||
Defaults to None.
|
||||
remote_config(dict, optional): remote config to be passed to the DVC
|
||||
repository.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
_OpenContextManager: A context manager that generatse a corresponding
|
||||
@@ -209,14 +217,29 @@ def open( # noqa, pylint: disable=redefined-builtin
|
||||
"rev": rev,
|
||||
"mode": mode,
|
||||
"encoding": encoding,
|
||||
"config": config,
|
||||
"remote_config": remote_config,
|
||||
}
|
||||
return _OpenContextManager(_open, args, kwargs)
|
||||
|
||||
|
||||
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
repo_kwargs: Dict[str, Any] = {"subrepos": True, "uninitialized": True}
|
||||
if remote:
|
||||
repo_kwargs["config"] = {"core": {"remote": remote}}
|
||||
def _open(
|
||||
path,
|
||||
repo=None,
|
||||
rev=None,
|
||||
remote=None,
|
||||
mode="r",
|
||||
encoding=None,
|
||||
config=None,
|
||||
remote_config=None,
|
||||
):
|
||||
repo_kwargs: Dict[str, Any] = {
|
||||
"subrepos": True,
|
||||
"uninitialized": True,
|
||||
"remote": remote,
|
||||
"config": config,
|
||||
"remote_config": remote_config,
|
||||
}
|
||||
|
||||
with Repo.open(repo, rev=rev, **repo_kwargs) as _repo:
|
||||
with _wrap_exceptions(_repo, path):
|
||||
@@ -251,13 +274,29 @@ def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
|
||||
|
||||
|
||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
def read(
|
||||
path,
|
||||
repo=None,
|
||||
rev=None,
|
||||
remote=None,
|
||||
mode="r",
|
||||
encoding=None,
|
||||
config=None,
|
||||
remote_config=None,
|
||||
):
|
||||
"""
|
||||
Returns the contents of a tracked file (by DVC or Git). For Git repos, HEAD
|
||||
is used unless a rev argument is supplied. The default remote is tried
|
||||
unless a remote argument is supplied.
|
||||
"""
|
||||
with open(
|
||||
path, repo=repo, rev=rev, remote=remote, mode=mode, encoding=encoding
|
||||
path,
|
||||
repo=repo,
|
||||
rev=rev,
|
||||
remote=remote,
|
||||
mode=mode,
|
||||
encoding=encoding,
|
||||
config=config,
|
||||
remote_config=remote_config,
|
||||
) as fd:
|
||||
return fd.read()
|
||||
|
||||
+81
-32
@@ -1,37 +1,9 @@
|
||||
import builtins
|
||||
import os
|
||||
from time import sleep
|
||||
from typing import List, Optional
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage.monitor import CheckpointTask
|
||||
|
||||
|
||||
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)
|
||||
from dvc.repo.experiments.show import tabulate
|
||||
|
||||
|
||||
def exp_save(
|
||||
@@ -69,3 +41,80 @@ def exp_save(
|
||||
return repo.experiments.save(
|
||||
name=name, force=force, include_untracked=include_untracked
|
||||
)
|
||||
|
||||
|
||||
def _postprocess(exp_rows):
|
||||
for exp_row in exp_rows:
|
||||
for k, v in exp_row.items():
|
||||
if isinstance(v, Text):
|
||||
v_str = str(v)
|
||||
try:
|
||||
exp_row[k] = float(v_str)
|
||||
except ValueError:
|
||||
exp_row[k] = v_str
|
||||
|
||||
if not exp_row[k]:
|
||||
exp_row[k] = None
|
||||
|
||||
return exp_rows
|
||||
|
||||
|
||||
def exp_show(
|
||||
repo: Optional[str] = None,
|
||||
revs: Optional[Union[str, List[str]]] = None,
|
||||
num: int = 1,
|
||||
param_deps: bool = False,
|
||||
force: bool = False,
|
||||
config: Optional[Dict] = None,
|
||||
) -> List[Dict]:
|
||||
"""Get DVC experiments tracked in `repo`.
|
||||
|
||||
Without arguments, this function will retrieve all experiments derived from
|
||||
the Git `HEAD`.
|
||||
|
||||
See the options below to customize the experiments retrieved.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
revs (Union[str, List[str]], optional): Git revision(s) (e.g. branch,
|
||||
tag, SHA commit) to use as a reference point to start listing
|
||||
experiments.
|
||||
Defaults to `None`, which will use `HEAD` as starting point.
|
||||
num (int, optional): show experiments from the last `num` commits
|
||||
(first parents) starting from the `revs` baseline.
|
||||
Give a negative value to include all first-parent commits (similar
|
||||
to `git log -n`).
|
||||
Defaults to 1.
|
||||
param_deps (bool, optional): include only parameters that are stage
|
||||
dependencies.
|
||||
Defaults to `False`.
|
||||
force (bool, optional): force re-collection of experiments instead of
|
||||
loading from internal experiments cache.
|
||||
DVC caches `exp_show` data for completed experiments to improve
|
||||
performance of subsequent calls.
|
||||
When `force` is specified, DVC will reload all experiment data and
|
||||
ignore any previously cached results.
|
||||
Defaults to `False`.
|
||||
config (dict, optional): config to be passed through to DVC project.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
List[Dict]: Each item in the list will contain a dictionary with
|
||||
the info for an individual experiment.
|
||||
See Examples below.
|
||||
"""
|
||||
with Repo.open(repo, config=config) as _repo:
|
||||
experiments = _repo.experiments.show(
|
||||
revs=revs,
|
||||
num=num,
|
||||
param_deps=param_deps,
|
||||
force=force,
|
||||
)
|
||||
td, _ = tabulate(experiments, fill_value=None)
|
||||
|
||||
return _postprocess(td.as_dict())
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def all_branches(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git branches in a DVC repository.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
Returns:
|
||||
List[str]: Names of the Git branches.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_branches()
|
||||
|
||||
|
||||
def all_commits(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git commits in a DVC repository.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
Returns:
|
||||
List[str]: SHAs of the Git commits.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_all_commits()
|
||||
|
||||
|
||||
def all_tags(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git tags in a DVC repository.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
Returns:
|
||||
List[str]: Names of the Git tags.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_tags()
|
||||
+18
-10
@@ -7,10 +7,6 @@ from funcy import first
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def _onerror_raise(exception: Exception, *args, **kwargs):
|
||||
raise exception
|
||||
|
||||
|
||||
def _postprocess(results):
|
||||
processed: Dict[str, Dict] = {}
|
||||
for rev, rev_data in results.items():
|
||||
@@ -23,7 +19,6 @@ def _postprocess(results):
|
||||
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
|
||||
@@ -41,6 +36,7 @@ def metrics_show(
|
||||
*targets: str,
|
||||
repo: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
config: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
"""Get metrics tracked in `repo`.
|
||||
|
||||
@@ -69,6 +65,8 @@ def metrics_show(
|
||||
hash or a dvc experiment name.
|
||||
If `repo` is not a Git repo, this option is ignored.
|
||||
If `None`, the current working tree will be used.
|
||||
config (dict, optional): config to be passed through to DVC project.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
@@ -135,13 +133,17 @@ def metrics_show(
|
||||
.. _Git revision:
|
||||
https://git-scm.com/docs/revisions
|
||||
"""
|
||||
from dvc.repo.metrics.show import to_relpath
|
||||
|
||||
with Repo.open(repo) as _repo:
|
||||
with Repo.open(repo, config=config) as _repo:
|
||||
metrics = _repo.metrics.show(
|
||||
targets=targets,
|
||||
revs=rev if rev is None else [rev],
|
||||
onerror=_onerror_raise,
|
||||
on_error="raise",
|
||||
)
|
||||
metrics = {
|
||||
k: to_relpath(_repo.fs, _repo.root_dir, v) for k, v in metrics.items()
|
||||
}
|
||||
|
||||
metrics = _postprocess(metrics)
|
||||
|
||||
@@ -157,6 +159,7 @@ def params_show(
|
||||
stages: Optional[Union[str, Iterable[str]]] = None,
|
||||
rev: Optional[str] = None,
|
||||
deps: bool = False,
|
||||
config: Optional[Dict] = None,
|
||||
) -> Dict:
|
||||
"""Get parameters tracked in `repo`.
|
||||
|
||||
@@ -195,6 +198,8 @@ def params_show(
|
||||
deps (bool, optional): Whether to retrieve only parameters that are
|
||||
stage dependencies or not.
|
||||
Defaults to `False`.
|
||||
config (dict, optional): config to be passed through to DVC project.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
@@ -376,17 +381,20 @@ def params_show(
|
||||
https://git-scm.com/docs/revisions
|
||||
|
||||
"""
|
||||
from dvc.repo.metrics.show import to_relpath
|
||||
|
||||
if isinstance(stages, str):
|
||||
stages = [stages]
|
||||
|
||||
with Repo.open(repo) as _repo:
|
||||
with Repo.open(repo, config=config) as _repo:
|
||||
params = _repo.params.show(
|
||||
revs=rev if rev is None else [rev],
|
||||
targets=targets,
|
||||
deps=deps,
|
||||
onerror=_onerror_raise,
|
||||
deps_only=deps,
|
||||
on_error="raise",
|
||||
stages=stages,
|
||||
)
|
||||
params = {k: to_relpath(_repo.fs, _repo.root_dir, v) for k, v in params.items()}
|
||||
|
||||
params = _postprocess(params)
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
# file is created during dvc build
|
||||
# pylint:disable=unused-import
|
||||
from . import _build # type: ignore[attr-defined, import]
|
||||
|
||||
PKG: Optional[str] = _build.PKG # type: ignore[assignment]
|
||||
except ImportError:
|
||||
PKG = None # type: ignore[assignment]
|
||||
+84
-19
@@ -1,40 +1,49 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
LEGACY_HASH_NAMES = {"md5-dos2unix", "params"}
|
||||
|
||||
|
||||
def _get_odb(repo, settings, fs=None):
|
||||
def _get_odb(
|
||||
repo,
|
||||
settings,
|
||||
fs=None,
|
||||
prefix: Optional[Tuple[str, ...]] = None,
|
||||
hash_name: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
if not settings:
|
||||
return None
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(repo, **settings)
|
||||
cls, config, fs_path = get_cloud_fs(repo.config, **settings)
|
||||
fs = fs or cls(**config)
|
||||
if prefix:
|
||||
fs_path = fs.path.join(fs_path, *prefix)
|
||||
if hash_name:
|
||||
config["hash_name"] = hash_name
|
||||
return get_odb(fs, fs_path, state=repo.state, **config)
|
||||
|
||||
|
||||
class CacheManager:
|
||||
CACHE_DIR = "cache"
|
||||
CLOUD_SCHEMES = [
|
||||
Schemes.S3,
|
||||
Schemes.GS,
|
||||
Schemes.SSH,
|
||||
Schemes.HDFS,
|
||||
Schemes.WEBHDFS,
|
||||
]
|
||||
FILES_DIR = "files"
|
||||
|
||||
def __init__(self, repo):
|
||||
self._repo = repo
|
||||
self.config = config = repo.config["cache"]
|
||||
self._odb = {}
|
||||
|
||||
default = None
|
||||
if repo and repo.local_dvc_dir:
|
||||
default = os.path.join(repo.local_dvc_dir, self.CACHE_DIR)
|
||||
|
||||
local = config.get("local")
|
||||
default = self.default_local_cache_dir
|
||||
|
||||
if local:
|
||||
settings = {"name": local}
|
||||
@@ -53,25 +62,81 @@ class CacheManager:
|
||||
if not isinstance(repo.fs, GitFileSystem):
|
||||
kwargs["fs"] = repo.fs
|
||||
|
||||
odb = _get_odb(repo, settings, **kwargs)
|
||||
odb = _get_odb(
|
||||
repo,
|
||||
settings,
|
||||
prefix=(self.FILES_DIR, DEFAULT_ALGORITHM),
|
||||
**kwargs,
|
||||
)
|
||||
self._odb["repo"] = odb
|
||||
self._odb[Schemes.LOCAL] = odb
|
||||
legacy_odb = _get_odb(repo, settings, hash_name="md5-dos2unix", **kwargs)
|
||||
self._odb["legacy"] = legacy_odb
|
||||
|
||||
def _init_odb(self, schemes):
|
||||
for scheme in schemes:
|
||||
remote = self.config.get(scheme)
|
||||
settings = {"name": remote} if remote else None
|
||||
self._odb[scheme] = _get_odb(self._repo, settings)
|
||||
self._odb[scheme] = _get_odb(
|
||||
self._repo,
|
||||
settings,
|
||||
prefix=(self.FILES_DIR, DEFAULT_ALGORITHM),
|
||||
)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in self._odb and name in self.CLOUD_SCHEMES:
|
||||
self._init_odb([name])
|
||||
|
||||
try:
|
||||
return self._odb[name]
|
||||
except KeyError as exc:
|
||||
raise AttributeError from exc
|
||||
|
||||
def by_scheme(self):
|
||||
self._init_odb(self.CLOUD_SCHEMES)
|
||||
yield from self._odb.items()
|
||||
|
||||
@property
|
||||
def local_cache_dir(self) -> str:
|
||||
"""Return base local cache directory without any prefixes.
|
||||
|
||||
(i.e. `dvc cache dir`).
|
||||
"""
|
||||
return self.legacy.path
|
||||
|
||||
@property
|
||||
def default_local_cache_dir(self) -> Optional[str]:
|
||||
repo = self._repo
|
||||
if repo and repo.local_dvc_dir:
|
||||
return os.path.join(repo.local_dvc_dir, self.CACHE_DIR)
|
||||
return None
|
||||
|
||||
|
||||
def migrate_2_to_3(repo: "Repo", dry: bool = False):
|
||||
"""Migrate legacy 2.x objects to 3.x cache.
|
||||
|
||||
Legacy 'md5-dos2unix' objects will be re-hashed with 'md5', added to 3.x cache,
|
||||
and then a link from the legacy 2.x location to the 3.x location will be created.
|
||||
"""
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
from dvc.ui import ui
|
||||
from dvc_data.hashfile.db.migrate import migrate, prepare
|
||||
|
||||
src = repo.cache.legacy
|
||||
dest = repo.cache.local
|
||||
if dry:
|
||||
oids = list(src._list_oids()) # pylint: disable=protected-access
|
||||
ui.write(
|
||||
f"{len(oids)} files will be re-hashed and migrated to the DVC 3.0 cache "
|
||||
"location."
|
||||
)
|
||||
return
|
||||
|
||||
with TqdmCallback(
|
||||
desc="Computing DVC 3.0 hashes",
|
||||
unit="files",
|
||||
) as cb:
|
||||
migration = prepare(src, dest, callback=cb)
|
||||
|
||||
with TqdmCallback(
|
||||
desc="Migrating to DVC 3.0 cache",
|
||||
unit="files",
|
||||
) as cb:
|
||||
count = migrate(migration, callback=cb)
|
||||
ui.write(f"Migrated {count} files to DVC 3.0 cache location.")
|
||||
|
||||
+7
-8
@@ -75,7 +75,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
from dvc.fs import AuthError, ConfigError, RemoteMissingDepsError
|
||||
|
||||
if isinstance(exc, RemoteMissingDepsError):
|
||||
from dvc.utils.pkg import PKG
|
||||
from dvc import PKG
|
||||
|
||||
proto = exc.protocol
|
||||
by_pkg = {
|
||||
@@ -83,9 +83,9 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
"conda": f"conda install -c conda-forge dvc-{proto}",
|
||||
}
|
||||
|
||||
cmd = by_pkg.get(PKG)
|
||||
if cmd:
|
||||
if PKG in by_pkg:
|
||||
link = format_link("https://dvc.org/doc/install")
|
||||
cmd = by_pkg.get(PKG)
|
||||
hint = (
|
||||
"To install dvc with those dependencies, run:\n"
|
||||
"\n"
|
||||
@@ -190,10 +190,9 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
if level and level <= logging.DEBUG:
|
||||
from platform import platform, python_implementation, python_version
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.utils.pkg import PKG
|
||||
from dvc import PKG, __version__
|
||||
|
||||
pyv = " ".join([python_implementation(), python_version()])
|
||||
pyv = f"{python_implementation()} {python_version()}"
|
||||
pkg = f" ({PKG})" if PKG else ""
|
||||
logger.debug("v%s%s, %s on %s", __version__, pkg, pyv, platform())
|
||||
logger.debug("command: %s", " ".join(argv or sys.argv))
|
||||
@@ -231,7 +230,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
logger.exception("")
|
||||
except DvcParserError:
|
||||
ret = 254
|
||||
except Exception as exc: # noqa, pylint: disable=broad-except
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
# pylint: disable=no-member
|
||||
ret = _log_exceptions(exc) or 255
|
||||
|
||||
@@ -245,7 +244,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
finally:
|
||||
logger.setLevel(outer_log_level)
|
||||
|
||||
from dvc.external_repo import clean_repos
|
||||
from dvc.repo.open_repo import clean_repos
|
||||
|
||||
# Remove cached repos in the end of the call, these are anonymous
|
||||
# so won't be reused by any other subsequent run anyway.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
from argparse import _AppendAction
|
||||
|
||||
|
||||
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
|
||||
def __call__(self, parser, namespace, values, option_string=None): # noqa: ARG002
|
||||
from funcy import ldistinct
|
||||
|
||||
items = getattr(namespace, self.dest) or []
|
||||
items.extend(map(str.strip, values.split(",")))
|
||||
setattr(namespace, self.dest, ldistinct(items))
|
||||
|
||||
|
||||
class KeyValueArgs(_AppendAction):
|
||||
def __call__(self, parser, namespace, values, option_string=None): # noqa: ARG002
|
||||
items = getattr(namespace, self.dest) or {}
|
||||
for value in filter(bool, values):
|
||||
key, _, value = value.partition("=")
|
||||
items[key.strip()] = value
|
||||
|
||||
setattr(namespace, self.dest, items)
|
||||
+1
-5
@@ -31,7 +31,6 @@ from dvc.commands import (
|
||||
install,
|
||||
ls,
|
||||
ls_url,
|
||||
machine,
|
||||
metrics,
|
||||
move,
|
||||
params,
|
||||
@@ -41,7 +40,6 @@ from dvc.commands import (
|
||||
remove,
|
||||
repro,
|
||||
root,
|
||||
run,
|
||||
stage,
|
||||
unprotect,
|
||||
update,
|
||||
@@ -62,7 +60,6 @@ COMMANDS = [
|
||||
remove,
|
||||
move,
|
||||
unprotect,
|
||||
run,
|
||||
repro,
|
||||
data_sync,
|
||||
gc,
|
||||
@@ -91,7 +88,6 @@ COMMANDS = [
|
||||
stage,
|
||||
experiments,
|
||||
check_ignore,
|
||||
machine,
|
||||
data,
|
||||
]
|
||||
|
||||
@@ -139,7 +135,6 @@ def get_parent_parser():
|
||||
from dvc._debug import add_debugging_flags
|
||||
|
||||
parent_parser = argparse.ArgumentParser(add_help=False)
|
||||
add_debugging_flags(parent_parser)
|
||||
log_level_group = parent_parser.add_mutually_exclusive_group()
|
||||
log_level_group.add_argument(
|
||||
"-q", "--quiet", action="count", default=0, help="Be quiet."
|
||||
@@ -147,6 +142,7 @@ def get_parent_parser():
|
||||
log_level_group.add_argument(
|
||||
"-v", "--verbose", action="count", default=0, help="Be verbose."
|
||||
)
|
||||
add_debugging_flags(parent_parser)
|
||||
|
||||
return parent_parser
|
||||
|
||||
|
||||
+25
-3
@@ -1,3 +1,27 @@
|
||||
import argparse
|
||||
|
||||
|
||||
class DictAction(argparse.Action):
|
||||
def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
|
||||
d = getattr(args, self.dest) or {}
|
||||
|
||||
if isinstance(values, list):
|
||||
kvs = values
|
||||
else:
|
||||
kvs = [values]
|
||||
|
||||
for kv in kvs:
|
||||
key, value = kv.split("=", 1)
|
||||
if not value:
|
||||
raise argparse.ArgumentError(
|
||||
self,
|
||||
f'Could not parse argument "{values}" as k1=v1 k2=v2 ... format',
|
||||
)
|
||||
d[key] = value
|
||||
|
||||
setattr(args, self.dest, d)
|
||||
|
||||
|
||||
def fix_subparsers(subparsers):
|
||||
"""Workaround for bug in Python 3. See more info at:
|
||||
https://bugs.python.org/issue16308
|
||||
@@ -16,9 +40,7 @@ def append_doc_link(help_message, path):
|
||||
if not path:
|
||||
return help_message
|
||||
doc_base = "https://man.dvc.org/"
|
||||
return "{message}\nDocumentation: {link}".format(
|
||||
message=help_message, link=format_link(doc_base + path)
|
||||
)
|
||||
return f"{help_message}\nDocumentation: {format_link(doc_base + path)}"
|
||||
|
||||
|
||||
def hide_subparsers_from_help(subparsers):
|
||||
|
||||
+44
-65
@@ -2,68 +2,61 @@ import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import KeyValueArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"--desc",
|
||||
type=str,
|
||||
metavar="<text>",
|
||||
help="User description of the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--meta",
|
||||
metavar="key=value",
|
||||
nargs=1,
|
||||
action=KeyValueArgs,
|
||||
help="Custom metadata to add to the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--label",
|
||||
dest="labels",
|
||||
type=str,
|
||||
action="append",
|
||||
metavar="<str>",
|
||||
help="Label for the data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
type=str,
|
||||
metavar="<str>",
|
||||
help="Type of the data.",
|
||||
)
|
||||
|
||||
|
||||
class CmdAdd(CmdBase):
|
||||
def validate_args(self) -> None:
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
args = self.args
|
||||
invalid_opt = None
|
||||
|
||||
if args.to_remote or args.out:
|
||||
message = "{option} can't be used with "
|
||||
message += "--to-remote" if args.to_remote else "--out"
|
||||
if len(args.targets) != 1:
|
||||
invalid_opt = "multiple targets"
|
||||
elif args.glob:
|
||||
invalid_opt = "--glob option"
|
||||
elif args.no_commit:
|
||||
invalid_opt = "--no-commit option"
|
||||
else:
|
||||
message = "{option} can't be used without --to-remote"
|
||||
if args.remote:
|
||||
invalid_opt = "--remote"
|
||||
elif args.remote_jobs:
|
||||
invalid_opt = "--remote-jobs"
|
||||
|
||||
if invalid_opt is not None:
|
||||
raise InvalidArgumentError(message.format(option=invalid_opt))
|
||||
|
||||
def run(self):
|
||||
from dvc.exceptions import DvcException, RecursiveAddingWhileUsingFilename
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
|
||||
try:
|
||||
if len(self.args.targets) > 1 and self.args.file:
|
||||
raise RecursiveAddingWhileUsingFilename()
|
||||
self.validate_args()
|
||||
except InvalidArgumentError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
try:
|
||||
self.repo.add(
|
||||
self.args.targets,
|
||||
recursive=self.args.recursive,
|
||||
no_commit=self.args.no_commit,
|
||||
fname=self.args.file,
|
||||
external=self.args.external,
|
||||
glob=self.args.glob,
|
||||
desc=self.args.desc,
|
||||
out=self.args.out,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
jobs=self.args.jobs,
|
||||
remote_jobs=self.args.remote_jobs,
|
||||
force=self.args.force,
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
@@ -80,36 +73,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help=ADD_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Recursively add files under directory targets.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-commit",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Don't put files/directories into cache.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--external",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allow targets that are outside of the DVC repository.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--glob",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allows targets containing shell-style wildcards.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file",
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
@@ -129,8 +104,7 @@ def add_parser(subparsers, parent_parser):
|
||||
metavar="<name>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
"--remote-jobs",
|
||||
type=int,
|
||||
help=(
|
||||
"Only used along with '--to-remote'. "
|
||||
@@ -140,8 +114,13 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(parser)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
|
||||
+29
-2
@@ -2,6 +2,7 @@ import argparse
|
||||
import os
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.commands.config import CmdConfig
|
||||
from dvc.ui import ui
|
||||
@@ -36,6 +37,14 @@ class CmdCacheDir(CmdConfig):
|
||||
return 0
|
||||
|
||||
|
||||
class CmdCacheMigrate(CmdBase):
|
||||
def run(self):
|
||||
from dvc.cachemgr import migrate_2_to_3
|
||||
|
||||
migrate_2_to_3(self.repo, dry=self.args.dry)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from dvc.commands.config import parent_config_parser
|
||||
|
||||
@@ -54,8 +63,6 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Use `dvc cache CMD --help` for command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(cache_subparsers)
|
||||
|
||||
parent_cache_config_parser = argparse.ArgumentParser(
|
||||
add_help=False, parents=[parent_config_parser]
|
||||
)
|
||||
@@ -86,3 +93,23 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
).complete = completion.DIR
|
||||
cache_dir_parser.set_defaults(func=CmdCacheDir)
|
||||
|
||||
CACHE_MIGRATE_HELP = "Migrate cached files to the DVC 3.0 cache location."
|
||||
cache_migrate_parser = cache_subparsers.add_parser(
|
||||
"migrate",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(CACHE_HELP, "cache/migrate"),
|
||||
help=CACHE_MIGRATE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
cache_migrate_parser.add_argument(
|
||||
"--dry",
|
||||
help=(
|
||||
"Only print actions which would be taken without actually migrating "
|
||||
"any data."
|
||||
),
|
||||
action="store_true",
|
||||
)
|
||||
cache_migrate_parser.set_defaults(func=CmdCacheMigrate)
|
||||
|
||||
fix_subparsers(cache_subparsers)
|
||||
|
||||
@@ -37,6 +37,7 @@ class CmdCheckout(CmdBase):
|
||||
force=self.args.force,
|
||||
relink=self.args.relink,
|
||||
recursive=self.args.recursive,
|
||||
allow_missing=self.args.allow_missing,
|
||||
)
|
||||
except CheckoutError as _exc:
|
||||
exc = _exc
|
||||
@@ -101,6 +102,12 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Recreate links or copies from cache to workspace.",
|
||||
)
|
||||
checkout_parser.add_argument(
|
||||
"--allow-missing",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Ignore errors if some of the files or directories are missing.",
|
||||
)
|
||||
checkout_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
|
||||
@@ -47,7 +47,10 @@ def add_parser(subparsers, parent_parser):
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Commit even if hash value for dependencies/outputs changed.",
|
||||
help=(
|
||||
"Commit data even if hash values for dependencies or "
|
||||
"outputs did not change."
|
||||
),
|
||||
)
|
||||
commit_parser.add_argument(
|
||||
"-d",
|
||||
|
||||
@@ -9,6 +9,9 @@ from dvc.ui import ui
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SUPPORTED_SHELLS = ["bash", "zsh"]
|
||||
|
||||
|
||||
class CmdCompletion(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
import shtab
|
||||
@@ -35,6 +38,6 @@ def add_parser(subparsers, parent_parser):
|
||||
"--shell",
|
||||
help="Shell syntax for completions.",
|
||||
default="bash",
|
||||
choices=["bash", "zsh"],
|
||||
choices=SUPPORTED_SHELLS,
|
||||
)
|
||||
completion_parser.set_defaults(func=CmdCompletion)
|
||||
|
||||
@@ -176,7 +176,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
dag_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
+12
-122
@@ -1,14 +1,11 @@
|
||||
import argparse
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from funcy import chunks, compact, log_durations
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.actions import CommaSeparatedArgs
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers, hide_subparsers_from_help
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
@@ -93,9 +90,7 @@ class CmdDataStatus(CmdBase):
|
||||
ui.write(f" ({hint})")
|
||||
|
||||
if isinstance(stage_status, dict):
|
||||
items = [
|
||||
": ".join([state, file]) for file, state in stage_status.items()
|
||||
]
|
||||
items = [f"{state}: {file}" for file, state in stage_status.items()]
|
||||
else:
|
||||
items = stage_status
|
||||
|
||||
@@ -117,6 +112,7 @@ class CmdDataStatus(CmdBase):
|
||||
status = self.repo.data_status(
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
remote_refresh=self.args.remote_refresh,
|
||||
)
|
||||
|
||||
@@ -131,63 +127,6 @@ class CmdDataStatus(CmdBase):
|
||||
return self._show_status(status)
|
||||
|
||||
|
||||
class CmdDataLs(CmdBase):
|
||||
@staticmethod
|
||||
def _show_table(
|
||||
d: Iterable[Dict[str, Any]],
|
||||
filter_types: Set[str],
|
||||
filter_labels: Set[str],
|
||||
markdown: bool = False,
|
||||
) -> None:
|
||||
from rich.style import Style
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
td = TabularData(
|
||||
columns=["Path", "Type", "Labels", "Description"], fill_value="-"
|
||||
)
|
||||
for entry in sorted(d, key=itemgetter("path")):
|
||||
typ = entry.get("type", "")
|
||||
desc = entry.get("desc", "-")
|
||||
labels = entry.get("labels", [])
|
||||
|
||||
if filter_types and typ not in filter_types:
|
||||
continue
|
||||
if filter_labels and filter_labels.isdisjoint(labels):
|
||||
continue
|
||||
|
||||
rich_label = ui.rich_text()
|
||||
for index, label in enumerate(labels):
|
||||
if index:
|
||||
rich_label.append(",")
|
||||
style = Style(bold=label in filter_labels, color="green")
|
||||
rich_label.append(label, style=style)
|
||||
|
||||
if markdown and desc:
|
||||
desc = desc.partition("\n")[0]
|
||||
|
||||
path = ui.rich_text(entry["path"], style="cyan")
|
||||
type_style = Style(bold=typ in filter_types, color="yellow")
|
||||
typ = ui.rich_text(entry.get("type", ""), style=type_style)
|
||||
td.append([path, typ or "-", rich_label or "-", desc])
|
||||
|
||||
td.render(markdown=markdown, rich_table=True)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo.data import ls
|
||||
|
||||
filter_labels = set(self.args.labels)
|
||||
filter_types = set(self.args.type)
|
||||
d = ls(self.repo, targets=self.args.targets, recursive=self.args.recursive)
|
||||
self._show_table(
|
||||
d,
|
||||
filter_labels=filter_labels,
|
||||
filter_types=filter_types,
|
||||
markdown=self.args.markdown,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
data_parser = subparsers.add_parser(
|
||||
"data",
|
||||
@@ -216,13 +155,6 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="json",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--granular",
|
||||
action="store_true",
|
||||
@@ -244,57 +176,15 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--remote-refresh",
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Refresh remote index.",
|
||||
help="Show files not in remote.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--no-remote-refresh",
|
||||
dest="remote_refresh",
|
||||
action="store_false",
|
||||
help="Use cached remote index (don't check remote).",
|
||||
)
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
DATA_LS_HELP = "List data tracked by DVC with its metadata."
|
||||
data_ls_parser = data_subparsers.add_parser(
|
||||
"ls",
|
||||
aliases=["list"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DATA_LS_HELP, "data/ls"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
add_help=False,
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
dest="markdown",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--type",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of type to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"--labels",
|
||||
action=CommaSeparatedArgs,
|
||||
default=[],
|
||||
help="Comma-separated list of labels to filter.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Recursively list from the specified directories.",
|
||||
)
|
||||
data_ls_parser.add_argument(
|
||||
"targets",
|
||||
default=None,
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
),
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
data_ls_parser.set_defaults(func=CmdDataLs)
|
||||
hide_subparsers_from_help(data_subparsers)
|
||||
|
||||
@@ -91,6 +91,8 @@ class CmdDataFetch(CmdDataBase):
|
||||
with_deps=self.args.with_deps,
|
||||
recursive=self.args.recursive,
|
||||
run_cache=self.args.run_cache,
|
||||
max_size=self.args.max_size,
|
||||
types=self.args.types,
|
||||
)
|
||||
self.log_summary({"fetched": processed_files_count})
|
||||
except DvcException:
|
||||
@@ -322,6 +324,22 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"--max-size",
|
||||
type=int,
|
||||
help="Fetch data files/directories that are each below specified size (bytes).",
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"--type",
|
||||
dest="types",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"Only fetch data files/directories that are of a particular "
|
||||
"type (metrics, plots)."
|
||||
),
|
||||
choices=["metrics", "plots"],
|
||||
)
|
||||
fetch_parser.set_defaults(func=CmdDataFetch)
|
||||
|
||||
# Status
|
||||
@@ -403,7 +421,6 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show status in JSON format.",
|
||||
|
||||
@@ -21,7 +21,7 @@ class CmdDestroy(CmdBase):
|
||||
)
|
||||
|
||||
if not self.args.force and not ui.confirm(statement):
|
||||
raise DvcException(
|
||||
raise DvcException( # noqa: TRY301
|
||||
"cannot destroy without a confirmation from the user."
|
||||
" Use `-f` to force."
|
||||
)
|
||||
|
||||
@@ -184,7 +184,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
help="Format the output into a JSON",
|
||||
action="store_true",
|
||||
default=False,
|
||||
@@ -197,7 +196,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
action="store_true",
|
||||
dest="markdown",
|
||||
|
||||
@@ -7,13 +7,12 @@ from dvc.commands.experiments import (
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
init,
|
||||
ls,
|
||||
pull,
|
||||
push,
|
||||
queue_worker,
|
||||
remove,
|
||||
rename,
|
||||
run,
|
||||
save,
|
||||
show,
|
||||
@@ -25,13 +24,12 @@ SUB_COMMANDS = [
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
init,
|
||||
ls,
|
||||
pull,
|
||||
push,
|
||||
queue_worker,
|
||||
remove,
|
||||
rename,
|
||||
run,
|
||||
save,
|
||||
show,
|
||||
@@ -82,6 +80,7 @@ def add_rev_selection_flags(
|
||||
experiments_subcmd_parser.add_argument(
|
||||
"--rev",
|
||||
type=str,
|
||||
action="append",
|
||||
default=None,
|
||||
help=msg,
|
||||
metavar="<commit>",
|
||||
|
||||
@@ -83,14 +83,12 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
@@ -20,6 +20,7 @@ class CmdExecutorRun(CmdBaseNoRepo):
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
infofile=self.args.infofile,
|
||||
copy_paths=self.args.copy_paths,
|
||||
message=self.args.message,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -47,4 +48,11 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
" Only used if `--temp` or `--queue` is specified."
|
||||
),
|
||||
)
|
||||
exec_run_parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
exec_run_parser.set_defaults(func=CmdExecutorRun)
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _raise_error_if_all_disabled(**kwargs):
|
||||
if not any(kwargs.values()):
|
||||
raise InvalidArgumentError(
|
||||
"Either of `-w|--workspace`, `-a|--all-branches`, `-T|--all-tags`,"
|
||||
" `--all-commits` or `--date` needs to be set."
|
||||
)
|
||||
|
||||
|
||||
class CmdExperimentsGC(CmdBase):
|
||||
def run(self):
|
||||
_raise_error_if_all_disabled(
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
commit_date=self.args.commit_date,
|
||||
workspace=self.args.workspace,
|
||||
)
|
||||
|
||||
msg = "This will remove all experiments except those derived from "
|
||||
|
||||
msg += "the workspace"
|
||||
if self.args.all_commits:
|
||||
msg += " and all git commits"
|
||||
else:
|
||||
if self.args.all_branches and self.args.all_tags:
|
||||
msg += " and all git branches and tags"
|
||||
elif self.args.all_branches:
|
||||
msg += " and all git branches"
|
||||
elif self.args.all_tags:
|
||||
msg += " and all git tags"
|
||||
|
||||
if self.args.commit_date:
|
||||
msg += f" and all git commits before date {self.args.commit_date}"
|
||||
msg += " of the current repo."
|
||||
|
||||
if self.args.queued:
|
||||
msg += " Run queued experiments will be preserved."
|
||||
else:
|
||||
msg += " Run queued experiments will be removed."
|
||||
|
||||
logger.warning(msg)
|
||||
|
||||
msg = "Are you sure you want to proceed?"
|
||||
if not self.args.force and not ui.confirm(msg):
|
||||
return 1
|
||||
|
||||
removed = self.repo.experiments.gc(
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
workspace=self.args.workspace,
|
||||
commit_date=self.args.commit_date,
|
||||
queued=self.args.queued,
|
||||
)
|
||||
|
||||
if removed:
|
||||
ui.write(
|
||||
f"Removed {removed} experiments.",
|
||||
"To remove unused cache files",
|
||||
"use 'dvc gc'.",
|
||||
)
|
||||
else:
|
||||
ui.write("No experiments to remove.")
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_GC_HELP = "Garbage collect unneeded experiments."
|
||||
EXPERIMENTS_GC_DESCRIPTION = (
|
||||
"Removes all experiments which are not derived"
|
||||
" from the specified Git revisions."
|
||||
)
|
||||
experiments_gc_parser = experiments_subparsers.add_parser(
|
||||
"gc",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_GC_DESCRIPTION, "exp/gc"),
|
||||
help=EXPERIMENTS_GC_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-w",
|
||||
"--workspace",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep experiments derived from the current workspace.",
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep experiments derived from the tips of all Git branches.",
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-T",
|
||||
"--all-tags",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep experiments derived from all Git tags.",
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-A",
|
||||
"--all-commits",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep experiments derived from all Git commits.",
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"--date",
|
||||
type=str,
|
||||
dest="commit_date",
|
||||
metavar="<YYYY-MM-DD>",
|
||||
default=None,
|
||||
help=(
|
||||
"Keep experiments from the commits after (inclusive) a certain "
|
||||
"date. Date must match the extended ISO 8601 format (YYYY-MM-DD)."
|
||||
),
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"--queued",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Keep queued experiments (experiments run queue will be cleared "
|
||||
"by default)."
|
||||
),
|
||||
)
|
||||
experiments_gc_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Force garbage collection - automatically agree to all prompts.",
|
||||
)
|
||||
experiments_gc_parser.set_defaults(func=CmdExperimentsGC)
|
||||
@@ -1,219 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from funcy import compact
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.dependency import Dependency
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsInit(CmdBase):
|
||||
DEFAULT_NAME = "train"
|
||||
CODE = "src"
|
||||
DATA = "data"
|
||||
MODELS = "models"
|
||||
DEFAULT_METRICS = "metrics.json"
|
||||
DEFAULT_PARAMS = "params.yaml"
|
||||
PLOTS = "plots"
|
||||
DEFAULTS = {
|
||||
"code": CODE,
|
||||
"data": DATA,
|
||||
"models": MODELS,
|
||||
"metrics": DEFAULT_METRICS,
|
||||
"params": DEFAULT_PARAMS,
|
||||
"plots": PLOTS,
|
||||
}
|
||||
|
||||
def run(self):
|
||||
from dvc.commands.stage import parse_cmd
|
||||
|
||||
cmd = parse_cmd(self.args.command)
|
||||
if not self.args.interactive and not cmd:
|
||||
raise InvalidArgumentError("command is not specified")
|
||||
|
||||
from dvc.repo.experiments.init import init
|
||||
|
||||
defaults = {}
|
||||
if not self.args.explicit:
|
||||
config = self.repo.config["exp"]
|
||||
defaults.update({**self.DEFAULTS, **config})
|
||||
|
||||
cli_args = compact(
|
||||
{
|
||||
"cmd": cmd,
|
||||
"code": self.args.code,
|
||||
"data": self.args.data,
|
||||
"models": self.args.models,
|
||||
"metrics": self.args.metrics,
|
||||
"params": self.args.params,
|
||||
"plots": self.args.plots,
|
||||
"live": self.args.live,
|
||||
}
|
||||
)
|
||||
|
||||
initialized_stage, initialized_deps, initialized_out_dirs = init(
|
||||
self.repo,
|
||||
name=self.args.name,
|
||||
type=self.args.type,
|
||||
defaults=defaults,
|
||||
overrides=cli_args,
|
||||
interactive=self.args.interactive,
|
||||
force=self.args.force,
|
||||
)
|
||||
self._post_init_display(
|
||||
initialized_stage, initialized_deps, initialized_out_dirs
|
||||
)
|
||||
if self.args.run:
|
||||
self.repo.experiments.run(targets=[initialized_stage.addressing])
|
||||
return 0
|
||||
|
||||
def _post_init_display(
|
||||
self,
|
||||
stage: "PipelineStage",
|
||||
new_deps: List["Dependency"],
|
||||
new_out_dirs: List[str],
|
||||
) -> None:
|
||||
from dvc.utils import humanize
|
||||
|
||||
path_fmt = "[green]{}[/green]".format
|
||||
if new_deps:
|
||||
deps_paths = humanize.join(map(path_fmt, new_deps))
|
||||
ui.write(f"Creating dependencies: {deps_paths}", styled=True)
|
||||
|
||||
if new_out_dirs:
|
||||
out_dirs_paths = humanize.join(map(path_fmt, new_out_dirs))
|
||||
ui.write(f"Creating output directories: {out_dirs_paths}", styled=True)
|
||||
|
||||
ui.write(
|
||||
f"Creating [b]{self.args.name}[/b] stage in [green]dvc.yaml[/]",
|
||||
styled=True,
|
||||
)
|
||||
if stage.outs or not self.args.run:
|
||||
# separate the above status-like messages with help/tips section
|
||||
ui.write(styled=True)
|
||||
|
||||
if stage.outs:
|
||||
outs_paths = humanize.join(map(path_fmt, stage.outs))
|
||||
tips = f"Ensure your experiment command creates {outs_paths}."
|
||||
ui.write(tips, styled=True)
|
||||
|
||||
if not self.args.run:
|
||||
ui.write(
|
||||
'You can now run your experiment using [b]"dvc exp run"[/].',
|
||||
styled=True,
|
||||
)
|
||||
else:
|
||||
# separate between `exp.run` output and `dvc exp init` output
|
||||
ui.write(styled=True)
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_INIT_HELP = "Quickly setup any project to use experiments."
|
||||
experiments_init_parser = experiments_subparsers.add_parser(
|
||||
"init",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_INIT_HELP, "exp/init"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=EXPERIMENTS_INIT_HELP,
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"command",
|
||||
nargs=argparse.REMAINDER,
|
||||
help="Command to execute.",
|
||||
metavar="command",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--run",
|
||||
action="store_true",
|
||||
help="Run the experiment after initializing it",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--interactive",
|
||||
"-i",
|
||||
action="store_true",
|
||||
help="Prompt for values that are not provided",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Overwrite existing stage",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--explicit",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only use the path values explicitly provided",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--name",
|
||||
"-n",
|
||||
help="Name of the stage to create (default: %(default)s)",
|
||||
default=CmdExperimentsInit.DEFAULT_NAME,
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--code",
|
||||
help=(
|
||||
"Path to the source file or directory "
|
||||
"which your experiments depend"
|
||||
f" (default: {CmdExperimentsInit.CODE})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--data",
|
||||
help=(
|
||||
"Path to the data file or directory "
|
||||
"which your experiments depend"
|
||||
f" (default: {CmdExperimentsInit.DATA})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--models",
|
||||
help=(
|
||||
"Path to the model file or directory for your experiments"
|
||||
f" (default: {CmdExperimentsInit.MODELS})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--params",
|
||||
help=(
|
||||
"Path to the parameters file for your experiments"
|
||||
f" (default: {CmdExperimentsInit.DEFAULT_PARAMS})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--metrics",
|
||||
help=(
|
||||
"Path to the metrics file for your experiments"
|
||||
f" (default: {CmdExperimentsInit.DEFAULT_METRICS})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--plots",
|
||||
help=(
|
||||
"Path to the plots file or directory for your experiments"
|
||||
f" (default: {CmdExperimentsInit.PLOTS})"
|
||||
),
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--live",
|
||||
help="Path to log dvclive outputs for your experiments",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--type",
|
||||
choices=["default", "checkpoint"],
|
||||
default="default",
|
||||
help="Select type of stage to create (default: %(default)s)",
|
||||
)
|
||||
experiments_init_parser.set_defaults(func=CmdExperimentsInit)
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -11,19 +12,43 @@ logger = logging.getLogger(__name__)
|
||||
class CmdExperimentsList(CmdBase):
|
||||
def run(self):
|
||||
name_only = self.args.name_only
|
||||
sha_only = self.args.sha_only
|
||||
git_remote = self.args.git_remote
|
||||
if sha_only and git_remote:
|
||||
raise InvalidArgumentError("--sha-only not supported with git_remote.")
|
||||
exps = self.repo.experiments.ls(
|
||||
all_commits=self.args.all_commits,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
git_remote=self.args.git_remote,
|
||||
git_remote=git_remote,
|
||||
)
|
||||
|
||||
from dvc.repo.experiments.utils import describe
|
||||
from dvc.scm import Git
|
||||
|
||||
if name_only or sha_only:
|
||||
names = {}
|
||||
else:
|
||||
assert isinstance(self.repo.scm, Git)
|
||||
names = describe(
|
||||
self.repo.scm,
|
||||
(baseline for baseline in exps),
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
for baseline in exps:
|
||||
if not name_only:
|
||||
ui.write(f"{baseline}:")
|
||||
for exp_name in exps[baseline]:
|
||||
indent = "" if name_only else "\t"
|
||||
ui.write(f"{indent}{exp_name}")
|
||||
if not (name_only or sha_only):
|
||||
name = names.get(baseline) or baseline[:7]
|
||||
ui.write(f"{name}:")
|
||||
for exp_name, rev in exps[baseline]:
|
||||
if name_only:
|
||||
ui.write(exp_name)
|
||||
elif sha_only:
|
||||
ui.write(rev)
|
||||
elif rev:
|
||||
ui.write(f"\t{rev[:7]} [{exp_name}]")
|
||||
else:
|
||||
ui.write(f"\t{exp_name}")
|
||||
|
||||
return 0
|
||||
|
||||
@@ -34,17 +59,25 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_LIST_HELP = "List local and remote experiments."
|
||||
experiments_list_parser = experiments_subparsers.add_parser(
|
||||
"list",
|
||||
aliases=["ls"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_LIST_HELP, "exp/list"),
|
||||
help=EXPERIMENTS_LIST_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
add_rev_selection_flags(experiments_list_parser, "List")
|
||||
experiments_list_parser.add_argument(
|
||||
display_group = experiments_list_parser.add_mutually_exclusive_group()
|
||||
display_group.add_argument(
|
||||
"--name-only",
|
||||
"--names-only",
|
||||
action="store_true",
|
||||
help="Only output experiment names (without parent commits).",
|
||||
help="Only output experiment names (without SHAs or parent commits).",
|
||||
)
|
||||
display_group.add_argument(
|
||||
"--sha-only",
|
||||
"--shas-only",
|
||||
action="store_true",
|
||||
help="Only output experiment commit SHAs (without names or parent commits).",
|
||||
)
|
||||
experiments_list_parser.add_argument(
|
||||
"git_remote",
|
||||
|
||||
@@ -3,23 +3,13 @@ import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsPull(CmdBase):
|
||||
def raise_error_if_all_disabled(self):
|
||||
if not any([self.args.experiment, self.args.all_commits, self.args.rev]):
|
||||
raise InvalidArgumentError(
|
||||
"Either provide an `experiment` argument, or use the "
|
||||
"`--rev` or `--all-commits` flag."
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.raise_error_if_all_disabled()
|
||||
|
||||
pulled_exps = self.repo.experiments.pull(
|
||||
self.args.git_remote,
|
||||
self.args.experiment,
|
||||
@@ -61,7 +51,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help=EXPERIMENTS_PULL_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
add_rev_selection_flags(experiments_pull_parser, "Pull", False)
|
||||
add_rev_selection_flags(experiments_pull_parser, "Pull", True)
|
||||
experiments_pull_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
|
||||
@@ -5,20 +5,12 @@ from typing import Any, Dict
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsPush(CmdBase):
|
||||
def raise_error_if_all_disabled(self):
|
||||
if not any([self.args.experiment, self.args.all_commits, self.args.rev]):
|
||||
raise InvalidArgumentError(
|
||||
"Either provide an `experiment` argument, or use the "
|
||||
"`--rev` or `--all-commits` flag."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_result(result: Dict[str, Any], remote: str):
|
||||
from dvc.utils import humanize
|
||||
@@ -52,13 +44,13 @@ class CmdExperimentsPush(CmdBase):
|
||||
ui.write(humanize.get_summary(stats.items()))
|
||||
|
||||
if project_url := result.get("url"):
|
||||
ui.write("[yellow]View your experiments at", project_url, styled=True)
|
||||
ui.rich_print(
|
||||
"View your experiments at", project_url, style="yellow", soft_wrap=True
|
||||
)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo.experiments.push import UploadError
|
||||
|
||||
self.raise_error_if_all_disabled()
|
||||
|
||||
try:
|
||||
result = self.repo.experiments.push(
|
||||
self.args.git_remote,
|
||||
@@ -100,7 +92,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help=EXPERIMENTS_PUSH_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
add_rev_selection_flags(experiments_push_parser, "Push", False)
|
||||
add_rev_selection_flags(experiments_push_parser, "Push", True)
|
||||
experiments_push_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
|
||||
@@ -50,6 +50,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_REMOVE_HELP = "Remove experiments."
|
||||
experiments_remove_parser = experiments_subparsers.add_parser(
|
||||
"remove",
|
||||
aliases=["rm"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_REMOVE_HELP, "exp/remove"),
|
||||
help=EXPERIMENTS_REMOVE_HELP,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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 CmdExperimentsRename(CmdBase):
|
||||
def run(self):
|
||||
from dvc.utils import humanize
|
||||
|
||||
if not (self.args.experiment and self.args.name):
|
||||
raise InvalidArgumentError(
|
||||
"An experiment to rename and a new experiment name are required."
|
||||
)
|
||||
renamed = self.repo.experiments.rename(
|
||||
exp_name=self.args.experiment,
|
||||
new_name=self.args.name,
|
||||
git_remote=self.args.git_remote,
|
||||
force=self.args.force,
|
||||
)
|
||||
if renamed:
|
||||
ui.write(f"Renamed experiments: {humanize.join(map(repr, renamed))}")
|
||||
else:
|
||||
ui.write("No experiments to rename.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_RENAME_HELP = "Rename experiments."
|
||||
experiments_rename_parser = experiments_subparsers.add_parser(
|
||||
"rename",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_RENAME_HELP, "exp/rename"),
|
||||
help=EXPERIMENTS_RENAME_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
rename_group = experiments_rename_parser.add_mutually_exclusive_group()
|
||||
rename_group.add_argument(
|
||||
"-g",
|
||||
"--git-remote",
|
||||
metavar="<git_remote>",
|
||||
help="Name or URL of the Git remote to rename the experiment from",
|
||||
)
|
||||
experiments_rename_parser.add_argument(
|
||||
"experiment",
|
||||
help="Experiment to rename.",
|
||||
nargs="?",
|
||||
metavar="<experiment>",
|
||||
)
|
||||
experiments_rename_parser.add_argument(
|
||||
"name",
|
||||
help="Name of new experiment.",
|
||||
metavar="<name>",
|
||||
)
|
||||
experiments_rename_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Replace experiment if it already exists.",
|
||||
)
|
||||
experiments_rename_parser.set_defaults(func=CmdExperimentsRename)
|
||||
@@ -1,55 +1,32 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.repro import CmdRepro
|
||||
from dvc.commands.repro import add_arguments as add_repro_arguments
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsRun(CmdRepro):
|
||||
def run(self):
|
||||
from dvc.compare import show_metrics
|
||||
|
||||
if self.args.checkpoint_resume:
|
||||
if self.args.reset:
|
||||
raise InvalidArgumentError("--reset and --rev are mutually exclusive.")
|
||||
if not (self.args.queue or self.args.tmp_dir):
|
||||
raise InvalidArgumentError(
|
||||
"--rev can only be used in conjunction with --queue or --temp."
|
||||
)
|
||||
|
||||
if self.args.reset:
|
||||
ui.write("Any existing checkpoints will be reset and re-run.")
|
||||
|
||||
results = self.repo.experiments.run(
|
||||
self.repo.experiments.run(
|
||||
name=self.args.name,
|
||||
queue=self.args.queue,
|
||||
run_all=self.args.run_all,
|
||||
jobs=self.args.jobs,
|
||||
params=self.args.set_param,
|
||||
checkpoint_resume=self.args.checkpoint_resume,
|
||||
reset=self.args.reset,
|
||||
tmp_dir=self.args.tmp_dir,
|
||||
machine=self.args.machine,
|
||||
copy_paths=self.args.copy_paths,
|
||||
message=self.args.message,
|
||||
**self._common_kwargs,
|
||||
)
|
||||
|
||||
if self.args.metrics and results:
|
||||
metrics = self.repo.metrics.show(revs=list(results))
|
||||
metrics.pop("workspace", None)
|
||||
show_metrics(metrics)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_RUN_HELP = "Run or resume an experiment."
|
||||
EXPERIMENTS_RUN_HELP = "Run an experiment."
|
||||
experiments_run_parser = experiments_subparsers.add_parser(
|
||||
"run",
|
||||
parents=[parent_parser],
|
||||
@@ -58,22 +35,6 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
_add_run_common(experiments_run_parser)
|
||||
experiments_run_parser.add_argument(
|
||||
"-r",
|
||||
"--rev",
|
||||
type=str,
|
||||
dest="checkpoint_resume",
|
||||
help=(
|
||||
"Continue the specified checkpoint experiment. Can only be used "
|
||||
"in conjunction with --queue or --temp."
|
||||
),
|
||||
metavar="<experiment_rev>",
|
||||
).complete = completion.EXPERIMENT
|
||||
experiments_run_parser.add_argument(
|
||||
"--reset",
|
||||
action="store_true",
|
||||
help="Reset existing checkpoints and restart the experiment.",
|
||||
)
|
||||
experiments_run_parser.set_defaults(func=CmdExperimentsRun)
|
||||
|
||||
|
||||
@@ -128,15 +89,6 @@ def _add_run_common(parser):
|
||||
"your workspace."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--machine",
|
||||
default=None,
|
||||
help=argparse.SUPPRESS,
|
||||
# help=(
|
||||
# "Run this experiment on the specified 'dvc machine' instance."
|
||||
# )
|
||||
# metavar="<name>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-C",
|
||||
"--copy-paths",
|
||||
@@ -147,3 +99,10 @@ def _add_run_common(parser):
|
||||
" Only used if `--temp` or `--queue` is specified."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ class CmdExperimentsSave(CmdBase):
|
||||
name=self.args.name,
|
||||
force=self.args.force,
|
||||
include_untracked=self.args.include_untracked,
|
||||
message=self.args.message,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to save experiment")
|
||||
@@ -26,10 +27,6 @@ class CmdExperimentsSave(CmdBase):
|
||||
else:
|
||||
name = self.repo.experiments.get_exact_name([ref])[ref]
|
||||
ui.write(f"Experiment has been saved as: {name}")
|
||||
ui.write(
|
||||
"\nTo promote an experiment to a Git branch run:\n\n"
|
||||
"\tdvc exp branch <exp> <branch>\n"
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -52,7 +49,6 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
@@ -75,4 +71,11 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help="List of untracked paths to include in the experiment.",
|
||||
metavar="<path>",
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
save_parser.set_defaults(func=CmdExperimentsSave)
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING, Dict, Iterable
|
||||
|
||||
from funcy import lmap
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.metrics import DEFAULT_PRECISION
|
||||
@@ -27,9 +25,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
experiment_types = {
|
||||
"checkpoint_tip": "│ ╓",
|
||||
"checkpoint_commit": "│ ╟",
|
||||
"checkpoint_base": "├─╨",
|
||||
"branch_commit": "├──",
|
||||
"branch_base": "└──",
|
||||
"baseline": "",
|
||||
@@ -59,7 +54,7 @@ def baseline_styler(typ):
|
||||
return {"style": "bold"} if typ == "baseline" else {}
|
||||
|
||||
|
||||
def show_experiments( # noqa: C901, PLR0912
|
||||
def show_experiments( # noqa: C901
|
||||
td: "TabularData",
|
||||
headers: Dict[str, Iterable[str]],
|
||||
keep=None,
|
||||
@@ -67,7 +62,6 @@ def show_experiments( # noqa: C901, PLR0912
|
||||
pager=True,
|
||||
csv=False,
|
||||
markdown=False,
|
||||
pcp=False,
|
||||
**kwargs,
|
||||
):
|
||||
if keep:
|
||||
@@ -110,51 +104,23 @@ def show_experiments( # noqa: C901, PLR0912
|
||||
}
|
||||
)
|
||||
|
||||
if kwargs.get("only_changed", False) or pcp:
|
||||
if kwargs.get("only_changed", False):
|
||||
td.drop_duplicates("cols", ignore_empty=False)
|
||||
|
||||
cols_to_drop = set()
|
||||
if drop is not None:
|
||||
cols_to_drop = {col for col in td.keys() if re.match(drop, col)} # noqa: SIM118
|
||||
if pcp:
|
||||
cols_to_drop.add("Created")
|
||||
td.drop(*cols_to_drop)
|
||||
|
||||
if pcp:
|
||||
subset = {x for x in td.keys() if x != "Experiment"} # noqa: SIM118
|
||||
td.dropna(
|
||||
"rows",
|
||||
how="all",
|
||||
subset=subset,
|
||||
)
|
||||
td.drop_duplicates("rows", subset=subset)
|
||||
if "Experiment" in td:
|
||||
td.column("Experiment")[:] = [
|
||||
# remove tree characters
|
||||
str(x).encode("ascii", "ignore").strip().decode()
|
||||
for x in td.column("Experiment")
|
||||
]
|
||||
out = kwargs.get("out") or "dvc_plots"
|
||||
output_file = os.path.join(out, "index.html")
|
||||
ui.write(
|
||||
td.to_parallel_coordinates(
|
||||
output_path=os.path.abspath(output_file),
|
||||
color_by=kwargs.get("sort_by") or "Experiment",
|
||||
)
|
||||
)
|
||||
if kwargs.get("open"):
|
||||
return ui.open_browser(output_file)
|
||||
|
||||
else:
|
||||
td.render(
|
||||
pager=pager,
|
||||
borders="horizontals",
|
||||
rich_table=True,
|
||||
header_styles=styles,
|
||||
row_styles=row_styles,
|
||||
csv=csv,
|
||||
markdown=markdown,
|
||||
)
|
||||
td.render(
|
||||
pager=pager,
|
||||
borders="horizontals",
|
||||
rich_table=True,
|
||||
header_styles=styles,
|
||||
row_styles=row_styles,
|
||||
csv=csv,
|
||||
markdown=markdown,
|
||||
)
|
||||
|
||||
|
||||
def _normalize_headers(names, count):
|
||||
@@ -221,9 +187,6 @@ class CmdExperimentsShow(CmdBase):
|
||||
csv=self.args.csv,
|
||||
markdown=self.args.markdown,
|
||||
only_changed=self.args.only_changed,
|
||||
pcp=self.args.pcp,
|
||||
out=self.args.out,
|
||||
open=self.args.open,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -316,21 +279,18 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print output in JSON format instead of a human-readable table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--csv",
|
||||
"--show-csv",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print output in csv format instead of a human-readable table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
@@ -345,27 +305,6 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
),
|
||||
metavar="<n>",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--parallel-coordinates-plot",
|
||||
"--pcp",
|
||||
dest="pcp",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Generate a Parallel Coordinates Plot from the tabulated output.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
default=None,
|
||||
help="Destination folder to save the Parallel Coordinates Plot to",
|
||||
metavar="<path>",
|
||||
).complete = completion.DIR
|
||||
experiments_show_parser.add_argument(
|
||||
"--open",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Open the Parallel Coordinates Plot directly in the browser.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--no-fetch",
|
||||
dest="fetch_running",
|
||||
|
||||
+15
-2
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdGC(CmdBase):
|
||||
def run(self): # noqa: C901
|
||||
def run(self): # noqa: C901, PLR0912
|
||||
from dvc.repo.gc import _validate_args
|
||||
|
||||
_validate_args(
|
||||
@@ -22,6 +22,8 @@ class CmdGC(CmdBase):
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
cloud=self.args.cloud,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
)
|
||||
|
||||
if self.args.rev:
|
||||
@@ -46,6 +48,10 @@ class CmdGC(CmdBase):
|
||||
|
||||
if self.args.all_experiments:
|
||||
msg += " and all experiments"
|
||||
|
||||
if self.args.not_in_remote:
|
||||
msg += " that are present in the DVC remote"
|
||||
|
||||
if self.args.repos:
|
||||
msg += " of the current and the following repos:"
|
||||
|
||||
@@ -74,6 +80,7 @@ class CmdGC(CmdBase):
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -157,12 +164,18 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Keep data files for all experiments.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep data files that are not present in the remote.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-c",
|
||||
"--cloud",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Collect garbage in remote repository.",
|
||||
help="Collect garbage in remote storage in addition to local cache.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-r",
|
||||
|
||||
+35
-1
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -36,6 +36,10 @@ class CmdGet(CmdBaseNoRepo):
|
||||
out=self.args.out,
|
||||
rev=self.args.rev,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
return 0
|
||||
except CloneError:
|
||||
@@ -94,4 +98,34 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
help=(
|
||||
"Path to a config file that will be merged with the config "
|
||||
"in the target repository."
|
||||
),
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help=(
|
||||
"Remote config options to merge with a remote's config (default or one "
|
||||
"specified by '--remote') in the target repository."
|
||||
),
|
||||
)
|
||||
get_parser.set_defaults(func=CmdGet)
|
||||
|
||||
+22
-2
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -14,7 +14,13 @@ class CmdGetUrl(CmdBaseNoRepo):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
Repo.get_url(self.args.url, out=self.args.out, jobs=self.args.jobs)
|
||||
Repo.get_url(
|
||||
self.args.url,
|
||||
out=self.args.out,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
fs_config=self.args.fs_config,
|
||||
)
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception("failed to get '%s'", self.args.url)
|
||||
@@ -46,4 +52,18 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"--fs-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help="Config options for the target url.",
|
||||
)
|
||||
get_parser.set_defaults(func=CmdGetUrl)
|
||||
|
||||
+35
-15
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -18,15 +18,14 @@ class CmdImport(CmdBase):
|
||||
self.args.url,
|
||||
self.args.path,
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
rev=self.args.rev,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
force=self.args.force,
|
||||
)
|
||||
except CloneError:
|
||||
logger.exception("failed to import '%s'", self.args.path)
|
||||
@@ -42,8 +41,6 @@ class CmdImport(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = (
|
||||
"Download file or directory tracked by DVC or by Git "
|
||||
"into the workspace, and track it."
|
||||
@@ -69,17 +66,19 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Destination path to download files to",
|
||||
metavar="<path>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override destination file or folder if exists.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--rev",
|
||||
nargs="?",
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--file",
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
@@ -106,6 +105,27 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
help=(
|
||||
"Path to a config file that will be merged with the config "
|
||||
"in the target repository."
|
||||
),
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help=(
|
||||
"Remote config options to merge with a remote's config (default or one "
|
||||
"specified by '--remote') in the target repository."
|
||||
),
|
||||
)
|
||||
import_parser.set_defaults(func=CmdImport)
|
||||
|
||||
+17
-14
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -15,17 +15,14 @@ class CmdImportUrl(CmdBase):
|
||||
self.repo.imp_url(
|
||||
self.args.url,
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
no_exec=self.args.no_exec,
|
||||
no_download=self.args.no_download,
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
desc=self.args.desc,
|
||||
type=self.args.type,
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
version_aware=self.args.version_aware,
|
||||
fs_config=self.args.fs_config,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception(
|
||||
@@ -40,8 +37,6 @@ class CmdImportUrl(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from .add import _add_annotating_args
|
||||
|
||||
IMPORT_HELP = "Download or copy file from URL and take it under DVC control."
|
||||
|
||||
import_parser = subparsers.add_parser(
|
||||
@@ -69,11 +64,6 @@ def add_parser(subparsers, parent_parser):
|
||||
import_parser.add_argument(
|
||||
"out", nargs="?", help="Destination path to put files to."
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"--file",
|
||||
help="Specify name of the .dvc file this command will generate.",
|
||||
metavar="<filename>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"--to-remote",
|
||||
action="store_true",
|
||||
@@ -112,11 +102,24 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--version-aware",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Import using cloud versioning. Implied if the URL contains a version ID.",
|
||||
)
|
||||
_add_annotating_args(import_parser)
|
||||
import_parser.add_argument(
|
||||
"--fs-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help="Config options for the target url.",
|
||||
)
|
||||
import_parser.set_defaults(func=CmdImportUrl)
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.commands.ls.ls_colors import LsColors
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
@@ -11,7 +11,18 @@ from dvc.ui import ui
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _prettify(entries, with_color=False):
|
||||
def _format_entry(entry, fmt):
|
||||
from dvc.utils.humanize import naturalsize
|
||||
|
||||
size = entry.get("size")
|
||||
if size is None:
|
||||
size = ""
|
||||
else:
|
||||
size = naturalsize(size)
|
||||
return size, fmt(entry)
|
||||
|
||||
|
||||
def show_entries(entries, with_color=False, with_size=False):
|
||||
if with_color:
|
||||
ls_colors = LsColors()
|
||||
fmt = ls_colors.format
|
||||
@@ -20,7 +31,12 @@ def _prettify(entries, with_color=False):
|
||||
def fmt(entry):
|
||||
return entry["path"]
|
||||
|
||||
return [fmt(entry) for entry in entries]
|
||||
if with_size:
|
||||
ui.table([_format_entry(entry, fmt) for entry in entries])
|
||||
return
|
||||
|
||||
# NOTE: this is faster than ui.table for very large number of entries
|
||||
ui.write("\n".join(fmt(entry) for entry in entries))
|
||||
|
||||
|
||||
class CmdList(CmdBaseNoRepo):
|
||||
@@ -34,12 +50,14 @@ class CmdList(CmdBaseNoRepo):
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
dvc_only=self.args.dvc_only,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
if self.args.json:
|
||||
ui.write_json(entries)
|
||||
elif entries:
|
||||
entries = _prettify(entries, with_color=True)
|
||||
ui.write("\n".join(entries))
|
||||
show_entries(entries, with_color=True, with_size=self.args.size)
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception("failed to list '%s'", self.args.url)
|
||||
@@ -71,7 +89,6 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
@@ -81,6 +98,34 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
help=(
|
||||
"Path to a config file that will be merged with the config "
|
||||
"in the target repository."
|
||||
),
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help=(
|
||||
"Remote config options to merge with a remote's config (default or one "
|
||||
"specified by '--remote') in the target repository."
|
||||
),
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--size",
|
||||
action="store_true",
|
||||
help="Show sizes.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
|
||||
+20
-6
@@ -2,10 +2,9 @@ import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
|
||||
from .ls import _prettify
|
||||
from .ls import show_entries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,10 +13,13 @@ class CmdListUrl(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
from dvc.repo import Repo
|
||||
|
||||
entries = Repo.ls_url(self.args.url, recursive=self.args.recursive)
|
||||
entries = Repo.ls_url(
|
||||
self.args.url,
|
||||
recursive=self.args.recursive,
|
||||
fs_config=self.args.fs_config,
|
||||
)
|
||||
if entries:
|
||||
entries = _prettify(entries, with_color=True)
|
||||
ui.write("\n".join(entries))
|
||||
show_entries(entries, with_color=True, with_size=self.args.size)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -40,4 +42,16 @@ def add_parser(subparsers, parent_parser):
|
||||
action="store_true",
|
||||
help="Recursively list files.",
|
||||
)
|
||||
lsurl_parser.add_argument(
|
||||
"--size",
|
||||
action="store_true",
|
||||
help="Show sizes.",
|
||||
)
|
||||
lsurl_parser.add_argument(
|
||||
"--fs-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help="Config options for the target url.",
|
||||
)
|
||||
lsurl_parser.set_defaults(func=CmdListUrl)
|
||||
|
||||
@@ -1,490 +0,0 @@
|
||||
import argparse
|
||||
from typing import Dict, List
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.commands.config import CmdConfig
|
||||
from dvc.compare import TabularData
|
||||
from dvc.config import ConfigError
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
|
||||
class MachineDisabledError(ConfigError):
|
||||
def __init__(self):
|
||||
super().__init__("Machine feature is disabled")
|
||||
|
||||
|
||||
class CmdMachineConfig(CmdConfig):
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
if not self.config["feature"].get("machine", False):
|
||||
raise MachineDisabledError
|
||||
|
||||
if getattr(self.args, "name", None):
|
||||
self.args.name = self.args.name.lower()
|
||||
|
||||
def _check_exists(self, conf):
|
||||
if self.args.name not in conf["machine"]:
|
||||
raise ConfigError(f"machine '{self.args.name}' doesn't exist.")
|
||||
|
||||
|
||||
class CmdMachineAdd(CmdMachineConfig):
|
||||
def run(self):
|
||||
from dvc.machine import validate_name
|
||||
|
||||
validate_name(self.args.name)
|
||||
|
||||
if self.args.default:
|
||||
ui.write(f"Setting '{self.args.name}' as a default machine.")
|
||||
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
if self.args.name in conf["machine"] and not self.args.force:
|
||||
raise ConfigError(
|
||||
"machine '{}' already exists. Use `-f|--force` to "
|
||||
"overwrite it.".format(self.args.name)
|
||||
)
|
||||
|
||||
conf["machine"][self.args.name] = {"cloud": self.args.cloud}
|
||||
if self.args.default:
|
||||
conf["core"]["machine"] = self.args.name
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineRemove(CmdMachineConfig):
|
||||
def run(self):
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
self._check_exists(conf)
|
||||
del conf["machine"][self.args.name]
|
||||
|
||||
up_to_level = self.args.level or "repo"
|
||||
# Remove core.machine refs to this machine in any shadowing configs
|
||||
for level in reversed(self.config.LEVELS):
|
||||
with self.config.edit(level) as conf:
|
||||
if conf["core"].get("machine") == self.args.name:
|
||||
del conf["core"]["machine"]
|
||||
|
||||
if level == up_to_level:
|
||||
break
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineList(CmdMachineConfig):
|
||||
TABLE_COLUMNS = [
|
||||
"name",
|
||||
"cloud",
|
||||
"region",
|
||||
"image",
|
||||
"spot",
|
||||
"spot_price",
|
||||
"instance_hdd_size",
|
||||
"instance_type",
|
||||
"ssh_private",
|
||||
"startup_script",
|
||||
]
|
||||
|
||||
PRIVATE_COLUMNS = ["ssh_private", "startup_script"]
|
||||
|
||||
def _hide_private(self, conf):
|
||||
for machine in conf:
|
||||
for column in self.PRIVATE_COLUMNS:
|
||||
if column in conf[machine]:
|
||||
conf[machine][column] = "***"
|
||||
|
||||
def _show_origin(self):
|
||||
levels = [self.args.level] if self.args.level else self.config.LEVELS
|
||||
for level in levels:
|
||||
conf = self.config.read(level)["machine"]
|
||||
if self.args.name:
|
||||
conf = conf.get(self.args.name, {})
|
||||
self._hide_private(conf)
|
||||
prefix = self._config_file_prefix(True, self.config, level)
|
||||
configs = list(self._format_config(conf, prefix))
|
||||
if configs:
|
||||
ui.write("\n".join(configs))
|
||||
|
||||
def _show_table(self):
|
||||
td = TabularData(self.TABLE_COLUMNS, fill_value="-")
|
||||
conf = self.config.read()["machine"]
|
||||
if self.args.name:
|
||||
conf = {self.args.name: conf.get(self.args.name, {})}
|
||||
self._hide_private(conf)
|
||||
for machine, machine_config in conf.items():
|
||||
machine_config["name"] = machine
|
||||
td.row_from_dict(machine_config)
|
||||
td.dropna("cols", "all")
|
||||
td.render()
|
||||
|
||||
def run(self):
|
||||
if self.args.show_origin:
|
||||
self._show_origin()
|
||||
else:
|
||||
self._show_table()
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineModify(CmdMachineConfig):
|
||||
def run(self):
|
||||
from dvc.config import merge
|
||||
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
merged = self.config.load_config_to_level(self.args.level)
|
||||
merge(merged, conf)
|
||||
self._check_exists(merged)
|
||||
|
||||
if self.args.name not in conf["machine"]:
|
||||
conf["machine"][self.args.name] = {}
|
||||
section = conf["machine"][self.args.name]
|
||||
if self.args.unset:
|
||||
section.pop(self.args.option, None)
|
||||
else:
|
||||
section[self.args.option] = self.args.value
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineRename(CmdBase):
|
||||
def _check_exists(self, conf):
|
||||
if self.args.name not in conf["machine"]:
|
||||
raise ConfigError(f"machine '{self.args.name}' doesn't exist.")
|
||||
|
||||
def _rename_default(self, conf):
|
||||
if conf["core"].get("machine") == self.args.name:
|
||||
conf["core"]["machine"] = self.args.new
|
||||
|
||||
def _check_before_rename(self):
|
||||
from dvc.machine import validate_name
|
||||
|
||||
validate_name(self.args.new)
|
||||
|
||||
all_config = self.config.load_config_to_level(None)
|
||||
if self.args.new in all_config.get("machine", {}):
|
||||
raise ConfigError(
|
||||
f"Rename failed. Machine '{self.args.new}' already exists."
|
||||
)
|
||||
ui.write(f"Rename machine '{self.args.name}' to '{self.args.new}'.")
|
||||
|
||||
def run(self):
|
||||
self._check_before_rename()
|
||||
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
self._check_exists(conf)
|
||||
conf["machine"][self.args.new] = conf["machine"][self.args.name]
|
||||
try:
|
||||
assert self.repo.machine
|
||||
self.repo.machine.rename(self.args.name, self.args.new)
|
||||
except DvcException as error:
|
||||
del conf["machine"][self.args.new]
|
||||
raise ConfigError("terraform rename failed") from error
|
||||
del conf["machine"][self.args.name]
|
||||
self._rename_default(conf)
|
||||
|
||||
up_to_level = self.args.level or "repo"
|
||||
for level in reversed(self.config.LEVELS):
|
||||
if level == up_to_level:
|
||||
break
|
||||
with self.config.edit(level) as level_conf:
|
||||
self._rename_default(level_conf)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineDefault(CmdMachineConfig):
|
||||
def run(self):
|
||||
if self.args.name is None and not self.args.unset:
|
||||
conf = self.config.read(self.args.level)
|
||||
try:
|
||||
ui.write(conf["core"]["machine"])
|
||||
except KeyError:
|
||||
ui.write("No default machine set")
|
||||
return 1
|
||||
else:
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
if self.args.unset:
|
||||
conf["core"].pop("machine", None)
|
||||
else:
|
||||
merged_conf = self.config.load_config_to_level(self.args.level)
|
||||
if (
|
||||
self.args.name in conf["machine"]
|
||||
or self.args.name in merged_conf["machine"]
|
||||
):
|
||||
conf["core"]["machine"] = self.args.name
|
||||
else:
|
||||
raise ConfigError(
|
||||
"default machine must be present in machine list."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineCreate(CmdBase):
|
||||
def run(self):
|
||||
if self.repo.machine is None:
|
||||
raise MachineDisabledError
|
||||
|
||||
self.repo.machine.create(self.args.name)
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineStatus(CmdBase):
|
||||
INSTANCE_FIELD = ["name", "instance", "status"]
|
||||
SHOWN_FIELD = [
|
||||
"cloud",
|
||||
"instance_ip",
|
||||
"instance_type",
|
||||
"instance_hdd_size",
|
||||
"instance_gpu",
|
||||
]
|
||||
FILL_VALUE = "-"
|
||||
|
||||
def _add_row(
|
||||
self,
|
||||
name: str,
|
||||
all_status: List[Dict],
|
||||
td: TabularData,
|
||||
):
|
||||
if not all_status:
|
||||
row = [
|
||||
name,
|
||||
self.FILL_VALUE,
|
||||
"offline",
|
||||
] # back to `None` after #7167
|
||||
td.append(row)
|
||||
for i, status in enumerate(all_status, start=1):
|
||||
row = [name, f"num_{i}", "running" if status else "offline"]
|
||||
for field in self.SHOWN_FIELD:
|
||||
value = str(status.get(field, ""))
|
||||
row.append(value)
|
||||
td.append(row)
|
||||
|
||||
def run(self):
|
||||
if self.repo.machine is None:
|
||||
raise MachineDisabledError
|
||||
|
||||
td = TabularData(
|
||||
self.INSTANCE_FIELD + self.SHOWN_FIELD, fill_value=self.FILL_VALUE
|
||||
)
|
||||
|
||||
if self.args.name:
|
||||
all_status = list(self.repo.machine.status(self.args.name))
|
||||
self._add_row(self.args.name, all_status, td)
|
||||
else:
|
||||
name_set = set()
|
||||
for level in self.repo.config.LEVELS:
|
||||
conf = self.repo.config.read(level)["machine"]
|
||||
name_set.update(conf.keys())
|
||||
name_list = list(name_set)
|
||||
for name in sorted(name_list):
|
||||
all_status = list(self.repo.machine.status(name))
|
||||
self._add_row(name, all_status, td)
|
||||
|
||||
td.dropna("cols", "all")
|
||||
td.render()
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineDestroy(CmdBase):
|
||||
def run(self):
|
||||
if self.repo.machine is None:
|
||||
raise MachineDisabledError
|
||||
|
||||
self.repo.machine.destroy(self.args.name)
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineSsh(CmdBase):
|
||||
def run(self):
|
||||
if self.repo.machine is None:
|
||||
raise MachineDisabledError
|
||||
|
||||
self.repo.machine.run_shell(self.args.name)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser): # noqa: PLR0915
|
||||
from dvc.commands.config import parent_config_parser
|
||||
|
||||
machine_HELP = "Set up and manage cloud machines."
|
||||
machine_parser = subparsers.add_parser(
|
||||
"machine",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(machine_HELP, "machine"),
|
||||
# NOTE: suppress help during development to hide command
|
||||
# help=machine_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
machine_subparsers = machine_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use `dvc machine CMD --help` for command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(machine_subparsers)
|
||||
|
||||
machine_ADD_HELP = "Add a new data machine."
|
||||
machine_add_parser = machine_subparsers.add_parser(
|
||||
"add",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
description=append_doc_link(machine_ADD_HELP, "machine/add"),
|
||||
help=machine_ADD_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_add_parser.add_argument("name", help="Name of the machine")
|
||||
machine_add_parser.add_argument(
|
||||
"cloud",
|
||||
help="Machine cloud. See full list of supported clouds at {}".format(
|
||||
format_link(
|
||||
"https://github.com/iterative/terraform-provider-iterative#machine"
|
||||
)
|
||||
),
|
||||
)
|
||||
machine_add_parser.add_argument(
|
||||
"-d",
|
||||
"--default",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Set as default machine.",
|
||||
)
|
||||
machine_add_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Force overwriting existing configs",
|
||||
)
|
||||
machine_add_parser.set_defaults(func=CmdMachineAdd)
|
||||
|
||||
machine_DEFAULT_HELP = "Set/unset the default machine."
|
||||
machine_default_parser = machine_subparsers.add_parser(
|
||||
"default",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
description=append_doc_link(machine_DEFAULT_HELP, "machine/default"),
|
||||
help=machine_DEFAULT_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_default_parser.add_argument("name", nargs="?", help="Name of the machine")
|
||||
machine_default_parser.add_argument(
|
||||
"-u",
|
||||
"--unset",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Unset default machine.",
|
||||
)
|
||||
machine_default_parser.set_defaults(func=CmdMachineDefault)
|
||||
|
||||
machine_LIST_HELP = "List the configuration of one/all machines."
|
||||
machine_list_parser = machine_subparsers.add_parser(
|
||||
"list",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
description=append_doc_link(machine_LIST_HELP, "machine/list"),
|
||||
help=machine_LIST_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_list_parser.add_argument(
|
||||
"--show-origin",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Show the source file containing each config value.",
|
||||
)
|
||||
machine_list_parser.add_argument(
|
||||
"name",
|
||||
nargs="?",
|
||||
type=str,
|
||||
help="name of machine to specify",
|
||||
)
|
||||
machine_list_parser.set_defaults(func=CmdMachineList)
|
||||
machine_MODIFY_HELP = "Modify the configuration of an machine."
|
||||
machine_modify_parser = machine_subparsers.add_parser(
|
||||
"modify",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
description=append_doc_link(machine_MODIFY_HELP, "machine/modify"),
|
||||
help=machine_MODIFY_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_modify_parser.add_argument("name", help="Name of the machine")
|
||||
machine_modify_parser.add_argument("option", help="Name of the option to modify.")
|
||||
machine_modify_parser.add_argument(
|
||||
"value", nargs="?", help="(optional) Value of the option."
|
||||
)
|
||||
machine_modify_parser.add_argument(
|
||||
"-u",
|
||||
"--unset",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Unset option.",
|
||||
)
|
||||
machine_modify_parser.set_defaults(func=CmdMachineModify)
|
||||
|
||||
machine_RENAME_HELP = "Rename a machine "
|
||||
machine_rename_parser = machine_subparsers.add_parser(
|
||||
"rename",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
description=append_doc_link(machine_RENAME_HELP, "remote/rename"),
|
||||
help=machine_RENAME_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_rename_parser.add_argument("name", help="Machine to be renamed")
|
||||
machine_rename_parser.add_argument("new", help="New name of the machine")
|
||||
machine_rename_parser.set_defaults(func=CmdMachineRename)
|
||||
|
||||
machine_REMOVE_HELP = "Remove an machine."
|
||||
machine_remove_parser = machine_subparsers.add_parser(
|
||||
"remove",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
description=append_doc_link(machine_REMOVE_HELP, "machine/remove"),
|
||||
help=machine_REMOVE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_remove_parser.add_argument("name", help="Name of the machine to remove.")
|
||||
machine_remove_parser.set_defaults(func=CmdMachineRemove)
|
||||
|
||||
machine_CREATE_HELP = "Create and start a machine instance."
|
||||
machine_create_parser = machine_subparsers.add_parser(
|
||||
"create",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(machine_CREATE_HELP, "machine/create"),
|
||||
help=machine_CREATE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_create_parser.add_argument("name", help="Name of the machine to create.")
|
||||
machine_create_parser.set_defaults(func=CmdMachineCreate)
|
||||
|
||||
machine_STATUS_HELP = "List the status of running instances for one/all machines."
|
||||
machine_status_parser = machine_subparsers.add_parser(
|
||||
"status",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(machine_STATUS_HELP, "machine/status"),
|
||||
help=machine_STATUS_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_status_parser.add_argument(
|
||||
"name", nargs="?", help="(optional) Name of the machine."
|
||||
)
|
||||
machine_status_parser.set_defaults(func=CmdMachineStatus)
|
||||
|
||||
machine_DESTROY_HELP = "Destroy an machine instance."
|
||||
machine_destroy_parser = machine_subparsers.add_parser(
|
||||
"destroy",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(machine_DESTROY_HELP, "machine/destroy"),
|
||||
help=machine_DESTROY_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_destroy_parser.add_argument(
|
||||
"name", help="Name of the machine instance to destroy."
|
||||
)
|
||||
machine_destroy_parser.set_defaults(func=CmdMachineDestroy)
|
||||
|
||||
machine_SSH_HELP = "Connect to a machine via SSH."
|
||||
machine_ssh_parser = machine_subparsers.add_parser(
|
||||
"ssh",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(machine_SSH_HELP, "machine/ssh"),
|
||||
help=machine_SSH_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_ssh_parser.add_argument(
|
||||
"name", help="Name of the machine instance to connect to."
|
||||
)
|
||||
machine_ssh_parser.set_defaults(func=CmdMachineSsh)
|
||||
+42
-26
@@ -4,7 +4,6 @@ import logging
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.serialize import encode_exception
|
||||
|
||||
@@ -20,17 +19,25 @@ class CmdMetricsBase(CmdBase):
|
||||
|
||||
class CmdMetricsShow(CmdMetricsBase):
|
||||
def run(self):
|
||||
try:
|
||||
metrics = self.repo.metrics.show(
|
||||
self.args.targets,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
recursive=self.args.recursive,
|
||||
from dvc.repo.metrics.show import to_relpath
|
||||
from dvc.utils import errored_revisions
|
||||
|
||||
metrics = self.repo.metrics.show(
|
||||
self.args.targets,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
)
|
||||
metrics = {
|
||||
k: to_relpath(self.repo.fs, self.repo.root_dir, v)
|
||||
for k, v in metrics.items()
|
||||
}
|
||||
|
||||
if errored := errored_revisions(metrics):
|
||||
ui.error_write(
|
||||
"DVC failed to load some metrics for following revisions:"
|
||||
f" '{', '.join(errored)}'."
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(metrics, default=encode_exception)
|
||||
@@ -52,17 +59,26 @@ class CmdMetricsShow(CmdMetricsBase):
|
||||
|
||||
class CmdMetricsDiff(CmdMetricsBase):
|
||||
def run(self):
|
||||
try:
|
||||
diff = self.repo.metrics.diff(
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
targets=self.args.targets,
|
||||
recursive=self.args.recursive,
|
||||
all=self.args.all,
|
||||
import os
|
||||
from os.path import relpath
|
||||
|
||||
diff_result = self.repo.metrics.diff(
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
targets=self.args.targets,
|
||||
all=self.args.all,
|
||||
)
|
||||
|
||||
errored = [rev for rev, err in diff_result.get("errors", {}).items() if err]
|
||||
if errored:
|
||||
ui.error_write(
|
||||
"DVC failed to load some metrics for following revisions:"
|
||||
f" '{', '.join(errored)}'."
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to show metrics diff")
|
||||
return 1
|
||||
|
||||
start = relpath(os.getcwd(), self.repo.root_dir)
|
||||
diff = diff_result.get("diff", {})
|
||||
diff = {relpath(path, start): result for path, result in diff.items()}
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
@@ -140,14 +156,12 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
@@ -186,10 +200,14 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"a_rev", nargs="?", help="Old Git commit to compare (defaults to HEAD)"
|
||||
"a_rev",
|
||||
nargs="?",
|
||||
help="Old Git commit to compare (defaults to HEAD)",
|
||||
default="HEAD",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"b_rev",
|
||||
default="workspace",
|
||||
nargs="?",
|
||||
help="New Git commit to compare (defaults to the current workspace)",
|
||||
)
|
||||
@@ -223,14 +241,12 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
@@ -30,6 +30,7 @@ def add_parser(subparsers, parent_parser):
|
||||
|
||||
move_parser = subparsers.add_parser(
|
||||
"move",
|
||||
aliases=["mv"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(MOVE_DESCRIPTION, "move"),
|
||||
help=MOVE_HELP,
|
||||
|
||||
+25
-14
@@ -4,7 +4,6 @@ import logging
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -14,17 +13,27 @@ class CmdParamsDiff(CmdBase):
|
||||
UNINITIALIZED = True
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
diff = self.repo.params.diff(
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
targets=self.args.targets,
|
||||
all=self.args.all,
|
||||
deps=self.args.deps,
|
||||
import os
|
||||
from os.path import relpath
|
||||
|
||||
diff_result = self.repo.params.diff(
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
targets=self.args.targets,
|
||||
all=self.args.all,
|
||||
deps_only=self.args.deps,
|
||||
)
|
||||
|
||||
errored = [rev for rev, err in diff_result.get("errors", {}).items() if err]
|
||||
if errored:
|
||||
ui.error_write(
|
||||
"DVC failed to load some metrics for following revisions:"
|
||||
f" '{', '.join(errored)}'."
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to show params diff")
|
||||
return 1
|
||||
|
||||
start = relpath(os.getcwd(), self.repo.root_dir)
|
||||
diff = diff_result.get("diff", {})
|
||||
diff = {relpath(path, start): result for path, result in diff.items()}
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
@@ -74,10 +83,14 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"a_rev", nargs="?", help="Old Git commit to compare (defaults to HEAD)"
|
||||
"a_rev",
|
||||
nargs="?",
|
||||
default="HEAD",
|
||||
help="Old Git commit to compare (defaults to HEAD)",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"b_rev",
|
||||
default="workspace",
|
||||
nargs="?",
|
||||
help="New Git commit to compare (defaults to the current workspace)",
|
||||
)
|
||||
@@ -105,14 +118,12 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
|
||||
@@ -430,7 +430,6 @@ def _add_ui_arguments(parser):
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
|
||||
@@ -30,8 +30,8 @@ class CmdRemoteAdd(CmdRemote):
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
if self.args.name in conf["remote"] and not self.args.force:
|
||||
raise ConfigError(
|
||||
"remote '{}' already exists. Use `-f|--force` to "
|
||||
"overwrite it.".format(self.args.name)
|
||||
f"remote '{self.args.name}' already exists. Use `-f|--force` to "
|
||||
"overwrite it."
|
||||
)
|
||||
|
||||
conf["remote"][self.args.name] = {"url": self.args.url}
|
||||
@@ -127,9 +127,7 @@ class CmdRemoteRename(CmdRemote):
|
||||
all_config = self.config.load_config_to_level(None)
|
||||
if self.args.new in all_config.get("remote", {}):
|
||||
raise ConfigError(
|
||||
"Rename failed. Remote name '{}' already exists.".format(
|
||||
{self.args.new}
|
||||
)
|
||||
f"Rename failed. Remote name {self.args.new!r} already exists."
|
||||
)
|
||||
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
|
||||
@@ -26,6 +26,7 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
remove_parser = subparsers.add_parser(
|
||||
"remove",
|
||||
aliases=["rm"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(REMOVE_HELP, "remove"),
|
||||
help=REMOVE_HELP,
|
||||
|
||||
+28
-13
@@ -16,12 +16,6 @@ class CmdRepro(CmdBase):
|
||||
else:
|
||||
ui.write("Use `dvc push` to send your updates to remote storage.")
|
||||
|
||||
if self.args.metrics:
|
||||
from dvc.compare import show_metrics
|
||||
|
||||
metrics = self.repo.metrics.show()
|
||||
show_metrics(metrics)
|
||||
|
||||
return 0
|
||||
|
||||
@property
|
||||
@@ -38,6 +32,8 @@ class CmdRepro(CmdBase):
|
||||
"recursive": self.args.recursive,
|
||||
"force_downstream": self.args.force_downstream,
|
||||
"pull": self.args.pull,
|
||||
"allow_missing": self.args.allow_missing,
|
||||
"on_error": self.args.on_error,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -104,13 +100,6 @@ and then the stage name name.
|
||||
default=False,
|
||||
help="Reproduce all stages in the specified directory.",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"-m",
|
||||
"--metrics",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show metrics after reproduction.",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--downstream",
|
||||
action="store_true",
|
||||
@@ -135,6 +124,12 @@ and then the stage name name.
|
||||
"from the run-cache."
|
||||
),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--allow-missing",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=("Skip stages with missing data but no other changes."),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--dry",
|
||||
action="store_true",
|
||||
@@ -143,6 +138,26 @@ and then the stage name name.
|
||||
"Only print the commands that would be executed without actually executing."
|
||||
),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"-k",
|
||||
"--keep-going",
|
||||
action="store_const",
|
||||
default="fail",
|
||||
const="keep-going",
|
||||
dest="on_error",
|
||||
help=(
|
||||
"Continue executing, skipping stages having dependencies "
|
||||
"on the failed stages"
|
||||
),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--ignore-errors",
|
||||
action="store_const",
|
||||
default="fail",
|
||||
const="ignore",
|
||||
dest="on_error",
|
||||
help="Ignore errors from stages.",
|
||||
)
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.stage import parse_cmd
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdRun(CmdBase):
|
||||
def run(self):
|
||||
if not any(
|
||||
[
|
||||
self.args.deps,
|
||||
self.args.outs,
|
||||
self.args.outs_no_cache,
|
||||
self.args.metrics,
|
||||
self.args.metrics_no_cache,
|
||||
self.args.plots,
|
||||
self.args.plots_no_cache,
|
||||
self.args.outs_persist,
|
||||
self.args.outs_persist_no_cache,
|
||||
self.args.checkpoints,
|
||||
self.args.params,
|
||||
self.args.command,
|
||||
]
|
||||
): # pragma: no cover
|
||||
logger.error(
|
||||
"too few arguments. Specify at least one: `-d`, `-o`, `-O`, "
|
||||
"`-m`, `-M`, `-p`, `--plots`, `--plots-no-cache`, "
|
||||
"`--outs-persist`, `--outs-persist-no-cache`, `command`."
|
||||
)
|
||||
return 1
|
||||
|
||||
kwargs = vars(self.args)
|
||||
kwargs.update(
|
||||
{
|
||||
"cmd": parse_cmd(self.args.command),
|
||||
"fname": kwargs.pop("file"),
|
||||
"no_exec": self.args.no_exec or bool(self.args.checkpoints),
|
||||
"run_cache": not kwargs.pop("no_run_cache"),
|
||||
}
|
||||
)
|
||||
try:
|
||||
self.repo.run(**kwargs)
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from dvc.commands.stage import _add_common_args
|
||||
|
||||
RUN_HELP = "Generate a dvc.yaml file from a command and execute the command."
|
||||
run_parser = subparsers.add_parser(
|
||||
"run",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(RUN_HELP, "run"),
|
||||
help=RUN_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
run_parser.add_argument("-n", "--name", help="Stage name.")
|
||||
run_parser.add_argument("--file", metavar="<filename>", help=argparse.SUPPRESS)
|
||||
run_parser.add_argument(
|
||||
"--single-stage",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
_add_common_args(run_parser)
|
||||
run_parser.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create dvc.yaml without actually running it.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--no-commit",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Don't put files/directories into cache.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--no-run-cache",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Execute the command even if this stage has already been run "
|
||||
"with the same command/dependencies/outputs/etc before."
|
||||
),
|
||||
)
|
||||
run_parser.set_defaults(func=CmdRun)
|
||||
+30
-18
@@ -1,5 +1,6 @@
|
||||
import argparse
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from itertools import chain, filterfalse
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List
|
||||
|
||||
@@ -119,8 +120,22 @@ def parse_cmd(commands: List[str]) -> str:
|
||||
return " ".join(map(quote_argument, commands))
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _disable_logging(highest_level=logging.CRITICAL):
|
||||
previous_level = logging.root.manager.disable
|
||||
|
||||
logging.disable(highest_level)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logging.disable(previous_level)
|
||||
|
||||
|
||||
class CmdStageAdd(CmdBase):
|
||||
def run(self):
|
||||
from dvc.repo import lock_repo
|
||||
|
||||
kwargs = vars(self.args)
|
||||
kwargs.update(
|
||||
{
|
||||
@@ -128,7 +143,15 @@ class CmdStageAdd(CmdBase):
|
||||
"params": parse_params(self.args.params),
|
||||
}
|
||||
)
|
||||
self.repo.stage.add(**kwargs)
|
||||
|
||||
with self.repo.scm_context, lock_repo(self.repo):
|
||||
with _disable_logging(logging.INFO):
|
||||
stage = self.repo.stage.add(**kwargs)
|
||||
logger.info("Added stage %r in %r", stage.addressing, stage.relpath)
|
||||
if self.args.run:
|
||||
stage.run()
|
||||
stage.dump(update_pipeline=False)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -172,23 +195,6 @@ def _add_common_args(parser):
|
||||
help="Declare output file or directory (do not put into DVC cache).",
|
||||
metavar="<filename>",
|
||||
).complete = completion.FILE
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--checkpoints",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"Declare checkpoint output file or directory for 'dvc exp run'. "
|
||||
"Not compatible with 'dvc repro'."
|
||||
),
|
||||
metavar="<filename>",
|
||||
).complete = completion.FILE
|
||||
parser.add_argument(
|
||||
"--external",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allow outputs that are outside of the DVC repository.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--outs-persist",
|
||||
action="append",
|
||||
@@ -257,6 +263,12 @@ def _add_common_args(parser):
|
||||
"This doesn't affect any DVC operations."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Execute the stage after generating it.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"command",
|
||||
nargs=argparse.REMAINDER,
|
||||
|
||||
+5
-25
@@ -22,7 +22,6 @@ from typing import (
|
||||
from funcy import reraise
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
from dvc.ui.table import CellT
|
||||
|
||||
|
||||
@@ -35,7 +34,7 @@ def with_value(value, default):
|
||||
|
||||
|
||||
class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
def __init__(self, columns: Sequence[str], fill_value: str = ""):
|
||||
def __init__(self, columns: Sequence[str], fill_value: Optional[str] = ""):
|
||||
self._columns: Dict[str, Column] = {name: Column() for name in columns}
|
||||
self._keys: List[str] = list(columns)
|
||||
self._fill_value = fill_value
|
||||
@@ -58,7 +57,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
return self._columns[name]
|
||||
|
||||
def items(self) -> ItemsView[str, Column]:
|
||||
projection = {k: self.column(k) for k in self.keys()} # noqa: SIM118
|
||||
projection = {k: self.column(k) for k in self.keys()}
|
||||
return projection.items()
|
||||
|
||||
def keys(self) -> List[str]:
|
||||
@@ -161,24 +160,6 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
writer.writerow(row)
|
||||
return buff.getvalue()
|
||||
|
||||
def to_parallel_coordinates(
|
||||
self, output_path: "StrPath", color_by: Optional[str] = None
|
||||
) -> "StrPath":
|
||||
from dvc_render.html import render_html
|
||||
from dvc_render.plotly import ParallelCoordinatesRenderer
|
||||
|
||||
render_html(
|
||||
renderers=[
|
||||
ParallelCoordinatesRenderer(
|
||||
self.as_dict(),
|
||||
color_by=color_by,
|
||||
fill_value=self._fill_value,
|
||||
)
|
||||
],
|
||||
output_file=output_path,
|
||||
)
|
||||
return output_path
|
||||
|
||||
def add_column(self, name: str) -> None:
|
||||
self._columns[name] = Column([self._fill_value] * len(self))
|
||||
self._keys.append(name)
|
||||
@@ -190,8 +171,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
self.add_column(key)
|
||||
|
||||
row: List["CellT"] = [
|
||||
with_value(d.get(key), self._fill_value)
|
||||
for key in self.keys() # noqa: SIM118
|
||||
with_value(d.get(key), self._fill_value) for key in self.keys()
|
||||
]
|
||||
self.append(row)
|
||||
|
||||
@@ -246,7 +226,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
to_drop -= match_line
|
||||
|
||||
if axis == "rows":
|
||||
for name in self.keys(): # noqa: SIM118
|
||||
for name in self.keys():
|
||||
self._columns[name] = Column(
|
||||
[x for n, x in enumerate(self._columns[name]) if n not in to_drop]
|
||||
)
|
||||
@@ -294,7 +274,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
else:
|
||||
unique_rows.append(tuple_row)
|
||||
|
||||
for name in self.keys(): # noqa: SIM118
|
||||
for name in self.keys():
|
||||
self._columns[name] = Column(
|
||||
[
|
||||
x
|
||||
|
||||
+87
-34
@@ -79,9 +79,6 @@ class Config(dict):
|
||||
ConfigError: thrown if config has an invalid format.
|
||||
"""
|
||||
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
SYSTEM_LEVELS = ("system", "global")
|
||||
REPO_LEVELS = ("repo", "local")
|
||||
# In the order they shadow each other
|
||||
@@ -93,20 +90,30 @@ class Config(dict):
|
||||
def __init__(
|
||||
self,
|
||||
dvc_dir: Optional["StrPath"] = None,
|
||||
local_dvc_dir: Optional["StrPath"] = None,
|
||||
validate: bool = True,
|
||||
fs: Optional["FileSystem"] = None,
|
||||
config: Optional["DictStrAny"] = None,
|
||||
remote: Optional[str] = None,
|
||||
remote_config: Optional["DictStrAny"] = None,
|
||||
): # pylint: disable=super-init-not-called
|
||||
from dvc.fs import LocalFileSystem
|
||||
|
||||
dvc_dir = os.fspath(dvc_dir) if dvc_dir else None
|
||||
self.dvc_dir = dvc_dir
|
||||
self.wfs = LocalFileSystem()
|
||||
self.fs = fs or self.wfs
|
||||
|
||||
if dvc_dir:
|
||||
self.dvc_dir = self.fs.path.abspath(self.fs.path.realpath(dvc_dir))
|
||||
self.dvc_dir = self.fs.path.abspath(dvc_dir)
|
||||
|
||||
self.load(validate=validate, config=config)
|
||||
self.local_dvc_dir = local_dvc_dir
|
||||
if not fs and not local_dvc_dir:
|
||||
self.local_dvc_dir = dvc_dir
|
||||
|
||||
self.load(
|
||||
validate=validate, config=config, remote=remote, remote_config=remote_config
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_cwd(cls, fs: Optional["FileSystem"] = None, **kwargs):
|
||||
@@ -121,14 +128,14 @@ class Config(dict):
|
||||
|
||||
@classmethod
|
||||
def get_dir(cls, level):
|
||||
from platformdirs import site_config_dir, user_config_dir
|
||||
from dvc.dirs import global_config_dir, system_config_dir
|
||||
|
||||
assert level in ("global", "system")
|
||||
|
||||
if level == "global":
|
||||
return user_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
return global_config_dir()
|
||||
if level == "system":
|
||||
return site_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
return system_config_dir()
|
||||
|
||||
@cached_property
|
||||
def files(self) -> Dict[str, str]:
|
||||
@@ -139,7 +146,9 @@ class Config(dict):
|
||||
|
||||
if self.dvc_dir is not None:
|
||||
files["repo"] = self.fs.path.join(self.dvc_dir, self.CONFIG)
|
||||
files["local"] = self.fs.path.join(self.dvc_dir, self.CONFIG_LOCAL)
|
||||
|
||||
if self.local_dvc_dir is not None:
|
||||
files["local"] = self.wfs.path.join(self.local_dvc_dir, self.CONFIG_LOCAL)
|
||||
|
||||
return files
|
||||
|
||||
@@ -157,7 +166,16 @@ class Config(dict):
|
||||
with open(config_file, "w+", encoding="utf-8"):
|
||||
return Config(dvc_dir)
|
||||
|
||||
def load(self, validate: bool = True, config: Optional["DictStrAny"] = None):
|
||||
def merge(self, config):
|
||||
merge(self, config)
|
||||
|
||||
def load(
|
||||
self,
|
||||
validate: bool = True,
|
||||
config: Optional["DictStrAny"] = None,
|
||||
remote: Optional[str] = None,
|
||||
remote_config: Optional["DictStrAny"] = None,
|
||||
):
|
||||
"""Loads config from all the config files.
|
||||
|
||||
Raises:
|
||||
@@ -172,6 +190,17 @@ class Config(dict):
|
||||
conf = self.validate(conf)
|
||||
|
||||
self.clear()
|
||||
|
||||
if remote:
|
||||
conf["core"]["remote"] = remote
|
||||
|
||||
if remote_config:
|
||||
remote = remote or conf["core"].get("remote")
|
||||
if not remote:
|
||||
raise ValueError("Missing remote name")
|
||||
|
||||
merge(conf, {"remote": {remote: remote_config}})
|
||||
|
||||
self.update(conf)
|
||||
|
||||
def _get_fs(self, level):
|
||||
@@ -179,23 +208,32 @@ class Config(dict):
|
||||
# the repo.
|
||||
return self.fs if level == "repo" else self.wfs
|
||||
|
||||
def _load_config(self, level):
|
||||
@staticmethod
|
||||
def load_file(path, fs=None) -> dict:
|
||||
from configobj import ConfigObj, ConfigObjError
|
||||
|
||||
from dvc.fs import localfs
|
||||
|
||||
fs = fs or localfs
|
||||
|
||||
with fs.open(path) as fobj:
|
||||
try:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
except ConfigObjError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
|
||||
return _parse_named(_lower_keys(conf_obj.dict()))
|
||||
|
||||
def _load_config(self, level):
|
||||
filename = self.files[level]
|
||||
fs = self._get_fs(level)
|
||||
|
||||
if fs.exists(filename):
|
||||
with fs.open(filename) as fobj:
|
||||
try:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
except UnicodeDecodeError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
except ConfigObjError as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
else:
|
||||
conf_obj = ConfigObj()
|
||||
return _parse_named(_lower_keys(conf_obj.dict()))
|
||||
try:
|
||||
return self.load_file(filename, fs=fs)
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
def _save_config(self, level, conf_dict):
|
||||
from configobj import ConfigObj
|
||||
@@ -223,21 +261,30 @@ class Config(dict):
|
||||
return conf
|
||||
|
||||
@staticmethod
|
||||
def _load_paths(conf, filename):
|
||||
abs_conf_dir = os.path.abspath(os.path.dirname(filename))
|
||||
def _resolve(conf_dir, path):
|
||||
from .config_schema import ExpPath, RelPath
|
||||
|
||||
def resolve(path):
|
||||
from .config_schema import RelPath
|
||||
if re.match(r"\w+://", path):
|
||||
return path
|
||||
|
||||
if os.path.isabs(path) or re.match(r"\w+://", path):
|
||||
return path
|
||||
if os.path.isabs(path):
|
||||
return path
|
||||
|
||||
# on windows convert slashes to backslashes
|
||||
# to have path compatible with abs_conf_dir
|
||||
if os.path.sep == "\\" and "/" in path:
|
||||
path = path.replace("/", "\\")
|
||||
# on windows convert slashes to backslashes
|
||||
# to have path compatible with abs_conf_dir
|
||||
if os.path.sep == "\\" and "/" in path:
|
||||
path = path.replace("/", "\\")
|
||||
|
||||
return RelPath(os.path.join(abs_conf_dir, path))
|
||||
expanded = os.path.expanduser(path)
|
||||
if os.path.isabs(expanded):
|
||||
return ExpPath(expanded, path)
|
||||
|
||||
return RelPath(os.path.abspath(os.path.join(conf_dir, path)))
|
||||
|
||||
@classmethod
|
||||
def _load_paths(cls, conf, filename):
|
||||
conf_dir = os.path.abspath(os.path.dirname(filename))
|
||||
resolve = partial(cls._resolve, conf_dir)
|
||||
|
||||
return Config._map_dirs(conf, resolve)
|
||||
|
||||
@@ -246,11 +293,17 @@ class Config(dict):
|
||||
from dvc.fs import localfs
|
||||
from dvc.utils import relpath
|
||||
|
||||
from .config_schema import RelPath
|
||||
from .config_schema import ExpPath, RelPath
|
||||
|
||||
if re.match(r"\w+://", path):
|
||||
return path
|
||||
|
||||
if isinstance(path, ExpPath):
|
||||
return path.def_path
|
||||
|
||||
if os.path.expanduser(path) != path:
|
||||
return localfs.path.as_posix(path)
|
||||
|
||||
if isinstance(path, RelPath) or not os.path.isabs(path):
|
||||
path = relpath(path, conf_dir)
|
||||
return localfs.path.as_posix(path)
|
||||
|
||||
+24
-9
@@ -8,6 +8,7 @@ from voluptuous import (
|
||||
All,
|
||||
Any,
|
||||
Coerce,
|
||||
Exclusive,
|
||||
Invalid,
|
||||
Lower,
|
||||
Optional,
|
||||
@@ -74,8 +75,19 @@ def ByUrl(mapping): # noqa: N802
|
||||
return validate
|
||||
|
||||
|
||||
class ExpPath(str):
|
||||
__slots__ = ("def_path",)
|
||||
|
||||
def_path: str
|
||||
|
||||
def __new__(cls, string, def_path):
|
||||
ret = super().__new__(cls, string)
|
||||
ret.def_path = def_path
|
||||
return ret
|
||||
|
||||
|
||||
class RelPath(str):
|
||||
pass
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class FeatureSchema(Schema):
|
||||
@@ -154,13 +166,13 @@ SCHEMA = {
|
||||
"machine": Lower,
|
||||
},
|
||||
"cache": {
|
||||
"local": str,
|
||||
"s3": str,
|
||||
"gs": str,
|
||||
"hdfs": str,
|
||||
"webhdfs": str,
|
||||
"ssh": str,
|
||||
"azure": str,
|
||||
"local": str, # obsoleted
|
||||
"s3": str, # obsoleted
|
||||
"gs": str, # obsoleted
|
||||
"hdfs": str, # obsoleted
|
||||
"webhdfs": str, # obsoleted
|
||||
"ssh": str, # obsoleted
|
||||
"azure": str, # obsoleted
|
||||
# This is for default local cache
|
||||
"dir": str,
|
||||
**LOCAL_COMMON,
|
||||
@@ -324,11 +336,14 @@ SCHEMA = {
|
||||
},
|
||||
"hydra": {
|
||||
Optional("enabled", default=False): Bool,
|
||||
"config_dir": str,
|
||||
Exclusive("config_dir", "config_source"): str,
|
||||
Exclusive("config_module", "config_source"): str,
|
||||
"config_name": str,
|
||||
},
|
||||
"studio": {
|
||||
"token": str,
|
||||
"url": str,
|
||||
"repo_url": str,
|
||||
Optional("offline", default=False): Bool,
|
||||
},
|
||||
}
|
||||
|
||||
+3
-1
@@ -28,7 +28,9 @@ def _popen(cmd, **kwargs) -> Popen:
|
||||
os.path.abspath(os.path.dirname(__file__)), "__main__.py"
|
||||
)
|
||||
prefix += [main_entrypoint]
|
||||
return Popen(prefix + cmd, close_fds=True, shell=False, **kwargs) # nosec B603
|
||||
return Popen(
|
||||
prefix + cmd, close_fds=True, shell=False, **kwargs # nosec B603 # noqa: S603
|
||||
)
|
||||
|
||||
|
||||
def _spawn_windows(cmd, env):
|
||||
|
||||
@@ -209,6 +209,30 @@ def draw(vertices, edges):
|
||||
Args:
|
||||
vertices (list): list of graph vertices.
|
||||
edges (list): list of graph edges.
|
||||
|
||||
Returns:
|
||||
str: ASCII representation
|
||||
|
||||
Example:
|
||||
>>> from dvc.dagascii import draw
|
||||
>>> vertices = [1, 2, 3, 4]
|
||||
>>> edges = [(1, 2), (2, 3), (2, 4), (1, 4)]
|
||||
>>> print(draw(vertices, edges))
|
||||
+---+ +---+
|
||||
| 3 | | 4 |
|
||||
+---+ *+---+
|
||||
* ** *
|
||||
* ** *
|
||||
* * *
|
||||
+---+ *
|
||||
| 2 | *
|
||||
+---+ *
|
||||
* *
|
||||
* *
|
||||
**
|
||||
+---+
|
||||
| 1 |
|
||||
+---+
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
# NOTE: coordinates might me negative, so we need to shift
|
||||
|
||||
+158
-30
@@ -1,18 +1,19 @@
|
||||
"""Manages dvc remotes that user can use with push/pull/status commands."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, Set, Tuple
|
||||
|
||||
from dvc.config import NoRemoteError, RemoteConfigError
|
||||
from dvc.fs.callbacks import Callback
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_data.hashfile.db import get_index
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
from dvc_data.hashfile.transfer import TransferResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,13 +30,40 @@ class Remote:
|
||||
|
||||
@cached_property
|
||||
def odb(self) -> "HashFileDB":
|
||||
from dvc.cachemgr import CacheManager
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
|
||||
|
||||
path = self.path
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(path, ".dvc", "cache")
|
||||
path = self.fs.path.join(
|
||||
path, ".dvc", CacheManager.FILES_DIR, DEFAULT_ALGORITHM
|
||||
)
|
||||
else:
|
||||
path = self.fs.path.join(path, CacheManager.FILES_DIR, DEFAULT_ALGORITHM)
|
||||
return get_odb(self.fs, path, hash_name=DEFAULT_ALGORITHM, **self.config)
|
||||
|
||||
return get_odb(self.fs, path, hash_name="md5", **self.config)
|
||||
@cached_property
|
||||
def legacy_odb(self) -> "HashFileDB":
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
path = self.path
|
||||
return get_odb(self.fs, path, hash_name="md5-dos2unix", **self.config)
|
||||
|
||||
|
||||
def _split_legacy_hash_infos(
|
||||
hash_infos: Iterable["HashInfo"],
|
||||
) -> Tuple[Set["HashInfo"], Set["HashInfo"]]:
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
legacy = set()
|
||||
default = set()
|
||||
for hi in hash_infos:
|
||||
if hi.name in LEGACY_HASH_NAMES:
|
||||
legacy.add(hi)
|
||||
else:
|
||||
default.add(hi)
|
||||
return legacy, default
|
||||
|
||||
|
||||
class DataCloud:
|
||||
@@ -63,7 +91,7 @@ class DataCloud:
|
||||
if name:
|
||||
from dvc.fs import get_cloud_fs
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
cls, config, fs_path = get_cloud_fs(self.repo.config, name=name)
|
||||
|
||||
if config.get("worktree"):
|
||||
version_aware = config.get("version_aware")
|
||||
@@ -101,12 +129,17 @@ class DataCloud:
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
hash_name: str = "md5",
|
||||
) -> "HashFileDB":
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
remote = self.get_remote(name=name, command=command)
|
||||
if remote.fs.version_aware or remote.worktree:
|
||||
raise NoRemoteError(
|
||||
f"'{command}' is unsupported for cloud versioned remotes"
|
||||
)
|
||||
if hash_name in LEGACY_HASH_NAMES:
|
||||
return remote.legacy_odb
|
||||
return remote.odb
|
||||
|
||||
def _log_missing(self, status: "CompareStatusResult"):
|
||||
@@ -150,16 +183,47 @@ class DataCloud:
|
||||
By default remote from core.remote config option is used.
|
||||
odb: optional ODB to push to. Overrides remote.
|
||||
"""
|
||||
odb = odb or self.get_remote_odb(remote, "push")
|
||||
return self.transfer(
|
||||
self.repo.cache.local,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=self.repo.cache.local,
|
||||
validate_status=self._log_missing,
|
||||
)
|
||||
if odb is not None:
|
||||
return self._push(objs, jobs=jobs, odb=odb)
|
||||
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
|
||||
result = TransferResult(set(), set())
|
||||
if legacy_objs:
|
||||
odb = self.get_remote_odb(remote, "push", hash_name="md5-dos2unix")
|
||||
t, f = self._push(legacy_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
if default_objs:
|
||||
odb = self.get_remote_odb(remote, "push")
|
||||
t, f = self._push(default_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
return result
|
||||
|
||||
def _push(
|
||||
self,
|
||||
objs: Iterable["HashInfo"],
|
||||
*,
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
) -> "TransferResult":
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Pushing to {odb.fs.unstrip_protocol(odb.path)}",
|
||||
unit="file",
|
||||
) as cb:
|
||||
return self.transfer(
|
||||
cache,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=cache,
|
||||
validate_status=self._log_missing,
|
||||
callback=cb,
|
||||
)
|
||||
|
||||
def pull(
|
||||
self,
|
||||
@@ -177,17 +241,49 @@ class DataCloud:
|
||||
By default remote from core.remote config option is used.
|
||||
odb: optional ODB to pull from. Overrides remote.
|
||||
"""
|
||||
odb = odb or self.get_remote_odb(remote, "pull")
|
||||
return self.transfer(
|
||||
odb,
|
||||
self.repo.cache.local,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
src_index=get_index(odb),
|
||||
cache_odb=self.repo.cache.local,
|
||||
verify=odb.verify,
|
||||
validate_status=self._log_missing,
|
||||
)
|
||||
if odb is not None:
|
||||
return self._pull(objs, jobs=jobs, odb=odb)
|
||||
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
|
||||
result = TransferResult(set(), set())
|
||||
if legacy_objs:
|
||||
odb = self.get_remote_odb(remote, "pull", hash_name="md5-dos2unix")
|
||||
assert odb.hash_name == "md5-dos2unix"
|
||||
t, f = self._pull(legacy_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
if default_objs:
|
||||
odb = self.get_remote_odb(remote, "pull")
|
||||
t, f = self._pull(default_objs, jobs=jobs, odb=odb)
|
||||
result.transferred.update(t)
|
||||
result.failed.update(f)
|
||||
return result
|
||||
|
||||
def _pull(
|
||||
self,
|
||||
objs: Iterable["HashInfo"],
|
||||
*,
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
) -> "TransferResult":
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Fetching from {odb.fs.unstrip_protocol(odb.path)}",
|
||||
unit="file",
|
||||
) as cb:
|
||||
return self.transfer(
|
||||
odb,
|
||||
cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
src_index=get_index(odb),
|
||||
cache_odb=cache,
|
||||
verify=odb.verify,
|
||||
validate_status=self._log_missing,
|
||||
callback=cb,
|
||||
)
|
||||
|
||||
def status(
|
||||
self,
|
||||
@@ -206,17 +302,49 @@ class DataCloud:
|
||||
is used.
|
||||
odb: optional ODB to check status from. Overrides remote.
|
||||
"""
|
||||
from dvc_data.hashfile.status import CompareStatusResult
|
||||
|
||||
if odb is not None:
|
||||
return self._status(objs, jobs=jobs, odb=odb)
|
||||
result = CompareStatusResult(set(), set(), set(), set())
|
||||
legacy_objs, default_objs = _split_legacy_hash_infos(objs)
|
||||
if legacy_objs:
|
||||
odb = self.get_remote_odb(remote, "status", hash_name="md5-dos2unix")
|
||||
assert odb.hash_name == "md5-dos2unix"
|
||||
o, m, n, d = self._status(legacy_objs, jobs=jobs, odb=odb)
|
||||
result.ok.update(o)
|
||||
result.missing.update(m)
|
||||
result.new.update(n)
|
||||
result.deleted.update(d)
|
||||
if default_objs:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
o, m, n, d = self._status(default_objs, jobs=jobs, odb=odb)
|
||||
result.ok.update(o)
|
||||
result.missing.update(m)
|
||||
result.new.update(n)
|
||||
result.deleted.update(d)
|
||||
return result
|
||||
|
||||
def _status(
|
||||
self,
|
||||
objs: Iterable["HashInfo"],
|
||||
*,
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
):
|
||||
from dvc_data.hashfile.status import compare_status
|
||||
|
||||
if not odb:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
return compare_status(
|
||||
self.repo.cache.local,
|
||||
cache,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=self.repo.cache.local,
|
||||
cache_odb=cache,
|
||||
)
|
||||
|
||||
def get_url_for(self, remote, checksum):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from collections import defaultdict
|
||||
from typing import Any, Mapping, Set
|
||||
from typing import Any, Dict, List, Mapping, Set
|
||||
|
||||
from dvc.output import ARTIFACT_SCHEMA, DIR_FILES_SCHEMA, Output
|
||||
|
||||
@@ -14,8 +14,8 @@ from .repo import RepoDependency
|
||||
SCHEMA: Mapping[str, Any] = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**RepoDependency.REPO_SCHEMA,
|
||||
**ParamsDependency.PARAM_SCHEMA,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
Output.PARAM_FS_CONFIG: dict,
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,11 @@ def loadd_from(stage, d_list):
|
||||
for d in d_list:
|
||||
p = d.pop(Output.PARAM_PATH, None)
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
ret.append(_get(stage, p, d, files=files))
|
||||
hash_name = d.pop(Output.PARAM_HASH, None)
|
||||
fs_config = d.pop(Output.PARAM_FS_CONFIG, None)
|
||||
ret.append(
|
||||
_get(stage, p, d, files=files, hash_name=hash_name, fs_config=fs_config)
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
@@ -54,7 +58,7 @@ def loads_from(stage, s_list, erepo=None, fs_config=None):
|
||||
]
|
||||
|
||||
|
||||
def _merge_params(s_list):
|
||||
def _merge_params(s_list) -> Dict[str, List[str]]:
|
||||
d = defaultdict(list)
|
||||
default_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
|
||||
|
||||
@@ -44,13 +44,10 @@ class Dependency(Output):
|
||||
if self.fs.version_aware:
|
||||
self.fs_path = self.fs.path.version_path(self.fs_path, rev)
|
||||
self.meta = self.get_meta()
|
||||
self.def_path = self.fs.path.version_path(
|
||||
self.def_path, self.meta.version_id
|
||||
)
|
||||
self.fs_path = self.fs.path.version_path(self.fs_path, self.meta.version_id)
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
fs_download(self.fs, self.fs_path, to, jobs=jobs)
|
||||
fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
|
||||
|
||||
def save(self):
|
||||
super().save()
|
||||
|
||||
+46
-32
@@ -2,10 +2,9 @@ import logging
|
||||
import os
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
import dpath
|
||||
from voluptuous import Any
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils.serialize import ParseError, load_path
|
||||
@@ -13,6 +12,9 @@ from dvc_data.hashfile.hash_info import HashInfo
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -32,9 +34,39 @@ class BadParamFileError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
def read_param_file(
|
||||
fs: "FileSystem",
|
||||
path: str,
|
||||
key_paths: Optional[List[str]] = None,
|
||||
flatten: bool = False,
|
||||
**load_kwargs,
|
||||
) -> Any:
|
||||
config = load_path(path, fs, **load_kwargs)
|
||||
if not key_paths:
|
||||
return config
|
||||
|
||||
ret = {}
|
||||
if flatten:
|
||||
for key_path in key_paths:
|
||||
try:
|
||||
ret[key_path] = dpath.get(config, key_path, separator=".")
|
||||
except KeyError:
|
||||
continue
|
||||
return ret
|
||||
|
||||
from dpath import merge
|
||||
|
||||
for key_path in key_paths:
|
||||
merge(
|
||||
ret,
|
||||
dpath.search(config, key_path, separator="."),
|
||||
separator=".",
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
class ParamsDependency(Dependency):
|
||||
PARAM_PARAMS = "params"
|
||||
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
|
||||
DEFAULT_PARAMS_FILE = "params.yaml"
|
||||
|
||||
def __init__(self, stage, path, params=None, repo=None):
|
||||
@@ -48,6 +80,7 @@ class ParamsDependency(Dependency):
|
||||
repo = repo or stage.repo
|
||||
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
|
||||
super().__init__(stage, path, repo=repo)
|
||||
self.hash_name = self.PARAM_PARAMS
|
||||
self.hash_info = hash_info
|
||||
|
||||
def dumpd(self, **kwargs):
|
||||
@@ -76,31 +109,19 @@ class ParamsDependency(Dependency):
|
||||
self, flatten: bool = True, **kwargs: typing.Any
|
||||
) -> Dict[str, typing.Any]:
|
||||
try:
|
||||
config = self.read_file()
|
||||
self.validate_filepath()
|
||||
except MissingParamsFile:
|
||||
config = {}
|
||||
return {}
|
||||
|
||||
if not self.params:
|
||||
return config
|
||||
|
||||
ret = {}
|
||||
if flatten:
|
||||
for param in self.params:
|
||||
try:
|
||||
ret[param] = dpath.get(config, param, separator=".")
|
||||
except KeyError:
|
||||
continue
|
||||
return ret
|
||||
|
||||
from dpath import merge
|
||||
|
||||
for param in self.params:
|
||||
merge(
|
||||
ret,
|
||||
dpath.search(config, param, separator="."),
|
||||
separator=".",
|
||||
try:
|
||||
return read_param_file(
|
||||
self.repo.fs,
|
||||
self.fs_path,
|
||||
list(self.params) if self.params else None,
|
||||
flatten=flatten,
|
||||
)
|
||||
return ret
|
||||
except ParseError as exc:
|
||||
raise BadParamFileError(f"Unable to read parameters from '{self}'") from exc
|
||||
|
||||
def workspace_status(self):
|
||||
if not self.exists:
|
||||
@@ -150,13 +171,6 @@ class ParamsDependency(Dependency):
|
||||
f"'{self}' is a directory, expected a parameters file"
|
||||
)
|
||||
|
||||
def read_file(self):
|
||||
self.validate_filepath()
|
||||
try:
|
||||
return load_path(self.fs_path, self.repo.fs)
|
||||
except ParseError as exc:
|
||||
raise BadParamFileError(f"Unable to read parameters from '{self}'") from exc
|
||||
|
||||
def get_hash(self):
|
||||
info = self.read_params()
|
||||
|
||||
|
||||
+50
-3
@@ -1,6 +1,7 @@
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Union
|
||||
|
||||
from voluptuous import Required
|
||||
from voluptuous import Any, Required
|
||||
|
||||
from dvc.utils import as_posix
|
||||
|
||||
@@ -16,16 +17,20 @@ class RepoDependency(Dependency):
|
||||
PARAM_URL = "url"
|
||||
PARAM_REV = "rev"
|
||||
PARAM_REV_LOCK = "rev_lock"
|
||||
PARAM_CONFIG = "config"
|
||||
PARAM_REMOTE = "remote"
|
||||
|
||||
REPO_SCHEMA = {
|
||||
PARAM_REPO: {
|
||||
Required(PARAM_URL): str,
|
||||
PARAM_REV: str,
|
||||
PARAM_REV_LOCK: str,
|
||||
PARAM_CONFIG: Any(str, dict),
|
||||
PARAM_REMOTE: Any(str, dict),
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, def_repo: Dict[str, str], stage: "Stage", *args, **kwargs):
|
||||
def __init__(self, def_repo: Dict[str, Any], stage: "Stage", *args, **kwargs):
|
||||
self.def_repo = def_repo
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
@@ -60,7 +65,28 @@ class RepoDependency(Dependency):
|
||||
self.def_repo[self.PARAM_REV_LOCK] = rev
|
||||
|
||||
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
repo = {self.PARAM_URL: self.def_repo[self.PARAM_URL]}
|
||||
|
||||
rev = self.def_repo.get(self.PARAM_REV)
|
||||
if rev:
|
||||
repo[self.PARAM_REV] = self.def_repo[self.PARAM_REV]
|
||||
|
||||
rev_lock = self.def_repo.get(self.PARAM_REV_LOCK)
|
||||
if rev_lock:
|
||||
repo[self.PARAM_REV_LOCK] = rev_lock
|
||||
|
||||
config = self.def_repo.get(self.PARAM_CONFIG)
|
||||
if config:
|
||||
repo[self.PARAM_CONFIG] = config
|
||||
|
||||
remote = self.def_repo.get(self.PARAM_REMOTE)
|
||||
if remote:
|
||||
repo[self.PARAM_REMOTE] = remote
|
||||
|
||||
return {
|
||||
self.PARAM_PATH: self.def_path,
|
||||
self.PARAM_REPO: repo,
|
||||
}
|
||||
|
||||
def update(self, rev: Optional[str] = None):
|
||||
if rev:
|
||||
@@ -77,12 +103,33 @@ class RepoDependency(Dependency):
|
||||
def _make_fs(
|
||||
self, rev: Optional[str] = None, locked: bool = True
|
||||
) -> "DVCFileSystem":
|
||||
from dvc.config import Config
|
||||
from dvc.fs import DVCFileSystem
|
||||
|
||||
rem = self.def_repo.get("remote")
|
||||
if isinstance(rem, dict):
|
||||
remote = None
|
||||
remote_config = rem
|
||||
else:
|
||||
remote = rem
|
||||
remote_config = None
|
||||
|
||||
conf = self.def_repo.get("config", {})
|
||||
if isinstance(conf, dict):
|
||||
config = deepcopy(conf)
|
||||
else:
|
||||
config = Config.load_file(conf)
|
||||
|
||||
config["cache"] = self.repo.config["cache"]
|
||||
config["cache"]["dir"] = self.repo.cache.local_cache_dir
|
||||
|
||||
return DVCFileSystem(
|
||||
url=self.def_repo[self.PARAM_URL],
|
||||
rev=rev or self._get_rev(locked=locked),
|
||||
subrepos=True,
|
||||
config=config,
|
||||
remote=remote,
|
||||
remote_config=remote_config,
|
||||
)
|
||||
|
||||
def _get_rev(self, locked: bool = True):
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
|
||||
import platformdirs
|
||||
|
||||
from . import env
|
||||
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
|
||||
def system_config_dir():
|
||||
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.site_config_dir(
|
||||
APPNAME, APPAUTHOR
|
||||
)
|
||||
|
||||
|
||||
def global_config_dir():
|
||||
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.user_config_dir(
|
||||
APPNAME, APPAUTHOR
|
||||
)
|
||||
|
||||
|
||||
def site_cache_dir():
|
||||
return os.getenv(env.DVC_SITE_CACHE_DIR) or platformdirs.site_cache_dir(
|
||||
APPNAME, APPAUTHOR, opinion=True
|
||||
)
|
||||
+6
-51
@@ -14,7 +14,6 @@ from typing import (
|
||||
)
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
|
||||
from dvc.stage import serialize
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileBadNameError,
|
||||
@@ -36,7 +35,6 @@ if TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
_T = TypeVar("_T")
|
||||
|
||||
DVC_FILE = "Dvcfile"
|
||||
DVC_FILE_SUFFIX = ".dvc"
|
||||
PROJECT_FILE = "dvc.yaml"
|
||||
LOCK_FILE = "dvc.lock"
|
||||
@@ -56,10 +54,7 @@ class ParametrizedDumpError(DvcException):
|
||||
|
||||
|
||||
def is_valid_filename(path):
|
||||
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [
|
||||
DVC_FILE,
|
||||
PROJECT_FILE,
|
||||
]
|
||||
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) == PROJECT_FILE
|
||||
|
||||
|
||||
def is_dvc_file(path):
|
||||
@@ -102,9 +97,7 @@ class FileMixin:
|
||||
self.verify = verify
|
||||
|
||||
def __repr__(self):
|
||||
return "{}: {}".format(
|
||||
self.__class__.__name__, relpath(self.path, self.repo.root_dir)
|
||||
)
|
||||
return f"{self.__class__.__name__}: {relpath(self.path, self.repo.root_dir)}"
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.path)
|
||||
@@ -358,36 +351,8 @@ class ProjectFile(FileMixin):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def get_lockfile_schema(d):
|
||||
from dvc.schema import COMPILED_LOCKFILE_V1_SCHEMA, COMPILED_LOCKFILE_V2_SCHEMA
|
||||
|
||||
schema = {
|
||||
LOCKFILE_VERSION.V1: COMPILED_LOCKFILE_V1_SCHEMA,
|
||||
LOCKFILE_VERSION.V2: COMPILED_LOCKFILE_V2_SCHEMA,
|
||||
}
|
||||
|
||||
version = LOCKFILE_VERSION.from_dict(d)
|
||||
return schema[version]
|
||||
|
||||
|
||||
def migrate_lock_v1_to_v2(d, version_info):
|
||||
stages = dict(d)
|
||||
|
||||
for key in stages:
|
||||
d.pop(key)
|
||||
|
||||
# forcing order, meta should always be at the top
|
||||
d.update(version_info)
|
||||
d["stages"] = stages
|
||||
|
||||
|
||||
def lockfile_schema(data: _T) -> _T:
|
||||
schema = get_lockfile_schema(data)
|
||||
return schema(data)
|
||||
|
||||
|
||||
class Lockfile(FileMixin):
|
||||
SCHEMA = staticmethod(lockfile_schema) # type: ignore[assignment]
|
||||
from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA
|
||||
|
||||
def _verify_filename(self):
|
||||
pass # lockfile path is hardcoded, so no need to verify here
|
||||
@@ -401,21 +366,12 @@ class Lockfile(FileMixin):
|
||||
self._check_gitignored()
|
||||
return {}, ""
|
||||
|
||||
@property
|
||||
def latest_version_info(self):
|
||||
version = LOCKFILE_VERSION.V2.value # pylint:disable=no-member
|
||||
return {SCHEMA_KWD: version}
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
version = LOCKFILE_VERSION.from_dict(data)
|
||||
if version == LOCKFILE_VERSION.V1:
|
||||
logger.info("Migrating lock file '%s' from v1 to v2", self.relpath)
|
||||
migrate_lock_v1_to_v2(data, self.latest_version_info)
|
||||
elif not data:
|
||||
data.update(self.latest_version_info)
|
||||
if not data:
|
||||
data.update({"schema": "2.0"})
|
||||
# order is important, meta should always be at the top
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
|
||||
@@ -436,8 +392,7 @@ class Lockfile(FileMixin):
|
||||
return
|
||||
|
||||
d, _ = self._load_yaml(round_trip=True)
|
||||
version = LOCKFILE_VERSION.from_dict(d)
|
||||
data = d if version == LOCKFILE_VERSION.V1 else d.get("stages", {})
|
||||
data = d.get("stages", {})
|
||||
if stage.name not in data:
|
||||
return
|
||||
|
||||
|
||||
+11
-8
@@ -1,14 +1,17 @@
|
||||
DVC_CHECKPOINT = "DVC_CHECKPOINT"
|
||||
DVC_DAEMON = "DVC_DAEMON"
|
||||
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
|
||||
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
DVC_EXP_NAME = "DVC_EXP_NAME"
|
||||
DVC_GLOBAL_CONFIG_DIR = "DVC_GLOBAL_CONFIG_DIR"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
|
||||
DVC_PAGER = "DVC_PAGER"
|
||||
DVC_ROOT = "DVC_ROOT"
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
|
||||
DVC_EXP_NAME = "DVC_EXP_NAME"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
|
||||
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
|
||||
DVC_SHOW_TRACEBACK = "DVC_SHOW_TRACEBACK"
|
||||
DVC_SITE_CACHE_DIR = "DVC_SITE_CACHE_DIR"
|
||||
DVC_STUDIO_OFFLINE = "DVC_STUDIO_OFFLINE"
|
||||
DVC_STUDIO_REPO_URL = "DVC_STUDIO_REPO_URL"
|
||||
DVC_STUDIO_TOKEN = "DVC_STUDIO_TOKEN" # noqa: S105 # nosec B105
|
||||
DVC_STUDIO_URL = "DVC_STUDIO_URL"
|
||||
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
|
||||
|
||||
+43
-48
@@ -1,8 +1,12 @@
|
||||
"""Exceptions raised by the dvc."""
|
||||
from typing import Dict, List
|
||||
import errno
|
||||
from typing import TYPE_CHECKING, Dict, List, Set
|
||||
|
||||
from dvc.utils import format_link
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.stage import Stage
|
||||
|
||||
|
||||
class DvcException(Exception):
|
||||
"""Base class for all dvc exceptions."""
|
||||
@@ -16,6 +20,10 @@ class DvcException(Exception):
|
||||
class InvalidArgumentError(ValueError, DvcException):
|
||||
"""Thrown if arguments are invalid."""
|
||||
|
||||
def __init__(self, msg, *args):
|
||||
self.msg = msg
|
||||
super().__init__(msg, *args)
|
||||
|
||||
|
||||
class OutputDuplicationError(DvcException):
|
||||
"""Thrown if a file/directory is specified as an output in more than one
|
||||
@@ -26,24 +34,25 @@ class OutputDuplicationError(DvcException):
|
||||
stages (list): list of paths to stages.
|
||||
"""
|
||||
|
||||
def __init__(self, output, stages):
|
||||
def __init__(self, output: str, stages: Set["Stage"]):
|
||||
from funcy import first
|
||||
|
||||
assert isinstance(output, str)
|
||||
assert all(hasattr(stage, "relpath") for stage in stages)
|
||||
msg = ""
|
||||
stage_names = [s.addressing for s in stages]
|
||||
stages_str = " ".join(stage_names)
|
||||
if len(stages) == 1:
|
||||
stage_name = first(stages)
|
||||
msg = f"output '{output}' is already specified in {stage_name}."
|
||||
stage = first(stages)
|
||||
msg = (
|
||||
f"output '{output}' is already specified in {stage}."
|
||||
f"\nUse `dvc remove {stage.addressing}` to stop tracking the "
|
||||
"overlapping output."
|
||||
)
|
||||
else:
|
||||
msg = "output '{}' is already specified in stages:\n{}".format(
|
||||
output, "\n".join(f"\t- {s}" for s in stage_names)
|
||||
stage_names = "\n".join(["\t- " + s.addressing for s in stages])
|
||||
msg = (
|
||||
f"output '{output}' is specified in:\n{stage_names}"
|
||||
"\nUse `dvc remove` with any of the above targets to stop tracking the "
|
||||
"overlapping output."
|
||||
)
|
||||
msg += (
|
||||
f"\nUse `dvc remove {stages_str}` to stop tracking the overlapping output."
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.stages = stages
|
||||
self.output = output
|
||||
@@ -62,12 +71,14 @@ class OutputNotFoundError(DvcException):
|
||||
self.output = output
|
||||
self.repo = repo
|
||||
super().__init__(
|
||||
"Unable to find DVC file with output '{path}'".format(
|
||||
path=relpath(self.output)
|
||||
)
|
||||
f"Unable to find DVC file with output {relpath(self.output)!r}"
|
||||
)
|
||||
|
||||
|
||||
class StageNotFoundError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class StagePathAsOutputError(DvcException):
|
||||
"""Thrown if directory that stage is going to be saved in is specified as
|
||||
an output of another stage.
|
||||
@@ -79,11 +90,7 @@ class StagePathAsOutputError(DvcException):
|
||||
|
||||
def __init__(self, stage, output):
|
||||
assert isinstance(output, str)
|
||||
super().__init__(
|
||||
"{stage} is within an output '{output}' of another stage".format(
|
||||
stage=stage, output=output
|
||||
)
|
||||
)
|
||||
super().__init__(f"{stage} is within an output {output!r} of another stage")
|
||||
|
||||
|
||||
class CircularDependencyError(DvcException):
|
||||
@@ -155,9 +162,7 @@ class CyclicGraphError(DvcException):
|
||||
class ConfirmRemoveError(DvcException):
|
||||
def __init__(self, path):
|
||||
super().__init__(
|
||||
"unable to remove '{}' without a confirmation. Use `-f` to force.".format(
|
||||
path
|
||||
)
|
||||
f"unable to remove {path!r} without a confirmation. Use `-f` to force."
|
||||
)
|
||||
|
||||
|
||||
@@ -166,9 +171,7 @@ class InitError(DvcException):
|
||||
|
||||
|
||||
class ReproductionError(DvcException):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
super().__init__(f"failed to reproduce '{name}'")
|
||||
pass
|
||||
|
||||
|
||||
class BadMetricError(DvcException):
|
||||
@@ -181,11 +184,6 @@ class BadMetricError(DvcException):
|
||||
)
|
||||
|
||||
|
||||
class RecursiveAddingWhileUsingFilename(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__("cannot use `fname` with multiple targets or `-R|--recursive`")
|
||||
|
||||
|
||||
class OverlappingOutputPathsError(DvcException):
|
||||
def __init__(self, parent, overlapping_out, message):
|
||||
self.parent = parent
|
||||
@@ -202,10 +200,22 @@ class ETagMismatchError(DvcException):
|
||||
def __init__(self, etag, cached_etag):
|
||||
super().__init__(
|
||||
"ETag mismatch detected when copying file to cache! "
|
||||
"(expected: '{}', actual: '{}')".format(etag, cached_etag)
|
||||
f"(expected: '{etag}', actual: '{cached_etag}')"
|
||||
)
|
||||
|
||||
|
||||
class FileExistsLocallyError(FileExistsError, DvcException):
|
||||
def __init__(self, path, hint=None):
|
||||
import os.path
|
||||
|
||||
self.path = path
|
||||
hint = "" if hint is None else f". {hint}"
|
||||
path_typ = "directory" if os.path.isdir(path) else "file"
|
||||
msg = f"The {path_typ} '{path}' already exists locally{hint}"
|
||||
super().__init__(msg)
|
||||
self.errno = errno.EEXIST
|
||||
|
||||
|
||||
class FileMissingError(DvcException):
|
||||
def __init__(self, path, hint=None):
|
||||
self.path = path
|
||||
@@ -291,22 +301,7 @@ class URLMissingError(DvcException):
|
||||
super().__init__(f"The path '{url}' does not exist")
|
||||
|
||||
|
||||
class RemoteCacheRequiredError(DvcException):
|
||||
def __init__(self, scheme, fs_path):
|
||||
super().__init__(
|
||||
(
|
||||
"Current operation was unsuccessful because '{}' requires "
|
||||
"existing cache on '{}' remote. See {} for information on how "
|
||||
"to set up remote cache."
|
||||
).format(
|
||||
fs_path,
|
||||
scheme,
|
||||
format_link("https://man.dvc.org/config#cache"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class IsADirectoryError(DvcException): # noqa,pylint:disable=redefined-builtin
|
||||
class IsADirectoryError(DvcException): # noqa: A001,pylint:disable=redefined-builtin
|
||||
"""Raised when a file operation is requested on a directory."""
|
||||
|
||||
|
||||
|
||||
+35
-11
@@ -1,3 +1,4 @@
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
||||
@@ -18,7 +19,7 @@ from dvc_objects.fs import ( # noqa: F401
|
||||
system,
|
||||
utils,
|
||||
)
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401, TCH001
|
||||
from dvc_objects.fs.errors import ( # noqa: F401
|
||||
AuthError,
|
||||
ConfigError,
|
||||
@@ -48,18 +49,45 @@ known_implementations.update(
|
||||
# pylint: enable=unused-import
|
||||
|
||||
|
||||
def download(fs, fs_path, to, jobs=None):
|
||||
def download(fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None):
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Downloading {fs.path.name(fs_path)}",
|
||||
unit="files",
|
||||
) as cb:
|
||||
fs.get(fs_path, to.fs_path, batch_size=jobs, callback=cb)
|
||||
# NOTE: We use dvc-objects generic.copy over fs.get since it makes file
|
||||
# download atomic and avoids fsspec glob/regex path expansion.
|
||||
if fs.isdir(fs_path):
|
||||
from_infos = [
|
||||
path
|
||||
for path in fs.find(fs_path)
|
||||
if not path.endswith(fs.path.flavour.sep)
|
||||
]
|
||||
if not from_infos:
|
||||
return localfs.makedirs(to, exist_ok=True)
|
||||
to_infos = [
|
||||
localfs.path.join(to, *fs.path.relparts(info, fs_path))
|
||||
for info in from_infos
|
||||
]
|
||||
else:
|
||||
from_infos = [fs_path]
|
||||
to_infos = [to]
|
||||
|
||||
cb.set_size(len(from_infos))
|
||||
jobs = jobs or fs.jobs
|
||||
generic.copy(
|
||||
fs,
|
||||
from_infos,
|
||||
localfs,
|
||||
to_infos,
|
||||
callback=cb,
|
||||
batch_size=jobs,
|
||||
)
|
||||
|
||||
|
||||
def parse_external_url(url, config=None):
|
||||
remote_config = dict(config) if config else {}
|
||||
remote_config["url"] = url
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(None, **remote_config)
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs({}, **remote_config)
|
||||
fs = fs_cls(**fs_config)
|
||||
return fs, fs_path
|
||||
|
||||
@@ -105,18 +133,14 @@ def _resolve_remote_refs(config, remote_conf):
|
||||
return remote_conf
|
||||
|
||||
base = get_fs_config(config, name=parsed.netloc)
|
||||
cls, _, _ = _get_cloud_fs(config, **base)
|
||||
cls, _, _ = get_cloud_fs(config, **base)
|
||||
relpath = parsed.path.lstrip("/").replace("/", cls.sep)
|
||||
url = cls.sep.join((base["url"], relpath))
|
||||
return {**base, **remote_conf, "url": url}
|
||||
|
||||
|
||||
def get_cloud_fs(repo, **kwargs):
|
||||
repo_config = repo.config if repo else {}
|
||||
return _get_cloud_fs(repo_config, **kwargs)
|
||||
|
||||
|
||||
def _get_cloud_fs(repo_config, **kwargs):
|
||||
def get_cloud_fs(repo_config, **kwargs):
|
||||
repo_config = repo_config or {}
|
||||
core_config = repo_config.get("core", {})
|
||||
|
||||
remote_conf = get_fs_config(repo_config, **kwargs)
|
||||
|
||||
+55
-5
@@ -2,12 +2,9 @@
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_objects.fs.callbacks import ( # noqa: F401
|
||||
DEFAULT_CALLBACK,
|
||||
Callback,
|
||||
TqdmCallback,
|
||||
)
|
||||
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.progress import TaskID
|
||||
@@ -15,6 +12,59 @@ if TYPE_CHECKING:
|
||||
from dvc.ui._rich_progress import RichTransferProgress
|
||||
|
||||
|
||||
# NOTE: this is very similar to dvc-objects.fs.callbacks.TqdmCallback,
|
||||
# but it works with our own Tqdm instance.
|
||||
class TqdmCallback(Callback):
|
||||
def __init__(
|
||||
self,
|
||||
size: Optional[int] = None,
|
||||
value: int = 0,
|
||||
progress_bar: Optional["Tqdm"] = None,
|
||||
**tqdm_kwargs,
|
||||
):
|
||||
tqdm_kwargs["total"] = size or -1
|
||||
self._tqdm_kwargs = tqdm_kwargs
|
||||
self._progress_bar = progress_bar
|
||||
self._stack = ExitStack()
|
||||
super().__init__(size=size, value=value)
|
||||
|
||||
@cached_property
|
||||
def progress_bar(self):
|
||||
progress_bar = (
|
||||
self._progress_bar
|
||||
if self._progress_bar is not None
|
||||
else Tqdm(**self._tqdm_kwargs)
|
||||
)
|
||||
return self._stack.enter_context(progress_bar)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self):
|
||||
self._stack.close()
|
||||
|
||||
def set_size(self, size):
|
||||
# Tqdm tries to be smart when to refresh,
|
||||
# so we try to force it to re-render.
|
||||
super().set_size(size)
|
||||
self.progress_bar.refresh()
|
||||
|
||||
def call(self, hook_name=None, **kwargs): # noqa: ARG002
|
||||
self.progress_bar.update_to(self.value, total=self.size)
|
||||
|
||||
def branch(
|
||||
self,
|
||||
path_1: "Union[str, BinaryIO]",
|
||||
path_2: str,
|
||||
kwargs: Dict[str, Any],
|
||||
child: Optional[Callback] = None,
|
||||
):
|
||||
child = child or TqdmCallback(
|
||||
bytes=True, desc=path_1 if isinstance(path_1, str) else path_2
|
||||
)
|
||||
return super().branch(path_1, path_2, kwargs, child=child)
|
||||
|
||||
|
||||
class RichCallback(Callback):
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
+73
-21
@@ -5,7 +5,7 @@ import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import threading
|
||||
from contextlib import suppress
|
||||
from contextlib import ExitStack, suppress
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Union
|
||||
|
||||
from fsspec.spec import AbstractFileSystem
|
||||
@@ -18,7 +18,7 @@ from .data import DataFileSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.types import StrPath
|
||||
from dvc.types import DictStrAny, StrPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,17 +38,24 @@ def _is_dvc_file(fname):
|
||||
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
|
||||
|
||||
|
||||
def _merge_info(repo, fs_info, dvc_info):
|
||||
def _merge_info(repo, key, fs_info, dvc_info):
|
||||
from . import utils
|
||||
|
||||
ret = {"repo": repo}
|
||||
|
||||
if dvc_info:
|
||||
dvc_info["isout"] = any(
|
||||
(len(out_key) <= len(key) and key[: len(out_key)] == out_key)
|
||||
for out_key in repo.index.data_keys["repo"]
|
||||
)
|
||||
dvc_info["isdvc"] = dvc_info["isout"]
|
||||
ret["dvc_info"] = dvc_info
|
||||
ret["type"] = dvc_info["type"]
|
||||
ret["size"] = dvc_info["size"]
|
||||
if not fs_info and "md5" in dvc_info:
|
||||
ret["md5"] = dvc_info["md5"]
|
||||
if not fs_info and "md5-dos2unix" in dvc_info:
|
||||
ret["md5-dos2unix"] = dvc_info["md5-dos2unix"]
|
||||
|
||||
if fs_info:
|
||||
ret["type"] = fs_info["type"]
|
||||
@@ -69,14 +76,22 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
cachable = False
|
||||
root_marker = "/"
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
url: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
repo: Optional["Repo"] = None,
|
||||
subrepos: bool = False,
|
||||
repo_factory: Optional[RepoFactory] = None,
|
||||
**repo_kwargs: Any,
|
||||
fo: Optional[str] = None,
|
||||
# pylint:disable-next=unused-argument
|
||||
target_options: Optional[Dict[str, Any]] = None, # noqa: ARG002
|
||||
# pylint:disable-next=unused-argument
|
||||
target_protocol: Optional[str] = None, # noqa: ARG002
|
||||
config: Optional["DictStrAny"] = None,
|
||||
remote: Optional[str] = None,
|
||||
remote_config: Optional["DictStrAny"] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""DVC + git-tracked files fs.
|
||||
|
||||
@@ -96,6 +111,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
By default, it ignores subrepos.
|
||||
repo_factory (callable): A function to initialize subrepo with.
|
||||
The default is `Repo`.
|
||||
config (dict): Repo config to be passed into `repo_factory`.
|
||||
remote (str): Remote name to be passed into `repo_factory`.
|
||||
remote_config(dict): Remote config to be passed into `repo_factory`.
|
||||
|
||||
Examples:
|
||||
- Opening a filesystem from repo in current working directory
|
||||
@@ -116,11 +134,21 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
from pygtrie import Trie
|
||||
|
||||
super().__init__()
|
||||
self._repo_stack = ExitStack()
|
||||
if repo is None:
|
||||
repo = self._make_repo(url=url, rev=rev, subrepos=subrepos, **repo_kwargs)
|
||||
url = url if url is not None else fo
|
||||
repo = self._make_repo(
|
||||
url=url,
|
||||
rev=rev,
|
||||
subrepos=subrepos,
|
||||
config=config,
|
||||
remote=remote,
|
||||
remote_config=remote_config,
|
||||
)
|
||||
assert repo is not None
|
||||
# pylint: disable=protected-access
|
||||
repo_factory = repo._fs_conf["repo_factory"]
|
||||
self._repo_stack.enter_context(repo)
|
||||
|
||||
if not repo_factory:
|
||||
from dvc.repo import Repo
|
||||
@@ -130,7 +158,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
self.repo_factory = repo_factory
|
||||
|
||||
def _getcwd():
|
||||
relparts = ()
|
||||
relparts: Tuple[str, ...] = ()
|
||||
assert repo is not None
|
||||
if repo.fs.path.isin(repo.fs.path.getcwd(), repo.root_dir):
|
||||
relparts = repo.fs.path.relparts(repo.fs.path.getcwd(), repo.root_dir)
|
||||
@@ -153,13 +181,26 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
|
||||
|
||||
@functools.cached_property
|
||||
def fsid(self) -> str:
|
||||
from fsspec.utils import tokenize
|
||||
|
||||
from dvc.scm import NoSCM
|
||||
|
||||
return "dvcfs_" + tokenize(
|
||||
self.repo.url or self.repo.root_dir,
|
||||
self.repo.get_rev() if not isinstance(self.repo.scm, NoSCM) else None,
|
||||
)
|
||||
|
||||
def _get_key(self, path: "StrPath") -> Key:
|
||||
path = os.fspath(path)
|
||||
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
|
||||
if parts == (os.curdir,):
|
||||
return ()
|
||||
return parts
|
||||
|
||||
def _get_key_from_relative(self, path) -> Key:
|
||||
path = self._strip_protocol(path)
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
return parts[1:]
|
||||
@@ -209,6 +250,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
scm=self.repo.scm,
|
||||
repo_factory=self.repo_factory,
|
||||
)
|
||||
self._repo_stack.enter_context(repo)
|
||||
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
|
||||
self._subrepos_trie[key] = repo
|
||||
|
||||
@@ -251,22 +293,19 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
try:
|
||||
return self.repo.fs.open(fs_path, mode=mode)
|
||||
except FileNotFoundError:
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
kw = {}
|
||||
if kwargs.get("cache_remote_stream", False):
|
||||
kw["cache_odb"] = repo.cache.local
|
||||
return dvc_fs.open(dvc_path, mode=mode, **kw)
|
||||
return dvc_fs.open(dvc_path, mode=mode, cache=kwargs.get("cache", False))
|
||||
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
"""Is this entry dvc-tracked?"""
|
||||
key = self._get_key_from_relative(path)
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
|
||||
try:
|
||||
return self.info(path).get("dvc_info", {}).get("isout", False)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def ls( # pylint: disable=arguments-differ # noqa: C901
|
||||
self, path, detail=True, dvc_only=False, **kwargs
|
||||
@@ -278,7 +317,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
dvc_infos = {}
|
||||
if dvc_fs:
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
with suppress(FileNotFoundError):
|
||||
with suppress(FileNotFoundError, NotADirectoryError):
|
||||
for info in dvc_fs.ls(dvc_path, detail=True):
|
||||
dvc_infos[dvc_fs.path.name(info["name"])] = info
|
||||
dvc_exists = bool(dvc_infos) or dvc_fs.exists(dvc_path)
|
||||
@@ -314,7 +353,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
continue
|
||||
|
||||
entry_path = self.path.join(path, name)
|
||||
info = _merge_info(repo, fs_infos.get(name), dvc_infos.get(name))
|
||||
info = _merge_info(
|
||||
repo, (*subkey, name), fs_infos.get(name), dvc_infos.get(name)
|
||||
)
|
||||
info["name"] = entry_path
|
||||
infos.append(info)
|
||||
paths.append(entry_path)
|
||||
@@ -340,7 +381,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
dvc_info = dvc_fs.fs.index.info(subkey)
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
dvc_info["name"] = dvc_path
|
||||
except FileNotFoundError:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
fs_info = None
|
||||
@@ -370,7 +411,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
if not dvc_info and not fs_info:
|
||||
raise FileNotFoundError
|
||||
|
||||
info = _merge_info(repo, fs_info, dvc_info)
|
||||
info = _merge_info(repo, subkey, fs_info, dvc_info)
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
@@ -387,6 +428,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
|
||||
|
||||
def close(self):
|
||||
self._repo_stack.close()
|
||||
|
||||
|
||||
class DVCFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
@@ -397,9 +441,13 @@ class DVCFileSystem(FileSystem):
|
||||
|
||||
@functools.cached_property
|
||||
# pylint: disable-next=invalid-overridden-method
|
||||
def fs(self) -> "DVCFileSystem":
|
||||
def fs(self) -> "_DVCFileSystem":
|
||||
return _DVCFileSystem(**self.fs_args)
|
||||
|
||||
@property
|
||||
def fsid(self) -> str:
|
||||
return self.fs.fsid
|
||||
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
return self.fs.isdvc(path, **kwargs)
|
||||
|
||||
@@ -420,3 +468,7 @@ class DVCFileSystem(FileSystem):
|
||||
path = os.path.relpath(path, self.repo.root_dir)
|
||||
|
||||
return as_posix(path)
|
||||
|
||||
def close(self):
|
||||
if "fs" in self.__dict__:
|
||||
self.fs.close()
|
||||
|
||||
+3
-1
@@ -148,7 +148,9 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
return bool(self.pattern_list)
|
||||
|
||||
|
||||
CheckIgnoreResult = namedtuple("CheckIgnoreResult", ["file", "match", "patterns"])
|
||||
CheckIgnoreResult = namedtuple( # noqa: PYI024
|
||||
"CheckIgnoreResult", ["file", "match", "patterns"]
|
||||
)
|
||||
|
||||
|
||||
def _no_match(path):
|
||||
|
||||
+20
-9
@@ -6,13 +6,12 @@ import platform
|
||||
|
||||
import psutil
|
||||
|
||||
from dvc import __version__
|
||||
from dvc import PKG, __version__
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.fs import Schemes, generic, get_fs_cls, get_fs_config, registry
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import SCMError
|
||||
from dvc.utils import error_link
|
||||
from dvc.utils.pkg import PKG
|
||||
|
||||
SUBPROJECTS = (
|
||||
"dvc_data",
|
||||
@@ -32,6 +31,7 @@ def get_dvc_info():
|
||||
f"Platform: Python {platform.python_version()} on {platform.platform()}",
|
||||
f"Subprojects:{_get_subprojects()}",
|
||||
f"Supports:{_get_supported_remotes()}",
|
||||
f"Config:{_get_config_dirs()}",
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -42,7 +42,7 @@ def get_dvc_info():
|
||||
# `dvc config cache.shared group`.
|
||||
if os.path.exists(repo.cache.local.path):
|
||||
info.append(f"Cache types: {_get_linktype_support_info(repo)}")
|
||||
fs_type = get_fs_type(repo.cache.local.path)
|
||||
fs_type = _get_fs_type(repo.cache.local.path)
|
||||
info.append(f"Cache directory: {fs_type}")
|
||||
else:
|
||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||
@@ -51,7 +51,7 @@ def get_dvc_info():
|
||||
info.append(f"Remotes: {_get_remotes(repo.config)}")
|
||||
|
||||
root_directory = repo.root_dir
|
||||
fs_root = get_fs_type(os.path.abspath(root_directory))
|
||||
fs_root = _get_fs_type(os.path.abspath(root_directory))
|
||||
info.append(f"Workspace directory: {fs_root}")
|
||||
info.append(f"Repo: {_get_dvc_repo_info(repo)}")
|
||||
info.append(f"Repo.site_cache_dir: {repo.site_cache_dir}")
|
||||
@@ -67,7 +67,7 @@ def _get_caches(cache):
|
||||
caches = (
|
||||
cache_type
|
||||
for cache_type, cache_instance in cache.by_scheme()
|
||||
if cache_instance and cache_type != "repo"
|
||||
if cache_instance and cache_type not in ("repo", "legacy")
|
||||
)
|
||||
|
||||
# Caches will be always non-empty including the local cache
|
||||
@@ -136,7 +136,18 @@ def _get_supported_remotes():
|
||||
return "\n\t" + ",\n\t".join(supported_remotes)
|
||||
|
||||
|
||||
def get_fs_type(path):
|
||||
def _get_config_dirs():
|
||||
from dvc.config import Config
|
||||
|
||||
dirs = [
|
||||
f"Global: {Config.get_dir('global')}",
|
||||
f"System: {Config.get_dir('system')}",
|
||||
]
|
||||
|
||||
return "\n\t" + "\n\t".join(dirs)
|
||||
|
||||
|
||||
def _get_fs_type(path):
|
||||
partition = {}
|
||||
for part in psutil.disk_partitions(all=True):
|
||||
if part.fstype:
|
||||
@@ -155,11 +166,11 @@ def get_fs_type(path):
|
||||
return ("unknown", "none")
|
||||
|
||||
|
||||
def _get_dvc_repo_info(self):
|
||||
if self.config.get("core", {}).get("no_scm", False):
|
||||
def _get_dvc_repo_info(repo):
|
||||
if repo.config.get("core", {}).get("no_scm", False):
|
||||
return "dvc (no_scm)"
|
||||
|
||||
if self.root_dir != self.scm.root_dir:
|
||||
if repo.root_dir != repo.scm.root_dir:
|
||||
return "dvc (subdir), git"
|
||||
|
||||
return "dvc, git"
|
||||
|
||||
+2
-2
@@ -56,7 +56,7 @@ def add_logging_level(level_name, level_num, method_name=None):
|
||||
|
||||
class LoggingException(Exception):
|
||||
def __init__(self, record):
|
||||
msg = f"failed to log {str(record)}"
|
||||
msg = f"failed to log {record!s}"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ class LoggerHandler(logging.StreamHandler):
|
||||
self.flush()
|
||||
except (BrokenPipeError, RecursionError):
|
||||
raise
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
except Exception: # noqa: BLE001, pylint: disable=broad-except
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
)
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.types import StrPath
|
||||
|
||||
from .backend.base import BaseMachineBackend
|
||||
|
||||
BackendCls = Type[BaseMachineBackend]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
RESERVED_NAMES = {"local", "localhost"}
|
||||
|
||||
DEFAULT_STARTUP_SCRIPT = """#!/bin/bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes python3 python3-pip
|
||||
python3 -m pip install --upgrade pip
|
||||
|
||||
pushd /etc/apt/sources.list.d
|
||||
sudo wget https://dvc.org/deb/dvc.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes dvc
|
||||
popd
|
||||
|
||||
sudo echo "OK" > /var/log/dvc-machine-init.log
|
||||
"""
|
||||
|
||||
|
||||
def validate_name(name: str):
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
name = name.lower()
|
||||
if name in RESERVED_NAMES:
|
||||
raise InvalidArgumentError(
|
||||
f"Machine name '{name}' is reserved for internal DVC use."
|
||||
)
|
||||
|
||||
|
||||
class MachineBackends(Mapping):
|
||||
try:
|
||||
from .backend.terraform import TerraformBackend
|
||||
except ImportError:
|
||||
TerraformBackend = None # type: ignore[assignment, misc]
|
||||
|
||||
DEFAULT: Dict[str, Optional["BackendCls"]] = {
|
||||
"terraform": TerraformBackend,
|
||||
}
|
||||
|
||||
def __getitem__(self, key: str) -> "BaseMachineBackend":
|
||||
"""Lazily initialize backends and cache it afterwards"""
|
||||
initialized = self.initialized.get(key)
|
||||
if not initialized:
|
||||
backend = self.backends[key]
|
||||
initialized = backend(os.path.join(self.tmp_dir, key), **self.kwargs)
|
||||
self.initialized[key] = initialized
|
||||
return initialized
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
selected: Optional[Iterable[str]],
|
||||
tmp_dir: "StrPath",
|
||||
**kwargs,
|
||||
) -> None:
|
||||
selected = selected or list(self.DEFAULT)
|
||||
self.backends: Dict[str, "BackendCls"] = {}
|
||||
for key in selected:
|
||||
cls = self.DEFAULT.get(key)
|
||||
if cls is None:
|
||||
raise DvcException(
|
||||
f"'dvc machine' backend '{key}' is missing required "
|
||||
"dependencies. Install them with:\n"
|
||||
f"\tpip install dvc[{key}]"
|
||||
)
|
||||
self.backends[key] = cls
|
||||
|
||||
self.initialized: Dict[str, "BaseMachineBackend"] = {}
|
||||
|
||||
self.tmp_dir = tmp_dir
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.backends)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.backends)
|
||||
|
||||
def close_initialized(self) -> None:
|
||||
for backend in self.initialized.values():
|
||||
backend.close()
|
||||
|
||||
|
||||
class MachineManager:
|
||||
"""Class that manages dvc cloud machines.
|
||||
|
||||
Args:
|
||||
repo (dvc.repo.Repo): repo instance that belongs to the repo that
|
||||
we are working on.
|
||||
|
||||
Raises:
|
||||
config.ConfigError: thrown when config has invalid format.
|
||||
"""
|
||||
|
||||
CLOUD_BACKENDS = {
|
||||
"aws": "terraform",
|
||||
"azure": "terraform",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self, repo: "Repo", backends: Optional[Iterable[str]] = None, **kwargs
|
||||
):
|
||||
self.repo = repo
|
||||
assert self.repo.tmp_dir
|
||||
tmp_dir = os.path.join(self.repo.tmp_dir, "machine")
|
||||
self.backends = MachineBackends(backends, tmp_dir=tmp_dir, **kwargs)
|
||||
|
||||
def get_config_and_backend(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
) -> Tuple[dict, "BaseMachineBackend"]:
|
||||
from dvc.config import NoMachineError
|
||||
|
||||
if not name:
|
||||
name = self.repo.config["core"].get("machine")
|
||||
|
||||
if name:
|
||||
config = self._get_config(name=name)
|
||||
backend = self._get_backend(config["cloud"])
|
||||
return config, backend
|
||||
|
||||
if bool(self.repo.config["machine"]):
|
||||
error_msg = (
|
||||
"no machine specified. Setup default machine with\n"
|
||||
" dvc machine default <name>\n"
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
"no machine specified. Create a default machine with\n"
|
||||
" dvc machine add -d <name> <cloud>"
|
||||
)
|
||||
|
||||
raise NoMachineError(error_msg)
|
||||
|
||||
def _get_config(self, **kwargs):
|
||||
config = self.repo.config
|
||||
name = kwargs.get("name")
|
||||
if name:
|
||||
try:
|
||||
conf = config["machine"][name.lower()]
|
||||
conf["name"] = name
|
||||
except KeyError:
|
||||
from dvc.config import MachineNotFoundError
|
||||
|
||||
raise MachineNotFoundError( # noqa: B904
|
||||
f"machine '{name}' doesn't exist"
|
||||
)
|
||||
else:
|
||||
conf = kwargs
|
||||
return conf
|
||||
|
||||
def _get_backend(self, cloud: str) -> "BaseMachineBackend":
|
||||
from dvc.config import NoMachineError
|
||||
|
||||
try:
|
||||
backend = self.CLOUD_BACKENDS[cloud]
|
||||
return self.backends[backend]
|
||||
except KeyError:
|
||||
raise NoMachineError( # noqa: B904
|
||||
f"Machine platform '{cloud}' unsupported"
|
||||
)
|
||||
|
||||
def create(self, name: Optional[str]):
|
||||
"""Create and start the specified machine instance."""
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
if "startup_script" in config:
|
||||
with open(config["startup_script"], encoding="utf-8") as fobj:
|
||||
startup_script = fobj.read()
|
||||
else:
|
||||
startup_script = DEFAULT_STARTUP_SCRIPT
|
||||
config["startup_script"] = startup_script
|
||||
config.pop("setup_script", None)
|
||||
return backend.create(**config)
|
||||
|
||||
def destroy(self, name: Optional[str]):
|
||||
"""Destroy the specified machine instance."""
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
return backend.destroy(**config)
|
||||
|
||||
def get_sshfs(self, name: Optional[str]):
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
return backend.get_sshfs(**config)
|
||||
|
||||
def run_shell(self, name: Optional[str]):
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
return backend.run_shell(**config)
|
||||
|
||||
def status(self, name: str) -> Iterator[Dict[Any, Any]]:
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
return backend.instances(**config)
|
||||
|
||||
def rename(self, name: str, new: str):
|
||||
"""move configuration to a new location if the machine is running."""
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
return backend.rename(new=new, **config)
|
||||
|
||||
def get_executor_kwargs(self, name: Optional[str]):
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
return backend.get_executor_kwargs(**config)
|
||||
|
||||
def get_setup_script(self, name: Optional[str]) -> Optional[str]:
|
||||
config, _backend = self.get_config_and_backend(name)
|
||||
return config.get("setup_script")
|
||||
@@ -1,50 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_ssh import SSHFileSystem
|
||||
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
class BaseMachineBackend(ABC):
|
||||
def __init__(self, tmp_dir: "StrPath", **kwargs):
|
||||
self.tmp_dir = tmp_dir
|
||||
|
||||
@abstractmethod
|
||||
def create(self, name: Optional[str] = None, **config):
|
||||
"""Create and start an instance of the specified machine."""
|
||||
|
||||
@abstractmethod
|
||||
def destroy(self, name: Optional[str] = None, **config):
|
||||
"""Stop and destroy all instances of the specified machine."""
|
||||
|
||||
@abstractmethod
|
||||
def instances(self, name: Optional[str] = None, **config) -> Iterator[dict]:
|
||||
"""Iterate over status of all instances of the specified machine."""
|
||||
|
||||
def close(self): # noqa: B027
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def run_shell(self, name: Optional[str] = None, **config):
|
||||
"""Spawn an interactive SSH shell for the specified machine."""
|
||||
|
||||
@abstractmethod
|
||||
def get_executor_kwargs(self, name: str, **config) -> dict:
|
||||
"""Return SSHExecutor kwargs which can be used for DVC
|
||||
experiment/pipeline execution on the specified machine.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@contextmanager
|
||||
def get_sshfs(
|
||||
self, name: Optional[str] = None, **config
|
||||
) -> Iterator["SSHFileSystem"]:
|
||||
"""Return an sshfs instance for the default directory on the
|
||||
specified machine."""
|
||||
|
||||
@abstractmethod
|
||||
def rename(self, name: str, new: str, **config):
|
||||
"""Rename a machine instance."""
|
||||
@@ -1,99 +0,0 @@
|
||||
import os
|
||||
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 .base import BaseMachineBackend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _sshfs(resource: dict):
|
||||
from tpi import TerraformProviderIterative
|
||||
|
||||
with TerraformProviderIterative.pemfile(resource) as pem:
|
||||
fs = SSHFileSystem(
|
||||
host=resource["instance_ip"],
|
||||
user="ubuntu",
|
||||
keyfile=pem,
|
||||
)
|
||||
yield fs
|
||||
|
||||
|
||||
class TerraformBackend(BaseMachineBackend):
|
||||
def __init__(self, tmp_dir: "StrPath", **kwargs):
|
||||
super().__init__(tmp_dir, **kwargs)
|
||||
os.makedirs(tmp_dir, exist_ok=True)
|
||||
|
||||
@contextmanager
|
||||
def make_tpi(self, name: str):
|
||||
from tpi import TPIError
|
||||
from tpi.terraform import TerraformBackend as TPIBackend
|
||||
|
||||
try:
|
||||
working_dir = os.path.join(self.tmp_dir, name)
|
||||
os.makedirs(working_dir, exist_ok=True)
|
||||
yield TPIBackend(working_dir=working_dir)
|
||||
except TPIError as exc:
|
||||
raise DvcException("TPI operation failed") from exc
|
||||
|
||||
def _tpi_func(self, fname, name: Optional[str] = None, **config):
|
||||
from tpi import TPIError
|
||||
|
||||
assert name
|
||||
with self.make_tpi(name) as tpi:
|
||||
func = getattr(tpi, fname)
|
||||
try:
|
||||
return func(name=name, **config)
|
||||
except TPIError as exc:
|
||||
raise DvcException(f"TPI {fname} failed") from exc
|
||||
|
||||
create = partialmethod(_tpi_func, "create") # type: ignore[assignment]
|
||||
destroy = partialmethod(_tpi_func, "destroy") # type: ignore[assignment]
|
||||
instances = partialmethod(_tpi_func, "instances") # type: ignore[assignment]
|
||||
run_shell = partialmethod(_tpi_func, "run_shell") # type: ignore[assignment]
|
||||
|
||||
def get_executor_kwargs(self, name: str, **config) -> dict:
|
||||
with self.make_tpi(name) as tpi:
|
||||
resource = tpi.default_resource(name)
|
||||
return {
|
||||
"host": resource["instance_ip"],
|
||||
"port": DEFAULT_PORT,
|
||||
"username": "ubuntu",
|
||||
"fs_factory": partial(_sshfs, dict(resource)),
|
||||
}
|
||||
|
||||
@contextmanager
|
||||
def get_sshfs( # pylint: disable=unused-argument
|
||||
self, name: Optional[str] = None, **config
|
||||
) -> Iterator["SSHFileSystem"]:
|
||||
assert name
|
||||
with self.make_tpi(name) as tpi:
|
||||
resource = tpi.default_resource(name)
|
||||
with _sshfs(resource) as fs:
|
||||
yield fs
|
||||
|
||||
def rename(self, name: str, new: str, **config):
|
||||
"""rename a dvc machine instance to a new name"""
|
||||
import shutil
|
||||
|
||||
mtype = "iterative_machine"
|
||||
|
||||
new_dir = os.path.join(self.tmp_dir, new)
|
||||
old_dir = os.path.join(self.tmp_dir, name)
|
||||
if os.path.exists(new_dir):
|
||||
raise DvcException(f"rename failed: path {new_dir} already exists")
|
||||
|
||||
if not os.path.exists(old_dir):
|
||||
return
|
||||
|
||||
with self.make_tpi(name) as tpi:
|
||||
tpi.state_mv(f"{mtype}.{name}", f"{mtype}.{new}", **config)
|
||||
|
||||
shutil.move(old_dir, new_dir)
|
||||
@@ -1,2 +0,0 @@
|
||||
# TODO: remove in 3.0, kept for backward compatibility
|
||||
from .cli import main # noqa: F401, pylint: disable=unused-import
|
||||
+392
-125
@@ -1,10 +1,11 @@
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import collecting, first, project
|
||||
@@ -18,33 +19,33 @@ from dvc.exceptions import (
|
||||
ConfirmRemoveError,
|
||||
DvcException,
|
||||
MergeError,
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc.utils import format_link
|
||||
from dvc.utils.objects import cached_property
|
||||
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.db import HashFileDB, add_update_tree
|
||||
from dvc_data.hashfile.hash import DEFAULT_ALGORITHM
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.istextfile import istextfile
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.hashfile.transfer import transfer as otransfer
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
from dvc_data.hashfile.tree import Tree, du
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
|
||||
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
|
||||
from .fs.callbacks import DEFAULT_CALLBACK
|
||||
from .fs.callbacks import DEFAULT_CALLBACK, Callback, TqdmCallback
|
||||
from .utils import relpath
|
||||
from .utils.fs import path_isin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_data.index import DataIndexKey
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
from .fs.callbacks import Callback
|
||||
from .ignore import DvcIgnoreFilter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -64,7 +65,7 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
|
||||
|
||||
# NOTE: currently there are only 3 possible checksum names:
|
||||
#
|
||||
# 1) md5 (LOCAL, SSH);
|
||||
# 1) md5 (LOCAL, SSH) (actually DVC 2.x md5-dos2unix)
|
||||
# 2) etag (S3, GS, OSS, AZURE, HTTP);
|
||||
# 3) checksum (HDFS);
|
||||
#
|
||||
@@ -73,7 +74,7 @@ CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
|
||||
HDFS_PARAM_CHECKSUM = "checksum"
|
||||
S3_PARAM_CHECKSUM = "etag"
|
||||
CHECKSUMS_SCHEMA = {
|
||||
LocalFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
"md5": CHECKSUM_SCHEMA, # DVC 2.x md5-dos2unix
|
||||
HDFS_PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3_PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
|
||||
}
|
||||
@@ -91,11 +92,12 @@ def loadd_from(stage, d_list):
|
||||
metric = d.pop(Output.PARAM_METRIC, False)
|
||||
plot = d.pop(Output.PARAM_PLOT, False)
|
||||
persist = d.pop(Output.PARAM_PERSIST, False)
|
||||
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
|
||||
remote = d.pop(Output.PARAM_REMOTE, None)
|
||||
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
|
||||
files = d.pop(Output.PARAM_FILES, None)
|
||||
push = d.pop(Output.PARAM_PUSH, True)
|
||||
hash_name = d.pop(Output.PARAM_HASH, None)
|
||||
fs_config = d.pop(Output.PARAM_FS_CONFIG, None)
|
||||
ret.append(
|
||||
_get(
|
||||
stage,
|
||||
@@ -105,11 +107,12 @@ def loadd_from(stage, d_list):
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
remote=remote,
|
||||
**annot,
|
||||
files=files,
|
||||
push=push,
|
||||
hash_name=hash_name,
|
||||
fs_config=fs_config,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
@@ -122,7 +125,6 @@ def loads_from(
|
||||
metric=False,
|
||||
plot=False,
|
||||
persist=False,
|
||||
checkpoint=False,
|
||||
remote=None,
|
||||
push=True,
|
||||
):
|
||||
@@ -135,7 +137,6 @@ def loads_from(
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
checkpoint=checkpoint,
|
||||
remote=remote,
|
||||
push=push,
|
||||
)
|
||||
@@ -191,7 +192,6 @@ def load_from_pipeline(stage, data, typ="outs"):
|
||||
[
|
||||
Output.PARAM_CACHE,
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
Output.PARAM_REMOTE,
|
||||
Output.PARAM_PUSH,
|
||||
*ANNOTATION_FIELDS,
|
||||
@@ -232,17 +232,29 @@ def merge_file_meta_from_cloud(entry: Dict) -> Dict:
|
||||
return entry
|
||||
|
||||
|
||||
def _serialize_tree_obj_to_files(obj: "Tree") -> List[Dict[str, Any]]:
|
||||
def _serialize_tree_obj_to_files(obj: Tree) -> List[Dict[str, Any]]:
|
||||
key = obj.PARAM_RELPATH
|
||||
return sorted(
|
||||
(
|
||||
{key: posixpath.sep.join(parts), **hi.to_dict(), **meta.to_dict()}
|
||||
{
|
||||
key: posixpath.sep.join(parts),
|
||||
**_serialize_hi_to_dict(hi),
|
||||
**meta.to_dict(),
|
||||
}
|
||||
for parts, meta, hi in obj
|
||||
),
|
||||
key=itemgetter(key),
|
||||
)
|
||||
|
||||
|
||||
def _serialize_hi_to_dict(hash_info: Optional[HashInfo]) -> Dict[str, Any]:
|
||||
if hash_info:
|
||||
if hash_info.name == "md5-dos2unix":
|
||||
return {"md5": hash_info.value}
|
||||
return hash_info.to_dict()
|
||||
return {}
|
||||
|
||||
|
||||
class OutputDoesNotExistError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = f"output '{path}' does not exist"
|
||||
@@ -276,12 +288,16 @@ class OutputIsIgnoredError(DvcException):
|
||||
super().__init__(f"Path '{match.file}' is ignored by\n{lines}")
|
||||
|
||||
|
||||
class CheckoutCallback(TqdmCallback):
|
||||
# disable branching for checkouts
|
||||
branch = Callback.branch # type: ignore[assignment]
|
||||
|
||||
|
||||
class Output:
|
||||
IS_DEPENDENCY = False
|
||||
|
||||
PARAM_PATH = "path"
|
||||
PARAM_CACHE = "cache"
|
||||
PARAM_CHECKPOINT = "checkpoint"
|
||||
PARAM_FILES = "files"
|
||||
PARAM_METRIC = "metric"
|
||||
PARAM_METRIC_TYPE = "type"
|
||||
@@ -298,15 +314,8 @@ class Output:
|
||||
PARAM_REMOTE = "remote"
|
||||
PARAM_PUSH = "push"
|
||||
PARAM_CLOUD = "cloud"
|
||||
|
||||
METRIC_SCHEMA = Any(
|
||||
None,
|
||||
bool,
|
||||
{
|
||||
PARAM_METRIC_TYPE: Any(str, None),
|
||||
PARAM_METRIC_XPATH: Any(str, None),
|
||||
},
|
||||
)
|
||||
PARAM_HASH = "hash"
|
||||
PARAM_FS_CONFIG = "fs_config"
|
||||
|
||||
DoesNotExistError: Type[DvcException] = OutputDoesNotExistError
|
||||
IsNotFileOrDirError: Type[DvcException] = OutputIsNotFileOrDirError
|
||||
@@ -322,7 +331,6 @@ class Output:
|
||||
metric=False,
|
||||
plot=False,
|
||||
persist=False,
|
||||
checkpoint=False,
|
||||
desc=None,
|
||||
type=None, # noqa: A002, pylint: disable=redefined-builtin
|
||||
labels=None,
|
||||
@@ -332,6 +340,7 @@ class Output:
|
||||
fs_config=None,
|
||||
files: Optional[List[Dict[str, Any]]] = None,
|
||||
push: bool = True,
|
||||
hash_name: Optional[str] = DEFAULT_ALGORITHM,
|
||||
):
|
||||
self.annot = Annotation(
|
||||
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
||||
@@ -345,10 +354,15 @@ class Output:
|
||||
if meta.version_id or files:
|
||||
fs_kwargs["version_aware"] = True
|
||||
|
||||
self.def_fs_config = fs_config
|
||||
if fs_config is not None:
|
||||
fs_kwargs.update(**fs_config)
|
||||
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(self.repo, url=path, **fs_kwargs)
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(
|
||||
self.repo.config if self.repo else {},
|
||||
url=path,
|
||||
**fs_kwargs,
|
||||
)
|
||||
self.fs = fs_cls(**fs_config)
|
||||
|
||||
if (
|
||||
@@ -389,7 +403,6 @@ class Output:
|
||||
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.can_push = push
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
@@ -404,21 +417,32 @@ class Output:
|
||||
)
|
||||
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),
|
||||
)
|
||||
self.hash_name, self.hash_info = self._compute_hash_info_from_meta(hash_name)
|
||||
self._compute_meta_hash_info_from_files()
|
||||
|
||||
def _compute_hash_info_from_meta(
|
||||
self, hash_name: Optional[str]
|
||||
) -> Tuple[str, HashInfo]:
|
||||
if self.is_in_repo:
|
||||
if hash_name is None:
|
||||
# Legacy 2.x output, use "md5-dos2unix" but read "md5" from
|
||||
# file meta
|
||||
hash_name = "md5-dos2unix"
|
||||
meta_name = "md5"
|
||||
else:
|
||||
meta_name = hash_name
|
||||
else:
|
||||
hash_name = meta_name = self.fs.PARAM_CHECKSUM
|
||||
assert hash_name
|
||||
|
||||
hash_info = HashInfo(
|
||||
name=hash_name,
|
||||
value=getattr(self.meta, meta_name, None),
|
||||
)
|
||||
return hash_name, hash_info
|
||||
|
||||
def _compute_meta_hash_info_from_files(self) -> None:
|
||||
if self.files:
|
||||
from dvc_data.hashfile.db import HashFileDB
|
||||
|
||||
tree = Tree.from_list(self.files, hash_name=self.hash_name)
|
||||
tree.digest(with_meta=True)
|
||||
self.odb = HashFileDB(tree.fs, tree.path + ".odb")
|
||||
@@ -431,7 +455,7 @@ class Output:
|
||||
self.meta.remote = first(f.get("remote") for f in self.files)
|
||||
elif self.meta.nfiles or self.hash_info and self.hash_info.isdir:
|
||||
self.meta.isdir = True
|
||||
if not self.hash_info and self.hash_name != "md5":
|
||||
if not self.hash_info and self.hash_name not in ("md5", "md5-dos2unix"):
|
||||
md5 = getattr(self.meta, "md5", None)
|
||||
if md5:
|
||||
self.hash_info = HashInfo("md5", md5)
|
||||
@@ -455,9 +479,7 @@ class Output:
|
||||
return fs.path.abspath(fs.path.normpath(fs_path))
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_name}: '{def_path}'".format(
|
||||
class_name=type(self).__name__, def_path=self.def_path
|
||||
)
|
||||
return f"{type(self).__name__}: {self.def_path!r}"
|
||||
|
||||
def __str__(self):
|
||||
if self.fs.protocol != "local":
|
||||
@@ -470,6 +492,9 @@ class Output:
|
||||
):
|
||||
return str(self.def_path)
|
||||
|
||||
if not self.fs.path.isin(self.fs_path, self.repo.root_dir):
|
||||
return self.fs_path
|
||||
|
||||
cur_dir = self.fs.path.getcwd()
|
||||
if self.fs.path.isin(cur_dir, self.repo.root_dir):
|
||||
return self.fs.path.relpath(self.fs_path, cur_dir)
|
||||
@@ -495,7 +520,7 @@ class Output:
|
||||
return False
|
||||
|
||||
return self.repo and self.fs.path.isin(
|
||||
self.fs.path.realpath(self.fs_path),
|
||||
self.fs_path,
|
||||
self.repo.root_dir,
|
||||
)
|
||||
|
||||
@@ -508,11 +533,19 @@ class Output:
|
||||
|
||||
@property
|
||||
def cache(self):
|
||||
odb_name = "repo" if self.is_in_repo else self.protocol
|
||||
odb = getattr(self.repo.cache, odb_name)
|
||||
if self.use_cache and odb is None:
|
||||
raise RemoteCacheRequiredError(self.fs.protocol, self.fs_path)
|
||||
return odb
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
assert self.is_in_repo
|
||||
odb_name = "legacy" if self.hash_name in LEGACY_HASH_NAMES else "repo"
|
||||
return getattr(self.repo.cache, odb_name)
|
||||
|
||||
@property
|
||||
def local_cache(self):
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
if self.hash_name in LEGACY_HASH_NAMES:
|
||||
return self.repo.cache.legacy
|
||||
return self.repo.cache.local
|
||||
|
||||
@property
|
||||
def cache_path(self):
|
||||
@@ -524,12 +557,24 @@ class Output:
|
||||
_, hash_info = self._get_hash_meta()
|
||||
return hash_info
|
||||
|
||||
def _build(
|
||||
self, *args, no_progress_bar=False, **kwargs
|
||||
) -> Tuple["HashFileDB", "Meta", "HashFile"]:
|
||||
from dvc.ui import ui
|
||||
|
||||
with ui.progress(
|
||||
unit="file",
|
||||
desc=f"Collecting files and computing hashes in {self}",
|
||||
disable=no_progress_bar,
|
||||
) as pb:
|
||||
return build(*args, callback=pb.as_callback(), **kwargs)
|
||||
|
||||
def _get_hash_meta(self):
|
||||
if self.use_cache:
|
||||
odb = self.cache
|
||||
else:
|
||||
odb = self.repo.cache.local
|
||||
_, meta, obj = build(
|
||||
odb = self.local_cache
|
||||
_, meta, obj = self._build(
|
||||
odb,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
@@ -655,6 +700,14 @@ class Output:
|
||||
# pylint: enable=no-member
|
||||
|
||||
def save(self) -> None:
|
||||
if self.use_cache and not self.is_in_repo:
|
||||
raise DvcException(
|
||||
f"Saving cached external output {self!s} is not supported "
|
||||
"since DVC 3.0. See "
|
||||
f"{format_link('https://dvc.org/doc/user-guide/upgrade')} "
|
||||
"for more info."
|
||||
)
|
||||
|
||||
if not self.exists:
|
||||
raise self.DoesNotExistError(self)
|
||||
|
||||
@@ -669,8 +722,9 @@ class Output:
|
||||
if self.metric:
|
||||
self.verify_metric()
|
||||
|
||||
self._update_legacy_hash_name()
|
||||
if self.use_cache:
|
||||
_, self.meta, self.obj = build(
|
||||
_, self.meta, self.obj = self._build(
|
||||
self.cache,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
@@ -678,8 +732,8 @@ class Output:
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
else:
|
||||
_, self.meta, self.obj = build(
|
||||
self.repo.cache.local,
|
||||
_, self.meta, self.obj = self._build(
|
||||
self.local_cache,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
@@ -692,6 +746,10 @@ class Output:
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.files = None
|
||||
|
||||
def _update_legacy_hash_name(self):
|
||||
if self.hash_name == "md5-dos2unix" and self.changed_checksum():
|
||||
self.hash_name = "md5"
|
||||
|
||||
def set_exec(self) -> None:
|
||||
if self.isfile() and self.meta.isexec:
|
||||
self.cache.set_exec(self.fs_path)
|
||||
@@ -726,35 +784,43 @@ class Output:
|
||||
if granular:
|
||||
obj = self._commit_granular_dir(filter_info, hardlink)
|
||||
else:
|
||||
staging, _, obj = build(
|
||||
staging, _, obj = self._build(
|
||||
self.cache,
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
otransfer(
|
||||
staging,
|
||||
self.cache,
|
||||
{obj.hash_info},
|
||||
shallow=False,
|
||||
hardlink=hardlink,
|
||||
)
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Committing {self} to cache",
|
||||
unit="file",
|
||||
) as cb:
|
||||
otransfer(
|
||||
staging,
|
||||
self.cache,
|
||||
{obj.hash_info},
|
||||
shallow=False,
|
||||
hardlink=hardlink,
|
||||
callback=cb,
|
||||
)
|
||||
if relink:
|
||||
self._checkout(
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
)
|
||||
rel = self.fs.path.relpath(filter_info or self.fs_path)
|
||||
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as cb:
|
||||
self._checkout(
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=cb,
|
||||
)
|
||||
self.set_exec()
|
||||
|
||||
def _commit_granular_dir(self, filter_info, hardlink) -> Optional["HashFile"]:
|
||||
prefix = self.fs.path.parts(self.fs.path.relpath(filter_info, self.fs_path))
|
||||
staging, _, obj = build(
|
||||
staging, _, obj = self._build(
|
||||
self.cache,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
@@ -765,16 +831,23 @@ class Output:
|
||||
save_obj = obj.filter(prefix)
|
||||
assert isinstance(save_obj, Tree)
|
||||
checkout_obj = save_obj.get_obj(self.cache, prefix)
|
||||
otransfer(
|
||||
staging,
|
||||
self.cache,
|
||||
{save_obj.hash_info} | {oid for _, _, oid in save_obj},
|
||||
shallow=True,
|
||||
hardlink=hardlink,
|
||||
)
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Committing {self} to cache",
|
||||
unit="file",
|
||||
) as cb:
|
||||
otransfer(
|
||||
staging,
|
||||
self.cache,
|
||||
{save_obj.hash_info} | {oid for _, _, oid in save_obj},
|
||||
shallow=True,
|
||||
hardlink=hardlink,
|
||||
callback=cb,
|
||||
)
|
||||
return checkout_obj
|
||||
|
||||
def dumpd(self, **kwargs): # noqa: C901, PLR0912
|
||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||
|
||||
ret: Dict[str, Any] = {}
|
||||
with_files = (
|
||||
(not self.IS_DEPENDENCY or self.stage.is_import)
|
||||
@@ -785,7 +858,12 @@ class Output:
|
||||
if not with_files:
|
||||
meta_d = self.meta.to_dict()
|
||||
meta_d.pop("isdir", None)
|
||||
ret.update(self.hash_info.to_dict())
|
||||
if self.hash_name in LEGACY_HASH_NAMES:
|
||||
# 2.x checksums get serialized with file meta
|
||||
name = "md5" if self.hash_name == "md5-dos2unix" else self.hash_name
|
||||
ret.update({name: self.hash_info.value})
|
||||
else:
|
||||
ret.update(self.hash_info.to_dict())
|
||||
ret.update(split_file_meta_from_cloud(meta_d))
|
||||
|
||||
if self.is_in_repo:
|
||||
@@ -793,8 +871,14 @@ class Output:
|
||||
else:
|
||||
path = self.def_path
|
||||
|
||||
if self.hash_name not in LEGACY_HASH_NAMES:
|
||||
ret[self.PARAM_HASH] = "md5"
|
||||
|
||||
ret[self.PARAM_PATH] = path
|
||||
|
||||
if self.def_fs_config:
|
||||
ret[self.PARAM_FS_CONFIG] = self.def_fs_config
|
||||
|
||||
if not self.IS_DEPENDENCY:
|
||||
ret.update(self.annot.to_dict())
|
||||
if not self.use_cache:
|
||||
@@ -816,9 +900,6 @@ class Output:
|
||||
if self.persist:
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
|
||||
if self.checkpoint:
|
||||
ret[self.PARAM_CHECKPOINT] = self.checkpoint
|
||||
|
||||
if self.remote:
|
||||
ret[self.PARAM_REMOTE] = self.remote
|
||||
|
||||
@@ -888,12 +969,18 @@ class Output:
|
||||
relink: bool = False,
|
||||
filter_info: Optional[str] = None,
|
||||
allow_missing: bool = False,
|
||||
checkpoint_reset: bool = False,
|
||||
**kwargs,
|
||||
) -> Optional[Tuple[bool, Optional[bool]]]:
|
||||
# callback passed act as a aggregate callback.
|
||||
# do not let checkout to call set_size and change progressbar.
|
||||
class CallbackProxy(Callback):
|
||||
def relative_update(self, inc: int = 1) -> None:
|
||||
progress_callback.relative_update(inc)
|
||||
return super().relative_update(inc)
|
||||
|
||||
callback = CallbackProxy()
|
||||
if not self.use_cache:
|
||||
if progress_callback != DEFAULT_CALLBACK:
|
||||
progress_callback.relative_update(self.get_files_number(filter_info))
|
||||
callback.relative_update(self.get_files_number(filter_info))
|
||||
return None
|
||||
|
||||
obj = self.get_obj(filter_info=filter_info)
|
||||
@@ -901,11 +988,6 @@ class Output:
|
||||
# backward compatibility
|
||||
return None
|
||||
|
||||
if self.checkpoint and checkpoint_reset:
|
||||
if self.exists:
|
||||
self.remove()
|
||||
return None
|
||||
|
||||
added = not self.exists
|
||||
|
||||
try:
|
||||
@@ -915,21 +997,24 @@ class Output:
|
||||
obj,
|
||||
self.cache,
|
||||
force=force,
|
||||
progress_callback=progress_callback,
|
||||
progress_callback=callback,
|
||||
relink=relink,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
**kwargs,
|
||||
)
|
||||
except CheckoutError:
|
||||
if allow_missing or self.checkpoint:
|
||||
if allow_missing:
|
||||
return None
|
||||
raise
|
||||
self.set_exec()
|
||||
return added, False if added else modified
|
||||
|
||||
def remove(self, ignore_remove=False):
|
||||
self.fs.remove(self.fs_path, recursive=True)
|
||||
try:
|
||||
self.fs.remove(self.fs_path, recursive=True)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if self.protocol != Schemes.LOCAL:
|
||||
return
|
||||
|
||||
@@ -956,7 +1041,9 @@ class Output:
|
||||
if odb is None:
|
||||
odb = self.cache
|
||||
|
||||
cls, config, from_info = get_cloud_fs(self.repo, url=source)
|
||||
cls, config, from_info = get_cloud_fs(
|
||||
self.repo.config if self.repo else {}, url=source
|
||||
)
|
||||
from_fs = cls(**config)
|
||||
|
||||
# When running import-url --to-remote / add --to-remote/-o ... we
|
||||
@@ -970,22 +1057,27 @@ 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 = build(
|
||||
staging, self.meta, obj = self._build(
|
||||
odb,
|
||||
from_info,
|
||||
from_fs,
|
||||
"md5",
|
||||
DEFAULT_ALGORITHM,
|
||||
upload=upload,
|
||||
no_progress_bar=no_progress_bar,
|
||||
)
|
||||
otransfer(
|
||||
staging,
|
||||
odb,
|
||||
{obj.hash_info},
|
||||
jobs=jobs,
|
||||
hardlink=False,
|
||||
shallow=False,
|
||||
)
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Transferring to {odb.fs.unstrip_protocol(odb.path)}",
|
||||
unit="file",
|
||||
) as cb:
|
||||
otransfer(
|
||||
staging,
|
||||
odb,
|
||||
{obj.hash_info},
|
||||
jobs=jobs,
|
||||
hardlink=False,
|
||||
shallow=False,
|
||||
callback=cb,
|
||||
)
|
||||
|
||||
self.hash_info = obj.hash_info
|
||||
self.files = None
|
||||
@@ -1005,10 +1097,10 @@ class Output:
|
||||
return len(obj) if obj else 0
|
||||
|
||||
def unprotect(self):
|
||||
if self.exists:
|
||||
if self.exists and self.use_cache:
|
||||
self.cache.unprotect(self.fs_path)
|
||||
|
||||
def get_dir_cache(self, **kwargs):
|
||||
def get_dir_cache(self, **kwargs) -> Optional["Tree"]:
|
||||
if not self.is_dir_checksum:
|
||||
raise DvcException("cannot get dir cache for file checksum")
|
||||
|
||||
@@ -1022,14 +1114,17 @@ class Output:
|
||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||
|
||||
if self.obj:
|
||||
assert isinstance(self.obj, Tree)
|
||||
return self.obj
|
||||
|
||||
try:
|
||||
self.obj = oload(self.cache, self.hash_info)
|
||||
obj = oload(self.cache, self.hash_info)
|
||||
assert isinstance(obj, Tree)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
self.obj = None
|
||||
obj = None
|
||||
|
||||
return self.obj
|
||||
self.obj = obj
|
||||
return obj
|
||||
|
||||
def _collect_used_dir_cache(
|
||||
self, remote=None, force=False, jobs=None, filter_info=None
|
||||
@@ -1054,7 +1149,7 @@ class Output:
|
||||
if not force and not prompt.confirm(msg.format(self.fs_path)):
|
||||
raise CollectCacheError( # noqa: B904
|
||||
"unable to fully collect used cache"
|
||||
" without cache for directory '{}'".format(self)
|
||||
f" without cache for directory '{self}'"
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -1083,19 +1178,15 @@ class Output:
|
||||
|
||||
if not self.hash_info:
|
||||
msg = (
|
||||
"Output '{}'({}) is missing version info. "
|
||||
f"Output '{self}'({self.stage}) is missing version info. "
|
||||
"Cache for it will not be collected. "
|
||||
"Use `dvc repro` to get your pipeline up to date.".format(
|
||||
self, self.stage
|
||||
)
|
||||
"Use `dvc repro` to get your pipeline up to date."
|
||||
)
|
||||
if self.exists:
|
||||
msg += (
|
||||
"\n"
|
||||
"You can also use `dvc commit {stage.addressing}` "
|
||||
"to associate existing '{out}' with {stage}.".format(
|
||||
out=self, stage=self.stage
|
||||
)
|
||||
f"You can also use `dvc commit {self.stage.addressing}` "
|
||||
f"to associate existing '{self}' with {self.stage}."
|
||||
)
|
||||
logger.warning(msg)
|
||||
return {}
|
||||
@@ -1112,7 +1203,9 @@ class Output:
|
||||
return {}
|
||||
|
||||
if self.remote:
|
||||
remote = self.repo.cloud.get_remote_odb(name=self.remote)
|
||||
remote = self.repo.cloud.get_remote_odb(
|
||||
name=self.remote, hash_name=self.hash_name
|
||||
)
|
||||
else:
|
||||
remote = None
|
||||
|
||||
@@ -1151,13 +1244,14 @@ class Output:
|
||||
self.hash_name,
|
||||
Meta.PARAM_SIZE,
|
||||
Meta.PARAM_NFILES,
|
||||
Output.PARAM_HASH,
|
||||
]
|
||||
|
||||
for opt in ignored:
|
||||
my.pop(opt, None)
|
||||
other.pop(opt, None)
|
||||
|
||||
if my != other:
|
||||
if my != other or self.hash_name != out.hash_name:
|
||||
raise MergeError("unable to auto-merge outputs with different options")
|
||||
|
||||
if not out.is_dir_checksum:
|
||||
@@ -1165,7 +1259,7 @@ class Output:
|
||||
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
from dvc_data.hashfile.tree import MergeError as TreeMergeError
|
||||
from dvc_data.hashfile.tree import du, merge
|
||||
from dvc_data.hashfile.tree import merge
|
||||
|
||||
assert other
|
||||
|
||||
@@ -1198,6 +1292,180 @@ class Output:
|
||||
nfiles=len(merged),
|
||||
)
|
||||
|
||||
def unstage(self, path: str) -> Tuple["Meta", "Tree"]:
|
||||
from pygtrie import Trie
|
||||
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
assert isinstance(self.fs.path, Path)
|
||||
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
|
||||
|
||||
if self.hash_info:
|
||||
tree = self.get_dir_cache()
|
||||
if tree is None:
|
||||
raise DvcException(f"could not read {self.hash_info.value!r}")
|
||||
else:
|
||||
tree = Tree()
|
||||
|
||||
trie = tree.as_trie()
|
||||
assert isinstance(trie, Trie)
|
||||
|
||||
try:
|
||||
del trie[rel_key:] # type: ignore[misc]
|
||||
except KeyError:
|
||||
raise FileNotFoundError( # noqa: B904
|
||||
errno.ENOENT,
|
||||
os.strerror(errno.ENOENT),
|
||||
path,
|
||||
)
|
||||
|
||||
new = tree.from_trie(trie)
|
||||
new.digest()
|
||||
return Meta(nfiles=len(new), isdir=True), new
|
||||
|
||||
def apply(
|
||||
self,
|
||||
path: str,
|
||||
obj: Union["Tree", "HashFile"],
|
||||
meta: "Meta",
|
||||
) -> Tuple["Meta", "Tree"]:
|
||||
from pygtrie import Trie
|
||||
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
assert isinstance(self.fs.path, Path)
|
||||
append_only = True
|
||||
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
|
||||
|
||||
if self.hash_info:
|
||||
tree = self.get_dir_cache()
|
||||
if tree is None:
|
||||
raise DvcException(f"could not read {self.hash_info.value!r}")
|
||||
else:
|
||||
tree = Tree()
|
||||
|
||||
trie = tree.as_trie()
|
||||
assert isinstance(trie, Trie)
|
||||
|
||||
try:
|
||||
del trie[rel_key:] # type: ignore[misc]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
append_only = False
|
||||
|
||||
items = {}
|
||||
if isinstance(obj, Tree):
|
||||
items = {(*rel_key, *key): (m, o) for key, m, o in obj}
|
||||
else:
|
||||
items = {rel_key: (meta, obj.hash_info)}
|
||||
trie.update(items)
|
||||
|
||||
new = Tree.from_trie(trie)
|
||||
new.digest()
|
||||
|
||||
size = self.meta.size if self.meta and self.meta.size else None
|
||||
if append_only and size and meta.size is not None:
|
||||
# if files were only appended, we can sum to the existing size
|
||||
size += meta.size
|
||||
elif self.hash_info and self.hash_info == new.hash_info:
|
||||
# if hashes are same, sizes must have been the same
|
||||
size = self.meta.size
|
||||
else:
|
||||
size = None
|
||||
|
||||
meta = Meta(nfiles=len(new), size=size, isdir=True)
|
||||
return meta, new
|
||||
|
||||
def add( # noqa: C901
|
||||
self, path: Optional[str] = None, no_commit: bool = False, relink: bool = True
|
||||
) -> Optional["HashFile"]:
|
||||
path = path or self.fs_path
|
||||
if self.hash_info and not self.is_dir_checksum and self.fs_path != path:
|
||||
raise DvcException(
|
||||
f"Cannot modify '{self}' which is being tracked as a file"
|
||||
)
|
||||
|
||||
assert self.repo
|
||||
self._update_legacy_hash_name()
|
||||
cache = self.cache if self.use_cache else self.local_cache
|
||||
assert isinstance(cache, HashFileDB)
|
||||
|
||||
new: "HashFile"
|
||||
try:
|
||||
assert self.hash_name
|
||||
staging, meta, obj = self._build(
|
||||
cache,
|
||||
path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=not self.use_cache,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
if self.fs_path == path:
|
||||
raise self.DoesNotExistError(self) from exc
|
||||
if not self.is_dir_checksum:
|
||||
raise
|
||||
|
||||
meta, new = self.unstage(path)
|
||||
staging, obj = None, None
|
||||
else:
|
||||
assert obj
|
||||
assert staging
|
||||
if self.fs_path != path:
|
||||
meta, new = self.apply(path, obj, meta)
|
||||
add_update_tree(staging, new)
|
||||
else:
|
||||
new = obj
|
||||
|
||||
self.obj = new
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.meta = meta
|
||||
self.files = None
|
||||
self.ignore()
|
||||
|
||||
if no_commit or not self.use_cache:
|
||||
return obj
|
||||
|
||||
if isinstance(new, Tree):
|
||||
add_update_tree(cache, new)
|
||||
|
||||
if not obj:
|
||||
return obj
|
||||
|
||||
assert staging
|
||||
assert obj.hash_info
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Adding {self} to cache",
|
||||
unit="file",
|
||||
) as cb:
|
||||
otransfer(
|
||||
staging,
|
||||
self.cache,
|
||||
{obj.hash_info},
|
||||
hardlink=relink,
|
||||
shallow=False,
|
||||
callback=cb,
|
||||
)
|
||||
|
||||
if relink:
|
||||
with CheckoutCallback(
|
||||
desc=f"Checking out {path}", unit="files"
|
||||
) as callback:
|
||||
self._checkout(
|
||||
path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=callback,
|
||||
)
|
||||
self.set_exec()
|
||||
return obj
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
return self.fs_path
|
||||
@@ -1257,10 +1525,9 @@ ARTIFACT_SCHEMA = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
**META_SCHEMA,
|
||||
Required(Output.PARAM_PATH): str,
|
||||
Output.PARAM_PLOT: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
Output.PARAM_CLOUD: CLOUD_SCHEMA,
|
||||
Output.PARAM_HASH: str,
|
||||
}
|
||||
|
||||
DIR_FILES_SCHEMA: Dict[str, Any] = {
|
||||
@@ -1274,8 +1541,8 @@ SCHEMA = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**ANNOTATION_SCHEMA,
|
||||
Output.PARAM_CACHE: bool,
|
||||
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
|
||||
Output.PARAM_REMOTE: str,
|
||||
Output.PARAM_PUSH: bool,
|
||||
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
|
||||
Output.PARAM_FS_CONFIG: dict,
|
||||
}
|
||||
|
||||
+130
-25
@@ -2,6 +2,7 @@ import logging
|
||||
import os
|
||||
from collections.abc import Mapping, Sequence
|
||||
from copy import deepcopy
|
||||
from itertools import product
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -51,6 +52,7 @@ VARS_KWD = "vars"
|
||||
WDIR_KWD = "wdir"
|
||||
PARAMS_KWD = "params"
|
||||
FOREACH_KWD = "foreach"
|
||||
MATRIX_KWD = "matrix"
|
||||
DO_KWD = "do"
|
||||
|
||||
DEFAULT_PARAMS_FILE = "params.yaml"
|
||||
@@ -105,7 +107,7 @@ def is_map_or_seq(data: Any) -> bool:
|
||||
return not isinstance(data, str) and _is_map_or_seq(data)
|
||||
|
||||
|
||||
def split_foreach_name(name: str) -> Tuple[str, Optional[str]]:
|
||||
def split_group_name(name: str) -> Tuple[str, Optional[str]]:
|
||||
group, *keys = name.rsplit(JOIN, maxsplit=1)
|
||||
return group, first(keys)
|
||||
|
||||
@@ -120,13 +122,15 @@ def check_interpolations(data: "DictStrAny", where: str, path: str):
|
||||
return recurse(func)(data)
|
||||
|
||||
|
||||
Definition = Union["ForeachDefinition", "EntryDefinition"]
|
||||
Definition = Union["ForeachDefinition", "EntryDefinition", "MatrixDefinition"]
|
||||
|
||||
|
||||
def make_definition(
|
||||
resolver: "DataResolver", name: str, definition: "DictStrAny", **kwargs
|
||||
) -> Definition:
|
||||
args = resolver, resolver.context, name, definition
|
||||
if MATRIX_KWD in definition:
|
||||
return MatrixDefinition(*args, **kwargs)
|
||||
if FOREACH_KWD in definition:
|
||||
return ForeachDefinition(*args, **kwargs)
|
||||
return EntryDefinition(*args, **kwargs)
|
||||
@@ -135,6 +139,7 @@ def make_definition(
|
||||
class DataResolver:
|
||||
def __init__(self, repo: "Repo", wdir: str, d: dict):
|
||||
self.fs = fs = repo.fs
|
||||
self.parsing_config = repo.config.get("parsing", {})
|
||||
|
||||
if os.path.isabs(wdir):
|
||||
wdir = fs.path.relpath(wdir)
|
||||
@@ -158,7 +163,8 @@ class DataResolver:
|
||||
self.tracked_vars: Dict[str, Mapping] = {}
|
||||
|
||||
stages_data = d.get(STAGES_KWD, {})
|
||||
# we wrap the definitions into ForeachDefinition and EntryDefinition,
|
||||
# we wrap the definitions into:
|
||||
# ForeachDefinition, MatrixDefinition, and EntryDefinition
|
||||
# that helps us to optimize, cache and selectively load each one of
|
||||
# them as we need, and simplify all of this DSL/parsing logic.
|
||||
self.definitions: Dict[str, Definition] = {
|
||||
@@ -167,12 +173,13 @@ class DataResolver:
|
||||
}
|
||||
|
||||
def resolve_one(self, name: str):
|
||||
group, key = split_foreach_name(name)
|
||||
group, key = split_group_name(name)
|
||||
|
||||
if not self._has_group_and_key(group, key):
|
||||
raise EntryNotFound(f"Could not find '{name}'")
|
||||
|
||||
# all of the checks for `key` not being None for `ForeachDefinition`
|
||||
# all of the checks for `key` not being None for
|
||||
# `ForeachDefinition`/`MatrixDefinition`
|
||||
# and/or `group` not existing in the `interim`, etc. should be
|
||||
# handled by the `self.has_key()` above.
|
||||
definition = self.definitions[group]
|
||||
@@ -189,7 +196,7 @@ class DataResolver:
|
||||
return {STAGES_KWD: data}
|
||||
|
||||
def has_key(self, key: str):
|
||||
return self._has_group_and_key(*split_foreach_name(key))
|
||||
return self._has_group_and_key(*split_group_name(key))
|
||||
|
||||
def _has_group_and_key(self, group: str, key: Optional[str] = None):
|
||||
try:
|
||||
@@ -197,16 +204,14 @@ class DataResolver:
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if key:
|
||||
return isinstance(definition, ForeachDefinition) and definition.has_member(
|
||||
key
|
||||
)
|
||||
return not isinstance(definition, ForeachDefinition)
|
||||
if not isinstance(definition, (ForeachDefinition, MatrixDefinition)):
|
||||
return key is None
|
||||
return key is not None and definition.has_member(key)
|
||||
|
||||
@collecting
|
||||
def get_keys(self):
|
||||
for name, definition in self.definitions.items():
|
||||
if isinstance(definition, ForeachDefinition):
|
||||
if isinstance(definition, (ForeachDefinition, MatrixDefinition)):
|
||||
yield from definition.get_generated_names()
|
||||
continue
|
||||
yield name
|
||||
@@ -255,8 +260,9 @@ class EntryDefinition:
|
||||
name = self.name
|
||||
if not skip_checks:
|
||||
# we can check for syntax errors as we go for interpolated entries,
|
||||
# but for foreach-generated ones, once is enough, which it does
|
||||
# that itself. See `ForeachDefinition.do_definition`.
|
||||
# but for foreach and matrix generated ones, once is enough, which it does
|
||||
# that itself. See `ForeachDefinition.template`
|
||||
# and `MatrixDefinition.template`.
|
||||
check_syntax_errors(self.definition, name, self.relpath)
|
||||
|
||||
# we need to pop vars from generated/evaluated data
|
||||
@@ -301,7 +307,10 @@ class EntryDefinition:
|
||||
) -> "DictStrAny":
|
||||
try:
|
||||
return context.resolve(
|
||||
value, skip_interpolation_checks=skip_checks, key=key
|
||||
value,
|
||||
skip_interpolation_checks=skip_checks,
|
||||
key=key,
|
||||
config=self.resolver.parsing_config,
|
||||
)
|
||||
except (ParseError, KeyNotInContext) as exc:
|
||||
format_and_raise(exc, f"'{self.where}.{self.name}.{key}'", self.relpath)
|
||||
@@ -327,17 +336,18 @@ class ForeachDefinition:
|
||||
self.name = name
|
||||
|
||||
assert DO_KWD in definition
|
||||
assert MATRIX_KWD not in definition
|
||||
self.foreach_data = definition[FOREACH_KWD]
|
||||
self._do_definition = definition[DO_KWD]
|
||||
self._template = definition[DO_KWD]
|
||||
|
||||
self.pair = IterationPair()
|
||||
self.where = where
|
||||
|
||||
@cached_property
|
||||
def do_definition(self):
|
||||
def template(self):
|
||||
# optimization: check for syntax errors only once for `foreach` stages
|
||||
check_syntax_errors(self._do_definition, self.name, self.relpath)
|
||||
return self._do_definition
|
||||
check_syntax_errors(self._template, self.name, self.relpath)
|
||||
return self._template
|
||||
|
||||
@cached_property
|
||||
def resolved_iterable(self):
|
||||
@@ -440,16 +450,111 @@ class ForeachDefinition:
|
||||
# i.e. quadratic complexity).
|
||||
generated = self._generate_name(key)
|
||||
entry = EntryDefinition(
|
||||
self.resolver, self.context, generated, self.do_definition
|
||||
self.resolver, self.context, generated, self.template
|
||||
)
|
||||
try:
|
||||
# optimization: skip checking for syntax errors on each foreach
|
||||
# generated stages. We do it once when accessing do_definition.
|
||||
# generated stages. We do it once when accessing template.
|
||||
return entry.resolve_stage(skip_checks=True)
|
||||
except ContextError as exc:
|
||||
format_and_raise(exc, f"stage '{generated}'", self.relpath)
|
||||
|
||||
# let mypy know that this state is unreachable as format_and_raise
|
||||
# does not return at all (it's not able to understand it for some
|
||||
# reason)
|
||||
raise AssertionError("unreachable")
|
||||
|
||||
class MatrixDefinition:
|
||||
def __init__(
|
||||
self,
|
||||
resolver: DataResolver,
|
||||
context: Context,
|
||||
name: str,
|
||||
definition: "DictStrAny",
|
||||
where: str = STAGES_KWD,
|
||||
):
|
||||
self.resolver = resolver
|
||||
self.relpath = self.resolver.relpath
|
||||
self.context = context
|
||||
self.name = name
|
||||
|
||||
assert MATRIX_KWD in definition
|
||||
assert DO_KWD not in definition
|
||||
assert FOREACH_KWD not in definition
|
||||
|
||||
self._template = definition.copy()
|
||||
self.matrix_data = self._template.pop(MATRIX_KWD)
|
||||
|
||||
self.pair = IterationPair()
|
||||
self.where = where
|
||||
|
||||
@cached_property
|
||||
def template(self) -> "DictStrAny":
|
||||
# optimization: check for syntax errors only once for `matrix` stages
|
||||
check_syntax_errors(self._template, self.name, self.relpath)
|
||||
return self._template
|
||||
|
||||
@cached_property
|
||||
def resolved_iterable(self) -> Dict[str, List]:
|
||||
return self._resolve_matrix_data()
|
||||
|
||||
def _resolve_matrix_data(self) -> Dict[str, List]:
|
||||
try:
|
||||
iterable = self.context.resolve(self.matrix_data, unwrap=False)
|
||||
except (ContextError, ParseError) as exc:
|
||||
format_and_raise(exc, f"'{self.where}.{self.name}.matrix'", self.relpath)
|
||||
return iterable
|
||||
|
||||
@cached_property
|
||||
def normalized_iterable(self) -> Dict[str, "DictStrAny"]:
|
||||
"""Convert sequence to Mapping with keys normalized."""
|
||||
iterable = self.resolved_iterable
|
||||
assert isinstance(iterable, Mapping)
|
||||
|
||||
ret: Dict[str, "DictStrAny"] = {}
|
||||
matrix = {key: enumerate(v) for key, v in iterable.items()}
|
||||
for combination in product(*matrix.values()):
|
||||
d: "DictStrAny" = {}
|
||||
fragments: List[str] = []
|
||||
for k, (i, v) in zip(matrix.keys(), combination):
|
||||
d[k] = v
|
||||
fragments.append(f"{k}{i}" if is_map_or_seq(v) else to_str(v))
|
||||
|
||||
key = "-".join(fragments)
|
||||
ret[key] = d
|
||||
return ret
|
||||
|
||||
def has_member(self, key: str) -> bool:
|
||||
return key in self.normalized_iterable
|
||||
|
||||
def get_generated_names(self) -> List[str]:
|
||||
return list(map(self._generate_name, self.normalized_iterable))
|
||||
|
||||
def _generate_name(self, key: str) -> str:
|
||||
return f"{self.name}{JOIN}{key}"
|
||||
|
||||
def resolve_all(self) -> "DictStrAny":
|
||||
return join(map(self.resolve_one, self.normalized_iterable))
|
||||
|
||||
def resolve_one(self, key: str) -> "DictStrAny":
|
||||
return self._each_iter(key)
|
||||
|
||||
def _each_iter(self, key: str) -> "DictStrAny":
|
||||
err_message = f"Could not find '{key}' in matrix group '{self.name}'"
|
||||
with reraise(KeyError, EntryNotFound(err_message)):
|
||||
value = self.normalized_iterable[key]
|
||||
|
||||
temp_dict = {self.pair.value: value}
|
||||
with self.context.set_temporarily(temp_dict, reserve=True):
|
||||
# optimization: item and key can be removed on __exit__() as they
|
||||
# are top-level values, and are not merged recursively.
|
||||
# This helps us avoid cloning context, which is slower
|
||||
# (increasing the size of the context might increase
|
||||
# the no. of items to be generated which means more cloning,
|
||||
# i.e. quadratic complexity).
|
||||
generated = self._generate_name(key)
|
||||
entry = EntryDefinition(
|
||||
self.resolver, self.context, generated, self.template
|
||||
)
|
||||
try:
|
||||
# optimization: skip checking for syntax errors on each matrix
|
||||
# generated stages. We do it once when accessing template.
|
||||
return entry.resolve_stage(skip_checks=True)
|
||||
except ContextError as exc:
|
||||
format_and_raise(exc, f"stage '{generated}'", self.relpath)
|
||||
|
||||
+19
-4
@@ -495,7 +495,12 @@ class Context(CtxDict):
|
||||
self.data.pop(key, None)
|
||||
|
||||
def resolve(
|
||||
self, src, unwrap=True, skip_interpolation_checks=False, key=None
|
||||
self,
|
||||
src,
|
||||
unwrap=True,
|
||||
skip_interpolation_checks=False,
|
||||
key=None,
|
||||
config=None,
|
||||
) -> Any:
|
||||
"""Recursively resolves interpolation and returns resolved data.
|
||||
|
||||
@@ -511,10 +516,15 @@ class Context(CtxDict):
|
||||
{'lst': [1, 2, 3]}
|
||||
"""
|
||||
func = recurse(self.resolve_str)
|
||||
return func(src, unwrap, skip_interpolation_checks, key)
|
||||
return func(src, unwrap, skip_interpolation_checks, key, config)
|
||||
|
||||
def resolve_str(
|
||||
self, src: str, unwrap=True, skip_interpolation_checks=False, key=None
|
||||
self,
|
||||
src: str,
|
||||
unwrap=True,
|
||||
skip_interpolation_checks=False,
|
||||
key=None,
|
||||
config=None,
|
||||
) -> str:
|
||||
"""Resolves interpolated string to it's original value,
|
||||
or in case of multiple interpolations, a combined string.
|
||||
@@ -535,7 +545,12 @@ class Context(CtxDict):
|
||||
return value
|
||||
# but not "${num} days"
|
||||
return str_interpolate(
|
||||
src, matches, self, skip_checks=skip_interpolation_checks, key=key
|
||||
src,
|
||||
matches,
|
||||
self,
|
||||
skip_checks=skip_interpolation_checks,
|
||||
key=key,
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user