Compare commits

..
207 Commits
Author SHA1 Message Date
Ruslan Kuprieiev c5f6cb0f9e dvc: bump to 0.50.1 2019-06-26 18:10:41 +03:00
Ruslan Kuprieiev d6d0b25173 appveyor: don't use setup.py for installing
Related https://github.com/iterative/dvc/issues/2191
2019-06-26 17:55:15 +03:00
Ruslan Kuprieiev 5e6ce00a6b Merge pull request #2193 from shcheklein/fix-s3-config-propagation
fix s3 credential handling: DVC config overrides environment
2019-06-26 17:46:38 +03:00
Ruslan Kuprieiev bbd31cdb19 Merge pull request #2189 from Suor/some-fixes
Fix relpath() with PathInfo
2019-06-26 16:45:20 +03:00
Alexander Schepanovski bdd5ca48da dvc: fix relpath()
- accept Path-like object as start in Python3.5-
- always return str

Closes #2188.
2019-06-26 15:19:22 +07:00
Ivan Shcheklein d4607d8050 fix s3 credential handling: DVC config overrides environment 2019-06-25 21:02:33 -07:00
Ruslan Kuprieiev a54a34042e appveyor: don't install dvc as editable 2019-06-25 23:13:16 +03:00
Ruslan Kuprieiev e0bfa0a8ac Merge pull request #2190 from Suor/fix-run
dvc: check stage name before running command
2019-06-25 16:41:46 +03:00
Alexander Schepanovski 6e761c7e7a dvc: check stage name before running command
Closes #2150.
2019-06-25 19:21:16 +07:00
Ruslan Kuprieiev 60ba9399da build: quote description 2019-06-25 15:11:21 +03:00
Ruslan Kuprieiev dd6ed7e7e9 test: s3: remove debug marker 2019-06-25 13:13:26 +03:00
Ruslan Kuprieiev a803e6a8e8 appveyor: fix typo in pip uninstall command 2019-06-25 12:47:02 +03:00
Ruslan Kuprieiev 55aad18a24 appveyor: install awscli from chocolatey 2019-06-25 12:13:37 +03:00
Ruslan Kuprieiev d58341b57c Merge pull request #2162 from Suor/api-remote-repo
dvc: add repo as url support to API
2019-06-25 11:47:33 +03:00
Alexander Schepanovski 4d248b4183 dvc: support external repos in api functions 2019-06-25 13:08:03 +07:00
Ruslan Kuprieiev dceca73f00 tests: debug appveyor s3 failure 2019-06-25 05:26:45 +03:00
Ruslan Kuprieiev 806064dcec Merge pull request #2186 from efiop/update
appveyor: require py3 to pass
2019-06-25 05:25:23 +03:00
Ruslan Kuprieiev babc4a7f7e appveyor: require py3 to pass
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-25 04:48:51 +03:00
Ruslan Kuprieiev 505403c864 build: use quotes for pkg description 2019-06-25 04:40:43 +03:00
Ruslan Kuprieiev cebc230ae4 import: don't use deprecated logger.warn() 2019-06-25 04:39:09 +03:00
Ruslan Kuprieiev d91700a3c7 Merge pull request #2184 from pared/1915
state: use pep-0249 sql query formatting
2019-06-25 03:46:16 +03:00
Ruslan Kuprieiev 52cda10af8 dvc: bump to 0.50.0 2019-06-25 02:12:16 +03:00
Ruslan Kuprieiev 7ed4840abc Merge pull request #2185 from efiop/update
import: add "beta feature" warning
2019-06-25 02:11:29 +03:00
Ruslan Kuprieiev 1123a303bf import: require 'path' argument
`dvc import` doesn't yet support importing the whole package.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-25 01:54:32 +03:00
Ruslan Kuprieiev 2141a5ac65 import: add "beta feature" warning 2019-06-25 01:54:32 +03:00
pawel b4fa4aec67 state: use pep-0249 sql query formatting 2019-06-25 00:07:32 +02:00
Ruslan Kuprieiev baccc06ec4 appveyor: uninstall default awscli 2019-06-24 14:18:40 +03:00
Ruslan Kuprieiev 2849efdf9e appveyor: install test requirements before configuring aws 2019-06-24 13:43:54 +03:00
Ruslan Kuprieiev 02f0bc78ea appveyor: remove typo 2019-06-24 13:43:05 +03:00
Ruslan Kuprieiev dde50a5b6f Merge pull request #2178 from efiop/2155
dvc: update paramiko requirements
2019-06-24 00:51:36 +03:00
Ruslan Kuprieiev 12c7689a74 dvc: update paramiko requirements
Fixes #2155

No longer need to explicitly ignore paramiko warnings.

Big thanks to @cowlinator for the heads-up!

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-24 00:24:06 +03:00
Ruslan Kuprieiev 5802efdfdb Merge pull request #2177 from shcheklein/fix-2170
Fix 2170: pipeline show command does not work
2019-06-24 00:20:22 +03:00
Ivan Shcheklein 478d6aec2f test: ls/dir depends on platform, dir is not supported by Mac 2019-06-23 14:09:22 -07:00
Ivan Shcheklein 6f05e7747f logger: use custom handler to propagare logging exceptions 2019-06-23 13:59:11 -07:00
Ivan Shcheklein c0a06d2ceb pipeline show command: ignore stage if command is not defined 2019-06-23 13:58:42 -07:00
Ruslan Kuprieiev 1e2da02d24 Merge pull request #2174 from efiop/master
tests: win: don't cleanup processes on Popen
2019-06-23 22:51:55 +03:00
Ruslan Kuprieiev 1cb70fc631 tests: win: don't cleanup processes on Popen
Workaround for two bugs:

https://bugs.python.org/issue37380

and

https://github.com/gitpython-developers/GitPython/issues/546

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-23 22:35:50 +03:00
Ruslan Kuprieiev 2a4d6229b1 Merge pull request #2173 from efiop/master
dvc: use remove() instead of shutil.rmtree()
2019-06-23 18:21:49 +03:00
Ruslan Kuprieiev 9bca1c9bff dvc: use remove() instead of shutil.rmtree()
Our `remove()` is able to handle read-only files which we or git
sometimes leave behind.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-23 18:11:30 +03:00
Ruslan Kuprieiev 5b01804706 Merge pull request #2167 from efiop/appveyor-py2
appveyor: test py2 and py3
2019-06-23 13:51:59 +03:00
Ruslan Kuprieiev 918ef646dc appveyor: test py2 and py3
Related to #2166

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-23 13:33:47 +03:00
Ruslan Kuprieiev fbd3700e32 Merge pull request #2168 from efiop/help-fix
dvc: fix help messages for `get` and `import-url`
2019-06-22 01:56:00 +03:00
Ruslan Kuprieiev 16bc30ddd5 dvc: don't forget to use compat str for schema 2019-06-22 00:57:05 +03:00
Ruslan Kuprieiev e890c0558a dvc: fix help messages for get and import-url
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-22 00:50:22 +03:00
Ruslan Kuprieiev 98b5fcb61d appveyor: set PATH after refreshenv 2019-06-21 23:20:41 +03:00
Ruslan Kuprieiev 55186703e1 appveyor: use python 3.6 instead of 3.7
Looks like 3.7 is not available yet.
2019-06-21 23:13:37 +03:00
Ruslan Kuprieiev c7ca83e226 setup.py: install win-unicode-console when running on windows
Fixes #2165
2019-06-21 22:41:37 +03:00
Ruslan Kuprieiev a9620ad169 Merge pull request #2131 from mroutis/optimize-ssh
ssh: increase jobs and parallelize batch_exists
2019-06-21 18:38:49 +03:00
Ruslan Kuprieiev ab07509010 Merge pull request #2164 from efiop/1011
dvc: destroy: don't remove data
2019-06-21 13:35:46 +03:00
Ruslan Kuprieiev 81fd0b3415 dvc: destroy: don't remove data
Fix #1011

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-21 13:13:29 +03:00
Ruslan Kuprieiev 074ea612f6 Merge pull request #2163 from efiop/repo
dvc: address review from #2160
2019-06-21 12:55:32 +03:00
Ruslan Kuprieiev d795cdf1ba dvc: address review from #2160
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-21 12:27:24 +03:00
Ruslan Kuprieiev 95096e5e55 Merge pull request #2160 from efiop/repo
dvc: replace pkg concept with external repo
2019-06-21 10:33:58 +03:00
Ruslan Kuprieiev 1ba8e0171d dvc: replace pkg concept with external repo
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-21 01:16:00 +03:00
Ruslan Kuprieiev 85fd990849 Merge pull request #2153 from vasinkd/SIGINT_support_2
dvc: handle sigint when running a command with `dvc run`
2019-06-20 19:57:24 +03:00
Kirill Vasin 1734e25827 added old signal persistance and fixed tests accordingly 2019-06-20 19:21:53 +03:00
Ruslan Kuprieiev af9afbee7b Merge pull request #2159 from pared/remove_dulwich
ignore: remove dulwich in favour of pathspec
2019-06-20 15:24:02 +03:00
Kirill Vasin 5457c16e76 replace exception catch with signal 2019-06-20 13:39:22 +03:00
pared c9b3bdc927 ignore: remove dulwich in favour of pathspec 2019-06-20 12:15:42 +02:00
Mr. Outis dda8368f20 ssh: increase jobs and parallelize batch_exists
- SSH number of jobs increased from 4 to 8.

- `batch_exists` now opens all the SFTP sessions available to
distribute the work between them. Number of sessions is restricted
by the `MaxSessions` option configurable by the `sshd_config`.

- Fix #2104

With the optimizations mentioned above, we increased the speed by 8x
(using the default `MaxSessions` configuration -- 10).

Shout-outs to @efiop, @shcheklein, and @dmpetrov for suggesting the
optimization techniques.
2019-06-19 17:45:59 -05:00
Ruslan Kuprieiev 58f233ceb0 Merge pull request #2134 from vibhor98/order-options
Fix #2063: Standardise order of command options in help messages
2019-06-19 19:45:54 +03:00
vibhor98 55d3153734 Reorder commands in pull 2019-06-19 21:42:39 +05:30
vibhor98 62ee6b6b9a Made review changes. 2019-06-19 20:55:46 +05:30
Ruslan Kuprieiev 676021d563 Merge pull request #2149 from efiop/1772
pkg: handle install/version errors
2019-06-19 17:27:05 +03:00
Ruslan Kuprieiev 104a37485c Merge pull request #2156 from efiop/http
http: enable progress bar
2019-06-19 14:33:43 +03:00
Ruslan Kuprieiev be255ba4b0 http: enable progress bar
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-19 14:22:52 +03:00
Kirill Vasin ccb8e338e1 added custom KeyboardInterrupt support 2019-06-18 19:11:49 +03:00
Ruslan Kuprieiev 025de9aeb5 Merge pull request #2141 from pared/2079
metrics: show: remove active branch from tags only show call
2019-06-18 19:09:16 +03:00
pawel 362fe1337e brancher: include working tree in every method call 2019-06-18 15:57:47 +02:00
pawel 005a6016aa brancher refactor 2019-06-18 15:28:51 +02:00
pared 38642eb127 metrics: show: remove active branch from tags only show call 2019-06-18 15:28:51 +02:00
Ruslan Kuprieiev d3bdca3f13 tests: use shortuuid for shorter paths
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-18 16:11:39 +03:00
Ruslan Kuprieiev 14dc0c0115 pkg: handle install/version errors
Fixes #1772

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-18 16:09:06 +03:00
Ruslan Kuprieiev c023b8da57 Merge pull request #2128 from pared/2119
pathinfo: extract relative path method to standalone method
2019-06-18 14:07:40 +03:00
prd 0615ff8730 pathinfo: extract relative path method to standalone method 2019-06-18 12:08:19 +02:00
Ruslan Kuprieiev f22d55b0f7 Merge pull request #2143 from efiop/2127
dvc: use realpath-ed paths consistently
2019-06-17 22:45:02 +03:00
Ruslan Kuprieiev 459b7fad29 dvc: use realpath-ed paths consistently
Fixes #2127
2019-06-17 22:20:28 +03:00
Ruslan Kuprieiev 7890c85e1e Merge pull request #2142 from efiop/pkgs-fixup
pkg: introduce `-f|--force` for `pkg install`
2019-06-17 21:36:10 +03:00
Ruslan Kuprieiev 4947069c4f pkg: introduce -f|--force for pkg install
Fixes #1773

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-17 21:06:10 +03:00
Ruslan Kuprieiev 573c84ec66 Merge pull request #2012 from efiop/pkgs
Initial pkg implementation
2019-06-17 20:21:15 +03:00
Ruslan Kuprieiev 914a587123 pkg: implement dvc pkg get command
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-17 18:12:36 +03:00
Ruslan Kuprieiev 5941ace270 dvc: initial 'dvc pkg' implementation
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-17 18:12:36 +03:00
vibhor98 276e90ee5d Reorder command options. 2019-06-16 17:29:01 +05:30
Ruslan Kuprieiev b02f2b531e Merge pull request #2122 from mroutis/fix-1762-progress-bar
status: show a progress bar while gathering info
2019-06-16 12:48:29 +03:00
Mr. Outis 223f0f6c0f status: show a progress bar while gathering info
Fix #1762
Fix #1825
2019-06-14 16:39:30 -05:00
Ruslan Kuprieiev ade4261cf5 Merge pull request #2133 from Suor/push-pull-error-code
dvc: return non zero error code on push/pull/fetch error
2019-06-14 20:43:02 +03:00
Alexander Schepanovski db75693fe1 dvc: return non zero error code on push/pull/fetch error
Closes #2038
2019-06-14 23:25:39 +07:00
Ruslan Kuprieiev 2f02280483 Merge pull request #2129 from Suor/download-refactor
dvc: unify remote upload/download code
2019-06-14 11:56:43 +03:00
Ruslan Kuprieiev 765c8570d2 Merge pull request #2121 from sanidhyamangal/master
extended patch for pr 2111
2019-06-14 11:50:22 +03:00
Alexander Schepanovski 80ca3eedc8 dvc: unify remote upload/download code
Bugs fixed along the way:
- unable to copy between buckets in GS
- unneeded connection init in RemoteGS.copy()
- connection not closed on some errors in RemoteSSH.download()
- inconsistent progress bar handling
2019-06-14 12:13:11 +07:00
Ruslan Kuprieiev 670af9afd5 Merge pull request #2130 from efiop/2039
dvc: move Updater out of Repo()
2019-06-14 01:07:14 +03:00
Ruslan Kuprieiev 0d330c1b9b dvc: move Updater out of Repo()
Fixes #2039

Required by #2012 to be able to fetch data through the API without
interuption from updater.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-14 00:54:36 +03:00
Ruslan Kuprieiev b1eebe2f86 diff: don't use self
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-14 00:20:56 +03:00
Sanidhya Mangal f39bb551d7 fixed: split lines for datasync 2019-06-14 02:49:14 +05:30
Sanidhya Mangal a831918195 fixed: splited helper text in data sync 2019-06-13 23:07:29 +05:30
Sanidhya Mangal 47cb449165 fixed: whitespace error 2019-06-13 23:00:25 +05:30
Sanidhya Mangal 62f55d6617 Merge remote-tracking branch 'upstream/master' 2019-06-13 22:50:07 +05:30
Sanidhya Mangal 498bbc647d fixed: lenghty message in help command 2019-06-13 22:49:39 +05:30
Ruslan Kuprieiev f36162d613 Merge pull request #2124 from efiop/reflink
system: handle PurePath
2019-06-13 12:09:26 +03:00
Ruslan Kuprieiev bfdb990b3b utils: dont use str() for dvc_walk
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-13 11:51:14 +03:00
Ruslan Kuprieiev 16e162941e system: handle PurePath
------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/efiop/git/dvc/dvc/remote/local/__init__.py", line 280, in upload
    no_progress_bar=no_progress_bar,
  File "/Users/efiop/git/dvc/dvc/utils/__init__.py", line 129, in copyfile
    System.reflink(src, dest)
  File "/Users/efiop/git/dvc/dvc/system.py", line 125, in reflink
    ret = System._reflink_darwin(source, link_name)
  File "/Users/efiop/git/dvc/dvc/system.py", line 89, in _reflink_darwin
    ctypes.c_char_p(src.encode("utf-8")),
AttributeError: 'PosixPathInfo' object has no attribute 'encode'
------------------------------------------------------------

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-13 11:51:14 +03:00
Sanidhya Mangal 61923a6687 Merge remote-tracking branch 'upstream/master' 2019-06-13 13:06:10 +05:30
Ruslan Kuprieiev dcca884696 Merge pull request #2126 from algomaster99/binary-info
Check whether package is installed from source or binary
2019-06-13 08:42:14 +03:00
Aman Sharma 11bafe4b7d Check whether package is installed from source or binary 2019-06-13 10:38:37 +05:30
Sanidhya Mangal e954a918ec Merge remote-tracking branch 'upstream/master' 2019-06-12 22:53:09 +05:30
Ruslan Kuprieiev 1c6f46c041 Merge pull request #2118 from pared/310
scm: git: end gitignore with newline
2019-06-12 18:08:43 +03:00
pawel 974bc39ae5 scm: git: end gitignore with newline 2019-06-12 16:52:36 +02:00
Ruslan Kuprieiev dd7876e72f Merge pull request #2117 from pared/2110
config: rename RemoteConfig variable in CmdRemoteConfig class
2019-06-12 12:10:29 +03:00
pawel 5ed6123101 config: rename RemoteConfig and CacheConfig variables in CmdRemoteConfig and CmdCacheDir class 2019-06-12 10:58:30 +02:00
Sanidhya Mangal 771b84a9f8 Merge remote-tracking branch 'upstream/master' 2019-06-12 13:59:33 +05:30
Sanidhya Mangal cb4a745d1b updated: help message for shared parent parser 2019-06-12 13:59:07 +05:30
Ruslan Kuprieiev 99d533310e Merge pull request #2044 from mroutis/fix-1762
remote: add other way to perform `cache_exists`
2019-06-12 00:10:55 +03:00
Mr. Outis d4f0d311fc remote: add other way to perform cache_exists
Close #1762
2019-06-11 08:55:48 -05:00
Ruslan Kuprieiev 7f73c6fa6f Merge pull request #2111 from sanidhyamangal/master
Patch: shared parser target content.
2019-06-11 16:31:09 +03:00
Ruslan Kuprieiev 39a8cab49d test: fix s3 test with extra args
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-11 16:28:11 +03:00
Sanidhya Mangal d356644754 code formatting: data sync 2019-06-11 18:31:34 +05:30
Ruslan Kuprieiev 4adcc8cee9 Merge pull request #2075 from Suor/refactor-path-info
dvc: path info refactoring
2019-06-11 15:50:26 +03:00
Alexander Schepanovski d80293c44d dvc: path info refactoring
Replaced Path* dataclasses with PathInfo/URLInfo/CloudURLInfo, which
know how parse, serialize, stringify and join themselves. They also
provide useful accessors like part of an url or pathlib.Path like
.parent or .name.

Along the way two families of bugs where fixed and should never occur in
the future:
- paths not matched as strings anymore, e.g. using str.startswith()
- Output.path is eliminated as ambiguous

Both families might had caused undefined surprising behavior when local
and remote paths clash.
2019-06-11 19:19:17 +07:00
Ruslan Kuprieiev 6a1629c559 Merge pull request #2090 from pared/hardlink_cache_check
remote: local: add cache status check hardlink optimization
2019-06-11 12:51:25 +03:00
pawel fe3f83ee05 remote: local: test: refactor unpacked dir tests 2019-06-11 10:21:57 +02:00
Sanidhya Mangal 2b3583a30e patched: shared parser target content 2019-06-11 00:23:54 +05:30
Sanidhya Mangal e44e3ad127 fixed: merge conflict 2019-06-11 00:19:47 +05:30
Sanidhya Mangal 1c14b8c579 patched: target content for shared parser 2019-06-11 00:17:47 +05:30
pawel ac5ca4656a remote: progress callback for unpacked dir creation 2019-06-10 20:19:47 +02:00
pawel cfbd381d42 remote: create unpacked dir after status check if needed 2019-06-10 14:44:03 +02:00
pawel b5464cbc59 remote: local: check if status verification dir already exists 2019-06-10 14:44:03 +02:00
pawel 5d25d5ce96 remote: local: refactor link method 2019-06-10 14:44:03 +02:00
pared 90ca5bd35a remote: local: add cache status check hardlink optimization 2019-06-10 14:44:03 +02:00
Ruslan Kuprieiev c587c59dc6 Merge pull request #2108 from ei-grad/config-choices
config: choices refactoring
2019-06-10 13:49:09 +03:00
Andrew Grigorev 00a2e9bb7f config: choices refactoring 2019-06-10 13:34:54 +03:00
Ruslan Kuprieiev 7dffa9f27d Merge pull request #2107 from dberenbaum/feature-1967
remote: add basic support for s3 server side encryption.
2019-06-09 00:33:55 +03:00
dlg227 fa6b764f97 refactor of remote s3 extra_args 2019-06-08 17:10:58 -04:00
Ruslan Kuprieiev ff12f90e6e Merge pull request #2103 from jorgeorpinel/master
dvc: standard way to refer to DVC-files
2019-06-07 23:34:48 +03:00
dlg227 fd3c84d682 remote: add basic support for s3 server side encryption.
Fixes #1967
2019-06-07 14:31:50 -04:00
Jorge Orpinel 3dffe8e5c0 dvc: i/DVC file[s]?/ -> /DVC-file($1)/
Standardizes way to refer to DVC-file(s)

(Related to #2102)
2019-06-07 03:33:20 -05:00
Ruslan Kuprieiev 1e7e2aaed5 Merge pull request #2098 from efiop/config
refactoring: move config logic from CLI to API
2019-06-06 21:53:53 +03:00
Ruslan Kuprieiev 8ef9133194 remote: use RemoteConfig class
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-06 16:00:44 +03:00
Ruslan Kuprieiev f69836c2b3 tests: remove fixme msg
https://github.com/iterative/dvc/issues/2086

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-06 14:41:02 +03:00
Ruslan Kuprieiev 9e9e078de3 config: move logic from CLI to API
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-06 14:41:02 +03:00
Ruslan Kuprieiev f41dcea248 tests: don't use var before assignment
```
repo_dir = <tests.basic_env.TestDirFixture object at 0x7f36c3f3d650>                  
git = <git.Repo "/tmp/dvc-test.2894.0LMoaT.eefefa48-172d-44e4-8c3d-4f65f76c78ee/.git">
                                                                                      
    @pytest.fixture                                                                   
    def dvc_repo(repo_dir, git):                                                      
        try:                                                                          
            dvc = DvcRepo.init(repo_dir._root_dir)                                    
            dvc.scm.commit("init dvc")                                                
            yield dvc                                                                 
        finally:                                                                      
>           dvc.scm.git.close()                                                       
E           UnboundLocalError: local variable 'dvc' referenced before assignment      
                                                                                      
/home/efiop/git/dvc/tests/conftest.py:76: UnboundLocalError                           
```
2019-06-06 13:44:13 +03:00
Ivan Shcheklein 67e6dc253a Update README.rst 2019-06-03 12:17:48 -07:00
Ruslan Kuprieiev 6e707277d6 Merge pull request #2082 from ei-grad/fix-pytest46-caplog
tests: fix for pytest>=4.6.0 caplog
2019-06-03 04:57:07 +03:00
Andrew Grigorev 5fce9a01e4 tests: fix for pytest>=4.6.0 caplog 2019-06-02 15:41:21 +03:00
Ruslan Kuprieiev 07e07489d0 Merge pull request #2084 from efiop/hdfs
Revert "tests: test hdfs on cron and releases"
2019-06-02 15:25:44 +03:00
Ruslan Kuprieiev 0242976734 Merge pull request #2081 from ei-grad/appveyor-all-branches
ci: don't limit appveyor to master and devel
2019-06-02 15:13:51 +03:00
Ruslan Kuprieiev 122b94129f Revert "tests: test hdfs on cron and releases"
This reverts commit 76c4755865.
2019-06-02 15:07:36 +03:00
Ruslan Kuprieiev 202c951b68 Merge pull request #2083 from efiop/hdfs
tests: test hdfs on cron and releases
2019-06-02 14:56:27 +03:00
Ruslan Kuprieiev 76c4755865 tests: test hdfs on cron and releases
Related to
https://github.com/iterative/dvc/issues/1629#issuecomment-498023424

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-06-02 14:44:56 +03:00
Ruslan Kuprieiev 616d79f2d8 tests: freeze pytest version 2019-06-02 13:59:20 +03:00
Andrew Grigorev a76b0acaed ci: don't limit appveyor to master and devel 2019-06-02 13:34:06 +03:00
Ivan Shcheklein 9a5a01b2b1 enable deepsource analyzer 2019-06-01 17:52:45 -07:00
Kurian Benoy c8b2c10d7f scripts:add description for debian package (#2025)
* scripts:add description for debian package

* add DESC variable to build_posix

* Fix typos
2019-06-01 20:35:47 +03:00
Aman Sharma 688bcaa0e3 Add info for link type and filesystem name (#1997)
* Add info for link and filesystem type

* Remove unnecessary `repo_dir` fixture

* Add cache info

* Classify `test_version.py` as functional test

* Remove redundancy

* Optimize some functions involved

* Add pathlib2 to install_requires

* Display fs type for cache directory

* Use `dvc.utils.compat` to import Path

* Remove redundancy in tests and make logs more descriptive
2019-06-01 14:40:59 +03:00
Ruslan Kuprieiev 0935cfae2a tests: show how long tests took 2019-05-31 11:19:18 +03:00
Ruslan Kuprieiev f6a0a45fa4 Merge pull request #2077 from ricardoserradas/riserrad/comma-to-message
Adding a comma to the message to improve understanding
2019-05-31 08:57:13 +03:00
Ricardo Serradas e93534abc1 Adding a comma to the message to improve understanding 2019-05-30 14:21:10 -07:00
Ruslan Kuprieiev ea54a2b2b4 appveyor: don't install git with cinst 2019-05-30 21:47:47 +03:00
Ruslan Kuprieiev fede39452b appveyor: fix typo 2019-05-30 21:46:23 +03:00
Ruslan Kuprieiev 01b8e55259 appveyor: install git 2019-05-30 21:45:02 +03:00
Ruslan Kuprieiev 677a52b830 appveyor: use quotes 2019-05-30 21:42:07 +03:00
Ruslan Kuprieiev c7b7fba3a2 appveyor: fix typo 2019-05-30 21:38:43 +03:00
Ruslan Kuprieiev dba34ec3f1 appveyor: don't forget to modify PATH for custom python
Related to https://github.com/iterative/dvc/issues/2061
2019-05-30 21:10:28 +03:00
Ruslan Kuprieiev 556b33ba28 Merge pull request #2074 from ei-grad/no-pool-for-single-thread
remote: don't use threadpool for single thread
2019-05-30 19:07:32 +03:00
Andrew Grigorev a1d3013596 remote: don't use threadpool for single thread 2019-05-30 18:27:00 +03:00
Jorge Orpinel 8df0712b06 update zsh autocomplete script with current command help messages + (#2064)
* dvc: fixes dvc.scm.Git.get_diff_trees docstring imprecision.

Parentheses were used in the "Returns:" description but its a dict, so changed to curly brackets;
and specified the 2 possible forms of the return structure.

* dvc: updates zsh autocomplete script with current command help messages.

Comparing vs actually running each command's `-h` help output.

* dvc: begins updating bask autocomplete script...

Only reorders existing commands and adds comments for those missing.

* scripts: removes temporary comments in bash autocomplete script.

Per https://github.com/iterative/dvc/pull/2064#pullrequestreview-243578115

Related to #2069
2019-05-30 05:57:30 +03:00
Ruslan Kuprieiev ca63d729e9 Merge pull request #2072 from pared/optimize_mtime_and_size
make get_mtime_and_size use single stat call for files
2019-05-30 03:21:11 +03:00
Ruslan Kuprieiev bae3134d15 Merge pull request #2071 from mroutis/progress-be-quiet
progress: bug fix with --quiet
2019-05-30 03:20:59 +03:00
pawel 012e045837 make get_mtime_and_size use single stat call for files 2019-05-29 16:47:13 -07:00
Mr. Outis e3b3f3f371 progress: bug fix with --quiet
Related to #1377
2019-05-29 16:45:10 -07:00
Ruslan Kuprieiev b2b92879c4 appveyor: use our own fork of oss-emulator 2019-05-30 02:25:22 +03:00
Ruslan Kuprieiev a5922ccec1 version: fix typo 2019-05-29 13:44:58 +03:00
Ruslan Kuprieiev fb0072d790 dvc: bump to 0.41.3 2019-05-29 13:39:24 +03:00
Ruslan Kuprieiev ab17abdde9 travis: skip remove verification when testing version 2019-05-29 13:38:36 +03:00
Ruslan Kuprieiev 45f2a2897a dvc: bump to 0.41.2 2019-05-29 13:12:40 +03:00
Ruslan Kuprieiev 360e295f9f Merge pull request #2068 from efiop/2066
remote: support remote:// notation for push/pull/gc
2019-05-29 13:10:43 +03:00
Ruslan Kuprieiev 31848d5c7f remote: support remote:// notation for push/pull/gc
Fixes #2066

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-05-29 12:54:31 +03:00
Ruslan Kuprieiev 1bf8ad1d3d dvc: bump to 0.41.1 2019-05-29 10:54:48 +03:00
Aman Sharma 55ad81bf9d Coalesce requirements.txt into setup.py (#2017)
* Coalesce `requirements.txt` and `tests/requirements.txt` into `setup.py`

* Remove requirements for tests

* Parse tests requirements in setup.py

* Append extra dependenices in test_requirements in setup.py

* Modify CI build to pass the tests

* Correctly install black requirements

* Install all requirements to pass the tests
2019-05-29 10:23:01 +03:00
Ruslan Kuprieiev 500a26ebd6 Merge pull request #2062 from Witiko/support-relinks
remote: local: try to reflink before copying
2019-05-29 03:57:39 +03:00
Vít Novotný e2567bb3f9 remote: local: try to reflink before copying
Fixes #2057
2019-05-29 02:34:02 +02:00
Ruslan Kuprieiev 5941012a19 dvc: bump to 0.41.0 2019-05-29 00:00:27 +03:00
Ruslan Kuprieiev 434f3a641c Merge pull request #2060 from pared/rollback_1526
remote: local: roll back cache dir metadata storing
2019-05-28 23:54:43 +03:00
pawel fae55c2ba2 remote: local: roll back cache dir metadata storing 2019-05-28 13:39:48 -07:00
Ruslan Kuprieiev 29fd8e2e41 Merge pull request #2030 from prihoda/recursive-repro
Add repro -R, support dirs & stage files when running pull/push/repro…
2019-05-27 23:09:02 +03:00
Ruslan Kuprieiev 9e0e09243e Merge pull request #2053 from Suor/test-remotes
Add initial get_url()/open() api implementation
2019-05-27 18:17:08 +03:00
David Příhoda d072c2bd79 Fix after renaming dvc fixture to dvc_repo. 2019-05-27 14:14:44 +02:00
Alexander Schepanovski ca93dbcf13 dvc: add get_url()/open() API
As a part of this:
- DataCloud._cloud was refactored into `.get_remote()`
- data stopped using internal _cloud
- added a parametrization for remote tests
2019-05-27 17:56:31 +07:00
David Příhoda e11c3cbbc5 Add repro -R, support dirs & stage files when running pull/push/repro -R. 2019-05-27 12:48:46 +02:00
Alexander Schepanovski eac6adada6 dvc: fix HDFS path 2019-05-25 17:55:23 +07:00
Alexander Schepanovski 7ddbbbc80f test: simplify google creds handling 2019-05-25 17:55:23 +07:00
Ruslan Kuprieiev 72bb57ba02 Merge pull request #2049 from efiop/python3-bin
ci: build binaries on python 3.7
2019-05-25 03:01:39 +03:00
Ruslan Kuprieiev 6e5e6ec431 Merge pull request #2052 from pared/remove_unnecessary_code_from_state
state: remove unused changed method
2019-05-25 02:43:30 +03:00
Ruslan Kuprieiev 4e9bc48f2a travis: build binary on python 3
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-05-25 02:40:16 +03:00
Ruslan Kuprieiev 095221ef7d appveyor: build windows binary on python 3.7
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-05-25 02:40:16 +03:00
pawel 3d2159816e state: remove unused changed method 2019-05-24 16:26:39 -07:00
Jorge OrpinelandRuslan Kuprieiev ce83231128 Updates to 1. zsh autocomplete script and 2. slow link type warning message (#2003)
* Document dvc.scm.git::Git::_get_diff_trees and shorten commit hashes in dvc.scm.git::Git::get_diff_trees

* diff: Changes to dvc.scm.git so `dvc diff` outputs short SHAs

- Docstring for Git class constructor
- Renames self.git for self.repo throughout module (involves several test changes)
- Better doc and logical refactor in Git._get_diff_trees
- `dvc diff` now outputs short SHA strings (Git._get_diff_trees) - hard coded to 7 length str
  (involves some test changes in tests/func/test_diff.py)

For #1902

* Undoes "Renames self.git for self.repo throughout module" introduced in 3d2dc83.

* autocomplete: Brings scripts/completion/dvc.zsh up-to-date

- Reorganizes commands in alphabetical order;
- Updates the command dscriptions according to their current usage (-h) output;
- Adds comments for MISSING command entries

* Update scripts/completion/dvc.zsh

Co-Authored-By: Ruslan Kuprieiev <[email protected]>

* Update scripts/completion/dvc.zsh

Co-Authored-By: Ruslan Kuprieiev <[email protected]>

* Update scripts/completion/dvc.zsh

Co-Authored-By: Ruslan Kuprieiev <[email protected]>

* Update scripts/completion/dvc.zsh

Co-Authored-By: Ruslan Kuprieiev <[email protected]>

* autocomplete: Adds missing commands to Zsh script

* cache: updates slow link type warning message (and test)

in accordance with https://github.com/iterative/dvc.org/pull/331

* Fixes accidental docstring rewrap

per https://github.com/iterative/dvc/pull/2003#discussion_r286206870

* Actually fixes accidental docstring rewrap...

per https://github.com/iterative/dvc/pull/2003#pullrequestreview-240994592

* Really fixes accidental docstring rewrap this time...
2019-05-25 00:00:47 +03:00
Ruslan Kuprieiev 8a4e5f937b Merge pull request #2034 from pared/rename_dvc_fixture
[RFC] tests: rename dvc repo fixture
2019-05-24 03:14:46 +03:00
Tadas Audinis ee6a0b2875 tests: resolve deprecation warnings (#2028)
* tests: resolve deprecation warnings

Fixes #2021

* tests: resolve deprecation warnings

Fixes #2021
2019-05-24 03:12:36 +03:00
Mr. Outis 037f32c8cc docs: add some docstrings about computing status (#2048) 2019-05-23 16:16:02 -07:00
Ruslan Kuprieiev 6569657390 Merge pull request #2035 from TomHortons/master
pipeline: show: dot: print dot
2019-05-22 06:22:30 +03:00
TomHortons d9a79ce345 pipeline: show: dot: use dvc.utils 2019-05-22 12:11:55 +09:00
TomHortons e3de8aac65 pipeline: show: dot: print dot 2019-05-22 11:11:23 +09:00
pawel 01c2f6c174 tests: rename dvc repo fixture 2019-05-21 15:04:43 -07:00
Ruslan Kuprieiev 3e461f8f11 Merge pull request #2007 from Suor/test-remotes
test: update/add helpers to run tests against remotes
2019-05-21 15:44:23 +03:00
Ruslan Kuprieiev 6b7e06e4b4 Merge pull request #2029 from pared/remove_unnecessary_args_state
remove unnecessary arguments in state methods
2019-05-21 15:33:05 +03:00
Alexander Schepanovski 0e1f20f0f5 test: update/add helpers to run tests against remotes 2019-05-21 17:38:53 +07:00
pawel 7eb99d2d87 remove unnecessary arguments in state methods 2019-05-20 18:52:25 -07:00
178 changed files with 4871 additions and 4265 deletions
+22 -22
View File
@@ -1,8 +1,3 @@
branches:
only:
- master
- devel
cache:
- C:\ProgramData\chocolatey\bin -> appveyor.yml
- C:\ProgramData\chocolatey\lib -> appveyor.yml
@@ -27,42 +22,45 @@ environment:
matrix:
- PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7.x" # currently 2.7.9
PYTHON_VERSION: "2.7.x"
PYTHON_ARCH: "32"
RUBY: C:\Ruby25\bin
# Disabled as unnecessary
# - PYTHON: "C:\\Python27-x64"
# PYTHON_VERSION: "2.7.x" # currently 2.7.9
# PYTHON_ARCH: "64"
- PYTHON: "C:\\Python37"
PYTHON_VERSION: "3.7.x"
PYTHON_ARCH: "32"
RUBY: C:\Ruby25\bin
install:
- SET PATH=%RUBY%;%PATH%
- git clone -q https://github.com/nanaya-tachibana/oss-emulator.git
- cinst awscli
- cinst wget
- cinst gcloudsdk
- cinst gsutil
- cinst openssl.light --version 1.1.1
- refreshenv
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%RUBY%;%PATH%"
- git clone -q https://github.com/iterative/oss-emulator.git
- gem install thor builder
- ps: cd oss-emulator
- ps: $OSSProcess = Start-Process -FilePath ruby -ArgumentList "bin\emulator -r store -p 50004" -PassThru -NoNewWindow
- ps: cd ..
- npm install -g azurite
# NOTE: azurite 3 on py3 suffers from bug [1], so we are using an older
# tried-and-true version instead.
#
# [1] https://github.com/Azure/Azurite/issues/215
- npm install -g [email protected]
- ps: $AzuriteProcess = Start-Process azurite-blob -PassThru
- cinst wget
- cinst awscli
- cinst gcloudsdk
- refreshenv
- cinst gsutil
- cinst openssl.light --version 1.1.1
- python -m pip install -U pip setuptools wheel
- pip install -r requirements.txt
- python setup.py install
- pip install .[all,tests]
build: false
before_test:
- pip uninstall -y awscli # Using awscli from chocolatey
- aws configure set aws_access_key_id "%aws_access_key_id%"
- 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 tests/requirements.txt
test_script:
- python -mtests
@@ -87,3 +85,5 @@ deploy:
secure: 25c1pNoXWbtTwUkKLcThwcl2A2xG8oXHsPuD73HVbQ7SRy9X6HzkCc2sXzhpfRtF
on:
APPVEYOR_REPO_TAG: true
PYTHON_VERSION: "3.7.x"
PYTHON_ARCH: "32"
+15
View File
@@ -0,0 +1,15 @@
version = 1
test_patterns = [
'*/tests/*'
]
exclude_patterns = [
]
[[analyzers]]
name = 'python'
enabled = true
runtime_version = '3.x.x'
+1
View File
@@ -6,3 +6,4 @@
/state-journal
/state-wal
/cache
/pkg
+8
View File
@@ -28,3 +28,11 @@ innosetup/config.ini
pip-wheel-metadata/
.vscode/
azurite
env.sh
tests/remotes_env
scripts/ci/gcp-creds.json
.gcp-creds.json
*~
+1 -1
View File
@@ -48,7 +48,7 @@ matrix:
language: ruby
rvm: 2.4.0
env:
- PYTHON_VER=3.6.2
- PYTHON_VER=3.7.3
- PYTHON_CONFIGURE_OPTS="--enable-framework"
- if: type = cron OR tag is present
os: osx
+2 -2
View File
@@ -31,8 +31,8 @@
|
**Data Science Version Control** or **DVC** is an **open-source** tool for data science and
machine learning projects. Key features:
**Data Version Control** or **DVC** is an **open-source** tool for data science and machine
learning projects. Key features:
#. simple **command line** Git-like experience. Does not require installing and maintaining
any databases. Does not depend on any proprietary online services;
-15
View File
@@ -19,18 +19,3 @@ dvc.logger.setup()
# 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
+74
View File
@@ -0,0 +1,74 @@
from contextlib import contextmanager
import os
import tempfile
try:
from contextlib import _GeneratorContextManager as GCM
except ImportError:
from contextlib import GeneratorContextManager as GCM
from dvc.utils import remove
from dvc.utils.compat import urlparse
from dvc.repo import Repo
from dvc.external_repo import ExternalRepo
def get_url(path, repo=None, remote=None):
"""Returns an url of a resource specified by path in repo"""
with _make_repo(repo) as _repo:
abspath = os.path.join(_repo.root_dir, path)
out, = _repo.find_outs_by_path(abspath)
remote_obj = _repo.cloud.get_remote(remote)
return str(remote_obj.checksum_to_path_info(out.checksum))
def open(path, repo=None, remote=None, mode="r", encoding=None):
"""Opens a specified resource as a file descriptor"""
args = (path,)
kwargs = {
"repo": repo,
"remote": remote,
"mode": mode,
"encoding": encoding,
}
return _OpenContextManager(_open, args, kwargs)
class _OpenContextManager(GCM):
def __init__(self, func, args, kwds):
self.gen = func(*args, **kwds)
self.func, self.args, self.kwds = func, args, kwds
def __getattr__(self, name):
raise AttributeError(
"dvc.api.open() should be used in a with statement"
)
def _open(path, repo=None, remote=None, mode="r", encoding=None):
with _make_repo(repo) as _repo:
abspath = os.path.join(_repo.root_dir, path)
with _repo.open(
abspath, remote=remote, mode=mode, encoding=encoding
) as fd:
yield fd
def read(path, repo=None, remote=None, mode="r", encoding=None):
"""Read a specified resource into string"""
with open(path, repo, remote=remote, mode=mode, encoding=encoding) as fd:
return fd.read()
@contextmanager
def _make_repo(repo_url):
if not repo_url or urlparse(repo_url).scheme == "":
yield Repo(repo_url)
else:
tmp_dir = tempfile.mkdtemp("dvc-repo")
try:
ext_repo = ExternalRepo(tmp_dir, url=repo_url)
ext_repo.install()
yield ext_repo.repo
finally:
remove(tmp_dir)
+16 -3
View File
@@ -7,6 +7,20 @@ import os
from dvc.config import Config
class CacheConfig(object):
def __init__(self, config):
self.config = config
def set_dir(self, dname, level=None):
from dvc.remote.config import RemoteConfig
configobj = self.config.get_configobj(level)
path = RemoteConfig.resolve_path(dname, configobj.filename)
self.config.set(
Config.SECTION_CACHE, Config.SECTION_CACHE_DIR, path, level=level
)
class Cache(object):
"""Class that manages cache locations of a dvc repo.
@@ -42,7 +56,7 @@ class Cache(object):
Config.SECTION_CACHE_PROTECTED: protected,
}
self.local = Remote(repo, settings)
self.local = Remote(repo, **settings)
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)
@@ -87,5 +101,4 @@ class Cache(object):
if not remote:
return None
settings = self.repo.config.get_remote_settings(remote)
return Remote(self.repo, settings)
return Remote(self.repo, name=remote)
+6 -2
View File
@@ -9,7 +9,8 @@ import logging
from dvc.command.base import fix_subparsers
import dvc.command.init as init
import dvc.command.pkg as pkg
import dvc.command.get as get
import dvc.command.get_url as get_url
import dvc.command.destroy as destroy
import dvc.command.remove as remove
import dvc.command.move as move
@@ -20,6 +21,7 @@ import dvc.command.data_sync as data_sync
import dvc.command.gc as gc
import dvc.command.add as add
import dvc.command.imp as imp
import dvc.command.imp_url as imp_url
import dvc.command.config as config
import dvc.command.checkout as checkout
import dvc.command.remote as remote
@@ -41,7 +43,8 @@ logger = logging.getLogger(__name__)
COMMANDS = [
init,
pkg,
get,
get_url,
destroy,
add,
remove,
@@ -52,6 +55,7 @@ COMMANDS = [
data_sync,
gc,
imp,
imp_url,
config,
checkout,
remote,
+1 -1
View File
@@ -54,7 +54,7 @@ def add_parser(subparsers, parent_parser):
help="Don't put files/directories into cache.",
)
add_parser.add_argument(
"-f", "--file", help="Specify name of the DVC file it generates."
"-f", "--file", help="Specify name of the DVC-file it generates."
)
add_parser.add_argument(
"targets", nargs="+", help="Input files/directories."
+3
View File
@@ -38,10 +38,13 @@ def append_doc_link(help_message, path):
class CmdBase(object):
def __init__(self, args):
from dvc.repo import Repo
from dvc.updater import Updater
self.repo = Repo()
self.config = self.repo.config
self.args = args
updater = Updater(self.repo.dvc_dir)
updater.check()
@property
def default_targets(self):
+7 -7
View File
@@ -3,18 +3,18 @@ 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
from dvc.cache import CacheConfig
class CmdCacheDir(CmdConfig):
def run(self):
self.args.name = "cache.dir"
self.args.value = CmdRemoteAdd.resolve_path(
self.args.value, self.configobj.filename
)
def __init__(self, args):
super(CmdCacheDir, self).__init__(args)
self.cache_config = CacheConfig(self.config)
return super(CmdCacheDir, self).run()
def run(self):
self.cache_config.set_dir(self.args.value, level=self.args.level)
return 0
def add_parser(subparsers, parent_parser):
+8 -8
View File
@@ -30,13 +30,6 @@ def add_parser(subparsers, parent_parser):
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",
@@ -44,6 +37,13 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Do not prompt when removing working directory files.",
)
checkout_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Checkout all dependencies of the specified target.",
)
checkout_parser.add_argument(
"-R",
"--recursive",
@@ -51,5 +51,5 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Checkout all subdirectories of the specified directory.",
)
checkout_parser.add_argument("targets", nargs="*", help="DVC files.")
checkout_parser.add_argument("targets", nargs="*", help="DVC-files.")
checkout_parser.set_defaults(func=CmdCheckout)
+2 -2
View File
@@ -34,7 +34,7 @@ class CmdCommit(CmdBase):
def add_parser(subparsers, parent_parser):
COMMIT_HELP = "Save changed data to cache and update DVC files."
COMMIT_HELP = "Save changed data to cache and update DVC-files."
commit_parser = subparsers.add_parser(
"commit",
@@ -65,6 +65,6 @@ def add_parser(subparsers, parent_parser):
help="Commit cache for subdirectories of the specified directory.",
)
commit_parser.add_argument(
"targets", nargs="*", default=None, help="DVC files."
"targets", nargs="*", default=None, help="DVC-files."
)
commit_parser.set_defaults(func=CmdCommit)
+22 -63
View File
@@ -1,12 +1,10 @@
from __future__ import unicode_literals
import os
import argparse
import logging
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.config import Config
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -14,87 +12,48 @@ logger = logging.getLogger(__name__)
class CmdConfig(CmdBaseNoRepo):
def __init__(self, args):
from dvc.repo import Repo, NotDvcRepoError
super(CmdConfig, self).__init__(args)
try:
dvc_dir = os.path.join(Repo.find_root(), Repo.DVC_DIR)
saved_exc = None
except NotDvcRepoError as exc:
dvc_dir = None
saved_exc = exc
self.config = Config(dvc_dir, validate=False)
if self.args.system:
self.configobj = self.config._system_config
elif self.args.glob:
self.configobj = self.config._global_config
elif self.args.local:
if dvc_dir is None:
raise saved_exc
self.configobj = self.config._local_config
else:
if dvc_dir is None:
raise saved_exc
self.configobj = self.config._repo_config
def _unset(self, section, opt=None, configobj=None):
if configobj is None:
configobj = self.configobj
try:
self.config.unset(configobj, section, opt)
self.config.save(configobj)
except DvcException:
logger.exception("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:
logger.exception("failed to show '{}'".format(self.args.name))
return 1
return 0
def _set(self, section, opt, value):
try:
self.config.set(self.configobj, section, opt, value)
self.config.save(self.configobj)
except DvcException:
logger.exception(
"failed to set '{}.{}' to '{}'".format(section, opt, value)
)
return 1
return 0
self.config = Config(validate=False)
def run(self):
section, opt = self.args.name.lower().strip().split(".", 1)
if self.args.unset:
return self._unset(section, opt)
self.config.unset(section, opt, level=self.args.level)
elif self.args.value is None:
return self._show(section, opt)
logger.info(self.config.get(section, opt, level=self.args.level))
else:
return self._set(section, opt, self.args.value)
self.config.set(
section, opt, self.args.value, level=self.args.level
)
return 0
parent_config_parser = argparse.ArgumentParser(add_help=False)
parent_config_parser.add_argument(
"--global",
dest="glob",
action="store_true",
default=False,
dest="level",
action="store_const",
const=Config.LEVEL_GLOBAL,
help="Use global config.",
)
parent_config_parser.add_argument(
"--system", action="store_true", default=False, help="Use system config."
"--system",
dest="level",
action="store_const",
const=Config.LEVEL_SYSTEM,
help="Use system config.",
)
parent_config_parser.add_argument(
"--local", action="store_true", default=False, help="Use local config."
"--local",
dest="level",
action="store_const",
const=Config.LEVEL_LOCAL,
help="Use local config.",
)
parent_config_parser.set_defaults(level=Config.LEVEL_REPO)
def add_parser(subparsers, parent_parser):
+18 -11
View File
@@ -4,6 +4,7 @@ import argparse
import logging
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -45,7 +46,7 @@ class CmdDataPull(CmdDataBase):
force=self.args.force,
recursive=self.args.recursive,
)
except Exception:
except DvcException:
logger.exception("failed to pull data from the cloud")
return 1
self.check_up_to_date(processed_files_count)
@@ -65,7 +66,7 @@ class CmdDataPush(CmdDataBase):
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except Exception:
except DvcException:
logger.exception("failed to push data to the cloud")
return 1
self.check_up_to_date(processed_files_count)
@@ -85,7 +86,7 @@ class CmdDataFetch(CmdDataBase):
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except Exception:
except DvcException:
logger.exception("failed to fetch data from the cloud")
return 1
self.check_up_to_date(processed_files_count)
@@ -113,7 +114,13 @@ def shared_parent_parser():
help="Show checksums instead of file names.",
)
shared_parent_parser.add_argument(
"targets", nargs="*", default=None, help="DVC files."
"targets",
nargs="*",
default=None,
help=(
"Limit the scope to these DVC-files."
" With -R a directory to search DVC-files in can be specified."
),
)
return shared_parent_parser
@@ -149,13 +156,6 @@ def add_parser(subparsers, _parent_parser):
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",
@@ -163,6 +163,13 @@ def add_parser(subparsers, _parent_parser):
default=False,
help="Do not prompt when removing working directory files.",
)
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(
"-R",
"--recursive",
+1 -1
View File
@@ -35,7 +35,7 @@ class CmdDestroy(CmdBase):
def add_parser(subparsers, parent_parser):
DESTROY_HELP = "Remove DVC files, local DVC config and data cache."
DESTROY_HELP = "Remove DVC-files, local DVC config and data cache."
destroy_parser = subparsers.add_parser(
"destroy",
+20 -14
View File
@@ -5,10 +5,10 @@ import argparse
import humanize
import inflect
import logging
from funcy import compact
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, append_doc_link
from dvc.utils.collections import compact
import dvc.repo.diff as diff
@@ -16,7 +16,8 @@ logger = logging.getLogger(__name__)
class CmdDiff(CmdBase):
def _print_size(self, size):
@staticmethod
def _print_size(size):
if size < 0:
change = "decreased by {}"
elif size > 0:
@@ -26,14 +27,16 @@ class CmdDiff(CmdBase):
natur_size = humanize.naturalsize(abs(size))
return change.format(natur_size)
def _get_md5_string(self, sign, file_name, checksum):
@staticmethod
def _get_md5_string(sign, file_name, checksum):
sample_msg = ""
if file_name:
sample_msg = "{}{} with md5 {}\n"
sample_msg = sample_msg.format(sign, file_name, checksum)
return sample_msg
def _get_dir_changes(self, dct):
@classmethod
def _get_dir_changes(cls, dct):
engine = inflect.engine()
changes_msg = (
"{} {} not changed, {} {} modified, {} {} added, "
@@ -48,11 +51,12 @@ class CmdDiff(CmdBase):
engine.plural("file", dct[diff.DIFF_NEW]),
dct[diff.DIFF_DEL],
engine.plural("file", dct[diff.DIFF_DEL]),
self._print_size(dct[diff.DIFF_SIZE]),
cls._print_size(dct[diff.DIFF_SIZE]),
)
return changes_msg
def _get_file_changes(self, dct):
@classmethod
def _get_file_changes(cls, dct):
if (
dct.get(diff.DIFF_OLD_FILE)
and dct.get(diff.DIFF_NEW_FILE)
@@ -69,19 +73,21 @@ class CmdDiff(CmdBase):
)
else:
msg = "file was modified, file size {}".format(
self._print_size(dct[diff.DIFF_SIZE])
cls._print_size(dct[diff.DIFF_SIZE])
)
return msg
def _get_royal_changes(self, dct):
@classmethod
def _get_royal_changes(cls, dct):
if dct[diff.DIFF_SIZE] != diff.DIFF_SIZE_UNKNOWN:
if dct.get("is_dir"):
return self._get_dir_changes(dct)
return cls._get_dir_changes(dct)
else:
return self._get_file_changes(dct)
return cls._get_file_changes(dct)
return "size is ?"
def _show(self, diff_dct):
@classmethod
def _show(cls, diff_dct):
msg = "dvc diff from {} to {}".format(
diff_dct[diff.DIFF_A_REF], diff_dct[diff.DIFF_B_REF]
)
@@ -90,18 +96,18 @@ class CmdDiff(CmdBase):
return
for dct in diff_dct[diff.DIFF_LIST]:
msg += "\n\ndiff for '{}'\n".format(dct[diff.DIFF_TARGET])
msg += self._get_md5_string(
msg += cls._get_md5_string(
"-",
dct.get(diff.DIFF_OLD_FILE),
dct.get(diff.DIFF_OLD_CHECKSUM),
)
msg += self._get_md5_string(
msg += cls._get_md5_string(
"+",
dct.get(diff.DIFF_NEW_FILE),
dct.get(diff.DIFF_NEW_CHECKSUM),
)
msg += "\n"
msg += self._get_royal_changes(dct)
msg += cls._get_royal_changes(dct)
logger.info(msg)
return msg
+52
View File
@@ -0,0 +1,52 @@
from __future__ import unicode_literals
import argparse
import logging
from dvc.repo import Repo
from dvc.exceptions import DvcException
from .base import CmdBaseNoRepo, append_doc_link
logger = logging.getLogger(__name__)
class CmdGet(CmdBaseNoRepo):
def run(self):
try:
Repo.get(
self.args.url,
path=self.args.path,
out=self.args.out,
rev=self.args.rev,
)
return 0
except DvcException:
logger.exception(
"failed to get '{}' from '{}'".format(
self.args.path, self.args.url
)
)
return 1
def add_parser(subparsers, parent_parser):
GET_HELP = "Download data from DVC repository."
get_parser = subparsers.add_parser(
"get",
parents=[parent_parser],
description=append_doc_link(GET_HELP, "get"),
help=GET_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
get_parser.add_argument(
"url", help="DVC repository URL to download data from."
)
get_parser.add_argument("path", help="Path to data within DVC repository.")
get_parser.add_argument(
"-o", "--out", nargs="?", help="Destination path to put data to."
)
get_parser.add_argument(
"--rev", nargs="?", help="DVC repository git revision."
)
get_parser.set_defaults(func=CmdGet)
+39
View File
@@ -0,0 +1,39 @@
from __future__ import unicode_literals
import argparse
import logging
from dvc.repo import Repo
from dvc.exceptions import DvcException
from .base import CmdBaseNoRepo, append_doc_link
logger = logging.getLogger(__name__)
class CmdGetUrl(CmdBaseNoRepo):
def run(self):
try:
Repo.get_url(self.args.url, out=self.args.out)
return 0
except DvcException:
logger.exception("failed to get '{}'".format(self.args.url))
return 1
def add_parser(subparsers, parent_parser):
GET_HELP = "Download or copy files from URL."
get_parser = subparsers.add_parser(
"get-url",
parents=[parent_parser],
description=append_doc_link(GET_HELP, "get-url"),
help=GET_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
get_parser.add_argument(
"url", help="See `dvc import-url -h` for full list of supported URLs."
)
get_parser.add_argument(
"out", nargs="?", help="Destination path to put data to."
)
get_parser.set_defaults(func=CmdGetUrl)
+19 -29
View File
@@ -1,10 +1,8 @@
from __future__ import unicode_literals
import argparse
import os
import logging
from dvc.utils.compat import urlparse
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, append_doc_link
@@ -14,19 +12,22 @@ logger = logging.getLogger(__name__)
class CmdImport(CmdBase):
def run(self):
logger.warning(
"This is a beta feature! Do not use in the production! Old "
"`dvc import` is now called `dvc import-url`, please use "
"it instead."
)
try:
default_out = os.path.basename(urlparse(self.args.url).path)
out = self.args.out or default_out
self.repo.imp(
self.args.url, out, self.args.resume, fname=self.args.file
self.args.url,
self.args.path,
out=self.args.out,
rev=self.args.rev,
)
except DvcException:
logger.exception(
"failed to import {}. You could also try downloading "
"it manually and adding it with `dvc add` command.".format(
self.args.url
"failed to import '{}' from '{}'.".format(
self.args.path, self.args.url
)
)
return 1
@@ -34,7 +35,10 @@ class CmdImport(CmdBase):
def add_parser(subparsers, parent_parser):
IMPORT_HELP = "Download or copy files from URL and take under DVC control."
IMPORT_HELP = (
"BETA: Download data from DVC repository and take it under DVC "
"control."
)
import_parser = subparsers.add_parser(
"import",
@@ -43,28 +47,14 @@ def add_parser(subparsers, parent_parser):
help=IMPORT_HELP,
formatter_class=argparse.RawTextHelpFormatter,
)
import_parser.add_argument("url", help="DVC repository URL.")
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`)",
"path", help="Path to data within DVC repository."
)
import_parser.add_argument(
"--resume",
action="store_true",
default=False,
help="Resume previously started download.",
"-o", "--out", nargs="?", help="Destination path to put data to."
)
import_parser.add_argument(
"out", nargs="?", help="Destination path to put files to."
)
import_parser.add_argument(
"-f", "--file", help="Specify name of the DVC file it generates."
"--rev", nargs="?", help="DVC repository git revision."
)
import_parser.set_defaults(func=CmdImport)
+69
View File
@@ -0,0 +1,69 @@
from __future__ import unicode_literals
import argparse
import logging
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdImportUrl(CmdBase):
def run(self):
try:
self.repo.imp_url(
self.args.url,
out=self.args.out,
resume=self.args.resume,
fname=self.args.file,
)
except DvcException:
logger.exception(
"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 file from URL and take it under DVC control."
)
import_parser = subparsers.add_parser(
"import-url",
parents=[parent_parser],
description=append_doc_link(IMPORT_HELP, "import-url"),
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.add_argument(
"-f", "--file", help="Specify name of the DVC-file it generates."
)
import_parser.set_defaults(func=CmdImportUrl)
+4 -4
View File
@@ -37,7 +37,7 @@ class CmdUnlock(CmdLockBase):
def add_parser(subparsers, parent_parser):
LOCK_HELP = "Lock DVC file."
LOCK_HELP = "Lock DVC-file."
lock_parser = subparsers.add_parser(
"lock",
parents=[parent_parser],
@@ -45,10 +45,10 @@ def add_parser(subparsers, parent_parser):
help=LOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
lock_parser.add_argument("targets", nargs="+", help="DVC files.")
lock_parser.add_argument("targets", nargs="+", help="DVC-files.")
lock_parser.set_defaults(func=CmdLock)
UNLOCK_HELP = "Unlock DVC file."
UNLOCK_HELP = "Unlock DVC-file."
unlock_parser = subparsers.add_parser(
"unlock",
parents=[parent_parser],
@@ -56,5 +56,5 @@ def add_parser(subparsers, parent_parser):
help=UNLOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unlock_parser.add_argument("targets", nargs="+", help="DVC files.")
unlock_parser.add_argument("targets", nargs="+", help="DVC-files.")
unlock_parser.set_defaults(func=CmdUnlock)
+1 -1
View File
@@ -27,7 +27,7 @@ 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"
"It renames and modifies the corresponding DVC-file to reflect the"
" changes."
)
+19 -13
View File
@@ -1,9 +1,9 @@
from __future__ import unicode_literals
from dvc.utils import relpath
from dvc.utils.compat import str
import argparse
import os
import logging
from dvc.exceptions import DvcException
@@ -21,7 +21,7 @@ class CmdPipelineShow(CmdBase):
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)
node = relpath(stage.path, self.repo.root_dir)
nodes = networkx.dfs_postorder_nodes(G, node)
if locked:
@@ -29,6 +29,8 @@ class CmdPipelineShow(CmdBase):
for n in nodes:
if commands:
if stages[n].cmd is None:
continue
logger.info(stages[n].cmd)
elif outs:
for out in stages[n].outs:
@@ -41,7 +43,7 @@ class CmdPipelineShow(CmdBase):
from dvc.stage import Stage
stage = Stage.load(self.repo, target)
node = os.path.relpath(stage.path, self.repo.root_dir)
node = relpath(stage.path, self.repo.root_dir)
pipelines = list(
filter(lambda g: node in g.nodes(), self.repo.pipelines())
@@ -115,7 +117,8 @@ class CmdPipelineShow(CmdBase):
observe_list.pop(0)
tree.show()
def __write_dot(self, target, commands, outs, filename):
def __write_dot(self, target, commands, outs):
from dvc.utils.compat import StringIO
import networkx
from networkx.drawing.nx_pydot import write_dot
@@ -124,7 +127,10 @@ class CmdPipelineShow(CmdBase):
simple_g = networkx.DiGraph()
simple_g.add_edges_from(edges)
write_dot(simple_g, filename)
dot_file = StringIO()
write_dot(simple_g, dot_file)
logger.info(dot_file.getvalue())
def run(self):
if not self.args.targets:
@@ -138,10 +144,7 @@ class CmdPipelineShow(CmdBase):
)
elif self.args.dot:
self.__write_dot(
target,
self.args.commands,
self.args.outs,
self.args.dot,
target, self.args.commands, self.args.outs
)
elif self.args.tree:
self._show_dependencies_tree(
@@ -208,14 +211,14 @@ def add_parser(subparsers, parent_parser):
"--commands",
action="store_true",
default=False,
help="Print commands instead of paths to DVC files.",
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.",
help="Print output files instead of paths to DVC-files.",
)
pipeline_show_parser.add_argument(
"-l",
@@ -231,7 +234,10 @@ def add_parser(subparsers, parent_parser):
help="Output DAG as ASCII.",
)
pipeline_show_parser.add_argument(
"--dot", help="Write DAG in .dot format."
"--dot",
action="store_true",
default=False,
help="Print DAG with .dot format.",
)
pipeline_show_parser.add_argument(
"--tree",
@@ -240,7 +246,7 @@ def add_parser(subparsers, parent_parser):
help="Output DAG as Dependencies Tree.",
)
pipeline_show_parser.add_argument(
"targets", nargs="*", help="DVC files. 'Dvcfile' by default."
"targets", nargs="*", help="DVC-files. 'Dvcfile' by default."
)
pipeline_show_parser.set_defaults(func=CmdPipelineShow)
-89
View File
@@ -1,89 +0,0 @@
from __future__ import unicode_literals
import argparse
import logging
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
logger = logging.getLogger(__name__)
class CmdPkgInstall(CmdBase):
def run(self):
try:
self.repo.pkg.install(
self.args.address,
self.args.target_dir,
self.args.select,
self.args.file,
)
return 0
except DvcException:
logger.exception(
"failed to install package '{}'".format(self.args.address)
)
return 1
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"),
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=CmdPkgInstall)
+37 -103
View File
@@ -1,130 +1,64 @@
from __future__ import unicode_literals
import argparse
import os
import re
import logging
from dvc.command.base import append_doc_link, fix_subparsers
from dvc.command.config import CmdConfig
from dvc.config import Config
from dvc.remote.config import RemoteConfig
logger = logging.getLogger(__name__)
class CmdRemoteAdd(CmdConfig):
@staticmethod
def resolve_path(path, config_file):
"""Resolve path relative to config file location.
class CmdRemoteConfig(CmdConfig):
def __init__(self, args):
super(CmdRemoteConfig, self).__init__(args)
self.remote_config = RemoteConfig(self.config)
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))
class CmdRemoteAdd(CmdRemoteConfig):
def run(self):
from dvc.remote import _get, RemoteLOCAL
remote = _get({Config.SECTION_REMOTE_URL: self.args.url})
if remote == RemoteLOCAL and not self.args.url.startswith("remote://"):
self.args.url = self.resolve_path(
self.args.url, self.configobj.filename
)
section = Config.SECTION_REMOTE_FMT.format(self.args.name)
if (section in self.configobj.keys()) and not self.args.force:
logger.error(
"Remote with name {} already exists. "
"Use -f (--force) to overwrite remote "
"with new value".format(self.args.name)
)
return 1
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
)
return ret
class CmdRemoteRemove(CmdConfig):
def _remove_default(self, config):
core = config.get(Config.SECTION_CORE, None)
if core is None:
return 0
default = core.get(Config.SECTION_CORE_REMOTE, None)
if default is None:
return 0
if default == self.args.name:
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)
ret = self._unset(section)
if ret != 0:
return ret
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:
break
self.remote_config.add(
self.args.name,
self.args.url,
force=self.args.force,
default=self.args.default,
level=self.args.level,
)
return 0
class CmdRemoteModify(CmdConfig):
class CmdRemoteRemove(CmdRemoteConfig):
def run(self):
section = Config.SECTION_REMOTE_FMT.format(self.args.name)
self.args.name = "{}.{}".format(section, self.args.option)
return super(CmdRemoteModify, self).run()
self.remote_config.remove(self.args.name, level=self.args.level)
return 0
class CmdRemoteDefault(CmdConfig):
class CmdRemoteModify(CmdRemoteConfig):
def run(self):
self.args.value = self.args.name
self.args.name = "core.remote"
return super(CmdRemoteDefault, self).run()
self.remote_config.modify(
self.args.name,
self.args.option,
self.args.value,
level=self.args.level,
)
return 0
class CmdRemoteList(CmdConfig):
class CmdRemoteDefault(CmdRemoteConfig):
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))
self.remote_config.set_default(
self.args.name, unset=self.args.unset, level=self.args.level
)
return 0
class CmdRemoteList(CmdRemoteConfig):
def run(self):
for name, url in self.remote_config.list(
level=self.args.level
).items():
logger.info("{}\t{}".format(name, url))
return 0
+4 -4
View File
@@ -43,7 +43,7 @@ class CmdRemove(CmdBase):
def add_parser(subparsers, parent_parser):
REMOVE_HELP = "Remove outputs of DVC file."
REMOVE_HELP = "Remove outputs of DVC-file."
remove_parser = subparsers.add_parser(
"remove",
parents=[parent_parser],
@@ -57,14 +57,14 @@ def add_parser(subparsers, parent_parser):
"--outs",
action="store_true",
default=True,
help="Only remove DVC file outputs.(default)",
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.",
help="Remove DVC-file and all its outputs.",
)
remove_parser.add_argument(
"-f",
@@ -73,5 +73,5 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Force purge.",
)
remove_parser.add_argument("targets", nargs="+", help="DVC files.")
remove_parser.add_argument("targets", nargs="+", help="DVC-files.")
remove_parser.set_defaults(func=CmdRemove)
+11 -4
View File
@@ -15,7 +15,6 @@ logger = logging.getLogger(__name__)
class CmdRepro(CmdBase):
def run(self):
recursive = not self.args.single_item
saved_dir = os.path.realpath(os.curdir)
if self.args.cwd:
os.chdir(self.args.cwd)
@@ -31,7 +30,7 @@ class CmdRepro(CmdBase):
try:
stages = self.repo.reproduce(
target,
recursive=recursive,
single_item=self.args.single_item,
force=self.args.force,
dry=self.args.dry,
interactive=self.args.interactive,
@@ -40,6 +39,7 @@ class CmdRepro(CmdBase):
ignore_build_cache=self.args.ignore_build_cache,
no_commit=self.args.no_commit,
downstream=self.args.downstream,
recursive=self.args.recursive,
)
if len(stages) == 0:
@@ -58,7 +58,7 @@ class CmdRepro(CmdBase):
def add_parser(subparsers, parent_parser):
REPRO_HELP = "Check for changes and reproduce DVC file and dependencies."
REPRO_HELP = "Check for changes and reproduce DVC-file and dependencies."
repro_parser = subparsers.add_parser(
"repro",
parents=[parent_parser],
@@ -69,7 +69,7 @@ def add_parser(subparsers, parent_parser):
repro_parser.add_argument(
"targets",
nargs="*",
help="DVC file to reproduce (default - 'Dvcfile').",
help="DVC-file to reproduce (default - 'Dvcfile').",
)
repro_parser.add_argument(
"-f",
@@ -128,6 +128,13 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Reproduce all pipelines in the repo.",
)
repro_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Reproduce all stages in the specified directory.",
)
repro_parser.add_argument(
"--ignore-build-cache",
action="store_true",
+2 -3
View File
@@ -1,19 +1,18 @@
from __future__ import unicode_literals
import argparse
import os
import logging
from dvc.repo import Repo
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.utils import relpath
logger = logging.getLogger(__name__)
class CmdRoot(CmdBaseNoRepo):
def run(self):
logger.info(os.path.relpath(Repo.find_root()))
logger.info(relpath(Repo.find_root()))
return 0
+2 -2
View File
@@ -127,7 +127,7 @@ def add_parser(subparsers, parent_parser):
"(do not put into DVC cache).",
)
run_parser.add_argument(
"-f", "--file", help="Specify name of the DVC file it generates."
"-f", "--file", help="Specify name of the DVC-file it generates."
)
run_parser.add_argument(
"-c", "--cwd", default=None, help="Deprecated, use -w and -f instead."
@@ -155,7 +155,7 @@ def add_parser(subparsers, parent_parser):
"--overwrite-dvcfile",
action="store_true",
default=False,
help="Overwrite existing dvc file without asking for confirmation.",
help="Overwrite existing DVC-file without asking for confirmation.",
)
run_parser.add_argument(
"--ignore-build-cache",
+6 -6
View File
@@ -82,14 +82,14 @@ def add_parser(subparsers, parent_parser):
)
tag_add_parser.add_argument("tag", help="Dvc tag.")
tag_add_parser.add_argument(
"targets", nargs="*", default=[None], help="Dvc files."
"targets", nargs="*", default=[None], help="DVC-files."
)
tag_add_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Add tag for all dependencies of the specified DVC file.",
help="Add tag for all dependencies of the specified DVC-file.",
)
tag_add_parser.add_argument(
"-R",
@@ -109,14 +109,14 @@ def add_parser(subparsers, parent_parser):
)
tag_remove_parser.add_argument("tag", help="Dvc tag.")
tag_remove_parser.add_argument(
"targets", nargs="*", default=[None], help="Dvc files."
"targets", nargs="*", default=[None], help="DVC-files."
)
tag_remove_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Remove tag for all dependencies of the specified DVC file.",
help="Remove tag for all dependencies of the specified DVC-file.",
)
tag_remove_parser.add_argument(
"-R",
@@ -135,14 +135,14 @@ def add_parser(subparsers, parent_parser):
help=TAG_LIST_HELP,
)
tag_list_parser.add_argument(
"targets", nargs="*", default=[None], help="Dvc files."
"targets", nargs="*", default=[None], help="DVC-files."
)
tag_list_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="List tags for all dependencies of the specified DVC file.",
help="List tags for all dependencies of the specified DVC-file.",
)
tag_list_parser.add_argument(
"-R",
+80 -4
View File
@@ -1,11 +1,19 @@
from __future__ import unicode_literals
import os
import psutil
import platform
import argparse
import logging
import uuid
from dvc.utils import is_binary
from dvc.utils.compat import pathlib
from dvc.repo import Repo
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.version import __version__
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc.system import System
logger = logging.getLogger(__name__)
@@ -13,18 +21,86 @@ logger = logging.getLogger(__name__)
class CmdVersion(CmdBaseNoRepo):
def run(self):
dvc_version = __version__
python_version = platform.python_version()
platform_type = platform.platform()
binary = is_binary()
info = (
"DVC version: {dvc_version}\n"
"Python version: {python_version}\n"
"Platform: {platform_type}"
"Platform: {platform_type}\n"
"Binary: {binary}\n"
).format(
dvc_version=__version__,
python_version=platform.python_version(),
platform_type=platform.platform(),
dvc_version=dvc_version,
python_version=python_version,
platform_type=platform_type,
binary=binary,
)
try:
repo = Repo()
root_directory = repo.root_dir
info += (
"Cache: {cache}\n"
"Filesystem type (cache directory): {fs_cache}\n"
).format(
cache=self.get_linktype_support_info(repo),
fs_cache=self.get_fs_type(repo.cache.local.cache_dir),
)
except NotDvcRepoError:
root_directory = os.getcwd()
info += ("Filesystem type (workspace): {fs_root}").format(
fs_root=self.get_fs_type(os.path.abspath(root_directory))
)
logger.info(info)
return 0
@staticmethod
def get_fs_type(path):
partition = {
pathlib.Path(part.mountpoint): (part.fstype, part.device)
for part in psutil.disk_partitions()
}
for parent in pathlib.Path(path).parents:
if parent in partition:
return partition[parent]
return ("unkown", "none")
@staticmethod
def get_linktype_support_info(repo):
links = {
"reflink": System.reflink,
"hardlink": System.hardlink,
"symlink": System.symlink,
}
fname = "." + str(uuid.uuid4())
src = os.path.join(repo.cache.local.cache_dir, fname)
open(src, "w").close()
dst = os.path.join(repo.root_dir, fname)
cache = []
for name, link in links.items():
try:
link(src, dst)
os.unlink(dst)
supported = True
except DvcException:
supported = False
cache.append(
"{name} - {supported}".format(
name=name, supported=True if supported else False
)
)
os.remove(src)
return ", ".join(cache)
def add_parser(subparsers, parent_parser):
VERSION_HELP = "Show DVC version and system/environment informaion."
+207 -222
View File
@@ -2,15 +2,17 @@
from __future__ import unicode_literals
from dvc.utils.compat import str, open, urlparse
from dvc.utils.compat import str, open
import os
import re
import copy
import errno
import configobj
import logging
from schema import Schema, Optional, And, Use, Regex
from dvc.exceptions import DvcException
from schema import Schema, Optional, And, Use, Regex, SchemaError
from dvc.exceptions import DvcException, NotDvcRepoError
logger = logging.getLogger(__name__)
@@ -20,12 +22,12 @@ class ConfigError(DvcException):
Args:
msg (str): error message.
ex (Exception): optional exception that has caused this error.
cause (Exception): optional exception that has caused this error.
"""
def __init__(self, msg, ex=None):
def __init__(self, msg, cause=None):
super(ConfigError, self).__init__(
"config file error: {}".format(msg), ex
"config file error: {}".format(msg), cause=cause
)
@@ -43,24 +45,6 @@ def supported_cache_type(types):
return True
def supported_loglevel(level):
"""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):
"""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):
"""Checks that value is a boolean.
@@ -109,6 +93,23 @@ def is_percent(val):
return int(val) >= 0 and int(val) <= 100
class Choices(object):
"""Checks that value belongs to the specified set of values
Args:
*choices: pass allowed values as arguments, or pass a list or
tuple as a single argument
"""
def __init__(self, *choices):
if len(choices) == 1 and isinstance(choices[0], (list, tuple)):
choices = choices[0]
self.choices = choices
def __call__(self, value):
return value in self.choices
class Config(object): # pylint: disable=too-many-instance-attributes
"""Class that manages configuration files for a dvc repo.
@@ -134,11 +135,18 @@ class Config(object): # pylint: disable=too-many-instance-attributes
CONFIG = "config"
CONFIG_LOCAL = "config.local"
LEVEL_LOCAL = 0
LEVEL_REPO = 1
LEVEL_GLOBAL = 2
LEVEL_SYSTEM = 3
BOOL_SCHEMA = And(str, is_bool, Use(to_bool))
SECTION_CORE = "core"
SECTION_CORE_LOGLEVEL = "loglevel"
SECTION_CORE_LOGLEVEL_SCHEMA = And(Use(str.lower), supported_loglevel)
SECTION_CORE_LOGLEVEL_SCHEMA = And(
Use(str.lower), Choices("info", "debug", "warning", "error")
)
SECTION_CORE_REMOTE = "remote"
SECTION_CORE_INTERACTIVE_SCHEMA = BOOL_SCHEMA
SECTION_CORE_INTERACTIVE = "interactive"
@@ -173,7 +181,11 @@ class Config(object): # pylint: disable=too-many-instance-attributes
# backward compatibility
SECTION_CORE_CLOUD = "cloud"
SECTION_CORE_CLOUD_SCHEMA = And(Use(str.lower), supported_cloud)
SECTION_CORE_CLOUD_SCHEMA = And(
Use(str.lower),
# only the clouds which were supported by obsoleted cloud config
Choices("aws", "gcp", "local", ""),
)
SECTION_CORE_STORAGEPATH = "storagepath"
SECTION_CORE_SCHEMA = {
@@ -201,6 +213,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
SECTION_AWS_REGION = "region"
SECTION_AWS_PROFILE = "profile"
SECTION_AWS_USE_SSL = "use_ssl"
SECTION_AWS_SSE = "sse"
SECTION_AWS_SCHEMA = {
SECTION_AWS_STORAGEPATH: str,
Optional(SECTION_AWS_REGION): str,
@@ -209,6 +222,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_AWS_ENDPOINT_URL): str,
Optional(SECTION_AWS_LIST_OBJECTS, default=False): BOOL_SCHEMA,
Optional(SECTION_AWS_USE_SSL, default=True): BOOL_SCHEMA,
Optional(SECTION_AWS_SSE): str,
}
# backward compatibility
@@ -241,6 +255,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
SECTION_REMOTE_TIMEOUT = "timeout"
SECTION_REMOTE_PASSWORD = "password"
SECTION_REMOTE_ASK_PASSWORD = "ask_password"
SECTION_REMOTE_NO_TRAVERSE = "no_traverse"
SECTION_REMOTE_SCHEMA = {
SECTION_REMOTE_URL: str,
Optional(SECTION_AWS_REGION): str,
@@ -249,6 +264,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_AWS_ENDPOINT_URL): str,
Optional(SECTION_AWS_LIST_OBJECTS, default=False): BOOL_SCHEMA,
Optional(SECTION_AWS_USE_SSL, default=True): BOOL_SCHEMA,
Optional(SECTION_AWS_SSE): str,
Optional(SECTION_GCP_PROJECTNAME): str,
Optional(SECTION_CACHE_TYPE): SECTION_CACHE_TYPE_SCHEMA,
Optional(SECTION_CACHE_PROTECTED, default=False): BOOL_SCHEMA,
@@ -263,6 +279,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_OSS_ACCESS_KEY_SECRET): str,
Optional(SECTION_OSS_ENDPOINT): str,
Optional(PRIVATE_CWD): str,
Optional(SECTION_REMOTE_NO_TRAVERSE, default=True): BOOL_SCHEMA,
}
SECTION_STATE = "state"
@@ -285,30 +302,20 @@ class Config(object): # pylint: disable=too-many-instance-attributes
}
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.dvc_dir = dvc_dir
self.validate = validate
if dvc_dir is not None:
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
self.config_file = os.path.join(dvc_dir, self.CONFIG)
self.config_local_file = os.path.join(dvc_dir, self.CONFIG_LOCAL)
if not dvc_dir:
try:
from dvc.repo import Repo
self.dvc_dir = os.path.join(Repo.find_dvc_dir())
except NotDvcRepoError:
self.dvc_dir = None
else:
self.dvc_dir = None
self.config_file = None
self.config_local_file = None
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
self._system_config = None
self._global_config = None
self._repo_config = None
self._local_config = None
self.config = None
self.load(validate=validate)
self.load()
@staticmethod
def get_global_config_dir():
@@ -350,35 +357,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
open(config_file, "w+").close()
return Config(dvc_dir)
def _load(self):
self._system_config = configobj.ConfigObj(self.system_config_file)
self._global_config = configobj.ConfigObj(self.global_config_file)
if self.config_file is not None:
self._repo_config = configobj.ConfigObj(self.config_file)
else:
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
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):
def _resolve_cache_path(self, config):
cache = config.get(self.SECTION_CACHE)
if cache is None:
return
@@ -387,72 +366,86 @@ class Config(object): # pylint: disable=too-many-instance-attributes
if cache_dir is None:
return
cache[self.PRIVATE_CWD] = os.path.dirname(fname)
cache[self.PRIVATE_CWD] = os.path.dirname(config.filename)
def _resolve_paths(self, config, fname):
if fname is None:
return
def _resolve_paths(self, config):
if config.filename is None:
return config
self._resolve_cache_path(config, fname)
for section in config.values():
ret = copy.deepcopy(config)
self._resolve_cache_path(ret)
for section in ret.values():
if self.SECTION_REMOTE_URL not in section.keys():
continue
section[self.PRIVATE_CWD] = os.path.dirname(fname)
section[self.PRIVATE_CWD] = os.path.dirname(ret.filename)
def load(self, validate=True):
return ret
def _load_configs(self):
system_config_file = os.path.join(
self.get_system_config_dir(), self.CONFIG
)
global_config_file = os.path.join(
self.get_global_config_dir(), self.CONFIG
)
self._system_config = configobj.ConfigObj(system_config_file)
self._global_config = configobj.ConfigObj(global_config_file)
self._repo_config = configobj.ConfigObj()
self._local_config = configobj.ConfigObj()
if not self.dvc_dir:
return
config_file = os.path.join(self.dvc_dir, self.CONFIG)
config_local_file = os.path.join(self.dvc_dir, self.CONFIG_LOCAL)
self._repo_config = configobj.ConfigObj(config_file)
self._local_config = configobj.ConfigObj(config_local_file)
@property
def config_local_file(self):
return self._local_config.filename
@property
def config_file(self):
return self._repo_config.filename
def load(self):
"""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()
self._load_configs()
self.config = configobj.ConfigObj()
for c in [
self._system_config,
self._global_config,
self._repo_config,
self._local_config,
]:
c = self._resolve_paths(c)
c = self._lower(c)
self.config.merge(c)
if not self.validate:
return
d = self.config.dict()
try:
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
for conf in [user, config, local]:
self.config = self._merge(self.config, conf)
if validate:
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._resolve_paths(self.config, self.config_file)
except Exception as ex:
raise ConfigError(ex)
@staticmethod
def _get_key(conf, name, add=False):
for k in conf.keys():
if k.lower() == name.lower():
return k
if add:
conf[name] = {}
return name
return None
d = Schema(self.SCHEMA).validate(d)
except SchemaError as exc:
raise ConfigError("config format error", cause=exc)
self.config = configobj.ConfigObj(d, write_empty_values=True)
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.
"""
@@ -467,121 +460,103 @@ class Config(object): # pylint: disable=too-many-instance-attributes
]
for conf in clist:
if conf.filename is None:
continue
self._save(conf)
try:
logger.debug("Writing '{}'.".format(conf.filename))
dname = os.path.dirname(os.path.abspath(conf.filename))
try:
os.makedirs(dname)
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
conf.write()
except Exception as exc:
msg = "failed to write config '{}'".format(conf.filename)
raise ConfigError(msg, exc)
def get_remote_settings(self, name):
import posixpath
"""
Args:
name (str): The name of the remote that we want to retrieve
Returns:
dict: The content beneath the given remote name.
Example:
>>> config = {'remote "server"': {'url': 'ssh://localhost/'}}
>>> get_remote_settings("server")
{'url': 'ssh://localhost/'}
"""
settings = self.config[self.SECTION_REMOTE_FMT.format(name)]
parsed = urlparse(settings["url"])
# Support for cross referenced remotes.
# This will merge the settings, giving priority to the outer reference.
# For example, having:
#
# dvc remote add server ssh://localhost
# dvc remote modify server user root
# dvc remote modify server ask_password true
#
# dvc remote add images remote://server/tmp/pictures
# dvc remote modify images user alice
# dvc remote modify images ask_password false
# dvc remote modify images password asdf1234
#
# Results on a config dictionary like:
#
# {
# "url": "ssh://localhost/tmp/pictures",
# "user": "alice",
# "password": "asdf1234",
# "ask_password": False,
# }
#
if parsed.scheme == "remote":
reference = self.get_remote_settings(parsed.netloc)
url = posixpath.join(reference["url"], parsed.path.lstrip("/"))
merged = reference.copy()
merged.update(settings)
merged["url"] = url
return merged
return settings
self.load()
@staticmethod
def unset(config, section, opt=None):
def _save(config):
if config.filename is None:
return
logger.debug("Writing '{}'.".format(config.filename))
dname = os.path.dirname(os.path.abspath(config.filename))
try:
os.makedirs(dname)
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
config.write()
def unset(self, section, opt=None, level=None, force=False):
"""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.
level (int): config level to use.
force (bool): don't error-out even if section doesn't exist. False
by default.
Raises:
dvc.config.ConfigError: thrown if section doesn't exist and
`force != True`.
"""
config = self.get_configobj(level)
if section not in config.keys():
if force:
return
raise ConfigError("section '{}' doesn't exist".format(section))
if opt is None:
del config[section]
return
if opt:
if opt not in config[section].keys():
if force:
return
raise ConfigError(
"option '{}.{}' doesn't exist".format(section, opt)
)
del config[section][opt]
if opt not in config[section].keys():
raise ConfigError(
"option '{}.{}' doesn't exist".format(section, opt)
)
del config[section][opt]
if not config[section]:
if not config[section]:
del config[section]
else:
del config[section]
@staticmethod
def set(config, section, opt, value):
self.save(config)
def set(self, section, opt, value, level=None, force=True):
"""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.
level (int): config level to use.
force (bool): set option even if section already exists. True by
default.
Raises:
dvc.config.ConfigError: thrown if section already exists and
`force != True`.
"""
config = self.get_configobj(level)
if section not in config.keys():
config[section] = {}
elif not force:
raise ConfigError(
"Section '{}' already exists. Use `-f|--force` to overwrite "
"section with new value.".format(section)
)
config[section][opt] = value
self.save(config)
@staticmethod
def show(config, section, opt):
"""Prints option value from the config.
def get(self, section, opt=None, level=None):
"""Return option value from the config.
Args:
config (configobj.ConfigObj): config to work on.
section (str): section name.
opt (str): option name.
level (int): config level to use.
Returns:
value (str, int): option value.
"""
config = self.get_configobj(level)
if section not in config.keys():
raise ConfigError("section '{}' doesn't exist".format(section))
@@ -590,25 +565,35 @@ class Config(object): # pylint: disable=too-many-instance-attributes
"option '{}.{}' doesn't exist".format(section, opt)
)
logger.info(config[section][opt])
@staticmethod
def _merge(first, second):
res = {}
sections = list(first.keys()) + list(second.keys())
for section in sections:
first_copy = first.get(section, {}).copy()
second_copy = second.get(section, {}).copy()
first_copy.update(second_copy)
res[section] = first_copy
return res
return config[section][opt]
@staticmethod
def _lower(config):
new_config = {}
new_config = configobj.ConfigObj()
for s_key, s_value in config.items():
new_s = {}
for key, value in s_value.items():
new_s[key.lower()] = str(value)
new_config[s_key.lower()] = new_s
return new_config
def get_configobj(self, level):
configs = {
self.LEVEL_LOCAL: self._local_config,
self.LEVEL_REPO: self._repo_config,
self.LEVEL_GLOBAL: self._global_config,
self.LEVEL_SYSTEM: self._system_config,
}
return configs.get(level, self._repo_config)
def list_options(self, section_regex, option, level=None):
ret = {}
config = self.get_configobj(level)
for section in config.keys():
r = re.match(section_regex, section)
if r:
name = r.group("name")
value = config[section].get(option, "")
ret[name] = value
return ret
+24 -36
View File
@@ -25,7 +25,6 @@ class DataCloud(object):
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.
@@ -43,40 +42,43 @@ class DataCloud(object):
"https": RemoteHTTP,
}
def __init__(self, repo, config=None):
def __init__(self, repo):
self.repo = repo
self._config = config
self._core = self._config[Config.SECTION_CORE]
@property
def _cloud(self):
"""Returns a Remote instance using the `core.remote` config"""
remote = self._core.get(Config.SECTION_CORE_REMOTE, "")
def _config(self):
return self.repo.config.config
@property
def _core(self):
return self._config.get(Config.SECTION_CORE, {})
def get_remote(self, remote=None, command="<command>"):
if not 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
# Old config format support for backward compatibility
if Config.SECTION_CORE_CLOUD in self._core:
msg = "using obsoleted config format. Consider updating."
logger.warning(msg)
return self._init_compat()
return None
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(command)
)
def _init_remote(self, 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"
raise ConfigError(msg.format(section))
return Remote(self.repo, cloud_config)
return Remote(self.repo, name=remote)
def _init_compat(self):
name = self._core.get(Config.SECTION_CORE_CLOUD, "").strip().lower()
if name == "":
self._cloud = None
return None
cloud_type = self.CLOUD_MAP.get(name, None)
@@ -104,20 +106,6 @@ class DataCloud(object):
cloud = cloud_type(self.repo, cloud_config)
return cloud
def _get_cloud(self, remote, cmd):
if remote:
return self._init_remote(remote)
if self._cloud:
return self._cloud
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=None, remote=None, show_checksums=False):
"""Push data items in a cloud-agnostic way.
@@ -132,7 +120,7 @@ class DataCloud(object):
return self.repo.cache.local.push(
targets,
jobs=jobs,
remote=self._get_cloud(remote, "push"),
remote=self.get_remote(remote, "push"),
show_checksums=show_checksums,
)
@@ -150,7 +138,7 @@ class DataCloud(object):
return self.repo.cache.local.pull(
targets,
jobs=jobs,
remote=self._get_cloud(remote, "pull"),
remote=self.get_remote(remote, "pull"),
show_checksums=show_checksums,
)
@@ -166,7 +154,7 @@ class DataCloud(object):
show_checksums (bool): show checksums instead of file names in
information messages.
"""
cloud = self._get_cloud(remote, "status")
remote = self.get_remote(remote, "status")
return self.repo.cache.local.status(
targets, jobs=jobs, remote=cloud, show_checksums=show_checksums
targets, jobs=jobs, remote=remote, show_checksums=show_checksums
)
+12 -4
View File
@@ -13,8 +13,11 @@ from dvc.dependency.hdfs import DependencyHDFS
from dvc.dependency.ssh import DependencySSH
from dvc.dependency.http import DependencyHTTP
from dvc.dependency.https import DependencyHTTPS
from .repo import DependencyREPO
from dvc.remote import Remote
from dvc.external_repo import ExternalRepo
DEPS = [
DependencyGS,
@@ -44,6 +47,7 @@ DEP_MAP = {
SCHEMA = output.SCHEMA.copy()
del SCHEMA[schema.Optional(OutputBase.PARAM_CACHE)]
del SCHEMA[schema.Optional(OutputBase.PARAM_METRIC)]
SCHEMA[schema.Optional(DependencyREPO.PARAM_REPO)] = ExternalRepo.SCHEMA
def _get(stage, p, info):
@@ -52,10 +56,13 @@ def _get(stage, p, info):
parsed = urlparse(p)
if parsed.scheme == "remote":
settings = stage.repo.config.get_remote_settings(parsed.netloc)
remote = Remote(stage.repo, settings)
remote = Remote(stage.repo, name=parsed.netloc)
return DEP_MAP[remote.scheme](stage, p, info, remote=remote)
if info and info.get(DependencyREPO.PARAM_REPO):
repo = info.pop(DependencyREPO.PARAM_REPO)
return DependencyREPO(repo, stage, p, info)
for d in DEPS:
if d.supported(p):
return d(stage, p, info)
@@ -70,8 +77,9 @@ def loadd_from(stage, d_list):
return ret
def loads_from(stage, s_list):
def loads_from(stage, s_list, erepo=None):
ret = []
for s in s_list:
ret.append(_get(stage, s, {}))
info = {DependencyREPO.PARAM_REPO: erepo} if erepo else {}
ret.append(_get(stage, s, info))
return ret
-11
View File
@@ -1,7 +1,5 @@
from __future__ import unicode_literals
from dvc.path import Path
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
@@ -9,12 +7,3 @@ from dvc.dependency.base import DependencyBase
class DependencyHTTP(DependencyBase, OutputBase):
REMOTE = RemoteHTTP
def __init__(self, stage, path, info=None, remote=None):
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 = Path(self.scheme, url=self.url, path=path)
+43
View File
@@ -0,0 +1,43 @@
from __future__ import unicode_literals
import os
import copy
from dvc.utils.compat import urlparse
from dvc.external_repo import ExternalRepo
from .local import DependencyLOCAL
class DependencyREPO(DependencyLOCAL):
PARAM_REPO = "repo"
def __init__(self, erepo, stage, *args, **kwargs):
self.erepo = ExternalRepo(stage.repo.dvc_dir, **erepo)
super(DependencyLOCAL, self).__init__(stage, *args, **kwargs)
def _parse_path(self, remote, path):
self.erepo.install(self.repo.cache.local.cache_dir)
out_path = os.path.join(
self.erepo.repo.root_dir, urlparse(path).path.lstrip("/")
)
out, = self.erepo.repo.find_outs_by_path(out_path)
self.info = copy.copy(out.info)
self._erepo_stage = copy.copy(out.stage.path)
return self.REMOTE.path_cls(out.cache_path)
@property
def is_in_repo(self):
return False
def dumpd(self):
ret = super(DependencyLOCAL, self).dumpd()
ret[self.PARAM_REPO] = self.erepo.dumpd()
return ret
def download(self, to, resume=False):
self.erepo.repo.fetch(self._erepo_stage)
to.info = copy.copy(self.info)
to.checkout()
+7 -8
View File
@@ -4,9 +4,10 @@ from __future__ import unicode_literals
from dvc.utils.compat import str, builtin_str
import os
import traceback
from dvc.utils import relpath
class DvcException(Exception):
"""Base class for all dvc exceptions.
@@ -62,7 +63,7 @@ class OutputNotFoundError(DvcException):
def __init__(self, output):
super(OutputNotFoundError, self).__init__(
"unable to find stage file with output '{path}'".format(
path=os.path.relpath(output)
path=relpath(output)
)
)
@@ -98,9 +99,7 @@ class CircularDependencyError(DvcException):
"""
def __init__(self, dependency):
assert isinstance(dependency, str) or isinstance(
dependency, builtin_str
)
assert isinstance(dependency, (str, builtin_str))
msg = (
"file/directory '{}' is specified as an output and as a "
@@ -118,7 +117,7 @@ class ArgumentDuplicationError(DvcException):
"""
def __init__(self, path):
assert isinstance(path, str) or isinstance(path, builtin_str)
assert isinstance(path, (str, builtin_str))
msg = "file '{}' is specified more than once."
super(ArgumentDuplicationError, self).__init__(msg.format(path))
@@ -200,7 +199,7 @@ 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)
relpath(path)
)
)
@@ -215,7 +214,7 @@ class NoMetricsError(DvcException):
class StageFileCorruptedError(DvcException):
def __init__(self, path, cause=None):
path = os.path.relpath(path)
path = relpath(path)
super(StageFileCorruptedError, self).__init__(
"unable to read stage file: {} "
"YAML file structure is corrupted".format(path),
+152
View File
@@ -0,0 +1,152 @@
from __future__ import unicode_literals
import os
import shutil
import logging
import shortuuid
from funcy import cached_property
from schema import Optional
from dvc.config import Config
from dvc.cache import CacheConfig
from dvc.exceptions import DvcException
from dvc.utils.compat import makedirs, str
from dvc.utils import remove
logger = logging.getLogger(__name__)
class ExternalRepoError(DvcException):
pass
class NotInstalledError(ExternalRepoError):
def __init__(self, name):
super(NotInstalledError, self).__init__(
"Repo '{}' is not installed".format(name)
)
class InstallError(ExternalRepoError):
def __init__(self, url, path, cause):
super(InstallError, self).__init__(
"Failed to install repo '{}' to '{}'".format(url, path),
cause=cause,
)
class RevError(ExternalRepoError):
def __init__(self, url, rev, cause):
super(RevError, self).__init__(
"Failed to access revision '{}' for repo '{}'".format(rev, url),
cause=cause,
)
class ExternalRepo(object):
REPOS_DIR = "repos"
PARAM_URL = "url"
PARAM_VERSION = "rev"
SCHEMA = {Optional(PARAM_URL): str, Optional(PARAM_VERSION): str}
def __init__(self, dvc_dir, **kwargs):
self.repos_dir = os.path.join(dvc_dir, self.REPOS_DIR)
self.url = kwargs[self.PARAM_URL]
self.name = "{}-{}".format(os.path.basename(self.url), hash(self.url))
self.rev = kwargs.get(self.PARAM_VERSION)
self.path = os.path.join(self.repos_dir, self.name)
@cached_property
def repo(self):
from dvc.repo import Repo
if not self.installed:
raise NotInstalledError(self.name)
return Repo(self.path, rev=self.rev)
@property
def installed(self):
return os.path.exists(self.path)
def _install_to(self, tmp_dir, cache_dir):
import git
try:
git.Repo.clone_from(
self.url, tmp_dir, depth=1, no_single_branch=True
)
except git.exc.GitCommandError as exc:
raise InstallError(self.url, tmp_dir, exc)
if self.rev:
try:
repo = git.Repo(tmp_dir)
repo.git.checkout(self.rev)
repo.close()
except git.exc.GitCommandError as exc:
raise RevError(self.url, self.rev, exc)
if cache_dir:
from dvc.repo import Repo
repo = Repo(tmp_dir)
cache_config = CacheConfig(repo.config)
cache_config.set_dir(cache_dir, level=Config.LEVEL_LOCAL)
repo.scm.git.close()
def install(self, cache_dir=None, force=False):
if self.installed and not force:
logger.info(
"Skipping installing '{}'('{}') as it is already "
"installed.".format(self.name, self.url)
)
return
makedirs(self.repos_dir, exist_ok=True)
# installing package to a temporary directory until we are sure that
# it has been installed correctly.
#
# Note that we can't use tempfile.TemporaryDirectory is using symlinks
# to tmpfs, so we won't be able to use move properly.
tmp_dir = os.path.join(self.repos_dir, "." + str(shortuuid.uuid()))
try:
self._install_to(tmp_dir, cache_dir)
except ExternalRepoError:
if os.path.exists(tmp_dir):
remove(tmp_dir)
raise
if self.installed:
self.uninstall()
shutil.move(tmp_dir, self.path)
def uninstall(self):
if not self.installed:
logger.info(
"Skipping uninstalling '{}' as it is not installed.".format(
self.name
)
)
return
remove(self.path)
def update(self):
self.repo.scm.fetch(self.rev)
def dumpd(self):
ret = {self.PARAM_URL: self.url}
if self.rev:
ret[self.PARAM_VERSION] = self.rev
return ret
+14 -39
View File
@@ -1,7 +1,10 @@
import os
from __future__ import unicode_literals
from dulwich.ignore import match_pattern, read_ignore_patterns
from dvc.utils.compat import cast_bytes
import os
from pathspec import PathSpec
from pathspec.patterns import GitWildMatchPattern
from dvc.utils import relpath
from dvc.utils.fs import get_parent_dirs_up_to
@@ -10,23 +13,12 @@ class DvcIgnoreFileHandler(object):
self.tree = tree
def read_patterns(self, path):
with self.tree.open(path, binary=True) as stream:
return self._read_patterns(stream)
with self.tree.open(path) as fobj:
return PathSpec.from_lines(GitWildMatchPattern, fobj)
def get_repo_root(self):
return self.tree.tree_root
def _read_patterns(self, binary_stream):
negate_patterns = []
patterns = []
for pattern in read_ignore_patterns(binary_stream):
if pattern.lstrip().startswith(b"!"):
negate_patterns.append(pattern)
else:
patterns.append(pattern)
return negate_patterns, patterns
class DvcIgnore(object):
DVCIGNORE_FILE = ".dvcignore"
@@ -39,12 +31,8 @@ class DvcIgnoreFromFile(DvcIgnore):
def __init__(self, ignore_file_path, ignore_handler):
self.ignore_file_path = ignore_file_path
self.dirname = os.path.normpath(os.path.dirname(ignore_file_path))
self.patterns = []
self.negate_patterns = []
self.negate_patterns, self.patterns = ignore_handler.read_patterns(
ignore_file_path
)
self.ignore_spec = ignore_handler.read_patterns(ignore_file_path)
def __call__(self, root, dirs, files):
files = [f for f in files if not self.matches(root, f)]
@@ -52,26 +40,13 @@ class DvcIgnoreFromFile(DvcIgnore):
return dirs, files
def get_match(self, abs_path):
rel_path = os.path.relpath(abs_path, self.dirname)
if os.name == "nt":
rel_path = rel_path.replace("\\", "/")
rel_path = cast_bytes(rel_path, "utf-8")
for pattern in self.patterns:
if match_pattern(
rel_path, pattern
) and self._no_negate_pattern_matches(rel_path):
return (abs_path, pattern, self.ignore_file_path)
return None
def matches(self, dirname, basename):
if self.get_match(os.path.join(dirname, basename)):
return True
return False
abs_path = os.path.join(dirname, basename)
relative_path = relpath(abs_path, self.dirname)
if os.name == "nt":
relative_path = relative_path.replace("\\", "/")
def _no_negate_pattern_matches(self, path):
return all([not match_pattern(path, p) for p in self.negate_patterns])
return self.ignore_spec.match_file(relative_path)
def __hash__(self):
return hash(self.ignore_file_path)
+19 -3
View File
@@ -10,6 +10,12 @@ import logging.config
import colorama
class LoggingException(Exception):
def __init__(self, record):
msg = "failed to log {}".format(str(record))
super(LoggingException, self).__init__(msg)
class ExcludeErrorsFilter(logging.Filter):
def filter(self, record):
return record.levelno < logging.ERROR
@@ -149,6 +155,12 @@ class ColorFormatter(logging.Formatter):
progress.clearln()
class LoggerHandler(logging.StreamHandler):
def handleError(self, record):
super(LoggerHandler, self).handleError(record)
raise LoggingException(record)
def setup(level=logging.INFO):
colorama.init()
@@ -159,14 +171,14 @@ def setup(level=logging.INFO):
"formatters": {"color": {"()": ColorFormatter}},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"class": "dvc.logger.LoggerHandler",
"level": "DEBUG",
"formatter": "color",
"stream": "ext://sys.stdout",
"filters": ["exclude_errors"],
},
"console_errors": {
"class": "logging.StreamHandler",
"class": "dvc.logger.LoggerHandler",
"level": "ERROR",
"formatter": "color",
"stream": "ext://sys.stderr",
@@ -176,7 +188,11 @@ def setup(level=logging.INFO):
"dvc": {
"level": level,
"handlers": ["console", "console_errors"],
}
},
"paramiko": {
"level": logging.CRITICAL,
"handlers": ["console", "console_errors"],
},
},
}
)
+5
View File
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import logging
from dvc.cli import parse_args
from dvc.config import ConfigError
from dvc.analytics import Analytics
from dvc.exceptions import NotDvcRepoError, DvcParserError
@@ -33,9 +34,13 @@ def main(argv=None):
elif args.verbose:
logger.setLevel(logging.DEBUG)
logging.getLogger("paramiko").setLevel(logging.DEBUG)
cmd = args.func(args)
ret = cmd.run_cmd()
except ConfigError:
logger.exception("configuration error")
ret = 251
except KeyboardInterrupt:
logger.exception("interrupted by the user")
ret = 252
+1 -2
View File
@@ -61,8 +61,7 @@ def _get(stage, p, info, cache, metric, persist=False, tags=None):
parsed = urlparse(p)
if parsed.scheme == "remote":
settings = stage.repo.config.get_remote_settings(parsed.netloc)
remote = Remote(stage.repo, settings)
remote = Remote(stage.repo, name=parsed.netloc)
return OUTS_MAP[remote.scheme](
stage,
p,
+137 -41
View File
@@ -1,13 +1,13 @@
from __future__ import unicode_literals
import re
import logging
from copy import copy
from schema import Or, Optional
import dvc.prompt as prompt
from dvc.exceptions import DvcException
from dvc.utils.compat import str
from dvc.utils.compat import str, urlparse
from dvc.remote.base import RemoteBASE
@@ -58,6 +58,8 @@ class OutputBase(object):
DoesNotExistError = OutputDoesNotExistError
IsNotFileOrDirError = OutputIsNotFileOrDirError
sep = "/"
def __init__(
self,
stage,
@@ -69,9 +71,19 @@ class OutputBase(object):
persist=False,
tags=None,
):
# This output (and dependency) objects have too many paths/urls
# here is a list and comments:
#
# .def_path - path from definition in stage file
# .path_info - PathInfo/URLInfo structured resolved path
# .fspath - local only, resolved
# .__str__ - for presentation purposes, def_path/relpath
#
# By resolved path, which contains actual location,
# should be absolute and don't contain remote:// refs.
self.stage = stage
self.repo = stage.repo
self.url = path
self.repo = stage.repo if stage else None
self.def_path = path
self.info = info
self.remote = remote or self.REMOTE(self.repo, {})
self.use_cache = False if self.IS_DEPENDENCY else cache
@@ -86,13 +98,26 @@ class OutputBase(object):
)
)
self.path_info = self._parse_path(remote, path)
def _parse_path(self, remote, path):
if remote:
parsed = urlparse(path)
return remote.path_info / parsed.path.lstrip("/")
else:
return self.REMOTE.path_cls(path)
def __repr__(self):
return "{class_name}: '{url}'".format(
class_name=type(self).__name__, url=(self.url or "No url")
return "{class_name}: '{def_path}'".format(
class_name=type(self).__name__, def_path=self.def_path
)
def __str__(self):
return self.url
return self.def_path
@property
def scheme(self):
return self.REMOTE.scheme
@property
def is_in_repo(self):
@@ -111,35 +136,13 @@ class OutputBase(object):
"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
return cls.REMOTE.supported(url)
@property
def cache_path(self):
return self.cache.checksum_to_path(self.checksum)
@property
def sep(self):
return "/"
return self.cache.checksum_to_path_info(self.checksum).url
@property
def checksum(self):
@@ -228,11 +231,11 @@ class OutputBase(object):
return
if self.is_in_repo:
if self.repo.scm.is_tracked(self.path):
if self.repo.scm.is_tracked(self.fspath):
raise OutputAlreadyTrackedError(self)
if self.use_cache:
self.repo.scm.ignore(self.path)
self.repo.scm.ignore(self.fspath)
self.info = self.remote.save_info(self.path_info)
@@ -242,7 +245,7 @@ class OutputBase(object):
def dumpd(self):
ret = copy(self.info)
ret[self.PARAM_PATH] = self.url
ret[self.PARAM_PATH] = self.def_path
if self.IS_DEPENDENCY:
return ret
@@ -269,8 +272,8 @@ class OutputBase(object):
"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 download(self, to, resume=False):
self.remote.download([self.path_info], [to.path_info], resume=resume)
def checkout(self, force=False, progress_callback=None, tag=None):
if not self.use_cache:
@@ -294,20 +297,20 @@ class OutputBase(object):
return
if ignore_remove and self.use_cache and self.is_in_repo:
self.repo.scm.ignore_remove(self.path)
self.repo.scm.ignore_remove(self.fspath)
def move(self, out):
if self.scheme == "local" and self.use_cache and self.is_in_repo:
self.repo.scm.ignore_remove(self.path)
self.repo.scm.ignore_remove(self.fspath)
self.remote.move(self.path_info, out.path_info)
self.url = out.url
self.path_info = copy(out.path_info)
self.def_path = out.def_path
self.path_info = out.path_info
self.save()
self.commit()
if self.scheme == "local" and self.use_cache and self.is_in_repo:
self.repo.scm.ignore(self.path)
self.repo.scm.ignore(self.fspath)
def get_files_number(self):
if not self.use_cache or not self.checksum:
@@ -321,3 +324,96 @@ class OutputBase(object):
def unprotect(self):
if self.exists:
self.remote.unprotect(self.path_info)
def _collect_used_dir_cache(self, remote=None, force=False, jobs=None):
"""Get a list of `info`s retaled to the given directory.
- Pull the directory entry from the remote cache if it was changed.
Example:
Given the following commands:
$ echo "foo" > directory/foo
$ echo "bar" > directory/bar
$ dvc add directory
It will return something similar to the following list:
[
{ 'path': 'directory/foo', 'md5': 'c157a79031e1', ... },
{ 'path': 'directory/bar', 'md5': 'd3b07384d113', ... },
]
"""
ret = []
if self.cache.changed_cache_file(self.checksum):
try:
self.repo.cloud.pull(
[
{
self.remote.PARAM_CHECKSUM: self.checksum,
"name": str(self),
}
],
jobs=jobs,
remote=remote,
show_checksums=False,
)
except DvcException:
logger.debug("failed to pull cache for '{}'".format(self))
if self.cache.changed_cache_file(self.checksum):
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(self)
)
else:
return ret
for entry in self.dir_cache:
info = copy(entry)
path_info = self.path_info / entry[self.remote.PARAM_RELPATH]
info["name"] = str(path_info)
ret.append(info)
return ret
def get_used_cache(self, **kwargs):
"""Get a dumpd of the given `out`, with an entry including the branch.
The `used_cache` of an output is no more than its `info`.
In case that the given output is a directory, it will also
include the `info` of its files.
"""
if self.stage.is_repo_import:
return []
if not self.use_cache:
return []
if not self.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(self, self.stage)
)
return []
ret = [{self.remote.PARAM_CHECKSUM: self.checksum, "name": str(self)}]
if not self.is_dir_checksum:
return ret
ret.extend(self._collect_used_dir_cache(**kwargs))
return ret
-30
View File
@@ -1,38 +1,8 @@
from __future__ import unicode_literals
import posixpath
from dvc.path.hdfs import PathHDFS
from dvc.utils.compat import urlparse
from dvc.output.base import OutputBase
from dvc.remote.hdfs import RemoteHDFS
class OutputHDFS(OutputBase):
REMOTE = RemoteHDFS
def __init__(
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
super(OutputHDFS, self).__init__(
stage,
path,
info=info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
if remote:
path = posixpath.join(remote.url, urlparse(path).path.lstrip("/"))
user = remote.user if remote else self.group("user")
self.path_info = PathHDFS(user=user, url=self.url, path=path)
+38 -62
View File
@@ -3,8 +3,7 @@ from __future__ import unicode_literals
import os
import logging
from dvc.path.local import PathLOCAL
from dvc.utils.compat import urlparse
from dvc.utils.compat import urlparse, fspath_py35, str
from dvc.istextfile import istextfile
from dvc.exceptions import DvcException
from dvc.remote.local import RemoteLOCAL
@@ -16,82 +15,58 @@ logger = logging.getLogger(__name__)
class OutputLOCAL(OutputBase):
REMOTE = RemoteLOCAL
sep = os.sep
def __init__(
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
super(OutputLOCAL, self).__init__(
stage,
path,
info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
if remote:
p = os.path.join(
remote.prefix, urlparse(self.url).path.lstrip("/")
)
def _parse_path(self, remote, path):
parsed = urlparse(path)
if parsed.scheme == "remote":
p = remote.path_info / parsed.path.lstrip("/")
else:
p = path
# NOTE: we can path either from command line or .dvc file,
# so we should expect both posix and windows style paths.
# PathInfo accepts both, i.e. / works everywhere, \ only on win.
#
# FIXME: if we have Windows path containig / or posix one with \
# then we have #2059 bug and can't really handle that.
p = self.REMOTE.path_cls(path)
if not p.is_absolute():
p = self.stage.wdir / p
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))
self.path_info = PathLOCAL(url=self.url, path=p)
abs_p = os.path.abspath(os.path.normpath(fspath_py35(p)))
return self.REMOTE.path_cls(abs_p)
def __str__(self):
return self.rel_path
return str(self.path_info)
@property
def fspath(self):
return self.path_info.fspath
@property
def is_in_repo(self):
return urlparse(self.url).scheme != "remote" and not os.path.isabs(
self.url
)
def_scheme = urlparse(self.def_path).scheme
return def_scheme != "remote" and not os.path.isabs(self.def_path)
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)
# Make path info the same but relative to passed stage
rel_path = self.path_info.relpath(self.stage.wdir)
self.path_info = (
self.REMOTE.path_cls(os.path.abspath(stage.wdir)) / rel_path
)
self.repo = Repo(self.path)
self.repo = Repo(self.fspath)
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)
def dumpd(self):
ret = super(OutputLOCAL, self).dumpd()
if self.is_in_repo:
path = self.remote.to_posixpath(
os.path.relpath(self.path, self.stage.wdir)
)
path = self.path_info.relpath(self.stage.wdir).as_posix()
else:
path = self.url
path = self.def_path
ret[self.PARAM_PATH] = path
@@ -101,13 +76,14 @@ class OutputLOCAL(OutputBase):
if not self.metric:
return
if not os.path.exists(self.path):
path = fspath_py35(self.path_info)
if not os.path.exists(path):
return
if os.path.isdir(self.path):
if os.path.isdir(path):
msg = "directory '{}' cannot be used as metrics."
raise DvcException(msg.format(self.rel_path))
raise DvcException(msg.format(self.path_info))
if not istextfile(self.path):
if not istextfile(path):
msg = "binary file '{}' cannot be used as metrics."
raise DvcException(msg.format(self.rel_path))
raise DvcException(msg.format(self.path_info))
-34
View File
@@ -1,42 +1,8 @@
from __future__ import unicode_literals
import posixpath
from dvc.path import Path
from dvc.remote.s3 import RemoteS3
from dvc.utils.compat import urlparse
from dvc.output.base import OutputBase
class OutputS3(OutputBase):
REMOTE = RemoteS3
def __init__(
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
super(OutputS3, self).__init__(
stage,
path,
info=info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
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 = Path(
self.scheme, bucket=bucket, path=path, url=self.url
)
-46
View File
@@ -1,54 +1,8 @@
from __future__ import unicode_literals
import getpass
import posixpath
from dvc.path.ssh import PathSSH
from dvc.utils.compat import urlparse
from dvc.output.base import OutputBase
from dvc.remote.ssh import RemoteSSH
class OutputSSH(OutputBase):
REMOTE = RemoteSSH
def __init__(
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
super(OutputSSH, self).__init__(
stage,
path,
info=info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
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())
)
if remote:
path = posixpath.join(
remote.prefix, urlparse(path).path.lstrip("/")
)
else:
path = parsed.path
self.path_info = PathSSH(
host=host, user=user, port=port, url=self.url, path=path
)
-29
View File
@@ -1,29 +0,0 @@
from dvc.scheme import Schemes
from dvc.path.azure import PathAZURE
from dvc.path.gs import PathGS
from dvc.path.hdfs import PathHDFS
from dvc.path.http import PathHTTP
from dvc.path.https import PathHTTPS
from dvc.path.local import PathLOCAL
from dvc.path.oss import PathOSS
from dvc.path.s3 import PathS3
from dvc.path.ssh import PathSSH
PATH_MAP = {
Schemes.SSH: PathSSH,
Schemes.HDFS: PathHDFS,
Schemes.S3: PathS3,
Schemes.AZURE: PathAZURE,
Schemes.HTTP: PathHTTP,
Schemes.HTTPS: PathHTTPS,
Schemes.GS: PathGS,
Schemes.LOCAL: PathLOCAL,
Schemes.OSS: PathOSS,
}
def Path(scheme, *args, **kwargs):
cls = PATH_MAP[scheme]
return cls(*args, **kwargs)
-6
View File
@@ -1,6 +0,0 @@
from dvc.scheme import Schemes
from .base import PathCloudBASE
class PathAZURE(PathCloudBASE):
scheme = Schemes.AZURE
-23
View File
@@ -1,23 +0,0 @@
from dvc.utils.compat import urlunsplit
class PathBASE(object):
scheme = None
def __init__(self, url=None, path=None):
self.url = url
self.path = path
def __str__(self):
return self.url
class PathCloudBASE(PathBASE):
def __init__(self, bucket, url=None, path=None):
super(PathCloudBASE, self).__init__(url, path)
self.bucket = bucket
def __str__(self):
if not self.url:
return urlunsplit((self.scheme, self.bucket, self.path, "", ""))
return self.url
-6
View File
@@ -1,6 +0,0 @@
from dvc.scheme import Schemes
from .base import PathCloudBASE
class PathGS(PathCloudBASE):
scheme = Schemes.GS
-16
View File
@@ -1,16 +0,0 @@
from dvc.utils.compat import urlunsplit
from dvc.scheme import Schemes
from .base import PathBASE
class PathHDFS(PathBASE):
scheme = Schemes.HDFS
def __init__(self, user, url=None, path=None):
super(PathHDFS, self).__init__(url, path)
self.user = user
def __str__(self):
if not self.url:
return urlunsplit((self.scheme, self.user, self.path, "", ""))
return self.url
-13
View File
@@ -1,13 +0,0 @@
from dvc.scheme import Schemes
from dvc.utils.compat import urlunsplit
from .base import PathBASE
class PathHTTP(PathBASE):
scheme = Schemes.HTTP
def __str__(self):
if not self.url:
return urlunsplit((self.scheme, self.path, "", "", ""))
return self.url
-6
View File
@@ -1,6 +0,0 @@
from dvc.scheme import Schemes
from .http import PathHTTP
class PathHTTPS(PathHTTP):
scheme = Schemes.HTTPS
-17
View File
@@ -1,17 +0,0 @@
import os
from dvc.scheme import Schemes
from .base import PathBASE
class PathLOCAL(PathBASE):
scheme = Schemes.LOCAL
def __str__(self):
if os.name == "nt" and not os.path.commonprefix(
[os.getcwd(), self.path]
):
# In case of windows, when cache is on different drive than
# workspace, we will get ValueError when trying to get relpath
return self.path
return os.path.relpath(self.path)
-6
View File
@@ -1,6 +0,0 @@
from dvc.scheme import Schemes
from .base import PathCloudBASE
class PathOSS(PathCloudBASE):
scheme = Schemes.OSS
-6
View File
@@ -1,6 +0,0 @@
from dvc.scheme import Schemes
from .base import PathCloudBASE
class PathS3(PathCloudBASE):
scheme = Schemes.S3
-26
View File
@@ -1,26 +0,0 @@
from dvc.utils.compat import urlunsplit
from dvc.scheme import Schemes
from .base import PathBASE
class PathSSH(PathBASE):
scheme = Schemes.SSH
def __init__(self, host, user, port, url=None, path=None):
super(PathSSH, self).__init__(url, path)
self.host = host
self.user = user
self.port = port
def __str__(self):
if not self.url:
return urlunsplit(
(
self.scheme,
"{}@{}:{}".format(self.user, self.host, self.port),
self.path,
"",
"",
)
)
return self.url
+210
View File
@@ -0,0 +1,210 @@
from __future__ import unicode_literals, absolute_import
import sys
import os
import posixpath
from funcy import cached_property
from dvc.utils.compat import str, builtin_str, basestring, is_py2
from dvc.utils.compat import pathlib, urlparse
# On Python 2.7/Windows sys.getfilesystemencoding() is set to mbcs,
# which is lossy, thus we can't use that,
# see https://github.com/mcmtroffaes/pathlib2/issues/56.
from dvc.utils import relpath
if is_py2:
fs_encoding = "utf-8"
class PathInfo(pathlib.PurePath):
scheme = "local"
def __new__(cls, *args):
# Construct a proper subclass depending on current os
if cls is PathInfo:
cls = WindowsPathInfo if os.name == "nt" else PosixPathInfo
return cls._from_parts(args)
@classmethod
def from_posix(cls, s):
return cls(PosixPathInfo(s))
def __str__(self):
path = self.__fspath__()
return relpath(path)
def __repr__(self):
return builtin_str("{}: '{}'").format(type(self).__name__, self)
# This permits passing it to file utils directly in Python 3.6+
# With Python 2.7, Python 3.5+ we are stuck with path_info.fspath for now
def __fspath__(self):
return pathlib.PurePath.__str__(self)
@property
def fspath(self):
return self.__fspath__()
url = fspath
def relpath(self, other):
return self.__class__(relpath(self, other))
def isin(self, other):
if isinstance(other, basestring):
other = self.__class__(other)
elif self.__class__ != other.__class__:
return False
return any(p == other for p in self.parents)
# pathlib2 uses bytes internally in Python 2, and we use unicode everywhere
# for paths in both pythons, thus we need this glue.
if is_py2:
__unicode__ = __str__
def __str__(self):
return self.__unicode__().encode(sys.getfilesystemencoding())
@classmethod
def _parse_args(cls, args):
args = [
a.encode(fs_encoding)
if isinstance(a, unicode) # noqa: F821
else a
for a in args
]
return super(PathInfo, cls)._parse_args(args)
@property
def name(self):
return super(PathInfo, self).name.decode(fs_encoding)
def __fspath__(self): # noqa: F811
return pathlib.PurePath.__str__(self).decode(fs_encoding)
def with_name(self, name):
return pathlib.PurePath.with_name(self, name.encode(fs_encoding))
def as_posix(self):
f = self._flavour
return str(self).replace(f.sep, "/")
class WindowsPathInfo(PathInfo, pathlib.PureWindowsPath):
pass
class PosixPathInfo(PathInfo, pathlib.PurePosixPath):
pass
class URLInfo(object):
DEFAULT_PORTS = {"http": 80, "https": 443, "ssh": 22}
def __init__(self, url):
self.parsed = urlparse(url)
assert self.parsed.scheme != "remote"
@classmethod
def from_parts(
cls, scheme=None, netloc=None, host=None, user=None, port=None, path=""
):
assert scheme and (bool(host) ^ bool(netloc))
if netloc is None:
netloc = host
if user:
netloc = user + "@" + host
if port:
netloc += ":" + str(port)
return cls("{}://{}{}".format(scheme, netloc, path))
@cached_property
def url(self):
p = self.parsed
netloc = p.hostname
if p.username:
netloc = p.username + "@" + netloc
if p.port and int(p.port) != self.DEFAULT_PORTS.get(p.scheme):
netloc += ":" + str(p.port)
return "{}://{}{}".format(p.scheme, netloc, p.path)
def __str__(self):
return self.url
def __repr__(self):
return "{}: '{}'".format(type(self).__name__, self)
def __eq__(self, other):
if isinstance(other, basestring):
other = self.__class__(other)
return str(self) == str(other)
def __hash__(self):
return hash(self.url)
def __div__(self, other):
p = self.parsed
new_path = posixpath.join(p.path, str(other))
if not new_path.startswith("/"):
new_path = "/" + new_path
new_url = "{}://{}{}".format(p.scheme, p.netloc, new_path)
return self.__class__(new_url)
__truediv__ = __div__
def __getattr__(self, name):
return getattr(self.parsed, name)
@property
def port(self):
return self.parsed.port or self.DEFAULT_PORTS.get(self.parsed.scheme)
@property
def host(self):
return self.parsed.hostname
@property
def user(self):
return self.parsed.username
@cached_property
def _path(self):
return pathlib.PurePosixPath(self.parsed.path)
@property
def name(self):
return self._path.name
@property
def parts(self):
return (self.scheme, self.netloc) + self._path.parts
@property
def bucket(self):
return self.netloc
@property
def parent(self):
return self.from_parts(
scheme=self.scheme, netloc=self.netloc, path=self._path.parent
)
def isin(self, other):
if isinstance(other, basestring):
other = self.__class__(other)
elif self.__class__ != other.__class__:
return False
return (
self.scheme == other.scheme
and self.netloc == other.netloc
and PathInfo(self.path).isin(PathInfo(other.path))
)
class CloudURLInfo(URLInfo):
@property
def path(self):
return self.parsed.path.lstrip("/")
+1 -1
View File
@@ -111,7 +111,7 @@ class Progress(object):
logger = logging.getLogger(__name__)
if logger.getEffectiveLevel() == "CRITICAL":
if logger.getEffectiveLevel() == logging.CRITICAL:
return
print(*args, **kwargs)
+10 -2
View File
@@ -10,6 +10,8 @@ from dvc.remote.http import RemoteHTTP
from dvc.remote.https import RemoteHTTPS
from dvc.remote.oss import RemoteOSS
from .config import RemoteConfig
REMOTES = [
RemoteAZURE,
@@ -31,5 +33,11 @@ def _get(config):
return RemoteLOCAL
def Remote(repo, config):
return _get(config)(repo, config)
def Remote(repo, **kwargs):
name = kwargs.get("name")
if name:
remote_config = RemoteConfig(repo.config)
settings = remote_config.get_settings(name)
else:
settings = kwargs
return _get(settings)(repo, settings)
+32 -98
View File
@@ -6,7 +6,6 @@ import re
import logging
from dvc.scheme import Schemes
from dvc.path.azure import PathAZURE
try:
from azure.storage.blob import BlockBlobService
@@ -14,11 +13,11 @@ try:
except ImportError:
BlockBlobService = None
from dvc.utils import tmp_fname, move
from dvc.utils.compat import urlparse, makedirs
from dvc.utils.compat import urlparse
from dvc.progress import progress
from dvc.config import Config
from dvc.remote.base import RemoteBASE
from dvc.path_info import CloudURLInfo
logger = logging.getLogger(__name__)
@@ -34,6 +33,7 @@ class Callback(object):
class RemoteAZURE(RemoteBASE):
scheme = Schemes.AZURE
path_cls = CloudURLInfo
REGEX = (
r"azure://((?P<path>[^=;]*)?|("
# backward compatibility
@@ -47,58 +47,57 @@ class RemoteAZURE(RemoteBASE):
def __init__(self, repo, config):
super(RemoteAZURE, self).__init__(repo, config)
self.url = config.get(Config.SECTION_REMOTE_URL, "azure://")
match = re.match(self.REGEX, self.url) # backward compatibility
url = config.get(Config.SECTION_REMOTE_URL, "azure://")
match = re.match(self.REGEX, url) # backward compatibility
path = match.group("path")
self.bucket = (
urlparse(self.url if path else "").netloc
bucket = (
urlparse(url if path else "").netloc
or match.group("container_name") # backward compatibility
or os.getenv("AZURE_STORAGE_CONTAINER_NAME")
)
self.prefix = urlparse(self.url).path.lstrip("/") if path else ""
self.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:
if not bucket:
raise ValueError("azure storage container name missing")
if not self.connection_string:
raise ValueError("azure storage connection string missing")
self.__blob_service = None
self.path_info = PathAZURE(bucket=self.bucket)
self.path_info = (
self.path_cls(url)
if path
else self.path_cls.from_parts(scheme=self.scheme, netloc=bucket)
)
@property
def blob_service(self):
if self.__blob_service is None:
logger.debug("URL {}".format(self.url))
logger.debug("URL {}".format(self.path_info))
logger.debug("Connection string {}".format(self.connection_string))
self.__blob_service = BlockBlobService(
connection_string=self.connection_string
)
logger.debug("Container name {}".format(self.bucket))
logger.debug("Container name {}".format(self.path_info.bucket))
try: # verify that container exists
self.__blob_service.list_blobs(
self.bucket, delimiter="/", num_results=1
self.path_info.bucket, delimiter="/", num_results=1
)
except AzureMissingResourceHttpError:
self.__blob_service.create_container(self.bucket)
self.__blob_service.create_container(self.path_info.bucket)
return self.__blob_service
def remove(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
logger.debug(
"Removing azure://{}/{}".format(path_info.bucket, path_info.path)
)
logger.debug("Removing {}".format(path_info))
self.blob_service.delete_blob(path_info.bucket, path_info.path)
def _list_paths(self, bucket, prefix):
@@ -118,85 +117,20 @@ class RemoteAZURE(RemoteBASE):
next_marker = blobs.next_marker
def list_cache_paths(self):
return self._list_paths(self.bucket, self.prefix)
return self._list_paths(self.path_info.bucket, self.path_info.path)
def upload(self, from_infos, to_infos, names=None, no_progress_bar=False):
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:
raise NotImplementedError
if from_info.scheme != "local":
raise NotImplementedError
bucket = to_info.bucket
path = to_info.path
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info.path, bucket, path
)
)
if not name:
name = os.path.basename(from_info.path)
cb = None if no_progress_bar else Callback(name)
try:
self.blob_service.create_blob_from_path(
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,
resume=False,
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
names = self._verify_path_args(from_infos, to_infos, names)
cb = None if no_progress_bar else Callback(name)
self.blob_service.create_blob_from_path(
to_info.bucket, to_info.path, from_file, progress_callback=cb
)
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 != "local":
raise NotImplementedError
bucket = from_info.bucket
path = from_info.path
logger.debug(
"Downloading '{}/{}' to '{}'".format(
bucket, path, to_info.path
)
)
tmp_file = tmp_fname(to_info.path)
if not name:
name = os.path.basename(to_info.path)
cb = None if no_progress_bar else Callback(name)
makedirs(os.path.dirname(to_info.path), exist_ok=True)
try:
self.blob_service.get_blob_to_path(
bucket, path, tmp_file, progress_callback=cb
)
except Exception:
msg = "failed to download '{}/{}'".format(bucket, path)
logger.warning(msg)
else:
move(tmp_file, to_info.path)
if not no_progress_bar:
progress.finish_target(name)
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
):
cb = None if no_progress_bar else Callback(name)
self.blob_service.get_blob_to_path(
from_info.bucket, from_info.path, to_file, progress_callback=cb
)
+238 -138
View File
@@ -1,26 +1,24 @@
from __future__ import unicode_literals
from copy import copy
from dvc.path.local import PathLOCAL
from dvc.utils.compat import str
from dvc.utils.compat import str, basestring, urlparse, fspath_py35, makedirs
import os
import re
import json
import ntpath
import logging
import tempfile
import posixpath
import itertools
from contextlib import contextmanager
from operator import itemgetter
from multiprocessing import cpu_count
from concurrent.futures import ThreadPoolExecutor
import dvc.prompt as prompt
from dvc.config import Config
from dvc.exceptions import DvcException, ConfirmRemoveError
from dvc.progress import progress
from dvc.utils import LARGE_DIR_SIZE, tmp_fname
from dvc.progress import progress, ProgressCallback
from dvc.utils import LARGE_DIR_SIZE, tmp_fname, to_chunks, move, relpath
from dvc.state import StateBase
from dvc.path_info import PathInfo, URLInfo
logger = logging.getLogger(__name__)
@@ -67,8 +65,8 @@ class RemoteMissingDepsError(DvcException):
class RemoteBASE(object):
scheme = None
REGEX = None
scheme = "base"
path_cls = URLInfo
REQUIRES = {}
JOBS = 4 * cpu_count()
@@ -104,13 +102,14 @@ class RemoteBASE(object):
raise RemoteMissingDepsError(msg)
self.protected = False
self.no_traverse = config.get(Config.SECTION_REMOTE_NO_TRAVERSE)
self.state = StateBase()
self._dir_info = {}
def __repr__(self):
return "{class_name}: '{url}'".format(
class_name=type(self).__name__, url=(self.url or "No url")
return "{class_name}: '{path_info}'".format(
class_name=type(self).__name__,
path_info=self.path_info or "No path",
)
def compat_config(config):
@@ -118,39 +117,14 @@ class RemoteBASE(object):
@classmethod
def supported(cls, config):
url = config[Config.SECTION_REMOTE_URL]
return cls.match(url) is not None
if isinstance(config, basestring):
url = config
else:
url = config[Config.SECTION_REMOTE_URL]
@staticmethod
def _replace_sep(path, from_sep, to_sep):
assert not ntpath.isabs(path)
assert not posixpath.isabs(path)
return path.replace(from_sep, to_sep)
@classmethod
def to_posixpath(cls, path):
return cls._replace_sep(path, "\\", "/")
@classmethod
def to_ntpath(cls, path):
return cls._replace_sep(path, "/", "\\")
def to_ospath(self, path):
if self.ospath == ntpath:
return self.to_ntpath(path)
elif self.ospath == posixpath:
return self.to_posixpath(path)
assert False
@classmethod
def match(cls, url):
return re.match(cls.REGEX, url)
def group(self, name):
m = self.match(self.url)
if not m:
return None
return m.group(name)
# NOTE: silently skipping remote, calling code should handle that
parsed = urlparse(url)
return parsed.scheme == cls.scheme
@property
def cache(self):
@@ -162,28 +136,31 @@ class RemoteBASE(object):
def _collect_dir(self, path_info):
dir_info = []
p_info = copy(path_info)
dpath = p_info.path
for root, dirs, files in self.walk(path_info):
if len(files) > LARGE_DIR_SIZE:
msg = (
"Computing md5 for a large directory {}. "
"This is only done once."
)
relpath = self.ospath.relpath(root)
logger.info(msg.format(relpath))
files = progress(files, name=relpath)
title = str(self.path_cls(root))
logger.info(msg.format(title))
files = progress(files, name=title)
for fname in files:
path = self.ospath.join(root, fname)
p_info.path = path
relpath = self.to_posixpath(self.ospath.relpath(path, dpath))
checksum = self.get_file_checksum(p_info)
file_info = self.path_cls(root) / fname
relative_path = file_info.relative_to(path_info)
dir_info.append(
{
self.PARAM_RELPATH: relpath,
self.PARAM_CHECKSUM: checksum,
# NOTE: this is lossy transformation:
# "hey\there" -> "hey/there"
# "hey/there" -> "hey/there"
# The latter is fine filename on Windows,
# which will transform to dir/file on back transform.
#
# Yes, this is a BUG, as long as we permit "/" in
# filenames on Windows and "\" on Unix
self.PARAM_RELPATH: relative_path.as_posix(),
self.PARAM_CHECKSUM: self.get_file_checksum(file_info),
}
)
@@ -192,32 +169,27 @@ class RemoteBASE(object):
def get_dir_checksum(self, path_info):
dir_info = self._collect_dir(path_info)
checksum, from_info, to_info = self._get_dir_info_checksum(
dir_info, path_info
)
checksum, tmp_info = self._get_dir_info_checksum(dir_info)
new_info = self.cache.checksum_to_path_info(checksum)
if self.cache.changed_cache_file(checksum):
self.cache.move(from_info, to_info)
self.cache.move(tmp_info, new_info)
self.state.save(path_info, checksum)
self.state.save(to_info, checksum)
self.state.save(new_info, checksum)
return checksum
def _get_dir_info_checksum(self, dir_info, path_info):
to_info = copy(path_info)
to_info.path = self.cache.ospath.join(self.cache.prefix, tmp_fname(""))
def _get_dir_info_checksum(self, dir_info):
tmp = tempfile.NamedTemporaryFile(delete=False).name
with open(tmp, "w+") as fobj:
json.dump(dir_info, fobj, sort_keys=True)
from_info = PathLOCAL(path=tmp)
from_info = PathInfo(tmp)
to_info = self.cache.path_info / tmp_fname("")
self.cache.upload([from_info], [to_info], no_progress_bar=True)
checksum = self.get_file_checksum(to_info) + self.CHECKSUM_DIR_SUFFIX
from_info = copy(to_info)
to_info.path = self.cache.checksum_to_path(checksum)
return checksum, from_info, to_info
return checksum, to_info
def get_dir_cache(self, checksum):
assert checksum
@@ -235,7 +207,7 @@ class RemoteBASE(object):
fobj = tempfile.NamedTemporaryFile(delete=False)
path = fobj.name
to_info = PathLOCAL(path=path)
to_info = self.path_cls(path)
self.cache.download([path_info], [to_info], no_progress_bar=True)
try:
@@ -249,11 +221,13 @@ class RemoteBASE(object):
if not isinstance(d, list):
msg = "dir cache file format error '{}' [skipping the file]"
logger.error(msg.format(os.path.relpath(path)))
logger.error(msg.format(relpath(path)))
return []
for info in d:
info[self.PARAM_RELPATH] = self.to_ospath(info[self.PARAM_RELPATH])
# NOTE: here is a BUG, see comment to .as_posix() below
relative_path = self.path_cls.from_posix(info[self.PARAM_RELPATH])
info[self.PARAM_RELPATH] = relative_path.fspath
return d
@@ -333,7 +307,7 @@ class RemoteBASE(object):
logger.debug("'{}' hasn't changed.".format(path_info))
return False
def link(self, from_info, to_info):
def link(self, from_info, to_info, link_type=None):
self.copy(from_info, to_info)
def _save_file(self, path_info, checksum, save_link=True):
@@ -360,13 +334,9 @@ class RemoteBASE(object):
cache_info = self.checksum_to_path_info(checksum)
dir_info = self.get_dir_cache(checksum)
entry_info = copy(path_info)
for entry in dir_info:
entry_info = path_info / entry[self.PARAM_RELPATH]
entry_checksum = entry[self.PARAM_CHECKSUM]
entry_info.path = self.ospath.join(
path_info.path, entry[self.PARAM_RELPATH]
)
self._save_file(entry_info, entry_checksum, save_link=False)
self.state.save_link(path_info)
@@ -385,7 +355,8 @@ class RemoteBASE(object):
def walk(self, path_info):
raise NotImplementedError
def protect(self, path_info):
@staticmethod
def protect(path_info):
pass
def save(self, path_info, checksum_info):
@@ -410,18 +381,113 @@ class RemoteBASE(object):
return
self._save_file(path_info, checksum)
@contextmanager
def transfer_context(self):
yield None
def upload(self, from_infos, to_infos, names=None, no_progress_bar=False):
if not hasattr(self, "_upload"):
raise RemoteActionNotImplemented("upload", self.scheme)
names = self._verify_path_args(to_infos, from_infos, names)
fails = 0
with self.transfer_context() as ctx:
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
msg = "Uploading '{}' to '{}'"
logger.debug(msg.format(from_info, to_info))
if not name:
name = from_info.name
if not no_progress_bar:
progress.update_target(name, 0, None)
try:
self._upload(
from_info.fspath,
to_info,
name=name,
ctx=ctx,
no_progress_bar=no_progress_bar,
)
except Exception:
fails += 1
msg = "failed to upload '{}' to '{}'"
logger.exception(msg.format(from_info, to_info))
continue
if not no_progress_bar:
progress.finish_target(name)
return fails
def download(
self,
from_infos,
to_infos,
names=None,
no_progress_bar=False,
name=None,
resume=False,
):
raise RemoteActionNotImplemented("download", self.scheme)
if not hasattr(self, "_download"):
raise RemoteActionNotImplemented("download", self.scheme)
def upload(self, from_infos, to_infos, names=None, no_progress_bar=False):
raise RemoteActionNotImplemented("upload", self.scheme)
names = self._verify_path_args(from_infos, to_infos, names)
fails = 0
with self.transfer_context() as ctx:
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 != "local":
self.copy(from_info, to_info, ctx=ctx)
continue
if to_info.scheme != "local":
raise NotImplementedError
msg = "Downloading '{}' to '{}'".format(from_info, to_info)
logger.debug(msg)
tmp_file = tmp_fname(to_info)
if not name:
name = to_info.name
if not no_progress_bar:
# real progress is not always available,
# lets at least show start and finish
progress.update_target(name, 0, None)
makedirs(fspath_py35(to_info.parent), exist_ok=True)
try:
self._download(
from_info,
tmp_file,
name=name,
ctx=ctx,
resume=resume,
no_progress_bar=no_progress_bar,
)
except Exception:
fails += 1
msg = "failed to download '{}' to '{}'"
logger.exception(msg.format(from_info, to_info))
continue
move(tmp_file, fspath_py35(to_info))
if not no_progress_bar:
progress.finish_target(name)
return fails
def remove(self, path_info):
raise RemoteActionNotImplemented("remove", self.scheme)
@@ -430,10 +496,10 @@ class RemoteBASE(object):
self.copy(from_info, to_info)
self.remove(from_info)
def copy(self, from_info, to_info):
def copy(self, from_info, to_info, ctx=None):
raise RemoteActionNotImplemented("copy", self.scheme)
def exists(self, path_infos):
def exists(self, path_info):
raise NotImplementedError
@classmethod
@@ -450,25 +516,11 @@ class RemoteBASE(object):
return names
@property
def ospath(self):
return posixpath
def checksum_to_path(self, checksum):
assert 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)
return "".join(self.path_cls(path).parts[-2:])
def checksum_to_path_info(self, checksum):
path_info = copy(self.path_info)
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]
return self.path_info / checksum[0:2] / checksum[2:]
def list_cache_paths(self):
raise NotImplementedError
@@ -483,9 +535,7 @@ class RemoteBASE(object):
)
def gc(self, cinfos):
from dvc.remote.local import RemoteLOCAL
used = {info[RemoteLOCAL.PARAM_CHECKSUM] for info in cinfos["local"]}
used = self.extract_used_local_checksums(cinfos)
if self.scheme != "":
used |= {
@@ -503,6 +553,17 @@ class RemoteBASE(object):
return removed
def changed_cache_file(self, checksum):
"""Compare the given checksum with the (corresponding) actual one.
- Use `State` as a cache for computed checksums
+ The entries are invalidated by taking into account the following:
* mtime
* inode
* size
* checksum
- Remove the file from cache if it doesn't match the actual checksum
"""
cache_info = self.checksum_to_path_info(checksum)
actual = self.get_checksum(cache_info)
@@ -528,11 +589,15 @@ class RemoteBASE(object):
if self.changed_cache_file(checksum):
return True
if not self._changed_unpacked_dir(checksum):
return False
for entry in self.get_dir_cache(checksum):
checksum = entry[self.PARAM_CHECKSUM]
if self.changed_cache_file(checksum):
entry_checksum = entry[self.PARAM_CHECKSUM]
if self.changed_cache_file(entry_checksum):
return True
self._update_unpacked_dir(checksum)
return False
def changed_cache(self, checksum):
@@ -541,15 +606,44 @@ class RemoteBASE(object):
return self.changed_cache_file(checksum)
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()))
"""Check if the given checksums are stored in the remote.
There are two ways of performing this check:
- Traverse: Get a list of all the files in the remote
(traversing the cache directory) and compare it with
the given checksums.
- No traverse: For each given checksum, run the `exists`
method and filter the checksums that aren't on the remote.
This is done in parallel threads.
It also shows a progress bar when performing the check.
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).
Returns:
A list with checksums that were found in the remote
"""
progress_callback = ProgressCallback(len(checksums))
def exists_with_progress(chunks):
return self.batch_exists(chunks, callback=progress_callback)
if self.no_traverse and hasattr(self, "batch_exists"):
with ThreadPoolExecutor(max_workers=self.JOBS) as executor:
path_infos = [self.checksum_to_path_info(x) for x in checksums]
chunks = to_chunks(path_infos, self.JOBS)
results = executor.map(exists_with_progress, chunks)
in_remote = itertools.chain.from_iterable(results)
ret = list(itertools.compress(checksums, in_remote))
progress_callback.finish("")
return ret
return list(set(checksums) & set(self.all()))
def already_cached(self, path_info):
current = self.get_checksum(path_info)
@@ -587,7 +681,7 @@ class RemoteBASE(object):
self.state.save_link(path_info)
self.state.save(path_info, checksum)
if progress_callback:
progress_callback.update(path_info.url)
progress_callback.update(str(path_info))
def makedirs(self, path_info):
raise NotImplementedError
@@ -604,13 +698,11 @@ class RemoteBASE(object):
logger.debug("Linking directory '{}'.".format(path_info))
entry_info = copy(path_info)
for entry in dir_info:
relpath = entry[self.PARAM_RELPATH]
relative_path = entry[self.PARAM_RELPATH]
entry_checksum = entry[self.PARAM_CHECKSUM]
entry_cache_info = self.checksum_to_path_info(entry_checksum)
entry_info.url = self.ospath.join(path_info.url, relpath)
entry_info.path = self.ospath.join(path_info.path, relpath)
entry_info = path_info / relative_path
entry_checksum_info = {self.PARAM_CHECKSUM: entry_checksum}
if self.changed(entry_info, entry_checksum_info):
@@ -619,7 +711,7 @@ class RemoteBASE(object):
self.link(entry_cache_info, entry_info)
self.state.save(entry_info, entry_checksum)
if progress_callback:
progress_callback.update(entry_info.url)
progress_callback.update(str(entry_info))
self._remove_redundant_files(path_info, dir_info, force)
@@ -628,30 +720,22 @@ class RemoteBASE(object):
def _remove_redundant_files(self, path_info, dir_info, force):
existing_files = set(
self.ospath.join(root, fname)
self.path_cls(root) / fname
for root, _, files in self.walk(path_info)
for fname in files
)
needed_files = set(
self.ospath.join(path_info.path, entry[self.PARAM_RELPATH])
for entry in dir_info
)
needed_files = {
path_info / entry[self.PARAM_RELPATH] for entry in dir_info
}
delta = existing_files - needed_files
d_info = copy(path_info)
for path in delta:
d_info.path = path
self.safe_remove(d_info, force)
for path in existing_files - needed_files:
self.safe_remove(path, force)
def checkout(
self, path_info, checksum_info, force=False, progress_callback=None
):
if (
path_info.scheme not in ["local"]
and path_info.scheme != self.scheme
):
if path_info.scheme not in ["local", self.scheme]:
raise NotImplementedError
checksum = checksum_info.get(self.PARAM_CHECKSUM)
@@ -690,3 +774,19 @@ class RemoteBASE(object):
@staticmethod
def unprotect(path_info):
pass
def _get_unpacked_dir_names(self, checksums):
return set()
def extract_used_local_checksums(self, cinfos):
from dvc.remote import RemoteLOCAL
used = {info[RemoteLOCAL.PARAM_CHECKSUM] for info in cinfos["local"]}
unpacked = self._get_unpacked_dir_names(used)
return used | unpacked
def _changed_unpacked_dir(self, checksum):
return True
def _update_unpacked_dir(self, checksum, progress_callback=None):
pass
+152
View File
@@ -0,0 +1,152 @@
import os
import logging
import posixpath
from dvc.utils import relpath
from dvc.utils.compat import urlparse
from dvc.config import Config, ConfigError
logger = logging.getLogger(__name__)
class RemoteConfig(object):
def __init__(self, config):
self.config = config
def get_settings(self, name):
"""
Args:
name (str): The name of the remote that we want to retrieve
Returns:
dict: The content beneath the given remote name.
Example:
>>> config = {'remote "server"': {'url': 'ssh://localhost/'}}
>>> get_settings("server")
{'url': 'ssh://localhost/'}
"""
settings = self.config.config.get(
Config.SECTION_REMOTE_FMT.format(name.lower())
)
if settings is None:
raise ConfigError(
"unable to find remote section '{}'".format(name)
)
parsed = urlparse(settings["url"])
# Support for cross referenced remotes.
# This will merge the settings, giving priority to the outer reference.
# For example, having:
#
# dvc remote add server ssh://localhost
# dvc remote modify server user root
# dvc remote modify server ask_password true
#
# dvc remote add images remote://server/tmp/pictures
# dvc remote modify images user alice
# dvc remote modify images ask_password false
# dvc remote modify images password asdf1234
#
# Results on a config dictionary like:
#
# {
# "url": "ssh://localhost/tmp/pictures",
# "user": "alice",
# "password": "asdf1234",
# "ask_password": False,
# }
#
if parsed.scheme == "remote":
reference = self.get_settings(parsed.netloc)
url = posixpath.join(reference["url"], parsed.path.lstrip("/"))
merged = reference.copy()
merged.update(settings)
merged["url"] = url
return merged
return settings
@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 relpath(path, os.path.dirname(config_file))
def add(self, name, url, default=False, force=False, level=None):
from dvc.remote import _get, RemoteLOCAL
configobj = self.config.get_configobj(level)
remote = _get({Config.SECTION_REMOTE_URL: url})
if remote == RemoteLOCAL and not url.startswith("remote://"):
url = self.resolve_path(url, configobj.filename)
self.config.set(
Config.SECTION_REMOTE_FMT.format(name),
Config.SECTION_REMOTE_URL,
url,
force=force,
level=level,
)
if default:
logger.info("Setting '{}' as a default remote.".format(name))
self.config.set(
Config.SECTION_CORE,
Config.SECTION_CORE_REMOTE,
name,
level=level,
)
def remove(self, name, level=None):
self.config.unset(Config.SECTION_REMOTE_FMT.format(name), level=level)
if level is None:
level = Config.LEVEL_REPO
for lev in [
Config.LEVEL_LOCAL,
Config.LEVEL_REPO,
Config.LEVEL_GLOBAL,
Config.LEVEL_SYSTEM,
]:
self.config.unset(
Config.SECTION_CORE,
Config.SECTION_CORE_REMOTE,
level=lev,
force=True,
)
if lev == level:
break
def modify(self, name, option, value, level=None):
self.config.set(
Config.SECTION_REMOTE_FMT.format(name), option, value, level=level
)
def list(self, level=None):
return self.config.list_options(
Config.SECTION_REMOTE_REGEX, Config.SECTION_REMOTE_URL, level=level
)
def set_default(self, name, unset=False, level=None):
if unset:
self.config.unset(Config.SECTION_CORE, Config.SECTION_CORE_REMOTE)
return
self.config.set(
Config.SECTION_CORE, Config.SECTION_CORE_REMOTE, name, level=level
)
+43 -120
View File
@@ -1,22 +1,19 @@
from __future__ import unicode_literals
import os
import logging
from dvc.scheme import Schemes
from dvc.path.gs import PathGS
import itertools
from contextlib import contextmanager
try:
from google.cloud import storage
except ImportError:
storage = None
from dvc.utils import tmp_fname, move
from dvc.utils.compat import urlparse, makedirs
from dvc.remote.base import RemoteBASE
from dvc.config import Config
from dvc.progress import progress
from dvc.exceptions import DvcException
from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
@@ -24,29 +21,24 @@ logger = logging.getLogger(__name__)
class RemoteGS(RemoteBASE):
scheme = Schemes.GS
REGEX = r"^gs://(?P<path>.*)$"
path_cls = CloudURLInfo
REQUIRES = {"google.cloud.storage": storage}
PARAM_CHECKSUM = "md5"
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)
storagepath = "gs://" + config.get(Config.SECTION_GCP_STORAGEPATH, "/")
url = config.get(Config.SECTION_REMOTE_URL, storagepath)
self.path_info = self.path_cls(url)
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 = PathGS(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_GCP_STORAGEPATH, "").lstrip("/")
ret[Config.SECTION_REMOTE_URL] = url
return ret
@@ -72,135 +64,66 @@ class RemoteGS(RemoteBASE):
md5 = base64.b64decode(b64_md5)
return codecs.getencoder("hex")(md5)[0].decode("utf-8")
def copy(self, from_info, to_info, gs=None):
gs = gs if gs else self.gs
def copy(self, from_info, to_info, ctx=None):
gs = ctx or self.gs
blob = gs.bucket(from_info.bucket).get_blob(from_info.path)
from_bucket = gs.bucket(from_info.bucket)
blob = from_bucket.get_blob(from_info.path)
if not blob:
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.path
)
to_bucket = gs.bucket(to_info.bucket)
from_bucket.copy_blob(blob, to_bucket, new_name=to_info.path)
def remove(self, path_info):
if path_info.scheme != "gs":
raise NotImplementedError
logger.debug(
"Removing gs://{}/{}".format(path_info.bucket, path_info.path)
)
logger.debug("Removing gs://{}".format(path_info))
blob = self.gs.bucket(path_info.bucket).get_blob(path_info.path)
if not blob:
return
blob.delete()
def _list_paths(self, bucket, prefix):
for blob in self.gs.bucket(bucket).list_blobs(prefix=prefix):
def _list_paths(self, bucket, prefix, gs=None):
gs = gs or self.gs
for blob in gs.bucket(bucket).list_blobs(prefix=prefix):
yield blob.name
def list_cache_paths(self):
return self._list_paths(self.bucket, self.prefix)
return self._list_paths(self.path_info.bucket, self.path_info.path)
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info.scheme == "gs"
paths = self._list_paths(path_info.bucket, path_info.path)
paths = set(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, no_progress_bar=False):
names = self._verify_path_args(to_infos, from_infos, names)
def batch_exists(self, path_infos, callback):
paths = []
gs = self.gs
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info.scheme != "gs":
raise NotImplementedError
if from_info.scheme != "local":
raise NotImplementedError
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info.path, to_info.bucket, to_info.path
)
for path_info in path_infos:
paths.append(
self._list_paths(path_info.bucket, path_info.path, gs)
)
callback.update(str(path_info))
if not name:
name = os.path.basename(from_info.path)
paths = set(itertools.chain.from_iterable(paths))
if not no_progress_bar:
progress.update_target(name, 0, None)
return [path_info.path in paths for path_info in path_infos]
try:
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.exception(
msg.format(from_info.path, to_info.bucket, to_info.path)
)
continue
@contextmanager
def transfer_context(self):
yield self.gs
progress.finish_target(name)
def _upload(self, from_file, to_info, ctx=None, **_kwargs):
bucket = ctx.bucket(to_info.bucket)
blob = bucket.blob(to_info.path)
blob.upload_from_filename(from_file)
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":
raise NotImplementedError
if to_info.scheme == "gs":
self.copy(from_info, to_info, gs=gs)
continue
if to_info.scheme != "local":
raise NotImplementedError
msg = "Downloading '{}/{}' to '{}'".format(
from_info.bucket, from_info.path, to_info.path
)
logger.debug(msg)
tmp_file = tmp_fname(to_info.path)
if not name:
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 pathpoints(start, finish)
progress.update_target(name, 0, None)
makedirs(os.path.dirname(to_info.path), exist_ok=True)
try:
bucket = gs.bucket(from_info.bucket)
blob = bucket.get_blob(from_info.path)
blob.download_to_filename(tmp_file)
except Exception:
msg = "failed to download '{}/{}' to '{}'"
logger.exception(
msg.format(from_info.bucket, from_info.path, to_info.path)
)
continue
move(tmp_file, to_info.path)
if not no_progress_bar:
progress.finish_target(name)
def _download(self, from_info, to_file, ctx=None, **_kwargs):
bucket = ctx.bucket(from_info.bucket)
blob = bucket.get_blob(from_info.path)
blob.download_to_filename(to_file)
+25 -64
View File
@@ -9,7 +9,7 @@ from subprocess import Popen, PIPE
from dvc.config import Config
from dvc.scheme import Schemes
from dvc.path.hdfs import PathHDFS
from dvc.remote.base import RemoteBASE, RemoteCmdError
from dvc.utils import fix_env, tmp_fname
@@ -24,16 +24,15 @@ class RemoteHDFS(RemoteBASE):
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")
url = config.get(Config.SECTION_REMOTE_URL, "/")
self.path_info = self.path_cls(url)
self.user = self.path_info.user
if not self.user:
self.user = config.get(
Config.SECTION_REMOTE_USER, getpass.getuser()
)
self.path_info = PathHDFS(user=self.user)
def hadoop_fs(self, cmd, user=None):
cmd = "hadoop fs -" + cmd
if user:
@@ -72,7 +71,7 @@ class RemoteHDFS(RemoteBASE):
)
return self._group(regex, stdout, "checksum")
def copy(self, from_info, to_info):
def copy(self, from_info, to_info, **_kwargs):
dname = posixpath.dirname(to_info.path)
self.hadoop_fs("mkdir -p {}".format(dname), user=to_info.user)
self.hadoop_fs(
@@ -103,73 +102,35 @@ class RemoteHDFS(RemoteBASE):
except RemoteCmdError:
return False
def upload(self, from_infos, to_infos, names=None, no_progress_bar=False):
names = self._verify_path_args(to_infos, from_infos, names)
def _upload(self, from_file, to_info, **_kwargs):
self.hadoop_fs(
"mkdir -p {}".format(to_info.parent.url), user=to_info.user
)
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info.scheme != "hdfs":
raise NotImplementedError
tmp_file = tmp_fname(to_info.url)
if from_info.scheme != "local":
raise NotImplementedError
self.hadoop_fs(
"copyFromLocal {} {}".format(from_file, tmp_file),
user=to_info.user,
)
self.hadoop_fs(
"mkdir -p {}".format(posixpath.dirname(to_info.path)),
user=to_info.user,
)
self.hadoop_fs(
"mv {} {}".format(tmp_file, to_info.url), user=to_info.user
)
tmp_file = tmp_fname(to_info.path)
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":
raise NotImplementedError
if to_info.scheme == "hdfs":
self.copy(from_info, to_info)
continue
if to_info.scheme != "local":
raise NotImplementedError
dname = os.path.dirname(to_info.path)
if not os.path.exists(dname):
os.makedirs(dname)
tmp_file = tmp_fname(to_info.path)
self.hadoop_fs(
"copyToLocal {} {}".format(from_info.path, tmp_file),
user=from_info.user,
)
os.rename(tmp_file, to_info.path)
def _download(self, from_info, to_file, **_kwargs):
self.hadoop_fs(
"copyToLocal {} {}".format(from_info.url, to_file),
user=from_info.user,
)
def list_cache_paths(self):
try:
self.hadoop_fs("test -e {}".format(self.prefix))
self.hadoop_fs("test -e {}".format(self.path_info.url))
except RemoteCmdError:
return []
stdout = self.hadoop_fs("ls -R {}".format(self.prefix))
stdout = self.hadoop_fs("ls -R {}".format(self.path_info.url))
lines = stdout.split("\n")
flist = []
for line in lines:
+23 -28
View File
@@ -1,8 +1,8 @@
from __future__ import unicode_literals
from dvc.scheme import Schemes
from dvc.path import Path
from dvc.utils.compat import open, makedirs
from dvc.utils.compat import open, makedirs, fspath_py35
import os
import threading
@@ -34,31 +34,26 @@ class ProgressBarCallback(object):
class RemoteHTTP(RemoteBASE):
scheme = Schemes.HTTP
REGEX = r"^http://.*$"
REQUEST_TIMEOUT = 10
CHUNK_SIZE = 2 ** 16
PARAM_CHECKSUM = "etag"
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 = Path(self.scheme)
@property
def prefix(self):
return self.cache_dir
url = config.get(Config.SECTION_REMOTE_URL)
self.path_info = self.path_cls(url) if url else None
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
no_progress_bar=False,
resume=False,
):
names = self._verify_path_args(to_infos, from_infos, names)
fails = 0
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info.scheme != self.scheme:
@@ -67,17 +62,15 @@ class RemoteHTTP(RemoteBASE):
if to_info.scheme != "local":
raise NotImplementedError
msg = "Downloading '{}' to '{}'".format(
from_info.path, to_info.path
)
msg = "Downloading '{}' to '{}'".format(from_info, to_info)
logger.debug(msg)
if not name:
name = os.path.basename(to_info.path)
name = to_info.name
makedirs(os.path.dirname(to_info.path), exist_ok=True)
makedirs(fspath_py35(to_info.parent), exist_ok=True)
total = self._content_length(from_info.path)
total = self._content_length(from_info.url)
if no_progress_bar or not total:
cb = None
@@ -86,35 +79,37 @@ class RemoteHTTP(RemoteBASE):
try:
self._download_to(
from_info.path, to_info.path, callback=cb, resume=resume
from_info.url, to_info.fspath, callback=cb, resume=resume
)
except Exception:
msg = "failed to download '{}'".format(from_info.path)
fails += 1
msg = "failed to download '{}'".format(from_info)
logger.exception(msg)
continue
if not no_progress_bar:
progress.finish_target(name)
return fails
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info.scheme == self.scheme
return bool(self._request("HEAD", path_info.path))
return bool(self._request("HEAD", path_info.url))
def cache_exists(self, md5s):
assert isinstance(md5s, list)
def batch_exists(self, path_infos, callback):
results = []
def func(md5):
return bool(self._request("HEAD", self.checksum_to_path(md5)))
for path_info in path_infos:
results.append(self.exists(path_info))
callback.update(str(path_info))
return list(filter(func, md5s))
return results
def _content_length(self, url):
return self._request("HEAD", url).headers.get("Content-Length")
def get_file_checksum(self, path_info):
url = path_info.path
url = path_info.url
etag = self._request("HEAD", url).headers.get("ETag") or self._request(
"HEAD", url
).headers.get("Content-MD5")
-1
View File
@@ -7,4 +7,3 @@ from .http import RemoteHTTP
class RemoteHTTPS(RemoteHTTP):
scheme = Schemes.HTTPS
REGEX = r"^https://.*$"
+206 -233
View File
@@ -3,17 +3,13 @@ from __future__ import unicode_literals
from copy import copy
from dvc.scheme import Schemes
from dvc.path.base import PathBASE
from dvc.path.local import PathLOCAL
from dvc.remote.local.slow_link_detection import slow_link_guard
from dvc.utils.compat import str, makedirs
from dvc.utils.compat import str, makedirs, fspath_py35
import os
import stat
import uuid
import ntpath
from shortuuid import uuid
import shutil
import posixpath
import logging
from dvc.system import System
@@ -32,24 +28,26 @@ from dvc.utils import (
tmp_fname,
file_md5,
walk_files,
relpath,
)
from dvc.config import Config
from dvc.exceptions import DvcException
from dvc.progress import progress
from concurrent.futures import ThreadPoolExecutor
from dvc.utils.fs import get_mtime_and_size, get_inode
from dvc.path_info import PathInfo
logger = logging.getLogger(__name__)
class RemoteLOCAL(RemoteBASE):
scheme = Schemes.LOCAL
REGEX = r"^(?P<path>.*)$"
path_cls = PathInfo
PARAM_CHECKSUM = "md5"
PARAM_PATH = "path"
UNPACKED_DIR_SUFFIX = ".unpacked"
DEFAULT_CACHE_TYPES = ["reflink", "copy"]
CACHE_TYPE_MAP = {
"copy": shutil.copyfile,
@@ -62,12 +60,6 @@ class RemoteLOCAL(RemoteBASE):
super(RemoteLOCAL, self).__init__(repo, config)
self.state = self.repo.state if self.repo else None
self.protected = config.get(Config.SECTION_CACHE_PROTECTED, False)
storagepath = config.get(Config.SECTION_AWS_STORAGEPATH, None)
self.cache_dir = config.get(Config.SECTION_REMOTE_URL, storagepath)
if self.cache_dir is not None and not os.path.isabs(self.cache_dir):
cwd = config[Config.PRIVATE_CWD]
self.cache_dir = os.path.abspath(os.path.join(cwd, self.cache_dir))
types = config.get(Config.SECTION_CACHE_TYPE, None)
if types:
@@ -77,36 +69,46 @@ class RemoteLOCAL(RemoteBASE):
else:
self.cache_types = copy(self.DEFAULT_CACHE_TYPES)
if self.cache_dir is not None and not os.path.exists(self.cache_dir):
os.mkdir(self.cache_dir)
# A clunky way to detect cache dir
storagepath = config.get(Config.SECTION_LOCAL_STORAGEPATH, None)
cache_dir = config.get(Config.SECTION_REMOTE_URL, storagepath)
if cache_dir is not None and not os.path.isabs(cache_dir):
cwd = config[Config.PRIVATE_CWD]
cache_dir = os.path.abspath(os.path.join(cwd, cache_dir))
if cache_dir is not None and not os.path.exists(cache_dir):
os.mkdir(cache_dir)
self.path_info = PathInfo(cache_dir) if cache_dir else None
self._dir_info = {}
self.path_info = PathLOCAL()
@property
def cache_dir(self):
return self.path_info.fspath if self.path_info else None
@classmethod
def supported(cls, config):
return True
@staticmethod
def compat_config(config):
ret = config.copy()
url = ret.pop(Config.SECTION_AWS_STORAGEPATH, "")
url = ret.pop(Config.SECTION_LOCAL_STORAGEPATH, "")
ret[Config.SECTION_REMOTE_URL] = url
return ret
@property
def url(self):
return self.cache_dir
@property
def prefix(self):
return self.cache_dir
def list_cache_paths(self):
clist = []
for entry in os.listdir(self.cache_dir):
subdir = os.path.join(self.cache_dir, entry)
if not os.path.isdir(subdir):
continue
assert self.path_info is not None
for cache in os.listdir(subdir):
clist.append(os.path.join(subdir, cache))
clist = []
for entry in os.listdir(fspath_py35(self.path_info)):
subdir = self.path_info / entry
if not os.path.isdir(fspath_py35(subdir)):
continue
clist.extend(
subdir / cache for cache in os.listdir(fspath_py35(subdir))
)
return clist
@@ -114,68 +116,84 @@ class RemoteLOCAL(RemoteBASE):
if not md5:
return None
return self.checksum_to_path(md5)
return self.checksum_to_path_info(md5).url
def exists(self, path_info):
assert isinstance(path_info, PathBASE)
assert path_info.scheme == "local"
return os.path.lexists(path_info.path)
return os.path.lexists(fspath_py35(path_info))
def makedirs(self, path_info):
if not self.exists(path_info):
os.makedirs(path_info.path)
os.makedirs(fspath_py35(path_info))
@slow_link_guard
def link(self, cache_info, path_info):
cache = cache_info.path
path = path_info.path
def link(self, from_info, to_info, link_type=None):
from_path = from_info.fspath
to_path = to_info.fspath
assert os.path.isfile(cache)
assert os.path.isfile(from_path)
dname = os.path.dirname(path)
dname = os.path.dirname(to_path)
if not os.path.exists(dname):
os.makedirs(dname)
# NOTE: just create an empty file for an empty cache
if os.path.getsize(cache) == 0:
open(path, "w+").close()
if os.path.getsize(from_path) == 0:
open(to_path, "w+").close()
msg = "Created empty file: {} -> {}".format(cache, path)
msg = "Created empty file: {} -> {}".format(from_path, to_path)
logger.debug(msg)
return
i = len(self.cache_types)
if not link_type:
link_types = self.cache_types
else:
link_types = [link_type]
self._try_links(from_info, to_info, link_types)
@classmethod
def _get_link_method(cls, link_type):
try:
return cls.CACHE_TYPE_MAP[link_type]
except KeyError:
raise DvcException(
"Cache type: '{}' not supported!".format(link_type)
)
def _link(self, from_info, to_info, link_method):
if self.exists(to_info):
raise DvcException("Link '{}' already exists!".format(to_info))
else:
link_method(from_info.fspath, to_info.fspath)
if self.protected:
self.protect(to_info)
msg = "Created {}'{}': {} -> {}".format(
"protected " if self.protected else "",
self.cache_types[0],
from_info,
to_info,
)
logger.debug(msg)
@slow_link_guard
def _try_links(self, from_info, to_info, link_types):
i = len(link_types)
while i > 0:
link_method = self._get_link_method(link_types[0])
try:
self.CACHE_TYPE_MAP[self.cache_types[0]](cache, path)
if self.protected:
self.protect(path_info)
msg = "Created {}'{}': {} -> {}".format(
"protected " if self.protected else "",
self.cache_types[0],
cache,
path,
)
logger.debug(msg)
self._link(from_info, to_info, link_method)
return
except DvcException as exc:
msg = "Cache type '{}' is not supported: {}"
logger.debug(msg.format(self.cache_types[0], str(exc)))
del self.cache_types[0]
logger.debug(msg.format(link_types[0], str(exc)))
del link_types[0]
i -= 1
raise DvcException("no possible cache types left to try out.")
@property
def ospath(self):
if os.name == "nt":
return ntpath
return posixpath
def already_cached(self, path_info):
assert path_info.scheme in ["", "local"]
@@ -187,7 +205,7 @@ class RemoteLOCAL(RemoteBASE):
return not self.changed_cache(current_md5)
def is_empty(self, path_info):
path = path_info.path
path = path_info.fspath
if self.isfile(path_info) and os.path.getsize(path) == 0:
return True
@@ -198,119 +216,65 @@ class RemoteLOCAL(RemoteBASE):
return False
def isfile(self, path_info):
return os.path.isfile(path_info.path)
return os.path.isfile(fspath_py35(path_info))
def isdir(self, path_info):
return os.path.isdir(path_info.path)
return os.path.isdir(fspath_py35(path_info))
def walk(self, path_info):
return os.walk(path_info.path)
return os.walk(fspath_py35(path_info))
def get_file_checksum(self, path_info):
return file_md5(path_info.path)[0]
return file_md5(fspath_py35(path_info))[0]
def remove(self, path_info):
if path_info.scheme != "local":
raise NotImplementedError
remove(path_info.path)
if self.exists(path_info):
remove(path_info.fspath)
def move(self, from_info, to_info):
if from_info.scheme != "local" or to_info.scheme != "local":
raise NotImplementedError
inp = from_info.path
outp = to_info.path
inp = from_info.fspath
outp = to_info.fspath
# moving in two stages to make the whole operation atomic in
# case inp and outp are in different filesystems and actual
# physical copying of data is happening
tmp = "{}.{}".format(outp, str(uuid.uuid4()))
tmp = "{}.{}".format(outp, str(uuid()))
move(inp, tmp)
move(tmp, outp)
def cache_exists(self, md5s):
assert isinstance(md5s, list)
return list(filter(lambda md5: not self.changed_cache_file(md5), md5s))
return [
checksum
for checksum in progress(md5s)
if not self.changed_cache_file(checksum)
]
def upload(self, from_infos, to_infos, names=None, no_progress_bar=False):
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 != "local":
raise NotImplementedError
if from_info.scheme != "local":
raise NotImplementedError
logger.debug(
"Uploading '{}' to '{}'".format(from_info.path, to_info.path)
)
if not name:
name = os.path.basename(from_info.path)
makedirs(os.path.dirname(to_info.path), exist_ok=True)
tmp_file = tmp_fname(to_info.path)
try:
copyfile(
from_info.path,
tmp_file,
name=name,
no_progress_bar=no_progress_bar,
)
os.rename(tmp_file, to_info.path)
except Exception:
logger.exception(
"failed to upload '{}' to '{}'".format(
from_info.path, to_info.path
)
)
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
resume=False,
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
names = self._verify_path_args(from_infos, to_infos, names)
makedirs(fspath_py35(to_info.parent), exist_ok=True)
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info.scheme != "local":
raise NotImplementedError
tmp_file = tmp_fname(to_info)
copyfile(
from_file, tmp_file, name=name, no_progress_bar=no_progress_bar
)
os.rename(tmp_file, fspath_py35(to_info))
if to_info.scheme != "local":
raise NotImplementedError
logger.debug(
"Downloading '{}' to '{}'".format(from_info.path, to_info.path)
)
if not name:
name = os.path.basename(to_info.path)
makedirs(os.path.dirname(to_info.path), exist_ok=True)
tmp_file = tmp_fname(to_info.path)
try:
copyfile(
from_info.path,
tmp_file,
no_progress_bar=no_progress_bar,
name=name,
)
move(tmp_file, to_info.path)
except Exception:
logger.exception(
"failed to download '{}' to '{}'".format(
from_info.path, to_info.path
)
)
continue
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
):
copyfile(
fspath_py35(from_info),
to_file,
no_progress_bar=no_progress_bar,
name=name,
)
def _group(self, checksum_infos, show_checksums=False):
by_md5 = {}
@@ -322,7 +286,7 @@ class RemoteLOCAL(RemoteBASE):
by_md5[md5] = {"name": md5}
continue
name = info[self.PARAM_PATH]
name = info["name"]
branch = info.get("branch")
if branch:
name += "({})".format(branch)
@@ -342,25 +306,15 @@ class RemoteLOCAL(RemoteBASE):
show_checksums=False,
download=False,
):
logger.info("Preparing to collect status from {}".format(remote.url))
title = "Collecting information"
ret = {}
progress.set_n_total(1)
progress.update_target(title, 0, 100)
progress.update_target(title, 10, 100)
ret = self._group(checksum_infos, show_checksums=show_checksums)
md5s = list(ret.keys())
progress.update_target(title, 30, 100)
logger.info(
"Preparing to collect status from {}".format(remote.path_info)
)
ret = self._group(checksum_infos, show_checksums=show_checksums) or {}
md5s = list(ret)
logger.info("Collecting information from local cache...")
local_exists = self.cache_exists(md5s)
progress.update_target(title, 40, 100)
# This is a performance optimization. We can safely assume that,
# if the resources that we want to fetch are already cached,
# there's no need to check the remote storage for the existance of
@@ -368,12 +322,9 @@ class RemoteLOCAL(RemoteBASE):
if download and sorted(local_exists) == sorted(md5s):
remote_exists = local_exists
else:
logger.info("Collecting information from remote cache...")
remote_exists = list(remote.cache_exists(md5s))
progress.update_target(title, 90, 100)
progress.finish_target(title)
self._fill_statuses(ret, local_exists, remote_exists)
self._log_missing_caches(ret)
@@ -390,26 +341,16 @@ class RemoteLOCAL(RemoteBASE):
info["status"] = status
def _get_chunks(self, download, remote, status_info, status, jobs):
title = "Analysing status."
progress.set_n_total(1)
total = len(status_info)
current = 0
cache = []
path_infos = []
names = []
for md5, info in status_info.items():
for md5, info in progress(
status_info.items(), name="Analysing status"
):
if info["status"] == status:
cache.append(self.checksum_to_path_info(md5))
path_infos.append(remote.checksum_to_path_info(md5))
names.append(info["name"])
current += 1
progress.update_target(title, current, total)
progress.finish_target(title)
progress.set_n_total(len(names))
if download:
to_infos = cache
@@ -418,12 +359,10 @@ class RemoteLOCAL(RemoteBASE):
to_infos = path_infos
from_infos = cache
return list(
zip(
to_chunks(from_infos, jobs),
to_chunks(to_infos, jobs),
to_chunks(names, jobs),
)
return (
to_chunks(from_infos, jobs),
to_chunks(to_infos, jobs),
to_chunks(names, jobs),
)
def _process(
@@ -434,12 +373,10 @@ class RemoteLOCAL(RemoteBASE):
show_checksums=False,
download=False,
):
msg = "Preparing to {} data {} '{}'"
logger.info(
msg.format(
"download" if download else "upload",
"from" if download else "to",
remote.url,
"Preparing to {} '{}'".format(
"download data from" if download else "upload data to",
remote.path_info,
)
)
@@ -463,19 +400,22 @@ class RemoteLOCAL(RemoteBASE):
chunks = self._get_chunks(download, remote, status_info, status, jobs)
if len(chunks) == 0:
if len(chunks[0]) == 0:
return 0
futures = []
with ThreadPoolExecutor(max_workers=jobs) as executor:
for from_infos, to_infos, names in chunks:
res = executor.submit(func, from_infos, to_infos, names=names)
futures.append(res)
if jobs > 1:
with ThreadPoolExecutor(max_workers=jobs) as executor:
fails = sum(executor.map(func, *chunks))
else:
fails = sum(map(func, *chunks))
for f in futures:
f.result()
if fails:
msg = "{} file(s) failed to {}"
raise DvcException(
msg.format(fails, "download" if download else "upload")
)
return len(chunks)
return len(chunks[0])
def push(self, checksum_infos, remote, jobs=None, show_checksums=False):
return self._process(
@@ -495,22 +435,6 @@ class RemoteLOCAL(RemoteBASE):
download=True,
)
def _changed_dir_cache(self, checksum):
mtime, size = get_mtime_and_size(self.cache_dir)
inode = get_inode(self.cache_dir)
existing_record = self.state.get_state_record_for_inode(inode)
if existing_record:
cached_mtime, cached_size, _, _ = existing_record
changed = not (mtime == cached_mtime and size == cached_size)
else:
changed = True
if not changed:
return False
return super(RemoteLOCAL, self)._changed_dir_cache(checksum)
def _log_missing_caches(self, checksum_info_dict):
missing_caches = [
(md5, info)
@@ -534,18 +458,14 @@ class RemoteLOCAL(RemoteBASE):
def _unprotect_file(path):
if System.is_symlink(path) or System.is_hardlink(path):
logger.debug("Unprotecting '{}'".format(path))
tmp = os.path.join(os.path.dirname(path), "." + str(uuid.uuid4()))
tmp = os.path.join(os.path.dirname(path), "." + str(uuid()))
# The operations order is important here - if some application
# would access the file during the process of copyfile then it
# would get only the part of file. So, at first, the file should be
# copied with the temporary name, and then original file should be
# replaced by new.
copyfile(
path,
tmp,
name="Unprotecting '{}'".format(os.path.relpath(path)),
)
copyfile(path, tmp, name="Unprotecting '{}'".format(relpath(path)))
remove(path)
os.rename(tmp, path)
@@ -564,7 +484,7 @@ class RemoteLOCAL(RemoteBASE):
@staticmethod
def unprotect(path_info):
path = path_info.path
path = path_info.fspath
if not os.path.exists(path):
raise DvcException(
"can't unprotect non-existing data '{}'".format(path)
@@ -577,4 +497,57 @@ class RemoteLOCAL(RemoteBASE):
@staticmethod
def protect(path_info):
os.chmod(path_info.path, stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH)
os.chmod(
fspath_py35(path_info), stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
)
def _get_unpacked_dir_path_info(self, checksum):
info = self.checksum_to_path_info(checksum)
return info.with_name(info.name + self.UNPACKED_DIR_SUFFIX)
def _path_info_changed(self, path_info):
if self.exists(path_info) and self.state.get(path_info):
return False
return True
def _update_unpacked_dir(self, checksum):
unpacked_dir_info = self._get_unpacked_dir_path_info(checksum)
if not self._path_info_changed(unpacked_dir_info):
return
self.remove(unpacked_dir_info)
try:
dir_info = self.get_dir_cache(checksum)
self._create_unpacked_dir(checksum, dir_info, unpacked_dir_info)
except DvcException:
logger.warning("Could not create '{}'".format(unpacked_dir_info))
self.remove(unpacked_dir_info)
def _create_unpacked_dir(self, checksum, dir_info, unpacked_dir_info):
self.makedirs(unpacked_dir_info)
for entry in progress(dir_info, name="Created unpacked dir"):
entry_cache_info = self.checksum_to_path_info(
entry[self.PARAM_CHECKSUM]
)
relative_path = entry[self.PARAM_RELPATH]
self.link(
entry_cache_info, unpacked_dir_info / relative_path, "hardlink"
)
self.state.save(unpacked_dir_info, checksum)
def _changed_unpacked_dir(self, checksum):
status_unpacked_dir_info = self._get_unpacked_dir_path_info(checksum)
return not self.state.get(status_unpacked_dir_info)
def _get_unpacked_dir_names(self, checksums):
unpacked = set()
for c in checksums:
if self.is_dir_checksum(c):
unpacked.add(c + self.UNPACKED_DIR_SUFFIX)
return unpacked
+6 -7
View File
@@ -31,13 +31,12 @@ class SlowLinkDetectorDecorator(object):
and self.should_display()
):
msg = (
"You can cut execution time considerably. Check:\n"
"You can cut execution time considerably by using a different"
" `cache.type` configuration.\nSee "
"{blue}https://dvc.org/doc/commands-reference/config#cache{"
"reset}"
"\nfor "
"more information.\nTo disable this message, run:\n'dvc "
"config "
"cache.slow_link_warning False'".format(
"reset} for more information.\n"
"To disable this message, run:\n"
"'dvc config cache.slow_link_warning false'".format(
blue=colorama.Fore.BLUE, reset=colorama.Fore.RESET
)
)
@@ -49,7 +48,7 @@ class SlowLinkDetectorDecorator(object):
def slow_link_guard(method):
def call(remote_local, *args, **kwargs):
cache_config = remote_local.repo.config.config.get(
Config.SECTION_CACHE
Config.SECTION_CACHE, {}
)
should_warn = cache_config.get(
Config.SECTION_CACHE_SLOW_LINK_WARNING, True
+23 -94
View File
@@ -5,19 +5,16 @@ import os
import logging
from dvc.scheme import Schemes
from dvc.path.oss import PathOSS
try:
import oss2
except ImportError:
oss2 = None
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.azure import Callback
from dvc.path_info import CloudURLInfo
logger = logging.getLogger(__name__)
@@ -43,7 +40,7 @@ class RemoteOSS(RemoteBASE):
"""
scheme = Schemes.OSS
REGEX = r"^oss://(?P<path>.*)?$"
path_cls = CloudURLInfo
REQUIRES = {"oss2": oss2}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
@@ -51,10 +48,8 @@ class RemoteOSS(RemoteBASE):
def __init__(self, repo, config):
super(RemoteOSS, self).__init__(repo, config)
self.url = config.get(Config.SECTION_REMOTE_URL)
parsed = urlparse(self.url)
self.bucket = parsed.netloc
self.prefix = parsed.path.lstrip("/")
url = config.get(Config.SECTION_REMOTE_URL)
self.path_info = self.path_cls(url) if url else None
self.endpoint = config.get(Config.SECTION_OSS_ENDPOINT) or os.getenv(
"OSS_ENDPOINT"
@@ -73,17 +68,17 @@ class RemoteOSS(RemoteBASE):
)
self._bucket = None
self.path_info = PathOSS(bucket=self.bucket)
@property
def oss_service(self):
if self._bucket is None:
logger.debug("URL {}".format(self.url))
logger.debug("URL {}".format(self.path_info))
logger.debug("key id {}".format(self.key_id))
logger.debug("key secret {}".format(self.key_secret))
auth = oss2.Auth(self.key_id, self.key_secret)
logger.debug("bucket name {}".format(self.bucket))
self._bucket = oss2.Bucket(auth, self.endpoint, self.bucket)
self._bucket = oss2.Bucket(
auth, self.endpoint, self.path_info.bucket
)
try: # verify that bucket exists
self._bucket.get_bucket_info()
except oss2.exceptions.NoSuchBucket:
@@ -99,10 +94,7 @@ class RemoteOSS(RemoteBASE):
if path_info.scheme != self.scheme:
raise NotImplementedError
logger.debug(
"Removing oss://{}/{}".format(path_info.bucket, path_info.path)
)
logger.debug("Removing oss://{}".format(path_info))
self.oss_service.delete_object(path_info.path)
def _list_paths(self, prefix):
@@ -110,83 +102,20 @@ class RemoteOSS(RemoteBASE):
yield blob.key
def list_cache_paths(self):
return self._list_paths(self.prefix)
return self._list_paths(self.path_info.path)
def upload(self, from_infos, to_infos, names=None, no_progress_bar=False):
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:
raise NotImplementedError
if from_info.scheme != "local":
raise NotImplementedError
bucket = to_info.bucket
path = to_info.path
logger.debug(
"Uploading '{}' to 'oss://{}/{}'".format(
from_info.path, bucket, path
)
)
if not name:
name = os.path.basename(from_info.path)
cb = None if no_progress_bar else Callback(name)
try:
self.oss_service.put_object_from_file(
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,
names=None,
no_progress_bar=False,
resume=False,
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
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:
raise NotImplementedError
if to_info.scheme != "local":
raise NotImplementedError
cb = None if no_progress_bar else Callback(name)
self.oss_service.put_object_from_file(
to_info.path, from_file, progress_callback=cb
)
bucket = from_info.bucket
path = from_info.path
logger.debug(
"Downloading 'oss://{}/{}' to '{}'".format(
bucket, path, to_info.path
)
)
tmp_file = tmp_fname(to_info.path)
if not name:
name = os.path.basename(to_info.path)
cb = None if no_progress_bar else Callback(name)
makedirs(os.path.dirname(to_info.path), exist_ok=True)
try:
self.oss_service.get_object_to_file(
path, tmp_file, progress_callback=cb
)
except Exception:
msg = "failed to download 'oss://{}/{}'".format(bucket, path)
logger.warning(msg)
else:
move(tmp_file, to_info.path)
if not no_progress_bar:
progress.finish_target(name)
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
):
cb = None if no_progress_bar else Callback(name)
self.oss_service.get_object_to_file(
from_info.path, to_file, progress_callback=cb
)
+69 -116
View File
@@ -3,21 +3,20 @@ from __future__ import unicode_literals
import os
import threading
import logging
from dvc.scheme import Schemes
from dvc.path.s3 import PathS3
import itertools
from contextlib import contextmanager
try:
import boto3
except ImportError:
boto3 = None
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.exceptions import DvcException, ETagMismatchError
from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
@@ -37,7 +36,7 @@ class Callback(object):
class RemoteS3(RemoteBASE):
scheme = Schemes.S3
REGEX = r"^s3://(?P<path>.*)$"
path_cls = CloudURLInfo
REQUIRES = {"boto3": boto3}
PARAM_CHECKSUM = "etag"
@@ -48,15 +47,12 @@ class RemoteS3(RemoteBASE):
config.get(Config.SECTION_AWS_STORAGEPATH, "").lstrip("/")
)
self.url = config.get(Config.SECTION_REMOTE_URL, storagepath)
url = config.get(Config.SECTION_REMOTE_URL, storagepath)
self.path_info = self.path_cls(url)
self.region = os.environ.get("AWS_DEFAULT_REGION") or config.get(
Config.SECTION_AWS_REGION
)
self.region = config.get(Config.SECTION_AWS_REGION)
self.profile = os.environ.get("AWS_PROFILE") or config.get(
Config.SECTION_AWS_PROFILE
)
self.profile = config.get(Config.SECTION_AWS_PROFILE)
self.endpoint_url = config.get(Config.SECTION_AWS_ENDPOINT_URL)
@@ -64,16 +60,16 @@ class RemoteS3(RemoteBASE):
self.use_ssl = config.get(Config.SECTION_AWS_USE_SSL, True)
self.extra_args = {}
self.sse = config.get(Config.SECTION_AWS_SSE, "")
if self.sse:
self.extra_args["ServerSideEncryption"] = self.sse
shared_creds = config.get(Config.SECTION_AWS_CREDENTIALPATH)
if 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 = PathS3(bucket=self.bucket)
@staticmethod
def compat_config(config):
ret = config.copy()
@@ -112,9 +108,11 @@ class RemoteS3(RemoteBASE):
return obj
@classmethod
def _copy_multipart(cls, s3, from_info, to_info, size, n_parts):
def _copy_multipart(
cls, s3, from_info, to_info, size, n_parts, extra_args
):
mpu = s3.create_multipart_upload(
Bucket=to_info.bucket, Key=to_info.path
Bucket=to_info.bucket, Key=to_info.path, **extra_args
)
mpu_id = mpu["UploadId"]
@@ -154,7 +152,7 @@ class RemoteS3(RemoteBASE):
)
@classmethod
def _copy(cls, s3, from_info, to_info):
def _copy(cls, s3, from_info, to_info, extra_args):
# NOTE: object's etag depends on the way it was uploaded to s3 or the
# way it was copied within the s3. More specifically, it depends on
# the chunk size that was used to transfer it, which would affect
@@ -182,32 +180,30 @@ class RemoteS3(RemoteBASE):
_, _, parts_suffix = etag.partition("-")
if parts_suffix:
n_parts = int(parts_suffix)
cls._copy_multipart(s3, from_info, to_info, size, n_parts)
cls._copy_multipart(
s3, from_info, to_info, size, n_parts, extra_args=extra_args
)
else:
source = {"Bucket": from_info.bucket, "Key": from_info.path}
s3.copy(source, to_info.bucket, to_info.path)
s3.copy(source, to_info.bucket, to_info.path, ExtraArgs=extra_args)
cached_etag = cls.get_etag(s3, to_info.bucket, to_info.path)
if etag != cached_etag:
raise ETagMismatchError(etag, cached_etag)
def copy(self, from_info, to_info, s3=None):
s3 = s3 if s3 else self.s3
self._copy(s3, from_info, to_info)
def copy(self, from_info, to_info, ctx=None):
self._copy(ctx or self.s3, from_info, to_info, self.extra_args)
def remove(self, path_info):
if path_info.scheme != "s3":
raise NotImplementedError
logger.debug(
"Removing s3://{}/{}".format(path_info.bucket, path_info.path)
)
logger.debug("Removing {}".format(path_info))
self.s3.delete_object(Bucket=path_info.bucket, Key=path_info.path)
def _list_paths(self, bucket, prefix):
def _list_paths(self, bucket, prefix, s3=None):
""" Read config for list object api, paginate through list objects."""
s3 = self.s3
s3 = s3 or self.s3
kwargs = {"Bucket": bucket, "Prefix": prefix}
if self.list_objects:
list_objects_api = "list_objects"
@@ -222,104 +218,61 @@ class RemoteS3(RemoteBASE):
yield item["Key"]
def list_cache_paths(self):
return self._list_paths(self.bucket, self.prefix)
return self._list_paths(self.path_info.bucket, self.path_info.path)
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info.scheme == "s3"
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, no_progress_bar=False):
names = self._verify_path_args(to_infos, from_infos, names)
def batch_exists(self, path_infos, callback):
paths = []
s3 = self.s3
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info.scheme != "s3":
raise NotImplementedError
if from_info.scheme != "local":
raise NotImplementedError
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info.path, to_info.bucket, to_info.path
)
for path_info in path_infos:
paths.append(
self._list_paths(path_info.bucket, path_info.path, s3)
)
callback.update(str(path_info))
if not name:
name = os.path.basename(from_info.path)
paths = set(itertools.chain.from_iterable(paths))
return [path_info.path in paths for path_info in path_infos]
total = os.path.getsize(from_info.path)
cb = None if no_progress_bar else Callback(name, total)
@contextmanager
def transfer_context(self):
yield self.s3
try:
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.exception(msg)
continue
def _upload(
self, from_file, to_info, name=None, ctx=None, no_progress_bar=False
):
total = os.path.getsize(from_file)
cb = None if no_progress_bar else Callback(name, total)
ctx.upload_file(
from_file,
to_info.bucket,
to_info.path,
Callback=cb,
ExtraArgs=self.extra_args,
)
progress.finish_target(name)
def download(
def _download(
self,
from_infos,
to_infos,
from_info,
to_file,
name=None,
ctx=None,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(from_infos, to_infos, names)
s3 = ctx
s3 = self.s3
if no_progress_bar:
cb = None
else:
total = s3.head_object(
Bucket=from_info.bucket, Key=from_info.path
)["ContentLength"]
cb = Callback(name, total)
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info.scheme != "s3":
raise NotImplementedError
if to_info.scheme == "s3":
self.copy(from_info, to_info, s3=s3)
continue
if to_info.scheme != "local":
raise NotImplementedError
msg = "Downloading '{}/{}' to '{}'".format(
from_info.bucket, from_info.path, to_info.path
)
logger.debug(msg)
tmp_file = tmp_fname(to_info.path)
if not name:
name = os.path.basename(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.path
)["ContentLength"]
cb = Callback(name, total)
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.exception(msg)
continue
move(tmp_file, to_info.path)
if not no_progress_bar:
progress.finish_target(name)
s3.download_file(
from_info.bucket, from_info.path, to_file, Callback=cb
)
+98 -107
View File
@@ -3,9 +3,9 @@ from __future__ import unicode_literals
import os
import getpass
import logging
from dvc.path import Schemes
from dvc.path.ssh import PathSSH
import itertools
import errno
from concurrent.futures import ThreadPoolExecutor
try:
import paramiko
@@ -15,8 +15,10 @@ except ImportError:
import dvc.prompt as prompt
from dvc.remote.ssh.connection import SSHConnection
from dvc.config import Config
from dvc.utils.compat import urlparse
from dvc.utils import to_chunks
from dvc.utils.compat import urlparse, StringIO
from dvc.remote.base import RemoteBASE
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
@@ -24,11 +26,6 @@ logger = logging.getLogger(__name__)
class RemoteSSH(RemoteBASE):
scheme = Schemes.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
@@ -38,27 +35,36 @@ class RemoteSSH(RemoteBASE):
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
url = config.get(Config.SECTION_REMOTE_URL)
if url:
parsed = urlparse(url)
user_ssh_config = self._load_user_ssh_config(parsed.hostname)
user_ssh_config = self._load_user_ssh_config(self.host)
host = user_ssh_config.get("hostname", parsed.hostname)
user = (
config.get(Config.SECTION_REMOTE_USER)
or parsed.username
or user_ssh_config.get("user")
or getpass.getuser()
)
port = (
config.get(Config.SECTION_REMOTE_PORT)
or parsed.port
or self._try_get_ssh_config_port(user_ssh_config)
or self.DEFAULT_PORT
)
self.path_info = self.path_cls.from_parts(
scheme=self.scheme,
host=host,
user=user,
port=port,
path=parsed.path,
)
else:
self.path_info = None
user_ssh_config = {}
self.host = user_ssh_config.get("hostname", self.host)
self.user = (
config.get(Config.SECTION_REMOTE_USER)
or parsed.username
or user_ssh_config.get("user")
or getpass.getuser()
)
self.prefix = parsed.path or "/"
self.port = (
config.get(Config.SECTION_REMOTE_PORT)
or parsed.port
or self._try_get_ssh_config_port(user_ssh_config)
or self.DEFAULT_PORT
)
self.keyfile = config.get(
Config.SECTION_REMOTE_KEY_FILE
) or self._try_get_ssh_config_keyfile(user_ssh_config)
@@ -68,10 +74,6 @@ class RemoteSSH(RemoteBASE):
Config.SECTION_REMOTE_ASK_PASSWORD, False
)
self.path_info = PathSSH(
host=self.host, user=self.user, port=self.port
)
@staticmethod
def ssh_config_filename():
return os.path.expanduser(os.path.join("~", ".ssh", "config"))
@@ -81,10 +83,12 @@ class RemoteSSH(RemoteBASE):
user_config_file = RemoteSSH.ssh_config_filename()
user_ssh_config = dict()
if hostname and os.path.exists(user_config_file):
ssh_config = paramiko.SSHConfig()
with open(user_config_file) as f:
ssh_config = paramiko.SSHConfig()
ssh_config.parse(f)
user_ssh_config = ssh_config.lookup(hostname)
# For whatever reason parsing directly from f is unreliable
f_copy = StringIO(f.read())
ssh_config.parse(f_copy)
user_ssh_config = ssh_config.lookup(hostname)
return user_ssh_config
@staticmethod
@@ -129,12 +133,37 @@ class RemoteSSH(RemoteBASE):
)
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.exists(path_info.path)
def batch_exists(self, path_infos, callback):
def _exists(chunk_and_channel):
chunk, channel = chunk_and_channel
ret = []
for path in chunk:
try:
channel.stat(path)
ret.append(True)
except IOError as exc:
if exc.errno != errno.ENOENT:
raise
ret.append(False)
callback.update(path)
return ret
with self.ssh(path_infos[0]) as ssh:
with ssh.open_max_sftp_channels() as channels:
max_workers = len(channels)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
paths = [path_info.path for path_info in path_infos]
chunks = to_chunks(paths, max_workers)
chunks_and_channels = zip(chunks, channels)
outcome = executor.map(_exists, chunks_and_channels)
results = list(itertools.chain.from_iterable(outcome))
return results
def get_file_checksum(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
@@ -146,12 +175,15 @@ class RemoteSSH(RemoteBASE):
with self.ssh(path_info) as ssh:
return ssh.isdir(path_info.path)
def copy(self, from_info, to_info):
def copy(self, from_info, to_info, ctx=None):
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)
if ctx:
ctx.cp(from_info.path, to_info.path)
else:
with self.ssh(from_info) as ssh:
ssh.cp(from_info.path, to_info.path)
def remove(self, path_info):
if path_info.scheme != self.scheme:
@@ -167,81 +199,40 @@ class RemoteSSH(RemoteBASE):
with self.ssh(from_info) as ssh:
ssh.move(from_info.path, to_info.path)
def download(
def transfer_context(self):
return self.ssh(self.path_info)
def _download(
self,
from_infos,
to_infos,
from_info,
to_file,
name=None,
ctx=None,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(from_infos, to_infos, names)
ssh = self.ssh(from_infos[0])
assert from_info.isin(self.path_info)
ctx.download(
from_info.path,
to_file,
progress_title=name,
no_progress_bar=no_progress_bar,
)
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,
no_progress_bar=no_progress_bar,
)
except Exception:
logger.exception(
"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, no_progress_bar=False):
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,
no_progress_bar=no_progress_bar,
)
except Exception:
logger.exception(
"failed to upload '{}' to '{}'".format(
from_info, to_info
)
)
def _upload(
self, from_file, to_info, name=None, ctx=None, no_progress_bar=False
):
assert to_info.isin(self.path_info)
ctx.upload(
from_file,
to_info.path,
progress_title=name,
no_progress_bar=no_progress_bar,
)
def list_cache_paths(self):
with self.ssh(self.path_info) as ssh:
return list(ssh.walk_files(self.prefix))
return list(ssh.walk_files(self.path_info.path))
def walk(self, path_info):
with self.ssh(path_info) as ssh:
+38 -50
View File
@@ -1,8 +1,8 @@
import errno
import os
import posixpath
import logging
from stat import S_ISDIR
import stat
from contextlib import contextmanager
try:
import paramiko
@@ -10,7 +10,7 @@ except ImportError:
paramiko = None
from dvc.utils import tmp_fname
from dvc.utils.compat import makedirs
from dvc.utils.compat import ignore_file_not_found
from dvc.progress import progress
from dvc.exceptions import DvcException
from dvc.remote.base import RemoteCmdError
@@ -73,40 +73,25 @@ class SSHConnection:
self._ssh.close()
def exists(self, path):
def st_mode(self, path):
self._sftp_connect()
try:
return self._sftp.stat(path)
except IOError:
return False
pass
with ignore_file_not_found():
return self._sftp.stat(path).st_mode
return 0
def exists(self, path, sftp=None):
return bool(self.st_mode(path))
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
return stat.S_ISDIR(self.st_mode(path))
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
return stat.S_ISREG(self.st_mode(path))
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
return stat.S_ISLNK(self.st_mode(path))
def makedirs(self, path):
self._sftp_connect()
@@ -149,7 +134,7 @@ class SSHConnection:
nondirs = []
for entry in dir_entries:
name = entry.filename
if S_ISDIR(entry.st_mode):
if stat.S_ISDIR(entry.st_mode):
dirs.append(name)
else:
nondirs.append(name)
@@ -171,11 +156,8 @@ class SSHConnection:
yield posixpath.join(root, fname)
def _remove_file(self, path):
try:
with ignore_file_not_found():
self._sftp.remove(path)
except IOError as exc:
if exc.errno != errno.ENOENT:
raise
def _remove_dir(self, path):
for root, dirs, files in self.walk(path, topdown=False):
@@ -186,11 +168,9 @@ class SSHConnection:
for dname in dirs:
path = posixpath.join(root, dname)
self._sftp.rmdir(dname)
try:
with ignore_file_not_found():
self._sftp.rmdir(path)
except IOError as exc:
if exc.errno != errno.ENOENT:
raise
def remove(self, path):
self._sftp_connect()
@@ -203,23 +183,15 @@ class SSHConnection:
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)
self._sftp.get(src, dest)
else:
if not progress_title:
progress_title = os.path.basename(dest)
progress_title = os.path.basename(src)
self._sftp.get(src, tmp_file, callback=create_cb(progress_title))
self._sftp.get(src, dest, callback=create_cb(progress_title))
progress.finish_target(progress_title)
if os.path.exists(dest):
os.remove(dest)
os.rename(tmp_file, dest)
def move(self, src, dst):
self.makedirs(posixpath.dirname(dst))
self._sftp_connect()
@@ -320,3 +292,19 @@ class SSHConnection:
def cp(self, src, dest):
self.makedirs(posixpath.dirname(dest))
self.execute("cp {} {}".format(src, dest))
@contextmanager
def open_max_sftp_channels(self):
try:
channels = []
while True:
try:
channels.append(self._ssh.open_sftp())
except paramiko.ssh_exception.ChannelException:
if not channels:
raise
break
yield channels
finally:
for channel in channels:
channel.close()
+82 -118
View File
@@ -3,16 +3,16 @@ from __future__ import unicode_literals
import os
import logging
import dvc.prompt as prompt
from dvc.config import Config
from dvc.exceptions import (
DvcException,
NotDvcRepoError,
OutputNotFoundError,
TargetNotDirectoryError,
)
from dvc.ignore import DvcIgnoreFileHandler
from dvc.path.local import PathLOCAL
from dvc.path_info import PathInfo
from dvc.utils.compat import open as _open
from dvc.utils import relpath
logger = logging.getLogger(__name__)
@@ -28,6 +28,7 @@ class Repo(object):
from dvc.repo.move import move
from dvc.repo.run import run
from dvc.repo.imp import imp
from dvc.repo.imp_url import imp_url
from dvc.repo.reproduce import reproduce
from dvc.repo.checkout import checkout
from dvc.repo.push import push
@@ -38,19 +39,18 @@ class Repo(object):
from dvc.repo.commit import commit
from dvc.repo.diff import diff
from dvc.repo.brancher import brancher
from dvc.repo.get import get
from dvc.repo.get_url import get_url
def __init__(self, root_dir=None):
from dvc.config import Config
def __init__(self, root_dir=None, rev=None):
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
from dvc.repo.tag import Tag
from dvc.repo.pkg import Pkg
root_dir = self.find_root(root_dir)
@@ -59,9 +59,13 @@ class Repo(object):
self.config = Config(self.dvc_dir)
self.tree = WorkingTree(self.root_dir)
self.scm = SCM(self.root_dir, repo=self)
if rev:
self.tree = self.scm.get_tree(rev)
else:
self.tree = WorkingTree(self.root_dir)
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.
@@ -74,29 +78,25 @@ class Repo(object):
logger.setLevel(level.upper())
self.cache = Cache(self)
self.cloud = DataCloud(self, config=self.config.config)
self.updater = Updater(self.dvc_dir)
self.cloud = DataCloud(self)
self.metrics = Metrics(self)
self.tag = Tag(self)
self.pkg = Pkg(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):
@classmethod
def find_root(cls, 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)
dvc_dir = os.path.join(root, cls.DVC_DIR)
if os.path.isdir(dvc_dir):
return root
if os.path.ismount(root):
@@ -104,10 +104,10 @@ class Repo(object):
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)
@classmethod
def find_dvc_dir(cls, root=None):
root_dir = cls.find_root(root)
return os.path.join(root_dir, cls.DVC_DIR)
@staticmethod
def init(root_dir=os.curdir, no_scm=False, force=False):
@@ -117,16 +117,21 @@ class Repo(object):
return Repo(root_dir)
def unprotect(self, target):
path_info = PathLOCAL(path=target)
return self.cache.local.unprotect(path_info)
return self.cache.local.unprotect(PathInfo(target))
def _ignore(self):
from dvc.updater import Updater
from dvc.external_repo import ExternalRepo
updater = Updater(self.dvc_dir)
flist = [
self.state.state_file,
self.lock.lock_file,
self.config.config_local_file,
self.updater.updater_file,
self.updater.lock.lock_file,
updater.updater_file,
updater.lock.lock_file,
os.path.join(self.dvc_dir, ExternalRepo.REPOS_DIR),
] + self.state.temp_files
if self.cache.local.cache_dir.startswith(self.root_dir):
@@ -157,14 +162,14 @@ class Repo(object):
import networkx as nx
from dvc.stage import Stage
if not target or recursive:
if not target or (recursive and os.path.isdir(target)):
return self.active_stages(target)
stage = Stage.load(self, target)
if not with_deps:
return [stage]
node = os.path.relpath(stage.path, self.root_dir)
node = relpath(stage.path, self.root_dir)
G = self._get_pipeline(node)
ret = []
@@ -173,73 +178,6 @@ class Repo(object):
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 out.dir_cache:
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
if not out.is_dir_checksum:
return ret
return self._collect_dir_cache(
out, branch=branch, remote=remote, force=force, jobs=jobs
)
def used_cache(
self,
target=None,
@@ -252,6 +190,20 @@ class Repo(object):
jobs=None,
recursive=False,
):
"""Get the stages related to the given target and collect
the `info` of its outputs.
This is useful to know what files from the cache are _in use_
(namely, a file described as an output on a stage).
The scope is, by default, the working directory, but you can use
`all_branches` or `all_tags` to expand scope.
Returns:
A dictionary with Schemes (representing output's location) as keys,
and a list with the outputs' `dumpd` as values.
"""
cache = {}
cache["local"] = []
cache["s3"] = []
@@ -264,7 +216,7 @@ class Repo(object):
all_branches=all_branches, all_tags=all_tags
):
if target:
if recursive:
if recursive and os.path.isdir(target):
stages = self.stages(target)
else:
stages = self.collect(target, with_deps=with_deps)
@@ -276,7 +228,7 @@ class Repo(object):
for stage in stages:
if active and not target and stage.locked:
logger.warning(
"DVC file '{path}' is locked. Its dependencies are"
"DVC-file '{path}' is locked. Its dependencies are"
" not going to be pushed/pulled/fetched.".format(
path=stage.relpath
)
@@ -284,14 +236,12 @@ class Repo(object):
for out in stage.outs:
scheme = out.path_info.scheme
used_cache = out.get_used_cache(
remote=remote, force=force, jobs=jobs
)
cache[scheme].extend(
self._collect_used_cache(
out,
branch=branch,
remote=remote,
force=force,
jobs=jobs,
)
dict(entry, branch=branch) for entry in used_cache
)
return cache
@@ -355,28 +305,28 @@ class Repo(object):
for out in stage.outs:
existing = []
for o in outs:
if o.path == out.path:
if o.path_info == out.path_info:
existing.append(o.stage)
in_o_dir = out.path.startswith(o.path + o.sep)
in_out_dir = o.path.startswith(out.path + out.sep)
in_o_dir = out.path_info.isin(o.path_info)
in_out_dir = o.path_info.isin(out.path_info)
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)
raise OutputDuplicationError(str(out), stages)
outs.append(out)
for stage in stages:
path_dir = os.path.dirname(stage.path) + os.sep
stage_path_info = PathInfo(stage.path)
for out in outs:
if path_dir.startswith(out.path + os.sep):
if stage_path_info.isin(out.path_info):
raise StagePathAsOutputError(stage.wdir, stage.relpath)
for stage in stages:
node = os.path.relpath(stage.path, self.root_dir)
node = relpath(stage.path, self.root_dir)
G.add_node(node, stage=stage)
G_active.add_node(node, stage=stage)
@@ -384,14 +334,14 @@ class Repo(object):
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)
out.path_info != dep.path_info
and not dep.path_info.isin(out.path_info)
and not out.path_info.isin(dep.path_info)
):
continue
dep_stage = out.stage
dep_node = os.path.relpath(dep_stage.path, self.root_dir)
dep_node = 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:
@@ -441,7 +391,8 @@ class Repo(object):
continue
stage = Stage.load(self, path)
for out in stage.outs:
outs.append(out.path + out.sep)
if out.scheme == "local":
outs.append(out.fspath + out.sep)
stages.append(stage)
def filter_dirs(dname, root=root):
@@ -480,10 +431,10 @@ class Repo(object):
is_dir = self.tree.isdir(abs_path)
def func(out):
if out.path == abs_path:
if out.scheme == "local" and out.fspath == abs_path:
return True
if is_dir and recursive and out.path.startswith(abs_path + os.sep):
if is_dir and recursive and out.path_info.isin(abs_path):
return True
return False
@@ -497,3 +448,16 @@ class Repo(object):
def is_dvc_internal(self, path):
path_parts = os.path.normpath(path).split(os.path.sep)
return self.DVC_DIR in path_parts
def open(self, path, remote=None, mode="r", encoding=None):
"""Opens a specified resource as a file descriptor"""
out, = self.find_outs_by_path(path)
if out.isdir():
raise ValueError("Can't open a dir")
with self.state:
cache_info = out.get_used_cache(remote=remote)
self.cloud.pull(cache_info, remote=remote)
cache_file = self.cache.local.checksum_to_path_info(out.checksum)
return _open(cache_file.fspath, mode=mode, encoding=encoding)
+6 -8
View File
@@ -1,3 +1,6 @@
from dvc.scm.tree import WorkingTree
def brancher( # noqa: E302
self, branches=None, all_branches=False, tags=None, all_tags=False
):
@@ -24,11 +27,8 @@ def brancher( # noqa: E302
scm = self.scm
if self.scm.is_dirty():
from dvc.scm.tree import WorkingTree
self.tree = WorkingTree(self.root_dir)
yield "Working Tree"
self.tree = WorkingTree(self.root_dir)
yield "working tree"
if all_branches:
branches = scm.list_branches()
@@ -36,9 +36,7 @@ def brancher( # noqa: E302
if all_tags:
tags = scm.list_tags()
if branches is None:
revs.extend([scm.active_branch()])
else:
if branches is not None:
revs.extend(branches)
if tags is not None:
+7 -5
View File
@@ -10,10 +10,12 @@ logger = logging.getLogger(__name__)
def _cleanup_unused_links(self, all_stages):
used = []
for stage in all_stages:
for out in stage.outs:
used.append(out.path)
used = [
out.fspath
for stage in all_stages
for out in stage.outs
if out.scheme == "local"
]
self.state.remove_unused_links(used)
@@ -47,7 +49,7 @@ def checkout(self, target=None, with_deps=False, force=False, recursive=False):
for stage in stages:
if stage.locked:
logger.warning(
"DVC file '{path}' is locked. Its dependencies are"
"DVC-file '{path}' is locked. Its dependencies are"
" not going to be checked out.".format(path=stage.relpath)
)
+3 -3
View File
@@ -1,8 +1,8 @@
import shutil
from dvc.utils import remove
def destroy(self):
for stage in self.stages():
stage.remove()
stage.remove(remove_outs=False)
shutil.rmtree(self.dvc_dir)
remove(self.dvc_dir)
+1 -1
View File
@@ -110,4 +110,4 @@ def gc(
_do_gc("azure", self.cache.azure.gc, clist)
if cloud:
_do_gc("remote", self.cloud._get_cloud(remote, "gc -c").gc, clist)
_do_gc("remote", self.cloud.get_remote(remote, "gc -c").gc, clist)
+48
View File
@@ -0,0 +1,48 @@
import os
import shortuuid
from dvc.config import Config
from dvc.path_info import PathInfo
from dvc.external_repo import ExternalRepo
from dvc.utils.compat import urlparse
from dvc.utils import remove
@staticmethod
def get(url, path, out=None, rev=None):
out = out or os.path.basename(urlparse(path).path)
# Creating a directory right beside the output to make sure that they
# are on the same filesystem, so we could take the advantage of
# reflink and/or hardlink. Not using tempfile.TemporaryDirectory
# because it will create a symlink to tmpfs, which defeats the purpose
# and won't work with reflink/hardlink.
dpath = os.path.dirname(os.path.abspath(out))
tmp_dir = os.path.join(dpath, "." + str(shortuuid.uuid()))
try:
erepo = ExternalRepo(tmp_dir, url=url, rev=rev)
erepo.install()
# Try any links possible to avoid data duplication.
#
# Not using symlink, because we need to remove cache after we are
# done, and to make that work we would have to copy data over
# anyway before removing the cache, so we might just copy it
# right away.
#
# Also, we can't use theoretical "move" link type here, because
# the same cache file might be used a few times in a directory.
erepo.repo.config.set(
Config.SECTION_CACHE,
Config.SECTION_CACHE_TYPE,
"reflink,hardlink,copy",
)
src = os.path.join(erepo.path, urlparse(path).path.lstrip("/"))
o, = erepo.repo.find_outs_by_path(src)
erepo.repo.fetch(o.stage.path)
o.path_info = PathInfo(os.path.abspath(out))
with o.repo.state:
o.checkout()
erepo.repo.scm.git.close()
finally:
if os.path.exists(tmp_dir):
remove(tmp_dir)
+19
View File
@@ -0,0 +1,19 @@
import os
import dvc.output as output
import dvc.dependency as dependency
from dvc.utils.compat import urlparse
@staticmethod
def get_url(url, out=None):
out = out or os.path.basename(urlparse(url).path)
if os.path.exists(url):
url = os.path.abspath(url)
out = os.path.abspath(out)
dep, = dependency.loads_from(None, [url])
out, = output.loads_from(None, [out], use_cache=False)
dep.download(out)
+6 -20
View File
@@ -1,23 +1,9 @@
from dvc.repo.scm_context import scm_context
from dvc.external_repo import ExternalRepo
@scm_context
def imp(self, url, out, resume=False, fname=None):
from dvc.stage import Stage
def imp(self, url, path, out=None, rev=None):
erepo = {ExternalRepo.PARAM_URL: url}
if rev is not None:
erepo[ExternalRepo.PARAM_VERSION] = rev
with self.state:
stage = Stage.create(
repo=self, cmd=None, deps=[url], outs=[out], fname=fname
)
if stage is None:
return None
self.check_dag(self.stages() + [stage])
with self.state:
stage.run(resume=resume)
stage.dump()
return stage
return self.imp_url(path, out=out, erepo=erepo)
+34
View File
@@ -0,0 +1,34 @@
import os
from dvc.utils.compat import urlparse
from dvc.repo.scm_context import scm_context
@scm_context
def imp_url(self, url, out=None, resume=False, fname=None, erepo=None):
from dvc.stage import Stage
default_out = os.path.basename(urlparse(url).path)
out = out or default_out
with self.state:
stage = Stage.create(
repo=self,
cmd=None,
deps=[url],
outs=[out],
fname=fname,
erepo=erepo,
)
if stage is None:
return None
self.check_dag(self.stages() + [stage])
with self.state:
stage.run(resume=resume)
stage.dump()
return stage
+4 -6
View File
@@ -1,5 +1,4 @@
import os
import shutil
import colorama
import logging
@@ -7,8 +6,7 @@ from dvc.repo import Repo
from dvc.scm import SCM, NoSCM
from dvc.config import Config
from dvc.exceptions import InitError
from dvc.utils import boxify
from dvc.utils import boxify, relpath, remove
logger = logging.getLogger(__name__)
@@ -59,7 +57,7 @@ def init(root_dir=os.curdir, no_scm=False, force=False):
Raises:
KeyError: Raises an exception.
"""
root_dir = os.path.abspath(root_dir)
root_dir = os.path.realpath(root_dir)
dvc_dir = os.path.join(root_dir, Repo.DVC_DIR)
scm = SCM(root_dir)
if isinstance(scm, NoSCM) and not no_scm:
@@ -74,11 +72,11 @@ def init(root_dir=os.curdir, no_scm=False, force=False):
if not force:
raise InitError(
"'{repo}' exists. Use '-f' to force.".format(
repo=os.path.relpath(dvc_dir)
repo=relpath(dvc_dir)
)
)
shutil.rmtree(dvc_dir)
remove(dvc_dir)
os.mkdir(dvc_dir)
+7 -11
View File
@@ -134,7 +134,7 @@ def _format_output(content, typ):
return content
def _read_metric(fd, typ=None, xpath=None, rel_path=None, branch=None):
def _read_metric(fd, typ=None, xpath=None, fname=None, branch=None):
typ = typ.lower().strip() if typ else typ
try:
if xpath:
@@ -146,7 +146,7 @@ def _read_metric(fd, typ=None, xpath=None, rel_path=None, branch=None):
except Exception:
logger.exception(
"unable to read metric in '{}' in branch '{}'".format(
rel_path, branch
fname, branch
)
)
return None
@@ -220,28 +220,24 @@ def _read_metrics(repo, metrics, branch):
for out, typ, xpath in metrics:
assert out.scheme == "local"
if not typ:
typ = os.path.splitext(out.path.lower())[1].replace(".", "")
typ = os.path.splitext(out.fspath.lower())[1].replace(".", "")
if out.use_cache:
open_fun = open
path = repo.cache.local.get(out.checksum)
else:
open_fun = repo.tree.open
path = out.path
path = out.fspath
try:
with open_fun(path) as fd:
metric = _read_metric(
fd,
typ=typ,
xpath=xpath,
rel_path=out.rel_path,
branch=branch,
fd, typ=typ, xpath=xpath, fname=str(out), branch=branch
)
except IOError as e:
if e.errno == errno.ENOENT:
logger.warning(
NO_METRICS_FILE_AT_REFERENCE_WARNING.format(
out.rel_path, branch
out.path_info, branch
)
)
metric = None
@@ -251,7 +247,7 @@ def _read_metrics(repo, metrics, branch):
if not metric:
continue
res[out.rel_path] = metric
res[str(out)] = metric
return res
+3 -2
View File
@@ -33,10 +33,11 @@ def move(self, from_path, to_path):
from dvc.stage import Stage
from_out = Output.loads_from(Stage(self), [from_path])[0]
assert from_out.scheme == "local"
to_path = _expand_target_path(from_path, to_path)
outs = self.find_outs_by_path(from_out.path)
outs = self.find_outs_by_path(from_out.fspath)
assert len(outs) == 1
out = outs[0]
stage = out.stage
@@ -45,7 +46,7 @@ def move(self, from_path, to_path):
raise MoveNotDataSourceError(stage.relpath)
stage_name = os.path.splitext(os.path.basename(stage.path))[0]
from_name = os.path.basename(from_out.path)
from_name = os.path.basename(from_out.fspath)
if stage_name == from_name:
os.unlink(stage.path)
-8
View File
@@ -1,8 +0,0 @@
class Pkg(object):
def __init__(self, repo):
self.repo = repo
def install(self, *args, **kwargs):
from dvc.repo.pkg.install import install
return install(self.repo, *args, **kwargs)
-173
View File
@@ -1,173 +0,0 @@
from __future__ import unicode_literals
import os
import shutil
import logging
from dvc.exceptions import DvcException
from dvc.stage import Stage
from dvc.scm.git.temp_git_repo import TempGitRepo
logger = logging.getLogger(__name__)
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, address, target_dir, select=[], fname=None
):
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, address, target_dir, select=[], fname=None
):
from git.cmd import Git
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(address).strip("/").split("/")[-1]
if not module_name:
raise DvcException(
"Package address error: unable to extract package name"
)
with TempGitRepo(
address, module_name, Package.MODULES_DIR
) as tmp_repo:
outputs_to_copy = tmp_repo.outs
if select:
outputs_to_copy = list(
filter(lambda out: out.dvc_path in 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(fname, target_dir, module_name)
try:
self.persist_stage_and_scm_state(
parent_repo, outputs_to_copy, target_dir, dvc_file
)
except Exception as ex:
raise DvcException(
"Package '{}' was installed "
"but stage file '{}' "
"was not created properly: {}".format(
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(self, address, target_dir, select=[], fname=None):
"""
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(target_dir):
raise DvcException(
"target directory '{}' does not exist".format(target_dir)
)
curr_dir = os.path.realpath(os.curdir)
if not os.path.realpath(target_dir).startswith(curr_dir):
raise DvcException(
"the current directory should be a subdirectory of the target "
"dir '{}'".format(target_dir)
)
addresses = [address] if address else PackageManager.read_packages()
for addr in addresses:
mgr = PackageManager.get_package(addr)
mgr.install_or_update(self, address, target_dir, select, fname)
+21 -13
View File
@@ -5,7 +5,7 @@ import logging
from dvc.exceptions import ReproductionError
from dvc.repo.scm_context import scm_context
from dvc.utils import relpath
logger = logging.getLogger(__name__)
@@ -15,7 +15,7 @@ def _reproduce_stage(stages, node, force, dry, interactive, no_commit):
if stage.locked:
logger.warning(
"DVC file '{path}' is locked. Its dependencies are"
"DVC-file '{path}' is locked. Its dependencies are"
" not going to be reproduced.".format(path=stage.relpath)
)
@@ -35,7 +35,7 @@ def _reproduce_stage(stages, node, force, dry, interactive, no_commit):
def reproduce(
self,
target=None,
recursive=True,
single_item=False,
force=False,
dry=False,
interactive=False,
@@ -44,7 +44,9 @@ def reproduce(
ignore_build_cache=False,
no_commit=False,
downstream=False,
recursive=False,
):
import networkx as nx
from dvc.stage import Stage
if not target and not all_pipelines:
@@ -56,10 +58,16 @@ def reproduce(
interactive = core.get(config.SECTION_CORE_INTERACTIVE, False)
targets = []
if pipeline or all_pipelines:
if recursive and os.path.isdir(target):
G = self.graph(from_directory=target)[1]
dir_targets = [
os.path.join(self.root_dir, n) for n in nx.dfs_postorder_nodes(G)
]
targets.extend(dir_targets)
elif pipeline or all_pipelines:
if pipeline:
stage = Stage.load(self, target)
node = os.path.relpath(stage.path, self.root_dir)
node = relpath(stage.path, self.root_dir)
pipelines = [self._get_pipeline(node)]
else:
pipelines = self.pipelines()
@@ -77,7 +85,7 @@ def reproduce(
stages = _reproduce(
self,
target,
recursive=recursive,
single_item=single_item,
force=force,
dry=dry,
interactive=interactive,
@@ -93,7 +101,7 @@ def reproduce(
def _reproduce(
self,
target,
recursive=True,
single_item=False,
force=False,
dry=False,
interactive=False,
@@ -107,9 +115,13 @@ def _reproduce(
stage = Stage.load(self, target)
G = self.graph()[1]
stages = nx.get_node_attributes(G, "stage")
node = os.path.relpath(stage.path, self.root_dir)
node = relpath(stage.path, self.root_dir)
if recursive:
if single_item:
ret = _reproduce_stage(
stages, node, force, dry, interactive, no_commit
)
else:
ret = _reproduce_stages(
G,
stages,
@@ -121,10 +133,6 @@ def _reproduce(
no_commit,
downstream,
)
else:
ret = _reproduce_stage(
stages, node, force, dry, interactive, no_commit
)
return ret
+27 -1
View File
@@ -14,7 +14,7 @@ def _local_status(self, target=None, with_deps=False):
for stage in stages:
if stage.locked:
logger.warning(
"DVC file '{path}' is locked. Its dependencies are"
"DVC-file '{path}' is locked. Its dependencies are"
" not going to be shown in the status output.".format(
path=stage.relpath
)
@@ -35,6 +35,32 @@ def _cloud_status(
with_deps=False,
all_tags=False,
):
"""Returns a dictionary with the files that are new or deleted.
- new: Remote doesn't have the file
- deleted: File is no longer in the local cache
Example:
Given the following commands:
$ echo "foo" > foo
$ echo "bar" > bar
$ dvc add foo bar
$ dvc status -c
It will return something like:
{ "foo": "new", "bar": "new" }
Now, after pushing and removing "bar" from the local cache:
$ dvc push
$ rm .dvc/cache/c1/57a79031e1c40f85931829bc5fc552
The result would be:
{ "bar": "deleted" }
"""
import dvc.remote.base as cloud
used = self.used_cache(
+41 -32
View File
@@ -6,7 +6,7 @@ import os
import logging
from dvc.utils.compat import str, open
from dvc.utils import fix_env
from dvc.utils import fix_env, relpath
from dvc.scm.base import (
Base,
SCMError,
@@ -42,10 +42,10 @@ class Git(Base):
from git.exc import InvalidGitRepositoryError
try:
self.git = git.Repo(root_dir)
self.git = git.Repo(self.root_dir)
except InvalidGitRepositoryError:
msg = "{} is not a git repository"
raise SCMError(msg.format(root_dir))
raise SCMError(msg.format(self.root_dir))
# NOTE: fixing LD_LIBRARY_PATH for binary built by PyInstaller.
# http://pyinstaller.readthedocs.io/en/stable/runtime-information.html
@@ -92,7 +92,7 @@ class Git(Base):
msg.format(self.GITIGNORE, path, ignore_file_dir)
)
entry = os.path.relpath(path, ignore_file_dir).replace(os.sep, "/")
entry = relpath(path, ignore_file_dir).replace(os.sep, "/")
# NOTE: using '/' prefix to make path unambiguous
if len(entry) > 0 and entry[0] != "/":
entry = "/" + entry
@@ -104,6 +104,16 @@ class Git(Base):
return entry, gitignore
@staticmethod
def _ignored(entry, gitignore_path):
if os.path.exists(gitignore_path):
with open(gitignore_path, "r") as fobj:
ignore_list = fobj.readlines()
return any(
filter(lambda x: x.strip() == entry.strip(), ignore_list)
)
return False
def ignore(self, path, in_curr_dir=False):
base_dir = (
os.path.realpath(os.curdir)
@@ -112,31 +122,30 @@ class Git(Base):
)
entry, gitignore = self._get_gitignore(path, base_dir)
ignore_list = []
if os.path.exists(gitignore):
with open(gitignore, "r") as f:
ignore_list = f.readlines()
if any(filter(lambda x: x.strip() == entry.strip(), ignore_list)):
return
if self._ignored(entry, gitignore):
return
msg = "Adding '{}' to '{}'.".format(
os.path.relpath(path), os.path.relpath(gitignore)
)
msg = "Adding '{}' to '{}'.".format(relpath(path), relpath(gitignore))
logger.info(msg)
self._add_entry_to_gitignore(entry, gitignore, ignore_list)
self._add_entry_to_gitignore(entry, gitignore)
self.track_file(os.path.relpath(gitignore))
self.track_file(relpath(gitignore))
self.ignored_paths.append(path)
@staticmethod
def _add_entry_to_gitignore(entry, gitignore, ignore_list):
content = entry
if ignore_list:
content = "\n" + content
with open(gitignore, "a", encoding="utf-8") as fobj:
fobj.write(content)
def _add_entry_to_gitignore(entry, gitignore):
with open(gitignore, "a+", encoding="utf-8") as fobj:
fobj.seek(0, os.SEEK_END)
if fobj.tell() == 0:
# Empty file
prefix = ""
else:
fobj.seek(fobj.tell() - 1, os.SEEK_SET)
last = fobj.read(1)
prefix = "" if last == "\n" else "\n"
fobj.write("{}{}\n".format(prefix, entry))
def ignore_remove(self, path):
entry, gitignore = self._get_gitignore(path)
@@ -152,7 +161,7 @@ class Git(Base):
with open(gitignore, "w") as fobj:
fobj.writelines(filtered)
self.track_file(os.path.relpath(gitignore))
self.track_file(relpath(gitignore))
def add(self, paths):
# NOTE: GitPython is not currently able to handle index version >= 3.
@@ -191,7 +200,7 @@ class Git(Base):
def is_tracked(self, path):
# it is equivalent to `bool(self.git.git.ls_files(path))` by
# functionality, but ls_files fails on unicode filenames
path = os.path.relpath(path, self.root_dir)
path = relpath(path, self.root_dir)
return path in [i[0] for i in self.git.index.entries]
def is_dirty(self):
@@ -242,7 +251,7 @@ class Git(Base):
logger.info(
"\n"
"To track the changes with git run:\n"
"To track the changes with git, run:\n"
"\n"
"\tgit add {files}".format(files=" ".join(self.files_to_track))
)
@@ -260,15 +269,14 @@ class Git(Base):
def _get_diff_trees(self, a_ref, b_ref):
"""Private method for getting the trees and commit hashes of 2 git
references.
Requires `gitdb` module (from gitpython package).
references. Requires `gitdb` module (from gitpython package).
Args:
a_ref(str) - git reference
b_ref(str) - second git reference. If None, uses HEAD
a_ref (str): git reference
b_ref (str): second git reference. If None, uses HEAD
Returns:
tuple - tuple with elements: (trees, commits)
tuple: tuple with elements: (trees, commits)
"""
from gitdb.exc import BadObject, BadName
@@ -294,11 +302,12 @@ class Git(Base):
Returns the dvc hash names of changed file/directory
Args:
a_ref(str) - git reference
b_ref(str) - optional second git reference, default None
a_ref (str): git reference
b_ref (str): optional second git reference, default None
Returns:
dict - dictionary with keys: (a_tree, b_tree, a_ref, b_ref, equal)
dict: dictionary with keys: {a_ref, b_ref, equal}
or {a_ref, b_ref, a_tree, b_tree}
"""
diff_dct = {DIFF_EQUAL: False}
trees, commits = self._get_diff_trees(a_ref, b_ref)
-148
View File
@@ -1,148 +0,0 @@
import os
import shutil
import tempfile
import logging
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class TempRepoException(DvcException):
"""Raise when temporary package repository has not properly created"""
def __init__(self, temp_repo, msg, cause=None):
m = "temp repository '{}' error: {}".format(temp_repo.addr, msg)
super(TempRepoException, self).__init__(m, cause)
class TempGitRepo(object):
GIT_DIR_TO_REMOVE = ".git"
GIT_FILES_TO_REMOVE = [".gitignore", ".gitmodules"]
SUFFIX = "tmp_DVC_mod"
def __init__(self, addr, module_name, modules_dir):
self.addr = addr
self.modules_dir = modules_dir
self._tmp_mod_prefix = module_name + "_" + self.SUFFIX + "_"
self._reset_state()
def _reset_state(self):
self._set_state(None, [])
def _set_state(self, cloned_tmp_dir, outs):
from dvc.repo import Repo
self.repo = Repo(cloned_tmp_dir) if cloned_tmp_dir else None
self._cloned_tmp_dir = cloned_tmp_dir
self.outs = outs
self._moved_files = []
def fetch(self, targets):
for target in targets:
try:
self.repo.fetch(target)
except Exception as ex:
msg = "error in fetching data from {}: {}".format(
os.path.basename(target), ex
)
raise TempRepoException(self, msg)
@property
def is_state_set(self):
return self._cloned_tmp_dir is not None
def __enter__(self):
if self.is_state_set:
raise TempRepoException(self, "Git repo cloning duplication")
module_temp_dir = None
try:
module_temp_dir = self._clone_to_temp_dir()
self._remove_git_files_and_dirs(module_temp_dir)
outputs = self._read_outputs(module_temp_dir)
self._set_state(module_temp_dir, outputs)
finally:
self._clean_mod_temp_dir(module_temp_dir)
return self
def _clone_to_temp_dir(self):
import git
result = tempfile.mktemp(
prefix=self._tmp_mod_prefix, dir=self.modules_dir
)
logger.debug(
"Cloning git repository {} to temp dir {}".format(
self.addr, result
)
)
git.Repo.clone_from(self.addr, result, depth=1)
return result
def _remove_git_files_and_dirs(self, module_temp_dir):
logger.debug(
"Removing git meta files from {}: {} dif and {}".format(
module_temp_dir,
TempGitRepo.GIT_DIR_TO_REMOVE,
", ".join(TempGitRepo.GIT_FILES_TO_REMOVE),
)
)
shutil.rmtree(
os.path.join(module_temp_dir, TempGitRepo.GIT_DIR_TO_REMOVE)
)
for item in TempGitRepo.GIT_FILES_TO_REMOVE:
fname = os.path.join(module_temp_dir, item)
if os.path.exists(fname):
os.remove(fname)
def _clean_mod_temp_dir(self, module_temp_dir):
if not self.is_state_set:
if module_temp_dir and os.path.exists(module_temp_dir):
shutil.rmtree(module_temp_dir, ignore_errors=True)
def _read_outputs(self, module_temp_dir):
from dvc.repo import Repo
pkg_repo = Repo(module_temp_dir)
stages = pkg_repo.stages()
return [out for s in stages for out in s.outs]
def persist_to(self, module_dir, parent_repo):
if not self.is_state_set:
raise TempRepoException(self, "cannot persist")
tmp_repo_cache = self.repo.cache.local.url
for prefix in os.listdir(tmp_repo_cache):
if len(prefix) != 2:
logger.warning(
"wrong dir format in cache {}: dir {}".format(
tmp_repo_cache, prefix
)
)
self._move_all_cache_files(parent_repo, prefix, tmp_repo_cache)
shutil.move(self._cloned_tmp_dir, module_dir)
self._reset_state()
@staticmethod
def _move_all_cache_files(parent_repo, prefix, tmp_repo_cache):
obj_name = os.path.join(tmp_repo_cache, prefix)
for suffix in os.listdir(obj_name):
src_path = os.path.join(tmp_repo_cache, prefix, suffix)
pre = os.path.join(parent_repo.cache.local.url, prefix)
if not os.path.exists(pre):
os.mkdir(pre)
dest = os.path.join(pre, suffix)
shutil.move(src_path, dest)
def __exit__(self, exc_type, exc_value, traceback):
if self.is_state_set:
shutil.rmtree(self._cloned_tmp_dir)
self._reset_state()
pass
+39 -5
View File
@@ -2,7 +2,9 @@ import errno
import os
from dvc.ignore import DvcIgnoreFilter
from dvc.utils import relpath
from dvc.utils.compat import StringIO, BytesIO
from dvc.exceptions import DvcException
from dvc.scm.tree import BaseTree
@@ -31,14 +33,14 @@ class GitTree(BaseTree):
def open(self, path, binary=False):
relpath = os.path.relpath(path, self.git.working_dir)
relative_path = relpath(path, self.git.working_dir)
obj = self.git_object_by_path(path)
if obj is None:
msg = "No such file in branch '{}'".format(self.rev)
raise IOError(errno.ENOENT, msg, relpath)
raise IOError(errno.ENOENT, msg, relative_path)
if obj.mode == GIT_MODE_DIR:
raise IOError(errno.EISDIR, "Is a directory", relpath)
raise IOError(errno.EISDIR, "Is a directory", relative_path)
# GitPython's obj.data_stream is a fragile thing, it is better to
# read it immediately, also it needs to be to decoded if we follow
@@ -76,10 +78,42 @@ class GitTree(BaseTree):
return True
return False
def _try_fetch_from_remote(self):
import git
try:
# checking if tag/branch exists locally
self.git.git.show_ref(self.rev, verify=True)
return
except git.exc.GitCommandError:
pass
try:
# checking if it exists on the remote
self.git.git.ls_remote("origin", self.rev, exit_code=True)
# fetching remote tag/branch so we can reference it locally
self.git.git.fetch("origin", "{rev}:{rev}".format(rev=self.rev))
except git.exc.GitCommandError:
pass
def git_object_by_path(self, path):
path = os.path.relpath(os.path.realpath(path), self.git.working_dir)
import git
path = relpath(os.path.realpath(path), self.git.working_dir)
assert path.split(os.sep, 1)[0] != ".."
tree = self.git.tree(self.rev)
self._try_fetch_from_remote()
try:
tree = self.git.tree(self.rev)
except git.exc.BadName as exc:
raise DvcException(
"revision '{}' not found in git '{}'".format(
self.rev, os.path.relpath(self.git.working_dir)
),
cause=exc,
)
if not path or path == ".":
return tree
for i in path.split(os.sep):
+72 -65
View File
@@ -7,7 +7,11 @@ import re
import os
import subprocess
import logging
import signal
import threading
from dvc.utils import relpath
from dvc.utils.compat import pathlib
from dvc.utils.fs import contains_symlink_up_to
from schema import Schema, SchemaError, Optional, Or, And
@@ -54,7 +58,7 @@ class StageFileAlreadyExistsError(DvcException):
class StageFileIsNotDvcFileError(DvcException):
def __init__(self, fname):
msg = "'{}' is not a dvc file".format(fname)
msg = "'{}' is not a DVC-file".format(fname)
sname = fname + Stage.STAGE_FILE_SUFFIX
if Stage.is_stage_file(sname):
@@ -175,7 +179,7 @@ class Stage(object):
@property
def relpath(self):
return os.path.relpath(self.path)
return relpath(self.path)
@property
def is_data_source(self):
@@ -212,13 +216,20 @@ class Stage(object):
"""Whether the stage file was created with `dvc import`."""
return not self.cmd and len(self.deps) == 1 and len(self.outs) == 1
@property
def is_repo_import(self):
if not self.is_import:
return False
return isinstance(self.deps[0], dependency.DependencyREPO)
def _changed_deps(self):
if self.locked:
return False
if self.is_callback:
logger.warning(
"Dvc file '{fname}' is a 'callback' stage "
"DVC-file '{fname}' is a 'callback' stage "
"(has a command and no dependencies) and thus always "
"considered as changed.".format(fname=self.relpath)
)
@@ -253,7 +264,7 @@ class Stage(object):
def _changed_md5(self):
if self.changed_md5():
logger.warning("Dvc file '{}' changed.".format(self.relpath))
logger.warning("DVC-file '{}' changed.".format(self.relpath))
return True
return False
@@ -286,8 +297,11 @@ class Stage(object):
for out in self.outs:
out.unprotect()
def remove(self, force=False):
self.remove_outs(ignore_remove=True, force=force)
def remove(self, force=False, remove_outs=True):
if remove_outs:
self.remove_outs(ignore_remove=True, force=force)
else:
self.unprotect_outs()
os.unlink(self.path)
def reproduce(
@@ -322,32 +336,20 @@ class Stage(object):
raise StageFileFormatError(fname, exc)
@classmethod
def _stage_fname(cls, fname, outs, add):
if fname:
return fname
def _stage_fname(cls, outs, add):
if not outs:
return cls.STAGE_FILE
out = outs[0]
path_handler = out.remote.ospath
fname = out.path_info.name + cls.STAGE_FILE_SUFFIX
fname = path_handler.basename(out.path) + cls.STAGE_FILE_SUFFIX
fname = Stage._expand_to_path_on_add_local(
add, fname, out, path_handler
)
return fname
@staticmethod
def _expand_to_path_on_add_local(add, fname, out, path_handler):
if (
add
and out.is_in_repo
and not contains_symlink_up_to(out.path, out.repo.root_dir)
and not contains_symlink_up_to(out.fspath, out.repo.root_dir)
):
fname = path_handler.join(path_handler.dirname(out.path), fname)
fname = out.path_info.with_name(fname).fspath
return fname
@staticmethod
@@ -430,6 +432,7 @@ class Stage(object):
validate_state=True,
outs_persist=None,
outs_persist_no_cache=None,
erepo=None,
):
if outs is None:
outs = []
@@ -470,12 +473,15 @@ class Stage(object):
outs_persist,
outs_persist_no_cache,
)
stage.deps = dependency.loads_from(stage, deps)
stage.deps = dependency.loads_from(stage, deps, erepo=erepo)
stage._check_circular_dependency()
stage._check_duplicated_arguments()
fname = Stage._stage_fname(fname, stage.outs, add=add)
if not fname:
fname = Stage._stage_fname(stage.outs, add=add)
stage._check_dvc_filename(fname)
wdir = os.path.abspath(wdir)
if cwd is not None:
@@ -555,7 +561,7 @@ class Stage(object):
raise StageFileBadNameError(
"bad stage filename '{}'. Stage files should be named"
" 'Dvcfile' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
os.path.relpath(fname), os.path.basename(fname)
relpath(fname), os.path.basename(fname)
)
)
@@ -583,7 +589,7 @@ class Stage(object):
# it raises the proper exceptions by priority:
# 1. when the file doesn't exists
# 2. filename is not a dvc filename
# 2. filename is not a DVC-file
# 3. path doesn't represent a regular file
Stage._check_file_exists(repo, fname)
Stage._check_dvc_filename(fname)
@@ -595,7 +601,7 @@ class Stage(object):
# looses keys in deps and outs load
state = copy.deepcopy(d)
Stage.validate(d, fname=os.path.relpath(fname))
Stage.validate(d, fname=relpath(fname))
path = os.path.abspath(fname)
stage = Stage(
@@ -619,16 +625,13 @@ class Stage(object):
return stage
def dumpd(self):
from dvc.remote.base import RemoteBASE
rel_wdir = relpath(self.wdir, os.path.dirname(self.path))
return {
key: value
for key, value in {
Stage.PARAM_MD5: self.md5,
Stage.PARAM_CMD: self.cmd,
Stage.PARAM_WDIR: RemoteBASE.to_posixpath(
os.path.relpath(self.wdir, os.path.dirname(self.path))
),
Stage.PARAM_WDIR: pathlib.PurePath(rel_wdir).as_posix(),
Stage.PARAM_LOCKED: self.locked,
Stage.PARAM_DEPS: [d.dumpd() for d in self.deps],
Stage.PARAM_OUTS: [o.dumpd() for o in self.outs],
@@ -643,15 +646,13 @@ class Stage(object):
self._check_dvc_filename(fname)
logger.info(
"Saving information to '{file}'.".format(
file=os.path.relpath(fname)
)
"Saving information to '{file}'.".format(file=relpath(fname))
)
d = self.dumpd()
apply_diff(d, self._state)
dump_stage_file(fname, self._state)
self.repo.scm.track_file(os.path.relpath(fname))
self.repo.scm.track_file(relpath(fname))
def _compute_md5(self):
from dvc.output.base import OutputBase
@@ -695,11 +696,11 @@ class Stage(object):
@staticmethod
def _changed_entries(entries):
ret = []
for entry in entries:
if entry.checksum and entry.changed_checksum():
ret.append(entry.rel_path)
return ret
return [
str(entry)
for entry in entries
if entry.checksum and entry.changed_checksum()
]
def check_can_commit(self, force):
changed_deps = self._changed_entries(self.deps)
@@ -751,38 +752,51 @@ class Stage(object):
def _check_circular_dependency(self):
from dvc.exceptions import CircularDependencyError
circular_dependencies = set(d.path for d in self.deps) & set(
o.path for o in self.outs
circular_dependencies = set(d.path_info for d in self.deps) & set(
o.path_info for o in self.outs
)
if circular_dependencies:
raise CircularDependencyError(circular_dependencies.pop())
raise CircularDependencyError(str(circular_dependencies.pop()))
def _check_duplicated_arguments(self):
from dvc.exceptions import ArgumentDuplicationError
from collections import Counter
path_counts = Counter(edge.path for edge in self.deps + self.outs)
path_counts = Counter(edge.path_info for edge in self.deps + self.outs)
for path, occurrence in path_counts.items():
if occurrence > 1:
raise ArgumentDuplicationError(path)
raise ArgumentDuplicationError(str(path))
def _run(self):
self._check_missing_deps()
executable = os.getenv("SHELL") if os.name != "nt" else None
self._warn_if_fish(executable)
p = subprocess.Popen(
self.cmd,
cwd=self.wdir,
shell=True,
env=fix_env(os.environ),
executable=executable,
main_thread = isinstance(
threading.current_thread(), threading._MainThread
)
p.communicate()
if main_thread:
old_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
if p.returncode != 0:
p = None
try:
p = subprocess.Popen(
self.cmd,
cwd=self.wdir,
shell=True,
env=fix_env(os.environ),
executable=executable,
close_fds=True,
)
p.communicate()
finally:
if main_thread:
signal.signal(signal.SIGINT, old_handler)
if (p is None) or (p.returncode != 0):
raise StageCmdFailedError(self)
def run(self, dry=False, resume=False, no_commit=False, force=False):
@@ -801,16 +815,14 @@ class Stage(object):
elif self.is_import:
logger.info(
"Importing '{dep}' -> '{out}'".format(
dep=self.deps[0].path, out=self.outs[0].path
dep=self.deps[0], out=self.outs[0]
)
)
if not dry:
if self._already_cached() and not force:
self.outs[0].checkout()
else:
self.deps[0].download(
self.outs[0].path_info, resume=resume
)
self.deps[0].download(self.outs[0], resume=resume)
elif self.is_data_source:
msg = "Verifying data sources in '{}'".format(self.relpath)
@@ -836,12 +848,7 @@ class Stage(object):
self.commit()
def check_missing_outputs(self):
paths = [
out.path if out.scheme != "local" else out.rel_path
for out in self.outs
if not out.exists
]
paths = [str(out) for out in self.outs if not out.exists]
if paths:
raise MissingDataSource(paths)
+45 -88
View File
@@ -7,9 +7,10 @@ import sqlite3
import logging
from dvc.config import Config
from dvc.utils import remove, current_timestamp
from dvc.utils import remove, current_timestamp, relpath
from dvc.exceptions import DvcException
from dvc.utils.fs import get_mtime_and_size, get_inode
from dvc.utils.compat import fspath_py35
logger = logging.getLogger(__name__)
@@ -116,29 +117,9 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
def __exit__(self, typ, value, tbck):
self.dump()
def changed(self, path, md5):
"""Check if file/directory has the expected md5.
Args:
path (str): path to the file/directory to check.
md5 (str): expected md5.
Returns:
bool: True if path has the expected md5, False otherwise.
"""
actual = self.update(path)
msg = "File '{}', md5 '{}', actual '{}'"
logger.debug(msg.format(path, md5, actual))
if not md5 or not actual:
return True
return actual.split(".")[0] != md5.split(".")[0]
def _execute(self, cmd):
def _execute(self, cmd, parameters=()):
logger.debug(cmd)
return self.cursor.execute(cmd)
return self.cursor.execute(cmd, parameters)
def _fetchall(self):
ret = self.cursor.fetchall()
@@ -249,8 +230,10 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
"""Saves state database."""
assert self.database is not None
cmd = "SELECT count from {} WHERE rowid={}"
self._execute(cmd.format(self.STATE_INFO_TABLE, self.STATE_INFO_ROW))
cmd = "SELECT count from {} WHERE rowid=?".format(
self.STATE_INFO_TABLE
)
self._execute(cmd, (self.STATE_INFO_ROW,))
ret = self._fetchall()
assert len(ret) == 1
assert len(ret[0]) == 1
@@ -280,16 +263,10 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
assert len(ret[0]) == 1
count = ret[0][0]
cmd = "UPDATE {} SET count = {} WHERE rowid = {}"
self._execute(
cmd.format(
self.STATE_INFO_TABLE,
self._to_sqlite(count),
self.STATE_INFO_ROW,
)
cmd = "UPDATE {} SET count = ? WHERE rowid = ?".format(
self.STATE_INFO_TABLE
)
self._update_cache_directory_state()
self._execute(cmd, (self._to_sqlite(count), self.STATE_INFO_ROW))
self.database.commit()
self.cursor.close()
@@ -303,60 +280,60 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
return actual_mtime != mtime or actual_size != size
def _update_state_record_timestamp_for_inode(self, actual_inode):
cmd = 'UPDATE {} SET timestamp = "{}" WHERE inode = {}'
cmd = "UPDATE {} SET timestamp = ? WHERE inode = ?".format(
self.STATE_TABLE
)
self._execute(
cmd.format(
self.STATE_TABLE,
current_timestamp(),
self._to_sqlite(actual_inode),
)
cmd, (current_timestamp(), self._to_sqlite(actual_inode))
)
def _update_state_for_path_changed(
self, path, actual_inode, actual_mtime, actual_size, checksum
self, actual_inode, actual_mtime, actual_size, checksum
):
cmd = (
"UPDATE {} SET "
'mtime = "{}", size = "{}", '
'md5 = "{}", timestamp = "{}" '
"WHERE inode = {}"
)
"mtime = ?, size = ?, "
"md5 = ?, timestamp = ? "
"WHERE inode = ?"
).format(self.STATE_TABLE)
self._execute(
cmd.format(
self.STATE_TABLE,
cmd,
(
actual_mtime,
actual_size,
checksum,
current_timestamp(),
self._to_sqlite(actual_inode),
)
),
)
def _insert_new_state_record(
self, path, actual_inode, actual_mtime, actual_size, checksum
self, actual_inode, actual_mtime, actual_size, checksum
):
assert checksum is not None
cmd = (
"INSERT INTO {}(inode, mtime, size, md5, timestamp) "
'VALUES ({}, "{}", "{}", "{}", "{}")'
)
"VALUES (?, ?, ?, ?, ?)"
).format(self.STATE_TABLE)
self._execute(
cmd.format(
self.STATE_TABLE,
cmd,
(
self._to_sqlite(actual_inode),
actual_mtime,
actual_size,
checksum,
current_timestamp(),
)
),
)
self.inserts += 1
def get_state_record_for_inode(self, inode):
cmd = "SELECT mtime, size, md5, timestamp from {} " "WHERE inode={}"
cmd = cmd.format(self.STATE_TABLE, self._to_sqlite(inode))
self._execute(cmd)
cmd = (
"SELECT mtime, size, md5, timestamp from {} WHERE "
"inode=?".format(self.STATE_TABLE)
)
self._execute(cmd, (self._to_sqlite(inode),))
results = self._fetchall()
if results:
# uniquness constrain on inode
@@ -374,7 +351,7 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
assert path_info.scheme == "local"
assert checksum is not None
path = path_info.path
path = fspath_py35(path_info)
assert os.path.exists(path)
actual_mtime, actual_size = get_mtime_and_size(path)
@@ -383,12 +360,12 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
existing_record = self.get_state_record_for_inode(actual_inode)
if not existing_record:
self._insert_new_state_record(
path, actual_inode, actual_mtime, actual_size, checksum
actual_inode, actual_mtime, actual_size, checksum
)
return
self._update_state_for_path_changed(
path, actual_inode, actual_mtime, actual_size, checksum
actual_inode, actual_mtime, actual_size, checksum
)
def get(self, path_info):
@@ -403,7 +380,7 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
doesn't exist in the state database.
"""
assert path_info.scheme == "local"
path = path_info.path
path = fspath_py35(path_info)
if not os.path.exists(path):
return None
@@ -430,22 +407,19 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
path_info (dict): path info to add to the list of links.
"""
assert path_info.scheme == "local"
path = path_info.path
path = fspath_py35(path_info)
if not os.path.exists(path):
return
mtime, _ = get_mtime_and_size(path)
inode = get_inode(path)
relpath = os.path.relpath(path, self.root_dir)
relative_path = relpath(path, self.root_dir)
cmd = (
"REPLACE INTO {}(path, inode, mtime) "
'VALUES ("{}", {}, "{}")'.format(
self.LINK_STATE_TABLE, relpath, self._to_sqlite(inode), mtime
)
cmd = "REPLACE INTO {}(path, inode, mtime) " "VALUES (?, ?, ?)".format(
self.LINK_STATE_TABLE
)
self._execute(cmd)
self._execute(cmd, (relative_path, self._to_sqlite(inode), mtime))
def remove_unused_links(self, used):
"""Removes all saved links except the ones that are used.
@@ -476,22 +450,5 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
unused.append(relpath)
for relpath in unused:
cmd = 'DELETE FROM {} WHERE path = "{}"'
self._execute(cmd.format(self.LINK_STATE_TABLE, relpath))
def _update_cache_directory_state(self):
cache_path = self.repo.cache.local.cache_dir
mtime, size = get_mtime_and_size(cache_path)
inode = get_inode(cache_path)
cmd = (
"INSERT OR REPLACE INTO {}(inode, size, mtime, timestamp, md5) "
'VALUES ({}, "{}", "{}", "{}", "")'.format(
self.STATE_TABLE,
self._to_sqlite(inode),
size,
mtime,
current_timestamp(),
)
)
self._execute(cmd)
cmd = "DELETE FROM {} WHERE path = ?".format(self.LINK_STATE_TABLE)
self._execute(cmd, (relpath,))
+17 -5
View File
@@ -1,6 +1,6 @@
from __future__ import unicode_literals
from dvc.utils.compat import str, open
from dvc.utils.compat import str, open, fspath
import os
import errno
@@ -16,6 +16,8 @@ class System(object):
import ctypes
from dvc.exceptions import DvcException
source, link_name = fspath(source), fspath(link_name)
if System.is_unix():
try:
os.link(source, link_name)
@@ -40,6 +42,8 @@ class System(object):
import ctypes
from dvc.exceptions import DvcException
source, link_name = fspath(source), fspath(link_name)
if System.is_unix():
try:
os.symlink(source, link_name)
@@ -117,6 +121,8 @@ class System(object):
import platform
from dvc.exceptions import DvcException
source, link_name = fspath(source), fspath(link_name)
system = platform.system()
try:
if system == "Windows":
@@ -134,7 +140,7 @@ class System(object):
raise DvcException("reflink is not supported")
@staticmethod
def getdirinfo(path):
def _getdirinfo(path):
import ctypes
from ctypes import c_void_p, c_wchar_p, Structure, WinError, POINTER
from ctypes.wintypes import DWORD, HANDLE, BOOL
@@ -206,6 +212,8 @@ class System(object):
@staticmethod
def inode(path):
path = fspath(path)
if System.is_unix():
import ctypes
@@ -216,7 +224,7 @@ class System(object):
inode = ctypes.c_ulong(inode).value
else:
# getdirinfo from ntfsutils works on both files and dirs
info = System.getdirinfo(path)
info = System._getdirinfo(path)
inode = abs(
hash(
(
@@ -267,6 +275,8 @@ class System(object):
@staticmethod
def is_symlink(path):
path = fspath(path)
if System.is_unix():
return os.path.islink(path)
@@ -275,14 +285,16 @@ class System(object):
from winnt import FILE_ATTRIBUTE_REPARSE_POINT
if os.path.lexists(path):
info = System.getdirinfo(path)
info = System._getdirinfo(path)
return info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT
return False
@staticmethod
def is_hardlink(path):
path = fspath(path)
if System.is_unix():
return os.stat(path).st_nlink > 1
info = System.getdirinfo(path)
info = System._getdirinfo(path)
return info.nNumberOfLinks > 1
+37 -18
View File
@@ -3,6 +3,7 @@
from __future__ import unicode_literals
from dvc.utils.compat import str, builtin_str, open, cast_bytes_py2, StringIO
from dvc.utils.compat import fspath
import os
import sys
@@ -45,8 +46,8 @@ def file_md5(fname):
if size >= LARGE_FILE_SIZE:
bar = True
msg = "Computing md5 for a large file {}. This is only done once."
logger.info(msg.format(os.path.relpath(fname)))
name = os.path.relpath(fname)
logger.info(msg.format(relpath(fname)))
name = relpath(fname)
total = 0
with open(fname, "rb") as fobj:
@@ -113,7 +114,9 @@ def dict_md5(d, exclude=[]):
def copyfile(src, dest, no_progress_bar=False, name=None):
"""Copy file with progress bar"""
from dvc.exceptions import DvcException
from dvc.progress import progress
from dvc.system import System
copied = 0
name = name if name else os.path.basename(dest)
@@ -122,15 +125,18 @@ def copyfile(src, dest, no_progress_bar=False, name=None):
if os.path.isdir(dest):
dest = os.path.join(dest, os.path.basename(src))
with open(src, "rb") as fsrc, open(dest, "wb+") as fdest:
while True:
buf = fsrc.read(LOCAL_CHUNK_SIZE)
if not buf:
break
fdest.write(buf)
copied += len(buf)
if not no_progress_bar:
progress.update_target(name, copied, total)
try:
System.reflink(src, dest)
except DvcException:
with open(src, "rb") as fsrc, open(dest, "wb+") as fdest:
while True:
buf = fsrc.read(LOCAL_CHUNK_SIZE)
if not buf:
break
fdest.write(buf)
copied += len(buf)
if not no_progress_bar:
progress.update_target(name, copied, total)
if not no_progress_bar:
progress.finish_target(name)
@@ -151,12 +157,13 @@ def move(src, dst):
def _chmod(func, p, excinfo):
perm = os.lstat(p).st_mode
perm |= stat.S_IWRITE
try:
perm = os.stat(p).st_mode
perm |= stat.S_IWRITE
os.chmod(p, perm)
except OSError as exc:
# NOTE: broken symlink case.
# broken symlink
if exc.errno != errno.ENOENT:
raise
@@ -164,7 +171,7 @@ def _chmod(func, p, excinfo):
def remove(path):
logger.debug("Removing '{}'".format(os.path.relpath(path)))
logger.debug("Removing '{}'".format(relpath(path)))
try:
if os.path.isdir(path):
@@ -227,9 +234,9 @@ def convert_to_unicode(data):
def tmp_fname(fname):
""" Temporary name for a partial download """
from uuid import uuid4
from shortuuid import uuid
return fname + "." + str(uuid4()) + ".tmp"
return fspath(fname) + "." + str(uuid()) + ".tmp"
def current_timestamp():
@@ -279,7 +286,7 @@ def dvc_walk(
def walk_files(directory, ignore_file_handler=None):
for root, _, files in dvc_walk(
str(directory), ignore_file_handler=ignore_file_handler
directory, ignore_file_handler=ignore_file_handler
):
for f in files:
yield os.path.join(root, f)
@@ -363,3 +370,15 @@ def _visual_center(line, width):
right_padding = spaces - left_padding
return (left_padding * " ") + line + (right_padding * " ")
def relpath(path, start=os.curdir):
path = fspath(path)
start = os.path.abspath(fspath(start))
# Windows path on different drive than curdir doesn't have relpath
if os.name == "nt" and not os.path.commonprefix(
[start, os.path.abspath(path)]
):
return path
return os.path.relpath(path, start)

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