Compare commits

..
507 Commits
Author SHA1 Message Date
Ruslan Kuprieiev 373dda6466 dvc: bump to 0.40.1 2019-05-12 00:01:01 +03:00
Ruslan Kuprieiev fc99ebed59 setup: match ruaml version with the one specified in requirements.txt 2019-05-12 00:00:19 +03:00
Ruslan Kuprieiev 750bd822b5 Merge pull request #1985 from efiop/fix-pyinstaller-oss2
pyinstaller: fix build for oss2
2019-05-11 21:55:22 +03:00
Ruslan Kuprieiev 797b356e3a pyinstaller: remove hooks that are already released
We've contributed these hooks a long time ago and they got finally
released, so we can remove our copies.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-05-11 21:44:55 +03:00
Ruslan Kuprieiev aacb0b4c98 pyinstaller: fix build for oss2
This version added support for oss as a remote, which has caused some
issues when building the binary package. To fix those, we need two
pyinstaller hools:

1) hook-Crypto.py
    This one has been added to PyInstaller's upstream, but wasn't
    released yet, so we are using a copy of it for now.

2) hook-aliyunsdkcore.py
    This one is a new one. Sent it to PyInstaller's team in a PR [1].

[1] https://github.com/pyinstaller/pyinstaller/pull/4228

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-05-11 21:35:41 +03:00
Ruslan Kuprieiev ea097780de remote: hdfs: fix typo 2019-05-11 17:33:32 +03:00
Ruslan Kuprieiev dc0c0914b2 Merge pull request #1984 from efiop/fix-version
cmd: use CmdBaseNoRepo for commands without repo
2019-05-11 17:28:15 +03:00
Ruslan Kuprieiev d4ef461fce version: use CmdBaseNoRepo
Currently `dvc version` outside of dvc repo raises "not dvc repo" error,
even though it is not really using repo in any way.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-05-11 17:20:30 +03:00
Ruslan Kuprieiev cc2f28c12b cmd: use CmdBaseNoRepo for commands without repo
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-05-11 17:09:19 +03:00
Ruslan Kuprieiev 3eaa725b01 Merge pull request #1968 from pared/1588
path info: replace path_info dicts with PathInfo class
2019-05-11 15:50:10 +03:00
Ruslan Kuprieiev 5075ea5feb Merge pull request #1982 from algomaster99/update-version-help
Update version help
2019-05-11 14:35:54 +03:00
pared 3ff10005fa path_info: remove dict path_info, introduce PathInfo class 2019-05-10 22:25:50 -07:00
Aman Sharma b3165a224e Update version help 2019-05-11 10:02:39 +05:30
Ruslan Kuprieiev 0a93de4bfe Merge pull request #1981 from mroutis/fix-1975
status: optimize the _fill_statuses method
2019-05-11 05:44:13 +03:00
Mr. Outis a91b198954 status: optimize the _fill_statuses method
Close #1975
2019-05-10 19:20:13 -07:00
Ruslan Kuprieiev f3c66bc866 Merge pull request #1980 from mroutis/fix-1964
install: append commands if hook already exists
2019-05-11 02:43:29 +03:00
Mr. Outis c39c2f087a install: append commands if hook already exists
Close #1964
2019-05-10 16:20:01 -07:00
Ruslan Kuprieiev 45f94e36b2 Merge pull request #1963 from algomaster99/dvc_version
Add `dvc version` command
2019-05-10 22:20:53 +03:00
Aman Sharma 46325808b8 Change regex pattern and move test under unit/command 2019-05-11 18:29:03 +05:30
Ruslan Kuprieiev 1322a5b2d8 Merge pull request #1978 from mroutis/fix-1973
logger: fix bug that ignored the progress bar
2019-05-10 21:25:15 +03:00
Aman Sharma bf9ca598c0 Pass codeclimate test 2019-05-10 19:45:17 +05:30
Jorge Orpinel b09bd0426e Document dvc.scm.git::Git::_get_diff_trees and shorten commit hashes for dvc diff (#1906)
* 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.

* diff: `dvc diff` uses code for `git rev_parse --short` instead of hard `[:7]` (for short commit hashes).

- Tests updated too.

Per https://github.com/iterative/dvc/pull/1906#discussion_r278510340

* tests: Reformatted with `black tests/func/test_diff.py`

* tests: Remove uneccessary `str()` casting form `dvc diff` tests (and reformat)
2019-05-10 15:30:36 +03:00
Aman Sharma db07f3a08d Modify tests to use pytest fixture 2019-05-10 13:00:42 +05:30
Mr. Outis 9978bc87a4 logger: fix bug that ignored the progress bar
Close #1973
2019-05-10 00:17:32 -07:00
Aman Sharma 21388952e9 Write tests for dvc version 2019-05-10 09:31:30 +05:30
Aman Sharma 9e8ad95e6d Change import of DVC version 2019-05-10 09:30:09 +05:30
Aman Sharma 7540fd7b48 Add dvc version command 2019-05-10 09:30:09 +05:30
nanaya-tachibana 7ceaf88c54 remote: add support for aliyun oss (#1961)
* remote: add support for aliyun oss

Usage:
$ dvc remote add myremote oss://my-bucket.endpoint/path
Set key id and key secret using modify command
$ dvc remote modify myremote oss_key_id my-key-id
$ dvc remote modify myremote oss_key_secret my-key-secret
or environment variables
$ export OSS_ACCESS_KEY_ID="my-key-id"
$ export OSS_ACCESS_KEY_SECRET="my-key-secret"

Ref:
oss python SDK: https://www.alibabacloud.com/help/doc-detail/32026.htm

* Not needed, since we don't support external dependencies/outputs on OSS.
See https://github.com/iterative/dvc/pull/1961.

* Add a way to test oss storage using docker.

Start a container running an oss emulator.
$ git clone https://github.com/nanaya-tachibana/oss-emulator.git
$ docker image build -t oss:1.0 oss-emulator
$ docker run --detach -p 8880:8880 --name oss-emulator oss:1.0
Setup environment variables.
$ export OSS_BUCKET='my-bucket'
$ export OSS_ENDPOINT='localhost:8880'
$ export OSS_ACCESS_KEY_ID='AccessKeyID'
$ export OSS_ACCESS_KEY_SECRET='AccessKeySecret'

* Use default key id and key secret when they are not given,
which gives read access to public read bucket and public bucket.

* test: add oss tests to appveyor.

* remove unneeded tests.

* remote: use s3 style url for oss storage and make endpoint a configurable value.

Usage:
$ dvc remote add myremote oss://my-bucket/path
Set key id, key secret and endpoint using modify command
$ dvc remote modify myremote oss_key_id my-key-id
$ dvc remote modify myremote oss_key_secret my-key-secret
$ dvc remote modify myremote oss_endpoint endpoint
or environment variables
$ export OSS_ACCESS_KEY_ID="my-key-id"
$ export OSS_ACCESS_KEY_SECRET="my-key-secret"
$ export OSS_ENDPOINT="endpoint"

* remote: fallback to [] if there are no cinfos

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

* test: remote: add oss CLI test

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

* travis: use iterative's fork of oss-emulator

Just to keep things in-house.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-05-09 15:35:41 +03:00
Ruslan Kuprieiev 6408b58b8d Merge pull request #1965 from khamutov/master
remote: add support for ssh config
2019-05-07 17:00:09 +03:00
khamutov 69a1d965e4 test: ssh: update mock version to 3.0.0 min 2019-05-07 11:28:50 +03:00
Ruslan Kuprieiev 6da0dd6f1c Merge pull request #1966 from msaroufim/master
removed munched sentence in readme
2019-05-07 01:18:30 +03:00
Mark Saroufim 0b6a29a5dc removed munched sentence in readme 2019-05-06 15:01:52 -07:00
khamutov 02eebd48eb test: ssh: add windows support for ssh config 2019-05-06 21:47:58 +03:00
khamutov 65fd7f9c8f test: ssh: add support for python 2.7 in mocking 2019-05-06 21:21:22 +03:00
khamutov a4632092a5 test: ssh: add mock_open compat version 2019-05-06 19:21:21 +03:00
Ruslan Kuprieiev 43a833dec2 dvc: bump to 0.40.0 2019-05-06 16:46:00 +03:00
khamutov 0afa99028a test: ssh: fixes wrong order of mocks 2019-05-06 09:05:39 +03:00
khamutov 2f1d5d49b4 test: ssh: mock ssh config file exists check 2019-05-05 21:09:05 +03:00
khamutov 0ac256cc96 add tests for ssh config 2019-05-05 19:37:49 +03:00
khamutov ce96334587 extract ssh config parsing 2019-05-05 13:20:28 +03:00
khamutov 7059ae3408 remote: add support for ssh config
Fixes #1613
2019-05-04 20:22:05 +03:00
Ruslan Kuprieiev 92cc9f7e6f Merge pull request #1962 from Suor/test-cloud-small-fix
test: skip data cloud cli tests properly, don't say it's passed
2019-05-02 14:39:33 +03:00
Alexander Schepanovski 7943add4c8 test: skip data cloud cli tests properly, don't say passed 2019-05-02 17:47:42 +07:00
Ruslan Kuprieiev 427b48d069 Merge pull request #1960 from pared/fix_dir_info_dumping_bug
remote: base: dump dir_info json with sorted keys
2019-05-02 09:58:02 +03:00
pared 28f7ccfc6c remote: base: dump dir_info json with sorted keys 2019-05-01 23:34:52 -07:00
Mr. Outis 30e38c91dc tests: use UTF-8 when creating metrics files (#1956) 2019-05-01 20:07:37 -07:00
Ruslan Kuprieiev 5da08289da test: ssh: remove test_connection 2019-05-02 05:56:49 +03:00
Ruslan Kuprieiev 61cb9b4af6 Merge pull request #1959 from mroutis/config-schema-refactor
config: implement bool_schema
2019-05-02 04:49:13 +03:00
Mr. Outis 60e83285e1 config: implement bool_schema 💅
Extracted from https://github.com/iterative/dvc/pull/1804/
2019-05-01 17:37:47 -07:00
Ruslan Kuprieiev 4e5ab29200 Merge pull request #1951 from pared/1551
install: add pre-push hook, refactor test
2019-05-01 21:41:01 +03:00
Ruslan Kuprieievandpared ba1463384f Update tests/func/test_install.py
Co-Authored-By: pared <[email protected]>
2019-05-01 11:11:29 -07:00
Ruslan Kuprieiev 1369c65617 Merge pull request #1953 from pared/refactor_test_commit
commit: test: pytest style
2019-05-01 21:10:40 +03:00
Ruslan Kuprieiev 755a8bab06 Merge pull request #1949 from Suor/tests-extra-processes
test: never run updater nor analytics in tests
2019-05-01 15:50:53 +03:00
Alexander Schepanovski 6e0e5b426b test: never run updater nor analytics in tests
Previously that was only the case when running tests via `python
-mtests` wrapper.
2019-05-01 13:48:57 +07:00
pared e88abfa41a test_commit: use pytest 2019-04-30 22:45:58 -07:00
pared a3857fe747 install: add pre-push hook, refactor test 2019-04-30 20:51:09 -07:00
Ruslan Kuprieiev 40ace1702b Merge pull request #1950 from mroutis/fix-1839
status: optimization for already downloaded files
2019-05-01 04:33:10 +03:00
Mr. Outis 29f58bda9b status: optimization for already downloaded files
Fix #1839
2019-04-30 17:09:04 -07:00
Ruslan Kuprieiev 2cba711237 travis: redirect pyenv log to a file 2019-05-01 02:55:29 +03:00
Ruslan Kuprieiev 69a6993233 Merge pull request #1935 from mroutis/fix-1758
add: warn when recursively adding large files
2019-05-01 02:08:44 +03:00
Mr. Outis d3502ebfe3 add: warn when recursively adding large files
Close #1758
2019-04-30 12:33:28 -07:00
Ruslan Kuprieiev 622a089743 Merge pull request #1942 from pared/1941
repo: get default logger level from logger
2019-04-30 21:20:23 +03:00
Ruslan Kuprieiev e99fc2d8e8 Merge pull request #1946 from Suor/pbar-large-dir
dvc: add progress bar when hashing a large dir
2019-04-30 19:15:53 +03:00
Alexander Schepanovski a6823e0113 dvc: add progress bar when hashing a large dir
Added an easier way to show progress bar over for loops.
2019-04-30 20:26:02 +07:00
Ruslan Kuprieiev 8c02cefb9e Merge pull request #1947 from Suor/tests-log-level
test: fix tests being dependent on log level and abandon default DEBUG level
2019-04-30 15:22:35 +03:00
Ruslan Kuprieiev c13dbc69eb Merge pull request #1948 from Suor/dup-tests
test: dedup some metrics tests
2019-04-30 14:14:44 +03:00
Alexander Schepanovski af4af45458 test: dedup some metrics tests
Closes #1928.
2019-04-30 18:01:54 +07:00
Alexander Schepanovski cdf318fff7 test: abandon default DEBUG log level in tests
Now simple `py.test` command won't use DEBUG log level and `py.test ...
--log-level=LEVEL` will do what it's supposed to.

Staging continues to use DEBUG log level.

This is part of #1888 resolution.
2019-04-30 17:43:39 +07:00
Alexander Schepanovski 7d067f78b2 test: fix tests being dependent on log level
Note we call main() to spare process overhead, this leads to main()
inheriting log level and later tests inheriting whatever main() run
left. This makes main to behave.
2019-04-30 17:33:49 +07:00
pared a1b12db00f repo: get default logger level from logger 2019-04-29 17:41:38 -07:00
Ruslan Kuprieiev 8bb21261e3 metrics: show: adjust warning 2019-04-30 01:37:05 +03:00
Ruslan Kuprieiev 3265a259f8 Merge pull request #1937 from pared/1917
metrics: show: do not throw on no metric present on branch
2019-04-30 01:35:02 +03:00
pared b13707cd0a metrics: show: refactor arguments naming 2019-04-29 15:24:07 -07:00
pared 59b4090e64 metrics: show: do not throw on no metric present on branch 2019-04-29 15:16:05 -07:00
Ruslan Kuprieiev 435095e35f Merge pull request #1914 from pared/1890
repo: different cache link types info on long lasting operations
2019-04-29 19:14:27 +03:00
Ruslan Kuprieiev 8a077ba5db Merge pull request #1945 from ag613915cao/replace_yield_fixture
Replace yield_fixture()
2019-04-29 14:48:10 +03:00
ag613915cao 0dabf7bbaa Replace yield_fixture()
Because yield_fixture() is deprecated since pytest 3.0 [1]. It is
not recommend to use the decorator instead of fixture().

[1] https://docs.pytest.org/en/latest/yieldfixture.html
2019-04-29 15:17:10 +07:00
Ruslan Kuprieiev 329385eb80 Merge pull request #1944 from brbarkley/feature_jsonpath-ng
dvc metrics show: implement jsonpath-ng
2019-04-28 16:53:24 +03:00
Brett Barkley 4260a6246d dvc metrics show: implement jsonpath-ng
updated setup.py
updated test_metrics.py::assertMetricsHaveRelativePaths

Fixes #1919
2019-04-28 09:01:28 -04:00
Brett Barkley beef1e32b0 dvc metrics show: implement jsonpath-ng
Fixes #1919
2019-04-28 08:28:36 -04:00
Ivan Shcheklein 27bf6e2928 readme: add donate patreon badge 2019-04-26 12:33:45 -07:00
pared f496ba4f03 remote: local: different cache link types info on long lasting linking 2019-04-26 10:17:16 -07:00
Ruslan Kuprieiev 360e459161 Merge pull request #1939 from Suor/gc-optimize
dvc: some minor optimizations
2019-04-26 19:02:05 +03:00
Alexander Schepanovski cdba077b61 dvc: some minor optimizations 2019-04-26 19:10:08 +07:00
Ruslan Kuprieiev 3a183f5303 Merge pull request #1911 from ag613915cao/enable_ssh_test_on_windows
Mocked SSH server and enable ssh test on windows
2019-04-26 14:24:00 +03:00
ag613915cao fd385c913b This commit intends to:
- Keep current SSH remote connection test on real system
- Faked and Mocked SSH server which running on both Linux and Windows
- Add corresponding tests

Fixed #1704
2019-04-26 18:07:16 +07:00
Ruslan Kuprieiev edefb5470f ssh: respect no_progress_bar on download/upload
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-26 04:45:52 +03:00
Ruslan Kuprieiev 65d31db8db state: remove redundant lines 2019-04-25 16:53:24 +03:00
Alexander Schepanovski 7478e7d4b0 test: clean up test dirs (#1904)
* test: clean up test dirs

Closes #1770. Partially does #1888.

* dvc: close anything opened

* test: finalize git repo explicitly to remove test dir later

This was previously an issue on Windows.
2019-04-25 14:55:39 +03:00
Mr. Outis c0104e2e61 repro: implement downstream option (#1926)
* repro: implement downstream option

Close #1555

* Update scripts/completion/dvc.zsh

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

* Update dvc/command/repro.py

Co-Authored-By: mroutis <[email protected]>
2019-04-25 04:50:44 +03:00
Ruslan Kuprieiev 44e20f1aa8 Merge pull request #1877 from mroutis/fix-1852
repro: persisting outputs invalidates build cache
2019-04-25 02:56:27 +03:00
Mr. Outis d42c8fbd14 repro: persisting outputs invalidates build cache
Fix #1852
2019-04-24 16:45:03 -07:00
Ruslan Kuprieiev 165f6472ca Merge pull request #1925 from efiop/1655
stage: commit cached stage to avoid data duplication
2019-04-24 22:22:48 +03:00
Ruslan Kuprieiev 9c77f2818c stage: commit cached stage to avoid data duplication
Fixes #1655

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-24 21:14:08 +03:00
Ruslan Kuprieiev b8018c16a7 Merge pull request #1892 from efiop/1654
support external ssh directories as dependencies and outputs
2019-04-24 05:57:06 +03:00
Ruslan Kuprieiev 37ee55b1f8 test: test ssh
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-24 05:09:48 +03:00
Ruslan Kuprieiev 49f87a5bff use ValueError instead of JSONDecodeError
JSONDecodeError is only available in never python 3 versions and still
inherits from ValueError, so we could just use it.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-24 05:09:48 +03:00
Ruslan Kuprieiev b97b99cf15 test: func: use pytest for outputs/dependencies
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-24 05:09:48 +03:00
Ruslan Kuprieiev 44b95f23a3 remote: ssh: support directories
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-24 05:09:48 +03:00
Ruslan Kuprieiev 00cfb04fea remote: generalize file/dir handling
Fixes #1654
Fixes #1655

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-24 05:09:48 +03:00
Ruslan Kuprieiev 41b67c3730 move: move directory into an existing directory
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-24 04:56:33 +03:00
Alexander Schepanovski ee8d9a1312 dvc: improve apply_diff() docstring
- explain how it works and why is it needed
- fix typo
2019-04-24 02:45:02 +03:00
Alexander Schepanovski 277b9e566c test: use caplog.at_level() to restore log level after test 2019-04-24 02:45:02 +03:00
pared dd8598e53e tree: walk: operate on tree absolute root 2019-04-23 02:38:23 +03:00
Alexander Schepanovski 8ef52f92ee dvc: add meta key to stages to store user data 2019-04-22 16:26:57 +03:00
ag613915cao 47fb2d3a3b Add test for ssh connection
This commit intends to mock SSH server that not depends on
OS which the test running on for testing purpose.
2019-04-21 04:22:59 +03:00
Alexander Schepanovski 0c867399bb dvc: retain formatting and comments in .dvc files (#1885)
* test: remove never used copy of load_stage_file()

* dvc: save comments and custom formatting in stage files

* Add a couple of TODOs

* test: test repro doesn't clear comments and formatting in .dvc

As part of this I started writing pytest-like tests and added commonly
used fixtures.

* test: simplify and prettify dvc formatting retained test

Also test that line comments after md5 fields are retained.

* test: fix tests

* dvc: fix stage and metrics opens not closed

* dvc: always use utf-8 in stage files

It used to encode in system default encoding. Not sure if PyYAML was
handling this silently or we were just lucky all the way.

* test: unit test apply_diff()

* test: remove TODOs and ASK

* dvc: polish dvc-formatting PR

* dvc: move stage load/dump utils to a new stage utils file

* dvc: clean up apply_diff()

* test: test stage md5 ignores comments

* test: remove pytests starting marker
2019-04-20 01:12:53 +03:00
Ruslan Kuprieiev b057c814ea Merge pull request #1895 from vyloy/master
import: support the '--file' option
2019-04-18 14:18:17 +03:00
vyloy 60c89adeb5 import: use os.path.join instead of hardcode path in the test 2019-04-18 09:25:38 +08:00
vyloy 01a28f2de9 import: improve some details about the '--file' option. 2019-04-18 08:56:14 +08:00
vyloy b07c386d02 import: support the '--file' option 2019-04-18 08:56:14 +08:00
Ruslan Kuprieiev 30573c4546 Merge pull request #1899 from efiop/pkg
pkg: comply with design of other dvc commands
2019-04-18 03:31:21 +03:00
Ruslan Kuprieiev 9618b32fe8 pkg: comply with design of other dvc commands
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-17 23:24:27 +03:00
Ivan Shcheklein 817c8a9fe9 Merge pull request #1897 from shcheklein/m
readme: new logo header
2019-04-16 17:00:42 -07:00
Ivan Shcheklein 0180e7c95b readme: new logo header 2019-04-16 16:57:30 -07:00
Ruslan Kuprieiev 6f11f1f51f test: move func tests to tests/func
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-16 20:19:22 +03:00
Yao Adzaku 6c730dd5cb pipeline: print locked DVC stages (#1882)
* pipeline: add option to display locked DVC stages.

The '--locked' option xplicitly displays only locked DVC stages.

Fixes #1864

* pipeline: support short option '-l' for displaying locked stages

* pipeline: minor cosmetic tweak

* pipeline: reformat code with black

* pipeline: test printing of locked stages

* pipeline: use caplog to ensure command outputs everything as expected

* pipeline: cleanup test

* pipeline: more strict comparison of test output
2019-04-16 16:46:50 +03:00
Paulo Olveira 75b4d0e483 s3: fix for corrupted cache on multipart uploads (#1867)
* s3: fixed wrong etag when copying multipart objects

The etag of multipart objects depends of the number of parts, when copying to the cache we should do so in the same number of parts that the original object was moved/uploaded in.

Fixes part of #1410

* s3: added check on copy for equal etag

* s3: added specific exception for ETag mismatch

* s3: use multipart copy to preserve etags

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

* test: add tests for etag preservation on s3

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

* test: requirements: use dev version moto

Specifically because of this https://github.com/spulec/moto/issues/1941

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

* test: requirements: install dev moto without -e

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

* test: stop using moto

Turned out to be quite buggy.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-15 21:03:22 +03:00
Ruslan Kuprieiev ec9e2c63f2 Update PULL_REQUEST_TEMPLATE.md 2019-04-15 20:59:23 +03:00
Ruslan Kuprieiev dee04ad647 Create PULL_REQUEST_TEMPLATE.md 2019-04-15 20:24:35 +03:00
Ruslan Kuprieiev 30235b3d45 Delete pull_request_template.md 2019-04-15 20:23:41 +03:00
Ruslan Kuprieiev 64e481ec48 github: add PR template with a small checklist 2019-04-15 20:18:04 +03:00
Ruslan Kuprieiev a74e4226a1 Merge pull request #1820 from pared/1499
ignore: introduce .dvcignore
2019-04-15 17:43:21 +03:00
Ruslan Kuprieiev bcd9373ebc Merge pull request #1841 from pared/1257
cache: maker reflink, copy, default links
2019-04-15 17:42:59 +03:00
pared c467ccebcb ignore: introduce .dvcignore, apply when searching for stage files 2019-04-15 14:31:34 +02:00
Ruslan Kuprieiev f20818cd73 Merge pull request #1886 from pared/fix_file_naming_add
add: dont use 'file' as var name during iteration
2019-04-15 14:34:36 +03:00
pared 35360cfecd add: dont use 'file' as var name during iteration 2019-04-15 13:25:06 +02:00
pared 074b6eaa9e test: move corrupted hardlink cache test test_repro -> test_run 2019-04-15 12:55:50 +02:00
pared 9049475412 test_repro: make cache use hardlink 2019-04-15 11:27:44 +02:00
pared e505c38f20 cache: make reflink, copy default links 2019-04-15 11:27:44 +02:00
Ruslan Kuprieiev 8f27c14a8c Merge pull request #1881 from shcheklein/m
diff: cli: add documentation link
2019-04-13 05:07:39 +03:00
Ivan Shcheklein 7785e48d67 diff: cli: add documentation link 2019-04-12 17:37:12 -07:00
Ruslan Kuprieiev c5cb02012f Merge pull request #1879 from maggyero/patch-1
Update README.rst
2019-04-12 18:55:11 +03:00
Géry Ogam a09863afbe Update README.rst 2019-04-12 17:44:03 +02:00
Ruslan Kuprieiev 700199edc0 Merge pull request #1875 from mroutis/try-to-fix-logger-test
tests: be paranoic with caplog
2019-04-12 14:07:47 +03:00
Mr. Outis 1cac81430e tests: be paranoic with caplog 2019-04-11 14:09:02 -05:00
Ruslan Kuprieiev e4290a72e5 dvc: bump to 0.35.7 2019-04-11 18:12:13 +03:00
Ruslan Kuprieiev 7fa869a7f8 Merge pull request #1872 from mroutis/fix-1853
dvc: deprecate --remove-outs
2019-04-11 07:01:22 +03:00
Mr. Outis f417f49c6e dvc: deprecate --remove-outs
Close #1853
2019-04-10 18:58:32 -05:00
Ruslan Kuprieiev d5a3e5db1a Merge pull request #1870 from mroutis/fix-1869
logger: use exception instead of error
2019-04-11 00:35:50 +03:00
Mr. Outis 059aef3ef0 logger: use exception instead of error
The problem was happening because `logger.error` doesn't include any
`exc_info`, and the `parse_exc` logic was previously to rely on the log
record's `exc_info` instead of the global stack trace.

There's still use cases when you want to just `log` an error without
printing the stack trace, those are left untouched.
You can search for them with: `grep -R 'logger.error' dvc/`

---

Fix #1869
2019-04-10 16:26:14 -05:00
Ruslan Kuprieiev 50a8ad5ac3 Merge pull request #1865 from mroutis/expect-dirtyness
version: show when the release version is dirty
2019-04-10 22:09:14 +03:00
Mr. Outis 75e16cdc09 version: show when the release version is dirty
- Silent stderr when calling `git describe` (fix #1866)
2019-04-10 13:14:23 -05:00
Ruslan Kuprieiev c7d0ffc9a4 Merge pull request #1860 from jorgeorpinel/master
Small `dvc diff` command help output changes.
2019-04-10 10:50:08 +03:00
Jorge Orpinel a8e1676400 'till'=>'until' in b_ref arg. help text 2019-04-09 19:40:26 -07:00
Jorge Orpinel fe44062381 Adds space after period. (See previous commit.) 2019-04-09 19:37:19 -07:00
Jorge Orpinel 970828693f Changes ',' for '.' in -t help text. 2019-04-09 19:35:58 -07:00
Ruslan Kuprieiev 5f9c3b695f dvc: bump to 0.35.6 2019-04-09 23:28:01 +03:00
Ruslan Kuprieiev 2d41ca544e Merge pull request #1859 from mroutis/fix-1858
logger: don not log without a message
2019-04-09 23:27:00 +03:00
Mr. Outis a90a3beba0 logger: don not log without a message
Close #1858
2019-04-09 14:59:10 -05:00
Ruslan Kuprieiev 54072d70b5 dvc: bump to 0.35.5 2019-04-09 16:49:14 +03:00
Ruslan Kuprieiev f97727c785 travis: osx: don't install header package 2019-04-09 16:46:33 +03:00
Ruslan Kuprieiev 4953110feb Merge pull request #1855 from mroutis/fix-1614
config: allow cross-referenced remotes
2019-04-09 15:19:41 +03:00
Ruslan Kuprieiev 212f9b7dd4 dvc: bump to 0.35.4 2019-04-09 15:01:35 +03:00
Paweł Redzyński c15fb975b3 Merge pull request #1857 from efiop/1856
checkout/remove: properly handle broken symlinks
2019-04-09 13:52:59 +02:00
Ruslan Kuprieiev 2460dc255e repo: scm_context: re-raise error directly
This way we preserve original traceback.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-09 14:09:54 +03:00
Ruslan Kuprieiev 56b52cc9bf checkout/remove: properly handle broken symlinks
Fixes #1856

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-09 14:09:54 +03:00
Ruslan Kuprieiev 458613b797 travis: osx: debug openssl issue 2019-04-09 09:35:00 +03:00
Mr. Outis d355b1e66e config: allow cross-referenced remotes
Close #1614
2019-04-08 22:10:03 -05:00
Ruslan Kuprieiev 441e4f4745 dvc: bump to 0.35.3 2019-04-08 15:46:31 +03:00
Ruslan Kuprieiev b8e97f5d07 travis: fix xcode version typo 2019-04-08 13:37:53 +03:00
Ruslan Kuprieiev a7d44c7dc3 dvc: bump to 0.35.2 2019-04-08 03:17:08 +03:00
Ruslan Kuprieiev fafad73602 daemon: don't forget to convert to bytes on py2 2019-04-08 03:07:33 +03:00
Ruslan Kuprieiev 3d24bd665c dvc: bump to 0.35.1 2019-04-08 02:43:28 +03:00
Ruslan Kuprieiev 1ef06cd056 Merge pull request #1850 from mroutis/version-refactor
version: privatize version.py methods and vars
2019-04-08 01:15:04 +03:00
Mr. Outis 727bfd955e version: privatize version.py methods and vars 2019-04-07 16:48:34 -05:00
Ruslan Kuprieiev c3b69175e8 Merge pull request #1849 from efiop/version
Version generation improvements
2019-04-07 23:51:33 +03:00
Ruslan Kuprieiev 54f7b43aac dvc: version: detect release
See failed release at [1]. Previously we used to look at TRAVIS_TAG env
var, but now we will try to detect release by matching current tag with
base version.

[1] https://travis-ci.com/iterative/dvc/builds/107365596

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-07 22:55:20 +03:00
Ruslan Kuprieiev 1b6f51cafc version: unused pkg_dir argument
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-07 22:45:27 +03:00
Ruslan Kuprieiev 8b22dcc144 Merge pull request #1848 from efiop/1803
daemon: don't spawn recursively
2019-04-07 21:53:42 +03:00
Ruslan Kuprieiev 7c540da5c4 daemon: don't spawn recursively
Fixes #1803

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-07 20:12:23 +03:00
Ruslan Kuprieiev 0ae525a181 Merge pull request #1847 from hugovk/rm-3.4
Drop support for EOL Python 3.4
2019-04-07 18:51:50 +03:00
Ruslan Kuprieiev 0a837eadfc Merge pull request #1822 from mroutis/pythonic-logger
logger: Use a more _pythonic_ logging
2019-04-07 18:22:31 +03:00
Ruslan Kuprieiev 59b7ab5bce dvc: bump to 0.35.0 2019-04-07 17:49:11 +03:00
Hugo c6d277ecdc Drop support for EOL Python 3.4 2019-04-07 17:41:44 +03:00
Ruslan Kuprieiev 46ddf3abe0 Merge pull request #1845 from hugovk/python_requires
Add python_requires and version classifiers
2019-04-07 17:33:05 +03:00
Hugo 4e43d65b84 Add explicit version classifiers 2019-04-07 17:05:38 +03:00
Hugo 354712e4a4 Add python_requires to help pip 2019-04-07 17:05:18 +03:00
Ruslan Kuprieiev 8c84103ff1 Merge pull request #1844 from rpip/patch-1
Fix typo
2019-04-07 14:27:08 +03:00
Yao Adzaku 3f9260b028 Fix typo 2019-04-07 11:19:37 +00:00
Mr. Outis 921b39dbfe tests: use pytest caplog instead of custom mock
Close #1753
2019-04-06 20:35:30 -06:00
Mr. Outis 85cd344152 logger: Use a more _pythonic_ logging
- Uses a formatter for coloring output
- Move the `progress_aware` decorator logic to the logger itself
- Refactor modules so each one uses their own instance of a logger

Close #1753
2019-04-06 18:19:32 -06:00
Ruslan Kuprieiev 6124a23138 Merge pull request #1831 from mroutis/fix-1824
dvc: refactor package versioning logic
2019-04-07 03:16:39 +03:00
Mr. Outis b4d0d37ac8 dvc: refactor package versioning logic
- Use `version.py` to compute a dynamic version stored in `__version__`.
  Version information was scattered across the source code
  (`VERSION_BASE`, `VERSION`, `__version__`, and `version.py`).

- Read `__version__` from `setup.py` and **pin** it using custom
  `build_py` command class.
  This way, we can maintain consistancy between the build version
  and the runtime one.
  Also, this gives us the ability to run `setup.py` without depending on
  on `dvc` itself.

Close #1824
2019-04-06 18:06:11 -06:00
Ruslan Kuprieiev 2aeb477c35 Merge pull request #1842 from efiop/1835
pkg: import git dynamically
2019-04-07 01:10:36 +03:00
Ruslan Kuprieiev e4ab3773dd pkg: move temp_git_repo to scm/git
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-06 23:29:18 +03:00
Ruslan Kuprieiev 5c008cfcb5 pkg: import git dynamically
This allows dvc to work even if there is no git present, as long as user
is not using `dvc pkg` command directly.

Fixes #1835

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-06 23:29:18 +03:00
Ruslan Kuprieiev 6105e5ead6 pkg: hide from help 2019-04-06 12:09:11 +03:00
Ruslan Kuprieiev 77eab3f1cf Merge pull request #1837 from efiop/is_local
add: check if output is local when generating stage name
2019-04-06 03:35:25 +03:00
Ruslan Kuprieiev 8ef5976ac8 add: check if output is local when generating stage name
When adding a remote file with

```
$ dvc add s3://bucket/path
```

it results in

```
Error: failed to add file - is local is not supported for s3
```

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-06 03:25:53 +03:00
Ruslan Kuprieiev 291fa05955 Merge pull request #1778 from django-kz/master
dvc: add support for dvc diff command
2019-04-05 13:48:12 +03:00
Zhanibek 505b76a2b8 repo: refactoring of dvc/repo/diff.py 2019-04-05 15:02:28 +06:00
Zhanibek a5c04e9296 command: refactoring of _show function in diff.py 2019-04-05 15:02:28 +06:00
Zhanibek fe043ce3a6 repo: refactoring of _get_diff_outs funciton 2019-04-05 15:02:28 +06:00
Zhanibek 585babb636 dvc: some refactoring of code 2019-04-05 15:02:28 +06:00
Zhanibek ca2f4c50ca repo: add checker for out.cheme != "local" case for dvc diff 2019-04-05 15:02:28 +06:00
Zhanibek fcca6aebe8 tests: add tests for CLI message output 2019-04-05 15:02:28 +06:00
Zhanibek f7550c19c0 repo: change FileNotFoundError exception to IOError 2019-04-05 15:02:28 +06:00
Zhanibek 7b3790e701 repo: fix macros placement from dvc/scm to dvc/repo/diff 2019-04-05 15:02:28 +06:00
Zhanibek c3d70f2958 repo: adjust repo.diff API to match dvc diff CLI 2019-04-05 15:02:28 +06:00
Zhanibek e5d62187b3 tests: fix the test for dvc diff command 2019-04-05 15:02:28 +06:00
Zhanibek c235850540 scm: change the order of dvc diff with one argument
Now the dvc diff reference command will return
the difference betweent from specified commit to current HEAD.
2019-04-05 15:02:28 +06:00
Zhanibek 5f68b8e5a0 dvc: replace keys in dictionary with macros in dvc/repo/diff.py 2019-04-05 15:02:28 +06:00
Zhanibek 44e2ff7f83 repo: change the returning value of repo.diff
repo.diff now returns list of dictionaries for each file/directory change
2019-04-05 15:02:28 +06:00
Zhanibek 23b420554f dvc: change help output of diff command and minor fixes 2019-04-05 15:02:28 +06:00
Zhanibek 49dead0474 tests: add CmdDiff command line parsing test 2019-04-05 15:02:28 +06:00
Zhanibek f0a9255ddb tests: fix test_diff with new logic 2019-04-05 15:02:28 +06:00
Zhanibek f0225d85cd dvc: change logic of getting dvc
Getting git snapshots of file/directory using GitTree object
2019-04-05 15:02:28 +06:00
Zhanibek 73b3b57200 test: change some test logic for test_diff.py 2019-04-05 15:02:28 +06:00
Zhanibek 3499c277ff dvc: fix comment style 2019-04-05 15:02:28 +06:00
Zhanibek 1c9d72942b scm: add usage of os.path.normpath 2019-04-05 15:02:28 +06:00
Zhanibek 8b50ef8bb6 repo: replace custom plural func to inflect lib usage 2019-04-05 15:02:28 +06:00
Zhanibek b5251960bf repo: replace custom _convert_size func to humanize lib 2019-04-05 15:02:28 +06:00
Zhanibek 8787995504 dvc: fixes style issues with string ops 2019-04-05 15:02:28 +06:00
Zhanibek 716a5eef7f move print output from repo/diff.py to command/diff.py and minor fixes in command/diff.py 2019-04-05 15:02:28 +06:00
Zhanibek c2639cfb3a dvc: add support for dvc diff command
Some summary statistics regarding the folder/file.
How many files were added/deleted/modified. How size changed.
TO DO things:
1)To  support for detecting diff after `dvc move`
command.
2)To add unit tests.
2019-04-05 15:02:28 +06:00
Ruslan Kuprieiev 2699a47f3e travis: build binary packages only for specific python and os versions
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-04 20:33:39 +03:00
Ruslan Kuprieiev 2a707b706c dvc: bump to 0.34.2 2019-04-04 17:23:42 +03:00
Ruslan Kuprieiev 97b03156d4 Merge pull request #1829 from efiop/1828
dvc: exclude `persist` when computing md5 for the stage
2019-04-04 17:22:36 +03:00
Ruslan Kuprieiev aeff967a13 dvc: exclude persist when computing md5 for the stage
Fixes #1828

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-04 17:12:16 +03:00
Ruslan Kuprieiev 5d41763066 test: requirements: update pytest 2019-04-04 08:27:12 +03:00
Ivan Shcheklein 0fc463094b readme: add missing comma 2019-04-03 21:46:19 -07:00
Ivan Shcheklein a21ee297a1 readme: fix rst italics format 2019-04-03 21:44:40 -07:00
Ivan Shcheklein 2399721d53 readme: emphasize: lfs and makefiles is analogy 2019-04-03 21:43:42 -07:00
Ivan Shcheklein 4ac2ba0989 readme: update description, summary, links 2019-04-03 21:39:20 -07:00
Ivan Shcheklein 55be3ea2f7 readme: use get started link instead of tutorial 2019-04-03 20:57:41 -07:00
Ruslan Kuprieiev 4893266664 dvc: bump to 0.34.1 2019-04-03 22:36:39 +03:00
Ruslan Kuprieiev ce66f97858 Merge pull request #1826 from efiop/pyinstaller
dvc: pyinstaller: fix google.cloud.storage hook
2019-04-03 20:20:08 +03:00
Ruslan Kuprieiev 1c28214728 pyinstaller: hooks: fix google.cloud.storage hook
Fixes #1697

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-03 18:50:15 +03:00
Ruslan Kuprieiev 6ec86e6694 pyinstaller: hook: remove checksumdir hook
That module is not used anymore in dvc.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-03 18:47:28 +03:00
Ruslan Kuprieiev a41705a757 dvc: bump to 0.34.0 2019-04-03 00:06:37 +03:00
Shao-Chun Ma 1a75866e72 metrics: automatic type detection (#1809)
Fixes #1553
2019-04-01 21:22:10 +03:00
Ruslan Kuprieiev cbdb18ff8c Merge pull request #1808 from puhoshville/issue-1760
cli: fix overwriting remotes in `dvc remote add`
2019-03-31 23:00:04 +03:00
Ruslan Kuprieiev b21b55e479 Merge pull request #1817 from pared/1807
checkout: reset progress bar on checkout
2019-03-31 22:56:49 +03:00
pared 0f5216cb14 checkout: reset progress bar on checkout 2019-03-31 21:48:21 +02:00
Ruslan Kuprieiev e99a4799f2 Merge pull request #1815 from efiop/status-missing
status: properly handle STATUS_MISSING case
2019-03-31 21:14:14 +03:00
Ruslan Kuprieiev 3794f364d8 status: properly handle STATUS_MISSING case
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-31 21:04:19 +03:00
Ruslan Kuprieiev bf2d3adcf8 Merge pull request #1810 from efiop/1515
system: reflink: workaround SIP error
2019-03-31 20:24:50 +03:00
Ruslan Kuprieiev c956df2cb0 Merge pull request #1813 from efiop/s3-link
dvc: use http remote for downloading dvc images
2019-03-31 20:24:40 +03:00
Ruslan Kuprieiev 0385dec5c0 dvc: use http remote for downloading dvc images
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-31 20:17:26 +03:00
Ruslan Kuprieiev 4904209fd3 Merge pull request #1812 from AmitAronovitch/azure_sas_fails
azure: do not create existing container
2019-03-31 17:38:13 +03:00
Amit Aronovitch 00b81445a9 azure: do not create existing container 2019-03-31 11:39:39 +03:00
Ruslan Kuprieiev 5109d759f1 system: reflink: workaround SIP error
Fixes #1515

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-30 20:53:06 +02:00
Dmitrii Pukhov 67f4c9329e Fix unnecessary raise, ConfirmRemoveError recover 2019-03-30 21:38:47 +09:00
Ruslan Kuprieievandpuhoshville 180b844f46 Update tests/test_remote.py
test_overwrite: fix case of none-zero result on initial stage

Co-Authored-By: puhoshville <[email protected]>
2019-03-30 21:21:37 +09:00
Dmitrii Pukhov 06052a8f6a cli: fix overwriting remotes in dvc remote add
Now dvc remote add requires user confirmation in case of overwriting of
an already existing remote.

Fixes #1760
2019-03-30 14:48:07 +09:00
Ruslan Kuprieiev b3addbd283 Merge pull request #1733 from mroutis/fix-1716
metrics: shows formatted multiline metrics
2019-03-30 03:14:55 +02:00
Ruslan Kuprieiev 374b75f6c7 Merge pull request #1794 from efiop/dvctags
[RFC] introduce `dvc tag`
2019-03-30 02:13:58 +02:00
Mr. Outis 298770efe9 metrics: shows formatted multiline metrics
Close #1716
2019-03-29 16:24:54 -07:00
Ruslan Kuprieiev c645fdd45e dvc: bump to 0.33.1 2019-03-29 21:51:58 +02:00
Ruslan Kuprieiev 278f817ec7 hadoop: use --force when removing data
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 21:31:22 +02:00
Ruslan Kuprieiev 4c5cb7afae checkout: reorganize "git checkout hint" exception
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 13:44:32 +02:00
Ruslan Kuprieiev c03b1c1693 test: add tests for dvc tag
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 13:44:11 +02:00
Ruslan Kuprieiev 6e4d74282a dvc: temporarily hide dvc tag
This feature is not yet ready for public use.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 13:44:11 +02:00
Ruslan Kuprieiev df971ec32d dvc: introduce dvc tag
Fixes #1766

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 13:36:58 +02:00
Ruslan Kuprieiev 76c15ab80d dvc: repo: unify collect() method
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 12:43:19 +02:00
Ruslan Kuprieiev aaa9167a00 dvc: fix remote.checkout() to follow same conventions
Currently `checkout()` accepts `OutputBase*` class as an argument, which
doesn't follow conventions from all other methods, which accept
path_info and/or checksum_info. To fix that, cache for dir info is moved
from output to the underlying remote.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 12:43:19 +02:00
Ruslan Kuprieiev db11175ad9 dvc: bump to 0.33.0 2019-03-29 03:57:30 +02:00
Ivan Shcheklein 4a9188c0b5 Move man doc links to descriptions, fix some help copies (#1805)
* move man doc links to descriptions, fix some help copies

* minor init force message fix

* deal with ugly inheritance in lock base

* remove unnecessary super calls

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

* Update dvc/remote/local.py

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

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

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

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

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

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

* Extract tmp git repo & fetch data

* Handle fetch\cloud errors and rename select option

* Modify gitignore files when deploy a package

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

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

* Existing tests fixes

* Basic test for packages

* Comment out pkg tests

* Codeclimate improvements

* Reformatting

* Enable pkg tests

* Fix tests no Windows

* Another old bug fix

* Fix encoding issue on Windows

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

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

* gitignorepath: OS specifix path to posix

* Forgotten assignment

* misc: cleanup small issues

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

* cli: pkg: hide from help

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

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

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

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

* Formatted with black, more explanation in GC_HELP

* Fix typo in GC_HELP

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-12 01:40:07 +02:00
Ruslan Kuprieiev c0674dc134 dvc: refactor requirements.txt
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-10 22:42:50 +02:00
Ruslan Kuprieiev 51c9357cd2 dvc: bump to 0.26.0 2019-02-10 21:51:16 +02:00
Ruslan Kuprieiev 9bfff1f957 Merge pull request #1600 from iterative/cached-metrics
Cached metrics
2019-02-10 21:33:04 +02:00
Ivan Shcheklein 41befea210 address PR review comments 2019-02-10 11:06:23 -08:00
Ruslan Kuprieiev fbd5393a2d appveyor: add chocolatey cache 2019-02-10 07:50:51 +02:00
Ivan Shcheklein 8817864e7a fix one more test on windows 2019-02-09 20:04:35 -08:00
Ivan Shcheklein bd28b711f9 minor: remove debug print 2019-02-09 19:58:10 -08:00
Ivan Shcheklein 5563f13273 fix metrics tests on windows 2019-02-09 19:57:41 -08:00
Ivan Shcheklein 36c4a3a5af add metrics tests 2019-02-09 19:00:40 -08:00
Ivan Shcheklein dc1242a107 fix error messages in metrics show 2019-02-09 18:57:15 -08:00
Ivan Shcheklein 17c6858ad3 minor: better variable names in stage class 2019-02-09 18:56:17 -08:00
Ivan Shcheklein dab347f424 fix few checks and messages in dvc run 2019-02-09 18:54:54 -08:00
Ivan Shcheklein 3287c3f407 add ability run tests from specified file(s) 2019-02-09 18:52:46 -08:00
Ivan Shcheklein 0ed44cf151 fix formatting 2019-02-09 15:44:12 -08:00
Ivan Shcheklein 2419f94ef3 use python3 in hook 2019-02-09 15:00:16 -08:00
Ruslan Kuprieiev 1eee7e3226 travis: enable cron builds
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 23:52:15 +02:00
Ivan Shcheklein 16570bad03 add cached metrics, cleanup warnings 2019-02-09 13:48:18 -08:00
Ruslan Kuprieiev fd2f9119e3 Merge pull request #1597 from mroutis/fix-1484
remote: define command error on the base
2019-02-09 22:46:45 +02:00
Ruslan Kuprieiev cbf24cd279 dvc: bump to 0.25.4 2019-02-09 21:07:57 +02:00
Ruslan Kuprieiev 8b35c69a95 dvc: generate version even without gitpython 2019-02-09 21:07:17 +02:00
Mr. Outis ec77bfa1af remote: define command error on the base
fix: #1484
2019-02-09 12:03:17 -06:00
Ruslan Kuprieiev bac1393fc2 Merge pull request #1592 from mroutis/fix-1591
remote: notify when operation is not supported
2019-02-09 08:44:19 +02:00
Ruslan Kuprieiev cb8e24237f windows: gen_config: don't use compat open 2019-02-09 07:31:59 +02:00
Ruslan Kuprieiev 5b548d679c dvc: bump to 0.25.3 2019-02-09 07:04:03 +02:00
Ruslan Kuprieiev 005239ade7 py2: cast env keys to bytes
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 06:59:21 +02:00
Ruslan Kuprieiev b67546d648 dvc: bump to 0.25.2 2019-02-09 04:17:56 +02:00
Ruslan Kuprieiev 40c84fe4ed py2: fix environ issue
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 03:59:45 +02:00
Mr. Outis 0d51d10ccf remote: notify when operation is not supported
close #1591
2019-02-08 19:54:54 -06:00
Ruslan Kuprieiev 9210e66b0c dvc: bump to 0.25.1 2019-02-09 03:06:18 +02:00
Ruslan Kuprieiev d3bb4f85df travis: don't forget to init pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 02:12:17 +02:00
Ruslan Kuprieiev cbce38074e travis: fix typo
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 02:01:52 +02:00
Ruslan Kuprieiev 1bb101d250 travis: debug pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 01:40:58 +02:00
Ruslan Kuprieiev 1d272f156f config: handle run outside of dvc repo
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-09 01:38:49 +02:00
Ruslan Kuprieiev 05bd4d4dc4 travis: use pyenv shell
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 22:20:41 +02:00
Ruslan Kuprieiev e22da1e957 travis: fix typo
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 21:52:13 +02:00
Ruslan Kuprieiev 4f36ce35ad travis: activate pyenv
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-02-08 21:39:15 +02:00
258 changed files with 15110 additions and 6087 deletions
+17 -18
View File
@@ -3,6 +3,10 @@ branches:
- master
- devel
cache:
- C:\ProgramData\chocolatey\bin -> appveyor.yml
- C:\ProgramData\chocolatey\lib -> appveyor.yml
platform: x86
environment:
@@ -16,36 +20,29 @@ environment:
secure: XN4jRtmGE5Bqg8pPZkwNs7kn3UEI73Rkldqc0MGsQISZBm5TNJZOPofDMc1QnUsf
AZURE_STORAGE_CONTAINER_NAME: appveyor-tests
AZURE_STORAGE_CONNECTION_STRING: DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;
OSS_ENDPOINT: localhost:50004
OSS_ACCESS_KEY_ID: AccessKeyID
OSS_ACCESS_KEY_SECRET: AccessKeySecret
matrix:
- PYTHON: "C:\\Python27"
PYTHON_VERSION: "2.7.x" # currently 2.7.9
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:\\Python33"
# PYTHON_VERSION: "3.3.x" # currently 3.3.5
# PYTHON_ARCH: "32"
#
# - PYTHON: "C:\\Python33-x64"
# PYTHON_VERSION: "3.3.x" # currently 3.3.5
# PYTHON_ARCH: "64"
#
# - PYTHON: "C:\\Python34"
# PYTHON_VERSION: "3.4.x" # currently 3.4.3
# PYTHON_ARCH: "32"
#
# - PYTHON: "C:\\Python34-x64"
# PYTHON_VERSION: "3.4.x" # currently 3.4.3
# PYTHON_ARCH: "64"
install:
- ps: Install-Product node
- SET PATH=%RUBY%;%PATH%
- git clone -q https://github.com/nanaya-tachibana/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
- ps: $AzuriteProcess = Start-Process azurite-blob -PassThru
- cinst wget
@@ -65,18 +62,20 @@ before_test:
- aws configure set aws_secret_access_key "%aws_secret_access_key%"
- aws configure set region us-east-2
- openssl enc -d -aes-256-cbc -md md5 -k "%GCP_CREDS%" -in scripts\ci\gcp-creds.json.enc -out scripts\ci\gcp-creds.json & exit 0
- pip install -r test-requirements.txt
- pip install -r tests/requirements.txt
test_script:
- python -mtests
after_test:
- if "%APPVEYOR_PULL_REQUEST_NUMBER%"=="" pip install codecov
- if "%APPVEYOR_PULL_REQUEST_NUMBER%"=="" codecov
- if "%APPVEYOR_REPO_TAG%"=="true" python -m dvc pull
- if "%APPVEYOR_REPO_TAG%"=="true" .\scripts\build_windows.cmd
on_finish:
- ps: Stop-Process $AzuriteProcess
- ps: Stop-Process $OSSProcess
artifacts:
- path: dvc*.exe
+1 -1
View File
@@ -1,4 +1,4 @@
['remote "dvc"']
url = s3://dvc-share/dvc/
url = https://dvc.org/s3/dvc
[core]
remote = dvc
+4 -1
View File
@@ -1,4 +1,7 @@
[flake8]
ignore = E203, E266, E501, W503, F403, F401
ignore =
E203, # Whitespace before ':'
E266, # Too many leading '#' for block comment
W503, # Line break occurred before a binary operator
max-line-length = 79
select = B,C,E,F,W,T4,B9
+9
View File
@@ -0,0 +1,9 @@
* [ ] Have you followed the guidelines in our
[Contributing document](https://dvc.org/doc/user-guide/contributing)?
* [ ] Does your PR affect documented changes or does it add new functionality
that should be documented? If yes, have you created a PR for
[dvc.org](https://github.com/iterative/dvc.org) documenting it or at
least opened an issue for it? If so, please add a link to it.
-----
+3 -2
View File
@@ -24,6 +24,7 @@ innosetup/config.ini
.coverage
dvc/version.py
*.swp
pip-wheel-metadata/
.vscode/
+3 -3
View File
@@ -1,11 +1,11 @@
repos:
- repo: https://github.com/ambv/black
rev: stable
rev: 19.3b0
hooks:
- id: black
language_version: python3.6
language_version: python3
- repo: https://gitlab.com/pycqa/flake8
rev: master
hooks:
- id: flake8
language_version: python3.6
language_version: python3
+22 -10
View File
@@ -3,6 +3,7 @@ cache:
- pip
- directories:
- /opt/python
- $HOME/.pyenv
sudo: true
dist: xenial
python: "3.7"
@@ -21,9 +22,6 @@ matrix:
- name: "2.7"
env:
- PYTHON_VER=2.7.13
- name: "3.4"
env:
- PYTHON_VER=3.4.6
- name: "3.5"
env:
- PYTHON_VER=3.5.3
@@ -50,7 +48,7 @@ matrix:
language: ruby
rvm: 2.4.0
env:
- PYTHON_VER=3.4.3
- PYTHON_VER=3.6.2
- PYTHON_CONFIGURE_OPTS="--enable-framework"
- if: type = cron OR tag is present
os: osx
@@ -60,20 +58,34 @@ matrix:
env:
- PYTHON_VER=3.5.0
- PYTHON_CONFIGURE_OPTS="--enable-framework"
- if: type = cron OR tag is present
os: osx
osx_image: xcode10.1
language: ruby
rvm: 2.4.0
env:
- PYTHON_VER=2.7.10
- CPPFLAGS="-I/usr/local/opt/openssl/include $CPPFLAGS"
- LDFLAGS="-L/usr/local/opt/openssl/lib $LDFLAGS"
- PYTHON_CONFIGURE_OPTS="--enable-framework"
- if: type = cron OR tag is present
os: osx
osx_image: xcode10.1
language: ruby
rvm: 2.4.0
env:
- PYTHON_VER=3.5.0
- PYTHON_CONFIGURE_OPTS="--enable-framework"
before_install:
- bash ./scripts/ci/before_install.sh
- if [[ -n "$PYTHON_VER" ]]; then
eval "$(pyenv init -)";
pyenv install --skip-existing --keep --verbose "$PYTHON_VER";
pyenv shell "$PYTHON_VER";
fi
- source env.sh
- python --version
install:
- bash ./scripts/ci/install.sh
script:
- bash ./scripts/ci/script.sh
deploy:
condition: "[[ $(python -c 'import sys; print(sys.version_info[0])') == '2' ]]"
condition: "[[ $(./scripts/ci/deploy_condition.sh) == 'true' ]]"
provider: releases
api_key:
secure: nyo17gF4Xl6DFL/KVkMewC805Cv+eK99L7SiYCU+WUAYDIzzFL3YJm7fm8bznPeor7DmpO7uSLSveMnJETBtdurW6JWhCTUss4yD66yFzpk6Z5cvHf/1UA5pdMHkkMM54fRaTRt6L2Yal7RPwGEQTBECLXW1Xmf78Y6+0l8lTvXQ9jXOQSRO/63VDKI5Zww2+XytVicUFlr0hLMGKUuaCjjXcQlAPzBgQz3/5Iz6EtzeTf2o+gTPjocbRbSFPikfiBXUn8XlJ8jBj7i114XJRJffw59KxYGDzxp+enfLBULGNoW0Np2hZqkKkPUIdB08C8XVbRXlFYX8cEYBczjqhv8A05p5F5CnltnqQ0KazcoUP2awPVqn68dWNRF+kqrnMBus8euYKIZ4dy/6bVEKJRa84ESJXZVDcwXTAXRqyovSxOlXqg9ZgAImMKBfKn/fpDX2xIvQPaXcnJxKbmcePfdbC1d0HYJ5jkbNaxKE/T15VvhzIho7mJ2WAv1r8QouQUuzqtpzIGbRn/KXHRUzCtyP0k4+eMKMLFOPzg848WsDarP3v8VYiSy4nFY7ZP7DRd3KttvW5bgVbSeO4jPmTgS6TnnpRwual9BJXSG7bjbfpIMii3bNZ7jWL4zpKT9Jfm8w9RGKLcX9HqkZ3+y60+LtYdhEfBvqsIlWQoIJBXg=
+1
View File
@@ -0,0 +1 @@
### See our contribution guide at [dvc.org](https://dvc.org/doc/user-guide/contributing#contributing).
+47 -23
View File
@@ -1,4 +1,4 @@
.. image:: https://dvc.org/static/img/logo-owl-readme.png
.. image:: https://dvc.org/static/img/logo-github-readme.png
:target: https://dvc.org
:alt: DVC logo
@@ -6,7 +6,7 @@
• `Docs <https://dvc.org/doc>`_
• `Twitter <https://twitter.com/iterativeai>`_
• `Chat (Community & Support) <https://dvc.org/chat>`_
• `Tutorial <https://dvc.org/doc/tutorial>`_
• `Tutorial <https://dvc.org/doc/get-started>`_
• `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
.. image:: https://travis-ci.com/iterative/dvc.svg?branch=master
@@ -25,20 +25,25 @@
:target: https://codecov.io/gh/iterative/dvc
:alt: Codecov
.. image:: https://img.shields.io/badge/patreon-donate-green.svg
:target: https://www.patreon.com/DVCorg/overview
:alt: Donate
|
**Data Science Version Control** or **DVC** is an **open-source** tool for data science and
machine learning projects. With a simple and flexible Git-like architecture and interface it
helps data scientists:
machine learning projects. Key features:
#. manage **machine learning models** - versioning, including data sets and transformations (scripts) that were
used to generate models;
#. simple **command line** Git-like experience. Does not require installing and maintaining
any databases. Does not depend on any proprietary online services;
#. make projects **reproducible**;
#. it manages and versions **datasets** and **machine learning models**. Data is saved in
S3, Google cloud, Azure, Alibaba cloud, SSH server, HDFS or even local HDD RAID;
#. make projects **shareable**;
#. it makes projects **reproducible** and **shareable**, it helps answering question how
the model was build;
#. manage experiments with branching and **metrics** tracking;
#. it helps manage experiments with Git tags or branches and **metrics** tracking;
It aims to replace tools like Excel and Docs that are being commonly used as a knowledge repo and
a ledger for the team, ad-hoc scripts to track and move deploy different model versions, ad-hoc
@@ -50,14 +55,30 @@ data file suffixes and prefixes.
How DVC works
=============
DVC is compatible with Git for storing code and the dependency graph (DAG), but not data files cache.
To store and share data files cache DVC supports remotes - any cloud (S3, Azure, Google Cloud, etc) or any on-premise
network storage (via SSH, for example).
We encourage you to read our `Get Started <https://dvc.org/doc/get-started>`_ to better understand what DVC
is and how does it fit your scenarios.
The easiest (but not perfect!) *analogy* to describe it: DVC is Git (or Git-lfs to be precise) + ``makefiles``
made right and tailored specifically for ML and Data Science scenarios.
#. ``Git/Git-lfs`` part - DVC helps you storing and sharing data artifacts, models. It connects them with your
Git repository.
#. ``Makefiles`` part - DVC describes how one data or model artifact was build from another data.
DVC usually runs along with Git. Git is used as usual to store and version code and DVC meta-files. DVC helps
to store data and model files seamlessly out of Git while preserving almost the same user experience as if they
were stored in Git itself. To store and share data files cache DVC supports remotes - any cloud (S3, Azure,
Google Cloud, etc) or any on-premise network storage (via SSH, for example).
.. image:: https://dvc.org/static/img/flow.gif
:target: https://dvc.org/static/img/flow.gif
:alt: how_dvc_works
DVC pipelines (aka computational graph) feature connects code and data together. In a very explicit way you can
specify, run, and save information that a certain command with certain dependencies needs to be run to produce
a model. See the quick start section below or check `Get Started <https://dvc.org/doc/get-started>`_ tutorial to
learn more.
Quick start
===========
@@ -66,7 +87,7 @@ Please read `Get Started <https://dvc.org/doc/get-started>`_ for the full versio
+-----------------------------------+-------------------------------------------------------------------+
| Step | Command |
+===================================+===================================================================+
| Track code and data together | | ``$ git add train.py`` |
| Track data | | ``$ git add train.py`` |
| | | ``$ dvc add images.zip`` |
+-----------------------------------+-------------------------------------------------------------------+
| Connect code and data by commands | | ``$ dvc run -d images.zip -o images/ unzip -q images.zip`` |
@@ -79,27 +100,30 @@ Please read `Get Started <https://dvc.org/doc/get-started>`_ for the full versio
| | | ``$ git commit -m 'The baseline model'`` |
| | | ``$ git push`` |
+-----------------------------------+-------------------------------------------------------------------+
| Share data and ML models | | ``$ dvc remote add myremote s3://mybucket/image_cnn`` |
| | | ``$ dvc config core.remote myremote`` |
| Share data and ML models | | ``$ dvc remote add myremote -d s3://mybucket/image_cnn`` |
| | | ``$ dvc push`` |
+-----------------------------------+-------------------------------------------------------------------+
Installation
============
There are three options to install DVC: ``pip``, Homebrew, or an OS-specific package:
Read this `instruction <https://dvc.org/doc/get-started/install>`_ to get more details. There are three
options to install DVC: ``pip``, Homebrew, or an OS-specific package:
pip (PyPI)
----------
Stable
^^^^^^
.. code-block:: bash
pip install dvc
Development
^^^^^^^^^^^
Depending on the remote storage type you plan to use to keep and share your data, you might need to specify
one of the optional dependencies: ``s3``, ``gs``, ``azure``, ``oss``, ``ssh``. Or ``all_remotes`` to include them all.
The command should look like this: ``pip install dvc[s3]`` - it installs the ``boto3`` library along with
DVC to support the AWS S3 storage.
To install the development version, run:
.. code-block:: bash
pip install git+git://github.com/iterative/dvc
@@ -123,7 +147,7 @@ Package
Self-contained packages for Windows, Linux, Mac are available. The latest version of the packages can be found at
GitHub `releases page <https://github.com/iterative/dvc/releases>`_.
Ubuntu / Debian (apt)
Ubuntu / Debian (deb)
^^^^^^^^^^^^^^^^^^^^^
.. code-block:: bash
@@ -139,7 +163,7 @@ Fedora / CentOS (rpm)
sudo yum update
sudo yum install dvc
Arch linux (AUR)
Arch Linux (AUR)
^^^^^^^^^^^^^^^^
*Unofficial package*, any inquiries regarding the AUR package,
`refer to the maintainer <https://github.com/mroutis/pkgbuilds>`_.
@@ -206,7 +230,7 @@ details.
Mailing List
============
Want to stay up to date? Want to help improve DVC by participating in our ocassional polls? Subscribe to our `mailing list <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_. No spam, really low traffic.
Want to stay up to date? Want to help improve DVC by participating in our occasional polls? Subscribe to our `mailing list <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_. No spam, really low traffic.
Copyright
=========
+18 -61
View File
@@ -6,70 +6,12 @@ Make your data science projects reproducible and shareable.
from __future__ import unicode_literals
import os
from dvc.version import __version__ # noqa: F401
import warnings
import dvc.logger
VERSION_BASE = "0.25.0"
__version__ = VERSION_BASE
PACKAGEPATH = os.path.abspath(os.path.dirname(__file__))
HOMEPATH = os.path.dirname(PACKAGEPATH)
VERSIONPATH = os.path.join(PACKAGEPATH, "version.py")
def _update_version_file():
"""Dynamically update version file."""
from git import Repo
from git.exc import InvalidGitRepositoryError
try:
repo = Repo(HOMEPATH)
except InvalidGitRepositoryError:
return __version__
sha = repo.head.object.hexsha
short_sha = repo.git.rev_parse(sha, short=6)
dirty = ".mod" if repo.is_dirty() else ""
ver = "{}+{}{}".format(__version__, short_sha, dirty)
# Write a helper file, that will be installed with the package
# and will provide a true version of the installed dvc
with open(VERSIONPATH, "w+") as fobj:
fobj.write("# AUTOGENERATED by dvc/__init__.py\n")
fobj.write('version = "{}"\n'.format(ver))
return ver
def _remove_version_file():
"""Remove version.py so that it doesn't get into the release."""
if os.path.exists(VERSIONPATH):
os.unlink(VERSIONPATH)
if os.path.exists(os.path.join(HOMEPATH, "setup.py")):
# dvc is run directly from source without installation or
# __version__ is called from setup.py
if (
os.getenv("APPVEYOR_REPO_TAG", "").lower() != "true"
and os.getenv("TRAVIS_TAG", "") == ""
):
__version__ = _update_version_file()
else: # pragma: no cover
_remove_version_file()
else: # pragma: no cover
# dvc was installed with pip or something. Hopefully we have our
# auto-generated version.py to help us provide a true version
try:
from dvc.version import version
__version__ = version
except Exception:
pass
VERSION = __version__
dvc.logger.setup()
# Ignore numpy's runtime warnings: https://github.com/numpy/numpy/pull/432.
# We don't directly import numpy, but our dependency networkx does, causing
@@ -77,3 +19,18 @@ VERSION = __version__
# we can simply ignore them so that they don't show up when you are using dvc.
warnings.filterwarnings("ignore", message="numpy.dtype size changed")
warnings.filterwarnings("ignore", message="numpy.ufunc size changed")
# Ignore paramiko's warning: https://github.com/paramiko/paramiko/issues/1386.
# This only affects paramiko 2.4.2 and should be fixed in the next version.
# Cryptography developers decided that it is a brilliant idea not to inherit
# from DeprecationWarning [1] because it is invisible by default, and decided
# to spam everyone instead. So it is their fault and not paramiko's.
#
# [1] https://github.com/pyca/cryptography/blob/2.6.1/src/cryptography/
# utils.py#L14
try:
from cryptography.utils import CryptographyDeprecationWarning
warnings.simplefilter("ignore", CryptographyDeprecationWarning)
except ImportError:
pass
+26 -16
View File
@@ -7,9 +7,12 @@ from dvc.utils.compat import str
import os
import json
import errno
import logging
import dvc.logger as logger
from dvc import VERSION
from dvc import __version__
logger = logging.getLogger(__name__)
class Analytics(object):
@@ -93,11 +96,12 @@ class Analytics(object):
with open(self.user_id_file, "r") as fobj:
try:
info = json.load(fobj)
return info[self.PARAM_USER_ID]
except json.JSONDecodeError as exc:
except ValueError as exc:
logger.debug("Failed to load user_id: {}".format(exc))
return None
return info[self.PARAM_USER_ID]
def _get_user_id(self):
from dvc.lock import LockError
@@ -161,19 +165,19 @@ class Analytics(object):
"""Collect analytics report."""
from dvc.scm import SCM
from dvc.utils import is_binary
from dvc.project import Project
from dvc.exceptions import NotDvcProjectError
from dvc.repo import Repo
from dvc.exceptions import NotDvcRepoError
self.info[self.PARAM_DVC_VERSION] = VERSION
self.info[self.PARAM_DVC_VERSION] = __version__
self.info[self.PARAM_IS_BINARY] = is_binary()
self.info[self.PARAM_USER_ID] = self._get_user_id()
self.info[self.PARAM_SYSTEM_INFO] = self._collect_system_info()
try:
scm = SCM(root_dir=Project.find_root())
scm = SCM(root_dir=Repo.find_root())
self.info[self.PARAM_SCM_CLASS] = type(scm).__name__
except NotDvcProjectError:
except NotDvcRepoError:
pass
def collect_cmd(self, args, ret):
@@ -201,11 +205,18 @@ class Analytics(object):
json.dump(self.info, fobj)
return fobj.name
@staticmethod
def _is_enabled_config(config):
from dvc.config import Config
core = config.config.get(Config.SECTION_CORE, {})
return core.get(Config.SECTION_CORE_ANALYTICS, True)
@staticmethod
def _is_enabled(cmd=None):
from dvc.config import Config
from dvc.project import Project
from dvc.exceptions import NotDvcProjectError
from dvc.repo import Repo
from dvc.exceptions import NotDvcRepoError
from dvc.command.daemon import CmdDaemonBase
if os.getenv("DVC_TEST"):
@@ -216,20 +227,19 @@ class Analytics(object):
if cmd is None or not hasattr(cmd, "config"):
try:
dvc_dir = Project.find_dvc_dir()
dvc_dir = Repo.find_dvc_dir()
config = Config(dvc_dir)
assert config is not None
except NotDvcProjectError:
except NotDvcRepoError:
config = Config(validate=False)
assert config is not None
else:
config = cmd.config
assert config is not None
core = config.config.get(Config.SECTION_CORE, {})
enabled = core.get(Config.SECTION_CORE_ANALYTICS, True)
enabled = Analytics._is_enabled_config(config)
logger.debug(
"Analytics is {}abled.".format("en" if enabled else "dis")
"Analytics is {}.".format("enabled" if enabled else "disabled")
)
return enabled
+50 -52
View File
@@ -1,4 +1,4 @@
"""Manages cache of a dvc project."""
"""Manages cache of a dvc repo."""
from __future__ import unicode_literals
@@ -8,86 +8,84 @@ from dvc.config import Config
class Cache(object):
"""Class that manages cache locations of a dvc project.
"""Class that manages cache locations of a dvc repo.
Args:
project (dvc.project.Project): project instance that this cache
belongs to.
repo (dvc.repo.Repo): repo instance that this cache belongs to.
"""
CACHE_DIR = "cache"
def __init__(self, project):
def __init__(self, repo):
from dvc.remote import Remote
self.project = project
self.repo = repo
config = project.config.config[Config.SECTION_CACHE]
config = repo.config.config[Config.SECTION_CACHE]
local = config.get(Config.SECTION_CACHE_LOCAL)
if local:
name = Config.SECTION_REMOTE_FMT.format(local)
sect = project.config.config[name]
settings = repo.config.config[name]
else:
default_cache_dir = os.path.join(project.dvc_dir, self.CACHE_DIR)
default_cache_dir = os.path.join(repo.dvc_dir, self.CACHE_DIR)
cache_dir = config.get(Config.SECTION_CACHE_DIR, default_cache_dir)
cache_type = config.get(Config.SECTION_CACHE_TYPE)
protected = config.get(Config.SECTION_CACHE_PROTECTED)
sect = {
settings = {
Config.PRIVATE_CWD: config.get(
Config.PRIVATE_CWD, project.dvc_dir
Config.PRIVATE_CWD, repo.dvc_dir
),
Config.SECTION_REMOTE_URL: cache_dir,
Config.SECTION_CACHE_TYPE: cache_type,
Config.SECTION_CACHE_PROTECTED: protected,
}
self._local = Remote(project, sect)
self._s3 = self._get_remote(config, Config.SECTION_CACHE_S3)
self._gs = self._get_remote(config, Config.SECTION_CACHE_GS)
self._ssh = self._get_remote(config, Config.SECTION_CACHE_SSH)
self._hdfs = self._get_remote(config, Config.SECTION_CACHE_HDFS)
self._azure = self._get_remote(config, Config.SECTION_CACHE_AZURE)
@property
def local(self):
"""Remote instance for local cache."""
return self._local
@property
def s3(self):
"""Remote instance for AWS S3 cache."""
return self._s3
@property
def gs(self):
"""Remote instance for Google Cloud Storage cache."""
return self._gs
@property
def ssh(self):
"""Remote instance for SSH cache."""
return self._ssh
@property
def hdfs(self):
"""Remote instance for HDFS cache."""
return self._hdfs
@property
def azure(self):
"""Remote instance for azure cache."""
return self._azure
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)
self.hdfs = self._get_remote(config, Config.SECTION_CACHE_HDFS)
self.azure = self._get_remote(config, Config.SECTION_CACHE_AZURE)
def _get_remote(self, config, name):
"""
The config file is stored in a way that allows you to have a
cache for each remote.
This is needed when specifying external outputs
(as they require you to have an external cache location).
Imagine a config file like the following:
['remote "dvc-storage"']
url = ssh://localhost/tmp
ask_password = true
[cache]
ssh = dvc-storage
This method resolves the name under the cache section into the
correct Remote instance.
Args:
config (dict): The cache section on the config file
name (str): Name of the section we are interested in to retrieve
Returns:
remote (dvc.Remote): Remote instance that the section is referring.
None when there's no remote with that name.
Example:
>>> _get_remote(config={'ssh': 'dvc-storage'}, name='ssh')
"""
from dvc.remote import Remote
remote = config.get(name, None)
remote = config.get(name)
if not remote:
return None
name = Config.SECTION_REMOTE_FMT.format(remote)
sect = self.project.config.config[name]
return Remote(self.project, sect)
settings = self.repo.config.get_remote_settings(remote)
return Remote(self.repo, settings)
+16 -3
View File
@@ -5,10 +5,11 @@ from __future__ import unicode_literals
import sys
import argparse
import logging
import dvc.logger as logger
from dvc.command.base import fix_subparsers
import dvc.command.init as init
import dvc.command.pkg as pkg
import dvc.command.destroy as destroy
import dvc.command.remove as remove
import dvc.command.move as move
@@ -29,12 +30,18 @@ import dvc.command.root as root
import dvc.command.lock as lock
import dvc.command.pipeline as pipeline
import dvc.command.daemon as daemon
import dvc.command.commit as commit
import dvc.command.tag as tag
import dvc.command.diff as diff
import dvc.command.version as version
from dvc.exceptions import DvcParserError
from dvc import VERSION
logger = logging.getLogger(__name__)
COMMANDS = [
init,
pkg,
destroy,
add,
remove,
@@ -56,6 +63,10 @@ COMMANDS = [
lock,
pipeline,
daemon,
commit,
tag,
diff,
version,
]
@@ -80,7 +91,9 @@ class VersionAction(argparse.Action): # pragma: no cover
"""Shows dvc version and exits."""
def __call__(self, parser, namespace, values, option_string=None):
print(VERSION)
from dvc import __version__
print(__version__)
sys.exit(0)
+38 -10
View File
@@ -1,25 +1,44 @@
from __future__ import unicode_literals
import dvc.logger as logger
import argparse
import logging
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdAdd(CmdBase):
def run(self):
for target in self.args.targets:
try:
self.project.add(target, recursive=self.args.recursive)
except DvcException:
logger.error("failed to add file")
return 1
try:
if len(self.args.targets) > 1 and self.args.file:
raise DvcException("can't use '--file' with multiple targets")
for target in self.args.targets:
self.repo.add(
target,
recursive=self.args.recursive,
no_commit=self.args.no_commit,
fname=self.args.file,
)
except DvcException:
logger.exception("failed to add file")
return 1
return 0
def add_parser(subparsers, parent_parser):
ADD_HELP = "Add files/directories to dvc."
ADD_HELP = "Take data files or directories under DVC control."
add_parser = subparsers.add_parser(
"add", parents=[parent_parser], description=ADD_HELP, help=ADD_HELP
"add",
parents=[parent_parser],
description=append_doc_link(ADD_HELP, "add"),
help=ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_parser.add_argument(
"-R",
@@ -28,6 +47,15 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Recursively add each file under the directory.",
)
add_parser.add_argument(
"--no-commit",
action="store_true",
default=False,
help="Don't put files/directories into cache.",
)
add_parser.add_argument(
"-f", "--file", help="Specify name of the DVC file it generates."
)
add_parser.add_argument(
"targets", nargs="+", help="Input files/directories."
)
+31 -16
View File
@@ -1,6 +1,10 @@
from __future__ import unicode_literals
import dvc.logger as logger
import colorama
import logging
logger = logging.getLogger(__name__)
def fix_subparsers(subparsers):
@@ -18,14 +22,26 @@ def fix_subparsers(subparsers):
subparsers.dest = "cmd"
def append_doc_link(help_message, path):
if not path:
return help_message
doc_base = "https://man.dvc.org/"
return "{message}\ndocumentation: {blue}{base}{path}{nc}".format(
message=help_message,
base=doc_base,
path=path,
blue=colorama.Fore.CYAN,
nc=colorama.Fore.RESET,
)
class CmdBase(object):
def __init__(self, args):
from dvc.project import Project
from dvc.repo import Repo
self.project = Project()
self.config = self.project.config
self.repo = Repo()
self.config = self.repo.config
self.args = args
self.set_loglevel(args)
@property
def default_targets(self):
@@ -36,25 +52,24 @@ class CmdBase(object):
logger.warning(msg)
return [Stage.STAGE_FILE]
@staticmethod
def set_loglevel(args):
"""Sets log level from CLI arguments."""
if args.quiet:
logger.be_quiet()
elif args.verbose:
logger.be_verbose()
def run_cmd(self):
from dvc.lock import LockError
try:
with self.project.lock:
with self.repo.lock:
return self.run()
except LockError:
logger.error("failed to lock before running a command")
logger.exception("failed to lock before running a command")
return 1
# Abstract methods that have to be implemented by any inheritance class
def run(self):
pass
class CmdBaseNoRepo(CmdBase):
def __init__(self, args):
self.args = args
def run_cmd(self):
return self.run()
+7 -4
View File
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import argparse
from dvc.command.base import fix_subparsers
from dvc.command.base import fix_subparsers, append_doc_link
from dvc.command.remote import CmdRemoteAdd
from dvc.command.config import CmdConfig
@@ -21,11 +21,13 @@ def add_parser(subparsers, parent_parser):
from dvc.command.config import parent_config_parser
CACHE_HELP = "Manage cache settings."
cache_parser = subparsers.add_parser(
"cache",
parents=[parent_parser],
description=CACHE_HELP,
description=append_doc_link(CACHE_HELP, "cache"),
help=CACHE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
cache_subparsers = cache_parser.add_subparsers(
@@ -38,11 +40,13 @@ def add_parser(subparsers, parent_parser):
add_help=False, parents=[parent_config_parser]
)
CACHE_DIR_HELP = "Configure cache directory location."
cache_dir_parser = cache_subparsers.add_parser(
"dir",
parents=[parent_cache_config_parser],
description=CACHE_DIR_HELP,
description=append_doc_link(CACHE_HELP, "cache-dir"),
help=CACHE_DIR_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
cache_dir_parser.add_argument(
"-u",
@@ -53,7 +57,6 @@ def add_parser(subparsers, parent_parser):
)
cache_dir_parser.add_argument(
"value",
nargs="?",
default=None,
help="Path to cache directory. Relative paths are resolved relative "
"to the current directory and saved to config relative to the "
+16 -4
View File
@@ -1,29 +1,34 @@
from __future__ import unicode_literals
from dvc.command.base import CmdBase
import argparse
from dvc.command.base import CmdBase, append_doc_link
class CmdCheckout(CmdBase):
def run(self):
if not self.args.targets:
self.project.checkout(force=self.args.force)
self.repo.checkout(force=self.args.force)
else:
for target in self.args.targets:
self.project.checkout(
self.repo.checkout(
target=target,
with_deps=self.args.with_deps,
force=self.args.force,
recursive=self.args.recursive,
)
return 0
def add_parser(subparsers, parent_parser):
CHECKOUT_HELP = "Checkout data files from cache."
checkout_parser = subparsers.add_parser(
"checkout",
parents=[parent_parser],
description=CHECKOUT_HELP,
description=append_doc_link(CHECKOUT_HELP, "checkout"),
help=CHECKOUT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
checkout_parser.add_argument(
"-d",
@@ -39,5 +44,12 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Do not prompt when removing working directory files.",
)
checkout_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Checkout all subdirectories of the specified directory.",
)
checkout_parser.add_argument("targets", nargs="*", help="DVC files.")
checkout_parser.set_defaults(func=CmdCheckout)
+70
View File
@@ -0,0 +1,70 @@
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 CmdCommit(CmdBase):
def run(self):
if not self.args.targets:
self.args.targets = [None]
for target in self.args.targets:
try:
self.repo.commit(
target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
force=self.args.force,
)
except DvcException:
logger.exception(
"failed to commit{}".format(
(" " + target) if target else ""
)
)
return 1
return 0
def add_parser(subparsers, parent_parser):
COMMIT_HELP = "Save changed data to cache and update DVC files."
commit_parser = subparsers.add_parser(
"commit",
parents=[parent_parser],
description=append_doc_link(COMMIT_HELP, "commit"),
help=COMMIT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
commit_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Commit even if checksums for dependencies/outputs changed.",
)
commit_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Commit all dependencies of the specified target.",
)
commit_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Commit cache for subdirectories of the specified directory.",
)
commit_parser.add_argument(
"targets", nargs="*", default=None, help="DVC files."
)
commit_parser.set_defaults(func=CmdCommit)
+18 -16
View File
@@ -2,23 +2,26 @@ from __future__ import unicode_literals
import os
import argparse
import logging
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.config import Config
from dvc.exceptions import DvcException
class CmdConfig(CmdBase):
def __init__(self, args):
from dvc.project import Project, NotDvcProjectError
logger = logging.getLogger(__name__)
self.args = args
class CmdConfig(CmdBaseNoRepo):
def __init__(self, args):
from dvc.repo import Repo, NotDvcRepoError
super(CmdConfig, self).__init__(args)
try:
dvc_dir = os.path.join(Project.find_root(), Project.DVC_DIR)
dvc_dir = os.path.join(Repo.find_root(), Repo.DVC_DIR)
saved_exc = None
except NotDvcProjectError as exc:
except NotDvcRepoError as exc:
dvc_dir = None
saved_exc = exc
@@ -34,10 +37,7 @@ class CmdConfig(CmdBase):
else:
if dvc_dir is None:
raise saved_exc
self.configobj = self.config._project_config
def run_cmd(self):
return self.run()
self.configobj = self.config._repo_config
def _unset(self, section, opt=None, configobj=None):
if configobj is None:
@@ -47,7 +47,7 @@ class CmdConfig(CmdBase):
self.config.unset(configobj, section, opt)
self.config.save(configobj)
except DvcException:
logger.error("failed to unset '{}'".format(self.args.name))
logger.exception("failed to unset '{}'".format(self.args.name))
return 1
return 0
@@ -55,7 +55,7 @@ class CmdConfig(CmdBase):
try:
self.config.show(self.configobj, section, opt)
except DvcException:
logger.error("failed to show '{}'".format(self.args.name))
logger.exception("failed to show '{}'".format(self.args.name))
return 1
return 0
@@ -64,7 +64,7 @@ class CmdConfig(CmdBase):
self.config.set(self.configobj, section, opt, value)
self.config.save(self.configobj)
except DvcException:
logger.error(
logger.exception(
"failed to set '{}.{}' to '{}'".format(section, opt, value)
)
return 1
@@ -99,11 +99,13 @@ parent_config_parser.add_argument(
def add_parser(subparsers, parent_parser):
CONFIG_HELP = "Get or set config options."
config_parser = subparsers.add_parser(
"config",
parents=[parent_config_parser, parent_parser],
description=CONFIG_HELP,
description=append_doc_link(CONFIG_HELP, "config"),
help=CONFIG_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
config_parser.add_argument(
"-u",
+7 -13
View File
@@ -1,26 +1,20 @@
from __future__ import unicode_literals
from dvc.command.base import CmdBase, fix_subparsers
from dvc.command.base import CmdBaseNoRepo, fix_subparsers
class CmdDaemonBase(CmdBase):
def __init__(self, args):
self.args = args
self.config = None
self.set_loglevel(args)
def run_cmd(self):
return self.run()
class CmdDaemonBase(CmdBaseNoRepo):
pass
class CmdDaemonUpdater(CmdDaemonBase):
def run(self):
import os
from dvc.project import Project
from dvc.repo import Repo
from dvc.updater import Updater
root_dir = Project.find_root()
dvc_dir = os.path.join(root_dir, Project.DVC_DIR)
root_dir = Repo.find_root()
dvc_dir = os.path.join(root_dir, Repo.DVC_DIR)
updater = Updater(dvc_dir)
updater.fetch(detach=False)
@@ -43,7 +37,7 @@ def add_parser(subparsers, parent_parser):
"daemon",
parents=[parent_parser],
description=DAEMON_HELP,
help=DAEMON_HELP,
add_help=False,
)
daemon_subparsers = daemon_parser.add_subparsers(
+39 -16
View File
@@ -1,12 +1,17 @@
from __future__ import unicode_literals
import argparse
import logging
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdDataBase(CmdBase):
UP_TO_DATE_MSG = "Everything is up to date."
def do_run(self, target):
pass
@@ -20,11 +25,16 @@ class CmdDataBase(CmdBase):
ret = 1
return ret
@classmethod
def check_up_to_date(cls, processed_files_count):
if processed_files_count == 0:
logger.info(cls.UP_TO_DATE_MSG)
class CmdDataPull(CmdDataBase):
def do_run(self, target=None):
try:
self.project.pull(
processed_files_count = self.repo.pull(
target=target,
jobs=self.args.jobs,
remote=self.args.remote,
@@ -36,15 +46,16 @@ class CmdDataPull(CmdDataBase):
recursive=self.args.recursive,
)
except Exception:
logger.error("failed to pull data from the cloud")
logger.exception("failed to pull data from the cloud")
return 1
self.check_up_to_date(processed_files_count)
return 0
class CmdDataPush(CmdDataBase):
def do_run(self, target=None):
try:
self.project.push(
processed_files_count = self.repo.push(
target=target,
jobs=self.args.jobs,
remote=self.args.remote,
@@ -55,15 +66,16 @@ class CmdDataPush(CmdDataBase):
recursive=self.args.recursive,
)
except Exception:
logger.error("failed to push data to the cloud")
logger.exception("failed to push data to the cloud")
return 1
self.check_up_to_date(processed_files_count)
return 0
class CmdDataFetch(CmdDataBase):
def do_run(self, target=None):
try:
self.project.fetch(
processed_files_count = self.repo.fetch(
target=target,
jobs=self.args.jobs,
remote=self.args.remote,
@@ -74,8 +86,9 @@ class CmdDataFetch(CmdDataBase):
recursive=self.args.recursive,
)
except Exception:
logger.error("failed to fetch data from the cloud")
logger.exception("failed to fetch data from the cloud")
return 1
self.check_up_to_date(processed_files_count)
return 0
@@ -110,12 +123,14 @@ def add_parser(subparsers, _parent_parser):
from dvc.command.status import CmdDataStatus
# Pull
PULL_HELP = "Pull data files from the cloud."
PULL_HELP = "Pull data files from a DVC remote storage."
pull_parser = subparsers.add_parser(
"pull",
parents=[shared_parent_parser()],
description=PULL_HELP,
description=append_doc_link(PULL_HELP, "pull"),
help=PULL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pull_parser.add_argument(
"-r", "--remote", help="Remote repository to pull from."
@@ -158,12 +173,14 @@ def add_parser(subparsers, _parent_parser):
pull_parser.set_defaults(func=CmdDataPull)
# Push
PUSH_HELP = "Push data files to the cloud."
PUSH_HELP = "Push data files to a DVC remote storage."
push_parser = subparsers.add_parser(
"push",
parents=[shared_parent_parser()],
description=PUSH_HELP,
description=append_doc_link(PUSH_HELP, "push"),
help=PUSH_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
push_parser.add_argument(
"-r", "--remote", help="Remote repository to push to."
@@ -199,12 +216,14 @@ def add_parser(subparsers, _parent_parser):
push_parser.set_defaults(func=CmdDataPush)
# Fetch
FETCH_HELP = "Fetch data files from the cloud."
FETCH_HELP = "Fetch data files from a DVC remote storage."
fetch_parser = subparsers.add_parser(
"fetch",
parents=[shared_parent_parser()],
description=FETCH_HELP,
description=append_doc_link(FETCH_HELP, "fetch"),
help=FETCH_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
fetch_parser.add_argument(
"-r", "--remote", help="Remote repository to fetch from."
@@ -240,13 +259,17 @@ def add_parser(subparsers, _parent_parser):
fetch_parser.set_defaults(func=CmdDataFetch)
# Status
STATUS_HELP = "Show the project status."
STATUS_HELP = (
"Show changed stages, compare local cache and a remote storage."
)
status_parser = subparsers.add_parser(
"status",
parents=[shared_parent_parser()],
description=STATUS_HELP,
description=append_doc_link(STATUS_HELP, "status"),
help=STATUS_HELP,
conflict_handler="resolve",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
status_parser.add_argument(
"-q",
+13 -9
View File
@@ -1,11 +1,16 @@
from __future__ import unicode_literals
import argparse
import logging
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdDestroy(CmdBase):
def run_cmd(self):
try:
@@ -22,23 +27,22 @@ class CmdDestroy(CmdBase):
" Use '-f' to force."
)
self.project.destroy()
self.repo.destroy()
except Exception:
logger.error("failed to destroy DVC")
logger.exception("failed to destroy DVC")
return 1
return 0
def add_parser(subparsers, parent_parser):
DESTROY_HELP = (
"Destroy dvc. Will remove all project's information, "
"data files and cache."
)
DESTROY_HELP = "Remove DVC files, local DVC config and data cache."
destroy_parser = subparsers.add_parser(
"destroy",
parents=[parent_parser],
description=DESTROY_HELP,
description=append_doc_link(DESTROY_HELP, "destroy"),
help=DESTROY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
destroy_parser.add_argument(
"-f",
+160
View File
@@ -0,0 +1,160 @@
from __future__ import unicode_literals
import argparse
import humanize
import inflect
import logging
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
logger = logging.getLogger(__name__)
class CmdDiff(CmdBase):
def _print_size(self, size):
if size < 0:
change = "decreased by {}"
elif size > 0:
change = "increased by {}"
else:
change = "not changed"
natur_size = humanize.naturalsize(abs(size))
return change.format(natur_size)
def _get_md5_string(self, 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):
engine = inflect.engine()
changes_msg = (
"{} {} not changed, {} {} modified, {} {} added, "
"{} {} deleted, size was {}"
)
changes_msg = changes_msg.format(
dct[diff.DIFF_IDENT],
engine.plural("file", dct[diff.DIFF_IDENT]),
dct[diff.DIFF_CHANGE],
engine.plural("file", dct[diff.DIFF_CHANGE]),
dct[diff.DIFF_NEW],
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]),
)
return changes_msg
def _get_file_changes(self, dct):
if (
dct.get(diff.DIFF_OLD_FILE)
and dct.get(diff.DIFF_NEW_FILE)
and dct[diff.DIFF_SIZE] == 0
):
msg = "file size was not changed"
elif dct.get(diff.DIFF_NEW_FILE):
msg = "added file with size {}".format(
humanize.naturalsize(dct[diff.DIFF_SIZE])
)
elif dct.get(diff.DIFF_OLD_FILE):
msg = "deleted file with size {}".format(
humanize.naturalsize(abs(dct[diff.DIFF_SIZE]))
)
else:
msg = "file was modified, file size {}".format(
self._print_size(dct[diff.DIFF_SIZE])
)
return msg
def _get_royal_changes(self, dct):
if dct[diff.DIFF_SIZE] != diff.DIFF_SIZE_UNKNOWN:
if dct.get("is_dir"):
return self._get_dir_changes(dct)
else:
return self._get_file_changes(dct)
return "size is ?"
def _show(self, diff_dct):
msg = "dvc diff from {} to {}".format(
diff_dct[diff.DIFF_A_REF], diff_dct[diff.DIFF_B_REF]
)
if diff_dct.get(diff.DIFF_EQUAL):
logger.info(msg)
return
for dct in diff_dct[diff.DIFF_LIST]:
msg += "\n\ndiff for '{}'\n".format(dct[diff.DIFF_TARGET])
msg += self._get_md5_string(
"-",
dct.get(diff.DIFF_OLD_FILE),
dct.get(diff.DIFF_OLD_CHECKSUM),
)
msg += self._get_md5_string(
"+",
dct.get(diff.DIFF_NEW_FILE),
dct.get(diff.DIFF_NEW_CHECKSUM),
)
msg += "\n"
msg += self._get_royal_changes(dct)
logger.info(msg)
return msg
def run(self):
try:
msg = self.repo.diff(
self.args.a_ref, target=self.args.target, b_ref=self.args.b_ref
)
self._show(msg)
except DvcException:
msg = "failed to get 'diff {}'"
args = " ".join(
compact([self.args.target, self.args.a_ref, self.args.b_ref])
)
msg = msg.format(args)
logger.exception(msg)
return 1
return 0
def add_parser(subparsers, parent_parser):
DIFF_DESCRIPTION = (
"Show diff of a data file or a directory that is under DVC control.\n"
"Some basic statistics summary, how many files were deleted/changed."
)
DIFF_HELP = "Show a diff of a DVC controlled data file or a directory."
diff_parser = subparsers.add_parser(
"diff",
parents=[parent_parser],
description=append_doc_link(DIFF_DESCRIPTION, "diff"),
help=DIFF_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
diff_parser.add_argument(
"-t",
"--target",
default=None,
help=(
"Source path to a data file or directory. Default None. "
"If not specified, compares all files and directories "
"that are under DVC control in the current working space."
),
)
diff_parser.add_argument(
"a_ref", help="Git reference from which diff calculates"
)
diff_parser.add_argument(
"b_ref",
help=(
"Git reference untill which diff calculates, if omitted "
"diff shows the difference between current HEAD and a_ref"
),
nargs="?",
default=None,
)
diff_parser.set_defaults(func=CmdDiff)
+31 -16
View File
@@ -1,10 +1,14 @@
from __future__ import unicode_literals
import argparse
import os
import dvc.prompt as prompt
import dvc.logger as logger
import logging
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdGC(CmdBase):
@@ -19,13 +23,13 @@ class CmdGC(CmdBase):
else:
msg += "all git branches and all git tags"
if self.args.projects is not None and len(self.args.projects) > 0:
msg += " of the current and the following projects:"
if self.args.repos is not None and len(self.args.repos) > 0:
msg += " of the current and the following repos:"
for project_path in self.args.projects:
msg += "\n - %s" % os.path.abspath(project_path)
for repo_path in self.args.repos:
msg += "\n - %s" % os.path.abspath(repo_path)
else:
msg += " of the current project."
msg += " of the current repo."
logger.warning(msg)
@@ -33,36 +37,44 @@ class CmdGC(CmdBase):
if not self.args.force and not prompt.confirm(msg):
return 1
self.project.gc(
self.repo.gc(
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
cloud=self.args.cloud,
remote=self.args.remote,
force=self.args.force,
jobs=self.args.jobs,
projects=self.args.projects,
repos=self.args.repos,
)
return 0
def add_parser(subparsers, parent_parser):
GC_HELP = "Collect garbage."
GC_HELP = "Collect unused data from DVC cache or a remote storage."
GC_DESCRIPTION = (
"Deletes all files in the cache or a remote which are not in\n"
"use by the specified git references (defaults to just HEAD)."
)
gc_parser = subparsers.add_parser(
"gc", parents=[parent_parser], description=GC_HELP, help=GC_HELP
"gc",
parents=[parent_parser],
description=append_doc_link(GC_DESCRIPTION, "gc"),
help=GC_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
gc_parser.add_argument(
"-a",
"--all-branches",
action="store_true",
default=False,
help="Collect garbage for all branches.",
help="Keep data files for the tips of all git branches.",
)
gc_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Collect garbage for all tags.",
help="Keep data files for all git tags.",
)
gc_parser.add_argument(
"-c",
@@ -72,14 +84,14 @@ def add_parser(subparsers, parent_parser):
help="Collect garbage in remote repository.",
)
gc_parser.add_argument(
"-r", "--remote", help="Remote repository to collect garbage in."
"-r", "--remote", help="Remote storage to collect garbage in."
)
gc_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force garbage collection.",
help="Force garbage collection - automatically agree to all prompts.",
)
gc_parser.add_argument(
"-j",
@@ -91,9 +103,12 @@ def add_parser(subparsers, parent_parser):
gc_parser.add_argument(
"-p",
"--projects",
dest="repos",
type=str,
nargs="*",
default=None,
help="Collect garbage for all given projects.",
help="Keep data files required by these projects "
"in addition to the current one. "
"Useful if you share a single cache across repos.",
)
gc_parser.set_defaults(func=CmdGC)
+29 -17
View File
@@ -1,11 +1,15 @@
from __future__ import unicode_literals
import argparse
import os
import logging
import dvc.logger as logger
from dvc.utils.compat import urlparse
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdImport(CmdBase):
@@ -15,9 +19,11 @@ class CmdImport(CmdBase):
out = self.args.out or default_out
self.project.imp(self.args.url, out, self.args.resume)
self.repo.imp(
self.args.url, out, self.args.resume, fname=self.args.file
)
except DvcException:
logger.error(
logger.exception(
"failed to import {}. You could also try downloading "
"it manually and adding it with `dvc add` command.".format(
self.args.url
@@ -28,25 +34,26 @@ class CmdImport(CmdBase):
def add_parser(subparsers, parent_parser):
IMPORT_HELP = "Import files from URL."
IMPORT_HELP = "Download or copy files from URL and take under DVC control."
import_parser = subparsers.add_parser(
"import",
parents=[parent_parser],
description=IMPORT_HELP,
description=append_doc_link(IMPORT_HELP, "import"),
help=IMPORT_HELP,
formatter_class=argparse.RawTextHelpFormatter,
)
import_parser.add_argument(
"url",
help="URL. Supported urls: "
"'/path/to/file', "
"'C:\\\\path\\to\\file', "
"'https://example.com/path/to/file', "
"'s3://bucket/path/to/file', "
"'gs://bucket/path/to/file', "
"'hdfs://example.com/path/to/file', "
"'ssh://example.com:/path/to/file', "
"'remote://myremote/path/to/file'(see "
"`dvc remote` commands). ",
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",
@@ -54,5 +61,10 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Resume previously started download.",
)
import_parser.add_argument("out", nargs="?", help="Output.")
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=CmdImport)
+27 -13
View File
@@ -1,33 +1,44 @@
from __future__ import unicode_literals
import dvc.logger as logger
import argparse
import logging
from dvc.command.base import CmdBaseNoRepo, append_doc_link
class CmdInit(object):
def __init__(self, args):
self.args = args
logger = logging.getLogger(__name__)
def run_cmd(self):
from dvc.project import Project, InitError
class CmdInit(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
from dvc.exceptions import InitError
try:
self.project = Project.init(
self.repo = Repo.init(
".", no_scm=self.args.no_scm, force=self.args.force
)
self.config = self.project.config
self.config = self.repo.config
except InitError:
logger.error("failed to initiate dvc")
logger.exception("failed to initiate dvc")
return 1
return 0
def add_parser(subparsers, parent_parser):
"""Setup parser for `dvc init`."""
INIT_HELP = (
"Initialize dvc over a directory " "(should already be a git dir)."
INIT_HELP = "Initialize DVC in the current directory."
INIT_DESCRIPTION = (
"Initialize DVC in the current directory. Expects directory\n"
"to be a Git repository unless --no-scm option is specified."
)
init_parser = subparsers.add_parser(
"init", parents=[parent_parser], description=INIT_HELP, help=INIT_HELP
"init",
parents=[parent_parser],
description=append_doc_link(INIT_DESCRIPTION, "init"),
help=INIT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
init_parser.add_argument(
"--no-scm",
@@ -41,6 +52,9 @@ def add_parser(subparsers, parent_parser):
"--force",
action="store_true",
default=False,
help="Overwrite '.dvc' if it exists. Will remove all local cache.",
help=(
"Overwrite existing '.dvc' directory. "
"This operation removes local cache."
),
)
init_parser.set_defaults(func=CmdInit)
+12 -6
View File
@@ -1,25 +1,31 @@
from __future__ import unicode_literals
import dvc.logger as logger
from dvc.command.base import CmdBase
import argparse
import logging
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdInstall(CmdBase):
def run_cmd(self):
try:
self.project.install()
self.repo.install()
except Exception:
logger.error("failed to install dvc hooks")
logger.exception("failed to install dvc hooks")
return 1
return 0
def add_parser(subparsers, parent_parser):
INSTALL_HELP = "Install dvc hooks into the repository."
INSTALL_HELP = "Install DVC git hooks into the repository."
install_parser = subparsers.add_parser(
"install",
parents=[parent_parser],
description=INSTALL_HELP,
description=append_doc_link(INSTALL_HELP, "install"),
help=INSTALL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
install_parser.set_defaults(func=CmdInstall)
+19 -9
View File
@@ -1,17 +1,22 @@
from __future__ import unicode_literals
import dvc.logger as logger
import argparse
import logging
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdLockBase(CmdBase):
def run(self, unlock=False):
def _run(self, unlock):
for target in self.args.targets:
try:
self.project.lock_stage(target, unlock=unlock)
self.repo.lock_stage(target, unlock=unlock)
except DvcException:
logger.error(
logger.exception(
"failed to {}lock '{}'".format(
"un" if unlock else "", target
)
@@ -23,18 +28,22 @@ class CmdLockBase(CmdBase):
class CmdLock(CmdLockBase):
def run(self):
return super(CmdLock, self).run(False)
return self._run(False)
class CmdUnlock(CmdLockBase):
def run(self):
return super(CmdUnlock, self).run(True)
return self._run(True)
def add_parser(subparsers, parent_parser):
LOCK_HELP = "Lock DVC file."
lock_parser = subparsers.add_parser(
"lock", parents=[parent_parser], description=LOCK_HELP, help=LOCK_HELP
"lock",
parents=[parent_parser],
description=append_doc_link(LOCK_HELP, "lock"),
help=LOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
lock_parser.add_argument("targets", nargs="+", help="DVC files.")
lock_parser.set_defaults(func=CmdLock)
@@ -43,8 +52,9 @@ def add_parser(subparsers, parent_parser):
unlock_parser = subparsers.add_parser(
"unlock",
parents=[parent_parser],
description=UNLOCK_HELP,
description=append_doc_link(UNLOCK_HELP, "unlock"),
help=UNLOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unlock_parser.add_argument("targets", nargs="+", help="DVC files.")
unlock_parser.set_defaults(func=CmdUnlock)
+79 -66
View File
@@ -1,34 +1,44 @@
from __future__ import unicode_literals
import dvc.logger as logger
import argparse
import logging
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
logger = logging.getLogger(__name__)
def show_metrics(metrics, all_branches=False, all_tags=False):
"""
Args:
metrics (list): Where each element is either a `list`
if an xpath was specified, otherwise a `str`
"""
for branch, val in metrics.items():
if all_branches or all_tags:
logger.info("{branch}:".format(branch=branch))
for fname, metric in val.items():
lines = metric if type(metric) is list else metric.splitlines()
if len(lines) > 1:
logger.info("\t{fname}:".format(fname=fname))
for line in lines:
logger.info("\t\t{content}".format(content=line))
else:
logger.info("\t{}: {}".format(fname, metric))
class CmdMetricsShow(CmdBase):
def run(self):
typ = self.args.type
xpath = self.args.xpath
try:
# backward compatibility
if self.args.json_path:
typ = "json"
xpath = self.args.json_path
elif self.args.tsv_path:
typ = "tsv"
xpath = self.args.tsv_path
elif self.args.htsv_path:
typ = "htsv"
xpath = self.args.htsv_path
elif self.args.csv_path:
typ = "csv"
xpath = self.args.csv_path
elif self.args.hcsv_path:
typ = "hcsv"
xpath = self.args.hcsv_path
else:
typ = self.args.type
xpath = self.args.xpath
self.project.metrics_show(
metrics = self.repo.metrics.show(
self.args.path,
typ=typ,
xpath=xpath,
@@ -36,8 +46,10 @@ class CmdMetricsShow(CmdBase):
all_tags=self.args.all_tags,
recursive=self.args.recursive,
)
show_metrics(metrics, self.args.all_branches, self.args.all_tags)
except DvcException:
logger.error("failed to show metrics")
logger.exception("failed to show metrics")
return 1
return 0
@@ -46,11 +58,11 @@ class CmdMetricsShow(CmdBase):
class CmdMetricsModify(CmdBase):
def run(self):
try:
self.project.metrics_modify(
self.repo.metrics.modify(
self.args.path, typ=self.args.type, xpath=self.args.xpath
)
except DvcException:
logger.error("failed to modify metrics")
logger.exception("failed to modify metric file settings")
return 1
return 0
@@ -59,12 +71,12 @@ class CmdMetricsModify(CmdBase):
class CmdMetricsAdd(CmdBase):
def run(self):
try:
self.project.metrics_add(
self.repo.metrics.add(
self.args.path, self.args.type, self.args.xpath
)
except DvcException:
msg = "failed to add metric file '{}'".format(self.args.path)
logger.error(msg)
logger.exception(msg)
return 1
return 0
@@ -73,60 +85,55 @@ class CmdMetricsAdd(CmdBase):
class CmdMetricsRemove(CmdBase):
def run(self):
try:
self.project.metrics_remove(self.args.path)
self.repo.metrics.remove(self.args.path)
except DvcException:
msg = "failed to remove metric file '{}'".format(self.args.path)
logger.error(msg)
logger.exception(msg)
return 1
return 0
def add_parser(subparsers, parent_parser):
METRICS_HELP = "Get metrics from all branches."
METRICS_HELP = "Commands to add, manage, collect and display metrics."
metrics_parser = subparsers.add_parser(
"metrics",
parents=[parent_parser],
description=METRICS_HELP,
description=append_doc_link(METRICS_HELP, "metrics"),
help=METRICS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_subparsers = metrics_parser.add_subparsers(
dest="cmd",
help="Use dvc metrics CMD --help for command-specific help.",
help="Use dvc metrics CMD --help to display command-specific help.",
)
fix_subparsers(metrics_subparsers)
METRICS_SHOW_HELP = "Show metrics."
METRICS_SHOW_HELP = "Output metric values."
metrics_show_parser = metrics_subparsers.add_parser(
"show",
parents=[parent_parser],
description=METRICS_SHOW_HELP,
description=append_doc_link(METRICS_SHOW_HELP, "metrics-show"),
help=METRICS_SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_show_parser.add_argument(
"path", nargs="?", help="Path to metrics file or directory"
"path", nargs="?", help="Path to a metric file or a directory."
)
metrics_show_parser.add_argument(
"-t", "--type", help="Type of metrics(RAW/JSON/TSV/HTSV/CSV/HCSV)."
"-t",
"--type",
help=(
"Type of metrics (json/tsv/htsv/csv/hcsv). "
"It can be detected by the file extension automatically. "
"Unsupported types will be treated as raw."
),
)
metrics_show_parser.add_argument(
"-x", "--xpath", help="JSON/TSV/HTSV/CSV/HCSV path."
)
metrics_show_group = metrics_show_parser.add_mutually_exclusive_group()
metrics_show_group.add_argument("--json-path", help="JSON path.")
metrics_show_group.add_argument(
"--tsv-path", help="TSV path 'row,column' (e.g. '1,2')."
)
metrics_show_group.add_argument(
"--htsv-path", help="Headed TSV path 'row,column (e.g. 'Name,3')."
)
metrics_show_group.add_argument(
"--csv-path", help="CSV path 'row,column' (e.g. '1,2')."
)
metrics_show_group.add_argument(
"--hcsv-path", help="Headed CSV path 'row,column' (e.g. 'Name,3')."
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
)
metrics_show_parser.add_argument(
"-a",
@@ -147,48 +154,54 @@ def add_parser(subparsers, parent_parser):
"--recursive",
action="store_true",
default=False,
help="If path is a directory, recursively search and process metric files in path.",
help=(
"If path is a directory, recursively search and process metric "
"files in path."
),
)
metrics_show_parser.set_defaults(func=CmdMetricsShow)
METRICS_ADD_HELP = "Add metrics."
METRICS_ADD_HELP = "Tag file as a metric file."
metrics_add_parser = metrics_subparsers.add_parser(
"add",
parents=[parent_parser],
description=METRICS_ADD_HELP,
description=append_doc_link(METRICS_ADD_HELP, "metrics-add"),
help=METRICS_ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_add_parser.add_argument(
"-t", "--type", help="Type of metrics(RAW/JSON/TSV/HTSV/CSV/HCSV)."
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
)
metrics_add_parser.add_argument(
"-x", "--xpath", help="JSON/TSV/HTSV/CSV/HCSV path."
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
)
metrics_add_parser.add_argument("path", help="Path to metrics file.")
metrics_add_parser.add_argument("path", help="Path to a metric file.")
metrics_add_parser.set_defaults(func=CmdMetricsAdd)
METRICS_MODIFY_HELP = "Modify metrics."
METRICS_MODIFY_HELP = "Modify metric file options."
metrics_modify_parser = metrics_subparsers.add_parser(
"modify",
parents=[parent_parser],
description=METRICS_MODIFY_HELP,
description=append_doc_link(METRICS_MODIFY_HELP, "metrics-modify"),
help=METRICS_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_modify_parser.add_argument(
"-t", "--type", help="Type of metrics(RAW/JSON/TSV/HTSV/CSV/HCSV)."
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
)
metrics_modify_parser.add_argument(
"-x", "--xpath", help="JSON/TSV/HTSV/CSV/HCSV path."
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
)
metrics_modify_parser.add_argument("path", help="Metrics file.")
metrics_modify_parser.add_argument("path", help="Path to a metric file.")
metrics_modify_parser.set_defaults(func=CmdMetricsModify)
METRICS_REMOVE_HELP = "Remove metrics."
METRICS_REMOVE_HELP = "Remove files's metric tag."
metrics_remove_parser = metrics_subparsers.add_parser(
"remove",
parents=[parent_parser],
description=METRICS_REMOVE_HELP,
description=append_doc_link(METRICS_REMOVE_HELP, "metrics-remove"),
help=METRICS_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_remove_parser.add_argument("path", help="Path to metrics file.")
metrics_remove_parser.add_argument("path", help="Path to a metric file.")
metrics_remove_parser.set_defaults(func=CmdMetricsRemove)
+20 -10
View File
@@ -1,32 +1,42 @@
from __future__ import unicode_literals
import dvc.logger as logger
import argparse
import logging
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdMove(CmdBase):
def run(self):
try:
self.project.move(self.args.src, self.args.dst)
self.repo.move(self.args.src, self.args.dst)
except DvcException:
msg = "failed to move '{}' -> '{}'".format(
self.args.src, self.args.dst
)
logger.error(msg)
logger.exception(msg)
return 1
return 0
def add_parser(subparsers, parent_parser):
description = (
"Rename or move a data file or a directory that "
"is under DVC control. It renames and modifies "
"the corresponding DVC file to reflect the changes."
MOVE_HELP = "Rename or move a DVC controlled data file or a directory."
MOVE_DESCRIPTION = (
"Rename or move a DVC controlled data file or a directory.\n"
"It renames and modifies the corresponding DVC file to reflect the"
" changes."
)
help = "Rename or move a DVC controlled data file or a directory."
move_parser = subparsers.add_parser(
"move", parents=[parent_parser], description=description, help=help
"move",
parents=[parent_parser],
description=append_doc_link(MOVE_DESCRIPTION, "move"),
help=MOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
move_parser.add_argument(
"src", help="Source path to a data file or directory."
+78 -21
View File
@@ -2,24 +2,32 @@ from __future__ import unicode_literals
from dvc.utils.compat import str
import argparse
import os
import logging
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
logger = logging.getLogger(__name__)
class CmdPipelineShow(CmdBase):
def _show(self, target, commands, outs):
def _show(self, target, commands, outs, locked):
import networkx
from dvc.stage import Stage
stage = Stage.load(self.project, target)
G = self.project.graph()[0]
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.project.root_dir)
node = os.path.relpath(stage.path, self.repo.root_dir)
nodes = networkx.dfs_postorder_nodes(G, node)
for n in networkx.dfs_postorder_nodes(G, node):
if locked:
nodes = [n for n in nodes if stages[n].locked]
for n in nodes:
if commands:
logger.info(stages[n].cmd)
elif outs:
@@ -32,11 +40,11 @@ class CmdPipelineShow(CmdBase):
import networkx
from dvc.stage import Stage
stage = Stage.load(self.project, target)
node = os.path.relpath(stage.path, self.project.root_dir)
stage = Stage.load(self.repo, target)
node = os.path.relpath(stage.path, self.repo.root_dir)
pipelines = list(
filter(lambda g: node in g.nodes(), self.project.pipelines())
filter(lambda g: node in g.nodes(), self.repo.pipelines())
)
assert len(pipelines) == 1
@@ -71,30 +79,54 @@ class CmdPipelineShow(CmdBase):
else:
edges.append((from_stage.relpath, to_stage.relpath))
return nodes, edges
return nodes, edges, networkx.is_tree(G)
def _show_ascii(self, target, commands, outs):
from dvc.dagascii import draw
nodes, edges = self.__build_graph(target, commands, outs)
nodes, edges, _ = self.__build_graph(target, commands, outs)
if not nodes:
return
draw(nodes, edges)
def _show_dependencies_tree(self, target, commands, outs):
from treelib import Tree
nodes, edges, is_tree = self.__build_graph(target, commands, outs)
if not nodes:
return
if not is_tree:
raise DvcException(
"DAG is not a tree, can not print it in tree-structure way, "
"please use --ascii instead"
)
tree = Tree()
tree.create_node(target, target) # Root node
observe_list = [target]
while len(observe_list) > 0:
current_root = observe_list[0]
for edge in edges:
if edge[0] == current_root:
tree.create_node(edge[1], edge[1], parent=current_root)
observe_list.append(edge[1])
observe_list.pop(0)
tree.show()
def __write_dot(self, target, commands, outs, filename):
import networkx
from networkx.drawing.nx_pydot import write_dot
_, edges = self.__build_graph(target, commands, outs)
_, edges, _ = self.__build_graph(target, commands, outs)
edges = [edge[::-1] for edge in edges]
simple_g = networkx.DiGraph()
simple_g.add_edges_from(edges)
write_dot(simple_g, filename)
def run(self, unlock=False):
def run(self):
if not self.args.targets:
self.args.targets = self.default_targets
@@ -111,11 +143,20 @@ class CmdPipelineShow(CmdBase):
self.args.outs,
self.args.dot,
)
elif self.args.tree:
self._show_dependencies_tree(
target, self.args.commands, self.args.outs
)
else:
self._show(target, self.args.commands, self.args.outs)
self._show(
target,
self.args.commands,
self.args.outs,
self.args.locked,
)
except DvcException:
msg = "failed to show pipeline for '{}'".format(target)
logger.error(msg)
logger.exception(msg)
return 1
return 0
@@ -124,7 +165,7 @@ class CmdPipelineList(CmdBase):
def run(self):
import networkx
pipelines = self.project.pipelines()
pipelines = self.repo.pipelines()
for p in pipelines:
stages = networkx.get_node_attributes(p, "stage")
for stage in stages:
@@ -137,12 +178,13 @@ class CmdPipelineList(CmdBase):
def add_parser(subparsers, parent_parser):
PIPELINE_HELP = "Manage pipeline."
PIPELINE_HELP = "Manage pipelines."
pipeline_parser = subparsers.add_parser(
"pipeline",
parents=[parent_parser],
description=PIPELINE_HELP,
description=append_doc_link(PIPELINE_HELP, "pipeline"),
help=PIPELINE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_subparsers = pipeline_parser.add_subparsers(
@@ -156,8 +198,9 @@ def add_parser(subparsers, parent_parser):
pipeline_show_parser = pipeline_subparsers.add_parser(
"show",
parents=[parent_parser],
description=PIPELINE_SHOW_HELP,
description=append_doc_link(PIPELINE_SHOW_HELP, "pipeline-show"),
help=PIPELINE_SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_show_group = pipeline_show_parser.add_mutually_exclusive_group()
pipeline_show_group.add_argument(
@@ -174,6 +217,13 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Print output files instead of paths to DVC files.",
)
pipeline_show_parser.add_argument(
"-l",
"--locked",
action="store_true",
default=False,
help="Print locked DVC stages",
)
pipeline_show_parser.add_argument(
"--ascii",
action="store_true",
@@ -183,6 +233,12 @@ def add_parser(subparsers, parent_parser):
pipeline_show_parser.add_argument(
"--dot", help="Write DAG in .dot format."
)
pipeline_show_parser.add_argument(
"--tree",
action="store_true",
default=False,
help="Output DAG as Dependencies Tree.",
)
pipeline_show_parser.add_argument(
"targets", nargs="*", help="DVC files. 'Dvcfile' by default."
)
@@ -192,7 +248,8 @@ def add_parser(subparsers, parent_parser):
pipeline_list_parser = pipeline_subparsers.add_parser(
"list",
parents=[parent_parser],
description=PIPELINE_LIST_HELP,
description=append_doc_link(PIPELINE_LIST_HELP, "pipeline-list"),
help=PIPELINE_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_list_parser.set_defaults(func=CmdPipelineList)
+89
View File
@@ -0,0 +1,89 @@
from __future__ import unicode_literals
import argparse
import 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)
+41 -16
View File
@@ -1,13 +1,16 @@
from __future__ import unicode_literals
import argparse
import os
import re
import logging
import dvc.logger as logger
from dvc.command.base import fix_subparsers
from dvc.config import Config
from dvc.remote import _get, RemoteLOCAL
from dvc.command.base import append_doc_link, fix_subparsers
from dvc.command.config import CmdConfig
from dvc.config import Config
logger = logging.getLogger(__name__)
class CmdRemoteAdd(CmdConfig):
@@ -30,13 +33,23 @@ class CmdRemoteAdd(CmdConfig):
return os.path.relpath(path, os.path.dirname(config_file))
def run(self):
from dvc.remote import _get, RemoteLOCAL
remote = _get({Config.SECTION_REMOTE_URL: self.args.url})
if remote == RemoteLOCAL:
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
@@ -76,7 +89,7 @@ class CmdRemoteRemove(CmdConfig):
for configobj in [
self.config._local_config,
self.config._project_config,
self.config._repo_config,
self.config._global_config,
self.config._system_config,
]:
@@ -118,12 +131,13 @@ class CmdRemoteList(CmdConfig):
def add_parser(subparsers, parent_parser):
from dvc.command.config import parent_config_parser
REMOTE_HELP = "Manage set of tracked repositories."
REMOTE_HELP = "Manage remote storage configuration."
remote_parser = subparsers.add_parser(
"remote",
parents=[parent_parser],
description=REMOTE_HELP,
description=append_doc_link(REMOTE_HELP, "remote"),
help=REMOTE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_subparsers = remote_parser.add_subparsers(
@@ -137,14 +151,14 @@ def add_parser(subparsers, parent_parser):
remote_add_parser = remote_subparsers.add_parser(
"add",
parents=[parent_config_parser, parent_parser],
description=REMOTE_ADD_HELP,
description=append_doc_link(REMOTE_ADD_HELP, "remote-add"),
help=REMOTE_ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_add_parser.add_argument("name", help="Name.")
remote_add_parser.add_argument(
"url",
help="URL. See full list of supported urls at "
"https://dvc.org/doc/commands-reference/remote",
help="URL. See full list of supported urls at " "man.dvc.org/remote",
)
remote_add_parser.add_argument(
"-d",
@@ -153,14 +167,22 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Set as default remote.",
)
remote_add_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force overwriting existing configs",
)
remote_add_parser.set_defaults(func=CmdRemoteAdd)
REMOTE_DEFAULT_HELP = "Set/unset default remote."
remote_default_parser = remote_subparsers.add_parser(
"default",
parents=[parent_config_parser, parent_parser],
description=REMOTE_DEFAULT_HELP,
description=append_doc_link(REMOTE_DEFAULT_HELP, "remote-default"),
help=REMOTE_DEFAULT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_default_parser.add_argument(
"name", nargs="?", help="Name of the remote."
@@ -178,8 +200,9 @@ def add_parser(subparsers, parent_parser):
remote_remove_parser = remote_subparsers.add_parser(
"remove",
parents=[parent_config_parser, parent_parser],
description=REMOTE_REMOVE_HELP,
description=append_doc_link(REMOTE_REMOVE_HELP, "remote-remove"),
help=REMOTE_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_remove_parser.add_argument("name", help="Name")
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
@@ -188,8 +211,9 @@ def add_parser(subparsers, parent_parser):
remote_modify_parser = remote_subparsers.add_parser(
"modify",
parents=[parent_config_parser, parent_parser],
description=REMOTE_MODIFY_HELP,
description=append_doc_link(REMOTE_MODIFY_HELP, "remote-modify"),
help=REMOTE_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_modify_parser.add_argument("name", help="Name.")
remote_modify_parser.add_argument("option", help="Option.")
@@ -203,11 +227,12 @@ def add_parser(subparsers, parent_parser):
)
remote_modify_parser.set_defaults(func=CmdRemoteModify)
REMOTE_LIST_HELP = "List remotes."
REMOTE_LIST_HELP = "List available remotes."
remote_list_parser = remote_subparsers.add_parser(
"list",
parents=[parent_config_parser, parent_parser],
description=REMOTE_LIST_HELP,
description=append_doc_link(REMOTE_LIST_HELP, "remote-list"),
help=REMOTE_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_list_parser.set_defaults(func=CmdRemoteList)
+11 -5
View File
@@ -1,9 +1,14 @@
from __future__ import unicode_literals
import argparse
import logging
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdRemove(CmdBase):
@@ -30,9 +35,9 @@ class CmdRemove(CmdBase):
for target in self.args.targets:
try:
outs_only = self._is_outs_only(target)
self.project.remove(target, outs_only=outs_only)
self.repo.remove(target, outs_only=outs_only)
except DvcException:
logger.error("failed to remove {}".format(target))
logger.exception("failed to remove {}".format(target))
return 1
return 0
@@ -42,8 +47,9 @@ def add_parser(subparsers, parent_parser):
remove_parser = subparsers.add_parser(
"remove",
parents=[parent_parser],
description=REMOVE_HELP,
description=append_doc_link(REMOVE_HELP, "remove"),
help=REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remove_parser_group = remove_parser.add_mutually_exclusive_group()
remove_parser_group.add_argument(
+33 -10
View File
@@ -1,13 +1,18 @@
from __future__ import unicode_literals
import argparse
import os
import logging
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.command.metrics import show_metrics
from dvc.command.status import CmdDataStatus
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdRepro(CmdBase):
def run(self):
recursive = not self.args.single_item
@@ -24,7 +29,7 @@ class CmdRepro(CmdBase):
ret = 0
for target in self.args.targets:
try:
stages = self.project.reproduce(
stages = self.repo.reproduce(
target,
recursive=recursive,
force=self.args.force,
@@ -33,15 +38,18 @@ class CmdRepro(CmdBase):
pipeline=self.args.pipeline,
all_pipelines=self.args.all_pipelines,
ignore_build_cache=self.args.ignore_build_cache,
no_commit=self.args.no_commit,
downstream=self.args.downstream,
)
if len(stages) == 0:
logger.info(CmdDataStatus.UP_TO_DATE_MSG)
if self.args.metrics:
self.project.metrics_show()
metrics = self.repo.metrics.show()
show_metrics(metrics)
except DvcException:
logger.error()
logger.exception("")
ret = 1
break
@@ -50,15 +58,18 @@ class CmdRepro(CmdBase):
def add_parser(subparsers, parent_parser):
REPRO_HELP = "Reproduce DVC file. Default file name - 'Dvcfile'."
REPRO_HELP = "Check for changes and reproduce DVC file and dependencies."
repro_parser = subparsers.add_parser(
"repro",
parents=[parent_parser],
description=REPRO_HELP,
description=append_doc_link(REPRO_HELP, "repro"),
help=REPRO_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
repro_parser.add_argument(
"targets", nargs="*", help="DVC file to reproduce."
"targets",
nargs="*",
help="DVC file to reproduce (default - 'Dvcfile').",
)
repro_parser.add_argument(
"-f",
@@ -79,7 +90,7 @@ def add_parser(subparsers, parent_parser):
"-c",
"--cwd",
default=os.path.curdir,
help="Directory within your project to reroduce from.",
help="Directory within your repo to reproduce from.",
)
repro_parser.add_argument(
"-m",
@@ -115,7 +126,7 @@ def add_parser(subparsers, parent_parser):
"--all-pipelines",
action="store_true",
default=False,
help="Reproduce all pipelines in the project.",
help="Reproduce all pipelines in the repo.",
)
repro_parser.add_argument(
"--ignore-build-cache",
@@ -124,4 +135,16 @@ def add_parser(subparsers, parent_parser):
help="Reproduce all descendants of a changed stage even if their "
"direct dependencies didn't change.",
)
repro_parser.add_argument(
"--no-commit",
action="store_true",
default=False,
help="Don't put files/directories into cache.",
)
repro_parser.add_argument(
"--downstream",
action="store_true",
default=False,
help="Reproduce the pipeline starting from the specified stage.",
)
repro_parser.set_defaults(func=CmdRepro)
+14 -8
View File
@@ -1,23 +1,29 @@
from __future__ import unicode_literals
import argparse
import os
import logging
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.repo import Repo
from dvc.command.base import CmdBaseNoRepo, append_doc_link
class CmdRoot(CmdBase):
def run_cmd(self):
return self.run()
logger = logging.getLogger(__name__)
class CmdRoot(CmdBaseNoRepo):
def run(self):
logger.info(os.path.relpath(self.project.root_dir))
logger.info(os.path.relpath(Repo.find_root()))
return 0
def add_parser(subparsers, parent_parser):
ROOT_HELP = "Relative path to project's directory."
ROOT_HELP = "Relative path to the repository's directory."
root_parser = subparsers.add_parser(
"root", parents=[parent_parser], description=ROOT_HELP, help=ROOT_HELP
"root",
parents=[parent_parser],
description=append_doc_link(ROOT_HELP, "root"),
help=ROOT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
root_parser.set_defaults(func=CmdRoot)
+67 -33
View File
@@ -1,13 +1,15 @@
from __future__ import unicode_literals
import os
import argparse
import logging
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdRun(CmdBase):
def run(self):
overwrite = self.args.yes or self.args.overwrite_dvcfile
@@ -17,31 +19,41 @@ class CmdRun(CmdBase):
self.args.deps,
self.args.outs,
self.args.outs_no_cache,
self.args.metrics,
self.args.metrics_no_cache,
self.args.outs_persist,
self.args.outs_persist_no_cache,
self.args.command,
]
): # pragma: no cover
logger.error(
"too few arguments. Specify at least one: '-d', "
"'-o', '-O', 'command'."
"too few arguments. Specify at least one: '-d', '-o', '-O',"
" '-m', '-M', '--outs-persist', '--outs-persist-no-cache',"
" 'command'."
)
return 1
try:
self.project.run(
self.repo.run(
cmd=self._parsed_cmd(),
outs=self.args.outs,
outs_no_cache=self.args.outs_no_cache,
metrics=self.args.metrics,
metrics_no_cache=self.args.metrics_no_cache,
deps=self.args.deps,
fname=self.args.file,
cwd=self.args.cwd,
wdir=self.args.wdir,
no_exec=self.args.no_exec,
overwrite=overwrite,
ignore_build_cache=self.args.ignore_build_cache,
remove_outs=self.args.remove_outs,
no_commit=self.args.no_commit,
outs_persist=self.args.outs_persist,
outs_persist_no_cache=self.args.outs_persist_no_cache,
)
except DvcException:
logger.error("failed to run command")
logger.exception("failed to run command")
return 1
return 0
@@ -69,12 +81,13 @@ class CmdRun(CmdBase):
def add_parser(subparsers, parent_parser):
RUN_HELP = (
"Generate a stage file from a given "
"command and execute the command."
)
RUN_HELP = "Generate a stage file from a command and execute the command."
run_parser = subparsers.add_parser(
"run", parents=[parent_parser], description=RUN_HELP, help=RUN_HELP
"run",
parents=[parent_parser],
description=append_doc_link(RUN_HELP, "run"),
help=RUN_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
run_parser.add_argument(
"-d",
@@ -88,39 +101,42 @@ def add_parser(subparsers, parent_parser):
"--outs",
action="append",
default=[],
help="Declare output data file or data directory.",
help="Declare output file or directory.",
)
run_parser.add_argument(
"-O",
"--outs-no-cache",
action="append",
default=[],
help="Declare output regular file or directory "
"(sync to Git, not DVC cache).",
help="Declare output file or directory "
"(do not put into DVC cache).",
)
run_parser.add_argument(
"-m",
"--metrics",
action="append",
default=[],
help="Declare output metric file or directory.",
)
run_parser.add_argument(
"-M",
"--metrics-no-cache",
action="append",
default=[],
help="Declare output metric file or " "directory (not cached by DVC).",
help="Declare output metric file or directory "
"(do not put into DVC cache).",
)
run_parser.add_argument(
"-f",
"--file",
help="Specify name of the stage file. It should be "
"either 'Dvcfile' or have a '.dvc' suffix (e.g. "
"'prepare.dvc', 'clean.dvc', etc) in order for "
"dvc to be able to find it later. By default "
"the first output basename + .dvc is used as "
"a stage filename.",
"-f", "--file", help="Specify name of the DVC file it generates."
)
run_parser.add_argument(
"-c",
"--cwd",
default=os.path.curdir,
help="Directory within your project to run your command and place "
"stage file in.",
"-c", "--cwd", default=None, help="Deprecated, use -w and -f instead."
)
run_parser.add_argument(
"-w",
"--wdir",
default=None,
help="Directory within your repo to run your command in.",
)
run_parser.add_argument(
"--no-exec",
@@ -133,9 +149,7 @@ def add_parser(subparsers, parent_parser):
"--yes",
action="store_true",
default=False,
help="(OBSOLETED, use --overwrite-dvcfile instead) Automatic 'yes' "
"answer to all prompts. E.g. when '.dvc' file exists and dvc "
"asks if you want to overwrite it.",
help="Deprecated, use --overwrite-dvcfile instead",
)
run_parser.add_argument(
"--overwrite-dvcfile",
@@ -154,7 +168,27 @@ def add_parser(subparsers, parent_parser):
"--remove-outs",
action="store_true",
default=False,
help="Remove outputs before running the command.",
help="Deprecated, this is now the default behavior",
)
run_parser.add_argument(
"--no-commit",
action="store_true",
default=False,
help="Don't put files/directories into cache.",
)
run_parser.add_argument(
"--outs-persist",
action="append",
default=[],
help="Declare output file or directory that will not be "
"removed upon repro.",
)
run_parser.add_argument(
"--outs-persist-no-cache",
action="append",
default=[],
help="Declare output file or directory that will not be "
"removed upon repro (do not put into DVC cache).",
)
run_parser.add_argument(
"command", nargs=argparse.REMAINDER, help="Command to execute."
+24 -9
View File
@@ -1,11 +1,16 @@
from __future__ import unicode_literals
import dvc.logger as logger
import logging
from dvc.command.data_sync import CmdDataBase
from dvc.utils.compat import str
logger = logging.getLogger(__name__)
class CmdDataStatus(CmdDataBase):
STATUS_LEN = 10
STATUS_LEN = 20
STATUS_INDENT = "\t"
UP_TO_DATE_MSG = "Pipeline is up to date. Nothing to reproduce."
@@ -17,18 +22,28 @@ class CmdDataStatus(CmdDataBase):
def _show(self, status, indent=0):
ind = indent * self.STATUS_INDENT
if isinstance(status, str):
logger.info("{}{}".format(ind, status))
return
if isinstance(status, list):
for entry in status:
self._show(entry, indent)
return
assert isinstance(status, dict)
for key, value in status.items():
if isinstance(value, dict):
logger.info("{}{}".format(ind, key))
if isinstance(value, str):
logger.info("{}{}{}".format(ind, self._normalize(value), key))
elif value:
logger.info("{}{}:".format(ind, key))
self._show(value, indent + 1)
else:
msg = "{}{}{}".format(ind, self._normalize(value), key)
logger.info(msg)
def do_run(self, target=None):
indent = 1 if self.args.cloud else 0
try:
st = self.project.status(
st = self.repo.status(
target=target,
jobs=self.args.jobs,
cloud=self.args.cloud,
@@ -47,6 +62,6 @@ class CmdDataStatus(CmdDataBase):
logger.info(self.UP_TO_DATE_MSG)
except Exception:
logger.error("failed to obtain data status")
logger.exception("failed to obtain data status")
return 1
return 0
+154
View File
@@ -0,0 +1,154 @@
import logging
from dvc.utils import to_yaml_string
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
logger = logging.getLogger(__name__)
class CmdTagAdd(CmdBase):
def run(self):
for target in self.args.targets:
try:
self.repo.tag.add(
self.args.tag,
target=target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except DvcException:
logger.exception("failed to add tag")
return 1
return 0
class CmdTagRemove(CmdBase):
def run(self):
for target in self.args.targets:
try:
self.repo.tag.remove(
self.args.tag,
target=target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except DvcException:
logger.exception("failed to remove tag")
return 1
return 0
class CmdTagList(CmdBase):
def run(self):
for target in self.args.targets:
try:
tags = self.repo.tag.list(
target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
if tags:
logger.info(to_yaml_string(tags))
except DvcException:
logger.exception("failed list tags")
return 1
return 0
def add_parser(subparsers, parent_parser):
TAG_HELP = "A set of commands to manage DVC tags."
tag_parser = subparsers.add_parser(
"tag",
parents=[parent_parser],
description=append_doc_link(TAG_HELP, "tag"),
add_help=False,
)
tag_subparsers = tag_parser.add_subparsers(
dest="cmd",
help="Use DVC tag CMD --help to display command-specific help.",
)
fix_subparsers(tag_subparsers)
TAG_ADD_HELP = "Add DVC tag."
tag_add_parser = tag_subparsers.add_parser(
"add",
parents=[parent_parser],
description=append_doc_link(TAG_ADD_HELP, "tag-add"),
help=TAG_ADD_HELP,
)
tag_add_parser.add_argument("tag", help="Dvc tag.")
tag_add_parser.add_argument(
"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.",
)
tag_add_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Add tag for subdirectories of the specified directory.",
)
tag_add_parser.set_defaults(func=CmdTagAdd)
TAG_REMOVE_HELP = "Remove DVC tag."
tag_remove_parser = tag_subparsers.add_parser(
"remove",
parents=[parent_parser],
description=append_doc_link(TAG_REMOVE_HELP, "tag-remove"),
help=TAG_REMOVE_HELP,
)
tag_remove_parser.add_argument("tag", help="Dvc tag.")
tag_remove_parser.add_argument(
"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.",
)
tag_remove_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Remove tag for subdirectories of the specified directory.",
)
tag_remove_parser.set_defaults(func=CmdTagRemove)
TAG_LIST_HELP = "List DVC tags."
tag_list_parser = tag_subparsers.add_parser(
"list",
parents=[parent_parser],
description=append_doc_link(TAG_LIST_HELP, "tag-list"),
help=TAG_LIST_HELP,
)
tag_list_parser.add_argument(
"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.",
)
tag_list_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="List tags for subdirectories of the specified directory.",
)
tag_list_parser.set_defaults(func=CmdTagList)
+12 -6
View File
@@ -1,29 +1,35 @@
from __future__ import unicode_literals
import dvc.logger as logger
import argparse
import logging
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
class CmdUnprotect(CmdBase):
def run(self):
for target in self.args.targets:
try:
self.project.unprotect(target)
self.repo.unprotect(target)
except DvcException:
msg = "failed to unprotect '{}'".format(target)
logger.error(msg)
logger.exception(msg)
return 1
return 0
def add_parser(subparsers, parent_parser):
UNPROTECT_HELP = "Unprotect data file/directory."
UNPROTECT_HELP = "Unprotect a data file/directory."
unprotect_parser = subparsers.add_parser(
"unprotect",
parents=[parent_parser],
description=UNPROTECT_HELP,
description=append_doc_link(UNPROTECT_HELP, "unprotect"),
help=UNPROTECT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unprotect_parser.add_argument(
"targets", nargs="+", help="Data files/directory."
+39
View File
@@ -0,0 +1,39 @@
from __future__ import unicode_literals
import platform
import argparse
import logging
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.version import __version__
logger = logging.getLogger(__name__)
class CmdVersion(CmdBaseNoRepo):
def run(self):
info = (
"DVC version: {dvc_version}\n"
"Python version: {python_version}\n"
"Platform: {platform_type}"
).format(
dvc_version=__version__,
python_version=platform.python_version(),
platform_type=platform.platform(),
)
logger.info(info)
return 0
def add_parser(subparsers, parent_parser):
VERSION_HELP = "Show DVC version and system/environment informaion."
version_parser = subparsers.add_parser(
"version",
parents=[parent_parser],
description=append_doc_link(VERSION_HELP, "version"),
help=VERSION_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
version_parser.set_defaults(func=CmdVersion)
+86 -25
View File
@@ -2,16 +2,18 @@
from __future__ import unicode_literals
from dvc.utils.compat import str, open
from dvc.utils.compat import str, open, urlparse
import os
import errno
import configobj
from schema import Schema, Optional, And, Use, Regex
import logging
import dvc.logger as logger
from schema import Schema, Optional, And, Use, Regex
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class ConfigError(DvcException):
"""DVC config exception.
@@ -108,11 +110,11 @@ def is_percent(val):
class Config(object): # pylint: disable=too-many-instance-attributes
"""Class that manages configuration files for a dvc project.
"""Class that manages configuration files for a dvc repo.
Args:
dvc_dir (str): optional path to `.dvc` directory, that is used to
access project-specific configs like .dvc/config and
access repo-specific configs like .dvc/config and
.dvc/config.local.
validate (bool): optional flag to tell dvc if it should validate the
config or just load it as is. 'True' by default.
@@ -132,14 +134,16 @@ class Config(object): # pylint: disable=too-many-instance-attributes
CONFIG = "config"
CONFIG_LOCAL = "config.local"
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_REMOTE = "remote"
SECTION_CORE_INTERACTIVE_SCHEMA = And(str, is_bool, Use(to_bool))
SECTION_CORE_INTERACTIVE_SCHEMA = BOOL_SCHEMA
SECTION_CORE_INTERACTIVE = "interactive"
SECTION_CORE_ANALYTICS = "analytics"
SECTION_CORE_ANALYTICS_SCHEMA = And(str, is_bool, Use(to_bool))
SECTION_CORE_ANALYTICS_SCHEMA = BOOL_SCHEMA
SECTION_CACHE = "cache"
SECTION_CACHE_DIR = "dir"
@@ -152,6 +156,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
SECTION_CACHE_SSH = "ssh"
SECTION_CACHE_HDFS = "hdfs"
SECTION_CACHE_AZURE = "azure"
SECTION_CACHE_SLOW_LINK_WARNING = "slow_link_warning"
SECTION_CACHE_SCHEMA = {
Optional(SECTION_CACHE_LOCAL): str,
Optional(SECTION_CACHE_S3): str,
@@ -161,10 +166,9 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_CACHE_AZURE): str,
Optional(SECTION_CACHE_DIR): str,
Optional(SECTION_CACHE_TYPE, default=None): SECTION_CACHE_TYPE_SCHEMA,
Optional(SECTION_CACHE_PROTECTED, default=False): And(
str, is_bool, Use(to_bool)
),
Optional(SECTION_CACHE_PROTECTED, default=False): BOOL_SCHEMA,
Optional(PRIVATE_CWD): str,
Optional(SECTION_CACHE_SLOW_LINK_WARNING, default=True): BOOL_SCHEMA,
}
# backward compatibility
@@ -173,7 +177,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
SECTION_CORE_STORAGEPATH = "storagepath"
SECTION_CORE_SCHEMA = {
Optional(SECTION_CORE_LOGLEVEL, default="info"): And(
Optional(SECTION_CORE_LOGLEVEL): And(
str, Use(str.lower), SECTION_CORE_LOGLEVEL_SCHEMA
),
Optional(SECTION_CORE_REMOTE, default=""): And(str, Use(str.lower)),
@@ -193,6 +197,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
SECTION_AWS_STORAGEPATH = "storagepath"
SECTION_AWS_CREDENTIALPATH = "credentialpath"
SECTION_AWS_ENDPOINT_URL = "endpointurl"
SECTION_AWS_LIST_OBJECTS = "listobjects"
SECTION_AWS_REGION = "region"
SECTION_AWS_PROFILE = "profile"
SECTION_AWS_USE_SSL = "use_ssl"
@@ -202,9 +207,8 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_AWS_PROFILE): str,
Optional(SECTION_AWS_CREDENTIALPATH): str,
Optional(SECTION_AWS_ENDPOINT_URL): str,
Optional(SECTION_AWS_USE_SSL, default=True): And(
str, is_bool, Use(to_bool)
),
Optional(SECTION_AWS_LIST_OBJECTS, default=False): BOOL_SCHEMA,
Optional(SECTION_AWS_USE_SSL, default=True): BOOL_SCHEMA,
}
# backward compatibility
@@ -223,6 +227,10 @@ class Config(object): # pylint: disable=too-many-instance-attributes
SECTION_LOCAL_SCHEMA = {SECTION_LOCAL_STORAGEPATH: str}
SECTION_AZURE_CONNECTION_STRING = "connection_string"
# Alibabacloud oss options
SECTION_OSS_ACCESS_KEY_ID = "oss_key_id"
SECTION_OSS_ACCESS_KEY_SECRET = "oss_key_secret"
SECTION_OSS_ENDPOINT = "oss_endpoint"
SECTION_REMOTE_REGEX = r'^\s*remote\s*"(?P<name>.*)"\s*$'
SECTION_REMOTE_FMT = 'remote "{}"'
@@ -239,21 +247,21 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_AWS_PROFILE): str,
Optional(SECTION_AWS_CREDENTIALPATH): str,
Optional(SECTION_AWS_ENDPOINT_URL): str,
Optional(SECTION_AWS_USE_SSL, default=True): And(
str, is_bool, Use(to_bool)
),
Optional(SECTION_AWS_LIST_OBJECTS, default=False): BOOL_SCHEMA,
Optional(SECTION_AWS_USE_SSL, default=True): BOOL_SCHEMA,
Optional(SECTION_GCP_PROJECTNAME): str,
Optional(SECTION_CACHE_TYPE): SECTION_CACHE_TYPE_SCHEMA,
Optional(SECTION_CACHE_PROTECTED, default=False): And(
str, is_bool, Use(to_bool)
),
Optional(SECTION_CACHE_PROTECTED, default=False): BOOL_SCHEMA,
Optional(SECTION_REMOTE_USER): str,
Optional(SECTION_REMOTE_PORT): Use(int),
Optional(SECTION_REMOTE_KEY_FILE): str,
Optional(SECTION_REMOTE_TIMEOUT): Use(int),
Optional(SECTION_REMOTE_PASSWORD): str,
Optional(SECTION_REMOTE_ASK_PASSWORD): And(str, is_bool, Use(to_bool)),
Optional(SECTION_REMOTE_ASK_PASSWORD): BOOL_SCHEMA,
Optional(SECTION_AZURE_CONNECTION_STRING): str,
Optional(SECTION_OSS_ACCESS_KEY_ID): str,
Optional(SECTION_OSS_ACCESS_KEY_SECRET): str,
Optional(SECTION_OSS_ENDPOINT): str,
Optional(PRIVATE_CWD): str,
}
@@ -295,7 +303,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
self._system_config = None
self._global_config = None
self._project_config = None
self._repo_config = None
self._local_config = None
self.config = None
@@ -347,9 +355,9 @@ class Config(object): # pylint: disable=too-many-instance-attributes
self._global_config = configobj.ConfigObj(self.global_config_file)
if self.config_file is not None:
self._project_config = configobj.ConfigObj(self.config_file)
self._repo_config = configobj.ConfigObj(self.config_file)
else:
self._project_config = configobj.ConfigObj()
self._repo_config = configobj.ConfigObj()
if self.config_local_file is not None:
self._local_config = configobj.ConfigObj(self.config_local_file)
@@ -382,6 +390,9 @@ class Config(object): # pylint: disable=too-many-instance-attributes
cache[self.PRIVATE_CWD] = os.path.dirname(fname)
def _resolve_paths(self, config, fname):
if fname is None:
return
self._resolve_cache_path(config, fname)
for section in config.values():
if self.SECTION_REMOTE_URL not in section.keys():
@@ -451,7 +462,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
clist = [
self._system_config,
self._global_config,
self._project_config,
self._repo_config,
self._local_config,
]
@@ -472,6 +483,56 @@ class Config(object): # pylint: disable=too-many-instance-attributes
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
@staticmethod
def unset(config, section, opt=None):
"""Unsets specified option and/or section in the config.
+34 -19
View File
@@ -5,12 +5,16 @@ from __future__ import unicode_literals
import os
import sys
import inspect
import logging
from subprocess import Popen
import dvc.logger as logger
from dvc.env import DVC_DAEMON
from dvc.utils import is_binary, fix_env
from dvc.utils.compat import cast_bytes_py2
logger = logging.getLogger(__name__)
CREATE_NEW_PROCESS_GROUP = 0x00000200
DETACHED_PROCESS = 0x00000008
@@ -42,7 +46,7 @@ def _spawn_posix(cmd, env):
if pid > 0:
return
except OSError:
logger.error("failed at first fork")
logger.exception("failed at first fork")
os._exit(1) # pylint: disable=protected-access
os.setsid()
@@ -53,7 +57,7 @@ def _spawn_posix(cmd, env):
if pid > 0:
os._exit(0) # pylint: disable=protected-access
except OSError:
logger.error("failed at second fork")
logger.exception("failed at second fork")
os._exit(1) # pylint: disable=protected-access
sys.stdin.close()
@@ -65,22 +69,8 @@ def _spawn_posix(cmd, env):
os._exit(0) # pylint: disable=protected-access
def daemon(args):
"""Launch a `dvc daemon` command in a detached process.
Args:
args (list): list of arguments to append to `dvc daemon` command.
"""
cmd = [sys.executable]
if not is_binary():
cmd += ["-m", "dvc"]
cmd += ["daemon", "-q"] + args
env = fix_env()
file_path = os.path.abspath(inspect.stack()[0][1])
env["PYTHONPATH"] = os.path.dirname(os.path.dirname(file_path))
logger.debug("Trying to spawn '{}' with env '{}'".format(cmd, env))
def _spawn(cmd, env):
logger.debug("Trying to spawn '{}'".format(cmd))
if os.name == "nt":
_spawn_windows(cmd, env)
@@ -90,3 +80,28 @@ def daemon(args):
raise NotImplementedError
logger.debug("Spawned '{}'".format(cmd))
def daemon(args):
"""Launch a `dvc daemon` command in a detached process.
Args:
args (list): list of arguments to append to `dvc daemon` command.
"""
if os.environ.get(DVC_DAEMON):
logger.debug("skipping launching a new daemon.")
return
cmd = [sys.executable]
if not is_binary():
cmd += ["-m", "dvc"]
cmd += ["daemon", "-q"] + args
env = fix_env()
file_path = os.path.abspath(inspect.stack()[0][1])
env[cast_bytes_py2("PYTHONPATH")] = cast_bytes_py2(
os.path.dirname(os.path.dirname(file_path))
)
env[cast_bytes_py2(DVC_DAEMON)] = cast_bytes_py2("1")
_spawn(cmd, env)
+20 -13
View File
@@ -2,26 +2,30 @@
from __future__ import unicode_literals
import dvc.logger as logger
from dvc.config import Config, ConfigError
import logging
from dvc.config import Config, ConfigError
from dvc.remote import Remote
from dvc.remote.s3 import RemoteS3
from dvc.remote.gs import RemoteGS
from dvc.remote.azure import RemoteAzure
from dvc.remote.oss import RemoteOSS
from dvc.remote.ssh import RemoteSSH
from dvc.remote.hdfs import RemoteHDFS
from dvc.remote.local import RemoteLOCAL
from dvc.remote.http import RemoteHTTP
logger = logging.getLogger(__name__)
class DataCloud(object):
"""Class that manages dvc remotes.
Args:
project (dvc.project.Project): project instance that belongs to the
project that we are working on.
config (dict): config of the project.
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.
@@ -31,6 +35,7 @@ class DataCloud(object):
"aws": RemoteS3,
"gcp": RemoteGS,
"azure": RemoteAzure,
"oss": RemoteOSS,
"ssh": RemoteSSH,
"hdfs": RemoteHDFS,
"local": RemoteLOCAL,
@@ -38,15 +43,17 @@ class DataCloud(object):
"https": RemoteHTTP,
}
def __init__(self, project, config=None):
self.project = project
def __init__(self, repo, config=None):
self.repo = repo
self._config = config
self._core = self._config[Config.SECTION_CORE]
@property
def _cloud(self):
"""Returns a Remote instance using the `core.remote` config"""
remote = self._core.get(Config.SECTION_CORE_REMOTE, "")
if remote != "":
if remote:
return self._init_remote(remote)
if self._core.get(Config.SECTION_CORE_CLOUD, None):
@@ -64,7 +71,7 @@ class DataCloud(object):
msg = "can't find remote section '{}' in config"
raise ConfigError(msg.format(section))
return Remote(self.project, cloud_config)
return Remote(self.repo, cloud_config)
def _init_compat(self):
name = self._core.get(Config.SECTION_CORE_CLOUD, "").strip().lower()
@@ -94,7 +101,7 @@ class DataCloud(object):
if global_storage_path:
logger.warning("using obsoleted config format. Consider updating.")
cloud = cloud_type(self.project, cloud_config)
cloud = cloud_type(self.repo, cloud_config)
return cloud
def _get_cloud(self, remote, cmd):
@@ -122,7 +129,7 @@ class DataCloud(object):
show_checksums (bool): show checksums instead of file names in
information messages.
"""
return self.project.cache.local.push(
return self.repo.cache.local.push(
targets,
jobs=jobs,
remote=self._get_cloud(remote, "push"),
@@ -140,7 +147,7 @@ class DataCloud(object):
show_checksums (bool): show checksums instead of file names in
information messages.
"""
return self.project.cache.local.pull(
return self.repo.cache.local.pull(
targets,
jobs=jobs,
remote=self._get_cloud(remote, "pull"),
@@ -160,6 +167,6 @@ class DataCloud(object):
information messages.
"""
cloud = self._get_cloud(remote, "status")
return self.project.cache.local.status(
return self.repo.cache.local.status(
targets, jobs=jobs, remote=cloud, show_checksums=show_checksums
)
+5 -6
View File
@@ -2,9 +2,6 @@ from __future__ import unicode_literals
import schema
from dvc.config import Config
from dvc.utils.compat import urlparse
import dvc.output as output
from dvc.output.base import OutputBase
from dvc.dependency.s3 import DependencyS3
@@ -46,11 +43,13 @@ del SCHEMA[schema.Optional(OutputBase.PARAM_METRIC)]
def _get(stage, p, info):
from dvc.utils.compat import urlparse
parsed = urlparse(p)
if parsed.scheme == "remote":
name = Config.SECTION_REMOTE_FMT.format(parsed.netloc)
sect = stage.project.config.config[name]
remote = Remote(stage.project, sect)
settings = stage.repo.config.get_remote_settings(parsed.netloc)
remote = Remote(stage.repo, settings)
return DEP_MAP[remote.scheme](stage, p, info, remote=remote)
for d in DEPS:
+2 -1
View File
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
from dvc.path.http import HTTPPathInfo
from dvc.utils.compat import urlparse, urljoin
from dvc.output.base import OutputBase
from dvc.remote.http import RemoteHTTP
@@ -16,4 +17,4 @@ class DependencyHTTP(DependencyBase, OutputBase):
if path.startswith("remote"):
path = urljoin(self.remote.cache_dir, urlparse(path).path)
self.path_info = {"scheme": urlparse(path).scheme, "path": path}
self.path_info = HTTPPathInfo(url=self.url, path=path)
+1
View File
@@ -0,0 +1 @@
DVC_DAEMON = "DVC_DAEMON"
+113 -12
View File
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
from dvc.utils.compat import str, builtin_str
import os
import traceback
@@ -11,7 +12,7 @@ class DvcException(Exception):
"""Base class for all dvc exceptions.
Args:
msg (str): message for this exception.
msg (unicode): message for this exception.
cause (Exception): optional cause exception.
"""
@@ -33,7 +34,7 @@ class OutputDuplicationError(DvcException):
stage.
Args:
output (str): path to the file/directory.
output (unicode): path to the file/directory.
stages (list): list of paths to stages.
"""
@@ -51,9 +52,24 @@ class OutputDuplicationError(DvcException):
super(OutputDuplicationError, self).__init__(msg)
class WorkingDirectoryAsOutputError(DvcException):
class OutputNotFoundError(DvcException):
"""Thrown if a file/directory not found in repository pipelines.
Args:
output (unicode): path to the file/directory.
"""
def __init__(self, output):
super(OutputNotFoundError, self).__init__(
"unable to find stage file with output '{path}'".format(
path=os.path.relpath(output)
)
)
class StagePathAsOutputError(DvcException):
"""Thrown if directory that stage is going to be saved in is specified as
an output of anothe stage.
an output of another stage.
Args:
cwd (str): path to the directory.
@@ -61,16 +77,16 @@ class WorkingDirectoryAsOutputError(DvcException):
output.
"""
def __init__(self, cwd, fname):
assert isinstance(cwd, str) or isinstance(cwd, builtin_str)
def __init__(self, wdir, fname):
assert isinstance(wdir, str) or isinstance(wdir, builtin_str)
assert isinstance(fname, str) or isinstance(fname, builtin_str)
msg = (
"current working directory '{cwd}' is specified as an output in"
" '{fname}'. Use another CWD to prevent any data removal.".format(
cwd=cwd, fname=fname
cwd=wdir, fname=fname
)
)
super(WorkingDirectoryAsOutputError, self).__init__(msg)
super(StagePathAsOutputError, self).__init__(msg)
class CircularDependencyError(DvcException):
@@ -125,16 +141,19 @@ class MoveNotDataSourceError(DvcException):
super(MoveNotDataSourceError, self).__init__(msg.format(path=path))
class NotDvcProjectError(DvcException):
"""Thrown if a directory is not a dvc project.
class NotDvcRepoError(DvcException):
"""Thrown if a directory is not a dvc repo.
Args:
root (str): path to the directory.
"""
def __init__(self, root):
msg = "not a dvc repository (checked up to mount point '{}')"
super(NotDvcProjectError, self).__init__(msg.format(root))
msg = (
"you are not inside of a dvc repository "
"(checked up to mount point '{}')"
)
super(NotDvcRepoError, self).__init__(msg.format(root))
class DvcParserError(DvcException):
@@ -163,3 +182,85 @@ class ConfirmRemoveError(DvcException):
"unable to remove '{}' without a confirmation from the user. Use "
"'-f' to force.".format(path)
)
class InitError(DvcException):
def __init__(self, msg):
super(InitError, self).__init__(msg)
class ReproductionError(DvcException):
def __init__(self, dvc_file_name, ex):
self.path = dvc_file_name
msg = "failed to reproduce '{}'".format(dvc_file_name)
super(ReproductionError, self).__init__(msg, cause=ex)
class BadMetricError(DvcException):
def __init__(self, path):
super(BadMetricError, self).__init__(
"'{}' does not exist, not a metric or is malformed".format(
os.path.relpath(path)
)
)
class NoMetricsError(DvcException):
def __init__(self):
super(NoMetricsError, self).__init__(
"no metric files in this repository. "
"Use 'dvc metrics add' to add a metric file to track."
)
class StageFileCorruptedError(DvcException):
def __init__(self, path, cause=None):
path = os.path.relpath(path)
super(StageFileCorruptedError, self).__init__(
"unable to read stage file: {} "
"YAML file structure is corrupted".format(path),
cause=cause,
)
class RecursiveAddingWhileUsingFilename(DvcException):
def __init__(self):
super(RecursiveAddingWhileUsingFilename, self).__init__(
"using fname with recursive is not allowed."
)
class OverlappingOutputPathsError(DvcException):
def __init__(self, out_1, out_2):
super(OverlappingOutputPathsError, self).__init__(
"Paths for outs:\n'{}'('{}')\n'{}'('{}')\noverlap. To avoid "
"unpredictable behaviour, rerun command with non overlapping outs "
"paths.".format(
str(out_1),
out_1.stage.relpath,
str(out_2),
out_2.stage.relpath,
)
)
class TargetNotDirectoryError(DvcException):
def __init__(self, path):
super(TargetNotDirectoryError, self).__init__(
"Target: {} is not a directory".format(path)
)
class CheckoutErrorSuggestGit(DvcException):
def __init__(self, target, cause):
super(CheckoutErrorSuggestGit, self).__init__(
"Did you mean 'git checkout {}'?".format(target), cause=cause
)
class ETagMismatchError(DvcException):
def __init__(self, etag, cached_etag):
super(ETagMismatchError, self).__init__(
"ETag mismatch detected when copying file to cache! "
"(expected: '{}', actual: '{}')".format(etag, cached_etag)
)
+134
View File
@@ -0,0 +1,134 @@
import os
from dulwich.ignore import match_pattern, read_ignore_patterns
from dvc.utils.compat import cast_bytes
from dvc.utils.fs import get_parent_dirs_up_to
class DvcIgnoreFileHandler(object):
def __init__(self, tree):
self.tree = tree
def read_patterns(self, path):
with self.tree.open(path, binary=True) as stream:
return self._read_patterns(stream)
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"
def __call__(self, root, dirs, files):
raise NotImplementedError
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
)
def __call__(self, root, dirs, files):
files = [f for f in files if not self.matches(root, f)]
dirs = [d for d in dirs if not self.matches(root, d)]
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
def _no_negate_pattern_matches(self, path):
return all([not match_pattern(path, p) for p in self.negate_patterns])
def __hash__(self):
return hash(self.ignore_file_path)
class DvcIgnoreConstant(DvcIgnore):
def __init__(self, basename):
self.basename = basename
class DvcIgnoreDir(DvcIgnoreConstant):
def __call__(self, root, dirs, files):
dirs = [d for d in dirs if not d == self.basename]
return dirs, files
class DvcIgnoreFile(DvcIgnoreConstant):
def __call__(self, root, dirs, files):
files = [f for f in files if not f == self.basename]
return dirs, files
class DvcIgnoreFilter(object):
def __init__(self, wdir, ignore_file_handler=None):
self.ignores = [
DvcIgnoreDir(".git"),
DvcIgnoreDir(".hg"),
DvcIgnoreDir(".dvc"),
DvcIgnoreFile(".dvcignore"),
]
self.ignore_file_handler = ignore_file_handler
self._process_ignores_in_parent_dirs(wdir)
def _process_ignores_in_parent_dirs(self, wdir):
if self.ignore_file_handler:
wdir = os.path.normpath(os.path.abspath(wdir))
ignore_search_end_dir = self.ignore_file_handler.get_repo_root()
parent_dirs = get_parent_dirs_up_to(wdir, ignore_search_end_dir)
for d in parent_dirs:
self.update(d)
def update(self, wdir):
ignore_file_path = os.path.join(wdir, DvcIgnore.DVCIGNORE_FILE)
if os.path.exists(ignore_file_path):
file_ignore = DvcIgnoreFromFile(
ignore_file_path, ignore_handler=self.ignore_file_handler
)
self.ignores.append(file_ignore)
def __call__(self, root, dirs, files):
if self.ignore_file_handler:
self.update(root)
for ignore in self.ignores:
dirs, files = ignore(root, dirs, files)
return dirs, files
+4 -4
View File
@@ -10,11 +10,11 @@ from dvc.exceptions import DvcException
class LockError(DvcException):
"""Thrown when unable to acquire the lock for dvc project."""
"""Thrown when unable to acquire the lock for dvc repo."""
class Lock(object):
"""Class for dvc project lock.
"""Class for dvc repo lock.
Args:
dvc_dir (str): path to the directory that the lock should be created
@@ -39,7 +39,7 @@ class Lock(object):
)
def lock(self):
"""Acquire lock for dvc project."""
"""Acquire lock for dvc repo."""
try:
self._do_lock()
return
@@ -49,7 +49,7 @@ class Lock(object):
self._do_lock()
def unlock(self):
"""Release lock for dvc project."""
"""Release lock for dvc repo."""
self._lock.close()
self._lock = None
+159 -271
View File
@@ -1,294 +1,182 @@
"""Manages logger for dvc project."""
"""Manages logging configuration for dvc repo."""
from __future__ import unicode_literals
from dvc.utils.compat import str
from dvc.utils.compat import str, StringIO
import re
import sys
import logging
import traceback
from contextlib import contextmanager
import logging.handlers
import logging.config
import colorama
def info(message):
"""Prints an info message."""
logger.info(message)
class ExcludeErrorsFilter(logging.Filter):
def filter(self, record):
return record.levelno < logging.ERROR
def debug(message):
"""Prints a debug message."""
prefix = colorize("Debug", color="blue")
class ColorFormatter(logging.Formatter):
"""Enable color support when logging to a terminal that supports it.
out = "{prefix}: {message}".format(prefix=prefix, message=message)
Color support on Windows versions that do not support ANSI color codes is
enabled by use of the colorama__ library.
See the colorama documentation for details.
logger.debug(out)
__ https://pypi.python.org/pypi/colorama
def warning(message):
"""Prints a warning message."""
prefix = colorize("Warning", color="yellow")
out = "{prefix}: {message}".format(prefix=prefix, message=message)
logger.warning(out)
def error(message=None):
"""Prints an error message."""
prefix = colorize("Error", color="red")
exception, stack_trace = _parse_exc()
out = (
"{prefix}: {description}"
"\n"
"{stack_trace}"
"\n"
"{footer}".format(
prefix=prefix,
description=_description(message, exception),
stack_trace=stack_trace,
footer=_footer(),
)
)
logger.error(out)
def box(message, border_color=None):
"""Prints a message in a box.
Args:
message (str): message to print.
border_color (str): name of a color to outline the box with.
For records containing `exc_info`, it will use a custom `_walk_exc` to
retrieve the whole tracebak.
"""
lines = message.split("\n")
max_width = max(_visual_width(line) for line in lines)
padding_horizontal = 5
padding_vertical = 1
box_size_horizontal = max_width + (padding_horizontal * 2)
chars = {"corner": "+", "horizontal": "-", "vertical": "|", "empty": " "}
margin = "{corner}{line}{corner}\n".format(
corner=chars["corner"], line=chars["horizontal"] * box_size_horizontal
)
padding_lines = [
"{border}{space}{border}\n".format(
border=colorize(chars["vertical"], color=border_color),
space=chars["empty"] * box_size_horizontal,
)
* padding_vertical
]
content_lines = [
"{border}{space}{content}{space}{border}\n".format(
border=colorize(chars["vertical"], color=border_color),
space=chars["empty"] * padding_horizontal,
content=_visual_center(line, max_width),
)
for line in lines
]
box_str = "{margin}{padding}{content}{padding}{margin}".format(
margin=colorize(margin, color=border_color),
padding="".join(padding_lines),
content="".join(content_lines),
)
logger.info(box_str)
def level():
"""Returns current log level."""
return logger.getEffectiveLevel()
def set_level(level_name):
"""Sets log level.
Args:
level_name (str): log level name. E.g. info, debug, warning, error,
critical.
"""
if not level_name:
return
levels = {
"info": logging.INFO,
"debug": logging.DEBUG,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL,
color_code = {
"DEBUG": colorama.Fore.BLUE,
"INFO": "",
"WARNING": colorama.Fore.YELLOW,
"ERROR": colorama.Fore.RED,
"CRITICAL": colorama.Fore.RED,
}
logger.setLevel(levels.get(level_name))
def be_quiet():
"""Disables all messages except critical ones."""
logger.setLevel(logging.CRITICAL)
def be_verbose():
"""Enables all messages."""
logger.setLevel(logging.DEBUG)
@contextmanager
def verbose():
"""Enables verbose mode for the context."""
previous_level = level()
be_verbose()
yield
logger.setLevel(previous_level)
@contextmanager
def quiet():
"""Enables quiet mode for the context."""
previous_level = level()
be_quiet()
yield
logger.setLevel(previous_level)
def is_quiet():
"""Returns whether or not all messages except critical ones are
disabled.
"""
return level() == logging.CRITICAL
def is_verbose():
"""Returns whether or not all messages are enabled."""
return level() == logging.DEBUG
def colorize(message, color=None):
"""Returns a message in a specified color."""
if not color:
return message
colors = {
"green": colorama.Fore.GREEN,
"yellow": colorama.Fore.YELLOW,
"blue": colorama.Fore.BLUE,
"red": colorama.Fore.RED,
}
return "{color}{message}{nc}".format(
color=colors.get(color, ""), message=message, nc=colorama.Fore.RESET
footer = (
"{yellow}Having any troubles?{nc}."
" Hit us up at {blue}https://dvc.org/support{nc},"
" we are always happy to help!"
).format(
blue=colorama.Fore.BLUE,
nc=colorama.Fore.RESET,
yellow=colorama.Fore.YELLOW,
)
def format(self, record):
if self._is_visible(record):
self._progress_aware()
def _init_colorama():
if record.levelname == "INFO":
return record.msg
if record.levelname == "ERROR" or record.levelname == "CRITICAL":
exception, stack_trace = self._parse_exc(record.exc_info)
return (
"{color}{levelname}{nc}: {description}"
"{stack_trace}\n"
"\n"
"{footer}"
).format(
color=self.color_code.get(record.levelname, ""),
nc=colorama.Fore.RESET,
levelname=record.levelname,
description=self._description(record.msg, exception),
msg=record.msg,
stack_trace=stack_trace,
footer=self.footer,
)
return "{color}{levelname}{nc}: {msg}".format(
color=self.color_code.get(record.levelname, ""),
nc=colorama.Fore.RESET,
levelname=record.levelname,
msg=record.msg,
)
def _current_level(self):
return logging.getLogger("dvc").getEffectiveLevel()
def _is_visible(self, record):
return record.levelno >= self._current_level()
def _description(self, message, exception):
description = ""
if exception and message:
description = "{message} - {exception}"
elif exception:
description = "{exception}"
elif message:
description = "{message}"
return description.format(message=message, exception=exception)
def _walk_exc(self, exc_info):
import traceback
buffer = StringIO()
traceback.print_exception(*exc_info, file=buffer)
exc = exc_info[1]
tb = buffer.getvalue()
exc_list = [str(exc)]
tb_list = [tb]
# NOTE: parsing chained exceptions. See dvc/exceptions.py for more info
while hasattr(exc, "cause") and exc.cause:
exc_list.append(str(exc.cause))
if hasattr(exc, "cause_tb") and exc.cause_tb:
tb_list.insert(0, str(exc.cause_tb))
exc = exc.cause
return exc_list, tb_list
def _parse_exc(self, exc_info):
if not exc_info:
return (None, "")
exc_list, tb_list = self._walk_exc(exc_info)
exception = ": ".join(exc_list)
if self._current_level() == logging.DEBUG:
stack_trace = (
"\n" "{red}{line}{nc}\n" "{stack_trace}" "{red}{line}{nc}"
).format(
red=colorama.Fore.RED,
nc=colorama.Fore.RESET,
line="-" * 60,
stack_trace="\n".join(tb_list),
)
else:
stack_trace = ""
return (exception, stack_trace)
def _progress_aware(self):
"""Add a new line if progress bar hasn't finished"""
from dvc.progress import progress
if not progress.is_finished:
progress._print()
progress.clearln()
def setup(level=logging.INFO):
colorama.init()
def set_default_level():
"""Sets default log level."""
logger.setLevel(logging.INFO)
def _add_handlers():
formatter = "%(message)s"
class _LogLevelFilter(logging.Filter):
# pylint: disable=too-few-public-methods
def filter(self, record):
return record.levelno <= logging.WARNING
sh_out = logging.StreamHandler(sys.stdout)
sh_out.setFormatter(logging.Formatter(formatter))
sh_out.setLevel(logging.DEBUG)
sh_out.addFilter(_LogLevelFilter())
sh_err = logging.StreamHandler(sys.stderr)
sh_err.setFormatter(logging.Formatter(formatter))
sh_err.setLevel(logging.ERROR)
logger.addHandler(sh_out)
logger.addHandler(sh_err)
def _walk_exc(exc):
exc_list = [str(exc)]
tb_list = [traceback.format_exc()]
# NOTE: parsing chained exceptions. See dvc/exceptions.py for more info.
while hasattr(exc, "cause") and exc.cause is not None:
exc_list.append(str(exc.cause))
if hasattr(exc, "cause_tb") and exc.cause_tb is not None:
tb_list.insert(0, str(exc.cause_tb))
exc = exc.cause
return (exc_list, tb_list)
def _parse_exc():
exc = sys.exc_info()[1]
if not exc:
return (None, "")
exc_list, tb_list = _walk_exc(exc)
exception = ": ".join(exc_list)
if is_verbose():
stack_trace = "{line}\n{stack_trace}{line}\n".format(
line=colorize("-" * 60, color="red"),
stack_trace="\n".join(tb_list),
)
else:
stack_trace = ""
return (exception, stack_trace)
def _description(message, exception):
if exception and message:
description = "{message} - {exception}"
elif exception:
description = "{exception}"
elif message:
description = "{message}"
return description.format(message=message, exception=exception)
def _footer():
return "{phrase} Hit us up at {url}, we are always happy to help!".format(
phrase=colorize("Having any troubles?", "yellow"),
url=colorize("https://dvc.org/support", "blue"),
logging.config.dictConfig(
{
"version": 1,
"filters": {"exclude_errors": {"()": ExcludeErrorsFilter}},
"formatters": {"color": {"()": ColorFormatter}},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "color",
"stream": "ext://sys.stdout",
"filters": ["exclude_errors"],
},
"console_errors": {
"class": "logging.StreamHandler",
"level": "ERROR",
"formatter": "color",
"stream": "ext://sys.stderr",
},
},
"loggers": {
"dvc": {
"level": level,
"handlers": ["console", "console_errors"],
}
},
}
)
def _visual_width(line):
"""Get the the number of columns required to display a string"""
return len(re.sub(colorama.ansitowin32.AnsiToWin32.ANSI_CSI_RE, "", line))
def _visual_center(line, width):
"""Center align string according to it's visual width"""
spaces = max(width - _visual_width(line), 0)
left_padding = int(spaces / 2)
right_padding = spaces - left_padding
return (left_padding * " ") + line + (right_padding * " ")
logger = logging.getLogger("dvc") # pylint: disable=invalid-name
set_default_level()
_add_handlers()
_init_colorama()
+19 -11
View File
@@ -2,11 +2,14 @@
from __future__ import unicode_literals
import dvc.logger as logger
import logging
from dvc.cli import parse_args
from dvc.command.base import CmdBase
from dvc.analytics import Analytics
from dvc.exceptions import NotDvcProjectError, DvcParserError
from dvc.exceptions import NotDvcRepoError, DvcParserError
logger = logging.getLogger("dvc")
def main(argv=None):
@@ -20,27 +23,32 @@ def main(argv=None):
"""
args = None
cmd = None
outerLogLevel = logger.level
try:
args = parse_args(argv)
# Init loglevel early in case we'll run
# into errors before setting it properly
CmdBase.set_loglevel(args)
if args.quiet:
logger.setLevel(logging.CRITICAL)
elif args.verbose:
logger.setLevel(logging.DEBUG)
cmd = args.func(args)
ret = cmd.run_cmd()
except KeyboardInterrupt:
logger.error("interrupted by the user")
logger.exception("interrupted by the user")
ret = 252
except NotDvcProjectError:
logger.error()
except NotDvcRepoError:
logger.exception("")
ret = 253
except DvcParserError:
ret = 254
except Exception: # pylint: disable=broad-except
logger.error("unexpected error")
logger.exception("unexpected error")
ret = 255
finally:
logger.setLevel(outerLogLevel)
Analytics().send_cmd(cmd, args, ret)
+80 -28
View File
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import schema
from dvc.config import Config
from dvc.path import Schemes
from dvc.utils.compat import urlparse, str
from dvc.output.base import OutputBase
@@ -26,45 +26,76 @@ OUTS = [
]
OUTS_MAP = {
"hdfs": OutputHDFS,
"s3": OutputS3,
"gs": OutputGS,
"ssh": OutputSSH,
"local": OutputLOCAL,
Schemes.HDFS: OutputHDFS,
Schemes.S3: OutputS3,
Schemes.GS: OutputGS,
Schemes.SSH: OutputSSH,
Schemes.LOCAL: OutputLOCAL,
}
SCHEMA = {
OutputBase.PARAM_PATH: str,
# NOTE: currently there are only 3 possible checksum names:
#
# 1) md5 (LOCAL, SSH, GS);
# 2) etag (S3);
# 3) checksum (HDFS);
#
# so when a few types of outputs share the same name, we only need
# specify it once.
# NOTE: currently there are only 3 possible checksum names:
#
# 1) md5 (LOCAL, SSH, GS);
# 2) etag (S3);
# 3) checksum (HDFS);
#
# so when a few types of outputs share the same name, we only need
# specify it once.
CHECKSUM_SCHEMA = {
schema.Optional(RemoteLOCAL.PARAM_CHECKSUM): schema.Or(str, None),
schema.Optional(RemoteS3.PARAM_CHECKSUM): schema.Or(str, None),
schema.Optional(RemoteHDFS.PARAM_CHECKSUM): schema.Or(str, None),
schema.Optional(OutputBase.PARAM_CACHE): bool,
schema.Optional(OutputBase.PARAM_METRIC): OutputBase.METRIC_SCHEMA,
}
TAGS_SCHEMA = {schema.Optional(str): CHECKSUM_SCHEMA}
def _get(stage, p, info, cache, metric):
SCHEMA = CHECKSUM_SCHEMA.copy()
SCHEMA[OutputBase.PARAM_PATH] = str
SCHEMA[schema.Optional(OutputBase.PARAM_CACHE)] = bool
SCHEMA[schema.Optional(OutputBase.PARAM_METRIC)] = OutputBase.METRIC_SCHEMA
SCHEMA[schema.Optional(OutputBase.PARAM_TAGS)] = TAGS_SCHEMA
SCHEMA[schema.Optional(OutputBase.PARAM_PERSIST)] = bool
def _get(stage, p, info, cache, metric, persist=False, tags=None):
parsed = urlparse(p)
if parsed.scheme == "remote":
name = Config.SECTION_REMOTE_FMT.format(parsed.netloc)
sect = stage.project.config.config[name]
remote = Remote(stage.project, sect)
settings = stage.repo.config.get_remote_settings(parsed.netloc)
remote = Remote(stage.repo, settings)
return OUTS_MAP[remote.scheme](
stage, p, info, cache=cache, remote=remote, metric=metric
stage,
p,
info,
cache=cache,
remote=remote,
metric=metric,
persist=persist,
tags=tags,
)
for o in OUTS:
if o.supported(p):
return o(stage, p, info, cache=cache, remote=None, metric=metric)
return OutputLOCAL(stage, p, info, cache=cache, remote=None, metric=metric)
return o(
stage,
p,
info,
cache=cache,
remote=None,
metric=metric,
persist=persist,
tags=tags,
)
return OutputLOCAL(
stage,
p,
info,
cache=cache,
remote=None,
metric=metric,
persist=persist,
tags=tags,
)
def loadd_from(stage, d_list):
@@ -73,12 +104,33 @@ def loadd_from(stage, d_list):
p = d.pop(OutputBase.PARAM_PATH)
cache = d.pop(OutputBase.PARAM_CACHE, True)
metric = d.pop(OutputBase.PARAM_METRIC, False)
ret.append(_get(stage, p, info=d, cache=cache, metric=metric))
persist = d.pop(OutputBase.PARAM_PERSIST, False)
tags = d.pop(OutputBase.PARAM_TAGS, None)
ret.append(
_get(
stage,
p,
info=d,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
)
return ret
def loads_from(stage, s_list, use_cache=True, metric=False):
def loads_from(stage, s_list, use_cache=True, metric=False, persist=False):
ret = []
for s in s_list:
ret.append(_get(stage, s, info={}, cache=use_cache, metric=metric))
ret.append(
_get(
stage,
s,
info={},
cache=use_cache,
metric=metric,
persist=persist,
)
)
return ret
+166 -39
View File
@@ -1,10 +1,17 @@
from __future__ import unicode_literals
from dvc.utils.compat import str
import re
import logging
from copy import copy
from schema import Or, Optional
from dvc.exceptions import DvcException
from dvc.utils.compat import str
from dvc.remote.base import RemoteBase
logger = logging.getLogger(__name__)
class OutputDoesNotExistError(DvcException):
@@ -28,13 +35,14 @@ class OutputAlreadyTrackedError(DvcException):
class OutputBase(object):
IS_DEPENDENCY = False
REMOTE = None
REMOTE = RemoteBase
PARAM_PATH = "path"
PARAM_CACHE = "cache"
PARAM_METRIC = "metric"
PARAM_METRIC_TYPE = "type"
PARAM_METRIC_XPATH = "xpath"
PARAM_PERSIST = "persist"
METRIC_SCHEMA = Or(
None,
@@ -45,24 +53,33 @@ class OutputBase(object):
},
)
PARAM_TAGS = "tags"
DoesNotExistError = OutputDoesNotExistError
IsNotFileOrDirError = OutputIsNotFileOrDirError
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
self.stage = stage
self.project = stage.project
self.repo = stage.repo
self.url = path
self.info = info
self.remote = remote or self.REMOTE(self.project, {})
self.remote = remote or self.REMOTE(self.repo, {})
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
self.persist = persist
self.tags = None if self.IS_DEPENDENCY else (tags or {})
if (
self.use_cache
and getattr(self.project.cache, self.REMOTE.scheme) is None
):
if self.use_cache and self.cache is None:
raise DvcException(
"no cache location setup for '{}' outputs.".format(
self.REMOTE.scheme
@@ -77,6 +94,23 @@ class OutputBase(object):
def __str__(self):
return self.url
@property
def is_in_repo(self):
return False
@property
def cache(self):
return getattr(self.repo.cache, self.scheme)
@property
def dir_cache(self):
return self.cache.get_dir_cache(self.checksum)
def assign_to_stage_file(self, target_repo):
raise DvcException(
"change repo is not supported for {}".format(self.scheme)
)
@classmethod
def match(cls, url):
return re.match(cls.REMOTE.REGEX, url)
@@ -97,7 +131,11 @@ class OutputBase(object):
@property
def path(self):
return self.path_info["path"]
return self.path_info.path
@property
def cache_path(self):
return self.cache.checksum_to_path(self.checksum)
@property
def sep(self):
@@ -107,45 +145,103 @@ class OutputBase(object):
def checksum(self):
return self.info.get(self.remote.PARAM_CHECKSUM)
@property
def is_dir_checksum(self):
return self.remote.is_dir_checksum(self.checksum)
@property
def exists(self):
return self.remote.exists(self.path_info)
def changed(self):
if not self.exists:
return True
if not self.use_cache:
return self.info != self.remote.save_info(self.path_info)
return getattr(self.project.cache, self.scheme).changed(
self.path_info, self.info
def changed_checksum(self):
return (
self.checksum
!= self.remote.save_info(self.path_info)[
self.remote.PARAM_CHECKSUM
]
)
def changed_cache(self):
if not self.use_cache or not self.checksum:
return True
cache = self.project.cache.__getattribute__(self.scheme)
return cache.changed_cache(self.checksum)
return self.cache.changed_cache(self.checksum)
def status(self):
if self.changed():
# FIXME better msgs
return {str(self): "changed"}
if self.checksum and self.use_cache and self.changed_cache():
return {str(self): "not in cache"}
if not self.exists:
return {str(self): "deleted"}
if self.changed_checksum():
return {str(self): "modified"}
if not self.checksum:
return {str(self): "new"}
return {}
def changed(self):
status = self.status()
logger.debug(str(status))
return bool(status)
@property
def is_empty(self):
return self.remote.is_empty(self.path_info)
def isdir(self):
return self.remote.isdir(self.path_info)
def isfile(self):
return self.remote.isfile(self.path_info)
def save(self):
if not self.exists:
raise self.DoesNotExistError(self)
if not self.isfile and not self.isdir:
raise self.IsNotFileOrDirError(self)
if self.is_empty:
logger.warning("'{}' is empty.".format(self))
if not self.use_cache:
self.info = self.remote.save_info(self.path_info)
else:
self.info = getattr(self.project.cache, self.scheme).save(
self.path_info
if self.metric:
self.verify_metric()
if not self.IS_DEPENDENCY:
logger.info(
"Output '{}' doesn't use cache. Skipping saving.".format(
self
)
)
return
assert not self.IS_DEPENDENCY
if not self.changed():
logger.info(
"Output '{}' didn't change. Skipping saving.".format(self)
)
return
if self.is_in_repo:
if self.repo.scm.is_tracked(self.path):
raise OutputAlreadyTrackedError(self)
if self.use_cache:
self.repo.scm.ignore(self.path)
self.info = self.remote.save_info(self.path_info)
def commit(self):
if self.use_cache:
self.cache.save(self.path_info, self.info)
def dumpd(self):
ret = self.info.copy()
ret = copy(self.info)
ret[self.PARAM_PATH] = self.url
if self.IS_DEPENDENCY:
@@ -161,18 +257,35 @@ class OutputBase(object):
del self.metric[self.PARAM_METRIC_XPATH]
ret[self.PARAM_METRIC] = self.metric
ret[self.PARAM_PERSIST] = self.persist
if self.tags:
ret[self.PARAM_TAGS] = self.tags
return ret
def verify_metric(self):
raise DvcException(
"verify metric is not supported for {}".format(self.scheme)
)
def download(self, to_info, resume=False):
self.remote.download([self.path_info], [to_info], resume=resume)
def checkout(self, force=False):
def checkout(self, force=False, progress_callback=None, tag=None):
if not self.use_cache:
return
getattr(self.project.cache, self.scheme).checkout(
self.path_info, self.info, force=force
if tag:
info = self.tags[tag]
else:
info = self.info
self.cache.checkout(
self.path_info,
info,
force=force,
progress_callback=progress_callback,
)
def remove(self, ignore_remove=False):
@@ -180,17 +293,31 @@ class OutputBase(object):
if self.scheme != "local":
return
if ignore_remove and self.use_cache and self.is_local:
self.project.scm.ignore_remove(self.path)
if ignore_remove and self.use_cache and self.is_in_repo:
self.repo.scm.ignore_remove(self.path)
def move(self, out):
if self.scheme == "local" and self.use_cache and self.is_local:
self.project.scm.ignore_remove(self.path)
if self.scheme == "local" and self.use_cache and self.is_in_repo:
self.repo.scm.ignore_remove(self.path)
self.remote.move(self.path_info, out.path_info)
self.url = out.url
self.path_info = out.path_info
self.path_info = copy(out.path_info)
self.save()
self.commit()
if self.scheme == "local" and self.use_cache and self.is_local:
self.project.scm.ignore(self.path)
if self.scheme == "local" and self.use_cache and self.is_in_repo:
self.repo.scm.ignore(self.path)
def get_files_number(self):
if not self.use_cache or not self.checksum:
return 0
if self.is_dir_checksum:
return len(self.dir_cache)
return 1
def unprotect(self):
if self.exists:
self.remote.unprotect(self.path_info)
+19 -3
View File
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
import posixpath
from dvc.path.hdfs import HDFSPathInfo
from dvc.utils.compat import urlparse
from dvc.output.base import OutputBase
from dvc.remote.hdfs import RemoteHDFS
@@ -11,12 +12,27 @@ class OutputHDFS(OutputBase):
REMOTE = RemoteHDFS
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
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
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 = {"scheme": "hdfs", "user": user, "path": path}
self.path_info = HDFSPathInfo(user=user, url=self.url, path=path)
+43 -50
View File
@@ -1,23 +1,42 @@
from __future__ import unicode_literals
import os
import logging
import dvc.logger as logger
from dvc.path.local import LocalPathInfo
from dvc.utils.compat import urlparse
from dvc.istextfile import istextfile
from dvc.exceptions import DvcException
from dvc.remote.local import RemoteLOCAL
from dvc.output.base import OutputBase, OutputAlreadyTrackedError
from dvc.output.base import OutputBase
logger = logging.getLogger(__name__)
class OutputLOCAL(OutputBase):
REMOTE = RemoteLOCAL
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
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
stage,
path,
info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
if remote:
p = os.path.join(
@@ -28,20 +47,31 @@ class OutputLOCAL(OutputBase):
if not os.path.isabs(p):
p = self.remote.to_ospath(p)
p = os.path.join(stage.cwd, p)
p = os.path.join(stage.wdir, p)
p = os.path.abspath(os.path.normpath(p))
self.path_info = {"scheme": "local", "path": p}
self.path_info = LocalPathInfo(url=self.url, path=p)
def __str__(self):
return self.rel_path
@property
def is_local(self):
def is_in_repo(self):
return urlparse(self.url).scheme != "remote" and not os.path.isabs(
self.url
)
def assign_to_stage_file(self, stage):
from dvc.repo import Repo
fullpath = os.path.abspath(stage.wdir)
self.path_info.path = os.path.join(fullpath, self.stage_path)
self.repo = Repo(self.path)
self.stage = stage
return self
@property
def sep(self):
return os.sep
@@ -51,14 +81,14 @@ class OutputLOCAL(OutputBase):
return os.path.relpath(self.path)
@property
def cache(self):
return self.project.cache.local.get(self.checksum)
def stage_path(self):
return os.path.relpath(self.path, self.stage.wdir)
def dumpd(self):
ret = super(OutputLOCAL, self).dumpd()
if self.is_local:
path = self.remote.unixpath(
os.path.relpath(self.path, self.stage.cwd)
if self.is_in_repo:
path = self.remote.to_posixpath(
os.path.relpath(self.path, self.stage.wdir)
)
else:
path = self.url
@@ -67,7 +97,7 @@ class OutputLOCAL(OutputBase):
return ret
def _verify_metric(self):
def verify_metric(self):
if not self.metric:
return
@@ -81,40 +111,3 @@ class OutputLOCAL(OutputBase):
if not istextfile(self.path):
msg = "binary file '{}' cannot be used as metrics."
raise DvcException(msg.format(self.rel_path))
def save(self):
if not os.path.exists(self.path):
raise self.DoesNotExistError(self.rel_path)
if not os.path.isfile(self.path) and not os.path.isdir(self.path):
raise self.IsNotFileOrDirError(self.rel_path)
if (os.path.isfile(self.path) and os.path.getsize(self.path) == 0) or (
os.path.isdir(self.path) and len(os.listdir(self.path)) == 0
):
msg = "file/directory '{}' is empty.".format(self.rel_path)
logger.warning(msg)
if not self.use_cache:
self.info = self.remote.save_info(self.path_info)
self._verify_metric()
if not self.IS_DEPENDENCY:
msg = "Output '{}' doesn't use cache. Skipping saving."
logger.info(msg.format(self.rel_path))
return
assert not self.IS_DEPENDENCY
if not self.changed():
msg = "Output '{}' didn't change. Skipping saving."
logger.info(msg.format(self.rel_path))
return
if self.is_local:
if self.project.scm.is_tracked(self.path):
raise OutputAlreadyTrackedError(self.rel_path)
if self.use_cache:
self.project.scm.ignore(self.path)
self.info = self.project.cache.local.save(self.path_info)
+22 -7
View File
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
import posixpath
from dvc.path.utils import PathInfo
from dvc.remote.s3 import RemoteS3
from dvc.utils.compat import urlparse
from dvc.output.base import OutputBase
@@ -11,17 +12,31 @@ class OutputS3(OutputBase):
REMOTE = RemoteS3
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
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
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 = {
"scheme": self.scheme,
"bucket": bucket,
"path": path,
}
self.path_info = PathInfo(
self.scheme, bucket=bucket, path=path, url=self.url
)
+21 -9
View File
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import getpass
import posixpath
from dvc.path.ssh import SSHPathInfo
from dvc.utils.compat import urlparse
from dvc.output.base import OutputBase
from dvc.remote.ssh import RemoteSSH
@@ -12,10 +13,25 @@ class OutputSSH(OutputBase):
REMOTE = RemoteSSH
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
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
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
@@ -33,10 +49,6 @@ class OutputSSH(OutputBase):
else:
path = parsed.path
self.path_info = {
"scheme": "ssh",
"host": host,
"port": port,
"user": user,
"path": path,
}
self.path_info = SSHPathInfo(
host=host, user=user, port=port, url=self.url, path=path
)
+34
View File
@@ -0,0 +1,34 @@
from dvc.utils.compat import urlunsplit
class Schemes:
SSH = "ssh"
HDFS = "hdfs"
S3 = "s3"
AZURE = "azure"
HTTP = "http"
GS = "gs"
LOCAL = "local"
OSS = "oss"
class BasePathInfo(object):
scheme = None
def __init__(self, url=None, path=None):
self.url = url
self.path = path
def __str__(self):
return self.url
class DefaultCloudPathInfo(BasePathInfo):
def __init__(self, bucket, url=None, path=None):
super(DefaultCloudPathInfo, 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
+5
View File
@@ -0,0 +1,5 @@
from dvc.path import Schemes, DefaultCloudPathInfo
class AzurePathInfo(DefaultCloudPathInfo):
scheme = Schemes.AZURE
+5
View File
@@ -0,0 +1,5 @@
from dvc.path import Schemes, DefaultCloudPathInfo
class GSPathInfo(DefaultCloudPathInfo):
scheme = Schemes.GS
+15
View File
@@ -0,0 +1,15 @@
from dvc.utils.compat import urlunsplit
from dvc.path import BasePathInfo, Schemes
class HDFSPathInfo(BasePathInfo):
scheme = Schemes.HDFS
def __init__(self, user, url=None, path=None):
super(HDFSPathInfo, 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
+16
View File
@@ -0,0 +1,16 @@
from dvc.path import BasePathInfo, Schemes
from dvc.utils.compat import urlparse, urlunsplit
class HTTPPathInfo(BasePathInfo):
@property
def scheme(self):
if self.path:
return urlparse(self.path).scheme
else:
return Schemes.HTTP
def __str__(self):
if not self.url:
return urlunsplit((self.scheme, self.path, "", "", ""))
return self.url
+10
View File
@@ -0,0 +1,10 @@
import os
from dvc.path import BasePathInfo, Schemes
class LocalPathInfo(BasePathInfo):
scheme = Schemes.LOCAL
def __str__(self):
return os.path.relpath(self.path)
+5
View File
@@ -0,0 +1,5 @@
from dvc.path import Schemes, DefaultCloudPathInfo
class OSSPathInfo(DefaultCloudPathInfo):
scheme = Schemes.OSS
+5
View File
@@ -0,0 +1,5 @@
from dvc.path import Schemes, DefaultCloudPathInfo
class S3PathInfo(DefaultCloudPathInfo):
scheme = Schemes.S3
+25
View File
@@ -0,0 +1,25 @@
from dvc.utils.compat import urlunsplit
from dvc.path import BasePathInfo, Schemes
class SSHPathInfo(BasePathInfo):
scheme = Schemes.SSH
def __init__(self, host, user, port, url=None, path=None):
super(SSHPathInfo, 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
+25
View File
@@ -0,0 +1,25 @@
from dvc.path import Schemes
from dvc.path.azure import AzurePathInfo
from dvc.path.gs import GSPathInfo
from dvc.path.hdfs import HDFSPathInfo
from dvc.path.http import HTTPPathInfo
from dvc.path.local import LocalPathInfo
from dvc.path.oss import OSSPathInfo
from dvc.path.s3 import S3PathInfo
from dvc.path.ssh import SSHPathInfo
PATH_MAP = {
Schemes.SSH: SSHPathInfo,
Schemes.HDFS: HDFSPathInfo,
Schemes.S3: S3PathInfo,
Schemes.AZURE: AzurePathInfo,
Schemes.HTTP: HTTPPathInfo,
Schemes.GS: GSPathInfo,
Schemes.LOCAL: LocalPathInfo,
Schemes.OSS: OSSPathInfo,
}
def PathInfo(scheme, *args, **kwargs):
cls = PATH_MAP[scheme]
return cls(*args, **kwargs)
+43 -8
View File
@@ -1,4 +1,4 @@
"""Manages progress bars for dvc project."""
"""Manages progress bars for dvc repo."""
from __future__ import print_function
from __future__ import unicode_literals
@@ -7,7 +7,8 @@ from dvc.utils.compat import str
import sys
import threading
import dvc.logger as logger
CLEARLINE_PATTERN = "\r\x1b[K"
class Progress(object):
@@ -31,14 +32,20 @@ class Progress(object):
"""Returns if all targets have finished."""
return self._n_total == self._n_finished
def _clearln(self):
self._print("\r\x1b[K", end="")
def clearln(self):
self._print(CLEARLINE_PATTERN, end="")
def _writeln(self, line):
self._clearln()
self.clearln()
self._print(line, end="")
sys.stdout.flush()
def reset(self):
with self._lock:
self._n_total = 0
self._n_finished = 0
self._line = None
def refresh(self, line=None):
"""Refreshes progress bar."""
# Just go away if it is locked. Will update next time
@@ -65,7 +72,7 @@ class Progress(object):
pbar = self._bar(name, 100, 100)
if sys.stdout.isatty():
self._clearln()
self.clearln()
self._print(pbar)
@@ -100,7 +107,11 @@ class Progress(object):
@staticmethod
def _print(*args, **kwargs):
if logger.is_quiet():
import logging
logger = logging.getLogger(__name__)
if logger.getEffectiveLevel() == "CRITICAL":
return
print(*args, **kwargs)
@@ -108,12 +119,36 @@ class Progress(object):
def __enter__(self):
self._lock.acquire(True)
if self._line is not None:
self._clearln()
self.clearln()
def __exit__(self, typ, value, tbck):
if self._line is not None:
self.refresh()
self._lock.release()
def __call__(self, seq, name="", total=None):
if total is None:
total = len(seq)
self.update_target(name, 0, total)
for done, item in enumerate(seq, start=1):
yield item
self.update_target(name, done, total)
self.finish_target(name)
class ProgressCallback(object):
def __init__(self, total):
self.total = total
self.current = 0
progress.reset()
def update(self, name, progress_to_add=1):
self.current += progress_to_add
progress.update_target(name, self.current, self.total)
def finish(self, name):
progress.finish_target(name)
progress = Progress() # pylint: disable=invalid-name
-1396
View File
File diff suppressed because it is too large Load Diff
+10 -7
View File
@@ -4,11 +4,15 @@ from __future__ import unicode_literals
from __future__ import print_function
import sys
import logging
from getpass import getpass
from dvc.utils.compat import input
logger = logging.getLogger(__name__)
def _ask(prompt, limited_to=None):
if not sys.stdout.isatty():
return None
@@ -25,10 +29,9 @@ def _ask(prompt, limited_to=None):
if answer in limited_to:
return answer
print(
"Your response must be one of: {options}. Please try again.".format(
options=limited_to
)
logger.info(
"Your response must be one of: {options}. "
"Please try again.".format(options=limited_to)
)
@@ -36,7 +39,7 @@ def confirm(statement):
"""Ask the user for confirmation about the specified statement.
Args:
statement (str): statement to ask the user confirmation about.
statement (unicode): statement to ask the user confirmation about.
Returns:
bool: whether or not specified statement was confirmed.
@@ -55,5 +58,5 @@ def password(statement):
Returns:
str: password entered by the user.
"""
prompt = "{statement}: ".format(statement=statement)
return getpass(prompt)
logger.info("{statement}: ".format(statement=statement))
return getpass("")
+4 -2
View File
@@ -7,6 +7,7 @@ from dvc.remote.local import RemoteLOCAL
from dvc.remote.s3 import RemoteS3
from dvc.remote.ssh import RemoteSSH
from dvc.remote.http import RemoteHTTP
from dvc.remote.oss import RemoteOSS
REMOTES = [
@@ -16,6 +17,7 @@ REMOTES = [
RemoteHTTP,
RemoteS3,
RemoteSSH,
RemoteOSS,
# NOTE: RemoteLOCAL is the default
]
@@ -27,5 +29,5 @@ def _get(config):
return RemoteLOCAL
def Remote(project, config):
return _get(config)(project, config)
def Remote(repo, config):
return _get(config)(repo, config)
+42 -32
View File
@@ -3,19 +3,27 @@ from __future__ import unicode_literals
import os
import re
import logging
from dvc.path import Schemes
from dvc.path.azure import AzurePathInfo
try:
from azure.storage.blob import BlockBlobService
from azure.common import AzureMissingResourceHttpError
except ImportError:
BlockBlobService = None
import dvc.logger as logger
from dvc.utils.compat import urlparse
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
logger = logging.getLogger(__name__)
class Callback(object):
def __init__(self, name):
self.name = name
@@ -25,7 +33,7 @@ class Callback(object):
class RemoteAzure(RemoteBase):
scheme = "azure"
scheme = Schemes.AZURE
REGEX = (
r"azure://((?P<path>[^=;]*)?|("
# backward compatibility
@@ -36,8 +44,8 @@ class RemoteAzure(RemoteBase):
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
def __init__(self, project, config):
super(RemoteAzure, self).__init__(project, config)
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
@@ -64,8 +72,7 @@ class RemoteAzure(RemoteBase):
raise ValueError("azure storage connection string missing")
self.__blob_service = None
self.path_info = {"scheme": self.scheme, "bucket": self.bucket}
self.path_info = AzurePathInfo(bucket=self.bucket)
@property
def blob_service(self):
@@ -76,20 +83,23 @@ class RemoteAzure(RemoteBase):
connection_string=self.connection_string
)
logger.debug("Container name {}".format(self.bucket))
self.__blob_service.create_container(self.bucket)
try: # verify that container exists
self.__blob_service.list_blobs(
self.bucket, delimiter="/", num_results=1
)
except AzureMissingResourceHttpError:
self.__blob_service.create_container(self.bucket)
return self.__blob_service
def remove(self, path_info):
if path_info["scheme"] != self.scheme:
if path_info.scheme != self.scheme:
raise NotImplementedError
logger.debug(
"Removing azure://{}/{}".format(
path_info["bucket"], path_info["path"]
)
"Removing azure://{}/{}".format(path_info.bucket, path_info.path)
)
self.blob_service.delete_blob(path_info["bucket"], path_info["path"])
self.blob_service.delete_blob(path_info.bucket, path_info.path)
def _list_paths(self, bucket, prefix):
blob_service = self.blob_service
@@ -110,36 +120,36 @@ class RemoteAzure(RemoteBase):
def list_cache_paths(self):
return self._list_paths(self.bucket, self.prefix)
def upload(self, from_infos, to_infos, names=None):
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:
if to_info.scheme != self.scheme:
raise NotImplementedError
if from_info["scheme"] != "local":
if from_info.scheme != "local":
raise NotImplementedError
bucket = to_info["bucket"]
path = to_info["path"]
bucket = to_info.bucket
path = to_info.path
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info["path"], bucket, path
from_info.path, bucket, path
)
)
if not name:
name = os.path.basename(from_info["path"])
name = os.path.basename(from_info.path)
cb = Callback(name)
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
bucket, path, from_info.path, progress_callback=cb
)
except Exception:
msg = "failed to upload '{}'".format(from_info["path"])
msg = "failed to upload '{}'".format(from_info.path)
logger.warning(msg)
else:
progress.finish_target(name)
@@ -155,28 +165,28 @@ class RemoteAzure(RemoteBase):
names = self._verify_path_args(from_infos, to_infos, names)
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info["scheme"] != self.scheme:
if from_info.scheme != self.scheme:
raise NotImplementedError
if to_info["scheme"] != "local":
if to_info.scheme != "local":
raise NotImplementedError
bucket = from_info["bucket"]
path = from_info["path"]
bucket = from_info.bucket
path = from_info.path
logger.debug(
"Downloading '{}/{}' to '{}'".format(
bucket, path, to_info["path"]
bucket, path, to_info.path
)
)
tmp_file = self.tmp_file(to_info["path"])
tmp_file = tmp_fname(to_info.path)
if not name:
name = os.path.basename(to_info["path"])
name = os.path.basename(to_info.path)
cb = None if no_progress_bar else Callback(name)
self._makedirs(to_info["path"])
makedirs(os.path.dirname(to_info.path), exist_ok=True)
try:
self.blob_service.get_blob_to_path(
@@ -186,7 +196,7 @@ class RemoteAzure(RemoteBase):
msg = "failed to download '{}/{}'".format(bucket, path)
logger.warning(msg)
else:
os.rename(tmp_file, to_info["path"])
move(tmp_file, to_info.path)
if not no_progress_bar:
progress.finish_target(name)
+405 -61
View File
@@ -1,20 +1,33 @@
from __future__ import unicode_literals
from copy import copy
from dvc.path.local import LocalPathInfo
from dvc.utils.compat import str
import os
import re
import errno
import json
import ntpath
import logging
import tempfile
import posixpath
from operator import itemgetter
from multiprocessing import cpu_count
import dvc.prompt as prompt
import dvc.logger as logger
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.state import StateBase
logger = logging.getLogger(__name__)
STATUS_OK = 1
STATUS_MISSING = 2
STATUS_NEW = 3
STATUS_DELETED = 4
@@ -22,7 +35,7 @@ STATUS_DELETED = 4
STATUS_MAP = {
# (local_exists, remote_exists)
(True, True): STATUS_OK,
(False, False): STATUS_OK,
(False, False): STATUS_MISSING,
(True, False): STATUS_NEW,
(False, True): STATUS_DELETED,
}
@@ -35,10 +48,18 @@ class DataCloudError(DvcException):
super(DataCloudError, self).__init__("Data sync error: {}".format(msg))
class RemoteBaseCmdError(DvcException):
def __init__(self, cmd, ret, err):
m = "SSH command '{}' finished with non-zero return code '{}': {}"
super(RemoteBaseCmdError, self).__init__(m.format(cmd, ret, err))
class RemoteCmdError(DvcException):
def __init__(self, remote, cmd, ret, err):
super(RemoteCmdError, self).__init__(
"{remote} command '{cmd}' finished with non-zero return code"
" {ret}': {err}".format(remote=remote, cmd=cmd, ret=ret, err=err)
)
class RemoteActionNotImplemented(DvcException):
def __init__(self, action, scheme):
m = "{} is not supported by {} remote".format(action, scheme)
super(RemoteActionNotImplemented, self).__init__(m)
class RemoteMissingDepsError(DvcException):
@@ -51,8 +72,11 @@ class RemoteBase(object):
REQUIRES = {}
JOBS = 4 * cpu_count()
def __init__(self, project, config):
self.project = project
PARAM_RELPATH = "relpath"
CHECKSUM_DIR_SUFFIX = ".dir"
def __init__(self, repo, config):
self.repo = repo
deps_ok = all(self.REQUIRES.values())
if not deps_ok:
missing = [k for k, v in self.REQUIRES.items() if v is None]
@@ -75,12 +99,15 @@ class RemoteBase(object):
"\n"
"If you have installed dvc from a binary package and you "
"are still seeing this message, please report it to us "
"using https://github.com/iterative/dvc/issues. Thank you!".format(
url, missing, " ".join(missing), self.scheme
)
)
"using https://github.com/iterative/dvc/issues. Thank you!"
).format(url, missing, " ".join(missing), self.scheme)
raise RemoteMissingDepsError(msg)
self.protected = False
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")
@@ -94,6 +121,27 @@ class RemoteBase(object):
url = config[Config.SECTION_REMOTE_URL]
return cls.match(url) is not None
@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)
@@ -104,15 +152,136 @@ class RemoteBase(object):
return None
return m.group(name)
@staticmethod
def tmp_file(fname):
""" Temporary name for a partial download """
import uuid
@property
def cache(self):
return getattr(self.repo.cache, self.scheme)
return fname + "." + str(uuid.uuid4())
def get_file_checksum(self, path_info):
raise NotImplementedError
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)
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)
dir_info.append(
{
self.PARAM_RELPATH: relpath,
self.PARAM_CHECKSUM: checksum,
}
)
# NOTE: sorting the list by path to ensure reproducibility
return sorted(dir_info, key=itemgetter(self.PARAM_RELPATH))
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
)
if self.cache.changed_cache_file(checksum):
self.cache.move(from_info, to_info)
self.state.save(path_info, checksum)
self.state.save(to_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(""))
tmp = tempfile.NamedTemporaryFile(delete=False).name
with open(tmp, "w+") as fobj:
json.dump(dir_info, fobj, sort_keys=True)
from_info = LocalPathInfo(path=tmp)
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
def get_dir_cache(self, checksum):
assert checksum
dir_info = self._dir_info.get(checksum)
if dir_info:
return dir_info
dir_info = self.load_dir_cache(checksum)
self._dir_info[checksum] = dir_info
return dir_info
def load_dir_cache(self, checksum):
path_info = self.checksum_to_path_info(checksum)
fobj = tempfile.NamedTemporaryFile(delete=False)
path = fobj.name
to_info = LocalPathInfo(path=path)
self.cache.download([path_info], [to_info], no_progress_bar=True)
try:
with open(path, "r") as fobj:
d = json.load(fobj)
except ValueError:
logger.exception("Failed to load dir cache '{}'".format(path_info))
return []
finally:
os.unlink(path)
if not isinstance(d, list):
msg = "dir cache file format error '{}' [skipping the file]"
logger.error(msg.format(os.path.relpath(path)))
return []
for info in d:
info[self.PARAM_RELPATH] = self.to_ospath(info[self.PARAM_RELPATH])
return d
@classmethod
def is_dir_checksum(cls, checksum):
return checksum.endswith(cls.CHECKSUM_DIR_SUFFIX)
def get_checksum(self, path_info):
if not self.exists(path_info):
return None
checksum = self.state.get(path_info)
if checksum:
return checksum
if self.isdir(path_info):
checksum = self.get_dir_checksum(path_info)
else:
checksum = self.get_file_checksum(path_info)
if checksum:
self.state.save(path_info, checksum)
return checksum
def save_info(self, path_info):
raise NotImplementedError
assert path_info.scheme == self.scheme
return {self.PARAM_CHECKSUM: self.get_checksum(path_info)}
def changed(self, path_info, checksum_info):
"""Checks if data has changed.
@@ -164,9 +333,83 @@ class RemoteBase(object):
logger.debug("'{}' hasn't changed.".format(path_info))
return False
def save(self, path_info):
def link(self, from_info, to_info):
self.copy(from_info, to_info)
def _save_file(self, path_info, checksum, save_link=True):
assert checksum
cache_info = self.checksum_to_path_info(checksum)
if self.changed_cache(checksum):
self.move(path_info, cache_info)
else:
self.remove(path_info)
self.link(cache_info, path_info)
if save_link:
self.state.save_link(path_info)
# we need to update path and cache, since in case of reflink,
# or copy cache type moving original file results in updates on
# next executed command, which causes md5 recalculation
self.state.save(path_info, checksum)
self.state.save(cache_info, checksum)
def _save_dir(self, path_info, checksum):
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_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)
self.state.save(cache_info, checksum)
self.state.save(path_info, checksum)
def is_empty(self, path_info):
return False
def isfile(self, path_info):
raise NotImplementedError
def isdir(self, path_info):
return False
def walk(self, path_info):
raise NotImplementedError
def protect(self, path_info):
pass
def save(self, path_info, checksum_info):
if path_info.scheme != self.scheme:
raise RemoteActionNotImplemented(
"save {} -> {}".format(path_info.scheme, self.scheme),
self.scheme,
)
checksum = checksum_info[self.PARAM_CHECKSUM]
if not self.changed_cache(checksum):
self._checkout(path_info, checksum)
return
self._save(path_info, checksum)
def _save(self, path_info, checksum):
to_info = self.checksum_to_path_info(checksum)
logger.info("Saving '{}' to '{}'.".format(path_info, to_info))
if self.isdir(path_info):
self._save_dir(path_info, checksum)
return
self._save_file(path_info, checksum)
def download(
self,
from_infos,
@@ -175,28 +418,20 @@ class RemoteBase(object):
name=None,
resume=False,
):
raise NotImplementedError
raise RemoteActionNotImplemented("download", self.scheme)
def upload(self, from_infos, to_infos, path_info, names=None):
raise NotImplementedError
def upload(self, from_infos, to_infos, names=None, no_progress_bar=False):
raise RemoteActionNotImplemented("upload", self.scheme)
def remove(self, path_info):
raise NotImplementedError
raise RemoteActionNotImplemented("remove", self.scheme)
def move(self, path_info):
raise NotImplementedError
def move(self, from_info, to_info):
self.copy(from_info, to_info)
self.remove(from_info)
def copy(self, from_info, to_info):
raise NotImplementedError
def _makedirs(self, fname):
dname = os.path.dirname(fname)
try:
os.makedirs(dname)
except OSError as e:
if e.errno != errno.EEXIST:
raise
raise RemoteActionNotImplemented("copy", self.scheme)
def exists(self, path_infos):
raise NotImplementedError
@@ -220,6 +455,7 @@ class RemoteBase(object):
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):
@@ -227,8 +463,8 @@ class RemoteBase(object):
return self.ospath.dirname(relpath) + self.ospath.basename(relpath)
def checksum_to_path_info(self, checksum):
path_info = self.path_info.copy()
path_info["path"] = self.checksum_to_path(checksum)
path_info = copy(self.path_info)
path_info.path = self.checksum_to_path(checksum)
return path_info
def md5s_to_path_infos(self, md5s):
@@ -249,10 +485,13 @@ class RemoteBase(object):
def gc(self, cinfos):
from dvc.remote.local import RemoteLOCAL
used = [info[RemoteLOCAL.PARAM_CHECKSUM] for info in cinfos["local"]]
used = {info[RemoteLOCAL.PARAM_CHECKSUM] for info in cinfos["local"]}
if self.scheme != "":
used += [info[self.PARAM_CHECKSUM] for info in cinfos[self.scheme]]
used |= {
info[self.PARAM_CHECKSUM]
for info in cinfos.get(self.scheme, [])
}
removed = False
for checksum in self.all():
@@ -263,24 +502,51 @@ class RemoteBase(object):
removed = True
return removed
def changed_cache(self, checksum):
cache = self.checksum_to_path_info(checksum)
expected = {self.PARAM_CHECKSUM: checksum}
actual = self.save_info(cache)
def changed_cache_file(self, checksum):
cache_info = self.checksum_to_path_info(checksum)
actual = self.get_checksum(cache_info)
logger.debug(
"Cache '{}' actual '{}'.".format(str(expected), str(actual))
"cache '{}' expected '{}' actual '{}'".format(
str(cache_info), checksum, actual
)
)
if expected != actual:
if self.exists(cache):
msg = "corrupted cache file {}"
logger.warning(msg.format(str(cache)))
self.remove(cache)
if not checksum or not actual:
return True
if actual.split(".")[0] == checksum.split(".")[0]:
return False
if self.exists(cache_info):
logger.warning("corrupted cache file '{}'.".format(cache_info))
self.remove(cache_info)
return True
def _changed_cache_dir(self):
# NOTE: only implemented for RemoteLOCAL
return True
def _changed_dir_cache(self, checksum):
if not self._changed_cache_dir():
return False
if self.changed_cache_file(checksum):
return True
for entry in self.get_dir_cache(checksum):
checksum = entry[self.PARAM_CHECKSUM]
if self.changed_cache_file(checksum):
return True
return False
def changed_cache(self, checksum):
if self.is_dir_checksum(checksum):
return self._changed_dir_cache(checksum)
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
@@ -293,7 +559,7 @@ class RemoteBase(object):
return list(filter(lambda checksum: checksum in checksums, self.all()))
def already_cached(self, path_info):
current = self.save_info(path_info)[self.PARAM_CHECKSUM]
current = self.get_checksum(path_info)
if not current:
return False
@@ -315,29 +581,92 @@ class RemoteBase(object):
self.remove(path_info)
def do_checkout(self, path_info, checksum, force=False):
def _checkout_file(
self, path_info, checksum, force, progress_callback=None
):
cache_info = self.checksum_to_path_info(checksum)
if self.exists(path_info):
msg = "data '{}' exists. Removing before checkout."
logger.warning(msg.format(str(path_info)))
self.safe_remove(path_info, force=force)
from_info = self.checksum_to_path_info(checksum)
self.copy(from_info, path_info)
self.link(cache_info, path_info)
self.state.save_link(path_info)
if progress_callback:
progress_callback.update(path_info.url)
def checkout(self, path_info, checksum_info, force=False):
scheme = path_info["scheme"]
if scheme not in ["", "local"] and scheme != self.scheme:
def makedirs(self, path_info):
raise NotImplementedError
def _checkout_dir(
self, path_info, checksum, force, progress_callback=None
):
# Create dir separately so that dir is created
# even if there are no files in it
if not self.exists(path_info):
self.makedirs(path_info)
dir_info = self.get_dir_cache(checksum)
logger.debug("Linking directory '{}'.".format(path_info))
entry_info = copy(path_info)
for entry in dir_info:
relpath = entry[self.PARAM_RELPATH]
checksum = entry[self.PARAM_CHECKSUM]
entry_cache_info = self.checksum_to_path_info(checksum)
entry_info.url = self.ospath.join(path_info.url, relpath)
entry_info.path = self.ospath.join(path_info.path, relpath)
entry_checksum_info = {self.PARAM_CHECKSUM: checksum}
if self.changed(entry_info, entry_checksum_info):
if self.exists(entry_info):
self.safe_remove(entry_info, force=force)
self.link(entry_cache_info, entry_info)
if progress_callback:
progress_callback.update(entry_info.url)
self._remove_redundant_files(path_info, dir_info, force)
self.state.save_link(path_info)
def _remove_redundant_files(self, path_info, dir_info, force):
existing_files = set(
self.ospath.join(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
)
delta = existing_files - needed_files
d_info = copy(path_info)
for path in delta:
d_info.path = path
self.safe_remove(d_info, 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
):
raise NotImplementedError
checksum = checksum_info.get(self.PARAM_CHECKSUM)
if not checksum:
msg = "No checksum info for '{}'."
logger.info(msg.format(str(path_info)))
logger.debug(msg.format(str(path_info)))
return
if not self.changed(path_info, checksum_info):
msg = "Data '{}' didn't change."
logger.info(msg.format(str(path_info)))
logger.debug(msg.format(str(path_info)))
return
if self.changed_cache(checksum):
@@ -347,6 +676,21 @@ class RemoteBase(object):
return
msg = "Checking out '{}' with cache '{}'."
logger.info(msg.format(str(path_info), checksum))
logger.debug(msg.format(str(path_info), checksum))
self.do_checkout(path_info, checksum, force=force)
return self._checkout(path_info, checksum, force, progress_callback)
def _checkout(
self, path_info, checksum, force=False, progress_callback=None
):
if not self.is_dir_checksum(checksum):
return self._checkout_file(
path_info, checksum, force, progress_callback=progress_callback
)
return self._checkout_dir(
path_info, checksum, force, progress_callback=progress_callback
)
@staticmethod
def unprotect(path_info):
pass
+50 -69
View File
@@ -1,29 +1,35 @@
from __future__ import unicode_literals
import os
import logging
from dvc.path import Schemes
from dvc.path.gs import GSPathInfo
try:
from google.cloud import storage
from google.auth.credentials import Credentials
except ImportError:
storage = None
import dvc.logger as logger
from dvc.utils.compat import urlparse
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
logger = logging.getLogger(__name__)
class RemoteGS(RemoteBase):
scheme = "gs"
scheme = Schemes.GS
REGEX = r"^gs://(?P<path>.*)$"
REQUIRES = {"google.cloud.storage": storage}
PARAM_CHECKSUM = "md5"
def __init__(self, project, config):
super(RemoteGS, self).__init__(project, config)
def __init__(self, repo, config):
super(RemoteGS, self).__init__(repo, config)
storagepath = "gs://"
storagepath += config.get(Config.SECTION_AWS_STORAGEPATH, "/")
storagepath.lstrip("/")
@@ -35,7 +41,7 @@ class RemoteGS(RemoteBase):
self.bucket = parsed.netloc
self.prefix = parsed.path.lstrip("/")
self.path_info = {"scheme": "gs", "bucket": self.bucket}
self.path_info = GSPathInfo(bucket=self.bucket)
@staticmethod
def compat_config(config):
@@ -52,10 +58,12 @@ class RemoteGS(RemoteBase):
else storage.Client(self.projectname)
)
def get_md5(self, bucket, path):
def get_file_checksum(self, path_info):
import base64
import codecs
bucket = path_info.bucket
path = path_info.path
blob = self.gs.bucket(bucket).get_blob(path)
if not blob:
return None
@@ -64,52 +72,28 @@ class RemoteGS(RemoteBase):
md5 = base64.b64decode(b64_md5)
return codecs.getencoder("hex")(md5)[0].decode("utf-8")
def save_info(self, path_info):
if path_info["scheme"] != "gs":
raise NotImplementedError
return {
self.PARAM_CHECKSUM: self.get_md5(
path_info["bucket"], path_info["path"]
)
}
def copy(self, from_info, to_info, gs=None):
gs = gs if gs else self.gs
blob = gs.bucket(from_info["bucket"]).get_blob(from_info["path"])
blob = gs.bucket(from_info.bucket).get_blob(from_info.path)
if not blob:
msg = "'{}' doesn't exist in the cloud".format(from_info["path"])
msg = "'{}' doesn't exist in the cloud".format(from_info.path)
raise DvcException(msg)
bucket = self.gs.bucket(to_info["bucket"])
bucket = self.gs.bucket(to_info.bucket)
bucket.copy_blob(
blob, self.gs.bucket(to_info["bucket"]), new_name=to_info["path"]
blob, self.gs.bucket(to_info.bucket), new_name=to_info.path
)
def save(self, path_info):
if path_info["scheme"] != "gs":
raise NotImplementedError
md5 = self.get_md5(path_info["bucket"], path_info["path"])
path = self.checksum_to_path(md5)
to_info = {"scheme": "gs", "bucket": self.bucket, "path": path}
self.copy(path_info, to_info)
return {self.PARAM_CHECKSUM: md5}
def remove(self, path_info):
if path_info["scheme"] != "gs":
if path_info.scheme != "gs":
raise NotImplementedError
logger.debug(
"Removing gs://{}/{}".format(
path_info["bucket"], path_info["path"]
)
"Removing gs://{}/{}".format(path_info.bucket, path_info.path)
)
blob = self.gs.bucket(path_info["bucket"]).get_blob(path_info["path"])
blob = self.gs.bucket(path_info.bucket).get_blob(path_info.path)
if not blob:
return
@@ -124,44 +108,43 @@ class RemoteGS(RemoteBase):
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] == "gs"
assert path_info.scheme == "gs"
paths = self._list_paths(path_info["bucket"], path_info["path"])
return any(path_info["path"] == path for path in paths)
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):
def upload(self, from_infos, to_infos, names=None, no_progress_bar=False):
names = self._verify_path_args(to_infos, from_infos, names)
gs = self.gs
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info["scheme"] != "gs":
if to_info.scheme != "gs":
raise NotImplementedError
if from_info["scheme"] != "local":
if from_info.scheme != "local":
raise NotImplementedError
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info["path"], to_info["bucket"], to_info["path"]
from_info.path, to_info.bucket, to_info.path
)
)
if not name:
name = os.path.basename(from_info["path"])
name = os.path.basename(from_info.path)
progress.update_target(name, 0, None)
if not no_progress_bar:
progress.update_target(name, 0, None)
try:
bucket = gs.bucket(to_info["bucket"])
blob = bucket.blob(to_info["path"])
blob.upload_from_filename(from_info["path"])
bucket = gs.bucket(to_info.bucket)
blob = bucket.blob(to_info.path)
blob.upload_from_filename(from_info.path)
except Exception:
msg = "failed to upload '{}' to '{}/{}'"
logger.error(
msg.format(
from_info["path"], to_info["bucket"], to_info["path"]
)
logger.exception(
msg.format(from_info.path, to_info.bucket, to_info.path)
)
continue
@@ -180,46 +163,44 @@ class RemoteGS(RemoteBase):
gs = self.gs
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info["scheme"] != "gs":
if from_info.scheme != "gs":
raise NotImplementedError
if to_info["scheme"] == "gs":
if to_info.scheme == "gs":
self.copy(from_info, to_info, gs=gs)
continue
if to_info["scheme"] != "local":
if to_info.scheme != "local":
raise NotImplementedError
msg = "Downloading '{}/{}' to '{}'".format(
from_info["bucket"], from_info["path"], to_info["path"]
from_info.bucket, from_info.path, to_info.path
)
logger.debug(msg)
tmp_file = self.tmp_file(to_info["path"])
tmp_file = tmp_fname(to_info.path)
if not name:
name = os.path.basename(to_info["path"])
name = os.path.basename(to_info.path)
if not no_progress_bar:
# percent_cb is not available for download_to_filename, so
# lets at least update progress at pathpoints(start, finish)
progress.update_target(name, 0, None)
self._makedirs(to_info["path"])
makedirs(os.path.dirname(to_info.path), exist_ok=True)
try:
bucket = gs.bucket(from_info["bucket"])
blob = bucket.get_blob(from_info["path"])
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.error(
msg.format(
from_info["bucket"], from_info["path"], to_info["path"]
)
logger.exception(
msg.format(from_info.bucket, from_info.path, to_info.path)
)
continue
os.rename(tmp_file, to_info["path"])
move(tmp_file, to_info.path)
if not no_progress_bar:
progress.finish_target(name)
+52 -63
View File
@@ -4,25 +4,26 @@ import os
import re
import getpass
import posixpath
import logging
from subprocess import Popen, PIPE
import dvc.logger as logger
from dvc.config import Config
from dvc.remote.base import RemoteBase, RemoteBaseCmdError
from dvc.utils import fix_env
from dvc.path import Schemes
from dvc.path.hdfs import HDFSPathInfo
from dvc.remote.base import RemoteBase, RemoteCmdError
from dvc.utils import fix_env, tmp_fname
class RemoteHDFSCmdError(RemoteBaseCmdError):
pass
logger = logging.getLogger(__name__)
class RemoteHDFS(RemoteBase):
scheme = "hdfs"
scheme = Schemes.HDFS
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
PARAM_CHECKSUM = "checksum"
def __init__(self, project, config):
super(RemoteHDFS, self).__init__(project, config)
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")
@@ -31,7 +32,7 @@ class RemoteHDFS(RemoteBase):
Config.SECTION_REMOTE_USER, getpass.getuser()
)
self.path_info = {"scheme": "hdfs", "user": self.user}
self.path_info = HDFSPathInfo(user=self.user)
def hadoop_fs(self, cmd, user=None):
cmd = "hadoop fs -" + cmd
@@ -55,7 +56,7 @@ class RemoteHDFS(RemoteBase):
)
out, err = p.communicate()
if p.returncode != 0:
raise RemoteHDFSCmdError(cmd, p.returncode, err)
raise RemoteCmdError(self.scheme, cmd, p.returncode, err)
return out.decode("utf-8")
@staticmethod
@@ -64,85 +65,69 @@ class RemoteHDFS(RemoteBase):
assert match is not None
return match.group(gname)
def checksum(self, path_info):
def get_file_checksum(self, path_info):
regex = r".*\t.*\t(?P<checksum>.*)"
stdout = self.hadoop_fs(
"checksum {}".format(path_info["path"]), user=path_info["user"]
"checksum {}".format(path_info.path), user=path_info.user
)
return self._group(regex, stdout, "checksum")
def copy(self, from_info, to_info):
dname = posixpath.dirname(to_info["path"])
self.hadoop_fs("mkdir -p {}".format(dname), user=to_info["user"])
dname = posixpath.dirname(to_info.path)
self.hadoop_fs("mkdir -p {}".format(dname), user=to_info.user)
self.hadoop_fs(
"cp -f {} {}".format(from_info["path"], to_info["path"]),
user=to_info["user"],
"cp -f {} {}".format(from_info.path, to_info.path),
user=to_info.user,
)
def rm(self, path_info):
self.hadoop_fs(
"rm {}".format(path_info["path"]), user=path_info["user"]
)
def save_info(self, path_info):
if path_info["scheme"] != "hdfs":
raise NotImplementedError
assert path_info.get("path")
return {self.PARAM_CHECKSUM: self.checksum(path_info)}
def save(self, path_info):
if path_info["scheme"] != "hdfs":
raise NotImplementedError
assert path_info.get("path")
checksum = self.checksum(path_info)
dest = path_info.copy()
dest["path"] = self.checksum_to_path(checksum)
self.copy(path_info, dest)
return {self.PARAM_CHECKSUM: checksum}
self.hadoop_fs("rm -f {}".format(path_info.path), user=path_info.user)
def remove(self, path_info):
if path_info["scheme"] != "hdfs":
if path_info.scheme != "hdfs":
raise NotImplementedError
assert path_info.get("path")
assert path_info.path
logger.debug("Removing {}".format(path_info["path"]))
logger.debug("Removing {}".format(path_info.path))
self.rm(path_info)
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] == "hdfs"
assert path_info.scheme == "hdfs"
try:
self.hadoop_fs("test -e {}".format(path_info["path"]))
self.hadoop_fs("test -e {}".format(path_info.path))
return True
except RemoteHDFSCmdError:
except RemoteCmdError:
return False
def upload(self, from_infos, to_infos, names=None):
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"] != "hdfs":
if to_info.scheme != "hdfs":
raise NotImplementedError
if from_info["scheme"] != "local":
if from_info.scheme != "local":
raise NotImplementedError
cmd = "mkdir -p {}".format(posixpath.dirname(to_info["path"]))
self.hadoop_fs(cmd, user=to_info["user"])
self.hadoop_fs(
"mkdir -p {}".format(posixpath.dirname(to_info.path)),
user=to_info.user,
)
cmd = "copyFromLocal {} {}".format(
from_info["path"], to_info["path"]
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
)
self.hadoop_fs(cmd, user=to_info["user"])
def download(
self,
@@ -155,29 +140,33 @@ class RemoteHDFS(RemoteBase):
names = self._verify_path_args(from_infos, to_infos, names)
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info["scheme"] != "hdfs":
if from_info.scheme != "hdfs":
raise NotImplementedError
if to_info["scheme"] == "hdfs":
if to_info.scheme == "hdfs":
self.copy(from_info, to_info)
continue
if to_info["scheme"] != "local":
if to_info.scheme != "local":
raise NotImplementedError
dname = os.path.dirname(to_info["path"])
dname = os.path.dirname(to_info.path)
if not os.path.exists(dname):
os.makedirs(dname)
cmd = "copyToLocal {} {}".format(
from_info["path"], to_info["path"]
tmp_file = tmp_fname(to_info.path)
self.hadoop_fs(
"copyToLocal {} {}".format(from_info.path, tmp_file),
user=from_info.user,
)
self.hadoop_fs(cmd, user=from_info["user"])
os.rename(tmp_file, to_info.path)
def list_cache_paths(self):
try:
self.hadoop_fs("test -e {}".format(self.prefix))
except RemoteHDFSCmdError:
except RemoteCmdError:
return []
stdout = self.hadoop_fs("ls -R {}".format(self.prefix))
+30 -31
View File
@@ -1,15 +1,22 @@
from __future__ import unicode_literals
from dvc.utils.compat import open
from dvc.path import Schemes
from dvc.path.http import HTTPPathInfo
from dvc.utils.compat import open, makedirs
import os
import threading
import requests
import logging
import dvc.logger as logger
from dvc.progress import progress
from dvc.exceptions import DvcException
from dvc.config import Config
from dvc.remote.base import RemoteBase
from dvc.utils import move
logger = logging.getLogger(__name__)
class ProgressBarCallback(object):
@@ -26,17 +33,18 @@ class ProgressBarCallback(object):
class RemoteHTTP(RemoteBase):
scheme = "http"
scheme = Schemes.HTTP
REGEX = r"^https?://.*$"
REQUEST_TIMEOUT = 10
CHUNK_SIZE = 2 ** 16
PARAM_CHECKSUM = "etag"
def __init__(self, project, config):
super(RemoteHTTP, self).__init__(project, config)
def __init__(self, repo, config):
super(RemoteHTTP, self).__init__(repo, config)
self.cache_dir = config.get(Config.SECTION_REMOTE_URL)
self.url = self.cache_dir
self.path_info = {"scheme": "http"}
self.path_info = HTTPPathInfo()
@property
def prefix(self):
@@ -53,23 +61,23 @@ class RemoteHTTP(RemoteBase):
names = self._verify_path_args(to_infos, from_infos, names)
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info["scheme"] not in ["http", "https"]:
if from_info.scheme not in ["http", "https"]:
raise NotImplementedError
if to_info["scheme"] != "local":
if to_info.scheme != "local":
raise NotImplementedError
msg = "Downloading '{}' to '{}'".format(
from_info["path"], to_info["path"]
from_info.path, to_info.path
)
logger.debug(msg)
if not name:
name = os.path.basename(to_info["path"])
name = os.path.basename(to_info.path)
self._makedirs(to_info["path"])
makedirs(os.path.dirname(to_info.path), exist_ok=True)
total = self._content_length(from_info["path"])
total = self._content_length(from_info.path)
if no_progress_bar or not total:
cb = None
@@ -78,15 +86,12 @@ class RemoteHTTP(RemoteBase):
try:
self._download_to(
from_info["path"],
to_info["path"],
callback=cb,
resume=resume,
from_info.path, to_info.path, callback=cb, resume=resume
)
except Exception:
msg = "failed to download '{}'".format(from_info["path"])
logger.error(msg)
msg = "failed to download '{}'".format(from_info.path)
logger.exception(msg)
continue
if not no_progress_bar:
@@ -94,8 +99,8 @@ class RemoteHTTP(RemoteBase):
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] in ["http", "https"]
return bool(self._request("HEAD", path_info.get("path")))
assert path_info.scheme in ["http", "https"]
return bool(self._request("HEAD", path_info.path))
def cache_exists(self, md5s):
assert isinstance(md5s, list)
@@ -105,25 +110,19 @@ class RemoteHTTP(RemoteBase):
return list(filter(func, md5s))
def save_info(self, path_info):
if path_info["scheme"] not in ["http", "https"]:
raise NotImplementedError
return {self.PARAM_CHECKSUM: self._etag(path_info["path"])}
def _content_length(self, url):
return self._request("HEAD", url).headers.get("Content-Length")
def _etag(self, url):
def get_file_checksum(self, path_info):
url = path_info.path
etag = self._request("HEAD", url).headers.get("ETag") or self._request(
"HEAD", url
).headers.get("Content-MD5")
if not etag:
raise DvcException(
"could not find an ETag or Content-MD5 header for '{url}'".format(
url=url
)
"could not find an ETag or "
"Content-MD5 header for '{url}'".format(url=url)
)
if etag.startswith("W/"):
@@ -148,7 +147,7 @@ class RemoteHTTP(RemoteBase):
mode, partial_file, request, transferred_bytes, callback
)
os.rename(partial_file, target_file)
move(partial_file, target_file)
def _write_request_content(
self, mode, partial_file, request, transferred_bytes, callback=None
-687
View File
@@ -1,687 +0,0 @@
from __future__ import unicode_literals
from dvc.utils.compat import str
import os
import stat
import uuid
import json
import ntpath
import shutil
import posixpath
from operator import itemgetter
import dvc.logger as logger
from dvc.system import System
from dvc.remote.base import RemoteBase, STATUS_MAP, STATUS_NEW, STATUS_DELETED
from dvc.utils import remove, move, copyfile, dict_md5, to_chunks
from dvc.utils import LARGE_DIR_SIZE
from dvc.config import Config
from dvc.exceptions import DvcException
from dvc.progress import progress
from concurrent.futures import ThreadPoolExecutor
class RemoteLOCAL(RemoteBase):
scheme = "local"
REGEX = r"^(?P<path>.*)$"
PARAM_CHECKSUM = "md5"
PARAM_PATH = "path"
PARAM_RELPATH = "relpath"
MD5_DIR_SUFFIX = ".dir"
CACHE_TYPES = ["reflink", "hardlink", "symlink", "copy"]
CACHE_TYPE_MAP = {
"copy": shutil.copyfile,
"symlink": System.symlink,
"hardlink": System.hardlink,
"reflink": System.reflink,
}
def __init__(self, project, config):
super(RemoteLOCAL, self).__init__(project, config)
self.state = self.project.state if self.project 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:
if isinstance(types, str):
types = [t.strip() for t in types.split(",")]
self.cache_types = types
else:
self.cache_types = self.CACHE_TYPES
if self.cache_dir is not None and not os.path.exists(self.cache_dir):
os.mkdir(self.cache_dir)
self.path_info = {"scheme": "local"}
@staticmethod
def compat_config(config):
ret = config.copy()
url = ret.pop(Config.SECTION_AWS_STORAGEPATH, "")
ret[Config.SECTION_REMOTE_URL] = url
return ret
@property
def url(self):
return self.cache_dir
@property
def prefix(self):
return self.cache_dir
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
for cache in os.listdir(subdir):
clist.append(os.path.join(subdir, cache))
return clist
def get(self, md5):
if not md5:
return None
return os.path.join(self.cache_dir, md5[0:2], md5[2:])
def changed_cache_file(self, md5):
cache = self.get(md5)
if self.state.changed(cache, md5=md5):
if os.path.exists(cache):
msg = "Corrupted cache file {}."
logger.warning(msg.format(os.path.relpath(cache)))
remove(cache)
return True
return False
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] == "local"
return os.path.exists(path_info["path"])
def changed_cache(self, md5):
cache = self.get(md5)
clist = [(cache, md5)]
while True:
if len(clist) == 0:
break
cache, md5 = clist.pop()
if self.changed_cache_file(md5):
return True
if not self.is_dir_cache(cache):
continue
for entry in self.load_dir_cache(md5):
md5 = entry[self.PARAM_CHECKSUM]
cache = self.get(md5)
clist.append((cache, md5))
return False
def link(self, cache, path):
assert os.path.isfile(cache)
dname = os.path.dirname(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()
msg = "Created empty file: {} -> {}".format(cache, path)
logger.debug(msg)
return
i = len(self.cache_types)
while i > 0:
try:
self.CACHE_TYPE_MAP[self.cache_types[0]](cache, path)
if self.protected:
os.chmod(path, stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH)
msg = "Created {}'{}': {} -> {}".format(
"protected " if self.protected else "",
self.cache_types[0],
cache,
path,
)
logger.debug(msg)
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]
i -= 1
raise DvcException("no possible cache types left to try out.")
@property
def ospath(self):
if os.name == "nt":
return ntpath
return posixpath
@classmethod
def to_ospath(cls, path):
if os.name == "nt":
return cls.ntpath(path)
return cls.unixpath(path)
@staticmethod
def unixpath(path):
assert not ntpath.isabs(path)
assert not posixpath.isabs(path)
return path.replace("\\", "/")
@staticmethod
def ntpath(path):
assert not ntpath.isabs(path)
assert not posixpath.isabs(path)
return path.replace("/", "\\")
def collect_dir_cache(self, dname):
dir_info = []
for root, dirs, files in os.walk(str(dname)):
bar = False
if len(files) > LARGE_DIR_SIZE:
msg = (
"Computing md5 for a large directory {}. "
"This is only done once."
)
logger.info(msg.format(os.path.relpath(root)))
bar = True
title = os.path.relpath(root)
processed = 0
total = len(files)
progress.update_target(title, 0, total)
for fname in files:
path = os.path.join(root, fname)
relpath = self.unixpath(os.path.relpath(path, dname))
if bar:
progress.update_target(title, processed, total)
processed += 1
md5 = self.state.update(path)
dir_info.append(
{self.PARAM_RELPATH: relpath, self.PARAM_CHECKSUM: md5}
)
if bar:
progress.finish_target(title)
# NOTE: sorting the list by path to ensure reproducibility
dir_info = sorted(dir_info, key=itemgetter(self.PARAM_RELPATH))
md5 = dict_md5(dir_info) + self.MD5_DIR_SUFFIX
if self.changed_cache_file(md5):
self.dump_dir_cache(md5, dir_info)
return (md5, dir_info)
def load_dir_cache(self, md5):
path = self.get(md5)
assert self.is_dir_cache(path)
try:
with open(path, "r") as fd:
d = json.load(fd)
except Exception:
msg = "Failed to load dir cache '{}'"
logger.error(msg.format(os.path.relpath(path)))
return []
if not isinstance(d, list):
msg = "dir cache file format error '{}' [skipping the file]"
logger.error(msg.format(os.path.relpath(path)))
return []
for info in d:
info["relpath"] = self.to_ospath(info["relpath"])
return d
def dump_dir_cache(self, md5, dir_info):
path = self.get(md5)
dname = os.path.dirname(path)
assert self.is_dir_cache(path)
assert isinstance(dir_info, list)
if not os.path.isdir(dname):
os.makedirs(dname)
# NOTE: Writing first and renaming after that
# to make sure that the operation is atomic.
tmp = "{}.{}".format(path, str(uuid.uuid4()))
with open(tmp, "w+") as fd:
json.dump(dir_info, fd, sort_keys=True)
move(tmp, path)
@classmethod
def is_dir_cache(cls, cache):
return cache.endswith(cls.MD5_DIR_SUFFIX)
def do_checkout(self, path_info, checksum, force=False):
path = path_info["path"]
md5 = checksum
cache = self.get(md5)
if not self.is_dir_cache(cache):
if os.path.exists(path):
self.safe_remove(path_info, force=force)
self.link(cache, path)
self.state.update_link(path)
return
# Create dir separately so that dir is created
# even if there are no files in it
if not os.path.exists(path):
os.makedirs(path)
dir_info = self.load_dir_cache(md5)
dir_relpath = os.path.relpath(path)
dir_size = len(dir_info)
bar = dir_size > LARGE_DIR_SIZE
logger.info("Linking directory '{}'.".format(dir_relpath))
for processed, entry in enumerate(dir_info):
relpath = entry[self.PARAM_RELPATH]
m = entry[self.PARAM_CHECKSUM]
p = os.path.join(path, relpath)
c = self.get(m)
entry_info = {"scheme": path_info["scheme"], self.PARAM_PATH: p}
entry_checksum_info = {self.PARAM_CHECKSUM: m}
if self.changed(entry_info, entry_checksum_info):
if os.path.exists(p):
self.safe_remove(entry_info, force=force)
self.link(c, p)
if bar:
progress.update_target(dir_relpath, processed, dir_size)
self._discard_working_directory_changes(path, dir_info, force=force)
self.state.update_link(path)
if bar:
progress.finish_target(dir_relpath)
def already_cached(self, path_info):
assert path_info["scheme"] in ["", "local"]
current_md5 = self.state.update(path_info["path"])
if not current_md5:
return False
return not self.changed_cache(current_md5)
def _discard_working_directory_changes(self, path, dir_info, force=False):
working_dir_files = set(
os.path.join(root, file)
for root, _, files in os.walk(str(path))
for file in files
)
cached_files = set(
os.path.join(path, file["relpath"]) for file in dir_info
)
delta = working_dir_files - cached_files
for file in delta:
self.safe_remove({"scheme": "local", "path": file}, force=force)
def _move(self, inp, outp):
# 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()))
move(inp, tmp)
move(tmp, outp)
def _save_file(self, path_info):
path = path_info["path"]
md5 = self.state.update(path)
assert md5 is not None
cache = self.get(md5)
if self.changed_cache(md5):
self._move(path, cache)
else:
remove(path)
self.link(cache, path)
self.state.update_link(path)
return {self.PARAM_CHECKSUM: md5}
def _save_dir(self, path_info):
path = path_info["path"]
md5, dir_info = self.state.update_info(path)
dir_relpath = os.path.relpath(path)
dir_size = len(dir_info)
bar = dir_size > LARGE_DIR_SIZE
logger.info("Linking directory '{}'.".format(dir_relpath))
for processed, entry in enumerate(dir_info):
relpath = entry[self.PARAM_RELPATH]
m = entry[self.PARAM_CHECKSUM]
p = os.path.join(path, relpath)
c = self.get(m)
if self.changed_cache(m):
self._move(p, c)
else:
remove(p)
self.link(c, p)
if bar:
progress.update_target(dir_relpath, processed, dir_size)
self.state.update_link(path)
if bar:
progress.finish_target(dir_relpath)
return {self.PARAM_CHECKSUM: md5}
def save(self, path_info):
if path_info["scheme"] != "local":
raise NotImplementedError
path = path_info["path"]
msg = "Saving '{}' to cache '{}'."
logger.info(
msg.format(os.path.relpath(path), os.path.relpath(self.cache_dir))
)
if os.path.isdir(path):
return self._save_dir(path_info)
else:
return self._save_file(path_info)
def save_info(self, path_info):
if path_info["scheme"] != "local":
raise NotImplementedError
return {self.PARAM_CHECKSUM: self.state.update(path_info["path"])}
def remove(self, path_info):
if path_info["scheme"] != "local":
raise NotImplementedError
remove(path_info["path"])
def move(self, from_info, to_info):
if from_info["scheme"] != "local" or to_info["scheme"] != "local":
raise NotImplementedError
move(from_info["path"], to_info["path"])
def cache_exists(self, md5s):
assert isinstance(md5s, list)
return list(filter(lambda md5: not self.changed_cache_file(md5), md5s))
def upload(self, from_infos, to_infos, names=None):
names = self._verify_path_args(to_infos, from_infos, names)
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info["scheme"] != "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"])
self._makedirs(to_info["path"])
try:
copyfile(from_info["path"], to_info["path"], name=name)
except Exception:
logger.error(
"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,
):
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"] != "local":
raise NotImplementedError
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"])
self._makedirs(to_info["path"])
tmp_file = self.tmp_file(to_info["path"])
try:
copyfile(
from_info["path"],
tmp_file,
no_progress_bar=no_progress_bar,
name=name,
)
except Exception:
logger.error(
"failed to download '{}' to '{}'".format(
from_info["path"], to_info["path"]
)
)
continue
os.rename(tmp_file, to_info["path"])
def _group(self, checksum_infos, show_checksums=False):
by_md5 = {}
for info in checksum_infos:
md5 = info[self.PARAM_CHECKSUM]
if show_checksums:
by_md5[md5] = {"name": md5}
continue
name = info[self.PARAM_PATH]
branch = info.get("branch")
if branch:
name += "({})".format(branch)
if md5 not in by_md5.keys():
by_md5[md5] = {"name": name}
else:
by_md5[md5]["name"] += " " + name
return by_md5
def status(self, checksum_infos, remote, jobs=None, show_checksums=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)
remote_exists = list(remote.cache_exists(md5s))
progress.update_target(title, 90, 100)
local_exists = self.cache_exists(md5s)
progress.finish_target(title)
for md5, info in ret.items():
info["status"] = STATUS_MAP[
(md5 in local_exists, md5 in remote_exists)
]
return ret
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():
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
from_infos = path_infos
else:
to_infos = path_infos
from_infos = cache
return list(
zip(
to_chunks(from_infos, jobs),
to_chunks(to_infos, jobs),
to_chunks(names, jobs),
)
)
def _process(
self,
checksum_infos,
remote,
jobs=None,
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,
)
)
if download:
func = remote.download
status = STATUS_DELETED
else:
func = remote.upload
status = STATUS_NEW
if jobs is None:
jobs = remote.JOBS
status_info = self.status(
checksum_infos, remote, jobs=jobs, show_checksums=show_checksums
)
chunks = self._get_chunks(download, remote, status_info, status, jobs)
if len(chunks) == 0:
return
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)
for f in futures:
f.result()
def push(self, checksum_infos, remote, jobs=None, show_checksums=False):
self._process(
checksum_infos,
remote,
jobs=jobs,
show_checksums=show_checksums,
download=False,
)
def pull(self, checksum_infos, remote, jobs=None, show_checksums=False):
self._process(
checksum_infos,
remote,
jobs=jobs,
show_checksums=show_checksums,
download=True,
)
+576
View File
@@ -0,0 +1,576 @@
from __future__ import unicode_literals
from copy import copy
from dvc.path import BasePathInfo, Schemes
from dvc.path.local import LocalPathInfo
from dvc.remote.local.slow_link_detection import slow_link_guard
from dvc.utils.compat import str, makedirs
import os
import stat
import uuid
import ntpath
import shutil
import posixpath
import logging
from dvc.system import System
from dvc.remote.base import (
RemoteBase,
STATUS_MAP,
STATUS_NEW,
STATUS_DELETED,
STATUS_MISSING,
)
from dvc.utils import (
remove,
move,
copyfile,
to_chunks,
tmp_fname,
file_md5,
walk_files,
)
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
logger = logging.getLogger(__name__)
class RemoteLOCAL(RemoteBase):
scheme = Schemes.LOCAL
REGEX = r"^(?P<path>.*)$"
PARAM_CHECKSUM = "md5"
PARAM_PATH = "path"
DEFAULT_CACHE_TYPES = ["reflink", "copy"]
CACHE_TYPE_MAP = {
"copy": shutil.copyfile,
"symlink": System.symlink,
"hardlink": System.hardlink,
"reflink": System.reflink,
}
def __init__(self, repo, config):
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:
if isinstance(types, str):
types = [t.strip() for t in types.split(",")]
self.cache_types = types
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)
self._dir_info = {}
self.path_info = LocalPathInfo()
@staticmethod
def compat_config(config):
ret = config.copy()
url = ret.pop(Config.SECTION_AWS_STORAGEPATH, "")
ret[Config.SECTION_REMOTE_URL] = url
return ret
@property
def url(self):
return self.cache_dir
@property
def prefix(self):
return self.cache_dir
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
for cache in os.listdir(subdir):
clist.append(os.path.join(subdir, cache))
return clist
def get(self, md5):
if not md5:
return None
return self.checksum_to_path(md5)
def exists(self, path_info):
assert isinstance(path_info, BasePathInfo)
assert path_info.scheme == "local"
return os.path.lexists(path_info.path)
def makedirs(self, path_info):
if not self.exists(path_info):
os.makedirs(path_info.path)
@slow_link_guard
def link(self, cache_info, path_info):
cache = cache_info.path
path = path_info.path
assert os.path.isfile(cache)
dname = os.path.dirname(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()
msg = "Created empty file: {} -> {}".format(cache, path)
logger.debug(msg)
return
i = len(self.cache_types)
while i > 0:
try:
self.CACHE_TYPE_MAP[self.cache_types[0]](cache, path)
if self.protected:
self.protect(path_info)
msg = "Created {}'{}': {} -> {}".format(
"protected " if self.protected else "",
self.cache_types[0],
cache,
path,
)
logger.debug(msg)
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]
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"]
current_md5 = self.get_checksum(path_info)
if not current_md5:
return False
return not self.changed_cache(current_md5)
def is_empty(self, path_info):
path = path_info.path
if self.isfile(path_info) and os.path.getsize(path) == 0:
return True
if self.isdir(path_info) and len(os.listdir(path)) == 0:
return True
return False
def isfile(self, path_info):
return os.path.isfile(path_info.path)
def isdir(self, path_info):
return os.path.isdir(path_info.path)
def walk(self, path_info):
return os.walk(path_info.path)
def get_file_checksum(self, path_info):
return file_md5(path_info.path)[0]
def remove(self, path_info):
if path_info.scheme != "local":
raise NotImplementedError
remove(path_info.path)
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
# 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()))
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))
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,
):
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 != "local":
raise NotImplementedError
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 _group(self, checksum_infos, show_checksums=False):
by_md5 = {}
for info in checksum_infos:
md5 = info[self.PARAM_CHECKSUM]
if show_checksums:
by_md5[md5] = {"name": md5}
continue
name = info[self.PARAM_PATH]
branch = info.get("branch")
if branch:
name += "({})".format(branch)
if md5 not in by_md5.keys():
by_md5[md5] = {"name": name}
else:
by_md5[md5]["name"] += " " + name
return by_md5
def status(
self,
checksum_infos,
remote,
jobs=None,
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)
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
# those files.
if download and sorted(local_exists) == sorted(md5s):
remote_exists = local_exists
else:
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)
return ret
def _fill_statuses(self, checksum_info_dir, local_exists, remote_exists):
# Using sets because they are way faster for lookups
local = set(local_exists)
remote = set(remote_exists)
for md5, info in checksum_info_dir.items():
status = STATUS_MAP[(md5 in local, md5 in remote)]
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():
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
from_infos = path_infos
else:
to_infos = path_infos
from_infos = cache
return list(
zip(
to_chunks(from_infos, jobs),
to_chunks(to_infos, jobs),
to_chunks(names, jobs),
)
)
def _process(
self,
checksum_infos,
remote,
jobs=None,
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,
)
)
if download:
func = remote.download
status = STATUS_DELETED
else:
func = remote.upload
status = STATUS_NEW
if jobs is None:
jobs = remote.JOBS
status_info = self.status(
checksum_infos,
remote,
jobs=jobs,
show_checksums=show_checksums,
download=download,
)
chunks = self._get_chunks(download, remote, status_info, status, jobs)
if len(chunks) == 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)
for f in futures:
f.result()
return len(chunks)
def push(self, checksum_infos, remote, jobs=None, show_checksums=False):
return self._process(
checksum_infos,
remote,
jobs=jobs,
show_checksums=show_checksums,
download=False,
)
def pull(self, checksum_infos, remote, jobs=None, show_checksums=False):
return self._process(
checksum_infos,
remote,
jobs=jobs,
show_checksums=show_checksums,
download=True,
)
def _changed_cache_dir(self):
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
return changed
def _log_missing_caches(self, checksum_info_dict):
missing_caches = [
(md5, info)
for md5, info in checksum_info_dict.items()
if info["status"] == STATUS_MISSING
]
if missing_caches:
missing_desc = "".join(
[
"\nname: {}, md5: {}".format(info["name"], md5)
for md5, info in missing_caches
]
)
msg = (
"Some of the cache files do not exist neither locally "
"nor on remote. Missing cache files: {}".format(missing_desc)
)
logger.warning(msg)
@staticmethod
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()))
# 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)),
)
remove(path)
os.rename(tmp, path)
else:
logger.debug(
"Skipping copying for '{}', since it is not "
"a symlink or a hardlink.".format(path)
)
os.chmod(path, os.stat(path).st_mode | stat.S_IWRITE)
@staticmethod
def _unprotect_dir(path):
for path in walk_files(path):
RemoteLOCAL._unprotect_file(path)
@staticmethod
def unprotect(path_info):
path = path_info.path
if not os.path.exists(path):
raise DvcException(
"can't unprotect non-existing data '{}'".format(path)
)
if os.path.isdir(path):
RemoteLOCAL._unprotect_dir(path)
else:
RemoteLOCAL._unprotect_file(path)
@staticmethod
def protect(path_info):
os.chmod(path_info.path, stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH)
+63
View File
@@ -0,0 +1,63 @@
import logging
import time
import colorama
from dvc.config import Config
logger = logging.getLogger(__name__)
class SlowLinkDetectorDecorator(object):
LINKING_TIMEOUT_SECONDS = 10.0
was_displayed = False
@classmethod
def should_display(cls):
if not cls.was_displayed:
cls.was_displayed = True
return True
return False
def __init__(self, method):
self.method = method
def __call__(self, *args, **kwargs):
start = time.time()
result = self.method(*args, **kwargs)
execution_time_seconds = time.time() - start
if (
execution_time_seconds >= self.LINKING_TIMEOUT_SECONDS
and self.should_display()
):
msg = (
"You can cut execution time considerably. Check:\n"
"{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(
blue=colorama.Fore.BLUE, reset=colorama.Fore.RESET
)
)
logger.warning(msg)
return result
def slow_link_guard(method):
def call(remote_local, *args, **kwargs):
cache_config = remote_local.repo.config.config.get(
Config.SECTION_CACHE
)
should_warn = cache_config.get(
Config.SECTION_CACHE_SLOW_LINK_WARNING, True
) and not cache_config.get(Config.SECTION_CACHE_TYPE, None)
if should_warn:
decorated = SlowLinkDetectorDecorator(method)
return decorated(remote_local, *args, **kwargs)
return method(remote_local, *args, **kwargs)
return call
+192
View File
@@ -0,0 +1,192 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import os
import logging
from dvc.path import Schemes
from dvc.path.oss import OSSPathInfo
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
logger = logging.getLogger(__name__)
class RemoteOSS(RemoteBase):
"""
oss2 document:
https://www.alibabacloud.com/help/doc-detail/32026.htm
Examples
----------
$ dvc remote add myremote oss://my-bucket/path
Set key id, key secret and endpoint using modify command
$ dvc remote modify myremote oss_key_id my-key-id
$ dvc remote modify myremote oss_key_secret my-key-secret
$ dvc remote modify myremote oss_endpoint endpoint
or environment variables
$ export OSS_ACCESS_KEY_ID="my-key-id"
$ export OSS_ACCESS_KEY_SECRET="my-key-secret"
$ export OSS_ENDPOINT="endpoint"
"""
scheme = Schemes.OSS
REGEX = r"^oss://(?P<path>.*)?$"
REQUIRES = {"oss2": oss2}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
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("/")
self.endpoint = config.get(Config.SECTION_OSS_ENDPOINT) or os.getenv(
"OSS_ENDPOINT"
)
self.key_id = (
config.get(Config.SECTION_OSS_ACCESS_KEY_ID)
or os.getenv("OSS_ACCESS_KEY_ID")
or "defaultId"
)
self.key_secret = (
config.get(Config.SECTION_OSS_ACCESS_KEY_SECRET)
or os.getenv("OSS_ACCESS_KEY_SECRET")
or "defaultSecret"
)
self._bucket = None
self.path_info = OSSPathInfo(bucket=self.bucket)
@property
def oss_service(self):
if self._bucket is None:
logger.debug("URL {}".format(self.url))
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)
try: # verify that bucket exists
self._bucket.get_bucket_info()
except oss2.exceptions.NoSuchBucket:
self._bucket.create_bucket(
oss2.BUCKET_ACL_PUBLIC_READ,
oss2.models.BucketCreateConfig(
oss2.BUCKET_STORAGE_CLASS_STANDARD
),
)
return self._bucket
def remove(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
logger.debug(
"Removing oss://{}/{}".format(path_info.bucket, path_info.path)
)
self.oss_service.delete_object(path_info.path)
def _list_paths(self, prefix):
for blob in oss2.ObjectIterator(self.oss_service, prefix=prefix):
yield blob.key
def list_cache_paths(self):
return self._list_paths(self.prefix)
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,
):
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
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)
+149 -92
View File
@@ -2,18 +2,24 @@ from __future__ import unicode_literals
import os
import threading
import logging
from dvc.path import Schemes
from dvc.path.s3 import S3PathInfo
try:
import boto3
except ImportError:
boto3 = None
import dvc.logger as logger
from dvc.utils.compat import urlparse
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
from dvc.exceptions import DvcException, ETagMismatchError
logger = logging.getLogger(__name__)
class Callback(object):
@@ -30,13 +36,13 @@ class Callback(object):
class RemoteS3(RemoteBase):
scheme = "s3"
scheme = Schemes.S3
REGEX = r"^s3://(?P<path>.*)$"
REQUIRES = {"boto3": boto3}
PARAM_CHECKSUM = "etag"
def __init__(self, project, config):
super(RemoteS3, self).__init__(project, config)
def __init__(self, repo, config):
super(RemoteS3, self).__init__(repo, config)
storagepath = "s3://{}".format(
config.get(Config.SECTION_AWS_STORAGEPATH, "").lstrip("/")
@@ -54,6 +60,8 @@ class RemoteS3(RemoteBase):
self.endpoint_url = config.get(Config.SECTION_AWS_ENDPOINT_URL)
self.list_objects = config.get(Config.SECTION_AWS_LIST_OBJECTS)
self.use_ssl = config.get(Config.SECTION_AWS_USE_SSL, True)
shared_creds = config.get(Config.SECTION_AWS_CREDENTIALPATH)
@@ -64,7 +72,7 @@ class RemoteS3(RemoteBase):
self.bucket = parsed.netloc
self.prefix = parsed.path.lstrip("/")
self.path_info = {"scheme": self.scheme, "bucket": self.bucket}
self.path_info = S3PathInfo(bucket=self.bucket)
@staticmethod
def compat_config(config):
@@ -83,125 +91,177 @@ class RemoteS3(RemoteBase):
"s3", endpoint_url=self.endpoint_url, use_ssl=self.use_ssl
)
def get_etag(self, bucket, path):
try:
obj = self.s3.head_object(Bucket=bucket, Key=path)
except Exception:
raise DvcException(
"s3://{}/{} does not exist".format(bucket, path)
)
@classmethod
def get_etag(cls, s3, bucket, path):
obj = cls.get_head_object(s3, bucket, path)
return obj["ETag"].strip('"')
def save_info(self, path_info):
if path_info["scheme"] != "s3":
raise NotImplementedError
def get_file_checksum(self, path_info):
return self.get_etag(self.s3, path_info.bucket, path_info.path)
return {
self.PARAM_CHECKSUM: self.get_etag(
path_info["bucket"], path_info["path"]
@staticmethod
def get_head_object(s3, bucket, path, *args, **kwargs):
try:
obj = s3.head_object(Bucket=bucket, Key=path, *args, **kwargs)
except Exception as exc:
raise DvcException(
"s3://{}/{} does not exist".format(bucket, path), exc
)
}
return obj
@classmethod
def _copy_multipart(cls, s3, from_info, to_info, size, n_parts):
mpu = s3.create_multipart_upload(
Bucket=to_info.bucket, Key=to_info.path
)
mpu_id = mpu["UploadId"]
parts = []
byte_position = 0
for i in range(1, n_parts + 1):
obj = cls.get_head_object(
s3, from_info.bucket, from_info.path, PartNumber=i
)
part_size = obj["ContentLength"]
lastbyte = byte_position + part_size - 1
if lastbyte > size:
lastbyte = size - 1
srange = "bytes={}-{}".format(byte_position, lastbyte)
part = s3.upload_part_copy(
Bucket=to_info.bucket,
Key=to_info.path,
PartNumber=i,
UploadId=mpu_id,
CopySourceRange=srange,
CopySource={"Bucket": from_info.bucket, "Key": from_info.path},
)
parts.append(
{"PartNumber": i, "ETag": part["CopyPartResult"]["ETag"]}
)
byte_position += part_size
assert n_parts == len(parts)
s3.complete_multipart_upload(
Bucket=to_info.bucket,
Key=to_info.path,
UploadId=mpu_id,
MultipartUpload={"Parts": parts},
)
@classmethod
def _copy(cls, s3, from_info, to_info):
# 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
# whether an object would be uploaded as a single part or as a
# multipart.
#
# If an object's etag looks like '8978c98bb5a48c2fb5f2c4c905768afa',
# then it was transfered as a single part, which means that the chunk
# size used to transfer it was greater or equal to the ContentLength
# of that object. So to preserve that tag over the next transfer, we
# could use any value >= ContentLength.
#
# If an object's etag looks like '50d67013a5e1a4070bef1fc8eea4d5f9-13',
# then it was transfered as a multipart, which means that the chunk
# size used to transfer it was less than ContentLength of that object.
# Unfortunately, in general, it doesn't mean that the chunk size was
# the same throughout the transfer, so it means that in order to
# preserve etag, we need to transfer each part separately, so the
# object is transfered in the same chunks as it was originally.
obj = cls.get_head_object(s3, from_info.bucket, from_info.path)
etag = obj["ETag"].strip('"')
size = obj["ContentLength"]
_, _, parts_suffix = etag.partition("-")
if parts_suffix:
n_parts = int(parts_suffix)
cls._copy_multipart(s3, from_info, to_info, size, n_parts)
else:
source = {"Bucket": from_info.bucket, "Key": from_info.path}
s3.copy(source, to_info.bucket, to_info.path)
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
source = {"Bucket": from_info["bucket"], "Key": from_info["path"]}
self.s3.copy(source, to_info["bucket"], to_info["path"])
def save(self, path_info):
if path_info["scheme"] != "s3":
raise NotImplementedError
etag = self.get_etag(path_info["bucket"], path_info["path"])
path = self.checksum_to_path(etag)
to_info = {"scheme": "s3", "bucket": self.bucket, "path": path}
self.copy(path_info, to_info)
return {self.PARAM_CHECKSUM: etag}
self._copy(s3, from_info, to_info)
def remove(self, path_info):
if path_info["scheme"] != "s3":
if path_info.scheme != "s3":
raise NotImplementedError
logger.debug(
"Removing s3://{}/{}".format(
path_info["bucket"], path_info["path"]
)
"Removing s3://{}/{}".format(path_info.bucket, path_info.path)
)
self.s3.delete_object(
Bucket=path_info["bucket"], Key=path_info["path"]
)
self.s3.delete_object(Bucket=path_info.bucket, Key=path_info.path)
def _list_paths(self, bucket, prefix):
""" Read config for list object api, paginate through list objects."""
s3 = self.s3
kwargs = {"Bucket": bucket, "Prefix": prefix}
while True:
# NOTE: list_objects_v2() is 90% faster than head_object [1]
#
# [1] https://www.peterbe.com/plog/
# fastest-way-to-find-out-if-a-file-exists-in-s3
resp = s3.list_objects_v2(**kwargs)
contents = resp.get("Contents", None)
if self.list_objects:
list_objects_api = "list_objects"
else:
list_objects_api = "list_objects_v2"
paginator = s3.get_paginator(list_objects_api)
for page in paginator.paginate(**kwargs):
contents = page.get("Contents", None)
if not contents:
break
for obj in contents:
yield obj["Key"]
token = resp.get("NextContinuationToken", None)
if not token:
break
kwargs["ContinuationToken"] = token
continue
for item in contents:
yield item["Key"]
def list_cache_paths(self):
return self._list_paths(self.bucket, self.prefix)
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] == "s3"
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)
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):
def upload(self, from_infos, to_infos, names=None, no_progress_bar=False):
names = self._verify_path_args(to_infos, from_infos, names)
s3 = self.s3
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info["scheme"] != "s3":
if to_info.scheme != "s3":
raise NotImplementedError
if from_info["scheme"] != "local":
if from_info.scheme != "local":
raise NotImplementedError
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info["path"], to_info["bucket"], to_info["path"]
from_info.path, to_info.bucket, to_info.path
)
)
if not name:
name = os.path.basename(from_info["path"])
name = os.path.basename(from_info.path)
total = os.path.getsize(from_info["path"])
cb = Callback(name, total)
total = os.path.getsize(from_info.path)
cb = None if no_progress_bar else Callback(name, total)
try:
s3.upload_file(
from_info["path"],
to_info["bucket"],
to_info["path"],
Callback=cb,
from_info.path, to_info.bucket, to_info.path, Callback=cb
)
except Exception:
msg = "failed to upload '{}'".format(from_info["path"])
logger.error(msg)
msg = "failed to upload '{}'".format(from_info.path)
logger.exception(msg)
continue
progress.finish_target(name)
@@ -219,50 +279,47 @@ class RemoteS3(RemoteBase):
s3 = self.s3
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info["scheme"] != "s3":
if from_info.scheme != "s3":
raise NotImplementedError
if to_info["scheme"] == "s3":
if to_info.scheme == "s3":
self.copy(from_info, to_info, s3=s3)
continue
if to_info["scheme"] != "local":
if to_info.scheme != "local":
raise NotImplementedError
msg = "Downloading '{}/{}' to '{}'".format(
from_info["bucket"], from_info["path"], to_info["path"]
from_info.bucket, from_info.path, to_info.path
)
logger.debug(msg)
tmp_file = self.tmp_file(to_info["path"])
tmp_file = tmp_fname(to_info.path)
if not name:
name = os.path.basename(to_info["path"])
name = os.path.basename(to_info.path)
self._makedirs(to_info["path"])
makedirs(os.path.dirname(to_info.path), exist_ok=True)
try:
if no_progress_bar:
cb = None
else:
total = s3.head_object(
Bucket=from_info["bucket"], Key=from_info["path"]
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,
from_info.bucket, from_info.path, tmp_file, Callback=cb
)
except Exception:
msg = "failed to download '{}/{}'".format(
from_info["bucket"], from_info["path"]
from_info.bucket, from_info.path
)
logger.error(msg)
logger.exception(msg)
continue
os.rename(tmp_file, to_info["path"])
move(tmp_file, to_info.path)
if not no_progress_bar:
progress.finish_target(name)
-394
View File
@@ -1,394 +0,0 @@
from __future__ import unicode_literals
import os
import getpass
import posixpath
try:
import paramiko
except ImportError:
paramiko = None
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.config import Config
from dvc.progress import progress
from dvc.utils.compat import urlparse
from dvc.exceptions import DvcException
from dvc.remote.base import RemoteBase, RemoteBaseCmdError
def sizeof_fmt(num, suffix="B"):
""" Convert number of bytes to human-readable string """
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, "Y", suffix)
def percent_cb(name, complete, total):
""" Callback for updating target progress """
logger.debug(
"{}: {} transferred out of {}".format(
name, sizeof_fmt(complete), sizeof_fmt(total)
)
)
progress.update_target(name, complete, total)
def create_cb(name):
""" Create callback function for multipart object """
return lambda cur, tot: percent_cb(name, cur, tot)
class RemoteSSHCmdError(RemoteBaseCmdError):
pass
class RemoteSSH(RemoteBase):
scheme = "ssh"
# NOTE: we support both URL-like (ssh://[user@]host.xz[:port]/path) and
# SCP-like (ssh://[user@]host.xz:path) urls.
REGEX = r"^ssh://.*$"
REQUIRES = {"paramiko": paramiko}
JOBS = 4
PARAM_CHECKSUM = "md5"
DEFAULT_PORT = 22
TIMEOUT = 1800
def __init__(self, project, config):
super(RemoteSSH, self).__init__(project, config)
self.url = config.get(Config.SECTION_REMOTE_URL, "ssh://")
parsed = urlparse(self.url)
self.host = parsed.hostname
self.user = (
config.get(Config.SECTION_REMOTE_USER)
or parsed.username
or getpass.getuser()
)
self.prefix = parsed.path
self.port = (
config.get(Config.SECTION_REMOTE_PORT)
or parsed.port
or self.DEFAULT_PORT
)
self.keyfile = config.get(Config.SECTION_REMOTE_KEY_FILE, None)
self.timeout = config.get(Config.SECTION_REMOTE_TIMEOUT, self.TIMEOUT)
self.password = config.get(Config.SECTION_REMOTE_PASSWORD, None)
self.ask_password = config.get(
Config.SECTION_REMOTE_ASK_PASSWORD, False
)
self.path_info = {
"scheme": "ssh",
"host": self.host,
"user": self.user,
"port": self.port,
}
def ssh(self, host=None, user=None, port=None):
msg = (
"Establishing ssh connection with '{}' "
"through port '{}' as user '{}'"
)
logger.debug(msg.format(host, port, user))
ssh = paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if self.ask_password and self.password is None:
msg = (
"Enter a private key passphrase or a password for "
"host '{}' port '{}' user '{}'"
).format(host, port, user)
self.password = prompt.password(msg)
ssh.connect(
host,
username=user,
port=port,
key_filename=self.keyfile,
timeout=self.timeout,
password=self.password,
)
return ssh
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info["scheme"] == "ssh"
with self.ssh(
path_info["host"], path_info["user"], path_info["port"]
) as ssh:
try:
self._exec(ssh, "test -e {}".format(path_info["path"]))
exists = True
except RemoteSSHCmdError:
exists = False
return exists
def _exec(self, ssh, cmd):
stdin, stdout, stderr = ssh.exec_command(cmd)
channel = stdout.channel
stdin.close()
channel.shutdown_write()
stdout_chunks = []
stderr_chunks = []
while (
not channel.closed
or channel.recv_ready()
or channel.recv_stderr_ready()
):
import select
got_chunk = False
readq, _, _ = select.select([stdout.channel], [], [], self.timeout)
for c in readq:
if c.recv_ready():
stdout_chunks.append(stdout.channel.recv(len(c.in_buffer)))
got_chunk = True
if c.recv_stderr_ready():
stderr_len = len(c.in_stderr_buffer)
s = stderr.channel.recv_stderr(stderr_len)
stderr_chunks.append(s)
got_chunk = True
if (
not got_chunk
and stdout.channel.exit_status_ready()
and not stderr.channel.recv_stderr_ready()
and not stdout.channel.recv_ready()
):
stdout.channel.shutdown_read()
stdout.channel.close()
break
stdout.close()
stderr.close()
ret = stdout.channel.recv_exit_status()
if ret != 0:
err = b"".join(stderr_chunks).decode("utf-8")
raise RemoteSSHCmdError(cmd, ret, err)
return b"".join(stdout_chunks).decode("utf-8")
def md5(self, path_info):
if path_info["scheme"] != "ssh":
raise NotImplementedError
ssh = self.ssh(
host=path_info["host"],
user=path_info["user"],
port=path_info["port"],
)
# Use different md5 commands depending on os
stdout = self._exec(ssh, "uname").strip()
if stdout == "Darwin":
md5cmd = "md5"
index = -1
elif stdout == "Linux":
md5cmd = "md5sum"
index = 0
else:
msg = "'{}' is not supported as a remote".format(stdout)
raise DvcException(msg)
stdout = self._exec(ssh, "{} {}".format(md5cmd, path_info["path"]))
md5 = stdout.split()[index]
ssh.close()
assert len(md5) == 32
return md5
def copy(self, from_info, to_info, ssh=None):
if from_info["scheme"] != "ssh" or to_info["scheme"] != "ssh":
raise NotImplementedError
assert from_info["host"] == to_info["host"]
assert from_info["user"] == to_info["user"]
s = (
ssh
if ssh
else self.ssh(
host=from_info["host"],
user=from_info["user"],
port=from_info["port"],
)
)
dname = posixpath.dirname(to_info["path"])
self._exec(s, "mkdir -p {}".format(dname))
self._exec(s, "cp {} {}".format(from_info["path"], to_info["path"]))
if not ssh:
s.close()
def save_info(self, path_info):
if path_info["scheme"] != "ssh":
raise NotImplementedError
return {self.PARAM_CHECKSUM: self.md5(path_info)}
def save(self, path_info):
if path_info["scheme"] != "ssh":
raise NotImplementedError
md5 = self.md5(path_info)
dest = path_info.copy()
dest["path"] = self.checksum_to_path(md5)
self.copy(path_info, dest)
return {self.PARAM_CHECKSUM: md5}
def remove(self, path_info):
if path_info["scheme"] != "ssh":
raise NotImplementedError
logger.debug(
"Removing ssh://{}@{}/{}".format(
path_info["user"], path_info["host"], path_info["path"]
)
)
ssh = self.ssh(
host=path_info["host"],
user=path_info["user"],
port=path_info["port"],
)
ssh.open_sftp().remove(path_info["path"])
ssh.close()
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(from_infos, to_infos, names)
ssh = self.ssh(
host=from_infos[0]["host"],
user=from_infos[0]["user"],
port=from_infos[0]["port"],
)
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info["scheme"] != "ssh":
raise NotImplementedError
if to_info["scheme"] == "ssh":
assert from_info["host"] == to_info["host"]
assert from_info["port"] == to_info["port"]
assert from_info["user"] == to_info["user"]
self.copy(from_info, to_info, ssh=ssh)
continue
if to_info["scheme"] != "local":
raise NotImplementedError
msg = "Downloading '{}/{}' to '{}'".format(
from_info["host"], from_info["path"], to_info["path"]
)
logger.debug(msg)
if not name:
name = os.path.basename(to_info["path"])
self._makedirs(to_info["path"])
tmp_file = self.tmp_file(to_info["path"])
try:
ssh.open_sftp().get(
from_info["path"], tmp_file, callback=create_cb(name)
)
except Exception:
msg = "failed to download '{}/{}' to '{}'"
logger.error(
msg.format(
from_info["host"], from_info["path"], to_info["path"]
)
)
continue
os.rename(tmp_file, to_info["path"])
progress.finish_target(name)
ssh.close()
def upload(self, from_infos, to_infos, names=None):
names = self._verify_path_args(to_infos, from_infos, names)
ssh = self.ssh(
host=to_infos[0]["host"],
user=to_infos[0]["user"],
port=to_infos[0]["port"],
)
sftp = ssh.open_sftp()
for from_info, to_info, name in zip(from_infos, to_infos, names):
if to_info["scheme"] != "ssh":
raise NotImplementedError
if from_info["scheme"] != "local":
raise NotImplementedError
logger.debug(
"Uploading '{}' to '{}/{}'".format(
from_info["path"], to_info["host"], to_info["path"]
)
)
if not name:
name = os.path.basename(from_info["path"])
dname = posixpath.dirname(to_info["path"])
self._exec(ssh, "mkdir -p {}".format(dname))
try:
sftp.put(
from_info["path"],
to_info["path"],
callback=create_cb(name),
)
except Exception:
msg = "failed to upload '{}' to '{}/{}'"
logger.error(
msg.format(
from_info["path"], to_info["host"], to_info["path"]
)
)
continue
progress.finish_target(name)
sftp.close()
ssh.close()
def list_cache_paths(self):
ssh = self.ssh(host=self.host, user=self.user, port=self.port)
cmd = "find {} -type f -follow -print".format(self.prefix)
stdout = self._exec(ssh, cmd)
flist = stdout.split()
ssh.close()
return flist
+256
View File
@@ -0,0 +1,256 @@
from __future__ import unicode_literals
import os
import getpass
import logging
from dvc.path import Schemes
from dvc.path.ssh import SSHPathInfo
try:
import paramiko
except ImportError:
paramiko = None
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.remote.base import RemoteBase
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
PARAM_CHECKSUM = "md5"
DEFAULT_PORT = 22
TIMEOUT = 1800
def __init__(self, repo, config):
super(RemoteSSH, self).__init__(repo, config)
self.url = config.get(Config.SECTION_REMOTE_URL, "ssh://")
parsed = urlparse(self.url)
self.host = parsed.hostname
user_ssh_config = self._load_user_ssh_config(self.host)
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)
self.timeout = config.get(Config.SECTION_REMOTE_TIMEOUT, self.TIMEOUT)
self.password = config.get(Config.SECTION_REMOTE_PASSWORD, None)
self.ask_password = config.get(
Config.SECTION_REMOTE_ASK_PASSWORD, False
)
self.path_info = SSHPathInfo(
host=self.host, user=self.user, port=self.port
)
@staticmethod
def ssh_config_filename():
return os.path.expanduser(os.path.join("~", ".ssh", "config"))
@staticmethod
def _load_user_ssh_config(hostname):
user_config_file = RemoteSSH.ssh_config_filename()
user_ssh_config = dict()
if hostname and os.path.exists(user_config_file):
with open(user_config_file) as f:
ssh_config = paramiko.SSHConfig()
ssh_config.parse(f)
user_ssh_config = ssh_config.lookup(hostname)
return user_ssh_config
@staticmethod
def _try_get_ssh_config_port(user_ssh_config):
try:
return int(user_ssh_config.get("port"))
except (ValueError, TypeError):
return None
@staticmethod
def _try_get_ssh_config_keyfile(user_ssh_config):
identity_file = user_ssh_config.get("identityfile")
if identity_file and len(identity_file) > 0:
return identity_file[0]
return None
def ssh(self, path_info):
host, user, port = path_info.host, path_info.user, path_info.port
logger.debug(
"Establishing ssh connection with '{host}' "
"through port '{port}' as user '{user}'".format(
host=host, user=user, port=port
)
)
if self.ask_password and not self.password:
self.password = prompt.password(
"Enter a private key passphrase or a password for "
"host '{host}' port '{port}' user '{user}'".format(
host=host, port=port, user=user
)
)
return SSHConnection(
host,
username=user,
port=port,
key_filename=self.keyfile,
timeout=self.timeout,
password=self.password,
)
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info.scheme == self.scheme
with self.ssh(path_info) as ssh:
return ssh.exists(path_info.path)
def get_file_checksum(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
with self.ssh(path_info) as ssh:
return ssh.md5(path_info.path)
def isdir(self, path_info):
with self.ssh(path_info) as ssh:
return ssh.isdir(path_info.path)
def copy(self, from_info, to_info):
if from_info.scheme != self.scheme or to_info.scheme != self.scheme:
raise NotImplementedError
with self.ssh(from_info) as ssh:
ssh.cp(from_info.path, to_info.path)
def remove(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
with self.ssh(path_info) as ssh:
ssh.remove(path_info.path)
def move(self, from_info, to_info):
if from_info.scheme != self.scheme or to_info.scheme != self.scheme:
raise NotImplementedError
with self.ssh(from_info) as ssh:
ssh.move(from_info.path, to_info.path)
def download(
self,
from_infos,
to_infos,
no_progress_bar=False,
names=None,
resume=False,
):
names = self._verify_path_args(from_infos, to_infos, names)
ssh = self.ssh(from_infos[0])
for to_info, from_info, name in zip(to_infos, from_infos, names):
if from_info.scheme != self.scheme:
raise NotImplementedError
if to_info.scheme == self.scheme:
ssh.cp(from_info.path, to_info.path)
continue
if to_info.scheme != "local":
raise NotImplementedError
logger.debug(
"Downloading '{host}/{path}' to '{dest}'".format(
host=from_info.host, path=from_info.path, dest=to_info.path
)
)
try:
ssh.download(
from_info.path,
to_info.path,
progress_title=name,
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 '{host}/{path}' to '{dest}'".format(
host=from_info.host,
path=from_info.path,
dest=to_info.path,
)
)
pass
def list_cache_paths(self):
with self.ssh(self.path_info) as ssh:
return list(ssh.walk_files(self.prefix))
def walk(self, path_info):
with self.ssh(path_info) as ssh:
for entry in ssh.walk(path_info.path):
yield entry
def makedirs(self, path_info):
with self.ssh(path_info) as ssh:
ssh.makedirs(path_info.path)
+322
View File
@@ -0,0 +1,322 @@
import errno
import os
import posixpath
import logging
from stat import S_ISDIR
try:
import paramiko
except ImportError:
paramiko = None
from dvc.utils import tmp_fname
from dvc.utils.compat import makedirs
from dvc.progress import progress
from dvc.exceptions import DvcException
from dvc.remote.base import RemoteCmdError
logger = logging.getLogger(__name__)
def sizeof_fmt(num, suffix="B"):
""" Convert number of bytes to human-readable string """
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
if abs(num) < 1024.0:
return "%3.1f%s%s" % (num, unit, suffix)
num /= 1024.0
return "%.1f%s%s" % (num, "Y", suffix)
def percent_cb(name, complete, total):
""" Callback for updating target progress """
logger.debug(
"{}: {} transferred out of {}".format(
name, sizeof_fmt(complete), sizeof_fmt(total)
)
)
progress.update_target(name, complete, total)
def create_cb(name):
""" Create callback function for multipart object """
return lambda cur, tot: percent_cb(name, cur, tot)
class SSHConnection:
def __init__(self, *args, **kwargs):
self.timeout = kwargs.get("timeout", 1800)
self._ssh = paramiko.SSHClient()
self._ssh.load_system_host_keys()
self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self._ssh.connect(*args, **kwargs)
self._sftp = None
self._sftp_alive = False
def __enter__(self):
return self
def __exit__(self, exception_type, exception_value, traceback):
self.close()
def _sftp_connect(self):
if not self._sftp or not self._sftp_alive:
self._sftp = self._ssh.open_sftp()
self._sftp_alive = True
def close(self):
if self._sftp:
self._sftp.close()
self._sftp_alive = False
self._ssh.close()
def exists(self, path):
self._sftp_connect()
try:
return self._sftp.stat(path)
except IOError:
return False
pass
def isdir(self, path):
from stat import S_ISDIR
self._sftp_connect()
try:
return S_ISDIR(self._sftp.stat(path).st_mode)
except IOError:
return False
def isfile(self, path):
from stat import S_ISREG
self._sftp_connect()
try:
return S_ISREG(self._sftp.stat(path).st_mode)
except IOError:
return False
def islink(self, path):
from stat import S_ISLNK
self._sftp_connect()
try:
return S_ISLNK(self._sftp.stat(path).st_mode)
except IOError:
return False
def makedirs(self, path):
self._sftp_connect()
if self.isdir(path):
return
if self.isfile(path) or self.islink(path):
raise DvcException(
"a file with the same name '{}' already exists".format(path)
)
head, tail = posixpath.split(path)
if head:
self.makedirs(head)
if tail:
self._sftp.mkdir(path)
def walk(self, directory, topdown=True):
# NOTE: original os.walk() implementation [1] with default options was
# used as a template.
#
# [1] https://github.com/python/cpython/blob/master/Lib/os.py
self._sftp_connect()
try:
dir_entries = self._sftp.listdir_attr(directory)
except IOError as exc:
raise DvcException(
"couldn't get the '{}' remote directory files list".format(
directory
),
cause=exc,
)
dirs = []
nondirs = []
for entry in dir_entries:
name = entry.filename
if S_ISDIR(entry.st_mode):
dirs.append(name)
else:
nondirs.append(name)
if topdown:
yield directory, dirs, nondirs
for dname in dirs:
newpath = posixpath.join(directory, dname)
for entry in self.walk(newpath, topdown=topdown):
yield entry
if not topdown:
yield directory, dirs, nondirs
def walk_files(self, directory):
for root, dirs, files in self.walk(directory):
for fname in files:
yield posixpath.join(root, fname)
def _remove_file(self, path):
try:
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):
for fname in files:
path = posixpath.join(root, fname)
self._remove_file(path)
for dname in dirs:
path = posixpath.join(root, dname)
self._sftp.rmdir(dname)
try:
self._sftp.rmdir(path)
except IOError as exc:
if exc.errno != errno.ENOENT:
raise
def remove(self, path):
self._sftp_connect()
if self.isdir(path):
self._remove_dir(path)
else:
self._remove_file(path)
def download(self, src, dest, no_progress_bar=False, progress_title=None):
self._sftp_connect()
makedirs(os.path.dirname(dest), exist_ok=True)
tmp_file = tmp_fname(dest)
if no_progress_bar:
self._sftp.get(src, tmp_file)
else:
if not progress_title:
progress_title = os.path.basename(dest)
self._sftp.get(src, tmp_file, callback=create_cb(progress_title))
progress.finish_target(progress_title)
if os.path.exists(dest):
os.remove(dest)
os.rename(tmp_file, dest)
def move(self, src, dst):
self.makedirs(posixpath.dirname(dst))
self._sftp_connect()
self._sftp.rename(src, dst)
def upload(self, src, dest, no_progress_bar=False, progress_title=None):
self._sftp_connect()
self.makedirs(posixpath.dirname(dest))
tmp_file = tmp_fname(dest)
if no_progress_bar:
self._sftp.put(src, tmp_file)
else:
if not progress_title:
progress_title = posixpath.basename(dest)
self._sftp.put(src, tmp_file, callback=create_cb(progress_title))
progress.finish_target(progress_title)
self._sftp.rename(tmp_file, dest)
def execute(self, cmd):
stdin, stdout, stderr = self._ssh.exec_command(cmd)
channel = stdout.channel
stdin.close()
channel.shutdown_write()
stdout_chunks = []
stderr_chunks = []
while (
not channel.closed
or channel.recv_ready()
or channel.recv_stderr_ready()
):
import select
got_chunk = False
readq, _, _ = select.select([stdout.channel], [], [], self.timeout)
for c in readq:
if c.recv_ready():
stdout_chunks.append(stdout.channel.recv(len(c.in_buffer)))
got_chunk = True
if c.recv_stderr_ready():
stderr_len = len(c.in_stderr_buffer)
s = stderr.channel.recv_stderr(stderr_len)
stderr_chunks.append(s)
got_chunk = True
if (
not got_chunk
and stdout.channel.exit_status_ready()
and not stderr.channel.recv_stderr_ready()
and not stdout.channel.recv_ready()
):
stdout.channel.shutdown_read()
stdout.channel.close()
break
stdout.close()
stderr.close()
ret = stdout.channel.recv_exit_status()
if ret != 0:
err = b"".join(stderr_chunks).decode("utf-8")
raise RemoteCmdError("ssh", cmd, ret, err)
return b"".join(stdout_chunks).decode("utf-8")
def md5(self, path):
"""
Use different md5 commands depending on the OS:
- Darwin's `md5` returns BSD-style checksums by default
- Linux's `md5sum` needs the `--tag` flag for a similar output
Example:
MD5 (foo.txt) = f3d220a856b52aabbf294351e8a24300
"""
uname = self.execute("uname").strip()
command = {
"Darwin": "md5 {}".format(path),
"Linux": "md5sum --tag {}".format(path),
}.get(uname)
if not command:
raise DvcException(
"'{uname}' is not supported as a remote".format(uname=uname)
)
md5 = self.execute(command).split()[-1]
assert len(md5) == 32
return md5
def cp(self, src, dest):
self.makedirs(posixpath.dirname(dest))
self.execute("cp {} {}".format(src, dest))
+499
View File
@@ -0,0 +1,499 @@
from __future__ import unicode_literals
import os
import logging
import dvc.prompt as prompt
from dvc.exceptions import (
DvcException,
NotDvcRepoError,
OutputNotFoundError,
TargetNotDirectoryError,
)
from dvc.ignore import DvcIgnoreFileHandler
from dvc.path.local import LocalPathInfo
logger = logging.getLogger(__name__)
class Repo(object):
DVC_DIR = ".dvc"
from dvc.repo.destroy import destroy
from dvc.repo.install import install
from dvc.repo.add import add
from dvc.repo.remove import remove
from dvc.repo.lock import lock as lock_stage
from dvc.repo.move import move
from dvc.repo.run import run
from dvc.repo.imp import imp
from dvc.repo.reproduce import reproduce
from dvc.repo.checkout import checkout
from dvc.repo.push import push
from dvc.repo.fetch import fetch
from dvc.repo.pull import pull
from dvc.repo.status import status
from dvc.repo.gc import gc
from dvc.repo.commit import commit
from dvc.repo.diff import diff
from dvc.repo.brancher import brancher
def __init__(self, root_dir=None):
from dvc.config import Config
from dvc.state import State
from dvc.lock import Lock
from dvc.scm import SCM
from dvc.cache import Cache
from dvc.data_cloud import DataCloud
from dvc.updater import Updater
from dvc.repo.metrics import Metrics
from dvc.scm.tree import WorkingTree
from dvc.repo.tag import Tag
from dvc.repo.pkg import Pkg
root_dir = self.find_root(root_dir)
self.root_dir = os.path.abspath(os.path.realpath(root_dir))
self.dvc_dir = os.path.join(self.root_dir, self.DVC_DIR)
self.config = Config(self.dvc_dir)
self.tree = WorkingTree(self.root_dir)
self.scm = SCM(self.root_dir, repo=self)
self.lock = Lock(self.dvc_dir)
# NOTE: storing state and link_state in the repository itself to avoid
# any possible state corruption in 'shared cache dir' scenario.
self.state = State(self, self.config.config)
core = self.config.config[Config.SECTION_CORE]
level = core.get(Config.SECTION_CORE_LOGLEVEL)
if level:
logger.setLevel(level.upper())
self.cache = Cache(self)
self.cloud = DataCloud(self, config=self.config.config)
self.updater = Updater(self.dvc_dir)
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):
if root is None:
root = os.getcwd()
else:
root = os.path.abspath(os.path.realpath(root))
while True:
dvc_dir = os.path.join(root, Repo.DVC_DIR)
if os.path.isdir(dvc_dir):
return root
if os.path.ismount(root):
break
root = os.path.dirname(root)
raise NotDvcRepoError(root)
@staticmethod
def find_dvc_dir(root=None):
root_dir = Repo.find_root(root)
return os.path.join(root_dir, Repo.DVC_DIR)
@staticmethod
def init(root_dir=os.curdir, no_scm=False, force=False):
from dvc.repo.init import init
init(root_dir=root_dir, no_scm=no_scm, force=force)
return Repo(root_dir)
def unprotect(self, target):
path_info = LocalPathInfo(path=target)
return self.cache.local.unprotect(path_info)
def _ignore(self):
flist = [
self.state.state_file,
self.lock.lock_file,
self.config.config_local_file,
self.updater.updater_file,
self.updater.lock.lock_file,
] + self.state.temp_files
if self.cache.local.cache_dir.startswith(self.root_dir):
flist += [self.cache.local.cache_dir]
self.scm.ignore_list(flist)
def check_dag(self, stages):
"""Generate graph including the new stage to check for errors"""
self.graph(stages=stages)
@staticmethod
def _check_cyclic_graph(graph):
import networkx as nx
from dvc.exceptions import CyclicGraphError
cycles = list(nx.simple_cycles(graph))
if cycles:
raise CyclicGraphError(cycles[0])
def _get_pipeline(self, node):
pipelines = [i for i in self.pipelines() if i.has_node(node)]
assert len(pipelines) == 1
return pipelines[0]
def collect(self, target, with_deps=False, recursive=False):
import networkx as nx
from dvc.stage import Stage
if not target or recursive:
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)
G = self._get_pipeline(node)
ret = []
for n in nx.dfs_postorder_nodes(G, node):
ret.append(G.node[n]["stage"])
return ret
def _collect_dir_cache(
self, out, branch=None, remote=None, force=False, jobs=None
):
info = out.dumpd()
ret = [info]
r = out.remote
md5 = info[r.PARAM_CHECKSUM]
if self.cache.local.changed_cache_file(md5):
try:
self.cloud.pull(
ret, jobs=jobs, remote=remote, show_checksums=False
)
except DvcException as exc:
msg = "Failed to pull cache for '{}': {}"
logger.debug(msg.format(out, exc))
if self.cache.local.changed_cache_file(md5):
msg = (
"Missing cache for directory '{}'. "
"Cache for files inside will be lost. "
"Would you like to continue? Use '-f' to force. "
)
if not force and not prompt.confirm(msg):
raise DvcException(
"unable to fully collect used cache"
" without cache for directory '{}'".format(out)
)
else:
return ret
for i in 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,
all_branches=False,
active=True,
with_deps=False,
all_tags=False,
remote=None,
force=False,
jobs=None,
recursive=False,
):
cache = {}
cache["local"] = []
cache["s3"] = []
cache["gs"] = []
cache["hdfs"] = []
cache["ssh"] = []
cache["azure"] = []
for branch in self.brancher(
all_branches=all_branches, all_tags=all_tags
):
if target:
if recursive:
stages = self.stages(target)
else:
stages = self.collect(target, with_deps=with_deps)
elif active:
stages = self.active_stages()
else:
stages = self.stages()
for stage in stages:
if active and not target and stage.locked:
logger.warning(
"DVC file '{path}' is locked. Its dependencies are"
" not going to be pushed/pulled/fetched.".format(
path=stage.relpath
)
)
for out in stage.outs:
scheme = out.path_info.scheme
cache[scheme].extend(
self._collect_used_cache(
out,
branch=branch,
remote=remote,
force=force,
jobs=jobs,
)
)
return cache
def graph(self, stages=None, from_directory=None):
"""Generate a graph by using the given stages on the given directory
The nodes of the graph are the stage's path relative to the root.
Edges are created when the output of one stage is used as a
dependency in other stage.
The direction of the edges goes from the stage to its dependency:
For example, running the following:
$ dvc run -o A "echo A > A"
$ dvc run -d A -o B "echo B > B"
$ dvc run -d B -o C "echo C > C"
Will create the following graph:
ancestors <--
|
C.dvc -> B.dvc -> A.dvc
| |
| --> descendants
|
------- pipeline ------>
|
v
(weakly connected components)
Args:
stages (list): used to build a graph, if None given, use the ones
on the `from_directory`.
from_directory (str): directory where to look at for stages, if
None is given, use the current working directory
Raises:
OutputDuplicationError: two outputs with the same path
StagePathAsOutputError: stage inside an output directory
OverlappingOutputPathsError: output inside output directory
CyclicGraphError: resulting graph has cycles
"""
import networkx as nx
from dvc.exceptions import (
OutputDuplicationError,
StagePathAsOutputError,
OverlappingOutputPathsError,
)
G = nx.DiGraph()
G_active = nx.DiGraph()
stages = stages or self.stages(from_directory, check_dag=False)
stages = [stage for stage in stages if stage]
outs = []
for stage in stages:
for out in stage.outs:
existing = []
for o in outs:
if o.path == out.path:
existing.append(o.stage)
in_o_dir = out.path.startswith(o.path + o.sep)
in_out_dir = o.path.startswith(out.path + out.sep)
if in_o_dir or in_out_dir:
raise OverlappingOutputPathsError(o, out)
if existing:
stages = [stage.relpath, existing[0].relpath]
raise OutputDuplicationError(out.path, stages)
outs.append(out)
for stage in stages:
path_dir = os.path.dirname(stage.path) + os.sep
for out in outs:
if path_dir.startswith(out.path + os.sep):
raise StagePathAsOutputError(stage.wdir, stage.relpath)
for stage in stages:
node = os.path.relpath(stage.path, self.root_dir)
G.add_node(node, stage=stage)
G_active.add_node(node, stage=stage)
for dep in stage.deps:
for out in outs:
if (
out.path != dep.path
and not dep.path.startswith(out.path + out.sep)
and not out.path.startswith(dep.path + dep.sep)
):
continue
dep_stage = out.stage
dep_node = os.path.relpath(dep_stage.path, self.root_dir)
G.add_node(dep_node, stage=dep_stage)
G.add_edge(node, dep_node)
if not stage.locked:
G_active.add_node(dep_node, stage=dep_stage)
G_active.add_edge(node, dep_node)
self._check_cyclic_graph(G)
return G, G_active
def pipelines(self, from_directory=None):
import networkx as nx
G, G_active = self.graph(from_directory=from_directory)
return [
G.subgraph(c).copy() for c in nx.weakly_connected_components(G)
]
def stages(self, from_directory=None, check_dag=True):
"""
Walks down the root directory looking for Dvcfiles,
skipping the directories that are related with
any SCM (e.g. `.git`), DVC itself (`.dvc`), or directories
tracked by DVC (e.g. `dvc add data` would skip `data/`)
NOTE: For large repos, this could be an expensive
operation. Consider using some memoization.
"""
from dvc.stage import Stage
if not from_directory:
from_directory = self.root_dir
elif not os.path.isdir(from_directory):
raise TargetNotDirectoryError(from_directory)
stages = []
outs = []
ignore_file_handler = DvcIgnoreFileHandler(self.tree)
for root, dirs, files in self.tree.walk(
from_directory, ignore_file_handler=ignore_file_handler
):
for fname in files:
path = os.path.join(root, fname)
if not Stage.is_valid_filename(path):
continue
stage = Stage.load(self, path)
for out in stage.outs:
outs.append(out.path + out.sep)
stages.append(stage)
def filter_dirs(dname, root=root):
path = os.path.join(root, dname)
if path in (self.dvc_dir, self.scm.dir):
return False
for out in outs:
if path == os.path.normpath(out) or path.startswith(out):
return False
return True
dirs[:] = list(filter(filter_dirs, dirs))
if check_dag:
self.check_dag(stages)
return stages
def active_stages(self, from_directory=None):
import networkx as nx
stages = []
for G in self.pipelines(from_directory):
stages.extend(list(nx.get_node_attributes(G, "stage").values()))
return stages
def find_outs_by_path(self, path, outs=None, recursive=False):
if not outs:
# there is no `from_directory=path` argument because some data
# files might be generated to an upper level, and so it is
# needed to look at all the files (project root_dir)
stages = self.stages()
outs = [out for stage in stages for out in stage.outs]
abs_path = os.path.abspath(path)
is_dir = self.tree.isdir(abs_path)
def func(out):
if out.path == abs_path:
return True
if is_dir and recursive and out.path.startswith(abs_path + os.sep):
return True
return False
matched = list(filter(func, outs))
if not matched:
raise OutputNotFoundError(path)
return matched
def is_dvc_internal(self, path):
path_parts = os.path.normpath(path).split(os.path.sep)
return self.DVC_DIR in path_parts
+73
View File
@@ -0,0 +1,73 @@
import os
import logging
import colorama
from dvc.repo.scm_context import scm_context
from dvc.stage import Stage
from dvc.utils import walk_files, LARGE_DIR_SIZE
from dvc.exceptions import RecursiveAddingWhileUsingFilename
logger = logging.getLogger(__name__)
@scm_context
def add(repo, target, recursive=False, no_commit=False, fname=None):
if recursive and fname:
raise RecursiveAddingWhileUsingFilename()
targets = _find_all_targets(repo, target, recursive)
if os.path.isdir(target) and len(targets) > LARGE_DIR_SIZE:
logger.warning(
"You are adding a large directory '{target}' recursively,"
" consider tracking it as a whole instead.\n"
"{purple}HINT:{nc} Remove the generated stage files and then"
" run {cyan}dvc add {target}{nc}".format(
purple=colorama.Fore.MAGENTA,
cyan=colorama.Fore.CYAN,
nc=colorama.Style.RESET_ALL,
target=target,
)
)
stages = _create_stages(repo, targets, fname, no_commit)
repo.check_dag(repo.stages() + stages)
for stage in stages:
stage.dump()
return stages
def _find_all_targets(repo, target, recursive):
if os.path.isdir(target) and recursive:
return [
fname
for fname in walk_files(target)
if not repo.is_dvc_internal(fname)
if not Stage.is_stage_file(fname)
if not repo.scm.belongs_to_scm(fname)
if not repo.scm.is_tracked(fname)
]
return [target]
def _create_stages(repo, targets, fname, no_commit):
stages = []
with repo.state:
for out in targets:
stage = Stage.create(repo=repo, outs=[out], add=True, fname=fname)
if not stage:
continue
stage.save()
if not no_commit:
stage.commit()
stages.append(stage)
return stages
+56
View File
@@ -0,0 +1,56 @@
def brancher( # noqa: E302
self, branches=None, all_branches=False, tags=None, all_tags=False
):
"""Generator that iterates over specified revisions.
Args:
branches (list): a list of branches to iterate over.
all_branches (bool): iterate over all available branches.
tags (list): a list of tags to iterate over.
all_tags (bool): iterate over all available tags.
Yields:
str: the display name for the currently selected tree, it could be:
- a git revision identifier
- empty string it there is no branches to iterate over
- "Working Tree" if there are uncommited changes in the SCM repo
"""
if not any([branches, all_branches, tags, all_tags]):
yield ""
return
saved_tree = self.tree
revs = []
scm = self.scm
if self.scm.is_dirty():
from dvc.scm.tree import WorkingTree
self.tree = WorkingTree(self.root_dir)
yield "Working Tree"
if all_branches:
branches = scm.list_branches()
if all_tags:
tags = scm.list_tags()
if branches is None:
revs.extend([scm.active_branch()])
else:
revs.extend(branches)
if tags is not None:
revs.extend(tags)
# NOTE: it might be a good idea to wrap this loop in try/finally block
# to don't leave the tree on some unexpected branch after the
# `brancher()`, but this could cause problems on exception handling
# code which might expect the tree on which exception was raised to
# stay in place. This behavior is a subject to change.
for rev in revs:
self.tree = scm.get_tree(rev)
yield rev
self.tree = saved_tree
+56
View File
@@ -0,0 +1,56 @@
from __future__ import unicode_literals
import logging
from dvc.exceptions import CheckoutErrorSuggestGit
from dvc.progress import ProgressCallback
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)
self.state.remove_unused_links(used)
def get_all_files_numbers(stages):
return sum(stage.get_all_files_number() for stage in stages)
def get_progress_callback(stages):
total_files_num = get_all_files_numbers(stages)
if total_files_num == 0:
return None
return ProgressCallback(total_files_num)
def checkout(self, target=None, with_deps=False, force=False, recursive=False):
from dvc.stage import StageFileDoesNotExistError, StageFileBadNameError
all_stages = self.stages()
try:
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
except (StageFileDoesNotExistError, StageFileBadNameError) as exc:
if not target:
raise
raise CheckoutErrorSuggestGit(target, exc)
with self.state:
_cleanup_unused_links(self, all_stages)
progress_callback = get_progress_callback(stages)
for stage in stages:
if stage.locked:
logger.warning(
"DVC file '{path}' is locked. Its dependencies are"
" not going to be checked out.".format(path=stage.relpath)
)
stage.checkout(force=force, progress_callback=progress_callback)
if progress_callback:
progress_callback.finish("Checkout finished!")
+7
View File
@@ -0,0 +1,7 @@
def commit(self, target, with_deps=False, recursive=False, force=False):
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
with self.state:
for stage in stages:
stage.check_can_commit(force=force)
stage.commit()
stage.dump()
+8
View File
@@ -0,0 +1,8 @@
import shutil
def destroy(self):
for stage in self.stages():
stage.remove()
shutil.rmtree(self.dvc_dir)
+252
View File
@@ -0,0 +1,252 @@
from __future__ import unicode_literals
import os
from errno import ENOENT
import dvc.logger as logger
from dvc.scm.base import FileNotInCommitError
from dvc.scm.git import (
DIFF_A_TREE,
DIFF_B_TREE,
DIFF_A_REF,
DIFF_B_REF,
DIFF_EQUAL,
)
DIFF_TARGET = "target"
DIFF_IS_DIR = "is_dir"
DIFF_OLD_FILE = "old_file"
DIFF_OLD_CHECKSUM = "old_checksum"
DIFF_NEW_FILE = "new_file"
DIFF_NEW_CHECKSUM = "new_checksum"
DIFF_SIZE = "size_diff"
DIFF_DEL = "del"
DIFF_IDENT = "ident"
DIFF_CHANGE = "changes"
DIFF_NEW = "new"
DIFF_MOVE = "moves"
DIFF_LIST = "diffs"
DIFF_SIZE_UNKNOWN = "?"
DIFF_A_OUTPUT = "a_output"
DIFF_B_OUTPUT = "b_output"
DIFF_DELETED = "deleted_file"
DIFF_IS_NEW = "created_file"
def _file_not_exists(error, result):
if error.errno == ENOENT:
result.update({DIFF_SIZE: DIFF_SIZE_UNKNOWN})
else:
raise error
def _extract_dir(self, dir_not_exists, output):
"""Extract the content of dvc tree file
Args:
self(object) - Repo class instance
dir_not_exists(bool) - flag for directory existence
output(object) - OutputLOCAL class instance
Returns:
dict - dictionary with keys - paths to file in .dvc/cache
values -checksums for that files
"""
if not dir_not_exists:
lst = output.dir_cache
return {i["relpath"]: i["md5"] for i in lst}
return {}
def _get_dir_info(dir_not_exists, a_output):
if not dir_not_exists:
return str(a_output), a_output.checksum
return "", ""
def _ident_files(a_entries, b_entries):
keys = [
key for key in a_entries.keys() if b_entries.get(key) == a_entries[key]
]
return len(keys)
def _modified_files(self, a_entries, b_entries):
keys = [key for key in a_entries.keys() if key in b_entries]
diff_size = 0
modified_count = 0
for key in keys:
if a_entries[key] != b_entries[key]:
modified_count
diff_size += os.path.getsize(
self.cache.local.get(b_entries[key])
) - os.path.getsize(self.cache.local.get(a_entries[key]))
return modified_count, diff_size
def _deleted_files(self, a_entries, b_entries):
diff_size = 0
deleted_count = 0
for key, value in a_entries.items():
if key not in b_entries:
deleted_count += 1
diff_size -= os.path.getsize(self.cache.local.get(a_entries[key]))
return deleted_count, diff_size
def _new_files(self, a_entries, b_entries):
diff_size = 0
new_count = 0
for key, value in b_entries.items():
if key not in a_entries:
new_count += 1
diff_size += os.path.getsize(self.cache.local.get(b_entries[key]))
return new_count, diff_size
def _get_tree_changes(self, a_entries, b_entries):
result = {
DIFF_DEL: 0,
DIFF_IDENT: 0,
DIFF_CHANGE: 0,
DIFF_NEW: 0,
DIFF_MOVE: 0,
}
result[DIFF_IDENT] = _ident_files(a_entries, b_entries)
result[DIFF_CHANGE], diff_size = _modified_files(
self, a_entries, b_entries
)
result[DIFF_SIZE] = diff_size
result[DIFF_DEL], diff_size = _deleted_files(self, a_entries, b_entries)
result[DIFF_SIZE] += diff_size
result[DIFF_NEW], diff_size = _new_files(self, a_entries, b_entries)
result[DIFF_SIZE] += diff_size
return result
def _check_local_cache(a_out, is_checked):
if a_out is not None and a_out.scheme != "local":
is_checked.append(str(a_out))
return True
return False
def _is_dir(path, a_outs, b_outs):
if a_outs.get(path):
return a_outs[path].is_dir_checksum
else:
return b_outs[path].is_dir_checksum
def _get_diff_outs(self, diff_dct):
self.tree = diff_dct[DIFF_A_TREE]
a_outs = {str(out): out for st in self.stages() for out in st.outs}
self.tree = diff_dct[DIFF_B_TREE]
b_outs = {str(out): out for st in self.stages() for out in st.outs}
outs_paths = set(a_outs.keys())
outs_paths.update(b_outs.keys())
results = {}
non_local_cache = []
for path in outs_paths:
check1 = _check_local_cache(a_outs.get(path), non_local_cache)
check2 = _check_local_cache(b_outs.get(path), non_local_cache)
# skip files/directories with non-local cache for now
if check1 or check2:
continue
results[path] = {}
results[path][DIFF_A_OUTPUT] = a_outs.get(path)
results[path][DIFF_B_OUTPUT] = b_outs.get(path)
results[path][DIFF_IS_NEW] = path not in a_outs
results[path][DIFF_DELETED] = path not in b_outs
results[path][DIFF_IS_DIR] = _is_dir(path, a_outs, b_outs)
if non_local_cache:
logger.warning(
"Diff is not supported for non-local outputs. Ignoring: {}".format(
non_local_cache
)
)
return results
def _diff_dir(self, target, diff_dct):
result = {DIFF_TARGET: target}
result[DIFF_IS_DIR] = True
a_entries, b_entries = {}, {}
try:
a_entries = _extract_dir(
self, diff_dct[DIFF_IS_NEW], diff_dct[DIFF_A_OUTPUT]
)
b_entries = _extract_dir(
self, diff_dct[DIFF_DELETED], diff_dct[DIFF_B_OUTPUT]
)
result[DIFF_OLD_FILE], result[DIFF_OLD_CHECKSUM] = _get_dir_info(
diff_dct[DIFF_IS_NEW], diff_dct[DIFF_A_OUTPUT]
)
result[DIFF_NEW_FILE], result[DIFF_NEW_CHECKSUM] = _get_dir_info(
diff_dct[DIFF_DELETED], diff_dct[DIFF_B_OUTPUT]
)
result.update(_get_tree_changes(self, a_entries, b_entries))
except IOError as e:
_file_not_exists(e, result)
return result
def _diff_file(self, target, diff_dct):
result = {DIFF_TARGET: target}
size = 0
try:
if not diff_dct[DIFF_IS_NEW]:
result[DIFF_OLD_FILE] = target
result[DIFF_OLD_CHECKSUM] = diff_dct[DIFF_A_OUTPUT].checksum
size -= os.path.getsize(
self.cache.local.get(diff_dct[DIFF_A_OUTPUT].checksum)
)
if not diff_dct[DIFF_DELETED]:
result[DIFF_NEW_FILE] = target
result[DIFF_NEW_CHECKSUM] = diff_dct[DIFF_B_OUTPUT].checksum
size += os.path.getsize(
self.cache.local.get(diff_dct[DIFF_B_OUTPUT].checksum)
)
result[DIFF_SIZE] = size
except IOError as e:
_file_not_exists(e, result)
return result
def _diff_royal(self, target, diff_dct):
if diff_dct[DIFF_IS_DIR]:
return _diff_dir(self, target, diff_dct)
return _diff_file(self, target, diff_dct)
def diff(self, a_ref, target=None, b_ref=None):
"""Gerenates diff message string output
Args:
target(str) - file/directory to check diff of
a_ref(str) - first tag
(optional) b_ref(str) - second git tag
Returns:
string: string of output message with diff info
"""
result = {}
diff_dct = self.scm.get_diff_trees(a_ref, b_ref=b_ref)
result[DIFF_A_REF] = diff_dct[DIFF_A_REF]
result[DIFF_B_REF] = diff_dct[DIFF_B_REF]
if diff_dct[DIFF_EQUAL]:
result[DIFF_EQUAL] = True
return result
result[DIFF_LIST] = []
diff_outs = _get_diff_outs(self, diff_dct)
if target is None:
result[DIFF_LIST] = [
_diff_royal(self, path, diff_outs[path]) for path in diff_outs
]
elif target in diff_outs:
result[DIFF_LIST] = [_diff_royal(self, target, diff_outs[target])]
else:
msg = "Have not found file/directory '{}' in the commits"
raise FileNotInCommitError(msg.format(target))
return result
+28
View File
@@ -0,0 +1,28 @@
from __future__ import unicode_literals
def fetch(
self,
target=None,
jobs=1,
remote=None,
all_branches=False,
show_checksums=False,
with_deps=False,
all_tags=False,
recursive=False,
):
with self.state:
used = self.used_cache(
target,
all_branches=all_branches,
all_tags=all_tags,
with_deps=with_deps,
force=True,
remote=remote,
jobs=jobs,
recursive=recursive,
)["local"]
return self.cloud.pull(
used, jobs, remote=remote, show_checksums=show_checksums
)
+113
View File
@@ -0,0 +1,113 @@
from __future__ import unicode_literals
import collections
import logging
logger = logging.getLogger(__name__)
def _merge_cache_lists(clists):
merged_cache = collections.defaultdict(list)
for cache_list in clists:
for scheme, cache in cache_list.items():
for item in cache:
if item not in merged_cache[scheme]:
merged_cache[scheme].append(item)
return merged_cache
def _load_all_used_cache(
repos,
target=None,
all_branches=False,
active=True,
with_deps=False,
all_tags=False,
remote=None,
force=False,
jobs=None,
):
clists = []
for repo in repos:
with repo.state:
repo_clist = repo.used_cache(
target=None,
all_branches=all_branches,
active=False,
with_deps=with_deps,
all_tags=all_tags,
remote=remote,
force=force,
jobs=jobs,
)
clists.append(repo_clist)
return clists
def _do_gc(typ, func, clist):
removed = func(clist)
if not removed:
logger.info("No unused {} cache to remove.".format(typ))
def gc(
self,
all_branches=False,
cloud=False,
remote=None,
with_deps=False,
all_tags=False,
force=False,
jobs=None,
repos=None,
):
from dvc.repo import Repo
all_repos = [self]
if repos:
all_repos.extend(Repo(path) for path in repos)
all_clists = _load_all_used_cache(
all_repos,
target=None,
all_branches=all_branches,
active=False,
with_deps=with_deps,
all_tags=all_tags,
remote=remote,
force=force,
jobs=jobs,
)
if len(all_clists) > 1:
clist = _merge_cache_lists(all_clists)
else:
clist = all_clists[0]
with self.state:
_do_gc("local", self.cache.local.gc, clist)
if self.cache.s3:
_do_gc("s3", self.cache.s3.gc, clist)
if self.cache.gs:
_do_gc("gs", self.cache.gs.gc, clist)
if self.cache.ssh:
_do_gc("ssh", self.cache.ssh.gc, clist)
if self.cache.hdfs:
_do_gc("hdfs", self.cache.hdfs.gc, clist)
if self.cache.azure:
_do_gc("azure", self.cache.azure.gc, clist)
if cloud:
_do_gc("remote", self.cloud._get_cloud(remote, "gc -c").gc, clist)
+23
View File
@@ -0,0 +1,23 @@
from dvc.repo.scm_context import scm_context
@scm_context
def imp(self, url, out, resume=False, fname=None):
from dvc.stage import Stage
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
+96
View File
@@ -0,0 +1,96 @@
import os
import shutil
import colorama
import logging
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
logger = logging.getLogger(__name__)
def _welcome_message():
logger.info(
boxify(
"DVC has enabled anonymous aggregate usage analytics.\n"
"Read the analytics documentation (and how to opt-out) here:\n"
"{blue}https://dvc.org/doc/user-guide/analytics{nc}".format(
blue=colorama.Fore.BLUE, nc=colorama.Fore.RESET
),
border_color="red",
)
)
msg = (
"{yellow}What's next?{nc}\n"
"{yellow}------------{nc}\n"
"- Check out the documentation: {blue}https://dvc.org/doc{nc}\n"
"- Get help and share ideas: {blue}https://dvc.org/chat{nc}\n"
"- Star us on GitHub: {blue}https://github.com/iterative/dvc{nc}"
).format(
yellow=colorama.Fore.YELLOW,
blue=colorama.Fore.BLUE,
nc=colorama.Fore.RESET,
)
logger.info(msg)
def init(root_dir=os.curdir, no_scm=False, force=False):
"""
Creates an empty repo on the given directory -- basically a
`.dvc` directory with subdirectories for configuration and cache.
It should be tracked by a SCM or use the `--no-scm` flag.
If the given directory is not empty, you must use the `--force`
flag to override it.
Args:
root_dir: Path to repo's root directory.
Returns:
Repo instance.
Raises:
KeyError: Raises an exception.
"""
root_dir = os.path.abspath(root_dir)
dvc_dir = os.path.join(root_dir, Repo.DVC_DIR)
scm = SCM(root_dir)
if isinstance(scm, NoSCM) and not no_scm:
raise InitError(
"{repo} is not tracked by any supported scm tool (e.g. git). "
"Use '--no-scm' if you don't want to use any scm.".format(
repo=root_dir
)
)
if os.path.isdir(dvc_dir):
if not force:
raise InitError(
"'{repo}' exists. Use '-f' to force.".format(
repo=os.path.relpath(dvc_dir)
)
)
shutil.rmtree(dvc_dir)
os.mkdir(dvc_dir)
config = Config.init(dvc_dir)
proj = Repo(root_dir)
scm.add([config.config_file])
if scm.ignore_file:
scm.add([os.path.join(dvc_dir, scm.ignore_file)])
logger.info("\nYou can now commit the changes to git.\n")
_welcome_message()
return proj
+2
View File
@@ -0,0 +1,2 @@
def install(self):
self.scm.install()
+8
View File
@@ -0,0 +1,8 @@
def lock(self, target, unlock=False):
from dvc.stage import Stage
stage = Stage.load(self, target)
stage.locked = False if unlock else True
stage.dump()
return stage
+23
View File
@@ -0,0 +1,23 @@
class Metrics(object):
def __init__(self, repo):
self.repo = repo
def add(self, *args, **kwargs):
from dvc.repo.metrics.add import add
return add(self.repo, *args, **kwargs)
def modify(self, *args, **kwargs):
from dvc.repo.metrics.modify import modify
return modify(self.repo, *args, **kwargs)
def show(self, *args, **kwargs):
from dvc.repo.metrics.show import show
return show(self.repo, *args, **kwargs)
def remove(self, *args, **kwargs):
from dvc.repo.metrics.remove import remove
return remove(self.repo, *args, **kwargs)
+7
View File
@@ -0,0 +1,7 @@
from dvc.repo.metrics.modify import modify
def add(repo, path, typ=None, xpath=None):
if not typ:
typ = "raw"
modify(repo, path, typ, xpath)

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