Compare commits

...
456 Commits
Author SHA1 Message Date
Ruslan Kuprieiev e623c4faf7 dvc: bump to 0.66.2 2019-10-30 17:20:05 +02:00
Ivan ShchekleinandRuslan Kuprieiev 2ec0aa0c37 failing while pulling imported directories (#2692)
* pull: fix-2684 - failing while pulling imported directories

* pull: fix-2684 - fix code indentation

* minor imported directories pull test update

Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2019-10-30 17:18:07 +02:00
Alexander Schepanovski 77fcd49695 dvc: fix tqdm dep (#2694)
tqdm==4.35.0 was throwing KeyError: 'percentage'.
2019-10-30 16:43:18 +02:00
Ruslan Kuprieiev 877e38551e lock: add a workaround for a flufl.lock bug (#2688)
This bug doesn't affect us during regular dvc CLI commands, but is not
visualy pleasing during API usage.

Fixes #2573
2019-10-29 20:54:56 +02:00
Jorge Orpinel a48481b01f dvc: change ` for ' in dynamic parts of output messages (#2644)
* dvc: change ` for ' in dinamic parts of DVC outputs

per https://github.com/iterative/dvc/pull/2644#issuecomment-545151398

* dvc: use ' for dynamic output and ` for DVC names

per https://github.com/iterative/dvc/pull/2644#discussion_r338417698
2019-10-28 22:00:53 +02:00
Sagun Karanjit 5d6918a04c Remove --all-commits options from usage output (#2682)
* Comment out --all-commits optional arg

* Suppress --all-commits optional arg
2019-10-28 17:27:34 +02:00
Aman Sharma 5f644745c7 Write test for get_inode (#2673) 2019-10-28 16:43:47 +02:00
Ruslan Kuprieiev b9c7d88fcc dvc: bump to 0.66.1 2019-10-26 23:36:13 +03:00
Ruslan Kuprieiev 64f78c164d Revert "Added path.lstrip for / in imports (#2651)" (#2675)
This reverts commit bfc0a3596f.
2019-10-26 23:35:22 +03:00
Ruslan Kuprieiev 1baf020de7 s3: don't use walk_files for list_cache_paths
No need to convert back and forth from string to PathInfo
2019-10-26 00:29:23 +03:00
Ruslan Kuprieiev 1ab9319682 gs: fix typo 2019-10-25 23:24:43 +03:00
Ruslan Kuprieiev 32ba773aee s3: don't forget to convert to regular path strings 2019-10-25 23:07:17 +03:00
Ruslan Kuprieiev fc485581f9 dvc: bump to 0.66.0 2019-10-25 22:46:50 +03:00
Ruslan Kuprieiev f3bacb7c28 gs: retry upload with smaller chunk size (#2661)
Default chunk size for gs is 100M, which might be too much for
particular network. So if we are getting ConnectionError, we
should try lowering the chunk size until we reach the minimum
allowed chunk size of 256K. Also note that `chunk_size` must be a
multiple of 256K per the API specification.

Fixes #2572
2019-10-25 22:45:54 +03:00
Sanket Saurav 88f45c2348 Exclude compat files from DeepSource analysis (#2670)
Hi team,

I just [noticed](https://github.com/iterative/dvc/pull/2548#issuecomment-536255251) that there are could be quite a number of false-positives in `dvc/utils/compat.py` because of, well, the things we have to do for compatibility. I would recommend adding that file to `exclude_patterns` to prevent the noise. I've proposed that in this PR.

We've seen some other projects report this as well, and we're working on proactively suggesting this via the product UI to make it easier.

Hope this helps!

Best,
Sanket
Founder, DeepSource
2019-10-25 15:28:09 +03:00
Mr. Outis 27b63e2674 s3: support for directories (#2619)
* s3: list_objects -> list_objects_api

* s3: use path_info for _list_paths

* s3: separate _list_paths from _list_objects

* s3: support max items for listing objects/paths

* s3: take into account directories on `exists`

* s3: overwrite `isdir` method

* s3: custom collect directory entry for s3

* s3: strip checksum when collecting directories

* remote: change walk implementation to walk_files

* s3: add comment about isdir logic

* s3: use `dir_path` correctly on `isdir`

* tests: add a few unit tests for S3

* s3: use path_info join instead of posixpath

* tests: use parametrize for s3 tests

* remote: use posixpath accordingly

* tests: remove undeeded AWS variables from S3 test

* tests: rearrange parametrize values for readability

* tests: add encoding, use a single s3 instance, for loop instead of parameterize

* py2: fix encoding
2019-10-25 03:41:04 +03:00
Ruslan Kuprieiev 35499faa7d readme: remove brew cask and update brew install cmd 2019-10-25 01:41:46 +03:00
Casper da Costa-Luis 9ce3b0c50c add: output improvements (#2546)
* some doc improvements

* add: intermediate progress

* update after rebase

* linting

* fix checksum progress cleanup by ensuring gc

* progress-aware logger.info

* re-fix checksum pbar closure

* multi-file add

* make stage.dump() less verbose.

TODO: check all instances of stage.dump and possibly make verbose

* decrease verbosity of checksum calculations

* leave dvc.add stats

* update/fix logger tests

* minor doc reword

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

* tidy API target_;list => targets

- as per https://github.com/iterative/dvc/pull/2546#discussion_r337042535

* tidy RecursiveAddingWhileUsingFilename message duplication

- fixes https://github.com/iterative/dvc/pull/2546#discussion_r337044670

* minor logging uncomment

- fixes https://github.com/iterative/dvc/pull/2546#discussion_r337051097

* minor doc improvements

- https://github.com/iterative/dvc/pull/2546#discussion_r337116255
- https://github.com/iterative/dvc/pull/2546#discussion_r337116809

* tidy exception

- fixes https://github.com/iterative/dvc/pull/2546#discussion_r337434142

* minor doc update

- fixes https://github.com/iterative/dvc/pull/2546/files#r337294491
- related to https://github.com/iterative/dvc.org/issues/719

* Adding => add

- fixes https://github.com/iterative/dvc/pull/2546#discussion_r337037137

* various review fixes

- minor comment as per
  https://github.com/iterative/dvc/pull/2546#discussion_r337927837
- revert warning style fix
  https://github.com/iterative/dvc/pull/2546#discussion_r338046035
- Abstract error consistency
  https://github.com/iterative/dvc/pull/2546#discussion_r337931070
- minor comment as per
  https://github.com/iterative/dvc/pull/2546#discussion_r338254958

* remove diabling progress option

- fixes https://github.com/iterative/dvc/pull/2546#discussion_r338314050
2019-10-25 01:09:19 +03:00
Ruslan Kuprieiev 3667830c2a dvc: bump to 0.65.0 2019-10-24 23:41:43 +03:00
Ruslan Kuprieiev 8d9e4ca934 graph: use find_cycle instead of simple_cycle (#2668)
We are incorrectly using `simple_cycle` here, `find_cycle` is much more
suitable for this application and much faster too. For example:

```
$ cat test_networkx.py
import networkx as nx
from dvc.repo.graph import check_acyclic

G = nx.scale_free_graph(1200)

list(nx.find_cycle(G, orientation='ignore'))

list(nx.simple_cycles(G))

$ python -mcProfile -scumtime test_networkx.py &> log.log
```

shows ~100s for `simple_cycles` and ~0s for `find_cycle`.

Fixes #2666
2019-10-24 23:41:11 +03:00
Casper da Costa-Luis ed1f643a6e ssh credential prompt before progress (#2660)
* ssh credential prompt before progress

* clean up ssh cred handling

- fixes https://github.com/iterative/dvc/pull/2660#discussion_r338536169
2019-10-24 19:14:07 +03:00
Ruslan Kuprieiev 24ed639901 setup: add upper limit for tqdm version
https://github.com/iterative/dvc/issues/2659#issuecomment-545940739
2019-10-24 17:29:53 +03:00
Ruslan Kuprieiev b8d1ec204c dvc: bump to 0.64.1 2019-10-24 17:15:30 +03:00
Casper da Costa-Luis 6f068857cb Desc fix (#2663)
* fix zero-width desc again

- fixes #2659

* correctly use tqdm API
2019-10-24 17:05:52 +03:00
Ruslan Kuprieiev 8d067c8805 setup: update project url 2019-10-24 15:19:04 +03:00
Dashamir Hoxha 7879c7cb31 Update URLs by replacing /contributing with /contributing/code (#2657)
* Update URLs by replacing /contributing with /contributing/code

* Rename contributing/code to contributing/core
2019-10-24 02:12:39 +03:00
Ruslan Kuprieiev bd5a8a5cef dvc: bump to 0.64.0 2019-10-23 22:48:33 +03:00
Ruslan Kuprieiev 480a612da4 output: base: don't forget to return empty cache (#2656)
* output: base: don't forget to return empty cache

CC @Suor

Fixes https://gist.github.com/leimbernon/21924e02a998e3f1604990f8a7824c37

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

* gc: add test for missing .dir cache file
2019-10-23 22:47:50 +03:00
Alexander Schepanovski 86811aac20 imp: dedup and parallelize fetching imported deps (#2654)
* imp: dedup and parallelize external fetches

This prevents same fetches for external artifacts, groups them by source
and fetches in parallel.

* imp: test that fetch works on non workspace deps
2019-10-23 18:02:50 +03:00
Alexander Schepanovski 30341438e0 gc: added --all-commits (#2643)
* dvc: fix branch suffixes in status/push/pull

* gc: add --all-commits flag

* gc: fix gc warning

Say that we always leave things used in the working tree
2019-10-23 18:00:31 +03:00
Srividya Krishnakumar bfc0a3596f Added path.lstrip for / in imports (#2651) 2019-10-23 03:54:10 +03:00
Ruslan Kuprieiev a0d364e78a version: fix warning 2019-10-22 23:21:22 +03:00
Ruslan Kuprieiev 878998433f dvc: bump to 0.63.4 2019-10-22 01:26:02 +03:00
Ruslan Kuprieiev 763bfb4734 http: reuse requests.Session (#2646)
This way we are able to properly utilize automatic connection pools and
not create new fds for each request, which overflows ulimit for max fds
very quickly on mac and windows. Kudos @pared for investigating 🎉

Fixes #2600

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-10-22 00:54:29 +03:00
Ruslan Kuprieiev 2274261808 progress: enable progress bar if it is a TTY
Fixes #2645
2019-10-21 23:35:47 +03:00
Ruslan Kuprieiev 3043ed2cb9 lock: skip using slow getaddrinfo (#2642)
This is basically Lock.__init__ copy-paste, except that
instead of using `socket.getfqdn()` we use `socket.gethostname()`
to speed this up. We've seen [1] `getfqdn()` take ~5sec to return
anything, which is way too slow. `gethostname()` is actually a
fallback for `getfqdn()` when it is not able to resolve a
canonical hostname through network. The claimfile that uses
`self._hostname` is still usable, as it uses `pid` and random
number to generate the resulting lock file name, which is unique
enough for our application.

[1] https://github.com/iterative/dvc/issues/2582

Fixes #2582
2019-10-21 19:20:08 +03:00
Ruslan Kuprieiev 59e62a7227 checkout: accept multiple targets (#2640)
Make `Repo.checkout` accept a list of targets, instead of having to feed
it targets one-by-one. This allows us to properly handle checkout
errors.

Fixes #2580
2019-10-21 17:06:21 +03:00
Alexander Schepanovski 33b0eefec0 dvc: dedup used cache (#2630)
* dvc: dedup used cache

This is a preparation step to make garbage collector work with more
commits. It has a value of its own:
- less exists calls to show cloud status or prepare push/pull
- same names are not repeated in status and pbar texts

* dvc: delay checksum names concatenation
2019-10-21 12:12:56 +03:00
Alexander Schepanovski 1f58fc6f42 dvc: dedup brancher (#2639) 2019-10-20 17:32:42 +03:00
Casper da Costa-Luis 1e01ce3ac9 isatty (#2550)
* suppress Tqdm when isatty

- fixes #2549

* fix tests

* unconditionally disable Tqdm if not isatty

- fixes https://github.com/iterative/dvc/pull/2550#discussion_r329386950

* add TQDM_DISABLE env var

- update tests
- add documentation

* TQDM_DISABLE => TQDM_ISATTY

* tests: mock.patch sys.stderr

* TQDM_ISATTY => DVC_ISATTY

as per https://github.com/iterative/dvc/pull/2550#discussion_r334565120

* dvc.progress.Tqdm file kwarg

- fixes https://github.com/iterative/dvc/pull/2550/files/ad1f245107c405697c4b61fe58a92fc0265c6588#r335706955

* dynamic env DVC_ISATTY

- fixes
  https://github.com/iterative/dvc/pull/2550/files/ad1f245107c405697c4b61fe58a92fc0265c6588#r335811454

* DVC_ISATTY in tests

- fixes https://github.com/iterative/dvc/pull/2550/files/ad1f245107c405697c4b61fe58a92fc0265c6588#r335811099

* re-add and abstract `env2bool` to `dvc.utils`

- partially reverts commit 524a0df188f566fb3e103f6965f1b6ac9c530886
- fixes
  https://github.com/iterative/dvc/pull/2550/files/ad1f245107c405697c4b61fe58a92fc0265c6588#r335811639

* env2bool tidy

* rename DVC_ISATTY => DVC_IGNORE_ISATTY

- fixes https://github.com/iterative/dvc/pull/2550#discussion_r335947382
2019-10-20 00:54:06 +03:00
Nabanita Dash a0cac4a3c8 changed order of imports (#2636) 2019-10-19 01:14:24 +03:00
Nabanita Dash 3eabb65781 corrected import order (#2634) 2019-10-19 00:32:45 +03:00
Ruslan Kuprieiev 638aaa254e dvc: bump to 0.63.3 2019-10-18 14:32:16 +03:00
Ruslan Kuprieiev 566d7b48d0 lock: monkeypatch socket.getfqdn used by flufl.lock (#2628)
Workaround for slow and obsoleted gethostbyaddr used in
`socket.getfqdn()`. See [1], [2] and [3] for more info.

[1] https://bugs.python.org/issue5004
[2] https://github.com/iterative/dvc/issues/2582
[3] https://gitlab.com/warsaw/flufl.lock/merge_requests/12

Flufl.lock guys seem unresponsive, so we have to monkeypatch
this by ourselves, at least for now.

Fixes #2582
2019-10-18 12:32:22 +03:00
Ruslan Kuprieiev 25ff08fdc8 dvc: bump to 0.63.2 2019-10-17 23:13:02 +03:00
Casper da Costa-Luis 8eb520f516 fix ncols_desc=0 (#2627)
- fixes #2598
2019-10-17 23:11:47 +03:00
Alok Kumar Sahoo e53f7bd0fd Fixed wrong order imports accordance to PEP (#2618)
* Fixed wrong order imports accordance to PEP

* Fixed Grouping

* Fixed Separations
2019-10-17 22:49:52 +03:00
SOFIA SUNAM c3109aa80d corrected ordering of base.py (#2626)
* corrected ordering of base.py

* corrected ordering of base.py

* corrected ordering of http.py

* corrected ordering of http.py

* corrected ordering of external repo.py
2019-10-17 22:37:42 +03:00
Ruslan Kuprieiev ccd39ef6cf dvc: bump to 0.63.1 2019-10-17 21:35:48 +03:00
Ruslan Kuprieiev 1bc076904d travis: use ruby 2.4.3 for mac 2019-10-17 21:12:38 +03:00
Ruslan Kuprieiev 32a9194eab hdfs: don't set default user (#2622)
https://opendatascience.slack.com/archives/CGGLZJ119/p1571253702008100?thread_ts=1571240388.006500&cid=CGGLZJ119
2019-10-17 20:09:21 +03:00
Ruslan Kuprieiev e2b40d5764 dvc: bump to 0.63.0 2019-10-17 15:52:17 +03:00
Ruslan Kuprieiev e7c5ea6bb4 setup: use more flexible networkx reqs
https://github.com/iterative/dvc/commit/50fc721811a015142b476e90f8d198a99e88892f
2019-10-17 15:13:12 +03:00
Ruslan Kuprieiev 50fc721811 setup: fix networkx reqs 2019-10-17 14:22:28 +03:00
Ruslan Kuprieiev 4171aac029 pylint: disable bad-continuation error
https://github.com/PyCQA/pylint/issues/289
2019-10-16 18:50:36 +03:00
Ruslan Kuprieiev 04105036c2 pylint: disable missing-docstring error 2019-10-16 18:46:53 +03:00
Ruslan Kuprieiev 2df6555ef0 setup: update pyinstaller to 3.5 (#2615)
As a part of the research for https://github.com/iterative/dvc/issues/2600
2019-10-16 17:22:25 +03:00
Mr. Outis 13a7e0f978 logger: send warning messages to stderr (#2612)
* logger: send warning messages to stderr

Fix #2561

* logger: filter warnings for stdout
2019-10-16 03:12:00 +03:00
Ruslan Kuprieiev 0bfb3c0ab2 get/import/url: handle existing output directory (#2610)
* get/import/url: handle existing output directory

Currently if you run

```
dvc get https://github.com/iterative/example-get-started model.pkl --out .
```

it will nuke your cwd. To fix that we need to properly handle existing
directories, same as usual unix cli utilities do that.

* travis: specify python version when installing from chocolatey

Fixes #2609
2019-10-15 22:42:52 +03:00
Kirill Malev 7c65fd8831 cli: changed help message (-h) to start with capital letter (#2591)
* cli: -h message now starts with capital letter
Fixes #1064

* Added explicit clarification for purposes of adding code
2019-10-15 14:32:14 +03:00
Ruslan Kuprieiev f4739b60ae Merge pull request #2607 from algomaster99/reduce-verbosity
Reduce verbosity of `dvc add`
2019-10-15 00:07:57 +03:00
Ruslan Kuprieiev 2491143d7a Merge pull request #2597 from efiop/2563
remote: local: don't auto-create directory in `__init__`
2019-10-14 23:34:21 +03:00
Ruslan Kuprieiev 803ffaec57 remote: local: don't auto-create directory in __init__
Imagine the scenario of shared cache. To set it up, user would do:

```
dvc config cache.dir /path
dvc config cache.shared group
```

Now, when he runs any dvc operation that involves the Repo instance,
RemoteLOCAL will also be initialized and it will currently auto-create
the directory with a regular `os.mkdir`, which is not using group
permissions. To solve that, we can't simply use `self.makdirs` instead,
as it will cause same issues if you run any dvc command before
`dvc config cache.shared group`. So what we should do is create
directories on demand, instead of creating it in advance, same as we do
in other remotes.

The change itself is pretty trivial and involved removing that `mkdir`
line, adjusting tests to not rely on cache dir existence and also
adjusting `dvc version` to not rely on cache directory existance.

Fixes #2563
2019-10-14 15:50:49 +03:00
Aman Sharma 9358a1bcc2 Reduce verbosity of dvc add 2019-10-14 13:56:06 +05:30
Ruslan Kuprieiev 2b6446a8d9 Merge pull request #2604 from Suor/reset-dvcignore
dvc: reset dvcignore
2019-10-13 18:07:43 +03:00
Alexander Schepanovski c26233acbb dvc: reset dvcignore
This depends on filesystem and affects stages collection so must be
reset.
2019-10-13 19:18:53 +07:00
Aman Sharma ed89725c98 Replace hyphen with underscore for command auto-completion (#2596)
* Replace hyphen with underscore for bash auto-completion

* Write function to replace hyphen and check for flags

* Nest function inside `_dvc`
2019-10-12 00:35:52 +03:00
Alexander Schepanovski 44264857c3 ui/perf: do not clone same repo multiple times (#2583)
* ui/perf: do not clone same repo multiple times

Closes #2490

External repos are still created in /tmp. They might be reused until
main() end. If no perfect match is found then a repo with the same url
is copied (not cloned again) and adjusted to fit a request: rev and
cache_dir.

NOTE: test_download_error_pulling_imported_stage() was modified to not
rely on clone error.

* test: fix import update test

* test: fix external_repo test under Windows

* dvc: fix non-master default branch for external repos

* dvc: remove unused .unset_dir() method
2019-10-11 13:56:37 +03:00
Ruslan Kuprieiev 5cabb6d115 Merge pull request #2595 from efiop/2563
dvc: move `local/__init__.py` -> `local.py`
2019-10-11 00:10:13 +03:00
Ruslan Kuprieiev 7ed8cea12a dvc: move local/__init__.py -> local.py
No need to keep that subdir for 1 file.
2019-10-10 23:34:02 +03:00
Ruslan Kuprieiev 4535d626bb Merge pull request #2590 from efiop/state
dvc: load state when taking repo lock in @locked
2019-10-10 22:49:16 +03:00
Ruslan Kuprieiev 33bd3d63c5 dvc: bump to 0.62.1 2019-10-10 14:40:04 +03:00
jahabdank 69195fdbfd Update __init__.py (#2593)
found an issue when doing `dvc cache dir`. Should end with os separator
2019-10-10 14:39:01 +03:00
Irchan Bani 711dc2ea9a setup: change define __version__ (#2592)
* setup: change define __version__

* setup: run pre-commit install, black, adn flake8
2019-10-10 14:21:16 +03:00
Ruslan Kuprieiev cba8ac7a8f dvc: load state when taking repo lock in @locked
We are always loading state when taking a repo lock, so doing it right
in @locked decorator simplifies the code for Repo methods.
2019-10-10 01:08:28 +03:00
Ruslan Kuprieiev f254004fae dvc: bump to 0.62.0 2019-10-09 11:47:52 +03:00
Ruslan Kuprieiev e34af30353 Merge pull request #2586 from efiop/2585
scripts: don't include completion scripts into mac pkg
2019-10-08 18:02:37 +03:00
Ruslan Kuprieiev 3b8c48793b scripts: don't include completion scripts into mac pkg
Fixes https://github.com/iterative/dvc/issues/2585
2019-10-08 16:48:36 +03:00
Paweł Redzyński d2f8cb0520 api: open scm controlled files (#2566)
* tree: make open method use mode and encoding

* api: try to open using self.tree if output not found

* repo: rename OutputMissingException

* test: api: test adding not cached output

* api: open: exists test instead of try

* repo: open: try opening from tree first

* repo: open: handle missing and not cached files

* repo: open: separate find out logic from opening
2019-10-08 15:23:42 +03:00
Vasilis Gerakaris 156fc9d4e1 dvc: Reduce import output verbosity (#2576)
Move some messages generated during import from
INFO to DEBUG log level.

Fixes #2489
2019-10-05 14:43:32 +03:00
Vasilis Gerakaris 49ffa129d5 dvc: Set max-line-length to 79 in .pylintrc (#2578)
Adjust the `max-line-length` option to the same value used by black and `.flake8` file.
2019-10-05 14:43:00 +03:00
Vasilis Gerakaris 8c1addff91 dvc: Fix typo in check_can_commit message (#2577) 2019-10-05 14:42:26 +03:00
Ruslan Kuprieiev 449f43f3cc requirements: require gitdb2 >= 2.0.6
Fixes https://github.com/iterative/dvc/issues/1880
2019-10-04 12:58:59 +03:00
Ruslan Kuprieiev 96eb7197c1 Merge pull request #2552 from mroutis/fix-2043
git: escape filename before ignoring it
2019-10-04 11:33:48 +03:00
Ruslan Kuprieiev f2cf5930aa Merge pull request #2571 from jimmy1134/master
Update to Readme
2019-10-04 11:31:23 +03:00
jimmy1134 e927964d20 Update to Readme
Additional clarification for what DVC aims to do.
2019-10-04 13:48:13 +10:00
Mr. Outis cfecb75c4c setup: pathspec@1d321d -> pathspec>=0.6.0 2019-10-03 21:55:21 -05:00
Mr. Outis e6ccf32189 scripts: upgrade pip before installing dependencies 2019-10-03 17:26:29 -05:00
Ruslan Kuprieiev 092c498baa dvc: bump to 0.61.2 2019-10-03 22:53:33 +03:00
Ruslan Kuprieiev f7188f564e Merge pull request #2568 from efiop/2181
deb/rpm/pkg: add zsh completion scripts
2019-10-03 22:52:50 +03:00
Ruslan Kuprieiev 41552d0af7 deb/rpm/pkg: add zsh completion scripts
Fixes #2181
2019-10-03 22:02:33 +03:00
Ruslan Kuprieiev 06f00b904a Merge pull request #2564 from nik123/master
scripts: make completion scripts part of the debian package
2019-10-03 21:50:51 +03:00
Nikita Kodenko b1e56896d6 Completion script for MacOS 2019-10-04 01:09:56 +07:00
Mr. Outis 1f6d8964b5 setup: pathspec@master -> pathspec@1d321d 2019-10-03 12:25:12 -05:00
Mr. Outis a47ea23561 git: use escape method from GitWildMatchPattern 2019-10-03 12:10:52 -05:00
Nikita Kodenko 680a250119 scripts: bash completion included in deb/rpm packages
Fixes #2181
2019-10-02 22:49:50 +07:00
Ruslan Kuprieiev 8787ac0380 dvc: bump to 0.61.1 2019-10-02 10:17:22 +03:00
Ruslan Kuprieiev fdf0a8e99a Merge pull request #2562 from efiop/pyenv
run: improve pyenv PATH modification criteria
2019-10-02 10:16:52 +03:00
Ruslan Kuprieiev 132de572af run: improve pyenv PATH modification criteria
https://discordapp.com/channels/485586884165107732/485596304961962003/628766062472724510

Related to #2506
2019-10-02 06:33:01 +03:00
Ruslan Kuprieiev 8fb3eaaa08 dvc: bump to 0.61.0 2019-10-01 10:56:00 +03:00
Ruslan Kuprieiev 39104a1dee Merge pull request #2557 from efiop/2506
run: make shell not load config files
2019-10-01 07:59:07 +03:00
Ruslan Kuprieiev ee976a1c1a run: make shell not load config files
Config files like `~/.zshenv`, `~/.bashenv` etc, are loaded on each
shell invocation, which might overwrite the environment breaking the
commands we are trying to wrap. To fix that, we need to provide
appropriate flags to the shell we are using to avoid loading those
configs.

Fixes #2506
2019-09-30 19:34:55 +03:00
Mr. Outis 9496f34f45 tests: Windows specific filename for gitignore 2019-09-30 11:28:38 -05:00
Mr. Outis 80cb1c919a git: escape question mark character 2019-09-30 11:27:53 -05:00
Mr. Outis e692be2dc9 sftp: add a more descriptive error on mkdirs (#2554)
* sftp: add a more descriptive error on mkdirs

Fix #2535

* sftp: raise exception instead of logging one
2019-09-30 19:24:31 +03:00
Mr. Outis a9201f8237 git: add a note referencing gitignore pattern 2019-09-30 11:21:39 -05:00
Paweł RedzyńskiandRuslan Kuprieiev 119548e2a1 utils: fix_env: dont delete LD_LIBRARY_PATH from env (#2541)
* utils: fix_env: dont delete LD_LIBRARY_PATH from env

* utils: add related issue to comment on why there is empty LD_LIBRARY_PATH

* Update dvc/utils/__init__.py

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

* scm: git: move LD_LIBRARY_PATH fix to Git.clone method
2019-09-30 15:28:36 +03:00
Ruslan Kuprieiev f0de362e0d Merge pull request #2548 from iterative/py27
fix py27 compat
2019-09-30 08:35:29 +03:00
Mr. Outis c9c36c2049 git: escape filename before ignoring it
Fix #2043
2019-09-29 23:16:46 -05:00
Casper da Costa-Luis 1f46ca2463 silly import fix 2019-09-29 17:56:52 +01:00
Casper da Costa-Luis 5f65e8096e back to initial approach 2019-09-29 17:54:04 +01:00
Casper da Costa-Luis 83f55de7a9 move compatibility helper 2019-09-29 16:43:37 +01:00
Ruslan Kuprieiev beea7e56f2 Merge pull request #2543 from casperdcl/checksum
remove unneeded TqdmThreadPoolExecutor
2019-09-29 09:15:01 +03:00
Ruslan Kuprieiev e3450257c6 Merge pull request #2544 from yfarjoun/patch-1
Added a space in error message
2019-09-29 09:13:10 +03:00
Casper da Costa-Luis a0d99e2486 remove unneeded TqdmThreadPoolExecutor
- fixes #2539
2019-09-28 23:54:14 +01:00
Casper da Costa-Luis 4c01a86302 fix py27 compat 2019-09-28 23:12:55 +01:00
Yossi Farjoun 6df4188bd2 Added a space in error message
als made consistent the placement of inter-word space when there's a newline in the code
2019-09-28 11:42:03 -04:00
Ruslan Kuprieiev 5c76b46b46 Merge pull request #2538 from mroutis/fix-2524
py2: display friendly error when raising a UnicodeError
2019-09-28 03:35:14 +03:00
Mr. Outis 39dc79b418 main: fix UnicodeError misplacement 2019-09-27 18:20:00 -05:00
Mr. Outis 0bdad65f04 main: make sure to raise UinocedeError for py3 2019-09-27 14:18:08 -05:00
Ruslan Kuprieiev 22afc48d9b Merge pull request #2540 from Suor/repo-imports
perf: import dvc.repo dynamically
2019-09-27 12:02:21 +03:00
Alexander Schepanovski 0203fd4017 perf: import dvc.repo dynamically 2019-09-27 15:08:42 +07:00
Mr. Outis 1763607a88 py2: display friendly error when raising a UnicodeError 2019-09-26 19:50:15 -05:00
Ruslan Kuprieiev 4061d4aff1 dvc: bump to 0.60.1 2019-09-26 18:34:06 +03:00
Ruslan Kuprieiev 3eb23870d2 build: posix: don't forget to install optional psutil
Fixes https://github.com/iterative/dvc/issues/2537
2019-09-26 18:33:36 +03:00
Ruslan Kuprieiev 381601f4ee dvc: cache graph/stages/pipelines (#2529)
* dvc: cache graph/stages/pipelines

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

* repo: check_dag: don't reset the cache

https://github.com/iterative/dvc/pull/2529#discussion_r327711928

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

* repo: rename check_dag(stages) -> check_modified_graph(new_stages)

https://github.com/iterative/dvc/pull/2529#discussion_r327708439

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

* repo: refactor "active" logic and remove it from Repo class

* repo: use tree.setter to automate cache reset

As noted by @Suor's , this is much less prone to errors.

* repo: move graph-related staticmethods into graph.py

As per @Suor's request.

* dvc: remove unused from_directory

https://github.com/iterative/dvc/pull/2529#discussion_r328079258

* repo: use _tree instead of __tree

https://github.com/iterative/dvc/pull/2529#discussion_r328071115

* repo: add a note about cache invalidation on repo.lock release

https://github.com/iterative/dvc/pull/2529#discussion_r328071635

* repo: rename reset() -> _reset()

https://github.com/iterative/dvc/pull/2529#discussion_r328079899

* pipeline: use graph helpers

https://github.com/iterative/dvc/pull/2529#discussion_r327502293

* repo: use list comprehension for collect()

https://github.com/iterative/dvc/pull/2529#discussion_r328076651

* repo: graph: only use first cycle

https://github.com/iterative/dvc/pull/2529#discussion_r328081346

* dvc: don't use G.nodes()

https://github.com/iterative/dvc/pull/2529#discussion_r328089671
https://github.com/iterative/dvc/pull/2529#discussion_r328089878
2019-09-26 07:42:29 +03:00
Ruslan Kuprieiev fea05b3924 Merge pull request #2536 from mroutis/contributing
move .github/CONTRIBUTING.md -> CONTRIBUTING.md
2019-09-26 06:27:30 +03:00
Mr. Outis 5c5dfed68f .github/CONTRIBUTING.md -> CONTRIBUTING.md 2019-09-25 21:33:46 -05:00
Ruslan Kuprieiev 987cf883c5 dvc: bump to 0.60.0 2019-09-25 11:12:20 +03:00
Ruslan Kuprieiev dcce70f4b9 output: base: checkout: make returns consistent
As reported by deep source: Either all return statements in a function should return an expression, or none of them should.
2019-09-24 16:33:15 +03:00
Paweł Redzyński be012e70cf checkout: raise on non existing dir cache (#2514)
* checkout: raise on non existing dir cache

* checkout: don't throw on failed load_dir_cache

* checkout: raise on failed checkouts, only after all checkouts are perfromed

* remote: base: raise on failed dir cache loading

* rename exception for failed checkout
2019-09-24 16:31:36 +03:00
Ruslan Kuprieiev 7409fadb84 Merge pull request #2527 from Suor/delay-cache
perf: delay cache creation
2019-09-24 08:11:45 +03:00
Alexander Schepanovski e2eb4ae0c3 perf: delay cache creation
The older code instantiates non-local cache remotes on Repo creation if
any of those are configured. And remote instantiation loads their API
libs.
2019-09-24 09:03:03 +07:00
Ruslan Kuprieiev 33f1cdfab8 Merge pull request #2525 from efiop/2506
dvc: remove PATH prefixes appended by pyenv
2019-09-23 19:35:46 +03:00
Ruslan Kuprieiev 313fdce5fa Merge pull request #2528 from Suor/datacloud-dead-code
dvc: remove dead code in DataCloud
2019-09-23 16:26:44 +03:00
Alexander Schepanovski 4006c6434e dvc: remove dead code in DataCloud 2019-09-23 20:00:19 +07:00
Ruslan Kuprieiev bd2b5900a1 Merge pull request #2526 from efiop/2495
dvc: remote: optimize imports
2019-09-23 12:44:09 +03:00
Ruslan Kuprieiev fd14ca3269 dvc: remote: optimize imports
Decreases `time dvc --version` from ~1.4 sec to ~0.3 sec.

Related #2495
Related #2445

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-09-23 11:30:45 +03:00
Ruslan Kuprieiev 43975c8618 dvc: remove PATH prefixes appended by pyenv
Unlike PyInstaller, pyenv doesn't leave backups of original env vars
when it modifies them. If we look into the shim, pyenv and pyenv-exec,
we can figure out that the PATH is modified like this:

    PATH=$PYENV_BIN_PATH:${bin_path}:${plugin_bin}:$PATH

where

    PYENV_BIN_PATH - starts with $PYENV_ROOT, see pyenv-exec source code.
    bin_path - might not start with $PYENV_ROOT as it runs realpath on
        it, see pyenv source code.
    plugin_bin - might contain more than 1 entry, which start with
        $PYENV_ROOT, see pyenv source code.

So having this, we can make a rightful assumption about what parts of the PATH
we need to remove in order to get the original PATH.

Fixes #2506
2019-09-23 04:25:42 +03:00
Ruslan Kuprieiev 70efa6c52a Merge pull request #2523 from efiop/daemon
daemon: use `sys.argv[0]` instead of `-m dvc`
2019-09-22 05:42:56 +03:00
Ruslan Kuprieiev 0347cfb891 gitignore: don't forget to add /tmp 2019-09-22 05:22:55 +03:00
Ruslan Kuprieiev 31b40816b5 daemon: use sys.argv[0] instead of -m dvc
This way dvc will actually launch the same script that it is running
from and not installed dvc module, that might not match.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-09-22 05:18:51 +03:00
Mr. OutisandRuslan Kuprieiev ae90904fb3 pull: download data from imported stages (#2499)
* wip - pull is working

* use fetch instead of reproduce

* test pull and fetch

* style changes

* fetch: catch config error on fetch instead of get_remote

* output: remove unused guard clause

* fetch: define a more specific error instead of ConfigError

* fetch: display number of errors correctly

* exceptions: remove "Failed" prefix to download/upload

* fetch: catch other exceptions that might halt the download

* fetch: log catched exceptions

* fetch: reformat exception

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

* fix formatting
2019-09-22 01:28:12 +03:00
Ruslan Kuprieiev 23fc2662d0 Merge pull request #2519 from efiop/755
dvc: move repo lock from CLI to API
2019-09-21 19:12:34 +03:00
Ruslan Kuprieiev 9d6ca1ef6c dvc: move repo lock from CLI to API
For the longest time we've had `repo.lock`, but were only using it from
CLI when running specific command, instead of simply properly taking it
on API operations. This patch takes care of that.
2019-09-21 05:10:06 +03:00
Ruslan Kuprieiev 64c3973522 Merge pull request #2517 from Suor/later-imports
perf: import some things later
2019-09-20 18:00:01 +03:00
Ruslan Kuprieiev f1d3f4344e Merge pull request #2494 from Suor/lazy
perf: do not use pkg_resources
2019-09-20 13:47:47 +03:00
Alexander Schepanovski cb5ff6e085 perf: import some things later
Saves on startup time
2019-09-20 16:45:02 +07:00
Paweł RedzyńskiandRuslan Kuprieiev d16178ab61 checkout: raise error on failed dir cache loading (#2503)
* remote: base: load_dir_cache: raise exception on failed download

* output: base: raise error on failed get_files_number

* remote: base: log failed download as warning

* Update dvc/output/base.py

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

* remote: base: dont download dir cache on load_dir_cache
2019-09-19 16:35:37 +03:00
Paweł RedzyńskiandRuslan Kuprieiev 349b4af3ae install: verify no hooks colliding with dvc exist (#2505)
* install: verify no hooks colliding with dvc exist

* test: install: remove obsolete tests

* Update tests/func/test_install.py

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

* test: install: import GitHookAlreadyExistsError
2019-09-19 15:40:58 +03:00
Ruslan Kuprieiev 67e31eaa2f Merge pull request #2504 from Suor/http-compat
perf: do not import http.server each run
2019-09-18 22:27:52 +03:00
Alexander Schepanovski ffb52b256b perf: do not import http.server each run
It is only used in tests.
2019-09-17 18:41:05 +07:00
Alexander Schepanovski eac0aa13b2 perf: use packaging.version instead of pkg_resources
This is the same functionality without a cost of loading a huge package.
2019-09-17 14:32:31 +07:00
Ruslan Kuprieiev e36f242f66 Merge pull request #2493 from pared/2345
output: prevent stage file from being used as output
2019-09-14 21:42:06 +03:00
Paweł RedzyńskiandRuslan Kuprieiev 53f6aead95 Update dvc/dependency/base.py
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2019-09-14 19:40:59 +02:00
Paweł RedzyńskiandRuslan Kuprieiev 74a0cef3e5 Update dvc/output/base.py
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2019-09-13 16:02:49 +02:00
pawel 0935930f46 output: prevent stage file from being used as output 2019-09-13 13:31:50 +02:00
Ruslan Kuprieiev 6d16227bc1 Merge pull request #2496 from efiop/2483
path_info: don't forget to override as_posix method
2019-09-13 05:12:39 +03:00
Ruslan Kuprieiev f0c43b4e02 path_info: don't forget to override as_posix method
Unlike original implementation [1] that uses `str()` we actually need
to use `fspath`, because we've overriden `__str__` method to return
relative paths, which will break original `as_posix`.

[1] https://github.com/python/cpython/blob/v3.7.0/Lib/pathlib.py#L692

Fixes #2483

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-09-13 04:52:03 +03:00
Alexander Schepanovski 293a50d75c perf: use fast entry points on non-wheel install
This avoids importing `pkg_resources` on startup, which saves around 200
ms.

NOTE: wheel installer uses its own entry points template, which is the same
fastentrypoints.py script provides, so no gain there.
2019-09-12 22:33:59 +07:00
Paweł RedzyńskiandRuslan Kuprieiev 69f0de0090 google-resumable-media build problems (#2449)
* google-resumable-media hook for pyinstaller

* working wtih g-r-m build with -e

* update google-cloud-storage

* google-cloud-storage hook: remove hiddenimports

* Update setup.py

Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2019-09-12 12:12:37 +03:00
Ruslan Kuprieiev da6a0ca933 lock: remove unused lifetime argument 2019-09-11 16:45:09 +03:00
Ruslan Kuprieiev 05adcf9a4f Merge pull request #2461 from Suor/nfs
dvc: switch to flufl lock
2019-09-11 16:43:40 +03:00
Ruslan Kuprieiev 8338d4507c Merge pull request #2488 from pared/2444
remote: checkout: reuse _checkout_file functionality
2019-09-11 15:22:18 +03:00
pawel d4c9c71a2f remote: checkout: reuse _checkout_file functionality 2019-09-11 11:26:30 +02:00
Alexander Schepanovski 3c3378bdca dvc: make lock lifetime long by default 2019-09-11 14:21:12 +07:00
Alexander Schepanovski d9d85f3cb0 dvc: fix locks in Python2.7/Windows
Return to zc.lockfile lock in Python 2, NFS is not supported there
anyway since sqlite3 doesn't support URIs.
2019-09-11 14:17:21 +07:00
Alexander Schepanovski f29f07ca88 dvc: do not lock sqlite db 2019-09-11 14:17:16 +07:00
Ruslan Kuprieiev 09f6b1c7b9 Merge pull request #2485 from pared/2477
move: use relpath to find target path
2019-09-10 16:23:27 +03:00
pawel a16dc58ae3 move: use relpath to find target path 2019-09-10 14:03:55 +02:00
Ruslan Kuprieiev 9fa6bee13a Merge pull request #2482 from efiop/master
test: repro: fix link types
2019-09-10 03:10:29 +03:00
Ruslan Kuprieiev 384fd4cd9a test: repro: fix link types 2019-09-10 02:12:28 +03:00
Ruslan Kuprieiev 2c003fdc04 Merge pull request #2426 from mroutis/close-1652
ssh: support linking
2019-09-09 20:48:22 +03:00
Ruslan Kuprieiev af8a7aa25d dvc: bump to 0.59.2 2019-09-09 20:28:37 +03:00
Mr. Outis 3c5bcbd463 local: mark static methods 2019-09-09 12:26:48 -05:00
Ruslan Kuprieiev b0b958e14b Merge pull request #2481 from efiop/2476
scm: remove `pkg install` leftovers
2019-09-09 20:16:01 +03:00
Ruslan Kuprieiev a529f7d8ad Merge pull request #2479 from efiop/always_reproduce
run: introduce --always-changed
2019-09-09 19:45:35 +03:00
Ruslan Kuprieiev ce27444dd2 scm: remove pkg install leftovers
Leftover code was causing problems when working with symlinks, because
it tried to use `realpath` on symlinks to further use it to determine
parent directory.

Fixes #2476
2019-09-09 19:43:10 +03:00
Mr. Outis 2a847ef25c tests: ignore ssh's hardlink and copy tests on windows 2019-09-09 11:20:29 -05:00
Mr. Outis e0fd808ab0 setup: iterative/mock-ssh-server -> pypi mock-ssh-server 2019-09-09 11:15:49 -05:00
Ruslan Kuprieiev 9638b454d7 Merge pull request #2478 from pared/2443
stage: prevent writing default wdir into stage file
2019-09-09 18:23:36 +03:00
Ruslan Kuprieiev 357ce82f28 run: introduce --always-changed
This is an extension of our callback stages (no deps, so always
considered as changed) for stages with dependencies, so that these
stages could be used properly in the middle of the DAG. See attached
issue for more info.

Related to #2378
2019-09-09 18:03:28 +03:00
pawel 90738fab9c stage: prevent writing default wdir into stage file 2019-09-09 16:20:33 +02:00
Ruslan Kuprieiev b3e4ff7cba test: install: don't forget to adjust hook tests 2019-09-09 05:10:30 +03:00
Ruslan Kuprieiev 79cd3d670f dvc: bump to 0.59.1 2019-09-09 00:26:54 +03:00
Ruslan Kuprieiev 2d2ac67e50 hooks: make syntax compatible with bare sh
`==` is not part of the posix and not all shells (e.g. dash) support it, making hooks break.
2019-09-09 00:26:04 +03:00
Mr. OutisandRuslan Kuprieiev 5103dfd4aa tests: fix skipif on ssh/test_connection.py
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2019-09-07 21:38:50 -05:00
Mr. Outis b4507444d5 version: adjust get_fs_type to discover all partitions 2019-09-06 19:27:23 -05:00
Ruslan Kuprieiev 2ee9da61f0 dvc: bump to 0.59.0 2019-09-07 00:13:05 +03:00
Ruslan Kuprieiev b61c97aa68 Merge pull request #2475 from efiop/2471
erepo: fix environment before running git
2019-09-07 00:12:35 +03:00
Ruslan Kuprieiev 60d8e8fbec erepo: fix environment before running git
Fixes #2471
2019-09-06 23:42:46 +03:00
Paweł Redzyński 0a214ae15a test: add: remove TestDvcGit (#2467) 2019-09-06 23:15:26 +03:00
Mr. Outis 190fbaa63b get: no relpath on output not found (#2435)
* get: no relpath on output not found

related #2430

* get: catch output not found
2019-09-06 23:14:07 +03:00
Mr. Outis 05cbe0c922 tests: support windows 2019-09-06 14:17:44 -05:00
Mr. Outis 737f1e141d tests: use repo_dir instead of custom fixtures 2019-09-06 13:28:48 -05:00
Mr. Outis b66996e0a1 ssh: support reflinks in Darwin 2019-09-06 13:24:25 -05:00
Mr. Outis b903fe8023 get: show error message when path is a DVC-file (#2470)
* get: show error message when path is a DVC-file

Fix #2468

* exceptions: rename output for data artifact

* get: use method to indetify if file is a DVC-file

* exceptions: change GetDVCFileError message
2019-09-06 21:11:23 +03:00
Mr. Outis dd1d5bdbb0 tests: PosixPath to string in chdir_tmp fixture 2019-09-06 12:57:06 -05:00
Mr. Outis 73133e8c7a tests: [re] fix hardlink stuff with HDFS 2019-09-06 12:35:38 -05:00
Mr. Outis 23f2650b24 ssh: remove write_through in TextIOWrapper 2019-09-06 12:07:22 -05:00
Mr. Outis f97c33178f tests: use copy as cache type for HDFS 2019-09-06 12:05:53 -05:00
Mr. Outis 6eb26de856 fix tests 2019-09-06 11:32:31 -05:00
Mr. Outis 38d4cbf5c1 fix broken merge 2019-09-06 09:42:56 -05:00
Mr. Outis 42168d34f3 Merge branch 'master' into close-1652 2019-09-06 09:29:54 -05:00
5ec1d274ae remote: base: don't checkout existing file (#2358)
* remote: base: don't checkout existing file

* remote: _checkout_file: split to smaller methods

* state: local: verify if file needs checkout considering link type and access rights

* tests: checkout: test avoiding unnecessary copying

* efiop review refactor

* remote: local: refactor _needs_checkout NOTE

* remote: local: remove bool cast for `protected`, wrap linking in try

* test: checkout: test relinking upon existing cache

* stage: remove obsolete unprotecing from Stage.create

* test: checkout: refactor setting link type

* test: checkout: parametrize relinking test

* remote: local: don't remove cache test file on cache_copy test

* remote: base: remove force from _needs_checkout arguments

* Update dvc/remote/local/__init__.py

Co-Authored-By: Alexander Schepanovski <[email protected]>

* suor review refactor

* remote: base: re-add note why do we unprotect on _checkout_file

* test: checkout: tests for relinking and resetting target protection

* remove checkout behavior change

* remove unused imports

* remote: base: reuse __checkout_file in __checkout_dir

* initial dir

* checkout: test: should relink on repeated add

* remote: cleanup commented lines

* remote: base/local: add save_link flag to _checkout_file

* remote: local: add allow_copy flag to unprotect method

* Update dvc/remote/local/__init__.py

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

* remote: local: remove unnecessary removal warning on _checkout_file

* remote: local: proper cache link type detection

* test: checkout: move repeated add tests

* Update dvc/remote/local/__init__.py

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

* remote: base: remove _needs_checkout

* Update dvc/remote/base.py

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

* remote: base: remove wrappers from _checkout_file
2019-09-06 17:20:32 +03:00
Mr. Outis 739236eab0 ssh: accept only wb mode for open 2019-09-06 08:48:22 -05:00
Alexander Schepanovski 6aeccfcc07 dvc: fix long lock filenames under Windows 2019-09-06 16:29:23 +07:00
Alexander Schepanovski 1bdaca0d6c dvc: switch to flufl lock
This locks employs hardlink trick, which works on NFS and alike unlike
fcntl/open locks.

Closes #1918.
2019-09-06 16:29:09 +07:00
Ruslan Kuprieiev 74bd7d59cf Merge pull request #2469 from pared/2465
remote: base: check progress_callback when skipping on checkout
2019-09-06 00:47:45 +03:00
Mr. Outis d7da4fcd37 fix formatting 2019-09-05 10:52:15 -05:00
Mr. Outis 369146a926 Merge branch 'master' into close-1652 2019-09-05 10:47:18 -05:00
pawel 5ec3cade68 remote: base: check progress_callback when skipping on checkout 2019-09-05 17:32:27 +02:00
Mr. Outis 0c231b68d7 fix formatting 2019-09-04 22:07:02 -05:00
Mr. Outis 079d589b59 local: use path_info for link operations 2019-09-04 19:38:16 -05:00
Mr. Outis 2fe6fc1ba6 refactor: use a module for slow link detection 2019-09-04 19:32:55 -05:00
Mr. Outis 9295bbd160 ssh: support opening files for writing 2019-09-04 15:13:26 -05:00
Mr. Outis 1ae9078fee revert remove assertions from open 2019-09-04 12:35:54 -05:00
Ruslan Kuprieiev 48d7ab6e22 dvc: bump to 0.58.1 2019-09-04 18:36:11 +03:00
Mr. Outis 1f0a57d4af remote: remove unnecessary variable in _try_links 2019-09-04 09:48:46 -05:00
Ruslan Kuprieiev 6d2f078a65 Merge pull request #2462 from pared/2125
stage: don't unprotect outs on stage create
2019-09-04 16:15:41 +03:00
pawel 651d925578 stage: don't unprotect outs on stage create 2019-09-04 12:54:32 +02:00
Ruslan Kuprieiev 287be8fd2e dvc: bump to 0.58.0 2019-09-04 00:37:25 +03:00
Ruslan Kuprieiev 213f46e821 Merge pull request #2460 from pared/2430
repo: get: intercept and convert NotDvcRepoErro to UrlNotDvcRepoError
2019-09-03 15:34:23 +03:00
pawel 4236f18e0c repo: get: intercept and convert NotDvcRepoErro to UrlNotDvcRepoError 2019-09-03 13:59:04 +02:00
Mr. Outis cdee18a7a6 fix formatting 2019-09-03 00:27:10 -05:00
Mr. Outis 3c69dc9fd5 tests: unit ssh symlink 2019-09-03 00:04:44 -05:00
Mr. Outis 88574f300e remote: remove unecessary else clause 2019-09-03 00:01:38 -05:00
Mr. Outis 69638527cb setup: mroutis/mock-ssh-server -> iterative/mock-ssh-server 2019-09-03 00:01:14 -05:00
Mr. Outis bbfaf3a22b setup: use mock-ssh-server version supporting lstat 2019-09-02 23:21:20 -05:00
Ruslan Kuprieiev 3a52732747 Merge pull request #2457 from jorgeorpinel/master
remove: fix help output
2019-09-01 10:35:56 +03:00
Jorge Orpinel 80076d88a1 remove: fix help output
per https://github.com/iterative/dvc/issues/2456#issuecomment-526857699

Fix #2456
2019-09-01 02:04:10 -05:00
Mr. Outis da65f8fe9a tests: functional ssh symlink 2019-08-31 13:34:48 -05:00
Mr. Outis 7b4584f86c ssh: stat -> lstat 2019-08-31 13:02:17 -05:00
Mr. Outis 70010bbb31 remote: remove unecessary makedirs on checkout 2019-08-31 12:59:34 -05:00
Mr. Outis 8d41833c7c path_info: remove urljoin leftover 2019-08-31 03:32:23 -05:00
Mr. Outis 0fc83be9ff tests: ssh hardlink optimization 2019-08-31 03:23:35 -05:00
Mr. Outis 0361b1b9c0 tests: ssh connection link methods 2019-08-31 01:56:00 -05:00
Mr. Outis 5f89242791 Merge branch 'master' into close-1652 2019-08-31 00:27:28 -05:00
Mr. Outis 40c61b3aaa remote: abstract link logic to base 2019-08-30 23:51:20 -05:00
Ruslan Kuprieiev 116684cb24 Merge pull request #2454 from pared/2441_workaround
workaround for google-resumable-media==0.3.3 build problem
2019-08-30 20:39:26 +03:00
pawel 331ab8c983 refactor note 2019-08-30 18:29:35 +02:00
Paweł RedzyńskiandRuslan Kuprieiev bd04972e9e Update setup.py
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2019-08-30 18:13:49 +02:00
Paweł RedzyńskiandRuslan Kuprieiev 48ac0b7c27 Update setup.py
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2019-08-30 18:13:40 +02:00
pawel 3c01f07ffd setup: add note why we restrict google-resumable-media version 2019-08-30 14:04:12 +02:00
pawel e766595687 workaround for google-resumable-media==0.3.3 build problem 2019-08-30 12:03:19 +02:00
Ruslan Kuprieiev 1ed36456fa Merge pull request #2436 from casperdcl/ui-tidy
progressbar cleanup
2019-08-29 13:24:27 +03:00
Ruslan Kuprieiev 5d05e9ab01 remote: quote pip install command
Fixes https://github.com/iterative/dvc/issues/2446
2019-08-28 21:13:30 +03:00
Casper da Costa-Luis 6c3029dede revert cache_dir move
partially reverts 5f81352f
2019-08-28 18:02:50 +01:00
Casper da Costa-Luis 7be68d9448 replace md5 => file 2019-08-28 17:58:53 +01:00
Casper da Costa-Luis 5f81352f90 add cache_dir to progress output 2019-08-28 17:44:12 +01:00
Casper da Costa-Luis 5413576e27 decrease final verbosity further; minor tidy 2019-08-28 17:22:33 +01:00
Casper da Costa-Luis bc452bb689 minor test fix 2019-08-27 20:30:43 +01:00
Casper da Costa-Luis f336afa861 remove Tqdm.truncate => add {ncols_desc} 2019-08-27 20:23:55 +01:00
Casper da Costa-Luis 03a5c6df5e progress default leave=False as per reviews 2019-08-27 14:00:04 +01:00
Casper da Costa-Luis 7171712fa9 delay bar_format 2019-08-27 13:41:37 +01:00
Casper da Costa-Luis 86605b6fa6 use funcy 2019-08-27 13:41:12 +01:00
Ruslan Kuprieiev e3f6c71576 Merge pull request #2428 from pared/2356
repo.active_stages should return active stages
2019-08-27 15:33:38 +03:00
Casper da Costa-Luis 043119b747 misc minor tidy as per review 2019-08-27 13:25:43 +01:00
pawel a9efabbc6e repo: used_cache: remove active flag 2019-08-27 14:08:00 +02:00
pawel babd5b2a12 repo: drop active_stages 2019-08-27 13:46:29 +02:00
pared 16b62d154a gc: remove unnecessary arguments for gc._load_all_used_cache 2019-08-27 13:46:29 +02:00
pared d02327fb53 repo: make repo.active_stages collect active stages only 2019-08-27 13:46:29 +02:00
pared 720608f379 repo: make repo.collect collect all stages, not active 2019-08-27 13:46:29 +02:00
Casper da Costa-Luis de97d3ddac persistent desc 2019-08-27 11:08:27 +01:00
Casper da Costa-Luis 7c6d68ad5a decrease verbosity, default progress leave=False 2019-08-27 10:57:18 +01:00
Ruslan Kuprieiev a2aaad17f6 Merge pull request #2440 from Suor/messages
Messages
2019-08-27 12:22:33 +03:00
Casper da Costa-Luis c8cb9ecef3 minor logic tidy
sacrifice accuracy for readability
2019-08-27 09:56:32 +01:00
Alexander Schepanovski b52265a1d2 ui: remove extraneous stage.save()
stage.update() already does that via .run(). That was causing printing a
confusing message to user.
2019-08-27 15:04:37 +07:00
Alexander Schepanovski c68b76f843 ui: remove "Reproducing ..." message
It as always followed by a more specific one, so it's superfluous.
2019-08-27 15:02:21 +07:00
Casper da Costa-Luis 06f920d138 fix unknown total progress
- depends on https://github.com/tqdm/tqdm/pull/800
2019-08-26 01:27:03 +01:00
Casper da Costa-Luis c9342754ed fixed with progress
- fixes #2434
2019-08-25 01:18:57 +01:00
Mr. Outis 83a9a6362b remote: remove unecessary + on open mode 2019-08-24 11:45:43 -05:00
Mr. Outis 9c9079753b remote: use getattr instead of dict to get link methods 2019-08-24 11:44:50 -05:00
Ruslan Kuprieiev 5747e37f41 Merge pull request #2433 from Suor/dirty-cache
cache: skip non-checksum files around cache
2019-08-24 17:21:45 +03:00
Alexander Schepanovski edcd1724b4 cache: skip non-checksum files around cache
Fixes #2394
2019-08-24 15:04:25 +07:00
Ruslan Kuprieiev 902edb1bee Merge pull request #2429 from efiop/fixup-2424
dvc: don't include gssapi
2019-08-24 10:14:47 +03:00
Mr. Outis 745b2e13a0 [wip] remote: abstract link logic to base 2019-08-24 01:47:58 -05:00
Mr. Outis 84872d231a docs: explain potential of hitting link limit 2019-08-24 01:20:26 -05:00
Mr. Outis 1be63dd93e ssh: implement interface for fs related operations 2019-08-24 00:16:37 -05:00
Ruslan Kuprieiev 096a043e21 dvc: don't include gssapi
If user needs it, he can install it by himself through pip.

https://github.com/iterative/dvc/pull/2424#issuecomment-523990535

https://github.com/iterative/dvc.org/pull/582
2019-08-24 07:20:06 +03:00
Ruslan Kuprieiev c5bcf94e2e Merge pull request #2427 from Suor/add
dvc: use wdir = "." on add
2019-08-24 07:17:41 +03:00
Mr. Outis d13b5a14f8 local: use PathInfo for link operations 2019-08-23 22:45:54 -05:00
Alexander Schepanovski 8984e9fed4 dvc: use wdir == "." on add
Fixes #2404
2019-08-24 10:23:41 +07:00
Mr. Outis 02cef7b9a3 ssh: makedirs before symlink 2019-08-23 22:18:48 -05:00
Mr. Outis 521647e47e ssh: implement slow_link_detection 2019-08-23 21:54:53 -05:00
Mr. Outis f4563bdd85 ssh: support reflinks and hardlinks 2019-08-23 21:20:50 -05:00
Mr. Outis 110da0c366 ssh: adapt link logic and support symlinks 2019-08-22 01:00:53 -05:00
Ruslan Kuprieiev 86c3cb785c readme: fix formatting 2019-08-22 04:30:44 +03:00
Ruslan Kuprieiev 208b181f9f readme: add conda-forge badge 2019-08-22 04:28:49 +03:00
Ruslan Kuprieiev 41351a2df2 dvc: bump to 0.57.0 2019-08-22 01:51:57 +03:00
Ruslan Kuprieiev e4f7c6124c Merge pull request #2424 from JoshuaPostel/ssh-gss-auth
ssh: add gss_auth option
2019-08-22 01:48:50 +03:00
Ruslan Kuprieiev 77c2dede1c Merge pull request #2422 from casperdcl/tqdm
minor unreachable code move
2019-08-21 22:05:42 +03:00
joshua postel cbc5e1b52d setup.py: add gssapi requirement
Fixes #2327
2019-08-21 14:10:53 -04:00
joshua postel cc87531df3 tests: added test for ssh gss_auth
Fixes #2327
2019-08-21 13:20:38 -04:00
joshua postel a1ad59ed03 ssh: add minimal support for gss_auth
Fixes #2327
2019-08-21 12:49:17 -04:00
Ruslan Kuprieiev 2d9497c524 Merge pull request #2421 from pared/2328
repo: move: save stage after moving
2019-08-21 16:33:13 +03:00
Casper da Costa-Luis e701221cff minor unreachable code move 2019-08-21 13:56:10 +01:00
Ruslan Kuprieiev 5aa9a2f501 Merge pull request #2333 from casperdcl/tqdm
change progress bar backend to tqdm
2019-08-21 15:38:51 +03:00
pared c4de07b2bd repo: move: save stage after moving 2019-08-21 14:34:23 +02:00
Ruslan Kuprieiev 4d267ef742 readme: fix incorrect installation instructions 2019-08-21 13:37:44 +03:00
Ruslan Kuprieiev fc98ba400c travis: remove unused cache entry 2019-08-20 20:12:31 +03:00
Ruslan Kuprieiev 2b2b591b11 Merge pull request #2419 from pared/2409
.travis.yml: add python version to build PyPi build scripts
2019-08-20 17:26:04 +03:00
Casper da Costa-Luis 4bd2ce25e1 get_files_number test fix 2019-08-20 14:27:20 +01:00
Casper da Costa-Luis 8faa02e837 quick review fixes 2019-08-20 14:18:43 +01:00
pared 77f3c6e1ad .travis.yaml: add python version to build PyPi build scripts 2019-08-20 15:17:23 +02:00
Ruslan Kuprieiev 4fb4b84a5f dvc: bump to 0.56.0 2019-08-20 03:56:14 +03:00
Ruslan Kuprieiev fe38867743 dvc: bump to 0.51.2 2019-08-20 03:55:21 +03:00
Ruslan Kuprieiev 750932d2c0 Merge pull request #2415 from AlJohri/enable-acl
add acl config
2019-08-20 03:53:05 +03:00
Al Johri 512bdc76f0 add acl config 2019-08-19 19:47:52 -04:00
Mr. OutisandRuslan Kuprieiev 8918bdc383 http: add exception when trying to traverse remote (#2411)
* http: add exception when trying to traverse remote

Fix #2380

* http: exception raise on `list_cache_paths` instead of `all`

`NotImplementedError` -> `RemoteActionNotImplemented`

* http: raise exception on `__init__`

* set default no_traverse to True on remote base

* tests: explicitly use traverse on data cloud and no traverse with HTTP

* remote: define a new RemoteConfigError

* remote: reuse ConfigError instead of RemoteConfigError

* tests: set no_traverse to False with SSHMocked

* deepsource: fix minor issue with spacing

* remove blankspace

Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2019-08-20 00:47:15 +03:00
Ruslan Kuprieiev 51d5a230da dvc: bump to 0.55.1 2019-08-19 16:01:16 +03:00
Casper da Costa-Luis 9b73ff72e9 avoid duplication
https://github.com/iterative/dvc/pull/2333#discussion_r315183224
2019-08-19 13:31:36 +01:00
Ruslan Kuprieiev f0c12ea162 Merge pull request #2407 from efiop/master
test: run: refactor signal handler tests
2019-08-19 15:22:25 +03:00
Ruslan Kuprieiev 89d3ccae0d test: run: refactor signal handler tests
Current test is testing for signal handler restoration in an obscure
way. As a result, the test wasn't actually testing anything, because
KeyboardInterrupt is raised by default_int_handler, and so raising it
in a mock won't be caught by signal handlers, because they are dealing
with real singnals.

Fix #2406

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-19 14:49:19 +03:00
Casper da Costa-Luis 8f7a7a55e6 fix logic 2019-08-19 11:59:25 +01:00
Casper da Costa-Luis bee90646a7 fix uncalled progress_callbacks 2019-08-19 11:39:55 +01:00
Ruslan Kuprieiev 239739d6e1 Merge pull request #2403 from gthb/no-dvc-checkout-on-git-file-checkout
fix: don't dvc checkout on git file checkout
2019-08-19 13:24:26 +03:00
Casper da Costa-Luis 1c8bf11238 Merge remote-tracking branch 'upstream/master' into tqdm 2019-08-19 11:15:13 +01:00
Ruslan Kuprieiev 30a7b77f0e stage: run: don't use unassigned old_handler
Fix #2406
2019-08-19 10:55:22 +03:00
Ruslan Kuprieiev 5a9dfbb6a4 config: use new remote url 2019-08-19 09:43:24 +03:00
Casper da Costa-Luis 6102caa309 hopefully temp fix for uncalled checkout progress_callback 2019-08-19 00:28:18 +01:00
Casper da Costa-Luis 13e76de5e9 suppress MD5 description prefix 2019-08-18 23:53:04 +01:00
Casper da Costa-Luis f90fd179b1 update tqdm version 2019-08-18 23:43:35 +01:00
Ruslan Kuprieiev 5b7cdd6bbf Merge pull request #2405 from efiop/master
dvc: remove unused code
2019-08-18 05:56:16 +03:00
Ruslan Kuprieiev 5915f17fcf output: base: remove unnecessary 'else'
Reported by DeepSource.
2019-08-18 05:15:08 +03:00
Ruslan Kuprieiev 875186d5a6 dvc: remove unused code
A bunch of leftovers from a very old pkg implementation.
2019-08-18 05:09:30 +03:00
Casper da Costa-Luis 9a2dea86c8 Tqdm longer descriptions 2019-08-18 00:19:49 +01:00
Casper da Costa-Luis 34dc163329 Tqdm auto leave=False for nested bars 2019-08-18 00:19:34 +01:00
Casper da Costa-Luis bd1fe67f99 probably correct oss2 Tqdm units (sparse Mandarin documentation) 2019-08-17 22:07:24 +01:00
Casper da Costa-Luis 49d4930c39 add Tqdm units 2019-08-17 22:05:48 +01:00
Casper da Costa-Luis 16113c880a update to bleeding-edge StreamHandler.emit
- https://github.com/iterative/dvc/pull/2333#discussion_r314950294
2019-08-17 16:52:47 +01:00
Casper da Costa-Luis 3098460f56 misc minor style fixes & optimisations 2019-08-17 16:36:59 +01:00
Casper da Costa-Luis 093ad20a84 flake8 unused imports 2019-08-17 16:15:15 +01:00
Casper da Costa-Luis ac4d03e66f remove auto-persistent bars
https://github.com/iterative/dvc/pull/2333/files#r314927686
2019-08-17 15:48:20 +01:00
Casper da Costa-Luis 534d6fdb0c remove defunct test, extraneous comments
- https://github.com/iterative/dvc/pull/2333#discussion_r314927966
2019-08-17 15:43:54 +01:00
Gunnlaugur Thor Briem e1dd75edcd fix: don't dvc checkout on git file checkout
Closes #2400
2019-08-16 22:11:16 +00:00
Casper da Costa-Luis 60566b380e minor test update 2019-08-16 22:33:00 +01:00
Casper da Costa-Luis adf05e6ff0 minor rename 2019-08-16 22:32:47 +01:00
Casper da Costa-Luis b018933b5a fix py2 MagicMock(sys.stderr).encoding in tests 2019-08-16 22:26:19 +01:00
Ruslan Kuprieiev d32515c969 Merge pull request #2393 from Suor/stream-more
api: stream api.open() for local, SSH and HDFS
2019-08-15 22:33:55 +03:00
Alexander Schepanovski 85289ad00b api: wrap not found errors
api.open()/read() will now raise OutputFileMissingError and
Remote*.open() will raise FileNotFoundError if target file is absent.
2019-08-15 23:20:48 +07:00
Ruslan Kuprieiev e1f676a191 dvc: bump to 0.55.0 2019-08-15 14:06:58 +03:00
Casper da Costa-Luis 8955a606c4 add .mailmap 2019-08-14 01:11:17 +01:00
Casper da Costa-Luis 39da3725f9 fix silly request header error 2019-08-14 00:56:20 +01:00
Casper da Costa-Luis 186f0cc483 a little tidy 2019-08-14 00:46:34 +01:00
Casper da Costa-Luis f88c2fe5fc remove old test 2019-08-14 00:19:44 +01:00
Casper da Costa-Luis c6f73fdc65 flake8 2019-08-13 23:47:44 +01:00
Casper da Costa-Luis d9b4ecfe7b better auto-closing TqdmThreadPoolExecutor 2019-08-13 23:46:20 +01:00
Casper da Costa-Luis a1f5c03683 tidy some ThreadPoolExecutor 2019-08-13 23:39:27 +01:00
Casper da Costa-Luis 3f8caeccc4 Merge remote-tracking branch 'upstream/master' into tqdm
major updates across dvc/remote/
2019-08-13 23:29:59 +01:00
Casper da Costa-Luis 3811141177 minor tidy 2019-08-13 22:59:12 +01:00
Casper da Costa-Luis 838151dc8f tidy & avoid excessive requests 2019-08-13 22:55:56 +01:00
Casper da Costa-Luis f373655cd5 minor potential kwargs bugfix 2019-08-13 22:49:51 +01:00
Ruslan Kuprieiev 5e4d26691a version: fix typo 2019-08-14 00:34:12 +03:00
Alexander Schepanovski 60a983b63f api: stream api.open() for local, SSH and HDFS 2019-08-13 18:19:52 +07:00
Ruslan Kuprieiev 7c19e9e4bf Merge pull request #2389 from Suor/streaming-clouds
api: stream data in api.open() for S3, GS and OSS
2019-08-13 11:30:41 +03:00
Alexander Schepanovski 641eb57b27 api: stream data in api.open() for S3, GS and OSS
Employ the same sign url and use resumable download approach tested for
Azure.
2019-08-13 14:09:51 +07:00
Ruslan Kuprieiev 3095b24ed4 Merge pull request #2375 from pared/2373
remote: gs/s3: remove batch_exists
2019-08-13 02:56:04 +03:00
Ruslan Kuprieiev 06d347178f remote: base: remove whitespace
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-13 02:25:15 +03:00
Ruslan Kuprieiev c308ad8a58 remote: base: don't forget about the progress bar
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-13 02:08:29 +03:00
pawel 8233f959c7 remote: ssh: reintroduce batch_exists 2019-08-12 14:10:16 +02:00
pawel f224790aab remote: base: remove unnecessary list casting 2019-08-12 11:01:02 +02:00
pared 394d6ffeeb remote: ssh: connection: remove open_max_sftp_channels 2019-08-12 11:01:02 +02:00
pared 132b571e72 remote: oss: fix _list_cache_args 2019-08-12 11:01:02 +02:00
pared 98de99f5a9 remote: azure: make blob_service cached property 2019-08-12 11:01:02 +02:00
pared 237f3e248b remote: base: modify cache_exists to use exists instead of batch_exists 2019-08-12 11:01:02 +02:00
pared 71abef74f5 remote: azure: implement batch_exists 2019-08-12 10:59:24 +02:00
Casper da Costa-Luis fbaadcb20f fix silly error 2019-08-12 01:50:04 +01:00
Casper da Costa-Luis f834e728e2 fix flake8 2019-08-12 00:18:30 +01:00
Casper da Costa-Luis 1d617459e4 ensure nested bars are cleared away 2019-08-12 00:05:37 +01:00
Casper da Costa-Luis 032d507ede persistent progress for large http files 2019-08-11 23:34:57 +01:00
Casper da Costa-Luis 56d1cca6bc log to correct stream 2019-08-11 23:23:16 +01:00
Casper da Costa-Luis 0de87af5fd progress-aware logging 2019-08-11 23:11:06 +01:00
Ruslan Kuprieiev a063f408a2 Merge pull request #2385 from efiop/azurite
travis: use azurite v3
2019-08-11 05:36:57 +03:00
Ruslan Kuprieiev 2f30bddfab travis: use azurite v3
Trying to fix azure error on our ci:

https://travis-ci.com/iterative/dvc/jobs/224085273

Workaround for #2387

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-11 05:04:25 +03:00
Casper da Costa-Luis 7328ff4866 update tqdm requirement 2019-08-11 01:41:01 +01:00
Casper da Costa-Luis 7665782d93 fix more callback usage 2019-08-11 01:40:22 +01:00
Casper da Costa-Luis ea8f4ba231 slight test update 2019-08-11 01:26:01 +01:00
Casper da Costa-Luis 120dfff05a naive test update 2019-08-11 01:21:50 +01:00
Casper da Costa-Luis ee4248df92 fix and update more tests 2019-08-11 01:17:42 +01:00
Ruslan Kuprieiev 71ab380d03 utils: remove: catch EPERM when chmoding a file
Fix https://github.com/iterative/dvc/pull/2298#issuecomment-520184196
2019-08-11 03:00:08 +03:00
Casper da Costa-Luis d0315a3509 minor black, test fixes 2019-08-11 00:09:18 +01:00
Casper da Costa-Luis 2f42db0eaa fix base progress_callback 2019-08-11 00:01:40 +01:00
Ruslan Kuprieiev b2f452ef04 Merge pull request #2319 from mroutis/fix-1497
metrics: allow multiple targets for show command
2019-08-10 20:10:31 +03:00
Mr. Outis a17caefd50 integrate @efiop 's suggestions 2019-08-10 11:37:35 -05:00
Casper da Costa-Luis 1925d6338b fix silly error 2019-08-10 17:18:12 +01:00
Casper da Costa-Luis 2ac99d87e5 update dvc.progress test stub 2019-08-10 17:18:12 +01:00
Casper da Costa-Luis 56db2ee5fe misc minor restructuring 2019-08-10 14:54:51 +01:00
Casper da Costa-Luis f88bf9218a tqdm auto-disabling with dynamic log level 2019-08-10 14:41:08 +01:00
Casper da Costa-Luis 9143f67ee0 black 2019-08-10 14:20:58 +01:00
Casper da Costa-Luis aed3451df9 pylint 2019-08-10 14:19:38 +01:00
Casper da Costa-Luis ca68eb4a4b add progress.Tqdm.update_desc(truncate=True) 2019-08-10 14:19:38 +01:00
Casper da Costa-Luis c02807deb3 tqdm remote.oss 2019-08-10 14:19:38 +01:00
Casper da Costa-Luis d6f816bc48 remove old progress
- progress.Progress
- progress.ProgressCallback
- progress.progress
2019-08-10 14:19:38 +01:00
Casper da Costa-Luis 82bb550e7d tqdm dvc.utils 2019-08-10 14:19:38 +01:00
Casper da Costa-Luis efd8a070ff tqdm remote.local 2019-08-10 14:19:38 +01:00
Casper da Costa-Luis bac3d9343b tqdm remote.ssh.connection 2019-08-10 14:17:49 +01:00
Casper da Costa-Luis 85d4f7ebb2 add progress.Tqdm(desc_truncate) 2019-08-10 14:17:49 +01:00
Casper da Costa-Luis f61e7d86a9 tqdm remote.azure 2019-08-10 14:17:49 +01:00
Casper da Costa-Luis d460d7e668 tqdm remote.s3, flake8 2019-08-10 14:15:16 +01:00
Casper da Costa-Luis 3f0b85cf15 add progress.Tqdm(truncate) 2019-08-10 14:15:16 +01:00
Casper da Costa-Luis 6aee6b6b16 add progress.Tqdm(bytes) 2019-08-10 14:15:16 +01:00
Casper da Costa-Luis 62cbeed8b3 tqdm remote.http 2019-08-10 14:15:16 +01:00
Casper da Costa-Luis cc281551eb tqdm repo.checkout 2019-08-10 14:15:16 +01:00
Casper da Costa-Luis c4b9d7dba7 minor bugfix 2019-08-10 14:15:16 +01:00
Casper da Costa-Luis 70ee31329f disable old progress 2019-08-10 14:15:16 +01:00
Casper da Costa-Luis 0ebce9c1e1 create dvc.progress.Tqdm, migrate dvc.remote.base 2019-08-10 14:15:16 +01:00
Casper da Costa-Luis 1511fd6dcc add preliminary tqdm work 2019-08-10 14:11:02 +01:00
Mr. Outis 163d872c92 fix tests for windows 2019-08-09 22:46:25 -05:00
Mr. Outis fa04c27580 add tests for recursive operations and file not found 2019-08-09 22:06:32 -05:00
Mr. Outis 0e46edd9bb fail silently but with a more sound error message
* Add a `_missing` key to the return value of `repo.metrics.show` to
include targets not recognized as metrics or not found.
2019-08-09 21:55:19 -05:00
Ruslan Kuprieiev 1b2823b5c5 Merge pull request #2384 from efiop/2361
remote: base: call makedirs in move
2019-08-10 05:30:16 +03:00
Ruslan Kuprieiev baa548eec9 remote: base: call makedirs in move
Remotes like s3/gs/etc don't need to create dirs in advance.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-10 04:53:50 +03:00
Ruslan Kuprieiev e9b1e7797e Merge pull request #2383 from shcheklein/update-doc-links
cli: update man links to the new scheme
2019-08-10 04:46:59 +03:00
Ruslan Kuprieiev f5a4227554 Merge pull request #2376 from efiop/2361
travis: speedup install time
2019-08-10 04:44:35 +03:00
Ruslan Kuprieiev 919a49a231 travis: windows: cache chocolatey downloads
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-10 04:16:11 +03:00
Ruslan Kuprieiev eb3b3f9f4e travis: windows: skip install stage
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-10 04:15:22 +03:00
Ruslan Kuprieiev 3f19036646 travis: osx: speedup install stage
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-10 04:15:22 +03:00
Ivan Shcheklein 6ae13b5795 cli: update man links to the new scheme 2019-08-09 17:54:26 -07:00
Ruslan Kuprieiev 60bcd3dc13 Merge pull request #2371 from Suor/streaming
api: make .open() stream data for Azure
2019-08-09 18:42:40 +03:00
Ruslan Kuprieiev deb3372a66 Merge pull request #2298 from efiop/marcelo
dvc: support `group` cache mode
2019-08-09 18:34:05 +03:00
Alexander Schepanovski e5e29065c8 api: make .open() stream data for Azure
This employs generating temporary signed public url and then streaming
from it. The bulk of the code might be reused for other remotes.

Url streaming does resume on connection error using "Range" header and
regenerates public url too.

If remote class doesn't provide .open() then this falls back to pulling
a file and opening it locally.
2019-08-09 21:51:43 +07:00
Ruslan Kuprieiev 2b36d25f49 remote: base: remove unused argument
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-09 17:10:40 +03:00
Ruslan Kuprieiev 9955f1c64f utils: merge isinstance checks
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-09 17:10:40 +03:00
Ruslan Kuprieiev 640e737570 remote: base: remove unused argument
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-09 17:10:40 +03:00
Ruslan Kuprieiev 24428e47c3 remote: local: cache_exists: match parameters from the BASE
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-09 17:10:40 +03:00
Ruslan Kuprieiev 713e288adc remote: base: checkout: remove unnecessary return
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-09 17:10:40 +03:00
Ruslan Kuprieiev b2fe3f7a71 utils: remove unnecessary else
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-09 17:10:40 +03:00
Ruslan Kuprieiev 9c5fd0c76c dvc: support group cache mode
In this mode, dvc will set appropriate file/dir permissions to support
sharing cache between different users within the same group.

Fixes #2287

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-09 17:10:40 +03:00
Ruslan Kuprieiev 5aae88b316 local: don't try to protect file if it is already protected
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-08-09 16:46:24 +03:00
Ruslan Kuprieiev f8673e79c6 requirements: update ruamel.yaml
Fixes #2334
2019-08-08 14:26:35 +03:00
Mr. Outis d4ef110794 exit with 1 if targets were not found 2019-08-06 21:10:48 -05:00
Ruslan Kuprieiev 17700fc4e8 Merge pull request #2353 from GildedHonour/issue1874
Fix #1874
2019-08-06 18:46:21 +03:00
alex maslakoff b78843881a fix 'diff cmd message' test 2019-08-06 13:51:30 +04:00
Mr. OutisandRuslan Kuprieiev 21a2d3c6ff remove no-op
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2019-08-05 15:30:28 -05:00
Mr. Outis 44f1cc6200 bring back debug message 2019-08-05 15:27:17 -05:00
Ruslan Kuprieiev 8ae01de0eb metrics: allow multiple targets for show command
Fix #1497
2019-08-05 10:19:11 -05:00
Ruslan Kuprieiev e0e02a9f6b dvc: bump to 0.54.1 2019-08-05 14:53:13 +03:00
Ruslan Kuprieiev 5ea53747a9 Merge pull request #2370 from MaxRis/azure-dep-version-upgrade
Update azure-storage-blob package (== 2.1.0)
2019-08-05 14:14:38 +03:00
Max Risuhin 078a5c063f Update azure-storage-blob package (== 2.1.0) 2019-08-05 01:27:19 -07:00
Ruslan Kuprieiev ab6d8f9f6f Merge pull request #2351 from RahulR19/master
Renamed git property to repo object in dvc/scm/git
2019-08-04 21:53:25 +03:00
RahulR19 371c57ac24 Renamed git to repo in dvc/scm/git along with changes in some tests. 2019-08-04 23:26:45 +05:30
Ruslan Kuprieiev 83ca54bc85 Merge pull request #2366 from SRITANU/issue_2253
dvc: Changes url description in get.py and imp.py
2019-08-04 04:31:11 +03:00
Sritanu Chakraborty ffa0c1caeb dvc: Changes url description in get.py and imp.py
Fixes #2253
2019-08-04 06:12:47 +05:30
alex maslakoff 619f53a555 remove unused imports 2019-08-03 20:31:28 +03:00
alex maslakoff 10fa6eddb1 fix 2 tests 2019-08-03 20:25:07 +03:00
Combalgorythm 353a32beb4 dvc:Changes UP_TO_DATE_MSG in status.py (#2364)
Fixes #2346
2019-08-03 18:55:25 +03:00
alex maslakoff 60359d8b98 merge 2 tests 2019-08-03 11:10:09 +03:00
alex maslakoff 81c8ed9da5 add a test for 'dir changes' 2019-08-02 16:09:10 +03:00
Ruslan Kuprieiev b4be9e8b78 Merge pull request #2343 from pared/2285_2
metrics show: extend NOTEs for show
2019-08-02 03:31:03 +03:00
alex maslakoff 433a43e5f0 reformat 2019-08-02 00:26:41 +03:00
alex maslakoff cf11cda846 fix wording for 'diff' cmd 2019-08-02 00:21:30 +03:00
pared 394b871452 metrics show: extend NOTEs for show 2019-08-01 11:40:02 +02:00
alex maslakoff 7347701145 add test for modified file content 2019-08-01 11:27:28 +03:00
RahulR19 79557ffb3c Renamed git property to repo object in dvc/scm/git 2019-07-31 21:56:53 +05:30
alex maslakoff dcfe6f34ce fix #1874 2019-07-31 16:07:20 +03:00
160 changed files with 4793 additions and 2611 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ test_patterns = [
]
exclude_patterns = [
'dvc/utils/compat.py'
]
[[analyzers]]
+1
View File
@@ -8,3 +8,4 @@
/cache
/pkg
/repos
/tmp
+1 -1
View File
@@ -1,4 +1,4 @@
['remote "dvc"']
url = https://dvc.org/s3/dvc
url = https://remote.dvc.org/dvc
[core]
remote = dvc
+1 -1
View File
@@ -1,5 +1,5 @@
* [ ] Have you followed the guidelines in our
[Contributing document](https://dvc.org/doc/user-guide/contributing)?
[Contributing document](https://dvc.org/doc/user-guide/contributing/core)?
* [ ] Does your PR affect documented changes or does it add new functionality
that should be documented? If yes, have you created a PR for
+1
View File
@@ -23,6 +23,7 @@ innosetup/config.ini
*.exe
.coverage
.coverage.*
*.swp
+6
View File
@@ -0,0 +1,6 @@
Paweł Redzyński <[email protected]>
Dmitry Petrov <[email protected]>
Earl Hathaway <[email protected]>
Nabanita Dash <[email protected]>
Kurian Benoy <[email protected]>
Sritanu Chakraborty <[email protected]>
+4 -2
View File
@@ -139,7 +139,9 @@ disable=print-statement,
deprecated-sys-function,
exception-escape,
comprehension-escape,
useless-object-inheritance
useless-object-inheritance,
missing-docstring,
bad-continuation
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
@@ -314,7 +316,7 @@ indent-after-paren=4
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
max-line-length=79
# Maximum number of lines in a module.
max-module-lines=1000
+43 -48
View File
@@ -1,20 +1,17 @@
language: python
cache:
directories:
- $HOME/.cache/pip
- /opt/python
- $HOME/.pyenv
- c/ProgramData/chocolatey/bin
- c/ProgramData/chocolatey/lib
sudo: true
dist: xenial
python: "3.7"
env:
global:
- HOMEBREW_NO_AUTO_UPDATE=1
- PYENV_ROOT="$HOME/.pyenv"
- PATH="$PYENV_ROOT/bin:$PATH"
- PIP_CACHE_DIR="$HOME/.cache/pip" # unify pip cache location for all platforms
stages:
- check
- test
- build
- name: deploy
@@ -22,93 +19,91 @@ stages:
# Travis doesn't support testing python on Mac yet, so we need
# to workaround it by installing it directly with brew.
#
# Note that we specify language as ruby-2.4.0 to help us satisfy
# Note that we specify language as ruby-2.4.3 to help us satisfy
# fpm dependencies, as travis runs el capitan with an old ruby
# version, that causes fpm to crash.
jobs:
include:
# patch formatting checks
- name: "Check patch formatting"
stage: "check"
os: linux
language: python
python: 3.7
before_install:
install:
script: ./scripts/ci/check_patch.sh
# test jobs
- name: "2.7 on Windows"
stage: test
os: windows # Windows 10.0.17134 N/A Build 17134
language: shell # 'language: python' is an error on Travis CI Windows
env:
- PYTHON_VER=2.7
- PY2=1
- name: "3.7 on Windows"
stage: test
os: windows # Windows 10.0.17134 N/A Build 17134
language: shell # 'language: python' is an error on Travis CI Windows
env:
- PYTHON_VER=3.7
- name: "2.7"
- name: "2.7 on Linux"
stage: test
language: python
python: 2.7
env:
- PYTHON_VER=2.7.13
- name: "3.5"
- PY2=1
- name: "3.5 on Linux"
stage: test
env:
- PYTHON_VER=3.5.3
- name: "3.6"
language: python
python: 3.5
- name: "3.6 on Linux"
stage: test
env:
- PYTHON_VER=3.6.2
- name: "3.7"
language: python
python: 3.6
- name: "3.7 on Linux"
stage: test
env:
# NOTE: using default python
- PYTHON_VER=
language: python
python: 3.7
- name: "2.7 on Mac"
stage: test
os: osx
osx_image: xcode7.3
language: ruby
rvm: 2.4.0
osx_image: xcode9.4
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"
- name: "3.7 on Mac"
- PY2=1
- name: "3.6 on Mac"
stage: test
os: osx
osx_image: xcode7.3
language: ruby
rvm: 2.4.0
env:
- PYTHON_VER=3.7.3
- PYTHON_CONFIGURE_OPTS="--enable-framework"
osx_image: xcode9.4
# build jobs
- name: "Mac pkg"
stage: build
os: osx
osx_image: xcode7.3
osx_image: xcode9.4
language: ruby
rvm: 2.4.0
env:
- PYTHON_VER=3.7.3
- PYTHON_CONFIGURE_OPTS="--enable-framework"
rvm: 2.4.3
install:
script: ./scripts/build_posix.sh
- name: "Windows pkg"
stage: build
os: windows
language: shell
env:
- PYTHON_VER=3.7
install:
script: ./scripts/build_windows.cmd
- name: "Linux pkgs"
stage: build
env:
# NOTE: using default python
- PYTHON_VER=
language: python
python: 3.7
before_install:
install:
script: ./scripts/build_posix.sh
- name: "Pypi pkgs"
stage: build
env:
# NOTE: using default python
- PYTHON_VER=
language: python
python: 3.7
script: ./scripts/build_package.sh
# deploy jobs
- name: "Pypi pkgs"
stage: deploy
language: python
python: 3.7
script: ./scripts/build_package.sh
deploy:
- provider: pypi
+1 -1
View File
@@ -1 +1 @@
### See our contribution guide at [dvc.org](https://dvc.org/doc/user-guide/contributing#contributing).
### See our contribution guide at [dvc.org](https://dvc.org/doc/user-guide/contributing/core).
+2
View File
@@ -0,0 +1,2 @@
include fastentrypoints.py
include LICENSE
+8 -9
View File
@@ -26,6 +26,10 @@
:target: https://www.patreon.com/DVCorg/overview
:alt: Donate
.. image:: https://anaconda.org/conda-forge/dvc/badges/version.svg
:target: https://anaconda.org/conda-forge/dvc
:alt: Conda-forge
|
**Data Version Control** or **DVC** is an **open-source** tool for data science and machine
@@ -42,7 +46,7 @@ learning projects. Key features:
#. 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
**DVC** aims to replace tools like Excel and Google Docs that are being commonly used as a knowledge repo and
a ledger for the team, ad-hoc scripts to track and move deploy different model versions, ad-hoc
data file suffixes and prefixes.
@@ -115,7 +119,7 @@ pip (PyPI)
pip install dvc
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.
one of the optional dependencies: ``s3``, ``gs``, ``azure``, ``oss``, ``ssh``. Or ``all`` 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.
@@ -130,14 +134,9 @@ Homebrew
.. code-block:: bash
brew install iterative/homebrew-dvc/dvc
brew install dvc
or:
.. code-block:: bash
brew cask install iterative/homebrew-dvc/dvc
Conda (Anaconda)
----------------
@@ -198,7 +197,7 @@ Related technologies
Contributing
============
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/>`_ for more
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more
details.
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/0
+3 -1
View File
@@ -6,10 +6,12 @@ Make your data science projects reproducible and shareable.
from __future__ import unicode_literals
from dvc.version import __version__ # noqa: F401
import warnings
import dvc.logger
from dvc.version import __version__ # noqa: F401
dvc.logger.setup()
+4 -3
View File
@@ -10,6 +10,7 @@ import errno
import logging
from dvc import __version__
from dvc.utils import env2bool
logger = logging.getLogger(__name__)
@@ -66,7 +67,7 @@ class Analytics(object):
raise
self.user_id_file = os.path.join(cdir, self.USER_ID_FILE)
self.user_id_file_lock = Lock(cdir, self.USER_ID_FILE + ".lock")
self.user_id_file_lock = Lock(self.user_id_file + ".lock")
@staticmethod
def load(path):
@@ -113,7 +114,7 @@ class Analytics(object):
return user_id
except LockError:
msg = "Failed to acquire '{}'"
logger.debug(msg.format(self.user_id_file_lock.lock_file))
logger.debug(msg.format(self.user_id_file_lock.lockfile))
def _collect_windows(self):
import sys
@@ -229,7 +230,7 @@ class Analytics(object):
def is_enabled(cmd=None):
from dvc.command.daemon import CmdDaemonBase
if os.getenv("DVC_TEST"):
if env2bool("DVC_TEST"):
return False
if isinstance(cmd, CmdDaemonBase):
+72 -37
View File
@@ -3,7 +3,10 @@
from __future__ import unicode_literals
import os
from collections import defaultdict
from funcy import cached_property
from dvc.utils.compat import builtin_str
from dvc.config import Config
@@ -21,6 +24,42 @@ class CacheConfig(object):
)
def _make_remote_property(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 creates a cached property, containing cache named `name`:
self.config == {'ssh': 'dvc-storage'}
self.ssh # a RemoteSSH instance
"""
def getter(self):
from dvc.remote import Remote
remote = self.config.get(name)
if not remote:
return None
return Remote(self.repo, name=remote)
getter.__name__ = builtin_str(name)
return cached_property(getter)
class Cache(object):
"""Class that manages cache locations of a dvc repo.
@@ -35,7 +74,7 @@ class Cache(object):
self.repo = repo
config = repo.config.config[Config.SECTION_CACHE]
self.config = config = repo.config.config[Config.SECTION_CACHE]
local = config.get(Config.SECTION_CACHE_LOCAL)
if local:
@@ -46,6 +85,7 @@ class Cache(object):
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)
shared = config.get(Config.SECTION_CACHE_SHARED)
settings = {
Config.PRIVATE_CWD: config.get(
@@ -54,51 +94,46 @@ class Cache(object):
Config.SECTION_REMOTE_URL: cache_dir,
Config.SECTION_CACHE_TYPE: cache_type,
Config.SECTION_CACHE_PROTECTED: protected,
Config.SECTION_CACHE_SHARED: shared,
}
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.
s3 = _make_remote_property(Config.SECTION_CACHE_S3)
gs = _make_remote_property(Config.SECTION_CACHE_GS)
ssh = _make_remote_property(Config.SECTION_CACHE_SSH)
hdfs = _make_remote_property(Config.SECTION_CACHE_HDFS)
azure = _make_remote_property(Config.SECTION_CACHE_AZURE)
This is needed when specifying external outputs
(as they require you to have an external cache location).
Imagine a config file like the following:
class NamedCache(object):
def __init__(self):
self._items = defaultdict(lambda: defaultdict(set))
self.external = defaultdict(set)
['remote "dvc-storage"']
url = ssh://localhost/tmp
ask_password = true
@classmethod
def make(cls, scheme, checksum, name):
cache = cls()
cache.add(scheme, checksum, name)
return cache
[cache]
ssh = dvc-storage
def __getitem__(self, key):
return self._items[key]
This method resolves the name under the cache section into the
correct Remote instance.
def add(self, scheme, checksum, name):
self._items[scheme][checksum].add(name)
Args:
config (dict): The cache section on the config file
name (str): Name of the section we are interested in to retrieve
def add_external(self, url, rev, path):
self.external[url, rev].add(path)
Returns:
remote (dvc.Remote): Remote instance that the section is referring.
None when there's no remote with that name.
def update(self, cache, suffix=""):
for scheme, src in cache._items.items():
dst = self._items[scheme]
for checksum, names in src.items():
if suffix:
dst[checksum].update(n + suffix for n in names)
else:
dst[checksum].update(names)
Example:
>>> _get_remote(config={'ssh': 'dvc-storage'}, name='ssh')
"""
from dvc.remote import Remote
remote = config.get(name)
if not remote:
return None
return Remote(self.repo, name=remote)
for repo_pair, files in cache.external.items():
self.external[repo_pair].update(files)
+13
View File
@@ -144,6 +144,19 @@ def parse_args(argv=None):
description=desc,
parents=[parent_parser],
formatter_class=argparse.RawTextHelpFormatter,
add_help=False,
)
# NOTE: We are doing this to capitalize help message.
# Unfortunately, there is no easier and clearer way to do it,
# as adding this argument in get_parent_parser() either in
# log_level_group or on parent_parser itself will cause unexpected error.
parser.add_argument(
"-h",
"--help",
action="help",
default=argparse.SUPPRESS,
help="Show this help message and exit.",
)
# NOTE: On some python versions action='version' prints to stderr
+20 -18
View File
@@ -5,6 +5,7 @@ import logging
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import RecursiveAddingWhileUsingFilename
logger = logging.getLogger(__name__)
@@ -14,49 +15,50 @@ class CmdAdd(CmdBase):
def run(self):
try:
if len(self.args.targets) > 1 and self.args.file:
raise DvcException("can't use '--file' with multiple targets")
raise RecursiveAddingWhileUsingFilename()
for target in self.args.targets:
self.repo.add(
target,
recursive=self.args.recursive,
no_commit=self.args.no_commit,
fname=self.args.file,
)
self.repo.add(
self.args.targets,
recursive=self.args.recursive,
no_commit=self.args.no_commit,
fname=self.args.file,
)
except DvcException:
logger.exception("failed to add file")
logger.exception("")
return 1
return 0
def add_parser(subparsers, parent_parser):
ADD_HELP = "Take data files or directories under DVC control."
ADD_HELP = "Track data files or directories with DVC."
add_parser = subparsers.add_parser(
parser = subparsers.add_parser(
"add",
parents=[parent_parser],
description=append_doc_link(ADD_HELP, "add"),
help=ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_parser.add_argument(
parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Recursively add each file under the directory.",
help="Recursively add files under directory targets.",
)
add_parser.add_argument(
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."
parser.add_argument(
"-f",
"--file",
help="Specify name of the DVC-file this command will generate.",
)
add_parser.add_argument(
parser.add_argument(
"targets", nargs="+", help="Input files/directories to add."
)
add_parser.set_defaults(func=CmdAdd)
parser.set_defaults(func=CmdAdd)
+2 -15
View File
@@ -26,7 +26,7 @@ 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(
return "{message}\nDocumentation: <{blue}{base}{path}{nc}>".format(
message=help_message,
base=doc_base,
path=path,
@@ -55,24 +55,11 @@ class CmdBase(object):
logger.warning(msg)
return [Stage.STAGE_FILE]
def run_cmd(self):
from dvc.lock import LockError
try:
with self.repo.lock:
return self.run()
except LockError:
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
raise NotImplementedError
class CmdBaseNoRepo(CmdBase):
def __init__(self, args):
self.args = args
def run_cmd(self):
return self.run()
+1 -1
View File
@@ -44,7 +44,7 @@ def add_parser(subparsers, parent_parser):
cache_dir_parser = cache_subparsers.add_parser(
"dir",
parents=[parent_parser, parent_cache_config_parser],
description=append_doc_link(CACHE_HELP, "cache-dir"),
description=append_doc_link(CACHE_HELP, "cache/dir"),
help=CACHE_DIR_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
+6 -10
View File
@@ -7,16 +7,12 @@ from dvc.command.base import CmdBase, append_doc_link
class CmdCheckout(CmdBase):
def run(self):
if not self.args.targets:
self.repo.checkout(force=self.args.force)
else:
for target in self.args.targets:
self.repo.checkout(
target=target,
with_deps=self.args.with_deps,
force=self.args.force,
recursive=self.args.recursive,
)
self.repo.checkout(
targets=self.args.targets,
with_deps=self.args.with_deps,
force=self.args.force,
recursive=self.args.recursive,
)
return 0
+2 -2
View File
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
class CmdDestroy(CmdBase):
def run_cmd(self):
def run(self):
try:
statement = (
"This will destroy all information about your pipelines,"
@@ -24,7 +24,7 @@ class CmdDestroy(CmdBase):
if not self.args.force and not prompt.confirm(statement):
raise DvcException(
"cannot destroy without a confirmation from the user."
" Use '-f' to force."
" Use `-f` to force."
)
self.repo.destroy()
+9 -2
View File
@@ -9,7 +9,6 @@ from funcy import compact
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, append_doc_link
import dvc.repo.diff as diff
logger = logging.getLogger(__name__)
@@ -37,9 +36,11 @@ class CmdDiff(CmdBase):
@classmethod
def _get_dir_changes(cls, dct):
import dvc.repo.diff as diff
engine = inflect.engine()
changes_msg = (
"{} {} not changed, {} {} modified, {} {} added, "
"{} {} untouched, {} {} modified, {} {} added, "
"{} {} deleted, size was {}"
)
changes_msg = changes_msg.format(
@@ -57,6 +58,8 @@ class CmdDiff(CmdBase):
@classmethod
def _get_file_changes(cls, dct):
import dvc.repo.diff as diff
if (
dct.get(diff.DIFF_OLD_FILE)
and dct.get(diff.DIFF_NEW_FILE)
@@ -79,6 +82,8 @@ class CmdDiff(CmdBase):
@classmethod
def _get_royal_changes(cls, dct):
import dvc.repo.diff as diff
if dct[diff.DIFF_SIZE] != diff.DIFF_SIZE_UNKNOWN:
if dct.get("is_dir"):
return cls._get_dir_changes(dct)
@@ -88,6 +93,8 @@ class CmdDiff(CmdBase):
@classmethod
def _show(cls, diff_dct):
import dvc.repo.diff as diff
msg = "dvc diff from {} to {}".format(
diff_dct[diff.DIFF_A_REF], diff_dct[diff.DIFF_B_REF]
)
+19 -10
View File
@@ -13,17 +13,19 @@ logger = logging.getLogger(__name__)
class CmdGC(CmdBase):
def run(self):
msg = "this will remove all cache except the cache that is used in "
if not self.args.all_branches and not self.args.all_tags:
msg += "the current git branch"
elif self.args.all_branches and not self.args.all_tags:
msg += "all git branches"
elif not self.args.all_branches and self.args.all_tags:
msg += "all git tags"
else:
msg += "all git branches and all git tags"
msg = "This will remove all cache except items used in "
if self.args.repos is not None and len(self.args.repos) > 0:
msg += "the working tree"
if self.args.all_commits:
msg += " and all git commits"
elif self.args.all_branches and self.args.all_tags:
msg += " and all git branches and tags"
elif self.args.all_branches:
msg += " and all git branches"
elif self.args.all_tags:
msg += " and all git tags"
if self.args.repos:
msg += " of the current and the following repos:"
for repo_path in self.args.repos:
@@ -40,6 +42,7 @@ class CmdGC(CmdBase):
self.repo.gc(
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
cloud=self.args.cloud,
remote=self.args.remote,
force=self.args.force,
@@ -76,6 +79,12 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Keep data files for all git tags.",
)
gc_parser.add_argument(
"--all-commits",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
gc_parser.add_argument(
"-c",
"--cloud",
+3 -2
View File
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
import argparse
import logging
from dvc.repo import Repo
from dvc.exceptions import DvcException
from .base import CmdBaseNoRepo, append_doc_link
@@ -13,6 +12,8 @@ logger = logging.getLogger(__name__)
class CmdGet(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
try:
Repo.get(
self.args.url,
@@ -40,7 +41,7 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
get_parser.add_argument(
"url", help="DVC repository URL to download data from."
"url", help="URL of Git repository with DVC project to download from."
)
get_parser.add_argument("path", help="Path to data within DVC repository.")
get_parser.add_argument(
+2 -1
View File
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
import argparse
import logging
from dvc.repo import Repo
from dvc.exceptions import DvcException
from .base import CmdBaseNoRepo, append_doc_link
@@ -13,6 +12,8 @@ logger = logging.getLogger(__name__)
class CmdGetUrl(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
try:
Repo.get_url(self.args.url, out=self.args.out)
return 0
+3 -1
View File
@@ -41,7 +41,9 @@ def add_parser(subparsers, parent_parser):
help=IMPORT_HELP,
formatter_class=argparse.RawTextHelpFormatter,
)
import_parser.add_argument("url", help="DVC repository URL.")
import_parser.add_argument(
"url", help="URL of Git repository with DVC project to download from."
)
import_parser.add_argument(
"path", help="Path to data within DVC repository."
)
+1 -1
View File
@@ -19,7 +19,7 @@ class CmdImportUrl(CmdBase):
except DvcException:
logger.exception(
"failed to import {}. You could also try downloading "
"it manually and adding it with `dvc add` command.".format(
"it manually, and adding it with `dvc add`.".format(
self.args.url
)
)
+1 -1
View File
@@ -53,7 +53,7 @@ def add_parser(subparsers, parent_parser):
action="store_true",
default=False,
help=(
"Overwrite existing '.dvc' directory. "
"Overwrite existing '.dvc/' directory. "
"This operation removes local cache."
),
)
+1 -1
View File
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
class CmdInstall(CmdBase):
def run_cmd(self):
def run(self):
try:
self.repo.install()
except Exception:
+21 -13
View File
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import argparse
import logging
from dvc.exceptions import DvcException
from dvc.exceptions import DvcException, BadMetricError
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
@@ -16,6 +16,10 @@ def show_metrics(metrics, all_branches=False, all_tags=False):
metrics (list): Where each element is either a `list`
if an xpath was specified, otherwise a `str`
"""
# When `metrics` contains a `None` key, it means that some files
# specified as `targets` in `repo.metrics.show` didn't contain any metrics.
missing = metrics.pop(None, None)
for branch, val in metrics.items():
if all_branches or all_tags:
logger.info("{branch}:".format(branch=branch))
@@ -32,16 +36,17 @@ def show_metrics(metrics, all_branches=False, all_tags=False):
else:
logger.info("\t{}: {}".format(fname, metric))
if missing:
raise BadMetricError(missing)
class CmdMetricsShow(CmdBase):
def run(self):
typ = self.args.type
xpath = self.args.xpath
try:
metrics = self.repo.metrics.show(
self.args.path,
typ=typ,
xpath=xpath,
self.args.targets,
typ=self.args.type,
xpath=self.args.xpath,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
recursive=self.args.recursive,
@@ -116,12 +121,15 @@ def add_parser(subparsers, parent_parser):
metrics_show_parser = metrics_subparsers.add_parser(
"show",
parents=[parent_parser],
description=append_doc_link(METRICS_SHOW_HELP, "metrics-show"),
description=append_doc_link(METRICS_SHOW_HELP, "metrics/show"),
help=METRICS_SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_show_parser.add_argument(
"path", nargs="?", help="Path to a metric file or a directory."
"targets",
nargs="*",
help="Metric files or directories (see -R) to show "
"(leave empty to display all)",
)
metrics_show_parser.add_argument(
"-t",
@@ -155,8 +163,8 @@ def add_parser(subparsers, parent_parser):
action="store_true",
default=False,
help=(
"If path is a directory, recursively search and process metric "
"files in path."
"If any target is a directory, recursively search and process "
"metric files."
),
)
metrics_show_parser.set_defaults(func=CmdMetricsShow)
@@ -165,7 +173,7 @@ def add_parser(subparsers, parent_parser):
metrics_add_parser = metrics_subparsers.add_parser(
"add",
parents=[parent_parser],
description=append_doc_link(METRICS_ADD_HELP, "metrics-add"),
description=append_doc_link(METRICS_ADD_HELP, "metrics/add"),
help=METRICS_ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -182,7 +190,7 @@ def add_parser(subparsers, parent_parser):
metrics_modify_parser = metrics_subparsers.add_parser(
"modify",
parents=[parent_parser],
description=append_doc_link(METRICS_MODIFY_HELP, "metrics-modify"),
description=append_doc_link(METRICS_MODIFY_HELP, "metrics/modify"),
help=METRICS_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -199,7 +207,7 @@ def add_parser(subparsers, parent_parser):
metrics_remove_parser = metrics_subparsers.add_parser(
"remove",
parents=[parent_parser],
description=append_doc_link(METRICS_REMOVE_HELP, "metrics-remove"),
description=append_doc_link(METRICS_REMOVE_HELP, "metrics/remove"),
help=METRICS_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
+7 -11
View File
@@ -19,7 +19,7 @@ class CmdPipelineShow(CmdBase):
from dvc.stage import Stage
stage = Stage.load(self.repo, target)
G = self.repo.graph()[0]
G = self.repo.graph
stages = networkx.get_node_attributes(G, "stage")
node = relpath(stage.path, self.repo.root_dir)
nodes = networkx.dfs_postorder_nodes(G, node)
@@ -41,20 +41,16 @@ class CmdPipelineShow(CmdBase):
def __build_graph(self, target, commands, outs):
import networkx
from dvc.stage import Stage
from dvc.repo.graph import get_pipeline
stage = Stage.load(self.repo, target)
node = relpath(stage.path, self.repo.root_dir)
pipelines = list(
filter(lambda g: node in g.nodes(), self.repo.pipelines())
)
assert len(pipelines) == 1
G = pipelines[0]
G = get_pipeline(self.repo.pipelines, node)
stages = networkx.get_node_attributes(G, "stage")
nodes = []
for n in G.nodes():
for n in G:
stage = stages[n]
if commands:
if stage.cmd is None:
@@ -168,7 +164,7 @@ class CmdPipelineList(CmdBase):
def run(self):
import networkx
pipelines = self.repo.pipelines()
pipelines = self.repo.pipelines
for p in pipelines:
stages = networkx.get_node_attributes(p, "stage")
for stage in stages:
@@ -201,7 +197,7 @@ def add_parser(subparsers, parent_parser):
pipeline_show_parser = pipeline_subparsers.add_parser(
"show",
parents=[parent_parser],
description=append_doc_link(PIPELINE_SHOW_HELP, "pipeline-show"),
description=append_doc_link(PIPELINE_SHOW_HELP, "pipeline/show"),
help=PIPELINE_SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -257,7 +253,7 @@ def add_parser(subparsers, parent_parser):
pipeline_list_parser = pipeline_subparsers.add_parser(
"list",
parents=[parent_parser],
description=append_doc_link(PIPELINE_LIST_HELP, "pipeline-list"),
description=append_doc_link(PIPELINE_LIST_HELP, "pipeline/list"),
help=PIPELINE_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
+5 -5
View File
@@ -85,7 +85,7 @@ def add_parser(subparsers, parent_parser):
remote_add_parser = remote_subparsers.add_parser(
"add",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_ADD_HELP, "remote-add"),
description=append_doc_link(REMOTE_ADD_HELP, "remote/add"),
help=REMOTE_ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -114,7 +114,7 @@ def add_parser(subparsers, parent_parser):
remote_default_parser = remote_subparsers.add_parser(
"default",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_DEFAULT_HELP, "remote-default"),
description=append_doc_link(REMOTE_DEFAULT_HELP, "remote/default"),
help=REMOTE_DEFAULT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -134,7 +134,7 @@ def add_parser(subparsers, parent_parser):
remote_remove_parser = remote_subparsers.add_parser(
"remove",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_REMOVE_HELP, "remote-remove"),
description=append_doc_link(REMOTE_REMOVE_HELP, "remote/remove"),
help=REMOTE_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -147,7 +147,7 @@ def add_parser(subparsers, parent_parser):
remote_modify_parser = remote_subparsers.add_parser(
"modify",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_MODIFY_HELP, "remote-modify"),
description=append_doc_link(REMOTE_MODIFY_HELP, "remote/modify"),
help=REMOTE_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
@@ -171,7 +171,7 @@ def add_parser(subparsers, parent_parser):
remote_list_parser = remote_subparsers.add_parser(
"list",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_LIST_HELP, "remote-list"),
description=append_doc_link(REMOTE_LIST_HELP, "remote/list"),
help=REMOTE_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
+2 -5
View File
@@ -28,7 +28,7 @@ class CmdRemove(CmdBase):
raise DvcException(
"Cannot purge without a confirmation from the user."
" Use '-f' to force."
" Use `-f` to force."
)
def run(self):
@@ -74,9 +74,6 @@ def add_parser(subparsers, parent_parser):
help="Force purge.",
)
remove_parser.add_argument(
"targets",
nargs="+",
help="DVC-files to remove. Optional. "
"(Finds all DVC-files in the workspace by default.)",
"targets", nargs="+", help="DVC-files to remove."
)
remove_parser.set_defaults(func=CmdRemove)
+2 -1
View File
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
import argparse
import logging
from dvc.repo import Repo
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.utils import relpath
@@ -12,6 +11,8 @@ logger = logging.getLogger(__name__)
class CmdRoot(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
logger.info(relpath(Repo.find_root()))
return 0
+10 -3
View File
@@ -27,9 +27,9 @@ class CmdRun(CmdBase):
]
): # pragma: no cover
logger.error(
"too few arguments. Specify at least one: '-d', '-o', '-O',"
" '-m', '-M', '--outs-persist', '--outs-persist-no-cache',"
" 'command'."
"too few arguments. Specify at least one: `-d`, `-o`, `-O`, "
"`-m`, `-M`, `--outs-persist`, `--outs-persist-no-cache`, "
"`command`."
)
return 1
@@ -51,6 +51,7 @@ class CmdRun(CmdBase):
no_commit=self.args.no_commit,
outs_persist=self.args.outs_persist,
outs_persist_no_cache=self.args.outs_persist_no_cache,
always_changed=self.args.always_changed,
)
except DvcException:
logger.exception("failed to run command")
@@ -189,6 +190,12 @@ def add_parser(subparsers, parent_parser):
help="Declare output file or directory that will not be "
"removed upon repro (do not put into DVC cache).",
)
run_parser.add_argument(
"--always-changed",
action="store_true",
default=False,
help="Always consider this DVC-file as changed.",
)
run_parser.add_argument(
"command", nargs=argparse.REMAINDER, help="Command to execute."
)
+1 -1
View File
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
class CmdDataStatus(CmdDataBase):
STATUS_LEN = 20
STATUS_INDENT = "\t"
UP_TO_DATE_MSG = "Pipelines are up to date. Nothing to reproduce."
UP_TO_DATE_MSG = "Data and pipelines are up to date."
def _normalize(self, s):
s += ":"
+26 -8
View File
@@ -5,15 +5,15 @@ import platform
import argparse
import logging
import uuid
import itertools
try:
import psutil
except ImportError:
psutil = None
from dvc.utils import is_binary
from dvc.utils import is_binary, relpath
from dvc.utils.compat import pathlib
from dvc.repo import Repo
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.version import __version__
from dvc.exceptions import DvcException, NotDvcRepoError
@@ -25,6 +25,8 @@ logger = logging.getLogger(__name__)
class CmdVersion(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
dvc_version = __version__
python_version = platform.python_version()
platform_type = platform.platform()
@@ -46,9 +48,22 @@ class CmdVersion(CmdBaseNoRepo):
repo = Repo()
root_directory = repo.root_dir
info += "Cache: {cache}\n".format(
cache=self.get_linktype_support_info(repo)
)
# cache_dir might not exist yet (e.g. after `dvc init`), and we
# can't auto-create it, as it might cause issues if the user
# later decides to enable shared cache mode with
# `dvc config cache.shared group`.
if os.path.exists(repo.cache.local.cache_dir):
info += "Cache: {cache}\n".format(
cache=self.get_linktype_support_info(repo)
)
else:
logger.warning(
"Unable to detect supported link types, as cache "
"directory '{}' doesn't exist. It is usually auto-created "
"by commands such as `dvc add/fetch/pull/run/import`, "
"but you could create it manually to enable this "
"check.".format(relpath(repo.cache.local.cache_dir))
)
if psutil:
info += (
@@ -69,9 +84,12 @@ class CmdVersion(CmdBaseNoRepo):
def get_fs_type(path):
partition = {
pathlib.Path(part.mountpoint): (part.fstype, part.device)
for part in psutil.disk_partitions()
for part in psutil.disk_partitions(all=True)
}
for parent in pathlib.Path(path).parents:
path = pathlib.Path(path)
for parent in itertools.chain([path], path.parents):
if parent in partition:
return partition[parent]
return ("unkown", "none")
@@ -109,7 +127,7 @@ class CmdVersion(CmdBaseNoRepo):
def add_parser(subparsers, parent_parser):
VERSION_HELP = "Show DVC version and system/environment informaion."
VERSION_HELP = "Show DVC version and system/environment information."
version_parser = subparsers.add_parser(
"version",
+20
View File
@@ -31,6 +31,18 @@ class ConfigError(DvcException):
)
class NoRemoteError(ConfigError):
def __init__(self, command, cause=None):
msg = (
"no remote specified. Setup default remote with\n"
" dvc config core.remote <name>\n"
"or use:\n"
" dvc {} -r <name>\n".format(command)
)
super(NoRemoteError, self).__init__(msg, cause=cause)
def supported_cache_type(types):
"""Checks if link type config option has a valid value.
@@ -160,6 +172,8 @@ class Config(object): # pylint: disable=too-many-instance-attributes
SECTION_CACHE_TYPE = "type"
SECTION_CACHE_TYPE_SCHEMA = supported_cache_type
SECTION_CACHE_PROTECTED = "protected"
SECTION_CACHE_SHARED = "shared"
SECTION_CACHE_SHARED_SCHEMA = And(Use(str.lower), Choices("group"))
SECTION_CACHE_LOCAL = "local"
SECTION_CACHE_S3 = "s3"
SECTION_CACHE_GS = "gs"
@@ -177,6 +191,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_CACHE_DIR): str,
Optional(SECTION_CACHE_TYPE, default=None): SECTION_CACHE_TYPE_SCHEMA,
Optional(SECTION_CACHE_PROTECTED, default=False): BOOL_SCHEMA,
Optional(SECTION_CACHE_SHARED): SECTION_CACHE_SHARED_SCHEMA,
Optional(PRIVATE_CWD): str,
Optional(SECTION_CACHE_SLOW_LINK_WARNING, default=True): BOOL_SCHEMA,
}
@@ -207,6 +222,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
SECTION_AWS_PROFILE = "profile"
SECTION_AWS_USE_SSL = "use_ssl"
SECTION_AWS_SSE = "sse"
SECTION_AWS_ACL = "acl"
SECTION_AWS_SCHEMA = {
SECTION_AWS_STORAGEPATH: str,
Optional(SECTION_AWS_REGION): str,
@@ -216,6 +232,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_AWS_LIST_OBJECTS, default=False): BOOL_SCHEMA,
Optional(SECTION_AWS_USE_SSL, default=True): BOOL_SCHEMA,
Optional(SECTION_AWS_SSE): str,
Optional(SECTION_AWS_ACL): str,
}
# backward compatibility
@@ -248,6 +265,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
SECTION_REMOTE_TIMEOUT = "timeout"
SECTION_REMOTE_PASSWORD = "password"
SECTION_REMOTE_ASK_PASSWORD = "ask_password"
SECTION_REMOTE_GSS_AUTH = "gss_auth"
SECTION_REMOTE_NO_TRAVERSE = "no_traverse"
SECTION_REMOTE_SCHEMA = {
SECTION_REMOTE_URL: str,
@@ -258,6 +276,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_AWS_LIST_OBJECTS, default=False): BOOL_SCHEMA,
Optional(SECTION_AWS_USE_SSL, default=True): BOOL_SCHEMA,
Optional(SECTION_AWS_SSE): str,
Optional(SECTION_AWS_ACL): str,
Optional(SECTION_GCP_PROJECTNAME): str,
Optional(SECTION_CACHE_TYPE): SECTION_CACHE_TYPE_SCHEMA,
Optional(SECTION_CACHE_PROTECTED, default=False): BOOL_SCHEMA,
@@ -267,6 +286,7 @@ class Config(object): # pylint: disable=too-many-instance-attributes
Optional(SECTION_REMOTE_TIMEOUT): Use(int),
Optional(SECTION_REMOTE_PASSWORD): str,
Optional(SECTION_REMOTE_ASK_PASSWORD): BOOL_SCHEMA,
Optional(SECTION_REMOTE_GSS_AUTH): BOOL_SCHEMA,
Optional(SECTION_AZURE_CONNECTION_STRING): str,
Optional(SECTION_OSS_ACCESS_KEY_ID): str,
Optional(SECTION_OSS_ACCESS_KEY_SECRET): str,
+1 -1
View File
@@ -94,7 +94,7 @@ def daemon(args):
cmd = [sys.executable]
if not is_binary():
cmd += ["-m", "dvc"]
cmd += [sys.argv[0]]
cmd += ["daemon", "-q"] + args
env = fix_env()
+12 -37
View File
@@ -4,16 +4,8 @@ from __future__ import unicode_literals
import logging
from dvc.config import Config, ConfigError
from dvc.config import Config, NoRemoteError
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__)
@@ -30,18 +22,6 @@ class DataCloud(object):
config.ConfigError: thrown when config has invalid format.
"""
CLOUD_MAP = {
"aws": RemoteS3,
"gcp": RemoteGS,
"azure": RemoteAZURE,
"oss": RemoteOSS,
"ssh": RemoteSSH,
"hdfs": RemoteHDFS,
"local": RemoteLOCAL,
"http": RemoteHTTP,
"https": RemoteHTTP,
}
def __init__(self, repo):
self.repo = repo
@@ -60,21 +40,16 @@ class DataCloud(object):
if remote:
return self._init_remote(remote)
raise ConfigError(
"No remote repository specified. Setup default repository with\n"
" dvc config core.remote <name>\n"
"or use:\n"
" dvc {} -r <name>\n".format(command)
)
raise NoRemoteError(command)
def _init_remote(self, remote):
return Remote(self.repo, name=remote)
def push(self, targets, jobs=None, remote=None, show_checksums=False):
def push(self, cache, jobs=None, remote=None, show_checksums=False):
"""Push data items in a cloud-agnostic way.
Args:
targets (list): list of targets to push to the cloud.
cache (NamedCache): named checksums to push to the cloud.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.RemoteBASE): optional remote to push to.
By default remote from core.remote config option is used.
@@ -82,17 +57,17 @@ class DataCloud(object):
information messages.
"""
return self.repo.cache.local.push(
targets,
cache,
jobs=jobs,
remote=self.get_remote(remote, "push"),
show_checksums=show_checksums,
)
def pull(self, targets, jobs=None, remote=None, show_checksums=False):
def pull(self, cache, jobs=None, remote=None, show_checksums=False):
"""Pull data items in a cloud-agnostic way.
Args:
targets (list): list of targets to pull from the cloud.
cache (NamedCache): named checksums to pull from the cloud.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.RemoteBASE): optional remote to pull from.
By default remote from core.remote config option is used.
@@ -100,25 +75,25 @@ class DataCloud(object):
information messages.
"""
return self.repo.cache.local.pull(
targets,
cache,
jobs=jobs,
remote=self.get_remote(remote, "pull"),
show_checksums=show_checksums,
)
def status(self, targets, jobs=None, remote=None, show_checksums=False):
def status(self, cache, jobs=None, remote=None, show_checksums=False):
"""Check status of data items in a cloud-agnostic way.
Args:
targets (list): list of targets to check status for.
cache (NamedCache): named checksums to check status for.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.RemoteBASE): optional remote to compare
targets to. By default remote from core.remote config option
cache to. By default remote from core.remote config option
is used.
show_checksums (bool): show checksums instead of file names in
information messages.
"""
remote = self.get_remote(remote, "status")
return self.repo.cache.local.status(
targets, jobs=jobs, remote=remote, show_checksums=show_checksums
cache, jobs=jobs, remote=remote, show_checksums=show_checksums
)
+8
View File
@@ -15,11 +15,19 @@ class DependencyIsNotFileOrDirError(DvcException):
super(DependencyIsNotFileOrDirError, self).__init__(msg)
class DependencyIsStageFileError(DvcException):
def __init__(self, path):
super(DependencyIsStageFileError, self).__init__(
"Stage file '{}' cannot be a dependency.".format(path)
)
class DependencyBase(object):
IS_DEPENDENCY = True
DoesNotExistError = DependencyDoesNotExistError
IsNotFileOrDirError = DependencyIsNotFileOrDirError
IsStageFileError = DependencyIsStageFileError
def update(self):
pass
+13 -3
View File
@@ -35,6 +35,11 @@ class DependencyREPO(DependencyLOCAL):
def is_in_repo(self):
return False
@property
def repo_pair(self):
d = self.def_repo
return d[self.PARAM_URL], d[self.PARAM_REV_LOCK] or d[self.PARAM_REV]
def __str__(self):
return "{} ({})".format(self.def_path, self.def_repo[self.PARAM_URL])
@@ -61,7 +66,7 @@ class DependencyREPO(DependencyLOCAL):
def dumpd(self):
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
def download(self, to, resume=False):
def fetch(self):
with self._make_repo(
cache_dir=self.repo.cache.local.cache_dir
) as repo:
@@ -70,8 +75,13 @@ class DependencyREPO(DependencyLOCAL):
out = repo.find_out_by_relpath(self.def_path)
with repo.state:
repo.cloud.pull(out.get_used_cache())
to.info = copy.copy(out.info)
to.checkout()
return out
def download(self, to):
out = self.fetch()
to.info = copy.copy(out.info)
to.checkout()
def update(self):
with self._make_repo(rev_lock=None) as repo:
+74 -23
View File
@@ -47,7 +47,7 @@ class OutputDuplicationError(DvcException):
for stage in stages
)
msg = (
"file/directory '{}' is specified as an output in more than one"
"file/directory '{}' is specified as an output in more than one "
"stage: {}"
).format(output, "\n ".join(stages))
super(OutputDuplicationError, self).__init__(msg)
@@ -82,8 +82,8 @@ class StagePathAsOutputError(DvcException):
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(
"current working directory '{cwd}' is specified as an output in "
"'{fname}'. Use another CWD to prevent any data removal.".format(
cwd=wdir, fname=fname
)
)
@@ -167,8 +167,8 @@ class CyclicGraphError(DvcException):
assert isinstance(stages, list)
stages = "\n".join("\t- {}".format(stage) for stage in stages)
msg = (
"you've introduced a cycle in your pipeline that involves"
" the following stages:"
"you've introduced a cycle in your pipeline that involves "
"the following stages:"
"\n"
"{stages}".format(stages=stages)
)
@@ -179,7 +179,7 @@ class ConfirmRemoveError(DvcException):
def __init__(self, path):
super(ConfirmRemoveError, self).__init__(
"unable to remove '{}' without a confirmation from the user. Use "
"'-f' to force.".format(path)
"`-f` to force.".format(path)
)
@@ -196,10 +196,11 @@ class ReproductionError(DvcException):
class BadMetricError(DvcException):
def __init__(self, path):
def __init__(self, paths):
super(BadMetricError, self).__init__(
"'{}' does not exist, not a metric or is malformed".format(
relpath(path)
"the following metrics do not exists, "
"are not metric files or are malformed: {paths}".format(
paths=", ".join("'{}'".format(path) for path in paths)
)
)
@@ -208,7 +209,7 @@ 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."
"Use `dvc metrics add` to add a metric file to track."
)
@@ -225,7 +226,7 @@ class StageFileCorruptedError(DvcException):
class RecursiveAddingWhileUsingFilename(DvcException):
def __init__(self):
super(RecursiveAddingWhileUsingFilename, self).__init__(
"using fname with recursive is not allowed."
"cannot use `fname` with multiple targets or `-R|--recursive`"
)
@@ -243,17 +244,10 @@ class OverlappingOutputPathsError(DvcException):
)
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
"Did you mean `git checkout {}`?".format(target), cause=cause
)
@@ -265,10 +259,11 @@ class ETagMismatchError(DvcException):
)
class OutputFileMissingError(DvcException):
def __init__(self, path):
super(OutputFileMissingError, self).__init__(
"Can't find {} neither locally nor on remote".format(path)
class FileMissingError(DvcException):
def __init__(self, path, cause=None):
super(FileMissingError, self).__init__(
"Can't find '{}' neither locally nor on remote".format(path),
cause=cause,
)
@@ -278,3 +273,59 @@ class DvcIgnoreInCollectedDirError(DvcException):
".dvcignore file should not be in collected dir path: "
"'{}'".format(ignore_dirname)
)
class UrlNotDvcRepoError(DvcException):
def __init__(self, url):
super(UrlNotDvcRepoError, self).__init__(
"URL '{}' is not a dvc repository.".format(url)
)
class GetDVCFileError(DvcException):
def __init__(self):
super(GetDVCFileError, self).__init__(
"the given path is a DVC-file, you must specify a data file "
"or a directory"
)
class GitHookAlreadyExistsError(DvcException):
def __init__(self, hook_name):
super(GitHookAlreadyExistsError, self).__init__(
"Hook '{}' already exists. Please refer to "
"https://man.dvc.org/install "
"for more info.".format(hook_name)
)
class DownloadError(DvcException):
def __init__(self, amount):
self.amount = amount
super(DownloadError, self).__init__(
"{amount} files failed to download".format(amount=amount)
)
class UploadError(DvcException):
def __init__(self, amount):
self.amount = amount
super(UploadError, self).__init__(
"{amount} files failed to upload".format(amount=amount)
)
class CheckoutError(DvcException):
def __init__(self, target_infos):
targets = [str(t) for t in target_infos]
m = (
"Checkout failed for following targets:\n {}\nDid you "
"forget to fetch?".format("\n".join(targets))
)
super(CheckoutError, self).__init__(m)
class CollectCacheError(DvcException):
pass
+77 -73
View File
@@ -1,87 +1,91 @@
from __future__ import unicode_literals
import os
import logging
import tempfile
from distutils.dir_util import copy_tree
from funcy import retry
from contextlib import contextmanager
from funcy import retry
from dvc.config import Config
from dvc.cache import CacheConfig
from dvc.exceptions import DvcException
from dvc.utils import remove
logger = logging.getLogger(__name__)
class ExternalRepoError(DvcException):
pass
class CloneError(ExternalRepoError):
def __init__(self, url, path, cause):
super(CloneError, self).__init__(
"Failed to clone repo '{}' to '{}'".format(url, path), cause=cause
)
class RevError(ExternalRepoError):
def __init__(self, url, rev, cause):
super(RevError, self).__init__(
"Failed to access revision '{}' for repo '{}'".format(rev, url),
cause=cause,
)
def _clone(cache_dir=None, **kwargs):
from dvc.repo import Repo
_path = tempfile.mkdtemp("dvc-repo")
_clone_git_repo(_path, **kwargs)
if cache_dir:
repo = Repo(_path)
cache_config = CacheConfig(repo.config)
cache_config.set_dir(cache_dir, level=Config.LEVEL_LOCAL)
repo.scm.git.close()
return Repo(_path)
def _clone_git_repo(to_path, url=None, rev=None, rev_lock=None):
import git
try:
repo = git.Repo.clone_from(url, to_path, no_single_branch=True)
except git.exc.GitCommandError as exc:
raise CloneError(url, to_path, exc)
try:
revision = rev_lock or rev
if revision:
try:
repo.git.checkout(revision)
except git.exc.GitCommandError as exc:
raise RevError(url, revision, exc)
finally:
repo.close()
def _remove(repo):
repo.scm.git.close()
if os.name == "nt":
# git.exe may hang for a while not permitting to remove temp dir
os_retry = retry(5, errors=OSError, timeout=0.1)
os_retry(remove)(repo.root_dir)
else:
remove(repo.root_dir)
REPO_CACHE = {}
@contextmanager
def external_repo(**kwargs):
repo = _clone(**kwargs)
def external_repo(url=None, rev=None, rev_lock=None, cache_dir=None):
from dvc.repo import Repo
path = _external_repo(url=url, rev=rev_lock or rev, cache_dir=cache_dir)
repo = Repo(path)
yield repo
_remove(repo)
repo.close()
def _external_repo(url=None, rev=None, cache_dir=None):
from dvc.config import Config
from dvc.cache import CacheConfig
from dvc.repo import Repo
key = (url, rev, cache_dir)
if key in REPO_CACHE:
return REPO_CACHE[key]
new_path = tempfile.mkdtemp("dvc-erepo")
# Copy and adjust existing clone
if (url, None, None) in REPO_CACHE:
old_path = REPO_CACHE[url, None, None]
# This one unlike shutil.copytree() works with an existing dir
copy_tree(old_path, new_path)
else:
# Create a new clone
_clone_repo(url, new_path)
# Save clean clone dir so that we will have access to a default branch
clean_clone_path = tempfile.mkdtemp("dvc-erepo")
copy_tree(new_path, clean_clone_path)
REPO_CACHE[url, None, None] = clean_clone_path
# Adjust new clone/copy to fit rev and cache_dir
repo = Repo(new_path)
try:
if rev is not None:
repo.scm.checkout(rev)
if cache_dir is not None:
cache_config = CacheConfig(repo.config)
cache_config.set_dir(cache_dir, level=Config.LEVEL_LOCAL)
finally:
# Need to close/reopen repo to force config reread
repo.close()
REPO_CACHE[key] = new_path
return new_path
def clean_repos():
# Outside code should not see cache while we are removing
repo_paths = list(REPO_CACHE.values())
REPO_CACHE.clear()
for path in repo_paths:
_remove(path)
def _remove(path):
if os.name == "nt":
# git.exe may hang for a while not permitting to remove temp dir
os_retry = retry(5, errors=OSError, timeout=0.1)
os_retry(remove)(path)
else:
remove(path)
def _clone_repo(url, path):
from dvc.scm.git import Git
git = Git.clone(url, path)
git.close()
+122 -37
View File
@@ -3,58 +3,143 @@
from __future__ import unicode_literals
import os
import hashlib
import time
import zc.lockfile
from datetime import timedelta
from funcy.py3 import lkeep
from dvc.exceptions import DvcException
from dvc.utils import makedirs
from dvc.utils.compat import is_py3
DEFAULT_TIMEOUT = 5
class LockError(DvcException):
"""Thrown when unable to acquire the lock for dvc repo."""
class Lock(object):
"""Class for dvc repo lock.
if is_py3:
import flufl.lock
Args:
dvc_dir (str): path to the directory that the lock should be created
in.
name (str): name of the lock file.
"""
class Lock(flufl.lock.Lock):
"""Class for dvc repo lock.
LOCK_FILE = "lock"
TIMEOUT = 5
Args:
lockfile (str): the lock filename
in.
tmp_dir (str): a directory to store claim files.
"""
def __init__(self, dvc_dir, name=LOCK_FILE):
self.lock_file = os.path.join(dvc_dir, name)
self._lock = None
def __init__(self, lockfile, tmp_dir=None):
import socket
def _do_lock(self):
try:
self._lock = zc.lockfile.LockFile(self.lock_file)
except zc.lockfile.LockError:
raise LockError(
"cannot perform the cmd since DVC is busy and "
"locked. Please retry the cmd later."
)
self._tmp_dir = tmp_dir
if self._tmp_dir is not None:
makedirs(self._tmp_dir, exist_ok=True)
# NOTE: this is basically Lock.__init__ copy-paste, except that
# instead of using `socket.getfqdn()` we use `socket.gethostname()`
# to speed this up. We've seen [1] `getfqdn()` take ~5sec to return
# anything, which is way too slow. `gethostname()` is actually a
# fallback for `getfqdn()` when it is not able to resolve a
# canonical hostname through network. The claimfile that uses
# `self._hostname` is still usable, as it uses `pid` and random
# number to generate the resulting lock file name, which is unique
# enough for our application.
#
# [1] https://github.com/iterative/dvc/issues/2582
self._hostname = socket.gethostname()
self._lockfile = lockfile
self._lifetime = timedelta(days=365) # Lock for good by default
self._separator = flufl.lock.SEP
self._set_claimfile()
self._owned = True
self._retry_errnos = []
@property
def lockfile(self):
return self._lockfile
@property
def files(self):
return lkeep([self._lockfile, self._tmp_dir])
def lock(self):
try:
super(Lock, self).lock(timedelta(seconds=DEFAULT_TIMEOUT))
except flufl.lock.TimeOutError:
raise LockError(
"cannot perform the cmd since DVC is busy and "
"locked. Please retry the cmd later."
)
def _set_claimfile(self, pid=None):
super(Lock, self)._set_claimfile(pid)
if self._tmp_dir is not None:
# Under Windows file path length is limited so we hash it
filename = hashlib.md5(self._claimfile.encode()).hexdigest()
self._claimfile = os.path.join(
self._tmp_dir, filename + ".lock"
)
# Fix for __del__ bug in flufl.lock [1] which is causing errors on
# Python shutdown [2].
# [1] https://gitlab.com/warsaw/flufl.lock/issues/7
# [2] https://github.com/iterative/dvc/issues/2573
def __del__(self):
try:
if self._owned:
self.finalize()
except ImportError:
pass
else:
import zc.lockfile
class Lock(object):
"""Class for dvc repo lock.
Uses zc.lockfile as backend.
"""
def __init__(self, lockfile, tmp_dir=None):
self.lockfile = lockfile
self._lock = None
@property
def files(self):
return [self.lockfile]
def _do_lock(self):
try:
self._lock = zc.lockfile.LockFile(self.lockfile)
except zc.lockfile.LockError:
raise LockError(
"cannot perform the cmd since DVC is busy and "
"locked. Please retry the cmd later."
)
def lock(self):
try:
self._do_lock()
return
except LockError:
time.sleep(DEFAULT_TIMEOUT)
def lock(self):
"""Acquire lock for dvc repo."""
try:
self._do_lock()
return
except LockError:
time.sleep(self.TIMEOUT)
self._do_lock()
def unlock(self):
self._lock.close()
self._lock = None
def unlock(self):
"""Release lock for dvc repo."""
self._lock.close()
self._lock = None
def __enter__(self):
self.lock()
def __enter__(self):
self.lock()
def __exit__(self, typ, value, tbck):
self.unlock()
def __exit__(self, typ, value, tbck):
self.unlock()
+54 -20
View File
@@ -2,7 +2,8 @@
from __future__ import unicode_literals
from dvc.utils.compat import str, StringIO
from dvc.utils.compat import str, StringIO, RecursionError
from dvc.progress import Tqdm
import logging
import logging.handlers
@@ -18,7 +19,12 @@ class LoggingException(Exception):
class ExcludeErrorsFilter(logging.Filter):
def filter(self, record):
return record.levelno < logging.ERROR
return record.levelno < logging.WARNING
class ExcludeInfoFilter(logging.Filter):
def filter(self, record):
return record.levelno < logging.INFO
class ColorFormatter(logging.Formatter):
@@ -43,7 +49,7 @@ class ColorFormatter(logging.Formatter):
}
footer = (
"{yellow}Having any troubles?{nc}."
"{yellow}Having any troubles?{nc}"
" Hit us up at {blue}https://dvc.org/support{nc},"
" we are always happy to help!"
).format(
@@ -53,9 +59,6 @@ class ColorFormatter(logging.Formatter):
)
def format(self, record):
if self._is_visible(record):
self._progress_aware()
if record.levelname == "INFO":
return record.msg
@@ -146,20 +149,25 @@ class ColorFormatter(logging.Formatter):
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()
class LoggerHandler(logging.StreamHandler):
def handleError(self, record):
super(LoggerHandler, self).handleError(record)
raise LoggingException(record)
def emit(self, record):
"""Write to Tqdm's stream so as to not break progressbars"""
try:
msg = self.format(record)
Tqdm.write(
msg, file=self.stream, end=getattr(self, "terminator", "\n")
)
self.flush()
except RecursionError:
raise
except Exception:
self.handleError(record)
def setup(level=logging.INFO):
colorama.init()
@@ -167,19 +175,29 @@ def setup(level=logging.INFO):
logging.config.dictConfig(
{
"version": 1,
"filters": {"exclude_errors": {"()": ExcludeErrorsFilter}},
"filters": {
"exclude_errors": {"()": ExcludeErrorsFilter},
"exclude_info": {"()": ExcludeInfoFilter},
},
"formatters": {"color": {"()": ColorFormatter}},
"handlers": {
"console": {
"console_info": {
"class": "dvc.logger.LoggerHandler",
"level": "DEBUG",
"level": "INFO",
"formatter": "color",
"stream": "ext://sys.stdout",
"filters": ["exclude_errors"],
},
"console_debug": {
"class": "dvc.logger.LoggerHandler",
"level": "DEBUG",
"formatter": "color",
"stream": "ext://sys.stdout",
"filters": ["exclude_info"],
},
"console_errors": {
"class": "dvc.logger.LoggerHandler",
"level": "ERROR",
"level": "WARNING",
"formatter": "color",
"stream": "ext://sys.stderr",
},
@@ -187,11 +205,27 @@ def setup(level=logging.INFO):
"loggers": {
"dvc": {
"level": level,
"handlers": ["console", "console_errors"],
"handlers": [
"console_info",
"console_debug",
"console_errors",
],
},
"paramiko": {
"level": logging.CRITICAL,
"handlers": ["console", "console_errors"],
"handlers": [
"console_info",
"console_debug",
"console_errors",
],
},
"flufl.lock": {
"level": logging.CRITICAL,
"handlers": [
"console_info",
"console_debug",
"console_errors",
],
},
},
}
+20 -3
View File
@@ -5,10 +5,13 @@ from __future__ import unicode_literals
import logging
from dvc.cli import parse_args
from dvc.lock import LockError
from dvc.config import ConfigError
from dvc.analytics import Analytics
from dvc.exceptions import NotDvcRepoError, DvcParserError
from dvc.remote.pool import close_pools
from dvc.external_repo import clean_repos
from dvc.utils.compat import is_py2
logger = logging.getLogger("dvc")
@@ -37,7 +40,10 @@ def main(argv=None):
logger.setLevel(logging.DEBUG)
cmd = args.func(args)
ret = cmd.run_cmd()
ret = cmd.run()
except LockError:
logger.exception("failed to lock before running a command")
ret = 250
except ConfigError:
logger.exception("configuration error")
ret = 251
@@ -49,15 +55,26 @@ def main(argv=None):
ret = 253
except DvcParserError:
ret = 254
except Exception: # pylint: disable=broad-except
logger.exception("unexpected error")
except Exception as exc: # pylint: disable=broad-except
if isinstance(exc, UnicodeError) and is_py2:
logger.exception(
"unicode is not supported in DVC for Python 2 "
"(end-of-life January 1, 2020), please upgrade to Python 3"
)
else:
logger.exception("unexpected error")
ret = 255
finally:
logger.setLevel(outerLogLevel)
# Python 2 fails to close these clean occasionally and users see
# weird error messages, so we do it manually
close_pools()
# Remove cached repos in the end of the call, these are anonymous
# so won't be reused by any other subsequent run anyway.
clean_repos()
Analytics().send_cmd(cmd, args, ret)
return ret
+40 -41
View File
@@ -5,8 +5,9 @@ from copy import copy
from schema import Or, Optional
from dvc.cache import NamedCache
import dvc.prompt as prompt
from dvc.exceptions import DvcException
from dvc.exceptions import DvcException, CollectCacheError
from dvc.utils.compat import str, urlparse
from dvc.remote.base import RemoteBASE
@@ -32,6 +33,13 @@ class OutputAlreadyTrackedError(DvcException):
super(OutputAlreadyTrackedError, self).__init__(msg)
class OutputIsStageFileError(DvcException):
def __init__(self, path):
super(OutputIsStageFileError, self).__init__(
"Stage file '{}' cannot be an output.".format(path)
)
class OutputBase(object):
IS_DEPENDENCY = False
@@ -57,6 +65,7 @@ class OutputBase(object):
DoesNotExistError = OutputDoesNotExistError
IsNotFileOrDirError = OutputIsNotFileOrDirError
IsStageFileError = OutputIsStageFileError
sep = "/"
@@ -71,6 +80,7 @@ class OutputBase(object):
persist=False,
tags=None,
):
self._validate_output_path(path)
# This output (and dependency) objects have too many paths/urls
# here is a list and comments:
#
@@ -104,8 +114,7 @@ class OutputBase(object):
if remote:
parsed = urlparse(path)
return remote.path_info / parsed.path.lstrip("/")
else:
return self.REMOTE.path_cls(path)
return self.REMOTE.path_cls(path)
def __repr__(self):
return "{class_name}: '{def_path}'".format(
@@ -138,11 +147,6 @@ class OutputBase(object):
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 supported(cls, url):
return cls.REMOTE.supported(url)
@@ -283,14 +287,15 @@ class OutputBase(object):
def checkout(self, force=False, progress_callback=None, tag=None):
if not self.use_cache:
return
progress_callback(str(self.path_info), self.get_files_number())
return None
if tag:
info = self.tags[tag]
else:
info = self.info
self.cache.checkout(
return self.cache.checkout(
self.path_info,
info,
force=force,
@@ -319,20 +324,17 @@ class OutputBase(object):
self.repo.scm.ignore(self.fspath)
def get_files_number(self):
if not self.use_cache or not self.checksum:
if not self.use_cache:
return 0
if self.is_dir_checksum:
return len(self.dir_cache)
return 1
return self.cache.get_files_number(self.checksum)
def unprotect(self):
if self.exists:
self.remote.unprotect(self.path_info)
def _collect_used_dir_cache(self, remote=None, force=False, jobs=None):
"""Get a list of `info`s retaled to the given directory.
"""Get a list of `info`s related to the given directory.
- Pull the directory entry from the remote cache if it was changed.
@@ -344,25 +346,19 @@ class OutputBase(object):
$ echo "bar" > directory/bar
$ dvc add directory
It will return something similar to the following list:
It will return a NamedCache like:
[
{ 'path': 'directory/foo', 'md5': 'c157a79031e1', ... },
{ 'path': 'directory/bar', 'md5': 'd3b07384d113', ... },
]
nc = NamedCache()
nc.add(self.scheme, 'c157a79031e1', 'directory/foo')
nc.add(self.scheme, 'd3b07384d113', 'directory/bar')
"""
ret = []
cache = NamedCache()
if self.cache.changed_cache_file(self.checksum):
try:
self.repo.cloud.pull(
[
{
self.remote.PARAM_CHECKSUM: self.checksum,
"name": str(self),
}
],
NamedCache.make("local", self.checksum, str(self)),
jobs=jobs,
remote=remote,
show_checksums=False,
@@ -377,20 +373,19 @@ class OutputBase(object):
"Would you like to continue? Use '-f' to force."
)
if not force and not prompt.confirm(msg.format(self.path_info)):
raise DvcException(
raise CollectCacheError(
"unable to fully collect used cache"
" without cache for directory '{}'".format(self)
)
else:
return ret
return cache
for entry in self.dir_cache:
info = copy(entry)
checksum = entry[self.remote.PARAM_CHECKSUM]
path_info = self.path_info / entry[self.remote.PARAM_RELPATH]
info["name"] = str(path_info)
ret.append(info)
cache.add(self.scheme, checksum, str(path_info))
return ret
return cache
def get_used_cache(self, **kwargs):
"""Get a dumpd of the given `out`, with an entry including the branch.
@@ -401,11 +396,8 @@ class OutputBase(object):
include the `info` of its files.
"""
if self.stage.is_repo_import:
return []
if not self.use_cache:
return []
return NamedCache()
if not self.info:
logger.warning(
@@ -413,13 +405,20 @@ class OutputBase(object):
"not be collected. Use dvc repro to get your pipeline up to "
"date.".format(self, self.stage)
)
return []
return NamedCache()
ret = [{self.remote.PARAM_CHECKSUM: self.checksum, "name": str(self)}]
ret = NamedCache.make(self.scheme, self.checksum, str(self))
if not self.is_dir_checksum:
return ret
ret.extend(self._collect_used_dir_cache(**kwargs))
ret.update(self._collect_used_dir_cache(**kwargs))
return ret
@classmethod
def _validate_output_path(cls, path):
from dvc.stage import Stage
if Stage.is_valid_filename(path):
raise cls.IsStageFileError(path)
-14
View File
@@ -47,20 +47,6 @@ class OutputLOCAL(OutputBase):
def_scheme = urlparse(self.def_path).scheme
return def_scheme != "remote" and not os.path.isabs(self.def_path)
def assign_to_stage_file(self, stage):
from dvc.repo import Repo
# Make path info the same but relative to passed stage
rel_path = self.path_info.relpath(self.stage.wdir)
self.path_info = (
self.REMOTE.path_cls(os.path.abspath(stage.wdir)) / rel_path
)
self.repo = Repo(self.fspath)
self.stage = stage
return self
def dumpd(self):
ret = super(OutputLOCAL, self).dumpd()
if self.is_in_repo:
+9 -4
View File
@@ -36,6 +36,15 @@ class PathInfo(pathlib.PurePath):
def from_posix(cls, s):
return cls(PosixPathInfo(s))
def as_posix(self):
f = self._flavour
# Unlike original implementation [1] that uses `str()` we actually need
# to use `fspath`, because we've overriden `__str__` method to return
# relative paths, which will break original `as_posix`.
#
# [1] https://github.com/python/cpython/blob/v3.7.0/Lib/pathlib.py#L692
return self.fspath.replace(f.sep, "/")
def __str__(self):
path = self.__fspath__()
return relpath(path)
@@ -94,10 +103,6 @@ class PathInfo(pathlib.PurePath):
def with_name(self, name):
return pathlib.PurePath.with_name(self, name.encode(fs_encoding))
def as_posix(self):
f = self._flavour
return str(self).replace(f.sep, "/")
class WindowsPathInfo(PathInfo, pathlib.PureWindowsPath):
pass
+123 -137
View File
@@ -1,154 +1,140 @@
"""Manages progress bars for dvc repo."""
from __future__ import print_function
from __future__ import unicode_literals
from dvc.utils.compat import str
import logging
import sys
import threading
from tqdm import tqdm
from concurrent.futures import ThreadPoolExecutor
from funcy import merge
from dvc.utils import env2bool
CLEARLINE_PATTERN = "\r\x1b[K"
logger = logging.getLogger(__name__)
class Progress(object):
class TqdmThreadPoolExecutor(ThreadPoolExecutor):
"""
Simple multi-target progress bar.
Ensure worker progressbars are cleared away properly.
"""
def __init__(self):
self._n_total = 0
self._n_finished = 0
self._lock = threading.Lock()
self._line = None
def set_n_total(self, total):
"""Sets total number of targets."""
self._n_total = total
self._n_finished = 0
@property
def is_finished(self):
"""Returns if all targets have finished."""
return self._n_total == self._n_finished
def clearln(self):
self._print(CLEARLINE_PATTERN, end="")
def _writeln(self, line):
self.clearln()
self._print(line, end="")
sys.stdout.flush()
def reset(self):
with self._lock:
self._n_total = 0
self._n_finished = 0
self._line = None
def refresh(self, line=None):
"""Refreshes progress bar."""
# Just go away if it is locked. Will update next time
if not self._lock.acquire(False):
return
if line is None:
line = self._line
if sys.stdout.isatty() and line is not None:
self._writeln(line)
self._line = line
self._lock.release()
def update_target(self, name, current, total):
"""Updates progress bar for a specified target."""
self.refresh(self._bar(name, current, total))
def finish_target(self, name):
"""Finishes progress bar for a specified target."""
# We have to write a msg about finished target
with self._lock:
pbar = self._bar(name, 100, 100)
if sys.stdout.isatty():
self.clearln()
self._print(pbar)
self._n_finished += 1
self._line = None
def _bar(self, target_name, current, total):
"""
Make a progress bar out of info, which looks like:
(1/2): [########################################] 100% master.zip
"""
bar_len = 30
if total is None:
state = 0
percent = "?% "
else:
total = int(total)
state = int((100 * current) / total) if current < total else 100
percent = str(state) + "% "
if self._n_total > 1:
num = "({}/{}): ".format(self._n_finished + 1, self._n_total)
else:
num = ""
n_sh = int((state * bar_len) / 100)
n_sp = bar_len - n_sh
pbar = "[" + "#" * n_sh + " " * n_sp + "] "
return num + pbar + percent + target_name
@staticmethod
def _print(*args, **kwargs):
import logging
logger = logging.getLogger(__name__)
if logger.getEffectiveLevel() == logging.CRITICAL:
return
print(*args, **kwargs)
def __enter__(self):
self._lock.acquire(True)
if self._line is not None:
self.clearln()
"""
Creates a blank initial dummy progress bar if needed so that workers
are forced to create "nested" bars.
"""
blank_bar = Tqdm(bar_format="Multi-Threaded:", leave=False)
if blank_bar.pos > 0:
# already nested - don't need a placeholder bar
blank_bar.close()
self.bar = blank_bar
return super(TqdmThreadPoolExecutor, self).__enter__()
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)
def __exit__(self, *a, **k):
super(TqdmThreadPoolExecutor, self).__exit__(*a, **k)
self.bar.close()
class ProgressCallback(object):
def __init__(self, total):
self.total = total
self.current = 0
progress.reset()
class Tqdm(tqdm):
"""
maximum-compatibility tqdm-based progressbars
"""
def update(self, name, progress_to_add=1):
self.current += progress_to_add
progress.update_target(name, self.current, self.total)
BAR_FMT_DEFAULT = (
"{percentage:3.0f}%|{bar:10}|"
"{desc:{ncols_desc}.{ncols_desc}}{n}/{total}"
" [{elapsed}<{remaining}, {rate_fmt:>11}{postfix}]"
)
BAR_FMT_NOTOTAL = (
"{desc:{ncols_desc}.{ncols_desc}}{n}"
" [{elapsed}<??:??, {rate_fmt:>11}{postfix}]"
)
def finish(self, name):
progress.finish_target(name)
def __init__(
self,
iterable=None,
disable=None,
level=logging.ERROR,
desc=None,
leave=False,
bar_format=None,
bytes=False, # pylint: disable=W0622
file=None,
**kwargs
):
"""
bytes : shortcut for
`unit='B', unit_scale=True, unit_divisor=1024, miniters=1`
desc : persists after `close()`
level : effective logging level for determining `disable`;
used only if `disable` is unspecified
disable : If (default: None), will be determined by logging level.
May be overridden to `True` due to non-TTY status.
Skip override by specifying env var `DVC_IGNORE_ISATTY`.
kwargs : anything accepted by `tqdm.tqdm()`
"""
kwargs = kwargs.copy()
kwargs.setdefault("unit_scale", True)
if bytes:
bytes_defaults = dict(
unit="B", unit_scale=True, unit_divisor=1024, miniters=1
)
kwargs = merge(bytes_defaults, kwargs)
if file is None:
file = sys.stderr
self.desc_persist = desc
# auto-disable based on `logger.level`
if disable is None:
disable = logger.getEffectiveLevel() > level
# auto-disable based on TTY
if (
not disable
and not env2bool("DVC_IGNORE_ISATTY")
and hasattr(file, "isatty")
):
disable = not file.isatty()
super(Tqdm, self).__init__(
iterable=iterable,
disable=disable,
leave=leave,
desc=desc,
bar_format="!",
**kwargs
)
if bar_format is None:
if self.__len__():
self.bar_format = self.BAR_FMT_DEFAULT
else:
self.bar_format = self.BAR_FMT_NOTOTAL
else:
self.bar_format = bar_format
self.refresh()
def update_desc(self, desc, n=1):
"""
Calls `set_description_str(desc)` and `update(n)`
"""
self.set_description_str(desc, refresh=False)
self.update(n)
progress = Progress() # pylint: disable=invalid-name
def update_to(self, current, total=None):
if total:
self.total = total # pylint: disable=W0613,W0201
self.update(current - self.n)
def close(self):
if self.desc_persist is not None:
self.set_description_str(self.desc_persist, refresh=False)
super(Tqdm, self).close()
@property
def format_dict(self):
"""inject `ncols_desc` to fill the display width (`ncols`)"""
d = super(Tqdm, self).format_dict
ncols = d["ncols"] or 80
ncols_desc = ncols - len(self.format_meter(ncols_desc=1, **d)) + 1
ncols_desc = max(ncols_desc, 0)
if ncols_desc:
d["ncols_desc"] = ncols_desc
else:
# work-around for zero-width description
d["ncols_desc"] = 1
d["prefix"] = ""
return d
+54 -38
View File
@@ -4,17 +4,14 @@ from __future__ import unicode_literals
import os
import re
import logging
from datetime import datetime, timedelta
from funcy import cached_property
from dvc.scheme import Schemes
try:
from azure.storage.blob import BlockBlobService
from azure.common import AzureMissingResourceHttpError
except ImportError:
BlockBlobService = None
from dvc.utils.compat import urlparse
from dvc.progress import progress
from dvc.progress import Tqdm
from dvc.config import Config
from dvc.remote.base import RemoteBASE
from dvc.path_info import CloudURLInfo
@@ -23,14 +20,6 @@ from dvc.path_info import CloudURLInfo
logger = logging.getLogger(__name__)
class Callback(object):
def __init__(self, name):
self.name = name
def __call__(self, current, total):
progress.update_target(self.name, current, total)
class RemoteAZURE(RemoteBASE):
scheme = Schemes.AZURE
path_cls = CloudURLInfo
@@ -40,7 +29,7 @@ class RemoteAZURE(RemoteBASE):
r"(ContainerName=(?P<container_name>[^;]+);?)?"
r"(?P<connection_string>.+)?)?)$"
)
REQUIRES = {"azure-storage-blob": BlockBlobService}
REQUIRES = {"azure-storage-blob": "azure.storage.blob"}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
@@ -69,29 +58,30 @@ class RemoteAZURE(RemoteBASE):
if not self.connection_string:
raise ValueError("azure storage connection string missing")
self.__blob_service = None
self.path_info = (
self.path_cls(url)
if path
else self.path_cls.from_parts(scheme=self.scheme, netloc=bucket)
)
@property
@cached_property
def blob_service(self):
if self.__blob_service is None:
logger.debug("URL {}".format(self.path_info))
logger.debug("Connection string {}".format(self.connection_string))
self.__blob_service = BlockBlobService(
connection_string=self.connection_string
from azure.storage.blob import BlockBlobService
from azure.common import AzureMissingResourceHttpError
logger.debug("URL {}".format(self.path_info))
logger.debug("Connection string {}".format(self.connection_string))
blob_service = BlockBlobService(
connection_string=self.connection_string
)
logger.debug("Container name {}".format(self.path_info.bucket))
try: # verify that container exists
blob_service.list_blobs(
self.path_info.bucket, delimiter="/", num_results=1
)
logger.debug("Container name {}".format(self.path_info.bucket))
try: # verify that container exists
self.__blob_service.list_blobs(
self.path_info.bucket, delimiter="/", num_results=1
)
except AzureMissingResourceHttpError:
self.__blob_service.create_container(self.path_info.bucket)
return self.__blob_service
except AzureMissingResourceHttpError:
blob_service.create_container(self.path_info.bucket)
return blob_service
def remove(self, path_info):
if path_info.scheme != self.scheme:
@@ -122,15 +112,41 @@ class RemoteAZURE(RemoteBASE):
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
cb = None if no_progress_bar else Callback(name)
self.blob_service.create_blob_from_path(
to_info.bucket, to_info.path, from_file, progress_callback=cb
)
with Tqdm(desc=name, disable=no_progress_bar, bytes=True) as pbar:
self.blob_service.create_blob_from_path(
to_info.bucket,
to_info.path,
from_file,
progress_callback=pbar.update_to,
)
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
):
cb = None if no_progress_bar else Callback(name)
self.blob_service.get_blob_to_path(
from_info.bucket, from_info.path, to_file, progress_callback=cb
with Tqdm(desc=name, disable=no_progress_bar, bytes=True) as pbar:
self.blob_service.get_blob_to_path(
from_info.bucket,
from_info.path,
to_file,
progress_callback=pbar.update_to,
)
def exists(self, path_info):
paths = self._list_paths(path_info.bucket, path_info.path)
return any(path_info.path == path for path in paths)
def _generate_download_url(self, path_info, expires=3600):
from azure.storage.blob import BlobPermissions
expires_at = datetime.utcnow() + timedelta(seconds=expires)
sas_token = self.blob_service.generate_blob_shared_access_signature(
path_info.bucket,
path_info.path,
permission=BlobPermissions.READ,
expiry=expires_at,
)
download_url = self.blob_service.make_blob_url(
path_info.bucket, path_info.path, sas_token=sas_token
)
return download_url
+274 -152
View File
@@ -1,16 +1,19 @@
from __future__ import unicode_literals
from dvc.ignore import DvcIgnore
from dvc.utils.compat import str, basestring, urlparse, fspath_py35, makedirs
from operator import itemgetter
from multiprocessing import cpu_count
import os
import json
import logging
import tempfile
import itertools
from operator import itemgetter
from multiprocessing import cpu_count
from functools import partial
from concurrent.futures import ThreadPoolExecutor
from copy import copy
from dvc.ignore import DvcIgnore
from dvc.utils.compat import str, basestring, urlparse, FileNotFoundError
from dvc.remote.slow_link_detection import slow_link_guard
import dvc.prompt as prompt
from dvc.config import Config
@@ -19,21 +22,19 @@ from dvc.exceptions import (
ConfirmRemoveError,
DvcIgnoreInCollectedDirError,
)
from dvc.progress import progress, ProgressCallback
from dvc.utils import LARGE_DIR_SIZE, tmp_fname, to_chunks, move, relpath
from dvc.progress import Tqdm
from dvc.utils import tmp_fname, move, relpath, makedirs
from dvc.state import StateNoop
from dvc.path_info import PathInfo, URLInfo
from dvc.utils.http import open_url
logger = logging.getLogger(__name__)
STATUS_OK = 1
STATUS_MISSING = 2
STATUS_NEW = 3
STATUS_DELETED = 4
STATUS_MAP = {
# (local_exists, remote_exists)
(True, True): STATUS_OK,
@@ -68,6 +69,14 @@ class RemoteMissingDepsError(DvcException):
pass
class DirCacheError(DvcException):
def __init__(self, checksum, cause=None):
super(DirCacheError, self).__init__(
"Failed to load dir cache for checksum: '{}'.".format(checksum),
cause=cause,
)
class RemoteBASE(object):
scheme = "base"
path_cls = URLInfo
@@ -77,36 +86,14 @@ class RemoteBASE(object):
PARAM_RELPATH = "relpath"
CHECKSUM_DIR_SUFFIX = ".dir"
CHECKSUM_JOBS = max(1, min(4, cpu_count() // 2))
DEFAULT_CACHE_TYPES = ["copy"]
state = StateNoop()
def __init__(self, repo, config):
self.repo = repo
deps_ok = all(self.REQUIRES.values())
if not deps_ok:
missing = [k for k, v in self.REQUIRES.items() if v is None]
url = config.get(
Config.SECTION_REMOTE_URL, "{}://".format(self.scheme)
)
msg = (
"URL '{}' is supported but requires these missing "
"dependencies: {}. If you have installed dvc using pip, "
"choose one of these options to proceed: \n"
"\n"
" 1) Install specific missing dependencies:\n"
" pip install {}\n"
" 2) Install dvc package that includes those missing "
"dependencies: \n"
" pip install dvc[{}]\n"
" 3) Install dvc package with all possible "
"dependencies included: \n"
" pip install dvc[all]\n"
"\n"
"If you have installed dvc from a binary package and you "
"are still seeing this message, please report it to us "
"using https://github.com/iterative/dvc/issues. Thank you!"
).format(url, missing, " ".join(missing), self.scheme)
raise RemoteMissingDepsError(msg)
self._check_requires(config)
core = config.get(Config.SECTION_CORE, {})
self.checksum_jobs = core.get(
@@ -114,9 +101,55 @@ class RemoteBASE(object):
)
self.protected = False
self.no_traverse = config.get(Config.SECTION_REMOTE_NO_TRAVERSE)
self.no_traverse = config.get(Config.SECTION_REMOTE_NO_TRAVERSE, True)
self._dir_info = {}
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)
self.cache_type_confirmed = False
def _check_requires(self, config):
import importlib
missing = []
for package, module in self.REQUIRES.items():
try:
importlib.import_module(module)
except ImportError:
missing.append(package)
if not missing:
return
url = config.get(
Config.SECTION_REMOTE_URL, "{}://".format(self.scheme)
)
msg = (
"URL '{}' is supported but requires these missing "
"dependencies: {}. If you have installed dvc using pip, "
"choose one of these options to proceed: \n"
"\n"
" 1) Install specific missing dependencies:\n"
" pip install {}\n"
" 2) Install dvc package that includes those missing "
"dependencies: \n"
" pip install 'dvc[{}]'\n"
" 3) Install dvc package with all possible "
"dependencies included: \n"
" pip install 'dvc[all]'\n"
"\n"
"If you have installed dvc from a binary package and you "
"are still seeing this message, please report it to us "
"using https://github.com/iterative/dvc/issues. Thank you!"
).format(url, missing, " ".join(missing), self.scheme)
raise RemoteMissingDepsError(msg)
def __repr__(self):
return "{class_name}: '{path_info}'".format(
class_name=type(self).__name__,
@@ -145,29 +178,23 @@ class RemoteBASE(object):
file_infos = list(file_infos)
with ThreadPoolExecutor(max_workers=self.checksum_jobs) as executor:
tasks = executor.map(self.get_file_checksum, file_infos)
if len(file_infos) > LARGE_DIR_SIZE:
msg = (
"Computing md5 for a large number of files. "
"This is only done once."
)
logger.info(msg)
tasks = progress(tasks, total=len(file_infos))
checksums = {
file_infos[index]: task for index, task in enumerate(tasks)
}
with Tqdm(
tasks,
total=len(file_infos),
unit="md5",
desc="Computing hashes (only done once)",
) as tasks:
checksums = dict(zip(file_infos, tasks))
return checksums
def _collect_dir(self, path_info):
file_infos = set()
for root, _dirs, files in self.walk(path_info):
if DvcIgnore.DVCIGNORE_FILE in files:
raise DvcIgnoreInCollectedDirError(root)
for fname in self.walk_files(path_info):
if DvcIgnore.DVCIGNORE_FILE == fname.name:
raise DvcIgnoreInCollectedDirError(fname.parent)
file_infos.update(path_info / root / fname for fname in files)
file_infos.add(fname)
checksums = {fi: self.state.get(fi) for fi in file_infos}
not_in_state = {
@@ -202,6 +229,7 @@ class RemoteBASE(object):
checksum, tmp_info = self._get_dir_info_checksum(dir_info)
new_info = self.cache.checksum_to_path_info(checksum)
if self.cache.changed_cache_file(checksum):
self.cache.makedirs(new_info.parent)
self.cache.move(tmp_info, new_info)
self.state.save(path_info, checksum)
@@ -228,30 +256,26 @@ class RemoteBASE(object):
if dir_info:
return dir_info
dir_info = self.load_dir_cache(checksum)
try:
dir_info = self.load_dir_cache(checksum)
except DirCacheError:
dir_info = []
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 = PathInfo(path)
self.cache.download(path_info, to_info, no_progress_bar=True)
try:
with open(path, "r") as fobj:
with self.cache.open(path_info, "r") as fobj:
d = json.load(fobj)
except ValueError:
logger.exception("Failed to load dir cache '{}'".format(path_info))
return []
finally:
os.unlink(path)
except (ValueError, FileNotFoundError) as exc:
raise DirCacheError(checksum, cause=exc)
if not isinstance(d, list):
msg = "dir cache file format error '{}' [skipping the file]"
logger.error(msg.format(relpath(path)))
logger.error(msg.format(relpath(path_info)))
return []
for info in d:
@@ -348,8 +372,48 @@ class RemoteBASE(object):
logger.debug("'{}' hasn't changed.".format(path_info))
return False
def link(self, from_info, to_info, link_type=None):
self.copy(from_info, to_info)
def link(self, from_info, to_info):
self._link(from_info, to_info, self.cache_types)
def _link(self, from_info, to_info, link_types):
assert self.isfile(from_info)
self.makedirs(to_info.parent)
self._try_links(from_info, to_info, link_types)
@slow_link_guard
def _try_links(self, from_info, to_info, link_types):
while link_types:
link_method = getattr(self, link_types[0])
try:
self._do_link(from_info, to_info, link_method)
self.cache_type_confirmed = True
return
except DvcException as exc:
msg = "Cache type '{}' is not supported: {}"
logger.debug(msg.format(link_types[0], str(exc)))
del link_types[0]
raise DvcException("no possible cache types left to try out.")
def _do_link(self, from_info, to_info, link_method):
if self.exists(to_info):
raise DvcException("Link '{}' already exists!".format(to_info))
link_method(from_info, to_info)
if self.protected:
self.protect(to_info)
msg = "Created {}'{}': {} -> {}".format(
"protected " if self.protected else "",
self.cache_types[0],
from_info,
to_info,
)
logger.debug(msg)
def _save_file(self, path_info, checksum, save_link=True):
assert checksum
@@ -388,12 +452,19 @@ class RemoteBASE(object):
return False
def isfile(self, path_info):
raise NotImplementedError
"""Optional: Overwrite only if the remote has a way to distinguish
between a directory and a file.
"""
return True
def isdir(self, path_info):
"""Optional: Overwrite only if the remote has a way to distinguish
between a directory and a file.
"""
return False
def walk(self, path_info):
def walk_files(self, path_info):
"""Return a generator with `PathInfo`s to all the files"""
raise NotImplementedError
@staticmethod
@@ -416,7 +487,7 @@ class RemoteBASE(object):
def _save(self, path_info, checksum):
to_info = self.checksum_to_path_info(checksum)
logger.info("Saving '{}' to '{}'.".format(path_info, to_info))
logger.debug("Saving '{}' to '{}'.".format(path_info, to_info))
if self.isdir(path_info):
self._save_dir(path_info, checksum)
return
@@ -436,9 +507,6 @@ class RemoteBASE(object):
name = name or from_info.name
if not no_progress_bar:
progress.update_target(name, 0, None)
try:
self._upload(
from_info.fspath,
@@ -451,12 +519,17 @@ class RemoteBASE(object):
logger.exception(msg.format(from_info, to_info))
return 1 # 1 fail
if not no_progress_bar:
progress.finish_target(name)
return 0
def download(self, from_info, to_info, name=None, no_progress_bar=False):
def download(
self,
from_info,
to_info,
name=None,
no_progress_bar=False,
file_mode=None,
dir_mode=None,
):
if not hasattr(self, "_download"):
raise RemoteActionNotImplemented("download", self.scheme)
@@ -474,12 +547,7 @@ class RemoteBASE(object):
name = name or to_info.name
if not no_progress_bar:
# real progress is not always available,
# lets at least show start and finish
progress.update_target(name, 0, None)
makedirs(fspath_py35(to_info.parent), exist_ok=True)
makedirs(to_info.parent, exist_ok=True, mode=dir_mode)
tmp_file = tmp_fname(to_info)
try:
@@ -491,13 +559,17 @@ class RemoteBASE(object):
logger.exception(msg.format(from_info, to_info))
return 1 # 1 fail
move(tmp_file, fspath_py35(to_info))
if not no_progress_bar:
progress.finish_target(name)
move(tmp_file, to_info, mode=file_mode)
return 0
def open(self, path_info, mode="r", encoding=None):
if hasattr(self, "_generate_download_url"):
get_url = partial(self._generate_download_url, path_info)
return open_url(get_url, mode=mode, encoding=encoding)
raise RemoteActionNotImplemented("open", self.scheme)
def remove(self, path_info):
raise RemoteActionNotImplemented("remove", self.scheme)
@@ -508,11 +580,25 @@ class RemoteBASE(object):
def copy(self, from_info, to_info):
raise RemoteActionNotImplemented("copy", self.scheme)
def symlink(self, from_info, to_info):
raise RemoteActionNotImplemented("symlink", self.scheme)
def hardlink(self, from_info, to_info):
raise RemoteActionNotImplemented("hardlink", self.scheme)
def reflink(self, from_info, to_info):
raise RemoteActionNotImplemented("reflink", self.scheme)
def exists(self, path_info):
raise NotImplementedError
def path_to_checksum(self, path):
return "".join(self.path_cls(path).parts[-2:])
parts = self.path_cls(path).parts[-2:]
if not (len(parts) == 2 and parts[0] and len(parts[0]) == 2):
raise ValueError("Bad cache file path")
return "".join(parts)
def checksum_to_path_info(self, checksum):
return self.path_info / checksum[0:2] / checksum[2:]
@@ -525,18 +611,18 @@ class RemoteBASE(object):
# is 32 bytes, so ~3200Mb list) and we don't really need all of it at
# the same time, so it makes sense to use a generator to gradually
# iterate over it, without keeping all of it in memory.
return (
self.path_to_checksum(path) for path in self.list_cache_paths()
)
for path in self.list_cache_paths():
try:
yield self.path_to_checksum(path)
except ValueError:
# We ignore all the non-cache looking files
pass
def gc(self, cinfos):
used = self.extract_used_local_checksums(cinfos)
def gc(self, named_cache):
used = self.extract_used_local_checksums(named_cache)
if self.scheme != "":
used |= {
info[self.PARAM_CHECKSUM]
for info in cinfos.get(self.scheme, [])
}
used.update(named_cache[self.scheme])
removed = False
for checksum in self.all():
@@ -600,7 +686,7 @@ class RemoteBASE(object):
return self._changed_dir_cache(checksum)
return self.changed_cache_file(checksum)
def cache_exists(self, checksums, jobs=None):
def cache_exists(self, checksums, jobs=None, name=None):
"""Check if the given checksums are stored in the remote.
There are two ways of performing this check:
@@ -618,27 +704,31 @@ class RemoteBASE(object):
take much shorter time to just retrieve everything they have under
a certain prefix (e.g. s3, gs, ssh, hdfs). Other remotes that can
check if particular file exists much quicker, use their own
implementation of cache_exists (see http, local).
implementation of cache_exists (see ssh, local).
Returns:
A list with checksums that were found in the remote
"""
progress_callback = ProgressCallback(len(checksums))
if not self.no_traverse:
return list(set(checksums) & set(self.all()))
def exists_with_progress(chunks):
return self.batch_exists(chunks, callback=progress_callback)
with Tqdm(
desc="Querying "
+ ("cache in " + name if name else "remote cache"),
total=len(checksums),
unit="file",
) as pbar:
if self.no_traverse and hasattr(self, "batch_exists"):
with ThreadPoolExecutor(max_workers=jobs or self.JOBS) as executor:
path_infos = [self.checksum_to_path_info(x) for x in checksums]
chunks = to_chunks(path_infos, num_chunks=self.JOBS)
results = executor.map(exists_with_progress, chunks)
in_remote = itertools.chain.from_iterable(results)
ret = list(itertools.compress(checksums, in_remote))
progress_callback.finish("")
def exists_with_progress(path_info):
ret = self.exists(path_info)
pbar.update_desc(str(path_info))
return ret
return list(set(checksums) & set(self.all()))
with ThreadPoolExecutor(max_workers=jobs or self.JOBS) as executor:
path_infos = map(self.checksum_to_path_info, checksums)
in_remote = executor.map(exists_with_progress, path_infos)
ret = list(itertools.compress(checksums, in_remote))
return ret
def already_cached(self, path_info):
current = self.get_checksum(path_info)
@@ -664,22 +754,50 @@ class RemoteBASE(object):
self.remove(path_info)
def _checkout_file(
self, path_info, checksum, force, progress_callback=None
self,
path_info,
checksum,
force,
progress_callback=None,
save_link=True,
):
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)))
# NOTE: In case if path_info is already cached and path_info's
# link type matches cache link type, we would like to avoid
# relinking.
if self.changed(
path_info, {self.PARAM_CHECKSUM: checksum}
) or not self._link_matches(path_info):
self.safe_remove(path_info, force=force)
self.link(cache_info, path_info)
self.state.save_link(path_info)
self.state.save(path_info, checksum)
cache_info = self.checksum_to_path_info(checksum)
self.link(cache_info, path_info)
if save_link:
self.state.save_link(path_info)
self.state.save(path_info, checksum)
else:
# NOTE: performing (un)protection costs us +/- the same as checking
# if path_info is protected. Instead of implementing logic,
# just (un)protect according to self.protected.
if self.protected:
self.protect(path_info)
else:
# NOTE dont allow copy, because we checked before that link
# type matches cache, and we don't want data duplication
self.unprotect(path_info, allow_copy=False)
if progress_callback:
progress_callback.update(str(path_info))
progress_callback(str(path_info))
def _link_matches(self, path_info):
return True
def makedirs(self, path_info):
raise NotImplementedError
"""Optional: Implement only if the remote needs to create
directories before copying/linking/moving data
"""
pass
def _checkout_dir(
self, path_info, checksum, force, progress_callback=None
@@ -696,17 +814,14 @@ class RemoteBASE(object):
for entry in dir_info:
relative_path = entry[self.PARAM_RELPATH]
entry_checksum = entry[self.PARAM_CHECKSUM]
entry_cache_info = self.checksum_to_path_info(entry_checksum)
entry_info = path_info / relative_path
entry_checksum_info = {self.PARAM_CHECKSUM: entry_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)
self.state.save(entry_info, entry_checksum)
if progress_callback:
progress_callback.update(str(entry_info))
self._checkout_file(
entry_info,
entry_checksum,
force,
progress_callback,
save_link=False,
)
self._remove_redundant_files(path_info, dir_info, force)
@@ -714,11 +829,7 @@ class RemoteBASE(object):
self.state.save(path_info, checksum)
def _remove_redundant_files(self, path_info, dir_info, force):
existing_files = set(
path_info / root / fname
for root, _, files in self.walk(path_info)
for fname in files
)
existing_files = set(self.walk_files(path_info))
needed_files = {
path_info / entry[self.PARAM_RELPATH] for entry in dir_info
@@ -734,29 +845,33 @@ class RemoteBASE(object):
raise NotImplementedError
checksum = checksum_info.get(self.PARAM_CHECKSUM)
failed = None
if not checksum:
logger.warning(
"No checksum info found for '{}'. "
"It won't be created.".format(str(path_info))
)
self.safe_remove(path_info, force=force)
return
failed = path_info
if not self.changed(path_info, checksum_info):
msg = "Data '{}' didn't change."
logger.debug(msg.format(str(path_info)))
return
if self.changed_cache(checksum):
elif self.changed_cache(checksum):
msg = "Cache '{}' not found. File '{}' won't be created."
logger.warning(msg.format(checksum, str(path_info)))
self.safe_remove(path_info, force=force)
return
failed = path_info
if failed:
if progress_callback:
progress_callback(
str(path_info), self.get_files_number(checksum)
)
return failed
msg = "Checking out '{}' with cache '{}'."
logger.debug(msg.format(str(path_info), checksum))
return self._checkout(path_info, checksum, force, progress_callback)
self._checkout(path_info, checksum, force, progress_callback)
return None
def _checkout(
self, path_info, checksum, force=False, progress_callback=None
@@ -769,22 +884,29 @@ class RemoteBASE(object):
path_info, checksum, force, progress_callback=progress_callback
)
def get_files_number(self, checksum):
if not checksum:
return 0
if self.is_dir_checksum(checksum):
return len(self.get_dir_cache(checksum))
return 1
@staticmethod
def unprotect(path_info):
def unprotect(path_info, allow_copy=True):
pass
def _get_unpacked_dir_names(self, checksums):
return set()
def extract_used_local_checksums(self, cinfos):
from dvc.remote import RemoteLOCAL
used = {info[RemoteLOCAL.PARAM_CHECKSUM] for info in cinfos["local"]}
def extract_used_local_checksums(self, named_cache):
used = set(named_cache["local"])
unpacked = self._get_unpacked_dir_names(used)
return used | unpacked
def _changed_unpacked_dir(self, checksum):
return True
def _update_unpacked_dir(self, checksum, progress_callback=None):
def _update_unpacked_dir(self, checksum):
pass
+54 -23
View File
@@ -1,28 +1,60 @@
from __future__ import unicode_literals
import logging
import itertools
from datetime import timedelta
from functools import wraps
from funcy import cached_property
try:
from google.cloud import storage
except ImportError:
storage = None
from dvc.utils.compat import FileNotFoundError # skipcq: PYL-W0622
from dvc.remote.base import RemoteBASE
from dvc.config import Config
from dvc.exceptions import DvcException
from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
def dynamic_chunk_size(func):
@wraps(func)
def wrapper(*args, **kwargs):
import requests
from google.cloud.storage.blob import Blob, _DEFAULT_CHUNKSIZE
# Default chunk size for gs is 100M, which might be too much for
# particular network (see [1]). So if we are getting ConnectionError,
# we should try lowering the chunk size until we reach the minimum
# allowed chunk size of 256K. Also note that `chunk_size` must be a
# multiple of 256K per the API specification.
#
# [1] https://github.com/iterative/dvc/issues/2572
# skipcq: PYL-W0212
multiplier = int(_DEFAULT_CHUNKSIZE / Blob._CHUNK_SIZE_MULTIPLE)
while True:
try:
# skipcq: PYL-W0212
chunk_size = Blob._CHUNK_SIZE_MULTIPLE * multiplier
return func(*args, chunk_size=chunk_size, **kwargs)
except requests.exceptions.ConnectionError:
multiplier = int(multiplier / 2)
if not multiplier:
raise
return wrapper
@dynamic_chunk_size
def _upload_to_bucket(bucket, from_file, to_info, **kwargs):
blob = bucket.blob(to_info.path, **kwargs)
blob.upload_from_filename(from_file)
class RemoteGS(RemoteBASE):
scheme = Schemes.GS
path_cls = CloudURLInfo
REQUIRES = {"google.cloud.storage": storage}
REQUIRES = {"google-cloud-storage": "google.cloud.storage"}
PARAM_CHECKSUM = "md5"
def __init__(self, repo, config):
@@ -37,10 +69,12 @@ class RemoteGS(RemoteBASE):
@cached_property
def gs(self):
from google.cloud.storage import Client
return (
storage.Client.from_service_account_json(self.credentialpath)
Client.from_service_account_json(self.credentialpath)
if self.credentialpath
else storage.Client(self.projectname)
else Client(self.projectname)
)
def get_file_checksum(self, path_info):
@@ -89,23 +123,20 @@ class RemoteGS(RemoteBASE):
paths = set(self._list_paths(path_info.bucket, path_info.path))
return any(path_info.path == path for path in paths)
def batch_exists(self, path_infos, callback):
paths = []
for path_info in path_infos:
paths.append(self._list_paths(path_info.bucket, path_info.path))
callback.update(str(path_info))
paths = set(itertools.chain.from_iterable(paths))
return [path_info.path in paths for path_info in path_infos]
def _upload(self, from_file, to_info, **_kwargs):
bucket = self.gs.bucket(to_info.bucket)
blob = bucket.blob(to_info.path)
blob.upload_from_filename(from_file)
_upload_to_bucket(bucket, from_file, to_info)
def _download(self, from_info, to_file, **_kwargs):
bucket = self.gs.bucket(from_info.bucket)
blob = bucket.get_blob(from_info.path)
blob.download_to_filename(to_file)
def _generate_download_url(self, path_info, expires=3600):
expiration = timedelta(seconds=int(expires))
bucket = self.gs.bucket(path_info.bucket)
blob = bucket.get_blob(path_info.path)
if blob is None:
raise FileNotFoundError
return blob.generate_signed_url(expiration=expiration)
+27 -15
View File
@@ -2,20 +2,16 @@ from __future__ import unicode_literals
import os
import re
import getpass
import io
import posixpath
import logging
from subprocess import Popen, PIPE
try:
import pyarrow
except ImportError:
pyarrow = None
from contextlib import contextmanager, closing
from dvc.config import Config
from dvc.scheme import Schemes
from dvc.utils.compat import urlparse
from dvc.utils.compat import urlparse, FileNotFoundError
from dvc.utils import fix_env, tmp_fname
from .pool import get_connection
@@ -28,7 +24,7 @@ class RemoteHDFS(RemoteBASE):
scheme = Schemes.HDFS
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
PARAM_CHECKSUM = "checksum"
REQUIRES = {"pyarrow": pyarrow}
REQUIRES = {"pyarrow": "pyarrow"}
def __init__(self, repo, config):
super(RemoteHDFS, self).__init__(repo, config)
@@ -39,11 +35,7 @@ class RemoteHDFS(RemoteBASE):
parsed = urlparse(url)
user = (
parsed.username
or config.get(Config.SECTION_REMOTE_USER)
or getpass.getuser()
)
user = parsed.username or config.get(Config.SECTION_REMOTE_USER)
self.path_info = self.path_cls.from_parts(
scheme=self.scheme,
@@ -53,8 +45,9 @@ class RemoteHDFS(RemoteBASE):
path=parsed.path,
)
@staticmethod
def hdfs(path_info):
def hdfs(self, path_info):
import pyarrow
return get_connection(
pyarrow.hdfs.connect,
path_info.host,
@@ -139,6 +132,25 @@ class RemoteHDFS(RemoteBASE):
with open(to_file, "wb+") as fobj:
hdfs.download(from_info.path, fobj)
@contextmanager
def open(self, path_info, mode="r", encoding=None):
assert mode in {"r", "rt", "rb"}
try:
with self.hdfs(path_info) as hdfs, closing(
hdfs.open(path_info.path, mode="rb")
) as fd:
if mode == "rb":
yield fd
else:
yield io.TextIOWrapper(fd, encoding=encoding)
except IOError as e:
# Empty .errno and not specific enough error class in pyarrow,
# see https://issues.apache.org/jira/browse/ARROW-6248
if "file does not exist" in str(e):
raise FileNotFoundError(*e.args)
raise
def list_cache_paths(self):
if not self.exists(self.path_info):
return []
+58 -55
View File
@@ -1,37 +1,23 @@
from __future__ import unicode_literals
from dvc.scheme import Schemes
from dvc.utils.compat import open
import threading
import requests
import logging
from dvc.progress import progress
from dvc.exceptions import DvcException
from dvc.config import Config
from dvc.remote.base import RemoteBASE
from funcy import cached_property
from dvc.scheme import Schemes
from dvc.utils.compat import open
from dvc.progress import Tqdm
from dvc.exceptions import DvcException
from dvc.config import Config, ConfigError
from dvc.remote.base import RemoteBASE
logger = logging.getLogger(__name__)
class ProgressBarCallback(object):
def __init__(self, name, total):
self.name = name
self.total = total
self.current = 0
self.lock = threading.Lock()
def __call__(self, byts):
with self.lock:
self.current += byts
progress.update_target(self.name, self.current, self.total)
class RemoteHTTP(RemoteBASE):
scheme = Schemes.HTTP
SESSION_RETRIES = 5
SESSION_BACKOFF_FACTOR = 0.1
REQUEST_TIMEOUT = 10
CHUNK_SIZE = 2 ** 16
PARAM_CHECKSUM = "etag"
@@ -42,46 +28,43 @@ class RemoteHTTP(RemoteBASE):
url = config.get(Config.SECTION_REMOTE_URL)
self.path_info = self.path_cls(url) if url else None
if not self.no_traverse:
raise ConfigError(
"HTTP doesn't support traversing the remote to list existing "
"files. Use: `dvc remote modify <name> no_traverse true`"
)
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
callback = None
if not no_progress_bar:
total = self._content_length(from_info.url)
if total:
callback = ProgressBarCallback(name, total)
request = self._request("GET", from_info.url, stream=True)
with open(to_file, "wb") as fd:
transferred_bytes = 0
for chunk in request.iter_content(chunk_size=self.CHUNK_SIZE):
fd.write(chunk)
fd.flush()
transferred_bytes += len(chunk)
if callback:
callback(transferred_bytes)
with Tqdm(
total=None if no_progress_bar else self._content_length(from_info),
leave=False,
bytes=True,
desc=from_info.url if name is None else name,
disable=no_progress_bar,
) as pbar:
with open(to_file, "wb") as fd:
for chunk in request.iter_content(chunk_size=self.CHUNK_SIZE):
fd.write(chunk)
fd.flush()
pbar.update(len(chunk))
def exists(self, path_info):
return bool(self._request("HEAD", path_info.url))
def batch_exists(self, path_infos, callback):
results = []
for path_info in path_infos:
results.append(self.exists(path_info))
callback.update(str(path_info))
return results
def _content_length(self, url):
return self._request("HEAD", url).headers.get("Content-Length")
def _content_length(self, url_or_request):
headers = getattr(
url_or_request,
"headers",
self._request("HEAD", url_or_request).headers,
)
res = headers.get("Content-Length")
return int(res) if res else None
def get_file_checksum(self, path_info):
url = path_info.url
etag = self._request("HEAD", url).headers.get("ETag") or self._request(
"HEAD", url
).headers.get("Content-MD5")
headers = self._request("HEAD", url).headers
etag = headers.get("ETag") or headers.get("Content-MD5")
if not etag:
raise DvcException(
@@ -97,12 +80,32 @@ class RemoteHTTP(RemoteBASE):
return etag
@cached_property
def _session(self):
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retries = Retry(
total=self.SESSION_RETRIES,
backoff_factor=self.SESSION_BACKOFF_FACTOR,
)
session.mount("http://", HTTPAdapter(max_retries=retries))
session.mount("https://", HTTPAdapter(max_retries=retries))
return session
def _request(self, method, url, **kwargs):
import requests
kwargs.setdefault("allow_redirects", True)
kwargs.setdefault("timeout", self.REQUEST_TIMEOUT)
try:
return requests.request(method, url, **kwargs)
return self._session.request(method, url, **kwargs)
except requests.exceptions.RequestException:
raise DvcException("could not perform a {} request".format(method))
@@ -1,17 +1,14 @@
from __future__ import unicode_literals
from copy import copy
from dvc.scheme import Schemes
from dvc.remote.local.slow_link_detection import slow_link_guard
from dvc.utils.compat import str, makedirs, fspath_py35
import os
import stat
from shortuuid import uuid
import shutil
import errno
import logging
from functools import partial
from shortuuid import uuid
from dvc.scheme import Schemes
from dvc.utils.compat import str, fspath_py35, open
from dvc.system import System
from dvc.remote.base import (
RemoteBASE,
@@ -28,12 +25,11 @@ from dvc.utils import (
file_md5,
walk_files,
relpath,
dvc_walk,
makedirs,
)
from dvc.config import Config
from dvc.exceptions import DvcException
from dvc.progress import progress
from concurrent.futures import ThreadPoolExecutor
from dvc.exceptions import DvcException, DownloadError, UploadError
from dvc.progress import Tqdm, TqdmThreadPoolExecutor
from dvc.path_info import PathInfo
@@ -49,24 +45,19 @@ class RemoteLOCAL(RemoteBASE):
UNPACKED_DIR_SUFFIX = ".unpacked"
DEFAULT_CACHE_TYPES = ["reflink", "copy"]
CACHE_TYPE_MAP = {
"copy": shutil.copyfile,
"symlink": System.symlink,
"hardlink": System.hardlink,
"reflink": System.reflink,
}
SHARED_MODE_MAP = {None: (0o644, 0o755), "group": (0o664, 0o775)}
def __init__(self, repo, config):
super(RemoteLOCAL, self).__init__(repo, config)
self.protected = config.get(Config.SECTION_CACHE_PROTECTED, False)
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)
shared = config.get(Config.SECTION_CACHE_SHARED)
self._file_mode, self._dir_mode = self.SHARED_MODE_MAP[shared]
if self.protected:
# cache files are set to be read-only for everyone
self._file_mode = stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
# A clunky way to detect cache dir
storagepath = config.get(Config.SECTION_LOCAL_STORAGEPATH, None)
@@ -76,9 +67,6 @@ class RemoteLOCAL(RemoteBASE):
cwd = config[Config.PRIVATE_CWD]
cache_dir = os.path.abspath(os.path.join(cwd, cache_dir))
if cache_dir is not None and not os.path.exists(cache_dir):
os.mkdir(cache_dir)
self.path_info = PathInfo(cache_dir) if cache_dir else None
self._dir_info = {}
@@ -114,81 +102,13 @@ class RemoteLOCAL(RemoteBASE):
return self.checksum_to_path_info(md5).url
def exists(self, path_info):
@staticmethod
def exists(path_info):
assert path_info.scheme == "local"
return os.path.lexists(fspath_py35(path_info))
def makedirs(self, path_info):
if not self.exists(path_info):
os.makedirs(fspath_py35(path_info))
def link(self, from_info, to_info, link_type=None):
from_path = from_info.fspath
to_path = to_info.fspath
assert os.path.isfile(from_path)
dname = os.path.dirname(to_path)
if not os.path.exists(dname):
os.makedirs(dname)
# NOTE: just create an empty file for an empty cache
if os.path.getsize(from_path) == 0:
open(to_path, "w+").close()
msg = "Created empty file: {} -> {}".format(from_path, to_path)
logger.debug(msg)
return
if not link_type:
link_types = self.cache_types
else:
link_types = [link_type]
self._try_links(from_info, to_info, link_types)
@classmethod
def _get_link_method(cls, link_type):
try:
return cls.CACHE_TYPE_MAP[link_type]
except KeyError:
raise DvcException(
"Cache type: '{}' not supported!".format(link_type)
)
def _link(self, from_info, to_info, link_method):
if self.exists(to_info):
raise DvcException("Link '{}' already exists!".format(to_info))
else:
link_method(from_info.fspath, to_info.fspath)
if self.protected:
self.protect(to_info)
msg = "Created {}'{}': {} -> {}".format(
"protected " if self.protected else "",
self.cache_types[0],
from_info,
to_info,
)
logger.debug(msg)
@slow_link_guard
def _try_links(self, from_info, to_info, link_types):
i = len(link_types)
while i > 0:
link_method = self._get_link_method(link_types[0])
try:
self._link(from_info, to_info, link_method)
return
except DvcException as exc:
msg = "Cache type '{}' is not supported: {}"
logger.debug(msg.format(link_types[0], str(exc)))
del link_types[0]
i -= 1
raise DvcException("no possible cache types left to try out.")
makedirs(path_info, exist_ok=True, mode=self._dir_mode)
def already_cached(self, path_info):
assert path_info.scheme in ["", "local"]
@@ -211,14 +131,21 @@ class RemoteLOCAL(RemoteBASE):
return False
def isfile(self, path_info):
@staticmethod
def isfile(path_info):
return os.path.isfile(fspath_py35(path_info))
def isdir(self, path_info):
@staticmethod
def isdir(path_info):
return os.path.isdir(fspath_py35(path_info))
def walk(self, path_info):
return dvc_walk(path_info, self.repo.dvcignore)
@staticmethod
def getsize(path_info):
return os.path.getsize(fspath_py35(path_info))
def walk_files(self, path_info):
for fname in walk_files(path_info, self.repo.dvcignore):
yield PathInfo(fname)
def get_file_checksum(self, path_info):
return file_md5(fspath_py35(path_info))[0]
@@ -234,20 +161,62 @@ class RemoteLOCAL(RemoteBASE):
if from_info.scheme != "local" or to_info.scheme != "local":
raise NotImplementedError
inp = from_info.fspath
outp = to_info.fspath
self.makedirs(to_info.parent)
# 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()))
move(inp, tmp)
move(tmp, outp)
if self.isfile(from_info):
mode = self._file_mode
else:
mode = self._dir_mode
def cache_exists(self, md5s, jobs=None):
move(from_info, to_info, mode=mode)
@staticmethod
def copy(from_info, to_info):
System.copy(from_info, to_info)
@staticmethod
def symlink(from_info, to_info):
System.symlink(from_info, to_info)
def hardlink(self, from_info, to_info):
# If there are a lot of empty files (which happens a lot in datasets),
# and the cache type is `hardlink`, we might reach link limits and
# will get something like: `too many links error`
#
# This is because all those empty files will have the same checksum
# (i.e. 68b329da9893e34099c7d8ad5cb9c940), therfore, they will be
# linked to the same file in the cache.
#
# From https://en.wikipedia.org/wiki/Hard_link
# * ext4 limits the number of hard links on a file to 65,000
# * Windows with NTFS has a limit of 1024 hard links on a file
#
# That's why we simply create an empty file rather than a link.
if self.getsize(from_info) == 0:
self.open(to_info, "w").close()
logger.debug(
"Created empty file: {src} -> {dest}".format(
src=str(from_info), dest=str(to_info)
)
)
return
System.hardlink(from_info, to_info)
@staticmethod
def reflink(from_info, to_info):
System.reflink(from_info, to_info)
def cache_exists(self, checksums, jobs=None, name=None):
return [
checksum
for checksum in progress(md5s)
for checksum in Tqdm(
checksums,
unit="file",
desc="Querying "
+ ("cache in " + name if name else "local cache"),
)
if not self.changed_cache_file(checksum)
]
@@ -272,44 +241,25 @@ class RemoteLOCAL(RemoteBASE):
name=name,
)
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["name"]
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
@staticmethod
def open(path_info, mode="r", encoding=None):
return open(fspath_py35(path_info), mode=mode, encoding=encoding)
def status(
self,
checksum_infos,
named_cache,
remote,
jobs=None,
show_checksums=False,
download=False,
):
logger.info(
logger.debug(
"Preparing to collect status from {}".format(remote.path_info)
)
ret = self._group(checksum_infos, show_checksums=show_checksums) or {}
md5s = list(ret)
md5s = list(named_cache[self.scheme])
logger.info("Collecting information from local cache...")
local_exists = self.cache_exists(md5s, jobs=jobs)
logger.debug("Collecting information from local cache...")
local_exists = self.cache_exists(md5s, jobs=jobs, name=self.cache_dir)
# This is a performance optimization. We can safely assume that,
# if the resources that we want to fetch are already cached,
@@ -318,16 +268,25 @@ class RemoteLOCAL(RemoteBASE):
if download and sorted(local_exists) == sorted(md5s):
remote_exists = local_exists
else:
logger.info("Collecting information from remote cache...")
remote_exists = list(remote.cache_exists(md5s, jobs=jobs))
logger.debug("Collecting information from remote cache...")
remote_exists = list(
remote.cache_exists(
md5s, jobs=jobs, name=str(remote.path_info)
)
)
ret = {
checksum: {"name": checksum if show_checksums else " ".join(names)}
for checksum, names in named_cache[self.scheme].items()
}
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):
@staticmethod
def _fill_statuses(checksum_info_dir, local_exists, remote_exists):
# Using sets because they are way faster for lookups
local = set(local_exists)
remote = set(remote_exists)
@@ -340,8 +299,8 @@ class RemoteLOCAL(RemoteBASE):
cache = []
path_infos = []
names = []
for md5, info in progress(
status_info.items(), name="Analysing status"
for md5, info in Tqdm(
status_info.items(), desc="Analysing status", unit="file"
):
if info["status"] == status:
cache.append(self.checksum_to_path_info(md5))
@@ -359,13 +318,13 @@ class RemoteLOCAL(RemoteBASE):
def _process(
self,
checksum_infos,
named_cache,
remote,
jobs=None,
show_checksums=False,
download=False,
):
logger.info(
logger.debug(
"Preparing to {} '{}'".format(
"download data from" if download else "upload data to",
remote.path_info,
@@ -373,7 +332,11 @@ class RemoteLOCAL(RemoteBASE):
)
if download:
func = remote.download
func = partial(
remote.download,
dir_mode=self._dir_mode,
file_mode=self._file_mode,
)
status = STATUS_DELETED
else:
func = remote.upload
@@ -383,7 +346,7 @@ class RemoteLOCAL(RemoteBASE):
jobs = remote.JOBS
status_info = self.status(
checksum_infos,
named_cache,
remote,
jobs=jobs,
show_checksums=show_checksums,
@@ -396,38 +359,38 @@ class RemoteLOCAL(RemoteBASE):
return 0
if jobs > 1:
with ThreadPoolExecutor(max_workers=jobs) as executor:
with TqdmThreadPoolExecutor(max_workers=jobs) as executor:
fails = sum(executor.map(func, *plans))
else:
fails = sum(map(func, *plans))
if fails:
msg = "{} files failed to {}"
raise DvcException(
msg.format(fails, "download" if download else "upload")
)
if download:
raise DownloadError(fails)
raise UploadError(fails)
return len(plans[0])
def push(self, checksum_infos, remote, jobs=None, show_checksums=False):
def push(self, named_cache, remote, jobs=None, show_checksums=False):
return self._process(
checksum_infos,
named_cache,
remote,
jobs=jobs,
show_checksums=show_checksums,
download=False,
)
def pull(self, checksum_infos, remote, jobs=None, show_checksums=False):
def pull(self, named_cache, remote, jobs=None, show_checksums=False):
return self._process(
checksum_infos,
named_cache,
remote,
jobs=jobs,
show_checksums=show_checksums,
download=True,
)
def _log_missing_caches(self, checksum_info_dict):
@staticmethod
def _log_missing_caches(checksum_info_dict):
missing_caches = [
(md5, info)
for md5, info in checksum_info_dict.items()
@@ -435,10 +398,8 @@ class RemoteLOCAL(RemoteBASE):
]
if missing_caches:
missing_desc = "".join(
[
"\nname: {}, md5: {}".format(info["name"], md5)
for md5, info in missing_caches
]
"\nname: {}, md5: {}".format(info["name"], md5)
for md5, info in missing_caches
)
msg = (
"Some of the cache files do not exist neither locally "
@@ -447,19 +408,22 @@ class RemoteLOCAL(RemoteBASE):
logger.warning(msg)
@staticmethod
def _unprotect_file(path):
def _unprotect_file(path, allow_copy=True):
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()))
if allow_copy:
logger.debug("Unprotecting '{}'".format(path))
tmp = os.path.join(os.path.dirname(path), "." + str(uuid()))
# The operations order is important here - if some application
# would access the file during the process of copyfile then it
# would get only the part of file. So, at first, the file should be
# copied with the temporary name, and then original file should be
# replaced by new.
copyfile(path, tmp, name="Unprotecting '{}'".format(relpath(path)))
remove(path)
os.rename(tmp, path)
# 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(relpath(path))
)
remove(path)
os.rename(tmp, path)
else:
logger.debug(
@@ -469,11 +433,11 @@ class RemoteLOCAL(RemoteBASE):
os.chmod(path, os.stat(path).st_mode | stat.S_IWRITE)
def _unprotect_dir(self, path):
for path in walk_files(path, self.repo.dvcignore):
RemoteLOCAL._unprotect_file(path)
def _unprotect_dir(self, path, allow_copy=True):
for fname in walk_files(path, self.repo.dvcignore):
self._unprotect_file(fname, allow_copy)
def unprotect(self, path_info):
def unprotect(self, path_info, allow_copy=True):
path = path_info.fspath
if not os.path.exists(path):
raise DvcException(
@@ -481,15 +445,26 @@ class RemoteLOCAL(RemoteBASE):
)
if os.path.isdir(path):
self._unprotect_dir(path)
self._unprotect_dir(path, allow_copy)
else:
RemoteLOCAL._unprotect_file(path)
self._unprotect_file(path, allow_copy)
@staticmethod
def protect(path_info):
os.chmod(
fspath_py35(path_info), stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
)
path = fspath_py35(path_info)
mode = stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
try:
os.chmod(path, mode)
except OSError as exc:
# In share cache scenario, we might not own the cache file, so we
# need to check if cache file is already protected.
if exc.errno not in [errno.EPERM, errno.EACCES]:
raise
actual = os.stat(path).st_mode
if actual & mode != mode:
raise
def _get_unpacked_dir_path_info(self, checksum):
info = self.checksum_to_path_info(checksum)
@@ -519,13 +494,18 @@ class RemoteLOCAL(RemoteBASE):
def _create_unpacked_dir(self, checksum, dir_info, unpacked_dir_info):
self.makedirs(unpacked_dir_info)
for entry in progress(dir_info, name="Created unpacked dir"):
for entry in Tqdm(dir_info, desc="Creating unpacked dir", unit="file"):
entry_cache_info = self.checksum_to_path_info(
entry[self.PARAM_CHECKSUM]
)
relative_path = entry[self.PARAM_RELPATH]
self.link(
entry_cache_info, unpacked_dir_info / relative_path, "hardlink"
# In shared cache mode some cache files might not be owned by the
# user, so we need to use symlinks because, unless
# /proc/sys/fs/protected_hardlinks is disabled, the user is not
# allowed to create hardlinks to files that he doesn't own.
link_types = ["hardlink", "symlink"]
self._link(
entry_cache_info, unpacked_dir_info / relative_path, link_types
)
self.state.save(unpacked_dir_info, checksum)
@@ -541,3 +521,36 @@ class RemoteLOCAL(RemoteBASE):
if self.is_dir_checksum(c):
unpacked.add(c + self.UNPACKED_DIR_SUFFIX)
return unpacked
def _get_cache_type(self, path_info):
if self.cache_type_confirmed:
return self.cache_types[0]
workspace_file = path_info.with_name("." + uuid())
test_cache_file = self.path_info / ".cache_type_test_file"
if not self.exists(test_cache_file):
with open(fspath_py35(test_cache_file), "wb") as fobj:
fobj.write(bytes(1))
try:
self.link(test_cache_file, workspace_file)
finally:
self.remove(workspace_file)
self.remove(test_cache_file)
self.cache_type_confirmed = True
return self.cache_types[0]
def _link_matches(self, path_info):
is_hardlink = System.is_hardlink(path_info)
is_symlink = System.is_symlink(path_info)
is_copy_or_reflink = not is_hardlink and not is_symlink
cache_type = self._get_cache_type(path_info)
if cache_type == "symlink":
return is_symlink
if cache_type == "hardlink":
return is_hardlink
return is_copy_or_reflink
-62
View File
@@ -1,62 +0,0 @@
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 by using a different"
" `cache.type` configuration.\nSee "
"{blue}https://dvc.org/doc/commands-reference/config#cache{"
"reset} for more information.\n"
"To disable this message, run:\n"
"'dvc config cache.slow_link_warning false'".format(
blue=colorama.Fore.BLUE, reset=colorama.Fore.RESET
)
)
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
+23 -15
View File
@@ -6,14 +6,9 @@ import logging
from dvc.scheme import Schemes
try:
import oss2
except ImportError:
oss2 = None
from dvc.config import Config
from dvc.remote.base import RemoteBASE
from dvc.remote.azure import Callback
from dvc.progress import Tqdm
from dvc.path_info import CloudURLInfo
@@ -41,7 +36,7 @@ class RemoteOSS(RemoteBASE):
scheme = Schemes.OSS
path_cls = CloudURLInfo
REQUIRES = {"oss2": oss2}
REQUIRES = {"oss2": "oss2"}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
@@ -71,6 +66,8 @@ class RemoteOSS(RemoteBASE):
@property
def oss_service(self):
import oss2
if self._bucket is None:
logger.debug("URL {}".format(self.path_info))
logger.debug("key id {}".format(self.key_id))
@@ -98,6 +95,8 @@ class RemoteOSS(RemoteBASE):
self.oss_service.delete_object(path_info.path)
def _list_paths(self, prefix):
import oss2
for blob in oss2.ObjectIterator(self.oss_service, prefix=prefix):
yield blob.key
@@ -107,15 +106,24 @@ class RemoteOSS(RemoteBASE):
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
cb = None if no_progress_bar else Callback(name)
self.oss_service.put_object_from_file(
to_info.path, from_file, progress_callback=cb
)
with Tqdm(desc=name, disable=no_progress_bar, bytes=True) as pbar:
self.oss_service.put_object_from_file(
to_info.path, from_file, progress_callback=pbar.update_to
)
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
):
cb = None if no_progress_bar else Callback(name)
self.oss_service.get_object_to_file(
from_info.path, to_file, progress_callback=cb
)
with Tqdm(desc=name, disable=no_progress_bar, bytes=True) as pbar:
self.oss_service.get_object_to_file(
from_info.path, to_file, progress_callback=pbar.update_to
)
def _generate_download_url(self, path_info, expires=3600):
assert path_info.bucket == self.path_info.bucket
return self.oss_service.sign_url("GET", path_info.path, expires)
def exists(self, path_info):
paths = self._list_paths(path_info.path)
return any(path_info.path == path for path in paths)
+80 -55
View File
@@ -1,17 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
import threading
import logging
import itertools
import posixpath
from funcy import cached_property
try:
import boto3
except ImportError:
boto3 = None
from dvc.progress import progress
from dvc.progress import Tqdm
from dvc.config import Config
from dvc.remote.base import RemoteBASE
from dvc.exceptions import DvcException, ETagMismatchError
@@ -21,23 +17,10 @@ from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
class Callback(object):
def __init__(self, name, total):
self.name = name
self.total = total
self.current = 0
self.lock = threading.Lock()
def __call__(self, byts):
with self.lock:
self.current += byts
progress.update_target(self.name, self.current, self.total)
class RemoteS3(RemoteBASE):
scheme = Schemes.S3
path_cls = CloudURLInfo
REQUIRES = {"boto3": boto3}
REQUIRES = {"boto3": "boto3"}
PARAM_CHECKSUM = "etag"
def __init__(self, repo, config):
@@ -56,7 +39,10 @@ class RemoteS3(RemoteBASE):
self.endpoint_url = config.get(Config.SECTION_AWS_ENDPOINT_URL)
self.list_objects = config.get(Config.SECTION_AWS_LIST_OBJECTS)
if config.get(Config.SECTION_AWS_LIST_OBJECTS):
self.list_objects_api = "list_objects"
else:
self.list_objects_api = "list_objects_v2"
self.use_ssl = config.get(Config.SECTION_AWS_USE_SSL, True)
@@ -66,12 +52,18 @@ class RemoteS3(RemoteBASE):
if self.sse:
self.extra_args["ServerSideEncryption"] = self.sse
self.acl = config.get(Config.SECTION_AWS_ACL, "")
if self.acl:
self.extra_args["ACL"] = self.acl
shared_creds = config.get(Config.SECTION_AWS_CREDENTIALPATH)
if shared_creds:
os.environ.setdefault("AWS_SHARED_CREDENTIALS_FILE", shared_creds)
@cached_property
def s3(self):
import boto3
session = boto3.session.Session(
profile_name=self.profile, region_name=self.region
)
@@ -194,58 +186,91 @@ class RemoteS3(RemoteBASE):
logger.debug("Removing {}".format(path_info))
self.s3.delete_object(Bucket=path_info.bucket, Key=path_info.path)
def _list_paths(self, bucket, prefix):
def _list_objects(self, path_info, max_items=None):
""" Read config for list object api, paginate through list objects."""
kwargs = {"Bucket": bucket, "Prefix": prefix}
if self.list_objects:
list_objects_api = "list_objects"
else:
list_objects_api = "list_objects_v2"
paginator = self.s3.get_paginator(list_objects_api)
kwargs = {
"Bucket": path_info.bucket,
"Prefix": path_info.path,
"PaginationConfig": {"MaxItems": max_items},
}
paginator = self.s3.get_paginator(self.list_objects_api)
for page in paginator.paginate(**kwargs):
contents = page.get("Contents", None)
if not contents:
continue
for item in contents:
yield item["Key"]
yield item
def _list_paths(self, path_info, max_items=None):
return (
item["Key"] for item in self._list_objects(path_info, max_items)
)
def list_cache_paths(self):
return self._list_paths(self.path_info.bucket, self.path_info.path)
return self._list_paths(self.path_info)
def exists(self, path_info):
paths = self._list_paths(path_info.bucket, path_info.path)
return any(path_info.path == path for path in paths)
dir_path = path_info / ""
fname = next(self._list_paths(path_info, max_items=1), "")
return path_info.path == fname or fname.startswith(dir_path.path)
def batch_exists(self, path_infos, callback):
paths = []
for path_info in path_infos:
paths.append(self._list_paths(path_info.bucket, path_info.path))
callback.update(str(path_info))
paths = set(itertools.chain.from_iterable(paths))
return [path_info.path in paths for path_info in path_infos]
def isdir(self, path_info):
# S3 doesn't have a concept for directories.
#
# Using `head_object` with a path pointing to a directory
# will throw a 404 error.
#
# A reliable way to know if a given path is a directory is by
# checking if there are more files sharing the same prefix
# with a `list_objects` call.
#
# We need to make sure that the path ends with a forward slash,
# since we can end with false-positives like the following example:
#
# bucket
# └── data
# ├── alice
# └── alpha
#
# Using `data/al` as prefix will return `[data/alice, data/alpha]`,
# While `data/al/` will return nothing.
#
dir_path = path_info / ""
return bool(list(self._list_paths(dir_path, max_items=1)))
def _upload(self, from_file, to_info, name=None, no_progress_bar=False):
total = os.path.getsize(from_file)
cb = None if no_progress_bar else Callback(name, total)
self.s3.upload_file(
from_file,
to_info.bucket,
to_info.path,
Callback=cb,
ExtraArgs=self.extra_args,
)
with Tqdm(
disable=no_progress_bar, total=total, bytes=True, desc=name
) as pbar:
self.s3.upload_file(
from_file,
to_info.bucket,
to_info.path,
Callback=pbar.update,
ExtraArgs=self.extra_args,
)
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
if no_progress_bar:
cb = None
total = None
else:
total = self.s3.head_object(
Bucket=from_info.bucket, Key=from_info.path
)["ContentLength"]
cb = Callback(name, total)
with Tqdm(
disable=no_progress_bar, total=total, bytes=True, desc=name
) as pbar:
self.s3.download_file(
from_info.bucket, from_info.path, to_file, Callback=pbar.update
)
self.s3.download_file(
from_info.bucket, from_info.path, to_file, Callback=cb
def _generate_download_url(self, path_info, expires=3600):
params = {"Bucket": path_info.bucket, "Key": path_info.path}
return self.s3.generate_presigned_url(
ClientMethod="get_object", Params=params, ExpiresIn=int(expires)
)
def walk_files(self, path_info, max_items=None):
for fname in self._list_paths(path_info, max_items):
yield path_info / posixpath.relpath(fname, path_info.path)
+50
View File
@@ -0,0 +1,50 @@
import logging
import time
import sys
from functools import wraps
import colorama
from dvc.config import Config
logger = logging.getLogger(__name__)
this = sys.modules[__name__]
this.timeout_seconds = 10.0
this.already_displayed = False
this.message = (
"You can cut execution time considerably by using a different "
"`cache.type` configuration.\n"
"See {blue}https://dvc.org/doc/commands-reference/config#cache{reset} "
"for more information.\n"
"To disable this message, run:\n"
"`dvc config cache.slow_link_warning false`".format(
blue=colorama.Fore.BLUE, reset=colorama.Fore.RESET
)
)
def slow_link_guard(f):
@wraps(f)
def wrapper(remote, *args, **kwargs):
if this.already_displayed:
return f(remote, *args, **kwargs)
config = remote.repo.config.config.get(Config.SECTION_CACHE, {})
cache_type = config.get(Config.SECTION_CACHE_TYPE)
should_warn = config.get(Config.SECTION_CACHE_SLOW_LINK_WARNING, True)
if not should_warn or cache_type:
return f(remote, *args, **kwargs)
start = time.time()
result = f(remote, *args, **kwargs)
delta = time.time() - start
if delta >= this.timeout_seconds:
logger.warning(this.message)
this.already_displayed = True
return result
return wrapper
+142 -51
View File
@@ -1,17 +1,15 @@
from __future__ import unicode_literals
import errno
import itertools
import io
import os
import posixpath
import getpass
import logging
import itertools
import errno
from concurrent.futures import ThreadPoolExecutor
import threading
try:
import paramiko
except ImportError:
paramiko = None
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager, closing
import dvc.prompt as prompt
from dvc.config import Config
@@ -20,9 +18,7 @@ from dvc.utils.compat import urlparse, StringIO
from dvc.remote.base import RemoteBASE
from dvc.scheme import Schemes
from dvc.remote.pool import get_connection
from .connection import SSHConnection
from dvc.progress import Tqdm
logger = logging.getLogger(__name__)
@@ -33,7 +29,7 @@ saved_passwords_lock = threading.Lock()
class RemoteSSH(RemoteBASE):
scheme = Schemes.SSH
REQUIRES = {"paramiko": paramiko}
REQUIRES = {"paramiko": "paramiko"}
JOBS = 4
PARAM_CHECKSUM = "md5"
@@ -44,9 +40,10 @@ class RemoteSSH(RemoteBASE):
# We use conservative setting of 4 instead to not exhaust max sessions.
CHECKSUM_JOBS = 4
DEFAULT_CACHE_TYPES = ["copy"]
def __init__(self, repo, config):
super(RemoteSSH, self).__init__(repo, config)
url = config.get(Config.SECTION_REMOTE_URL)
if url:
parsed = urlparse(url)
@@ -84,6 +81,7 @@ class RemoteSSH(RemoteBASE):
self.ask_password = config.get(
Config.SECTION_REMOTE_ASK_PASSWORD, False
)
self.gss_auth = config.get(Config.SECTION_REMOTE_GSS_AUTH, False)
@staticmethod
def ssh_config_filename():
@@ -91,8 +89,10 @@ class RemoteSSH(RemoteBASE):
@staticmethod
def _load_user_ssh_config(hostname):
import paramiko
user_config_file = RemoteSSH.ssh_config_filename()
user_ssh_config = dict()
user_ssh_config = {}
if hostname and os.path.exists(user_config_file):
ssh_config = paramiko.SSHConfig()
with open(user_config_file) as f:
@@ -116,9 +116,10 @@ class RemoteSSH(RemoteBASE):
return identity_file[0]
return None
def ssh(self, path_info):
def ensure_credentials(self, path_info=None):
if path_info is None:
path_info = self.path_info
host, user, port = path_info.host, path_info.user, path_info.port
# NOTE: we use the same password regardless of the server :(
if self.ask_password and self.password is None:
with saved_passwords_lock:
@@ -134,48 +135,26 @@ class RemoteSSH(RemoteBASE):
)
self.password = password
def ssh(self, path_info):
self.ensure_credentials(path_info)
from .connection import SSHConnection
return get_connection(
SSHConnection,
host,
username=user,
port=port,
path_info.host,
username=path_info.user,
port=path_info.port,
key_filename=self.keyfile,
timeout=self.timeout,
password=self.password,
gss_auth=self.gss_auth,
)
def exists(self, path_info):
with self.ssh(path_info) as ssh:
return ssh.exists(path_info.path)
def batch_exists(self, path_infos, callback):
def _exists(chunk_and_channel):
chunk, channel = chunk_and_channel
ret = []
for path in chunk:
try:
channel.stat(path)
ret.append(True)
except IOError as exc:
if exc.errno != errno.ENOENT:
raise
ret.append(False)
callback.update(path)
return ret
with self.ssh(path_infos[0]) as ssh:
channels = ssh.open_max_sftp_channels()
max_workers = len(channels)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
paths = [path_info.path for path_info in path_infos]
chunks = to_chunks(paths, num_chunks=max_workers)
chunks_and_channels = zip(chunks, channels)
outcome = executor.map(_exists, chunks_and_channels)
results = list(itertools.chain.from_iterable(outcome))
return results
def get_file_checksum(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
@@ -187,12 +166,54 @@ class RemoteSSH(RemoteBASE):
with self.ssh(path_info) as ssh:
return ssh.isdir(path_info.path)
def isfile(self, path_info):
with self.ssh(path_info) as ssh:
return ssh.isfile(path_info.path)
def getsize(self, path_info):
with self.ssh(path_info) as ssh:
return ssh.getsize(path_info.path)
def copy(self, from_info, to_info):
if not from_info.scheme == to_info.scheme == self.scheme:
raise NotImplementedError
with self.ssh(from_info) as ssh:
ssh.copy(from_info.path, to_info.path)
def symlink(self, from_info, to_info):
if not from_info.scheme == to_info.scheme == self.scheme:
raise NotImplementedError
with self.ssh(from_info) as ssh:
ssh.symlink(from_info.path, to_info.path)
def hardlink(self, from_info, to_info):
if not from_info.scheme == to_info.scheme == self.scheme:
raise NotImplementedError
# See dvc/remote/local/__init__.py - hardlink()
if self.getsize(from_info) == 0:
with self.ssh(to_info) as ssh:
ssh.sftp.open(to_info.path, "w").close()
logger.debug(
"Created empty file: {src} -> {dest}".format(
src=str(from_info), dest=str(to_info)
)
)
return
with self.ssh(from_info) as ssh:
ssh.hardlink(from_info.path, to_info.path)
def reflink(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)
ssh.reflink(from_info.path, to_info.path)
def remove(self, path_info):
if path_info.scheme != self.scheme:
@@ -228,15 +249,85 @@ class RemoteSSH(RemoteBASE):
no_progress_bar=no_progress_bar,
)
@contextmanager
def open(self, path_info, mode="r", encoding=None):
assert mode in {"r", "rt", "rb", "wb"}
with self.ssh(path_info) as ssh, closing(
ssh.sftp.open(path_info.path, mode)
) as fd:
if "b" in mode:
yield fd
else:
yield io.TextIOWrapper(fd, encoding=encoding)
def list_cache_paths(self):
with self.ssh(self.path_info) as ssh:
return list(ssh.walk_files(self.path_info.path))
def walk(self, path_info):
def walk_files(self, path_info):
with self.ssh(path_info) as ssh:
for entry in ssh.walk(path_info.path):
yield entry
for fname in ssh.walk_files(path_info.path):
yield path_info / posixpath.relpath(fname, path_info.path)
def makedirs(self, path_info):
with self.ssh(path_info) as ssh:
ssh.makedirs(path_info.path)
def batch_exists(self, path_infos, callback):
def _exists(chunk_and_channel):
chunk, channel = chunk_and_channel
ret = []
for path in chunk:
try:
channel.stat(path)
ret.append(True)
except IOError as exc:
if exc.errno != errno.ENOENT:
raise
ret.append(False)
callback(path)
return ret
with self.ssh(path_infos[0]) as ssh:
channels = ssh.open_max_sftp_channels()
max_workers = len(channels)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
paths = [path_info.path for path_info in path_infos]
chunks = to_chunks(paths, num_chunks=max_workers)
chunks_and_channels = zip(chunks, channels)
outcome = executor.map(_exists, chunks_and_channels)
results = list(itertools.chain.from_iterable(outcome))
return results
def cache_exists(self, checksums, jobs=None, name=None):
"""This is older implementation used in remote/base.py
We are reusing it in RemoteSSH, because SSH's batch_exists proved to be
faster than current approach (relying on exists(path_info)) applied in
remote/base.
"""
if not self.no_traverse:
return list(set(checksums) & set(self.all()))
# possibly prompt for credentials before "Querying" progress output
self.ensure_credentials()
with Tqdm(
desc="Querying "
+ ("cache in " + name if name else "remote cache"),
total=len(checksums),
unit="file",
) as pbar:
def exists_with_progress(chunks):
return self.batch_exists(chunks, callback=pbar.update_desc)
with ThreadPoolExecutor(max_workers=jobs or self.JOBS) as executor:
path_infos = [self.checksum_to_path_info(x) for x in checksums]
chunks = to_chunks(path_infos, num_chunks=self.JOBS)
results = executor.map(exists_with_progress, chunks)
in_remote = itertools.chain.from_iterable(results)
ret = list(itertools.compress(checksums, in_remote))
return ret
+45 -36
View File
@@ -12,7 +12,7 @@ except ImportError:
from dvc.utils import tmp_fname
from dvc.utils.compat import ignore_file_not_found
from dvc.progress import progress
from dvc.progress import Tqdm
from dvc.exceptions import DvcException
from dvc.remote.base import RemoteCmdError
@@ -29,21 +29,6 @@ def sizeof_fmt(num, suffix="B"):
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, host, *args, **kwargs):
logger.debug(
@@ -75,7 +60,13 @@ class SSHConnection:
def st_mode(self, path):
with ignore_file_not_found():
return self.sftp.stat(path).st_mode
return self.sftp.lstat(path).st_mode
return 0
def getsize(self, path):
with ignore_file_not_found():
return self.sftp.lstat(path).st_size
return 0
@@ -115,7 +106,10 @@ class SSHConnection:
# Since paramiko errors are very vague we need to recheck
# whether it's because path already exists or something else
if e.errno == errno.EACCES or not self.exists(path):
raise
raise DvcException(
"unable to create remote directory '{}'".format(path),
cause=e,
)
def walk(self, directory, topdown=True):
# NOTE: original os.walk() implementation [1] with default options was
@@ -183,14 +177,12 @@ class SSHConnection:
self._remove_file(path)
def download(self, src, dest, no_progress_bar=False, progress_title=None):
if no_progress_bar:
self.sftp.get(src, dest)
else:
if not progress_title:
progress_title = os.path.basename(src)
self.sftp.get(src, dest, callback=create_cb(progress_title))
progress.finish_target(progress_title)
with Tqdm(
desc=progress_title or os.path.basename(src),
disable=no_progress_bar,
bytes=True,
) as pbar:
self.sftp.get(src, dest, callback=pbar.update_to)
def move(self, src, dst):
self.makedirs(posixpath.dirname(dst))
@@ -199,15 +191,13 @@ class SSHConnection:
def upload(self, src, dest, no_progress_bar=False, progress_title=None):
self.makedirs(posixpath.dirname(dest))
tmp_file = tmp_fname(dest)
if not progress_title:
progress_title = posixpath.basename(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)
with Tqdm(
desc=progress_title, disable=no_progress_bar, bytes=True
) as pbar:
self.sftp.put(src, tmp_file, callback=pbar.update_to)
self.sftp.rename(tmp_file, dest)
@@ -286,8 +276,7 @@ class SSHConnection:
assert len(md5) == 32
return md5
def cp(self, src, dest):
self.makedirs(posixpath.dirname(dest))
def copy(self, src, dest):
self.execute("cp {} {}".format(src, dest))
def open_max_sftp_channels(self):
@@ -301,3 +290,23 @@ class SSHConnection:
raise
break
return self._sftp_channels
def open(self, *args, **kwargs):
return self.sftp.open(*args, **kwargs)
def symlink(self, src, dest):
self.sftp.symlink(src, dest)
def reflink(self, src, dest):
if self.uname == "Linux":
return self.execute("cp --reflink {} {}".format(src, dest))
if self.uname == "Darwin":
return self.execute("cp -c {} {}".format(src, dest))
raise DvcException(
"'{}' is not supported as a SSH remote".format(self.uname)
)
def hardlink(self, src, dest):
self.execute("ln {} {}".format(src, dest))
+157 -122
View File
@@ -2,26 +2,42 @@ from __future__ import unicode_literals
import os
import logging
from contextlib import contextmanager
from itertools import chain
from functools import wraps
from funcy import cached_property
from dvc.config import Config
from dvc.exceptions import (
NotDvcRepoError,
OutputNotFoundError,
TargetNotDirectoryError,
OutputFileMissingError,
FileMissingError,
)
from dvc.ignore import DvcIgnoreFilter
from dvc.path_info import PathInfo
from dvc.utils.compat import open as _open, fspath_py35
from dvc.remote.base import RemoteActionNotImplemented
from dvc.utils.compat import open as _open, fspath_py35, FileNotFoundError
from dvc.utils import relpath
from .graph import check_acyclic, get_pipeline, get_pipelines, get_stages
logger = logging.getLogger(__name__)
def locked(f):
@wraps(f)
def wrapper(repo, *args, **kwargs):
with repo.lock, repo.state:
ret = f(repo, *args, **kwargs)
# Our graph cache is no longer valid after we release the repo.lock
repo._reset()
return ret
return wrapper
class Repo(object):
DVC_DIR = ".dvc"
@@ -35,9 +51,9 @@ class Repo(object):
from dvc.repo.imp import imp
from dvc.repo.imp_url import imp_url
from dvc.repo.reproduce import reproduce
from dvc.repo.checkout import checkout
from dvc.repo.checkout import _checkout
from dvc.repo.push import push
from dvc.repo.fetch import fetch
from dvc.repo.fetch import _fetch
from dvc.repo.pull import pull
from dvc.repo.status import status
from dvc.repo.gc import gc
@@ -65,11 +81,14 @@ class Repo(object):
self.config = Config(self.dvc_dir)
self.scm = SCM(self.root_dir, repo=self)
self.scm = SCM(self.root_dir)
self.tree = WorkingTree(self.root_dir)
self.lock = Lock(self.dvc_dir)
self.lock = Lock(
os.path.join(self.dvc_dir, "lock"),
tmp_dir=os.path.join(self.dvc_dir, "tmp"),
)
# 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)
@@ -88,6 +107,17 @@ class Repo(object):
self._ignore()
@property
def tree(self):
return self._tree
@tree.setter
def tree(self, tree):
self._tree = tree
# Our graph cache is no longer valid, as it was based on the previous
# tree.
self._reset()
def __repr__(self):
return "Repo: '{root_dir}'".format(root_dir=self.root_dir)
@@ -127,64 +157,63 @@ class Repo(object):
updater = Updater(self.dvc_dir)
flist = [
self.lock.lock_file,
self.config.config_local_file,
updater.updater_file,
updater.lock.lock_file,
] + self.state.files
flist = (
[self.config.config_local_file, updater.updater_file]
+ self.state.files
+ self.lock.files
+ updater.lock.files
)
if self.cache.local.cache_dir.startswith(self.root_dir):
if self.cache.local.cache_dir.startswith(self.root_dir + os.sep):
flist += [self.cache.local.cache_dir]
self.scm.ignore_list(flist)
def check_dag(self, stages):
def check_modified_graph(self, new_stages):
"""Generate graph including the new stage to check for errors"""
self.graph(stages=stages)
self._collect_graph(self.stages + new_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):
def collect(self, target, with_deps=False, recursive=False, graph=None):
import networkx as nx
from dvc.stage import Stage
if not target or (recursive and os.path.isdir(target)):
return self.active_stages(target)
G = graph or self.graph
if not target:
return get_stages(G)
target = os.path.abspath(target)
if recursive and os.path.isdir(target):
attrs = nx.get_node_attributes(G, "stage")
nodes = [node for node in nx.dfs_postorder_nodes(G)]
ret = []
for node in nodes:
stage = attrs[node]
if stage.path.startswith(target + os.sep):
ret.append(stage)
return ret
stage = Stage.load(self, target)
if not with_deps:
return [stage]
node = relpath(stage.path, self.root_dir)
G = self._get_pipeline(node)
pipeline = get_pipeline(get_pipelines(G), node)
ret = []
for n in nx.dfs_postorder_nodes(G, node):
ret.append(G.node[n]["stage"])
return ret
return [
pipeline.node[n]["stage"]
for n in nx.dfs_postorder_nodes(pipeline, node)
]
def used_cache(
self,
targets=None,
all_branches=False,
active=True,
with_deps=False,
all_tags=False,
all_commits=False,
remote=None,
force=False,
jobs=None,
@@ -203,54 +232,41 @@ class Repo(object):
A dictionary with Schemes (representing output's location) as keys,
and a list with the outputs' `dumpd` as values.
"""
from dvc.cache import NamedCache
cache = {}
cache["local"] = []
cache["s3"] = []
cache["gs"] = []
cache["hdfs"] = []
cache["ssh"] = []
cache["azure"] = []
cache = NamedCache()
for branch in self.brancher(
all_branches=all_branches, all_tags=all_tags
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
):
if targets:
stages = []
for target in targets:
if recursive and os.path.isdir(target):
stages.extend(self.stages(target))
else:
stages.extend(
self.collect(target, with_deps=with_deps)
)
elif active:
stages = self.active_stages()
collected = self.collect(
target, recursive=recursive, with_deps=with_deps
)
stages.extend(collected)
else:
stages = self.stages()
stages = self.stages
for stage in stages:
if active and not targets and stage.locked:
logger.warning(
"DVC-file '{path}' is locked. Its dependencies are"
" not going to be pushed/pulled/fetched.".format(
path=stage.relpath
)
)
if stage.is_repo_import:
dep, = stage.deps
cache.external[dep.repo_pair].add(dep.def_path)
continue
for out in stage.outs:
scheme = out.path_info.scheme
used_cache = out.get_used_cache(
remote=remote, force=force, jobs=jobs
)
cache[scheme].extend(
dict(entry, branch=branch) for entry in used_cache
)
suffix = "({})".format(branch) if branch else ""
cache.update(used_cache, suffix=suffix)
return cache
def graph(self, stages=None, from_directory=None):
def _collect_graph(self, stages=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.
@@ -280,11 +296,8 @@ class Repo(object):
(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
stages (list): used to build a graph, if None given, collect stages
in the repository.
Raises:
OutputDuplicationError: two outputs with the same path
@@ -300,8 +313,7 @@ class Repo(object):
)
G = nx.DiGraph()
G_active = nx.DiGraph()
stages = stages or self.stages(from_directory, check_dag=False)
stages = stages or self.collect_stages()
stages = [stage for stage in stages if stage]
outs = {}
@@ -328,7 +340,6 @@ class Repo(object):
node = relpath(stage.path, self.root_dir)
G.add_node(node, stage=stage)
G_active.add_node(node, stage=stage)
for dep in stage.deps:
if dep.path_info is None:
@@ -344,22 +355,18 @@ class Repo(object):
dep_node = relpath(dep_stage.path, self.root_dir)
G.add_node(dep_node, stage=dep_stage)
G.add_edge(node, dep_node)
if not stage.locked:
G_active.add_node(dep_node, stage=dep_stage)
G_active.add_edge(node, dep_node)
self._check_cyclic_graph(G)
check_acyclic(G)
return G, G_active
return G
def pipelines(self, from_directory=None):
import networkx as nx
@cached_property
def graph(self):
return self._collect_graph()
G, G_active = self.graph(from_directory=from_directory)
return [
G.subgraph(c).copy() for c in nx.weakly_connected_components(G)
]
@cached_property
def pipelines(self):
return get_pipelines(self.graph)
@staticmethod
def _filter_out_dirs(dirs, outs, root_dir):
@@ -372,7 +379,7 @@ class Repo(object):
return list(filter(filter_dirs, dirs))
def stages(self, from_directory=None, check_dag=True):
def collect_stages(self):
"""
Walks down the root directory looking for Dvcfiles,
skipping the directories that are related with
@@ -384,16 +391,11 @@ class Repo(object):
"""
from dvc.stage import Stage
if not from_directory:
from_directory = self.root_dir
elif not os.path.isdir(from_directory):
raise TargetNotDirectoryError(from_directory)
stages = []
outs = []
for root, dirs, files in self.tree.walk(
from_directory, dvcignore=self.dvcignore
self.root_dir, dvcignore=self.dvcignore
):
for fname in files:
path = os.path.join(root, fname)
@@ -407,26 +409,15 @@ class Repo(object):
dirs[:] = self._filter_out_dirs(dirs, outs, root)
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
@cached_property
def stages(self):
return get_stages(self.graph)
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]
outs = [out for stage in self.stages for out in stage.outs]
abs_path = os.path.abspath(path)
is_dir = self.tree.isdir(abs_path)
@@ -455,25 +446,69 @@ class Repo(object):
path_parts = os.path.normpath(path).split(os.path.sep)
return self.DVC_DIR in path_parts
@contextmanager
def open(self, path, remote=None, mode="r", encoding=None):
"""Opens a specified resource as a file descriptor"""
out, = self.find_outs_by_path(path)
cause = None
try:
out, = self.find_outs_by_path(path)
except OutputNotFoundError as e:
out = None
cause = e
if out and out.use_cache:
try:
with self._open_cached(out, remote, mode, encoding) as fd:
yield fd
return
except FileNotFoundError as e:
raise FileMissingError(relpath(path, self.root_dir), cause=e)
if self.tree.exists(path):
with self.tree.open(path, mode, encoding) as fd:
yield fd
return
raise FileMissingError(relpath(path, self.root_dir), cause=cause)
def _open_cached(self, out, remote=None, mode="r", encoding=None):
if out.isdir():
raise ValueError("Can't open a dir")
cache_file = self.cache.local.checksum_to_path_info(out.checksum)
cache_file = fspath_py35(cache_file)
with self.state:
cache_info = out.get_used_cache(remote=remote)
self.cloud.pull(cache_info, remote=remote)
if os.path.exists(cache_file):
return _open(cache_file, mode=mode, encoding=encoding)
# Since pull may just skip with a warning, we need to check it here
if not os.path.exists(cache_file):
raise OutputFileMissingError(relpath(path, self.root_dir))
try:
remote_obj = self.cloud.get_remote(remote)
remote_info = remote_obj.checksum_to_path_info(out.checksum)
return remote_obj.open(remote_info, mode=mode, encoding=encoding)
except RemoteActionNotImplemented:
with self.state:
cache_info = out.get_used_cache(remote=remote)
self.cloud.pull(cache_info, remote=remote)
return _open(cache_file, mode=mode, encoding=encoding)
return _open(cache_file, mode=mode, encoding=encoding)
@cached_property
def dvcignore(self):
return DvcIgnoreFilter(self.root_dir)
def close(self):
self.scm.close()
@locked
def checkout(self, *args, **kwargs):
return self._checkout(*args, **kwargs)
@locked
def fetch(self, *args, **kwargs):
return self._fetch(*args, **kwargs)
def _reset(self):
self.__dict__.pop("graph", None)
self.__dict__.pop("stages", None)
self.__dict__.pop("pipelines", None)
self.__dict__.pop("dvcignore", None)
+52 -32
View File
@@ -1,43 +1,65 @@
from __future__ import unicode_literals
import os
import logging
import colorama
from six import string_types
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
from dvc.progress import Tqdm
from . import locked
logger = logging.getLogger(__name__)
@locked
@scm_context
def add(repo, target, recursive=False, no_commit=False, fname=None):
def add(repo, targets, recursive=False, no_commit=False, fname=None):
if recursive and fname:
raise RecursiveAddingWhileUsingFilename()
targets = _find_all_targets(repo, target, recursive)
if isinstance(targets, string_types):
targets = [targets]
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 DVC-file 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_list = []
with Tqdm(total=len(targets), desc="Add", unit="file", leave=True) as pbar:
for target in targets:
sub_targets = _find_all_targets(repo, target, recursive)
pbar.total += len(sub_targets) - 1
stages = _create_stages(repo, targets, fname, no_commit)
if os.path.isdir(target) and len(sub_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 DVC-file and then"
" run {cyan}dvc add {target}{nc}".format(
purple=colorama.Fore.MAGENTA,
cyan=colorama.Fore.CYAN,
nc=colorama.Style.RESET_ALL,
target=target,
)
)
repo.check_dag(repo.stages() + stages)
stages = _create_stages(repo, sub_targets, fname, pbar=pbar)
for stage in stages:
stage.dump()
return stages
repo.check_modified_graph(stages)
for stage in stages:
stage.save()
if not no_commit:
stage.commit()
stage.dump()
stages_list += stages
# remove filled bar bit of progress, leaving stats
pbar.bar_format = pbar.BAR_FMT_DEFAULT.replace("|{bar:10}|", " ")
return stages_list
def _find_all_targets(repo, target, recursive):
@@ -53,21 +75,19 @@ def _find_all_targets(repo, target, recursive):
return [target]
def _create_stages(repo, targets, fname, no_commit):
def _create_stages(repo, targets, fname, pbar=None):
stages = []
with repo.state:
for out in targets:
stage = Stage.create(repo, outs=[out], add=True, fname=fname)
for out in targets:
stage = Stage.create(repo, outs=[out], add=True, fname=fname)
if not stage:
continue
if not stage:
if pbar is not None:
pbar.total -= 1
continue
stage.save()
if not no_commit:
stage.commit()
stages.append(stage)
stages.append(stage)
if pbar is not None:
pbar.update_desc(out)
return stages
+22 -13
View File
@@ -1,8 +1,14 @@
from funcy import group_by
from dvc.scm.tree import WorkingTree
def brancher( # noqa: E302
self, branches=None, all_branches=False, tags=None, all_tags=False
self,
branches=None,
all_branches=False,
tags=None,
all_tags=False,
all_commits=False,
):
"""Generator that iterates over specified revisions.
@@ -18,7 +24,7 @@ def brancher( # noqa: E302
- 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]):
if not any([branches, all_branches, tags, all_tags, all_commits]):
yield ""
return
@@ -30,25 +36,28 @@ def brancher( # noqa: E302
self.tree = WorkingTree(self.root_dir)
yield "working tree"
if all_branches:
branches = scm.list_branches()
if all_commits:
revs = scm.list_all_commits()
else:
if all_branches:
branches = scm.list_branches()
if all_tags:
tags = scm.list_tags()
if all_tags:
tags = scm.list_tags()
if branches is not None:
revs.extend(branches)
if branches is not None:
revs.extend(branches)
if tags is not None:
revs.extend(tags)
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
for sha, names in group_by(scm.resolve_rev, revs).items():
self.tree = scm.get_tree(sha)
yield ", ".join(names)
self.tree = saved_tree
+30 -25
View File
@@ -2,8 +2,8 @@ from __future__ import unicode_literals
import logging
from dvc.exceptions import CheckoutErrorSuggestGit
from dvc.progress import ProgressCallback
from dvc.exceptions import CheckoutErrorSuggestGit, CheckoutError
from dvc.progress import Tqdm
logger = logging.getLogger(__name__)
@@ -23,29 +23,32 @@ 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):
def _checkout(
self, targets=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)
stages = set()
targets = targets or [None]
for target in targets:
try:
new = self.collect(
target, with_deps=with_deps, recursive=recursive
)
stages.update(new)
except (StageFileDoesNotExistError, StageFileBadNameError) as exc:
if not target:
raise
raise CheckoutErrorSuggestGit(target, exc)
_cleanup_unused_links(self, self.stages)
total = get_all_files_numbers(stages)
if total == 0:
logger.info("Nothing to do")
failed = []
with Tqdm(
total=total, unit="file", desc="Checkout", disable=total == 0
) as pbar:
for stage in stages:
if stage.locked:
logger.warning(
@@ -53,6 +56,8 @@ def checkout(self, target=None, with_deps=False, force=False, recursive=False):
" 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!")
failed.extend(
stage.checkout(force=force, progress_callback=pbar.update_desc)
)
if failed:
raise CheckoutError(failed)
+8 -5
View File
@@ -1,7 +1,10 @@
from . import locked
@locked
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()
for stage in stages:
stage.check_can_commit(force=force)
stage.commit()
stage.dump()
+1 -1
View File
@@ -2,7 +2,7 @@ from dvc.utils import remove
def destroy(self):
for stage in self.stages():
for stage in self.stages:
stage.remove(remove_outs=False)
remove(self.dvc_dir)
+6 -3
View File
@@ -14,6 +14,8 @@ from dvc.scm.git import (
DIFF_EQUAL,
)
from . import locked
DIFF_TARGET = "target"
DIFF_IS_DIR = "is_dir"
@@ -77,7 +79,7 @@ def _modified_files(self, a_entries, b_entries):
modified_count = 0
for key in keys:
if a_entries[key] != b_entries[key]:
modified_count
modified_count += 1
diff_size += os.path.getsize(
self.cache.local.get(b_entries[key])
) - os.path.getsize(self.cache.local.get(a_entries[key]))
@@ -140,9 +142,9 @@ def _is_dir(path, a_outs, b_outs):
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}
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}
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 = {}
@@ -220,6 +222,7 @@ def _diff_royal(self, target, diff_dct):
return _diff_file(self, target, diff_dct)
@locked
def diff(self, a_ref, target=None, b_ref=None):
"""Gerenates diff message string output
+88 -13
View File
@@ -1,7 +1,18 @@
from __future__ import unicode_literals
import logging
def fetch(
from dvc.config import NoRemoteError
from dvc.exceptions import DownloadError, OutputNotFoundError
from dvc.scm.base import CloneError
from dvc.external_repo import external_repo
from dvc.cache import NamedCache
logger = logging.getLogger(__name__)
def _fetch(
self,
targets=None,
jobs=None,
@@ -12,17 +23,81 @@ def fetch(
all_tags=False,
recursive=False,
):
with self.state:
used = self.used_cache(
targets,
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(
"""Download data items from a cloud and imported repositories
Returns:
int: number of succesfully downloaded files
Raises:
DownloadError: thrown when there are failed downloads, either
during `cloud.pull` or trying to fetch imported files
config.NoRemoteError: thrown when downloading only local files and no
remote is configured
"""
used = self.used_cache(
targets,
all_branches=all_branches,
all_tags=all_tags,
with_deps=with_deps,
force=True,
remote=remote,
jobs=jobs,
recursive=recursive,
)
downloaded = 0
failed = 0
try:
downloaded += self.cloud.pull(
used, jobs, remote=remote, show_checksums=show_checksums
)
except NoRemoteError:
if not used.external and used["local"]:
raise
except DownloadError as exc:
failed += exc.amount
for (repo_url, repo_rev), files in used.external.items():
d, f = _fetch_external(self, repo_url, repo_rev, files)
downloaded += d
failed += f
if failed:
raise DownloadError(failed)
return downloaded
def _fetch_external(self, repo_url, repo_rev, files):
failed = 0
cache_dir = self.cache.local.cache_dir
try:
with external_repo(repo_url, repo_rev, cache_dir=cache_dir) as repo:
with repo.state:
cache = NamedCache()
for name in files:
try:
out = repo.find_out_by_relpath(name)
except OutputNotFoundError:
failed += 1
logger.exception(
"failed to fetch data for '{}'".format(name)
)
continue
else:
cache.update(out.get_used_cache())
try:
return repo.cloud.pull(cache), failed
except DownloadError as exc:
failed += exc.amount
except CloneError:
failed += 1
logger.exception(
"failed to fetch data for '{}'".format(", ".join(files))
)
return 0, failed
+38 -75
View File
@@ -1,61 +1,21 @@
from __future__ import unicode_literals
import collections
import logging
from dvc.cache import NamedCache
from . import locked
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(
targets=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))
@locked
def gc(
self,
all_branches=False,
@@ -63,51 +23,54 @@ def gc(
remote=None,
with_deps=False,
all_tags=False,
all_commits=False,
force=False,
jobs=None,
repos=None,
):
from dvc.utils.compat import ExitStack
from dvc.repo import Repo
all_repos = [self]
all_repos = []
if repos:
all_repos.extend(Repo(path) for path in repos)
all_repos = [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,
)
with ExitStack() as stack:
for repo in all_repos:
stack.enter_context(repo.lock)
stack.enter_context(repo.state)
if len(all_clists) > 1:
clist = _merge_cache_lists(all_clists)
else:
clist = all_clists[0]
used = NamedCache()
for repo in all_repos + [self]:
used.update(
repo.used_cache(
all_branches=all_branches,
with_deps=with_deps,
all_tags=all_tags,
all_commits=all_commits,
remote=remote,
force=force,
jobs=jobs,
)
)
with self.state:
_do_gc("local", self.cache.local.gc, clist)
_do_gc("local", self.cache.local.gc, used)
if self.cache.s3:
_do_gc("s3", self.cache.s3.gc, clist)
if self.cache.s3:
_do_gc("s3", self.cache.s3.gc, used)
if self.cache.gs:
_do_gc("gs", self.cache.gs.gc, clist)
if self.cache.gs:
_do_gc("gs", self.cache.gs.gc, used)
if self.cache.ssh:
_do_gc("ssh", self.cache.ssh.gc, clist)
if self.cache.ssh:
_do_gc("ssh", self.cache.ssh.gc, used)
if self.cache.hdfs:
_do_gc("hdfs", self.cache.hdfs.gc, clist)
if self.cache.hdfs:
_do_gc("hdfs", self.cache.hdfs.gc, used)
if self.cache.azure:
_do_gc("azure", self.cache.azure.gc, clist)
if self.cache.azure:
_do_gc("azure", self.cache.azure.gc, used)
if cloud:
_do_gc("remote", self.cloud.get_remote(remote, "gc -c").gc, clist)
if cloud:
_do_gc("remote", self.cloud.get_remote(remote, "gc -c").gc, used)
+20 -3
View File
@@ -1,17 +1,29 @@
import logging
import os
import shortuuid
from dvc.stage import Stage
from dvc.config import Config
from dvc.exceptions import (
NotDvcRepoError,
UrlNotDvcRepoError,
GetDVCFileError,
OutputNotFoundError,
)
from dvc.path_info import PathInfo
from dvc.external_repo import external_repo
from dvc.state import StateNoop
from dvc.utils import remove
from dvc.utils.compat import urlparse
from dvc.utils import remove, resolve_output
logger = logging.getLogger(__name__)
@staticmethod
def get(url, path, out=None, rev=None):
out = out or os.path.basename(urlparse(path).path)
out = resolve_output(path, out)
if Stage.is_valid_filename(out):
raise GetDVCFileError()
# Creating a directory right beside the output to make sure that they
# are on the same filesystem, so we could take the advantage of
@@ -48,5 +60,10 @@ def get(url, path, out=None, rev=None):
o.path_info = PathInfo(os.path.abspath(out))
with o.repo.state:
o.checkout()
except NotDvcRepoError:
raise UrlNotDvcRepoError(url)
except OutputNotFoundError:
raise OutputNotFoundError(path)
finally:
remove(tmp_dir)
+2 -2
View File
@@ -3,12 +3,12 @@ import os
import dvc.output as output
import dvc.dependency as dependency
from dvc.utils.compat import urlparse
from dvc.utils import resolve_output
@staticmethod
def get_url(url, out=None):
out = out or os.path.basename(urlparse(url).path)
out = resolve_output(url, out)
if os.path.exists(url):
url = os.path.abspath(url)
+36
View File
@@ -0,0 +1,36 @@
from __future__ import unicode_literals
def check_acyclic(graph):
import networkx as nx
from dvc.exceptions import CyclicGraphError
try:
edges = nx.find_cycle(graph, orientation="original")
except nx.NetworkXNoCycle:
return
stages = set()
for from_node, to_node, _ in edges:
stages.add(from_node)
stages.add(to_node)
raise CyclicGraphError(list(stages))
def get_pipeline(pipelines, node):
found = [i for i in pipelines if i.has_node(node)]
assert len(found) == 1
return found[0]
def get_pipelines(G):
import networkx as nx
return [G.subgraph(c).copy() for c in nx.weakly_connected_components(G)]
def get_stages(G):
import networkx
return list(networkx.get_node_attributes(G, "stage").values())
+10 -9
View File
@@ -1,25 +1,26 @@
from dvc.utils.compat import pathlib
from dvc.utils import resolve_output
from dvc.repo.scm_context import scm_context
from . import locked as locked_repo
@locked_repo
@scm_context
def imp_url(self, url, out=None, fname=None, erepo=None, locked=True):
from dvc.stage import Stage
out = out or pathlib.PurePath(url).name
out = resolve_output(url, out)
with self.state:
stage = Stage.create(
self, cmd=None, deps=[url], outs=[out], fname=fname, erepo=erepo
)
stage = Stage.create(
self, cmd=None, deps=[url], outs=[out], fname=fname, erepo=erepo
)
if stage is None:
return None
self.check_dag(self.stages() + [stage])
self.check_modified_graph([stage])
with self.state:
stage.run()
stage.run()
stage.locked = locked
+2 -2
View File
@@ -65,7 +65,7 @@ def init(root_dir=os.curdir, no_scm=False, force=False):
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(
"Use `--no-scm` if you don't want to use any scm.".format(
repo=root_dir
)
)
@@ -73,7 +73,7 @@ def init(root_dir=os.curdir, no_scm=False, force=False):
if os.path.isdir(dvc_dir):
if not force:
raise InitError(
"'{repo}' exists. Use '-f' to force.".format(
"'{repo}' exists. Use `-f` to force.".format(
repo=relpath(dvc_dir)
)
)
+4
View File
@@ -1,3 +1,7 @@
from . import locked
@locked
def lock(self, target, unlock=False):
from dvc.stage import Stage
+2
View File
@@ -1,8 +1,10 @@
from __future__ import unicode_literals
from dvc.repo import locked
from dvc.exceptions import DvcException
@locked
def modify(repo, path, typ=None, xpath=None, delete=False):
supported_types = ["raw", "json", "csv", "tsv", "hcsv", "htsv"]
outs = repo.find_outs_by_path(path)
+34 -15
View File
@@ -8,7 +8,8 @@ import logging
from jsonpath_ng.ext import parse
from dvc.exceptions import OutputNotFoundError, BadMetricError, NoMetricsError
from dvc.repo import locked
from dvc.exceptions import OutputNotFoundError, NoMetricsError
from dvc.utils.compat import builtin_str, open, StringIO, csv_reader
NO_METRICS_FILE_AT_REFERENCE_WARNING = (
@@ -159,8 +160,8 @@ def _collect_metrics(repo, path, recursive, typ, xpath, branch):
path (str): Path to a metric file or a directory.
recursive (bool): If path is a directory, do a recursive search for
metrics on the given path.
typ (str): The type of metric to search for, could be one of the
following (raw|json|tsv|htsv|csv|hcsv).
typ (str): The type that will be used to interpret the metric file,
one of the followings - (raw|json|tsv|htsv|csv|hcsv).
xpath (str): Path to search for.
branch (str): Branch to look up for metrics.
@@ -170,7 +171,7 @@ def _collect_metrics(repo, path, recursive, typ, xpath, branch):
- typ:
- xpath:
"""
outs = [out for stage in repo.stages() for out in stage.outs]
outs = [out for stage in repo.stages for out in stage.outs]
if path:
try:
@@ -188,16 +189,15 @@ def _collect_metrics(repo, path, recursive, typ, xpath, branch):
if not o.metric:
continue
# NOTE this case assumes that metric has been modified with
# `dvc metric modify -t {} -x {}`, so stage file should contain
# info what type metric is and what is its xpath
# NOTE this case assumes that typ has not been provided in CLI call
# and one of the following cases:
# - stage file contains metric type
# - typ will be read from file extension later
if not typ and isinstance(o.metric, dict):
t = o.metric.get(o.PARAM_METRIC_TYPE, typ)
# NOTE user might want to check different xpath, hence xpath first
x = xpath or o.metric.get(o.PARAM_METRIC_XPATH)
else:
# NOTE if metric has not been modified, user needs to provide
# both metric type and xpath
t = typ
x = xpath
@@ -258,9 +258,10 @@ def _read_metrics(repo, metrics, branch):
return res
@locked
def show(
repo,
path=None,
targets=None,
typ=None,
xpath=None,
all_branches=False,
@@ -268,16 +269,34 @@ def show(
recursive=False,
):
res = {}
found = set()
if not targets:
# Iterate once to call `_collect_metrics` on all the stages
targets = [None]
for branch in repo.brancher(all_branches=all_branches, all_tags=all_tags):
entries = _collect_metrics(repo, path, recursive, typ, xpath, branch)
metrics = _read_metrics(repo, entries, branch)
metrics = {}
for target in targets:
entries = _collect_metrics(
repo, target, recursive, typ, xpath, branch
)
metric = _read_metrics(repo, entries, branch)
if metric:
found.add(target)
metrics.update(metric)
if metrics:
res[branch] = metrics
if not res:
if path:
raise BadMetricError(path)
if not res and not any(targets):
raise NoMetricsError()
missing = set(targets) - found
if missing:
res[None] = missing
return res
+8 -5
View File
@@ -3,6 +3,8 @@ import os
from dvc.exceptions import MoveNotDataSourceError
from dvc.repo.scm_context import scm_context
from . import locked
def _expand_target_path(from_path, to_path):
if os.path.isdir(to_path):
@@ -10,6 +12,7 @@ def _expand_target_path(from_path, to_path):
return to_path
@locked
@scm_context
def move(self, from_path, to_path):
"""
@@ -59,11 +62,11 @@ def move(self, from_path, to_path):
os.path.join(os.curdir, os.path.dirname(to_path))
)
to_out = Output.loads_from(
stage, [os.path.basename(to_path)], out.use_cache, out.metric
)[0]
to_path = os.path.relpath(to_path, stage.wdir)
with self.state:
out.move(to_out)
to_out = Output.loads_from(stage, [to_path], out.use_cache, out.metric)[0]
out.move(to_out)
stage.save()
stage.dump()
+7 -8
View File
@@ -1,6 +1,9 @@
from __future__ import unicode_literals
from . import locked
@locked
def pull(
self,
targets=None,
@@ -12,7 +15,7 @@ def pull(
force=False,
recursive=False,
):
processed_files_count = self.fetch(
processed_files_count = self._fetch(
targets,
jobs,
remote=remote,
@@ -21,11 +24,7 @@ def pull(
with_deps=with_deps,
recursive=recursive,
)
for target in targets or [None]:
self.checkout(
target=target,
with_deps=with_deps,
force=force,
recursive=recursive,
)
self._checkout(
targets=targets, with_deps=with_deps, force=force, recursive=recursive
)
return processed_files_count
+14 -12
View File
@@ -1,6 +1,9 @@
from __future__ import unicode_literals
from . import locked
@locked
def push(
self,
targets=None,
@@ -11,15 +14,14 @@ def push(
all_tags=False,
recursive=False,
):
with self.state:
used = self.used_cache(
targets,
all_branches=all_branches,
all_tags=all_tags,
with_deps=with_deps,
force=True,
remote=remote,
jobs=jobs,
recursive=recursive,
)["local"]
return self.cloud.push(used, jobs, remote=remote)
used = self.used_cache(
targets,
all_branches=all_branches,
all_tags=all_tags,
with_deps=with_deps,
force=True,
remote=remote,
jobs=jobs,
recursive=recursive,
)
return self.cloud.push(used, jobs, remote=remote)
+4
View File
@@ -1,3 +1,7 @@
from . import locked
@locked
def remove(self, target, outs_only=False):
from dvc.stage import Stage
+39 -24
View File
@@ -1,12 +1,15 @@
from __future__ import unicode_literals
import os
import logging
from dvc.exceptions import ReproductionError
from dvc.repo.scm_context import scm_context
from dvc.utils import relpath
from . import locked
from .graph import get_pipeline, get_pipelines
logger = logging.getLogger(__name__)
@@ -29,6 +32,24 @@ def _reproduce_stage(stages, node, **kwargs):
return [stage]
def _get_active_graph(G):
import networkx as nx
active = G.copy()
stages = nx.get_node_attributes(G, "stage")
for node in G:
stage = stages[node]
if not stage.locked:
continue
for n in nx.dfs_postorder_nodes(G, node):
if n == node:
continue
if n in active:
active.remove_node(n)
return active
@locked
@scm_context
def reproduce(
self,
@@ -52,43 +73,37 @@ def reproduce(
config.SECTION_CORE_INTERACTIVE, False
)
targets = []
if recursive and os.path.isdir(target):
G = self.graph(from_directory=target)[1]
dir_targets = [
os.path.join(self.root_dir, n) for n in nx.dfs_postorder_nodes(G)
]
targets.extend(dir_targets)
elif pipeline or all_pipelines:
if pipeline:
active_graph = _get_active_graph(self.graph)
active_pipelines = get_pipelines(active_graph)
if pipeline or all_pipelines:
if all_pipelines:
pipelines = active_pipelines
else:
stage = Stage.load(self, target)
node = relpath(stage.path, self.root_dir)
pipelines = [self._get_pipeline(node)]
else:
pipelines = self.pipelines()
pipelines = [get_pipeline(active_pipelines, node)]
targets = []
for G in pipelines:
for node in G.nodes():
attrs = nx.get_node_attributes(G, "stage")
for node in G:
if G.in_degree(node) == 0:
targets.append(os.path.join(self.root_dir, node))
targets.append(attrs[node])
else:
targets.append(target)
targets = self.collect(target, recursive=recursive, graph=active_graph)
ret = []
with self.state:
for target in targets:
stages = _reproduce(self, target, **kwargs)
ret.extend(stages)
for target in targets:
stages = _reproduce(self, active_graph, target, **kwargs)
ret.extend(stages)
return ret
def _reproduce(self, target, **kwargs):
def _reproduce(self, G, stage, **kwargs):
import networkx as nx
from dvc.stage import Stage
stage = Stage.load(self, target)
G = self.graph()[1]
stages = nx.get_node_attributes(G, "stage")
node = relpath(stage.path, self.root_dir)
+7 -7
View File
@@ -1,23 +1,23 @@
from __future__ import unicode_literals
from dvc.repo.scm_context import scm_context
from . import locked
from .scm_context import scm_context
@locked
@scm_context
def run(self, no_exec=False, **kwargs):
from dvc.stage import Stage
with self.state:
stage = Stage.create(self, **kwargs)
stage = Stage.create(self, **kwargs)
if stage is None:
return None
self.check_dag(self.stages() + [stage])
self.check_modified_graph([stage])
with self.state:
if not no_exec:
stage.run(no_commit=kwargs.get("no_commit", False))
if not no_exec:
stage.run(no_commit=kwargs.get("no_commit", False))
stage.dump()
+15 -13
View File
@@ -3,6 +3,8 @@ from __future__ import unicode_literals
import logging
from funcy.py3 import cat
from . import locked
logger = logging.getLogger(__name__)
@@ -74,7 +76,7 @@ def _cloud_status(
force=True,
remote=remote,
jobs=jobs,
)["local"]
)
ret = {}
status_info = self.cloud.status(used, jobs, remote=remote)
@@ -91,6 +93,7 @@ def _cloud_status(
return ret
@locked
def status(
self,
targets=None,
@@ -101,15 +104,14 @@ def status(
with_deps=False,
all_tags=False,
):
with self.state:
if cloud or remote:
return _cloud_status(
self,
targets,
jobs,
all_branches=all_branches,
with_deps=with_deps,
remote=remote,
all_tags=all_tags,
)
return _local_status(self, targets, with_deps=with_deps)
if cloud or remote:
return _cloud_status(
self,
targets,
jobs,
all_branches=all_branches,
with_deps=with_deps,
remote=remote,
all_tags=all_tags,
)
return _local_status(self, targets, with_deps=with_deps)
+4
View File
@@ -1,9 +1,13 @@
import logging
from copy import copy
from dvc.repo import locked
logger = logging.getLogger(__name__)
@locked
def add(self, tag, target=None, with_deps=False, recursive=False):
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
for stage in stages:
+4
View File
@@ -1,3 +1,7 @@
from dvc.repo import locked
@locked
def list(self, target=None, with_deps=False, recursive=False):
ret = {}
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
+3
View File
@@ -1,9 +1,12 @@
import logging
from dvc.repo import locked
logger = logging.getLogger(__name__)
@locked
def remove(self, tag, target=None, with_deps=False, recursive=False):
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
for stage in stages:
+5 -3
View File
@@ -1,9 +1,11 @@
from . import locked
@locked
def update(self, target):
from dvc.stage import Stage
stage = Stage.load(self, target)
with self.state:
stage.update()
stage.save()
stage.update()
stage.dump()
+3 -3
View File
@@ -12,7 +12,7 @@ class NoSCM(Base):
pass
def SCM(root_dir, repo=None): # pylint: disable=invalid-name
def SCM(root_dir): # pylint: disable=invalid-name
"""Returns SCM instance that corresponds to a repo at the specified
path.
@@ -24,6 +24,6 @@ def SCM(root_dir, repo=None): # pylint: disable=invalid-name
dvc.scm.base.Base: SCM instance.
"""
if Git.is_repo(root_dir) or Git.is_submodule(root_dir):
return Git(root_dir, repo=repo)
return Git(root_dir)
return NoSCM(root_dir, repo=repo)
return NoSCM(root_dir)
+23 -7
View File
@@ -11,28 +11,37 @@ class SCMError(DvcException):
"""Base class for source control management errors."""
class FileNotInRepoError(DvcException):
class FileNotInRepoError(SCMError):
"""Thrown when trying to find .gitignore for a file that is not in a scm
repository.
"""
class FileNotInCommitError(DvcException):
class FileNotInCommitError(SCMError):
"""Thrown when trying to find a file/directory that is not
in the specified commit in the repository.
"""
class FileNotInTargetSubdirError(DvcException):
"""Thrown when trying to place .gitignore for a file that not in
the file subdirectory."""
class CloneError(SCMError):
def __init__(self, url, path, cause):
super(CloneError, self).__init__(
"Failed to clone repo '{}' to '{}'".format(url, path), cause=cause
)
class RevError(SCMError):
def __init__(self, url, rev, cause):
super(RevError, self).__init__(
"Failed to access revision '{}' for repo '{}'".format(rev, url),
cause=cause,
)
class Base(object):
"""Base class for source control management driver implementations."""
def __init__(self, root_dir=os.curdir, repo=None):
self.repo = repo
def __init__(self, root_dir=os.curdir):
self.root_dir = os.path.realpath(root_dir)
def __repr__(self):
@@ -111,6 +120,10 @@ class Base(object):
"""Returns a list of available tags in the repo."""
return []
def list_all_commits(self): # pylint: disable=no-self-use
"""Returns a list of commits in the repo."""
return []
def install(self):
"""Adds dvc commands to SCM hooks for the repo."""
@@ -144,3 +157,6 @@ class Base(object):
def belongs_to_scm(self, path):
"""Return boolean whether file belongs to scm"""
def close(self):
""" Method to close the files """
+116 -55
View File
@@ -5,13 +5,18 @@ from __future__ import unicode_literals
import os
import logging
from dvc.utils.compat import str, open
from dvc.utils import fix_env, relpath
from funcy import cached_property
from pathspec.patterns import GitWildMatchPattern
from dvc.exceptions import GitHookAlreadyExistsError
from dvc.utils.compat import str, open, cast_bytes_py2
from dvc.utils import fix_env, relpath, is_binary
from dvc.scm.base import (
Base,
SCMError,
FileNotInRepoError,
FileNotInTargetSubdirError,
CloneError,
RevError,
)
from dvc.scm.git.tree import GitTree
@@ -32,17 +37,17 @@ class Git(Base):
GITIGNORE = ".gitignore"
GIT_DIR = ".git"
def __init__(self, root_dir=os.curdir, repo=None):
def __init__(self, root_dir=os.curdir):
"""Git class constructor.
Requires `Repo` class from `git` module (from gitpython package).
"""
super(Git, self).__init__(root_dir, repo=repo)
super(Git, self).__init__(root_dir)
import git
from git.exc import InvalidGitRepositoryError
try:
self.git = git.Repo(self.root_dir)
self.repo = git.Repo(self.root_dir)
except InvalidGitRepositoryError:
msg = "{} is not a git repository"
raise SCMError(msg.format(self.root_dir))
@@ -51,11 +56,50 @@ class Git(Base):
# http://pyinstaller.readthedocs.io/en/stable/runtime-information.html
env = fix_env(None)
libpath = env.get("LD_LIBRARY_PATH", None)
self.git.git.update_environment(LD_LIBRARY_PATH=libpath)
self.repo.git.update_environment(LD_LIBRARY_PATH=libpath)
self.ignored_paths = []
self.files_to_track = set()
@staticmethod
def clone(url, to_path, rev=None):
import git
ld_key = "LD_LIBRARY_PATH"
env = fix_env(None)
if is_binary() and ld_key not in env.keys():
# In fix_env, we delete LD_LIBRARY_PATH key if it was empty before
# PyInstaller modified it. GitPython, in git.Repo.clone_from, uses
# env to update its own internal state. When there is no key in
# env, this value is not updated and GitPython re-uses
# LD_LIBRARY_PATH that has been set by PyInstaller.
# See [1] for more info.
# [1] https://github.com/gitpython-developers/GitPython/issues/924
env[cast_bytes_py2(ld_key)] = ""
try:
tmp_repo = git.Repo.clone_from(
url,
to_path,
env=env, # needed before we can fix it in __init__
no_single_branch=True,
)
tmp_repo.close()
except git.exc.GitCommandError as exc:
raise CloneError(url, to_path, exc)
# NOTE: using our wrapper to make sure that env is fixed in __init__
repo = Git(to_path)
if rev:
try:
repo.checkout(rev)
except git.exc.GitCommandError as exc:
raise RevError(url, rev, exc)
return repo
@staticmethod
def is_repo(root_dir):
return os.path.isdir(Git._get_git_dir(root_dir))
@@ -70,28 +114,18 @@ class Git(Base):
@property
def dir(self):
return self.git.git_dir
return self.repo.git_dir
@property
def ignore_file(self):
return self.GITIGNORE
def _get_gitignore(self, path, ignore_file_dir=None):
if not ignore_file_dir:
ignore_file_dir = os.path.dirname(os.path.realpath(path))
def _get_gitignore(self, path):
ignore_file_dir = os.path.dirname(path)
assert os.path.isabs(path)
assert os.path.isabs(ignore_file_dir)
if not path.startswith(ignore_file_dir):
msg = (
"{} file has to be located in one of '{}' subdirectories"
", not outside '{}'"
)
raise FileNotInTargetSubdirError(
msg.format(self.GITIGNORE, path, ignore_file_dir)
)
entry = relpath(path, ignore_file_dir).replace(os.sep, "/")
# NOTE: using '/' prefix to make path unambiguous
if len(entry) > 0 and entry[0] != "/":
@@ -114,19 +148,14 @@ class Git(Base):
)
return False
def ignore(self, path, in_curr_dir=False):
base_dir = (
os.path.realpath(os.curdir)
if in_curr_dir
else os.path.dirname(path)
)
entry, gitignore = self._get_gitignore(path, base_dir)
def ignore(self, path):
entry, gitignore = self._get_gitignore(path)
if self._ignored(entry, gitignore):
return
msg = "Adding '{}' to '{}'.".format(relpath(path), relpath(gitignore))
logger.info(msg)
logger.debug(msg)
self._add_entry_to_gitignore(entry, gitignore)
@@ -134,8 +163,9 @@ class Git(Base):
self.ignored_paths.append(path)
@staticmethod
def _add_entry_to_gitignore(entry, gitignore):
def _add_entry_to_gitignore(self, entry, gitignore):
entry = GitWildMatchPattern.escape(entry)
with open(gitignore, "a+", encoding="utf-8") as fobj:
fobj.seek(0, os.SEEK_END)
if fobj.tell() == 0:
@@ -167,58 +197,65 @@ class Git(Base):
# NOTE: GitPython is not currently able to handle index version >= 3.
# See https://github.com/iterative/dvc/issues/610 for more details.
try:
self.git.index.add(paths)
self.repo.index.add(paths)
except AssertionError:
msg = (
"failed to add '{}' to git. You can add those files"
" manually using 'git add'."
" See 'https://github.com/iterative/dvc/issues/610'"
" for more details.".format(str(paths))
"failed to add '{}' to git. You can add those files "
"manually using `git add`. See "
"https://github.com/iterative/dvc/issues/610 for more "
"details.".format(str(paths))
)
logger.exception(msg)
def commit(self, msg):
self.git.index.commit(msg)
self.repo.index.commit(msg)
def checkout(self, branch, create_new=False):
if create_new:
self.git.git.checkout("HEAD", b=branch)
self.repo.git.checkout("HEAD", b=branch)
else:
self.git.git.checkout(branch)
self.repo.git.checkout(branch)
def branch(self, branch):
self.git.git.branch(branch)
self.repo.git.branch(branch)
def tag(self, tag):
self.git.git.tag(tag)
self.repo.git.tag(tag)
def untracked_files(self):
files = self.git.untracked_files
return [os.path.join(self.git.working_dir, fname) for fname in files]
files = self.repo.untracked_files
return [os.path.join(self.repo.working_dir, fname) for fname in files]
def is_tracked(self, path):
# it is equivalent to `bool(self.git.git.ls_files(path))` by
# it is equivalent to `bool(self.repo.git.ls_files(path))` by
# functionality, but ls_files fails on unicode filenames
path = relpath(path, self.root_dir)
return path in [i[0] for i in self.git.index.entries]
return path in [i[0] for i in self.repo.index.entries]
def is_dirty(self):
return self.git.is_dirty()
return self.repo.is_dirty()
def active_branch(self):
return self.git.active_branch.name
return self.repo.active_branch.name
def list_branches(self):
return [h.name for h in self.git.heads]
return [h.name for h in self.repo.heads]
def list_tags(self):
return [t.name for t in self.git.tags]
return [t.name for t in self.repo.tags]
def list_all_commits(self):
return [c.hexsha for c in self.repo.iter_commits("--all")]
def _install_hook(self, name, cmd):
command = '[ -z "$(git ls-files .dvc)" ] || exec dvc {}'.format(cmd)
command = (
'[ "$3" = "0" ]'
' || [ -z "$(git ls-files .dvc)" ]'
" || exec dvc {}".format(cmd)
)
hook = os.path.join(self.root_dir, self.GIT_DIR, "hooks", name)
hook = self._hook_path(name)
if os.path.isfile(hook):
with open(hook, "r+") as fobj:
@@ -231,6 +268,8 @@ class Git(Base):
os.chmod(hook, 0o777)
def install(self):
self._verify_dvc_hooks()
self._install_hook("post-checkout", "checkout")
self._install_hook("pre-commit", "status")
self._install_hook("pre-push", "push")
@@ -266,7 +305,7 @@ class Git(Base):
return basename == self.ignore_file or Git.GIT_DIR in path_parts
def get_tree(self, rev):
return GitTree(self.git, rev)
return GitTree(self.repo, rev)
def _get_diff_trees(self, a_ref, b_ref):
"""Private method for getting the trees and commit hashes of 2 git
@@ -284,10 +323,10 @@ class Git(Base):
trees = {DIFF_A_TREE: None, DIFF_B_TREE: None}
commits = []
if b_ref is None:
b_ref = self.git.head.commit
b_ref = self.repo.head.commit
try:
a_commit = self.git.git.rev_parse(a_ref, short=True)
b_commit = self.git.git.rev_parse(b_ref, short=True)
a_commit = self.repo.git.rev_parse(a_ref, short=True)
b_commit = self.repo.git.rev_parse(b_ref, short=True)
# See https://gitpython.readthedocs.io
# /en/2.1.11/reference.html#git.objects.base.Object.__str__
commits.append(a_commit)
@@ -322,4 +361,26 @@ class Git(Base):
return diff_dct
def get_rev(self):
return self.git.git.rev_parse("HEAD")
return self.repo.git.rev_parse("HEAD")
def resolve_rev(self, rev):
return self.repo.git.rev_parse(rev)
def close(self):
self.repo.close()
@cached_property
def _hooks_home(self):
return os.path.join(self.root_dir, self.GIT_DIR, "hooks")
def _hook_path(self, name):
return os.path.join(self._hooks_home, name)
def _verify_hook(self, name):
if os.path.exists(self._hook_path(name)):
raise GitHookAlreadyExistsError(name)
def _verify_dvc_hooks(self):
self._verify_hook("post-checkout")
self._verify_hook("pre-commit")
self._verify_hook("pre-push")
+4 -3
View File
@@ -45,7 +45,8 @@ class GitTree(BaseTree):
def tree_root(self):
return self.git.working_dir
def open(self, path, binary=False):
def open(self, path, mode="r", encoding="utf-8"):
assert mode in {"r", "rb"}
relative_path = relpath(path, self.git.working_dir)
@@ -61,9 +62,9 @@ class GitTree(BaseTree):
# the `open()` behavior (since data_stream.read() returns bytes,
# and `open` with default "r" mode returns str)
data = obj.data_stream.read()
if binary:
if mode == "rb":
return BytesIO(data)
return StringIO(data.decode("utf-8"))
return StringIO(data.decode(encoding))
def exists(self, path):
return self.git_object_by_path(path) is not None
+3 -5
View File
@@ -11,7 +11,7 @@ class BaseTree(object):
def tree_root(self):
pass
def open(self, path, binary=False):
def open(self, path, mode="r", encoding="utf-8"):
"""Open file and return a stream."""
def exists(self, path):
@@ -42,11 +42,9 @@ class WorkingTree(BaseTree):
def tree_root(self):
return self.repo_root
def open(self, path, binary=False):
def open(self, path, mode="r", encoding="utf-8"):
"""Open file and return a stream."""
if binary:
return open(path, "rb")
return open(path, encoding="utf-8")
return open(path, mode=mode, encoding=encoding)
def exists(self, path):
"""Test whether a path exists."""
+78 -34
View File
@@ -9,6 +9,7 @@ import subprocess
import logging
import signal
import threading
from itertools import chain
from dvc.utils import relpath
from dvc.utils.compat import pathlib
@@ -138,6 +139,7 @@ class Stage(object):
PARAM_OUTS = "outs"
PARAM_LOCKED = "locked"
PARAM_META = "meta"
PARAM_ALWAYS_CHANGED = "always_changed"
SCHEMA = {
Optional(PARAM_MD5): Or(str, None),
@@ -147,6 +149,7 @@ class Stage(object):
Optional(PARAM_OUTS): Or(And(list, Schema([output.SCHEMA])), None),
Optional(PARAM_LOCKED): bool,
Optional(PARAM_META): object,
Optional(PARAM_ALWAYS_CHANGED): bool,
}
TAG_REGEX = r"^(?P<path>.*)@(?P<tag>[^\\/@:]*)$"
@@ -163,6 +166,7 @@ class Stage(object):
locked=False,
tag=None,
state=None,
always_changed=False,
):
if deps is None:
deps = []
@@ -178,6 +182,7 @@ class Stage(object):
self.md5 = md5
self.locked = locked
self.tag = tag
self.always_changed = always_changed
self._state = state or {}
def __repr__(self):
@@ -237,12 +242,15 @@ class Stage(object):
if self.is_callback:
logger.warning(
"DVC-file '{fname}' is a 'callback' stage "
"DVC-file '{fname}' is a \"callback\" stage "
"(has a command and no dependencies) and thus always "
"considered as changed.".format(fname=self.relpath)
)
return True
if self.always_changed:
return True
for dep in self.deps:
status = dep.status()
if status:
@@ -285,7 +293,7 @@ class Stage(object):
if ret:
logger.warning("Stage '{}' changed.".format(self.relpath))
else:
logger.info("Stage '{}' didn't change.".format(self.relpath))
logger.debug("Stage '{}' didn't change.".format(self.relpath))
return ret
@@ -326,8 +334,6 @@ class Stage(object):
if interactive and not prompt.confirm(msg):
raise DvcException("reproduction aborted by the user")
logger.info("Reproducing '{stage}'".format(stage=self.relpath))
self.run(**kwargs)
logger.debug("'{stage}' was reproduced".format(stage=self.relpath))
@@ -438,25 +444,27 @@ class Stage(object):
wdir = kwargs.get("wdir", None)
cwd = kwargs.get("cwd", None)
fname = kwargs.get("fname", None)
add = kwargs.get("add", False)
# Backward compatibility for `cwd` option
if wdir is None and cwd is not None:
if fname is not None and os.path.basename(fname) != fname:
raise StageFileBadNameError(
"DVC-file name '{fname}' may not contain subdirectories"
" if '-c|--cwd' (deprecated) is specified. Use '-w|--wdir'"
" along with '-f' to specify DVC-file path with working"
" if `-c|--cwd` (deprecated) is specified. Use `-w|--wdir`"
" along with `-f` to specify DVC-file path with working"
" directory.".format(fname=fname)
)
wdir = cwd
else:
wdir = os.curdir if wdir is None else wdir
elif wdir is None:
wdir = os.curdir
stage = Stage(
repo=repo,
wdir=wdir,
cmd=kwargs.get("cmd", None),
locked=kwargs.get("locked", False),
always_changed=kwargs.get("always_changed", False),
)
Stage._fill_stage_outputs(stage, **kwargs)
@@ -468,11 +476,18 @@ class Stage(object):
stage._check_duplicated_arguments()
if not fname:
fname = Stage._stage_fname(
stage.outs, add=kwargs.get("add", False)
)
fname = Stage._stage_fname(stage.outs, add)
stage._check_dvc_filename(fname)
# Autodetecting wdir for add, we need to create outs first to do that,
# so we start with wdir = . and remap out paths later.
if add and kwargs.get("wdir") is None and cwd is None:
wdir = os.path.dirname(fname)
for out in chain(stage.outs, stage.deps):
if out.is_in_repo:
out.def_path = relpath(out.path_info, wdir)
wdir = os.path.abspath(wdir)
if cwd is not None:
@@ -498,8 +513,6 @@ class Stage(object):
stage.remove_outs(ignore_remove=False)
logger.warning("Build cache is ignored when using --remove-outs.")
ignore_build_cache = True
else:
stage.unprotect_outs()
if os.path.exists(path) and any(out.persist for out in stage.outs):
logger.warning("Build cache is ignored when persisting outputs.")
@@ -510,6 +523,7 @@ class Stage(object):
not ignore_build_cache
and stage.is_cached
and not stage.is_callback
and not stage.always_changed
):
logger.info("Stage is cached, skipping.")
return None
@@ -557,8 +571,8 @@ class Stage(object):
def _check_dvc_filename(fname):
if not Stage.is_valid_filename(fname):
raise StageFileBadNameError(
"bad DVC-file name '{}'. DVC-files should be named"
" 'Dvcfile' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
"bad DVC-file name '{}'. DVC-files should be named "
"'Dvcfile' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
relpath(fname), os.path.basename(fname)
)
)
@@ -614,6 +628,7 @@ class Stage(object):
md5=d.get(Stage.PARAM_MD5),
locked=d.get(Stage.PARAM_LOCKED, False),
tag=tag,
always_changed=d.get(Stage.PARAM_ALWAYS_CHANGED, False),
state=state,
)
@@ -624,16 +639,21 @@ class Stage(object):
def dumpd(self):
rel_wdir = relpath(self.wdir, os.path.dirname(self.path))
wdir = pathlib.PurePath(rel_wdir).as_posix()
wdir = wdir if wdir != "." else None
return {
key: value
for key, value in {
Stage.PARAM_MD5: self.md5,
Stage.PARAM_CMD: self.cmd,
Stage.PARAM_WDIR: pathlib.PurePath(rel_wdir).as_posix(),
Stage.PARAM_WDIR: wdir,
Stage.PARAM_LOCKED: self.locked,
Stage.PARAM_DEPS: [d.dumpd() for d in self.deps],
Stage.PARAM_OUTS: [o.dumpd() for o in self.outs],
Stage.PARAM_META: self._state.get("meta"),
Stage.PARAM_ALWAYS_CHANGED: self.always_changed,
}.items()
if value
}
@@ -643,7 +663,7 @@ class Stage(object):
self._check_dvc_filename(fname)
logger.info(
logger.debug(
"Saving information to '{file}'.".format(file=relpath(fname))
)
d = self.dumpd()
@@ -711,13 +731,12 @@ class Stage(object):
msg += " and " if (changed_deps and changed_outs) else ""
msg += "outputs {}".format(changed_outs) if changed_outs else ""
msg += "md5" if not (changed_deps or changed_outs) else ""
msg += " of '{}' changed. Are you sure you commit it?".format(
self.relpath
)
msg += " of '{}' changed.".format(self.relpath)
msg += "Are you sure you want to commit it?"
if not force and not prompt.confirm(msg):
raise StageCommitError(
"unable to commit changed '{}'. Use `-f|--force` to "
"force.`".format(self.relpath)
"force.".format(self.relpath)
)
self.save()
@@ -769,28 +788,48 @@ class Stage(object):
def _run(self):
self._check_missing_deps()
executable = os.getenv("SHELL") if os.name != "nt" else None
self._warn_if_fish(executable)
kwargs = {"cwd": self.wdir, "env": fix_env(None), "close_fds": True}
if os.name == "nt":
kwargs["shell"] = True
cmd = self.cmd
else:
# NOTE: when you specify `shell=True`, `Popen` [1] will default to
# `/bin/sh` on *nix and will add ["/bin/sh", "-c"] to your command.
# But we actually want to run the same shell that we are running
# from right now, which is usually determined by the `SHELL` env
# var. So instead, we compose our command on our own, making sure
# to include special flags to prevent shell from reading any
# configs and modifying env, which may change the behavior or the
# command we are running. See [2] for more info.
#
# [1] https://github.com/python/cpython/blob/3.7/Lib/subprocess.py
# #L1426
# [2] https://github.com/iterative/dvc/issues/2506
# #issuecomment-535396799
kwargs["shell"] = False
executable = os.getenv("SHELL") or "/bin/sh"
self._warn_if_fish(executable)
opts = {"zsh": ["--no-rcs"], "bash": ["--noprofile", "--norc"]}
name = os.path.basename(executable).lower()
cmd = [executable] + opts.get(name, []) + ["-c", self.cmd]
main_thread = isinstance(
threading.current_thread(), threading._MainThread
)
old_handler = None
p = None
try:
p = subprocess.Popen(
self.cmd,
cwd=self.wdir,
shell=True,
env=fix_env(os.environ),
executable=executable,
close_fds=True,
)
p = subprocess.Popen(cmd, **kwargs)
if main_thread:
old_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
p.communicate()
finally:
if main_thread:
if old_handler:
signal.signal(signal.SIGINT, old_handler)
if (p is None) or (p.returncode != 0):
@@ -833,6 +872,7 @@ class Stage(object):
if (
not force
and not self.is_callback
and not self.always_changed
and self._already_cached()
):
self.checkout()
@@ -850,10 +890,14 @@ class Stage(object):
raise MissingDataSource(paths)
def checkout(self, force=False, progress_callback=None):
failed_checkouts = []
for out in self.outs:
out.checkout(
failed = out.checkout(
force=force, tag=self.tag, progress_callback=progress_callback
)
if failed:
failed_checkouts.append(failed)
return failed_checkouts
@staticmethod
def _status(entries):
@@ -879,7 +923,7 @@ class Stage(object):
if self.changed_md5():
ret.append("changed checksum")
if self.is_callback:
if self.is_callback or self.always_changed:
ret.append("always changed")
if ret:
+30 -2
View File
@@ -3,6 +3,7 @@
from __future__ import unicode_literals
import os
import re
import sqlite3
import logging
@@ -10,7 +11,8 @@ from dvc.config import Config
from dvc.utils import remove, current_timestamp, relpath, to_chunks
from dvc.exceptions import DvcException
from dvc.utils.fs import get_mtime_and_size, get_inode
from dvc.utils.compat import fspath_py35
from dvc.utils.compat import fspath_py35, urlencode, urlunparse, is_py2
SQLITE_MAX_VARIABLES_NUMBER = 999
@@ -213,7 +215,11 @@ class State(object): # pylint: disable=too-many-instance-attributes
assert self.cursor is None
assert self.inserts == 0
empty = not os.path.exists(self.state_file)
self.database = sqlite3.connect(self.state_file)
# NOTE: we use nolock option because fcntl() lock sqlite uses
# doesn't work on some older NFS/CIFS filesystems.
# This opens a possibility of data corruption by concurrent writes,
# which is prevented by repo lock.
self.database = _connect_sqlite(self.state_file, {"nolock": 1})
self.cursor = self.database.cursor()
# Try loading once to check that the file is indeed a database
@@ -473,3 +479,25 @@ class State(object): # pylint: disable=too-many-instance-attributes
self.LINK_STATE_TABLE, ",".join(["?"] * len(chunk_unused))
)
self._execute(cmd, tuple(chunk_unused))
def _connect_sqlite(filename, options):
# Connect by URI was added in Python 3.4 and sqlite 3.7.7,
# we ignore options, which should be fine unless repo is on old NFS/CIFS
if is_py2 or sqlite3.sqlite_version_info < (3, 7, 7):
return sqlite3.connect(filename)
uri = _build_sqlite_uri(filename, options)
return sqlite3.connect(uri, uri=True)
def _build_sqlite_uri(filename, options):
# Convert filename to uri according to https://www.sqlite.org/uri.html, 3.1
uri_path = filename.replace("?", "%3f").replace("#", "%23")
if os.name == "nt":
uri_path = uri_path.replace("\\", "/")
uri_path = re.sub(r"^([a-z]:)", "/\\1", uri_path, flags=re.I)
uri_path = re.sub(r"/+", "/", uri_path)
# Empty netloc, params and fragment
return urlunparse(("file", "", uri_path, "", urlencode(options), ""))
+8 -3
View File
@@ -1,9 +1,9 @@
from __future__ import unicode_literals
from dvc.utils.compat import str, open, fspath
import os
import errno
import shutil
from dvc.utils.compat import str, open, fspath
class System(object):
@@ -11,6 +11,11 @@ class System(object):
def is_unix():
return os.name != "nt"
@staticmethod
def copy(src, dest):
src, dest = fspath(src), fspath(dest)
return shutil.copyfile(src, dest)
@staticmethod
def hardlink(source, link_name):
import ctypes
+10 -8
View File
@@ -4,13 +4,12 @@ import sys
import os
import time
import logging
import requests
import colorama
from pkg_resources import parse_version
from packaging import version
from dvc import __version__
from dvc.lock import Lock, LockError
from dvc.utils import is_binary, boxify
from dvc.utils import is_binary, boxify, env2bool
logger = logging.getLogger(__name__)
@@ -25,8 +24,10 @@ class Updater(object): # pragma: no cover
def __init__(self, dvc_dir):
self.dvc_dir = dvc_dir
self.updater_file = os.path.join(dvc_dir, self.UPDATER_FILE)
self.lock = Lock(dvc_dir, self.updater_file + ".lock")
self.current = parse_version(__version__).base_version
self.lock = Lock(
self.updater_file + ".lock", tmp_dir=os.path.join(dvc_dir, "tmp")
)
self.current = version.parse(__version__).base_version
def _is_outdated_file(self):
ctime = os.path.getmtime(self.updater_file)
@@ -41,10 +42,10 @@ class Updater(object): # pragma: no cover
func()
except LockError:
msg = "Failed to acquire '{}' before {} updates"
logger.debug(msg.format(self.lock.lock_file, action))
logger.debug(msg.format(self.lock.lockfile, action))
def check(self):
if os.getenv("CI") or os.getenv("DVC_TEST"):
if os.getenv("CI") or env2bool("DVC_TEST"):
return
self._with_lock(self._check, "checking")
@@ -80,6 +81,7 @@ class Updater(object): # pragma: no cover
def _get_latest_version(self):
import json
import requests
try:
r = requests.get(self.URL, timeout=self.TIMEOUT_GET)
@@ -93,7 +95,7 @@ class Updater(object): # pragma: no cover
json.dump(info, fobj)
def _is_outdated(self):
return parse_version(self.current) < parse_version(self.latest)
return version.parse(self.current) < version.parse(self.latest)
def _notify(self):
if not sys.stdout.isatty():
+157 -64
View File
@@ -9,8 +9,9 @@ from dvc.utils.compat import (
cast_bytes_py2,
StringIO,
fspath_py35,
fspath,
makedirs as _makedirs,
)
from dvc.utils.compat import fspath
import os
import sys
@@ -26,13 +27,14 @@ import colorama
import re
import logging
from shortuuid import uuid
from ruamel.yaml import YAML
logger = logging.getLogger(__name__)
LOCAL_CHUNK_SIZE = 1024 * 1024
LARGE_FILE_SIZE = 1024 * 1024 * 1024
LOCAL_CHUNK_SIZE = 2 ** 20 # 1 MB
LARGE_FILE_SIZE = 2 ** 30 # 1 GB
LARGE_DIR_SIZE = 100
@@ -42,44 +44,44 @@ def dos2unix(data):
def file_md5(fname):
""" get the (md5 hexdigest, md5 digest) of a file """
from dvc.progress import progress
from dvc.progress import Tqdm
from dvc.istextfile import istextfile
if os.path.exists(fname):
hash_md5 = hashlib.md5()
binary = not istextfile(fname)
size = os.path.getsize(fname)
bar = False
no_progress_bar = True
if size >= LARGE_FILE_SIZE:
bar = True
no_progress_bar = False
msg = "Computing md5 for a large file {}. This is only done once."
logger.info(msg.format(relpath(fname)))
name = relpath(fname)
total = 0
name = relpath(fname)
with open(fname, "rb") as fobj:
while True:
data = fobj.read(LOCAL_CHUNK_SIZE)
if not data:
break
with Tqdm(
desc=name,
disable=no_progress_bar,
total=size,
bytes=True,
leave=False,
) as pbar:
with open(fname, "rb") as fobj:
while True:
data = fobj.read(LOCAL_CHUNK_SIZE)
if not data:
break
if bar:
total += len(data)
progress.update_target(name, total, size)
if binary:
chunk = data
else:
chunk = dos2unix(data)
if binary:
chunk = data
else:
chunk = dos2unix(data)
hash_md5.update(chunk)
if bar:
progress.finish_target(name)
hash_md5.update(chunk)
pbar.update(len(data))
return (hash_md5.hexdigest(), hash_md5.digest())
else:
return (None, None)
return (None, None)
def bytes_md5(byts):
@@ -99,14 +101,13 @@ def dict_filter(d, exclude=()):
if isinstance(d, list):
return [dict_filter(e, exclude) for e in d]
elif isinstance(d, dict):
if isinstance(d, dict):
items = ((fix_key(k), v) for k, v in d.items())
return {
k: dict_filter(v, exclude) for k, v in items if k not in exclude
}
else:
return d
return d
def dict_md5(d, exclude=()):
@@ -118,10 +119,9 @@ def dict_md5(d, exclude=()):
def copyfile(src, dest, no_progress_bar=False, name=None):
"""Copy file with progress bar"""
from dvc.exceptions import DvcException
from dvc.progress import progress
from dvc.progress import Tqdm
from dvc.system import System
copied = 0
name = name if name else os.path.basename(dest)
total = os.stat(src).st_size
@@ -131,32 +131,56 @@ def copyfile(src, dest, no_progress_bar=False, name=None):
try:
System.reflink(src, dest)
except DvcException:
with open(src, "rb") as fsrc, open(dest, "wb+") as fdest:
while True:
buf = fsrc.read(LOCAL_CHUNK_SIZE)
if not buf:
break
fdest.write(buf)
copied += len(buf)
if not no_progress_bar:
progress.update_target(name, copied, total)
if not no_progress_bar:
progress.finish_target(name)
with Tqdm(
desc=name, disable=no_progress_bar, total=total, bytes=True
) as pbar:
with open(src, "rb") as fsrc, open(dest, "wb+") as fdest:
while True:
buf = fsrc.read(LOCAL_CHUNK_SIZE)
if not buf:
break
fdest.write(buf)
pbar.update(len(buf))
def move(src, dst):
dst = os.path.abspath(dst)
dname = os.path.dirname(dst)
if not os.path.exists(dname):
os.makedirs(dname)
def makedirs(path, exist_ok=False, mode=None):
path = fspath_py35(path)
if os.path.islink(src):
shutil.copy(os.readlink(src), dst)
os.unlink(src)
if mode is None:
_makedirs(path, exist_ok=exist_ok)
return
shutil.move(src, dst)
umask = os.umask(0)
try:
_makedirs(path, exist_ok=exist_ok, mode=mode)
finally:
os.umask(umask)
def move(src, dst, mode=None):
"""Atomically move src to dst and chmod it with mode.
Moving is performed in two stages to make the whole operation atomic in
case src and dst are on different filesystems and actual physical copying
of data is happening.
"""
src = fspath_py35(src)
dst = fspath_py35(dst)
dst = os.path.abspath(dst)
tmp = "{}.{}".format(dst, str(uuid()))
if os.path.islink(src):
shutil.copy(os.readlink(src), tmp)
os.unlink(src)
else:
shutil.move(src, tmp)
if mode is not None:
os.chmod(tmp, mode)
shutil.move(tmp, dst)
def _chmod(func, p, excinfo):
@@ -166,8 +190,8 @@ def _chmod(func, p, excinfo):
try:
os.chmod(p, perm)
except OSError as exc:
# broken symlink
if exc.errno != errno.ENOENT:
# broken symlink or file is not owned by us
if exc.errno not in [errno.ENOENT, errno.EPERM]:
raise
func(p)
@@ -208,7 +232,7 @@ def _to_chunks_by_chunks_number(list_to_split, num_chunks):
def to_chunks(list_to_split, num_chunks=None, chunk_size=None):
if (num_chunks and chunk_size) or (not num_chunks and not chunk_size):
raise ValueError(
"One and only one of 'num_chunks', 'chunk_size' must be defined"
"Only one among `num_chunks` or `chunk_size` must be defined."
)
if chunk_size:
return _split(list_to_split, chunk_size)
@@ -221,9 +245,11 @@ def is_binary():
return getattr(sys, "frozen", False)
# NOTE: Fix env variables modified by PyInstaller
# http://pyinstaller.readthedocs.io/en/stable/runtime-information.html
def fix_env(env=None):
"""Fix env variables modified by PyInstaller [1] and pyenv [2].
[1] http://pyinstaller.readthedocs.io/en/stable/runtime-information.html
[2] https://github.com/pyenv/pyenv/issues/985
"""
if env is None:
env = os.environ.copy()
else:
@@ -238,24 +264,70 @@ def fix_env(env=None):
else:
env.pop(lp_key, None)
# Unlike PyInstaller, pyenv doesn't leave backups of original env vars
# when it modifies them. If we look into the shim, pyenv and pyenv-exec,
# we can figure out that the PATH is modified like this:
#
# PATH=$PYENV_BIN_PATH:${bin_path}:${plugin_bin}:$PATH
#
# where
#
# PYENV_BIN_PATH - might not start with $PYENV_ROOT if we are running
# `system` version of the command, see pyenv-exec source code.
# bin_path - might not start with $PYENV_ROOT as it runs realpath on
# it, but always has `libexec` part in it, see pyenv source code.
# plugin_bin - might contain more than 1 entry, which start with
# $PYENV_ROOT, see pyenv source code.
#
# Also, we know that whenever pyenv is running, it exports these env vars:
#
# PYENV_DIR
# PYENV_HOOK_PATH
# PYENV_VERSION
# PYENV_ROOT
#
# So having this, we can make a rightful assumption about what parts of the
# PATH we need to remove in order to get the original PATH.
path = env.get("PATH", "")
parts = path.split(":")
bin_path = parts[1] if len(parts) > 2 else ""
pyenv_dir = env.get("PYENV_DIR")
pyenv_hook_path = env.get("PYENV_HOOK_PATH")
pyenv_version = env.get("PYENV_VERSION")
pyenv_root = env.get("PYENV_ROOT")
env_matches = all([pyenv_dir, pyenv_hook_path, pyenv_version, pyenv_root])
bin_path_matches = os.path.basename(bin_path) == "libexec"
# NOTE: we don't support pyenv-win
if os.name != "nt" and env_matches and bin_path_matches:
# removing PYENV_BIN_PATH and bin_path
parts = parts[2:]
if parts:
# removing plugin_bin from the left
plugin_bin = os.path.join(pyenv_root, "plugins")
while parts[0].startswith(plugin_bin):
del parts[0]
env["PATH"] = ":".join(parts)
return env
def convert_to_unicode(data):
if isinstance(data, builtin_str):
return str(data)
elif isinstance(data, dict):
if isinstance(data, dict):
return dict(map(convert_to_unicode, data.items()))
elif isinstance(data, list) or isinstance(data, tuple):
if isinstance(data, (list, tuple)):
return type(data)(map(convert_to_unicode, data))
else:
return data
return data
def tmp_fname(fname):
""" Temporary name for a partial download """
from shortuuid import uuid
return fspath(fname) + "." + str(uuid()) + ".tmp"
@@ -388,3 +460,24 @@ def relpath(path, start=os.curdir):
):
return path
return os.path.relpath(path, start)
def env2bool(var, undefined=False):
"""
undefined: return value if env var is unset
"""
var = os.getenv(var, None)
if var is None:
return undefined
return bool(re.search("1|y|yes|true", var, flags=re.I))
def resolve_output(inp, out):
from dvc.utils.compat import urlparse
name = os.path.basename(urlparse(inp).path)
if not out:
return name
if os.path.isdir(out):
return os.path.join(out, name)
return out

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