Compare commits

...
486 Commits
Author SHA1 Message Date
Ruslan Kuprieiev c62b1a1c88 test: debug checkout test 2018-11-15 13:55:48 +02:00
Ruslan Kuprieiev 055f71a968 state: increase row limit to 100M 2018-11-15 12:16:28 +02:00
Ruslan Kuprieiev ebed534c7a Merge pull request #1327 from mroutis/fix-1326
checkout: Clean working directory changes of tracked dir
2018-11-14 19:28:27 +02:00
Mr. Outis b4588671a0 Add a test for it 2018-11-14 11:03:53 -06:00
Mr. Outis 423d620b89 checkout: Clean working directory changes of tracked dir
Fix #1326
2018-11-14 10:53:02 -06:00
Ruslan Kuprieiev 65dd8cafec dvc: bump to 0.20.4 2018-11-14 17:22:41 +02:00
Ruslan Kuprieiev 8106d6bf09 test: debug test_checkout 2018-11-14 15:03:40 +02:00
Ruslan Kuprieiev 9c33deab34 Merge pull request #1325 from efiop/master
gitignore: use unambiguous paths
2018-11-14 14:54:08 +02:00
Ruslan Kuprieiev 6de91c0b28 gitignore: use unambiguous paths
Fixes #1324

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-11-14 14:43:00 +02:00
Mr. Outis 88ed542b94 gitignore: Add temporary SQLite files (#1322)
Fix #1319
2018-11-13 19:29:21 -06:00
Mr. Outis 55eadba0ec cache: Cosmetic changes 💅 (#1321) 2018-11-13 13:30:34 -06:00
Mr. Outis fc7545b6cf checkout: Remove selectively on directory files when changed (#1320)
Fix: #1220
2018-11-13 13:30:19 -06:00
Ruslan Kuprieiev 7d069d2210 dvc: bump to 0.20.3 2018-11-11 23:12:08 +02:00
Ruslan Kuprieiev fb65bb3967 state: add workaround for VACUUM bug on python 3.6 (#1315)
Fixes #1314

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-11-11 23:11:30 +02:00
Mr. Outis 4d03f69a86 remote: Show progress bar when linking large dirs (#1309)
* remote: Show progress bar when linking large dirs

Close #1222

* Fix pycodestyle

* Use Logger.debug instead of local verbose argument

* Show the progress bar also during checkout
2018-11-10 23:56:50 +02:00
Ruslan Kuprieiev 31585c8d1a Merge pull request #1313 from efiop/master
run: warn if using fish shell
2018-11-10 21:38:09 +02:00
Ruslan Kuprieiev d41498bdea run: warn if using fish shell
Fixes #1307

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-11-10 20:39:28 +02:00
Ruslan Kuprieiev 46831ea00a dvc: bump to 0.20.2 2018-11-08 14:45:44 +02:00
Ruslan Kuprieiev 20517e44b8 test: save cache backup 2018-11-08 14:26:52 +02:00
Ruslan Kuprieiev 1922d1189d Merge pull request #1306 from mroutis/fix-1041
s3: Use default boto3 session
2018-11-08 12:06:00 +02:00
Mr. Outis 4ee615e5b4 Prefer using ENV_VARS before dvc config file 2018-11-07 19:11:18 -06:00
Mr. Outis c06eca5709 s3: Use default boto3 session
Fixes #1041
2018-11-07 18:19:53 -06:00
Ruslan Kuprieiev e6e72febf9 remote: use uuid for tmp name generation 2018-11-08 00:48:41 +02:00
Ruslan Kuprieiev 763839f6d0 state: debug travis 2018-11-08 00:33:02 +02:00
Ruslan Kuprieiev 6b50757aaa state: remove whitespace 2018-11-07 23:46:41 +02:00
Ruslan Kuprieiev d2bc79bec3 state: add debug message 2018-11-07 23:41:10 +02:00
Ruslan Kuprieiev 33d8506db7 state: add debug message 2018-11-07 23:25:41 +02:00
Ruslan Kuprieiev 683f2c9a57 tests: debug travis failure 2018-11-07 23:05:58 +02:00
Ruslan Kuprieiev d09de7e658 reqs: update google-cloud-storage 2018-11-07 22:46:22 +02:00
Ruslan Kuprieiev 181ed5e55c reqs: update google-cloud-storage 2018-11-07 22:46:01 +02:00
Ruslan Kuprieiev aa8b52908e test: sleep before checkout 2018-11-07 21:23:48 +02:00
Ruslan Kuprieiev 7e1b584479 system: add a better message if symlink failed 2018-11-07 21:21:45 +02:00
Ruslan Kuprieiev 3700fbf99d Merge pull request #1303 from ynop/gc-multi-project
gc: support used cache collection for several repositories using the same remote storage
2018-11-07 21:02:33 +02:00
Mr. Outis b6842fb089 updater: Add custom update message depending on package manager (#1287)
* updater: Add custom update message depending on package manager

Close #938
Close #1031

* Reduce cognitive complexity and avoid too many return functions

* Use a dictionary instead of if/else

* Take into account pkg installation with OSX

* updater: add platform detection

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-11-07 19:04:52 +02:00
Ruslan Kuprieiev 2afe950a27 Merge pull request #1302 from efiop/master
dvc: introduce `pipeline list` command
2018-11-07 19:04:30 +02:00
buec 19eeb62e6e cli: add gc option to include additional projects 2018-11-07 18:03:41 +01:00
Ruslan Kuprieiev abfb48e896 dvc: introduce pipeline list command
Fixes #1301

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-11-07 18:39:29 +02:00
buec 111c51f44b gc: add option to collect cache for multiple projects 2018-11-07 17:36:15 +01:00
Ruslan Kuprieiev f304d3548f Merge pull request #1299 from mroutis/add-installation-methods
readme: Add OS-specific installation instructions
2018-11-06 23:00:42 +02:00
Mr. Outis eef34b567b readme: Add OS-specific installation instructions 2018-11-06 14:44:33 -06:00
Ruslan Kuprieiev 49cadb68da test: don't forget to sleep() 2018-11-06 17:39:57 +02:00
Ruslan Kuprieiev 75d6c930ca dvc: bump to 0.20.1 2018-11-06 14:33:52 +02:00
Ruslan Kuprieiev 71ac9572bb Merge pull request #1297 from efiop/master
Remote fixes
2018-11-06 14:33:17 +02:00
Ruslan Kuprieiev 7720c35544 checkout: add --with-deps support
Fixes #1296

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-11-06 14:19:38 +02:00
Ruslan Kuprieiev f00d08d46d remote: properly collect dir cache
Fixes #1293

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-11-06 14:19:38 +02:00
Ruslan Kuprieiev 7398a174a8 azure: add gc support
Fixes #1232

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-11-06 14:19:38 +02:00
Paweł Redzyński 4aab443675 Add separate stream handler for error logging (#1292)
* Add separate stream handler for error logging

* test stdout and stderr

* review refactor, newline after test

* mock instead of unittest.mock

* use filter instead of lambda

* add result checks

* remove Logger pseudo-integration test, add direct test

* tests, refactor, StringIO intercepts stdout and stderr messages

* Check exact error message

Co-Authored-By: pared <[email protected]>

* Check exact non-error message

Co-Authored-By: pared <[email protected]>
2018-11-04 12:41:43 +02:00
Mr. Outis bdf4095297 updater: Add some colors and a frame to the update message (#1273)
Close #1032
2018-10-31 17:52:50 -06:00
Mr. Outis acae1b996f scm: Add dir property to Base class (#1281)
Close #1279
2018-10-30 19:32:08 -06:00
Mr. Outis c4295fa05c run: Add abspath to circular dependency check and include outs_no_cache (#1272)
Close #1235
2018-10-29 11:44:48 -06:00
Mr. Outis 7ae97009fa cli: Move mutual exclusion of --quiet and --verbose to the parser level (#1271)
Close #1270
2018-10-28 22:14:59 -06:00
Mr. Outis 7865ddd128 remove: Add confirmation when trying to purge (#1269)
Close #1228
2018-10-28 21:14:32 -06:00
Ruslan Kuprieiev 838d597983 dvc: bump to 0.20.0 2018-10-27 10:32:35 +03:00
Ruslan Kuprieiev 0ee7d5a7f8 Merge pull request #1268 from efiop/master
dvc: introduce protected mode
2018-10-26 16:24:35 +03:00
Ruslan Kuprieiev 8966d6ce47 dvc: introduce protected mode
This is an opt-in mode, that will make data in the workspace
read-only, thus protecting cache from corruption when hardlinks or
symlinks are used. User can use `dvc unprotect file` command, to
replace read-only link to cache with an editable copy. We can use
this for now to decide if we want to go with a `unified workflow`
in the future or if we want to set unprotected `reflink,copy`
by default.

Fixes #799

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-26 16:07:11 +03:00
Ruslan Kuprieiev d27fe82053 Merge pull request #1267 from efiop/master
dvc: don't use $SHELL on windows
2018-10-26 14:31:38 +03:00
Ruslan Kuprieiev d108e9b03d dvc: don't use $SHELL on windows
Related to #1238

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-26 14:21:05 +03:00
Ruslan Kuprieiev b01245f007 cli: add a note about cwd 2018-10-24 12:53:04 +03:00
Ruslan Kuprieiev a9d21f19ab Merge pull request #1265 from efiop/master
stage: check that cwd is inside the current project
2018-10-24 12:44:42 +03:00
Ruslan Kuprieiev ed93e0d0d9 stage: check that cwd is inside the current project
Fixes #1253

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-24 12:26:28 +03:00
Ruslan Kuprieiev afcc51417c Merge pull request #1264 from mroutis/remove-deprecation-warning
logger: Remove deprecation warning reported by pytest
2018-10-24 11:09:33 +03:00
Mr. Outis 0d42ad0d1e Prevent E501 by splitting long lines 2018-10-24 00:43:42 -05:00
Mr. Outis 0ec77180d4 logger: Remove deprecation warning reported by pytest
dvc/logger.py:113: DeprecationWarning: The 'warn' method is deprecated, use 'warning' instead
2018-10-24 00:17:58 -05:00
Ruslan Kuprieiev d5470419ce Merge pull request #1261 from suzil/cleanup/fix-readme-typo
dvc: Fix typo in README.rst
2018-10-23 17:14:59 +03:00
Ruslan Kuprieiev 9306ea2cd4 Merge pull request #1262 from efiop/master
root: don't check project's lock
2018-10-23 17:10:07 +03:00
Ruslan Kuprieiev 7493804312 root: don't check project's lock
Fixes #1260

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-23 16:53:26 +03:00
Susannah Klaneček c8ca89f3dd dvc: fix typo in README.rst 2018-10-23 15:47:11 +02:00
Ruslan Kuprieiev 548dc3657b Merge pull request #1256 from mroutis/check-circular-deps
run: Check for circular dependencies
2018-10-23 14:21:43 +03:00
Mr. Outis 6d848cfe64 [fix] Check if set is empty before doing a _pop_ 2018-10-23 00:41:48 -05:00
Ruslan Kuprieiev bc711417cd dvc: update to 0.19.15 2018-10-23 03:13:54 +03:00
Ruslan Kuprieiev 5654fc269c Merge pull request #1254 from efiop/master
system: catch link exceptions
2018-10-23 02:37:58 +03:00
Mr. Outis 239313d67e run: Check for circular dependencies
Close #1235
2018-10-22 17:29:55 -05:00
Ruslan Kuprieiev b415ae9a21 system: catch link exceptions
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-22 22:44:33 +03:00
Ruslan Kuprieiev 7a004fa328 dvc: bump to 0.19.14 2018-10-22 01:38:21 +03:00
Ruslan Kuprieiev 4c5f9910bb Merge pull request #1249 from efiop/master
install: add pre-checkout hook
2018-10-22 01:37:50 +03:00
Ruslan Kuprieiev 437c2e7cbe install: add pre-checkout hook
Fixes #1248

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-22 01:18:55 +03:00
Ruslan Kuprieiev 94cf83c1cd Merge pull request #1247 from efiop/master
link: don't create links for empty files
2018-10-21 07:41:31 +03:00
Ruslan Kuprieiev 3f18200539 link: don't create links for empty files
Related to #1222

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-21 07:13:13 +03:00
Ruslan Kuprieiev 43f26fdbd3 Merge pull request #1246 from efiop/master
link: properly catch error on windows
2018-10-21 05:02:39 +03:00
Ruslan Kuprieiev 37f85720a2 appveyor: fix openssl package version
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-21 04:52:42 +03:00
Ruslan Kuprieiev 03eac5b0e5 link: properly catch error on windows
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-21 03:43:19 +03:00
Ruslan Kuprieiev 0bc96a2fcb Merge pull request #1245 from efiop/master
scm: allow all_tags and all_branches
2018-10-21 03:23:43 +03:00
Ruslan Kuprieiev 90a636e33f scm: allow all_tags and all_branches
Fixes #1243

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-21 01:47:07 +03:00
Ruslan Kuprieiev 046010a710 dvc: update to 0.19.13 2018-10-19 23:34:16 +03:00
Ruslan Kuprieiev 09a21cf854 Merge pull request #1242 from efiop/master
system: add workaround for buggy os.lstat().st_ino
2018-10-19 23:14:59 +03:00
Ruslan Kuprieiev e7c920d783 system: add workaround for buggy os.lstat().st_ino
Fixes #1241

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-19 22:56:53 +03:00
Ruslan Kuprieiev aa07b10882 Merge pull request #1240 from efiop/master
remote: use warning if upload/download failed
2018-10-19 04:02:27 +03:00
Ruslan Kuprieiev 1c36ef3f43 remote: use warning if upload/download failed
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-19 03:48:29 +03:00
Ruslan Kuprieiev 06697c786c Merge pull request #1237 from efiop/master
codeclimate: turn all the checks on
2018-10-19 02:49:51 +03:00
Ruslan Kuprieiev eb7b9d9b89 s3: don't throw exception when file doesn't exist 2018-10-19 02:43:18 +03:00
Ruslan Kuprieiev 9ed4f113f2 codeclimate: turn all the checks back on
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-18 04:56:09 +03:00
Ruslan Kuprieiev 84e748f831 Merge pull request #1236 from efiop/master
remote: properly check reqs for a legacy config
2018-10-18 03:23:55 +03:00
Ruslan Kuprieiev 78a20a3895 remote: properly check reqs for a legacy config
Fixes #1231

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-18 03:12:47 +03:00
Ruslan Kuprieiev 0f20c86eab Merge pull request #1229 from efiop/master
progress reporting fixes
2018-10-17 08:12:36 +03:00
Ruslan Kuprieiev 7adafd0b5c progress: properly report on dir with large dirs
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-17 07:50:28 +03:00
Ruslan Kuprieiev 16fef9bb87 logger: clear progress bar before printing
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-17 07:50:28 +03:00
Ruslan Kuprieiev 815c591adc dvc: bump to 0.19.12 2018-10-16 05:27:56 +03:00
Ruslan Kuprieiev 22ba202462 Merge pull request #1226 from efiop/master
state file fixes
2018-10-16 05:27:26 +03:00
Ruslan Kuprieiev 53652399b3 system: ensure inode value is correct
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-16 05:12:16 +03:00
Ruslan Kuprieiev f57198e6af state: store uint64 as int64
Fixes #1225

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-16 05:12:16 +03:00
Ruslan Kuprieiev a7b34ddecd state: add debug msgs
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-16 01:40:22 +03:00
Ruslan Kuprieiev c195b18490 Merge pull request #1203 from mroutis/shell-completion
Add DVC shell completion for Bash and Zsh
2018-10-15 19:17:16 +03:00
Ruslan Kuprieiev feae736e40 cli: fix line length 2018-10-15 03:39:08 +03:00
Ruslan Kuprieiev 54b1ecbf4a metrics: fix typos 2018-10-15 03:32:39 +03:00
Ruslan Kuprieiev cf44027604 test: checkout: sleep 2018-10-14 05:03:47 +03:00
Ruslan Kuprieiev 0e77b9a2fb Merge pull request #1223 from efiop/master
Misc fixes
2018-10-14 03:13:54 +03:00
Ruslan Kuprieiev 6194b7c209 state: don't check version if db is empty
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-14 03:03:41 +03:00
Ruslan Kuprieiev 4503a7f503 dvc: check if cache file inside dir was changed
Fixes #1125

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-14 03:03:41 +03:00
Ruslan Kuprieiev 31fed211f6 test: use suffixes and prefixes in tmpdir names
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-14 00:37:02 +03:00
Mr. Outis 9837625a0d Add draft for DVC shell completion in Bash 2018-10-12 14:01:03 -05:00
Ruslan Kuprieiev a0b5bdd143 dvc: bump to 0.19.11 2018-10-12 15:58:39 +03:00
Ruslan Kuprieiev 888db1bbb6 Merge pull request #1216 from amjadsaadeh/master
hdfs output files: fix import error when using hdfs output files on w…
2018-10-12 15:56:41 +03:00
Amjad Saadeh ecb4f0b998 hdfs output files: fix import error when using hdfs output files on windows
When output files are located on hdfs an import error has been raised.

Fixes #1215
2018-10-12 14:43:08 +02:00
Ruslan Kuprieiev c6811ae005 dvc: bump to 0.19.10 2018-10-12 04:22:26 +03:00
Ruslan Kuprieiev 6ad674ca88 Merge pull request #1213 from efiop/master
state: use UNSIGNED INTEGER for inodes
2018-10-12 04:22:03 +03:00
Ruslan Kuprieiev 45afd36d76 state: use UNSIGNED INTEGER for inodes
Fixes #1207

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-12 03:51:29 +03:00
Ruslan Kuprieiev 0aac01ab88 state: add debug msg
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-12 03:51:29 +03:00
Ruslan Kuprieiev ce3bcc4af9 Merge pull request #1211 from efiop/master
dagascii: import asciimatics only when needed
2018-10-12 00:11:06 +03:00
Ruslan Kuprieiev 06064061d6 dagascii: import asciimatics only when needed
Related to #1210

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-11 23:53:56 +03:00
Ruslan Kuprieiev cae6e712c6 dvc: bump to 0.19.9 2018-10-11 16:11:17 +03:00
Ruslan Kuprieiev 4b525fe1fc system: fix typo in _reflink_darwin 2018-10-11 16:05:27 +03:00
Mr. Outis 31b7a5766f Add draft for DVC shell completion in Zsh 2018-10-10 23:21:05 -05:00
Ruslan Kuprieiev 13a2e4692d Merge pull request #1201 from efiop/master
repro: show which dependencies have changed
2018-10-11 00:10:39 +03:00
Ruslan Kuprieiev 1de87bc91c repro: show which dependencies have changed
Fixes #1164

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-10 23:40:26 +03:00
Ruslan Kuprieiev 82b10a8256 Merge pull request #1200 from efiop/master
import: add a note about supported schemes
2018-10-10 02:59:09 +03:00
Ruslan Kuprieiev a736082595 import: add a note about supported schemes
Fixes #1199

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-10 02:47:20 +03:00
Ruslan Kuprieiev 4888ab82a7 dvc: bump to 0.19.8 2018-10-09 21:01:17 +03:00
Ruslan Kuprieiev bc93cc0bf6 Merge pull request #1198 from efiop/master
hdfs: don't use close_fds on Windows
2018-10-09 21:00:55 +03:00
Ruslan Kuprieiev 971f3259f1 hdfs: don't use close_fds on Windows
Fixes #1197

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-09 20:42:18 +03:00
Ruslan Kuprieiev a4f9789aa4 travis: upload coverage on python 2 2018-10-08 03:30:02 +03:00
Ruslan Kuprieiev 584972ba0a travis: upload coverage on every build 2018-10-08 03:08:35 +03:00
Ruslan Kuprieiev b2b76b84d6 Merge pull request #1195 from efiop/master
dvc: stop using ntfsutils module
2018-10-07 18:07:51 +03:00
Ruslan Kuprieiev a6ea2f3db9 dvc: stop using ntfsutils module
Fixes #1162

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-07 17:49:29 +03:00
Ruslan Kuprieiev 881f188016 tests: run in parallel mode 2018-10-07 03:15:56 +03:00
Ruslan Kuprieiev c3f145591b Merge pull request #1193 from efiop/master
test: upload coverage only on CRON
2018-10-07 02:19:27 +03:00
Ruslan Kuprieiev 1a8842d8c6 test: upload coverage only on CRON
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-07 02:09:46 +03:00
Ruslan Kuprieiev 143a7f79a1 test: add version requirements for codecov and coverage 2018-10-06 23:24:55 +03:00
Ruslan Kuprieiev 7bff8c6f89 dagascii: don't try to cover tty code 2018-10-06 16:46:01 +03:00
Ruslan Kuprieiev 2daa694bf2 Merge pull request #1192 from efiop/master
dagascii: add more keys
2018-10-06 15:52:52 +03:00
Ruslan Kuprieiev 27a96ef814 dagascii: add more keys
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-06 15:41:36 +03:00
Ruslan Kuprieiev 315bbfde3b Merge pull request #1191 from efiop/master
show: use asciimatics to provide scrollable graph
2018-10-06 14:31:43 +03:00
Ruslan Kuprieiev 53b37bb266 show: use asciimatics to provide scrollable graph
Fixes #1177

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-06 01:04:00 +03:00
Ruslan Kuprieiev 005db9bb3a Merge pull request #1190 from efiop/master
dvc: add a note about dvc.org/support to error msgs
2018-10-05 22:24:20 +03:00
Ruslan Kuprieiev 7b36fd007c dvc: add a note about dvc.org/support to error msgs
Fixes #1152

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-05 21:26:55 +03:00
Ruslan Kuprieiev 2c9b08a89b Merge pull request #1189 from mroutis/update-networkx
Update networkx package (== 2.1, >= 2.1)
2018-10-05 03:07:54 +03:00
Ruslan Kuprieiev 66a962976e Merge pull request #1188 from mroutis/add-aur-repo
Add AUR package
2018-10-05 03:07:28 +03:00
Mr. Outis 4eff31dd5e Update README noticing that the AUR package is not officially maintained 2018-10-04 18:55:26 -05:00
Mr. Outis 377be75dd0 Update networkx package in setup.py 2018-10-04 18:44:10 -05:00
Ruslan Kuprieiev e015b9a167 readme: add a new line 2018-10-05 02:14:12 +03:00
Mr. Outis 2a7ec2d2df Update networkx package (== 2.1, >= 2.1)
The latest version is 2.2 (https://pypi.org/project/networkx).

Updating it to prevent collisions with some package managers already
using the latest one.
2018-10-04 18:08:27 -05:00
Mr. Outis 3d34f4e8c7 Add arch linux repository 2018-10-04 18:03:56 -05:00
Ruslan Kuprieiev c10bd344ea Merge pull request #1185 from efiop/master
stage: properly report StageFileFormatError
2018-10-04 13:30:11 +03:00
Ruslan Kuprieiev 6a8cd31ac9 stage: properly report StageFileFormatError
Fixes #1186

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-04 13:21:01 +03:00
Ruslan Kuprieiev 2c49d7d004 Merge pull request #1182 from efiop/master
gc: add a comfirmation prompt
2018-10-03 00:00:15 +03:00
Ruslan Kuprieiev 990bbe32c1 gc: add a comfirmation prompt
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-02 23:39:45 +03:00
Ruslan Kuprieiev f3668b0a4e cli: status: fix typo 2018-10-02 01:41:45 +03:00
Ruslan Kuprieiev 73eaa2a611 dvc: bump to 0.19.7 2018-10-01 21:35:42 +03:00
Ruslan Kuprieiev e63ae3d676 Merge pull request #1181 from jeepkd/fix-aws-env
s3: fix dvc not loading AWS settings from environment variables
2018-10-01 21:32:22 +03:00
Jeep Kiddee 9528ad6a1d Fix dvc not loading AWS settings from environment variables 2018-10-02 00:59:59 +07:00
Ruslan Kuprieiev aea92e0bdf Merge pull request #1180 from efiop/master
Misc test improvements
2018-10-01 18:26:42 +03:00
Ruslan Kuprieiev d1077c4457 test: improve coverage
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-01 18:11:17 +03:00
Ruslan Kuprieiev edf73dc52f test: skip hdfs test if can't connect
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-01 16:29:55 +03:00
Ruslan Kuprieiev b587f7833a dvc: bump to 0.19.6 2018-10-01 15:47:20 +03:00
Ruslan Kuprieiev 998918c690 Merge pull request #1179 from efiop/master
Misc fixes
2018-10-01 15:44:54 +03:00
Ruslan Kuprieiev 8aa53a5e71 dvc: introduce per-remote --jobs default
Fixes #1167

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-01 15:28:38 +03:00
Ruslan Kuprieiev cc2ebb2099 dvc: introduce -T|--all-tags option
Fixes #1150

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-10-01 12:13:17 +03:00
Ruslan Kuprieiev b9439f2e5b dvc: bump to 0.19.5 2018-09-29 23:06:51 +03:00
Ruslan Kuprieiev d112d2a829 Merge pull request #1175 from efiop/master
Misc fixes
2018-09-28 21:55:58 +03:00
Ruslan Kuprieiev f4fc3f47e4 show: don't use asciicanvas package
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-28 21:42:59 +03:00
Ruslan Kuprieiev 417ad8706e state: use state version
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-28 21:42:59 +03:00
Ruslan Kuprieiev 2e09dbb592 output: be more verbose
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-28 21:42:59 +03:00
Ruslan Kuprieiev 67d63a4a5b updater: check for updates every day
Our release schedule got really quick, so we can't afford to wait a week between checking for updates, because there might be new important versions with bugfixes released every day. Checking once per day should not cause too much inconvenience for users.
2018-09-28 18:20:29 +03:00
Ruslan Kuprieiev 8eb6fd5285 Merge pull request #1173 from efiop/master
repro: introduce -P|--all-pipelines
2018-09-28 13:58:48 +03:00
Ruslan Kuprieiev 669b2ec303 repro: introduce -P|--all-pipelines
Fixes #1165

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-28 13:33:23 +03:00
Ruslan Kuprieiev 04b3cd40e0 Merge pull request #1172 from efiop/master
s3: support env variables
2018-09-28 12:26:55 +03:00
Ruslan Kuprieiev 12d6292ed6 s3: support env variables
Fixes #1160

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-28 12:17:00 +03:00
Ruslan Kuprieiev 27edc4f027 dvc: bump to 0.19.4 2018-09-28 11:05:52 +03:00
Ruslan Kuprieiev 5b0abe61ab Merge pull request #1171 from efiop/master
move: use relpath intead of rel_path property
2018-09-28 10:25:09 +03:00
Ruslan Kuprieiev 1e1daeaae1 move: use relpath intead of rel_path property
Fixes #1170

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-28 10:06:51 +03:00
Ruslan Kuprieiev ece2b8ae0b dvc: bump to 0.19.3 2018-09-27 08:58:36 +03:00
Ruslan Kuprieiev fcea9dccc5 Merge pull request #1166 from efiop/master
dvc: remove unused module
2018-09-26 16:26:45 +03:00
Ruslan Kuprieiev 51b390146a dvc: remove unused module
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-26 15:34:26 +03:00
Ruslan Kuprieiev 1a6dedfe60 cli: add a note that remove --outs is default behavior. 2018-09-25 09:07:32 +03:00
Ruslan Kuprieiev 88f0913f91 Merge pull request #1161 from peter-schmidbauer/patch-1
dvc: project: Fix typos in warnings
2018-09-24 12:28:10 +03:00
Peter Schmidbauer b38112795a dvc: project: Fix typos in warnings 2018-09-24 09:30:07 +02:00
Ruslan Kuprieiev 363db9125f pull: add dummy call to try establishing the connection (#1159)
Fixes #1151

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-24 04:41:55 +03:00
pared 89e03c8606 pipeline show add --dot option (#1141)
* pipeline show add --dot option

* refactor common code, pydot in requirements

* code formatting

* code formatting
2018-09-24 02:50:41 +03:00
Ruslan Kuprieiev 7bed7213dc Merge pull request #1158 from efiop/master
osx: reflink: check if clib has clonefile()
2018-09-23 14:46:36 +03:00
Ruslan Kuprieiev 76c2f812f0 osx: reflink: check if clib has clonefile()
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-23 14:39:09 +03:00
Ruslan Kuprieiev 8d151273c8 Merge pull request #1157 from efiop/master
reflink: osx: check that not older than High Sierra
2018-09-23 13:44:27 +03:00
Ruslan Kuprieiev 10e10f9535 reflink: osx: check that not older than High Sierra
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-23 13:29:18 +03:00
Ruslan Kuprieiev 3fd5603921 Merge pull request #1156 from efiop/master
dvc: use ctypes instead of cffi
2018-09-23 12:53:52 +03:00
Ruslan Kuprieiev ff63376d49 dvc: use ctypes instead of cffi
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-23 12:45:16 +03:00
Ruslan Kuprieiev 5071b5026d setup: exclude tests 2018-09-23 09:56:19 +03:00
Ruslan Kuprieiev c08804eddd dvc: bump to 0.19.2 2018-09-23 08:59:14 +03:00
Ruslan Kuprieiev cbc8cf23f9 Merge pull request #1155 from efiop/master
requirements: require newer google-cloud
2018-09-23 08:51:16 +03:00
Ruslan Kuprieiev 5279b1ae8f requirements: stop using obsoleted google-cloud
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-23 08:36:42 +03:00
Ruslan Kuprieiev 057663696a Merge pull request #1154 from efiop/master
setup: misc fixes
2018-09-23 06:23:46 +03:00
Ruslan Kuprieiev 8b513fb336 setup: misc fixes
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-23 06:06:01 +03:00
Ivan Shcheklein 04373e81e8 update README links 2018-09-22 10:30:44 -07:00
Ivan Shcheklein 2601766323 update README links, description 2018-09-22 10:28:42 -07:00
Ruslan Kuprieiev ad216acce4 travis: osx: modify CPPFLAGS and LDFLAGS 2018-09-22 14:53:30 +03:00
Ruslan Kuprieiev 5753944161 travis: osx: install openssl 2018-09-22 13:40:25 +03:00
Ruslan Kuprieiev b7ac347c06 travis: install: upgrade setuptools 2018-09-22 13:02:40 +03:00
Ruslan Kuprieiev beedab77f1 dvc: bump to 0.19.1 2018-09-22 12:27:59 +03:00
Ruslan Kuprieiev b1b0e3413d Merge pull request #1148 from efiop/master
Build osx packages on older versions
2018-09-22 12:24:57 +03:00
Ruslan Kuprieiev c4593cd369 travis: build on older xcode version
Fixes #1147

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-22 12:12:45 +03:00
Ruslan Kuprieiev 6a55d1502f dvc: avoid using pyreflink module
Fixes #1149

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-22 12:11:18 +03:00
Ruslan Kuprieiev e93a87f451 dvc: bump to 0.19.0 2018-09-21 11:09:00 +03:00
Ruslan Kuprieiev 3fff8d771d Merge pull request #1145 from efiop/master
dvc: properly join paths for push/pull progress bar
2018-09-21 09:31:54 +03:00
Ruslan Kuprieiev 69bb753980 dvc: properly join paths for push/pull progress bar
Fixes #1095

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-21 09:22:06 +03:00
Ruslan Kuprieiev 1d654d0444 Merge pull request #1143 from efiop/master
dvc: introduce -d|--with-deps option
2018-09-21 09:14:22 +03:00
Ruslan Kuprieiev 0618d9d0dc dvc: introduce -d|--with-deps option
Fixes #1077

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-21 08:45:09 +03:00
Ruslan Kuprieiev dea130cc61 Merge pull request #1142 from jeepkd/support-git-submodule
scm: support git submodule
2018-09-21 00:24:32 +03:00
Jeep Kiddee bbd5096de7 scm: support git submodule
Fixes #1093
2018-09-21 02:17:28 +07:00
Ruslan Kuprieiev 545ea1efa5 Merge pull request #1139 from efiop/master
scm: require abs path
2018-09-20 00:48:19 +03:00
Ruslan Kuprieiev e1841f07cb scm: require abs path
Fixes #1136

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-20 00:14:16 +03:00
Ruslan Kuprieiev 04efd1d6d3 Merge pull request #1138 from efiop/master
test: don't forget to check return code
2018-09-19 23:49:11 +03:00
Ruslan Kuprieiev 89aa2a957e test: don't forget to check return code
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-19 23:38:58 +03:00
Ruslan Kuprieiev 1da4546515 Merge pull request #1137 from efiop/master
local: use is_local property
2018-09-19 22:01:46 +03:00
Ruslan Kuprieiev 49c22cb139 local: use is_local property
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-19 21:40:00 +03:00
Ruslan Kuprieiev 4aa26a0a3a Merge pull request #1135 from efiop/master
ssh: properly join chunks on python3
2018-09-19 19:31:41 +03:00
Ruslan Kuprieiev 8d5675a92c ssh: properly join chunks on python3
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-19 18:58:36 +03:00
Ruslan Kuprieiev 2b1ea1e4b9 Merge pull request #1133 from efiop/master
repro: introduce -p|--pipeline option
2018-09-19 10:01:13 +03:00
Ruslan Kuprieiev 34faad3e96 repro: introduce -p|--pipeline option
Fixes #1107

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-19 09:35:06 +03:00
Ruslan Kuprieiev 9dab3f2e49 ssh: fix order 2018-09-19 07:19:02 +03:00
Ruslan Kuprieiev 72e8b6983b Merge pull request #1132 from efiop/master
ssh: add 'password' and 'ask_password' options
2018-09-19 06:52:16 +03:00
Ruslan Kuprieiev 682737e4a2 ssh: add 'password' and 'ask_password' options
Fixes #1124

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-19 05:50:06 +03:00
Ruslan Kuprieiev 8932254c12 Merge pull request #1131 from efiop/master
dvc: list changed files
2018-09-18 13:14:39 +03:00
Ruslan Kuprieiev 0c97e19d5c dvc: list files changed
Fixes #1130

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-18 12:37:55 +03:00
Ruslan Kuprieiev e698703969 Merge pull request #1129 from efiop/master
run: check that -f doesn't contain any subdirs
2018-09-18 07:59:04 +03:00
Ruslan Kuprieiev 571713484b run: check that -f doesn't contain any subdirs
Fixes #1110

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-18 07:46:28 +03:00
Ruslan Kuprieiev 65a9370281 Merge pull request #1128 from efiop/master
state: limit number of entries
2018-09-18 03:53:18 +03:00
Ruslan Kuprieiev 0043bd5de3 state: limit number of rows
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-18 03:40:41 +03:00
Ruslan Kuprieiev 33d04d0d59 state: count number of inserts and track size of the db
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-18 01:33:31 +03:00
Ruslan Kuprieiev 5e3d7c6c2b Merge pull request #1126 from efiop/master
state optimization
2018-09-17 10:59:32 +03:00
Ruslan Kuprieiev 59dcdb02ea state: optimize operations
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-17 10:47:16 +03:00
Ruslan Kuprieiev 23d00a15b9 state: remove unused method
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-17 10:47:16 +03:00
Ruslan Kuprieiev 2c3c0b08bd state: save timestamp
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-17 10:47:16 +03:00
Ruslan Kuprieiev 75d7fc2206 state: merge State and LinkState classes
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-17 10:47:16 +03:00
Ruslan Kuprieiev d704c41682 dvc: use INTEGER for inode
Fixes #1117

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-17 10:47:16 +03:00
Ruslan Kuprieiev 428811bb71 Merge pull request #1122 from efiop/master
dvc: don't scan output directories for dvc files
2018-09-15 07:45:21 +03:00
Ruslan Kuprieiev 470825aec5 dvc: don't scan output directories for dvc files
`dvc status` for directory with 100K files sped up from ~6 sec to ~2
sec.

Fixes #1121

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-15 07:24:00 +03:00
Ruslan Kuprieiev 5fbfc135da Merge pull request #1120 from efiop/master
Misc enhancements
2018-09-15 05:08:54 +03:00
Ruslan Kuprieiev b4e62772b8 state: merge state and link state
Fixes #1091

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-15 04:54:26 +03:00
Ruslan Kuprieiev 912f0f72e5 run: require at least some arguments
Fix #1100

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-15 04:54:26 +03:00
Ruslan Kuprieiev 5730b1d088 Merge pull request #1118 from efiop/master
repro: don't check for missing dependencies when running with `--dry`
2018-09-14 00:33:45 +03:00
Ruslan Kuprieiev 5c762f316e repro: don't check for missing dependencies when running with --dry
Fixes #1113

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-14 00:23:28 +03:00
Ruslan Kuprieiev 7791dc8cfb Merge pull request #1116 from efiop/master
state: remove unused params
2018-09-13 18:24:09 +03:00
Ruslan Kuprieiev dc344056c5 state: remove unused params
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-13 18:01:23 +03:00
Ruslan Kuprieiev 87e82daad2 Merge pull request #1112 from efiop/master
state: use sqlite
2018-09-12 16:24:34 +03:00
Ruslan Kuprieiev f044a6090f state: use sqlite
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-12 15:52:34 +03:00
Ruslan Kuprieiev c68823e8d0 dvc: bump to 0.18.15 2018-09-10 15:57:24 +03:00
Ruslan Kuprieiev a17dc2504d Merge pull request #1106 from efiop/master
stage: check that deps exist before running the command
2018-09-09 23:25:04 +03:00
Ruslan Kuprieiev 537e7c75a0 stage: check that deps exist before running the command
Fixes #1101

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-09 22:59:19 +03:00
Ruslan Kuprieiev 76d6b37b1e Merge pull request #1105 from efiop/master
ssh: don't recv() before checking with recv_ready()
2018-09-09 18:59:31 +03:00
Ruslan Kuprieiev 8d13ba164e ssh: don't recv() before checking with recv_ready()
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-09 18:48:57 +03:00
Ruslan Kuprieiev 559c097394 dvc: bump to 0.18.14 2018-09-08 21:39:20 +03:00
Ruslan Kuprieiev c97bfea90f Merge pull request #1103 from efiop/master
ssh fixes
2018-09-08 21:30:44 +03:00
Ruslan Kuprieiev abf395da99 ssh: properly poll output
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-08 21:14:50 +03:00
Ruslan Kuprieiev 0687c4e199 ssh: add timeout
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-08 16:17:08 +03:00
Ruslan Kuprieiev d1931e3dc0 readme: add discord link 2018-09-06 23:20:57 +03:00
Ruslan Kuprieiev 78c9d1edab dvc: bump to 0.18.13 2018-09-05 17:12:19 +03:00
Ruslan Kuprieiev 7ef8645465 push/pull/fetch: lower the thread multiplier
Fixes #1094
2018-09-05 17:11:45 +03:00
Ruslan Kuprieiev 208540c66d dvc: bump to 0.18.12 2018-09-04 19:30:28 +03:00
Ruslan Kuprieiev 216a6e5c1c Merge pull request #1089 from efiop/master
state: use json.dumps() instead of dump()
2018-09-04 19:28:54 +03:00
Ruslan Kuprieiev 8d8a94bcb7 state: use json.dumps() instead of dump()
This actually 10 times faster, because everything is stored in the
memory.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-04 19:09:25 +03:00
Ruslan Kuprieiev 1a18c4d3f1 Merge pull request #1088 from efiop/master
state: limit db size and add a file size limit
2018-09-04 18:53:32 +03:00
Ruslan Kuprieiev 211b957d17 state: limit db size and add a file size limit
Fixes #1087

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-04 18:24:19 +03:00
Ruslan Kuprieiev 4f6967a017 dvc: bump to 0.18.11 2018-09-03 00:21:23 +03:00
Ruslan Kuprieiev c1ac1b4c6e Merge pull request #1082 from efiop/master
state: don't forget to lock before dumping db
2018-09-03 00:06:11 +03:00
Ruslan Kuprieiev 022eea8214 state: don't forget to lock before dumping db
Related to #1081

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-02 23:47:50 +03:00
Ruslan Kuprieiev f1f1a97fbf Merge pull request #1080 from efiop/master
state: don't forget subdirs when computing effective mtime
2018-09-01 15:56:36 +03:00
Ruslan Kuprieiev a3aefe5f16 state: don't forget subdirs when computing effective mtime
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-09-01 15:41:42 +03:00
Ivan Shcheklein de3d8b528f remove Slack from README 2018-08-31 10:34:50 -07:00
Ruslan Kuprieiev 9c041a375f test: sleep to compensate for poor mtime resolution 2018-08-31 05:09:32 +03:00
Ruslan Kuprieiev 086155daa6 dvc: bump to 0.18.10 2018-08-31 04:16:52 +03:00
Ruslan Kuprieiev d47e6c39ab Merge pull request #1076 from efiop/master
remote: properly remove remote section
2018-08-31 04:15:42 +03:00
Ruslan Kuprieiev 249b60c030 remote: properly remove remote section
Fixes #1075

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-31 04:07:31 +03:00
Ruslan Kuprieiev 392cd006e3 tests: sleep() before creating a file 2018-08-30 17:27:05 +03:00
Ruslan Kuprieiev 2b0391629b readme: add slack channel 2018-08-30 13:41:16 +03:00
Ruslan Kuprieiev 3b66176f6b dvc: bump to 0.18.9 2018-08-30 00:51:45 +03:00
Ruslan Kuprieiev baa6bcd035 Merge pull request #1073 from efiop/master
run: add `-y|--yes` option to bypass prompts
2018-08-30 00:51:00 +03:00
Ruslan Kuprieiev b159ace6d7 run: add -y|--yes option to bypass prompts
Fixes #1072

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-30 00:38:28 +03:00
Ruslan Kuprieiev ffde4a6a7a Merge pull request #1071 from efiop/master
readme fixes
2018-08-29 20:53:49 +03:00
Ruslan Kuprieiev 9a95698cd9 travis: check readme formatting
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-29 20:34:28 +03:00
Ruslan Kuprieiev 221dbf8245 readme: fix formatting
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-29 20:22:25 +03:00
Ruslan Kuprieiev 3d15391c7a dvc: bump to 0.18.8 2018-08-29 19:32:55 +03:00
Ruslan Kuprieiev f6f2ebb83e Merge pull request #1070 from efiop/master
dvc: properly compute mtime of the directory
2018-08-29 19:32:29 +03:00
Ruslan Kuprieiev ac80d59704 dvc: properly compute mtime of the directory
Fixes #1067

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-29 19:17:50 +03:00
Ruslan Kuprieiev 3574c8954b dvc: update to 0.18.7 2018-08-29 16:01:28 +03:00
Ruslan Kuprieiev 0d78705227 Merge pull request #1069 from efiop/master
Misc
2018-08-29 16:00:45 +03:00
Ruslan Kuprieiev bf6ed76678 config: don't forget to convert port to int
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-29 15:50:25 +03:00
Ruslan Kuprieiev 076a2145db test: don't forget to sleep before modifying a file
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-29 14:54:27 +03:00
Ruslan Kuprieiev bb3cca6530 dvc: bump to 0.18.6 2018-08-29 14:02:31 +03:00
Ruslan Kuprieiev 165422a066 Merge pull request #1068 from efiop/master
misc fixes
2018-08-29 13:59:30 +03:00
Ruslan Kuprieiev 49baf38871 dvc: properly compute mtime for the directory
Fixes #1067

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-29 13:52:20 +03:00
Ruslan Kuprieiev 3461df1d8f config: properly merge with local config
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-29 13:52:20 +03:00
Ruslan Kuprieiev 03a3cf2f80 dvc: bump to 0.18.5 2018-08-28 13:05:36 +03:00
Ruslan Kuprieiev 94911965c4 Merge pull request #1066 from efiop/master
misc pipeline improvements
2018-08-28 12:45:06 +03:00
Ruslan Kuprieiev 55b3c47bbf pipeline: process edges and nodes separately
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-28 11:36:06 +03:00
Ruslan Kuprieiev 825da210cf dvc: use __str__ method for deps/outs
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-28 11:36:06 +03:00
Ruslan Kuprieiev ac97c4021d dvc: bump to 0.18.4 2018-08-27 17:51:09 +03:00
Ruslan Kuprieiev 213e757b52 Merge pull request #1063 from efiop/master
test: cover `pipeline show`
2018-08-27 15:05:38 +03:00
Ruslan Kuprieiev 6f0e293c9a test: cover pipeline show
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-27 14:40:21 +03:00
Ruslan Kuprieiev 7783bb900b Merge pull request #1062 from efiop/master
pipeline: fix corner case
2018-08-27 13:59:49 +03:00
Ruslan Kuprieiev bdca1ce8cc pipeline: fix corner case
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-27 13:44:16 +03:00
Ruslan Kuprieiev 80912b4a2f Merge pull request #1061 from efiop/master
ssh enhancements
2018-08-27 13:41:06 +03:00
Ruslan Kuprieiev c46bba6889 ssh: configure private key file
Fixes #1059

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-27 12:54:47 +03:00
Ruslan Kuprieiev 1283689ed4 ssh: add ability to configure port
Fixes #1060

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-27 09:31:35 +03:00
Ruslan Kuprieiev 9c5bbecb2e Merge pull request #1056 from efiop/master
repro: print a msg when pipeline is up to date
2018-08-24 20:08:29 +03:00
Ruslan Kuprieiev 316ea1d7cb repro: print a msg when pipeline is up to date
Fixes #1049

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-24 19:37:33 +03:00
Ruslan Kuprieiev 2139978646 Merge pull request #1055 from efiop/master
dvc: check stage filename
2018-08-24 19:00:52 +03:00
Ruslan Kuprieiev fa89ec8789 dvc: check stage filename
Fixes #1054

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-24 18:45:56 +03:00
Ruslan Kuprieiev 93ef937fc2 dvc: bump to 0.18.3 2018-08-24 15:54:04 +03:00
Ruslan Kuprieiev 253315e4a8 Merge pull request #1052 from efiop/master
dependency: only use rel_path for local deps
2018-08-24 14:54:39 +03:00
Ruslan Kuprieiev 9579cb7928 config: fix ConfigError
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-24 14:36:38 +03:00
Ruslan Kuprieiev 18f0ca0d92 dependency: only use rel_path for local deps
Fixes #1048

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-24 14:36:38 +03:00
Ivan Shcheklein d555bae6e1 try to use flow GIF 2018-08-23 22:57:32 -07:00
Ruslan Kuprieiev 4a43a29f11 dvc: update to 0.18.2 2018-08-23 13:14:50 +03:00
Ruslan Kuprieiev 2ff7d9b571 Merge pull request #1047 from efiop/master
s3: properly configure session and clients
2018-08-23 10:36:16 +03:00
Ruslan Kuprieiev 9d303b6b1b s3: properly configure session and clients
Fixes #1020

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-23 10:17:11 +03:00
Ruslan Kuprieiev f682e556ec Merge pull request #1046 from efiop/master
test: cover pipeline show --ascii
2018-08-23 08:07:47 +03:00
Ruslan Kuprieiev 26e007f407 test: cover pipeline show --ascii
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-23 07:57:23 +03:00
Ruslan Kuprieiev a7f14c2d3c Merge pull request #1045 from efiop/master
dvc: print command specific help
2018-08-23 07:39:37 +03:00
Ruslan Kuprieiev 79f9cedd37 dvc: print command specific help
Fixes #1044

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-23 07:30:37 +03:00
Ruslan Kuprieiev 623d3e2fed dvc: bump to 0.18.1 2018-08-23 04:48:45 +03:00
Ruslan Kuprieiev cb7eed71de setup: update grandalf reqs 2018-08-23 04:42:27 +03:00
Ruslan Kuprieiev 9135944d7c reqs: use grandalf 0.6 2018-08-23 04:41:49 +03:00
Ruslan Kuprieiev ac4fff2c45 dvc: bump to 0.18.0 2018-08-22 17:22:04 +03:00
Ruslan Kuprieiev 912538f165 Merge pull request #1040 from efiop/master
dvc: use our errors in the argparser
2018-08-22 13:05:52 +03:00
Ruslan Kuprieiev 35c5d4c294 dvc: use our errors in the argparser
Fixes #1037

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-22 10:13:58 +03:00
Ruslan Kuprieiev 3887e973c4 Merge pull request #1036 from efiop/master
logger: use colored prefix instead of coloring everything
2018-08-22 05:32:34 +03:00
Ruslan Kuprieiev 173a9fc7aa logger: use colored prefix instead of coloring everything
Fixes #1034

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-22 05:25:27 +03:00
Ruslan Kuprieiev 0281c2714c Merge pull request #1033 from efiop/master
dagascii: make vertical space smaller
2018-08-22 05:15:58 +03:00
Ruslan Kuprieiev 6d10bff23b dagascii: make vertical space smaller
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-22 05:03:48 +03:00
Ruslan Kuprieiev ff5152ee5f Merge pull request #1027 from efiop/master
dvc: add --recursive
2018-08-21 10:51:07 +03:00
Ruslan Kuprieiev c6f642fd6b dvc: add --recursive
Fixes #1026

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-21 10:41:19 +03:00
Ruslan Kuprieiev ee0dcc384d Merge pull request #1024 from efiop/master
Visualize DAG in terminal using Sugiyama layout and ASCII rendering
2018-08-19 21:17:55 +03:00
Ruslan Kuprieiev 8a484df228 pipeline: use Dvcfile by default
Fixes #970

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-19 21:01:47 +03:00
Ruslan Kuprieiev c378e4b3d2 dvc: pipeline show: create ASCII visualizations for DAG
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-19 21:01:12 +03:00
Ruslan Kuprieiev 00d53f50b1 Merge pull request #1023 from efiop/master
metrics: use --xpath if no xpath is specified in the stage file
2018-08-18 10:18:25 +03:00
Ruslan Kuprieiev 65ffc7d248 metrics: use --xpath if no xpath is specified in the stage file
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-18 05:06:29 +03:00
Ruslan Kuprieiev 95479e6d6b dvc: bump to 0.17.1 2018-08-17 14:24:42 +03:00
Ruslan Kuprieiev 79b9ae75e9 Merge pull request #1021 from efiop/master
dvc: properly resolve dvc name for external local files
2018-08-17 14:23:09 +03:00
Ruslan Kuprieiev a46b1be925 test: add test for dvc add for external local files
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-17 14:07:55 +03:00
Ruslan Kuprieiev a566e92ce0 dvc: properly resolve dvc name for external local files
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-17 11:20:59 +03:00
Ruslan Kuprieiev adbfbfd732 readme: add shell signs 2018-08-17 02:14:54 +03:00
Ruslan Kuprieiev d2b562e25a readme: fix "mailing list" section 2018-08-17 01:48:17 +03:00
Ruslan Kuprieiev bcea7a132a readme: fix header 2018-08-17 01:45:41 +03:00
Ruslan Kuprieiev d1aa1f3491 readme: wording 2018-08-17 01:32:45 +03:00
Ruslan Kuprieiev 246b80d6a4 readme: fix link 2018-08-17 01:31:33 +03:00
Ruslan Kuprieiev 8f2d5c4195 readme: add a 'Mailing list' link to the header 2018-08-17 01:27:22 +03:00
Ruslan Kuprieiev bd9da20e20 readme: add a note about our mailing list 2018-08-17 01:25:07 +03:00
Ruslan Kuprieiev c6138729f9 Merge pull request #1017 from efiop/master
help: add a better description for `dvc init -f`
2018-08-14 20:56:23 +03:00
Ruslan Kuprieiev 6743b36894 help: add a better description for dvc init -f
https://github.com/iterative/dvc.org/issues/71

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-14 20:08:28 +03:00
Ruslan Kuprieiev 0dd995453f dvc: bump to 0.17.0 2018-08-14 05:12:53 +03:00
Dmitry Petrov 42a15d91f7 Merge pull request #1016 from iterative/issue_templates
Simplify issue templates
2018-08-13 19:05:23 -07:00
Dmitry a6cb238982 Simplify issue templates 2018-08-13 18:49:06 -07:00
Ruslan Kuprieiev 1b4d7abce3 Update .coveragerc 2018-08-14 03:45:34 +03:00
Ruslan Kuprieiev e6def1287d Merge pull request #1014 from efiop/master
test: improve test coverage
2018-08-14 02:41:19 +03:00
Ruslan Kuprieiev ce5a46d032 test: test output duplication
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-14 02:33:25 +03:00
Ruslan Kuprieiev fcadd18069 coverage: ignore code dependent on tty
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-14 02:33:25 +03:00
Ruslan Kuprieiev a3588c4504 Merge pull request #1013 from efiop/master
Misc repro fixes
2018-08-14 02:16:14 +03:00
Ruslan Kuprieiev 86322fbe35 repro: print help when Dvcfile doesn't exist
Fixes #1003

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-14 02:08:12 +03:00
Ruslan Kuprieiev c635adbfc7 repro: fix msgs
Fixes #1008

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-14 02:02:07 +03:00
Ruslan Kuprieiev 6e95508e9e Merge pull request #1012 from efiop/master
destroy: mention removing data files
2018-08-13 08:08:30 +03:00
Ruslan Kuprieiev aaca0e7a38 destroy: mention removing data files
Fixes #1000

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-13 07:30:58 +03:00
Ruslan Kuprieiev 487993995a Merge pull request #1010 from efiop/master
misc fixes
2018-08-13 07:16:06 +03:00
Ruslan Kuprieiev e84f231c34 help: add cmd description
Fixes #1007

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-13 07:02:40 +03:00
Ruslan Kuprieiev 2b19b6bbfa prompt: add y/n to msg
Fixes #999

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-13 06:29:05 +03:00
Ruslan Kuprieiev db017543d5 dvc: detect output duplication
Fixes #1006

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-13 06:24:32 +03:00
Ruslan Kuprieiev bc2c84370d run: change prompt message
Fixes #1005

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-13 05:43:56 +03:00
Ruslan Kuprieiev d34119d6f0 Merge pull request #1004 from efiop/master
status: print msg if there is nothing to reproduce
2018-08-13 05:31:33 +03:00
Ruslan Kuprieiev b940380e12 status: print msg if there is nothing to reproduce
Fixes #1001

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-13 04:52:19 +03:00
Ruslan Kuprieiev e3d39063ba Merge pull request #1002 from efiop/master
help: fix punctuation
2018-08-13 04:20:54 +03:00
Ruslan Kuprieiev 11db27f333 help: fix punctuation
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-13 03:43:13 +03:00
Ruslan Kuprieiev 15210f73b2 Merge pull request #998 from efiop/master
travis: retry pip install
2018-08-13 00:17:30 +03:00
Ruslan Kuprieiev 9be3f4f122 travis: retry pip install
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-12 23:38:34 +03:00
Ruslan Kuprieiev ddf1839d38 Merge pull request #997 from efiop/master
Misc fixes
2018-08-11 21:39:31 +03:00
Ruslan Kuprieiev 3ec0a5c4ae remote: add additional checks and msgs for checkout
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-11 21:27:07 +03:00
Ruslan Kuprieiev 3814e0da4e remote: add changed() method
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-11 05:37:08 +03:00
Ruslan Kuprieiev 292702473b remote: local: rename changed() -> changed_cache()
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-11 05:37:08 +03:00
Ruslan Kuprieiev 6776f6e21c dvc: print more info on checkout
Fixes #987

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-11 05:37:08 +03:00
Ruslan Kuprieiev b261554818 dvc: remote: format "No Remote repository" error
Fixes #988

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-11 05:37:08 +03:00
Ruslan Kuprieiev 10dd535176 travis: cache pip
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-11 05:37:08 +03:00
Ruslan Kuprieiev e8949c1a77 Merge pull request #996 from efiop/master
config: add core.interactive
2018-08-10 23:49:27 +03:00
Ruslan Kuprieiev 6a8703e54e config: add core.interactive
Fixes #989

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-10 23:07:46 +03:00
Ruslan Kuprieiev 6cc0c30937 Merge pull request #994 from efiop/master
dvc: print a msg when there is no cache to remove
2018-08-10 19:30:49 +03:00
Ruslan Kuprieiev b15f5ffa3b dvc: print a msg when there is no cache to remove
Fix #990

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-10 18:52:35 +03:00
Ivan Shcheklein d343f8562c add alts to contributor images, it's better to have them if image is not loading 2018-08-09 19:14:19 -07:00
Ivan Shcheklein 321c1d5f8b Merge pull request #993 from sergey48k/master
Added hall-of-fame to README
2018-08-09 19:08:14 -07:00
Sergey Surkov f6e20b162c Addressed comments for hall-of-fame
replaced: dataversioncontrol -> iterative
replaced: sergey48K -> efiop

Note that this change means that GitHub API for hall-of-fame will be counted against efiop's limit. Should be small though: 10-20 requests per hour.
2018-08-09 19:06:15 -07:00
Sergey Surkov a249df3f5d Added hall-of-fame to README 2018-08-09 17:02:56 -07:00
Ruslan Kuprieiev fb4b979183 Merge pull request #991 from efiop/master
dvc: ignore unraleted numpy runtime warnings
2018-08-09 08:06:00 +03:00
Ruslan Kuprieiev 98a7150c65 dvc: ignore unraleted numpy runtime warnings
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-09 07:12:56 +03:00
Ruslan Kuprieiev dd99a09135 github: add "Feature request" issue template 2018-08-08 23:36:32 +03:00
Ruslan Kuprieiev f854f5a97a github: Add "Bug report" issue template 2018-08-08 23:35:04 +03:00
Ruslan Kuprieiev 9e068234ba dvc: bump to 0.16.6 2018-08-07 06:02:08 +03:00
Ruslan Kuprieiev 6600ccf478 cli: use sys.exit 2018-08-07 06:01:43 +03:00
Ruslan Kuprieiev 84abef7ac0 Merge pull request #986 from efiop/master
setup: use extras_require for backward compatibility
2018-08-07 05:32:41 +03:00
Ruslan Kuprieiev 9ff2503a72 dvc: bump to 0.16.5
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-07 05:14:34 +03:00
Ruslan Kuprieiev 4ff4a1042e setup: use extras_require for backward compatibility
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-07 05:13:35 +03:00
Ruslan Kuprieiev 2195ff2b13 dvc: bump to 0.16.4 2018-08-07 04:50:36 +03:00
Ruslan Kuprieiev e539e74fde Merge pull request #985 from efiop/master
setup: use proper env marker for futures dependency
2018-08-07 04:49:55 +03:00
Ruslan Kuprieiev c345552460 setup: use proper env marker for futures dependency
Fixes #968

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-07 04:35:13 +03:00
Ruslan Kuprieiev cee8e1ec9f Merge pull request #984 from efiop/master
scm: fix LD_LIBRARY_PATH if running from a bundle
2018-08-07 04:35:04 +03:00
Ruslan Kuprieiev e4b8cf2e0e scm: fix LD_LIBRARY_PATH if running from a bundle
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-07 04:27:35 +03:00
Ruslan Kuprieiev e2e7649afe Merge pull request #983 from efiop/master
dvc: fix LD_LIBRARY_PATH on binaries
2018-08-07 03:30:15 +03:00
Ruslan Kuprieiev 77b2e45a64 dvc: fix LD_LIBRARY_PATH on binaries
Fixes #965

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-07 03:22:16 +03:00
Ruslan Kuprieiev e403568afe Merge pull request #982 from efiop/master
dvc: print more info for add/run/checkout
2018-08-07 01:08:36 +03:00
Ruslan Kuprieiev 79eac34163 dvc: print more info for add/run/checkout
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-07 00:59:17 +03:00
Ruslan Kuprieiev d9b7f7b97a Merge pull request #981 from efiop/master
dvc: print help if CLI args are wrong
2018-08-06 20:00:40 +03:00
Ruslan Kuprieiev cbe9fde7a6 dvc: print help if CLI args are wrong
Fixes #979

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-06 19:49:31 +03:00
Ruslan Kuprieiev 24be60bf39 setup.py: fix typo 2018-08-06 00:31:27 +03:00
Ruslan Kuprieiev fe38b2768f setup: don't forget to import sys 2018-08-06 00:11:05 +03:00
Ruslan Kuprieiev 105d8fc9b5 setup: fix install requirement 2018-08-06 00:02:35 +03:00
Ruslan Kuprieiev 9d7581c6bd dvc: bump to 0.16.3 2018-08-05 23:21:09 +03:00
Ruslan Kuprieiev a14946d0bb travis: script: fix version format
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-05 23:19:01 +03:00
Ruslan Kuprieiev 6ad361b741 dvc: use custom VersionAction
On some Python versions action=version prints to stderr and on some to
stdout.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-05 23:17:36 +03:00
Ruslan Kuprieiev 6fe6c04f6c travis: script: strip new line 2018-08-05 21:22:19 +03:00
Ruslan Kuprieiev 23b167f5d7 travis: script.sh: use echo instead of print 2018-08-05 20:26:46 +03:00
Ruslan Kuprieiev 2dde6e848b travis: properly check for command existance 2018-08-05 18:43:59 +03:00
Ruslan Kuprieiev 5e5705ae7e Merge pull request #980 from efiop/master
travis: test version reported by pip
2018-08-05 18:19:01 +03:00
Ruslan Kuprieiev a2d3ced7f1 travis: test version reported by pip
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-05 18:11:00 +03:00
Ruslan Kuprieiev 0804f84679 dvc: bump to 0.16.2
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-05 17:34:16 +03:00
Ruslan Kuprieiev 02da5eabdc dvc: bump to 0.16.0 2018-08-05 04:47:23 +03:00
Ruslan Kuprieiev 85ab6a141b Merge pull request #978 from efiop/master
dvc: fix help output
2018-08-05 04:45:02 +03:00
Ruslan Kuprieiev 8d1e18d543 dvc: fix help output
Fixes #934

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-05 04:36:55 +03:00
Ruslan Kuprieiev d42809abd3 Merge pull request #977 from efiop/master
repro: introduce --dry and --interactive
2018-08-05 01:05:22 +03:00
Ruslan Kuprieiev f5bb3e4501 tests: use shallow=False with filecmp.cmp()
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-05 00:52:29 +03:00
Ruslan Kuprieiev aab04a22ad repro: introduce --prompt
Fixes #894

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-05 00:21:35 +03:00
Ruslan Kuprieiev ed8052c229 repro: introduce --dry option
Fixes #972
Related to #894

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-05 00:21:35 +03:00
Ruslan Kuprieiev 7baf368367 Merge pull request #976 from efiop/master
test: improve coverage
2018-08-04 20:10:55 +03:00
Ruslan Kuprieiev b5b8b8d06c test: test for unsupported outs/deps
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-04 19:59:23 +03:00
Ruslan Kuprieiev 75a6352d3d logger: don't cover tty code
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-04 19:59:23 +03:00
Ruslan Kuprieiev baac7a16bd lock: remove unused method init()
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-04 19:59:23 +03:00
Ruslan Kuprieiev e4137542df coverage: skip service lines
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-04 19:59:23 +03:00
Ruslan Kuprieiev b9a5bde22f test: cover status
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-04 19:59:23 +03:00
Ruslan Kuprieiev 891ec0bad4 test: cover metrics
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-04 19:59:23 +03:00
Ruslan Kuprieiev a0a138c69f Merge pull request #975 from efiop/master
run: ask for confirmation when overwriting stage file
2018-08-04 19:15:55 +03:00
Ivan Shcheklein 1e4f6e9f09 Merge branch 'master' of github.com:iterative/dvc 2018-08-04 09:02:49 -07:00
Ivan Shcheklein 9f5ba8c38f minor README fixes - typos, layouts, etc 2018-08-04 09:02:10 -07:00
Ruslan Kuprieiev ef7aa024f9 run: ask for confirmation when overwriting stage file
Fixes #974

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-04 18:44:13 +03:00
Ruslan Kuprieiev 48c9916937 Merge pull request #971 from sotte/fix_typos_in_cli
Add missing whitespaces in CLI help texts
2018-08-04 16:02:42 +03:00
Stefan Otte 04f0320126 Add missing whitespaces in CLI help texts 2018-08-04 10:18:07 +02:00
Ivan Shcheklein 536f369f7e fix typo in README 2018-08-03 22:10:18 -07:00
Ivan Shcheklein 23a4b296e9 try README improvements 2018-08-03 21:59:29 -07:00
Ivan Shcheklein a4482bac3b README image 2018-08-03 21:33:35 -07:00
Ruslan Kuprieiev e5fd24f530 Update __init__.py 2018-08-03 22:43:18 +03:00
Ruslan Kuprieiev 4dc61beab0 travis: fix typo 2018-08-03 21:35:44 +03:00
Ruslan Kuprieiev f02bf73587 reqs: only requre futures pkg on python 2.7
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-03 20:55:23 +03:00
Ruslan Kuprieiev cb13e45632 dvc: bump to 0.15.2 2018-08-03 20:43:46 +03:00
Ruslan Kuprieiev d0ce57adac reqs: move pytest to test-requirements
Fixes #966

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-03 13:33:24 +03:00
Ruslan Kuprieiev eaa994527d reqs: don't forget futures
Fixes #967

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-03 13:25:36 +03:00
Ruslan Kuprieiev 89617501a4 Merge pull request #964 from efiop/master
dvc: additional progress info
2018-08-02 21:27:43 +03:00
Ruslan Kuprieiev 8be51b6056 dvc: additional progress info
Fixes #957

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-02 21:15:04 +03:00
Ivan Shcheklein f7a17a9336 Merge pull request #963 from konrad/master
fix link to tutorial
2018-08-02 09:47:00 -07:00
Konrad Förstner 2788288e8e fix link to tutorial 2018-08-02 18:39:08 +02:00
Ruslan Kuprieiev 80e7040a21 config: print a better msg for unsupported URL
Fixes #954

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-02 16:05:59 +03:00
Ruslan Kuprieiev bd4bbf1438 build: disable binary build on Python 3.7
Fixes #962

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-02 14:46:38 +03:00
Ruslan Kuprieiev 935cbc9513 readme: remove dvc logo 2018-08-02 02:25:35 +03:00
Ruslan Kuprieiev 98c5875668 dvc: bump to 0.15.1 2018-08-02 00:39:45 +03:00
Ruslan Kuprieiev 42111b6348 readme: don't use "raw" directive
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-02 00:38:08 +03:00
Ruslan Kuprieiev 3363b85367 dvc: bump to 0.15.0 2018-08-01 22:05:48 +03:00
Dmitry Petrov 83994d6ea7 Ordering in the header fix 2018-08-01 10:52:13 -07:00
Dmitry Petrov 568a8fb904 Ordering in the header 2018-08-01 10:51:12 -07:00
Ruslan Kuprieiev 47d00e80d2 Merge pull request #960 from efiop/master
dvc: print info when computing md5 of a large file
2018-08-01 16:10:55 +03:00
Ruslan Kuprieiev a9114d57a5 dvc: print info when computing md5 of a large file
Fixes #958

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-08-01 15:39:05 +03:00
Ruslan Kuprieiev 1bfcc12853 readme: use stock plucks 2018-08-01 11:03:39 +03:00
Ivan Shcheklein be817c5b84 add alts to pictures to fail gracefully in README 2018-07-31 23:55:49 -07:00
Ivan Shcheklein 1f15cf23e7 remove backlinks generation for the contents in README 2018-07-31 23:45:14 -07:00
Ivan Shcheklein 28e2f7d06f make contents header bold 2018-07-31 23:36:30 -07:00
Ivan Shcheklein 6bf853d635 try to add contents 2018-07-31 23:32:38 -07:00
Ivan Shcheklein 8120104f2e get everything back, it was broken before for a different reason 2018-07-31 23:26:06 -07:00
Ivan Shcheklein 1cb41f0a8a try to remove links to fix pypi 2018-07-31 23:23:44 -07:00
Ivan Shcheklein ad204cf915 remove raw html, pypi support is broken 2018-07-31 23:21:27 -07:00
Ivan Shcheklein ac3963878e cleanup and simplify README 2018-07-31 23:04:52 -07:00
Ivan Shcheklein dd891a8cd3 add menu links to the README top 2018-07-31 22:55:03 -07:00
Ivan Shcheklein 6c54810034 fix enumerated list in README 2018-07-31 22:41:32 -07:00
Ivan Shcheklein f23671a26a have to use raw image html, see github markup issue #295 2018-07-31 22:34:48 -07:00
Ivan Shcheklein 229983c403 try to add logo image to README 2018-07-31 22:20:14 -07:00
Ruslan Kuprieiev 6b042fb15a Merge pull request #955 from efiop/master
status: don't forget about missing cache
2018-07-30 17:39:24 +03:00
Ruslan Kuprieiev 1879d49bc2 status: don't forget about missing cache
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-07-30 17:27:24 +03:00
Ruslan Kuprieiev 627606d6f2 Merge pull request #952 from efiop/master
dvc: show filenames in `push/pull` output
2018-07-26 21:48:06 +03:00
Ruslan Kuprieiev 7e4f39d8eb dvc: show filenames in push/pull output
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-07-26 16:50:25 +03:00
90 changed files with 5335 additions and 1117 deletions
+1 -1
View File
@@ -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
-9
View File
@@ -1,14 +1,5 @@
languages:
Python: true
checks:
argument-count:
enabled: false
method-complexity:
enabled: false
method-lines:
config:
threshold: 50
exclude_paths:
- tests/
+2
View File
@@ -7,3 +7,5 @@ exclude_lines =
if __name__ == .__main__.:
pass
raise NotImplementedError
# pragma: no cover
# pragma: no cover
+3 -1
View File
@@ -5,4 +5,6 @@ config.local
updater
cache
state
state.lock
state.lock
state-journal
state-wal
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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
View File
@@ -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):
+2 -1
View File
@@ -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
+12 -3
View File
@@ -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)
+4 -4
View File
@@ -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
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+9 -2
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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))
-4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+4 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
-17
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+71
View File
@@ -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
+218
View File
@@ -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
+10 -7
View File
@@ -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,
)
+5 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))
+14
View File
@@ -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'
+5
View File
@@ -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
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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
View File
@@ -1,7 +1,6 @@
import os
import stat
import shutil
import filecmp
from dvc.main import main
from dvc.command.imp import CmdImport
+5 -1
View File
@@ -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)
+35
View File
@@ -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):
+9 -1
View File
@@ -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
View File
@@ -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)
+10
View File
@@ -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
View File
@@ -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)
+44
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+31
View File
@@ -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
View File
@@ -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)