mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c62b1a1c88 | ||
|
|
055f71a968 | ||
|
|
ebed534c7a | ||
|
|
b4588671a0 | ||
|
|
423d620b89 | ||
|
|
65dd8cafec | ||
|
|
8106d6bf09 | ||
|
|
9c33deab34 | ||
|
|
6de91c0b28 | ||
|
|
88ed542b94 | ||
|
|
55eadba0ec | ||
|
|
fc7545b6cf | ||
|
|
7d069d2210 | ||
|
|
fb65bb3967 | ||
|
|
4d03f69a86 | ||
|
|
31585c8d1a | ||
|
|
d41498bdea | ||
|
|
46831ea00a | ||
|
|
20517e44b8 | ||
|
|
1922d1189d | ||
|
|
4ee615e5b4 | ||
|
|
c06eca5709 | ||
|
|
e6e72febf9 | ||
|
|
763839f6d0 | ||
|
|
6b50757aaa | ||
|
|
d2bc79bec3 | ||
|
|
33d8506db7 | ||
|
|
683f2c9a57 | ||
|
|
d09de7e658 | ||
|
|
181ed5e55c | ||
|
|
aa8b52908e | ||
|
|
7e1b584479 | ||
|
|
3700fbf99d | ||
|
|
b6842fb089 | ||
|
|
2afe950a27 | ||
|
|
19eeb62e6e | ||
|
|
abfb48e896 | ||
|
|
111c51f44b | ||
|
|
f304d3548f | ||
|
|
eef34b567b | ||
|
|
49cadb68da | ||
|
|
75d6c930ca | ||
|
|
71ac9572bb | ||
|
|
7720c35544 | ||
|
|
f00d08d46d | ||
|
|
7398a174a8 | ||
|
|
4aab443675 | ||
|
|
bdf4095297 | ||
|
|
acae1b996f | ||
|
|
c4295fa05c | ||
|
|
7ae97009fa | ||
|
|
7865ddd128 | ||
|
|
838d597983 | ||
|
|
0ee7d5a7f8 | ||
|
|
8966d6ce47 | ||
|
|
d27fe82053 | ||
|
|
d108e9b03d | ||
|
|
b01245f007 | ||
|
|
a9d21f19ab | ||
|
|
ed93e0d0d9 | ||
|
|
afcc51417c | ||
|
|
0d42ad0d1e | ||
|
|
0ec77180d4 | ||
|
|
d5470419ce | ||
|
|
9306ea2cd4 | ||
|
|
7493804312 | ||
|
|
c8ca89f3dd | ||
|
|
548dc3657b | ||
|
|
6d848cfe64 | ||
|
|
bc711417cd | ||
|
|
5654fc269c | ||
|
|
239313d67e | ||
|
|
b415ae9a21 | ||
|
|
7a004fa328 | ||
|
|
4c5f9910bb | ||
|
|
437c2e7cbe | ||
|
|
94cf83c1cd | ||
|
|
3f18200539 | ||
|
|
43f26fdbd3 | ||
|
|
37f85720a2 | ||
|
|
03eac5b0e5 | ||
|
|
0bc96a2fcb | ||
|
|
90a636e33f | ||
|
|
046010a710 | ||
|
|
09a21cf854 | ||
|
|
e7c920d783 | ||
|
|
aa07b10882 | ||
|
|
1c36ef3f43 | ||
|
|
06697c786c | ||
|
|
eb7b9d9b89 | ||
|
|
9ed4f113f2 | ||
|
|
84e748f831 | ||
|
|
78a20a3895 | ||
|
|
0f20c86eab | ||
|
|
7adafd0b5c | ||
|
|
16fef9bb87 | ||
|
|
815c591adc | ||
|
|
22ba202462 | ||
|
|
53652399b3 | ||
|
|
f57198e6af | ||
|
|
a7b34ddecd | ||
|
|
c195b18490 | ||
|
|
feae736e40 | ||
|
|
54b1ecbf4a | ||
|
|
cf44027604 | ||
|
|
0e77b9a2fb | ||
|
|
6194b7c209 | ||
|
|
4503a7f503 | ||
|
|
31fed211f6 | ||
|
|
9837625a0d | ||
|
|
a0b5bdd143 | ||
|
|
888db1bbb6 | ||
|
|
ecb4f0b998 | ||
|
|
c6811ae005 | ||
|
|
6ad674ca88 | ||
|
|
45afd36d76 | ||
|
|
0aac01ab88 | ||
|
|
ce3bcc4af9 | ||
|
|
06064061d6 | ||
|
|
cae6e712c6 | ||
|
|
4b525fe1fc | ||
|
|
31b7a5766f | ||
|
|
13a2e4692d | ||
|
|
1de87bc91c | ||
|
|
82b10a8256 | ||
|
|
a736082595 | ||
|
|
4888ab82a7 | ||
|
|
bc93cc0bf6 | ||
|
|
971f3259f1 | ||
|
|
a4f9789aa4 | ||
|
|
584972ba0a | ||
|
|
b2b76b84d6 | ||
|
|
a6ea2f3db9 | ||
|
|
881f188016 | ||
|
|
c3f145591b | ||
|
|
1a8842d8c6 | ||
|
|
143a7f79a1 | ||
|
|
7bff8c6f89 | ||
|
|
2daa694bf2 | ||
|
|
27a96ef814 | ||
|
|
315bbfde3b | ||
|
|
53b37bb266 | ||
|
|
005db9bb3a | ||
|
|
7b36fd007c | ||
|
|
2c9b08a89b | ||
|
|
66a962976e | ||
|
|
4eff31dd5e | ||
|
|
377be75dd0 | ||
|
|
e015b9a167 | ||
|
|
2a7ec2d2df | ||
|
|
3d34f4e8c7 | ||
|
|
c10bd344ea | ||
|
|
6a8cd31ac9 | ||
|
|
2c49d7d004 | ||
|
|
990bbe32c1 | ||
|
|
f3668b0a4e | ||
|
|
73eaa2a611 | ||
|
|
e63ae3d676 | ||
|
|
9528ad6a1d | ||
|
|
aea92e0bdf | ||
|
|
d1077c4457 | ||
|
|
edf73dc52f | ||
|
|
b587f7833a | ||
|
|
998918c690 | ||
|
|
8aa53a5e71 | ||
|
|
cc2ebb2099 | ||
|
|
b9439f2e5b | ||
|
|
d112d2a829 | ||
|
|
f4fc3f47e4 | ||
|
|
417ad8706e | ||
|
|
2e09dbb592 | ||
|
|
67d63a4a5b | ||
|
|
8eb6fd5285 | ||
|
|
669b2ec303 | ||
|
|
04b3cd40e0 | ||
|
|
12d6292ed6 | ||
|
|
27edc4f027 | ||
|
|
5b0abe61ab | ||
|
|
1e1daeaae1 | ||
|
|
ece2b8ae0b | ||
|
|
fcea9dccc5 | ||
|
|
51b390146a | ||
|
|
1a6dedfe60 | ||
|
|
88f0913f91 | ||
|
|
b38112795a | ||
|
|
363db9125f | ||
|
|
89e03c8606 | ||
|
|
7bed7213dc | ||
|
|
76c2f812f0 | ||
|
|
8d151273c8 | ||
|
|
10e10f9535 | ||
|
|
3fd5603921 | ||
|
|
ff63376d49 | ||
|
|
5071b5026d | ||
|
|
c08804eddd | ||
|
|
cbc8cf23f9 | ||
|
|
5279b1ae8f | ||
|
|
057663696a | ||
|
|
8b513fb336 | ||
|
|
04373e81e8 | ||
|
|
2601766323 | ||
|
|
ad216acce4 | ||
|
|
5753944161 | ||
|
|
b7ac347c06 | ||
|
|
beedab77f1 | ||
|
|
b1b0e3413d | ||
|
|
c4593cd369 | ||
|
|
6a55d1502f | ||
|
|
e93a87f451 | ||
|
|
3fff8d771d | ||
|
|
69bb753980 | ||
|
|
1d654d0444 | ||
|
|
0618d9d0dc | ||
|
|
dea130cc61 | ||
|
|
bbd5096de7 | ||
|
|
545ea1efa5 | ||
|
|
e1841f07cb | ||
|
|
04efd1d6d3 | ||
|
|
89aa2a957e | ||
|
|
1da4546515 | ||
|
|
49c22cb139 | ||
|
|
4aa26a0a3a | ||
|
|
8d5675a92c | ||
|
|
2b1ea1e4b9 | ||
|
|
34faad3e96 | ||
|
|
9dab3f2e49 | ||
|
|
72e8b6983b | ||
|
|
682737e4a2 | ||
|
|
8932254c12 | ||
|
|
0c97e19d5c | ||
|
|
e698703969 | ||
|
|
571713484b | ||
|
|
65a9370281 | ||
|
|
0043bd5de3 | ||
|
|
33d04d0d59 | ||
|
|
5e3d7c6c2b | ||
|
|
59dcdb02ea | ||
|
|
23d00a15b9 | ||
|
|
2c3c0b08bd | ||
|
|
75d7fc2206 | ||
|
|
d704c41682 | ||
|
|
428811bb71 | ||
|
|
470825aec5 | ||
|
|
5fbfc135da | ||
|
|
b4e62772b8 | ||
|
|
912f0f72e5 | ||
|
|
5730b1d088 | ||
|
|
5c762f316e | ||
|
|
7791dc8cfb | ||
|
|
dc344056c5 | ||
|
|
87e82daad2 | ||
|
|
f044a6090f | ||
|
|
c68823e8d0 | ||
|
|
a17dc2504d | ||
|
|
537e7c75a0 | ||
|
|
76d6b37b1e | ||
|
|
8d13ba164e | ||
|
|
559c097394 | ||
|
|
c97bfea90f | ||
|
|
abf395da99 | ||
|
|
0687c4e199 | ||
|
|
d1931e3dc0 | ||
|
|
78c9d1edab | ||
|
|
7ef8645465 | ||
|
|
208540c66d | ||
|
|
216a6e5c1c | ||
|
|
8d8a94bcb7 | ||
|
|
1a18c4d3f1 | ||
|
|
211b957d17 | ||
|
|
4f6967a017 | ||
|
|
c1ac1b4c6e | ||
|
|
022eea8214 | ||
|
|
f1f1a97fbf | ||
|
|
a3aefe5f16 | ||
|
|
de3d8b528f | ||
|
|
9c041a375f | ||
|
|
086155daa6 | ||
|
|
d47e6c39ab | ||
|
|
249b60c030 | ||
|
|
392cd006e3 | ||
|
|
2b0391629b | ||
|
|
3b66176f6b | ||
|
|
baa6bcd035 | ||
|
|
b159ace6d7 | ||
|
|
ffde4a6a7a | ||
|
|
9a95698cd9 | ||
|
|
221dbf8245 | ||
|
|
3d15391c7a | ||
|
|
f6f2ebb83e | ||
|
|
ac80d59704 | ||
|
|
3574c8954b | ||
|
|
0d78705227 | ||
|
|
bf6ed76678 | ||
|
|
076a2145db | ||
|
|
bb3cca6530 | ||
|
|
165422a066 | ||
|
|
49baf38871 | ||
|
|
3461df1d8f | ||
|
|
03a3cf2f80 | ||
|
|
94911965c4 | ||
|
|
55b3c47bbf | ||
|
|
825da210cf | ||
|
|
ac97c4021d | ||
|
|
213e757b52 | ||
|
|
6f0e293c9a | ||
|
|
7783bb900b | ||
|
|
bdca1ce8cc | ||
|
|
80912b4a2f | ||
|
|
c46bba6889 | ||
|
|
1283689ed4 | ||
|
|
9c5bbecb2e | ||
|
|
316ea1d7cb | ||
|
|
2139978646 | ||
|
|
fa89ec8789 | ||
|
|
93ef937fc2 | ||
|
|
253315e4a8 | ||
|
|
9579cb7928 | ||
|
|
18f0ca0d92 | ||
|
|
d555bae6e1 | ||
|
|
4a43a29f11 | ||
|
|
2ff7d9b571 | ||
|
|
9d303b6b1b | ||
|
|
f682e556ec | ||
|
|
26e007f407 | ||
|
|
a7f14c2d3c | ||
|
|
79f9cedd37 | ||
|
|
623d3e2fed | ||
|
|
cb7eed71de | ||
|
|
9135944d7c | ||
|
|
ac4fff2c45 | ||
|
|
912538f165 | ||
|
|
35c5d4c294 | ||
|
|
3887e973c4 | ||
|
|
173a9fc7aa | ||
|
|
0281c2714c | ||
|
|
6d10bff23b | ||
|
|
ff5152ee5f | ||
|
|
c6f642fd6b | ||
|
|
ee0dcc384d | ||
|
|
8a484df228 | ||
|
|
c378e4b3d2 | ||
|
|
00d53f50b1 | ||
|
|
65ffc7d248 | ||
|
|
95479e6d6b | ||
|
|
79b9ae75e9 | ||
|
|
a46b1be925 | ||
|
|
a566e92ce0 | ||
|
|
adbfbfd732 | ||
|
|
d2b562e25a | ||
|
|
bcea7a132a | ||
|
|
d1aa1f3491 | ||
|
|
246b80d6a4 | ||
|
|
8f2d5c4195 | ||
|
|
bd9da20e20 | ||
|
|
c6138729f9 | ||
|
|
6743b36894 | ||
|
|
0dd995453f | ||
|
|
42a15d91f7 | ||
|
|
a6cb238982 | ||
|
|
1b4d7abce3 | ||
|
|
e6def1287d | ||
|
|
ce5a46d032 | ||
|
|
fcadd18069 | ||
|
|
a3588c4504 | ||
|
|
86322fbe35 | ||
|
|
c635adbfc7 | ||
|
|
6e95508e9e | ||
|
|
aaca0e7a38 | ||
|
|
487993995a | ||
|
|
e84f231c34 | ||
|
|
2b19b6bbfa | ||
|
|
db017543d5 | ||
|
|
bc2c84370d | ||
|
|
d34119d6f0 | ||
|
|
b940380e12 | ||
|
|
e3d39063ba | ||
|
|
11db27f333 | ||
|
|
15210f73b2 | ||
|
|
9be3f4f122 | ||
|
|
ddf1839d38 | ||
|
|
3ec0a5c4ae | ||
|
|
3814e0da4e | ||
|
|
292702473b | ||
|
|
6776f6e21c | ||
|
|
b261554818 | ||
|
|
10dd535176 | ||
|
|
e8949c1a77 | ||
|
|
6a8703e54e | ||
|
|
6cc0c30937 | ||
|
|
b15f5ffa3b | ||
|
|
d343f8562c | ||
|
|
321c1d5f8b | ||
|
|
f6e20b162c | ||
|
|
a249df3f5d | ||
|
|
fb4b979183 | ||
|
|
98a7150c65 | ||
|
|
dd99a09135 | ||
|
|
f854f5a97a | ||
|
|
9e068234ba | ||
|
|
6600ccf478 | ||
|
|
84abef7ac0 | ||
|
|
9ff2503a72 | ||
|
|
4ff4a1042e | ||
|
|
2195ff2b13 | ||
|
|
e539e74fde | ||
|
|
c345552460 | ||
|
|
cee8e1ec9f | ||
|
|
e4b8cf2e0e | ||
|
|
e2e7649afe | ||
|
|
77b2e45a64 | ||
|
|
e403568afe | ||
|
|
79eac34163 | ||
|
|
d9b7f7b97a | ||
|
|
cbe9fde7a6 | ||
|
|
24be60bf39 | ||
|
|
fe38b2768f | ||
|
|
105d8fc9b5 | ||
|
|
9d7581c6bd | ||
|
|
a14946d0bb | ||
|
|
6ad361b741 | ||
|
|
6fe6c04f6c | ||
|
|
23b167f5d7 | ||
|
|
2dde6e848b | ||
|
|
5e5705ae7e | ||
|
|
a2d3ced7f1 | ||
|
|
0804f84679 | ||
|
|
02da5eabdc | ||
|
|
85ab6a141b | ||
|
|
8d1e18d543 | ||
|
|
d42809abd3 | ||
|
|
f5bb3e4501 | ||
|
|
aab04a22ad | ||
|
|
ed8052c229 | ||
|
|
7baf368367 | ||
|
|
b5b8b8d06c | ||
|
|
75a6352d3d | ||
|
|
baac7a16bd | ||
|
|
e4137542df | ||
|
|
b9a5bde22f | ||
|
|
891ec0bad4 | ||
|
|
a0a138c69f | ||
|
|
1e4f6e9f09 | ||
|
|
9f5ba8c38f | ||
|
|
ef7aa024f9 | ||
|
|
48c9916937 | ||
|
|
04f0320126 | ||
|
|
536f369f7e | ||
|
|
23a4b296e9 | ||
|
|
a4482bac3b | ||
|
|
e5fd24f530 | ||
|
|
4dc61beab0 | ||
|
|
f02bf73587 | ||
|
|
cb13e45632 | ||
|
|
d0ce57adac | ||
|
|
eaa994527d | ||
|
|
89617501a4 | ||
|
|
8be51b6056 | ||
|
|
f7a17a9336 | ||
|
|
2788288e8e | ||
|
|
80e7040a21 | ||
|
|
bd4bbf1438 | ||
|
|
935cbc9513 | ||
|
|
98c5875668 | ||
|
|
42111b6348 | ||
|
|
3363b85367 | ||
|
|
83994d6ea7 | ||
|
|
568a8fb904 | ||
|
|
47d00e80d2 | ||
|
|
a9114d57a5 | ||
|
|
1bfcc12853 | ||
|
|
be817c5b84 | ||
|
|
1f15cf23e7 | ||
|
|
28e2f7d06f | ||
|
|
6bf853d635 | ||
|
|
8120104f2e | ||
|
|
1cb41f0a8a | ||
|
|
ad204cf915 | ||
|
|
ac3963878e | ||
|
|
dd891a8cd3 | ||
|
|
6c54810034 | ||
|
|
f23671a26a | ||
|
|
229983c403 | ||
|
|
6b042fb15a | ||
|
|
1879d49bc2 | ||
|
|
627606d6f2 | ||
|
|
7e4f39d8eb |
+1
-1
@@ -54,7 +54,7 @@ install:
|
||||
- cinst gcloudsdk
|
||||
- refreshenv
|
||||
- cinst gsutil
|
||||
- cinst openssl.light
|
||||
- cinst openssl.light --version 1.1.1
|
||||
- python -m pip install -U pip setuptools wheel
|
||||
- pip install -r requirements.txt
|
||||
- python setup.py install
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
languages:
|
||||
Python: true
|
||||
|
||||
checks:
|
||||
argument-count:
|
||||
enabled: false
|
||||
method-complexity:
|
||||
enabled: false
|
||||
method-lines:
|
||||
config:
|
||||
threshold: 50
|
||||
|
||||
exclude_paths:
|
||||
- tests/
|
||||
|
||||
@@ -7,3 +7,5 @@ exclude_lines =
|
||||
if __name__ == .__main__.:
|
||||
pass
|
||||
raise NotImplementedError
|
||||
# pragma: no cover
|
||||
# pragma: no cover
|
||||
|
||||
+3
-1
@@ -5,4 +5,6 @@ config.local
|
||||
updater
|
||||
cache
|
||||
state
|
||||
state.lock
|
||||
state.lock
|
||||
state-journal
|
||||
state-wal
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
|
||||
**Please provide information about your setup**
|
||||
DVC version(i.e. `dvc --version`), Platform and method of installation (pip, homebrew, pkg Mac, exe (Windows), DEB(Linux), RPM(Linux))
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
+13
-7
@@ -1,4 +1,5 @@
|
||||
language: python
|
||||
cache: pip
|
||||
sudo: required
|
||||
dist: trusty
|
||||
group: deprecated-2017Q4
|
||||
@@ -15,18 +16,24 @@ python:
|
||||
# version, that causes fpm to crash.
|
||||
matrix:
|
||||
include:
|
||||
# 3.7 needs Xenial image because of https://github.com/travis-ci/travis-ci/issues/9069
|
||||
- python: "3.7"
|
||||
dist: xenial
|
||||
sudo: true
|
||||
- if: type = cron OR tag is present
|
||||
os: osx
|
||||
osx_image: xcode9.3
|
||||
osx_image: xcode7.3
|
||||
language: ruby
|
||||
rvm: 2.4.0
|
||||
env:
|
||||
- PYTHON_VER=2.7.10
|
||||
- PYENV_ROOT="$HOME/.pyenv"
|
||||
- PATH="$PYENV_ROOT/bin:$PATH"
|
||||
- CPPFLAGS="-I/usr/local/opt/openssl/include $CPPFLAGS"
|
||||
- LDFLAGS="-L/usr/local/opt/openssl/lib $LDFLAGS"
|
||||
- if: type = cron OR tag is present
|
||||
os: osx
|
||||
osx_image: xcode9.3
|
||||
osx_image: xcode7.3
|
||||
language: ruby
|
||||
rvm: 2.4.0
|
||||
env:
|
||||
@@ -35,23 +42,20 @@ matrix:
|
||||
- PATH="$PYENV_ROOT/bin:$PATH"
|
||||
- if: type = cron OR tag is present
|
||||
os: osx
|
||||
osx_image: xcode9.3
|
||||
osx_image: xcode7.3
|
||||
language: ruby
|
||||
rvm: 2.4.0
|
||||
env:
|
||||
- PYTHON_VER=3.5.0
|
||||
- PYENV_ROOT="$HOME/.pyenv"
|
||||
- PATH="$PYENV_ROOT/bin:$PATH"
|
||||
# 3.7 needs Xenial image because of https://github.com/travis-ci/travis-ci/issues/9069
|
||||
- python: "3.7"
|
||||
dist: xenial
|
||||
sudo: true
|
||||
before_install:
|
||||
- bash ./scripts/ci/before_install.sh
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
eval "$(pyenv init -)";
|
||||
PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install --skip-existing --keep --verbose "$PYTHON_VER";
|
||||
pyenv shell "$PYTHON_VER";
|
||||
brew install openssl;
|
||||
brew cask install google-cloud-sdk;
|
||||
fi
|
||||
- which python
|
||||
@@ -77,6 +81,8 @@ install:
|
||||
- bash ./scripts/ci/install.sh
|
||||
- pip install flake8
|
||||
before_script:
|
||||
# stop the build if there are any readme formatting errors
|
||||
- python setup.py checkdocs
|
||||
# stop the build if there are any flake8 errors
|
||||
- flake8 dvc
|
||||
script:
|
||||
|
||||
+144
-46
@@ -1,20 +1,68 @@
|
||||
.. image:: https://img.shields.io/travis/iterative/dvc/master.svg?label=Linux%20%26%20Mac%20OS
|
||||
:target: https://travis-ci.org/iterative/dvc
|
||||
.. image:: https://dvc.org/static/img/logo-owl-readme.png
|
||||
:target: https://dvc.org
|
||||
:alt: DVC logo
|
||||
|
||||
.. image:: https://img.shields.io/appveyor/ci/iterative/dvc/master.svg?label=Windows
|
||||
`Website <https://dvc.org>`_
|
||||
• `Docs <https://dvc.org/doc>`_
|
||||
• `Twitter <https://twitter.com/iterativeai>`_
|
||||
• `Chat (Community & Support) <https://dvc.org/chat>`_
|
||||
• `Tutorial <https://dvc.org/doc/tutorial>`_
|
||||
• `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
|
||||
|
||||
.. image:: https://travis-ci.org/iterative/dvc.svg?branch=master
|
||||
:target: https://travis-ci.org/iterative/dvc
|
||||
:alt: Travis
|
||||
|
||||
.. image:: https://ci.appveyor.com/api/projects/status/github/iterative/dvc?branch=master&svg=true
|
||||
:target: https://ci.appveyor.com/project/iterative/dvc/branch/master
|
||||
:alt: Windows Build
|
||||
|
||||
.. image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
|
||||
:target: https://codeclimate.com/github/iterative/dvc
|
||||
:alt: Code Climate
|
||||
|
||||
.. image:: https://codecov.io/gh/iterative/dvc/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/iterative/dvc
|
||||
:alt: Codecov
|
||||
|
||||
Data Version Control or DVC is an open source tool for data science projects.
|
||||
It helps data scientists manage their code and data together in a simple form of Git-like commands.
|
||||
|
|
||||
|
||||
Get started
|
||||
**Data Science Version Control** or **DVC** is an **open-source** tool for data science and
|
||||
machine learning projects. With a simple and flexible Git-like architecture and interface it
|
||||
helps data scientists:
|
||||
|
||||
#. manage **machine learning models** - versioning, including data sets and transformations (scripts) that were
|
||||
used to generate models;
|
||||
|
||||
#. make projects **reproducible**;
|
||||
|
||||
#. make projects **shareable**;
|
||||
|
||||
#. manage experiments with branching and **metrics** tracking;
|
||||
|
||||
It aims to replace tools like Excel and Docs that are being commonly used as a knowledge repo and
|
||||
a ledger for the team, ad-hoc scripts to track and move deploy different model versions, ad-hoc
|
||||
data file suffixes and prefixes.
|
||||
|
||||
.. contents:: **Contents**
|
||||
:backlinks: none
|
||||
|
||||
How DVC works
|
||||
=============
|
||||
|
||||
DVC is compatible with Git for storing code and the dependency graph (DAG), but not data files cache.
|
||||
To store and share data files cache DVC supports remotes - any cloud (S3, Azure, Google Cloud, etc) or any on-premise
|
||||
network storage (via SSH, for example).
|
||||
|
||||
.. image:: https://dvc.org/static/img/flow.gif
|
||||
:target: https://dvc.org/static/img/flow.gif
|
||||
:alt: how_dvc_works
|
||||
|
||||
Quick start
|
||||
===========
|
||||
|
||||
Please read `Get Started <https://dvc.org/doc/get-started>`_ for the full version. Common workflow commands include:
|
||||
|
||||
+-----------------------------------+-------------------------------------------------------------------+
|
||||
| Step | Command |
|
||||
+===================================+===================================================================+
|
||||
@@ -32,86 +80,136 @@ Get started
|
||||
| | | ``$ git push`` |
|
||||
+-----------------------------------+-------------------------------------------------------------------+
|
||||
| Share data and ML models | | ``$ dvc remote add myremote s3://mybucket/image_cnn`` |
|
||||
| | | ``$ dvc core.remote myremote`` |
|
||||
| | | ``$ dvc config core.remote myremote`` |
|
||||
| | | ``$ dvc push`` |
|
||||
+-----------------------------------+-------------------------------------------------------------------+
|
||||
|
||||
See more in `tutorial <https://dvc.org/doc/tutorial>`_.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Packages
|
||||
--------
|
||||
There are three options to install DVC: ``pip``, Homebrew, or an OS-specific package:
|
||||
|
||||
Operating system dependent packages are the recommended way to install DVC.
|
||||
The latest version of the packages can be found at GitHub releases page: https://github.com/iterative/dvc/releases
|
||||
|
||||
Python Pip
|
||||
pip (PyPI)
|
||||
----------
|
||||
|
||||
DVC could be installed via the Python Package Index (PyPI).
|
||||
|
||||
Stable
|
||||
^^^^^^
|
||||
.. code-block:: bash
|
||||
|
||||
pip install dvc
|
||||
|
||||
Homebrew (Mac OS)
|
||||
-----------------
|
||||
Development
|
||||
^^^^^^^^^^^
|
||||
.. code-block:: bash
|
||||
|
||||
Formula:
|
||||
^^^^^^^^
|
||||
pip install git+git://github.com/iterative/dvc
|
||||
|
||||
Homebrew
|
||||
--------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install iterative/homebrew-dvc/dvc
|
||||
|
||||
Cask:
|
||||
^^^^^
|
||||
or:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew cask install iterative/homebrew-dvc/dvc
|
||||
|
||||
Links
|
||||
=====
|
||||
Package
|
||||
-------
|
||||
|
||||
Website: https://dvc.org
|
||||
Self-contained packages for Windows, Linux, Mac are available. The latest version of the packages can be found at
|
||||
GitHub `releases page <https://github.com/iterative/dvc/releases>`_.
|
||||
|
||||
Tutorial: https://dvc.org/doc/tutorial
|
||||
Ubuntu / Debian (apt)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
.. code-block:: bash
|
||||
|
||||
Documentation: http://dvc.org/doc
|
||||
sudo wget https://dvc.org/deb/dvc.list -O etc/apt/sources.list.d/dvc.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install dvc
|
||||
|
||||
Discussion: https://discuss.dvc.org/
|
||||
Fedora / CentOS (rpm)
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
.. code-block:: bash
|
||||
|
||||
sudo wget https://dvc.org/rpm/dvc.repo -O /etc/yum.repos.d/dvc.repo
|
||||
sudo yum update
|
||||
sudo yum install dvc
|
||||
|
||||
Arch linux (AUR)
|
||||
^^^^^^^^^^^^^^^^
|
||||
*Unofficial package*, any inquiries regarding the AUR package,
|
||||
`refer to the maintainer <https://github.com/mroutis/pkgbuilds>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
yay -S dvc
|
||||
|
||||
Related technologies
|
||||
====================
|
||||
|
||||
#. `Git-annex <https://git-annex.branchable.com/>`_ - DVC uses the idea of storing the content of large files (that you
|
||||
don't want to see in your Git repository) in a local key-value store and uses file hardlinks/symlinks instead of the
|
||||
copying actual files.
|
||||
|
||||
#. `Git-annex <https://git-annex.branchable.com/>`_ - DVC uses the idea of storing the content of large files (that you don't want to see in your Git repository) in a local key-value store and uses file hardlinks/symlinks instead of the copying actual files.
|
||||
#. `Git-LFS <https://git-lfs.github.com/>`_.
|
||||
#. Makefile (and it's analogues). DVC tracks dependencies (DAG).
|
||||
#. `Workflow Management Systems <https://en.wikipedia.org/wiki/Workflow_management_system>`_. DVC is workflow management system designed specificaly to manage machine learning experiments. DVC was built on top of Git.
|
||||
#. `Git-LFS <https://git-lfs.github.com/>`_ - DVC is compatible with any remote storage (S3, Google Cloud, Azure, SSH,
|
||||
etc). DVC utilizes reflinks or hardlinks to avoid copy operation on checkouts which makes much more efficient for
|
||||
large data files.
|
||||
|
||||
DVC is compatible with Git for storing code and the dependency graph (DAG), but not data files cache.
|
||||
Data files caches can be transferred separately - now data cache transfer throught AWS S3, Azure Blob Storage and GCP storge are supported.
|
||||
|
||||
How DVC works
|
||||
=============
|
||||
|
||||
|
||||
.. image:: https://dvc.org/static/img/flow.png
|
||||
:target: https://dvc.org/static/img/flow.png
|
||||
:alt: how_dvc_works
|
||||
#. *Makefile* (and its analogues). DVC tracks dependencies (DAG).
|
||||
|
||||
#. `Workflow Management Systems <https://en.wikipedia.org/wiki/Workflow_management_system>`_. DVC is a workflow
|
||||
management system designed specifically to manage machine learning experiments. DVC is built on top of Git.
|
||||
|
||||
Contributing
|
||||
=========
|
||||
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/>`_ for more details.
|
||||
============
|
||||
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/>`_ for more
|
||||
details.
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/0
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/0
|
||||
:alt: 0
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/1
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/1
|
||||
:alt: 1
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/2
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/2
|
||||
:alt: 2
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/3
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/3
|
||||
:alt: 3
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/4
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/4
|
||||
:alt: 4
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/5
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/5
|
||||
:alt: 5
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/6
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/6
|
||||
:alt: 6
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/7
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/7
|
||||
:alt: 7
|
||||
|
||||
Mailing List
|
||||
============
|
||||
|
||||
Want to stay up to date? Want to help improve DVC by participating in our ocassional polls? Subscribe to our `mailing list <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_. No spam, really low traffic.
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This project is distributed under the Apache license version 2.0 (see the LICENSE file in the project root).
|
||||
|
||||
By submitting a pull request for this project, you agree to license your contribution under the Apache license version 2.0 to this project.
|
||||
By submitting a pull request for this project, you agree to license your contribution under the Apache license version
|
||||
2.0 to this project.
|
||||
|
||||
+14
-4
@@ -4,8 +4,10 @@ DVC
|
||||
Make your data science projects reproducible and shareable.
|
||||
"""
|
||||
import os
|
||||
import warnings
|
||||
|
||||
VERSION_BASE = '0.14.4'
|
||||
|
||||
VERSION_BASE = '0.20.4'
|
||||
__version__ = VERSION_BASE
|
||||
|
||||
PACKAGEPATH = os.path.abspath(os.path.dirname(__file__))
|
||||
@@ -31,13 +33,13 @@ if os.path.exists(os.path.join(HOMEPATH, 'setup.py')):
|
||||
with open(VERSIONPATH, 'w+') as fd:
|
||||
fd.write('# AUTOGENERATED by dvc/__init__.py\n')
|
||||
fd.write('version = "{}"\n'.format(__version__))
|
||||
except Exception:
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
# Remove version.py so that it doesn't get into the release
|
||||
if os.path.exists(VERSIONPATH):
|
||||
os.unlink(VERSIONPATH)
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
# dvc was installed with pip or something. Hopefully we have our
|
||||
# auto-generated version.py to help us provide a true version
|
||||
try:
|
||||
@@ -47,3 +49,11 @@ else:
|
||||
pass
|
||||
|
||||
VERSION = __version__
|
||||
|
||||
|
||||
# Ignore numpy's runtime warnings: https://github.com/numpy/numpy/pull/432.
|
||||
# We don't directly import numpy, but our dependency networkx does, causing
|
||||
# these warnings in some environments. Luckily these warnings are benign and
|
||||
# we can simply ignore them so that they don't show up when you are using dvc.
|
||||
warnings.filterwarnings("ignore", message="numpy.dtype size changed")
|
||||
warnings.filterwarnings("ignore", message="numpy.ufunc size changed")
|
||||
|
||||
+10
-6
@@ -12,21 +12,25 @@ class Cache(object):
|
||||
self.project = project
|
||||
|
||||
config = project.config._config[Config.SECTION_CACHE]
|
||||
local = config.get(Config.SECTION_CACHE_LOCAL)
|
||||
|
||||
local = config.get(Config.SECTION_CACHE_LOCAL, None)
|
||||
if local:
|
||||
name = Config.SECTION_REMOTE_FMT.format(local)
|
||||
sect = project.config._config[name]
|
||||
else:
|
||||
sect = {}
|
||||
cache_dir = config.get(Config.SECTION_CACHE_DIR, self.CACHE_DIR)
|
||||
cache_type = config.get(Config.SECTION_CACHE_TYPE)
|
||||
protected = config.get(Config.SECTION_CACHE_PROTECTED)
|
||||
|
||||
if not os.path.isabs(cache_dir):
|
||||
cache_dir = os.path.join(project.dvc_dir, cache_dir)
|
||||
cache_dir = os.path.abspath(os.path.realpath(cache_dir))
|
||||
sect[Config.SECTION_REMOTE_URL] = cache_dir
|
||||
t = config.get(Config.SECTION_CACHE_TYPE, None)
|
||||
if t:
|
||||
sect[Config.SECTION_CACHE_TYPE] = t
|
||||
|
||||
sect = {
|
||||
Config.SECTION_REMOTE_URL: cache_dir,
|
||||
Config.SECTION_CACHE_TYPE: cache_type,
|
||||
Config.SECTION_CACHE_PROTECTED: protected
|
||||
}
|
||||
|
||||
self.local = Remote(project, sect)
|
||||
|
||||
|
||||
+417
-112
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -7,7 +7,7 @@ class CmdAdd(CmdBase):
|
||||
def run(self):
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
self.project.add(target)
|
||||
self.project.add(target, recursive=self.args.recursive)
|
||||
except DvcException as ex:
|
||||
Logger.error('Failed to add \'{}\''.format(target), ex)
|
||||
return 1
|
||||
|
||||
+3
-2
@@ -15,9 +15,10 @@ class CmdBase(object):
|
||||
|
||||
@staticmethod
|
||||
def _set_loglevel(args):
|
||||
if args.quiet and not args.verbose:
|
||||
if args.quiet:
|
||||
Logger.be_quiet()
|
||||
elif not args.quiet and args.verbose:
|
||||
|
||||
elif args.verbose:
|
||||
Logger.be_verbose()
|
||||
|
||||
def _find_root(self):
|
||||
|
||||
@@ -7,5 +7,6 @@ class CmdCheckout(CmdBase):
|
||||
self.project.checkout()
|
||||
else:
|
||||
for target in self.args.targets:
|
||||
self.project.checkout(target=target)
|
||||
self.project.checkout(target=target,
|
||||
with_deps=self.args.with_deps)
|
||||
return 0
|
||||
|
||||
@@ -22,7 +22,10 @@ class CmdDataPull(CmdDataBase):
|
||||
self.project.pull(target=target,
|
||||
jobs=self.args.jobs,
|
||||
remote=self.args.remote,
|
||||
all_branches=self.args.all_branches)
|
||||
show_checksums=self.args.show_checksums,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
with_deps=self.args.with_deps)
|
||||
except Exception as exc:
|
||||
self.project.logger.error('Failed to pull data from the cloud',
|
||||
exc)
|
||||
@@ -36,7 +39,10 @@ class CmdDataPush(CmdDataBase):
|
||||
self.project.push(target=target,
|
||||
jobs=self.args.jobs,
|
||||
remote=self.args.remote,
|
||||
all_branches=self.args.all_branches)
|
||||
show_checksums=self.args.show_checksums,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
with_deps=self.args.with_deps)
|
||||
except Exception as exc:
|
||||
self.project.logger.error('Failed to push data to the cloud', exc)
|
||||
return 1
|
||||
@@ -49,7 +55,10 @@ class CmdDataFetch(CmdDataBase):
|
||||
self.project.fetch(target=target,
|
||||
jobs=self.args.jobs,
|
||||
remote=self.args.remote,
|
||||
all_branches=self.args.all_branches)
|
||||
show_checksums=self.args.show_checksums,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
with_deps=self.args.with_deps)
|
||||
except Exception as exc:
|
||||
self.project.logger.error('Failed to fetch data from the cloud',
|
||||
exc)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.prompt import prompt
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class CmdDestroy(CmdBase):
|
||||
def run_cmd(self):
|
||||
try:
|
||||
msg = u'This will destroy all information about your pipelines ' \
|
||||
u'as well as cache in .dvc/cache.\n' \
|
||||
msg = u'This will destroy all information about your pipelines, ' \
|
||||
u'all data files, as well as cache in .dvc/cache.\n' \
|
||||
u'Are you sure you want to continue?'
|
||||
|
||||
if not self.args.force and not prompt(msg, False):
|
||||
if not self.args.force \
|
||||
and not self.project.prompt.prompt(msg, False):
|
||||
msg = u'Cannot destroy without a confirmation from the ' \
|
||||
u'user. Use \'-f\' to force.'
|
||||
raise DvcException(msg)
|
||||
|
||||
+31
-1
@@ -1,9 +1,39 @@
|
||||
import os
|
||||
|
||||
from dvc.command.base import CmdBase
|
||||
|
||||
|
||||
class CmdGC(CmdBase):
|
||||
def run(self):
|
||||
msg = 'This will remove all cache except the cache that is used in '
|
||||
if not self.args.all_branches and not self.args.all_tags:
|
||||
msg += 'the current git branch'
|
||||
elif self.args.all_branches and not self.args.all_tags:
|
||||
msg += 'all git branches'
|
||||
elif not self.args.all_branches and self.args.all_tags:
|
||||
msg += 'all git tags'
|
||||
else:
|
||||
msg += 'all git branches and all git tags'
|
||||
|
||||
if self.args.projects is not None and len(self.args.projects) > 0:
|
||||
msg += ' of the current and the following projects:'
|
||||
|
||||
for project_path in self.args.projects:
|
||||
msg += '\n - %s' % os.path.abspath(project_path)
|
||||
else:
|
||||
msg += ' of the current project.'
|
||||
|
||||
self.project.logger.warn(msg)
|
||||
|
||||
msg = 'Are you sure you want to proceed?'
|
||||
if not self.args.force and not self.project.prompt.prompt(msg):
|
||||
return 1
|
||||
|
||||
self.project.gc(all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
cloud=self.args.cloud,
|
||||
remote=self.args.remote)
|
||||
remote=self.args.remote,
|
||||
force=self.args.force,
|
||||
jobs=self.args.jobs,
|
||||
projects=self.args.projects)
|
||||
return 0
|
||||
|
||||
@@ -28,7 +28,8 @@ class CmdMetricsShow(CmdBase):
|
||||
self.project.metrics_show(self.args.path,
|
||||
typ=typ,
|
||||
xpath=xpath,
|
||||
all_branches=self.args.all_branches)
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags)
|
||||
except DvcException as exc:
|
||||
self.project.logger.error('Failed to show metrics', exc)
|
||||
return 1
|
||||
|
||||
+99
-6
@@ -2,34 +2,127 @@ import os
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.dagascii import Dagascii
|
||||
|
||||
|
||||
class CmdPipelineShow(CmdBase):
|
||||
def _show(self, target, commands, outs):
|
||||
import networkx
|
||||
from dvc.project import NotDvcFileError
|
||||
from dvc.stage import Stage
|
||||
|
||||
stage = Stage.load(self.project, target)
|
||||
G = self.project.graph()[0]
|
||||
stages = networkx.get_node_attributes(G, 'stage')
|
||||
node = os.path.relpath(os.path.abspath(target), self.project.root_dir)
|
||||
if node not in stages:
|
||||
raise NotDvcFileError(node)
|
||||
node = os.path.relpath(stage.path, self.project.root_dir)
|
||||
|
||||
for n in networkx.dfs_postorder_nodes(G, node):
|
||||
if commands:
|
||||
self.project.logger.info(stages[n].cmd)
|
||||
elif outs:
|
||||
for out in stages[n].outs:
|
||||
self.project.logger.info(out.rel_path)
|
||||
self.project.logger.info(str(out))
|
||||
else:
|
||||
self.project.logger.info(n)
|
||||
|
||||
def __build_graph(self, target, commands, outs):
|
||||
import networkx
|
||||
from dvc.stage import Stage
|
||||
|
||||
stage = Stage.load(self.project, target)
|
||||
node = os.path.relpath(stage.path, self.project.root_dir)
|
||||
|
||||
pipelines = list(filter(lambda g: node in g.nodes(),
|
||||
self.project.pipelines()))
|
||||
|
||||
assert len(pipelines) == 1
|
||||
G = pipelines[0]
|
||||
stages = networkx.get_node_attributes(G, 'stage')
|
||||
|
||||
nodes = []
|
||||
for n in G.nodes():
|
||||
stage = stages[n]
|
||||
if commands:
|
||||
if stage.cmd is None:
|
||||
continue
|
||||
nodes.append(stage.cmd)
|
||||
elif outs:
|
||||
for out in stage.outs:
|
||||
nodes.append(str(out))
|
||||
else:
|
||||
nodes.append(stage.relpath)
|
||||
|
||||
edges = []
|
||||
for e in G.edges():
|
||||
from_stage = stages[e[0]]
|
||||
to_stage = stages[e[1]]
|
||||
if commands:
|
||||
if to_stage.cmd is None:
|
||||
continue
|
||||
edges.append((from_stage.cmd, to_stage.cmd))
|
||||
elif outs:
|
||||
for from_out in from_stage.outs:
|
||||
for to_out in to_stage.outs:
|
||||
edges.append((str(from_out),
|
||||
str(to_out)))
|
||||
else:
|
||||
edges.append((from_stage.relpath, to_stage.relpath))
|
||||
|
||||
return nodes, edges
|
||||
|
||||
def _show_ascii(self, target, commands, outs):
|
||||
nodes, edges = self.__build_graph(target, commands, outs)
|
||||
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
d = Dagascii(nodes, edges)
|
||||
d.draw()
|
||||
|
||||
def __write_dot(self, target, commands, outs, filename):
|
||||
import networkx
|
||||
from networkx.drawing.nx_pydot import write_dot
|
||||
|
||||
_, edges = self.__build_graph(target, commands, outs)
|
||||
edges = [edge[::-1] for edge in edges]
|
||||
|
||||
simple_g = networkx.DiGraph()
|
||||
simple_g.add_edges_from(edges)
|
||||
write_dot(simple_g, filename)
|
||||
|
||||
def run(self, unlock=False):
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
self._show(target, self.args.commands, self.args.outs)
|
||||
if self.args.ascii:
|
||||
self._show_ascii(target,
|
||||
self.args.commands,
|
||||
self.args.outs)
|
||||
elif self.args.dot:
|
||||
self.__write_dot(target,
|
||||
self.args.commands,
|
||||
self.args.outs,
|
||||
self.args.dot)
|
||||
else:
|
||||
self._show(target,
|
||||
self.args.commands,
|
||||
self.args.outs)
|
||||
except DvcException as ex:
|
||||
msg = 'Failed to show pipeline for \'{}\''.format(target)
|
||||
self.project.logger.error(msg, ex)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
class CmdPipelineList(CmdBase):
|
||||
def run(self):
|
||||
import networkx
|
||||
|
||||
pipelines = self.project.pipelines()
|
||||
for p in pipelines:
|
||||
stages = networkx.get_node_attributes(p, 'stage')
|
||||
for stage in stages:
|
||||
self.project.logger.info(stage)
|
||||
if len(stages) != 0:
|
||||
self.project.logger.info("="*80)
|
||||
self.project.logger.info("{} pipeline(s) total".format(len(pipelines)))
|
||||
|
||||
return 0
|
||||
|
||||
+29
-1
@@ -1,4 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
import configobj
|
||||
|
||||
from dvc.config import Config
|
||||
from dvc.command.config import CmdConfig
|
||||
@@ -23,9 +25,35 @@ class CmdRemoteAdd(CmdConfig):
|
||||
|
||||
|
||||
class CmdRemoteRemove(CmdConfig):
|
||||
def _remove_default(self, config_file, remote):
|
||||
path = os.path.join(os.path.dirname(self.config_file),
|
||||
config_file)
|
||||
config = configobj.ConfigObj(path)
|
||||
|
||||
core = config.get(Config.SECTION_CORE, None)
|
||||
if core is None:
|
||||
return
|
||||
|
||||
default = core.get(Config.SECTION_CORE_REMOTE, None)
|
||||
if default is None:
|
||||
return
|
||||
|
||||
if default == remote:
|
||||
del config[Config.SECTION_CORE][Config.SECTION_CORE_REMOTE]
|
||||
if len(config[Config.SECTION_CORE]) == 0:
|
||||
del config[Config.SECTION_CORE]
|
||||
|
||||
config.write()
|
||||
|
||||
def run(self):
|
||||
section = Config.SECTION_REMOTE_FMT.format(self.args.name)
|
||||
return self.unset(section, Config.SECTION_REMOTE_URL)
|
||||
ret = self.unset(section)
|
||||
if ret != 0:
|
||||
return ret
|
||||
|
||||
self._remove_default(Config.CONFIG, self.args.name)
|
||||
self._remove_default(Config.CONFIG_LOCAL, self.args.name)
|
||||
return 0
|
||||
|
||||
|
||||
class CmdRemoteModify(CmdConfig):
|
||||
|
||||
+23
-1
@@ -4,10 +4,32 @@ from dvc.logger import Logger
|
||||
|
||||
|
||||
class CmdRemove(CmdBase):
|
||||
def _is_outs_only(self, target):
|
||||
if not self.args.purge:
|
||||
return True
|
||||
|
||||
if self.args.force:
|
||||
return False
|
||||
|
||||
msg = (
|
||||
u'Are you sure you want to remove {} with its outputs?'
|
||||
.format(target)
|
||||
)
|
||||
|
||||
confirmed = self.project.prompt.prompt(msg)
|
||||
|
||||
if confirmed:
|
||||
return False
|
||||
|
||||
raise DvcException(
|
||||
u'Cannot purge without a confirmation from the user.'
|
||||
u" Use '-f' to force."
|
||||
)
|
||||
|
||||
def run(self):
|
||||
outs_only = not self.args.purge
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
outs_only = self._is_outs_only(target)
|
||||
self.project.remove(target, outs_only=outs_only)
|
||||
except DvcException as ex:
|
||||
Logger.error('Failed to remove {}'.format(target), ex)
|
||||
|
||||
+14
-10
@@ -1,12 +1,12 @@
|
||||
import os
|
||||
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command.status import CmdDataStatus
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class CmdRepro(CmdBase):
|
||||
def run(self):
|
||||
from dvc.project import ReproductionError
|
||||
|
||||
recursive = not self.args.single_item
|
||||
saved_dir = os.path.realpath(os.curdir)
|
||||
if self.args.cwd:
|
||||
@@ -15,21 +15,25 @@ class CmdRepro(CmdBase):
|
||||
ret = 0
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
self.project.reproduce(target,
|
||||
stages = self.project.reproduce(
|
||||
target,
|
||||
recursive=recursive,
|
||||
force=self.args.force)
|
||||
force=self.args.force,
|
||||
dry=self.args.dry,
|
||||
interactive=self.args.interactive,
|
||||
pipeline=self.args.pipeline,
|
||||
all_pipelines=self.args.all_pipelines)
|
||||
|
||||
if len(stages) == 0:
|
||||
self.project.logger.info(CmdDataStatus.UP_TO_DATE_MSG)
|
||||
|
||||
if self.args.metrics:
|
||||
self.project.metrics_show()
|
||||
except ReproductionError as ex:
|
||||
except DvcException as ex:
|
||||
msg = 'Failed to reproduce \'{}\''.format(target)
|
||||
self.project.logger.error(msg, ex)
|
||||
ret = 1
|
||||
break
|
||||
except Exception as ex:
|
||||
msg = 'Failed to reproduce \'{}\' - unexpected error'
|
||||
self.project.logger.error(msg.format(target), ex)
|
||||
ret = 1
|
||||
break
|
||||
|
||||
os.chdir(saved_dir)
|
||||
return ret
|
||||
|
||||
@@ -4,6 +4,9 @@ from dvc.command.base import CmdBase
|
||||
|
||||
|
||||
class CmdRoot(CmdBase):
|
||||
def run_cmd(self):
|
||||
return self.run()
|
||||
|
||||
def run(self):
|
||||
self.project.logger.info(os.path.relpath(self.project.root_dir))
|
||||
return 0
|
||||
|
||||
@@ -5,6 +5,9 @@ from dvc.exceptions import DvcException
|
||||
class CmdRun(CmdBase):
|
||||
def run(self):
|
||||
try:
|
||||
if self.args.yes:
|
||||
self.project.prompt.default = True
|
||||
|
||||
self.project.run(cmd=' '.join(self.args.command),
|
||||
outs=self.args.outs,
|
||||
outs_no_cache=self.args.outs_no_cache,
|
||||
|
||||
+14
-5
@@ -4,6 +4,7 @@ from dvc.command.data_sync import CmdDataBase
|
||||
class CmdDataStatus(CmdDataBase):
|
||||
STATUS_LEN = 10
|
||||
STATUS_INDENT = '\t'
|
||||
UP_TO_DATE_MSG = "Pipeline is up to date. Nothing to reproduce."
|
||||
|
||||
def _normalize(self, s):
|
||||
s += ':'
|
||||
@@ -24,11 +25,19 @@ class CmdDataStatus(CmdDataBase):
|
||||
def do_run(self, target=None):
|
||||
indent = 1 if self.args.cloud else 0
|
||||
try:
|
||||
status = self.project.status(target=target,
|
||||
jobs=self.args.jobs,
|
||||
cloud=self.args.cloud,
|
||||
remote=self.args.remote)
|
||||
self._show(status, indent)
|
||||
st = self.project.status(target=target,
|
||||
jobs=self.args.jobs,
|
||||
cloud=self.args.cloud,
|
||||
show_checksums=self.args.show_checksums,
|
||||
remote=self.args.remote,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
with_deps=self.args.with_deps)
|
||||
if st:
|
||||
self._show(st, indent)
|
||||
else:
|
||||
self.project.logger.info(self.UP_TO_DATE_MSG)
|
||||
|
||||
except Exception as exc:
|
||||
self.project.logger.error('Failed to obtain data status', exc)
|
||||
return 1
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.command.base import CmdBase
|
||||
|
||||
|
||||
class CmdUnprotect(CmdBase):
|
||||
def run(self):
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
self.project.unprotect(target)
|
||||
except DvcException as ex:
|
||||
msg = "Failed to unprotect '{}'".format(target)
|
||||
self.project.logger.error(msg, ex)
|
||||
return 1
|
||||
return 0
|
||||
+66
-11
@@ -10,8 +10,9 @@ from dvc.exceptions import DvcException
|
||||
|
||||
class ConfigError(DvcException):
|
||||
""" DVC config exception """
|
||||
def __init__(self, ex=None):
|
||||
super(ConfigError, self).__init__('Config file error', ex)
|
||||
def __init__(self, msg, ex=None):
|
||||
super(ConfigError, self).__init__('Config file error: {}'.format(msg),
|
||||
ex)
|
||||
|
||||
|
||||
def supported_url(url):
|
||||
@@ -36,6 +37,22 @@ def supported_cloud(cloud):
|
||||
return cloud in ['aws', 'gcp', 'local', '']
|
||||
|
||||
|
||||
def is_bool(val):
|
||||
return val.lower() in ['true', 'false']
|
||||
|
||||
|
||||
def to_bool(val):
|
||||
return val.lower() == 'true'
|
||||
|
||||
|
||||
def is_whole(val):
|
||||
return int(val) >= 0
|
||||
|
||||
|
||||
def is_percent(val):
|
||||
return int(val) >= 0 and int(val) <= 100
|
||||
|
||||
|
||||
class Config(object):
|
||||
CONFIG = 'config'
|
||||
CONFIG_LOCAL = 'config.local'
|
||||
@@ -44,11 +61,14 @@ class Config(object):
|
||||
SECTION_CORE_LOGLEVEL = 'loglevel'
|
||||
SECTION_CORE_LOGLEVEL_SCHEMA = And(Use(str.lower), supported_loglevel)
|
||||
SECTION_CORE_REMOTE = 'remote'
|
||||
SECTION_CORE_INTERACTIVE_SCHEMA = And(str, is_bool, Use(to_bool))
|
||||
SECTION_CORE_INTERACTIVE = 'interactive'
|
||||
|
||||
SECTION_CACHE = 'cache'
|
||||
SECTION_CACHE_DIR = 'dir'
|
||||
SECTION_CACHE_TYPE = 'type'
|
||||
SECTION_CACHE_TYPE_SCHEMA = supported_cache_type
|
||||
SECTION_CACHE_PROTECTED = 'protected'
|
||||
SECTION_CACHE_LOCAL = 'local'
|
||||
SECTION_CACHE_S3 = 's3'
|
||||
SECTION_CACHE_GS = 'gs'
|
||||
@@ -63,9 +83,10 @@ class Config(object):
|
||||
Optional(SECTION_CACHE_SSH): str,
|
||||
Optional(SECTION_CACHE_AZURE): str,
|
||||
|
||||
# backward compatibility
|
||||
Optional(SECTION_CACHE_DIR, default='cache'): str,
|
||||
Optional(SECTION_CACHE_TYPE, default=None): SECTION_CACHE_TYPE_SCHEMA,
|
||||
Optional(SECTION_CACHE_PROTECTED,
|
||||
default=False): And(str, is_bool, Use(to_bool)),
|
||||
}
|
||||
|
||||
# backward compatibility
|
||||
@@ -78,6 +99,8 @@ class Config(object):
|
||||
default='info'): And(str, Use(str.lower),
|
||||
SECTION_CORE_LOGLEVEL_SCHEMA),
|
||||
Optional(SECTION_CORE_REMOTE, default=''): And(str, Use(str.lower)),
|
||||
Optional(SECTION_CORE_INTERACTIVE,
|
||||
default=False): SECTION_CORE_INTERACTIVE_SCHEMA,
|
||||
|
||||
# backward compatibility
|
||||
Optional(SECTION_CORE_CLOUD, default=''): SECTION_CORE_CLOUD_SCHEMA,
|
||||
@@ -94,9 +117,9 @@ class Config(object):
|
||||
SECTION_AWS_SCHEMA = {
|
||||
SECTION_AWS_STORAGEPATH: str,
|
||||
Optional(SECTION_AWS_REGION): str,
|
||||
Optional(SECTION_AWS_PROFILE, default='default'): str,
|
||||
Optional(SECTION_AWS_CREDENTIALPATH, default=''): str,
|
||||
Optional(SECTION_AWS_ENDPOINT_URL, default=None): str,
|
||||
Optional(SECTION_AWS_PROFILE): str,
|
||||
Optional(SECTION_AWS_CREDENTIALPATH): str,
|
||||
Optional(SECTION_AWS_ENDPOINT_URL): str,
|
||||
}
|
||||
|
||||
# backward compatibility
|
||||
@@ -119,21 +142,42 @@ class Config(object):
|
||||
SECTION_REMOTE_FMT = 'remote "{}"'
|
||||
SECTION_REMOTE_URL = 'url'
|
||||
SECTION_REMOTE_USER = 'user'
|
||||
SECTION_REMOTE_PORT = 'port'
|
||||
SECTION_REMOTE_KEY_FILE = 'keyfile'
|
||||
SECTION_REMOTE_TIMEOUT = 'timeout'
|
||||
SECTION_REMOTE_PASSWORD = 'password'
|
||||
SECTION_REMOTE_ASK_PASSWORD = 'ask_password'
|
||||
SECTION_REMOTE_SCHEMA = {
|
||||
SECTION_REMOTE_URL: supported_url,
|
||||
SECTION_REMOTE_URL: And(supported_url, error="Unsupported URL"),
|
||||
Optional(SECTION_AWS_REGION): str,
|
||||
Optional(SECTION_AWS_PROFILE, default='default'): str,
|
||||
Optional(SECTION_AWS_CREDENTIALPATH, default=''): str,
|
||||
Optional(SECTION_AWS_ENDPOINT_URL, default=None): str,
|
||||
Optional(SECTION_AWS_PROFILE): str,
|
||||
Optional(SECTION_AWS_CREDENTIALPATH): str,
|
||||
Optional(SECTION_AWS_ENDPOINT_URL): str,
|
||||
Optional(SECTION_GCP_PROJECTNAME): str,
|
||||
Optional(SECTION_CACHE_TYPE): SECTION_CACHE_TYPE_SCHEMA,
|
||||
Optional(SECTION_CACHE_PROTECTED,
|
||||
default=False): And(str, is_bool, Use(to_bool)),
|
||||
Optional(SECTION_REMOTE_USER): str,
|
||||
Optional(SECTION_REMOTE_PORT): Use(int),
|
||||
Optional(SECTION_REMOTE_KEY_FILE): str,
|
||||
Optional(SECTION_REMOTE_TIMEOUT): Use(int),
|
||||
Optional(SECTION_REMOTE_PASSWORD): str,
|
||||
Optional(SECTION_REMOTE_ASK_PASSWORD): And(str, is_bool, Use(to_bool)),
|
||||
}
|
||||
|
||||
SECTION_STATE = 'state'
|
||||
SECTION_STATE_ROW_LIMIT = 'row_limit'
|
||||
SECTION_STATE_ROW_CLEANUP_QUOTA = 'row_cleanup_quota'
|
||||
SECTION_STATE_SCHEMA = {
|
||||
Optional(SECTION_STATE_ROW_LIMIT): And(Use(int), is_whole),
|
||||
Optional(SECTION_STATE_ROW_CLEANUP_QUOTA): And(Use(int), is_percent),
|
||||
}
|
||||
|
||||
SCHEMA = {
|
||||
Optional(SECTION_CORE, default={}): SECTION_CORE_SCHEMA,
|
||||
Optional(Regex(SECTION_REMOTE_REGEX)): SECTION_REMOTE_SCHEMA,
|
||||
Optional(SECTION_CACHE, default={}): SECTION_CACHE_SCHEMA,
|
||||
Optional(SECTION_STATE, default={}): SECTION_STATE_SCHEMA,
|
||||
|
||||
# backward compatibility
|
||||
Optional(SECTION_AWS, default={}): SECTION_AWS_SCHEMA,
|
||||
@@ -154,7 +198,7 @@ class Config(object):
|
||||
# need to convert our config to dict before passing it to
|
||||
self._config = self._lower(self._config)
|
||||
local = self._lower(local)
|
||||
self._config.update(local)
|
||||
self._config = self._merge(self._config, local)
|
||||
|
||||
self._config = Schema(self.SCHEMA).validate(self._config)
|
||||
|
||||
@@ -165,6 +209,17 @@ class Config(object):
|
||||
except Exception as ex:
|
||||
raise ConfigError(ex)
|
||||
|
||||
@staticmethod
|
||||
def _merge(first, second):
|
||||
res = {}
|
||||
sections = list(first.keys()) + list(second.keys())
|
||||
for section in sections:
|
||||
f = first.get(section, {}).copy()
|
||||
s = second.get(section, {}).copy()
|
||||
f.update(s)
|
||||
res[section] = f
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _lower(config):
|
||||
new_config = {}
|
||||
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
import sys
|
||||
import math
|
||||
import select
|
||||
|
||||
from grandalf.graphs import Vertex, Edge, Graph
|
||||
from grandalf.layouts import SugiyamaLayout
|
||||
from grandalf.routing import route_with_lines, EdgeViewer
|
||||
|
||||
|
||||
class AsciiCanvas(object):
|
||||
def __init__(self, cols, lines):
|
||||
assert cols > 1
|
||||
assert lines > 1
|
||||
|
||||
self.cols = cols
|
||||
self.lines = lines
|
||||
|
||||
self.canvas = [[' '] * cols for l in range(lines)]
|
||||
|
||||
def draw(self):
|
||||
if sys.stdout.isatty(): # pragma: no cover
|
||||
from asciimatics.screen import Screen
|
||||
Screen.wrapper(self._do_draw)
|
||||
else:
|
||||
for line in self.canvas:
|
||||
print(''.join(line))
|
||||
|
||||
def _do_draw(self, screen): # pragma: no cover
|
||||
from asciimatics.event import KeyboardEvent
|
||||
|
||||
offset_x = 0
|
||||
offset_y = 0
|
||||
smaxrow, smaxcol = screen.dimensions
|
||||
assert smaxrow > 1
|
||||
assert smaxcol > 1
|
||||
smaxrow -= 1
|
||||
smaxcol -= 1
|
||||
|
||||
if self.lines + 1 > smaxrow:
|
||||
max_y = self.lines + 1 - smaxrow
|
||||
else:
|
||||
max_y = 0
|
||||
|
||||
if self.cols + 1 > smaxcol:
|
||||
max_x = self.cols + 1 - smaxcol
|
||||
else:
|
||||
max_x = 0
|
||||
|
||||
while True:
|
||||
for y in range(smaxrow + 1):
|
||||
y_index = offset_y + y
|
||||
line = []
|
||||
for x in range(smaxcol + 1):
|
||||
x_index = offset_x + x
|
||||
if len(self.canvas) > y_index \
|
||||
and len(self.canvas[y_index]) > x_index:
|
||||
line.append(self.canvas[y_index][x_index])
|
||||
else:
|
||||
line.append(' ')
|
||||
assert len(line) == (smaxcol + 1)
|
||||
screen.print_at(''.join(line), 0, y)
|
||||
|
||||
screen.refresh()
|
||||
|
||||
# NOTE: get_event() doesn't block by itself,
|
||||
# so we have to do the blocking ourselves.
|
||||
select.select([sys.stdin], [], [], None)
|
||||
|
||||
event = screen.get_event()
|
||||
if not isinstance(event, KeyboardEvent):
|
||||
continue
|
||||
|
||||
k = event.key_code
|
||||
if k == screen.KEY_DOWN or k == ord('s'):
|
||||
offset_y += 1
|
||||
elif k == screen.KEY_PAGE_DOWN or k == ord('S'):
|
||||
offset_y += smaxrow
|
||||
elif k == screen.KEY_UP or k == ord('w'):
|
||||
offset_y -= 1
|
||||
elif k == screen.KEY_PAGE_UP or k == ord('W'):
|
||||
offset_y -= smaxrow
|
||||
elif k == screen.KEY_RIGHT or k == ord('d'):
|
||||
offset_x += 1
|
||||
elif k == ord('D'):
|
||||
offset_x += smaxcol
|
||||
elif k == screen.KEY_LEFT or k == ord('a'):
|
||||
offset_x -= 1
|
||||
elif k == ord('A'):
|
||||
offset_x -= smaxcol
|
||||
elif k == ord('q') or k == ord('Q'):
|
||||
break
|
||||
|
||||
if offset_y > max_y:
|
||||
offset_y = max_y
|
||||
elif offset_y < 0:
|
||||
offset_y = 0
|
||||
|
||||
if offset_x > max_x:
|
||||
offset_x = max_x
|
||||
elif offset_x < 0:
|
||||
offset_x = 0
|
||||
|
||||
def point(self, x, y, char):
|
||||
assert x >= 0
|
||||
assert x < self.cols
|
||||
assert y >= 0
|
||||
assert y < self.lines
|
||||
|
||||
self.canvas[y][x] = char
|
||||
|
||||
def line(self, x0, y0, x1, y1, char):
|
||||
if x0 > x1:
|
||||
x1, x0 = x0, x1
|
||||
y1, y0 = y0, y1
|
||||
|
||||
dx = x1 - x0
|
||||
dy = y1 - y0
|
||||
|
||||
if dx == 0 and dy == 0:
|
||||
self.point(x0, y0, char)
|
||||
elif abs(dx) >= abs(dy):
|
||||
for x in range(x0, x1 + 1):
|
||||
if dx == 0:
|
||||
y = y0
|
||||
else:
|
||||
y = y0 + int(round((x - x0) * dy / float((dx))))
|
||||
self.point(x, y, char)
|
||||
elif y0 < y1:
|
||||
for y in range(y0, y1 + 1):
|
||||
if dy == 0:
|
||||
x = x0
|
||||
else:
|
||||
x = x0 + int(round((y - y0) * dx / float((dy))))
|
||||
self.point(x, y, char)
|
||||
else:
|
||||
for y in range(y1, y0 + 1):
|
||||
if dy == 0:
|
||||
x = x0
|
||||
else:
|
||||
x = x1 + int(round((y - y1) * dx / float((dy))))
|
||||
self.point(x, y, char)
|
||||
|
||||
def text(self, x, y, text):
|
||||
for i, c in enumerate(text):
|
||||
self.point(x + i, y, c)
|
||||
|
||||
def box(self, x0, y0, w, h):
|
||||
assert w > 1
|
||||
assert h > 1
|
||||
|
||||
w -= 1
|
||||
h -= 1
|
||||
|
||||
for x in range(x0, x0 + w):
|
||||
self.point(x, y0, '-')
|
||||
self.point(x, y0 + h, '-')
|
||||
|
||||
for y in range(y0, y0 + h):
|
||||
self.point(x0, y, '|')
|
||||
self.point(x0 + w, y, '|')
|
||||
|
||||
self.point(x0, y0, '+')
|
||||
self.point(x0 + w, y0, '+')
|
||||
self.point(x0, y0 + h, '+')
|
||||
self.point(x0 + w, y0 + h, '+')
|
||||
|
||||
|
||||
class Dagascii(object):
|
||||
def __init__(self, vertexes, edges):
|
||||
#
|
||||
# Just a reminder about naming conventions:
|
||||
# +------------X
|
||||
# |
|
||||
# |
|
||||
# |
|
||||
# |
|
||||
# Y
|
||||
#
|
||||
|
||||
V = {v: Vertex(" {} ".format(v)) for v in vertexes}
|
||||
# NOTE: reverting edges to correctly orientate the graph
|
||||
E = [Edge(V[e], V[s]) for s, e in edges]
|
||||
V = V.values()
|
||||
g = Graph(V, E)
|
||||
|
||||
class VertexViewer(object):
|
||||
h = 3 # top and bottom box edges + text
|
||||
|
||||
def __init__(self, name):
|
||||
self.w = len(name) + 2 # right and left bottom edges + text
|
||||
|
||||
for v in V:
|
||||
v.view = VertexViewer(v.data)
|
||||
|
||||
# NOTE: determine min box length to create the best layout
|
||||
minw = min([v.view.w for v in V])
|
||||
|
||||
for e in E:
|
||||
e.view = EdgeViewer()
|
||||
|
||||
sug = SugiyamaLayout(g.C[0])
|
||||
gr = g.C[0]
|
||||
r = list(filter(lambda x: len(x.e_in()) == 0, gr.sV))
|
||||
|
||||
sug.init_all(roots=r, optimize=True)
|
||||
|
||||
sug.yspace = VertexViewer.h
|
||||
sug.xspace = minw
|
||||
sug.route_edge = route_with_lines
|
||||
|
||||
sug.draw()
|
||||
|
||||
self.sug = sug
|
||||
|
||||
def draw(self):
|
||||
# NOTE: coordinates might me negative, so we need to shift
|
||||
# everything to the positive plane before we actually draw it.
|
||||
Xs = []
|
||||
Ys = []
|
||||
|
||||
for v in self.sug.g.sV:
|
||||
# NOTE: moving boxes w/2 to the left
|
||||
Xs.append(v.view.xy[0] - v.view.w/2.0)
|
||||
Xs.append(v.view.xy[0] + v.view.w/2.0)
|
||||
Ys.append(v.view.xy[1])
|
||||
Ys.append(v.view.xy[1] + v.view.h)
|
||||
|
||||
for e in self.sug.g.sE:
|
||||
for xy in e.view._pts:
|
||||
Xs.append(xy[0])
|
||||
Ys.append(xy[1])
|
||||
|
||||
minx = min(Xs)
|
||||
miny = min(Ys)
|
||||
maxx = max(Xs)
|
||||
maxy = max(Ys)
|
||||
|
||||
canvas_cols = int(math.ceil(math.ceil(maxx) - math.floor(minx))) + 1
|
||||
canvas_lines = int(round(maxy - miny))
|
||||
|
||||
canvas = AsciiCanvas(canvas_cols, canvas_lines)
|
||||
|
||||
# NOTE: first draw edges so that node boxes could overwrite them
|
||||
for e in self.sug.g.sE:
|
||||
assert len(e.view._pts) > 1
|
||||
for index in range(1, len(e.view._pts)):
|
||||
start = e.view._pts[index - 1]
|
||||
end = e.view._pts[index]
|
||||
|
||||
start_x = int(round(start[0] - minx))
|
||||
start_y = int(round(start[1] - miny))
|
||||
end_x = int(round(end[0] - minx))
|
||||
end_y = int(round(end[1] - miny))
|
||||
|
||||
assert start_x >= 0
|
||||
assert start_y >= 0
|
||||
assert end_x >= 0
|
||||
assert end_y >= 0
|
||||
|
||||
canvas.line(start_x, start_y,
|
||||
end_x, end_y,
|
||||
'*')
|
||||
|
||||
for v in self.sug.g.sV:
|
||||
# NOTE: moving boxes w/2 to the left
|
||||
x = v.view.xy[0] - v.view.w/2.0
|
||||
y = v.view.xy[1]
|
||||
|
||||
canvas.box(int(round(x - minx)),
|
||||
int(round(y - miny)),
|
||||
v.view.w,
|
||||
v.view.h)
|
||||
|
||||
canvas.text(int(round(x - minx)) + 1,
|
||||
int(round(y - miny)) + 1,
|
||||
v.data)
|
||||
|
||||
canvas.draw()
|
||||
+23
-9
@@ -73,6 +73,11 @@ class DataCloud(object):
|
||||
msg = "Can't find cloud section '{}' in config"
|
||||
raise ConfigError(msg.format(name))
|
||||
|
||||
# NOTE: check if the class itself has everything needed for operation.
|
||||
# E.g. all the imported packages.
|
||||
if not cloud_type.supported(cloud_type.compat_config(cloud_config)):
|
||||
raise ConfigError("Unsupported cloud '{}'".format(name))
|
||||
|
||||
return self._init_cloud(cloud_config, cloud_type)
|
||||
|
||||
def _init_cloud(self, cloud_config, cloud_type):
|
||||
@@ -83,37 +88,46 @@ class DataCloud(object):
|
||||
cloud = cloud_type(self.project, cloud_config)
|
||||
return cloud
|
||||
|
||||
def _get_cloud(self, remote):
|
||||
def _get_cloud(self, remote, cmd):
|
||||
if remote:
|
||||
return self._init_remote(remote)
|
||||
|
||||
if self._cloud:
|
||||
return self._cloud
|
||||
|
||||
msg = "No remote repository specified. Setup default repository " \
|
||||
"with 'dvc config core.remote <name>' or use '-r <name>'."
|
||||
msg = "No remote repository specified. Setup default " \
|
||||
"repository with\n"\
|
||||
" dvc config core.remote <name>\n" \
|
||||
"or use:\n" \
|
||||
" dvc {} -r <name>".format(cmd)
|
||||
raise ConfigError(msg)
|
||||
|
||||
def push(self, targets, jobs=1, remote=None):
|
||||
def push(self, targets, jobs=1, remote=None, show_checksums=False):
|
||||
"""
|
||||
Push data items in a cloud-agnostic way.
|
||||
"""
|
||||
return self.project.cache.local.push(targets,
|
||||
jobs=jobs,
|
||||
remote=self._get_cloud(remote))
|
||||
remote=self._get_cloud(remote,
|
||||
'push'),
|
||||
show_checksums=show_checksums)
|
||||
|
||||
def pull(self, targets, jobs=1, remote=None):
|
||||
def pull(self, targets, jobs=1, remote=None, show_checksums=False):
|
||||
"""
|
||||
Pull data items in a cloud-agnostic way.
|
||||
"""
|
||||
return self.project.cache.local.pull(targets,
|
||||
jobs=jobs,
|
||||
remote=self._get_cloud(remote))
|
||||
remote=self._get_cloud(remote,
|
||||
'pull'),
|
||||
show_checksums=show_checksums)
|
||||
|
||||
def status(self, targets, jobs=1, remote=None):
|
||||
def status(self, targets, jobs=1, remote=None, show_checksums=False):
|
||||
"""
|
||||
Check status of data items in a cloud-agnostic way.
|
||||
"""
|
||||
cloud = self._get_cloud(remote, 'status')
|
||||
return self.project.cache.local.status(targets,
|
||||
jobs=jobs,
|
||||
remote=self._get_cloud(remote))
|
||||
remote=cloud,
|
||||
show_checksums=show_checksums)
|
||||
|
||||
@@ -31,6 +31,9 @@ class DependencyBase(object):
|
||||
self.project = stage.project
|
||||
self.path = path
|
||||
|
||||
def __str__(self):
|
||||
return self.path
|
||||
|
||||
@classmethod
|
||||
def match(cls, url):
|
||||
return re.match(cls.REGEX, url)
|
||||
@@ -51,7 +54,7 @@ class DependencyBase(object):
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
return self.remote.exists([self.path_info])
|
||||
return self.remote.exists([self.path_info])[0]
|
||||
|
||||
def changed(self):
|
||||
raise NotImplementedError
|
||||
@@ -59,7 +62,11 @@ class DependencyBase(object):
|
||||
def status(self):
|
||||
if self.changed():
|
||||
# FIXME better msgs
|
||||
return {self.rel_path: 'changed'}
|
||||
if self.path_info['scheme'] == 'local':
|
||||
p = self.rel_path
|
||||
else:
|
||||
p = self.path
|
||||
return {p: 'changed'}
|
||||
return {}
|
||||
|
||||
def save(self):
|
||||
|
||||
+14
-4
@@ -13,7 +13,7 @@ from dvc.remote.local import RemoteLOCAL
|
||||
|
||||
|
||||
class DependencyLOCAL(DependencyBase):
|
||||
REGEX = r'^(?P<path>(/+|.:\\+)?.*)$'
|
||||
REGEX = r'^(?P<path>(/+|.:\\+)?[^:]*)$'
|
||||
|
||||
DoesNotExistError = DependencyDoesNotExistError
|
||||
IsNotFileOrDirError = DependencyIsNotFileOrDirError
|
||||
@@ -38,6 +38,15 @@ class DependencyLOCAL(DependencyBase):
|
||||
self.path_info = {'scheme': 'local',
|
||||
'path': self.path}
|
||||
|
||||
def __str__(self):
|
||||
return self.rel_path
|
||||
|
||||
@property
|
||||
def is_local(self):
|
||||
assert os.path.isabs(self.path)
|
||||
assert os.path.isabs(self.project.root_dir)
|
||||
return self.path.startswith(self.project.root_dir)
|
||||
|
||||
@property
|
||||
def sep(self):
|
||||
return os.sep
|
||||
@@ -58,7 +67,8 @@ class DependencyLOCAL(DependencyBase):
|
||||
if not self.exists:
|
||||
raise self.DoesNotExistError(self.rel_path)
|
||||
|
||||
if not os.path.isfile(self.path) and not os.path.isdir(self.path):
|
||||
if not os.path.isfile(self.path) \
|
||||
and not os.path.isdir(self.path): # pragma: no cover
|
||||
raise self.IsNotFileOrDirError(self.rel_path)
|
||||
|
||||
if (os.path.isfile(self.path) and os.path.getsize(self.path) == 0) or \
|
||||
@@ -68,12 +78,12 @@ class DependencyLOCAL(DependencyBase):
|
||||
self.info = self.remote.save_info(self.path_info)
|
||||
|
||||
def dumpd(self):
|
||||
if self.path.startswith(self.stage.project.root_dir):
|
||||
if self.is_local:
|
||||
path = self.remote.unixpath(os.path.relpath(self.path,
|
||||
self.stage.cwd))
|
||||
else:
|
||||
path = self.path
|
||||
|
||||
info = self.info
|
||||
info = self.info.copy()
|
||||
info[self.PARAM_PATH] = path
|
||||
return info
|
||||
|
||||
@@ -18,6 +18,7 @@ class DependencySSH(DependencyBase):
|
||||
self.remote = remote if remote else RemoteSSH(stage.project, {})
|
||||
|
||||
host = remote.host if remote else self.group('host')
|
||||
port = remote.port if remote else RemoteSSH.DEFAULT_PORT
|
||||
user = remote.user if remote else self.group('user')
|
||||
if remote:
|
||||
path = posixpath.join(remote.prefix,
|
||||
@@ -27,6 +28,7 @@ class DependencySSH(DependencyBase):
|
||||
|
||||
self.path_info = {'scheme': 'ssh',
|
||||
'host': host,
|
||||
'port': port,
|
||||
'user': user,
|
||||
'path': path}
|
||||
|
||||
|
||||
+22
-4
@@ -8,7 +8,7 @@ class DvcException(Exception):
|
||||
if cause:
|
||||
try:
|
||||
self.cause_tb = traceback.format_exc()
|
||||
except Exception:
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
super(DvcException, self).__init__(msg)
|
||||
|
||||
@@ -24,6 +24,24 @@ class OutputDuplicationError(DvcException):
|
||||
assert isinstance(output, str)
|
||||
assert isinstance(stages, list)
|
||||
assert all(isinstance(stage, str) for stage in stages)
|
||||
msg = "File '{}' is specified as an output in more than one stage: {}"
|
||||
super(OutputDuplicationError, self).__init__(msg.format(output,
|
||||
str(stages)))
|
||||
msg = "File '{}' is specified as an output in more than one stage:{}"
|
||||
s = ""
|
||||
for stage in stages:
|
||||
s += "\n " + stage
|
||||
super(OutputDuplicationError, self).__init__(msg.format(output, s))
|
||||
|
||||
|
||||
class CircularDependencyError(DvcException):
|
||||
def __init__(self, dependency):
|
||||
assert isinstance(dependency, str)
|
||||
msg = "File '{}' is specified as an output and as a dependency."
|
||||
super(CircularDependencyError, self).__init__(msg.format(dependency))
|
||||
|
||||
|
||||
class MoveNotDataSourceError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = "Move is not permitted for stages that are not data sources. " \
|
||||
"You need to either move '{path}' to a new location and edit " \
|
||||
"it by hand, or remove '{path}' and create a new one at the " \
|
||||
"desired location."
|
||||
super(MoveNotDataSourceError, self).__init__(msg.format(path=path))
|
||||
|
||||
@@ -17,10 +17,6 @@ class Lock(object):
|
||||
self.lock_file = os.path.join(dvc_dir, name)
|
||||
self._lock = None
|
||||
|
||||
@staticmethod
|
||||
def init(dvc_dir):
|
||||
return Lock(dvc_dir)
|
||||
|
||||
def _do_lock(self):
|
||||
try:
|
||||
self._lock = zc.lockfile.LockFile(self.lock_file)
|
||||
|
||||
+147
-20
@@ -2,11 +2,28 @@ import sys
|
||||
import logging
|
||||
import colorama
|
||||
import traceback
|
||||
import re
|
||||
|
||||
|
||||
colorama.init()
|
||||
|
||||
|
||||
def visual_width(line):
|
||||
""" Get the the number of columns required to display a string """
|
||||
|
||||
return len(re.sub(colorama.ansitowin32.AnsiToWin32.ANSI_CSI_RE, '', line))
|
||||
|
||||
|
||||
def visual_center(line, width):
|
||||
""" Center align string according to it's visual width """
|
||||
|
||||
spaces = max(width - visual_width(line), 0)
|
||||
left_padding = int(spaces / 2)
|
||||
right_padding = (spaces - left_padding)
|
||||
|
||||
return (left_padding * ' ') + line + (right_padding * ' ')
|
||||
|
||||
|
||||
class Logger(object):
|
||||
FMT = '%(message)s'
|
||||
DEFAULT_LEVEL = logging.INFO
|
||||
@@ -19,9 +36,16 @@ class Logger(object):
|
||||
}
|
||||
|
||||
COLOR_MAP = {
|
||||
'debug': colorama.Fore.BLUE,
|
||||
'warn': colorama.Fore.YELLOW,
|
||||
'error': colorama.Fore.RED
|
||||
'green': colorama.Fore.GREEN,
|
||||
'yellow': colorama.Fore.YELLOW,
|
||||
'blue': colorama.Fore.BLUE,
|
||||
'red': colorama.Fore.RED,
|
||||
}
|
||||
|
||||
LEVEL_COLOR_MAP = {
|
||||
'debug': 'blue',
|
||||
'warn': 'yellow',
|
||||
'error': 'red',
|
||||
}
|
||||
|
||||
def __init__(self, loglevel=None):
|
||||
@@ -30,11 +54,22 @@ class Logger(object):
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(logging.Formatter(Logger.FMT))
|
||||
sh.setLevel(logging.DEBUG)
|
||||
|
||||
Logger.logger().addHandler(sh)
|
||||
class LogLevelFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
return record.levelno <= logging.WARNING
|
||||
|
||||
sh_out = logging.StreamHandler(sys.stdout)
|
||||
sh_out.setFormatter(logging.Formatter(Logger.FMT))
|
||||
sh_out.setLevel(logging.DEBUG)
|
||||
sh_out.addFilter(LogLevelFilter())
|
||||
|
||||
sh_err = logging.StreamHandler(sys.stderr)
|
||||
sh_err.setFormatter(logging.Formatter(Logger.FMT))
|
||||
sh_err.setLevel(logging.ERROR)
|
||||
|
||||
Logger.logger().addHandler(sh_out)
|
||||
Logger.logger().addHandler(sh_err)
|
||||
Logger.set_level()
|
||||
|
||||
@staticmethod
|
||||
@@ -58,12 +93,12 @@ class Logger(object):
|
||||
Logger.logger().setLevel(logging.DEBUG)
|
||||
|
||||
@staticmethod
|
||||
def colorize(msg, typ):
|
||||
def colorize(msg, color):
|
||||
header = ''
|
||||
footer = ''
|
||||
|
||||
if sys.stdout.isatty():
|
||||
header = Logger.COLOR_MAP.get(typ.lower(), '')
|
||||
if sys.stdout.isatty(): # pragma: no cover
|
||||
header = Logger.COLOR_MAP.get(color.lower(), '')
|
||||
footer = colorama.Style.RESET_ALL
|
||||
|
||||
return u'{}{}{}'.format(header, msg, footer)
|
||||
@@ -83,21 +118,113 @@ class Logger(object):
|
||||
return (str_exc, str_tb)
|
||||
|
||||
@staticmethod
|
||||
def error(msg, exc=None):
|
||||
def _prefix(msg, typ):
|
||||
color = Logger.LEVEL_COLOR_MAP.get(typ.lower(), '')
|
||||
return Logger.colorize('{}: '.format(msg), color)
|
||||
|
||||
@staticmethod
|
||||
def error_prefix():
|
||||
return Logger._prefix('Error', 'error')
|
||||
|
||||
@staticmethod
|
||||
def warning_prefix():
|
||||
return Logger._prefix('Warning', 'warn')
|
||||
|
||||
@staticmethod
|
||||
def debug_prefix():
|
||||
return Logger._prefix('Debug', 'debug')
|
||||
|
||||
@staticmethod
|
||||
def _with_progress(func, msg):
|
||||
from dvc.progress import progress
|
||||
with progress:
|
||||
func(msg)
|
||||
|
||||
@staticmethod
|
||||
def _error_exc(exc):
|
||||
if exc is None:
|
||||
return
|
||||
|
||||
if Logger.logger().getEffectiveLevel() != logging.DEBUG:
|
||||
return
|
||||
|
||||
prefix = Logger.error_prefix()
|
||||
str_exc, str_tb = Logger.parse_exc(exc)
|
||||
if Logger.logger().getEffectiveLevel() == logging.DEBUG and exc:
|
||||
str_tb = str_tb if str_tb else traceback.format_exc()
|
||||
Logger.logger().error(str_tb)
|
||||
return Logger.logger().error(Logger.colorize(msg + str_exc, 'error'))
|
||||
str_tb = str_tb if str_tb else traceback.format_exc()
|
||||
Logger._with_progress(Logger.logger().error, prefix + str_tb)
|
||||
|
||||
@staticmethod
|
||||
def warn(msg):
|
||||
return Logger.logger().warn(Logger.colorize(msg, 'warn'))
|
||||
def _with_exc(func, msg, suffix="", exc=None):
|
||||
Logger._error_exc(exc)
|
||||
msg = msg + Logger.parse_exc(exc)[0] + suffix
|
||||
Logger._with_progress(func, msg)
|
||||
|
||||
@staticmethod
|
||||
def debug(msg):
|
||||
return Logger.logger().debug(Logger.colorize(msg, 'debug'))
|
||||
def error(msg, exc=None):
|
||||
chat = "\n\nHaving any troubles? Hit us up at dvc.org/support, " \
|
||||
"we are always happy to help!"
|
||||
Logger._with_exc(Logger.logger().error,
|
||||
Logger.error_prefix() + msg,
|
||||
suffix=chat,
|
||||
exc=exc)
|
||||
|
||||
@classmethod
|
||||
def warn(cls, msg, exc=None):
|
||||
cls._with_exc(cls.logger().warning,
|
||||
cls.warning_prefix() + msg,
|
||||
exc=exc)
|
||||
|
||||
@classmethod
|
||||
def debug(cls, msg, exc=None):
|
||||
cls._with_exc(cls.logger().debug,
|
||||
cls.debug_prefix() + msg,
|
||||
exc=exc)
|
||||
|
||||
@staticmethod
|
||||
def info(msg):
|
||||
return Logger.logger().info(Logger.colorize(msg, 'info'))
|
||||
Logger._with_progress(Logger.logger().info, msg)
|
||||
|
||||
@classmethod
|
||||
def box(cls, msg, border_color=''):
|
||||
lines = msg.split('\n')
|
||||
max_width = max(visual_width(line) for line in lines)
|
||||
|
||||
padding_horizontal = 5
|
||||
padding_vertical = 1
|
||||
|
||||
box_size_horizontal = (max_width + (padding_horizontal * 2))
|
||||
|
||||
chars = {
|
||||
'corner': '+',
|
||||
'horizontal': '-',
|
||||
'vertical': '|',
|
||||
'empty': ' ',
|
||||
}
|
||||
|
||||
margin = "{corner}{line}{corner}\n".format(
|
||||
corner=chars['corner'],
|
||||
line=chars['horizontal'] * box_size_horizontal,
|
||||
)
|
||||
|
||||
padding_lines = [
|
||||
"{border}{space}{border}\n".format(
|
||||
border=cls.colorize(chars['vertical'], border_color),
|
||||
space=chars['empty'] * box_size_horizontal,
|
||||
) * padding_vertical
|
||||
]
|
||||
|
||||
content_lines = [
|
||||
"{border}{space}{content}{space}{border}\n".format(
|
||||
border=cls.colorize(chars['vertical'], border_color),
|
||||
space=chars['empty'] * padding_horizontal,
|
||||
content=visual_center(line, max_width),
|
||||
) for line in lines
|
||||
]
|
||||
|
||||
box = "{margin}{padding}{content}{padding}{margin}".format(
|
||||
margin=cls.colorize(margin, border_color),
|
||||
padding=''.join(padding_lines),
|
||||
content=''.join(content_lines),
|
||||
)
|
||||
|
||||
print(box)
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class OutputGS(DependencyGS):
|
||||
return True
|
||||
|
||||
if self.use_cache \
|
||||
and self.info != self.project.cache.gs.save_info(self.path_info):
|
||||
and self.project.cache.gs.changed(self.path_info, self.info):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class OutputHDFS(DependencyHDFS):
|
||||
return True
|
||||
|
||||
if self.use_cache \
|
||||
and self.info != self.project.cache.hdfs.save_info(self.path_info):
|
||||
and self.project.cache.hdfs.changed(self.path_info, self.info):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
+5
-13
@@ -40,10 +40,6 @@ class OutputLOCAL(DependencyLOCAL):
|
||||
def cache(self):
|
||||
return self.project.cache.local.get(self.md5)
|
||||
|
||||
@property
|
||||
def is_local(self):
|
||||
return self.path.startswith(self.project.root_dir)
|
||||
|
||||
def dumpd(self):
|
||||
ret = super(OutputLOCAL, self).dumpd()
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
@@ -62,17 +58,12 @@ class OutputLOCAL(DependencyLOCAL):
|
||||
if not self.use_cache:
|
||||
return super(OutputLOCAL, self).changed()
|
||||
|
||||
return self.info != self.project.cache.local.save_info(self.path_info)
|
||||
return self.project.cache.local.changed(self.path_info, self.info)
|
||||
|
||||
def checkout(self):
|
||||
if not self.use_cache:
|
||||
return
|
||||
|
||||
if not self.changed():
|
||||
msg = u'Data file \'{}\' didn\'t change, skipping checkout.'
|
||||
self.project.logger.debug(msg.format(self.rel_path))
|
||||
return
|
||||
|
||||
self.project.cache.local.checkout(self.path_info, self.info)
|
||||
|
||||
def _verify_metric(self):
|
||||
@@ -92,13 +83,14 @@ class OutputLOCAL(DependencyLOCAL):
|
||||
super(OutputLOCAL, self).save()
|
||||
self._verify_metric()
|
||||
msg = 'Output \'{}\' doesn\'t use cache. Skipping saving.'
|
||||
self.project.logger.debug(msg.format(self.rel_path))
|
||||
self.project.logger.info(msg.format(self.rel_path))
|
||||
return
|
||||
|
||||
if not os.path.exists(self.path):
|
||||
raise self.DoesNotExistError(self.rel_path)
|
||||
|
||||
if not os.path.isfile(self.path) and not os.path.isdir(self.path):
|
||||
if not os.path.isfile(self.path) \
|
||||
and not os.path.isdir(self.path): # pragma: no cover
|
||||
raise self.IsNotFileOrDirError(self.rel_path)
|
||||
|
||||
if (os.path.isfile(self.path) and os.path.getsize(self.path) == 0) or \
|
||||
@@ -108,7 +100,7 @@ class OutputLOCAL(DependencyLOCAL):
|
||||
|
||||
if not self.changed():
|
||||
msg = 'Output \'{}\' didn\'t change. Skipping saving.'
|
||||
self.project.logger.debug(msg.format(self.rel_path))
|
||||
self.project.logger.info(msg.format(self.rel_path))
|
||||
return
|
||||
|
||||
if self.is_local:
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class OutputS3(DependencyS3):
|
||||
return True
|
||||
|
||||
if self.use_cache \
|
||||
and self.info != self.project.cache.s3.save_info(self.path_info):
|
||||
and self.project.cache.s3.changed(self.path_info, self.info):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class OutputSSH(DependencySSH):
|
||||
return True
|
||||
|
||||
if self.use_cache \
|
||||
and self.info != self.project.cache.ssh.save_info(self.path_info):
|
||||
and self.project.cache.ssh.changed(self.path_info, self.info):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
+29
-12
@@ -11,11 +11,16 @@ class Progress(object):
|
||||
self._n_total = 0
|
||||
self._n_finished = 0
|
||||
self._lock = threading.Lock()
|
||||
self._line = None
|
||||
|
||||
def set_n_total(self, total):
|
||||
self._n_total = total
|
||||
self._n_finished = 0
|
||||
|
||||
@property
|
||||
def is_finished(self):
|
||||
return self._n_total == self._n_finished
|
||||
|
||||
def _clearln(self):
|
||||
print('\r\x1b[K', end='')
|
||||
|
||||
@@ -24,18 +29,23 @@ class Progress(object):
|
||||
print(line, end='')
|
||||
sys.stdout.flush()
|
||||
|
||||
def update_target(self, name, current, total):
|
||||
def refresh(self, line=None):
|
||||
# Just go away if it is locked. Will update next time
|
||||
if not self._lock.acquire(False):
|
||||
return
|
||||
|
||||
bar = self._bar(name, current, total)
|
||||
if line is None:
|
||||
line = self._line
|
||||
|
||||
if sys.stdout.isatty():
|
||||
self._writeln(bar)
|
||||
if sys.stdout.isatty() and line is not None:
|
||||
self._writeln(line)
|
||||
self._line = line
|
||||
|
||||
self._lock.release()
|
||||
|
||||
def update_target(self, name, current, total):
|
||||
self.refresh(self._bar(name, current, total))
|
||||
|
||||
def finish_target(self, name):
|
||||
# We have to write a msg about finished target
|
||||
with self._lock:
|
||||
@@ -47,13 +57,13 @@ class Progress(object):
|
||||
print(bar)
|
||||
|
||||
self._n_finished += 1
|
||||
self._line = None
|
||||
|
||||
def _bar(self, target_name, current, total):
|
||||
"""
|
||||
Make a progress bar out of info, which looks like:
|
||||
(1/2): [########################################] 100% master.zip
|
||||
"""
|
||||
total_len = 100
|
||||
bar_len = 30
|
||||
|
||||
if total is None:
|
||||
@@ -64,19 +74,26 @@ class Progress(object):
|
||||
progress = int((100 * current)/total) if current < total else 100
|
||||
percent = str(progress) + "% "
|
||||
|
||||
num = "({}/{}): ".format(self._n_finished + 1, self._n_total)
|
||||
if self._n_total > 1:
|
||||
num = "({}/{}): ".format(self._n_finished + 1, self._n_total)
|
||||
else:
|
||||
num = ""
|
||||
|
||||
n_sh = int((progress * bar_len)/100)
|
||||
n_sp = bar_len - n_sh
|
||||
bar = "[" + '#'*n_sh + ' '*n_sp + "] "
|
||||
|
||||
name_len = total_len - len(num + bar + percent)
|
||||
if len(target_name) > name_len:
|
||||
name = target_name[:name_len]
|
||||
else:
|
||||
name = target_name
|
||||
return num + bar + percent + target_name
|
||||
|
||||
return num + bar + percent + name
|
||||
def __enter__(self):
|
||||
self._lock.acquire(True)
|
||||
if self._line is not None:
|
||||
self._clearln()
|
||||
|
||||
def __exit__(self, type, value, tb):
|
||||
if self._line is not None:
|
||||
self.refresh()
|
||||
self._lock.release()
|
||||
|
||||
|
||||
progress = Progress()
|
||||
|
||||
+602
-142
@@ -1,6 +1,7 @@
|
||||
import collections
|
||||
import os
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.exceptions import DvcException, MoveNotDataSourceError
|
||||
from dvc.stage import Stage
|
||||
|
||||
|
||||
@@ -9,15 +10,6 @@ class InitError(DvcException):
|
||||
super(InitError, self).__init__(msg)
|
||||
|
||||
|
||||
class NotDvcFileError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = '\'{}\' is not a DVC file'.format(path)
|
||||
p = path + Stage.STAGE_FILE_SUFFIX
|
||||
if Stage.is_stage_file(p):
|
||||
msg += '. Maybe you meant \'{}\'?'.format(p)
|
||||
super(NotDvcFileError, self).__init__(msg)
|
||||
|
||||
|
||||
class ReproductionError(DvcException):
|
||||
def __init__(self, dvc_file_name, ex):
|
||||
self.path = dvc_file_name
|
||||
@@ -31,23 +23,23 @@ class Project(object):
|
||||
def __init__(self, root_dir):
|
||||
from dvc.logger import Logger
|
||||
from dvc.config import Config
|
||||
from dvc.state import LinkState, State
|
||||
from dvc.state import State
|
||||
from dvc.lock import Lock
|
||||
from dvc.scm import SCM
|
||||
from dvc.cache import Cache
|
||||
from dvc.data_cloud import DataCloud
|
||||
from dvc.updater import Updater
|
||||
from dvc.prompt import Prompt
|
||||
|
||||
self.root_dir = os.path.abspath(os.path.realpath(root_dir))
|
||||
self.dvc_dir = os.path.join(self.root_dir, self.DVC_DIR)
|
||||
|
||||
self.config = Config(self.dvc_dir)
|
||||
self.scm = SCM(self.root_dir)
|
||||
self.scm = SCM(self.root_dir, project=self)
|
||||
self.lock = Lock(self.dvc_dir)
|
||||
# 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)
|
||||
self.link_state = LinkState(self)
|
||||
self.state = State(self, self.config._config)
|
||||
|
||||
core = self.config._config[Config.SECTION_CORE]
|
||||
self.logger = Logger(core.get(Config.SECTION_CORE_LOGLEVEL, None))
|
||||
@@ -55,11 +47,23 @@ class Project(object):
|
||||
self.cache = Cache(self)
|
||||
self.cloud = DataCloud(self, config=self.config._config)
|
||||
self.updater = Updater(self.dvc_dir)
|
||||
self.prompt = Prompt()
|
||||
|
||||
self._files_to_git_add = []
|
||||
|
||||
self._ignore()
|
||||
|
||||
self.updater.check()
|
||||
|
||||
def _remind_to_git_add(self):
|
||||
if len(self._files_to_git_add) == 0:
|
||||
return
|
||||
|
||||
msg = '\nTo track the changes with git run:\n\n'
|
||||
msg += '\tgit add ' + " ".join(self._files_to_git_add)
|
||||
|
||||
self.logger.info(msg)
|
||||
|
||||
@staticmethod
|
||||
def init(root_dir=os.curdir, no_scm=False, force=False):
|
||||
"""
|
||||
@@ -77,10 +81,10 @@ class Project(object):
|
||||
import shutil
|
||||
from dvc.scm import SCM, Base
|
||||
from dvc.config import Config
|
||||
from dvc.logger import Logger
|
||||
|
||||
root_dir = os.path.abspath(root_dir)
|
||||
dvc_dir = os.path.join(root_dir, Project.DVC_DIR)
|
||||
|
||||
scm = SCM(root_dir)
|
||||
if type(scm) == Base and not no_scm:
|
||||
msg = "{} is not tracked by any supported scm tool(e.g. git)."
|
||||
@@ -101,8 +105,23 @@ class Project(object):
|
||||
if scm.ignore_file():
|
||||
scm.add([os.path.join(dvc_dir, scm.ignore_file())])
|
||||
|
||||
Logger.info('You can now commit the changes to git.')
|
||||
|
||||
return proj
|
||||
|
||||
@staticmethod
|
||||
def load_all(projects_paths):
|
||||
"""
|
||||
Instantiate all projects in the given list of paths.
|
||||
|
||||
Args:
|
||||
projects_paths: List of paths to projects.
|
||||
|
||||
Returns:
|
||||
List of Project instances in the same order of the given paths.
|
||||
"""
|
||||
return [Project(path) for path in projects_paths]
|
||||
|
||||
def destroy(self):
|
||||
import shutil
|
||||
|
||||
@@ -112,13 +131,12 @@ class Project(object):
|
||||
shutil.rmtree(self.dvc_dir)
|
||||
|
||||
def _ignore(self):
|
||||
flist = [self.state.state_file,
|
||||
self.state._lock_file.lock_file,
|
||||
self.link_state.state_file,
|
||||
self.link_state._lock_file.lock_file,
|
||||
self.lock.lock_file,
|
||||
self.config.config_local_file,
|
||||
self.updater.updater_file]
|
||||
flist = [
|
||||
self.state.state_file,
|
||||
self.lock.lock_file,
|
||||
self.config.config_local_file,
|
||||
self.updater.updater_file,
|
||||
] + self.state.temp_files
|
||||
|
||||
if self.cache.local.cache_dir.startswith(self.root_dir):
|
||||
flist += [self.cache.local.cache_dir]
|
||||
@@ -131,19 +149,63 @@ class Project(object):
|
||||
def to_dvc_path(self, path):
|
||||
return os.path.relpath(path, self.root_dir)
|
||||
|
||||
def add(self, fname):
|
||||
stage = Stage.loads(project=self,
|
||||
outs=[fname],
|
||||
add=True)
|
||||
def _check_output_duplication(self, outs):
|
||||
from dvc.exceptions import OutputDuplicationError
|
||||
|
||||
stage.save()
|
||||
stage.dump()
|
||||
return stage
|
||||
for stage in self.stages():
|
||||
for o in stage.outs:
|
||||
for out in outs:
|
||||
if o.path == out.path and o.stage.path != out.stage.path:
|
||||
stages = [o.stage.relpath, out.stage.relpath]
|
||||
raise OutputDuplicationError(o.path, stages)
|
||||
|
||||
def _check_circular_dependency(self, deps, outs):
|
||||
from dvc.exceptions import CircularDependencyError
|
||||
|
||||
circular_dependencies = (
|
||||
set(file.path for file in deps) &
|
||||
set(file.path for file in outs)
|
||||
)
|
||||
|
||||
if circular_dependencies:
|
||||
raise CircularDependencyError(circular_dependencies.pop())
|
||||
|
||||
def add(self, fname, recursive=False):
|
||||
fnames = []
|
||||
if recursive and os.path.isdir(fname):
|
||||
fnames = []
|
||||
for root, dirs, files in os.walk(fname):
|
||||
for f in files:
|
||||
path = os.path.join(root, f)
|
||||
if Stage.is_stage_file(path):
|
||||
continue
|
||||
if os.path.basename(path) == self.scm.ignore_file():
|
||||
continue
|
||||
if self.scm.is_tracked(path):
|
||||
continue
|
||||
fnames.append(path)
|
||||
else:
|
||||
fnames = [fname]
|
||||
|
||||
stages = []
|
||||
self._files_to_git_add = []
|
||||
with self.state:
|
||||
for f in fnames:
|
||||
stage = Stage.loads(project=self,
|
||||
outs=[f],
|
||||
add=True)
|
||||
|
||||
self._check_output_duplication(stage.outs)
|
||||
|
||||
stage.save()
|
||||
stage.dump()
|
||||
stages.append(stage)
|
||||
|
||||
self._remind_to_git_add()
|
||||
|
||||
return stages
|
||||
|
||||
def remove(self, target, outs_only=False):
|
||||
if not Stage.is_stage_file(target):
|
||||
raise NotDvcFileError(target)
|
||||
|
||||
stage = Stage.load(self, target)
|
||||
if outs_only:
|
||||
stage.remove_outs()
|
||||
@@ -153,9 +215,6 @@ class Project(object):
|
||||
return stage
|
||||
|
||||
def lock_stage(self, target, unlock=False):
|
||||
if not Stage.is_stage_file(target):
|
||||
raise NotDvcFileError(target)
|
||||
|
||||
stage = Stage.load(self, target)
|
||||
stage.locked = False if unlock else True
|
||||
stage.dump()
|
||||
@@ -169,39 +228,74 @@ class Project(object):
|
||||
[from_path])[0]
|
||||
|
||||
found = False
|
||||
for stage in self.stages():
|
||||
for out in stage.outs:
|
||||
if out.path != from_out.path:
|
||||
continue
|
||||
self._files_to_git_add = []
|
||||
with self.state:
|
||||
for stage in self.stages():
|
||||
for out in stage.outs:
|
||||
if out.path != from_out.path:
|
||||
continue
|
||||
|
||||
if not stage.is_data_source:
|
||||
msg = 'Dvcfile \'{}\' is not a data source.'
|
||||
raise DvcException(msg.format(stage.rel_path))
|
||||
if not stage.is_data_source:
|
||||
raise MoveNotDataSourceError(stage.relpath)
|
||||
|
||||
found = True
|
||||
to_out = Output.loads_from(out.stage,
|
||||
[to_path],
|
||||
out.cache,
|
||||
out.metric)[0]
|
||||
out.move(to_out)
|
||||
found = True
|
||||
to_out = Output.loads_from(out.stage,
|
||||
[to_path],
|
||||
out.cache,
|
||||
out.metric)[0]
|
||||
out.move(to_out)
|
||||
|
||||
stage_base = os.path.basename(stage.path)
|
||||
stage_base = stage_base.rstrip(Stage.STAGE_FILE_SUFFIX)
|
||||
stage_base = os.path.basename(stage.path)
|
||||
stage_base = stage_base.rstrip(Stage.STAGE_FILE_SUFFIX)
|
||||
|
||||
stage_dir = os.path.dirname(stage.path)
|
||||
from_base = os.path.basename(from_path)
|
||||
to_base = os.path.basename(to_path)
|
||||
if stage_base == from_base:
|
||||
os.unlink(stage.path)
|
||||
path = to_base + Stage.STAGE_FILE_SUFFIX
|
||||
stage.path = os.path.join(stage_dir, path)
|
||||
stage_dir = os.path.dirname(stage.path)
|
||||
from_base = os.path.basename(from_path)
|
||||
to_base = os.path.basename(to_path)
|
||||
if stage_base == from_base:
|
||||
os.unlink(stage.path)
|
||||
path = to_base + Stage.STAGE_FILE_SUFFIX
|
||||
stage.path = os.path.join(stage_dir, path)
|
||||
|
||||
stage.dump()
|
||||
stage.dump()
|
||||
|
||||
self._remind_to_git_add()
|
||||
|
||||
if not found:
|
||||
msg = 'Unable to find dvcfile with output \'{}\''
|
||||
raise DvcException(msg.format(from_path))
|
||||
|
||||
def _unprotect_file(self, path):
|
||||
import stat
|
||||
import uuid
|
||||
from dvc.utils import copyfile, move, remove
|
||||
|
||||
self.logger.debug("Unprotecting '{}'".format(path))
|
||||
|
||||
tmp = os.path.join(os.path.dirname(path), '.' + str(uuid.uuid4()))
|
||||
move(path, tmp)
|
||||
|
||||
copyfile(tmp, path)
|
||||
|
||||
remove(tmp)
|
||||
|
||||
os.chmod(path, os.stat(path).st_mode | stat.S_IWRITE)
|
||||
|
||||
def _unprotect_dir(self, path):
|
||||
for root, dirs, files in os.walk(path):
|
||||
for f in files:
|
||||
path = os.path.join(root, f)
|
||||
self._unprotect_file(path)
|
||||
|
||||
def unprotect(self, path):
|
||||
if not os.path.exists(path):
|
||||
raise DvcException("Can't unprotect non-existing "
|
||||
"data '{}'".format(path))
|
||||
|
||||
if os.path.isdir(path):
|
||||
self._unprotect_dir(path)
|
||||
else:
|
||||
self._unprotect_file(path)
|
||||
|
||||
def run(self,
|
||||
cmd=None,
|
||||
deps=[],
|
||||
@@ -210,7 +304,8 @@ class Project(object):
|
||||
metrics_no_cache=[],
|
||||
fname=Stage.STAGE_FILE,
|
||||
cwd=os.curdir,
|
||||
no_exec=False):
|
||||
no_exec=False,
|
||||
overwrite=False):
|
||||
stage = Stage.loads(project=self,
|
||||
fname=fname,
|
||||
cmd=cmd,
|
||||
@@ -218,10 +313,21 @@ class Project(object):
|
||||
outs=outs,
|
||||
outs_no_cache=outs_no_cache,
|
||||
metrics_no_cache=metrics_no_cache,
|
||||
deps=deps)
|
||||
if not no_exec:
|
||||
stage.run()
|
||||
deps=deps,
|
||||
overwrite=overwrite)
|
||||
|
||||
self._check_output_duplication(stage.outs)
|
||||
self._check_circular_dependency(stage.deps, stage.outs)
|
||||
|
||||
self._files_to_git_add = []
|
||||
with self.state:
|
||||
if not no_exec:
|
||||
stage.run()
|
||||
|
||||
stage.dump()
|
||||
|
||||
self._remind_to_git_add()
|
||||
|
||||
return stage
|
||||
|
||||
def imp(self, url, out):
|
||||
@@ -230,45 +336,124 @@ class Project(object):
|
||||
deps=[url],
|
||||
outs=[out])
|
||||
|
||||
stage.run()
|
||||
self._check_output_duplication(stage.outs)
|
||||
|
||||
self._files_to_git_add = []
|
||||
with self.state:
|
||||
stage.run()
|
||||
|
||||
stage.dump()
|
||||
|
||||
self._remind_to_git_add()
|
||||
|
||||
return stage
|
||||
|
||||
def _reproduce_stage(self, stages, node, force):
|
||||
def _reproduce_stage(self, stages, node, force, dry, interactive):
|
||||
stage = stages[node]
|
||||
|
||||
if stage.locked:
|
||||
msg = 'DVC file \'{}\' is locked. Its dependecies are not ' \
|
||||
msg = 'DVC file \'{}\' is locked. Its dependencies are not ' \
|
||||
'going to be reproduced.'
|
||||
self.logger.warn(msg.format(stage.relpath))
|
||||
|
||||
stage = stage.reproduce(force=force)
|
||||
stage = stage.reproduce(force=force, dry=dry, interactive=interactive)
|
||||
if not stage:
|
||||
return []
|
||||
stage.dump()
|
||||
|
||||
if not dry:
|
||||
stage.dump()
|
||||
|
||||
return [stage]
|
||||
|
||||
def reproduce(self, target, recursive=True, force=False):
|
||||
def reproduce(self,
|
||||
target=None,
|
||||
recursive=True,
|
||||
force=False,
|
||||
dry=False,
|
||||
interactive=False,
|
||||
pipeline=False,
|
||||
all_pipelines=False):
|
||||
|
||||
if target is None and not all_pipelines:
|
||||
raise ValueError()
|
||||
|
||||
if not interactive:
|
||||
config = self.config
|
||||
core = config._config[config.SECTION_CORE]
|
||||
interactive = core.get(config.SECTION_CORE_INTERACTIVE, False)
|
||||
|
||||
targets = []
|
||||
if pipeline or all_pipelines:
|
||||
if pipeline:
|
||||
stage = Stage.load(self, target)
|
||||
node = os.path.relpath(stage.path, self.root_dir)
|
||||
pipelines = [self._get_pipeline(node)]
|
||||
else:
|
||||
pipelines = self.pipelines()
|
||||
|
||||
for G in pipelines:
|
||||
for node in G.nodes():
|
||||
if G.in_degree(node) == 0:
|
||||
targets.append(os.path.join(self.root_dir, node))
|
||||
else:
|
||||
targets.append(target)
|
||||
|
||||
self._files_to_git_add = []
|
||||
|
||||
ret = []
|
||||
with self.state:
|
||||
for target in targets:
|
||||
stages = self._reproduce(target,
|
||||
recursive=recursive,
|
||||
force=force,
|
||||
dry=dry,
|
||||
interactive=interactive)
|
||||
ret.extend(stages)
|
||||
|
||||
self._remind_to_git_add()
|
||||
|
||||
return ret
|
||||
|
||||
def _reproduce(self,
|
||||
target,
|
||||
recursive=True,
|
||||
force=False,
|
||||
dry=False,
|
||||
interactive=False):
|
||||
import networkx as nx
|
||||
|
||||
stage = Stage.load(self, target)
|
||||
G = self.graph()[1]
|
||||
stages = nx.get_node_attributes(G, 'stage')
|
||||
node = os.path.relpath(os.path.abspath(target), self.root_dir)
|
||||
if node not in stages:
|
||||
raise NotDvcFileError(target)
|
||||
node = os.path.relpath(stage.path, self.root_dir)
|
||||
|
||||
if recursive:
|
||||
return self._reproduce_stages(G, stages, node, force)
|
||||
ret = self._reproduce_stages(G,
|
||||
stages,
|
||||
node,
|
||||
force,
|
||||
dry,
|
||||
interactive)
|
||||
else:
|
||||
ret = self._reproduce_stage(stages,
|
||||
node,
|
||||
force,
|
||||
dry,
|
||||
interactive)
|
||||
|
||||
return self._reproduce_stage(stages, node, force)
|
||||
return ret
|
||||
|
||||
def _reproduce_stages(self, G, stages, node, force):
|
||||
def _reproduce_stages(self, G, stages, node, force, dry, interactive):
|
||||
import networkx as nx
|
||||
|
||||
result = []
|
||||
for n in nx.dfs_postorder_nodes(G, node):
|
||||
try:
|
||||
result += self._reproduce_stage(stages, n, force)
|
||||
result += self._reproduce_stage(stages,
|
||||
n,
|
||||
force,
|
||||
dry,
|
||||
interactive)
|
||||
except Exception as ex:
|
||||
raise ReproductionError(stages[n].relpath, ex)
|
||||
return result
|
||||
@@ -278,39 +463,138 @@ class Project(object):
|
||||
for stage in all_stages:
|
||||
for out in stage.outs:
|
||||
used.append(out.path)
|
||||
self.link_state.remove_unused(used)
|
||||
self.state.remove_unused_links(used)
|
||||
|
||||
def checkout(self, target=None):
|
||||
def checkout(self, target=None, with_deps=False):
|
||||
all_stages = self.active_stages()
|
||||
stages = all_stages
|
||||
|
||||
if target:
|
||||
if not Stage.is_stage_file(target):
|
||||
raise NotDvcFileError(target)
|
||||
stages = [Stage.load(self, target)]
|
||||
else:
|
||||
stages = all_stages
|
||||
stages = self._collect(target, with_deps=with_deps)
|
||||
|
||||
self._cleanup_unused_links(all_stages)
|
||||
with self.state:
|
||||
self._cleanup_unused_links(all_stages)
|
||||
|
||||
for stage in stages:
|
||||
if stage.locked:
|
||||
msg = 'DVC file \'{}\' is locked. Its dependecies are not ' \
|
||||
'going to be checked out.'
|
||||
self.logger.warn(msg.format(stage.relpath))
|
||||
for stage in stages:
|
||||
if stage.locked:
|
||||
msg = 'DVC file \'{}\' is locked. Its dependencies are ' \
|
||||
'not going to be checked out.'
|
||||
self.logger.warn(msg.format(stage.relpath))
|
||||
|
||||
stage.checkout()
|
||||
stage.checkout()
|
||||
|
||||
def _used_cache(self, target=None, all_branches=False, active=True):
|
||||
def _get_pipeline(self, node):
|
||||
pipelines = list(filter(lambda g: node in g.nodes(),
|
||||
self.pipelines()))
|
||||
assert len(pipelines) == 1
|
||||
return pipelines[0]
|
||||
|
||||
def _collect(self, target, with_deps=False):
|
||||
import networkx as nx
|
||||
|
||||
stage = Stage.load(self, target)
|
||||
if not with_deps:
|
||||
return [stage]
|
||||
|
||||
node = os.path.relpath(stage.path, self.root_dir)
|
||||
G = self._get_pipeline(node)
|
||||
stages = nx.get_node_attributes(G, 'stage')
|
||||
|
||||
ret = [stage]
|
||||
for n in nx.dfs_postorder_nodes(G, node):
|
||||
ret.append(stages[n])
|
||||
|
||||
return ret
|
||||
|
||||
def _collect_dir_cache(self,
|
||||
out,
|
||||
branch=None,
|
||||
remote=None,
|
||||
force=False,
|
||||
jobs=None):
|
||||
info = out.dumpd()
|
||||
ret = [info]
|
||||
r = out.remote
|
||||
md5 = info[r.PARAM_MD5]
|
||||
|
||||
if self.cache.local.changed_cache_file(md5):
|
||||
try:
|
||||
self.cloud.pull(ret,
|
||||
jobs=jobs,
|
||||
remote=remote,
|
||||
show_checksums=False)
|
||||
except DvcException as exc:
|
||||
msg = "Failed to pull cache for '{}': {}"
|
||||
self.logger.debug(msg.format(out, exc))
|
||||
|
||||
if self.cache.local.changed_cache_file(md5):
|
||||
msg = "Missing cache for directory '{}'. " \
|
||||
"Cache for files inside will be lost. " \
|
||||
"Would you like to continue? Use '-f' to force. "
|
||||
if not (force or self.prompt.prompt(msg, False)):
|
||||
raise DvcException("Unable to fully collect "
|
||||
"used cache without cache "
|
||||
"for directory '{}'".format(out))
|
||||
else:
|
||||
return ret
|
||||
|
||||
for i in self.cache.local.load_dir_cache(md5):
|
||||
i['branch'] = branch
|
||||
i[r.PARAM_PATH] = os.path.join(info[r.PARAM_PATH],
|
||||
i[r.PARAM_RELPATH])
|
||||
ret.append(i)
|
||||
|
||||
return ret
|
||||
|
||||
def _collect_used_cache(self,
|
||||
out,
|
||||
branch=None,
|
||||
remote=None,
|
||||
force=False,
|
||||
jobs=None):
|
||||
if not out.use_cache or not out.info:
|
||||
return []
|
||||
|
||||
info = out.dumpd()
|
||||
info['branch'] = branch
|
||||
ret = [info]
|
||||
|
||||
if out.path_info['scheme'] != 'local':
|
||||
return ret
|
||||
|
||||
md5 = info[out.remote.PARAM_MD5]
|
||||
cache = self.cache.local.get(md5)
|
||||
if not out.remote.is_dir_cache(cache):
|
||||
return ret
|
||||
|
||||
return self._collect_dir_cache(out,
|
||||
branch=branch,
|
||||
remote=remote,
|
||||
force=force,
|
||||
jobs=jobs)
|
||||
|
||||
def _used_cache(self,
|
||||
target=None,
|
||||
all_branches=False,
|
||||
active=True,
|
||||
with_deps=False,
|
||||
all_tags=False,
|
||||
remote=None,
|
||||
force=False,
|
||||
jobs=None):
|
||||
cache = {}
|
||||
cache['local'] = []
|
||||
cache['s3'] = []
|
||||
cache['gs'] = []
|
||||
cache['hdfs'] = []
|
||||
cache['ssh'] = []
|
||||
cache['azure'] = []
|
||||
|
||||
for branch in self.scm.brancher(all_branches=all_branches):
|
||||
for branch in self.scm.brancher(all_branches=all_branches,
|
||||
all_tags=all_tags):
|
||||
if target:
|
||||
stages = [Stage.load(self, target)]
|
||||
stages = self._collect(target,
|
||||
with_deps=with_deps)
|
||||
elif active:
|
||||
stages = self.active_stages()
|
||||
else:
|
||||
@@ -318,55 +602,174 @@ class Project(object):
|
||||
|
||||
for stage in stages:
|
||||
if active and not target and stage.locked:
|
||||
msg = 'DVC file \'{}\' is locked. Its dependecies are ' \
|
||||
msg = 'DVC file \'{}\' is locked. Its dependencies are ' \
|
||||
'not going to be pushed/pulled/fetched.'
|
||||
self.logger.warn(msg.format(stage.relpath))
|
||||
|
||||
for out in stage.outs:
|
||||
if not out.use_cache or not out.info:
|
||||
continue
|
||||
|
||||
cache[out.path_info['scheme']] += [out.info]
|
||||
scheme = out.path_info['scheme']
|
||||
cache[scheme] += self._collect_used_cache(out,
|
||||
branch=branch,
|
||||
remote=remote,
|
||||
force=force,
|
||||
jobs=jobs)
|
||||
|
||||
return cache
|
||||
|
||||
def gc(self, all_branches=False, cloud=False, remote=None):
|
||||
clist = self._used_cache(target=None,
|
||||
all_branches=all_branches,
|
||||
active=False)
|
||||
self.cache.local.gc(clist)
|
||||
@staticmethod
|
||||
def merge_cache_lists(clists):
|
||||
merged_cache = collections.defaultdict(list)
|
||||
|
||||
if self.cache.s3:
|
||||
self.cache.s3.gc(clist)
|
||||
for cache_list in clists:
|
||||
for scheme, cache in cache_list.items():
|
||||
for item in cache:
|
||||
if item not in merged_cache[scheme]:
|
||||
merged_cache[scheme].append(item)
|
||||
|
||||
if self.cache.gs:
|
||||
self.cache.gs.gc(clist)
|
||||
return merged_cache
|
||||
|
||||
if self.cache.ssh:
|
||||
self.cache.ssh.gc(clist)
|
||||
@staticmethod
|
||||
def load_all_used_cache(projects,
|
||||
target=None,
|
||||
all_branches=False,
|
||||
active=True,
|
||||
with_deps=False,
|
||||
all_tags=False,
|
||||
remote=None,
|
||||
force=False,
|
||||
jobs=None):
|
||||
clists = []
|
||||
|
||||
if self.cache.hdfs:
|
||||
self.cache.hdfs.gc(clist)
|
||||
for project in projects:
|
||||
with project.state:
|
||||
project_clist = project._used_cache(target=None,
|
||||
all_branches=all_branches,
|
||||
active=False,
|
||||
with_deps=with_deps,
|
||||
all_tags=all_tags,
|
||||
remote=remote,
|
||||
force=force,
|
||||
jobs=jobs)
|
||||
|
||||
if self.cache.azure:
|
||||
self.cache.azure.gc(clist)
|
||||
clists.append(project_clist)
|
||||
|
||||
if cloud:
|
||||
self.cloud._get_cloud(remote).gc(clist)
|
||||
return clists
|
||||
|
||||
def push(self, target=None, jobs=1, remote=None, all_branches=False):
|
||||
self.cloud.push(self._used_cache(target, all_branches)['local'],
|
||||
jobs,
|
||||
remote=remote)
|
||||
def _do_gc(self, typ, func, clist):
|
||||
removed = func(clist)
|
||||
if not removed:
|
||||
self.logger.info("No unused {} cache to remove.".format(typ))
|
||||
|
||||
def fetch(self, target=None, jobs=1, remote=None, all_branches=False):
|
||||
self.cloud.pull(self._used_cache(target, all_branches)['local'],
|
||||
jobs,
|
||||
remote=remote)
|
||||
def gc(self,
|
||||
all_branches=False,
|
||||
cloud=False,
|
||||
remote=None,
|
||||
with_deps=False,
|
||||
all_tags=False,
|
||||
force=False,
|
||||
jobs=None,
|
||||
projects=None):
|
||||
|
||||
def pull(self, target=None, jobs=1, remote=None, all_branches=False):
|
||||
self.fetch(target, jobs, remote=remote, all_branches=all_branches)
|
||||
self.checkout(target=target)
|
||||
all_projects = [self]
|
||||
|
||||
if projects is not None and len(projects) > 0:
|
||||
all_projects.extend(Project.load_all(projects))
|
||||
|
||||
all_clists = Project.load_all_used_cache(all_projects,
|
||||
target=None,
|
||||
all_branches=all_branches,
|
||||
active=False,
|
||||
with_deps=with_deps,
|
||||
all_tags=all_tags,
|
||||
remote=remote,
|
||||
force=force,
|
||||
jobs=jobs)
|
||||
|
||||
if len(all_clists) > 1:
|
||||
clist = Project.merge_cache_lists(all_clists)
|
||||
else:
|
||||
clist = all_clists[0]
|
||||
|
||||
with self.state:
|
||||
self._do_gc('local', self.cache.local.gc, clist)
|
||||
|
||||
if self.cache.s3:
|
||||
self._do_gc('s3', self.cache.s3.gc, clist)
|
||||
|
||||
if self.cache.gs:
|
||||
self._do_gc('gs', self.cache.gs.gc, clist)
|
||||
|
||||
if self.cache.ssh:
|
||||
self._do_gc('ssh', self.cache.ssh.gc, clist)
|
||||
|
||||
if self.cache.hdfs:
|
||||
self._do_gc('hdfs', self.cache.hdfs.gc, clist)
|
||||
|
||||
if self.cache.azure:
|
||||
self._do_gc('azure', self.cache.azure.gc, clist)
|
||||
|
||||
if cloud:
|
||||
self._do_gc('remote', self.cloud._get_cloud(remote,
|
||||
'gc -c').gc, clist)
|
||||
|
||||
def push(self,
|
||||
target=None,
|
||||
jobs=1,
|
||||
remote=None,
|
||||
all_branches=False,
|
||||
show_checksums=False,
|
||||
with_deps=False,
|
||||
all_tags=False):
|
||||
with self.state:
|
||||
used = self._used_cache(target,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
with_deps=with_deps,
|
||||
force=True,
|
||||
remote=remote,
|
||||
jobs=jobs)['local']
|
||||
self.cloud.push(used,
|
||||
jobs,
|
||||
remote=remote,
|
||||
show_checksums=show_checksums)
|
||||
|
||||
def fetch(self,
|
||||
target=None,
|
||||
jobs=1,
|
||||
remote=None,
|
||||
all_branches=False,
|
||||
show_checksums=False,
|
||||
with_deps=False,
|
||||
all_tags=False):
|
||||
with self.state:
|
||||
used = self._used_cache(target,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
with_deps=with_deps,
|
||||
force=True,
|
||||
remote=remote,
|
||||
jobs=jobs)['local']
|
||||
self.cloud.pull(used,
|
||||
jobs,
|
||||
remote=remote,
|
||||
show_checksums=show_checksums)
|
||||
|
||||
def pull(self,
|
||||
target=None,
|
||||
jobs=1,
|
||||
remote=None,
|
||||
all_branches=False,
|
||||
show_checksums=False,
|
||||
with_deps=False,
|
||||
all_tags=False):
|
||||
self.fetch(target,
|
||||
jobs,
|
||||
remote=remote,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
show_checksums=show_checksums,
|
||||
with_deps=with_deps)
|
||||
self.checkout(target=target, with_deps=with_deps)
|
||||
|
||||
def _local_status(self, target=None):
|
||||
status = {}
|
||||
@@ -378,21 +781,37 @@ class Project(object):
|
||||
|
||||
for stage in stages:
|
||||
if stage.locked:
|
||||
msg = 'DVC file \'{}\' is locked. Its dependecies are not ' \
|
||||
'going to be shown in status output.'
|
||||
msg = 'DVC file \'{}\' is locked. Its dependencies are not ' \
|
||||
'going to be shown in the status output.'
|
||||
self.logger.warn(msg.format(stage.relpath))
|
||||
|
||||
status.update(stage.status())
|
||||
|
||||
return status
|
||||
|
||||
def _cloud_status(self, target=None, jobs=1, remote=None):
|
||||
def _cloud_status(self,
|
||||
target=None,
|
||||
jobs=1,
|
||||
remote=None,
|
||||
show_checksums=False,
|
||||
all_branches=False,
|
||||
with_deps=False,
|
||||
all_tags=False):
|
||||
import dvc.remote.base as cloud
|
||||
|
||||
used = self._used_cache(target,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
with_deps=with_deps,
|
||||
force=True,
|
||||
remote=remote,
|
||||
jobs=jobs)['local']
|
||||
|
||||
status = {}
|
||||
for md5, ret in self.cloud.status(self._used_cache(target)['local'],
|
||||
for md5, ret in self.cloud.status(used,
|
||||
jobs,
|
||||
remote=remote):
|
||||
remote=remote,
|
||||
show_checksums=show_checksums):
|
||||
if ret == cloud.STATUS_OK:
|
||||
continue
|
||||
|
||||
@@ -405,10 +824,25 @@ class Project(object):
|
||||
|
||||
return status
|
||||
|
||||
def status(self, target=None, jobs=1, cloud=False, remote=None):
|
||||
if cloud:
|
||||
return self._cloud_status(target, jobs, remote=remote)
|
||||
return self._local_status(target)
|
||||
def status(self,
|
||||
target=None,
|
||||
jobs=1,
|
||||
cloud=False,
|
||||
remote=None,
|
||||
show_checksums=False,
|
||||
all_branches=False,
|
||||
with_deps=False,
|
||||
all_tags=False):
|
||||
with self.state:
|
||||
if cloud:
|
||||
return self._cloud_status(target,
|
||||
jobs,
|
||||
remote=remote,
|
||||
show_checksums=show_checksums,
|
||||
all_branches=all_branches,
|
||||
with_deps=with_deps,
|
||||
all_tags=all_tags)
|
||||
return self._local_status(target)
|
||||
|
||||
def _read_metric_json(self, fd, json_path):
|
||||
import json
|
||||
@@ -478,7 +912,7 @@ class Project(object):
|
||||
|
||||
abs_path = os.path.abspath(path)
|
||||
matched = [out for out in outs if out.path == abs_path]
|
||||
stages = [out.stage.path for out in matched]
|
||||
stages = [out.stage.relpath for out in matched]
|
||||
if len(stages) > 1:
|
||||
raise OutputDuplicationError(path, stages)
|
||||
|
||||
@@ -488,9 +922,11 @@ class Project(object):
|
||||
path=None,
|
||||
typ=None,
|
||||
xpath=None,
|
||||
all_branches=False):
|
||||
all_branches=False,
|
||||
all_tags=False):
|
||||
res = {}
|
||||
for branch in self.scm.brancher(all_branches=all_branches):
|
||||
for branch in self.scm.brancher(all_branches=all_branches,
|
||||
all_tags=all_tags):
|
||||
astages = self.active_stages()
|
||||
outs = [out for stage in astages for out in stage.outs]
|
||||
|
||||
@@ -511,8 +947,8 @@ class Project(object):
|
||||
entries = []
|
||||
for o in metrics:
|
||||
if not typ and isinstance(o.metric, dict):
|
||||
t = o.metric.get(o.PARAM_METRIC_TYPE, None)
|
||||
x = o.metric.get(o.PARAM_METRIC_XPATH, None)
|
||||
t = o.metric.get(o.PARAM_METRIC_TYPE, typ)
|
||||
x = o.metric.get(o.PARAM_METRIC_XPATH, xpath)
|
||||
else:
|
||||
t = typ
|
||||
x = xpath
|
||||
@@ -535,7 +971,7 @@ class Project(object):
|
||||
res[branch][rel] = metric
|
||||
|
||||
for branch, val in res.items():
|
||||
if all_branches:
|
||||
if all_branches or all_tags:
|
||||
self.logger.info('{}:'.format(branch))
|
||||
for fname, metric in val.items():
|
||||
self.logger.info('\t{}: {}'.format(fname, metric))
|
||||
@@ -594,14 +1030,22 @@ class Project(object):
|
||||
|
||||
def graph(self):
|
||||
import networkx as nx
|
||||
from dvc.exceptions import OutputDuplicationError
|
||||
|
||||
G = nx.DiGraph()
|
||||
G_active = nx.DiGraph()
|
||||
stages = self.stages()
|
||||
|
||||
outs = []
|
||||
outs_by_path = {}
|
||||
for stage in stages:
|
||||
outs += stage.outs
|
||||
for o in stage.outs:
|
||||
existing = outs_by_path.get(o.path, None)
|
||||
if existing is not None:
|
||||
stages = [o.stage.relpath, existing.stage.relpath]
|
||||
raise OutputDuplicationError(o.path, stages)
|
||||
outs.append(o)
|
||||
outs_by_path[o.path] = o
|
||||
|
||||
# collect the whole DAG
|
||||
for stage in stages:
|
||||
@@ -653,12 +1097,28 @@ class Project(object):
|
||||
|
||||
def stages(self):
|
||||
stages = []
|
||||
outs = []
|
||||
for root, dirs, files in os.walk(self.root_dir):
|
||||
for fname in files:
|
||||
path = os.path.join(root, fname)
|
||||
if not Stage.is_stage_file(path):
|
||||
continue
|
||||
stages.append(Stage.load(self, path))
|
||||
stage = Stage.load(self, path)
|
||||
for out in stage.outs:
|
||||
outs.append(out.path + out.sep)
|
||||
stages.append(stage)
|
||||
|
||||
def filter_dirs(dname):
|
||||
path = os.path.join(root, dname)
|
||||
if path == self.dvc_dir or path == self.scm.dir:
|
||||
return False
|
||||
for out in outs:
|
||||
if path == os.path.normpath(out) or path.startswith(out):
|
||||
return False
|
||||
return True
|
||||
|
||||
dirs[:] = list(filter(filter_dirs, dirs))
|
||||
|
||||
return stages
|
||||
|
||||
def active_stages(self):
|
||||
|
||||
+29
-7
@@ -1,5 +1,7 @@
|
||||
import sys
|
||||
|
||||
from dvc.progress import progress
|
||||
|
||||
try:
|
||||
# NOTE: in Python3 raw_input() was renamed to input()
|
||||
input = raw_input
|
||||
@@ -7,12 +9,32 @@ except NameError:
|
||||
pass
|
||||
|
||||
|
||||
def prompt(msg, default=False):
|
||||
if not sys.stdout.isatty():
|
||||
return default
|
||||
class Prompt(object):
|
||||
def __init__(self):
|
||||
self.default = None
|
||||
|
||||
answer = input(msg + u'\n').lower()
|
||||
while answer not in ['yes', 'no', 'y', 'n']:
|
||||
answer = input('Enter \'yes\' or \'no\'.\n').lower()
|
||||
def prompt(self, msg, default=False): # pragma: no cover
|
||||
if self.default is not None:
|
||||
return self.default
|
||||
|
||||
return answer[0] == "y"
|
||||
if not sys.stdout.isatty():
|
||||
return default
|
||||
|
||||
answer = input(msg + u' (y/n)\n').lower()
|
||||
while answer not in ['yes', 'no', 'y', 'n']:
|
||||
answer = input('Enter \'yes\' or \'no\'.\n').lower()
|
||||
|
||||
return answer[0] == "y"
|
||||
|
||||
def prompt_password(self, msg): # pragma: no cover
|
||||
import getpass
|
||||
|
||||
if not sys.stdout.isatty():
|
||||
return None
|
||||
|
||||
msg = 'Enter password for {}:\n'.format(msg)
|
||||
|
||||
if not progress.is_finished:
|
||||
msg = u'\n' + msg
|
||||
|
||||
return getpass.getpass(msg)
|
||||
|
||||
@@ -14,14 +14,11 @@ REMOTES = [RemoteHDFS, RemoteSSH, RemoteS3, RemoteGS, RemoteAzure, RemoteLOCAL]
|
||||
|
||||
def supported_url(url):
|
||||
config = {Config.SECTION_REMOTE_URL: url}
|
||||
for r in REMOTES:
|
||||
if r.supported(config):
|
||||
return True
|
||||
return False
|
||||
return any(remote.supported(config) for remote in REMOTES)
|
||||
|
||||
|
||||
def Remote(project, config):
|
||||
for r in REMOTES:
|
||||
if r.supported(config):
|
||||
return r(project, config)
|
||||
for remote in REMOTES:
|
||||
if remote.supported(config):
|
||||
return remote(project, config)
|
||||
raise UnsupportedRemoteError(str(config))
|
||||
|
||||
+47
-33
@@ -1,6 +1,7 @@
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
import re
|
||||
import posixpath
|
||||
|
||||
try:
|
||||
from azure.storage.blob import BlockBlobService
|
||||
@@ -34,8 +35,8 @@ class RemoteAzure(RemoteBase):
|
||||
super(RemoteAzure, self).__init__(project, config)
|
||||
self.project = project
|
||||
|
||||
url = config.get(Config.SECTION_REMOTE_URL)
|
||||
match = re.match(self.REGEX, url)
|
||||
self.url = config.get(Config.SECTION_REMOTE_URL)
|
||||
match = re.match(self.REGEX, self.url)
|
||||
|
||||
self.bucket = (
|
||||
match.group('container_name')
|
||||
@@ -92,6 +93,9 @@ class RemoteAzure(RemoteBase):
|
||||
#
|
||||
# return {self.PARAM_ETAG: etag}
|
||||
#
|
||||
# def changed(self, path_info, checksum_info):
|
||||
# return checksum_info != self.save_info(path_info)
|
||||
#
|
||||
# def _copy_blob(self, to_bucket, to_key, from_bucket, from_key):
|
||||
# source = self.blob_service.make_blob_url(from_bucket, from_key)
|
||||
#
|
||||
@@ -118,14 +122,14 @@ class RemoteAzure(RemoteBase):
|
||||
# from_bucket=self.bucket,
|
||||
# from_key='{}/{}'.format(etag[0:2], etag[2:]))
|
||||
#
|
||||
# def remove(self, path_info):
|
||||
# if path_info['scheme'] != self.scheme:
|
||||
# raise NotImplementedError
|
||||
#
|
||||
# Logger.debug('Removing azure://{}/{}'.format(path_info['bucket'],
|
||||
# path_info['key']))
|
||||
#
|
||||
# self.blob_service.delete_blob(path_info['bucket'], path_info['key'])
|
||||
def remove(self, path_info):
|
||||
if path_info['scheme'] != self.scheme:
|
||||
raise NotImplementedError
|
||||
|
||||
Logger.debug('Removing azure://{}/{}'.format(path_info['bucket'],
|
||||
path_info['key']))
|
||||
|
||||
self.blob_service.delete_blob(path_info['bucket'], path_info['key'])
|
||||
|
||||
def md5s_to_path_infos(self, md5s):
|
||||
return [{
|
||||
@@ -134,10 +138,12 @@ class RemoteAzure(RemoteBase):
|
||||
'key': '{}/{}'.format(md5[0:2], md5[2:])
|
||||
} for md5 in md5s]
|
||||
|
||||
def exists(self, path_infos):
|
||||
keys = {blob.name
|
||||
def _all_keys(self):
|
||||
return {blob.name
|
||||
for blob in self.blob_service.list_blobs(self.bucket)}
|
||||
|
||||
def exists(self, path_infos):
|
||||
keys = self._all_keys()
|
||||
ret = []
|
||||
for path_info in path_infos:
|
||||
if path_info['scheme'] != self.scheme:
|
||||
@@ -173,8 +179,8 @@ class RemoteAzure(RemoteBase):
|
||||
self.blob_service.create_blob_from_path(
|
||||
bucket, key, from_info['path'], progress_callback=cb)
|
||||
except Exception as ex:
|
||||
Logger.error("Failed to upload '{}'".format(from_info['path']),
|
||||
ex)
|
||||
msg = "Failed to upload '{}'".format(from_info['path'])
|
||||
Logger.warn(msg, ex)
|
||||
else:
|
||||
progress.finish_target(name)
|
||||
|
||||
@@ -210,28 +216,36 @@ class RemoteAzure(RemoteBase):
|
||||
self.blob_service.get_blob_to_path(
|
||||
bucket, key, tmp_file, progress_callback=cb)
|
||||
except Exception as exc:
|
||||
Logger.error("Failed to download '{}/{}'".format(
|
||||
bucket, key), exc)
|
||||
msg = "Failed to download '{}/{}'".format(bucket, key)
|
||||
Logger.warn(msg, exc)
|
||||
else:
|
||||
os.rename(tmp_file, to_info['path'])
|
||||
|
||||
if not no_progress_bar:
|
||||
progress.finish_target(name)
|
||||
|
||||
# FIXME: temporarily disabled because of the lack of test for external azure
|
||||
# dependencies/outputs/cache.
|
||||
#
|
||||
# def gc(self, cinfos):
|
||||
# used = [info[self.PARAM_ETAG] for info in cinfos['azure']]
|
||||
# used += [info[RemoteLOCAL.PARAM_MD5] for info in cinfos['local']]
|
||||
#
|
||||
# all_blobs = self.blob_service.list_blobs(self.bucket)
|
||||
#
|
||||
# for blob in all_blobs:
|
||||
# etag = blob.properties.etag
|
||||
# if etag in used:
|
||||
# continue
|
||||
# path_info = {'scheme': self.scheme,
|
||||
# 'key': blob.name,
|
||||
# 'bucket': self.bucket}
|
||||
# self.remove(path_info)
|
||||
def _path_to_etag(self, path):
|
||||
assert len(path.split('/')) == 2
|
||||
return posixpath.dirname(path) + posixpath.basename(path)
|
||||
|
||||
def _all(self):
|
||||
keys = self._all_keys()
|
||||
return [self._path_to_etag(key) for key in keys]
|
||||
|
||||
def gc(self, cinfos):
|
||||
from dvc.remote.local import RemoteLOCAL
|
||||
|
||||
used = [info[self.PARAM_ETAG] for info in cinfos['azure']]
|
||||
used += [info[RemoteLOCAL.PARAM_MD5] for info in cinfos['local']]
|
||||
|
||||
removed = False
|
||||
for etag in self._all():
|
||||
if etag in used:
|
||||
continue
|
||||
path_info = {'scheme': self.scheme,
|
||||
'key': posixpath.join(etag[0:2], etag[2:]),
|
||||
'bucket': self.bucket}
|
||||
self.remove(path_info)
|
||||
removed = True
|
||||
|
||||
return removed
|
||||
|
||||
+30
-6
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import re
|
||||
import errno
|
||||
from multiprocessing import cpu_count
|
||||
|
||||
from dvc.config import Config
|
||||
from dvc.logger import Logger
|
||||
@@ -30,20 +31,40 @@ class DataCloudError(DvcException):
|
||||
class RemoteBase(object):
|
||||
REGEX = None
|
||||
REQUIRES = {}
|
||||
JOBS = 4 * cpu_count()
|
||||
|
||||
def __init__(self, project, config):
|
||||
pass
|
||||
|
||||
def compat_config(config):
|
||||
return config.copy()
|
||||
|
||||
@classmethod
|
||||
def supported(cls, config):
|
||||
url = config[Config.SECTION_REMOTE_URL]
|
||||
url_ok = cls.match(url)
|
||||
url_ok = cls.match(url) is not None
|
||||
deps_ok = all(cls.REQUIRES.values())
|
||||
if url_ok and not deps_ok:
|
||||
missing = [k for k, v in cls.REQUIRES.items() if v is None]
|
||||
msg = "URL \'{}\' is supported but requires " \
|
||||
"these missing dependencies: {}"
|
||||
Logger.warn(msg.format(url, str(missing)))
|
||||
msg = "URL \'{}\' is supported but requires these missing " \
|
||||
"dependencies: {}. If you have installed dvc using pip, " \
|
||||
"choose one of these options to proceed: \n" \
|
||||
"\n" \
|
||||
" 1) Install specific missing dependencies:\n" \
|
||||
" pip install {}\n" \
|
||||
" 2) Install dvc package that includes those missing " \
|
||||
"dependencies: \n" \
|
||||
" pip install dvc[{}]\n" \
|
||||
" 3) Install dvc package with all possible " \
|
||||
"dependencies included: \n" \
|
||||
" pip install dvc[all]\n" \
|
||||
"\n" \
|
||||
"If you have installed dvc from a binary package and you " \
|
||||
"are still seeing this message, please report it to us " \
|
||||
"using https://github.com/iterative/dvc/issues. Thank you!"
|
||||
msg = msg.format(url, missing, " ".join(missing), cls.scheme)
|
||||
Logger.warn(msg)
|
||||
|
||||
return url_ok and deps_ok
|
||||
|
||||
@classmethod
|
||||
@@ -59,12 +80,15 @@ class RemoteBase(object):
|
||||
@staticmethod
|
||||
def tmp_file(fname):
|
||||
""" Temporary name for a partial download """
|
||||
# FIXME probably better use uuid()
|
||||
return fname + '.part'
|
||||
import uuid
|
||||
return fname + '.' + str(uuid.uuid4())
|
||||
|
||||
def save_info(self, path_info):
|
||||
raise NotImplementedError
|
||||
|
||||
def changed(self, path_info, checksum_info):
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, path_info):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
+67
-6
@@ -33,6 +33,13 @@ class RemoteGS(RemoteBase):
|
||||
self.url = config.get(Config.SECTION_REMOTE_URL, storagepath)
|
||||
self.projectname = config.get(Config.SECTION_GCP_PROJECTNAME, None)
|
||||
|
||||
@staticmethod
|
||||
def compat_config(config):
|
||||
ret = config.copy()
|
||||
url = 'gs://' + ret.pop(Config.SECTION_AWS_STORAGEPATH, '').lstrip('/')
|
||||
ret[Config.SECTION_REMOTE_URL] = url
|
||||
return ret
|
||||
|
||||
@property
|
||||
def bucket(self):
|
||||
return urlparse(self.url).netloc
|
||||
@@ -59,6 +66,37 @@ class RemoteGS(RemoteBase):
|
||||
return {self.PARAM_ETAG: self.get_etag(path_info['bucket'],
|
||||
path_info['key'])}
|
||||
|
||||
@staticmethod
|
||||
def to_string(path_info):
|
||||
return "{}://{}/{}".format(path_info['scheme'],
|
||||
path_info['bucket'],
|
||||
path_info['key'])
|
||||
|
||||
def changed_cache(self, etag):
|
||||
key = posixpath.join(self.prefix, etag[0:2], etag[2:])
|
||||
cache = {'scheme': 'gs', 'bucket': self.bucket, 'key': key}
|
||||
|
||||
if {self.PARAM_ETAG: etag} != self.save_info(cache):
|
||||
if self.exists([cache])[0]:
|
||||
msg = 'Corrupted cache file {}'
|
||||
Logger.warn(msg.format(self.to_string(cache)))
|
||||
self.remove(cache)
|
||||
return True
|
||||
return False
|
||||
|
||||
def changed(self, path_info, checksum_info):
|
||||
if not self.exists([path_info])[0]:
|
||||
return True
|
||||
|
||||
etag = checksum_info.get(self.PARAM_ETAG, None)
|
||||
if etag is None:
|
||||
return True
|
||||
|
||||
if self.changed_cache(etag):
|
||||
return True
|
||||
|
||||
return checksum_info != self.save_info(path_info)
|
||||
|
||||
def _copy(self, from_info, to_info, gs=None):
|
||||
gs = gs if gs else self.gs
|
||||
|
||||
@@ -92,6 +130,25 @@ class RemoteGS(RemoteBase):
|
||||
if not etag:
|
||||
return
|
||||
|
||||
if not self.changed(path_info, checksum_info):
|
||||
msg = "Data '{}' didn't change."
|
||||
Logger.info(msg.format(self.to_string(path_info)))
|
||||
return
|
||||
|
||||
if self.changed_cache(etag):
|
||||
msg = "Cache '{}' not found. File '{}' won't be created."
|
||||
Logger.warn(msg.format(etag, self.to_string(path_info)))
|
||||
return
|
||||
|
||||
if self.exists([path_info])[0]:
|
||||
msg = "Data '{}' exists. Removing before checkout."
|
||||
Logger.warn(msg.format(self.to_string(path_info)))
|
||||
self.remove(path_info)
|
||||
return
|
||||
|
||||
msg = "Checking out '{}' with cache '{}'."
|
||||
Logger.info(msg.format(self.to_string(path_info), etag))
|
||||
|
||||
key = posixpath.join(self.prefix, etag[0:2], etag[2:])
|
||||
from_info = {'scheme': 'gs', 'bucket': self.bucket, 'key': key}
|
||||
|
||||
@@ -157,9 +214,9 @@ class RemoteGS(RemoteBase):
|
||||
blob.upload_from_filename(from_info['path'])
|
||||
except Exception as exc:
|
||||
msg = "Failed to upload '{}' to '{}/{}'"
|
||||
Logger.error(msg.format(from_info['path'],
|
||||
to_info['bucket'],
|
||||
to_info['key']), exc)
|
||||
Logger.warn(msg.format(from_info['path'],
|
||||
to_info['bucket'],
|
||||
to_info['key']), exc)
|
||||
continue
|
||||
|
||||
progress.finish_target(name)
|
||||
@@ -206,9 +263,9 @@ class RemoteGS(RemoteBase):
|
||||
blob.download_to_filename(tmp_file)
|
||||
except Exception as exc:
|
||||
msg = "Failed to download '{}/{}' to '{}'"
|
||||
Logger.error(msg.format(from_info['bucket'],
|
||||
from_info['key'],
|
||||
to_info['path']), exc)
|
||||
Logger.warn(msg.format(from_info['bucket'],
|
||||
from_info['key'],
|
||||
to_info['path']), exc)
|
||||
continue
|
||||
|
||||
os.rename(tmp_file, to_info['path'])
|
||||
@@ -229,6 +286,7 @@ class RemoteGS(RemoteBase):
|
||||
used = [info[self.PARAM_ETAG] for info in cinfos['gs']]
|
||||
used += [info[RemoteLOCAL.PARAM_MD5] for info in cinfos['local']]
|
||||
|
||||
removed = False
|
||||
for etag in self._all_etags():
|
||||
if etag in used:
|
||||
continue
|
||||
@@ -237,3 +295,6 @@ class RemoteGS(RemoteBase):
|
||||
'key': posixpath.join(self.prefix,
|
||||
etag[0:2], etag[2:])}
|
||||
self.remove(path_info)
|
||||
removed = True
|
||||
|
||||
return removed
|
||||
|
||||
+66
-2
@@ -9,6 +9,7 @@ from dvc.remote.base import RemoteBase
|
||||
from dvc.remote.local import RemoteLOCAL
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.logger import Logger
|
||||
from dvc.utils import fix_env
|
||||
|
||||
|
||||
class RemoteHDFS(RemoteBase):
|
||||
@@ -28,10 +29,17 @@ class RemoteHDFS(RemoteBase):
|
||||
cmd = 'hadoop fs -' + cmd
|
||||
if user:
|
||||
cmd = 'HADOOP_USER_NAME={} '.format(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 = Popen(cmd,
|
||||
shell=True,
|
||||
close_fds=True,
|
||||
executable=os.getenv('SHELL'),
|
||||
close_fds=close_fds,
|
||||
executable=executable,
|
||||
env=fix_env(os.environ),
|
||||
stdin=PIPE,
|
||||
stdout=PIPE,
|
||||
stderr=PIPE)
|
||||
@@ -70,6 +78,39 @@ class RemoteHDFS(RemoteBase):
|
||||
|
||||
return {self.PARAM_CHECKSUM: self.checksum(path_info)}
|
||||
|
||||
@staticmethod
|
||||
def to_string(path_info):
|
||||
return "{}://{}".format(path_info['scheme'],
|
||||
path_info['url'])
|
||||
|
||||
def changed_cache(self, checksum):
|
||||
cache = {}
|
||||
cache['scheme'] = 'hdfs'
|
||||
cache['user'] = self.user
|
||||
cache['url'] = posixpath.join(self.url, checksum[0:2], checksum[2:])
|
||||
|
||||
if {self.PARAM_CHECKSUM: checksum} != self.save_info(cache):
|
||||
if self.exists([cache])[0]:
|
||||
msg = 'Corrupted cache file {}'
|
||||
Logger.warn(msg.format(self.to_string(cache)))
|
||||
self.remove(cache)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def changed(self, path_info, checksum_info):
|
||||
if not self.exists([path_info])[0]:
|
||||
return True
|
||||
|
||||
checksum = checksum_info.get(self.PARAM_CHECKSUM, None)
|
||||
if checksum is None:
|
||||
return True
|
||||
|
||||
if self.changed_cache(self, checksum):
|
||||
return True
|
||||
|
||||
return checksum_info != self.save_info(path_info)
|
||||
|
||||
def save(self, path_info):
|
||||
if path_info['scheme'] != 'hdfs':
|
||||
raise NotImplementedError
|
||||
@@ -94,6 +135,25 @@ class RemoteHDFS(RemoteBase):
|
||||
if not checksum:
|
||||
return
|
||||
|
||||
if not self.changed(path_info, checksum_info):
|
||||
msg = "Data '{}' didn't change."
|
||||
Logger.info(msg.format(self.to_string(path_info)))
|
||||
return
|
||||
|
||||
if self.changed_cache(checksum):
|
||||
msg = "Cache '{}' not found. File '{}' won't be created."
|
||||
Logger.warn(msg.format(checksum, self.to_string(path_info)))
|
||||
return
|
||||
|
||||
if self.exists([path_info])[0]:
|
||||
msg = "Data '{}' exists. Removing before checkout."
|
||||
Logger.warn(msg.format(self.to_string(path_info)))
|
||||
self.remove(path_info)
|
||||
return
|
||||
|
||||
msg = "Checking out '{}' with cache '{}'."
|
||||
Logger.info(msg.format(self.to_string(path_info), checksum))
|
||||
|
||||
src = path_info.copy()
|
||||
src['url'] = posixpath.join(self.url, checksum[0:2], checksum[2:])
|
||||
|
||||
@@ -197,6 +257,7 @@ class RemoteHDFS(RemoteBase):
|
||||
used = [info[self.PARAM_CHECKSUM] for info in cinfos['hdfs']]
|
||||
used += [info[RemoteLOCAL.PARAM_MD5] for info in cinfos['local']]
|
||||
|
||||
removed = False
|
||||
for checksum in self._all_checksums():
|
||||
if checksum in used:
|
||||
continue
|
||||
@@ -205,3 +266,6 @@ class RemoteHDFS(RemoteBase):
|
||||
'url': posixpath.join(self.url,
|
||||
checksum[0:2], checksum[2:])}
|
||||
self.remove(path_info)
|
||||
removed = True
|
||||
|
||||
return removed
|
||||
|
||||
+322
-85
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import stat
|
||||
import uuid
|
||||
import json
|
||||
import ntpath
|
||||
@@ -9,7 +10,8 @@ from operator import itemgetter
|
||||
from dvc.system import System
|
||||
from dvc.remote.base import RemoteBase, STATUS_MAP
|
||||
from dvc.logger import Logger
|
||||
from dvc.utils import remove, move, copyfile, file_md5, dict_md5, to_chunks
|
||||
from dvc.utils import remove, move, copyfile, dict_md5, to_chunks
|
||||
from dvc.utils import LARGE_DIR_SIZE
|
||||
from dvc.config import Config
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.progress import progress
|
||||
@@ -20,6 +22,7 @@ class RemoteLOCAL(RemoteBase):
|
||||
scheme = ''
|
||||
REGEX = r'^(?P<path>(/+|.:\\+).*)$'
|
||||
PARAM_MD5 = 'md5'
|
||||
PARAM_PATH = 'path'
|
||||
PARAM_RELPATH = 'relpath'
|
||||
MD5_DIR_SUFFIX = '.dir'
|
||||
|
||||
@@ -34,7 +37,7 @@ class RemoteLOCAL(RemoteBase):
|
||||
def __init__(self, project, config):
|
||||
self.project = project
|
||||
self.state = self.project.state
|
||||
self.link_state = project.link_state
|
||||
self.protected = config.get(Config.SECTION_CACHE_PROTECTED, False)
|
||||
storagepath = config.get(Config.SECTION_AWS_STORAGEPATH, None)
|
||||
self.cache_dir = config.get(Config.SECTION_REMOTE_URL, storagepath)
|
||||
|
||||
@@ -49,6 +52,17 @@ class RemoteLOCAL(RemoteBase):
|
||||
if self.cache_dir is not None and not os.path.exists(self.cache_dir):
|
||||
os.mkdir(self.cache_dir)
|
||||
|
||||
@staticmethod
|
||||
def compat_config(config):
|
||||
ret = config.copy()
|
||||
url = ret.pop(Config.SECTION_AWS_STORAGEPATH, '')
|
||||
ret[Config.SECTION_REMOTE_URL] = url
|
||||
return ret
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.cache_dir
|
||||
|
||||
@property
|
||||
def prefix(self):
|
||||
return self.cache_dir
|
||||
@@ -76,14 +90,35 @@ class RemoteLOCAL(RemoteBase):
|
||||
relpath = os.path.relpath(path, self.cache_dir)
|
||||
return os.path.dirname(relpath) + os.path.basename(relpath)
|
||||
|
||||
def changed(self, md5):
|
||||
def changed_cache_file(self, md5):
|
||||
cache = self.get(md5)
|
||||
if self.state.changed(cache, md5=md5):
|
||||
if os.path.exists(cache):
|
||||
msg = 'Corrupted cache file {}'
|
||||
msg = 'Corrupted cache file {}.'
|
||||
Logger.warn(msg.format(os.path.relpath(cache)))
|
||||
remove(cache)
|
||||
return True
|
||||
return False
|
||||
|
||||
def changed_cache(self, md5):
|
||||
cache = self.get(md5)
|
||||
clist = [(cache, md5)]
|
||||
|
||||
while True:
|
||||
if len(clist) == 0:
|
||||
break
|
||||
|
||||
cache, md5 = clist.pop()
|
||||
if self.changed_cache_file(md5):
|
||||
return True
|
||||
|
||||
if not self.is_dir_cache(cache):
|
||||
continue
|
||||
|
||||
for entry in self.load_dir_cache(md5):
|
||||
md5 = entry[self.PARAM_MD5]
|
||||
cache = self.get(md5)
|
||||
clist.append((cache, md5))
|
||||
|
||||
return False
|
||||
|
||||
@@ -94,12 +129,36 @@ class RemoteLOCAL(RemoteBase):
|
||||
if not os.path.exists(dname):
|
||||
os.makedirs(dname)
|
||||
|
||||
# NOTE: just create an empty file for an empty cache
|
||||
if os.path.getsize(cache) == 0:
|
||||
open(path, 'w+').close()
|
||||
|
||||
msg = "Created empty file: {} -> {}".format(
|
||||
os.path.relpath(cache),
|
||||
os.path.relpath(path),
|
||||
)
|
||||
|
||||
Logger.debug(msg)
|
||||
return
|
||||
|
||||
i = len(self.cache_types)
|
||||
while i > 0:
|
||||
try:
|
||||
self.CACHE_TYPE_MAP[self.cache_types[0]](cache, path)
|
||||
|
||||
if self.protected:
|
||||
os.chmod(path, stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH)
|
||||
|
||||
msg = "Created {}'{}': {} -> {}".format(
|
||||
'protected ' if self.protected else '',
|
||||
self.cache_types[0],
|
||||
os.path.relpath(cache),
|
||||
os.path.relpath(path)
|
||||
)
|
||||
|
||||
Logger.debug(msg)
|
||||
return
|
||||
except Exception as exc:
|
||||
except DvcException as exc:
|
||||
msg = 'Cache type \'{}\' is not supported: {}'
|
||||
Logger.debug(msg.format(self.cache_types[0], str(exc)))
|
||||
del self.cache_types[0]
|
||||
@@ -129,24 +188,38 @@ class RemoteLOCAL(RemoteBase):
|
||||
dir_info = []
|
||||
|
||||
for root, dirs, files in os.walk(dname):
|
||||
bar = False
|
||||
|
||||
if len(files) > LARGE_DIR_SIZE:
|
||||
msg = "Computing md5 for a large directory {}. " \
|
||||
"This is only done once."
|
||||
Logger.info(msg.format(os.path.relpath(root)))
|
||||
bar = True
|
||||
title = os.path.relpath(root)
|
||||
processed = 0
|
||||
total = len(files)
|
||||
progress.update_target(title, 0, total)
|
||||
|
||||
for fname in files:
|
||||
path = os.path.join(root, fname)
|
||||
relpath = self.unixpath(os.path.relpath(path, dname))
|
||||
|
||||
# FIXME: we could've used md5 = state.update(path, dump=False)
|
||||
# here, but it is around twice as slow(on ssd, don't know about
|
||||
# hdd) for a directory with small files. What we could do here
|
||||
# is introduce some kind of a limit for file size, after which
|
||||
# we would actually register it in our state file.
|
||||
md5 = file_md5(path)[0]
|
||||
if bar:
|
||||
progress.update_target(title, processed, total)
|
||||
processed += 1
|
||||
|
||||
md5 = self.state.update(path)
|
||||
dir_info.append({self.PARAM_RELPATH: relpath,
|
||||
self.PARAM_MD5: md5})
|
||||
|
||||
if bar:
|
||||
progress.finish_target(title)
|
||||
|
||||
# NOTE: sorting the list by path to ensure reproducibility
|
||||
dir_info = sorted(dir_info, key=itemgetter(self.PARAM_RELPATH))
|
||||
|
||||
md5 = dict_md5(dir_info) + self.MD5_DIR_SUFFIX
|
||||
if self.changed(md5):
|
||||
if self.changed_cache(md5):
|
||||
self.dump_dir_cache(md5, dir_info)
|
||||
|
||||
return (md5, dir_info)
|
||||
@@ -197,7 +270,7 @@ class RemoteLOCAL(RemoteBase):
|
||||
|
||||
def checkout(self, path_info, checksum_info):
|
||||
path = path_info['path']
|
||||
md5 = checksum_info.get(self.PARAM_MD5, None)
|
||||
md5 = checksum_info.get(self.PARAM_MD5)
|
||||
cache = self.get(md5)
|
||||
|
||||
if not cache:
|
||||
@@ -205,23 +278,29 @@ class RemoteLOCAL(RemoteBase):
|
||||
Logger.warn(msg.format(os.path.relpath(path)))
|
||||
return
|
||||
|
||||
if self.changed(md5):
|
||||
if not self.changed(path_info, checksum_info):
|
||||
msg = "Data '{}' didn't change."
|
||||
Logger.info(msg.format(os.path.relpath(path)))
|
||||
return
|
||||
|
||||
if self.changed_cache(md5):
|
||||
msg = u'Cache \'{}\' not found. File \'{}\' won\'t be created.'
|
||||
Logger.warn(msg.format(md5, os.path.relpath(path)))
|
||||
remove(path)
|
||||
return
|
||||
|
||||
if os.path.exists(path):
|
||||
msg = u'Data \'{}\' exists. Removing before checkout'
|
||||
Logger.debug(msg.format(os.path.relpath(path)))
|
||||
remove(path)
|
||||
|
||||
msg = u'Checking out \'{}\' with cache \'{}\''
|
||||
Logger.debug(msg.format(os.path.relpath(path), md5))
|
||||
msg = u'Checking out \'{}\' with cache \'{}\'.'
|
||||
Logger.info(msg.format(os.path.relpath(path), md5))
|
||||
|
||||
if not self.is_dir_cache(cache):
|
||||
|
||||
if os.path.exists(path):
|
||||
msg = u'File \'{}\' exists. Removing before checkout.'
|
||||
Logger.warn(msg.format(os.path.relpath(path)))
|
||||
remove(path)
|
||||
|
||||
self.link(cache, path)
|
||||
self.link_state.update(path)
|
||||
self.state.update_link(path)
|
||||
return
|
||||
|
||||
# Create dir separately so that dir is created
|
||||
@@ -229,13 +308,58 @@ class RemoteLOCAL(RemoteBase):
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
for entry in self.load_dir_cache(md5):
|
||||
md5 = entry[self.PARAM_MD5]
|
||||
c = self.get(md5)
|
||||
dir_info = self.load_dir_cache(md5)
|
||||
dir_relpath = os.path.relpath(path)
|
||||
dir_size = len(dir_info)
|
||||
bar = dir_size > LARGE_DIR_SIZE
|
||||
|
||||
Logger.info("Linking directory '{}'.".format(dir_relpath))
|
||||
|
||||
for processed, entry in enumerate(dir_info):
|
||||
relpath = entry[self.PARAM_RELPATH]
|
||||
m = entry[self.PARAM_MD5]
|
||||
p = os.path.join(path, relpath)
|
||||
self.link(c, p)
|
||||
self.link_state.update(path)
|
||||
c = self.get(m)
|
||||
|
||||
entry_info = {
|
||||
'scheme': path_info['scheme'],
|
||||
self.PARAM_PATH: p,
|
||||
}
|
||||
|
||||
entry_checksum_info = {
|
||||
self.PARAM_MD5: m,
|
||||
}
|
||||
|
||||
if self.changed(entry_info, entry_checksum_info):
|
||||
remove(p)
|
||||
self.link(c, p)
|
||||
|
||||
if bar:
|
||||
progress.update_target(dir_relpath, processed, dir_size)
|
||||
|
||||
self._discard_working_directory_changes(path, dir_info)
|
||||
|
||||
self.state.update_link(path)
|
||||
|
||||
if bar:
|
||||
progress.finish_target(dir_relpath)
|
||||
|
||||
def _discard_working_directory_changes(self, path, dir_info):
|
||||
working_dir_files = set(
|
||||
os.path.join(root, file)
|
||||
for root, _, files in os.walk(path)
|
||||
for file in files
|
||||
)
|
||||
|
||||
cached_files = set(
|
||||
os.path.join(path, file['relpath'])
|
||||
for file in dir_info
|
||||
)
|
||||
|
||||
delta = working_dir_files - cached_files
|
||||
|
||||
for file in delta:
|
||||
remove(file)
|
||||
|
||||
def _move(self, inp, outp):
|
||||
# moving in two stages to make last the move atomic in
|
||||
@@ -251,34 +375,45 @@ class RemoteLOCAL(RemoteBase):
|
||||
|
||||
cache = self.get(md5)
|
||||
|
||||
if self.changed(md5):
|
||||
if self.changed_cache(md5):
|
||||
self._move(path, cache)
|
||||
else:
|
||||
remove(path)
|
||||
|
||||
self.link(cache, path)
|
||||
self.link_state.update(path)
|
||||
self.state.update_link(path)
|
||||
|
||||
return {self.PARAM_MD5: md5}
|
||||
|
||||
def _save_dir(self, path_info):
|
||||
path = path_info['path']
|
||||
md5, dir_info = self.state.update_info(path)
|
||||
dir_relpath = os.path.relpath(path)
|
||||
dir_size = len(dir_info)
|
||||
bar = dir_size > LARGE_DIR_SIZE
|
||||
|
||||
for entry in dir_info:
|
||||
Logger.info("Linking directory '{}'.".format(dir_relpath))
|
||||
|
||||
for processed, entry in enumerate(dir_info):
|
||||
relpath = entry[self.PARAM_RELPATH]
|
||||
m = entry[self.PARAM_MD5]
|
||||
p = os.path.join(path, relpath)
|
||||
c = self.get(m)
|
||||
|
||||
if self.changed(m):
|
||||
if self.changed_cache(m):
|
||||
self._move(p, c)
|
||||
else:
|
||||
remove(p)
|
||||
|
||||
self.link(c, p)
|
||||
|
||||
self.link_state.update(path)
|
||||
if bar:
|
||||
progress.update_target(dir_relpath, processed, dir_size)
|
||||
|
||||
self.state.update_link(path)
|
||||
|
||||
if bar:
|
||||
progress.finish_target(dir_relpath)
|
||||
|
||||
return {self.PARAM_MD5: md5}
|
||||
|
||||
@@ -288,6 +423,10 @@ class RemoteLOCAL(RemoteBase):
|
||||
|
||||
path = path_info['path']
|
||||
|
||||
msg = "Saving '{}' to cache '{}'."
|
||||
Logger.info(msg.format(os.path.relpath(path),
|
||||
os.path.relpath(self.cache_dir)))
|
||||
|
||||
if os.path.isdir(path):
|
||||
return self._save_dir(path_info)
|
||||
else:
|
||||
@@ -299,6 +438,19 @@ class RemoteLOCAL(RemoteBase):
|
||||
|
||||
return {self.PARAM_MD5: self.state.update(path_info['path'])}
|
||||
|
||||
def changed(self, path_info, checksum_info):
|
||||
if not self.exists([path_info])[0]:
|
||||
return True
|
||||
|
||||
md5 = checksum_info.get(self.PARAM_MD5, None)
|
||||
if md5 is None:
|
||||
return True
|
||||
|
||||
if self.changed_cache(md5):
|
||||
return True
|
||||
|
||||
return checksum_info != self.save_info(path_info)
|
||||
|
||||
def remove(self, path_info):
|
||||
if path_info['scheme'] != 'local':
|
||||
raise NotImplementedError
|
||||
@@ -344,8 +496,8 @@ class RemoteLOCAL(RemoteBase):
|
||||
copyfile(from_info['path'], to_info['path'], name=name)
|
||||
except Exception as exc:
|
||||
msg = "Failed to upload '{}' tp '{}'"
|
||||
Logger.error(msg.format(from_info['path'],
|
||||
to_info['path']), exc)
|
||||
Logger.warn(msg.format(from_info['path'],
|
||||
to_info['path']), exc)
|
||||
|
||||
def download(self,
|
||||
from_infos,
|
||||
@@ -376,123 +528,208 @@ class RemoteLOCAL(RemoteBase):
|
||||
name=name)
|
||||
except Exception as exc:
|
||||
msg = "Failed to download '{}' to '{}'"
|
||||
Logger.error(msg.format(from_info['path'],
|
||||
to_info['path']), exc)
|
||||
Logger.warn(msg.format(from_info['path'],
|
||||
to_info['path']), exc)
|
||||
continue
|
||||
|
||||
os.rename(tmp_file, to_info['path'])
|
||||
|
||||
def _collect(self, checksum_infos):
|
||||
missing = []
|
||||
collected = []
|
||||
def _group(self, checksum_infos, show_checksums=False):
|
||||
by_md5 = {}
|
||||
|
||||
for info in checksum_infos:
|
||||
md5 = info[self.PARAM_MD5]
|
||||
cache = self.get(md5)
|
||||
if not self.is_dir_cache(info[self.PARAM_MD5]):
|
||||
|
||||
if show_checksums:
|
||||
by_md5[md5] = md5
|
||||
continue
|
||||
if not os.path.exists(cache):
|
||||
missing.append(info)
|
||||
continue
|
||||
collected.extend(self.load_dir_cache(md5))
|
||||
collected.extend(checksum_infos)
|
||||
return collected, missing
|
||||
|
||||
name = info[self.PARAM_PATH]
|
||||
branch = info.get('branch')
|
||||
if branch:
|
||||
name += '({})'.format(branch)
|
||||
|
||||
if md5 not in by_md5.keys():
|
||||
by_md5[md5] = ''
|
||||
else:
|
||||
by_md5[md5] += ' '
|
||||
|
||||
by_md5[md5] += name
|
||||
|
||||
return list(by_md5.keys()), list(by_md5.values())
|
||||
|
||||
def gc(self, checksum_infos):
|
||||
checksum_infos = self._collect(checksum_infos['local'])[0]
|
||||
checksum_infos = checksum_infos['local']
|
||||
used_md5s = [info[self.PARAM_MD5] for info in checksum_infos]
|
||||
|
||||
removed = False
|
||||
for md5 in self.all():
|
||||
if md5 in used_md5s:
|
||||
continue
|
||||
remove(self.get(md5))
|
||||
removed = True
|
||||
|
||||
return removed
|
||||
|
||||
def status(self, checksum_infos, remote, jobs=None, show_checksums=False):
|
||||
Logger.info("Preparing to pull data from {}".format(remote.url))
|
||||
title = "Collecting information"
|
||||
|
||||
progress.set_n_total(1)
|
||||
progress.update_target(title, 0, 100)
|
||||
|
||||
progress.update_target(title, 10, 100)
|
||||
|
||||
md5s, names = self._group(checksum_infos,
|
||||
show_checksums=show_checksums)
|
||||
|
||||
progress.update_target(title, 20, 100)
|
||||
|
||||
def status(self, checksum_infos, remote, jobs=1):
|
||||
checksum_infos = self._collect(checksum_infos)[0]
|
||||
md5s = [info[self.PARAM_MD5] for info in checksum_infos]
|
||||
path_infos = remote.md5s_to_path_infos(md5s)
|
||||
|
||||
progress.update_target(title, 30, 100)
|
||||
|
||||
remote_exists = remote.exists(path_infos)
|
||||
local_exists = [not self.changed(md5) for md5 in md5s]
|
||||
|
||||
return [(md5, STATUS_MAP[l, r]) for md5, l, r in zip(md5s,
|
||||
local_exists,
|
||||
remote_exists)]
|
||||
progress.update_target(title, 90, 100)
|
||||
|
||||
def _do_pull(self, checksum_infos, remote, jobs=1, no_progress_bar=False):
|
||||
md5s = [info[self.PARAM_MD5] for info in checksum_infos]
|
||||
local_exists = [not self.changed_cache_file(md5) for md5 in md5s]
|
||||
|
||||
progress.finish_target(title)
|
||||
|
||||
return [(name, STATUS_MAP[l, r]) for name, l, r in zip(names,
|
||||
local_exists,
|
||||
remote_exists)]
|
||||
|
||||
def pull(self,
|
||||
checksum_infos,
|
||||
remote,
|
||||
jobs=None,
|
||||
show_checksums=False):
|
||||
title = "Collecting information"
|
||||
|
||||
progress.set_n_total(1)
|
||||
progress.update_target(title, 0, 100)
|
||||
|
||||
grouped = zip(*self._group(checksum_infos,
|
||||
show_checksums=show_checksums))
|
||||
|
||||
progress.update_target(title, 10, 100)
|
||||
|
||||
md5s = []
|
||||
names = []
|
||||
# NOTE: filter files that are not corrupted
|
||||
md5s = list(filter(lambda md5: self.changed(md5), md5s))
|
||||
for md5, name in grouped:
|
||||
if self.changed_cache_file(md5):
|
||||
md5s.append(md5)
|
||||
names.append(name)
|
||||
|
||||
progress.update_target(title, 30, 100)
|
||||
|
||||
cache = [{'scheme': 'local', 'path': self.get(md5)} for md5 in md5s]
|
||||
|
||||
progress.update_target(title, 50, 100)
|
||||
|
||||
path_infos = remote.md5s_to_path_infos(md5s)
|
||||
|
||||
assert len(path_infos) == len(cache) == len(md5s)
|
||||
progress.update_target(title, 60, 100)
|
||||
|
||||
# NOTE: dummy call to try to establish a connection
|
||||
# to see if we need to ask user for a password.
|
||||
remote.exists(remote.md5s_to_path_infos(['000']))
|
||||
|
||||
progress.update_target(title, 70, 100)
|
||||
|
||||
assert len(path_infos) == len(cache) == len(md5s) == len(names)
|
||||
|
||||
if jobs is None:
|
||||
jobs = remote.JOBS
|
||||
|
||||
chunks = list(zip(to_chunks(path_infos, jobs),
|
||||
to_chunks(cache, jobs),
|
||||
to_chunks(md5s, jobs)))
|
||||
to_chunks(names, jobs)))
|
||||
|
||||
progress.set_n_total(len(md5s))
|
||||
progress.finish_target(title)
|
||||
|
||||
progress.set_n_total(len(names))
|
||||
|
||||
if len(chunks) == 0:
|
||||
return
|
||||
|
||||
futures = []
|
||||
with ThreadPoolExecutor(max_workers=len(chunks)) as executor:
|
||||
for from_infos, to_infos, md5s in chunks:
|
||||
for from_infos, to_infos, names in chunks:
|
||||
res = executor.submit(remote.download,
|
||||
from_infos,
|
||||
to_infos,
|
||||
names=md5s,
|
||||
no_progress_bar=no_progress_bar)
|
||||
names=names)
|
||||
futures.append(res)
|
||||
|
||||
for f in futures:
|
||||
f.result()
|
||||
|
||||
def pull(self, checksum_infos, remote, jobs=1):
|
||||
# NOTE: try fetching missing dir info
|
||||
checksum_infos, missing = self._collect(checksum_infos)
|
||||
if len(missing) > 0:
|
||||
self._do_pull(missing, remote, jobs, no_progress_bar=True)
|
||||
checksum_infos += self._collect(missing)[0]
|
||||
def push(self, checksum_infos, remote, jobs=None, show_checksums=False):
|
||||
Logger.info("Preparing to push data to {}".format(remote.url))
|
||||
title = "Collecting information"
|
||||
|
||||
self._do_pull(checksum_infos, remote, jobs)
|
||||
|
||||
def push(self, checksum_infos, remote, jobs=1):
|
||||
checksum_infos = self._collect(checksum_infos)[0]
|
||||
md5s = [info[self.PARAM_MD5] for info in checksum_infos]
|
||||
progress.set_n_total(1)
|
||||
progress.update_target(title, 0, 100)
|
||||
|
||||
# NOTE: verifying that our cache is not corrupted
|
||||
md5s = list(filter(lambda md5: not self.changed(md5), md5s))
|
||||
def func(info):
|
||||
return not self.changed_cache_file(info[self.PARAM_MD5])
|
||||
checksum_infos = list(filter(func, checksum_infos))
|
||||
|
||||
progress.update_target(title, 20, 100)
|
||||
|
||||
# NOTE: filter files that are already uploaded
|
||||
path_infos = remote.md5s_to_path_infos(md5s)
|
||||
lexists = remote.exists(path_infos)
|
||||
md5s_exist = filter(lambda x: not x[1], list(zip(md5s, lexists)))
|
||||
md5s = [md5 for md5, exists in md5s_exist]
|
||||
md5s = [i[self.PARAM_MD5] for i in checksum_infos]
|
||||
exists = remote.exists(remote.md5s_to_path_infos(md5s))
|
||||
|
||||
progress.update_target(title, 30, 100)
|
||||
|
||||
def func(entry):
|
||||
return not entry[0]
|
||||
|
||||
assert len(exists) == len(checksum_infos)
|
||||
infos_exist = list(filter(func, zip(exists, checksum_infos)))
|
||||
checksum_infos = [i for e, i in infos_exist]
|
||||
|
||||
progress.update_target(title, 70, 100)
|
||||
|
||||
md5s, names = self._group(checksum_infos,
|
||||
show_checksums=show_checksums)
|
||||
cache = [{'scheme': 'local', 'path': self.get(md5)} for md5 in md5s]
|
||||
|
||||
progress.update_target(title, 80, 100)
|
||||
|
||||
path_infos = remote.md5s_to_path_infos(md5s)
|
||||
|
||||
assert len(path_infos) == len(cache) == len(md5s)
|
||||
assert len(path_infos) == len(cache) == len(md5s) == len(names)
|
||||
|
||||
progress.update_target(title, 90, 100)
|
||||
|
||||
if jobs is None:
|
||||
jobs = remote.JOBS
|
||||
|
||||
chunks = list(zip(to_chunks(path_infos, jobs),
|
||||
to_chunks(cache, jobs),
|
||||
to_chunks(md5s, jobs)))
|
||||
to_chunks(names, jobs)))
|
||||
|
||||
progress.set_n_total(len(md5s))
|
||||
progress.finish_target(title)
|
||||
|
||||
progress.set_n_total(len(names))
|
||||
|
||||
if len(chunks) == 0:
|
||||
return
|
||||
|
||||
futures = []
|
||||
with ThreadPoolExecutor(max_workers=len(chunks)) as executor:
|
||||
for to_infos, from_infos, md5s in chunks:
|
||||
for to_infos, from_infos, names in chunks:
|
||||
res = executor.submit(remote.upload,
|
||||
from_infos,
|
||||
to_infos,
|
||||
names=md5s)
|
||||
names=names)
|
||||
futures.append(res)
|
||||
|
||||
for f in futures:
|
||||
|
||||
+94
-32
@@ -40,26 +40,36 @@ class RemoteS3(RemoteBase):
|
||||
PARAM_ETAG = 'etag'
|
||||
|
||||
def __init__(self, project, config):
|
||||
import configobj
|
||||
|
||||
self.project = project
|
||||
storagepath = 's3://' + config.get(Config.SECTION_AWS_STORAGEPATH,
|
||||
'').lstrip('/')
|
||||
|
||||
storagepath = 's3://{}'.format(
|
||||
config.get(Config.SECTION_AWS_STORAGEPATH, '').lstrip('/')
|
||||
)
|
||||
|
||||
self.url = config.get(Config.SECTION_REMOTE_URL, storagepath)
|
||||
self.region = config.get(Config.SECTION_AWS_REGION, None)
|
||||
self.profile = config.get(Config.SECTION_AWS_PROFILE, 'default')
|
||||
self.endpoint_url = config.get(Config.SECTION_AWS_ENDPOINT_URL, None)
|
||||
|
||||
credentialpath = config.get(Config.SECTION_AWS_CREDENTIALPATH, None)
|
||||
if credentialpath:
|
||||
creds_conf = configobj.ConfigObj(credentialpath)
|
||||
creds = creds_conf.get(self.profile, {})
|
||||
else:
|
||||
creds = {}
|
||||
self.region = (
|
||||
os.environ.get('AWS_DEFAULT_REGION') or
|
||||
config.get(Config.SECTION_AWS_REGION)
|
||||
)
|
||||
|
||||
self.region = creds.get('region', self.region)
|
||||
self.aws_access_key_id = creds.get('aws_access_key_id', None)
|
||||
self.aws_secret_access_key = creds.get('aws_secret_access_key', None)
|
||||
self.profile = (
|
||||
os.environ.get('AWS_PROFILE') or
|
||||
config.get(Config.SECTION_AWS_PROFILE)
|
||||
)
|
||||
|
||||
self.endpoint_url = config.get(Config.SECTION_AWS_ENDPOINT_URL)
|
||||
|
||||
shared_creds = config.get(Config.SECTION_AWS_CREDENTIALPATH)
|
||||
if shared_creds:
|
||||
os.environ.setdefault('AWS_SHARED_CREDENTIALS_FILE', shared_creds)
|
||||
|
||||
@staticmethod
|
||||
def compat_config(config):
|
||||
ret = config.copy()
|
||||
url = 's3://' + ret.pop(Config.SECTION_AWS_STORAGEPATH, '').lstrip('/')
|
||||
ret[Config.SECTION_REMOTE_URL] = url
|
||||
return ret
|
||||
|
||||
@property
|
||||
def bucket(self):
|
||||
@@ -71,12 +81,10 @@ class RemoteS3(RemoteBase):
|
||||
|
||||
@property
|
||||
def s3(self):
|
||||
session = boto3.session.Session(profile_name=self.profile)
|
||||
return session.client('s3',
|
||||
aws_access_key_id=self.aws_access_key_id,
|
||||
aws_secret_access_key=self.aws_secret_access_key,
|
||||
region_name=self.region,
|
||||
endpoint_url=self.endpoint_url)
|
||||
session = boto3.session.Session(profile_name=self.profile,
|
||||
region_name=self.region)
|
||||
|
||||
return session.client('s3', endpoint_url=self.endpoint_url)
|
||||
|
||||
def get_etag(self, bucket, key):
|
||||
try:
|
||||
@@ -93,6 +101,19 @@ class RemoteS3(RemoteBase):
|
||||
return {self.PARAM_ETAG: self.get_etag(path_info['bucket'],
|
||||
path_info['key'])}
|
||||
|
||||
def changed(self, path_info, checksum_info):
|
||||
if not self.exists([path_info])[0]:
|
||||
return True
|
||||
|
||||
etag = checksum_info.get(self.PARAM_ETAG, None)
|
||||
if etag is None:
|
||||
return True
|
||||
|
||||
if self.changed_cache(etag):
|
||||
return True
|
||||
|
||||
return checksum_info != self.save_info(path_info)
|
||||
|
||||
def _copy(self, from_info, to_info, s3=None):
|
||||
s3 = s3 if s3 else self.s3
|
||||
|
||||
@@ -112,6 +133,23 @@ class RemoteS3(RemoteBase):
|
||||
|
||||
return {self.PARAM_ETAG: etag}
|
||||
|
||||
@staticmethod
|
||||
def to_string(path_info):
|
||||
return "s3://{}/{}".format(path_info['bucket'], path_info['key'])
|
||||
|
||||
def changed_cache(self, etag):
|
||||
key = posixpath.join(self.prefix, etag[0:2], etag[2:])
|
||||
cache = {'scheme': 's3', 'bucket': self.bucket, 'key': key}
|
||||
|
||||
if {self.PARAM_ETAG: etag} != self.save_info(cache):
|
||||
if self.exists([cache])[0]:
|
||||
msg = 'Corrupted cache file {}'
|
||||
Logger.warn(msg.format(self.to_string(cache)))
|
||||
self.remove(cache)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def checkout(self, path_info, checksum_info):
|
||||
if path_info['scheme'] != 's3':
|
||||
raise NotImplementedError
|
||||
@@ -120,6 +158,25 @@ class RemoteS3(RemoteBase):
|
||||
if not etag:
|
||||
return
|
||||
|
||||
if not self.changed(path_info, checksum_info):
|
||||
msg = "Data '{}' didn't change."
|
||||
Logger.info(msg.format(self.to_string(path_info)))
|
||||
return
|
||||
|
||||
if self.changed_cache(etag):
|
||||
msg = "Cache '{}' not found. File '{}' won't be created."
|
||||
Logger.warn(msg.format(etag, self.to_string(path_info)))
|
||||
return
|
||||
|
||||
if self.exists([path_info])[0]:
|
||||
msg = "Data '{}' exists. Removing before checkout."
|
||||
Logger.warn(msg.format(self.to_string(path_info)))
|
||||
self.remove(path_info)
|
||||
return
|
||||
|
||||
msg = "Checking out '{}' with cache '{}'."
|
||||
Logger.info(msg.format(self.to_string(path_info), etag))
|
||||
|
||||
key = posixpath.join(self.prefix, etag[0:2], etag[2:])
|
||||
from_info = {'scheme': 's3', 'bucket': self.bucket, 'key': key}
|
||||
|
||||
@@ -210,7 +267,7 @@ class RemoteS3(RemoteBase):
|
||||
Callback=cb)
|
||||
except Exception as exc:
|
||||
msg = "Failed to upload '{}'".format(from_info['path'])
|
||||
Logger.error(msg, exc)
|
||||
Logger.warn(msg, exc)
|
||||
continue
|
||||
|
||||
progress.finish_target(name)
|
||||
@@ -244,16 +301,17 @@ class RemoteS3(RemoteBase):
|
||||
if not name:
|
||||
name = os.path.basename(to_info['path'])
|
||||
|
||||
if no_progress_bar:
|
||||
cb = None
|
||||
else:
|
||||
total = s3.head_object(Bucket=from_info['bucket'],
|
||||
Key=from_info['key'])['ContentLength']
|
||||
cb = Callback(name, total)
|
||||
|
||||
self._makedirs(to_info['path'])
|
||||
|
||||
try:
|
||||
if no_progress_bar:
|
||||
cb = None
|
||||
else:
|
||||
total = s3.head_object(
|
||||
Bucket=from_info['bucket'],
|
||||
Key=from_info['key'])['ContentLength']
|
||||
cb = Callback(name, total)
|
||||
|
||||
s3.download_file(from_info['bucket'],
|
||||
from_info['key'],
|
||||
tmp_file,
|
||||
@@ -261,8 +319,8 @@ class RemoteS3(RemoteBase):
|
||||
except Exception as exc:
|
||||
msg = "Failed to download '{}/{}'".format(from_info['bucket'],
|
||||
from_info['key'])
|
||||
Logger.error(msg, exc)
|
||||
return
|
||||
Logger.warn(msg, exc)
|
||||
continue
|
||||
|
||||
os.rename(tmp_file, to_info['path'])
|
||||
|
||||
@@ -281,6 +339,7 @@ class RemoteS3(RemoteBase):
|
||||
used_etags = [info[self.PARAM_ETAG] for info in cinfos['s3']]
|
||||
used_etags += [info[RemoteLOCAL.PARAM_MD5] for info in cinfos['local']]
|
||||
|
||||
removed = False
|
||||
for etag in self._all():
|
||||
if etag in used_etags:
|
||||
continue
|
||||
@@ -289,3 +348,6 @@ class RemoteS3(RemoteBase):
|
||||
etag[0:2], etag[2:]),
|
||||
'bucket': self.bucket}
|
||||
self.remove(path_info)
|
||||
removed = True
|
||||
|
||||
return removed
|
||||
|
||||
+155
-20
@@ -44,8 +44,14 @@ class RemoteSSH(RemoteBase):
|
||||
REGEX = r'^ssh://((?P<user>.*)@)?(?P<host>[^/]*):(?P<path>/.*)$'
|
||||
|
||||
REQUIRES = {'paramiko': paramiko}
|
||||
|
||||
JOBS = 4
|
||||
|
||||
PARAM_MD5 = 'md5'
|
||||
|
||||
DEFAULT_PORT = 22
|
||||
TIMEOUT = 1800
|
||||
|
||||
def __init__(self, project, config):
|
||||
self.project = project
|
||||
self.url = config.get(Config.SECTION_REMOTE_URL, '/')
|
||||
@@ -55,30 +61,49 @@ class RemoteSSH(RemoteBase):
|
||||
self.user = config.get(Config.SECTION_REMOTE_USER,
|
||||
getpass.getuser())
|
||||
self.prefix = self.group('path')
|
||||
self.port = config.get(Config.SECTION_REMOTE_PORT, self.DEFAULT_PORT)
|
||||
self.keyfile = config.get(Config.SECTION_REMOTE_KEY_FILE, None)
|
||||
self.timeout = config.get(Config.SECTION_REMOTE_TIMEOUT, self.TIMEOUT)
|
||||
self.password = config.get(Config.SECTION_REMOTE_PASSWORD, None)
|
||||
self.ask_password = config.get(Config.SECTION_REMOTE_ASK_PASSWORD,
|
||||
False)
|
||||
|
||||
def md5s_to_path_infos(self, md5s):
|
||||
return [{'scheme': 'ssh',
|
||||
'host': self.host,
|
||||
'user': self.user,
|
||||
'port': self.port,
|
||||
'path': posixpath.join(self.prefix,
|
||||
md5[0:2], md5[2:])} for md5 in md5s]
|
||||
|
||||
def ssh(self, host=None, user=None):
|
||||
msg = "Establishing ssh connection with '{}' as user '{}'"
|
||||
Logger.debug(msg.format(host, user))
|
||||
def ssh(self, host=None, user=None, port=None):
|
||||
msg = "Establishing ssh connection with '{}' " \
|
||||
"through port '{}' as user '{}'"
|
||||
Logger.debug(msg.format(host, port, user))
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
|
||||
ssh.load_system_host_keys()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
ssh.connect(host, username=user)
|
||||
if self.ask_password and self.password is None:
|
||||
msg = "host '{}' port '{}' user '{}'".format(host, port, user)
|
||||
self.password = self.project.prompt.prompt_password(msg)
|
||||
|
||||
ssh.connect(host,
|
||||
username=user,
|
||||
port=port,
|
||||
key_filename=self.keyfile,
|
||||
timeout=self.timeout,
|
||||
password=self.password)
|
||||
|
||||
return ssh
|
||||
|
||||
def exists(self, path_infos):
|
||||
ret = []
|
||||
ssh = self.ssh(host=self.host, user=self.user)
|
||||
ssh = self.ssh(host=self.host,
|
||||
user=self.user,
|
||||
port=self.port)
|
||||
cmd = 'find {} -type f -follow -print'.format(self.prefix)
|
||||
stdout = self._exec(ssh, cmd)
|
||||
plist = stdout.split()
|
||||
@@ -94,16 +119,55 @@ class RemoteSSH(RemoteBase):
|
||||
|
||||
def _exec(self, ssh, cmd):
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
channel = stdout.channel
|
||||
|
||||
stdin.close()
|
||||
channel.shutdown_write()
|
||||
|
||||
stdout_chunks = []
|
||||
stderr_chunks = []
|
||||
while (not channel.closed
|
||||
or channel.recv_ready()
|
||||
or channel.recv_stderr_ready()):
|
||||
import select
|
||||
got_chunk = False
|
||||
readq, _, _ = select.select([stdout.channel], [], [], self.timeout)
|
||||
for c in readq:
|
||||
if c.recv_ready():
|
||||
stdout_chunks.append(stdout.channel.recv(len(c.in_buffer)))
|
||||
got_chunk = True
|
||||
|
||||
if c.recv_stderr_ready():
|
||||
stderr_len = len(c.in_stderr_buffer)
|
||||
s = stderr.channel.recv_stderr(stderr_len)
|
||||
stderr_chunks.append(s)
|
||||
got_chunk = True
|
||||
|
||||
if not got_chunk \
|
||||
and stdout.channel.exit_status_ready() \
|
||||
and not stderr.channel.recv_stderr_ready() \
|
||||
and not stdout.channel.recv_ready():
|
||||
stdout.channel.shutdown_read()
|
||||
stdout.channel.close()
|
||||
break
|
||||
|
||||
stdout.close()
|
||||
stderr.close()
|
||||
|
||||
if stdout.channel.recv_exit_status() != 0:
|
||||
DvcException('SSH command \'{}\' failed: {}'.format(cmd,
|
||||
stderr.read()))
|
||||
return stdout.read().decode('utf-8')
|
||||
err = b''.join(stderr_chunks).decode('utf-8')
|
||||
msg = 'SSH command \'{}\' failed: {}'.format(cmd, err)
|
||||
raise DvcException(msg)
|
||||
|
||||
return b''.join(stdout_chunks).decode('utf-8')
|
||||
|
||||
def md5(self, path_info):
|
||||
if path_info['scheme'] != 'ssh':
|
||||
raise NotImplementedError
|
||||
|
||||
ssh = self.ssh(host=path_info['host'], user=path_info['user'])
|
||||
ssh = self.ssh(host=path_info['host'],
|
||||
user=path_info['user'],
|
||||
port=path_info['port'])
|
||||
|
||||
# Use different md5 commands depending on os
|
||||
stdout = self._exec(ssh, 'uname').strip()
|
||||
@@ -133,7 +197,8 @@ class RemoteSSH(RemoteBase):
|
||||
assert from_info['user'] == to_info['user']
|
||||
|
||||
s = ssh if ssh else self.ssh(host=from_info['host'],
|
||||
user=from_info['user'])
|
||||
user=from_info['user'],
|
||||
port=from_info['port'])
|
||||
|
||||
dname = posixpath.dirname(to_info['path'])
|
||||
self._exec(s, 'mkdir -p {}'.format(dname))
|
||||
@@ -148,6 +213,43 @@ class RemoteSSH(RemoteBase):
|
||||
|
||||
return {self.PARAM_MD5: self.md5(path_info)}
|
||||
|
||||
@staticmethod
|
||||
def to_string(path_info):
|
||||
return "{}://{}@{}:{}".format(path_info['scheme'],
|
||||
path_info['user'],
|
||||
path_info['host'],
|
||||
path_info['path'])
|
||||
|
||||
def changed_cache(self, md5):
|
||||
cache = {}
|
||||
cache['scheme'] = 'ssh'
|
||||
cache['host'] = self.host
|
||||
cache['port'] = self.port
|
||||
cache['user'] = self.user
|
||||
cache['path'] = posixpath.join(self.prefix, md5[0:2], md5[2:])
|
||||
|
||||
if {self.PARAM_MD5: md5} != self.save_info(cache):
|
||||
if self.exists([cache])[0]:
|
||||
msg = 'Corrupted cache file {}'
|
||||
Logger.warn(msg.format(self.to_string(cache)))
|
||||
self.remove(cache)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def changed(self, path_info, checksum_info):
|
||||
if not self.exists([path_info])[0]:
|
||||
return True
|
||||
|
||||
md5 = checksum_info.get(self.PARAM_MD5, None)
|
||||
if md5 is None:
|
||||
return True
|
||||
|
||||
if self.changed_code(md5):
|
||||
return True
|
||||
|
||||
return checksum_info != self.save_info(path_info)
|
||||
|
||||
def save(self, path_info):
|
||||
if path_info['scheme'] != 'ssh':
|
||||
raise NotImplementedError
|
||||
@@ -168,6 +270,25 @@ class RemoteSSH(RemoteBase):
|
||||
if not md5:
|
||||
return
|
||||
|
||||
if not self.changed(path_info, checksum_info):
|
||||
msg = "Data '{}' didn't change."
|
||||
Logger.info(msg.format(self.to_string(path_info)))
|
||||
return
|
||||
|
||||
if self.changed_cache(md5):
|
||||
msg = "Cache '{}' not found. File '{}' won't be created."
|
||||
Logger.warn(msg.format(md5, self.to_string(path_info)))
|
||||
return
|
||||
|
||||
if self.exists([path_info])[0]:
|
||||
msg = "Data '{}' exists. Removing before checkout."
|
||||
Logger.warn(msg.format(self.to_string(path_info)))
|
||||
self.remove(path_info)
|
||||
return
|
||||
|
||||
msg = "Checking out '{}' with cache '{}'."
|
||||
Logger.info(msg.format(self.to_string(path_info), md5))
|
||||
|
||||
src = path_info.copy()
|
||||
src['path'] = posixpath.join(self.prefix, md5[0:2], md5[2:])
|
||||
|
||||
@@ -181,7 +302,9 @@ class RemoteSSH(RemoteBase):
|
||||
path_info['host'],
|
||||
path_info['path']))
|
||||
|
||||
ssh = self.ssh(host=path_info['host'], user=path_info['user'])
|
||||
ssh = self.ssh(host=path_info['host'],
|
||||
user=path_info['user'],
|
||||
port=path_info['port'])
|
||||
ssh.open_sftp().remove(path_info['path'])
|
||||
ssh.close()
|
||||
|
||||
@@ -192,7 +315,9 @@ class RemoteSSH(RemoteBase):
|
||||
names=None):
|
||||
names = self._verify_path_args(from_infos, to_infos, names)
|
||||
|
||||
ssh = self.ssh(host=from_infos[0]['host'], user=from_infos[0]['user'])
|
||||
ssh = self.ssh(host=from_infos[0]['host'],
|
||||
user=from_infos[0]['user'],
|
||||
port=from_infos[0]['port'])
|
||||
|
||||
for to_info, from_info, name in zip(to_infos, from_infos, names):
|
||||
if from_info['scheme'] != 'ssh':
|
||||
@@ -200,6 +325,7 @@ class RemoteSSH(RemoteBase):
|
||||
|
||||
if to_info['scheme'] == 'ssh':
|
||||
assert from_info['host'] == to_info['host']
|
||||
assert from_info['port'] == to_info['port']
|
||||
assert from_info['user'] == to_info['user']
|
||||
self.cp(from_info, to_info, ssh=ssh)
|
||||
continue
|
||||
@@ -223,9 +349,9 @@ class RemoteSSH(RemoteBase):
|
||||
callback=create_cb(name))
|
||||
except Exception as exc:
|
||||
msg = "Failed to download '{}/{}' to '{}'"
|
||||
Logger.error(msg.format(from_info['host'],
|
||||
from_info['path'],
|
||||
to_info['path']), exc)
|
||||
Logger.warn(msg.format(from_info['host'],
|
||||
from_info['path'],
|
||||
to_info['path']), exc)
|
||||
continue
|
||||
|
||||
os.rename(tmp_file, to_info['path'])
|
||||
@@ -236,7 +362,9 @@ class RemoteSSH(RemoteBase):
|
||||
def upload(self, from_infos, to_infos, names=None):
|
||||
names = self._verify_path_args(to_infos, from_infos, names)
|
||||
|
||||
ssh = self.ssh(host=to_infos[0]['host'], user=to_infos[0]['user'])
|
||||
ssh = self.ssh(host=to_infos[0]['host'],
|
||||
user=to_infos[0]['user'],
|
||||
port=to_infos[0]['port'])
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
for from_info, to_info, name in zip(from_infos, to_infos, names):
|
||||
@@ -262,9 +390,9 @@ class RemoteSSH(RemoteBase):
|
||||
callback=create_cb(name))
|
||||
except Exception as exc:
|
||||
msg = "Failed to upload '{}' to '{}/{}'"
|
||||
Logger.error(msg.format(from_info['path'],
|
||||
to_info['host'],
|
||||
to_info['path'], exc))
|
||||
Logger.warn(msg.format(from_info['path'],
|
||||
to_info['host'],
|
||||
to_info['path'], exc))
|
||||
continue
|
||||
|
||||
progress.finish_target(name)
|
||||
@@ -277,7 +405,9 @@ class RemoteSSH(RemoteBase):
|
||||
return posixpath.dirname(relpath) + posixpath.basename(relpath)
|
||||
|
||||
def _all_md5s(self):
|
||||
ssh = self.ssh(host=self.host, user=self.user)
|
||||
ssh = self.ssh(host=self.host,
|
||||
user=self.user,
|
||||
port=self.port)
|
||||
cmd = 'find {} -type f -follow -print'.format(self.prefix)
|
||||
stdout = self._exec(ssh, cmd)
|
||||
flist = stdout.split()
|
||||
@@ -289,12 +419,17 @@ class RemoteSSH(RemoteBase):
|
||||
used = [info[self.PARAM_MD5] for info in cinfos['ssh']]
|
||||
used += [info[RemoteLOCAL.PARAM_MD5] for info in cinfos['local']]
|
||||
|
||||
removed = False
|
||||
for md5 in self._all_md5s():
|
||||
if md5 in used:
|
||||
continue
|
||||
path_info = {'scheme': 'ssh',
|
||||
'user': self.user,
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'path': posixpath.join(self.prefix,
|
||||
md5[0:2], md5[2:])}
|
||||
self.remove(path_info)
|
||||
removed = True
|
||||
|
||||
return removed
|
||||
|
||||
+101
-27
@@ -2,6 +2,7 @@ import os
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.logger import Logger
|
||||
from dvc.utils import fix_env
|
||||
|
||||
|
||||
class SCMError(DvcException):
|
||||
@@ -13,13 +14,22 @@ class FileNotInRepoError(DvcException):
|
||||
|
||||
|
||||
class Base(object):
|
||||
def __init__(self, root_dir=os.curdir):
|
||||
self.root_dir = os.path.abspath(os.path.realpath(root_dir))
|
||||
def __init__(self, root_dir=os.curdir, project=None):
|
||||
self.project = project
|
||||
self.root_dir = root_dir
|
||||
|
||||
@property
|
||||
def dir(self):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_repo(root_dir):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_submodule(root_dir):
|
||||
return True
|
||||
|
||||
def ignore(self, path):
|
||||
pass
|
||||
|
||||
@@ -44,18 +54,40 @@ class Base(object):
|
||||
def branch(self, branch):
|
||||
pass
|
||||
|
||||
def brancher(self, branches=None, all_branches=False):
|
||||
if not branches and not all_branches:
|
||||
def tag(self, tag):
|
||||
pass
|
||||
|
||||
def brancher(self,
|
||||
branches=None,
|
||||
all_branches=False,
|
||||
tags=None,
|
||||
all_tags=False):
|
||||
if not branches and not all_branches \
|
||||
and not tags and not all_tags:
|
||||
yield ''
|
||||
return
|
||||
|
||||
saved = self.active_branch()
|
||||
if not branches:
|
||||
branches = self.list_branches() if all_branches else [saved]
|
||||
for branch in branches:
|
||||
print("OLOLO " + branch)
|
||||
self.checkout(branch)
|
||||
yield branch
|
||||
revs = []
|
||||
|
||||
if all_branches:
|
||||
branches = self.list_branches()
|
||||
|
||||
if all_tags:
|
||||
tags = self.list_tags()
|
||||
|
||||
if branches is None:
|
||||
revs.extend([saved])
|
||||
else:
|
||||
revs.extend(branches)
|
||||
|
||||
if tags is not None:
|
||||
revs.extend(tags)
|
||||
|
||||
for rev in revs:
|
||||
self.checkout(rev)
|
||||
yield rev
|
||||
|
||||
self.checkout(saved)
|
||||
|
||||
def untracked_files(self):
|
||||
@@ -70,6 +102,9 @@ class Base(object):
|
||||
def list_branches(self):
|
||||
pass
|
||||
|
||||
def list_tags(self):
|
||||
pass
|
||||
|
||||
def install(self):
|
||||
pass
|
||||
|
||||
@@ -78,29 +113,47 @@ class Git(Base):
|
||||
GITIGNORE = '.gitignore'
|
||||
GIT_DIR = '.git'
|
||||
|
||||
def __init__(self, root_dir=os.curdir):
|
||||
super(Git, self).__init__(root_dir)
|
||||
def __init__(self, root_dir=os.curdir, project=None):
|
||||
super(Git, self).__init__(root_dir, project=project)
|
||||
|
||||
import git
|
||||
from git.exc import InvalidGitRepositoryError
|
||||
try:
|
||||
self.repo = git.Repo(root_dir)
|
||||
except InvalidGitRepositoryError:
|
||||
raise SCMError('{} is not a git repository'.format(root_dir))
|
||||
msg = '{} is not a git repository'
|
||||
raise SCMError(msg.format(root_dir))
|
||||
|
||||
# NOTE: fixing LD_LIBRARY_PATH for binary built by PyInstaller.
|
||||
# http://pyinstaller.readthedocs.io/en/stable/runtime-information.html
|
||||
env = fix_env(None)
|
||||
lp = env.get('LD_LIBRARY_PATH', None)
|
||||
self.repo.git.update_environment(LD_LIBRARY_PATH=lp)
|
||||
|
||||
@staticmethod
|
||||
def is_repo(root_dir):
|
||||
git_dir = os.path.join(root_dir, Git.GIT_DIR)
|
||||
return os.path.isdir(git_dir)
|
||||
return os.path.isdir(Git._get_git_dir(root_dir))
|
||||
|
||||
@staticmethod
|
||||
def is_submodule(root_dir):
|
||||
return os.path.isfile(Git._get_git_dir(root_dir))
|
||||
|
||||
@staticmethod
|
||||
def _get_git_dir(root_dir):
|
||||
return os.path.join(root_dir, Git.GIT_DIR)
|
||||
|
||||
@property
|
||||
def dir(self):
|
||||
return self.repo.git_dir
|
||||
|
||||
def ignore_file(self):
|
||||
return self.GITIGNORE
|
||||
|
||||
def _get_gitignore(self, path):
|
||||
entry = os.path.basename(path)
|
||||
gitignore = os.path.join(os.path.abspath(os.path.dirname(path)),
|
||||
self.GITIGNORE)
|
||||
gitignore = os.path.abspath(os.path.realpath(gitignore))
|
||||
assert os.path.isabs(path)
|
||||
# NOTE: using '/' prefix to make path unambiguous
|
||||
entry = '/' + os.path.basename(path)
|
||||
gitignore = os.path.join(os.path.dirname(path), self.GITIGNORE)
|
||||
|
||||
if not gitignore.startswith(self.root_dir):
|
||||
raise FileNotInRepoError(path)
|
||||
@@ -118,11 +171,19 @@ class Git(Base):
|
||||
if len(filtered) != 0:
|
||||
return
|
||||
|
||||
msg = "Adding '{}' to '{}'.".format(os.path.relpath(path),
|
||||
os.path.relpath(gitignore))
|
||||
Logger.info(msg)
|
||||
|
||||
content = entry
|
||||
if len(ignore_list) > 0:
|
||||
content = '\n' + content
|
||||
|
||||
open(gitignore, 'a').write(content)
|
||||
with open(gitignore, 'a') as fd:
|
||||
fd.write(content)
|
||||
|
||||
if self.project is not None:
|
||||
self.project._files_to_git_add.append(os.path.relpath(gitignore))
|
||||
|
||||
def ignore_remove(self, path):
|
||||
entry, gitignore = self._get_gitignore(path)
|
||||
@@ -138,6 +199,9 @@ class Git(Base):
|
||||
with open(gitignore, 'w') as fd:
|
||||
fd.writelines(filtered)
|
||||
|
||||
if self.project is not None:
|
||||
self.project._files_to_git_add.append(os.path.relpath(gitignore))
|
||||
|
||||
def add(self, paths):
|
||||
# NOTE: GitPython is not currently able to handle index version >= 3.
|
||||
# See https://github.com/iterative/dvc/issues/610 for more details.
|
||||
@@ -162,6 +226,9 @@ class Git(Base):
|
||||
def branch(self, branch):
|
||||
self.repo.git.branch(branch)
|
||||
|
||||
def tag(self, tag):
|
||||
self.repo.git.tag(tag)
|
||||
|
||||
def untracked_files(self):
|
||||
files = self.repo.untracked_files
|
||||
return [os.path.join(self.repo.working_dir, fname) for fname in files]
|
||||
@@ -175,21 +242,28 @@ class Git(Base):
|
||||
def list_branches(self):
|
||||
return [h.name for h in self.repo.heads]
|
||||
|
||||
def install(self):
|
||||
def list_tags(self):
|
||||
return [t.name for t in self.repo.tags]
|
||||
|
||||
def _install_hook(self, name, cmd):
|
||||
hook = os.path.join(self.root_dir,
|
||||
self.GIT_DIR,
|
||||
'hooks',
|
||||
'post-checkout')
|
||||
name)
|
||||
if os.path.isfile(hook):
|
||||
msg = 'Git hook \'{}\' already exists.'
|
||||
raise SCMError(msg.format(os.path.relpath(hook)))
|
||||
with open(hook, 'w+') as fd:
|
||||
fd.write('#!/bin/sh\nexec dvc checkout\n')
|
||||
fd.write('#!/bin/sh\nexec dvc {}\n'.format(cmd))
|
||||
os.chmod(hook, 0o777)
|
||||
|
||||
def install(self):
|
||||
self._install_hook('post-checkout', 'checkout')
|
||||
self._install_hook('pre-commit', 'status')
|
||||
|
||||
def SCM(root_dir=os.curdir, no_scm=False):
|
||||
if Git.is_repo(root_dir):
|
||||
return Git(root_dir)
|
||||
|
||||
return Base(root_dir)
|
||||
def SCM(root_dir, no_scm=False, project=None):
|
||||
if Git.is_repo(root_dir) or Git.is_submodule(root_dir):
|
||||
return Git(root_dir, project=project)
|
||||
|
||||
return Base(root_dir, project=project)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import signal
|
||||
import threading
|
||||
|
||||
from dvc.logger import Logger
|
||||
|
||||
|
||||
class SignalHandler(object):
|
||||
def __enter__(self):
|
||||
if isinstance(threading.current_thread(), threading._MainThread):
|
||||
self.old_handler = signal.signal(signal.SIGINT, self.handler)
|
||||
|
||||
def handler(self, sig, frame):
|
||||
Logger.debug('Ignoring SIGINT during critical parts of code...')
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if isinstance(threading.current_thread(), threading._MainThread):
|
||||
signal.signal(signal.SIGINT, self.old_handler)
|
||||
+198
-46
@@ -1,15 +1,14 @@
|
||||
import os
|
||||
import yaml
|
||||
import itertools
|
||||
import posixpath
|
||||
import subprocess
|
||||
from schema import Schema, SchemaError, Optional, Or, And
|
||||
import posixpath
|
||||
|
||||
import dvc.dependency as dependency
|
||||
import dvc.output as output
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.logger import Logger
|
||||
from dvc.utils import dict_md5
|
||||
from dvc.utils import dict_md5, fix_env
|
||||
|
||||
|
||||
class StageCmdFailedError(DvcException):
|
||||
@@ -19,8 +18,45 @@ class StageCmdFailedError(DvcException):
|
||||
|
||||
|
||||
class StageFileFormatError(DvcException):
|
||||
def __init__(self):
|
||||
super(StageFileFormatError, self).__init__('Stage file format error')
|
||||
def __init__(self, fname, e):
|
||||
msg = "Stage file '{}' format error: {}".format(fname, str(e))
|
||||
super(StageFileFormatError, self).__init__(msg)
|
||||
|
||||
|
||||
class StageFileDoesNotExistError(DvcException):
|
||||
def __init__(self, fname):
|
||||
msg = "'{}' does not exist.".format(fname)
|
||||
super(StageFileDoesNotExistError, self).__init__(msg)
|
||||
|
||||
|
||||
class StageFileIsNotDvcFileError(DvcException):
|
||||
def __init__(self, fname):
|
||||
msg = "'{}' is not a dvc file".format(fname)
|
||||
super(StageFileIsNotDvcFileError, self).__init__(msg)
|
||||
|
||||
|
||||
class StageFileBadNameError(DvcException):
|
||||
def __init__(self, msg):
|
||||
super(StageFileBadNameError, self).__init__(msg)
|
||||
|
||||
|
||||
class StageBadCwdError(DvcException):
|
||||
def __init__(self, cwd):
|
||||
msg = "Stage cwd '{}' is outside of the current dvc project"
|
||||
super(StageBadCwdError, self).__init__(msg.format(cwd))
|
||||
|
||||
|
||||
class MissingDep(DvcException):
|
||||
def __init__(self, deps):
|
||||
assert len(deps) > 0
|
||||
|
||||
if len(deps) > 1:
|
||||
dep = 'dependencies'
|
||||
else:
|
||||
dep = 'dependency'
|
||||
|
||||
msg = u'missing {}: {}'.format(dep, ', '.join(map(str, deps)))
|
||||
super(MissingDep, self).__init__(msg)
|
||||
|
||||
|
||||
class MissingDataSource(DvcException):
|
||||
@@ -80,16 +116,20 @@ class Stage(object):
|
||||
return self.cmd is None
|
||||
|
||||
@staticmethod
|
||||
def is_stage_file(path):
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
|
||||
def is_stage_filename(path):
|
||||
if not path.endswith(Stage.STAGE_FILE_SUFFIX) \
|
||||
and os.path.basename(path) != Stage.STAGE_FILE:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_stage_file(path):
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
|
||||
return Stage.is_stage_filename(path)
|
||||
|
||||
def changed_md5(self):
|
||||
md5 = self.dumpd().get(self.PARAM_MD5, None)
|
||||
|
||||
@@ -100,8 +140,6 @@ class Stage(object):
|
||||
if self.md5 and md5 and self.md5 == md5:
|
||||
return False
|
||||
|
||||
msg = "Dvc file '{}' md5 changed(expected '{}', actual '{}')"
|
||||
self.project.logger.debug(msg.format(self.relpath, self.md5, md5))
|
||||
return True
|
||||
|
||||
@property
|
||||
@@ -114,30 +152,51 @@ class Stage(object):
|
||||
len(self.deps) == 1 and \
|
||||
len(self.outs) == 1
|
||||
|
||||
def changed(self):
|
||||
def changed(self, print_info=False):
|
||||
ret = False
|
||||
|
||||
if print_info:
|
||||
log = self.project.logger.info
|
||||
else:
|
||||
log = self.project.logger.debug
|
||||
|
||||
if self.is_callback:
|
||||
msg = "Dvc file '{}' is a 'callback' stage (has a command and " \
|
||||
"no dependencies) and thus always considered as changed."
|
||||
self.project.logger.warn(msg.format(self.relpath))
|
||||
ret = True
|
||||
|
||||
if self.locked:
|
||||
entries = self.outs
|
||||
else:
|
||||
entries = itertools.chain(self.outs, self.deps)
|
||||
|
||||
for entry in entries:
|
||||
if entry.changed():
|
||||
if not self.locked:
|
||||
for dep in self.deps:
|
||||
if not dep.changed():
|
||||
continue
|
||||
if print_info:
|
||||
msg = "Dependency '{}' of '{}' changed."
|
||||
log(msg.format(dep, self.relpath))
|
||||
ret = True
|
||||
|
||||
for out in self.outs:
|
||||
if not out.changed():
|
||||
continue
|
||||
if print_info:
|
||||
msg = "Output '{}' of '{}' changed."
|
||||
log(msg.format(out, self.relpath))
|
||||
ret = True
|
||||
|
||||
if self.changed_md5():
|
||||
if print_info:
|
||||
msg = "Dvc file '{}' changed."
|
||||
log(msg.format(self.relpath))
|
||||
ret = True
|
||||
|
||||
if ret:
|
||||
msg = u'Dvc file \'{}\' changed'.format(self.relpath)
|
||||
msg = "Stage '{}' changed.".format(self.relpath)
|
||||
color = 'yellow'
|
||||
else:
|
||||
msg = u'Dvc file \'{}\' didn\'t change'.format(self.relpath)
|
||||
msg = "Stage '{}' didn't change.".format(self.relpath)
|
||||
color = 'green'
|
||||
|
||||
self.project.logger.debug(msg)
|
||||
log(Logger.colorize(msg, color))
|
||||
|
||||
return ret
|
||||
|
||||
@@ -149,17 +208,23 @@ class Stage(object):
|
||||
self.remove_outs(ignore_remove=True)
|
||||
os.unlink(self.path)
|
||||
|
||||
def reproduce(self, force=False):
|
||||
if not self.changed() and not force:
|
||||
def reproduce(self, force=False, dry=False, interactive=False):
|
||||
if not self.changed(print_info=True) and not force:
|
||||
return None
|
||||
|
||||
if (self.cmd or self.is_import) and not self.locked:
|
||||
if (self.cmd or self.is_import) and not self.locked and not dry:
|
||||
# Removing outputs only if we actually have command to reproduce
|
||||
self.remove_outs(ignore_remove=False)
|
||||
|
||||
msg = "Going to reproduce '{}'. Are you sure you want to continue?"
|
||||
msg = msg.format(self.relpath)
|
||||
if interactive \
|
||||
and not self.project.prompt.prompt(msg):
|
||||
raise DvcException('Reproduction aborted by the user')
|
||||
|
||||
self.project.logger.info(u'Reproducing \'{}\''.format(self.relpath))
|
||||
|
||||
self.run()
|
||||
self.run(dry=dry)
|
||||
|
||||
msg = u'\'{}\' was reproduced'.format(self.relpath)
|
||||
self.project.logger.debug(msg)
|
||||
@@ -167,17 +232,15 @@ class Stage(object):
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def validate(d):
|
||||
def validate(d, fname=None):
|
||||
try:
|
||||
Schema(Stage.SCHEMA).validate(d)
|
||||
except SchemaError as exc:
|
||||
Logger.debug(str(exc))
|
||||
raise StageFileFormatError()
|
||||
raise StageFileFormatError(fname, exc)
|
||||
|
||||
@staticmethod
|
||||
def loadd(project, d, path):
|
||||
Stage.validate(d)
|
||||
|
||||
Stage.validate(d, fname=os.path.relpath(path))
|
||||
path = os.path.abspath(path)
|
||||
cwd = os.path.dirname(path)
|
||||
cmd = d.get(Stage.PARAM_CMD, None)
|
||||
@@ -213,10 +276,18 @@ class Stage(object):
|
||||
if not fname:
|
||||
fname = path.basename(out.path) + cls.STAGE_FILE_SUFFIX
|
||||
|
||||
cwd = path.dirname(out.path) if not cwd or add else cwd
|
||||
if not cwd or (add and out.is_local):
|
||||
cwd = path.dirname(out.path)
|
||||
|
||||
return (fname, cwd)
|
||||
|
||||
@staticmethod
|
||||
def _check_inside_project(project, cwd):
|
||||
assert project is not None
|
||||
proj_dir = os.path.realpath(project.root_dir)
|
||||
if not os.path.realpath(cwd).startswith(proj_dir):
|
||||
raise StageBadCwdError(cwd)
|
||||
|
||||
@staticmethod
|
||||
def loads(project=None,
|
||||
cmd=None,
|
||||
@@ -227,7 +298,8 @@ class Stage(object):
|
||||
fname=None,
|
||||
cwd=os.curdir,
|
||||
locked=False,
|
||||
add=False):
|
||||
add=False,
|
||||
overwrite=True):
|
||||
stage = Stage(project=project,
|
||||
cwd=cwd,
|
||||
cmd=cmd,
|
||||
@@ -239,18 +311,57 @@ class Stage(object):
|
||||
use_cache=False, metric=True)
|
||||
stage.deps = dependency.loads_from(stage, deps)
|
||||
|
||||
if fname is not None and os.path.basename(fname) != fname:
|
||||
msg = "Stage file name '{}' should not contain subdirectories. " \
|
||||
"Use '-c|--cwd' to change location of the stage file."
|
||||
raise StageFileBadNameError(msg.format(fname))
|
||||
|
||||
fname, cwd = Stage._stage_fname_cwd(fname, cwd, stage.outs, add=add)
|
||||
|
||||
Stage._check_inside_project(project, cwd)
|
||||
|
||||
cwd = os.path.abspath(cwd)
|
||||
path = os.path.join(cwd, fname)
|
||||
|
||||
if os.path.exists(path):
|
||||
relpath = os.path.relpath(path)
|
||||
msg = "'{}' already exists. " \
|
||||
"Do you wish to run the command and overwrite it?"
|
||||
if not overwrite \
|
||||
and not project.prompt.prompt(msg.format(relpath), False):
|
||||
raise DvcException("'{}' already exists".format(relpath))
|
||||
|
||||
stage.cwd = cwd
|
||||
stage.path = path
|
||||
|
||||
return stage
|
||||
|
||||
@staticmethod
|
||||
def _check_dvc_filename(fname):
|
||||
if not Stage.is_stage_filename(fname):
|
||||
msg = "Bad stage filename '{}'. Stage files should be named " \
|
||||
"'Dvcfile' or have a '.dvc' suffix(e.g. '{}.dvc')."
|
||||
raise StageFileBadNameError(msg.format(os.path.relpath(fname),
|
||||
os.path.basename(fname)))
|
||||
|
||||
@staticmethod
|
||||
def _check_dvc_file(fname):
|
||||
sname = fname + Stage.STAGE_FILE_SUFFIX
|
||||
if Stage.is_stage_file(sname):
|
||||
Logger.info("Do you mean '{}'?".format(sname))
|
||||
|
||||
@staticmethod
|
||||
def load(project, fname):
|
||||
if not os.path.exists(fname):
|
||||
Stage._check_dvc_file(fname)
|
||||
raise StageFileDoesNotExistError(fname)
|
||||
|
||||
Stage._check_dvc_filename(fname)
|
||||
|
||||
if not Stage.is_stage_file(fname):
|
||||
Stage._check_dvc_file(fname)
|
||||
raise StageFileIsNotDvcFileError(fname)
|
||||
|
||||
with open(fname, 'r') as fd:
|
||||
return Stage.loadd(project, yaml.safe_load(fd), fname)
|
||||
|
||||
@@ -279,9 +390,16 @@ class Stage(object):
|
||||
if not fname:
|
||||
fname = self.path
|
||||
|
||||
self._check_dvc_filename(fname)
|
||||
|
||||
msg = "Saving information to '{}'.".format(os.path.relpath(fname))
|
||||
Logger.info(msg)
|
||||
|
||||
with open(fname, 'w') as fd:
|
||||
yaml.safe_dump(self.dumpd(), fd, default_flow_style=False)
|
||||
|
||||
self.project._files_to_git_add.append(os.path.relpath(fname))
|
||||
|
||||
def save(self):
|
||||
for dep in self.deps:
|
||||
dep.save()
|
||||
@@ -289,35 +407,69 @@ class Stage(object):
|
||||
for out in self.outs:
|
||||
out.save()
|
||||
|
||||
def run(self):
|
||||
def _check_missing_deps(self):
|
||||
missing = []
|
||||
for dep in self.deps:
|
||||
if not dep.exists:
|
||||
missing.append(dep)
|
||||
|
||||
if len(missing) > 0:
|
||||
raise MissingDep(missing)
|
||||
|
||||
def _check_if_fish(self, executable): # pragma: no cover
|
||||
if executable is None \
|
||||
or os.path.basename(os.path.realpath(executable)) != 'fish':
|
||||
return
|
||||
|
||||
msg = "DVC detected that you are using fish as your default " \
|
||||
"shell. Be aware that it might cause problems by overwriting " \
|
||||
"your current environment variables with values defined " \
|
||||
"in '.fishrc', which might affect your command. See " \
|
||||
"https://github.com/iterative/dvc/issues/1307. "
|
||||
self.project.logger.warn(msg)
|
||||
|
||||
def _run(self):
|
||||
self._check_missing_deps()
|
||||
executable = os.getenv('SHELL') if os.name != 'nt' else None
|
||||
self._check_if_fish(executable)
|
||||
|
||||
p = subprocess.Popen(self.cmd,
|
||||
cwd=self.cwd,
|
||||
shell=True,
|
||||
env=fix_env(os.environ),
|
||||
executable=executable)
|
||||
p.communicate()
|
||||
|
||||
if p.returncode != 0:
|
||||
raise StageCmdFailedError(self)
|
||||
|
||||
def run(self, dry=False):
|
||||
if self.locked:
|
||||
msg = u'Verifying outputs in locked stage \'{}\''
|
||||
self.project.logger.info(msg.format(self.relpath))
|
||||
self.check_missing_outputs()
|
||||
if not dry:
|
||||
self.check_missing_outputs()
|
||||
elif self.is_import:
|
||||
msg = u'Importing \'{}\' -> \'{}\''
|
||||
self.project.logger.info(msg.format(self.deps[0].path,
|
||||
self.outs[0].path))
|
||||
|
||||
self.deps[0].download(self.outs[0].path_info)
|
||||
if not dry:
|
||||
self.deps[0].download(self.outs[0].path_info)
|
||||
elif self.is_data_source:
|
||||
msg = u'Verifying data sources in \'{}\''.format(self.relpath)
|
||||
self.project.logger.info(msg)
|
||||
self.check_missing_outputs()
|
||||
if not dry:
|
||||
self.check_missing_outputs()
|
||||
else:
|
||||
msg = u'Running command:\n\t{}'.format(self.cmd)
|
||||
self.project.logger.info(msg)
|
||||
|
||||
p = subprocess.Popen(self.cmd,
|
||||
cwd=self.cwd,
|
||||
shell=True,
|
||||
env=os.environ,
|
||||
executable=os.getenv('SHELL'))
|
||||
p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise StageCmdFailedError(self)
|
||||
if not dry:
|
||||
self._run()
|
||||
|
||||
self.save()
|
||||
if not dry:
|
||||
self.save()
|
||||
|
||||
def check_missing_outputs(self):
|
||||
outs = [out for out in self.outs if not out.exists]
|
||||
|
||||
+281
-141
@@ -1,60 +1,78 @@
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import sqlite3
|
||||
import nanotime
|
||||
import threading
|
||||
|
||||
from dvc.lock import Lock
|
||||
from dvc.config import Config
|
||||
from dvc.system import System
|
||||
from dvc.utils import file_md5, remove
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.signal_handler import SignalHandler
|
||||
from dvc.logger import Logger
|
||||
|
||||
|
||||
class StateEntry(object):
|
||||
PARAM_MTIME = 'mtime'
|
||||
PARAM_MD5 = 'md5'
|
||||
|
||||
def __init__(self, md5, mtime):
|
||||
self.mtime = mtime
|
||||
self.md5 = md5
|
||||
|
||||
def update(self, md5, mtime):
|
||||
self.mtime = mtime
|
||||
self.md5 = md5
|
||||
|
||||
@staticmethod
|
||||
def loadd(d):
|
||||
mtime = d[StateEntry.PARAM_MTIME]
|
||||
md5 = d[StateEntry.PARAM_MD5]
|
||||
return StateEntry(md5, mtime)
|
||||
|
||||
def dumpd(self):
|
||||
return {
|
||||
self.PARAM_MD5: self.md5,
|
||||
self.PARAM_MTIME: self.mtime,
|
||||
}
|
||||
|
||||
|
||||
class StateDuplicateError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class State(object):
|
||||
VERSION = 2
|
||||
STATE_FILE = 'state'
|
||||
STATE_LOCK_FILE = 'state.lock'
|
||||
STATE_TABLE = 'state'
|
||||
STATE_TABLE_LAYOUT = "inode INTEGER PRIMARY KEY, " \
|
||||
"mtime TEXT NOT NULL, " \
|
||||
"md5 TEXT NOT NULL, " \
|
||||
"timestamp TEXT NOT NULL"
|
||||
|
||||
def __init__(self, project):
|
||||
STATE_INFO_TABLE = 'state_info'
|
||||
STATE_INFO_TABLE_LAYOUT = 'count INTEGER'
|
||||
STATE_INFO_ROW = 1
|
||||
|
||||
LINK_STATE_TABLE = 'link_state'
|
||||
LINK_STATE_TABLE_LAYOUT = "path TEXT PRIMARY KEY, " \
|
||||
"inode INTEGER NOT NULL, " \
|
||||
"mtime TEXT NOT NULL"
|
||||
|
||||
STATE_ROW_LIMIT = 100000000
|
||||
STATE_ROW_CLEANUP_QUOTA = 50
|
||||
|
||||
MAX_INT = 2**63 - 1
|
||||
MAX_UINT = 2**64 - 2
|
||||
|
||||
def __init__(self, project, config):
|
||||
self.project = project
|
||||
self.dvc_dir = project.dvc_dir
|
||||
self._lock = threading.Lock()
|
||||
if self.dvc_dir:
|
||||
self.state_file = os.path.join(self.dvc_dir, self.STATE_FILE)
|
||||
self._lock_file = Lock(self.dvc_dir, self.STATE_LOCK_FILE)
|
||||
else:
|
||||
self.root_dir = project.root_dir
|
||||
|
||||
self.row_limit = 100
|
||||
self.row_cleanup_quota = 50
|
||||
|
||||
c = config.get(Config.SECTION_STATE, {})
|
||||
self.row_limit = c.get(Config.SECTION_STATE_ROW_LIMIT,
|
||||
self.STATE_ROW_LIMIT)
|
||||
self.row_cleanup_quota = c.get(Config.SECTION_STATE_ROW_CLEANUP_QUOTA,
|
||||
self.STATE_ROW_CLEANUP_QUOTA)
|
||||
|
||||
if not self.dvc_dir:
|
||||
self.state_file = None
|
||||
self._lock_file = threading.Lock()
|
||||
self._db = self.load()
|
||||
return
|
||||
|
||||
self.state_file = os.path.join(self.dvc_dir, self.STATE_FILE)
|
||||
|
||||
# https://www.sqlite.org/tempfiles.html
|
||||
self.temp_files = [
|
||||
self.state_file + '-journal',
|
||||
self.state_file + '-wal',
|
||||
]
|
||||
|
||||
self.db = None
|
||||
self.c = None
|
||||
self.inserts = 0
|
||||
|
||||
def __enter__(self):
|
||||
self.load()
|
||||
|
||||
def __exit__(self, type, value, tb):
|
||||
self.dump()
|
||||
|
||||
@staticmethod
|
||||
def init(project):
|
||||
@@ -69,142 +87,266 @@ class State(object):
|
||||
def changed(self, path, md5):
|
||||
actual = self.update(path)
|
||||
|
||||
msg = "File '{}', md5 '{}', actual '{}'"
|
||||
Logger.debug(msg.format(path, md5, actual))
|
||||
|
||||
if not md5 or not actual:
|
||||
return True
|
||||
|
||||
return actual.split('.')[0] != md5.split('.')[0]
|
||||
|
||||
def load(self):
|
||||
if not self.state_file or not os.path.isfile(self.state_file):
|
||||
return {}
|
||||
def _execute(self, cmd):
|
||||
Logger.debug(cmd)
|
||||
return self.c.execute(cmd)
|
||||
|
||||
with open(self.state_file, 'r') as fd:
|
||||
def _fetchall(self):
|
||||
ret = self.c.fetchall()
|
||||
Logger.debug("fetched: {}".format(ret))
|
||||
return ret
|
||||
|
||||
def _to_sqlite(self, n):
|
||||
assert n >= 0
|
||||
assert n < self.MAX_UINT
|
||||
# NOTE: sqlite stores unit as signed ints, so maximum uint is 2^63-1
|
||||
# see http://jakegoulding.com/blog/2011/02/06/sqlite-64-bit-integers/
|
||||
if n > self.MAX_INT:
|
||||
ret = -(n - self.MAX_INT)
|
||||
else:
|
||||
ret = n
|
||||
assert self._from_sqlite(ret) == n
|
||||
return ret
|
||||
|
||||
def _from_sqlite(self, n):
|
||||
assert abs(n) <= self.MAX_INT
|
||||
if n < 0:
|
||||
return abs(n) + self.MAX_INT
|
||||
assert n < self.MAX_UINT
|
||||
assert n >= 0
|
||||
return n
|
||||
|
||||
def _load(self, empty=False):
|
||||
from dvc import VERSION
|
||||
|
||||
if not empty:
|
||||
cmd = "PRAGMA user_version;"
|
||||
self._execute(cmd)
|
||||
ret = self._fetchall()
|
||||
assert len(ret) == 1
|
||||
assert len(ret[0]) == 1
|
||||
assert isinstance(ret[0][0], int)
|
||||
version = ret[0][0]
|
||||
|
||||
if version > self.VERSION:
|
||||
msg = "You are using an old version '{}' of dvc that is " \
|
||||
"using state file version '{}' which is not " \
|
||||
"compatible with the state file version '{}' that " \
|
||||
"is used in this projet. Please upgrade right now!"
|
||||
raise DvcException(msg.format(VERSION,
|
||||
self.VERSION,
|
||||
version))
|
||||
elif version < self.VERSION:
|
||||
msg = "State file version '{}' is too old. " \
|
||||
"Reformatting to the current version '{}'."
|
||||
self.project.logger.warn(msg.format(version, self.VERSION))
|
||||
cmd = "DROP TABLE IF EXISTS {};"
|
||||
self._execute(cmd.format(self.STATE_TABLE))
|
||||
self._execute(cmd.format(self.STATE_INFO_TABLE))
|
||||
self._execute(cmd.format(self.LINK_STATE_TABLE))
|
||||
|
||||
# Check that the state file is indeed a database
|
||||
cmd = "CREATE TABLE IF NOT EXISTS {} ({})"
|
||||
self._execute(cmd.format(self.STATE_TABLE,
|
||||
self.STATE_TABLE_LAYOUT))
|
||||
self._execute(cmd.format(self.STATE_INFO_TABLE,
|
||||
self.STATE_INFO_TABLE_LAYOUT))
|
||||
self._execute(cmd.format(self.LINK_STATE_TABLE,
|
||||
self.LINK_STATE_TABLE_LAYOUT))
|
||||
|
||||
cmd = "INSERT OR IGNORE INTO {} (count) SELECT 0 " \
|
||||
"WHERE NOT EXISTS (SELECT * FROM {})"
|
||||
self._execute(cmd.format(self.STATE_INFO_TABLE,
|
||||
self.STATE_INFO_TABLE))
|
||||
|
||||
cmd = "PRAGMA user_version = {};"
|
||||
self._execute(cmd.format(self.VERSION))
|
||||
|
||||
def load(self):
|
||||
retries = 1
|
||||
while True:
|
||||
assert self.db is None
|
||||
assert self.c is None
|
||||
assert self.inserts == 0
|
||||
empty = not os.path.exists(self.state_file)
|
||||
self.db = sqlite3.connect(self.state_file)
|
||||
self.c = self.db.cursor()
|
||||
|
||||
# Try loading once to check that the file is indeed a database
|
||||
# and reformat it if it is not.
|
||||
try:
|
||||
return json.load(fd)
|
||||
except ValueError as exc:
|
||||
Logger.error('Failed to load \'{}\''.format(self.state_file),
|
||||
exc)
|
||||
return {}
|
||||
self._load(empty=empty)
|
||||
return
|
||||
except sqlite3.DatabaseError:
|
||||
self.c.close()
|
||||
self.db.close()
|
||||
self.db = None
|
||||
self.c = None
|
||||
self.inserts = 0
|
||||
if retries > 0:
|
||||
os.unlink(self.state_file)
|
||||
retries -= 1
|
||||
else:
|
||||
raise
|
||||
|
||||
def _vacuum(self):
|
||||
# NOTE: see https://bugs.python.org/issue28518
|
||||
self.db.isolation_level = None
|
||||
self._execute("VACUUM")
|
||||
self.db.isolation_level = ''
|
||||
|
||||
def dump(self):
|
||||
if not self.state_file:
|
||||
return
|
||||
assert self.db is not None
|
||||
|
||||
with SignalHandler():
|
||||
with open(self.state_file, 'w+') as fd:
|
||||
json.dump(self._db, fd)
|
||||
cmd = "SELECT count from {} WHERE rowid={}"
|
||||
self._execute(cmd.format(self.STATE_INFO_TABLE,
|
||||
self.STATE_INFO_ROW))
|
||||
ret = self._fetchall()
|
||||
assert len(ret) == 1
|
||||
assert len(ret[0]) == 1
|
||||
count = self._from_sqlite(ret[0][0]) + self.inserts
|
||||
|
||||
if count > self.row_limit:
|
||||
msg = "Cleaning up state. This might take a while."
|
||||
self.project.logger.warn(msg)
|
||||
|
||||
delete = (count - self.row_limit)
|
||||
delete += int(self.row_limit * (self.row_cleanup_quota/100.))
|
||||
cmd = "DELETE FROM {} WHERE timestamp IN (" \
|
||||
"SELECT timestamp FROM {} ORDER BY timestamp ASC LIMIT {});"
|
||||
self._execute(cmd.format(self.STATE_TABLE,
|
||||
self.STATE_TABLE,
|
||||
delete))
|
||||
|
||||
self._vacuum()
|
||||
|
||||
cmd = "SELECT COUNT(*) FROM {}"
|
||||
|
||||
self._execute(cmd.format(self.STATE_TABLE))
|
||||
ret = self._fetchall()
|
||||
assert len(ret) == 1
|
||||
assert len(ret[0]) == 1
|
||||
count = ret[0][0]
|
||||
|
||||
cmd = "UPDATE {} SET count = {} WHERE rowid = {}"
|
||||
self._execute(cmd.format(self.STATE_INFO_TABLE,
|
||||
self._to_sqlite(count),
|
||||
self.STATE_INFO_ROW))
|
||||
|
||||
self.db.commit()
|
||||
self.c.close()
|
||||
self.db.close()
|
||||
self.db = None
|
||||
self.c = None
|
||||
self.inserts = 0
|
||||
|
||||
@staticmethod
|
||||
def mtime(path):
|
||||
return str(int(nanotime.timestamp(os.path.getmtime(path))))
|
||||
mtime = os.path.getmtime(path)
|
||||
if os.path.isdir(path):
|
||||
for root, dirs, files in os.walk(path):
|
||||
for entry in list(dirs) + list(files):
|
||||
p = os.path.join(root, entry)
|
||||
m = os.path.getmtime(p)
|
||||
if m > mtime:
|
||||
mtime = m
|
||||
|
||||
return str(int(nanotime.timestamp(mtime)))
|
||||
|
||||
@staticmethod
|
||||
def inode(path):
|
||||
return str(System.inode(path))
|
||||
Logger.debug('Path {} inode {}'.format(path, System.inode(path)))
|
||||
return System.inode(path)
|
||||
|
||||
def _do_update(self, path, dump=True):
|
||||
def _do_update(self, path):
|
||||
if not os.path.exists(path):
|
||||
return (None, None)
|
||||
|
||||
mtime = self.mtime(path)
|
||||
inode = self.inode(path)
|
||||
|
||||
md5 = self._get(inode, mtime)
|
||||
if md5:
|
||||
return (md5, None)
|
||||
cmd = 'SELECT * from {} WHERE inode={}'.format(self.STATE_TABLE,
|
||||
self._to_sqlite(inode))
|
||||
|
||||
md5, info = self._collect(path)
|
||||
state = StateEntry(md5, mtime)
|
||||
d = state.dumpd()
|
||||
|
||||
with self._lock:
|
||||
with self._lock_file:
|
||||
self._db[inode] = d
|
||||
|
||||
if dump:
|
||||
self.dump()
|
||||
self._execute(cmd)
|
||||
ret = self._fetchall()
|
||||
if len(ret) == 0:
|
||||
md5, info = self._collect(path)
|
||||
cmd = 'INSERT INTO {}(inode, mtime, md5, timestamp) ' \
|
||||
'VALUES ({}, "{}", "{}", "{}")'
|
||||
self._execute(cmd.format(self.STATE_TABLE,
|
||||
self._to_sqlite(inode),
|
||||
mtime,
|
||||
md5,
|
||||
int(nanotime.timestamp(time.time()))))
|
||||
self.inserts += 1
|
||||
else:
|
||||
assert len(ret) == 1
|
||||
assert len(ret[0]) == 4
|
||||
i, m, md5, timestamp = ret[0]
|
||||
i = self._from_sqlite(i)
|
||||
assert i == inode
|
||||
msg = "Inode '{}', mtime '{}', actual mtime '{}'."
|
||||
Logger.debug(msg.format(i, m, mtime))
|
||||
if mtime != m:
|
||||
md5, info = self._collect(path)
|
||||
cmd = 'UPDATE {} SET ' \
|
||||
'mtime = "{}", md5 = "{}", timestamp = "{}" ' \
|
||||
'WHERE inode = {}'
|
||||
self._execute(cmd.format(self.STATE_TABLE,
|
||||
mtime,
|
||||
md5,
|
||||
int(nanotime.timestamp(time.time())),
|
||||
self._to_sqlite(inode)))
|
||||
else:
|
||||
info = None
|
||||
cmd = 'UPDATE {} SET timestamp = "{}" WHERE inode = {}'
|
||||
self._execute(cmd.format(self.STATE_TABLE,
|
||||
int(nanotime.timestamp(time.time())),
|
||||
self._to_sqlite(inode)))
|
||||
|
||||
return (md5, info)
|
||||
|
||||
def update(self, path, dump=True):
|
||||
return self._do_update(path, dump=dump)[0]
|
||||
def update(self, path):
|
||||
return self._do_update(path)[0]
|
||||
|
||||
def update_info(self, path, dump=True):
|
||||
md5, info = self._do_update(path, dump=dump)
|
||||
def update_info(self, path):
|
||||
md5, info = self._do_update(path)
|
||||
if not info:
|
||||
info = self.project.cache.local.load_dir_cache(md5)
|
||||
return (md5, info)
|
||||
|
||||
def _get(self, inode, mtime):
|
||||
with self._lock:
|
||||
with self._lock_file:
|
||||
d = self._db.get(inode, None)
|
||||
|
||||
if not d:
|
||||
return None
|
||||
|
||||
state = StateEntry.loadd(d)
|
||||
if mtime == state.mtime:
|
||||
return state.md5
|
||||
|
||||
return None
|
||||
|
||||
def get(self, path):
|
||||
mtime = self.mtime(path)
|
||||
inode = self.inode(path)
|
||||
|
||||
return self._get(inode, mtime)
|
||||
|
||||
|
||||
class LinkStateEntry(object):
|
||||
PARAM_MTIME = 'mtime'
|
||||
PARAM_INODE = 'inode'
|
||||
|
||||
def __init__(self, inode, mtime):
|
||||
self.mtime = mtime
|
||||
self.inode = inode
|
||||
|
||||
@staticmethod
|
||||
def loadd(d):
|
||||
mtime = d[LinkStateEntry.PARAM_MTIME]
|
||||
inode = d[LinkStateEntry.PARAM_INODE]
|
||||
return LinkStateEntry(inode, mtime)
|
||||
|
||||
def dumpd(self):
|
||||
return {
|
||||
self.PARAM_INODE: self.inode,
|
||||
self.PARAM_MTIME: self.mtime,
|
||||
}
|
||||
|
||||
|
||||
class LinkState(State):
|
||||
STATE_FILE = 'link.state'
|
||||
STATE_LOCK_FILE = STATE_FILE + '.lock'
|
||||
|
||||
def __init__(self, project):
|
||||
super(LinkState, self).__init__(project)
|
||||
self.root_dir = project.root_dir
|
||||
|
||||
def update(self, path, dump=True):
|
||||
def update_link(self, path):
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
|
||||
mtime = self.mtime(path)
|
||||
inode = self.inode(path)
|
||||
relpath = os.path.relpath(path, self.root_dir)
|
||||
|
||||
with self._lock:
|
||||
state = LinkStateEntry(inode, mtime)
|
||||
d = state.dumpd()
|
||||
self._db[os.path.relpath(path, self.root_dir)] = d
|
||||
if dump:
|
||||
with self._lock_file:
|
||||
self.dump()
|
||||
cmd = 'REPLACE INTO {}(path, inode, mtime) ' \
|
||||
'VALUES ("{}", {}, "{}")'.format(self.LINK_STATE_TABLE,
|
||||
relpath,
|
||||
self._to_sqlite(inode),
|
||||
mtime)
|
||||
self._execute(cmd)
|
||||
|
||||
def _do_remove_unused(self, used):
|
||||
items = self._db.copy().items()
|
||||
for p, s in items:
|
||||
def remove_unused_links(self, used):
|
||||
unused = []
|
||||
|
||||
self._execute('SELECT * FROM {}'.format(self.LINK_STATE_TABLE))
|
||||
for row in self.c:
|
||||
p, i, m = row
|
||||
i = self._from_sqlite(i)
|
||||
path = os.path.join(self.root_dir, p)
|
||||
state = LinkStateEntry.loadd(s)
|
||||
|
||||
if path in used:
|
||||
continue
|
||||
@@ -215,13 +357,11 @@ class LinkState(State):
|
||||
inode = self.inode(path)
|
||||
mtime = self.mtime(path)
|
||||
|
||||
if inode == state.inode and mtime == state.mtime:
|
||||
if i == inode and m == mtime:
|
||||
Logger.debug('Removing \'{}\' as unused link.'.format(path))
|
||||
remove(path)
|
||||
del self._db[p]
|
||||
unused.append(p)
|
||||
|
||||
def remove_unused(self, used):
|
||||
with self._lock:
|
||||
with self._lock_file:
|
||||
self._do_remove_unused(used)
|
||||
self.dump()
|
||||
for p in unused:
|
||||
cmd = 'DELETE FROM {} WHERE path = "{}"'
|
||||
self._execute(cmd.format(self.LINK_STATE_TABLE, p))
|
||||
|
||||
+165
-34
@@ -1,9 +1,4 @@
|
||||
import os
|
||||
import ctypes
|
||||
import reflink
|
||||
|
||||
if os.name == 'nt':
|
||||
import ntfsutils.hardlink as winlink
|
||||
|
||||
|
||||
class System(object):
|
||||
@@ -13,15 +8,38 @@ class System(object):
|
||||
|
||||
@staticmethod
|
||||
def hardlink(source, link_name):
|
||||
if System.is_unix():
|
||||
return os.link(source, link_name)
|
||||
import ctypes
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
return winlink.create(source, link_name)
|
||||
if System.is_unix():
|
||||
try:
|
||||
os.link(source, link_name)
|
||||
return
|
||||
except Exception as exc:
|
||||
raise DvcException('link', cause=exc)
|
||||
|
||||
CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW
|
||||
CreateHardLink.argtypes = [ctypes.c_wchar_p,
|
||||
ctypes.c_wchar_p,
|
||||
ctypes.c_void_p]
|
||||
CreateHardLink.restype = ctypes.wintypes.BOOL
|
||||
|
||||
res = CreateHardLink(link_name, source, None)
|
||||
if res == 0:
|
||||
raise DvcException('CreateHardLinkW', cause=ctypes.WinError())
|
||||
|
||||
@staticmethod
|
||||
def symlink(source, link_name):
|
||||
import ctypes
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
if System.is_unix():
|
||||
return os.symlink(source, link_name)
|
||||
try:
|
||||
os.symlink(source, link_name)
|
||||
return
|
||||
except Exception as exc:
|
||||
msg = "Failed to symlink '{}' -> '{}': {}"
|
||||
raise DvcException(msg.format(source, link_name, str(exc)))
|
||||
|
||||
flags = 0
|
||||
if source is not None and os.path.isdir(source):
|
||||
@@ -32,45 +50,158 @@ class System(object):
|
||||
func.restype = ctypes.c_ubyte
|
||||
|
||||
if func(link_name, source, flags) == 0:
|
||||
raise ctypes.WinError()
|
||||
raise DvcException('CreateSymbolicLinkW', cause=ctypes.WinError())
|
||||
|
||||
@staticmethod
|
||||
def _reflink_darwin(src, dst):
|
||||
import ctypes
|
||||
|
||||
clib = ctypes.CDLL('libc.dylib')
|
||||
if not hasattr(clib, 'clonefile'):
|
||||
return -1
|
||||
|
||||
clonefile = clib.clonefile
|
||||
clonefile.argtypes = [ctypes.c_char_p,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_int]
|
||||
clonefile.restype = ctypes.c_int
|
||||
|
||||
return clonefile(ctypes.c_char_p(src.encode('utf-8')),
|
||||
ctypes.c_char_p(dst.encode('utf-8')),
|
||||
ctypes.c_int(0))
|
||||
|
||||
@staticmethod
|
||||
def _reflink_windows(src, dst):
|
||||
return -1
|
||||
|
||||
@staticmethod
|
||||
def _reflink_linux(src, dst):
|
||||
import os
|
||||
import fcntl
|
||||
|
||||
FICLONE = 0x40049409
|
||||
|
||||
s = open(src, 'r')
|
||||
d = open(dst, 'w+')
|
||||
|
||||
try:
|
||||
ret = fcntl.ioctl(d.fileno(), FICLONE, s.fileno())
|
||||
except IOError:
|
||||
s.close()
|
||||
d.close()
|
||||
os.unlink(dst)
|
||||
raise
|
||||
|
||||
s.close()
|
||||
d.close()
|
||||
|
||||
if ret != 0:
|
||||
os.unlink(dst)
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def reflink(source, link_name):
|
||||
return reflink.reflink(source, link_name)
|
||||
import platform
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
system = platform.system()
|
||||
try:
|
||||
if system == 'Windows':
|
||||
ret = System._reflink_windows(source, link_name)
|
||||
elif system == 'Darwin':
|
||||
ret = System._reflink_darwin(source, link_name)
|
||||
elif system == 'Linux':
|
||||
ret = System._reflink_linux(source, link_name)
|
||||
else:
|
||||
ret = -1
|
||||
except IOError:
|
||||
ret = -1
|
||||
|
||||
if ret != 0:
|
||||
raise DvcException('Reflink is not supported')
|
||||
|
||||
# FIXME
|
||||
# Temporary fix while waiting for the PR to be merged and released:
|
||||
# https://github.com/sunshowers/ntfs/pull/11
|
||||
@staticmethod
|
||||
def getdirinfo(path):
|
||||
from ntfsutils.fs import FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_READ
|
||||
from ntfsutils.fs import OPEN_EXISTING, BY_HANDLE_FILE_INFORMATION
|
||||
from ntfsutils.fs import CreateFile, WinError
|
||||
from ntfsutils.fs import GetFileInformationByHandle, CloseHandle
|
||||
import ctypes
|
||||
from ctypes import c_void_p, c_wchar_p, Structure, WinError, POINTER
|
||||
from ctypes.wintypes import DWORD, HANDLE, BOOL
|
||||
|
||||
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
|
||||
FILE_SHARE_READ = 0x00000001
|
||||
OPEN_EXISTING = 3
|
||||
|
||||
class FILETIME(Structure):
|
||||
_fields_ = [("dwLowDateTime", DWORD),
|
||||
("dwHighDateTime", DWORD)]
|
||||
|
||||
class BY_HANDLE_FILE_INFORMATION(Structure):
|
||||
_fields_ = [("dwFileAttributes", DWORD),
|
||||
("ftCreationTime", FILETIME),
|
||||
("ftLastAccessTime", FILETIME),
|
||||
("ftLastWriteTime", FILETIME),
|
||||
("dwVolumeSerialNumber", DWORD),
|
||||
("nFileSizeHigh", DWORD),
|
||||
("nFileSizeLow", DWORD),
|
||||
("nNumberOfLinks", DWORD),
|
||||
("nFileIndexHigh", DWORD),
|
||||
("nFileIndexLow", DWORD)]
|
||||
|
||||
flags = FILE_FLAG_BACKUP_SEMANTICS
|
||||
hfile = CreateFile(path,
|
||||
0,
|
||||
FILE_SHARE_READ,
|
||||
None,
|
||||
OPEN_EXISTING,
|
||||
flags,
|
||||
None)
|
||||
|
||||
func = ctypes.windll.kernel32.CreateFileW
|
||||
func.argtypes = [c_wchar_p,
|
||||
DWORD,
|
||||
DWORD,
|
||||
c_void_p,
|
||||
DWORD,
|
||||
DWORD,
|
||||
HANDLE]
|
||||
func.restype = HANDLE
|
||||
|
||||
hfile = func(path,
|
||||
0,
|
||||
FILE_SHARE_READ,
|
||||
None,
|
||||
OPEN_EXISTING,
|
||||
flags,
|
||||
None)
|
||||
if hfile is None:
|
||||
raise WinError()
|
||||
|
||||
func = ctypes.windll.kernel32.GetFileInformationByHandle
|
||||
func.argtypes = [HANDLE, POINTER(BY_HANDLE_FILE_INFORMATION)]
|
||||
func.restype = BOOL
|
||||
|
||||
info = BY_HANDLE_FILE_INFORMATION()
|
||||
rv = GetFileInformationByHandle(hfile, info)
|
||||
CloseHandle(hfile)
|
||||
rv = func(hfile, info)
|
||||
|
||||
func = ctypes.windll.kernel32.CloseHandle
|
||||
func.argtypes = [HANDLE]
|
||||
func.restype = BOOL
|
||||
|
||||
func(hfile)
|
||||
|
||||
if rv == 0:
|
||||
raise WinError()
|
||||
|
||||
return info
|
||||
|
||||
@staticmethod
|
||||
def inode(path):
|
||||
if System.is_unix():
|
||||
return os.lstat(path).st_ino
|
||||
|
||||
# getdirinfo from ntfsutils works on both files and dirs
|
||||
info = System.getdirinfo(path)
|
||||
return hash((info.dwVolumeSerialNumber,
|
||||
info.nFileIndexHigh,
|
||||
info.nFileIndexLow))
|
||||
import ctypes
|
||||
inode = os.lstat(path).st_ino
|
||||
# NOTE: See https://bugs.python.org/issue29619 and
|
||||
# https://stackoverflow.com/questions/34643289/
|
||||
# pythons-os-stat-is-returning-wrong-inode-value
|
||||
inode = ctypes.c_ulong(inode).value
|
||||
else:
|
||||
# getdirinfo from ntfsutils works on both files and dirs
|
||||
info = System.getdirinfo(path)
|
||||
inode = abs(hash((info.dwVolumeSerialNumber,
|
||||
info.nFileIndexHigh,
|
||||
info.nFileIndexLow)))
|
||||
assert inode >= 0
|
||||
assert inode < 2**64
|
||||
return inode
|
||||
|
||||
+116
-21
@@ -1,29 +1,26 @@
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
import colorama
|
||||
|
||||
from dvc import VERSION_BASE
|
||||
from dvc.logger import Logger
|
||||
|
||||
|
||||
class Updater(object):
|
||||
class Updater(object): # pragma: no cover
|
||||
URL = 'https://4ki8820rsf.execute-api.us-east-2.amazonaws.com/' \
|
||||
'prod/latest-version'
|
||||
UPDATER_FILE = 'updater'
|
||||
TIMEOUT = 7 * 24 * 60 * 60 # every week
|
||||
TIMEOUT = 24 * 60 * 60 # every day
|
||||
TIMEOUT_GET = 10
|
||||
|
||||
def __init__(self, dvc_dir):
|
||||
self.dvc_dir = dvc_dir
|
||||
self.updater_file = os.path.join(dvc_dir, self.UPDATER_FILE)
|
||||
|
||||
@staticmethod
|
||||
def init(dvc_dir):
|
||||
return Updater(dvc_dir)
|
||||
self.current = VERSION_BASE
|
||||
|
||||
def check(self):
|
||||
current = VERSION_BASE
|
||||
|
||||
if os.getenv('CI'):
|
||||
return
|
||||
|
||||
@@ -39,23 +36,121 @@ class Updater(object):
|
||||
Logger.info('Checking for updates...')
|
||||
|
||||
try:
|
||||
r = requests.get(self.URL, timeout=self.TIMEOUT_GET)
|
||||
j = r.json()
|
||||
latest = j['version']
|
||||
open(self.updater_file, 'w+').close()
|
||||
self._get_latest_version()
|
||||
except Exception as exc:
|
||||
msg = 'Failed to obtain latest version: {}'.format(str(exc))
|
||||
Logger.debug(msg)
|
||||
return
|
||||
|
||||
l_major, l_minor, l_patch = [int(x) for x in latest.split('.')]
|
||||
c_major, c_minor, c_patch = [int(x) for x in current.split('.')]
|
||||
if self._is_outdated():
|
||||
self._notify()
|
||||
|
||||
if l_major <= c_major and \
|
||||
l_minor <= c_minor and \
|
||||
l_patch <= c_patch:
|
||||
return
|
||||
def _get_latest_version(self):
|
||||
r = requests.get(self.URL, timeout=self.TIMEOUT_GET)
|
||||
j = r.json()
|
||||
self.latest = j['version']
|
||||
open(self.updater_file, 'w+').close()
|
||||
|
||||
msg = 'You are using dvc version {}, however version {} is ' \
|
||||
'available. Consider upgrading.'
|
||||
Logger.warn(msg.format(current, latest))
|
||||
def _is_outdated(self):
|
||||
l_major, l_minor, l_patch = [int(x) for x in self.latest.split('.')]
|
||||
c_major, c_minor, c_patch = [int(x) for x in self.current.split('.')]
|
||||
|
||||
return (l_major > c_major or
|
||||
l_minor > c_minor or
|
||||
l_patch > c_patch)
|
||||
|
||||
def _notify(self):
|
||||
message = (
|
||||
'Update available {red}{current}{reset} -> {green}{latest}{reset}'
|
||||
+ '\n'
|
||||
+ self._get_update_instructions()
|
||||
).format(red=colorama.Fore.RED,
|
||||
reset=colorama.Fore.RESET,
|
||||
green=colorama.Fore.GREEN,
|
||||
yellow=colorama.Fore.YELLOW,
|
||||
blue=colorama.Fore.BLUE,
|
||||
current=self.current,
|
||||
latest=self.latest)
|
||||
|
||||
if sys.stdout.isatty():
|
||||
Logger.box(message, border_color='yellow')
|
||||
|
||||
def _get_update_instructions(self):
|
||||
instructions = {
|
||||
'pip': 'Run {yellow}pip{reset} install dvc {blue}--upgrade{reset}',
|
||||
'yum': 'Run {yellow}yum{reset} update dvc',
|
||||
'yay': 'Run {yellow}yay{reset} {blue}-S{reset} dvc',
|
||||
'formula': 'Run {yellow}brew{reset} upgrade dvc',
|
||||
'cask': 'Run {yellow}brew cask{reset} upgrade dvc',
|
||||
'apt': ('Run {yellow}apt-get{reset} install'
|
||||
' {blue}--only-upgrade{reset} dvc'),
|
||||
'binary': ('To upgrade follow this steps:\n'
|
||||
'1. Uninstall dvc binary\n'
|
||||
'2. Go to {blue}https://dvc.org{reset}\n'
|
||||
'3. Download and install new binary'),
|
||||
None: ('Find the latest release at\n{blue}'
|
||||
'https://github.com/iterative/dvc/releases/latest'
|
||||
'{reset}'),
|
||||
}
|
||||
|
||||
package_manager = self._get_package_manager()
|
||||
|
||||
return instructions[package_manager]
|
||||
|
||||
@staticmethod
|
||||
def _is_binary():
|
||||
return getattr(sys, 'frozen', False)
|
||||
|
||||
def _get_linux(self):
|
||||
import distro
|
||||
|
||||
if not self._is_binary():
|
||||
return 'pip'
|
||||
|
||||
package_managers = {
|
||||
'rhel': 'yum',
|
||||
'centos': 'yum',
|
||||
'fedora': 'yum',
|
||||
'amazon': 'yum',
|
||||
'opensuse': 'yum',
|
||||
'ubuntu': 'apt',
|
||||
'debian': 'apt',
|
||||
}
|
||||
|
||||
return package_managers.get(distro.id())
|
||||
|
||||
def _get_darwin(self):
|
||||
if not self._is_binary():
|
||||
if __file__.startswith('/usr/local/Cellar'):
|
||||
return 'formula'
|
||||
else:
|
||||
return 'pip'
|
||||
|
||||
# NOTE: both pkg and cask put dvc binary into /usr/local/bin,
|
||||
# so in order to know which method of installation was used,
|
||||
# we need to actually call `brew cask`
|
||||
ret = os.system('brew cask ls dvc')
|
||||
if ret == 0:
|
||||
return 'cask'
|
||||
|
||||
return None
|
||||
|
||||
def _get_windows(self):
|
||||
return None if self._is_binary() else 'pip'
|
||||
|
||||
def _get_package_manager(self):
|
||||
import platform
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
m = {
|
||||
'Windows': self._get_windows,
|
||||
'Darwin': self._get_darwin,
|
||||
'Linux': self._get_linux,
|
||||
}
|
||||
|
||||
system = platform.system()
|
||||
func = m.get(system)
|
||||
if func is None:
|
||||
raise DvcException("Not supported system '{}'".format(system))
|
||||
|
||||
return func()
|
||||
|
||||
+51
-3
@@ -2,17 +2,21 @@
|
||||
Helpers for other modules.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
import math
|
||||
import json
|
||||
import shutil
|
||||
import hashlib
|
||||
|
||||
from dvc.istextfile import istextfile
|
||||
from dvc.progress import progress
|
||||
from dvc.istextfile import istextfile
|
||||
from dvc.logger import Logger
|
||||
|
||||
|
||||
LOCAL_CHUNK_SIZE = 1024*1024
|
||||
LARGE_FILE_SIZE = 1024*1024*1024
|
||||
LARGE_DIR_SIZE = 100
|
||||
|
||||
|
||||
def dos2unix(data):
|
||||
@@ -24,6 +28,14 @@ def file_md5(fname):
|
||||
if os.path.exists(fname):
|
||||
hash_md5 = hashlib.md5()
|
||||
binary = not istextfile(fname)
|
||||
size = os.path.getsize(fname)
|
||||
bar = False
|
||||
if size >= LARGE_FILE_SIZE:
|
||||
bar = True
|
||||
msg = "Computing md5 for a large file {}. This is only done once."
|
||||
Logger.info(msg.format(os.path.relpath(fname)))
|
||||
name = os.path.relpath(fname)
|
||||
total = 0
|
||||
|
||||
with open(fname, 'rb') as fobj:
|
||||
while True:
|
||||
@@ -31,6 +43,10 @@ def file_md5(fname):
|
||||
if not data:
|
||||
break
|
||||
|
||||
if bar:
|
||||
total += len(data)
|
||||
progress.update_target(name, total, size)
|
||||
|
||||
if binary:
|
||||
chunk = data
|
||||
else:
|
||||
@@ -38,6 +54,9 @@ def file_md5(fname):
|
||||
|
||||
hash_md5.update(chunk)
|
||||
|
||||
if bar:
|
||||
progress.finish_target(name)
|
||||
|
||||
return (hash_md5.hexdigest(), hash_md5.digest())
|
||||
else:
|
||||
return (None, None)
|
||||
@@ -84,6 +103,7 @@ def copyfile(src, dest, no_progress_bar=False, name=None):
|
||||
|
||||
|
||||
def move(src, dst):
|
||||
dst = os.path.abspath(dst)
|
||||
dname = os.path.dirname(dst)
|
||||
if not os.path.exists(dname):
|
||||
os.makedirs(dname)
|
||||
@@ -101,10 +121,17 @@ def remove(path):
|
||||
return
|
||||
|
||||
Logger.debug(u'Removing \'{}\''.format(os.path.relpath(path)))
|
||||
|
||||
def _chmod(func, p, excinfo):
|
||||
perm = os.stat(p).st_mode
|
||||
perm |= stat.S_IWRITE
|
||||
os.chmod(p, perm)
|
||||
func(p)
|
||||
|
||||
if os.path.isfile(path):
|
||||
os.unlink(path)
|
||||
_chmod(os.unlink, path, None)
|
||||
else:
|
||||
shutil.rmtree(path)
|
||||
shutil.rmtree(path, onerror=_chmod)
|
||||
|
||||
|
||||
def to_chunks(l, jobs):
|
||||
@@ -117,3 +144,24 @@ def to_chunks(l, jobs):
|
||||
n = 1
|
||||
|
||||
return [l[x:x+n] for x in range(0, len(l), n)]
|
||||
|
||||
|
||||
# NOTE: Fix env variables modified by PyInstaller
|
||||
# http://pyinstaller.readthedocs.io/en/stable/runtime-information.html
|
||||
def fix_env(env):
|
||||
if env is None:
|
||||
env = os.environ.copy()
|
||||
else:
|
||||
env = env.copy()
|
||||
|
||||
# NOTE: Check if we are in a bundle
|
||||
# https://pythonhosted.org/PyInstaller/runtime-information.html
|
||||
if getattr(sys, 'frozen', False):
|
||||
lp_key = 'LD_LIBRARY_PATH'
|
||||
lp_orig = env.get(lp_key + '_ORIG', None)
|
||||
if lp_orig is not None:
|
||||
env[lp_key] = lp_orig
|
||||
else:
|
||||
env.pop(lp_key, None)
|
||||
|
||||
return env
|
||||
|
||||
+7
-5
@@ -4,21 +4,23 @@ ply>=3.9 # See https://github.com/pyinstaller/pyinstaller/issues/1945
|
||||
configparser>=3.5.0
|
||||
zc.lockfile>=1.2.1
|
||||
future>=0.16.0
|
||||
google-cloud==0.32.0
|
||||
google-cloud-storage==1.13.0
|
||||
PyInstaller==3.3.1
|
||||
colorama>=0.3.9
|
||||
configobj>=5.0.6
|
||||
networkx==2.1
|
||||
networkx>=2.1
|
||||
pyyaml>=3.12
|
||||
gitpython>=2.1.8
|
||||
ntfsutils>=0.1.4
|
||||
setuptools>=34.0.0
|
||||
pytest>=3.3.1
|
||||
nanotime>=0.5.2
|
||||
pyasn1>=0.4.1
|
||||
schema>=0.6.7
|
||||
paramiko>=2.4.1
|
||||
jsonpath-rw==1.4.0
|
||||
reflink==0.2.0
|
||||
requests>=2.18.4
|
||||
wheel>=0.31.1
|
||||
futures>=3.2.0; python_version == "2.7"
|
||||
grandalf==0.6
|
||||
pydot>=1.2.4
|
||||
asciimatics>=1.10.0
|
||||
distro>=1.3.0
|
||||
|
||||
+15
-3
@@ -3,9 +3,21 @@
|
||||
set -x
|
||||
set -e
|
||||
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -r test-requirements.txt
|
||||
N_RETRIES=3
|
||||
|
||||
function retry {
|
||||
for i in $(seq $N_RETRIES); do
|
||||
$@ && break
|
||||
done
|
||||
}
|
||||
|
||||
# NOTE: it is not uncommon for pip to hang on travis for what seems to be
|
||||
# networking issues. Thus, let's retry a few times to see if it will eventially
|
||||
# work or not.
|
||||
retry pip install --upgrade pip setuptools wheel
|
||||
retry pip install -r requirements.txt
|
||||
retry pip install -r test-requirements.txt
|
||||
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "DVC Tester"
|
||||
|
||||
|
||||
+24
-2
@@ -6,11 +6,33 @@ set -e
|
||||
python -mtests
|
||||
|
||||
if [[ "$TRAVIS_PULL_REQUEST" == "false" && \
|
||||
"$TRAVIS_SECURE_ENV_VARS" == "true" ]]; then
|
||||
"$TRAVIS_SECURE_ENV_VARS" == "true" && \
|
||||
"$(python -c 'import sys; print(sys.version_info[0])')" == '2' ]]; then
|
||||
codecov
|
||||
fi
|
||||
|
||||
if [[ ! -z "$TRAVIS_TAG" ]]; then
|
||||
./scripts/build_package.sh
|
||||
./scripts/build_posix.sh
|
||||
|
||||
# Test version
|
||||
pip uninstall -y dvc
|
||||
if [ -x "$(command -v dvc)" ]; then
|
||||
echo "ERROR: dvc command already exists!!!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pip install dist/dvc-*.whl
|
||||
if [[ "$(dvc --version)" != "$TRAVIS_TAG" ]]; then
|
||||
echo "ERROR: 'dvc --version'$(dvc -V) doesn't match '$TRAVIS_TAG'"
|
||||
exit 1
|
||||
fi
|
||||
pip uninstall -y dvc
|
||||
|
||||
PY_VER=$(python -c 'import sys; print(sys.version_info[0:2])')
|
||||
if [[ "$PY_VER" != '(3, 7)' ]]; then
|
||||
./scripts/build_posix.sh
|
||||
else
|
||||
echo "Not building dvc binary package on Python 3.7, since it is "\
|
||||
"not supported by PyInstaller yet."
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Repository: https://github.com/iterative/dvc
|
||||
#
|
||||
# References:
|
||||
# - https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html
|
||||
# - https://opensource.com/article/18/3/creating-bash-completion-script
|
||||
#----------------------------------------------------------
|
||||
|
||||
_dvc_commands='init destroy add import checkout run pull push fetch \
|
||||
status repro remove move gc config remote metrics install \
|
||||
root lock unlock pipeline'
|
||||
|
||||
_dvc_global_options="-h --help -q --quiet -V --verbose"
|
||||
_dvc_options="-h --help -v --version"
|
||||
_dvc_init="--no-scm -f --force"
|
||||
_dvc_destroy="-f --force"
|
||||
_dvc_add="-R --recursive"
|
||||
_dvc_run="--no-exec -f --file -c --cwd -d --deps -o --outs -O --outs-no-cache -M --metrics-no-cache -y --yes"
|
||||
_dvc_pull="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps"
|
||||
_dvc_push="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps"
|
||||
_dvc_fetch="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps"
|
||||
_dvc_status="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps -c --cloud"
|
||||
_dvc_repro="--dry -f --force -s --single-item -c --cwd -m --metrics -i --interactive -p --pipeline -P --all-pipelines"
|
||||
_dvc_remove="--dry -o --outs -p --purge"
|
||||
_dvc_gc="-a --all-branches -T --all-tags -c --cloud -r --remote"
|
||||
_dvc_config="-u --unset --local"
|
||||
_dvc_checkout="$(compgen -G '*.dvc')"
|
||||
_dvc_move="$(compgen -G '*')"
|
||||
_dvc_lock="$(compgen -G '*.dvc')"
|
||||
_dvc_unlock="$(compgen -G '*.dvc')"
|
||||
_dvc_import=""
|
||||
_dvc_remote=""
|
||||
_dvc_metrics=""
|
||||
_dvc_install=""
|
||||
_dvc_root=""
|
||||
_dvc_pipeline=""
|
||||
|
||||
# Notes:
|
||||
#
|
||||
# `COMPREPLY` contains what will be rendered after completion is triggered
|
||||
#
|
||||
# `word` refers to the current typed word
|
||||
#
|
||||
# `${!var}` is to evaluate the content of `var` and expand its content as a variable
|
||||
#
|
||||
# hello="world"
|
||||
# x="hello"
|
||||
# ${!x} -> ${hello} -> "world"
|
||||
#
|
||||
_dvc () {
|
||||
local word="${COMP_WORDS[COMP_CWORD]}"
|
||||
|
||||
COMPREPLY=()
|
||||
|
||||
if [ "${COMP_CWORD}" -eq 1 ]; then
|
||||
case "$word" in
|
||||
-*) COMPREPLY=($(compgen -W "$_dvc_options" -- "$word")) ;;
|
||||
*) COMPREPLY=($(compgen -W "$_dvc_commands" -- "$word")) ;;
|
||||
esac
|
||||
elif [ "${COMP_CWORD}" -eq 2 ]; then
|
||||
local options_list="_dvc_${COMP_WORDS[1]}"
|
||||
|
||||
COMPREPLY=($(compgen -W "$_dvc_global_options ${!options_list}" -- "$word"))
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _dvc dvc
|
||||
@@ -0,0 +1,218 @@
|
||||
#compdef dvc
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Repository: https://github.com/iterative/dvc
|
||||
#
|
||||
# References:
|
||||
# - https://github.com/zsh-users/zsh-completions
|
||||
# - http://zsh.sourceforge.net/Doc/Release/Completion-System.html
|
||||
# - https://mads-hartmann.com/2017/08/06/writing-zsh-completion-scripts.html
|
||||
# - http://www.linux-mag.com/id/1106/
|
||||
#----------------------------------------------------------
|
||||
|
||||
_dvc_commands() {
|
||||
local _commands=(
|
||||
"init:Initialize dvc over a directory (should already be a git dir)."
|
||||
"destroy:Destroy dvc. Will remove all project's information, data files and cache."
|
||||
"add:Add files/directories to dvc."
|
||||
"import:Import files from URL."
|
||||
"checkout:Checkout data files from cache."
|
||||
"run:Generate a stage file from a given command and execute the command."
|
||||
"pull:Pull data files from the cloud."
|
||||
"push:Push data files to the cloud."
|
||||
"fetch:Fetch data files from the cloud."
|
||||
"status:Show the project status."
|
||||
"repro:Reproduce DVC file. Default file name - 'Dvcfile'."
|
||||
"remove:Remove outputs of DVC file."
|
||||
"move:Move output of DVC file."
|
||||
"gc:Collect garbage."
|
||||
"config:Get or set config options."
|
||||
"remote:Manage set of tracked repositories."
|
||||
"metrics:Get metrics from all branches."
|
||||
"install:Install dvc hooks into the repository."
|
||||
"root:Relative path to project's directory."
|
||||
"lock:Lock DVC file."
|
||||
"unlock:Unlock DVC file."
|
||||
"pipeline:Manage pipeline."
|
||||
)
|
||||
|
||||
_describe 'dvc commands' _commands
|
||||
}
|
||||
|
||||
_dvc_global_options=(
|
||||
"(-)"{-h,--help}"[Show help message related to the command]"
|
||||
"(-)"{-q,--quiet}"[Be quiet.]"
|
||||
"(-)"{-V,--verbose}"[Be verbose.]"
|
||||
)
|
||||
|
||||
_dvc_options=(
|
||||
"(-)"{-h,--help}"[Show this help message and exit]"
|
||||
"(-)"{-v,--version}"[Show program's version]"
|
||||
)
|
||||
|
||||
_dvc_init=(
|
||||
"--no-scm[Initiate dvc in directory that is not tracket by any scm tool]"
|
||||
{-f,--force}"[Overwrite '.dvc' if it exists. Will remove all local cache.]"
|
||||
)
|
||||
|
||||
_dvc_destroy=(
|
||||
{-f,--force}"[Overwrite '.dvc' if it exists. Will remove all local cache.]"
|
||||
)
|
||||
|
||||
_dvc_add=(
|
||||
{-R,--recursive}"[Recursively add each file under the directory.]"
|
||||
)
|
||||
|
||||
_dvc_import=(
|
||||
"1:URL:"
|
||||
"2:Output:"
|
||||
)
|
||||
|
||||
_dvc_checkout=(
|
||||
"1:DVC files:_files -g '(*.dvc|Dvcfile)'"
|
||||
)
|
||||
|
||||
_dvc_run=(
|
||||
"--no-exec[Only create stage file without actually running it.]"
|
||||
{-f,--file}"[Specify name of the stage file.]:File:_files"
|
||||
{-c,--cwd}"[Directory to run your command and place state file in.]:CWD:_files -/"
|
||||
"*"{-d,--deps}"[Declare dependencies for reproducible cmd.]:Dependency:_files"
|
||||
"*"{-o,--outs}"[Declare output data file or data directory.]:Output data:_files"
|
||||
"*"{-O,--outs-no-cache}"[Declare output regular file or directory.]:Output regular:_files"
|
||||
"*"{-M,--metrics-no-cache}"[Declare output metric file or directory]:Metrics (no cache):_files"
|
||||
{-y,--yes}"[Automatic 'yes' answer to all prompts.]"
|
||||
)
|
||||
|
||||
_dvc_pull=(
|
||||
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
|
||||
"--show-checksums[Show checksums instead of file names]"
|
||||
{-j,--jobs}"[Number of jobs to run simultaneously]:Number of jobs:"
|
||||
{-r,--remote}"[Remote repository to pull from]:Remote repository:"
|
||||
{-a,--all-branches}"[Fetch cache for all branches]"
|
||||
{-T,--all-tags}"[Fetch cache for all tags]"
|
||||
{-d,--with-deps}"[Fetch cache for all dependencies of the specified target]"
|
||||
)
|
||||
|
||||
_dvc_push=(
|
||||
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
|
||||
"--show-checksums[Show checksums instead of file names]"
|
||||
{-j,--jobs}"[Number of jobs to run simultaneously]:Number of jobs:"
|
||||
{-r,--remote}"[Remote repository to pull from]:Remote repository:"
|
||||
{-a,--all-branches}"[Fetch cache for all branches]"
|
||||
{-T,--all-tags}"[Fetch cache for all tags]"
|
||||
{-d,--with-deps}"[Fetch cache for all dependencies of the specified target]"
|
||||
)
|
||||
|
||||
_dvc_fetch=(
|
||||
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
|
||||
"--show-checksums[Show checksums instead of file names]"
|
||||
{-j,--jobs}"[Number of jobs to run simultaneously]:Number of jobs:"
|
||||
{-r,--remote}"[Remote repository to pull from]:Remote repository:"
|
||||
{-a,--all-branches}"[Fetch cache for all branches]"
|
||||
{-T,--all-tags}"[Fetch cache for all tags]"
|
||||
{-d,--with-deps}"[Fetch cache for all dependencies of the specified target]"
|
||||
)
|
||||
|
||||
_dvc_status=(
|
||||
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
|
||||
"--show-checksums[Show checksums instead of file names]"
|
||||
{-j,--jobs}"[Number of jobs to run simultaneously]:Number of jobs:"
|
||||
{-r,--remote}"[Remote repository to pull from]:Remote repository:"
|
||||
{-a,--all-branches}"[Fetch cache for all branches]"
|
||||
{-T,--all-tags}"[Fetch cache for all tags]"
|
||||
{-d,--with-deps}"[Fetch cache for all dependencies of the specified target]"
|
||||
{-c,--cloud}"[Show status of a local cache compared to a remote repository]"
|
||||
)
|
||||
|
||||
_dvc_repro=(
|
||||
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
|
||||
"--dry[Only print the commands that would be executed without actually executing]"
|
||||
{-f,--force}"[Reproduce even if dependencies were not changed.]"
|
||||
{-s,--single-item}"[Reproduce only single data item without recursive dependencies check.]"
|
||||
{-c,--cwd}"[Directory to reproduce from]:CWD:_files -/"
|
||||
{-m,--metrics}"[Show metrics after reproduction.]"
|
||||
{-i,--interactive}"[Ask for confirmation before reproducing each stage.]"
|
||||
{-p,--pipeline}"[Reproduce the whole pipeline that the specified stage file belongs to.]"
|
||||
{-P,--all-pipelines}"[Reproduce all pipelines in the project.]"
|
||||
)
|
||||
|
||||
_dvc_remove=(
|
||||
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
|
||||
"--dry[Only print the commands that would be executed without actually executing]"
|
||||
{-o,--outs}"[Only remove DVC file outputs.(default)]"
|
||||
{-p,--purge}"[Remove DVC file and all its outputs]"
|
||||
)
|
||||
|
||||
_dvc_move=(
|
||||
"1:Source:_files"
|
||||
"2:Destination:"
|
||||
)
|
||||
|
||||
_dvc_gc=(
|
||||
{-a,--all-branches}"[Collect garbage for all branches]"
|
||||
{-T,--all-tags}"[Collect garbage for all tags]"
|
||||
{-c,--cloud}"[Collect garbage in remote repository]"
|
||||
{-r,--remote}"[Remote repository to collect garbage in]:Remote repository:"
|
||||
)
|
||||
|
||||
_dvc_config=(
|
||||
{-u,--unset}"[Unset option.]"
|
||||
"--local[Unset option.]"
|
||||
)
|
||||
|
||||
_dvc_remote=(
|
||||
"1:Task:(add remove modify list)"
|
||||
)
|
||||
|
||||
_dvc_metrics=(
|
||||
"1:Task:(show add modify remove)"
|
||||
)
|
||||
|
||||
_dvc_install=()
|
||||
|
||||
_dvc_root=()
|
||||
|
||||
_dvc_lock=(
|
||||
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
|
||||
)
|
||||
|
||||
_dvc_unlock=(
|
||||
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
|
||||
)
|
||||
|
||||
_dvc_pipeline=(
|
||||
"1:Task:(show)"
|
||||
)
|
||||
|
||||
typeset -A opt_args
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
_arguments \
|
||||
$_dvc_options \
|
||||
'1: :_dvc_commands' \
|
||||
'*::args:->args'
|
||||
|
||||
case $words[1] in
|
||||
init) _arguments $_dvc_global_options $_dvc_init ;;
|
||||
destroy) _arguments $_dvc_global_options $_dvc_destroy ;;
|
||||
add) _arguments $_dvc_global_options $_dvc_add ;;
|
||||
import) _arguments $_dvc_global_options $_dvc_import ;;
|
||||
checkout) _arguments $_dvc_global_options $_dvc_checkout ;;
|
||||
run) _arguments $_dvc_global_options $_dvc_run ;;
|
||||
pull) _arguments $_dvc_global_options $_dvc_pull ;;
|
||||
push) _arguments $_dvc_global_options $_dvc_push ;;
|
||||
fetch) _arguments $_dvc_global_options $_dvc_fetch ;;
|
||||
status) _arguments $_dvc_global_options $_dvc_status ;;
|
||||
repro) _arguments $_dvc_global_options $_dvc_repro ;;
|
||||
remove) _arguments $_dvc_global_options $_dvc_remove ;;
|
||||
move) _arguments $_dvc_global_options $_dvc_move ;;
|
||||
gc) _arguments $_dvc_global_options $_dvc_gc ;;
|
||||
config) _arguments $_dvc_global_options $_dvc_config ;;
|
||||
remote) _arguments $_dvc_global_options $_dvc_remote ;;
|
||||
metrics) _arguments $_dvc_global_options $_dvc_metrics ;;
|
||||
install) _arguments $_dvc_global_options $_dvc_install ;;
|
||||
root) _arguments $_dvc_global_options $_dvc_root ;;
|
||||
lock) _arguments $_dvc_global_options $_dvc_lock ;;
|
||||
unlock) _arguments $_dvc_global_options $_dvc_unlock ;;
|
||||
pipeline) _arguments $_dvc_global_options $_dvc_pipeline ;;
|
||||
esac
|
||||
@@ -1,3 +1,4 @@
|
||||
import sys
|
||||
import platform
|
||||
from setuptools import setup, find_packages
|
||||
from distutils.errors import DistutilsPlatformError
|
||||
@@ -11,23 +12,23 @@ install_requires = [
|
||||
"future>=0.16.0",
|
||||
"colorama>=0.3.9",
|
||||
"configobj>=5.0.6",
|
||||
"networkx==2.1",
|
||||
"networkx>=2.1",
|
||||
"pyyaml>=3.12",
|
||||
"gitpython>=2.1.8",
|
||||
"ntfsutils>=0.1.4",
|
||||
"setuptools>=34.0.0",
|
||||
"pytest>=3.3.1",
|
||||
"nanotime>=0.5.2",
|
||||
"pyasn1>=0.4.1",
|
||||
"schema>=0.6.7",
|
||||
"jsonpath-rw==1.4.0",
|
||||
"reflink==0.2.0",
|
||||
"requests>=2.18.4",
|
||||
"grandalf==0.6",
|
||||
"asciimatics>=1.10.0",
|
||||
"distro>=1.3.0",
|
||||
]
|
||||
|
||||
# Extra dependencies for remote integrations
|
||||
gs = [
|
||||
"google-cloud==0.32.0",
|
||||
"google-cloud-storage==1.13.0",
|
||||
]
|
||||
s3 = [
|
||||
"boto3==1.7.4",
|
||||
@@ -56,6 +57,8 @@ setup(
|
||||
's3': s3,
|
||||
'azure': azure,
|
||||
'ssh': ssh,
|
||||
# NOTE: https://github.com/inveniosoftware/troubleshooting/issues/1
|
||||
':python_version=="2.7"': ['futures'],
|
||||
},
|
||||
keywords='data science, data version control, machine learning',
|
||||
classifiers=[
|
||||
@@ -63,11 +66,11 @@ setup(
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 3',
|
||||
],
|
||||
packages=find_packages(exclude=['bin', 'tests', 'functests']),
|
||||
packages=find_packages(exclude=['tests']),
|
||||
include_package_data=True,
|
||||
url='http://dataversioncontrol.com',
|
||||
entry_points={
|
||||
'console_scripts': ['dvc = dvc.main:main']
|
||||
},
|
||||
zip_safe=False
|
||||
zip_safe=False,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
pytest>=3.3.1
|
||||
nose>=1.3.7
|
||||
mock>=2.0.0
|
||||
coverage
|
||||
codecov
|
||||
coverage>=4.5.1
|
||||
codecov>=2.0.15
|
||||
xmltodict>=0.11.0
|
||||
awscli>=1.14.18
|
||||
google-compute-engine
|
||||
pywin32; sys_platform == 'win32'
|
||||
Pygments
|
||||
collective.checkdocs
|
||||
|
||||
+2
-1
@@ -7,4 +7,5 @@ os.chdir(REPO_ROOT)
|
||||
os.putenv('PATH', '{}:{}'.format(os.path.join(REPO_ROOT, 'bin'), os.getenv('PATH')))
|
||||
os.putenv('DVC_HOME', REPO_ROOT)
|
||||
|
||||
check_call('nosetests -v --processes=-1 --process-timeout=200 --cover-inclusive --cover-erase --cover-package=dvc --with-coverage', shell=True)
|
||||
cmd = 'nosetests -v --processes=-1 --process-timeout=200 --cover-inclusive --cover-erase --cover-package=dvc --with-coverage'
|
||||
check_call(cmd, shell=True)
|
||||
|
||||
+18
-1
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
import tempfile
|
||||
from git import Repo
|
||||
@@ -38,8 +39,15 @@ class TestDir(TestCase):
|
||||
with open(name, 'a') as f:
|
||||
f.write(contents)
|
||||
|
||||
@staticmethod
|
||||
def mkdtemp():
|
||||
prefix = 'dvc-test.{}.'.format(os.getpid())
|
||||
suffix = '.{}'.format(uuid.uuid4())
|
||||
return tempfile.mkdtemp(prefix=prefix, suffix=suffix)
|
||||
|
||||
def setUp(self):
|
||||
self._root_dir = tempfile.mkdtemp()
|
||||
self._root_dir = TestDir.mkdtemp()
|
||||
|
||||
self._pushd(self._root_dir)
|
||||
self.create(self.FOO, self.FOO_CONTENTS)
|
||||
self.create(self.BAR, self.BAR_CONTENTS)
|
||||
@@ -61,6 +69,15 @@ class TestGit(TestDir):
|
||||
self.git.index.commit('add code')
|
||||
|
||||
|
||||
class TestGitSubmodule(TestGit):
|
||||
def setUp(self):
|
||||
super(TestGitSubmodule, self).setUp()
|
||||
subrepo = Repo.init()
|
||||
subrepo_path = 'subrepo'
|
||||
self.git.create_submodule(subrepo_path, subrepo_path, subrepo.git_dir)
|
||||
self._pushd(subrepo_path)
|
||||
|
||||
|
||||
class TestDvc(TestGit):
|
||||
def setUp(self):
|
||||
super(TestDvc, self).setUp()
|
||||
|
||||
+59
-9
@@ -18,7 +18,10 @@ class TestAdd(TestDvc):
|
||||
def test(self):
|
||||
md5 = file_md5(self.FOO)[0]
|
||||
|
||||
stage = self.dvc.add(self.FOO)
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
|
||||
self.assertIsInstance(stage, Stage)
|
||||
self.assertTrue(os.path.isfile(stage.path))
|
||||
@@ -39,8 +42,10 @@ class TestAddDirectory(TestDvc):
|
||||
dname = 'directory'
|
||||
os.mkdir(dname)
|
||||
self.create(os.path.join(dname, 'file'), 'file')
|
||||
stage = self.dvc.add(dname)
|
||||
self.assertNotEqual(stage, None)
|
||||
stages = self.dvc.add(dname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
self.assertEqual(len(stage.deps), 0)
|
||||
self.assertEqual(len(stage.outs), 1)
|
||||
|
||||
@@ -51,12 +56,29 @@ class TestAddDirectory(TestDvc):
|
||||
self.assertTrue('\\' not in info['relpath'])
|
||||
|
||||
|
||||
class TestAddDirectoryRecursive(TestDvc):
|
||||
def test(self):
|
||||
stages = self.dvc.add(self.DATA_DIR, recursive=True)
|
||||
self.assertEqual(len(stages), 2)
|
||||
|
||||
|
||||
class TestAddCmdDirectoryRecursive(TestDvc):
|
||||
def test(self):
|
||||
ret = main(['add',
|
||||
'--recursive',
|
||||
self.DATA_DIR])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
|
||||
class TestAddDirectoryWithForwardSlash(TestDvc):
|
||||
def test(self):
|
||||
dname = 'directory/'
|
||||
os.mkdir(dname)
|
||||
self.create(os.path.join(dname, 'file'), 'file')
|
||||
stage = self.dvc.add(dname)
|
||||
stages = self.dvc.add(dname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
self.assertEquals(os.path.abspath('directory.dvc'), stage.path)
|
||||
|
||||
class TestAddTrackedFile(TestDvc):
|
||||
@@ -77,26 +99,54 @@ class TestAddDirWithExistingCache(TestDvc):
|
||||
os.mkdir(dname)
|
||||
shutil.copyfile(self.FOO, fname)
|
||||
|
||||
self.dvc.add(self.FOO)
|
||||
self.dvc.add(dname)
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
stages = self.dvc.add(dname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
|
||||
|
||||
class TestAddModifiedDir(TestDvc):
|
||||
def test(self):
|
||||
self.dvc.add(self.DATA_DIR)
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
os.unlink(self.DATA)
|
||||
self.dvc.add(self.DATA_DIR)
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
|
||||
|
||||
class TestAddFileInDir(TestDvc):
|
||||
def test(self):
|
||||
stage = self.dvc.add(self.DATA_SUB)
|
||||
stages = self.dvc.add(self.DATA_SUB)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertNotEqual(stage, None)
|
||||
self.assertEqual(len(stage.deps), 0)
|
||||
self.assertEqual(len(stage.outs), 1)
|
||||
self.assertEqual(stage.relpath, self.DATA_SUB + '.dvc')
|
||||
|
||||
|
||||
class TestAddExternalLocalFile(TestDvc):
|
||||
def test(self):
|
||||
dname = TestDvc.mkdtemp()
|
||||
fname = os.path.join(dname, 'foo')
|
||||
shutil.copyfile(self.FOO, fname)
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertNotEqual(stage, None)
|
||||
self.assertEqual(len(stage.deps), 0)
|
||||
self.assertEqual(len(stage.outs), 1)
|
||||
self.assertEqual(stage.relpath, 'foo.dvc')
|
||||
self.assertEqual(len(os.listdir(dname)), 1)
|
||||
self.assertTrue(os.path.isfile(fname))
|
||||
self.assertTrue(filecmp.cmp(fname, 'foo', shallow=False))
|
||||
|
||||
|
||||
class TestCmdAdd(TestDvc):
|
||||
def test(self):
|
||||
ret = main(['add',
|
||||
|
||||
+10
-2
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from dvc.cache import Cache
|
||||
from dvc.system import System
|
||||
@@ -52,7 +51,7 @@ class TestCacheLoadBadDirCache(TestDvc):
|
||||
|
||||
class TestExternalCacheDir(TestDvc):
|
||||
def test(self):
|
||||
cache_dir = tempfile.mkdtemp()
|
||||
cache_dir = TestDvc.mkdtemp()
|
||||
|
||||
ret = main(['config', 'cache.dir', cache_dir])
|
||||
self.assertEqual(ret, 0)
|
||||
@@ -103,3 +102,12 @@ class TestSharedCacheDir(TestDir):
|
||||
self.assertEqual(len(os.listdir(os.path.join(cache_dir, subdirs[0]))), 1)
|
||||
self.assertEqual(len(os.listdir(os.path.join(cache_dir, subdirs[1]))), 1)
|
||||
self.assertEqual(len(os.listdir(os.path.join(cache_dir, subdirs[2]))), 1)
|
||||
|
||||
|
||||
class TestCacheLinkType(TestDvc):
|
||||
def test(self):
|
||||
ret = main(['config', 'cache.type', 'reflink,copy'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(['add', self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
+151
-13
@@ -4,8 +4,11 @@ import time
|
||||
import stat
|
||||
import shutil
|
||||
import filecmp
|
||||
import collections
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.project import Project
|
||||
from dvc.system import System
|
||||
from tests.basic_env import TestDvc
|
||||
from tests.test_repro import TestRepro
|
||||
from dvc.stage import Stage
|
||||
@@ -16,7 +19,11 @@ class TestCheckout(TestRepro):
|
||||
def setUp(self):
|
||||
super(TestCheckout, self).setUp()
|
||||
|
||||
self.data_dir_stage = self.dvc.add(self.DATA_DIR)
|
||||
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.data_dir_stage = stages[0]
|
||||
self.assertTrue(self.data_dir_stage is not None)
|
||||
|
||||
self.orig = 'orig'
|
||||
shutil.copy(self.FOO, self.orig)
|
||||
@@ -32,7 +39,7 @@ class TestCheckout(TestRepro):
|
||||
|
||||
def _test_checkout(self):
|
||||
self.assertTrue(os.path.isfile(self.FOO))
|
||||
self.assertTrue(filecmp.cmp(self.FOO, self.orig))
|
||||
self.assertTrue(filecmp.cmp(self.FOO, self.orig, shallow=False))
|
||||
|
||||
|
||||
class TestCheckoutSingleStage(TestCheckout):
|
||||
@@ -61,6 +68,34 @@ class TestCheckoutCorruptedCacheFile(TestRepro):
|
||||
self.assertFalse(os.path.isfile(cache))
|
||||
|
||||
|
||||
class TestCheckoutCorruptedCacheDir(TestDvc):
|
||||
def test(self):
|
||||
time.sleep(1)
|
||||
|
||||
# NOTE: using 'copy' so that cache and link don't have same inode
|
||||
ret = main(['config', 'cache.type', 'copy'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.dvc = Project('.')
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertEqual(len(stages[0].outs), 1)
|
||||
out = stages[0].outs[0]
|
||||
|
||||
# NOTE: modifying cache file for one of the files inside the directory
|
||||
# to check if dvc will detect that the cache is corrupted.
|
||||
entry = self.dvc.cache.local.load_dir_cache(out.md5)[0]
|
||||
md5 = entry[self.dvc.cache.local.PARAM_MD5]
|
||||
cache = self.dvc.cache.local.get(md5)
|
||||
|
||||
with open(cache, 'w+') as fobj:
|
||||
fobj.write('1')
|
||||
|
||||
self.dvc.checkout()
|
||||
|
||||
self.assertFalse(os.path.exists(cache))
|
||||
|
||||
|
||||
class TestCmdCheckout(TestCheckout):
|
||||
def test(self):
|
||||
ret = main(['checkout'])
|
||||
@@ -74,13 +109,29 @@ class CheckoutBase(TestDvc):
|
||||
def commit_data_file(self, fname, content='random text'):
|
||||
with open(fname, 'w') as fd:
|
||||
fd.write(content)
|
||||
self.dvc.add(fname)
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
self.dvc.scm.add([fname + '.dvc', '.gitignore'])
|
||||
self.dvc.scm.commit('adding ' + fname)
|
||||
|
||||
def read_ignored(self):
|
||||
return list(map(lambda s: s.strip('\n'), open(self.GIT_IGNORE).readlines()))
|
||||
|
||||
def outs_info(self, stage):
|
||||
FileInfo = collections.namedtuple('FileInfo', 'path inode')
|
||||
|
||||
paths = [
|
||||
os.path.join(root, file)
|
||||
for output in stage.outs
|
||||
for root, _, files in os.walk(output.path)
|
||||
for file in files
|
||||
]
|
||||
|
||||
return [
|
||||
FileInfo(path=path, inode=System.inode(path))
|
||||
for path in paths
|
||||
]
|
||||
|
||||
class TestRemoveFilesWhenCheckout(CheckoutBase):
|
||||
def test(self):
|
||||
@@ -93,7 +144,8 @@ class TestRemoveFilesWhenCheckout(CheckoutBase):
|
||||
|
||||
# add the file into a separate branch
|
||||
self.dvc.scm.checkout(branch_1, True)
|
||||
main(['checkout'])
|
||||
ret = main(['checkout'])
|
||||
self.assertEqual(ret, 0)
|
||||
self.commit_data_file(fname)
|
||||
|
||||
# Checkout back in master
|
||||
@@ -102,10 +154,63 @@ class TestRemoveFilesWhenCheckout(CheckoutBase):
|
||||
|
||||
# Make sure `dvc checkout` removes the file
|
||||
# self.dvc.checkout()
|
||||
main(['checkout'])
|
||||
ret = main(['checkout'])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertFalse(os.path.exists(fname))
|
||||
|
||||
|
||||
class TestCheckoutCleanWorkingDir(CheckoutBase):
|
||||
def test(self):
|
||||
from tests.test_data_cloud import sleep
|
||||
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
stage = stages[0]
|
||||
|
||||
sleep()
|
||||
|
||||
working_dir_change = os.path.join(self.DATA_DIR, 'not_cached.txt')
|
||||
with open(working_dir_change, 'w') as f:
|
||||
f.write('not_cached')
|
||||
|
||||
sleep()
|
||||
|
||||
ret = main(['checkout', stage.relpath])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertFalse(os.path.exists(working_dir_change))
|
||||
|
||||
|
||||
class TestCheckoutSelectiveRemove(CheckoutBase):
|
||||
def test(self):
|
||||
from tests.test_data_cloud import sleep
|
||||
|
||||
# Use copy to test for changes in the inodes
|
||||
ret = main(['config', 'cache.type', 'copy'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
staged_files = self.outs_info(stage)
|
||||
|
||||
sleep()
|
||||
|
||||
os.remove(staged_files[0].path)
|
||||
|
||||
sleep()
|
||||
|
||||
ret = main(['checkout', stage.relpath])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
sleep()
|
||||
|
||||
checkedout_files = self.outs_info(stage)
|
||||
|
||||
self.assertEqual(len(staged_files), len(checkedout_files))
|
||||
self.assertEqual(staged_files[0].path, checkedout_files[0].path)
|
||||
self.assertNotEqual(staged_files[0].inode, checkedout_files[0].inode)
|
||||
self.assertEqual(staged_files[1].inode, checkedout_files[1].inode)
|
||||
|
||||
|
||||
class TestGitIgnoreBasic(CheckoutBase):
|
||||
def test(self):
|
||||
fname1 = 'file_1'
|
||||
@@ -129,8 +234,8 @@ class TestGitIgnoreBasic(CheckoutBase):
|
||||
|
||||
self.assertEqual(len(ignored), 2)
|
||||
|
||||
self.assertIn(fname1, ignored)
|
||||
self.assertIn(fname2, ignored)
|
||||
self.assertIn('/' + fname1, ignored)
|
||||
self.assertIn('/' + fname2, ignored)
|
||||
|
||||
|
||||
class TestGitIgnoreWhenCheckout(CheckoutBase):
|
||||
@@ -145,21 +250,24 @@ class TestGitIgnoreWhenCheckout(CheckoutBase):
|
||||
self.commit_data_file(fname_master)
|
||||
|
||||
self.dvc.scm.checkout(branch_1, True)
|
||||
main(['checkout'])
|
||||
ret = main(['checkout'])
|
||||
self.assertEqual(ret, 0)
|
||||
self.commit_data_file(fname_branch)
|
||||
|
||||
self.dvc.scm.checkout(branch_master)
|
||||
main(['checkout'])
|
||||
ret = main(['checkout'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ignored = self.read_ignored()
|
||||
|
||||
self.assertEqual(len(ignored), 1)
|
||||
self.assertIn(fname_master, ignored)
|
||||
self.assertIn('/' + fname_master, ignored)
|
||||
|
||||
self.dvc.scm.checkout(branch_1)
|
||||
main(['checkout'])
|
||||
ret = main(['checkout'])
|
||||
self.assertEqual(ret, 0)
|
||||
ignored = self.read_ignored()
|
||||
self.assertIn(fname_branch, ignored)
|
||||
self.assertIn('/' + fname_branch, ignored)
|
||||
|
||||
|
||||
class TestCheckoutMissingMd5InStageFile(TestRepro):
|
||||
@@ -181,7 +289,10 @@ class TestCheckoutEmptyDir(TestDvc):
|
||||
dname = 'empty_dir'
|
||||
os.mkdir(dname)
|
||||
|
||||
stage = self.dvc.add(dname)
|
||||
stages = self.dvc.add(dname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
self.assertEqual(len(stage.outs), 1)
|
||||
|
||||
stage.outs[0].remove()
|
||||
@@ -191,3 +302,30 @@ class TestCheckoutEmptyDir(TestDvc):
|
||||
|
||||
self.assertTrue(os.path.isdir(dname))
|
||||
self.assertEqual(len(os.listdir(dname)), 0)
|
||||
|
||||
|
||||
class TestCheckoutNotCachedFile(TestDvc):
|
||||
def test(self):
|
||||
cmd = 'python {} {} {}'.format(self.CODE, self.FOO, 'out')
|
||||
|
||||
self.dvc.add(self.FOO)
|
||||
stage = self.dvc.run(cmd=cmd,
|
||||
deps=[self.FOO, self.CODE],
|
||||
outs_no_cache=['out'])
|
||||
|
||||
self.dvc.checkout()
|
||||
|
||||
|
||||
class TestCheckoutWithDeps(TestRepro):
|
||||
def test(self):
|
||||
os.unlink(self.FOO)
|
||||
os.unlink(self.file1)
|
||||
|
||||
self.assertFalse(os.path.exists(self.FOO))
|
||||
self.assertFalse(os.path.exists(self.file1))
|
||||
|
||||
ret = main(['checkout', self.file1_stage, '--with-deps'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertTrue(os.path.exists(self.FOO))
|
||||
self.assertTrue(os.path.exists(self.file1))
|
||||
|
||||
@@ -129,6 +129,20 @@ class TestGC(TestDvc):
|
||||
self.assertIsInstance(args.func(args), CmdGC)
|
||||
|
||||
|
||||
class TestGCMultipleProjects(TestDvc):
|
||||
def test(self):
|
||||
args = parse_args([
|
||||
'gc',
|
||||
'-p',
|
||||
'/tmp/asdf',
|
||||
'/tmp/xyz'
|
||||
])
|
||||
|
||||
self.assertIsInstance(args.func(args), CmdGC)
|
||||
|
||||
self.assertEqual(args.projects, ['/tmp/asdf', '/tmp/xyz'])
|
||||
|
||||
|
||||
class TestConfig(TestDvc):
|
||||
def test(self):
|
||||
name = 'param'
|
||||
|
||||
@@ -28,6 +28,11 @@ class TestConfigCLI(TestDvc):
|
||||
ret = main(['root'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
# NOTE: check that `dvc root` is not blocked with dvc lock
|
||||
with self.dvc.lock:
|
||||
ret = main(['root'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def _do_test(self, local=False):
|
||||
section = 'setsection'
|
||||
field = 'setfield'
|
||||
|
||||
+88
-59
@@ -1,12 +1,11 @@
|
||||
from subprocess import CalledProcessError
|
||||
from subprocess import check_output
|
||||
from subprocess import check_output, Popen
|
||||
from unittest import SkipTest
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import shutil
|
||||
import getpass
|
||||
import tempfile
|
||||
import platform
|
||||
|
||||
from dvc.main import main
|
||||
@@ -94,11 +93,18 @@ def _should_test_hdfs():
|
||||
except (CalledProcessError, IOError):
|
||||
return False
|
||||
|
||||
p = Popen('hadoop fs -ls hdfs://127.0.0.1/',
|
||||
shell=True,
|
||||
executable=os.getenv('SHELL'))
|
||||
p.communicate()
|
||||
if p.returncode != 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_local_storagepath():
|
||||
return tempfile.mkdtemp()
|
||||
return TestDvc.mkdtemp()
|
||||
|
||||
|
||||
def get_local_url():
|
||||
@@ -154,10 +160,10 @@ class TestDataCloud(TestDvc):
|
||||
def test(self):
|
||||
config = TEST_CONFIG
|
||||
|
||||
clist = [('s3://', RemoteS3),
|
||||
('gs://', RemoteGS),
|
||||
clist = [('s3://mybucket/', RemoteS3),
|
||||
('gs://mybucket/', RemoteGS),
|
||||
('ssh://user@localhost:/', RemoteSSH),
|
||||
(tempfile.mkdtemp(), RemoteLOCAL)]
|
||||
(TestDvc.mkdtemp(), RemoteLOCAL)]
|
||||
|
||||
if _should_test_azure():
|
||||
clist.append(('azure://ContainerName=', RemoteAzure))
|
||||
@@ -204,65 +210,73 @@ class TestDataCloudBase(TestDvc):
|
||||
def _test_cloud(self):
|
||||
self._setup_cloud()
|
||||
|
||||
stage = self.dvc.add(self.FOO)
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
cache = stage.outs[0].cache
|
||||
info = stage.outs[0].info
|
||||
info = stage.outs[0].dumpd()
|
||||
md5 = info['md5']
|
||||
|
||||
stage_dir = self.dvc.add(self.DATA_DIR)
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage_dir = stages[0]
|
||||
self.assertTrue(stage_dir is not None)
|
||||
|
||||
cache_dir = stage_dir.outs[0].cache
|
||||
info_dir = stage_dir.outs[0].info
|
||||
info_dir = stage_dir.outs[0].dumpd()
|
||||
md5_dir = info_dir['md5']
|
||||
|
||||
# Check status
|
||||
sleep()
|
||||
status = self.cloud.status([info])
|
||||
self.assertEqual([(md5, STATUS_NEW)], status)
|
||||
with self.cloud.project.state:
|
||||
# Check status
|
||||
sleep()
|
||||
status = self.cloud.status([info], show_checksums=True)
|
||||
self.assertEqual([(md5, STATUS_NEW)], status)
|
||||
|
||||
status_dir = self.cloud.status([info_dir])
|
||||
self.assertTrue((md5_dir, STATUS_NEW) in status_dir)
|
||||
status_dir = self.cloud.status([info_dir], show_checksums=True)
|
||||
self.assertTrue((md5_dir, STATUS_NEW) in status_dir)
|
||||
|
||||
# Push and check status
|
||||
self.cloud.push([info])
|
||||
self.assertTrue(os.path.exists(cache))
|
||||
self.assertTrue(os.path.isfile(cache))
|
||||
# Push and check status
|
||||
self.cloud.push([info])
|
||||
self.assertTrue(os.path.exists(cache))
|
||||
self.assertTrue(os.path.isfile(cache))
|
||||
|
||||
self.cloud.push([info_dir])
|
||||
self.assertTrue(os.path.isfile(cache_dir))
|
||||
self.cloud.push([info_dir])
|
||||
self.assertTrue(os.path.isfile(cache_dir))
|
||||
|
||||
sleep()
|
||||
status = self.cloud.status([info])
|
||||
self.assertEqual([(md5, STATUS_OK)], status)
|
||||
sleep()
|
||||
status = self.cloud.status([info], show_checksums=True)
|
||||
self.assertEqual([(md5, STATUS_OK)], status)
|
||||
|
||||
status_dir = self.cloud.status([info_dir])
|
||||
self.assertTrue((md5_dir, STATUS_OK) in status_dir)
|
||||
status_dir = self.cloud.status([info_dir], show_checksums=True)
|
||||
self.assertTrue((md5_dir, STATUS_OK) in status_dir)
|
||||
|
||||
# Remove and check status
|
||||
sleep()
|
||||
shutil.rmtree(self.dvc.cache.local.cache_dir)
|
||||
# Remove and check status
|
||||
sleep()
|
||||
shutil.rmtree(self.dvc.cache.local.cache_dir)
|
||||
|
||||
status = self.cloud.status([info])
|
||||
self.assertEqual([(md5, STATUS_DELETED)], status)
|
||||
status = self.cloud.status([info], show_checksums=True)
|
||||
self.assertEqual([(md5, STATUS_DELETED)], status)
|
||||
|
||||
status_dir = self.cloud.status([info_dir])
|
||||
self.assertEqual([(md5_dir, STATUS_DELETED)], status_dir)
|
||||
status_dir = self.cloud.status([info_dir], show_checksums=True)
|
||||
self.assertEqual([(md5_dir, STATUS_DELETED)], status_dir)
|
||||
|
||||
# Pull and check status
|
||||
self.cloud.pull([info])
|
||||
self.assertTrue(os.path.exists(cache))
|
||||
self.assertTrue(os.path.isfile(cache))
|
||||
with open(cache, 'r') as fd:
|
||||
self.assertEqual(fd.read(), self.FOO_CONTENTS)
|
||||
# Pull and check status
|
||||
self.cloud.pull([info])
|
||||
self.assertTrue(os.path.exists(cache))
|
||||
self.assertTrue(os.path.isfile(cache))
|
||||
with open(cache, 'r') as fd:
|
||||
self.assertEqual(fd.read(), self.FOO_CONTENTS)
|
||||
|
||||
self.cloud.pull([info_dir])
|
||||
self.assertTrue(os.path.isfile(cache_dir))
|
||||
self.cloud.pull([info_dir])
|
||||
self.assertTrue(os.path.isfile(cache_dir))
|
||||
|
||||
sleep()
|
||||
status = self.cloud.status([info])
|
||||
self.assertEqual([(md5, STATUS_OK)], status)
|
||||
sleep()
|
||||
status = self.cloud.status([info], show_checksums=True)
|
||||
self.assertEqual([(md5, STATUS_OK)], status)
|
||||
|
||||
status_dir = self.cloud.status([info_dir])
|
||||
self.assertTrue((md5_dir, STATUS_OK) in status_dir)
|
||||
status_dir = self.cloud.status([info_dir], show_checksums=True)
|
||||
self.assertTrue((md5_dir, STATUS_OK) in status_dir)
|
||||
|
||||
def test(self):
|
||||
self._ensure_should_run()
|
||||
@@ -328,9 +342,6 @@ class TestRemoteSSH(TestDataCloudBase):
|
||||
def _get_cloud_class(self):
|
||||
return RemoteSSH
|
||||
|
||||
def test(self):
|
||||
super(TestRemoteSSH, self).test()
|
||||
|
||||
|
||||
class TestRemoteHDFS(TestDataCloudBase):
|
||||
def _should_test(self):
|
||||
@@ -342,9 +353,6 @@ class TestRemoteHDFS(TestDataCloudBase):
|
||||
def _get_cloud_class(self):
|
||||
return RemoteHDFS
|
||||
|
||||
def test(self):
|
||||
super(TestRemoteHDFS, self).test()
|
||||
|
||||
|
||||
class TestDataCloudCLIBase(TestDvc):
|
||||
def main(self, args):
|
||||
@@ -358,15 +366,21 @@ class TestDataCloudCLIBase(TestDvc):
|
||||
else:
|
||||
args += []
|
||||
|
||||
stage = self.dvc.add(self.FOO)
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
cache = stage.outs[0].cache
|
||||
|
||||
stage_dir = self.dvc.add(self.DATA_DIR)
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage_dir = stages[0]
|
||||
self.assertTrue(stage_dir is not None)
|
||||
cache_dir = stage_dir.outs[0].cache
|
||||
|
||||
# FIXME check status output
|
||||
sleep()
|
||||
self.main(['status', '-c'] + args)
|
||||
self.main(['status', '-c', '--show-checksums'] + args)
|
||||
|
||||
self.main(['push'] + args)
|
||||
self.assertTrue(os.path.exists(cache))
|
||||
@@ -374,12 +388,12 @@ class TestDataCloudCLIBase(TestDvc):
|
||||
self.assertTrue(os.path.isfile(cache_dir))
|
||||
|
||||
sleep()
|
||||
self.main(['status', '-c'] + args)
|
||||
self.main(['status', '-c', '--show-checksums'] + args)
|
||||
|
||||
shutil.rmtree(self.dvc.cache.local.cache_dir)
|
||||
|
||||
sleep()
|
||||
self.main(['status', '-c'] + args)
|
||||
self.main(['status', '-c', '--show-checksums'] + args)
|
||||
|
||||
self.main(['fetch'] + args)
|
||||
self.assertTrue(os.path.exists(cache))
|
||||
@@ -398,7 +412,22 @@ class TestDataCloudCLIBase(TestDvc):
|
||||
self.assertEqual(fd.read(), self.FOO_CONTENTS)
|
||||
self.assertTrue(os.path.isfile(cache_dir))
|
||||
|
||||
self.main(['status', '-c'] + args)
|
||||
self.main(['status', '-c', '--show-checksums'] + args)
|
||||
|
||||
# NOTE: check if remote gc works correctly on directories
|
||||
self.main(['gc', '-c', '-f'] + args)
|
||||
shutil.move(self.dvc.cache.local.cache_dir, self.dvc.cache.local.cache_dir + '.back')
|
||||
|
||||
sleep()
|
||||
self.main(['fetch'] + args)
|
||||
|
||||
sleep()
|
||||
self.main(['pull'] + args)
|
||||
self.assertTrue(os.path.exists(cache))
|
||||
self.assertTrue(os.path.isfile(cache))
|
||||
self.assertTrue(os.path.isfile(cache_dir))
|
||||
self.assertTrue(os.path.isfile(self.FOO))
|
||||
self.assertTrue(os.path.isdir(self.DATA_DIR))
|
||||
|
||||
def _should_test(self):
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from dvc.dependency import _get
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
|
||||
class TestUnsupported(TestDvc):
|
||||
def test(self):
|
||||
with self.assertRaises(DvcException):
|
||||
_get(None, 'unsupported://a/b', None)
|
||||
+141
-2
@@ -1,13 +1,18 @@
|
||||
import os
|
||||
import shutil
|
||||
import filecmp
|
||||
import configobj
|
||||
|
||||
from git import Repo
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.utils import file_md5
|
||||
from dvc.stage import Stage
|
||||
from dvc.project import Project
|
||||
from dvc.command.gc import CmdGC
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
from tests.basic_env import TestDir
|
||||
|
||||
|
||||
class TestGC(TestDvc):
|
||||
@@ -16,7 +21,8 @@ class TestGC(TestDvc):
|
||||
|
||||
self.dvc.add(self.FOO)
|
||||
self.dvc.add(self.DATA_DIR)
|
||||
self.good_cache = [self.dvc.cache.local.get(md5) for md5 in self.dvc.cache.local.all()]
|
||||
self.good_cache = [self.dvc.cache.local.get(
|
||||
md5) for md5 in self.dvc.cache.local.all()]
|
||||
|
||||
self.bad_cache = []
|
||||
for i in ['123', '234', '345']:
|
||||
@@ -29,7 +35,8 @@ class TestGC(TestDvc):
|
||||
self._test_gc()
|
||||
|
||||
def test_cli(self):
|
||||
ret = main(['gc'])
|
||||
ret = main(['gc', '-f'])
|
||||
self.assertEqual(ret, 0)
|
||||
self._test_gc()
|
||||
|
||||
def _test_gc(self):
|
||||
@@ -39,3 +46,135 @@ class TestGC(TestDvc):
|
||||
|
||||
for c in self.good_cache:
|
||||
self.assertTrue(os.path.exists(c))
|
||||
|
||||
|
||||
class TestGCBranchesTags(TestDvc):
|
||||
def _check_cache(self, num):
|
||||
total = 0
|
||||
for root, dirs, files in os.walk(os.path.join('.dvc', 'cache')):
|
||||
total += len(files)
|
||||
self.assertEqual(total, num)
|
||||
|
||||
def test(self):
|
||||
fname = 'file'
|
||||
|
||||
with open(fname, 'w+') as fobj:
|
||||
fobj.write('v1.0')
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.dvc.scm.add(['.gitignore', stages[0].relpath])
|
||||
self.dvc.scm.commit('v1.0')
|
||||
self.dvc.scm.tag('v1.0')
|
||||
|
||||
self.dvc.scm.checkout('test', create_new=True)
|
||||
self.dvc.remove(stages[0].relpath, outs_only=True)
|
||||
with open(fname, 'w+') as fobj:
|
||||
fobj.write('test')
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.dvc.scm.add(['.gitignore', stages[0].relpath])
|
||||
self.dvc.scm.commit('test')
|
||||
|
||||
self.dvc.scm.checkout('master')
|
||||
self.dvc.remove(stages[0].relpath, outs_only=True)
|
||||
with open(fname, 'w+') as fobj:
|
||||
fobj.write('trash')
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.dvc.scm.add(['.gitignore', stages[0].relpath])
|
||||
self.dvc.scm.commit('trash')
|
||||
|
||||
self.dvc.remove(stages[0].relpath, outs_only=True)
|
||||
with open(fname, 'w+') as fobj:
|
||||
fobj.write('master')
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.dvc.scm.add(['.gitignore', stages[0].relpath])
|
||||
self.dvc.scm.commit('master')
|
||||
|
||||
self._check_cache(4)
|
||||
|
||||
self.dvc.gc(all_tags=True, all_branches=True)
|
||||
|
||||
self._check_cache(3)
|
||||
|
||||
self.dvc.gc(all_tags=False, all_branches=True)
|
||||
|
||||
self._check_cache(2)
|
||||
|
||||
self.dvc.gc(all_tags=True, all_branches=False)
|
||||
|
||||
self._check_cache(1)
|
||||
|
||||
|
||||
class TestGCMultipleProjects(TestDvc):
|
||||
def _check_cache(self, num):
|
||||
total = 0
|
||||
for root, dirs, files in os.walk(os.path.join('.dvc', 'cache')):
|
||||
total += len(files)
|
||||
self.assertEqual(total, num)
|
||||
|
||||
def setUp(self):
|
||||
super(TestGCMultipleProjects, self).setUp()
|
||||
self.additional_path = TestDir.mkdtemp()
|
||||
self.additional_git = Repo.init(self.additional_path)
|
||||
self.additional_dvc = Project.init(self.additional_path)
|
||||
|
||||
cache_path = os.path.join(self._root_dir, '.dvc', 'cache')
|
||||
config_path = os.path.join(self.additional_path, '.dvc',
|
||||
'config.local')
|
||||
cfg = configobj.ConfigObj()
|
||||
cfg.filename = config_path
|
||||
cfg['cache'] = {
|
||||
'dir': cache_path
|
||||
}
|
||||
cfg.write()
|
||||
|
||||
self.additional_dvc = Project(self.additional_path)
|
||||
|
||||
def test(self):
|
||||
|
||||
# ADD FILE ONLY IN MAIN PROJECT
|
||||
fname = 'only_in_first'
|
||||
with open(fname, 'w+') as fobj:
|
||||
fobj.write('only in main project')
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
|
||||
# ADD FILE IN MAIN PROJECT THAT IS ALSO IN SECOND PROJECT
|
||||
fname = 'in_both'
|
||||
with open(fname, 'w+') as fobj:
|
||||
fobj.write('in both projects')
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
|
||||
cwd = os.getcwd()
|
||||
os.chdir(self.additional_path)
|
||||
# ADD FILE ONLY IN SECOND PROJECT
|
||||
fname = os.path.join(self.additional_path, 'only_in_second')
|
||||
with open(fname, 'w+') as fobj:
|
||||
fobj.write('only in additional project')
|
||||
|
||||
stages = self.additional_dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
|
||||
# ADD FILE IN SECOND PROJECT THAT IS ALSO IN MAIN PROJECT
|
||||
fname = os.path.join(self.additional_path, 'in_both')
|
||||
with open(fname, 'w+') as fobj:
|
||||
fobj.write('in both projects')
|
||||
|
||||
stages = self.additional_dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
|
||||
os.chdir(cwd)
|
||||
|
||||
self._check_cache(3)
|
||||
|
||||
self.dvc.gc(projects=[self.additional_path])
|
||||
self._check_cache(3)
|
||||
|
||||
self.dvc.gc()
|
||||
self._check_cache(2)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import stat
|
||||
import shutil
|
||||
import filecmp
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.command.imp import CmdImport
|
||||
|
||||
@@ -13,7 +13,11 @@ class TestInstall(TestDvc):
|
||||
ret = main(['install'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
self.assertTrue(os.path.isfile('.git/hooks/post-checkout'))
|
||||
def hook(name):
|
||||
return os.path.join('.git', 'hooks', name)
|
||||
|
||||
self.assertTrue(os.path.isfile(hook('post-checkout')))
|
||||
self.assertTrue(os.path.isfile(hook('pre-commit')))
|
||||
|
||||
self.dvc.add(self.FOO)
|
||||
os.unlink(self.FOO)
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
from mock import patch
|
||||
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
from io import StringIO
|
||||
|
||||
import colorama
|
||||
import logging
|
||||
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.config import Config
|
||||
from dvc.logger import Logger
|
||||
from dvc.main import main
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
|
||||
class StringContaining(str):
|
||||
def __eq__(self, other):
|
||||
return self in other
|
||||
|
||||
|
||||
class TestLogger(TestDvc):
|
||||
def test_config(self):
|
||||
Logger('debug')
|
||||
@@ -34,6 +47,28 @@ class TestLogger(TestDvc):
|
||||
# This is not a tty, so it should not colorize anything
|
||||
self.assertEqual(msg, name)
|
||||
|
||||
@patch('sys.stderr', new_callable=StringIO)
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_stderr(self, mock_stdout, mock_stderr):
|
||||
error_message = 'error msg'
|
||||
Logger.init()
|
||||
Logger.error(error_message)
|
||||
self.assertEqual('', mock_stdout.getvalue())
|
||||
self.assertEqual('Error: {}\n\nHaving any troubles? '
|
||||
'Hit us up at dvc.org/support, we '
|
||||
'are always happy to help!\n'.format(error_message),
|
||||
mock_stderr.getvalue())
|
||||
|
||||
@patch('sys.stderr', new_callable=StringIO)
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_stdout(self, mock_stdout, mock_stderr):
|
||||
non_error_message = 'non-error message'
|
||||
Logger.init()
|
||||
Logger.info(non_error_message)
|
||||
self.assertEqual('', mock_stderr.getvalue())
|
||||
self.assertEqual('{}\n'.format(non_error_message),
|
||||
mock_stdout.getvalue())
|
||||
|
||||
|
||||
class TestLoggerCLI(TestDvc):
|
||||
def test(self):
|
||||
|
||||
@@ -110,6 +110,9 @@ class TestMetricsReproCLI(TestDvc):
|
||||
ret = main(['metrics', 'add', 'metrics'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(['metrics', 'modify', '-t', 'csv', '-x', '0,0', 'metrics'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(['repro', '-f', '-m', stage.path])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
@@ -195,6 +198,9 @@ class TestMetricsCLI(TestMetrics):
|
||||
ret = main(['metrics', 'add', 'non-existing'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
ret = main(['metrics', 'modify', 'non-existing'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
ret = main(['metrics', 'remove', 'non-existing'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
@@ -213,7 +219,9 @@ class TestCachedMetrics(TestDvc):
|
||||
with open('metrics.json', 'w+') as fd:
|
||||
json.dump({'metrics': branch}, fd)
|
||||
|
||||
stage = self.dvc.add('metrics.json')
|
||||
stages = self.dvc.add('metrics.json')
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertNotEqual(stage, None)
|
||||
|
||||
self.dvc.scm.add(['.gitignore', 'metrics.json.dvc'])
|
||||
|
||||
+14
-1
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.exceptions import DvcException, MoveNotDataSourceError
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
from tests.test_repro import TestRepro
|
||||
|
||||
|
||||
class TestMove(TestDvc):
|
||||
@@ -39,3 +40,15 @@ class TestCmdMove(TestDvc):
|
||||
|
||||
ret = main(['move', 'non-existing-file', 'dst'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
|
||||
class TestMoveNotDataSource(TestRepro):
|
||||
def test(self):
|
||||
from dvc.project import Project
|
||||
|
||||
self.dvc = Project(self._root_dir)
|
||||
with self.assertRaises(MoveNotDataSourceError):
|
||||
self.dvc.move(self.file1, 'dst')
|
||||
|
||||
ret = main(['move', self.file1, 'dst'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from dvc.output import _get
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
|
||||
class TestUnsupported(TestDvc):
|
||||
def test(self):
|
||||
with self.assertRaises(DvcException):
|
||||
_get(None, 'unsupported://a/b', None, None, None)
|
||||
+155
-1
@@ -1,9 +1,71 @@
|
||||
from dvc.main import main
|
||||
|
||||
from tests.test_repro import TestRepro
|
||||
from tests.basic_env import TestDvc
|
||||
from tests.test_repro import TestRepro, TestReproChangedDeepData
|
||||
import os
|
||||
|
||||
|
||||
class TestPipelineShowSingle(TestDvc):
|
||||
def setUp(self):
|
||||
super(TestPipelineShowSingle, self).setUp()
|
||||
self.stage = 'foo.dvc'
|
||||
self.dotFile = 'graph.dot'
|
||||
ret = main(['add', self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test(self):
|
||||
ret = main(['pipeline', 'show', self.stage])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_commands(self):
|
||||
ret = main(['pipeline', 'show', self.stage, '--commands'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_outs(self):
|
||||
ret = main(['pipeline', 'show', self.stage, '--outs'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_ascii(self):
|
||||
ret = main(['pipeline', 'show', '--ascii', self.stage])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_dot(self):
|
||||
ret = main(['pipeline', 'show', '--dot', self.dotFile, self.stage])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.dotFile))
|
||||
|
||||
def test_ascii_commands(self):
|
||||
ret = main(['pipeline', 'show', '--ascii', self.stage, '--commands'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_ascii_outs(self):
|
||||
ret = main(['pipeline', 'show', '--ascii', self.stage, '--outs'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_dot_commands(self):
|
||||
ret = main(['pipeline', 'show', '--dot', self.dotFile, self.stage, '--commands'])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.dotFile))
|
||||
|
||||
def test_dot_outs(self):
|
||||
ret = main(['pipeline', 'show', '--dot', self.dotFile, self.stage, '--outs'])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.dotFile))
|
||||
|
||||
def test_not_dvc_file(self):
|
||||
ret = main(['pipeline', 'show', self.FOO])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
def test_non_existing(self):
|
||||
ret = main(['pipeline', 'show', 'non-existing'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
|
||||
class TestPipelineShow(TestRepro):
|
||||
def setUp(self):
|
||||
super(TestPipelineShow, self).setUp()
|
||||
self.dotFile = 'graph.dot'
|
||||
|
||||
def test(self):
|
||||
ret = main(['pipeline', 'show', self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
@@ -16,6 +78,33 @@ class TestPipelineShow(TestRepro):
|
||||
ret = main(['pipeline', 'show', self.file1_stage, '--outs'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_ascii(self):
|
||||
ret = main(['pipeline', 'show', '--ascii', self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_dot(self):
|
||||
ret = main(['pipeline', 'show', '--dot', self.dotFile, self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.dotFile))
|
||||
|
||||
def test_ascii_commands(self):
|
||||
ret = main(['pipeline', 'show', '--ascii', self.file1_stage, '--commands'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_ascii_outs(self):
|
||||
ret = main(['pipeline', 'show', '--ascii', self.file1_stage, '--outs'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_dot_commands(self):
|
||||
ret = main(['pipeline', 'show', '--dot', self.dotFile, self.file1_stage, '--commands'])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.dotFile))
|
||||
|
||||
def test_dot_outs(self):
|
||||
ret = main(['pipeline', 'show', '--dot', self.dotFile, self.file1_stage, '--outs'])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.dotFile))
|
||||
|
||||
def test_not_dvc_file(self):
|
||||
ret = main(['pipeline', 'show', self.file1])
|
||||
self.assertNotEqual(ret, 0)
|
||||
@@ -23,3 +112,68 @@ class TestPipelineShow(TestRepro):
|
||||
def test_non_existing(self):
|
||||
ret = main(['pipeline', 'show', 'non-existing'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
|
||||
class TestPipelineShowDeep(TestReproChangedDeepData):
|
||||
def setUp(self):
|
||||
super(TestPipelineShowDeep, self).setUp()
|
||||
self.dotFile = 'graph.dot'
|
||||
|
||||
def test(self):
|
||||
ret = main(['pipeline', 'show', self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_commands(self):
|
||||
ret = main(['pipeline', 'show', self.file1_stage, '--commands'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_outs(self):
|
||||
ret = main(['pipeline', 'show', self.file1_stage, '--outs'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_ascii(self):
|
||||
ret = main(['pipeline', 'show', '--ascii', self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_dot(self):
|
||||
ret = main(['pipeline', 'show', '--dot', self.dotFile, self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.dotFile))
|
||||
|
||||
def test_ascii_commands(self):
|
||||
ret = main(['pipeline', 'show', '--ascii', self.file1_stage, '--commands'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_ascii_outs(self):
|
||||
ret = main(['pipeline', 'show', '--ascii', self.file1_stage, '--outs'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
def test_dot_commands(self):
|
||||
ret = main(['pipeline', 'show', '--dot', self.dotFile, self.file1_stage, '--commands'])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.dotFile))
|
||||
|
||||
def test_dot_outs(self):
|
||||
ret = main(['pipeline', 'show', '--dot', self.dotFile, self.file1_stage, '--outs'])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(self.dotFile))
|
||||
|
||||
def test_not_dvc_file(self):
|
||||
ret = main(['pipeline', 'show', self.file1])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
def test_non_existing(self):
|
||||
ret = main(['pipeline', 'show', 'non-existing'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
|
||||
class TestPipelineListEmpty(TestDvc):
|
||||
def test(self):
|
||||
ret = main(['pipeline', 'list'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
|
||||
class TestPipelineListSingle(TestPipelineShowDeep):
|
||||
def test(self):
|
||||
ret = main(['pipeline', 'list'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import os
|
||||
import configobj
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.project import Project
|
||||
from dvc.command.remote import CmdRemoteAdd
|
||||
from dvc.exceptions import UnsupportedRemoteError
|
||||
from dvc.remote import Remote
|
||||
@@ -41,3 +45,43 @@ class TestRemote(TestDvc):
|
||||
def test_unsupported(self):
|
||||
with self.assertRaises(UnsupportedRemoteError):
|
||||
Remote(self.dvc, {Config.SECTION_REMOTE_URL: 'unsupported://url'})
|
||||
|
||||
|
||||
class TestRemoteRemoveDefault(TestDvc):
|
||||
def test(self):
|
||||
remote = 'mys3'
|
||||
self.assertEqual(main(['remote', 'add', '--default', remote, 's3://bucket/name']), 0)
|
||||
self.assertEqual(main(['remote', 'modify', remote, 'profile', 'default']), 0)
|
||||
self.assertEqual(main(['config', '--local', 'core.remote', remote]), 0)
|
||||
|
||||
config = configobj.ConfigObj(os.path.join(self.dvc.dvc_dir, Config.CONFIG))
|
||||
local_config = configobj.ConfigObj(os.path.join(self.dvc.dvc_dir, Config.CONFIG_LOCAL))
|
||||
self.assertEqual(config[Config.SECTION_CORE][Config.SECTION_CORE_REMOTE], remote)
|
||||
self.assertEqual(local_config[Config.SECTION_CORE][Config.SECTION_CORE_REMOTE], remote)
|
||||
|
||||
self.assertEqual(main(['remote', 'remove', remote]), 0)
|
||||
config = configobj.ConfigObj(os.path.join(self.dvc.dvc_dir, Config.CONFIG))
|
||||
local_config = configobj.ConfigObj(os.path.join(self.dvc.dvc_dir, Config.CONFIG_LOCAL))
|
||||
section = Config.SECTION_REMOTE_FMT.format(remote)
|
||||
self.assertTrue(section not in config.keys())
|
||||
|
||||
core = config.get(Config.SECTION_CORE, None)
|
||||
if core is not None:
|
||||
self.assertTrue(Config.SECTION_CORE_REMOTE not in core.keys())
|
||||
|
||||
core = local_config.get(Config.SECTION_CORE, None)
|
||||
if core is not None:
|
||||
self.assertTrue(Config.SECTION_CORE_REMOTE not in core.keys())
|
||||
|
||||
|
||||
class TestRemoteRemove(TestDvc):
|
||||
def test(self):
|
||||
ret = main(['config', 'core.jobs', '1'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
remote = 'mys3'
|
||||
ret = main(['remote', 'add', remote, 's3://bucket/name'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(['remote', 'remove', remote])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
+36
-6
@@ -1,16 +1,20 @@
|
||||
import os
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.stage import Stage
|
||||
from dvc.project import NotDvcFileError
|
||||
from dvc.stage import Stage, StageFileDoesNotExistError
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.command.remove import CmdRemove
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
from mock import patch
|
||||
|
||||
|
||||
class TestRemove(TestDvc):
|
||||
def test(self):
|
||||
stage = self.dvc.add(self.FOO)
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
stage_removed = self.dvc.remove(stage.path, outs_only=True)
|
||||
|
||||
self.assertIsInstance(stage_removed, Stage)
|
||||
@@ -26,13 +30,16 @@ class TestRemove(TestDvc):
|
||||
|
||||
class TestRemoveNonExistentFile(TestDvc):
|
||||
def test(self):
|
||||
with self.assertRaises(NotDvcFileError) as cx:
|
||||
with self.assertRaises(StageFileDoesNotExistError) as cx:
|
||||
self.dvc.remove('non_existent_dvc_file')
|
||||
|
||||
|
||||
class TestRemoveDirectory(TestDvc):
|
||||
def test(self):
|
||||
stage_add = self.dvc.add(self.DATA_DIR)
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage_add = stages[0]
|
||||
self.assertTrue(stage_add is not None)
|
||||
stage_removed = self.dvc.remove(stage_add.path)
|
||||
self.assertEqual(stage_add.path, stage_removed.path)
|
||||
self.assertFalse(os.path.exists(self.DATA_DIR))
|
||||
@@ -41,7 +48,10 @@ class TestRemoveDirectory(TestDvc):
|
||||
|
||||
class TestCmdRemove(TestDvc):
|
||||
def test(self):
|
||||
stage = self.dvc.add(self.FOO)
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
stage = stages[0]
|
||||
self.assertTrue(stage is not None)
|
||||
ret = main(['remove',
|
||||
stage.path])
|
||||
self.assertEqual(ret, 0)
|
||||
@@ -49,3 +59,23 @@ class TestCmdRemove(TestDvc):
|
||||
ret = main(['remove',
|
||||
'non-existing-dvc-file'])
|
||||
self.assertNotEqual(ret, 0)
|
||||
|
||||
class TestRemovePurge(TestDvc):
|
||||
def test(self):
|
||||
dvcfile = self.dvc.add(self.FOO)[0].path
|
||||
ret = main(['remove', '--purge', '--force', dvcfile])
|
||||
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertFalse(os.path.exists(self.FOO))
|
||||
self.assertFalse(os.path.exists(dvcfile))
|
||||
|
||||
@patch('dvc.prompt.Prompt.prompt')
|
||||
def test_force(self, mock_prompt):
|
||||
mock_prompt.return_value = False
|
||||
|
||||
dvcfile = self.dvc.add(self.FOO)[0].path
|
||||
ret = main(['remove', '--purge', dvcfile])
|
||||
|
||||
mock_prompt.assert_called()
|
||||
self.assertEqual(ret, 1)
|
||||
self.assertRaises(DvcException)
|
||||
|
||||
+213
-32
@@ -4,7 +4,6 @@ import stat
|
||||
import shutil
|
||||
import filecmp
|
||||
import getpass
|
||||
import tempfile
|
||||
import posixpath
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
@@ -14,10 +13,11 @@ from google.cloud import storage as gc
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.command.repro import CmdRepro
|
||||
from dvc.project import Project, ReproductionError, NotDvcFileError
|
||||
from dvc.project import Project, ReproductionError
|
||||
from dvc.utils import file_md5
|
||||
from dvc.remote.local import RemoteLOCAL
|
||||
from dvc.stage import Stage
|
||||
from dvc.stage import Stage, StageFileDoesNotExistError
|
||||
from dvc.system import System
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
from tests.test_data_cloud import _should_test_aws, TEST_AWS_REPO_BUCKET
|
||||
@@ -30,7 +30,10 @@ class TestRepro(TestDvc):
|
||||
def setUp(self):
|
||||
super(TestRepro, self).setUp()
|
||||
|
||||
self.foo_stage = self.dvc.add(self.FOO)
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.foo_stage = stages[0]
|
||||
self.assertTrue(self.foo_stage is not None)
|
||||
|
||||
self.file1 = 'file1'
|
||||
self.file1_stage = self.file1 + '.dvc'
|
||||
@@ -50,7 +53,10 @@ class TestReproFail(TestRepro):
|
||||
|
||||
class TestReproDepUnderDir(TestDvc):
|
||||
def test(self):
|
||||
self.dir_stage = self.dvc.add(self.DATA_DIR)
|
||||
stages = self.dvc.add(self.DATA_DIR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.dir_stage = stages[0]
|
||||
self.assertTrue(self.dir_stage is not None)
|
||||
|
||||
sleep()
|
||||
|
||||
@@ -61,7 +67,7 @@ class TestReproDepUnderDir(TestDvc):
|
||||
deps=[self.DATA, self.CODE],
|
||||
cmd='python {} {} {}'.format(self.CODE, self.DATA, self.file1))
|
||||
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.DATA))
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.DATA, shallow=False))
|
||||
|
||||
os.unlink(self.DATA)
|
||||
shutil.copyfile(self.FOO, self.DATA)
|
||||
@@ -70,7 +76,7 @@ class TestReproDepUnderDir(TestDvc):
|
||||
|
||||
stages = self.dvc.reproduce(self.file1_stage)
|
||||
self.assertEqual(len(stages), 2)
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.FOO))
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.FOO, shallow=False))
|
||||
|
||||
|
||||
class TestReproNoDeps(TestRepro):
|
||||
@@ -101,7 +107,7 @@ class TestReproChangedCode(TestRepro):
|
||||
|
||||
stages = self.dvc.reproduce(self.file1_stage)
|
||||
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR))
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR, shallow=False))
|
||||
self.assertEqual(len(stages), 1)
|
||||
|
||||
def swap_code(self):
|
||||
@@ -117,7 +123,7 @@ class TestReproChangedData(TestRepro):
|
||||
|
||||
stages = self.dvc.reproduce(self.file1_stage)
|
||||
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR))
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR, shallow=False))
|
||||
self.assertEqual(len(stages), 2)
|
||||
|
||||
def swap_foo_with_bar(self):
|
||||
@@ -125,24 +131,140 @@ class TestReproChangedData(TestRepro):
|
||||
shutil.copyfile(self.BAR, self.FOO)
|
||||
|
||||
|
||||
class TestReproChangedDeepData(TestReproChangedData):
|
||||
class TestReproDry(TestReproChangedData):
|
||||
def test(self):
|
||||
file2 = 'file2'
|
||||
file2_stage = file2 + '.dvc'
|
||||
self.dvc.run(fname=file2_stage,
|
||||
outs=[file2],
|
||||
deps=[self.file1, self.CODE],
|
||||
cmd='python {} {} {}'.format(self.CODE, self.file1, file2))
|
||||
|
||||
self.swap_foo_with_bar()
|
||||
|
||||
stages = self.dvc.reproduce(file2_stage)
|
||||
stages = self.dvc.reproduce(self.file1_stage, dry=True)
|
||||
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR))
|
||||
self.assertTrue(filecmp.cmp(file2, self.BAR))
|
||||
self.assertTrue(len(stages), 2)
|
||||
self.assertFalse(filecmp.cmp(self.file1, self.BAR, shallow=False))
|
||||
|
||||
ret = main(['repro', '--dry', self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertFalse(filecmp.cmp(self.file1, self.BAR, shallow=False))
|
||||
|
||||
|
||||
class TestReproUpToDate(TestRepro):
|
||||
def test(self):
|
||||
ret = main(['repro', self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
|
||||
class TestReproDryNoExec(TestDvc):
|
||||
def test(self):
|
||||
deps = []
|
||||
for d in range(3):
|
||||
idir = 'idir{}'.format(d)
|
||||
odir = 'odir{}'.format(d)
|
||||
|
||||
deps.append('-d')
|
||||
deps.append(odir)
|
||||
|
||||
os.mkdir(idir)
|
||||
|
||||
f = os.path.join(idir, 'file')
|
||||
with open(f, 'w+') as fobj:
|
||||
fobj.write(str(d))
|
||||
|
||||
if os.name == 'nt':
|
||||
cp = 'copy'
|
||||
else:
|
||||
cp = 'cp'
|
||||
|
||||
ret = main(['run',
|
||||
'--no-exec',
|
||||
'-d', idir,
|
||||
'-o', odir,
|
||||
"python -c 'import shutil; "
|
||||
"shutil.copytree(\"{}\", \"{}\")'".format(idir, odir)])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(['run', '--no-exec', '-f', 'Dvcfile'] + deps)
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(['repro', '--dry'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
|
||||
class TestReproChangedDeepData(TestReproChangedData):
|
||||
def setUp(self):
|
||||
super(TestReproChangedDeepData, self).setUp()
|
||||
|
||||
self.file2 = 'file2'
|
||||
self.file2_stage = self.file2 + '.dvc'
|
||||
self.dvc.run(fname=self.file2_stage,
|
||||
outs=[self.file2],
|
||||
deps=[self.file1, self.CODE],
|
||||
cmd='python {} {} {}'.format(self.CODE,
|
||||
self.file1,
|
||||
self.file2))
|
||||
|
||||
def test(self):
|
||||
self.swap_foo_with_bar()
|
||||
|
||||
stages = self.dvc.reproduce(self.file2_stage)
|
||||
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR, shallow=False))
|
||||
self.assertTrue(filecmp.cmp(self.file2, self.BAR, shallow=False))
|
||||
self.assertEqual(len(stages), 3)
|
||||
|
||||
|
||||
class TestReproPipeline(TestReproChangedDeepData):
|
||||
def test(self):
|
||||
stages = self.dvc.reproduce(self.file1_stage,
|
||||
force=True,
|
||||
pipeline=True)
|
||||
self.assertEqual(len(stages), 3)
|
||||
|
||||
def test_cli(self):
|
||||
ret = main(['repro', '--pipeline', '-f', self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
|
||||
class TestReproPipelines(TestDvc):
|
||||
def setUp(self):
|
||||
super(TestReproPipelines, self).setUp()
|
||||
|
||||
stages = self.dvc.add(self.FOO)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.foo_stage = stages[0]
|
||||
self.assertTrue(self.foo_stage is not None)
|
||||
|
||||
stages = self.dvc.add(self.BAR)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.bar_stage = stages[0]
|
||||
self.assertTrue(self.bar_stage is not None)
|
||||
|
||||
self.file1 = 'file1'
|
||||
self.file1_stage = self.file1 + '.dvc'
|
||||
self.dvc.run(fname=self.file1_stage,
|
||||
outs=[self.file1],
|
||||
deps=[self.FOO, self.CODE],
|
||||
cmd='python {} {} {}'.format(self.CODE, self.FOO, self.file1))
|
||||
|
||||
self.file2 = 'file2'
|
||||
self.file2_stage = self.file2 + '.dvc'
|
||||
self.dvc.run(fname=self.file2_stage,
|
||||
outs=[self.file2],
|
||||
deps=[self.BAR, self.CODE],
|
||||
cmd='python {} {} {}'.format(self.CODE, self.BAR, self.file2))
|
||||
|
||||
|
||||
def test(self):
|
||||
stages = self.dvc.reproduce(all_pipelines=True, force=True)
|
||||
self.assertEqual(len(stages), 4)
|
||||
names = [stage.relpath for stage in stages]
|
||||
self.assertTrue(self.foo_stage.relpath in names)
|
||||
self.assertTrue(self.bar_stage.relpath in names)
|
||||
self.assertTrue(self.file1_stage in names)
|
||||
self.assertTrue(self.file2_stage in names)
|
||||
|
||||
def test_cli(self):
|
||||
ret = main(['repro', '-f', '-P'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
|
||||
class TestReproLocked(TestReproChangedData):
|
||||
def test(self):
|
||||
file2 = 'file2'
|
||||
@@ -162,12 +284,12 @@ class TestReproLocked(TestReproChangedData):
|
||||
ret = main(['unlock', file2_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
stages = self.dvc.reproduce(file2_stage)
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR))
|
||||
self.assertTrue(filecmp.cmp(file2, self.BAR))
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR, shallow=False))
|
||||
self.assertTrue(filecmp.cmp(file2, self.BAR, shallow=False))
|
||||
self.assertEqual(len(stages), 3)
|
||||
|
||||
def test_non_existing(self):
|
||||
with self.assertRaises(NotDvcFileError):
|
||||
with self.assertRaises(StageFileDoesNotExistError):
|
||||
self.dvc.lock_stage('non-existing-stage')
|
||||
|
||||
ret = main(['lock', 'non-existing-stage'])
|
||||
@@ -182,7 +304,7 @@ class TestReproPhony(TestReproChangedData):
|
||||
|
||||
self.dvc.reproduce(stage.path)
|
||||
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR))
|
||||
self.assertTrue(filecmp.cmp(self.file1, self.BAR, shallow=False))
|
||||
|
||||
|
||||
class TestNonExistingOutput(TestRepro):
|
||||
@@ -199,7 +321,7 @@ class TestReproDataSource(TestReproChangedData):
|
||||
|
||||
stages = self.dvc.reproduce(self.foo_stage.path)
|
||||
|
||||
self.assertTrue(filecmp.cmp(self.FOO, self.BAR))
|
||||
self.assertTrue(filecmp.cmp(self.FOO, self.BAR, shallow=False))
|
||||
self.assertEqual(stages[0].outs[0].md5, file_md5(self.BAR)[0])
|
||||
|
||||
|
||||
@@ -230,6 +352,55 @@ class TestReproChangedDir(TestDvc):
|
||||
self.assertEqual(len(stages), 1)
|
||||
|
||||
|
||||
class TestReproChangedDirData(TestDvc):
|
||||
def test(self):
|
||||
dir_name = 'dir'
|
||||
dir_code = 'dir_code.py'
|
||||
|
||||
sleep()
|
||||
|
||||
with open(dir_code, 'w+') as fd:
|
||||
fd.write("import os; import sys; import shutil; shutil.copytree(sys.argv[1], sys.argv[2])")
|
||||
|
||||
sleep()
|
||||
|
||||
stage = self.dvc.run(outs=[dir_name],
|
||||
deps=[self.DATA_DIR, dir_code],
|
||||
cmd="python {} {} {}".format(dir_code,
|
||||
self.DATA_DIR,
|
||||
dir_name))
|
||||
self.assertTrue(stage is not None)
|
||||
|
||||
stages = self.dvc.reproduce(stage.path)
|
||||
self.assertEqual(len(stages), 0)
|
||||
|
||||
sleep()
|
||||
|
||||
with open(self.DATA_SUB, 'a') as fd:
|
||||
fd.write('add')
|
||||
|
||||
sleep()
|
||||
|
||||
stages = self.dvc.reproduce(stage.path)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
sleep()
|
||||
|
||||
# Check that dvc indeed registers changed output dir
|
||||
shutil.move(self.BAR, dir_name)
|
||||
sleep()
|
||||
stages = self.dvc.reproduce(stage.path)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
|
||||
# Check that dvc registers mtime change for the directory.
|
||||
System.hardlink(self.DATA_SUB, self.DATA_SUB + '.lnk')
|
||||
sleep()
|
||||
stages = self.dvc.reproduce(stage.path)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(stages[0] is not None)
|
||||
|
||||
|
||||
class TestReproMissingMd5InStageFile(TestRepro):
|
||||
def test(self):
|
||||
with open(self.file1_stage, 'r') as fd:
|
||||
@@ -245,8 +416,10 @@ class TestReproMissingMd5InStageFile(TestRepro):
|
||||
self.assertEqual(len(stages), 1)
|
||||
|
||||
|
||||
class TestCmdRepro(TestRepro):
|
||||
class TestCmdRepro(TestReproChangedData):
|
||||
def test(self):
|
||||
self.swap_foo_with_bar()
|
||||
|
||||
ret = main(['status'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
@@ -278,7 +451,7 @@ class TestCmdReproChdir(TestDvc):
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(foo))
|
||||
self.assertTrue(os.path.isfile(bar))
|
||||
self.assertTrue(filecmp.cmp(foo, bar))
|
||||
self.assertTrue(filecmp.cmp(foo, bar, shallow=False))
|
||||
|
||||
os.unlink(bar)
|
||||
|
||||
@@ -287,7 +460,7 @@ class TestCmdReproChdir(TestDvc):
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile(foo))
|
||||
self.assertTrue(os.path.isfile(bar))
|
||||
self.assertTrue(filecmp.cmp(foo, bar))
|
||||
self.assertTrue(filecmp.cmp(foo, bar, shallow=False))
|
||||
|
||||
|
||||
class TestReproExternalBase(TestDvc):
|
||||
@@ -346,7 +519,7 @@ class TestReproExternalBase(TestDvc):
|
||||
|
||||
import_stage = self.dvc.imp(out_foo_path, 'import')
|
||||
self.assertTrue(os.path.exists('import'))
|
||||
self.assertTrue(filecmp.cmp('import', self.FOO))
|
||||
self.assertTrue(filecmp.cmp('import', self.FOO, shallow=False))
|
||||
|
||||
import_remote_stage = self.dvc.imp(out_foo_path, out_foo_path + '_imported')
|
||||
|
||||
@@ -358,10 +531,12 @@ class TestReproExternalBase(TestDvc):
|
||||
|
||||
sleep()
|
||||
|
||||
self.dvc.status()
|
||||
|
||||
stages = self.dvc.reproduce(import_stage.path)
|
||||
self.assertEqual(len(stages), 1)
|
||||
self.assertTrue(os.path.exists('import'))
|
||||
self.assertTrue(filecmp.cmp('import', self.BAR))
|
||||
self.assertTrue(filecmp.cmp('import', self.BAR, shallow=False))
|
||||
|
||||
stages = self.dvc.reproduce(cmd_stage.path)
|
||||
self.assertEqual(len(stages), 1)
|
||||
@@ -479,7 +654,7 @@ class TestReproExternalSSH(TestReproExternalBase):
|
||||
@property
|
||||
def bucket(self):
|
||||
if not self._dir:
|
||||
self._dir = tempfile.mkdtemp()
|
||||
self._dir = TestDvc.mkdtemp()
|
||||
return '{}@127.0.0.1:{}'.format(getpass.getuser(), self._dir)
|
||||
|
||||
def cmd(self, i, o):
|
||||
@@ -526,7 +701,7 @@ class TestReproExternalSSH(TestReproExternalBase):
|
||||
class TestReproExternalLOCAL(TestReproExternalBase):
|
||||
def setUp(self):
|
||||
super(TestReproExternalLOCAL, self).setUp()
|
||||
self.tmpdir = tempfile.mkdtemp()
|
||||
self.tmpdir = TestDvc.mkdtemp()
|
||||
|
||||
def should_test(self):
|
||||
return True
|
||||
@@ -588,3 +763,9 @@ class TestReproShell(TestDvc):
|
||||
|
||||
with open(fname, 'r') as fd:
|
||||
self.assertEqual(os.getenv('SHELL'), fd.read().strip())
|
||||
|
||||
class TestReproNoSCM(TestRepro):
|
||||
def test(self):
|
||||
shutil.rmtree(self.dvc.scm.dir)
|
||||
ret = main(['repro', self.file1_stage])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
+75
-4
@@ -3,8 +3,10 @@ import filecmp
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.utils import file_md5
|
||||
from dvc.stage import Stage
|
||||
from dvc.stage import Stage, StageFileBadNameError, MissingDep
|
||||
from dvc.stage import StageBadCwdError
|
||||
from dvc.command.run import CmdRun
|
||||
from dvc.exceptions import OutputDuplicationError, CircularDependencyError
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
@@ -15,7 +17,7 @@ class TestRun(TestDvc):
|
||||
deps = [self.FOO, self.CODE]
|
||||
outs = [os.path.join(self.dvc.root_dir, 'out')]
|
||||
outs_no_cache = []
|
||||
fname = os.path.join(self.dvc.root_dir, 'out.dvc')
|
||||
fname = 'out.dvc'
|
||||
cwd = os.curdir
|
||||
|
||||
self.dvc.add(self.FOO)
|
||||
@@ -26,7 +28,7 @@ class TestRun(TestDvc):
|
||||
fname=fname,
|
||||
cwd=cwd)
|
||||
|
||||
self.assertTrue(filecmp.cmp(self.FOO, 'out'))
|
||||
self.assertTrue(filecmp.cmp(self.FOO, 'out', shallow=False))
|
||||
self.assertTrue(os.path.isfile(stage.path))
|
||||
self.assertEqual(stage.cmd, cmd)
|
||||
self.assertEqual(len(stage.deps), len(deps))
|
||||
@@ -35,6 +37,14 @@ class TestRun(TestDvc):
|
||||
self.assertEqual(stage.outs[0].md5, file_md5(self.FOO)[0])
|
||||
self.assertTrue(stage.path, fname)
|
||||
|
||||
with self.assertRaises(OutputDuplicationError):
|
||||
stage = self.dvc.run(cmd=cmd,
|
||||
deps=deps,
|
||||
outs=outs,
|
||||
outs_no_cache=outs_no_cache,
|
||||
fname='duplicate' + fname,
|
||||
cwd=cwd)
|
||||
|
||||
|
||||
class TestRunEmpty(TestDvc):
|
||||
def test(self):
|
||||
@@ -46,6 +56,37 @@ class TestRunEmpty(TestDvc):
|
||||
cwd=os.curdir)
|
||||
|
||||
|
||||
class TestRunMissingDep(TestDvc):
|
||||
def test(self):
|
||||
with self.assertRaises(MissingDep):
|
||||
self.dvc.run(cmd='',
|
||||
deps=['non-existing-dep'],
|
||||
outs=[],
|
||||
outs_no_cache=[],
|
||||
fname='empty.dvc',
|
||||
cwd=os.curdir)
|
||||
|
||||
|
||||
class TestRunBadStageFilename(TestDvc):
|
||||
def test(self):
|
||||
with self.assertRaises(StageFileBadNameError):
|
||||
self.dvc.run(cmd='',
|
||||
deps=[],
|
||||
outs=[],
|
||||
outs_no_cache=[],
|
||||
fname='empty',
|
||||
cwd=os.curdir)
|
||||
|
||||
with self.assertRaises(StageFileBadNameError):
|
||||
self.dvc.run(cmd='',
|
||||
deps=[],
|
||||
outs=[],
|
||||
outs_no_cache=[],
|
||||
fname=os.path.join(self.DATA_DIR, 'empty.dvc'),
|
||||
cwd=os.curdir)
|
||||
|
||||
|
||||
|
||||
class TestRunNoExec(TestDvc):
|
||||
def test(self):
|
||||
self.dvc.run(cmd='python {} {} {}'.format(self.CODE, self.FOO, 'out'),
|
||||
@@ -53,6 +94,36 @@ class TestRunNoExec(TestDvc):
|
||||
self.assertFalse(os.path.exists('out'))
|
||||
|
||||
|
||||
class TestRunCircularDependency(TestDvc):
|
||||
def test(self):
|
||||
with self.assertRaises(CircularDependencyError):
|
||||
self.dvc.run(cmd='',
|
||||
deps=[self.FOO],
|
||||
outs=[self.FOO],
|
||||
fname='circular-dependency.dvc')
|
||||
|
||||
def test_outs_no_cache(self):
|
||||
with self.assertRaises(CircularDependencyError):
|
||||
self.dvc.run(cmd='',
|
||||
deps=[self.FOO],
|
||||
outs_no_cache=[self.FOO],
|
||||
fname='circular-dependency.dvc')
|
||||
|
||||
def test_non_normalized_paths(self):
|
||||
with self.assertRaises(CircularDependencyError):
|
||||
self.dvc.run(cmd='',
|
||||
deps=['./foo'],
|
||||
outs=['foo'],
|
||||
fname='circular-dependency.dvc')
|
||||
|
||||
|
||||
class TestRunBadCwd(TestDvc):
|
||||
def test(self):
|
||||
with self.assertRaises(StageBadCwdError):
|
||||
self.dvc.run(cmd='',
|
||||
cwd=self.mkdtemp())
|
||||
|
||||
|
||||
class TestCmdRun(TestDvc):
|
||||
def test_run(self):
|
||||
ret = main(['run',
|
||||
@@ -64,7 +135,7 @@ class TestCmdRun(TestDvc):
|
||||
self.assertEqual(ret, 0)
|
||||
self.assertTrue(os.path.isfile('out'))
|
||||
self.assertTrue(os.path.isfile('out.dvc'))
|
||||
self.assertTrue(filecmp.cmp(self.FOO, 'out'))
|
||||
self.assertTrue(filecmp.cmp(self.FOO, 'out', shallow=False))
|
||||
|
||||
def test_run_bad_command(self):
|
||||
ret = main(['run',
|
||||
|
||||
+21
-5
@@ -4,7 +4,7 @@ from git import Repo
|
||||
|
||||
from dvc.scm import SCM, Base, Git
|
||||
|
||||
from tests.basic_env import TestDir, TestGit
|
||||
from tests.basic_env import TestDir, TestGit, TestGitSubmodule
|
||||
|
||||
|
||||
class TestSCM(TestDir):
|
||||
@@ -27,21 +27,37 @@ class TestSCMGit(TestGit):
|
||||
self.assertTrue('foo' in self.git.git.ls_files())
|
||||
|
||||
|
||||
class TestSCMGitSubmodule(TestGitSubmodule):
|
||||
def test_git_submodule(self):
|
||||
self.assertIsInstance(SCM(os.curdir), Git)
|
||||
|
||||
def test_is_submodule(self):
|
||||
self.assertTrue(Git.is_submodule(os.curdir))
|
||||
|
||||
def test_commit_in_submodule(self):
|
||||
G = Git(self._root_dir)
|
||||
G.add(['foo'])
|
||||
G.commit('add')
|
||||
self.assertTrue('foo' in self.git.git.ls_files())
|
||||
|
||||
|
||||
class TestIgnore(TestGit):
|
||||
def _count_gitignore(self):
|
||||
with open(Git.GITIGNORE, 'r') as fd:
|
||||
lines = fd.readlines()
|
||||
return len(list(filter(lambda x: x.strip() == 'foo', lines)))
|
||||
return len(list(filter(lambda x: x.strip() == '/' + self.FOO,
|
||||
lines)))
|
||||
|
||||
def test(self):
|
||||
git = Git(self._root_dir)
|
||||
foo = os.path.join(self._root_dir, self.FOO)
|
||||
|
||||
git.ignore('foo')
|
||||
git.ignore(foo)
|
||||
self.assertTrue(os.path.isfile(Git.GITIGNORE))
|
||||
self.assertEqual(self._count_gitignore(), 1)
|
||||
|
||||
git.ignore('foo')
|
||||
git.ignore(foo)
|
||||
self.assertEqual(self._count_gitignore(), 1)
|
||||
|
||||
git.ignore_remove('foo')
|
||||
git.ignore_remove(foo)
|
||||
self.assertEqual(self._count_gitignore(), 0)
|
||||
|
||||
+36
-23
@@ -3,8 +3,9 @@ import stat
|
||||
import time
|
||||
|
||||
from dvc.system import System
|
||||
from dvc.state import State, StateEntry
|
||||
from dvc.state import State
|
||||
from dvc.utils import file_md5
|
||||
from dvc.main import main
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
@@ -16,31 +17,43 @@ class TestState(TestDvc):
|
||||
mtime = os.path.getmtime(path)
|
||||
inode = System.inode(path)
|
||||
|
||||
state = State(self.dvc)
|
||||
state = State(self.dvc, self.dvc.config._config)
|
||||
|
||||
entry_md5 = state.update(path)
|
||||
self.assertEqual(entry_md5, md5)
|
||||
with state:
|
||||
entry_md5 = state.update(path)
|
||||
self.assertEqual(entry_md5, md5)
|
||||
|
||||
# Sleep some time to simulate realistic behavior.
|
||||
# Some filesystems have a bad date resolution for
|
||||
# mtime(i.e. 1sec for HFS) that cause problems with
|
||||
# our 'state' system not being able to distinguish
|
||||
# files that were modified within that delta.
|
||||
time.sleep(1)
|
||||
# Sleep some time to simulate realistic behavior.
|
||||
# Some filesystems have a bad date resolution for
|
||||
# mtime(i.e. 1sec for HFS) that cause problems with
|
||||
# our 'state' system not being able to distinguish
|
||||
# files that were modified within that delta.
|
||||
time.sleep(1)
|
||||
|
||||
os.unlink(path)
|
||||
with open(path, 'w+') as fd:
|
||||
fd.write('1')
|
||||
os.unlink(path)
|
||||
with open(path, 'w+') as fd:
|
||||
fd.write('1')
|
||||
|
||||
md5 = file_md5(path)[0]
|
||||
mtime = os.path.getmtime(path)
|
||||
inode = System.inode(path)
|
||||
md5 = file_md5(path)[0]
|
||||
mtime = os.path.getmtime(path)
|
||||
inode = System.inode(path)
|
||||
|
||||
entry_md5 = state.update(path)
|
||||
self.assertEqual(entry_md5, md5)
|
||||
entry_md5 = state.update(path)
|
||||
self.assertEqual(entry_md5, md5)
|
||||
|
||||
# Reload state db to make sure that it stays the same
|
||||
old_db = state._db
|
||||
state = State(self.dvc)
|
||||
new_db = state._db
|
||||
self.assertEqual(old_db, new_db)
|
||||
|
||||
class TestStateOverflow(TestDvc):
|
||||
def test(self):
|
||||
# NOTE: trying to add more entries than state can handle,
|
||||
# to see if it will clean up and vacuum successfully
|
||||
ret = main(['config', 'state.row_limit', '10'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
dname = 'dir'
|
||||
os.mkdir(dname)
|
||||
for i in range(20):
|
||||
with open(os.path.join(dname, str(i)), 'w+') as fobj:
|
||||
fobj.write(str(i))
|
||||
|
||||
ret = main(['add', 'dir'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
import stat
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.project import Project
|
||||
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
|
||||
class TestUnprotect(TestDvc):
|
||||
def test(self):
|
||||
ret = main(['config', 'cache.protected', 'true'])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
ret = main(['add', self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertNotEqual(os.stat(self.FOO).st_mode & (stat.S_IREAD|
|
||||
stat.S_IRGRP|
|
||||
stat.S_IROTH), 0)
|
||||
self.assertEqual(os.stat(self.FOO).st_mode & (stat.S_IWRITE|
|
||||
stat.S_IWGRP|
|
||||
stat.S_IWOTH), 0)
|
||||
|
||||
ret = main(['unprotect', self.FOO])
|
||||
self.assertEqual(ret, 0)
|
||||
|
||||
self.assertNotEqual(os.stat(self.FOO).st_mode & (stat.S_IREAD|
|
||||
stat.S_IRGRP|
|
||||
stat.S_IROTH|
|
||||
stat.S_IWRITE), 0)
|
||||
+2
-2
@@ -18,10 +18,10 @@ class TestUtils(TestDvc):
|
||||
os.mkdir(dest_dir)
|
||||
|
||||
utils.copyfile(src, dest)
|
||||
self.assertTrue(filecmp.cmp(src, dest))
|
||||
self.assertTrue(filecmp.cmp(src, dest, shallow=False))
|
||||
|
||||
utils.copyfile(src, dest_dir)
|
||||
self.assertTrue(filecmp.cmp(src, '{}/{}'.format(dest_dir, src)))
|
||||
self.assertTrue(filecmp.cmp(src, '{}/{}'.format(dest_dir, src), shallow=False))
|
||||
|
||||
shutil.rmtree(dest_dir)
|
||||
os.remove(src)
|
||||
|
||||
Reference in New Issue
Block a user