Compare commits

..
469 Commits
Author SHA1 Message Date
Ruslan Kuprieiev db11175ad9 dvc: bump to 0.33.0 2019-03-29 03:57:30 +02:00
Ivan Shcheklein 4a9188c0b5 Move man doc links to descriptions, fix some help copies (#1805)
* move man doc links to descriptions, fix some help copies

* minor init force message fix

* deal with ugly inheritance in lock base

* remove unnecessary super calls

* add return back to python lock/unlock run functions
2019-03-29 03:56:41 +02:00
Paweł Redzyński 6f9dac280f stage: remove outs upon import (#1796)
* stage: remove outs upon import

* Update dvc/remote/local.py

Co-Authored-By: pared <[email protected]>
2019-03-28 16:45:47 +02:00
Ruslan Kuprieiev c48a5e99be Merge pull request #1795 from ei-grad/fix-test-analytics
tests: fix for globally disabled analytics
2019-03-28 14:45:53 +02:00
Andrew Grigorev e0bf7314a0 tests: fix for globally disabled analytics 2019-03-28 14:54:04 +03:00
Ruslan Kuprieiev f0d3207573 Merge pull request #1793 from pared/1788
repo: fix stages gathering for checkout
2019-03-28 13:47:11 +02:00
pared ace92cb422 repo: fix stages gathering for checkout 2019-03-28 12:13:39 +01:00
Ruslan Kuprieiev a613aafd39 Merge pull request #1787 from ei-grad/ssh-walk-files-error-description
ssh: walk_files error description
2019-03-27 21:00:29 +02:00
Andrew Grigorev 587b645ec9 ssh: walk_files error description 2019-03-27 20:16:24 +03:00
Ruslan Kuprieiev a0e8eff944 Merge pull request #1789 from pared/1786
run: make run respect persist flag
2019-03-27 16:48:38 +02:00
pared bf68fbffec run: make run respect persist flag 2019-03-27 13:28:53 +01:00
Ruslan Kuprieiev 35218a77ab Merge pull request #1781 from puhoshville/fix-1038
cli: add pipeline show --tree
2019-03-26 17:21:33 +02:00
Dmitrii Pukhov 263dee8650 Replace ValueError with DvcException 2019-03-26 22:39:41 +09:00
Ruslan Kuprieiev b791e1f57b Merge pull request #1782 from pared/1740
metrics: rename show, repro: display metrics
2019-03-26 13:10:46 +02:00
Ruslan Kuprieiev 01441f4d12 Merge pull request #1785 from pared/1784
remove: force remove to remove persistent outputs
2019-03-26 13:00:03 +02:00
pared 7b7ecf5bc1 metrics: rename show, repro: fix metrics display problem 2019-03-26 11:56:35 +01:00
pared 70b804307d remove: force remove to remove persistent outputs 2019-03-26 11:41:12 +01:00
Dmitrii Pukhov 5346fc44bc Add treelib dependency to setup.py 2019-03-26 13:51:29 +09:00
Dmitrii Pukhov fba43277e6 cli: add pipeline show --tree
Add option to unfold dependencies into a tree.

Fixes #1038
2019-03-26 01:29:49 +09:00
Ruslan Kuprieiev 2776f2f317 appveyor: debug codecov failure 2019-03-25 18:29:21 +02:00
Paweł Redzyński aa42f76355 Merge pull request #1780 from pared/cmd_run_add_outs_persist_to_obligatory_fields
run: add --outs-persist and --outs-persist-no-cache to obligatory run args
2019-03-25 17:16:30 +01:00
pared af8e9ff17a run: add --outs-persist and --outs-persist-no-cache to obligatory arguments 2019-03-25 17:07:48 +01:00
Ruslan Kuprieiev 49ae03fdc5 Merge pull request #1779 from pared/1763
repo: check whether outputs in graphs do not overlap
2019-03-25 16:19:54 +02:00
pared 2b3107f72c Check whether outputs in graphs do not overlap 2019-03-25 14:19:15 +01:00
Ruslan Kuprieiev e94bc88b26 Merge pull request #1759 from pared/1214
[WIP] run: add --outs-persist and --outs-persist-no-cache options
2019-03-25 11:43:45 +02:00
pared 85b26c2063 run: add --outs-persist option 2019-03-25 09:43:58 +01:00
Peter Fogh ac15a42eac More error information is needed wrt. remote files (#1737)
As described in #1735, I think DVC needs to output more info about remote files,
Thus, I have added this to stage.py.
Additionally, I think that DVC should not crash if the SSH client is trying to remove a fil which already does not exists. This, I have implemented the catch of the FileNotFoundError : [Errno 2] No such file exception.

Fixes #1735
2019-03-23 00:36:46 +02:00
Lee Yi Jie Joel 2ba657c77a Add links to documentation for all commands (#1767) 2019-03-22 20:32:06 +02:00
Andrew Grigorev e67185b75a Merge pull request #1776 from ei-grad/cyclic-import-3
Fix cyclic dependency
2019-03-22 20:10:15 +03:00
Andrew Grigorev 7d79043bbb dvc.scm.scm_context -> dvc.repo.scm_context.scm_context
The `dvc/scm/__init__.py:scm_context` works with `Repo` objects and
is used mostly in `dvc/repo/...`, so it is better to put it in
`dvc/repo/scm_context.py`.

Fix #1769
2019-03-22 19:53:41 +03:00
Andrew Grigorev 6fbdf1b5b0 dvc.repo.tree -> dvc.scm.tree
Tree objects are used as `Repo().tree`, so it seemed logical to put them
inside `dvc/repo/tree.py`. But what is more important - `dvc.repo.Repo`
depends on `dvc.scm` and it is not convinient to use `dvc.repo.tree` in
`dvc.scm.git`.

Fix #1769
2019-03-22 19:53:41 +03:00
Ruslan Kuprieiev 402eb2da01 travis: debug codecov failure 2019-03-22 15:50:30 +02:00
Ruslan Kuprieiev d27e8ded61 Merge pull request #1749 from mroutis/fix-1747
cache: provide at least one value in parser
2019-03-21 22:47:19 +02:00
Ruslan Kuprieiev bc65805749 Merge pull request #1709 from ei-grad/brancher-refactor
Brancher refactor
2019-03-21 22:12:56 +02:00
Mr. Outis 1df5dea351 cache: provide at least one value in parser
Fix #1747
2019-03-21 12:28:46 -07:00
Ruslan Kuprieiev 0897e8e9ba Merge pull request #1764 from mroutis/update-completion-scripts
scripts: update completion for zsh and bash
2019-03-21 21:18:56 +02:00
Andrew Grigorev 9219398d83 tests: fix #1754 2019-03-21 19:47:42 +03:00
Andrew Grigorev 63dd3f2c25 tests: fix get_gitignore_content
readlines() returns entries with \n on the end
2019-03-21 19:47:42 +03:00
Andrew Grigorev 2ef3a7c7c0 tests: windows has different linesep 2019-03-21 19:47:42 +03:00
Andrew Grigorev 8fa968cf33 collect: nx.dfs_postorder_nodes yeilds the passed node 2019-03-21 19:47:42 +03:00
Andrew Grigorev 16f74f6894 tests: test_metrics missing .dvc files in commit 2019-03-21 19:47:42 +03:00
Andrew Grigorev 8c8b585de9 tests: test_install commit and clean
Uncommited files wouldn't be accessible as git objects.

It needs `git clean -fd` to amend "Working Tree" metrics collection
which took its part when the git tree is dirty.
2019-03-21 19:47:42 +03:00
Andrew Grigorev 9e6c52079b Abstract access to file objects
Abstract access to filesystem and its implementation to access files and
particulary Stage objects in different git branches without `git
checkout` execution.
2019-03-21 19:47:42 +03:00
Mr. Outis ce65771596 scripts: update completion for zsh and bash
Fix #1732
2019-03-21 09:28:54 -07:00
Andrew Grigorev e782344a98 Move dvc/scm/git.py to dvc/scm/git/__init__.py 2019-03-21 18:03:06 +03:00
Paweł Redzyński c65495e15f RFC data cloud: declutter test (#1761) 2019-03-21 16:48:28 +02:00
Ivan Shcheklein 9e944c3c61 Merge pull request #1757 from J0/patch-1
Fix typo in repro.py
2019-03-20 16:13:54 -07:00
Lee Yi Jie Joel e0a1d7803d Fix typo in repro.py 2019-03-20 16:12:15 -07:00
Paweł Redzyński 5a736cb79a remote local, repo: use walk_files instead of os.walk (#1750) 2019-03-20 18:45:21 +02:00
Ruslan Kuprieiev 24072c8083 daemon: don't dump env into debug log 2019-03-20 16:07:33 +02:00
Paweł Redzyński ee16409495 checkout: progress bar (#1713) 2019-03-20 13:39:10 +02:00
Dmitry Petrov 70bc101571 [WIP] Pkg install (#1742)
* Download Git package

* Extract tmp git repo & fetch data

* Handle fetch\cloud errors and rename select option

* Modify gitignore files when deploy a package

* More tests for git ignore file. Fix an old issue.

* Raise and exception when .gitignor not in a file subdir

* Existing tests fixes

* Basic test for packages

* Comment out pkg tests

* Codeclimate improvements

* Reformatting

* Enable pkg tests

* Fix tests no Windows

* Another old bug fix

* Fix encoding issue on Windows

* Another windows fix: apply compat.str magic to file write

* Back to posix path in gitignore and apply compat.str magic to tests

* gitignorepath: OS specifix path to posix

* Forgotten assignment

* misc: cleanup small issues

Signed-off-by: Ruslan Kuprieiev <[email protected]>

* cli: pkg: hide from help

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-20 04:09:12 +02:00
Ruslan Kuprieiev a691985576 cli: daemon: hide from help
Fixes #1657
2019-03-20 03:30:55 +02:00
Ruslan Kuprieiev 1d08ed8834 repo: unprotect: remove unused move 2019-03-19 20:03:01 +02:00
Ruslan Kuprieiev 7cd618ce5d repo: unprotect: use os.rename() instead of move()
Follow up for #1752
2019-03-19 19:42:25 +02:00
Ruslan Kuprieiev 90e41de5f7 Merge pull request #1752 from ei-grad/unprotect-improvement
unprotect: copy should preceed the move
2019-03-19 19:36:34 +02:00
Andrew Grigorev beb32b387e unprotect: copy should preceed the move
It is better to make a file copy before moving it, since move is
a nearly-atomic operation and copy is a long one.

Also the biggest problem here is that if some application would access
the file during the process of `copyfile` then it would get the part of
file.
2019-03-19 20:14:42 +03:00
Ruslan Kuprieiev e1346a8a36 Merge pull request #1741 from shcheklein/migrate-to-pytest
Migrate to pytest
2019-03-18 11:25:07 +02:00
Ivan Shcheklein 08036b445b set timeouts method to thread to avoid problems in parallel run 2019-03-17 20:11:01 -07:00
Ivan Shcheklein f085e01717 try to fix progess bar test in a parallel pytest environment 2019-03-17 19:33:09 -07:00
Ivan Shcheklein 75421f0f21 migrate to pytest 2019-03-17 14:49:49 -07:00
Ruslan Kuprieiev a54eaeb59e Merge pull request #1734 from mroutis/fix-1698
cli: Use correct color codes on Windows
2019-03-17 18:37:45 +02:00
Ruslan Kuprieiev 7a5ff38198 Merge pull request #1738 from PeterFogh/patch-1
Added `--exe` argument to nosetest default
2019-03-17 15:50:46 +02:00
Ruslan Kuprieiev ee51ccab4a test: reword --exec explanation
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-17 15:44:02 +02:00
Peter Fogh d87ceb4e46 Added --exe argument to nosetest default
See https://github.com/iterative/dvc/issues/1736#issuecomment-473665122
2019-03-17 14:34:18 +01:00
Ruslan Kuprieiev fc646882ad deploy: use xcode 8.3 for mac 2019-03-17 14:38:51 +02:00
Mr. Outis e4bc59ca03 cli: Use correct color codes on Windows
Fix #1698
2019-03-16 17:51:28 -06:00
Ruslan Kuprieiev db5a068f16 dvc: bump to 0.32.1 2019-03-14 23:00:47 +02:00
Ruslan Kuprieiev 62b4d3d112 Merge pull request #1728 from mroutis/fix-1721
stage: convert wdir path to posix
2019-03-14 22:56:56 +02:00
Ruslan Kuprieiev b5d7bf0144 test: format with latest black
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-14 22:41:37 +02:00
Mr. Outis 21778f6719 stage: convert wdir path to posix
Fix #1721
2019-03-14 14:40:28 -06:00
Ruslan Kuprieiev acf88d88be test: requirements: fix black version 2019-03-14 22:39:43 +02:00
Ruslan Kuprieiev a3c8ab3fbf pre-commit: fix black version 2019-03-14 22:39:08 +02:00
Ruslan Kuprieiev f57b55db79 test: requirements: use latest stable black version 2019-03-14 22:03:35 +02:00
Ruslan Kuprieiev 98733ba503 dvc: bump to 0.32.0 2019-03-14 16:15:40 +02:00
Ruslan Kuprieiev 5fbf4f6db3 Revert "cli: status: remove unused code"
This reverts commit ac27b4076a.

Fixes #1727
2019-03-14 12:46:57 +02:00
Ruslan Kuprieiev 3efa2600e7 Merge pull request #1725 from Qsi1984/patch-1
Fixing mixup of Posixpath and os.path
2019-03-14 11:49:17 +02:00
Neuenschwander Markus c5c03ea1ba Fixing mixup of Posixpath and os.path 2019-03-14 10:19:00 +01:00
Ruslan Kuprieiev fdf2b1c016 Merge pull request #1694 from efiop/paramiko
dvc: ignore cryptography's warnings
2019-03-14 03:05:21 +02:00
Ruslan Kuprieiev fc81ea10f5 Merge pull request #1642 from efiop/master
cli: status: remove unused code
2019-03-14 01:32:37 +02:00
Ruslan Kuprieiev 0fc476e4f4 dvc: ignore cryptography's warnings
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-14 01:29:44 +02:00
Ruslan Kuprieiev ac27b4076a cli: status: remove unused code
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-14 01:25:15 +02:00
Ruslan Kuprieiev 48e85970d9 exceptions: fix NotDvcRepoError message
Fixes #1723
2019-03-13 23:17:42 +02:00
Ruslan Kuprieiev 804ba334f6 Merge pull request #1700 from pared/1699
add: prevent adding git files
2019-03-13 22:22:39 +02:00
Ruslan Kuprieiev 9334f751ad Merge pull request #1690 from pared/1677
push/pull/status -c: should complain on no checksum locally and remotely
2019-03-13 18:52:37 +02:00
pared e1daa921ee push/pull/status -c: should warn on no checksum locally and remotely 2019-03-13 17:42:51 +01:00
Ruslan Kuprieiev 37716efa3c Merge pull request #1719 from iterative/create_contributingmd
CONTRIBUTING: point to dvc.org docs
2019-03-13 16:36:03 +02:00
Paweł Redzyński cdfa8bf2b2 CONTRIBUTING: point to dvc.org docs 2019-03-13 15:08:29 +01:00
Ruslan Kuprieiev fe5cd32824 dvc: bump to 0.31.1 2019-03-13 15:24:13 +02:00
pared 4897a6a44f add: prevent adding git files 2019-03-13 14:11:59 +01:00
Ruslan Kuprieiev d2a75afd70 test: increase timeout 2019-03-13 15:10:35 +02:00
Ruslan Kuprieiev 52cd26f93a nose: try limiting number of processes to 4 2019-03-13 14:30:04 +02:00
Ruslan Kuprieiev ca648457ec Merge pull request #1673 from pared/1641
add: cleanup .gitignore decorator
2019-03-13 13:59:26 +02:00
Ruslan Kuprieiev 64e5b1e3b1 Merge pull request #1695 from pared/1640
yaml loading error: display relative path
2019-03-13 13:47:04 +02:00
Ruslan Kuprieiev 5530125d80 remote: s3: account for empty pages 2019-03-13 13:33:55 +02:00
Ruslan Kuprieiev 175ec76063 remote: hdfs: fix typos 2019-03-13 12:16:38 +02:00
Ruslan Kuprieiev d5e5527409 Merge pull request #1712 from shcheklein/fix-1688
Apply inode value mapping when checking for an existing entry
2019-03-13 12:04:24 +02:00
Ivan Shcheklein dc02e4e810 apply inode value mapping when checking for an existing entry 2019-03-11 19:12:58 -07:00
pared 161d3ce7ef yaml loading error: display relative path 2019-03-11 23:19:45 +01:00
pared f8711b3faa repo/scm: move tracking reminder from repo to scm 2019-03-11 23:13:34 +01:00
pared 7a01d5fe47 add: cleanup .gitignore decorator 2019-03-11 22:54:21 +01:00
Ruslan Kuprieiev 5c39c6b8bd Merge pull request #1708 from bwalsh/s3-list-objects-v1
remote: adds support for s3.list_objects
2019-03-11 22:14:50 +02:00
Brian Walsh a65a3dc517 remote: adds support for s3.list_objects
Optionally use list_objects, useful for ceph and other s3 emulators
fixes: #1476
2019-03-11 11:07:35 -07:00
Ruslan Kuprieiev cb10210572 dvc: bump to 0.31.0 2019-03-10 16:56:22 +02:00
Ruslan Kuprieiev d98810cf9c dvc: bump to 0.30.2 2019-03-10 16:54:58 +02:00
Ruslan Kuprieiev 40f8181acf Merge pull request #1671 from mroutis/fix-1356
add: support the `--file` option
2019-03-10 01:07:36 +02:00
Ruslan Kuprieiev 3c659b534c Merge pull request #1703 from Qsi1984/patch-1
Fixes #1701
2019-03-09 16:57:40 +02:00
Neuenschwander Markus ad324f0eb2 Fixing #1701
Fixing bug #1701 which made ssh remotes unusable on windows machines.
2019-03-08 20:53:42 +01:00
Ruslan Kuprieievandmroutis bf873b7c43 Update dvc/exceptions.py
Co-Authored-By: mroutis <[email protected]>
2019-03-07 17:55:57 -06:00
Ruslan Kuprieiev c118e4d6f0 Merge pull request #1684 from mroutis/fix-1667
stage: always reproduce when --force is given
2019-03-07 13:36:17 +02:00
Mr. Outis b319e18510 add: support the --file option
Fix #1356
2019-03-06 16:18:29 -06:00
Mr. Outis 599ee58a5f stage: always reproduce when --force is given
Fix #1667
2019-03-05 22:36:23 -06:00
Ruslan Kuprieiev 610cfa984c Merge pull request #1687 from pared/1640
utils: translate yaml file loading exception to dvc one
2019-03-05 23:41:56 +02:00
Ruslan Kuprieiev 63dc3974e3 Merge pull request #1678 from mroutis/fix-1674
remote: upload/download to a temp file
2019-03-05 23:34:44 +02:00
pared a86ba00927 utils: translate yaml file loading exception to dvc one 2019-03-05 14:27:34 +01:00
Ruslan Kuprieiev 97c0997fe8 dvc: bump to 0.30.1 2019-03-03 05:07:29 +02:00
Ruslan Kuprieiev a36d772723 Merge pull request #1681 from shcheklein/fix-1680
Stage checksum with default wdir fix
2019-03-03 04:53:25 +02:00
Ivan Shcheklein ee7cb41fcb add tests to properly test wdir in checksum calculation 2019-03-02 16:37:00 -08:00
Ivan Shcheklein 0b32974506 fix stage checksum calculation when wdir is default 2019-03-02 16:24:23 -08:00
Mr. Outis 1c850af12d remote: upload/download to a temp file
Fix #1674
2019-03-02 10:26:13 -06:00
Ruslan Kuprieiev 9175c45b14 dvc: bump to 0.30.0 2019-03-01 21:26:42 +02:00
Ruslan Kuprieiev cce25db97e Merge pull request #1672 from shcheklein/add_wdir_tests
Add more tests for wdir/fname new logic
2019-03-01 16:15:27 +02:00
Ruslan Kuprieiev 0913a65661 Merge pull request #1668 from prihoda/patch-1
Log computed stage MD5 in verbose mode
2019-03-01 15:27:47 +02:00
prihoda d63db35100 Log computed stage MD5 in verbose mode
Sometimes it is handy to manually update the stage MD5 checksum after some non-breaking changes were made (e.g. renaming an output file). Logging the computed md5 hash in verbose mode can help experienced users to fix this problem.
2019-03-01 14:11:29 +02:00
Ivan Shcheklein e26db30f60 add more tests for wdir/fname new logic 2019-02-28 21:43:14 -08:00
Ruslan Kuprieiev 3bd3f47193 Merge pull request #1669 from pared/rfc_unify_stage_file_loading
stage: extract yaml loading to util method
2019-02-28 18:22:40 +02:00
pared 8ee53f8c81 stage: extract yaml loading to util method 2019-02-28 16:27:16 +01:00
Ruslan Kuprieiev 92d7445716 Merge pull request #1662 from pared/1648
stage: check if local path contains symlink and, if so, treat added files as relpath
2019-02-28 16:22:01 +02:00
pared f8ef28c4c6 stage: check if local path contains symlink and, if so, treat added files as relpath 2019-02-28 15:00:05 +01:00
Ruslan Kuprieiev 8ff27454e6 Merge pull request #1660 from mroutis/fix-1602
logger: Format progress bar interruption
2019-02-28 14:52:18 +02:00
Mr. Outis 93920baaec logger: Format progress bar interruption
Fix #1602
2019-02-27 21:24:22 -06:00
Ruslan Kuprieiev d65c7dc4d2 Merge pull request #1663 from pared/rfc_1641
[RFC] add command: refactor
2019-02-27 16:12:11 +02:00
pared df1201a508 add command: refactor 2019-02-27 13:48:40 +01:00
Ruslan Kuprieiev 2214f6b65d Merge pull request #1661 from pared/1659
remote local: add dir state update after processing the files
2019-02-26 13:28:50 +02:00
pared a8a32091f7 remote local: add dir state update after processing the files 2019-02-26 12:15:02 +01:00
Ruslan Kuprieiev 68603f012e Merge pull request #1647 from pared/1526
remote local/state : add check for cache dir metadata changes
2019-02-25 20:11:44 +02:00
Ivan Shcheklein 38536903e1 [WIP] Introduce wdir for DVC stage files (#1658)
* introduce wdir logic to superceed and expand -f and -cwd

* add stage file name and wdir validation tests and fix some checks
2019-02-25 19:52:54 +02:00
pared 4464e29c15 remote local/state : add check for cache dir metadata changes 2019-02-25 18:35:02 +01:00
Guy Smoilov 220b4b26a2 Clarified command line flags for dvc gc (#1644)
* Clarified command line flags for dvc gc

* Formatted with black, more explanation in GC_HELP

* Fix typo in GC_HELP

* Style corrections
2019-02-23 09:39:03 +02:00
Ruslan Kuprieiev 63f87ba9fe Merge pull request #1653 from efiop/output
output/deps: raise proper exception when file/dir doesn't exist
2019-02-23 09:32:00 +02:00
Ruslan Kuprieiev 6bf62e6576 Merge pull request #1646 from mroutis/fix-1570
ssh: upload to a tmp file to prevent data corruption
2019-02-22 20:30:36 +02:00
Ruslan Kuprieiev ef9f7da038 setup: fix flake8 errors
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-22 19:52:09 +02:00
Ruslan Kuprieiev 9ca00adde8 dvc: modify flake8 configuration
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-22 19:36:19 +02:00
Mr. Outis 600514cba9 ssh: upload to a tmp file to prevent data corruption
Fix #1570
2019-02-22 10:40:55 -06:00
Ruslan Kuprieiev 9961956c01 output/deps: raise proper exception when file/dir doesn't exist
From discussion on discord
https://discordapp.com/channels/485586884165107732/485596304961962003/548083580052701205

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-22 14:59:01 +02:00
Ruslan Kuprieiev 281aa612f3 Merge pull request #1651 from mroutis/fix-1617
status: imply --cloud when a remote is given
2019-02-22 13:02:29 +02:00
Ruslan Kuprieiev 3a16dc3bf9 Merge pull request #1650 from mroutis/fix-1634
stage: always reproduce callback stages
2019-02-22 13:01:11 +02:00
Mr. Outis 5cfcc956db status: imply --cloud when a remote is given
Fix #1617
2019-02-21 18:32:48 -06:00
Mr. Outis d640859fc4 stage: always reproduce callback stages
Fix #1634
2019-02-21 15:56:38 -06:00
Ruslan Kuprieiev 9a298fc835 Merge pull request #1649 from mroutis/fix-makedirs
py2: raise OSErrors on makedirs
2019-02-21 22:14:22 +02:00
Ruslan Kuprieievandmroutis 3c01102bbd 💅 remove unneccesary e
Co-Authored-By: mroutis <[email protected]>
2019-02-21 12:56:10 -06:00
Ruslan Kuprieiev 967954a35f travis: try caching pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-21 19:48:05 +02:00
Ruslan Kuprieiev 5579fee604 travis: cache pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-21 19:41:45 +02:00
Mr. Outis d32705f01b py2: raise OSErrors on makedirs 2019-02-21 11:34:09 -06:00
Dmitry Petrov a41103434d Merge pull request #1639 from dmpetrov/suppress_outputs_in_tests
Suppress outputs in tests
2019-02-19 13:46:25 -08:00
Dmitry fdecb8d45c Remove unnecessary import 2019-02-19 13:05:03 -08:00
Dmitry 54bcba7177 Fix tests in metrics 2019-02-19 13:04:16 -08:00
Dmitry fecaa5a0a2 Suppress tests output 2019-02-19 13:03:37 -08:00
Ruslan Kuprieiev c1cdf77414 dvc: bump to 0.29.0 2019-02-19 21:44:01 +02:00
Ruslan Kuprieiev 2cf4e99d75 Merge pull request #1638 from efiop/master
ssh: external-outs: check if cache exists before copying
2019-02-19 21:43:13 +02:00
Ruslan Kuprieiev e5107b1da3 ssh: external-outs: check if cache exists before copying
Fixes #1635

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-19 21:33:47 +02:00
Ruslan Kuprieiev 1b703b8bc4 Merge pull request #1637 from mroutis/fix-1636
move: awareness of cwd when moving files
2019-02-19 20:17:00 +02:00
Mr. Outis 758c71e249 move: awareness of cwd when moving files
fix #1636
2019-02-19 11:55:43 -06:00
Ruslan Kuprieiev f0b1c30222 Merge pull request #1610 from mroutis/fix-830
ssh: Use SFTP when possible
2019-02-19 16:49:46 +02:00
Ruslan Kuprieiev 0693adb130 Merge pull request #1624 from shcheklein/master
Cleanup metrics
2019-02-19 08:29:49 +02:00
Ivan Shcheklein b79085ec69 add tests for metrics wrong type and some other edge cases 2019-02-18 17:17:20 -08:00
Mr. Outis 5e7a152b53 ssh: use sftp when possible
Related #830 [1/2]
2019-02-18 19:03:06 -06:00
Ruslan Kuprieiev 801a492a6b scripts: don't forget sdist for pypi
Fixes #1628

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-18 21:24:49 +02:00
Ruslan Kuprieiev 5b3dd1930f dvc: bump to 0.28.2
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-18 21:23:41 +02:00
Ivan Shcheklein e9b57251b0 Merge branch 'master' of github.com:iterative/dvc 2019-02-18 09:44:39 -08:00
Ruslan Kuprieiev 00e03cadbd dvc: bump to 0.28.1 2019-02-18 04:16:56 +02:00
Ruslan Kuprieiev 8c8176ec91 Merge pull request #1625 from efiop/master
travis: add xcode10.1 build
2019-02-18 04:16:06 +02:00
Ruslan Kuprieiev 3c49dfa293 travis: add xcode10.1 build
Related to #1623

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-18 04:03:04 +02:00
Ruslan Kuprieiev 36510548a5 local: fix typo
https://github.com/iterative/dvc/issues/1623
2019-02-18 03:39:51 +02:00
Ivan Shcheklein 708558da62 metrics tests: add test for wrong metric type, uppercase type 2019-02-17 15:24:46 -08:00
Ivan Shcheklein bd382c87d9 cleanup metrics help, fix type and xpath handling 2019-02-17 15:08:58 -08:00
Ruslan Kuprieiev 2eb39c461f dvc: bump to 0.28.0 2019-02-17 15:11:26 +02:00
Ruslan Kuprieiev 25ae052434 Merge pull request #1601 from efiop/919
Introduce `dvc commit`
2019-02-17 10:04:33 +02:00
Ruslan Kuprieiev 01a120f9fc dvc: introduce dvc commit
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-16 21:19:36 +02:00
Ruslan Kuprieiev 26c9fc1ba4 status: improve message verbosity
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-16 21:18:54 +02:00
Ruslan Kuprieiev 1f18119b74 repro: introduce --no-commit
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-16 21:18:54 +02:00
Ruslan Kuprieiev 99f4107d58 add: introduce --no-commit
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-16 21:18:54 +02:00
Ruslan Kuprieiev 3bc0b9728b run: introduce --no-commit
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-16 21:18:26 +02:00
Ruslan Kuprieiev 0eab7ed29e stage: split save() into save() + commit()
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-16 21:18:26 +02:00
Ruslan Kuprieiev bbd27527ff remote: RFC: remote.save()
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-16 21:18:26 +02:00
Ruslan Kuprieiev 4d4d926479 Merge pull request #1621 from pared/1221-2
add: update state for known checksum for dir
2019-02-16 20:06:35 +02:00
pared a3c873d953 add: update state for known checksum for dir 2019-02-16 18:45:28 +01:00
Ruslan Kuprieiev 9e407430dc Merge pull request #1620 from cand126/master
push: When there's nothing to push, show a message
2019-02-16 19:13:50 +02:00
Shao-Chun Ma 643a6ac194 command: refactor
Change staticmethod to classmethod.

Fixes #1619
2019-02-16 17:45:32 +01:00
Shao-Chun Ma c12fae0f1d command: fix for python 2
Remove the syntax that only works for python 3.

Fixes #1619
2019-02-16 17:06:38 +01:00
Ruslan Kuprieiev 4ccc121893 Merge pull request #1603 from pared/1221
add: recalculate checksum after link creation, test
2019-02-16 17:51:58 +02:00
pared c4274bd1a4 add: recalculate checksum after link creation, test 2019-02-16 16:30:20 +01:00
Shao-Chun Ma d05e01a86e command: refactor
Extract method check_up_to_date().

Fixes #1619
2019-02-16 16:26:45 +01:00
Shao-Chun Ma 6898c46508 command: refactor
Move the message to parent class.

Fixes #1619
2019-02-16 16:14:04 +01:00
Shao-Chun Ma 73b2a3362c dvc: refactor
Add a new return value for pull, push, and fetch.
It represents the number of files processed.

Fixes #1619
2019-02-16 15:25:05 +01:00
Shao-Chun Ma 33cdc50c88 remote: add a message
push: When there's nothing to push, show a message

Fixes #1619
2019-02-16 13:24:35 +01:00
Ruslan Kuprieiev def24131fe dvc: bump to 0.27.1 2019-02-16 09:28:36 +02:00
Ruslan Kuprieiev 2e97830d38 Merge pull request #1616 from efiop/master
dvc: optimize imports
2019-02-16 08:03:41 +02:00
Ruslan Kuprieiev 0f024a9655 dvc: optimize imports
Currently `dvc` takes ~0.7sec to finish. After this patch: 0.17sec.
Profiling has been done using:

    python -X importtime -m dvc 2> log.log
    tuna log.log

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-16 07:45:03 +02:00
Ruslan Kuprieiev f6899c07c8 Merge pull request #1612 from efiop/ignore_build_cache
repro: fix --ignore-build-cache logic
2019-02-16 07:28:39 +02:00
Ruslan Kuprieiev 8636f473e9 repro: fix --ignore-build-cache logic
@robogeek has discovered that `dvc repro --ignore-build-cache` actually
force-reproduced ascendants of a changed stage instead of only
reproducing descendants. That bug was caused by a typo in an if
condition plus improper test.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-15 08:01:48 +02:00
Ruslan Kuprieiev 73f023c99d Merge pull request #1611 from efiop/master
ssh: handle url without path
2019-02-15 05:44:15 +02:00
Ruslan Kuprieiev 70c21f7f3e ssh: handle url without path
https://discordapp.com/channels/485586884165107732/485596304961962003/545702950350356502

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-15 04:38:45 +02:00
Ruslan Kuprieiev dc5a676306 Merge pull request #1607 from efiop/pylint
dvc: split Project methods into separate files
2019-02-14 00:27:24 +02:00
Ruslan Kuprieiev a3099bca6a metrics: fix bad metric file case handling
Also some refactoring got into this, sorry :(

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-14 00:15:18 +02:00
Ruslan Kuprieiev ba8d83e9b1 dvc: refactor dvc project class
1) split Project's methods into separate files;
2) rename Project into Repo(similar to gitpython's Repo)

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-13 03:31:23 +02:00
Ivan Shcheklein beb45202fd Merge pull request #1606 from 0xflotus/patch-1
fixed occasional
2019-02-11 20:25:32 -08:00
0xflotus ab94667103 fixed occasional 2019-02-12 05:18:20 +01:00
Ruslan Kuprieiev 940731941d dvc: bump to 0.27.0 2019-02-12 05:03:39 +02:00
Ruslan Kuprieiev 448bd493cf Merge pull request #1605 from shcheklein/master
Fix metrics show all braches/tags behavior when target is not specified
2019-02-12 04:56:14 +02:00
Ivan Shcheklein 51381fb0b0 fix metrics show all when no target is specified 2019-02-11 18:27:19 -08:00
Ruslan Kuprieiev 95345c45d0 dvc: bump to 0.26.1 2019-02-12 01:51:25 +02:00
Ruslan Kuprieiev b7bcf0cc37 Merge pull request #1604 from efiop/master
updater: don't use io.open when working with json
2019-02-12 01:49:49 +02:00
Ruslan Kuprieiev 91ed40fbfa updater: don't use io.open when working with json
Otherwise we get this on python2:
```
======================================================================
ERROR: test_fetch (tests.unit.updater.TestUpdater)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/efiop/.pyenv/versions/2.7.15/envs/2.7.15-dvc/local/lib/
python2.7/site-packages/mock/mock.py", line 1305, in patched
    return func(*args, **keywargs)
  File "/home/efiop/git/dvc/tests/unit/updater.py", line 34, in test_fetch
    self.dvc.updater.fetch(detach=False)
  File "/home/efiop/git/dvc/dvc/updater.py", line 77, in fetch
    self._with_lock(self._get_latest_version, "fetching")
  File "/home/efiop/git/dvc/dvc/updater.py", line 39, in _with_lock
    func()
  File "/home/efiop/git/dvc/dvc/updater.py", line 91, in _get_latest_version
    json.dump(convert_to_unicode(info), fobj)
  File "/usr/lib/python2.7/json/__init__.py", line 190, in dump
    fp.write(chunk)
TypeError: write() argument 1 must be unicode, not str
-------------------- >> begin captured logging << --------------------
```

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-12 01:40:07 +02:00
Ruslan Kuprieiev c0674dc134 dvc: refactor requirements.txt
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-10 22:42:50 +02:00
Ruslan Kuprieiev 51c9357cd2 dvc: bump to 0.26.0 2019-02-10 21:51:16 +02:00
Ruslan Kuprieiev 9bfff1f957 Merge pull request #1600 from iterative/cached-metrics
Cached metrics
2019-02-10 21:33:04 +02:00
Ivan Shcheklein 41befea210 address PR review comments 2019-02-10 11:06:23 -08:00
Ruslan Kuprieiev fbd5393a2d appveyor: add chocolatey cache 2019-02-10 07:50:51 +02:00
Ivan Shcheklein 8817864e7a fix one more test on windows 2019-02-09 20:04:35 -08:00
Ivan Shcheklein bd28b711f9 minor: remove debug print 2019-02-09 19:58:10 -08:00
Ivan Shcheklein 5563f13273 fix metrics tests on windows 2019-02-09 19:57:41 -08:00
Ivan Shcheklein 36c4a3a5af add metrics tests 2019-02-09 19:00:40 -08:00
Ivan Shcheklein dc1242a107 fix error messages in metrics show 2019-02-09 18:57:15 -08:00
Ivan Shcheklein 17c6858ad3 minor: better variable names in stage class 2019-02-09 18:56:17 -08:00
Ivan Shcheklein dab347f424 fix few checks and messages in dvc run 2019-02-09 18:54:54 -08:00
Ivan Shcheklein 3287c3f407 add ability run tests from specified file(s) 2019-02-09 18:52:46 -08:00
Ivan Shcheklein 0ed44cf151 fix formatting 2019-02-09 15:44:12 -08:00
Ivan Shcheklein 2419f94ef3 use python3 in hook 2019-02-09 15:00:16 -08:00
Ruslan Kuprieiev 1eee7e3226 travis: enable cron builds
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 23:52:15 +02:00
Ivan Shcheklein 16570bad03 add cached metrics, cleanup warnings 2019-02-09 13:48:18 -08:00
Ruslan Kuprieiev fd2f9119e3 Merge pull request #1597 from mroutis/fix-1484
remote: define command error on the base
2019-02-09 22:46:45 +02:00
Ruslan Kuprieiev cbf24cd279 dvc: bump to 0.25.4 2019-02-09 21:07:57 +02:00
Ruslan Kuprieiev 8b35c69a95 dvc: generate version even without gitpython 2019-02-09 21:07:17 +02:00
Mr. Outis ec77bfa1af remote: define command error on the base
fix: #1484
2019-02-09 12:03:17 -06:00
Ruslan Kuprieiev bac1393fc2 Merge pull request #1592 from mroutis/fix-1591
remote: notify when operation is not supported
2019-02-09 08:44:19 +02:00
Ruslan Kuprieiev cb8e24237f windows: gen_config: don't use compat open 2019-02-09 07:31:59 +02:00
Ruslan Kuprieiev 5b548d679c dvc: bump to 0.25.3 2019-02-09 07:04:03 +02:00
Ruslan Kuprieiev 005239ade7 py2: cast env keys to bytes
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 06:59:21 +02:00
Ruslan Kuprieiev b67546d648 dvc: bump to 0.25.2 2019-02-09 04:17:56 +02:00
Ruslan Kuprieiev 40c84fe4ed py2: fix environ issue
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 03:59:45 +02:00
Mr. Outis 0d51d10ccf remote: notify when operation is not supported
close #1591
2019-02-08 19:54:54 -06:00
Ruslan Kuprieiev 9210e66b0c dvc: bump to 0.25.1 2019-02-09 03:06:18 +02:00
Ruslan Kuprieiev d3bb4f85df travis: don't forget to init pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 02:12:17 +02:00
Ruslan Kuprieiev cbce38074e travis: fix typo
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 02:01:52 +02:00
Ruslan Kuprieiev 1bb101d250 travis: debug pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 01:40:58 +02:00
Ruslan Kuprieiev 1d272f156f config: handle run outside of dvc repo
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 01:38:49 +02:00
Ruslan Kuprieiev 05bd4d4dc4 travis: use pyenv shell
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 22:20:41 +02:00
Ruslan Kuprieiev e22da1e957 travis: fix typo
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 21:52:13 +02:00
Ruslan Kuprieiev 4f36ce35ad travis: activate pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 21:39:15 +02:00
Ruslan Kuprieiev 74509cf29f Merge pull request #1587 from mroutis/fix-unicode
py2: Add unicode support for open and os.walk
2019-02-08 10:32:19 +02:00
Ruslan Kuprieiev 96a6409660 Merge pull request #1542 from mroutis/update-status-quiet
status: update --quiet help message
2019-02-08 10:31:32 +02:00
Ruslan Kuprieiev 28061fb287 travis: don't define google creds file path
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 10:16:13 +02:00
Mr. Outis d6b5729105 status: update --quiet help message 2019-02-08 00:48:42 -06:00
Ruslan Kuprieiev 369c86bfde travis: fix env var definitions
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 08:39:17 +02:00
Mr. Outis 2be7232d1a py2: Add unicode support for open and os.walk 2019-02-07 23:33:30 -06:00
Ruslan Kuprieiev e29eeccf8a dvc: bump to 0.25.0 2019-02-08 07:10:53 +02:00
Ruslan Kuprieiev 95368b8f52 test: use "flaky" nose plugin for flaky tests
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 06:05:16 +02:00
Ruslan Kuprieiev 82b9d33674 test: ssh: create external output dir
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 04:19:44 +02:00
Ruslan Kuprieiev 9091ef2dc0 test: ssh: don't create dirs
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 03:51:21 +02:00
Ruslan Kuprieiev 515211a4dd travis: increase ulimit -u
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 02:44:25 +02:00
Ruslan Kuprieiev 53e40e32ed tests: use paramiko instead of ssh cmds
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 01:35:05 +02:00
Ruslan Kuprieiev 865f3a3f05 test: fix styling
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-07 08:47:12 +02:00
Ruslan Kuprieiev c95e6eb856 tests: fix tests on travis
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-07 08:43:43 +02:00
Ruslan Kuprieiev 92d4acc978 travis: mac: set maxproc to 2048 2019-02-07 06:31:30 +02:00
Ruslan Kuprieiev a95ea2075f travis: debug mac os 2019-02-07 05:27:24 +02:00
Ruslan Kuprieiev 9d663f5035 travis: mac: debug proc limits 2019-02-07 04:40:06 +02:00
Ruslan Kuprieiev d1bc9ebed5 Merge pull request #1583 from efiop/1581
add: don't try to save cached stages
2019-02-07 01:38:13 +02:00
Ruslan Kuprieiev 441fc3ad97 add: don't try to save cached stages
Cached stages are already saved, so there is no need to try to save them
once again.

Fixes #1581

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-06 14:26:21 +02:00
Ruslan Kuprieiev 94a8054d29 test: gs: refresh credentials
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-06 01:46:23 +02:00
Ruslan Kuprieiev 99ef6081c1 test: use creds when testing external gs deps/outs
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-06 00:13:19 +02:00
Thibault de Boissiere 2c67b59fc0 checkout: Suggest git checkout when dcv checkout fails (#1580) 2019-02-05 23:46:13 +02:00
Ruslan Kuprieiev f6535183a4 travis: gcloud: don't forget to authenticate
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-05 22:50:00 +02:00
Ruslan Kuprieiev f9ee8ed566 test: use deepcopy for TEST_CONFIG
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-05 04:08:41 +02:00
Ruslan Kuprieiev aaf71121b6 test: don't overwrite TEST_CONFIG
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-05 02:57:41 +02:00
Ruslan Kuprieiev cdcfcc8b6d travis: specify GOOGLE_APPLICATION_CREDENTIALS
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-05 02:21:33 +02:00
Ruslan Kuprieiev 2ef7d2ef49 Merge pull request #1578 from mroutis/add-flake8-docstrings
style: Add flake8-docstrings
2019-02-05 01:33:31 +02:00
Mehdi Hamoumi ffc94cc471 Add GCP credentials environment variable to config (#1567) 2019-02-05 01:33:04 +02:00
Mr. Outis 2c354cffe0 style: Add flake8-docstrings 2019-02-04 17:07:59 -06:00
Paweł Redzyński 31426c1548 data cloud: lowercase remote name to match config values (#1575) 2019-02-04 22:49:15 +02:00
Paweł Redzyński 3d07c0a039 import: add resume flag (#1540)
Resume import
2019-02-04 21:40:08 +02:00
Ruslan Kuprieiev a2fb7da23d test: set maximum RLIMIT_NPROC
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-04 03:09:21 +02:00
Ruslan Kuprieiev 4896f08ddf dvc: comply with both flake8 and black
flake8 provides some additional features like unused vars check, etc, so
it makes total sense to use it as well.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-04 00:10:36 +02:00
Ruslan Kuprieiev 1cc2e0edcb tests: don't forget to import GitCommandNotFound
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-03 23:56:43 +02:00
Yossi Farjoun 2cbe964af8 typo in error message (#1569) 2019-02-03 21:09:41 +02:00
Ruslan Kuprieiev 611ab96fd1 tests: fix formatting
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-03 21:07:44 +02:00
Ruslan Kuprieiev eaca7dc80c travis: osx: handle EAGAIN error
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-03 15:52:21 +02:00
Thibault de Boissiere 70a16b2dec Implement -R option for dvc metrics show (#1543) 2019-02-02 05:18:06 +02:00
Ruslan Kuprieiev 413b938160 travis: osx: upgrade pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-01 14:55:31 +02:00
Ruslan Kuprieiev 91edbc65f2 dvc: format the rest of the files with black
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-01 14:49:53 +02:00
Ruslan Kuprieiev c5a887dc4e travis: list python versions supported by pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-01 11:59:23 +02:00
Ruslan Kuprieiev 514b424104 travis: use pyenv to install python on osx
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-01 10:32:58 +02:00
Ruslan Kuprieiev 09ddacd144 Merge pull request #1565 from iterative/code-of-checkout
add code of conduct
2019-02-01 04:51:43 +02:00
Ruslan Kuprieiev 9b72cf75c4 Merge pull request #1564 from efiop/master
checkout: handle EOFError when running in a git hook
2019-02-01 04:47:37 +02:00
Svetlana Grinchenko b05fd18f8e add code of conduct 2019-01-31 16:51:01 -08:00
Ruslan Kuprieiev ba091a6d34 checkout: handle EOFError when running in a git hook
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 22:56:48 +02:00
Ruslan Kuprieiev 1aab9e92b4 travis: enable hadoop and azure for non-PR builds
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 06:37:36 +02:00
Ruslan Kuprieiev 2a284e3fd4 Merge pull request #1559 from efiop/master
travis: test azure and hadoop only during cron jobs
2019-01-31 06:33:07 +02:00
Ruslan Kuprieiev 9fdb922ab7 travis: test azure and hadoop only during cron jobs
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 06:03:27 +02:00
Ruslan Kuprieiev 4335336e0a travis: fix python install
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 04:49:04 +02:00
Ruslan Kuprieiev 8f4d4f56aa travis: cache /opt/python
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 04:30:30 +02:00
Ruslan Kuprieiev 2030dfef81 travis: upgrade python on osx
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 04:09:43 +02:00
Ruslan Kuprieiev eff4e1fe6c dvc: don't format for python3.6
We need dvc to work on older python versions, so we can't use the new
and shiny syntax.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 03:21:22 +02:00
Ruslan Kuprieiev 7240a57253 dvc: re-format with black
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 03:03:36 +02:00
Ruslan Kuprieiev f78c5bdaa6 dvc: add black configuration and git hook
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 02:54:29 +02:00
Ruslan Kuprieiev 0c168b27b8 travis: fix typo
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 02:02:45 +02:00
Ruslan Kuprieiev 1a5a840aee travis: fix build on osx
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-31 01:57:04 +02:00
Ruslan Kuprieiev f9581f8d42 Merge pull request #1547 from efiop/master
dvc: use black instead of flake8
2019-01-30 06:45:18 +02:00
Ruslan Kuprieiev 4069e34130 dvc: use black instead of flake8
Fixes #1546

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-30 03:48:36 +02:00
Ruslan Kuprieiev b5c3a14edf Merge pull request #1544 from iterative/minor-fixes
Minor fixes
2019-01-29 12:24:57 +02:00
Ivan Shcheklein 2e66119465 boto version up to make aws dependencies compatible with test requirements 2019-01-28 20:57:36 -08:00
Ivan Shcheklein 5db382c9af minor: clarify the comment around local remote move operation 2019-01-28 20:56:53 -08:00
Ruslan Kuprieiev 4a52f5fc49 Merge pull request #1538 from mroutis/fix-1412
remote http: use Content-MD5 header if there is not ETag available
2019-01-29 03:46:25 +02:00
Mr. Outis 3009a25ae2 remote http: use Content-MD5 header if there is not ETag available
Fix #1412
2019-01-28 19:02:41 -06:00
Paweł Redzyński 423c212d3f Merge pull request #1541 from pared/1539
Lock test http server
2019-01-28 22:29:54 +01:00
pared d5fecc34bd test: httpd: add lock for server 2019-01-28 22:01:34 +01:00
Ruslan Kuprieiev 3d30a43534 Merge pull request #1537 from mroutis/close-1519
status: --quiet reflects changes on its exit code
2019-01-28 21:37:01 +02:00
Mr. Outis 4218f85853 status: --quiet reflects changes on its exit code
Fix #1519
2019-01-28 11:28:44 -06:00
Ruslan Kuprieiev 66668d9803 Merge pull request #1533 from mroutis/fix-1519
py2: add unicode literals
2019-01-28 19:10:14 +02:00
Mr. Outis 0d46921ec7 py2: add unicode literals
Fix #1512
2019-01-27 20:40:21 -06:00
Ruslan Kuprieiev 4e0d4b87bf project: add: remove redundant assignment 2019-01-28 01:27:37 +02:00
Ruslan Kuprieiev 9c73814805 dvc: bump to 0.24.3 2019-01-26 14:53:58 +02:00
Ruslan Kuprieiev 720d5d675f Merge pull request #1530 from efiop/master
remote: gs: don't forget to set projectname
2019-01-26 14:08:07 +02:00
Ruslan Kuprieiev 5dcc56a542 Merge pull request #1531 from efiop/1525
daemon: set PYTHONPATH
2019-01-26 13:53:18 +02:00
Ruslan Kuprieiev 313c975c1f Merge pull request #1532 from iterative/move-description-improvements
Move description improvements
2019-01-26 13:30:25 +02:00
Ivan Shcheklein b1a93ffc14 add a newline to keep the total length under 80 chars 2019-01-26 01:35:14 -08:00
Ivan Shcheklein 6e70db113d dvc move: improve help and description messages 2019-01-26 01:30:49 -08:00
Ruslan Kuprieiev e3304e14fa daemon: set PYTHONPATH
Fixes #1525

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-26 01:54:45 +02:00
Ruslan Kuprieiev a314209add remote: gs: don't forget to set projectname
Fixes #1529

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-26 00:30:08 +02:00
Ruslan Kuprieiev fdb103ed66 dvc: bump to 0.24.2 2019-01-24 18:14:57 +02:00
Ruslan Kuprieiev ce258b45fc remote: base: fix typo 2019-01-24 18:04:11 +02:00
Ruslan Kuprieiev 4cb1e747ef dvc: bump to 0.24.1 2019-01-24 17:36:49 +02:00
Ruslan Kuprieiev fa4e72935b Merge pull request #1523 from efiop/master
stage: don't exclude md5s for deps/outs
2019-01-24 17:35:15 +02:00
Ruslan Kuprieiev f5aa0da0ae stage: don't exclude md5s for deps/outs
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-24 17:02:07 +02:00
Ruslan Kuprieiev dc50872949 dvc: fix dynamic version check 2019-01-24 00:21:48 +02:00
Ruslan Kuprieiev 983120ba35 dvc: bump to 0.24.0 2019-01-23 23:21:29 +02:00
Ruslan Kuprieiev cca6598e6b Merge pull request #1514 from efiop/master
RFC: second iteration of complying with pylint
2019-01-23 12:27:18 +02:00
Ruslan Kuprieiev a108249db3 RFC: state: comply with pylint
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-23 03:35:46 +02:00
Ruslan Kuprieiev 225d2566dd RFC: prompt: comply with pylint
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-23 03:35:46 +02:00
Ruslan Kuprieiev 621230855b RFC: scm: comply with pylint
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-23 03:35:46 +02:00
Ruslan Kuprieiev 9b91f60f36 Merge pull request #1431 from mroutis/fix-1348
stage: Checkout cached output when possible
2019-01-23 03:31:47 +02:00
Ruslan Kuprieiev 5c89d7de92 Merge pull request #1460 from mroutis/fix-1285
project: Detect circular dependencies on graph
2019-01-23 03:03:08 +02:00
Mr. Outis 185600d344 project: Detect circular dependencies on graph
Fix #1285
2019-01-22 18:39:56 -06:00
Ruslan Kuprieiev 7ba4aeae42 Merge pull request #1521 from guysmoilov/master
Added DAGsHub to "Related Technologies"
2019-01-23 00:44:45 +02:00
Guy Smoilov 884c80df46 Added DAGsHub to "Related Technologies" 2019-01-22 15:32:26 +02:00
Ruslan Kuprieiev c9df567938 Merge pull request #1505 from pared/1486
move: handle directory as target argument
2019-01-22 00:19:15 +02:00
pared c120c8b663 move: handle directory as target argument 2019-01-21 21:37:52 +01:00
Ruslan Kuprieiev f94717f4f1 Merge pull request #1513 from pared/1464-2
-R options acts as boolean, minor daemon fix
2019-01-21 21:05:29 +02:00
pared 761a6c8f13 sync: convert -R option to boolean 2019-01-21 18:58:45 +01:00
Ruslan Kuprieiev 7400046240 Merge pull request #1517 from pared/1515
remove unnecessary staticmethod annotation
2019-01-19 14:28:58 +02:00
pared b718badeb5 remove unnecessary staticmethod annotation 2019-01-19 13:16:21 +01:00
Mr. Outis 823077aa2b stage: Checkout cached output when possible
Fix #1348
2019-01-19 00:05:25 -06:00
Mr. Outis e600e3a736 remote: Use checksum instead of MD5/ETag 2019-01-19 00:02:28 -06:00
Mr. Outis d768607733 💅 Fix some typos and aesthetic changes (#1508) 2019-01-18 23:47:27 -06:00
Ruslan Kuprieiev 4daaeaeb6b Merge pull request #1507 from efiop/master
RFC: first iteration to comply with pylint
2019-01-17 21:33:52 +02:00
Ruslan Kuprieiev 6fec8b16cd RFC: conform to pylint
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-16 14:12:19 +02:00
Paweł Redzyński 2bcdb0ba54 Merge pull request #1504 from pared/1503
move: move stage file with target file
2019-01-15 20:28:25 +01:00
pared d078e40d52 move: move stage file with target file 2019-01-15 19:14:24 +01:00
Ruslan Kuprieiev e177bd4172 Merge pull request #1502 from efiop/1488
state: don't forget to compute size of an individual file
2019-01-15 18:22:12 +02:00
Ruslan Kuprieiev 60e9ed4f0b state: account for directories when computing size
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-15 17:17:15 +02:00
Ruslan Kuprieiev ced57a4382 state: don't forget to compute size of an individual file
Fixes #1488

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-15 13:08:25 +02:00
Paweł Redzyński 52ffae81c4 imp: refactor import message (#1501)
* imp: refactor import message

* import: reword error message

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-15 11:48:02 +02:00
Paweł Redzyński dc23feb9ec Merge pull request #1498 from pared/1496
test: enlarge test base bar content
2019-01-14 20:42:21 +01:00
pared 62967759d3 test: change test base bar content 2019-01-14 19:10:39 +01:00
Paweł Redzyński 64a4883ba1 state: cast file size to string
Fixes #1485
2019-01-13 12:09:13 +02:00
Ruslan Kuprieiev b3df249c9f Merge pull request #1492 from mroutis/fix-move
move: Take into account suffix
2019-01-11 21:13:49 +02:00
Mr. Outis 0372e1d87d move: Take into account suffix
🔨 Refactor

Fix #1491
2019-01-11 12:57:42 -06:00
Ruslan Kuprieiev 471029cc6d Merge pull request #1479 from pared/1474
project: support recursive sync operations
2019-01-10 19:50:23 +02:00
pared 6f7e2095d3 project: support recursive sync operations 2019-01-10 18:02:23 +01:00
Ruslan Kuprieiev a15f68a20d Merge pull request #1482 from efiop/master
RFC: remote: unify changed() method
2019-01-10 01:18:22 +02:00
Ruslan Kuprieiev 313541d047 RFC: remote: unify changed() method
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-09 17:46:36 +02:00
Ruslan Kuprieiev ee40a9a48a Merge pull request #1480 from mroutis/fix-1478
run: Parse and quote correctly the command
2019-01-09 09:44:31 +02:00
Mr. Outis 7be42c8402 run: Parse and quote correctly the command
Fix: #1478
2019-01-09 00:01:55 -06:00
Paweł Redzyński bad2ef5f0a Merge pull request #1474 from efiop/1473
ssh: `user` and `port` configuration
2019-01-08 17:01:53 +01:00
Ruslan Kuprieiev 3053cf2b1f ssh: support specifying port in the url
Fixes #1473

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-08 10:12:43 +02:00
Ruslan Kuprieiev 5d381da5d9 ssh: change order of user configuration
Unfortunately this breaks backward compatibility, but we have to do it,
because current order is totally wrong. Configuring user with `dvc
remote modify myssh user` is an explicit action and should have higher
priority than implicit url. This is also important, since in the next
patch in this PR we will introduce support for `port` configuration,
that should also follow the same logic.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-08 10:12:43 +02:00
Ruslan Kuprieiev 3e8fa8014d Merge pull request #1470 from efiop/master
RFC: dependencies/outputs: unify classes
2019-01-07 20:05:22 +02:00
Ruslan Kuprieiev ce009c894f RFC: deps/outs: add more comments
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-07 18:40:45 +02:00
Ruslan Kuprieiev 68d965f4fb RFC: utils: create a separate module
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-07 18:20:56 +02:00
Ruslan Kuprieiev 7cd24e0c1f RFC: dependencies/outputs: unify classes
There is a lot of duplication in current Output/Dependency classes, that was
left "just in case" while developing different schemes. Since our code base is
now getting more and more stable and we now know what we need to add support
for any new dep/out type in the future, we can now safely unify most of the
code.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-05 23:17:55 +02:00
Paweł Redzyński 61e0ea9478 [WIP] 1445 warning on outdated stage (#1466)
* prepare test structure

* add warning on empty out info, test

* reformat line lengtha according to pep8

* change warning message
2019-01-05 14:03:13 +02:00
Ruslan Kuprieiev 8ce405a29c Merge pull request #1467 from efiop/1284
Support relative path for cache dir and local remotes
2019-01-04 21:27:11 +02:00
Ruslan Kuprieiev 3aefa7b60e Merge pull request #1469 from IlyaKisil/develop
readme: Fix typo in ubuntu/debian installation guide
2019-01-04 21:17:17 +02:00
Ruslan Kuprieiev 484f318c27 remote: local: support relative paths
Fixes #1421

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-04 21:14:03 +02:00
Ilya Kisil efb606da0b readme: Fix typo in ubuntu/debian installation guide 2019-01-04 18:53:04 +00:00
Ruslan Kuprieiev 294b259579 cache: support relative paths
Fixes #1284

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-04 16:12:39 +02:00
Ruslan Kuprieiev db6cd8e1bb dvc: introduce dvc cache dir command
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-04 09:02:37 +02:00
Ruslan Kuprieiev d1be895b61 cli: extract --global/--system/--local into a common parser
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-04 07:21:44 +02:00
Ruslan Kuprieiev 1f549d6659 readme: update travis links 2019-01-03 13:48:35 +02:00
Ruslan Kuprieiev 0c30723cf3 Merge pull request #1463 from efiop/1451
local: support remote:// syntax
2019-01-02 21:41:47 +02:00
Ruslan Kuprieiev 8ca0d299b5 deps: unify dumpd method
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-02 17:54:25 +02:00
Ruslan Kuprieiev 2bdb75df56 deps: use scheme property
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-02 17:43:24 +02:00
Ruslan Kuprieiev c5b8d098d8 local: support remote:// syntax
Fixes #1451

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-02 17:43:24 +02:00
Ruslan Kuprieiev d09e469210 deps: use self.url instead of self.path
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-01-02 15:05:38 +02:00
Ruslan Kuprieiev 17f7f46bf4 Merge pull request #1461 from mroutis/init-logging
init: Improve information logging
2018-12-29 00:11:47 +02:00
Mr. Outis 3ddc54403d init: Boxify message about analytics for more visibility 2018-12-28 13:59:28 -06:00
Mr. Outis 2d21c4a6e8 init: Do not show "now commit" message with --no-scm 2018-12-28 13:59:03 -06:00
Ruslan Kuprieiev 3dcfd44d11 Merge pull request #1459 from efiop/1458
azure: fix backward compatibility
2018-12-28 21:04:16 +02:00
Ruslan Kuprieiev d3de148fc4 azure: fix backward compatibility
Fixes #1458

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-28 19:58:25 +02:00
Ruslan Kuprieiev 51b888e8f2 codecov: enable comments back 2018-12-26 23:00:33 +02:00
Ruslan Kuprieiev d1d1a83816 Merge pull request #1457 from efiop/1456
logger: handle chained exceptions
2018-12-26 16:59:36 +02:00
Ruslan Kuprieiev feb6f48a5a logger: handle chained exceptions
Fixes #1456

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-26 03:38:34 +02:00
Ruslan Kuprieiev 80c5ff7b64 Merge pull request #1454 from mroutis/rfc-logger
logger [rfc]: Use a module instead of a class
2018-12-24 22:02:08 +02:00
Ruslan Kuprieiev ed11f6541c dvc: bump to 0.23.2
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-24 18:39:59 +02:00
Ruslan Kuprieiev f0cbfe6c72 build: fix deb and rpm packages
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-24 18:39:19 +02:00
Ruslan Kuprieiev 0cd680cccf build: don't use system gem 2018-12-24 08:56:23 +02:00
Ruslan Kuprieiev f714b738b8 dvc: bump to 0.23.1 2018-12-24 08:13:29 +02:00
Ruslan Kuprieiev 65d74ee43c build: use --after-{install,remove} scripts
Fixes #1455

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-24 08:11:29 +02:00
Ruslan Kuprieiev 12fde38407 scripts: fix typo in gem command 2018-12-24 05:13:46 +02:00
Mr. Outis 4c420d5f4d logger [rfc]: Use a module instead of a class
- Remove deprecation warnings
- Format error output and remove duplicated info

Close #1086
2018-12-23 21:06:39 -06:00
Ruslan Kuprieiev eb907cf8cc gem: don't use obsoleted arguments 2018-12-24 04:44:51 +02:00
Ruslan Kuprieiev 33257a3efd dvc: bump to 0.23.0 2018-12-24 03:52:00 +02:00
Ruslan Kuprieiev 9a09d95f1a Merge pull request #1452 from iterative/analytics-transparency-improvements
Analytics transparency improvements
2018-12-22 05:16:50 +02:00
Ruslan Kuprieiev 3840b8fa75 test: gc: skip if key file does not exist
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-22 04:49:20 +02:00
Ivan Shcheklein 16ec387a7e Merge branch 'master' into analytics-transparency-improvements 2018-12-21 18:14:12 -08:00
Ivan Shcheklein 8e73b97374 add an analytics notice on dvc init 2018-12-21 18:13:50 -08:00
Ruslan Kuprieiev 716f63cba8 Merge pull request #1440 from efiop/1430
remote: RFC
2018-12-21 19:05:59 +02:00
Ruslan Kuprieiev 4029116b85 remote: RFC: unify push/pull
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-21 18:18:33 +02:00
Ivan Shcheklein f3171e1d47 make updater and analytics URLs human readable 2018-12-20 17:09:53 -08:00
Ruslan Kuprieiev c8b39235bf remote: local: fix info msgs
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:16:14 +02:00
Ruslan Kuprieiev ef766fb4d3 remote: RFC: unify checkout()
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:09:26 +02:00
Ruslan Kuprieiev ecb80d98dd remote: RFC: unify cache_exists()
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:09:26 +02:00
Ruslan Kuprieiev 7758e53b35 remote: RFC: unify changed_cache
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:09:26 +02:00
Ruslan Kuprieiev 141de1443c remote: RFC: unify gc code
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:09:26 +02:00
Ruslan Kuprieiev 2a46247cb2 remote: RFC: unify checksum <-> path convertions
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:09:26 +02:00
Ruslan Kuprieiev 933d524ebe remote: RFC: use 'path' instead of 'key'
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:09:26 +02:00
Ruslan Kuprieiev b396251130 azure: remove unused code
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:09:26 +02:00
Ruslan Kuprieiev 7600ccf8be remote: introduce cache_exists() method
Making this method separate from exists() makes it easier to optimize
requests for large amounts of cache, since we can re-use the same prefix
for retrieving all the necessary information in a one single request.

Related #1430

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:09:26 +02:00
Ruslan Kuprieiev 4fa8618734 git: update .gitignore
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-20 22:09:26 +02:00
Ruslan Kuprieiev 865d265274 Merge pull request #1447 from efiop/1444
daemon: fix daemon when running in binary mode
2018-12-19 22:04:46 +02:00
Ruslan Kuprieiev 7ecb9e87ae test: fix "overwrite" test on mac 2018-12-19 15:56:33 +02:00
Ruslan Kuprieiev eb810e617e Merge pull request #1436 from mroutis/fix-1435
repro: Fix --all-pipelines option
2018-12-18 18:35:37 +02:00
Ruslan Kuprieiev e1f736eaad daemon: fix daemon when running in binary mode
When running from binary, we need to fixup environment variables
before launching new processes. We also need to detach from the parent
tty, because otherwise it causes problems for PyInstaller.

Plus, when binary is built as --onefile it still creates pop-up terminal
on windows, so we need to switch to --onedir.

Fixes #1446
Fixes #1444

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-18 18:13:55 +02:00
Ruslan Kuprieiev 4c753ff599 Merge pull request #1441 from efiop/1439
state: save file size
2018-12-17 13:12:53 +02:00
Ruslan Kuprieiev d5dd0fd804 test: don't use sleep
Since we now use file size to help detect file changes on filesystems
with bad mtime resolution, we no longer need these explicit sleep()
calls in our tests for mac os.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-17 12:42:35 +02:00
Ruslan Kuprieiev d1cb0a3450 state: save file size
This will help us detect file change more reliably even on filesystems
with bad mtime resolution.

Fixes #1439

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-17 12:42:35 +02:00
Ruslan Kuprieiev b7e473fc20 Merge pull request #1434 from mroutis/pipeline
RFC: Refactor pipeline definition
2018-12-17 12:30:33 +02:00
Mr. Outis e8a081030f repro: Fix --all-pipelines option
Fix #1435
2018-12-16 18:31:40 -06:00
Mr. Outis 4142f65531 Add tests for pipelines 2018-12-16 18:06:37 -06:00
Ruslan Kuprieiev 763e6f5436 remote: local: optimize dir cache collection
Related #1331

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-16 01:12:34 +02:00
Ruslan Kuprieiev 8fdb768bdd Merge pull request #1438 from mroutis/rfc-prompt
RFC: Prompt as functions instead of a class
2018-12-15 19:51:14 +02:00
Ruslan Kuprieiev f6350cc2c6 Merge pull request #1437 from efiop/master
dvc: use local scheme as default
2018-12-14 19:29:17 +02:00
Mr. Outis 948021f5b0 rfc: Prompt as functions instead of a class 2018-12-14 10:47:18 -06:00
Mr. Outis df044c60ac rfc: Move welcome message to a private function 2018-12-14 10:45:58 -06:00
Ruslan Kuprieiev 53e4e1c0a9 dvc: use local scheme as default
There is no way to detect if some path is a valid path in a general
case, since every filesystem has its own requirements and limitations.
Instead, just default to a local path and let fs handle the rest.

Fixes #1433

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-14 14:47:37 +02:00
Mr. Outis cb142f9f0e project: Refactor pipeline
What's a pipeline if not the weekly connected component subgraphs of a
directed acyclic graph?
2018-12-13 13:37:45 -06:00
Ruslan Kuprieiev 286373f9c7 test: fix whitespace 2018-12-13 08:40:24 +02:00
Ruslan Kuprieiev f5af2e1aec tests: fix test on mac os 2018-12-13 07:09:46 +02:00
Ruslan Kuprieiev dfecaff7a6 Merge pull request #1432 from efiop/master
Misc fixes
2018-12-12 20:13:36 +02:00
Ruslan Kuprieiev eaa2d559db azure: support prefix
Fixes #1336

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-12 19:54:41 +02:00
Ruslan Kuprieiev ed3030707f cli: add dvc remote default command
Fixes #1357

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-12 19:21:34 +02:00
Ruslan Kuprieiev 7c2eb02209 repro: introduce --ignore-build-cache
Fixes #1342

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-12 19:21:34 +02:00
Ruslan Kuprieiev 147d87b78a Merge pull request #1422 from mroutis/fix-1255
run: Prevent argument duplication with deps/outs
2018-12-12 08:37:38 +02:00
Ruslan Kuprieiev ee8cf4595b Merge pull request #1428 from mroutis/fix-1394
remote s3: Add option to disable the use of SSL
2018-12-12 08:13:27 +02:00
Mr. Outis 21f19b1296 fix logger issue 2018-12-11 15:06:09 -06:00
Mr. Outis a1ecbdd001 Merge branch 'master' into fix-1255 2018-12-11 14:56:05 -06:00
Mr. Outis 9971f7a624 run: Prevent argument duplication with deps/outs
Fix #1255
Fix #1168
Close #1259

RFC:
- Add / fix docstrings and privatize methods
- Add `__repr__` method for core objects
- Change the name of methods to make them more readable
2018-12-11 14:54:45 -06:00
Mr. Outis d1009a7489 remote s3: Add option to disable the use of SSL 2018-12-11 14:24:43 -06:00
Ruslan Kuprieiev b408ee1445 Merge pull request #1427 from efiop/master
ssh: mention private key passphrase in the prompt
2018-12-11 14:39:24 +02:00
Ruslan Kuprieiev 22724f0500 ssh: mention private key passphrase in the prompt
Fixes #1425
2018-12-11 14:11:21 +02:00
Ruslan Kuprieiev 91a5701169 Merge pull request #1426 from efiop/master
logger: optimize calls
2018-12-11 14:07:58 +02:00
Ruslan Kuprieiev e02a33f934 logger: shortcut logger in quiet and normal modes
In scenarios with millions of files, late shorcutting of
debug msgs results in massive overhead. In order to avoid
that, just shortcut logger messages ASAP if verbose mode
is not enabled.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-11 12:07:28 +02:00
Ruslan Kuprieiev cf8ab29129 local: don't use relpath in debug msgs
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-11 05:24:59 +02:00
Ruslan Kuprieiev b8e5c51424 cli: fix run description 2018-12-10 05:58:35 +02:00
Mr. Outis 19007ff081 scripts: Add file completion to dvc add command (#1423) 2018-12-07 22:39:24 -06:00
Ruslan Kuprieiev 8574f37217 Merge pull request #1418 from efiop/master
test: enable flake8 checks for tests
2018-12-06 07:07:19 +02:00
Ruslan Kuprieiev a67988a80e test: enable flake8 checks for tests
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2018-12-06 06:40:55 +02:00
Ruslan Kuprieiev ea2caf9dfd cli: update description for dvc status --with-deps
Fixes #1408
2018-12-06 05:34:13 +02:00
Ruslan Kuprieiev 778bb984b0 cli: don't forget colon and space
Fixes #1417
2018-12-06 05:20:02 +02:00
219 changed files with 16446 additions and 8529 deletions
+6 -2
View File
@@ -3,6 +3,10 @@ branches:
- master
- devel
cache:
- C:\ProgramData\chocolatey\bin -> appveyor.yml
- C:\ProgramData\chocolatey\lib -> appveyor.yml
platform: x86
environment:
@@ -10,7 +14,6 @@ environment:
secure: veTfhOrd4wUpG7NlRsvS3tufuaFfST8i7SZa2HVH6Vc=
aws_secret_access_key:
secure: nCQcN/Mzce0erwcNOU3yAuWgF6ZuTvPBEqTF3XFp1poGcDbSIg+s67eVxsxWCXQj
GOOGLE_APPLICATION_CREDENTIALS: gcp-creds.json
GCP_CREDS:
secure: 96fJ3r2i2GohbXHwnSs5N4EplQ7q8YmLpPWM0AC+f4s=
CODECOV_TOKEN:
@@ -66,12 +69,13 @@ before_test:
- aws configure set aws_secret_access_key "%aws_secret_access_key%"
- aws configure set region us-east-2
- openssl enc -d -aes-256-cbc -md md5 -k "%GCP_CREDS%" -in scripts\ci\gcp-creds.json.enc -out scripts\ci\gcp-creds.json & exit 0
- pip install -r test-requirements.txt
- pip install -r tests/requirements.txt
test_script:
- python -mtests
after_test:
- if "%APPVEYOR_PULL_REQUEST_NUMBER%"=="" pip install codecov
- if "%APPVEYOR_PULL_REQUEST_NUMBER%"=="" codecov
- if "%APPVEYOR_REPO_TAG%"=="true" python -m dvc pull
- if "%APPVEYOR_REPO_TAG%"=="true" .\scripts\build_windows.cmd
+1 -1
View File
@@ -1 +1 @@
comment: false
+8 -10
View File
@@ -1,10 +1,8 @@
link.state
link.state.lock
lock
config.local
updater
cache
state
state.lock
state-journal
state-wal
/state
/lock
/config.local
/updater
/updater.lock
/state-journal
/state-wal
/cache
+7
View File
@@ -0,0 +1,7 @@
[flake8]
ignore =
E203, # Whitespace before ':'
E266, # Too many leading '#' for block comment
W503, # Line break occurred before a binary operator
max-line-length = 79
select = B,C,E,F,W,T4,B9
+4
View File
@@ -3,6 +3,7 @@ neatlynx/__pycache__
.idea
cache
*.pyc
.env/
cache
.dvc.conf.lock
@@ -26,3 +27,6 @@ innosetup/config.ini
dvc/version.py
*.swp
pip-wheel-metadata/
.vscode/
+11
View File
@@ -0,0 +1,11 @@
repos:
- repo: https://github.com/ambv/black
rev: 19.3b0
hooks:
- id: black
language_version: python3
- repo: https://gitlab.com/pycqa/flake8
rev: master
hooks:
- id: flake8
language_version: python3
+568
View File
@@ -0,0 +1,568 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-whitelist=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
useless-object-inheritance
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit
[LOGGING]
# Format style used to check logging format string. `old` means using %
# formatting, while `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package..
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style.
#class-attribute-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_,
s3,
gs,
x, y,
x0, y0,
x1, y1,
dx, dy
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style.
#variable-rgx=
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
[DESIGN]
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement.
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=cls
[IMPORTS]
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=optparse,tkinter.tix
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled).
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled).
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception".
overgeneral-exceptions=Exception
+51 -51
View File
@@ -1,13 +1,16 @@
language: python
cache: pip
sudo: required
dist: trusty
group: deprecated-2017Q4
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
cache:
- pip
- directories:
- /opt/python
- $HOME/.pyenv
sudo: true
dist: xenial
python: "3.7"
env:
global:
- PYENV_ROOT="$HOME/.pyenv"
- PATH="$PYENV_ROOT/bin:$PATH"
# Travis doesn't support testing python on Mac yet, so we need
# to workaround it by installing it directly with brew.
#
@@ -16,10 +19,22 @@ 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
- name: "2.7"
env:
- PYTHON_VER=2.7.13
- name: "3.4"
env:
- PYTHON_VER=3.4.6
- name: "3.5"
env:
- PYTHON_VER=3.5.3
- name: "3.6"
env:
- PYTHON_VER=3.6.2
- name: "3.7"
env:
# NOTE: using default python
- PYTHON_VER=
- if: type = cron OR tag is present
os: osx
osx_image: xcode7.3
@@ -27,10 +42,9 @@ matrix:
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"
- PYTHON_CONFIGURE_OPTS="--enable-framework"
- if: type = cron OR tag is present
os: osx
osx_image: xcode7.3
@@ -38,8 +52,7 @@ matrix:
rvm: 2.4.0
env:
- PYTHON_VER=3.4.3
- PYENV_ROOT="$HOME/.pyenv"
- PATH="$PYENV_ROOT/bin:$PATH"
- PYTHON_CONFIGURE_OPTS="--enable-framework"
- if: type = cron OR tag is present
os: osx
osx_image: xcode7.3
@@ -47,48 +60,35 @@ matrix:
rvm: 2.4.0
env:
- PYTHON_VER=3.5.0
- PYENV_ROOT="$HOME/.pyenv"
- PATH="$PYENV_ROOT/bin:$PATH"
- PYTHON_CONFIGURE_OPTS="--enable-framework"
- if: type = cron OR tag is present
os: osx
osx_image: xcode10.1
language: ruby
rvm: 2.4.0
env:
- PYTHON_VER=2.7.10
- CPPFLAGS="-I/usr/local/opt/openssl/include $CPPFLAGS"
- LDFLAGS="-L/usr/local/opt/openssl/lib $LDFLAGS"
- PYTHON_CONFIGURE_OPTS="--enable-framework"
- if: type = cron OR tag is present
os: osx
osx_image: xcode10.1
language: ruby
rvm: 2.4.0
env:
- PYTHON_VER=3.5.0
- PYTHON_CONFIGURE_OPTS="--enable-framework"
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
- which pip
- source env.sh
- python --version
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then
bash ./scripts/ci/install_hadoop.sh;
source ~/.bashrc;
hadoop fs -help;
pushd ~;
sudo mkdir -p /home/hadoop/hadoopinfra/hdfs/namenode/;
sudo chmod 777 /home/hadoop/hadoopinfra/hdfs/namenode/;
sudo chmod 777 -R /usr/local/hadoop;
hdfs namenode -format;
start-dfs.sh;
jps;
hadoop fs -ls /;
hadoop fs -ls hdfs://127.0.0.1/;
hadoop fs -mkdir hdfs://127.0.0.1/ololo;
popd;
fi
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:
- bash ./scripts/ci/script.sh
deploy:
condition: "[[ $(python -c 'import sys; print(sys.version_info[0])') == '2' ]]"
condition: "[[ $(./scripts/ci/deploy_condition.sh) == 'true' ]]"
provider: releases
api_key:
secure: nyo17gF4Xl6DFL/KVkMewC805Cv+eK99L7SiYCU+WUAYDIzzFL3YJm7fm8bznPeor7DmpO7uSLSveMnJETBtdurW6JWhCTUss4yD66yFzpk6Z5cvHf/1UA5pdMHkkMM54fRaTRt6L2Yal7RPwGEQTBECLXW1Xmf78Y6+0l8lTvXQ9jXOQSRO/63VDKI5Zww2+XytVicUFlr0hLMGKUuaCjjXcQlAPzBgQz3/5Iz6EtzeTf2o+gTPjocbRbSFPikfiBXUn8XlJ8jBj7i114XJRJffw59KxYGDzxp+enfLBULGNoW0Np2hZqkKkPUIdB08C8XVbRXlFYX8cEYBczjqhv8A05p5F5CnltnqQ0KazcoUP2awPVqn68dWNRF+kqrnMBus8euYKIZ4dy/6bVEKJRa84ESJXZVDcwXTAXRqyovSxOlXqg9ZgAImMKBfKn/fpDX2xIvQPaXcnJxKbmcePfdbC1d0HYJ5jkbNaxKE/T15VvhzIho7mJ2WAv1r8QouQUuzqtpzIGbRn/KXHRUzCtyP0k4+eMKMLFOPzg848WsDarP3v8VYiSy4nFY7ZP7DRd3KttvW5bgVbSeO4jPmTgS6TnnpRwual9BJXSG7bjbfpIMii3bNZ7jWL4zpKT9Jfm8w9RGKLcX9HqkZ3+y60+LtYdhEfBvqsIlWQoIJBXg=
+77
View File
@@ -0,0 +1,77 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at info@dvc.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+1
View File
@@ -0,0 +1 @@
### See our contribution guide at [dvc.org](https://dvc.org/doc/user-guide/contributing#contributing).
+6 -4
View File
@@ -9,8 +9,8 @@
• `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
.. image:: https://travis-ci.com/iterative/dvc.svg?branch=master
:target: https://travis-ci.com/iterative/dvc
:alt: Travis
.. image:: https://ci.appveyor.com/api/projects/status/github/iterative/dvc?branch=master&svg=true
@@ -127,7 +127,7 @@ Ubuntu / Debian (apt)
^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
sudo wget https://dvc.org/deb/dvc.list -O etc/apt/sources.list.d/dvc.list
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
@@ -164,6 +164,8 @@ Related technologies
#. `Workflow Management Systems <https://en.wikipedia.org/wiki/Workflow_management_system>`_. DVC is a workflow
management system designed specifically to manage machine learning experiments. DVC is built on top of Git.
#. `DAGsHub <https://dagshub.com/>`_ Is a Github equivalent for DVC - pushing your Git+DVC based repo to DAGsHub will give you a high level dashboard of your project, including DVC pipeline and metrics visualizations, as well as links to DVC managed files if they are in cloud storage.
Contributing
============
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/>`_ for more
@@ -204,7 +206,7 @@ details.
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.
Want to stay up to date? Want to help improve DVC by participating in our occasional polls? Subscribe to our `mailing list <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_. No spam, really low traffic.
Copyright
=========
+63 -24
View File
@@ -3,47 +3,71 @@ DVC
----
Make your data science projects reproducible and shareable.
"""
from __future__ import unicode_literals
import os
import warnings
VERSION_BASE = '0.22.0'
VERSION_BASE = "0.33.0"
__version__ = VERSION_BASE
PACKAGEPATH = os.path.abspath(os.path.dirname(__file__))
HOMEPATH = os.path.dirname(PACKAGEPATH)
VERSIONPATH = os.path.join(PACKAGEPATH, 'version.py')
VERSIONPATH = os.path.join(PACKAGEPATH, "version.py")
if os.path.exists(os.path.join(HOMEPATH, 'setup.py')):
def _update_version_file():
"""Dynamically update version file."""
try:
from git import Repo
from git.exc import InvalidGitRepositoryError
except ImportError:
return __version__
try:
repo = Repo(HOMEPATH)
except InvalidGitRepositoryError:
return __version__
sha = repo.head.object.hexsha
short_sha = repo.git.rev_parse(sha, short=6)
dirty = ".mod" if repo.is_dirty() else ""
ver = "{}+{}{}".format(__version__, short_sha, dirty)
# Write a helper file, that will be installed with the package
# and will provide a true version of the installed dvc
with open(VERSIONPATH, "w+") as fobj:
fobj.write("# AUTOGENERATED by dvc/__init__.py\n")
fobj.write('version = "{}"\n'.format(ver))
return ver
def _remove_version_file():
"""Remove version.py so that it doesn't get into the release."""
if os.path.exists(VERSIONPATH):
os.unlink(VERSIONPATH)
if os.path.exists(os.path.join(HOMEPATH, "setup.py")):
# dvc is run directly from source without installation or
# __version__ is called from setup.py
if os.getenv('APPVEYOR_REPO_TAG', '').lower() != 'true' \
and os.getenv('TRAVIS_TAG', '') == '':
# Dynamically update version
try:
import git
repo = git.Repo(HOMEPATH)
sha = repo.head.object.hexsha
short_sha = repo.git.rev_parse(sha, short=6)
dirty = '.mod' if repo.is_dirty() else ''
__version__ = '{}+{}{}'.format(__version__, short_sha, dirty)
# Write a helper file, that will be installed with the package
# and will provide a true version of the installed dvc
with open(VERSIONPATH, 'w+') as fd:
fd.write('# AUTOGENERATED by dvc/__init__.py\n')
fd.write('version = "{}"\n'.format(__version__))
except Exception: # pragma: no cover
pass
if (
os.getenv("APPVEYOR_REPO_TAG", "").lower() != "true"
and os.getenv("TRAVIS_TAG", "") == ""
and os.getenv("DVC_TEST", "").lower() != "true"
):
__version__ = _update_version_file()
else: # pragma: no cover
# Remove version.py so that it doesn't get into the release
if os.path.exists(VERSIONPATH):
os.unlink(VERSIONPATH)
_remove_version_file()
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:
from dvc.version import version
__version__ = version
except Exception:
pass
@@ -57,3 +81,18 @@ VERSION = __version__
# 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")
# Ignore paramiko's warning: https://github.com/paramiko/paramiko/issues/1386.
# This only affects paramiko 2.4.2 and should be fixed in the next version.
# Cryptography developers decided that it is a brilliant idea not to inherit
# from DeprecationWarning [1] because it is invisible by default, and decided
# to spam everyone instead. So it is their fault and not paramiko's.
#
# [1] https://github.com/pyca/cryptography/blob/2.6.1/src/cryptography/
# utils.py#L14
try:
from cryptography.utils import CryptographyDeprecationWarning
warnings.simplefilter("ignore", CryptographyDeprecationWarning)
except ImportError:
pass
+5 -1
View File
@@ -1,6 +1,10 @@
"""Main entry point for dvc command line tool."""
from __future__ import unicode_literals
import sys
from dvc.main import main
if __name__ == '__main__':
if __name__ == "__main__":
main(sys.argv[1:])
+109 -61
View File
@@ -1,66 +1,86 @@
"""Collect and send usage analytics"""
from __future__ import unicode_literals
from dvc.utils.compat import str
import os
import json
import errno
import dvc.logger as logger
from dvc import VERSION
from dvc.logger import Logger
class Analytics(object):
URL = 'https://dddc07bh9f.execute-api.us-east-2.amazonaws.com/production'
"""Class for collecting and sending usage analytics.
Args:
info (dict): optional existing analytics report.
"""
URL = "https://analytics.dvc.org"
TIMEOUT_POST = 5
USER_ID_FILE = 'user_id'
USER_ID_FILE = "user_id"
PARAM_DVC_VERSION = 'dvc_version'
PARAM_USER_ID = 'user_id'
PARAM_SYSTEM_INFO = 'system_info'
PARAM_DVC_VERSION = "dvc_version"
PARAM_USER_ID = "user_id"
PARAM_SYSTEM_INFO = "system_info"
PARAM_OS = 'os'
PARAM_OS = "os"
PARAM_WINDOWS_VERSION_MAJOR = 'windows_version_major'
PARAM_WINDOWS_VERSION_MINOR = 'windows_version_minor'
PARAM_WINDOWS_VERSION_BUILD = 'windows_version_build'
PARAM_WINDOWS_VERSION_SERVICE_PACK = 'windows_version_service_pack'
PARAM_WINDOWS_VERSION_MAJOR = "windows_version_major"
PARAM_WINDOWS_VERSION_MINOR = "windows_version_minor"
PARAM_WINDOWS_VERSION_BUILD = "windows_version_build"
PARAM_WINDOWS_VERSION_SERVICE_PACK = "windows_version_service_pack"
PARAM_MAC_VERSION = 'mac_version'
PARAM_MAC_VERSION = "mac_version"
PARAM_LINUX_DISTRO = 'linux_distro'
PARAM_LINUX_DISTRO_VERSION = 'linux_distro_version'
PARAM_LINUX_DISTRO_LIKE = 'linux_distro_like'
PARAM_LINUX_DISTRO = "linux_distro"
PARAM_LINUX_DISTRO_VERSION = "linux_distro_version"
PARAM_LINUX_DISTRO_LIKE = "linux_distro_like"
PARAM_SCM_CLASS = 'scm_class'
PARAM_IS_BINARY = 'is_binary'
PARAM_CMD_CLASS = 'cmd_class'
PARAM_CMD_RETURN_CODE = 'cmd_return_code'
PARAM_SCM_CLASS = "scm_class"
PARAM_IS_BINARY = "is_binary"
PARAM_CMD_CLASS = "cmd_class"
PARAM_CMD_RETURN_CODE = "cmd_return_code"
def __init__(self, info={}):
def __init__(self, info=None):
from dvc.config import Config
from dvc.lock import Lock
if info is None:
info = {}
self.info = info
cdir = Config.get_global_config_dir()
try:
os.makedirs(cdir)
except OSError as e:
if e.errno != errno.EEXIST:
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
self.user_id_file = os.path.join(cdir, self.USER_ID_FILE)
self.user_id_file_lock = Lock(cdir, self.USER_ID_FILE + '.lock')
self.user_id_file_lock = Lock(cdir, self.USER_ID_FILE + ".lock")
@staticmethod
def load(path):
with open(path, 'r') as fobj:
a = Analytics(info=json.load(fobj))
"""Loads analytics report from json file specified by path.
Args:
path (str): path to json file with analytics report.
"""
with open(path, "r") as fobj:
analytics = Analytics(info=json.load(fobj))
os.unlink(path)
return a
return analytics
def _write_user_id(self):
import uuid
with open(self.user_id_file, 'w+') as fobj:
with open(self.user_id_file, "w+") as fobj:
user_id = str(uuid.uuid4())
info = {self.PARAM_USER_ID: user_id}
json.dump(info, fobj)
@@ -70,12 +90,12 @@ class Analytics(object):
if not os.path.exists(self.user_id_file):
return None
with open(self.user_id_file, 'r') as fobj:
with open(self.user_id_file, "r") as fobj:
try:
info = json.load(fobj)
return info[self.PARAM_USER_ID]
except json.JSONDecodeError as exc:
Logger.debug("Failed to load user_id: {}".format(exc))
logger.debug("Failed to load user_id: {}".format(exc))
return None
def _get_user_id(self):
@@ -89,13 +109,14 @@ class Analytics(object):
return user_id
except LockError:
msg = "Failed to acquire '{}'"
Logger.debug(msg.format(self.user_id_file_lock.lock_file))
logger.debug(msg.format(self.user_id_file_lock.lock_file))
def _collect_windows(self):
import sys
version = sys.getwindowsversion()
version = sys.getwindowsversion() # pylint: disable=no-member
info = {}
info[self.PARAM_OS] = 'windows'
info[self.PARAM_OS] = "windows"
info[self.PARAM_WINDOWS_VERSION_MAJOR] = version.major
info[self.PARAM_WINDOWS_VERSION_MINOR] = version.minor
info[self.PARAM_WINDOWS_VERSION_BUILD] = version.build
@@ -104,15 +125,17 @@ class Analytics(object):
def _collect_darwin(self):
import platform
info = {}
info[self.PARAM_OS] = 'mac'
info[self.PARAM_OS] = "mac"
info[self.PARAM_MAC_VERSION] = platform.mac_ver()[0]
return info
def _collect_linux(self):
import distro
info = {}
info[self.PARAM_OS] = 'linux'
info[self.PARAM_OS] = "linux"
info[self.PARAM_LINUX_DISTRO] = distro.id()
info[self.PARAM_LINUX_DISTRO_VERSION] = distro.version()
info[self.PARAM_LINUX_DISTRO_LIKE] = distro.like()
@@ -120,21 +143,26 @@ class Analytics(object):
def _collect_system_info(self):
import platform
system = platform.system()
if system == 'Windows':
if system == "Windows":
return self._collect_windows()
elif system == 'Darwin':
if system == "Darwin":
return self._collect_darwin()
elif system == 'Linux':
if system == "Linux":
return self._collect_linux()
else:
raise NotImplementedError
raise NotImplementedError
def collect(self):
"""Collect analytics report."""
from dvc.scm import SCM
from dvc.utils import is_binary
from dvc.project import Project
from dvc.exceptions import NotDvcProjectError
from dvc.repo import Repo
from dvc.exceptions import NotDvcRepoError
self.info[self.PARAM_DVC_VERSION] = VERSION
self.info[self.PARAM_IS_BINARY] = is_binary()
@@ -143,12 +171,13 @@ class Analytics(object):
self.info[self.PARAM_SYSTEM_INFO] = self._collect_system_info()
try:
scm = SCM(root_dir=Project._find_root())
scm = SCM(root_dir=Repo.find_root())
self.info[self.PARAM_SCM_CLASS] = type(scm).__name__
except NotDvcProjectError:
except NotDvcRepoError:
pass
def collect_cmd(self, args, ret):
"""Collect analytics info from a CLI command."""
from dvc.command.daemon import CmdDaemonAnalytics
assert isinstance(ret, int) or ret is None
@@ -156,60 +185,79 @@ class Analytics(object):
if ret is not None:
self.info[self.PARAM_CMD_RETURN_CODE] = ret
if args is not None and hasattr(args, 'func'):
if args is not None and hasattr(args, "func"):
assert args.func != CmdDaemonAnalytics
self.info[self.PARAM_CMD_CLASS] = args.func.__name__
def dump(self):
"""Save analytics report to a temporary file.
Returns:
str: path to the temporary file that contains the analytics report.
"""
import tempfile
with tempfile.NamedTemporaryFile(delete=False, mode='w') as fobj:
with tempfile.NamedTemporaryFile(delete=False, mode="w") as fobj:
json.dump(self.info, fobj)
return fobj.name
@staticmethod
def _is_enabled_config(config):
from dvc.config import Config
core = config.config.get(Config.SECTION_CORE, {})
return core.get(Config.SECTION_CORE_ANALYTICS, True)
@staticmethod
def _is_enabled(cmd=None):
from dvc.config import Config
from dvc.project import Project
from dvc.exceptions import NotDvcProjectError
from dvc.repo import Repo
from dvc.exceptions import NotDvcRepoError
from dvc.command.daemon import CmdDaemonBase
if os.getenv('DVC_TEST'):
if os.getenv("DVC_TEST"):
return False
if isinstance(cmd, CmdDaemonBase):
return False
if cmd is None or not hasattr(cmd, 'config'):
if cmd is None or not hasattr(cmd, "config"):
try:
dvc_dir = Project._find_dvc_dir()
dvc_dir = Repo.find_dvc_dir()
config = Config(dvc_dir)
assert config is not None
except NotDvcProjectError:
except NotDvcRepoError:
config = Config(validate=False)
assert config is not None
else:
config = cmd.config
assert config is not None
core = config._config.get(Config.SECTION_CORE, {})
enabled = core.get(Config.SECTION_CORE_ANALYTICS, True)
Logger.debug("Analytics is {}abled."
.format('en' if enabled else 'dis'))
enabled = Analytics._is_enabled_config(config)
logger.debug(
"Analytics is {}.".format("enabled" if enabled else "disabled")
)
return enabled
@staticmethod
def send_cmd(cmd, args, ret):
from dvc.daemon import Daemon
"""Collect and send analytics for CLI command.
Args:
args (list): parsed args for the CLI command.
ret (int): return value of the CLI command.
"""
from dvc.daemon import daemon
if not Analytics._is_enabled(cmd):
return
a = Analytics()
a.collect_cmd(args, ret)
Daemon()(['analytics', a.dump()])
analytics = Analytics()
analytics.collect_cmd(args, ret)
daemon(["analytics", analytics.dump()])
def send(self):
"""Collect and send analytics."""
import requests
if not self._is_enabled():
@@ -217,9 +265,9 @@ class Analytics(object):
self.collect()
Logger.debug("Sending analytics: {}".format(self.info))
logger.debug("Sending analytics: {}".format(self.info))
try:
requests.post(self.URL, json=self.info, timeout=self.TIMEOUT_POST)
except requests.exceptions.RequestException as exc:
Logger.debug("Failed to send analytics: {}".format(str(exc)))
logger.debug("Failed to send analytics: {}".format(str(exc)))
+59 -19
View File
@@ -1,44 +1,84 @@
"""Manages cache of a dvc repo."""
from __future__ import unicode_literals
import os
from dvc.config import Config
class Cache(object):
CACHE_DIR = 'cache'
"""Class that manages cache locations of a dvc repo.
def __init__(self, project):
Args:
repo (dvc.repo.Repo): repo instance that this cache belongs to.
"""
CACHE_DIR = "cache"
def __init__(self, repo):
from dvc.remote import Remote
self.project = project
self.repo = repo
config = project.config._config[Config.SECTION_CACHE]
config = repo.config.config[Config.SECTION_CACHE]
local = config.get(Config.SECTION_CACHE_LOCAL)
if local:
name = Config.SECTION_REMOTE_FMT.format(local)
sect = project.config._config[name]
sect = repo.config.config[name]
else:
cache_dir = config.get(Config.SECTION_CACHE_DIR, self.CACHE_DIR)
default_cache_dir = os.path.join(repo.dvc_dir, self.CACHE_DIR)
cache_dir = config.get(Config.SECTION_CACHE_DIR, default_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.PRIVATE_CWD: config.get(
Config.PRIVATE_CWD, repo.dvc_dir
),
Config.SECTION_REMOTE_URL: cache_dir,
Config.SECTION_CACHE_TYPE: cache_type,
Config.SECTION_CACHE_PROTECTED: protected
Config.SECTION_CACHE_PROTECTED: protected,
}
self.local = Remote(project, sect)
self._local = Remote(repo, sect)
self.s3 = self._get_remote(config, Config.SECTION_CACHE_S3)
self.gs = self._get_remote(config, Config.SECTION_CACHE_GS)
self.ssh = self._get_remote(config, Config.SECTION_CACHE_SSH)
self.hdfs = self._get_remote(config, Config.SECTION_CACHE_HDFS)
self.azure = self._get_remote(config, Config.SECTION_CACHE_AZURE)
self._s3 = self._get_remote(config, Config.SECTION_CACHE_S3)
self._gs = self._get_remote(config, Config.SECTION_CACHE_GS)
self._ssh = self._get_remote(config, Config.SECTION_CACHE_SSH)
self._hdfs = self._get_remote(config, Config.SECTION_CACHE_HDFS)
self._azure = self._get_remote(config, Config.SECTION_CACHE_AZURE)
@property
def local(self):
"""Remote instance for local cache."""
return self._local
@property
def s3(self):
"""Remote instance for AWS S3 cache."""
return self._s3
@property
def gs(self):
"""Remote instance for Google Cloud Storage cache."""
return self._gs
@property
def ssh(self):
"""Remote instance for SSH cache."""
return self._ssh
@property
def hdfs(self):
"""Remote instance for HDFS cache."""
return self._hdfs
@property
def azure(self):
"""Remote instance for azure cache."""
return self._azure
def _get_remote(self, config, name):
from dvc.remote import Remote
@@ -48,5 +88,5 @@ class Cache(object):
return None
name = Config.SECTION_REMOTE_FMT.format(remote)
sect = self.project.config._config[name]
return Remote(self.project, sect)
sect = self.repo.config.config[name]
return Remote(self.repo, sect)
+117 -1057
View File
File diff suppressed because it is too large Load Diff
+60 -8
View File
@@ -1,14 +1,66 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.logger import Logger
from dvc.command.base import CmdBase, append_doc_link
class CmdAdd(CmdBase):
def run(self):
for target in self.args.targets:
try:
self.project.add(target, recursive=self.args.recursive)
except DvcException as ex:
Logger.error('Failed to add \'{}\''.format(target), ex)
return 1
try:
if len(self.args.targets) > 1 and self.args.file:
raise DvcException("can't use '--file' with multiple targets")
for target in self.args.targets:
self.repo.add(
target,
recursive=self.args.recursive,
no_commit=self.args.no_commit,
fname=self.args.file,
)
except DvcException:
logger.error("failed to add file")
return 1
return 0
def add_parser(subparsers, parent_parser):
ADD_HELP = "Take data files or directories under DVC control."
add_parser = subparsers.add_parser(
"add",
parents=[parent_parser],
description=append_doc_link(ADD_HELP, "add"),
help=ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Recursively add each file under the directory.",
)
add_parser.add_argument(
"--no-commit",
action="store_true",
default=False,
help="Don't put files/directories into cache.",
)
add_parser.add_argument(
"-f",
"--file",
help="Specify name of the stage file. It should be "
"either 'Dvcfile' or have a '.dvc' suffix (e.g. "
"'prepare.dvc', 'clean.dvc', etc) in order for "
"dvc to be able to find it later. By default "
"the output basename + .dvc is used as a stage filename. "
"(NOTE: It can't be used when specifying multiple targets)",
)
add_parser.add_argument(
"targets", nargs="+", help="Input files/directories."
)
add_parser.set_defaults(func=CmdAdd)
+53 -11
View File
@@ -1,31 +1,73 @@
from dvc.logger import Logger
from __future__ import unicode_literals
import colorama
import dvc.logger as logger
def fix_subparsers(subparsers):
"""Workaround for bug in Python 3. See more info at:
https://bugs.python.org/issue16308
https://github.com/iterative/dvc/issues/769
Args:
subparsers: subparsers to fix.
"""
from dvc.utils.compat import is_py3
if is_py3: # pragma: no cover
subparsers.required = True
subparsers.dest = "cmd"
def append_doc_link(help_message, path):
if not path:
return help_message
doc_base = "https://man.dvc.org/"
return "{message}\ndocumentation: {blue}{base}{path}{nc}".format(
message=help_message,
base=doc_base,
path=path,
blue=colorama.Fore.CYAN,
nc=colorama.Fore.RESET,
)
class CmdBase(object):
def __init__(self, args):
from dvc.project import Project
from dvc.repo import Repo
self.project = Project()
self.config = self.project.config
self.repo = Repo()
self.config = self.repo.config
self.args = args
self._set_loglevel(args)
self.set_loglevel(args)
@property
def default_targets(self):
"""Default targets for `dvc repro` and `dvc pipeline`."""
from dvc.stage import Stage
msg = "assuming default target '{}'.".format(Stage.STAGE_FILE)
logger.warning(msg)
return [Stage.STAGE_FILE]
@staticmethod
def _set_loglevel(args):
def set_loglevel(args):
"""Sets log level from CLI arguments."""
if args.quiet:
Logger.be_quiet()
logger.be_quiet()
elif args.verbose:
Logger.be_verbose()
logger.be_verbose()
def run_cmd(self):
from dvc.lock import LockError
try:
with self.project.lock:
with self.repo.lock:
return self.run()
except LockError as ex:
Logger.error('Failed to lock before running a command', ex)
except LockError:
logger.error("failed to lock before running a command")
return 1
# Abstract methods that have to be implemented by any inheritance class
+65
View File
@@ -0,0 +1,65 @@
from __future__ import unicode_literals
import argparse
from dvc.command.base import fix_subparsers, append_doc_link
from dvc.command.remote import CmdRemoteAdd
from dvc.command.config import CmdConfig
class CmdCacheDir(CmdConfig):
def run(self):
self.args.name = "cache.dir"
self.args.value = CmdRemoteAdd.resolve_path(
self.args.value, self.configobj.filename
)
return super(CmdCacheDir, self).run()
def add_parser(subparsers, parent_parser):
from dvc.command.config import parent_config_parser
CACHE_HELP = "Manage cache settings."
cache_parser = subparsers.add_parser(
"cache",
parents=[parent_parser],
description=append_doc_link(CACHE_HELP, "cache"),
help=CACHE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
cache_subparsers = cache_parser.add_subparsers(
dest="cmd", help="Use dvc cache CMD --help for command-specific help."
)
fix_subparsers(cache_subparsers)
parent_cache_config_parser = argparse.ArgumentParser(
add_help=False, parents=[parent_config_parser]
)
CACHE_DIR_HELP = "Configure cache directory location."
cache_dir_parser = cache_subparsers.add_parser(
"dir",
parents=[parent_cache_config_parser],
description=append_doc_link(CACHE_HELP, "cache-dir"),
help=CACHE_DIR_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
cache_dir_parser.add_argument(
"-u",
"--unset",
default=False,
action="store_true",
help="Unset option.",
)
cache_dir_parser.add_argument(
"value",
default=None,
help="Path to cache directory. Relative paths are resolved relative "
"to the current directory and saved to config relative to the "
"config file location.",
)
cache_dir_parser.set_defaults(func=CmdCacheDir)
+47 -5
View File
@@ -1,13 +1,55 @@
from dvc.command.base import CmdBase
from __future__ import unicode_literals
import argparse
from dvc.command.base import CmdBase, append_doc_link
class CmdCheckout(CmdBase):
def run(self):
if not self.args.targets:
self.project.checkout(force=self.args.force)
self.repo.checkout(force=self.args.force)
else:
for target in self.args.targets:
self.project.checkout(target=target,
with_deps=self.args.with_deps,
force=self.args.force)
self.repo.checkout(
target=target,
with_deps=self.args.with_deps,
force=self.args.force,
recursive=self.args.recursive,
)
return 0
def add_parser(subparsers, parent_parser):
CHECKOUT_HELP = "Checkout data files from cache."
checkout_parser = subparsers.add_parser(
"checkout",
parents=[parent_parser],
description=append_doc_link(CHECKOUT_HELP, "checkout"),
help=CHECKOUT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
checkout_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Checkout all dependencies of the specified target.",
)
checkout_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Do not prompt when removing working directory files.",
)
checkout_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Checkout all subdirectories of the specified directory.",
)
checkout_parser.add_argument("targets", nargs="*", help="DVC files.")
checkout_parser.set_defaults(func=CmdCheckout)
+67
View File
@@ -0,0 +1,67 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, append_doc_link
class CmdCommit(CmdBase):
def run(self):
if not self.args.targets:
self.args.targets = [None]
for target in self.args.targets:
try:
self.repo.commit(
target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
force=self.args.force,
)
except DvcException:
logger.error(
"failed to commit{}".format(
(" " + target) if target else ""
)
)
return 1
return 0
def add_parser(subparsers, parent_parser):
COMMIT_HELP = "Save changed data to cache and update DVC files."
commit_parser = subparsers.add_parser(
"commit",
parents=[parent_parser],
description=append_doc_link(COMMIT_HELP, "commit"),
help=COMMIT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
commit_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Commit even if checksums for dependencies/outputs changed.",
)
commit_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Commit all dependencies of the specified target.",
)
commit_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Commit cache for subdirectories of the specified directory.",
)
commit_parser.add_argument(
"targets", nargs="*", default=None, help="DVC files."
)
commit_parser.set_defaults(func=CmdCommit)
+59 -17
View File
@@ -1,21 +1,24 @@
import os
from __future__ import unicode_literals
from dvc.command.base import CmdBase
from dvc.logger import Logger
import os
import argparse
import dvc.logger as logger
from dvc.command.base import CmdBase, append_doc_link
from dvc.config import Config
from dvc.exceptions import DvcException
class CmdConfig(CmdBase):
def __init__(self, args):
from dvc.project import Project, NotDvcProjectError
from dvc.repo import Repo, NotDvcRepoError
self.args = args
try:
dvc_dir = os.path.join(Project._find_root(), Project.DVC_DIR)
dvc_dir = os.path.join(Repo.find_root(), Repo.DVC_DIR)
saved_exc = None
except NotDvcProjectError as exc:
except NotDvcRepoError as exc:
dvc_dir = None
saved_exc = exc
@@ -31,7 +34,7 @@ class CmdConfig(CmdBase):
else:
if dvc_dir is None:
raise saved_exc
self.configobj = self.config._project_config
self.configobj = self.config._repo_config
def run_cmd(self):
return self.run()
@@ -43,16 +46,16 @@ class CmdConfig(CmdBase):
try:
self.config.unset(configobj, section, opt)
self.config.save(configobj)
except DvcException as exc:
Logger.error("Failed to unset '{}'".format(self.args.name), exc)
except DvcException:
logger.error("failed to unset '{}'".format(self.args.name))
return 1
return 0
def _show(self, section, opt):
try:
self.config.show(self.configobj, section, opt)
except DvcException as exc:
Logger.error("Failed to show '{}'".format(self.args.name), exc)
except DvcException:
logger.error("failed to show '{}'".format(self.args.name))
return 1
return 0
@@ -60,16 +63,15 @@ class CmdConfig(CmdBase):
try:
self.config.set(self.configobj, section, opt, value)
self.config.save(self.configobj)
except DvcException as exc:
Logger.error("Failed to set '{}.{}' to '{}'".format(section,
opt,
value),
exc)
except DvcException:
logger.error(
"failed to set '{}.{}' to '{}'".format(section, opt, value)
)
return 1
return 0
def run(self):
section, opt = self.args.name.lower().strip().split('.', 1)
section, opt = self.args.name.lower().strip().split(".", 1)
if self.args.unset:
return self._unset(section, opt)
@@ -77,3 +79,43 @@ class CmdConfig(CmdBase):
return self._show(section, opt)
else:
return self._set(section, opt, self.args.value)
parent_config_parser = argparse.ArgumentParser(add_help=False)
parent_config_parser.add_argument(
"--global",
dest="glob",
action="store_true",
default=False,
help="Use global config.",
)
parent_config_parser.add_argument(
"--system", action="store_true", default=False, help="Use system config."
)
parent_config_parser.add_argument(
"--local", action="store_true", default=False, help="Use local config."
)
def add_parser(subparsers, parent_parser):
CONFIG_HELP = "Get or set config options."
config_parser = subparsers.add_parser(
"config",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(CONFIG_HELP, "config"),
help=CONFIG_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
config_parser.add_argument(
"-u",
"--unset",
default=False,
action="store_true",
help="Unset option.",
)
config_parser.add_argument("name", help="Option name.")
config_parser.add_argument(
"value", nargs="?", default=None, help="Option value."
)
config_parser.set_defaults(func=CmdConfig)
+42 -5
View File
@@ -1,11 +1,13 @@
from dvc.command.base import CmdBase
from __future__ import unicode_literals
from dvc.command.base import CmdBase, fix_subparsers
class CmdDaemonBase(CmdBase):
def __init__(self, args):
self.args = args
self.config = None
self._set_loglevel(args)
self.set_loglevel(args)
def run_cmd(self):
return self.run()
@@ -14,11 +16,11 @@ class CmdDaemonBase(CmdBase):
class CmdDaemonUpdater(CmdDaemonBase):
def run(self):
import os
from dvc.project import Project
from dvc.repo import Repo
from dvc.updater import Updater
root_dir = Project._find_root()
dvc_dir = os.path.join(root_dir, Project.DVC_DIR)
root_dir = Repo.find_root()
dvc_dir = os.path.join(root_dir, Repo.DVC_DIR)
updater = Updater(dvc_dir)
updater.fetch(detach=False)
@@ -33,3 +35,38 @@ class CmdDaemonAnalytics(CmdDaemonBase):
analytics.send()
return 0
def add_parser(subparsers, parent_parser):
DAEMON_HELP = "Service daemon."
daemon_parser = subparsers.add_parser(
"daemon",
parents=[parent_parser],
description=DAEMON_HELP,
add_help=False,
)
daemon_subparsers = daemon_parser.add_subparsers(
dest="cmd", help="Use dvc daemon CMD --help for command-specific help."
)
fix_subparsers(daemon_subparsers)
DAEMON_UPDATER_HELP = "Fetch latest available version."
daemon_updater_parser = daemon_subparsers.add_parser(
"updater",
parents=[parent_parser],
description=DAEMON_UPDATER_HELP,
help=DAEMON_UPDATER_HELP,
)
daemon_updater_parser.set_defaults(func=CmdDaemonUpdater)
DAEMON_ANALYTICS_HELP = "Send dvc usage analytics."
daemon_analytics_parser = daemon_subparsers.add_parser(
"analytics",
parents=[parent_parser],
description=DAEMON_ANALYTICS_HELP,
help=DAEMON_ANALYTICS_HELP,
)
daemon_analytics_parser.add_argument("target", help="Analytics file.")
daemon_analytics_parser.set_defaults(func=CmdDaemonAnalytics)
+278 -31
View File
@@ -1,7 +1,14 @@
from dvc.command.base import CmdBase
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.command.base import CmdBase, append_doc_link
class CmdDataBase(CmdBase):
UP_TO_DATE_MSG = "Everything is up to date."
def do_run(self, target):
pass
@@ -15,53 +22,293 @@ class CmdDataBase(CmdBase):
ret = 1
return ret
@classmethod
def check_up_to_date(cls, processed_files_count):
if processed_files_count == 0:
logger.info(cls.UP_TO_DATE_MSG)
class CmdDataPull(CmdDataBase):
def do_run(self, target=None):
try:
self.project.pull(target=target,
jobs=self.args.jobs,
remote=self.args.remote,
show_checksums=self.args.show_checksums,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
with_deps=self.args.with_deps,
force=self.args.force)
except Exception as exc:
self.project.logger.error('Failed to pull data from the cloud',
exc)
processed_files_count = self.repo.pull(
target=target,
jobs=self.args.jobs,
remote=self.args.remote,
show_checksums=self.args.show_checksums,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
with_deps=self.args.with_deps,
force=self.args.force,
recursive=self.args.recursive,
)
except Exception:
logger.error("failed to pull data from the cloud")
return 1
self.check_up_to_date(processed_files_count)
return 0
class CmdDataPush(CmdDataBase):
def do_run(self, target=None):
try:
self.project.push(target=target,
jobs=self.args.jobs,
remote=self.args.remote,
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)
processed_files_count = self.repo.push(
target=target,
jobs=self.args.jobs,
remote=self.args.remote,
show_checksums=self.args.show_checksums,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except Exception:
logger.error("failed to push data to the cloud")
return 1
self.check_up_to_date(processed_files_count)
return 0
class CmdDataFetch(CmdDataBase):
def do_run(self, target=None):
try:
self.project.fetch(target=target,
jobs=self.args.jobs,
remote=self.args.remote,
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)
processed_files_count = self.repo.fetch(
target=target,
jobs=self.args.jobs,
remote=self.args.remote,
show_checksums=self.args.show_checksums,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except Exception:
logger.error("failed to fetch data from the cloud")
return 1
self.check_up_to_date(processed_files_count)
return 0
def shared_parent_parser():
from dvc.cli import get_parent_parser
# Parent parser used in pull/push/status
shared_parent_parser = argparse.ArgumentParser(
add_help=False, parents=[get_parent_parser()]
)
shared_parent_parser.add_argument(
"-j",
"--jobs",
type=int,
default=None,
help="Number of jobs to run simultaneously.",
)
shared_parent_parser.add_argument(
"--show-checksums",
action="store_true",
default=False,
help="Show checksums instead of file names.",
)
shared_parent_parser.add_argument(
"targets", nargs="*", default=None, help="DVC files."
)
return shared_parent_parser
def add_parser(subparsers, _parent_parser):
from dvc.command.status import CmdDataStatus
# Pull
PULL_HELP = "Pull data files from a DVC remote storage."
pull_parser = subparsers.add_parser(
"pull",
parents=[shared_parent_parser()],
description=append_doc_link(PULL_HELP, "pull"),
help=PULL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pull_parser.add_argument(
"-r", "--remote", help="Remote repository to pull from."
)
pull_parser.add_argument(
"-a",
"--all-branches",
action="store_true",
default=False,
help="Fetch cache for all branches.",
)
pull_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Fetch cache for all tags.",
)
pull_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Fetch cache for all dependencies of the specified target.",
)
pull_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Do not prompt when removing working directory files.",
)
pull_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Pull cache for subdirectories of the specified directory.",
)
pull_parser.set_defaults(func=CmdDataPull)
# Push
PUSH_HELP = "Push data files to a DVC remote storage."
push_parser = subparsers.add_parser(
"push",
parents=[shared_parent_parser()],
description=append_doc_link(PUSH_HELP, "push"),
help=PUSH_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
push_parser.add_argument(
"-r", "--remote", help="Remote repository to push to."
)
push_parser.add_argument(
"-a",
"--all-branches",
action="store_true",
default=False,
help="Push cache for all branches.",
)
push_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Push cache for all tags.",
)
push_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Push cache for all dependencies of the specified target.",
)
push_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Push cache from subdirectories of specified directory.",
)
push_parser.set_defaults(func=CmdDataPush)
# Fetch
FETCH_HELP = "Fetch data files from a DVC remote storage."
fetch_parser = subparsers.add_parser(
"fetch",
parents=[shared_parent_parser()],
description=append_doc_link(FETCH_HELP, "fetch"),
help=FETCH_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
fetch_parser.add_argument(
"-r", "--remote", help="Remote repository to fetch from."
)
fetch_parser.add_argument(
"-a",
"--all-branches",
action="store_true",
default=False,
help="Fetch cache for all branches.",
)
fetch_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Fetch cache for all tags.",
)
fetch_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Fetch cache for all dependencies of the " "specified target.",
)
fetch_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Fetch cache for subdirectories of specified directory.",
)
fetch_parser.set_defaults(func=CmdDataFetch)
# Status
STATUS_HELP = (
"Show changed stages, compare local cache and a remote storage."
)
status_parser = subparsers.add_parser(
"status",
parents=[shared_parent_parser()],
description=append_doc_link(STATUS_HELP, "status"),
help=STATUS_HELP,
conflict_handler="resolve",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
status_parser.add_argument(
"-q",
"--quiet",
action="store_true",
default=False,
help=(
"Suppresses all output."
" Exit with 0 if pipeline is up to date, otherwise 1."
),
)
status_parser.add_argument(
"-c",
"--cloud",
action="store_true",
default=False,
help="Show status of a local cache compared to a remote repository.",
)
status_parser.add_argument(
"-r", "--remote", help="Remote repository to compare local cache to."
)
status_parser.add_argument(
"-a",
"--all-branches",
action="store_true",
default=False,
help="Show status of a local cache compared to a remote repository "
"for all branches.",
)
status_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Show status of a local cache compared to a remote repository "
"for all tags.",
)
status_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Show status for all dependencies of the specified target.",
)
status_parser.set_defaults(func=CmdDataStatus)
+41 -12
View File
@@ -1,22 +1,51 @@
from dvc.command.base import CmdBase
from __future__ import unicode_literals
import argparse
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
class CmdDestroy(CmdBase):
def run_cmd(self):
try:
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?'
statement = (
"This will destroy all information about your pipelines,"
" all data files, as well as cache in .dvc/cache."
"\n"
"Are you sure you want to continue?"
)
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)
if not self.args.force and not prompt.confirm(statement):
raise DvcException(
"cannot destroy without a confirmation from the user."
" Use '-f' to force."
)
self.project.destroy()
except Exception as exc:
self.project.logger.error('Failed to destroy DVC', exc)
self.repo.destroy()
except Exception:
logger.error("failed to destroy DVC")
return 1
return 0
def add_parser(subparsers, parent_parser):
DESTROY_HELP = "Remove DVC files, local DVC config and data cache."
destroy_parser = subparsers.add_parser(
"destroy",
parents=[parent_parser],
description=append_doc_link(DESTROY_HELP, "destroy"),
help=DESTROY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
destroy_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force destruction.",
)
destroy_parser.set_defaults(func=CmdDestroy)
+94 -22
View File
@@ -1,39 +1,111 @@
import os
from __future__ import unicode_literals
from dvc.command.base import CmdBase
import argparse
import os
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.command.base import CmdBase, append_doc_link
class CmdGC(CmdBase):
def run(self):
msg = 'This will remove all cache except the cache that is used in '
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'
msg += "the current git branch"
elif self.args.all_branches and not self.args.all_tags:
msg += 'all git branches'
msg += "all git branches"
elif not self.args.all_branches and self.args.all_tags:
msg += 'all git tags'
msg += "all git tags"
else:
msg += 'all git branches and all git tags'
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:'
if self.args.repos is not None and len(self.args.repos) > 0:
msg += " of the current and the following repos:"
for project_path in self.args.projects:
msg += '\n - %s' % os.path.abspath(project_path)
for repo_path in self.args.repos:
msg += "\n - %s" % os.path.abspath(repo_path)
else:
msg += ' of the current project.'
msg += " of the current repo."
self.project.logger.warn(msg)
logger.warning(msg)
msg = 'Are you sure you want to proceed?'
if not self.args.force and not self.project.prompt.prompt(msg):
msg = "Are you sure you want to proceed?"
if not self.args.force and not prompt.confirm(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,
force=self.args.force,
jobs=self.args.jobs,
projects=self.args.projects)
self.repo.gc(
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
cloud=self.args.cloud,
remote=self.args.remote,
force=self.args.force,
jobs=self.args.jobs,
repos=self.args.repos,
)
return 0
def add_parser(subparsers, parent_parser):
GC_HELP = "Collect unused data from DVC cache or a remote storage."
GC_DESCRIPTION = (
"Deletes all files in the cache or a remote which are not in\n"
"use by the specified git references (defaults to just HEAD)."
)
gc_parser = subparsers.add_parser(
"gc",
parents=[parent_parser],
description=append_doc_link(GC_DESCRIPTION, "gc"),
help=GC_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
gc_parser.add_argument(
"-a",
"--all-branches",
action="store_true",
default=False,
help="Keep data files for the tips of all git branches.",
)
gc_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Keep data files for all git tags.",
)
gc_parser.add_argument(
"-c",
"--cloud",
action="store_true",
default=False,
help="Collect garbage in remote repository.",
)
gc_parser.add_argument(
"-r", "--remote", help="Remote storage to collect garbage in."
)
gc_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force garbage collection - automatically agree to all prompts.",
)
gc_parser.add_argument(
"-j",
"--jobs",
type=int,
default=None,
help="Number of jobs to run simultaneously.",
)
gc_parser.add_argument(
"-p",
"--projects",
dest="repos",
type=str,
nargs="*",
default=None,
help="Keep data files required by these projects "
"in addition to the current one. "
"Useful if you share a single cache across repos.",
)
gc_parser.set_defaults(func=CmdGC)
+49 -13
View File
@@ -1,26 +1,62 @@
from __future__ import unicode_literals
import argparse
import os
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
import dvc.logger as logger
from dvc.utils.compat import urlparse
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.logger import Logger
from dvc.command.base import CmdBase, append_doc_link
class CmdImport(CmdBase):
def run(self):
try:
default_out = os.path.basename(
urlparse(self.args.url).path
)
default_out = os.path.basename(urlparse(self.args.url).path)
out = self.args.out or default_out
self.project.imp(self.args.url, out)
except DvcException as ex:
Logger.error('Failed to import {}'.format(self.args.url), ex)
self.repo.imp(self.args.url, out, self.args.resume)
except DvcException:
logger.error(
"failed to import {}. You could also try downloading "
"it manually and adding it with `dvc add` command.".format(
self.args.url
)
)
return 1
return 0
def add_parser(subparsers, parent_parser):
IMPORT_HELP = "Download or copy files from URL and take under DVC control."
import_parser = subparsers.add_parser(
"import",
parents=[parent_parser],
description=append_doc_link(IMPORT_HELP, "import"),
help=IMPORT_HELP,
formatter_class=argparse.RawTextHelpFormatter,
)
import_parser.add_argument(
"url",
help="Supported urls:\n"
"/path/to/file\n"
"C:\\\\path\\to\\file\n"
"https://example.com/path/to/file\n"
"s3://bucket/path/to/file\n"
"gs://bucket/path/to/file\n"
"hdfs://example.com/path/to/file\n"
"ssh://example.com:/path/to/file\n"
"remote://myremote/path/to/file (see `dvc remote`)",
)
import_parser.add_argument(
"--resume",
action="store_true",
default=False,
help="Resume previously started download.",
)
import_parser.add_argument(
"out", nargs="?", help="Destination path to put files to."
)
import_parser.set_defaults(func=CmdImport)
+49 -8
View File
@@ -1,4 +1,9 @@
from dvc.logger import Logger
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.command.base import append_doc_link
class CmdInit(object):
@@ -6,14 +11,50 @@ class CmdInit(object):
self.args = args
def run_cmd(self):
from dvc.project import Project, InitError
from dvc.repo import Repo
from dvc.exceptions import InitError
try:
self.project = Project.init('.',
no_scm=self.args.no_scm,
force=self.args.force)
self.config = self.project.config
except InitError as e:
Logger.error('Failed to initiate dvc', e)
self.repo = Repo.init(
".", no_scm=self.args.no_scm, force=self.args.force
)
self.config = self.repo.config
except InitError:
logger.error("failed to initiate dvc")
return 1
return 0
def add_parser(subparsers, parent_parser):
"""Setup parser for `dvc init`."""
INIT_HELP = "Initialize DVC in the current directory."
INIT_DESCRIPTION = (
"Initialize DVC in the current directory. Expects directory\n"
"to be a Git repository unless --no-scm option is specified."
)
init_parser = subparsers.add_parser(
"init",
parents=[parent_parser],
description=append_doc_link(INIT_DESCRIPTION, "init"),
help=INIT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
init_parser.add_argument(
"--no-scm",
action="store_true",
default=False,
help="Initiate dvc in directory that is "
"not tracked by any scm tool (e.g. git).",
)
init_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help=(
"Overwrite existing '.dvc' directory. "
"This operation removes local cache."
),
)
init_parser.set_defaults(func=CmdInit)
+21 -5
View File
@@ -1,12 +1,28 @@
from dvc.logger import Logger
from dvc.command.base import CmdBase
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.command.base import CmdBase, append_doc_link
class CmdInstall(CmdBase):
def run_cmd(self):
try:
self.project.install()
except Exception as e:
Logger.error('Failed to install dvc hooks', e)
self.repo.install()
except Exception:
logger.error("failed to install dvc hooks")
return 1
return 0
def add_parser(subparsers, parent_parser):
INSTALL_HELP = "Install DVC git hooks into the repository."
install_parser = subparsers.add_parser(
"install",
parents=[parent_parser],
description=append_doc_link(INSTALL_HELP, "install"),
help=INSTALL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
install_parser.set_defaults(func=CmdInstall)
+41 -9
View File
@@ -1,25 +1,57 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdLockBase(CmdBase):
def run(self, unlock=False):
def _run(self, unlock):
for target in self.args.targets:
try:
self.project.lock_stage(target, unlock=unlock)
except DvcException as ex:
msg = 'Failed to {}lock \'{}\''
msg = msg.format('un' if unlock else '', target)
self.project.logger.error(msg, ex)
self.repo.lock_stage(target, unlock=unlock)
except DvcException:
logger.error(
"failed to {}lock '{}'".format(
"un" if unlock else "", target
)
)
return 1
return 0
class CmdLock(CmdLockBase):
def run(self):
return super(CmdLock, self).run(False)
return self._run(False)
class CmdUnlock(CmdLockBase):
def run(self):
return super(CmdUnlock, self).run(True)
return self._run(True)
def add_parser(subparsers, parent_parser):
LOCK_HELP = "Lock DVC file."
lock_parser = subparsers.add_parser(
"lock",
parents=[parent_parser],
description=append_doc_link(LOCK_HELP, "lock"),
help=LOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
lock_parser.add_argument("targets", nargs="+", help="DVC files.")
lock_parser.set_defaults(func=CmdLock)
UNLOCK_HELP = "Unlock DVC file."
unlock_parser = subparsers.add_parser(
"unlock",
parents=[parent_parser],
description=append_doc_link(UNLOCK_HELP, "unlock"),
help=UNLOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unlock_parser.add_argument("targets", nargs="+", help="DVC files.")
unlock_parser.set_defaults(func=CmdUnlock)
+150 -42
View File
@@ -1,37 +1,38 @@
from dvc.command.base import CmdBase
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
def show_metrics(metrics, all_branches=False, all_tags=False):
for branch, val in metrics.items():
if all_branches or all_tags:
logger.info("{}:".format(branch))
for fname, metric in val.items():
logger.info("\t{}: {}".format(fname, metric))
class CmdMetricsShow(CmdBase):
def run(self):
typ = self.args.type
xpath = self.args.xpath
try:
# backward compatibility
if self.args.json_path:
typ = 'json'
xpath = self.args.json_path
elif self.args.tsv_path:
typ = 'tsv'
xpath = self.args.tsv_path
elif self.args.htsv_path:
typ = 'htsv'
xpath = self.args.htsv_path
elif self.args.csv_path:
typ = 'csv'
xpath = self.args.csv_path
elif self.args.hcsv_path:
typ = 'hcsv'
xpath = self.args.hcsv_path
else:
typ = self.args.type
xpath = self.args.xpath
metrics = self.repo.metrics.show(
self.args.path,
typ=typ,
xpath=xpath,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
recursive=self.args.recursive,
)
self.project.metrics_show(self.args.path,
typ=typ,
xpath=xpath,
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)
show_metrics(metrics, self.args.all_branches, self.args.all_tags)
except DvcException:
logger.error("failed to show metrics")
return 1
return 0
@@ -40,11 +41,11 @@ class CmdMetricsShow(CmdBase):
class CmdMetricsModify(CmdBase):
def run(self):
try:
self.project.metrics_modify(self.args.path,
typ=self.args.type,
xpath=self.args.xpath)
except DvcException as exc:
self.project.logger.error('Failed to modify metrics', exc)
self.repo.metrics.modify(
self.args.path, typ=self.args.type, xpath=self.args.xpath
)
except DvcException:
logger.error("failed to modify metric file settings")
return 1
return 0
@@ -53,12 +54,12 @@ class CmdMetricsModify(CmdBase):
class CmdMetricsAdd(CmdBase):
def run(self):
try:
self.project.metrics_add(self.args.path,
self.args.type,
self.args.xpath)
except DvcException as exc:
msg = 'Failed to add metric file \'{}\''.format(self.args.path)
self.project.logger.error(msg, exc)
self.repo.metrics.add(
self.args.path, self.args.type, self.args.xpath
)
except DvcException:
msg = "failed to add metric file '{}'".format(self.args.path)
logger.error(msg)
return 1
return 0
@@ -67,10 +68,117 @@ class CmdMetricsAdd(CmdBase):
class CmdMetricsRemove(CmdBase):
def run(self):
try:
self.project.metrics_remove(self.args.path)
except DvcException as exc:
msg = 'Failed to remove metric file \'{}\''.format(self.args.path)
self.project.logger.error(msg, exc)
self.repo.metrics.remove(self.args.path)
except DvcException:
msg = "failed to remove metric file '{}'".format(self.args.path)
logger.error(msg)
return 1
return 0
def add_parser(subparsers, parent_parser):
METRICS_HELP = "Commands to add, manage, collect and display metrics."
metrics_parser = subparsers.add_parser(
"metrics",
parents=[parent_parser],
description=append_doc_link(METRICS_HELP, "metrics"),
help=METRICS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_subparsers = metrics_parser.add_subparsers(
dest="cmd",
help="Use dvc metrics CMD --help to display command-specific help.",
)
fix_subparsers(metrics_subparsers)
METRICS_SHOW_HELP = "Output metric values."
metrics_show_parser = metrics_subparsers.add_parser(
"show",
parents=[parent_parser],
description=append_doc_link(METRICS_SHOW_HELP, "metrics-show"),
help=METRICS_SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_show_parser.add_argument(
"path", nargs="?", help="Path to a metric file or a directory."
)
metrics_show_parser.add_argument(
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
)
metrics_show_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
)
metrics_show_parser.add_argument(
"-a",
"--all-branches",
action="store_true",
default=False,
help="Show metrics for all branches.",
)
metrics_show_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Show metrics for all tags.",
)
metrics_show_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help=(
"If path is a directory, recursively search and process metric "
"files in path."
),
)
metrics_show_parser.set_defaults(func=CmdMetricsShow)
METRICS_ADD_HELP = "Tag file as a metric file."
metrics_add_parser = metrics_subparsers.add_parser(
"add",
parents=[parent_parser],
description=append_doc_link(METRICS_ADD_HELP, "metrics-add"),
help=METRICS_ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_add_parser.add_argument(
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
)
metrics_add_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
)
metrics_add_parser.add_argument("path", help="Path to a metric file.")
metrics_add_parser.set_defaults(func=CmdMetricsAdd)
METRICS_MODIFY_HELP = "Modify metric file options."
metrics_modify_parser = metrics_subparsers.add_parser(
"modify",
parents=[parent_parser],
description=append_doc_link(METRICS_MODIFY_HELP, "metrics-modify"),
help=METRICS_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_modify_parser.add_argument(
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
)
metrics_modify_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
)
metrics_modify_parser.add_argument("path", help="Path to a metric file.")
metrics_modify_parser.set_defaults(func=CmdMetricsModify)
METRICS_REMOVE_HELP = "Remove files's metric tag."
metrics_remove_parser = metrics_subparsers.add_parser(
"remove",
parents=[parent_parser],
description=append_doc_link(METRICS_REMOVE_HELP, "metrics-remove"),
help=METRICS_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_remove_parser.add_argument("path", help="Path to a metric file.")
metrics_remove_parser.set_defaults(func=CmdMetricsRemove)
+34 -6
View File
@@ -1,14 +1,42 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdMove(CmdBase):
def run(self):
try:
self.project.move(self.args.src, self.args.dst)
except DvcException as ex:
msg = 'Failed to move \'{}\' -> \'{}\''.format(self.args.src,
self.args.dst)
self.project.logger.error(msg, ex)
self.repo.move(self.args.src, self.args.dst)
except DvcException:
msg = "failed to move '{}' -> '{}'".format(
self.args.src, self.args.dst
)
logger.error(msg)
return 1
return 0
def add_parser(subparsers, parent_parser):
MOVE_HELP = "Rename or move a DVC controlled data file or a directory."
MOVE_DESCRIPTION = (
"Rename or move a DVC controlled data file or a directory.\n"
"It renames and modifies the corresponding DVC file to reflect the"
" changes."
)
move_parser = subparsers.add_parser(
"move",
parents=[parent_parser],
description=append_doc_link(MOVE_DESCRIPTION, "move"),
help=MOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
move_parser.add_argument(
"src", help="Source path to a data file or directory."
)
move_parser.add_argument("dst", help="Destination path.")
move_parser.set_defaults(func=CmdMove)
+147 -39
View File
@@ -1,7 +1,14 @@
from __future__ import unicode_literals
import argparse
from dvc.utils.compat import str
import os
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
class CmdPipelineShow(CmdBase):
@@ -9,33 +16,34 @@ class CmdPipelineShow(CmdBase):
import networkx
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(stage.path, self.project.root_dir)
stage = Stage.load(self.repo, target)
G = self.repo.graph()[0]
stages = networkx.get_node_attributes(G, "stage")
node = os.path.relpath(stage.path, self.repo.root_dir)
for n in networkx.dfs_postorder_nodes(G, node):
if commands:
self.project.logger.info(stages[n].cmd)
logger.info(stages[n].cmd)
elif outs:
for out in stages[n].outs:
self.project.logger.info(str(out))
logger.info(str(out))
else:
self.project.logger.info(n)
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)
stage = Stage.load(self.repo, target)
node = os.path.relpath(stage.path, self.repo.root_dir)
pipelines = list(filter(lambda g: node in g.nodes(),
self.project.pipelines()))
pipelines = list(
filter(lambda g: node in g.nodes(), self.repo.pipelines())
)
assert len(pipelines) == 1
G = pipelines[0]
stages = networkx.get_node_attributes(G, 'stage')
stages = networkx.get_node_attributes(G, "stage")
nodes = []
for n in G.nodes():
@@ -61,29 +69,51 @@ class CmdPipelineShow(CmdBase):
elif outs:
for from_out in from_stage.outs:
for to_out in to_stage.outs:
edges.append((str(from_out),
str(to_out)))
edges.append((str(from_out), str(to_out)))
else:
edges.append((from_stage.relpath, to_stage.relpath))
return nodes, edges
return nodes, edges, networkx.is_tree(G)
def _show_ascii(self, target, commands, outs):
from dvc.dagascii import Dagascii
from dvc.dagascii import draw
nodes, edges = self.__build_graph(target, commands, outs)
nodes, edges, _ = self.__build_graph(target, commands, outs)
if not nodes:
return
d = Dagascii(nodes, edges)
d.draw()
draw(nodes, edges)
def _show_dependencies_tree(self, target, commands, outs):
from treelib import Tree
nodes, edges, is_tree = self.__build_graph(target, commands, outs)
if not nodes:
return
if not is_tree:
raise DvcException(
"DAG is not a tree, can not print it in tree-structure way, "
"please use --ascii instead"
)
tree = Tree()
tree.create_node(target, target) # Root node
observe_list = [target]
while len(observe_list) > 0:
current_root = observe_list[0]
for edge in edges:
if edge[0] == current_root:
tree.create_node(edge[1], edge[1], parent=current_root)
observe_list.append(edge[1])
observe_list.pop(0)
tree.show()
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, _ = self.__build_graph(target, commands, outs)
edges = [edge[::-1] for edge in edges]
simple_g = networkx.DiGraph()
@@ -91,24 +121,31 @@ class CmdPipelineShow(CmdBase):
write_dot(simple_g, filename)
def run(self, unlock=False):
if not self.args.targets:
self.args.targets = self.default_targets
for target in self.args.targets:
try:
if self.args.ascii:
self._show_ascii(target,
self.args.commands,
self.args.outs)
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)
self.__write_dot(
target,
self.args.commands,
self.args.outs,
self.args.dot,
)
elif self.args.tree:
self._show_dependencies_tree(
target, self.args.commands, self.args.outs
)
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)
self._show(target, self.args.commands, self.args.outs)
except DvcException:
msg = "failed to show pipeline for '{}'".format(target)
logger.error(msg)
return 1
return 0
@@ -117,13 +154,84 @@ class CmdPipelineList(CmdBase):
def run(self):
import networkx
pipelines = self.project.pipelines()
pipelines = self.repo.pipelines()
for p in pipelines:
stages = networkx.get_node_attributes(p, 'stage')
stages = networkx.get_node_attributes(p, "stage")
for stage in stages:
self.project.logger.info(stage)
logger.info(stage)
if len(stages) != 0:
self.project.logger.info("="*80)
self.project.logger.info("{} pipeline(s) total".format(len(pipelines)))
logger.info("=" * 80)
logger.info("{} pipeline(s) total".format(len(pipelines)))
return 0
def add_parser(subparsers, parent_parser):
PIPELINE_HELP = "Manage pipelines."
pipeline_parser = subparsers.add_parser(
"pipeline",
parents=[parent_parser],
description=append_doc_link(PIPELINE_HELP, "pipeline"),
help=PIPELINE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_subparsers = pipeline_parser.add_subparsers(
dest="cmd",
help="Use dvc pipeline CMD --help for command-specific help.",
)
fix_subparsers(pipeline_subparsers)
PIPELINE_SHOW_HELP = "Show pipeline."
pipeline_show_parser = pipeline_subparsers.add_parser(
"show",
parents=[parent_parser],
description=append_doc_link(PIPELINE_SHOW_HELP, "pipeline-show"),
help=PIPELINE_SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_show_group = pipeline_show_parser.add_mutually_exclusive_group()
pipeline_show_group.add_argument(
"-c",
"--commands",
action="store_true",
default=False,
help="Print commands instead of paths to DVC files.",
)
pipeline_show_group.add_argument(
"-o",
"--outs",
action="store_true",
default=False,
help="Print output files instead of paths to DVC files.",
)
pipeline_show_parser.add_argument(
"--ascii",
action="store_true",
default=False,
help="Output DAG as ASCII.",
)
pipeline_show_parser.add_argument(
"--dot", help="Write DAG in .dot format."
)
pipeline_show_parser.add_argument(
"--tree",
action="store_true",
default=False,
help="Output DAG as Dependencies Tree.",
)
pipeline_show_parser.add_argument(
"targets", nargs="*", help="DVC files. 'Dvcfile' by default."
)
pipeline_show_parser.set_defaults(func=CmdPipelineShow)
PIPELINE_LIST_HELP = "List pipelines."
pipeline_list_parser = pipeline_subparsers.add_parser(
"list",
parents=[parent_parser],
description=append_doc_link(PIPELINE_LIST_HELP, "pipeline-list"),
help=PIPELINE_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_list_parser.set_defaults(func=CmdPipelineList)
+89
View File
@@ -0,0 +1,89 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
from dvc.repo.pkg import PackageParams
class CmdPkg(CmdBase):
def run(self, unlock=False):
try:
pkg_params = PackageParams(
self.args.address,
self.args.target_dir,
self.args.select,
self.args.file,
)
return self.repo.install_pkg(pkg_params)
except DvcException:
logger.error(
"failed to install package '{}'".format(self.args.address)
)
return 1
pass
def add_parser(subparsers, parent_parser):
from dvc.command.config import parent_config_parser
PKG_HELP = "Manage DVC packages."
pkg_parser = subparsers.add_parser(
"pkg",
parents=[parent_parser],
description=append_doc_link(PKG_HELP, "pkg"),
help=PKG_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=False,
)
pkg_subparsers = pkg_parser.add_subparsers(
dest="cmd", help="Use dvc pkg CMD --help for command-specific help."
)
fix_subparsers(pkg_subparsers)
PKG_INSTALL_HELP = "Install package."
pkg_install_parser = pkg_subparsers.add_parser(
"install",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(PKG_INSTALL_HELP, "pkg-install"),
help=PKG_INSTALL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pkg_install_parser.add_argument(
"address",
nargs="?",
default="",
help="Package address: git://<url> or https://github.com/...",
)
pkg_install_parser.add_argument(
"target_dir",
metavar="target",
nargs="?",
default=".",
help="Target directory to deploy package outputs. "
"Default value is the current dir.",
)
pkg_install_parser.add_argument(
"-s",
"--select",
metavar="OUT",
action="append",
default=[],
help="Select and persist only specified outputs from a package. "
"The parameter can be used multiple times. "
"All outputs will be selected by default.",
)
pkg_install_parser.add_argument(
"-f",
"--file",
help="Specify name of the stage file. It should be "
"either 'Dvcfile' or have a '.dvc' suffix (e.g. "
"'prepare.dvc', 'clean.dvc', etc). "
"By default the file has 'mod_' prefix and imported package name "
"followed by .dvc",
)
pkg_install_parser.set_defaults(func=CmdPkg)
+164 -18
View File
@@ -1,23 +1,54 @@
from __future__ import unicode_literals
import argparse
import os
import re
import dvc.logger as logger
from dvc.command.base import fix_subparsers, append_doc_link
from dvc.config import Config
from dvc.command.config import CmdConfig
from dvc.logger import Logger
class CmdRemoteAdd(CmdConfig):
@staticmethod
def resolve_path(path, config_file):
"""Resolve path relative to config file location.
Args:
path: Path to be resolved.
config_file: Path to config file, which `path` is specified
relative to.
Returns:
Path relative to the `config_file` location. If `path` is an
absolute path then it will be returned without change.
"""
if os.path.isabs(path):
return path
return os.path.relpath(path, os.path.dirname(config_file))
def run(self):
from dvc.remote import _get, RemoteLOCAL
remote = _get({Config.SECTION_REMOTE_URL: self.args.url})
if remote == RemoteLOCAL:
self.args.url = self.resolve_path(
self.args.url, self.configobj.filename
)
section = Config.SECTION_REMOTE_FMT.format(self.args.name)
ret = self._set(section, Config.SECTION_REMOTE_URL, self.args.url)
if ret != 0:
return ret
if self.args.default:
msg = 'Setting \'{}\' as a default remote.'.format(self.args.name)
Logger.info(msg)
ret = self._set(Config.SECTION_CORE,
Config.SECTION_CORE_REMOTE,
self.args.name)
msg = "Setting '{}' as a default remote.".format(self.args.name)
logger.info(msg)
ret = self._set(
Config.SECTION_CORE, Config.SECTION_CORE_REMOTE, self.args.name
)
return ret
@@ -33,9 +64,11 @@ class CmdRemoteRemove(CmdConfig):
return 0
if default == self.args.name:
return self._unset(Config.SECTION_CORE,
opt=Config.SECTION_CORE_REMOTE,
configobj=config)
return self._unset(
Config.SECTION_CORE,
opt=Config.SECTION_CORE_REMOTE,
configobj=config,
)
def run(self):
section = Config.SECTION_REMOTE_FMT.format(self.args.name)
@@ -43,10 +76,12 @@ class CmdRemoteRemove(CmdConfig):
if ret != 0:
return ret
for configobj in [self.config._local_config,
self.config._project_config,
self.config._global_config,
self.config._system_config]:
for configobj in [
self.config._local_config,
self.config._repo_config,
self.config._global_config,
self.config._system_config,
]:
self._remove_default(configobj)
self.config.save(configobj)
if configobj == self.configobj:
@@ -58,17 +93,128 @@ class CmdRemoteRemove(CmdConfig):
class CmdRemoteModify(CmdConfig):
def run(self):
section = Config.SECTION_REMOTE_FMT.format(self.args.name)
self.args.name = '{}.{}'.format(section, self.args.option)
self.args.name = "{}.{}".format(section, self.args.option)
return super(CmdRemoteModify, self).run()
class CmdRemoteDefault(CmdConfig):
def run(self):
self.args.value = self.args.name
self.args.name = "core.remote"
return super(CmdRemoteDefault, self).run()
class CmdRemoteList(CmdConfig):
def run(self):
for section in self.configobj.keys():
r = re.match(Config.SECTION_REMOTE_REGEX, section)
if r:
name = r.group('name')
url = self.configobj[section].get(Config.SECTION_REMOTE_URL,
'')
Logger.info('{}\t{}'.format(name, url))
name = r.group("name")
url = self.configobj[section].get(
Config.SECTION_REMOTE_URL, ""
)
logger.info("{}\t{}".format(name, url))
return 0
def add_parser(subparsers, parent_parser):
from dvc.command.config import parent_config_parser
REMOTE_HELP = "Manage remote storage configuration."
remote_parser = subparsers.add_parser(
"remote",
parents=[parent_parser],
description=append_doc_link(REMOTE_HELP, "remote"),
help=REMOTE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_subparsers = remote_parser.add_subparsers(
dest="cmd",
help="Use dvc remote CMD --help for " "command-specific help.",
)
fix_subparsers(remote_subparsers)
REMOTE_ADD_HELP = "Add remote."
remote_add_parser = remote_subparsers.add_parser(
"add",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_ADD_HELP, "remote-add"),
help=REMOTE_ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_add_parser.add_argument("name", help="Name.")
remote_add_parser.add_argument(
"url",
help="URL. See full list of supported urls at " "man.dvc.org/remote",
)
remote_add_parser.add_argument(
"-d",
"--default",
action="store_true",
default=False,
help="Set as default remote.",
)
remote_add_parser.set_defaults(func=CmdRemoteAdd)
REMOTE_DEFAULT_HELP = "Set/unset default remote."
remote_default_parser = remote_subparsers.add_parser(
"default",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_DEFAULT_HELP, "remote-default"),
help=REMOTE_DEFAULT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_default_parser.add_argument(
"name", nargs="?", help="Name of the remote."
)
remote_default_parser.add_argument(
"-u",
"--unset",
action="store_true",
default=False,
help="Unset default remote.",
)
remote_default_parser.set_defaults(func=CmdRemoteDefault)
REMOTE_REMOVE_HELP = "Remove remote."
remote_remove_parser = remote_subparsers.add_parser(
"remove",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_REMOVE_HELP, "remote-remove"),
help=REMOTE_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_remove_parser.add_argument("name", help="Name")
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
REMOTE_MODIFY_HELP = "Modify remote."
remote_modify_parser = remote_subparsers.add_parser(
"modify",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_MODIFY_HELP, "remote-modify"),
help=REMOTE_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_modify_parser.add_argument("name", help="Name.")
remote_modify_parser.add_argument("option", help="Option.")
remote_modify_parser.add_argument("value", nargs="?", help="Value.")
remote_modify_parser.add_argument(
"-u",
"--unset",
default=False,
action="store_true",
help="Unset option.",
)
remote_modify_parser.set_defaults(func=CmdRemoteModify)
REMOTE_LIST_HELP = "List available remotes."
remote_list_parser = remote_subparsers.add_parser(
"list",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_LIST_HELP, "remote-list"),
help=REMOTE_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_list_parser.set_defaults(func=CmdRemoteList)
+50 -13
View File
@@ -1,6 +1,11 @@
from __future__ import unicode_literals
import argparse
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.logger import Logger
from dvc.command.base import CmdBase, append_doc_link
class CmdRemove(CmdBase):
@@ -11,27 +16,59 @@ class CmdRemove(CmdBase):
if self.args.force:
return False
msg = (
u'Are you sure you want to remove {} with its outputs?'
.format(target)
msg = "Are you sure you want to remove {} with its outputs?".format(
target
)
confirmed = self.project.prompt.prompt(msg)
if confirmed:
if prompt.confirm(msg):
return False
raise DvcException(
u'Cannot purge without a confirmation from the user.'
u" Use '-f' to force."
"Cannot purge without a confirmation from the user."
" Use '-f' to force."
)
def run(self):
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)
self.repo.remove(target, outs_only=outs_only)
except DvcException:
logger.error("failed to remove {}".format(target))
return 1
return 0
def add_parser(subparsers, parent_parser):
REMOVE_HELP = "Remove outputs of DVC file."
remove_parser = subparsers.add_parser(
"remove",
parents=[parent_parser],
description=append_doc_link(REMOVE_HELP, "remove"),
help=REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remove_parser_group = remove_parser.add_mutually_exclusive_group()
remove_parser_group.add_argument(
"-o",
"--outs",
action="store_true",
default=True,
help="Only remove DVC file outputs.(default)",
)
remove_parser_group.add_argument(
"-p",
"--purge",
action="store_true",
default=False,
help="Remove DVC file and all its outputs.",
)
remove_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force purge.",
)
remove_parser.add_argument("targets", nargs="+", help="DVC files.")
remove_parser.set_defaults(func=CmdRemove)
+115 -14
View File
@@ -1,6 +1,11 @@
from __future__ import unicode_literals
import argparse
import os
from dvc.command.base import CmdBase
import dvc.logger as logger
from dvc.command.base import CmdBase, append_doc_link
from dvc.command.metrics import show_metrics
from dvc.command.status import CmdDataStatus
from dvc.exceptions import DvcException
@@ -12,28 +17,124 @@ class CmdRepro(CmdBase):
if self.args.cwd:
os.chdir(self.args.cwd)
# Dirty hack so the for loop below can at least enter once
if self.args.all_pipelines:
self.args.targets = [None]
elif not self.args.targets:
self.args.targets = self.default_targets
ret = 0
for target in self.args.targets:
try:
stages = self.project.reproduce(
target,
recursive=recursive,
force=self.args.force,
dry=self.args.dry,
interactive=self.args.interactive,
pipeline=self.args.pipeline,
all_pipelines=self.args.all_pipelines)
stages = self.repo.reproduce(
target,
recursive=recursive,
force=self.args.force,
dry=self.args.dry,
interactive=self.args.interactive,
pipeline=self.args.pipeline,
all_pipelines=self.args.all_pipelines,
ignore_build_cache=self.args.ignore_build_cache,
no_commit=self.args.no_commit,
)
if len(stages) == 0:
self.project.logger.info(CmdDataStatus.UP_TO_DATE_MSG)
logger.info(CmdDataStatus.UP_TO_DATE_MSG)
if self.args.metrics:
self.project.metrics_show()
except DvcException as ex:
msg = 'Failed to reproduce \'{}\''.format(target)
self.project.logger.error(msg, ex)
metrics = self.repo.metrics.show()
show_metrics(metrics)
except DvcException:
logger.error()
ret = 1
break
os.chdir(saved_dir)
return ret
def add_parser(subparsers, parent_parser):
REPRO_HELP = "Check for changes and reproduce DVC file and dependencies."
repro_parser = subparsers.add_parser(
"repro",
parents=[parent_parser],
description=append_doc_link(REPRO_HELP, "repro"),
help=REPRO_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
repro_parser.add_argument(
"targets",
nargs="*",
help="DVC file to reproduce (default - 'Dvcfile').",
)
repro_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Reproduce even if dependencies were not changed.",
)
repro_parser.add_argument(
"-s",
"--single-item",
action="store_true",
default=False,
help="Reproduce only single data item without recursive dependencies "
"check.",
)
repro_parser.add_argument(
"-c",
"--cwd",
default=os.path.curdir,
help="Directory within your repo to reproduce from.",
)
repro_parser.add_argument(
"-m",
"--metrics",
action="store_true",
default=False,
help="Show metrics after reproduction.",
)
repro_parser.add_argument(
"--dry",
action="store_true",
default=False,
help="Only print the commands that would be executed without "
"actually executing.",
)
repro_parser.add_argument(
"-i",
"--interactive",
action="store_true",
default=False,
help="Ask for confirmation before reproducing each stage.",
)
repro_parser.add_argument(
"-p",
"--pipeline",
action="store_true",
default=False,
help="Reproduce the whole pipeline that the specified stage file "
"belongs to.",
)
repro_parser.add_argument(
"-P",
"--all-pipelines",
action="store_true",
default=False,
help="Reproduce all pipelines in the repo.",
)
repro_parser.add_argument(
"--ignore-build-cache",
action="store_true",
default=False,
help="Reproduce all descendants of a changed stage even if their "
"direct dependencies didn't change.",
)
repro_parser.add_argument(
"--no-commit",
action="store_true",
default=False,
help="Don't put files/directories into cache.",
)
repro_parser.set_defaults(func=CmdRepro)
+18 -2
View File
@@ -1,6 +1,10 @@
from __future__ import unicode_literals
import argparse
import os
from dvc.command.base import CmdBase
import dvc.logger as logger
from dvc.command.base import CmdBase, append_doc_link
class CmdRoot(CmdBase):
@@ -8,5 +12,17 @@ class CmdRoot(CmdBase):
return self.run()
def run(self):
self.project.logger.info(os.path.relpath(self.project.root_dir))
logger.info(os.path.relpath(self.repo.root_dir))
return 0
def add_parser(subparsers, parent_parser):
ROOT_HELP = "Relative path to the repository's directory."
root_parser = subparsers.add_parser(
"root",
parents=[parent_parser],
description=append_doc_link(ROOT_HELP, "root"),
help=ROOT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
root_parser.set_defaults(func=CmdRoot)
+191 -31
View File
@@ -1,40 +1,200 @@
from dvc.command.base import CmdBase
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
class CmdRun(CmdBase):
def _joined_cmd(self):
if len(self.args.command) == 0:
return ''
if len(self.args.command) == 1:
return self.args.command[0]
cmd = ''
for chunk in self.args.command:
if len(chunk.split()) != 1:
fmt = ' "{}"'
else:
fmt = ' {}'
cmd += fmt.format(chunk)
return cmd
def run(self):
overwrite = (self.args.yes or self.args.overwrite_dvcfile)
overwrite = self.args.yes or self.args.overwrite_dvcfile
if not any(
[
self.args.deps,
self.args.outs,
self.args.outs_no_cache,
self.args.metrics,
self.args.metrics_no_cache,
self.args.outs_persist,
self.args.outs_persist_no_cache,
self.args.command,
]
): # pragma: no cover
logger.error(
"too few arguments. Specify at least one: '-d', '-o', '-O',"
" '-m', '-M', '--outs-persist', '--outs-persist-no-cache',"
" 'command'."
)
return 1
try:
self.project.run(cmd=self._joined_cmd(),
outs=self.args.outs,
outs_no_cache=self.args.outs_no_cache,
metrics_no_cache=self.args.metrics_no_cache,
deps=self.args.deps,
fname=self.args.file,
cwd=self.args.cwd,
no_exec=self.args.no_exec,
overwrite=overwrite,
ignore_build_cache=self.args.ignore_build_cache,
remove_outs=self.args.remove_outs)
except DvcException as ex:
self.project.logger.error('Failed to run command', ex)
self.repo.run(
cmd=self._parsed_cmd(),
outs=self.args.outs,
outs_no_cache=self.args.outs_no_cache,
metrics=self.args.metrics,
metrics_no_cache=self.args.metrics_no_cache,
deps=self.args.deps,
fname=self.args.file,
cwd=self.args.cwd,
wdir=self.args.wdir,
no_exec=self.args.no_exec,
overwrite=overwrite,
ignore_build_cache=self.args.ignore_build_cache,
remove_outs=self.args.remove_outs,
no_commit=self.args.no_commit,
outs_persist=self.args.outs_persist,
outs_persist_no_cache=self.args.outs_persist_no_cache,
)
except DvcException:
logger.error("failed to run command")
return 1
return 0
def _parsed_cmd(self):
"""
We need to take into account two cases:
- ['python code.py foo bar']: Used mainly with dvc as a library
- ['echo', 'foo bar']: List of arguments received from the CLI
The second case would need quoting, as it was passed through:
dvc run echo "foo bar"
"""
if len(self.args.command) < 2:
return " ".join(self.args.command)
return " ".join(self._quote_argument(arg) for arg in self.args.command)
def _quote_argument(self, argument):
if " " not in argument or '"' in argument:
return argument
return '"{}"'.format(argument)
def add_parser(subparsers, parent_parser):
RUN_HELP = "Generate a stage file from a command and execute the command."
run_parser = subparsers.add_parser(
"run",
parents=[parent_parser],
description=append_doc_link(RUN_HELP, "run"),
help=RUN_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
run_parser.add_argument(
"-d",
"--deps",
action="append",
default=[],
help="Declare dependencies for reproducible cmd.",
)
run_parser.add_argument(
"-o",
"--outs",
action="append",
default=[],
help="Declare output file or directory.",
)
run_parser.add_argument(
"-O",
"--outs-no-cache",
action="append",
default=[],
help="Declare output file or directory "
"(do not put into DVC cache).",
)
run_parser.add_argument(
"-m",
"--metrics",
action="append",
default=[],
help="Declare output metric file or directory.",
)
run_parser.add_argument(
"-M",
"--metrics-no-cache",
action="append",
default=[],
help="Declare output metric file or directory "
"(do not put into DVC cache).",
)
run_parser.add_argument(
"-f",
"--file",
help="Specify name of the stage file. It should be "
"either 'Dvcfile' or have a '.dvc' suffix (e.g. "
"'prepare.dvc', 'clean.dvc', etc) in order for "
"dvc to be able to find it later. By default "
"the first output basename + .dvc is used as "
"a stage filename.",
)
run_parser.add_argument(
"-c", "--cwd", default=None, help="Deprecated, use -w and -f instead."
)
run_parser.add_argument(
"-w",
"--wdir",
default=None,
help="Directory within your repo to run your command in.",
)
run_parser.add_argument(
"--no-exec",
action="store_true",
default=False,
help="Only create stage file without actually running it.",
)
run_parser.add_argument(
"-y",
"--yes",
action="store_true",
default=False,
help="Deprecated, use --overwrite-dvcfile instead",
)
run_parser.add_argument(
"--overwrite-dvcfile",
action="store_true",
default=False,
help="Overwrite existing dvc file without asking for confirmation.",
)
run_parser.add_argument(
"--ignore-build-cache",
action="store_true",
default=False,
help="Run this stage even if it has been already ran with the same "
"command/dependencies/outputs/etc before.",
)
run_parser.add_argument(
"--remove-outs",
action="store_true",
default=False,
help="Remove outputs before running the command.",
)
run_parser.add_argument(
"--no-commit",
action="store_true",
default=False,
help="Don't put files/directories into cache.",
)
run_parser.add_argument(
"--outs-persist",
action="append",
default=[],
help="Declare output file or directory that will not be "
"removed upon repro.",
)
run_parser.add_argument(
"--outs-persist-no-cache",
action="append",
default=[],
help="Declare output file or directory that will not be "
"removed upon repro (do not put into DVC cache).",
)
run_parser.add_argument(
"command", nargs=argparse.REMAINDER, help="Command to execute."
)
run_parser.set_defaults(func=CmdRun)
+41 -22
View File
@@ -1,44 +1,63 @@
from __future__ import unicode_literals
import dvc.logger as logger
from dvc.command.data_sync import CmdDataBase
from dvc.utils.compat import str
class CmdDataStatus(CmdDataBase):
STATUS_LEN = 10
STATUS_INDENT = '\t'
STATUS_LEN = 20
STATUS_INDENT = "\t"
UP_TO_DATE_MSG = "Pipeline is up to date. Nothing to reproduce."
def _normalize(self, s):
s += ':'
s += ":"
assert len(s) < self.STATUS_LEN
return s + (self.STATUS_LEN - len(s))*' '
return s + (self.STATUS_LEN - len(s)) * " "
def _show(self, status, indent=0):
ind = indent * self.STATUS_INDENT
if isinstance(status, str):
logger.info("{}{}".format(ind, status))
return
if isinstance(status, list):
for entry in status:
self._show(entry, indent)
return
assert isinstance(status, dict)
for key, value in status.items():
if isinstance(value, dict):
self.project.logger.info('{}{}'.format(ind, key))
self._show(value, indent+1)
else:
msg = '{}{}{}'.format(ind, self._normalize(value), key)
self.project.logger.info(msg)
if isinstance(value, str):
logger.info("{}{}{}".format(ind, self._normalize(value), key))
elif value:
logger.info("{}{}:".format(ind, key))
self._show(value, indent + 1)
def do_run(self, target=None):
indent = 1 if self.args.cloud else 0
try:
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)
st = self.repo.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)
if self.args.quiet:
return 1
else:
self._show(st, indent)
else:
self.project.logger.info(self.UP_TO_DATE_MSG)
logger.info(self.UP_TO_DATE_MSG)
except Exception as exc:
self.project.logger.error('Failed to obtain data status', exc)
except Exception:
logger.error("failed to obtain data status")
return 1
return 0
+25 -5
View File
@@ -1,14 +1,34 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
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)
self.repo.unprotect(target)
except DvcException:
msg = "failed to unprotect '{}'".format(target)
logger.error(msg)
return 1
return 0
def add_parser(subparsers, parent_parser):
UNPROTECT_HELP = "Unprotect a data file/directory."
unprotect_parser = subparsers.add_parser(
"unprotect",
parents=[parent_parser],
description=append_doc_link(UNPROTECT_HELP, "unprotect"),
help=UNPROTECT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unprotect_parser.add_argument(
"targets", nargs="+", help="Data files/directory."
)
unprotect_parser.set_defaults(func=CmdUnprotect)
+333 -131
View File
@@ -1,87 +1,157 @@
"""
DVC config objects.
"""
"""DVC config objects."""
from __future__ import unicode_literals
from dvc.utils.compat import str, open
import os
import errno
import configobj
from schema import Schema, Optional, And, Use, Regex
from dvc.logger import Logger
import dvc.logger as logger
from dvc.exceptions import DvcException
class ConfigError(DvcException):
""" DVC config exception """
"""DVC config exception.
Args:
msg (str): error message.
ex (Exception): optional exception that has caused this error.
"""
def __init__(self, msg, ex=None):
super(ConfigError, self).__init__('Config file error: {}'.format(msg),
ex)
def supported_url(url):
from dvc.remote import supported_url as supported
return supported(url)
super(ConfigError, self).__init__(
"config file error: {}".format(msg), ex
)
def supported_cache_type(types):
"""Checks if link type config option has a valid value.
Args:
types (list/string): type(s) of links that dvc should try out.
"""
if isinstance(types, str):
types = [t.strip() for t in types.split(',')]
for t in types:
if t not in ['reflink', 'hardlink', 'symlink', 'copy']:
types = [typ.strip() for typ in types.split(",")]
for typ in types:
if typ not in ["reflink", "hardlink", "symlink", "copy"]:
return False
return True
def supported_loglevel(level):
return level in ['info', 'debug', 'warning', 'error']
"""Checks if log level config option has a valid value.
Args:
level (str): log level name.
"""
return level in ["info", "debug", "warning", "error"]
def supported_cloud(cloud):
return cloud in ['aws', 'gcp', 'local', '']
"""Checks if obsoleted cloud option has a valid value.
Args:
cloud (str): cloud type name.
"""
return cloud in ["aws", "gcp", "local", ""]
def is_bool(val):
return val.lower() in ['true', 'false']
"""Checks that value is a boolean.
Args:
val (str): string value verify.
Returns:
bool: True if value stands for boolean, False otherwise.
"""
return val.lower() in ["true", "false"]
def to_bool(val):
return val.lower() == 'true'
"""Converts value to boolean.
Args:
val (str): string to convert to boolean.
Returns:
bool: True if value.lower() == 'true', False otherwise.
"""
return val.lower() == "true"
def is_whole(val):
"""Checks that value is a whole integer.
Args:
val (str): number string to verify.
Returns:
bool: True if val is a whole number, False otherwise.
"""
return int(val) >= 0
def is_percent(val):
"""Checks that value is a percent.
Args:
val (str): number string to verify.
Returns:
bool: True if 0<=value<=100, False otherwise.
"""
return int(val) >= 0 and int(val) <= 100
class Config(object):
APPNAME = 'dvc'
APPAUTHOR = 'iterative'
class Config(object): # pylint: disable=too-many-instance-attributes
"""Class that manages configuration files for a dvc repo.
CONFIG = 'config'
CONFIG_LOCAL = 'config.local'
Args:
dvc_dir (str): optional path to `.dvc` directory, that is used to
access repo-specific configs like .dvc/config and
.dvc/config.local.
validate (bool): optional flag to tell dvc if it should validate the
config or just load it as is. 'True' by default.
SECTION_CORE = 'core'
SECTION_CORE_LOGLEVEL = 'loglevel'
Raises:
ConfigError: thrown when config has an invalid format.
"""
APPNAME = "dvc"
APPAUTHOR = "iterative"
# NOTE: used internally in RemoteLOCAL to know config
# location, that url should resolved relative to.
PRIVATE_CWD = "_cwd"
CONFIG = "config"
CONFIG_LOCAL = "config.local"
SECTION_CORE = "core"
SECTION_CORE_LOGLEVEL = "loglevel"
SECTION_CORE_LOGLEVEL_SCHEMA = And(Use(str.lower), supported_loglevel)
SECTION_CORE_REMOTE = 'remote'
SECTION_CORE_REMOTE = "remote"
SECTION_CORE_INTERACTIVE_SCHEMA = And(str, is_bool, Use(to_bool))
SECTION_CORE_INTERACTIVE = 'interactive'
SECTION_CORE_ANALYTICS = 'analytics'
SECTION_CORE_INTERACTIVE = "interactive"
SECTION_CORE_ANALYTICS = "analytics"
SECTION_CORE_ANALYTICS_SCHEMA = And(str, is_bool, Use(to_bool))
SECTION_CACHE = 'cache'
SECTION_CACHE_DIR = 'dir'
SECTION_CACHE_TYPE = 'type'
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'
SECTION_CACHE_SSH = 'ssh'
SECTION_CACHE_HDFS = 'hdfs'
SECTION_CACHE_AZURE = 'azure'
SECTION_CACHE_PROTECTED = "protected"
SECTION_CACHE_LOCAL = "local"
SECTION_CACHE_S3 = "s3"
SECTION_CACHE_GS = "gs"
SECTION_CACHE_SSH = "ssh"
SECTION_CACHE_HDFS = "hdfs"
SECTION_CACHE_AZURE = "azure"
SECTION_CACHE_SCHEMA = {
Optional(SECTION_CACHE_LOCAL): str,
Optional(SECTION_CACHE_S3): str,
@@ -89,94 +159,114 @@ class Config(object):
Optional(SECTION_CACHE_HDFS): str,
Optional(SECTION_CACHE_SSH): str,
Optional(SECTION_CACHE_AZURE): str,
Optional(SECTION_CACHE_DIR, default='cache'): str,
Optional(SECTION_CACHE_DIR): str,
Optional(SECTION_CACHE_TYPE, default=None): SECTION_CACHE_TYPE_SCHEMA,
Optional(SECTION_CACHE_PROTECTED,
default=False): And(str, is_bool, Use(to_bool)),
Optional(SECTION_CACHE_PROTECTED, default=False): And(
str, is_bool, Use(to_bool)
),
Optional(PRIVATE_CWD): str,
}
# backward compatibility
SECTION_CORE_CLOUD = 'cloud'
SECTION_CORE_CLOUD = "cloud"
SECTION_CORE_CLOUD_SCHEMA = And(Use(str.lower), supported_cloud)
SECTION_CORE_STORAGEPATH = 'storagepath'
SECTION_CORE_STORAGEPATH = "storagepath"
SECTION_CORE_SCHEMA = {
Optional(SECTION_CORE_LOGLEVEL,
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,
Optional(SECTION_CORE_ANALYTICS,
default=True): SECTION_CORE_ANALYTICS_SCHEMA,
Optional(SECTION_CORE_LOGLEVEL, 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,
Optional(
SECTION_CORE_ANALYTICS, default=True
): SECTION_CORE_ANALYTICS_SCHEMA,
# backward compatibility
Optional(SECTION_CORE_CLOUD, default=''): SECTION_CORE_CLOUD_SCHEMA,
Optional(SECTION_CORE_STORAGEPATH, default=''): str,
Optional(SECTION_CORE_CLOUD, default=""): SECTION_CORE_CLOUD_SCHEMA,
Optional(SECTION_CORE_STORAGEPATH, default=""): str,
}
# backward compatibility
SECTION_AWS = 'aws'
SECTION_AWS_STORAGEPATH = 'storagepath'
SECTION_AWS_CREDENTIALPATH = 'credentialpath'
SECTION_AWS_ENDPOINT_URL = 'endpointurl'
SECTION_AWS_REGION = 'region'
SECTION_AWS_PROFILE = 'profile'
SECTION_AWS = "aws"
SECTION_AWS_STORAGEPATH = "storagepath"
SECTION_AWS_CREDENTIALPATH = "credentialpath"
SECTION_AWS_ENDPOINT_URL = "endpointurl"
SECTION_AWS_LIST_OBJECTS = "listobjects"
SECTION_AWS_REGION = "region"
SECTION_AWS_PROFILE = "profile"
SECTION_AWS_USE_SSL = "use_ssl"
SECTION_AWS_SCHEMA = {
SECTION_AWS_STORAGEPATH: str,
Optional(SECTION_AWS_REGION): str,
Optional(SECTION_AWS_PROFILE): str,
Optional(SECTION_AWS_CREDENTIALPATH): str,
Optional(SECTION_AWS_ENDPOINT_URL): str,
Optional(SECTION_AWS_LIST_OBJECTS, default=False): And(
str, is_bool, Use(to_bool)
),
Optional(SECTION_AWS_USE_SSL, default=True): And(
str, is_bool, Use(to_bool)
),
}
# backward compatibility
SECTION_GCP = 'gcp'
SECTION_GCP = "gcp"
SECTION_GCP_STORAGEPATH = SECTION_AWS_STORAGEPATH
SECTION_GCP_PROJECTNAME = 'projectname'
SECTION_GCP_CREDENTIALPATH = SECTION_AWS_CREDENTIALPATH
SECTION_GCP_PROJECTNAME = "projectname"
SECTION_GCP_SCHEMA = {
SECTION_GCP_STORAGEPATH: str,
Optional(SECTION_GCP_PROJECTNAME): str,
}
# backward compatibility
SECTION_LOCAL = 'local'
SECTION_LOCAL = "local"
SECTION_LOCAL_STORAGEPATH = SECTION_AWS_STORAGEPATH
SECTION_LOCAL_SCHEMA = {
SECTION_LOCAL_STORAGEPATH: str,
}
SECTION_LOCAL_SCHEMA = {SECTION_LOCAL_STORAGEPATH: str}
SECTION_AZURE_CONNECTION_STRING = "connection_string"
SECTION_REMOTE_REGEX = r'^\s*remote\s*"(?P<name>.*)"\s*$'
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_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: And(supported_url, error="Unsupported URL"),
SECTION_REMOTE_URL: str,
Optional(SECTION_AWS_REGION): str,
Optional(SECTION_AWS_PROFILE): str,
Optional(SECTION_AWS_CREDENTIALPATH): str,
Optional(SECTION_AWS_ENDPOINT_URL): str,
Optional(SECTION_AWS_LIST_OBJECTS, default=False): And(
str, is_bool, Use(to_bool)
),
Optional(SECTION_AWS_USE_SSL, default=True): And(
str, is_bool, Use(to_bool)
),
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_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)),
Optional(SECTION_AZURE_CONNECTION_STRING): str,
Optional(PRIVATE_CWD): str,
}
SECTION_STATE = 'state'
SECTION_STATE_ROW_LIMIT = 'row_limit'
SECTION_STATE_ROW_CLEANUP_QUOTA = 'row_cleanup_quota'
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),
@@ -187,7 +277,6 @@ class Config(object):
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,
Optional(SECTION_GCP, default={}): SECTION_GCP_SCHEMA,
@@ -195,10 +284,12 @@ class Config(object):
}
def __init__(self, dvc_dir=None, validate=True):
self.system_config_file = os.path.join(self.get_system_config_dir(),
self.CONFIG)
self.global_config_file = os.path.join(self.get_global_config_dir(),
self.CONFIG)
self.system_config_file = os.path.join(
self.get_system_config_dir(), self.CONFIG
)
self.global_config_file = os.path.join(
self.get_global_config_dir(), self.CONFIG
)
if dvc_dir is not None:
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
@@ -209,24 +300,53 @@ class Config(object):
self.config_file = None
self.config_local_file = None
self._system_config = None
self._global_config = None
self._repo_config = None
self._local_config = None
self.config = None
self.load(validate=validate)
@staticmethod
def get_global_config_dir():
"""Returns global config location. E.g. ~/.config/dvc/config.
Returns:
str: path to the global config directory.
"""
from appdirs import user_config_dir
return user_config_dir(appname=Config.APPNAME,
appauthor=Config.APPAUTHOR)
return user_config_dir(
appname=Config.APPNAME, appauthor=Config.APPAUTHOR
)
@staticmethod
def get_system_config_dir():
"""Returns system config location. E.g. /etc/dvc.conf.
Returns:
str: path to the system config directory.
"""
from appdirs import site_config_dir
return site_config_dir(appname=Config.APPNAME,
appauthor=Config.APPAUTHOR)
return site_config_dir(
appname=Config.APPNAME, appauthor=Config.APPAUTHOR
)
@staticmethod
def init(dvc_dir):
"""Initializes dvc config.
Args:
dvc_dir (str): path to .dvc directory.
Returns:
dvc.config.Config: config object.
"""
config_file = os.path.join(dvc_dir, Config.CONFIG)
open(config_file, 'w+').close()
open(config_file, "w+").close()
return Config(dvc_dir)
def _load(self):
@@ -234,68 +354,123 @@ class Config(object):
self._global_config = configobj.ConfigObj(self.global_config_file)
if self.config_file is not None:
self._project_config = configobj.ConfigObj(self.config_file)
self._repo_config = configobj.ConfigObj(self.config_file)
else:
self._project_config = configobj.ConfigObj()
self._repo_config = configobj.ConfigObj()
if self.config_local_file is not None:
self._local_config = configobj.ConfigObj(self.config_local_file)
else:
self._local_config = configobj.ConfigObj()
self._config = None
self.config = None
def _load_config(self, path):
config = configobj.ConfigObj(path)
config = self._lower(config)
self._resolve_paths(config, path)
return config
@staticmethod
def _resolve_path(path, config_file):
assert os.path.isabs(config_file)
config_dir = os.path.dirname(config_file)
return os.path.abspath(os.path.join(config_dir, path))
def _resolve_cache_path(self, config, fname):
cache = config.get(self.SECTION_CACHE)
if cache is None:
return
cache_dir = cache.get(self.SECTION_CACHE_DIR)
if cache_dir is None:
return
cache[self.PRIVATE_CWD] = os.path.dirname(fname)
def _resolve_paths(self, config, fname):
if fname is None:
return
self._resolve_cache_path(config, fname)
for section in config.values():
if self.SECTION_REMOTE_URL not in section.keys():
continue
section[self.PRIVATE_CWD] = os.path.dirname(fname)
def load(self, validate=True):
"""Loads config from all the config files.
Args:
validate (bool): optional flag to tell dvc if it should validate
the config or just load it as is. 'True' by default.
Raises:
dvc.config.ConfigError: thrown if config has invalid format.
"""
self._load()
try:
self._config = configobj.ConfigObj(self.system_config_file)
user = configobj.ConfigObj(self.global_config_file)
config = configobj.ConfigObj(self.config_file)
local = configobj.ConfigObj(self.config_local_file)
self.config = self._load_config(self.system_config_file)
user = self._load_config(self.global_config_file)
config = self._load_config(self.config_file)
local = self._load_config(self.config_local_file)
# NOTE: schema doesn't support ConfigObj.Section validation, so we
# need to convert our config to dict before passing it to
self._config = self._lower(self._config)
for c in [user, config, local]:
c = self._lower(c)
self._config = self._merge(self._config, c)
for conf in [user, config, local]:
self.config = self._merge(self.config, conf)
if validate:
self._config = Schema(self.SCHEMA).validate(self._config)
self.config = Schema(self.SCHEMA).validate(self.config)
# NOTE: now converting back to ConfigObj
self._config = configobj.ConfigObj(self._config,
write_empty_values=True)
self._config.filename = self.config_file
self.config = configobj.ConfigObj(
self.config, write_empty_values=True
)
self.config.filename = self.config_file
self._resolve_paths(self.config, self.config_file)
except Exception as ex:
raise ConfigError(ex)
def _get_key(self, d, name, add=False):
for k in d.keys():
@staticmethod
def _get_key(conf, name, add=False):
for k in conf.keys():
if k.lower() == name.lower():
return k
if add:
d[name] = {}
conf[name] = {}
return name
return None
def save(self, config=None):
"""Saves config to config files.
Args:
config (configobj.ConfigObj): optional config object to save.
Raises:
dvc.config.ConfigError: thrown if failed to write config file.
"""
if config is not None:
clist = [config]
else:
clist = [self._system_config,
self._global_config,
self._project_config,
self._local_config]
clist = [
self._system_config,
self._global_config,
self._repo_config,
self._local_config,
]
for conf in clist:
if conf.filename is None:
continue
try:
Logger.debug("Writing '{}'.".format(conf.filename))
logger.debug("Writing '{}'.".format(conf.filename))
dname = os.path.dirname(os.path.abspath(conf.filename))
try:
os.makedirs(dname)
@@ -304,50 +479,77 @@ class Config(object):
raise
conf.write()
except Exception as exc:
msg = "Failed to write config '{}'".format(conf.filename)
msg = "failed to write config '{}'".format(conf.filename)
raise ConfigError(msg, exc)
def unset(self, config, section, opt=None):
@staticmethod
def unset(config, section, opt=None):
"""Unsets specified option and/or section in the config.
Args:
config (configobj.ConfigObj): config to work on.
section (str): section name.
opt (str): optional option name.
"""
if section not in config.keys():
raise ConfigError("Section '{}' doesn't exist".format(section))
raise ConfigError("section '{}' doesn't exist".format(section))
if opt is None:
del config[section]
return
if opt not in config[section].keys():
raise ConfigError("Option '{}.{}' doesn't exist".format(section,
opt))
raise ConfigError(
"option '{}.{}' doesn't exist".format(section, opt)
)
del config[section][opt]
if len(config[section]) == 0:
if not config[section]:
del config[section]
def set(self, config, section, opt, value):
@staticmethod
def set(config, section, opt, value):
"""Sets specified option in the config.
Args:
config (configobj.ConfigObj): config to work on.
section (str): section name.
opt (str): option name.
value: value to set option to.
"""
if section not in config.keys():
config[section] = {}
config[section][opt] = value
def show(self, config, section, opt):
@staticmethod
def show(config, section, opt):
"""Prints option value from the config.
Args:
config (configobj.ConfigObj): config to work on.
section (str): section name.
opt (str): option name.
"""
if section not in config.keys():
raise ConfigError("Section '{}' doesn't exist".format(section))
raise ConfigError("section '{}' doesn't exist".format(section))
if opt not in config[section].keys():
raise ConfigError("Option '{}.{}' doesn't exist".format(section,
opt))
raise ConfigError(
"option '{}.{}' doesn't exist".format(section, opt)
)
Logger.info(config[section][opt])
logger.info(config[section][opt])
@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
first_copy = first.get(section, {}).copy()
second_copy = second.get(section, {}).copy()
first_copy.update(second_copy)
res[section] = first_copy
return res
@staticmethod
@@ -356,6 +558,6 @@ class Config(object):
for s_key, s_value in config.items():
new_s = {}
for key, value in s_value.items():
new_s[key.lower()] = value
new_s[key.lower()] = str(value)
new_config[s_key.lower()] = new_s
return new_config
+80 -47
View File
@@ -1,62 +1,95 @@
"""Launch `dvc daemon` command in a separate detached process."""
from __future__ import unicode_literals
import os
import sys
import inspect
from subprocess import Popen
from dvc.logger import Logger
import dvc.logger as logger
from dvc.utils import is_binary, fix_env
from dvc.utils.compat import cast_bytes_py2
class Daemon(object): # pragma: no cover
def _spawn_windows(self, cmd):
CREATE_NEW_PROCESS_GROUP = 0x00000200
DETACHED_PROCESS = 0x00000008
creationflags = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
Popen(cmd,
close_fds=True,
shell=False,
creationflags=creationflags)
CREATE_NEW_PROCESS_GROUP = 0x00000200
DETACHED_PROCESS = 0x00000008
def _spawn_posix(self, cmd):
try:
pid = os.fork()
if pid > 0:
return
except OSError as exc:
Logger.error("Failed at first fork", exc)
sys.exit(1)
os.setsid()
os.umask(0)
def _spawn_windows(cmd, env):
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
try:
pid = os.fork()
if pid > 0:
sys.exit(0)
except OSError as exc:
Logger.error("Failed at second fork", exc)
sys.exit(1)
creationflags = CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
sys.stdin.close()
sys.stdout.close()
sys.stderr.close()
startupinfo = STARTUPINFO()
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
Popen(cmd,
close_fds=True,
shell=False)
Popen(
cmd,
env=env,
close_fds=True,
shell=False,
creationflags=creationflags,
startupinfo=startupinfo,
).communicate()
def __call__(self, args):
from dvc.utils import is_binary
cmd = [sys.executable]
if not is_binary():
cmd += ['-m', 'dvc']
cmd += ['daemon', '-q'] + args
def _spawn_posix(cmd, env):
# NOTE: using os._exit instead of sys.exit, because dvc built
# with PyInstaller has trouble with SystemExit exeption and throws
# errors such as "[26338] Failed to execute script __main__"
try:
pid = os.fork()
if pid > 0:
return
except OSError:
logger.error("failed at first fork")
os._exit(1) # pylint: disable=protected-access
Logger.debug("Trying to spawn '{}'".format(cmd))
os.setsid()
os.umask(0)
if os.name == 'nt':
self._spawn_windows(cmd)
elif os.name == 'posix':
self._spawn_posix(cmd)
else:
raise NotImplementedError
try:
pid = os.fork()
if pid > 0:
os._exit(0) # pylint: disable=protected-access
except OSError:
logger.error("failed at second fork")
os._exit(1) # pylint: disable=protected-access
Logger.debug("Spawned '{}'".format(cmd))
sys.stdin.close()
sys.stdout.close()
sys.stderr.close()
Popen(cmd, env=env, close_fds=True, shell=False).communicate()
os._exit(0) # pylint: disable=protected-access
def daemon(args):
"""Launch a `dvc daemon` command in a detached process.
Args:
args (list): list of arguments to append to `dvc daemon` command.
"""
cmd = [sys.executable]
if not is_binary():
cmd += ["-m", "dvc"]
cmd += ["daemon", "-q"] + args
env = fix_env()
file_path = os.path.abspath(inspect.stack()[0][1])
env[cast_bytes_py2("PYTHONPATH")] = cast_bytes_py2(
os.path.dirname(os.path.dirname(file_path))
)
logger.debug("Trying to spawn '{}'".format(cmd))
if os.name == "nt":
_spawn_windows(cmd, env)
elif os.name == "posix":
_spawn_posix(cmd, env)
else:
raise NotImplementedError
logger.debug("Spawned '{}'".format(cmd))
+203 -119
View File
@@ -1,3 +1,8 @@
"""Draws DAG in ASCII."""
from __future__ import unicode_literals
from __future__ import print_function
import sys
import math
@@ -6,7 +11,40 @@ from grandalf.layouts import SugiyamaLayout
from grandalf.routing import route_with_lines, EdgeViewer
class VertexViewer(object):
"""Class to define vertex box boundaries that will be accounted for during
graph building by grandalf.
Args:
name (str): name of the vertex.
"""
HEIGHT = 3 # top and bottom box edges + text
def __init__(self, name):
# pylint: disable=invalid-name
self._h = self.HEIGHT # top and bottom box edges + text
self._w = len(name) + 2 # right and left bottom edges + text
@property
def h(self): # pylint: disable=invalid-name
"""Height of the box."""
return self._h
@property
def w(self): # pylint: disable=invalid-name
"""Width of the box."""
return self._w
class AsciiCanvas(object):
"""Class for drawing in ASCII.
Args:
cols (int): number of columns in the canvas. Should be > 1.
lines (int): number of lines in the canvas. Should be > 1.
"""
TIMEOUT = 10
def __init__(self, cols, lines):
@@ -16,17 +54,21 @@ class AsciiCanvas(object):
self.cols = cols
self.lines = lines
self.canvas = [[' '] * cols for l in range(lines)]
self.canvas = [[" "] * cols for l in range(lines)]
def draw(self):
"""Draws ASCII canvas on the screen."""
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))
print("".join(line))
def _do_draw(self, screen): # pragma: no cover
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches, too-many-statements
from dvc.system import System
from asciimatics.event import KeyboardEvent
@@ -54,13 +96,15 @@ class AsciiCanvas(object):
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])
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(' ')
line.append(" ")
assert len(line) == (smaxcol + 1)
screen.print_at(''.join(line), 0, y)
screen.print_at("".join(line), 0, y)
screen.refresh()
@@ -79,23 +123,23 @@ class AsciiCanvas(object):
continue
k = event.key_code
if k == screen.KEY_DOWN or k == ord('s'):
if k == screen.KEY_DOWN or k == ord("s"):
offset_y += 1
elif k == screen.KEY_PAGE_DOWN or k == ord('S'):
elif k == screen.KEY_PAGE_DOWN or k == ord("S"):
offset_y += smaxrow
elif k == screen.KEY_UP or k == ord('w'):
elif k == screen.KEY_UP or k == ord("w"):
offset_y -= 1
elif k == screen.KEY_PAGE_UP or k == ord('W'):
elif k == screen.KEY_PAGE_UP or k == ord("W"):
offset_y -= smaxrow
elif k == screen.KEY_RIGHT or k == ord('d'):
elif k == screen.KEY_RIGHT or k == ord("d"):
offset_x += 1
elif k == ord('D'):
elif k == ord("D"):
offset_x += smaxcol
elif k == screen.KEY_LEFT or k == ord('a'):
elif k == screen.KEY_LEFT or k == ord("a"):
offset_x -= 1
elif k == ord('A'):
elif k == ord("A"):
offset_x -= smaxcol
elif k == ord('q') or k == ord('Q'):
elif k == ord("q") or k == ord("Q"):
break
if offset_y > max_y:
@@ -109,6 +153,17 @@ class AsciiCanvas(object):
offset_x = 0
def point(self, x, y, char):
"""Create a point on ASCII canvas.
Args:
x (int): x coordinate. Should be >= 0 and < number of columns in
the canvas.
y (int): y coordinate. Should be >= 0 an < number of lines in the
canvas.
char (str): character to place in the specified point on the
canvas.
"""
assert len(char) == 1
assert x >= 0
assert x < self.cols
assert y >= 0
@@ -117,6 +172,16 @@ class AsciiCanvas(object):
self.canvas[y][x] = char
def line(self, x0, y0, x1, y1, char):
"""Create a line on ASCII canvas.
Args:
x0 (int): x coordinate where the line should start.
y0 (int): y coordinate where the line should start.
x1 (int): x coordinate where the line should end.
y1 (int): y coordinate where the line should end.
char (str): character to draw the line with.
"""
# pylint: disable=too-many-arguments, too-many-branches
if x0 > x1:
x1, x0 = x0, x1
y1, y0 = y0, y1
@@ -149,138 +214,157 @@ class AsciiCanvas(object):
self.point(x, y, char)
def text(self, x, y, text):
for i, c in enumerate(text):
self.point(x + i, y, c)
"""Print a text on ASCII canvas.
def box(self, x0, y0, w, h):
assert w > 1
assert h > 1
Args:
x (int): x coordinate where the text should start.
y (int): y coordinate where the text should start.
text (str): string that should be printed.
"""
for i, char in enumerate(text):
self.point(x + i, y, char)
w -= 1
h -= 1
def box(self, x0, y0, width, height):
"""Create a box on ASCII canvas.
for x in range(x0, x0 + w):
self.point(x, y0, '-')
self.point(x, y0 + h, '-')
Args:
x0 (int): x coordinate of the box corner.
y0 (int): y coordinate of the box corner.
width (int): box width.
height (int): box height.
"""
assert width > 1
assert height > 1
for y in range(y0, y0 + h):
self.point(x0, y, '|')
self.point(x0 + w, y, '|')
width -= 1
height -= 1
self.point(x0, y0, '+')
self.point(x0 + w, y0, '+')
self.point(x0, y0 + h, '+')
self.point(x0 + w, y0 + h, '+')
for x in range(x0, x0 + width):
self.point(x, y0, "-")
self.point(x, y0 + height, "-")
for y in range(y0, y0 + height):
self.point(x0, y, "|")
self.point(x0 + width, y, "|")
self.point(x0, y0, "+")
self.point(x0 + width, y0, "+")
self.point(x0, y0 + height, "+")
self.point(x0 + width, y0 + height, "+")
class Dagascii(object):
def __init__(self, vertexes, edges):
#
# Just a reminder about naming conventions:
# +------------X
# |
# |
# |
# |
# Y
#
def _build_sugiyama_layout(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)
vertexes = {v: Vertex(" {} ".format(v)) for v in vertexes}
# NOTE: reverting edges to correctly orientate the graph
edges = [Edge(vertexes[e], vertexes[s]) for s, e in edges]
vertexes = vertexes.values()
graph = Graph(vertexes, edges)
class VertexViewer(object):
h = 3 # top and bottom box edges + text
for vertex in vertexes:
vertex.view = VertexViewer(vertex.data)
def __init__(self, name):
self.w = len(name) + 2 # right and left bottom edges + text
# NOTE: determine min box length to create the best layout
minw = min([v.view.w for v in vertexes])
for v in V:
v.view = VertexViewer(v.data)
for edge in edges:
edge.view = EdgeViewer()
# NOTE: determine min box length to create the best layout
minw = min([v.view.w for v in V])
sug = SugiyamaLayout(graph.C[0])
graph = graph.C[0]
roots = list(filter(lambda x: len(x.e_in()) == 0, graph.sV))
for e in E:
e.view = EdgeViewer()
sug.init_all(roots=roots, optimize=True)
sug = SugiyamaLayout(g.C[0])
gr = g.C[0]
r = list(filter(lambda x: len(x.e_in()) == 0, gr.sV))
sug.yspace = VertexViewer.HEIGHT
sug.xspace = minw
sug.route_edge = route_with_lines
sug.init_all(roots=r, optimize=True)
sug.draw()
sug.yspace = VertexViewer.h
sug.xspace = minw
sug.route_edge = route_with_lines
return sug
sug.draw()
self.sug = sug
def draw(vertexes, edges):
"""Build a DAG and draw it in ASCII.
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 = []
Args:
vertexes (list): list of graph vertexes.
edges (list): list of graph edges.
"""
# pylint: disable=too-many-locals
# NOTE: coordinates might me negative, so we need to shift
# everything to the positive plane before we actually draw it.
Xs = [] # pylint: disable=invalid-name
Ys = [] # pylint: disable=invalid-name
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)
sug = _build_sugiyama_layout(vertexes, edges)
for e in self.sug.g.sE:
for xy in e.view._pts:
Xs.append(xy[0])
Ys.append(xy[1])
for vertex in sug.g.sV:
# NOTE: moving boxes w/2 to the left
Xs.append(vertex.view.xy[0] - vertex.view.w / 2.0)
Xs.append(vertex.view.xy[0] + vertex.view.w / 2.0)
Ys.append(vertex.view.xy[1])
Ys.append(vertex.view.xy[1] + vertex.view.h)
minx = min(Xs)
miny = min(Ys)
maxx = max(Xs)
maxy = max(Ys)
for edge in sug.g.sE:
for x, y in edge.view._pts: # pylint: disable=protected-access
Xs.append(x)
Ys.append(y)
canvas_cols = int(math.ceil(math.ceil(maxx) - math.floor(minx))) + 1
canvas_lines = int(round(maxy - miny))
minx = min(Xs)
miny = min(Ys)
maxx = max(Xs)
maxy = max(Ys)
canvas = AsciiCanvas(canvas_cols, canvas_lines)
canvas_cols = int(math.ceil(math.ceil(maxx) - math.floor(minx))) + 1
canvas_lines = int(round(maxy - miny))
# 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]
canvas = AsciiCanvas(canvas_cols, canvas_lines)
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))
# NOTE: first draw edges so that node boxes could overwrite them
for edge in sug.g.sE:
# pylint: disable=protected-access
assert len(edge.view._pts) > 1
for index in range(1, len(edge.view._pts)):
start = edge.view._pts[index - 1]
end = edge.view._pts[index]
assert start_x >= 0
assert start_y >= 0
assert end_x >= 0
assert end_y >= 0
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))
canvas.line(start_x, start_y,
end_x, end_y,
'*')
assert start_x >= 0
assert start_y >= 0
assert end_x >= 0
assert end_y >= 0
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.line(start_x, start_y, end_x, end_y, "*")
canvas.box(int(round(x - minx)),
int(round(y - miny)),
v.view.w,
v.view.h)
for vertex in sug.g.sV:
# NOTE: moving boxes w/2 to the left
x = vertex.view.xy[0] - vertex.view.w / 2.0
y = vertex.view.xy[1]
canvas.text(int(round(x - minx)) + 1,
int(round(y - miny)) + 1,
v.data)
canvas.box(
int(round(x - minx)),
int(round(y - miny)),
vertex.view.w,
vertex.view.h,
)
canvas.draw()
canvas.text(
int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data
)
canvas.draw()
+97 -69
View File
@@ -1,6 +1,11 @@
from dvc.logger import Logger
"""Manages dvc remotes that user can use with push/pull/status comamnds."""
from __future__ import unicode_literals
import dvc.logger as logger
from dvc.config import Config, ConfigError
from dvc.remote import Remote
from dvc.remote.s3 import RemoteS3
from dvc.remote.gs import RemoteGS
from dvc.remote.azure import RemoteAzure
@@ -11,85 +16,85 @@ from dvc.remote.http import RemoteHTTP
class DataCloud(object):
"""Class that manages dvc remotes.
Args:
repo (dvc.repo.Repo): repo instance that belongs to the repo that
we are working on.
config (dict): config of the repo.
Raises:
config.ConfigError: thrown when config has invalid format.
"""
CLOUD_MAP = {
'aws': RemoteS3,
'gcp': RemoteGS,
'azure': RemoteAzure,
'ssh': RemoteSSH,
'hdfs': RemoteHDFS,
'local': RemoteLOCAL,
'http': RemoteHTTP,
'https': RemoteHTTP,
"aws": RemoteS3,
"gcp": RemoteGS,
"azure": RemoteAzure,
"ssh": RemoteSSH,
"hdfs": RemoteHDFS,
"local": RemoteLOCAL,
"http": RemoteHTTP,
"https": RemoteHTTP,
}
def __init__(self, project, config=None):
self.project = project
def __init__(self, repo, config=None):
self.repo = repo
self._config = config
self._core = self._config[Config.SECTION_CORE]
@property
def _cloud(self):
remote = self._core.get(Config.SECTION_CORE_REMOTE, '')
if remote != '':
remote = self._core.get(Config.SECTION_CORE_REMOTE, "")
if remote != "":
return self._init_remote(remote)
if self._core.get(Config.SECTION_CORE_CLOUD, None):
# backward compatibility
msg = 'Using obsoleted config format. Consider updating.'
Logger.warn(msg)
msg = "using obsoleted config format. Consider updating."
logger.warning(msg)
return self._init_compat()
return None
@staticmethod
def supported(config):
for cloud in DataCloud.CLOUD_MAP.values():
if cloud.supported(config):
return cloud
return None
def _init_remote(self, remote):
section = Config.SECTION_REMOTE_FMT.format(remote)
section = Config.SECTION_REMOTE_FMT.format(remote).lower()
cloud_config = self._config.get(section, None)
if not cloud_config:
msg = "Can't find remote section '{}' in config"
msg = "can't find remote section '{}' in config"
raise ConfigError(msg.format(section))
cloud_type = self.supported(cloud_config)
if not cloud_type:
raise ConfigError("Unsupported cloud '{}'".format(cloud_config))
return self._init_cloud(cloud_config, cloud_type)
return Remote(self.repo, cloud_config)
def _init_compat(self):
name = self._core.get(Config.SECTION_CORE_CLOUD, '').strip().lower()
if name == '':
name = self._core.get(Config.SECTION_CORE_CLOUD, "").strip().lower()
if name == "":
self._cloud = None
return
return None
cloud_type = self.CLOUD_MAP.get(name, None)
if not cloud_type:
msg = "Wrong cloud type {} specified"
raise ConfigError(msg.format(name))
msg = "wrong cloud type '{}' specified".format(name)
raise ConfigError(msg)
cloud_config = self._config.get(name, None)
if not cloud_config:
msg = "Can't find cloud section '{}' in config"
raise ConfigError(msg.format(name))
msg = "can't find cloud section '{}' in config".format(name)
raise ConfigError(msg)
# 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))
raise ConfigError("unsupported cloud '{}'".format(name))
return self._init_cloud(cloud_config, cloud_type)
def _init_cloud(self, cloud_config, cloud_type):
global_storage_path = self._core.get(Config.SECTION_CORE_STORAGEPATH)
if global_storage_path:
Logger.warn('Using obsoleted config format. Consider updating.')
logger.warning("using obsoleted config format. Consider updating.")
cloud = cloud_type(self.project, cloud_config)
cloud = cloud_type(self.repo, cloud_config)
return cloud
def _get_cloud(self, remote, cmd):
@@ -99,39 +104,62 @@ class DataCloud(object):
if self._cloud:
return self._cloud
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)
raise ConfigError(
"No remote repository specified. Setup default repository with\n"
" dvc config core.remote <name>\n"
"or use:\n"
" dvc {} -r <name>\n".format(cmd)
)
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,
'push'),
show_checksums=show_checksums)
def push(self, targets, jobs=None, remote=None, show_checksums=False):
"""Push data items in a cloud-agnostic way.
def pull(self, targets, jobs=1, remote=None, show_checksums=False):
Args:
targets (list): list of targets to push to the cloud.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.RemoteBase): optional remote to push to.
By default remote from core.remote config option is used.
show_checksums (bool): show checksums instead of file names in
information messages.
"""
Pull data items in a cloud-agnostic way.
"""
return self.project.cache.local.pull(targets,
jobs=jobs,
remote=self._get_cloud(remote,
'pull'),
show_checksums=show_checksums)
return self.repo.cache.local.push(
targets,
jobs=jobs,
remote=self._get_cloud(remote, "push"),
show_checksums=show_checksums,
)
def status(self, targets, jobs=1, remote=None, show_checksums=False):
def pull(self, targets, jobs=None, remote=None, show_checksums=False):
"""Pull data items in a cloud-agnostic way.
Args:
targets (list): list of targets to pull from the cloud.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.RemoteBase): optional remote to pull from.
By default remote from core.remote config option is used.
show_checksums (bool): show checksums instead of file names in
information messages.
"""
Check status of data items in a cloud-agnostic way.
return self.repo.cache.local.pull(
targets,
jobs=jobs,
remote=self._get_cloud(remote, "pull"),
show_checksums=show_checksums,
)
def status(self, targets, jobs=None, remote=None, show_checksums=False):
"""Check status of data items in a cloud-agnostic way.
Args:
targets (list): list of targets to check status for.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.RemoteBase): optional remote to compare
targets to. By default remote from core.remote config option
is used.
show_checksums (bool): show checksums instead of file names in
information messages.
"""
cloud = self._get_cloud(remote, 'status')
return self.project.cache.local.status(targets,
jobs=jobs,
remote=cloud,
show_checksums=show_checksums)
cloud = self._get_cloud(remote, "status")
return self.repo.cache.local.status(
targets, jobs=jobs, remote=cloud, show_checksums=show_checksums
)
+26 -30
View File
@@ -1,14 +1,12 @@
from __future__ import unicode_literals
import schema
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from dvc.exceptions import DvcException
from dvc.config import Config
from dvc.utils.compat import urlparse
from dvc.dependency.base import DependencyBase
import dvc.output as output
from dvc.output.base import OutputBase
from dvc.dependency.s3 import DependencyS3
from dvc.dependency.gs import DependencyGS
from dvc.dependency.local import DependencyLOCAL
@@ -17,56 +15,54 @@ from dvc.dependency.ssh import DependencySSH
from dvc.dependency.http import DependencyHTTP
from dvc.remote import Remote
from dvc.remote.local import RemoteLOCAL
from dvc.remote.s3 import RemoteS3
from dvc.remote.hdfs import RemoteHDFS
DEPS = [
DependencyGS,
DependencyHDFS,
DependencyHTTP,
DependencyLOCAL,
DependencyS3,
DependencySSH,
# NOTE: DependencyLOCAL is the default choice
]
DEP_MAP = {
'': DependencyLOCAL,
'ssh': DependencySSH,
's3': DependencyS3,
'gs': DependencyGS,
'hdfs': DependencyHDFS,
'http': DependencyHTTP,
'https': DependencyHTTP,
"local": DependencyLOCAL,
"ssh": DependencySSH,
"s3": DependencyS3,
"gs": DependencyGS,
"hdfs": DependencyHDFS,
"http": DependencyHTTP,
"https": DependencyHTTP,
}
# We are skipping RemoteHTTP.PARAM_ETAG because is the same as RemoteS3
SCHEMA = {
DependencyBase.PARAM_PATH: str,
schema.Optional(RemoteLOCAL.PARAM_MD5): schema.Or(str, None),
schema.Optional(RemoteS3.PARAM_ETAG): schema.Or(str, None),
schema.Optional(RemoteHDFS.PARAM_CHECKSUM): schema.Or(str, None),
}
# NOTE: schema for dependencies is basically the same as for outputs, but
# without output-specific entries like 'cache' (whether or not output is
# cached, see -o and -O flags for `dvc run`) and 'metric' (whether or not
# output is a metric file and how to parse it, see `-M` flag for `dvc run`).
SCHEMA = output.SCHEMA.copy()
del SCHEMA[schema.Optional(OutputBase.PARAM_CACHE)]
del SCHEMA[schema.Optional(OutputBase.PARAM_METRIC)]
def _get(stage, p, info):
parsed = urlparse(p)
if parsed.scheme == 'remote':
if parsed.scheme == "remote":
name = Config.SECTION_REMOTE_FMT.format(parsed.netloc)
sect = stage.project.config._config[name]
remote = Remote(stage.project, sect)
sect = stage.repo.config.config[name]
remote = Remote(stage.repo, sect)
return DEP_MAP[remote.scheme](stage, p, info, remote=remote)
for d in DEPS:
if d.supported(p):
return d(stage, p, info)
raise DvcException('Dependency \'{}\' is not supported'.format(p))
return DependencyLOCAL(stage, p, info)
def loadd_from(stage, d_list):
ret = []
for d in d_list:
p = d.pop(DependencyBase.PARAM_PATH)
p = d.pop(OutputBase.PARAM_PATH)
ret.append(_get(stage, p, d))
return ret
+10 -67
View File
@@ -1,79 +1,22 @@
import re
from __future__ import unicode_literals
from dvc.exceptions import DvcException
class DependencyError(DvcException):
def __init__(self, path, msg):
msg = 'Dependency \'{}\' error: {}'.format(path, msg)
super(DependencyError, self).__init__(msg)
class DependencyDoesNotExistError(DependencyError):
class DependencyDoesNotExistError(DvcException):
def __init__(self, path):
msg = 'does not exist'
super(DependencyDoesNotExistError, self).__init__(path, msg)
msg = "dependency '{}' does not exist".format(path)
super(DependencyDoesNotExistError, self).__init__(msg)
class DependencyIsNotFileOrDirError(DependencyError):
class DependencyIsNotFileOrDirError(DvcException):
def __init__(self, path):
msg = 'not a file or directory'
super(DependencyIsNotFileOrDirError, self).__init__(path, msg)
msg = "dependency '{}' is not a file or directory".format(path)
super(DependencyIsNotFileOrDirError, self).__init__(msg)
class DependencyBase(object):
REGEX = None
IS_DEPENDENCY = True
PARAM_PATH = 'path'
def __init__(self, stage, path):
self.stage = stage
self.project = stage.project
self.path = path
def __str__(self):
return self.path
@classmethod
def match(cls, url):
return re.match(cls.REGEX, url)
def group(self, name):
match = self.match(self.path)
if not match:
return None
return match.group(name)
@classmethod
def supported(cls, url):
return cls.match(url) is not None
@property
def sep(self):
return '/'
@property
def exists(self):
return self.remote.exists([self.path_info])[0]
def changed(self):
raise NotImplementedError
def status(self):
if self.changed():
# FIXME better msgs
if self.path_info['scheme'] == 'local':
p = self.rel_path
else:
p = self.path
return {p: 'changed'}
return {}
def save(self):
raise NotImplementedError
def dumpd(self):
return {self.PARAM_PATH: self.path}
def download(self, to_info):
self.remote.download([self.path_info], [to_info])
DoesNotExistError = DependencyDoesNotExistError
IsNotFileOrDirError = DependencyIsNotFileOrDirError
+5 -22
View File
@@ -1,25 +1,8 @@
import posixpath
from __future__ import unicode_literals
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from dvc.dependency.s3 import DependencyS3
from dvc.remote.gs import RemoteGS
from dvc.output.gs import OutputGS
from dvc.dependency.base import DependencyBase
class DependencyGS(DependencyS3):
REGEX = RemoteGS.REGEX
def __init__(self, stage, path, info=None, remote=None):
super(DependencyGS, self).__init__(stage, path)
self.info = info
self.remote = remote if remote else RemoteGS(stage.project, {})
bucket = remote.bucket if remote else urlparse(path).netloc
key = urlparse(path).path.lstrip('/')
if remote:
key = posixpath.join(remote.prefix, key)
self.path_info = {'scheme': 'gs',
'bucket': bucket,
'key': key}
class DependencyGS(DependencyBase, OutputGS):
pass
+4 -34
View File
@@ -1,38 +1,8 @@
import posixpath
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from __future__ import unicode_literals
from dvc.output.hdfs import OutputHDFS
from dvc.dependency.base import DependencyBase
from dvc.remote.hdfs import RemoteHDFS
class DependencyHDFS(DependencyBase):
REGEX = RemoteHDFS.REGEX
def __init__(self, stage, path, info=None, remote=None):
super(DependencyHDFS, self).__init__(stage, path)
self.info = info
self.remote = remote if remote else RemoteHDFS(stage.project, {})
if remote:
path = posixpath.join(remote.url, urlparse(path).path.lstrip('/'))
user = remote.user if remote else self.group('user')
self.path_info = {'scheme': 'hdfs',
'user': user,
'url': path}
def changed(self):
if not self.exists:
return True
return self.info != self.remote.save_info(self.path_info)
def save(self):
self.info = self.remote.save_info(self.path_info)
def dumpd(self):
ret = self.info
ret[self.PARAM_PATH] = self.path
return ret
class DependencyHDFS(DependencyBase, OutputHDFS):
pass
+11 -31
View File
@@ -1,39 +1,19 @@
try:
from urlparse import urlparse, urljoin
except ImportError:
from urllib.parse import urlparse, urljoin
from __future__ import unicode_literals
from dvc.dependency.base import DependencyBase
from dvc.utils.compat import urlparse, urljoin
from dvc.output.base import OutputBase
from dvc.remote.http import RemoteHTTP
from dvc.dependency.base import DependencyBase
class DependencyHTTP(DependencyBase):
REGEX = RemoteHTTP.REGEX
class DependencyHTTP(DependencyBase, OutputBase):
REMOTE = RemoteHTTP
def __init__(self, stage, path, info=None, remote=None):
super(DependencyHTTP, self).__init__(stage, path)
self.info = info
self.remote = remote or RemoteHTTP(stage.project, {})
if path.startswith('remote'):
super(DependencyHTTP, self).__init__(
stage, path, info=info, remote=remote
)
if path.startswith("remote"):
path = urljoin(self.remote.cache_dir, urlparse(path).path)
self.path_info = {
'scheme': urlparse(path).scheme,
'url': path,
}
def save(self):
self.info = self.remote.save_info(self.path_info)
def dumpd(self):
ret = self.info
ret[self.PARAM_PATH] = self.path
return ret
def changed(self):
if not self.exists:
return True
return self.info != self.remote.save_info(self.path_info)
self.path_info = {"scheme": urlparse(path).scheme, "path": path}
+4 -85
View File
@@ -1,89 +1,8 @@
import os
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from __future__ import unicode_literals
from dvc.dependency.base import DependencyBase
from dvc.dependency.base import DependencyDoesNotExistError
from dvc.dependency.base import DependencyIsNotFileOrDirError
from dvc.logger import Logger
from dvc.remote.local import RemoteLOCAL
from dvc.output.local import OutputLOCAL
class DependencyLOCAL(DependencyBase):
REGEX = r'^(?P<path>(/+|.:\\+)?[^:]*)$'
DoesNotExistError = DependencyDoesNotExistError
IsNotFileOrDirError = DependencyIsNotFileOrDirError
def __init__(self, stage, path, info=None, remote=None):
self.stage = stage
self.project = stage.project
self.info = info
if remote is not None:
self.remote = remote
else:
self.remote = RemoteLOCAL(stage.project, {})
if remote:
path = os.path.join(remote.prefix, urlparse(path).path.lstrip('/'))
if not os.path.isabs(path):
path = self.remote.ospath(path)
path = os.path.join(stage.cwd, path)
self.path = os.path.abspath(os.path.normpath(path))
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
@property
def rel_path(self):
return os.path.relpath(self.path)
def changed(self):
if not self.exists:
return True
info = self.remote.save_info(self.path_info)
return self.info != info
def save(self):
if not self.exists:
raise self.DoesNotExistError(self.rel_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 \
(os.path.isdir(self.path) and len(os.listdir(self.path)) == 0):
Logger.warn("File/directory '{}' is empty.".format(self.rel_path))
self.info = self.remote.save_info(self.path_info)
def dumpd(self):
if self.is_local:
path = self.remote.unixpath(os.path.relpath(self.path,
self.stage.cwd))
else:
path = self.path
info = self.info.copy()
info[self.PARAM_PATH] = path
return info
class DependencyLOCAL(DependencyBase, OutputLOCAL):
pass
+4 -36
View File
@@ -1,40 +1,8 @@
import posixpath
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from __future__ import unicode_literals
from dvc.output.s3 import OutputS3
from dvc.dependency.base import DependencyBase
from dvc.remote.s3 import RemoteS3
class DependencyS3(DependencyBase):
REGEX = RemoteS3.REGEX
def __init__(self, stage, path, info=None, remote=None):
super(DependencyS3, self).__init__(stage, path)
self.info = info
self.remote = remote or RemoteS3(stage.project, {})
bucket = remote.bucket if remote else urlparse(path).netloc
key = urlparse(path).path.lstrip('/')
if remote:
key = posixpath.join(remote.prefix, key)
self.path_info = {'scheme': 's3',
'bucket': bucket,
'key': key}
def changed(self):
if not self.exists:
return True
return self.info != self.remote.save_info(self.path_info)
def save(self):
self.info = self.remote.save_info(self.path_info)
def dumpd(self):
ret = self.info
ret[self.PARAM_PATH] = self.path
return ret
class DependencyS3(DependencyBase, OutputS3):
pass
+4 -43
View File
@@ -1,47 +1,8 @@
import posixpath
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from __future__ import unicode_literals
from dvc.output.ssh import OutputSSH
from dvc.dependency.base import DependencyBase
from dvc.remote.ssh import RemoteSSH
class DependencySSH(DependencyBase):
REGEX = RemoteSSH.REGEX
def __init__(self, stage, path, info=None, remote=None):
super(DependencySSH, self).__init__(stage, path)
self.info = info
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,
urlparse(path).path.lstrip('/'))
else:
path = self.match(self.path).group('path')
self.path_info = {'scheme': 'ssh',
'host': host,
'port': port,
'user': user,
'path': path}
def changed(self):
if not self.exists:
return True
return self.info != self.remote.save_info(self.path_info)
def save(self):
self.info = self.remote.save_info(self.path_info)
def dumpd(self):
ret = self.info
ret[self.PARAM_PATH] = self.path
return ret
class DependencySSH(DependencyBase, OutputSSH):
pass
+216 -36
View File
@@ -1,70 +1,250 @@
"""Exceptions raised by the dvc."""
from __future__ import unicode_literals
from dvc.utils.compat import str, builtin_str
import os
import traceback
class DvcException(Exception):
"""Base class for all dvc exceptions.
Args:
msg (unicode): message for this exception.
cause (Exception): optional cause exception.
"""
def __init__(self, msg, cause=None):
# NOTE: unlike python 3, python 2 doesn't have built-in support
# for chained exceptions, so we are using our own implementation.
self.cause = cause
self.cause_tb = None
if cause:
try:
self.cause_tb = traceback.format_exc()
except Exception: # pragma: no cover
except AttributeError: # pragma: no cover
pass
super(DvcException, self).__init__(msg)
class UnsupportedRemoteError(DvcException):
def __init__(self, config):
msg = "Remote '{}' is not supported.".format(config)
super(UnsupportedRemoteError, self).__init__(msg)
class OutputDuplicationError(DvcException):
"""Thrown if a file/directory is specified as an output in more than one
stage.
Args:
output (unicode): path to the file/directory.
stages (list): list of paths to stages.
"""
def __init__(self, output, stages):
assert isinstance(output, str)
assert isinstance(output, str) or isinstance(output, builtin_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:{}"
s = ""
for stage in stages:
s += "\n " + stage
super(OutputDuplicationError, self).__init__(msg.format(output, s))
class WorkingDirectoryAsOutputError(DvcException):
def __init__(self, cwd, fname):
assert isinstance(cwd, str)
assert isinstance(fname, str)
msg = (
"Current working directory '{cwd}' is specified as an output in"
" '{fname}'. Use another CWD to prevent any data removal."
.format(cwd=cwd, fname=fname)
assert all(
isinstance(stage, str) or isinstance(stage, builtin_str)
for stage in stages
)
super(WorkingDirectoryAsOutputError, self).__init__(msg)
msg = (
"file/directory '{}' is specified as an output in more than one"
"stage: {}"
).format(output, "\n ".join(stages))
super(OutputDuplicationError, self).__init__(msg)
class OutputNotFoundError(DvcException):
"""Thrown if a file/directory not found in repository pipelines.
Args:
output (unicode): path to the file/directory.
"""
def __init__(self, output):
super(OutputNotFoundError, self).__init__(
"unable to find stage file with output '{path}'".format(
path=os.path.relpath(output)
)
)
class StagePathAsOutputError(DvcException):
"""Thrown if directory that stage is going to be saved in is specified as
an output of another stage.
Args:
cwd (str): path to the directory.
fname (str): path to the stage file that has cwd specified as an
output.
"""
def __init__(self, wdir, fname):
assert isinstance(wdir, str) or isinstance(wdir, builtin_str)
assert isinstance(fname, str) or isinstance(fname, builtin_str)
msg = (
"current working directory '{cwd}' is specified as an output in"
" '{fname}'. Use another CWD to prevent any data removal.".format(
cwd=wdir, fname=fname
)
)
super(StagePathAsOutputError, self).__init__(msg)
class CircularDependencyError(DvcException):
"""Thrown if a file/directory specified both as an output and as a
dependency.
Args:
dependency (str): path to the dependency.
"""
def __init__(self, dependency):
assert isinstance(dependency, str)
msg = "File '{}' is specified as an output and as a dependency."
assert isinstance(dependency, str) or isinstance(
dependency, builtin_str
)
msg = (
"file/directory '{}' is specified as an output and as a "
"dependency."
)
super(CircularDependencyError, self).__init__(msg.format(dependency))
class MoveNotDataSourceError(DvcException):
class ArgumentDuplicationError(DvcException):
"""Thrown if a file/directory is specified as a dependency/output more
than once.
Args:
path (str): path to the file/directory.
"""
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."
assert isinstance(path, str) or isinstance(path, builtin_str)
msg = "file '{}' is specified more than once."
super(ArgumentDuplicationError, self).__init__(msg.format(path))
class MoveNotDataSourceError(DvcException):
"""Thrown if attempted to move a file/directory that is not an output
in a data source stage.
Args:
path (str): path to the file/directory.
"""
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))
class NotDvcProjectError(DvcException):
class NotDvcRepoError(DvcException):
"""Thrown if a directory is not a dvc repo.
Args:
root (str): path to the directory.
"""
def __init__(self, root):
msg = "Not a dvc repository (checked up to mount point {})"
super(NotDvcProjectError, self).__init__(msg.format(root))
msg = (
"you are not inside of a dvc repository "
"(checked up to mount point '{}')"
)
super(NotDvcRepoError, self).__init__(msg.format(root))
class DvcParserError(DvcException):
"""Base class for CLI parser errors."""
def __init__(self):
super(DvcException, self).__init__("Parser error")
super(DvcParserError, self).__init__("parser error")
class CyclicGraphError(DvcException):
def __init__(self, stages):
assert isinstance(stages, list)
stages = "\n".join("\t- {}".format(stage) for stage in stages)
msg = (
"you've introduced a cycle in your pipeline that involves"
" the following stages:"
"\n"
"{stages}".format(stages=stages)
)
super(CyclicGraphError, self).__init__(msg)
class ConfirmRemoveError(DvcException):
def __init__(self, path):
super(ConfirmRemoveError, self).__init__(
"unable to remove '{}' without a confirmation from the user. Use "
"'-f' to force.".format(path)
)
class InitError(DvcException):
def __init__(self, msg):
super(InitError, self).__init__(msg)
class ReproductionError(DvcException):
def __init__(self, dvc_file_name, ex):
self.path = dvc_file_name
msg = "failed to reproduce '{}'".format(dvc_file_name)
super(ReproductionError, self).__init__(msg, cause=ex)
class BadMetricError(DvcException):
def __init__(self, path):
super(BadMetricError, self).__init__(
"'{}' does not exist, not a metric or is malformed".format(
os.path.relpath(path)
)
)
class NoMetricsError(DvcException):
def __init__(self):
super(NoMetricsError, self).__init__(
"no metric files in this repository. "
"Use 'dvc metrics add' to add a metric file to track."
)
class StageFileCorruptedError(DvcException):
def __init__(self, path):
path = os.path.relpath(path)
super(StageFileCorruptedError, self).__init__(
"unable to read stage file: {} "
"YAML file structure is corrupted".format(path)
)
class RecursiveAddingWhileUsingFilename(DvcException):
def __init__(self):
super(RecursiveAddingWhileUsingFilename, self).__init__(
"using fname with recursive is not allowed."
)
class OverlappingOutputPathsError(DvcException):
def __init__(self, out_1, out_2):
super(OverlappingOutputPathsError, self).__init__(
"Paths for outs:\n'{}'('{}')\n'{}'('{}')\noverlap. To avoid "
"unpredictable behaviour, rerun command with non overlapping outs "
"paths.".format(
str(out_1),
out_1.stage.relpath,
str(out_2),
out_2.stage.relpath,
)
)
class TargetNotDirectoryError(DvcException):
def __init__(self, path):
super(TargetNotDirectoryError, self).__init__(
"Target: {} is not a directory".format(path)
)
+20 -14
View File
@@ -1,19 +1,24 @@
"""Use heuristics to guess if it is a text file or a binary file."""
from __future__ import unicode_literals
from dvc.utils.compat import is_py3, open
# Based on https://eli.thegreenplace.net/2011/10/19/
# perls-guess-if-file-is-text-or-binary-implemented-in-python
import sys
PY3 = sys.version_info[0] == 3
# A function that takes an integer in the 8-bit range and returns
# a single-character byte object in py3 / a single-character string
# in py2.
#
int2byte = (lambda x: bytes((x,))) if PY3 else chr
def _int2byte(i):
if is_py3:
return bytes((i,))
return chr(i)
_text_characters = (
b''.join(int2byte(i) for i in range(32, 127)) +
b'\n\r\t\f\b')
TEXT_CHARS = b"".join(_int2byte(i) for i in range(32, 127)) + b"\n\r\t\f\b"
def istextfile(fname, blocksize=512):
@@ -22,17 +27,18 @@ def istextfile(fname, blocksize=512):
If more than 30% of the chars in the block are non-text, or there
are NUL ('\x00') bytes in the block, assume this is a binary file.
"""
with open(fname, 'rb') as fd:
block = fd.read(blocksize)
with open(fname, "rb") as fobj:
block = fobj.read(blocksize)
if b'\x00' in block:
# Files with null bytes are binary
return False
elif not block:
if not block:
# An empty file is considered a valid text file
return True
if b"\x00" in block:
# Files with null bytes are binary
return False
# Use translate's 'deletechars' argument to efficiently remove all
# occurrences of _text_characters from the block
nontext = block.translate(None, _text_characters)
# occurrences of TEXT_CHARS from the block
nontext = block.translate(None, TEXT_CHARS)
return float(len(nontext)) / len(block) <= 0.30
+21 -5
View File
@@ -1,3 +1,7 @@
"""Manages dvc lock file."""
from __future__ import unicode_literals
import os
import time
import zc.lockfile
@@ -6,11 +10,19 @@ from dvc.exceptions import DvcException
class LockError(DvcException):
pass
"""Thrown when unable to acquire the lock for dvc repo."""
class Lock(object):
LOCK_FILE = 'lock'
"""Class for dvc repo lock.
Args:
dvc_dir (str): path to the directory that the lock should be created
in.
name (str): name of the lock file.
"""
LOCK_FILE = "lock"
TIMEOUT = 5
def __init__(self, dvc_dir, name=LOCK_FILE):
@@ -21,10 +33,13 @@ class Lock(object):
try:
self._lock = zc.lockfile.LockFile(self.lock_file)
except zc.lockfile.LockError:
raise LockError('Cannot perform the cmd since DVC is busy and '
'locked. Please retry the cmd later.')
raise LockError(
"cannot perform the cmd since DVC is busy and "
"locked. Please retry the cmd later."
)
def lock(self):
"""Acquire lock for dvc repo."""
try:
self._do_lock()
return
@@ -34,11 +49,12 @@ class Lock(object):
self._do_lock()
def unlock(self):
"""Release lock for dvc repo."""
self._lock.close()
self._lock = None
def __enter__(self):
self.lock()
def __exit__(self, type, value, tb):
def __exit__(self, typ, value, tbck):
self.unlock()
+269 -206
View File
@@ -1,252 +1,315 @@
"""Manages logger for dvc repo."""
from __future__ import unicode_literals
from dvc.exceptions import DvcException
from dvc.utils.compat import str
from dvc.progress import progress_aware
import re
import sys
import logging
import colorama
import traceback
import re
from contextlib import contextmanager
import colorama
colorama.init()
@progress_aware
def info(message):
"""Prints an info message."""
logger.info(message)
def visual_width(line):
""" Get the the number of columns required to display a string """
# Do not annotate debug with progress_aware. Why? Debug messages are called
# in analytics.py. Trigger introduced by @progress_aware may run even
# if log_level != DEBUG (look at Progress.print) Which might result in
# messy terminal after any command (because analytics run in separate thread)
def debug(message):
"""Prints a debug message."""
prefix = colorize("Debug", color="blue")
return len(re.sub(colorama.ansitowin32.AnsiToWin32.ANSI_CSI_RE, '', line))
out = "{prefix}: {message}".format(prefix=prefix, message=message)
logger.debug(out)
def visual_center(line, width):
""" Center align string according to it's visual width """
@progress_aware
def warning(message, parse_exception=False):
"""Prints a warning message."""
prefix = colorize("Warning", color="yellow")
spaces = max(width - visual_width(line), 0)
left_padding = int(spaces / 2)
right_padding = (spaces - left_padding)
exception, stack_trace = None, ""
if parse_exception:
exception, stack_trace = _parse_exc()
return (left_padding * ' ') + line + (right_padding * ' ')
out = "{prefix}: {description}".format(
prefix=prefix, description=_description(message, exception)
)
if stack_trace:
out += "\n{stack_trace}".format(stack_trace=stack_trace)
logger.warning(out)
class Logger(object):
FMT = '%(message)s'
DEFAULT_LEVEL = logging.INFO
@progress_aware
def error(message=None):
"""Prints an error message."""
prefix = colorize("Error", color="red")
LEVEL_MAP = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warn': logging.WARNING,
'error': logging.ERROR
exception, stack_trace = _parse_exc()
out = (
"{prefix}: {description}"
"\n"
"{stack_trace}"
"\n"
"{footer}".format(
prefix=prefix,
description=_description(message, exception),
stack_trace=stack_trace,
footer=_footer(),
)
)
logger.error(out)
def box(message, border_color=None):
"""Prints a message in a box.
Args:
message (unicode): message to print.
border_color (unicode): name of a color to outline the box with.
"""
lines = message.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=colorize(chars["vertical"], color=border_color),
space=chars["empty"] * box_size_horizontal,
)
* padding_vertical
]
content_lines = [
"{border}{space}{content}{space}{border}\n".format(
border=colorize(chars["vertical"], color=border_color),
space=chars["empty"] * padding_horizontal,
content=_visual_center(line, max_width),
)
for line in lines
]
box_str = "{margin}{padding}{content}{padding}{margin}".format(
margin=colorize(margin, color=border_color),
padding="".join(padding_lines),
content="".join(content_lines),
)
logger.info(box_str)
def level():
"""Returns current log level."""
return logger.getEffectiveLevel()
def set_level(level_name):
"""Sets log level.
Args:
level_name (str): log level name. E.g. info, debug, warning, error,
critical.
"""
if not level_name:
return
levels = {
"info": logging.INFO,
"debug": logging.DEBUG,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL,
}
COLOR_MAP = {
'green': colorama.Fore.GREEN,
'yellow': colorama.Fore.YELLOW,
'blue': colorama.Fore.BLUE,
'red': colorama.Fore.RED,
logger.setLevel(levels.get(level_name))
def be_quiet():
"""Disables all messages except critical ones."""
logger.setLevel(logging.CRITICAL)
def be_verbose():
"""Enables all messages."""
logger.setLevel(logging.DEBUG)
@contextmanager
def verbose():
"""Enables verbose mode for the context."""
previous_level = level()
be_verbose()
yield
logger.setLevel(previous_level)
@contextmanager
def quiet():
"""Enables quiet mode for the context."""
previous_level = level()
be_quiet()
yield
logger.setLevel(previous_level)
def is_quiet():
"""Returns whether or not all messages except critical ones are
disabled.
"""
return level() == logging.CRITICAL
def is_verbose():
"""Returns whether or not all messages are enabled."""
return level() == logging.DEBUG
def colorize(message, color=None):
"""Returns a message in a specified color."""
if not color:
return message
colors = {
"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',
}
return "{color}{message}{nc}".format(
color=colors.get(color, ""), message=message, nc=colorama.Fore.RESET
)
def __init__(self, loglevel=None):
if loglevel:
Logger.set_level(loglevel)
@staticmethod
def init():
def set_default_level():
"""Sets default log level."""
logger.setLevel(logging.INFO)
class LogLevelFilter(logging.Filter):
def filter(self, record):
return record.levelno <= logging.WARNING
if Logger._already_initialized():
return
def _add_handlers():
# NOTE: We need to initialize colorama before setting the stream handlers
# so it can wrap stdout/stderr and convert color codes to Windows.
colorama.init()
sh_out = logging.StreamHandler(sys.stdout)
sh_out.setFormatter(logging.Formatter(Logger.FMT))
sh_out.setLevel(logging.DEBUG)
sh_out.addFilter(LogLevelFilter())
formatter = "%(message)s"
sh_err = logging.StreamHandler(sys.stderr)
sh_err.setFormatter(logging.Formatter(Logger.FMT))
sh_err.setLevel(logging.ERROR)
class _LogLevelFilter(logging.Filter):
# pylint: disable=too-few-public-methods
def filter(self, record):
return record.levelno <= logging.WARNING
Logger.logger().addHandler(sh_out)
Logger.logger().addHandler(sh_err)
Logger.set_level()
sh_out = logging.StreamHandler(sys.stdout)
sh_out.setFormatter(logging.Formatter(formatter))
sh_out.setLevel(logging.DEBUG)
sh_out.addFilter(_LogLevelFilter())
@staticmethod
def logger():
return logging.getLogger('dvc')
sh_err = logging.StreamHandler(sys.stderr)
sh_err.setFormatter(logging.Formatter(formatter))
sh_err.setLevel(logging.ERROR)
@staticmethod
def set_level(level=None):
if not level:
lvl = Logger.DEFAULT_LEVEL
else:
lvl = Logger.LEVEL_MAP.get(level.lower(), Logger.DEFAULT_LEVEL)
Logger.logger().setLevel(lvl)
logger.addHandler(sh_out)
logger.addHandler(sh_err)
@staticmethod
def be_quiet():
Logger.logger().setLevel(logging.CRITICAL)
@staticmethod
def be_verbose():
Logger.logger().setLevel(logging.DEBUG)
def _walk_exc(exc):
exc_list = [str(exc)]
tb_list = [traceback.format_exc()]
@staticmethod
def colorize(msg, color):
header = ''
footer = ''
# NOTE: parsing chained exceptions. See dvc/exceptions.py for more info.
while hasattr(exc, "cause") and exc.cause is not None:
exc_list.append(str(exc.cause))
if hasattr(exc, "cause_tb") and exc.cause_tb is not None:
tb_list.insert(0, str(exc.cause_tb))
exc = exc.cause
if sys.stdout.isatty(): # pragma: no cover
header = Logger.COLOR_MAP.get(color.lower(), '')
footer = colorama.Style.RESET_ALL
return exc_list, tb_list
return u'{}{}{}'.format(header, msg, footer)
@staticmethod
def parse_exc(exc, tb=None):
str_tb = tb if tb else None
str_exc = str(exc) if exc else ""
l_str_exc = []
def _parse_exc():
exc = sys.exc_info()[1]
if not exc:
return (None, "")
if len(str_exc) != 0:
l_str_exc.append(str_exc)
exc_list, tb_list = _walk_exc(exc)
if exc and hasattr(exc, 'cause') and exc.cause:
cause_tb = exc.cause_tb if hasattr(exc, 'cause_tb') else None
l_cause_str_exc, cause_str_tb = Logger.parse_exc(exc.cause,
cause_tb)
exception = ": ".join(exc_list)
str_tb = cause_str_tb
l_str_exc += l_cause_str_exc
if is_verbose():
stack_trace = "{line}\n{stack_trace}{line}\n".format(
line=colorize("-" * 60, color="red"),
stack_trace="\n".join(tb_list),
)
else:
stack_trace = ""
return (l_str_exc, str_tb)
return (exception, stack_trace)
@staticmethod
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 _with_exc(func, prefix, msg, suffix="", exc=None):
l_str_exc, str_tb = Logger.parse_exc(exc)
if exc is not None and Logger.is_verbose():
str_tb = str_tb if str_tb else traceback.format_exc()
Logger._with_progress(Logger.logger().error, str_tb)
l_msg = [prefix]
if msg is not None and len(msg) != 0:
l_msg.append(msg)
l_msg += l_str_exc
Logger._with_progress(func, ': '.join(l_msg) + suffix)
@staticmethod
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):
Logger._with_progress(Logger.logger().info, msg)
@staticmethod
def is_quiet():
return Logger.logger().level == logging.CRITICAL
@staticmethod
def is_verbose():
return Logger.logger().getEffectiveLevel() == logging.DEBUG
@classmethod
def box(cls, msg, border_color=''):
if cls.is_quiet():
return
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,
def _description(message, exception):
if exception and message:
description = "{message} - {exception}"
elif exception:
description = "{exception}"
elif message:
description = "{message}"
else:
raise DvcException(
"Unexpected error - either exception or message must be provided"
)
padding_lines = [
"{border}{space}{border}\n".format(
border=cls.colorize(chars['vertical'], border_color),
space=chars['empty'] * box_size_horizontal,
) * padding_vertical
]
return description.format(message=message, exception=exception)
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),
)
def _footer():
return "{phrase} Hit us up at {url}, we are always happy to help!".format(
phrase=colorize("Having any troubles?", "yellow"),
url=colorize("https://dvc.org/support", "blue"),
)
print(box)
@staticmethod
def _already_initialized():
return bool(Logger.logger().handlers)
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 * " ")
logger = logging.getLogger("dvc") # pylint: disable=invalid-name
set_default_level()
_add_handlers()
+20 -10
View File
@@ -1,13 +1,23 @@
from dvc.logger import Logger
"""Main entry point for dvc CLI."""
from __future__ import unicode_literals
import dvc.logger as logger
from dvc.cli import parse_args
from dvc.command.base import CmdBase
from dvc.analytics import Analytics
from dvc.exceptions import NotDvcProjectError, DvcParserError
from dvc.exceptions import NotDvcRepoError, DvcParserError
def main(argv=None):
Logger.init()
"""Run dvc CLI command.
Args:
argv: optional list of arguments to parse. sys.argv is used by default.
Returns:
int: command's return code.
"""
args = None
cmd = None
try:
@@ -15,21 +25,21 @@ def main(argv=None):
# Init loglevel early in case we'll run
# into errors before setting it properly
CmdBase._set_loglevel(args)
CmdBase.set_loglevel(args)
cmd = args.func(args)
ret = cmd.run_cmd()
except KeyboardInterrupt as ex:
Logger.error("Interrupted by the user", ex)
except KeyboardInterrupt:
logger.error("interrupted by the user")
ret = 252
except NotDvcProjectError as ex:
Logger.error("", ex)
except NotDvcRepoError:
logger.error()
ret = 253
except DvcParserError:
ret = 254
except Exception as ex:
Logger.error('Unexpected error', ex)
except Exception: # pylint: disable=broad-except
logger.error("unexpected error")
ret = 255
Analytics().send_cmd(cmd, args, ret)
+81 -32
View File
@@ -1,10 +1,11 @@
from __future__ import unicode_literals
import schema
from dvc.exceptions import DvcException
from dvc.config import Config
from dvc.utils.compat import urlparse, str
from dvc.dependency import SCHEMA, urlparse
from dvc.dependency.base import DependencyBase
from dvc.output.base import OutputBase
from dvc.output.s3 import OutputS3
from dvc.output.gs import OutputGS
from dvc.output.local import OutputLOCAL
@@ -12,51 +13,99 @@ from dvc.output.hdfs import OutputHDFS
from dvc.output.ssh import OutputSSH
from dvc.remote import Remote
from dvc.remote.s3 import RemoteS3
from dvc.remote.hdfs import RemoteHDFS
from dvc.remote.local import RemoteLOCAL
OUTS = [
OutputHDFS,
OutputS3,
OutputGS,
OutputSSH,
# NOTE: OutputLOCAL is the default choice
]
OUTS_MAP = {
"hdfs": OutputHDFS,
"s3": OutputS3,
"gs": OutputGS,
"ssh": OutputSSH,
"local": OutputLOCAL,
}
SCHEMA = {
OutputBase.PARAM_PATH: str,
# NOTE: currently there are only 3 possible checksum names:
#
# 1) md5 (LOCAL, SSH, GS);
# 2) etag (S3);
# 3) checksum (HDFS);
#
# so when a few types of outputs share the same name, we only need
# specify it once.
schema.Optional(RemoteLOCAL.PARAM_CHECKSUM): schema.Or(str, None),
schema.Optional(RemoteS3.PARAM_CHECKSUM): schema.Or(str, None),
schema.Optional(RemoteHDFS.PARAM_CHECKSUM): schema.Or(str, None),
schema.Optional(OutputBase.PARAM_CACHE): bool,
schema.Optional(OutputBase.PARAM_METRIC): OutputBase.METRIC_SCHEMA,
schema.Optional(OutputBase.PARAM_PERSIST): bool,
}
OUTS = [OutputHDFS, OutputS3, OutputGS, OutputSSH, OutputLOCAL]
OUTS_MAP = {'hdfs': OutputHDFS,
's3': OutputS3,
'gs': OutputGS,
'ssh': OutputSSH,
'': OutputLOCAL}
SCHEMA[schema.Optional(OutputLOCAL.PARAM_CACHE)] = bool
SCHEMA[schema.Optional(OutputLOCAL.PARAM_METRIC)] = OutputLOCAL.METRIC_SCHEMA
def _get(stage, p, info, cache, metric):
def _get(stage, p, info, cache, metric, persist):
parsed = urlparse(p)
if parsed.scheme == 'remote':
if parsed.scheme == "remote":
name = Config.SECTION_REMOTE_FMT.format(parsed.netloc)
sect = stage.project.config._config[name]
remote = Remote(stage.project, sect)
return OUTS_MAP[remote.scheme](stage,
p,
info,
cache=cache,
remote=remote,
metric=metric)
sect = stage.repo.config.config[name]
remote = Remote(stage.repo, sect)
return OUTS_MAP[remote.scheme](
stage,
p,
info,
cache=cache,
remote=remote,
metric=metric,
persist=persist,
)
for o in OUTS:
if o.supported(p):
return o(stage, p, info, cache=cache, remote=None, metric=metric)
raise DvcException('Output \'{}\' is not supported'.format(p))
return OutputLOCAL(
stage,
p,
info,
cache=cache,
remote=None,
metric=metric,
persist=persist,
)
def loadd_from(stage, d_list):
ret = []
for d in d_list:
p = d.pop(DependencyBase.PARAM_PATH)
cache = d.pop(OutputLOCAL.PARAM_CACHE, True)
metric = d.pop(OutputLOCAL.PARAM_METRIC, False)
ret.append(_get(stage, p, info=d, cache=cache, metric=metric))
p = d.pop(OutputBase.PARAM_PATH)
cache = d.pop(OutputBase.PARAM_CACHE, True)
metric = d.pop(OutputBase.PARAM_METRIC, False)
persist = d.pop(OutputBase.PARAM_PERSIST, False)
ret.append(
_get(stage, p, info=d, cache=cache, metric=metric, persist=persist)
)
return ret
def loads_from(stage, s_list, use_cache=True, metric=False):
def loads_from(stage, s_list, use_cache=True, metric=False, persist=False):
ret = []
for s in s_list:
ret.append(_get(stage, s, info={}, cache=use_cache, metric=metric))
ret.append(
_get(
stage,
s,
info={},
cache=use_cache,
metric=metric,
persist=persist,
)
)
return ret
+237 -14
View File
@@ -1,24 +1,247 @@
from __future__ import unicode_literals
import re
from schema import Or, Optional
from dvc.exceptions import DvcException
from dvc.utils.compat import str
from dvc.remote.base import RemoteBase
class OutputError(DvcException):
def __init__(self, path, msg):
msg = 'Output \'{}\' error: {}'.format(path, msg)
super(OutputError, self).__init__(msg)
class OutputDoesNotExistError(OutputError):
class OutputDoesNotExistError(DvcException):
def __init__(self, path):
super(OutputDoesNotExistError, self).__init__(path, 'does not exist')
msg = "output '{}' does not exist".format(path)
super(OutputDoesNotExistError, self).__init__(msg)
class OutputIsNotFileOrDirError(OutputError):
class OutputIsNotFileOrDirError(DvcException):
def __init__(self, path):
msg = 'not a file or directory'
super(OutputIsNotFileOrDirError, self).__init__(path, msg)
msg = "output '{}' is not a file or directory".format(path)
super(OutputIsNotFileOrDirError, self).__init__(msg)
class OutputAlreadyTrackedError(OutputError):
class OutputAlreadyTrackedError(DvcException):
def __init__(self, path):
msg = 'already tracked by scm(e.g. git)'
super(OutputAlreadyTrackedError, self).__init__(path, msg)
msg = "output '{}' is already tracked by scm (e.g. git)".format(path)
super(OutputAlreadyTrackedError, self).__init__(msg)
class OutputBase(object):
IS_DEPENDENCY = False
REMOTE = RemoteBase
PARAM_PATH = "path"
PARAM_CACHE = "cache"
PARAM_METRIC = "metric"
PARAM_METRIC_TYPE = "type"
PARAM_METRIC_XPATH = "xpath"
PARAM_PERSIST = "persist"
METRIC_SCHEMA = Or(
None,
bool,
{
Optional(PARAM_METRIC_TYPE): Or(str, None),
Optional(PARAM_METRIC_XPATH): Or(str, None),
},
)
DoesNotExistError = OutputDoesNotExistError
IsNotFileOrDirError = OutputIsNotFileOrDirError
def __init__(
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
):
self.stage = stage
self.repo = stage.repo
self.url = path
self.info = info
self.remote = remote or self.REMOTE(self.repo, {})
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
self.persist = persist
if (
self.use_cache
and getattr(self.repo.cache, self.REMOTE.scheme) is None
):
raise DvcException(
"no cache location setup for '{}' outputs.".format(
self.REMOTE.scheme
)
)
def __repr__(self):
return "{class_name}: '{url}'".format(
class_name=type(self).__name__, url=(self.url or "No url")
)
def __str__(self):
return self.url
@property
def is_local(self):
raise DvcException(
"is local is not supported for {}".format(self.scheme)
)
def assign_to_stage_file(self, target_repo):
raise DvcException(
"change repo is not supported for {}".format(self.scheme)
)
@classmethod
def match(cls, url):
return re.match(cls.REMOTE.REGEX, url)
def group(self, name):
match = self.match(self.url)
if not match:
return None
return match.group(name)
@classmethod
def supported(cls, url):
return cls.match(url) is not None
@property
def scheme(self):
return self.REMOTE.scheme
@property
def path(self):
return self.path_info["path"]
@property
def sep(self):
return "/"
@property
def checksum(self):
return self.info.get(self.remote.PARAM_CHECKSUM)
@property
def exists(self):
return self.remote.exists(self.path_info)
def changed_checksum(self):
return (
self.checksum
!= self.remote.save_info(self.path_info)[
self.remote.PARAM_CHECKSUM
]
)
def changed_cache(self):
if not self.use_cache or not self.checksum:
return True
cache = self.repo.cache.__getattribute__(self.scheme)
return cache.changed_cache(self.checksum)
def status(self):
if self.checksum and self.use_cache and self.changed_cache():
return {str(self): "not in cache"}
if not self.exists:
return {str(self): "deleted"}
if self.changed_checksum():
return {str(self): "modified"}
if not self.checksum:
return {str(self): "new"}
return {}
def changed(self):
return bool(self.status())
def save(self):
if not self.exists:
raise self.DoesNotExistError(self)
self.info = self.remote.save_info(self.path_info)
def commit(self):
if self.use_cache:
getattr(self.repo.cache, self.scheme).save(
self.path_info, self.info
)
def dumpd(self):
ret = self.info.copy()
ret[self.PARAM_PATH] = self.url
if self.IS_DEPENDENCY:
return ret
ret[self.PARAM_CACHE] = self.use_cache
if isinstance(self.metric, dict):
if (
self.PARAM_METRIC_XPATH in self.metric
and not self.metric[self.PARAM_METRIC_XPATH]
):
del self.metric[self.PARAM_METRIC_XPATH]
ret[self.PARAM_METRIC] = self.metric
ret[self.PARAM_PERSIST] = self.persist
return ret
def verify_metric(self):
raise DvcException(
"verify metric is not supported for {}".format(self.scheme)
)
def download(self, to_info, resume=False):
self.remote.download([self.path_info], [to_info], resume=resume)
def checkout(self, force=False, progress_callback=None):
if not self.use_cache:
return
getattr(self.repo.cache, self.scheme).checkout(
output=self, force=force, progress_callback=progress_callback
)
def remove(self, ignore_remove=False):
self.remote.remove(self.path_info)
if self.scheme != "local":
return
if ignore_remove and self.use_cache and self.is_local:
self.repo.scm.ignore_remove(self.path)
def move(self, out):
if self.scheme == "local" and self.use_cache and self.is_local:
self.repo.scm.ignore_remove(self.path)
self.remote.move(self.path_info, out.path_info)
self.url = out.url
self.path_info = out.path_info
self.save()
self.commit()
if self.scheme == "local" and self.use_cache and self.is_local:
self.repo.scm.ignore(self.path)
def get_files_number(self):
if self.use_cache and self.checksum:
return 1
return 0
def unprotect(self):
if self.exists:
self.remote.unprotect(self.path_info)
+6 -51
View File
@@ -1,53 +1,8 @@
from dvc.dependency.gs import DependencyGS
from dvc.exceptions import DvcException
from __future__ import unicode_literals
from dvc.remote.gs import RemoteGS
from dvc.output.s3 import OutputS3
class OutputGS(DependencyGS):
PARAM_CACHE = 'cache'
PARAM_METRIC = 'metric'
def __init__(self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False):
super(OutputGS, self).__init__(stage, path, info, remote=remote)
self.use_cache = cache
self.metric = metric
if cache and self.project.cache.gs is None:
raise DvcException("No cache location setup for \'gs\' outputs.")
def dumpd(self):
ret = super(OutputGS, self).dumpd()
ret[self.PARAM_CACHE] = self.use_cache
ret[self.PARAM_METRIC] = self.metric
return ret
def changed(self):
if super(OutputGS, self).changed():
return True
if self.use_cache \
and self.project.cache.gs.changed(self.path_info, self.info):
return True
return False
def checkout(self, force=False):
if not self.use_cache:
return
self.project.cache.gs.checkout(self.path_info, self.info)
def save(self):
super(OutputGS, self).save()
if not self.use_cache:
return
self.info = self.project.cache.gs.save(self.path_info)
def remove(self, ignore_remove=False):
self.remote.remove(self.path_info)
class OutputGS(OutputS3):
REMOTE = RemoteGS
+32 -50
View File
@@ -1,53 +1,35 @@
from dvc.dependency.hdfs import DependencyHDFS
from dvc.exceptions import DvcException
from __future__ import unicode_literals
import posixpath
from dvc.utils.compat import urlparse
from dvc.output.base import OutputBase
from dvc.remote.hdfs import RemoteHDFS
class OutputHDFS(DependencyHDFS):
PARAM_CACHE = 'cache'
PARAM_METRIC = 'metric'
class OutputHDFS(OutputBase):
REMOTE = RemoteHDFS
def __init__(self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False):
super(OutputHDFS, self).__init__(stage, path, info, remote=remote)
self.use_cache = cache
self.metric = metric
if cache and self.project.cache.hdfs is None:
raise DvcException("No cache location setup for \'hdfs\' outputs.")
def dumpd(self):
ret = super(OutputHDFS, self).dumpd()
ret[self.PARAM_CACHE] = self.use_cache
ret[self.PARAM_METRIC] = self.metric
return ret
def changed(self):
if super(OutputHDFS, self).changed():
return True
if self.use_cache \
and self.project.cache.hdfs.changed(self.path_info, self.info):
return True
return False
def checkout(self, force=False):
if not self.use_cache:
return
self.project.cache.hdfs.checkout(self.path_info, self.info)
def save(self):
super(OutputHDFS, self).save()
if not self.use_cache:
return
self.info = self.project.cache.hdfs.save(self.path_info)
def remove(self, ignore_remove=False):
self.remote.remove(self.path_info)
def __init__(
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
):
super(OutputHDFS, self).__init__(
stage,
path,
info=info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
)
if remote:
path = posixpath.join(remote.url, urlparse(path).path.lstrip("/"))
user = remote.user if remote else self.group("user")
self.path_info = {"scheme": "hdfs", "user": user, "path": path}
+120 -84
View File
@@ -1,74 +1,107 @@
from __future__ import unicode_literals
import os
from schema import Optional, Or
from dvc.output.base import OutputDoesNotExistError, OutputIsNotFileOrDirError
from dvc.output.base import OutputAlreadyTrackedError
from dvc.dependency.local import DependencyLOCAL
from dvc.exceptions import DvcException
import dvc.logger as logger
from dvc.utils.compat import urlparse
from dvc.istextfile import istextfile
from dvc.exceptions import DvcException
from dvc.remote.local import RemoteLOCAL
from dvc.output.base import OutputBase, OutputAlreadyTrackedError
class OutputLOCAL(DependencyLOCAL):
PARAM_CACHE = 'cache'
PARAM_METRIC = 'metric'
PARAM_METRIC_TYPE = 'type'
PARAM_METRIC_XPATH = 'xpath'
class OutputLOCAL(OutputBase):
REMOTE = RemoteLOCAL
METRIC_SCHEMA = Or(None, bool,
{Optional(PARAM_METRIC_TYPE): Or(str, None),
Optional(PARAM_METRIC_XPATH): Or(str, None)})
def __init__(
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
):
super(OutputLOCAL, self).__init__(
stage,
path,
info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
)
if remote:
p = os.path.join(
remote.prefix, urlparse(self.url).path.lstrip("/")
)
else:
p = path
DoesNotExistError = OutputDoesNotExistError
IsNotFileOrDirError = OutputIsNotFileOrDirError
if not os.path.isabs(p):
p = self.remote.to_ospath(p)
p = os.path.join(stage.wdir, p)
p = os.path.abspath(os.path.normpath(p))
def __init__(self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False):
super(OutputLOCAL, self).__init__(stage, path, info, remote=remote)
self.use_cache = cache
self.metric = metric
self.path_info = {"scheme": "local", "path": p}
self._dir_cache = {}
def __str__(self):
return self.rel_path
@property
def md5(self):
return self.info.get(self.project.cache.local.PARAM_MD5, None)
def is_local(self):
return urlparse(self.url).scheme != "remote" and not os.path.isabs(
self.url
)
def assign_to_stage_file(self, stage):
from dvc.repo import Repo
fullpath = os.path.abspath(stage.wdir)
self.path_info["path"] = os.path.join(fullpath, self.stage_path)
self.repo = Repo(self.path)
self.stage = stage
return self
@property
def sep(self):
return os.sep
@property
def rel_path(self):
return os.path.relpath(self.path)
@property
def stage_path(self):
return os.path.relpath(self.path, self.stage.wdir)
@property
def cache(self):
return self.project.cache.local.get(self.md5)
return self.repo.cache.local.get(self.checksum)
@property
def is_dir_cache(self):
return self.repo.cache.local.is_dir_cache(self.checksum)
def dumpd(self):
ret = super(OutputLOCAL, self).dumpd()
ret[self.PARAM_CACHE] = self.use_cache
if self.is_local:
path = self.remote.unixpath(
os.path.relpath(self.path, self.stage.wdir)
)
else:
path = self.url
if isinstance(self.metric, dict):
if self.PARAM_METRIC_XPATH in self.metric and \
not self.metric[self.PARAM_METRIC_XPATH]:
del self.metric[self.PARAM_METRIC_XPATH]
if self.metric:
ret[self.PARAM_METRIC] = self.metric
ret[self.PARAM_PATH] = path
return ret
def changed(self):
if not self.use_cache:
return super(OutputLOCAL, self).changed()
return self.project.cache.local.changed(self.path_info, self.info)
def checkout(self, force=False):
if not self.use_cache:
return
self.project.cache.local.checkout(self.path_info,
self.info,
force=force)
def _verify_metric(self):
def verify_metric(self):
if not self.metric:
return
@@ -76,60 +109,63 @@ class OutputLOCAL(DependencyLOCAL):
return
if os.path.isdir(self.path):
msg = 'Directory \'{}\' cannot be used as metrics.'
msg = "directory '{}' cannot be used as metrics."
raise DvcException(msg.format(self.rel_path))
if not istextfile(self.path):
msg = 'Binary file \'{}\' cannot be used as metrics.'
msg = "binary file '{}' cannot be used as metrics."
raise DvcException(msg.format(self.rel_path))
def save(self):
if not self.use_cache:
super(OutputLOCAL, self).save()
self._verify_metric()
msg = 'Output \'{}\' doesn\'t use cache. Skipping saving.'
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): # pragma: no cover
if not os.path.isfile(self.path) and not os.path.isdir(self.path):
raise self.IsNotFileOrDirError(self.rel_path)
if (os.path.isfile(self.path) and os.path.getsize(self.path) == 0) or \
(os.path.isdir(self.path) and len(os.listdir(self.path)) == 0):
msg = "File/directory '{}' is empty.".format(self.rel_path)
self.project.logger.warn(msg)
if (os.path.isfile(self.path) and os.path.getsize(self.path) == 0) or (
os.path.isdir(self.path) and len(os.listdir(self.path)) == 0
):
msg = "file/directory '{}' is empty.".format(self.rel_path)
logger.warning(msg)
if not self.use_cache:
self.info = self.remote.save_info(self.path_info)
self.verify_metric()
if not self.IS_DEPENDENCY:
msg = "Output '{}' doesn't use cache. Skipping saving."
logger.info(msg.format(self.rel_path))
return
assert not self.IS_DEPENDENCY
if not self.changed():
msg = 'Output \'{}\' didn\'t change. Skipping saving.'
self.project.logger.info(msg.format(self.rel_path))
msg = "Output '{}' didn't change. Skipping saving."
logger.info(msg.format(self.rel_path))
return
if self.is_local:
if self.project.scm.is_tracked(self.path):
if self.repo.scm.is_tracked(self.path):
raise OutputAlreadyTrackedError(self.rel_path)
if self.use_cache:
self.project.scm.ignore(self.path)
self.repo.scm.ignore(self.path)
self.info = self.project.cache.local.save(self.path_info)
self.info = self.remote.save_info(self.path_info)
def remove(self, ignore_remove=False):
self.remote.remove(self.path_info)
if ignore_remove and self.use_cache and self.is_local:
self.project.scm.ignore_remove(self.path)
@property
def dir_cache(self):
if self.checksum not in self._dir_cache.keys():
self._dir_cache[
self.checksum
] = self.repo.cache.local.load_dir_cache(self.checksum)
def move(self, out):
if self.use_cache and self.is_local:
self.project.scm.ignore_remove(self.path)
return self._dir_cache[self.checksum]
self.remote.move(self.path_info, out.path_info)
self.path = out.path
self.path_info = out.path_info
self.save()
def get_files_number(self):
if self.cache is None:
return 0
if self.use_cache and self.is_local:
self.project.scm.ignore(self.path)
if self.is_dir_cache:
return len(self.dir_cache)
return 1
+37 -50
View File
@@ -1,53 +1,40 @@
from dvc.dependency.s3 import DependencyS3
from dvc.exceptions import DvcException
from __future__ import unicode_literals
import posixpath
from dvc.remote.s3 import RemoteS3
from dvc.utils.compat import urlparse
from dvc.output.base import OutputBase
class OutputS3(DependencyS3):
PARAM_CACHE = 'cache'
PARAM_METRIC = 'metric'
class OutputS3(OutputBase):
REMOTE = RemoteS3
def __init__(self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False):
super(OutputS3, self).__init__(stage, path, info, remote=remote)
self.use_cache = cache
self.metric = metric
if cache and self.project.cache.s3 is None:
raise DvcException("No cache location setup for \'s3\' outputs.")
def dumpd(self):
ret = super(OutputS3, self).dumpd()
ret[self.PARAM_CACHE] = self.use_cache
ret[self.PARAM_METRIC] = self.metric
return ret
def changed(self):
if super(OutputS3, self).changed():
return True
if self.use_cache \
and self.project.cache.s3.changed(self.path_info, self.info):
return True
return False
def checkout(self, force=False):
if not self.use_cache:
return
self.project.cache.s3.checkout(self.path_info, self.info)
def save(self):
super(OutputS3, self).save()
if not self.use_cache:
return
self.info = self.project.cache.s3.save(self.path_info)
def remove(self, ignore_remove=False):
self.remote.remove(self.path_info)
def __init__(
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
):
super(OutputS3, self).__init__(
stage,
path,
info=info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
)
bucket = remote.bucket if remote else urlparse(path).netloc
path = urlparse(path).path.lstrip("/")
if remote:
path = posixpath.join(remote.prefix, path)
self.path_info = {
"scheme": self.scheme,
"bucket": bucket,
"path": path,
}
+50 -48
View File
@@ -1,53 +1,55 @@
from dvc.dependency.ssh import DependencySSH
from dvc.exceptions import DvcException
from __future__ import unicode_literals
import getpass
import posixpath
from dvc.utils.compat import urlparse
from dvc.output.base import OutputBase
from dvc.remote.ssh import RemoteSSH
class OutputSSH(DependencySSH):
PARAM_CACHE = 'cache'
PARAM_METRIC = 'metric'
class OutputSSH(OutputBase):
REMOTE = RemoteSSH
def __init__(self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False):
super(OutputSSH, self).__init__(stage, path, info, remote=remote)
self.use_cache = cache
self.metric = metric
if cache and self.project.cache.ssh is None:
raise DvcException("No cache location setup for \'ssh\' outputs.")
def __init__(
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
):
super(OutputSSH, self).__init__(
stage,
path,
info=info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
)
parsed = urlparse(path)
host = remote.host if remote else parsed.hostname
port = (
remote.port if remote else (parsed.port or RemoteSSH.DEFAULT_PORT)
)
user = (
remote.user if remote else (parsed.username or getpass.getuser())
)
def dumpd(self):
ret = super(OutputSSH, self).dumpd()
ret[self.PARAM_CACHE] = self.use_cache
ret[self.PARAM_METRIC] = self.metric
return ret
if remote:
path = posixpath.join(
remote.prefix, urlparse(path).path.lstrip("/")
)
else:
path = parsed.path
def changed(self):
if super(OutputSSH, self).changed():
return True
if self.use_cache \
and self.project.cache.ssh.changed(self.path_info, self.info):
return True
return False
def checkout(self, force=False):
if not self.use_cache:
return
self.project.cache.ssh.checkout(self.path_info, self.info)
def save(self):
super(OutputSSH, self).save()
if not self.use_cache:
return
self.info = self.project.cache.ssh.save(self.path_info)
def remove(self, ignore_remove=False):
self.remote.remove(self.path_info)
self.path_info = {
"scheme": "ssh",
"host": host,
"port": port,
"user": user,
"path": path,
}
+68 -19
View File
@@ -1,12 +1,21 @@
"""Manages progress bars for dvc repo."""
from __future__ import print_function
from __future__ import unicode_literals
from dvc.utils.compat import str
import sys
import threading
CLEARLINE_PATTERN = "\r\x1b[K"
class Progress(object):
"""
Simple multi-target progress bar.
"""
def __init__(self):
self._n_total = 0
self._n_finished = 0
@@ -14,22 +23,31 @@ class Progress(object):
self._line = None
def set_n_total(self, total):
"""Sets total number of targets."""
self._n_total = total
self._n_finished = 0
@property
def is_finished(self):
"""Returns if all targets have finished."""
return self._n_total == self._n_finished
def _clearln(self):
self._print('\r\x1b[K', end='')
def clearln(self):
self.print(CLEARLINE_PATTERN, end="")
def _writeln(self, line):
self._clearln()
self._print(line, end='')
self.clearln()
self.print(line, end="")
sys.stdout.flush()
def reset(self):
with self._lock:
self._n_total = 0
self._n_finished = 0
self._line = None
def refresh(self, line=None):
"""Refreshes progress bar."""
# Just go away if it is locked. Will update next time
if not self._lock.acquire(False):
return
@@ -44,17 +62,19 @@ class Progress(object):
self._lock.release()
def update_target(self, name, current, total):
"""Updates progress bar for a specified target."""
self.refresh(self._bar(name, current, total))
def finish_target(self, name):
"""Finishes progress bar for a specified target."""
# We have to write a msg about finished target
with self._lock:
bar = self._bar(name, 100, 100)
pbar = self._bar(name, 100, 100)
if sys.stdout.isatty():
self._clearln()
self.clearln()
self._print(bar)
self.print(pbar)
self._n_finished += 1
self._line = None
@@ -67,28 +87,29 @@ class Progress(object):
bar_len = 30
if total is None:
progress = 0
state = 0
percent = "?% "
else:
total = int(total)
progress = int((100 * current)/total) if current < total else 100
percent = str(progress) + "% "
state = int((100 * current) / total) if current < total else 100
percent = str(state) + "% "
if self._n_total > 1:
num = "({}/{}): ".format(self._n_finished + 1, self._n_total)
else:
num = ""
n_sh = int((progress * bar_len)/100)
n_sh = int((state * bar_len) / 100)
n_sp = bar_len - n_sh
bar = "[" + '#'*n_sh + ' '*n_sp + "] "
pbar = "[" + "#" * n_sh + " " * n_sp + "] "
return num + bar + percent + target_name
return num + pbar + percent + target_name
def _print(self, *args, **kwargs):
from dvc.logger import Logger
@staticmethod
def print(*args, **kwargs):
import dvc.logger as logger
if Logger.is_quiet():
if logger.is_quiet():
return
print(*args, **kwargs)
@@ -96,12 +117,40 @@ class Progress(object):
def __enter__(self):
self._lock.acquire(True)
if self._line is not None:
self._clearln()
self.clearln()
def __exit__(self, type, value, tb):
def __exit__(self, typ, value, tbck):
if self._line is not None:
self.refresh()
self._lock.release()
progress = Progress()
def progress_aware(f):
""" Decorator to add a new line if progress bar hasn't finished """
from functools import wraps
@wraps(f)
def wrapper(*args, **kwargs):
if not progress.is_finished:
progress.print()
progress.clearln()
return f(*args, **kwargs)
return wrapper
class ProgressCallback(object):
def __init__(self, total):
self.total = total
self.current = 0
def update(self, name, progress_to_add=1):
self.current += progress_to_add
progress.update_target(name, self.current, self.total)
def finish(self, name):
progress.finish_target(name)
progress = Progress() # pylint: disable=invalid-name
-1233
View File
File diff suppressed because it is too large Load Diff
+52 -31
View File
@@ -1,40 +1,61 @@
"""Manages user prompts."""
from __future__ import unicode_literals
from __future__ import print_function
import sys
from getpass import getpass
from dvc.progress import progress
try:
# NOTE: in Python3 raw_input() was renamed to input()
input = raw_input
except NameError:
pass
from dvc.progress import progress_aware
from dvc.utils.compat import input
class Prompt(object):
def __init__(self):
self.default = None
def _ask(prompt, limited_to=None):
if not sys.stdout.isatty():
return None
def prompt(self, msg, default=False): # pragma: no cover
if self.default is not None:
return self.default
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():
while True:
try:
answer = input(prompt + " ").lower()
except EOFError:
return None
msg = 'Enter password for {}:\n'.format(msg)
if not limited_to:
return answer
if not progress.is_finished:
msg = u'\n' + msg
if answer in limited_to:
return answer
return getpass.getpass(msg)
print(
"Your response must be one of: {options}. "
"Please try again.".format(options=limited_to)
)
@progress_aware
def confirm(statement):
"""Ask the user for confirmation about the specified statement.
Args:
statement (unicode): statement to ask the user confirmation about.
Returns:
bool: whether or not specified statement was confirmed.
"""
prompt = "{statement} [y/n]".format(statement=statement)
answer = _ask(prompt, limited_to=["yes", "no", "y", "n"])
return answer and answer.startswith("y")
@progress_aware
def password(statement):
"""Ask the user for a password.
Args:
statement (str): string to prompt the user with.
Returns:
str: password entered by the user.
"""
prompt = "{statement}: ".format(statement=statement)
return getpass(prompt)
+10 -12
View File
@@ -1,3 +1,5 @@
from __future__ import unicode_literals
from dvc.remote.azure import RemoteAzure
from dvc.remote.gs import RemoteGS
from dvc.remote.hdfs import RemoteHDFS
@@ -6,28 +8,24 @@ from dvc.remote.s3 import RemoteS3
from dvc.remote.ssh import RemoteSSH
from dvc.remote.http import RemoteHTTP
from dvc.config import Config
from dvc.exceptions import UnsupportedRemoteError
REMOTES = [
RemoteAzure,
RemoteGS,
RemoteHDFS,
RemoteHTTP,
RemoteLOCAL,
RemoteS3,
RemoteSSH,
# NOTE: RemoteLOCAL is the default
]
def supported_url(url):
config = {Config.SECTION_REMOTE_URL: url}
return any(remote.supported(config) for remote in REMOTES)
def Remote(project, config):
def _get(config):
for remote in REMOTES:
if remote.supported(config):
return remote(project, config)
raise UnsupportedRemoteError(str(config))
return remote
return RemoteLOCAL
def Remote(repo, config):
return _get(config)(repo, config)
+99 -167
View File
@@ -1,14 +1,17 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import os
import re
import posixpath
try:
from azure.storage.blob import BlockBlobService
except ImportError:
BlockBlobService = None
from dvc.logger import Logger
import dvc.logger as logger
from dvc.utils import tmp_fname, move
from dvc.utils.compat import urlparse, makedirs
from dvc.progress import progress
from dvc.config import Config
from dvc.remote.base import RemoteBase
@@ -23,239 +26,168 @@ class Callback(object):
class RemoteAzure(RemoteBase):
scheme = 'azure'
REGEX = (r'^azure://'
r'(ContainerName=(?P<container_name>[^;]+);?)?'
r'(?P<connection_string>.+)?$')
REQUIRES = {'azure-storage-blob': BlockBlobService}
PARAM_ETAG = 'etag'
scheme = "azure"
REGEX = (
r"azure://((?P<path>[^=;]*)?|("
# backward compatibility
r"(ContainerName=(?P<container_name>[^;]+);?)?"
r"(?P<connection_string>.+)?)?)$"
)
REQUIRES = {"azure-storage-blob": BlockBlobService}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
def __init__(self, project, config):
super(RemoteAzure, self).__init__(project, config)
self.project = project
def __init__(self, repo, config):
super(RemoteAzure, self).__init__(repo, config)
self.url = config.get(Config.SECTION_REMOTE_URL)
match = re.match(self.REGEX, self.url)
self.url = config.get(Config.SECTION_REMOTE_URL, "azure://")
match = re.match(self.REGEX, self.url) # backward compatibility
path = match.group("path")
self.bucket = (
match.group('container_name')
or os.getenv('AZURE_STORAGE_CONTAINER_NAME'))
urlparse(self.url if path else "").netloc
or match.group("container_name") # backward compatibility
or os.getenv("AZURE_STORAGE_CONTAINER_NAME")
)
# FIXME: currently Azure doesn't support prefixes
self.prefix = '/'
self.prefix = urlparse(self.url).path.lstrip("/") if path else ""
self.connection_string = (
match.group('connection_string')
or os.getenv('AZURE_STORAGE_CONNECTION_STRING'))
config.get(Config.SECTION_AZURE_CONNECTION_STRING)
or match.group("connection_string") # backward compatibility
or os.getenv("AZURE_STORAGE_CONNECTION_STRING")
)
if not self.bucket:
raise ValueError('Azure Storage container name missing')
raise ValueError("azure storage container name missing")
if not self.connection_string:
raise ValueError('Azure Storage connection string missing')
raise ValueError("azure storage connection string missing")
self.__blob_service = None
self.path_info = {"scheme": self.scheme, "bucket": self.bucket}
@property
def blob_service(self):
if self.__blob_service is None:
logger.debug("URL {}".format(self.url))
logger.debug("Connection string {}".format(self.connection_string))
self.__blob_service = BlockBlobService(
connection_string=self.connection_string)
connection_string=self.connection_string
)
logger.debug("Container name {}".format(self.bucket))
self.__blob_service.create_container(self.bucket)
return self.__blob_service
# FIXME: temporarily disabled because of the lack of test for external azure
# dependencies/outputs/cache.
#
# def _get_etag(self, bucket, key):
# try:
# blob = self.blob_service.get_blob_properties(bucket, key)
# return blob.properties.etag
# except Exception:
# return None
#
# def save_info(self, path_info):
# if path_info['scheme'] != self.scheme:
# raise NotImplementedError
#
# return {self.PARAM_ETAG: self._get_etag(
# path_info['bucket'], path_info['key'])}
#
# def save(self, path_info):
# if path_info['scheme'] != self.scheme:
# raise NotImplementedError
#
# etag = self._get_etag(path_info['bucket'], path_info['key'])
# dest_key = '{}/{}'.format(etag[0:2], etag[2:])
#
# self._copy_blob(to_bucket=self.bucket,
# to_key=dest_key,
# from_bucket=path_info['bucket'],
# from_key=path_info['key'])
#
# 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)
#
# copy = self.blob_service.copy_blob(to_bucket, to_key, source)
#
# if self.COPY_POLL_SECONDS <= 0:
# return
#
# while copy.status != 'success':
# time.sleep(self.COPY_POLL_SECONDS)
# copy = self.blob_service.get_blob_properties(
# to_bucket, to_key).properties.copy
#
# def checkout(self, path_info, checksum_info):
# if path_info['scheme'] != self.scheme:
# raise NotImplementedError
#
# etag = checksum_info.get(self.PARAM_ETAG, None)
# if not etag:
# return
#
# self._copy_blob(to_bucket=path_info['bucket'],
# to_key=path_info['key'],
# from_bucket=self.bucket,
# from_key='{}/{}'.format(etag[0:2], etag[2:]))
#
def remove(self, path_info):
if path_info['scheme'] != self.scheme:
if path_info["scheme"] != self.scheme:
raise NotImplementedError
Logger.debug('Removing azure://{}/{}'.format(path_info['bucket'],
path_info['key']))
logger.debug(
"Removing azure://{}/{}".format(
path_info["bucket"], path_info["path"]
)
)
self.blob_service.delete_blob(path_info['bucket'], path_info['key'])
self.blob_service.delete_blob(path_info["bucket"], path_info["path"])
def md5s_to_path_infos(self, md5s):
return [{
'scheme': self.scheme,
'bucket': self.bucket,
'key': '{}/{}'.format(md5[0:2], md5[2:])
} for md5 in md5s]
def _list_paths(self, bucket, prefix):
blob_service = self.blob_service
next_marker = None
while True:
blobs = blob_service.list_blobs(
bucket, prefix=prefix, marker=next_marker
)
def _all_keys(self, bucket, prefix):
return {blob.name
for blob in self.blob_service.list_blobs(bucket,
prefix=prefix)}
for blob in blobs:
yield blob.name
def exists(self, path_infos):
ret = []
if not blobs.next_marker:
break
if len(path_infos) == 0:
return ret
next_marker = blobs.next_marker
bucket = path_infos[0]['bucket']
key = path_infos[0]['key']
for path_info in path_infos:
assert path_info['scheme'] == self.scheme
assert path_info['bucket'] == bucket
key = path_info['key']
ret.append(key in self._all_keys(bucket, key))
return ret
def list_cache_paths(self):
return self._list_paths(self.bucket, self.prefix)
def upload(self, from_infos, to_infos, names=None):
names = self._verify_path_args(to_infos, from_infos, names)
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info['scheme'] != self.scheme:
if to_info["scheme"] != self.scheme:
raise NotImplementedError
if from_info['scheme'] != 'local':
if from_info["scheme"] != "local":
raise NotImplementedError
bucket = to_info['bucket']
key = to_info['key']
bucket = to_info["bucket"]
path = to_info["path"]
Logger.debug("Uploading '{}' to '{}/{}'".format(
from_info['path'], bucket, key))
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info["path"], bucket, path
)
)
if not name:
name = os.path.basename(from_info['path'])
name = os.path.basename(from_info["path"])
cb = Callback(name)
try:
self.blob_service.create_blob_from_path(
bucket, key, from_info['path'], progress_callback=cb)
except Exception as ex:
msg = "Failed to upload '{}'".format(from_info['path'])
Logger.warn(msg, ex)
bucket, path, from_info["path"], progress_callback=cb
)
except Exception:
msg = "failed to upload '{}'".format(from_info["path"])
logger.warning(msg)
else:
progress.finish_target(name)
def download(self,
from_infos,
to_infos,
no_progress_bar=False,
names=None):
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(from_infos, to_infos, names)
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info['scheme'] != self.scheme:
if from_info["scheme"] != self.scheme:
raise NotImplementedError
if to_info['scheme'] != 'local':
if to_info["scheme"] != "local":
raise NotImplementedError
bucket = from_info['bucket']
key = from_info['key']
bucket = from_info["bucket"]
path = from_info["path"]
Logger.debug("Downloading '{}/{}' to '{}'".format(
bucket, key, to_info['path']))
logger.debug(
"Downloading '{}/{}' to '{}'".format(
bucket, path, to_info["path"]
)
)
tmp_file = self.tmp_file(to_info['path'])
tmp_file = tmp_fname(to_info["path"])
if not name:
name = os.path.basename(to_info['path'])
name = os.path.basename(to_info["path"])
cb = None if no_progress_bar else Callback(name)
self._makedirs(to_info['path'])
makedirs(os.path.dirname(to_info["path"]), exist_ok=True)
try:
self.blob_service.get_blob_to_path(
bucket, key, tmp_file, progress_callback=cb)
except Exception as exc:
msg = "Failed to download '{}/{}'".format(bucket, key)
Logger.warn(msg, exc)
bucket, path, tmp_file, progress_callback=cb
)
except Exception:
msg = "failed to download '{}/{}'".format(bucket, path)
logger.warning(msg)
else:
os.rename(tmp_file, to_info['path'])
move(tmp_file, to_info["path"])
if not no_progress_bar:
progress.finish_target(name)
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(self.bucket, self.prefix)
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
+289 -64
View File
@@ -1,14 +1,19 @@
import os
from __future__ import unicode_literals
from dvc.utils.compat import str
import re
import errno
import posixpath
from multiprocessing import cpu_count
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.config import Config
from dvc.logger import Logger
from dvc.exceptions import DvcException
from dvc.exceptions import DvcException, ConfirmRemoveError
STATUS_OK = 1
STATUS_MISSING = 2
STATUS_NEW = 3
STATUS_DELETED = 4
@@ -16,7 +21,7 @@ STATUS_DELETED = 4
STATUS_MAP = {
# (local_exists, remote_exists)
(True, True): STATUS_OK,
(False, False): STATUS_OK,
(False, False): STATUS_MISSING,
(True, False): STATUS_NEW,
(False, True): STATUS_DELETED,
}
@@ -24,23 +29,67 @@ STATUS_MAP = {
class DataCloudError(DvcException):
""" Data Cloud exception """
def __init__(self, msg):
super(DataCloudError, self).__init__('Data sync error: {}'.format(msg))
super(DataCloudError, self).__init__("Data sync error: {}".format(msg))
class RemoteBaseCmdError(DvcException):
def __init__(self, cmd, ret, err):
m = "SSH command '{}' finished with non-zero return code '{}': {}"
super(RemoteBaseCmdError, self).__init__(m.format(cmd, ret, err))
class RemoteCmdError(DvcException):
def __init__(self, remote, cmd, ret, err):
super(RemoteCmdError, self).__init__(
"{remote} command '{cmd}' finished with non-zero return code"
" {ret}': {err}".format(remote=remote, cmd=cmd, ret=ret, err=err)
)
class RemoteActionNotImplemented(DvcException):
def __init__(self, action, scheme):
m = "{} is not supported by {} remote".format(action, scheme)
super(RemoteActionNotImplemented, self).__init__(m)
class RemoteMissingDepsError(DvcException):
pass
class RemoteBase(object):
scheme = None
REGEX = None
REQUIRES = {}
JOBS = 4 * cpu_count()
def __init__(self, project, config):
pass
def __init__(self, repo, config):
self.repo = repo
deps_ok = all(self.REQUIRES.values())
if not deps_ok:
missing = [k for k, v in self.REQUIRES.items() if v is None]
url = config.get(
Config.SECTION_REMOTE_URL, "{}://".format(self.scheme)
)
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!"
).format(url, missing, " ".join(missing), self.scheme)
raise RemoteMissingDepsError(msg)
def __repr__(self):
return "{class_name}: '{url}'".format(
class_name=type(self).__name__, url=(self.url or "No url")
)
def compat_config(config):
return config.copy()
@@ -48,30 +97,7 @@ class RemoteBase(object):
@classmethod
def supported(cls, config):
url = config[Config.SECTION_REMOTE_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: {}. 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
return cls.match(url) is not None
@classmethod
def match(cls, url):
@@ -83,47 +109,97 @@ class RemoteBase(object):
return None
return m.group(name)
@staticmethod
def tmp_file(fname):
""" Temporary name for a partial download """
import uuid
return fname + '.' + str(uuid.uuid4())
def save_info(self, path_info):
raise NotImplementedError
def changed(self, path_info, checksum_info):
raise NotImplementedError
"""Checks if data has changed.
def save(self, path_info):
raise NotImplementedError
A file is considered changed if:
- It doesn't exist on the working directory (was unlinked)
- Checksum is not computed (saving a new file)
- The checkusm stored in the State is different from the given one
- There's no file in the cache
def checkout(self, path_info, checksum_info, force=False):
raise NotImplementedError
Args:
path_info: dict with path information.
checksum: expected checksum for this data.
def download(self, from_infos, to_infos, no_progress_bar=False, name=None):
raise NotImplementedError
Returns:
bool: True if data has changed, False otherwise.
"""
def upload(self, from_infos, to_infos, path_info, name=None):
raise NotImplementedError
logger.debug(
"checking if '{}'('{}') has changed.".format(
path_info, checksum_info
)
)
if not self.exists(path_info):
logger.debug("'{}' doesn't exist.".format(path_info))
return True
checksum = checksum_info.get(self.PARAM_CHECKSUM)
if checksum is None:
logger.debug("checksum for '{}' is missing.".format(path_info))
return True
if self.changed_cache(checksum):
logger.debug(
"cache for '{}'('{}') has changed.".format(path_info, checksum)
)
return True
actual = self.save_info(path_info)[self.PARAM_CHECKSUM]
if checksum != actual:
logger.debug(
"checksum '{}'(actual '{}') for '{}' has changed.".format(
checksum, actual, path_info
)
)
return True
logger.debug("'{}' hasn't changed.".format(path_info))
return False
def save(self, path_info, checksum_info):
if path_info["scheme"] != self.scheme:
raise RemoteActionNotImplemented(
"save {} -> {}".format(path_info["scheme"], self.scheme),
self.scheme,
)
checksum = checksum_info[self.PARAM_CHECKSUM]
if not self.changed_cache(checksum):
return
to_info = self.checksum_to_path_info(checksum)
logger.info("Saving '{}' to '{}'.".format(path_info, to_info))
self.copy(path_info, to_info)
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
name=None,
resume=False,
):
raise RemoteActionNotImplemented("download", self.scheme)
def upload(self, from_infos, to_infos, names=None):
raise RemoteActionNotImplemented("upload", self.scheme)
def remove(self, path_info):
raise NotImplementedError
raise RemoteActionNotImplemented("remove", self.scheme)
def move(self, path_info):
raise NotImplementedError
raise RemoteActionNotImplemented("move", self.scheme)
def _makedirs(self, fname):
dname = os.path.dirname(fname)
try:
os.makedirs(dname)
except OSError as e:
if e.errno != errno.EEXIST:
raise
def md5s_to_path_infos(self, md5s):
raise NotImplementedError
def copy(self, from_info, to_info):
raise RemoteActionNotImplemented("copy", self.scheme)
def exists(self, path_infos):
raise NotImplementedError
@@ -141,3 +217,152 @@ class RemoteBase(object):
assert len(names) == len(to_infos)
return names
@property
def ospath(self):
return posixpath
def checksum_to_path(self, checksum):
return self.ospath.join(self.prefix, checksum[0:2], checksum[2:])
def path_to_checksum(self, path):
relpath = self.ospath.relpath(path, self.prefix)
return self.ospath.dirname(relpath) + self.ospath.basename(relpath)
def checksum_to_path_info(self, checksum):
path_info = self.path_info.copy()
path_info["path"] = self.checksum_to_path(checksum)
return path_info
def md5s_to_path_infos(self, md5s):
return [self.checksum_to_path_info(md5) for md5 in md5s]
def list_cache_paths(self):
raise NotImplementedError
def all(self):
# NOTE: The list might be way too big(e.g. 100M entries, md5 for each
# is 32 bytes, so ~3200Mb list) and we don't really need all of it at
# the same time, so it makes sense to use a generator to gradually
# iterate over it, without keeping all of it in memory.
return (
self.path_to_checksum(path) for path in self.list_cache_paths()
)
def gc(self, cinfos):
from dvc.remote.local import RemoteLOCAL
used = [info[RemoteLOCAL.PARAM_CHECKSUM] for info in cinfos["local"]]
if self.scheme != "":
used += [info[self.PARAM_CHECKSUM] for info in cinfos[self.scheme]]
removed = False
for checksum in self.all():
if checksum in used:
continue
path_info = self.checksum_to_path_info(checksum)
self.remove(path_info)
removed = True
return removed
def changed_cache(self, checksum):
cache = self.checksum_to_path_info(checksum)
expected = {self.PARAM_CHECKSUM: checksum}
if not self.exists(cache):
return True
actual = self.save_info(cache)
logger.debug(
"Cache '{}' actual '{}'.".format(str(expected), str(actual))
)
if expected != actual:
if self.exists(cache):
msg = "corrupted cache file {}"
logger.warning(msg.format(str(cache)))
self.remove(cache)
return True
return False
def cache_exists(self, checksums):
# NOTE: The reason for such an odd logic is that most of the remotes
# take much shorter time to just retrieve everything they have under
# a certain prefix(e.g. s3, gs, ssh, hdfs). Other remotes that can
# check if particular file exists much quicker, use their own
# implementation of cache_exists(see http, local).
#
# Result of all() might be way too big, so we should walk through
# it in one pass.
return list(filter(lambda checksum: checksum in checksums, self.all()))
def already_cached(self, path_info):
current = self.save_info(path_info)[self.PARAM_CHECKSUM]
if not current:
return False
return not self.changed_cache(current)
def safe_remove(self, path_info, force=False):
if not self.exists(path_info):
return
if not force and not self.already_cached(path_info):
msg = (
"file '{}' is going to be removed."
" Are you sure you want to proceed?".format(str(path_info))
)
if not prompt.confirm(msg):
raise ConfirmRemoveError(str(path_info))
self.remove(path_info)
def do_checkout(self, output, force=False, progress_callback=None):
path_info = output.path_info
checksum = output.info.get(self.PARAM_CHECKSUM)
if self.exists(path_info):
msg = "data '{}' exists. Removing before checkout."
logger.warning(msg.format(str(path_info)))
self.safe_remove(path_info, force=force)
from_info = self.checksum_to_path_info(checksum)
self.copy(from_info, path_info)
def checkout(self, output, force=False, progress_callback=None):
scheme = output.path_info["scheme"]
if scheme not in ["", "local"] and scheme != self.scheme:
raise NotImplementedError
checksum = output.info.get(self.PARAM_CHECKSUM)
if not checksum:
msg = "No checksum info for '{}'."
logger.debug(msg.format(str(output.path_info)))
return
if not self.changed(output.path_info, output.info):
msg = "Data '{}' didn't change."
logger.debug(msg.format(str(output.path_info)))
return
if self.changed_cache(checksum):
msg = "Cache '{}' not found. File '{}' won't be created."
logger.warning(msg.format(checksum, str(output.path_info)))
self.safe_remove(output.path_info, force=force)
return
msg = "Checking out '{}' with cache '{}'."
logger.debug(msg.format(str(output.path_info), checksum))
self.do_checkout(
output, force=force, progress_callback=progress_callback
)
@staticmethod
def unprotect(path_info):
pass
+109 -210
View File
@@ -1,205 +1,121 @@
from __future__ import unicode_literals
import os
import posixpath
try:
from google.cloud import storage
except ImportError:
storage = None
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from dvc.logger import Logger
import dvc.logger as logger
from dvc.utils import tmp_fname, move
from dvc.utils.compat import urlparse, makedirs
from dvc.remote.base import RemoteBase
from dvc.remote.local import RemoteLOCAL
from dvc.config import Config
from dvc.progress import progress
from dvc.exceptions import DvcException
class RemoteGS(RemoteBase):
scheme = 'gs'
REGEX = r'^gs://(?P<path>.*)$'
REQUIRES = {'google.cloud.storage': storage}
PARAM_MD5 = 'md5'
scheme = "gs"
REGEX = r"^gs://(?P<path>.*)$"
REQUIRES = {"google.cloud.storage": storage}
PARAM_CHECKSUM = "md5"
def __init__(self, project, config):
self.project = project
storagepath = 'gs://'
storagepath += config.get(Config.SECTION_AWS_STORAGEPATH, '/')
storagepath.lstrip('/')
def __init__(self, repo, config):
super(RemoteGS, self).__init__(repo, config)
storagepath = "gs://"
storagepath += config.get(Config.SECTION_AWS_STORAGEPATH, "/")
storagepath.lstrip("/")
self.url = config.get(Config.SECTION_REMOTE_URL, storagepath)
self.projectname = config.get(Config.SECTION_GCP_PROJECTNAME, None)
self.credentialpath = config.get(Config.SECTION_GCP_CREDENTIALPATH)
parsed = urlparse(self.url)
self.bucket = parsed.netloc
self.prefix = parsed.path.lstrip("/")
self.path_info = {"scheme": "gs", "bucket": self.bucket}
@staticmethod
def compat_config(config):
ret = config.copy()
url = 'gs://' + ret.pop(Config.SECTION_AWS_STORAGEPATH, '').lstrip('/')
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
@property
def prefix(self):
return urlparse(self.url).path.lstrip('/')
@property
def gs(self):
return storage.Client()
return (
storage.Client.from_service_account_json(self.credentialpath)
if self.credentialpath
else storage.Client(self.projectname)
)
def get_md5(self, bucket, key):
def get_md5(self, bucket, path):
import base64
import codecs
blob = self.gs.bucket(bucket).get_blob(key)
blob = self.gs.bucket(bucket).get_blob(path)
if not blob:
return None
b64_md5 = blob.md5_hash
md5 = base64.b64decode(b64_md5)
return codecs.getencoder('hex')(md5)[0].decode('utf-8')
return codecs.getencoder("hex")(md5)[0].decode("utf-8")
def save_info(self, path_info):
if path_info['scheme'] != 'gs':
if path_info["scheme"] != "gs":
raise NotImplementedError
return {self.PARAM_MD5: self.get_md5(path_info['bucket'],
path_info['key'])}
return {
self.PARAM_CHECKSUM: self.get_md5(
path_info["bucket"], path_info["path"]
)
}
@staticmethod
def to_string(path_info):
return "{}://{}/{}".format(path_info['scheme'],
path_info['bucket'],
path_info['key'])
def changed_cache(self, md5):
key = posixpath.join(self.prefix, md5[0:2], md5[2:])
cache = {'scheme': 'gs', 'bucket': self.bucket, 'key': key}
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_cache(md5):
return True
return checksum_info != self.save_info(path_info)
def _copy(self, from_info, to_info, gs=None):
def copy(self, from_info, to_info, gs=None):
gs = gs if gs else self.gs
blob = gs.bucket(from_info['bucket']).get_blob(from_info['key'])
blob = gs.bucket(from_info["bucket"]).get_blob(from_info["path"])
if not blob:
msg = '{} doesn\'t exist in the cloud'
raise DvcException(msg.format(from_info['key']))
msg = "'{}' doesn't exist in the cloud".format(from_info["path"])
raise DvcException(msg)
bucket = self.gs.bucket(to_info['bucket'])
bucket.copy_blob(blob,
self.gs.bucket(to_info['bucket']),
new_name=to_info['key'])
def save(self, path_info):
if path_info['scheme'] != 'gs':
raise NotImplementedError
md5 = self.get_md5(path_info['bucket'], path_info['key'])
key = posixpath.join(self.prefix, md5[0:2], md5[2:])
to_info = {'scheme': 'gs', 'bucket': self.bucket, 'key': key}
self._copy(path_info, to_info)
return {self.PARAM_MD5: md5}
def checkout(self, path_info, checksum_info):
if path_info['scheme'] != 'gs':
raise NotImplementedError
md5 = checksum_info.get(self.PARAM_MD5, None)
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))
key = posixpath.join(self.prefix, md5[0:2], md5[2:])
from_info = {'scheme': 'gs', 'bucket': self.bucket, 'key': key}
self._copy(from_info, path_info)
bucket = self.gs.bucket(to_info["bucket"])
bucket.copy_blob(
blob, self.gs.bucket(to_info["bucket"]), new_name=to_info["path"]
)
def remove(self, path_info):
if path_info['scheme'] != 'gs':
if path_info["scheme"] != "gs":
raise NotImplementedError
Logger.debug("Removing gs://{}/{}".format(path_info['bucket'],
path_info['key']))
logger.debug(
"Removing gs://{}/{}".format(
path_info["bucket"], path_info["path"]
)
)
blob = self.gs.bucket(path_info['bucket']).get_blob(path_info['key'])
blob = self.gs.bucket(path_info["bucket"]).get_blob(path_info["path"])
if not blob:
return
blob.delete()
def md5s_to_path_infos(self, md5s):
return [{'scheme': 'gs',
'bucket': self.bucket,
'key': posixpath.join(self.prefix,
md5[0:2], md5[2:])} for md5 in md5s]
def _list_paths(self, bucket, prefix):
for blob in self.gs.bucket(bucket).list_blobs(prefix=prefix):
yield blob.name
def exists(self, path_infos):
ret = []
def list_cache_paths(self):
return self._list_paths(self.bucket, self.prefix)
if len(path_infos) == 0:
return ret
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] == "gs"
gs = self.gs
bucket_name = path_infos[0]['bucket']
bucket = gs.bucket(bucket_name)
for path_info in path_infos:
assert self.scheme == path_info['scheme']
assert bucket_name == path_info['bucket']
exists = False
key = path_info['key']
keys = [blob.name for blob in bucket.list_blobs(prefix=key)]
if key in keys:
exists = True
ret.append(exists)
return ret
paths = self._list_paths(path_info["bucket"], path_info["path"])
return any(path_info["path"] == path for path in paths)
def upload(self, from_infos, to_infos, names=None):
names = self._verify_path_args(to_infos, from_infos, names)
@@ -207,108 +123,91 @@ class RemoteGS(RemoteBase):
gs = self.gs
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info['scheme'] != 'gs':
if to_info["scheme"] != "gs":
raise NotImplementedError
if from_info['scheme'] != 'local':
if from_info["scheme"] != "local":
raise NotImplementedError
Logger.debug("Uploading '{}' to '{}/{}'".format(from_info['path'],
to_info['bucket'],
to_info['key']))
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info["path"], to_info["bucket"], to_info["path"]
)
)
if not name:
name = os.path.basename(from_info['path'])
name = os.path.basename(from_info["path"])
progress.update_target(name, 0, None)
try:
bucket = gs.bucket(to_info['bucket'])
blob = bucket.blob(to_info['key'])
blob.upload_from_filename(from_info['path'])
except Exception as exc:
msg = "Failed to upload '{}' to '{}/{}'"
Logger.warn(msg.format(from_info['path'],
to_info['bucket'],
to_info['key']), exc)
bucket = gs.bucket(to_info["bucket"])
blob = bucket.blob(to_info["path"])
blob.upload_from_filename(from_info["path"])
except Exception:
msg = "failed to upload '{}' to '{}/{}'"
logger.error(
msg.format(
from_info["path"], to_info["bucket"], to_info["path"]
)
)
continue
progress.finish_target(name)
def download(self,
from_infos,
to_infos,
no_progress_bar=False,
names=None):
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(from_infos, to_infos, names)
gs = self.gs
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info['scheme'] != 'gs':
if from_info["scheme"] != "gs":
raise NotImplementedError
if to_info['scheme'] == 'gs':
self._copy(from_info, to_info, gs=gs)
if to_info["scheme"] == "gs":
self.copy(from_info, to_info, gs=gs)
continue
if to_info['scheme'] != 'local':
if to_info["scheme"] != "local":
raise NotImplementedError
msg = "Downloading '{}/{}' to '{}'".format(from_info['bucket'],
from_info['key'],
to_info['path'])
Logger.debug(msg)
msg = "Downloading '{}/{}' to '{}'".format(
from_info["bucket"], from_info["path"], to_info["path"]
)
logger.debug(msg)
tmp_file = self.tmp_file(to_info['path'])
tmp_file = tmp_fname(to_info["path"])
if not name:
name = os.path.basename(to_info['path'])
name = os.path.basename(to_info["path"])
if not no_progress_bar:
# percent_cb is not available for download_to_filename, so
# lets at least update progress at keypoints(start, finish)
# lets at least update progress at pathpoints(start, finish)
progress.update_target(name, 0, None)
self._makedirs(to_info['path'])
makedirs(os.path.dirname(to_info["path"]), exist_ok=True)
try:
bucket = gs.bucket(from_info['bucket'])
blob = bucket.get_blob(from_info['key'])
bucket = gs.bucket(from_info["bucket"])
blob = bucket.get_blob(from_info["path"])
blob.download_to_filename(tmp_file)
except Exception as exc:
msg = "Failed to download '{}/{}' to '{}'"
Logger.warn(msg.format(from_info['bucket'],
from_info['key'],
to_info['path']), exc)
except Exception:
msg = "failed to download '{}/{}' to '{}'"
logger.error(
msg.format(
from_info["bucket"], from_info["path"], to_info["path"]
)
)
continue
os.rename(tmp_file, to_info['path'])
move(tmp_file, to_info["path"])
if not no_progress_bar:
progress.finish_target(name)
def _path_to_md5(self, path):
relpath = posixpath.relpath(path, self.prefix)
return posixpath.dirname(relpath) + posixpath.basename(relpath)
def _all_md5s(self):
blobs = self.gs.bucket(self.bucket).list_blobs(prefix=self.prefix)
blobs = list(blobs)
return [self._path_to_md5(blob.name) for blob in blobs]
def gc(self, cinfos):
used = [info[self.PARAM_MD5] for info in cinfos['gs']]
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': 'gs',
'bucket': self.bucket,
'key': posixpath.join(self.prefix,
md5[0:2], md5[2:])}
self.remove(path_info)
removed = True
return removed
+109 -193
View File
@@ -1,55 +1,58 @@
from __future__ import unicode_literals
import os
import re
import getpass
import posixpath
from subprocess import Popen, PIPE
import dvc.logger as logger
from dvc.config import Config
from dvc.remote.base import RemoteBase, RemoteBaseCmdError
from dvc.remote.local import RemoteLOCAL
from dvc.logger import Logger
from dvc.utils import fix_env
class RemoteHDFSCmdError(RemoteBaseCmdError):
pass
from dvc.remote.base import RemoteBase, RemoteCmdError
from dvc.utils import fix_env, tmp_fname
class RemoteHDFS(RemoteBase):
scheme = 'hdfs'
REGEX = r'^hdfs://((?P<user>.*)@)?.*$'
PARAM_CHECKSUM = 'checksum'
scheme = "hdfs"
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
PARAM_CHECKSUM = "checksum"
def __init__(self, project, config):
self.project = project
self.url = config.get(Config.SECTION_REMOTE_URL, '/')
self.user = self.group('user')
def __init__(self, repo, config):
super(RemoteHDFS, self).__init__(repo, config)
self.url = config.get(Config.SECTION_REMOTE_URL, "/")
self.prefix = self.url
self.user = self.group("user")
if not self.user:
self.user = config.get(Config.SECTION_REMOTE_USER,
getpass.getuser())
self.user = config.get(
Config.SECTION_REMOTE_USER, getpass.getuser()
)
self.path_info = {"scheme": "hdfs", "user": self.user}
def hadoop_fs(self, cmd, user=None):
cmd = 'hadoop fs -' + cmd
cmd = "hadoop fs -" + cmd
if user:
cmd = 'HADOOP_USER_NAME={} '.format(user) + cmd
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')
close_fds = os.name != "nt"
executable = os.getenv('SHELL') if os.name != 'nt' else None
p = Popen(cmd,
shell=True,
close_fds=close_fds,
executable=executable,
env=fix_env(os.environ),
stdin=PIPE,
stdout=PIPE,
stderr=PIPE)
executable = os.getenv("SHELL") if os.name != "nt" else None
p = Popen(
cmd,
shell=True,
close_fds=close_fds,
executable=executable,
env=fix_env(os.environ),
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
)
out, err = p.communicate()
if p.returncode != 0:
raise RemoteHDFSCmdError(cmd, p.returncode, err)
return out.decode('utf-8')
raise RemoteCmdError(self.scheme, cmd, p.returncode, err)
return out.decode("utf-8")
@staticmethod
def _group(regex, s, gname):
@@ -58,212 +61,125 @@ class RemoteHDFS(RemoteBase):
return match.group(gname)
def checksum(self, path_info):
regex = r'.*\t.*\t(?P<checksum>.*)'
stdout = self.hadoop_fs('checksum {}'.format(path_info['url']),
user=path_info['user'])
return self._group(regex, stdout, 'checksum')
regex = r".*\t.*\t(?P<checksum>.*)"
stdout = self.hadoop_fs(
"checksum {}".format(path_info["path"]), user=path_info["user"]
)
return self._group(regex, stdout, "checksum")
def cp(self, from_info, to_info):
self.hadoop_fs('mkdir -p {}'.format(posixpath.dirname(to_info['url'])),
user=to_info['user'])
self.hadoop_fs('cp -f {} {}'.format(from_info['url'], to_info['url']),
user=to_info['user'])
def copy(self, from_info, to_info):
dname = posixpath.dirname(to_info["path"])
self.hadoop_fs("mkdir -p {}".format(dname), user=to_info["user"])
self.hadoop_fs(
"cp -f {} {}".format(from_info["path"], to_info["path"]),
user=to_info["user"],
)
def rm(self, path_info):
self.hadoop_fs('rm {}'.format(path_info['url']),
user=path_info['user'])
self.hadoop_fs(
"rm {}".format(path_info["path"]), user=path_info["user"]
)
def save_info(self, path_info):
if path_info['scheme'] != 'hdfs':
if path_info["scheme"] != "hdfs":
raise NotImplementedError
assert path_info.get('url')
assert path_info.get("path")
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(checksum):
return True
return checksum_info != self.save_info(path_info)
def save(self, path_info):
if path_info['scheme'] != 'hdfs':
raise NotImplementedError
assert path_info.get('url')
checksum = self.checksum(path_info)
dest = path_info.copy()
dest['url'] = posixpath.join(self.url, checksum[0:2], checksum[2:])
self.cp(path_info, dest)
return {self.PARAM_CHECKSUM: checksum}
def checkout(self, path_info, checksum_info):
if path_info['scheme'] != 'hdfs':
raise NotImplementedError
assert path_info.get('url')
checksum = checksum_info.get(self.PARAM_CHECKSUM, None)
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:])
self.cp(src, path_info)
def remove(self, path_info):
if path_info['scheme'] != 'hdfs':
if path_info["scheme"] != "hdfs":
raise NotImplementedError
assert path_info.get('url')
assert path_info.get("path")
Logger.debug('Removing {}'.format(path_info['url']))
logger.debug("Removing {}".format(path_info["path"]))
self.rm(path_info)
def md5s_to_path_infos(self, md5s):
return [{'scheme': 'hdfs',
'user': self.user,
'url': posixpath.join(self.url,
md5[0:2], md5[2:])} for md5 in md5s]
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] == "hdfs"
def exists(self, path_infos):
ret = []
if len(path_infos) == 0:
return ret
for path_info in path_infos:
assert path_info['scheme'] == self.scheme
try:
self.hadoop_fs('test -e {}'.format(path_info['url']))
exists = True
except RemoteHDFSCmdError:
exists = False
ret.append(exists)
return ret
try:
self.hadoop_fs("test -e {}".format(path_info["path"]))
return True
except RemoteCmdError:
return False
def upload(self, from_infos, to_infos, names=None):
names = self._verify_path_args(to_infos, from_infos, names)
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info['scheme'] != 'hdfs':
if to_info["scheme"] != "hdfs":
raise NotImplementedError
if from_info['scheme'] != 'local':
if from_info["scheme"] != "local":
raise NotImplementedError
cmd = 'mkdir -p {}'.format(posixpath.dirname(to_info['url']))
self.hadoop_fs(cmd, user=to_info['user'])
self.hadoop_fs(
"mkdir -p {}".format(posixpath.dirname(to_info["path"])),
user=to_info["user"],
)
cmd = 'copyFromLocal {} {}'.format(from_info['path'],
to_info['url'])
self.hadoop_fs(cmd, user=to_info['user'])
tmp_file = tmp_fname(to_info["path"])
def download(self,
from_infos,
to_infos,
no_progress_bar=False,
names=None):
self.hadoop_fs(
"copyFromLocal {} {}".format(from_info["path"], tmp_file),
user=to_info["user"],
)
self.hadoop_fs(
"mv {} {}".format(tmp_file, to_info["path"]),
user=to_info["user"],
)
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(from_infos, to_infos, names)
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info['scheme'] != 'hdfs':
if from_info["scheme"] != "hdfs":
raise NotImplementedError
if to_info['scheme'] == 'hdfs':
self.cp(from_info, to_info)
if to_info["scheme"] == "hdfs":
self.copy(from_info, to_info)
continue
if to_info['scheme'] != 'local':
if to_info["scheme"] != "local":
raise NotImplementedError
dname = os.path.dirname(to_info['path'])
dname = os.path.dirname(to_info["path"])
if not os.path.exists(dname):
os.makedirs(dname)
cmd = 'copyToLocal {} {}'.format(from_info['url'], to_info['path'])
self.hadoop_fs(cmd, user=from_info['user'])
tmp_file = tmp_fname(to_info["path"])
def _path_to_checksum(self, path):
relpath = posixpath.relpath(path, self.url)
return posixpath.dirname(relpath) + posixpath.basename(relpath)
self.hadoop_fs(
"copyToLocal {} {}".format(from_info["path"], tmp_file),
user=from_info["user"],
)
def _all_checksums(self):
stdout = self.hadoop_fs('ls -R {}'.format(self.url))
lines = stdout.split('\n')
os.rename(tmp_file, to_info["path"])
def list_cache_paths(self):
try:
self.hadoop_fs("test -e {}".format(self.prefix))
except RemoteCmdError:
return []
stdout = self.hadoop_fs("ls -R {}".format(self.prefix))
lines = stdout.split("\n")
flist = []
for line in lines:
if not line.startswith('-'):
if not line.startswith("-"):
continue
flist.append(line.split()[-1])
return [self._path_to_checksum(path) for path in flist]
def gc(self, cinfos):
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
path_info = {'scheme': 'hdfs',
'user': self.user,
'url': posixpath.join(self.url,
checksum[0:2], checksum[2:])}
self.remove(path_info)
removed = True
return removed
return flist
+113 -59
View File
@@ -1,13 +1,16 @@
from __future__ import unicode_literals
from dvc.utils.compat import open, makedirs
import os
import threading
import requests
import posixpath
from dvc.logger import Logger
import dvc.logger as logger
from dvc.progress import progress
from dvc.exceptions import DvcException
from dvc.config import Config
from dvc.remote.base import RemoteBase
from dvc.utils import move
class ProgressBarCallback(object):
@@ -24,45 +27,50 @@ class ProgressBarCallback(object):
class RemoteHTTP(RemoteBase):
scheme = 'http'
REGEX = r'^https?://.*$'
scheme = "http"
REGEX = r"^https?://.*$"
REQUEST_TIMEOUT = 10
CHUNK_SIZE = 1000000 # Megabyte
PARAM_ETAG = 'etag'
CHUNK_SIZE = 2 ** 16
PARAM_CHECKSUM = "etag"
def __init__(self, project, config):
self.project = project
def __init__(self, repo, config):
super(RemoteHTTP, self).__init__(repo, config)
self.cache_dir = config.get(Config.SECTION_REMOTE_URL)
self.url = self.cache_dir
self.path_info = {"scheme": "http"}
@property
def prefix(self):
return self.cache_dir
def download(self,
from_infos,
to_infos,
no_progress_bar=False,
names=None):
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(to_infos, from_infos, names)
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info['scheme'] not in ['http', 'https']:
if from_info["scheme"] not in ["http", "https"]:
raise NotImplementedError
if to_info['scheme'] != 'local':
if to_info["scheme"] != "local":
raise NotImplementedError
msg = "Downloading '{}' to '{}'".format(from_info['url'],
to_info['path'])
Logger.debug(msg)
msg = "Downloading '{}' to '{}'".format(
from_info["path"], to_info["path"]
)
logger.debug(msg)
tmp_file = self.tmp_file(to_info['path'])
if not name:
name = os.path.basename(to_info['path'])
name = os.path.basename(to_info["path"])
self._makedirs(to_info['path'])
makedirs(os.path.dirname(to_info["path"]), exist_ok=True)
total = self._content_length(from_info['url'])
total = self._content_length(from_info["path"])
if no_progress_bar or not total:
cb = None
@@ -70,48 +78,55 @@ class RemoteHTTP(RemoteBase):
cb = ProgressBarCallback(name, total)
try:
self._download_to(from_info['url'], tmp_file, callback=cb)
except Exception as exc:
msg = "Failed to download '{}'".format(from_info['url'])
Logger.warn(msg, exc)
continue
self._download_to(
from_info["path"],
to_info["path"],
callback=cb,
resume=resume,
)
os.rename(tmp_file, to_info['path'])
except Exception:
msg = "failed to download '{}'".format(from_info["path"])
logger.error(msg)
continue
if not no_progress_bar:
progress.finish_target(name)
def exists(self, path_infos):
return [
bool(self._request('HEAD', path_info.get('url')))
for path_info in path_infos
]
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] in ["http", "https"]
return bool(self._request("HEAD", path_info.get("path")))
def cache_exists(self, md5s):
assert isinstance(md5s, list)
def func(md5):
return bool(self._request("HEAD", self.checksum_to_path(md5)))
return list(filter(func, md5s))
def save_info(self, path_info):
if path_info['scheme'] not in ['http', 'https']:
if path_info["scheme"] not in ["http", "https"]:
raise NotImplementedError
return {self.PARAM_ETAG: self._etag(path_info['url'])}
def md5s_to_path_infos(self, md5s):
return [
{
'scheme': 'http',
'url': posixpath.join(self.prefix, md5[0:2], md5[2:]),
}
for md5 in md5s
]
return {self.PARAM_CHECKSUM: self._etag(path_info["path"])}
def _content_length(self, url):
return self._request('HEAD', url).headers.get('Content-Length')
return self._request("HEAD", url).headers.get("Content-Length")
def _etag(self, url):
etag = self._request('HEAD', url).headers.get('ETag')
etag = self._request("HEAD", url).headers.get("ETag") or self._request(
"HEAD", url
).headers.get("Content-MD5")
if not etag:
raise DvcException("Could not find an ETag for '{}'".format(url))
raise DvcException(
"could not find an ETag or "
"Content-MD5 header for '{url}'".format(url=url)
)
if etag.startswith('W/'):
if etag.startswith("W/"):
raise DvcException(
"Weak ETags are not supported."
" (Etag: '{etag}', URL: '{url}')".format(etag=etag, url=url)
@@ -119,24 +134,63 @@ class RemoteHTTP(RemoteBase):
return etag
def _download_to(self, url, file, callback=None):
r = self._request('GET', url, stream=True)
def _download_to(self, url, target_file, callback=None, resume=False):
request = self._request("GET", url, stream=True)
partial_file = target_file + ".part"
with open(file, 'wb') as fd:
bytes_transfered = 0
mode, transferred_bytes = self._determine_mode_get_transferred_bytes(
partial_file, resume
)
for chunk in r.iter_content(chunk_size=self.CHUNK_SIZE):
fd.write(chunk)
self._validate_existing_file_size(transferred_bytes, partial_file)
self._write_request_content(
mode, partial_file, request, transferred_bytes, callback
)
move(partial_file, target_file)
def _write_request_content(
self, mode, partial_file, request, transferred_bytes, callback=None
):
with open(partial_file, mode) as fd:
for index, chunk in enumerate(
request.iter_content(chunk_size=self.CHUNK_SIZE)
):
chunk_number = index + 1
if chunk_number * self.CHUNK_SIZE > transferred_bytes:
fd.write(chunk)
fd.flush()
transferred_bytes += len(chunk)
if callback:
bytes_transfered += len(chunk)
callback(bytes_transfered)
callback(transferred_bytes)
def _validate_existing_file_size(self, bytes_transferred, partial_file):
if bytes_transferred % self.CHUNK_SIZE != 0:
raise DvcException(
"File {}, might be corrupted, please remove "
"it and retry importing".format(partial_file)
)
def _determine_mode_get_transferred_bytes(self, partial_file, resume):
if os.path.exists(partial_file) and resume:
mode = "ab"
bytes_transfered = os.path.getsize(partial_file)
else:
mode = "wb"
bytes_transfered = 0
return mode, bytes_transfered
def _request(self, method, url, **kwargs):
kwargs.setdefault('allow_redirects', True)
kwargs.setdefault('timeout', self.REQUEST_TIMEOUT)
kwargs.setdefault("allow_redirects", True)
kwargs.setdefault("timeout", self.REQUEST_TIMEOUT)
try:
return requests.request(method, url, **kwargs)
except requests.exceptions.RequestException:
raise DvcException('Could not perform a {} request'.format(method))
raise DvcException("could not perform a {} request".format(method))
def gc(self):
raise NotImplementedError
+385 -366
View File
File diff suppressed because it is too large Load Diff
+130 -237
View File
@@ -1,22 +1,19 @@
from __future__ import unicode_literals
import os
import threading
import posixpath
try:
import boto3
except ImportError:
boto3 = None
try:
from urlparse import urlparse
except ImportError:
from urllib.parse import urlparse
from dvc.logger import Logger
import dvc.logger as logger
from dvc.utils import tmp_fname, move
from dvc.utils.compat import urlparse, makedirs
from dvc.progress import progress
from dvc.config import Config
from dvc.remote.base import RemoteBase
from dvc.remote.local import RemoteLOCAL
from dvc.exceptions import DvcException
@@ -34,215 +31,126 @@ class Callback(object):
class RemoteS3(RemoteBase):
scheme = 's3'
REGEX = r'^s3://(?P<path>.*)$'
REQUIRES = {'boto3': boto3}
PARAM_ETAG = 'etag'
scheme = "s3"
REGEX = r"^s3://(?P<path>.*)$"
REQUIRES = {"boto3": boto3}
PARAM_CHECKSUM = "etag"
def __init__(self, project, config):
self.project = project
def __init__(self, repo, config):
super(RemoteS3, self).__init__(repo, config)
storagepath = 's3://{}'.format(
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 = (
os.environ.get('AWS_DEFAULT_REGION') or
config.get(Config.SECTION_AWS_REGION)
self.region = os.environ.get("AWS_DEFAULT_REGION") or config.get(
Config.SECTION_AWS_REGION
)
self.profile = (
os.environ.get('AWS_PROFILE') or
config.get(Config.SECTION_AWS_PROFILE)
self.profile = os.environ.get("AWS_PROFILE") or config.get(
Config.SECTION_AWS_PROFILE
)
self.endpoint_url = config.get(Config.SECTION_AWS_ENDPOINT_URL)
self.list_objects = config.get(Config.SECTION_AWS_LIST_OBJECTS)
self.use_ssl = config.get(Config.SECTION_AWS_USE_SSL, True)
shared_creds = config.get(Config.SECTION_AWS_CREDENTIALPATH)
if shared_creds:
os.environ.setdefault('AWS_SHARED_CREDENTIALS_FILE', shared_creds)
os.environ.setdefault("AWS_SHARED_CREDENTIALS_FILE", shared_creds)
parsed = urlparse(self.url)
self.bucket = parsed.netloc
self.prefix = parsed.path.lstrip("/")
self.path_info = {"scheme": self.scheme, "bucket": self.bucket}
@staticmethod
def compat_config(config):
ret = config.copy()
url = 's3://' + ret.pop(Config.SECTION_AWS_STORAGEPATH, '').lstrip('/')
url = "s3://" + ret.pop(Config.SECTION_AWS_STORAGEPATH, "").lstrip("/")
ret[Config.SECTION_REMOTE_URL] = url
return ret
@property
def bucket(self):
return urlparse(self.url).netloc
@property
def prefix(self):
return urlparse(self.url).path.lstrip('/')
@property
def s3(self):
session = boto3.session.Session(profile_name=self.profile,
region_name=self.region)
session = boto3.session.Session(
profile_name=self.profile, region_name=self.region
)
return session.client('s3', endpoint_url=self.endpoint_url)
return session.client(
"s3", endpoint_url=self.endpoint_url, use_ssl=self.use_ssl
)
def get_etag(self, bucket, key):
def get_etag(self, bucket, path):
try:
obj = self.s3.head_object(Bucket=bucket, Key=key)
obj = self.s3.head_object(Bucket=bucket, Key=path)
except Exception:
raise DvcException('s3://{}/{} does not exist'.format(bucket, key))
raise DvcException(
"s3://{}/{} does not exist".format(bucket, path)
)
return obj['ETag'].strip('"')
return obj["ETag"].strip('"')
def save_info(self, path_info):
if path_info['scheme'] != 's3':
if path_info["scheme"] != "s3":
raise NotImplementedError
return {self.PARAM_ETAG: self.get_etag(path_info['bucket'],
path_info['key'])}
return {
self.PARAM_CHECKSUM: self.get_etag(
path_info["bucket"], path_info["path"]
)
}
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):
def copy(self, from_info, to_info, s3=None):
s3 = s3 if s3 else self.s3
source = {'Bucket': from_info['bucket'],
'Key': from_info['key']}
self.s3.copy(source, to_info['bucket'], to_info['key'])
def save(self, path_info):
if path_info['scheme'] != 's3':
raise NotImplementedError
etag = self.get_etag(path_info['bucket'], path_info['key'])
key = posixpath.join(self.prefix, etag[0:2], etag[2:])
to_info = {'scheme': 's3', 'bucket': self.bucket, 'key': key}
self._copy(path_info, to_info)
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
etag = checksum_info.get(self.PARAM_ETAG, None)
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}
self._copy(from_info, path_info)
source = {"Bucket": from_info["bucket"], "Key": from_info["path"]}
self.s3.copy(source, to_info["bucket"], to_info["path"])
def remove(self, path_info):
if path_info['scheme'] != 's3':
if path_info["scheme"] != "s3":
raise NotImplementedError
Logger.debug('Removing s3://{}/{}'.format(path_info['bucket'],
path_info['key']))
logger.debug(
"Removing s3://{}/{}".format(
path_info["bucket"], path_info["path"]
)
)
self.s3.delete_object(Bucket=path_info['bucket'],
Key=path_info['key'])
self.s3.delete_object(
Bucket=path_info["bucket"], Key=path_info["path"]
)
def md5s_to_path_infos(self, md5s):
return [{'scheme': self.scheme,
'bucket': self.bucket,
'key': posixpath.join(self.prefix,
md5[0:2], md5[2:])} for md5 in md5s]
def _all_keys(self, bucket, prefix):
def _list_paths(self, bucket, prefix):
""" Read config for list object api, paginate through list objects."""
s3 = self.s3
keys = []
kwargs = {'Bucket': bucket,
'Prefix': prefix}
while True:
# NOTE: list_objects_v2() is 90% faster than head_object [1]
#
# [1] https://www.peterbe.com/plog/
# fastest-way-to-find-out-if-a-file-exists-in-s3
resp = s3.list_objects_v2(**kwargs)
contents = resp.get('Contents', None)
kwargs = {"Bucket": bucket, "Prefix": prefix}
if self.list_objects:
list_objects_api = "list_objects"
else:
list_objects_api = "list_objects_v2"
paginator = s3.get_paginator(list_objects_api)
for page in paginator.paginate(**kwargs):
contents = page.get("Contents", None)
if not contents:
break
continue
for item in contents:
yield item["Key"]
for obj in contents:
keys.append(obj['Key'])
def list_cache_paths(self):
return self._list_paths(self.bucket, self.prefix)
token = resp.get('NextContinuationToken', None)
if not token:
break
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] == "s3"
kwargs['ContinuationToken'] = token
return keys
def exists(self, path_infos):
ret = []
if len(path_infos) == 0:
return ret
bucket = path_infos[0]['bucket']
for path_info in path_infos:
assert path_info['scheme'] == self.scheme
assert path_info['bucket'] == bucket
exists = False
key = path_info['key']
if key in self._all_keys(bucket, key):
exists = True
ret.append(exists)
return ret
paths = self._list_paths(path_info["bucket"], path_info["path"])
return any(path_info["path"] == path for path in paths)
def upload(self, from_infos, to_infos, names=None):
names = self._verify_path_args(to_infos, from_infos, names)
@@ -250,110 +158,95 @@ class RemoteS3(RemoteBase):
s3 = self.s3
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info['scheme'] != 's3':
if to_info["scheme"] != "s3":
raise NotImplementedError
if from_info['scheme'] != 'local':
if from_info["scheme"] != "local":
raise NotImplementedError
Logger.debug("Uploading '{}' to '{}/{}'".format(from_info['path'],
to_info['bucket'],
to_info['key']))
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info["path"], to_info["bucket"], to_info["path"]
)
)
if not name:
name = os.path.basename(from_info['path'])
name = os.path.basename(from_info["path"])
total = os.path.getsize(from_info['path'])
total = os.path.getsize(from_info["path"])
cb = Callback(name, total)
try:
s3.upload_file(from_info['path'],
to_info['bucket'],
to_info['key'],
Callback=cb)
except Exception as exc:
msg = "Failed to upload '{}'".format(from_info['path'])
Logger.warn(msg, exc)
s3.upload_file(
from_info["path"],
to_info["bucket"],
to_info["path"],
Callback=cb,
)
except Exception:
msg = "failed to upload '{}'".format(from_info["path"])
logger.error(msg)
continue
progress.finish_target(name)
def download(self,
from_infos,
to_infos,
no_progress_bar=False,
names=None):
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(from_infos, to_infos, names)
s3 = self.s3
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info['scheme'] != 's3':
if from_info["scheme"] != "s3":
raise NotImplementedError
if to_info['scheme'] == 's3':
self._copy(from_info, to_info, s3=s3)
if to_info["scheme"] == "s3":
self.copy(from_info, to_info, s3=s3)
continue
if to_info['scheme'] != 'local':
if to_info["scheme"] != "local":
raise NotImplementedError
msg = "Downloading '{}/{}' to '{}'".format(from_info['bucket'],
from_info['key'],
to_info['path'])
Logger.debug(msg)
msg = "Downloading '{}/{}' to '{}'".format(
from_info["bucket"], from_info["path"], to_info["path"]
)
logger.debug(msg)
tmp_file = self.tmp_file(to_info['path'])
tmp_file = tmp_fname(to_info["path"])
if not name:
name = os.path.basename(to_info['path'])
name = os.path.basename(to_info["path"])
self._makedirs(to_info['path'])
makedirs(os.path.dirname(to_info["path"]), exist_ok=True)
try:
if no_progress_bar:
cb = None
else:
total = s3.head_object(
Bucket=from_info['bucket'],
Key=from_info['key'])['ContentLength']
Bucket=from_info["bucket"], Key=from_info["path"]
)["ContentLength"]
cb = Callback(name, total)
s3.download_file(from_info['bucket'],
from_info['key'],
tmp_file,
Callback=cb)
except Exception as exc:
msg = "Failed to download '{}/{}'".format(from_info['bucket'],
from_info['key'])
Logger.warn(msg, exc)
s3.download_file(
from_info["bucket"],
from_info["path"],
tmp_file,
Callback=cb,
)
except Exception:
msg = "failed to download '{}/{}'".format(
from_info["bucket"], from_info["path"]
)
logger.error(msg)
continue
os.rename(tmp_file, to_info['path'])
move(tmp_file, to_info["path"])
if not no_progress_bar:
progress.finish_target(name)
def _path_to_etag(self, path):
relpath = posixpath.relpath(path, self.prefix)
return posixpath.dirname(relpath) + posixpath.basename(relpath)
def _all(self):
keys = self._all_keys(self.bucket, self.prefix)
return [self._path_to_etag(key) for key in keys]
def gc(self, cinfos):
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
path_info = {'scheme': 's3',
'key': posixpath.join(self.prefix,
etag[0:2], etag[2:]),
'bucket': self.bucket}
self.remove(path_info)
removed = True
return removed
-449
View File
@@ -1,449 +0,0 @@
import os
import getpass
import posixpath
try:
import paramiko
except ImportError:
paramiko = None
from dvc.logger import Logger
from dvc.progress import progress
from dvc.remote.base import RemoteBase, RemoteBaseCmdError
from dvc.remote.local import RemoteLOCAL
from dvc.config import Config
from dvc.exceptions import DvcException
def sizeof_fmt(num, suffix='B'):
""" Convert number of bytes to human-readable string """
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, 'Y', suffix)
def percent_cb(name, complete, total):
""" Callback for updating target progress """
Logger.debug('{}: {} transferred out of {}'.format(name,
sizeof_fmt(complete),
sizeof_fmt(total)))
progress.update_target(name, complete, total)
def create_cb(name):
""" Create callback function for multipart object """
return (lambda cur, tot: percent_cb(name, cur, tot))
class RemoteSSHCmdError(RemoteBaseCmdError):
pass
class RemoteSSH(RemoteBase):
scheme = 'ssh'
# NOTE: temporarily only absolute paths are allowed
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, '/')
self.host = self.group('host')
self.user = self.group('user')
if not self.user:
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, 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())
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 = []
if len(path_infos) == 0:
return ret
host = path_infos[0]['host']
user = path_infos[0]['user']
port = path_infos[0]['port']
ssh = self.ssh(host=host,
user=user,
port=port)
for path_info in path_infos:
assert host == path_info['host']
assert user == path_info['user']
assert port == path_info['port']
try:
self._exec(ssh, 'test -e {}'.format(path_info['path']))
exists = True
except RemoteSSHCmdError:
exists = False
ret.append(exists)
ssh.close()
return ret
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()
ret = stdout.channel.recv_exit_status()
if ret != 0:
err = b''.join(stderr_chunks).decode('utf-8')
raise RemoteSSHCmdError(cmd, ret, err)
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'],
port=path_info['port'])
# Use different md5 commands depending on os
stdout = self._exec(ssh, 'uname').strip()
if stdout == 'Darwin':
md5cmd = 'md5'
index = -1
elif stdout == 'Linux':
md5cmd = 'md5sum'
index = 0
else:
msg = '\'{}\' is not supported as a remote'.format(stdout)
raise DvcException(msg)
stdout = self._exec(ssh, '{} {}'.format(md5cmd, path_info['path']))
md5 = stdout.split()[index]
ssh.close()
assert len(md5) == 32
return md5
def cp(self, from_info, to_info, ssh=None):
if from_info['scheme'] != 'ssh' or to_info['scheme'] != 'ssh':
raise NotImplementedError
assert from_info['host'] == to_info['host']
assert from_info['user'] == to_info['user']
s = ssh if ssh else self.ssh(host=from_info['host'],
user=from_info['user'],
port=from_info['port'])
dname = posixpath.dirname(to_info['path'])
self._exec(s, 'mkdir -p {}'.format(dname))
self._exec(s, 'cp {} {}'.format(from_info['path'], to_info['path']))
if not ssh:
s.close()
def save_info(self, path_info):
if path_info['scheme'] != 'ssh':
raise NotImplementedError
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_cache(md5):
return True
return checksum_info != self.save_info(path_info)
def save(self, path_info):
if path_info['scheme'] != 'ssh':
raise NotImplementedError
md5 = self.md5(path_info)
dest = path_info.copy()
dest['path'] = posixpath.join(self.prefix, md5[0:2], md5[2:])
self.cp(path_info, dest)
return {self.PARAM_MD5: md5}
def checkout(self, path_info, checksum_info):
if path_info['scheme'] != 'ssh':
raise NotImplementedError
md5 = checksum_info.get(self.PARAM_MD5, None)
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:])
self.cp(src, path_info)
def remove(self, path_info):
if path_info['scheme'] != 'ssh':
raise NotImplementedError
Logger.debug('Removing ssh://{}@{}/{}'.format(path_info['user'],
path_info['host'],
path_info['path']))
ssh = self.ssh(host=path_info['host'],
user=path_info['user'],
port=path_info['port'])
ssh.open_sftp().remove(path_info['path'])
ssh.close()
def download(self,
from_infos,
to_infos,
no_progress_bar=False,
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'],
port=from_infos[0]['port'])
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info['scheme'] != 'ssh':
raise NotImplementedError
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
if to_info['scheme'] != 'local':
raise NotImplementedError
msg = "Downloading '{}/{}' to '{}'".format(from_info['host'],
from_info['path'],
to_info['path'])
Logger.debug(msg)
if not name:
name = os.path.basename(to_info['path'])
self._makedirs(to_info['path'])
tmp_file = self.tmp_file(to_info['path'])
try:
ssh.open_sftp().get(from_info['path'],
tmp_file,
callback=create_cb(name))
except Exception as exc:
msg = "Failed to download '{}/{}' to '{}'"
Logger.warn(msg.format(from_info['host'],
from_info['path'],
to_info['path']), exc)
continue
os.rename(tmp_file, to_info['path'])
progress.finish_target(name)
ssh.close()
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'],
port=to_infos[0]['port'])
sftp = ssh.open_sftp()
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info['scheme'] != 'ssh':
raise NotImplementedError
if from_info['scheme'] != 'local':
raise NotImplementedError
Logger.debug("Uploading '{}' to '{}/{}'".format(from_info['path'],
to_info['host'],
to_info['path']))
if not name:
name = os.path.basename(from_info['path'])
dname = posixpath.dirname(to_info['path'])
self._exec(ssh, 'mkdir -p {}'.format(dname))
try:
sftp.put(from_info['path'],
to_info['path'],
callback=create_cb(name))
except Exception as exc:
msg = "Failed to upload '{}' to '{}/{}'"
Logger.warn(msg.format(from_info['path'],
to_info['host'],
to_info['path'], exc))
continue
progress.finish_target(name)
sftp.close()
ssh.close()
def _path_to_md5(self, path):
relpath = posixpath.relpath(path, self.prefix)
return posixpath.dirname(relpath) + posixpath.basename(relpath)
def _all_md5s(self):
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()
ssh.close()
return [self._path_to_md5(path) for path in flist]
def gc(self, cinfos):
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
+198
View File
@@ -0,0 +1,198 @@
from __future__ import unicode_literals
import getpass
try:
import paramiko
except ImportError:
paramiko = None
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.remote.ssh.connection import SSHConnection
from dvc.config import Config
from dvc.utils.compat import urlparse
from dvc.remote.base import RemoteBase
class RemoteSSH(RemoteBase):
scheme = "ssh"
# NOTE: we support both URL-like (ssh://[user@]host.xz[:port]/path) and
# SCP-like (ssh://[user@]host.xz:/absolute/path) urls.
REGEX = r"^ssh://.*$"
REQUIRES = {"paramiko": paramiko}
JOBS = 4
PARAM_CHECKSUM = "md5"
DEFAULT_PORT = 22
TIMEOUT = 1800
def __init__(self, repo, config):
super(RemoteSSH, self).__init__(repo, config)
self.url = config.get(Config.SECTION_REMOTE_URL, "ssh://")
parsed = urlparse(self.url)
self.host = parsed.hostname
self.user = (
config.get(Config.SECTION_REMOTE_USER)
or parsed.username
or getpass.getuser()
)
self.prefix = parsed.path or "/"
self.port = (
config.get(Config.SECTION_REMOTE_PORT)
or parsed.port
or 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
)
self.path_info = {
"scheme": "ssh",
"host": self.host,
"user": self.user,
"port": self.port,
}
def ssh(self, host=None, user=None, port=None, **kwargs):
logger.debug(
"Establishing ssh connection with '{host}' "
"through port '{port}' as user '{user}'".format(
host=host, user=user, port=port
)
)
if self.ask_password and not self.password:
self.password = prompt.password(
"Enter a private key passphrase or a password for "
"host '{host}' port '{port}' user '{user}'".format(
host=host, port=port, user=user
)
)
return SSHConnection(
host,
username=user,
port=port,
key_filename=self.keyfile,
timeout=self.timeout,
password=self.password,
)
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] == self.scheme
with self.ssh(**path_info) as ssh:
return ssh.file_exists(path_info["path"])
def md5(self, path_info):
if path_info["scheme"] != self.scheme:
raise NotImplementedError
with self.ssh(**path_info) as ssh:
return ssh.md5(path_info["path"])
def copy(self, from_info, to_info):
if (
from_info["scheme"] != self.scheme
or to_info["scheme"] != self.scheme
):
raise NotImplementedError
with self.ssh(**from_info) as ssh:
ssh.cp(from_info["path"], to_info["path"])
def save_info(self, path_info):
if path_info["scheme"] != self.scheme:
raise NotImplementedError
return {self.PARAM_CHECKSUM: self.md5(path_info)}
def remove(self, path_info):
if path_info["scheme"] != self.scheme:
raise NotImplementedError
with self.ssh(**path_info) as ssh:
ssh.remove(path_info["path"])
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(from_infos, to_infos, names)
ssh = self.ssh(**from_infos[0])
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info["scheme"] != self.scheme:
raise NotImplementedError
if to_info["scheme"] == self.scheme:
ssh.cp(from_info["path"], to_info["path"])
continue
if to_info["scheme"] != "local":
raise NotImplementedError
logger.debug(
"Downloading '{host}/{path}' to '{dest}'".format(
host=from_info["host"],
path=from_info["path"],
dest=to_info["path"],
)
)
try:
ssh.download(
from_info["path"], to_info["path"], progress_title=name
)
except Exception:
logger.error(
"failed to download '{host}/{path}' to '{dest}'".format(
host=from_info["host"],
path=from_info["path"],
dest=to_info["path"],
)
)
continue
ssh.close()
def upload(self, from_infos, to_infos, names=None):
names = self._verify_path_args(to_infos, from_infos, names)
with self.ssh(**to_infos[0]) as ssh:
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info["scheme"] != self.scheme:
raise NotImplementedError
if from_info["scheme"] != "local":
raise NotImplementedError
try:
ssh.upload(
from_info["path"], to_info["path"], progress_title=name
)
except Exception:
logger.error(
"failed to upload '{host}/{path}' to '{dest}'".format(
host=from_info["host"],
path=from_info["path"],
dest=to_info["path"],
)
)
pass
def list_cache_paths(self):
with self.ssh(**self.path_info) as ssh:
return list(ssh.walk_files(self.prefix))
+277
View File
@@ -0,0 +1,277 @@
import errno
import os
import posixpath
try:
import paramiko
except ImportError:
paramiko = None
import dvc.logger as logger
from dvc.utils import tmp_fname
from dvc.utils.compat import makedirs
from dvc.progress import progress
from dvc.exceptions import DvcException
from dvc.remote.base import RemoteCmdError
def sizeof_fmt(num, suffix="B"):
""" Convert number of bytes to human-readable string """
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, "Y", suffix)
def percent_cb(name, complete, total):
""" Callback for updating target progress """
logger.debug(
"{}: {} transferred out of {}".format(
name, sizeof_fmt(complete), sizeof_fmt(total)
)
)
progress.update_target(name, complete, total)
def create_cb(name):
""" Create callback function for multipart object """
return lambda cur, tot: percent_cb(name, cur, tot)
class SSHConnection:
def __init__(self, *args, **kwargs):
self.timeout = kwargs.get("timeout", 1800)
self._ssh = paramiko.SSHClient()
self._ssh.load_system_host_keys()
self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self._ssh.connect(*args, **kwargs)
self._sftp = None
self._sftp_alive = False
def __enter__(self):
return self
def __exit__(self, exception_type, exception_value, traceback):
self.close()
def _sftp_connect(self):
if not self._sftp or not self._sftp_alive:
self._sftp = self._ssh.open_sftp()
self._sftp_alive = True
def close(self):
if self._sftp:
self._sftp.close()
self._sftp_alive = False
self._ssh.close()
def file_exists(self, path):
self._sftp_connect()
try:
return self._sftp.stat(path)
except IOError:
return False
pass
def isdir(self, path):
from stat import S_ISDIR
self._sftp_connect()
try:
return S_ISDIR(self._sftp.stat(path).st_mode)
except IOError:
return False
def isfile(self, path):
from stat import S_ISREG
self._sftp_connect()
try:
return S_ISREG(self._sftp.stat(path).st_mode)
except IOError:
return False
def islink(self, path):
from stat import S_ISLNK
self._sftp_connect()
try:
return S_ISLNK(self._sftp.stat(path).st_mode)
except IOError:
return False
def makedirs(self, path):
self._sftp_connect()
if self.isdir(path):
return
if self.isfile(path) or self.islink(path):
raise DvcException(
"a file with the same name '{}' already exists".format(path)
)
head, tail = os.path.split(path)
if head:
self.makedirs(head)
if tail:
self._sftp.mkdir(path)
def walk_files(self, directory):
from stat import S_ISLNK, S_ISDIR, S_ISREG
self._sftp_connect()
try:
dir_entries = self._sftp.listdir_attr(directory)
except IOError as exc:
raise DvcException(
"couldn't get the '{}' remote directory files list".format(
directory
),
cause=exc,
)
for entry in dir_entries:
path = posixpath.join(directory, entry.filename)
if S_ISLNK(entry.st_mode):
path = self._sftp.readlink(directory)
entry = self._sftp.stat(path)
if S_ISDIR(entry.st_mode):
for inner_path in self.walk_files(path):
yield inner_path
elif S_ISREG(entry.st_mode):
yield path
def remove(self, path):
self._sftp_connect()
try:
self._sftp.remove(path)
except IOError as exc:
if exc.errno != errno.ENOENT:
raise
def download(self, src, dest, no_progress_bar=False, progress_title=None):
self._sftp_connect()
makedirs(os.path.dirname(dest), exist_ok=True)
tmp_file = tmp_fname(dest)
if no_progress_bar:
self._sftp.get(src, tmp_file)
else:
if not progress_title:
progress_title = os.path.basename(dest)
self._sftp.get(src, tmp_file, callback=create_cb(progress_title))
progress.finish_target(progress_title)
if os.path.exists(dest):
os.remove(dest)
os.rename(tmp_file, dest)
def upload(self, src, dest, no_progress_bar=False, progress_title=None):
self._sftp_connect()
self.makedirs(posixpath.dirname(dest))
tmp_file = tmp_fname(dest)
if no_progress_bar:
self._sftp.put(src, tmp_file)
else:
if not progress_title:
progress_title = posixpath.basename(dest)
self._sftp.put(src, tmp_file, callback=create_cb(progress_title))
progress.finish_target(progress_title)
self._sftp.rename(tmp_file, dest)
def execute(self, cmd):
stdin, stdout, stderr = self._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()
ret = stdout.channel.recv_exit_status()
if ret != 0:
err = b"".join(stderr_chunks).decode("utf-8")
raise RemoteCmdError("ssh", cmd, ret, err)
return b"".join(stdout_chunks).decode("utf-8")
def md5(self, path):
"""
Use different md5 commands depending on the OS:
- Darwin's `md5` returns BSD-style checksums by default
- Linux's `md5sum` needs the `--tag` flag for a similar output
Example:
MD5 (foo.txt) = f3d220a856b52aabbf294351e8a24300
"""
uname = self.execute("uname").strip()
command = {
"Darwin": "md5 {}".format(path),
"Linux": "md5sum --tag {}".format(path),
}.get(uname)
if not command:
raise DvcException(
"'{uname}' is not supported as a remote".format(uname=uname)
)
md5 = self.execute(command).split()[-1]
assert len(md5) == 32
return md5
def cp(self, src, dest):
self.makedirs(posixpath.dirname(dest))
self.execute("cp {} {}".format(src, dest))
+442
View File
@@ -0,0 +1,442 @@
from __future__ import unicode_literals
import os
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.exceptions import (
DvcException,
NotDvcRepoError,
OutputNotFoundError,
TargetNotDirectoryError,
)
class Repo(object):
DVC_DIR = ".dvc"
from dvc.repo.destroy import destroy
from dvc.repo.install import install
from dvc.repo.add import add
from dvc.repo.remove import remove
from dvc.repo.lock import lock as lock_stage
from dvc.repo.move import move
from dvc.repo.run import run
from dvc.repo.imp import imp
from dvc.repo.reproduce import reproduce
from dvc.repo.checkout import checkout
from dvc.repo.push import push
from dvc.repo.fetch import fetch
from dvc.repo.pull import pull
from dvc.repo.status import status
from dvc.repo.gc import gc
from dvc.repo.commit import commit
from dvc.repo.pkg import install_pkg
from dvc.repo.brancher import brancher
def __init__(self, root_dir=None):
from dvc.config import Config
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.repo.metrics import Metrics
from dvc.scm.tree import WorkingTree
root_dir = self.find_root(root_dir)
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.tree = WorkingTree()
self.scm = SCM(self.root_dir, repo=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.config.config)
core = self.config.config[Config.SECTION_CORE]
logger.set_level(core.get(Config.SECTION_CORE_LOGLEVEL))
self.cache = Cache(self)
self.cloud = DataCloud(self, config=self.config.config)
self.updater = Updater(self.dvc_dir)
self.metrics = Metrics(self)
self._ignore()
self.updater.check()
def __repr__(self):
return "Repo: '{root_dir}'".format(root_dir=self.root_dir)
@staticmethod
def find_root(root=None):
if root is None:
root = os.getcwd()
else:
root = os.path.abspath(os.path.realpath(root))
while True:
dvc_dir = os.path.join(root, Repo.DVC_DIR)
if os.path.isdir(dvc_dir):
return root
if os.path.ismount(root):
break
root = os.path.dirname(root)
raise NotDvcRepoError(root)
@staticmethod
def find_dvc_dir(root=None):
root_dir = Repo.find_root(root)
return os.path.join(root_dir, Repo.DVC_DIR)
@staticmethod
def init(root_dir=os.curdir, no_scm=False, force=False):
from dvc.repo.init import init
init(root_dir=root_dir, no_scm=no_scm, force=force)
return Repo(root_dir)
def unprotect(self, target):
path_info = {"schema": "local", "path": target}
return self.cache.local.unprotect(path_info)
def _ignore(self):
flist = [
self.state.state_file,
self.lock.lock_file,
self.config.config_local_file,
self.updater.updater_file,
self.updater.lock.lock_file,
] + self.state.temp_files
if self.cache.local.cache_dir.startswith(self.root_dir):
flist += [self.cache.local.cache_dir]
self.scm.ignore_list(flist)
def check_dag(self, stages):
"""Generate graph including the new stage to check for errors"""
self.graph(stages=stages)
@staticmethod
def _check_cyclic_graph(graph):
import networkx as nx
from dvc.exceptions import CyclicGraphError
cycles = list(nx.simple_cycles(graph))
if cycles:
raise CyclicGraphError(cycles[0])
def _get_pipeline(self, node):
pipelines = [i for i in self.pipelines() if i.has_node(node)]
assert len(pipelines) == 1
return pipelines[0]
def collect(self, target, with_deps=False):
import networkx as nx
from dvc.stage import Stage
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)
ret = []
for n in nx.dfs_postorder_nodes(G, node):
ret.append(G.node[n]["stage"])
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_CHECKSUM]
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 '{}': {}"
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 and not prompt.confirm(msg):
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:
if not out.info:
logger.warning(
"Output '{}'({}) is missing version "
"info. Cache for it will not be collected. "
"Use dvc repro to get your pipeline up to "
"date.".format(out, out.stage)
)
return []
info = out.dumpd()
info["branch"] = branch
ret = [info]
if out.scheme != "local":
return ret
md5 = info[out.remote.PARAM_CHECKSUM]
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,
recursive=False,
):
cache = {}
cache["local"] = []
cache["s3"] = []
cache["gs"] = []
cache["hdfs"] = []
cache["ssh"] = []
cache["azure"] = []
for branch in self.brancher(
all_branches=all_branches, all_tags=all_tags
):
if target:
if recursive:
stages = self.stages(target)
else:
stages = self.collect(target, with_deps=with_deps)
elif active:
stages = self.active_stages()
else:
stages = self.stages()
for stage in stages:
if active and not target and stage.locked:
logger.warning(
"DVC file '{path}' is locked. Its dependencies are"
" not going to be pushed/pulled/fetched.".format(
path=stage.relpath
)
)
for out in stage.outs:
scheme = out.path_info["scheme"]
cache[scheme] += self._collect_used_cache(
out,
branch=branch,
remote=remote,
force=force,
jobs=jobs,
)
return cache
def graph(self, stages=None, from_directory=None):
import networkx as nx
from dvc.exceptions import (
OutputDuplicationError,
StagePathAsOutputError,
OverlappingOutputPathsError,
)
G = nx.DiGraph()
G_active = nx.DiGraph()
stages = stages or self.stages(from_directory, check_dag=False)
stages = [stage for stage in stages if stage]
outs = []
for stage in stages:
for out in stage.outs:
existing = []
for o in outs:
if o.path == out.path:
existing.append(o.stage)
in_o_dir = out.path.startswith(o.path + o.sep)
in_out_dir = o.path.startswith(out.path + out.sep)
if in_o_dir or in_out_dir:
raise OverlappingOutputPathsError(o, out)
if existing:
stages = [stage.relpath, existing[0].relpath]
raise OutputDuplicationError(out.path, stages)
outs.append(out)
for stage in stages:
path_dir = os.path.dirname(stage.path) + os.sep
for out in outs:
if path_dir.startswith(out.path + os.sep):
raise StagePathAsOutputError(stage.wdir, stage.relpath)
# collect the whole DAG
for stage in stages:
node = os.path.relpath(stage.path, self.root_dir)
G.add_node(node, stage=stage)
G_active.add_node(node, stage=stage)
for dep in stage.deps:
for out in outs:
if (
out.path != dep.path
and not dep.path.startswith(out.path + out.sep)
and not out.path.startswith(dep.path + dep.sep)
):
continue
dep_stage = out.stage
dep_node = os.path.relpath(dep_stage.path, self.root_dir)
G.add_node(dep_node, stage=dep_stage)
G.add_edge(node, dep_node)
if not stage.locked:
G_active.add_node(dep_node, stage=dep_stage)
G_active.add_edge(node, dep_node)
self._check_cyclic_graph(G)
return G, G_active
def pipelines(self, from_directory=None):
import networkx as nx
G, G_active = self.graph(from_directory=from_directory)
return [
G.subgraph(c).copy() for c in nx.weakly_connected_components(G)
]
def stages(self, from_directory=None, check_dag=True):
"""
Walks down the root directory looking for Dvcfiles,
skipping the directories that are related with
any SCM (e.g. `.git`), DVC itself (`.dvc`), or directories
tracked by DVC (e.g. `dvc add data` would skip `data/`)
NOTE: For large repos, this could be an expensive
operation. Consider using some memoization.
"""
from dvc.stage import Stage
if not from_directory:
from_directory = self.root_dir
elif not os.path.isdir(from_directory):
raise TargetNotDirectoryError(from_directory)
stages = []
outs = []
for root, dirs, files in self.tree.walk(from_directory):
for fname in files:
path = os.path.join(root, fname)
if not Stage.is_valid_filename(path):
continue
stage = Stage.load(self, path)
for out in stage.outs:
outs.append(out.path + out.sep)
stages.append(stage)
def filter_dirs(dname, root=root):
path = os.path.join(root, dname)
if path in (self.dvc_dir, 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))
if check_dag:
self.check_dag(stages)
return stages
def active_stages(self, from_directory=None):
import networkx as nx
stages = []
for G in self.pipelines(from_directory):
stages.extend(list(nx.get_node_attributes(G, "stage").values()))
return stages
def find_outs_by_path(self, path, outs=None, recursive=False):
if not outs:
# there is no `from_directory=path` argument because some data
# files might be generated to an upper level, and so it is
# needed to look at all the files (project root_dir)
stages = self.stages()
outs = [out for stage in stages for out in stage.outs]
abs_path = os.path.abspath(path)
is_dir = self.tree.isdir(abs_path)
def func(out):
if out.path == abs_path:
return True
if is_dir and recursive and out.path.startswith(abs_path + os.sep):
return True
return False
matched = list(filter(func, outs))
if not matched:
raise OutputNotFoundError(path)
return matched
def is_dvc_internal(self, path):
path_parts = os.path.normpath(path).split(os.path.sep)
return self.DVC_DIR in path_parts
+55
View File
@@ -0,0 +1,55 @@
import os
from dvc.repo.scm_context import scm_context
from dvc.stage import Stage
from dvc.utils import walk_files
from dvc.exceptions import RecursiveAddingWhileUsingFilename
@scm_context
def add(repo, target, recursive=False, no_commit=False, fname=None):
if recursive and fname:
raise RecursiveAddingWhileUsingFilename()
targets = _find_all_targets(repo, target, recursive)
stages = _create_stages(repo, targets, fname, no_commit)
repo.check_dag(repo.stages() + stages)
for stage in stages:
stage.dump()
return stages
def _find_all_targets(repo, target, recursive):
if os.path.isdir(target) and recursive:
return [
file
for file in walk_files(target)
if not repo.is_dvc_internal(file)
if not Stage.is_stage_file(file)
if not repo.scm.belongs_to_scm(file)
if not repo.scm.is_tracked(file)
]
return [target]
def _create_stages(repo, targets, fname, no_commit):
stages = []
with repo.state:
for out in targets:
stage = Stage.create(repo=repo, outs=[out], add=True, fname=fname)
if not stage:
continue
stage.save()
if not no_commit:
stage.commit()
stages.append(stage)
return stages
+56
View File
@@ -0,0 +1,56 @@
def brancher( # noqa: E302
self, branches=None, all_branches=False, tags=None, all_tags=False
):
"""Generator that iterates over specified revisions.
Args:
branches (list): a list of branches to iterate over.
all_branches (bool): iterate over all available branches.
tags (list): a list of tags to iterate over.
all_tags (bool): iterate over all available tags.
Yields:
str: the display name for the currently selected tree, it could be:
- a git revision identifier
- empty string it there is no branches to iterate over
- "Working Tree" if there are uncommited changes in the SCM repo
"""
if not any([branches, all_branches, tags, all_tags]):
yield ""
return
saved_tree = self.tree
revs = []
scm = self.scm
if self.scm.is_dirty():
from dvc.scm.tree import WorkingTree
self.tree = WorkingTree()
yield "Working Tree"
if all_branches:
branches = scm.list_branches()
if all_tags:
tags = scm.list_tags()
if branches is None:
revs.extend([scm.active_branch()])
else:
revs.extend(branches)
if tags is not None:
revs.extend(tags)
# NOTE: it might be a good idea to wrap this loop in try/finally block
# to don't leave the tree on some unexpected branch after the
# `brancher()`, but this could cause problems on exception handling
# code which might expect the tree on which exception was raised to
# stay in place. This behavior is a subject to change.
for rev in revs:
self.tree = scm.get_tree(rev)
yield rev
self.tree = saved_tree
+55
View File
@@ -0,0 +1,55 @@
from __future__ import unicode_literals
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.progress import ProgressCallback
def _cleanup_unused_links(self, all_stages):
used = []
for stage in all_stages:
for out in stage.outs:
used.append(out.path)
self.state.remove_unused_links(used)
def get_all_files_numbers(stages):
return sum(stage.get_all_files_number() for stage in stages)
def get_progress_callback(stages):
total_files_num = get_all_files_numbers(stages)
if total_files_num == 0:
return None
return ProgressCallback(total_files_num)
def checkout(self, target=None, with_deps=False, force=False, recursive=False):
from dvc.stage import StageFileDoesNotExistError, StageFileBadNameError
all_stages = self.stages()
if target and not recursive:
try:
stages = self.collect(target, with_deps=with_deps)
except (StageFileDoesNotExistError, StageFileBadNameError) as exc:
raise DvcException(
str(exc) + " Did you mean 'git checkout {}'?".format(target)
)
else:
stages = self.active_stages(target)
with self.state:
_cleanup_unused_links(self, all_stages)
progress_callback = get_progress_callback(stages)
for stage in stages:
if stage.locked:
logger.warning(
"DVC file '{path}' is locked. Its dependencies are"
" not going to be checked out.".format(path=stage.relpath)
)
stage.checkout(force=force, progress_callback=progress_callback)
if progress_callback:
progress_callback.finish("Checkout finished!")
+11
View File
@@ -0,0 +1,11 @@
def commit(self, target, with_deps=False, recursive=False, force=False):
if target and not recursive:
stages = self.collect(target, with_deps=with_deps)
else:
stages = self.active_stages(target)
with self.state:
for stage in stages:
stage.check_can_commit(force=force)
stage.commit()
stage.dump()
+8
View File
@@ -0,0 +1,8 @@
import shutil
def destroy(self):
for stage in self.stages():
stage.remove()
shutil.rmtree(self.dvc_dir)
+28
View File
@@ -0,0 +1,28 @@
from __future__ import unicode_literals
def fetch(
self,
target=None,
jobs=1,
remote=None,
all_branches=False,
show_checksums=False,
with_deps=False,
all_tags=False,
recursive=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,
recursive=recursive,
)["local"]
return self.cloud.pull(
used, jobs, remote=remote, show_checksums=show_checksums
)
+111
View File
@@ -0,0 +1,111 @@
from __future__ import unicode_literals
import collections
import dvc.logger as logger
def _merge_cache_lists(clists):
merged_cache = collections.defaultdict(list)
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)
return merged_cache
def _load_all_used_cache(
repos,
target=None,
all_branches=False,
active=True,
with_deps=False,
all_tags=False,
remote=None,
force=False,
jobs=None,
):
clists = []
for repo in repos:
with repo.state:
repo_clist = repo.used_cache(
target=None,
all_branches=all_branches,
active=False,
with_deps=with_deps,
all_tags=all_tags,
remote=remote,
force=force,
jobs=jobs,
)
clists.append(repo_clist)
return clists
def _do_gc(typ, func, clist):
removed = func(clist)
if not removed:
logger.info("No unused {} cache to remove.".format(typ))
def gc(
self,
all_branches=False,
cloud=False,
remote=None,
with_deps=False,
all_tags=False,
force=False,
jobs=None,
repos=None,
):
from dvc.repo import Repo
all_repos = [self]
if repos:
all_repos.extend(Repo(path) for path in repos)
all_clists = _load_all_used_cache(
all_repos,
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 = _merge_cache_lists(all_clists)
else:
clist = all_clists[0]
with self.state:
_do_gc("local", self.cache.local.gc, clist)
if self.cache.s3:
_do_gc("s3", self.cache.s3.gc, clist)
if self.cache.gs:
_do_gc("gs", self.cache.gs.gc, clist)
if self.cache.ssh:
_do_gc("ssh", self.cache.ssh.gc, clist)
if self.cache.hdfs:
_do_gc("hdfs", self.cache.hdfs.gc, clist)
if self.cache.azure:
_do_gc("azure", self.cache.azure.gc, clist)
if cloud:
_do_gc("remote", self.cloud._get_cloud(remote, "gc -c").gc, clist)
+21
View File
@@ -0,0 +1,21 @@
from dvc.repo.scm_context import scm_context
@scm_context
def imp(self, url, out, resume=False):
from dvc.stage import Stage
with self.state:
stage = Stage.create(repo=self, cmd=None, deps=[url], outs=[out])
if stage is None:
return None
self.check_dag(self.stages() + [stage])
with self.state:
stage.run(resume=resume)
stage.dump()
return stage
+90
View File
@@ -0,0 +1,90 @@
import os
import shutil
import colorama
import dvc.logger as logger
from dvc.repo import Repo
from dvc.scm import SCM, NoSCM
from dvc.config import Config
from dvc.exceptions import InitError
def _welcome_message():
logger.box(
"DVC has enabled anonymous aggregate usage analytics.\n"
"Read the analytics documentation (and how to opt-out) here:\n"
"{blue}https://dvc.org/doc/user-guide/analytics{nc}".format(
blue=colorama.Fore.BLUE, nc=colorama.Fore.RESET
),
border_color="red",
)
msg = (
"{yellow}What's next?{nc}\n"
"{yellow}------------{nc}\n"
"- Check out the documentation: {blue}https://dvc.org/doc{nc}\n"
"- Get help and share ideas: {blue}https://dvc.org/chat{nc}\n"
"- Star us on GitHub: {blue}https://github.com/iterative/dvc{nc}"
).format(
yellow=colorama.Fore.YELLOW,
blue=colorama.Fore.BLUE,
nc=colorama.Fore.RESET,
)
logger.info(msg)
def init(root_dir=os.curdir, no_scm=False, force=False):
"""
Creates an empty repo on the given directory -- basically a
`.dvc` directory with subdirectories for configuration and cache.
It should be tracked by a SCM or use the `--no-scm` flag.
If the given directory is not empty, you must use the `--force`
flag to override it.
Args:
root_dir: Path to repo's root directory.
Returns:
Repo instance.
Raises:
KeyError: Raises an exception.
"""
root_dir = os.path.abspath(root_dir)
dvc_dir = os.path.join(root_dir, Repo.DVC_DIR)
scm = SCM(root_dir)
if isinstance(scm, NoSCM) and not no_scm:
raise InitError(
"{repo} is not tracked by any supported scm tool (e.g. git). "
"Use '--no-scm' if you don't want to use any scm.".format(
repo=root_dir
)
)
if os.path.isdir(dvc_dir):
if not force:
raise InitError(
"'{repo}' exists. Use '-f' to force.".format(
repo=os.path.relpath(dvc_dir)
)
)
shutil.rmtree(dvc_dir)
os.mkdir(dvc_dir)
config = Config.init(dvc_dir)
proj = Repo(root_dir)
scm.add([config.config_file])
if scm.ignore_file:
scm.add([os.path.join(dvc_dir, scm.ignore_file)])
logger.info("\nYou can now commit the changes to git.\n")
_welcome_message()
return proj
+2
View File
@@ -0,0 +1,2 @@
def install(self):
self.scm.install()
+8
View File
@@ -0,0 +1,8 @@
def lock(self, target, unlock=False):
from dvc.stage import Stage
stage = Stage.load(self, target)
stage.locked = False if unlock else True
stage.dump()
return stage
+23
View File
@@ -0,0 +1,23 @@
class Metrics(object):
def __init__(self, repo):
self.repo = repo
def add(self, *args, **kwargs):
from dvc.repo.metrics.add import add
return add(self.repo, *args, **kwargs)
def modify(self, *args, **kwargs):
from dvc.repo.metrics.modify import modify
return modify(self.repo, *args, **kwargs)
def show(self, *args, **kwargs):
from dvc.repo.metrics.show import show
return show(self.repo, *args, **kwargs)
def remove(self, *args, **kwargs):
from dvc.repo.metrics.remove import remove
return remove(self.repo, *args, **kwargs)
+7
View File
@@ -0,0 +1,7 @@
from dvc.repo.metrics.modify import modify
def add(repo, path, typ=None, xpath=None):
if not typ:
typ = "raw"
modify(repo, path, typ, xpath)
+40
View File
@@ -0,0 +1,40 @@
from __future__ import unicode_literals
from dvc.exceptions import DvcException
def modify(repo, path, typ=None, xpath=None, delete=False):
supported_types = ["raw", "json", "csv", "tsv", "hcsv", "htsv"]
outs = repo.find_outs_by_path(path)
assert len(outs) == 1
out = outs[0]
if out.scheme != "local":
msg = "output '{}' scheme '{}' is not supported for metrics"
raise DvcException(msg.format(out.path, out.path_info["scheme"]))
if typ is not None:
typ = typ.lower().strip()
if typ not in ["raw", "json", "csv", "tsv", "hcsv", "htsv"]:
msg = (
"metric type '{typ}' is not supported, "
"must be one of [{types}]"
)
raise DvcException(
msg.format(typ=typ, types=", ".join(supported_types))
)
if not isinstance(out.metric, dict):
out.metric = {}
out.metric[out.PARAM_METRIC_TYPE] = typ
if xpath is not None:
if not isinstance(out.metric, dict):
out.metric = {}
out.metric[out.PARAM_METRIC_XPATH] = xpath
if delete:
out.metric = None
out.verify_metric()
out.stage.dump()
+5
View File
@@ -0,0 +1,5 @@
from dvc.repo.metrics.modify import modify
def remove(repo, path):
modify(repo, path, delete=True)
+176
View File
@@ -0,0 +1,176 @@
from __future__ import unicode_literals
import os
import csv
import json
from jsonpath_rw import parse
import dvc.logger as logger
from dvc.exceptions import OutputNotFoundError, BadMetricError, NoMetricsError
from dvc.utils.compat import builtin_str, open
def _read_metric_json(fd, json_path):
parser = parse(json_path)
return [x.value for x in parser.find(json.load(fd))]
def _get_values(row):
if isinstance(row, dict):
return list(row.values())
else:
return row
def _do_read_metric_xsv(reader, row, col):
if col is not None and row is not None:
return [reader[row][col]]
elif col is not None:
return [r[col] for r in reader]
elif row is not None:
return _get_values(reader[row])
return [_get_values(r) for r in reader]
def _read_metric_hxsv(fd, hxsv_path, delimiter):
indices = hxsv_path.split(",")
row = indices[0]
row = int(row) if row else None
col = indices[1] if len(indices) > 1 and indices[1] else None
reader = list(csv.DictReader(fd, delimiter=builtin_str(delimiter)))
return _do_read_metric_xsv(reader, row, col)
def _read_metric_xsv(fd, xsv_path, delimiter):
indices = xsv_path.split(",")
row = indices[0]
row = int(row) if row else None
col = int(indices[1]) if len(indices) > 1 and indices[1] else None
reader = list(csv.reader(fd, delimiter=builtin_str(delimiter)))
return _do_read_metric_xsv(reader, row, col)
def _read_typed_metric(typ, xpath, fd):
if typ == "json":
ret = _read_metric_json(fd, xpath)
elif typ == "csv":
ret = _read_metric_xsv(fd, xpath, ",")
elif typ == "tsv":
ret = _read_metric_xsv(fd, xpath, "\t")
elif typ == "hcsv":
ret = _read_metric_hxsv(fd, xpath, ",")
elif typ == "htsv":
ret = _read_metric_hxsv(fd, xpath, "\t")
else:
ret = fd.read().strip()
return ret
def _read_metric(fd, typ=None, xpath=None, rel_path=None, branch=None):
typ = typ.lower().strip() if typ else typ
try:
if xpath:
return _read_typed_metric(typ, xpath.strip(), fd)
else:
return fd.read().strip()
# Json path library has to be replaced or wrapped in
# order to fix this too broad except clause.
except Exception:
logger.warning(
"unable to read metric in '{}' in branch '{}'".format(
rel_path, branch
),
parse_exception=True,
)
return None
def _collect_metrics(self, path, recursive, typ, xpath, branch):
outs = [out for stage in self.stages() for out in stage.outs]
if path:
try:
outs = self.find_outs_by_path(path, outs=outs, recursive=recursive)
except OutputNotFoundError:
logger.debug(
"stage file not for found for '{}' in branch '{}'".format(
path, branch
)
)
return []
res = []
for o in outs:
if not o.metric:
continue
if not typ and isinstance(o.metric, dict):
t = o.metric.get(o.PARAM_METRIC_TYPE, typ)
x = o.metric.get(o.PARAM_METRIC_XPATH, xpath)
else:
t = typ
x = xpath
res.append((o, t, x))
return res
def _read_metrics_filesystem(path, typ, xpath, rel_path, branch):
if not os.path.exists(path):
return None
with open(path, "r") as fd:
return _read_metric(
fd, typ=typ, xpath=xpath, rel_path=rel_path, branch=branch
)
def _read_metrics(self, metrics, branch):
res = {}
for out, typ, xpath in metrics:
assert out.scheme == "local"
if out.use_cache:
metric = _read_metrics_filesystem(
self.cache.local.get(out.checksum),
typ=typ,
xpath=xpath,
rel_path=out.rel_path,
branch=branch,
)
else:
fd = self.tree.open(out.path)
metric = _read_metric(
fd, typ=typ, xpath=xpath, rel_path=out.rel_path, branch=branch
)
if not metric:
continue
res[out.rel_path] = metric
return res
def show(
self,
path=None,
typ=None,
xpath=None,
all_branches=False,
all_tags=False,
recursive=False,
):
res = {}
for branch in self.brancher(all_branches=all_branches, all_tags=all_tags):
entries = _collect_metrics(self, path, recursive, typ, xpath, branch)
metrics = _read_metrics(self, entries, branch)
if metrics:
res[branch] = metrics
if not res:
if path:
raise BadMetricError(path)
raise NoMetricsError()
return res
+68
View File
@@ -0,0 +1,68 @@
import os
from dvc.exceptions import MoveNotDataSourceError
from dvc.repo.scm_context import scm_context
def _expand_target_path(from_path, to_path):
if os.path.isdir(to_path) and not os.path.isdir(from_path):
return os.path.join(to_path, os.path.basename(from_path))
return to_path
@scm_context
def move(self, from_path, to_path):
"""
Renames an output file and modifies the stage associated
to reflect the change on the pipeline.
If the output has the same name as its stage, it would
also rename the corresponding stage file.
E.g.
Having: (hello, hello.dvc)
$ dvc move hello greetings
Result: (greeting, greeting.dvc)
It only works with outputs generated by `add` or `import`,
also known as data sources.
"""
import dvc.output as Output
from dvc.stage import Stage
from_out = Output.loads_from(Stage(self), [from_path])[0]
to_path = _expand_target_path(from_path, to_path)
outs = self.find_outs_by_path(from_out.path)
assert len(outs) == 1
out = outs[0]
stage = out.stage
if not stage.is_data_source:
raise MoveNotDataSourceError(stage.relpath)
stage_name = os.path.splitext(os.path.basename(stage.path))[0]
from_name = os.path.basename(from_out.path)
if stage_name == from_name:
os.unlink(stage.path)
stage.path = os.path.join(
os.path.dirname(to_path),
os.path.basename(to_path) + Stage.STAGE_FILE_SUFFIX,
)
stage.wdir = os.path.abspath(
os.path.join(os.curdir, os.path.dirname(to_path))
)
to_out = Output.loads_from(
stage, [os.path.basename(to_path)], out.cache, out.metric
)[0]
with self.state:
out.move(to_out)
stage.dump()
+201
View File
@@ -0,0 +1,201 @@
from __future__ import unicode_literals
import os
import shutil
from git.cmd import Git
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.stage import Stage
from dvc.temp_git_repo import TempGitRepo
class PackageParams(object):
def __init__(self, address, target_dir, select=[], file=None):
self.address = address
self.target_dir = target_dir
self.select = select
self.file = file
@property
def all_addresses(self):
if self.address:
return [self.address]
return PackageManager.read_packages()
class PackageManager(object):
PACKAGE_FILE = "package.yaml"
@staticmethod
def read_packages():
return []
@staticmethod
def get_package(addr):
for pkg_class in [GitPackage]:
try:
return pkg_class()
except Exception:
pass
return None
def __init__(self, addr):
self._addr = addr
class Package(object):
MODULES_DIR = "dvc_mod"
def install_or_update(self, parent_repo, pkg_param):
raise NotImplementedError(
"A method of abstract Package class was called"
)
def is_in_root(self):
return True
class GitPackage(Package):
DEF_DVC_FILE_PREFIX = "mod_"
def install_or_update(self, parent_repo, pkg_params):
if not self.is_in_root():
raise DvcException(
"This command can be run only from a repository root"
)
if not os.path.exists(self.MODULES_DIR):
logger.debug("Creating modules dir {}".format(self.MODULES_DIR))
os.makedirs(self.MODULES_DIR)
parent_repo.scm.ignore(os.path.abspath(self.MODULES_DIR))
module_name = (
Git.polish_url(pkg_params.address).strip("/").split("/")[-1]
)
if not module_name:
raise DvcException(
"Package address error: unable to extract package name"
)
with TempGitRepo(
pkg_params.address, module_name, Package.MODULES_DIR
) as tmp_repo:
outputs_to_copy = tmp_repo.outs
if pkg_params.select:
outputs_to_copy = list(
filter(
lambda out: out.dvc_path in pkg_params.select,
outputs_to_copy,
)
)
fetched_stage_files = set(
map(lambda o: o.stage.path, outputs_to_copy)
)
tmp_repo.fetch(fetched_stage_files)
module_dir = self.create_module_dir(module_name)
tmp_repo.persist_to(module_dir, parent_repo)
dvc_file = self.get_dvc_file_name(
pkg_params.file, pkg_params.target_dir, module_name
)
try:
self.persist_stage_and_scm_state(
parent_repo,
outputs_to_copy,
pkg_params.target_dir,
dvc_file,
)
except Exception as ex:
raise DvcException(
"Package '{}' was installed "
"but stage file '{}' "
"was not created properly: {}".format(
pkg_params.address, dvc_file, ex
)
)
parent_repo.checkout(dvc_file)
@staticmethod
def persist_stage_and_scm_state(
parent_repo, outputs_to_copy, target_dir, dvc_file
):
stage = Stage.create(
repo=parent_repo,
fname=dvc_file,
validate_state=False,
wdir=target_dir,
)
stage.outs = list(
map(lambda o: o.assign_to_stage_file(stage), outputs_to_copy)
)
for out in stage.outs:
parent_repo.scm.ignore(out.path, in_curr_dir=True)
stage.dump()
@staticmethod
def create_module_dir(module_name):
module_dir = os.path.join(GitPackage.MODULES_DIR, module_name)
if os.path.exists(module_dir):
logger.info("Updating package {}".format(module_name))
shutil.rmtree(module_dir)
else:
logger.info("Adding package {}".format(module_name))
return module_dir
def get_dvc_file_name(self, stage_file, target_dir, module_name):
if stage_file:
dvc_file_path = stage_file
else:
dvc_file_name = self.DEF_DVC_FILE_PREFIX + module_name + ".dvc"
dvc_file_path = os.path.join(target_dir, dvc_file_name)
return dvc_file_path
def install_pkg(self, pkg_params):
"""
Install package.
The command can be run only from DVC project root.
E.g.
Having: DVC package in https://github.com/dmpetrov/tag_classifier
$ dvc pkg install https://github.com/dmpetrov/tag_classifier
Result: tag_classifier package in dvc_mod/ directory
"""
if not os.path.isdir(pkg_params.target_dir):
logger.error(
"Unable to install package: "
"target directory '{}' does not exist".format(
pkg_params.target_dir
)
)
return 1
curr_dir = os.path.realpath(os.curdir)
if not os.path.realpath(pkg_params.target_dir).startswith(curr_dir):
logger.error(
"Unable to install package: the current dir should be"
" a subdirectory of the target dir {}".format(
pkg_params.target_dir
)
)
return 1
for addr in pkg_params.all_addresses:
try:
mgr = PackageManager.get_package(addr)
mgr.install_or_update(self, pkg_params)
except Exception as ex:
logger.error("Unable to install package: ".format(ex))
return 1
return 0
+29
View File
@@ -0,0 +1,29 @@
from __future__ import unicode_literals
def pull(
self,
target=None,
jobs=1,
remote=None,
all_branches=False,
show_checksums=False,
with_deps=False,
all_tags=False,
force=False,
recursive=False,
):
processed_files_count = self.fetch(
target,
jobs,
remote=remote,
all_branches=all_branches,
all_tags=all_tags,
show_checksums=show_checksums,
with_deps=with_deps,
recursive=recursive,
)
self.checkout(
target=target, with_deps=with_deps, force=force, recursive=recursive
)
return processed_files_count
+28
View File
@@ -0,0 +1,28 @@
from __future__ import unicode_literals
def push(
self,
target=None,
jobs=1,
remote=None,
all_branches=False,
show_checksums=False,
with_deps=False,
all_tags=False,
recursive=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,
recursive=recursive,
)["local"]
return self.cloud.push(
used, jobs, remote=remote, show_checksums=show_checksums
)

Some files were not shown because too many files have changed in this diff Show More