Compare commits

...
298 Commits
Author SHA1 Message Date
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
Ruslan Kuprieiev 3db25ea67d dvc: bump to 2.1.0 2021-05-03 16:29:49 +03:00
Saugat Pachhai d6afd26bea metrics diff/show: rollback to rounding fractional digits with precision (#5904) 2021-05-02 16:56:00 +03:00
Saugat Pachhai dba0aaeab7 accept dict for header styling (#5905)
I found that it's harder to style a header with the list using index,
they are to be applied by header name. Otherwise, we might end up using
a different header for coloring, or worse throw some error because the
header name and style don't match.
Row does have it's property of index, so it makes sense there.
2021-04-30 18:43:38 +05:45
Paweł Redzyński d509d77193 list: add --show-json option (#5888)
* list: add --show-json option

* list: add test for --show-json
2021-04-30 11:40:44 +02:00
Saugat Pachhai 102817e53a simplify experiments row collection logic (#5901) 2021-04-30 15:11:57 +05:45
Peter Rowlands (변기호) f771b1d5b0 exp run: error out when trying to resume from intermediate checkpoint w/no modifications (#5829)
* apply: allow applying exp with multiple possible branches

* default to new checkpoint branch after applying ambiguous commit

* output shas in addition to exp names by default

* update show tests

* git: allow force checkout in detach_head context

* exp run: error when trying to resume from a non-head checkpoint without
modifications

* add test for apply/resume from intermediate checkpoint

* list some possible heads when multiple matches
2021-04-30 15:12:44 +09:00
Peter Rowlands (변기호) 753803c177 pygit: force release of odb contexts after checkout/merge (#5849) 2021-04-30 10:18:37 +09:00
Ruslan Kuprieiev c9a748421a dependabot: add "maintenance" label (#5898) 2021-04-29 16:59:53 +03:00
Ruslan Kuprieiev cab4126ed4 output: relative paths outside of repo (#5893)
* output: relative paths outside of repo

Fixes #5870

* scm: enhance FileNotInRepo message
2021-04-28 16:13:01 +03:00
Batuhan Taskaya 2172add905 fs: introduce tree.find() (#5879) 2021-04-28 13:39:19 +03:00
Saugat Pachhai b2f71d459b Revert "build(deps): bump pylint-pytest from 1.0.3 to 1.1.2 (#5850)" (#5890)
This reverts commit 8930be06e8.
2021-04-27 13:32:39 +00:00
Saugat Pachhai 111c282808 exp show: separate ui representation of exp names and identifiers (#5885) 2021-04-27 18:37:35 +05:45
Saugat Pachhai 42cf394fde diff: use ui.table for --show-md (#5887) 2021-04-27 17:10:08 +05:45
dependabot[bot] 9c94b6dcaa build(deps): bump google-cloud-storage from 1.37.1 to 1.38.0 (#5883)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.37.1 to 1.38.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.37.1...v1.38.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-27 16:03:08 +05:45
dependabot[bot] a05be55e7e build(deps): bump pylint from 2.8.1 to 2.8.2 (#5884)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.8.1 to 2.8.2.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.8.1...v2.8.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-27 16:02:45 +05:45
Saugat Pachhai 8fd2eba9a2 refactor exp show: use new abstractions (#5880) 2021-04-27 13:14:56 +05:45
Saugat Pachhai f38d163d5c refactor diff and show to use new table ui and new data structure (#5872)
* use new table data structure for diff and show

* fix params diff showing changes

* metrics show: use default precision

* skip precision format for exp diff params

* exp diff: separate tables in --quiet mode

* change precision to signify significants digits

changed f specifier to g, so that we show significant digits
instead of fractional digits. This does changes how precision
worked for metrics/params diff/show.
2021-04-27 11:05:52 +05:45
Saugat Pachhai 0860e419e4 ui: tabular data (#5852)
* introduce tabular data structure

* ignore ellipsis
2021-04-26 14:54:23 +03:00
Saugat Pachhai 2978a5d7e9 dagascii: prune unused pager (#5876) 2021-04-26 14:52:29 +03:00
dependabot[bot] af6921e9ac build(deps): bump pre-commit/action from v2.0.2 to v2.0.3 (#5856)
Bumps [pre-commit/action](https://github.com/pre-commit/action) from v2.0.2 to v2.0.3.
- [Release notes](https://github.com/pre-commit/action/releases)
- [Commits](https://github.com/pre-commit/action/compare/v2.0.2...9b88afc9cd57fd75b655d5c71bd38146d07135fe)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-26 14:08:40 +03:00
dependabot[bot] 8930be06e8 build(deps): bump pylint-pytest from 1.0.3 to 1.1.2 (#5850)
Bumps [pylint-pytest](https://github.com/reverbc/pylint-pytest) from 1.0.3 to 1.1.2.
- [Release notes](https://github.com/reverbc/pylint-pytest/releases)
- [Changelog](https://github.com/reverbc/pylint-pytest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reverbc/pylint-pytest/compare/pylint-pytest-1.0.3...pylint-pytest-1.1.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-26 14:05:33 +03:00
dependabot[bot] 9eba910023 build(deps): bump pytest-mock from 3.5.1 to 3.6.0 (#5874)
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.5.1 to 3.6.0.
- [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.5.1...v3.6.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-26 13:21:58 +03:00
dependabot[bot] 0728777494 build(deps): bump pylint from 2.7.4 to 2.8.1 (#5873)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.7.4 to 2.8.1.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.7.4...pylint-2.8.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-26 13:21:51 +03:00
Jan 54c0675fc6 Add periods (#5867) 2021-04-23 13:58:12 +03:00
Peter Rowlands (변기호) e0d90cdda1 exp run: dvc commit DVC-tracked data deps when stashing an experiment (#5859)
* stage,commit: add support for only committing data stages from
experiments

* exp run: `dvc commit` data deps when stashing exp changes

* add test for modified DVC-tracked data when generating experiment

* test with and without params change

* handle run without target
2021-04-23 11:59:15 +09:00
Batuhan Taskaya 17d7b06f7b output: introduce BaseOutput.transfer (#5861)
* output: introduce BaseOutput.transfer

* add: use BaseOutput.transfer for to-cache
2021-04-22 15:01:44 +03:00
dependabot[bot] 8ac26b58ee build(deps): bump codecov/codecov-action from v1.4.0 to v1.4.1 (#5857)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.4.0 to v1.4.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.4.0...967e2b38a85a62bd61be5529ada27ebc109948c2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-22 13:50:46 +03:00
Batuhan Taskaya a42e363217 objects: don't recompute hash on odb.add() if verify is False (#5860) 2021-04-22 13:50:24 +03:00
Paweł Redzyński d87ace0968 html: enable providing custom html page templates (#5694)
* html: enable providing custom html page templates

* plots: add html template configuration option
2021-04-20 11:02:14 +02:00
Saugat Pachhai d275e6b7b5 cmd: stage list: use ui.table (#5847) 2021-04-20 12:04:26 +05:45
dependabot[bot] 71ee20412b build(deps): bump pyinstaller from 4.2 to 4.3 (#5843)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 4.2 to 4.3.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v4.2...v4.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 07:13:26 +03:00
Batuhan TaskayaandRuslan Kuprieiev 68897aa0de update: only update the missing hashes with --to-remote (#5773)
* objects: merge transfer() with stage()

* objects: drop streamfile

* objects: preserve the signature of get_file_hash

* objects: override hash_jobs if jobs= passed

* objects: support no progress bar directive

* tests: spy on _get_file_hash

* objects: update=... directive

* update: only transfer missing caches with --to-remote

* tests: remove local_remote from the primary test (due to it moves)

* objects: add move=True/False option

* objects: implement _get_file_obj on top of get_file_hash

* objects: use threading on save()

* objects: pass jobs= on to-cache as well

* objects: use in place tqdm

* objects: check for FileExistsError

* Update dvc/objects/__init__.py

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

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-04-19 18:09:35 +03:00
Batuhan Taskaya a8b7fda5a2 metrics: pass --precision to _show_metrics (#5839)
* metrics: pass --precision to _show_metrics

* tests: return empty string
2021-04-19 17:25:45 +03:00
Ruslan Kuprieiev 3472cb1a6a tests: don't set sensitive env secrets for all steps (#5823)
Due to https://about.codecov.io/security-update/ , there is a potential that we've leaked these. This PR takes a more granular approach when setting secret env vars so that this doesn't happen in the future.

We'll change these secrets after this PR is merged.
2021-04-19 16:39:37 +03:00
Peter Rowlands (변기호) 60ea24410e exp: use exp baseline for DVC commands that compare to HEAD during exp run (#5818)
* exp: add util method for getting baseline HEAD during run

* exp: use baseline for dvc commands which compare to HEAD during exp run

* add test for new util method

* update plots diff test mock
2021-04-19 14:42:58 +09:00
Naveen Sinha daf07451f8 Add support for kerberos token based auth (#5809)
* Add support for kerberos token based auth

* Added kerb_ticket as attribute to HDFSFileSystem

* Lint fix for config schema

* Updated black style changes

* Removed staticmethod decorator from hdfs
2021-04-17 10:02:58 +03:00
dependabot[bot] 48890c22b7 build(deps): bump codecov/codecov-action from v1.3.2 to v1.4.0 (#5838)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.3.2 to v1.4.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.3.2...0e28ff86a50029a44d10df6ed4c308711925a6a8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-17 11:20:46 +05:45
Jorge Orpinel 5e92bef733 dvc: add -A flag to * --all-commits (#5783) 2021-04-16 14:19:44 +03:00
Bhagawan Subedi ca7a02d522 erepo: replace copy_tree from disutils with copytree of shutil (#5827)
Fixes: #5817
2021-04-16 15:02:16 +05:45
Peter Rowlands (변기호) 29b9c5370c exp run: disallow --rev for workspace runs (#5831) 2021-04-16 18:02:13 +09:00
Peter Rowlands (변기호) b08a3a8caa exp run: suppress "finished run" output on --dry (#5830) 2021-04-16 16:36:39 +09:00
Ruslan Kuprieiev d02ab4bafd dvc: bump to 2.0.18 2021-04-15 23:19:04 +03:00
Ruslan Kuprieiev 853d304ce2 repo: handle rev without scm (#5822)
This part of logic used to be only used by external repo for cloned
repos, but since we now handle local repos without cloning, we don't
always pass scm instance. This PR automatically creates scm instance
for such cases.

Fixes #5590
2021-04-15 23:16:01 +03:00
Peter Rowlands (변기호) 3143975437 exp show: fix bug where params from commits were merged with workspace (#5813) 2021-04-14 18:02:50 +09:00
Ivan Shcheklein 57904d1fe2 Update README: use mamba instead of conda
More context is here https://github.com/iterative/dvc.org/issues/2370
2021-04-13 12:52:20 -07:00
dependabot[bot] 2472c04873 build(deps): bump actions/cache from v2.1.4 to v2.1.5 (#5805)
Bumps [actions/cache](https://github.com/actions/cache) from v2.1.4 to v2.1.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.4...1a9e2138d905efd099035b49d8b7a3888c653ca8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 14:23:05 +03:00
dependabot[bot] c71a576a7a build(deps): bump actions/setup-python from v2.2.1 to v2.2.2 (#5804)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from v2.2.1 to v2.2.2.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2.2.1...dc73133d4da04e56a135ae2246682783cc7c7cb6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 14:22:57 +03:00
Paweł RedzyńskiandJorge Orpinel c39f91ecf4 params: read from ParamsDependency if possible (#5550)
* params: read from ParamsDependency if possible

* params: introduce include_untracked flag

* params: include untracked upon targets

* experiments: add option to show tracked params

* fixing test

* params: rename tracked to deps

* params: roll back to default params.yaml behavior

* experiments: add option to show only used params on diff

* Update dvc/command/experiments.py

Co-authored-by: Jorge Orpinel <[email protected]>

* Update dvc/command/experiments.py

Co-authored-by: Jorge Orpinel <[email protected]>

* Update dvc/command/params.py

Co-authored-by: Jorge Orpinel <[email protected]>

Co-authored-by: Jorge Orpinel <[email protected]>
2021-04-12 17:08:02 +02:00
dependabot[bot] 7e5d32c2c4 build(deps): bump moto from 2.0.4 to 2.0.5 (#5799)
Bumps [moto](https://github.com/spulec/moto) from 2.0.4 to 2.0.5.
- [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.4...2.0.5)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 15:56:39 +05:45
Batuhan Taskaya 353e4cf374 fsspec: update to 0.9 (including remotes) (#5788) 2021-04-09 14:12:25 +03:00
Saugat Pachhai fed3b2c0cc ui: unify tables (#5756) 2021-04-09 13:51:49 +03:00
Paweł Redzyński eccc320606 remote: use configured jobs number (#5747) 2021-04-08 17:52:42 +03:00
Peter Rowlands (변기호) a984f95cbd exp run: jobs should default to 1 (#5785) 2021-04-08 19:27:48 +09:00
dependabot[bot] 02d9684c33 build(deps): bump codecov/codecov-action from v1.3.1 to v1.3.2 (#5770)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.3.1 to v1.3.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.3.1...9b0b9bbe2c64e9ed41413180dd7398450dfeee14)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-07 19:14:40 +05:45
dependabot[bot] 9a9e2ed274 build(deps): bump google-cloud-storage from 1.37.0 to 1.37.1 (#5771)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.37.0 to 1.37.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.37.0...v1.37.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-07 18:35:54 +05:45
Peter Rowlands (변기호) f35cf7ea1d exp run: --reset no longer implies --force (#5751)
* exp run: --reset no longer implies --force

* ref update still needs to be forced on checkpoint --reset
2021-04-06 22:10:38 +09:00
Ruslan Kuprieiev 4f8cfc025b gha: remove leftovers 2021-04-06 14:19:25 +03:00
Ruslan Kuprieiev cd98761e63 Delete debian directory
Closes #2800
2021-04-06 14:06:09 +03:00
Ruslan Kuprieiev 14f05073ad gha: packages: don't build snap (#5774)
* Delete snap directory

* gha: packages: remove snap
2021-04-06 13:59:09 +03:00
f74b63806e azure: use account_name with default credentials (#5725)
* Fix DefaultAzureCredential usage

* Fix typo and printout

* Update dvc/fs/azure.py

Co-authored-by: Batuhan Taskaya <[email protected]>

* Disable logging spam

* Run precommit hook

Co-authored-by: Charlie Hewitt <[email protected]>
Co-authored-by: Batuhan Taskaya <[email protected]>
2021-04-06 12:48:15 +03:00
Batuhan Taskaya ddb1ece283 setup: fix all fsspec remote versions (#5769) 2021-04-06 10:54:13 +03:00
dependabot[bot]andSaugat Pachhai 9e7e01ce9f build(deps): bump pre-commit/action from v2.0.0 to v2.0.2 (#5767)
* build(deps): bump pre-commit/action from v2.0.0 to v2.0.1

Bumps [pre-commit/action](https://github.com/pre-commit/action) from v2.0.0 to v2.0.1.
- [Release notes](https://github.com/pre-commit/action/releases)
- [Commits](https://github.com/pre-commit/action/compare/v2.0.0...b54880c283dfc80a3fc1baf61807e8ae84471361)

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

* Update .github/workflows/tests.yaml

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai <[email protected]>
2021-04-05 22:58:34 +05:45
Saugat Pachhai 0206812d1b Fix test assertion with environment when using pyenv (#5765) 2021-04-05 22:56:05 +05:45
David de la Iglesia CastroandPeter Rowlands c634577463 exp show: Allow for wildcard patterns in include and exclude params/m… (#5720)
* exp show: Allow for wildcard patterns in include and exclude params/metrics

Fixes #5642

* Add test case for nested metrics/params

* match full string in _filter_name

* pre-commit

* Update dvc/command/experiments.py

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

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
2021-04-05 20:52:29 +09:00
dependabot[bot] 1a995eafce build(deps): bump pytest from 6.2.2 to 6.2.3 (#5764)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.2 to 6.2.3.
- [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.2...6.2.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-05 15:35:50 +05:45
Saugat PachhaiandSaugat Pachhai 19d0b2af56 use sys.executable instead of hardcoded python (#5763)
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2021-04-03 07:22:07 +00:00
Saugat Pachhai 7b25a9de24 Update pycryptodome to 3.10 (#5762)
3.10 has wheels for Python3.9 in Windows.
2021-04-03 12:10:24 +05:45
Ruslan Kuprieiev 2049a9dbdd dvc: bump to 2.0.17 2021-04-02 15:57:17 +03:00
Ruslan Kuprieiev ba38bb300c sign: increase timeout to 180 sec 2021-04-02 15:56:56 +03:00
Batuhan Taskaya e78375e3d9 setup: bump adlfs to 0.7 (#5760) 2021-04-02 13:35:15 +03:00
Ruslan Kuprieiev 585ff36486 dvc: bump to 2.0.16 2021-04-01 21:34:52 +03:00
Ruslan Kuprieiev b2503d5178 pyinstaller: hooks: bring back hook-googleapiclient.model.py
Got deleted by mistake in https://github.com/iterative/dvc/pull/5682/files
2021-04-01 21:34:26 +03:00
dependabot[bot] 32b342b1e4 build(deps): bump moto from 2.0.3 to 2.0.4 (#5755)
Bumps [moto](https://github.com/spulec/moto) from 2.0.3 to 2.0.4.
- [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.3...2.0.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-01 14:16:56 +05:45
Peter Rowlands (변기호) 66caa345b1 git: verify commit user is set (#5754) 2021-04-01 17:04:45 +09:00
Peter Rowlands (변기호) 17e5da7759 exp show: fix column collapse behavior (#5753) 2021-04-01 16:57:09 +09:00
Peter Rowlands (변기호) aa74712576 exp show: fix queued exp display (#5752) 2021-04-01 16:24:01 +09:00
Ruslan Kuprieiev c4622c6311 sign: increase timeout to 90 2021-04-01 02:11:28 +03:00
Ruslan Kuprieiev 2f759d60be sign: increase timeout 2021-04-01 01:41:36 +03:00
c3a61ad9cf Added SSL verification option (#5732)
* Added SSL verification option

* Changed param name to avoid conflicts

* verify_ssl now accepts strings

* Added simple unit tests

* Changed parameter name fron verify_ssl to ssl_verify so it's consistent with HTTP remotes

Forget to change 1 test

* add processing on ssl_verify param

typo

* revert back to bool param

* Update dvc/config_schema.py

Co-authored-by: Nigel <[email protected]>
Co-authored-by: Nigel <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-03-31 23:31:38 +03:00
Saugat Pachhai 7c7e73e3d2 status: collect stages only once (#5741)
This is achieved by using `lock_repo`
context manager that can be used in a recursive depth
that only resets stages when it has left any lock_repo
blocks.

Also added some ui related tests for the status which I found
missing
2021-03-31 18:31:27 +03:00
Saugat Pachhai 098a519305 webdav: use root url to check and on mkdir to find recurse depth (#5742) 2021-03-31 15:46:49 +03:00
Peter Rowlands (변기호) 4d4f9427d1 dvcignore: ignore file '.git' by default (#5745)
* dvcignore: ignore file '.git' by default

* update tests
2021-03-31 13:47:34 +03:00
Peter Rowlands (변기호) e91168812a build: fix fpm build zsh-completion path (#5743) 2021-03-31 16:45:34 +09:00
dependabot[bot] 57c082e41b build(deps): bump moto from 2.0.2 to 2.0.3 (#5744)
Bumps [moto](https://github.com/spulec/moto) from 2.0.2 to 2.0.3.
- [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.2...2.0.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-31 13:22:44 +05:45
Ruslan Kuprieiev 6be2efc9cf dvc: bump to 2.0.15 2021-03-31 00:06:33 +03:00
Saugat Pachhai 3b3b5c8997 parsing: resolve relative paths when loading parameters (#5734)
* parsing: resolve relative paths when loading parameters

* test with git commits as well
2021-03-30 23:50:04 +03:00
Saugat Pachhai b5f31daab0 ui: Use sys.stdout and sys.stderr dynamically (#5729)
Before, we used to set it during initialization. But this
made it hard to test using capsys as capsys changes the
reference to sys.stdout and sys.stderr with the mocked one
during the test. But since we are using dvc.ui globally and
setting it to a real sys.stdout and sys.stderr before capsys
comes into effect, testing was not possible with it.

Now, by using property hack, we will be using the values of
sys.stdout and sys.stderr during the time of write, so the mocking
would work and would be easier to test.
2021-03-30 23:44:46 +03:00
Alex Lopez 4e7f924e8b dvc: fix breakage with rich==10.0.0 (#5733)
* dvc: fix breakage with rich==10.0.0

Change in the arguments of __rich_measure__.

Fixes #5722

* dvc: Fix linting breakage from rich==10.0.0

Fixes: #5733

* dvc: set lower bound for rich dependency to 10.0.0

Fixes #5722
2021-03-30 20:57:40 +03:00
Ruslan Kuprieiev 09cb495423 dvc: bump to 2.0.14 2021-03-30 15:44:20 +03:00
Peter Rowlands (변기호) 262816c20e git: update dulwich to 0.20.21 (#5730) 2021-03-30 21:23:14 +09:00
dependabot[bot] a1aab35f99 build(deps): bump pylint from 2.7.2 to 2.7.4 (#5726)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.7.2 to 2.7.4.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.7.2...pylint-2.7.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-30 15:07:25 +05:45
Ruslan Kuprieiev 7d1a35388e dvc: bump to 2.0.13 2021-03-29 16:56:06 +03:00
Ruslan Kuprieiev 98edde06a4 sign: increase timeout 2021-03-29 16:27:05 +03:00
Ruslan Kuprieiev 30f9ad4b54 sign: add timeout and verbosity 2021-03-29 15:54:32 +03:00
Saugat Pachhai 547db15d34 Add test_requirements.txt to the manifest (#5724) 2021-03-29 12:56:02 +05:45
Saugat Pachhai f6b04c54e0 deps: limit rich version to <10 (#5723)
The recent release of rich 10.0.0 broke some internal APIs
that we were depending on for exp show.
See https://github.com/iterative/dvc/issues/5722
2021-03-29 12:09:02 +05:45
Ruslan Kuprieiev 1a25ebe3bd osxpkg: notarize: fix typo 2021-03-29 01:07:06 +03:00
Ruslan Kuprieiev 813f4d1f07 dvc: bump to 2.0.12 2021-03-27 00:13:30 +02:00
Saugat Pachhai 0a2e9f98d5 dvc: update precommit hooks (#5716)
* update pre-commit hooks

* fix mypy errors

1. Casting `columns` to the custom Column with collapse.
2. Ignoring typedef difference for RichTable._collapse_widths.
3. Suppressing error with conditional paramiko imports
   https://github.com/python/mypy/issues/1153
2021-03-26 22:41:56 +05:45
Saugat Pachhai f8c56742c5 pin urllib3 to 1.26.4 (#5714) 2021-03-26 21:29:08 +05:45
dependabot[bot]andSaugat Pachhai 7b922d0ec3 build(deps): bump pytest-mock from 1.11.2 to 3.5.1 (#5705)
* build(deps): bump pytest-mock from 1.11.2 to 3.5.1

Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 1.11.2 to 3.5.1.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v1.11.2...v3.5.1)

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

* change spy.return_value to spy.spy_return

This was introduced in pytest-mock 2.0.0
https://github.com/pytest-dev/pytest-mock/commit/78499644cb800f8fe65a5c17e81d168fa2a4c6b9

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2021-03-26 21:19:37 +05:45
dependabot[bot]andSaugat Pachhai fae57091b0 build(deps): update pytest requirement from <6.2.2,>=6.0.1 to ==6.2.2 (#5706)
* build(deps): update pytest requirement

Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [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.0.1...6.2.2)

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

* Pin test_requirements.txt to 6.2.2

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai <[email protected]>
2021-03-26 21:02:20 +05:45
Saugat Pachhai cd9098211f remove pytest-tap (#5713) 2021-03-26 20:43:57 +05:45
dependabot[bot] 7b0ecad660 build(deps): bump google-cloud-storage from 1.19.0 to 1.37.0 (#5711)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.19.0 to 1.37.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.19.0...v1.37.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-26 14:27:12 +00:00
dependabot[bot] 27c8fb2872 build(deps): update urllib3 requirement from <1.26.0 to <1.27.0 (#5707)
Updates the requirements on [urllib3](https://github.com/urllib3/urllib3) to permit the latest version.
- [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/0.3...1.26.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-26 20:06:01 +05:45
dependabot[bot]andSaugat Pachhai 5ca813024f build(deps): bump moto from 1.3.16.dev122 to 2.0.2 (#5709)
* build(deps): bump moto from 1.3.16.dev122 to 2.0.3.dev12

Bumps [moto](https://github.com/spulec/moto) from 1.3.16.dev122 to 2.0.3.dev12.
- [Release notes](https://github.com/spulec/moto/releases)
- [Changelog](https://github.com/spulec/moto/blob/master/CHANGELOG.md)
- [Commits](https://github.com/spulec/moto/commits)

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

* Update test_requirements.txt

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai <[email protected]>
2021-03-26 20:05:32 +05:45
Batuhan Taskaya 042b1c7f7d pull: skip isdir() checks on downloading files (#5696)
* pull: use download_file

* tree: implement filesystem.download_file
2021-03-26 15:22:45 +02:00
dependabot[bot] 61e470b85e build(deps): bump jaraco-windows from 3.9.2 to 5.5.0 (#5700)
Bumps [jaraco-windows](https://github.com/jaraco/jaraco.windows) from 3.9.2 to 5.5.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/3.9.2...v5.5.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-26 18:41:44 +05:45
dependabot[bot] c3bf0e1cb5 build(deps): bump actions/setup-python from v2.1.4 to v2.2.1 (#5702)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from v2.1.4 to v2.2.1.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v2.1.4...3105fb18c05ddd93efea5f9e0bef7a03a6e9e7df)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-26 18:40:55 +05:45
dependabot[bot] cdb5d668da build(deps): bump actions/upload-release-asset from v1.0.1 to v1.0.2 (#5704)
Bumps [actions/upload-release-asset](https://github.com/actions/upload-release-asset) from v1.0.1 to v1.0.2.
- [Release notes](https://github.com/actions/upload-release-asset/releases)
- [Commits](https://github.com/actions/upload-release-asset/compare/v1.0.1...e8f9f06c4b078e705bd2ea027f0926603fc9b4d5)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-26 12:48:57 +00:00
dependabot[bot] 81d0d7aae7 build(deps): bump beautifulsoup4 from 4.4.0 to 4.9.3 (#5703)
Bumps [beautifulsoup4](http://www.crummy.com/software/BeautifulSoup/bs4/) from 4.4.0 to 4.9.3.

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-26 12:45:32 +00:00
dependabot[bot] f1375fd706 build(deps): bump actions/cache from v2 to v2.1.4 (#5699)
Bumps [actions/cache](https://github.com/actions/cache) from v2 to v2.1.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2...26968a09c0ea4f3e233fdddbafd1166051a095f6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-26 12:40:45 +00:00
dependabot[bot] d04aaf4ae1 build(deps): bump actions/checkout from v2.3.2 to v2.3.4 (#5698)
Bumps [actions/checkout](https://github.com/actions/checkout) from v2.3.2 to v2.3.4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.2...5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-26 12:40:26 +00:00
dependabot[bot] 191b8c1a6d build(deps): bump codecov/codecov-action from v1.0.13 to v1.3.1 (#5701)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.0.13 to v1.3.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.0.13...fcebab03f26c7530a22baa63f06b3e0515f0c7cd)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-26 12:40:17 +00:00
Saugat Pachhai 8c90771388 Add dependabot to update github-actions and python requirements (#5697) 2021-03-26 18:20:10 +05:45
Saugat Pachhai 1931a6ec1e Move tests requirements to test_requirements.txt (#5674)
Plan is to add dependabot to update those packages required for testing
2021-03-26 14:13:43 +02:00
Ruslan Kuprieiev 4ffa0675b4 build: fix --notarize 2021-03-26 03:47:34 +02:00
Ruslan Kuprieiev c0671a2d9b gha: don't upload pkg artifacts 2021-03-26 03:18:29 +02:00
Ruslan Kuprieiev f8498d6340 gha: don't build packages for PRs 2021-03-26 03:17:22 +02:00
Ruslan Kuprieiev 27b589f1b9 dvc: bump to 2.0.11 2021-03-26 03:06:36 +02:00
Ruslan Kuprieiev 49cdc2bea3 build: add osx signing/notarization (#5695)
* build: add osx signing/notarization

* gha: temporarily enable pkg builds
2021-03-26 02:31:32 +02:00
Ruslan Kuprieiev a3a55339ab dvc: bump to 2.0.10 2021-03-25 02:21:52 +02:00
Ruslan Kuprieiev 8abffbf68b scripts: fpm: install zsh competion to install / "share"
Currently we create /usr/share/... which is not allowed by macos:

> the package is trying to install content to the system volume
2021-03-25 02:21:11 +02:00
Ruslan Kuprieiev 2e76dabbaa dvc: bump to 2.0.9 2021-03-24 20:30:48 +02:00
Ruslan Kuprieiev 8bebed93dd build: osxpkg: build lxml (#5687)
lxml wheels are built with an old SDK, which breaks osxpkg notarization:

```
	* error for path "dvc-2.0.7.pkg/dvc-2.0.7.pkg Contents/Payload/usr/local/lib/dvc/lxml/etree.cpython-37m-darwin.so": The binary uses an SDK older than the 10.9 SDK.
```
2021-03-24 20:30:03 +02:00
Ruslan Kuprieiev 58a5442d71 dvc: bump to 2.0.8 2021-03-24 19:20:04 +02:00
Saugat Pachhai 76ece8c64a plots: quote and resolve paths properly (#5670)
This commit fixes 3 things:
1. The path printed on the terminal is properly is quoted.  This was
problematic when the output is piped to `xdg-open` if the path contained
spaces or quotes or special characters.

2. The path is resolved. This is not an issue at all, but makes the
output nicer.

3. The path with which the browser is opened with uses relative path.
This is done to solve issue in WSL, that the absolute path is not
accessible outside the WSL for host programs as the linux filesystem
is available on `/wsl$` path.
2021-03-24 20:27:24 +05:45
Saugat Pachhai 8b6d655ebe use global UI object for printing (#5653) 2021-03-24 17:22:17 +05:45
Ruslan Kuprieiev f2ba67aece build: upgrade pyinstaller to 4.2 (#5682)
* build: upgrade pyinstaller to 4.2

* pyinstaller: hooks: remove hook-googleapiclient.model.py

https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/src/_pyinstaller_hooks_contrib/hooks/stdhooks/hook-googleapiclient.model.py

* gha: use python 3.8 for pyinstaller
2021-03-24 09:43:05 +02:00
Ruslan Kuprieiev 19cdef1675 build: separate building and packaging (#5675)
* build: separate pyinstaller and fpm/innosetup

* gha: upload deb/rpm/osxpkg/exe artifacts

* build: dynamically generate dvc version
2021-03-23 19:55:44 +02:00
Saugat Pachhai dc3fe6fbec Update pylint-pytest to 1.0.3 (#5673) 2021-03-23 15:28:33 +05:45
Ruslan Kuprieiev 619c55d399 dvc: bump to 2.0.7 2021-03-22 15:29:52 +02:00
Alex Ford b3bfa346c3 Using existing logging level if defined in addLoggingLevel (#5632)
* Use existing level if defined in addLoggingLevel

Fixes https://github.com/iterative/dvc/issues/4546

* Add addLoggingLevel test

* Format fix.
2021-03-22 15:15:16 +02:00
Saugat Pachhai 0a8927e3d8 diff: clear calculated renamed files from inverted hashmap (#5652)
* diff: clear calculated renamed files from inverted hashmap

We were trying to reuse the first encounter of file from inverted hashmap again and again, which was throwing ValueError

* refactor

* remove ordering

* add tests
2021-03-22 15:13:52 +02:00
Batuhan Taskaya 35597ed83d fsspec: skip instance cache on all filesystems (#5668) 2021-03-22 14:36:21 +02:00
Batuhan Taskaya 21fc06e947 fsspec: support raw buckets on path splitting (#5658) 2021-03-22 02:01:06 +02:00
Saugat Pachhai f2b216261e move Repo.diff related test to diff (#5654) 2021-03-19 16:58:21 +05:45
Batuhan Taskaya 6026573860 update: use unresolved path with --to-remote calls (#5631) 2021-03-17 16:48:00 +02:00
Ruslan Kuprieiev dcc55aa645 dvc: bump to 2.0.6 2021-03-17 00:01:10 +02:00
Batuhan Taskaya 862e18488c gs: process etag when it is available (#5634) 2021-03-16 14:08:59 +02:00
Saugat Pachhai 0b810fb256 Advertise Python 3.9 support in classifiers 2021-03-16 13:37:37 +05:45
Helio MachadoandPeter Rowlands 4ed55d0051 build: add pyinstaller hook for googleapiclient (#5619)
* build: add pyinstaller hook for googleapiclient

The Google API Discovery Service JSON files from
googleapiclient/discovery_cache/documents were being
excluded due to a bug and, particularly, drive.v2.json
is essential for our gdrive backend. Now we're explicitly
asking PyInstaller to embed these files on the generated packages.

Fixes #5618

* Suppress linter error for unrecognized imports

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

* Apply black style after manual code review

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
2021-03-14 12:56:49 +02:00
nik123 6ac7def6f1 setup: remove duplicate 'pytest-cov' dependency (#5617)
* setup: remove duplicate 'pytest-cov' dependency

* setup: returned pytest-cov version
2021-03-14 14:08:11 +05:45
Peter Rowlands (변기호) 8c1d46e502 checkpoints: completely remove checkpoint outs on exp run --reset (#5586)
* checkpoints: remove checkpoint outs on --reset

* exp run: no longer prune/reset lockfiles

* update tests

* make --queue imply --reset unless --rev is provided
2021-03-13 01:29:50 +09:00
Batuhan Taskaya 11b0581536 import-url: support importing empty directories on object storage services (#5536)
* import-url: support importing empty directories on object storage services

Empty directories are basically files with a trailing slash at the end of their names,
with empty contents on object service providers (such as azure, google storage). This
patch implements the `is_empty()` method for the `FSSpecWrapper` to determining whether
a path is empty (directory or a file) and also tweaks `.isdir()` in order to see empty
directories.

The base class is only used by azure and gcsfs for now, but if we go on the route of
migrating normal providers (like proper filesystems, not just object storages), we might
need to do these tweaks on a subclass (like `ObjectStorageFSSpecWrapper` etc), but I
don't think it is neccesary as of now.

The `is_empty` method is already there, and also used in the importing part to
warn users that they are importing something empty (https://github.com/iterative/dvc/blob/e385e59a098cd661ea1ea4257c9a5735c5c32bc6/dvc/output/base.py#L252-L254)
so this is not a new method. Just for the reference, as far as I can locate there
is no variant of this method in the fsspec.

* tests: revert custom mkdir for cloud storages

* tests: allign assingments

* add some comments
2021-03-12 18:11:08 +02:00
Batuhan Taskaya abcb5b2f6f gs: disable consistency checker (#5613) 2021-03-12 14:04:54 +02:00
Peter Rowlands (변기호) d365e6d5b4 exp run: fix issue where duplicate workspace runs would incorrectly conflict (#5611)
* exp run: gracefully handle duplicate workspace runs

* exp run: fix issue where errors could be logged twice

* move ref conflict check into its own function
2021-03-12 20:13:49 +09:00
Peter Rowlands (변기호) bd470e9586 stage add: unsuppress -c/--checkpoints help text (#5599)
* stage add: unsuppress -c/--checkpoints help text

* review fix
2021-03-12 14:47:12 +09:00
Peter Rowlands (변기호) fe4cb9c439 exp run: do nothing if result matches parent (HEAD) (#5600) 2021-03-12 14:09:03 +09:00
Ruslan Kuprieiev 2db55032c4 objects: file: remove unused load() method (#5610) 2021-03-12 05:35:48 +02:00
Peter Rowlands (변기호) 686898240c Revert "repro/exp run: use git status when reminding to git-track files (#5544)" (#5596)
This reverts commit ce2d7c65db.
2021-03-12 11:50:13 +09:00
Saugat Pachhai d245008987 plots: support opening plots file directly in the browser (#5584)
* plots: support opening plots file directly in the browser

* add tests

* fix path on windows tests
2021-03-12 04:23:07 +02:00
Ruslan Kuprieiev 29ad87ebe2 objects: split into separate files (#5609) 2021-03-12 04:12:46 +02:00
Ruslan Kuprieiev 4af3b3d20d dvc: refactor objects (#5607)
* dvc: get rid of DirInfo

DirInfo was created before we had proper objects, as a way to have a
structure that represents a staged directory. Right now we have a
dedicated Tree object that is specifically meant for that.

As a result, HashInfo no longer needs to have dir_info and we no longer
need `get_hash/get_dir_hash` helpers.

* objects: move Tree into tree.py
2021-03-12 01:10:34 +02:00
Saugat Pachhai b691b15007 Add a link to help debugging more information regarding the issue (#5601)
* Add a link to help debugging more information regarding the issue

* Update .github/ISSUE_TEMPLATE/bug_report.md
2021-03-11 16:02:53 +02:00
Saugat Pachhai fd66ef6bc1 http: fix info() if there is no content-length header (#5602) 2021-03-11 16:01:24 +02:00
Saugat Pachhai e709437cd2 update pylint and pin dependencies (#5603)
Started having some issues with pylint-pytest, pinning it as well
2021-03-11 15:24:27 +05:45
Saugat Pachhai 6c00fedb46 remove flaky-service
Looks like the project is dead: https://github.com/GoogleCloudPlatform/flaky-service.

And, the site https://flaky.dev seems to be dead.
2021-03-11 15:21:12 +05:45
Peter Rowlands (변기호) cc9ded2081 exp: subclass tempfile.TemporaryDirectory to handle cleanup (#5598)
* exp: subclass tempfile.TemporaryDirectory to handle cleanup

* override cleanup() since cls._rmtree does not exist in <3.8
2021-03-11 14:54:36 +09:00
Dmitry Petrov 5b300d8aae Add a link to Barrak&Adams citation (#5592) 2021-03-11 00:01:05 +02:00
Ruslan Kuprieiev cf1070a531 dvc: bump to 2.0.5 2021-03-10 11:33:47 +02:00
Laurens Duijvesteijn 9f224ecde3 scm: add force parameter to checkout() (#5582)
The `GitPythonBackend.checkout()` method already implicitly supported
this by forwarding `**kwargs` to the backend. `pygit2` didn't support
this yet.

This patch fixes this inconsistency.
2021-03-10 14:42:48 +09:00
Ruslan Kuprieiev 8433b36266 dvc: bump to 2.0.4 2021-03-09 17:59:26 +02:00
Batuhan Taskaya 3e75fe99e3 gdrive: change the temporary directory location to .dvc/tmp (#5583) 2021-03-09 17:44:48 +02:00
Batuhan Taskaya b221c16c89 pull: proper path join for partial pulls (#5579)
* pull: proper path join for partial pulls

* use os.path.join in the tests for windows
2021-03-09 15:36:44 +02:00
Dmitry Petrov 7ae2d37116 Add Barrak, Eghan & Adam citation (#5573) 2021-03-09 14:50:42 +02:00
Laurens Duijvesteijn e0a0342ab3 fs: ssh: pass timeout to paramiko's connect() (#5569)
Prior to this commit, DVC could hang indefinitely when connecting to
remote SSH filesystems. This should address this.
2021-03-08 22:59:26 +02:00
Peter Rowlands (변기호) d68f6d3696 pygit/dulwich: implement stash functionality (#5286)
* pygit2: disable set_ref with reflog message

* git: implement is_dirty in dulwich

* pygit2: implement _stash_apply

* pygit: update to 1.5.0

* enable set_ref(message=...) for pygit2

* git: replace backend reflog_delete in favor of _stash_drop

* pygit: support stash_push with custom refs

* update tests for backend stash_drop behavior

* pygit: raise MergeConflictError on failed stash apply

* pygit: reload index from disk before stash apply

* update stash_apply checkout strategy

* mark flaky test
2021-03-08 17:24:35 +02:00
Ruslan Kuprieiev b4232bdc7c state: use pickle v4 (#5571)
Kudos @skshetry for the discovery, research and fix.

Fixes #5563
2021-03-08 17:00:40 +02:00
Batuhan Taskaya f7d8761734 gdrive: use the normal file backend for saved credentials (#5554)
* gdrive: sync with pydrive's save backend changes

* bump pydrive version

* better temporary credentials managements for service accounts

* proper temp var management
2021-03-08 16:30:05 +02:00
Ruslan Kuprieiev 70c445d1ee utils: makedirs: ignore chmod errors (#5570)
Same as with `protect()`, these errors might happen if fs is immutable
or doesn't support chmod()-ing. We can safely ignore these.

Fixes #5552
2021-03-08 16:28:02 +02:00
Ruslan Kuprieiev 47bcb02ef1 fs: git: use pygit2 (#5559) 2021-03-06 02:54:13 +02:00
Ruslan Kuprieiev 4a75067f4c github: template: use dvc doctor instead of dvc version 2021-03-05 16:08:46 +02:00
Ruslan Kuprieiev 70f4c44ec6 dvc: bump to 2.0.3 2021-03-05 13:08:22 +02:00
af37527560 setup: rename azure.indentity to azure-identity (#5548)
* simple like that

* Update dvc/version.py

Co-authored-by: Marc-Antoine Belanger <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-03-05 01:43:32 +02:00
391 changed files with 10037 additions and 7195 deletions
+1
View File
@@ -13,3 +13,4 @@ exclude_lines =
raise NotImplementedError
# pragma: no cover
# pragma: no cover
\.\.\.$
+1
View File
@@ -0,0 +1 @@
* @iterative/dvc
+6 -3
View File
@@ -48,14 +48,17 @@ A clear and concise description of what you expect to happen.
This is required to ensure that we can reproduce the bug.
-->
**Output of `dvc version`:**
**Output of `dvc doctor`:**
```console
$ dvc version
$ dvc doctor
```
**Additional Information (if any):**
<!--
Please check https://github.com/iterative/dvc/wiki/Debugging-DVC on ways to gather more information regarding the issue.
If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
-->
If the issue is regarding the performance, please attach the profiling information and the benchmark comparisons.
-->
+16
View File
@@ -0,0 +1,16 @@
version: 2
updates:
- directory: "/"
package-ecosystem: "pip"
schedule:
interval: "daily"
labels:
- "maintenance"
- directory: "/"
package-ecosystem: "github-actions"
schedule:
interval: "daily"
labels:
- "maintenance"
+84 -71
View File
@@ -6,91 +6,130 @@ on:
release:
types: [released, prereleased]
jobs:
linux:
runs-on: ubuntu-16.04
deb:
runs-on: ubuntu-18.04
steps:
- uses: actions/[email protected].2
- name: Set up Python 3.7
uses: actions/setup-python@v2.1.4
- uses: actions/[email protected].4
- name: Set up Python 3.8
uses: actions/setup-python@v2.2.2
with:
python-version: 3.7
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: Build
run: ./scripts/build_posix.sh
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build deb
run: python scripts/build.py deb
- name: Publish deb
if: github.event_name == 'release'
uses: actions/[email protected].1
uses: actions/[email protected].2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_path: scripts/fpm/dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_name: dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_content_type: binary/octet-stream
- name: Publish rpm
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_name: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_content_type: binary/octet-stream
mac:
runs-on: macos-10.15
rpm:
runs-on: ubuntu-18.04
steps:
- uses: actions/[email protected].2
- name: Set up Python 3.7
uses: actions/setup-python@v2.1.4
- uses: actions/[email protected].4
- name: Set up Python 3.8
uses: actions/setup-python@v2.2.2
with:
python-version: 3.7
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: Build
run: ./scripts/build_posix.sh
- name: Publish
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build rpm
run: python scripts/build.py rpm
- name: Publish rpm
if: github.event_name == 'release'
uses: actions/[email protected].1
uses: actions/[email protected].2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dvc-${{ github.event.release.tag_name }}.pkg
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_name: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_content_type: binary/octet-stream
osxpkg:
runs-on: macos-10.15
steps:
- uses: actions/[email protected]
- name: Set up Python 3.8
uses: actions/[email protected]
with:
python-version: 3.8
- name: Set up Ruby 2.4
uses: actions/[email protected]
with:
ruby-version: 2.4
- name: Install deps
run: |
# available lxml (webdav dependency) wheels are built with an old
# SDK, which breaks osxpkg notarization. Building from scratch
# instead.
pip install .[all] --no-binary lxml
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build
run: python scripts/build.py osxpkg
- name: Publish
if: github.event_name == 'release'
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}.pkg
asset_name: dvc-${{ github.event.release.tag_name }}.pkg
asset_content_type: binary/octet-stream
windows:
exe:
runs-on: windows-2019
steps:
- uses: actions/[email protected].2
- name: Set up Python 3.7
uses: actions/setup-python@v2.1.4
- uses: actions/[email protected].4
- name: Set up Python 3.8
uses: actions/setup-python@v2.2.2
with:
python-version: 3.7
python-version: 3.8
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
- name: Pull images
run: |
dvc pull
- name: Build
run: scripts\build_windows.cmd
run: python scripts\build.py exe
shell: cmd
- name: Publish
if: github.event_name == 'release'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/[email protected].1
uses: actions/[email protected].2
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dvc-${{ github.event.release.tag_name }}.exe
asset_path: scripts/innosetup/dvc-${{ github.event.release.tag_name }}.exe
asset_name: dvc-${{ github.event.release.tag_name }}.exe
asset_content_type: binary/octet-stream
python:
runs-on: ubuntu-16.04
pip:
runs-on: ubuntu-18.04
steps:
- uses: actions/[email protected].2
- uses: actions/[email protected].4
- name: Set up Python 3.7
uses: actions/setup-python@v2.1.4
uses: actions/setup-python@v2.2.2
with:
python-version: 3.7
- name: Install
@@ -103,29 +142,3 @@ jobs:
with:
user: __token__
password: ${{ secrets.PYPI_TOKEN }}
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- name: Build the package
uses: snapcore/action-build@v1
id: snapcraft
# - name: Test the package
# run: |
# sudo snap install --dangerous --classic ${{ steps.snapcraft.outputs.snap }}
# dvc version
# sudo snap remove dvc
- name: Publish to the edge channel
uses: snapcore/action-publish@v1
if: github.event_name == 'push'
with:
store_login: ${{ secrets.STORE_LOGIN }}
snap: ${{ steps.snapcraft.outputs.snap }}
release: edge,v2/edge
- name: Publish to the stable channel
uses: snapcore/action-publish@v1
if: ${{ github.event_name == 'release' && github.event.release.released }}
with:
store_login: ${{ secrets.STORE_LOGIN }}
snap: ${{ steps.snapcraft.outputs.snap }}
release: stable,v2/stable
+43 -24
View File
@@ -7,34 +7,34 @@ on:
push:
branches:
- master
schedule:
- cron: '5 1 * * *' # every day at 01:05
env:
DVC_TEST: "true"
HOMEBREW_NO_AUTO_UPDATE: 1
SHELL: /bin/bash
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 }}
GS_CREDS_JSON: ${{ secrets.GS_CREDS_JSON }}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected].2
- uses: actions/[email protected].4
- name: Set up Python 3.8
uses: actions/setup-python@v2.1.4
uses: actions/setup-python@v2.2.2
with:
python-version: 3.8
- name: Install requirements
run: pip install ".[all,tests]" Pygments collective.checkdocs pre-commit
run: |
pip install wheel
pip install ".[all,tests]" Pygments collective.checkdocs pre-commit
- name: install temporary dependencies
run: |
pip install git+https://github.com/isidentical/gdrivefs@service-account
pip install git+https://github.com/isidentical/pydata-google-auth@service-account
- name: Check README
run: python setup.py checkdocs
- uses: pre-commit/[email protected].0
- uses: pre-commit/[email protected].3
tests:
runs-on: ${{ matrix.os }}
strategy:
@@ -51,9 +51,9 @@ jobs:
pyv: "3.9"
steps:
- uses: actions/[email protected].2
- uses: actions/[email protected].4
- name: Set up Python
uses: actions/setup-python@v2.1.4
uses: actions/setup-python@v2.2.2
with:
python-version: ${{ matrix.pyv }}
- name: get pip cache dir
@@ -62,7 +62,7 @@ jobs:
echo "::set-output name=dir::$(pip cache dir)"
- name: set pip cache
id: pip-cache
uses: actions/cache@v2
uses: actions/cache@v2.1.6
with:
path: ${{ steps.pip-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
@@ -83,25 +83,44 @@ jobs:
git config --global user.name "DVC Tester"
- name: setup gs creds
shell: bash
env:
GS_CREDS_JSON: ${{ secrets.GS_CREDS_JSON }}
if: env.GS_CREDS_JSON != ''
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
run: python -m tests --all --cov-report=xml --cov-report=term --tap-combined -n=4
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 -n=4
--cov-report=xml --cov-report=term
--all ${{ env.extra_test_args }}
- name: upload coverage report
uses: codecov/codecov-action@v1.0.13
uses: codecov/codecov-action@v1.5.2
with:
file: ./coverage.xml
fail_ci_if_error: false
- name: Post TAP
# Running custom fork with an option, that does not fail CI on error
uses: skshetry/flaky-service/packages/action@master
if: always() && github.ref == 'refs/heads/master'
with:
file-path: ${{github.workspace}}/testresults.tap
repo-token: ${{ secrets.GITHUB_TOKEN }}
os: ${{runner.os}}
matrix: ${{toJson(matrix)}}
repo-description: ${{github.event.repository.description}}
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
+13 -5
View File
@@ -1,14 +1,17 @@
ci:
skip: [pylint, dvc-pre-commit]
repos:
- hooks:
- id: black
language_version: python3
repo: https://github.com/ambv/black
rev: 19.10b0
rev: 21.6b0
- hooks:
- id: isort
language_version: python3
repo: https://github.com/timothycrosley/isort
rev: 5.6.4
rev: 5.9.1
- hooks:
- id: flake8
language_version: python3
@@ -18,13 +21,18 @@ repos:
- flake8-debugger
- flake8-string-format
repo: https://gitlab.com/pycqa/flake8
rev: 3.8.4
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.790'
rev: 'v0.910'
- repo: local
hooks:
- id: pylint
@@ -39,7 +47,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
+12 -5
View File
@@ -15,10 +15,13 @@ ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
init-hook="import sys; \
from pathlib import Path; \
from pylint.config import find_pylintrc; \
sys.path.append(Path(find_pylintrc()).parent / 'tests');"
init-hook="import sys;
from pathlib import Path;
from pylint.config import find_pylintrc;
tests_folder = Path(find_pylintrc()).parent / 'tests'
sys.path.append(str(tests_folder));
"
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
@@ -178,7 +181,10 @@ disable=format,
# we use it to speedup/optimize
import-outside-toplevel,
fixme,
# we use cause of the exception for error message output
# so some of the times we don't use `raise from` to not show the cause
# of the error
raise-missing-from,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
@@ -307,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]
+1
View File
@@ -1,2 +1,3 @@
include fastentrypoints.py
include LICENSE
include test_requirements.txt
+20 -2
View File
@@ -129,7 +129,11 @@ Conda (Anaconda)
.. code-block:: bash
conda install -c conda-forge dvc
conda install -c conda-forge mamba # installs much faster than conda
mamba install -c conda-forge dvc
Depending on the remote storage type you plan to use to keep and share your data, you might need to
install optional dependencies: `dvc-s3`, `dvc-azure`, `dvc-gdrive`, `dvc-gs`, `dvc-oss`, `dvc-ssh`.
pip (PyPI)
----------
@@ -178,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 efficent 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.
@@ -191,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
============
@@ -222,6 +237,9 @@ Citation
Iterative, *DVC: Data Version Control - Git for Data & Models* (2020)
`DOI:10.5281/zenodo.012345 <https://doi.org/10.5281/zenodo.3677553>`_.
Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and Source Code - Empirical Study of DVC Projects <https://mcis.cs.queensu.ca/publications/2021/saner.pdf>`_ , in Proceedings of the 28th IEEE International Conference on Software Analysis, Evolution, and Reengineering, SANER 2021. Hawaii, USA.
.. |Banner| image:: https://dvc.org/img/logo-github-readme.png
:target: https://dvc.org
:alt: DVC logo
-5
View File
@@ -1,5 +0,0 @@
dvc (0.75.b1) experimental; urgency=low
* First native Debian package version.
-- Dmitry Petrov <[email protected]> Sat, 07 Dec 2019 09:49:28 +0300
-1
View File
@@ -1 +0,0 @@
10
-33
View File
@@ -1,33 +0,0 @@
Source: dvc
Maintainer: Dmitry Petrov <[email protected]>
Section: python
Priority: optional
Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9)
Standards-Version: 4.0.0
Homepage: http://dvc.org
Vcs-Git: https://github.com/iterative/dvc.git
Vcs-browser: https://github.com/iterative/dvc
Package: dvc
Architecture: all
Depends: ${misc:Depends}, ${python3:Depends}, python3-flufl.lock (>=3.2)
Recommends: python3-arrow (=0.14.0),
python3-paramiko (>=2.5.0),
python3-gssapi (>= 2.5.0),
python3-boto3 (>=1.9.86),
python3-pydrive (=1.3.1),
python3-azure-storage-blob (=12.1.0),
python3-oss2 (=2.6.1),
python3-google-cloud-storage (=1.19.0)
Description: Git for data scientists - manage your code and data together
Data Version Control or DVC is an open-source tool for data science and machine learning projects.
.
Key features:
.
1. Simple command line Git-like experience. Does not require installing and maintaining any databases. Does not depend on any proprietary online services.
.
2. Management and versioning of datasets and machine learning models. Data is saved in S3, Google cloud, Azure, Alibaba cloud, SSH server, HDFS, or even local HDD RAID.
.
3. Makes projects reproducible and shareable; helping to answer questions about how a model was built.
.
4. Helps manage experiments with Git tags/branches and metrics tracking.
-11
View File
@@ -1,11 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: DVC
Upstream-Contact: Dmitry Petrov <[email protected]>
Source: https://github.com/iterative/dvc
Files: *
Copyright: Dmitry Petrov <[email protected]>
License: Apache 2.0
On Debian systems, the full text of the Apache 2.0
License can be found in the file
`/usr/share/common-licenses/Apache-2.0`.
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/make -f
export PYBUILD_NAME=dvc
%:
dh $@ --with python3 --buildsystem=pybuild
-1
View File
@@ -1 +0,0 @@
3.0 (native)
-4
View File
@@ -1,4 +0,0 @@
# please also check http://pypi.debian.net/dvc/watch
version=3
opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
http://pypi.debian.net/dvc/dvc-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+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
)
+2 -3
View File
@@ -4,7 +4,6 @@ from contextlib import _GeneratorContextManager as GCM
from funcy import reraise
from dvc.exceptions import OutputNotFoundError, PathMissingError
from dvc.objects.stage import get_hash
from dvc.path_info import PathInfo
from dvc.repo import Repo
@@ -29,8 +28,8 @@ def get_url(path, repo=None, rev=None, remote=None):
raise OutputNotFoundError(path, repo)
cloud = metadata.repo.cloud
hash_info = get_hash(path_info, _repo.repo_fs, "md5", _repo.odb)
return cloud.get_url_for(remote, checksum=hash_info.value)
md5 = metadata.repo.dvcfs.info(path_info)["md5"]
return cloud.get_url_for(remote, checksum=md5)
def open( # noqa, pylint: disable=redefined-builtin
+72 -26
View File
@@ -2,18 +2,21 @@ 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.objects import ObjectFormatError, check, load
from dvc.objects.stage import get_hash
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]
slow_link_guard,
)
from dvc.types import Optional
logger = logging.getLogger(__name__)
@@ -30,7 +33,7 @@ def _changed(path_info, fs, obj, cache):
return True
try:
actual = get_hash(path_info, fs, obj.hash_info.name, cache)
actual = stage(cache, path_info, fs, obj.hash_info.name).hash_info
except FileNotFoundError:
logger.debug("'%s' doesn't exist.", path_info)
return True
@@ -56,7 +59,7 @@ def _remove(path_info, fs, cache, force=False):
fs.remove(path_info)
return
current = get_hash(path_info, fs, fs.PARAM_CHECKSUM, cache)
current = stage(cache, path_info, fs, fs.PARAM_CHECKSUM).hash_info
try:
obj = load(cache, current)
check(cache, obj)
@@ -94,7 +97,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
)
@@ -131,7 +134,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:
@@ -147,7 +150,14 @@ 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
@@ -167,17 +177,24 @@ 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, dir_info, 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 dir_info.items()}
needed_files = {path_info.joinpath(*key) for key, _ in obj}
redundant_files = existing_files - needed_files
for path in redundant_files:
_remove(path, fs, cache, force)
@@ -186,9 +203,17 @@ def _remove_redundant_files(path_info, fs, dir_info, 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):
@@ -197,27 +222,29 @@ def _checkout_dir(
logger.debug("Linking directory '%s'.", path_info)
for entry_key, entry_hash_info in obj.hash_info.dir_info.items():
for entry_key, entry_obj in obj:
entry_modified = _checkout_file(
path_info.joinpath(*entry_key),
fs,
cache.get(entry_hash_info),
entry_obj,
cache,
force,
progress_callback,
relink,
state=None,
)
if entry_modified:
modified = True
modified = (
_remove_redundant_files(
path_info, fs, obj.hash_info.dir_info, cache, force
path_info, fs, obj, cache, force, dvcignore=dvcignore
)
or modified
)
fs.repo.state.save(path_info, fs, obj.hash_info)
if state:
state.save(path_info, fs, obj.hash_info)
# relink is not modified, assume it as nochange
return modified and not relink
@@ -231,17 +258,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
@@ -255,6 +293,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
@@ -271,7 +311,7 @@ def checkout(
failed = path_info
elif not relink and not _changed(path_info, fs, obj, cache):
logger.trace("Data '%s' didn't change.", path_info)
logger.trace("Data '%s' didn't change.", path_info) # type: ignore
skip = True
else:
try:
@@ -288,9 +328,7 @@ def checkout(
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
@@ -298,5 +336,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 -4
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"
@@ -45,6 +45,10 @@ class CmdBase(ABC):
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):
pass
@@ -55,3 +59,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)
+2 -4
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):
@@ -101,7 +99,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
+9 -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",
@@ -156,6 +157,7 @@ def add_parser(subparsers, _parent_parser):
help="Fetch cache for all tags.",
)
pull_parser.add_argument(
"-A",
"--all-commits",
action="store_true",
default=False,
@@ -207,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",
@@ -224,6 +226,7 @@ def add_parser(subparsers, _parent_parser):
help="Push cache for all tags.",
)
push_parser.add_argument(
"-A",
"--all-commits",
action="store_true",
default=False,
@@ -270,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",
@@ -290,6 +290,7 @@ def add_parser(subparsers, _parent_parser):
help="Fetch cache for all tags.",
)
fetch_parser.add_argument(
"-A",
"--all-commits",
action="store_true",
default=False,
@@ -370,6 +371,7 @@ def add_parser(subparsers, _parent_parser):
"for all tags.",
)
status_parser.add_argument(
"-A",
"--all-commits",
action="store_true",
default=False,
+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."
+18 -30
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.utils.diff import table
header = ["Status", "Hash", "Path"] if show_hash else ["Status", "Path"]
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:
@@ -39,12 +37,12 @@ def _show_md(diff, show_hash=False, hide_missing=False):
else:
rows.append([status, path])
return table(header, rows, True)
ui.table(rows, headers=headers, markdown=True)
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,14 +106,7 @@ 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
@@ -126,9 +117,8 @@ class CmdDiff(CmdBase):
)
if not hide_missing:
fmt += ", {not in cache} not in cache"
groups.append(fmt.format_map(summary))
return "\n\n".join(groups)
ui.write(fmt.format_map(summary))
def run(self):
from dvc.exceptions import DvcException
@@ -155,13 +145,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")
+291 -258
View File
@@ -1,24 +1,33 @@
import argparse
import logging
from collections import Counter, OrderedDict, defaultdict
from collections.abc import Mapping
from datetime import date, datetime
from itertools import groupby
from typing import Dict, Iterable, Optional
from fnmatch import fnmatch
from typing import TYPE_CHECKING, Dict, Iterable, Optional
import dvc.prompt as prompt
from funcy import lmap
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
if TYPE_CHECKING:
from rich.text import Text
from dvc.compare import TabularData
logger = logging.getLogger(__name__)
SHOW_MAX_WIDTH = 1024
FILL_VALUE = "-"
def _filter_name(names, label, filter_strs):
@@ -27,32 +36,23 @@ def _filter_name(names, label, filter_strs):
for filter_s in filter_strs:
path, _, name = filter_s.rpartition(":")
path_filters[path].append(tuple(name.split(".")))
path_filters[path].append(name)
for path, filters in path_filters.items():
if path:
match_paths = [path]
else:
match_paths = names.keys()
for length, groups in groupby(filters, len):
for group in groups:
for match_path in match_paths:
possible_names = [
tuple(name.split(".")) for name in names[match_path]
]
matches = [
name
for name in possible_names
if name[:length] == group
]
if not matches:
name = ".".join(group)
raise InvalidArgumentError(
f"'{name}' does not match any known {label}"
)
ret[match_path].update(
{".".join(match): None for match in matches}
for match_path in match_paths:
for f in filters:
matches = [
name for name in names[match_path] if fnmatch(name, f)
]
if not matches:
raise InvalidArgumentError(
f"'{f}' does not match any known {label}"
)
ret[match_path].update({match: None for match in matches})
return ret
@@ -103,7 +103,6 @@ def _collect_names(all_experiments, **kwargs):
for exp in experiments.values():
_update_names(metric_names, exp.get("metrics", {}).items())
_update_names(param_names, exp.get("params", {}).items())
metric_names = _filter_names(
metric_names,
"metrics",
@@ -111,7 +110,7 @@ def _collect_names(all_experiments, **kwargs):
kwargs.get("exclude_metrics"),
)
param_names = _filter_names(
(param_names),
param_names,
"params",
kwargs.get("include_params"),
kwargs.get("exclude_params"),
@@ -120,13 +119,22 @@ 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,
metric_names,
param_names,
precision=DEFAULT_PRECISION,
no_timestamp=False,
sort_by=None,
sort_order=None,
):
@@ -143,56 +151,56 @@ def _collect_rows(
new_checkpoint = True
for i, (rev, exp) in enumerate(experiments.items()):
row = []
style = None
queued = "*" if exp.get("queued", False) else ""
queued = str(exp.get("queued") or "")
is_baseline = rev == "baseline"
tip = exp.get("checkpoint_tip")
parent = ""
if rev == "baseline":
if Git.is_sha(base_rev):
name_rev = base_rev[:7]
else:
name_rev = base_rev
name = exp.get("name", name_rev)
row.append(f"{name}")
style = "bold"
if is_baseline:
name_rev = base_rev[:7] if Git.is_sha(base_rev) else base_rev
else:
if tip:
parent_rev = exp.get("checkpoint_parent", "")
parent_exp = experiments.get(parent_rev, {})
parent_tip = parent_exp.get("checkpoint_tip")
if tip == parent_tip:
if new_checkpoint:
tree = "│ ╓"
else:
tree = "│ ╟"
new_checkpoint = False
else:
if parent_rev == base_rev:
tree = "├─╨"
else:
tree = "│ ╟"
parent = f" ({parent_rev[:7]})"
new_checkpoint = True
name_rev = rev[:7]
exp_name = exp.get("name", "")
tip = exp.get("checkpoint_tip")
parent_rev = exp.get("checkpoint_parent", "")
parent_exp = experiments.get(parent_rev, {})
parent_tip = parent_exp.get("checkpoint_tip")
parent = ""
if is_baseline:
typ = "baseline"
elif tip:
if tip == parent_tip:
typ = (
"checkpoint_tip" if new_checkpoint else "checkpoint_commit"
)
elif parent_rev == base_rev:
typ = "checkpoint_base"
else:
if i < len(experiments) - 1:
tree = "├──"
else:
tree = "└──"
new_checkpoint = True
name = exp.get("name", rev[:7])
row.append(f"{tree} {queued}{name}{parent}")
typ = "checkpoint_commit"
parent = parent_rev[:7]
elif i < len(experiments) - 1:
typ = "branch_commit"
else:
typ = "branch_base"
if not no_timestamp:
row.append(_format_time(exp.get("timestamp")))
if not is_baseline:
new_checkpoint = not (tip and tip == parent_tip)
row = [
exp_name,
name_rev,
queued,
typ,
_format_time(exp.get("timestamp")),
parent,
]
_extend_row(
row, metric_names, exp.get("metrics", {}).items(), precision
)
_extend_row(row, param_names, exp.get("params", {}).items(), precision)
yield row, style
yield row
def _sort_column(sort_by, metric_names, param_names):
@@ -233,7 +241,7 @@ def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
return _sort((tip, experiments[tip]))
data = exp.get(typ, {}).get(sort_path, {})
val = flatten(data).get(sort_name)
return (val is None, val)
return val is None, val
ret = OrderedDict()
if "baseline" in experiments:
@@ -245,7 +253,7 @@ def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
def _format_time(timestamp):
if timestamp is None:
return "-"
return FILL_VALUE
if timestamp.date() == date.today():
fmt = "%I:%M %p"
else:
@@ -253,42 +261,23 @@ def _format_time(timestamp):
return timestamp.strftime(fmt)
def _format_field(val, precision=DEFAULT_PRECISION):
if isinstance(val, float):
fmt = f"{{:.{precision}g}}"
return fmt.format(val)
if isinstance(val, Mapping):
return {k: _format_field(v) for k, v in val.items()}
if isinstance(val, list):
return [_format_field(x) for x in val]
return str(val)
def _extend_row(row, names, items, precision):
from rich.text import Text
from dvc.compare import _format_field, with_value
if not items:
row.extend(["-"] * len(names))
row.extend(FILL_VALUE for keys in names.values() for _ in keys)
return
for fname, item in items:
if isinstance(item, dict):
item = flatten(item)
else:
item = {fname: item}
item = flatten(item) if isinstance(item, dict) else {fname: item}
for name in names[fname]:
if name in item:
value = item[name]
if value is None:
text = "-"
else:
# wrap field data in rich.Text, otherwise rich may
# interpret unescaped braces from list/dict types as rich
# markup tags
text = Text(str(_format_field(value, precision)))
row.append(text)
else:
row.append("-")
value = with_value(item.get(name), FILL_VALUE)
# wrap field data in rich.Text, otherwise rich may
# interpret unescaped braces from list/dict types as rich
# markup tags
row.append(Text(str(_format_field(value, precision))))
def _parse_filter_list(param_list):
@@ -302,9 +291,68 @@ def _parse_filter_list(param_list):
return ret
def _experiments_table(all_experiments, **kwargs):
from dvc.utils.table import Table
def experiments_table(
all_experiments,
metric_headers,
metric_names,
param_headers,
param_names,
sort_by=None,
sort_order=None,
precision=DEFAULT_PRECISION,
) -> "TabularData":
from funcy import lconcat
from dvc.compare import TabularData
headers = ["Experiment", "rev", "queued", "typ", "Created", "parent"]
td = TabularData(
lconcat(headers, metric_headers, param_headers), fill_value=FILL_VALUE
)
for base_rev, experiments in all_experiments.items():
rows = _collect_rows(
base_rev,
experiments,
metric_names,
param_names,
sort_by=sort_by,
sort_order=sort_order,
precision=precision,
)
td.extend(rows)
return td
def prepare_exp_id(kwargs) -> "Text":
from rich.text import Text
exp_name = kwargs["Experiment"]
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")
suff = f" ({parent})" if parent else ""
text.append(suff)
tree = experiment_types[typ]
queued = "*" if kwargs.get("queued") else ""
pref = (f"{tree} " if tree else "") + queued
return Text(pref) + text
def baseline_styler(typ):
return {"style": "bold"} if typ == "baseline" else {}
def show_experiments(
all_experiments, pager=True, no_timestamp=False, **kwargs
):
include_metrics = _parse_filter_list(kwargs.pop("include_metrics", []))
exclude_metrics = _parse_filter_list(kwargs.pop("exclude_metrics", []))
include_params = _parse_filter_list(kwargs.pop("include_params", []))
@@ -317,44 +365,66 @@ def _experiments_table(all_experiments, **kwargs):
include_params=include_params,
exclude_params=exclude_params,
)
metric_headers = _normalize_headers(metric_names)
param_headers = _normalize_headers(param_names)
table = Table()
table.add_column(
"Experiment", no_wrap=True, header_style="black on grey93"
)
if not kwargs.get("no_timestamp", False):
table.add_column("Created", header_style="black on grey93")
_add_data_columns(
table,
td = experiments_table(
all_experiments,
metric_headers,
metric_names,
justify="right",
no_wrap=True,
header_style="black on cornsilk1",
)
_add_data_columns(
table, param_names, justify="left", header_style="black on light_cyan1"
param_headers,
param_names,
kwargs.get("sort_by"),
kwargs.get("sort_order"),
kwargs.get("precision"),
)
for base_rev, experiments in all_experiments.items():
for row, _, in _collect_rows(
base_rev, experiments, metric_names, param_names, **kwargs,
):
table.add_row(*row)
if no_timestamp:
td.drop("Created")
return table
row_styles = lmap(baseline_styler, td.column("typ"))
merge_headers = ["Experiment", "rev", "queued", "typ", "parent"]
td.column("Experiment")[:] = map(prepare_exp_id, td.as_dict(merge_headers))
td.drop(*merge_headers[1:])
headers = {"metrics": metric_headers, "params": param_headers}
styles = {
"Experiment": {"no_wrap": True, "header_style": "black on grey93"},
"Created": {"header_style": "black on grey93"},
}
header_bg_colors = {"metrics": "cornsilk1", "params": "light_cyan1"}
styles.update(
{
header: {
"justify": "right" if typ == "metrics" else "left",
"header_style": f"black on {header_bg_colors[typ]}",
"collapse": idx != 0,
"no_wrap": typ == "metrics",
}
for typ, hs in headers.items()
for idx, header in enumerate(hs)
}
)
td.render(
pager=pager,
borders=True,
rich_table=True,
header_styles=styles,
row_styles=row_styles,
)
def _add_data_columns(table, names, **kwargs):
def _normalize_headers(names):
count = Counter(
name for path in names for name in names[path] for path in names
)
first = True
for path in names:
for name in names[path]:
col_name = name if count[name] == 1 else f"{path}:{name}"
kwargs["collapse"] = False if first else True
table.add_column(col_name, **kwargs)
first = False
return [
name if count[name] == 1 else f"{path}:{name}"
for path in names
for name in names[path]
]
def _format_json(item):
@@ -365,8 +435,6 @@ def _format_json(item):
class CmdExperimentsShow(CmdBase):
def run(self):
from rich.console import Console
try:
all_experiments = self.repo.experiments.show(
all_branches=self.args.all_branches,
@@ -374,20 +442,18 @@ class CmdExperimentsShow(CmdBase):
all_commits=self.args.all_commits,
sha_only=self.args.sha,
num=self.args.num,
param_deps=self.args.param_deps,
)
except DvcException:
logger.exception("failed to show experiments")
return 1
if self.args.show_json:
import json
if self.args.show_json:
import json
logger.info(json.dumps(all_experiments, default=_format_json))
return 0
if self.args.precision is None:
precision = DEFAULT_PRECISION
else:
precision = self.args.precision
table = _experiments_table(
ui.write(json.dumps(all_experiments, default=_format_json))
else:
show_experiments(
all_experiments,
include_metrics=self.args.include_metrics,
exclude_metrics=self.args.exclude_metrics,
@@ -396,29 +462,9 @@ class CmdExperimentsShow(CmdBase):
no_timestamp=self.args.no_timestamp,
sort_by=self.args.sort_by,
sort_order=self.args.sort_order,
precision=precision,
precision=self.args.precision or DEFAULT_PRECISION,
pager=not self.args.no_pager,
)
console = Console()
if self.args.no_pager:
console.print(table)
else:
from dvc.utils.pager import DvcPager
# NOTE: rich does not have native support for unlimited width
# via pager. we override rich table compression by setting
# console width to the full width of the table
measurement = table.__rich_measure__(console, SHOW_MAX_WIDTH)
console._width = ( # pylint: disable=protected-access
measurement.maximum
)
with console.pager(pager=DvcPager(), styles=True):
console.print(table)
except DvcException:
logger.exception("failed to show experiments")
return 1
return 0
@@ -432,43 +478,6 @@ class CmdExperimentsApply(CmdBase):
return 0
def _show_diff(
diff,
title="",
markdown=False,
no_path=False,
old=False,
precision=DEFAULT_PRECISION,
):
from dvc.utils.diff import table
rows = []
for fname, diff_ in diff.items():
sorted_diff = OrderedDict(sorted(diff_.items()))
for item, change in sorted_diff.items():
row = [] if no_path else [fname]
row.append(item)
if old:
row.append(_format_field(change.get("old"), precision))
row.append(_format_field(change["new"], precision))
row.append(
_format_field(
change.get("diff", "diff not supported"), precision
)
)
rows.append(row)
header = [] if no_path else ["Path"]
header.append(title)
if old:
header.extend(["Old", "New"])
else:
header.append("Value")
header.append("Change")
return table(header, rows, markdown)
class CmdExperimentsDiff(CmdBase):
def run(self):
@@ -477,45 +486,57 @@ class CmdExperimentsDiff(CmdBase):
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
all=self.args.all,
param_deps=self.args.param_deps,
)
if self.args.show_json:
import json
logger.info(json.dumps(diff))
else:
if self.args.precision is None:
precision = DEFAULT_PRECISION
else:
precision = self.args.precision
diffs = [("metrics", "Metric"), ("params", "Param")]
for key, title in diffs:
table = _show_diff(
diff[key],
title=title,
markdown=self.args.show_md,
no_path=self.args.no_path,
old=self.args.old,
precision=precision,
)
if table:
logger.info(table)
logger.info("")
except DvcException:
logger.exception("failed to show experiments diff")
return 1
if self.args.show_json:
import json
ui.write(json.dumps(diff))
else:
from dvc.compare import show_diff
precision = self.args.precision or DEFAULT_PRECISION
diffs = [("metrics", "Metric"), ("params", "Param")]
for idx, (key, title) in enumerate(diffs):
if idx:
# we are printing tables even in `--quiet` mode
# so we should also be printing the "table" separator
ui.write(force=True)
show_diff(
diff[key],
title=title,
markdown=self.args.show_md,
no_path=self.args.no_path,
old=self.args.old,
on_empty_diff="diff not supported",
precision=precision if key == "metrics" else None,
)
return 0
class CmdExperimentsRun(CmdRepro):
def run(self):
from dvc.command.metrics import _show_metrics
from dvc.compare import show_metrics
if self.args.checkpoint_resume:
if self.args.reset:
raise InvalidArgumentError(
"--reset and --rev are mutually exclusive."
)
if not (self.args.queue or self.args.tmp_dir):
raise InvalidArgumentError(
"--rev can only be used in conjunction with "
"--queue or --temp."
)
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,
@@ -532,7 +553,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
@@ -586,12 +607,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
@@ -640,15 +662,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
@@ -667,15 +689,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
@@ -685,7 +707,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
@@ -783,6 +805,12 @@ def add_parser(subparsers, parent_parser):
help="Exclude the specified params from output table.",
metavar="<params_list>",
)
experiments_show_parser.add_argument(
"--param-deps",
action="store_true",
default=False,
help="Show only params that are stage dependencies.",
)
experiments_show_parser.add_argument(
"--sort-by",
help="Sort related experiments by the specified metric or param.",
@@ -840,7 +868,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)
@@ -868,6 +896,12 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Show unchanged metrics/params as well.",
)
experiments_diff_parser.add_argument(
"--param-deps",
action="store_true",
default=False,
help="Show only params that are stage dependencies.",
)
experiments_diff_parser.add_argument(
"--show-json",
action="store_true",
@@ -903,9 +937,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],
@@ -920,9 +952,8 @@ def add_parser(subparsers, parent_parser):
type=str,
dest="checkpoint_resume",
help=(
"Continue the specified checkpoint experiment. "
"(Only required for explicitly resuming checkpoints in queued "
"or temp dir runs.)"
"Continue the specified checkpoint experiment. Can only be used "
"in conjunction with --queue or --temp."
),
metavar="<experiment_rev>",
).complete = completion.EXPERIMENT
@@ -967,6 +998,7 @@ def add_parser(subparsers, parent_parser):
help="Keep experiments derived from all Git tags.",
)
experiments_gc_parser.add_argument(
"-A",
"--all-commits",
action="store_true",
default=False,
@@ -999,10 +1031,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)
@@ -1025,7 +1057,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",
@@ -1097,7 +1129,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)
@@ -1153,7 +1185,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)
@@ -1166,7 +1198,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",
@@ -1215,6 +1247,7 @@ def _add_run_common(parser):
"-j",
"--jobs",
type=int,
default=1,
help="Run the specified number of experiments at a time in parallel.",
metavar="<number>",
)
+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)
+3 -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(
@@ -97,6 +97,7 @@ def add_parser(subparsers, parent_parser):
help="Keep data files for all Git tags.",
)
gc_parser.add_argument(
"-A",
"--all-commits",
action="store_true",
default=False,
+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
+4 -7
View File
@@ -1,12 +1,9 @@
import argparse
import logging
import os
from dvc.command import completion
from dvc.command.base import CmdBase, fix_subparsers
from dvc.utils.html import write
logger = logging.getLogger(__name__)
from dvc.ui import ui
class CmdLive(CmdBase):
@@ -16,9 +13,9 @@ class CmdLive(CmdBase):
metrics, plots = self.repo.live.show(target=target, revs=revs)
html_path = self.args.target + ".html"
write(html_path, plots, metrics)
self.repo.plots.write_html(html_path, plots, metrics)
logger.info(f"\nfile://{os.path.abspath(html_path)}")
ui.write("\nfile://", os.path.abspath(html_path), sep="")
return 0
@@ -36,7 +33,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",
+11 -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__)
@@ -34,9 +34,13 @@ class CmdList(CmdBaseNoRepo):
recursive=self.args.recursive,
dvc_only=self.args.dvc_only,
)
if entries:
entries = _prettify(entries, sys.stdout.isatty())
logger.info("\n".join(entries))
if self.args.show_json:
import json
ui.write(json.dumps(entries))
elif 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}'")
@@ -65,6 +69,9 @@ def add_parser(subparsers, parent_parser):
list_parser.add_argument(
"--dvc-only", action="store_true", help="Show only DVC outputs."
)
list_parser.add_argument(
"--show-json", action="store_true", help="Show output in JSON format."
)
list_parser.add_argument(
"--rev",
nargs="?",
+42 -121
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 BadMetricError, DvcException
from dvc.ui import ui
logger = logging.getLogger(__name__)
@@ -11,64 +12,6 @@ logger = logging.getLogger(__name__)
DEFAULT_PRECISION = 5
def _show_metrics(
metrics,
markdown=False,
all_branches=False,
all_tags=False,
all_commits=False,
precision=None,
):
from dvc.utils.diff import format_dict, table
from dvc.utils.flatten import flatten
# 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)
with_rev = any([all_branches, all_tags, all_commits])
header_set = set()
rows = []
if precision is None:
precision = DEFAULT_PRECISION
def _round(val):
if isinstance(val, float):
return round(val, precision)
return val
for _branch, val in metrics.items():
for _fname, metric in val.items():
if not isinstance(metric, dict):
header_set.add("")
continue
for key, _val in flatten(format_dict(metric)).items():
header_set.add(key)
header = sorted(header_set)
for branch, val in metrics.items():
for fname, metric in val.items():
row = []
if with_rev:
row.append(branch)
row.append(fname)
if not isinstance(metric, dict):
row.append(str(metric))
rows.append(row)
continue
flattened_val = flatten(format_dict(metric))
for i in header:
row.append(_round(flattened_val.get(i)))
rows.append(row)
header.insert(0, "Path")
if with_rev:
header.insert(0, "Revision")
if missing:
raise BadMetricError(missing)
return table(header, rows, markdown)
class CmdMetricsBase(CmdBase):
UNINITIALIZED = True
@@ -83,61 +26,37 @@ class CmdMetricsShow(CmdMetricsBase):
all_commits=self.args.all_commits,
recursive=self.args.recursive,
)
if self.args.show_json:
import json
logger.info(json.dumps(metrics))
else:
table = _show_metrics(
metrics,
self.args.show_md,
self.args.all_branches,
self.args.all_tags,
self.args.all_commits,
)
if table:
logger.info(table)
except DvcException:
logger.exception("")
return 1
if self.args.show_json:
import json
ui.write(json.dumps(metrics))
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,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
precision=self.args.precision or DEFAULT_PRECISION,
round_digits=True,
)
return 0
def _show_diff(diff, markdown=False, no_path=False, precision=None):
from collections import OrderedDict
from dvc.utils.diff import table
if precision is None:
precision = DEFAULT_PRECISION
def _round(val):
if isinstance(val, float):
return round(val, precision)
return val
rows = []
for fname, mdiff in diff.items():
sorted_mdiff = OrderedDict(sorted(mdiff.items()))
for metric, change in sorted_mdiff.items():
row = [] if no_path else [fname]
row.append(metric)
row.append(_round(change.get("old")))
row.append(_round(change["new"]))
row.append(_round(change.get("diff")))
rows.append(row)
header = [] if no_path else ["Path"]
header.append("Metric")
header.extend(["Old", "New"])
header.append("Change")
return table(header, rows, markdown)
class CmdMetricsDiff(CmdMetricsBase):
def run(self):
try:
@@ -148,25 +67,26 @@ class CmdMetricsDiff(CmdMetricsBase):
recursive=self.args.recursive,
all=self.args.all,
)
if self.args.show_json:
import json
logger.info(json.dumps(diff))
else:
table = _show_diff(
diff,
self.args.show_md,
self.args.no_path,
precision=self.args.precision,
)
if table:
logger.info(table)
except DvcException:
logger.exception("failed to show metrics diff")
return 1
if self.args.show_json:
import json
ui.write(json.dumps(diff))
else:
from dvc.compare import show_diff
show_diff(
diff,
title="Metric",
markdown=self.args.show_md,
no_path=self.args.no_path,
precision=self.args.precision or DEFAULT_PRECISION,
round_digits=True,
)
return 0
@@ -219,6 +139,7 @@ def add_parser(subparsers, parent_parser):
help="Show metrics for all tags.",
)
metrics_show_parser.add_argument(
"-A",
"--all-commits",
action="store_true",
default=False,
+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)
+23 -32
View File
@@ -1,35 +1,14 @@
import argparse
import logging
from collections import OrderedDict
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__)
def _show_diff(diff, markdown=False, no_path=False):
from dvc.utils.diff import table
rows = []
for fname, pdiff in diff.items():
sorted_pdiff = OrderedDict(sorted(pdiff.items()))
for param, change in sorted_pdiff.items():
row = [] if no_path else [fname]
row.append(param)
row.append(change["old"])
row.append(change["new"])
rows.append(row)
header = [] if no_path else ["Path"]
header.append("Param")
header.append("Old")
header.append("New")
return table(header, rows, markdown)
class CmdParamsDiff(CmdBase):
UNINITIALIZED = True
@@ -40,21 +19,27 @@ class CmdParamsDiff(CmdBase):
b_rev=self.args.b_rev,
targets=self.args.targets,
all=self.args.all,
deps=self.args.deps,
)
if self.args.show_json:
import json
logger.info(json.dumps(diff))
else:
table = _show_diff(diff, self.args.show_md, self.args.no_path)
if table:
logger.info(table)
except DvcException:
logger.exception("failed to show params diff")
return 1
if self.args.show_json:
import json
ui.write(json.dumps(diff))
else:
from dvc.compare import show_diff
show_diff(
diff,
title="Param",
markdown=self.args.show_md,
no_path=self.args.no_path,
show_changes=False,
)
return 0
@@ -111,6 +96,12 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Show unchanged params as well.",
)
params_diff_parser.add_argument(
"--deps",
action="store_true",
default=False,
help="Show only params that are stage dependencies.",
)
params_diff_parser.add_argument(
"--show-json",
action="store_true",
+38 -13
View File
@@ -1,12 +1,11 @@
import argparse
import logging
import os
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
from dvc.utils.html import write
logger = logging.getLogger(__name__)
@@ -23,6 +22,8 @@ class CmdPlots(CmdBase):
return {k: v for k, v in props.items() if v is not None}
def run(self):
from pathlib import Path
if self.args.show_vega:
if not self.args.targets:
logger.error("please specify a target for `--show-vega`")
@@ -38,20 +39,32 @@ class CmdPlots(CmdBase):
if self.args.show_vega:
target = self.args.targets[0]
logger.info(plots[target])
ui.write(plots[target])
return 0
path = self.args.out or "plots.html"
path = os.path.join(os.getcwd(), path)
write(path, plots)
logger.info(f"file://{path}")
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()
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
return 0
@@ -77,7 +90,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
@@ -85,7 +98,7 @@ class CmdPlotsModify(CmdPlots):
def add_parser(subparsers, parent_parser):
PLOTS_HELP = (
"Commands to visualize and compare plot metrics in structured files "
"(JSON, YAML, CSV, TSV)"
"(JSON, YAML, CSV, TSV)."
)
plots_parser = subparsers.add_parser(
@@ -150,7 +163,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)
@@ -165,7 +178,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(
@@ -229,3 +242,15 @@ def _add_output_arguments(parser):
default=False,
help="Show output in Vega format.",
)
parser.add_argument(
"--open",
action="store_true",
default=False,
help="Open plot file directly in the browser.",
)
parser.add_argument(
"--html-template",
default=None,
help="Custom HTML template for VEGA visualization.",
metavar="<path>",
)
+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
+7 -7
View File
@@ -1,27 +1,27 @@
import argparse
import logging
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link
from dvc.command.metrics import _show_metrics
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."
)
if self.args.metrics:
from dvc.compare import show_metrics
metrics = self.repo.metrics.show()
logger.info(_show_metrics(metrics))
show_metrics(metrics)
return 0
+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",
+14 -13
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] = []
@@ -75,16 +75,14 @@ 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
)
return dict.fromkeys(collected).keys()
def run(self):
from dvc.utils.diff import table
from dvc.ui import ui
def log_error(relpath: str, exc: Exception):
if self.args.fail:
@@ -98,8 +96,7 @@ class CmdStageList(CmdBase):
names_only = self.args.names_only
data = prepare_stages_data(stages, description=not names_only)
if data:
print(table(header=(), rows=data.items()))
ui.table(data.items())
return 0
@@ -116,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)
@@ -202,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",
@@ -255,7 +255,8 @@ def _add_common_args(parser):
"--checkpoints",
action="append",
default=[],
help=argparse.SUPPRESS,
help="Declare checkpoint output file or directory for 'dvc exp run'. "
"Not compatible with 'dvc repro'.",
metavar="<filename>",
).complete = completion.FILE
parser.add_argument(
+34 -24
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,25 +39,32 @@ class CmdDataStatus(CmdDataBase):
for key, value in status.items():
if isinstance(value, str):
logger.info("{}{}{}".format(ind, self._normalize(value), key))
ui.write("{}{}{}".format(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
indent = 1 if self.args.cloud else 0
try:
st = self.repo.status(
targets=self.args.targets,
jobs=self.args.jobs,
cloud=self.args.cloud,
remote=self.args.remote,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
with lock_repo(self.repo):
try:
st = self.repo.status(
targets=self.args.targets,
jobs=self.args.jobs,
cloud=self.args.cloud,
remote=self.args.remote,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except DvcException:
logger.exception("")
return 1
if self.args.quiet:
return bool(st)
@@ -64,20 +72,22 @@ class CmdDataStatus(CmdDataBase):
if self.args.show_json:
import json
logger.info(json.dumps(st))
elif st:
ui.write(json.dumps(st))
return 0
if st:
self._show(st, indent)
elif not self.repo.stages:
logger.info(self.EMPTY_PROJECT_MSG)
return 0
# additional hints for the user
if not self.repo.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(
"remote"
)
logger.info(self.IN_SYNC_MSG.format(remote=remote))
ui.write(self.IN_SYNC_MSG.format(remote=remote))
else:
logger.info(self.UP_TO_DATE_MSG)
ui.write(self.UP_TO_DATE_MSG)
except DvcException:
logger.exception("")
return 1
return 0
+2 -2
View File
@@ -23,7 +23,7 @@ class CmdUnprotect(CmdBase):
def add_parser(subparsers, parent_parser):
UNPROTECT_HELP = (
"Unprotect tracked files or directories (when hardlinks or symlinks "
"have been enabled with `dvc config cache.type`)"
"have been enabled with `dvc config cache.type`)."
)
unprotect_parser = subparsers.add_parser(
"unprotect",
@@ -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",
+2 -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__)
@@ -11,7 +12,7 @@ class CmdVersion(CmdBaseNoRepo):
from dvc.info import get_dvc_info
dvc_info = get_dvc_info()
logger.info(dvc_info)
ui.write(dvc_info, force=True)
return 0
+321
View File
@@ -0,0 +1,321 @@
from collections import abc
from itertools import chain, repeat, zip_longest
from operator import itemgetter
from typing import (
TYPE_CHECKING,
Any,
Dict,
ItemsView,
Iterable,
Iterator,
List,
Mapping,
MutableSequence,
Sequence,
Tuple,
Union,
overload,
)
from funcy import reraise
if TYPE_CHECKING:
from dvc.ui.table import CellT
class Column(List["CellT"]):
pass
def with_value(value, default):
return default if value is None else value
class TabularData(MutableSequence[Sequence["CellT"]]):
def __init__(self, columns: Sequence[str], fill_value: str = ""):
self._columns: Dict[str, Column] = {name: Column() for name in columns}
self._keys: List[str] = list(columns)
self._fill_value = fill_value
@property
def columns(self) -> List[Column]:
return list(map(self.column, self.keys()))
def column(self, name: str) -> Column:
return self._columns[name]
def items(self) -> ItemsView[str, Column]:
projection = {k: self.column(k) for k in self.keys()}
return projection.items()
def keys(self) -> List[str]:
return self._keys
def _iter_col_row(
self, row: Sequence["CellT"]
) -> Iterator[Tuple["CellT", Column]]:
for val, col in zip_longest(row, self.columns):
if col is None:
break
yield with_value(val, self._fill_value), col
def append(self, value: Sequence["CellT"]) -> None:
for val, col in self._iter_col_row(value):
col.append(val)
def extend(self, values: Iterable[Sequence["CellT"]]) -> None:
for row in values:
self.append(row)
def insert(self, index: int, value: Sequence["CellT"]) -> None:
for val, col in self._iter_col_row(value):
col.insert(index, val)
def __iter__(self) -> Iterator[List["CellT"]]:
return map(list, zip(*self.columns))
def __getattr__(self, item: str) -> Column:
with reraise(KeyError, AttributeError):
return self.column(item)
def __getitem__(self, item: Union[int, slice]):
func = itemgetter(item)
it = map(func, self.columns)
if isinstance(item, slice):
it = map(list, zip(*it))
return list(it)
@overload
def __setitem__(self, item: int, value: Sequence["CellT"]) -> None:
...
@overload
def __setitem__(
self, item: slice, value: Iterable[Sequence["CellT"]]
) -> None:
...
def __setitem__(self, item, value) -> None:
it = value
if isinstance(item, slice):
n = len(self.columns)
normalized_rows = (
chain(val, repeat(self._fill_value, n - len(val)))
for val in value
)
# we need to transpose those rows into columnar format
# as we work in terms of column-based arrays
it = zip(*normalized_rows)
for i, col in self._iter_col_row(it):
col[item] = i
def __delitem__(self, item: Union[int, slice]) -> None:
for col in self.columns:
del col[item]
def __len__(self) -> int:
return len(self.columns[0])
@property
def shape(self) -> Tuple[int, int]:
return len(self.columns), len(self)
def drop(self, *col_names: str) -> None:
for col_name in col_names:
self._keys.remove(col_name)
self._columns.pop(col_name)
def rename(self, from_col_name: str, to_col_name: str) -> None:
self._columns[to_col_name] = self._columns.pop(from_col_name)
self._keys[self._keys.index(from_col_name)] = to_col_name
def project(self, *col_names: str) -> None:
self.drop(*(set(self._keys) - set(col_names)))
self._keys = list(col_names)
def to_csv(self) -> str:
import csv
from io import StringIO
buff = StringIO()
writer = csv.writer(buff)
writer.writerow(self.keys())
for row in self:
writer.writerow(row)
return buff.getvalue()
def add_column(self, name: str) -> None:
self._columns[name] = Column([self._fill_value] * len(self))
self._keys.append(name)
def row_from_dict(self, d: Mapping[str, "CellT"]) -> None:
keys = self.keys()
for key in d:
if key not in keys:
self.add_column(key)
row: List["CellT"] = [
with_value(d.get(key), self._fill_value) for key in self.keys()
]
self.append(row)
def render(self, **kwargs: Any):
from dvc.ui import ui
ui.table(self, headers=self.keys(), **kwargs)
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))
]
def _normalize_float(val: float, precision: int):
return f"{val:.{precision}g}"
def _format_field(
val: Any, precision: int = None, round_digits: bool = False
) -> str:
def _format(_val):
if isinstance(_val, float) and precision:
func = round if round_digits else _normalize_float
return func(_val, precision)
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]
return _val
return str(_format(val))
def diff_table(
diff,
title: str,
old: bool = True,
no_path: bool = False,
show_changes: bool = True,
precision: int = None,
round_digits: bool = False,
on_empty_diff: str = None,
) -> TabularData:
headers: List[str] = ["Path", title, "Old", "New", "Change"]
fill_value = "-"
td = TabularData(headers, fill_value=fill_value)
for fname, diff_in_file in diff.items():
for item, change in sorted(diff_in_file.items()):
old_value = with_value(change.get("old"), fill_value)
new_value = with_value(change.get("new"), fill_value)
diff_value = with_value(
change.get("diff", on_empty_diff), fill_value
)
td.append(
[
fname,
str(item),
_format_field(old_value, precision, round_digits),
_format_field(new_value, precision, round_digits),
_format_field(diff_value, precision, round_digits),
]
)
if no_path:
td.drop("Path")
if not show_changes:
td.drop("Change")
if not old:
td.drop("Old")
td.rename("New", "Value")
return td
def show_diff(
diff,
title: str,
old: bool = True,
no_path: bool = False,
show_changes: bool = True,
precision: int = None,
round_digits: bool = False,
on_empty_diff: str = None,
markdown: bool = False,
) -> None:
td = diff_table(
diff,
title=title,
old=old,
no_path=no_path,
show_changes=show_changes,
precision=precision,
round_digits=round_digits,
on_empty_diff=on_empty_diff,
)
td.render(markdown=markdown)
def metrics_table(
metrics,
all_branches: bool = False,
all_tags: bool = False,
all_commits: bool = False,
precision: int = None,
round_digits: bool = False,
):
from dvc.utils.diff import format_dict
from dvc.utils.flatten import flatten
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}
flattened = (
flatten(format_dict(metric))
if isinstance(metric, dict)
else {"": metric}
)
row_data.update(
{
k: _format_field(v, precision, round_digits)
for k, v in flattened.items()
}
)
td.row_from_dict(row_data)
rev, path, *metrics_headers = td.keys()
td.project(rev, path, *sorted(metrics_headers))
if not any([all_branches, all_tags, all_commits]):
td.drop("Revision")
return td
def show_metrics(
metrics,
markdown: bool = False,
all_branches: bool = False,
all_tags: bool = False,
all_commits: bool = False,
precision: int = None,
round_digits: bool = False,
) -> None:
td = metrics_table(
metrics,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
precision=precision,
round_digits=round_digits,
)
td.render(markdown=markdown)
+3 -3
View File
@@ -63,7 +63,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 +79,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 +154,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:
+6 -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,6 +146,7 @@ SCHEMA = {
"session_token": str,
Optional("listobjects", default=False): Bool, # obsoleted
Optional("use_ssl", default=True): Bool,
"ssl_verify": Any(Bool, str),
"sse": str,
"sse_kms_key_id": str,
"acl": str,
@@ -171,7 +173,7 @@ SCHEMA = {
"allow_agent": Bool,
**REMOTE_COMMON,
},
"hdfs": {"user": str, **REMOTE_COMMON},
"hdfs": {"user": str, "kerb_ticket": str, **REMOTE_COMMON},
"webhdfs": {
"hdfscli_config": str,
"webhdfs_token": str,
@@ -187,6 +189,7 @@ SCHEMA = {
"tenant_id": str,
"client_id": str,
"client_secret": str,
"allow_anonymous_login": bool,
**REMOTE_COMMON,
},
"oss": {
@@ -222,4 +225,5 @@ SCHEMA = {
# enabled by default. It's of no use, kept for backward compatibility.
Optional("parametrization", default=True): Bool
},
"plots": {"html_template": str},
}
+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()
-38
View File
@@ -3,52 +3,14 @@
import logging
import math
import os
import pydoc
import sys
from grandalf.graphs import Edge, Graph, Vertex
from grandalf.layouts import SugiyamaLayout
from grandalf.routing import EdgeViewer, route_with_lines
from dvc.env import DVC_PAGER
from dvc.utils import format_link
logger = logging.getLogger(__name__)
DEFAULT_PAGER = "less"
DEFAULT_PAGER_FORMATTED = "{} --chop-long-lines --clear-screen".format(
DEFAULT_PAGER
)
def make_pager(cmd):
def pager(text):
return pydoc.tempfilepager(pydoc.plain(text), cmd)
return pager
def find_pager():
if not sys.stdout.isatty():
return pydoc.plainpager
env_pager = os.getenv(DVC_PAGER)
if env_pager:
return make_pager(env_pager)
if os.system(f"({DEFAULT_PAGER}) 2>{os.devnull}") == 0:
return make_pager(DEFAULT_PAGER_FORMATTED)
logger.warning(
"Unable to find `less` in the PATH. Check out "
"{} for more info.".format(
format_link("https://man.dvc.org/pipeline/show")
)
)
return pydoc.plainpager
class VertexViewer:
"""Class to define vertex box boundaries that will be accounted for during
graph building by grandalf.
+49 -50
View File
@@ -1,6 +1,11 @@
"""Manages dvc remotes that user can use with push/pull/status commands."""
import logging
from typing import TYPE_CHECKING, Iterable, Optional
if TYPE_CHECKING:
from dvc.objects.file import HashFile
from dvc.remote.base import Remote
logger = logging.getLogger(__name__)
@@ -19,7 +24,11 @@ class DataCloud:
def __init__(self, repo):
self.repo = repo
def get_remote(self, name=None, command="<command>"):
def get_remote(
self,
name: Optional[str] = None,
command: str = "<command>",
) -> "Remote":
from dvc.config import NoRemoteError
if not name:
@@ -49,97 +58,87 @@ class DataCloud:
return get_remote(self.repo, name=name)
def push(
self, cache, jobs=None, remote=None, show_checksums=False,
self,
objs: Iterable["HashFile"],
jobs: Optional[int] = None,
remote: Optional[str] = None,
show_checksums: bool = False,
):
"""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 remote to push to.
By default remote from core.remote config option is used.
show_checksums (bool): show checksums instead of file names in
show_checksums: show checksums instead of file names in
information messages.
"""
remote = self.get_remote(remote, "push")
remote_obj = self.get_remote(remote, "push")
return remote.push(
return remote_obj.push(
self.repo.odb.local,
cache,
objs,
jobs=jobs,
show_checksums=show_checksums,
)
def pull(
self, cache, jobs=None, remote=None, show_checksums=False,
self,
objs: Iterable["HashFile"],
jobs: Optional[int] = None,
remote: Optional[str] = None,
show_checksums: bool = False,
):
"""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 remote to pull from.
By default remote from core.remote config option is used.
show_checksums (bool): show checksums instead of file names in
show_checksums: show checksums instead of file names in
information messages.
"""
remote = self.get_remote(remote, "pull")
remote_obj = self.get_remote(remote, "pull")
return remote.pull(
return remote_obj.pull(
self.repo.odb.local,
cache,
objs,
jobs=jobs,
show_checksums=show_checksums,
)
def transfer(self, source, jobs=None, remote=None, command=None):
"""Transfer data items in a cloud-agnostic way.
Args:
source (str): url for the source location.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.BaseRemote): optional remote to compare
cache to. By default remote from core.remote config option
is used.
command (str): the command which is benefitting from this function
(to be used for reporting better error messages).
"""
from dvc.fs import get_cloud_fs
from_fs = get_cloud_fs(self.repo, url=source)
remote = self.get_remote(remote, command)
return remote.transfer(from_fs, from_fs.path_info, jobs=jobs)
def status(
self,
cache,
jobs=None,
remote=None,
show_checksums=False,
log_missing=True,
objs: Iterable["HashFile"],
jobs: Optional[int] = None,
remote: Optional[str] = None,
show_checksums: bool = False,
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
show_checksums: show checksums instead of file names in
information messages.
log_missing (bool): log warning messages if file doesn't exist
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(
remote_obj = self.get_remote(remote, "status")
return remote_obj.status(
self.repo.odb.local,
cache,
objs,
jobs=jobs,
show_checksums=show_checksums,
log_missing=log_missing,
)
def get_url_for(self, remote, checksum):
remote = self.get_remote(remote)
return str(remote.odb.hash_to_path_info(checksum))
remote_obj = self.get_remote(remote)
return str(remote_obj.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
+21 -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"
@@ -84,19 +85,34 @@ class ParamsDependency(LocalDependency):
def status(self):
return self.workspace_status()
def read_params(self):
def _read(self):
if not self.exists:
return {}
suffix = self.path_info.suffix.lower()
loader = LOADERS[suffix]
try:
config = loader(self.path_info, fs=self.repo.fs)
return loader(self.path_info, fs=self.repo.fs)
except ParseError as exc:
raise BadParamFileError(
f"Unable to read parameters from '{self}'"
) from exc
def read_params_d(self):
config = self._read()
ret = {}
for param in self.params:
dpath.util.merge(
ret,
dpath.util.search(config, param, separator="."),
separator=".",
)
return ret
def read_params(self):
config = self._read()
ret = {}
for param in self.params:
try:
@@ -112,7 +128,7 @@ class ParamsDependency(LocalDependency):
if missing_params:
raise MissingParamsError(
"Parameters '{}' are missing from '{}'.".format(
", ".join(missing_params), self,
", ".join(missing_params), self
)
)
+126 -49
View File
@@ -1,11 +1,19 @@
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, Optional, Set
from voluptuous import Required
from dvc.path_info import PathInfo
from .local import LocalDependency
from .base import Dependency
if TYPE_CHECKING:
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,46 +29,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,
follow_subrepos=False,
).hash_info
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"}
@@ -78,32 +62,28 @@ 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
from dvc.objects.db.git import GitObjectDB
from dvc.repo.fetch import fetch_from_odb
odb = self.repo.odb.local
for odb, objs in self.get_used_objs().items():
if not isinstance(odb, GitObjectDB):
fetch_from_odb(self.repo, odb, objs, jobs=jobs)
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, jobs=jobs, follow_subrepos=False,
)
save(odb, obj, jobs=jobs)
checkout(to.path_info, to.fs, obj, odb)
obj = self.get_obj()
save(self.repo.odb.local, obj, jobs=jobs)
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()
@@ -112,3 +92,100 @@ 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["HashFile"]]:
from dvc.config import NoRemoteError
from dvc.exceptions import NoOutputOrStageError, PathMissingError
from dvc.objects.db.git import GitObjectDB
from dvc.objects.stage import stage
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_objs = 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)
try:
for odb, objs 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
self._check_circular_import(odb)
used_objs[odb].update(objs)
except (NoRemoteError, NoOutputOrStageError):
pass
try:
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
self._staged_objs[rev] = staged_obj
git_odb = GitObjectDB(repo.repo_fs, repo.root_dir)
used_objs[git_odb].add(staged_obj)
return used_objs
def _check_circular_import(self, odb):
from dvc.exceptions import CircularImportError
from dvc.fs.repo import RepoFileSystem
if not odb or not isinstance(odb.fs, RepoFileSystem):
return
self_url = self.repo.url or self.repo.root_dir
if odb.fs.repo_url is not None and odb.fs.repo_url == self_url:
raise CircularImportError(self, odb.fs.repo_url, self_url)
def get_obj(self, filter_info=None, **kwargs):
from dvc.objects.stage import stage
odb = self.repo.odb.local
locked = kwargs.pop("locked", True)
with self._make_repo(locked=locked, cache_dir=odb.cache_dir) as repo:
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
obj = self._staged_objs.get(rev)
if obj is not None:
return obj
path_info = PathInfo(repo.root_dir) / str(self.def_path)
obj = stage(
odb,
path_info,
repo.repo_fs,
odb.fs.PARAM_CHECKSUM,
)
self._staged_objs[rev] = obj
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
-117
View File
@@ -1,117 +0,0 @@
import posixpath
from operator import itemgetter
from funcy import cached_property
from pygtrie import Trie
from .hash_info import HashInfo
def _diff(ancestor, other, allow_removed=False):
from dictdiffer import diff
from dvc.exceptions import MergeError
allowed = ["add"]
if allow_removed:
allowed.append("remove")
result = list(diff(ancestor, other))
for typ, _, _ in result:
if typ not in allowed:
raise MergeError(
"unable to auto-merge directories with diff that contains "
f"'{typ}'ed files"
)
return result
def _merge(ancestor, our, their):
import copy
from dictdiffer import patch
our_diff = _diff(ancestor, our)
if not our_diff:
return copy.deepcopy(their)
their_diff = _diff(ancestor, their)
if not their_diff:
return copy.deepcopy(our)
# make sure there are no conflicting files
_diff(our, their, allow_removed=True)
return patch(our_diff + their_diff, ancestor)
class DirInfo:
PARAM_RELPATH = "relpath"
def __init__(self):
self._dict = {}
def as_dict(self):
return self._dict.copy()
def add(self, key, hash_info):
self.__dict__.pop("trie", None)
self._dict[key] = hash_info
def get(self, key, default=None):
return self._dict.get(key, default)
@cached_property
def trie(self):
return Trie(self._dict)
@property
def size(self):
try:
return sum(hash_info.size for _, hash_info in self._dict.items())
except TypeError:
return None
@property
def nfiles(self):
return len(self._dict)
def items(self):
for key, hash_info in self._dict.items():
yield key, hash_info
@classmethod
def from_list(cls, lst):
ret = DirInfo()
for _entry in lst:
entry = _entry.copy()
relpath = entry.pop(cls.PARAM_RELPATH)
parts = tuple(relpath.split(posixpath.sep))
hash_info = HashInfo.from_dict(entry)
ret.add(parts, hash_info)
return ret
def to_list(self):
# Sorting the list by path to ensure reproducibility
return sorted(
(
{
# NOTE: not using hash_info.to_dict() because we don't want
# size/nfiles fields at this point.
hash_info.name: hash_info.value,
self.PARAM_RELPATH: posixpath.sep.join(parts),
}
for parts, hash_info in self._dict.items() # noqa: B301
),
key=itemgetter(self.PARAM_RELPATH),
)
def merge(self, ancestor, their):
merged_dict = _merge(
ancestor.as_dict(), self.as_dict(), their.as_dict(),
)
merged = DirInfo()
for key, hi in merged_dict.items():
merged.add(key, hi)
return merged
+32 -14
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,7 +199,7 @@ class SingleStageFile(FileMixin):
assert not isinstance(stage, PipelineStage)
if self.verify:
check_dvc_filename(self.path)
check_dvcfile_path(self.repo, self.path)
logger.debug(
"Saving information to '{file}'.".format(file=relpath(self.path))
)
@@ -218,7 +236,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)
+1
View File
@@ -6,3 +6,4 @@ DVCLIVE_PATH = "DVCLIVE_PATH"
DVCLIVE_SUMMARY = "DVCLIVE_SUMMARY"
DVCLIVE_HTML = "DVCLIVE_HTML"
DVCLIVE_RESUME = "DVCLIVE_RESUME"
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
+11 -3
View File
@@ -35,7 +35,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
@@ -283,7 +283,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 +380,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}'."
)
+3 -6
View File
@@ -36,10 +36,7 @@ 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 {}
@@ -144,7 +141,7 @@ def _cached_clone(url, rev, for_write=False):
revision checked out. If for_write is set prevents reusing this dir via
cache.
"""
from distutils.dir_util import copy_tree
from shutil import copytree
# even if we have already cloned this repo, we may need to
# fetch/fast-forward to get specified rev
@@ -156,7 +153,7 @@ def _cached_clone(url, rev, for_write=False):
# Copy to a new dir to keep the clone clean
repo_path = tempfile.mkdtemp("dvc-erepo")
logger.debug("erepo: making a copy of %s clone", url)
copy_tree(clone_path, repo_path)
copytree(clone_path, repo_path)
# Check out the specified revision
if for_write:
+29 -4
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 = {
@@ -82,9 +81,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)
+116 -42
View File
@@ -1,15 +1,19 @@
import asyncio
import logging
import os
import sys
import threading
from contextlib import contextmanager
from funcy import cached_property, wrap_prop
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 = (
@@ -19,11 +23,66 @@ _DEFAULT_CREDS_STEPS = (
)
@contextmanager
def _temp_event_loop():
"""When trying to initalize azure filesystem instances
with DefaultCredentials, the authentication process requires
to have an access to a separate event loop. The normal calls
are run in a separate loop managed by the fsspec, but the
DefaultCredentials assumes that the callee is managing their
own event loop. This function checks whether is there any
event loop set, and if not it creates a temporary event loop
and set it. After the context is finalized, it restores the
original event loop back (if there is any)."""
try:
original_loop = asyncio.get_event_loop()
original_policy = asyncio.get_event_loop_policy()
except RuntimeError:
original_loop = None
original_policy = None
# From 3.8>= and onwards, asyncio changed the default
# loop policy for windows to use proactor loops instead
# of selector based ones. Due to that, proxied connections
# doesn't work with the aiohttp and this is most likely an
# upstream bug that needs to be solved outside of DVC. Until
# such issue is resolved, we need to manage this;
# https://github.com/aio-libs/aiohttp/issues/4536
if sys.version_info >= (3, 8) and os.name == "nt":
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
loop = original_loop or asyncio.new_event_loop()
try:
asyncio.set_event_loop(loop)
yield
finally:
if original_loop is None:
loop.close()
asyncio.set_event_loop(original_loop)
asyncio.set_event_loop_policy(original_policy)
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,45 +93,65 @@ 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
self.login_method, self.login_info = self._prepare_credentials(config)
def _prepare_credentials(self, config):
def _prepare_credentials(self, **config):
from azure.identity.aio import DefaultAzureCredential
# Disable spam from failed cred types for DefaultAzureCredential
logging.getLogger("azure.identity.aio").setLevel(logging.ERROR)
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(login_info.values()):
login_info["credentials"] = DefaultAzureCredential()
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)
):
login_info["credential"] = DefaultAzureCredential(
exclude_interactive_browser_credential=False
)
for login_method, required_keys in [ # noqa
("connection string", ["connection_string"]),
@@ -82,27 +161,19 @@ class AzureFileSystem(FSSpecWrapper):
),
("account key", ["account_name", "account_key"]),
("SAS token", ["account_name", "sas_token"]),
(
f"default credentials ({_DEFAULT_CREDS_STEPS})",
["account_name", "credential"],
),
("anonymous login", ["account_name"]),
(f"default credentials ({_DEFAULT_CREDS_STEPS})", ["credentials"]),
]:
if all(login_info.get(key) is not None for key in required_keys):
break
else:
login_method = None
return login_method, 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")
self.login_method = login_method
return login_info
@wrap_prop(threading.Lock())
@cached_property
@@ -111,11 +182,8 @@ class AzureFileSystem(FSSpecWrapper):
from azure.core.exceptions import AzureError
try:
file_system = AzureBlobFileSystem(**self.login_info)
if self.bucket not in [
container.rstrip("/") for container in file_system.ls("/")
]:
file_system.mkdir(self.bucket)
with _temp_event_loop():
file_system = AzureBlobFileSystem(**self.fs_args)
except (ValueError, AzureError) as e:
raise AzureAuthError(
f"Authentication to Azure Blob Storage via {self.login_method}"
@@ -124,3 +192,9 @@ class AzureFileSystem(FSSpecWrapper):
) from e
return file_system
def open(
self, path_info, mode="r", **kwargs
): # pylint: disable=arguments-differ
with _temp_event_loop():
return self.fs.open(self._with_bucket(path_info), mode=mode)
+50 -42
View File
@@ -1,11 +1,9 @@
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import partial
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,10 +33,12 @@ class RemoteMissingDepsError(DvcException):
class BaseFileSystem:
sep = "/"
scheme = "base"
REQUIRES: ClassVar[Dict[str, str]] = {}
PATH_CLS = URLInfo # type: Any
JOBS = 4 * cpu_count()
_JOBS = 4 * cpu_count()
CHECKSUM_DIR_SUFFIX = ".dir"
HASH_JOBS = max(1, min(4, cpu_count() // 2))
@@ -54,29 +54,19 @@ 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.path_info = None
@classmethod
def _strip_protocol(cls, path: str):
return path
@cached_property
def jobs(self):
return (
self.config.get("jobs")
or (self.repo and self.repo.config["core"].get("jobs"))
or self.JOBS
)
@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 +81,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 +90,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 +129,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 +156,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.
@@ -175,9 +169,12 @@ class BaseFileSystem:
"""
raise NotImplementedError
def ls(self, path_info, detail=False, **kwargs):
def ls(self, path_info, detail=False):
raise RemoteActionNotImplemented("ls", self.scheme)
def find(self, path_info, detail=False, prefix=None):
raise RemoteActionNotImplemented("find", self.scheme)
def is_empty(self, path_info):
return False
@@ -219,9 +216,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)
@@ -242,14 +237,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,
@@ -258,6 +262,7 @@ class BaseFileSystem:
name=None,
no_progress_bar=False,
jobs=None,
_only_file=False,
**kwargs,
):
if not hasattr(self, "_download"):
@@ -273,16 +278,21 @@ class BaseFileSystem:
if to_info.scheme != "local":
raise NotImplementedError
if self.isdir(from_info):
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:
makedirs(to_info, exist_ok=True)
return None
to_infos = (
to_info / info.relative_to(from_info) for info in from_infos
)
@@ -294,7 +304,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:
@@ -318,9 +328,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)
+22 -22
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,14 @@ 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"]
def _find_outs(self, path, *args, **kwargs):
outs = self.repo.find_outs_by_path(path, *args, **kwargs)
@@ -42,15 +45,15 @@ 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)
file_hash = out.hash_info.dir_info.get(key)
if file_hash:
return file_hash
obj = out.obj.trie.get(key)
if obj:
return obj.hash_info
raise FileNotFoundError
def open( # type: ignore
@@ -67,13 +70,17 @@ 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
except NoRemoteError as exc:
raise FileNotFoundError from exc
if out.is_dir_checksum:
checksum = self._get_granular_hash(path, out).value
else:
@@ -131,21 +138,14 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
# pull dir cache if needed
out.get_dir_cache(**kwargs)
dir_cache = out.dir_cache
if not dir_cache:
raise FileNotFoundError
from dvc.objects import Tree
hash_info = Tree.save_dir_info(out.odb, dir_cache)
if hash_info != out.hash_info:
if not out.obj:
raise FileNotFoundError
def _add_dir(self, trie, out, **kwargs):
self._fetch_dir(out, **kwargs)
base = out.path_info.parts
for key, _ in out.dir_cache.items(): # noqa: B301
for key, _ in out.obj: # noqa: B301
trie[base + key] = None
def _walk(self, root, trie, topdown=True, **kwargs):
@@ -241,9 +241,9 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
elif meta.part_of_output:
(out,) = meta.outs
key = path_info.relative_to(out.path_info).parts
hash_info = out.hash_info.dir_info.trie.get(key)
if hash_info:
ret["size"] = hash_info.size
ret[hash_info.name] = hash_info.value
obj = out.obj.trie.get(key)
if obj:
ret["size"] = obj.size
ret[obj.hash_info.name] = obj.hash_info.value
return ret
+104 -23
View File
@@ -1,5 +1,8 @@
import os
import shutil
from functools import lru_cache
from funcy import cached_property
from dvc.progress import Tqdm
@@ -8,27 +11,40 @@ from .base import BaseFileSystem
# pylint: disable=no-member
class FSSpecWrapper(BaseFileSystem):
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
def _strip_bucket(self, entry):
_, entry = entry.split("/", 1)
return 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
def _strip_buckets(self, entries, detail, prefix=None):
return path or bucket
def _strip_buckets(self, entries, detail=False):
for entry in entries:
if detail:
entry = self._entry_hook(entry.copy())
entry["name"] = self._strip_bucket(entry["name"])
else:
entry = self._strip_bucket(
f"{prefix}/{entry}" if prefix else entry
)
entry = self._strip_bucket(entry)
yield entry
def _entry_hook(self, entry):
@@ -36,11 +52,33 @@ class FSSpecWrapper(BaseFileSystem):
entries within info() and ls(detail=True) calls"""
return entry
def isdir(self, path_info):
def _prepare_credentials(
self, **config
): # pylint: disable=unused-argument
"""Prepare the arguments for authentication to the
host filesystem"""
return {}
def _isdir(self, path_info):
return self.fs.isdir(self._with_bucket(path_info))
def isdir(self, path_info):
try:
return self._isdir(path_info)
except FileNotFoundError:
return False
def isfile(self, path_info):
return self.fs.isfile(self._with_bucket(path_info))
try:
return not self._isdir(path_info)
except FileNotFoundError:
return False
def is_empty(self, path_info):
entry = self.info(path_info)
if entry["type"] == "directory":
return not self.fs.ls(self._with_bucket(path_info))
return entry["size"] == 0
def open(
self, path_info, mode="r", **kwargs
@@ -50,24 +88,25 @@ class FSSpecWrapper(BaseFileSystem):
def copy(self, from_info, to_info):
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, recursive=False
): # pylint: disable=arguments-differ
def ls(self, path_info, detail=False):
path = self._with_bucket(path_info)
if recursive:
for root, _, files in self.fs.walk(path, detail=detail):
if detail:
files = files.values()
yield from self._strip_buckets(files, detail, prefix=root)
return None
files = self.fs.ls(path, detail=detail)
yield from self._strip_buckets(files, detail=detail)
yield from self._strip_buckets(self.ls(path, detail=detail), detail)
# 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()
yield from self._strip_buckets(files, detail=detail)
def walk_files(self, path_info, **kwargs):
for file in self.ls(path_info, recursive=True):
for file in self.find(path_info, **kwargs):
yield path_info.replace(path=file)
def remove(self, path_info):
@@ -79,20 +118,20 @@ class FSSpecWrapper(BaseFileSystem):
info["name"] = self._strip_bucket(info["name"])
return info
def _upload_fobj(self, fobj, to_info):
def _upload_fobj(self, fobj, to_info, size=None):
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)
size = os.path.getsize(from_file)
with open(from_file, "rb") as fobj:
self.upload_fobj(
fobj,
self._with_bucket(to_info),
size=size,
desc=name,
total=total,
no_progress_bar=no_progress_bar,
)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
@@ -113,3 +152,45 @@ 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
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 = 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)
+46 -27
View File
@@ -85,7 +85,7 @@ class GDriveURLInfo(CloudURLInfo):
self._spath = re.sub("/{2,}", "/", self._spath.rstrip("/"))
class GDriveFileSystem(BaseFileSystem):
class GDriveFileSystem(BaseFileSystem): # pylint:disable=abstract-method
scheme = Schemes.GDRIVE
PATH_CLS = GDriveURLInfo
PARAM_CHECKSUM = "checksum"
@@ -100,8 +100,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,17 +126,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(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 (
@@ -208,8 +217,15 @@ class GDriveFileSystem(BaseFileSystem):
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive
if os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA):
with open(self._gdrive_user_credentials_path, "w") as cred_file:
temporary_save_path = self._gdrive_user_credentials_path
is_credentials_temp = os.getenv(
GDriveFileSystem.GDRIVE_CREDENTIALS_DATA
)
if self._use_service_account:
temporary_save_path = self._gdrive_service_credentials_path
if is_credentials_temp:
with open(temporary_save_path, "w") as cred_file:
cred_file.write(
os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA)
)
@@ -217,6 +233,7 @@ class GDriveFileSystem(BaseFileSystem):
auth_settings = {
"client_config_backend": "settings",
"save_credentials": True,
"save_credentials_backend": "file",
"save_credentials_file": self._gdrive_user_credentials_path,
"get_refresh_token": True,
"oauth_scope": [
@@ -226,13 +243,16 @@ class GDriveFileSystem(BaseFileSystem):
}
if self._use_service_account:
auth_settings["save_credentials_backend"] = "json"
auth_settings["service_config"] = {
"client_user_email": self._service_account_user_email,
"client_json_file_path": self._service_account_json_file_path,
}
if is_credentials_temp:
auth_settings["service_config"][
"client_json_file_path"
] = temporary_save_path
else:
auth_settings["save_credentials_backend"] = "file"
auth_settings["client_config"] = {
"client_id": self._client_id or self.DEFAULT_GDRIVE_CLIENT_ID,
"client_secret": self._client_secret
@@ -267,8 +287,8 @@ class GDriveFileSystem(BaseFileSystem):
except Exception as exc:
raise GDriveAuthError(self.credentials_location) from exc
finally:
if os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA):
os.remove(self._gdrive_user_credentials_path)
if is_credentials_temp:
os.remove(temporary_save_path)
return GoogleDrive(gauth)
@@ -503,7 +523,7 @@ class GDriveFileSystem(BaseFileSystem):
assert not create
raise FileMissingError(path_info, hint)
def exists(self, path_info, use_dvcignore=True):
def exists(self, path_info) -> bool:
try:
self._get_item_id(path_info)
except FileMissingError:
@@ -518,7 +538,7 @@ class GDriveFileSystem(BaseFileSystem):
query = f"({query}) and trashed=false"
return self._gdrive_list(query)
def _ls_recursive(self, path_info, detail=False):
def find(self, path_info, detail=False, prefix=None):
root_path = path_info.path
seen_paths = set()
@@ -555,13 +575,7 @@ class GDriveFileSystem(BaseFileSystem):
else:
yield item_path
def ls(
self, path_info, detail=False, recursive=False
): # pylint: disable=arguments-differ
if recursive:
yield from self._ls_recursive(path_info, detail=detail)
return None
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]
@@ -591,8 +605,8 @@ class GDriveFileSystem(BaseFileSystem):
self._cache_path_id(root_path, *dir_ids)
def walk_files(self, path_info, **kwargs):
for filename in self.ls(path_info, recursive=True):
yield path_info.replace(path=filename)
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)
@@ -604,22 +618,27 @@ class GDriveFileSystem(BaseFileSystem):
gdrive_file.FetchMetadata(fields="fileSize")
return {"size": gdrive_file.get("fileSize")}
def _upload_fobj(self, fobj, to_info):
def _upload_fobj(self, fobj, to_info, **kwargs):
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
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,
size=os.path.getsize(from_file),
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):
+22 -72
View File
@@ -1,8 +1,6 @@
import errno
import os
from funcy import cached_property
from dvc.utils import is_exec, relpath
from .base import BaseFileSystem
@@ -11,43 +9,31 @@ 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
@@ -66,50 +52,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 +86,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):
@@ -158,17 +117,8 @@ class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
errno.ENOENT, os.strerror(errno.ENOENT), path
)
@property
def hash_jobs(self): # pylint: disable=invalid-overridden-method
# NOTE: gitpython is not threadsafe. See
# https://github.com/iterative/dvc/issues/4079
return 1
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)
+9 -14
View File
@@ -6,32 +6,27 @@ 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)
self.login_info = self._prepare_credentials(config)
def _prepare_credentials(self, config):
login_info = {}
def _prepare_credentials(self, **config):
login_info = {"consistency": None}
login_info["project"] = config.get("projectname")
login_info["token"] = config.get("credentialpath")
return login_info
def _entry_hook(self, entry):
entry["etag"] = base64.b64decode(entry["etag"]).hex()
if "etag" in entry:
entry["etag"] = base64.b64decode(entry["etag"]).hex()
return entry
@wrap_prop(threading.Lock())
@@ -39,4 +34,4 @@ class GSFileSystem(FSSpecWrapper):
def fs(self):
from gcsfs import GCSFileSystem
return GCSFileSystem(**self.login_info)
return GCSFileSystem(**self.fs_args)
+21 -25
View File
@@ -6,7 +6,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 +64,23 @@ 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
self.host = config["host"]
self.user = config.get("user")
self.port = config.get("port")
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.kerb_ticket = config.get("kerb_ticket")
@staticmethod
def hdfs(path_info):
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
# NOTE: HadoopFileSystem is not meant to be closed by us and doesn't
@@ -100,6 +95,7 @@ class HDFSFileSystem(BaseFileSystem):
path_info.host,
path_info.port,
user=path_info.user,
kerb_ticket=self.kerb_ticket,
)
@contextmanager
@@ -121,7 +117,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:
@@ -157,15 +153,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):
@@ -234,7 +230,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)
+16 -21
View File
@@ -1,10 +1,12 @@
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
@@ -35,16 +37,10 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
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)
@@ -54,12 +50,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":
@@ -102,7 +98,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,
)
@@ -137,7 +133,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
@@ -146,13 +142,12 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
raise HTTPError(res.status_code, res.reason)
def info(self, path_info):
url = path_info.url
headers = self._head(url).headers
etag = headers.get("ETag") or headers.get("Content-MD5")
size = int(headers.get("Content-Length"))
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}
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,12 +184,12 @@ 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
def _content_length(response):
def _content_length(response) -> Optional[int]:
res = response.headers.get("Content-Length")
return int(res) if res else None
+15 -67
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):
@@ -133,9 +87,6 @@ class LocalFileSystem(BaseFileSystem):
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):
@@ -154,7 +105,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:
@@ -210,7 +161,7 @@ class LocalFileSystem(BaseFileSystem):
}
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 +178,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)
+7 -4
View File
@@ -5,21 +5,24 @@ class MemoryFileSystem(BaseFileSystem):
scheme = "local"
PARAM_CHECKSUM = "md5"
def __init__(self, repo, config):
def __init__(self, **kwargs):
from fsspec.implementations.memory import MemoryFileSystem as MemFS
super().__init__(repo, config)
super().__init__(**kwargs)
self.fs = MemFS()
def exists(self, path_info, use_dvcignore=True):
return self.fs.exists(path_info.path)
def exists(self, path_info) -> bool:
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
+19 -29
View File
@@ -39,13 +39,9 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
COPY_POLL_SECONDS = 5
LIST_OBJECT_PAGE_SIZE = 100
def __init__(self, repo, config):
super().__init__(repo, config)
def __init__(self, **config):
super().__init__(**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 = (
@@ -68,27 +64,19 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
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)
return oss2.Auth(self.key_id, self.key_secret)
# 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 _get_bucket(self, bucket):
import oss2
return oss2.Bucket(self.oss_service, self.endpoint, bucket)
def _generate_download_url(self, path_info, expires=3600):
assert path_info.bucket == self.bucket
return self._get_bucket(path_info.bucket).sign_url(
"GET", path_info.path, expires
)
return self.oss_service.sign_url("GET", path_info.path, expires)
def exists(self, path_info, use_dvcignore=True):
def exists(self, path_info) -> bool:
paths = self._list_paths(path_info)
return any(path_info.path == path for path in paths)
@@ -96,7 +84,7 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
import oss2
for blob in oss2.ObjectIterator(
self.oss_service, prefix=path_info.path
self._get_bucket(path_info.bucket), prefix=path_info.path
):
yield blob.key
@@ -114,16 +102,17 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
raise NotImplementedError
logger.debug(f"Removing oss://{path_info}")
self.oss_service.delete_object(path_info.path)
self._get_bucket(path_info.bucket).delete_object(path_info.path)
def _upload_fobj(self, fobj, to_info):
self.oss_service.put_object(to_info.path, fobj)
def _upload_fobj(self, fobj, to_info, **kwargs):
self._get_bucket(to_info.bucket).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(
bucket = self._get_bucket(to_info.bucket)
bucket.put_object_from_file(
to_info.path, from_file, progress_callback=pbar.update_to
)
@@ -133,8 +122,9 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
with Tqdm(desc=name, disable=no_progress_bar, bytes=True) as pbar:
import oss2
bucket = self._get_bucket(from_info.bucket)
oss2.resumable_download(
self.oss_service,
bucket,
from_info.path,
to_file,
progress_callback=pbar.update_to,
+49 -47
View File
@@ -31,13 +31,15 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
kwargs: Additional keyword arguments passed to the `DvcFileSystem()`.
"""
sep = os.sep
scheme = "local"
PARAM_CHECKSUM = "md5"
def __init__(
self, repo, subrepos=False, repo_factory: RepoFactory = None,
self, repo=None, subrepos=False, repo_factory: RepoFactory = None
):
super().__init__(repo, {"url": repo.root_dir})
super().__init__()
from dvc.utils.collections import PathStringTrie
@@ -49,6 +51,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 +64,15 @@ 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
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 +102,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 +117,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
@@ -142,19 +150,20 @@ 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
@@ -167,6 +176,9 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
def isdir(self, path): # pylint: disable=arguments-differ
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)
@@ -195,6 +207,9 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
def isfile(self, path): # pylint: disable=arguments-differ
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 +253,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 +319,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 +345,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,16 +360,20 @@ 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
@@ -380,15 +386,11 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
with open(to_file, "wb+") as to_fobj:
with Tqdm.wrapattr(
to_fobj, "write", desc=name, disable=no_progress_bar,
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
def metadata(self, path):
abspath = os.path.abspath(path)
path_info = PathInfo(abspath)
+157 -389
View File
@@ -1,74 +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.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",
@@ -76,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:
@@ -93,370 +57,174 @@ 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,
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"],
)
@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 ls(
self, path_info, detail=False, recursive=False
): # pylint: disable=arguments-differ
assert recursive
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)
+53 -63
View File
@@ -5,11 +5,10 @@ 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 import prompt
from dvc.hash_info import HashInfo
from dvc.scheme import Schemes
@@ -30,10 +29,10 @@ def ask_password(host, user, port):
)
class SSHFileSystem(BaseFileSystem):
class SSHFileSystem(BaseFileSystem): # pylint:disable=abstract-method
scheme = Schemes.SSH
REQUIRES = {"paramiko": "paramiko"}
JOBS = 4
_JOBS = 4
PARAM_CHECKSUM = "md5"
# At any given time some of the connections will go over network and
@@ -46,36 +45,21 @@ class SSHFileSystem(BaseFileSystem):
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)
def __init__(self, **config):
super().__init__(**config)
user_ssh_config = self._load_user_ssh_config(config["host"])
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.host = user_ssh_config.get("hostname", config["host"])
self.user = (
config.get("user")
or user_ssh_config.get("user")
or getpass.getuser()
)
self.port = (
config.get("port")
or self._try_get_ssh_config_port(user_ssh_config)
or self.DEFAULT_PORT
)
self.keyfile = config.get(
"keyfile"
@@ -93,6 +77,16 @@ class SSHFileSystem(BaseFileSystem):
self.sock = None
self.allow_agent = config.get("allow_agent", True)
@staticmethod
def _get_kwargs_from_urls(urlpath):
from fsspec.implementations.sftp import SFTPFileSystem
# pylint:disable=protected-access
kwargs = SFTPFileSystem._get_kwargs_from_urls(urlpath)
if "username" in kwargs:
kwargs["user"] = kwargs.pop("username")
return kwargs
@staticmethod
def ssh_config_filename():
return os.path.expanduser(os.path.join("~", ".ssh", "config"))
@@ -120,25 +114,21 @@ class SSHFileSystem(BaseFileSystem):
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
def ensure_credentials(self):
# 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)
self.password = ask_password(self.host, self.user, self.port)
def ssh(self, path_info):
self.ensure_credentials(path_info)
def ssh(self):
self.ensure_credentials()
from .connection import SSHConnection
return get_connection(
SSHConnection,
path_info.host,
username=path_info.user,
port=path_info.port,
self.host,
username=self.user,
port=self.port,
key_filename=self.keyfile,
timeout=self.timeout,
password=self.password,
@@ -151,7 +141,7 @@ class SSHFileSystem(BaseFileSystem):
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(
with self.ssh() as ssh, closing(
ssh.sftp.open(path_info.path, mode)
) as fd:
if "b" in mode:
@@ -159,20 +149,20 @@ class SSHFileSystem(BaseFileSystem):
else:
yield io.TextIOWrapper(fd, encoding=encoding)
def exists(self, path_info, use_dvcignore=True):
with self.ssh(path_info) as ssh:
def exists(self, path_info) -> bool:
with self.ssh() as ssh:
return ssh.exists(path_info.path)
def isdir(self, path_info):
with self.ssh(path_info) as ssh:
with self.ssh() as ssh:
return ssh.isdir(path_info.path)
def isfile(self, path_info):
with self.ssh(path_info) as ssh:
with self.ssh() as ssh:
return ssh.isfile(path_info.path)
def walk_files(self, path_info, **kwargs):
with self.ssh(path_info) as ssh:
with self.ssh() as ssh:
for fname in ssh.walk_files(path_info.path):
yield path_info.replace(path=fname)
@@ -180,32 +170,32 @@ class SSHFileSystem(BaseFileSystem):
if path_info.scheme != self.scheme:
raise NotImplementedError
with self.ssh(path_info) as ssh:
with self.ssh() as ssh:
ssh.remove(path_info.path)
def makedirs(self, path_info):
with self.ssh(path_info) as ssh:
with self.ssh() 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:
with self.ssh() 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:
with self.ssh() 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:
with self.ssh() as ssh:
ssh.symlink(from_info.path, to_info.path)
def hardlink(self, from_info, to_info):
@@ -215,7 +205,7 @@ class SSHFileSystem(BaseFileSystem):
# See dvc/remote/local/__init__.py - hardlink()
if self.getsize(from_info) == 0:
with self.ssh(to_info) as ssh:
with self.ssh() as ssh:
ssh.sftp.open(to_info.path, "w").close()
logger.debug(
@@ -225,18 +215,18 @@ class SSHFileSystem(BaseFileSystem):
)
return
with self.ssh(from_info) as ssh:
with self.ssh() 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:
with self.ssh() as ssh:
ssh.reflink(from_info.path, to_info.path)
def md5(self, path_info):
with self.ssh(path_info) as ssh:
with self.ssh() as ssh:
return HashInfo(
"md5",
ssh.md5(path_info.path),
@@ -244,16 +234,16 @@ class SSHFileSystem(BaseFileSystem):
)
def info(self, path_info):
with self.ssh(path_info) as ssh:
with self.ssh() as ssh:
return ssh.info(path_info.path)
def _upload_fobj(self, fobj, to_info):
def _upload_fobj(self, fobj, to_info, **kwargs):
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:
with self.ssh() as ssh:
ssh.download(
from_info.path,
to_file,
@@ -264,7 +254,7 @@ class SSHFileSystem(BaseFileSystem):
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
with self.ssh(to_info) as ssh:
with self.ssh() as ssh:
ssh.upload(
from_file,
to_info.path,

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