Compare commits

...
219 Commits
Author SHA1 Message Date
Ruslan Kuprieiev 82e559a51a dvc: bump to 2.6.3 2021-08-17 15:17:48 +03:00
Ruslan Kuprieiev 33c6a9b19b analytics: introduce DVC_NO_ANALYTICS (#6453)
Replacing DVC_ANALYTICS. This is following our other env conventions and
will not have effect if set to false/no/etc, and will only work if set
to true/yes/y/etc. Will disable analytics even if it is explicitly
enabled in our configs.

Kudos @skshetry
2021-08-17 15:16:40 +03:00
Saugat Pachhai 14cc748139 Increase coverage target to 90%. 2021-08-17 12:23:35 +05:45
David de la Iglesia Castro 40abf34c8c Update dvc exp branch help (#6439) 2021-08-17 11:14:44 +09:00
Ruslan Kuprieiev 9fef38e778 dvc: bump to 2.6.2 2021-08-17 02:52:07 +03:00
Batuhan Taskaya b44e1c3646 fetched: correctly order mixed odbs (#6436) 2021-08-17 02:51:08 +03:00
Ruslan Kuprieiev c9b3d9dbae dvc: bump to 2.6.1 2021-08-17 01:49:06 +03:00
Ruslan Kuprieiev c2bf0dc713 analytics: add DVC_ANALYTICS env var (#6448) 2021-08-17 01:48:38 +03:00
Saugat Pachhai 451bd3ebc8 Change ui messaging for the updater (#6437) 2021-08-16 18:31:44 +03:00
Arjun Vikram 41bc81910f plots: upgrade to latest Vega library versions (#6371) 2021-08-16 16:22:09 +02:00
Ruslan Kuprieiev 0aec0e3249 updater: only notify in dvc doctor (#6247)
We've used updater for years now to notify our users ASAP about new
releases. These days dvc is more stable and we've actually been
receiving numerous complains about updater being too annoying and
even introduced a config flag to turn it off, which a lot of people do
and which defeats its purpose. Instead of introducing some complex logic
that would notify more rarely and once for the user instead of once for
every dvc project they have - this PR just turns it off for everything,
except `dvc doctor/version`, which is the command that people use when
they are running into issues and that will give them a hint to just try
out the newest version first, as it might be already fixed.

This PR is a more of a compromise, to get the most out of updater while
removing a pain point of it being too annoying, without investing much
time into it and not having to maintain complex unnecessary logic. We
might get back to introducing something more intricate in the future,
if the need arises.

Fixes #5912
2021-08-16 17:28:49 +05:45
Batuhan Taskaya 8406e605ab fs: find(..., detail=True) now casts values properly (#6432)
* fs: find(..., detail=True) now casts values properly

* bump pydrive2
2021-08-16 14:26:07 +03:00
Saugat Pachhai 1bb7f39e14 add support for Python 3.10 (#6422)
* add support for Python 3.10

pyarrow is not available, so we are skipping tests for it.
And, restricting `hdfs` extras to be not installed in Python3.10.

Pathlib removed `_init` method and `init` parameter, so just working
around that. It's not a big change.

* skip windows on 3.10; update pip/wheel/setuptools during install

pywin32 is not yet available for 3.10 Python, so skipping those for now

* install libgit2 with Python3.10 as pygit2 wheel is not available

* install libgit2

* only run on macOS, libgit2-1.1 not available on ubuntu
2021-08-16 16:24:28 +05:45
dependabot[bot] a285d2ba47 build(deps): Bump pygments from 2.9.0 to 2.10.0 (#6430)
Bumps [pygments](https://github.com/pygments/pygments) from 2.9.0 to 2.10.0.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.9.0...2.10.0)

---
updated-dependencies:
- dependency-name: pygments
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-16 10:03:34 +03:00
Ruslan Kuprieiev 13c1314099 dvc: bump to 2.6.0 2021-08-15 17:39:36 +03:00
Ruslan Kuprieiev 27b54198a4 pyinstaller: build: check dvc doctor output (#6425) 2021-08-15 17:30:17 +03:00
Saugat Pachhai a7f01b65a5 Unpin networkx
Networkx v2.6.2 now does not require any additional requirements. All of the other 2.6 releases have been yanked. So, it's safe for us to unpin it.
2021-08-13 14:53:10 +05:45
Gao de97a49401 Remove a special queued experiments (#6393)
* Remove a special queued experiments

fix #6157
1. dvc exp remove not accept queued experiments name
2. add some tests for this feature

* Extract tests and add revision support

1. Extract remove experiments to a new file.
2. revision can be used to remove special queued experiment

* Accept shortened revisions

* Split removing committed and queued exp functions

* Api name change

* Return to the old API name

* shorten some functions

* Error message change

* Better test cases, more corner case

* Still raise exception in a mixed case
2021-08-13 15:15:21 +08:00
dependabot[bot]andBatuhan Taskaya 4c957bf4ab build(deps): bump google-cloud-storage from 1.41.1 to 1.42.0 (#6415)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.41.1 to 1.42.0.
- [Release notes](https://github.com/googleapis/python-storage/releases)
- [Changelog](https://github.com/googleapis/python-storage/blob/master/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-storage/compare/v1.41.1...v1.42.0)

---
updated-dependencies:
- dependency-name: google-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: Batuhan Taskaya <[email protected]>
2021-08-12 13:15:34 +03:00
6dd435eefe Push running checkpoint to remote (#6332)
* Push running checkpoint to remote

Fix #6182

* Update dvc/repo/experiments/executor/base.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* Get the branch name

* Finish this PR

1. move env name to dvc/env.py.
2. add some tests for it.

* Update after finally commit

* Some problems found in the review

1. change the behavior of self remote
2. do not use string DVC_EXP_AUTO_PUSH
3. downgrade the logger level in auto push
4. use full branch ref

* Update dvc/repo/experiments/executor/base.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* add a remote check before auto push

* Split current one env into two

* Rename DVC_EXP_CHECKPOINT_PUSH to DVC_EXP_AUTO_PUSH

* value error

* Remove getenv_bool switch to env2bool

* Better on handling Dulwich exceptions

* Name changing removing prints

* Update dvc/env.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* Some changes in review

* Add validation to repo URL

* Add new type of SCMError

* Update dvc/repo/experiments/executor/base.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* Use spy tests call counts and args

* Update dvc/scm/git/backend/base.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* Update dvc/scm/git/backend/dulwich.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Remove validate_git_repo to validate_git_remote

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-08-12 15:17:08 +08:00
Saugat Pachhai fb96b13f13 Fix issue while sending analytics report (#6407)
* demonstrate issue while sending analytics report

When sending analytics, we are not flushing the data to the temp file
which causes failure at times, especially on Windows

* close file before calling daemonized analytics send command

This way, the contents bufferred will get a chance to be flushed.
2021-08-10 16:07:46 +03:00
Peter Rowlands (변기호) 6c646a7070 pygit: account for locked dirs on windows during checkout (#6408) 2021-08-10 15:18:32 +03:00
dependabot[bot] 955aedee13 build(deps): bump jaraco-windows from 5.5.0 to 5.6.0 (#6396)
Bumps [jaraco-windows](https://github.com/jaraco/jaraco.windows) from 5.5.0 to 5.6.0.
- [Release notes](https://github.com/jaraco/jaraco.windows/releases)
- [Changelog](https://github.com/jaraco/jaraco.windows/blob/main/CHANGES.rst)
- [Commits](https://github.com/jaraco/jaraco.windows/compare/v5.5.0...v5.6.0)

---
updated-dependencies:
- dependency-name: jaraco-windows
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-10 15:16:15 +03:00
Ruslan Kuprieiev e99e6cd367 repofs: use underlying fs.download to download files (#6401) 2021-08-10 13:12:56 +03:00
Batuhan Taskaya 1f8278252a gdrive: migrate to pydrive2.fs (#6399) 2021-08-10 12:59:44 +03:00
dependabot[bot] 38cd7d01c2 build(deps): bump wheel from 0.36.2 to 0.37.0 (#6404)
Bumps [wheel](https://github.com/pypa/wheel) from 0.36.2 to 0.37.0.
- [Release notes](https://github.com/pypa/wheel/releases)
- [Changelog](https://github.com/pypa/wheel/blob/master/docs/news.rst)
- [Commits](https://github.com/pypa/wheel/compare/0.36.2...0.37.0)

---
updated-dependencies:
- dependency-name: wheel
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-10 08:35:38 +05:45
Maxime RischardandRuslan Kuprieiev 63b710b7c0 More granular read-write lock requests in the run() and reproduce() methods of the Stage class (#5815)
* Remove rwlock decorator for the run() method of the Stage class.

Instead of pre-emptively grabbing read locks on the dependencies
and write locks on the outputs when calling the run() method,
delegate the locking mechanism to other methods.
This results in more selective locking that is less likely
to cause a LockError when executing steps in parallel.

* Remove rwlocked decorator from reproduce() method of Stage class.

* Reinstate read-lock (not write) for run method of Stage class

* fix bugs and typos

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-08-10 01:49:21 +03:00
Ruslan Kuprieiev d97a19e97d refodb: cache ref objects (#6392)
* refodb: cache ref objects

* odb: ignore FileNotFoundError when removing
2021-08-09 13:53:38 +03:00
Saugat Pachhai fd76e0991f cleanup index, and add test for used_objs (#6390) 2021-08-06 15:35:01 +05:45
Gao 3d604a8455 Show name of experiments in the queue (#6382)
* Show name of stashed experiments

* Add tests for it

* Reorder the stash and commit rev name
2021-08-06 11:02:16 +09:00
Saugat Pachhai 6c33fbb617 introduce index (#6300)
* introduce index

* migrate tests to use index

* migrate checkout/diff to use index.outs

* test for index

* fix typings

* add few more tests
2021-08-05 14:23:40 +03:00
Nikita KodenkoandPeter Rowlands 3be395b3a7 Unittest to pytest: moved few unittest tests to pytest (#6379)
* tests: rm TestGit in func/test_init.py

* tests: func: rm TestSCMGit in test_scm.py

* test: func: rm _check_dirs in test_init.py

* tests: func: minor fixes in test_scm.py

* Update tests/func/test_scm.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
2021-08-04 15:11:09 +03:00
Peter Rowlands (변기호) 114a07e7a4 objects: use object IDs and references instead of naive objs in status/transfer (#6360)
* objects.status: use object IDs instead of naive objects

* objects.transfer: use object IDs instead of naive objects

* objects: add ReferenceHashFile

* odb: add ReferenceObjectDB

* repo: collect used object IDs instead of naive objs

* support basic/naive fs serialization

* objects.stage: stage file objects in addition to trees

* objects: remove save(), update transfer()

* output/dep: update for osave removal

* path_info: support both url and path attributes

* status/gc: use object IDs instead of naive objs

* diff: update staging usage

* commit: update staging usage

* tests: clean staging when needed

* tests: update for save/transfer/stage changes

* dvcfs: don't allow config serialization (repofs should be used instead)

* utils: return mtime as str, size as int

* tests: update unit tests for save/stage/transfer changes

* objects: use Repo.open when needed in ReferenceHashFile

* fix ls test staging

* objects.stage: don't skip hash check when loading from state

- individual hashes will still be loaded from state if they are still
  valid

* ReferenceHashFile: serialize repo fs paths relative to repo root

* preserve state when staging

* objects.tree: skip hash computation in digest() when we have already
loaded hash from state

* always check file existence in referencehashfile

* update repofs test since stage no longer allows nonexistent files

* odb: only protect on check() if hash was verified

* staging: use unique memfs namespace for staging per dest ODB

* tests: clean full staging namespace

* refodb: preserve repofs cache dir

* repodependency: stage imports to local ODB and strict check circular
imports

* fetch: fetch staging last

* staging: make dest odb mandatory and preserve state

* refodb: cache nonlocal fs's

* fix rebase errors

* repofs: support full instantiation from fs config

* repofs: preserve subrepo factory for erepos

* tests: clarify repofs dirty file behavior
2021-08-04 13:55:52 +03:00
Peter Rowlands (변기호) 34697ef298 exp gc: fix --queued warning message (#6381) 2021-08-03 10:37:38 +00:00
Saugat Pachhai 1eccfa9d33 benedict: cleanup code and add tests (#6380) 2021-08-03 12:28:42 +05:45
dependabot[bot]andPeter Rowlands 6c14bf39dc build(deps): bump pylint from 2.8.3 to 2.9.6 (#6369)
* build(deps): bump pylint from 2.8.3 to 2.9.6

Bumps [pylint](https://github.com/PyCQA/pylint) from 2.8.3 to 2.9.6.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.8.3...v2.9.6)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

* pylint: fix 2.9.6 issues

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Peter Rowlands <[email protected]>
2021-08-03 10:29:55 +09:00
Saugat Pachhai 7444696dcd Fix merging of params when empty (#6378)
* Fix merging of params when empty

Recent release of python-benedict==0.24.1 started ignoring empty dictionaries
during merge. The way we expected to work was questionable and also how
benedict itself tries to handle the references to empty dictionary is
also questionable.

I consider this to be a proper way to handle this even if this gets fixed
on the benedict side.

* Revert "setup: limit python-benedict version (#6377)"

This reverts commit 15de826266.

* add comment for why passing through benedict
2021-08-02 22:45:04 +03:00
pre-commit-ci[bot] 76be53bc3e [pre-commit.ci] pre-commit autoupdate (#6376)
updates:
- [github.com/timothycrosley/isort: 5.9.2 → 5.9.3](https://github.com/timothycrosley/isort/compare/5.9.2...5.9.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-08-02 21:46:35 +03:00
Ruslan Kuprieiev 8d71c3861f fs: raise RemoteNotFoundError (#6375) 2021-08-02 21:09:55 +03:00
Ruslan Kuprieiev 15de826266 setup: limit python-benedict version (#6377)
Temporary fix for #6374 Kudos @skshetry
2021-08-02 20:44:03 +03:00
Batuhan Taskaya 9634efeb84 ssh: handle concurrency issues for raw command execution (#6370)
* ssh: handle concurrency issues for raw command execution

* setup: bump sshfs
2021-07-29 14:40:49 +03:00
Ruslan Kuprieiev e3a541aa00 setup: relax aiobotocore requirements (#6367)
Fixes #6366
2021-07-28 16:19:16 +03:00
Batuhan Taskaya 9b4b6e5b31 ssh: migrate to sshfs (#6295) 2021-07-28 12:49:53 +03:00
dependabot[bot] b294bca383 build(deps): bump codecov/codecov-action from 2.0.1 to 2.0.2 (#6361)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-28 10:21:10 +03:00
Batuhan Taskaya 4a843dbd61 odb: use regular upload() when the source filesystem is local (#6365)
* odb: use regular upload() when the source filesystem is local

* support download_file too
2021-07-27 15:55:49 +03:00
Ruslan Kuprieiev 1e43c303a7 output: cache tree objects (#6301) 2021-07-24 23:07:43 +03:00
meieraleandAlexander Meier 985c0328c7 remote: add excludes for AzureDefaultCredentials (#6359)
add additional excludes to DefaultAzureCredential
which are needed in a Windows Environment to
use az cli provided credentials, etc.

Co-authored-by: Alexander Meier <[email protected]>
2021-07-23 16:02:16 +03:00
Batuhan Taskaya d5722e3edf fsspec: use rm_file() (#6358) 2021-07-23 14:10:16 +03:00
Paweł Redzyński 67b42ebe4f plots: cleanup data extraction (#6355)
* plots: cleanup data extraction

* fixup
2021-07-23 10:28:36 +09:00
Batuhan Taskaya 1d7e754044 setup: bump flatten-dict to 0.4.1 (#6357) 2021-07-22 21:46:18 +03:00
Batuhan Taskaya c8739008d1 fsspec: implement callback mixins (#6322)
* fsspec: implement callback mixins

* drop redundant refresh() calls
2021-07-22 18:42:28 +03:00
Nikita KodenkoandSaugat Pachhai e731e35e6c tests: moved few unittest tests to pytest (#6339)
* tests: moved few unittest tests to pytest

* tests: unittest.mock -> pytest-mock in few tests

* tests: test_prompt: unittest.mock -> pytest-mock

* Update tests/unit/test_daemon.py

Co-authored-by: Saugat Pachhai <[email protected]>

Co-authored-by: Saugat Pachhai <[email protected]>
2021-07-22 18:06:54 +03:00
Peter Rowlands (변기호) cd7c95a785 ci: temporarily mark gdrive xfail (#6348) 2021-07-22 11:14:54 +03:00
dependabot[bot] 771b89ddf2 build(deps): bump google-cloud-storage from 1.41.0 to 1.41.1 (#6341)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.41.0 to 1.41.1.
- [Release notes](https://github.com/googleapis/python-storage/releases)
- [Changelog](https://github.com/googleapis/python-storage/blob/master/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-storage/compare/v1.41.0...v1.41.1)

---
updated-dependencies:
- dependency-name: google-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-22 00:42:07 +03:00
Saugat Pachhai 64587e8b6d do not print in style by default for errors (#6344) 2021-07-21 14:42:29 +03:00
Saugat Pachhai 5faf9b3a7d update webdav4 to latest fsspec version (#6343) 2021-07-21 10:17:14 +00:00
Peter Rowlands (변기호) 12d1e3e981 objects: fix gdrive CI issue (#6338)
* fs: ensure gdrivefs.info returns integer size

* transfer: only verify on pull
2021-07-20 18:54:49 +03:00
pre-commit-ci[bot] d95bd62f8e [pre-commit.ci] pre-commit autoupdate (#6333)
updates:
- [github.com/ambv/black: 21.6b0 → 21.7b0](https://github.com/ambv/black/compare/21.6b0...21.7b0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-07-20 14:12:05 +03:00
dependabot[bot] 005bca1da6 build(deps): bump codecov/codecov-action from 1.5.2 to 2.0.1 (#6334)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1.5.2 to 2.0.1.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v1.5.2...v2.0.1)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-20 14:06:50 +03:00
Peter Rowlands (변기호) 3d84df930b objects: migrate remote push/pull to objects.transfer (#6308)
* objects: migrate remote status to objects.status

* data_cloud: use objects.status

* objects: migrate remote._process to objects.transfer

* data_cloud: use objects.transfer

* objects: handle memfs staging objects in transfer

* fetch/import: use unified transfer()

* update get_remote and transfer exception usage

* update tests

* objects.transfer: use src ODB verify rules after xfer

* odb: move index from remote into odb

* objects.transfer: skip src status check when possible

* update tests

* remove dvc.remote

* odb.index: migrate to diskcache

* objects: fix status/transfer optimization

* update index usage

* state: remove dead sqlite related code

* update cloud tests

* drop unnecessary remove in tests

* use abstract base class in odb index

* status: load trees from local cache instead of remote odb when possible

* handle status when dir cache is explicitly removed
2021-07-20 13:30:01 +03:00
Batuhan Taskaya 66d0df2d12 setup: upgrade adlfs to 2021.7.1 (#6337)
The previous release got yanked on the PyPI, due to it containing backwards incompatible changes.
2021-07-20 08:59:45 +00:00
Christian Clauss d8b67f6f78 Upgrade to modern Python syntax (#6296) 2021-07-19 01:30:37 +03:00
dependabot[bot] 6711b647d9 build(deps): bump google-cloud-storage from 1.40.0 to 1.41.0 (#6311)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.40.0 to 1.41.0.
- [Release notes](https://github.com/googleapis/python-storage/releases)
- [Changelog](https://github.com/googleapis/python-storage/blob/master/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-storage/compare/v1.40.0...v1.41.0)

---
updated-dependencies:
- dependency-name: google-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-19 00:41:23 +03:00
Batuhan Taskaya 1e81983d6d import-url: use the same chunk size for istextblock() calls (#6303) 2021-07-19 00:28:42 +03:00
Ruslan Kuprieiev 97809661bd setup: pathspec <0.9.0 (#6331) 2021-07-18 18:24:28 +03:00
Batuhan Taskaya b229588df3 azure: move to default fsspec loop (#6323) 2021-07-18 17:47:20 +03:00
Sayanta Banerjee cae117cfd4 Update README.rst (#6330)
* Update README.rst

More clean and simple

* Update README.rst

Rectification from the previous pull
2021-07-16 23:44:30 +03:00
Paweł Redzyński 21af7438db plots/metrics/params/experiments show: stop throwing exceptions (#5984)
* params/metrics/plots/exp show: introduce onerror

* fixup

* plots/metrics/params: removing errors

* removing more errors

* removing errors

* some more removing

* new plots ux

* new ux for metrics

* new ux for params

* fixing tests

* fixups

* minor test fixes

* minor test fixes

* commands: plots/params: fix ui imports

* working on error handlng decorator, stopped at show checkpoints

* error_handler, further dvelopment

* plots just before moving loading to collect

* appending errors to results: initial passing

* fixup

* onerror: remove onerror fixture

* fixups

* plots/metrics/params/experiments: temporarily remove error warnings

* plots: extract parsing out of class method

* metrics/experiments: provide exception serializer for show-json

* params/plots/metrics: signal errors

* rebase fixups

* fix TODOs

* fixup

* skshetry's review refactor
2021-07-16 15:36:04 +02:00
Batuhan Taskaya 27c1722b90 setup: bump ossfs==2021.7.5 (#6326) 2021-07-16 20:48:57 +08:00
Batuhan Taskaya 6c8673b158 setup: upgrade fsspec/s3fs/gcsfs to 2021.7.0 (#6317) 2021-07-15 11:33:07 +02:00
Batuhan Taskaya 6275f5d0d7 fsspec: create directories on upload/upload_fobj (#6309) 2021-07-14 15:42:51 +03:00
GaoandRuslan Kuprieiev 93f36d9351 oss: migrate to ossfs (#6307)
* Migrate to ossfs

fix 6177

* Add verification to object stored on oss

* Change the oss remote's dependency.

pass all of the tests

* Performance optimaztion and ossfs version update

* Remove the argument to set the resumable get/put

1. Experiments shows resumable get/put has little influnence on performance.
2. Just use resumable ones as default

* Use a new api for performance optimization

* Without filesize progress bar version

* With progress bar version

* Bump and add comments

1. Bump ossfs from 2021.6.2 to 2021.7.0
2. add some comments to the verification trigger

* Change the name of kwrags following parent class.

* Bump to new ossfs and rewrite upload_fobj

Because some problem with oss-emulater
1. bump to ossfs 2021.7.1
2. rewrite upload_fobj

* Optimization on rm_files

* Some optimization follow the old version.

* Update dvc/fs/oss.py

Co-authored-by: Ruslan Kuprieiev <[email protected]>

* Update dvc/objects/db/oss.py

Co-authored-by: Ruslan Kuprieiev <[email protected]>

* bump into ossfs 2021.7.2

* Bump into ossfs 2021.7.3OC

* Switch to real_oss_server

* Switch to a real oss server

* Use the oss bucket located in US.

* Remove some emulator related code

* Add envs to ci

* remove some oss pytest XFAIL

Because we didn't use emulator, we can remove some pytest XFAILs

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-07-13 15:27:34 +03:00
Batuhan Taskaya 58f0911234 azure: support directory imports with azure (#6245)
* azure: support directory imports with azure

* upgrade azurite to 3.13

* tmp: use azurite 3.12?

* upgrade to azurite 3.13.1

* bump adlfs to 2021.7.0
2021-07-13 13:39:30 +03:00
pre-commit-ci[bot] f45311e658 [pre-commit.ci] pre-commit autoupdate (#6306)
updates:
- [github.com/timothycrosley/isort: 5.9.1 → 5.9.2](https://github.com/timothycrosley/isort/compare/5.9.1...5.9.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-07-13 08:49:27 +05:45
Peter Rowlands (변기호) 1271db6b59 exp show: display running/queued state for experiments (#6174)
* exp: allow fetching from arbitrary executor by git URL

* exp: write info for running executors to .dvc/tmp/exp/run

currently only includes PID and git fetch URL

* exp: refactor stash/queue cleanup

* exp: add ability to collect dict of currently running experiments

* exp show: display run/queue state in the table

* exp run: unlock main repo while running tmpdir executors

* add tests for new exp show behavior

* fix workspace checkpoint run handling

* fix issue where running stash revs were shown as queued

* use "running"/"queued"

* add executor (location) column

* compare: add TabularData.is_empty

* use td.is_empty to drop state/executor

* use "workspace" and "temp" as executor names
2021-07-13 00:20:38 +09:00
Saugat Pachhai beb610c3ab add: ignore duplicated entries (#6286) 2021-07-12 20:43:41 +05:45
Batuhan Taskaya 4557de420c import-url: compute the stage hash in --to-remote (#6175)
* import-url: compute the stage hash in --to-remote
* save_deps() before calculating the md5
2021-07-12 09:59:34 +03:00
dependabot[bot] 481a411a35 build(deps): bump wsgidav from 3.1.0 to 3.1.1 (#6302)
Bumps [wsgidav](https://github.com/mar10/wsgidav) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/mar10/wsgidav/releases)
- [Changelog](https://github.com/mar10/wsgidav/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mar10/wsgidav/compare/v3.1.0...v3.1.1)

---
updated-dependencies:
- dependency-name: wsgidav
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-12 11:26:14 +05:45
Ruslan Kuprieiev 52047214b7 fs: repo: handle directories with multiple artifacts (#6278)
Related to #5677
2021-07-12 02:17:07 +03:00
Peter Rowlands (변기호) 71bae7e4af objects: use separate staging ODB for staging trees (#6195)
* fs: use fsspecwrapper in memfs

* odb: add memfs staging ODB in ODBManager

* odb: add read_only attribute for odb instances

* objects: handle memfs paths when staging trees

* odb: remove git ODB

* output: use staging ODB

* RepoDependency: use staging odb for imports

* dvcfs: handle case where dir cache is unavailable

* fix circular import check

* odb: use temp local ODB for staging

* objects: use staging ODB in stage() and tree load

* objects: add typing for add/check/load/stage

* output/dep: use new staging

* add unit tests for local odb staging

* localfs: account for string paths in move()

* include local in odb.by_scheme

* tests: count dir cache like any other object in test_pull_git_imports

* odb: clear staging on gc()

* objects: add tree.get/filter/insert

* output: use obj.filter/get for granular dir commit

* squash filter logic output

* dependency: don't filter imports at the dep level

* don't init staging ODB for external cache

* use staging ODB when computing tree hash for external outs

* remove unneeded force in test granular commit

* use unique-per-manager memfs URLs for staging

* odb: make staging per-ODBManager

* objects: move state dependent staging into ODBManager

* objects: support checking multiple odbs

- staging dependent checks can be done via ODBManager.check

* checkout: do state lookup before explicit stage() in _changed()

* output: use odbmanager based staging

* RepoDependency: use odbmanager based staging

* update obj staging unit tests

* checkout: remove check(), handle individual file exceptions on link()

* diff: use odbmanager based staging

* objects: add ObjectPermissionError for read/write perm errors

* odb: move staging out of odbmanager

* objects: handle staging ODB in objects.stage()

* objects.tree: don't stage anything in tree methods

* odb: clear staged objects on gc()

* remote: don't filter objs by scheme, objs are already grouped by ODB

* diff: update objects.stage usage

* output/dep: update stage() usage

* update ODB tests

* update func tests

* remove memfs from FS_MAP

* revert unneeded objects.check() changes

* remove unused tree.insert()

* revert unused odbmanager changes

* add separate odb error classes

* stage external outs directly in ODB

* check for obj existence in main ODB before staging

* use .dvc/tmp/staging instead of per-odb staging

* gc: only gc loal staging once
2021-07-09 18:48:28 +03:00
Ruslan Kuprieiev 67c46b98dd dvc: bump to 2.5.4 2021-07-08 23:02:16 +03:00
Ruslan Kuprieiev bd5809572e setup: relax requirements for importlib-metadata 2021-07-08 23:01:48 +03:00
Ruslan Kuprieiev 05832c81fc dvc: bump to 2.5.3 2021-07-08 19:34:26 +03:00
Ruslan KuprieievandSaugat Pachhai 86df89e3a8 pkg: don't build universal wheels (#6299)
Kudos @skshetry for discovering that we've been publishing py2.py3
wheels on pypi and researching the cause.

Co-authored-by: Saugat Pachhai <[email protected]>

Co-authored-by: Saugat Pachhai <[email protected]>
2021-07-08 19:33:52 +03:00
Ruslan Kuprieiev c33776eaee dvc: bump to 2.5.2 2021-07-08 18:30:11 +03:00
kareemjanoandpre-commit-ci[bot] 22781f5ef0 fs: s3: don't forget to pass verify option (#6298)
* Update s3.py

pass verify flag when creating a session

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-07-08 21:12:36 +05:45
Ruslan Kuprieiev ae478ccdc0 dvc: bump to 2.5.1 2021-07-08 17:48:15 +03:00
Batuhan Taskaya 87b49fd9f9 staging: optimize the number of info() calls (#6281)
* staging: optimize the number of info() calls

* fs: make info() give details about the object
2021-07-07 22:53:43 +03:00
Batuhan Taskaya f90b9cdee1 doctor: show versions for fs requirements (#6273)
* doctor: show versions for fs requirements

* plugins => supports
2021-07-07 21:07:16 +03:00
Christian ClaussandSaugat Pachhai 40f24e5668 pre-commit: introduce codespell check
* codespell --ignore-words-list="ba,datas,fo"

* up-to-date -> uptodate

* Add pre-commit hook...

* Pre-commit needs quotes?

* Single quotes?

* Arrrgggsss

* Update .pre-commit-config.yaml

Co-authored-by: Saugat Pachhai <[email protected]>

Co-authored-by: Saugat Pachhai <[email protected]>
2021-07-07 17:14:15 +02:00
Hadas Arikandpre-commit-ci[bot] 9d9ff10be1 diff: only show states with values in summary (#6229)
* Fixed diff summary to only show states with values.

* Removed redundant comma by using join.

* Swithced for loop to generator for better readability.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-07-06 21:03:32 +09:00
Batuhan Taskaya 98f7d1e481 objects: hash_file: retrieve size from hash_info (#6284)
* objects: hash_file: retrieve size from hash_info

* objects: drop Tree.size implementation
2021-07-06 10:56:13 +03:00
Saugat Pachhai 9cf22d3edc add: reduce os.fspath calls on recursive (#6287) 2021-07-06 06:43:56 +00:00
Saugat Pachhai 4b6c6a0e79 add: fix progress bar on single target (#6288) 2021-07-06 12:28:16 +05:45
Saugat Pachhai 6bccb21af6 prevent-tqdm-interleaving (#6280) 2021-07-06 13:37:37 +09:00
Saugat Pachhai beb6277d13 cleaning up add, progress bars and status (#6224)
* cleanup code

* fix progress bar updating message even when index > total

* cleanup
2021-07-06 01:08:35 +03:00
Kurian Benoyandpre-commit-ci[bot] 2485779d59 run: update -h message (#6197)
* update dvc exp run -h message

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-07-03 23:02:19 +03:00
Ruslan Kuprieiev 247b9c8be8 dvc: bump to 2.5.0 2021-07-03 22:34:11 +03:00
Saugat Pachhai 8d5f9ab6e5 add: do not delete stage files before add (#6239)
* add: do not delete stage files before add

Because of the way we collect stages and cache them, we were not able to
collect them for the `add` without removing them from the workspace.
As doing so, we'd have two same/similar stages - one collected from the
workspace and the other just created from the `dvc add` in-memory.

This would raise errors during graph checks, so we started to delete them
and reset them (which is very recently, see #2886 and #3349).

By deleting the file before we even do any checks, we are making DVC
fragile, and results in data loss for the users with even simple mistakes.
This should make it more reliable and robust.

And, recently, we have started to keep state of a lot of things, that by
resetting them on each stage, we waste a lot of performance, especially
on gitignores. We cache the dulwich's IgnoreManager, which when resetted
too many times, will waste a lot of our time just collecting them again
next time (see #6227).

It's hard to say how much this improves, as this very much depends on
no. of gitignores in the repo (which can be assumed to be quite in number
for a dvc repo) and the amount of files that we are adding
(eg: `-R` adding a large directory).

On a directory with 10,000 files (in a datadet-registry repo),
creating stages on `dvc add -R` went from 64 files/sec to 1.1k files/sec.

* add tests

* make the test more specific
2021-07-03 22:02:06 +03:00
Ruslan Kuprieiev 07f5f6b633 ssh: don't use path_info for connections (#6259) 2021-07-03 21:54:14 +03:00
Ruslan Kuprieiev aa255a1102 output: optimize obj collection (#6277)
* output: str only once

Calling `__str__` takes a long time and if we are dealing with a large
dataset, it will result in a very substantial time. For example,
locally, for 700K objects, it took 20sec more.

Related to #6276

* output: don't use os.path.join

Takes around 2sec for 700K objects.

* output: use fs.sep

* output: remove unused sep
2021-07-03 21:53:55 +03:00
Peter Rowlands (변기호) cfcb7e1294 pygit: handle annotated tags in resolve_refish (#6274) 2021-07-02 18:00:47 +03:00
Saugat Pachhai b8d9800e3c separate validation and transformation of args from the add func (#6220)
* separate validation and transformation of args from add func

* make tests pass for Python3.6

* cleanup

* use dict for attr-access and patch boundarguments.arguments
2021-07-01 21:15:02 +05:45
dependabot[bot] fe46d50847 build(deps): bump google-cloud-storage from 1.39.0 to 1.40.0 (#6261)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.39.0 to 1.40.0.
- [Release notes](https://github.com/googleapis/python-storage/releases)
- [Changelog](https://github.com/googleapis/python-storage/blob/master/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-storage/compare/v1.39.0...v1.40.0)

---
updated-dependencies:
- dependency-name: google-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-01 21:14:20 +05:45
Batuhan Taskaya e3ff6d54a9 fsspec: loosen the prefix check to cover both None and False (#6246)
* fsspec: loosen the prefix check to cover both None and False

* gs: disable prefix-based search
2021-07-01 14:59:35 +03:00
Saugat Pachhai ce45ec8a28 align metrics to right and params to left in exp table (#6254)
Made a mistake during refactoring on styling before
2021-06-30 16:05:17 +05:45
Miguel Méndez cc5ba985b0 import: error message when imported file does not exist (#6240)
* Fix exception handling

* Replace exceptions with PathMissingError
2021-06-30 15:49:15 +09:00
Ruslan Kuprieiev 87cdbf8ace dvc: bump to 2.4.3 2021-06-29 17:27:21 +03:00
Tomasz Pietruszka eda8cd5846 git: Keep trailing slash when checking if directory is gitignored (#6232)
* Keep trailing slash when checking if directory is gitignored

* Calling is_ignored with correct type (could be Path-like)

* Fix test to comply to annotated types

* Fix separator issues

* Relaxed typing in is_ignored

* Added test

* Test scm

* Fix test on Windows

* Expanded test_ignored_dir_unignored_subdirs

* Even more assertions
2021-06-29 21:49:29 +09:00
pre-commit-ci[bot] 61835f6511 [pre-commit.ci] pre-commit autoupdate (#6243)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.902 → v0.910](https://github.com/pre-commit/mirrors-mypy/compare/v0.902...v0.910)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-06-29 07:24:12 +05:45
Jon Crall e72354e98e Loosen versions for fsspec and pygtrie (#6242) 2021-06-28 23:57:49 +03:00
Ruslan Kuprieiev d1b36e36ca dvc: bump to 2.4.2 2021-06-28 20:19:42 +03:00
Batuhan Taskaya 6ec66ab3de output: don't lower the etag field on the schema when deserializing (#6238) 2021-06-28 20:15:58 +03:00
Peter Rowlands (변기호) 7c13897674 utils: normalize case in path_isin functions (#6236)
* utils: normalize case in path_isin

* utils: normalize case in contains_symlink_up_to

* split platform test
2021-06-28 19:21:44 +09:00
dependabot[bot] 1db4637f2d build(deps): bump crc32c from 2.2 to 2.2.post0 (#6234)
Bumps [crc32c](https://github.com/ICRAR/crc32c) from 2.2 to 2.2.post0.
- [Release notes](https://github.com/ICRAR/crc32c/releases)
- [Changelog](https://github.com/ICRAR/crc32c/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ICRAR/crc32c/compare/v2.2...v2.2.post0)

---
updated-dependencies:
- dependency-name: crc32c
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-28 12:17:14 +03:00
dependabot[bot] ca3757b45b build(deps): bump urllib3 from 1.26.5 to 1.26.6 (#6235)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.5 to 1.26.6.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/1.26.6/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.5...1.26.6)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-28 08:43:13 +05:45
Ruslan Kuprieiev 4e792ae61c setup: update fsspec to 2021.6.1 (#6219)
* setup: update fsspec to 2021.6.1

* fs: local: remove temp workaround
2021-06-24 10:06:42 +05:45
dependabot[bot] 6b13f76fdb build(deps): bump mypy from 0.902 to 0.910 (#6212)
Bumps [mypy](https://github.com/python/mypy) from 0.902 to 0.910.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.902...v0.910)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-24 10:17:52 +09:00
Ruslan Kuprieiev 53e9460c65 staging: remove redundant exists() call (#6217) 2021-06-24 00:19:06 +03:00
dependabot[bot] dafe6e991c build(deps): bump google-cloud-storage from 1.38.0 to 1.39.0 (#6211)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.38.0 to 1.39.0.
- [Release notes](https://github.com/googleapis/python-storage/releases)
- [Changelog](https://github.com/googleapis/python-storage/blob/master/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-storage/compare/v1.38.0...v1.39.0)

---
updated-dependencies:
- dependency-name: google-cloud-storage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-23 19:07:43 +00:00
Ruslan Kuprieiev 09946847d2 dvc: bump to 2.4.1 2021-06-23 16:08:10 +03:00
Saugat PachhaiandRuslan Kuprieiev 6e5e62857b Bump fsspec version (#6214)
* Bump fsspec version

* local: use fsspec 2021.06.0 with a temp hack

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-06-23 16:07:32 +03:00
Saugat Pachhai b7c6df0ece Create CODEOWNERS (#6213) 2021-06-23 11:56:37 +05:45
Ruslan Kuprieiev 6779d3d431 dvc: bump to 2.4.0 2021-06-22 20:14:46 +03:00
pre-commit-ci[bot]andRuslan Kuprieiev 5844a0cd5b [pre-commit.ci] pre-commit autoupdate (#6167)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/ambv/black: 21.5b2 → 21.6b0](https://github.com/ambv/black/compare/21.5b2...21.6b0)
- [github.com/timothycrosley/isort: 5.8.0 → 5.9.1](https://github.com/timothycrosley/isort/compare/5.8.0...5.9.1)
- [github.com/pre-commit/mirrors-mypy: v0.812 → v0.902](https://github.com/pre-commit/mirrors-mypy/compare/v0.812...v0.902)

* test_requirements: add types packages

* pre-commit: add additional dependencies for mypy

* pre-commit: fix formatting

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-06-22 13:59:11 +03:00
Saugat Pachhai ce3a3ab100 pass size to upload_fobj (#6187)
This allows us to select between chunked and non-chunked uploading
in Webdav, and hopefully in the http itself.
2021-06-22 13:16:15 +03:00
yehiyam d1a219df0a repo: preserve meta field on move (#6201)
fixes #6200
2021-06-22 13:12:20 +03:00
Saugat Pachhai 476af100de ui: migrating rest to the ui.write (#6176)
* complete migration to ui.write

* fix small bugs
2021-06-21 22:14:59 +03:00
Saugat Pachhai f2998630a3 env: track DVC_IGNORE_ISATTY as a env that we support (#6202) 2021-06-21 05:35:39 +00:00
Laurens Duijvesteijn 9a7b6d2a1a serialize: Wrap UnicodeDecodeErrors (#6193)
Also expose `encoding` and `path` as attributes, so programmatic
consumers can access it easily.
2021-06-19 11:35:50 +05:45
Peter Rowlands (변기호) 1d3524f144 odb: treat external repo dependencies as objects (#6109)
* objects: add ExternalRepoFile object type

* RepoDependency: implement get_obj (use ExternalRepoFile)

* drop distinction between used external and used objects

* update tests

* remove unneeded fetch from ExternalRepoFile

* wrap erepo errors as an object error

* update cloud/remote functions for new used_objs behavior

* fix update() rev/locked behavior

* update import tests

* remove ExternalRepoFile

* move get_obj and get_used_objs implementations back into repo dependency

* return objects plus associated ODB (remote) in get_used_objs

* odb: add tmp_dir field and support creating Remote from an odb

* return dict mapping odb -> objects in get_used_objs

* use dummy git odb for git imports

* fetch: support pulling from specific odb

* update cloud funcs for get_used_objs usage

* update tests for used_objs behavior

* use fetch in dep.download

* catch circular imports from local fs repos

* add tests for chained and circular imports

* move tmp_dir into odb config

* update return docstring for repo.used_objs
2021-06-17 14:41:33 +03:00
Batuhan Taskaya 1a5fa228ab azure: add allow_anonymous_login option to the schema (#6190) 2021-06-17 14:24:07 +03:00
Saugat Pachhai adeb9ef63e fix webdav tests and then add some (#6189) 2021-06-17 13:10:53 +03:00
dependabot[bot] 3e93bc286f build(deps): bump pytest-xdist from 2.2.1 to 2.3.0 (#6188)
Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/pytest-dev/pytest-xdist/releases)
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v2.2.1...v2.3.0)

---
updated-dependencies:
- dependency-name: pytest-xdist
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-17 09:37:36 +05:45
Batuhan Taskaya ea89f4a102 add, import-url: preserve size for to-remote/to-cache (#6162) 2021-06-16 18:42:51 +03:00
dependabot[bot] 78299ad345 build(deps): bump pytest-docker from 0.10.1 to 0.10.3 (#6172)
Bumps [pytest-docker](https://github.com/avast/pytest-docker) from 0.10.1 to 0.10.3.
- [Release notes](https://github.com/avast/pytest-docker/releases)
- [Changelog](https://github.com/avast/pytest-docker/blob/master/CHANGELOG.md)
- [Commits](https://github.com/avast/pytest-docker/compare/v0.10.1...v0.10.3)

---
updated-dependencies:
- dependency-name: pytest-docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-15 14:00:38 +05:45
Batuhan Taskaya b9df4451b1 s3, gs, azure: re-enable prefix-based search optimization (#6151) 2021-06-13 16:55:36 +03:00
Saugat Pachhai df67dc2db8 webdav: bring back support for user in url (#6155)
Also added some optimizations, and merged Webdavs into the same file.
2021-06-11 19:52:42 +05:45
dependabot[bot] 37fb73606c build(deps): bump mypy from 0.901 to 0.902 (#6153)
Bumps [mypy](https://github.com/python/mypy) from 0.901 to 0.902.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.901...v0.902)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-11 15:17:38 +05:45
Batuhan Taskaya 0deddf81cf setup: bump fsspec/s3fs/gcsfs to 2021.06.0 (#6137)
* setup: bump fsspec/s3fs/gcsfs to 2021.06.0

* bump fsspec to upstream
2021-06-10 15:17:43 +03:00
Ruslan Kuprieiev 429d54e146 stage: monitor: don't forget to unprotect outs (#6134)
Fixes #6083
2021-06-10 10:53:51 +02:00
dependabot[bot] 32859763be build(deps): bump mypy from 0.812 to 0.901 (#6140)
Bumps [mypy](https://github.com/python/mypy) from 0.812 to 0.901.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.812...v0.901)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-10 10:56:39 +05:45
Saugat Pachhai 5012494213 http/webdav: support specifying certs in ssl (#6142) 2021-06-10 02:24:56 +03:00
dependabot[bot] 57c728269d build(deps): bump codecov/codecov-action from 1.5.0 to 1.5.2 (#6139)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1.5.0 to 1.5.2.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v1.5.0...v1.5.2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-09 23:30:31 +05:45
pre-commit-ci[bot] 3ca18cb400 [pre-commit.ci] pre-commit autoupdate (#6135)
updates:
- [github.com/ambv/black: 21.5b1 → 21.5b2](https://github.com/ambv/black/compare/21.5b1...21.5b2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-06-08 12:29:17 +05:45
Dmitry PetrovandIvan Shcheklein 1335c7f5e4 Add Studio to the related technologies list (#6131)
* Add Studio to the related technologies list

* Add CML to DVC Studio description

* fix k8s names a bit

Co-authored-by: Ivan Shcheklein <[email protected]>
2021-06-07 23:48:35 +03:00
Ruslan Kuprieiev 8b9b2f6907 ls: specify rev as kwarg (#6121)
Fixes #5865
2021-06-07 18:36:35 +03:00
tomrutterandTom Rutter 2f8cf8d7d3 add: prevent crash while getting relpath (#5837)
* add: prevent crash while getting relpath

Since python 3.8 os.path.realpath has resolved windows network shares
to UNC paths. This breaks the logic for relpath as the UNC and mapped
drive paths don't have an obvious common prefix. As it is not easy to go
back to the mapped drive path from UNC path, this is resolved by always
using the UNC paths for calculating relative paths in windows.

Fixes #5832

* relpath: Update changes following review

Two changes:
 - use pytest.mark.skipif rather than if statement inside test to make
   it clearer that test is windows specific
 - Use tail rather than upper_path to denote rest of path after drive
    name taken off (to match python documentation)

Pull request: #5837

* tests, linter: update to add test case and apply linter though pre-commit

Co-authored-by: Tom Rutter <[email protected]>
2021-06-07 11:33:11 +02:00
dependabot[bot] bf5c28b71a build(deps): bump rtCamp/action-slack-notify from 2.1.3 to 2.2.0 (#6128)
Bumps [rtCamp/action-slack-notify](https://github.com/rtCamp/action-slack-notify) from 2.1.3 to 2.2.0.
- [Release notes](https://github.com/rtCamp/action-slack-notify/releases)
- [Commits](https://github.com/rtCamp/action-slack-notify/compare/v2.1.3...v2.2.0)

---
updated-dependencies:
- dependency-name: rtCamp/action-slack-notify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-07 14:04:32 +05:45
Saugat Pachhai ed2a0b4282 webdav4: use webdav4 (#6099) 2021-06-04 16:42:26 +03:00
eb46c03c6c output/remote: get rid of NamedCache (#6008)
* objects: make HashInfo & HashFile hashable

* stage/output: collect objects (HashFiles) instead of NamedCache

* remote/data_cloud: use objs instead of NamedCache

* repo: use objects instead of NamedCache

* rerun black after rebase

* remove NamedCache usage from run-cache

* drop NamedCache

* update HashInfo.__hash__

* update unit tests

* revert remote_name change

* start cleaning up test failures

* fix tree obj expansion

* update missing cache logging

* fix external dict handling

* update naive odb.gc

* update state save behavior for pull

* update test_data_cloud

* update test_remote

* fix gc with multiple repos

* objects.tree: support filtering w/preserved hash_info

* fix stage cache restore

* drop fs.path_info from HashFile.__hash__

* objects: add optional name attribute

* revert missing cache logging name/suffix changes

* revert name changes in remote tests

* review fixes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update dvc/remote/base.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Co-authored-by: Ruslan Kuprieiev <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-06-02 22:19:58 +03:00
Ruslan Kuprieiev 5fb181fcf8 odb: don't use fs.path_info (#6059)
* odb: don't use fs.path_info

* fs: get rid of fs.path_info
2021-06-02 20:43:51 +03:00
Batuhan Taskaya 0e3db0d323 ci: move to ubuntu-18 from ubuntu-16 (#6101) 2021-06-02 17:31:28 +03:00
Ruslan Kuprieiev 5e4de841d1 dvc: bump to 2.3.0 2021-06-02 15:08:23 +03:00
Batuhan Taskaya 9e822edafc fs: temporarily disable prefix-based search for fsspec-implementations (#6096)
* fs: temporarily disable prefix-based search for fsspec-implementations

* tests: add the other hash
2021-06-02 15:06:48 +03:00
dependabot[bot] 9d8b0b80a0 build(deps): bump pytest-cov from 2.12.0 to 2.12.1 (#6095)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.12.0 to 2.12.1.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.12.0...v2.12.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-02 14:42:32 +03:00
rgvanwesepandRobert Van Wesep 59cfc2c4a4 config, remote: Made S3 CA bundle customizable (#6018)
* config, remote: Made S3 CA bundle customizable

botocore allows a path to a custom CA bundle either by passing a path
to the CA bundle file into the verify argument of
boto3.session.Session.client or passing None (the default) which will
fall back to the AWS config. Previously, the DVC config only accepted a
boolean into the ssl_verify option in the remote S3 config. This changes
the DVC config to accept both string and None in addition to boolean and
defaults to None. I also changed the default for ssl_verfiy to None in
BaseS3FileSystem. Thus, if ssl_verify is not provided, botocore will
fall back to the AWS config.

Testing

Unit tests to cover the changes to the config schema and addition
ssl_verify types that will be passed into S3FileSystem. Also, ran

dvc push -r object-store data/cifar-10-python.tar.gz

in my work environment that has a private S3 endpoint that requires a
custom CA bundle, both with and without ssl_verify specified in the
config. This was successful, showing that communication could be
established. And I ran

dvc remote modify object-store ssl_verify "$HOME/.aws/cabundle.pem"

and confirmed that the custom CA bundle path was added to the config.

Fixes #6012

* Removed default None on ssl_verify

Responding to PR comment, removed the Optional, default None on
ssl_verify since the config keys are optional by default. Rather
than a missing ssl_verify producing a None that eventually gets
filtered, it doesn't appear in the parsed config in the first place.

Co-authored-by: Robert Van Wesep <[email protected]>
2021-06-01 16:17:45 +03:00
Saugat Pachhai c7a3a000e0 try skipping local pre-commit hooks (#6090) 2021-06-01 16:03:18 +05:45
dependabot[bot] 782e2aba87 build(deps): bump pylint from 2.8.2 to 2.8.3 (#6087)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.8.2 to 2.8.3.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.8.2...v2.8.3)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-01 08:49:12 +05:45
Ruslan Kuprieiev 488a6f8076 fs: don't automatically create buckets (#6086)
* fs: azure: don't create bucket

* fs: oss: don't create bucket
2021-06-01 01:29:17 +03:00
Batuhan Taskaya 1fbf0da3df azure: better error messages / allow_anonymous_login option (#5833)
* azure: better error messages / allow_anonymous_login option

* dont pass repo to the initializer
2021-06-01 01:17:03 +03:00
Saugat Pachhai bf5deb08e4 pin networkx to 2.5 (#6085)
I am bit hesitant to add an upper-bound here
as networkx seems to be requiring a lot of
dependencies from next version onwards.
2021-05-31 21:39:48 +03:00
Ruslan Kuprieiev 04349a50eb dvc: bump to 2.2.0 2021-05-31 13:43:57 +03:00
Saugat Pachhai a172067c7d fix errors raised by pylint (#6081)
astroid==2.5.7 has improved inference and was throwing
errors that were false negatives before. Fixing these
and ignoring some.

Also added `funcy.decorator` to the `signature-mutators`,
as it changes function signature (i.e. provides `Call` as
a first argument which is not required to pass).

Example errors that we were getting because of this:

```
tests/remotes/gdrive.py:40:11: E1120: No value for argument 'tries' in function call (no-value-for-parameter)
************* Module dvc.stage.serialize
dvc/stage/serialize.py:38:1: E1120: No value for argument 'func' in
function call (no-value-for-parameter)
************* Module dvc.fs.http
dvc/fs/http.py:19:1: E1120: No value for argument 'ctx' in function call (no-value-for-parameter)
************* Module dvc.fs.gdrive
dvc/fs/gdrive.py:66:11: E1120: No value for argument 'tries' in function call (no-value-for-parameter)
dvc/fs/gdrive.py:459:5: E1120: No value for argument 'ctx' in function call (no-value-for-parameter)
```
2021-05-31 05:46:18 +00:00
Saugat Pachhai 7700d55529 use kwargs when passing command line args to run (#6075) 2021-05-29 21:04:55 +05:45
Dmitry Petrov a55c47e89f AirFlow integration link (#6077)
* Update README.rst
* typo fix
2021-05-28 19:43:36 -07:00
dependabot[bot] efda015de4 build(deps): bump actions/cache from 2.1.5 to 2.1.6 (#6073)
Bumps [actions/cache](https://github.com/actions/cache) from 2.1.5 to 2.1.6.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.5...v2.1.6)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-28 14:43:23 +05:45
Saugat Pachhai a74855b9f7 fix empty string parsing for the command (#6074)
Fixes #6072
2021-05-28 06:51:27 +00:00
Saugat Pachhai 63dba45c14 ui: write gets a styled param for rich console printing (#6037) 2021-05-28 09:30:56 +05:45
dependabot[bot] e96f0cfed5 build(deps): bump urllib3 from 1.26.4 to 1.26.5 (#6069)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 13:07:14 +05:45
Saugat Pachhai d818e70bf3 output/dep: specify schema as a dictionary (#6061) 2021-05-26 14:58:51 +03:00
Saugat Pachhai e28726c057 fix mypy errors (#6062) 2021-05-26 06:01:04 +00:00
Ruslan Kuprieiev 8a7ccab82f fs: deps/outs: get rid of types (#6054)
Pre-requisite for removing fs.path_info and switching to regular strings
as paths in filesystems.

With new filesystems we no longer need the zoo of different dependency
and output types, as most of them are now just handled by the underlying
filesystem. Though deps/outs are still due for a refactor in other
aspects.
2021-05-25 19:25:09 +03:00
Batuhan Taskaya 50ec64b841 setup: bump fsspec/s3fs/gcsfs to 2021.5.0 (#6056) 2021-05-25 14:54:56 +03:00
Ruslan Kuprieiev aa672c5108 setup: bump dulwich to 0.20.23 (#6053)
Fixes #6052
2021-05-25 00:30:22 +03:00
Peter Rowlands (변기호) 9dda6bd32c git: update dulwich to 0.20.22 (#6049)
* git: update dulwich to 0.20.22

* git: disable reading system config in CI
2021-05-24 17:40:14 +09:00
Saugat Pachhai bbe06df18e ui: migrate dag (#6044) 2021-05-21 07:35:49 +00:00
Batuhan Taskaya 960ece926c fsspec: split the cloud-specific functionality from the base wrapper (#6014)
* fsspec: split the cloud-specific functionality from the base fs

* rename Cloud to Object
2021-05-19 18:12:07 +03:00
Ruslan Kuprieiev c6b4c94fde dvcfs: raise FileNotFoundError on missing version info (#6017) 2021-05-18 19:12:35 +03:00
Saugat Pachhai b9000ed926 pre-commit: update black to 21.5b1 (#6032)
* pre-commit: update black to 21.5b1

also updated others, but black is more noticeable

* remove magic trailing comma
2021-05-18 15:51:15 +00:00
Saugat Pachhai 58c465ef13 ui: migrate completion to ui.write (#6029) 2021-05-18 12:02:18 +00:00
Saugat Pachhai 789faa4f11 ui: migrate checkout to ui.write (#6028) 2021-05-18 13:55:11 +02:00
dependabot[bot] 9a5b7251b3 build(deps): bump pydocstyle from 6.0.0 to 6.1.1 (#6027)
Bumps [pydocstyle](https://github.com/PyCQA/pydocstyle) from 6.0.0 to 6.1.1.
- [Release notes](https://github.com/PyCQA/pydocstyle/releases)
- [Changelog](https://github.com/PyCQA/pydocstyle/blob/master/docs/release_notes.rst)
- [Commits](https://github.com/PyCQA/pydocstyle/compare/6.0.0...6.1.1)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-18 14:42:05 +05:45
Ivan Shcheklein aab5dec7f5 rwlock: add missing space to the message (#6026) 2021-05-18 12:32:35 +09:00
Saugat Pachhai e6a2b8a2ad ui: migrate cache:dir and check-ignore to ui.write (#6021) 2021-05-17 18:50:58 +03:00
dependabot[bot] 9c4a8c384a build(deps): bump pytest-cov from 2.11.1 to 2.12.0 (#6022)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.11.1 to 2.12.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.11.1...v2.12.0)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-17 15:26:41 +05:45
Peter Rowlands (변기호) 71ea12877d stage: allow duplicate values in _read_env (#6007) 2021-05-14 15:49:41 +02:00
Saugat Pachhai c6f49bd23d ls: use ui.write (#6004) 2021-05-14 10:19:55 +05:45
Ruslan Kuprieiev a5b080645c dvcignore: refactor from_file(s) (#6002) 2021-05-13 12:23:58 +02:00
Ruslan Kuprieiev 6d941e6cb5 fs: get rid of fs_root (#6000)
* dvcignore: don't use fs_root

* fs: get rid of fs_root
2021-05-13 04:15:56 +03:00
Ruslan Kuprieiev da0f0406fb fs: fix typo 2021-05-12 22:28:58 +03:00
Ruslan Kuprieiev 89b40afee7 fs: make constructor closer to fsspec (#5911)
* fs: don't use repo for jobs/hash_jobs

* fs: local: exists: use fsspec's LocalFileSystem

* fs: make constructor comply with fsspec
2021-05-12 22:03:09 +03:00
dependabot[bot] 0ca08d2fea build(deps): bump rtCamp/action-slack-notify from 2 to 2.1.3 (#5993)
Bumps [rtCamp/action-slack-notify](https://github.com/rtCamp/action-slack-notify) from 2 to 2.1.3.
- [Release notes](https://github.com/rtCamp/action-slack-notify/releases)
- [Commits](https://github.com/rtCamp/action-slack-notify/compare/v2...v2.1.3)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-12 13:06:50 +05:45
dependabot[bot] 14e1511eee build(deps): bump actions/setup-ruby from 1 to 1.1.3 (#5994)
Bumps [actions/setup-ruby](https://github.com/actions/setup-ruby) from 1 to 1.1.3.
- [Release notes](https://github.com/actions/setup-ruby/releases)
- [Commits](https://github.com/actions/setup-ruby/compare/v1...v1.1.3)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-12 13:06:40 +05:45
b349b39d62 Decoupling dvcignore and fs (#5812)
* Decoupling dvcignore and fs

[x] remove dvcignore from fs objects
[x] attach dvcignore to repo
[x] add ignore check to functions(add, checkout, output, pipeline, etc)
[x] pass all tests
[x] final check

* Solve all of the arugments error.

* add dvcignore to dvc list

* Solve some test_ignore cases

* add dvcignore to checkout.

1. add dvcignore to 'checkout', 'add'
2. add ignore_filter to fs

* Solve two tests

* Get rid of ignore_filter

* Add ignore examine to download

* Solve a test issue

* Solve dvcignore and subrepo problems

* Solve sub repo error

* Revert some changes

* pass all tests

* Remove debug codes

* Some final exams

* dvcignore trie tree only update when needed

* reuse of the dir result in path walk.

* Some change after code review.

* Recover something from false `--force` push

* add abspath trans to dvcginore

* pass pylint

* pylint related

* Update dvc/fs/repo.py

Co-authored-by: Ruslan Kuprieiev <[email protected]>

* Changes related to reviews.

* dvcignore call api change

from dvcignore.walk_files(fs.walk(path_info)) to dvcignore.walk_files(fs, path_info)

* Get rid of PathInfo in test_ignore.py

* fs.walk follow the arguments of os.walk

* Dvcignore as parameters

* pass dvcignore as arguments to checkouts

* Decoupling state and repo

* Update dvc/dvcfile.py

Co-authored-by: Paweł Redzyński <[email protected]>

* Update dvc/objects/stage.py

Co-authored-by: Paweł Redzyński <[email protected]>

* Solve some small problems

* Add tests to verify that dependency now not ignore

fix#5256

* Update dvc/ignore.py

* Update dvc/fs/git.py

* Update dvc/repo/__init__.py

* Some small improvements

* pass black

* Refactor is_ignored

* Update dvc/output/base.py

* repofs: use dvcignore in exists/isdir/isfile

Per https://github.com/iterative/dvc/pull/5812/files#r630604524

Co-authored-by: myname <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
Co-authored-by: Paweł Redzyński <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-05-12 05:29:04 +03:00
Saugat Pachhai 25eb9ad8c1 RFC: respect $PAGER env vars, make -FRSX default in less (#5906) 2021-05-11 16:35:18 +00:00
Saugat Pachhai eab8146b76 ui: use sys.stdout and sys.stderr all the time (#5991)
We used to allow changing default output and error stream
but that was problematic for the testing. Now it just uses
sys, so capsys should work for the testing.
The proper way to change sys.stdout and sys.stderr is
by using 'redirect_stdout' now.
2021-05-11 11:33:22 +00:00
Batuhan Taskaya e7571ab325 setup: fixate aiobotocore/botocore/boto3 versions (#5987) 2021-05-10 18:28:11 +03:00
Batuhan Taskaya fc4e31bbf8 azure: use selector event loop for windows (#5983) 2021-05-10 16:50:58 +03:00
dependabot[bot] 674693bb86 build(deps): bump pytest-mock from 3.6.0 to 3.6.1 (#5981)
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.6.0 to 3.6.1.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.6.0...v3.6.1)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-07 18:20:01 +05:45
Ruslan Kuprieiev 0ad1408da7 plots: handle NoRemoteError for dirs (#5976) 2021-05-07 14:35:35 +03:00
Saugat Pachhai 19f2fe52e6 tests: skip copying sqlite files in test directories (#5977) 2021-05-06 18:51:07 +05:45
Saugat Pachhai 77be01ee2d try to close dvc.state on exit (#5962)
* try to close dvc.state on exit

I have a feeling that it is causing the issues with
shutil.copytree and some PermissionError during 'init',
as we are not closing it between multiple 'main()' calls.
But I am not quite sure, and this seems like a good thing to
do anyway.

* fixup

* close dvc on exit

* close cache on tests and on init
2021-05-06 14:32:25 +03:00
Saugat Pachhai eabf6ddf27 run gdrive tests in a single job, run all on schedule (#5974) 2021-05-06 17:09:34 +05:45
Batuhan Taskaya 597f730e7d s3: respect credentialpath/configpath (#5973) 2021-05-06 12:27:29 +03:00
ciaochiaociaoandChiao-Wei Hsu 3670bf4fc3 commit,completion: Add stages completion for dvc commit (#5963)
Fixes #5960

Co-authored-by: Chiao-Wei Hsu <[email protected]>
2021-05-05 15:53:20 +03:00
Saugat Pachhai c2f1a1e162 Notify CI failures to Slack (#5970) 2021-05-05 18:37:33 +05:45
Batuhan Taskaya a234f53b76 azure: use a custom event loop for authentication (#5958)
* azure: use a custom event loop for authentication

* add test_temp_event_loop

* wrap open() too!

* ignore aiohttp warnings for normal level use-cases

* rename tests/unit/remote/test_azure to tests/unit/fs/test_azure
2021-05-05 15:41:55 +03:00
dependabot[bot] 0a0d146386 build(deps): bump pytest from 6.2.3 to 6.2.4 (#5968)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.3 to 6.2.4.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.3...6.2.4)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-05 14:31:20 +05:45
Batuhan Taskaya d4a3df73df s3: fsspec migration (#5683)
* s3: migrate to s3fs

* s3: strip etag

* s3: disable instance cache

* tests: use the proper remote:// form in test_checkout

* tests: move/drop some tests

* setup: depend on s3fs

* tests: normalize s3 tests with some cleanups

* Use _download/_upload with old implementation

* fsspec: cache .exists() results

* setup: pin the boto version

* setup: install s3fs[boto3]

* s3: proper exception management

* s3: config improvements

* setup: reference without quotes

* setup: install boto separately

* setup: install moto server

* s3: mocking adjustments for moto_server

* s3: mark some fixtures with scope=session

* s3: s3_fake_creds_file scopes to func

* fsspec: add some docs regarding exists() optimization

* fsspec: exists() fallback

* fsspec: revert exists() optimization

* tests: ssl_verify => verify

* setup: temporarily fetch from master

* s3: add boto as a requirement

* tests: do an extra check regarding race conditions

* tests: use motoserver docker image

* tests: wrap config deletion with try/finally

* tests: disable s3 by default

* setup: upgrade fsspec
2021-05-05 11:36:10 +03:00
Peter Rowlands (변기호) 256cb06921 move: validate new stage before move (#5953)
* move: validate new stage before move

* verify that nothing was moved in tests
2021-05-05 13:09:55 +09:00
Saugat Pachhai ff56379f8a tests: rerun gdrive and gs on failures (#5959)
we just need to mark them with needs_internet marker.
Unfortunately, it's not possible to do that automatically
with the fixtures as fixtures are per-session and we need
something on more higher level (which the marker is not passed
through in the pytest).
2021-05-04 17:59:50 +03:00
Saugat Pachhai 4733757bb1 use realpath for is_in_repo calculation (#5954)
This is because, the repo root is used from a realpath, and the stage.wdir is abspath.
We might need to use same kinds of path, but this patch seems to be the easiest/fastest
way to fix the flaky tests (if all the tests agree).
2021-05-04 17:59:09 +03:00
dependabot[bot] 3e9fd100ea build(deps): bump moto from 2.0.5 to 2.0.6 (#5955)
Bumps [moto](https://github.com/spulec/moto) from 2.0.5 to 2.0.6.
- [Release notes](https://github.com/spulec/moto/releases)
- [Changelog](https://github.com/spulec/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spulec/moto/compare/2.0.5...2.0.6)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-04 17:45:30 +03:00
dependabot[bot] c1a49b440d build(deps): bump pygments from 2.8.1 to 2.9.0 (#5956)
Bumps [pygments](https://github.com/pygments/pygments) from 2.8.1 to 2.9.0.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.8.1...2.9.0)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-04 17:45:16 +03:00
dependabot[bot] c2734a2cd7 build(deps): bump codecov/codecov-action from v1.4.1 to v1.5.0 (#5957)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.4.1 to v1.5.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v1.4.1...a1ed4b322b4b38cb846afb5a0ebfa17086917d27)

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-04 17:45:01 +03:00
Saugat Pachhai 71ab15771a exp show: decoupling tree and styling from row collection (#5903)
* exp show: try to split presentation logic

Trying to remove tree and the bold/row-styling part even more

* fix naming of the experiment types
2021-05-04 18:15:55 +05:45
Peter Rowlands (변기호) 53414a0618 dvcfile/stage: check if dvcfile would be gitignored on stage creation (#5907)
* dvcfile: add check for gitignored when validating filenames

* update tests

* update exception using in tests

* review fixes

- clarify method name
- use FileIsGitIgnored
2021-05-04 15:14:55 +09:00
Saugat Pachhai 708587a159 tests: mark test_stash_pop as xfail on linux (#5952)
I tried increasing max_run to very high number: 1000. Even in those
cases, it failed 947 times. So I don't really see a reason to mark
it as flaky. Anyway, we are treating it as xfail by keeping it failing
in the CI, so this PR does not change anything for us. :)
2021-05-04 11:22:57 +05:45
387 changed files with 10486 additions and 9014 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ coverage:
status:
project:
default:
target: 85
target: 90
threshold: 10%
patch:
default:
+1
View File
@@ -0,0 +1 @@
* @iterative/dvc
+6 -6
View File
@@ -7,7 +7,7 @@ on:
types: [released, prereleased]
jobs:
deb:
runs-on: ubuntu-16.04
runs-on: ubuntu-18.04
steps:
- uses: actions/[email protected]
- name: Set up Python 3.8
@@ -15,7 +15,7 @@ jobs:
with:
python-version: 3.8
- name: Set up Ruby 2.4
uses: actions/setup-ruby@v1
uses: actions/setup-ruby@v1.1.3
with:
ruby-version: 2.4
- name: Install deps
@@ -36,7 +36,7 @@ jobs:
asset_name: dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_content_type: binary/octet-stream
rpm:
runs-on: ubuntu-16.04
runs-on: ubuntu-18.04
steps:
- uses: actions/[email protected]
- name: Set up Python 3.8
@@ -44,7 +44,7 @@ jobs:
with:
python-version: 3.8
- name: Set up Ruby 2.4
uses: actions/setup-ruby@v1
uses: actions/setup-ruby@v1.1.3
with:
ruby-version: 2.4
- name: Install deps
@@ -73,7 +73,7 @@ jobs:
with:
python-version: 3.8
- name: Set up Ruby 2.4
uses: actions/setup-ruby@v1
uses: actions/setup-ruby@v1.1.3
with:
ruby-version: 2.4
- name: Install deps
@@ -125,7 +125,7 @@ jobs:
asset_name: dvc-${{ github.event.release.tag_name }}.exe
asset_content_type: binary/octet-stream
pip:
runs-on: ubuntu-16.04
runs-on: ubuntu-18.04
steps:
- uses: actions/[email protected]
- name: Set up Python 3.7
+42 -8
View File
@@ -7,6 +7,8 @@ on:
push:
branches:
- master
schedule:
- cron: '5 1 * * *' # every day at 01:05
env:
DVC_TEST: "true"
@@ -40,13 +42,14 @@ jobs:
matrix:
os: [ubuntu-18.04, windows-2019, macos-10.15]
pyv: ["3.6", "3.7", "3.8", "3.9"]
# macos 11.0 workers take too long to start up
# include:
# - os: "macos-11.0"
# pyv: "3.9"
include:
- os: "macos-10.15"
pyv: "3.10.0-rc.1"
exclude:
- os: macos-10.15
pyv: "3.9"
- os: macos-10.15
pyv: "3.7"
steps:
- uses: actions/[email protected]
@@ -60,16 +63,20 @@ jobs:
echo "::set-output name=dir::$(pip cache dir)"
- name: set pip cache
id: pip-cache
uses: actions/[email protected].5
uses: actions/[email protected].6
with:
path: ${{ steps.pip-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
- name: install libgit2 on Python3.10 in macOS
if: matrix.pyv == '3.10.0-rc.1' && matrix.os == 'macos-10.15'
run: |
brew install libgit2
- name: install
if: steps.cache.pip-cache-dir.cache-hit != 'true'
run: |
pip install wheel
pip install --upgrade pip setuptools wheel
pip install -e ".[all,tests]"
- name: install temporary dependencies
run: |
@@ -87,14 +94,41 @@ jobs:
run: |
mkdir -p scripts/ci
echo "$GS_CREDS_JSON" > scripts/ci/gcp-creds.json
- name: disable gdrive tests
if: >-
github.event_name != 'schedule' &&
(matrix.pyv != '3.8' || matrix.os != 'ubuntu-18.04')
run: |
echo "extra_test_args=--disable-gdrive" >> $GITHUB_ENV
- name: run tests
env:
GDRIVE_CREDENTIALS_DATA: ${{ secrets.GDRIVE_CREDENTIALS_DATA }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: python -m tests --all --cov-report=xml --cov-report=term -n=4
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID}}
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET}}
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT}}
run: >-
python -m tests -n=4
--cov-report=xml --cov-report=term
--all ${{ env.extra_test_args }}
- name: upload coverage report
uses: codecov/codecov-action@v1.4.1
uses: codecov/codecov-action@v2.0.2
with:
file: ./coverage.xml
fail_ci_if_error: false
notify:
if: github.ref == 'refs/heads/master' && failure()
needs:
- lint
- tests
runs-on: ubuntu-latest
steps:
- name: Slack Notification
uses: rtCamp/[email protected]
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: 'CI Failed on master :boom:'
SLACK_TITLE: CI Status
SLACK_USERNAME: DVC-CI
+20 -5
View File
@@ -1,14 +1,24 @@
ci:
skip: [pylint, dvc-pre-commit]
repos:
- hooks:
- id: black
language_version: python3
repo: https://github.com/ambv/black
rev: 19.10b0
rev: 21.7b0
- hooks:
- id: codespell
args:
- --ignore-words-list
- ba,datas,fo,uptodate
repo: https://github.com/codespell-project/codespell
rev: v2.1.0
- hooks:
- id: isort
language_version: python3
repo: https://github.com/timothycrosley/isort
rev: 5.8.0
rev: 5.9.3
- hooks:
- id: flake8
language_version: python3
@@ -18,13 +28,18 @@ repos:
- flake8-debugger
- flake8-string-format
repo: https://gitlab.com/pycqa/flake8
rev: 3.9.0
rev: 3.9.2
- hooks:
- id: mypy
language_version: python3
files: ^dvc/
additional_dependencies:
- types-requests
- types-paramiko
- types-tabulate
- types-toml
repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.812'
rev: 'v0.910'
- repo: local
hooks:
- id: pylint
@@ -39,7 +54,7 @@ repos:
id: beautysh
language_version: python3
repo: https://github.com/lovesegfault/beautysh
rev: 6.0.1
rev: v6.1.0
- hooks:
- args:
- git-hook
+1
View File
@@ -313,6 +313,7 @@ missing-member-hint-distance=1
# showing a hint for a missing member.
missing-member-max-choices=1
signature-mutators=funcy.decorators.decorator,
[VARIABLES]
+15 -4
View File
@@ -29,9 +29,9 @@ learning projects. Key features:
#. Helps manage experiments with Git tags/branches and **metrics** tracking.
**DVC** aims to replace spreadsheet and document sharing tools (such as Excel or Google Docs)
which are being used frequently as both knowledge repositories and team ledgers.
DVC also replaces both ad-hoc scripts to track, move, and deploy different model versions;
as well as ad-hoc data file suffixes and prefixes.
frequently used as both knowledge repositories and team ledgers.
DVC also replaces both ad-hoc scripts to track, move, and deploy different model versions
and ad-hoc data file suffixes and prefixes.
.. contents:: **Contents**
:backlinks: none
@@ -182,6 +182,12 @@ Fedora / CentOS (rpm)
Comparison to related technologies
==================================
#. *Data Engineering* tools such as `AirFlow <https://airflow.apache.org/>`_, `Luigi <https://github.com/spotify/luigi>`_ and
others - in DVC data, model and ML pipelines represent a single ML project and focuses on data scientist' experience while
data engineering pipeline orchestrates multiple data projects and focuses on efficient execution. DVC project can be used
from data pipeline as a single execution step. `AirFlow DVC <https://github.com/covid-genomics/airflow-dvc>`_ is an example
of such integration.
#. `Git-annex <https://git-annex.branchable.com/>`_ - DVC uses the idea of storing the content of large files (which should
not be in a Git repository) in a local key-value store, and uses file hardlinks/symlinks instead of
copying/duplicating files.
@@ -195,7 +201,12 @@ Comparison to related technologies
#. `Workflow Management Systems <https://en.wikipedia.org/wiki/Workflow_management_system>`_ - DVC is a workflow
management system designed specifically to manage machine learning experiments. DVC is built on top of Git.
#. `DAGsHub <https://dagshub.com/>`_ - This is a Github equivalent for DVC. Pushing Git+DVC based repositories to DAGsHub will produce in a high level project dashboard; including DVC pipelines and metrics visualizations, as well as links to any DVC-managed files present in cloud storage.
#. `DAGsHub <https://dagshub.com/>`_ - online service of Git+DVC repositories with pipeline and metrics visualization and
DVC-specific cloud storage.
#. `DVC Studio <https://studio.iterative.ai/>`_ - online service of DVC repositories visualisation from DVC team. Also,
integrated with `CML (CI/CD for ML) <https://cml.dev/>`_ for training models in clouds and Kubernetes.
Contributing
============
+4 -4
View File
@@ -77,15 +77,15 @@ def add_debugging_flags(parser):
from argparse import SUPPRESS
parser.add_argument(
"--cprofile", action="store_true", default=False, help=SUPPRESS,
"--cprofile", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument("--cprofile-dump", help=SUPPRESS)
parser.add_argument(
"--pdb", action="store_true", default=False, help=SUPPRESS,
"--pdb", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument(
"--instrument", action="store_true", default=False, help=SUPPRESS,
"--instrument", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument(
"--instrument-open", action="store_true", default=False, help=SUPPRESS,
"--instrument-open", action="store_true", default=False, help=SUPPRESS
)
+9 -4
View File
@@ -2,6 +2,8 @@ import json
import logging
import os
from .env import DVC_NO_ANALYTICS
logger = logging.getLogger(__name__)
@@ -32,7 +34,7 @@ def collect_and_send_report(args=None, return_code=None):
with tempfile.NamedTemporaryFile(delete=False, mode="w") as fobj:
json.dump(report, fobj)
daemon(["analytics", fobj.name])
daemon(["analytics", fobj.name])
def is_enabled():
@@ -42,9 +44,12 @@ def is_enabled():
if env2bool("DVC_TEST"):
return False
enabled = to_bool(
Config(validate=False).get("core", {}).get("analytics", "true")
)
enabled = not os.getenv(DVC_NO_ANALYTICS)
if enabled:
enabled = to_bool(
Config(validate=False).get("core", {}).get("analytics", "true")
)
logger.debug("Analytics is {}abled.".format("en" if enabled else "dis"))
return enabled
+107 -56
View File
@@ -2,24 +2,26 @@ import logging
from shortuuid import uuid
import dvc.prompt as prompt
from dvc import prompt
from dvc.exceptions import (
CacheLinkError,
CheckoutError,
ConfirmRemoveError,
DvcException,
)
from dvc.ignore import DvcIgnoreFilter
from dvc.objects import check, load
from dvc.objects.errors import ObjectFormatError
from dvc.objects.stage import stage
from dvc.remote.slow_link_detection import ( # type: ignore[attr-defined]
from dvc.objects.db.slow_link_detection import ( # type: ignore[attr-defined]
slow_link_guard,
)
from dvc.objects.errors import ObjectFormatError
from dvc.objects.stage import stage
from dvc.types import Optional
logger = logging.getLogger(__name__)
def _changed(path_info, fs, obj, cache):
def _changed(path_info, fs, obj, cache, state=None):
logger.trace("checking if '%s'('%s') has changed.", path_info, obj)
try:
@@ -30,11 +32,15 @@ def _changed(path_info, fs, obj, cache):
)
return True
try:
actual = stage(cache, path_info, fs, obj.hash_info.name).hash_info
except FileNotFoundError:
logger.debug("'%s' doesn't exist.", path_info)
return True
actual = state.get(path_info, fs) if state else None
if actual is None:
try:
actual = stage(
cache, path_info, fs, obj.hash_info.name, dry_run=True
)[1].hash_info
except FileNotFoundError:
logger.debug("'%s' doesn't exist.", path_info)
return True
if obj.hash_info != actual:
logger.debug(
@@ -57,7 +63,9 @@ def _remove(path_info, fs, cache, force=False):
fs.remove(path_info)
return
current = stage(cache, path_info, fs, fs.PARAM_CHECKSUM).hash_info
current = stage(cache, path_info, fs, fs.PARAM_CHECKSUM, dry_run=True)[
1
].hash_info
try:
obj = load(cache, current)
check(cache, obj)
@@ -95,7 +103,7 @@ def _do_link(cache, from_info, to_info, link_method):
link_method(from_info, to_info)
logger.debug(
"Created '%s': %s -> %s", cache.cache_types[0], from_info, to_info,
"Created '%s': %s -> %s", cache.cache_types[0], from_info, to_info
)
@@ -118,9 +126,11 @@ def _try_links(cache, from_info, to_info, link_types):
def _link(cache, from_info, to_info):
assert cache.fs.isfile(from_info)
cache.makedirs(to_info.parent)
_try_links(cache, from_info, to_info, cache.cache_types)
try:
_try_links(cache, from_info, to_info, cache.cache_types)
except FileNotFoundError as exc:
raise CheckoutError([to_info]) from exc
def _cache_is_copy(cache, path_info):
@@ -132,7 +142,7 @@ def _cache_is_copy(cache, path_info):
return True
workspace_file = path_info.with_name("." + uuid())
test_cache_file = cache.fs.path_info / ".cache_type_test_file"
test_cache_file = cache.path_info / ".cache_type_test_file"
if not cache.fs.exists(test_cache_file):
cache.makedirs(test_cache_file.parent)
with cache.fs.open(test_cache_file, "wb") as fobj:
@@ -148,13 +158,20 @@ def _cache_is_copy(cache, path_info):
def _checkout_file(
path_info, fs, obj, cache, force, progress_callback=None, relink=False,
path_info,
fs,
obj,
cache,
force,
progress_callback=None,
relink=False,
state=None,
):
"""The file is changed we need to checkout a new copy"""
modified = False
cache_info = cache.hash_to_path_info(obj.hash_info.value)
if fs.exists(path_info):
if not relink and _changed(path_info, fs, obj, cache):
if not relink and _changed(path_info, fs, obj, cache, state=state):
modified = True
_remove(path_info, fs, cache, force=force)
_link(cache, cache_info, path_info)
@@ -168,15 +185,22 @@ def _checkout_file(
_link(cache, cache_info, path_info)
modified = True
fs.repo.state.save(path_info, fs, obj.hash_info)
if state:
state.save(path_info, fs, obj.hash_info)
if progress_callback:
progress_callback(str(path_info))
return modified
def _remove_redundant_files(path_info, fs, obj, cache, force):
existing_files = set(fs.walk_files(path_info))
def _remove_redundant_files(
path_info, fs, obj, cache, force, dvcignore: Optional[DvcIgnoreFilter]
):
if dvcignore:
existing_files = set(dvcignore.walk_files(fs, path_info))
else:
existing_files = set(fs.walk_files(path_info))
needed_files = {path_info.joinpath(*key) for key, _ in obj}
redundant_files = existing_files - needed_files
@@ -187,9 +211,17 @@ def _remove_redundant_files(path_info, fs, obj, cache, force):
def _checkout_dir(
path_info, fs, obj, cache, force, progress_callback=None, relink=False,
path_info,
fs,
obj,
cache,
force,
progress_callback=None,
relink=False,
dvcignore: Optional[DvcIgnoreFilter] = None,
state=None,
):
modified = False, False
modified = False
# Create dir separately so that dir is created
# even if there are no files in it
if not fs.exists(path_info):
@@ -198,24 +230,36 @@ def _checkout_dir(
logger.debug("Linking directory '%s'.", path_info)
failed = []
for entry_key, entry_obj in obj:
entry_modified = _checkout_file(
path_info.joinpath(*entry_key),
fs,
entry_obj,
cache,
force,
progress_callback,
relink,
)
if entry_modified:
modified = True
try:
entry_modified = _checkout_file(
path_info.joinpath(*entry_key),
fs,
entry_obj,
cache,
force,
progress_callback,
relink,
state=None,
)
if entry_modified:
modified = True
except CheckoutError as exc:
failed.extend(exc.target_infos)
modified = (
_remove_redundant_files(path_info, fs, obj, cache, force) or modified
_remove_redundant_files(
path_info, fs, obj, cache, force, dvcignore=dvcignore
)
or modified
)
fs.repo.state.save(path_info, fs, obj.hash_info)
if failed:
raise CheckoutError(failed)
if state:
state.save(path_info, fs, obj.hash_info)
# relink is not modified, assume it as nochange
return modified and not relink
@@ -229,17 +273,28 @@ def _checkout(
force=False,
progress_callback=None,
relink=False,
dvcignore: Optional[DvcIgnoreFilter] = None,
state=None,
):
if not obj.hash_info.isdir:
ret = _checkout_file(
path_info, fs, obj, cache, force, progress_callback, relink
path_info, fs, obj, cache, force, progress_callback, relink, state
)
else:
ret = _checkout_dir(
path_info, fs, obj, cache, force, progress_callback, relink,
path_info,
fs,
obj,
cache,
force,
progress_callback,
relink,
dvcignore=dvcignore,
state=state,
)
fs.repo.state.save_link(path_info, fs)
if state:
state.save_link(path_info, fs)
return ret
@@ -253,6 +308,8 @@ def checkout(
progress_callback=None,
relink=False,
quiet=False,
dvcignore: Optional[DvcIgnoreFilter] = None,
state=None,
):
if path_info.scheme not in ["local", cache.fs.scheme]:
raise NotImplementedError
@@ -268,27 +325,13 @@ def checkout(
_remove(path_info, fs, cache, force=force)
failed = path_info
elif not relink and not _changed(path_info, fs, obj, cache):
logger.trace("Data '%s' didn't change.", path_info)
elif not relink and not _changed(path_info, fs, obj, cache, state=state):
logger.trace("Data '%s' didn't change.", path_info) # type: ignore
skip = True
else:
try:
check(cache, obj)
except (FileNotFoundError, ObjectFormatError):
if not quiet:
logger.warning(
"Cache '%s' not found. File '%s' won't be created.",
obj.hash_info,
path_info,
)
_remove(path_info, fs, cache, force=force)
failed = path_info
if failed or skip:
if progress_callback and obj:
progress_callback(
str(path_info), len(obj),
)
progress_callback(str(path_info), len(obj))
if failed:
raise CheckoutError([failed])
return
@@ -296,5 +339,13 @@ def checkout(
logger.debug("Checking out '%s' with cache '%s'.", path_info, obj)
return _checkout(
path_info, fs, obj, cache, force, progress_callback, relink,
path_info,
fs,
obj,
cache,
force,
progress_callback,
relink,
dvcignore=dvcignore,
state=state,
)
+1 -1
View File
@@ -117,6 +117,6 @@ def add_parser(subparsers, parent_parser):
),
)
parser.add_argument(
"targets", nargs="+", help="Input files/directories to add.",
"targets", nargs="+", help="Input files/directories to add."
).complete = completion.FILE
parser.set_defaults(func=CmdAdd)
+11 -9
View File
@@ -7,11 +7,11 @@ logger = logging.getLogger(__name__)
def fix_subparsers(subparsers):
"""Workaround for bug in Python 3. See more info at:
https://bugs.python.org/issue16308
https://github.com/iterative/dvc/issues/769
https://bugs.python.org/issue16308
https://github.com/iterative/dvc/issues/769
Args:
subparsers: subparsers to fix.
Args:
subparsers: subparsers to fix.
"""
subparsers.required = True
subparsers.dest = "cmd"
@@ -33,17 +33,16 @@ class CmdBase(ABC):
def __init__(self, args):
from dvc.repo import Repo
from dvc.updater import Updater
os.chdir(args.cd)
self.repo = Repo(uninitialized=self.UNINITIALIZED)
self.config = self.repo.config
self.args = args
if self.repo.tmp_dir:
hardlink_lock = self.config["core"].get("hardlink_lock", False)
updater = Updater(self.repo.tmp_dir, hardlink_lock=hardlink_lock)
updater.check()
def do_run(self):
with self.repo:
return self.run()
@abstractmethod
def run(self):
@@ -55,3 +54,6 @@ class CmdBaseNoRepo(CmdBase):
self.args = args
os.chdir(args.cd)
def do_run(self):
return self.run()
+2 -4
View File
@@ -1,11 +1,9 @@
import argparse
import logging
from dvc.command import completion
from dvc.command.base import append_doc_link, fix_subparsers
from dvc.command.config import CmdConfig
logger = logging.getLogger(__name__)
from dvc.ui import ui
class CmdCacheDir(CmdConfig):
@@ -17,7 +15,7 @@ class CmdCacheDir(CmdConfig):
# Use merged config with default values
conf = self.config
self._check(conf, False, "cache", "dir")
logger.info(conf["cache"]["dir"])
ui.write(conf["cache"]["dir"])
return 0
with self.config.edit(level=self.args.level) as conf:
if self.args.unset:
+5 -7
View File
@@ -1,16 +1,14 @@
import argparse
import logging
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
from dvc.ui import ui
class CmdCheckIgnore(CmdBase):
def __init__(self, args):
super().__init__(args)
self.ignore_filter = self.repo.fs.dvcignore
self.ignore_filter = self.repo.dvcignore
def _show_results(self, result):
if not result.match and not self.args.non_matching:
@@ -22,9 +20,9 @@ class CmdCheckIgnore(CmdBase):
patterns = patterns[-1:]
for pattern in patterns:
logger.info("{}\t{}".format(pattern, result.file))
ui.write(pattern, result.file, sep="\t")
else:
logger.info(result.file)
ui.write(result.file)
def _check_one_file(self, target):
result = self.ignore_filter.check_ignore(target)
@@ -122,6 +120,6 @@ def add_parser(subparsers, parent_parser):
help="Read paths from standard input instead of providing `targets`.",
)
parser.add_argument(
"targets", nargs="*", help="File or directory paths to check",
"targets", nargs="*", help="File or directory paths to check"
).complete = completion.FILE
parser.set_defaults(func=CmdCheckIgnore)
+10 -20
View File
@@ -1,38 +1,28 @@
import argparse
import logging
import operator
import colorama
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import CheckoutError
logger = logging.getLogger(__name__)
from dvc.ui import ui
def log_changes(stats):
colors = [
("modified", colorama.Fore.YELLOW,),
("added", colorama.Fore.GREEN),
("deleted", colorama.Fore.RED,),
]
colors = {
"modified": "yellow",
"added": "green",
"deleted": "red",
}
for state, color in colors:
for state, color in colors.items():
entries = stats.get(state)
if not entries:
continue
for entry in entries:
logger.info(
"{color}{state}{nc}{spacing}{entry}".format(
color=color,
state=state[0].upper(),
nc=colorama.Fore.RESET,
spacing="\t",
entry=entry,
)
ui.write(
f"[{color}]{state[0].upper()}", entry, styled=True, sep="\t"
)
@@ -58,7 +48,7 @@ class CmdCheckout(CmdBase):
msg = get_summary(
sorted(stats.items(), key=operator.itemgetter(0))
)
logger.info(msg or default_message)
ui.write(msg or default_message)
else:
log_changes(stats)
+1 -1
View File
@@ -71,5 +71,5 @@ def add_parser(subparsers, parent_parser):
nargs="*",
help="stages or .dvc files to commit. Optional. "
"(Finds all DVC files in the workspace by default.)",
).complete = completion.DVC_FILE
).complete = completion.DVCFILES_AND_STAGE
commit_parser.set_defaults(func=CmdCommit)
+5 -13
View File
@@ -4,6 +4,7 @@ import logging
import shtab
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.ui import ui
logger = logging.getLogger(__name__)
FILE = shtab.FILE
@@ -67,25 +68,16 @@ _dvc_compadd_exps() {
""",
}
DVC_FILE = {
"bash": "_dvc_compgen_DVCFiles",
"zsh": "_dvc_compadd_DVCFiles",
}
DVC_FILE = {"bash": "_dvc_compgen_DVCFiles", "zsh": "_dvc_compadd_DVCFiles"}
STAGE = {
"bash": "_dvc_compgen_stages",
"zsh": "_dvc_compadd_stages",
}
STAGE = {"bash": "_dvc_compgen_stages", "zsh": "_dvc_compadd_stages"}
DVCFILES_AND_STAGE = {
"bash": "_dvc_compgen_stages_and_files",
"zsh": "_dvc_compadd_stages_and_files",
}
EXPERIMENT = {
"bash": "_dvc_compgen_exps",
"zsh": "_dvc_compadd_exps",
}
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
class CmdCompletion(CmdBaseNoRepo):
@@ -95,7 +87,7 @@ class CmdCompletion(CmdBaseNoRepo):
parser = get_main_parser()
shell = self.args.shell
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
print(script)
ui.write(script, force=True)
return 0
+5 -2
View File
@@ -3,6 +3,7 @@ import logging
import os
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.ui import ui
logger = logging.getLogger(__name__)
@@ -71,7 +72,9 @@ class CmdConfig(CmdBaseNoRepo):
prefix = self._config_file_prefix(
self.args.show_origin, self.config, level
)
logger.info("\n".join(self._format_config(conf, prefix)))
configs = list(self._format_config(conf, prefix))
if configs:
ui.write("\n".join(configs))
return 0
@@ -96,7 +99,7 @@ class CmdConfig(CmdBaseNoRepo):
prefix = self._config_file_prefix(
self.args.show_origin, self.config, level
)
logger.info("{}{}".format(prefix, conf[section][opt]))
ui.write(prefix, conf[section][opt], sep="")
break
return 0
+1 -1
View File
@@ -67,6 +67,6 @@ def add_parser(subparsers, parent_parser):
help=DAEMON_ANALYTICS_HELP,
)
daemon_analytics_parser.add_argument(
"target", help="Analytics file.",
"target", help="Analytics file."
).complete = completion.FILE
daemon_analytics_parser.set_defaults(func=CmdDaemonAnalytics)
+8 -8
View File
@@ -1,9 +1,7 @@
import argparse
import logging
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
from dvc.ui import ui
def _show_ascii(G):
@@ -38,18 +36,20 @@ def _collect_targets(repo, target, outs):
return [stage.addressing for stage, _ in pairs]
targets = []
outs_trie = repo.index.outs_trie
for stage, info in pairs:
if not info:
targets.extend([str(out) for out in stage.outs])
continue
for out in repo.outs_trie.itervalues(prefix=info.parts): # noqa: B301
for out in outs_trie.itervalues(prefix=info.parts): # noqa: B301
targets.extend(str(out))
return targets
def _transform(repo, outs):
def _transform(index, outs):
import networkx as nx
from dvc.stage import Stage
@@ -57,7 +57,7 @@ def _transform(repo, outs):
def _relabel(node) -> str:
return node.addressing if isinstance(node, Stage) else str(node)
G = repo.outs_graph if outs else repo.graph
G = index.outs_graph if outs else index.graph
return nx.relabel_nodes(G, _relabel, copy=True)
@@ -87,7 +87,7 @@ def _filter(G, targets, full):
def _build(repo, target=None, full=False, outs=False):
targets = _collect_targets(repo, target, outs)
G = _transform(repo, outs)
G = _transform(repo.index, outs)
return _filter(G, targets, full)
@@ -101,7 +101,7 @@ class CmdDAG(CmdBase):
)
if self.args.dot:
logger.info(_show_dot(G))
ui.write(_show_dot(G))
else:
from dvc.utils.pager import pager
+5 -7
View File
@@ -9,10 +9,11 @@ logger = logging.getLogger(__name__)
class CmdDataBase(CmdBase):
def log_summary(self, stats):
from dvc.ui import ui
from dvc.utils.humanize import get_summary
default_msg = "Everything is up to date."
logger.info(get_summary(stats.items()) or default_msg)
ui.write(get_summary(stats.items()) or default_msg)
class CmdDataPull(CmdDataBase):
@@ -139,7 +140,7 @@ def add_parser(subparsers, _parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pull_parser.add_argument(
"-r", "--remote", help="Remote storage to pull from", metavar="<name>",
"-r", "--remote", help="Remote storage to pull from", metavar="<name>"
)
pull_parser.add_argument(
"-a",
@@ -208,7 +209,7 @@ def add_parser(subparsers, _parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
push_parser.add_argument(
"-r", "--remote", help="Remote storage to push to", metavar="<name>",
"-r", "--remote", help="Remote storage to push to", metavar="<name>"
)
push_parser.add_argument(
"-a",
@@ -272,10 +273,7 @@ def add_parser(subparsers, _parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
fetch_parser.add_argument(
"-r",
"--remote",
help="Remote storage to fetch from",
metavar="<name>",
"-r", "--remote", help="Remote storage to fetch from", metavar="<name>"
)
fetch_parser.add_argument(
"-a",
+2 -2
View File
@@ -1,7 +1,6 @@
import argparse
import logging
import dvc.prompt as prompt
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
@@ -10,6 +9,7 @@ logger = logging.getLogger(__name__)
class CmdDestroy(CmdBase):
def run(self):
from dvc.exceptions import DvcException
from dvc.ui import ui
try:
statement = (
@@ -19,7 +19,7 @@ class CmdDestroy(CmdBase):
"Are you sure you want to continue?"
)
if not self.args.force and not prompt.confirm(statement):
if not self.args.force and not ui.confirm(statement):
raise DvcException(
"cannot destroy without a confirmation from the user."
" Use `-f` to force."
+20 -34
View File
@@ -3,10 +3,9 @@ import json
import logging
import os
import colorama
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link
from dvc.ui import ui
logger = logging.getLogger(__name__)
@@ -18,13 +17,12 @@ def _digest(checksum):
def _show_md(diff, show_hash=False, hide_missing=False):
from dvc.ui import ui
headers = ["Status", "Hash", "Path"] if show_hash else ["Status", "Path"]
rows = []
statuses = ["added", "deleted", "renamed", "modified"]
if not hide_missing:
statuses.append("not in cache")
for status in statuses:
entries = diff.get(status, [])
if not entries:
@@ -44,7 +42,7 @@ def _show_md(diff, show_hash=False, hide_missing=False):
class CmdDiff(CmdBase):
@staticmethod
def _format(diff, hide_missing=False):
def _show_diff(diff, hide_missing=False):
"""
Given a diff structure, generate a string of paths separated
by new lines and grouped together by their state.
@@ -71,15 +69,14 @@ class CmdDiff(CmdBase):
"""
colors = {
"added": colorama.Fore.GREEN,
"modified": colorama.Fore.YELLOW,
"deleted": colorama.Fore.RED,
"renamed": colorama.Fore.GREEN,
"not in cache": colorama.Fore.YELLOW,
"added": "green",
"modified": "yellow",
"deleted": "red",
"renamed": "green",
"not in cache": "yellow",
}
summary = {}
groups = []
states = ["added", "deleted", "renamed", "modified"]
if not hide_missing:
@@ -91,7 +88,8 @@ class CmdDiff(CmdBase):
if not entries:
continue
content = []
header = state.capitalize()
ui.write(f"[{colors[state]}]{header}[/]:", styled=True)
for entry in entries:
path = entry["path"]
@@ -99,7 +97,7 @@ class CmdDiff(CmdBase):
path = f"{path['old']} -> {path['new']}"
checksum = entry.get("hash")
summary[state] += 1 if not path.endswith(os.sep) else 0
content.append(
ui.write(
"{space}{checksum}{separator}{path}".format(
space=" ",
checksum=_digest(checksum) if checksum else "",
@@ -108,27 +106,17 @@ class CmdDiff(CmdBase):
)
)
groups.append(
"{color}{header}{nc}:\n{content}".format(
color=colors[state],
header=state.capitalize(),
nc=colorama.Fore.RESET,
content="\n".join(content),
)
)
ui.write()
if not sum(summary.values()):
return None
fmt = (
"files summary: {added} added, {deleted} deleted,"
" {renamed} renamed, {modified} modified"
states_summary = ", ".join(
f"{summary[state]} {state}"
for state in states
if summary[state] > 0
)
if not hide_missing:
fmt += ", {not in cache} not in cache"
groups.append(fmt.format_map(summary))
return "\n\n".join(groups)
ui.write("files summary:", states_summary)
def run(self):
from dvc.exceptions import DvcException
@@ -155,13 +143,11 @@ class CmdDiff(CmdBase):
diff[key] = entries
if self.args.show_json:
logger.info(json.dumps(diff))
ui.write(json.dumps(diff))
elif self.args.show_md:
logger.info(_show_md(diff, show_hash, hide_missing))
_show_md(diff, show_hash, hide_missing)
elif diff:
output = self._format(diff, hide_missing)
if output:
logger.info(output)
self._show_diff(diff, hide_missing)
except DvcException:
logger.exception("failed to get diff")
+133 -82
View File
@@ -5,18 +5,22 @@ from datetime import date, datetime
from fnmatch import fnmatch
from typing import TYPE_CHECKING, Dict, Iterable, Optional
from funcy import constantly, iffy, lmap
from funcy import lmap
import dvc.prompt as prompt
from dvc import prompt
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.command.metrics import DEFAULT_PRECISION
from dvc.command.repro import CmdRepro
from dvc.command.repro import add_arguments as add_repro_arguments
from dvc.exceptions import DvcException, InvalidArgumentError
from dvc.ui import ui
from dvc.utils.flatten import flatten
from dvc.utils.serialize import encode_exception
if TYPE_CHECKING:
from rich.text import Text
from dvc.compare import TabularData
@@ -25,6 +29,7 @@ logger = logging.getLogger(__name__)
SHOW_MAX_WIDTH = 1024
FILL_VALUE = "-"
FILL_VALUE_ERRORED = "!"
def _filter_name(names, label, filter_strs):
@@ -87,6 +92,7 @@ def _filter_names(
def _update_names(names, items):
for name, item in items:
item = item.get("data", {})
if isinstance(item, dict):
item = flatten(item)
names[name].update({key: None for key in item})
@@ -97,7 +103,8 @@ def _collect_names(all_experiments, **kwargs):
param_names = defaultdict(dict)
for _, experiments in all_experiments.items():
for exp in experiments.values():
for exp_data in experiments.values():
exp = exp_data.get("data", {})
_update_names(metric_names, exp.get("metrics", {}).items())
_update_names(param_names, exp.get("params", {}).items())
metric_names = _filter_names(
@@ -116,6 +123,16 @@ def _collect_names(all_experiments, **kwargs):
return metric_names, param_names
experiment_types = {
"checkpoint_tip": "│ ╓",
"checkpoint_commit": "│ ╟",
"checkpoint_base": "├─╨",
"branch_commit": "├──",
"branch_base": "└──",
"baseline": "",
}
def _collect_rows(
base_rev,
experiments,
@@ -137,8 +154,15 @@ def _collect_rows(
)
new_checkpoint = True
for i, (rev, exp) in enumerate(experiments.items()):
queued = str(exp.get("queued") or "")
for i, (rev, results) in enumerate(experiments.items()):
exp = results.get("data", {})
if exp.get("running"):
state = "Running"
elif exp.get("queued"):
state = "Queued"
else:
state = FILL_VALUE
executor = exp.get("executor", FILL_VALUE)
is_baseline = rev == "baseline"
if is_baseline:
@@ -147,48 +171,57 @@ def _collect_rows(
name_rev = rev[:7]
exp_name = exp.get("name", "")
if is_baseline:
name = exp_name or name_rev
elif exp_name:
name = f"{rev[:7]} [[bold]{exp['name']}[/]]"
else:
name = name_rev
tip = exp.get("checkpoint_tip")
parent_rev = exp.get("checkpoint_parent", "")
parent_exp = experiments.get(parent_rev, {})
parent_exp = experiments.get(parent_rev, {}).get("data", {})
parent_tip = parent_exp.get("checkpoint_tip")
parent = ""
if is_baseline:
tree = ""
typ = "baseline"
elif tip:
if tip == parent_tip:
tree = "│ ╓" if new_checkpoint else "│ ╟"
typ = (
"checkpoint_tip" if new_checkpoint else "checkpoint_commit"
)
elif parent_rev == base_rev:
tree = "├─╨"
typ = "checkpoint_base"
else:
tree = "│ ╟"
typ = "checkpoint_commit"
parent = parent_rev[:7]
elif i < len(experiments) - 1:
typ = "branch_commit"
else:
tree = "├──" if i < len(experiments) - 1 else "└──"
typ = "branch_base"
if not is_baseline:
new_checkpoint = not (tip and tip == parent_tip)
row = [
name,
queued,
tree,
exp_name,
name_rev,
typ,
_format_time(exp.get("timestamp")),
rev == "baseline",
parent,
state,
executor,
]
fill_value = FILL_VALUE_ERRORED if results.get("error") else FILL_VALUE
_extend_row(
row, metric_names, exp.get("metrics", {}).items(), precision
row,
metric_names,
exp.get("metrics", {}).items(),
precision,
fill_value=fill_value,
)
_extend_row(
row,
param_names,
exp.get("params", {}).items(),
precision,
fill_value=fill_value,
)
_extend_row(row, param_names, exp.get("params", {}).items(), precision)
yield row
@@ -251,19 +284,23 @@ def _format_time(timestamp):
return timestamp.strftime(fmt)
def _extend_row(row, names, items, precision):
def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
from rich.text import Text
from dvc.compare import _format_field, with_value
if not items:
row.extend(FILL_VALUE for keys in names.values() for _ in keys)
row.extend(fill_value for keys in names.values() for _ in keys)
return
for fname, item in items:
for fname, data in items:
item = data.get("data", {})
item = flatten(item) if isinstance(item, dict) else {fname: item}
for name in names[fname]:
value = with_value(item.get(name), FILL_VALUE)
value = with_value(
item.get(name),
FILL_VALUE_ERRORED if data.get("error", None) else fill_value,
)
# wrap field data in rich.Text, otherwise rich may
# interpret unescaped braces from list/dict types as rich
# markup tags
@@ -297,11 +334,12 @@ def experiments_table(
headers = [
"Experiment",
"queued",
"ident_guide",
"rev",
"typ",
"Created",
"is_baseline",
"parent",
"State",
"Executor",
]
td = TabularData(
lconcat(headers, metric_headers, param_headers), fill_value=FILL_VALUE
@@ -321,17 +359,29 @@ def experiments_table(
return td
def prepare_exp_id(kwargs):
def prepare_exp_id(kwargs) -> "Text":
from rich.text import Text
exp_name = kwargs["Experiment"]
ident = kwargs.get("ident_guide")
queued = kwargs.get("queued")
rev = kwargs["rev"]
typ = kwargs.get("typ", "baseline")
if typ == "baseline" or not exp_name:
text = Text(exp_name or rev)
else:
text = Text.assemble(rev, " [", (exp_name, "bold"), "]")
parent = kwargs.get("parent")
return "{}{}{}{}".format(
f"{ident} " if ident else "",
"*" if queued else "",
exp_name,
f" ({parent})" if parent else "",
)
suff = f" ({parent})" if parent else ""
text.append(suff)
tree = experiment_types[typ]
pref = f"{tree} " if tree else ""
return Text(pref) + text
def baseline_styler(typ):
return {"style": "bold"} if typ == "baseline" else {}
def show_experiments(
@@ -366,11 +416,13 @@ def show_experiments(
if no_timestamp:
td.drop("Created")
baseline_styler = iffy(constantly({"style": "bold"}), default={})
row_styles = lmap(baseline_styler, td.column("is_baseline"))
td.drop("is_baseline")
for col in ("State", "Executor"):
if td.is_empty(col):
td.drop(col)
merge_headers = ["Experiment", "queued", "ident_guide", "parent"]
row_styles = lmap(baseline_styler, td.column("typ"))
merge_headers = ["Experiment", "rev", "typ", "parent"]
td.column("Experiment")[:] = map(prepare_exp_id, td.as_dict(merge_headers))
td.drop(*merge_headers[1:])
@@ -378,12 +430,14 @@ def show_experiments(
styles = {
"Experiment": {"no_wrap": True, "header_style": "black on grey93"},
"Created": {"header_style": "black on grey93"},
"State": {"header_style": "black on grey93"},
"Executor": {"header_style": "black on grey93"},
}
header_bg_colors = {"metrics": "cornsilk1", "params": "light_cyan1"}
styles.update(
{
header: {
"justify": "left" if typ == "metrics" else "params",
"justify": "right" if typ == "metrics" else "left",
"header_style": f"black on {header_bg_colors[typ]}",
"collapse": idx != 0,
"no_wrap": typ == "metrics",
@@ -416,7 +470,7 @@ def _normalize_headers(names):
def _format_json(item):
if isinstance(item, (date, datetime)):
return item.isoformat()
raise TypeError
return encode_exception(item)
class CmdExperimentsShow(CmdBase):
@@ -437,7 +491,7 @@ class CmdExperimentsShow(CmdBase):
if self.args.show_json:
import json
logger.info(json.dumps(all_experiments, default=_format_json))
ui.write(json.dumps(all_experiments, default=_format_json))
else:
show_experiments(
all_experiments,
@@ -481,7 +535,7 @@ class CmdExperimentsDiff(CmdBase):
if self.args.show_json:
import json
logger.info(json.dumps(diff))
ui.write(json.dumps(diff))
else:
from dvc.compare import show_diff
@@ -489,8 +543,6 @@ class CmdExperimentsDiff(CmdBase):
diffs = [("metrics", "Metric"), ("params", "Param")]
for idx, (key, title) in enumerate(diffs):
if idx:
from dvc.ui import ui
# we are printing tables even in `--quiet` mode
# so we should also be printing the "table" separator
ui.write(force=True)
@@ -524,7 +576,7 @@ class CmdExperimentsRun(CmdRepro):
)
if self.args.reset:
logger.info("Any existing checkpoints will be reset and re-run.")
ui.write("Any existing checkpoints will be reset and re-run.")
results = self.repo.experiments.run(
name=self.args.name,
@@ -541,7 +593,7 @@ class CmdExperimentsRun(CmdRepro):
if self.args.metrics and results:
metrics = self.repo.metrics.show(revs=list(results))
metrics.pop("workspace", None)
logger.info(show_metrics(metrics))
show_metrics(metrics)
return 0
@@ -577,7 +629,7 @@ class CmdExperimentsGC(CmdRepro):
msg += " of the current repo."
if self.args.queued:
msg += " Run queued experiments will be preserved."
if self.args.queued:
else:
msg += " Run queued experiments will be removed."
logger.warning(msg)
@@ -595,12 +647,13 @@ class CmdExperimentsGC(CmdRepro):
)
if removed:
logger.info(
f"Removed {removed} experiments. To remove unused cache files "
"use 'dvc gc'."
ui.write(
f"Removed {removed} experiments.",
"To remove unused cache files",
"use 'dvc gc'.",
)
else:
logger.info("No experiments to remove.")
ui.write("No experiments to remove.")
return 0
@@ -649,15 +702,15 @@ class CmdExperimentsPush(CmdBase):
run_cache=self.args.run_cache,
)
logger.info(
"Pushed experiment '%s' to Git remote '%s'.",
self.args.experiment,
self.args.git_remote,
ui.write(
f"Pushed experiment '{self.args.experiment}'"
f"to Git remote '{self.args.git_remote}'."
)
if not self.args.push_cache:
logger.info(
"To push cached outputs for this experiment to DVC remote "
"storage, re-run this command without '--no-cache'."
ui.write(
"To push cached outputs",
"for this experiment to DVC remote storage,"
"re-run this command without '--no-cache'.",
)
return 0
@@ -676,15 +729,15 @@ class CmdExperimentsPull(CmdBase):
run_cache=self.args.run_cache,
)
logger.info(
"Pulled experiment '%s' from Git remote '%s'. ",
self.args.experiment,
self.args.git_remote,
ui.write(
f"Pulled experiment '{self.args.experiment}'",
f"from Git remote '{self.args.git_remote}'.",
)
if not self.args.pull_cache:
logger.info(
"To pull cached outputs for this experiment from DVC remote "
"storage, re-run this command without '--no-cache'."
ui.write(
"To pull cached outputs for this experiment"
"from DVC remote storage,"
"re-run this command without '--no-cache'."
)
return 0
@@ -694,7 +747,7 @@ class CmdExperimentsRemove(CmdBase):
def run(self):
self.repo.experiments.remove(
exp_names=self.args.experiment, queue=self.args.queue,
exp_names=self.args.experiment, queue=self.args.queue
)
return 0
@@ -855,7 +908,7 @@ def add_parser(subparsers, parent_parser):
help="Fail if this command would overwrite conflicting changes.",
)
experiments_apply_parser.add_argument(
"experiment", help="Experiment to be applied.",
"experiment", help="Experiment to be applied."
).complete = completion.EXPERIMENT
experiments_apply_parser.set_defaults(func=CmdExperimentsApply)
@@ -924,9 +977,7 @@ def add_parser(subparsers, parent_parser):
)
experiments_diff_parser.set_defaults(func=CmdExperimentsDiff)
EXPERIMENTS_RUN_HELP = (
"Reproduce complete or partial experiment pipelines."
)
EXPERIMENTS_RUN_HELP = "Run or resume an experiment."
experiments_run_parser = experiments_subparsers.add_parser(
"run",
parents=[parent_parser],
@@ -1020,10 +1071,10 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_branch_parser.add_argument(
"experiment", help="Experiment to be promoted.",
"experiment", help="Experiment to be promoted."
)
experiments_branch_parser.add_argument(
"branch", help="Git branch name to use.",
"branch", help="Git branch name to use."
)
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
@@ -1046,7 +1097,7 @@ def add_parser(subparsers, parent_parser):
metavar="<rev>",
)
experiments_list_parser.add_argument(
"--all", action="store_true", help="List all experiments.",
"--all", action="store_true", help="List all experiments."
)
experiments_list_parser.add_argument(
"--names-only",
@@ -1118,7 +1169,7 @@ def add_parser(subparsers, parent_parser):
metavar="<git_remote>",
)
experiments_push_parser.add_argument(
"experiment", help="Experiment to push.", metavar="<experiment>",
"experiment", help="Experiment to push.", metavar="<experiment>"
).complete = completion.EXPERIMENT
experiments_push_parser.set_defaults(func=CmdExperimentsPush)
@@ -1174,7 +1225,7 @@ def add_parser(subparsers, parent_parser):
metavar="<git_remote>",
)
experiments_pull_parser.add_argument(
"experiment", help="Experiment to pull.", metavar="<experiment>",
"experiment", help="Experiment to pull.", metavar="<experiment>"
)
experiments_pull_parser.set_defaults(func=CmdExperimentsPull)
@@ -1187,7 +1238,7 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_remove_parser.add_argument(
"--queue", action="store_true", help="Remove all queued experiments.",
"--queue", action="store_true", help="Remove all queued experiments."
)
experiments_remove_parser.add_argument(
"experiment",
+2 -2
View File
@@ -40,7 +40,7 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
freeze_parser.add_argument(
"targets", nargs="+", help="Stages or .dvc files to freeze",
"targets", nargs="+", help="Stages or .dvc files to freeze"
).complete = completion.DVC_FILE
freeze_parser.set_defaults(func=CmdFreeze)
@@ -53,6 +53,6 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unfreeze_parser.add_argument(
"targets", nargs="+", help="Stages or .dvc files to unfreeze",
"targets", nargs="+", help="Stages or .dvc files to unfreeze"
).complete = completion.DVC_FILE
unfreeze_parser.set_defaults(func=CmdUnfreeze)
+2 -2
View File
@@ -2,8 +2,8 @@ import argparse
import logging
import os
import dvc.prompt as prompt
from dvc.command.base import CmdBase, append_doc_link
from dvc.ui import ui
logger = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ class CmdGC(CmdBase):
logger.warning(msg)
msg = "Are you sure you want to proceed?"
if not self.args.force and not prompt.confirm(msg):
if not self.args.force and not ui.confirm(msg):
return 1
self.repo.gc(
+3 -2
View File
@@ -12,12 +12,13 @@ logger = logging.getLogger(__name__)
class CmdGet(CmdBaseNoRepo):
def _show_url(self):
from dvc.api import get_url
from dvc.ui import ui
try:
url = get_url(
self.args.path, repo=self.args.url, rev=self.args.rev
)
logger.info(url)
ui.write(url, force=True)
except DvcException:
logger.exception("failed to show URL")
return 1
@@ -64,7 +65,7 @@ def add_parser(subparsers, parent_parser):
"url", help="Location of DVC or Git repository to download from"
)
get_parser.add_argument(
"path", help="Path to a file or directory within the repository",
"path", help="Path to a file or directory within the repository"
).complete = completion.FILE
get_parser.add_argument(
"-o",
+1 -1
View File
@@ -34,7 +34,7 @@ def add_parser(subparsers, parent_parser):
"url", help="See `dvc import-url -h` for full list of supported URLs."
)
get_parser.add_argument(
"out", nargs="?", help="Destination path to put data to.",
"out", nargs="?", help="Destination path to put data to."
).complete = completion.DIR
get_parser.add_argument(
"-j",
+4 -6
View File
@@ -105,7 +105,7 @@ def add_parser(subparsers, parent_parser):
help=PRE_COMMIT_HELP,
)
pre_commit_parser.add_argument(
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool."
)
pre_commit_parser.set_defaults(func=CmdPreCommit)
@@ -117,7 +117,7 @@ def add_parser(subparsers, parent_parser):
help=POST_CHECKOUT_HELP,
)
post_checkout_parser.add_argument(
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool."
)
post_checkout_parser.set_defaults(func=CmdPostCheckout)
@@ -129,7 +129,7 @@ def add_parser(subparsers, parent_parser):
help=PRE_PUSH_HELP,
)
pre_push_parser.add_argument(
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool."
)
pre_push_parser.set_defaults(func=CmdPrePush)
@@ -146,9 +146,7 @@ def add_parser(subparsers, parent_parser):
help="Ancestor's version of the conflicting file.",
)
merge_driver_parser.add_argument(
"--our",
required=True,
help="Current version of the conflicting file.",
"--our", required=True, help="Current version of the conflicting file."
)
merge_driver_parser.add_argument(
"--their",
+1 -1
View File
@@ -48,7 +48,7 @@ def add_parser(subparsers, parent_parser):
"url", help="Location of DVC or Git repository to download from"
)
import_parser.add_argument(
"path", help="Path to a file or directory within the repository",
"path", help="Path to a file or directory within the repository"
).complete = completion.FILE
import_parser.add_argument(
"-o",
+1 -1
View File
@@ -58,7 +58,7 @@ def add_parser(subparsers, parent_parser):
"remote://remote_name/path/to/file/or/dir (see `dvc remote`)",
)
import_parser.add_argument(
"out", nargs="?", help="Destination path to put files to.",
"out", nargs="?", help="Destination path to put files to."
).complete = completion.DIR
import_parser.add_argument(
"--file",
+8 -6
View File
@@ -12,8 +12,10 @@ logger = logging.getLogger(__name__)
def _welcome_message():
from dvc.ui import ui
if analytics.is_enabled():
logger.info(
ui.write(
boxify(
"DVC has enabled anonymous aggregate usage analytics.\n"
"Read the analytics documentation (and how to opt-out) here:\n"
@@ -30,7 +32,7 @@ def _welcome_message():
f"- Star us on GitHub: {fmt_link('https://github.com/iterative/dvc')}"
).format(yellow=colorama.Fore.YELLOW, nc=colorama.Fore.RESET)
logger.info(msg)
ui.write(msg)
class CmdInit(CmdBaseNoRepo):
@@ -39,14 +41,14 @@ class CmdInit(CmdBaseNoRepo):
from dvc.repo import Repo
try:
self.repo = Repo.init(
with Repo.init(
".",
no_scm=self.args.no_scm,
force=self.args.force,
subdir=self.args.subdir,
)
self.config = self.repo.config
_welcome_message()
) as repo:
self.config = repo.config
_welcome_message()
except InitError:
logger.exception("failed to initiate DVC")
return 1
+8 -9
View File
@@ -1,11 +1,9 @@
import argparse
import logging
import os
from dvc.command import completion
from dvc.command.base import CmdBase, fix_subparsers
logger = logging.getLogger(__name__)
from dvc.ui import ui
class CmdLive(CmdBase):
@@ -14,12 +12,13 @@ class CmdLive(CmdBase):
def _run(self, target, revs=None):
metrics, plots = self.repo.live.show(target=target, revs=revs)
html_path = self.args.target + ".html"
self.repo.plots.write_html(html_path, plots, metrics)
if plots:
html_path = self.args.target + ".html"
self.repo.plots.write_html(html_path, plots, metrics)
logger.info(f"\nfile://{os.path.abspath(html_path)}")
return 0
ui.write("\nfile://", os.path.abspath(html_path), sep="")
return 0
return 1
class CmdLiveShow(CmdLive):
@@ -35,7 +34,7 @@ class CmdLiveDiff(CmdLive):
def shared_parent_parser():
parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument(
"target", help="Logs dir to produce summary from",
"target", help="Logs dir to produce summary from"
).complete = completion.DIR
parent_parser.add_argument(
"-o",
+4 -4
View File
@@ -1,11 +1,11 @@
import argparse
import logging
import sys
from dvc.command import completion
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.command.ls.ls_colors import LsColors
from dvc.exceptions import DvcException
from dvc.ui import ui
logger = logging.getLogger(__name__)
@@ -37,10 +37,10 @@ class CmdList(CmdBaseNoRepo):
if self.args.show_json:
import json
logger.info(json.dumps(entries))
ui.write(json.dumps(entries))
elif entries:
entries = _prettify(entries, sys.stdout.isatty())
logger.info("\n".join(entries))
entries = _prettify(entries, with_color=True)
ui.write("\n".join(entries))
return 0
except DvcException:
logger.exception(f"failed to list '{self.args.url}'")
+5 -10
View File
@@ -3,7 +3,9 @@ import logging
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.exceptions import BadMetricError, DvcException
from dvc.exceptions import DvcException
from dvc.ui import ui
from dvc.utils.serialize import encode_exception
logger = logging.getLogger(__name__)
@@ -32,17 +34,10 @@ class CmdMetricsShow(CmdMetricsBase):
if self.args.show_json:
import json
logger.info(json.dumps(metrics))
ui.write(json.dumps(metrics, default=encode_exception))
else:
from dvc.compare import show_metrics
# 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)
if missing:
raise BadMetricError(missing)
show_metrics(
metrics,
markdown=self.args.show_md,
@@ -73,7 +68,7 @@ class CmdMetricsDiff(CmdMetricsBase):
if self.args.show_json:
import json
logger.info(json.dumps(diff))
ui.write(json.dumps(diff))
else:
from dvc.compare import show_diff
+2 -2
View File
@@ -37,9 +37,9 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
move_parser.add_argument(
"src", help="Source path to a data file or directory.",
"src", help="Source path to a data file or directory."
).complete = completion.FILE
move_parser.add_argument(
"dst", help="Destination path.",
"dst", help="Destination path."
).complete = completion.FILE
move_parser.set_defaults(func=CmdMove)
+2 -1
View File
@@ -4,6 +4,7 @@ import logging
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
from dvc.ui import ui
logger = logging.getLogger(__name__)
@@ -27,7 +28,7 @@ class CmdParamsDiff(CmdBase):
if self.args.show_json:
import json
logger.info(json.dumps(diff))
ui.write(json.dumps(diff))
else:
from dvc.compare import show_diff
+28 -19
View File
@@ -4,6 +4,7 @@ import logging
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
from dvc.ui import ui
from dvc.utils import format_link
logger = logging.getLogger(__name__)
@@ -38,30 +39,38 @@ class CmdPlots(CmdBase):
if self.args.show_vega:
target = self.args.targets[0]
logger.info(plots[target])
ui.write(plots[target])
return 0
rel: str = self.args.out or "plots.html"
path = (Path.cwd() / rel).resolve()
self.repo.plots.write_html(
path, plots=plots, html_template_path=self.args.html_template
)
except DvcException:
logger.exception("")
return 1
assert path.is_absolute() # as_uri throws ValueError if not absolute
url = path.as_uri()
logger.info(url)
if self.args.open:
import webbrowser
if plots:
rel: str = self.args.out or "plots.html"
path: Path = (Path.cwd() / rel).resolve()
self.repo.plots.write_html(
path, plots=plots, html_template_path=self.args.html_template
)
opened = webbrowser.open(rel)
if not opened:
logger.error("Failed to open. Please try opening it manually.")
return 1
assert (
path.is_absolute()
) # as_uri throws ValueError if not absolute
url = path.as_uri()
ui.write(url)
if self.args.open:
import webbrowser
opened = webbrowser.open(rel)
if not opened:
ui.error_write(
"Failed to open. Please try opening it manually."
)
return 1
else:
ui.error_write(
"No plots were loaded, visualization file will not be created."
)
return 0
@@ -87,7 +96,7 @@ class CmdPlotsDiff(CmdPlots):
class CmdPlotsModify(CmdPlots):
def run(self):
self.repo.plots.modify(
self.args.target, props=self._props(), unset=self.args.unset,
self.args.target, props=self._props(), unset=self.args.unset
)
return 0
@@ -160,7 +169,7 @@ def add_parser(subparsers, parent_parser):
help=argparse.SUPPRESS,
)
plots_diff_parser.add_argument(
"revisions", nargs="*", default=None, help="Git commits to plot from",
"revisions", nargs="*", default=None, help="Git commits to plot from"
)
_add_props_arguments(plots_diff_parser)
_add_output_arguments(plots_diff_parser)
@@ -175,7 +184,7 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
plots_modify_parser.add_argument(
"target", help="Metric file to set properties to",
"target", help="Metric file to set properties to"
).complete = completion.FILE
_add_props_arguments(plots_modify_parser)
plots_modify_parser.add_argument(
+4 -6
View File
@@ -1,12 +1,10 @@
import argparse
import logging
from dvc.command.base import append_doc_link, fix_subparsers
from dvc.command.config import CmdConfig
from dvc.ui import ui
from dvc.utils import format_link
logger = logging.getLogger(__name__)
class CmdRemote(CmdConfig):
def __init__(self, args):
@@ -27,7 +25,7 @@ class CmdRemoteAdd(CmdRemote):
from dvc.config import ConfigError
if self.args.default:
logger.info(f"Setting '{self.args.name}' as a default remote.")
ui.write(f"Setting '{self.args.name}' as a default remote.")
with self.config.edit(self.args.level) as conf:
if self.args.name in conf["remote"] and not self.args.force:
@@ -90,7 +88,7 @@ class CmdRemoteDefault(CmdRemote):
try:
print(conf["core"]["remote"])
except KeyError:
logger.info("No default remote set")
ui.write("No default remote set")
return 1
else:
with self.config.edit(self.args.level) as conf:
@@ -116,7 +114,7 @@ class CmdRemoteList(CmdRemote):
def run(self):
conf = self.config.read(self.args.level)
for name, conf in conf["remote"].items():
logger.info("{}\t{}".format(name, conf["url"]))
ui.write(name, conf["url"], sep="\t")
return 0
+4 -5
View File
@@ -1,20 +1,19 @@
import argparse
import logging
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link
from dvc.command.status import CmdDataStatus
logger = logging.getLogger(__name__)
class CmdRepro(CmdBase):
def run(self):
from dvc.ui import ui
stages = self.repo.reproduce(**self._repro_kwargs)
if len(stages) == 0:
logger.info(CmdDataStatus.UP_TO_DATE_MSG)
ui.write(CmdDataStatus.UP_TO_DATE_MSG)
else:
logger.info(
ui.write(
"Use `dvc push` to send your updates to " "remote storage."
)
+2 -1
View File
@@ -10,8 +10,9 @@ logger = logging.getLogger(__name__)
class CmdRoot(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
from dvc.ui import ui
logger.info(relpath(Repo.find_root()))
ui.write(relpath(Repo.find_root()))
return 0
+12 -33
View File
@@ -33,36 +33,17 @@ class CmdRun(CmdBase):
)
return 1
kwargs = vars(self.args)
kwargs.update(
{
"cmd": parse_cmd(self.args.cmd),
"fname": kwargs.pop("file"),
"no_exec": (self.args.no_exec or bool(self.args.checkpoints)),
"run_cache": not kwargs.pop("no_run_cache"),
}
)
try:
self.repo.run(
cmd=parse_cmd(self.args.cmd),
outs=self.args.outs,
outs_no_cache=self.args.outs_no_cache,
metrics=self.args.metrics,
metrics_no_cache=self.args.metrics_no_cache,
plots=self.args.plots,
plots_no_cache=self.args.plots_no_cache,
live=self.args.live,
live_no_cache=self.args.live_no_cache,
live_no_summary=self.args.live_no_summary,
live_no_html=self.args.live_no_html,
deps=self.args.deps,
params=self.args.params,
fname=self.args.file,
wdir=self.args.wdir,
no_exec=(self.args.no_exec or bool(self.args.checkpoints)),
force=self.args.force,
run_cache=not self.args.no_run_cache,
no_commit=self.args.no_commit,
outs_persist=self.args.outs_persist,
outs_persist_no_cache=self.args.outs_persist_no_cache,
checkpoints=self.args.checkpoints,
always_changed=self.args.always_changed,
name=self.args.name,
single_stage=self.args.single_stage,
external=self.args.external,
desc=self.args.desc,
)
self.repo.run(**kwargs)
except DvcException:
logger.exception("")
return 1
@@ -83,11 +64,9 @@ def add_parser(subparsers, parent_parser):
help=RUN_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
run_parser.add_argument("-n", "--name", help="Stage name.")
run_parser.add_argument(
"-n", "--name", help="Stage name.",
)
run_parser.add_argument(
"--file", metavar="<filename>", help=argparse.SUPPRESS,
"--file", metavar="<filename>", help=argparse.SUPPRESS
)
run_parser.add_argument(
"--no-exec",
+11 -10
View File
@@ -9,7 +9,7 @@ from dvc.utils.cli_parse import parse_params
from dvc.utils.humanize import truncate_text
if TYPE_CHECKING:
from dvc.output.base import BaseOutput
from dvc.output import Output
from dvc.stage import Stage
logger = logging.getLogger(__name__)
@@ -18,7 +18,7 @@ MAX_TEXT_LENGTH = 80
def generate_description(stage: "Stage") -> str:
def part_desc(outs: Iterable["BaseOutput"]) -> str:
def part_desc(outs: Iterable["Output"]) -> str:
return ", ".join(out.def_path for out in outs)
if not stage.deps and not stage.outs:
@@ -27,7 +27,7 @@ def generate_description(stage: "Stage") -> str:
if not stage.outs and stage.deps:
return "Depends on " + part_desc(stage.deps)
def is_plot_or_metric(out: "BaseOutput"):
def is_plot_or_metric(out: "Output"):
return bool(out.plot) or bool(out.metric)
desc: List[str] = []
@@ -66,7 +66,7 @@ def prepare_stages_data(
class CmdStageList(CmdBase):
def _get_stages(self) -> Iterable["Stage"]:
if self.args.all:
stages: List["Stage"] = self.repo.stages # type: ignore
stages: List["Stage"] = self.repo.index.stages # type: ignore
logger.trace( # type: ignore[attr-defined]
"%d no. of stages found", len(stages)
)
@@ -75,9 +75,7 @@ class CmdStageList(CmdBase):
# removing duplicates while maintaining order
collected = chain.from_iterable(
self.repo.stage.collect(
target=target,
recursive=self.args.recursive,
accept_group=True,
target=target, recursive=self.args.recursive, accept_group=True
)
for target in self.args.targets
)
@@ -115,8 +113,11 @@ def parse_cmd(commands: List[str]) -> str:
"""
def quote_argument(arg: str):
should_quote = " " in arg and '"' not in arg
return f'"{arg}"' if should_quote else arg
if not arg:
return '""'
if " " in arg and '"' not in arg:
return f'"{arg}"'
return arg
if len(commands) < 2:
return " ".join(commands)
@@ -201,7 +202,7 @@ def _add_common_args(parser):
metavar="<path>",
)
parser.add_argument(
"--live", help="Declare output as dvclive.", metavar="<path>",
"--live", help="Declare output as dvclive.", metavar="<path>"
)
parser.add_argument(
"--live-no-cache",
+6 -6
View File
@@ -2,6 +2,7 @@ import logging
from dvc.command.data_sync import CmdDataBase
from dvc.exceptions import DvcException
from dvc.ui import ui
from dvc.utils import format_link
logger = logging.getLogger(__name__)
@@ -26,7 +27,7 @@ class CmdDataStatus(CmdDataBase):
ind = indent * self.STATUS_INDENT
if isinstance(status, str):
logger.info(f"{ind}{status}")
ui.write(f"{ind}{status}")
return
if isinstance(status, list):
@@ -38,14 +39,13 @@ class CmdDataStatus(CmdDataBase):
for key, value in status.items():
if isinstance(value, str):
logger.info("{}{}{}".format(ind, self._normalize(value), key))
ui.write(f"{ind}{self._normalize(value)}{key}")
elif value:
logger.info(f"{ind}{key}:")
ui.write(f"{ind}{key}:")
self._show(value, indent + 1)
def run(self):
from dvc.repo import lock_repo
from dvc.ui import ui
indent = 1 if self.args.cloud else 0
@@ -72,7 +72,7 @@ class CmdDataStatus(CmdDataBase):
if self.args.show_json:
import json
logger.info(json.dumps(st))
ui.write(json.dumps(st))
return 0
if st:
@@ -80,7 +80,7 @@ class CmdDataStatus(CmdDataBase):
return 0
# additional hints for the user
if not self.repo.stages:
if not self.repo.index.stages:
ui.write(self.EMPTY_PROJECT_MSG)
elif self.args.cloud or self.args.remote:
remote = self.args.remote or self.repo.config["core"].get(
+1 -1
View File
@@ -33,6 +33,6 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unprotect_parser.add_argument(
"targets", nargs="+", help="Data files/directories to unprotect.",
"targets", nargs="+", help="Data files/directories to unprotect."
).complete = completion.FILE
unprotect_parser.set_defaults(func=CmdUnprotect)
+1 -1
View File
@@ -36,7 +36,7 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
update_parser.add_argument(
"targets", nargs="+", help=".dvc files to update.",
"targets", nargs="+", help=".dvc files to update."
).complete = completion.DVC_FILE
update_parser.add_argument(
"--rev",
+5 -1
View File
@@ -2,6 +2,7 @@ import argparse
import logging
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.ui import ui
logger = logging.getLogger(__name__)
@@ -9,9 +10,12 @@ logger = logging.getLogger(__name__)
class CmdVersion(CmdBaseNoRepo):
def run(self):
from dvc.info import get_dvc_info
from dvc.updater import notify_updates
dvc_info = get_dvc_info()
logger.info(dvc_info)
ui.write(dvc_info, force=True)
notify_updates()
return 0
+14 -9
View File
@@ -1,4 +1,4 @@
from collections.abc import Mapping
from collections import abc
from itertools import chain, repeat, zip_longest
from operator import itemgetter
from typing import (
@@ -9,6 +9,7 @@ from typing import (
Iterable,
Iterator,
List,
Mapping,
MutableSequence,
Sequence,
Tuple,
@@ -133,6 +134,10 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
self.drop(*(set(self._keys) - set(col_names)))
self._keys = list(col_names)
def is_empty(self, col_name: str) -> bool:
col = self.column(col_name)
return not any(item != self._fill_value for item in col)
def to_csv(self) -> str:
import csv
from io import StringIO
@@ -149,7 +154,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
self._columns[name] = Column([self._fill_value] * len(self))
self._keys.append(name)
def row_from_dict(self, d: Dict[str, "CellT"]) -> None:
def row_from_dict(self, d: Mapping[str, "CellT"]) -> None:
keys = self.keys()
for key in d:
if key not in keys:
@@ -165,7 +170,9 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
ui.table(self, headers=self.keys(), **kwargs)
def as_dict(self, cols: Iterable[str] = None) -> Iterable[Dict[str, str]]:
def as_dict(
self, cols: Iterable[str] = None
) -> Iterable[Dict[str, "CellT"]]:
keys = self.keys() if cols is None else set(cols)
return [
{k: self._columns[k][i] for k in keys} for i in range(len(self))
@@ -183,7 +190,7 @@ def _format_field(
if isinstance(_val, float) and precision:
func = round if round_digits else _normalize_float
return func(_val, precision)
if isinstance(_val, Mapping):
if isinstance(_val, abc.Mapping):
return {k: _format(v) for k, v in _val.items()}
if isinstance(_val, list):
return [_format(x) for x in _val]
@@ -274,11 +281,9 @@ def metrics_table(
td = TabularData(["Revision", "Path"], fill_value="-")
for branch, val in metrics.items():
for fname, metric in val.items():
row_data: Dict[str, str] = {
"Revision": branch,
"Path": fname,
}
for fname, metric in val.get("data", {}).items():
row_data: Dict[str, str] = {"Revision": branch, "Path": fname}
metric = metric.get("data", {})
flattened = (
flatten(format_dict(metric))
if isinstance(metric, dict)
+12 -4
View File
@@ -20,7 +20,15 @@ class ConfigError(DvcException):
super().__init__(f"config file error: {msg}")
class NoRemoteError(ConfigError):
class RemoteConfigError(ConfigError):
pass
class NoRemoteError(RemoteConfigError):
pass
class RemoteNotFoundError(RemoteConfigError):
pass
@@ -63,7 +71,7 @@ class Config(dict):
CONFIG_LOCAL = "config.local"
def __init__(
self, dvc_dir=None, validate=True, fs=None, config=None,
self, dvc_dir=None, validate=True, fs=None, config=None
): # pylint: disable=super-init-not-called
from dvc.fs.local import LocalFileSystem
@@ -79,7 +87,7 @@ class Config(dict):
else:
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
self.wfs = LocalFileSystem(None, {"url": self.dvc_dir})
self.wfs = LocalFileSystem(url=self.dvc_dir)
self.fs = fs or self.wfs
self.load(validate=validate, config=config)
@@ -154,7 +162,7 @@ class Config(dict):
filename = self.files[level]
fs = self._get_fs(level)
if fs.exists(filename, use_dvcignore=False):
if fs.exists(filename):
with fs.open(filename) as fobj:
conf_obj = ConfigObj(fobj)
else:
+8 -2
View File
@@ -93,7 +93,7 @@ HTTP_COMMON = {
"user": str,
"password": str,
"ask_password": Bool,
"ssl_verify": Bool,
"ssl_verify": Any(Bool, str),
"method": str,
}
WEBDAV_COMMON = {
@@ -104,6 +104,7 @@ WEBDAV_COMMON = {
"cert_path": str,
"key_path": str,
"timeout": Coerce(int),
"ssl_verify": Any(Bool, str),
}
SCHEMA = {
@@ -145,7 +146,7 @@ SCHEMA = {
"session_token": str,
Optional("listobjects", default=False): Bool, # obsoleted
Optional("use_ssl", default=True): Bool,
Optional("ssl_verify", default=True): Bool,
"ssl_verify": Any(Bool, str),
"sse": str,
"sse_kms_key_id": str,
"acl": str,
@@ -188,6 +189,11 @@ SCHEMA = {
"tenant_id": str,
"client_id": str,
"client_secret": str,
"allow_anonymous_login": Bool,
"exclude_environment_credential": Bool,
"exclude_visual_studio_code_credential": Bool,
"exclude_shared_token_cache_credential": Bool,
"exclude_managed_identity_credential": Bool,
**REMOTE_COMMON,
},
"oss": {
+1 -1
View File
@@ -33,7 +33,7 @@ def _spawn_windows(cmd, env):
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
_popen(
cmd, env=env, creationflags=creationflags, startupinfo=startupinfo,
cmd, env=env, creationflags=creationflags, startupinfo=startupinfo
).communicate()
+10 -10
View File
@@ -163,7 +163,7 @@ class AsciiCanvas:
self.point(x0 + width, y0 + height, "+")
def _build_sugiyama_layout(vertexes, edges):
def _build_sugiyama_layout(vertices, edges):
#
# Just a reminder about naming conventions:
# +------------X
@@ -174,17 +174,17 @@ def _build_sugiyama_layout(vertexes, edges):
# Y
#
vertexes = {v: Vertex(f" {v} ") for v in vertexes}
vertices = {v: Vertex(f" {v} ") for v in vertices}
# NOTE: reverting edges to correctly orientate the graph
edges = [Edge(vertexes[e], vertexes[s]) for s, e in edges]
vertexes = vertexes.values()
graph = Graph(vertexes, edges)
edges = [Edge(vertices[e], vertices[s]) for s, e in edges]
vertices = vertices.values()
graph = Graph(vertices, edges)
for vertex in vertexes:
for vertex in vertices:
vertex.view = VertexViewer(vertex.data)
# NOTE: determine min box length to create the best layout
minw = min(v.view.w for v in vertexes)
minw = min(v.view.w for v in vertices)
for edge in edges:
edge.view = EdgeViewer()
@@ -204,11 +204,11 @@ def _build_sugiyama_layout(vertexes, edges):
return sug
def draw(vertexes, edges):
def draw(vertices, edges):
"""Build a DAG and draw it in ASCII.
Args:
vertexes (list): list of graph vertexes.
vertices (list): list of graph vertices.
edges (list): list of graph edges.
"""
# pylint: disable=too-many-locals
@@ -217,7 +217,7 @@ def draw(vertexes, edges):
Xs = [] # pylint: disable=invalid-name
Ys = [] # pylint: disable=invalid-name
sug = _build_sugiyama_layout(vertexes, edges)
sug = _build_sugiyama_layout(vertices, edges)
for vertex in sug.g.sV:
# NOTE: moving boxes w/2 to the left
+75 -42
View File
@@ -1,6 +1,13 @@
"""Manages dvc remotes that user can use with push/pull/status commands."""
import logging
from typing import TYPE_CHECKING, Iterable, Optional
from dvc.objects.db import get_index
if TYPE_CHECKING:
from dvc.hash_info import HashInfo
from dvc.objects.db.base import ObjectDB
logger = logging.getLogger(__name__)
@@ -19,14 +26,18 @@ class DataCloud:
def __init__(self, repo):
self.repo = repo
def get_remote(self, name=None, command="<command>"):
def get_remote_odb(
self,
name: Optional[str] = None,
command: str = "<command>",
) -> "ObjectDB":
from dvc.config import NoRemoteError
if not name:
name = self.repo.config["core"].get("remote")
if name:
return self._init_remote(name)
return self._init_odb(name)
if bool(self.repo.config["remote"]):
error_msg = (
@@ -43,85 +54,107 @@ class DataCloud:
raise NoRemoteError(error_msg)
def _init_remote(self, name):
from dvc.remote import get_remote
def _init_odb(self, name):
from dvc.fs import get_cloud_fs
from dvc.objects.db import get_odb
return get_remote(self.repo, name=name)
cls, config, path_info = get_cloud_fs(self.repo, name=name)
config["tmp_dir"] = self.repo.tmp_dir
return get_odb(cls(**config), path_info, **config)
def push(
self, cache, jobs=None, remote=None, show_checksums=False,
self,
objs: Iterable["HashInfo"],
jobs: Optional[int] = None,
remote: Optional[str] = None,
odb: Optional["ObjectDB"] = None,
):
"""Push data items in a cloud-agnostic way.
Args:
cache (NamedCache): named checksums to push to the cloud.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.BaseRemote): optional remote to push to.
objs: objects to push to the cloud.
jobs: number of jobs that can be running simultaneously.
remote: optional name of remote to push to.
By default remote from core.remote config option is used.
show_checksums (bool): show checksums instead of file names in
information messages.
odb: optional ODB to push to. Overrides remote.
"""
remote = self.get_remote(remote, "push")
from dvc.objects.transfer import transfer
return remote.push(
if not odb:
odb = self.get_remote_odb(remote, "push")
return transfer(
self.repo.odb.local,
cache,
odb,
objs,
jobs=jobs,
show_checksums=show_checksums,
dest_index=get_index(odb),
cache_odb=self.repo.odb.local,
)
def pull(
self, cache, jobs=None, remote=None, show_checksums=False,
self,
objs: Iterable["HashInfo"],
jobs: Optional[int] = None,
remote: Optional[str] = None,
odb: Optional["ObjectDB"] = None,
):
"""Pull data items in a cloud-agnostic way.
Args:
cache (NamedCache): named checksums to pull from the cloud.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.BaseRemote): optional remote to pull from.
objs: objects to pull from the cloud.
jobs: number of jobs that can be running simultaneously.
remote: optional name of remote to pull from.
By default remote from core.remote config option is used.
show_checksums (bool): show checksums instead of file names in
information messages.
odb: optional ODB to pull from. Overrides remote.
"""
remote = self.get_remote(remote, "pull")
from dvc.objects.transfer import transfer
return remote.pull(
if not odb:
odb = self.get_remote_odb(remote, "pull")
return transfer(
odb,
self.repo.odb.local,
cache,
objs,
jobs=jobs,
show_checksums=show_checksums,
src_index=get_index(odb),
cache_odb=self.repo.odb.local,
verify=odb.verify,
)
def status(
self,
cache,
jobs=None,
remote=None,
show_checksums=False,
log_missing=True,
objs: Iterable["HashInfo"],
jobs: Optional[int] = None,
remote: Optional[str] = None,
odb: Optional["ObjectDB"] = None,
log_missing: bool = True,
):
"""Check status of data items in a cloud-agnostic way.
Args:
cache (NamedCache): named checksums to check status for.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.BaseRemote): optional remote to compare
objs: objects to check status for.
jobs: number of jobs that can be running simultaneously.
remote: optional remote to compare
cache to. By default remote from core.remote config option
is used.
show_checksums (bool): show checksums instead of file names in
information messages.
log_missing (bool): log warning messages if file doesn't exist
odb: optional ODB to check status from. Overrides remote.
log_missing: log warning messages if file doesn't exist
neither in cache, neither in cloud.
"""
remote = self.get_remote(remote, "status")
return remote.status(
from dvc.objects.status import compare_status
if not odb:
odb = self.get_remote_odb(remote, "status")
return compare_status(
self.repo.odb.local,
cache,
odb,
objs,
jobs=jobs,
show_checksums=show_checksums,
log_missing=log_missing,
dest_index=get_index(odb),
cache_odb=self.repo.odb.local,
)
def get_url_for(self, remote, checksum):
remote = self.get_remote(remote)
return str(remote.odb.hash_to_path_info(checksum))
remote_odb = self.get_remote_odb(remote)
return str(remote_odb.hash_to_path_info(checksum))
+11 -46
View File
@@ -1,58 +1,24 @@
from collections import defaultdict
from urllib.parse import urlparse
from typing import Any, Mapping
import dvc.output as output
from dvc.dependency.azure import AzureDependency
from dvc.dependency.gs import GSDependency
from dvc.dependency.hdfs import HDFSDependency
from dvc.dependency.http import HTTPDependency
from dvc.dependency.https import HTTPSDependency
from dvc.dependency.local import LocalDependency
from dvc.dependency.param import ParamsDependency
from dvc.dependency.s3 import S3Dependency
from dvc.dependency.ssh import SSHDependency
from dvc.dependency.webdav import WebDAVDependency
from dvc.dependency.webdavs import WebDAVSDependency
from dvc.dependency.webhdfs import WebHDFSDependency
from dvc.output.base import BaseOutput
from dvc.scheme import Schemes
from dvc.output import ARTIFACT_SCHEMA, Output
from ..fs import get_cloud_fs
from .base import Dependency
from .param import ParamsDependency
from .repo import RepoDependency
DEP_MAP = {
Schemes.LOCAL: LocalDependency,
Schemes.SSH: SSHDependency,
Schemes.S3: S3Dependency,
Schemes.AZURE: AzureDependency,
Schemes.GS: GSDependency,
Schemes.HDFS: HDFSDependency,
Schemes.HTTP: HTTPDependency,
Schemes.HTTPS: HTTPSDependency,
Schemes.WEBDAV: WebDAVDependency,
Schemes.WEBDAVS: WebDAVSDependency,
Schemes.WEBHDFS: WebHDFSDependency,
}
# NOTE: schema for dependencies is basically the same as for outputs, but
# without output-specific entries like 'cache' (whether or not output is
# cached, see -o and -O flags for `dvc run`) and 'metric' (whether or not
# output is a metrics file and how to parse it, see `-M` flag for `dvc run`).
SCHEMA = output.SCHEMA.copy()
del SCHEMA[BaseOutput.PARAM_CACHE]
del SCHEMA[BaseOutput.PARAM_METRIC]
del SCHEMA[BaseOutput.PARAM_DESC]
SCHEMA.update(RepoDependency.REPO_SCHEMA)
SCHEMA.update(ParamsDependency.PARAM_SCHEMA)
SCHEMA: Mapping[str, Any] = {
**ARTIFACT_SCHEMA,
**RepoDependency.REPO_SCHEMA,
**ParamsDependency.PARAM_SCHEMA,
}
def _get(stage, p, info):
parsed = urlparse(p) if p else None
if parsed and parsed.scheme == "remote":
fs = get_cloud_fs(stage.repo, name=parsed.netloc)
return DEP_MAP[fs.scheme](stage, p, info, fs=fs)
if info and info.get(RepoDependency.PARAM_REPO):
repo = info.pop(RepoDependency.PARAM_REPO)
return RepoDependency(repo, stage, p, info)
@@ -61,14 +27,13 @@ def _get(stage, p, info):
params = info.pop(ParamsDependency.PARAM_PARAMS)
return ParamsDependency(stage, p, params)
dep_cls = DEP_MAP.get(parsed.scheme, LocalDependency)
return dep_cls(stage, p, info)
return Dependency(stage, p, info)
def loadd_from(stage, d_list):
ret = []
for d in d_list:
p = d.pop(BaseOutput.PARAM_PATH, None)
p = d.pop(Output.PARAM_PATH, None)
ret.append(_get(stage, p, d))
return ret
-8
View File
@@ -1,8 +0,0 @@
from dvc.dependency.base import BaseDependency
from dvc.output.base import BaseOutput
from ..fs.azure import AzureFileSystem
class AzureDependency(BaseDependency, BaseOutput):
FS_CLS = AzureFileSystem
+2 -1
View File
@@ -1,6 +1,7 @@
from typing import Type
from dvc.exceptions import DvcException
from dvc.output import Output
class DependencyDoesNotExistError(DvcException):
@@ -20,7 +21,7 @@ class DependencyIsStageFileError(DvcException):
super().__init__(f"DVC file '{path}' cannot be a dependency.")
class BaseDependency:
class Dependency(Output):
IS_DEPENDENCY = True
DoesNotExistError = DependencyDoesNotExistError # type: Type[DvcException]
-8
View File
@@ -1,8 +0,0 @@
from dvc.dependency.base import BaseDependency
from dvc.output.base import BaseOutput
from ..fs.gs import GSFileSystem
class GSDependency(BaseDependency, BaseOutput):
FS_CLS = GSFileSystem
-6
View File
@@ -1,6 +0,0 @@
from dvc.dependency.base import BaseDependency
from dvc.output.hdfs import HDFSOutput
class HDFSDependency(BaseDependency, HDFSOutput):
pass
-8
View File
@@ -1,8 +0,0 @@
from dvc.dependency.base import BaseDependency
from dvc.output.base import BaseOutput
from ..fs.http import HTTPFileSystem
class HTTPDependency(BaseDependency, BaseOutput):
FS_CLS = HTTPFileSystem
-6
View File
@@ -1,6 +0,0 @@
from ..fs.https import HTTPSFileSystem
from .http import HTTPDependency
class HTTPSDependency(HTTPDependency):
FS_CLS = HTTPSFileSystem
-6
View File
@@ -1,6 +0,0 @@
from dvc.dependency.base import BaseDependency
from dvc.output.local import LocalOutput
class LocalDependency(BaseDependency, LocalOutput):
pass
+5 -5
View File
@@ -4,11 +4,12 @@ from collections import defaultdict
import dpath.util
from voluptuous import Any
from dvc.dependency.local import LocalDependency
from dvc.exceptions import DvcException
from dvc.hash_info import HashInfo
from dvc.utils.serialize import LOADERS, ParseError
from .base import Dependency
class MissingParamsError(DvcException):
pass
@@ -18,7 +19,7 @@ class BadParamFileError(DvcException):
pass
class ParamsDependency(LocalDependency):
class ParamsDependency(Dependency):
PARAM_PARAMS = "params"
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
DEFAULT_PARAMS_FILE = "params.yaml"
@@ -39,7 +40,6 @@ class ParamsDependency(LocalDependency):
path
or os.path.join(stage.repo.root_dir, self.DEFAULT_PARAMS_FILE),
info=info,
fs=stage.repo.fs,
)
def dumpd(self):
@@ -98,7 +98,7 @@ class ParamsDependency(LocalDependency):
f"Unable to read parameters from '{self}'"
) from exc
def read_params_d(self):
def read_params_d(self, **kwargs):
config = self._read()
ret = {}
@@ -128,7 +128,7 @@ class ParamsDependency(LocalDependency):
if missing_params:
raise MissingParamsError(
"Parameters '{}' are missing from '{}'.".format(
", ".join(missing_params), self,
", ".join(missing_params), self
)
)
+149 -57
View File
@@ -1,11 +1,21 @@
import os
from collections import defaultdict
from copy import copy
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple
from voluptuous import Required
from dvc.path_info import PathInfo
from .local import LocalDependency
from .base import Dependency
if TYPE_CHECKING:
from dvc.hash_info import HashInfo
from dvc.objects.db.base import ObjectDB
from dvc.objects.file import HashFile
class RepoDependency(LocalDependency):
class RepoDependency(Dependency):
PARAM_REPO = "repo"
PARAM_URL = "url"
PARAM_REV = "rev"
@@ -21,47 +31,22 @@ class RepoDependency(LocalDependency):
def __init__(self, def_repo, stage, *args, **kwargs):
self.def_repo = def_repo
self._staged_objs: Dict[str, "HashFile"] = {}
super().__init__(stage, *args, **kwargs)
def _parse_path(self, fs, path):
def _parse_path(self, fs, path_info):
return None
@property
def is_in_repo(self):
return False
@property
def repo_pair(self):
d = self.def_repo
rev = d.get(self.PARAM_REV_LOCK) or d.get(self.PARAM_REV)
return d[self.PARAM_URL], rev
def __str__(self):
return "{} ({})".format(self.def_path, self.def_repo[self.PARAM_URL])
def _make_repo(self, *, locked=True, **kwargs):
from dvc.external_repo import external_repo
d = self.def_repo
rev = (d.get("rev_lock") if locked else None) or d.get("rev")
return external_repo(d["url"], rev=rev, **kwargs)
def _get_hash(self, locked=True):
from dvc.objects.stage import stage
with self._make_repo(locked=locked) as repo:
path_info = PathInfo(repo.root_dir) / self.def_path
return stage(
self.repo.odb.local,
path_info,
repo.repo_fs,
self.repo.odb.local.fs.PARAM_CHECKSUM,
follow_subrepos=False,
).hash_info
return f"{self.def_path} ({self.def_repo[self.PARAM_URL]})"
def workspace_status(self):
current = self._get_hash(locked=True)
updated = self._get_hash(locked=False)
current = self.get_obj(locked=True).hash_info
updated = self.get_obj(locked=False).hash_info
if current != updated:
return {str(self): "update available"}
@@ -79,37 +64,23 @@ class RepoDependency(LocalDependency):
def download(self, to, jobs=None):
from dvc.checkout import checkout
from dvc.config import NoRemoteError
from dvc.exceptions import NoOutputOrStageError
from dvc.objects import save
from dvc.objects.stage import stage
odb = self.repo.odb.local
for odb, objs in self.get_used_objs().items():
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
with self._make_repo(cache_dir=odb.cache_dir) as repo:
if self.def_repo.get(self.PARAM_REV_LOCK) is None:
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
path_info = PathInfo(repo.root_dir) / self.def_path
try:
repo.fetch([path_info.fspath], jobs=jobs, recursive=True)
except (NoOutputOrStageError, NoRemoteError):
pass
obj = stage(
odb,
path_info,
repo.repo_fs,
odb.fs.PARAM_CHECKSUM,
jobs=jobs,
follow_subrepos=False,
)
save(odb, obj, jobs=jobs)
checkout(to.path_info, to.fs, obj, odb)
obj = self.get_obj()
checkout(
to.path_info,
to.fs,
obj,
self.repo.odb.local,
dvcignore=None,
state=self.repo.state,
)
def update(self, rev=None):
if rev:
self.def_repo[self.PARAM_REV] = rev
with self._make_repo(locked=False) as repo:
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
@@ -118,3 +89,124 @@ class RepoDependency(LocalDependency):
# origin project url and rev_lock, and it makes RepoDependency
# immutable, hence its impossible for checksum to change.
return False
def get_used_objs(
self, **kwargs
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
used, _ = self._get_used_and_obj(**kwargs)
return used
def _get_used_and_obj(
self, obj_only=False, **kwargs
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "HashFile"]:
from dvc.config import NoRemoteError
from dvc.exceptions import NoOutputOrStageError, PathMissingError
from dvc.objects.stage import stage
from dvc.objects.tree import Tree
local_odb = self.repo.odb.local
locked = kwargs.pop("locked", True)
with self._make_repo(
locked=locked, cache_dir=local_odb.cache_dir
) as repo:
used_obj_ids = defaultdict(set)
rev = repo.get_rev()
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
self.def_repo[self.PARAM_REV_LOCK] = rev
path_info = PathInfo(repo.root_dir) / str(self.def_path)
if not obj_only:
try:
for odb, obj_ids in repo.used_objs(
[os.fspath(path_info)],
force=True,
jobs=kwargs.get("jobs"),
recursive=True,
).items():
if odb is None:
odb = repo.cloud.get_remote_odb()
odb.read_only = True
self._check_circular_import(odb, obj_ids)
used_obj_ids[odb].update(obj_ids)
except (NoRemoteError, NoOutputOrStageError):
pass
try:
staging, staged_obj = stage(
local_odb,
path_info,
repo.repo_fs,
local_odb.fs.PARAM_CHECKSUM,
)
except FileNotFoundError as exc:
raise PathMissingError(
self.def_path, self.def_repo[self.PARAM_URL]
) from exc
staging = copy(staging)
staging.read_only = True
self._staged_objs[rev] = staged_obj
used_obj_ids[staging].add(staged_obj.hash_info)
if isinstance(staged_obj, Tree):
used_obj_ids[staging].update(
entry.hash_info for _, entry in staged_obj
)
return used_obj_ids, staged_obj
def _check_circular_import(self, odb, obj_ids):
from dvc.exceptions import CircularImportError
from dvc.fs.repo import RepoFileSystem
from dvc.objects.db.reference import ReferenceObjectDB
from dvc.objects.tree import Tree
if not isinstance(odb, ReferenceObjectDB):
return
def iter_objs():
for hash_info in obj_ids:
if hash_info.isdir:
tree = Tree.load(odb, hash_info)
yield from (odb.get(entry.hash_info) for _, entry in tree)
else:
yield odb.get(hash_info)
checked_urls = set()
for obj in iter_objs():
if not isinstance(obj.fs, RepoFileSystem):
continue
if (
obj.fs.repo_url in checked_urls
or obj.fs.root_dir in checked_urls
):
continue
self_url = self.repo.url or self.repo.root_dir
if (
obj.fs.repo_url is not None
and obj.fs.repo_url == self_url
or obj.fs.root_dir == self.repo.root_dir
):
raise CircularImportError(self, obj.fs.repo_url, self_url)
checked_urls.update([obj.fs.repo_url, obj.fs.root_dir])
def get_obj(self, filter_info=None, **kwargs):
locked = kwargs.get("locked", True)
rev = self._get_rev(locked=locked)
if rev in self._staged_objs:
return self._staged_objs[rev]
_, obj = self._get_used_and_obj(
obj_only=True, filter_info=filter_info, **kwargs
)
return obj
def _make_repo(self, locked=True, **kwargs):
from dvc.external_repo import external_repo
d = self.def_repo
rev = self._get_rev(locked=locked)
return external_repo(d[self.PARAM_URL], rev=rev, **kwargs)
def _get_rev(self, locked=True):
d = self.def_repo
return (d.get(self.PARAM_REV_LOCK) if locked else None) or d.get(
self.PARAM_REV
)
-6
View File
@@ -1,6 +0,0 @@
from dvc.dependency.base import BaseDependency
from dvc.output.s3 import S3Output
class S3Dependency(BaseDependency, S3Output):
pass
-6
View File
@@ -1,6 +0,0 @@
from dvc.dependency.base import BaseDependency
from dvc.output.ssh import SSHOutput
class SSHDependency(BaseDependency, SSHOutput):
pass
-8
View File
@@ -1,8 +0,0 @@
from dvc.dependency.base import BaseDependency
from dvc.output.base import BaseOutput
from ..fs.webdav import WebDAVFileSystem
class WebDAVDependency(BaseDependency, BaseOutput):
FS_CLS = WebDAVFileSystem
-6
View File
@@ -1,6 +0,0 @@
from ..fs.webdavs import WebDAVSFileSystem
from .webdav import WebDAVDependency
class WebDAVSDependency(WebDAVDependency):
FS_CLS = WebDAVSFileSystem
-6
View File
@@ -1,6 +0,0 @@
from dvc.dependency.base import BaseDependency
from dvc.output.webhdfs import WebHDFSOutput
class WebHDFSDependency(BaseDependency, WebHDFSOutput):
pass
+34 -18
View File
@@ -37,8 +37,12 @@ PIPELINE_LOCK = "dvc.lock"
class FileIsGitIgnored(DvcException):
def __init__(self, path):
super().__init__(f"'{path}' is git-ignored.")
def __init__(self, path, pipeline_file=False):
super().__init__(
"{}'{}' is git-ignored.".format(
"bad DVC file name " if pipeline_file else "", path
)
)
class LockfileCorruptedError(DvcException):
@@ -66,7 +70,19 @@ def is_lock_file(path):
return os.path.basename(path) == PIPELINE_LOCK
def check_dvc_filename(path):
def is_git_ignored(repo, path):
from dvc.fs.local import LocalFileSystem
from dvc.scm.base import NoSCMError
try:
return isinstance(repo.fs, LocalFileSystem) and repo.scm.is_ignored(
path
)
except NoSCMError:
return False
def check_dvcfile_path(repo, path):
if not is_valid_filename(path):
raise StageFileBadNameError(
"bad DVC file name '{}'. DVC files should be named "
@@ -75,6 +91,9 @@ def check_dvc_filename(path):
)
)
if is_git_ignored(repo, path):
raise FileIsGitIgnored(relpath(path), True)
class FileMixin:
SCHEMA = None
@@ -105,18 +124,15 @@ class FileMixin:
return relpath(self.path)
def exists(self):
return self.repo.fs.exists(self.path)
is_ignored = self.repo.dvcignore.is_ignored_file(self.path)
return self.repo.fs.exists(self.path) and not is_ignored
def _is_git_ignored(self):
from dvc.fs.local import LocalFileSystem
return isinstance(
self.repo.fs, LocalFileSystem
) and self.repo.scm.is_ignored(self.path)
return is_git_ignored(self.repo, self.path)
def _verify_filename(self):
if self.verify:
check_dvc_filename(self.path)
check_dvcfile_path(self.repo, self.path)
def _check_gitignored(self):
if self._is_git_ignored():
@@ -129,8 +145,10 @@ class FileMixin:
# 3. path doesn't represent a regular file
# 4. when the file is git ignored
if not self.exists():
is_ignored = self.repo.fs.exists(self.path, use_dvcignore=False)
raise StageFileDoesNotExistError(self.path, dvc_ignored=is_ignored)
dvc_ignored = self.repo.dvcignore.is_ignored_file(self.path)
raise StageFileDoesNotExistError(
self.path, dvc_ignored=dvc_ignored
)
self._verify_filename()
if not self.repo.fs.isfile(self.path):
@@ -181,10 +199,8 @@ class SingleStageFile(FileMixin):
assert not isinstance(stage, PipelineStage)
if self.verify:
check_dvc_filename(self.path)
logger.debug(
"Saving information to '{file}'.".format(file=relpath(self.path))
)
check_dvcfile_path(self.repo, self.path)
logger.debug(f"Saving information to '{relpath(self.path)}'.")
dump_yaml(self.path, serialize.to_single_stage_file(stage))
self.repo.scm.track_file(self.relpath)
@@ -218,7 +234,7 @@ class PipelineFile(FileMixin):
assert isinstance(stage, PipelineStage)
if self.verify:
check_dvc_filename(self.path)
check_dvcfile_path(self.repo, self.path)
if update_pipeline and not stage.is_data_source:
self._dump_pipeline_file(stage)
@@ -282,7 +298,7 @@ class PipelineFile(FileMixin):
if not self.exists():
return
with open(self.path, "r", encoding="utf-8") as f:
with open(self.path, encoding="utf-8") as f:
d = parse_yaml_for_update(f.read(), self.path)
self.validate(d, self.path)
+4
View File
@@ -6,3 +6,7 @@ DVCLIVE_PATH = "DVCLIVE_PATH"
DVCLIVE_SUMMARY = "DVCLIVE_SUMMARY"
DVCLIVE_HTML = "DVCLIVE_HTML"
DVCLIVE_RESUME = "DVCLIVE_RESUME"
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
+20 -40
View File
@@ -1,5 +1,4 @@
"""Exceptions raised by the dvc."""
from typing import List
class DvcException(Exception):
@@ -35,7 +34,7 @@ class OutputDuplicationError(DvcException):
)
else:
msg = "output '{}' is already specified in stages:\n{}".format(
output, "\n".join(f"\t- {s.addressing}" for s in stages),
output, "\n".join(f"\t- {s.addressing}" for s in stages)
)
super().__init__(msg)
self.stages = stages
@@ -173,36 +172,6 @@ class BadMetricError(DvcException):
)
class MetricsError(DvcException):
pass
class NoMetricsParsedError(MetricsError):
def __init__(self, command):
super().__init__(
f"Could not parse {command} files. Use `-v` option to see more "
"details."
)
class NoMetricsFoundError(MetricsError):
def __init__(self, command, run_options):
super().__init__(
f"No {command} files in this repository. "
f"Use `{run_options}` options for "
f"`dvc run` to mark stage outputs as {command}."
)
class MetricDoesNotExistError(MetricsError):
def __init__(self, targets: List[str]):
if len(targets) == 1:
msg = "'{}' does not exist."
else:
msg = "'{}' do not exist."
super().__init__(msg.format(", ".join(targets)))
class RecursiveAddingWhileUsingFilename(DvcException):
def __init__(self):
super().__init__(
@@ -259,18 +228,21 @@ class GitHookAlreadyExistsError(DvcException):
)
class DownloadError(DvcException):
class FileTransferError(DvcException):
_METHOD = "transfer"
def __init__(self, amount):
self.amount = amount
super().__init__(f"{amount} files failed to download")
super().__init__(f"{amount} files failed to {self._METHOD}")
class UploadError(DvcException):
def __init__(self, amount):
self.amount = amount
class DownloadError(FileTransferError):
_METHOD = "download"
super().__init__(f"{amount} files failed to upload")
class UploadError(FileTransferError):
_METHOD = "upload"
class CheckoutError(DvcException):
@@ -283,7 +255,7 @@ class CheckoutError(DvcException):
m = (
"Checkout failed for following targets:\n{}\nIs your "
"cache up to date?\n{}".format(
"\n".join(targets), error_link("missing-files"),
"\n".join(targets), error_link("missing-files")
)
)
super().__init__(m)
@@ -380,7 +352,15 @@ class CacheLinkError(DvcException):
def __init__(self, path_infos):
msg = "No possible cache link types for '{}'. {}".format(
", ".join([str(path) for path in path_infos]), self.SUPPORT_LINK,
", ".join([str(path) for path in path_infos]), self.SUPPORT_LINK
)
super().__init__(msg)
self.path_infos = path_infos
class CircularImportError(DvcException):
def __init__(self, dep, a, b):
super().__init__(
f"'{dep}' contains invalid circular import. "
f"DVC repo '{a}' already imports from '{b}'."
)
+14 -13
View File
@@ -36,23 +36,12 @@ def external_repo(
rev = rev or "refs/remotes/origin/HEAD"
cache_config = {
"cache": {
"dir": cache_dir or _get_cache_dir(url),
"type": cache_types,
}
"cache": {"dir": cache_dir or _get_cache_dir(url), "type": cache_types}
}
config = _get_remote_config(url) if os.path.isdir(url) else {}
config.update(cache_config)
def make_repo(path, **_kwargs):
_config = cache_config.copy()
if os.path.isdir(url):
rel = os.path.relpath(path, _kwargs["scm"].root_dir)
repo_path = os.path.join(url, rel)
_config.update(_get_remote_config(repo_path))
return Repo(path, config=_config, **_kwargs)
root_dir = path if for_write else os.path.realpath(path)
repo_kwargs = dict(
root_dir=root_dir,
@@ -60,7 +49,7 @@ def external_repo(
scm=None if for_write else Git(root_dir),
rev=None if for_write else rev,
config=config,
repo_factory=make_repo,
repo_factory=erepo_factory(url, cache_config),
**kwargs,
)
@@ -90,6 +79,18 @@ def external_repo(
_remove(path)
def erepo_factory(url, cache_config, *args, **kwargs):
def make_repo(path, **_kwargs):
_config = cache_config.copy()
if os.path.isdir(url):
rel = os.path.relpath(path, _kwargs["scm"].root_dir)
repo_path = os.path.join(url, rel)
_config.update(_get_remote_config(repo_path))
return Repo(path, config=_config, **_kwargs)
return make_repo
CLONES: Dict[str, str] = {}
CACHE_DIRS: Dict[str, str] = {}
+38 -7
View File
@@ -12,8 +12,7 @@ from .local import LocalFileSystem
from .oss import OSSFileSystem
from .s3 import S3FileSystem
from .ssh import SSHFileSystem
from .webdav import WebDAVFileSystem
from .webdavs import WebDAVSFileSystem
from .webdav import WebDAVFileSystem, WebDAVSFileSystem
from .webhdfs import WebHDFSFileSystem
FS_MAP = {
@@ -33,15 +32,21 @@ FS_MAP = {
}
def get_fs_cls(remote_conf):
scheme = urlparse(remote_conf["url"]).scheme
def get_fs_cls(remote_conf, scheme=None):
if not scheme:
scheme = urlparse(remote_conf["url"]).scheme
return FS_MAP.get(scheme, LocalFileSystem)
def get_fs_config(config, **kwargs):
name = kwargs.get("name")
if name:
remote_conf = config["remote"][name.lower()]
try:
remote_conf = config["remote"][name.lower()]
except KeyError:
from dvc.config import RemoteNotFoundError
raise RemoteNotFoundError(f"remote '{name}' doesn't exist")
else:
remote_conf = kwargs
return _resolve_remote_refs(config, remote_conf)
@@ -82,9 +87,35 @@ def get_cloud_fs(repo, **kwargs):
from dvc.config import ConfigError
from dvc.config_schema import SCHEMA, Invalid
remote_conf = get_fs_config(repo.config, **kwargs)
repo_config = repo.config if repo else {}
core_config = repo_config.get("core", {})
remote_conf = get_fs_config(repo_config, **kwargs)
try:
remote_conf = SCHEMA["remote"][str](remote_conf)
except Invalid as exc:
raise ConfigError(str(exc)) from None
return get_fs_cls(remote_conf)(repo, remote_conf)
if "jobs" not in remote_conf:
jobs = core_config.get("jobs")
if jobs:
remote_conf["jobs"] = jobs
if "checksum_jobs" not in remote_conf:
checksum_jobs = core_config.get("checksum_jobs")
if checksum_jobs:
remote_conf["checksum_jobs"] = checksum_jobs
cls = get_fs_cls(remote_conf)
if cls == GDriveFileSystem and repo:
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
url = remote_conf.pop("url")
path_info = cls.PATH_CLS(
cls._strip_protocol(url) # pylint:disable=protected-access
)
extras = cls._get_kwargs_from_urls(url) # pylint:disable=protected-access
conf = {**extras, **remote_conf} # remote config takes priority
return cls, conf, path_info
+2 -2
View File
@@ -1,7 +1,7 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List
from dvc.output import BaseOutput
from dvc.output import Output
from dvc.path_info import PathInfo
if TYPE_CHECKING:
@@ -33,7 +33,7 @@ class Metadata:
# optional fields
isdir: bool = False # is it a directory?
is_exec: bool = False # is it an executable?
outs: List[BaseOutput] = field(default_factory=list) # list of outputs
outs: List[Output] = field(default_factory=list) # list of outputs
def __post_init__(self):
self.output_exists = bool(self.outs)
+69 -43
View File
@@ -2,14 +2,16 @@ import logging
import os
import threading
from funcy import cached_property, wrap_prop
from fsspec.asyn import fsspec_loop
from fsspec.utils import infer_storage_options
from funcy import cached_property, memoize, wrap_prop
from dvc.exceptions import DvcException
from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
from dvc.utils import format_link
from .fsspec_wrapper import FSSpecWrapper
from .fsspec_wrapper import ObjectFSWrapper
logger = logging.getLogger(__name__)
_DEFAULT_CREDS_STEPS = (
@@ -23,7 +25,21 @@ class AzureAuthError(DvcException):
pass
class AzureFileSystem(FSSpecWrapper):
@memoize
def _az_config():
# NOTE: ideally we would've used get_default_cli().config from
# azure.cli.core, but azure-cli-core has a lot of conflicts with other
# dependencies. So instead we are just use knack directly
from knack.config import CLIConfig
config_dir = os.getenv(
"AZURE_CONFIG_DIR", os.path.expanduser(os.path.join("~", ".azure"))
)
return CLIConfig(config_dir=config_dir, config_env_var_prefix="AZURE")
# pylint:disable=abstract-method
class AzureFileSystem(ObjectFSWrapper):
scheme = Schemes.AZURE
PATH_CLS = CloudURLInfo
PARAM_CHECKSUM = "etag"
@@ -34,21 +50,23 @@ class AzureFileSystem(FSSpecWrapper):
"azure-identity": "azure.identity",
}
def __init__(self, repo, config):
super().__init__(repo, config)
@classmethod
def _strip_protocol(cls, path: str):
bucket = infer_storage_options(path).get("host")
if bucket:
return path
url = config.get("url")
self.path_info = self.PATH_CLS(url)
self.bucket = self.path_info.bucket
bucket = _az_config().get("storage", "container_name", None)
return f"azure://{bucket}"
if not self.bucket:
container = self._az_config.get("storage", "container_name", None)
url = f"azure://{container}"
@staticmethod
def _get_kwargs_from_urls(urlpath):
ops = infer_storage_options(urlpath)
if "host" in ops:
return {"bucket": ops["host"]}
return {}
self.path_info = self.PATH_CLS(url)
self.bucket = self.path_info.bucket
def _prepare_credentials(self, config):
def _prepare_credentials(self, **config):
from azure.identity.aio import DefaultAzureCredential
# Disable spam from failed cred types for DefaultAzureCredential
@@ -57,28 +75,54 @@ class AzureFileSystem(FSSpecWrapper):
login_info = {}
login_info["connection_string"] = config.get(
"connection_string",
self._az_config.get("storage", "connection_string", None),
_az_config().get("storage", "connection_string", None),
)
login_info["account_name"] = config.get(
"account_name", self._az_config.get("storage", "account", None)
"account_name", _az_config().get("storage", "account", None)
)
login_info["account_key"] = config.get(
"account_key", self._az_config.get("storage", "key", None)
"account_key", _az_config().get("storage", "key", None)
)
login_info["sas_token"] = config.get(
"sas_token", self._az_config.get("storage", "sas_token", None)
"sas_token", _az_config().get("storage", "sas_token", None)
)
login_info["tenant_id"] = config.get("tenant_id")
login_info["client_id"] = config.get("client_id")
login_info["client_secret"] = config.get("client_secret")
if not any(
value for key, value in login_info.items() if key != "account_name"
):
login_info["credential"] = DefaultAzureCredential(
exclude_interactive_browser_credential=False
if not (login_info["account_name"] or login_info["connection_string"]):
raise AzureAuthError(
"Authentication to Azure Blob Storage requires either "
"account_name or connection_string.\nLearn more about "
"configuration settings at "
+ format_link("https://man.dvc.org/remote/modify")
)
any_secondary = any(
value for key, value in login_info.items() if key != "account_name"
)
if (
login_info["account_name"]
and not any_secondary
and not config.get("allow_anonymous_login", False)
):
with fsspec_loop():
login_info["credential"] = DefaultAzureCredential(
exclude_interactive_browser_credential=False,
exclude_environment_credential=config.get(
"exclude_environment_credential", False
),
exclude_visual_studio_code_credential=config.get(
"exclude_visual_studio_code_credential", False
),
exclude_shared_token_cache_credential=config.get(
"exclude_shared_token_cache_credential", False
),
exclude_managed_identity_credential=config.get(
"exclude_managed_identity_credential", False
),
)
for login_method, required_keys in [ # noqa
("connection string", ["connection_string"]),
(
@@ -101,18 +145,6 @@ class AzureFileSystem(FSSpecWrapper):
self.login_method = login_method
return login_info
@cached_property
def _az_config(self):
# NOTE: ideally we would've used get_default_cli().config from
# azure.cli.core, but azure-cli-core has a lot of conflicts with other
# dependencies. So instead we are just use knack directly
from knack.config import CLIConfig
config_dir = os.getenv(
"AZURE_CONFIG_DIR", os.path.expanduser(os.path.join("~", ".azure"))
)
return CLIConfig(config_dir=config_dir, config_env_var_prefix="AZURE")
@wrap_prop(threading.Lock())
@cached_property
def fs(self):
@@ -120,16 +152,10 @@ class AzureFileSystem(FSSpecWrapper):
from azure.core.exceptions import AzureError
try:
file_system = AzureBlobFileSystem(**self.fs_args)
if self.bucket not in [
container.rstrip("/") for container in file_system.ls("/")
]:
file_system.mkdir(self.bucket)
return AzureBlobFileSystem(**self.fs_args)
except (ValueError, AzureError) as e:
raise AzureAuthError(
f"Authentication to Azure Blob Storage via {self.login_method}"
" failed.\nLearn more about configuration settings at"
f" {format_link('https://man.dvc.org/remote/modify')}"
) from e
return file_system
+43 -39
View File
@@ -4,8 +4,6 @@ from functools import partial, partialmethod
from multiprocessing import cpu_count
from typing import Any, ClassVar, Dict, FrozenSet, Optional
from funcy import cached_property
from dvc.exceptions import DvcException
from dvc.path_info import URLInfo
from dvc.progress import Tqdm
@@ -35,6 +33,8 @@ class RemoteMissingDepsError(DvcException):
class BaseFileSystem:
sep = "/"
scheme = "base"
REQUIRES: ClassVar[Dict[str, str]] = {}
PATH_CLS = URLInfo # type: Any
@@ -54,29 +54,24 @@ class BaseFileSystem:
PARAM_CHECKSUM: ClassVar[Optional[str]] = None
DETAIL_FIELDS: FrozenSet[str] = frozenset()
def __init__(self, repo, config):
self.repo = repo
self.config = config
def __init__(self, **kwargs):
self._check_requires(**kwargs)
self._check_requires()
self.jobs = kwargs.get("jobs") or self._JOBS
self.hash_jobs = kwargs.get("checksum_jobs") or self.HASH_JOBS
self._config = kwargs
self.path_info = None
@property
def config(self):
return self._config
@cached_property
def jobs(self):
return (
self.config.get("jobs")
or (self.repo and self.repo.config["core"].get("jobs"))
or self._JOBS
)
@classmethod
def _strip_protocol(cls, path: str):
return path
@cached_property
def hash_jobs(self):
return (
self.config.get("checksum_jobs")
or (self.repo and self.repo.config["core"].get("checksum_jobs"))
or self.HASH_JOBS
)
@staticmethod
def _get_kwargs_from_urls(urlpath): # pylint:disable=unused-argument
return {}
@classmethod
def get_missing_deps(cls):
@@ -91,7 +86,7 @@ class BaseFileSystem:
return missing
def _check_requires(self):
def _check_requires(self, **kwargs):
from ..scheme import Schemes
from ..utils import format_link
from ..utils.pkg import PKG
@@ -100,7 +95,7 @@ class BaseFileSystem:
if not missing:
return
url = self.config.get("url", f"{self.scheme}://")
url = kwargs.get("url", f"{self.scheme}://")
scheme = self.scheme
if scheme == Schemes.WEBDAVS:
@@ -139,7 +134,7 @@ class BaseFileSystem:
raise RemoteActionNotImplemented("open", self.scheme)
def exists(self, path_info, use_dvcignore=True) -> bool:
def exists(self, path_info) -> bool:
raise NotImplementedError
# pylint: disable=unused-argument
@@ -166,6 +161,10 @@ class BaseFileSystem:
"""Check if this file is an independent copy."""
return False # We can't be sure by default
def walk(self, top, topdown=True, onerror=None, **kwargs):
"""Return a generator with (root, dirs, files)."""
raise NotImplementedError
def walk_files(self, path_info, **kwargs):
"""Return a generator with `PathInfo`s to all the files.
@@ -178,7 +177,7 @@ class BaseFileSystem:
def ls(self, path_info, detail=False):
raise RemoteActionNotImplemented("ls", self.scheme)
def find(self, path_info, detail=False):
def find(self, path_info, detail=False, prefix=None):
raise RemoteActionNotImplemented("find", self.scheme)
def is_empty(self, path_info):
@@ -193,7 +192,7 @@ class BaseFileSystem:
def remove(self, path_info):
raise RemoteActionNotImplemented("remove", self.scheme)
def makedirs(self, path_info):
def makedirs(self, path_info, **kwargs):
"""Optional: Implement only if the remote needs to create
directories before copying/linking/moving data
"""
@@ -222,9 +221,7 @@ class BaseFileSystem:
return False
return hash_.endswith(cls.CHECKSUM_DIR_SUFFIX)
def upload(
self, from_info, to_info, name=None, no_progress_bar=False,
):
def upload(self, from_info, to_info, name=None, no_progress_bar=False):
if not hasattr(self, "_upload"):
raise RemoteActionNotImplemented("upload", self.scheme)
@@ -245,14 +242,23 @@ class BaseFileSystem:
no_progress_bar=no_progress_bar,
)
def upload_fobj(self, fobj, to_info, no_progress_bar=False, **pbar_args):
def upload_fobj(
self, fobj, to_info, no_progress_bar=False, size=None, **pbar_args
):
if not hasattr(self, "_upload_fobj"):
raise RemoteActionNotImplemented("upload_fobj", self.scheme)
with Tqdm.wrapattr(
fobj, "read", disable=no_progress_bar, bytes=True, **pbar_args
fobj,
"read",
disable=no_progress_bar,
bytes=True,
total=size,
**pbar_args,
) as wrapped:
self._upload_fobj(wrapped, to_info) # pylint: disable=no-member
self._upload_fobj( # pylint: disable=no-member
wrapped, to_info, size=size
)
def download(
self,
@@ -279,14 +285,14 @@ class BaseFileSystem:
if not _only_file and self.isdir(from_info):
return self._download_dir(
from_info, to_info, name, no_progress_bar, jobs, **kwargs,
from_info, to_info, name, no_progress_bar, jobs, **kwargs
)
return self._download_file(from_info, to_info, name, no_progress_bar,)
return self._download_file(from_info, to_info, name, no_progress_bar)
download_file = partialmethod(download, _only_file=True)
def _download_dir(
self, from_info, to_info, name, no_progress_bar, jobs, **kwargs,
self, from_info, to_info, name, no_progress_bar, jobs, **kwargs
):
from_infos = list(self.walk_files(from_info, **kwargs))
if not from_infos:
@@ -303,7 +309,7 @@ class BaseFileSystem:
disable=no_progress_bar,
) as pbar:
download_files = pbar.wrap_fn(
partial(self._download_file, name=name, no_progress_bar=True,)
partial(self._download_file, name=name, no_progress_bar=True)
)
max_workers = jobs or self.jobs
with ThreadPoolExecutor(max_workers=max_workers) as executor:
@@ -327,9 +333,7 @@ class BaseFileSystem:
entry.cancel()
raise exc
def _download_file(
self, from_info, to_info, name, no_progress_bar,
):
def _download_file(self, from_info, to_info, name, no_progress_bar):
makedirs(to_info.parent, exist_ok=True)
logger.debug("Downloading '%s' to '%s'", from_info, to_info)
+40 -19
View File
@@ -10,7 +10,7 @@ from ._metadata import Metadata
from .base import BaseFileSystem
if typing.TYPE_CHECKING:
from dvc.output.base import BaseOutput
from dvc.output import Output
logger = logging.getLogger(__name__)
@@ -23,11 +23,18 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
repo: DVC repo.
"""
sep = os.sep
scheme = "local"
PARAM_CHECKSUM = "md5"
def __init__(self, repo):
super().__init__(repo, {"url": repo.root_dir})
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.repo = kwargs["repo"]
@property
def config(self):
raise NotImplementedError
def _find_outs(self, path, *args, **kwargs):
outs = self.repo.find_outs_by_path(path, *args, **kwargs)
@@ -42,20 +49,20 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
return outs
def _get_granular_hash(
self, path_info: PathInfo, out: "BaseOutput", remote=None
self, path_info: PathInfo, out: "Output", remote=None
):
assert isinstance(path_info, PathInfo)
# NOTE: use string paths here for performance reasons
key = tuple(relpath(path_info, out.path_info).split(os.sep))
out.get_dir_cache(remote=remote)
if out.obj is None:
raise FileNotFoundError
obj = out.obj.trie.get(key)
if obj:
return obj.hash_info
raise FileNotFoundError
def open( # type: ignore
self, path: PathInfo, mode="r", encoding=None, remote=None, **kwargs
): # pylint: disable=arguments-differ
def _get_fs_path(self, path: PathInfo, remote=None):
try:
outs = self._find_outs(path, strict=False)
except OutputNotFoundError as exc:
@@ -67,37 +74,45 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
raise IsADirectoryError
out = outs[0]
if not out.hash_info:
raise FileNotFoundError
if out.changed_cache(filter_info=path):
from dvc.config import NoRemoteError
try:
remote_obj = self.repo.cloud.get_remote(remote)
except NoRemoteError:
raise FileNotFoundError
remote_odb = self.repo.cloud.get_remote_odb(remote)
except NoRemoteError as exc:
raise FileNotFoundError from exc
if out.is_dir_checksum:
checksum = self._get_granular_hash(path, out).value
else:
checksum = out.hash_info.value
remote_info = remote_obj.odb.hash_to_path_info(checksum)
return remote_obj.fs.open(
remote_info, mode=mode, encoding=encoding
)
remote_info = remote_odb.hash_to_path_info(checksum)
return remote_odb.fs, remote_info
if out.is_dir_checksum:
checksum = self._get_granular_hash(path, out).value
cache_path = out.odb.hash_to_path_info(checksum).url
else:
cache_path = out.cache_path
return open(cache_path, mode=mode, encoding=encoding)
return out.odb.fs, cache_path
def exists(self, path): # pylint: disable=arguments-differ
def open( # type: ignore
self, path: PathInfo, mode="r", encoding=None, **kwargs
): # pylint: disable=arguments-renamed
fs, fspath = self._get_fs_path(path, **kwargs)
return fs.open(fspath, mode=mode, encoding=encoding)
def exists(self, path): # pylint: disable=arguments-renamed
try:
self.metadata(path)
return True
except FileNotFoundError:
return False
def isdir(self, path): # pylint: disable=arguments-differ
def isdir(self, path): # pylint: disable=arguments-renamed
try:
meta = self.metadata(path)
return meta.isdir
@@ -120,7 +135,7 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
except FileNotFoundError:
return True
def isfile(self, path): # pylint: disable=arguments-differ
def isfile(self, path): # pylint: disable=arguments-renamed
try:
meta = self.metadata(path)
return meta.isfile
@@ -226,7 +241,7 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
def info(self, path_info):
meta = self.metadata(path_info)
ret = {"type": "dir" if meta.isdir else "file"}
ret = {"type": "directory" if meta.isdir else "file"}
if meta.is_output and len(meta.outs) == 1 and meta.outs[0].hash_info:
hash_info = meta.outs[0].hash_info
ret["size"] = hash_info.size
@@ -240,3 +255,9 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
ret[obj.hash_info.name] = obj.hash_info.value
return ret
def _download(self, from_info, to_file, **kwargs):
fs, path = self._get_fs_path(from_info)
fs._download( # pylint: disable=protected-access
path, to_file, **kwargs
)
+160 -50
View File
@@ -2,36 +2,41 @@ import os
import shutil
from functools import lru_cache
from funcy import cached_property
from dvc.progress import Tqdm
from .base import BaseFileSystem
from .local import LocalFileSystem
# pylint: disable=no-member
class FSSpecWrapper(BaseFileSystem):
def __init__(self, repo, config):
super().__init__(repo, config)
self.fs_args = {"skip_instance_cache": True}
self.fs_args.update(self._prepare_credentials(config))
TRAVERSE_PREFIX_LEN = 2
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.fs_args = {"skip_instance_cache": True}
self.fs_args.update(self._prepare_credentials(**kwargs))
@cached_property
def fs(self):
raise NotImplementedError
@lru_cache(512)
def _with_bucket(self, path):
if isinstance(path, self.PATH_CLS):
return f"{path.bucket}/{path.path}"
return path
return str(path)
def _strip_bucket(self, entry):
try:
bucket, path = entry.split("/", 1)
except ValueError:
# If there is no path attached, only returns
# the bucket (top-level).
bucket, path = entry, None
return entry
return path or bucket
@staticmethod
def _get_kwargs_from_urls(urlpath):
from fsspec.utils import infer_storage_options
options = infer_storage_options(urlpath)
options.pop("path", None)
options.pop("protocol", None)
return options
def _strip_buckets(self, entries, detail=False):
for entry in entries:
@@ -43,37 +48,32 @@ class FSSpecWrapper(BaseFileSystem):
yield entry
def _entry_hook(self, entry):
"""Simple hook method to be overriden when wanted to process
"""Simple hook method to be overridden when wanted to process
entries within info() and ls(detail=True) calls"""
return entry
def _prepare_credentials(self, config): # pylint: disable=unused-argument
def _prepare_credentials(
self, **config
): # pylint: disable=unused-argument
"""Prepare the arguments for authentication to the
host filesystem"""
return {}
def isdir(self, path_info):
if not self.exists(path_info):
return False
def _isdir(self, path_info):
return self.fs.isdir(self._with_bucket(path_info))
# Directory in object storages are interpreted differently
# among different fsspec providers, so this logic is a temporary
# measure for us to adapt as of now. It checks whether it is a
# directory (as in a prefix with contents) or whether it is an empty
# file where it's name ends with a forward slash
entry = self.info(path_info)
return entry["type"] == "directory" or (
entry["size"] == 0
and entry["type"] == "file"
and entry["name"].endswith("/")
)
def isdir(self, path_info):
try:
return self._isdir(path_info)
except FileNotFoundError:
return False
def isfile(self, path_info):
if not self.exists(path_info):
try:
return not self._isdir(path_info)
except FileNotFoundError:
return False
return not self.isdir(path_info)
def is_empty(self, path_info):
entry = self.info(path_info)
if entry["type"] == "directory":
@@ -86,9 +86,10 @@ class FSSpecWrapper(BaseFileSystem):
return self.fs.open(self._with_bucket(path_info), mode=mode)
def copy(self, from_info, to_info):
self.makedirs(to_info.parent)
self.fs.copy(self._with_bucket(from_info), self._with_bucket(to_info))
def exists(self, path_info, use_dvcignore=False):
def exists(self, path_info) -> bool:
return self.fs.exists(self._with_bucket(path_info))
def ls(self, path_info, detail=False):
@@ -96,28 +97,24 @@ class FSSpecWrapper(BaseFileSystem):
files = self.fs.ls(path, detail=detail)
yield from self._strip_buckets(files, detail=detail)
def find(self, path_info, detail=False):
# pylint: disable=unused-argument
def find(self, path_info, detail=False, prefix=None):
path = self._with_bucket(path_info)
files = self.fs.find(path, detail=detail)
if detail:
files = files.values()
# When calling find() on a file, it returns the same file in a list.
# For object-based storages, the same behavior applies to empty
# directories since they are represented as files. This condition
# checks whether we should yield an empty list (if it is an empty
# directory) or just yield the file itself.
if len(files) == 1 and files[0] == path and self.isdir(path_info):
return None
yield from self._strip_buckets(files, detail=detail)
def walk_files(self, path_info, **kwargs):
for file in self.find(path_info):
for file in self.find(path_info, **kwargs):
yield path_info.replace(path=file)
def move(self, from_info, to_info):
self.fs.move(self._with_bucket(from_info), self._with_bucket(to_info))
def remove(self, path_info):
self.fs.rm(self._with_bucket(path_info))
self.fs.rm_file(self._with_bucket(path_info))
def info(self, path_info):
info = self.fs.info(self._with_bucket(path_info)).copy()
@@ -125,20 +122,27 @@ class FSSpecWrapper(BaseFileSystem):
info["name"] = self._strip_bucket(info["name"])
return info
def _upload_fobj(self, fobj, to_info):
def makedirs(self, path_info, **kwargs):
self.fs.makedirs(
self._with_bucket(path_info), exist_ok=kwargs.pop("exist_ok", True)
)
def _upload_fobj(self, fobj, to_info, size=None):
self.makedirs(to_info.parent)
with self.open(to_info, "wb") as fdest:
shutil.copyfileobj(fobj, fdest, length=fdest.blocksize)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **kwargs
):
total = os.path.getsize(from_file)
self.makedirs(to_info.parent)
size = os.path.getsize(from_file)
with open(from_file, "rb") as fobj:
self.upload_fobj(
fobj,
self._with_bucket(to_info),
to_info,
size=size,
desc=name,
total=total,
no_progress_bar=no_progress_bar,
)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
@@ -159,3 +163,109 @@ class FSSpecWrapper(BaseFileSystem):
) as wrapped:
with open(to_file, "wb") as fdest:
shutil.copyfileobj(wrapped, fdest, length=fobj.blocksize)
# pylint: disable=abstract-method
class ObjectFSWrapper(FSSpecWrapper):
TRAVERSE_PREFIX_LEN = 3
@lru_cache(512)
def _with_bucket(self, path):
if isinstance(path, self.PATH_CLS):
return f"{path.bucket}/{path.path}"
return path
def _strip_bucket(self, entry):
try:
bucket, path = entry.split("/", 1)
except ValueError:
# If there is no path attached, only returns
# the bucket (top-level).
bucket, path = entry, None
return path or bucket
def makedirs(self, path_info, **kwargs):
# For object storages make this method a no-op. The original
# fs.makedirs() method will only check if the bucket exists
# and create if it doesn't though we don't want to support
# that behavior, and the check will cost some time so we'll
# simply ignore all mkdir()/makedirs() calls.
return None
def _isdir(self, path_info):
# Directory in object storages are interpreted differently
# among different fsspec providers, so this logic is a temporary
# measure for us to adapt as of now. It checks whether it is a
# directory (as in a prefix with contents) or whether it is an empty
# file where it's name ends with a forward slash
entry = self.info(path_info)
return entry["type"] == "directory" or (
entry["size"] == 0
and entry["type"] == "file"
and entry["name"].endswith("/")
)
def find(self, path_info, detail=False, prefix=None):
if prefix:
path = self._with_bucket(path_info.parent)
files = self.fs.find(
path, detail=detail, prefix=path_info.parts[-1]
)
else:
path = self._with_bucket(path_info)
files = self.fs.find(path, detail=detail)
if detail:
files = list(files.values())
# When calling find() on a file, it returns the same file in a list.
# For object-based storages, the same behavior applies to empty
# directories since they are represented as files. This condition
# checks whether we should yield an empty list (if it is an empty
# directory) or just yield the file itself.
if len(files) == 1 and files[0] == path and self.isdir(path_info):
return None
yield from self._strip_buckets(files, detail=detail)
_LOCAL_FS = LocalFileSystem()
class CallbackMixin:
"""Use the native ``get_file()``/``put_file()`` APIs
if the target filesystem supports callbacks."""
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **pbar_args
):
with Tqdm(
desc=name,
disable=no_progress_bar,
bytes=True,
total=-1,
**pbar_args,
) as pbar:
self.fs.put_file(
os.fspath(from_file),
self._with_bucket(to_info),
callback=pbar.as_callback(_LOCAL_FS, from_file),
)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **pbar_args
):
with Tqdm(
desc=name,
disable=no_progress_bar,
bytes=True,
total=-1,
**pbar_args,
) as pbar:
self.fs.get_file(
self._with_bucket(from_info),
os.fspath(to_file),
callback=pbar.as_callback(self, from_info),
)
+33 -364
View File
@@ -1,24 +1,18 @@
import io
import logging
import os
import posixpath
import re
import threading
from collections import defaultdict
from contextlib import contextmanager
from urllib.parse import urlparse
from funcy import cached_property, retry, wrap_prop, wrap_with
from funcy.py3 import cat
from funcy import cached_property, retry, wrap_prop
from dvc.exceptions import DvcException, FileMissingError
from dvc.exceptions import DvcException
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.scheme import Schemes
from dvc.utils import format_link, tmp_fname
from dvc.utils.stream import IterStream
from .base import BaseFileSystem
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
logger = logging.getLogger(__name__)
FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"
@@ -85,7 +79,9 @@ class GDriveURLInfo(CloudURLInfo):
self._spath = re.sub("/{2,}", "/", self._spath.rstrip("/"))
class GDriveFileSystem(BaseFileSystem):
class GDriveFileSystem(
CallbackMixin, FSSpecWrapper
): # pylint:disable=abstract-method
scheme = Schemes.GDRIVE
PATH_CLS = GDriveURLInfo
PARAM_CHECKSUM = "checksum"
@@ -100,8 +96,8 @@ class GDriveFileSystem(BaseFileSystem):
DEFAULT_GDRIVE_CLIENT_ID = "710796635688-iivsgbgsb6uv1fap6635dhvuei09o66c.apps.googleusercontent.com" # noqa: E501
DEFAULT_GDRIVE_CLIENT_SECRET = "a1Fz59uTpVNeG_VGuSKDLJXv"
def __init__(self, repo, config):
super().__init__(repo, config)
def __init__(self, **config):
super().__init__(**config)
self.path_info = self.PATH_CLS(config["url"])
@@ -126,20 +122,26 @@ class GDriveFileSystem(BaseFileSystem):
self._client_id = config.get("gdrive_client_id")
self._client_secret = config.get("gdrive_client_secret")
self._validate_config()
tmp_dir = config["gdrive_credentials_tmp_dir"]
assert tmp_dir
self._gdrive_service_credentials_path = tmp_fname(
os.path.join(self.repo.tmp_dir, "")
os.path.join(tmp_dir, "")
)
self._gdrive_user_credentials_path = (
tmp_fname(os.path.join(self.repo.tmp_dir, ""))
tmp_fname(os.path.join(tmp_dir, ""))
if os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA)
else config.get(
"gdrive_user_credentials_file",
os.path.join(
self.repo.tmp_dir, self.DEFAULT_USER_CREDENTIALS_FILE,
),
os.path.join(tmp_dir, self.DEFAULT_USER_CREDENTIALS_FILE),
)
)
@staticmethod
def _get_kwargs_from_urls(urlpath):
return {"url": urlpath}
def _validate_config(self):
# Validate Service Account configuration
if (
@@ -207,9 +209,9 @@ class GDriveFileSystem(BaseFileSystem):
@wrap_prop(threading.RLock())
@cached_property
def _drive(self):
def fs(self):
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive
from pydrive2.fs import GDriveFileSystem as _GDriveFileSystem
temporary_save_path = self._gdrive_user_credentials_path
is_credentials_temp = os.getenv(
@@ -284,352 +286,19 @@ class GDriveFileSystem(BaseFileSystem):
if is_credentials_temp:
os.remove(temporary_save_path)
return GoogleDrive(gauth)
@wrap_prop(threading.RLock())
@cached_property
def _ids_cache(self):
cache = {
"dirs": defaultdict(list),
"ids": {},
"root_id": self._get_item_id(
self.path_info,
use_cache=False,
hint="Confirm the directory exists and you can access it.",
),
}
self._cache_path_id(self._path, cache["root_id"], cache=cache)
for item in self._gdrive_list(
"'{}' in parents and trashed=false".format(cache["root_id"])
):
item_path = (self.path_info / item["title"]).path
self._cache_path_id(item_path, item["id"], cache=cache)
return cache
def _cache_path_id(self, path, *item_ids, cache=None):
cache = cache or self._ids_cache
for item_id in item_ids:
cache["dirs"][path].append(item_id)
cache["ids"][item_id] = path
@cached_property
def _list_params(self):
params = {"corpora": "default"}
if self._bucket != "root" and self._bucket != "appDataFolder":
drive_id = self._gdrive_shared_drive_id(self._bucket)
if drive_id:
logger.debug(
"GDrive remote '{}' is using shared drive id '{}'.".format(
self.path_info, drive_id
)
)
params["driveId"] = drive_id
params["corpora"] = "drive"
return params
@_gdrive_retry
def _gdrive_shared_drive_id(self, item_id):
from pydrive2.files import ApiRequestError
param = {"id": item_id}
# it does not create a file on the remote
item = self._drive.CreateFile(param)
# ID of the shared drive the item resides in.
# Only populated for items in shared drives.
try:
item.FetchMetadata("driveId")
except ApiRequestError as exc:
error_code = exc.error.get("code", 0)
if error_code == 404:
raise DvcException(
"'{}' for '{}':\n\n"
"1. Confirm the directory exists and you can access it.\n"
"2. Make sure that credentials in '{}'\n"
" are correct for this remote e.g. "
"use the `gdrive_user_credentials_file` config\n"
" option if you use multiple GDrive remotes with "
"different email accounts.\n\nDetails".format(
item_id, self.path_info, self.credentials_location
)
) from exc
raise
return item.get("driveId", None)
@_gdrive_retry
def _gdrive_upload_fobj(self, fobj, parent_id, title):
item = self._drive.CreateFile(
{"title": title, "parents": [{"id": parent_id}]}
)
item.content = fobj
item.Upload()
return item
@_gdrive_retry
def _gdrive_download_file(
self, item_id, to_file, progress_desc, no_progress_bar
):
param = {"id": item_id}
# it does not create a file on the remote
gdrive_file = self._drive.CreateFile(param)
with Tqdm(
desc=progress_desc,
disable=no_progress_bar,
bytes=True,
# explicit `bar_format` as `total` will be set by `update_to`
bar_format=Tqdm.BAR_FMT_DEFAULT,
) as pbar:
gdrive_file.GetContentFile(to_file, callback=pbar.update_to)
@contextmanager
@_gdrive_retry
def open(self, path_info, mode="r", encoding=None, **kwargs):
assert mode in {"r", "rt", "rb"}
item_id = self._get_item_id(path_info)
param = {"id": item_id}
# it does not create a file on the remote
gdrive_file = self._drive.CreateFile(param)
fd = gdrive_file.GetContentIOBuffer()
stream = IterStream(iter(fd))
if mode != "rb":
stream = io.TextIOWrapper(stream, encoding=encoding)
yield stream
@_gdrive_retry
def gdrive_delete_file(self, item_id):
from pydrive2.files import ApiRequestError
param = {"id": item_id}
# it does not create a file on the remote
item = self._drive.CreateFile(param)
try:
item.Trash() if self._trash_only else item.Delete()
except ApiRequestError as exc:
http_error_code = exc.error.get("code", 0)
if (
http_error_code == 403
and self._list_params["corpora"] == "drive"
and exc.GetField("location") == "file.permissions"
):
raise DvcException(
"Insufficient permissions to {}. You should have {} "
"access level for the used shared drive. More details "
"at {}.".format(
"move the file into Trash"
if self._trash_only
else "permanently delete the file",
"Manager or Content Manager"
if self._trash_only
else "Manager",
"https://support.google.com/a/answer/7337554",
)
) from exc
raise
def _gdrive_list(self, query):
param = {"q": query, "maxResults": 1000}
param.update(self._list_params)
file_list = self._drive.ListFile(param)
# Isolate and decorate fetching of remote drive items in pages.
get_list = _gdrive_retry(lambda: next(file_list, None))
# Fetch pages until None is received, lazily flatten the thing.
return cat(iter(get_list, None))
@_gdrive_retry
def _gdrive_create_dir(self, parent_id, title):
parent = {"id": parent_id}
item = self._drive.CreateFile(
{"title": title, "parents": [parent], "mimeType": FOLDER_MIME_TYPE}
)
item.Upload()
return item
@wrap_with(threading.RLock())
def _create_dir(self, parent_id, title, remote_path):
cached = self._ids_cache["dirs"].get(remote_path)
if cached:
return cached[0]
item = self._gdrive_create_dir(parent_id, title)
if parent_id == self._ids_cache["root_id"]:
self._cache_path_id(remote_path, item["id"])
return item["id"]
def _get_remote_item_ids(self, parent_ids, title):
if not parent_ids:
return None
query = "trashed=false and ({})".format(
" or ".join(
f"'{parent_id}' in parents" for parent_id in parent_ids
)
)
query += " and title='{}'".format(title.replace("'", "\\'"))
# GDrive list API is case insensitive, we need to compare
# all results and pick the ones with the right title
return [
item["id"]
for item in self._gdrive_list(query)
if item["title"] == title
]
def _get_cached_item_ids(self, path, use_cache):
if not path:
return [self._bucket]
if use_cache:
return self._ids_cache["dirs"].get(path, [])
return []
def _path_to_item_ids(self, path, create=False, use_cache=True):
item_ids = self._get_cached_item_ids(path, use_cache)
if item_ids:
return item_ids
parent_path, title = posixpath.split(path)
parent_ids = self._path_to_item_ids(parent_path, create, use_cache)
item_ids = self._get_remote_item_ids(parent_ids, title)
if item_ids:
return item_ids
return (
[self._create_dir(min(parent_ids), title, path)] if create else []
return _GDriveFileSystem(
self._with_bucket(self.path_info),
gauth,
trash_only=self._trash_only,
)
def _get_item_id(self, path_info, create=False, use_cache=True, hint=None):
assert path_info.bucket == self._bucket
def _with_bucket(self, path):
if isinstance(path, self.PATH_CLS):
return posixpath.join(path.bucket, path.path)
item_ids = self._path_to_item_ids(path_info.path, create, use_cache)
if item_ids:
return min(item_ids)
return super()._with_bucket(path)
assert not create
raise FileMissingError(path_info, hint)
def exists(self, path_info, use_dvcignore=True):
try:
self._get_item_id(path_info)
except FileMissingError:
return False
else:
return True
def _gdrive_list_ids(self, query_ids):
query = " or ".join(
f"'{query_id}' in parents" for query_id in query_ids
)
query = f"({query}) and trashed=false"
return self._gdrive_list(query)
def find(self, path_info, detail=False):
root_path = path_info.path
seen_paths = set()
dir_ids = [self._ids_cache["ids"].copy()]
while dir_ids:
query_ids = {
dir_id: dir_name
for dir_id, dir_name in dir_ids.pop().items()
if posixpath.commonpath([root_path, dir_name]) == root_path
if dir_id not in seen_paths
}
if not query_ids:
continue
seen_paths |= query_ids.keys()
new_query_ids = {}
dir_ids.append(new_query_ids)
for item in self._gdrive_list_ids(query_ids):
parent_id = item["parents"][0]["id"]
item_path = posixpath.join(query_ids[parent_id], item["title"])
if item["mimeType"] == FOLDER_MIME_TYPE:
new_query_ids[item["id"]] = item_path
self._cache_path_id(item_path, item["id"])
continue
if detail:
yield {
"type": "file",
"name": item_path,
"size": item["fileSize"],
"checksum": item["md5Checksum"],
}
else:
yield item_path
def ls(self, path_info, detail=False):
cached = path_info.path in self._ids_cache["dirs"]
if cached:
dir_ids = self._ids_cache["dirs"][path_info.path]
else:
dir_ids = self._path_to_item_ids(path_info.path)
if not dir_ids:
return None
root_path = path_info.path
for item in self._gdrive_list_ids(dir_ids):
item_path = posixpath.join(root_path, item["title"])
if detail:
if item["mimeType"] == FOLDER_MIME_TYPE:
yield {"type": "directory", "name": item_path}
else:
yield {
"type": "file",
"name": item_path,
"size": item["fileSize"],
"checksum": item["md5Checksum"],
}
else:
yield item_path
if not cached:
self._cache_path_id(root_path, *dir_ids)
def walk_files(self, path_info, **kwargs):
for file in self.find(path_info):
yield path_info.replace(path=file)
def remove(self, path_info):
item_id = self._get_item_id(path_info)
self.gdrive_delete_file(item_id)
def info(self, path_info):
item_id = self._get_item_id(path_info)
gdrive_file = self._drive.CreateFile({"id": item_id})
gdrive_file.FetchMetadata(fields="fileSize")
return {"size": gdrive_file.get("fileSize")}
def _upload_fobj(self, fobj, to_info):
dirname = to_info.parent
assert dirname
parent_id = self._get_item_id(dirname, create=True)
self._gdrive_upload_fobj(fobj, parent_id, to_info.name)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
with open(from_file, "rb") as fobj:
self.upload_fobj(
fobj,
to_info,
no_progress_bar=no_progress_bar,
desc=name or to_info.name,
total=os.path.getsize(from_file),
)
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
item_id = self._get_item_id(from_info)
self._gdrive_download_file(item_id, to_file, name, no_progress_bar)
def _upload_fobj(self, fobj, to_info, size: int = None):
rpath = self._with_bucket(to_info)
self.makedirs(os.path.dirname(rpath))
return self.fs.upload_fobj(fobj, rpath, size=size)
+42 -68
View File
@@ -1,7 +1,6 @@
import errno
import os
from funcy import cached_property
import stat
from dvc.utils import is_exec, relpath
@@ -11,46 +10,34 @@ from .base import BaseFileSystem
class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
"""Proxies the repo file access methods to Git objects"""
def __init__(
self, root_dir, trie, use_dvcignore=False, dvcignore_root=None
):
super().__init__(None, {})
self._fs_root = root_dir
sep = os.sep
scheme = "local"
def __init__(self, root_dir, trie):
super().__init__()
self._root = root_dir
self.trie = trie
self.use_dvcignore = use_dvcignore
self.dvcignore_root = dvcignore_root
@property
def rev(self):
return self.trie.rev
@property
def fs_root(self):
return self._fs_root
def _get_key(self, path):
if isinstance(path, str):
if not os.path.isabs(path):
relparts = path.split(os.sep)
else:
relparts = relpath(path, self.fs_root).split(os.sep)
relparts = relpath(path, self._root).split(os.sep)
else:
relparts = path.relative_to(self.fs_root).parts
relparts = path.relative_to(self._root).parts
if relparts == ["."]:
return ()
return tuple(relparts)
@cached_property
def dvcignore(self):
from dvc.ignore import DvcIgnoreFilter, DvcIgnoreFilterNoop
root = self.dvcignore_root or self.fs_root
cls = DvcIgnoreFilter if self.use_dvcignore else DvcIgnoreFilterNoop
return cls(self, root)
def open(
self, path, mode="r", encoding=None
): # pylint: disable=arguments-differ
self, path, mode="r", encoding=None, **kwargs
): # pylint: disable=arguments-renamed
# NOTE: this is incorrect, we need to use locale to determine default
# encoding.
encoding = encoding or "utf-8"
@@ -66,50 +53,27 @@ class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
errno.EISDIR, os.strerror(errno.EISDIR), path
) from exc
def exists(
self, path, use_dvcignore=True
): # pylint: disable=arguments-differ
def _is_ignored(path):
return self.dvcignore.is_ignored_file(
path
) or self.dvcignore.is_ignored_dir(path)
if use_dvcignore and _is_ignored(path):
return False
key = self._get_key(path)
def exists(self, path_info) -> bool:
key = self._get_key(path_info)
return self.trie.exists(key)
def isdir(
self, path, use_dvcignore=True
): # pylint: disable=arguments-differ
if use_dvcignore and self.dvcignore.is_ignored_dir(path):
return False
key = self._get_key(path)
def isdir(self, path_info) -> bool:
key = self._get_key(path_info)
return self.trie.isdir(key)
def isfile(self, path): # pylint: disable=arguments-differ
if self.dvcignore.is_ignored_file(path):
return False
key = self._get_key(path)
def isfile(self, path_info) -> bool:
key = self._get_key(path_info)
return self.trie.isfile(key)
def walk(
self,
top,
topdown=True,
onerror=None,
use_dvcignore=True,
ignore_subrepos=True,
):
def walk(self, top, topdown=True, onerror=None, **kwargs):
"""Directory tree generator.
See `os.walk` for the docs. Differences:
- no support for symlinks
"""
if not self.isdir(top, use_dvcignore=use_dvcignore):
if not self.isdir(top):
if onerror:
if self.exists(top, use_dvcignore=use_dvcignore):
if self.exists(top):
exc = NotADirectoryError(
errno.ENOTDIR, os.strerror(errno.ENOTDIR), top
)
@@ -123,13 +87,9 @@ class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
key = self._get_key(top)
for prefix, dirs, files in self.trie.walk(key, topdown=topdown):
if prefix:
root = os.path.join(self.fs_root, os.sep.join(prefix))
root = os.path.join(self._root, os.sep.join(prefix))
else:
root = self.fs_root
if use_dvcignore:
dirs[:], files[:] = self.dvcignore(
root, dirs, files, ignore_subrepos=ignore_subrepos,
)
root = self._root
yield root, dirs, files
def isexec(self, path_info):
@@ -147,7 +107,10 @@ class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
raise FileNotFoundError(
errno.ENOENT, os.strerror(errno.ENOENT), path_info
)
return {"size": st.st_size}
return {
"size": st.st_size,
"type": "directory" if stat.S_ISDIR(st.st_mode) else "file",
}
def stat(self, path):
key = self._get_key(path)
@@ -158,11 +121,22 @@ class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
errno.ENOENT, os.strerror(errno.ENOENT), path
)
def walk_files(self, top): # pylint: disable=arguments-differ
for root, _, files in self.walk(top):
def walk_files(self, path_info, **kwargs):
for root, _, files in self.walk(path_info, **kwargs):
for file in files:
# NOTE: os.path.join is ~5.5 times slower
yield f"{root}{os.sep}{file}"
def _reset(self):
return self.__dict__.pop("dvcignore", None)
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **kwargs
):
import shutil
from dvc.progress import Tqdm
with open(to_file, "wb+") as to_fobj:
with Tqdm.wrapattr(
to_fobj, "write", desc=name, disable=no_progress_bar
) as wrapped:
with self.open(from_info, "rb", **kwargs) as from_fobj:
shutil.copyfileobj(from_fobj, wrapped)
+5 -9
View File
@@ -6,23 +6,19 @@ from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
from .fsspec_wrapper import FSSpecWrapper
from .fsspec_wrapper import ObjectFSWrapper
class GSFileSystem(FSSpecWrapper):
# pylint:disable=abstract-method
class GSFileSystem(ObjectFSWrapper):
scheme = Schemes.GS
PATH_CLS = CloudURLInfo
REQUIRES = {"gcsfs": "gcsfs"}
PARAM_CHECKSUM = "etag"
DETAIL_FIELDS = frozenset(("etag", "size"))
TRAVERSE_PREFIX_LEN = 2
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url", "gs://")
self.path_info = self.PATH_CLS(url)
def _prepare_credentials(self, config):
def _prepare_credentials(self, **config):
login_info = {"consistency": None}
login_info["project"] = config.get("projectname")
login_info["token"] = config.get("credentialpath")
+34 -28
View File
@@ -1,3 +1,4 @@
import errno
import io
import logging
import os
@@ -6,7 +7,6 @@ import shutil
import subprocess
from collections import deque
from contextlib import closing, contextmanager
from urllib.parse import urlparse
from dvc.hash_info import HashInfo
from dvc.progress import Tqdm
@@ -65,27 +65,22 @@ class HDFSFileSystem(BaseFileSystem):
PARAM_CHECKSUM = "checksum"
TRAVERSE_PREFIX_LEN = 2
def __init__(self, repo, config):
super().__init__(repo, config)
def __init__(self, **config):
super().__init__(**config)
self.path_info = None
url = config.get("url")
if not url:
return
parsed = urlparse(url)
user = parsed.username or config.get("user")
self.path_info = self.PATH_CLS.from_parts(
scheme=self.scheme,
host=parsed.hostname,
user=user,
port=parsed.port,
path=parsed.path,
)
self.host = config["host"]
self.user = config.get("user")
self.port = config.get("port")
self.kerb_ticket = config.get("kerb_ticket")
@staticmethod
def _get_kwargs_from_urls(urlpath):
from fsspec.implementations.hdfs import PyArrowHDFS
# pylint:disable=protected-access
return PyArrowHDFS._get_kwargs_from_urls(urlpath)
def hdfs(self, path_info):
import pyarrow.fs
@@ -123,7 +118,7 @@ class HDFSFileSystem(BaseFileSystem):
raise FileNotFoundError(*e.args)
raise
def exists(self, path_info, use_dvcignore=True):
def exists(self, path_info) -> bool:
assert not isinstance(path_info, list)
assert path_info.scheme == "hdfs"
with self.hdfs(path_info) as hdfs:
@@ -159,15 +154,15 @@ class HDFSFileSystem(BaseFileSystem):
if not topdown:
yield root, dirs, nondirs
def walk(self, path_info, **kwargs):
if not self.isdir(path_info):
def walk(self, top, topdown=True, onerror=None, **kwargs):
if not self.isdir(top):
return
with self.hdfs(path_info) as hdfs:
with self.hdfs(top) as hdfs:
for root, dnames, fnames in self._walk(
hdfs, path_info.path, **kwargs
hdfs, top.path, topdown=topdown
):
yield path_info.replace(path=root), dnames, fnames
yield top.replace(path=root), dnames, fnames
def walk_files(self, path_info, **kwargs):
for root, _, fnames in self.walk(path_info):
@@ -188,7 +183,7 @@ class HDFSFileSystem(BaseFileSystem):
else:
hdfs.delete_file(path_info.path)
def makedirs(self, path_info):
def makedirs(self, path_info, **kwargs):
with self.hdfs(path_info) as hdfs:
# NOTE: fs.create_dir creates parents by default
hdfs.create_dir(path_info.path)
@@ -226,8 +221,19 @@ class HDFSFileSystem(BaseFileSystem):
def info(self, path_info):
with self.hdfs(path_info) as hdfs:
finfo = hdfs.get_file_info(path_info.path)
return {"size": finfo.size}
from pyarrow.fs import FileType
file_info = hdfs.get_file_info(path_info.path)
if file_info.type is FileType.Directory:
kind = "directory"
elif file_info.type is FileType.File:
kind = "file"
else:
raise FileNotFoundError(
errno.ENOENT, os.strerror(errno.ENOENT), path_info
)
return {"size": file_info.size, "type": kind}
def checksum(self, path_info):
return HashInfo(
@@ -236,7 +242,7 @@ class HDFSFileSystem(BaseFileSystem):
size=self.getsize(path_info),
)
def _upload_fobj(self, fobj, to_info):
def _upload_fobj(self, fobj, to_info, **kwargs):
with self.hdfs(to_info) as hdfs:
with hdfs.open_output_stream(to_info.path) as fdest:
shutil.copyfileobj(fobj, fdest)
+13 -17
View File
@@ -2,10 +2,11 @@ import logging
import os.path
import threading
from typing import Optional
from urllib.parse import urlparse
from funcy import cached_property, memoize, wrap_prop, wrap_with
import dvc.prompt as prompt
from dvc import prompt
from dvc.exceptions import DvcException, HTTPError
from dvc.path_info import HTTPURLInfo
from dvc.progress import Tqdm
@@ -30,22 +31,17 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
PATH_CLS = HTTPURLInfo
PARAM_CHECKSUM = "etag"
CAN_TRAVERSE = False
REQUIRES = {"requests": "requests"}
SESSION_RETRIES = 5
SESSION_BACKOFF_FACTOR = 0.1
REQUEST_TIMEOUT = 60
CHUNK_SIZE = 2 ** 16
def __init__(self, repo, config):
super().__init__(repo, config)
def __init__(self, **config):
super().__init__(**config)
url = config.get("url")
if url:
self.path_info = self.PATH_CLS(url)
self.user = config.get("user", None)
self.host = self.path_info.host
else:
self.path_info = None
self.user = config.get("user", None)
self.auth = config.get("auth", None)
self.custom_auth_header = config.get("custom_auth_header", None)
@@ -55,12 +51,12 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
self.ssl_verify = config.get("ssl_verify", True)
self.method = config.get("method", "POST")
def _auth_method(self):
def _auth_method(self, url):
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
if self.auth:
if self.ask_password and self.password is None:
self.password = ask_password(self.host, self.user)
self.password = ask_password(urlparse(url).hostname, self.user)
if self.auth == "basic":
return HTTPBasicAuth(self.user, self.password)
if self.auth == "digest":
@@ -103,7 +99,7 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
res = self._session.request(
method,
url,
auth=self._auth_method(),
auth=self._auth_method(url),
headers=self.headers,
**kwargs,
)
@@ -138,7 +134,7 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
return response
def exists(self, path_info, use_dvcignore=True):
def exists(self, path_info) -> bool:
res = self._head(path_info.url)
if res.status_code == 404:
return False
@@ -150,9 +146,9 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
resp = self._head(path_info.url)
etag = resp.headers.get("ETag") or resp.headers.get("Content-MD5")
size = self._content_length(resp)
return {"etag": etag, "size": size}
return {"etag": etag, "size": size, "type": "file"}
def _upload_fobj(self, fobj, to_info):
def _upload_fobj(self, fobj, to_info, **kwargs):
def chunks(fobj):
while True:
chunk = fobj.read(self.CHUNK_SIZE)
@@ -189,9 +185,9 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
self.upload_fobj(
fobj,
to_info,
size=None if no_progress_bar else os.path.getsize(from_file),
no_progress_bar=no_progress_bar,
desc=name or to_info.url,
total=None if no_progress_bar else os.path.getsize(from_file),
)
@staticmethod
+21 -71
View File
@@ -2,8 +2,6 @@ import logging
import os
import stat
from funcy import cached_property
from dvc.path_info import PathInfo
from dvc.scheme import Schemes
from dvc.system import System
@@ -16,76 +14,40 @@ logger = logging.getLogger(__name__)
class LocalFileSystem(BaseFileSystem):
sep = os.sep
scheme = Schemes.LOCAL
PATH_CLS = PathInfo
PARAM_CHECKSUM = "md5"
PARAM_PATH = "path"
TRAVERSE_PREFIX_LEN = 2
def __init__(self, repo, config, use_dvcignore=False, dvcignore_root=None):
super().__init__(repo, config)
url = config.get("url")
self.path_info = self.PATH_CLS(url) if url else None
self.use_dvcignore = use_dvcignore
self.dvcignore_root = dvcignore_root
def __init__(self, **config):
from fsspec.implementations.local import LocalFileSystem as LocalFS
@property
def fs_root(self):
return self.config.get("url")
@cached_property
def dvcignore(self):
from dvc.ignore import DvcIgnoreFilter, DvcIgnoreFilterNoop
root = self.dvcignore_root or self.fs_root
cls = DvcIgnoreFilter if self.use_dvcignore else DvcIgnoreFilterNoop
return cls(self, root)
super().__init__(**config)
self.fs = LocalFS()
@staticmethod
def open(path_info, mode="r", encoding=None, **kwargs):
return open(path_info, mode=mode, encoding=encoding)
def exists(self, path_info, use_dvcignore=True):
def exists(self, path_info) -> bool:
assert isinstance(path_info, str) or path_info.scheme == "local"
if self.repo:
ret = os.path.lexists(path_info)
else:
ret = os.path.exists(path_info)
if not ret:
return False
if not use_dvcignore:
return True
return self.fs.exists(path_info)
return not self.dvcignore.is_ignored_file(
path_info
) and not self.dvcignore.is_ignored_dir(path_info)
def isfile(self, path_info) -> bool:
return os.path.isfile(path_info)
def isfile(self, path_info):
if not os.path.isfile(path_info):
return False
return not self.dvcignore.is_ignored_file(path_info)
def isdir(
self, path_info, use_dvcignore=True
): # pylint: disable=arguments-differ
if not os.path.isdir(path_info):
return False
return not (use_dvcignore and self.dvcignore.is_ignored_dir(path_info))
def isdir(self, path_info) -> bool:
return os.path.isdir(path_info)
def iscopy(self, path_info):
return not (
System.is_symlink(path_info) or System.is_hardlink(path_info)
)
def walk(
self,
top,
topdown=True,
onerror=None,
use_dvcignore=True,
ignore_subrepos=True,
):
def walk(self, top, topdown=True, onerror=None, **kwargs):
"""Directory fs generator.
See `os.walk` for the docs. Differences:
@@ -94,14 +56,6 @@ class LocalFileSystem(BaseFileSystem):
for root, dirs, files in os.walk(
top, topdown=topdown, onerror=onerror
):
if use_dvcignore:
dirs[:], files[:] = self.dvcignore(
os.path.abspath(root),
dirs,
files,
ignore_subrepos=ignore_subrepos,
)
yield os.path.normpath(root), dirs, files
def walk_files(self, path_info, **kwargs):
@@ -125,21 +79,20 @@ class LocalFileSystem(BaseFileSystem):
raise NotImplementedError
remove(path_info)
def makedirs(self, path_info):
makedirs(path_info, exist_ok=True)
def makedirs(self, path_info, **kwargs):
makedirs(path_info, exist_ok=kwargs.pop("exist_ok", True))
def isexec(self, path_info):
mode = self.stat(path_info).st_mode
return is_exec(mode)
def stat(self, path):
if self.dvcignore.is_ignored(path):
raise FileNotFoundError
return os.stat(path)
def move(self, from_info, to_info):
if from_info.scheme != "local" or to_info.scheme != "local":
if (
isinstance(from_info, PathInfo) and from_info.scheme != "local"
) or (isinstance(to_info, PathInfo) and to_info.scheme != "local"):
raise NotImplementedError
self.makedirs(to_info.parent)
@@ -154,7 +107,7 @@ class LocalFileSystem(BaseFileSystem):
self.remove(tmp_info)
raise
def _upload_fobj(self, fobj, to_info):
def _upload_fobj(self, fobj, to_info, **kwargs):
self.makedirs(to_info.parent)
tmp_info = to_info.parent / tmp_fname("")
try:
@@ -206,11 +159,11 @@ class LocalFileSystem(BaseFileSystem):
st = os.stat(path_info)
return {
"size": st.st_size,
"type": "dir" if stat.S_ISDIR(st.st_mode) else "file",
"type": "directory" if stat.S_ISDIR(st.st_mode) else "file",
}
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs,
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
makedirs(to_info.parent, exist_ok=True)
@@ -227,6 +180,3 @@ class LocalFileSystem(BaseFileSystem):
copyfile(
from_info, to_file, no_progress_bar=no_progress_bar, name=name
)
def _reset(self):
return self.__dict__.pop("dvcignore", None)
+28 -27
View File
@@ -1,34 +1,35 @@
from .base import BaseFileSystem
import threading
from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.scheme import Schemes
from .fsspec_wrapper import FSSpecWrapper
class MemoryFileSystem(BaseFileSystem):
scheme = "local"
class MemoryFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
scheme = Schemes.MEMORY
PARAM_CHECKSUM = "md5"
PATH_CLS = CloudURLInfo
TRAVERSE_PREFIX_LEN = 2
DEFAULT_BLOCKSIZE = 4096
def __init__(self, repo, config):
def __eq__(self, other):
# NOTE: all fsspec MemoryFileSystem instances are equivalent and use a
# single global store
return isinstance(other, type(self))
__hash__ = FSSpecWrapper.__hash__
@wrap_prop(threading.Lock())
@cached_property
def fs(self):
from fsspec.implementations.memory import MemoryFileSystem as MemFS
super().__init__(repo, config)
return MemFS(**self.fs_args)
self.fs = MemFS()
def exists(self, path_info, use_dvcignore=True):
return self.fs.exists(path_info.fspath)
def open(self, path_info, mode="r", encoding=None, **kwargs):
return self.fs.open(
path_info.fspath, mode=mode, encoding=encoding, **kwargs
)
def info(self, path_info):
return self.fs.info(path_info.fspath)
def stat(self, path_info):
import os
info = self.fs.info(path_info.fspath)
return os.stat_result((0, 0, 0, 0, 0, 0, info["size"], 0, 0, 0))
def walk_files(self, path_info, **kwargs):
raise NotImplementedError
def open(self, *args, **kwargs):
with super().open(*args, **kwargs) as fobj:
fobj.blocksize = self.DEFAULT_BLOCKSIZE
return fobj
+16 -116
View File
@@ -5,137 +5,37 @@ import threading
from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.scheme import Schemes
from .base import BaseFileSystem
from .fsspec_wrapper import CallbackMixin, ObjectFSWrapper
logger = logging.getLogger(__name__)
class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
"""
oss2 document:
https://www.alibabacloud.com/help/doc-detail/32026.htm
Examples
----------
$ dvc remote add myremote oss://my-bucket/path
Set key id, key secret and endpoint using modify command
$ dvc remote modify myremote oss_key_id my-key-id
$ dvc remote modify myremote oss_key_secret my-key-secret
$ dvc remote modify myremote oss_endpoint endpoint
or environment variables
$ export OSS_ACCESS_KEY_ID="my-key-id"
$ export OSS_ACCESS_KEY_SECRET="my-key-secret"
$ export OSS_ENDPOINT="endpoint"
"""
# pylint:disable=abstract-method
class OSSFileSystem(CallbackMixin, ObjectFSWrapper):
scheme = Schemes.OSS
PATH_CLS = CloudURLInfo
REQUIRES = {"oss2": "oss2"}
REQUIRES = {"ossfs": "ossfs"}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
LIST_OBJECT_PAGE_SIZE = 100
DETAIL_FIELDS = frozenset(("etag", "size"))
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url")
self.path_info = self.PATH_CLS(url) if url else None
self.bucket = self.path_info.bucket
self.endpoint = config.get("oss_endpoint") or os.getenv("OSS_ENDPOINT")
self.key_id = (
config.get("oss_key_id")
or os.getenv("OSS_ACCESS_KEY_ID")
or "defaultId"
def _prepare_credentials(self, **config):
login_info = {}
login_info["key"] = config.get("oss_key_id") or os.getenv(
"OSS_ACCESS_KEY_ID"
)
self.key_secret = (
config.get("oss_key_secret")
or os.getenv("OSS_ACCESS_KEY_SECRET")
or "defaultSecret"
login_info["secret"] = config.get("oss_key_secret") or os.getenv(
"OSS_ACCESS_KEY_SECRET"
)
login_info["endpoint"] = config.get("oss_endpoint")
return login_info
@wrap_prop(threading.Lock())
@cached_property
def oss_service(self):
import oss2
def fs(self):
from ossfs import OSSFileSystem as _OSSFileSystem
logger.debug(f"key id: {self.key_id}")
logger.debug(f"key secret: {self.key_secret}")
auth = oss2.Auth(self.key_id, self.key_secret)
bucket = oss2.Bucket(auth, self.endpoint, self.bucket)
# Ensure bucket exists
try:
bucket.get_bucket_info()
except oss2.exceptions.NoSuchBucket:
bucket.create_bucket(
oss2.BUCKET_ACL_PUBLIC_READ,
oss2.models.BucketCreateConfig(
oss2.BUCKET_STORAGE_CLASS_STANDARD
),
)
return bucket
def _generate_download_url(self, path_info, expires=3600):
assert path_info.bucket == self.bucket
return self.oss_service.sign_url("GET", path_info.path, expires)
def exists(self, path_info, use_dvcignore=True):
paths = self._list_paths(path_info)
return any(path_info.path == path for path in paths)
def _list_paths(self, path_info):
import oss2
for blob in oss2.ObjectIterator(
self.oss_service, prefix=path_info.path
):
yield blob.key
def walk_files(self, path_info, **kwargs):
if not kwargs.pop("prefix", False):
path_info = path_info / ""
for fname in self._list_paths(path_info):
if fname.endswith("/"):
continue
yield path_info.replace(path=fname)
def remove(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
logger.debug(f"Removing oss://{path_info}")
self.oss_service.delete_object(path_info.path)
def _upload_fobj(self, fobj, to_info):
self.oss_service.put_object(to_info.path, fobj)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
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
):
with Tqdm(desc=name, disable=no_progress_bar, bytes=True) as pbar:
import oss2
oss2.resumable_download(
self.oss_service,
from_info.path,
to_file,
progress_callback=pbar.update_to,
)
return _OSSFileSystem(**self.fs_args)
+148 -69
View File
@@ -31,16 +31,33 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
kwargs: Additional keyword arguments passed to the `DvcFileSystem()`.
"""
sep = os.sep
scheme = "local"
PARAM_CHECKSUM = "md5"
PARAM_REPO_URL = "repo_url"
PARAM_REPO_ROOT = "repo_root"
PARAM_REV = "rev"
PARAM_CACHE_DIR = "cache_dir"
PARAM_CACHE_TYPES = "cache_types"
PARAM_SUBREPOS = "subrepos"
def __init__(
self, repo, subrepos=False, repo_factory: RepoFactory = None,
self,
repo: Optional["Repo"] = None,
subrepos=False,
repo_factory: RepoFactory = None,
**kwargs,
):
super().__init__(repo, {"url": repo.root_dir})
super().__init__()
from dvc.utils.collections import PathStringTrie
if repo is None:
repo, repo_factory = self._repo_from_fs_config(
subrepos=subrepos, **kwargs
)
if not repo_factory:
from dvc.repo import Repo
@@ -49,6 +66,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
self.repo_factory = repo_factory
self._main_repo = repo
self.hash_jobs = repo.fs.hash_jobs
self.root_dir = repo.root_dir
self._traverse_subrepos = subrepos
@@ -61,9 +79,76 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
"""Keep a dvcfs instance of each repo."""
if hasattr(repo, "dvc_dir"):
self._dvcfss[repo.root_dir] = DvcFileSystem(repo)
self._dvcfss[repo.root_dir] = DvcFileSystem(repo=repo)
def _get_repo(self, path) -> Optional["Repo"]:
@property
def repo_url(self):
if self._main_repo is None:
return None
return self._main_repo.url
@property
def config(self):
return {
self.PARAM_REPO_URL: self.repo_url,
self.PARAM_REPO_ROOT: self.root_dir,
self.PARAM_REV: getattr(self._main_repo.fs, "rev", None),
self.PARAM_CACHE_DIR: os.path.abspath(
self._main_repo.odb.local.cache_dir
),
self.PARAM_CACHE_TYPES: self._main_repo.odb.local.cache_types,
self.PARAM_SUBREPOS: self._traverse_subrepos,
}
@classmethod
def _repo_from_fs_config(
cls, **config
) -> Tuple["Repo", Optional["RepoFactory"]]:
from dvc.external_repo import erepo_factory, external_repo
from dvc.repo import Repo
url = config.get(cls.PARAM_REPO_URL)
root = config.get(cls.PARAM_REPO_ROOT)
assert url or root
def _open(*args, **kwargs):
# NOTE: if original repo was an erepo (and has a URL),
# we cannot use Repo.open() since it will skip erepo
# cache/remote setup for local URLs
if url is None:
return Repo.open(*args, **kwargs)
return external_repo(*args, **kwargs)
cache_dir = config.get(cls.PARAM_CACHE_DIR)
cache_config = (
{}
if not cache_dir
else {
"cache": {
"dir": cache_dir,
"type": config.get(cls.PARAM_CACHE_TYPES),
}
}
)
repo_kwargs: dict = {
"rev": config.get(cls.PARAM_REV),
"subrepos": config.get(cls.PARAM_SUBREPOS, False),
"uninitialized": True,
}
factory: Optional["RepoFactory"] = None
if url is None:
repo_kwargs["config"] = cache_config
else:
repo_kwargs["cache_dir"] = cache_dir
factory = erepo_factory(url, cache_config)
with _open(
url if url else root,
**repo_kwargs,
) as repo:
return repo, factory
def _get_repo(self, path: str) -> Optional["Repo"]:
"""Returns repo that the path falls in, using prefix.
If the path is already tracked/collected, it just returns the repo.
@@ -93,11 +178,11 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
if self._is_dvc_repo(d):
repo = self.repo_factory(
d,
scm=self.repo.scm,
rev=self.repo.get_rev(),
scm=self._main_repo.scm,
rev=self._main_repo.get_rev(),
repo_factory=self.repo_factory,
)
self._dvcfss[repo.root_dir] = DvcFileSystem(repo)
self._dvcfss[repo.root_dir] = DvcFileSystem(repo=repo)
self._subrepos_trie[d] = repo
def _is_dvc_repo(self, dir_path):
@@ -108,8 +193,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
from dvc.repo import Repo
repo_path = os.path.join(dir_path, Repo.DVC_DIR)
# dvcignore will ignore subrepos, therefore using `use_dvcignore=False`
return self._main_repo.fs.isdir(repo_path, use_dvcignore=False)
return self._main_repo.fs.isdir(repo_path)
def _get_fs_pair(
self, path
@@ -128,7 +212,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
def open(
self, path, mode="r", encoding="utf-8", **kwargs
): # pylint: disable=arguments-differ
): # pylint: disable=arguments-renamed
if "b" in mode:
encoding = None
@@ -142,31 +226,35 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
return dvc_fs.open(path_info, mode=mode, encoding=encoding, **kwargs)
def exists(
self, path, use_dvcignore=True
): # pylint: disable=arguments-differ
fs, dvc_fs = self._get_fs_pair(path)
def exists(self, path_info) -> bool:
fs, dvc_fs = self._get_fs_pair(path_info)
if not dvc_fs:
return fs.exists(path)
return fs.exists(path_info)
if fs.exists(path):
if dvc_fs.repo.dvcignore.is_ignored(fs, path_info):
return False
if fs.exists(path_info):
return True
try:
meta = dvc_fs.metadata(path)
meta = dvc_fs.metadata(path_info)
except FileNotFoundError:
return False
(out,) = meta.outs
assert len(meta.outs) == 1
if fs.exists(out.path_info):
return False
for out in meta.outs:
if fs.exists(out.path_info):
return False
return True
def isdir(self, path): # pylint: disable=arguments-differ
def isdir(self, path): # pylint: disable=arguments-renamed
fs, dvc_fs = self._get_fs_pair(path)
if dvc_fs and dvc_fs.repo.dvcignore.is_ignored_dir(path):
return False
try:
st = fs.stat(path)
return stat.S_ISDIR(st.st_mode)
@@ -182,19 +270,22 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
except FileNotFoundError:
return False
(out,) = meta.outs
assert len(meta.outs) == 1
if fs.exists(out.path_info):
return False
for out in meta.outs:
if fs.exists(out.path_info):
return False
return meta.isdir
def isdvc(self, path, **kwargs):
_, dvc_fs = self._get_fs_pair(path)
return dvc_fs is not None and dvc_fs.isdvc(path, **kwargs)
def isfile(self, path): # pylint: disable=arguments-differ
def isfile(self, path): # pylint: disable=arguments-renamed
fs, dvc_fs = self._get_fs_pair(path)
if dvc_fs and dvc_fs.repo.dvcignore.is_ignored_file(path):
return False
try:
st = fs.stat(path)
return stat.S_ISREG(st.st_mode)
@@ -238,13 +329,11 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
def _subrepo_walk(self, dir_path, **kwargs):
"""Walk into a new repo.
NOTE: subrepo will only be discovered when walking if
ignore_subrepos is set to False.
NOTE: subrepo will only be discovered when walking if
ignore_subrepos is set to False.
"""
fs, dvc_fs = self._get_fs_pair(dir_path)
fs_walk = fs.walk(
dir_path, topdown=True, ignore_subrepos=not self._traverse_subrepos
)
fs_walk = fs.walk(dir_path, topdown=True)
if dvc_fs:
dvc_walk = dvc_fs.walk(dir_path, topdown=True, **kwargs)
else:
@@ -306,15 +395,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
elif dirname in repo_set:
yield from self._walk(repo_walk, None, dvcfiles=dvcfiles)
def walk(
self,
top,
topdown=True,
onerror=None,
dvcfiles=False,
follow_subrepos=None,
**kwargs
): # pylint: disable=arguments-differ
def walk(self, top, topdown=True, onerror=None, **kwargs):
"""Walk and merge both DVC and repo fss.
Args:
@@ -340,17 +421,14 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
onerror(NotADirectoryError(top))
return
ignore_subrepos = not self._traverse_subrepos
if follow_subrepos is not None:
ignore_subrepos = not follow_subrepos
repo = self._get_repo(os.path.abspath(top))
dvcfiles = kwargs.pop("dvcfiles", False)
fs, dvc_fs = self._get_fs_pair(top)
repo_exists = fs.exists(top)
repo_walk = fs.walk(
top,
topdown=topdown,
onerror=onerror,
ignore_subrepos=ignore_subrepos,
repo_walk = repo.dvcignore.walk(
fs, top, topdown=topdown, onerror=onerror, **kwargs
)
if not dvc_fs or (repo_exists and dvc_fs.isdvc(top)):
@@ -358,36 +436,37 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
return
if not repo_exists:
yield from dvc_fs.walk(top, topdown=topdown, **kwargs)
yield from dvc_fs.walk(
top, topdown=topdown, onerror=onerror, **kwargs
)
dvc_walk = None
if dvc_fs.exists(top):
dvc_walk = dvc_fs.walk(top, topdown=topdown, **kwargs)
dvc_walk = dvc_fs.walk(
top, topdown=topdown, onerror=onerror, **kwargs
)
yield from self._walk(repo_walk, dvc_walk, dvcfiles=dvcfiles)
def walk_files(self, top, **kwargs): # pylint: disable=arguments-differ
for root, _, files in self.walk(top, **kwargs):
def walk_files(self, path_info, **kwargs):
for root, _, files in self.walk(path_info, **kwargs):
for fname in files:
yield PathInfo(root) / fname
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **kwargs
):
import shutil
def _download(self, from_info, to_file, **kwargs):
fs, dvc_fs = self._get_fs_pair(from_info)
try:
fs._download( # pylint: disable=protected-access
from_info, to_file, **kwargs
)
return
except FileNotFoundError:
if not dvc_fs:
raise
from dvc.progress import Tqdm
with open(to_file, "wb+") as to_fobj:
with Tqdm.wrapattr(
to_fobj, "write", desc=name, disable=no_progress_bar,
) as wrapped:
with self.open(from_info, "rb", **kwargs) as from_fobj:
shutil.copyfileobj(from_fobj, wrapped)
@property
def hash_jobs(self): # pylint: disable=invalid-overridden-method
return self._main_repo.fs.hash_jobs
dvc_fs._download( # pylint: disable=protected-access
from_info, to_file, **kwargs
)
def metadata(self, path):
abspath = os.path.abspath(path)
+158 -387
View File
@@ -1,75 +1,34 @@
import logging
import functools
import os
import threading
from contextlib import contextmanager
from collections import defaultdict
from funcy import cached_property, wrap_prop
from dvc.config import ConfigError
from dvc.exceptions import DvcException, ETagMismatchError
from dvc.hash_info import HashInfo
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.scheme import Schemes
from dvc.utils import conversions, error_link
from .base import BaseFileSystem
logger = logging.getLogger(__name__)
from .fsspec_wrapper import ObjectFSWrapper
_AWS_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".aws", "config")
class S3FileSystem(BaseFileSystem):
# pylint:disable=abstract-method
class BaseS3FileSystem(ObjectFSWrapper):
scheme = Schemes.S3
PATH_CLS = CloudURLInfo
REQUIRES = {"boto3": "boto3"}
REQUIRES = {"s3fs": "s3fs", "boto3": "boto3"}
PARAM_CHECKSUM = "etag"
DETAIL_FIELDS = frozenset(("etag", "size"))
def __init__(self, repo, config):
super().__init__(repo, config)
_GRANTS = {
"grant_full_control": "GrantFullControl",
"grant_read": "GrantRead",
"grant_read_acp": "GrantReadACP",
"grant_write_acp": "GrantWriteACP",
}
url = config.get("url", "s3://")
self.path_info = self.PATH_CLS(url)
self.region = config.get("region")
self.profile = config.get("profile")
self.endpoint_url = config.get("endpointurl")
self.use_ssl = config.get("use_ssl", True)
self.ssl_verify = config.get("ssl_verify", True)
self.extra_args = {}
self.sse = config.get("sse")
if self.sse:
self.extra_args["ServerSideEncryption"] = self.sse
self.sse_kms_key_id = config.get("sse_kms_key_id")
if self.sse_kms_key_id:
self.extra_args["SSEKMSKeyId"] = self.sse_kms_key_id
self.acl = config.get("acl")
if self.acl:
self.extra_args["ACL"] = self.acl
self._append_aws_grants_to_extra_args(config)
self.access_key_id = config.get("access_key_id")
self.secret_access_key = config.get("secret_access_key")
self.session_token = config.get("session_token")
shared_creds = config.get("credentialpath")
if shared_creds:
os.environ.setdefault("AWS_SHARED_CREDENTIALS_FILE", shared_creds)
config_path = config.get("configpath")
if config_path:
os.environ.setdefault("AWS_CONFIG_FILE", config_path)
self._transfer_config = None
# https://github.com/aws/aws-cli/blob/0376c6262d6b15dc36c82e6da6e1aad10249cc8c/awscli/customizations/s3/transferconfig.py#L107-L113
_TRANSFER_CONFIG_ALIASES = {
"max_queue_size": "max_io_queue",
"max_concurrent_requests": "max_concurrency",
@@ -77,11 +36,15 @@ class S3FileSystem(BaseFileSystem):
"multipart_chunksize": "multipart_chunksize",
}
def _transform_config(self, s3_config):
def _split_s3_config(self, s3_config):
"""Splits the general s3 config into 2 different config
objects, one for transfer.TransferConfig and other is the
general session config"""
from boto3.s3.transfer import TransferConfig
from dvc.utils import conversions
config, transfer_config = {}, {}
for key, value in s3_config.items():
if key in self._TRANSFER_CONFIG_ALIASES:
@@ -94,367 +57,175 @@ class S3FileSystem(BaseFileSystem):
else:
config[key] = value
return config, transfer_config
# pylint: disable=attribute-defined-outside-init
self._transfer_config = TransferConfig(**transfer_config)
return config
def _process_config(self):
from boto3.s3.transfer import TransferConfig
def _load_aws_config_file(self, profile):
from botocore.configloader import load_config
# pylint: disable=attribute-defined-outside-init
self._transfer_config = None
config_path = os.environ.get("AWS_CONFIG_FILE", _AWS_CONFIG_PATH)
if not os.path.exists(config_path):
return None
config = load_config(config_path)
profile = config["profiles"].get(self.profile or "default")
if not profile:
profile_config = config["profiles"].get(profile or "default")
if not profile_config:
return None
s3_config = profile.get("s3", {})
s3_config, transfer_config = self._transform_config(s3_config)
self._transfer_config = TransferConfig(**transfer_config)
return s3_config
s3_config = profile_config.get("s3", {})
return self._split_s3_config(s3_config)
def _prepare_credentials(self, **config):
from dvc.config import ConfigError
from dvc.utils.flatten import flatten, unflatten
login_info = defaultdict(dict)
# credentials
login_info["key"] = config.get("access_key_id")
login_info["secret"] = config.get("secret_access_key")
login_info["token"] = config.get("session_token")
# session configuration
login_info["profile"] = config.get("profile")
login_info["use_ssl"] = config.get("use_ssl", True)
# extra client configuration
client = login_info["client_kwargs"]
client["region_name"] = config.get("region")
client["endpoint_url"] = config.get("endpointurl")
client["verify"] = config.get("ssl_verify")
# encryptions
additional = login_info["s3_additional_kwargs"]
additional["ServerSideEncryption"] = config.get("sse")
additional["SSEKMSKeyId"] = config.get("sse_kms_key_id")
additional["ACL"] = config.get("acl")
for grant_option, grant_key in self._GRANTS.items():
if config.get(grant_option):
if additional["ACL"]:
raise ConfigError(
"`acl` and `grant_*` AWS S3 config options "
"are mutually exclusive"
)
additional[grant_key] = config[grant_option]
# config kwargs
session_config = login_info["config_kwargs"]
session_config["s3"] = self._load_aws_config_file(
login_info["profile"]
)
shared_creds = config.get("credentialpath")
if shared_creds:
os.environ.setdefault("AWS_SHARED_CREDENTIALS_FILE", shared_creds)
config_path = config.get("configpath")
if config_path:
os.environ.setdefault("AWS_CONFIG_FILE", config_path)
return unflatten(
{
key: value
for key, value in flatten(login_info).items()
if value is not None
}
)
def _entry_hook(self, entry):
entry = entry.copy()
if "ETag" in entry:
entry["etag"] = entry["ETag"].strip('"')
return entry
@wrap_prop(threading.Lock())
@cached_property
def fs(self):
from s3fs import S3FileSystem as _S3FileSystem
return _S3FileSystem(**self.fs_args)
def _translate_exceptions(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
func(*args, **kwargs)
except Exception as exc:
from s3fs.errors import translate_boto_error
raise translate_boto_error(exc)
return wrapper
class S3FileSystem(BaseS3FileSystem): # pylint:disable=abstract-method
@wrap_prop(threading.Lock())
@cached_property
def s3(self):
import boto3
login_info = self.fs_args
client_kwargs = login_info.get("client_kwargs", {})
session_opts = {
"profile_name": self.profile,
"region_name": self.region,
"profile_name": login_info.get("profile"),
"region_name": client_kwargs.get("region_name"),
}
if self.access_key_id:
session_opts["aws_access_key_id"] = self.access_key_id
if self.secret_access_key:
session_opts["aws_secret_access_key"] = self.secret_access_key
if self.session_token:
session_opts["aws_session_token"] = self.session_token
if "key" in login_info:
session_opts["aws_access_key_id"] = login_info["key"]
if "secret" in login_info:
session_opts["aws_secret_access_key"] = login_info["secret"]
if "token" in login_info:
session_opts["aws_session_token"] = login_info["token"]
session = boto3.session.Session(**session_opts)
s3_config = self._process_config()
return session.resource(
"s3",
endpoint_url=self.endpoint_url,
verify=self.ssl_verify,
use_ssl=self.use_ssl,
config=boto3.session.Config(
signature_version="s3v4", s3=s3_config
),
endpoint_url=client_kwargs.get("endpoint_url"),
use_ssl=login_info["use_ssl"],
verify=client_kwargs.get("verify"),
)
@contextmanager
def _get_s3(self):
from botocore.exceptions import (
EndpointConnectionError,
NoCredentialsError,
)
try:
yield self.s3
except NoCredentialsError as exc:
link = error_link("no-credentials")
raise DvcException(
f"Unable to find AWS credentials. {link}"
) from exc
except EndpointConnectionError as exc:
link = error_link("connection-error")
name = self.endpoint_url or "AWS S3"
raise DvcException(
f"Unable to connect to '{name}'. {link}"
) from exc
@contextmanager
def _get_bucket(self, bucket):
with self._get_s3() as s3:
try:
yield s3.Bucket(bucket)
except s3.meta.client.exceptions.NoSuchBucket as exc:
link = error_link("no-bucket")
raise DvcException(
f"Bucket '{bucket}' does not exist. {link}"
) from exc
@contextmanager
def _get_obj(self, path_info):
with self._get_bucket(path_info.bucket) as bucket:
try:
yield bucket.Object(path_info.path)
except bucket.meta.client.exceptions.NoSuchKey as exc:
raise DvcException(f"{path_info.url} does not exist") from exc
def _append_aws_grants_to_extra_args(self, config):
# Keys for extra_args can be one of the following list:
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/customizations/s3.html#boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS
"""
ALLOWED_UPLOAD_ARGS = [
'ACL', 'CacheControl', 'ContentDisposition', 'ContentEncoding',
'ContentLanguage', 'ContentType', 'Expires', 'GrantFullControl',
'GrantRead', 'GrantReadACP', 'GrantWriteACP', 'Metadata',
'RequestPayer', 'ServerSideEncryption', 'StorageClass',
'SSECustomerAlgorithm', 'SSECustomerKey', 'SSECustomerKeyMD5',
'SSEKMSKeyId', 'WebsiteRedirectLocation'
]
"""
grants = {
"grant_full_control": "GrantFullControl",
"grant_read": "GrantRead",
"grant_read_acp": "GrantReadACP",
"grant_write_acp": "GrantWriteACP",
}
for grant_option, extra_args_key in grants.items():
if config.get(grant_option):
if self.acl:
raise ConfigError(
"`acl` and `grant_*` AWS S3 config options "
"are mutually exclusive"
)
self.extra_args[extra_args_key] = config.get(grant_option)
def _generate_download_url(self, path_info, expires=3600):
params = {"Bucket": path_info.bucket, "Key": path_info.path}
with self._get_s3() as s3:
return s3.meta.client.generate_presigned_url(
ClientMethod="get_object",
Params=params,
ExpiresIn=int(expires),
)
def exists(self, path_info, use_dvcignore=True):
"""Check if the blob exists. If it does not exist,
it could be a part of a directory path.
eg: if `data/file.txt` exists, check for `data` should return True
"""
return self.isfile(path_info) or self.isdir(path_info)
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 isfile(self, path_info):
if path_info.path.endswith("/"):
return False
return path_info.path in self._list_paths(path_info)
def info(self, path_info):
# temporary workaround, will be removed when migrating to s3fs
if self.isdir(path_info):
return {"type": "directory"}
with self._get_obj(path_info) as obj:
return {
"type": "file",
"etag": obj.e_tag.strip('"'),
"size": obj.content_length,
}
def _list_paths(self, path_info, max_items=None):
with self._get_bucket(path_info.bucket) as bucket:
obj_summaries = bucket.objects.filter(Prefix=path_info.path)
if max_items is not None:
obj_summaries = obj_summaries.page_size(max_items).limit(
max_items
)
for obj_summary in obj_summaries:
yield obj_summary.key
def walk_files(self, path_info, **kwargs):
if not kwargs.pop("prefix", False):
path_info = path_info / ""
for fname in self._list_paths(path_info, **kwargs):
if fname.endswith("/"):
continue
yield path_info.replace(path=fname)
def find(self, path_info, detail=False):
with self._get_bucket(path_info.bucket) as bucket:
for obj_summary in bucket.objects.filter(Prefix=path_info.path):
if detail:
yield {
"type": "file",
"name": obj_summary.key,
"size": obj_summary.size,
"etag": obj_summary.e_tag.strip('"'),
}
else:
yield obj_summary.key
def remove(self, path_info):
if path_info.scheme != "s3":
raise NotImplementedError
logger.debug(f"Removing {path_info}")
with self._get_obj(path_info) as obj:
obj.delete()
def makedirs(self, path_info):
# We need to support creating empty directories, which means
# creating an object with an empty body and a trailing slash `/`.
#
# We are not creating directory objects for every parent prefix,
# as it is not required.
if not path_info.path:
return
dir_path = path_info / ""
with self._get_obj(dir_path) as obj:
obj.put(Body="")
def copy(self, from_info, to_info):
with self._get_s3() as s3:
self._copy(s3.meta.client, from_info, to_info, self.extra_args)
@classmethod
def _copy_multipart(
cls, s3, from_info, to_info, size, n_parts, extra_args
):
mpu = s3.create_multipart_upload(
Bucket=to_info.bucket, Key=to_info.path, **extra_args
)
mpu_id = mpu["UploadId"]
parts = []
byte_position = 0
for i in range(1, n_parts + 1):
obj = s3.head_object(
Bucket=from_info.bucket, Key=from_info.path, PartNumber=i
)
part_size = obj["ContentLength"]
lastbyte = byte_position + part_size - 1
if lastbyte > size:
lastbyte = size - 1
srange = f"bytes={byte_position}-{lastbyte}"
part = s3.upload_part_copy(
Bucket=to_info.bucket,
Key=to_info.path,
PartNumber=i,
UploadId=mpu_id,
CopySourceRange=srange,
CopySource={"Bucket": from_info.bucket, "Key": from_info.path},
)
parts.append(
{"PartNumber": i, "ETag": part["CopyPartResult"]["ETag"]}
)
byte_position += part_size
assert n_parts == len(parts)
s3.complete_multipart_upload(
Bucket=to_info.bucket,
Key=to_info.path,
UploadId=mpu_id,
MultipartUpload={"Parts": parts},
)
@classmethod
def _copy(cls, s3, from_info, to_info, extra_args):
# NOTE: object's etag depends on the way it was uploaded to s3 or the
# way it was copied within the s3. More specifically, it depends on
# the chunk size that was used to transfer it, which would affect
# whether an object would be uploaded as a single part or as a
# multipart.
#
# If an object's etag looks like '8978c98bb5a48c2fb5f2c4c905768afa',
# then it was transferred as a single part, which means that the chunk
# size used to transfer it was greater or equal to the ContentLength
# of that object. So to preserve that tag over the next transfer, we
# could use any value >= ContentLength.
#
# If an object's etag looks like '50d67013a5e1a4070bef1fc8eea4d5f9-13',
# then it was transferred as a multipart, which means that the chunk
# size used to transfer it was less than ContentLength of that object.
# Unfortunately, in general, it doesn't mean that the chunk size was
# the same throughout the transfer, so it means that in order to
# preserve etag, we need to transfer each part separately, so the
# object is transfered in the same chunks as it was originally.
from boto3.s3.transfer import TransferConfig
obj = s3.head_object(Bucket=from_info.bucket, Key=from_info.path)
etag = obj["ETag"].strip('"')
size = obj["ContentLength"]
_, _, parts_suffix = etag.partition("-")
if parts_suffix:
n_parts = int(parts_suffix)
cls._copy_multipart(
s3, from_info, to_info, size, n_parts, extra_args=extra_args
)
else:
source = {"Bucket": from_info.bucket, "Key": from_info.path}
s3.copy(
source,
to_info.bucket,
to_info.path,
ExtraArgs=extra_args,
Config=TransferConfig(multipart_threshold=size + 1),
)
cached_etag = s3.head_object(Bucket=to_info.bucket, Key=to_info.path)[
"ETag"
].strip('"')
if etag != cached_etag:
raise ETagMismatchError(etag, cached_etag)
def etag(self, path_info):
with self._get_obj(path_info) as obj:
return HashInfo("etag", size=obj.content_length,)
def _upload_fobj(self, fobj, to_info):
with self._get_obj(to_info) as obj:
obj.upload_fileobj(fobj, Config=self._transfer_config)
bucket = self.s3.Bucket(path_info.bucket)
return bucket.Object(path_info.path)
@_translate_exceptions
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
self, from_file, to_info, name=None, no_progress_bar=False, **pbar_args
):
with self._get_obj(to_info) as obj:
total = os.path.getsize(from_file)
with Tqdm(
disable=no_progress_bar, total=total, bytes=True, desc=name
) as pbar:
obj.upload_file(
from_file,
Callback=pbar.update,
ExtraArgs=self.extra_args,
Config=self._transfer_config,
)
total = os.path.getsize(from_file)
with Tqdm(
disable=no_progress_bar,
total=total,
bytes=True,
desc=name,
**pbar_args,
) as pbar:
obj = self._get_obj(to_info)
obj.upload_file(
from_file,
Callback=pbar.update,
ExtraArgs=self.fs_args.get("s3_additional_kwargs"),
Config=self._transfer_config,
)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
with self._get_obj(from_info) as obj:
with Tqdm(
disable=no_progress_bar,
total=obj.content_length,
bytes=True,
desc=name,
) as pbar:
obj.download_file(
to_file, Callback=pbar.update, Config=self._transfer_config
)
@_translate_exceptions
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **pbar_args
):
obj = self._get_obj(from_info)
with Tqdm(
disable=no_progress_bar,
total=obj.content_length,
bytes=True,
desc=name,
**pbar_args,
) as pbar:
obj.download_file(to_file, Callback=pbar.update)
+125
View File
@@ -0,0 +1,125 @@
import getpass
import os.path
import threading
from funcy import cached_property, first, memoize, silent, wrap_prop, wrap_with
from dvc import prompt
from dvc.scheme import Schemes
from dvc.utils.fs import as_atomic
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
_SSH_TIMEOUT = 60 * 30
_SSH_CONFIG_FILE = os.path.expanduser(os.path.join("~", ".ssh", "config"))
@wrap_with(threading.Lock())
@memoize
def ask_password(host, user, port):
return prompt.password(
"Enter a private key passphrase or a password for "
"host '{host}' port '{port}' user '{user}'".format(
host=host, port=port, user=user
)
)
# pylint:disable=abstract-method
class SSHFileSystem(CallbackMixin, FSSpecWrapper):
scheme = Schemes.SSH
REQUIRES = {"sshfs": "sshfs"}
DEFAULT_PORT = 22
PARAM_CHECKSUM = "md5"
def _with_bucket(self, path):
if isinstance(path, self.PATH_CLS):
path_str = path.path
else:
path_str = super()._with_bucket(path)
return os.path.normpath(path_str)
def _prepare_credentials(self, **config):
self.CAN_TRAVERSE = True
from sshfs.config import parse_config
login_info = {}
try:
user_ssh_config = parse_config(host=config["host"])
except FileNotFoundError:
user_ssh_config = {}
login_info["host"] = user_ssh_config.get("Hostname", config["host"])
login_info["username"] = (
config.get("user")
or config.get("username")
or user_ssh_config.get("User")
or getpass.getuser()
)
login_info["port"] = (
config.get("port")
or silent(int)(user_ssh_config.get("Port"))
or self.DEFAULT_PORT
)
login_info["password"] = config.get("password")
if user_ssh_config.get("IdentityFile"):
config.setdefault(
"keyfile", first(user_ssh_config.get("IdentityFile"))
)
keyfile = config.get("keyfile")
if keyfile:
login_info["client_keys"] = [os.path.expanduser(keyfile)]
login_info["timeout"] = config.get("timeout", _SSH_TIMEOUT)
# These two settings fine tune the asyncssh to use the
# fastest encryption algorithm and disable compression
# altogether (since it blocking, it is slowing down
# the transfers in a considerable rate, and even for
# compressible data it is making it extremely slow).
# See: https://github.com/ronf/asyncssh/issues/374
login_info["encryption_algs"] = [
"[email protected]",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr",
]
login_info["compression_algs"] = None
login_info["gss_auth"] = config.get("gss_auth", False)
login_info["agent_forwarding"] = config.get("agent_forwarding", True)
login_info["proxy_command"] = user_ssh_config.get("ProxyCommand")
if config.get("ask_password") and login_info["password"] is None:
login_info["password"] = ask_password(
login_info["host"], login_info["username"], login_info["port"]
)
# We are going to automatically add stuff to known_hosts
# something like paramiko's AutoAddPolicy()
login_info["known_hosts"] = None
return login_info
@wrap_prop(threading.Lock())
@cached_property
def fs(self):
from sshfs import SSHFileSystem as _SSHFileSystem
return _SSHFileSystem(**self.fs_args)
# Ensure that if an interrupt happens during the transfer, we don't
# pollute the cache.
def _upload_fobj(self, fobj, to_info, *args, **kwargs):
with as_atomic(self, to_info) as tmp_file:
super()._upload_fobj(fobj, tmp_file, *args, **kwargs)
def _upload(self, from_file, to_info, *args, **kwargs):
with as_atomic(self, to_info) as tmp_file:
super()._upload(from_file, tmp_file, *args, **kwargs)
-273
View File
@@ -1,273 +0,0 @@
import getpass
import io
import logging
import os
import shutil
import threading
from contextlib import closing, contextmanager
from urllib.parse import urlparse
from funcy import first, memoize, silent, wrap_with
import dvc.prompt as prompt
from dvc.hash_info import HashInfo
from dvc.scheme import Schemes
from ..base import BaseFileSystem
from ..pool import get_connection
logger = logging.getLogger(__name__)
@wrap_with(threading.Lock())
@memoize
def ask_password(host, user, port):
return prompt.password(
"Enter a private key passphrase or a password for "
"host '{host}' port '{port}' user '{user}'".format(
host=host, port=port, user=user
)
)
class SSHFileSystem(BaseFileSystem):
scheme = Schemes.SSH
REQUIRES = {"paramiko": "paramiko"}
_JOBS = 4
PARAM_CHECKSUM = "md5"
# At any given time some of the connections will go over network and
# paramiko stuff, so we would ideally have it double of server processors.
# We use conservative setting of 4 instead to not exhaust max sessions.
CHECKSUM_JOBS = 4
DEFAULT_CACHE_TYPES = ["copy"]
TRAVERSE_PREFIX_LEN = 2
DEFAULT_PORT = 22
TIMEOUT = 1800
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url")
if url:
parsed = urlparse(url)
user_ssh_config = self._load_user_ssh_config(parsed.hostname)
host = user_ssh_config.get("hostname", parsed.hostname)
user = (
config.get("user")
or parsed.username
or user_ssh_config.get("user")
or getpass.getuser()
)
port = (
config.get("port")
or parsed.port
or self._try_get_ssh_config_port(user_ssh_config)
or self.DEFAULT_PORT
)
self.path_info = self.PATH_CLS.from_parts(
scheme=self.scheme,
host=host,
user=user,
port=port,
path=parsed.path,
)
else:
self.path_info = None
user_ssh_config = {}
self.keyfile = config.get(
"keyfile"
) or self._try_get_ssh_config_keyfile(user_ssh_config)
self.timeout = config.get("timeout", self.TIMEOUT)
self.password = config.get("password", None)
self.ask_password = config.get("ask_password", False)
self.gss_auth = config.get("gss_auth", False)
proxy_command = user_ssh_config.get("proxycommand", False)
if proxy_command:
import paramiko
self.sock = paramiko.ProxyCommand(proxy_command)
else:
self.sock = None
self.allow_agent = config.get("allow_agent", True)
@staticmethod
def ssh_config_filename():
return os.path.expanduser(os.path.join("~", ".ssh", "config"))
@staticmethod
def _load_user_ssh_config(hostname):
import paramiko
user_config_file = SSHFileSystem.ssh_config_filename()
user_ssh_config = {}
if hostname and os.path.exists(user_config_file):
ssh_config = paramiko.SSHConfig()
with open(user_config_file) as f:
# For whatever reason parsing directly from f is unreliable
f_copy = io.StringIO(f.read())
ssh_config.parse(f_copy)
user_ssh_config = ssh_config.lookup(hostname)
return user_ssh_config
@staticmethod
def _try_get_ssh_config_port(user_ssh_config):
return silent(int)(user_ssh_config.get("port"))
@staticmethod
def _try_get_ssh_config_keyfile(user_ssh_config):
return first(user_ssh_config.get("identityfile") or ())
def ensure_credentials(self, path_info=None):
if path_info is None:
path_info = self.path_info
# NOTE: we use the same password regardless of the server :(
if self.ask_password and self.password is None:
host, user, port = path_info.host, path_info.user, path_info.port
self.password = ask_password(host, user, port)
def ssh(self, path_info):
self.ensure_credentials(path_info)
from .connection import SSHConnection
return get_connection(
SSHConnection,
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,
sock=self.sock,
allow_agent=self.allow_agent,
)
@contextmanager
def open(self, path_info, mode="r", encoding=None, **kwargs):
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 exists(self, path_info, use_dvcignore=True):
with self.ssh(path_info) as ssh:
return ssh.exists(path_info.path)
def isdir(self, path_info):
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 walk_files(self, path_info, **kwargs):
with self.ssh(path_info) as ssh:
for fname in ssh.walk_files(path_info.path):
yield path_info.replace(path=fname)
def remove(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
with self.ssh(path_info) as ssh:
ssh.remove(path_info.path)
def makedirs(self, path_info):
with self.ssh(path_info) as ssh:
ssh.makedirs(path_info.path)
def move(self, from_info, to_info):
if from_info.scheme != self.scheme or to_info.scheme != self.scheme:
raise NotImplementedError
with self.ssh(from_info) as ssh:
ssh.move(from_info.path, to_info.path)
def 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.atomic_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.reflink(from_info.path, to_info.path)
def md5(self, path_info):
with self.ssh(path_info) as ssh:
return HashInfo(
"md5",
ssh.md5(path_info.path),
size=ssh.getsize(path_info.path),
)
def info(self, path_info):
with self.ssh(path_info) as ssh:
return ssh.info(path_info.path)
def _upload_fobj(self, fobj, to_info):
self.makedirs(to_info.parent)
with self.open(to_info, mode="wb") as fdest:
shutil.copyfileobj(fobj, fdest)
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
with self.ssh(from_info) as ssh:
ssh.download(
from_info.path,
to_file,
progress_title=name,
no_progress_bar=no_progress_bar,
)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
with self.ssh(to_info) as ssh:
ssh.upload(
from_file,
to_info.path,
progress_title=name,
no_progress_bar=no_progress_bar,
)
-356
View File
@@ -1,356 +0,0 @@
import errno
import logging
import os
import posixpath
import shlex
import stat
from contextlib import suppress
from funcy import cached_property
from dvc.exceptions import DvcException
from dvc.progress import Tqdm
from dvc.utils import tmp_fname
from ..base import RemoteCmdError
try:
import paramiko
except ImportError:
paramiko = None # type: ignore
logger = logging.getLogger(__name__)
def sizeof_fmt(num, suffix="B"):
""" Convert number of bytes to human-readable string """
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
if abs(num) < 1024.0:
return f"{num:3.1f}{unit}{suffix}"
num /= 1024.0
return "{:.1f}{}{}".format(num, "Y", suffix)
class SSHConnection:
def __init__(self, host, *args, **kwargs):
logger.debug(
"Establishing ssh connection with '{host}' "
"through port '{port}' as user '{username}'".format(
host=host, **kwargs
)
)
kwargs.setdefault("timeout", 1800)
self.timeout = kwargs["timeout"]
self._ssh = paramiko.SSHClient()
# Explicitly disable paramiko logger. Due to how paramiko dynamically
# loads loggers, it is not disabled by DVC disable_other_loggers().
# See https://github.com/iterative/dvc/issues/3482
self._ssh.set_log_channel("dvc.paramiko")
logging.getLogger("dvc.paramiko").disabled = True
logging.getLogger("dvc.paramiko.sftp").disabled = True
self._ssh.load_system_host_keys()
self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self._ssh.connect(host, *args, **kwargs)
self._ssh.get_transport().set_keepalive(10)
self._sftp_channels = []
@property
def sftp(self):
if not self._sftp_channels:
self._sftp_channels = [self._ssh.open_sftp()]
return self._sftp_channels[0]
def close(self):
for sftp in self._sftp_channels:
sftp.close()
self._ssh.close()
def st_mode(self, path):
lstat = None
with suppress(FileNotFoundError):
lstat = self.sftp.lstat(path)
return lstat.st_mode if lstat else 0
def info(self, path):
st = self.sftp.stat(path)
return {
"size": st.st_size,
"type": "dir" if stat.S_ISDIR(st.st_mode) else "file",
}
def getsize(self, path):
return self.info(path)["size"]
def exists(self, path):
return bool(self.st_mode(path))
def isdir(self, path):
return stat.S_ISDIR(self.st_mode(path))
def isfile(self, path):
return stat.S_ISREG(self.st_mode(path))
def islink(self, path):
return stat.S_ISLNK(self.st_mode(path))
def makedirs(self, path):
# Single stat call will say whether this is a dir, a file or a link
st_mode = self.st_mode(path)
if stat.S_ISDIR(st_mode):
return
if stat.S_ISREG(st_mode) or stat.S_ISLNK(st_mode):
raise DvcException(
f"a file with the same name '{path}' already exists"
)
head, tail = posixpath.split(path)
if head:
self.makedirs(head)
if tail:
try:
self.sftp.mkdir(path)
except OSError as exc:
# Since paramiko errors are very vague we need to recheck
# whether it's because path already exists or something else
if exc.errno == errno.EACCES or not self.exists(path):
raise DvcException(
f"unable to create remote directory '{path}'"
) from exc
def walk(self, directory, topdown=True):
# NOTE: original os.walk() implementation [1] with default options was
# used as a template.
#
# [1] https://github.com/python/cpython/blob/master/Lib/os.py
try:
dir_entries = self.sftp.listdir_attr(directory)
except OSError as exc:
raise DvcException(
"couldn't get the '{}' remote directory files list".format(
directory
)
) from exc
dirs = []
nondirs = []
for entry in dir_entries:
name = entry.filename
if stat.S_ISDIR(entry.st_mode):
dirs.append(name)
else:
nondirs.append(name)
if topdown:
yield directory, dirs, nondirs
for dname in dirs:
newpath = posixpath.join(directory, dname)
yield from self.walk(newpath, topdown=topdown)
if not topdown:
yield directory, dirs, nondirs
def walk_files(self, directory):
for root, _, files in self.walk(directory):
for fname in files:
yield posixpath.join(root, fname)
def _remove_file(self, path):
with suppress(FileNotFoundError):
self.sftp.remove(path)
def _remove_dir(self, path):
for root, dirs, files in self.walk(path, topdown=False):
for fname in files:
with suppress(FileNotFoundError):
self._remove_file(posixpath.join(root, fname))
for dname in dirs:
with suppress(FileNotFoundError):
self.sftp.rmdir(posixpath.join(root, dname))
with suppress(FileNotFoundError):
self.sftp.rmdir(path)
def remove(self, path):
if self.isdir(path):
self._remove_dir(path)
else:
self._remove_file(path)
def download(self, src, dest, no_progress_bar=False, progress_title=None):
with Tqdm(
desc=progress_title or os.path.basename(src),
disable=no_progress_bar,
bytes=True,
total=self.getsize(src),
) as pbar:
self.sftp.get(src, dest, callback=pbar.update_to)
def move(self, src, dst):
"""Rename src to dst, if it is not possible (in case src and dst are
on different filesystems) and actual physical copying of data is
happening.
"""
self.makedirs(posixpath.dirname(dst))
try:
self.sftp.rename(src, dst)
except OSError:
self.atomic_copy(src, dst)
self.remove(src)
def atomic_copy(self, src, dst):
tmp = tmp_fname(dst)
try:
self.copy(src, tmp)
self.sftp.rename(tmp, dst)
finally:
self.remove(tmp)
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)
with Tqdm(
desc=progress_title,
disable=no_progress_bar,
bytes=True,
total=os.path.getsize(src),
) as pbar:
self.sftp.put(src, tmp_file, callback=pbar.update_to)
self.sftp.rename(tmp_file, dest)
def execute(self, cmd):
stdin, stdout, stderr = self._ssh.exec_command(cmd)
channel = stdout.channel
stdin.close()
channel.shutdown_write()
stdout_chunks = []
stderr_chunks = []
while (
not channel.closed
or channel.recv_ready()
or channel.recv_stderr_ready()
):
import selectors
selector = selectors.DefaultSelector()
selector.register(stdout.channel, selectors.EVENT_READ)
got_chunk = False
events = selector.select(self.timeout)
for key, _ in events:
c = key.fileobj
if c.recv_ready():
stdout_chunks.append(stdout.channel.recv(len(c.in_buffer)))
got_chunk = True
if c.recv_stderr_ready():
stderr_len = len(c.in_stderr_buffer)
s = stderr.channel.recv_stderr(stderr_len)
stderr_chunks.append(s)
got_chunk = True
if (
not got_chunk
and stdout.channel.exit_status_ready()
and not stderr.channel.recv_stderr_ready()
and not stdout.channel.recv_ready()
):
stdout.channel.shutdown_read()
stdout.channel.close()
break
stdout.close()
stderr.close()
ret = stdout.channel.recv_exit_status()
if ret != 0:
err = b"".join(stderr_chunks).decode("utf-8")
raise RemoteCmdError("ssh", cmd, ret, err)
return b"".join(stdout_chunks).decode("utf-8")
@cached_property
def uname(self):
return self.execute("uname").strip()
def md5(self, path):
"""
Use different md5 commands depending on the OS:
- Darwin's `md5` returns BSD-style checksums by default
- Linux's `md5sum` needs the `--tag` flag for a similar output
Example:
MD5 (foo.txt) = f3d220a856b52aabbf294351e8a24300
"""
path = shlex.quote(path)
if self.uname == "Linux":
md5 = self.execute("md5sum " + path).split()[0]
elif self.uname == "Darwin":
md5 = self.execute("md5 " + path).split()[-1]
else:
raise DvcException(
f"'{self.uname}' is not supported as a SSH remote"
)
assert len(md5) == 32
return md5
def copy(self, src, dest):
dest = shlex.quote(dest)
src = shlex.quote(src)
self.execute(f"cp {src} {dest}")
def open_max_sftp_channels(self):
# If there are more than 1 it means we've already opened max amount
if len(self._sftp_channels) <= 1:
while True:
try:
self._sftp_channels.append(self._ssh.open_sftp())
except paramiko.ssh_exception.ChannelException:
if not self._sftp_channels:
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):
dest = shlex.quote(dest)
src = shlex.quote(src)
if self.uname == "Linux":
return self.execute(f"cp --reflink {src} {dest}")
if self.uname == "Darwin":
return self.execute(f"cp -c {src} {dest}")
raise DvcException(f"'{self.uname}' is not supported as a SSH remote")
def hardlink(self, src, dest):
dest = shlex.quote(dest)
src = shlex.quote(src)
self.execute(f"ln {src} {dest}")
+61 -240
View File
@@ -1,273 +1,94 @@
import io
import logging
import os
import threading
from collections import deque
from functools import lru_cache
from funcy import cached_property, nullcontext, wrap_prop
from funcy import cached_property, wrap_prop
from dvc.config import ConfigError
from dvc.exceptions import DvcException
from dvc.path_info import HTTPURLInfo, WebDAVURLInfo
from dvc.progress import Tqdm
from dvc.path_info import WebDAVURLInfo
from dvc.scheme import Schemes
from .base import BaseFileSystem
from .fsspec_wrapper import FSSpecWrapper
from .http import ask_password
logger = logging.getLogger(__name__)
class WebDAVConnectionError(DvcException):
def __init__(self, host):
super().__init__(f"Unable to connect to WebDAV {host}.")
class WebDAVFileSystem(BaseFileSystem): # pylint:disable=abstract-method
# Use webdav scheme
class WebDAVFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
scheme = Schemes.WEBDAV
# URLInfo for Webdav ~ replaces webdav -> http
PATH_CLS = WebDAVURLInfo
# Traversable as walk_files is implemented
CAN_TRAVERSE = True
# Length of walk_files prefix
TRAVERSE_PREFIX_LEN = 2
# Implementation based on webdav3.client
REQUIRES = {"webdavclient3": "webdav3.client"}
# Chunk size for buffered upload/download with progress bar
CHUNK_SIZE = 2 ** 16
REQUIRES = {"webdav4": "webdav4"}
PARAM_CHECKSUM = "etag"
DETAIL_FIELDS = frozenset(("etag", "size"))
# Constructor
def __init__(self, repo, config):
# Call BaseFileSystem constructor
super().__init__(repo, config)
def __init__(self, **config):
super().__init__(**config)
# Get username from configuration
self.user = config.get("user", None)
cert_path = config.get("cert_path", None)
key_path = config.get("key_path", None)
self.prefix = config.get("prefix", "")
self.fs_args.update(
{
"base_url": config["url"],
"cert": cert_path if not key_path else (cert_path, key_path),
"verify": config.get("ssl_verify", True),
"timeout": config.get("timeout", 30),
}
)
# Get password from configuration (might be None ~ not set)
self.password = config.get("password", None)
@staticmethod
def _get_kwargs_from_urls(urlpath):
path_info = WebDAVURLInfo(urlpath)
return {
"prefix": path_info.path.rstrip("/"),
"host": path_info.replace(path="").url,
"url": path_info.url.rstrip("/"),
"user": path_info.user,
}
# Whether to ask for password if it is not set
self.ask_password = config.get("ask_password", False)
def _prepare_credentials(self, **config):
user = config.get("user", None)
password = config.get("password", None)
# Use token for webdav auth
self.token = config.get("token", None)
headers = {}
token = config.get("token")
auth = None
if token:
headers.update({"Authorization": f"Bearer {token}"})
elif user:
if not password and config.get("ask_password"):
password = ask_password(config["host"], user)
auth = (user, password)
# Path to certificate
self.cert_path = config.get("cert_path", None)
return {"headers": headers, "auth": auth}
# Path to private key
self.key_path = config.get("key_path", None)
# Connection timeout
self.timeout = config.get("timeout", 30)
# Get URL from configuration
self.url = config.get("url", None)
# If URL in config parse path_info
if self.url:
self.path_info = self.PATH_CLS(self.url)
# If username not specified try to use from URL
if self.user is None and self.path_info.user is not None:
self.user = self.path_info.user
# Construct hostname from path_info by stripping path
http_info = HTTPURLInfo(self.path_info.url)
self.hostname = http_info.replace(path="").url
# Webdav client
@wrap_prop(threading.Lock())
@cached_property
def _client(self):
from webdav3.client import Client
def fs(self):
from webdav4.fsspec import WebdavFileSystem
# Set password or ask for it
if self.ask_password and self.password is None and self.token is None:
self.password = ask_password(self.hostname, self.user)
return WebdavFileSystem(**self.fs_args)
# Setup webdav client options dictionary
options = {
"webdav_hostname": self.hostname,
"webdav_login": self.user,
"webdav_password": self.password,
"webdav_token": self.token,
"webdav_cert_path": self.cert_path,
"webdav_key_path": self.key_path,
"webdav_timeout": self.timeout,
"webdav_chunk_size": self.CHUNK_SIZE,
}
def _upload_fobj(self, fobj, to_info, size: int = None):
rpath = self.translate_path_info(to_info)
self.makedirs(os.path.dirname(rpath))
# using upload_fileobj to directly upload fileobj rather than buffering
# and using overwrite=True to avoid check for an extra exists call,
# as caller should ensure that the file does not exist beforehand.
return self.fs.client.upload_fileobj(
fobj, rpath, overwrite=True, size=size
)
client = Client(options)
@lru_cache(512)
def translate_path_info(self, path):
if isinstance(path, self.PATH_CLS):
return path.path[len(self.prefix) :].lstrip("/")
return path
# Check whether client options are valid
if not client.valid():
raise ConfigError(
f"Configuration for WebDAV {self.hostname} is invalid."
)
_with_bucket = translate_path_info
# Check whether connection is valid (root should always exist)
if not client.check(self.path_info.path):
raise WebDAVConnectionError(self.hostname)
return client
def open(self, path_info, mode="r", encoding=None, **kwargs):
from webdav3.exceptions import RemoteResourceNotFound
assert mode in {"r", "rt", "rb"}
fobj = io.BytesIO()
try:
self._client.download_from(buff=fobj, remote_path=path_info.path)
except RemoteResourceNotFound as exc:
raise FileNotFoundError from exc
fobj.seek(0)
if "mode" == "rb":
return fobj
return io.TextIOWrapper(fobj, encoding=encoding)
# Checks whether file/directory exists at remote
def exists(self, path_info, use_dvcignore=True):
# Use webdav check to test for file existence
return self._client.check(path_info.path)
# Checks whether path points to directory
def isdir(self, path_info):
# Use webdav is_dir to test whether path points to a directory
return self._client.is_dir(path_info.path)
def walk_files(self, path_info, **kwargs):
if not self.exists(path_info):
return
for file in self.find(path_info):
yield path_info.replace(path=file)
def ls(self, path_info, detail=False):
for entry in self._client.list(path_info.path):
path = entry["path"]
if detail:
if entry["isdir"]:
yield {"type": "directory", "name": path}
else:
yield {
"type": "file",
"name": path,
"size": entry["size"],
"etag": entry["etag"],
}
else:
yield path
def find(self, path_info, detail=False):
dirs = deque([path_info.path])
while dirs:
for entry in self._client.list(dirs.pop(), get_info=True):
path = entry["path"]
if entry["isdir"]:
dirs.append(path)
continue
if detail:
yield {
"type": "file",
"name": path,
"size": entry["size"],
"etag": entry["etag"],
}
else:
yield path
# Removes file/directory
def remove(self, path_info):
# Use webdav client clean (DELETE) method to remove file/directory
self._client.clean(path_info.path)
# Creates directories
def makedirs(self, path_info):
# Terminate recursion
if path_info.path == self.path_info.path or self.exists(path_info):
return
# Recursively descent to root
self.makedirs(path_info.parent)
# Construct directory at current recursion depth
self._client.mkdir(path_info.path)
# Moves file/directory at remote
def move(self, from_info, to_info):
# Webdav client move
self._client.move(from_info.path, to_info.path)
def _upload_fobj(self, fobj, to_info):
# In contrast to other upload_fobj implementations, this one does not
# exactly do a chunked-upload but rather put everything in one request.
self.makedirs(to_info.parent)
self._client.upload_to(buff=fobj, remote_path=to_info.path)
# Downloads file from remote to file
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
# Progress from HTTPFileSystem
with open(to_file, "wb") as fd:
with Tqdm.wrapattr(
fd,
"write",
total=None if no_progress_bar else self.getsize(from_info),
leave=False,
desc=from_info.url if name is None else name,
disable=no_progress_bar,
) as fd_wrapped:
# Download from WebDAV via buffer
self._client.download_from(
buff=fd_wrapped, remote_path=from_info.path
)
# Uploads file to remote
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs,
):
# First try to create parent directories
self.makedirs(to_info.parent)
file_size = os.path.getsize(from_file)
with open(from_file, "rb") as fd:
progress_context = (
nullcontext(fd)
if file_size == 0
else Tqdm.wrapattr(
fd,
"read",
total=None if no_progress_bar else file_size,
leave=False,
desc=to_info.url if name is None else name,
disable=no_progress_bar,
)
)
with progress_context as fd_wrapped:
self._client.upload_to(
buff=fd_wrapped, remote_path=to_info.path
)
def info(self, path_info):
info = self._client.info(path_info.path)
return {
"size": int(info["size"]),
"etag": info["etag"],
}
class WebDAVSFileSystem(WebDAVFileSystem): # pylint:disable=abstract-method
scheme = Schemes.WEBDAVS
-7
View File
@@ -1,7 +0,0 @@
from dvc.scheme import Schemes
from .webdav import WebDAVFileSystem
class WebDAVSFileSystem(WebDAVFileSystem): # pylint:disable=abstract-method
scheme = Schemes.WEBDAVS
+19 -19
View File
@@ -4,7 +4,6 @@ import posixpath
import shutil
import threading
from contextlib import contextmanager
from urllib.parse import urlparse
from funcy import cached_property, wrap_prop
@@ -30,33 +29,34 @@ def update_pbar(pbar, total):
return update
class WebHDFSFileSystem(BaseFileSystem):
class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
scheme = Schemes.WEBHDFS
PATH_CLS = CloudURLInfo
REQUIRES = {"hdfs": "hdfs"}
PARAM_CHECKSUM = "checksum"
TRAVERSE_PREFIX_LEN = 2
def __init__(self, repo, config):
super().__init__(repo, config)
def __init__(self, **config):
super().__init__(**config)
self.path_info = None
url = config.get("url")
if not url:
return
self.path_info = self.PATH_CLS(url)
parsed = urlparse(url)
self.host = parsed.hostname
self.user = parsed.username or config.get("user")
self.port = parsed.port
self.host = config["host"]
self.user = config.get("user")
self.port = config.get("port")
self.hdfscli_config = config.get("hdfscli_config")
self.token = config.get("webhdfs_token")
self.alias = config.get("webhdfs_alias")
@staticmethod
def _get_kwargs_from_urls(urlpath):
from fsspec.implementations.webhdfs import WebHDFS
return (
WebHDFS._get_kwargs_from_urls( # pylint:disable=protected-access
urlpath
)
)
@wrap_prop(threading.Lock())
@cached_property
def hdfs_client(self):
@@ -114,7 +114,7 @@ class WebHDFSFileSystem(BaseFileSystem):
self.hdfs_client.delete(path_info.path)
def exists(self, path_info, use_dvcignore=True):
def exists(self, path_info) -> bool:
assert not isinstance(path_info, list)
assert path_info.scheme == "webhdfs"
@@ -123,7 +123,7 @@ class WebHDFSFileSystem(BaseFileSystem):
def info(self, path_info):
st = self.hdfs_client.status(path_info.path)
return {"size": st["length"]}
return {"size": st["length"], "type": "file"}
def checksum(self, path_info):
return HashInfo(
@@ -141,7 +141,7 @@ class WebHDFSFileSystem(BaseFileSystem):
self.hdfs_client.makedirs(to_info.parent.path)
self.hdfs_client.rename(from_info.path, to_info.path)
def _upload_fobj(self, fobj, to_info):
def _upload_fobj(self, fobj, to_info, **kwargs):
with self.hdfs_client.write(to_info.path) as fdest:
shutil.copyfileobj(fobj, fdest)
+4
View File
@@ -14,6 +14,7 @@ class HashInfo:
value: Optional[str]
size: Optional[int] = field(default=None, compare=False)
nfiles: Optional[int] = field(default=None, compare=False)
obj_name: Optional[str] = field(default=None, compare=False)
def __bool__(self):
return bool(self.value)
@@ -21,6 +22,9 @@ class HashInfo:
def __str__(self):
return f"{self.name}: {self.value}"
def __hash__(self):
return hash((self.name, self.value))
@classmethod
def from_dict(cls, d):
_d = d.copy() if d else {}
+116 -83
View File
@@ -7,9 +7,12 @@ from itertools import groupby, takewhile
from pathspec.patterns import GitWildMatchPattern
from pathspec.util import normalize_file
from dvc.fs.base import BaseFileSystem
from dvc.path_info import PathInfo
from dvc.pathspec_math import PatternInfo, merge_patterns
from dvc.scheme import Schemes
from dvc.system import System
from dvc.types import AnyPath, List, Optional
from dvc.utils import relpath
from dvc.utils.collections import PathStringTrie
@@ -49,16 +52,12 @@ class DvcIgnorePatterns(DvcIgnore):
]
@classmethod
def from_files(cls, ignore_file_path, fs):
assert os.path.isabs(ignore_file_path)
dirname = os.path.normpath(os.path.dirname(ignore_file_path))
ignore_file_rel_path = os.path.relpath(ignore_file_path, fs.fs_root)
with fs.open(ignore_file_path, encoding="utf-8") as fobj:
def from_file(cls, path, fs, name):
assert os.path.isabs(path)
dirname = os.path.normpath(os.path.dirname(path))
with fs.open(path, encoding="utf-8") as fobj:
path_spec_lines = [
PatternInfo(
line,
"{}:{}:{}".format(ignore_file_rel_path, line_no + 1, line),
)
PatternInfo(line, f"{name}:{line_no + 1}:{line}")
for line_no, line in enumerate(
map(str.strip, fobj.readlines())
)
@@ -67,7 +66,7 @@ class DvcIgnorePatterns(DvcIgnore):
return cls(path_spec_lines, dirname)
def __call__(self, root, dirs, files):
def __call__(self, root: List[str], dirs: List[str], files: List[str]):
files = [f for f in files if not self.matches(root, f)]
dirs = [d for d in dirs if not self.matches(root, d, True)]
@@ -159,26 +158,6 @@ def _no_match(path):
return CheckIgnoreResult(path, False, ["::"])
class DvcIgnoreFilterNoop:
def __init__(self, fs, root_dir):
pass
def __call__(self, root, dirs, files, **kwargs):
return dirs, files
def is_ignored_dir(self, _):
return False
def is_ignored_file(self, _):
return False
def check_ignore(self, path):
return _no_match(path)
def is_ignored(self, _):
return False
class DvcIgnoreFilter:
def __init__(self, fs, root_dir):
from dvc.repo import Repo
@@ -187,33 +166,40 @@ class DvcIgnoreFilter:
".hg/",
".git/",
".git",
"{}/".format(Repo.DVC_DIR),
f"{Repo.DVC_DIR}/",
]
self.fs = fs
self.root_dir = root_dir
self.ignores_trie_fs = PathStringTrie()
self._ignores_trie_subrepos = PathStringTrie()
self.ignores_trie_fs[root_dir] = DvcIgnorePatterns(
default_ignore_patterns, root_dir
)
self._ignored_subrepos = PathStringTrie()
self._update(self.root_dir)
self._ignores_trie_subrepos[root_dir] = self.ignores_trie_fs[root_dir]
self._update(
self.root_dir,
self._ignores_trie_subrepos,
dnames=None,
ignore_subrepos=False,
)
self._update(
self.root_dir,
self.ignores_trie_fs,
dnames=None,
ignore_subrepos=True,
)
def _update(self, dirname):
self._update_sub_repo(dirname)
old_pattern = self.ignores_trie_fs.longest_prefix(dirname).value
def _update_trie(self, dirname: str, trie: PathStringTrie) -> None:
old_pattern = trie.longest_prefix(dirname).value
matches = old_pattern.matches(dirname, DvcIgnore.DVCIGNORE_FILE, False)
ignore_file_path = os.path.join(dirname, DvcIgnore.DVCIGNORE_FILE)
if not matches and self.fs.exists(
ignore_file_path, use_dvcignore=False
):
new_pattern = DvcIgnorePatterns.from_files(
ignore_file_path, self.fs
)
path = os.path.join(dirname, DvcIgnore.DVCIGNORE_FILE)
if not matches and self.fs.exists(path):
name = os.path.relpath(path, self.root_dir)
new_pattern = DvcIgnorePatterns.from_file(path, self.fs, name)
if old_pattern:
self.ignores_trie_fs[dirname] = DvcIgnorePatterns(
trie[dirname] = DvcIgnorePatterns(
*merge_patterns(
old_pattern.pattern_list,
old_pattern.dirname,
@@ -222,11 +208,32 @@ class DvcIgnoreFilter:
)
)
else:
self.ignores_trie_fs[dirname] = new_pattern
trie[dirname] = new_pattern
elif old_pattern:
self.ignores_trie_fs[dirname] = old_pattern
trie[dirname] = old_pattern
def _update_sub_repo(self, path):
def _update(
self,
dirname: str,
ignore_trie: PathStringTrie,
dnames: Optional["List"],
ignore_subrepos: bool,
) -> None:
self._update_trie(dirname, ignore_trie)
if ignore_subrepos:
if dnames is None:
try:
_, dnames, _ = next(self.fs.walk(dirname))
except StopIteration:
dnames = []
for dname in dnames:
self._update_sub_repo(
os.path.join(dirname, dname), ignore_trie
)
def _update_sub_repo(self, path, ignore_trie: PathStringTrie):
from dvc.repo import Repo
if path == self.root_dir:
@@ -237,14 +244,11 @@ class DvcIgnoreFilter:
return
root, dname = os.path.split(path)
self._ignored_subrepos[root] = self._ignored_subrepos.get(
root, set()
) | {dname}
pattern_info = PatternInfo(f"/{dname}/", f"in sub_repo:{dname}")
new_pattern = DvcIgnorePatterns([pattern_info], root)
old_pattern = self.ignores_trie_fs.longest_prefix(root).value
old_pattern = ignore_trie.longest_prefix(root).value
if old_pattern:
self.ignores_trie_fs[root] = DvcIgnorePatterns(
ignore_trie[root] = DvcIgnorePatterns(
*merge_patterns(
old_pattern.pattern_list,
old_pattern.dirname,
@@ -253,25 +257,50 @@ class DvcIgnoreFilter:
)
)
else:
self.ignores_trie_fs[root] = new_pattern
ignore_trie[root] = new_pattern
def __call__(self, root, dirs, files, ignore_subrepos=True):
for dname in dirs:
self._update_sub_repo(os.path.join(root, dname))
ignore_pattern = self._get_trie_pattern(root)
abs_root = os.path.abspath(root)
ignore_pattern = self._get_trie_pattern(
abs_root, dnames=dirs, ignore_subrepos=ignore_subrepos
)
if ignore_pattern:
dirs, files = ignore_pattern(root, dirs, files)
if not ignore_subrepos:
dirs.extend(self._ignored_subrepos.get(root, []))
dirs, files = ignore_pattern(abs_root, dirs, files)
return dirs, files
def _get_trie_pattern(self, dirname):
ignore_pattern = self.ignores_trie_fs.get(dirname)
def walk(self, fs: BaseFileSystem, path_info: AnyPath, **kwargs):
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
if fs.scheme == Schemes.LOCAL:
for root, dirs, files in fs.walk(path_info, **kwargs):
dirs[:], files[:] = self(
root, dirs, files, ignore_subrepos=ignore_subrepos
)
yield root, dirs, files
else:
yield from fs.walk(path_info, **kwargs)
def walk_files(self, fs: BaseFileSystem, path_info: AnyPath, **kwargs):
if fs.scheme == Schemes.LOCAL:
for root, _, files in self.walk(fs, path_info, **kwargs):
for file in files:
# NOTE: os.path.join is ~5.5 times slower
yield PathInfo(f"{root}{os.sep}{file}")
else:
yield from fs.walk_files(path_info)
def _get_trie_pattern(
self, dirname, dnames: Optional["List"] = None, ignore_subrepos=True
) -> Optional["DvcIgnorePatterns"]:
if ignore_subrepos:
ignores_trie = self.ignores_trie_fs
else:
ignores_trie = self._ignores_trie_subrepos
ignore_pattern = ignores_trie.get(dirname)
if ignore_pattern:
return ignore_pattern
prefix = self.ignores_trie_fs.longest_prefix(dirname).key
prefix = ignores_trie.longest_prefix(dirname).key
if not prefix:
# outside of the repo
return None
@@ -286,33 +315,31 @@ class DvcIgnoreFilter:
dirs.append(dirname)
for parent in dirs:
self._update(parent)
self._update(parent, ignores_trie, dnames, ignore_subrepos)
return self.ignores_trie_fs.get(dirname)
return ignores_trie.get(dirname)
def _is_ignored(self, path, is_dir=False):
def _is_ignored(
self, path: str, is_dir: bool = False, ignore_subrepos: bool = True
):
if self._outside_repo(path):
return False
dirname, basename = os.path.split(os.path.normpath(path))
ignore_pattern = self._get_trie_pattern(dirname)
ignore_pattern = self._get_trie_pattern(dirname, None, ignore_subrepos)
if ignore_pattern:
return ignore_pattern.matches(dirname, basename, is_dir)
return False
def _is_subrepo(self, path):
dirname, basename = os.path.split(os.path.normpath(path))
return basename in self._ignored_subrepos.get(dirname, set())
def is_ignored_dir(self, path, ignore_subrepos=True):
def is_ignored_dir(self, path: str, ignore_subrepos: bool = True) -> bool:
"Only used in LocalFileSystem"
path = os.path.abspath(path)
if not ignore_subrepos:
return not self._is_subrepo(path)
if path == self.root_dir:
return False
return self._is_ignored(path, True)
return self._is_ignored(path, True, ignore_subrepos=ignore_subrepos)
def is_ignored_file(self, path):
def is_ignored_file(self, path: str) -> bool:
"Only used in LocalFileSystem"
path = os.path.abspath(path)
return self._is_ignored(path, False)
@@ -339,21 +366,27 @@ class DvcIgnoreFilter:
pattern = self._get_trie_pattern(dirname)
if pattern:
matches = pattern.matches(
dirname, basename, os.path.isdir(full_target), True,
dirname, basename, os.path.isdir(full_target), True
)
if matches:
return CheckIgnoreResult(target, True, matches)
return _no_match(target)
def is_ignored(self, path):
def is_ignored(
self, fs: BaseFileSystem, path: str, ignore_subrepos: bool = True
) -> bool:
# NOTE: can't use self.check_ignore(path).match for now, see
# https://github.com/iterative/dvc/issues/4555
if os.path.isfile(path):
if fs.scheme != Schemes.LOCAL:
return False
if fs.isfile(path):
return self.is_ignored_file(path)
if os.path.isdir(path):
return self.is_ignored_dir(path)
return self.is_ignored_file(path) or self.is_ignored_dir(path)
if fs.isdir(path):
return self.is_ignored_dir(path, ignore_subrepos)
return self.is_ignored_file(path) or self.is_ignored_dir(
path, ignore_subrepos
)
def init(path):
+36 -31
View File
@@ -20,6 +20,11 @@ if PKG is None:
else:
package = f"({PKG})"
try:
import importlib.metadata as importlib_metadata
except ImportError: # < 3.8
import importlib_metadata # type: ignore[no-redef]
def get_dvc_info():
info = [
@@ -27,38 +32,34 @@ def get_dvc_info():
"---------------------------------",
f"Platform: Python {platform.python_version()} on "
f"{platform.platform()}",
f"Supports: {_get_supported_remotes()}",
f"Supports:{_get_supported_remotes()}",
]
try:
repo = Repo()
with Repo() as 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.odb.local.cache_dir):
info.append(f"Cache types: {_get_linktype_support_info(repo)}")
fs_type = get_fs_type(repo.odb.local.cache_dir)
info.append(f"Cache directory: {fs_type}")
else:
info.append("Cache types: " + error_link("no-dvc-cache"))
# 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.odb.local.cache_dir):
info.append(
"Cache types: {}".format(_get_linktype_support_info(repo))
)
fs_type = get_fs_type(repo.odb.local.cache_dir)
info.append(f"Cache directory: {fs_type}")
else:
info.append("Cache types: " + error_link("no-dvc-cache"))
info.append(f"Caches: {_get_caches(repo.odb)}")
info.append(f"Remotes: {_get_remotes(repo.config)}")
info.append(f"Caches: {_get_caches(repo.odb)}")
info.append(f"Remotes: {_get_remotes(repo.config)}")
root_directory = repo.root_dir
fs_root = get_fs_type(os.path.abspath(root_directory))
info.append(f"Workspace directory: {fs_root}")
info.append(f"Repo: {_get_dvc_repo_info(repo)}")
except NotDvcRepoError:
pass
except SCMError:
info.append("Repo: dvc, git (broken)")
else:
root_directory = repo.root_dir
fs_root = get_fs_type(os.path.abspath(root_directory))
info.append(f"Workspace directory: {fs_root}")
info.append("Repo: {}".format(_get_dvc_repo_info(repo)))
return "\n".join(info)
@@ -115,19 +116,23 @@ def _get_linktype_support_info(repo):
def _get_supported_remotes():
supported_remotes = []
for scheme, fs_cls in FS_MAP.items():
if not fs_cls.get_missing_deps():
supported_remotes.append(scheme)
dependencies = []
for requirement in fs_cls.REQUIRES:
dependencies.append(
f"{requirement} = "
f"{importlib_metadata.version(requirement)}"
)
if len(supported_remotes) == len(FS_MAP):
return "All remotes"
remote_info = scheme
if dependencies:
remote_info += " (" + ", ".join(dependencies) + ")"
supported_remotes.append(remote_info)
if len(supported_remotes) == 1:
return supported_remotes
return ", ".join(supported_remotes)
assert len(supported_remotes) >= 1
return "\n\t" + ",\n\t".join(supported_remotes)
def get_fs_type(path):
+6 -5
View File
@@ -5,6 +5,7 @@
TEXT_CHARS = bytes(range(32, 127)) + b"\n\r\t\f\b"
DEFAULT_CHUNK_SIZE = 512
def istextblock(block):
@@ -22,11 +23,11 @@ def istextblock(block):
return float(len(nontext)) / len(block) <= 0.30
def istextfile(fname, fs, blocksize=512):
""" Uses heuristics to guess whether the given file is text or binary,
by reading a single block of bytes from the file.
If more than 30% of the chars in the block are non-text, or there
are NUL ('\x00') bytes in the block, assume this is a binary file.
def istextfile(fname, fs, blocksize=DEFAULT_CHUNK_SIZE):
"""Uses heuristics to guess whether the given file is text or binary,
by reading a single block of bytes from the file.
If more than 30% of the chars in the block are non-text, or there
are NUL ('\x00') bytes in the block, assume this is a binary file.
"""
with fs.open(fname, "rb") as fobj:
block = fobj.read(blocksize)
+15 -4
View File
@@ -62,7 +62,7 @@ def addLoggingLevel(levelName, levelNum, methodName=None):
class LoggingException(Exception):
def __init__(self, record):
msg = "failed to log {}".format(str(record))
msg = f"failed to log {str(record)}"
super().__init__(msg)
@@ -188,8 +188,8 @@ def _stack_trace(exc_info):
def disable_other_loggers():
logging.captureWarnings(True)
root = logging.root
for (logger_name, logger) in root.manager.loggerDict.items():
loggerDict = logging.root.manager.loggerDict # pylint: disable=no-member
for logger_name, logger in loggerDict.items():
if logger_name != "dvc" and not logger_name.startswith("dvc."):
logger.disabled = True
@@ -197,6 +197,17 @@ def disable_other_loggers():
def setup(level=logging.INFO):
colorama.init()
if level >= logging.DEBUG:
# Unclosed session errors for asyncio/aiohttp are only available
# on the tracing mode for extensive debug purposes. They are really
# noisy, and this is potentially somewhere in the client library
# not managing their own session. Even though it is the best practice
# for them to do so, we can be assured that these errors raised when
# the object is getting deallocated, so no need to take any extensive
# action.
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
logging.getLogger("aiohttp").setLevel(logging.CRITICAL)
addLoggingLevel("TRACE", logging.DEBUG - 5)
logging.config.dictConfig(
{
@@ -245,7 +256,7 @@ def setup(level=logging.INFO):
"console_trace",
"console_errors",
],
},
}
},
"disable_existing_loggers": False,
}
+1 -1
View File
@@ -52,7 +52,7 @@ def main(argv=None): # noqa: C901
with debugtools(args):
cmd = args.func(args)
ret = cmd.run()
ret = cmd.do_run()
except ConfigError:
logger.exception("configuration error")
ret = 251

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