mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
064fbfd0d5 | ||
|
|
e8e1c76504 | ||
|
|
532e115782 | ||
|
|
189f4a0216 | ||
|
|
0bc101964e | ||
|
|
3319808ed2 | ||
|
|
cd37ebca37 | ||
|
|
4385720fab | ||
|
|
d52123f850 | ||
|
|
9803db6a64 | ||
|
|
bcb1ba0ea8 | ||
|
|
389fd48b5a | ||
|
|
2ecbb16769 | ||
|
|
ec73300782 | ||
|
|
954ac6ab03 | ||
|
|
1020b06a16 | ||
|
|
9cf1f45e71 | ||
|
|
b67e75c691 | ||
|
|
728731d4d7 | ||
|
|
6723a34bda | ||
|
|
eb26b9a78d | ||
|
|
42641ddd20 | ||
|
|
b6f2a8e653 | ||
|
|
024f06d672 | ||
|
|
8dd3b28729 | ||
|
|
d4178c080b | ||
|
|
302f5b07f6 | ||
|
|
7d68863ba2 | ||
|
|
09f48a5aaa | ||
|
|
004e15c2c0 | ||
|
|
382edfc382 | ||
|
|
dd535f617b | ||
|
|
15eb326188 | ||
|
|
34f402659c | ||
|
|
3e808b6f37 | ||
|
|
ea52ab2260 | ||
|
|
57c6b1036f | ||
|
|
950b576cbd | ||
|
|
2cbe25ac90 | ||
|
|
c9cd4a8c51 | ||
|
|
95c726b297 | ||
|
|
281b97c8ef | ||
|
|
a282e0e48c | ||
|
|
48581715b6 | ||
|
|
13ca959e98 | ||
|
|
a0f4e12fb4 | ||
|
|
d95dd27773 | ||
|
|
f76ef84c88 | ||
|
|
f2f4227656 | ||
|
|
7ca8fbbc82 | ||
|
|
8ec1ff44de | ||
|
|
c19a11152a | ||
|
|
cab60b54f8 | ||
|
|
fc7caa487f | ||
|
|
568abb3e28 | ||
|
|
3e7cff3e35 | ||
|
|
7dc5420548 | ||
|
|
644e9ede92 | ||
|
|
a9512e8585 | ||
|
|
00e53c5c32 | ||
|
|
0aaf96bce4 | ||
|
|
e07b99ed33 | ||
|
|
7bc906011f | ||
|
|
b84227db89 | ||
|
|
318b269ad1 | ||
|
|
1176b6d875 | ||
|
|
ee6539292b | ||
|
|
bf313d92aa | ||
|
|
46289182d2 | ||
|
|
cb941a836d | ||
|
|
a70b3087b4 | ||
|
|
c9dd137426 | ||
|
|
93e6526907 | ||
|
|
230eb7087d | ||
|
|
c3b91e5f85 | ||
|
|
eac030598c | ||
|
|
068b8371aa | ||
|
|
888b6392bb | ||
|
|
b24a31b834 | ||
|
|
45553c5b30 | ||
|
|
28f57933db | ||
|
|
25dca0b665 | ||
|
|
45c4e1998d | ||
|
|
eea67a6396 | ||
|
|
f43c04f935 | ||
|
|
3486435a7f | ||
|
|
e0fd0790fc | ||
|
|
f99d3cda57 | ||
|
|
003461f9a2 | ||
|
|
88660f6a66 | ||
|
|
7df6090a9c | ||
|
|
fdba79c519 | ||
|
|
2f6b6cc586 | ||
|
|
0fea622266 | ||
|
|
4a1985d7cd | ||
|
|
c8903dc3a6 | ||
|
|
28dd39a1a0 | ||
|
|
ef9d8580cc | ||
|
|
1af23f32b7 | ||
|
|
7b488887b4 | ||
|
|
8262dbd37c | ||
|
|
264d78b637 | ||
|
|
1b2400842d | ||
|
|
81db879c51 | ||
|
|
d668cbeb45 | ||
|
|
a466241552 | ||
|
|
7e3e257c7f | ||
|
|
ab56e4bb9b | ||
|
|
36ed182f26 | ||
|
|
045d1f5e3e | ||
|
|
5214aec1bb | ||
|
|
72066f233b | ||
|
|
cb6f625dd9 | ||
|
|
87fadd3225 | ||
|
|
8f028028b9 | ||
|
|
9aec8f1926 | ||
|
|
4175f9ffa4 | ||
|
|
c5b1a73dc5 | ||
|
|
044e79000d | ||
|
|
be6128470f | ||
|
|
dd0d4f5a2c | ||
|
|
6e0dfb94e4 | ||
|
|
ce6c8a5678 | ||
|
|
b927337a30 | ||
|
|
03b372a073 | ||
|
|
915a755e2f | ||
|
|
22cc2a5cbc | ||
|
|
a67d8a44b3 | ||
|
|
80a52e65ad | ||
|
|
208ccb2430 | ||
|
|
97e97c7f24 | ||
|
|
1d1451ddd3 | ||
|
|
0cc7c4c02b | ||
|
|
d3d459caa0 | ||
|
|
a4dc8ddc74 | ||
|
|
3eceeea3e8 | ||
|
|
8fc959060a | ||
|
|
0638d99fe3 | ||
|
|
24abc6f328 | ||
|
|
4dd544121f | ||
|
|
869ea442ef | ||
|
|
922eb20e44 | ||
|
|
a2f91ed5fd | ||
|
|
017762fc2c | ||
|
|
874aceccf8 | ||
|
|
41aee9a3b7 | ||
|
|
0d895e2661 | ||
|
|
14eaba4cdb | ||
|
|
3064738a34 | ||
|
|
c07592131a | ||
|
|
52a9cddc14 | ||
|
|
38cbebfb28 | ||
|
|
5017c2f282 | ||
|
|
1bf6cc7c2f | ||
|
|
27666ffba7 | ||
|
|
e2d705fd76 | ||
|
|
719e93192f | ||
|
|
501256708d | ||
|
|
800fbc7ad0 | ||
|
|
73f2fa8c93 | ||
|
|
b652670cbf | ||
|
|
6de3e734ed | ||
|
|
4b2bec89ec | ||
|
|
6c0bc9563c | ||
|
|
f964a44e7c | ||
|
|
464620806c | ||
|
|
4244e5a475 | ||
|
|
0c41a25bbe | ||
|
|
26f435a433 | ||
|
|
966dc8a1ca | ||
|
|
6d96ecb376 | ||
|
|
9e57d1c32d | ||
|
|
c08655ac38 | ||
|
|
4f16852005 | ||
|
|
7bef6fd64f | ||
|
|
422d883e18 | ||
|
|
48004b1340 | ||
|
|
2ae421cea2 | ||
|
|
812fde9891 | ||
|
|
4ed4766692 | ||
|
|
9943cd43b1 | ||
|
|
7d3f35e6ab | ||
|
|
4de261f717 | ||
|
|
db75878bef | ||
|
|
7f5c53c10e | ||
|
|
259d590527 | ||
|
|
709bfe598f | ||
|
|
7e4f443002 | ||
|
|
2a3a93314b | ||
|
|
a1b146669c | ||
|
|
b8478a8b04 | ||
|
|
0ae9130052 | ||
|
|
af34f5a0f1 | ||
|
|
e9a7e0657d | ||
|
|
8abda93e3e | ||
|
|
53255c22b3 | ||
|
|
6da15629c4 | ||
|
|
5cfe61928e | ||
|
|
32467ada32 | ||
|
|
5cbaa8d98e | ||
|
|
4762c38ff7 | ||
|
|
66acbc892a | ||
|
|
d2edbae7e3 | ||
|
|
be0c827b95 | ||
|
|
d6ecef7169 | ||
|
|
e8b649a75f | ||
|
|
8767f42f39 | ||
|
|
8580e66d60 | ||
|
|
c24ae2b0c3 | ||
|
|
4e17279440 | ||
|
|
50cdf4f213 | ||
|
|
f628f670c9 | ||
|
|
d7489bb65f | ||
|
|
5a4f4d3a8e | ||
|
|
2c24b24562 | ||
|
|
df9f76c2c0 | ||
|
|
0db3ece060 | ||
|
|
7c437cc349 | ||
|
|
3ed5efceb7 | ||
|
|
d33df60e41 | ||
|
|
c5415342d0 | ||
|
|
dd86076ba6 | ||
|
|
7dc3859400 | ||
|
|
4f3b28e268 | ||
|
|
32f435c47f | ||
|
|
054feae778 | ||
|
|
2abe515391 | ||
|
|
1eb89a2dc0 | ||
|
|
9c300a0a1c | ||
|
|
7b68a6f0ca | ||
|
|
ee823a59b9 | ||
|
|
16f404b16a | ||
|
|
0eda4bd91c | ||
|
|
a098a50c8d | ||
|
|
f57a378c0c | ||
|
|
3e0a50e52e | ||
|
|
ec72866eb9 | ||
|
|
e53d390ff9 | ||
|
|
dd330ac592 | ||
|
|
4ccb712b6d | ||
|
|
ecee2d3522 | ||
|
|
20bb6ddf3c | ||
|
|
e58fcd864b | ||
|
|
0514db3bfa | ||
|
|
1e0b2d7af0 | ||
|
|
bfcc8f877c | ||
|
|
46ad8745d3 | ||
|
|
eb13d3ca83 | ||
|
|
f46beb4958 | ||
|
|
9a1d74048a | ||
|
|
cce6aed378 | ||
|
|
612696cf4a | ||
|
|
3f5f983f5f | ||
|
|
fed9f088ca | ||
|
|
05eaeef012 | ||
|
|
48c0527755 | ||
|
|
4668a35f64 | ||
|
|
8107ec849b | ||
|
|
19284b3b4c | ||
|
|
315d1cde99 | ||
|
|
1d415efd0b | ||
|
|
e7c20aa1d8 | ||
|
|
058377f33e | ||
|
|
c7b1b14905 | ||
|
|
fbd8f5c185 | ||
|
|
7fe370b729 | ||
|
|
f9df6d2a16 | ||
|
|
2e4fb376da | ||
|
|
6d6a3f17b1 | ||
|
|
bdaf4aced6 | ||
|
|
0bf1802d4b | ||
|
|
6d3045320b | ||
|
|
3469dda8b8 | ||
|
|
eefede92b2 | ||
|
|
e295434ec6 | ||
|
|
b1f1b01ffc | ||
|
|
f41d7effdb | ||
|
|
6a417885b8 | ||
|
|
4221d9f70b | ||
|
|
5822d53687 | ||
|
|
a478a0ea98 | ||
|
|
e81c88e501 | ||
|
|
b286c311f4 | ||
|
|
d39c0ce618 | ||
|
|
1a8680de7d | ||
|
|
696c704632 | ||
|
|
3e68cd7105 | ||
|
|
73088fc7b9 | ||
|
|
978d5ac227 | ||
|
|
21585d18ab | ||
|
|
198cdeec06 | ||
|
|
e049c4bab1 | ||
|
|
e2ed671cc9 | ||
|
|
8a97b21b76 | ||
|
|
a113097817 | ||
|
|
fbcc3f389c | ||
|
|
7521e91d05 | ||
|
|
18d147e2f8 | ||
|
|
39d5d36033 | ||
|
|
091c59e663 | ||
|
|
f3010d1d65 | ||
|
|
36a2bf472b | ||
|
|
93a200cf5f | ||
|
|
dae08255d0 | ||
|
|
237e9d4354 | ||
|
|
788b4d3bb5 | ||
|
|
40c61c4f62 | ||
|
|
1a1294848b | ||
|
|
93dab26313 |
@@ -0,0 +1,23 @@
|
||||
name: benchmarks
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: run benchmarks
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
- uses: iterative/dvc-bench@master
|
||||
with:
|
||||
pytest_options: "-k 'test_init or test_help'"
|
||||
|
||||
@@ -10,11 +10,13 @@ jobs:
|
||||
deb:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python 3.8
|
||||
- uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -39,11 +41,13 @@ jobs:
|
||||
rpm:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python 3.8
|
||||
- uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -68,11 +72,13 @@ jobs:
|
||||
osxpkg:
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python 3.8
|
||||
- uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -100,11 +106,13 @@ jobs:
|
||||
exe:
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- name: Set up Python 3.8
|
||||
- uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
- name: Install deps
|
||||
run: |
|
||||
pip install .[all]
|
||||
@@ -128,7 +136,9 @@ jobs:
|
||||
pip:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -136,7 +146,9 @@ jobs:
|
||||
- name: Install
|
||||
run: pip install ".[all,tests]"
|
||||
- name: Build packages
|
||||
run: ./scripts/build_package.sh
|
||||
run: |
|
||||
pip install -U pip
|
||||
./scripts/build_package.sh
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
|
||||
@@ -16,19 +16,21 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/[email protected]
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/[email protected]
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install requirements
|
||||
run: |
|
||||
pip install wheel
|
||||
pip install ".[all,tests,terraform]" Pygments collective.checkdocs pre-commit
|
||||
- name: install temporary dependencies
|
||||
run: |
|
||||
pip install git+https://github.com/isidentical/gdrivefs@service-account
|
||||
pip install git+https://github.com/isidentical/pydata-google-auth@service-account
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install ".[dev]" pre-commit
|
||||
- name: Check README
|
||||
run: python setup.py checkdocs
|
||||
- uses: pre-commit/[email protected]
|
||||
@@ -38,19 +40,12 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-18.04, windows-2019, macos-10.15]
|
||||
pyv: ["3.6", "3.7", "3.8", "3.9"]
|
||||
include:
|
||||
- os: "macos-10.15"
|
||||
pyv: "3.10.0-rc.1"
|
||||
exclude:
|
||||
- os: macos-10.15
|
||||
pyv: "3.9"
|
||||
- os: macos-10.15
|
||||
pyv: "3.7"
|
||||
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.7", "3.8", "3.9", "3.10"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -61,58 +56,29 @@ jobs:
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
- name: set pip cache
|
||||
id: pip-cache
|
||||
uses: actions/[email protected].6
|
||||
uses: actions/[email protected].7
|
||||
with:
|
||||
path: ${{ steps.pip-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: install libgit2 on Python3.10 in macOS
|
||||
if: matrix.pyv == '3.10.0-rc.1' && matrix.os == 'macos-10.15'
|
||||
run: |
|
||||
brew install libgit2
|
||||
- name: install
|
||||
if: steps.cache.pip-cache-dir.cache-hit != 'true'
|
||||
run: |
|
||||
pip install --upgrade pip setuptools wheel
|
||||
pip install -e ".[all,tests,terraform]"
|
||||
- name: install temporary dependencies
|
||||
run: |
|
||||
pip install git+https://github.com/isidentical/gdrivefs@service-account
|
||||
pip install git+https://github.com/isidentical/pydata-google-auth@service-account
|
||||
pip install -e ".[dev]"
|
||||
- name: setup git
|
||||
run: |
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "DVC Tester"
|
||||
- name: setup gs creds
|
||||
shell: bash
|
||||
env:
|
||||
GS_CREDS_JSON: ${{ secrets.GS_CREDS_JSON }}
|
||||
if: env.GS_CREDS_JSON != ''
|
||||
run: |
|
||||
mkdir -p scripts/ci
|
||||
echo "$GS_CREDS_JSON" > scripts/ci/gcp-creds.json
|
||||
- name: disable gdrive tests
|
||||
if: >-
|
||||
github.event_name != 'schedule' &&
|
||||
(matrix.pyv != '3.8' || matrix.os != 'ubuntu-18.04')
|
||||
run: |
|
||||
echo "extra_test_args=--disable-gdrive" >> $GITHUB_ENV
|
||||
- name: run tests
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
GDRIVE_CREDENTIALS_DATA: ${{ secrets.GDRIVE_CREDENTIALS_DATA }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID}}
|
||||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET}}
|
||||
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT}}
|
||||
timeout-minutes: 40
|
||||
run: >-
|
||||
python -m tests -n=4
|
||||
python -m tests -n=auto
|
||||
--cov-report=xml --cov-report=term
|
||||
--all ${{ env.extra_test_args }}
|
||||
${{ env.extra_test_args }}
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v2.0.3
|
||||
uses: codecov/codecov-action@v2.1.0
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
|
||||
@@ -13,6 +13,7 @@ build/
|
||||
dist/
|
||||
|
||||
*.egg-info/
|
||||
.eggs
|
||||
|
||||
hooks/*.pyc
|
||||
|
||||
@@ -38,3 +39,4 @@ scripts/ci/gcp-creds.json
|
||||
.gcp-creds.json
|
||||
|
||||
*~
|
||||
/dvc/_dvc_version.py
|
||||
|
||||
+16
-16
@@ -1,24 +1,29 @@
|
||||
ci:
|
||||
skip: [pylint, dvc-pre-commit]
|
||||
skip: [mypy, pylint, dvc-pre-commit]
|
||||
|
||||
repos:
|
||||
- hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
repo: https://github.com/ambv/black
|
||||
rev: 21.8b0
|
||||
rev: 21.12b0
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
- --ignore-words-list
|
||||
- ba,datas,fo,uptodate
|
||||
- ba,datas,fo,uptodate,cachable
|
||||
repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.1.0
|
||||
- hooks:
|
||||
- id: isort
|
||||
language_version: python3
|
||||
repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.9.3
|
||||
rev: 5.10.1
|
||||
- hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
@@ -29,19 +34,14 @@ repos:
|
||||
- flake8-string-format
|
||||
repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.9.2
|
||||
- hooks:
|
||||
- id: mypy
|
||||
language_version: python3
|
||||
files: ^dvc/
|
||||
additional_dependencies:
|
||||
- types-requests
|
||||
- types-paramiko
|
||||
- types-tabulate
|
||||
- types-toml
|
||||
repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: 'v0.910'
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy
|
||||
entry: mypy
|
||||
files: ^dvc/
|
||||
language: system
|
||||
types: [python]
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: pylint
|
||||
@@ -54,7 +54,7 @@ repos:
|
||||
id: beautysh
|
||||
language_version: python3
|
||||
repo: https://github.com/lovesegfault/beautysh
|
||||
rev: v6.1.0
|
||||
rev: v6.2.1
|
||||
- hooks:
|
||||
- args:
|
||||
- git-hook
|
||||
|
||||
@@ -1,615 +0,0 @@
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=pygit2
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
init-hook="import sys;
|
||||
from pathlib import Path;
|
||||
from pylint.config import find_pylintrc;
|
||||
|
||||
tests_folder = Path(find_pylintrc()).parent / 'tests'
|
||||
sys.path.append(str(tests_folder));
|
||||
"
|
||||
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint_pytest,pylint_plugin_disable
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=format,
|
||||
# ^ most of them are covered by flake8,
|
||||
# and are conventions rather than warnings or errors
|
||||
refactoring, # will be noisy if it starts giving suggestions
|
||||
spelling,
|
||||
# design is too-few-* and too-many-* checks, too-much-* for us right now
|
||||
design,
|
||||
# following were generated by default
|
||||
# most of them are redundant Py3 checks for Py2+3 codebase
|
||||
print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape,
|
||||
# custom disables start here
|
||||
no-self-use,
|
||||
invalid-name,
|
||||
# already a check enabled for `wildcard-import`
|
||||
unused-wildcard-import,
|
||||
misplaced-comparison-constant,
|
||||
# probably soon: https://github.com/iterative/dvc/issues/1843
|
||||
logging-format-interpolation,
|
||||
logging-fstring-interpolation,
|
||||
# we don't have proper docstrings yet
|
||||
# even if we do, we'll be doing it gradually
|
||||
missing-function-docstring,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
duplicate-code,
|
||||
# too noisy, and detects issues even when import is inside a function
|
||||
# see: https://github.com/PyCQA/pylint/issues/850
|
||||
cyclic-import,
|
||||
# isort should cover these:
|
||||
wrong-import-order,
|
||||
wrong-import-position,
|
||||
ungrouped-imports,
|
||||
multiple-imports,
|
||||
# we use it to speedup/optimize
|
||||
import-outside-toplevel,
|
||||
fixme,
|
||||
# we use cause of the exception for error message output
|
||||
# so some of the times we don't use `raise from` to not show the cause
|
||||
# of the error
|
||||
raise-missing-from,
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member,
|
||||
no-else-return,
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Format style used to check logging format string. `old` means using %
|
||||
# formatting, while `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package..
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=hdfs.*,pytest.lazy_fixture,logging.TRACE,logger.trace,sys.getwindowsversion
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,Dvcfile
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=azure
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
signature-mutators=funcy.decorators.decorator,
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_|args|kwargs
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=79
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,
|
||||
dict-separator
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_,
|
||||
s3,
|
||||
gs,
|
||||
x, y,
|
||||
x0, y0,
|
||||
x1, y1,
|
||||
dx, dy
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement.
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception".
|
||||
overgeneral-exceptions=Exception
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
Copyright 2017-2021 Iterative, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
include fastentrypoints.py
|
||||
include LICENSE
|
||||
recursive-include requirements *.txt
|
||||
recursive-include dvc/repo/plots/templates *.json
|
||||
|
||||
+40
-1
@@ -1,10 +1,39 @@
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Callable, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
|
||||
|
||||
@contextmanager
|
||||
def yappi_profile(
|
||||
path: Union[Callable[[], str], str] = None, wall_clock: bool = True
|
||||
):
|
||||
try:
|
||||
import yappi # pylint: disable=import-error
|
||||
except ImportError:
|
||||
print("Failed to run profiler, yappi is not installed")
|
||||
yield
|
||||
return
|
||||
|
||||
yappi.set_clock_type("wall" if wall_clock else "cpu")
|
||||
|
||||
yappi.start()
|
||||
yield
|
||||
yappi.stop()
|
||||
|
||||
# pylint:disable=no-member
|
||||
if path:
|
||||
stats = yappi.get_func_stats()
|
||||
fpath = path() if callable(path) else path
|
||||
stats.save(fpath, "callgrind")
|
||||
else:
|
||||
yappi.get_func_stats().print_all()
|
||||
yappi.get_thread_stats().print_all()
|
||||
|
||||
yappi.clear_stats()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def instrument(html_output=False):
|
||||
"""Run a statistical profiler"""
|
||||
@@ -70,6 +99,13 @@ def debugtools(args: "Namespace" = None, **kwargs):
|
||||
stack.enter_context(profile(kw.get("cprofile_dump")))
|
||||
if kw.get("instrument") or kw.get("instrument_open"):
|
||||
stack.enter_context(instrument(kw.get("instrument_open", False)))
|
||||
if kw.get("yappi"):
|
||||
from datetime import datetime
|
||||
|
||||
output = "callgrind.dvc-{0:%Y%m%d}_{0:%H%M%S}.out"
|
||||
stack.enter_context(
|
||||
yappi_profile(path=lambda: output.format(datetime.now()))
|
||||
)
|
||||
yield
|
||||
|
||||
|
||||
@@ -79,6 +115,9 @@ def add_debugging_flags(parser):
|
||||
parser.add_argument(
|
||||
"--cprofile", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yappi", 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
|
||||
|
||||
+7
-5
@@ -68,7 +68,7 @@ def send(path):
|
||||
url = "https://analytics.dvc.org"
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
with open(path) as fobj:
|
||||
with open(path, encoding="utf-8") as fobj:
|
||||
report = json.load(fobj)
|
||||
|
||||
report.update(_runtime_info())
|
||||
@@ -82,10 +82,12 @@ def send(path):
|
||||
|
||||
|
||||
def _scm_in_use():
|
||||
from scmrepo.noscm import NoSCM
|
||||
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import SCM, NoSCM
|
||||
from dvc.scm.base import SCMError
|
||||
|
||||
from .scm import SCM, SCMError
|
||||
|
||||
try:
|
||||
scm = SCM(root_dir=Repo.find_root())
|
||||
@@ -173,13 +175,13 @@ def _find_or_create_user_id():
|
||||
try:
|
||||
with Lock(lockfile):
|
||||
try:
|
||||
with open(fname) as fobj:
|
||||
with open(fname, encoding="utf-8") as fobj:
|
||||
user_id = json.load(fobj)["user_id"]
|
||||
|
||||
except (FileNotFoundError, ValueError, KeyError):
|
||||
user_id = str(uuid.uuid4())
|
||||
|
||||
with open(fname, "w") as fobj:
|
||||
with open(fname, "w", encoding="utf-8") as fobj:
|
||||
json.dump({"user_id": user_id}, fobj)
|
||||
|
||||
return user_id
|
||||
|
||||
+4
-5
@@ -4,7 +4,6 @@ from contextlib import _GeneratorContextManager as GCM
|
||||
from funcy import reraise
|
||||
|
||||
from dvc.exceptions import OutputNotFoundError, PathMissingError
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
@@ -20,15 +19,15 @@ def get_url(path, repo=None, rev=None, remote=None):
|
||||
directory in the remote storage.
|
||||
"""
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
path_info = PathInfo(_repo.root_dir) / path
|
||||
fs_path = _repo.fs.path.join(_repo.root_dir, path)
|
||||
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
||||
metadata = _repo.repo_fs.metadata(path_info)
|
||||
metadata = _repo.repo_fs.metadata(fs_path)
|
||||
|
||||
if not metadata.is_dvc:
|
||||
raise OutputNotFoundError(path, repo)
|
||||
|
||||
cloud = metadata.repo.cloud
|
||||
md5 = metadata.repo.dvcfs.info(path_info)["md5"]
|
||||
md5 = metadata.repo.dvcfs.info(fs_path)["md5"]
|
||||
return cloud.get_url_for(remote, checksum=md5)
|
||||
|
||||
|
||||
@@ -113,7 +112,7 @@ def make_checkpoint():
|
||||
root_dir, Repo.DVC_DIR, "tmp", CheckpointTask.SIGNAL_FILE
|
||||
)
|
||||
|
||||
with builtins.open(signal_file, "w") as fobj:
|
||||
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("")
|
||||
|
||||
-351
@@ -1,351 +0,0 @@
|
||||
import logging
|
||||
|
||||
from shortuuid import uuid
|
||||
|
||||
from dvc import prompt
|
||||
from dvc.exceptions import (
|
||||
CacheLinkError,
|
||||
CheckoutError,
|
||||
ConfirmRemoveError,
|
||||
DvcException,
|
||||
)
|
||||
from dvc.ignore import DvcIgnoreFilter
|
||||
from dvc.objects import check, load
|
||||
from dvc.objects.db.slow_link_detection import ( # type: ignore[attr-defined]
|
||||
slow_link_guard,
|
||||
)
|
||||
from dvc.objects.errors import ObjectFormatError
|
||||
from dvc.objects.stage import stage
|
||||
from dvc.types import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _changed(path_info, fs, obj, cache, state=None):
|
||||
logger.trace("checking if '%s'('%s') has changed.", path_info, obj)
|
||||
|
||||
try:
|
||||
check(cache, obj)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
logger.debug(
|
||||
"cache for '%s'('%s') has changed.", path_info, obj.hash_info
|
||||
)
|
||||
return True
|
||||
|
||||
actual = state.get(path_info, fs) if state else None
|
||||
if actual is None:
|
||||
try:
|
||||
actual = stage(
|
||||
cache, path_info, fs, obj.hash_info.name, dry_run=True
|
||||
)[1].hash_info
|
||||
except FileNotFoundError:
|
||||
logger.debug("'%s' doesn't exist.", path_info)
|
||||
return True
|
||||
|
||||
if obj.hash_info != actual:
|
||||
logger.debug(
|
||||
"hash value '%s' for '%s' has changed (actual '%s').",
|
||||
obj.hash_info,
|
||||
actual,
|
||||
path_info,
|
||||
)
|
||||
return True
|
||||
|
||||
logger.trace("'%s' hasn't changed.", path_info)
|
||||
return False
|
||||
|
||||
|
||||
def _remove(path_info, fs, cache, force=False):
|
||||
if not fs.exists(path_info):
|
||||
return
|
||||
|
||||
if force:
|
||||
fs.remove(path_info)
|
||||
return
|
||||
|
||||
current = stage(cache, path_info, fs, fs.PARAM_CHECKSUM, dry_run=True)[
|
||||
1
|
||||
].hash_info
|
||||
try:
|
||||
obj = load(cache, current)
|
||||
check(cache, obj)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
msg = (
|
||||
f"file/directory '{path_info}' is going to be removed. "
|
||||
"Are you sure you want to proceed?"
|
||||
)
|
||||
|
||||
if not prompt.confirm(msg):
|
||||
raise ConfirmRemoveError(str(path_info))
|
||||
|
||||
fs.remove(path_info)
|
||||
|
||||
|
||||
def _verify_link(cache, path_info, link_type):
|
||||
if link_type == "hardlink" and cache.fs.getsize(path_info) == 0:
|
||||
return
|
||||
|
||||
if cache.cache_type_confirmed:
|
||||
return
|
||||
|
||||
is_link = getattr(cache.fs, f"is_{link_type}", None)
|
||||
if is_link and not is_link(path_info):
|
||||
cache.fs.remove(path_info)
|
||||
raise DvcException(f"failed to verify {link_type}")
|
||||
|
||||
cache.cache_type_confirmed = True
|
||||
|
||||
|
||||
def _do_link(cache, from_info, to_info, link_method):
|
||||
if cache.fs.exists(to_info):
|
||||
raise DvcException(f"Link '{to_info}' already exists!")
|
||||
|
||||
link_method(from_info, to_info)
|
||||
|
||||
logger.debug(
|
||||
"Created '%s': %s -> %s", cache.cache_types[0], from_info, to_info
|
||||
)
|
||||
|
||||
|
||||
@slow_link_guard
|
||||
def _try_links(cache, from_info, to_info, link_types):
|
||||
while link_types:
|
||||
link_method = getattr(cache.fs, link_types[0])
|
||||
try:
|
||||
_do_link(cache, from_info, to_info, link_method)
|
||||
_verify_link(cache, to_info, link_types[0])
|
||||
return
|
||||
|
||||
except DvcException as exc:
|
||||
logger.debug(
|
||||
"Cache type '%s' is not supported: %s", link_types[0], exc
|
||||
)
|
||||
del link_types[0]
|
||||
|
||||
raise CacheLinkError([to_info])
|
||||
|
||||
|
||||
def _link(cache, from_info, to_info):
|
||||
cache.makedirs(to_info.parent)
|
||||
try:
|
||||
_try_links(cache, from_info, to_info, cache.cache_types)
|
||||
except FileNotFoundError as exc:
|
||||
raise CheckoutError([to_info]) from exc
|
||||
|
||||
|
||||
def _cache_is_copy(cache, path_info):
|
||||
"""Checks whether cache uses copies."""
|
||||
if cache.cache_type_confirmed:
|
||||
return cache.cache_types[0] == "copy"
|
||||
|
||||
if set(cache.cache_types) <= {"copy"}:
|
||||
return True
|
||||
|
||||
workspace_file = path_info.with_name("." + uuid())
|
||||
test_cache_file = cache.path_info / ".cache_type_test_file"
|
||||
if not cache.fs.exists(test_cache_file):
|
||||
cache.makedirs(test_cache_file.parent)
|
||||
with cache.fs.open(test_cache_file, "wb") as fobj:
|
||||
fobj.write(bytes(1))
|
||||
try:
|
||||
_link(cache, test_cache_file, workspace_file)
|
||||
finally:
|
||||
cache.fs.remove(workspace_file)
|
||||
cache.fs.remove(test_cache_file)
|
||||
|
||||
cache.cache_type_confirmed = True
|
||||
return cache.cache_types[0] == "copy"
|
||||
|
||||
|
||||
def _checkout_file(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
state=None,
|
||||
):
|
||||
"""The file is changed we need to checkout a new copy"""
|
||||
modified = False
|
||||
cache_info = cache.hash_to_path_info(obj.hash_info.value)
|
||||
if fs.exists(path_info):
|
||||
if not relink and _changed(path_info, fs, obj, cache, state=state):
|
||||
modified = True
|
||||
_remove(path_info, fs, cache, force=force)
|
||||
_link(cache, cache_info, path_info)
|
||||
else:
|
||||
if fs.iscopy(path_info) and _cache_is_copy(cache, path_info):
|
||||
cache.unprotect(path_info)
|
||||
else:
|
||||
_remove(path_info, fs, cache, force=force)
|
||||
_link(cache, cache_info, path_info)
|
||||
else:
|
||||
_link(cache, cache_info, path_info)
|
||||
modified = True
|
||||
|
||||
if state:
|
||||
state.save(path_info, fs, obj.hash_info)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(str(path_info))
|
||||
|
||||
return modified
|
||||
|
||||
|
||||
def _remove_redundant_files(
|
||||
path_info, fs, obj, cache, force, dvcignore: Optional[DvcIgnoreFilter]
|
||||
):
|
||||
if dvcignore:
|
||||
existing_files = set(dvcignore.walk_files(fs, path_info))
|
||||
else:
|
||||
existing_files = set(fs.walk_files(path_info))
|
||||
|
||||
needed_files = {path_info.joinpath(*key) for key, _ in obj}
|
||||
redundant_files = existing_files - needed_files
|
||||
for path in redundant_files:
|
||||
_remove(path, fs, cache, force)
|
||||
|
||||
return bool(redundant_files)
|
||||
|
||||
|
||||
def _checkout_dir(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
modified = False
|
||||
# Create dir separately so that dir is created
|
||||
# even if there are no files in it
|
||||
if not fs.exists(path_info):
|
||||
modified = True
|
||||
fs.makedirs(path_info)
|
||||
|
||||
logger.debug("Linking directory '%s'.", path_info)
|
||||
|
||||
failed = []
|
||||
for entry_key, entry_obj in obj:
|
||||
try:
|
||||
entry_modified = _checkout_file(
|
||||
path_info.joinpath(*entry_key),
|
||||
fs,
|
||||
entry_obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
state=None,
|
||||
)
|
||||
if entry_modified:
|
||||
modified = True
|
||||
except CheckoutError as exc:
|
||||
failed.extend(exc.target_infos)
|
||||
|
||||
modified = (
|
||||
_remove_redundant_files(
|
||||
path_info, fs, obj, cache, force, dvcignore=dvcignore
|
||||
)
|
||||
or modified
|
||||
)
|
||||
|
||||
if failed:
|
||||
raise CheckoutError(failed)
|
||||
|
||||
if state:
|
||||
state.save(path_info, fs, obj.hash_info)
|
||||
|
||||
# relink is not modified, assume it as nochange
|
||||
return modified and not relink
|
||||
|
||||
|
||||
def _checkout(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force=False,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
if not obj.hash_info.isdir:
|
||||
ret = _checkout_file(
|
||||
path_info, fs, obj, cache, force, progress_callback, relink, state
|
||||
)
|
||||
else:
|
||||
ret = _checkout_dir(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
dvcignore=dvcignore,
|
||||
state=state,
|
||||
)
|
||||
|
||||
if state:
|
||||
state.save_link(path_info, fs)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def checkout(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force=False,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
quiet=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
if path_info.scheme not in ["local", cache.fs.scheme]:
|
||||
raise NotImplementedError
|
||||
|
||||
failed = None
|
||||
skip = False
|
||||
if not obj:
|
||||
if not quiet:
|
||||
logger.warning(
|
||||
"No file hash info found for '%s'. It won't be created.",
|
||||
path_info,
|
||||
)
|
||||
_remove(path_info, fs, cache, force=force)
|
||||
failed = path_info
|
||||
|
||||
elif not relink and not _changed(path_info, fs, obj, cache, state=state):
|
||||
logger.trace("Data '%s' didn't change.", path_info) # type: ignore
|
||||
skip = True
|
||||
|
||||
if failed or skip:
|
||||
if progress_callback and obj:
|
||||
progress_callback(str(path_info), len(obj))
|
||||
if failed:
|
||||
raise CheckoutError([failed])
|
||||
return
|
||||
|
||||
logger.debug("Checking out '%s' with cache '%s'.", path_info, obj)
|
||||
|
||||
return _checkout(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
dvcignore=dvcignore,
|
||||
state=state,
|
||||
)
|
||||
+17
-4
@@ -66,7 +66,8 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
)
|
||||
return 1
|
||||
|
||||
levels = [self.args.level] if self.args.level else self.config.LEVELS
|
||||
levels = self._get_appropriate_levels(self.args.level)
|
||||
|
||||
for level in levels:
|
||||
conf = self.config.read(level)
|
||||
prefix = self._config_file_prefix(
|
||||
@@ -81,9 +82,7 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
def _get(self, remote, section, opt):
|
||||
from dvc.config import ConfigError
|
||||
|
||||
levels = (
|
||||
[self.args.level] if self.args.level else self.config.LEVELS[::-1]
|
||||
)
|
||||
levels = self._get_appropriate_levels(self.args.level)[::-1]
|
||||
|
||||
for level in levels:
|
||||
conf = self.config.read(level)
|
||||
@@ -136,6 +135,20 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
f"option '{opt}' doesn't exist in {name} '{section}'"
|
||||
)
|
||||
|
||||
def _get_appropriate_levels(self, levels):
|
||||
if levels:
|
||||
self._validate_level_for_non_repo_operation(levels)
|
||||
return [levels]
|
||||
if self.config.dvc_dir is None:
|
||||
return self.config.SYSTEM_LEVELS
|
||||
return self.config.LEVELS
|
||||
|
||||
def _validate_level_for_non_repo_operation(self, level):
|
||||
from dvc.config import ConfigError
|
||||
|
||||
if self.config.dvc_dir is None and level in self.config.REPO_LEVELS:
|
||||
raise ConfigError("Not inside a DVC repo")
|
||||
|
||||
@staticmethod
|
||||
def _format_config(config, prefix=""):
|
||||
from dvc.utils.flatten import flatten
|
||||
|
||||
+7
-6
@@ -38,12 +38,14 @@ def _collect_targets(repo, target, outs):
|
||||
targets = []
|
||||
|
||||
outs_trie = repo.index.outs_trie
|
||||
for stage, info in pairs:
|
||||
if not info:
|
||||
for stage, path in pairs:
|
||||
if not path:
|
||||
targets.extend([str(out) for out in stage.outs])
|
||||
continue
|
||||
|
||||
for out in outs_trie.itervalues(prefix=info.parts): # noqa: B301
|
||||
for out in outs_trie.itervalues(
|
||||
prefix=repo.fs.path.parts(path)
|
||||
): # noqa: B301
|
||||
targets.extend(str(out))
|
||||
|
||||
return targets
|
||||
@@ -103,9 +105,8 @@ class CmdDAG(CmdBase):
|
||||
if self.args.dot:
|
||||
ui.write(_show_dot(G))
|
||||
else:
|
||||
from dvc.utils.pager import pager
|
||||
|
||||
pager(_show_ascii(G))
|
||||
with ui.pager():
|
||||
ui.write(_show_ascii(G))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -393,6 +393,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
help="Show status of all stages in the specified directory.",
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
|
||||
+8
-6
@@ -1,5 +1,4 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -16,7 +15,7 @@ def _digest(checksum):
|
||||
return "{}..{}".format(checksum["old"][0:8], checksum["new"][0:8])
|
||||
|
||||
|
||||
def _show_md(diff, show_hash=False, hide_missing=False):
|
||||
def _show_markdown(diff, show_hash=False, hide_missing=False):
|
||||
headers = ["Status", "Hash", "Path"] if show_hash else ["Status", "Path"]
|
||||
rows = []
|
||||
statuses = ["added", "deleted", "renamed", "modified"]
|
||||
@@ -142,10 +141,10 @@ class CmdDiff(CmdBase):
|
||||
del entry["hash"]
|
||||
diff[key] = entries
|
||||
|
||||
if self.args.show_json:
|
||||
ui.write(json.dumps(diff))
|
||||
elif self.args.show_md:
|
||||
_show_md(diff, show_hash, hide_missing)
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
elif self.args.markdown:
|
||||
_show_markdown(diff, show_hash, hide_missing)
|
||||
elif diff:
|
||||
self._show_diff(diff, hide_missing)
|
||||
|
||||
@@ -188,6 +187,7 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
help="Format the output into a JSON",
|
||||
action="store_true",
|
||||
@@ -200,9 +200,11 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
action="store_true",
|
||||
dest="markdown",
|
||||
default=False,
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
|
||||
+238
-42
@@ -5,9 +5,8 @@ from datetime import date, datetime
|
||||
from fnmatch import fnmatch
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, Optional
|
||||
|
||||
from funcy import lmap
|
||||
from funcy import compact, lmap
|
||||
|
||||
from dvc import prompt
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.command.metrics import DEFAULT_PRECISION
|
||||
@@ -19,9 +18,8 @@ from dvc.utils.flatten import flatten
|
||||
from dvc.utils.serialize import encode_exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.compare import TabularData
|
||||
from dvc.ui import RichText
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -144,7 +142,7 @@ def _collect_rows(
|
||||
fill_value=FILL_VALUE,
|
||||
iso=False,
|
||||
):
|
||||
from dvc.scm.git import Git
|
||||
from scmrepo.git import Git
|
||||
|
||||
if sort_by:
|
||||
sort_path, sort_name, sort_type = _sort_column(
|
||||
@@ -260,11 +258,12 @@ def _sort_column(sort_by, metric_names, param_names):
|
||||
def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
|
||||
def _sort(item):
|
||||
rev, exp = item
|
||||
tip = exp.get("checkpoint_tip")
|
||||
exp_data = exp.get("data", {})
|
||||
tip = exp_data.get("checkpoint_tip")
|
||||
if tip and tip != rev:
|
||||
# Sort checkpoint experiments by tip commit
|
||||
return _sort((tip, experiments[tip]))
|
||||
data = exp.get(typ, {}).get(sort_path, {})
|
||||
data = exp_data.get(typ, {}).get(sort_path, {}).get("data", {})
|
||||
val = flatten(data).get(sort_name)
|
||||
return val is None, val
|
||||
|
||||
@@ -291,8 +290,6 @@ def _format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
|
||||
|
||||
|
||||
def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.compare import _format_field, with_value
|
||||
|
||||
if not items:
|
||||
@@ -307,10 +304,10 @@ def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
|
||||
item.get(name),
|
||||
FILL_VALUE_ERRORED if data.get("error", None) else fill_value,
|
||||
)
|
||||
# wrap field data in rich.Text, otherwise rich may
|
||||
# wrap field data in ui.rich_text, otherwise rich may
|
||||
# interpret unescaped braces from list/dict types as rich
|
||||
# markup tags
|
||||
row.append(Text(str(_format_field(value, precision))))
|
||||
row.append(ui.rich_text(str(_format_field(value, precision))))
|
||||
|
||||
|
||||
def _parse_filter_list(param_list):
|
||||
@@ -361,17 +358,15 @@ def experiments_table(
|
||||
return td
|
||||
|
||||
|
||||
def prepare_exp_id(kwargs) -> "Text":
|
||||
from rich.text import Text
|
||||
|
||||
def prepare_exp_id(kwargs) -> "RichText":
|
||||
exp_name = kwargs["Experiment"]
|
||||
rev = kwargs["rev"]
|
||||
typ = kwargs.get("typ", "baseline")
|
||||
|
||||
if typ == "baseline" or not exp_name:
|
||||
text = Text(exp_name or rev)
|
||||
text = ui.rich_text(exp_name or rev)
|
||||
else:
|
||||
text = Text.assemble(rev, " [", (exp_name, "bold"), "]")
|
||||
text = ui.rich_text.assemble(rev, " [", (exp_name, "bold"), "]")
|
||||
|
||||
parent = kwargs.get("parent")
|
||||
suff = f" ({parent})" if parent else ""
|
||||
@@ -379,7 +374,7 @@ def prepare_exp_id(kwargs) -> "Text":
|
||||
|
||||
tree = experiment_types[typ]
|
||||
pref = f"{tree} " if tree else ""
|
||||
return Text(pref) + text
|
||||
return ui.rich_text(pref) + text
|
||||
|
||||
|
||||
def baseline_styler(typ):
|
||||
@@ -387,7 +382,12 @@ def baseline_styler(typ):
|
||||
|
||||
|
||||
def show_experiments(
|
||||
all_experiments, pager=True, no_timestamp=False, show_csv=False, **kwargs
|
||||
all_experiments,
|
||||
pager=True,
|
||||
no_timestamp=False,
|
||||
csv=False,
|
||||
markdown=False,
|
||||
**kwargs,
|
||||
):
|
||||
from funcy.seqs import flatten as flatten_list
|
||||
|
||||
@@ -443,7 +443,7 @@ def show_experiments(
|
||||
|
||||
row_styles = lmap(baseline_styler, td.column("typ"))
|
||||
|
||||
if not show_csv:
|
||||
if not csv:
|
||||
merge_headers = ["Experiment", "rev", "typ", "parent"]
|
||||
td.column("Experiment")[:] = map(
|
||||
prepare_exp_id, td.as_dict(merge_headers)
|
||||
@@ -471,13 +471,17 @@ def show_experiments(
|
||||
}
|
||||
)
|
||||
|
||||
if kwargs.get("only_changed", False):
|
||||
td.drop_duplicates("cols")
|
||||
|
||||
td.render(
|
||||
pager=pager,
|
||||
borders=True,
|
||||
rich_table=True,
|
||||
header_styles=styles,
|
||||
row_styles=row_styles,
|
||||
show_csv=show_csv,
|
||||
csv=csv,
|
||||
markdown=markdown,
|
||||
)
|
||||
|
||||
|
||||
@@ -510,18 +514,16 @@ class CmdExperimentsShow(CmdBase):
|
||||
logger.exception("failed to show experiments")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(all_experiments, default=_format_json))
|
||||
if self.args.json:
|
||||
ui.write_json(all_experiments, default=_format_json)
|
||||
else:
|
||||
precision = (
|
||||
self.args.precision or None
|
||||
if self.args.show_csv
|
||||
if self.args.csv
|
||||
else DEFAULT_PRECISION
|
||||
)
|
||||
fill_value = "" if self.args.show_csv else FILL_VALUE
|
||||
iso = True if self.args.show_csv else False
|
||||
fill_value = "" if self.args.csv else FILL_VALUE
|
||||
iso = True if self.args.csv else False
|
||||
|
||||
show_experiments(
|
||||
all_experiments,
|
||||
@@ -536,7 +538,9 @@ class CmdExperimentsShow(CmdBase):
|
||||
fill_value=fill_value,
|
||||
iso=iso,
|
||||
pager=not self.args.no_pager,
|
||||
show_csv=self.args.show_csv,
|
||||
csv=self.args.csv,
|
||||
markdown=self.args.markdown,
|
||||
only_changed=self.args.only_changed,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -565,10 +569,8 @@ class CmdExperimentsDiff(CmdBase):
|
||||
logger.exception("failed to show experiments diff")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(diff))
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
@@ -583,11 +585,12 @@ class CmdExperimentsDiff(CmdBase):
|
||||
show_diff(
|
||||
diff[key],
|
||||
title=title,
|
||||
markdown=self.args.show_md,
|
||||
markdown=self.args.markdown,
|
||||
no_path=self.args.no_path,
|
||||
old=self.args.old,
|
||||
on_empty_diff="diff not supported",
|
||||
precision=precision if key == "metrics" else None,
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
)
|
||||
|
||||
return 0
|
||||
@@ -668,7 +671,7 @@ class CmdExperimentsGC(CmdRepro):
|
||||
logger.warning(msg)
|
||||
|
||||
msg = "Are you sure you want to proceed?"
|
||||
if not self.args.force and not prompt.confirm(msg):
|
||||
if not self.args.force and not ui.confirm(msg):
|
||||
return 1
|
||||
|
||||
removed = self.repo.experiments.gc(
|
||||
@@ -789,6 +792,91 @@ class CmdExperimentsRemove(CmdBase):
|
||||
return 0
|
||||
|
||||
|
||||
class CmdExperimentsInit(CmdBase):
|
||||
DEFAULT_NAME = "train"
|
||||
CODE = "src"
|
||||
DATA = "data"
|
||||
MODELS = "models"
|
||||
DEFAULT_METRICS = "metrics.json"
|
||||
DEFAULT_PARAMS = "params.yaml"
|
||||
PLOTS = "plots"
|
||||
DVCLIVE = "dvclive"
|
||||
DEFAULTS = {
|
||||
"code": CODE,
|
||||
"data": DATA,
|
||||
"models": MODELS,
|
||||
"metrics": DEFAULT_METRICS,
|
||||
"params": DEFAULT_PARAMS,
|
||||
"plots": PLOTS,
|
||||
"live": DVCLIVE,
|
||||
}
|
||||
EXP_LINK = "https://s.dvc.org/g/exp/run"
|
||||
|
||||
def run(self):
|
||||
from dvc.command.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 = init(
|
||||
self.repo,
|
||||
name=self.args.name,
|
||||
type=self.args.type,
|
||||
defaults=defaults,
|
||||
overrides=cli_args,
|
||||
interactive=self.args.interactive,
|
||||
force=self.args.force,
|
||||
)
|
||||
|
||||
text = ui.rich_text.assemble(
|
||||
"\n" if self.args.interactive else "",
|
||||
"Created ",
|
||||
(self.args.name, "bright_blue"),
|
||||
" stage in ",
|
||||
("dvc.yaml", "green"),
|
||||
".",
|
||||
)
|
||||
if not self.args.run:
|
||||
text.append_text(
|
||||
ui.rich_text.assemble(
|
||||
" To run, use ",
|
||||
('"dvc exp run"', "green"),
|
||||
".\nSee ",
|
||||
(self.EXP_LINK, "repr.url"),
|
||||
".",
|
||||
)
|
||||
)
|
||||
|
||||
ui.write(text, styled=True)
|
||||
if self.args.run:
|
||||
return self.repo.experiments.run(
|
||||
targets=[initialized_stage.addressing]
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
EXPERIMENTS_HELP = "Commands to run and compare experiments."
|
||||
|
||||
@@ -894,7 +982,10 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--sort-order",
|
||||
help="Sort order to use with --sort-by.",
|
||||
help=(
|
||||
"Sort order to use with --sort-by."
|
||||
" Defaults to ascending ('asc')."
|
||||
),
|
||||
choices=("asc", "desc"),
|
||||
default="asc",
|
||||
)
|
||||
@@ -911,17 +1002,27 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Always show git commit SHAs instead of branch/tag names.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print output in JSON format instead of a human-readable table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--csv",
|
||||
"--show-csv",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print output in csv format instead of a human-readable table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--precision",
|
||||
type=int,
|
||||
@@ -931,6 +1032,15 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<n>",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--only-changed",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Only show metrics/params with values varying "
|
||||
"across the selected experiments."
|
||||
),
|
||||
)
|
||||
experiments_show_parser.set_defaults(func=CmdExperimentsShow)
|
||||
|
||||
EXPERIMENTS_APPLY_HELP = (
|
||||
@@ -985,23 +1095,20 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show only params that are stage dependencies.",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--old",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show old metric/param value.",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--no-path",
|
||||
action="store_true",
|
||||
@@ -1303,6 +1410,95 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
experiments_remove_parser.set_defaults(func=CmdExperimentsRemove)
|
||||
|
||||
EXPERIMENTS_INIT_HELP = "Initialize 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"
|
||||
f" (default: {CmdExperimentsInit.DVCLIVE})",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--type",
|
||||
choices=["default", "dl"],
|
||||
default="default",
|
||||
help="Select type of stage to create (default: %(default)s)",
|
||||
)
|
||||
experiments_init_parser.set_defaults(func=CmdExperimentsInit)
|
||||
|
||||
|
||||
def _add_run_common(parser):
|
||||
"""Add common args for 'exp run' and 'exp resume'."""
|
||||
|
||||
@@ -34,10 +34,8 @@ class CmdList(CmdBaseNoRepo):
|
||||
recursive=self.args.recursive,
|
||||
dvc_only=self.args.dvc_only,
|
||||
)
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(entries))
|
||||
if self.args.json:
|
||||
ui.write_json(entries)
|
||||
elif entries:
|
||||
entries = _prettify(entries, with_color=True)
|
||||
ui.write("\n".join(entries))
|
||||
@@ -54,6 +52,7 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
list_parser = subparsers.add_parser(
|
||||
"list",
|
||||
aliases=["ls"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(LIST_HELP, "list"),
|
||||
help=LIST_HELP,
|
||||
@@ -70,7 +69,10 @@ def add_parser(subparsers, parent_parser):
|
||||
"--dvc-only", action="store_true", help="Show only DVC outputs."
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--show-json", action="store_true", help="Show output in JSON format."
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--rev",
|
||||
|
||||
+209
-3
@@ -2,7 +2,10 @@ import argparse
|
||||
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.command.config import CmdConfig
|
||||
from dvc.compare import TabularData
|
||||
from dvc.config import ConfigError
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.types import Dict, List
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
@@ -69,6 +72,60 @@ class CmdMachineRemove(CmdMachineConfig):
|
||||
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
|
||||
@@ -88,6 +145,54 @@ class CmdMachineModify(CmdMachineConfig):
|
||||
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(
|
||||
"Rename failed. Machine '{}' already exists.".format(
|
||||
self.args.new
|
||||
)
|
||||
)
|
||||
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:
|
||||
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:
|
||||
@@ -127,6 +232,59 @@ class CmdMachineCreate(CmdBase):
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineStatus(CmdBase):
|
||||
INSTANCE_FIELD = ["name", "instance", "status"]
|
||||
SHOWN_FIELD = [
|
||||
"cloud",
|
||||
"instance_ip",
|
||||
"instance_type",
|
||||
"instance_hdd_size",
|
||||
"instance_gpu",
|
||||
]
|
||||
|
||||
def _add_row(
|
||||
self,
|
||||
name: str,
|
||||
all_status: List[Dict],
|
||||
td: TabularData,
|
||||
):
|
||||
|
||||
if not all_status:
|
||||
row = [name, None, "offline"]
|
||||
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="-"
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -219,6 +377,27 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
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",
|
||||
@@ -243,6 +422,18 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
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",
|
||||
@@ -259,7 +450,7 @@ def add_parser(subparsers, parent_parser):
|
||||
machine_CREATE_HELP = "Create and start a machine instance."
|
||||
machine_create_parser = machine_subparsers.add_parser(
|
||||
"create",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(machine_CREATE_HELP, "machine/create"),
|
||||
help=machine_CREATE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
@@ -269,10 +460,25 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
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_config_parser, parent_parser],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(machine_DESTROY_HELP, "machine/destroy"),
|
||||
help=machine_DESTROY_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
@@ -285,7 +491,7 @@ def add_parser(subparsers, parent_parser):
|
||||
machine_SSH_HELP = "Connect to a machine via SSH."
|
||||
machine_ssh_parser = machine_subparsers.add_parser(
|
||||
"ssh",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(machine_SSH_HELP, "machine/ssh"),
|
||||
help=machine_SSH_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
|
||||
+14
-10
@@ -31,16 +31,14 @@ class CmdMetricsShow(CmdMetricsBase):
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(metrics, default=encode_exception))
|
||||
if self.args.json:
|
||||
ui.write_json(metrics, default=encode_exception)
|
||||
else:
|
||||
from dvc.compare import show_metrics
|
||||
|
||||
show_metrics(
|
||||
metrics,
|
||||
markdown=self.args.show_md,
|
||||
markdown=self.args.markdown,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
@@ -65,20 +63,20 @@ class CmdMetricsDiff(CmdMetricsBase):
|
||||
logger.exception("failed to show metrics diff")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(diff))
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
show_diff(
|
||||
diff,
|
||||
title="Metric",
|
||||
markdown=self.args.show_md,
|
||||
markdown=self.args.markdown,
|
||||
no_path=self.args.no_path,
|
||||
precision=self.args.precision or DEFAULT_PRECISION,
|
||||
round_digits=True,
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
)
|
||||
|
||||
return 0
|
||||
@@ -140,15 +138,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show metrics for all commits.",
|
||||
)
|
||||
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",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
@@ -220,15 +221,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show unchanged metrics as well.",
|
||||
)
|
||||
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",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
|
||||
@@ -25,19 +25,19 @@ class CmdParamsDiff(CmdBase):
|
||||
logger.exception("failed to show params diff")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(diff))
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
show_diff(
|
||||
diff,
|
||||
title="Param",
|
||||
markdown=self.args.show_md,
|
||||
markdown=self.args.markdown,
|
||||
no_path=self.args.no_path,
|
||||
show_changes=False,
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
)
|
||||
|
||||
return 0
|
||||
@@ -103,15 +103,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show only params that are stage dependencies.",
|
||||
)
|
||||
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",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
|
||||
+6
-11
@@ -62,19 +62,11 @@ class CmdPlots(CmdBase):
|
||||
html_template_path=self.args.html_template,
|
||||
)
|
||||
|
||||
assert index_path.is_absolute()
|
||||
url = index_path.as_uri()
|
||||
ui.write(url)
|
||||
ui.write(index_path.as_uri())
|
||||
|
||||
if self.args.open:
|
||||
import webbrowser
|
||||
return ui.open_browser(index_path)
|
||||
|
||||
opened = webbrowser.open(index_path)
|
||||
if not opened:
|
||||
ui.error_write(
|
||||
"Failed to open. Please try opening it manually."
|
||||
)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
except DvcException:
|
||||
@@ -183,7 +175,10 @@ def add_parser(subparsers, parent_parser):
|
||||
_add_output_arguments(plots_diff_parser)
|
||||
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
|
||||
|
||||
PLOTS_MODIFY_HELP = "Modify display properties of plot metrics files."
|
||||
PLOTS_MODIFY_HELP = (
|
||||
"Modify display properties of data-series plots "
|
||||
"(has no effect on image-type plots)."
|
||||
)
|
||||
plots_modify_parser = plots_subparsers.add_parser(
|
||||
"modify",
|
||||
parents=[parent_parser],
|
||||
|
||||
@@ -86,7 +86,7 @@ class CmdRemoteDefault(CmdRemote):
|
||||
if self.args.name is None and not self.args.unset:
|
||||
conf = self.config.read(self.args.level)
|
||||
try:
|
||||
print(conf["core"]["remote"])
|
||||
ui.write(conf["core"]["remote"])
|
||||
except KeyError:
|
||||
ui.write("No default remote set")
|
||||
return 1
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ class CmdRun(CmdBase):
|
||||
self.args.outs_persist_no_cache,
|
||||
self.args.checkpoints,
|
||||
self.args.params,
|
||||
self.args.cmd,
|
||||
self.args.command,
|
||||
]
|
||||
): # pragma: no cover
|
||||
logger.error(
|
||||
@@ -36,7 +36,7 @@ class CmdRun(CmdBase):
|
||||
kwargs = vars(self.args)
|
||||
kwargs.update(
|
||||
{
|
||||
"cmd": parse_cmd(self.args.cmd),
|
||||
"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"),
|
||||
|
||||
@@ -129,7 +129,7 @@ class CmdStageAdd(CmdBase):
|
||||
kwargs = vars(self.args)
|
||||
kwargs.update(
|
||||
{
|
||||
"cmd": parse_cmd(kwargs.pop("cmd")),
|
||||
"cmd": parse_cmd(kwargs.pop("command")),
|
||||
"params": parse_params(self.args.params),
|
||||
}
|
||||
)
|
||||
@@ -281,7 +281,7 @@ def _add_common_args(parser):
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"cmd",
|
||||
"command",
|
||||
nargs=argparse.REMAINDER,
|
||||
help="Command to execute.",
|
||||
metavar="command",
|
||||
|
||||
@@ -69,10 +69,8 @@ class CmdDataStatus(CmdDataBase):
|
||||
if self.args.quiet:
|
||||
return bool(st)
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(st))
|
||||
if self.args.json:
|
||||
ui.write_json(st)
|
||||
return 0
|
||||
|
||||
if st:
|
||||
|
||||
+95
-4
@@ -12,6 +12,7 @@ from typing import (
|
||||
Mapping,
|
||||
MutableSequence,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
overload,
|
||||
@@ -115,6 +116,8 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
del col[item]
|
||||
|
||||
def __len__(self) -> int:
|
||||
if not self._columns:
|
||||
return 0
|
||||
return len(self.columns[0])
|
||||
|
||||
@property
|
||||
@@ -168,7 +171,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
def render(self, **kwargs: Any):
|
||||
from dvc.ui import ui
|
||||
|
||||
if kwargs.pop("show_csv", False):
|
||||
if kwargs.pop("csv", False):
|
||||
ui.write(self.to_csv(), end="")
|
||||
else:
|
||||
ui.table(self, headers=self.keys(), **kwargs)
|
||||
@@ -181,6 +184,86 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
{k: self._columns[k][i] for k in keys} for i in range(len(self))
|
||||
]
|
||||
|
||||
def dropna(self, axis: str = "rows", how="any"):
|
||||
if axis not in ["rows", "cols"]:
|
||||
raise ValueError(
|
||||
f"Invalid 'axis' value {axis}."
|
||||
"Choose one of ['rows', 'cols']"
|
||||
)
|
||||
if how not in ["any", "all"]:
|
||||
raise ValueError(
|
||||
f"Invalid 'how' value {how}." "Choose one of ['any', 'all']"
|
||||
)
|
||||
|
||||
match_line: Set = set()
|
||||
match = True
|
||||
if how == "all":
|
||||
match = False
|
||||
|
||||
for n_row, row in enumerate(self):
|
||||
for n_col, col in enumerate(row):
|
||||
if (col == self._fill_value) is match:
|
||||
if axis == "rows":
|
||||
match_line.add(n_row)
|
||||
break
|
||||
else:
|
||||
match_line.add(self.keys()[n_col])
|
||||
|
||||
to_drop = match_line
|
||||
if how == "all":
|
||||
if axis == "rows":
|
||||
to_drop = set(range(len(self)))
|
||||
else:
|
||||
to_drop = set(self.keys())
|
||||
to_drop -= match_line
|
||||
|
||||
if axis == "rows":
|
||||
for name in self.keys():
|
||||
self._columns[name] = Column(
|
||||
[
|
||||
x
|
||||
for n, x in enumerate(self._columns[name])
|
||||
if n not in to_drop
|
||||
]
|
||||
)
|
||||
else:
|
||||
self.drop(*to_drop)
|
||||
|
||||
def drop_duplicates(self, axis: str = "rows"):
|
||||
if axis not in ["rows", "cols"]:
|
||||
raise ValueError(
|
||||
f"Invalid 'axis' value {axis}."
|
||||
"Choose one of ['rows', 'cols']"
|
||||
)
|
||||
|
||||
if axis == "cols":
|
||||
cols_to_drop: List[str] = []
|
||||
for n_col, col in enumerate(self.columns):
|
||||
# Cast to str because Text is not hashable error
|
||||
unique_vals = {str(x) for x in col if x != self._fill_value}
|
||||
if len(unique_vals) == 1:
|
||||
cols_to_drop.append(self.keys()[n_col])
|
||||
self.drop(*cols_to_drop)
|
||||
|
||||
elif axis == "rows":
|
||||
unique_rows = []
|
||||
rows_to_drop: List[int] = []
|
||||
for n_row, row in enumerate(self):
|
||||
tuple_row = tuple(row)
|
||||
if tuple_row in unique_rows:
|
||||
rows_to_drop.append(n_row)
|
||||
else:
|
||||
unique_rows.append(tuple_row)
|
||||
|
||||
for name in self.keys():
|
||||
self._columns[name] = Column(
|
||||
[
|
||||
x
|
||||
for n, x in enumerate(self._columns[name])
|
||||
if n not in rows_to_drop
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _normalize_float(val: float, precision: int):
|
||||
return f"{val:.{precision}g}"
|
||||
@@ -211,8 +294,12 @@ def diff_table(
|
||||
precision: int = None,
|
||||
round_digits: bool = False,
|
||||
on_empty_diff: str = None,
|
||||
a_rev: str = None,
|
||||
b_rev: str = None,
|
||||
) -> TabularData:
|
||||
headers: List[str] = ["Path", title, "Old", "New", "Change"]
|
||||
a_rev = a_rev or "HEAD"
|
||||
b_rev = b_rev or "workspace"
|
||||
headers: List[str] = ["Path", title, a_rev, b_rev, "Change"]
|
||||
fill_value = "-"
|
||||
td = TabularData(headers, fill_value=fill_value)
|
||||
|
||||
@@ -240,8 +327,8 @@ def diff_table(
|
||||
td.drop("Change")
|
||||
|
||||
if not old:
|
||||
td.drop("Old")
|
||||
td.rename("New", "Value")
|
||||
td.drop(a_rev)
|
||||
td.rename(b_rev, "Value")
|
||||
|
||||
return td
|
||||
|
||||
@@ -256,6 +343,8 @@ def show_diff(
|
||||
round_digits: bool = False,
|
||||
on_empty_diff: str = None,
|
||||
markdown: bool = False,
|
||||
a_rev: str = None,
|
||||
b_rev: str = None,
|
||||
) -> None:
|
||||
td = diff_table(
|
||||
diff,
|
||||
@@ -266,6 +355,8 @@ def show_diff(
|
||||
precision=precision,
|
||||
round_digits=round_digits,
|
||||
on_empty_diff=on_empty_diff,
|
||||
a_rev=a_rev,
|
||||
b_rev=b_rev,
|
||||
)
|
||||
td.render(markdown=markdown)
|
||||
|
||||
|
||||
+8
-5
@@ -8,7 +8,6 @@ from functools import partial
|
||||
from funcy import cached_property, compact, memoize, re_find
|
||||
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
from dvc.path_info import PathInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -76,8 +75,10 @@ class Config(dict):
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
SYSTEM_LEVELS = ("system", "global")
|
||||
REPO_LEVELS = ("repo", "local")
|
||||
# In the order they shadow each other
|
||||
LEVELS = ("system", "global", "repo", "local")
|
||||
LEVELS = SYSTEM_LEVELS + REPO_LEVELS
|
||||
|
||||
CONFIG = "config"
|
||||
CONFIG_LOCAL = "config.local"
|
||||
@@ -139,7 +140,7 @@ class Config(dict):
|
||||
dvc.config.Config: config object.
|
||||
"""
|
||||
config_file = os.path.join(dvc_dir, Config.CONFIG)
|
||||
open(config_file, "w+").close()
|
||||
open(config_file, "w+", encoding="utf-8").close()
|
||||
return Config(dvc_dir)
|
||||
|
||||
def load(self, validate=True, config=None):
|
||||
@@ -227,6 +228,7 @@ class Config(dict):
|
||||
|
||||
@staticmethod
|
||||
def _to_relpath(conf_dir, path):
|
||||
from dvc.fs.local import localfs
|
||||
from dvc.utils import relpath
|
||||
|
||||
from .config_schema import RelPath
|
||||
@@ -236,8 +238,9 @@ class Config(dict):
|
||||
|
||||
if isinstance(path, RelPath) or not os.path.isabs(path):
|
||||
path = relpath(path, conf_dir)
|
||||
return localfs.path.as_posix(path)
|
||||
|
||||
return PathInfo(path).as_posix()
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def _save_paths(conf, filename):
|
||||
@@ -286,7 +289,7 @@ class Config(dict):
|
||||
def edit(self, level=None, validate=True):
|
||||
# NOTE: we write to repo config by default, same as git config
|
||||
level = level or "repo"
|
||||
if level in {"repo", "local"} and self.dvc_dir is None:
|
||||
if self.dvc_dir is None and level in self.REPO_LEVELS:
|
||||
raise ConfigError("Not inside a DVC repo")
|
||||
|
||||
conf = self.load_one(level)
|
||||
|
||||
+30
-4
@@ -156,6 +156,8 @@ SCHEMA = {
|
||||
"grant_write_acp": str,
|
||||
"grant_full_control": str,
|
||||
"cache_regions": bool,
|
||||
"read_timeout": Coerce(int),
|
||||
"connect_timeout": Coerce(int),
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gs": {
|
||||
@@ -177,10 +179,12 @@ SCHEMA = {
|
||||
},
|
||||
"hdfs": {"user": str, "kerb_ticket": str, **REMOTE_COMMON},
|
||||
"webhdfs": {
|
||||
"hdfscli_config": str,
|
||||
"webhdfs_token": str,
|
||||
"user": str,
|
||||
"webhdfs_alias": str,
|
||||
"kerberos": Bool,
|
||||
"kerberos_principal": str,
|
||||
"proxy_to": str,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"token": str,
|
||||
"use_https": Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"azure": {
|
||||
@@ -223,13 +227,26 @@ SCHEMA = {
|
||||
)
|
||||
},
|
||||
"state": {
|
||||
"dir": str,
|
||||
"row_limit": All(Coerce(int), Range(1)), # obsoleted
|
||||
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
|
||||
},
|
||||
"index": {
|
||||
"dir": str,
|
||||
},
|
||||
"machine": {
|
||||
str: {
|
||||
"cloud": All(Lower, Choices("aws", "azure")),
|
||||
"region": All(
|
||||
Lower, Choices("us-west", "us-east", "eu-west", "eu-north")
|
||||
),
|
||||
"image": str,
|
||||
"spot": Bool,
|
||||
"spot_price": Coerce(float),
|
||||
"instance_hdd_size": Coerce(int),
|
||||
"instance_type": Lower,
|
||||
"instance_gpu": Lower,
|
||||
"ssh_private": str,
|
||||
"startup_script": str,
|
||||
},
|
||||
},
|
||||
@@ -240,4 +257,13 @@ SCHEMA = {
|
||||
Optional("parametrization", default=True): Bool,
|
||||
},
|
||||
"plots": {"html_template": str},
|
||||
"exp": {
|
||||
"code": str,
|
||||
"data": str,
|
||||
"models": str,
|
||||
"metrics": str,
|
||||
"params": str,
|
||||
"plots": str,
|
||||
"live": str,
|
||||
},
|
||||
}
|
||||
|
||||
+4
-5
@@ -12,8 +12,6 @@ from dvc.utils import fix_env, is_binary
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CREATE_NO_WINDOW = 0x08000000 # only available since Python 3.7 in subprocess.
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs):
|
||||
prefix = [sys.executable]
|
||||
@@ -28,6 +26,7 @@ def _popen(cmd, **kwargs):
|
||||
def _spawn_windows(cmd, env):
|
||||
from subprocess import (
|
||||
CREATE_NEW_PROCESS_GROUP,
|
||||
CREATE_NO_WINDOW,
|
||||
STARTF_USESHOWWINDOW,
|
||||
STARTUPINFO,
|
||||
)
|
||||
@@ -49,17 +48,17 @@ def _spawn_posix(cmd, env):
|
||||
# with PyInstaller has trouble with SystemExit exception and throws
|
||||
# errors such as "[26338] Failed to execute script __main__"
|
||||
try:
|
||||
pid = os.fork()
|
||||
pid = os.fork() # pylint: disable=no-member
|
||||
if pid > 0:
|
||||
return
|
||||
except OSError:
|
||||
logger.exception("failed at first fork")
|
||||
os._exit(1) # pylint: disable=protected-access
|
||||
|
||||
os.setsid()
|
||||
os.setsid() # pylint: disable=no-member
|
||||
|
||||
try:
|
||||
pid = os.fork()
|
||||
pid = os.fork() # pylint: disable=no-member
|
||||
if pid > 0:
|
||||
os._exit(0) # pylint: disable=protected-access
|
||||
except OSError:
|
||||
|
||||
+6
-5
@@ -58,9 +58,9 @@ class DataCloud:
|
||||
from dvc.fs import get_cloud_fs
|
||||
from dvc.objects.db import get_odb
|
||||
|
||||
cls, config, path_info = get_cloud_fs(self.repo, name=name)
|
||||
config["tmp_dir"] = self.repo.tmp_dir
|
||||
return get_odb(cls(**config), path_info, **config)
|
||||
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return get_odb(cls(**config), fs_path, **config)
|
||||
|
||||
def push(
|
||||
self,
|
||||
@@ -156,5 +156,6 @@ class DataCloud:
|
||||
)
|
||||
|
||||
def get_url_for(self, remote, checksum):
|
||||
remote_odb = self.get_remote_odb(remote)
|
||||
return str(remote_odb.hash_to_path_info(checksum))
|
||||
odb = self.get_remote_odb(remote)
|
||||
path = odb.hash_to_path(checksum)
|
||||
return odb.fs.unstrip_protocol(path)
|
||||
|
||||
+22
-2
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -10,6 +11,8 @@ from dvc.utils.serialize import LOADERS, ParseError
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MissingParamsError(DvcException):
|
||||
pass
|
||||
@@ -89,10 +92,10 @@ class ParamsDependency(Dependency):
|
||||
if not self.exists:
|
||||
return {}
|
||||
|
||||
suffix = self.path_info.suffix.lower()
|
||||
suffix = self.repo.fs.path.suffix(self.fs_path).lower()
|
||||
loader = LOADERS[suffix]
|
||||
try:
|
||||
return loader(self.path_info, fs=self.repo.fs)
|
||||
return loader(self.fs_path, fs=self.repo.fs)
|
||||
except ParseError as exc:
|
||||
raise BadParamFileError(
|
||||
f"Unable to read parameters from '{self}'"
|
||||
@@ -133,3 +136,20 @@ class ParamsDependency(Dependency):
|
||||
)
|
||||
|
||||
return HashInfo(self.PARAM_PARAMS, info)
|
||||
|
||||
def save(self):
|
||||
if not self.exists:
|
||||
raise self.DoesNotExistError(self)
|
||||
|
||||
if not self.isfile and not self.isdir:
|
||||
raise self.IsNotFileOrDirError(self)
|
||||
|
||||
if self.is_empty:
|
||||
logger.warning(f"'{self}' is empty.")
|
||||
|
||||
self.ignore()
|
||||
|
||||
if self.metric or self.plot:
|
||||
self.verify_metric()
|
||||
|
||||
self.hash_info = self.get_hash()
|
||||
|
||||
+9
-13
@@ -5,8 +5,6 @@ from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
|
||||
|
||||
from voluptuous import Required
|
||||
|
||||
from dvc.path_info import PathInfo
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -34,7 +32,7 @@ class RepoDependency(Dependency):
|
||||
self._staged_objs: Dict[str, "HashFile"] = {}
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
def _parse_path(self, fs, path_info):
|
||||
def _parse_path(self, fs, fs_path):
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -63,14 +61,14 @@ class RepoDependency(Dependency):
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc.checkout import checkout
|
||||
from dvc.objects.checkout import checkout
|
||||
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
||||
|
||||
obj = self.get_obj()
|
||||
checkout(
|
||||
to.path_info,
|
||||
to.fs_path,
|
||||
to.fs,
|
||||
obj,
|
||||
self.repo.odb.local,
|
||||
@@ -114,11 +112,11 @@ class RepoDependency(Dependency):
|
||||
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = rev
|
||||
|
||||
path_info = PathInfo(repo.root_dir) / str(self.def_path)
|
||||
path = os.path.abspath(os.path.join(repo.root_dir, self.def_path))
|
||||
if not obj_only:
|
||||
try:
|
||||
for odb, obj_ids in repo.used_objs(
|
||||
[os.fspath(path_info)],
|
||||
[path],
|
||||
force=True,
|
||||
jobs=kwargs.get("jobs"),
|
||||
recursive=True,
|
||||
@@ -132,9 +130,9 @@ class RepoDependency(Dependency):
|
||||
pass
|
||||
|
||||
try:
|
||||
staging, staged_obj = stage(
|
||||
staging, _, staged_obj = stage(
|
||||
local_odb,
|
||||
path_info,
|
||||
path,
|
||||
repo.repo_fs,
|
||||
local_odb.fs.PARAM_CHECKSUM,
|
||||
)
|
||||
@@ -148,9 +146,7 @@ class RepoDependency(Dependency):
|
||||
self._staged_objs[rev] = staged_obj
|
||||
used_obj_ids[staging].add(staged_obj.hash_info)
|
||||
if isinstance(staged_obj, Tree):
|
||||
used_obj_ids[staging].update(
|
||||
entry.hash_info for _, entry in staged_obj
|
||||
)
|
||||
used_obj_ids[staging].update(oid for _, _, oid in staged_obj)
|
||||
return used_obj_ids, staged_obj
|
||||
|
||||
def _check_circular_import(self, odb, obj_ids):
|
||||
@@ -166,7 +162,7 @@ class RepoDependency(Dependency):
|
||||
for hash_info in obj_ids:
|
||||
if hash_info.isdir:
|
||||
tree = Tree.load(odb, hash_info)
|
||||
yield from (odb.get(entry.hash_info) for _, entry in tree)
|
||||
yield from (odb.get(oid) for _, _, oid in tree)
|
||||
else:
|
||||
yield odb.get(hash_info)
|
||||
|
||||
|
||||
+38
-56
@@ -1,10 +1,7 @@
|
||||
import collections
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
from voluptuous import MultipleInvalid
|
||||
from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
|
||||
@@ -12,23 +9,18 @@ from dvc.stage import serialize
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileBadNameError,
|
||||
StageFileDoesNotExistError,
|
||||
StageFileFormatError,
|
||||
StageFileIsNotDvcFileError,
|
||||
)
|
||||
from dvc.types import AnyPath
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.collections import apply_diff
|
||||
from dvc.utils.serialize import (
|
||||
dump_yaml,
|
||||
modify_yaml,
|
||||
parse_yaml,
|
||||
parse_yaml_for_update,
|
||||
)
|
||||
from dvc.utils.serialize import dump_yaml, modify_yaml
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_T = TypeVar("_T")
|
||||
|
||||
DVC_FILE = "Dvcfile"
|
||||
DVC_FILE_SUFFIX = ".dvc"
|
||||
@@ -45,10 +37,6 @@ class FileIsGitIgnored(DvcException):
|
||||
)
|
||||
|
||||
|
||||
class LockfileCorruptedError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class ParametrizedDumpError(DvcException):
|
||||
pass
|
||||
|
||||
@@ -72,7 +60,7 @@ def is_lock_file(path):
|
||||
|
||||
def is_git_ignored(repo, path):
|
||||
from dvc.fs.local import LocalFileSystem
|
||||
from dvc.scm.base import NoSCMError
|
||||
from dvc.scm import NoSCMError
|
||||
|
||||
try:
|
||||
return isinstance(repo.fs, LocalFileSystem) and repo.scm.is_ignored(
|
||||
@@ -96,7 +84,7 @@ def check_dvcfile_path(repo, path):
|
||||
|
||||
|
||||
class FileMixin:
|
||||
SCHEMA = None
|
||||
SCHEMA: Callable[[_T], _T]
|
||||
|
||||
def __init__(self, repo, path, verify=True, **kwargs):
|
||||
self.repo = repo
|
||||
@@ -138,7 +126,11 @@ class FileMixin:
|
||||
if self._is_git_ignored():
|
||||
raise FileIsGitIgnored(self.path)
|
||||
|
||||
def _load(self):
|
||||
def load(self, **kwargs: Any) -> Any:
|
||||
d, _ = self._load(**kwargs)
|
||||
return d
|
||||
|
||||
def _load(self, **kwargs: Any) -> Tuple[Any, str]:
|
||||
# it raises the proper exceptions by priority:
|
||||
# 1. when the file doesn't exists
|
||||
# 2. filename is not a DVC file
|
||||
@@ -155,18 +147,23 @@ class FileMixin:
|
||||
raise StageFileIsNotDvcFileError(self.path)
|
||||
|
||||
self._check_gitignored()
|
||||
with self.repo.fs.open(self.path, encoding="utf-8") as fd:
|
||||
stage_text = fd.read()
|
||||
d = parse_yaml(stage_text, self.path)
|
||||
return self.validate(d, self.relpath), stage_text
|
||||
return self._load_yaml(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, d, fname=None):
|
||||
assert isinstance(cls.SCHEMA, collections.abc.Callable)
|
||||
try:
|
||||
return cls.SCHEMA(d) # pylint: disable=not-callable
|
||||
except MultipleInvalid as exc:
|
||||
raise StageFileFormatError(f"'{fname}' format error: {exc}")
|
||||
def validate(cls, d: _T, fname: str = None) -> _T:
|
||||
from dvc.utils.strictyaml import validate
|
||||
|
||||
return validate(d, cls.SCHEMA, path=fname) # type: ignore[arg-type]
|
||||
|
||||
def _load_yaml(self, **kwargs: Any) -> Tuple[Any, str]:
|
||||
from dvc.utils import strictyaml
|
||||
|
||||
return strictyaml.load(
|
||||
self.path,
|
||||
self.SCHEMA, # type: ignore[arg-type]
|
||||
self.repo.fs,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def remove(self, force=False): # pylint: disable=unused-argument
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
@@ -202,7 +199,7 @@ class SingleStageFile(FileMixin):
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
logger.debug(f"Saving information to '{relpath(self.path)}'.")
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage))
|
||||
self.repo.scm.track_file(self.relpath)
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def remove_stage(self, stage): # pylint: disable=unused-argument
|
||||
self.remove()
|
||||
@@ -271,7 +268,7 @@ class PipelineFile(FileMixin):
|
||||
else:
|
||||
data["stages"].update(stage_data)
|
||||
|
||||
self.repo.scm.track_file(self.relpath)
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
@property
|
||||
def stage(self):
|
||||
@@ -298,10 +295,7 @@ class PipelineFile(FileMixin):
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
d = parse_yaml_for_update(f.read(), self.path)
|
||||
|
||||
self.validate(d, self.path)
|
||||
d, _ = self._load_yaml(round_trip=True)
|
||||
if stage.name not in d.get("stages", {}):
|
||||
return
|
||||
|
||||
@@ -343,34 +337,25 @@ def migrate_lock_v1_to_v2(d, version_info):
|
||||
d["stages"] = stages
|
||||
|
||||
|
||||
def lockfile_schema(data: _T) -> _T:
|
||||
schema = get_lockfile_schema(data)
|
||||
return schema(data)
|
||||
|
||||
|
||||
class Lockfile(FileMixin):
|
||||
@classmethod
|
||||
def validate(cls, d, fname=None):
|
||||
schema = get_lockfile_schema(d)
|
||||
try:
|
||||
return schema(d)
|
||||
except MultipleInvalid as exc:
|
||||
raise StageFileFormatError(f"'{fname}' format error: {exc}")
|
||||
SCHEMA = staticmethod(lockfile_schema) # type: ignore[assignment]
|
||||
|
||||
def _verify_filename(self):
|
||||
pass # lockfile path is hardcoded, so no need to verify here
|
||||
|
||||
def _load(self):
|
||||
def _load(self, **kwargs: Any):
|
||||
try:
|
||||
return super()._load()
|
||||
return super()._load(**kwargs)
|
||||
except StageFileDoesNotExistError:
|
||||
# we still need to account for git-ignored dvc.lock file
|
||||
# even though it may not exist or have been .dvcignored
|
||||
self._check_gitignored()
|
||||
return {}, ""
|
||||
except StageFileFormatError as exc:
|
||||
raise LockfileCorruptedError(
|
||||
f"Lockfile '{self.relpath}' is corrupted."
|
||||
) from exc
|
||||
|
||||
def load(self):
|
||||
d, _ = self._load()
|
||||
return d
|
||||
|
||||
@property
|
||||
def latest_version_info(self):
|
||||
@@ -403,16 +388,13 @@ class Lockfile(FileMixin):
|
||||
data["stages"].update(stage_data)
|
||||
|
||||
if modified:
|
||||
self.repo.scm.track_file(self.relpath)
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def remove_stage(self, stage):
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
d = parse_yaml_for_update(f.read(), self.path)
|
||||
self.validate(d, self.path)
|
||||
|
||||
d, _ = self._load_yaml(round_trip=True)
|
||||
version = LOCKFILE_VERSION.from_dict(d)
|
||||
data = d if version == LOCKFILE_VERSION.V1 else d.get("stages", {})
|
||||
if stage.name not in data:
|
||||
|
||||
+11
-18
@@ -216,18 +216,6 @@ class DvcIgnoreInCollectedDirError(DvcException):
|
||||
)
|
||||
|
||||
|
||||
class GitHookAlreadyExistsError(DvcException):
|
||||
def __init__(self, hook_name):
|
||||
from dvc.utils import format_link
|
||||
|
||||
super().__init__(
|
||||
"Hook '{}' already exists. Please refer to {} for more "
|
||||
"info.".format(
|
||||
hook_name, format_link("https://man.dvc.org/install")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class FileTransferError(DvcException):
|
||||
_METHOD = "transfer"
|
||||
|
||||
@@ -305,7 +293,7 @@ class PathMissingError(DvcException):
|
||||
|
||||
|
||||
class RemoteCacheRequiredError(DvcException):
|
||||
def __init__(self, path_info):
|
||||
def __init__(self, scheme, fs_path):
|
||||
from dvc.utils import format_link
|
||||
|
||||
super().__init__(
|
||||
@@ -314,8 +302,8 @@ class RemoteCacheRequiredError(DvcException):
|
||||
"existing cache on '{}' remote. See {} for information on how "
|
||||
"to set up remote cache."
|
||||
).format(
|
||||
path_info,
|
||||
path_info.scheme,
|
||||
fs_path,
|
||||
scheme,
|
||||
format_link("https://man.dvc.org/config#cache"),
|
||||
)
|
||||
)
|
||||
@@ -350,12 +338,12 @@ class CacheLinkError(DvcException):
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, path_infos):
|
||||
def __init__(self, fs_paths):
|
||||
msg = "No possible cache link types for '{}'. {}".format(
|
||||
", ".join([str(path) for path in path_infos]), self.SUPPORT_LINK
|
||||
", ".join(fs_paths), self.SUPPORT_LINK
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.path_infos = path_infos
|
||||
self.fs_paths = fs_paths
|
||||
|
||||
|
||||
class CircularImportError(DvcException):
|
||||
@@ -364,3 +352,8 @@ class CircularImportError(DvcException):
|
||||
f"'{dep}' contains invalid circular import. "
|
||||
f"DVC repo '{a}' already imports from '{b}'."
|
||||
)
|
||||
|
||||
|
||||
class PrettyDvcException(DvcException):
|
||||
def __pretty_exc__(self, **kwargs):
|
||||
"""Print prettier exception message."""
|
||||
|
||||
+10
-6
@@ -16,17 +16,20 @@ from dvc.exceptions import (
|
||||
PathMissingError,
|
||||
)
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import CloneError, map_scm_exception
|
||||
from dvc.utils import relpath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
@map_scm_exception()
|
||||
def external_repo(
|
||||
url, rev=None, for_write=False, cache_dir=None, cache_types=None, **kwargs
|
||||
):
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.scm.git import Git
|
||||
|
||||
logger.debug("Creating external repo %s@%s", url, rev)
|
||||
path = _cached_clone(url, rev, for_write=for_write)
|
||||
@@ -173,7 +176,7 @@ def _clone_default_branch(url, rev, for_write=False):
|
||||
|
||||
The cloned is reactualized with git pull unless rev is a known sha.
|
||||
"""
|
||||
from dvc.scm.git import Git
|
||||
from scmrepo.git import Git
|
||||
|
||||
clone_path, shallow = CLONES.get(url, (None, False))
|
||||
|
||||
@@ -198,14 +201,15 @@ def _clone_default_branch(url, rev, for_write=False):
|
||||
logger.debug("erepo: git pull '%s'", url)
|
||||
git.pull()
|
||||
else:
|
||||
from dvc.scm import clone
|
||||
|
||||
logger.debug("erepo: git clone '%s' to a temporary dir", url)
|
||||
clone_path = tempfile.mkdtemp("dvc-clone")
|
||||
if not for_write and rev and not Git.is_sha(rev):
|
||||
# If rev is a tag or branch name try shallow clone first
|
||||
from dvc.scm.base import CloneError
|
||||
|
||||
try:
|
||||
git = Git.clone(url, clone_path, shallow_branch=rev)
|
||||
git = clone(url, clone_path, shallow_branch=rev)
|
||||
shallow = True
|
||||
logger.debug(
|
||||
"erepo: using shallow clone for branch '%s'", rev
|
||||
@@ -213,7 +217,7 @@ def _clone_default_branch(url, rev, for_write=False):
|
||||
except CloneError:
|
||||
pass
|
||||
if not git:
|
||||
git = Git.clone(url, clone_path)
|
||||
git = clone(url, clone_path)
|
||||
shallow = False
|
||||
CLONES[url] = (clone_path, shallow)
|
||||
finally:
|
||||
@@ -237,7 +241,7 @@ def _unshallow(git):
|
||||
|
||||
|
||||
def _git_checkout(repo_path, rev):
|
||||
from dvc.scm.git import Git
|
||||
from scmrepo.git import Git
|
||||
|
||||
logger.debug("erepo: git checkout %s@%s", repo_path, rev)
|
||||
git = Git(repo_path)
|
||||
|
||||
+10
-11
@@ -1,4 +1,3 @@
|
||||
import posixpath
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ..scheme import Schemes
|
||||
@@ -39,7 +38,7 @@ def get_fs_cls(remote_conf, scheme=None):
|
||||
return FS_MAP.get(scheme, LocalFileSystem)
|
||||
|
||||
|
||||
def get_fs_config(config, **kwargs):
|
||||
def get_fs_config(repo, config, **kwargs):
|
||||
name = kwargs.get("name")
|
||||
if name:
|
||||
try:
|
||||
@@ -50,10 +49,10 @@ def get_fs_config(config, **kwargs):
|
||||
raise RemoteNotFoundError(f"remote '{name}' doesn't exist")
|
||||
else:
|
||||
remote_conf = kwargs
|
||||
return _resolve_remote_refs(config, remote_conf)
|
||||
return _resolve_remote_refs(repo, config, remote_conf)
|
||||
|
||||
|
||||
def _resolve_remote_refs(config, remote_conf):
|
||||
def _resolve_remote_refs(repo, config, remote_conf):
|
||||
# Support for cross referenced remotes.
|
||||
# This will merge the settings, shadowing base ref with remote_conf.
|
||||
# For example, having:
|
||||
@@ -79,8 +78,10 @@ def _resolve_remote_refs(config, remote_conf):
|
||||
if parsed.scheme != "remote":
|
||||
return remote_conf
|
||||
|
||||
base = get_fs_config(config, name=parsed.netloc)
|
||||
url = posixpath.join(base["url"], parsed.path.lstrip("/"))
|
||||
base = get_fs_config(repo, config, name=parsed.netloc)
|
||||
cls, _, _ = get_cloud_fs(repo, **base)
|
||||
relpath = parsed.path.lstrip("/").replace("/", cls.sep)
|
||||
url = cls.sep.join((base["url"], relpath))
|
||||
return {**base, **remote_conf, "url": url}
|
||||
|
||||
|
||||
@@ -91,7 +92,7 @@ def get_cloud_fs(repo, **kwargs):
|
||||
repo_config = repo.config if repo else {}
|
||||
core_config = repo_config.get("core", {})
|
||||
|
||||
remote_conf = get_fs_config(repo_config, **kwargs)
|
||||
remote_conf = get_fs_config(repo, repo_config, **kwargs)
|
||||
try:
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf)
|
||||
except Invalid as exc:
|
||||
@@ -113,10 +114,8 @@ def get_cloud_fs(repo, **kwargs):
|
||||
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
|
||||
|
||||
url = remote_conf.pop("url")
|
||||
path_info = cls.PATH_CLS(
|
||||
cls._strip_protocol(url) # pylint:disable=protected-access
|
||||
)
|
||||
fs_path = cls._strip_protocol(url) # pylint:disable=protected-access
|
||||
|
||||
extras = cls._get_kwargs_from_urls(url) # pylint:disable=protected-access
|
||||
conf = {**extras, **remote_conf} # remote config takes priority
|
||||
return cls, conf, path_info
|
||||
return cls, conf, fs_path
|
||||
|
||||
+6
-5
@@ -2,7 +2,6 @@ from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from dvc.output import Output
|
||||
from dvc.path_info import PathInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
@@ -16,7 +15,7 @@ class Metadata:
|
||||
"""
|
||||
|
||||
# required field
|
||||
path_info: PathInfo
|
||||
fs_path: str
|
||||
repo: "Repo"
|
||||
|
||||
# computed fields
|
||||
@@ -47,11 +46,13 @@ class Metadata:
|
||||
else:
|
||||
out = self.outs[0]
|
||||
# or, the path itself could be an output
|
||||
self.is_output = self.path_info == out.path_info
|
||||
self.is_output = self.fs_path == out.fs_path
|
||||
|
||||
# or, a directory must have an output somewhere deep inside it
|
||||
if not self.is_output:
|
||||
self.contains_outputs = out.path_info.isin(self.path_info)
|
||||
self.contains_outputs = self.repo.fs.path.isin(
|
||||
out.fs_path, self.fs_path
|
||||
)
|
||||
|
||||
# or, the path could be a part of an output, i.e. inside of an output
|
||||
self.part_of_output = not self.is_output and not self.contains_outputs
|
||||
@@ -69,4 +70,4 @@ class Metadata:
|
||||
return not self.isdir
|
||||
|
||||
def __str__(self):
|
||||
return f"Metadata: {self.path_info}"
|
||||
return f"Metadata: {self.fs_path}"
|
||||
|
||||
+9
-9
@@ -7,11 +7,10 @@ from fsspec.utils import infer_storage_options
|
||||
from funcy import cached_property, memoize, wrap_prop
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.utils import format_link
|
||||
|
||||
from .fsspec_wrapper import ObjectFSWrapper
|
||||
from .fsspec_wrapper import CallbackMixin, ObjectFSWrapper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_DEFAULT_CREDS_STEPS = (
|
||||
@@ -39,9 +38,8 @@ def _az_config():
|
||||
|
||||
|
||||
# pylint:disable=abstract-method
|
||||
class AzureFileSystem(ObjectFSWrapper):
|
||||
class AzureFileSystem(CallbackMixin, ObjectFSWrapper):
|
||||
scheme = Schemes.AZURE
|
||||
PATH_CLS = CloudURLInfo
|
||||
PARAM_CHECKSUM = "etag"
|
||||
DETAIL_FIELDS = frozenset(("etag", "size"))
|
||||
REQUIRES = {
|
||||
@@ -52,12 +50,14 @@ class AzureFileSystem(ObjectFSWrapper):
|
||||
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str):
|
||||
bucket = infer_storage_options(path).get("host")
|
||||
if bucket:
|
||||
return path
|
||||
opts = infer_storage_options(path)
|
||||
if opts.get("host"):
|
||||
return "{host}{path}".format(**opts)
|
||||
|
||||
bucket = _az_config().get("storage", "container_name", None)
|
||||
return f"azure://{bucket}"
|
||||
return _az_config().get("storage", "container_name", None)
|
||||
|
||||
def unstrip_protocol(self, path: str) -> str:
|
||||
return "azure://" + path.lstrip("/")
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
|
||||
+138
-104
@@ -1,12 +1,17 @@
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from functools import partial, partialmethod
|
||||
from functools import partialmethod
|
||||
from multiprocessing import cpu_count
|
||||
from typing import Any, ClassVar, Dict, FrozenSet, Optional
|
||||
from typing import ClassVar, Dict, FrozenSet, Optional
|
||||
|
||||
from funcy import cached_property
|
||||
from tqdm.utils import CallbackIOWrapper
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.path_info import URLInfo
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.progress import DEFAULT_CALLBACK, FsspecCallback
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import tmp_fname
|
||||
from dvc.utils.fs import makedirs, move
|
||||
|
||||
@@ -31,12 +36,11 @@ class RemoteMissingDepsError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class BaseFileSystem:
|
||||
class FileSystem:
|
||||
sep = "/"
|
||||
|
||||
scheme = "base"
|
||||
REQUIRES: ClassVar[Dict[str, str]] = {}
|
||||
PATH_CLS = URLInfo # type: Any
|
||||
_JOBS = 4 * cpu_count()
|
||||
|
||||
CHECKSUM_DIR_SUFFIX = ".dir"
|
||||
@@ -64,10 +68,19 @@ class BaseFileSystem:
|
||||
def config(self):
|
||||
return self._config
|
||||
|
||||
@cached_property
|
||||
def path(self):
|
||||
from .path import Path
|
||||
|
||||
return Path(self.sep)
|
||||
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str):
|
||||
return path
|
||||
|
||||
def unstrip_protocol(self, path):
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath): # pylint:disable=unused-argument
|
||||
return {}
|
||||
@@ -124,36 +137,36 @@ class BaseFileSystem:
|
||||
f"dependencies: {missing}. {hint}"
|
||||
)
|
||||
|
||||
def checksum(self, path_info) -> str:
|
||||
def checksum(self, path) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def open(self, path_info, mode: str = "r", encoding: str = None, **kwargs):
|
||||
def open(self, path, mode: str = "r", encoding: str = None, **kwargs):
|
||||
raise RemoteActionNotImplemented("open", self.scheme)
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
def exists(self, path) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
def isdir(self, path_info):
|
||||
def isdir(self, path):
|
||||
"""Optional: Overwrite only if the remote has a way to distinguish
|
||||
between a directory and a file.
|
||||
"""
|
||||
return False
|
||||
|
||||
def isfile(self, path_info):
|
||||
def isfile(self, path):
|
||||
"""Optional: Overwrite only if the remote has a way to distinguish
|
||||
between a directory and a file.
|
||||
"""
|
||||
return True
|
||||
|
||||
def isexec(self, path_info):
|
||||
def isexec(self, path):
|
||||
"""Optional: Overwrite only if the remote has a way to distinguish
|
||||
between executable and non-executable file.
|
||||
"""
|
||||
return False
|
||||
|
||||
def iscopy(self, path_info):
|
||||
def iscopy(self, path):
|
||||
"""Check if this file is an independent copy."""
|
||||
return False # We can't be sure by default
|
||||
|
||||
@@ -161,34 +174,28 @@ class BaseFileSystem:
|
||||
"""Return a generator with (root, dirs, files)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
"""Return a generator with `PathInfo`s to all the files.
|
||||
def find(self, path, prefix=None):
|
||||
"""Return a generator with `str`s to all the files.
|
||||
|
||||
Optional kwargs:
|
||||
prefix (bool): If true `path_info` will be treated as a prefix
|
||||
prefix (bool): If true `path` will be treated as a prefix
|
||||
rather than directory path.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def ls(self, path_info, detail=False):
|
||||
raise RemoteActionNotImplemented("ls", self.scheme)
|
||||
|
||||
def find(self, path_info, detail=False, prefix=None):
|
||||
raise RemoteActionNotImplemented("find", self.scheme)
|
||||
|
||||
def is_empty(self, path_info):
|
||||
def is_empty(self, path):
|
||||
return False
|
||||
|
||||
def info(self, path_info):
|
||||
def info(self, path):
|
||||
raise NotImplementedError
|
||||
|
||||
def getsize(self, path_info):
|
||||
return self.info(path_info).get("size")
|
||||
def getsize(self, path):
|
||||
return self.info(path).get("size")
|
||||
|
||||
def remove(self, path_info):
|
||||
def remove(self, path):
|
||||
raise RemoteActionNotImplemented("remove", self.scheme)
|
||||
|
||||
def makedirs(self, path_info, **kwargs):
|
||||
def makedirs(self, path, **kwargs):
|
||||
"""Optional: Implement only if the remote needs to create
|
||||
directories before copying/linking/moving data
|
||||
"""
|
||||
@@ -223,128 +230,155 @@ class BaseFileSystem:
|
||||
to_info,
|
||||
total=None,
|
||||
desc=None,
|
||||
callback=None,
|
||||
no_progress_bar=False,
|
||||
**pbar_args,
|
||||
):
|
||||
is_file_obj = hasattr(from_info, "read")
|
||||
method = "upload_fobj" if is_file_obj else "_upload"
|
||||
method = "upload_fobj" if is_file_obj else "put_file"
|
||||
if not hasattr(self, method):
|
||||
raise RemoteActionNotImplemented(method, self.scheme)
|
||||
|
||||
if to_info.scheme != self.scheme:
|
||||
raise NotImplementedError
|
||||
if not is_file_obj:
|
||||
from .local import localfs
|
||||
|
||||
logger.debug("Uploading '%s' to '%s'", from_info, to_info)
|
||||
if is_file_obj:
|
||||
with Tqdm.wrapattr(
|
||||
from_info,
|
||||
"read",
|
||||
desc = desc or localfs.path.name(from_info)
|
||||
|
||||
stack = contextlib.ExitStack()
|
||||
if not callback:
|
||||
pbar = ui.progress(
|
||||
desc=desc,
|
||||
disable=no_progress_bar,
|
||||
bytes=True,
|
||||
total=total,
|
||||
desc=desc,
|
||||
total=total or -1,
|
||||
**pbar_args,
|
||||
) as wrapped:
|
||||
)
|
||||
stack.enter_context(pbar)
|
||||
callback = pbar.as_callback()
|
||||
if total:
|
||||
callback.set_size(total)
|
||||
|
||||
with stack:
|
||||
if is_file_obj:
|
||||
wrapped = CallbackIOWrapper(
|
||||
callback.relative_update, from_info
|
||||
)
|
||||
# `size` is used to provide hints to the WebdavFileSystem
|
||||
# for legacy servers.
|
||||
# pylint: disable=no-member
|
||||
return self.upload_fobj(wrapped, to_info, size=total)
|
||||
|
||||
if from_info.scheme != "local":
|
||||
raise NotImplementedError
|
||||
|
||||
name = desc or from_info.name
|
||||
return self._upload( # noqa, pylint: disable=no-member
|
||||
from_info.fspath,
|
||||
to_info,
|
||||
name=name,
|
||||
no_progress_bar=no_progress_bar,
|
||||
**pbar_args,
|
||||
)
|
||||
logger.debug("Uploading '%s' to '%s'", from_info, to_info)
|
||||
# pylint: disable=no-member
|
||||
return self.put_file(
|
||||
os.fspath(from_info), to_info, callback=callback
|
||||
)
|
||||
|
||||
def download(
|
||||
self,
|
||||
from_info,
|
||||
to_info,
|
||||
name=None,
|
||||
callback=None,
|
||||
no_progress_bar=False,
|
||||
jobs=None,
|
||||
_only_file=False,
|
||||
**kwargs,
|
||||
):
|
||||
if not hasattr(self, "_download"):
|
||||
raise RemoteActionNotImplemented("download", self.scheme)
|
||||
from .local import localfs
|
||||
|
||||
if from_info.scheme != self.scheme:
|
||||
raise NotImplementedError
|
||||
if not hasattr(self, "get_file"):
|
||||
raise RemoteActionNotImplemented("get_file", self.scheme)
|
||||
|
||||
if to_info.scheme == self.scheme != "local":
|
||||
self.copy(from_info, to_info)
|
||||
return 0
|
||||
download_dir = not _only_file and self.isdir(from_info)
|
||||
|
||||
if to_info.scheme != "local":
|
||||
raise NotImplementedError
|
||||
|
||||
if not _only_file and self.isdir(from_info):
|
||||
return self._download_dir(
|
||||
from_info, to_info, name, no_progress_bar, jobs, **kwargs
|
||||
desc = name or localfs.path.name(to_info)
|
||||
stack = contextlib.ExitStack()
|
||||
if not callback:
|
||||
pbar_kwargs = {"unit": "Files"} if download_dir else {}
|
||||
pbar = ui.progress(
|
||||
total=-1,
|
||||
desc="Downloading directory" if download_dir else desc,
|
||||
bytes=not download_dir,
|
||||
disable=no_progress_bar,
|
||||
**pbar_kwargs,
|
||||
)
|
||||
return self._download_file(from_info, to_info, name, no_progress_bar)
|
||||
stack.enter_context(pbar)
|
||||
callback = pbar.as_callback()
|
||||
|
||||
with stack:
|
||||
if download_dir:
|
||||
return self._download_dir(
|
||||
from_info, to_info, callback=callback, jobs=jobs, **kwargs
|
||||
)
|
||||
return self._download_file(from_info, to_info, callback=callback)
|
||||
|
||||
download_file = partialmethod(download, _only_file=True)
|
||||
|
||||
def _download_dir(
|
||||
self, from_info, to_info, name, no_progress_bar, jobs, **kwargs
|
||||
self,
|
||||
from_info,
|
||||
to_info,
|
||||
callback=DEFAULT_CALLBACK,
|
||||
jobs=None,
|
||||
**kwargs,
|
||||
):
|
||||
from_infos = list(self.walk_files(from_info, **kwargs))
|
||||
from .local import localfs
|
||||
|
||||
from_infos = list(self.find(from_info, **kwargs))
|
||||
if not from_infos:
|
||||
makedirs(to_info, exist_ok=True)
|
||||
return None
|
||||
|
||||
to_infos = (
|
||||
to_info / info.relative_to(from_info) for info in from_infos
|
||||
localfs.path.join(to_info, *self.path.relparts(info, from_info))
|
||||
for info in from_infos
|
||||
)
|
||||
callback.set_size(len(from_infos))
|
||||
|
||||
with Tqdm(
|
||||
total=len(from_infos),
|
||||
desc="Downloading directory",
|
||||
unit="Files",
|
||||
disable=no_progress_bar,
|
||||
) as pbar:
|
||||
download_files = pbar.wrap_fn(
|
||||
partial(self._download_file, name=name, no_progress_bar=True)
|
||||
)
|
||||
max_workers = jobs or self.jobs
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
futures = [
|
||||
executor.submit(download_files, from_info, to_info)
|
||||
for from_info, to_info in zip(from_infos, to_infos)
|
||||
]
|
||||
download_files = FsspecCallback.wrap_fn(callback, self._download_file)
|
||||
max_workers = jobs or self.jobs
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
futures = [
|
||||
executor.submit(download_files, from_info, to_info)
|
||||
for from_info, to_info in zip(from_infos, to_infos)
|
||||
]
|
||||
|
||||
# NOTE: unlike pulling/fetching cache, where we need to
|
||||
# download everything we can, not raising an error here might
|
||||
# turn very ugly, as the user might think that he has
|
||||
# downloaded a complete directory, while having a partial one,
|
||||
# which might cause unexpected results in his pipeline.
|
||||
for future in as_completed(futures):
|
||||
# NOTE: executor won't let us raise until all futures that
|
||||
# it has are finished, so we need to cancel them ourselves
|
||||
# before re-raising.
|
||||
exc = future.exception()
|
||||
if exc:
|
||||
for entry in futures:
|
||||
entry.cancel()
|
||||
raise exc
|
||||
# NOTE: unlike pulling/fetching cache, where we need to
|
||||
# download everything we can, not raising an error here might
|
||||
# turn very ugly, as the user might think that he has
|
||||
# downloaded a complete directory, while having a partial one,
|
||||
# which might cause unexpected results in his pipeline.
|
||||
for future in as_completed(futures):
|
||||
# NOTE: executor won't let us raise until all futures that
|
||||
# it has are finished, so we need to cancel them ourselves
|
||||
# before re-raising.
|
||||
exc = future.exception()
|
||||
if exc:
|
||||
for entry in futures:
|
||||
entry.cancel()
|
||||
raise exc
|
||||
|
||||
def _download_file(self, from_info, to_info, name, no_progress_bar):
|
||||
makedirs(to_info.parent, exist_ok=True)
|
||||
|
||||
logger.debug("Downloading '%s' to '%s'", from_info, to_info)
|
||||
name = name or to_info.name
|
||||
def _download_file(
|
||||
self,
|
||||
from_info,
|
||||
to_info,
|
||||
callback=DEFAULT_CALLBACK,
|
||||
):
|
||||
from .local import localfs
|
||||
|
||||
makedirs(localfs.path.parent(to_info), exist_ok=True)
|
||||
tmp_file = tmp_fname(to_info)
|
||||
|
||||
self._download( # noqa, pylint: disable=no-member
|
||||
from_info, tmp_file, name=name, no_progress_bar=no_progress_bar
|
||||
)
|
||||
logger.debug("Downloading '%s' to '%s'", from_info, to_info)
|
||||
try:
|
||||
# noqa, pylint: disable=no-member
|
||||
self.get_file(from_info, tmp_file, callback=callback)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# do we need to rollback makedirs for previously not-existing
|
||||
# directories?
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
os.unlink(tmp_file)
|
||||
raise
|
||||
|
||||
move(tmp_file, to_info)
|
||||
|
||||
+68
-63
@@ -3,20 +3,20 @@ import os
|
||||
import typing
|
||||
|
||||
from dvc.exceptions import OutputNotFoundError
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.utils import relpath
|
||||
|
||||
from ..progress import DEFAULT_CALLBACK
|
||||
from ._metadata import Metadata
|
||||
from .base import BaseFileSystem
|
||||
from .base import FileSystem
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from dvc.output import Output
|
||||
|
||||
from dvc.types import AnyPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
class DvcFileSystem(FileSystem): # pylint:disable=abstract-method
|
||||
"""DVC repo fs.
|
||||
|
||||
Args:
|
||||
@@ -48,28 +48,25 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
return outs
|
||||
|
||||
def _get_granular_hash(
|
||||
self, path_info: PathInfo, out: "Output", remote=None
|
||||
):
|
||||
assert isinstance(path_info, PathInfo)
|
||||
def _get_granular_hash(self, path: "AnyPath", out: "Output", remote=None):
|
||||
# NOTE: use string paths here for performance reasons
|
||||
key = tuple(relpath(path_info, out.path_info).split(os.sep))
|
||||
key = tuple(relpath(path, out.fs_path).split(os.sep))
|
||||
out.get_dir_cache(remote=remote)
|
||||
if out.obj is None:
|
||||
raise FileNotFoundError
|
||||
obj = out.obj.trie.get(key)
|
||||
if obj:
|
||||
return obj.hash_info
|
||||
(_, oid) = out.obj.trie.get(key) or (None, None)
|
||||
if oid:
|
||||
return oid
|
||||
raise FileNotFoundError
|
||||
|
||||
def _get_fs_path(self, path: PathInfo, remote=None):
|
||||
def _get_fs_path(self, path: "AnyPath", remote=None):
|
||||
try:
|
||||
outs = self._find_outs(path, strict=False)
|
||||
except OutputNotFoundError as exc:
|
||||
raise FileNotFoundError from exc
|
||||
|
||||
if len(outs) != 1 or (
|
||||
outs[0].is_dir_checksum and path == outs[0].path_info
|
||||
outs[0].is_dir_checksum and path == outs[0].fs_path
|
||||
):
|
||||
raise IsADirectoryError
|
||||
|
||||
@@ -89,18 +86,20 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
checksum = self._get_granular_hash(path, out).value
|
||||
else:
|
||||
checksum = out.hash_info.value
|
||||
remote_info = remote_odb.hash_to_path_info(checksum)
|
||||
return remote_odb.fs, remote_info
|
||||
remote_fs_path = remote_odb.hash_to_path(checksum)
|
||||
return remote_odb.fs, remote_fs_path
|
||||
|
||||
if out.is_dir_checksum:
|
||||
checksum = self._get_granular_hash(path, out).value
|
||||
cache_path = out.odb.hash_to_path_info(checksum).url
|
||||
cache_path = out.odb.fs.unstrip_protocol(
|
||||
out.odb.hash_to_path(checksum)
|
||||
)
|
||||
else:
|
||||
cache_path = out.cache_path
|
||||
return out.odb.fs, cache_path
|
||||
|
||||
def open( # type: ignore
|
||||
self, path: PathInfo, mode="r", encoding=None, **kwargs
|
||||
self, path: str, mode="r", encoding=None, **kwargs
|
||||
): # pylint: disable=arguments-renamed
|
||||
fs, fspath = self._get_fs_path(path, **kwargs)
|
||||
return fs.open(fspath, mode=mode, encoding=encoding)
|
||||
@@ -119,18 +118,18 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def check_isdir(self, path_info, outs):
|
||||
def check_isdir(self, path, outs):
|
||||
if len(outs) != 1:
|
||||
return True
|
||||
|
||||
out = outs[0]
|
||||
if not out.is_dir_checksum:
|
||||
return out.path_info != path_info
|
||||
if out.path_info == path_info:
|
||||
return out.fs_path != path
|
||||
if out.fs_path == path:
|
||||
return True
|
||||
|
||||
try:
|
||||
self._get_granular_hash(path_info, out)
|
||||
self._get_granular_hash(path, out)
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
return True
|
||||
@@ -152,42 +151,46 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
def _add_dir(self, trie, out, **kwargs):
|
||||
self._fetch_dir(out, **kwargs)
|
||||
|
||||
base = out.path_info.parts
|
||||
for key, _ in out.obj: # noqa: B301
|
||||
base = out.fs.path.parts(out.fs_path)
|
||||
for key, _, _ in out.obj: # noqa: B301
|
||||
trie[base + key] = None
|
||||
|
||||
def _walk(self, root, trie, topdown=True, **kwargs):
|
||||
dirs = set()
|
||||
files = []
|
||||
|
||||
out = trie.get(root.parts)
|
||||
root_parts = self.path.parts(root)
|
||||
out = trie.get(root_parts)
|
||||
if out and out.is_dir_checksum:
|
||||
self._add_dir(trie, out, **kwargs)
|
||||
|
||||
root_len = len(root.parts)
|
||||
for key, out in trie.iteritems(prefix=root.parts): # noqa: B301
|
||||
if key == root.parts:
|
||||
continue
|
||||
root_len = len(root_parts)
|
||||
try:
|
||||
for key, out in trie.iteritems(prefix=root_parts): # noqa: B301
|
||||
if key == root_parts:
|
||||
continue
|
||||
|
||||
name = key[root_len]
|
||||
if len(key) > root_len + 1 or (out and out.is_dir_checksum):
|
||||
dirs.add(name)
|
||||
continue
|
||||
name = key[root_len]
|
||||
if len(key) > root_len + 1 or (out and out.is_dir_checksum):
|
||||
dirs.add(name)
|
||||
continue
|
||||
|
||||
files.append(name)
|
||||
files.append(name)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
assert topdown
|
||||
dirs = list(dirs)
|
||||
yield root.fspath, dirs, files
|
||||
yield root, dirs, files
|
||||
|
||||
for dname in dirs:
|
||||
yield from self._walk(root / dname, trie)
|
||||
yield from self._walk(self.path.join(root, dname), trie)
|
||||
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
from pygtrie import Trie
|
||||
|
||||
assert topdown
|
||||
root = PathInfo(os.path.abspath(top))
|
||||
root = os.path.abspath(top)
|
||||
try:
|
||||
meta = self.metadata(root)
|
||||
except FileNotFoundError:
|
||||
@@ -202,18 +205,18 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
trie = Trie()
|
||||
for out in meta.outs:
|
||||
trie[out.path_info.parts] = out
|
||||
trie[out.fs.path.parts(out.fs_path)] = out
|
||||
|
||||
if out.is_dir_checksum and root.isin_or_eq(out.path_info):
|
||||
if out.is_dir_checksum and self.path.isin_or_eq(root, out.fs_path):
|
||||
self._add_dir(trie, out, **kwargs)
|
||||
|
||||
yield from self._walk(root, trie, topdown=topdown, **kwargs)
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for root, _, files in self.walk(path_info):
|
||||
def find(self, path, prefix=None):
|
||||
for root, _, files in self.walk(path):
|
||||
for fname in files:
|
||||
# NOTE: os.path.join is ~5.5 times slower
|
||||
yield PathInfo(f"{root}{os.sep}{fname}")
|
||||
yield f"{root}{os.sep}{fname}"
|
||||
|
||||
def isdvc(self, path, recursive=False, strict=True):
|
||||
try:
|
||||
@@ -224,46 +227,48 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
recurse = recursive or not strict
|
||||
return meta.output_exists if recurse else meta.is_output
|
||||
|
||||
def isexec(self, path_info): # pylint: disable=unused-argument
|
||||
def isexec(self, path): # pylint: disable=unused-argument
|
||||
return False
|
||||
|
||||
def metadata(self, path_info):
|
||||
path_info = PathInfo(os.path.abspath(path_info))
|
||||
def metadata(self, fs_path):
|
||||
abspath = os.path.abspath(fs_path)
|
||||
|
||||
try:
|
||||
outs = self._find_outs(path_info, strict=False, recursive=True)
|
||||
outs = self._find_outs(abspath, strict=False, recursive=True)
|
||||
except OutputNotFoundError as exc:
|
||||
raise FileNotFoundError from exc
|
||||
|
||||
meta = Metadata(path_info=path_info, outs=outs, repo=self.repo)
|
||||
meta.isdir = meta.isdir or self.check_isdir(meta.path_info, meta.outs)
|
||||
meta = Metadata(fs_path=abspath, outs=outs, repo=self.repo)
|
||||
meta.isdir = meta.isdir or self.check_isdir(meta.fs_path, meta.outs)
|
||||
return meta
|
||||
|
||||
def info(self, path_info):
|
||||
meta = self.metadata(path_info)
|
||||
def info(self, path):
|
||||
meta = self.metadata(path)
|
||||
ret = {"type": "directory" if meta.isdir else "file"}
|
||||
if meta.is_output and len(meta.outs) == 1 and meta.outs[0].hash_info:
|
||||
hash_info = meta.outs[0].hash_info
|
||||
ret["size"] = hash_info.size
|
||||
ret[hash_info.name] = hash_info.value
|
||||
out = meta.outs[0]
|
||||
ret["size"] = out.meta.size
|
||||
ret[out.hash_info.name] = out.hash_info.value
|
||||
elif meta.part_of_output:
|
||||
(out,) = meta.outs
|
||||
key = path_info.relative_to(out.path_info).parts
|
||||
obj = out.obj.trie.get(key)
|
||||
if obj:
|
||||
ret["size"] = obj.size
|
||||
ret[obj.hash_info.name] = obj.hash_info.value
|
||||
key = self.path.parts(self.path.relpath(path, out.fs_path))
|
||||
(obj_meta, oid) = out.obj.trie.get(key) or (None, None)
|
||||
if oid:
|
||||
ret["size"] = obj_meta.size if obj_meta else 0
|
||||
ret[oid.name] = oid.value
|
||||
|
||||
return ret
|
||||
|
||||
def _download(self, from_info, to_file, **kwargs):
|
||||
def get_file(
|
||||
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
fs, path = self._get_fs_path(from_info)
|
||||
fs._download( # pylint: disable=protected-access
|
||||
path, to_file, **kwargs
|
||||
fs.get_file( # pylint: disable=protected-access
|
||||
path, to_file, callback=callback, **kwargs
|
||||
)
|
||||
|
||||
def checksum(self, path_info):
|
||||
info = self.info(path_info)
|
||||
def checksum(self, path):
|
||||
info = self.info(path)
|
||||
md5 = info.get("md5")
|
||||
if md5:
|
||||
return md5
|
||||
|
||||
+129
-181
@@ -1,17 +1,28 @@
|
||||
import os
|
||||
import shutil
|
||||
from functools import lru_cache
|
||||
from typing import IO, TYPE_CHECKING, Any, Dict, Iterator, Optional, overload
|
||||
|
||||
from funcy import cached_property
|
||||
from tqdm.utils import CallbackIOWrapper
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.progress import DEFAULT_CALLBACK
|
||||
|
||||
from .base import BaseFileSystem
|
||||
from .local import LocalFileSystem
|
||||
from .base import FileSystem
|
||||
|
||||
FSPath = str
|
||||
AnyFSPath = str
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
# An info() entry, might evolve to a TypedDict
|
||||
# in the future (e.g for properly type 'size' etc).
|
||||
Entry = Dict[str, Any]
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
class FSSpecWrapper(BaseFileSystem):
|
||||
class FSSpecWrapper(FileSystem):
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -19,18 +30,8 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
self.fs_args = {"skip_instance_cache": True}
|
||||
self.fs_args.update(self._prepare_credentials(**kwargs))
|
||||
|
||||
@cached_property
|
||||
def fs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _with_bucket(self, path):
|
||||
return str(path)
|
||||
|
||||
def _strip_bucket(self, entry):
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
def _get_kwargs_from_urls(urlpath: str) -> "Dict[str, Any]":
|
||||
from fsspec.utils import infer_storage_options
|
||||
|
||||
options = infer_storage_options(urlpath)
|
||||
@@ -38,158 +39,117 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
options.pop("protocol", None)
|
||||
return options
|
||||
|
||||
def _strip_buckets(self, entries, detail=False):
|
||||
for entry in entries:
|
||||
if detail:
|
||||
entry = self._entry_hook(entry.copy())
|
||||
entry["name"] = self._strip_bucket(entry["name"])
|
||||
else:
|
||||
entry = self._strip_bucket(entry)
|
||||
yield entry
|
||||
|
||||
def _entry_hook(self, entry):
|
||||
"""Simple hook method to be overridden when wanted to process
|
||||
entries within info() and ls(detail=True) calls"""
|
||||
return entry
|
||||
@cached_property
|
||||
def fs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _prepare_credentials(
|
||||
self, **config
|
||||
): # pylint: disable=unused-argument
|
||||
self, **config: Dict[str, Any] # pylint: disable=unused-argument
|
||||
) -> Dict[str, Any]:
|
||||
"""Prepare the arguments for authentication to the
|
||||
host filesystem"""
|
||||
return {}
|
||||
|
||||
def _isdir(self, path_info):
|
||||
return self.fs.isdir(self._with_bucket(path_info))
|
||||
def _isdir(self, path: AnyFSPath) -> bool:
|
||||
return self.fs.isdir(path)
|
||||
|
||||
def isdir(self, path_info):
|
||||
def isdir(self, path: AnyFSPath) -> bool:
|
||||
try:
|
||||
return self._isdir(path_info)
|
||||
return self._isdir(path)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def isfile(self, path_info):
|
||||
def isfile(self, path: AnyFSPath) -> bool:
|
||||
try:
|
||||
return not self._isdir(path_info)
|
||||
return not self._isdir(path)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def is_empty(self, path_info):
|
||||
entry = self.info(path_info)
|
||||
def is_empty(self, path: AnyFSPath) -> bool:
|
||||
entry = self.info(path)
|
||||
if entry["type"] == "directory":
|
||||
return not self.fs.ls(self._with_bucket(path_info))
|
||||
return not self.fs.ls(path)
|
||||
return entry["size"] == 0
|
||||
|
||||
def open(
|
||||
self, path_info, mode="r", **kwargs
|
||||
): # pylint: disable=arguments-differ
|
||||
return self.fs.open(self._with_bucket(path_info), mode=mode)
|
||||
self,
|
||||
path: AnyFSPath,
|
||||
mode: str = "r",
|
||||
encoding: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> "IO": # pylint: disable=arguments-differ
|
||||
return self.fs.open(path, mode=mode)
|
||||
|
||||
def checksum(self, path_info):
|
||||
return self.fs.checksum(self._with_bucket(path_info))
|
||||
def checksum(self, path: AnyFSPath) -> str:
|
||||
return self.fs.checksum(path)
|
||||
|
||||
def copy(self, from_info, to_info):
|
||||
self.makedirs(to_info.parent)
|
||||
self.fs.copy(self._with_bucket(from_info), self._with_bucket(to_info))
|
||||
def copy(self, from_info: AnyFSPath, to_info: AnyFSPath) -> None:
|
||||
self.makedirs(self.path.parent(to_info))
|
||||
self.fs.copy(from_info, to_info)
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
return self.fs.exists(self._with_bucket(path_info))
|
||||
def exists(self, path: AnyFSPath) -> bool:
|
||||
return self.fs.exists(path)
|
||||
|
||||
def ls(self, path_info, detail=False):
|
||||
path = self._with_bucket(path_info)
|
||||
files = self.fs.ls(path, detail=detail)
|
||||
yield from self._strip_buckets(files, detail=detail)
|
||||
@overload
|
||||
def ls(
|
||||
self, path: AnyFSPath, detail: "Literal[True]"
|
||||
) -> "Iterator[Entry]":
|
||||
...
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def find(self, path_info, detail=False, prefix=None):
|
||||
path = self._with_bucket(path_info)
|
||||
files = self.fs.find(path, detail=detail)
|
||||
if detail:
|
||||
files = files.values()
|
||||
@overload
|
||||
def ls(self, path: AnyFSPath, detail: "Literal[False]") -> Iterator[str]:
|
||||
...
|
||||
|
||||
yield from self._strip_buckets(files, detail=detail)
|
||||
def ls(self, path, detail=False):
|
||||
yield from self.fs.ls(path, detail=detail)
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for file in self.find(path_info, **kwargs):
|
||||
yield path_info.replace(path=file)
|
||||
def find(self, path, prefix=None):
|
||||
yield from self.fs.find(path)
|
||||
|
||||
def move(self, from_info, to_info):
|
||||
self.fs.move(self._with_bucket(from_info), self._with_bucket(to_info))
|
||||
def move(self, from_info: AnyFSPath, to_info: AnyFSPath) -> None:
|
||||
self.fs.move(from_info, to_info)
|
||||
|
||||
def remove(self, path_info):
|
||||
self.fs.rm_file(self._with_bucket(path_info))
|
||||
def remove(self, path: AnyFSPath) -> None:
|
||||
self.fs.rm_file(path)
|
||||
|
||||
def info(self, path_info):
|
||||
info = self.fs.info(self._with_bucket(path_info)).copy()
|
||||
info = self._entry_hook(info)
|
||||
info["name"] = self._strip_bucket(info["name"])
|
||||
return info
|
||||
def info(self, path: AnyFSPath) -> "Entry":
|
||||
return self.fs.info(path)
|
||||
|
||||
def makedirs(self, path_info, **kwargs):
|
||||
self.fs.makedirs(
|
||||
self._with_bucket(path_info), exist_ok=kwargs.pop("exist_ok", True)
|
||||
)
|
||||
def makedirs(self, path: AnyFSPath, **kwargs) -> None:
|
||||
self.fs.makedirs(path, exist_ok=kwargs.pop("exist_ok", True))
|
||||
|
||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
||||
self.makedirs(to_info.parent)
|
||||
def put_file(
|
||||
self,
|
||||
from_file: AnyFSPath,
|
||||
to_info: AnyFSPath,
|
||||
callback: Any = DEFAULT_CALLBACK,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.fs.put_file(from_file, to_info, callback=callback, **kwargs)
|
||||
self.fs.invalidate_cache(self.path.parent(to_info))
|
||||
|
||||
def get_file(
|
||||
self,
|
||||
from_info: AnyFSPath,
|
||||
to_info: AnyFSPath,
|
||||
callback: Any = DEFAULT_CALLBACK,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.fs.get_file(from_info, to_info, callback=callback, **kwargs)
|
||||
|
||||
def upload_fobj(self, fobj: IO, to_info: AnyFSPath, **kwargs) -> None:
|
||||
self.makedirs(self.path.parent(to_info))
|
||||
with self.open(to_info, "wb") as fdest:
|
||||
shutil.copyfileobj(fobj, fdest, length=fdest.blocksize)
|
||||
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **kwargs
|
||||
):
|
||||
self.makedirs(to_info.parent)
|
||||
size = os.path.getsize(from_file)
|
||||
with open(from_file, "rb") as fobj:
|
||||
with Tqdm.wrapattr(
|
||||
fobj,
|
||||
"read",
|
||||
disable=no_progress_bar,
|
||||
bytes=True,
|
||||
total=size,
|
||||
desc=name,
|
||||
) as wrapped:
|
||||
self.upload_fobj(wrapped, to_info, size=size)
|
||||
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
|
||||
|
||||
def _download(
|
||||
self, from_info, to_file, name=None, no_progress_bar=False, **pbar_args
|
||||
):
|
||||
total = self.getsize(self._with_bucket(from_info))
|
||||
with self.open(from_info, "rb") as fobj:
|
||||
with Tqdm.wrapattr(
|
||||
fobj,
|
||||
"read",
|
||||
desc=name,
|
||||
disable=no_progress_bar,
|
||||
bytes=True,
|
||||
total=total,
|
||||
**pbar_args,
|
||||
) as wrapped:
|
||||
with open(to_file, "wb") as fdest:
|
||||
shutil.copyfileobj(wrapped, fdest, length=fobj.blocksize)
|
||||
shutil.copyfileobj(
|
||||
fobj, fdest, length=getattr(fdest, "blocksize", None)
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class ObjectFSWrapper(FSSpecWrapper):
|
||||
TRAVERSE_PREFIX_LEN = 3
|
||||
|
||||
@lru_cache(512)
|
||||
def _with_bucket(self, path):
|
||||
if isinstance(path, self.PATH_CLS):
|
||||
return f"{path.bucket}/{path.path}"
|
||||
return path
|
||||
|
||||
def _strip_bucket(self, entry):
|
||||
try:
|
||||
bucket, path = entry.split("/", 1)
|
||||
except ValueError:
|
||||
# If there is no path attached, only returns
|
||||
# the bucket (top-level).
|
||||
bucket, path = entry, None
|
||||
return path or bucket
|
||||
|
||||
def makedirs(self, path_info, **kwargs):
|
||||
def makedirs(self, path: AnyFSPath, **kwargs) -> None:
|
||||
# For object storages make this method a no-op. The original
|
||||
# fs.makedirs() method will only check if the bucket exists
|
||||
# and create if it doesn't though we don't want to support
|
||||
@@ -197,42 +157,37 @@ class ObjectFSWrapper(FSSpecWrapper):
|
||||
# simply ignore all mkdir()/makedirs() calls.
|
||||
return None
|
||||
|
||||
def _isdir(self, path_info):
|
||||
def _isdir(self, path: AnyFSPath) -> bool:
|
||||
# Directory in object storages are interpreted differently
|
||||
# among different fsspec providers, so this logic is a temporary
|
||||
# measure for us to adapt as of now. It checks whether it is a
|
||||
# directory (as in a prefix with contents) or whether it is an empty
|
||||
# file where it's name ends with a forward slash
|
||||
|
||||
entry = self.info(path_info)
|
||||
entry = self.info(path)
|
||||
return entry["type"] == "directory" or (
|
||||
entry["size"] == 0
|
||||
and entry["type"] == "file"
|
||||
and entry["name"].endswith("/")
|
||||
)
|
||||
|
||||
def find(self, path_info, detail=False, prefix=None):
|
||||
def find(self, path, prefix=None):
|
||||
if prefix:
|
||||
path = self._with_bucket(path_info.parent)
|
||||
files = self.fs.find(
|
||||
path, detail=detail, prefix=path_info.parts[-1]
|
||||
)
|
||||
with_prefix = self.path.parent(path)
|
||||
files = self.fs.find(with_prefix, prefix=self.path.parts(path)[-1])
|
||||
else:
|
||||
path = self._with_bucket(path_info)
|
||||
files = self.fs.find(path, detail=detail)
|
||||
|
||||
if detail:
|
||||
files = list(files.values())
|
||||
with_prefix = path
|
||||
files = self.fs.find(path)
|
||||
|
||||
# When calling find() on a file, it returns the same file in a list.
|
||||
# For object-based storages, the same behavior applies to empty
|
||||
# directories since they are represented as files. This condition
|
||||
# checks whether we should yield an empty list (if it is an empty
|
||||
# directory) or just yield the file itself.
|
||||
if len(files) == 1 and files[0] == path and self.isdir(path_info):
|
||||
if len(files) == 1 and files[0] == with_prefix and self.isdir(path):
|
||||
return None
|
||||
|
||||
yield from self._strip_buckets(files, detail=detail)
|
||||
yield from files
|
||||
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@@ -249,49 +204,42 @@ class NoDirectoriesMixin:
|
||||
def walk(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def walk_files(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def ls(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
_LOCAL_FS = LocalFileSystem()
|
||||
|
||||
|
||||
class CallbackMixin:
|
||||
"""Use the native ``get_file()``/``put_file()`` APIs
|
||||
if the target filesystem supports callbacks."""
|
||||
"""Provides callback support for the filesystem that don't support yet."""
|
||||
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **pbar_args
|
||||
def put_file(
|
||||
self,
|
||||
from_file,
|
||||
to_info,
|
||||
callback=DEFAULT_CALLBACK,
|
||||
**kwargs,
|
||||
):
|
||||
with Tqdm(
|
||||
desc=name,
|
||||
disable=no_progress_bar,
|
||||
bytes=True,
|
||||
total=-1,
|
||||
**pbar_args,
|
||||
) as pbar:
|
||||
self.fs.put_file(
|
||||
os.fspath(from_file),
|
||||
self._with_bucket(to_info),
|
||||
callback=pbar.as_callback(_LOCAL_FS, from_file),
|
||||
)
|
||||
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
|
||||
"""Add compatibility support for Callback."""
|
||||
# pylint: disable=protected-access
|
||||
self.makedirs(self.path.parent(to_info))
|
||||
size = os.path.getsize(from_file)
|
||||
with open(from_file, "rb") as fobj:
|
||||
callback.set_size(size)
|
||||
wrapped = CallbackIOWrapper(callback.relative_update, fobj)
|
||||
self.upload_fobj(wrapped, to_info)
|
||||
self.fs.invalidate_cache(self.path.parent(to_info))
|
||||
|
||||
def _download(
|
||||
self, from_info, to_file, name=None, no_progress_bar=False, **pbar_args
|
||||
def get_file(
|
||||
self,
|
||||
from_info,
|
||||
to_info,
|
||||
callback=DEFAULT_CALLBACK,
|
||||
**kwargs,
|
||||
):
|
||||
with Tqdm(
|
||||
desc=name,
|
||||
disable=no_progress_bar,
|
||||
bytes=True,
|
||||
total=-1,
|
||||
**pbar_args,
|
||||
) as pbar:
|
||||
self.fs.get_file(
|
||||
self._with_bucket(from_info),
|
||||
os.fspath(to_file),
|
||||
callback=pbar.as_callback(self, from_info),
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
total: int = self.getsize(from_info)
|
||||
if total:
|
||||
callback.set_size(total)
|
||||
|
||||
with self.open(from_info, "rb") as fobj, open(to_info, "wb") as fdest:
|
||||
wrapped = CallbackIOWrapper(callback.relative_update, fobj)
|
||||
shutil.copyfileobj(wrapped, fdest, length=fobj.blocksize)
|
||||
|
||||
+25
-70
@@ -1,18 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import threading
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import cached_property, retry, wrap_prop
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.utils import format_link, tmp_fname
|
||||
|
||||
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
|
||||
from .fsspec_wrapper import FSSpecWrapper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"
|
||||
@@ -34,56 +30,8 @@ class GDriveAuthError(DvcException):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def _gdrive_retry(func):
|
||||
def should_retry(exc):
|
||||
from pydrive2.files import ApiRequestError
|
||||
|
||||
if not isinstance(exc, ApiRequestError):
|
||||
return False
|
||||
|
||||
error_code = exc.error.get("code", 0)
|
||||
result = False
|
||||
if 500 <= error_code < 600:
|
||||
result = True
|
||||
|
||||
if error_code == 403:
|
||||
result = exc.GetField("reason") in [
|
||||
"userRateLimitExceeded",
|
||||
"rateLimitExceeded",
|
||||
]
|
||||
if result:
|
||||
logger.debug(f"Retrying GDrive API call, error: {exc}.")
|
||||
|
||||
return result
|
||||
|
||||
# 16 tries, start at 0.5s, multiply by golden ratio, cap at 20s
|
||||
return retry(
|
||||
16,
|
||||
timeout=lambda a: min(0.5 * 1.618 ** a, 20),
|
||||
filter_errors=should_retry,
|
||||
)(func)
|
||||
|
||||
|
||||
class GDriveURLInfo(CloudURLInfo):
|
||||
def __init__(self, url):
|
||||
super().__init__(url)
|
||||
|
||||
# GDrive URL host part is case sensitive,
|
||||
# we are restoring it here.
|
||||
p = urlparse(url)
|
||||
self.host = p.netloc
|
||||
assert self.netloc == self.host
|
||||
|
||||
# Normalize path. Important since we have a cache (path to ID)
|
||||
# and don't want to deal with different variations of path in it.
|
||||
self._spath = re.sub("/{2,}", "/", self._spath.rstrip("/"))
|
||||
|
||||
|
||||
class GDriveFileSystem(
|
||||
CallbackMixin, FSSpecWrapper
|
||||
): # pylint:disable=abstract-method
|
||||
class GDriveFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
||||
scheme = Schemes.GDRIVE
|
||||
PATH_CLS = GDriveURLInfo
|
||||
PARAM_CHECKSUM = "checksum"
|
||||
REQUIRES = {"pydrive2": "pydrive2"}
|
||||
# Always prefer traverse for GDrive since API usage quotas are a concern.
|
||||
@@ -97,11 +45,14 @@ class GDriveFileSystem(
|
||||
DEFAULT_GDRIVE_CLIENT_SECRET = "a1Fz59uTpVNeG_VGuSKDLJXv"
|
||||
|
||||
def __init__(self, **config):
|
||||
from fsspec.utils import infer_storage_options
|
||||
|
||||
super().__init__(**config)
|
||||
|
||||
self.path_info = self.PATH_CLS(config["url"])
|
||||
self.url = config["url"]
|
||||
opts = infer_storage_options(self.url)
|
||||
|
||||
if not self.path_info.bucket:
|
||||
if not opts["host"]:
|
||||
raise DvcException(
|
||||
"Empty GDrive URL '{}'. Learn more at {}".format(
|
||||
config["url"],
|
||||
@@ -109,8 +60,8 @@ class GDriveFileSystem(
|
||||
)
|
||||
)
|
||||
|
||||
self._bucket = self.path_info.bucket
|
||||
self._path = self.path_info.path
|
||||
self._bucket = opts["host"]
|
||||
self._path = opts["path"].lstrip("/")
|
||||
self._trash_only = config.get("gdrive_trash_only")
|
||||
self._use_service_account = config.get("gdrive_use_service_account")
|
||||
self._service_account_user_email = config.get(
|
||||
@@ -138,6 +89,17 @@ class GDriveFileSystem(
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path):
|
||||
from fsspec.utils import infer_storage_options
|
||||
|
||||
opts = infer_storage_options(path)
|
||||
|
||||
return "{host}{path}".format(**opts)
|
||||
|
||||
def unstrip_protocol(self, path):
|
||||
return f"gdrive://{path}"
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
return {"url": urlpath}
|
||||
@@ -221,7 +183,7 @@ class GDriveFileSystem(
|
||||
temporary_save_path = self._gdrive_service_credentials_path
|
||||
|
||||
if is_credentials_temp:
|
||||
with open(temporary_save_path, "w") as cred_file:
|
||||
with open(temporary_save_path, "w", encoding="utf-8") as cred_file:
|
||||
cred_file.write(
|
||||
os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA)
|
||||
)
|
||||
@@ -287,18 +249,11 @@ class GDriveFileSystem(
|
||||
os.remove(temporary_save_path)
|
||||
|
||||
return _GDriveFileSystem(
|
||||
self._with_bucket(self.path_info),
|
||||
f"{self._bucket}/{self._path}",
|
||||
gauth,
|
||||
trash_only=self._trash_only,
|
||||
)
|
||||
|
||||
def _with_bucket(self, path):
|
||||
if isinstance(path, self.PATH_CLS):
|
||||
return posixpath.join(path.bucket, path.path)
|
||||
|
||||
return super()._with_bucket(path)
|
||||
|
||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
||||
rpath = self._with_bucket(to_info)
|
||||
self.makedirs(os.path.dirname(rpath))
|
||||
return self.fs.upload_fobj(fobj, rpath, **kwargs)
|
||||
self.makedirs(os.path.dirname(to_info))
|
||||
return self.fs.upload_fobj(fobj, to_info, **kwargs)
|
||||
|
||||
+52
-111
@@ -1,131 +1,72 @@
|
||||
import errno
|
||||
import os
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from dvc.utils import is_exec, relpath
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from .base import BaseFileSystem
|
||||
from .fsspec_wrapper import AnyFSPath, FSSpecWrapper
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.fs import GitFileSystem as FsspecGitFileSystem
|
||||
from scmrepo.git import Git
|
||||
from scmrepo.git.objects import GitTrie
|
||||
|
||||
|
||||
class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
class GitFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
||||
"""Proxies the repo file access methods to Git objects"""
|
||||
|
||||
sep = os.sep
|
||||
|
||||
scheme = "local"
|
||||
|
||||
def __init__(self, root_dir, trie):
|
||||
def __init__(
|
||||
self,
|
||||
path: str = None,
|
||||
rev: str = None,
|
||||
scm: "Git" = None,
|
||||
trie: "GitTrie" = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
super().__init__()
|
||||
self._root = root_dir
|
||||
self.trie = trie
|
||||
self.fs_args.update(
|
||||
{
|
||||
"path": path,
|
||||
"rev": rev,
|
||||
"scm": scm,
|
||||
"trie": trie,
|
||||
"rev_resolver": resolve_rev,
|
||||
**kwargs,
|
||||
}
|
||||
)
|
||||
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self) -> "FsspecGitFileSystem":
|
||||
from scmrepo.fs import GitFileSystem as FsspecGitFileSystem
|
||||
|
||||
return FsspecGitFileSystem(**self.fs_args)
|
||||
|
||||
@property
|
||||
def rev(self):
|
||||
return self.trie.rev
|
||||
def rev(self) -> str:
|
||||
return self.fs.rev
|
||||
|
||||
def _get_key(self, path):
|
||||
if isinstance(path, str):
|
||||
if not os.path.isabs(path):
|
||||
relparts = path.split(os.sep)
|
||||
else:
|
||||
relparts = relpath(path, self._root).split(os.sep)
|
||||
else:
|
||||
relparts = path.relative_to(self._root).parts
|
||||
if relparts == ["."]:
|
||||
return ()
|
||||
return tuple(relparts)
|
||||
def isexec(self, path: AnyFSPath) -> bool:
|
||||
from dvc.utils import is_exec
|
||||
|
||||
def open(
|
||||
self, path, mode="r", encoding=None, **kwargs
|
||||
): # pylint: disable=arguments-renamed
|
||||
# NOTE: this is incorrect, we need to use locale to determine default
|
||||
# encoding.
|
||||
encoding = encoding or "utf-8"
|
||||
|
||||
key = self._get_key(path)
|
||||
try:
|
||||
return self.trie.open(key, mode=mode, encoding=encoding)
|
||||
except KeyError as exc:
|
||||
msg = os.strerror(errno.ENOENT) + f"in branch '{self.rev}'"
|
||||
raise FileNotFoundError(errno.ENOENT, msg, path) from exc
|
||||
except IsADirectoryError as exc:
|
||||
raise IsADirectoryError(
|
||||
errno.EISDIR, os.strerror(errno.EISDIR), path
|
||||
) from exc
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
key = self._get_key(path_info)
|
||||
return self.trie.exists(key)
|
||||
|
||||
def isdir(self, path_info) -> bool:
|
||||
key = self._get_key(path_info)
|
||||
return self.trie.isdir(key)
|
||||
|
||||
def isfile(self, path_info) -> bool:
|
||||
key = self._get_key(path_info)
|
||||
return self.trie.isfile(key)
|
||||
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
"""Directory tree generator.
|
||||
|
||||
See `os.walk` for the docs. Differences:
|
||||
- no support for symlinks
|
||||
"""
|
||||
if not self.isdir(top):
|
||||
if onerror:
|
||||
if self.exists(top):
|
||||
exc = NotADirectoryError(
|
||||
errno.ENOTDIR, os.strerror(errno.ENOTDIR), top
|
||||
)
|
||||
else:
|
||||
exc = FileNotFoundError(
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), top
|
||||
)
|
||||
onerror(exc)
|
||||
return []
|
||||
|
||||
key = self._get_key(top)
|
||||
for prefix, dirs, files in self.trie.walk(key, topdown=topdown):
|
||||
if prefix:
|
||||
root = os.path.join(self._root, os.sep.join(prefix))
|
||||
else:
|
||||
root = self._root
|
||||
yield root, dirs, files
|
||||
|
||||
def isexec(self, path_info):
|
||||
if not self.exists(path_info):
|
||||
return is_exec(self.info(path)["mode"])
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
mode = self.info(path_info)["mode"]
|
||||
return is_exec(mode)
|
||||
def isfile(self, path: AnyFSPath) -> bool:
|
||||
return self.fs.isfile(path)
|
||||
|
||||
def info(self, path_info):
|
||||
key = self._get_key(path_info)
|
||||
try:
|
||||
return self.trie.info(key)
|
||||
except KeyError:
|
||||
raise FileNotFoundError(
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), path_info
|
||||
)
|
||||
|
||||
def checksum(self, path_info):
|
||||
return self.info(path_info)["sha"]
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for root, _, files in self.walk(path_info, **kwargs):
|
||||
for file in files:
|
||||
# NOTE: os.path.join is ~5.5 times slower
|
||||
yield f"{root}{os.sep}{file}"
|
||||
|
||||
def _download(
|
||||
self, from_info, to_file, name=None, no_progress_bar=False, **kwargs
|
||||
def walk(
|
||||
self,
|
||||
top: AnyFSPath,
|
||||
topdown: bool = True,
|
||||
onerror: Callable[[OSError], None] = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
import shutil
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
with open(to_file, "wb+") as to_fobj:
|
||||
with Tqdm.wrapattr(
|
||||
to_fobj, "write", desc=name, disable=no_progress_bar
|
||||
) as wrapped:
|
||||
with self.open(from_info, "rb", **kwargs) as from_fobj:
|
||||
shutil.copyfileobj(from_fobj, wrapped)
|
||||
return self.fs.walk(top, topdown=topdown, onerror=onerror, **kwargs)
|
||||
|
||||
+4
-13
@@ -1,22 +1,18 @@
|
||||
import base64
|
||||
import threading
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .fsspec_wrapper import ObjectFSWrapper
|
||||
|
||||
|
||||
# pylint:disable=abstract-method
|
||||
class GSFileSystem(ObjectFSWrapper):
|
||||
from .fsspec_wrapper import CallbackMixin, ObjectFSWrapper
|
||||
|
||||
|
||||
class GSFileSystem(CallbackMixin, ObjectFSWrapper):
|
||||
scheme = Schemes.GS
|
||||
PATH_CLS = CloudURLInfo
|
||||
REQUIRES = {"gcsfs": "gcsfs"}
|
||||
PARAM_CHECKSUM = "etag"
|
||||
DETAIL_FIELDS = frozenset(("etag", "size"))
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
login_info = {"consistency": None}
|
||||
@@ -24,11 +20,6 @@ class GSFileSystem(ObjectFSWrapper):
|
||||
login_info["token"] = config.get("credentialpath")
|
||||
return login_info
|
||||
|
||||
def _entry_hook(self, entry):
|
||||
if "etag" in entry:
|
||||
entry["etag"] = base64.b64decode(entry["etag"]).hex()
|
||||
return entry
|
||||
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self):
|
||||
|
||||
+69
-255
@@ -1,285 +1,99 @@
|
||||
import errno
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections import deque
|
||||
from contextlib import closing, contextmanager
|
||||
import threading
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.utils import fix_env, tmp_fname
|
||||
from dvc.utils import fix_env
|
||||
|
||||
from .base import BaseFileSystem, RemoteCmdError
|
||||
from .pool import get_connection
|
||||
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
CHECKSUM_REGEX = re.compile(r".*\t.*\t(?P<checksum>.*)")
|
||||
|
||||
|
||||
def _hadoop_fs(cmd, user=None):
|
||||
cmd = "hadoop fs -" + cmd
|
||||
if user:
|
||||
cmd = f"HADOOP_USER_NAME={user} " + cmd
|
||||
|
||||
# NOTE: close_fds doesn't work with redirected stdin/stdout/stderr.
|
||||
# See https://github.com/iterative/dvc/issues/1197.
|
||||
close_fds = os.name != "nt"
|
||||
|
||||
executable = os.getenv("SHELL") if os.name != "nt" else None
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
shell=True,
|
||||
close_fds=close_fds,
|
||||
executable=executable,
|
||||
env=fix_env(os.environ),
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise RemoteCmdError("hdfs", cmd, p.returncode, err)
|
||||
return out.decode("utf-8")
|
||||
|
||||
|
||||
def _group(regex, s, gname):
|
||||
match = re.match(regex, s)
|
||||
assert match is not None
|
||||
return match.group(gname)
|
||||
|
||||
|
||||
def _hadoop_fs_checksum(path_info):
|
||||
# NOTE: pyarrow doesn't support checksum, so we need to use hadoop
|
||||
regex = r".*\t.*\t(?P<checksum>.*)"
|
||||
stdout = _hadoop_fs(f"checksum {path_info.url}", user=path_info.user)
|
||||
return _group(regex, stdout, "checksum")
|
||||
|
||||
|
||||
class HDFSFileSystem(BaseFileSystem):
|
||||
# pylint: disable=abstract-method
|
||||
class HDFSFileSystem(CallbackMixin, FSSpecWrapper):
|
||||
scheme = Schemes.HDFS
|
||||
REQUIRES = {"pyarrow": "pyarrow"}
|
||||
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
|
||||
REQUIRES = {"fsspec": "fsspec", "pyarrow": "pyarrow"}
|
||||
PARAM_CHECKSUM = "checksum"
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
BLOCK_SIZE = 2 ** 26 # 128MB
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str) -> str:
|
||||
from fsspec.utils import infer_storage_options
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
return infer_storage_options(path)["path"].lstrip("/")
|
||||
|
||||
self.host = config["host"]
|
||||
self.user = config.get("user")
|
||||
self.port = config.get("port")
|
||||
|
||||
self.kerb_ticket = config.get("kerb_ticket")
|
||||
def unstrip_protocol(self, path: str) -> str:
|
||||
host = self.fs_args["host"]
|
||||
port = self.fs_args["port"]
|
||||
path = path.lstrip("/")
|
||||
return f"hdfs://{host}:{port}/{path}"
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
from fsspec.implementations.hdfs import PyArrowHDFS
|
||||
from fsspec.implementations.arrow import HadoopFileSystem
|
||||
|
||||
# pylint:disable=protected-access
|
||||
return PyArrowHDFS._get_kwargs_from_urls(urlpath)
|
||||
return HadoopFileSystem._get_kwargs_from_urls(urlpath)
|
||||
|
||||
def hdfs(self, path_info):
|
||||
import pyarrow.fs
|
||||
def _prepare_credentials(self, **config):
|
||||
return config
|
||||
|
||||
# NOTE: HadoopFileSystem is not meant to be closed by us and doesn't
|
||||
# have close() method or alternative, so we add a noop one to satisfy
|
||||
# our connection pool.
|
||||
class HDFSConnection(pyarrow.fs.HadoopFileSystem):
|
||||
def close(self):
|
||||
pass
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self):
|
||||
from fsspec.implementations.arrow import HadoopFileSystem
|
||||
|
||||
return get_connection(
|
||||
HDFSConnection,
|
||||
path_info.host,
|
||||
path_info.port,
|
||||
user=path_info.user,
|
||||
kerb_ticket=self.kerb_ticket,
|
||||
return HadoopFileSystem(**self.fs_args)
|
||||
|
||||
def checksum(self, path):
|
||||
return self._checksum(path)
|
||||
|
||||
def _checksum(self, path, **kwargs):
|
||||
# PyArrow doesn't natively support retrieving the
|
||||
# checksum, so we have to use hadoop fs
|
||||
|
||||
url = self.unstrip_protocol(path)
|
||||
|
||||
result = self._run_command(
|
||||
f"checksum {url}",
|
||||
env=fix_env(os.environ),
|
||||
user=self.fs_args["user"],
|
||||
)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
@contextmanager
|
||||
def open(self, path_info, mode="r", encoding=None, **kwargs):
|
||||
assert mode in {"r", "rt", "rb"}
|
||||
match = CHECKSUM_REGEX.match(result)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
with self.hdfs(path_info) as hdfs, closing(
|
||||
hdfs.open_input_stream(path_info.path)
|
||||
) as fd:
|
||||
if mode == "rb":
|
||||
yield fd
|
||||
else:
|
||||
yield io.TextIOWrapper(fd, encoding=encoding)
|
||||
except OSError as e:
|
||||
# Empty .errno and not specific enough error class in pyarrow,
|
||||
# see https://issues.apache.org/jira/browse/ARROW-6248
|
||||
if "file does not exist" in str(e):
|
||||
raise FileNotFoundError(*e.args)
|
||||
raise
|
||||
return match.group("checksum")
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
assert not isinstance(path_info, list)
|
||||
assert path_info.scheme == "hdfs"
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
import pyarrow.fs
|
||||
def _run_command(self, cmd, env=None, user=None):
|
||||
cmd = "hadoop fs -" + cmd
|
||||
if user:
|
||||
cmd = f"HADOOP_USER_NAME={user} " + cmd
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
return file_info.type != pyarrow.fs.FileType.NotFound
|
||||
# NOTE: close_fds doesn't work with redirected stdin/stdout/stderr.
|
||||
# See https://github.com/iterative/dvc/issues/1197.
|
||||
close_fds = os.name != "nt"
|
||||
|
||||
def _walk(self, hdfs, root, topdown=True):
|
||||
import posixpath
|
||||
|
||||
from pyarrow.fs import FileSelector, FileType
|
||||
|
||||
dirs = deque()
|
||||
nondirs = deque()
|
||||
|
||||
selector = FileSelector(root)
|
||||
for entry in hdfs.get_file_info(selector):
|
||||
if entry.type == FileType.Directory:
|
||||
dirs.append(entry.base_name)
|
||||
else:
|
||||
nondirs.append(entry.base_name)
|
||||
|
||||
if topdown:
|
||||
yield root, dirs, nondirs
|
||||
|
||||
for dname in dirs:
|
||||
# NOTE: posixpath.join() is slower
|
||||
yield from self._walk(
|
||||
hdfs, f"{root}{posixpath.sep}{dname}", topdown=topdown
|
||||
)
|
||||
|
||||
if not topdown:
|
||||
yield root, dirs, nondirs
|
||||
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
if not self.isdir(top):
|
||||
return
|
||||
|
||||
with self.hdfs(top) as hdfs:
|
||||
for root, dnames, fnames in self._walk(
|
||||
hdfs, top.path, topdown=topdown
|
||||
):
|
||||
yield top.replace(path=root), dnames, fnames
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for root, _, fnames in self.walk(path_info):
|
||||
yield from (root / fname for fname in fnames)
|
||||
|
||||
def remove(self, path_info):
|
||||
if path_info.scheme != "hdfs":
|
||||
raise NotImplementedError
|
||||
|
||||
if self.exists(path_info):
|
||||
logger.debug(f"Removing {path_info.path}")
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
import pyarrow.fs
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
if file_info.type == pyarrow.fs.FileType.Directory:
|
||||
hdfs.delete_dir(path_info.path)
|
||||
else:
|
||||
hdfs.delete_file(path_info.path)
|
||||
|
||||
def makedirs(self, path_info, **kwargs):
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
# NOTE: fs.create_dir creates parents by default
|
||||
hdfs.create_dir(path_info.path)
|
||||
|
||||
def copy(self, from_info, to_info, **_kwargs):
|
||||
# NOTE: hdfs.copy_file is not supported yet in pyarrow
|
||||
with self.hdfs(to_info) as hdfs:
|
||||
# NOTE: this is how `hadoop fs -cp` works too: it copies through
|
||||
# your local machine.
|
||||
with closing(hdfs.open_input_stream(from_info.path)) as from_fobj:
|
||||
tmp_info = to_info.parent / tmp_fname(to_info.name)
|
||||
try:
|
||||
with closing(
|
||||
hdfs.open_output_stream(tmp_info.path)
|
||||
) as tmp_fobj:
|
||||
shutil.copyfileobj(from_fobj, tmp_fobj)
|
||||
hdfs.move(tmp_info.path, to_info.path)
|
||||
except Exception:
|
||||
self.remove(tmp_info)
|
||||
raise
|
||||
|
||||
def isfile(self, path_info):
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
import pyarrow.fs
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
return file_info.type == pyarrow.fs.FileType.File
|
||||
|
||||
def isdir(self, path_info):
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
import pyarrow.fs
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
return file_info.type == pyarrow.fs.FileType.Directory
|
||||
|
||||
def info(self, path_info):
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
from pyarrow.fs import FileType
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
if file_info.type is FileType.Directory:
|
||||
kind = "directory"
|
||||
elif file_info.type is FileType.File:
|
||||
kind = "file"
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), path_info
|
||||
)
|
||||
|
||||
return {"size": file_info.size, "type": kind}
|
||||
|
||||
def checksum(self, path_info):
|
||||
return HashInfo(
|
||||
"checksum",
|
||||
_hadoop_fs_checksum(path_info),
|
||||
size=self.getsize(path_info),
|
||||
executable = os.getenv("SHELL") if os.name != "nt" else None
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
shell=True,
|
||||
close_fds=close_fds,
|
||||
executable=executable,
|
||||
env=env or os.environ,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = p.communicate()
|
||||
|
||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
||||
with self.hdfs(to_info) as hdfs:
|
||||
with hdfs.open_output_stream(to_info.path) as fdest:
|
||||
shutil.copyfileobj(fobj, fdest, self.BLOCK_SIZE)
|
||||
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
|
||||
):
|
||||
with self.hdfs(to_info) as hdfs:
|
||||
tmp_file = tmp_fname(to_info.path)
|
||||
total = os.path.getsize(from_file)
|
||||
with open(from_file, "rb") as fobj:
|
||||
with Tqdm.wrapattr(
|
||||
fobj,
|
||||
"read",
|
||||
desc=name,
|
||||
total=total,
|
||||
disable=no_progress_bar,
|
||||
) as wrapped:
|
||||
with hdfs.open_output_stream(tmp_file) as sobj:
|
||||
shutil.copyfileobj(wrapped, sobj, self.BLOCK_SIZE)
|
||||
hdfs.move(tmp_file, to_info.path)
|
||||
|
||||
def _download(
|
||||
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
|
||||
):
|
||||
with self.hdfs(from_info) as hdfs:
|
||||
file_info = hdfs.get_file_info(from_info.path)
|
||||
total = file_info.size
|
||||
with open(to_file, "wb+") as fobj:
|
||||
with Tqdm.wrapattr(
|
||||
fobj,
|
||||
"write",
|
||||
desc=name,
|
||||
total=total,
|
||||
disable=no_progress_bar,
|
||||
) as wrapped:
|
||||
with hdfs.open_input_stream(from_info.path) as sobj:
|
||||
shutil.copyfileobj(sobj, wrapped, self.BLOCK_SIZE)
|
||||
if p.returncode != 0:
|
||||
raise subprocess.CalledProcessError(p.returncode, cmd, out, err)
|
||||
else:
|
||||
return out.decode("utf-8")
|
||||
|
||||
+20
-7
@@ -1,12 +1,13 @@
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
from funcy import cached_property, memoize, wrap_with
|
||||
|
||||
from dvc import prompt
|
||||
from dvc.path_info import HTTPURLInfo
|
||||
from dvc.progress import DEFAULT_CALLBACK
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper, NoDirectoriesMixin
|
||||
from .fsspec_wrapper import AnyFSPath, FSSpecWrapper, NoDirectoriesMixin
|
||||
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
@@ -32,9 +33,8 @@ def make_context(ssl_verify):
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
|
||||
class HTTPFileSystem(NoDirectoriesMixin, FSSpecWrapper):
|
||||
scheme = Schemes.HTTP
|
||||
PATH_CLS = HTTPURLInfo
|
||||
PARAM_CHECKSUM = "checksum"
|
||||
REQUIRES = {"aiohttp": "aiohttp", "aiohttp-retry": "aiohttp_retry"}
|
||||
CAN_TRAVERSE = False
|
||||
@@ -87,6 +87,9 @@ class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
|
||||
ssl=make_context(config["ssl_verify"])
|
||||
)
|
||||
|
||||
# Allow reading proxy configurations from the environment.
|
||||
client_kwargs["trust_env"] = True
|
||||
|
||||
credentials["get_client"] = self.get_client
|
||||
self.upload_method = config.get("method", "POST")
|
||||
return credentials
|
||||
@@ -99,6 +102,7 @@ class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
|
||||
attempts=self.SESSION_RETRIES,
|
||||
factor=self.SESSION_BACKOFF_FACTOR,
|
||||
max_timeout=self.REQUEST_TIMEOUT,
|
||||
exceptions=[aiohttp.ClientError],
|
||||
)
|
||||
|
||||
# The default timeout for the aiohttp is 300 seconds
|
||||
@@ -123,6 +127,15 @@ class HTTPFileSystem(CallbackMixin, NoDirectoriesMixin, FSSpecWrapper):
|
||||
|
||||
return _HTTPFileSystem(**self.fs_args)
|
||||
|
||||
def _entry_hook(self, entry):
|
||||
entry["checksum"] = entry.get("ETag") or entry.get("Content-MD5")
|
||||
return entry
|
||||
def unstrip_protocol(self, path: str) -> str:
|
||||
return path
|
||||
|
||||
def put_file(
|
||||
self,
|
||||
from_file: AnyFSPath,
|
||||
to_info: AnyFSPath,
|
||||
callback: Any = DEFAULT_CALLBACK,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
kwargs.setdefault("method", self.upload_method)
|
||||
super().put_file(from_file, to_info, callback=callback, **kwargs)
|
||||
|
||||
+53
-63
@@ -1,22 +1,21 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.system import System
|
||||
from dvc.utils import is_exec, tmp_fname
|
||||
from dvc.utils.fs import copy_fobj_to_file, copyfile, makedirs, move, remove
|
||||
|
||||
from .base import BaseFileSystem
|
||||
from ..progress import DEFAULT_CALLBACK
|
||||
from .base import FileSystem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LocalFileSystem(BaseFileSystem):
|
||||
class LocalFileSystem(FileSystem):
|
||||
sep = os.sep
|
||||
|
||||
scheme = Schemes.LOCAL
|
||||
PATH_CLS = PathInfo
|
||||
PARAM_CHECKSUM = "md5"
|
||||
PARAM_PATH = "path"
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
@@ -28,28 +27,29 @@ class LocalFileSystem(BaseFileSystem):
|
||||
self.fs = LocalFS()
|
||||
|
||||
@staticmethod
|
||||
def open(path_info, mode="r", encoding=None, **kwargs):
|
||||
return open(path_info, mode=mode, encoding=encoding)
|
||||
def open(path, mode="r", encoding=None, **kwargs):
|
||||
return open(path, mode=mode, encoding=encoding)
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
assert isinstance(path_info, str) or path_info.scheme == "local"
|
||||
def exists(self, path) -> bool:
|
||||
# TODO: replace this with os.path.exists once the problem is fixed on
|
||||
# the fsspec https://github.com/intake/filesystem_spec/issues/742
|
||||
return os.path.lexists(path_info)
|
||||
return os.path.lexists(path)
|
||||
|
||||
def checksum(self, path_info) -> str:
|
||||
return self.fs.checksum(path_info)
|
||||
def checksum(self, path) -> str:
|
||||
from fsspec.utils import tokenize
|
||||
|
||||
def isfile(self, path_info) -> bool:
|
||||
return os.path.isfile(path_info)
|
||||
st = os.stat(path)
|
||||
|
||||
def isdir(self, path_info) -> bool:
|
||||
return os.path.isdir(path_info)
|
||||
return str(int(tokenize([st.st_ino, st.st_mtime, st.st_size]), 16))
|
||||
|
||||
def iscopy(self, path_info):
|
||||
return not (
|
||||
System.is_symlink(path_info) or System.is_hardlink(path_info)
|
||||
)
|
||||
def isfile(self, path) -> bool:
|
||||
return os.path.isfile(path)
|
||||
|
||||
def isdir(self, path) -> bool:
|
||||
return os.path.isdir(path)
|
||||
|
||||
def iscopy(self, path):
|
||||
return not (System.is_symlink(path) or System.is_hardlink(path))
|
||||
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
"""Directory fs generator.
|
||||
@@ -62,45 +62,37 @@ class LocalFileSystem(BaseFileSystem):
|
||||
):
|
||||
yield os.path.normpath(root), dirs, files
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for root, _, files in self.walk(path_info):
|
||||
def find(self, path, prefix=None):
|
||||
for root, _, files in self.walk(path):
|
||||
for file in files:
|
||||
# NOTE: os.path.join is ~5.5 times slower
|
||||
yield PathInfo(f"{root}{os.sep}{file}")
|
||||
yield f"{root}{os.sep}{file}"
|
||||
|
||||
def is_empty(self, path_info):
|
||||
if self.isfile(path_info) and os.path.getsize(path_info) == 0:
|
||||
def is_empty(self, path):
|
||||
if self.isfile(path) and os.path.getsize(path) == 0:
|
||||
return True
|
||||
|
||||
if self.isdir(path_info) and len(os.listdir(path_info)) == 0:
|
||||
if self.isdir(path) and len(os.listdir(path)) == 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def remove(self, path_info):
|
||||
if isinstance(path_info, PathInfo):
|
||||
if path_info.scheme != "local":
|
||||
raise NotImplementedError
|
||||
remove(path_info)
|
||||
def remove(self, path):
|
||||
remove(path)
|
||||
|
||||
def makedirs(self, path_info, **kwargs):
|
||||
makedirs(path_info, exist_ok=kwargs.pop("exist_ok", True))
|
||||
def makedirs(self, path, **kwargs):
|
||||
makedirs(path, exist_ok=kwargs.pop("exist_ok", True))
|
||||
|
||||
def isexec(self, path_info):
|
||||
mode = self.info(path_info)["mode"]
|
||||
def isexec(self, path):
|
||||
mode = self.info(path)["mode"]
|
||||
return is_exec(mode)
|
||||
|
||||
def move(self, from_info, to_info):
|
||||
if (
|
||||
isinstance(from_info, PathInfo) and from_info.scheme != "local"
|
||||
) or (isinstance(to_info, PathInfo) and to_info.scheme != "local"):
|
||||
raise NotImplementedError
|
||||
|
||||
self.makedirs(to_info.parent)
|
||||
self.makedirs(self.path.parent(to_info))
|
||||
move(from_info, to_info)
|
||||
|
||||
def copy(self, from_info, to_info):
|
||||
tmp_info = to_info.parent / tmp_fname("")
|
||||
tmp_info = self.path.join(self.path.parent(to_info), tmp_fname(""))
|
||||
try:
|
||||
copyfile(from_info, tmp_info)
|
||||
os.rename(tmp_info, to_info)
|
||||
@@ -109,8 +101,8 @@ class LocalFileSystem(BaseFileSystem):
|
||||
raise
|
||||
|
||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
||||
self.makedirs(to_info.parent)
|
||||
tmp_info = to_info.parent / tmp_fname("")
|
||||
self.makedirs(self.path.parent(to_info))
|
||||
tmp_info = self.path.join(self.path.parent(to_info), tmp_fname(""))
|
||||
try:
|
||||
copy_fobj_to_file(fobj, tmp_info)
|
||||
os.rename(tmp_info, to_info)
|
||||
@@ -123,8 +115,8 @@ class LocalFileSystem(BaseFileSystem):
|
||||
System.symlink(from_info, to_info)
|
||||
|
||||
@staticmethod
|
||||
def is_symlink(path_info):
|
||||
return System.is_symlink(path_info)
|
||||
def is_symlink(path):
|
||||
return System.is_symlink(path)
|
||||
|
||||
def hardlink(self, from_info, to_info):
|
||||
# If there are a lot of empty files (which happens a lot in datasets),
|
||||
@@ -149,30 +141,28 @@ class LocalFileSystem(BaseFileSystem):
|
||||
System.hardlink(from_info, to_info)
|
||||
|
||||
@staticmethod
|
||||
def is_hardlink(path_info):
|
||||
return System.is_hardlink(path_info)
|
||||
def is_hardlink(path):
|
||||
return System.is_hardlink(path)
|
||||
|
||||
def reflink(self, from_info, to_info):
|
||||
System.reflink(from_info, to_info)
|
||||
|
||||
def info(self, path_info):
|
||||
return self.fs.info(path_info)
|
||||
def info(self, path):
|
||||
return self.fs.info(path)
|
||||
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
|
||||
def put_file(
|
||||
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
makedirs(to_info.parent, exist_ok=True)
|
||||
|
||||
tmp_file = tmp_fname(to_info)
|
||||
copyfile(
|
||||
from_file, tmp_file, name=name, no_progress_bar=no_progress_bar
|
||||
)
|
||||
parent = self.path.parent(to_info)
|
||||
makedirs(parent, exist_ok=True)
|
||||
tmp_file = self.path.join(parent, tmp_fname())
|
||||
copyfile(from_file, tmp_file, callback=callback)
|
||||
os.replace(tmp_file, to_info)
|
||||
|
||||
@staticmethod
|
||||
def _download(
|
||||
from_info, to_file, name=None, no_progress_bar=False, **_kwargs
|
||||
def get_file(
|
||||
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
copyfile(
|
||||
from_info, to_file, no_progress_bar=no_progress_bar, name=name
|
||||
)
|
||||
copyfile(from_info, to_file, callback=callback)
|
||||
|
||||
|
||||
localfs = LocalFileSystem()
|
||||
|
||||
@@ -2,7 +2,6 @@ import threading
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .fsspec_wrapper import FSSpecWrapper
|
||||
@@ -11,7 +10,6 @@ from .fsspec_wrapper import FSSpecWrapper
|
||||
class MemoryFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
||||
scheme = Schemes.MEMORY
|
||||
PARAM_CHECKSUM = "md5"
|
||||
PATH_CLS = CloudURLInfo
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
DEFAULT_BLOCKSIZE = 4096
|
||||
|
||||
|
||||
+2
-4
@@ -4,18 +4,16 @@ import threading
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .fsspec_wrapper import CallbackMixin, ObjectFSWrapper
|
||||
from .fsspec_wrapper import ObjectFSWrapper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint:disable=abstract-method
|
||||
class OSSFileSystem(CallbackMixin, ObjectFSWrapper):
|
||||
class OSSFileSystem(ObjectFSWrapper):
|
||||
scheme = Schemes.OSS
|
||||
PATH_CLS = CloudURLInfo
|
||||
REQUIRES = {"ossfs": "ossfs"}
|
||||
PARAM_CHECKSUM = "etag"
|
||||
COPY_POLL_SECONDS = 5
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import ntpath
|
||||
import posixpath
|
||||
|
||||
|
||||
class Path:
|
||||
def __init__(self, sep):
|
||||
if sep == posixpath.sep:
|
||||
self.flavour = posixpath
|
||||
elif sep == ntpath.sep:
|
||||
self.flavour = ntpath
|
||||
else:
|
||||
raise ValueError(f"unsupported separator '{sep}'")
|
||||
|
||||
def join(self, *parts):
|
||||
return self.flavour.join(*parts)
|
||||
|
||||
def parts(self, path):
|
||||
drive, path = self.flavour.splitdrive(path)
|
||||
|
||||
ret = []
|
||||
while True:
|
||||
path, part = self.flavour.split(path)
|
||||
|
||||
if part:
|
||||
ret.append(part)
|
||||
continue
|
||||
|
||||
if path:
|
||||
ret.append(path)
|
||||
|
||||
break
|
||||
|
||||
ret.reverse()
|
||||
|
||||
if drive:
|
||||
ret = [drive] + ret
|
||||
|
||||
return tuple(ret)
|
||||
|
||||
def parent(self, path):
|
||||
return self.flavour.dirname(path)
|
||||
|
||||
def parents(self, path):
|
||||
parts = self.parts(path)
|
||||
return tuple(
|
||||
self.join(*parts[:length])
|
||||
for length in range(len(parts) - 1, 0, -1)
|
||||
)
|
||||
|
||||
def name(self, path):
|
||||
return self.parts(path)[-1]
|
||||
|
||||
def suffix(self, path):
|
||||
name = self.name(path)
|
||||
_, dot, suffix = name.partition(".")
|
||||
return dot + suffix
|
||||
|
||||
def with_name(self, path, name):
|
||||
parts = list(self.parts(path))
|
||||
parts[-1] = name
|
||||
return self.join(*parts)
|
||||
|
||||
def with_suffix(self, path, suffix):
|
||||
parts = list(self.parts(path))
|
||||
real_path, _, _ = parts[-1].partition(".")
|
||||
parts[-1] = real_path + suffix
|
||||
return self.join(*parts)
|
||||
|
||||
def isin(self, left, right):
|
||||
left_parts = self.parts(left)
|
||||
right_parts = self.parts(right)
|
||||
left_len = len(left_parts)
|
||||
right_len = len(right_parts)
|
||||
return left_len > right_len and left_parts[:right_len] == right_parts
|
||||
|
||||
def isin_or_eq(self, left, right):
|
||||
return left == right or self.isin(left, right)
|
||||
|
||||
def overlaps(self, left, right):
|
||||
# pylint: disable=arguments-out-of-order
|
||||
return self.isin_or_eq(left, right) or self.isin(right, left)
|
||||
|
||||
def relpath(self, path, base):
|
||||
assert len(path) > len(base)
|
||||
assert path.startswith(base)
|
||||
normpath = path.rstrip(self.flavour.sep)
|
||||
normbase = base.rstrip(self.flavour.sep)
|
||||
return normpath[len(normbase) + 1 :]
|
||||
|
||||
def relparts(self, path, base):
|
||||
return self.parts(self.relpath(path, base))
|
||||
|
||||
def as_posix(self, path):
|
||||
return path.replace(self.flavour.sep, posixpath.sep)
|
||||
+45
-48
@@ -7,9 +7,8 @@ from typing import TYPE_CHECKING, Callable, Optional, Tuple, Type, Union
|
||||
|
||||
from funcy import lfilter, wrap_with
|
||||
|
||||
from dvc.path_info import PathInfo
|
||||
|
||||
from .base import BaseFileSystem
|
||||
from ..progress import DEFAULT_CALLBACK
|
||||
from .base import FileSystem
|
||||
from .dvc import DvcFileSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -20,7 +19,7 @@ logger = logging.getLogger(__name__)
|
||||
RepoFactory = Union[Callable[[str], "Repo"], Type["Repo"]]
|
||||
|
||||
|
||||
class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
class RepoFileSystem(FileSystem): # pylint:disable=abstract-method
|
||||
"""DVC + git-tracked files fs.
|
||||
|
||||
Args:
|
||||
@@ -163,7 +162,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
if not prefix:
|
||||
return None
|
||||
|
||||
parents = (parent.fspath for parent in PathInfo(path).parents)
|
||||
parents = (parent for parent in self.path.parents(path))
|
||||
dirs = [path] + list(takewhile(lambda p: p != prefix, parents))
|
||||
dirs.reverse()
|
||||
self._update(dirs, starting_repo=repo)
|
||||
@@ -194,9 +193,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
repo_path = os.path.join(dir_path, Repo.DVC_DIR)
|
||||
return self._main_repo.fs.isdir(repo_path)
|
||||
|
||||
def _get_fs_pair(
|
||||
self, path
|
||||
) -> Tuple[BaseFileSystem, Optional[DvcFileSystem]]:
|
||||
def _get_fs_pair(self, path) -> Tuple[FileSystem, Optional[DvcFileSystem]]:
|
||||
"""
|
||||
Returns a pair of fss based on repo the path falls in, using prefix.
|
||||
"""
|
||||
@@ -216,34 +213,33 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
encoding = None
|
||||
|
||||
fs, dvc_fs = self._get_fs_pair(path)
|
||||
path_info = PathInfo(path)
|
||||
try:
|
||||
return fs.open(path_info, mode=mode, encoding=encoding)
|
||||
return fs.open(path, mode=mode, encoding=encoding)
|
||||
except FileNotFoundError:
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
return dvc_fs.open(path_info, mode=mode, encoding=encoding, **kwargs)
|
||||
return dvc_fs.open(path, mode=mode, encoding=encoding, **kwargs)
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
fs, dvc_fs = self._get_fs_pair(path_info)
|
||||
def exists(self, path) -> bool:
|
||||
fs, dvc_fs = self._get_fs_pair(path)
|
||||
|
||||
if not dvc_fs:
|
||||
return fs.exists(path_info)
|
||||
return fs.exists(path)
|
||||
|
||||
if dvc_fs.repo.dvcignore.is_ignored(fs, path_info):
|
||||
if dvc_fs.repo.dvcignore.is_ignored(fs, path):
|
||||
return False
|
||||
|
||||
if fs.exists(path_info):
|
||||
if fs.exists(path):
|
||||
return True
|
||||
|
||||
try:
|
||||
meta = dvc_fs.metadata(path_info)
|
||||
meta = dvc_fs.metadata(path)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
for out in meta.outs:
|
||||
if fs.exists(out.path_info):
|
||||
if fs.exists(out.fs_path):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -270,7 +266,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
return False
|
||||
|
||||
for out in meta.outs:
|
||||
if fs.exists(out.path_info):
|
||||
if fs.exists(out.fs_path):
|
||||
return False
|
||||
|
||||
return meta.isdir
|
||||
@@ -302,15 +298,15 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
(out,) = meta.outs
|
||||
assert len(meta.outs) == 1
|
||||
if fs.exists(out.path_info):
|
||||
if fs.exists(out.fs_path):
|
||||
return False
|
||||
return meta.isfile
|
||||
|
||||
def isexec(self, path_info):
|
||||
fs, dvc_fs = self._get_fs_pair(path_info)
|
||||
if dvc_fs and dvc_fs.exists(path_info):
|
||||
return dvc_fs.isexec(path_info)
|
||||
return fs.isexec(path_info)
|
||||
def isexec(self, path):
|
||||
fs, dvc_fs = self._get_fs_pair(path)
|
||||
if dvc_fs and dvc_fs.exists(path):
|
||||
return dvc_fs.isexec(path)
|
||||
return fs.isexec(path)
|
||||
|
||||
def _dvc_walk(self, walk):
|
||||
try:
|
||||
@@ -443,39 +439,40 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
yield from self._walk(repo_walk, dvc_walk, dvcfiles=dvcfiles)
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for root, _, files in self.walk(path_info, **kwargs):
|
||||
def find(self, path, prefix=None):
|
||||
for root, _, files in self.walk(path):
|
||||
for fname in files:
|
||||
yield PathInfo(root) / fname
|
||||
yield self.path.join(root, fname)
|
||||
|
||||
def _download(self, from_info, to_file, **kwargs):
|
||||
def get_file(
|
||||
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
fs, dvc_fs = self._get_fs_pair(from_info)
|
||||
try:
|
||||
fs._download( # pylint: disable=protected-access
|
||||
from_info, to_file, **kwargs
|
||||
fs.get_file( # pylint: disable=protected-access
|
||||
from_info, to_file, callback=callback, **kwargs
|
||||
)
|
||||
return
|
||||
except FileNotFoundError:
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
dvc_fs._download( # pylint: disable=protected-access
|
||||
from_info, to_file, **kwargs
|
||||
dvc_fs.get_file( # pylint: disable=protected-access
|
||||
from_info, to_file, callback=callback, **kwargs
|
||||
)
|
||||
|
||||
def metadata(self, path):
|
||||
abspath = os.path.abspath(path)
|
||||
path_info = PathInfo(abspath)
|
||||
fs, dvc_fs = self._get_fs_pair(path_info)
|
||||
fs_path = os.path.abspath(path)
|
||||
fs, dvc_fs = self._get_fs_pair(fs_path)
|
||||
|
||||
dvc_meta = None
|
||||
if dvc_fs:
|
||||
with suppress(FileNotFoundError):
|
||||
dvc_meta = dvc_fs.metadata(path_info)
|
||||
dvc_meta = dvc_fs.metadata(fs_path)
|
||||
|
||||
info_result = None
|
||||
with suppress(FileNotFoundError):
|
||||
info_result = fs.info(path_info)
|
||||
info_result = fs.info(fs_path)
|
||||
|
||||
if not info_result and not dvc_meta:
|
||||
raise FileNotFoundError
|
||||
@@ -483,8 +480,8 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
from ._metadata import Metadata
|
||||
|
||||
meta = dvc_meta or Metadata(
|
||||
path_info=path_info,
|
||||
repo=self._get_repo(abspath) or self._main_repo,
|
||||
fs_path=fs_path,
|
||||
repo=self._get_repo(fs_path) or self._main_repo,
|
||||
)
|
||||
|
||||
isdir = bool(info_result) and info_result["type"] == "directory"
|
||||
@@ -496,18 +493,18 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
meta.is_exec = bool(info_result) and is_exec(info_result["mode"])
|
||||
return meta
|
||||
|
||||
def info(self, path_info):
|
||||
fs, dvc_fs = self._get_fs_pair(path_info)
|
||||
def info(self, path):
|
||||
fs, dvc_fs = self._get_fs_pair(path)
|
||||
|
||||
try:
|
||||
return fs.info(path_info)
|
||||
return fs.info(path)
|
||||
except FileNotFoundError:
|
||||
return dvc_fs.info(path_info)
|
||||
return dvc_fs.info(path)
|
||||
|
||||
def checksum(self, path_info):
|
||||
fs, dvc_fs = self._get_fs_pair(path_info)
|
||||
def checksum(self, path):
|
||||
fs, dvc_fs = self._get_fs_pair(path)
|
||||
|
||||
try:
|
||||
return fs.checksum(path_info)
|
||||
return fs.checksum(path)
|
||||
except FileNotFoundError:
|
||||
return dvc_fs.checksum(path_info)
|
||||
return dvc_fs.checksum(path)
|
||||
|
||||
+34
-40
@@ -5,8 +5,7 @@ from collections import defaultdict
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.progress import DEFAULT_CALLBACK
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .fsspec_wrapper import ObjectFSWrapper
|
||||
@@ -17,7 +16,6 @@ _AWS_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".aws", "config")
|
||||
# pylint:disable=abstract-method
|
||||
class BaseS3FileSystem(ObjectFSWrapper):
|
||||
scheme = Schemes.S3
|
||||
PATH_CLS = CloudURLInfo
|
||||
REQUIRES = {"s3fs": "s3fs", "boto3": "boto3"}
|
||||
PARAM_CHECKSUM = "etag"
|
||||
DETAIL_FIELDS = frozenset(("etag", "size"))
|
||||
@@ -99,6 +97,11 @@ class BaseS3FileSystem(ObjectFSWrapper):
|
||||
client["endpoint_url"] = config.get("endpointurl")
|
||||
client["verify"] = config.get("ssl_verify")
|
||||
|
||||
# timeout configuration
|
||||
config_kwargs = login_info["config_kwargs"]
|
||||
config_kwargs["read_timeout"] = config.get("read_timeout")
|
||||
config_kwargs["connect_timeout"] = config.get("connect_timeout")
|
||||
|
||||
# encryptions
|
||||
additional = login_info["s3_additional_kwargs"]
|
||||
additional["ServerSideEncryption"] = config.get("sse")
|
||||
@@ -143,12 +146,6 @@ class BaseS3FileSystem(ObjectFSWrapper):
|
||||
}
|
||||
)
|
||||
|
||||
def _entry_hook(self, entry):
|
||||
entry = entry.copy()
|
||||
if "ETag" in entry:
|
||||
entry["etag"] = entry["ETag"].strip('"')
|
||||
return entry
|
||||
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self):
|
||||
@@ -156,6 +153,15 @@ class BaseS3FileSystem(ObjectFSWrapper):
|
||||
|
||||
return _S3FileSystem(**self.fs_args)
|
||||
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str) -> str:
|
||||
from fsspec.utils import infer_storage_options
|
||||
|
||||
return infer_storage_options(path)["path"]
|
||||
|
||||
def unstrip_protocol(self, path):
|
||||
return "s3://" + path.lstrip("/")
|
||||
|
||||
|
||||
def _translate_exceptions(func):
|
||||
@functools.wraps(func)
|
||||
@@ -199,41 +205,29 @@ class S3FileSystem(BaseS3FileSystem): # pylint:disable=abstract-method
|
||||
verify=client_kwargs.get("verify"),
|
||||
)
|
||||
|
||||
def _get_obj(self, path_info):
|
||||
bucket = self.s3.Bucket(path_info.bucket)
|
||||
return bucket.Object(path_info.path)
|
||||
def _get_obj(self, path):
|
||||
bucket_name, key, _ = self.fs.split_path(path)
|
||||
bucket = self.s3.Bucket(bucket_name)
|
||||
return bucket.Object(key)
|
||||
|
||||
@_translate_exceptions
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **pbar_args
|
||||
def put_file(
|
||||
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
total = os.path.getsize(from_file)
|
||||
with Tqdm(
|
||||
disable=no_progress_bar,
|
||||
total=total,
|
||||
bytes=True,
|
||||
desc=name,
|
||||
**pbar_args,
|
||||
) as pbar:
|
||||
obj = self._get_obj(to_info)
|
||||
obj.upload_file(
|
||||
from_file,
|
||||
Callback=pbar.update,
|
||||
ExtraArgs=self.fs_args.get("s3_additional_kwargs"),
|
||||
Config=self._transfer_config,
|
||||
)
|
||||
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
|
||||
callback.set_size(os.path.getsize(from_file))
|
||||
obj = self._get_obj(to_info)
|
||||
obj.upload_file(
|
||||
from_file,
|
||||
Callback=callback.relative_update,
|
||||
ExtraArgs=self.fs_args.get("s3_additional_kwargs"),
|
||||
Config=self._transfer_config,
|
||||
)
|
||||
self.fs.invalidate_cache(self.path.parent(to_info))
|
||||
|
||||
@_translate_exceptions
|
||||
def _download(
|
||||
self, from_info, to_file, name=None, no_progress_bar=False, **pbar_args
|
||||
def get_file(
|
||||
self, from_info, to_info, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
obj = self._get_obj(from_info)
|
||||
with Tqdm(
|
||||
disable=no_progress_bar,
|
||||
total=obj.content_length,
|
||||
bytes=True,
|
||||
desc=name,
|
||||
**pbar_args,
|
||||
) as pbar:
|
||||
obj.download_file(to_file, Callback=pbar.update)
|
||||
callback.set_size(obj.content_length)
|
||||
obj.download_file(to_info, Callback=callback.relative_update)
|
||||
|
||||
+23
-15
@@ -8,7 +8,8 @@ from dvc import prompt
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.utils.fs import as_atomic
|
||||
|
||||
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
|
||||
from ..progress import DEFAULT_CALLBACK
|
||||
from .fsspec_wrapper import FSSpecWrapper
|
||||
|
||||
_SSH_TIMEOUT = 60 * 30
|
||||
_SSH_CONFIG_FILE = os.path.expanduser(os.path.join("~", ".ssh", "config"))
|
||||
@@ -26,19 +27,24 @@ def ask_password(host, user, port):
|
||||
|
||||
|
||||
# pylint:disable=abstract-method
|
||||
class SSHFileSystem(CallbackMixin, FSSpecWrapper):
|
||||
class SSHFileSystem(FSSpecWrapper):
|
||||
scheme = Schemes.SSH
|
||||
REQUIRES = {"sshfs": "sshfs"}
|
||||
|
||||
DEFAULT_PORT = 22
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
def _with_bucket(self, path):
|
||||
if isinstance(path, self.PATH_CLS):
|
||||
path_str = path.path
|
||||
else:
|
||||
path_str = super()._with_bucket(path)
|
||||
return os.path.normpath(path_str)
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str) -> str:
|
||||
from fsspec.utils import infer_storage_options
|
||||
|
||||
return infer_storage_options(path)["path"]
|
||||
|
||||
def unstrip_protocol(self, path: str) -> str:
|
||||
host = self.fs_args["host"]
|
||||
port = self.fs_args["port"]
|
||||
path = path.lstrip("/")
|
||||
return f"ssh://{host}:{port}/{path}"
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
self.CAN_TRAVERSE = True
|
||||
@@ -66,6 +72,11 @@ class SSHFileSystem(CallbackMixin, FSSpecWrapper):
|
||||
or self.DEFAULT_PORT
|
||||
)
|
||||
|
||||
if config.get("ask_password") and config.get("password") is None:
|
||||
config["password"] = ask_password(
|
||||
login_info["host"], login_info["username"], login_info["port"]
|
||||
)
|
||||
|
||||
login_info["password"] = config.get("password")
|
||||
login_info["passphrase"] = config.get("password")
|
||||
|
||||
@@ -100,11 +111,6 @@ class SSHFileSystem(CallbackMixin, FSSpecWrapper):
|
||||
login_info["agent_forwarding"] = config.get("agent_forwarding", True)
|
||||
login_info["proxy_command"] = user_ssh_config.get("ProxyCommand")
|
||||
|
||||
if config.get("ask_password") and login_info["password"] is None:
|
||||
login_info["password"] = ask_password(
|
||||
login_info["host"], login_info["username"], login_info["port"]
|
||||
)
|
||||
|
||||
# We are going to automatically add stuff to known_hosts
|
||||
# something like paramiko's AutoAddPolicy()
|
||||
login_info["known_hosts"] = None
|
||||
@@ -124,6 +130,8 @@ class SSHFileSystem(CallbackMixin, FSSpecWrapper):
|
||||
with as_atomic(self, to_info) as tmp_file:
|
||||
super().upload_fobj(fobj, tmp_file, **kwargs)
|
||||
|
||||
def _upload(self, from_file, to_info, *args, **kwargs):
|
||||
def put_file(
|
||||
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
with as_atomic(self, to_info) as tmp_file:
|
||||
super()._upload(from_file, tmp_file, *args, **kwargs)
|
||||
super().put_file(from_file, tmp_file, callback=callback, **kwargs)
|
||||
|
||||
+137
-25
@@ -1,40 +1,99 @@
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from .base import RemoteActionNotImplemented
|
||||
from .local import LocalFileSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import AnyPath, DvcPath
|
||||
|
||||
from .base import BaseFileSystem
|
||||
from .base import FileSystem
|
||||
from .types import AnyPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def transfer(
|
||||
from_fs: "BaseFileSystem",
|
||||
from_info: "AnyPath",
|
||||
to_fs: "BaseFileSystem",
|
||||
to_info: "DvcPath",
|
||||
move: bool = False,
|
||||
def _link(
|
||||
link: "AnyPath",
|
||||
from_fs: "FileSystem",
|
||||
from_path: "AnyPath",
|
||||
to_fs: "FileSystem",
|
||||
to_path: "AnyPath",
|
||||
) -> None:
|
||||
use_move = isinstance(from_fs, type(to_fs)) and move
|
||||
if not isinstance(from_fs, type(to_fs)):
|
||||
raise OSError(errno.EXDEV, "can't link across filesystems")
|
||||
|
||||
try:
|
||||
if use_move:
|
||||
return to_fs.move(from_info, to_info)
|
||||
func = getattr(to_fs, link)
|
||||
func(from_path, to_path)
|
||||
except (OSError, AttributeError, RemoteActionNotImplemented) as exc:
|
||||
# NOTE: there are so many potential error codes that a link call can
|
||||
# throw, that is safer to just catch them all and try another link.
|
||||
raise OSError(
|
||||
errno.ENOTSUP, f"'{link}' is not supported by {type(from_fs)}"
|
||||
) from exc
|
||||
|
||||
if isinstance(from_fs, LocalFileSystem):
|
||||
if not isinstance(from_info, from_fs.PATH_CLS):
|
||||
from_info = from_fs.PATH_CLS(from_info)
|
||||
return to_fs.upload(from_info, to_info)
|
||||
|
||||
if isinstance(to_fs, LocalFileSystem):
|
||||
return from_fs.download_file(from_info, to_info)
|
||||
def _copy(
|
||||
from_fs: "FileSystem",
|
||||
from_path: "AnyPath",
|
||||
to_fs: "FileSystem",
|
||||
to_path: "AnyPath",
|
||||
) -> None:
|
||||
if isinstance(from_fs, LocalFileSystem):
|
||||
return to_fs.upload(from_path, to_path)
|
||||
|
||||
with from_fs.open(from_info, mode="rb") as fobj:
|
||||
size = from_fs.getsize(from_info)
|
||||
return to_fs.upload(fobj, to_info, total=size)
|
||||
if isinstance(to_fs, LocalFileSystem):
|
||||
return from_fs.download_file(from_path, to_path)
|
||||
|
||||
with from_fs.open(from_path, mode="rb") as fobj:
|
||||
size = from_fs.getsize(from_path)
|
||||
return to_fs.upload(fobj, to_path, total=size)
|
||||
|
||||
|
||||
def _try_links(
|
||||
links: List["AnyPath"],
|
||||
from_fs: "FileSystem",
|
||||
from_path: "AnyPath",
|
||||
to_fs: "FileSystem",
|
||||
to_path: "AnyPath",
|
||||
) -> None:
|
||||
error = None
|
||||
while links:
|
||||
link = links[0]
|
||||
|
||||
if link == "copy":
|
||||
return _copy(from_fs, from_path, to_fs, to_path)
|
||||
|
||||
try:
|
||||
return _link(link, from_fs, from_path, to_fs, to_path)
|
||||
except OSError as exc:
|
||||
if exc.errno not in [errno.ENOTSUP, errno.EXDEV]:
|
||||
raise
|
||||
error = exc
|
||||
|
||||
del links[0]
|
||||
|
||||
raise OSError(
|
||||
errno.ENOTSUP, "no more link types left to try out"
|
||||
) from error
|
||||
|
||||
|
||||
def transfer(
|
||||
from_fs: "FileSystem",
|
||||
from_path: "AnyPath",
|
||||
to_fs: "FileSystem",
|
||||
to_path: "AnyPath",
|
||||
hardlink: bool = False,
|
||||
links: Optional[List["AnyPath"]] = None,
|
||||
) -> None:
|
||||
try:
|
||||
assert not (hardlink and links)
|
||||
if hardlink:
|
||||
links = links or ["reflink", "hardlink", "copy"]
|
||||
else:
|
||||
links = links or ["reflink", "copy"]
|
||||
_try_links(links, from_fs, from_path, to_fs, to_path)
|
||||
except OSError as exc:
|
||||
# If the target file already exists, we are going to simply
|
||||
# ignore the exception (#4992).
|
||||
@@ -48,9 +107,62 @@ def transfer(
|
||||
and exc.__context__
|
||||
and isinstance(exc.__context__, FileExistsError)
|
||||
):
|
||||
logger.debug("'%s' file already exists, skipping", to_info)
|
||||
if use_move:
|
||||
from_fs.remove(from_info)
|
||||
logger.debug("'%s' file already exists, skipping", to_path)
|
||||
return None
|
||||
|
||||
raise
|
||||
|
||||
|
||||
def _test_link(
|
||||
link: "AnyPath",
|
||||
from_fs: "FileSystem",
|
||||
from_file: "AnyPath",
|
||||
to_fs: "FileSystem",
|
||||
to_file: "AnyPath",
|
||||
) -> bool:
|
||||
try:
|
||||
_try_links([link], from_fs, from_file, to_fs, to_file)
|
||||
except OSError:
|
||||
logger.debug("", exc_info=True)
|
||||
return False
|
||||
|
||||
try:
|
||||
_is_link_func = getattr(to_fs, f"is_{link}")
|
||||
return _is_link_func(to_file)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_links(
|
||||
links: List["AnyPath"],
|
||||
from_fs: "FileSystem",
|
||||
from_path: "AnyPath",
|
||||
to_fs: "FileSystem",
|
||||
to_path: "AnyPath",
|
||||
) -> List["AnyPath"]:
|
||||
from dvc.utils import tmp_fname
|
||||
|
||||
from_file = from_fs.path.join(from_path, tmp_fname())
|
||||
to_file = to_fs.path.join(
|
||||
to_fs.path.parent(to_path),
|
||||
tmp_fname(),
|
||||
)
|
||||
|
||||
from_fs.makedirs(from_fs.path.parent(from_file))
|
||||
with from_fs.open(from_file, "wb") as fobj:
|
||||
fobj.write(b"test")
|
||||
|
||||
ret = []
|
||||
try:
|
||||
for link in links:
|
||||
try:
|
||||
if _test_link(link, from_fs, from_file, to_fs, to_file):
|
||||
ret.append(link)
|
||||
finally:
|
||||
to_fs.remove(to_file)
|
||||
finally:
|
||||
from_fs.remove(from_file)
|
||||
|
||||
return ret
|
||||
|
||||
+27
-18
@@ -1,10 +1,8 @@
|
||||
import logging
|
||||
import threading
|
||||
from functools import lru_cache
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.path_info import WebDAVURLInfo
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .fsspec_wrapper import FSSpecWrapper
|
||||
@@ -15,7 +13,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class WebDAVFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
||||
scheme = Schemes.WEBDAV
|
||||
PATH_CLS = WebDAVURLInfo
|
||||
CAN_TRAVERSE = True
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
REQUIRES = {"webdav4": "webdav4"}
|
||||
@@ -37,14 +34,35 @@ class WebDAVFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str) -> str:
|
||||
from fsspec.utils import infer_storage_options
|
||||
|
||||
return infer_storage_options(path)["path"].lstrip("/")
|
||||
|
||||
def unstrip_protocol(self, path: str) -> str:
|
||||
return self.fs_args["base_url"] + "/" + path
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
path_info = WebDAVURLInfo(urlpath)
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
parsed = urlparse(urlpath)
|
||||
scheme = parsed.scheme.replace("webdav", "http")
|
||||
return {
|
||||
"prefix": path_info.path.rstrip("/"),
|
||||
"host": path_info.replace(path="").url,
|
||||
"url": path_info.url.rstrip("/"),
|
||||
"user": path_info.user,
|
||||
"prefix": parsed.path.rstrip("/"),
|
||||
"host": urlunparse((scheme, parsed.netloc, "", None, None, None)),
|
||||
"url": urlunparse(
|
||||
(
|
||||
scheme,
|
||||
parsed.netloc,
|
||||
parsed.path.rstrip("/"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
),
|
||||
"user": parsed.username,
|
||||
}
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
@@ -71,20 +89,11 @@ class WebDAVFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
||||
return WebdavFileSystem(**self.fs_args)
|
||||
|
||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
||||
rpath = self.translate_path_info(to_info)
|
||||
size = kwargs.get("size")
|
||||
# using upload_fileobj to directly upload fileobj rather than buffering
|
||||
# and using overwrite=True to avoid check for an extra exists call,
|
||||
# as caller should ensure that the file does not exist beforehand.
|
||||
return self.fs.upload_fileobj(fobj, rpath, overwrite=True, size=size)
|
||||
|
||||
@lru_cache(512)
|
||||
def translate_path_info(self, path):
|
||||
if isinstance(path, self.PATH_CLS):
|
||||
return path.path[len(self.prefix) :].lstrip("/")
|
||||
return path
|
||||
|
||||
_with_bucket = translate_path_info
|
||||
return self.fs.upload_fileobj(fobj, to_info, overwrite=True, size=size)
|
||||
|
||||
|
||||
class WebDAVSFileSystem(WebDAVFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
+28
-143
@@ -1,51 +1,29 @@
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import shutil
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .base import BaseFileSystem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# pylint:disable=abstract-method
|
||||
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
|
||||
|
||||
|
||||
def update_pbar(pbar, total):
|
||||
"""Update pbar to accept the two arguments passed by hdfs"""
|
||||
|
||||
def update(_, bytes_transfered):
|
||||
if bytes_transfered == -1:
|
||||
pbar.update_to(total)
|
||||
return
|
||||
pbar.update_to(bytes_transfered)
|
||||
|
||||
return update
|
||||
|
||||
|
||||
class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
class WebHDFSFileSystem(CallbackMixin, FSSpecWrapper):
|
||||
scheme = Schemes.WEBHDFS
|
||||
PATH_CLS = CloudURLInfo
|
||||
REQUIRES = {"hdfs": "hdfs"}
|
||||
REQUIRES = {"fsspec": "fsspec"}
|
||||
PARAM_CHECKSUM = "checksum"
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str) -> str:
|
||||
from fsspec.utils import infer_storage_options
|
||||
|
||||
self.host = config["host"]
|
||||
self.user = config.get("user")
|
||||
self.port = config.get("port")
|
||||
return infer_storage_options(path)["path"]
|
||||
|
||||
self.hdfscli_config = config.get("hdfscli_config")
|
||||
self.token = config.get("webhdfs_token")
|
||||
self.alias = config.get("webhdfs_alias")
|
||||
def unstrip_protocol(self, path: str) -> str:
|
||||
host = self.fs_args["host"]
|
||||
port = self.fs_args["port"]
|
||||
path = path.lstrip("/")
|
||||
return f"webhdfs://{host}:{port}/{path}"
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
@@ -57,115 +35,22 @@ class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
)
|
||||
)
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
self._ssl_verify = config.pop("ssl_verify", True)
|
||||
principal = config.pop("kerberos_principal", None)
|
||||
if principal:
|
||||
config["kerb_kwargs"] = {"principal": principal}
|
||||
return config
|
||||
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def hdfs_client(self):
|
||||
import hdfs
|
||||
def fs(self):
|
||||
from fsspec.implementations.webhdfs import WebHDFS
|
||||
|
||||
logger.debug("HDFSConfig: %s", self.hdfscli_config)
|
||||
fs = WebHDFS(**self.fs_args)
|
||||
fs.session.verify = self._ssl_verify
|
||||
return fs
|
||||
|
||||
try:
|
||||
return hdfs.config.Config(self.hdfscli_config).get_client(
|
||||
self.alias
|
||||
)
|
||||
except hdfs.util.HdfsError as exc:
|
||||
exc_msg = str(exc)
|
||||
errors = (
|
||||
"No alias specified",
|
||||
"Invalid configuration file",
|
||||
f"Alias {self.alias} not found",
|
||||
)
|
||||
if not any(err in exc_msg for err in errors):
|
||||
raise
|
||||
|
||||
http_url = f"http://{self.host}:{self.port}"
|
||||
logger.debug("URL: %s", http_url)
|
||||
|
||||
if self.token is not None:
|
||||
client = hdfs.TokenClient(http_url, token=self.token, root="/")
|
||||
else:
|
||||
client = hdfs.InsecureClient(
|
||||
http_url, user=self.user, root="/"
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
@contextmanager
|
||||
def open(self, path_info, mode="r", encoding=None, **kwargs):
|
||||
assert mode in {"r", "rt", "rb"}
|
||||
|
||||
with self.hdfs_client.read(
|
||||
path_info.path, encoding=encoding
|
||||
) as reader:
|
||||
yield reader
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
if not self.exists(path_info):
|
||||
return
|
||||
|
||||
root = path_info.path
|
||||
for path, _, fnames in self.hdfs_client.walk(root):
|
||||
for fname in fnames:
|
||||
yield path_info.replace(path=posixpath.join(path, fname))
|
||||
|
||||
def remove(self, path_info):
|
||||
if path_info.scheme != self.scheme:
|
||||
raise NotImplementedError
|
||||
|
||||
self.hdfs_client.delete(path_info.path)
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
assert not isinstance(path_info, list)
|
||||
assert path_info.scheme == "webhdfs"
|
||||
|
||||
status = self.hdfs_client.status(path_info.path, strict=False)
|
||||
return status is not None
|
||||
|
||||
def info(self, path_info):
|
||||
st = self.hdfs_client.status(path_info.path)
|
||||
return {"size": st["length"], "type": "file"}
|
||||
|
||||
def checksum(self, path_info):
|
||||
return HashInfo(
|
||||
"checksum",
|
||||
self.hdfs_client.checksum(path_info.path)["bytes"],
|
||||
size=self.hdfs_client.status(path_info.path)["length"],
|
||||
)
|
||||
|
||||
def copy(self, from_info, to_info, **_kwargs):
|
||||
with self.hdfs_client.read(from_info.path) as reader:
|
||||
with self.hdfs_client.write(to_info.path) as writer:
|
||||
shutil.copyfileobj(reader, writer)
|
||||
|
||||
def move(self, from_info, to_info):
|
||||
self.hdfs_client.makedirs(to_info.parent.path)
|
||||
self.hdfs_client.rename(from_info.path, to_info.path)
|
||||
|
||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
||||
with self.hdfs_client.write(to_info.path) as fdest:
|
||||
shutil.copyfileobj(fobj, fdest)
|
||||
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
|
||||
):
|
||||
total = os.path.getsize(from_file)
|
||||
with Tqdm(
|
||||
desc=name, total=total, disable=no_progress_bar, bytes=True
|
||||
) as pbar:
|
||||
self.hdfs_client.upload(
|
||||
to_info.path,
|
||||
from_file,
|
||||
overwrite=True,
|
||||
progress=update_pbar(pbar, total),
|
||||
)
|
||||
|
||||
def _download(
|
||||
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
|
||||
):
|
||||
total = self.getsize(from_info)
|
||||
with Tqdm(
|
||||
desc=name, total=total, disable=no_progress_bar, bytes=True
|
||||
) as pbar:
|
||||
self.hdfs_client.download(
|
||||
from_info.path, to_file, progress=update_pbar(pbar, total)
|
||||
)
|
||||
def checksum(self, path):
|
||||
ukey = self.fs.ukey(path)
|
||||
return ukey["bytes"]
|
||||
|
||||
+3
-16
@@ -7,13 +7,8 @@ HASH_DIR_SUFFIX = ".dir"
|
||||
|
||||
@dataclass
|
||||
class HashInfo:
|
||||
PARAM_SIZE = "size"
|
||||
PARAM_NFILES = "nfiles"
|
||||
|
||||
name: Optional[str]
|
||||
value: Optional[str]
|
||||
size: Optional[int] = field(default=None, compare=False)
|
||||
nfiles: Optional[int] = field(default=None, compare=False)
|
||||
obj_name: Optional[str] = field(default=None, compare=False)
|
||||
|
||||
def __bool__(self):
|
||||
@@ -27,15 +22,11 @@ class HashInfo:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
_d = d.copy() if d else {}
|
||||
size = _d.pop(cls.PARAM_SIZE, None)
|
||||
nfiles = _d.pop(cls.PARAM_NFILES, None)
|
||||
|
||||
if not _d:
|
||||
if not d:
|
||||
return cls(None, None)
|
||||
|
||||
((name, value),) = _d.items()
|
||||
return cls(name, value, size=size, nfiles=nfiles)
|
||||
((name, value),) = d.items()
|
||||
return cls(name, value)
|
||||
|
||||
def to_dict(self):
|
||||
ret = OrderedDict()
|
||||
@@ -43,10 +34,6 @@ class HashInfo:
|
||||
return ret
|
||||
|
||||
ret[self.name] = self.value
|
||||
if self.size is not None:
|
||||
ret[self.PARAM_SIZE] = self.size
|
||||
if self.nfiles is not None:
|
||||
ret[self.PARAM_NFILES] = self.nfiles
|
||||
return ret
|
||||
|
||||
@property
|
||||
|
||||
+13
-16
@@ -7,11 +7,10 @@ from itertools import groupby, takewhile
|
||||
from pathspec.patterns import GitWildMatchPattern
|
||||
from pathspec.util import normalize_file
|
||||
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.fs.base import FileSystem
|
||||
from dvc.fs.local import localfs
|
||||
from dvc.pathspec_math import PatternInfo, merge_patterns
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.system import System
|
||||
from dvc.types import AnyPath, List, Optional
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.collections import PathStringTrie
|
||||
@@ -84,7 +83,7 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
else:
|
||||
return False
|
||||
|
||||
if not System.is_unix():
|
||||
if os.name == "nt":
|
||||
path = normalize_file(path)
|
||||
return path
|
||||
|
||||
@@ -268,25 +267,25 @@ class DvcIgnoreFilter:
|
||||
dirs, files = ignore_pattern(abs_root, dirs, files)
|
||||
return dirs, files
|
||||
|
||||
def walk(self, fs: BaseFileSystem, path_info: AnyPath, **kwargs):
|
||||
def walk(self, fs: FileSystem, path: AnyPath, **kwargs):
|
||||
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
|
||||
if fs.scheme == Schemes.LOCAL:
|
||||
for root, dirs, files in fs.walk(path_info, **kwargs):
|
||||
for root, dirs, files in fs.walk(path, **kwargs):
|
||||
dirs[:], files[:] = self(
|
||||
root, dirs, files, ignore_subrepos=ignore_subrepos
|
||||
)
|
||||
yield root, dirs, files
|
||||
else:
|
||||
yield from fs.walk(path_info, **kwargs)
|
||||
yield from fs.walk(path, **kwargs)
|
||||
|
||||
def walk_files(self, fs: BaseFileSystem, path_info: AnyPath, **kwargs):
|
||||
def find(self, fs: FileSystem, path: AnyPath, **kwargs):
|
||||
if fs.scheme == Schemes.LOCAL:
|
||||
for root, _, files in self.walk(fs, path_info, **kwargs):
|
||||
for root, _, files in self.walk(fs, path, **kwargs):
|
||||
for file in files:
|
||||
# NOTE: os.path.join is ~5.5 times slower
|
||||
yield PathInfo(f"{root}{os.sep}{file}")
|
||||
yield f"{root}{os.sep}{file}"
|
||||
else:
|
||||
yield from fs.walk_files(path_info)
|
||||
yield from fs.find(path)
|
||||
|
||||
def _get_trie_pattern(
|
||||
self, dirname, dnames: Optional["List"] = None, ignore_subrepos=True
|
||||
@@ -308,7 +307,7 @@ class DvcIgnoreFilter:
|
||||
dirs = list(
|
||||
takewhile(
|
||||
lambda path: path != prefix,
|
||||
(parent.fspath for parent in PathInfo(dirname).parents),
|
||||
(parent for parent in localfs.path.parents(dirname)),
|
||||
)
|
||||
)
|
||||
dirs.reverse()
|
||||
@@ -344,8 +343,6 @@ class DvcIgnoreFilter:
|
||||
return self._is_ignored(path, False)
|
||||
|
||||
def _outside_repo(self, path):
|
||||
path = PathInfo(path)
|
||||
|
||||
# paths outside of the repo should be ignored
|
||||
path = relpath(path, self.root_dir)
|
||||
if path.startswith("..") or (
|
||||
@@ -374,7 +371,7 @@ class DvcIgnoreFilter:
|
||||
return _no_match(target)
|
||||
|
||||
def is_ignored(
|
||||
self, fs: BaseFileSystem, path: str, ignore_subrepos: bool = True
|
||||
self, fs: FileSystem, path: str, ignore_subrepos: bool = True
|
||||
) -> bool:
|
||||
# NOTE: can't use self.check_ignore(path).match for now, see
|
||||
# https://github.com/iterative/dvc/issues/4555
|
||||
@@ -394,7 +391,7 @@ def init(path):
|
||||
if os.path.exists(dvcignore):
|
||||
return dvcignore
|
||||
|
||||
with open(dvcignore, "w") as fobj:
|
||||
with open(dvcignore, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(
|
||||
"# Add patterns of files dvc should ignore, which could improve\n"
|
||||
"# the performance. Learn more at\n"
|
||||
|
||||
+25
-43
@@ -2,23 +2,17 @@ import itertools
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import uuid
|
||||
|
||||
import psutil
|
||||
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
from dvc import __version__
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.fs import FS_MAP, get_fs_cls, get_fs_config
|
||||
from dvc.fs.utils import test_links
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm.base import SCMError
|
||||
from dvc.system import System
|
||||
from dvc.scm import SCMError
|
||||
from dvc.utils import error_link
|
||||
from dvc.utils.pkg import PKG
|
||||
from dvc.version import __version__
|
||||
|
||||
if PKG is None:
|
||||
package = ""
|
||||
else:
|
||||
package = f"({PKG})"
|
||||
|
||||
try:
|
||||
import importlib.metadata as importlib_metadata
|
||||
@@ -26,6 +20,9 @@ except ImportError: # < 3.8
|
||||
import importlib_metadata # type: ignore[no-redef]
|
||||
|
||||
|
||||
package = "" if PKG is None else f"({PKG})"
|
||||
|
||||
|
||||
def get_dvc_info():
|
||||
info = [
|
||||
f"DVC version: {__version__} {package}",
|
||||
@@ -76,7 +73,7 @@ def _get_caches(cache):
|
||||
|
||||
def _get_remotes(config):
|
||||
schemes = (
|
||||
get_fs_cls(get_fs_config(config, name=remote)).scheme
|
||||
get_fs_cls(get_fs_config(None, config, name=remote)).scheme
|
||||
for remote in config["remote"]
|
||||
)
|
||||
|
||||
@@ -84,35 +81,17 @@ def _get_remotes(config):
|
||||
|
||||
|
||||
def _get_linktype_support_info(repo):
|
||||
odb = repo.odb.local
|
||||
|
||||
links = {
|
||||
"reflink": (System.reflink, None),
|
||||
"hardlink": (System.hardlink, System.is_hardlink),
|
||||
"symlink": (System.symlink, System.is_symlink),
|
||||
}
|
||||
links = test_links(
|
||||
["reflink", "hardlink", "symlink"],
|
||||
odb.fs,
|
||||
odb.fs_path,
|
||||
repo.fs,
|
||||
repo.root_dir,
|
||||
)
|
||||
|
||||
fname = "." + str(uuid.uuid4())
|
||||
src = os.path.join(repo.odb.local.cache_dir, fname)
|
||||
open(src, "w").close()
|
||||
dst = os.path.join(repo.root_dir, fname)
|
||||
|
||||
cache = []
|
||||
|
||||
for name, (link, is_link) in links.items():
|
||||
try:
|
||||
link(src, dst)
|
||||
status = "supported"
|
||||
if is_link and not is_link(dst):
|
||||
status = "broken"
|
||||
os.unlink(dst)
|
||||
except DvcException:
|
||||
status = "not supported"
|
||||
|
||||
if status == "supported":
|
||||
cache.append(name)
|
||||
os.remove(src)
|
||||
|
||||
return ", ".join(cache)
|
||||
return ", ".join(links)
|
||||
|
||||
|
||||
def _get_supported_remotes():
|
||||
@@ -136,11 +115,14 @@ def _get_supported_remotes():
|
||||
|
||||
|
||||
def get_fs_type(path):
|
||||
|
||||
partition = {
|
||||
pathlib.Path(part.mountpoint): (part.fstype + " on " + part.device)
|
||||
for part in psutil.disk_partitions(all=True)
|
||||
}
|
||||
partition = {}
|
||||
for part in psutil.disk_partitions(all=True):
|
||||
if part.fstype != "":
|
||||
try:
|
||||
mountpoint = pathlib.Path(part.mountpoint).resolve()
|
||||
partition[mountpoint] = part.fstype + " on " + part.device
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
# need to follow the symlink: https://github.com/iterative/dvc/issues/5065
|
||||
path = pathlib.Path(path).resolve()
|
||||
|
||||
+2
-13
@@ -176,25 +176,14 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
||||
except flufl.lock.TimeOutError:
|
||||
raise LockError(FAILED_TO_LOCK_MESSAGE)
|
||||
|
||||
def _set_claimfile(self, pid=None):
|
||||
super()._set_claimfile(pid)
|
||||
def _set_claimfile(self):
|
||||
super()._set_claimfile()
|
||||
|
||||
if self._tmp_dir is not None:
|
||||
# Under Windows file path length is limited so we hash it
|
||||
filename = hashlib.md5(self._claimfile.encode()).hexdigest()
|
||||
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
|
||||
|
||||
# Fix for __del__ bug in flufl.lock [1] which is causing errors on
|
||||
# Python shutdown [2].
|
||||
# [1] https://gitlab.com/warsaw/flufl.lock/issues/7
|
||||
# [2] https://github.com/iterative/dvc/issues/2573
|
||||
def __del__(self):
|
||||
try:
|
||||
if self._owned:
|
||||
self.finalize()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False):
|
||||
cls = HardlinkLock if hardlink_lock else Lock
|
||||
|
||||
@@ -142,9 +142,22 @@ class LoggerHandler(logging.StreamHandler):
|
||||
super().handleError(record)
|
||||
raise LoggingException(record)
|
||||
|
||||
def emit_pretty_exception(self, exc, verbose: bool = False):
|
||||
return exc.__pretty_exc__(verbose=verbose)
|
||||
|
||||
def emit(self, record):
|
||||
"""Write to Tqdm's stream so as to not break progress-bars"""
|
||||
try:
|
||||
if record.exc_info:
|
||||
_, exc, *_ = record.exc_info
|
||||
if hasattr(exc, "__pretty_exc__"):
|
||||
try:
|
||||
self.emit_pretty_exception(exc, verbose=_is_verbose())
|
||||
if not _is_verbose():
|
||||
return
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
msg = self.format(record)
|
||||
Tqdm.write(
|
||||
msg, file=self.stream, end=getattr(self, "terminator", "\n")
|
||||
|
||||
+16
-1
@@ -2,8 +2,10 @@ import logging
|
||||
import os
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Optional,
|
||||
Tuple,
|
||||
@@ -181,7 +183,7 @@ class MachineManager:
|
||||
"""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"]) as fobj:
|
||||
with open(config["startup_script"], encoding="utf-8") as fobj:
|
||||
startup_script = fobj.read()
|
||||
else:
|
||||
startup_script = DEFAULT_STARTUP_SCRIPT
|
||||
@@ -200,3 +202,16 @@ class MachineManager:
|
||||
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)
|
||||
|
||||
@@ -4,13 +4,12 @@ from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.ssh import SSHFileSystem
|
||||
from dvc.repo.experiments.executor.base import BaseExecutor
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
class BaseMachineBackend(ABC):
|
||||
def __init__(self, tmp_dir: "StrPath", **kwargs):
|
||||
raise NotImplementedError
|
||||
self.tmp_dir = tmp_dir
|
||||
|
||||
@abstractmethod
|
||||
def create(self, name: Optional[str] = None, **config):
|
||||
@@ -34,10 +33,8 @@ class BaseMachineBackend(ABC):
|
||||
"""Spawn an interactive SSH shell for the specified machine."""
|
||||
|
||||
@abstractmethod
|
||||
def get_executor(
|
||||
self, name: Optional[str] = None, **config
|
||||
) -> "BaseExecutor":
|
||||
"""Return an executor instance which can be used for DVC
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -48,3 +45,7 @@ class BaseMachineBackend(ABC):
|
||||
) -> 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,35 +1,103 @@
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from functools import partial, partialmethod
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
from tpi import TerraformProviderIterative, terraform
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.fs.ssh import SSHFileSystem
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
from .base import BaseMachineBackend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo.experiments.executor.base import BaseExecutor
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
class TerraformBackend(terraform.TerraformBackend, BaseMachineBackend):
|
||||
def __init__(self, tmp_dir: "StrPath", **kwargs):
|
||||
super().__init__(tmp_dir)
|
||||
@contextmanager
|
||||
def _sshfs(resource: dict):
|
||||
from tpi import TerraformProviderIterative
|
||||
|
||||
def get_executor(
|
||||
self, name: Optional[str] = None, **config
|
||||
) -> "BaseExecutor":
|
||||
raise NotImplementedError
|
||||
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)
|
||||
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)
|
||||
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": SSHFileSystem.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"]:
|
||||
resource = self._default_resource(name)
|
||||
with TerraformProviderIterative.pemfile(resource) as pem:
|
||||
fs = SSHFileSystem(
|
||||
host=resource["instance_ip"],
|
||||
user="ubuntu",
|
||||
keyfile=pem,
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -14,8 +14,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def check(odb: "ObjectDB", obj: "HashFile", **kwargs):
|
||||
if isinstance(obj, Tree):
|
||||
for _, entry in obj:
|
||||
odb.check(entry.hash_info, **kwargs)
|
||||
for _, _, oid in obj:
|
||||
odb.check(oid, **kwargs)
|
||||
|
||||
odb.check(obj.hash_info, **kwargs)
|
||||
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import logging
|
||||
from itertools import chain
|
||||
|
||||
from dvc import prompt
|
||||
from dvc.exceptions import CacheLinkError, CheckoutError, ConfirmRemoveError
|
||||
from dvc.fs.utils import test_links, transfer
|
||||
from dvc.ignore import DvcIgnoreFilter
|
||||
from dvc.objects.db.slow_link_detection import ( # type: ignore[attr-defined]
|
||||
slow_link_guard,
|
||||
)
|
||||
from dvc.objects.diff import ROOT
|
||||
from dvc.objects.diff import diff as odiff
|
||||
from dvc.objects.stage import stage
|
||||
from dvc.types import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _remove(fs_path, fs, in_cache, force=False):
|
||||
if not fs.exists(fs_path):
|
||||
return
|
||||
|
||||
if force:
|
||||
fs.remove(fs_path)
|
||||
return
|
||||
|
||||
if not in_cache:
|
||||
msg = (
|
||||
f"file/directory '{fs_path}' is going to be removed. "
|
||||
"Are you sure you want to proceed?"
|
||||
)
|
||||
|
||||
if not prompt.confirm(msg):
|
||||
raise ConfirmRemoveError(fs_path)
|
||||
|
||||
fs.remove(fs_path)
|
||||
|
||||
|
||||
def _relink(link, cache, cache_info, fs, path, in_cache, force):
|
||||
_remove(path, fs, in_cache, force=force)
|
||||
link(cache, cache_info, fs, path)
|
||||
# NOTE: Depending on a file system (e.g. on NTFS), `_remove` might reset
|
||||
# read-only permissions in order to delete a hardlink to protected object,
|
||||
# which will also reset it for the object itself, making it unprotected,
|
||||
# so we need to protect it back.
|
||||
cache.protect(cache_info)
|
||||
|
||||
|
||||
def _checkout_file(
|
||||
link,
|
||||
fs_path,
|
||||
fs,
|
||||
change,
|
||||
cache,
|
||||
force,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
state=None,
|
||||
):
|
||||
"""The file is changed we need to checkout a new copy"""
|
||||
modified = False
|
||||
|
||||
cache_fs_path = cache.hash_to_path(change.new.oid.value)
|
||||
if change.old.oid:
|
||||
if relink:
|
||||
if fs.iscopy(fs_path) and cache.cache_types[0] == "copy":
|
||||
cache.unprotect(fs_path)
|
||||
else:
|
||||
_relink(
|
||||
link,
|
||||
cache,
|
||||
cache_fs_path,
|
||||
fs,
|
||||
fs_path,
|
||||
change.old.in_cache,
|
||||
force=force,
|
||||
)
|
||||
else:
|
||||
modified = True
|
||||
_relink(
|
||||
link,
|
||||
cache,
|
||||
cache_fs_path,
|
||||
fs,
|
||||
fs_path,
|
||||
change.old.in_cache,
|
||||
force=force,
|
||||
)
|
||||
else:
|
||||
link(cache, cache_fs_path, fs, fs_path)
|
||||
modified = True
|
||||
|
||||
if state:
|
||||
state.save(fs_path, fs, change.new.oid)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(fs_path)
|
||||
|
||||
return modified
|
||||
|
||||
|
||||
def _diff(
|
||||
fs_path,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
relink=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
):
|
||||
old = None
|
||||
try:
|
||||
_, _, old = stage(
|
||||
cache,
|
||||
fs_path,
|
||||
fs,
|
||||
obj.hash_info.name if obj else cache.fs.PARAM_CHECKSUM,
|
||||
dry_run=True,
|
||||
dvcignore=dvcignore,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
diff = odiff(old, obj, cache)
|
||||
|
||||
if relink:
|
||||
diff.modified.extend(diff.unchanged)
|
||||
|
||||
return diff
|
||||
|
||||
|
||||
class Link:
|
||||
def __init__(self, links):
|
||||
self._links = links
|
||||
|
||||
@slow_link_guard
|
||||
def __call__(self, cache, from_path, to_fs, to_path):
|
||||
if to_fs.exists(to_path):
|
||||
to_fs.remove(to_path) # broken symlink
|
||||
|
||||
cache.makedirs(cache.fs.path.parent(to_path))
|
||||
try:
|
||||
transfer(cache.fs, from_path, to_fs, to_path, links=self._links)
|
||||
except FileNotFoundError as exc:
|
||||
raise CheckoutError([to_path]) from exc
|
||||
except OSError as exc:
|
||||
raise CacheLinkError([to_path]) from exc
|
||||
|
||||
|
||||
def _checkout(
|
||||
diff,
|
||||
fs_path,
|
||||
fs,
|
||||
cache,
|
||||
force=False,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
state=None,
|
||||
):
|
||||
if not diff:
|
||||
return
|
||||
|
||||
links = test_links(cache.cache_types, cache.fs, cache.fs_path, fs, fs_path)
|
||||
if not links:
|
||||
raise CacheLinkError([fs_path])
|
||||
link = Link(links)
|
||||
|
||||
for change in diff.deleted:
|
||||
entry_path = (
|
||||
fs.path.join(fs_path, *change.old.key)
|
||||
if change.old.key != ROOT
|
||||
else fs_path
|
||||
)
|
||||
_remove(entry_path, fs, change.old.in_cache, force=force)
|
||||
|
||||
failed = []
|
||||
for change in chain(diff.added, diff.modified):
|
||||
entry_path = (
|
||||
fs.path.join(fs_path, *change.new.key)
|
||||
if change.new.key != ROOT
|
||||
else fs_path
|
||||
)
|
||||
if change.new.oid.isdir:
|
||||
fs.makedirs(entry_path)
|
||||
continue
|
||||
|
||||
try:
|
||||
_checkout_file(
|
||||
link,
|
||||
entry_path,
|
||||
fs,
|
||||
change,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
state=state,
|
||||
)
|
||||
except CheckoutError as exc:
|
||||
failed.extend(exc.target_infos)
|
||||
|
||||
if failed:
|
||||
raise CheckoutError(failed)
|
||||
|
||||
|
||||
def checkout(
|
||||
fs_path,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force=False,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
quiet=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
# if scheme(fs_path) not in ["local", cache.fs.scheme]:
|
||||
# raise NotImplementedError
|
||||
|
||||
diff = _diff(
|
||||
fs_path,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
relink=relink,
|
||||
dvcignore=dvcignore,
|
||||
)
|
||||
|
||||
failed = []
|
||||
if not obj:
|
||||
if not quiet:
|
||||
logger.warning(
|
||||
"No file hash info found for '%s'. It won't be created.",
|
||||
fs_path,
|
||||
)
|
||||
failed.append(fs_path)
|
||||
elif not diff:
|
||||
logger.trace("Data '%s' didn't change.", fs_path) # type: ignore
|
||||
|
||||
try:
|
||||
_checkout(
|
||||
diff,
|
||||
fs_path,
|
||||
fs,
|
||||
cache,
|
||||
force=force,
|
||||
progress_callback=progress_callback,
|
||||
relink=relink,
|
||||
state=state,
|
||||
)
|
||||
except CheckoutError as exc:
|
||||
failed.extend(exc.target_infos)
|
||||
|
||||
if diff and state:
|
||||
state.save_link(fs_path, fs)
|
||||
if not failed:
|
||||
state.save(fs_path, fs, obj.hash_info)
|
||||
|
||||
if failed or not diff:
|
||||
if progress_callback and obj:
|
||||
progress_callback(fs_path, len(obj))
|
||||
if failed:
|
||||
raise CheckoutError(failed)
|
||||
return
|
||||
|
||||
return bool(diff) and not relink
|
||||
@@ -6,22 +6,22 @@ if TYPE_CHECKING:
|
||||
from .index import ObjectDBIndexBase
|
||||
|
||||
|
||||
def get_odb(fs, path_info, **config):
|
||||
def get_odb(fs, fs_path, **config):
|
||||
from .base import ObjectDB
|
||||
from .gdrive import GDriveObjectDB
|
||||
from .local import LocalObjectDB
|
||||
from .oss import OSSObjectDB
|
||||
|
||||
if fs.scheme == Schemes.LOCAL:
|
||||
return LocalObjectDB(fs, path_info, **config)
|
||||
return LocalObjectDB(fs, fs_path, **config)
|
||||
|
||||
if fs.scheme == Schemes.GDRIVE:
|
||||
return GDriveObjectDB(fs, path_info, **config)
|
||||
return GDriveObjectDB(fs, fs_path, **config)
|
||||
|
||||
if fs.scheme == Schemes.OSS:
|
||||
return OSSObjectDB(fs, path_info, **config)
|
||||
return OSSObjectDB(fs, fs_path, **config)
|
||||
|
||||
return ObjectDB(fs, path_info, **config)
|
||||
return ObjectDB(fs, fs_path, **config)
|
||||
|
||||
|
||||
def _get_odb(repo, settings):
|
||||
@@ -30,9 +30,9 @@ def _get_odb(repo, settings):
|
||||
if not settings:
|
||||
return None
|
||||
|
||||
cls, config, path_info = get_cloud_fs(repo, **settings)
|
||||
cls, config, fs_path = get_cloud_fs(repo, **settings)
|
||||
config["tmp_dir"] = repo.tmp_dir
|
||||
return get_odb(cls(**config), path_info, state=repo.state, **config)
|
||||
return get_odb(cls(**config), fs_path, state=repo.state, **config)
|
||||
|
||||
|
||||
def get_index(odb) -> "ObjectDBIndexBase":
|
||||
@@ -43,7 +43,9 @@ def get_index(odb) -> "ObjectDBIndexBase":
|
||||
cls = ObjectDBIndex if odb.tmp_dir else ObjectDBIndexNoop
|
||||
return cls(
|
||||
odb.tmp_dir,
|
||||
hashlib.sha256(odb.path_info.url.encode("utf-8")).hexdigest(),
|
||||
hashlib.sha256(
|
||||
odb.fs.unstrip_protocol(odb.fs_path).encode("utf-8")
|
||||
).hexdigest(),
|
||||
odb.fs.CHECKSUM_DIR_SUFFIX,
|
||||
)
|
||||
|
||||
|
||||
+64
-68
@@ -10,9 +10,9 @@ from dvc.objects.file import HashFile
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.fs.base import FileSystem
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.types import AnyPath, DvcPath
|
||||
from dvc.types import AnyPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,15 +23,14 @@ class ObjectDB:
|
||||
DEFAULT_CACHE_TYPES = ["copy"]
|
||||
CACHE_MODE: Optional[int] = None
|
||||
|
||||
def __init__(self, fs: "BaseFileSystem", path_info: "AnyPath", **config):
|
||||
def __init__(self, fs: "FileSystem", path: str, **config):
|
||||
from dvc.state import StateNoop
|
||||
|
||||
self.fs = fs
|
||||
self.path_info = path_info
|
||||
self.fs_path = path
|
||||
self.state = config.get("state", StateNoop())
|
||||
self.verify = config.get("verify", self.DEFAULT_VERIFY)
|
||||
self.cache_types = config.get("type") or copy(self.DEFAULT_CACHE_TYPES)
|
||||
self.cache_type_confirmed = False
|
||||
self.slow_link_warning = config.get("slow_link_warning", True)
|
||||
self.tmp_dir = config.get("tmp_dir")
|
||||
self.read_only = config.get("read_only", False)
|
||||
@@ -50,26 +49,25 @@ class ObjectDB:
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
self.fs == other.fs
|
||||
and self.path_info == other.path_info
|
||||
and self.fs_path == other.fs_path
|
||||
and self.read_only == other.read_only
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.fs.scheme, self.path_info))
|
||||
return hash((self.fs.scheme, self.fs_path))
|
||||
|
||||
def exists(self, hash_info: "HashInfo"):
|
||||
return self.fs.exists(self.hash_to_path_info(hash_info.value))
|
||||
return self.fs.exists(self.hash_to_path(hash_info.value))
|
||||
|
||||
def move(self, from_info, to_info):
|
||||
self.fs.move(from_info, to_info)
|
||||
|
||||
def makedirs(self, path_info):
|
||||
self.fs.makedirs(path_info)
|
||||
def makedirs(self, fs_path):
|
||||
self.fs.makedirs(fs_path)
|
||||
|
||||
def get(self, hash_info: "HashInfo"):
|
||||
"""get raw object"""
|
||||
return HashFile(
|
||||
# Prefer string path over PathInfo when possible due to performance
|
||||
self.hash_to_path(hash_info.value),
|
||||
self.fs,
|
||||
hash_info,
|
||||
@@ -77,27 +75,30 @@ class ObjectDB:
|
||||
|
||||
def _add_file(
|
||||
self,
|
||||
from_fs: "BaseFileSystem",
|
||||
from_fs: "FileSystem",
|
||||
from_info: "AnyPath",
|
||||
to_info: "DvcPath",
|
||||
to_info: "AnyPath",
|
||||
_hash_info: "HashInfo",
|
||||
move: bool = False,
|
||||
hardlink: bool = False,
|
||||
):
|
||||
from dvc import fs
|
||||
|
||||
self.makedirs(to_info.parent)
|
||||
self.makedirs(self.fs.path.parent(to_info))
|
||||
return fs.utils.transfer(
|
||||
from_fs, from_info, self.fs, to_info, move=move
|
||||
from_fs,
|
||||
from_info,
|
||||
self.fs,
|
||||
to_info,
|
||||
hardlink=hardlink,
|
||||
)
|
||||
|
||||
def add(
|
||||
self,
|
||||
path_info: "AnyPath",
|
||||
fs: "BaseFileSystem",
|
||||
fs_path: "AnyPath",
|
||||
fs: "FileSystem",
|
||||
hash_info: "HashInfo",
|
||||
move: bool = True,
|
||||
hardlink: bool = False,
|
||||
verify: Optional[bool] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if self.read_only:
|
||||
raise ObjectDBPermissionError("Cannot add to read-only ODB")
|
||||
@@ -110,39 +111,32 @@ class ObjectDB:
|
||||
except (ObjectFormatError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
cache_info = self.hash_to_path_info(hash_info.value)
|
||||
self._add_file(fs, path_info, cache_info, hash_info, move=move)
|
||||
cache_fs_path = self.hash_to_path(hash_info.value)
|
||||
self._add_file(
|
||||
fs, fs_path, cache_fs_path, hash_info, hardlink=hardlink
|
||||
)
|
||||
|
||||
try:
|
||||
if verify:
|
||||
self.check(hash_info, check_hash=True)
|
||||
self.protect(cache_info)
|
||||
self.state.save(cache_info, self.fs, hash_info)
|
||||
self.protect(cache_fs_path)
|
||||
self.state.save(cache_fs_path, self.fs, hash_info)
|
||||
except (ObjectFormatError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
callback = kwargs.get("download_callback")
|
||||
if callback:
|
||||
callback(1)
|
||||
|
||||
def hash_to_path_info(self, hash_) -> "DvcPath":
|
||||
return self.path_info / hash_[0:2] / hash_[2:]
|
||||
|
||||
# Override to return path as a string instead of PathInfo for clouds
|
||||
# which support string paths (see local)
|
||||
def hash_to_path(self, hash_):
|
||||
return self.hash_to_path_info(hash_)
|
||||
return self.fs.path.join(self.fs_path, hash_[0:2], hash_[2:])
|
||||
|
||||
def protect(self, path_info): # pylint: disable=unused-argument
|
||||
def protect(self, fs_path): # pylint: disable=unused-argument
|
||||
pass
|
||||
|
||||
def is_protected(self, path_info): # pylint: disable=unused-argument
|
||||
def is_protected(self, fs_path): # pylint: disable=unused-argument
|
||||
return False
|
||||
|
||||
def unprotect(self, path_info): # pylint: disable=unused-argument
|
||||
def unprotect(self, fs_path): # pylint: disable=unused-argument
|
||||
pass
|
||||
|
||||
def set_exec(self, path_info): # pylint: disable=unused-argument
|
||||
def set_exec(self, fs_path): # pylint: disable=unused-argument
|
||||
pass
|
||||
|
||||
def check(
|
||||
@@ -165,52 +159,54 @@ class ObjectDB:
|
||||
"""
|
||||
|
||||
obj = self.get(hash_info)
|
||||
if self.is_protected(obj.path_info):
|
||||
if self.is_protected(obj.fs_path):
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"Assuming '%s' is unchanged since it is read-only",
|
||||
obj.path_info,
|
||||
obj.fs_path,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
obj.check(self, check_hash=check_hash)
|
||||
except ObjectFormatError:
|
||||
logger.warning("corrupted cache file '%s'.", obj.path_info)
|
||||
logger.warning("corrupted cache file '%s'.", obj.fs_path)
|
||||
with suppress(FileNotFoundError):
|
||||
self.fs.remove(obj.path_info)
|
||||
self.fs.remove(obj.fs_path)
|
||||
raise
|
||||
|
||||
if check_hash:
|
||||
# making cache file read-only so we don't need to check it
|
||||
# next time
|
||||
self.protect(obj.path_info)
|
||||
self.protect(obj.fs_path)
|
||||
|
||||
def _list_paths(self, prefix=None, progress_callback=None):
|
||||
if prefix:
|
||||
if len(prefix) > 2:
|
||||
path_info = self.path_info / prefix[:2] / prefix[2:]
|
||||
fs_path = self.fs.path.join(
|
||||
self.fs_path, prefix[:2], prefix[2:]
|
||||
)
|
||||
else:
|
||||
path_info = self.path_info / prefix[:2]
|
||||
fs_path = self.fs.path.join(self.fs_path, prefix[:2])
|
||||
prefix = True
|
||||
else:
|
||||
path_info = self.path_info
|
||||
fs_path = self.fs_path
|
||||
prefix = False
|
||||
if progress_callback:
|
||||
for file_info in self.fs.walk_files(path_info, prefix=prefix):
|
||||
for file_info in self.fs.find(fs_path, prefix=prefix):
|
||||
progress_callback()
|
||||
yield file_info.path
|
||||
yield file_info
|
||||
else:
|
||||
yield from self.fs.walk_files(path_info, prefix=prefix)
|
||||
yield from self.fs.find(fs_path, prefix=prefix)
|
||||
|
||||
def _path_to_hash(self, path):
|
||||
parts = self.fs.PATH_CLS(path).parts[-2:]
|
||||
parts = self.fs.path.parts(path)[-2:]
|
||||
|
||||
if not (len(parts) == 2 and parts[0] and len(parts[0]) == 2):
|
||||
raise ValueError(f"Bad cache file path '{path}'")
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
def list_hashes(self, prefix=None, progress_callback=None):
|
||||
def _list_hashes(self, prefix=None, progress_callback=None):
|
||||
"""Iterate over hashes in this fs.
|
||||
|
||||
If `prefix` is specified, only hashes which begin with `prefix`
|
||||
@@ -226,12 +222,12 @@ class ObjectDB:
|
||||
|
||||
def _hashes_with_limit(self, limit, prefix=None, progress_callback=None):
|
||||
count = 0
|
||||
for hash_ in self.list_hashes(prefix, progress_callback):
|
||||
for hash_ in self._list_hashes(prefix, progress_callback):
|
||||
yield hash_
|
||||
count += 1
|
||||
if count > limit:
|
||||
logger.debug(
|
||||
"`list_hashes()` returned max '{}' hashes, "
|
||||
"`_list_hashes()` returned max '{}' hashes, "
|
||||
"skipping remaining results".format(limit)
|
||||
)
|
||||
return
|
||||
@@ -270,7 +266,7 @@ class ObjectDB:
|
||||
max_hashes / total_prefixes, prefix, update
|
||||
)
|
||||
else:
|
||||
hashes = self.list_hashes(prefix, update)
|
||||
hashes = self._list_hashes(prefix, update)
|
||||
|
||||
remote_hashes = set(hashes)
|
||||
if remote_hashes:
|
||||
@@ -280,7 +276,7 @@ class ObjectDB:
|
||||
logger.debug(f"Estimated remote size: {remote_size} files")
|
||||
return remote_size, remote_hashes
|
||||
|
||||
def list_hashes_traverse(
|
||||
def _list_hashes_traverse(
|
||||
self, remote_size, remote_hashes, jobs=None, name=None
|
||||
):
|
||||
"""Iterate over all hashes found in this fs.
|
||||
@@ -326,7 +322,7 @@ class ObjectDB:
|
||||
|
||||
def list_with_update(prefix):
|
||||
return list(
|
||||
self.list_hashes(
|
||||
self._list_hashes(
|
||||
prefix=prefix, progress_callback=pbar.update
|
||||
)
|
||||
)
|
||||
@@ -350,10 +346,10 @@ class ObjectDB:
|
||||
)
|
||||
|
||||
if not self.fs.CAN_TRAVERSE:
|
||||
return self.list_hashes()
|
||||
return self._list_hashes()
|
||||
|
||||
remote_size, remote_hashes = self._estimate_remote_size(name=name)
|
||||
return self.list_hashes_traverse(
|
||||
return self._list_hashes_traverse(
|
||||
remote_size, remote_hashes, jobs, name
|
||||
)
|
||||
|
||||
@@ -379,18 +375,18 @@ class ObjectDB:
|
||||
removed = False
|
||||
# hashes must be sorted to ensure we always remove .dir files first
|
||||
for hash_ in sorted(
|
||||
self.all(jobs, str(self.path_info)),
|
||||
self.all(jobs, self.fs_path),
|
||||
key=self.fs.is_dir_hash,
|
||||
reverse=True,
|
||||
):
|
||||
if hash_ in used_hashes:
|
||||
continue
|
||||
path_info = self.hash_to_path_info(hash_)
|
||||
fs_path = self.hash_to_path(hash_)
|
||||
if self.fs.is_dir_hash(hash_):
|
||||
# backward compatibility
|
||||
# pylint: disable=protected-access
|
||||
self._remove_unpacked_dir(hash_)
|
||||
self.fs.remove(path_info)
|
||||
self.fs.remove(fs_path)
|
||||
removed = True
|
||||
|
||||
return removed
|
||||
@@ -407,16 +403,16 @@ class ObjectDB:
|
||||
unit="file",
|
||||
) as pbar:
|
||||
|
||||
def exists_with_progress(path_info):
|
||||
ret = self.fs.exists(path_info)
|
||||
pbar.update_msg(str(path_info))
|
||||
def exists_with_progress(fs_path):
|
||||
ret = self.fs.exists(fs_path)
|
||||
pbar.update_msg(fs_path)
|
||||
return ret
|
||||
|
||||
with ThreadPoolExecutor(
|
||||
max_workers=jobs or self.fs.jobs
|
||||
) as executor:
|
||||
path_infos = map(self.hash_to_path_info, hashes)
|
||||
in_remote = executor.map(exists_with_progress, path_infos)
|
||||
fs_paths = map(self.hash_to_path, hashes)
|
||||
in_remote = executor.map(exists_with_progress, fs_paths)
|
||||
ret = list(itertools.compress(hashes, in_remote))
|
||||
return ret
|
||||
|
||||
@@ -459,7 +455,7 @@ class ObjectDB:
|
||||
|
||||
# During the tests, for ensuring that the traverse behavior
|
||||
# is working we turn on this option. It will ensure the
|
||||
# list_hashes_traverse() is called.
|
||||
# _list_hashes_traverse() is called.
|
||||
always_traverse = getattr(self.fs, "_ALWAYS_TRAVERSE", False)
|
||||
|
||||
hashes = set(hashes)
|
||||
@@ -496,6 +492,6 @@ class ObjectDB:
|
||||
|
||||
logger.debug(f"Querying '{len(hashes)}' hashes via traverse")
|
||||
remote_hashes = set(
|
||||
self.list_hashes_traverse(remote_size, remote_hashes, jobs, name)
|
||||
self._list_hashes_traverse(remote_size, remote_hashes, jobs, name)
|
||||
)
|
||||
return list(hashes & set(remote_hashes))
|
||||
|
||||
+36
-33
@@ -7,7 +7,6 @@ from shortuuid import uuid
|
||||
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.objects.errors import ObjectFormatError
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.fs import copyfile, remove, umask, walk_files
|
||||
@@ -22,9 +21,9 @@ class LocalObjectDB(ObjectDB):
|
||||
CACHE_MODE = 0o444
|
||||
UNPACKED_DIR_SUFFIX = ".unpacked"
|
||||
|
||||
def __init__(self, fs, path_info, **config):
|
||||
super().__init__(fs, path_info, **config)
|
||||
self.cache_dir = path_info.fspath
|
||||
def __init__(self, fs, fs_path, **config):
|
||||
super().__init__(fs, fs_path, **config)
|
||||
self.cache_dir = fs_path
|
||||
|
||||
shared = config.get("shared")
|
||||
if shared:
|
||||
@@ -36,11 +35,11 @@ class LocalObjectDB(ObjectDB):
|
||||
|
||||
@property
|
||||
def cache_dir(self):
|
||||
return self.path_info.fspath if self.path_info else None
|
||||
return self.fs_path if self.fs_path else None
|
||||
|
||||
@cache_dir.setter
|
||||
def cache_dir(self, value):
|
||||
self.path_info = PathInfo(value) if value else None
|
||||
self.fs_path = value
|
||||
|
||||
@cached_property
|
||||
def cache_path(self):
|
||||
@@ -50,10 +49,10 @@ class LocalObjectDB(ObjectDB):
|
||||
super().move(from_info, to_info)
|
||||
os.chmod(to_info, self._file_mode)
|
||||
|
||||
def makedirs(self, path_info):
|
||||
def makedirs(self, fs_path):
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
makedirs(path_info, exist_ok=True, mode=self._dir_mode)
|
||||
makedirs(fs_path, exist_ok=True, mode=self._dir_mode)
|
||||
|
||||
def hash_to_path(self, hash_):
|
||||
# NOTE: `self.cache_path` is already normalized so we can simply use
|
||||
@@ -81,26 +80,30 @@ class LocalObjectDB(ObjectDB):
|
||||
return ret
|
||||
|
||||
def _list_paths(self, prefix=None, progress_callback=None):
|
||||
assert self.path_info is not None
|
||||
assert self.fs_path is not None
|
||||
if prefix:
|
||||
path_info = self.path_info / prefix[:2]
|
||||
if not self.fs.exists(path_info):
|
||||
fs_path = self.fs.path.join(self.fs_path, prefix[:2])
|
||||
if not self.fs.exists(fs_path):
|
||||
return
|
||||
else:
|
||||
path_info = self.path_info
|
||||
fs_path = self.fs_path
|
||||
|
||||
# NOTE: use utils.fs walk_files since fs.walk_files will not follow
|
||||
# symlinks
|
||||
if progress_callback:
|
||||
for path in walk_files(path_info):
|
||||
for path in walk_files(fs_path):
|
||||
progress_callback()
|
||||
yield path
|
||||
else:
|
||||
yield from walk_files(path_info)
|
||||
yield from walk_files(fs_path)
|
||||
|
||||
def _remove_unpacked_dir(self, hash_):
|
||||
info = self.hash_to_path_info(hash_)
|
||||
path_info = info.with_name(info.name + self.UNPACKED_DIR_SUFFIX)
|
||||
self.fs.remove(path_info)
|
||||
hash_fs_path = self.hash_to_path(hash_)
|
||||
fs_path = self.fs.path.with_name(
|
||||
hash_fs_path,
|
||||
self.fs.path.name(hash_fs_path) + self.UNPACKED_DIR_SUFFIX,
|
||||
)
|
||||
self.fs.remove(fs_path)
|
||||
|
||||
def _unprotect_file(self, path):
|
||||
if self.fs.is_symlink(path) or self.fs.is_hardlink(path):
|
||||
@@ -126,47 +129,47 @@ class LocalObjectDB(ObjectDB):
|
||||
os.chmod(path, self._file_mode)
|
||||
|
||||
def _unprotect_dir(self, path):
|
||||
for fname in self.fs.walk_files(path):
|
||||
for fname in self.fs.find(path):
|
||||
self._unprotect_file(fname)
|
||||
|
||||
def unprotect(self, path_info):
|
||||
if not os.path.exists(path_info):
|
||||
def unprotect(self, fs_path):
|
||||
if not os.path.exists(fs_path):
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
raise DvcException(
|
||||
f"can't unprotect non-existing data '{path_info}'"
|
||||
f"can't unprotect non-existing data '{fs_path}'"
|
||||
)
|
||||
|
||||
if os.path.isdir(path_info):
|
||||
self._unprotect_dir(path_info)
|
||||
if os.path.isdir(fs_path):
|
||||
self._unprotect_dir(fs_path)
|
||||
else:
|
||||
self._unprotect_file(path_info)
|
||||
self._unprotect_file(fs_path)
|
||||
|
||||
def protect(self, path_info):
|
||||
def protect(self, fs_path):
|
||||
try:
|
||||
os.chmod(path_info, self.CACHE_MODE)
|
||||
os.chmod(fs_path, self.CACHE_MODE)
|
||||
except OSError:
|
||||
# NOTE: not being able to protect cache file is not fatal, it
|
||||
# might happen on funky filesystems (e.g. Samba, see #5255),
|
||||
# read-only filesystems or in a shared cache scenario.
|
||||
logger.trace("failed to protect '%s'", path_info, exc_info=True)
|
||||
logger.trace("failed to protect '%s'", fs_path, exc_info=True)
|
||||
|
||||
def is_protected(self, path_info):
|
||||
def is_protected(self, fs_path):
|
||||
try:
|
||||
mode = os.stat(path_info).st_mode
|
||||
mode = os.stat(fs_path).st_mode
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
return stat.S_IMODE(mode) == self.CACHE_MODE
|
||||
|
||||
def set_exec(self, path_info):
|
||||
mode = os.stat(path_info).st_mode | stat.S_IEXEC
|
||||
def set_exec(self, fs_path):
|
||||
mode = os.stat(fs_path).st_mode | stat.S_IEXEC
|
||||
try:
|
||||
os.chmod(path_info, mode)
|
||||
os.chmod(fs_path, mode)
|
||||
except OSError:
|
||||
logger.trace(
|
||||
"failed to chmod '%s' '%s'",
|
||||
oct(mode),
|
||||
path_info,
|
||||
fs_path,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
+15
-11
@@ -10,9 +10,9 @@ from ..reference import ReferenceHashFile
|
||||
from .base import ObjectDB
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.fs.base import FileSystem
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.types import AnyPath, DvcPath
|
||||
from dvc.types import AnyPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,7 +26,7 @@ class ReferenceObjectDB(ObjectDB):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._fs_cache: Dict[tuple, "BaseFileSystem"] = {}
|
||||
self._fs_cache: Dict[tuple, "FileSystem"] = {}
|
||||
self._obj_cache: Dict["HashInfo", "ReferenceHashFile"] = {}
|
||||
|
||||
def get(self, hash_info: "HashInfo"):
|
||||
@@ -36,9 +36,9 @@ class ReferenceObjectDB(ObjectDB):
|
||||
return self._obj_cache[hash_info]
|
||||
except KeyError:
|
||||
pass
|
||||
path_info = self.hash_to_path(hash_info.value)
|
||||
fs_path = self.hash_to_path(hash_info.value)
|
||||
try:
|
||||
with self.fs.open(path_info, "rb") as fobj:
|
||||
with self.fs.open(fs_path, "rb") as fobj:
|
||||
ref_file = ReferenceHashFile.from_bytes(
|
||||
fobj.read(), fs_cache=self._fs_cache
|
||||
)
|
||||
@@ -47,23 +47,27 @@ class ReferenceObjectDB(ObjectDB):
|
||||
try:
|
||||
ref_file.check(self, check_hash=False)
|
||||
except ObjectFormatError:
|
||||
self.fs.remove(path_info)
|
||||
self.fs.remove(fs_path)
|
||||
raise
|
||||
self._obj_cache[hash_info] = ref_file
|
||||
return ref_file
|
||||
|
||||
def _add_file(
|
||||
self,
|
||||
from_fs: "BaseFileSystem",
|
||||
from_fs: "FileSystem",
|
||||
from_info: "AnyPath",
|
||||
to_info: "DvcPath",
|
||||
to_info: "AnyPath",
|
||||
hash_info: "HashInfo",
|
||||
move: bool = False,
|
||||
hardlink: bool = False,
|
||||
):
|
||||
self.makedirs(to_info.parent)
|
||||
self.makedirs(self.fs.path.parent(to_info))
|
||||
if hash_info.isdir:
|
||||
return super()._add_file(
|
||||
from_fs, from_info, to_info, hash_info, move
|
||||
from_fs,
|
||||
from_info,
|
||||
to_info,
|
||||
hash_info,
|
||||
hardlink=hardlink,
|
||||
)
|
||||
ref_file = ReferenceHashFile(from_info, from_fs, hash_info)
|
||||
self._obj_cache[hash_info] = ref_file
|
||||
|
||||
@@ -25,15 +25,15 @@ this.message = (
|
||||
|
||||
def slow_link_guard(f):
|
||||
@wraps(f)
|
||||
def wrapper(odb, *args, **kwargs):
|
||||
def wrapper(self, odb, *args, **kwargs):
|
||||
if this.already_displayed:
|
||||
return f(odb, *args, **kwargs)
|
||||
return f(self, odb, *args, **kwargs)
|
||||
|
||||
if not odb.slow_link_warning or odb.cache_types:
|
||||
return f(odb, *args, **kwargs)
|
||||
return f(self, odb, *args, **kwargs)
|
||||
|
||||
start = time.time()
|
||||
result = f(odb, *args, **kwargs)
|
||||
result = f(self, odb, *args, **kwargs)
|
||||
delta = time.time() - start
|
||||
|
||||
if delta >= this.timeout_seconds:
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.hash_info import HashInfo
|
||||
|
||||
from .file import HashFile
|
||||
|
||||
ADD = "add"
|
||||
MODIFY = "modify"
|
||||
DELETE = "delete"
|
||||
UNCHANGED = "unchanged"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeEntry:
|
||||
in_cache: bool
|
||||
key: Tuple[str]
|
||||
oid: Optional["HashInfo"] = field(default=None)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.oid)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, TreeEntry):
|
||||
return False
|
||||
|
||||
if self.key != other.key:
|
||||
return False
|
||||
|
||||
return self.oid == other.oid
|
||||
|
||||
|
||||
@dataclass
|
||||
class Change:
|
||||
old: TreeEntry
|
||||
new: TreeEntry
|
||||
|
||||
@property
|
||||
def typ(self):
|
||||
if not self.old and not self.new:
|
||||
return UNCHANGED
|
||||
|
||||
if self.old and not self.new:
|
||||
return DELETE
|
||||
|
||||
if not self.old and self.new:
|
||||
return ADD
|
||||
|
||||
if self.old != self.new:
|
||||
return MODIFY
|
||||
|
||||
return UNCHANGED
|
||||
|
||||
def __bool__(self):
|
||||
return self.typ != UNCHANGED
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiffResult:
|
||||
added: List[Change] = field(default_factory=list, compare=True)
|
||||
modified: List[Change] = field(default_factory=list, compare=True)
|
||||
deleted: List[Change] = field(default_factory=list, compare=True)
|
||||
unchanged: List[Change] = field(default_factory=list, compare=True)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.added or self.modified or self.deleted)
|
||||
|
||||
|
||||
ROOT = ("",)
|
||||
|
||||
|
||||
def diff(
|
||||
old: Optional["HashFile"], new: Optional["HashFile"], cache
|
||||
) -> DiffResult:
|
||||
from .tree import Tree
|
||||
|
||||
if old is None and new is None:
|
||||
return DiffResult()
|
||||
|
||||
def _get_keys(obj):
|
||||
if not obj:
|
||||
return []
|
||||
return [ROOT] + (
|
||||
[key for key, _, _ in obj] if isinstance(obj, Tree) else []
|
||||
)
|
||||
|
||||
old_keys = set(_get_keys(old))
|
||||
new_keys = set(_get_keys(new))
|
||||
|
||||
def _get_oid(obj, key):
|
||||
if not obj or key == ROOT:
|
||||
return obj.hash_info if obj else None
|
||||
|
||||
entry_obj = obj.get(cache, key)
|
||||
return entry_obj.hash_info if entry_obj else None
|
||||
|
||||
def _in_cache(oid, cache):
|
||||
from .errors import ObjectFormatError
|
||||
|
||||
if not oid:
|
||||
return False
|
||||
|
||||
try:
|
||||
cache.check(oid)
|
||||
return True
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return False
|
||||
|
||||
ret = DiffResult()
|
||||
for key in old_keys | new_keys:
|
||||
old_oid = _get_oid(old, key)
|
||||
new_oid = _get_oid(new, key)
|
||||
|
||||
change = Change(
|
||||
old=TreeEntry(_in_cache(old_oid, cache), key, old_oid),
|
||||
new=TreeEntry(_in_cache(new_oid, cache), key, new_oid),
|
||||
)
|
||||
|
||||
if change.typ == ADD:
|
||||
ret.added.append(change)
|
||||
elif change.typ == MODIFY:
|
||||
ret.modified.append(change)
|
||||
elif change.typ == DELETE:
|
||||
ret.deleted.append(change)
|
||||
else:
|
||||
assert change.typ == UNCHANGED
|
||||
if not change.new.in_cache and not (
|
||||
change.new.oid and change.new.oid.isdir
|
||||
):
|
||||
ret.modified.append(change)
|
||||
else:
|
||||
ret.unchanged.append(change)
|
||||
return ret
|
||||
+11
-15
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from .errors import ObjectFormatError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.fs.base import FileSystem
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.types import AnyPath
|
||||
|
||||
@@ -18,20 +18,16 @@ logger = logging.getLogger(__name__)
|
||||
class HashFile:
|
||||
def __init__(
|
||||
self,
|
||||
path_info: Optional["AnyPath"],
|
||||
fs: Optional["BaseFileSystem"],
|
||||
fs_path: Optional["AnyPath"],
|
||||
fs: Optional["FileSystem"],
|
||||
hash_info: "HashInfo",
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
self.path_info = path_info
|
||||
self.fs_path = fs_path
|
||||
self.fs = fs
|
||||
self.hash_info = hash_info
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self.hash_info.size
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
@@ -45,7 +41,7 @@ class HashFile:
|
||||
if not isinstance(other, HashFile):
|
||||
return False
|
||||
return (
|
||||
self.path_info == other.path_info
|
||||
self.fs_path == other.fs_path
|
||||
and self.fs == other.fs
|
||||
and self.hash_info == other.hash_info
|
||||
)
|
||||
@@ -54,7 +50,7 @@ class HashFile:
|
||||
return hash(
|
||||
(
|
||||
self.hash_info,
|
||||
self.path_info,
|
||||
self.fs_path,
|
||||
self.fs.scheme if self.fs else None,
|
||||
)
|
||||
)
|
||||
@@ -62,9 +58,9 @@ class HashFile:
|
||||
def check(self, odb: "ObjectDB", check_hash: bool = True):
|
||||
if not check_hash:
|
||||
assert self.fs
|
||||
if not self.fs.exists(self.path_info):
|
||||
if not self.fs.exists(self.fs_path):
|
||||
raise FileNotFoundError(
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), self.path_info
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), self.fs_path
|
||||
)
|
||||
else:
|
||||
return None
|
||||
@@ -74,13 +70,13 @@ class HashFile:
|
||||
def _check_hash(self, odb):
|
||||
from .stage import get_file_hash
|
||||
|
||||
actual = get_file_hash(
|
||||
self.path_info, self.fs, self.hash_info.name, odb.state
|
||||
_, actual = get_file_hash(
|
||||
self.fs_path, self.fs, self.hash_info.name, odb.state
|
||||
)
|
||||
|
||||
logger.trace(
|
||||
"cache '%s' expected '%s' actual '%s'",
|
||||
self.path_info,
|
||||
self.fs_path,
|
||||
self.hash_info,
|
||||
actual,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Meta:
|
||||
PARAM_SIZE = "size"
|
||||
PARAM_NFILES = "nfiles"
|
||||
|
||||
size: Optional[int] = field(default=None)
|
||||
nfiles: Optional[int] = field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
if not d:
|
||||
return cls()
|
||||
|
||||
size = d.pop(cls.PARAM_SIZE, None)
|
||||
nfiles = d.pop(cls.PARAM_NFILES, None)
|
||||
return cls(size=size, nfiles=nfiles)
|
||||
|
||||
def to_dict(self):
|
||||
ret = OrderedDict()
|
||||
|
||||
if self.size is not None:
|
||||
ret[self.PARAM_SIZE] = self.size
|
||||
|
||||
if self.nfiles is not None:
|
||||
ret[self.PARAM_NFILES] = self.nfiles
|
||||
|
||||
return ret
|
||||
+17
-18
@@ -8,7 +8,7 @@ from .errors import ObjectFormatError
|
||||
from .file import HashFile
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.fs.base import FileSystem
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.types import AnyPath
|
||||
|
||||
@@ -25,23 +25,23 @@ class ReferenceHashFile(HashFile):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path_info: "AnyPath",
|
||||
fs: "BaseFileSystem",
|
||||
fs_path: "AnyPath",
|
||||
fs: "FileSystem",
|
||||
hash_info: "HashInfo",
|
||||
checksum: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(path_info, fs, hash_info, **kwargs)
|
||||
self.checksum = checksum or fs.checksum(path_info)
|
||||
super().__init__(fs_path, fs, hash_info, **kwargs)
|
||||
self.checksum = checksum or fs.checksum(fs_path)
|
||||
|
||||
def __str__(self):
|
||||
return f"ref object {self.hash_info} -> {self.path_info}"
|
||||
return f"ref object {self.hash_info} -> {self.fs_path}"
|
||||
|
||||
def check(self, odb: "ObjectDB", check_hash: bool = True):
|
||||
assert self.fs
|
||||
if not self.fs.exists(self.path_info):
|
||||
if not self.fs.exists(self.fs_path):
|
||||
raise FileNotFoundError(
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), self.path_info
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), self.fs_path
|
||||
)
|
||||
if self.checksum != self._get_checksum():
|
||||
raise ObjectFormatError(f"{self} is changed")
|
||||
@@ -50,23 +50,22 @@ class ReferenceHashFile(HashFile):
|
||||
|
||||
def _get_checksum(self) -> str:
|
||||
assert self.fs
|
||||
return self.fs.checksum(self.path_info)
|
||||
return self.fs.checksum(self.fs_path)
|
||||
|
||||
def to_bytes(self):
|
||||
from dvc.fs.repo import RepoFileSystem
|
||||
from dvc.path_info import PathInfo
|
||||
|
||||
# NOTE: dumping reference FS's this way is insecure, as the
|
||||
# fully parsed remote FS config will include credentials
|
||||
#
|
||||
# ReferenceHashFiles should currently only be serialized in
|
||||
# memory and not to disk
|
||||
path_info = self.path_info
|
||||
fs_path = self.fs_path
|
||||
if isinstance(self.fs, RepoFileSystem):
|
||||
path_info = PathInfo(path_info).relative_to(self.fs.root_dir)
|
||||
fs_path = self.fs.path.relpath(fs_path, self.fs.root_dir)
|
||||
|
||||
dict_ = {
|
||||
self.PARAM_PATH: path_info,
|
||||
self.PARAM_PATH: fs_path,
|
||||
self.PARAM_HASH: self.hash_info,
|
||||
self.PARAM_CHECKSUM: self.checksum,
|
||||
self.PARAM_FS_CONFIG: self.config_tuple(self.fs),
|
||||
@@ -87,7 +86,7 @@ class ReferenceHashFile(HashFile):
|
||||
raise ObjectFormatError("ReferenceHashFile is corrupted") from exc
|
||||
|
||||
try:
|
||||
path_info = dict_[cls.PARAM_PATH]
|
||||
fs_path = dict_[cls.PARAM_PATH]
|
||||
hash_info = dict_[cls.PARAM_HASH]
|
||||
except KeyError as exc:
|
||||
raise ObjectFormatError("ReferenceHashFile is corrupted") from exc
|
||||
@@ -98,19 +97,19 @@ class ReferenceHashFile(HashFile):
|
||||
config = dict(config_pairs)
|
||||
if RepoFileSystem.PARAM_REPO_URL in config:
|
||||
fs = RepoFileSystem(**config)
|
||||
path_info = fs.root_dir / path_info
|
||||
fs_path = fs.path.join(fs.root_dir, fs_path)
|
||||
else:
|
||||
fs_cls = get_fs_cls(config, scheme=path_info.scheme)
|
||||
fs_cls = get_fs_cls(config, scheme=scheme)
|
||||
fs = fs_cls(**config)
|
||||
return ReferenceHashFile(
|
||||
path_info,
|
||||
fs_path,
|
||||
fs,
|
||||
hash_info,
|
||||
checksum=dict_.get(cls.PARAM_CHECKSUM),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def config_tuple(fs: "BaseFileSystem"):
|
||||
def config_tuple(fs: "FileSystem"):
|
||||
return (
|
||||
fs.scheme,
|
||||
tuple(
|
||||
|
||||
+123
-104
@@ -1,10 +1,9 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Dict, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Tuple
|
||||
|
||||
from dvc.exceptions import DvcIgnoreInCollectedDirError
|
||||
from dvc.hash_info import HashInfo
|
||||
@@ -14,10 +13,11 @@ from dvc.utils import file_md5
|
||||
|
||||
from .db.reference import ReferenceObjectDB
|
||||
from .file import HashFile
|
||||
from .meta import Meta
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.types import AnyPath, DvcPath
|
||||
from dvc.fs.base import FileSystem
|
||||
from dvc.types import AnyPath
|
||||
|
||||
from .db.base import ObjectDB
|
||||
|
||||
@@ -27,74 +27,92 @@ logger = logging.getLogger(__name__)
|
||||
_STAGING_MEMFS_PATH = "dvc-staging"
|
||||
|
||||
|
||||
def _upload_file(path_info, fs, odb, upload_odb):
|
||||
def _upload_file(from_fs_path, fs, odb, upload_odb):
|
||||
from dvc.utils import tmp_fname
|
||||
from dvc.utils.stream import HashedStreamReader
|
||||
|
||||
tmp_info = upload_odb.path_info / tmp_fname()
|
||||
with fs.open(path_info, mode="rb", chunk_size=fs.CHUNK_SIZE) as stream:
|
||||
fs_path = upload_odb.fs.path
|
||||
tmp_info = fs_path.join(upload_odb.fs_path, tmp_fname())
|
||||
with fs.open(from_fs_path, mode="rb", chunk_size=fs.CHUNK_SIZE) as stream:
|
||||
stream = HashedStreamReader(stream)
|
||||
size = fs.getsize(path_info)
|
||||
upload_odb.fs.upload(stream, tmp_info, desc=path_info.name, total=size)
|
||||
size = fs.getsize(from_fs_path)
|
||||
upload_odb.fs.upload(
|
||||
stream, tmp_info, desc=fs_path.name(from_fs_path), total=size
|
||||
)
|
||||
|
||||
odb.add(tmp_info, upload_odb.fs, stream.hash_info)
|
||||
return path_info, odb.get(stream.hash_info)
|
||||
meta = Meta(size=size)
|
||||
return from_fs_path, meta, odb.get(stream.hash_info)
|
||||
|
||||
|
||||
def _get_file_hash(path_info, fs, name):
|
||||
info = fs.info(path_info)
|
||||
def _adapt_info(info, scheme):
|
||||
if scheme == "s3" and "ETag" in info:
|
||||
info["etag"] = info["ETag"].strip('"')
|
||||
elif scheme == "gs" and "etag" in info:
|
||||
import base64
|
||||
|
||||
info["etag"] = base64.b64decode(info["etag"]).hex()
|
||||
elif scheme.startswith("http") and (
|
||||
"ETag" in info or "Content-MD5" in info
|
||||
):
|
||||
info["checksum"] = info.get("ETag") or info.get("Content-MD5")
|
||||
return info
|
||||
|
||||
|
||||
def _get_file_hash(fs_path, fs, name):
|
||||
info = _adapt_info(fs.info(fs_path), fs.scheme)
|
||||
|
||||
if name in info:
|
||||
assert not info[name].endswith(".dir")
|
||||
return HashInfo(name, info[name], size=info["size"])
|
||||
hash_value = info[name]
|
||||
elif hasattr(fs, name):
|
||||
func = getattr(fs, name)
|
||||
hash_value = func(fs_path)
|
||||
elif name == "md5":
|
||||
hash_value = file_md5(fs_path, fs)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
func = getattr(fs, name, None)
|
||||
if func:
|
||||
return func(path_info)
|
||||
|
||||
if name == "md5":
|
||||
return HashInfo(
|
||||
name, file_md5(path_info, fs), size=fs.getsize(path_info)
|
||||
)
|
||||
|
||||
raise NotImplementedError
|
||||
meta = Meta(size=info["size"])
|
||||
hash_info = HashInfo(name, hash_value)
|
||||
return meta, hash_info
|
||||
|
||||
|
||||
def get_file_hash(path_info, fs, name, state=None):
|
||||
def get_file_hash(fs_path, fs, name, state=None):
|
||||
if state:
|
||||
hash_info = state.get( # pylint: disable=assignment-from-none
|
||||
path_info, fs
|
||||
meta, hash_info = state.get( # pylint: disable=assignment-from-none
|
||||
fs_path, fs
|
||||
)
|
||||
if hash_info:
|
||||
return hash_info
|
||||
return meta, hash_info
|
||||
|
||||
hash_info = _get_file_hash(path_info, fs, name)
|
||||
meta, hash_info = _get_file_hash(fs_path, fs, name)
|
||||
|
||||
if state:
|
||||
assert ".dir" not in hash_info.value
|
||||
state.save(path_info, fs, hash_info)
|
||||
state.save(fs_path, fs, hash_info)
|
||||
|
||||
return hash_info
|
||||
return meta, hash_info
|
||||
|
||||
|
||||
def _get_file_obj(
|
||||
path_info, fs, name, odb=None, upload_odb=None, dry_run=False
|
||||
):
|
||||
def _stage_file(fs_path, fs, name, odb=None, upload_odb=None, dry_run=False):
|
||||
state = odb.state if odb else None
|
||||
hash_info = get_file_hash(path_info, fs, name, state=state)
|
||||
meta, hash_info = get_file_hash(fs_path, fs, name, state=state)
|
||||
if upload_odb and not dry_run:
|
||||
assert odb and name == "md5"
|
||||
return _upload_file(path_info, fs, odb, upload_odb)
|
||||
return _upload_file(fs_path, fs, odb, upload_odb)
|
||||
|
||||
if dry_run:
|
||||
obj = HashFile(path_info, fs, hash_info)
|
||||
obj = HashFile(fs_path, fs, hash_info)
|
||||
else:
|
||||
odb.add(path_info, fs, hash_info, move=False)
|
||||
odb.add(fs_path, fs, hash_info, hardlink=False)
|
||||
obj = odb.get(hash_info)
|
||||
return path_info, obj
|
||||
|
||||
return fs_path, meta, obj
|
||||
|
||||
|
||||
def _build_objects(
|
||||
path_info,
|
||||
fs_path,
|
||||
fs,
|
||||
name,
|
||||
dvcignore=None,
|
||||
@@ -103,9 +121,9 @@ def _build_objects(
|
||||
**kwargs,
|
||||
):
|
||||
if dvcignore:
|
||||
walk_iterator = dvcignore.walk_files(fs, path_info)
|
||||
walk_iterator = dvcignore.find(fs, fs_path)
|
||||
else:
|
||||
walk_iterator = fs.walk_files(path_info)
|
||||
walk_iterator = fs.find(fs_path)
|
||||
with Tqdm(
|
||||
unit="md5",
|
||||
desc="Computing file/dir hashes (only done once)",
|
||||
@@ -113,7 +131,7 @@ def _build_objects(
|
||||
) as pbar:
|
||||
worker = pbar.wrap_fn(
|
||||
partial(
|
||||
_get_file_obj,
|
||||
_stage_file,
|
||||
fs=fs,
|
||||
name=name,
|
||||
**kwargs,
|
||||
@@ -125,17 +143,18 @@ def _build_objects(
|
||||
yield from executor.map(worker, walk_iterator)
|
||||
|
||||
|
||||
def _iter_objects(path_info, fs, name, **kwargs):
|
||||
yield from _build_objects(path_info, fs, name, **kwargs)
|
||||
def _iter_objects(fs_path, fs, name, **kwargs):
|
||||
yield from _build_objects(fs_path, fs, name, **kwargs)
|
||||
|
||||
|
||||
def _build_tree(path_info, fs, name, **kwargs):
|
||||
def _build_tree(fs_path, fs, name, **kwargs):
|
||||
from .tree import Tree
|
||||
|
||||
tree_meta = Meta(size=0, nfiles=0)
|
||||
tree = Tree(None, None, None)
|
||||
for file_info, obj in _iter_objects(path_info, fs, name, **kwargs):
|
||||
if DvcIgnore.DVCIGNORE_FILE == file_info.name:
|
||||
raise DvcIgnoreInCollectedDirError(file_info.parent)
|
||||
for file_fs_path, meta, obj in _iter_objects(fs_path, fs, name, **kwargs):
|
||||
if DvcIgnore.DVCIGNORE_FILE == fs.path.name(file_fs_path):
|
||||
raise DvcIgnoreInCollectedDirError(fs.path.parent(file_fs_path))
|
||||
|
||||
# NOTE: this is lossy transformation:
|
||||
# "hey\there" -> "hey/there"
|
||||
@@ -145,11 +164,18 @@ def _build_tree(path_info, fs, name, **kwargs):
|
||||
#
|
||||
# Yes, this is a BUG, as long as we permit "/" in
|
||||
# filenames on Windows and "\" on Unix
|
||||
tree.add(file_info.relative_to(path_info).parts, obj)
|
||||
return tree
|
||||
|
||||
key = fs.path.relparts(file_fs_path, fs_path)
|
||||
assert key
|
||||
tree.add(key, meta, obj.hash_info)
|
||||
|
||||
tree_meta.size += meta.size
|
||||
tree_meta.nfiles += 1
|
||||
|
||||
return tree_meta, tree
|
||||
|
||||
|
||||
def _get_tree_obj(path_info, fs, fs_info, name, odb=None, **kwargs):
|
||||
def _stage_tree(fs_path, fs, fs_info, name, odb=None, **kwargs):
|
||||
from .tree import Tree
|
||||
|
||||
value = fs_info.get(name)
|
||||
@@ -157,51 +183,50 @@ def _get_tree_obj(path_info, fs, fs_info, name, odb=None, **kwargs):
|
||||
hash_info = HashInfo(name, value)
|
||||
try:
|
||||
tree = Tree.load(odb, hash_info)
|
||||
# NOTE: loaded entries are naive objects with hash_infos but no
|
||||
# path_info. For staging trees, obj.path_info should be relative
|
||||
# to the staging src `path_info` and src fs
|
||||
for key, entry in tree:
|
||||
entry.fs = fs
|
||||
entry.path_info = path_info.joinpath(*key)
|
||||
return tree
|
||||
return Meta(nfiles=len(tree)), tree
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
tree = _build_tree(path_info, fs, name, odb=odb, **kwargs)
|
||||
meta, tree = _build_tree(fs_path, fs, name, odb=odb, **kwargs)
|
||||
state = odb.state if odb and odb.state else None
|
||||
hash_info = None
|
||||
if state:
|
||||
hash_info = state.get( # pylint: disable=assignment-from-none
|
||||
path_info, fs
|
||||
_, hash_info = state.get( # pylint: disable=assignment-from-none
|
||||
fs_path, fs
|
||||
)
|
||||
tree.digest(hash_info=hash_info)
|
||||
odb.add(tree.path_info, tree.fs, tree.hash_info, move=True)
|
||||
odb.add(tree.fs_path, tree.fs, tree.hash_info, hardlink=False)
|
||||
raw = odb.get(tree.hash_info)
|
||||
# cleanup unneeded memfs tmpfile and return tree based on the
|
||||
# ODB fs/path
|
||||
if odb.fs != tree.fs:
|
||||
tree.fs.remove(tree.path_info)
|
||||
tree.fs.remove(tree.fs_path)
|
||||
tree.fs = raw.fs
|
||||
tree.path_info = raw.path_info
|
||||
return tree
|
||||
tree.fs_path = raw.fs_path
|
||||
return meta, tree
|
||||
|
||||
|
||||
_url_cache: Dict[str, str] = {}
|
||||
|
||||
|
||||
def _make_staging_url(path_info: "AnyPath"):
|
||||
from dvc.path_info import CloudURLInfo
|
||||
def _make_staging_url(
|
||||
fs: "FileSystem", odb: "ObjectDB", fs_path: Optional[str]
|
||||
):
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
url = CloudURLInfo(f"{Schemes.MEMORY}://{_STAGING_MEMFS_PATH}")
|
||||
if path_info:
|
||||
if isinstance(path_info, (str, pathlib.PurePath)):
|
||||
path = os.path.abspath(path_info)
|
||||
else:
|
||||
path = str(path_info)
|
||||
if path not in _url_cache:
|
||||
_url_cache[path] = hashlib.sha256(path.encode("utf-8")).hexdigest()
|
||||
url /= _url_cache[path]
|
||||
url = f"{Schemes.MEMORY}://{_STAGING_MEMFS_PATH}"
|
||||
|
||||
if fs_path is not None:
|
||||
if odb.fs.scheme == Schemes.LOCAL:
|
||||
fs_path = os.path.abspath(fs_path)
|
||||
|
||||
if fs_path not in _url_cache:
|
||||
_url_cache[fs_path] = hashlib.sha256(
|
||||
fs_path.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
url = fs.path.join(url, _url_cache[fs_path])
|
||||
|
||||
return url
|
||||
|
||||
|
||||
@@ -214,18 +239,18 @@ def _get_staging(odb: "ObjectDB") -> "ObjectDB":
|
||||
from dvc.fs.memory import MemoryFileSystem
|
||||
|
||||
fs = MemoryFileSystem()
|
||||
path_info = _make_staging_url(odb.path_info)
|
||||
fs_path = _make_staging_url(fs, odb, odb.fs_path)
|
||||
state = odb.state
|
||||
return ReferenceObjectDB(fs, path_info, state=state)
|
||||
return ReferenceObjectDB(fs, fs_path, state=state)
|
||||
|
||||
|
||||
def _load_from_state(odb, staging, path_info, fs, name):
|
||||
def _load_from_state(odb, staging, fs_path, fs, name):
|
||||
from . import check, load
|
||||
from .errors import ObjectFormatError
|
||||
from .tree import Tree
|
||||
|
||||
state = odb.state
|
||||
hash_info = state.get(path_info, fs)
|
||||
meta, hash_info = state.get(fs_path, fs)
|
||||
if hash_info:
|
||||
for odb_ in (odb, staging):
|
||||
if odb_.exists(hash_info):
|
||||
@@ -233,16 +258,9 @@ def _load_from_state(odb, staging, path_info, fs, name):
|
||||
obj = load(odb_, hash_info)
|
||||
check(odb_, obj, check_hash=False)
|
||||
if isinstance(obj, Tree):
|
||||
obj.hash_info.nfiles = len(obj)
|
||||
for key, entry in obj:
|
||||
entry.fs = fs
|
||||
entry.path_info = path_info.joinpath(*key)
|
||||
else:
|
||||
obj.fs = fs
|
||||
obj.path_info = path_info
|
||||
meta.nfiles = len(obj)
|
||||
assert obj.hash_info.name == name
|
||||
obj.hash_info.size = hash_info.size
|
||||
return odb_, obj
|
||||
return odb_, meta, obj
|
||||
except (ObjectFormatError, FileNotFoundError):
|
||||
pass
|
||||
raise FileNotFoundError
|
||||
@@ -255,10 +273,10 @@ def _stage_external_tree_info(odb, tree, name):
|
||||
# that would be universal for all clouds.
|
||||
assert odb and name != "md5"
|
||||
|
||||
odb.add(tree.path_info, tree.fs, tree.hash_info)
|
||||
odb.add(tree.fs_path, tree.fs, tree.hash_info)
|
||||
raw = odb.get(tree.hash_info)
|
||||
hash_info = get_file_hash(raw.path_info, raw.fs, name, state=odb.state)
|
||||
tree.path_info = raw.path_info
|
||||
_, hash_info = get_file_hash(raw.fs_path, raw.fs, name, state=odb.state)
|
||||
tree.fs_path = raw.fs_path
|
||||
tree.fs = raw.fs
|
||||
tree.hash_info.name = hash_info.name
|
||||
tree.hash_info.value = hash_info.value
|
||||
@@ -269,13 +287,13 @@ def _stage_external_tree_info(odb, tree, name):
|
||||
|
||||
def stage(
|
||||
odb: "ObjectDB",
|
||||
path_info: "DvcPath",
|
||||
fs: "BaseFileSystem",
|
||||
fs_path: "AnyPath",
|
||||
fs: "FileSystem",
|
||||
name: str,
|
||||
upload: bool = False,
|
||||
dry_run: bool = False,
|
||||
**kwargs,
|
||||
) -> Tuple["ObjectDB", "HashFile"]:
|
||||
) -> Tuple["ObjectDB", "Meta", "HashFile"]:
|
||||
"""Stage (prepare) objects from the given path for addition to an ODB.
|
||||
|
||||
Returns at tuple of (staging_odb, object) where addition to the ODB can
|
||||
@@ -289,19 +307,20 @@ def stage(
|
||||
ODB filesystem, and staged objects will reference the uploaded path rather
|
||||
than the original source path.
|
||||
"""
|
||||
assert path_info and path_info.scheme == fs.scheme
|
||||
assert fs_path
|
||||
# assert scheme(fs_path) == fs.scheme
|
||||
|
||||
details = fs.info(path_info)
|
||||
details = fs.info(fs_path)
|
||||
staging = _get_staging(odb)
|
||||
if odb:
|
||||
try:
|
||||
return _load_from_state(odb, staging, path_info, fs, name)
|
||||
return _load_from_state(odb, staging, fs_path, fs, name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
if details["type"] == "directory":
|
||||
obj = _get_tree_obj(
|
||||
path_info,
|
||||
meta, obj = _stage_tree(
|
||||
fs_path,
|
||||
fs,
|
||||
details,
|
||||
name,
|
||||
@@ -314,8 +333,8 @@ def stage(
|
||||
if name != "md5":
|
||||
obj = _stage_external_tree_info(odb, obj, name)
|
||||
else:
|
||||
_, obj = _get_file_obj(
|
||||
path_info,
|
||||
_, meta, obj = _stage_file(
|
||||
fs_path,
|
||||
fs,
|
||||
name,
|
||||
odb=staging,
|
||||
@@ -324,6 +343,6 @@ def stage(
|
||||
)
|
||||
|
||||
if odb and odb.state and obj.hash_info:
|
||||
odb.state.save(path_info, fs, obj.hash_info)
|
||||
odb.state.save(fs_path, fs, obj.hash_info)
|
||||
|
||||
return staging, obj
|
||||
return staging, meta, obj
|
||||
|
||||
@@ -56,7 +56,7 @@ def _indexed_dir_hashes(odb, index, dir_objs, name, cache_odb):
|
||||
tree = Tree.load(cache_odb, HashInfo(name, dir_hash))
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
file_hashes = [entry.hash_info.value for _, entry in tree]
|
||||
file_hashes = [oid.value for _, _, oid in tree]
|
||||
if dir_hash not in index:
|
||||
logger.debug(
|
||||
"Indexing new .dir '%s' with '%s' nested files",
|
||||
@@ -86,7 +86,7 @@ def status(
|
||||
exists: objs that exist in odb
|
||||
missing: objs that do not exist in ODB
|
||||
"""
|
||||
logger.debug("Preparing to collect status from '%s'", odb.path_info)
|
||||
logger.debug("Preparing to collect status from '%s'", odb.fs_path)
|
||||
if not name:
|
||||
name = odb.fs.PARAM_CHECKSUM
|
||||
|
||||
@@ -102,9 +102,9 @@ def status(
|
||||
tree = None
|
||||
else:
|
||||
tree = Tree.load(cache_odb, hash_info)
|
||||
for _, entry in tree:
|
||||
assert entry.hash_info and entry.hash_info.value
|
||||
hash_infos[entry.hash_info.value] = entry.hash_info
|
||||
for _, _, oid in tree:
|
||||
assert oid and oid.value
|
||||
hash_infos[oid.value] = oid
|
||||
if index:
|
||||
dir_objs[hash_info.value] = tree
|
||||
hash_infos[hash_info.value] = hash_info
|
||||
@@ -116,7 +116,7 @@ def status(
|
||||
hashes: Set[str] = set(hash_infos.keys())
|
||||
exists: Set[str] = set()
|
||||
|
||||
logger.debug("Collecting status from '%s'", odb.path_info)
|
||||
logger.debug("Collecting status from '%s'", odb.fs_path)
|
||||
if index and hashes:
|
||||
if dir_objs:
|
||||
exists = hashes.intersection(
|
||||
@@ -128,9 +128,7 @@ def status(
|
||||
hashes.difference_update(exists)
|
||||
|
||||
if hashes:
|
||||
exists.update(
|
||||
odb.hashes_exist(hashes, name=str(odb.path_info), **kwargs)
|
||||
)
|
||||
exists.update(odb.hashes_exist(hashes, name=odb.fs_path, **kwargs))
|
||||
return StatusResult(
|
||||
{hash_infos[hash_] for hash_ in exists},
|
||||
{hash_infos[hash_] for hash_ in (hashes - exists)},
|
||||
|
||||
+15
-11
@@ -20,9 +20,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def _log_exceptions(func):
|
||||
@wraps(func)
|
||||
def wrapper(path_info, *args, **kwargs):
|
||||
def wrapper(fs_path, *args, **kwargs):
|
||||
try:
|
||||
func(path_info, *args, **kwargs)
|
||||
func(fs_path, *args, **kwargs)
|
||||
return 0
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# NOTE: this means we ran out of file descriptors and there is no
|
||||
@@ -31,7 +31,7 @@ def _log_exceptions(func):
|
||||
if isinstance(exc, OSError) and exc.errno == errno.EMFILE:
|
||||
raise
|
||||
|
||||
logger.exception("failed to transfer '%s'", path_info)
|
||||
logger.exception("failed to transfer '%s'", fs_path)
|
||||
return 1
|
||||
|
||||
return wrapper
|
||||
@@ -74,7 +74,7 @@ def _do_transfer(
|
||||
dir_obj = find_tree_by_obj_id([cache_odb, src], dir_hash)
|
||||
assert dir_obj
|
||||
|
||||
entry_ids = {entry.hash_info for _, entry in dir_obj}
|
||||
entry_ids = {oid for _, _, oid in dir_obj}
|
||||
bound_file_ids = all_file_ids & entry_ids
|
||||
all_file_ids -= entry_ids
|
||||
|
||||
@@ -87,8 +87,8 @@ def _do_transfer(
|
||||
)
|
||||
logger.error(
|
||||
"failed to upload '%s' to '%s'",
|
||||
src.get(dir_obj.hash_info).path_info,
|
||||
dest.get(dir_obj.hash_info).path_info,
|
||||
src.get(dir_obj.hash_info).fs_path,
|
||||
dest.get(dir_obj.hash_info).fs_path,
|
||||
)
|
||||
total_fails += dir_fails + 1
|
||||
elif entry_ids.intersection(missing_ids):
|
||||
@@ -117,7 +117,7 @@ def _do_transfer(
|
||||
# index successfully pushed dirs
|
||||
if dest_index:
|
||||
for dir_obj in succeeded_dir_objs:
|
||||
file_hashes = {entry.hash_info.value for _, entry in dir_obj}
|
||||
file_hashes = {oid.value for _, _, oid in dir_obj}
|
||||
logger.debug(
|
||||
"Indexing pushed dir '%s' with '%s' nested files",
|
||||
dir_obj.hash_info,
|
||||
@@ -133,7 +133,7 @@ def transfer(
|
||||
obj_ids: Iterable["HashInfo"],
|
||||
jobs: Optional[int] = None,
|
||||
verify: bool = False,
|
||||
move: bool = False,
|
||||
hardlink: bool = False,
|
||||
**kwargs,
|
||||
) -> int:
|
||||
"""Transfer (copy) the specified objects from one ODB to another.
|
||||
@@ -144,8 +144,8 @@ def transfer(
|
||||
|
||||
logger.debug(
|
||||
"Preparing to transfer data from '%s' to '%s'",
|
||||
src.path_info,
|
||||
dest.path_info,
|
||||
src.fs_path,
|
||||
dest.fs_path,
|
||||
)
|
||||
if src == dest:
|
||||
return 0
|
||||
@@ -157,7 +157,11 @@ def transfer(
|
||||
def func(hash_info: "HashInfo") -> None:
|
||||
obj = src.get(hash_info)
|
||||
return dest.add(
|
||||
obj.path_info, obj.fs, obj.hash_info, verify=verify, move=move
|
||||
obj.fs_path,
|
||||
obj.fs,
|
||||
obj.hash_info,
|
||||
verify=verify,
|
||||
hardlink=hardlink,
|
||||
)
|
||||
|
||||
total = len(status.new)
|
||||
|
||||
+33
-42
@@ -12,6 +12,8 @@ from .stage import get_file_hash
|
||||
if TYPE_CHECKING:
|
||||
from dvc.hash_info import HashInfo
|
||||
|
||||
from .meta import Meta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -20,7 +22,7 @@ class Tree(HashFile):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._dict: Dict[Tuple[str], "HashFile"] = {}
|
||||
self._dict: Dict[Tuple[str], Tuple["Meta", "HashInfo"]] = {}
|
||||
|
||||
@cached_property
|
||||
def trie(self):
|
||||
@@ -28,38 +30,34 @@ class Tree(HashFile):
|
||||
|
||||
return Trie(self._dict)
|
||||
|
||||
def add(self, key, obj):
|
||||
def add(self, key: Tuple[str], meta: "Meta", oid: "HashInfo"):
|
||||
self.__dict__.pop("trie", None)
|
||||
self._dict[key] = obj
|
||||
self._dict[key] = (meta, oid)
|
||||
|
||||
def digest(self, hash_info: Optional["HashInfo"] = None):
|
||||
from dvc.fs.memory import MemoryFileSystem
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.utils import tmp_fname
|
||||
|
||||
memfs = MemoryFileSystem()
|
||||
path_info = CloudURLInfo("memory://{}".format(tmp_fname("")))
|
||||
with memfs.open(path_info, "wb") as fobj:
|
||||
fs_path = "memory://{}".format(tmp_fname(""))
|
||||
with memfs.open(fs_path, "wb") as fobj:
|
||||
fobj.write(self.as_bytes())
|
||||
self.fs = memfs
|
||||
self.path_info = path_info
|
||||
self.fs_path = fs_path
|
||||
if hash_info:
|
||||
self.hash_info = hash_info
|
||||
else:
|
||||
self.hash_info = get_file_hash(path_info, memfs, "md5")
|
||||
_, self.hash_info = get_file_hash(fs_path, memfs, "md5")
|
||||
assert self.hash_info.value
|
||||
self.hash_info.value += ".dir"
|
||||
try:
|
||||
self.hash_info.size = sum(obj.size for _, obj in self)
|
||||
except TypeError:
|
||||
self.hash_info.size = None
|
||||
self.hash_info.nfiles = len(self)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._dict)
|
||||
|
||||
def __iter__(self):
|
||||
yield from self._dict.items()
|
||||
yield from (
|
||||
(key, value[0], value[1]) for key, value in self._dict.items()
|
||||
)
|
||||
|
||||
def as_dict(self):
|
||||
return self._dict.copy()
|
||||
@@ -73,10 +71,10 @@ class Tree(HashFile):
|
||||
{
|
||||
# NOTE: not using hash_info.to_dict() because we don't want
|
||||
# size/nfiles fields at this point.
|
||||
obj.hash_info.name: obj.hash_info.value,
|
||||
oid.name: oid.value,
|
||||
self.PARAM_RELPATH: posixpath.sep.join(parts),
|
||||
}
|
||||
for parts, obj in self._dict.items() # noqa: B301
|
||||
for parts, _, oid in self # noqa: B301
|
||||
),
|
||||
key=itemgetter(self.PARAM_RELPATH),
|
||||
)
|
||||
@@ -94,8 +92,7 @@ class Tree(HashFile):
|
||||
relpath = entry.pop(cls.PARAM_RELPATH)
|
||||
parts = tuple(relpath.split(posixpath.sep))
|
||||
hash_info = HashInfo.from_dict(entry)
|
||||
obj = HashFile(None, None, hash_info)
|
||||
tree.add(parts, obj)
|
||||
tree.add(parts, None, hash_info)
|
||||
return tree
|
||||
|
||||
@classmethod
|
||||
@@ -103,7 +100,7 @@ class Tree(HashFile):
|
||||
obj = odb.get(hash_info)
|
||||
|
||||
try:
|
||||
with obj.fs.open(obj.path_info, "r") as fobj:
|
||||
with obj.fs.open(obj.fs_path, "r") as fobj:
|
||||
raw = json.load(fobj)
|
||||
except ValueError as exc:
|
||||
raise ObjectFormatError(f"{obj} is corrupted") from exc
|
||||
@@ -111,15 +108,13 @@ class Tree(HashFile):
|
||||
if not isinstance(raw, list):
|
||||
logger.error(
|
||||
"dir cache file format error '%s' [skipping the file]",
|
||||
obj.path_info,
|
||||
obj.fs_path,
|
||||
)
|
||||
raise ObjectFormatError(f"{obj} is corrupted")
|
||||
|
||||
tree = cls.from_list(raw)
|
||||
tree.path_info = obj.path_info
|
||||
tree.fs_path = obj.fs_path
|
||||
tree.fs = obj.fs
|
||||
for _, entry_obj in tree:
|
||||
entry_obj.fs = obj.fs
|
||||
tree.hash_info = hash_info
|
||||
|
||||
return tree
|
||||
@@ -129,43 +124,42 @@ class Tree(HashFile):
|
||||
inside prefix.
|
||||
|
||||
The returned tree will contain the original tree's hash_info and
|
||||
path_info.
|
||||
fs_path.
|
||||
|
||||
Returns an empty tree if no object exists at the specified prefix.
|
||||
"""
|
||||
tree = Tree(self.path_info, self.fs, self.hash_info)
|
||||
tree = Tree(self.fs_path, self.fs, self.hash_info)
|
||||
try:
|
||||
for key, obj in self.trie.items(prefix):
|
||||
tree.add(key, obj)
|
||||
for key, (meta, oid) in self.trie.items(prefix):
|
||||
tree.add(key, meta, oid)
|
||||
except KeyError:
|
||||
pass
|
||||
return tree
|
||||
|
||||
def get(self, prefix: Tuple[str]) -> Optional[HashFile]:
|
||||
def get(self, odb, prefix: Tuple[str]) -> Optional[HashFile]:
|
||||
"""Return object at the specified prefix in this tree.
|
||||
|
||||
Returns None if no object exists at the specified prefix.
|
||||
"""
|
||||
obj = self._dict.get(prefix)
|
||||
if obj:
|
||||
return obj
|
||||
_, oid = self._dict.get(prefix) or (None, None)
|
||||
if oid:
|
||||
return odb.get(oid)
|
||||
|
||||
tree = Tree(None, None, None)
|
||||
depth = len(prefix)
|
||||
try:
|
||||
for key, obj in self.trie.items(prefix):
|
||||
tree.add(key[depth:], obj)
|
||||
for key, (meta, entry_oid) in self.trie.items(prefix):
|
||||
tree.add(key[depth:], meta, entry_oid)
|
||||
except KeyError:
|
||||
return None
|
||||
tree.digest()
|
||||
return tree
|
||||
|
||||
|
||||
def _get_dir_size(odb, tree):
|
||||
def du(odb, tree):
|
||||
try:
|
||||
return sum(
|
||||
odb.fs.getsize(odb.hash_to_path_info(obj.hash_info.value))
|
||||
for _, obj in tree
|
||||
odb.fs.getsize(odb.hash_to_path(oid.value)) for _, _, oid in tree
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
@@ -226,11 +220,8 @@ def merge(odb, ancestor_info, our_info, their_info):
|
||||
merged_dict = _merge(ancestor.as_dict(), our.as_dict(), their.as_dict())
|
||||
|
||||
merged = Tree(None, None, None)
|
||||
for key, hi in merged_dict.items():
|
||||
merged.add(key, hi)
|
||||
for key, (meta, oid) in merged_dict.items():
|
||||
merged.add(key, meta, oid)
|
||||
merged.digest()
|
||||
|
||||
odb.add(merged.path_info, merged.fs, merged.hash_info)
|
||||
hash_info = merged.hash_info
|
||||
hash_info.size = _get_dir_size(odb, merged)
|
||||
return hash_info
|
||||
return merged
|
||||
|
||||
+108
-88
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set, Type
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -9,7 +8,6 @@ from funcy import collecting, project
|
||||
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
|
||||
|
||||
from dvc import objects, prompt
|
||||
from dvc.checkout import checkout
|
||||
from dvc.exceptions import (
|
||||
CheckoutError,
|
||||
CollectCacheError,
|
||||
@@ -17,6 +15,7 @@ from dvc.exceptions import (
|
||||
MergeError,
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc.objects.checkout import checkout
|
||||
|
||||
from .fs import get_cloud_fs
|
||||
from .fs.hdfs import HDFSFileSystem
|
||||
@@ -26,6 +25,7 @@ from .hash_info import HashInfo
|
||||
from .istextfile import istextfile
|
||||
from .objects import Tree
|
||||
from .objects.errors import ObjectFormatError
|
||||
from .objects.meta import Meta
|
||||
from .objects.stage import stage as ostage
|
||||
from .objects.transfer import transfer as otransfer
|
||||
from .scheme import Schemes
|
||||
@@ -292,7 +292,7 @@ class Output:
|
||||
):
|
||||
self.repo = stage.repo if stage else None
|
||||
|
||||
fs_cls, fs_config, path_info = get_cloud_fs(self.repo, url=path)
|
||||
fs_cls, fs_config, fs_path = get_cloud_fs(self.repo, url=path)
|
||||
self.fs = fs_cls(**fs_config)
|
||||
|
||||
if (
|
||||
@@ -309,13 +309,13 @@ class Output:
|
||||
# here is a list and comments:
|
||||
#
|
||||
# .def_path - path from definition in DVC file
|
||||
# .path_info - PathInfo/URLInfo structured resolved path
|
||||
# .fspath - local only, resolved
|
||||
# .__str__ - for presentation purposes, def_path/relpath
|
||||
#
|
||||
# By resolved path, which contains actual location,
|
||||
# should be absolute and don't contain remote:// refs.
|
||||
self.stage = stage
|
||||
self.meta = Meta.from_dict(info)
|
||||
self.hash_info = HashInfo.from_dict(info)
|
||||
self.use_cache = False if self.IS_DEPENDENCY else cache
|
||||
self.metric = False if self.IS_DEPENDENCY else metric
|
||||
@@ -325,32 +325,32 @@ class Output:
|
||||
self.live = live
|
||||
self.desc = desc
|
||||
|
||||
self.path_info = self._parse_path(self.fs, path_info)
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
if self.use_cache and self.odb is None:
|
||||
raise RemoteCacheRequiredError(self.path_info)
|
||||
raise RemoteCacheRequiredError(self.fs.scheme, self.fs_path)
|
||||
|
||||
self.obj = None
|
||||
self.isexec = False if self.IS_DEPENDENCY else isexec
|
||||
|
||||
self.remote = remote
|
||||
|
||||
def _parse_path(self, fs, path_info):
|
||||
def _parse_path(self, fs, fs_path):
|
||||
if fs.scheme != "local":
|
||||
return path_info
|
||||
return fs_path
|
||||
|
||||
parsed = urlparse(self.def_path)
|
||||
if parsed.scheme != "remote":
|
||||
# NOTE: we can path either from command line or .dvc file,
|
||||
# so we should expect both posix and windows style paths.
|
||||
# PathInfo accepts both, i.e. / works everywhere, \ only on win.
|
||||
# paths accepts both, i.e. / works everywhere, \ only on win.
|
||||
#
|
||||
# FIXME: if we have Windows path containing / or posix one with \
|
||||
# then we have #2059 bug and can't really handle that.
|
||||
if self.stage and not path_info.is_absolute():
|
||||
path_info = self.stage.wdir / path_info
|
||||
if self.stage and not os.path.isabs(fs_path):
|
||||
fs_path = fs.path.join(self.stage.wdir, fs_path)
|
||||
|
||||
abs_p = os.path.abspath(os.path.normpath(path_info))
|
||||
return fs.PATH_CLS(abs_p)
|
||||
abs_p = os.path.abspath(os.path.normpath(fs_path))
|
||||
return abs_p
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_name}: '{def_path}'".format(
|
||||
@@ -370,9 +370,9 @@ class Output:
|
||||
|
||||
cur_dir = os.getcwd()
|
||||
if path_isin(cur_dir, self.repo.root_dir):
|
||||
return relpath(self.path_info, cur_dir)
|
||||
return relpath(self.fs_path, cur_dir)
|
||||
|
||||
return relpath(self.path_info, self.repo.root_dir)
|
||||
return relpath(self.fs_path, self.repo.root_dir)
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
@@ -390,7 +390,7 @@ class Output:
|
||||
return False
|
||||
|
||||
return self.repo and path_isin(
|
||||
os.path.realpath(self.path_info), self.repo.root_dir
|
||||
os.path.realpath(self.fs_path), self.repo.root_dir
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -406,7 +406,9 @@ class Output:
|
||||
|
||||
@property
|
||||
def cache_path(self):
|
||||
return self.odb.hash_to_path_info(self.hash_info.value).url
|
||||
return self.odb.fs.unstrip_protocol(
|
||||
self.odb.hash_to_path(self.hash_info.value)
|
||||
)
|
||||
|
||||
def get_hash(self):
|
||||
if self.use_cache:
|
||||
@@ -415,9 +417,9 @@ class Output:
|
||||
else:
|
||||
odb = self.repo.odb.local
|
||||
name = self.fs.PARAM_CHECKSUM
|
||||
_, obj = ostage(
|
||||
_, _, obj = ostage(
|
||||
odb,
|
||||
self.path_info,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
name,
|
||||
dvcignore=self.dvcignore,
|
||||
@@ -437,10 +439,10 @@ class Output:
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
if self._is_path_dvcignore(self.path_info):
|
||||
if self._is_path_dvcignore(self.fs_path):
|
||||
return False
|
||||
|
||||
return self.fs.exists(self.path_info)
|
||||
return self.fs.exists(self.fs_path)
|
||||
|
||||
def changed_checksum(self):
|
||||
return self.hash_info != self.get_hash()
|
||||
@@ -490,17 +492,17 @@ class Output:
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return self.fs.is_empty(self.path_info)
|
||||
return self.fs.is_empty(self.fs_path)
|
||||
|
||||
def isdir(self):
|
||||
if self._is_path_dvcignore(self.path_info):
|
||||
if self._is_path_dvcignore(self.fs_path):
|
||||
return False
|
||||
return self.fs.isdir(self.path_info)
|
||||
return self.fs.isdir(self.fs_path)
|
||||
|
||||
def isfile(self):
|
||||
if self._is_path_dvcignore(self.path_info):
|
||||
if self._is_path_dvcignore(self.fs_path):
|
||||
return False
|
||||
return self.fs.isfile(self.path_info)
|
||||
return self.fs.isfile(self.fs_path)
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
@@ -511,13 +513,13 @@ class Output:
|
||||
if self.repo.scm.is_tracked(self.fspath):
|
||||
raise OutputAlreadyTrackedError(self)
|
||||
|
||||
self.repo.scm.ignore(self.fspath)
|
||||
self.repo.scm_context.ignore(self.fspath)
|
||||
|
||||
def ignore_remove(self):
|
||||
if not self.use_scm_ignore:
|
||||
return
|
||||
|
||||
self.repo.scm.ignore_remove(self.fspath)
|
||||
self.repo.scm_context.ignore_remove(self.fspath)
|
||||
|
||||
# pylint: enable=no-member
|
||||
|
||||
@@ -537,7 +539,15 @@ class Output:
|
||||
self.verify_metric()
|
||||
|
||||
if not self.use_cache:
|
||||
self.hash_info = self.get_hash()
|
||||
_, self.meta, obj = ostage(
|
||||
self.repo.odb.local,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
dvcignore=self.dvcignore,
|
||||
dry_run=True,
|
||||
)
|
||||
self.hash_info = obj.hash_info
|
||||
if not self.IS_DEPENDENCY:
|
||||
logger.debug(
|
||||
"Output '%s' doesn't use cache. Skipping saving.", self
|
||||
@@ -546,23 +556,19 @@ class Output:
|
||||
|
||||
assert not self.IS_DEPENDENCY
|
||||
|
||||
if not self.changed():
|
||||
logger.debug("Output '%s' didn't change. Skipping saving.", self)
|
||||
return
|
||||
|
||||
_, self.obj = ostage(
|
||||
_, self.meta, self.obj = ostage(
|
||||
self.odb,
|
||||
self.path_info,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
dvcignore=self.dvcignore,
|
||||
)
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.isexec = self.isfile() and self.fs.isexec(self.path_info)
|
||||
self.isexec = self.isfile() and self.fs.isexec(self.fs_path)
|
||||
|
||||
def set_exec(self):
|
||||
if self.isfile() and self.isexec:
|
||||
self.odb.set_exec(self.path_info)
|
||||
self.odb.set_exec(self.fs_path)
|
||||
|
||||
def commit(self, filter_info=None):
|
||||
if not self.exists:
|
||||
@@ -574,14 +580,14 @@ class Output:
|
||||
granular = (
|
||||
self.is_dir_checksum
|
||||
and filter_info
|
||||
and filter_info != self.path_info
|
||||
and filter_info != self.fs_path
|
||||
)
|
||||
if granular:
|
||||
obj = self._commit_granular_dir(filter_info)
|
||||
else:
|
||||
staging, obj = ostage(
|
||||
staging, _, obj = ostage(
|
||||
self.odb,
|
||||
filter_info or self.path_info,
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
dvcignore=self.dvcignore,
|
||||
@@ -591,10 +597,10 @@ class Output:
|
||||
self.odb,
|
||||
{obj.hash_info},
|
||||
shallow=False,
|
||||
move=True,
|
||||
hardlink=True,
|
||||
)
|
||||
checkout(
|
||||
filter_info or self.path_info,
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.odb,
|
||||
@@ -605,30 +611,34 @@ class Output:
|
||||
self.set_exec()
|
||||
|
||||
def _commit_granular_dir(self, filter_info):
|
||||
prefix = filter_info.relative_to(self.path_info).parts
|
||||
staging, save_obj = ostage(
|
||||
prefix = self.fs.path.parts(
|
||||
self.fs.path.relpath(filter_info, self.fs_path)
|
||||
)
|
||||
staging, _, save_obj = ostage(
|
||||
self.odb,
|
||||
self.path_info,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.odb.fs.PARAM_CHECKSUM,
|
||||
dvcignore=self.dvcignore,
|
||||
)
|
||||
save_obj = save_obj.filter(prefix)
|
||||
checkout_obj = save_obj.get(prefix)
|
||||
checkout_obj = save_obj.get(self.odb, prefix)
|
||||
otransfer(
|
||||
staging,
|
||||
self.odb,
|
||||
{save_obj.hash_info} | {entry.hash_info for _, entry in save_obj},
|
||||
{save_obj.hash_info} | {oid for _, _, oid in save_obj},
|
||||
shallow=True,
|
||||
move=True,
|
||||
hardlink=True,
|
||||
)
|
||||
return checkout_obj
|
||||
|
||||
def dumpd(self):
|
||||
ret = copy(self.hash_info.to_dict())
|
||||
ret = {**self.hash_info.to_dict(), **self.meta.to_dict()}
|
||||
|
||||
if self.is_in_repo:
|
||||
path = self.path_info.relpath(self.stage.wdir).as_posix()
|
||||
path = self.fs.path.as_posix(
|
||||
relpath(self.fs_path, self.stage.wdir)
|
||||
)
|
||||
else:
|
||||
path = self.def_path
|
||||
|
||||
@@ -679,22 +689,21 @@ class Output:
|
||||
if not self.metric or self.plot:
|
||||
return
|
||||
|
||||
path = os.fspath(self.path_info)
|
||||
if not os.path.exists(path):
|
||||
if not os.path.exists(self.fs_path):
|
||||
return
|
||||
|
||||
name = "metrics" if self.metric else "plot"
|
||||
if os.path.isdir(path):
|
||||
if os.path.isdir(self.fs_path):
|
||||
msg = "directory '%s' cannot be used as %s."
|
||||
logger.debug(msg, str(self.path_info), name)
|
||||
logger.debug(msg, str(self), name)
|
||||
return
|
||||
|
||||
if not istextfile(path, self.fs):
|
||||
if not istextfile(self.fs_path, self.fs):
|
||||
msg = "binary file '{}' cannot be used as {}."
|
||||
raise DvcException(msg.format(self.path_info, name))
|
||||
raise DvcException(msg.format(self.fs_path, name))
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
self.fs.download(self.path_info, to.path_info, jobs=jobs)
|
||||
self.fs.download(self.fs_path, to.fs_path, jobs=jobs)
|
||||
|
||||
def get_obj(self, filter_info=None, **kwargs):
|
||||
if self.obj:
|
||||
@@ -707,9 +716,10 @@ class Output:
|
||||
else:
|
||||
return None
|
||||
|
||||
if filter_info and filter_info != self.path_info:
|
||||
prefix = filter_info.relative_to(self.path_info).parts
|
||||
obj = obj.get(prefix)
|
||||
fs_path = self.fs.path
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
prefix = fs_path.relparts(filter_info, self.fs_path)
|
||||
obj = obj.get(self.odb, prefix)
|
||||
|
||||
return obj
|
||||
|
||||
@@ -726,12 +736,12 @@ class Output:
|
||||
if not self.use_cache:
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
str(self.path_info), self.get_files_number(filter_info)
|
||||
self.fs_path, self.get_files_number(filter_info)
|
||||
)
|
||||
return None
|
||||
|
||||
obj = self.get_obj(filter_info=filter_info)
|
||||
if not obj and (filter_info and filter_info != self.path_info):
|
||||
if not obj and (filter_info and filter_info != self.fs_path):
|
||||
# backward compatibility
|
||||
return None
|
||||
|
||||
@@ -744,7 +754,7 @@ class Output:
|
||||
|
||||
try:
|
||||
modified = checkout(
|
||||
filter_info or self.path_info,
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.odb,
|
||||
@@ -762,7 +772,7 @@ class Output:
|
||||
return added, False if added else modified
|
||||
|
||||
def remove(self, ignore_remove=False):
|
||||
self.fs.remove(self.path_info)
|
||||
self.fs.remove(self.fs_path)
|
||||
if self.scheme != Schemes.LOCAL:
|
||||
return
|
||||
|
||||
@@ -772,16 +782,16 @@ class Output:
|
||||
def move(self, out):
|
||||
# pylint: disable=no-member
|
||||
if self.scheme == "local" and self.use_scm_ignore:
|
||||
self.repo.scm.ignore_remove(self.fspath)
|
||||
self.repo.scm_context.ignore_remove(self.fspath)
|
||||
|
||||
self.fs.move(self.path_info, out.path_info)
|
||||
self.fs.move(self.fs_path, out.fs_path)
|
||||
self.def_path = out.def_path
|
||||
self.path_info = out.path_info
|
||||
self.fs_path = out.fs_path
|
||||
self.save()
|
||||
self.commit()
|
||||
|
||||
if self.scheme == "local" and self.use_scm_ignore:
|
||||
self.repo.scm.ignore(self.fspath)
|
||||
self.repo.scm_context.ignore(self.fspath)
|
||||
|
||||
def transfer(
|
||||
self, source, odb=None, jobs=None, update=False, no_progress_bar=False
|
||||
@@ -803,7 +813,7 @@ class Output:
|
||||
|
||||
upload = not (update and from_fs.isdir(from_info))
|
||||
jobs = jobs or min((from_fs.jobs, odb.fs.jobs))
|
||||
staging, obj = ostage(
|
||||
staging, self.meta, obj = ostage(
|
||||
odb,
|
||||
from_info,
|
||||
from_fs,
|
||||
@@ -817,7 +827,7 @@ class Output:
|
||||
odb,
|
||||
{obj.hash_info},
|
||||
jobs=jobs,
|
||||
move=upload,
|
||||
hardlink=False,
|
||||
shallow=False,
|
||||
)
|
||||
|
||||
@@ -831,15 +841,15 @@ class Output:
|
||||
if not self.hash_info.isdir:
|
||||
return 1
|
||||
|
||||
if not filter_info or filter_info == self.path_info:
|
||||
return self.hash_info.nfiles or 0
|
||||
if not filter_info or filter_info == self.fs_path:
|
||||
return self.meta.nfiles or 0
|
||||
|
||||
obj = self.get_obj(filter_info=filter_info)
|
||||
return len(obj) if obj else 0
|
||||
|
||||
def unprotect(self):
|
||||
if self.exists:
|
||||
self.odb.unprotect(self.path_info)
|
||||
self.odb.unprotect(self.fs_path)
|
||||
|
||||
def get_dir_cache(self, **kwargs):
|
||||
if not self.is_dir_checksum:
|
||||
@@ -849,8 +859,9 @@ class Output:
|
||||
try:
|
||||
objects.check(self.odb, obj)
|
||||
except FileNotFoundError:
|
||||
remote = self.repo.cloud.get_remote_odb(self.remote)
|
||||
self.repo.cloud.pull([obj.hash_info], odb=remote, **kwargs)
|
||||
if self.remote:
|
||||
kwargs["remote"] = self.remote
|
||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||
|
||||
if self.obj:
|
||||
return self.obj
|
||||
@@ -880,7 +891,7 @@ class Output:
|
||||
"Cache for files inside will be lost. "
|
||||
"Would you like to continue? Use '-f' to force."
|
||||
)
|
||||
if not force and not prompt.confirm(msg.format(self.path_info)):
|
||||
if not force and not prompt.confirm(msg.format(self.fs_path)):
|
||||
raise CollectCacheError(
|
||||
"unable to fully collect used cache"
|
||||
" without cache for directory '{}'".format(self)
|
||||
@@ -888,8 +899,10 @@ class Output:
|
||||
return None
|
||||
|
||||
obj = self.get_obj()
|
||||
if filter_info and filter_info != self.path_info:
|
||||
prefix = filter_info.relative_to(self.path_info).parts
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
prefix = self.fs.path.parts(
|
||||
self.fs.path.relpath(filter_info, self.fs_path)
|
||||
)
|
||||
obj = obj.filter(prefix)
|
||||
return obj
|
||||
|
||||
@@ -945,9 +958,9 @@ class Output:
|
||||
obj.hash_info.obj_name = name
|
||||
oids = {obj.hash_info}
|
||||
if isinstance(obj, Tree):
|
||||
for key, entry_obj in obj:
|
||||
entry_obj.hash_info.obj_name = self.fs.sep.join([name, *key])
|
||||
oids.add(entry_obj.hash_info)
|
||||
for key, _, oid in obj:
|
||||
oid.obj_name = self.fs.sep.join([name, *key])
|
||||
oids.add(oid)
|
||||
return oids
|
||||
|
||||
def get_used_external(
|
||||
@@ -980,8 +993,8 @@ class Output:
|
||||
|
||||
ignored = [
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
HashInfo.PARAM_SIZE,
|
||||
HashInfo.PARAM_NFILES,
|
||||
Meta.PARAM_SIZE,
|
||||
Meta.PARAM_NFILES,
|
||||
]
|
||||
|
||||
for opt in ignored:
|
||||
@@ -999,7 +1012,7 @@ class Output:
|
||||
)
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
from dvc.objects.tree import merge
|
||||
from dvc.objects.tree import du, merge
|
||||
|
||||
assert other
|
||||
|
||||
@@ -1012,13 +1025,20 @@ class Output:
|
||||
self._check_can_merge(self)
|
||||
self._check_can_merge(other)
|
||||
|
||||
self.hash_info = merge(
|
||||
merged = merge(
|
||||
self.odb, ancestor_info, self.hash_info, other.hash_info
|
||||
)
|
||||
self.odb.add(merged.fs_path, merged.fs, merged.hash_info)
|
||||
|
||||
self.hash_info = merged.hash_info
|
||||
self.meta = Meta(
|
||||
size=du(self.odb, merged),
|
||||
nfiles=len(merged),
|
||||
)
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
return self.path_info.fspath
|
||||
return self.fs_path
|
||||
|
||||
@property
|
||||
def is_decorated(self) -> bool:
|
||||
@@ -1039,8 +1059,8 @@ ARTIFACT_SCHEMA = {
|
||||
Output.PARAM_PLOT: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
HashInfo.PARAM_SIZE: int,
|
||||
HashInfo.PARAM_NFILES: int,
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Output.PARAM_ISEXEC: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ from funcy import cached_property, collecting, first, isa, join, reraise
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.parsing.interpolate import ParseError
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.utils import relpath
|
||||
|
||||
from .context import (
|
||||
@@ -134,10 +133,10 @@ def make_definition(
|
||||
|
||||
|
||||
class DataResolver:
|
||||
def __init__(self, repo: "Repo", wdir: PathInfo, d: dict):
|
||||
def __init__(self, repo: "Repo", wdir: str, d: dict):
|
||||
self.fs = fs = repo.fs
|
||||
self.wdir = wdir
|
||||
self.relpath = relpath(self.wdir / "dvc.yaml")
|
||||
self.relpath = relpath(fs.path.join(self.wdir, "dvc.yaml"))
|
||||
|
||||
vars_ = d.get(VARS_KWD, [])
|
||||
check_interpolations(vars_, VARS_KWD, self.relpath)
|
||||
@@ -232,7 +231,7 @@ class EntryDefinition:
|
||||
|
||||
def _resolve_wdir(
|
||||
self, context: Context, name: str, wdir: str = None
|
||||
) -> PathInfo:
|
||||
) -> str:
|
||||
if not wdir:
|
||||
return self.wdir
|
||||
|
||||
@@ -240,7 +239,7 @@ class EntryDefinition:
|
||||
wdir = to_str(context.resolve_str(wdir))
|
||||
except (ContextError, ParseError) as exc:
|
||||
format_and_raise(exc, f"'{self.where}.{name}.wdir'", self.relpath)
|
||||
return self.wdir / wdir
|
||||
return self.resolver.fs.path.join(self.wdir, wdir)
|
||||
|
||||
def resolve(self, **kwargs):
|
||||
try:
|
||||
|
||||
@@ -19,7 +19,6 @@ from dvc.parsing.interpolate import (
|
||||
recurse,
|
||||
str_interpolate,
|
||||
)
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.utils import relpath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -356,7 +355,7 @@ class Context(CtxDict):
|
||||
|
||||
@classmethod
|
||||
def load_from(
|
||||
cls, fs, path: PathInfo, select_keys: List[str] = None
|
||||
cls, fs, path: str, select_keys: List[str] = None
|
||||
) -> "Context":
|
||||
from dvc.utils.serialize import LOADERS
|
||||
|
||||
@@ -396,18 +395,17 @@ class Context(CtxDict):
|
||||
raise ReservedKeyError(matches)
|
||||
return super().merge_update(other, overwrite=overwrite)
|
||||
|
||||
def merge_from(self, fs, item: str, wdir: PathInfo, overwrite=False):
|
||||
def merge_from(self, fs, item: str, wdir: str, overwrite=False):
|
||||
path, _, keys_str = item.partition(":")
|
||||
select_keys = lfilter(bool, keys_str.split(",")) if keys_str else None
|
||||
|
||||
abspath = os.path.abspath(wdir / path)
|
||||
path_info = PathInfo(abspath)
|
||||
abspath = os.path.abspath(fs.path.join(wdir, path))
|
||||
if abspath in self.imports:
|
||||
if not select_keys and self.imports[abspath] is None:
|
||||
return # allow specifying complete filepath multiple times
|
||||
self.check_loaded(abspath, item, select_keys)
|
||||
|
||||
ctx = Context.load_from(fs, path_info, select_keys)
|
||||
ctx = Context.load_from(fs, abspath, select_keys)
|
||||
|
||||
try:
|
||||
self.merge_update(ctx, overwrite=overwrite)
|
||||
@@ -439,12 +437,12 @@ class Context(CtxDict):
|
||||
self,
|
||||
fs,
|
||||
vars_: List,
|
||||
wdir: PathInfo,
|
||||
wdir: str,
|
||||
stage_name: str = None,
|
||||
default: str = None,
|
||||
):
|
||||
if default:
|
||||
to_import = wdir / default
|
||||
to_import = fs.path.join(wdir, default)
|
||||
if fs.exists(to_import):
|
||||
self.merge_from(fs, default, wdir)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class ProcessNotTerminatedError(DvcException):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Managed process '{name}' has not been terminated.")
|
||||
|
||||
|
||||
class ProcessNotFoundError(DvcException):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Managed process '{name}' does not exist.")
|
||||
|
||||
|
||||
class TimeoutExpired(DvcException):
|
||||
def __init__(self, cmd, timeout):
|
||||
super().__init__(
|
||||
f"'{cmd}' did not complete before timeout '{timeout}'"
|
||||
)
|
||||
self.cmd = cmd
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class UnsupportedSignalError(DvcException):
|
||||
def __init__(self, sig):
|
||||
super().__init__(f"Unsupported signal: {sig}")
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Serverless process manager."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import Generator, List, Optional, Tuple, Union
|
||||
|
||||
from funcy.flow import reraise
|
||||
from shortuuid import uuid
|
||||
|
||||
from .exceptions import ProcessNotTerminatedError, UnsupportedSignalError
|
||||
from .process import ManagedProcess, ProcessInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""Manager for controlling background ManagedProcess(es).
|
||||
|
||||
Spawned process entries are kept in the manager directory until they
|
||||
are explicitly removed (with remove() or cleanup()) so that return
|
||||
value and log information can be accessed after a process has completed.
|
||||
"""
|
||||
|
||||
def __init__(self, wdir: Optional[str] = None):
|
||||
self.wdir = wdir or "."
|
||||
|
||||
def __iter__(self) -> Generator[str, None, None]:
|
||||
if not os.path.exists(self.wdir):
|
||||
return
|
||||
for name in os.listdir(self.wdir):
|
||||
yield name
|
||||
|
||||
def __getitem__(self, key: str) -> "ProcessInfo":
|
||||
info_path = os.path.join(self.wdir, key, f"{key}.json")
|
||||
try:
|
||||
with open(info_path, encoding="utf-8") as fobj:
|
||||
return ProcessInfo.from_dict(json.load(fobj))
|
||||
except FileNotFoundError:
|
||||
raise KeyError
|
||||
|
||||
@reraise(FileNotFoundError, KeyError)
|
||||
def __setitem__(self, key: str, value: "ProcessInfo"):
|
||||
info_path = os.path.join(self.wdir, key, f"{key}.json")
|
||||
with open(info_path, "w", encoding="utf-8") as fobj:
|
||||
return json.dump(value.asdict(), fobj)
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
path = os.path.join(self.wdir, key)
|
||||
if os.path.exists(path):
|
||||
remove(path)
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def processes(self) -> Generator[Tuple[str, "ProcessInfo"], None, None]:
|
||||
for name in self:
|
||||
try:
|
||||
yield name, self[name]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
def spawn(self, args: Union[str, List[str]], name: Optional[str] = None):
|
||||
"""Run the given command in the background."""
|
||||
name = name or uuid()
|
||||
pid = ManagedProcess.spawn(
|
||||
args,
|
||||
wdir=os.path.join(self.wdir, name),
|
||||
name=name,
|
||||
)
|
||||
logger.debug(
|
||||
"Spawned managed process '%s' (PID: '%d')",
|
||||
name,
|
||||
pid,
|
||||
)
|
||||
|
||||
def send_signal(self, name: str, sig: int):
|
||||
"""Send `signal` to the specified named process."""
|
||||
process_info = self[name]
|
||||
if sys.platform == "win32":
|
||||
if sig not in (
|
||||
signal.SIGTERM,
|
||||
signal.CTRL_C_EVENT,
|
||||
signal.CTRL_BREAK_EVENT,
|
||||
):
|
||||
raise UnsupportedSignalError(sig)
|
||||
|
||||
def handle_closed_process():
|
||||
logging.warning(
|
||||
f"Process {name} had already aborted unexpectedly."
|
||||
)
|
||||
process_info.returncode = -1
|
||||
self[name] = process_info
|
||||
|
||||
if process_info.returncode is None:
|
||||
try:
|
||||
os.kill(process_info.pid, sig)
|
||||
except ProcessLookupError:
|
||||
handle_closed_process()
|
||||
raise
|
||||
except OSError as exc:
|
||||
if sys.platform == "win32":
|
||||
if exc.winerror == 87:
|
||||
handle_closed_process()
|
||||
raise ProcessLookupError from exc
|
||||
raise
|
||||
|
||||
def terminate(self, name: str):
|
||||
"""Terminate the specified named process."""
|
||||
self.send_signal(name, signal.SIGTERM)
|
||||
|
||||
def kill(self, name: str):
|
||||
"""Kill the specified named process."""
|
||||
if sys.platform == "win32":
|
||||
self.send_signal(name, signal.SIGTERM)
|
||||
else:
|
||||
self.send_signal(name, signal.SIGKILL)
|
||||
|
||||
def remove(self, name: str, force: bool = False):
|
||||
"""Remove the specified named process from this manager.
|
||||
|
||||
If the specified process is still running, it will be forcefully killed
|
||||
if `force` is True`, otherwise an exception will be raised.
|
||||
|
||||
Raises:
|
||||
ProcessNotTerminatedError if the specified process is still
|
||||
running and was not forcefully killed.
|
||||
"""
|
||||
try:
|
||||
process_info = self[name]
|
||||
except KeyError:
|
||||
return
|
||||
if process_info.returncode is None and not force:
|
||||
raise ProcessNotTerminatedError(name)
|
||||
try:
|
||||
self.kill(name)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
del self[name]
|
||||
|
||||
def cleanup(self, force: bool = False):
|
||||
"""Remove stale (terminated) processes from this manager."""
|
||||
for name in self:
|
||||
try:
|
||||
self.remove(name, force)
|
||||
except ProcessNotTerminatedError:
|
||||
continue
|
||||
@@ -0,0 +1,180 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
from contextlib import AbstractContextManager
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import List, Optional, TextIO, Union
|
||||
|
||||
from funcy import cached_property
|
||||
from shortuuid import uuid
|
||||
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
from .exceptions import TimeoutExpired
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessInfo:
|
||||
pid: int
|
||||
stdin: Optional[str]
|
||||
stdout: Optional[str]
|
||||
stderr: Optional[str]
|
||||
returncode: Optional[int]
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
return cls(**d)
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class ManagedProcess(AbstractContextManager):
|
||||
"""Run the specified command with redirected output.
|
||||
|
||||
stdout and stderr will both be redirected to <name>.out.
|
||||
Interactive processes (requiring stdin input) are currently unsupported.
|
||||
|
||||
Parameters:
|
||||
args: Command to be run.
|
||||
wdir: If specified, redirected output files will be placed in `wdir`.
|
||||
name: Name to use for this process, if not specified a UUID will be
|
||||
generated instead.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
args: Union[str, List[str]],
|
||||
wdir: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
self.args: List[str] = (
|
||||
shlex.split(args, posix=os.name == "posix")
|
||||
if isinstance(args, str)
|
||||
else list(args)
|
||||
)
|
||||
self.wdir = wdir
|
||||
self.name = name or uuid()
|
||||
self.returncode: Optional[int] = None
|
||||
self._stdout: Optional[TextIO] = None
|
||||
self._stderr: Optional[TextIO] = None
|
||||
|
||||
self._run()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.wait()
|
||||
|
||||
def _close_fds(self):
|
||||
if self._stdout:
|
||||
self._stdout.close()
|
||||
self._stdout = None
|
||||
if self._stderr:
|
||||
self._stderr.close()
|
||||
self._stderr = None
|
||||
|
||||
def _make_path(self, path: str) -> str:
|
||||
return os.path.join(self.wdir, path) if self.wdir else path
|
||||
|
||||
@cached_property
|
||||
def stdout_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.out")
|
||||
|
||||
@cached_property
|
||||
def info_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.json")
|
||||
|
||||
@cached_property
|
||||
def pidfile_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.pid")
|
||||
|
||||
@property
|
||||
def info(self) -> "ProcessInfo":
|
||||
return ProcessInfo(
|
||||
pid=self.pid,
|
||||
stdin=None,
|
||||
stdout=self.stdout_path,
|
||||
stderr=None,
|
||||
returncode=self.returncode,
|
||||
)
|
||||
|
||||
def _make_wdir(self):
|
||||
if self.wdir:
|
||||
makedirs(self.wdir, exist_ok=True)
|
||||
|
||||
def _dump(self):
|
||||
self._make_wdir()
|
||||
with open(self.info_path, "w", encoding="utf-8") as fobj:
|
||||
json.dump(self.info.asdict(), fobj)
|
||||
with open(self.pidfile_path, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(str(self.pid))
|
||||
|
||||
def _run(self):
|
||||
self._make_wdir()
|
||||
logger.debug(
|
||||
"Appending output to '%s'",
|
||||
self.stdout_path,
|
||||
)
|
||||
self._stdout = open(self.stdout_path, "ab")
|
||||
try:
|
||||
self._proc = subprocess.Popen(
|
||||
self.args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=self._stdout,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
shell=False,
|
||||
)
|
||||
self.pid: int = self._proc.pid
|
||||
self._dump()
|
||||
except Exception:
|
||||
if self._proc is not None:
|
||||
self._proc.kill()
|
||||
self._close_fds()
|
||||
raise
|
||||
|
||||
def wait(self, timeout: Optional[int] = None) -> Optional[int]:
|
||||
"""Block until a process started with `run` has completed.
|
||||
|
||||
Raises:
|
||||
TimeoutExpired if `timeout` was set and the process
|
||||
did not terminate after `timeout` seconds.
|
||||
"""
|
||||
if self.returncode is not None:
|
||||
return self.returncode
|
||||
try:
|
||||
self._proc.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise TimeoutExpired(exc.cmd, exc.timeout) from exc
|
||||
self.returncode = self._proc.returncode
|
||||
self._close_fds()
|
||||
self._dump()
|
||||
return self.returncode
|
||||
|
||||
@classmethod
|
||||
def spawn(cls, *args, **kwargs) -> Optional[int]:
|
||||
"""Spawn a ManagedProcess command in the background.
|
||||
|
||||
Returns: The spawned process PID.
|
||||
"""
|
||||
import multiprocessing as mp
|
||||
|
||||
proc = mp.Process(
|
||||
target=cls._spawn,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
daemon=True,
|
||||
)
|
||||
proc.start()
|
||||
# Do not terminate the child daemon when the main process exits
|
||||
# pylint: disable=protected-access
|
||||
mp.process._children.discard(proc) # type: ignore[attr-defined]
|
||||
return proc.pid
|
||||
|
||||
@classmethod
|
||||
def _spawn(cls, *args, **kwargs):
|
||||
with cls(*args, **kwargs):
|
||||
pass
|
||||
+35
-13
@@ -1,5 +1,4 @@
|
||||
"""Manages progress bars for DVC repo."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from threading import RLock
|
||||
@@ -137,8 +136,8 @@ class Tqdm(tqdm):
|
||||
|
||||
return wrapped
|
||||
|
||||
def as_callback(self, fs, path_info):
|
||||
return FsspecCallback(fs, path_info, self)
|
||||
def as_callback(self):
|
||||
return FsspecCallback(self)
|
||||
|
||||
def close(self):
|
||||
self.postfix["info"] = ""
|
||||
@@ -168,23 +167,46 @@ class Tqdm(tqdm):
|
||||
|
||||
|
||||
class FsspecCallback(fsspec.Callback):
|
||||
def __init__(self, fs, path_info, progress_bar):
|
||||
self.fs = fs
|
||||
self.path_info = path_info
|
||||
def __init__(self, progress_bar):
|
||||
self.progress_bar = progress_bar
|
||||
super().__init__()
|
||||
|
||||
def set_size(self, size):
|
||||
"""``set_size()`` is an API that might be called with ``None`` if the
|
||||
information is not already present on the caller. In that case, we'll
|
||||
retrieve the size from an ``info()`` call."""
|
||||
if size is None:
|
||||
size = self.fs.info(self.path_info)["size"]
|
||||
self.progress_bar.total = size
|
||||
self.progress_bar.refresh()
|
||||
if size is not None:
|
||||
self.progress_bar.total = size
|
||||
self.progress_bar.refresh()
|
||||
super().set_size(size)
|
||||
|
||||
def relative_update(self, inc=1):
|
||||
self.progress_bar.update(inc)
|
||||
super().relative_update(inc)
|
||||
|
||||
def absolute_update(self, value):
|
||||
self.progress_bar.update_to(value)
|
||||
super().absolute_update(value)
|
||||
|
||||
@staticmethod
|
||||
def wrap_fn(cb, fn):
|
||||
def wrapped(*args, **kwargs):
|
||||
res = fn(*args, **kwargs)
|
||||
cb.relative_update()
|
||||
return res
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def tdqm_or_callback_wrapped(
|
||||
fobj, method, total, callback=None, **pbar_kwargs
|
||||
):
|
||||
if callback:
|
||||
from funcy import nullcontext
|
||||
from tqdm.utils import CallbackIOWrapper
|
||||
|
||||
callback.set_size(total)
|
||||
wrapper = CallbackIOWrapper(callback.relative_update, fobj, method)
|
||||
return nullcontext(wrapper)
|
||||
|
||||
return Tqdm.wrapattr(fobj, method, total=total, bytes=True, **pbar_kwargs)
|
||||
|
||||
|
||||
DEFAULT_CALLBACK = fsspec.callbacks.NoOpCallback()
|
||||
|
||||
+3
-32
@@ -1,33 +1,4 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
from .image import ImageRenderer
|
||||
from .vega import VegaRenderer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Renderer:
|
||||
def __init__(self, data: Dict):
|
||||
self.data = data
|
||||
|
||||
from dvc.render.utils import get_files
|
||||
|
||||
files = get_files(self.data)
|
||||
|
||||
# we assume comparison of same file between revisions for now
|
||||
assert len(files) == 1
|
||||
self.filename = files.pop()
|
||||
|
||||
def _convert(self, path: "StrPath"):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def DIV(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def generate_html(self, path: "StrPath"):
|
||||
"""this method might edit content of path"""
|
||||
partial = self._convert(path)
|
||||
div_id = f"plot_{self.filename.replace('.', '_').replace('/', '_')}"
|
||||
return self.DIV.format(id=div_id, partial=partial)
|
||||
RENDERERS = [ImageRenderer, VegaRenderer]
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import abc
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
REVISION_FIELD = "rev"
|
||||
INDEX_FIELD = "step"
|
||||
|
||||
|
||||
class BadTemplateError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class Renderer(abc.ABC):
|
||||
def __init__(self, data: Dict, templates=None):
|
||||
self.data = data
|
||||
self.templates = templates
|
||||
|
||||
from dvc.render.utils import get_files
|
||||
|
||||
files = get_files(self.data)
|
||||
|
||||
# we assume comparison of same file between revisions for now
|
||||
assert len(files) == 1
|
||||
self.filename = files.pop()
|
||||
|
||||
def _convert(self, path: "StrPath"):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def DIV(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def SCRIPTS(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def as_json(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def _remove_special_chars(string: str):
|
||||
return string.translate(
|
||||
{ord(c): "_" for c in r"!@#$%^&*()[]{};,<>?\/:.|`~=_+"}
|
||||
)
|
||||
|
||||
def generate_html(self, path: "StrPath"):
|
||||
"""this method might edit content of path"""
|
||||
partial = self._convert(path)
|
||||
|
||||
div_id = self._remove_special_chars(self.filename)
|
||||
div_id = f"plot_{div_id}"
|
||||
|
||||
return self.DIV.format(id=div_id, partial=partial)
|
||||
+18
-11
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.render import Renderer
|
||||
from dvc.render.base import Renderer
|
||||
from dvc.types import StrPath
|
||||
|
||||
PAGE_HTML = """<!DOCTYPE html>
|
||||
@@ -13,9 +13,7 @@ PAGE_HTML = """<!DOCTYPE html>
|
||||
<head>
|
||||
{refresh_tag}
|
||||
<title>DVC Plot</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
{scripts}
|
||||
</head>
|
||||
<body>
|
||||
{plot_divs}
|
||||
@@ -29,8 +27,9 @@ class MissingPlaceholderError(DvcException):
|
||||
|
||||
|
||||
class HTML:
|
||||
PLACEHOLDER = "plot_divs"
|
||||
PLACEHOLDER_FORMAT_STR = f"{{{PLACEHOLDER}}}"
|
||||
SCRIPTS_PLACEHOLDER = "scripts"
|
||||
PLOTS_PLACEHOLDER = "plot_divs"
|
||||
PLOTS_PLACEHOLDER_FORMAT_STR = f"{{{PLOTS_PLACEHOLDER}}}"
|
||||
REFRESH_PLACEHOLDER = "refresh_tag"
|
||||
REFRESH_TAG = '<meta http-equiv="refresh" content="{}">'
|
||||
|
||||
@@ -40,11 +39,12 @@ class HTML:
|
||||
refresh_seconds: Optional[int] = None,
|
||||
):
|
||||
template = template or PAGE_HTML
|
||||
if self.PLACEHOLDER_FORMAT_STR not in template:
|
||||
raise MissingPlaceholderError(self.PLACEHOLDER_FORMAT_STR)
|
||||
if self.PLOTS_PLACEHOLDER_FORMAT_STR not in template:
|
||||
raise MissingPlaceholderError(self.PLOTS_PLACEHOLDER_FORMAT_STR)
|
||||
|
||||
self.template = template
|
||||
self.elements: List[str] = []
|
||||
self.scripts: str = ""
|
||||
self.refresh_tag = ""
|
||||
if refresh_seconds is not None:
|
||||
self.refresh_tag = self.REFRESH_TAG.format(refresh_seconds)
|
||||
@@ -65,13 +65,19 @@ class HTML:
|
||||
self.elements.append(tabulate.tabulate(rows, header, tablefmt="html"))
|
||||
return self
|
||||
|
||||
def with_scripts(self, scripts: str) -> "HTML":
|
||||
if scripts not in self.scripts:
|
||||
self.scripts += f"\n{scripts}"
|
||||
return self
|
||||
|
||||
def with_element(self, html: str) -> "HTML":
|
||||
self.elements.append(html)
|
||||
return self
|
||||
|
||||
def embed(self) -> str:
|
||||
kwargs = {
|
||||
self.PLACEHOLDER: "\n".join(self.elements),
|
||||
self.SCRIPTS_PLACEHOLDER: self.scripts,
|
||||
self.PLOTS_PLACEHOLDER: "\n".join(self.elements),
|
||||
self.REFRESH_PLACEHOLDER: self.refresh_tag,
|
||||
}
|
||||
return self.template.format(**kwargs)
|
||||
@@ -89,7 +95,7 @@ def write(
|
||||
|
||||
page_html = None
|
||||
if template_path:
|
||||
with open(template_path) as fobj:
|
||||
with open(template_path, encoding="utf-8") as fobj:
|
||||
page_html = fobj.read()
|
||||
|
||||
document = HTML(page_html, refresh_seconds=refresh_seconds)
|
||||
@@ -98,10 +104,11 @@ def write(
|
||||
document.with_element("<br>")
|
||||
|
||||
for renderer in renderers:
|
||||
document.with_scripts(renderer.SCRIPTS)
|
||||
document.with_element(renderer.generate_html(path))
|
||||
|
||||
index = Path(os.path.join(path, "index.html"))
|
||||
|
||||
with open(index, "w") as fd:
|
||||
with open(index, "w", encoding="utf-8") as fd:
|
||||
fd.write(document.embed())
|
||||
return index
|
||||
|
||||
+12
-5
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.render import Renderer
|
||||
from dvc.render.base import Renderer
|
||||
from dvc.render.utils import get_files
|
||||
from dvc.utils import relpath
|
||||
|
||||
@@ -13,10 +13,13 @@ class ImageRenderer(Renderer):
|
||||
DIV = """
|
||||
<div
|
||||
id="{id}"
|
||||
style="border: 1px solid;">
|
||||
style="border:1px solid black;text-align:center;
|
||||
white-space: nowrap;overflow-y:hidden;">
|
||||
{partial}
|
||||
</div>"""
|
||||
|
||||
SCRIPTS = ""
|
||||
|
||||
def _write_image(
|
||||
self,
|
||||
page_dir_path: "StrPath",
|
||||
@@ -30,15 +33,16 @@ class ImageRenderer(Renderer):
|
||||
img_path = os.path.join(
|
||||
static, f"{revision}_{filename.replace(os.sep, '_')}"
|
||||
)
|
||||
rel_img_path = relpath(img_path, page_dir_path)
|
||||
with open(img_path, "wb") as fd:
|
||||
fd.write(image_data)
|
||||
return """
|
||||
<div>
|
||||
<div
|
||||
style="border:1px dotted black;margin:2px;display: inline-block;
|
||||
overflow:hidden;margin-left:8px;">
|
||||
<p>{title}</p>
|
||||
<img src="{src}">
|
||||
</div>""".format(
|
||||
title=revision, src=rel_img_path
|
||||
title=revision, src=(relpath(img_path, page_dir_path))
|
||||
)
|
||||
|
||||
def _convert(self, path: "StrPath"):
|
||||
@@ -57,6 +61,9 @@ class ImageRenderer(Renderer):
|
||||
return "\n".join(div_content)
|
||||
return ""
|
||||
|
||||
def as_json(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def matches(data):
|
||||
files = get_files(data)
|
||||
|
||||
+13
-35
@@ -1,65 +1,43 @@
|
||||
import os.path
|
||||
from typing import Dict, List, Set
|
||||
from typing import Dict, List
|
||||
|
||||
import dpath.util
|
||||
|
||||
|
||||
def get_files(data: Dict) -> Set:
|
||||
def get_files(data: Dict) -> List:
|
||||
files = set()
|
||||
for rev in data.keys():
|
||||
for file in data[rev].get("data", {}).keys():
|
||||
files.add(file)
|
||||
return files
|
||||
sorted_files = sorted(files)
|
||||
return sorted_files
|
||||
|
||||
|
||||
def group_by_filename(plots_data: Dict) -> List[Dict]:
|
||||
# TODO use dpath.util.search once
|
||||
# https://github.com/dpath-maintainers/dpath-python/issues/147 is released
|
||||
# now cannot search when errors are present in data
|
||||
files = get_files(plots_data)
|
||||
grouped = []
|
||||
|
||||
for file in files:
|
||||
tmp: Dict = {}
|
||||
for revision, revision_data in plots_data.items():
|
||||
if file in revision_data.get("data", {}):
|
||||
if "data" not in tmp:
|
||||
tmp[revision] = {"data": {}}
|
||||
tmp[revision]["data"].update(
|
||||
{file: revision_data["data"][file]}
|
||||
)
|
||||
grouped.append(tmp)
|
||||
|
||||
grouped.append(dpath.util.search(plots_data, ["*", "*", file]))
|
||||
return grouped
|
||||
|
||||
|
||||
def find_vega(repo, plots_data, target):
|
||||
# TODO same as group_by_filename
|
||||
grouped = group_by_filename(plots_data)
|
||||
found = None
|
||||
for plot_group in grouped:
|
||||
files = get_files(plot_group)
|
||||
assert len(files) == 1
|
||||
file = files.pop()
|
||||
if file == target:
|
||||
found = plot_group
|
||||
break
|
||||
|
||||
found = dpath.util.search(plots_data, ["*", "*", target])
|
||||
from dvc.render.vega import VegaRenderer
|
||||
|
||||
if found and VegaRenderer.matches(found):
|
||||
return VegaRenderer(found, repo.plots.templates).get_vega()
|
||||
return VegaRenderer(found, repo.plots.templates).as_json()
|
||||
return ""
|
||||
|
||||
|
||||
def match_renderers(plots_data, templates):
|
||||
from dvc.render.image import ImageRenderer
|
||||
from dvc.render.vega import VegaRenderer
|
||||
from dvc.render import RENDERERS
|
||||
|
||||
renderers = []
|
||||
for g in group_by_filename(plots_data):
|
||||
if VegaRenderer.matches(g):
|
||||
renderers.append(VegaRenderer(g, templates))
|
||||
if ImageRenderer.matches(g):
|
||||
renderers.append(ImageRenderer(g))
|
||||
for renderer_class in RENDERERS:
|
||||
if renderer_class.matches(g):
|
||||
renderers.append(renderer_class(g, templates))
|
||||
return renderers
|
||||
|
||||
|
||||
|
||||
+48
-21
@@ -5,30 +5,23 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Union
|
||||
from funcy import first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.render import Renderer
|
||||
from dvc.render.base import (
|
||||
INDEX_FIELD,
|
||||
REVISION_FIELD,
|
||||
BadTemplateError,
|
||||
Renderer,
|
||||
)
|
||||
from dvc.render.utils import get_files
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo.plots.template import PlotTemplates
|
||||
from dvc.types import StrPath
|
||||
|
||||
REVISION_FIELD = "rev"
|
||||
INDEX_FIELD = "step"
|
||||
|
||||
|
||||
class PlotMetricTypeError(DvcException):
|
||||
def __init__(self, file):
|
||||
super().__init__(
|
||||
"'{}' - file type error\n"
|
||||
"Only JSON, YAML, CSV and TSV formats are supported.".format(file)
|
||||
)
|
||||
|
||||
|
||||
class PlotDataStructureError(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"Plot data extraction failed. Please see "
|
||||
"https://man.dvc.org/plot for supported data formats."
|
||||
"https://man.dvc.org/plots for supported data formats."
|
||||
)
|
||||
|
||||
|
||||
@@ -108,9 +101,11 @@ class VegaRenderer(Renderer):
|
||||
</div>
|
||||
"""
|
||||
|
||||
def __init__(self, data: Dict, templates: "PlotTemplates"):
|
||||
super().__init__(data)
|
||||
self.templates = templates
|
||||
SCRIPTS = """
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
"""
|
||||
|
||||
def _squash_props(self) -> Dict:
|
||||
resolved: Dict[str, str] = {}
|
||||
@@ -144,10 +139,39 @@ class VegaRenderer(Renderer):
|
||||
datapoints.extend(tmp)
|
||||
return datapoints
|
||||
|
||||
def get_vega(self) -> Optional[str]:
|
||||
def fill_template(self, template, datapoints, props=None):
|
||||
props = props or {}
|
||||
|
||||
content = deepcopy(template.content)
|
||||
if template.anchor_str("data") not in template.content:
|
||||
anchor = template.anchor("data")
|
||||
raise BadTemplateError(
|
||||
f"Template '{template.name}' is not using '{anchor}' anchor"
|
||||
)
|
||||
|
||||
if props.get("x"):
|
||||
template.check_field_exists(datapoints, props.get("x"))
|
||||
if props.get("y"):
|
||||
template.check_field_exists(datapoints, props.get("y"))
|
||||
|
||||
content = template.fill_anchor(content, "data", datapoints)
|
||||
|
||||
props.setdefault("title", "")
|
||||
props.setdefault("x_label", props.get("x"))
|
||||
props.setdefault("y_label", props.get("y"))
|
||||
|
||||
names = ["title", "x", "y", "x_label", "y_label"]
|
||||
for name in names:
|
||||
value = props.get(name)
|
||||
if value is not None:
|
||||
content = template.fill_anchor(content, name, value)
|
||||
|
||||
return content
|
||||
|
||||
def as_json(self) -> Optional[str]:
|
||||
props = self._squash_props()
|
||||
|
||||
template = self.templates.load(props.get("template") or "default")
|
||||
template = self.templates.load(props.get("template", None))
|
||||
|
||||
if not props.get("x") and template.has_anchor("x"):
|
||||
props["append_index"] = True
|
||||
@@ -162,11 +186,14 @@ class VegaRenderer(Renderer):
|
||||
props["y"] = first(
|
||||
f for f in reversed(fields) if f not in skip
|
||||
)
|
||||
return template.render(datapoints, props=props)
|
||||
filled_template = self.fill_template(template, datapoints, props)
|
||||
|
||||
return filled_template
|
||||
|
||||
return None
|
||||
|
||||
def _convert(self, path: "StrPath"):
|
||||
return self.get_vega()
|
||||
return self.as_json()
|
||||
|
||||
@staticmethod
|
||||
def matches(data):
|
||||
|
||||
+83
-36
@@ -11,16 +11,15 @@ from dvc.exceptions import FileMissingError
|
||||
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
|
||||
from dvc.exceptions import NotDvcRepoError, OutputNotFoundError
|
||||
from dvc.ignore import DvcIgnoreFilter
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.utils import env2bool
|
||||
from dvc.utils.fs import path_isin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.fs.base import FileSystem
|
||||
from dvc.objects.file import HashFile
|
||||
from dvc.repo.scm_context import SCMContext
|
||||
from dvc.scm import Base
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -91,15 +90,18 @@ class Repo:
|
||||
):
|
||||
assert bool(scm) == bool(rev)
|
||||
|
||||
from dvc.scm import SCM
|
||||
from dvc.scm.base import SCMError
|
||||
from dvc.scm.git import Git
|
||||
from dvc.fs.git import GitFileSystem
|
||||
from dvc.scm import SCM, Base, Git, SCMError
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
dvc_dir = None
|
||||
tmp_dir = None
|
||||
try:
|
||||
fs = scm.get_fs(rev) if isinstance(scm, Git) and rev else None
|
||||
fs = (
|
||||
GitFileSystem(scm=scm, rev=rev)
|
||||
if isinstance(scm, Git) and rev
|
||||
else None
|
||||
)
|
||||
root_dir = self.find_root(root_dir, fs)
|
||||
dvc_dir = os.path.join(root_dir, self.DVC_DIR)
|
||||
tmp_dir = os.path.join(dvc_dir, "tmp")
|
||||
@@ -113,13 +115,39 @@ class Repo:
|
||||
except SCMError:
|
||||
scm = SCM(os.curdir, no_scm=True)
|
||||
|
||||
from dvc.scm import Base
|
||||
|
||||
assert isinstance(scm, Base)
|
||||
root_dir = scm.root_dir
|
||||
|
||||
return root_dir, dvc_dir, tmp_dir
|
||||
|
||||
def _get_database_dir(self, db_name):
|
||||
# NOTE: by default, store SQLite-based remote indexes and state's
|
||||
# `links` and `md5s` caches in the repository itself to avoid any
|
||||
# possible state corruption in 'shared cache dir' scenario, but allow
|
||||
# user to override this through config when, say, the repository is
|
||||
# located on a mounted volume — see
|
||||
# https://github.com/iterative/dvc/issues/4420
|
||||
base_db_dir = self.config.get(db_name, {}).get("dir", None)
|
||||
if not base_db_dir:
|
||||
return self.tmp_dir
|
||||
|
||||
import hashlib
|
||||
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
root_dir_hash = hashlib.sha224(
|
||||
self.root_dir.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
db_dir = os.path.join(
|
||||
base_db_dir,
|
||||
self.DVC_DIR,
|
||||
f"{os.path.basename(self.root_dir)}-{root_dir_hash[0:7]}",
|
||||
)
|
||||
|
||||
makedirs(db_dir, exist_ok=True)
|
||||
return db_dir
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_dir=None,
|
||||
@@ -133,16 +161,16 @@ class Repo:
|
||||
):
|
||||
from dvc.config import Config
|
||||
from dvc.data_cloud import DataCloud
|
||||
from dvc.fs.git import GitFileSystem
|
||||
from dvc.fs.local import LocalFileSystem
|
||||
from dvc.lock import LockNoop, make_lock
|
||||
from dvc.machine import MachineManager
|
||||
from dvc.objects.db import ODBManager
|
||||
from dvc.repo.live import Live
|
||||
from dvc.repo.metrics import Metrics
|
||||
from dvc.repo.params import Params
|
||||
from dvc.repo.plots import Plots
|
||||
from dvc.repo.stage import StageLoad
|
||||
from dvc.scm import SCM
|
||||
from dvc.scm import SCM, Git
|
||||
from dvc.stage.cache import StageCache
|
||||
from dvc.state import State, StateNoop
|
||||
|
||||
@@ -157,7 +185,8 @@ class Repo:
|
||||
)
|
||||
|
||||
if scm:
|
||||
self._fs = scm.get_fs(rev)
|
||||
assert isinstance(scm, Git)
|
||||
self._fs = GitFileSystem(scm=scm, rev=rev)
|
||||
else:
|
||||
self._fs = LocalFileSystem(url=self.root_dir)
|
||||
|
||||
@@ -183,10 +212,8 @@ class Repo:
|
||||
friendly=True,
|
||||
)
|
||||
|
||||
# NOTE: storing state and link_state in the repository itself to
|
||||
# avoid any possible state corruption in 'shared cache dir'
|
||||
# scenario.
|
||||
self.state = State(self.root_dir, self.tmp_dir, self.dvcignore)
|
||||
state_db_dir = self._get_database_dir("state")
|
||||
self.state = State(self.root_dir, state_db_dir, self.dvcignore)
|
||||
self.odb = ODBManager(self)
|
||||
|
||||
self.stage_cache = StageCache(self)
|
||||
@@ -198,14 +225,6 @@ class Repo:
|
||||
self.params = Params(self)
|
||||
self.live = Live(self)
|
||||
|
||||
if self.tmp_dir and (
|
||||
self.config["feature"].get("machine", False)
|
||||
or env2bool("DVC_TEST")
|
||||
):
|
||||
self.machine = MachineManager(self)
|
||||
else:
|
||||
self.machine = None
|
||||
|
||||
self.stage_collection_error_handler: Optional[
|
||||
Callable[[str, Exception], None]
|
||||
] = None
|
||||
@@ -237,8 +256,7 @@ class Repo:
|
||||
|
||||
@cached_property
|
||||
def scm(self):
|
||||
from dvc.scm import SCM
|
||||
from dvc.scm.base import SCMError
|
||||
from dvc.scm import SCM, SCMError
|
||||
|
||||
if self._scm:
|
||||
return self._scm
|
||||
@@ -253,6 +271,12 @@ class Repo:
|
||||
return SCM(self.root_dir, no_scm=True)
|
||||
raise
|
||||
|
||||
@cached_property
|
||||
def scm_context(self) -> "SCMContext":
|
||||
from dvc.repo.scm_context import SCMContext
|
||||
|
||||
return SCMContext(self.scm, self.config)
|
||||
|
||||
@cached_property
|
||||
def dvcignore(self) -> DvcIgnoreFilter:
|
||||
|
||||
@@ -263,7 +287,10 @@ class Repo:
|
||||
|
||||
assert self.scm
|
||||
if isinstance(self.fs, LocalFileSystem):
|
||||
return self.scm.get_rev()
|
||||
from dvc.scm import map_scm_exception
|
||||
|
||||
with map_scm_exception():
|
||||
return self.scm.get_rev()
|
||||
return self.fs.rev
|
||||
|
||||
@cached_property
|
||||
@@ -272,12 +299,23 @@ class Repo:
|
||||
|
||||
return Experiments(self)
|
||||
|
||||
@cached_property
|
||||
def machine(self):
|
||||
from dvc.machine import MachineManager
|
||||
|
||||
if self.tmp_dir and (
|
||||
self.config["feature"].get("machine", False)
|
||||
or env2bool("DVC_TEST")
|
||||
):
|
||||
return MachineManager(self)
|
||||
return None
|
||||
|
||||
@property
|
||||
def fs(self) -> "BaseFileSystem":
|
||||
def fs(self) -> "FileSystem":
|
||||
return self._fs
|
||||
|
||||
@fs.setter
|
||||
def fs(self, fs: "BaseFileSystem"):
|
||||
def fs(self, fs: "FileSystem"):
|
||||
self._fs = fs
|
||||
# Our graph cache is no longer valid, as it was based on the previous
|
||||
# fs.
|
||||
@@ -326,7 +364,7 @@ class Repo:
|
||||
)
|
||||
|
||||
def unprotect(self, target):
|
||||
return self.odb.local.unprotect(PathInfo(target))
|
||||
return self.odb.local.unprotect(target)
|
||||
|
||||
def _ignore(self):
|
||||
flist = [self.config.files["local"], self.tmp_dir]
|
||||
@@ -334,7 +372,8 @@ class Repo:
|
||||
if path_isin(self.odb.local.cache_dir, self.root_dir):
|
||||
flist += [self.odb.local.cache_dir]
|
||||
|
||||
self.scm.ignore_list(flist)
|
||||
for file in flist:
|
||||
self.scm_context.ignore(file)
|
||||
|
||||
def brancher(self, *args, **kwargs):
|
||||
from dvc.repo.brancher import brancher
|
||||
@@ -418,14 +457,18 @@ class Repo:
|
||||
outs = outs or self.index.outs_graph
|
||||
|
||||
abs_path = os.path.abspath(path)
|
||||
path_info = PathInfo(abs_path)
|
||||
match = path_info.__eq__ if strict else path_info.isin_or_eq
|
||||
fs_path = abs_path
|
||||
|
||||
def func(out):
|
||||
if out.scheme == "local" and match(out.path_info):
|
||||
def eq(one, two):
|
||||
return one == two
|
||||
|
||||
match = eq if strict else out.fs.path.isin_or_eq
|
||||
|
||||
if out.scheme == "local" and match(fs_path, out.fs_path):
|
||||
return True
|
||||
|
||||
if recursive and out.path_info.isin(path_info):
|
||||
if recursive and out.fs.path.isin(out.fs_path, fs_path):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -452,13 +495,17 @@ class Repo:
|
||||
|
||||
return RepoFileSystem(self, subrepos=self.subrepos, **self._fs_conf)
|
||||
|
||||
@cached_property
|
||||
def index_db_dir(self):
|
||||
return self._get_database_dir("index")
|
||||
|
||||
@contextmanager
|
||||
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
|
||||
"""Opens a specified resource as a file descriptor"""
|
||||
from dvc.fs.repo import RepoFileSystem
|
||||
|
||||
fs = RepoFileSystem(self, subrepos=True)
|
||||
path = PathInfo(self.root_dir) / path
|
||||
path = self.fs.path.join(self.root_dir, path)
|
||||
try:
|
||||
with fs.open(
|
||||
path, mode=mode, encoding=encoding, remote=remote
|
||||
|
||||
+3
-3
@@ -87,8 +87,8 @@ def translate_graph_error(stages: Stages) -> Iterator[None]:
|
||||
yield
|
||||
except OverlappingOutputPathsError as exc:
|
||||
msg = OVERLAPPING_OUTPUT_FMT.format(
|
||||
out=exc.overlapping_out.path_info,
|
||||
parent=exc.parent.path_info,
|
||||
out=exc.overlapping_out.fs_path,
|
||||
parent=exc.parent.fs_path,
|
||||
parent_stage=exc.parent.stage.addressing,
|
||||
)
|
||||
raise OverlappingOutputPathsError(exc.parent, exc.overlapping_out, msg)
|
||||
@@ -228,7 +228,7 @@ def _find_all_targets(
|
||||
from dvc.dvcfile import is_dvc_file
|
||||
|
||||
if os.path.isdir(target) and recursive:
|
||||
files = map(os.fspath, repo.dvcignore.walk_files(repo.fs, target))
|
||||
files = repo.dvcignore.find(repo.fs, target)
|
||||
yield from (
|
||||
path
|
||||
for path in files
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from functools import partial
|
||||
|
||||
from funcy import group_by
|
||||
|
||||
|
||||
@@ -58,8 +60,12 @@ def brancher( # noqa: E302
|
||||
|
||||
try:
|
||||
if revs:
|
||||
for sha, names in group_by(scm.resolve_rev, revs).items():
|
||||
self.fs = scm.get_fs(sha)
|
||||
from dvc.fs.git import GitFileSystem
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
rev_resolver = partial(resolve_rev, scm)
|
||||
for sha, names in group_by(rev_resolver, revs).items():
|
||||
self.fs = GitFileSystem(scm=scm, rev=sha)
|
||||
# ignore revs that don't contain repo root
|
||||
# (i.e. revs from before a subdir=True repo was init'ed)
|
||||
if self.fs.exists(self.root_dir):
|
||||
|
||||
+27
-26
@@ -2,8 +2,7 @@ import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, List, Tuple
|
||||
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.types import DvcPath
|
||||
from dvc.types import AnyPath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.output import Output
|
||||
@@ -14,7 +13,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
FilterFn = Callable[["Output"], bool]
|
||||
Outputs = List["Output"]
|
||||
DvcPaths = List[DvcPath]
|
||||
AnyPaths = List[AnyPath]
|
||||
StrPaths = List[str]
|
||||
|
||||
|
||||
def _collect_outs(
|
||||
@@ -32,39 +32,40 @@ def _collect_paths(
|
||||
rev: str = None,
|
||||
):
|
||||
from dvc.fs.repo import RepoFileSystem
|
||||
from dvc.utils import relpath
|
||||
|
||||
path_infos = [PathInfo(os.path.abspath(target)) for target in targets]
|
||||
fs_paths = [os.path.abspath(target) for target in targets]
|
||||
fs = RepoFileSystem(repo)
|
||||
|
||||
target_infos = []
|
||||
for path_info in path_infos:
|
||||
target_paths = []
|
||||
for fs_path in fs_paths:
|
||||
|
||||
if recursive and fs.isdir(path_info):
|
||||
target_infos.extend(repo.dvcignore.walk_files(fs, path_info))
|
||||
if recursive and fs.isdir(fs_path):
|
||||
target_paths.extend(repo.dvcignore.find(fs, fs_path))
|
||||
|
||||
if not fs.exists(path_info):
|
||||
if not fs.exists(fs_path):
|
||||
rel = relpath(fs_path)
|
||||
if rev == "workspace" or rev == "":
|
||||
logger.warning(
|
||||
"'%s' was not found in current workspace.", path_info
|
||||
)
|
||||
logger.warning("'%s' was not found in current workspace.", rel)
|
||||
else:
|
||||
logger.warning("'%s' was not found at: '%s'.", path_info, rev)
|
||||
target_infos.append(path_info)
|
||||
return target_infos
|
||||
logger.warning("'%s' was not found at: '%s'.", rel, rev)
|
||||
target_paths.append(fs_path)
|
||||
return target_paths
|
||||
|
||||
|
||||
def _filter_duplicates(
|
||||
outs: Outputs, path_infos: DvcPaths
|
||||
) -> Tuple[Outputs, DvcPaths]:
|
||||
outs: Outputs, fs_paths: StrPaths
|
||||
) -> Tuple[Outputs, StrPaths]:
|
||||
res_outs: Outputs = []
|
||||
res_infos = path_infos
|
||||
fs_res_paths = fs_paths
|
||||
|
||||
for out in outs:
|
||||
if out.path_info in path_infos:
|
||||
if out.fs_path in fs_paths:
|
||||
res_outs.append(out)
|
||||
res_infos.remove(out.path_info)
|
||||
# MUTATING THE SAME LIST!!
|
||||
fs_res_paths.remove(out.fs_path)
|
||||
|
||||
return res_outs, res_infos
|
||||
return res_outs, fs_res_paths
|
||||
|
||||
|
||||
def collect(
|
||||
@@ -74,15 +75,15 @@ def collect(
|
||||
output_filter: FilterFn = None,
|
||||
rev: str = None,
|
||||
recursive: bool = False,
|
||||
) -> Tuple[Outputs, DvcPaths]:
|
||||
) -> Tuple[Outputs, StrPaths]:
|
||||
assert targets or output_filter
|
||||
|
||||
outs: Outputs = _collect_outs(repo, output_filter=output_filter, deps=deps)
|
||||
|
||||
if not targets:
|
||||
path_infos: DvcPaths = []
|
||||
return outs, path_infos
|
||||
fs_paths: StrPaths = []
|
||||
return outs, fs_paths
|
||||
|
||||
target_infos = _collect_paths(repo, targets, recursive=recursive, rev=rev)
|
||||
target_paths = _collect_paths(repo, targets, recursive=recursive, rev=rev)
|
||||
|
||||
return _filter_duplicates(outs, target_infos)
|
||||
return _filter_duplicates(outs, target_paths)
|
||||
|
||||
+24
-18
@@ -37,14 +37,14 @@ def diff(self, a_rev="HEAD", b_rev=None, targets=None):
|
||||
# workspace paths/checksums if b_rev was None
|
||||
continue
|
||||
|
||||
targets_path_infos = None
|
||||
targets_paths = None
|
||||
if targets is not None:
|
||||
# convert targets to path_infos, and capture any missing targets
|
||||
targets_path_infos, missing_targets[rev] = _targets_to_path_infos(
|
||||
# convert targets to paths, and capture any missing targets
|
||||
targets_paths, missing_targets[rev] = _targets_to_paths(
|
||||
repo_fs, targets
|
||||
)
|
||||
|
||||
results[rev] = _paths_checksums(self, targets_path_infos)
|
||||
results[rev] = _paths_checksums(self, targets_paths)
|
||||
|
||||
if targets is not None:
|
||||
# check for overlapping missing targets between a_rev and b_rev
|
||||
@@ -133,13 +133,14 @@ def _output_paths(repo, targets):
|
||||
for output in repo.index.outs:
|
||||
if _exists(output):
|
||||
yield_output = targets is None or any(
|
||||
output.path_info.isin_or_eq(target) for target in targets
|
||||
output.fs.path.isin_or_eq(output.fs_path, target)
|
||||
for target in targets
|
||||
)
|
||||
|
||||
if on_working_fs:
|
||||
_, obj = ostage(
|
||||
_, _, obj = ostage(
|
||||
repo.odb.local,
|
||||
output.path_info,
|
||||
output.fs_path,
|
||||
repo.odb.local.fs,
|
||||
"md5",
|
||||
dry_run=True,
|
||||
@@ -158,19 +159,24 @@ def _output_paths(repo, targets):
|
||||
|
||||
if output.is_dir_checksum and (
|
||||
yield_output
|
||||
or any(target.isin(output.path_info) for target in targets)
|
||||
or any(
|
||||
output.fs.path.isin(target, output.fs_path)
|
||||
for target in targets
|
||||
)
|
||||
):
|
||||
yield from _dir_output_paths(output.path_info, obj, targets)
|
||||
yield from _dir_output_paths(
|
||||
output.fs, output.fs_path, obj, targets
|
||||
)
|
||||
|
||||
|
||||
def _dir_output_paths(path_info, obj, targets=None):
|
||||
for key, entry_obj in obj:
|
||||
fname = path_info.joinpath(*key)
|
||||
def _dir_output_paths(fs, fs_path, obj, targets=None):
|
||||
for key, _, oid in obj:
|
||||
fname = fs.path.join(fs_path, *key)
|
||||
if targets is None or any(
|
||||
fname.isin_or_eq(target) for target in targets
|
||||
fs.path.isin_or_eq(fname, target) for target in targets
|
||||
):
|
||||
# pylint: disable=no-member
|
||||
yield str(fname), entry_obj.hash_info.value
|
||||
yield fs.path.join(fs.path.name(fs_path), *key), oid.value
|
||||
|
||||
|
||||
def _filter_missing(repo_fs, paths):
|
||||
@@ -185,17 +191,17 @@ def _filter_missing(repo_fs, paths):
|
||||
pass
|
||||
|
||||
|
||||
def _targets_to_path_infos(repo_fs, targets):
|
||||
path_infos = []
|
||||
def _targets_to_paths(repo_fs, targets):
|
||||
paths = []
|
||||
missing = []
|
||||
|
||||
for target in targets:
|
||||
if repo_fs.exists(target):
|
||||
path_infos.append(repo_fs.metadata(target).path_info)
|
||||
paths.append(repo_fs.metadata(target).fs_path)
|
||||
else:
|
||||
missing.append(target)
|
||||
|
||||
return path_infos, missing
|
||||
return paths, missing
|
||||
|
||||
|
||||
def _calculate_renamed(new, old, added, deleted):
|
||||
|
||||
@@ -10,10 +10,9 @@ from typing import Dict, Iterable, Mapping, Optional
|
||||
|
||||
from funcy import cached_property, first
|
||||
|
||||
from dvc.dependency.param import MissingParamsError
|
||||
from dvc.env import DVCLIVE_RESUME
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
from dvc.utils import relpath
|
||||
|
||||
from .base import (
|
||||
@@ -42,7 +41,9 @@ def scm_locked(f):
|
||||
# different sequences of git operations at once
|
||||
@wraps(f)
|
||||
def wrapper(exp, *args, **kwargs):
|
||||
with exp.scm_lock:
|
||||
from dvc.scm import map_scm_exception
|
||||
|
||||
with map_scm_exception(), exp.scm_lock:
|
||||
return f(exp, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -89,7 +90,7 @@ class Experiments:
|
||||
|
||||
def __init__(self, repo):
|
||||
from dvc.lock import make_lock
|
||||
from dvc.scm.base import NoSCMError
|
||||
from dvc.scm import NoSCMError
|
||||
|
||||
if repo.config["core"].get("no_scm", False):
|
||||
raise NoSCMError
|
||||
@@ -98,6 +99,7 @@ class Experiments:
|
||||
self.scm_lock = make_lock(
|
||||
os.path.join(self.repo.tmp_dir, "exp_scm_lock"),
|
||||
tmp_dir=self.repo.tmp_dir,
|
||||
hardlink_lock=repo.config["core"].get("hardlink_lock", False),
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -116,7 +118,7 @@ class Experiments:
|
||||
|
||||
@cached_property
|
||||
def stash(self):
|
||||
from dvc.scm.git import Stash
|
||||
from scmrepo.git import Stash
|
||||
|
||||
return Stash(self.scm, EXPS_STASH)
|
||||
|
||||
@@ -162,7 +164,7 @@ class Experiments:
|
||||
the human-readable name in the experiment branch ref. Has no
|
||||
effect of branch is specified.
|
||||
"""
|
||||
with self.scm.detach_head() as orig_head:
|
||||
with self.scm.detach_head(client="dvc") as orig_head:
|
||||
stash_head = orig_head
|
||||
if baseline_rev is None:
|
||||
baseline_rev = orig_head
|
||||
@@ -350,19 +352,32 @@ class Experiments:
|
||||
)
|
||||
self.scm.add(self.args_file)
|
||||
|
||||
def _format_new_params_msg(self, new_params, config_path):
|
||||
"""Format an error message for when new parameters are identified"""
|
||||
new_param_count = len(new_params)
|
||||
pluralise = "s are" if new_param_count > 1 else " is"
|
||||
param_list = ", ".join(new_params)
|
||||
return (
|
||||
f"{new_param_count} parameter{pluralise} missing "
|
||||
f"from '{config_path}': {param_list}"
|
||||
)
|
||||
|
||||
def _update_params(self, params: dict):
|
||||
"""Update experiment params files with the specified values."""
|
||||
from dvc.utils.collections import merge_params
|
||||
from dvc.utils.collections import NewParamsFound, merge_params
|
||||
from dvc.utils.serialize import MODIFIERS
|
||||
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
|
||||
for params_fname in params:
|
||||
path = PathInfo(params_fname)
|
||||
suffix = path.suffix.lower()
|
||||
for path in params:
|
||||
suffix = self.repo.fs.path.suffix(path).lower()
|
||||
modify_data = MODIFIERS[suffix]
|
||||
with modify_data(path, fs=self.repo.fs) as data:
|
||||
merge_params(data, params[params_fname])
|
||||
try:
|
||||
merge_params(data, params[path], allow_new=False)
|
||||
except NewParamsFound as e:
|
||||
msg = self._format_new_params_msg(e.new_params, path)
|
||||
raise MissingParamsError(msg)
|
||||
|
||||
# Force params file changes to be staged in git
|
||||
# Otherwise in certain situations the changes to params file may be
|
||||
@@ -395,7 +410,9 @@ class Experiments:
|
||||
self.scm.reset()
|
||||
|
||||
if checkpoint_resume:
|
||||
resume_rev = self.scm.resolve_rev(checkpoint_resume)
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
resume_rev = resolve_rev(self.scm, checkpoint_resume)
|
||||
try:
|
||||
self.check_baseline(resume_rev)
|
||||
checkpoint_resume = resume_rev
|
||||
@@ -458,6 +475,17 @@ class Experiments:
|
||||
"\tdvc exp branch <exp> <branch>\n"
|
||||
)
|
||||
|
||||
def _validate_new_ref(self, exp_ref: ExpRefInfo):
|
||||
from .utils import check_ref_format
|
||||
|
||||
if not exp_ref.name:
|
||||
return
|
||||
|
||||
check_ref_format(self.scm, exp_ref)
|
||||
|
||||
if self.scm.get_ref(str(exp_ref)):
|
||||
raise ExperimentExistsError(exp_ref.name)
|
||||
|
||||
@scm_locked
|
||||
def new(self, *args, checkpoint_resume: Optional[str] = None, **kwargs):
|
||||
"""Create a new experiment.
|
||||
@@ -470,6 +498,16 @@ class Experiments:
|
||||
*args, resume_rev=checkpoint_resume, **kwargs
|
||||
)
|
||||
|
||||
name = kwargs.get("name", None)
|
||||
baseline_sha = kwargs.get("baseline_rev") or self.repo.scm.get_rev()
|
||||
exp_ref = ExpRefInfo(baseline_sha=baseline_sha, name=name)
|
||||
|
||||
try:
|
||||
self._validate_new_ref(exp_ref)
|
||||
except ExperimentExistsError as err:
|
||||
if not (kwargs.get("force", False) or kwargs.get("reset", False)):
|
||||
raise err
|
||||
|
||||
return self._stash_exp(*args, **kwargs)
|
||||
|
||||
def _resume_checkpoint(
|
||||
@@ -638,8 +676,8 @@ class Experiments:
|
||||
name=item.name,
|
||||
branch=item.branch,
|
||||
tmp_dir=base_tmp_dir,
|
||||
cache_dir=self.repo.odb.local.cache_dir,
|
||||
)
|
||||
executor.init_cache(self.repo, stash_rev)
|
||||
executors[stash_rev] = executor
|
||||
|
||||
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
|
||||
@@ -657,6 +695,8 @@ class Experiments:
|
||||
dict mapping stash revs to the successfully executed experiments
|
||||
for each stash rev.
|
||||
"""
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
result: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||
|
||||
manager = Manager()
|
||||
@@ -759,6 +799,7 @@ class Experiments:
|
||||
@unlocked_repo
|
||||
def _workspace_repro(self) -> Mapping[str, str]:
|
||||
"""Run the most recently stashed experiment in the workspace."""
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
from .executor.base import BaseExecutor
|
||||
@@ -771,7 +812,7 @@ class Experiments:
|
||||
# `checkout --force` here will not lose any data (popping stash commit
|
||||
# will result in conflict between workspace params and stashed CLI
|
||||
# params, but we always want the stashed version).
|
||||
with self.scm.detach_head(entry.rev, force=True):
|
||||
with self.scm.detach_head(entry.rev, force=True, client="dvc"):
|
||||
rev = self.stash.pop()
|
||||
self.scm.set_ref(EXEC_BASELINE, entry.baseline_rev)
|
||||
if entry.branch:
|
||||
@@ -844,7 +885,9 @@ class Experiments:
|
||||
return self._get_baseline(rev)
|
||||
|
||||
def _get_baseline(self, rev):
|
||||
rev = self.scm.resolve_rev(rev)
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
rev = resolve_rev(self.scm, rev)
|
||||
|
||||
if rev in self.stash_revs:
|
||||
entry = self.stash_revs.get(rev)
|
||||
@@ -893,7 +936,7 @@ class Experiments:
|
||||
from .executor.base import BaseExecutor, ExecutorInfo
|
||||
|
||||
result = {}
|
||||
for pidfile in self.repo.fs.walk_files(
|
||||
for pidfile in self.repo.fs.find(
|
||||
os.path.join(self.repo.tmp_dir, self.EXEC_PID_DIR)
|
||||
):
|
||||
rev, _ = os.path.splitext(os.path.basename(pidfile))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user