Compare commits

..
304 Commits
Author SHA1 Message Date
Ruslan Kuprieiev 38dc0f85d4 dvc: bump to 1.0.0b3 2020-06-15 22:15:57 +03:00
Paweł Redzyński d2dbe9649c plots: add smooth template (#4046) 2020-06-15 22:15:27 +03:00
Ruslan Kuprieiev ece6f2d3ee dvc: bump to 1.0.0b2 2020-06-15 21:36:05 +03:00
Ruslan Kuprieiev b672622324 plots: make sure template is using data anchor (#4045)
Fixes #3969
2020-06-15 21:14:07 +03:00
Ruslan Kuprieiev c9391df313 plots: modify: handle --unset for nonexistent options (#4044)
* tests: plots: reorginize the structure

* plots: modify: handle --unset for nonexistent options

Fixes #3933
2020-06-15 20:31:08 +03:00
Peter Rowlands (변기호) a0c229cf9b remote: continue remote/cache/tree refactoring (#4041)
* remote: move constants into trees

* remote: replace Remote() Cache() helper functions

* output: update for remote changes

* cache/data_cloud: update for remote changes

* dependency: update for remote changes

* stage: update for remote changes

* remote: move remaining non-index methods into tree

* tests: update remote unit tests

* remote: LocalCache needs to lock remote index, not cache index

* fix bugs from moved PARAM_CHECKSUM/PARAM_RELPATH

* tests: repo tree open needs state context

* remote: make Remote.gc a class method

* operates on obj.tree, so that GC can be done with both remote and
  cache instances

* bug fixes

* tests: update test_data_cloud

* bug fixes

* tests: update func tests
2020-06-15 16:22:55 +03:00
karajan1001 a59f90fac9 Performance improvement of dvcignore (#3967)
fix#3869
1.Use big regex.

* Solve windows

* add rule order test

* Solve ignore order

* remove list comprehensions
2020-06-15 11:37:01 +02:00
Ruslan Kuprieiev bba502314d dvc: bump to 1.0.0b1 2020-06-14 20:44:44 +03:00
Saugat Pachhai bc4bf66d17 (bugfix) get/import: fix broken progress bar (#4039)
PR #3672 (6d8499e) extended `LocalRemote::_get_plans` to return one
`checksums` too. As all of the args from `_get_plans` was passed
down to `download()`, it recognized extra arg of checksum as
`no_progress_bar` due to which it became True and stopped showing
progress bar at all.

Fix #3874
2020-06-14 20:42:53 +03:00
f7348af1e0 metrics & plots: help output improvements (#3963)
* metrics & plots: help output improvements

* metrics & plots: ui: updated help messages

* refactor: removed redundant parentheses

* metrics: ui: updated 'targets' description in 'metrics show'

* metrics: ui: reordered params to match the order in cmd ref

* plots: ui: params modified to match cmd ref

* plots: ui: updated diff help

* Update dvc/command/plots.py

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

* Update dvc/command/plots.py

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

* Update dvc/command/plots.py

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

* Update dvc/command/plots.py

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

* Update dvc/command/plots.py

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

* Update dvc/command/plots.py

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

Co-authored-by: Ruslan Kuprieiev <[email protected]>
Co-authored-by: Jorge Orpinel <[email protected]>
2020-06-14 20:39:33 +03:00
Casper da Costa-Luis 4d140a9944 gdrive: add open (#3916)
* gdrive: add open

Fixes #3408
Related #2865
Fixes #3897

* dependency: add gdrive

* test: api: open: gdrive

* rollback pydrive dependency

* Revert "dependency: add gdrive"

This reverts commit fd333261d2e9fe1364cc278138c79890a1f5fe7a.

* tests: remotes: GDrive: fix get_url

* tests: api: fully test GDrive

* minor typo fix

* attempt gdrive tests fix

* fix gdrive credentials config

* replace GDrivePathNotFound => FileMissingError

* fix test_open_external[GDrive]

* add ensure_dir_scm

* minor exception fix
2020-06-13 23:53:25 +03:00
Casper da Costa-Luis 2ba73daedc snap: make future major version upgrades easier (#4038)
Related: #3999
Related: #4037
2020-06-13 23:27:03 +03:00
Ruslan Kuprieiev 7a31a35cbf dvc: bump to 1.0.0b0 2020-06-13 00:56:14 +03:00
Saugat Pachhai 24a14563cc run: preserve order when -f/--force overwrite stage (#4035)
This commit preserves comments, meta and order inside a dvc.yaml
when `-f/--force` is used. But, it does not maintain comments/meta
for .dvc files.

Related: #4014
2020-06-13 00:55:15 +03:00
Ruslan Kuprieiev a1d3440f85 metrics: diff: introduce --precision (#4026)
Rounding to 5 digits by default.

Fixes #3904
2020-06-13 00:54:42 +03:00
Saugat Pachhai 45dcb7c27d Update dag.py (#4036) 2020-06-12 20:33:19 +03:00
Saugat Pachhai 9ff6fef53a bugfix: get dump folder contents in respective output folder (#4034)
The relative path for external git directory was computed relative to
the root of the repo itself, which created a relative path which is
current working directory. So, all of the contents of the erepo to download
was dumped in the pwd rather than inside of it's own directory.

Fix #4033
2020-06-12 20:31:18 +03:00
Saugat PachhaiandRuslan Kuprieiev 9e7dc2640e schema: provide a jsonschema for dvc.yaml (#4028)
* schema: provide a jsonschema for dvc.yaml

* Update scripts/schema/dvc-yaml.json

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

* Update scripts/schema/dvc-yaml.json

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-06-12 20:09:59 +05:45
Paweł Redzyński 584c97a58d plots: fix handling of multiple plots from single revision (#4031) 2020-06-12 16:08:08 +03:00
Saugat Pachhai b94a18cfb9 tests: tests for dvcfile (#4032) 2020-06-12 15:54:05 +03:00
Peter Rowlands (변기호) 9ead641c95 remote: separate cloud remote and cloud cache classes (#4019)
* remote: move get_file_checksum() into tree

* makes RemoteTree and RepoTree consistent with regard to checksum
  calculation

* remote: save() now takes explicit tree parameter

* when tree is remote.tree, save will be a move + link operation
  (same as default existing behavior)
* when saving path from a different tree, save will be a copy operation

* tests: update for moved remote/tree functions

* remote: move get_checksum into tree

* remote: separate cloud remote and cache classes

* tests: update unit tests for remote/cache separation

* remote: cloud cache should extend cloud remote

* remote: move LocalRemote.get

* tests: update func tests

* dependency: update for moved get_checksum

* remote: fix state lookup bug
2020-06-12 15:52:47 +03:00
Peter Rowlands (변기호) 652f5ab439 remote: use separate working tree instance for local cache (#3991)
* dvcignore: ignore paths outside of CleanTree root

* remote: local remote/cache should always use its own working tree

* repo tree (CleanTree) instance should not be relevant to local cache
  operations. When the dvcignore CleanTree is relevant, repo.tree should
  be used. For local remote/cache operations, the local remote work_tree
  should be used.

* state: tie state to local cache working tree

* tests: update tests

* state: use os.path.exists instead of WorkingTree.exists in some cases
2020-06-12 12:52:38 +03:00
bjonnhandJonathan Bisson 28a123e727 Remove a remnant umask to solve #4021 (#4022)
Co-authored-by: Jonathan Bisson <[email protected]>
2020-06-11 19:47:25 +03:00
Ruslan Kuprieiev 566472f088 dvc: bump to 1.0.0a11 2020-06-11 19:09:51 +03:00
Saugat PachhaiandRuslan Kuprieiev 2e12a31c5a tests: cover run cache with functional tests (#4018)
* tests: cover run cache with functional tests

* run-cache: use _log_exceptions wrapper

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-06-11 18:44:21 +03:00
Alexander Schepanovski d7788887f0 plots: fix a couple of bugs (#4017)
- running .plots.collect() not from repo root used to skip plot files
- make Plots.render() be more convenient to be called from outside
2020-06-11 18:20:12 +07:00
Ruslan Kuprieiev 87276faef2 RepoTree: work with dirty repos (#4005)
Currently `RepoTree.open` will throw FileNotFoundError if you have a
file that is tracked by dvc, but which has no md5 or a cache for which
is missing, even though you have the file itself in your workspace.

This corresponds very nicely to the `--no-commit` or `--no-exec`
scenario, where you might not want to commit your artifacts to cache
just yet.

Fixes #3974
2020-06-11 09:50:27 +03:00
Saugat Pachhai 4042d5232d run: use -f/--force to overwrite, fix msg (#4011)
Also, makes fixes for `--overwrite` rename to `--force`
and also remove `remove_with_prompt` which was only being
used with `force\=True` anyway.

Fix #4003
2020-06-10 20:17:27 +03:00
Saugat Pachhai fda14c82d3 plots modify: do not convert output types to plot on unset (#4009)
Fix #4006
2020-06-10 15:31:17 +03:00
Saugat Pachhai 64be262d18 dvc.yaml: preserve meta (#4008) 2020-06-10 14:37:21 +03:00
Saugat Pachhai 1d6c80001d plots modify: complain if template does not exist (#3995) 2020-06-10 16:12:56 +05:45
Ruslan Kuprieiev 91a7194e4f add/run/import-url: don't use -f as --file (#4004)
If anything, `-f` is closer to `--force` than to `--file`. So let's set
this straight.
2020-06-10 05:04:55 +03:00
Ruslan Kuprieiev afe539aaba plots: rename --no-csv-header to --no-header (#4001)
* plots: rename --no-csv-header to --no-header

Fixes #3996

* fix typo
2020-06-10 03:34:25 +03:00
Ruslan Kuprieiev f259bc52cd config/remote/cache: make --system/--global/--local mutually exclusive (#4000) 2020-06-10 02:32:18 +03:00
Ruslan Kuprieiev db69def834 status: introduce --show-json (#3998)
Fixes #1406
2020-06-10 01:51:30 +03:00
Casper da Costa-Luis 5efc3bbee2 snap: build fixes (#3990)
* snap: drop sudo

Fixes #3989

* snap: ci: fix builds

* snap: ci: developer notifications

* snap: minimise size

* debug commit

* Revert "snap: minimise size"

This reverts commit 77b41a96f68b2c8df833803d0a644172b7db4779.

* fix image info

* Revert "debug commit"

This reverts commit 232baf0ad78335b0f3806456a3f20b82cdf0f24c.

* snap: v1 channels

Related: #3872
2020-06-10 00:00:15 +03:00
Alexander Schepanovski 7230d87137 Refactor plots (#3994)
- separate data collection and rendering
- provide a way to use custom template loader
- drop hard-coding datafiles in template feature
2020-06-09 22:47:17 +03:00
Paweł Redzyński 56833387ce plots: find metric data in yaml dictionary (#3992) 2020-06-09 16:42:28 +03:00
Saugat Pachhai 593e67318b do not consider stage cached if checksum/cmd has changed (#3993) 2020-06-09 18:46:49 +05:45
Saugat Pachhai afc183f4e7 tests serializing of stage to lockfile (#3988)
* tests: serialize stage for lockfile

* config: make isort/black compatible

* convert class based tests to pytest
2020-06-09 15:48:17 +03:00
Saugat Pachhai d201fad1e3 tests: serialize pipeline file (#3985)
serialize: make params ordered
2020-06-09 14:22:55 +03:00
Ruslan Kuprieiev 5be12b5aac plots: fix help message (#3983)
Per https://github.com/iterative/dvc/pull/3911#discussion_r436238442
2020-06-09 14:15:15 +03:00
Saugat Pachhai c1e5b10cbd remove unnecessary dependencies (#3987)
Fix #3971
2020-06-09 12:43:34 +03:00
Saugat Pachhai b47b2c5673 tests: update mock-ssh-server (#3986) 2020-06-09 12:39:10 +03:00
Ruslan Kuprieiev 939f78948a remote: tree: don't ignore exceptions (#3977)
Moving ignore logic to the remote itself, so that tree could behave
properly.
2020-06-09 11:25:13 +03:00
Saugat Pachhai 215ea4944c log: show modifying if entry already exists in dvcfile (#3978) 2020-06-08 18:56:00 +03:00
Saugat Pachhai 4e12e76e19 tests: unit testing for StageLoader for Pipelinefile (#3976) 2020-06-08 18:55:16 +03:00
Saugat Pachhai 33092e8614 issue template: do not auto-add labels on new issues (#3979) 2020-06-08 19:47:42 +05:45
Saugat Pachhai ae7b604531 Fix loading of params from stage cache (#3972) 2020-06-08 12:43:13 +03:00
Ruslan Kuprieiev b7c174641e tests: mark s3 test as flaky 2020-06-08 12:27:25 +03:00
Ruslan Kuprieiev cb43f9cdda dvc: bump to 1.0.0a10 2020-06-08 11:56:58 +03:00
Ruslan Kuprieiev 791ce22759 pyinstaller: add hook for google-api-python-client (#3965)
Fixes #3913
2020-06-08 11:53:12 +03:00
Casper da Costa-Luis c70adebc75 snap: restrict site-package checks (#3878)
* snap: restrict site-package checks

Fixes #3514

* add comment
2020-06-07 07:50:15 +03:00
2c9cb87679 Subcommand help (#3956)
* change DvcParser to print subcommand help

* add tests for subcommand help

* docstring refactoring

* remote: finish separating RemoteTree methods (#3946)

* remote: move upload()/download() into tree

* remote: move path_info/path_cls into tree

* remote.azure: finish moving methods into tree

* remote.gs: finish moving methods into tree

* remote.hdfs: finish moving methods to tree

* remote.http: finish moving methods into tree

* remote.oss: finish moving methods into tree

* remote.s3: finish moving methods into tree

* remote.ssh: finish moving methods into tree

* remote.gdrive: finish moving methods into tree

* tests: update remote unit tests

* tests: update func tests

* remote: use walk_files() for all remotes

* list_cache_paths() now uses tree.walk_files() for all remotes except
  local/ssh

* fix DS warnings

* bugfixes

* Unify metrics behavior with dirs (#3935)

* run: disable directories for -m/-M flags

* run: reformatted string literals

* run: simplified metrics check

* tests: func: run: dirs as metrics outs

* tests: func: run: reformatted string literals

* tests: use pytest

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

* refactor/test multistage load for params and outputs (#3949)

* refactor multistage load for params and outputs

* tests: load params

* tests: output loading from pipeline file

* fix test

* fix typo in name

* split params load

* rename params func s/inject_values/fill_values

* fix tests

* simplify loads_params and output.load_from_pipeline

* address @pared's suggestions for tests

* remote: fix build errors (#3957)

* remote: fix remaining gdrive build issues (#3959)

* tests: fix gdrive build

* remote.gdrive: fix walk_files prefix relpath

* change DvcParser to print subcommand help

* add tests for subcommand help

* docstring refactoring

* change tests to pytest-style

* remove gettext

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
Co-authored-by: nik123 <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
Co-authored-by: Saugat Pachhai <[email protected]>
2020-06-05 23:44:02 +03:00
Peter Rowlands (변기호) b57077af11 remote: fix remaining gdrive build issues (#3959)
* tests: fix gdrive build

* remote.gdrive: fix walk_files prefix relpath
2020-06-05 15:36:47 +09:00
Peter Rowlands (변기호) 803bb3eb61 remote: fix build errors (#3957) 2020-06-05 05:14:34 +03:00
Saugat Pachhai 99b58d3d05 refactor/test multistage load for params and outputs (#3949)
* refactor multistage load for params and outputs

* tests: load params

* tests: output loading from pipeline file

* fix test

* fix typo in name

* split params load

* rename params func s/inject_values/fill_values

* fix tests

* simplify loads_params and output.load_from_pipeline

* address @pared's suggestions for tests
2020-06-04 23:35:07 +03:00
nik123andRuslan Kuprieiev 5bbaa28cae Unify metrics behavior with dirs (#3935)
* run: disable directories for -m/-M flags

* run: reformatted string literals

* run: simplified metrics check

* tests: func: run: dirs as metrics outs

* tests: func: run: reformatted string literals

* tests: use pytest

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-06-04 23:32:00 +03:00
Peter Rowlands (변기호) 74bd8137ba remote: finish separating RemoteTree methods (#3946)
* remote: move upload()/download() into tree

* remote: move path_info/path_cls into tree

* remote.azure: finish moving methods into tree

* remote.gs: finish moving methods into tree

* remote.hdfs: finish moving methods to tree

* remote.http: finish moving methods into tree

* remote.oss: finish moving methods into tree

* remote.s3: finish moving methods into tree

* remote.ssh: finish moving methods into tree

* remote.gdrive: finish moving methods into tree

* tests: update remote unit tests

* tests: update func tests

* remote: use walk_files() for all remotes

* list_cache_paths() now uses tree.walk_files() for all remotes except
  local/ssh

* fix DS warnings

* bugfixes
2020-06-04 23:22:31 +03:00
Ruslan Kuprieiev 72a576cefe dvc: bump to 1.0.0a9 2020-06-04 06:06:25 +03:00
Ruslan Kuprieiev 036ce9398e tests: move mock-ssh-server git dependency (#3953)
This one doesn't allow us to release new pre-releases, because pypi
forbids that:

https://travis-ci.com/github/iterative/dvc/jobs/343343967#L2447

Let's install it separately for now, until there is a new official
mock-ssh-server release.
2020-06-04 05:54:29 +03:00
Ruslan Kuprieiev 13012f9c0c plots: completion/spelling fixups (#3952)
* completion: don't forget --show-vega

Fixes #3890

* plots: VEGA -> Vega
2020-06-04 05:49:20 +03:00
Ruslan Kuprieiev a65c7989c8 tests: don't cache gdrive url (#3941)
This makes `GDrive.get_url` behaviour consistent with the rest of the
test remotes.

Related to #3916
2020-06-04 04:01:12 +03:00
Ruslan Kuprieiev 3590fe8414 dvc: bump to 1.0.0a8 2020-06-04 00:10:10 +03:00
Ruslan Kuprieiev 05100122e1 plots: remove print
Leftover from https://github.com/iterative/dvc/pull/3948

FIxes #3951
2020-06-04 00:09:16 +03:00
Alexander Schepanovski 9a55b909f8 Rename --xlab to --x-label, X_TITLE to X_LABEL, same for y (#3948)
Renamed:
- cli options
- dvcfile plot props
- template anchors
- variable names throughout the code
2020-06-03 20:30:17 +03:00
Ruslan Kuprieiev ff54ac1bfb plots: fix error when there are no plots in repo (#3940)
Consistent with `dvc metrics show/diff` behaviour.

Fixes #3925
2020-06-03 08:54:48 +03:00
Ruslan Kuprieiev 57e920aa39 tests: don't forget --external when adding external files 2020-06-03 08:38:51 +03:00
Ruslan Kuprieiev ac4f351daf dvc: require --external for external outputs (#3929)
Fixes #1545
2020-06-03 06:13:09 +03:00
Peter Rowlands (변기호) 2c0116ccf9 remote: separate tree functions from main remote classes (#3931)
* BaseRemote: start moving tree functions into BaseRemoteTree

* remote: add LocalRemoteTree

* remote: add cloud remote trees

* remote: move remove(), makedirs() into tree

* remote: move move(), copy(), symlink(), hardlink(), reflink() into tree

* fix attributes for moved remote methods

* tests: update unit tests for moved remote tree methods

* tests: mv unit/remote/test_remote_dir.py unit/remote/test_remote_tree.py

* fix attributes for moved remote tree functions

* tests: update func tests for moved remote tree methods

* test fixes

* fix DS warnings
2020-06-03 06:04:25 +03:00
Ruslan Kuprieiev c03ce4aa0f dag: fix help
per https://github.com/iterative/dvc.org/pull/1383#discussion_r433397776
2020-06-03 04:25:15 +03:00
Ruslan Kuprieiev 72dcbc0a9e remote: modify: fix --unset (#3939)
https://discordapp.com/channels/485586884165107732/563406153334128681/717474697721413686

Fixes https://github.com/iterative/dvc/issues/3938
2020-06-03 02:09:08 +03:00
Alexander Schepanovski fd65f44b7f Fix quadratic behavior in Repo._collect_stages() (#3932) 2020-06-02 16:56:49 +03:00
Ruslan Kuprieiev 7504b68c12 dvc: bump to 1.0.0a7 2020-06-02 03:34:04 +03:00
Ruslan Kuprieiev 86f3457dec dvc: remove dvc metrics add/remove (#3928)
We will be re-introducing `metrics modify` in the future and
will likely revisit `add/remove` too, so it is better to get
rid of these for now and introduce later with a clear scneario
in mind.

Fixes #3849
Fixes #3907
2020-06-02 00:17:03 +03:00
Saugat Pachhai 219662597b refactor: rename utils.stage to utils.yaml (#3923) 2020-06-01 16:49:46 +03:00
Alexander Schepanovski 536966f9d1 Fix modifying plot (#3926)
- no error when modifying plot with no props yet
- do not turn into non-plot if all props are unset
2020-06-01 16:19:11 +03:00
Saugat Pachhai 076eadceba bugfix: rerun stage when command is changed (#3922)
Previously, the stage would not rerun if the deps/outs are cached
even if the cmd has changed.

Versions affected: >0.93.0

Fixes #3918
2020-06-01 14:04:31 +05:45
Ruslan Kuprieiev d1e08b6157 dvc: replace pipeline with dag (#3905)
`pipeline list` is no more, it had some very questionable applications
(none to be precise) and was duplicating old `pipeline show` functionality.
We might introduce a different command to assist in listing (e.g. for
shell completion) in the future.

New `dvc dag` replaces `dvc pipeline show` and by-default will show
all pipelines in the project in ASCII (so there is no more --ascii)
and also `--dot` support, just like before. When given a target,
`dvc dag` (in any mode) will show DAG that leads to the target,
skipping the descendants, unless `--full` is specified.
2020-06-01 02:35:36 +03:00
Saugat Pachhai edfa1741db implement removing stage (#3881)
This should allow us to be able to remove a stage from a dvcfile,
(if it's a single stage file, it removes it), and make us able to
delete the overwrite the stage on run as well.

This commit:
* changes `run` `--overwrite-dvcfile` flag to `--overwrite`
* removes prompt in `run` for overwrite and fails hard
* changes `remove`: now it by default removes stage entry and unprotect outs
* `remove` has `--outs` to remove outputs instead of unprotecting them
2020-05-31 17:41:19 +03:00
Ruslan Kuprieiev 9ee62ef8f0 dvc: rename "working tree" to "workspace" (#3914)
Fixes #3892
2020-05-31 17:39:04 +03:00
Ruslan Kuprieiev 0bb49d2549 dvc: rename dvc lock -> dvc freeze (#3915)
Fixes #3901
2020-05-31 03:38:21 +03:00
Alexander SchepanovskiandRuslan Kuprieiev 516d82e411 Add plots props and plots modify params (#3911)
* Add plots props and `plots modify` params

* Update dvc/command/plots.py

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

* Update dvc/repo/plots/show.py

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

* Update dvc/repo/plots/show.py

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

* Update dvc/repo/plots/show.py

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

* Update dvc/repo/plots/show.py

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

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-05-30 14:37:11 +03:00
Saugat Pachhai ec312d8601 use fork of mockssh-server (#3910) 2020-05-30 08:31:59 +05:45
Ruslan Kuprieiev f504245f35 pre-commit: don't run dvc in parallel (#3912)
* pre-commit: use local hooks

* pre-commit: don't run dvc in parallel

By default pre-commit will try to run hooks in parallel, which results
in some dvc instances failing to acquire repo locks.

Fixes #3751
2020-05-29 21:20:46 +03:00
Saugat Pachhai bb50fb8664 use higher level API selectors instead of select (#3899) 2020-05-29 09:25:07 +05:45
Jorge Orpinel 777a9f80ae Update bug report template (#3903)
done directly from GH
2020-05-29 08:09:35 +05:45
Ruslan Kuprieiev 02a52d038a tests: make ssh_server session scoped (#3898)
Fixes #3888
2020-05-28 16:09:58 +03:00
Peter Rowlands (변기호) 7d0a06248d dvcignore: use correct root .dvcignore for subrepos (#3895)
* ignore: dvcignore root is distinct from tree_root

* for subrepos, dvcignore root should be the subrepo's .dvcignore, not
  host repo's .dvcignore

* tests: add test for subrepo dvcignore behavior
2020-05-28 20:52:48 +09:00
Ruslan Kuprieiev 2d8c6043ca plots: replace --show-json with --show-vega (#3891)
Fixes #3890
2020-05-28 14:42:41 +03:00
Nicholas A. Del Grosso 3f996048e8 Bugfix: _credentials_location (#3896)
_credentials_location  to credentials_location to removed attributeerror
2020-05-28 14:41:34 +03:00
Peter Rowlands (변기호) 8d9435e8ee brancher: only yield git revisions that contain repo_root (#3883)
* tests: add test for metrics show -T with pre-existing tags

* brancher: ignore git revisions that do not contain repo

* tests: update test to include cmd

* CleanTree: tree_root for subrepos should be the repo root, not scm root

* only force subrepo root for git trees

* fix windows shell quotes
2020-05-28 12:26:29 +09:00
Saugat Pachhai 889a17e2fc modify issue template (#3887)
* modify issue template

* change bug issue template
* auto add labels
* use emojis
* add link to Discord chat
2020-05-28 08:21:02 +05:45
Ruslan Kuprieiev 63972c3d12 dvc: bump to 1.0.0a6 2020-05-27 13:59:03 +03:00
Ruslan Kuprieiev 7027c631c5 dvcignore: fix bug in subrepo detection (#3880) 2020-05-27 12:13:53 +03:00
Saugat Pachhai e8f6cd7f5b run: raise error when command is not specified (#3850)
* run: raise error when command is not specified

* tests: adjust tests/func/plots/test_plots.py

* tests: adjust tests/func/plots/test_diff.py

* tests: add command to metrics/plots/params

* tests: fix test_remote.py

* tests: fix test_ls.py

* tests: fix test_repro* and test_run*

* tests: fix script in test_ls, was failing on Win
2020-05-27 12:13:26 +03:00
Peter Rowlands (변기호) 10f2bb93fa ignore: CleanTree should always check ignores, not just for walk() (#3876)
* tests: test that CleanTree works outside of walk()

* ignore: CleanTree should always check ignores, not just for walk()

* CleanTree: handle broken symlinks

* handle case for linking from shared local cache dirs

* Fix windows relpath/different drive issue
2020-05-27 12:07:38 +03:00
Saugat PachhaiandRuslan Kuprieiev 2fecc66cb9 addressing: allow stage name addressing without ":" (#3842)
* addressing: allow stage name addressing without colon

* repo: reorganize imports

* refactor/reorganize collect_granular

* add tests for stage addressing

* fix ds and cc issues

* fix unpack error in test

* tests: stage string representations

* exception: add error message

Also remove `cause` when throwing NoOutputOrStage, as
CheckoutErrorSuggestGit will start chaining all the messages
in cause of the exception.

* rename NoOutputOrStage NoOutputOrStageError

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-05-26 03:51:51 +03:00
Ruslan Kuprieiev b47998463f run-cache: don't forget to set wdir (#3873)
Fixes #3848
2020-05-25 20:41:36 +03:00
Peter Rowlands (변기호) d53b740f35 state: always use working trees (#3871)
* tests: add test case for push/pull with --all-commits

* state: always use working tree
2020-05-25 15:47:30 +03:00
Saugat Pachhai 9198fa7459 log: reduce log, make it readable/easier (#3834)
* log: streamline/make it readable

* Remove 'run' from log message

* fix args passing in imports

* tests: fix test

* run: Add quotes around stage name

* run: add quotes for run-cache restore log

* address @dmitry's comments
2020-05-25 05:33:16 +03:00
Ruslan Kuprieiev 6954efc6e6 dvc: bump to 1.0.0a6 2020-05-25 03:25:37 +03:00
Ruslan Kuprieiev d77af8196e optimize local status (#3867)
* remote: remove unpacked dir

We've done a lot of optimizations lately, which made unpacked dir trick
obsoleted.

* local: save us a stat call in is_protected

* local: speedup checksum_to_path ~x5.5

* tree: walk: don't use os.path.join

It is ~5.5 times slower than joining by hand.

* dvcignore: optimize matching x10

On a repo with a dvcignore with 1 pattern and a directory with 400K
files, `dvc status` now takes ~8 sec instead of ~30 sec.

To achieve that, we make some assumptions about the paths formats that
we are dealing with, so we could use simpler logic instead of using
very slow `relpath`, `abspath` etc on every entry in a directory.

It is also clear that CleanTree behavior is inconsistent (even tests
expect very different outputs from it), so we will need to look into
this later.
2020-05-25 03:20:11 +03:00
karajan1001 c5920b00ab Auto-completion of the dvc remote rename command (#3868)
fix #3847
2020-05-24 20:41:35 +03:00
Casper da Costa-Luis ce8036e535 snap: release non-stable-semver tags to beta (#3865)
* snap: release non-stable-semver tags to beta

- fixes #3860

* snap: document --beta
2020-05-24 20:40:53 +03:00
Ruslan Kuprieiev b7ad2dc841 test: fix hdfs/azurite/oss tests (#3864)
* travis: fix typo

* tests: azure: fix expected etag format

* test: ssh: increase the number of tries

* travis: don't install docker

* dvc: update pyarrow

* tests: ssh: don't leak connections

* remote: don't fail when cache/remote doesn't have prefix subdir
2020-05-23 23:46:40 +03:00
Ruslan Kuprieiev 5ab948f8b6 tests: unit: azure: add etag test (#3862) 2020-05-23 19:38:38 +03:00
Steffan Sluis f9fbe97509 azure: support external file dependencies (#3852)
* azure: support external dependencies and outputs

- Implement get_file_checksum with get_etag
- Implement a DependencyAzure and OutputAzure classes
- Add AzureOutput to OUTS, OUTS_MAP and CHECKSUMS_SCHEMA
- Add AzureDependency to DEPS and DEPS_MAP
- Add unit tests for AzureOutput and AzureDependency

Fixes #3540.

* Use content_md5 instead of etag

The etag is different for the same file in different locations.

* Use etag and remove external outputs for Azure

Due to inconsistencies with Azure's Content-MD5 property, it is not
stable enough to be used at the moment. Using the etag offers enough
reliability to support external dependencies. See also:
https://discordapp.com/channels/485586884165107732/565699007037571084/713462619469643796
2020-05-23 18:53:52 +03:00
Ruslan Kuprieiev 2fba2e030a dvcfile: serialize params on --no-exec (#3859)
* dvc.yaml: don't generate lockfile on --no-exec

* dvcfile: serialize params on --no-exec

Fixes #3851
2020-05-23 03:19:38 +03:00
Ruslan Kuprieiev 3e951162b8 dvc: bump to 1.0.0a4 2020-05-22 23:00:24 +03:00
Paweł Redzyński d74358eecd remote: local: protect uploaded files (#3858) 2020-05-22 22:59:48 +03:00
Ruslan Kuprieiev 58c9ff0de2 azure: drop obsoleted url format (#3856) 2020-05-22 20:36:04 +03:00
Paweł Redzyński f0e991790e plot: fixup for template help message (#3854) 2020-05-22 20:30:02 +03:00
Peter Rowlands (변기호) b7cd785eba external_repo: use RepoTree for fetch/import/get (#3841)
* erepo: load external repos directly from git tree

* erepo: add fetch_external

* remote/RepoTree: add callback for keeping track of download counts when
saving tree to cache

* repo: use erepo.fetch_external() in repo.fetch()

* erepo: return cache save_infos in fetch_external

* DependencyRepo: use erepo.fetch_external and cache.checkout instead of
pull_to

* erepo: remove pull_to

* DvcTree: support granularity for isdir()

* DvcTree: fix indentation bug

* RepoTree: add copytree() method

* LocalRemote: use working tree for local cache walk_files

* repo: state should be noop for GitTree based erepos

* erepo: add get_external()

* test_get: remove unneeded caplog check

* erepo: add get_rev()

* tests: fix erepo test bug

* ls: use RepoTree

* remote: support trees in _collect_dir

* remote: support trees in get_dir_checksum

* RepoTree: fix dir_cache relpaths in windows

* remote: fix duplicated fetch bug

* utils: don't skip md5 dos2unix check for git blobs

* otherwise md5 checksums will not match when comparing git version to
  cached filesystem version on windows
2020-05-22 17:23:31 +03:00
Ruslan Kuprieiev 21a3efe2c7 dvc: add: set top level md5 to None (#3846)
This way it won't be included into produced dvcfiles (we don't need it
for `dvc add`).
2020-05-22 17:21:43 +03:00
Ruslan Kuprieiev 865d355554 params/metrics: introduce --no-path and --old (#3845)
* metrics: diff: add --no-path

Part of #3690

* params: diff: introduce --no-path

* metrics: diff: introduce --old

Part of #3690
2020-05-22 04:35:42 +03:00
Ruslan Kuprieiev 84712c4acc dvc: don't write default values for cache/persist/metric (#3844) 2020-05-22 02:58:26 +03:00
Ruslan Kuprieiev 0f16e15453 test: add: fix typo 2020-05-22 01:11:45 +03:00
61f30ed5ba plots: point template help info to manual (#3830)
* plots: point template help info to manual

* Update dvc/command/plots.py

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

Co-authored-by: Ruslan Kuprieiev <[email protected]>
Co-authored-by: Jorge Orpinel <[email protected]>
2020-05-22 00:53:04 +03:00
Ruslan Kuprieiev cde15e3d3e run-cache: don't forget to account for params (#3843)
Fixes #3827
2020-05-22 00:52:21 +03:00
Ruslan Kuprieiev 7672e324d2 output: base: use Git instead of git
https://github.com/iterative/dvc/pull/3808#discussion_r428853696
2020-05-21 22:44:39 +03:00
Ruslan Kuprieiev 130b76c7a5 plots: remove --select CLI option (#3840)
* plots: remove --select from CLI

It needs reconsideration, we can't release it as is right now. E.g. it
expects xpath to start with `$`, but that will get interpreted by your
shell, which is bad.

* completion: update plots
2020-05-21 17:57:27 +03:00
Puneetha PaiandPuneetha Pai 040458a726 (OutputAlreadyTrackedError): Add git suggestions to stop tracking files (#3808)
Fixes: #3678
When file has been already tracked under git, OutputAlreadyTrackedError gets raised.
This commits add git command suggestions to help user to stop tracking file from git,
so in future commits they can use DVC for the file.

Co-authored-by: Puneetha Pai <[email protected]>
2020-05-21 15:17:42 +03:00
Peter Rowlands (변기호) 75e795c491 remote: support saving RepoTree objects directly to cache (#3825)
* DvcTree: use out.dir_cache instead of out.collect_use_dir_cache

* RepoTree: support filtering out dvcfiles during walk()

* RepoTree: support recursive isdvc()

* remote: support computing checksums for tree objects

* RepoTree: pull dir contents on walk() if in `fetch` mode

* tests: add func test for DVC dir pull on RepoTree.walk()

* DvcTree: support granular open()

* RepoTree: add get_file_checksum()

* remote: revert checksum changes

* remote: add tree support for cache.save()

* tests: test saving directly to cache from RepoTree

* remote: local cache needs its own WorkingTree instance when repo.tree is
GitTree

* RepoTree: state context should be entered by open()/walk() caller

* RepoTree: only pull dir contents when cache has changed

* tree: update tree tests

* fix ds warnings, update comments

* fix windows tests
2020-05-21 06:31:28 +03:00
Saugat Pachhai 4dac2092c9 repro: fix help message for default target (#3831) 2020-05-21 00:17:30 +03:00
Shankho Boron GhoshandRestyled.io 19eaced304 dvc : renamed --outs-only to --dvc-only (#3797)
* Renamed --outs-only to --dvc-only

* Changed outs_only to dvc_only

* replace output_only with dvc_only

* Restyled by black

Co-authored-by: Restyled.io <[email protected]>
2020-05-20 18:24:16 +03:00
Casper da Costa-Luis 184028f176 logger: add trace, count --verbose/--quiet (#3806)
* logger: initial verbosity/quiet counts

Fixes #2332

* actually use TRACE

* logger: properly patch in trace

* FATAL -> CRITICAL

* only change log level if not info

Makes testing easier

* tests: logger: fix and update

* state: logger: debug => trace

* logger: filter duplicate debug messages
2020-05-20 18:23:31 +03:00
Ruslan Kuprieiev 5f63d538d2 dvc: bump to 1.0.0a3 2020-05-20 18:01:24 +03:00
Ruslan Kuprieiev 324ff4be59 remote: is_dir_checksum: handle empty/None checksum (#3836)
Fixed #3434
2020-05-20 18:00:41 +03:00
karajan1001andPeter Rowlands 3f846a1794 Add a new rename command (#3794)
* fixed #3599

1. add new command `dvc remote rename <old> <new>`.
2. add tests for this new command.

* Update dvc/command/remote.py

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

* Change about code review

* For deepsource

* Update dvc/command/remote.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
2020-05-20 17:53:25 +03:00
Paweł Redzyński b52c925d5f lock: make froze message clickable (#3833) 2020-05-20 16:05:09 +03:00
Ruslan Kuprieiev 4a4f07968c dvc: bump to 1.0.0a2 2020-05-20 15:21:07 +03:00
Ruslan Kuprieiev 09136f2378 plots: add plot markers to DVC files (#3807)
* run: add --plot/--plot-no-cache

Part of #3409

* plot: move show/diff to API

* plots: show marked plots by default

Involves some refactoring to make plots comply with the rest of the code
base.

* dvc.yaml: use top-level metrics and plots

* dvc.yaml: add support for plot template

* plots: add support for templates in dvc.yaml

* dvc.yaml: allow metrics/plots to have persist/cache

* plots: render single page by-default and introduce --show-json

* plot: use "working tree" instead of "workspace"

Just to sync this with the rest of the codebase. We should consider
renaming it to "workspace" everywhere, but for now I opt to not touch
it to not break something that depends on it.

* plots: rename -f|--file to -o|--out

Better corresponds to other commands like `import`.

* plots: use RepoTree and brancher instead of api

* stage: get rid of OutputParams

* tests: plots: add simple diff test

* plots: use `--targets` instead of `--datafile`

Makes it comply with `dvc metrics/params` CLI options.

* tests: unit: plots: add --show-json test

* stage: cleanup exception for format error

Based on feedback from the users.

* lockfile: use relpath in corruption error
2020-05-20 04:48:53 +03:00
nik123 aaca1401ea Fixes #3814 (#3828) 2020-05-20 04:14:04 +03:00
Saugat Pachhai 2aaf065ea3 default target: reduce loglevel to debug (#3822) 2020-05-19 16:35:13 +03:00
Saugat Pachhai d4ec871a76 repro: do not log when stage is locked and is repo import (#3823) 2020-05-19 16:23:28 +03:00
Paweł Redzyński f44ddcf64e plot: dont zero y axis (#3813) 2020-05-19 16:20:21 +03:00
Peter Rowlands (변기호) b88b3b81c6 utils: support use of (GitTree) tree file objects in utils (#3819)
* GitTree: add stat() function

* BaseTree: make walk_files a base tree method

* WorkingTree: add stat() method (wrapper for os.stat)

* utils.fs: support non-working tree for get_mtime_and_size

* utils.fs: add copy_fobj_to_file

* utils: support using tree.open in file_md5
2020-05-19 16:20:04 +03:00
Ruslan Kuprieiev 81d04f887a stage: fix commit (#3816) 2020-05-19 03:30:52 +03:00
Peter Rowlands (변기호) afbb3a0add repo: Support streaming and pulling files on RepoTree/DvcTree.open() (#3810)
* DvcTree: support stream or fetch on open()

* tests: test DvcTree dir cache support for walk()

* RepoTree: finish implementation of BaseTree functions

* repo: use RepoTree/DvcTree.open() for repo.open_by_relpath

* repo: only use RepoTree when needed for now

* fix docstring

* RepoTree: only use DvcTree.open for DVC outs
2020-05-18 15:16:11 +03:00
Saugat Pachhai b8aa8ad951 pyupgrade: 3.6+ (#3805)
* pyupgrade: 3.6+

* tests: pyupgrade 3.6+
2020-05-15 16:48:02 +03:00
Saugat Pachhai 9897f7c470 test: metric/cache/persist keys (#3785) 2020-05-15 13:58:50 +03:00
Saugat Pachhai cf7f125c14 refactor: simplify checkouts and run, get rid of check_can_commit (#3800)
* stage: split checkouts

* stage: simplify can check commit

* split shell executable expansion to separate function

* refactor changed_entries for better message on pipeline file

* fix commit msg entry for stage

* reduce complexity of run_stage

* simplify prompt for commit

* fixes

* refactor commit

* refactor run_stage

* commit refactor
2020-05-15 10:08:32 +05:45
Ruslan Kuprieiev cec4b4e003 dvc: rename plot to plots (#3802)
* dvc: rename plot to plots

Follows `dvc params/metrics` convention and is needed in preparation for new
commands and some refactoring.

Part of #3409

* completion: rename plot -> plots
2020-05-14 22:28:22 +03:00
Ruslan Kuprieiev ff4fcc937b stage: fix --dry-run (#3799)
* tests: unit: move stage tests into a separate dir

* stage: fix --dry-run

Part of #3777
2020-05-14 20:19:34 +05:45
Saugat Pachhai 1676a7a1cd stage: moving things around, refactor (#3793)
* stage: extract unnecessary methods out of Stage class

* stage: change naming of functions

* stage: extract dump equal to utils

* stage: extract computation of md5 to utils

* stage: extract command run related logic in run.py

* stage: reorganize run()

* stage: boolean cosmetic changes

* stage: reorder save() and extract some into func

* stage: refactor status() into smaller functions

* extract resolve_wdir into utils

* merge _changed() and changed()

* cosmetic changes

* extract dump to utils

* extract import updates/sync to imports.py

* remove meta from PipelineStage

* fix imports, params load

* make run_stage a function, fix mocks in tests

* use original cmd

* rollback imports on decorators

* fix ds and cc
2020-05-14 16:43:02 +03:00
Ruslan Kuprieiev 974cf097dd run/repro: rename "build cache" -> "run cache" (#3788)
Part of #3777
2020-05-14 02:27:18 +03:00
Ruslan Kuprieiev 597ab98d0e diff: hanle repo with no commits (#3784)
`git diff` doesn't throw errors, so we could do the same.

Fixes #3398
2020-05-14 02:14:37 +03:00
Peter Rowlands (변기호) 833e82a7ea Drop Python 3.5 support (#3792)
* drop python 3.5 support

* dvc: remove fspath() use inside os.path calls

* dvc: replace dvc.compat.fspath with os.fspath

* tests: replace dvc.compat.fspath with os.fspath

* utils: relpath() should use fspath/str

* utils: os.readlink doesn't support path-like obj on Windows until 3.8
2020-05-14 01:46:42 +03:00
Peter Rowlands (변기호) 3916d4fa07 tests: mark test_data_cloud::test_pull_git_imports as flaky (#3790) 2020-05-14 00:16:37 +09:00
Paweł Redzyński d81b56978f plot: add doctype at the beggining of html embedding (#3787) 2020-05-13 00:17:05 +03:00
Jorge Orpinel 1e7efffe42 plot: update some output strings (#3759)
* plot: update datafile desc.

* plot: link to cmd ref in output

* plot: update --stdout desc
per https://github.com/iterative/dvc/pull/3577#pullrequestreview-407036181

* plot: a few more output string updates
per https://github.com/iterative/dvc/pull/3759#pullrequestreview-409011291
and https://github.com/iterative/dvc.org/pull/1283
2020-05-12 16:52:06 +03:00
Saugat Pachhai cc16b28a26 diff: autocompletion script for params, and show-md (#3786) 2020-05-12 16:41:09 +03:00
Paweł Redzyński 451c7aac04 tests: erepo_dir: extract remote setup to separate method (#3748) 2020-05-12 02:05:22 +03:00
Ruslan Kuprieiev 7b4466c982 run-cache: support uncached files (#3778)
Currently we only restore `--outs`, but `--outs-no-cache` are also
useful, especially when they are metrics files
(`-M|--metrics-no-cache`). So this PR makes run-cache save uncached
files to our cache and also tries to checkout them back during the
`restore` phase.
2020-05-12 02:02:53 +03:00
Jack WellsandRuslan Kuprieiev 7ca3a1119c Added S3 SSEKMSKeyId config option (#3762)
* Added S3 SSEKMSKeyId config option

* test: unit: s3: add SSEKMSKeyId test

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-05-11 22:52:55 +03:00
Saugat Pachhai 2c026066db pipeline file: disallow punctuation characters in stage name (#3767)
except hyphen and underscore
2020-05-11 19:11:47 +03:00
nik123 7f17c6aa03 refactor: check if targets is instances of str (#3779) 2020-05-11 19:10:14 +03:00
Ned Letcher 9f015fac1d import: check erepo is None before modifying url - fixes #3780 (#3781)
* import: check erepo is None before modifying url - fixes #3780

* import: add test to cover this local import
2020-05-11 18:29:35 +03:00
Saugat PachhaiandRuslan Kuprieiev 666e203bbb params/metrics: diff: implement tabulated markdown output (--show-md) (#3757)
* params/metrics: diff: implement tabulated markdown output

Via `--show-md` on `diff`s.

* table: show None on missing val

* tests: use `dedent`, add tests for markdown

* diff: add --show-md

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-05-10 18:39:51 +03:00
Karthikeyan Singaravelan ce946d1160 Import ABC from collections.abc for Python 3 compatibility. (#3772) 2020-05-10 10:49:32 +05:45
karajan1001 76b31ac282 fix #3588 (#3770)
* fix #3588

remove improper edges from output DAG.

* less code modification

* deepsource required

* deepsource required

* refactoring for reducing cognitive complexity.
2020-05-09 21:00:46 +03:00
6093a1fe97 Dvc remote default in list validation (#3715)
* fix #3470

Add validation to restrict default remote repo in list of remote repos.

* fix #3470

1. add two tests
2. add one validation
3. modified remote remove to satithe validation change

* Pass DeepSource

* deepsource required

* Redesigned.

* fix typo

* Revert "deepsource required"

This reverts commit 1ae458c34f49e8d7b35c39c1e6c592b423493a97.

* Revert "Pass DeepSource"

This reverts commit b254d104df65295e0a1156f3979b365be9d38a3d.

Co-authored-by: karajan1001 <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-05-09 20:59:17 +03:00
Ruslan Kuprieiev d62a54c7b0 push/pull: properly collect run cache (#3768)
Fixes #1234
2020-05-08 23:34:35 +03:00
nik123 c922da4f3f update: --recursive flag (#3760)
* update: --recursive flag

* refactor: applied isort changes to dvc/repo/update.py

* update: removed default values, check for str

* update: fixed code climate cognitive complexity issues
2020-05-08 23:23:48 +03:00
Ruslan Kuprieiev 2a16b021ad stage: hide unwanted warnings (#3763)
These messages all correpond to expected behaviour, so there is no need
to polute the output with unnecessary warnings.
2020-05-08 15:52:09 +03:00
Ruslan Kuprieiev fc42ca721c dvc: bump to 1.0.0a1 2020-05-07 19:33:30 +03:00
Ruslan Kuprieiev 785dcfeef0 push/pull: naively transfer run cache (#3746)
This is the most simple (aka dumb) implementation for this
functionality that is needed to unblock CICD development.
2020-05-07 16:55:10 +03:00
Paweł Redzyński 9714b14456 plot: fix help messages (#3734) 2020-05-07 13:22:55 +03:00
Ivan Shcheklein 6a406abde8 trying to enable the sponsorship button 2020-05-06 21:09:45 -07:00
Ivan Shcheklein 7ca055e868 fix: bump PyDrive2 to fix Gdrive remote download 2020-05-06 20:11:33 -07:00
Ruslan Kuprieiev 6a64f86178 pre-commit: use isort seeder 2020-05-07 00:25:41 +03:00
Ruslan Kuprieiev 227dbf44d9 isort: fix config 2020-05-07 00:17:38 +03:00
Casper da Costa-Luis 2bb7257a06 lint: isort pre-commit & misc improvements (#3749)
* test: clean flake8 config root clutter

* test: pre-commit: add isort

Compatible with black (see https://github.com/timothycrosley/isort/issues/694)

* lint: reorder imports

* lint: more complex imports

* lint: fix deepsource

* config: isort: add first party dvc

* tests: restyled: add isort

* temp commit to revert before merge

* Revert "temp commit to revert before merge"

This reverts commit 11db0fd8e95fe891597cbb9bf804480909b19e24.
2020-05-06 23:56:07 +03:00
Saugat Pachhai e3831066de repro: use dvc.yaml by default (#3747)
Note: This does not change default for `pipeline` as
it cannot show multiple DAGs at the same time. If we
are able to show multiple DAGs, we can just default to
`dvc.yaml`.
2020-05-06 18:33:42 +03:00
Paweł Redzyński aa99cc9934 plot: check field exists (#3742) 2020-05-06 18:33:10 +03:00
Casper da Costa-Luis 26e203242f gdrive: download: stream & add progress (#3722)
* wip

* gdrive: add progress

Part of #2865
See https://github.com/iterative/dvc/issues/2865#issuecomment-622984785

* gdrive: move towards next pydrive2 release

- depends on https://github.com/iterative/PyDrive2/pull/30

* update to latest pydrive>=1.4.11

* avoid unneeded API call

* progress: gdrive: ensure proper bar_format
2020-05-06 17:02:16 +03:00
Saugat Pachhai d00bd4dfe5 pull/fetch/push: add stats (#3735)
* pull/fetch/push: add stats

* fix typo: s/logging/logger

* pull: make stats detailed, with summary at the bottom

* tests: mock properly, set return_value

* add a line space
2020-05-05 16:10:50 +03:00
Casper da Costa-Luis 795f001929 Snap version (#3741)
* snap: potential build fix

* snap: fix travis
2020-05-05 12:48:19 +03:00
Ruslan Kuprieiev 93d7b9173b tests: use --single-stage 2020-05-04 20:23:49 +03:00
Ruslan Kuprieiev bc7f177c7a run: use multistage dvcfiles by default (#3740)
Adding hidden `--single-stage` to simplify the test migration.
2020-05-04 19:55:39 +03:00
Ruslan Kuprieiev 6f73a67c60 dvc: bump to 1.0.0a0 2020-05-04 19:46:49 +03:00
Ruslan Kuprieiev 68a0c39ad1 run: remove deprecated -y|--yes (#3737) 2020-05-04 17:51:07 +03:00
Saugat Pachhai c186a8ea8f ui: simplify/shorten error messages (#3730)
* ui: simplify/shorten error messages

* address efiop's review
2020-05-04 17:04:28 +03:00
Paweł Redzyński af8ac006ad plot: completion for zsh (#3733) 2020-05-04 15:01:29 +03:00
Ruslan Kuprieiev b2ea279c9e cache: rename .dvc/cache/run to .dvc/cache/runs (#3732)
To be consistent with how git does it (e.g. objects) as well as with
future cache structure where we plan on adding `dirs`/`files`/`objects`
subdirs in the future.
2020-05-04 13:26:52 +03:00
Saugat Pachhai 2dda540db6 Make comparisons in is_cached independent of order (#3731)
On is_cached, we used to compare two dicts: one of current stage in memory
created by `run` and one that's already written to the file.
As `outs` and `dicts` are lists (which is generate by `dumpd`), the
comparison was dependent on the order of outs and dicts.

So, the `run` before and now, can be in different order and would fail
the comparisons.
2020-05-04 11:40:42 +03:00
Ruslan Kuprieiev 76e6ba24f7 run: add to .gitignore on --no-exec (#3729) 2020-05-04 02:19:56 +03:00
Ruslan Kuprieiev 505b88afae diff: make output consistent with other diff commands (#3727)
* diff: make output consistent with other diff commands

Fixes #3435

* tests: plot: fix flaky test
2020-05-04 00:25:55 +03:00
nik123 415a85c6be status: --recursive flag (#3726)
* status: --recursive flag

* tests: fixed test_status_recursive for Windows platform
2020-05-03 21:12:17 +03:00
Ruslan Kuprieiev 4e9b38970a dvc: move temporary files to .dvc/tmp (#3725)
Fixes #3666
2020-05-03 18:42:12 +03:00
Ruslan Kuprieiev 3e1eb6d1e7 stage: rename .dvc/cache/stages -> .dvc/cache/run (#3724) 2020-05-03 17:19:37 +03:00
Casper da Costa-Luis c873787d73 progress: add postfix info to avoid overwriting desc (#3721)
* progress: persist primary description

Move subsequent updates to a postfix.
Clear postfix on exit.
TODO: align nicely.
Fixes #3681.

* progress: move to posfix[info] for full control

It was bound to happen.

* progress: git: persist description
2020-05-02 18:07:05 +03:00
Paweł RedzyńskiandRuslan Kuprieiev e553511085 Metrics - plotting for multiple revisions initial (#3577)
* init

* rename to plot data insertion basig on dicts update

* revision support

* roll back revision

* plot makedirs for backward compatibility

* log path

* pretty plot link to visualization page

* make target default title

* efiop review

* efiop review

* plot multiple initial

* add some missing metric file tests

* proper id generation

* proper id generation

* add confusion matrix template

* refactor tests

* plot from dvct file

* plot from dvct

* brush up commands

* fix confusion matrix multiple plot

* plot: change confusion matrix data schema

* should be working as intended

* support for src file in dvct files

* minor fixes

* plot: support json templates

* plot: rename confusion template

* plot: polish command behaviour

* fix test for json

* plot: test command

* some minor fixes for tests

* plot: unit test loading

* plot: unit test loading

* plot: handle TODOS

* cleanup

* use mocker

* plot: support tsv

* plot: command refactoring

* plot: fix windows issues with tests

* plot: test: some more windows fixes

* plot: _load_from_revisions complexity fix

* plot: reduce complexity

* plot: complexity reduction

* plot: deepsource suggestions

* plot: move template path evaluation

* fixup

* fixup

* exception on no datafile and no template

* json metric load with OrderedDict

* plot: improve handling non-existing files on revisions

* plot: improve handling non-existing files on revisions

* change default plot path

* some exceptions and fixes

* add yaml metrics support

* fixup

* some more suggestions

* default filename fix

* efiop review requests

* log exception on failur

* move revisions deduction to commands

* json templates

* extract template filling to separate method

* some parsing improvements

* add columns functionality

* extract default data transformation to separate method

* plot: initial support for jsonpath

* plot: rename columns to filters, tests are dict based

* plot: fixups

* plot: refactoring

* repo: plot: convert to package

* plot: data loading refactor, support searching for data

* plot: raise if wrong fields provided

* plot: command description

* plot: default: pass y axis info for default plot

* plot: get rid of fieldnames, expect ordered data

* plot: handle default plot in separate method

* plot: fix default

* plot: command option names fixes

* refactoring

* fixes

* plot: provide option for stdout redirection

* plot: rename show-json to no-html

* plot: add no-csv-header option

* plot: improve error message for wrongly structured metric

* plot: match template name exactly, whit suffix appended only

* plot: dmpetrov and ivan review

* plot: refactor --stdout help message

* plot: move template to repo/plot

* plot: add -x and -y options

* plot: add -x and -y options

* plot: command: order change

* plot: scatter

* plot: rename confusion matrix template, new name generation format

* plot: add title anchor

* plot: review from jorgeorpinel

* plot: rename filter and result options to select and file

* plot: add --title, --x-title, --y-title

* plot: xlab ylab

* Update dvc/repo/plot/template.py

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

* Update dvc/repo/plot/template.py

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

* efiop review

* plot: bash completion

* plot: static code analysis fixes

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-05-01 22:45:03 +03:00
Ruslan KuprieievandSaugat Pachhai 827c994e74 stage: cache: use lockfiles (#3713)
* stage: cache: use lockfiles

* reorganize

* Reorganize, use fill_from_lock to load stage cache

* load params from build cache

* adjust tests

Co-authored-by: Saugat Pachhai <[email protected]>
2020-05-01 15:23:11 +03:00
Saugat PachhaiandRuslan Kuprieiev 22c60dd608 dump: deterministic lockfile dump (#3711)
* dump: lockfile is dumped deterministically

The dump is no longer deterministic/dependent on the pipeline file,
but is sorted based on file names in outs, deps or params.
Also, the params inside each files are also sorted based on name.
However, the objects inside params are not sorted deterministically
as I think it's too much to sort that, and is not easy (considering
the types of objects it might hold, eg: lists, objects, etc).

This will also provide ordered dumps for Python3.5

* fix windows 3.8 test

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-04-30 22:34:06 +03:00
Ruslan Kuprieiev f9088e1c67 dvc: rename pipelines.yaml -> dvc.yaml (#3710) 2020-04-30 18:19:51 +03:00
Ruslan Kuprieiev 71d156ac49 run: try to save deps before running the command (#3709)
Unlike old `_check_missing_deps`, this also verifies that we are able to
save more complex dependencies such as parameters, where we not only
care about the config file, but also about the parameters in it.

Fixes #3707
2020-04-30 17:16:16 +03:00
Ruslan Kuprieiev d998c0a536 serialize: use checksums that are already saved (#3708)
`get_checksum()` recomputes the checksum which might not match the
pre-recorded one. `checksum` is the one that was `save()`ed during run
and it is the one that should be used in the lockfile.
2020-04-30 16:21:14 +03:00
Peter Rowlands (변기호) af12f0d06c remote: adjust traverse threshold multiplier (#3705)
* Fixes #3704
2020-04-30 12:12:11 +09:00
Ruslan Kuprieiev 18e8f07572 dvc: introduce local build cache (#3603)
This patch introduces `.dvc/cache/stages` that is used to store previous
runs and their results, which could then be reused later when we stumble
upon the same command with the same deps and outs.

Format of build cache entries is single-line json, which is readable by
humans and might also be used for lock files discussed in #1871.

Related to #1871
Local part of #1234
2020-04-30 00:22:32 +03:00
Ivan ShchekleinandJorge Orpinel 8aefbac223 gdrive: fix multi-remote workflow, cont. cleanup (#3686)
* remote, minor: fix parameter method name for consistency

* gdrive: cleanup, fix workflow with multiple gdrive remotes

* config: resolve gdrive cred file parth, typo fix

* grdive: address deepsource warning

* gdrive: fix tests after simplifying auth flow

* gdrive: address PR review, use backticks where appropriate

Co-Authored-By: Jorge Orpinel <[email protected]>

* gdrive: exception text improvements

Co-Authored-By: Jorge Orpinel <[email protected]>

* gdrive: fix exception message

Co-Authored-By: Jorge Orpinel <[email protected]>

* gdrive: fix root not found exception message

Co-Authored-By: Jorge Orpinel <[email protected]>

* gdrive: minor warnings/exceptions text improvement

* gdrive: add tests for the gdrive_user_credentials_file relative path

* gdrive: address review, slightly change text

* gdrive: comments -> docstrings, addressing PR review

Co-authored-by: Jorge Orpinel <[email protected]>
2020-04-29 23:25:56 +03:00
Dmitry ChichkovandRuslan Kuprieiev 40c6b567e3 setup: relax python-dateutil pip version constraint to include v2.8.2. #3701 (#3702)
* Relax python-dateutil pip version constraint to include v2.8.2.  #3701

[WIP]. Attempt to relax upper constraints on the version of python-dateutil, to include 2.8.1 and 2.8.2. 

Original constraint was:
    "python-dateutil<2.8.1,>=2.1",  # Consolidates azure-blob-storage and boto3

* Update setup.py

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-04-29 23:15:13 +03:00
Saugat Pachhai 907853b985 tag: getting rid of it (#3699)
* tags: get rid of it

* tests: adjust for tags and allow @ in stage name

* disallow @ on stage name

* fix ds issue, leftover tag in Stage
2020-04-29 16:57:31 +03:00
Peter Rowlands (변기호) 31fe77848f remote.ssh: suppress paramiko logging (#3675)
* remote.ssh: suppress paramiko logging

* use SSHClient.set_log_channel
2020-04-29 18:17:53 +05:45
Saugat Pachhai 7edee84306 dvc: implement params support for pipeline file (#3694)
* dvc: implement params support for pipeline file

* reorganize dump/load of params

* add tests for params

* fix order of dictionary in Python 3.5
2020-04-29 14:35:26 +03:00
nik123 1e65098819 refactor: dvc/output class names unification (#3695) 2020-04-28 22:29:32 +03:00
Peter Rowlands (변기호) 4fdf7ca7fd repo: use reverse post-order DFS in repro --downstream (#3689)
* tests: ensure repro --downstream preserves dependency order

* repo: use reverse post-order DFS in repro --downstream

- Fixes #3602

* make test_repro and test_repro_multistage consistent

* rebase master, update multistage test
2020-04-28 18:17:10 +03:00
Ruslan Kuprieiev a7b77672e6 innosetup: rename exe app name
Fixes https://github.com/iterative/dvc/issues/3687
2020-04-28 16:08:17 +03:00
Peter Rowlands (변기호) 6d8499ea6d remote: use string paths over PathInfo for performance reasons (#3672)
* remote: use checksums instead of paths when filling dir statuses

* remote: prefer using str paths over PathInfo for performance reasons

* only use string paths for RemoteLOCAL

* only optimize calls made from RemoteLOCAL.cache_exists

* fix ds warning

* use os.path in local

* remote: re-add checksum_to_path() to return string paths when applicable

- cloud remotes still default to using PathInfo's

* cache fspath string

* use abspath in checksum_to_path

- if path is not relpath from cwd or abspath, posix lstat() syscall
  runtime doubles (from calculating relpath from cwd)
2020-04-28 15:19:51 +03:00
Ruslan Kuprieiev 6d698eab08 stage: exceptions: update formatting 2020-04-28 13:56:35 +03:00
Saugat PachhaiandRuslan Kuprieiev 1937527249 dvc: implement multistage dvcfile (#3676)
* dvc: use lockfile as yaml instead of json

* refactor Dvcfile into Pipeline file and Single stage file

* fix tests

* dvc: fix outputs

* add more tests for collection of outputs

* add tests for data cloud/get/import/ls

* tests: test for checkouts

* Allow other checksums other than md5

* tests: use iterdir instead of os.listdir

* cleanup errors reported by cc and ds

* utils: throw DvcException instead of plain Exception

* tests: use yaml.load instead of json.load

* run: split assignments

* Update dvc/stage/exceptions.py

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-04-28 13:36:05 +03:00
nik123 e03128a8de refactor: dvc/dependency class names unification (#3688) 2020-04-28 12:49:09 +03:00
nik123 fefbcc0b4c refactor: dvc/remotes name unification (#3684)
Partially fixes #2089
2020-04-26 20:55:00 +03:00
b312895e77 progress: add main bars (#3594)
* progress: add main push/pull bar

Fixes #3452
Related #1840
Related #3565

* adjust desc

* fix merge

Co-authored-by: Ruslan Kuprieiev <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-04-25 22:04:47 +03:00
Ruslan Kuprieiev e2981d2743 git: is_tracked: use ls_files instead of walking the index (#3677)
Current implementation doesn't work with directories, which results in
hard-to-debug bugs like #3561. The bug that made us walk through index
manually is now fixed in gitpython, so we can simply use `ls_files` from
now on. Added some tests to prevent this from happening in the future.

Fixes #3561
2020-04-24 19:18:58 +03:00
Ruslan Kuprieiev ae9bc914d6 pre-commit: use flake8 v3.7.9 2020-04-24 10:51:09 +03:00
Ruslan Kuprieiev 6167702345 flake8: freeze to 3.7.9 2020-04-24 10:50:24 +03:00
Peter Rowlands (변기호) 7e9ff62d89 flake8: fix ambiguous var names (#3674) 2020-04-24 13:35:34 +09:00
a8b7e33010 Convert all paths in config to posix-style (#3665)
* Convert all paths in config to posix-style

* Use PathInfo for consistancy, add unit test and change functional tests

* Exclude Windows test on Linux

* Don't patch Config for all the tests!

* Don't do the linux test on Windows either

* De-hackify the unit test, make the functional test more explicit

* tests: adjust tests

Co-authored-by: Charles Baynham <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-04-24 02:47:21 +03:00
Peter Rowlands (변기호) a2de48bdfb output: use strings instead of PathInfo for performance reasons (#3663)
* output: don't use PathInfo when collecting used dir cache

* fix windows path handling
2020-04-23 13:59:40 +03:00
Ruslan Kuprieiev d1640cf10f pre-commit: make pre-commit hook verbose 2020-04-22 18:44:51 +03:00
Ruslan Kuprieiev 45944ddde7 metrics: rework metrics implementation (#3658)
Dropping support for all types except json and yaml. No more xpath
support.

Fixes #3572
2020-04-22 13:39:59 +03:00
charlesbaynhamandCharles Baynham 52369bdf47 Fix repro duplicating effort while checking steps #3644 (#3645)
* Only reproduce steps once

* Add test

* Fix linter nasties

* Got the order wrong

* Check for previous run

* Formatting

* Do the de-duplication in _reproduce_stages

Co-authored-by: Charles Baynham <[email protected]>
2020-04-21 21:18:53 +03:00
254cf83310 metrics diff error message in --no-scm projects (#3650)
* fix #3605

* changed exception raise position

* Updated error msg

* message changed

* metrics diff: output edits per
per https://github.com/iterative/dvc/pull/3650
and https://github.com/iterative/dvc/pull/3650#pullrequestreview-396837915

* Update tests/func/test_metrics.py

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

* NoSCM now had a default attribute getting method

`NoSCM` Object now raise `NoSCMError` when calling attributes which only in `Git` Object

* Joined lines

Mail change test

* tests: diff: use NoSCMError

Co-authored-by: myname <[email protected]>
Co-authored-by: karajan1001 <[email protected]>
Co-authored-by: Jorge Orpinel <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-04-21 21:15:46 +03:00
Ruslan Kuprieiev bca5edf108 gc: don't forget to load remote index (#3660)
Kudos @pmrowla
2020-04-21 20:49:36 +03:00
Peter Rowlands (변기호) b04964f4b7 remote: locally index list of checksums available on cloud remotes (#3634)
* remote: locally index list of checksums available on cloud remotes

* fix review issues

- use pytest fixtures
- use sha256 digest
- s/fd/fobj/
- check more specific IO errors

* remote: use .dir checksums to validate index

* command: add --drop-index option for push/pull/status

* tests: --drop-index unit tests

* remote: make index threadsafe

* remote: force re-index after gc

* only save/dump index once per command

* remote: add helper index functions update_all/replace_all

* remote: store index as sqlite3 database

* fix deepsource warnings

* rename --drop-index to --clear-index

* rename index.invalidate to index.clear

* functional tests for remote.index

* fix index not being updated on status -c

* remote: add index.intersection()

* remote: use index when making assumptions about remote .dir contents

* add missing func tests

* do not index standalone files

* cleanup index test fixture

* fix deepsource warnings

* update autocompletion scripts/cli help message for --clear-index

* tests: remove unused mocker

* push: only index successfully pushed dirs

- do not index files on partial push/upload

* remote: revert behavior to trust .dir files on remote

- skip unnecessary index check for dir contents if .dir file exists on
  the remote

* fix missing RemoteIndexNoop functions

* separate index validation from status dir exists query

* bugfix: include indexed checksums for all cache_exists cases

* review fix: cleanup debug messages

* tests: fix DS warning

* remove --clear-index

* remove unused index functions
2020-04-21 15:13:45 +03:00
Saugat Pachhai 63c466d821 refactor: extract stage loading outside of dvcfile (#3655)
* refactor: extract stage loading outside of dvcfile

* repo: simplify stage collection

* fix typing
2020-04-20 23:55:02 +03:00
nik123 224b0fc2ce tests: test_directories (#3649)
Fixed test_directories test in tests/func/test_diff.py
2020-04-18 19:24:03 +03:00
Jorge Orpinel 14b213adec Specify cmd arg names (metavars), et al. (#3626)
* add: -f arg name from FILE to <filename>
to match dvc.org cmd ref

* dvc: cmd arg names (metavars) to match docs
for #3626

* metrics: update help output to indicate supported types
per #3409
and for #3572

* dvc: reorder subcommands to match docs
for #3608
2020-04-18 16:48:53 +03:00
Casper da Costa-Luis f0c81bcef5 scm: git: clone: add progress (#3595)
* scm: git: clone: add progress

* git: add progress desc

* wip

* git: tidy message
2020-04-17 23:07:22 +03:00
Saugat Pachhai 128fa7f887 dvc: add a support for multistage dvcfile. (#3584)
Now, pipeline stage files can house multiple stages, and separate lockfiles
are created which has the checksums, whereas Dvcfile will be clean and human
readable and editable. The *.dvc files will be generated for output files.

It is available via a hidden flag: -n.

Fixes #1871
PR: #3584
2020-04-17 22:58:36 +03:00
Ruslan Kuprieiev 11e56dbdf9 tests: add some DvcTree unit tests (#3640) 2020-04-15 21:39:39 +03:00
Ruslan Kuprieiev c60f364774 dvc: introduce DvcTree (#3639)
This is useful as it is in `dvc ls` implementation, but this is also a
prerequisite for `RepoTree` that will combine git(workspace) files and
dvc files into one tree, allowing things like `dvc metrics` and
`external_repo` to have a universal interface to access whatever
file/dir they want without cumbersome git/dvc path detection.
2020-04-15 18:33:09 +03:00
Ruslan Kuprieiev 45c2c72b60 remote: don't use PathInfo.from_posix (#3637)
Using it to load dirs takes around 50 sec compared to 0.4 with simple
`replace()`. Path objects are great, but not when you need lots of them.

Related to #3635
2020-04-15 17:47:52 +03:00
d3d471c0a5 remote config validation (#3628)
* remote config validation

Fixes #3552

* solving config test fail

* Validation problem in config merging

sloving validation problem in config merging from different levels.
Only those config in lower levels can affect the validation process.

* reduce Cognitive Complexity

* Two request changes

1. Duplicate load current level of config
2. rewrite test to pytest style

* black it

* config: don't use all_levels

Co-authored-by: karajan1001 <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-04-15 17:08:37 +03:00
Ruslan Kuprieiev 8455b7bece params: diff: introduce --all (#3631)
* params: diff: introduce --all

Makes `dvc params diff` also show unchanged params.

* params: diff: show all present parameters
2020-04-15 13:21:24 +03:00
7275843c08 remote: use .dir checksum existence to infer file contents existence (#3632)
* repo: separate dir cache and file cache in memory

- `used_cache()`/`get_used_cache()` in repo/stage/output now return
  tuples of (dir_cache, file_cache) instead of one flat/merged cache

* update tests for new get_used_cache behavior

* remote: if .dir checksum exists on remote, assume contents also exists

- affects all commands which use `cache_exists()` (remote status)

* push: only upload .dir file after full file contents has been uploaded

* gc: always remove .dir checksums first

* functional tests for push/gc

* repo: support nesting dir caches in NamedCache

* remote: NamedCache updates

* Fix tests

* Fix deepsource warnings

* tests: use pytest mocker fixture

* Update dvc/cache.py

Co-Authored-By: Saugat Pachhai <[email protected]>

Co-authored-by: Ruslan Kuprieiev <[email protected]>
Co-authored-by: Saugat Pachhai <[email protected]>
2020-04-14 23:07:58 +03:00
Saugat PachhaiandRuslan Kuprieiev 65854c4946 refactor: extract stage's relationship with Dvcfile out from Stage (#3619)
* refactor stage into dvcfile

* Split create_stage out of dvcfile to Stage::create() and overwrite

* Remove logging from Repo::run

* tests: stop using --remove-outs

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-04-12 23:09:29 +03:00
Marcel Ribeiro Dantas 211f56c043 Updates latest version of PyDrive2 (#3624)
This latest version of PyDrive2 fixes an issue for Google Drive remote management in machines with an old version of httplib2.
2020-04-12 18:39:40 +03:00
Ruslan Kuprieiev fc6a10f486 stage: resolve stage path before creating the stage (#3630)
Current logic is extremely complex and messy, hiding a lot of bugs
inside. There are pretty much only 2 scenarios for stage creation:

1) `dvc add/import/etc` when dvcfile is created beside the output file
2) `dvc run` when dvcfile is created at the place of execution

both are transparently affected by `--fname` flag. Since we've removed
`--cwd` complexity already, there is no reason to generalize stage path
resolution for those two scenarios.
2020-04-12 18:16:13 +03:00
Ruslan Kuprieiev 63fe2893a8 run: remove deprecated --cwd (#3623)
It has been deprecated for a long time now and it significantly
complicates the code, hiding multiple bugs.
2020-04-11 19:57:05 +03:00
Ruslan Kuprieiev 43dcc97512 dvc: bump to 0.93.0 2020-04-10 23:22:30 +03:00
Ruslan Kuprieiev ce2481bbe8 remove: try deleting before trying to chmod (#3620)
Windows (or rather NTFS) doesn't allow deleting read-only files, which made us
try to chmod +w first. But that is not necessery on Linux and Mac, and
has led to unnecessary cache file mode changes, which was causing issues
in shared cache scenario when a user that doesn't own a file was trying
to automatically protect it.

Fixes #3618
2020-04-10 23:21:16 +03:00
Casper da Costa-Luis e05157f8e0 completion: bash: fix files (#3613)
Fixes lock, metrics and move autocompletion of files
2020-04-10 10:02:40 +03:00
Ruslan Kuprieiev 0e854f9534 params: diff: sort table (#3617) 2020-04-09 21:23:18 +03:00
Ruslan Kuprieiev cdef709e8a params: fix bug when collecting multiple params (#3615) 2020-04-09 15:41:35 +03:00
Ruslan Kuprieiev c1ac6bb06b dvc: introduce dvc params diff (#3573)
* params: use repo.tree

* remote: local: use `repo.tree` for `exists()`

* param: handle missing params

* stage: move status warning to debug

* dvc: introduce `dvc params diff`

Fixes #3528
2020-04-09 05:18:49 +03:00
Ruslan Kuprieiev 290a9fd434 analytics: collect runtime info in the worker (#3610)
Shaves off 0.2 seconds from any dvc command.

Related to #3475
2020-04-08 15:57:05 +03:00
Ruslan Kuprieiev 26980ae45b metrics: diff: fix float precision formatting (#3609)
Texttable will try to detect the column type automatically, which
results in a low precision of the float that it is showing. We need to
disable that auto-formatting and just use `str(val)` instead.
2020-04-07 19:42:25 +03:00
Jorge Orpinel f325921315 diff: update help output to match docs, et al. (#3601)
* diff: update help output to match docs
per https://github.com/iterative/dvc/pull/3566#pullrequestreview-388346350

* terms: git->Git, scm->SCM, spacing
2020-04-07 12:50:11 +03:00
Peter Rowlands (변기호) dfe184f149 tests: convert remote unit tests to pytest (#3604)
- update tests/unit/remote tests to use pytest style tests
- use pytest dvc repo fixtures instead of improperly mocked repo's or
  None
2020-04-07 10:45:49 +03:00
Ivan Shcheklein ca60c8e0c6 gdrive: improve granularaliry of the retry function, fixes 3580 (#3593) 2020-04-06 10:54:56 +03:00
Ruslan Kuprieiev 6ab06bbf9e dvc: bump to 0.92.1 2020-04-05 04:35:22 +03:00
Ivan Shcheklein 1b8e0725d8 gdrive: bump pydrive2 version to fix large file uploads (#3592) 2020-04-05 04:34:38 +03:00
Ivan ShchekleinandRuslan Kuprieiev 7b1dbd6109 CLI: redirect warnings to logger (#3591)
* redirect warnings module messages to logger to silent them in gdrive

* minor cleanup: remove unused var assignment

* dvc: don't ignore warnings manually

We use logger.captureWarnings() instead for CLI and for API this is harmful.

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-04-04 22:18:14 +03:00
Ruslan Kuprieiev 7bd473371c push/pull/status/metrics: add support for --all-commits (#3587)
First steps for #1691
2020-04-04 19:30:11 +03:00
Casper da Costa-Luis b9d560c584 progress: better threading (#3583)
Fixes #3565
Continues #3500 <- #2905
2020-04-02 22:10:15 +01:00
charlesbaynhamandCharles Baynham f4bec650ed Only import speedcopy on Win > 7 (#3581)
* system: Only import speedcopy for Windows > 7

Fixes #3259

* Allow for Win 8

Co-authored-by: Charles Baynham <[email protected]>
2020-04-02 17:13:46 +03:00
Ruslan Kuprieiev 8e675a9d42 metrics: diff: don't print "No changes" (#3576)
Makes it easier to use in shell scripts. E.g. `-z $(dvc metrics diff)`, which
has the same semantics as `git diff`. Also syncs up with the behaviour
of `dvc diff`.

Fixes #3469
2020-04-02 04:54:40 +03:00
Ruslan Kuprieiev a338fad036 dvc: bump to 0.92.0 2020-04-02 01:15:02 +03:00
Peter Rowlands (변기호) ee0066fae4 remote: use progress bar for remote cache query status during dvc gc (#3559)
* remote: use progress bar for remote cache query status during `dvc gc`

* test that cache_checksums() handles unexpected paths

* Fix SSHMocked tests
2020-04-01 18:27:54 +03:00
Ruslan Kuprieiev b02babee9e dvc: bump to 0.91.4 2020-04-01 17:03:40 +03:00
Nate Gadzhibalaev f4d91b35d7 CLI: Fixed cmds help strings (#3566)
This fixes a few command help string outputs that were
cut short because of a typo.
2020-04-01 14:00:16 +03:00
60ace79305 remote: ADD sock attribute and pass it to SSHConnection constructor (#3562)
* remote: ADD sock attribute and pass it to SSHConnection constructor

The sock attribute is set to a `paramiko.ProxyCommand` object
if 'ProxyCommand' exists in config to `None` other-wise
(which is the default option in `paramiko.client.SSHClient.connect()`).

FIXES #3560

* Move paramiko import to RemoteSSH __init__

* Update dvc/remote/ssh/__init__.py

* Update dvc/remote/ssh/__init__.py

* Update dvc/remote/ssh/__init__.py

* Update dvc/remote/ssh/__init__.py

Co-authored-by: Sergio Garcia <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-03-31 23:54:57 +03:00
Jorge Orpinel d2ea180a9a pull: update help output (-R option) (#3555) 2020-03-31 19:53:34 +03:00
Ruslan Kuprieiev 55644f376b dvc: bump to 0.91.3 2020-03-31 12:11:05 +03:00
Ruslan Kuprieiev 38e818bb1d remote: skip non-cache looking files when estimating remote size (#3557)
* remote: base: improve exception message

* remote: use `all()` instead of `list_cache_paths()`

`list_cache_paths()` doesn't ignore paths that don't look like hashes,
which makes dvc raise an exception when it tries to convert such paths
to hashes. We should use `all()` instead.

Discord context:
https://discordapp.com/channels/485586884165107732/485596304961962003/694454282719592458

* remote: all: print debug message when skipping paths

This will allow us to see it from verbose logs coming from users.
2020-03-31 12:10:01 +03:00
Peter Rowlands (변기호) d276ba4acc remote: short-circuit remote size estimation for large remotes (#3537) 2020-03-30 14:00:01 +03:00
Ivan Shcheklein f6077d06c4 gdrive: method signatures cleanup, more safeguards (#3548) 2020-03-30 09:49:34 +03:00
Ruslan Kuprieiev 0c2e4d0b72 dvc: bump to 0.91.2 2020-03-30 02:10:26 +03:00
Casper da Costa-Luis 617014ffbb snap: deploy: fix authentication (#3551)
Fixes #3550
2020-03-29 22:45:11 +03:00
Casper da Costa-Luis eeb6b60fe6 progress: limit maximum running bars (#3500)
* progress: limit maximum running bars

Fixes #2905
Closes #3453

* progress: add hidden message for overflow bars

* progress: fix nested bars properly

Just use latest `tqdm` :)
2020-03-29 20:54:32 +03:00
Casper da Costa-Luis 1ff4706f3a deploy: snap: timeout after 10 min (#3547)
Because `snapcraft push` hangs on cloud CI.
Imagine if `git push` did the same!
2020-03-29 16:33:51 +03:00
Peter Rowlands (변기호)andCasper da Costa-Luis acdc8763c6 remote: use progress bar when paginating (#3532)
* remote: use progress bar when paginating

- add `progress_callback` parameter to `list_cache_paths()` so that
  remotes can update remote cache traverse progress bar after fetching a
  page

* Show remote size estimation status as no-total progress bar

* Update pbar per item rather than per page

* make tests more explicit (don't use mock.ANY)

* Apply suggestions from code review

minor fixes/tidy

* Use progress_callback in ssh/local list_cache_paths()

- keep ssh/local consistent with other remotes (even though `dvc
  gc`/`RemoteBASE.all()` do not currently use progress bars)

* Fix review issues

Co-authored-by: Casper da Costa-Luis <[email protected]>
2020-03-28 12:51:42 +09:00
Casper da Costa-Luis af6c4291b9 progress: wrappers (#3522)
* progress: add wrap_fn

Closes #3521
Related https://github.com/tqdm/tqdm/issues/583

* progress: use wrappers in more places
2020-03-27 22:17:34 +02:00
3190480c12 message: change already exists message (#3525)
* message: change already exists message

Fixes #1050

* Update dvc/stage.py

Co-authored-by: karajan1001 <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-03-27 22:11:55 +02:00
Max Hora 8a4827017e gdrive: use default client and secret id; Debug message on retry of failed GDrive API call. (#3539) 2020-03-27 22:04:45 +02:00
SdgJlbl d8ac973362 Update PyYAML required version (#3545) 2020-03-27 21:21:21 +02:00
Saugat Pachhai a7aecf7c82 s3/gs: do not try to make directory on top level path (#3524)
* s3/gs: do not try to make directory on top level

* tests: unittest makedirs for s3 and gs

* remove setattr and use patch.object

* Update tests/unit/remote/test_remote.py
2020-03-27 16:25:43 +02:00
Paweł Redzyński eee3d7674f diff: analyse working tree when dirty (#3534) 2020-03-27 15:17:55 +02:00
Casper da Costa-Luis f69cb7a902 path_info: HTTPURLInfo minor inheritance tweak (#3502)
* path_info: HTTPURLInfo minor inheritance tweak

* path_info: rename `fill_parts` => `_fill_parts`
2020-03-26 17:35:47 +02:00
Ruslan Kuprieiev 02f92b3ac8 dvc: bump to 0.91.1 2020-03-26 03:42:47 +02:00
Ruslan Kuprieiev 974f81791f protect: ignore EROFS (#3527)
* protect: fix bug in mode verification

* protect: ignore EROFS

Fixes #3510
2020-03-26 03:33:48 +02:00
Ruslan Kuprieiev 912a98e05e metrics: diff: fix bug with unchanged raw metrics (#3535) 2020-03-26 03:02:34 +02:00
Max Hora 42e3b8aa25 gdrive: config option to move remote file to trash only (#3478) 2020-03-25 20:39:39 +02:00
Ruslan Kuprieiev 9de8d1e0fa params: use dpath for xpaths (#3531)
Currently `dvc run -p angles` is broken when you use it with `params.yaml`
that looks like this:

```
angles:
    - 2
    - 3
    - 100
    - 3.2
    - 36.2
    - 3.6
```

because we are incorrectly using `flatten` instead of simply using some
xpath-like library to access params that are specified.
2020-03-25 06:17:09 +02:00
Ruslan Kuprieiev 0bf6fdae14 metrics: diff: handle deleted/new metric (#3529)
Partial fix for https://github.com/iterative/dvc/issues/3469
2020-03-25 05:23:21 +02:00
Peter Rowlands (변기호) 9b38aa4680 remote: Optimize traverse/no_traverse behavior (#3501)
* remote: Optimize traverse/no_traverse behavior

- estimate remote file count by fetching a single parent cache dir, then
  determine whether or not to use no_traverse method for checking
  remainder of cache entries in `cache_exists`
- thread requests when traversing full remote file lists (by fetching
  one parent cache dir per thread)

* Use arbitrary prefix length for traverse remote cache queries

- default to 3
- for remotes that only support per-directory query use 2 (gdrive, hdfs)

* Add unit tests for new traverse/no_traverse behavior

* Fix deepsource warning

* Fix review issues

* fix CI

* Fix review issues

* obsolete no_traverse config option

- remove RemoteBASE.DEFAULT_NO_TRAVERSE and replace it with
  RemoteBASE.CAN_TRAVERSE (True for all remotes except http)

* review issues - use 'object exists' instead of 'no_traverse'

* fix obsolete no_traverse test issues

* fix SSHMocked func tests
2020-03-25 05:04:20 +02:00
Helge Munk JacobsenandRuslan Kuprieiev 28748ff345 introduce hyper parameters and config (#3515)
* Work in progress

* added file parsing and name validation + adjust schema

* Exceptions on bad input

* Support multiple parameters

* Support multi 's in
Having any troubles? Hit us up at https://dvc.org/support, we are always happy to help!

* fix formatting

* dep: param: fix schema

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

* run: check if --params is specified

* dvc: rework param handling

* params: fix exc formatting

* params: support basic nested configs

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-03-24 17:56:39 +02:00
Ruslan Kuprieiev 1ddab1077b dvc: bump to 0.91.0 2020-03-24 16:15:12 +02:00
Saugat Pachhai 1455e9551e pull/checkout: fix empty output if no state gets changed (#3520) 2020-03-24 13:48:06 +02:00
Saugat Pachhai 9fd7d747c1 pull: support pulling git-only imports (#3503)
* pull: support pulling git-only imports

* pull: also add git imports from external dvc repo

* fetch: add logging

* pull: don't save link to state

When `checkout` or `pull` is done, it'll try to cleanup unused links.
So, when we add from erepo to cache, it creates a link, which DVC will try
to cleanup. Also, it'll provide unwanted checkout change details. So, skipping those

* tests: refactor/rewrite

* flake8: why it's complaining?

* tests: do not leak file descriptors
2020-03-24 13:45:51 +02:00
Alexander SchepanovskiandRuslan Kuprieiev a4094f0a7f dvc: add Repo(skip_checks) flag (#3490)
* dvc: add Repo._skip_graph_checks flag

Will skip modified graph checks on add, run and import. The aim is to
avoid collecting stages and building graph on these commands.

The skip_checks flag is aimed to be private, but let people use dvc on
big repos without monkey patching or writing yaml files themselves.

* dvc: explain skip_graphs_checks flag in a comment

* test: that skip_graph_checks works

* repo: improve hack description and test

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-03-24 01:39:54 +02:00
Ramón Valles ee26afe65e git: quote filenames @ remind_to_track (#3315)
Fix #3303
2020-03-23 18:22:51 +02:00
Jorge Orpinel 9e7740572c dvc: update cmd output strings to match docs (#3471)
* metrics show: update -h output to match docs
per https://github.com/iterative/dvc.org/pull/1014/commits/2c3452119545987f77b764e4aa6fc94d051092a7

* metric: update argument desc per iterative/dvc.org/pull/1014

* metrics: match iterative/dvc.org/pull/1014 again...
using term "formatting" instead of "options"

* list: update help output to match iterative/dvc.org/pull/1014
That's iterative/dvc.org/pull/1014

* dvc: add docstring to open_by_relpath
It's a public fn after all. Similar to Repo class

* help: update cmd output strings to match with docs

* remote: update help output

* term: file descriptor -> file object

* metrics: update add and remove help outputs
per https://github.com/iterative/dvc/pull/3471#pullrequestreview-377170844

* remove: update help output
per https://github.com/iterative/dvc/pull/3471#pullrequestreview-377172394

* root: update help output
per https://github.com/iterative/dvc/pull/3471#pullrequestreview-377173196
2020-03-23 17:40:36 +02:00
Saugat Pachhai 205a605887 external_repo: do not try too hard to fix upstream (#3512)
* tests: importing from bare repo

* external_repo: don't try too hard to set upstream repo especially if url is not a dvc repo.

* Update tests/func/test_import.py

* Add a comment regarding bare-repo clone
2020-03-20 15:54:47 +02:00
Ruslan Kuprieiev 8f546f253b Revert "Restyle #3393 introduce hyper parameters and config (#3516)" (#3517)
This reverts commit 65d22c7dbd.
2020-03-20 00:54:21 +02:00
65d22c7dbd Restyle #3393 introduce hyper parameters and config (#3516)
* Work in progress

* added file parsing and name validation + adjust schema

* Exceptions on bad input

* Support multiple parameters

* Support multi 's in
Having any troubles? Hit us up at https://dvc.org/support, we are always happy to help!

* Restyled by black

Co-authored-by: elgehelge <[email protected]>
Co-authored-by: Restyled.io <[email protected]>
2020-03-20 00:53:41 +02:00
Ruslan Kuprieiev caf5b931b9 readme: fix flowchart link 2020-03-19 19:21:49 +02:00
Saugat Pachhai 84760dc738 stage: reorganize check for stage paths and exceptions (#3509) 2020-03-19 14:35:12 +02:00
Saugat Pachhai 93246f0658 version: fix bug when .git dir is removed, show more info regarding repo (#3508)
* version: fix error when .git dir is removed

* version: show dvc/git information

* version: remove introducing no_scm params in Repo api
2020-03-19 13:06:34 +02:00
Ivan Shcheklein 4489f98dc3 readme: update flow chart link 2020-03-18 21:06:41 -07:00
Ivan Shcheklein de19d15332 readme: fix banner link after some dvc.org changes 2020-03-18 16:09:49 -07:00
326 changed files with 19555 additions and 8763 deletions
-7
View File
@@ -1,7 +0,0 @@
[flake8]
ignore =
E203, # Whitespace before ':'
E266, # Too many leading '#' for block comment
W503, # Line break occurred before a binary operator
max-line-length = 79
select = B,C,E,F,W,T4,B9
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
patreon: DVCorg
+13 -5
View File
@@ -1,10 +1,18 @@
---
name: Bug report
about: Create a report to help us improve
name: "\U0001F41B Bug Report"
about: Create a bug report to help us improve DVC
---
## Bug Report
**Please provide information about your setup**
DVC version(i.e. `dvc --version`), Platform and method of installation (pip, homebrew, pkg Mac, exe (Windows), DEB(Linux), RPM(Linux))
### Please provide information about your setup
**Output of `dvc version`:**
```console
$ dvc version
```
**Additional Information (if any):**
If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: "🤗 Need help?"
url: https://dvc.org/chat
about: If you have a question, ask us on Discord. Please join with this invite 👉
+1 -2
View File
@@ -1,6 +1,5 @@
---
name: Feature request
name: "\U0001F680 Feature Request"
about: Suggest an idea for this project
---
+36 -8
View File
@@ -4,11 +4,20 @@ repos:
language_version: python3
repo: https://github.com/ambv/black
rev: 19.10b0
- repo: https://github.com/asottile/seed-isort-config
rev: v2.1.1
hooks:
- id: seed-isort-config
- hooks:
- id: isort
language_version: python3
repo: https://github.com/timothycrosley/isort
rev: 4.3.21
- hooks:
- id: flake8
language_version: python3
repo: https://gitlab.com/pycqa/flake8
rev: master
rev: 3.7.9
- hooks:
- args:
- -i
@@ -18,18 +27,37 @@ repos:
repo: https://github.com/lovesegfault/beautysh
rev: master
- hooks:
- id: dvc-pre-commit
language_version: python3
- args:
- git-hook
- pre-commit
entry: dvc
id: dvc-pre-commit
language: system
name: DVC pre-commit
stages:
- commit
- id: dvc-pre-push
language_version: python3
verbose: true
require_serial: true
- args:
- git-hook
- pre-push
entry: dvc
id: dvc-pre-push
language: system
name: DVC pre-push
stages:
- push
require_serial: true
- always_run: true
args:
- git-hook
- post-checkout
entry: dvc
id: dvc-post-checkout
language_version: python3
language: system
minimum_pre_commit_version: 2.2.0
name: DVC post-checkout
stages:
- post-checkout
repo: https://github.com/iterative/dvc
rev: master
require_serial: true
repo: local
+4
View File
@@ -6,8 +6,10 @@
language: python
language_version: python3
name: DVC pre-commit
require_serial: true
stages:
- commit
verbose: true
- args:
- git-hook
- pre-push
@@ -16,6 +18,7 @@
language: python
language_version: python3
name: DVC pre-push
require_serial: true
stages:
- push
- always_run: true
@@ -28,5 +31,6 @@
language_version: python3
minimum_pre_commit_version: 2.2.0
name: DVC post-checkout
require_serial: true
stages:
- post-checkout
+9
View File
@@ -9,3 +9,12 @@ restylers:
- "**/*.py"
interpreters:
- python
- name: isort
image: restyled/restyler-isort:v4.3.21
command:
- isort
arguments: []
include:
- "**/*.py"
interpreters:
- python
+16 -8
View File
@@ -46,9 +46,6 @@ jobs:
env:
- PYTHON_VERSION=3.8.0
- PATH=/c/Python38:/c/Python38/Scripts:$PATH
- name: "3.5 on Linux"
language: python
python: 3.5
- name: "3.6 on Linux"
language: python
python: 3.6
@@ -101,8 +98,12 @@ jobs:
- name: lxd
channel: stable
env:
- SNAPCRAFT_IMAGE_INFO: '{"build_url": "$TRAVIS_BUILD_URL"}'
- SNAPCRAFT_IMAGE_INFO: |
'{"build_url": "$TRAVIS_JOB_WEB_URL"}'
- SNAPCRAFT_BUILD_ENVIRONMENT: lxd
- SNAPCRAFT_BUILD_INFO: 1 # https://snapcraft.io/blog/introducing-developer-notifications-for-snap-security-updates
install:
- sudo usermod --append --groups lxd $USER
- sudo /snap/bin/lxd.migrate -yes
- sudo /snap/bin/lxd waitready
- sudo /snap/bin/lxd init --auto
@@ -151,12 +152,19 @@ deploy:
tags: true
repo: iterative/dvc
stage: build
- provider: snap
snap: dvc_*.snap
channel: $SNAP_CHANNEL
- provider: script
skip_cleanup: true
script: "(echo $SNAP_TOKEN | snapcraft login --with -) && timeout 600 snapcraft push dvc_*.snap --release $SNAP_CHANNEL || echo timed out"
on:
all_branches: true
condition: "$(./scripts/ci/deploy_condition.sh dvc_*.snap) && ($TRAVIS_BRANCH = master || -n $TRAVIS_TAG)"
condition: "$(./scripts/ci/deploy_condition.sh dvc_*.snap) && ($TRAVIS_BRANCH = master || -n $TRAVIS_TAG) && -n $SNAP_CHANNEL"
repo: iterative/dvc
stage: build
- provider: script
skip_cleanup: true
script: "(echo $SNAP_TOKEN | snapcraft login --with -) && timeout 600 snapcraft push dvc_*.snap --release $SNAP_CHANNEL_MAJOR || echo timed out"
on:
all_branches: true
condition: "$(./scripts/ci/deploy_condition.sh dvc_*.snap) && ($TRAVIS_BRANCH = master || -n $TRAVIS_TAG) && -n $SNAP_CHANNEL_MAJOR"
repo: iterative/dvc
stage: build
+5 -4
View File
@@ -101,7 +101,8 @@ Snap (Snapcraft/Linux)
snap install dvc --classic
This corresponds to the latest tagged release.
Add ``--edge`` for the latest ``master`` version.
Add ``--beta`` for the latest tagged release candidate,
or ``--edge`` for the latest ``master`` version.
Choco (Chocolatey/Windows)
--------------------------
@@ -255,7 +256,7 @@ Citation
Iterative, *DVC: Data Version Control - Git for Data & Models* (2020)
`DOI:10.5281/zenodo.012345 <https://doi.org/10.5281/zenodo.3677553>`_.
.. |Banner| image:: https://dvc.org/static/img/logo-github-readme.png
.. |Banner| image:: https://dvc.org/img/logo-github-readme.png
:target: https://dvc.org
:alt: DVC logo
@@ -307,6 +308,6 @@ Iterative, *DVC: Data Version Control - Git for Data & Models* (2020)
:target: https://doi.org/10.5281/zenodo.3677553
:alt: DOI
.. |Flowchart| image:: https://dvc.org/static/img/flow.gif
:target: https://dvc.org/static/img/flow.gif
.. |Flowchart| image:: https://dvc.org/img/flow.gif
:target: https://dvc.org/img/flow.gif
:alt: how_dvc_works
-10
View File
@@ -3,17 +3,7 @@ DVC
----
Make your data science projects reproducible and shareable.
"""
import warnings
import dvc.logger
from dvc.version import __version__ # noqa: F401
dvc.logger.setup()
# Ignore numpy's runtime warnings: https://github.com/numpy/numpy/pull/432.
# We don't directly import numpy, but our dependency networkx does, causing
# these warnings in some environments. Luckily these warnings are benign and
# we can simply ignore them so that they don't show up when you are using dvc.
warnings.filterwarnings("ignore", message="numpy.dtype size changed")
warnings.filterwarnings("ignore", message="numpy.ufunc size changed")
+15 -14
View File
@@ -2,12 +2,12 @@ import json
import logging
import os
import platform
import requests
import sys
import tempfile
import uuid
import distro
import requests
from dvc import __version__
from dvc.config import Config, to_bool
@@ -20,7 +20,6 @@ from dvc.scm.base import SCMError
from dvc.utils import env2bool, is_binary
from dvc.utils.fs import makedirs
logger = logging.getLogger(__name__)
@@ -36,7 +35,7 @@ def collect_and_send_report(args=None, return_code=None):
report as a JSON, where the _collector_ generates it and the _sender_
removes it after sending it.
"""
report = _runtime_info()
report = {}
# Include command execution information on the report only when available.
if args and hasattr(args, "func"):
@@ -62,7 +61,7 @@ def is_enabled():
return enabled
def send(report):
def send(path):
"""
Side effect: Removes the report after sending it.
@@ -73,13 +72,17 @@ def send(report):
url = "https://analytics.dvc.org"
headers = {"content-type": "application/json"}
with open(report, "rb") as fobj:
try:
requests.post(url, data=fobj, headers=headers, timeout=5)
except requests.exceptions.RequestException:
logger.debug("failed to send analytics report", exc_info=True)
with open(path) as fobj:
report = json.load(fobj)
os.remove(report)
report.update(_runtime_info())
try:
requests.post(url, json=report, headers=headers, timeout=5)
except requests.exceptions.RequestException:
logger.debug("failed to send analytics report", exc_info=True)
os.remove(path)
def _scm_in_use():
@@ -155,7 +158,7 @@ def _find_or_create_user_id():
try:
with Lock(lockfile):
try:
with open(fname, "r") as fobj:
with open(fname) as fobj:
user_id = json.load(fobj)["user_id"]
except (FileNotFoundError, ValueError, KeyError):
@@ -167,6 +170,4 @@ def _find_or_create_user_id():
return user_id
except LockError:
logger.debug(
"Failed to acquire '{lockfile}'".format(lockfile=lockfile)
)
logger.debug(f"Failed to acquire '{lockfile}'")
+4 -3
View File
@@ -1,16 +1,17 @@
import os
from contextlib import contextmanager, _GeneratorContextManager as GCM
from contextlib import _GeneratorContextManager as GCM
from contextlib import contextmanager
from dvc.repo import Repo
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc.external_repo import external_repo
from dvc.repo import Repo
class UrlNotDvcRepoError(DvcException):
"""Thrown if the given URL is not a DVC repository."""
def __init__(self, url):
super().__init__("'{}' is not a DVC repository.".format(url))
super().__init__(f"'{url}' is not a DVC repository.")
def get_url(path, repo=None, rev=None, remote=None):
+79 -13
View File
@@ -28,19 +28,21 @@ def _make_remote_property(name):
"""
def getter(self):
from dvc.remote import Remote
from dvc.remote import get_cloud_tree
from dvc.remote.base import CloudCache
remote = self.config.get(name)
if not remote:
return None
return Remote(self.repo, name=remote)
tree = get_cloud_tree(self.repo, name=remote)
return CloudCache(tree)
getter.__name__ = name
return cached_property(getter)
class Cache(object):
class Cache:
"""Class that manages cache locations of a DVC repo.
Args:
@@ -50,7 +52,8 @@ class Cache(object):
CACHE_DIR = "cache"
def __init__(self, repo):
from dvc.remote import Remote
from dvc.remote import get_cloud_tree
from dvc.remote.local import LocalCache
self.repo = repo
self.config = config = repo.config["cache"]
@@ -62,7 +65,8 @@ class Cache(object):
else:
settings = {**config, "url": config["dir"]}
self.local = Remote(repo, **settings)
tree = get_cloud_tree(repo, **settings)
self.local = LocalCache(tree)
s3 = _make_remote_property("s3")
gs = _make_remote_property("gs")
@@ -71,9 +75,39 @@ class Cache(object):
azure = _make_remote_property("azure")
class NamedCache(object):
class NamedCacheItem:
def __init__(self):
self._items = defaultdict(lambda: defaultdict(set))
self.names = set()
self.children = defaultdict(NamedCacheItem)
def __eq__(self, other):
return self.names == other.names and self.children == other.children
def child_keys(self):
for key, child in self.children.items():
yield key
yield from child.child_keys()
def child_names(self):
for key, child in self.children.items():
yield key, child.names
yield from child.child_names()
def add(self, checksum, item):
self.children[checksum].update(item)
def update(self, item, suffix=""):
if suffix:
self.names.update(n + suffix for n in item.names)
else:
self.names.update(item.names)
for checksum, child_item in item.children.items():
self.children[checksum].update(child_item)
class NamedCache:
def __init__(self):
self._items = defaultdict(lambda: defaultdict(NamedCacheItem))
self.external = defaultdict(set)
@classmethod
@@ -86,7 +120,18 @@ class NamedCache(object):
return self._items[key]
def add(self, scheme, checksum, name):
self._items[scheme][checksum].add(name)
"""Add a mapped name for the specified checksum."""
self._items[scheme][checksum].names.add(name)
def add_child_cache(self, checksum, cache, suffix=""):
"""Add/update child cache for the specified checksum."""
for scheme, src in cache._items.items():
dst = self._items[scheme][checksum].children
for child_checksum, item in src.items():
dst[child_checksum].update(item, suffix=suffix)
for repo_pair, files in cache.external.items():
self.external[repo_pair].update(files)
def add_external(self, url, rev, path):
self.external[url, rev].add(path)
@@ -94,11 +139,32 @@ class NamedCache(object):
def update(self, cache, suffix=""):
for scheme, src in cache._items.items():
dst = self._items[scheme]
for checksum, names in src.items():
if suffix:
dst[checksum].update(n + suffix for n in names)
else:
dst[checksum].update(names)
for checksum, item in src.items():
dst[checksum].update(item, suffix=suffix)
for repo_pair, files in cache.external.items():
self.external[repo_pair].update(files)
def scheme_keys(self, scheme):
"""Iterate over a flat list of all keys for the specified scheme,
including children.
"""
for key, item in self._items[scheme].items():
yield key
yield from item.child_keys()
def scheme_names(self, scheme):
"""Iterate over a flat list of checksum, names items for the specified
scheme, including children.
"""
for key, item in self._items[scheme].items():
yield key, item.names
yield from item.child_names()
def dir_keys(self, scheme):
return (
key for key, item in self._items[scheme].items() if item.children
)
def child_keys(self, scheme, checksum):
return self._items[scheme][checksum].child_keys()
+28 -16
View File
@@ -10,36 +10,36 @@ from .command import (
commit,
config,
daemon,
dag,
data_sync,
destroy,
diff,
freeze,
gc,
get,
get_url,
git_hook,
imp,
imp_url,
init,
install,
ls,
lock,
metrics,
move,
pipeline,
params,
plots,
remote,
remove,
repro,
root,
run,
tag,
unprotect,
update,
version,
git_hook,
)
from .command.base import fix_subparsers
from .exceptions import DvcParserError
logger = logging.getLogger(__name__)
COMMANDS = [
@@ -62,36 +62,52 @@ COMMANDS = [
remote,
cache,
metrics,
params,
install,
root,
ls,
lock,
pipeline,
freeze,
dag,
daemon,
commit,
tag,
diff,
version,
update,
git_hook,
plots,
]
class DvcParser(argparse.ArgumentParser):
"""Custom parser class for dvc CLI."""
def error(self, message):
def error(self, message, command=None):
"""Custom error method.
Args:
message (str): error message.
command (str): subcommand name for help message
Raises:
dvc.exceptions.DvcParser: dvc parser exception.
"""
logger.error(message)
if command is not None:
for action in self._actions:
if action.dest == "cmd" and command in action.choices:
subparser = action.choices[command]
subparser.print_help()
raise DvcParserError()
self.print_help()
raise DvcParserError()
# override this to send subcommand name to error method
def parse_args(self, args=None, namespace=None):
args, argv = self.parse_known_args(args, namespace)
if argv:
msg = "unrecognized arguments: %s"
self.error(msg % " ".join(argv), args.cmd)
return args
class VersionAction(argparse.Action): # pragma: no cover
# pylint: disable=too-few-public-methods
@@ -115,14 +131,10 @@ def get_parent_parser():
log_level_group = parent_parser.add_mutually_exclusive_group()
log_level_group.add_argument(
"-q", "--quiet", action="store_true", default=False, help="Be quiet."
"-q", "--quiet", action="count", default=0, help="Be quiet."
)
log_level_group.add_argument(
"-v",
"--verbose",
action="store_true",
default=False,
help="Be verbose.",
"-v", "--verbose", action="count", default=0, help="Be verbose."
)
return parent_parser
+9 -2
View File
@@ -1,7 +1,7 @@
import argparse
import logging
from dvc.command.base import append_doc_link, CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException, RecursiveAddingWhileUsingFilename
logger = logging.getLogger(__name__)
@@ -18,6 +18,7 @@ class CmdAdd(CmdBase):
recursive=self.args.recursive,
no_commit=self.args.no_commit,
fname=self.args.file,
external=self.args.external,
)
except DvcException:
@@ -50,9 +51,15 @@ def add_parser(subparsers, parent_parser):
help="Don't put files/directories into cache.",
)
parser.add_argument(
"-f",
"--external",
action="store_true",
default=False,
help="Allow targets that are outside of the DVC repository.",
)
parser.add_argument(
"--file",
help="Specify name of the DVC-file this command will generate.",
metavar="<filename>",
)
parser.add_argument(
"targets", nargs="+", help="Input files/directories to add."
+6 -8
View File
@@ -1,6 +1,5 @@
import logging
logger = logging.getLogger(__name__)
@@ -27,7 +26,7 @@ def append_doc_link(help_message, path):
)
class CmdBase(object):
class CmdBase:
def __init__(self, args):
from dvc.repo import Repo
from dvc.updater import Updater
@@ -36,17 +35,16 @@ class CmdBase(object):
self.config = self.repo.config
self.args = args
hardlink_lock = self.config["core"].get("hardlink_lock", False)
updater = Updater(self.repo.dvc_dir, hardlink_lock=hardlink_lock)
updater = Updater(self.repo.tmp_dir, hardlink_lock=hardlink_lock)
updater.check()
@property
def default_targets(self):
"""Default targets for `dvc repro` and `dvc pipeline`."""
from dvc.stage import Stage
"""Default targets for `dvc repro`."""
from dvc.dvcfile import PIPELINE_FILE
msg = "assuming default target '{}'.".format(Stage.STAGE_FILE)
logger.warning(msg)
return [Stage.STAGE_FILE]
logger.debug(f"assuming default target '{PIPELINE_FILE}'.")
return [PIPELINE_FILE]
# Abstract methods that have to be implemented by any inheritance class
def run(self):
+8 -27
View File
@@ -1,39 +1,16 @@
import argparse
import logging
import operator
import colorama
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import CheckoutError
from dvc.utils.humanize import get_summary
logger = logging.getLogger(__name__)
def _human_join(words):
words = list(words)
if not words:
return ""
return (
"{before} and {after}".format(
before=", ".join(words[:-1]), after=words[-1],
)
if len(words) > 1
else words[0]
)
def log_summary(stats):
states = ("added", "deleted", "modified")
summary = (
"{} {}".format(len(stats[state]), state)
for state in states
if stats.get(state)
)
logger.info(_human_join(summary))
def log_changes(stats):
colors = [
("modified", colorama.Fore.YELLOW,),
@@ -75,7 +52,11 @@ class CmdCheckout(CmdBase):
stats = exc.stats
if self.args.summary:
log_summary(stats)
default_message = "No changes."
msg = get_summary(
sorted(stats.items(), key=operator.itemgetter(0))
)
logger.info(msg or default_message)
else:
log_changes(stats)
+1 -3
View File
@@ -1,11 +1,9 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
+5 -4
View File
@@ -1,7 +1,7 @@
import argparse
import logging
from dvc.command.base import append_doc_link, CmdBaseNoRepo
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.config import Config, ConfigError
logger = logging.getLogger(__name__)
@@ -50,21 +50,22 @@ class CmdConfig(CmdBaseNoRepo):
parent_config_parser = argparse.ArgumentParser(add_help=False)
parent_config_parser.add_argument(
level_group = parent_config_parser.add_mutually_exclusive_group()
level_group.add_argument(
"--global",
dest="level",
action="store_const",
const="global",
help="Use global config.",
)
parent_config_parser.add_argument(
level_group.add_argument(
"--system",
dest="level",
action="store_const",
const="system",
help="Use system config.",
)
parent_config_parser.add_argument(
level_group.add_argument(
"--local",
dest="level",
action="store_const",
+3 -3
View File
@@ -1,5 +1,4 @@
from dvc.command.base import CmdBaseNoRepo
from dvc.command.base import fix_subparsers
from dvc.command.base import CmdBaseNoRepo, fix_subparsers
class CmdDaemonBase(CmdBaseNoRepo):
@@ -15,9 +14,10 @@ class CmdDaemonUpdater(CmdDaemonBase):
root_dir = Repo.find_root()
dvc_dir = os.path.join(root_dir, Repo.DVC_DIR)
tmp_dir = os.path.join(dvc_dir, "tmp")
config = Config(dvc_dir, validate=False)
hardlink_lock = config.get("core", {}).get("hardlink_lock", False)
updater = Updater(dvc_dir, hardlink_lock=hardlink_lock)
updater = Updater(tmp_dir, hardlink_lock=hardlink_lock)
updater.fetch(detach=False)
return 0
+115
View File
@@ -0,0 +1,115 @@
import argparse
import logging
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
def _show_ascii(G):
from dvc.repo.graph import get_pipelines
from dvc.dagascii import draw
pipelines = get_pipelines(G)
ret = []
for pipeline in pipelines:
ret.append(draw(pipeline.nodes, pipeline.edges))
return "\n".join(ret)
def _show_dot(G):
import io
from networkx.drawing.nx_pydot import write_dot
dot_file = io.StringIO()
write_dot(G, dot_file)
return dot_file.getvalue()
def _build(G, target=None, full=False):
import networkx as nx
from dvc.repo.graph import get_pipeline, get_pipelines
if target:
H = get_pipeline(get_pipelines(G), target)
if not full:
descendants = nx.descendants(G, target)
descendants.add(target)
H.remove_nodes_from(set(G.nodes()) - descendants)
else:
H = G
def _relabel(stage):
return stage.addressing
return nx.relabel_nodes(H, _relabel, copy=False)
class CmdDAG(CmdBase):
def run(self):
try:
target = None
if self.args.target:
stages = self.repo.collect(self.args.target)
if len(stages) > 1:
logger.error(
f"'{self.args.target}' contains more than one stage "
"{stages}, please specify one stage"
)
return 1
target = stages[0]
G = _build(self.repo.graph, target=target, full=self.args.full,)
if self.args.dot:
logger.info(_show_dot(G))
else:
from dvc.utils.pager import pager
pager(_show_ascii(G))
return 0
except DvcException:
msg = "failed to show "
if self.args.target:
msg += f"a pipeline for '{target}'"
else:
msg += "pipelines"
logger.exception(msg)
return 1
def add_parser(subparsers, parent_parser):
DAG_HELP = "Visualize DVC project DAG."
dag_parser = subparsers.add_parser(
"dag",
parents=[parent_parser],
description=append_doc_link(DAG_HELP, "dag"),
help=DAG_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
dag_parser.add_argument(
"--dot",
action="store_true",
default=False,
help="Print DAG with .dot format.",
)
dag_parser.add_argument(
"--full",
action="store_true",
default=False,
help=(
"Show full DAG that the target belongs too, instead of "
"showing DAG consisting only of ancestors."
),
)
dag_parser.add_argument(
"target",
nargs="?",
help="Stage or output to show pipeline for (optional). "
"Finds all stages in the workspace by default.",
)
dag_parser.set_defaults(func=CmdDAG)
+104 -29
View File
@@ -1,23 +1,25 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.checkout import log_summary
from dvc.exceptions import DvcException, CheckoutError
from dvc.command.base import CmdBase, append_doc_link
from dvc.command.checkout import log_changes
from dvc.exceptions import CheckoutError, DvcException
from dvc.utils.humanize import get_summary
logger = logging.getLogger(__name__)
class CmdDataBase(CmdBase):
@classmethod
def check_up_to_date(cls, processed_files_count):
if processed_files_count == 0:
logger.info("Everything is up to date.")
def log_summary(self, stats):
default_msg = "Everything is up to date."
logger.info(get_summary(stats.items()) or default_msg)
class CmdDataPull(CmdDataBase):
def log_summary(self, stats):
log_changes(stats)
super().log_summary(stats)
def run(self):
try:
stats = self.repo.pull(
@@ -26,17 +28,18 @@ class CmdDataPull(CmdDataBase):
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,
force=self.args.force,
recursive=self.args.recursive,
run_cache=self.args.run_cache,
)
log_summary(stats)
self.log_summary(stats)
except (CheckoutError, DvcException) as exc:
log_summary(getattr(exc, "stats", {}))
self.log_summary(getattr(exc, "stats", {}))
logger.exception("failed to pull data from the cloud")
return 1
self.check_up_to_date(stats["downloaded"])
return 0
@@ -49,13 +52,15 @@ class CmdDataPush(CmdDataBase):
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,
run_cache=self.args.run_cache,
)
self.log_summary({"pushed": processed_files_count})
except DvcException:
logger.exception("failed to push data to the cloud")
return 1
self.check_up_to_date(processed_files_count)
return 0
@@ -68,13 +73,15 @@ class CmdDataFetch(CmdDataBase):
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,
run_cache=self.args.run_cache,
)
self.log_summary({"fetched": processed_files_count})
except DvcException:
logger.exception("failed to fetch data from the cloud")
return 1
self.check_up_to_date(processed_files_count)
return 0
@@ -82,27 +89,31 @@ def shared_parent_parser():
from dvc.cli import get_parent_parser
# Parent parser used in pull/push/status
shared_parent_parser = argparse.ArgumentParser(
parent_parser = argparse.ArgumentParser(
add_help=False, parents=[get_parent_parser()]
)
shared_parent_parser.add_argument(
"-j", "--jobs", type=int, help="Number of jobs to run simultaneously."
parent_parser.add_argument(
"-j",
"--jobs",
type=int,
help="Number of jobs to run simultaneously.",
metavar="<number>",
)
shared_parent_parser.add_argument(
parent_parser.add_argument(
"targets",
nargs="*",
help="Limit command scope to these DVC-files. "
"Using -R, directories to search DVC-files in can also be given.",
)
return shared_parent_parser
return parent_parser
def add_parser(subparsers, _parent_parser):
from dvc.command.status import CmdDataStatus
# Pull
PULL_HELP = "Pull data files from a DVC remote storage."
PULL_HELP = "Download tracked files or directories from remote storage."
pull_parser = subparsers.add_parser(
"pull",
@@ -112,7 +123,7 @@ def add_parser(subparsers, _parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pull_parser.add_argument(
"-r", "--remote", help="Remote repository to pull from."
"-r", "--remote", help="Remote storage to pull from", metavar="<name>",
)
pull_parser.add_argument(
"-a",
@@ -128,6 +139,12 @@ def add_parser(subparsers, _parent_parser):
default=False,
help="Fetch cache for all tags.",
)
pull_parser.add_argument(
"--all-commits",
action="store_true",
default=False,
help="Fetch cache for all commits.",
)
pull_parser.add_argument(
"-f",
"--force",
@@ -149,11 +166,16 @@ def add_parser(subparsers, _parent_parser):
default=False,
help="Pull cache for subdirectories of the specified directory.",
)
pull_parser.add_argument(
"--run-cache",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
pull_parser.set_defaults(func=CmdDataPull)
# Push
PUSH_HELP = "Uploads files or directories tracked by DVC"
" to remote storage."
PUSH_HELP = "Upload tracked files or directories to remote storage."
push_parser = subparsers.add_parser(
"push",
@@ -163,7 +185,7 @@ def add_parser(subparsers, _parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
push_parser.add_argument(
"-r", "--remote", help="Remote repository to push to."
"-r", "--remote", help="Remote storage to push to", metavar="<name>",
)
push_parser.add_argument(
"-a",
@@ -179,6 +201,12 @@ def add_parser(subparsers, _parent_parser):
default=False,
help="Push cache for all tags.",
)
push_parser.add_argument(
"--all-commits",
action="store_true",
default=False,
help="Push cache for all commits.",
)
push_parser.add_argument(
"-d",
"--with-deps",
@@ -191,13 +219,21 @@ def add_parser(subparsers, _parent_parser):
"--recursive",
action="store_true",
default=False,
help="Push cache from subdirectories of specified directory.",
help="Push cache for subdirectories of specified directory.",
)
push_parser.add_argument(
"--run-cache",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
push_parser.set_defaults(func=CmdDataPush)
# Fetch
FETCH_HELP = "Get files or directories tracked by DVC from remote storage"
" into the cache."
FETCH_HELP = (
"Get tracked files or directories from remote storage"
" into the cache."
)
fetch_parser = subparsers.add_parser(
"fetch",
@@ -207,7 +243,10 @@ def add_parser(subparsers, _parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
fetch_parser.add_argument(
"-r", "--remote", help="Remote repository to fetch from."
"-r",
"--remote",
help="Remote storage to fetch from",
metavar="<name>",
)
fetch_parser.add_argument(
"-a",
@@ -223,6 +262,12 @@ def add_parser(subparsers, _parent_parser):
default=False,
help="Fetch cache for all tags.",
)
fetch_parser.add_argument(
"--all-commits",
action="store_true",
default=False,
help="Fetch cache for all commits.",
)
fetch_parser.add_argument(
"-d",
"--with-deps",
@@ -237,6 +282,12 @@ def add_parser(subparsers, _parent_parser):
default=False,
help="Fetch cache for subdirectories of specified directory.",
)
fetch_parser.add_argument(
"--run-cache",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
fetch_parser.set_defaults(func=CmdDataFetch)
# Status
@@ -270,7 +321,10 @@ def add_parser(subparsers, _parent_parser):
help="Show status of a local cache compared to a remote repository.",
)
status_parser.add_argument(
"-r", "--remote", help="Remote repository to compare local cache to."
"-r",
"--remote",
help="Remote storage to compare local cache to",
metavar="<name>",
)
status_parser.add_argument(
"-a",
@@ -288,6 +342,13 @@ def add_parser(subparsers, _parent_parser):
help="Show status of a local cache compared to a remote repository "
"for all tags.",
)
status_parser.add_argument(
"--all-commits",
action="store_true",
default=False,
help="Show status of a local cache compared to a remote repository "
"for all commits.",
)
status_parser.add_argument(
"-d",
"--with-deps",
@@ -295,4 +356,18 @@ def add_parser(subparsers, _parent_parser):
default=False,
help="Show status for all dependencies of the specified target.",
)
status_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Show status of all stages in the specified directory.",
)
status_parser.add_argument(
"--show-json",
action="store_true",
default=False,
help="Show status in JSON format.",
)
status_parser.set_defaults(func=CmdDataStatus)
+1 -3
View File
@@ -2,11 +2,9 @@ import argparse
import logging
import dvc.prompt as prompt
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
+28 -12
View File
@@ -8,10 +8,24 @@ import colorama
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
def _show_md(diff):
from dvc.utils.diff import table
rows = []
for status in ["added", "deleted", "modified"]:
entries = diff.get(status, [])
if not entries:
continue
paths = sorted([entry["path"] for entry in entries])
for path in paths:
rows.append([status, path])
return table(["Status", "Path"], rows, True)
class CmdDiff(CmdBase):
@staticmethod
def _format(diff):
@@ -97,20 +111,17 @@ class CmdDiff(CmdBase):
try:
diff = self.repo.diff(self.args.a_rev, self.args.b_rev)
if not any(diff.values()):
return 0
if not self.args.show_hash:
for _, entries in diff.items():
for entry in entries:
del entry["hash"]
if self.args.show_json:
res = json.dumps(diff)
else:
res = self._format(diff)
logger.info(res)
logger.info(json.dumps(diff))
elif self.args.show_md:
logger.info(_show_md(diff))
elif diff:
logger.info(self._format(diff))
except DvcException:
logger.exception("failed to get diff")
@@ -120,9 +131,8 @@ class CmdDiff(CmdBase):
def add_parser(subparsers, parent_parser):
DIFF_DESCRIPTION = (
"Show changes between commits in the DVC repository,"
" or between a commit and the workspace."
"Prints the list of paths added, modified, or deleted."
"Show added, modified, or deleted data between commits in the DVC"
" repository, or between a commit and the workspace."
)
diff_parser = subparsers.add_parser(
"diff",
@@ -154,4 +164,10 @@ def add_parser(subparsers, parent_parser):
action="store_true",
default=False,
)
diff_parser.add_argument(
"--show-md",
help="Show tabulated output in the Markdown format (GFM).",
action="store_true",
default=False,
)
diff_parser.set_defaults(func=CmdDiff)
+57
View File
@@ -0,0 +1,57 @@
import argparse
import logging
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdFreezeBase(CmdBase):
def _run(self, func, name):
ret = 0
for target in self.args.targets:
try:
func(target)
except DvcException:
logger.exception(f"failed to {name} '{target}'")
ret = 1
return ret
class CmdFreeze(CmdFreezeBase):
def run(self):
return self._run(self.repo.freeze, "freeze")
class CmdUnfreeze(CmdFreezeBase):
def run(self):
return self._run(self.repo.unfreeze, "unfreeze")
def add_parser(subparsers, parent_parser):
FREEZE_HELP = "Freeze DVC-files."
freeze_parser = subparsers.add_parser(
"freeze",
parents=[parent_parser],
description=append_doc_link(FREEZE_HELP, "freeze"),
help=FREEZE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
freeze_parser.add_argument(
"targets", nargs="+", help="DVC-files to freeze."
)
freeze_parser.set_defaults(func=CmdFreeze)
UNFREEZE_HELP = "Unfreeze DVC-files."
unfreeze_parser = subparsers.add_parser(
"unfreeze",
parents=[parent_parser],
description=append_doc_link(UNFREEZE_HELP, "unfreeze"),
help=UNFREEZE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unfreeze_parser.add_argument(
"targets", nargs="+", help="DVC-files to unfreeze."
)
unfreeze_parser.set_defaults(func=CmdUnfreeze)
+13 -7
View File
@@ -3,9 +3,7 @@ import logging
import os
import dvc.prompt as prompt
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
@@ -23,7 +21,7 @@ class CmdGC(CmdBase):
msg = "This will remove all cache except items used in "
msg += "the working tree"
msg += "the workspace"
if self.args.all_commits:
msg += " and all git commits"
elif self.args.all_branches and self.args.all_tags:
@@ -79,7 +77,7 @@ def add_parser(subparsers, parent_parser):
"--workspace",
action="store_true",
default=False,
help="Keep data files used in the current workspace (working tree).",
help="Keep data files used in the current workspace.",
)
gc_parser.add_argument(
"-a",
@@ -109,7 +107,10 @@ def add_parser(subparsers, parent_parser):
help="Collect garbage in remote repository.",
)
gc_parser.add_argument(
"-r", "--remote", help="Remote storage to collect garbage in."
"-r",
"--remote",
help="Remote storage to collect garbage in",
metavar="<name>",
)
gc_parser.add_argument(
"-f",
@@ -119,7 +120,11 @@ def add_parser(subparsers, parent_parser):
help="Force garbage collection - automatically agree to all prompts.",
)
gc_parser.add_argument(
"-j", "--jobs", type=int, help="Number of jobs to run simultaneously."
"-j",
"--jobs",
type=int,
help="Number of jobs to run simultaneously.",
metavar="<number>",
)
gc_parser.add_argument(
"-p",
@@ -130,5 +135,6 @@ def add_parser(subparsers, parent_parser):
help="Keep data files required by these projects "
"in addition to the current one. "
"Useful if you share a single cache across repos.",
metavar="<paths>",
)
gc_parser.set_defaults(func=CmdGC)
+10 -4
View File
@@ -1,10 +1,9 @@
import argparse
import logging
from .base import append_doc_link
from .base import CmdBaseNoRepo
from dvc.exceptions import DvcException
from .base import CmdBaseNoRepo, append_doc_link
logger = logging.getLogger(__name__)
@@ -66,10 +65,17 @@ def add_parser(subparsers, parent_parser):
"path", help="Path to a file or directory within the repository"
)
get_parser.add_argument(
"-o", "--out", nargs="?", help="Destination path to download files to"
"-o",
"--out",
nargs="?",
help="Destination path to download files to",
metavar="<path>",
)
get_parser.add_argument(
"--rev", nargs="?", help="Git revision (e.g. SHA, branch, tag)"
"--rev",
nargs="?",
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
get_parser.add_argument(
"--show-url",
+2 -3
View File
@@ -1,10 +1,9 @@
import argparse
import logging
from .base import append_doc_link
from .base import CmdBaseNoRepo
from dvc.exceptions import DvcException
from .base import CmdBaseNoRepo, append_doc_link
logger = logging.getLogger(__name__)
@@ -17,7 +16,7 @@ class CmdGetUrl(CmdBaseNoRepo):
Repo.get_url(self.args.url, out=self.args.out)
return 0
except DvcException:
logger.exception("failed to get '{}'".format(self.args.url))
logger.exception(f"failed to get '{self.args.url}'")
return 1
+10 -5
View File
@@ -1,11 +1,9 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -48,9 +46,16 @@ def add_parser(subparsers, parent_parser):
"path", help="Path to a file or directory within the repository"
)
import_parser.add_argument(
"-o", "--out", nargs="?", help="Destination path to download files to"
"-o",
"--out",
nargs="?",
help="Destination path to download files to",
metavar="<path>",
)
import_parser.add_argument(
"--rev", nargs="?", help="Git revision (e.g. SHA, branch, tag)"
"--rev",
nargs="?",
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
import_parser.set_defaults(func=CmdImport)
+4 -4
View File
@@ -1,11 +1,9 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -59,6 +57,8 @@ def add_parser(subparsers, parent_parser):
"out", nargs="?", help="Destination path to put files to."
)
import_parser.add_argument(
"-f", "--file", help="Specify name of the DVC-file it generates."
"--file",
help="Specify name of the DVC-file this command will generate.",
metavar="<filename>",
)
import_parser.set_defaults(func=CmdImportUrl)
+2 -4
View File
@@ -1,9 +1,7 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBaseNoRepo
from dvc.command.base import CmdBaseNoRepo, append_doc_link
logger = logging.getLogger(__name__)
@@ -47,7 +45,7 @@ def add_parser(subparsers, parent_parser):
action="store_true",
default=False,
help="Initiate DVC in directory that is "
"not tracked by any scm tool (e.g. git).",
"not tracked by any SCM tool (e.g. Git).",
)
init_parser.add_argument(
"-f",
+1 -3
View File
@@ -1,9 +1,7 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
logger = logging.getLogger(__name__)
-61
View File
@@ -1,61 +0,0 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdLockBase(CmdBase):
def _run(self, unlock):
for target in self.args.targets:
try:
self.repo.lock_stage(target, unlock=unlock)
except DvcException:
logger.exception(
"failed to {}lock '{}'".format(
"un" if unlock else "", target
)
)
return 1
return 0
class CmdLock(CmdLockBase):
def run(self):
return self._run(False)
class CmdUnlock(CmdLockBase):
def run(self):
return self._run(True)
def add_parser(subparsers, parent_parser):
LOCK_HELP = "Lock DVC-files."
lock_parser = subparsers.add_parser(
"lock",
parents=[parent_parser],
description=append_doc_link(LOCK_HELP, "lock"),
help=LOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
lock_parser.add_argument("targets", nargs="+", help="DVC-files to lock.")
lock_parser.set_defaults(func=CmdLock)
UNLOCK_HELP = "Unlock DVC-files."
unlock_parser = subparsers.add_parser(
"unlock",
parents=[parent_parser],
description=append_doc_link(UNLOCK_HELP, "unlock"),
help=UNLOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unlock_parser.add_argument(
"targets", nargs="+", help="DVC-files to unlock."
)
unlock_parser.set_defaults(func=CmdUnlock)
+8 -7
View File
@@ -2,12 +2,10 @@ import argparse
import logging
import sys
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBaseNoRepo
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.command.ls.ls_colors import LsColors
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -33,14 +31,14 @@ class CmdList(CmdBaseNoRepo):
self.args.path,
rev=self.args.rev,
recursive=self.args.recursive,
outs_only=self.args.outs_only,
dvc_only=self.args.dvc_only,
)
if entries:
entries = _prettify(entries, sys.stdout.isatty())
logger.info("\n".join(entries))
return 0
except DvcException:
logger.exception("failed to list '{}'".format(self.args.url))
logger.exception(f"failed to list '{self.args.url}'")
return 1
@@ -64,10 +62,13 @@ def add_parser(subparsers, parent_parser):
help="Recursively list files.",
)
list_parser.add_argument(
"--outs-only", action="store_true", help="Show only DVC outputs."
"--dvc-only", action="store_true", help="Show only DVC outputs."
)
list_parser.add_argument(
"--rev", nargs="?", help="Git revision (e.g. branch, tag, SHA)"
"--rev",
nargs="?",
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
list_parser.add_argument(
"path",
+2 -2
View File
@@ -1,7 +1,7 @@
import os
class LsColors(object):
class LsColors:
default = "rs=0:di=01;34:ex=01;32"
def __init__(self, lscolors=None):
@@ -45,4 +45,4 @@ class LsColors(object):
if not val:
return text
rs = self._codes.get("rs", 0)
return "\033[{}m{}\033[{}m".format(val, text, rs)
return f"\033[{val}m{text}\033[{rs}m"
+126 -166
View File
@@ -1,46 +1,37 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import fix_subparsers
from dvc.exceptions import BadMetricError
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.exceptions import BadMetricError, DvcException
logger = logging.getLogger(__name__)
def show_metrics(metrics, all_branches=False, all_tags=False):
"""
Args:
metrics (list): Where each element is either a `list`
if an xpath was specified, otherwise a `str`
"""
DEFAULT_PRECISION = 5
def show_metrics(
metrics, all_branches=False, all_tags=False, all_commits=False
):
from flatten_json import flatten
from dvc.utils.diff import format_dict
# When `metrics` contains a `None` key, it means that some files
# specified as `targets` in `repo.metrics.show` didn't contain any metrics.
missing = metrics.pop(None, None)
for branch, val in metrics.items():
if all_branches or all_tags:
logger.info("{branch}:".format(branch=branch))
if all_branches or all_tags or all_commits:
logger.info(f"{branch}:")
for fname, metric in val.items():
if isinstance(metric, dict):
lines = list(metric.values())
elif isinstance(metric, list):
lines = metric
else:
lines = metric.splitlines()
if not isinstance(metric, dict):
logger.info("\t{}: {}".format(fname, str(metric)))
continue
if len(lines) > 1:
logger.info("\t{fname}:".format(fname=fname))
for line in lines:
logger.info("\t\t{content}".format(content=line))
else:
logger.info("\t{}: {}".format(fname, metric))
logger.info(f"\t{fname}:")
for key, value in flatten(format_dict(metric), ".").items():
logger.info(f"\t\t{key}: {value}")
if missing:
raise BadMetricError(missing)
@@ -51,14 +42,23 @@ class CmdMetricsShow(CmdBase):
try:
metrics = self.repo.metrics.show(
self.args.targets,
typ=self.args.type,
xpath=self.args.xpath,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
recursive=self.args.recursive,
)
show_metrics(metrics, self.args.all_branches, self.args.all_tags)
if self.args.show_json:
import json
logger.info(json.dumps(metrics))
else:
show_metrics(
metrics,
self.args.all_branches,
self.args.all_tags,
self.args.all_commits,
)
except DvcException:
logger.exception("failed to show metrics")
return 1
@@ -66,70 +66,41 @@ class CmdMetricsShow(CmdBase):
return 0
class CmdMetricsModify(CmdBase):
def run(self):
try:
self.repo.metrics.modify(
self.args.path, typ=self.args.type, xpath=self.args.xpath
)
except DvcException:
logger.exception("failed to modify metric file settings")
return 1
def _show_diff(diff, markdown=False, no_path=False, old=False, precision=None):
from collections import OrderedDict
return 0
from dvc.utils.diff import table
if precision is None:
precision = DEFAULT_PRECISION
class CmdMetricsAdd(CmdBase):
def run(self):
try:
self.repo.metrics.add(
self.args.path, self.args.type, self.args.xpath
)
except DvcException:
msg = "failed to add metric file '{}'".format(self.args.path)
logger.exception(msg)
return 1
def _round(val):
if isinstance(val, float):
return round(val, precision)
return 0
return val
class CmdMetricsRemove(CmdBase):
def run(self):
try:
self.repo.metrics.remove(self.args.path)
except DvcException:
msg = "failed to remove metric file '{}'".format(self.args.path)
logger.exception(msg)
return 1
return 0
def _show_diff(diff):
from texttable import Texttable
if not diff:
return "No changes."
table = Texttable()
# remove borders to make it easier for users to copy stuff
table.set_chars(("", "", "", ""))
table.set_deco(0)
rows = [["Path", "Metric", "Value", "Change"]]
rows = []
for fname, mdiff in diff.items():
for metric, change in mdiff.items():
rows.append(
[
fname,
metric,
change["new"],
change.get("diff", "diff not supported"),
]
)
table.add_rows(rows)
return table.draw()
sorted_mdiff = OrderedDict(sorted(mdiff.items()))
for metric, change in sorted_mdiff.items():
row = [] if no_path else [fname]
row.append(metric)
if old:
row.append(_round(change.get("old")))
row.append(_round(change["new"]))
row.append(_round(change.get("diff", "diff not supported")))
rows.append(row)
header = [] if no_path else ["Path"]
header.append("Metric")
if old:
header.extend(["Old", "New"])
else:
header.append("Value")
header.append("Change")
return table(header, rows, markdown)
class CmdMetricsDiff(CmdBase):
@@ -139,9 +110,8 @@ class CmdMetricsDiff(CmdBase):
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
targets=self.args.targets,
typ=self.args.type,
xpath=self.args.xpath,
recursive=self.args.recursive,
all=self.args.all,
)
if self.args.show_json:
@@ -149,7 +119,15 @@ class CmdMetricsDiff(CmdBase):
logger.info(json.dumps(diff))
else:
logger.info(_show_diff(diff))
table = _show_diff(
diff,
self.args.show_md,
self.args.no_path,
self.args.old,
precision=self.args.precision,
)
if table:
logger.info(table)
except DvcException:
logger.exception("failed to show metrics diff")
@@ -159,7 +137,7 @@ class CmdMetricsDiff(CmdBase):
def add_parser(subparsers, parent_parser):
METRICS_HELP = "Commands to add, manage, collect and display metrics."
METRICS_HELP = "Commands to display and compare metrics."
metrics_parser = subparsers.add_parser(
"metrics",
@@ -176,7 +154,7 @@ def add_parser(subparsers, parent_parser):
fix_subparsers(metrics_subparsers)
METRICS_SHOW_HELP = "Output metric values."
METRICS_SHOW_HELP = "Print metrics, with optional formatting."
metrics_show_parser = metrics_subparsers.add_parser(
"show",
parents=[parent_parser],
@@ -187,20 +165,11 @@ def add_parser(subparsers, parent_parser):
metrics_show_parser.add_argument(
"targets",
nargs="*",
help="Metric files or directories (see -R) to show",
)
metrics_show_parser.add_argument(
"-t",
"--type",
help=(
"Type of metrics (json/tsv/htsv/csv/hcsv). "
"It can be detected by the file extension automatically. "
"Unsupported types will be treated as raw."
"Limit command scope to these metric files. Using -R, "
"directories to search metric files in can also be given."
),
)
metrics_show_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
)
metrics_show_parser.add_argument(
"-a",
"--all-branches",
@@ -215,6 +184,18 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Show metrics for all tags.",
)
metrics_show_parser.add_argument(
"--all-commits",
action="store_true",
default=False,
help="Show metrics for all commits.",
)
metrics_show_parser.add_argument(
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
metrics_show_parser.add_argument(
"-R",
"--recursive",
@@ -227,53 +208,10 @@ def add_parser(subparsers, parent_parser):
)
metrics_show_parser.set_defaults(func=CmdMetricsShow)
METRICS_ADD_HELP = "Tag file as a metric file."
metrics_add_parser = metrics_subparsers.add_parser(
"add",
parents=[parent_parser],
description=append_doc_link(METRICS_ADD_HELP, "metrics/add"),
help=METRICS_ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
METRICS_DIFF_HELP = (
"Show changes in metrics between commits in the DVC repository, or "
"between a commit and the workspace."
)
metrics_add_parser.add_argument(
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
)
metrics_add_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
)
metrics_add_parser.add_argument("path", help="Path to a metric file.")
metrics_add_parser.set_defaults(func=CmdMetricsAdd)
METRICS_MODIFY_HELP = "Modify metric file options."
metrics_modify_parser = metrics_subparsers.add_parser(
"modify",
parents=[parent_parser],
description=append_doc_link(METRICS_MODIFY_HELP, "metrics/modify"),
help=METRICS_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_modify_parser.add_argument(
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
)
metrics_modify_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
)
metrics_modify_parser.add_argument("path", help="Path to a metric file.")
metrics_modify_parser.set_defaults(func=CmdMetricsModify)
METRICS_REMOVE_HELP = "Remove files's metric tag."
metrics_remove_parser = metrics_subparsers.add_parser(
"remove",
parents=[parent_parser],
description=append_doc_link(METRICS_REMOVE_HELP, "metrics/remove"),
help=METRICS_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_remove_parser.add_argument("path", help="Path to a metric file.")
metrics_remove_parser.set_defaults(func=CmdMetricsRemove)
METRICS_DIFF_HELP = "Show changes in metrics between commits"
" in the DVC repository, or between a commit and the workspace."
metrics_diff_parser = metrics_subparsers.add_parser(
"diff",
parents=[parent_parser],
@@ -287,27 +225,16 @@ def add_parser(subparsers, parent_parser):
metrics_diff_parser.add_argument(
"b_rev",
nargs="?",
help=("New Git commit to compare (defaults to the current workspace)"),
help="New Git commit to compare (defaults to the current workspace)",
)
metrics_diff_parser.add_argument(
"--targets",
nargs="*",
help=(
"Metric files or directories (see -R) to show diff for. "
"Shows diff for all metric files by default."
"Limit command scope to these metric files. Using -R, "
"directories to search metric files in can also be given."
),
)
metrics_diff_parser.add_argument(
"-t",
"--type",
help=(
"Type of metrics (json/tsv/htsv/csv/hcsv). "
"It can be detected by the file extension automatically. "
"Unsupported types will be treated as raw."
),
)
metrics_diff_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
metavar="<paths>",
)
metrics_diff_parser.add_argument(
"-R",
@@ -319,10 +246,43 @@ def add_parser(subparsers, parent_parser):
"metric files."
),
)
metrics_diff_parser.add_argument(
"--all",
action="store_true",
default=False,
help="Show unchanged metrics as well.",
)
metrics_diff_parser.add_argument(
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
metrics_diff_parser.add_argument(
"--show-md",
action="store_true",
default=False,
help="Show tabulated output in the Markdown format (GFM).",
)
metrics_diff_parser.add_argument(
"--old",
action="store_true",
default=False,
help="Show old metric value.",
)
metrics_diff_parser.add_argument(
"--no-path",
action="store_true",
default=False,
help="Don't show metric path.",
)
metrics_diff_parser.add_argument(
"--precision",
type=int,
help=(
"Round metrics to `n` digits precision after the decimal point. "
f"Rounds to {DEFAULT_PRECISION} digits by default."
),
metavar="<n>",
)
metrics_diff_parser.set_defaults(func=CmdMetricsDiff)
+1 -3
View File
@@ -1,11 +1,9 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
+118
View File
@@ -0,0 +1,118 @@
import argparse
import logging
from collections import OrderedDict
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
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):
def run(self):
try:
diff = self.repo.params.diff(
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
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)
if table:
logger.info(table)
except DvcException:
logger.exception("failed to show params diff")
return 1
return 0
def add_parser(subparsers, parent_parser):
PARAMS_HELP = "Commands to display params."
params_parser = subparsers.add_parser(
"params",
parents=[parent_parser],
description=append_doc_link(PARAMS_HELP, "params"),
help=PARAMS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
params_subparsers = params_parser.add_subparsers(
dest="cmd",
help="Use `dvc params CMD --help` to display command-specific help.",
)
fix_subparsers(params_subparsers)
PARAMS_DIFF_HELP = (
"Show changes in params between commits in the DVC repository, or "
"between a commit and the workspace."
)
params_diff_parser = params_subparsers.add_parser(
"diff",
parents=[parent_parser],
description=append_doc_link(PARAMS_DIFF_HELP, "params/diff"),
help=PARAMS_DIFF_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
params_diff_parser.add_argument(
"a_rev", nargs="?", help="Old Git commit to compare (defaults to HEAD)"
)
params_diff_parser.add_argument(
"b_rev",
nargs="?",
help=("New Git commit to compare (defaults to the current workspace)"),
)
params_diff_parser.add_argument(
"--all",
action="store_true",
default=False,
help="Show unchanged params as well.",
)
params_diff_parser.add_argument(
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
params_diff_parser.add_argument(
"--show-md",
action="store_true",
default=False,
help="Show tabulated output in the Markdown format (GFM).",
)
params_diff_parser.add_argument(
"--no-path",
action="store_true",
default=False,
help="Don't show params path.",
)
params_diff_parser.set_defaults(func=CmdParamsDiff)
-242
View File
@@ -1,242 +0,0 @@
import argparse
import logging
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdPipelineShow(CmdBase):
def _show(self, target, commands, outs, locked):
import networkx
from dvc.stage import Stage
stage = Stage.load(self.repo, target)
G = self.repo.graph
stages = networkx.dfs_postorder_nodes(G, stage)
if locked:
stages = [s for s in stages if s.locked]
for stage in stages:
if commands:
if stage.cmd is None:
continue
logger.info(stage.cmd)
elif outs:
for out in stage.outs:
logger.info(str(out))
else:
logger.info(stage.path_in_repo)
def _build_graph(self, target, commands, outs):
import networkx
from dvc.stage import Stage
from dvc.repo.graph import get_pipeline
target_stage = Stage.load(self.repo, target)
G = get_pipeline(self.repo.pipelines, target_stage)
nodes = set()
for stage in networkx.dfs_preorder_nodes(G, target_stage):
if commands:
if stage.cmd is None:
continue
nodes.add(stage.cmd)
elif outs:
for out in stage.outs:
nodes.add(str(out))
else:
nodes.add(stage.relpath)
edges = []
for from_stage, to_stage in networkx.edge_dfs(G, target_stage):
if commands:
if to_stage.cmd is None:
continue
edges.append((from_stage.cmd, to_stage.cmd))
elif outs:
for from_out in from_stage.outs:
for to_out in to_stage.outs:
edges.append((str(from_out), str(to_out)))
else:
edges.append((from_stage.relpath, to_stage.relpath))
return list(nodes), edges, networkx.is_tree(G)
def _show_ascii(self, target, commands, outs):
from dvc.dagascii import draw
nodes, edges, _ = self._build_graph(target, commands, outs)
if not nodes:
return
draw(nodes, edges)
def _show_dependencies_tree(self, target, commands, outs):
from treelib import Tree
nodes, edges, is_tree = self._build_graph(target, commands, outs)
if not nodes:
return
if not is_tree:
raise DvcException(
"DAG is not a tree, can not print it in tree-structure way, "
"please use --ascii instead"
)
tree = Tree()
tree.create_node(target, target) # Root node
observe_list = [target]
while len(observe_list) > 0:
current_root = observe_list[0]
for edge in edges:
if edge[0] == current_root:
tree.create_node(edge[1], edge[1], parent=current_root)
observe_list.append(edge[1])
observe_list.pop(0)
tree.show()
def _write_dot(self, target, commands, outs):
import io
import networkx
from networkx.drawing.nx_pydot import write_dot
_, edges, _ = self._build_graph(target, commands, outs)
edges = [edge[::-1] for edge in edges]
simple_g = networkx.DiGraph()
simple_g.add_edges_from(edges)
dot_file = io.StringIO()
write_dot(simple_g, dot_file)
logger.info(dot_file.getvalue())
def run(self):
if not self.args.targets:
self.args.targets = self.default_targets
for target in self.args.targets:
try:
if self.args.ascii:
self._show_ascii(
target, self.args.commands, self.args.outs
)
elif self.args.dot:
self._write_dot(target, self.args.commands, self.args.outs)
elif self.args.tree:
self._show_dependencies_tree(
target, self.args.commands, self.args.outs
)
else:
self._show(
target,
self.args.commands,
self.args.outs,
self.args.locked,
)
except DvcException:
msg = "failed to show pipeline for '{}'".format(target)
logger.exception(msg)
return 1
return 0
class CmdPipelineList(CmdBase):
def run(self):
pipelines = self.repo.pipelines
for pipeline in pipelines:
for stage in pipeline:
logger.info(stage.relpath)
if len(pipeline) != 0:
logger.info("=" * 80)
logger.info("{} pipelines total".format(len(pipelines)))
return 0
def add_parser(subparsers, parent_parser):
PIPELINE_HELP = "Manage pipelines."
pipeline_parser = subparsers.add_parser(
"pipeline",
parents=[parent_parser],
description=append_doc_link(PIPELINE_HELP, "pipeline"),
help=PIPELINE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_subparsers = pipeline_parser.add_subparsers(
dest="cmd",
help="Use `dvc pipeline CMD --help` for command-specific help.",
)
fix_subparsers(pipeline_subparsers)
PIPELINE_SHOW_HELP = "Show pipelines."
pipeline_show_parser = pipeline_subparsers.add_parser(
"show",
parents=[parent_parser],
description=append_doc_link(PIPELINE_SHOW_HELP, "pipeline/show"),
help=PIPELINE_SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_show_group = pipeline_show_parser.add_mutually_exclusive_group()
pipeline_show_group.add_argument(
"-c",
"--commands",
action="store_true",
default=False,
help="Print commands instead of paths to DVC-files.",
)
pipeline_show_group.add_argument(
"-o",
"--outs",
action="store_true",
default=False,
help="Print output files instead of paths to DVC-files.",
)
pipeline_show_parser.add_argument(
"-l",
"--locked",
action="store_true",
default=False,
help="Print locked DVC stages",
)
pipeline_show_parser.add_argument(
"--ascii",
action="store_true",
default=False,
help="Output DAG as ASCII.",
)
pipeline_show_parser.add_argument(
"--dot",
action="store_true",
default=False,
help="Print DAG with .dot format.",
)
pipeline_show_parser.add_argument(
"--tree",
action="store_true",
default=False,
help="Output DAG as Dependencies Tree.",
)
pipeline_show_parser.add_argument(
"targets",
nargs="*",
help="DVC-files to show pipeline for. Optional. "
"(Finds all DVC-files in the workspace by default.)",
)
pipeline_show_parser.set_defaults(func=CmdPipelineShow)
PIPELINE_LIST_HELP = "List pipelines."
pipeline_list_parser = pipeline_subparsers.add_parser(
"list",
parents=[parent_parser],
description=append_doc_link(PIPELINE_LIST_HELP, "pipeline/list"),
help=PIPELINE_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_list_parser.set_defaults(func=CmdPipelineList)
+232
View File
@@ -0,0 +1,232 @@
import argparse
import logging
import os
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
from dvc.schema import PLOT_PROPS
from dvc.utils import format_link
logger = logging.getLogger(__name__)
PAGE_HTML = """<!DOCTYPE html>
<html>
<head>
<title>DVC Plot</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
</head>
<body>
{divs}
</body>
</html>"""
DIV_HTML = """<div id = "{id}"></div>
<script type = "text/javascript">
var spec = {vega_json};
vegaEmbed('#{id}', spec);
</script>"""
class CmdPlots(CmdBase):
def _func(self, *args, **kwargs):
raise NotImplementedError
def _props(self):
# Pass only props specified by user, to not shadow ones from plot def
props = {p: getattr(self.args, p) for p in PLOT_PROPS}
return {k: v for k, v in props.items() if v is not None}
def run(self):
if self.args.show_vega:
if not self.args.targets:
logger.error("please specify a target for `--show-vega`")
return 1
if len(self.args.targets) > 1:
logger.error(
"you can only specify one target for `--show-vega`"
)
return 1
try:
plots = self._func(targets=self.args.targets, props=self._props())
if self.args.show_vega:
target = self.args.targets[0]
logger.info(plots[target])
return 0
divs = [
DIV_HTML.format(id=f"plot{i}", vega_json=plot)
for i, plot in enumerate(plots.values())
]
html = PAGE_HTML.format(divs="\n".join(divs))
path = self.args.out or "plots.html"
with open(path, "w") as fobj:
fobj.write(html)
logger.info(
"file://{}".format(os.path.join(self.repo.root_dir, path))
)
except DvcException:
logger.exception("")
return 1
return 0
class CmdPlotsShow(CmdPlots):
def _func(self, *args, **kwargs):
return self.repo.plots.show(*args, **kwargs)
class CmdPlotsDiff(CmdPlots):
def _func(self, *args, **kwargs):
return self.repo.plots.diff(*args, revs=self.args.revisions, **kwargs)
class CmdPlotsModify(CmdPlots):
def run(self):
self.repo.plots.modify(
self.args.target, props=self._props(), unset=self.args.unset,
)
return 0
def add_parser(subparsers, parent_parser):
PLOTS_HELP = (
"Commands to visualize and compare plot metrics in structured files "
"(JSON, YAML, CSV, TSV)"
)
plots_parser = subparsers.add_parser(
"plots",
parents=[parent_parser],
description=append_doc_link(PLOTS_HELP, "plots"),
help=PLOTS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
plots_subparsers = plots_parser.add_subparsers(
dest="cmd",
help="Use `dvc plots CMD --help` to display command-specific help.",
)
fix_subparsers(plots_subparsers)
SHOW_HELP = "Generate plots from metric files."
plots_show_parser = plots_subparsers.add_parser(
"show",
parents=[parent_parser],
description=append_doc_link(SHOW_HELP, "plots/show"),
help=SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
plots_show_parser.add_argument(
"targets",
nargs="*",
help="Plots files to visualize. Shows all plots by default.",
)
_add_props_arguments(plots_show_parser)
_add_output_arguments(plots_show_parser)
plots_show_parser.set_defaults(func=CmdPlotsShow)
PLOTS_DIFF_HELP = (
"Show multiple versions of plot metrics "
"by plotting them in a single image."
)
plots_diff_parser = plots_subparsers.add_parser(
"diff",
parents=[parent_parser],
description=append_doc_link(PLOTS_DIFF_HELP, "plots/diff"),
help=PLOTS_DIFF_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
plots_diff_parser.add_argument(
"--targets",
nargs="*",
help="Plots file to visualize. Shows all plots by default.",
metavar="<path>",
)
plots_diff_parser.add_argument(
"revisions", nargs="*", default=None, help="Git commits to plot from",
)
_add_props_arguments(plots_diff_parser)
_add_output_arguments(plots_diff_parser)
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
PLOTS_MODIFY_HELP = "Modify display properties of plot metric files."
plots_modify_parser = plots_subparsers.add_parser(
"modify",
parents=[parent_parser],
description=append_doc_link(PLOTS_MODIFY_HELP, "plots/modify"),
help=PLOTS_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
plots_modify_parser.add_argument(
"target", help="Metric file to set properties to",
)
_add_props_arguments(plots_modify_parser)
plots_modify_parser.add_argument(
"--unset",
nargs="*",
metavar="<property>",
help="Unset one or more display properties.",
)
plots_modify_parser.set_defaults(func=CmdPlotsModify)
def _add_props_arguments(parser):
parser.add_argument(
"-t",
"--template",
nargs="?",
default=None,
help=(
"Special JSON or HTML schema file to inject with the data. "
"See {}".format(
format_link("https://man.dvc.org/plots#plot-templates")
)
),
metavar="<path>",
)
parser.add_argument(
"-x", default=None, help="Field name for X axis.", metavar="<field>"
)
parser.add_argument(
"-y", default=None, help="Field name for Y axis.", metavar="<field>"
)
parser.add_argument(
"--no-header",
action="store_false",
dest="header",
default=None, # Use default None to distinguish when it's not used
help="Provided CSV or TSV datafile does not have a header.",
)
parser.add_argument(
"--title", default=None, metavar="<text>", help="Plot title."
)
parser.add_argument(
"--x-label", default=None, help="X axis label", metavar="<text>"
)
parser.add_argument(
"--y-label", default=None, help="Y axis label", metavar="<text>"
)
def _add_output_arguments(parser):
parser.add_argument(
"-o",
"--out",
default=None,
help="Destination path to save plots to",
metavar="<path>",
)
parser.add_argument(
"--show-vega",
action="store_true",
default=False,
help="Show output in Vega format.",
)
+78 -27
View File
@@ -1,9 +1,9 @@
import argparse
import logging
from dvc.config import ConfigError
from dvc.command.base import append_doc_link, fix_subparsers
from dvc.command.config import CmdConfig
from dvc.config import ConfigError
from dvc.utils import format_link
logger = logging.getLogger(__name__)
@@ -18,17 +18,13 @@ class CmdRemote(CmdConfig):
def _check_exists(self, conf):
if self.args.name not in conf["remote"]:
raise ConfigError(
"remote '{}' doesn't exists.".format(self.args.name)
)
raise ConfigError(f"remote '{self.args.name}' doesn't exists.")
class CmdRemoteAdd(CmdRemote):
def run(self):
if self.args.default:
logger.info(
"Setting '{}' as a default remote.".format(self.args.name)
)
logger.info(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:
@@ -66,7 +62,11 @@ class CmdRemoteModify(CmdRemote):
def run(self):
with self.config.edit(self.args.level) as conf:
self._check_exists(conf)
conf["remote"][self.args.name][self.args.option] = self.args.value
section = conf["remote"][self.args.name]
if self.args.unset:
section.pop(self.args.option, None)
else:
section[self.args.option] = self.args.value
return 0
@@ -85,7 +85,18 @@ class CmdRemoteDefault(CmdRemote):
if self.args.unset:
conf["core"].pop("remote", None)
else:
conf["core"]["remote"] = self.args.name
merged_conf = self.config.load_config_to_level(
self.args.level
)
if (
self.args.name in conf["remote"]
or self.args.name in merged_conf["remote"]
):
conf["core"]["remote"] = self.args.name
else:
raise ConfigError(
"default remote must be present in remote list."
)
return 0
@@ -97,10 +108,39 @@ class CmdRemoteList(CmdRemote):
return 0
class CmdRemoteRename(CmdRemote):
def _rename_default(self, conf):
if conf["core"].get("remote") == self.args.name:
conf["core"]["remote"] = self.args.new
def run(self):
all_config = self.config.load_config_to_level(None)
if self.args.new in all_config.get("remote", {}):
raise ConfigError(
"Rename failed. Remote name '{}' already exists.".format(
{self.args.new}
)
)
with self.config.edit(self.args.level) as conf:
self._check_exists(conf)
conf["remote"][self.args.new] = conf["remote"][self.args.name]
del conf["remote"][self.args.name]
self._rename_default(conf)
for level in reversed(self.config.LEVELS):
if level == self.args.level:
break
with self.config.edit(level) as level_conf:
self._rename_default(level_conf)
return 0
def add_parser(subparsers, parent_parser):
from dvc.command.config import parent_config_parser
REMOTE_HELP = "Manage remote storage configuration."
REMOTE_HELP = "Set up and manage data remotes."
remote_parser = subparsers.add_parser(
"remote",
parents=[parent_parser],
@@ -116,7 +156,7 @@ def add_parser(subparsers, parent_parser):
fix_subparsers(remote_subparsers)
REMOTE_ADD_HELP = "Add remote."
REMOTE_ADD_HELP = "Add a new data remote."
remote_add_parser = remote_subparsers.add_parser(
"add",
parents=[parent_config_parser, parent_parser],
@@ -147,7 +187,7 @@ def add_parser(subparsers, parent_parser):
)
remote_add_parser.set_defaults(func=CmdRemoteAdd)
REMOTE_DEFAULT_HELP = "Set/unset default remote."
REMOTE_DEFAULT_HELP = "Set/unset the default data remote."
remote_default_parser = remote_subparsers.add_parser(
"default",
parents=[parent_config_parser, parent_parser],
@@ -167,20 +207,7 @@ def add_parser(subparsers, parent_parser):
)
remote_default_parser.set_defaults(func=CmdRemoteDefault)
REMOTE_REMOVE_HELP = "Remove remote."
remote_remove_parser = remote_subparsers.add_parser(
"remove",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_REMOVE_HELP, "remote/remove"),
help=REMOTE_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_remove_parser.add_argument(
"name", help="Name of the remote to remove."
)
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
REMOTE_MODIFY_HELP = "Modify remote."
REMOTE_MODIFY_HELP = "Modify the configuration of a data remote."
remote_modify_parser = remote_subparsers.add_parser(
"modify",
parents=[parent_config_parser, parent_parser],
@@ -204,7 +231,7 @@ def add_parser(subparsers, parent_parser):
)
remote_modify_parser.set_defaults(func=CmdRemoteModify)
REMOTE_LIST_HELP = "List available remotes."
REMOTE_LIST_HELP = "List all available data remotes."
remote_list_parser = remote_subparsers.add_parser(
"list",
parents=[parent_config_parser, parent_parser],
@@ -213,3 +240,27 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_list_parser.set_defaults(func=CmdRemoteList)
REMOTE_REMOVE_HELP = "Remove a data remote."
remote_remove_parser = remote_subparsers.add_parser(
"remove",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_REMOVE_HELP, "remote/remove"),
help=REMOTE_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_remove_parser.add_argument(
"name", help="Name of the remote to remove."
)
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
REMOTE_RENAME_HELP = "Rename a DVC remote"
remote_rename_parser = remote_subparsers.add_parser(
"rename",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(REMOTE_RENAME_HELP, "remote/rename"),
help=REMOTE_RENAME_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_rename_parser.add_argument("name", help="Remote to be renamed")
remote_rename_parser.add_argument("new", help="New name of the remote")
remote_rename_parser.set_defaults(func=CmdRemoteRename)
+6 -45
View File
@@ -1,48 +1,25 @@
import argparse
import logging
import dvc.prompt as prompt
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdRemove(CmdBase):
def _is_outs_only(self, target):
if not self.args.purge:
return True
if self.args.force:
return False
msg = "Are you sure you want to remove '{}' with its outputs?".format(
target
)
if prompt.confirm(msg):
return False
raise DvcException(
"Cannot purge without a confirmation from the user."
" Use `-f` to force."
)
def run(self):
for target in self.args.targets:
try:
outs_only = self._is_outs_only(target)
self.repo.remove(target, outs_only=outs_only)
self.repo.remove(target, outs=self.args.outs)
except DvcException:
logger.exception("failed to remove '{}'".format(target))
logger.exception(f"failed to remove '{target}'")
return 1
return 0
def add_parser(subparsers, parent_parser):
REMOVE_HELP = "Remove DVC-file outputs."
REMOVE_HELP = "Remove stage entry and unprotect outputs"
remove_parser = subparsers.add_parser(
"remove",
parents=[parent_parser],
@@ -50,27 +27,11 @@ def add_parser(subparsers, parent_parser):
help=REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remove_parser_group = remove_parser.add_mutually_exclusive_group()
remove_parser_group.add_argument(
"-o",
remove_parser.add_argument(
"--outs",
action="store_true",
default=True,
help="Only remove DVC-file outputs. (Default)",
)
remove_parser_group.add_argument(
"-p",
"--purge",
action="store_true",
default=False,
help="Remove DVC-file and all its outputs.",
)
remove_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force purge.",
help="Remove outputs as well.",
)
remove_parser.add_argument(
"targets", nargs="+", help="DVC-files to remove."
+19 -7
View File
@@ -2,13 +2,12 @@ import argparse
import logging
import os
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.command.metrics import show_metrics
from dvc.command.status import CmdDataStatus
from dvc.dvcfile import PIPELINE_FILE
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -35,10 +34,11 @@ class CmdRepro(CmdBase):
interactive=self.args.interactive,
pipeline=self.args.pipeline,
all_pipelines=self.args.all_pipelines,
ignore_build_cache=self.args.ignore_build_cache,
run_cache=not self.args.no_run_cache,
no_commit=self.args.no_commit,
downstream=self.args.downstream,
recursive=self.args.recursive,
force_downstream=self.args.force_downstream,
)
if len(stages) == 0:
@@ -57,7 +57,9 @@ class CmdRepro(CmdBase):
def add_parser(subparsers, parent_parser):
REPRO_HELP = "Check for changes and reproduce stages and dependencies."
REPRO_HELP = (
"Reproduce complete or partial pipelines by executing their stages."
)
repro_parser = subparsers.add_parser(
"repro",
parents=[parent_parser],
@@ -68,7 +70,7 @@ def add_parser(subparsers, parent_parser):
repro_parser.add_argument(
"targets",
nargs="*",
help="DVC-file to reproduce. 'Dvcfile' by default.",
help=f"Stages to reproduce. '{PIPELINE_FILE}' by default.",
)
repro_parser.add_argument(
"-f",
@@ -90,6 +92,7 @@ def add_parser(subparsers, parent_parser):
"--cwd",
default=os.path.curdir,
help="Directory within your repo to reproduce from.",
metavar="<path>",
)
repro_parser.add_argument(
"-m",
@@ -135,7 +138,16 @@ def add_parser(subparsers, parent_parser):
help="Reproduce all stages in the specified directory.",
)
repro_parser.add_argument(
"--ignore-build-cache",
"--no-run-cache",
action="store_true",
default=False,
help=(
"Execute stage commands even if they have already been run with "
"the same command/dependencies/outputs/etc before."
),
)
repro_parser.add_argument(
"--force-downstream",
action="store_true",
default=False,
help="Reproduce all descendants of a changed stage even if their "
+2 -3
View File
@@ -1,8 +1,7 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBaseNoRepo
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.utils import relpath
logger = logging.getLogger(__name__)
@@ -17,7 +16,7 @@ class CmdRoot(CmdBaseNoRepo):
def add_parser(subparsers, parent_parser):
ROOT_HELP = "Relative path to the repository's directory."
ROOT_HELP = "Return the relative path to the root of the DVC project."
root_parser = subparsers.add_parser(
"root",
parents=[parent_parser],
+72 -36
View File
@@ -1,18 +1,14 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdRun(CmdBase):
def run(self):
overwrite = self.args.yes or self.args.overwrite_dvcfile
if not any(
[
self.args.deps,
@@ -20,15 +16,18 @@ class CmdRun(CmdBase):
self.args.outs_no_cache,
self.args.metrics,
self.args.metrics_no_cache,
self.args.plots,
self.args.plots_no_cache,
self.args.outs_persist,
self.args.outs_persist_no_cache,
self.args.params,
self.args.command,
]
): # pragma: no cover
logger.error(
"too few arguments. Specify at least one: `-d`, `-o`, `-O`, "
"`-m`, `-M`, `--outs-persist`, `--outs-persist-no-cache`, "
"`command`."
"`-m`, `-M`, `-p`, `--plots`, `--plots-no-cache`, "
"`--outs-persist`, `--outs-persist-no-cache`, `command`."
)
return 1
@@ -39,21 +38,25 @@ class CmdRun(CmdBase):
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,
deps=self.args.deps,
params=self.args.params,
fname=self.args.file,
cwd=self.args.cwd,
wdir=self.args.wdir,
no_exec=self.args.no_exec,
overwrite=overwrite,
ignore_build_cache=self.args.ignore_build_cache,
remove_outs=self.args.remove_outs,
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,
always_changed=self.args.always_changed,
name=self.args.name,
single_stage=self.args.single_stage,
external=self.args.external,
)
except DvcException:
logger.exception("failed to run command")
logger.exception("")
return 1
return 0
@@ -77,7 +80,7 @@ class CmdRun(CmdBase):
if " " not in argument or '"' in argument:
return argument
return '"{}"'.format(argument)
return f'"{argument}"'
def add_parser(subparsers, parent_parser):
@@ -95,6 +98,10 @@ def add_parser(subparsers, parent_parser):
action="append",
default=[],
help="Declare dependencies for reproducible cmd.",
metavar="<path>",
)
run_parser.add_argument(
"-n", "--name", help="Stage name.",
)
run_parser.add_argument(
"-o",
@@ -102,6 +109,7 @@ def add_parser(subparsers, parent_parser):
action="append",
default=[],
help="Declare output file or directory.",
metavar="<filename>",
)
run_parser.add_argument(
"-O",
@@ -110,32 +118,56 @@ def add_parser(subparsers, parent_parser):
default=[],
help="Declare output file or directory "
"(do not put into DVC cache).",
metavar="<filename>",
)
run_parser.add_argument(
"-p",
"--params",
action="append",
default=[],
help="Declare parameter to use as additional dependency.",
metavar="[<filename>:]<params_list>",
)
run_parser.add_argument(
"-m",
"--metrics",
action="append",
default=[],
help="Declare output metric file or directory.",
help="Declare output metric file.",
metavar="<path>",
)
run_parser.add_argument(
"-M",
"--metrics-no-cache",
action="append",
default=[],
help="Declare output metric file or directory "
"(do not put into DVC cache).",
help="Declare output metric file (do not put into DVC cache).",
metavar="<path>",
)
run_parser.add_argument(
"-f", "--file", help="Specify name of the DVC-file it generates."
"--plots",
action="append",
default=[],
help="Declare output plot file.",
metavar="<path>",
)
run_parser.add_argument(
"-c", "--cwd", help="Deprecated, use -w and -f instead."
"--plots-no-cache",
action="append",
default=[],
help="Declare output plot file (do not put into DVC cache).",
metavar="<path>",
)
run_parser.add_argument(
"--file",
help="Specify name of the DVC-file this command will generate.",
metavar="<filename>",
)
run_parser.add_argument(
"-w",
"--wdir",
help="Directory within your repo to run your command in.",
metavar="<path>",
)
run_parser.add_argument(
"--no-exec",
@@ -144,30 +176,20 @@ def add_parser(subparsers, parent_parser):
help="Only create stage file without actually running it.",
)
run_parser.add_argument(
"-y",
"--yes",
"-f",
"--force",
action="store_true",
default=False,
help="Deprecated, use --overwrite-dvcfile instead",
help="Overwrite existing stage",
)
run_parser.add_argument(
"--overwrite-dvcfile",
"--no-run-cache",
action="store_true",
default=False,
help="Overwrite existing DVC-file without asking for confirmation.",
)
run_parser.add_argument(
"--ignore-build-cache",
action="store_true",
default=False,
help="Run this stage even if it has been already ran with the same "
"command/dependencies/outputs/etc before.",
)
run_parser.add_argument(
"--remove-outs",
action="store_true",
default=False,
help="Deprecated, this is now the default behavior",
help=(
"Execute the command even if this stage has already been run "
"with the same command/dependencies/outputs/etc before."
),
)
run_parser.add_argument(
"--no-commit",
@@ -181,6 +203,7 @@ def add_parser(subparsers, parent_parser):
default=[],
help="Declare output file or directory that will not be "
"removed upon repro.",
metavar="<filename>",
)
run_parser.add_argument(
"--outs-persist-no-cache",
@@ -188,6 +211,7 @@ def add_parser(subparsers, parent_parser):
default=[],
help="Declare output file or directory that will not be "
"removed upon repro (do not put into DVC cache).",
metavar="<filename>",
)
run_parser.add_argument(
"--always-changed",
@@ -195,6 +219,18 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Always consider this DVC-file as changed.",
)
run_parser.add_argument(
"--single-stage",
action="store_true",
default=False,
help=argparse.SUPPRESS,
)
run_parser.add_argument(
"--external",
action="store_true",
default=False,
help="Allow outputs that are outside of the DVC repository.",
)
run_parser.add_argument(
"command", nargs=argparse.REMAINDER, help="Command to execute."
)
+14 -8
View File
@@ -2,7 +2,6 @@ import logging
from dvc.command.data_sync import CmdDataBase
logger = logging.getLogger(__name__)
@@ -20,7 +19,7 @@ class CmdDataStatus(CmdDataBase):
ind = indent * self.STATUS_INDENT
if isinstance(status, str):
logger.info("{}{}".format(ind, status))
logger.info(f"{ind}{status}")
return
if isinstance(status, list):
@@ -34,7 +33,7 @@ class CmdDataStatus(CmdDataBase):
if isinstance(value, str):
logger.info("{}{}{}".format(ind, self._normalize(value), key))
elif value:
logger.info("{}{}:".format(ind, key))
logger.info(f"{ind}{key}:")
self._show(value, indent + 1)
def run(self):
@@ -47,13 +46,20 @@ class CmdDataStatus(CmdDataBase):
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,
)
if st:
if self.args.quiet:
return 1
else:
self._show(st, indent)
if self.args.quiet:
return bool(st)
if self.args.show_json:
import json
logger.info(json.dumps(st))
elif st:
self._show(st, indent)
else:
logger.info(self.UP_TO_DATE_MSG)
-156
View File
@@ -1,156 +0,0 @@
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import fix_subparsers
from dvc.exceptions import DvcException
from dvc.utils import to_yaml_string
logger = logging.getLogger(__name__)
class CmdTagAdd(CmdBase):
def run(self):
for target in self.args.targets:
try:
self.repo.tag.add(
self.args.tag,
target=target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except DvcException:
logger.exception("failed to add tag")
return 1
return 0
class CmdTagRemove(CmdBase):
def run(self):
for target in self.args.targets:
try:
self.repo.tag.remove(
self.args.tag,
target=target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except DvcException:
logger.exception("failed to remove tag")
return 1
return 0
class CmdTagList(CmdBase):
def run(self):
for target in self.args.targets:
try:
tags = self.repo.tag.list(
target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
if tags:
logger.info(to_yaml_string(tags))
except DvcException:
logger.exception("failed list tags")
return 1
return 0
def add_parser(subparsers, parent_parser):
TAG_HELP = "A set of commands to manage DVC tags."
tag_parser = subparsers.add_parser(
"tag",
parents=[parent_parser],
description=append_doc_link(TAG_HELP, "tag"),
add_help=False,
)
tag_subparsers = tag_parser.add_subparsers(
dest="cmd",
help="Use DVC tag CMD --help to display command-specific help.",
)
fix_subparsers(tag_subparsers)
TAG_ADD_HELP = "Add DVC tag."
tag_add_parser = tag_subparsers.add_parser(
"add",
parents=[parent_parser],
description=append_doc_link(TAG_ADD_HELP, "tag-add"),
help=TAG_ADD_HELP,
)
tag_add_parser.add_argument("tag", help="Dvc tag.")
tag_add_parser.add_argument(
"targets", nargs="*", default=[None], help="DVC-files."
)
tag_add_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Add tag for all dependencies of the specified DVC-file.",
)
tag_add_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Add tag for subdirectories of the specified directory.",
)
tag_add_parser.set_defaults(func=CmdTagAdd)
TAG_REMOVE_HELP = "Remove DVC tag."
tag_remove_parser = tag_subparsers.add_parser(
"remove",
parents=[parent_parser],
description=append_doc_link(TAG_REMOVE_HELP, "tag-remove"),
help=TAG_REMOVE_HELP,
)
tag_remove_parser.add_argument("tag", help="Dvc tag.")
tag_remove_parser.add_argument(
"targets", nargs="*", default=[None], help="DVC-files."
)
tag_remove_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Remove tag for all dependencies of the specified DVC-file.",
)
tag_remove_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Remove tag for subdirectories of the specified directory.",
)
tag_remove_parser.set_defaults(func=CmdTagRemove)
TAG_LIST_HELP = "List DVC tags."
tag_list_parser = tag_subparsers.add_parser(
"list",
parents=[parent_parser],
description=append_doc_link(TAG_LIST_HELP, "tag-list"),
help=TAG_LIST_HELP,
)
tag_list_parser.add_argument(
"targets", nargs="*", default=[None], help="DVC-files."
)
tag_list_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="List tags for all dependencies of the specified DVC-file.",
)
tag_list_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="List tags for subdirectories of the specified directory.",
)
tag_list_parser.set_defaults(func=CmdTagList)
+2 -4
View File
@@ -1,11 +1,9 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -15,7 +13,7 @@ class CmdUnprotect(CmdBase):
try:
self.repo.unprotect(target)
except DvcException:
msg = "failed to unprotect '{}'".format(target)
msg = f"failed to unprotect '{target}'"
logger.exception(msg)
return 1
return 0
+21 -10
View File
@@ -1,23 +1,24 @@
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
logger = logging.getLogger(__name__)
class CmdUpdate(CmdBase):
def run(self):
ret = 0
for target in self.args.targets:
try:
self.repo.update(target, self.args.rev)
except DvcException:
logger.exception("failed to update '{}'.".format(target))
ret = 1
try:
self.repo.update(
targets=self.args.targets,
rev=self.args.rev,
recursive=self.args.recursive,
)
except DvcException:
logger.exception("failed update data")
ret = 1
return ret
@@ -34,6 +35,16 @@ def add_parser(subparsers, parent_parser):
"targets", nargs="+", help="DVC-files to update."
)
update_parser.add_argument(
"--rev", nargs="?", help="Git revision (e.g. SHA, branch, tag)"
"--rev",
nargs="?",
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
update_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Update all stages in the specified directory.",
)
update_parser.set_defaults(func=CmdUpdate)
+40 -22
View File
@@ -2,21 +2,23 @@ import argparse
import itertools
import logging
import os
import pathlib
import platform
import uuid
import pathlib
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc.scm.base import SCMError
from dvc.system import System
from dvc.utils import is_binary, relpath
from dvc.utils.pkg import PKG
from dvc.version import __version__
try:
import psutil
except ImportError:
psutil = None
from dvc.utils import is_binary, relpath
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.version import __version__
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc.system import System
from dvc.utils.pkg import PKG
logger = logging.getLogger(__name__)
@@ -26,12 +28,12 @@ class CmdVersion(CmdBaseNoRepo):
from dvc.repo import Repo
info = [
"DVC version: {}".format(__version__),
"Python version: {}".format(platform.python_version()),
"Platform: {}".format(platform.platform()),
"Binary: {}".format(is_binary()),
"Package: {}".format(PKG),
"Supported remotes: {}".format(self.get_supported_remotes()),
f"DVC version: {__version__}",
f"Python version: {platform.python_version()}",
f"Platform: {platform.platform()}",
f"Binary: {is_binary()}",
f"Package: {PKG}",
f"Supported remotes: {self.get_supported_remotes()}",
]
try:
@@ -49,7 +51,7 @@ class CmdVersion(CmdBaseNoRepo):
if psutil:
fs_type = self.get_fs_type(repo.cache.local.cache_dir)
info.append(
"Filesystem type (cache directory): {}".format(fs_type)
f"Filesystem type (cache directory): {fs_type}"
)
else:
logger.warning(
@@ -62,10 +64,15 @@ class CmdVersion(CmdBaseNoRepo):
except NotDvcRepoError:
root_directory = os.getcwd()
except SCMError:
root_directory = os.getcwd()
info.append("Repo: dvc, git (broken)")
else:
info.append("Repo: {}".format(_get_dvc_repo_info(repo)))
if psutil:
fs_root = self.get_fs_type(os.path.abspath(root_directory))
info.append("Filesystem type (workspace): {}".format(fs_root))
info.append(f"Filesystem type (workspace): {fs_root}")
logger.info("\n".join(info))
return 0
@@ -108,26 +115,37 @@ class CmdVersion(CmdBaseNoRepo):
os.unlink(dst)
except DvcException:
status = "not supported"
cache.append("{name} - {status}".format(name=name, status=status))
cache.append(f"{name} - {status}")
os.remove(src)
return ", ".join(cache)
@staticmethod
def get_supported_remotes():
from dvc.remote import REMOTES
from dvc.remote import TREES
supported_remotes = []
for remote in REMOTES:
if not remote.get_missing_deps():
supported_remotes.append(remote.scheme)
for tree_cls in TREES:
if not tree_cls.get_missing_deps():
supported_remotes.append(tree_cls.scheme)
return ", ".join(supported_remotes)
def add_parser(subparsers, parent_parser):
VERSION_HELP = "Show DVC version and system/environment information."
def _get_dvc_repo_info(repo):
if repo.config.get("core", {}).get("no_scm", False):
return "dvc (no_scm)"
if repo.root_dir != repo.scm.root_dir:
return "dvc (subdir), git"
return "dvc, git"
def add_parser(subparsers, parent_parser):
VERSION_HELP = (
"Display the DVC version and system/environment information."
)
version_parser = subparsers.add_parser(
"version",
parents=[parent_parser],
-42
View File
@@ -1,42 +0,0 @@
"""Python version compatibility code"""
# Backport os.fspath() from Python 3.6
try:
from os import fspath # noqa: F821
fspath_py35 = lambda s: s # noqa: E731
except ImportError:
def fspath(path):
"""Return the path representation of a path-like object.
If str or bytes is passed in, it is returned unchanged. Otherwise the
os.PathLike interface is used to get the path representation. If the
path representation is not str or bytes, TypeError is raised. If the
provided path is not str, bytes, or os.PathLike, TypeError is raised.
"""
if isinstance(path, (str, bytes)):
return path
# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path_repr = path_type.__fspath__(path)
except AttributeError:
if hasattr(path_type, "__fspath__"):
raise
else:
raise TypeError(
"expected str, bytes or os.PathLike object, "
"not " + path_type.__name__
)
if isinstance(path_repr, (str, bytes)):
return path_repr
else:
raise TypeError(
"expected {}.__fspath__() to return str or bytes, "
"not {}".format(path_type.__name__, type(path_repr).__name__)
)
fspath_py35 = fspath
+55 -23
View File
@@ -1,16 +1,27 @@
"""DVC config objects."""
from contextlib import contextmanager
import logging
import os
import re
from contextlib import contextmanager
from functools import partial
from urllib.parse import urlparse
from funcy import cached_property, re_find, walk_values, compact
import configobj
from voluptuous import Schema, Optional, Invalid, ALLOW_EXTRA
from voluptuous import All, Any, Lower, Range, Coerce
from funcy import cached_property, compact, re_find, walk_values
from voluptuous import (
ALLOW_EXTRA,
All,
Any,
Coerce,
Invalid,
Lower,
Optional,
Range,
Schema,
)
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc.path_info import PathInfo
from dvc.utils import relpath
logger = logging.getLogger(__name__)
@@ -20,7 +31,7 @@ class ConfigError(DvcException):
"""DVC config exception."""
def __init__(self, msg):
super().__init__("config file error: {}".format(msg))
super().__init__(f"config file error: {msg}")
class NoRemoteError(ConfigError):
@@ -78,7 +89,7 @@ def ByUrl(mapping):
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.netloc == "":
return schemas[""](data)
if parsed.scheme not in schemas:
raise Invalid("Unsupported URL type {}://".format(parsed.scheme))
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
return schemas[parsed.scheme](data)
@@ -92,7 +103,7 @@ class RelPath(str):
REMOTE_COMMON = {
"url": str,
"checksum_jobs": All(Coerce(int), Range(1)),
"no_traverse": Bool,
Optional("no_traverse"): Bool, # obsoleted
"verify": Bool,
}
LOCAL_COMMON = {
@@ -140,6 +151,7 @@ SCHEMA = {
Optional("listobjects", default=False): Bool,
Optional("use_ssl", default=True): Bool,
"sse": str,
"sse_kms_key_id": str,
"acl": str,
"grant_read": str,
"grant_read_acp": str,
@@ -179,6 +191,7 @@ SCHEMA = {
"gdrive_service_account_email": str,
"gdrive_service_account_user_email": str,
"gdrive_service_account_p12_file_path": str,
Optional("gdrive_trash_only", default=False): Bool,
**REMOTE_COMMON,
},
"http": {**HTTP_COMMON, **REMOTE_COMMON},
@@ -277,10 +290,7 @@ class Config(dict):
Raises:
ConfigError: thrown if config has an invalid format.
"""
conf = {}
for level in self.LEVELS:
if level in self.files:
_merge(conf, self.load_one(level))
conf = self.load_config_to_level()
if validate:
conf = self.validate(conf)
@@ -296,7 +306,7 @@ class Config(dict):
conf = _load_config(self.files[level])
conf = self._load_paths(conf, self.files[level])
# Autovivify sections
# Auto-verify sections
for key in COMPILED_SCHEMA.schema:
conf.setdefault(key, {})
@@ -313,25 +323,42 @@ class Config(dict):
return Config._map_dirs(conf, resolve)
@staticmethod
def _to_relpath(conf_dir, path):
if re.match(r"\w+://", path):
return path
if isinstance(path, RelPath) or not os.path.isabs(path):
path = relpath(path, conf_dir)
return PathInfo(path).as_posix()
@staticmethod
def _save_paths(conf, filename):
conf_dir = os.path.dirname(filename)
def rel(path):
if re.match(r"\w+://", path):
return path
if isinstance(path, RelPath) or not os.path.isabs(path):
return relpath(path, conf_dir)
return path
rel = partial(Config._to_relpath, conf_dir)
return Config._map_dirs(conf, rel)
@staticmethod
def _map_dirs(conf, func):
dirs_schema = {"cache": {"dir": func}, "remote": {str: {"url": func}}}
dirs_schema = {
"cache": {"dir": func},
"remote": {
str: {"url": func, "gdrive_user_credentials_file": func}
},
}
return Schema(dirs_schema, extra=ALLOW_EXTRA)(conf)
def load_config_to_level(self, level=None):
merged_conf = {}
for merge_level in self.LEVELS:
if merge_level == level:
break
if merge_level in self.files:
_merge(merged_conf, self.load_one(merge_level))
return merged_conf
@contextmanager
def edit(self, level="repo"):
if level in {"repo", "local"} and self.dvc_dir is None:
@@ -341,6 +368,11 @@ class Config(dict):
yield conf
conf = self._save_paths(conf, self.files[level])
merged_conf = self.load_config_to_level(level)
_merge(merged_conf, conf)
self.validate(merged_conf)
_save_config(self.files[level], conf)
self.load()
@@ -358,7 +390,7 @@ def _load_config(filename):
def _save_config(filename, conf_dict):
logger.debug("Writing '{}'.".format(filename))
logger.debug(f"Writing '{filename}'.")
os.makedirs(os.path.dirname(filename), exist_ok=True)
config = configobj.ConfigObj(_pack_remotes(conf_dict))
@@ -385,7 +417,7 @@ def _pack_remotes(conf):
# Transform remote.name -> 'remote "name"'
for name, val in conf["remote"].items():
result['remote "{}"'.format(name)] = val
result[f'remote "{name}"'] = val
result.pop("remote", None)
return result
+3 -6
View File
@@ -7,9 +7,7 @@ import sys
from subprocess import Popen
from dvc.env import DVC_DAEMON
from dvc.utils import fix_env
from dvc.utils import is_binary
from dvc.utils import fix_env, is_binary
logger = logging.getLogger(__name__)
@@ -48,7 +46,6 @@ def _spawn_posix(cmd, env):
os._exit(1) # pylint: disable=protected-access
os.setsid()
os.umask(0)
try:
pid = os.fork()
@@ -68,7 +65,7 @@ def _spawn_posix(cmd, env):
def _spawn(cmd, env):
logger.debug("Trying to spawn '{}'".format(cmd))
logger.debug(f"Trying to spawn '{cmd}'")
if os.name == "nt":
_spawn_windows(cmd, env)
@@ -77,7 +74,7 @@ def _spawn(cmd, env):
else:
raise NotImplementedError
logger.debug("Spawned '{}'".format(cmd))
logger.debug(f"Spawned '{cmd}'")
def daemon(args):
+12 -16
View File
@@ -6,12 +6,9 @@ import os
import pydoc
import sys
from grandalf.graphs import Edge
from grandalf.graphs import Graph
from grandalf.graphs import Vertex
from grandalf.graphs import Edge, Graph, Vertex
from grandalf.layouts import SugiyamaLayout
from grandalf.routing import EdgeViewer
from grandalf.routing import route_with_lines
from grandalf.routing import EdgeViewer, route_with_lines
from dvc.env import DVC_PAGER
from dvc.utils import format_link
@@ -40,7 +37,7 @@ def find_pager():
if env_pager:
return make_pager(env_pager)
if os.system("({}) 2>{}".format(DEFAULT_PAGER, os.devnull)) == 0:
if os.system(f"({DEFAULT_PAGER}) 2>{os.devnull}") == 0:
return make_pager(DEFAULT_PAGER_FORMATTED)
logger.warning(
@@ -52,7 +49,7 @@ def find_pager():
return pydoc.plainpager
class VertexViewer(object):
class VertexViewer:
"""Class to define vertex box boundaries that will be accounted for during
graph building by grandalf.
@@ -78,7 +75,7 @@ class VertexViewer(object):
return self._w
class AsciiCanvas(object):
class AsciiCanvas:
"""Class for drawing in ASCII.
Args:
@@ -95,14 +92,13 @@ class AsciiCanvas(object):
self.cols = cols
self.lines = lines
self.canvas = [[" "] * cols for l in range(lines)]
self.canvas = [[" "] * cols for line in range(lines)]
def draw(self):
"""Draws ASCII canvas on the screen."""
pager = find_pager()
lines = map("".join, self.canvas)
joined_lines = os.linesep.join(lines)
pager(joined_lines)
return joined_lines
def point(self, x, y, char):
"""Create a point on ASCII canvas.
@@ -148,21 +144,21 @@ class AsciiCanvas(object):
if dx == 0:
y = y0
else:
y = y0 + int(round((x - x0) * dy / float((dx))))
y = y0 + int(round((x - x0) * dy / float(dx)))
self.point(x, y, char)
elif y0 < y1:
for y in range(y0, y1 + 1):
if dy == 0:
x = x0
else:
x = x0 + int(round((y - y0) * dx / float((dy))))
x = x0 + int(round((y - y0) * dx / float(dy)))
self.point(x, y, char)
else:
for y in range(y1, y0 + 1):
if dy == 0:
x = x0
else:
x = x1 + int(round((y - y1) * dx / float((dy))))
x = x1 + int(round((y - y1) * dx / float(dy)))
self.point(x, y, char)
def text(self, x, y, text):
@@ -216,7 +212,7 @@ def _build_sugiyama_layout(vertexes, edges):
# Y
#
vertexes = {v: Vertex(" {} ".format(v)) for v in vertexes}
vertexes = {v: Vertex(f" {v} ") for v in vertexes}
# NOTE: reverting edges to correctly orientate the graph
edges = [Edge(vertexes[e], vertexes[s]) for s, e in edges]
vertexes = vertexes.values()
@@ -319,4 +315,4 @@ def draw(vertexes, edges):
int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data
)
canvas.draw()
return canvas.draw()
+29 -25
View File
@@ -3,13 +3,12 @@
import logging
from dvc.config import NoRemoteError
from dvc.remote import Remote
from dvc.remote import get_remote
logger = logging.getLogger(__name__)
class DataCloud(object):
class DataCloud:
"""Class that manages dvc remotes.
Args:
@@ -23,12 +22,12 @@ class DataCloud(object):
def __init__(self, repo):
self.repo = repo
def get_remote(self, remote=None, command="<command>"):
if not remote:
remote = self.repo.config["core"].get("remote")
def get_remote(self, name=None, command="<command>"):
if not name:
name = self.repo.config["core"].get("remote")
if remote:
return self._init_remote(remote)
if name:
return self._init_remote(name)
if bool(self.repo.config["remote"]):
error_msg = (
@@ -45,55 +44,60 @@ class DataCloud(object):
raise NoRemoteError(error_msg)
def _init_remote(self, remote):
return Remote(self.repo, name=remote)
def _init_remote(self, name):
return get_remote(self.repo, name=name)
def push(self, cache, jobs=None, remote=None, show_checksums=False):
def push(
self, cache, jobs=None, remote=None, show_checksums=False,
):
"""Push data items in a cloud-agnostic way.
Args:
cache (NamedCache): named checksums to push to the cloud.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.RemoteBASE): optional remote to push to.
remote (dvc.remote.base.BaseRemote): 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
information messages.
"""
remote = self.get_remote(remote, "push")
return self.repo.cache.local.push(
cache,
jobs=jobs,
remote=self.get_remote(remote, "push"),
show_checksums=show_checksums,
cache, jobs=jobs, remote=remote, show_checksums=show_checksums,
)
def pull(self, cache, jobs=None, remote=None, show_checksums=False):
def pull(
self, cache, jobs=None, remote=None, show_checksums=False,
):
"""Pull data items in a cloud-agnostic way.
Args:
cache (NamedCache): named checksums to pull from the cloud.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.RemoteBASE): optional remote to pull from.
remote (dvc.remote.base.BaseRemote): 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
information messages.
"""
remote = self.get_remote(remote, "pull")
downloaded_items_num = self.repo.cache.local.pull(
cache, jobs=jobs, remote=remote, show_checksums=show_checksums
)
if not remote.verify:
if not remote.tree.verify:
self._save_pulled_checksums(cache)
return downloaded_items_num
def _save_pulled_checksums(self, cache):
for checksum in cache["local"].keys():
for checksum in cache.scheme_keys("local"):
cache_file = self.repo.cache.local.checksum_to_path_info(checksum)
if self.repo.cache.local.exists(cache_file):
# We can safely save here, as existing corrupted files will be
# removed upon status, while files corrupted during download
# will not be moved from tmp_file (see `RemoteBASE.download()`)
if self.repo.cache.local.tree.exists(cache_file):
# We can safely save here, as existing corrupted files will
# be removed upon status, while files corrupted during
# download will not be moved from tmp_file
# (see `RemoteBASE.download()`)
self.repo.state.save(cache_file, checksum)
def status(self, cache, jobs=None, remote=None, show_checksums=False):
@@ -102,7 +106,7 @@ class DataCloud(object):
Args:
cache (NamedCache): named checksums to check status for.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.RemoteBASE): optional remote to compare
remote (dvc.remote.base.BaseRemote): 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
+75 -37
View File
@@ -1,37 +1,42 @@
from collections import defaultdict
from urllib.parse import urlparse
import dvc.output as output
from dvc.dependency.gs import DependencyGS
from dvc.dependency.hdfs import DependencyHDFS
from dvc.dependency.http import DependencyHTTP
from dvc.dependency.https import DependencyHTTPS
from dvc.dependency.local import DependencyLOCAL
from dvc.dependency.s3 import DependencyS3
from dvc.dependency.ssh import DependencySSH
from dvc.output.base import OutputBase
from dvc.remote import Remote
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.output.base import BaseOutput
from dvc.remote import get_remote
from dvc.scheme import Schemes
from .repo import DependencyREPO
from .repo import RepoDependency
DEPS = [
DependencyGS,
DependencyHDFS,
DependencyHTTP,
DependencyHTTPS,
DependencyS3,
DependencySSH,
# NOTE: DependencyLOCAL is the default choice
AzureDependency,
GSDependency,
HDFSDependency,
HTTPDependency,
HTTPSDependency,
S3Dependency,
SSHDependency,
# NOTE: LocalDependency is the default choice
]
DEP_MAP = {
Schemes.LOCAL: DependencyLOCAL,
Schemes.SSH: DependencySSH,
Schemes.S3: DependencyS3,
Schemes.GS: DependencyGS,
Schemes.HDFS: DependencyHDFS,
Schemes.HTTP: DependencyHTTP,
Schemes.HTTPS: DependencyHTTPS,
Schemes.LOCAL: LocalDependency,
Schemes.SSH: SSHDependency,
Schemes.S3: S3Dependency,
Schemes.AZURE: AzureDependency,
Schemes.GS: GSDependency,
Schemes.HDFS: HDFSDependency,
Schemes.HTTP: HTTPDependency,
Schemes.HTTPS: HTTPSDependency,
}
@@ -40,39 +45,72 @@ DEP_MAP = {
# cached, see -o and -O flags for `dvc run`) and 'metric' (whether or not
# output is a metric file and how to parse it, see `-M` flag for `dvc run`).
SCHEMA = output.SCHEMA.copy()
del SCHEMA[OutputBase.PARAM_CACHE]
del SCHEMA[OutputBase.PARAM_METRIC]
SCHEMA[DependencyREPO.PARAM_REPO] = DependencyREPO.REPO_SCHEMA
del SCHEMA[BaseOutput.PARAM_CACHE]
del SCHEMA[BaseOutput.PARAM_METRIC]
SCHEMA.update(RepoDependency.REPO_SCHEMA)
SCHEMA.update(ParamsDependency.PARAM_SCHEMA)
def _get(stage, p, info):
parsed = urlparse(p)
if parsed.scheme == "remote":
remote = Remote(stage.repo, name=parsed.netloc)
parsed = urlparse(p) if p else None
if parsed and parsed.scheme == "remote":
remote = get_remote(stage.repo, name=parsed.netloc)
return DEP_MAP[remote.scheme](stage, p, info, remote=remote)
if info and info.get(DependencyREPO.PARAM_REPO):
repo = info.pop(DependencyREPO.PARAM_REPO)
return DependencyREPO(repo, stage, p, info)
if info and info.get(RepoDependency.PARAM_REPO):
repo = info.pop(RepoDependency.PARAM_REPO)
return RepoDependency(repo, stage, p, info)
if info and info.get(ParamsDependency.PARAM_PARAMS):
params = info.pop(ParamsDependency.PARAM_PARAMS)
return ParamsDependency(stage, p, params)
for d in DEPS:
if d.supported(p):
return d(stage, p, info)
return DependencyLOCAL(stage, p, info)
return LocalDependency(stage, p, info)
def loadd_from(stage, d_list):
ret = []
for d in d_list:
p = d.pop(OutputBase.PARAM_PATH)
p = d.pop(BaseOutput.PARAM_PATH, None)
ret.append(_get(stage, p, d))
return ret
def loads_from(stage, s_list, erepo=None):
assert isinstance(s_list, list)
ret = []
for s in s_list:
info = {DependencyREPO.PARAM_REPO: erepo} if erepo else {}
info = {RepoDependency.PARAM_REPO: erepo} if erepo else {}
ret.append(_get(stage, s, info))
return ret
def _merge_params(s_list):
d = defaultdict(list)
default_file = ParamsDependency.DEFAULT_PARAMS_FILE
for key in s_list:
if isinstance(key, str):
d[default_file].append(key)
continue
if not isinstance(key, dict):
msg = "Only list of str/dict is supported. Got: "
msg += f"'{type(key).__name__}'."
raise ValueError(msg)
for k, params in key.items():
if not isinstance(params, list):
msg = "Expected list of params for custom params file "
msg += f"'{k}', got '{type(params).__name__}'."
raise ValueError(msg)
d[k].extend(params)
return d
def loads_params(stage, s_list):
d = _merge_params(s_list)
return [
ParamsDependency(stage, path, params) for path, params in d.items()
]
+7
View File
@@ -0,0 +1,7 @@
from dvc.dependency.base import BaseDependency
from dvc.output.base import BaseOutput
from dvc.remote.azure import AzureRemoteTree
class AzureDependency(BaseDependency, BaseOutput):
TREE_CLS = AzureRemoteTree
+4 -6
View File
@@ -3,24 +3,22 @@ from dvc.exceptions import DvcException
class DependencyDoesNotExistError(DvcException):
def __init__(self, path):
msg = "dependency '{}' does not exist".format(path)
msg = f"dependency '{path}' does not exist"
super().__init__(msg)
class DependencyIsNotFileOrDirError(DvcException):
def __init__(self, path):
msg = "dependency '{}' is not a file or directory".format(path)
msg = f"dependency '{path}' is not a file or directory"
super().__init__(msg)
class DependencyIsStageFileError(DvcException):
def __init__(self, path):
super().__init__(
"Stage file '{}' cannot be a dependency.".format(path)
)
super().__init__(f"Stage file '{path}' cannot be a dependency.")
class DependencyBase(object):
class BaseDependency:
IS_DEPENDENCY = True
DoesNotExistError = DependencyDoesNotExistError
+3 -3
View File
@@ -1,6 +1,6 @@
from dvc.dependency.base import DependencyBase
from dvc.output.gs import OutputGS
from dvc.dependency.base import BaseDependency
from dvc.output.gs import GSOutput
class DependencyGS(DependencyBase, OutputGS):
class GSDependency(BaseDependency, GSOutput):
pass
+3 -3
View File
@@ -1,6 +1,6 @@
from dvc.dependency.base import DependencyBase
from dvc.output.hdfs import OutputHDFS
from dvc.dependency.base import BaseDependency
from dvc.output.hdfs import HDFSOutput
class DependencyHDFS(DependencyBase, OutputHDFS):
class HDFSDependency(BaseDependency, HDFSOutput):
pass
+5 -5
View File
@@ -1,7 +1,7 @@
from dvc.dependency.base import DependencyBase
from dvc.output.base import OutputBase
from dvc.remote.http import RemoteHTTP
from dvc.dependency.base import BaseDependency
from dvc.output.base import BaseOutput
from dvc.remote.http import HTTPRemoteTree
class DependencyHTTP(DependencyBase, OutputBase):
REMOTE = RemoteHTTP
class HTTPDependency(BaseDependency, BaseOutput):
TREE_CLS = HTTPRemoteTree
+5 -4
View File
@@ -1,6 +1,7 @@
from .http import DependencyHTTP
from dvc.remote.https import RemoteHTTPS
from dvc.remote.https import HTTPSRemoteTree
from .http import HTTPDependency
class DependencyHTTPS(DependencyHTTP):
REMOTE = RemoteHTTPS
class HTTPSDependency(HTTPDependency):
TREE_CLS = HTTPSRemoteTree
+3 -3
View File
@@ -1,6 +1,6 @@
from dvc.dependency.base import DependencyBase
from dvc.output.local import OutputLOCAL
from dvc.dependency.base import BaseDependency
from dvc.output.local import LocalOutput
class DependencyLOCAL(DependencyBase, OutputLOCAL):
class LocalDependency(BaseDependency, LocalOutput):
pass
+116
View File
@@ -0,0 +1,116 @@
import os
from collections import defaultdict
import dpath.util
import yaml
from voluptuous import Any
from dvc.dependency.local import LocalDependency
from dvc.exceptions import DvcException
class MissingParamsError(DvcException):
pass
class BadParamFileError(DvcException):
pass
class ParamsDependency(LocalDependency):
PARAM_PARAMS = "params"
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
DEFAULT_PARAMS_FILE = "params.yaml"
def __init__(self, stage, path, params):
info = {}
self.params = []
if params:
if isinstance(params, list):
self.params = params
else:
assert isinstance(params, dict)
self.params = list(params.keys())
info = params
super().__init__(
stage,
path
or os.path.join(stage.repo.root_dir, self.DEFAULT_PARAMS_FILE),
info=info,
)
def fill_values(self, values=None):
"""Load params values dynamically."""
if not values:
return
for param in self.params:
value = values.get(param)
if value:
self.info[param] = value
def save(self):
super().save()
self.info = self.save_info()
def status(self):
status = super().status()
if status[str(self)] == "deleted":
return status
status = defaultdict(dict)
info = self.read_params()
for param in self.params:
if param not in info.keys():
st = "deleted"
elif param not in self.info:
st = "new"
elif info[param] != self.info[param]:
st = "modified"
else:
assert info[param] == self.info[param]
continue
status[str(self)][param] = st
return status
def dumpd(self):
return {
self.PARAM_PATH: self.def_path,
self.PARAM_PARAMS: self.info or self.params,
}
def read_params(self):
if not self.exists:
return {}
with self.repo.tree.open(self.path_info, "r") as fobj:
try:
config = yaml.safe_load(fobj)
except yaml.YAMLError as exc:
raise BadParamFileError(
f"Unable to read parameters from '{self}'"
) from exc
ret = {}
for param in self.params:
try:
ret[param] = dpath.util.get(config, param, separator=".")
except KeyError:
pass
return ret
def save_info(self):
info = self.read_params()
missing_params = set(self.params) - set(info.keys())
if missing_params:
raise MissingParamsError(
"Parameters '{}' are missing from '{}'.".format(
", ".join(missing_params), self,
)
)
return info
+24 -12
View File
@@ -2,21 +2,24 @@ import os
from voluptuous import Required
from .local import DependencyLOCAL
from dvc.exceptions import OutputNotFoundError
from dvc.path_info import PathInfo
from .local import LocalDependency
class DependencyREPO(DependencyLOCAL):
class RepoDependency(LocalDependency):
PARAM_REPO = "repo"
PARAM_URL = "url"
PARAM_REV = "rev"
PARAM_REV_LOCK = "rev_lock"
REPO_SCHEMA = {
Required(PARAM_URL): str,
PARAM_REV: str,
PARAM_REV_LOCK: str,
PARAM_REPO: {
Required(PARAM_URL): str,
PARAM_REV: str,
PARAM_REV_LOCK: str,
}
}
def __init__(self, def_repo, stage, *args, **kwargs):
@@ -47,13 +50,22 @@ class DependencyREPO(DependencyLOCAL):
return external_repo(d["url"], rev=rev)
def _get_checksum(self, locked=True):
from dvc.repo.tree import RepoTree
with self._make_repo(locked=locked) as repo:
try:
return repo.find_out_by_relpath(self.def_path).info["md5"]
except OutputNotFoundError:
path = PathInfo(os.path.join(repo.root_dir, self.def_path))
# we want stream but not fetch, so DVC out directories are
# walked, but dir contents is not fetched
tree = RepoTree(repo, stream=True)
# We are polluting our repo cache with some dir listing here
return self.repo.cache.local.get_checksum(path)
if tree.isdir(path):
return self.repo.cache.local.get_checksum(path, tree)
return tree.get_file_checksum(path)
def status(self):
current_checksum = self._get_checksum(locked=True)
@@ -73,16 +85,16 @@ class DependencyREPO(DependencyLOCAL):
def download(self, to):
with self._make_repo() as repo:
if self.def_repo.get(self.PARAM_REV_LOCK) is None:
self.def_repo[self.PARAM_REV_LOCK] = repo.scm.get_rev()
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
if hasattr(repo, "cache"):
repo.cache.local.cache_dir = self.repo.cache.local.cache_dir
repo.pull_to(self.def_path, to.path_info)
cache = self.repo.cache.local
with repo.use_cache(cache):
_, _, cache_infos = repo.fetch_external([self.def_path])
cache.checkout(to.path_info, cache_infos[0])
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.scm.get_rev()
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
+3 -3
View File
@@ -1,6 +1,6 @@
from dvc.dependency.base import DependencyBase
from dvc.output.s3 import OutputS3
from dvc.dependency.base import BaseDependency
from dvc.output.s3 import S3Output
class DependencyS3(DependencyBase, OutputS3):
class S3Dependency(BaseDependency, S3Output):
pass
+3 -3
View File
@@ -1,6 +1,6 @@
from dvc.dependency.base import DependencyBase
from dvc.output.ssh import OutputSSH
from dvc.dependency.base import BaseDependency
from dvc.output.ssh import SSHOutput
class DependencySSH(DependencyBase, OutputSSH):
class SSHDependency(BaseDependency, SSHOutput):
pass
+301
View File
@@ -0,0 +1,301 @@
import contextlib
import logging
import os
from voluptuous import MultipleInvalid
from dvc.exceptions import DvcException
from dvc.stage import serialize
from dvc.stage.exceptions import (
StageFileBadNameError,
StageFileDoesNotExistError,
StageFileFormatError,
StageFileIsNotDvcFileError,
)
from dvc.stage.loader import SingleStageLoader, StageLoader
from dvc.utils import relpath
from dvc.utils.collections import apply_diff
from dvc.utils.yaml import dump_yaml, parse_yaml, parse_yaml_for_update
logger = logging.getLogger(__name__)
DVC_FILE = "Dvcfile"
DVC_FILE_SUFFIX = ".dvc"
PIPELINE_FILE = "dvc.yaml"
PIPELINE_LOCK = "dvc.lock"
class LockfileCorruptedError(DvcException):
pass
def is_valid_filename(path):
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [
DVC_FILE,
PIPELINE_FILE,
]
def is_dvc_file(path):
return os.path.isfile(path) and (
is_valid_filename(path) or os.path.basename(path) == PIPELINE_LOCK
)
def check_dvc_filename(path):
if not is_valid_filename(path):
raise StageFileBadNameError(
"bad DVC-file name '{}'. DVC-files should be named "
"'Dvcfile' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
relpath(path), os.path.basename(path)
)
)
class FileMixin:
SCHEMA = None
def __init__(self, repo, path, **kwargs):
self.repo = repo
self.path = path
def __repr__(self):
return "{}: {}".format(
self.__class__.__name__, relpath(self.path, self.repo.root_dir)
)
def __hash__(self):
return hash(self.path)
def __eq__(self, other):
return self.repo == other.repo and os.path.abspath(
self.path
) == os.path.abspath(other.path)
def __str__(self):
return f"{self.__class__.__name__}: {self.relpath}"
@property
def relpath(self):
return relpath(self.path)
def exists(self):
return self.repo.tree.exists(self.path)
def _load(self):
# it raises the proper exceptions by priority:
# 1. when the file doesn't exists
# 2. filename is not a DVC-file
# 3. path doesn't represent a regular file
if not self.exists():
raise StageFileDoesNotExistError(self.path)
check_dvc_filename(self.path)
if not self.repo.tree.isfile(self.path):
raise StageFileIsNotDvcFileError(self.path)
with self.repo.tree.open(self.path) as fd:
stage_text = fd.read()
d = parse_yaml(stage_text, self.path)
self.validate(d, self.relpath)
return d, stage_text
@classmethod
def validate(cls, d, fname=None):
assert cls.SCHEMA
try:
cls.SCHEMA(d)
except MultipleInvalid as exc:
raise StageFileFormatError(f"'{fname}' format error: {exc}")
def remove(self, force=False):
with contextlib.suppress(FileNotFoundError):
os.unlink(self.path)
def dump(self, stage, **kwargs):
raise NotImplementedError
class SingleStageFile(FileMixin):
from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA
def __init__(self, repo, path):
super().__init__(repo, path)
@property
def stage(self):
data, raw = self._load()
return SingleStageLoader.load_stage(self, data, raw)
@property
def stages(self):
data, raw = self._load()
return SingleStageLoader(self, data, raw)
def dump(self, stage, **kwargs):
"""Dumps given stage appropriately in the dvcfile."""
from dvc.stage import PipelineStage
assert not isinstance(stage, PipelineStage)
check_dvc_filename(self.path)
logger.debug(
"Saving information to '{file}'.".format(file=relpath(self.path))
)
dump_yaml(self.path, serialize.to_single_stage_file(stage))
self.repo.scm.track_file(self.relpath)
def remove_stage(self, stage):
self.remove()
class PipelineFile(FileMixin):
"""Abstraction for pipelines file, .yaml + .lock combined."""
from dvc.schema import COMPILED_MULTI_STAGE_SCHEMA as SCHEMA
@property
def _lockfile(self):
return Lockfile(self.repo, os.path.splitext(self.path)[0] + ".lock")
def dump(self, stage, update_pipeline=False, no_lock=False):
"""Dumps given stage appropriately in the dvcfile."""
from dvc.stage import PipelineStage
assert isinstance(stage, PipelineStage)
check_dvc_filename(self.path)
if update_pipeline and not stage.is_data_source:
self._dump_pipeline_file(stage)
if not no_lock:
self._dump_lockfile(stage)
def _dump_lockfile(self, stage):
self._lockfile.dump(stage)
def _dump_pipeline_file(self, stage):
data = {}
if self.exists():
with open(self.path) as fd:
data = parse_yaml_for_update(fd.read(), self.path)
else:
logger.info("Creating '%s'", self.relpath)
open(self.path, "w+").close()
data["stages"] = data.get("stages", {})
stage_data = serialize.to_pipeline_file(stage)
existing_entry = stage.name in data["stages"]
action = "Modifying" if existing_entry else "Adding"
logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
if existing_entry:
orig_stage_data = data["stages"][stage.name]
if "meta" in orig_stage_data:
stage_data[stage.name]["meta"] = orig_stage_data["meta"]
apply_diff(stage_data[stage.name], orig_stage_data)
else:
data["stages"].update(stage_data)
dump_yaml(self.path, data)
self.repo.scm.track_file(self.relpath)
@property
def stage(self):
raise DvcException(
"PipelineFile has multiple stages. Please specify it's name."
)
@property
def stages(self):
data, _ = self._load()
lockfile_data = self._lockfile.load()
return StageLoader(self, data.get("stages", {}), lockfile_data)
def remove(self, force=False):
if not force:
logger.warning("Cannot remove pipeline file.")
return
super().remove()
self._lockfile.remove()
def remove_stage(self, stage):
self._lockfile.remove_stage(stage)
if not self.exists():
return
with open(self.path, "r") as f:
d = parse_yaml_for_update(f.read(), self.path)
self.validate(d, self.path)
if stage.name not in d.get("stages", {}):
return
logger.debug("Removing '%s' from '%s'", stage.name, self.path)
del d["stages"][stage.name]
dump_yaml(self.path, d)
class Lockfile(FileMixin):
from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA
def load(self):
if not self.exists():
return {}
with self.repo.tree.open(self.path) as fd:
data = parse_yaml(fd.read(), self.path)
try:
self.validate(data, fname=self.relpath)
except StageFileFormatError:
raise LockfileCorruptedError(
f"Lockfile '{self.relpath}' is corrupted."
)
return data
def dump(self, stage, **kwargs):
stage_data = serialize.to_lockfile(stage)
if not self.exists():
modified = True
logger.info("Generating lock file '%s'", self.relpath)
data = stage_data
open(self.path, "w+").close()
else:
with self.repo.tree.open(self.path, "r") as fd:
data = parse_yaml_for_update(fd.read(), self.path)
modified = data.get(stage.name, {}) != stage_data.get(
stage.name, {}
)
if modified:
logger.info("Updating lock file '%s'", self.relpath)
data.update(stage_data)
dump_yaml(self.path, data)
if modified:
self.repo.scm.track_file(self.relpath)
def remove_stage(self, stage):
if not self.exists():
return
with open(self.path) as f:
d = parse_yaml_for_update(f.read(), self.path)
self.validate(d, self.path)
if stage.name not in d:
return
logger.debug("Removing '%s' from '%s'", stage.name, self.path)
del d[stage.name]
dump_yaml(self.path, d)
class Dvcfile:
def __new__(cls, repo, path, **kwargs):
assert path
assert repo
_, ext = os.path.splitext(path)
if ext in [".yaml", ".yml"]:
return PipelineFile(repo, path, **kwargs)
# fallback to single stage file for better error messages
return SingleStageFile(repo, path, **kwargs)
+61 -46
View File
@@ -1,11 +1,16 @@
"""Exceptions raised by the dvc."""
from funcy import first
from dvc.utils import relpath, format_link
from dvc.utils import format_link, relpath
class DvcException(Exception):
"""Base class for all dvc exceptions."""
def __init__(self, msg, *args):
assert msg
super().__init__(msg, *args)
class InvalidArgumentError(ValueError, DvcException):
"""Thrown if arguments are invalid."""
@@ -23,12 +28,17 @@ class OutputDuplicationError(DvcException):
def __init__(self, output, stages):
assert isinstance(output, str)
assert all(hasattr(stage, "relpath") for stage in stages)
msg = (
"file/directory '{}' is specified as an output in more than one "
"stage: {}\n"
"This is not allowed. Consider using a different output name."
).format(output, "\n ".join(s.relpath for s in stages))
if len(stages) == 1:
msg = "output '{}' is already specified in {}.".format(
output, first(stages)
)
else:
msg = "output '{}' is already specified in stages:\n{}".format(
output, "\n".join(f"\t- {s.addressing}" for s in stages),
)
super().__init__(msg)
self.stages = stages
self.output = output
class OutputNotFoundError(DvcException):
@@ -60,8 +70,8 @@ class StagePathAsOutputError(DvcException):
def __init__(self, stage, output):
assert isinstance(output, str)
super().__init__(
"'{stage}' is within an output '{output}' of another stage".format(
stage=stage.relpath, output=output
"{stage} is within an output '{output}' of another stage".format(
stage=stage, output=output
)
)
@@ -77,10 +87,7 @@ class CircularDependencyError(DvcException):
def __init__(self, dependency):
assert isinstance(dependency, str)
msg = (
"file/directory '{}' is specified as an output and as a "
"dependency."
)
msg = "'{}' is specified as an output and as a dependency."
super().__init__(msg.format(dependency))
@@ -94,7 +101,7 @@ class ArgumentDuplicationError(DvcException):
def __init__(self, path):
assert isinstance(path, str)
super().__init__("file '{}' is specified more than once.".format(path))
super().__init__(f"file '{path}' is specified more than once.")
class MoveNotDataSourceError(DvcException):
@@ -129,12 +136,8 @@ class DvcParserError(DvcException):
class CyclicGraphError(DvcException):
def __init__(self, stages):
assert isinstance(stages, list)
stages = "\n".join("\t- {}".format(stage.relpath) for stage in stages)
msg = (
"you've introduced a cycle in your pipeline that involves "
"the following stages:"
"\n"
"{stages}".format(stages=stages)
msg = "Pipeline has a cycle involving: {}.".format(
", ".join(s.addressing for s in stages)
)
super().__init__(msg)
@@ -142,7 +145,7 @@ class CyclicGraphError(DvcException):
class ConfirmRemoveError(DvcException):
def __init__(self, path):
super().__init__(
"unable to remove '{}' without a confirmation from the user. Use "
"unable to remove '{}' without a confirmation. Use "
"`-f` to force.".format(path)
)
@@ -154,7 +157,7 @@ class InitError(DvcException):
class ReproductionError(DvcException):
def __init__(self, dvc_file_name):
self.path = dvc_file_name
super().__init__("failed to reproduce '{}'".format(dvc_file_name))
super().__init__(f"failed to reproduce '{dvc_file_name}'")
class BadMetricError(DvcException):
@@ -162,7 +165,7 @@ class BadMetricError(DvcException):
super().__init__(
"the following metrics do not exist, "
"are not metric files or are malformed: {paths}".format(
paths=", ".join("'{}'".format(path) for path in paths)
paths=", ".join(f"'{path}'" for path in paths)
)
)
@@ -170,17 +173,24 @@ class BadMetricError(DvcException):
class NoMetricsError(DvcException):
def __init__(self):
super().__init__(
"no metric files in this repository. "
"Use `dvc metrics add` to add a metric file to track."
"no metric files in this repository. Use `-m/-M` options for "
"`dvc run` to mark stage outputs as metrics."
)
class StageFileCorruptedError(DvcException):
class NoPlotsError(DvcException):
def __init__(self):
super().__init__(
"no plots in this repository. Use `--plots/--plots-no-cache` "
"options for `dvc run` to mark stage outputs as plots."
)
class YAMLFileCorruptedError(DvcException):
def __init__(self, path):
path = relpath(path)
super().__init__(
"unable to read DVC-file: {} "
"YAML file structure is corrupted".format(path)
f"unable to read: '{path}', YAML file structure is corrupted"
)
@@ -200,7 +210,7 @@ class OverlappingOutputPathsError(DvcException):
class CheckoutErrorSuggestGit(DvcException):
def __init__(self, target):
super().__init__("Did you mean `git checkout {}`?".format(target))
super().__init__(f"Did you mean `git checkout {target}`?")
class ETagMismatchError(DvcException):
@@ -212,10 +222,11 @@ class ETagMismatchError(DvcException):
class FileMissingError(DvcException):
def __init__(self, path):
def __init__(self, path, hint=None):
self.path = path
hint = "" if hint is None else f". {hint}"
super().__init__(
"Can't find '{}' neither locally nor on remote".format(path)
f"Can't find '{path}' neither locally nor on remote{hint}"
)
@@ -241,18 +252,14 @@ class DownloadError(DvcException):
def __init__(self, amount):
self.amount = amount
super().__init__(
"{amount} files failed to download".format(amount=amount)
)
super().__init__(f"{amount} files failed to download")
class UploadError(DvcException):
def __init__(self, amount):
self.amount = amount
super().__init__(
"{amount} files failed to upload".format(amount=amount)
)
super().__init__(f"{amount} files failed to upload")
class CheckoutError(DvcException):
@@ -274,7 +281,7 @@ class CollectCacheError(DvcException):
class NoRemoteInExternalRepoError(DvcException):
def __init__(self, url):
super().__init__(
"No DVC remote is specified in target repository '{}'.".format(url)
f"No DVC remote is specified in target repository '{url}'."
)
@@ -289,7 +296,7 @@ class NoOutputInExternalRepoError(DvcException):
class HTTPError(DvcException):
def __init__(self, code, reason):
super().__init__("'{} {}'".format(code, reason))
super().__init__(f"'{code} {reason}'")
class PathMissingError(DvcException):
@@ -297,19 +304,15 @@ class PathMissingError(DvcException):
"The path '{}' does not exist in the target repository '{}'"
" neither as a DVC output nor as a Git-tracked file."
)
default_msg_output_only = (
default_msg_dvc_only = (
"The path '{}' does not exist in the target repository '{}'"
" as an DVC output."
)
def __init__(self, path, repo, output_only=False):
msg = (
self.default_msg
if not output_only
else self.default_msg_output_only
)
def __init__(self, path, repo, dvc_only=False):
msg = self.default_msg if not dvc_only else self.default_msg_dvc_only
super().__init__(msg.format(path, repo))
self.output_only = output_only
self.dvc_only = dvc_only
class RemoteCacheRequiredError(DvcException):
@@ -329,3 +332,15 @@ class RemoteCacheRequiredError(DvcException):
class IsADirectoryError(DvcException):
"""Raised when a file operation is requested on a directory."""
class NoOutputOrStageError(DvcException):
"""
Raised when the target is neither an output nor a stage name in dvc.yaml
"""
def __init__(self, target, file):
super().__init__(
f"'{target}' "
f"does not exist as an output or a stage name in '{file}'"
)
+137 -90
View File
@@ -1,23 +1,27 @@
import logging
import os
import tempfile
import threading
from contextlib import contextmanager
from distutils.dir_util import copy_tree
import threading
from typing import Iterable
from funcy import retry, suppress, wrap_with, cached_property
from funcy import cached_property, retry, wrap_with
from dvc.path_info import PathInfo
from dvc.compat import fspath
from dvc.repo import Repo
from dvc.config import NoRemoteError, NotDvcRepoError
from dvc.exceptions import NoRemoteInExternalRepoError, CheckoutError
from dvc.exceptions import OutputNotFoundError, NoOutputInExternalRepoError
from dvc.exceptions import FileMissingError, PathMissingError
from dvc.utils.fs import remove, fs_copy, move
from dvc.utils import tmp_fname
from dvc.exceptions import (
FileMissingError,
NoOutputInExternalRepoError,
NoRemoteInExternalRepoError,
OutputNotFoundError,
PathMissingError,
)
from dvc.path_info import PathInfo
from dvc.repo import Repo
from dvc.repo.tree import RepoTree
from dvc.scm.git import Git
from dvc.scm.tree import is_working_tree
from dvc.utils.fs import remove
logger = logging.getLogger(__name__)
@@ -26,10 +30,12 @@ logger = logging.getLogger(__name__)
def external_repo(url, rev=None, for_write=False):
logger.debug("Creating external repo %s@%s", url, rev)
path = _cached_clone(url, rev, for_write=for_write)
if not rev:
rev = "HEAD"
try:
repo = ExternalRepo(path, url)
repo = ExternalRepo(path, url, rev, for_write=for_write)
except NotDvcRepoError:
repo = ExternalGitRepo(path, url)
repo = ExternalGitRepo(path, url, rev)
try:
yield repo
@@ -61,59 +67,100 @@ def clean_repos():
_remove(path)
class ExternalRepo(Repo):
def __init__(self, root_dir, url):
super().__init__(root_dir)
class BaseExternalRepo:
_local_cache = None
@property
def local_cache(self):
if hasattr(self, "cache"):
return self.cache.local
return self._local_cache
@contextmanager
def use_cache(self, cache):
"""Use the specified cache in place of default tmpdir cache for
download operations.
"""
if hasattr(self, "cache"):
save_cache = self.cache.local
self.cache.local = cache
self._local_cache = cache
yield
if hasattr(self, "cache"):
self.cache.local = save_cache
self._local_cache = None
@cached_property
def repo_tree(self):
return RepoTree(self, fetch=True)
def get_rev(self):
if is_working_tree(self.tree):
return self.scm.get_rev()
if hasattr(self.tree, "tree"):
return self.tree.tree.rev
return self.tree.rev
def fetch_external(self, paths: Iterable, **kwargs):
"""Fetch specified external repo paths into cache.
Returns 3-tuple in the form
(downloaded, failed, list(cache_infos))
where cache_infos can be used as checkout targets for the
fetched paths.
"""
download_results = []
failed = 0
paths = [PathInfo(self.root_dir) / path for path in paths]
def download_update(result):
download_results.append(result)
save_infos = []
for path in paths:
if not self.repo_tree.exists(path):
raise PathMissingError(path, self.url)
save_info = self.local_cache.save(
path,
self.repo_tree,
None,
save_link=False,
download_callback=download_update,
)
save_infos.append(save_info)
return sum(download_results), failed, save_infos
def get_external(self, path, dest):
"""Convenience wrapper for fetch_external and checkout."""
if self.local_cache:
# fetch DVC and git files to tmpdir cache, then checkout
_, _, save_infos = self.fetch_external([path])
self.local_cache.checkout(PathInfo(dest), save_infos[0])
else:
# git-only erepo with no cache, just copy files directly
# to dest
path = PathInfo(self.root_dir) / path
if not self.repo_tree.exists(path):
raise PathMissingError(path, self.url)
self.repo_tree.copytree(path, dest)
class ExternalRepo(Repo, BaseExternalRepo):
def __init__(self, root_dir, url, rev, for_write=False):
if for_write:
super().__init__(root_dir)
else:
root_dir = os.path.realpath(root_dir)
super().__init__(root_dir, scm=Git(root_dir), rev=rev)
self.url = url
self._set_cache_dir()
self._fix_upstream()
def pull_to(self, path, to_info):
"""
Pull the corresponding file or directory specified by `path` and
checkout it into `to_info`.
It works with files tracked by Git and DVC, and also local files
outside the repository.
"""
out = None
path_info = PathInfo(self.root_dir) / path
with suppress(OutputNotFoundError):
(out,) = self.find_outs_by_path(fspath(path_info), strict=False)
try:
if out and out.use_cache:
self._pull_cached(out, path_info, to_info)
return
# Check if it is handled by Git (it can't have an absolute path)
if os.path.isabs(path):
raise FileNotFoundError
fs_copy(fspath(path_info), fspath(to_info))
except FileNotFoundError:
raise PathMissingError(path, self.url)
def _pull_cached(self, out, path_info, dest):
with self.state:
tmp = PathInfo(tmp_fname(dest))
src = tmp / path_info.relative_to(out.path_info)
out.path_info = tmp
# Only pull unless all needed cache is present
if out.changed_cache(filter_info=src):
self.cloud.pull(out.get_used_cache(filter_info=src))
try:
out.checkout(filter_info=src)
except CheckoutError:
raise FileNotFoundError
move(src, dest)
remove(tmp)
@wrap_with(threading.Lock())
def _set_cache_dir(self):
try:
@@ -122,14 +169,23 @@ class ExternalRepo(Repo):
cache_dir = CACHE_DIRS[self.url] = tempfile.mkdtemp("dvc-cache")
self.cache.local.cache_dir = cache_dir
self._local_cache = self.cache.local
def _fix_upstream(self):
if not os.path.isdir(self.url):
return
remote_name = self.config["core"].get("remote")
src_repo = Repo(self.url)
try:
src_repo = Repo(self.url)
except NotDvcRepoError:
# If ExternalRepo does not throw NotDvcRepoError and Repo does,
# the self.url might be a bare git repo.
# NOTE: This will fail to resolve remote with relative path,
# same as if it was a remote DVC repo.
return
try:
remote_name = self.config["core"].get("remote")
if remote_name:
self._fix_local_remote(src_repo, remote_name)
else:
@@ -154,10 +210,11 @@ class ExternalRepo(Repo):
self.config["core"]["remote"] = "auto-generated-upstream"
class ExternalGitRepo:
def __init__(self, root_dir, url):
self.root_dir = root_dir
class ExternalGitRepo(BaseExternalRepo):
def __init__(self, root_dir, url, rev):
self.root_dir = os.path.realpath(root_dir)
self.url = url
self.tree = self.scm.get_tree(rev)
@cached_property
def scm(self):
@@ -170,22 +227,15 @@ class ExternalGitRepo:
def find_out_by_relpath(self, path):
raise OutputNotFoundError(path, self)
def pull_to(self, path, to_info):
try:
# Git handled files can't have absolute path
if os.path.isabs(path):
raise FileNotFoundError
fs_copy(os.path.join(self.root_dir, path), fspath(to_info))
except FileNotFoundError:
raise PathMissingError(path, self.url)
@contextmanager
def open_by_relpath(self, path, mode="r", encoding=None, **kwargs):
"""Opens a specified resource as a file object."""
tree = RepoTree(self)
try:
abs_path = os.path.join(self.root_dir, path)
with open(abs_path, mode, encoding=encoding) as fd:
yield fd
with tree.open(
path, mode=mode, encoding=encoding, **kwargs
) as fobj:
yield fobj
except FileNotFoundError:
raise PathMissingError(path, self.url)
@@ -197,14 +247,12 @@ def _cached_clone(url, rev, for_write=False):
revision checked out. If for_write is set prevents reusing this dir via
cache.
"""
if not for_write and Git.is_sha(rev) and (url, rev) in CLONES:
return CLONES[url, rev]
# even if we have already cloned this repo, we may need to
# fetch/fast-forward to get specified rev
clone_path = _clone_default_branch(url, rev)
rev_sha = Git(clone_path).resolve_rev(rev or "HEAD")
if not for_write and (url, rev_sha) in CLONES:
return CLONES[url, rev_sha]
if not for_write and (url) in CLONES:
return CLONES[url]
# Copy to a new dir to keep the clone clean
repo_path = tempfile.mkdtemp("dvc-erepo")
@@ -212,11 +260,10 @@ def _cached_clone(url, rev, for_write=False):
copy_tree(clone_path, repo_path)
# Check out the specified revision
if rev is not None:
if for_write:
_git_checkout(repo_path, rev)
if not for_write:
CLONES[url, rev_sha] = repo_path
else:
CLONES[url] = repo_path
return repo_path
+125 -24
View File
@@ -1,17 +1,21 @@
import logging
import os
import re
from itertools import groupby
from funcy import cached_property
from pathspec import PathSpec
from pathspec.patterns import GitWildMatchPattern
from pathspec.util import normalize_file
from dvc.path_info import PathInfo
from dvc.scm.tree import BaseTree
from dvc.system import System
from dvc.utils import relpath
logger = logging.getLogger(__name__)
class DvcIgnore(object):
class DvcIgnore:
DVCIGNORE_FILE = ".dvcignore"
def __call__(self, root, dirs, files):
@@ -26,7 +30,16 @@ class DvcIgnorePatterns(DvcIgnore):
self.dirname = os.path.normpath(os.path.dirname(ignore_file_path))
with tree.open(ignore_file_path, encoding="utf-8") as fobj:
self.ignore_spec = PathSpec.from_lines(GitWildMatchPattern, fobj)
path_spec_lines = fobj.readlines()
regex_pattern_list = map(
GitWildMatchPattern.pattern_to_regex, path_spec_lines
)
self.ignore_spec = [
(ignore, re.compile("|".join(item[0] for item in group)))
for ignore, group in groupby(
regex_pattern_list, lambda x: x[1]
)
]
def __call__(self, root, dirs, files):
files = [f for f in files if not self.matches(root, f)]
@@ -35,12 +48,28 @@ class DvcIgnorePatterns(DvcIgnore):
return dirs, files
def matches(self, dirname, basename):
abs_path = os.path.join(dirname, basename)
rel_path = relpath(abs_path, self.dirname)
if os.pardir + os.sep in rel_path:
# NOTE: `relpath` is too slow, so we have to assume that both
# `dirname` and `self.dirname` are relative or absolute together.
prefix = self.dirname + os.sep
if dirname == self.dirname:
path = basename
elif dirname.startswith(prefix):
rel = dirname[len(prefix) :]
# NOTE: `os.path.join` is ~x5.5 slower
path = f"{rel}{os.sep}{basename}"
else:
return False
return self.ignore_spec.match_file(rel_path)
if not System.is_unix():
path = normalize_file(path)
return self.ignore(path)
def ignore(self, path):
result = False
for ignore, pattern in self.ignore_spec:
if pattern.match(path):
result = ignore
return result
def __hash__(self):
return hash(self.ignore_file_path)
@@ -76,21 +105,22 @@ class DvcIgnoreRepo(DvcIgnore):
def is_dvc_repo(directory):
from dvc.repo import Repo
return os.path.isdir(os.path.join(directory, Repo.DVC_DIR))
return os.path.isdir(os.path.join(root, directory, Repo.DVC_DIR))
dirs = [d for d in dirs if not is_dvc_repo(d)]
return dirs, files
class DvcIgnoreFilter(object):
def __init__(self, tree):
class DvcIgnoreFilter:
def __init__(self, tree, root_dir):
self.tree = tree
self.root_dir = root_dir
self.ignores = {
DvcIgnoreDirs([".git", ".hg", ".dvc"]),
DvcIgnoreRepo(),
}
for root, dirs, files in self.tree.walk(self.tree.tree_root):
for root, dirs, files in self.tree.walk(self.root_dir):
self._update(root)
dirs[:], files[:] = self(root, dirs, files)
@@ -107,36 +137,107 @@ class DvcIgnoreFilter(object):
class CleanTree(BaseTree):
def __init__(self, tree):
def __init__(self, tree, tree_root=None):
self.tree = tree
if tree_root:
self._tree_root = tree_root
else:
self._tree_root = self.tree.tree_root
@cached_property
def dvcignore(self):
return DvcIgnoreFilter(self.tree)
return DvcIgnoreFilter(self.tree, self.tree_root)
@property
def tree_root(self):
return self.tree.tree_root
return self._tree_root
def open(self, path, mode="r", encoding="utf-8"):
return self.tree.open(path, mode, encoding)
if self.isfile(path):
return self.tree.open(path, mode, encoding)
raise FileNotFoundError
def exists(self, path):
return self.tree.exists(path)
if self.tree.exists(path) and self._parents_exist(path):
if self.tree.isdir(path):
return self._valid_dirname(path)
return self._valid_filename(path)
return False
def isdir(self, path):
return self.tree.isdir(path)
return (
self.tree.isdir(path)
and self._parents_exist(path)
and self._valid_dirname(path)
)
def _valid_dirname(self, path):
path = os.path.abspath(path)
if path == self.tree_root:
return True
dirname, basename = os.path.split(path)
dirs, _ = self.dvcignore(dirname, [basename], [])
if dirs:
return True
return False
def isfile(self, path):
return self.tree.isfile(path)
return (
self.tree.isfile(path)
and self._parents_exist(path)
and self._valid_filename(path)
)
def _valid_filename(self, path):
dirname, basename = os.path.split(os.path.normpath(path))
_, files = self.dvcignore(os.path.abspath(dirname), [], [basename])
if files:
return True
return False
def isexec(self, path):
return self.exists(path) and self.tree.isexec(path)
def _parents_exist(self, path):
from dvc.repo import Repo
path = PathInfo(path)
# if parent is tree_root or inside a .dvc dir we can skip this check
if path.parent == self.tree_root or Repo.DVC_DIR in path.parts:
return True
# paths outside of the CleanTree root should be ignored
path = relpath(path, self.tree_root)
if path.startswith("..") or (
os.name == "nt"
and not os.path.commonprefix(
[os.path.abspath(path), self.tree_root]
)
):
return False
# check if parent directories are in our ignores, starting from
# tree_root
for parent_dir in reversed(PathInfo(path).parents):
dirname, basename = os.path.split(parent_dir)
if basename == ".":
# parent_dir == tree_root
continue
dirs, _ = self.dvcignore(os.path.abspath(dirname), [basename], [])
if not dirs:
return False
return True
def walk(self, top, topdown=True):
for root, dirs, files in self.tree.walk(top, topdown):
dirs[:], files[:] = self.dvcignore(root, dirs, files)
dirs[:], files[:] = self.dvcignore(
os.path.abspath(root), dirs, files
)
yield root, dirs, files
def walk_files(self, top):
for root, _, files in self.walk(top):
for file in files:
yield os.path.join(root, file)
def stat(self, path):
if self.exists(path):
return self.tree.stat(path)
raise FileNotFoundError
+6 -2
View File
@@ -7,13 +7,17 @@
TEXT_CHARS = bytes(range(32, 127)) + b"\n\r\t\f\b"
def istextfile(fname, blocksize=512):
def istextfile(fname, blocksize=512, tree=None):
""" Uses heuristics to guess whether the given file is text or binary,
by reading a single block of bytes from the file.
If more than 30% of the chars in the block are non-text, or there
are NUL ('\x00') bytes in the block, assume this is a binary file.
"""
with open(fname, "rb") as fobj:
if tree:
open_func = tree.open
else:
open_func = open
with open_func(fname, "rb") as fobj:
block = fobj.read(blocksize)
if not block:
+5 -5
View File
@@ -5,19 +5,19 @@ import os
import time
from datetime import timedelta
import zc.lockfile
import flufl.lock
import zc.lockfile
from dvc.exceptions import DvcException
from dvc.utils import format_link
from dvc.progress import Tqdm
from dvc.utils import format_link
DEFAULT_TIMEOUT = 5
FAILED_TO_LOCK_MESSAGE = (
"cannot perform the command because another DVC process seems to be "
"running on this project. If that is not the case, manually remove "
"`.dvc/lock` and try again."
"`.dvc/tmp/lock` and try again."
)
@@ -25,7 +25,7 @@ class LockError(DvcException):
"""Thrown when unable to acquire the lock for DVC repo."""
class Lock(object):
class Lock:
"""Class for DVC repo lock.
Uses zc.lockfile as backend.
@@ -47,7 +47,7 @@ class Lock(object):
disable=not self._friendly,
desc=(
"If DVC froze, see `hardlink_lock` in {}".format(
format_link("man.dvc.org/config#core")
format_link("https://man.dvc.org/config#core")
)
),
):
+51 -11
View File
@@ -7,7 +7,6 @@ import colorama
from dvc.progress import Tqdm
FOOTER = (
"\n{yellow}Having any troubles?{nc}"
" Hit us up at {blue}https://dvc.org/support{nc},"
@@ -19,20 +18,45 @@ FOOTER = (
)
def addLoggingLevel(levelName, levelNum, methodName=None):
"""
Adds a new logging level to the `logging` module and the
currently configured logging class.
Based on https://stackoverflow.com/questions/2183233
"""
if methodName is None:
methodName = levelName.lower()
assert not hasattr(logging, levelName)
assert not hasattr(logging, methodName)
assert not hasattr(logging.getLoggerClass(), methodName)
def logForLevel(self, message, *args, **kwargs):
if self.isEnabledFor(levelNum):
self._log(levelNum, message, args, **kwargs)
def logToRoot(message, *args, **kwargs):
logging.log(levelNum, message, *args, **kwargs)
logging.addLevelName(levelNum, levelName)
setattr(logging, levelName, levelNum)
setattr(logging.getLoggerClass(), methodName, logForLevel)
setattr(logging, methodName, logToRoot)
class LoggingException(Exception):
def __init__(self, record):
msg = "failed to log {}".format(str(record))
super().__init__(msg)
class ExcludeErrorsFilter(logging.Filter):
def filter(self, record):
return record.levelno < logging.WARNING
def excludeFilter(level):
class ExcludeLevelFilter(logging.Filter):
def filter(self, record):
return record.levelno < level
class ExcludeInfoFilter(logging.Filter):
def filter(self, record):
return record.levelno < logging.INFO
return ExcludeLevelFilter
class ColorFormatter(logging.Formatter):
@@ -48,6 +72,7 @@ class ColorFormatter(logging.Formatter):
"""
color_code = {
"TRACE": colorama.Fore.GREEN,
"DEBUG": colorama.Fore.BLUE,
"WARNING": colorama.Fore.YELLOW,
"ERROR": colorama.Fore.RED,
@@ -117,7 +142,11 @@ class LoggerHandler(logging.StreamHandler):
def _is_verbose():
return logging.getLogger("dvc").getEffectiveLevel() == logging.DEBUG
return (
logging.NOTSET
< logging.getLogger("dvc").getEffectiveLevel()
<= logging.DEBUG
)
def _iter_causes(exc):
@@ -143,6 +172,7 @@ def _stack_trace(exc_info):
def disable_other_loggers():
logging.captureWarnings(True)
root = logging.root
for (logger_name, logger) in root.manager.loggerDict.items():
if logger_name != "dvc" and not logger_name.startswith("dvc."):
@@ -152,12 +182,14 @@ def disable_other_loggers():
def setup(level=logging.INFO):
colorama.init()
addLoggingLevel("TRACE", logging.DEBUG - 5)
logging.config.dictConfig(
{
"version": 1,
"filters": {
"exclude_errors": {"()": ExcludeErrorsFilter},
"exclude_info": {"()": ExcludeInfoFilter},
"exclude_errors": {"()": excludeFilter(logging.WARNING)},
"exclude_info": {"()": excludeFilter(logging.INFO)},
"exclude_debug": {"()": excludeFilter(logging.DEBUG)},
},
"formatters": {"color": {"()": ColorFormatter}},
"handlers": {
@@ -175,6 +207,13 @@ def setup(level=logging.INFO):
"stream": "ext://sys.stdout",
"filters": ["exclude_info"],
},
"console_trace": {
"class": "dvc.logger.LoggerHandler",
"level": "TRACE",
"formatter": "color",
"stream": "ext://sys.stdout",
"filters": ["exclude_debug"],
},
"console_errors": {
"class": "dvc.logger.LoggerHandler",
"level": "WARNING",
@@ -188,6 +227,7 @@ def setup(level=logging.INFO):
"handlers": [
"console_info",
"console_debug",
"console_trace",
"console_errors",
],
},
+17 -11
View File
@@ -6,12 +6,11 @@ import logging
from dvc import analytics
from dvc.cli import parse_args
from dvc.config import ConfigError
from dvc.exceptions import DvcParserError
from dvc.exceptions import NotDvcRepoError
from dvc.exceptions import DvcException, DvcParserError, NotDvcRepoError
from dvc.external_repo import clean_repos
from dvc.logger import disable_other_loggers, FOOTER
from dvc.utils import format_link
from dvc.logger import FOOTER, disable_other_loggers
from dvc.remote.pool import close_pools
from dvc.utils import format_link
# Workaround for CPython bug. See [1] and [2] for more info.
# [1] https://github.com/aws/aws-cli/blob/1.16.277/awscli/clidriver.py#L55
@@ -19,7 +18,6 @@ from dvc.remote.pool import close_pools
"".encode("idna")
logger = logging.getLogger("dvc")
@@ -33,18 +31,23 @@ def main(argv=None):
int: command's return code.
"""
args = None
cmd = None
disable_other_loggers()
outerLogLevel = logger.level
try:
args = parse_args(argv)
if args.quiet:
logger.setLevel(logging.CRITICAL)
elif args.verbose:
logger.setLevel(logging.DEBUG)
verbosity = args.verbose - args.quiet
if verbosity:
logger.setLevel(
{
-2: logging.CRITICAL,
-1: logging.ERROR,
1: logging.DEBUG,
2: logging.TRACE,
}[max(-2, min(verbosity, 2))]
)
logger.trace(args)
cmd = args.func(args)
ret = cmd.run()
@@ -59,6 +62,9 @@ def main(argv=None):
ret = 253
except DvcParserError:
ret = 254
except DvcException:
ret = 255
logger.exception("")
except Exception as exc: # pylint: disable=broad-except
if isinstance(exc, OSError) and exc.errno == errno.EMFILE:
logger.exception(
+102 -56
View File
@@ -1,32 +1,35 @@
from collections import defaultdict
from urllib.parse import urlparse
from voluptuous import Any, Required, Lower, Length, Coerce, And, SetTo
from dvc.output.base import OutputBase
from dvc.output.gs import OutputGS
from dvc.output.hdfs import OutputHDFS
from dvc.output.local import OutputLOCAL
from dvc.output.s3 import OutputS3
from dvc.output.ssh import OutputSSH
from dvc.remote import Remote
from dvc.remote.hdfs import RemoteHDFS
from dvc.remote.local import RemoteLOCAL
from dvc.remote.s3 import RemoteS3
from funcy import collecting, project
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
from dvc.output.base import BaseOutput
from dvc.output.gs import GSOutput
from dvc.output.hdfs import HDFSOutput
from dvc.output.local import LocalOutput
from dvc.output.s3 import S3Output
from dvc.output.ssh import SSHOutput
from dvc.remote import get_remote
from dvc.remote.hdfs import HDFSRemoteTree
from dvc.remote.local import LocalRemoteTree
from dvc.remote.s3 import S3RemoteTree
from dvc.scheme import Schemes
OUTS = [
OutputHDFS,
OutputS3,
OutputGS,
OutputSSH,
# NOTE: OutputLOCAL is the default choice
HDFSOutput,
S3Output,
GSOutput,
SSHOutput,
# NOTE: LocalOutput is the default choice
]
OUTS_MAP = {
Schemes.HDFS: OutputHDFS,
Schemes.S3: OutputS3,
Schemes.GS: OutputGS,
Schemes.SSH: OutputSSH,
Schemes.LOCAL: OutputLOCAL,
Schemes.HDFS: HDFSOutput,
Schemes.S3: S3Output,
Schemes.GS: GSOutput,
Schemes.SSH: SSHOutput,
Schemes.LOCAL: LocalOutput,
}
CHECKSUM_SCHEMA = Any(
@@ -44,26 +47,26 @@ CHECKSUM_SCHEMA = Any(
# so when a few types of outputs share the same name, we only need
# specify it once.
CHECKSUMS_SCHEMA = {
RemoteLOCAL.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
RemoteS3.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
RemoteHDFS.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
LocalRemoteTree.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
S3RemoteTree.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
HDFSRemoteTree.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
}
TAGS_SCHEMA = {str: CHECKSUMS_SCHEMA}
SCHEMA = CHECKSUMS_SCHEMA.copy()
SCHEMA[Required(OutputBase.PARAM_PATH)] = str
SCHEMA[OutputBase.PARAM_CACHE] = bool
SCHEMA[OutputBase.PARAM_METRIC] = OutputBase.METRIC_SCHEMA
SCHEMA[OutputBase.PARAM_TAGS] = TAGS_SCHEMA
SCHEMA[OutputBase.PARAM_PERSIST] = bool
SCHEMA[Required(BaseOutput.PARAM_PATH)] = str
SCHEMA[BaseOutput.PARAM_CACHE] = bool
SCHEMA[BaseOutput.PARAM_METRIC] = BaseOutput.METRIC_SCHEMA
SCHEMA[BaseOutput.PARAM_PLOT] = bool
SCHEMA[BaseOutput.PARAM_PERSIST] = bool
def _get(stage, p, info, cache, metric, persist=False, tags=None):
def _get(
stage, p, info=None, cache=True, metric=False, plot=False, persist=False
):
parsed = urlparse(p)
if parsed.scheme == "remote":
remote = Remote(stage.repo, name=parsed.netloc)
remote = get_remote(stage.repo, name=parsed.netloc)
return OUTS_MAP[remote.scheme](
stage,
p,
@@ -71,8 +74,8 @@ def _get(stage, p, info, cache, metric, persist=False, tags=None):
cache=cache,
remote=remote,
metric=metric,
plot=plot,
persist=persist,
tags=tags,
)
for o in OUTS:
@@ -84,29 +87,29 @@ def _get(stage, p, info, cache, metric, persist=False, tags=None):
cache=cache,
remote=None,
metric=metric,
plot=plot,
persist=persist,
tags=tags,
)
return OutputLOCAL(
return LocalOutput(
stage,
p,
info,
cache=cache,
remote=None,
metric=metric,
plot=plot,
persist=persist,
tags=tags,
)
def loadd_from(stage, d_list):
ret = []
for d in d_list:
p = d.pop(OutputBase.PARAM_PATH)
cache = d.pop(OutputBase.PARAM_CACHE, True)
metric = d.pop(OutputBase.PARAM_METRIC, False)
persist = d.pop(OutputBase.PARAM_PERSIST, False)
tags = d.pop(OutputBase.PARAM_TAGS, None)
p = d.pop(BaseOutput.PARAM_PATH)
cache = d.pop(BaseOutput.PARAM_CACHE, True)
metric = d.pop(BaseOutput.PARAM_METRIC, False)
plot = d.pop(BaseOutput.PARAM_PLOT, False)
persist = d.pop(BaseOutput.PARAM_PERSIST, False)
ret.append(
_get(
stage,
@@ -114,24 +117,67 @@ def loadd_from(stage, d_list):
info=d,
cache=cache,
metric=metric,
plot=plot,
persist=persist,
tags=tags,
)
)
return ret
def loads_from(stage, s_list, use_cache=True, metric=False, persist=False):
ret = []
for s in s_list:
ret.append(
_get(
stage,
s,
info={},
cache=use_cache,
metric=metric,
persist=persist,
)
def loads_from(
stage, s_list, use_cache=True, metric=False, plot=False, persist=False
):
return [
_get(
stage,
s,
info={},
cache=use_cache,
metric=metric,
plot=plot,
persist=persist,
)
return ret
for s in s_list
]
def _split_dict(d, keys):
return project(d, keys), project(d, d.keys() - keys)
def _merge_data(s_list):
d = defaultdict(dict)
for key in s_list:
if isinstance(key, str):
d[key].update({})
continue
if not isinstance(key, dict):
raise ValueError(f"'{type(key).__name__}' not supported.")
for k, flags in key.items():
if not isinstance(flags, dict):
raise ValueError(
f"Expected dict for '{k}', got: '{type(flags).__name__}'"
)
d[k].update(flags)
return d
@collecting
def load_from_pipeline(stage, s_list, typ="outs"):
if typ not in (stage.PARAM_OUTS, stage.PARAM_METRICS, stage.PARAM_PLOTS):
raise ValueError(f"'{typ}' key is not allowed for pipeline files.")
metric = typ == stage.PARAM_METRICS
plot = typ == stage.PARAM_PLOTS
d = _merge_data(s_list)
for path, flags in d.items():
plt_d = {}
if plot:
from dvc.schema import PLOT_PROPS
plt_d, flags = _split_dict(flags, keys=PLOT_PROPS.keys())
extra = project(flags, ["cache", "persist"])
yield _get(stage, path, {}, plot=plt_d or plot, metric=metric, **extra)
+129 -82
View File
@@ -1,52 +1,68 @@
import logging
from urllib.parse import urlparse
import os
from copy import copy
from urllib.parse import urlparse
from voluptuous import Any
import dvc.prompt as prompt
from dvc.cache import NamedCache
from dvc.exceptions import CollectCacheError, RemoteCacheRequiredError
from dvc.exceptions import DvcException
from dvc.remote.base import RemoteBASE
from dvc.exceptions import (
CollectCacheError,
DvcException,
RemoteCacheRequiredError,
)
from dvc.remote.base import BaseRemoteTree, Remote
logger = logging.getLogger(__name__)
class OutputDoesNotExistError(DvcException):
def __init__(self, path):
msg = "output '{}' does not exist".format(path)
msg = f"output '{path}' does not exist"
super().__init__(msg)
class OutputIsNotFileOrDirError(DvcException):
def __init__(self, path):
msg = "output '{}' is not a file or directory".format(path)
msg = f"output '{path}' is not a file or directory"
super().__init__(msg)
class OutputAlreadyTrackedError(DvcException):
def __init__(self, path):
msg = "output '{}' is already tracked by scm (e.g. git)".format(path)
msg = f""" output '{path}' is already tracked by SCM (e.g. Git).
You can remove it from Git, then add to DVC.
To stop tracking from Git:
git rm -r --cached '{path}'
git commit -m "stop tracking {path}" """
super().__init__(msg)
class OutputIsStageFileError(DvcException):
def __init__(self, path):
super().__init__("Stage file '{}' cannot be an output.".format(path))
super().__init__(f"Stage file '{path}' cannot be an output.")
class OutputBase(object):
class BaseOutput:
IS_DEPENDENCY = False
REMOTE = RemoteBASE
REMOTE_CLS = Remote
TREE_CLS = BaseRemoteTree
PARAM_PATH = "path"
PARAM_CACHE = "cache"
PARAM_METRIC = "metric"
PARAM_METRIC_TYPE = "type"
PARAM_METRIC_XPATH = "xpath"
PARAM_PLOT = "plot"
PARAM_PLOT_TEMPLATE = "template"
PARAM_PLOT_X = "x"
PARAM_PLOT_Y = "y"
PARAM_PLOT_X_LABEL = "x_label"
PARAM_PLOT_Y_LABEL = "y_label"
PARAM_PLOT_TITLE = "title"
PARAM_PLOT_HEADER = "header"
PARAM_PERSIST = "persist"
METRIC_SCHEMA = Any(
@@ -58,8 +74,6 @@ class OutputBase(object):
},
)
PARAM_TAGS = "tags"
DoesNotExistError = OutputDoesNotExistError
IsNotFileOrDirError = OutputIsNotFileOrDirError
IsStageFileError = OutputIsStageFileError
@@ -74,8 +88,8 @@ class OutputBase(object):
remote=None,
cache=True,
metric=False,
plot=False,
persist=False,
tags=None,
):
self._validate_output_path(path)
# This output (and dependency) objects have too many paths/urls
@@ -92,11 +106,15 @@ class OutputBase(object):
self.repo = stage.repo if stage else None
self.def_path = path
self.info = info
self.remote = remote or self.REMOTE(self.repo, {})
if remote:
self.remote = remote
else:
tree = self.TREE_CLS(self.repo, {})
self.remote = self.REMOTE_CLS(tree)
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
self.plot = False if self.IS_DEPENDENCY else plot
self.persist = persist
self.tags = None if self.IS_DEPENDENCY else (tags or {})
self.path_info = self._parse_path(remote, path)
if self.use_cache and self.cache is None:
@@ -106,7 +124,7 @@ class OutputBase(object):
if remote:
parsed = urlparse(path)
return remote.path_info / parsed.path.lstrip("/")
return self.REMOTE.path_cls(path)
return self.TREE_CLS.PATH_CLS(path)
def __repr__(self):
return "{class_name}: '{def_path}'".format(
@@ -118,7 +136,7 @@ class OutputBase(object):
@property
def scheme(self):
return self.REMOTE.scheme
return self.TREE_CLS.scheme
@property
def is_in_repo(self):
@@ -141,15 +159,26 @@ class OutputBase(object):
@classmethod
def supported(cls, url):
return cls.REMOTE.supported(url)
return cls.TREE_CLS.supported(url)
@property
def cache_path(self):
return self.cache.checksum_to_path_info(self.checksum).url
@property
def checksum_type(self):
return self.remote.tree.PARAM_CHECKSUM
@property
def checksum(self):
return self.info.get(self.remote.PARAM_CHECKSUM)
return self.info.get(self.remote.tree.PARAM_CHECKSUM)
@checksum.setter
def checksum(self, checksum):
self.info[self.remote.tree.PARAM_CHECKSUM] = checksum
def get_checksum(self):
return self.remote.get_checksum(self.path_info)
@property
def is_dir_checksum(self):
@@ -157,10 +186,13 @@ class OutputBase(object):
@property
def exists(self):
return self.remote.exists(self.path_info)
return self.remote.tree.exists(self.path_info)
def save_info(self):
return self.remote.save_info(self.path_info)
def changed_checksum(self):
return self.checksum != self.remote.get_checksum(self.path_info)
return self.checksum != self.get_checksum()
def changed_cache(self, filter_info=None):
if not self.use_cache or not self.checksum:
@@ -190,13 +222,22 @@ class OutputBase(object):
@property
def is_empty(self):
return self.remote.is_empty(self.path_info)
return self.remote.tree.is_empty(self.path_info)
def isdir(self):
return self.remote.isdir(self.path_info)
return self.remote.tree.isdir(self.path_info)
def isfile(self):
return self.remote.isfile(self.path_info)
return self.remote.tree.isfile(self.path_info)
def ignore(self):
if not self.use_scm_ignore:
return
if self.repo.scm.is_tracked(self.fspath):
raise OutputAlreadyTrackedError(self)
self.repo.scm.ignore(self.fspath)
def save(self):
if not self.exists:
@@ -206,39 +247,32 @@ class OutputBase(object):
raise self.IsNotFileOrDirError(self)
if self.is_empty:
logger.warning("'{}' is empty.".format(self))
logger.warning(f"'{self}' is empty.")
if self.use_scm_ignore:
if self.repo.scm.is_tracked(self.fspath):
raise OutputAlreadyTrackedError(self)
self.ignore()
self.repo.scm.ignore(self.fspath)
if self.metric or self.plot:
self.verify_metric()
if not self.use_cache:
self.info = self.remote.save_info(self.path_info)
if self.metric:
self.verify_metric()
self.info = self.save_info()
if not self.IS_DEPENDENCY:
logger.info(
"Output '{}' doesn't use cache. Skipping saving.".format(
self
)
logger.debug(
"Output '%s' doesn't use cache. Skipping saving.", self
)
return
assert not self.IS_DEPENDENCY
if not self.changed():
logger.info(
"Output '{}' didn't change. Skipping saving.".format(self)
)
logger.debug("Output '%s' didn't change. Skipping saving.", self)
return
self.info = self.remote.save_info(self.path_info)
self.info = self.save_info()
def commit(self):
if self.use_cache:
self.cache.save(self.path_info, self.info)
self.cache.save(self.path_info, self.cache.tree, self.info)
def dumpd(self):
ret = copy(self.info)
@@ -247,7 +281,8 @@ class OutputBase(object):
if self.IS_DEPENDENCY:
return ret
ret[self.PARAM_CACHE] = self.use_cache
if not self.use_cache:
ret[self.PARAM_CACHE] = self.use_cache
if isinstance(self.metric, dict):
if (
@@ -256,27 +291,27 @@ class OutputBase(object):
):
del self.metric[self.PARAM_METRIC_XPATH]
ret[self.PARAM_METRIC] = self.metric
ret[self.PARAM_PERSIST] = self.persist
if self.metric:
ret[self.PARAM_METRIC] = self.metric
if self.tags:
ret[self.PARAM_TAGS] = self.tags
if self.plot:
ret[self.PARAM_PLOT] = self.plot
if self.persist:
ret[self.PARAM_PERSIST] = self.persist
return ret
def verify_metric(self):
raise DvcException(
"verify metric is not supported for {}".format(self.scheme)
)
raise DvcException(f"verify metric is not supported for {self.scheme}")
def download(self, to):
self.remote.download(self.path_info, to.path_info)
self.remote.tree.download(self.path_info, to.path_info)
def checkout(
self,
force=False,
progress_callback=None,
tag=None,
relink=False,
filter_info=None,
):
@@ -287,14 +322,9 @@ class OutputBase(object):
)
return None
if tag:
info = self.tags[tag]
else:
info = self.info
return self.cache.checkout(
self.path_info,
info,
self.info,
force=force,
progress_callback=progress_callback,
relink=relink,
@@ -302,7 +332,7 @@ class OutputBase(object):
)
def remove(self, ignore_remove=False):
self.remote.remove(self.path_info)
self.remote.tree.remove(self.path_info)
if self.scheme != "local":
return
@@ -313,7 +343,7 @@ class OutputBase(object):
if self.scheme == "local" and self.use_scm_ignore:
self.repo.scm.ignore_remove(self.fspath)
self.remote.move(self.path_info, out.path_info)
self.remote.tree.move(self.path_info, out.path_info)
self.def_path = out.def_path
self.path_info = out.path_info
self.save()
@@ -332,9 +362,20 @@ class OutputBase(object):
def unprotect(self):
if self.exists:
self.remote.unprotect(self.path_info)
self.remote.tree.unprotect(self.path_info)
def _collect_used_dir_cache(
def get_dir_cache(self, **kwargs):
if not self.is_dir_checksum:
raise DvcException("cannot get dir cache for file checksum")
if self.cache.changed_cache_file(self.checksum):
self.repo.cloud.pull(
NamedCache.make("local", self.checksum, str(self)),
show_checksums=False,
**kwargs,
)
return self.dir_cache
def collect_used_dir_cache(
self, remote=None, force=False, jobs=None, filter_info=None
):
"""Get a list of `info`s related to the given directory.
@@ -358,16 +399,10 @@ class OutputBase(object):
cache = NamedCache()
if self.cache.changed_cache_file(self.checksum):
try:
self.repo.cloud.pull(
NamedCache.make("local", self.checksum, str(self)),
jobs=jobs,
remote=remote,
show_checksums=False,
)
except DvcException:
logger.debug("failed to pull cache for '{}'".format(self))
try:
self.get_dir_cache(jobs=jobs, remote=remote)
except DvcException:
logger.debug(f"failed to pull cache for '{self}'")
if self.cache.changed_cache_file(self.checksum):
msg = (
@@ -383,11 +418,21 @@ class OutputBase(object):
else:
return cache
path = str(self.path_info)
filter_path = str(filter_info) if filter_info else None
is_win = os.name == "nt"
for entry in self.dir_cache:
checksum = entry[self.remote.PARAM_CHECKSUM]
info = self.path_info / entry[self.remote.PARAM_RELPATH]
if not filter_info or info.isin_or_eq(filter_info):
cache.add(self.scheme, checksum, str(info))
checksum = entry[self.remote.tree.PARAM_CHECKSUM]
entry_relpath = entry[self.remote.tree.PARAM_RELPATH]
if is_win:
entry_relpath = entry_relpath.replace("/", os.sep)
entry_path = os.path.join(path, entry_relpath)
if (
not filter_path
or entry_path == filter_path
or entry_path.startswith(filter_path + os.sep)
):
cache.add(self.scheme, checksum, entry_path)
return cache
@@ -420,9 +465,9 @@ class OutputBase(object):
if self.exists:
msg += (
"\n"
"You can also use `dvc commit {stage}` to associate "
"existing '{out}' with '{stage}'.".format(
out=self, stage=self.stage.relpath
"You can also use `dvc commit {stage.addressing}` "
"to associate existing '{out}' with {stage}.".format(
out=self, stage=self.stage
)
)
logger.warning(msg)
@@ -433,13 +478,15 @@ class OutputBase(object):
if not self.is_dir_checksum:
return ret
ret.update(self._collect_used_dir_cache(**kwargs))
ret.add_child_cache(
self.checksum, self.collect_used_dir_cache(**kwargs),
)
return ret
@classmethod
def _validate_output_path(cls, path):
from dvc.stage import Stage
from dvc.dvcfile import is_valid_filename
if Stage.is_valid_filename(path):
if is_valid_filename(path):
raise cls.IsStageFileError(path)
+4 -4
View File
@@ -1,6 +1,6 @@
from dvc.output.s3 import OutputS3
from dvc.remote.gs import RemoteGS
from dvc.output.s3 import S3Output
from dvc.remote.gs import GSRemoteTree
class OutputGS(OutputS3):
REMOTE = RemoteGS
class GSOutput(S3Output):
TREE_CLS = GSRemoteTree
+4 -4
View File
@@ -1,6 +1,6 @@
from dvc.output.base import OutputBase
from dvc.remote.hdfs import RemoteHDFS
from dvc.output.base import BaseOutput
from dvc.remote.hdfs import HDFSRemoteTree
class OutputHDFS(OutputBase):
REMOTE = RemoteHDFS
class HDFSOutput(BaseOutput):
TREE_CLS = HDFSRemoteTree
+15 -15
View File
@@ -4,18 +4,17 @@ from urllib.parse import urlparse
from dvc.exceptions import DvcException
from dvc.istextfile import istextfile
from dvc.output.base import OutputBase
from dvc.remote.local import RemoteLOCAL
from dvc.output.base import BaseOutput
from dvc.remote.local import LocalRemote, LocalRemoteTree
from dvc.utils import relpath
from dvc.compat import fspath_py35
from dvc.utils.fs import path_isin
logger = logging.getLogger(__name__)
class OutputLOCAL(OutputBase):
REMOTE = RemoteLOCAL
class LocalOutput(BaseOutput):
REMOTE_CLS = LocalRemote
TREE_CLS = LocalRemoteTree
sep = os.sep
def __init__(self, stage, path, *args, **kwargs):
@@ -35,12 +34,12 @@ class OutputLOCAL(OutputBase):
#
# FIXME: if we have Windows path containing / or posix one with \
# then we have #2059 bug and can't really handle that.
p = self.REMOTE.path_cls(path)
p = self.TREE_CLS.PATH_CLS(path)
if not p.is_absolute():
p = self.stage.wdir / p
abs_p = os.path.abspath(os.path.normpath(fspath_py35(p)))
return self.REMOTE.path_cls(abs_p)
abs_p = os.path.abspath(os.path.normpath(p))
return self.TREE_CLS.PATH_CLS(abs_p)
def __str__(self):
if not self.is_in_repo:
@@ -73,17 +72,18 @@ class OutputLOCAL(OutputBase):
return ret
def verify_metric(self):
if not self.metric:
if not self.metric or self.plot:
return
path = fspath_py35(self.path_info)
path = os.fspath(self.path_info)
if not os.path.exists(path):
return
name = "metrics" if self.metric else "plot"
if os.path.isdir(path):
msg = "directory '{}' cannot be used as metrics."
raise DvcException(msg.format(self.path_info))
msg = "directory '{}' cannot be used as {}."
raise IsADirectoryError(msg.format(self.path_info, name))
if not istextfile(path):
msg = "binary file '{}' cannot be used as metrics."
raise DvcException(msg.format(self.path_info))
msg = "binary file '{}' cannot be used as {}."
raise DvcException(msg.format(self.path_info, name))
+4 -4
View File
@@ -1,6 +1,6 @@
from dvc.output.base import OutputBase
from dvc.remote.s3 import RemoteS3
from dvc.output.base import BaseOutput
from dvc.remote.s3 import S3RemoteTree
class OutputS3(OutputBase):
REMOTE = RemoteS3
class S3Output(BaseOutput):
TREE_CLS = S3RemoteTree
+5 -4
View File
@@ -1,6 +1,7 @@
from dvc.output.base import OutputBase
from dvc.remote.ssh import RemoteSSH
from dvc.output.base import BaseOutput
from dvc.remote.ssh import SSHRemote, SSHRemoteTree
class OutputSSH(OutputBase):
REMOTE = RemoteSSH
class SSHOutput(BaseOutput):
REMOTE_CLS = SSHRemote
TREE_CLS = SSHRemoteTree
+14 -20
View File
@@ -1,6 +1,6 @@
import os
import posixpath
import pathlib
import posixpath
from urllib.parse import urlparse
from funcy import cached_property
@@ -8,7 +8,7 @@ from funcy import cached_property
from dvc.utils import relpath
class _BasePath(object):
class _BasePath:
def overlaps(self, other):
if isinstance(other, (str, bytes)):
other = self.__class__(other)
@@ -33,10 +33,6 @@ class PathInfo(pathlib.PurePath, _BasePath):
cls = WindowsPathInfo if os.name == "nt" else PosixPathInfo
return cls._from_parts(args)
@classmethod
def from_posix(cls, s):
return cls(PosixPathInfo(s))
def as_posix(self):
f = self._flavour
# Unlike original implementation [1] that uses `str()` we actually need
@@ -54,7 +50,6 @@ class PathInfo(pathlib.PurePath, _BasePath):
return "{}: '{}'".format(type(self).__name__, self)
# This permits passing it to file utils directly in Python 3.6+
# With Python 2.7, Python 3.5+ we are stuck with path_info.fspath for now
def __fspath__(self):
return pathlib.PurePath.__str__(self)
@@ -92,7 +87,7 @@ class _URLPathInfo(PosixPathInfo):
__unicode__ = __str__
class _URLPathParents(object):
class _URLPathParents:
def __init__(self, src):
self.src = src
self._parents = self.src._path.parents
@@ -104,7 +99,7 @@ class _URLPathParents(object):
return self.src.replace(path=self._parents[idx])
def __repr__(self):
return "<{}.parents>".format(self.src)
return f"<{self.src}.parents>"
class URLInfo(_BasePath):
@@ -115,7 +110,7 @@ class URLInfo(_BasePath):
assert not p.query and not p.params and not p.fragment
assert p.password is None
self.fill_parts(p.scheme, p.hostname, p.username, p.port, p.path)
self._fill_parts(p.scheme, p.hostname, p.username, p.port, p.path)
@classmethod
def from_parts(
@@ -124,13 +119,13 @@ class URLInfo(_BasePath):
assert bool(host) ^ bool(netloc)
if netloc is not None:
return cls("{}://{}{}".format(scheme, netloc, path))
return cls(f"{scheme}://{netloc}{path}")
obj = cls.__new__(cls)
obj.fill_parts(scheme, host, user, port, path)
obj._fill_parts(scheme, host, user, port, path)
return obj
def fill_parts(self, scheme, host, user, port, path):
def _fill_parts(self, scheme, host, user, port, path):
assert scheme != "remote"
assert isinstance(path, (str, bytes, _URLPathInfo))
@@ -158,7 +153,7 @@ class URLInfo(_BasePath):
@cached_property
def url(self):
return "{}://{}{}".format(self.scheme, self.netloc, self._spath)
return f"{self.scheme}://{self.netloc}{self._spath}"
def __str__(self):
return self.url
@@ -220,10 +215,10 @@ class URLInfo(_BasePath):
if isinstance(other, (str, bytes)):
other = self.__class__(other)
if self.__class__ != other.__class__:
msg = "'{}' has incompatible class with '{}'".format(self, other)
msg = f"'{self}' has incompatible class with '{other}'"
raise ValueError(msg)
if self._base_parts != other._base_parts:
msg = "'{}' does not start with '{}'".format(self, other)
msg = f"'{self}' does not start with '{other}'"
raise ValueError(msg)
return self._path.relative_to(other._path)
@@ -244,6 +239,8 @@ class CloudURLInfo(URLInfo):
class HTTPURLInfo(URLInfo):
__hash__ = URLInfo.__hash__
def __init__(self, url):
p = urlparse(url)
stripped = p._replace(params=None, query=None, fragment=None)
@@ -280,7 +277,7 @@ class HTTPURLInfo(URLInfo):
)
obj = cls.__new__(cls)
obj.fill_parts(scheme, host, user, port, path)
obj._fill_parts(scheme, host, user, port, path)
obj.params = params
obj.query = query
obj.fragment = fragment
@@ -314,6 +311,3 @@ class HTTPURLInfo(URLInfo):
and self._path == other._path
and self._extra_parts == other._extra_parts
)
def __hash__(self):
return hash(self.parts)
+38 -21
View File
@@ -19,18 +19,17 @@ class Tqdm(tqdm):
BAR_FMT_DEFAULT = (
"{percentage:3.0f}% {desc}|{bar}|"
"{n_fmt}/{total_fmt}"
" [{elapsed}<{remaining}, {rate_fmt:>11}{postfix}]"
"{postfix[info]}{n_fmt}/{total_fmt}"
" [{elapsed}<{remaining}, {rate_fmt:>11}]"
)
# nested bars should have fixed bar widths to align nicely
BAR_FMT_DEFAULT_NESTED = (
"{percentage:3.0f}%|{bar:10}|{desc:{ncols_desc}.{ncols_desc}}"
"{n_fmt}/{total_fmt}"
" [{elapsed}<{remaining}, {rate_fmt:>11}{postfix}]"
"{postfix[info]}{n_fmt}/{total_fmt}"
" [{elapsed}<{remaining}, {rate_fmt:>11}]"
)
BAR_FMT_NOTOTAL = (
"{desc:{ncols_desc}.{ncols_desc}}{n_fmt}"
" [{elapsed}, {rate_fmt:>11}{postfix}]"
"{desc}{bar:b}|{postfix[info]}{n_fmt} [{elapsed}, {rate_fmt:>11}]"
)
BYTES_DEFAULTS = dict(
unit="B", unit_scale=True, unit_divisor=1024, miniters=1
@@ -47,6 +46,7 @@ class Tqdm(tqdm):
bytes=False, # pylint: disable=W0622
file=None,
total=None,
postfix=None,
**kwargs
):
"""
@@ -68,7 +68,6 @@ class Tqdm(tqdm):
kwargs.setdefault("unit_scale", total > 999 if total else True)
if file is None:
file = sys.stderr
self.desc_persist = desc
# auto-disable based on `logger.level`
if not disable:
disable = logger.getEffectiveLevel() > level
@@ -89,6 +88,7 @@ class Tqdm(tqdm):
total=total,
**kwargs
)
self.postfix = postfix or {"info": ""}
if bar_format is None:
if self.__len__():
self.bar_format = (
@@ -102,11 +102,11 @@ class Tqdm(tqdm):
self.bar_format = bar_format
self.refresh()
def update_desc(self, desc, n=1):
def update_msg(self, msg, n=1):
"""
Calls `set_description_str(desc)` and `update(n)`
Sets `msg` as a postfix and calls `update(n)`.
"""
self.set_description_str(desc, refresh=False)
self.postfix["info"] = " %s |" % msg
self.update(n)
def update_to(self, current, total=None):
@@ -114,13 +114,27 @@ class Tqdm(tqdm):
self.total = total # pylint: disable=W0613,W0201
self.update(current - self.n)
def wrap_fn(self, fn, callback=None):
"""
Returns a wrapped `fn` which calls `callback()` on each call.
`callback` is `self.update` by default.
"""
if callback is None:
callback = self.update
def wrapped(*args, **kwargs):
res = fn(*args, **kwargs)
callback()
return res
return wrapped
def close(self):
if self.desc_persist is not None:
self.set_description_str(self.desc_persist, refresh=False)
# unknown/zero ETA
self.bar_format = self.bar_format.replace("<{remaining}", "")
# remove completed bar
self.bar_format = self.bar_format.replace("|{bar:10}|", " ")
self.postfix["info"] = ""
# remove ETA (either unknown or zero); remove completed bar
self.bar_format = self.bar_format.replace("<{remaining}", "").replace(
"|{bar:10}|", " "
)
super().close()
@property
@@ -128,12 +142,15 @@ class Tqdm(tqdm):
"""inject `ncols_desc` to fill the display width (`ncols`)"""
d = super().format_dict
ncols = d["ncols"] or 80
ncols_desc = ncols - len(self.format_meter(ncols_desc=1, **d)) + 1
ncols_desc = max(ncols_desc, 0)
if ncols_desc:
d["ncols_desc"] = ncols_desc
# assumes `bar_format` has max one of ("ncols_desc" & "ncols_info")
ncols_left = (
ncols - len(self.format_meter(ncols_desc=1, ncols_info=1, **d)) + 1
)
ncols_left = max(ncols_left, 0)
if ncols_left:
d["ncols_desc"] = d["ncols_info"] = ncols_left
else:
# work-around for zero-width description
d["ncols_desc"] = 1
d["ncols_desc"] = d["ncols_info"] = 1
d["prefix"] = ""
return d
+2 -2
View File
@@ -38,7 +38,7 @@ def confirm(statement):
Returns:
bool: whether or not specified statement was confirmed.
"""
prompt = "{statement} [y/n]".format(statement=statement)
prompt = f"{statement} [y/n]"
answer = _ask(prompt, limited_to=["yes", "no", "y", "n"])
return answer and answer.startswith("y")
@@ -52,5 +52,5 @@ def password(statement):
Returns:
str: password entered by the user.
"""
logger.info("{statement}: ".format(statement=statement))
logger.info(f"{statement}: ")
return getpass("")
+43 -30
View File
@@ -1,47 +1,46 @@
import posixpath
from urllib.parse import urlparse
from dvc.remote.azure import RemoteAZURE
from dvc.remote.gdrive import RemoteGDrive
from dvc.remote.gs import RemoteGS
from dvc.remote.hdfs import RemoteHDFS
from dvc.remote.http import RemoteHTTP
from dvc.remote.https import RemoteHTTPS
from dvc.remote.local import RemoteLOCAL
from dvc.remote.oss import RemoteOSS
from dvc.remote.s3 import RemoteS3
from dvc.remote.ssh import RemoteSSH
from dvc.remote.azure import AzureRemoteTree
from dvc.remote.base import Remote
from dvc.remote.gdrive import GDriveRemoteTree
from dvc.remote.gs import GSRemoteTree
from dvc.remote.hdfs import HDFSRemoteTree
from dvc.remote.http import HTTPRemoteTree
from dvc.remote.https import HTTPSRemoteTree
from dvc.remote.local import LocalRemote, LocalRemoteTree
from dvc.remote.oss import OSSRemoteTree
from dvc.remote.s3 import S3RemoteTree
from dvc.remote.ssh import SSHRemote, SSHRemoteTree
REMOTES = [
RemoteAZURE,
RemoteGDrive,
RemoteGS,
RemoteHDFS,
RemoteHTTP,
RemoteHTTPS,
RemoteS3,
RemoteSSH,
RemoteOSS,
# NOTE: RemoteLOCAL is the default
TREES = [
AzureRemoteTree,
GDriveRemoteTree,
GSRemoteTree,
HDFSRemoteTree,
HTTPRemoteTree,
HTTPSRemoteTree,
S3RemoteTree,
SSHRemoteTree,
OSSRemoteTree,
# NOTE: LocalRemoteTree is the default
]
def _get(remote_conf):
for remote in REMOTES:
if remote.supported(remote_conf):
return remote
return RemoteLOCAL
def _get_tree(remote_conf):
for tree_cls in TREES:
if tree_cls.supported(remote_conf):
return tree_cls
return LocalRemoteTree
def Remote(repo, **kwargs):
def _get_conf(repo, **kwargs):
name = kwargs.get("name")
if name:
remote_conf = repo.config["remote"][name.lower()]
else:
remote_conf = kwargs
remote_conf = _resolve_remote_refs(repo.config, remote_conf)
return _get(remote_conf)(repo, remote_conf)
return _resolve_remote_refs(repo.config, remote_conf)
def _resolve_remote_refs(config, remote_conf):
@@ -73,3 +72,17 @@ def _resolve_remote_refs(config, remote_conf):
base = config["remote"][parsed.netloc]
url = posixpath.join(base["url"], parsed.path.lstrip("/"))
return {**base, **remote_conf, "url": url}
def get_cloud_tree(repo, **kwargs):
remote_conf = _get_conf(repo, **kwargs)
return _get_tree(remote_conf)(repo, remote_conf)
def get_remote(repo, **kwargs):
tree = get_cloud_tree(repo, **kwargs)
if tree.scheme == "local":
return LocalRemote(tree)
if tree.scheme == "ssh":
return SSHRemote(tree)
return Remote(tree)
+56 -66
View File
@@ -1,63 +1,38 @@
import logging
import os
import re
from datetime import datetime, timedelta
from urllib.parse import urlparse
import threading
from datetime import datetime, timedelta
from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import RemoteBASE
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
class RemoteAZURE(RemoteBASE):
class AzureRemoteTree(BaseRemoteTree):
scheme = Schemes.AZURE
path_cls = CloudURLInfo
REGEX = (
r"azure://((?P<path>[^=;]*)?|("
# backward compatibility
r"(ContainerName=(?P<container_name>[^;]+);?)?"
r"(?P<connection_string>.+)?)?)$"
)
PATH_CLS = CloudURLInfo
REQUIRES = {"azure-storage-blob": "azure.storage.blob"}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
LIST_OBJECT_PAGE_SIZE = 5000
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url", "azure://")
self.path_info = self.PATH_CLS(url)
match = re.match(self.REGEX, url) # backward compatibility
path = match.group("path")
bucket = (
urlparse(url if path else "").netloc
or match.group("container_name") # backward compatibility
or os.getenv("AZURE_STORAGE_CONTAINER_NAME")
)
if not self.path_info.bucket:
container = os.getenv("AZURE_STORAGE_CONTAINER_NAME")
self.path_info = self.PATH_CLS(f"azure://{container}")
self.connection_string = (
config.get("connection_string")
or match.group("connection_string") # backward compatibility
or os.getenv("AZURE_STORAGE_CONNECTION_STRING")
)
if not bucket:
raise ValueError("azure storage container name missing")
if not self.connection_string:
raise ValueError("azure storage connection string missing")
self.path_info = (
self.path_cls(url)
if path
else self.path_cls.from_parts(scheme=self.scheme, netloc=bucket)
self.connection_string = config.get("connection_string") or os.getenv(
"AZURE_STORAGE_CONNECTION_STRING"
)
@wrap_prop(threading.Lock())
@@ -66,12 +41,12 @@ class RemoteAZURE(RemoteBASE):
from azure.storage.blob import BlockBlobService
from azure.common import AzureMissingResourceHttpError
logger.debug("URL {}".format(self.path_info))
logger.debug("Connection string {}".format(self.connection_string))
logger.debug(f"URL {self.path_info}")
logger.debug(f"Connection string {self.connection_string}")
blob_service = BlockBlobService(
connection_string=self.connection_string
)
logger.debug("Container name {}".format(self.path_info.bucket))
logger.debug(f"Container name {self.path_info.bucket}")
try: # verify that container exists
blob_service.list_blobs(
self.path_info.bucket, delimiter="/", num_results=1
@@ -80,12 +55,31 @@ class RemoteAZURE(RemoteBASE):
blob_service.create_container(self.path_info.bucket)
return blob_service
def remove(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
def get_etag(self, path_info):
etag = self.blob_service.get_blob_properties(
path_info.bucket, path_info.path
).properties.etag
return etag.strip('"')
logger.debug("Removing {}".format(path_info))
self.blob_service.delete_blob(path_info.bucket, path_info.path)
def _generate_download_url(self, path_info, expires=3600):
from azure.storage.blob import BlobPermissions
expires_at = datetime.utcnow() + timedelta(seconds=expires)
sas_token = self.blob_service.generate_blob_shared_access_signature(
path_info.bucket,
path_info.path,
permission=BlobPermissions.READ,
expiry=expires_at,
)
download_url = self.blob_service.make_blob_url(
path_info.bucket, path_info.path, sas_token=sas_token
)
return download_url
def exists(self, path_info):
paths = self._list_paths(path_info.bucket, path_info.path)
return any(path_info.path == path for path in paths)
def _list_paths(self, bucket, prefix):
blob_service = self.blob_service
@@ -103,8 +97,24 @@ class RemoteAZURE(RemoteBASE):
next_marker = blobs.next_marker
def list_cache_paths(self):
return self._list_paths(self.path_info.bucket, self.path_info.path)
def walk_files(self, path_info, **kwargs):
for fname in self._list_paths(
path_info.bucket, path_info.path, **kwargs
):
if fname.endswith("/"):
continue
yield path_info.replace(path=fname)
def remove(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
logger.debug(f"Removing {path_info}")
self.blob_service.delete_blob(path_info.bucket, path_info.path)
def get_file_checksum(self, path_info):
return self.get_etag(path_info)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
@@ -127,23 +137,3 @@ class RemoteAZURE(RemoteBASE):
to_file,
progress_callback=pbar.update_to,
)
def exists(self, path_info):
paths = self._list_paths(path_info.bucket, path_info.path)
return any(path_info.path == path for path in paths)
def _generate_download_url(self, path_info, expires=3600):
from azure.storage.blob import BlobPermissions
expires_at = datetime.utcnow() + timedelta(seconds=expires)
sas_token = self.blob_service.generate_blob_shared_access_signature(
path_info.bucket,
path_info.path,
permission=BlobPermissions.READ,
expiry=expires_at,
)
download_url = self.blob_service.make_blob_url(
path_info.bucket, path_info.path, sas_token=sas_token
)
return download_url
+916 -535
View File
File diff suppressed because it is too large Load Diff
+367 -218
View File
@@ -1,54 +1,69 @@
from collections import defaultdict
import io
import logging
import os
import posixpath
import logging
import re
import threading
from collections import defaultdict
from contextlib import contextmanager
from urllib.parse import urlparse
from funcy import retry, wrap_with, wrap_prop, cached_property
from funcy import cached_property, retry, wrap_prop, wrap_with
from funcy.py3 import cat
from dvc.progress import Tqdm
from dvc.scheme import Schemes
from dvc.exceptions import DvcException, FileMissingError
from dvc.path_info import CloudURLInfo
from dvc.remote.base import RemoteBASE
from dvc.exceptions import DvcException
from dvc.utils import tmp_fname, format_link
from dvc.progress import Tqdm
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
from dvc.utils import format_link, tmp_fname
from dvc.utils.stream import IterStream
logger = logging.getLogger(__name__)
FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"
class GDrivePathNotFound(DvcException):
def __init__(self, path_info):
super().__init__("Google Drive path '{}' not found.".format(path_info))
class GDriveAuthError(DvcException):
def __init__(self, cred_location):
if cred_location:
message = (
"GDrive remote auth failed with credentials in '{}'.\n"
"Backup first, remove or fix them, and run DVC again.\n"
"It should do auth again and refresh the credentials.\n\n"
"Details:".format(cred_location)
)
else:
message = "Failed to authenticate GDrive remote"
super().__init__(message)
class GDriveAccessTokenRefreshError(DvcException):
def __init__(self):
super().__init__("Google Drive access token refreshment is failed.")
class GDriveMissedCredentialKeyError(DvcException):
def __init__(self, path):
super().__init__(
"Google Drive user credentials file '{}' "
"misses value for key.".format(path)
)
def gdrive_retry(func):
retry_re = re.compile(r"HttpError (403|500|502|503|504)")
def _gdrive_retry(func):
def should_retry(exc):
from pydrive2.files import ApiRequestError
return isinstance(exc, ApiRequestError) and retry_re.search(str(exc))
if not isinstance(exc, ApiRequestError):
return False
# 15 tries, start at 0.5s, multiply by golden ratio, cap at 20s
error_code = exc.error.get("code", 0)
result = False
if 500 <= error_code < 600:
result = True
if error_code == 403:
result = exc.GetField("reason") in [
"userRateLimitExceeded",
"rateLimitExceeded",
]
if result:
logger.debug(f"Retrying GDrive API call, error: {exc}.")
return result
# 16 tries, start at 0.5s, multiply by golden ratio, cap at 20s
return retry(
15,
16,
timeout=lambda a: min(0.5 * 1.618 ** a, 20),
filter_errors=should_retry,
)(func)
@@ -69,30 +84,36 @@ class GDriveURLInfo(CloudURLInfo):
self._spath = re.sub("/{2,}", "/", self._spath.rstrip("/"))
class RemoteGDrive(RemoteBASE):
class GDriveRemoteTree(BaseRemoteTree):
scheme = Schemes.GDRIVE
path_cls = GDriveURLInfo
PATH_CLS = GDriveURLInfo
REQUIRES = {"pydrive2": "pydrive2"}
DEFAULT_NO_TRAVERSE = False
DEFAULT_VERIFY = True
# Always prefer traverse for GDrive since API usage quotas are a concern.
TRAVERSE_WEIGHT_MULTIPLIER = 1
TRAVERSE_PREFIX_LEN = 2
GDRIVE_CREDENTIALS_DATA = "GDRIVE_CREDENTIALS_DATA"
DEFAULT_USER_CREDENTIALS_FILE = "gdrive-user-credentials.json"
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)
self.path_info = self.path_cls(config["url"])
self.path_info = self.PATH_CLS(config["url"])
if not self.path_info.bucket:
raise DvcException(
"Empty Google Drive URL '{}'. Learn more at "
"{}.".format(
"Empty GDrive URL '{}'. Learn more at {}".format(
config["url"],
format_link("https://man.dvc.org/remote/add"),
)
)
self._bucket = self.path_info.bucket
self._trash_only = config.get("gdrive_trash_only")
self._use_service_account = config.get("gdrive_use_service_account")
self._service_account_email = config.get(
"gdrive_service_account_email"
@@ -108,17 +129,15 @@ class RemoteGDrive(RemoteBASE):
self._validate_config()
self._gdrive_user_credentials_path = (
tmp_fname(os.path.join(self.repo.tmp_dir, ""))
if os.getenv(RemoteGDrive.GDRIVE_CREDENTIALS_DATA)
if os.getenv(GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA)
else config.get(
"gdrive_user_credentials_file",
os.path.join(
self.repo.tmp_dir, self.DEFAULT_USER_CREDENTIALS_FILE
self.repo.tmp_dir, self.DEFAULT_USER_CREDENTIALS_FILE,
),
)
)
self._list_params = None
def _validate_config(self):
# Validate Service Account configuration
if self._use_service_account and (
@@ -126,125 +145,208 @@ class RemoteGDrive(RemoteBASE):
or not self._service_account_p12_file_path
):
raise DvcException(
"To use service account please specify {}, {} and "
"{} in DVC config. Learn more at "
"{}.".format(
"gdrive_service_account_email",
"gdrive_service_account_p12_file_path",
"gdrive_service_account_user_email (optional)",
format_link("https://man.dvc.org/remote/modify"),
"To use service account, set `gdrive_service_account_email`,\n"
"`gdrive_service_account_p12_file_path`, and optionally "
"`gdrive_service_account_user_email`\nin DVC config. "
"Learn more at {}".format(
format_link("https://man.dvc.org/remote/modify")
)
)
# Validate OAuth 2.0 Client ID configuration
if not self._use_service_account and (
not self._client_id or not self._client_secret
):
raise DvcException(
"Please specify Google Drive's client id and "
"secret in DVC config. Learn more at "
"{}.".format(format_link("https://man.dvc.org/remote/modify"))
)
if not self._use_service_account:
if bool(self._client_id) != bool(self._client_secret):
raise DvcException(
"Please specify GDrive's client ID and secret in "
"DVC config or omit both to use the defaults.\n"
"Learn more at {}".format(
format_link("https://man.dvc.org/remote/modify")
)
)
@cached_property
def credentials_location(self):
"""
Helper to determine where will GDrive remote read credentials from.
Useful for tests, exception messages, etc. Returns either env variable
name if it's set or actual path to the credentials file.
"""
if os.getenv(GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA):
return GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA
if os.path.exists(self._gdrive_user_credentials_path):
return self._gdrive_user_credentials_path
return None
@staticmethod
def _validate_credentials(auth, settings):
"""
Detects discrepancy in DVC config and cached credentials file.
Usually happens when a second remote is added and it is using
the same credentials default file. Or when someones decides to change
DVC config client id or secret but forgets to remove the cached
credentials file.
"""
if not os.getenv(GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA):
if (
settings["client_config"]["client_id"]
!= auth.credentials.client_id
or settings["client_config"]["client_secret"]
!= auth.credentials.client_secret
):
logger.warning(
"Client ID and secret configured do not match the "
"actual ones used\nto access the remote. Do you "
"use multiple GDrive remotes and forgot to\nset "
"`gdrive_user_credentials_file` for one or more of them? "
"Learn more at\n{}.\n".format(
format_link("https://man.dvc.org/remote/modify")
)
)
@wrap_prop(threading.RLock())
@cached_property
def drive(self):
from pydrive2.auth import RefreshError
def _drive(self):
from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive
if os.getenv(RemoteGDrive.GDRIVE_CREDENTIALS_DATA):
with open(
self._gdrive_user_credentials_path, "w"
) as credentials_file:
credentials_file.write(
os.getenv(RemoteGDrive.GDRIVE_CREDENTIALS_DATA)
if os.getenv(GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA):
with open(self._gdrive_user_credentials_path, "w") as cred_file:
cred_file.write(
os.getenv(GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA)
)
GoogleAuth.DEFAULT_SETTINGS["client_config_backend"] = "settings"
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": [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.appdata",
],
}
if self._use_service_account:
GoogleAuth.DEFAULT_SETTINGS["service_config"] = {
auth_settings["service_config"] = {
"client_service_email": self._service_account_email,
"client_user_email": self._service_account_user_email,
"client_pkcs12_file_path": self._service_account_p12_file_path,
}
else:
GoogleAuth.DEFAULT_SETTINGS["client_config"] = {
"client_id": self._client_id,
"client_secret": self._client_secret,
auth_settings["client_config"] = {
"client_id": self._client_id or self.DEFAULT_GDRIVE_CLIENT_ID,
"client_secret": self._client_secret
or self.DEFAULT_GDRIVE_CLIENT_SECRET,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"revoke_uri": "https://oauth2.googleapis.com/revoke",
"redirect_uri": "",
}
GoogleAuth.DEFAULT_SETTINGS["save_credentials"] = True
GoogleAuth.DEFAULT_SETTINGS["save_credentials_backend"] = "file"
GoogleAuth.DEFAULT_SETTINGS[
"save_credentials_file"
] = self._gdrive_user_credentials_path
GoogleAuth.DEFAULT_SETTINGS["get_refresh_token"] = True
GoogleAuth.DEFAULT_SETTINGS["oauth_scope"] = [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.appdata",
]
# Pass non existent settings path to force DEFAULT_SETTINGS loading
GoogleAuth.DEFAULT_SETTINGS.update(auth_settings)
# Pass non existent settings path to force DEFAULT_SETTINGS loadings
gauth = GoogleAuth(settings_file="")
try:
logger.debug(
"GDrive remote auth with config '{}'.".format(
GoogleAuth.DEFAULT_SETTINGS
)
)
if self._use_service_account:
gauth.ServiceAuth()
else:
gauth.CommandLineAuth()
except RefreshError as exc:
raise GDriveAccessTokenRefreshError from exc
except KeyError as exc:
raise GDriveMissedCredentialKeyError(
self._gdrive_user_credentials_path
) from exc
# Handle pydrive2.auth.AuthenticationError and other auth failures
GDriveRemoteTree._validate_credentials(gauth, auth_settings)
# Handle AuthenticationError, RefreshError and other auth failures
# It's hard to come up with a narrow exception, since PyDrive throws
# a lot of different errors - broken credentials file, refresh token
# expired, flow failed, etc.
except Exception as exc:
raise DvcException("Google Drive authentication failed") from exc
raise GDriveAuthError(self.credentials_location) from exc
finally:
if os.getenv(RemoteGDrive.GDRIVE_CREDENTIALS_DATA):
if os.getenv(GDriveRemoteTree.GDRIVE_CREDENTIALS_DATA):
os.remove(self._gdrive_user_credentials_path)
return GoogleDrive(gauth)
@wrap_prop(threading.RLock())
@cached_property
def cache(self):
cache = {"dirs": defaultdict(list), "ids": {}}
def _ids_cache(self):
cache = {
"dirs": defaultdict(list),
"ids": {},
"root_id": self._get_item_id(
self.path_info,
use_cache=False,
hint="Confirm the directory exists and you can access it.",
),
}
cache["root_id"] = self._get_remote_id(self.path_info)
cache["dirs"][self.path_info.path] = [cache["root_id"]]
self._cache_path(self.path_info.path, cache["root_id"], cache)
self._cache_path_id(self.path_info.path, cache["root_id"], cache)
for item in self.gdrive_list_item(
for item in self._gdrive_list(
"'{}' in parents and trashed=false".format(cache["root_id"])
):
remote_path = (self.path_info / item["title"]).path
self._cache_path(remote_path, item["id"], cache)
item_path = (self.path_info / item["title"]).path
self._cache_path_id(item_path, item["id"], cache)
return cache
def _cache_path(self, remote_path, remote_id, cache=None):
cache = cache or self.cache
cache["dirs"][remote_path].append(remote_id)
cache["ids"][remote_id] = remote_path
def _cache_path_id(self, path, item_id, cache=None):
cache = cache or self._ids_cache
cache["dirs"][path].append(item_id)
cache["ids"][item_id] = path
@cached_property
def list_params(self):
def _list_params(self):
params = {"corpora": "default"}
if self._bucket != "root" and self._bucket != "appDataFolder":
drive_id = self._get_remote_drive_id(self._bucket)
drive_id = self._gdrive_shared_drive_id(self._bucket)
if drive_id:
logger.debug(
"GDrive remote '{}' is using shared drive id '{}'.".format(
self.path_info, drive_id
)
)
params["driveId"] = drive_id
params["corpora"] = "drive"
return params
@gdrive_retry
def gdrive_upload_file(
@_gdrive_retry
def _gdrive_shared_drive_id(self, item_id):
from pydrive2.files import ApiRequestError
param = {"id": item_id}
# it does not create a file on the remote
item = self._drive.CreateFile(param)
# ID of the shared drive the item resides in.
# Only populated for items in shared drives.
try:
item.FetchMetadata("driveId")
except ApiRequestError as exc:
error_code = exc.error.get("code", 0)
if error_code == 404:
raise DvcException(
"'{}' for '{}':\n\n"
"1. Confirm the directory exists and you can access it.\n"
"2. Make sure that credentials in '{}'\n"
" are correct for this remote e.g. "
"use the `gdrive_user_credentials_file` config\n"
" option if you use multiple GDrive remotes with "
"different email accounts.\n\nDetails".format(
item_id, self.path_info, self.credentials_location
)
) from exc
raise
return item.get("driveId", None)
@_gdrive_retry
def _gdrive_upload_file(
self,
parent_id,
title,
@@ -252,7 +354,7 @@ class RemoteGDrive(RemoteBASE):
from_file="",
progress_name="",
):
item = self.drive.CreateFile(
item = self._drive.CreateFile(
{"title": title, "parents": [{"id": parent_id}]}
)
@@ -272,161 +374,208 @@ class RemoteGDrive(RemoteBASE):
item.Upload()
return item
@gdrive_retry
def gdrive_download_file(
self, file_id, to_file, progress_name, no_progress_bar
@_gdrive_retry
def _gdrive_download_file(
self, item_id, to_file, progress_desc, no_progress_bar
):
param = {"id": file_id}
param = {"id": item_id}
# it does not create a file on the remote
gdrive_file = self.drive.CreateFile(param)
bar_format = (
"Downloading {desc:{ncols_desc}.{ncols_desc}}... "
+ Tqdm.format_sizeof(int(gdrive_file["fileSize"]), "B", 1024)
)
gdrive_file = self._drive.CreateFile(param)
with Tqdm(
bar_format=bar_format, desc=progress_name, disable=no_progress_bar
):
gdrive_file.GetContentFile(to_file)
desc=progress_desc,
disable=no_progress_bar,
bytes=True,
# explicit `bar_format` as `total` will be set by `update_to`
bar_format=Tqdm.BAR_FMT_DEFAULT,
) as pbar:
gdrive_file.GetContentFile(to_file, callback=pbar.update_to)
def gdrive_list_item(self, query):
@contextmanager
@_gdrive_retry
def open(self, path_info, mode="r", encoding=None):
assert mode in {"r", "rt", "rb"}
item_id = self._get_item_id(path_info)
param = {"id": item_id}
# it does not create a file on the remote
gdrive_file = self._drive.CreateFile(param)
fd = gdrive_file.GetContentIOBuffer()
stream = IterStream(iter(fd))
if mode != "rb":
stream = io.TextIOWrapper(stream, encoding=encoding)
yield stream
@_gdrive_retry
def gdrive_delete_file(self, item_id):
from pydrive2.files import ApiRequestError
param = {"id": item_id}
# it does not create a file on the remote
item = self._drive.CreateFile(param)
try:
item.Trash() if self._trash_only else item.Delete()
except ApiRequestError as exc:
http_error_code = exc.error.get("code", 0)
if (
http_error_code == 403
and self._list_params["corpora"] == "drive"
and exc.GetField("location") == "file.permissions"
):
raise DvcException(
"Insufficient permissions to {}. You should have {} "
"access level for the used shared drive. More details "
"at {}.".format(
"move the file into Trash"
if self._trash_only
else "permanently delete the file",
"Manager or Content Manager"
if self._trash_only
else "Manager",
"https://support.google.com/a/answer/7337554",
)
) from exc
raise
def _gdrive_list(self, query):
param = {"q": query, "maxResults": 1000}
param.update(self.list_params)
param.update(self._list_params)
file_list = self._drive.ListFile(param)
file_list = self.drive.ListFile(param)
# Isolate and decorate fetching of remote drive items in pages.
get_list = _gdrive_retry(lambda: next(file_list, None))
# Isolate and decorate fetching of remote drive items in pages
get_list = gdrive_retry(lambda: next(file_list, None))
# Fetch pages until None is received, lazily flatten the thing
# Fetch pages until None is received, lazily flatten the thing.
return cat(iter(get_list, None))
@wrap_with(threading.RLock())
def gdrive_create_dir(self, parent_id, title, remote_path):
cached = self.cache["dirs"].get(remote_path)
if cached:
return cached[0]
item = self._create_remote_dir(parent_id, title)
if parent_id == self.cache["root_id"]:
self._cache_path(remote_path, item["id"])
return item["id"]
@gdrive_retry
def _create_remote_dir(self, parent_id, title):
@_gdrive_retry
def _gdrive_create_dir(self, parent_id, title):
parent = {"id": parent_id}
item = self.drive.CreateFile(
item = self._drive.CreateFile(
{"title": title, "parents": [parent], "mimeType": FOLDER_MIME_TYPE}
)
item.Upload()
return item
@gdrive_retry
def _delete_remote_file(self, remote_id):
param = {"id": remote_id}
# it does not create a file on the remote
item = self.drive.CreateFile(param)
item.Delete()
@wrap_with(threading.RLock())
def _create_dir(self, parent_id, title, remote_path):
cached = self._ids_cache["dirs"].get(remote_path)
if cached:
return cached[0]
@gdrive_retry
def _get_remote_item(self, name, parents_ids):
if not parents_ids:
item = self._gdrive_create_dir(parent_id, title)
if parent_id == self._ids_cache["root_id"]:
self._cache_path_id(remote_path, item["id"])
return item["id"]
def _get_remote_item_ids(self, parent_ids, title):
if not parent_ids:
return None
query = "({})".format(
query = "trashed=false and ({})".format(
" or ".join(
"'{}' in parents".format(parent_id)
for parent_id in parents_ids
f"'{parent_id}' in parents" for parent_id in parent_ids
)
)
query += " and title='{}'".format(title.replace("'", "\\'"))
query += " and trashed=false and title='{}'".format(name)
# GDrive list API is case insensitive, we need to compare
# all results and pick the ones with the right title
return [
item["id"]
for item in self._gdrive_list(query)
if item["title"] == title
]
# Remote might contain items with duplicated path (titles).
# We thus limit number of items.
param = {"q": query, "maxResults": 1}
param.update(self.list_params)
# Limit found remote items count to 1 in response
item_list = self.drive.ListFile(param).GetList()
return next(iter(item_list), None)
@gdrive_retry
def _get_remote_drive_id(self, remote_id):
param = {"id": remote_id}
# it does not create a file on the remote
item = self.drive.CreateFile(param)
item.FetchMetadata("driveId")
return item.get("driveId", None)
def _get_cached_remote_ids(self, path):
def _get_cached_item_ids(self, path, use_cache):
if not path:
return [self._bucket]
if "cache" in self.__dict__:
return self.cache["dirs"].get(path, [])
if use_cache:
return self._ids_cache["dirs"].get(path, [])
return []
def _path_to_remote_ids(self, path, create):
remote_ids = self._get_cached_remote_ids(path)
if remote_ids:
return remote_ids
def _path_to_item_ids(self, path, create, use_cache):
item_ids = self._get_cached_item_ids(path, use_cache)
if item_ids:
return item_ids
parent_path, part = posixpath.split(path)
parent_ids = self._path_to_remote_ids(parent_path, create)
item = self._get_remote_item(part, parent_ids)
parent_path, title = posixpath.split(path)
parent_ids = self._path_to_item_ids(parent_path, create, use_cache)
item_ids = self._get_remote_item_ids(parent_ids, title)
if item_ids:
return item_ids
if not item:
return (
[self.gdrive_create_dir(parent_ids[0], part, path)]
if create
else []
)
return (
[self._create_dir(min(parent_ids), title, path)] if create else []
)
return [item["id"]]
def _get_remote_id(self, path_info, create=False):
def _get_item_id(self, path_info, create=False, use_cache=True, hint=None):
assert path_info.bucket == self._bucket
remote_ids = self._path_to_remote_ids(path_info.path, create)
if not remote_ids:
raise GDrivePathNotFound(path_info)
item_ids = self._path_to_item_ids(path_info.path, create, use_cache)
if item_ids:
return min(item_ids)
return remote_ids[0]
assert not create
raise FileMissingError(path_info, hint)
def exists(self, path_info):
try:
self._get_remote_id(path_info)
except GDrivePathNotFound:
self._get_item_id(path_info)
except FileMissingError:
return False
else:
return True
def _upload(self, from_file, to_info, name, no_progress_bar):
def _list_paths(self, prefix=None):
if not self._ids_cache["ids"]:
return
if prefix:
dir_ids = self._ids_cache["dirs"].get(prefix[:2])
if not dir_ids:
return
else:
dir_ids = self._ids_cache["ids"]
parents_query = " or ".join(
f"'{dir_id}' in parents" for dir_id in dir_ids
)
query = f"({parents_query}) and trashed=false"
for item in self._gdrive_list(query):
parent_id = item["parents"][0]["id"]
yield posixpath.join(
self._ids_cache["ids"][parent_id], item["title"]
)
def walk_files(self, path_info, **kwargs):
if path_info == self.path_info:
prefix = None
else:
prefix = path_info.path
for fname in self._list_paths(prefix=prefix, **kwargs):
yield path_info.replace(fname)
def remove(self, path_info):
item_id = self._get_item_id(path_info)
self.gdrive_delete_file(item_id)
def get_file_checksum(self, path_info):
raise NotImplementedError
def _upload(self, from_file, to_info, name=None, no_progress_bar=False):
dirname = to_info.parent
assert dirname
parent_id = self._get_remote_id(dirname, True)
parent_id = self._get_item_id(dirname, True)
self.gdrive_upload_file(
self._gdrive_upload_file(
parent_id, to_info.name, no_progress_bar, from_file, name
)
def _download(self, from_info, to_file, name, no_progress_bar):
file_id = self._get_remote_id(from_info)
self.gdrive_download_file(file_id, to_file, name, no_progress_bar)
def list_cache_paths(self):
if not self.cache["ids"]:
return
parents_query = " or ".join(
"'{}' in parents".format(dir_id) for dir_id in self.cache["ids"]
)
query = "({}) and trashed=false".format(parents_query)
for item in self.gdrive_list_item(query):
parent_id = item["parents"][0]["id"]
yield posixpath.join(self.cache["ids"][parent_id], item["title"])
def remove(self, path_info):
remote_id = self._get_remote_id(path_info)
self._delete_remote_file(remote_id)
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
item_id = self._get_item_id(from_info)
self._gdrive_download_file(item_id, to_file, name, no_progress_bar)
+78 -79
View File
@@ -1,16 +1,15 @@
import logging
from datetime import timedelta
from functools import wraps
import io
import os.path
import threading
from datetime import timedelta
from functools import wraps
from funcy import cached_property, wrap_prop
from dvc.exceptions import DvcException
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import RemoteBASE
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
@@ -54,7 +53,7 @@ def _upload_to_bucket(
no_progress_bar=False,
):
blob = bucket.blob(to_info.path, chunk_size=chunk_size)
with io.open(from_file, mode="rb") as fobj:
with open(from_file, mode="rb") as fobj:
with Tqdm.wrapattr(
fobj,
"read",
@@ -65,9 +64,9 @@ def _upload_to_bucket(
blob.upload_from_file(wrapped)
class RemoteGS(RemoteBASE):
class GSRemoteTree(BaseRemoteTree):
scheme = Schemes.GS
path_cls = CloudURLInfo
PATH_CLS = CloudURLInfo
REQUIRES = {"google-cloud-storage": "google.cloud.storage"}
PARAM_CHECKSUM = "md5"
@@ -75,7 +74,7 @@ class RemoteGS(RemoteBASE):
super().__init__(repo, config)
url = config.get("url", "gs:///")
self.path_info = self.path_cls(url)
self.path_info = self.PATH_CLS(url)
self.projectname = config.get("projectname", None)
self.credentialpath = config.get("credentialpath")
@@ -91,6 +90,76 @@ class RemoteGS(RemoteBASE):
else Client(self.projectname)
)
def _generate_download_url(self, path_info, expires=3600):
expiration = timedelta(seconds=int(expires))
bucket = self.gs.bucket(path_info.bucket)
blob = bucket.get_blob(path_info.path)
if blob is None:
raise FileNotFoundError
return blob.generate_signed_url(expiration=expiration)
def exists(self, path_info):
"""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):
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
blob = self.gs.bucket(path_info.bucket).blob(path_info.path)
return blob.exists()
def _list_paths(self, path_info, max_items=None):
for blob in self.gs.bucket(path_info.bucket).list_blobs(
prefix=path_info.path, max_results=max_items
):
yield blob.name
def walk_files(self, path_info, **kwargs):
for fname in self._list_paths(path_info / "", **kwargs):
# skip nested empty directories
if fname.endswith("/"):
continue
yield path_info.replace(fname)
def remove(self, path_info):
if path_info.scheme != "gs":
raise NotImplementedError
logger.debug(f"Removing gs://{path_info}")
blob = self.gs.bucket(path_info.bucket).get_blob(path_info.path)
if not blob:
return
blob.delete()
def makedirs(self, path_info):
if not path_info.path:
return
self.gs.bucket(path_info.bucket).blob(
(path_info / "").path
).upload_from_string("")
def copy(self, from_info, to_info):
from_bucket = self.gs.bucket(from_info.bucket)
blob = from_bucket.get_blob(from_info.path)
if not blob:
msg = f"'{from_info.path}' doesn't exist in the cloud"
raise DvcException(msg)
to_bucket = self.gs.bucket(to_info.bucket)
from_bucket.copy_blob(blob, to_bucket, new_name=to_info.path)
def get_file_checksum(self, path_info):
import base64
import codecs
@@ -105,67 +174,6 @@ class RemoteGS(RemoteBASE):
md5 = base64.b64decode(b64_md5)
return codecs.getencoder("hex")(md5)[0].decode("utf-8")
def copy(self, from_info, to_info):
from_bucket = self.gs.bucket(from_info.bucket)
blob = from_bucket.get_blob(from_info.path)
if not blob:
msg = "'{}' doesn't exist in the cloud".format(from_info.path)
raise DvcException(msg)
to_bucket = self.gs.bucket(to_info.bucket)
from_bucket.copy_blob(blob, to_bucket, new_name=to_info.path)
def remove(self, path_info):
if path_info.scheme != "gs":
raise NotImplementedError
logger.debug("Removing gs://{}".format(path_info))
blob = self.gs.bucket(path_info.bucket).get_blob(path_info.path)
if not blob:
return
blob.delete()
def _list_paths(self, path_info, max_items=None):
for blob in self.gs.bucket(path_info.bucket).list_blobs(
prefix=path_info.path, max_results=max_items
):
yield blob.name
def list_cache_paths(self):
return self._list_paths(self.path_info)
def walk_files(self, path_info):
for fname in self._list_paths(path_info / ""):
# skip nested empty directories
if fname.endswith("/"):
continue
yield path_info.replace(fname)
def makedirs(self, path_info):
self.gs.bucket(path_info.bucket).blob(
(path_info / "").path
).upload_from_string("")
def isdir(self, path_info):
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
blob = self.gs.bucket(path_info.bucket).blob(path_info.path)
return blob.exists()
def exists(self, path_info):
"""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 _upload(self, from_file, to_info, name=None, no_progress_bar=False):
bucket = self.gs.bucket(to_info.bucket)
_upload_to_bucket(
@@ -179,7 +187,7 @@ class RemoteGS(RemoteBASE):
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
bucket = self.gs.bucket(from_info.bucket)
blob = bucket.get_blob(from_info.path)
with io.open(to_file, mode="wb") as fobj:
with open(to_file, mode="wb") as fobj:
with Tqdm.wrapattr(
fobj,
"write",
@@ -188,12 +196,3 @@ class RemoteGS(RemoteBASE):
disable=no_progress_bar,
) as wrapped:
blob.download_to_file(wrapped)
def _generate_download_url(self, path_info, expires=3600):
expiration = timedelta(seconds=int(expires))
bucket = self.gs.bucket(path_info.bucket)
blob = bucket.get_blob(path_info.path)
if blob is None:
raise FileNotFoundError
return blob.generate_signed_url(expiration=expiration)
+86 -72
View File
@@ -8,22 +8,25 @@ from collections import deque
from contextlib import closing, contextmanager
from urllib.parse import urlparse
from .base import RemoteBASE, RemoteCmdError
from .pool import get_connection
from dvc.scheme import Schemes
from dvc.utils import fix_env, tmp_fname
from .base import BaseRemoteTree, RemoteCmdError
from .pool import get_connection
logger = logging.getLogger(__name__)
class RemoteHDFS(RemoteBASE):
class HDFSRemoteTree(BaseRemoteTree):
scheme = Schemes.HDFS
REQUIRES = {"pyarrow": "pyarrow"}
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
PARAM_CHECKSUM = "checksum"
REQUIRES = {"pyarrow": "pyarrow"}
TRAVERSE_PREFIX_LEN = 2
def __init__(self, repo, config):
super().__init__(repo, config)
self.path_info = None
url = config.get("url")
if not url:
@@ -32,7 +35,7 @@ class RemoteHDFS(RemoteBASE):
parsed = urlparse(url)
user = parsed.username or config.get("user")
self.path_info = self.path_cls.from_parts(
self.path_info = self.PATH_CLS.from_parts(
scheme=self.scheme,
host=parsed.hostname,
user=user,
@@ -40,7 +43,8 @@ class RemoteHDFS(RemoteBASE):
path=parsed.path,
)
def hdfs(self, path_info):
@staticmethod
def hdfs(path_info):
import pyarrow
return get_connection(
@@ -50,10 +54,84 @@ class RemoteHDFS(RemoteBASE):
user=path_info.user,
)
@contextmanager
def open(self, path_info, mode="r", encoding=None):
assert mode in {"r", "rt", "rb"}
try:
with self.hdfs(path_info) as hdfs, closing(
hdfs.open(path_info.path, mode="rb")
) as fd:
if mode == "rb":
yield fd
else:
yield io.TextIOWrapper(fd, encoding=encoding)
except OSError as e:
# Empty .errno and not specific enough error class in pyarrow,
# see https://issues.apache.org/jira/browse/ARROW-6248
if "file does not exist" in str(e):
raise FileNotFoundError(*e.args)
raise
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info.scheme == "hdfs"
with self.hdfs(path_info) as hdfs:
return hdfs.exists(path_info.path)
def walk_files(self, path_info, **kwargs):
if not self.exists(path_info):
return
root = path_info.path
dirs = deque([root])
with self.hdfs(self.path_info) as hdfs:
if not hdfs.exists(root):
return
while dirs:
try:
entries = hdfs.ls(dirs.pop(), detail=True)
for entry in entries:
if entry["kind"] == "directory":
dirs.append(urlparse(entry["name"]).path)
elif entry["kind"] == "file":
path = urlparse(entry["name"]).path
yield path_info.replace(path=path)
except OSError:
# When searching for a specific prefix pyarrow raises an
# exception if the specified cache dir does not exist
pass
def remove(self, path_info):
if path_info.scheme != "hdfs":
raise NotImplementedError
if self.exists(path_info):
logger.debug(f"Removing {path_info.path}")
with self.hdfs(path_info) as hdfs:
hdfs.rm(path_info.path)
def copy(self, from_info, to_info, **_kwargs):
dname = posixpath.dirname(to_info.path)
with self.hdfs(to_info) as hdfs:
hdfs.mkdir(dname)
# NOTE: this is how `hadoop fs -cp` works too: it copies through
# your local machine.
with hdfs.open(from_info.path, "rb") as from_fobj:
tmp_info = to_info.parent / tmp_fname(to_info.name)
try:
with hdfs.open(tmp_info.path, "wb") as tmp_fobj:
tmp_fobj.upload(from_fobj)
hdfs.rename(tmp_info.path, to_info.path)
except Exception:
self.remove(tmp_info)
raise
def hadoop_fs(self, cmd, user=None):
cmd = "hadoop fs -" + cmd
if user:
cmd = "HADOOP_USER_NAME={} ".format(user) + cmd
cmd = f"HADOOP_USER_NAME={user} " + cmd
# NOTE: close_fds doesn't work with redirected stdin/stdout/stderr.
# See https://github.com/iterative/dvc/issues/1197.
@@ -85,41 +163,10 @@ class RemoteHDFS(RemoteBASE):
# NOTE: pyarrow doesn't support checksum, so we need to use hadoop
regex = r".*\t.*\t(?P<checksum>.*)"
stdout = self.hadoop_fs(
"checksum {}".format(path_info.path), user=path_info.user
f"checksum {path_info.path}", user=path_info.user
)
return self._group(regex, stdout, "checksum")
def copy(self, from_info, to_info, **_kwargs):
dname = posixpath.dirname(to_info.path)
with self.hdfs(to_info) as hdfs:
hdfs.mkdir(dname)
# NOTE: this is how `hadoop fs -cp` works too: it copies through
# your local machine.
with hdfs.open(from_info.path, "rb") as from_fobj:
tmp_info = to_info.parent / tmp_fname(to_info.name)
try:
with hdfs.open(tmp_info.path, "wb") as tmp_fobj:
tmp_fobj.upload(from_fobj)
hdfs.rename(tmp_info.path, to_info.path)
except Exception:
self.remove(tmp_info)
raise
def remove(self, path_info):
if path_info.scheme != "hdfs":
raise NotImplementedError
if self.exists(path_info):
logger.debug("Removing {}".format(path_info.path))
with self.hdfs(path_info) as hdfs:
hdfs.rm(path_info.path)
def exists(self, path_info):
assert not isinstance(path_info, list)
assert path_info.scheme == "hdfs"
with self.hdfs(path_info) as hdfs:
return hdfs.exists(path_info.path)
def _upload(self, from_file, to_info, **_kwargs):
with self.hdfs(to_info) as hdfs:
hdfs.mkdir(posixpath.dirname(to_info.path))
@@ -132,36 +179,3 @@ class RemoteHDFS(RemoteBASE):
with self.hdfs(from_info) as hdfs:
with open(to_file, "wb+") as fobj:
hdfs.download(from_info.path, fobj)
@contextmanager
def open(self, path_info, mode="r", encoding=None):
assert mode in {"r", "rt", "rb"}
try:
with self.hdfs(path_info) as hdfs, closing(
hdfs.open(path_info.path, mode="rb")
) as fd:
if mode == "rb":
yield fd
else:
yield io.TextIOWrapper(fd, encoding=encoding)
except IOError as e:
# Empty .errno and not specific enough error class in pyarrow,
# see https://issues.apache.org/jira/browse/ARROW-6248
if "file does not exist" in str(e):
raise FileNotFoundError(*e.args)
raise
def list_cache_paths(self):
if not self.exists(self.path_info):
return
dirs = deque([self.path_info.path])
with self.hdfs(self.path_info) as hdfs:
while dirs:
for entry in hdfs.ls(dirs.pop(), detail=True):
if entry["kind"] == "directory":
dirs.append(urlparse(entry["name"]).path)
elif entry["kind"] == "file":
yield urlparse(entry["name"]).path
+79 -83
View File
@@ -4,12 +4,11 @@ import threading
from funcy import cached_property, memoize, wrap_prop, wrap_with
from dvc.path_info import HTTPURLInfo
import dvc.prompt as prompt
from dvc.config import ConfigError
from dvc.exceptions import DvcException, HTTPError
from dvc.path_info import HTTPURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import RemoteBASE
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
@@ -24,104 +23,36 @@ def ask_password(host, user):
)
class RemoteHTTP(RemoteBASE):
class HTTPRemoteTree(BaseRemoteTree):
scheme = Schemes.HTTP
path_cls = HTTPURLInfo
PATH_CLS = HTTPURLInfo
PARAM_CHECKSUM = "etag"
CAN_TRAVERSE = False
SESSION_RETRIES = 5
SESSION_BACKOFF_FACTOR = 0.1
REQUEST_TIMEOUT = 10
CHUNK_SIZE = 2 ** 16
PARAM_CHECKSUM = "etag"
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url")
if url:
self.path_info = self.path_cls(url)
self.path_info = self.PATH_CLS(url)
user = config.get("user", None)
if user:
self.path_info.user = user
else:
self.path_info = None
if not self.no_traverse:
raise ConfigError(
"HTTP doesn't support traversing the remote to list existing "
"files. Use: `dvc remote modify <name> no_traverse true`"
)
self.auth = config.get("auth", None)
self.custom_auth_header = config.get("custom_auth_header", None)
self.password = config.get("password", None)
self.ask_password = config.get("ask_password", False)
self.headers = {}
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
response = self._request("GET", from_info.url, stream=True)
if response.status_code != 200:
raise HTTPError(response.status_code, response.reason)
with Tqdm(
total=None if no_progress_bar else self._content_length(response),
leave=False,
bytes=True,
desc=from_info.url if name is None else name,
disable=no_progress_bar,
) as pbar:
with open(to_file, "wb") as fd:
for chunk in response.iter_content(chunk_size=self.CHUNK_SIZE):
fd.write(chunk)
pbar.update(len(chunk))
def _upload(self, from_file, to_info, name=None, no_progress_bar=False):
with Tqdm(
total=None if no_progress_bar else os.path.getsize(from_file),
leave=False,
bytes=True,
desc=to_info.url if name is None else name,
disable=no_progress_bar,
) as pbar:
def chunks():
with open(from_file, "rb") as fd:
while True:
chunk = fd.read(self.CHUNK_SIZE)
if not chunk:
break
pbar.update(len(chunk))
yield chunk
response = self._request("POST", to_info.url, data=chunks())
if response.status_code not in (200, 201):
raise HTTPError(response.status_code, response.reason)
def exists(self, path_info):
return bool(self._request("HEAD", path_info.url))
def _content_length(self, response):
res = response.headers.get("Content-Length")
return int(res) if res else None
def get_file_checksum(self, path_info):
url = path_info.url
headers = self._request("HEAD", url).headers
etag = headers.get("ETag") or headers.get("Content-MD5")
if not etag:
raise DvcException(
"could not find an ETag or "
"Content-MD5 header for '{url}'".format(url=url)
)
if etag.startswith("W/"):
raise DvcException(
"Weak ETags are not supported."
" (Etag: '{etag}', URL: '{url}')".format(etag=etag, url=url)
)
return etag
def auth_method(self, path_info=None):
def _auth_method(self, path_info=None):
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
if path_info is None:
@@ -158,7 +89,7 @@ class RemoteHTTP(RemoteBASE):
return session
def _request(self, method, url, **kwargs):
def request(self, method, url, **kwargs):
import requests
kwargs.setdefault("allow_redirects", True)
@@ -168,7 +99,7 @@ class RemoteHTTP(RemoteBASE):
res = self._session.request(
method,
url,
auth=self.auth_method(),
auth=self._auth_method(),
headers=self.headers,
**kwargs,
)
@@ -188,7 +119,72 @@ class RemoteHTTP(RemoteBASE):
return res
except requests.exceptions.RequestException:
raise DvcException("could not perform a {} request".format(method))
raise DvcException(f"could not perform a {method} request")
def gc(self):
raise NotImplementedError
def exists(self, path_info):
return bool(self.request("HEAD", path_info.url))
def get_file_checksum(self, path_info):
url = path_info.url
headers = self.request("HEAD", url).headers
etag = headers.get("ETag") or headers.get("Content-MD5")
if not etag:
raise DvcException(
"could not find an ETag or "
"Content-MD5 header for '{url}'".format(url=url)
)
if etag.startswith("W/"):
raise DvcException(
"Weak ETags are not supported."
" (Etag: '{etag}', URL: '{url}')".format(etag=etag, url=url)
)
return etag
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
response = self.request("GET", from_info.url, stream=True)
if response.status_code != 200:
raise HTTPError(response.status_code, response.reason)
with open(to_file, "wb") as fd:
with Tqdm.wrapattr(
fd,
"write",
total=None
if no_progress_bar
else self._content_length(response),
leave=False,
desc=from_info.url if name is None else name,
disable=no_progress_bar,
) as fd_wrapped:
for chunk in response.iter_content(chunk_size=self.CHUNK_SIZE):
fd_wrapped.write(chunk)
def _upload(self, from_file, to_info, name=None, no_progress_bar=False):
def chunks():
with open(from_file, "rb") as fd:
with Tqdm.wrapattr(
fd,
"read",
total=None
if no_progress_bar
else os.path.getsize(from_file),
leave=False,
desc=to_info.url if name is None else name,
disable=no_progress_bar,
) as fd_wrapped:
while True:
chunk = fd_wrapped.read(self.CHUNK_SIZE)
if not chunk:
break
yield chunk
response = self.request("POST", to_info.url, data=chunks())
if response.status_code not in (200, 201):
raise HTTPError(response.status_code, response.reason)
@staticmethod
def _content_length(response):
res = response.headers.get("Content-Length")
return int(res) if res else None
+3 -2
View File
@@ -1,6 +1,7 @@
from .http import RemoteHTTP
from dvc.scheme import Schemes
from .http import HTTPRemoteTree
class RemoteHTTPS(RemoteHTTP):
class HTTPSRemoteTree(HTTPRemoteTree):
scheme = Schemes.HTTPS
+212
View File
@@ -0,0 +1,212 @@
import logging
import os
import sqlite3
import threading
from funcy import lchunks
from dvc.state import _connect_sqlite
logger = logging.getLogger(__name__)
class RemoteIndexNoop:
"""No-op class for remotes which are not indexed (i.e. local)."""
def __init__(self, *args, **kwargs):
pass
def __enter__(self):
pass
def __exit__(self, typ, value, tbck):
pass
def __iter__(self):
return iter([])
def __contains__(self, checksum):
return False
@staticmethod
def checksums():
return []
@staticmethod
def dir_checksums():
return []
def load(self):
pass
def dump(self):
pass
def clear(self):
pass
def update(self, *args):
pass
@staticmethod
def intersection(*args):
return []
class RemoteIndex:
"""Class for indexing remote checksums in a sqlite3 database.
Args:
repo: repo for this remote index.
name: name for this index. Index db will be loaded from and saved to
``.dvc/tmp/index/{name}.idx``.
dir_suffix: suffix used for naming directory checksums
"""
INDEX_SUFFIX = ".idx"
VERSION = 1
INDEX_TABLE = "remote_index"
INDEX_TABLE_LAYOUT = "checksum TEXT PRIMARY KEY, " "dir INTEGER NOT NULL"
def __init__(self, repo, name, dir_suffix=".dir"):
self.path = os.path.join(repo.index_dir, f"{name}{self.INDEX_SUFFIX}")
self.dir_suffix = dir_suffix
self.database = None
self.cursor = None
self.modified = False
self.lock = threading.Lock()
def __iter__(self):
cmd = f"SELECT checksum FROM {self.INDEX_TABLE}"
for (checksum,) in self._execute(cmd):
yield checksum
def __enter__(self):
self.lock.acquire()
self.load()
def __exit__(self, typ, value, tbck):
self.dump()
self.lock.release()
def __contains__(self, checksum):
cmd = "SELECT checksum FROM {} WHERE checksum = (?)".format(
self.INDEX_TABLE
)
self._execute(cmd, (checksum,))
return self.cursor.fetchone() is not None
def checksums(self):
"""Iterate over checksums stored in the index."""
return iter(self)
def dir_checksums(self):
"""Iterate over .dir checksums stored in the index."""
cmd = f"SELECT checksum FROM {self.INDEX_TABLE} WHERE dir = 1"
for (checksum,) in self._execute(cmd):
yield checksum
def is_dir_checksum(self, checksum):
return checksum.endswith(self.dir_suffix)
def _execute(self, cmd, parameters=()):
return self.cursor.execute(cmd, parameters)
def _executemany(self, cmd, seq_of_parameters):
return self.cursor.executemany(cmd, seq_of_parameters)
def _prepare_db(self, empty=False):
if not empty:
cmd = "PRAGMA user_version;"
self._execute(cmd)
ret = self.cursor.fetchall()
assert len(ret) == 1
assert len(ret[0]) == 1
assert isinstance(ret[0][0], int)
version = ret[0][0]
if version != self.VERSION:
logger.error(
"Index file version '{}' will be reformatted "
"to the current version '{}'.".format(
version, self.VERSION,
)
)
cmd = "DROP TABLE IF EXISTS {};"
self._execute(cmd.format(self.INDEX_TABLE))
cmd = "CREATE TABLE IF NOT EXISTS {} ({})"
self._execute(cmd.format(self.INDEX_TABLE, self.INDEX_TABLE_LAYOUT))
cmd = "PRAGMA user_version = {};"
self._execute(cmd.format(self.VERSION))
def load(self):
"""(Re)load this index database."""
retries = 1
while True:
assert self.database is None
assert self.cursor is None
empty = not os.path.isfile(self.path)
self.database = _connect_sqlite(self.path, {"nolock": 1})
self.cursor = self.database.cursor()
try:
self._prepare_db(empty=empty)
return
except sqlite3.DatabaseError:
self.cursor.close()
self.database.close()
self.database = None
self.cursor = None
if retries > 0:
os.unlink(self.path)
retries -= 1
else:
raise
def dump(self):
"""Save this index database."""
assert self.database is not None
self.database.commit()
self.cursor.close()
self.database.close()
self.database = None
self.cursor = None
def clear(self):
"""Clear this index (to force re-indexing later).
Changes to the index will not committed until dump() is called.
"""
cmd = f"DELETE FROM {self.INDEX_TABLE}"
self._execute(cmd)
def update(self, dir_checksums, file_checksums):
"""Update this index, adding the specified checksums.
Changes to the index will not committed until dump() is called.
"""
cmd = "INSERT OR IGNORE INTO {} (checksum, dir) VALUES (?, ?)".format(
self.INDEX_TABLE
)
self._executemany(
cmd, ((checksum, True) for checksum in dir_checksums)
)
self._executemany(
cmd, ((checksum, False) for checksum in file_checksums)
)
def intersection(self, checksums):
"""Iterate over values from `checksums` which exist in the index."""
# sqlite has a compile time limit of 999, see:
# https://www.sqlite.org/c3ref/c_limit_attached.html#sqlitelimitvariablenumber
for chunk in lchunks(999, checksums):
cmd = "SELECT checksum FROM {} WHERE checksum IN ({})".format(
self.INDEX_TABLE, ",".join("?" for checksum in chunk)
)
for (checksum,) in self._execute(cmd, chunk):
yield checksum
+469 -262
View File
@@ -2,93 +2,99 @@ import errno
import logging
import os
import stat
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import partial, wraps
from funcy import cached_property, concat
from shortuuid import uuid
from dvc.compat import fspath_py35
from dvc.exceptions import DvcException, DownloadError, UploadError
from dvc.exceptions import DownloadError, DvcException, UploadError
from dvc.path_info import PathInfo
from dvc.progress import Tqdm
from dvc.remote.base import RemoteBASE, STATUS_MAP
from dvc.remote.base import STATUS_DELETED, STATUS_MISSING, STATUS_NEW
from dvc.remote.base import (
STATUS_DELETED,
STATUS_MAP,
STATUS_MISSING,
STATUS_NEW,
BaseRemoteTree,
CloudCache,
Remote,
index_locked,
)
from dvc.remote.index import RemoteIndexNoop
from dvc.scheme import Schemes
from dvc.scm.tree import is_working_tree
from dvc.scm.tree import WorkingTree, is_working_tree
from dvc.system import System
from dvc.utils import file_md5, relpath, tmp_fname
from dvc.utils.fs import copyfile, move, makedirs, remove, walk_files
from dvc.utils.fs import (
copy_fobj_to_file,
copyfile,
makedirs,
move,
remove,
walk_files,
)
logger = logging.getLogger(__name__)
class RemoteLOCAL(RemoteBASE):
class LocalRemoteTree(BaseRemoteTree):
scheme = Schemes.LOCAL
path_cls = PathInfo
PATH_CLS = PathInfo
PARAM_CHECKSUM = "md5"
PARAM_PATH = "path"
TRAVERSE_PREFIX_LEN = 2
UNPACKED_DIR_SUFFIX = ".unpacked"
DEFAULT_CACHE_TYPES = ["reflink", "copy"]
CACHE_MODE = 0o444
SHARED_MODE_MAP = {None: (0o644, 0o755), "group": (0o664, 0o775)}
def __init__(self, repo, config):
super().__init__(repo, config)
self.cache_dir = config.get("url")
self._dir_info = {}
url = config.get("url")
self.path_info = self.PATH_CLS(url) if url else None
@property
def state(self):
return self.repo.state
@property
def cache_dir(self):
return self.path_info.fspath if self.path_info else None
@cache_dir.setter
def cache_dir(self, value):
self.path_info = PathInfo(value) if value else None
@classmethod
def supported(cls, config):
return True
def list_cache_paths(self):
assert self.path_info is not None
return walk_files(self.path_info)
def get(self, md5):
if not md5:
return None
return self.checksum_to_path_info(md5).url
@cached_property
def work_tree(self):
# When using repo.brancher, repo.tree may change to/from WorkingTree to
# GitTree arbitarily. When repo.tree is GitTree, local cache needs to
# use its own WorkingTree instance.
if self.repo:
return WorkingTree(self.repo.root_dir)
return None
@staticmethod
def exists(path_info):
assert path_info.scheme == "local"
return os.path.lexists(fspath_py35(path_info))
def open(path_info, mode="r", encoding=None):
return open(path_info, mode=mode, encoding=encoding)
def makedirs(self, path_info):
makedirs(path_info, exist_ok=True, mode=self._dir_mode)
def exists(self, path_info):
assert isinstance(path_info, str) or path_info.scheme == "local"
if not self.repo:
return os.path.exists(path_info)
return self.work_tree.exists(path_info)
def already_cached(self, path_info):
assert path_info.scheme in ["", "local"]
def isfile(self, path_info):
if not self.repo:
return os.path.isfile(path_info)
return self.work_tree.isfile(path_info)
current_md5 = self.get_checksum(path_info)
def isdir(self, path_info):
if not self.repo:
return os.path.isdir(path_info)
return self.work_tree.isdir(path_info)
if not current_md5:
return False
def iscopy(self, path_info):
return not (
System.is_symlink(path_info) or System.is_hardlink(path_info)
)
return not self.changed_cache(current_md5)
def _verify_link(self, path_info, link_type):
if link_type == "hardlink" and self.getsize(path_info) == 0:
return
super()._verify_link(path_info, link_type)
def walk_files(self, path_info, **kwargs):
for fname in self.work_tree.walk_files(path_info):
yield PathInfo(fname)
def is_empty(self, path_info):
path = path_info.fspath
@@ -101,38 +107,19 @@ class RemoteLOCAL(RemoteBASE):
return False
@staticmethod
def isfile(path_info):
return os.path.isfile(fspath_py35(path_info))
@staticmethod
def isdir(path_info):
return os.path.isdir(fspath_py35(path_info))
def iscopy(self, path_info):
return not (
System.is_symlink(path_info) or System.is_hardlink(path_info)
)
@staticmethod
def getsize(path_info):
return os.path.getsize(fspath_py35(path_info))
def walk_files(self, path_info):
assert is_working_tree(self.repo.tree)
for fname in self.repo.tree.walk_files(path_info):
yield PathInfo(fname)
def get_file_checksum(self, path_info):
return file_md5(path_info)[0]
def remove(self, path_info):
if path_info.scheme != "local":
raise NotImplementedError
if isinstance(path_info, PathInfo):
if path_info.scheme != "local":
raise NotImplementedError
path = path_info.fspath
else:
path = path_info
if self.exists(path_info):
remove(path_info.fspath)
if self.exists(path):
remove(path)
def makedirs(self, path_info):
makedirs(path_info, exist_ok=True, mode=self.dir_mode)
def move(self, from_info, to_info, mode=None):
if from_info.scheme != "local" or to_info.scheme != "local":
@@ -142,9 +129,9 @@ class RemoteLOCAL(RemoteBASE):
if mode is None:
if self.isfile(from_info):
mode = self._file_mode
mode = self.file_mode
else:
mode = self._dir_mode
mode = self.dir_mode
move(from_info, to_info, mode=mode)
@@ -152,8 +139,19 @@ class RemoteLOCAL(RemoteBASE):
tmp_info = to_info.parent / tmp_fname(to_info.name)
try:
System.copy(from_info, tmp_info)
os.chmod(fspath_py35(tmp_info), self._file_mode)
os.rename(fspath_py35(tmp_info), fspath_py35(to_info))
os.chmod(tmp_info, self.file_mode)
os.rename(tmp_info, to_info)
except Exception:
self.remove(tmp_info)
raise
def copy_fobj(self, fobj, to_info):
self.makedirs(to_info.parent)
tmp_info = to_info.parent / tmp_fname(to_info.name)
try:
copy_fobj_to_file(fobj, tmp_info)
os.chmod(tmp_info, self.file_mode)
os.rename(tmp_info, to_info)
except Exception:
self.remove(tmp_info)
raise
@@ -201,10 +199,183 @@ class RemoteLOCAL(RemoteBASE):
System.reflink(from_info, tmp_info)
# NOTE: reflink has its own separate inode, so you can set permissions
# that are different from the source.
os.chmod(fspath_py35(tmp_info), self._file_mode)
os.rename(fspath_py35(tmp_info), fspath_py35(to_info))
os.chmod(tmp_info, self.file_mode)
os.rename(tmp_info, to_info)
def cache_exists(self, checksums, jobs=None, name=None):
def _unprotect_file(self, path):
if System.is_symlink(path) or System.is_hardlink(path):
logger.debug(f"Unprotecting '{path}'")
tmp = os.path.join(os.path.dirname(path), "." + uuid())
# The operations order is important here - if some application
# would access the file during the process of copyfile then it
# would get only the part of file. So, at first, the file should be
# copied with the temporary name, and then original file should be
# replaced by new.
copyfile(path, tmp, name="Unprotecting '{}'".format(relpath(path)))
remove(path)
os.rename(tmp, path)
else:
logger.debug(
"Skipping copying for '{}', since it is not "
"a symlink or a hardlink.".format(path)
)
os.chmod(path, self.file_mode)
def _unprotect_dir(self, path):
assert is_working_tree(self.repo.tree)
for fname in self.repo.tree.walk_files(path):
self._unprotect_file(fname)
def unprotect(self, path_info):
path = path_info.fspath
if not os.path.exists(path):
raise DvcException(f"can't unprotect non-existing data '{path}'")
if os.path.isdir(path):
self._unprotect_dir(path)
else:
self._unprotect_file(path)
def protect(self, path_info):
path = os.fspath(path_info)
mode = self.CACHE_MODE
try:
os.chmod(path, mode)
except OSError as exc:
# There is nothing we need to do in case of a read-only file system
if exc.errno == errno.EROFS:
return
# In shared cache scenario, we might not own the cache file, so we
# need to check if cache file is already protected.
if exc.errno not in [errno.EPERM, errno.EACCES]:
raise
actual = stat.S_IMODE(os.stat(path).st_mode)
if actual != mode:
raise
def is_protected(self, path_info):
try:
mode = os.stat(path_info).st_mode
except FileNotFoundError:
return False
return stat.S_IMODE(mode) == self.CACHE_MODE
def get_file_checksum(self, path_info):
return file_md5(path_info)[0]
@staticmethod
def getsize(path_info):
return os.path.getsize(path_info)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
makedirs(to_info.parent, exist_ok=True)
tmp_file = tmp_fname(to_info)
copyfile(
from_file, tmp_file, name=name, no_progress_bar=no_progress_bar
)
self.protect(tmp_file)
os.rename(tmp_file, to_info)
@staticmethod
def _download(
from_info, to_file, name=None, no_progress_bar=False, **_kwargs
):
copyfile(
from_info, to_file, no_progress_bar=no_progress_bar, name=name
)
def list_paths(self, prefix=None, progress_callback=None):
assert self.path_info is not None
if prefix:
path_info = self.path_info / prefix[:2]
if not self.exists(path_info):
return
else:
path_info = self.path_info
# NOTE: use utils.fs walk_files since tree.walk_files will not follow
# symlinks
if progress_callback:
for path in walk_files(path_info):
progress_callback()
yield path
else:
yield from walk_files(path_info)
def _remove_unpacked_dir(self, checksum):
info = self.checksum_to_path_info(checksum)
path_info = info.with_name(info.name + self.UNPACKED_DIR_SUFFIX)
self.remove(path_info)
def _log_exceptions(func, operation):
@wraps(func)
def wrapper(from_info, to_info, *args, **kwargs):
try:
func(from_info, to_info, *args, **kwargs)
return 0
except Exception as exc:
# NOTE: this means we ran out of file descriptors and there is no
# reason to try to proceed, as we will hit this error anyways.
if isinstance(exc, OSError) and exc.errno == errno.EMFILE:
raise
logger.exception(
"failed to %s '%s' to '%s'", operation, from_info, to_info
)
return 1
return wrapper
class LocalRemote(Remote):
INDEX_CLS = RemoteIndexNoop
class LocalCache(CloudCache):
DEFAULT_CACHE_TYPES = ["reflink", "copy"]
CACHE_MODE = LocalRemoteTree.CACHE_MODE
def __init__(self, tree):
super().__init__(tree)
self.cache_dir = tree.config.get("url")
@property
def cache_dir(self):
return self.tree.path_info.fspath if self.tree.path_info else None
@cache_dir.setter
def cache_dir(self, value):
self.tree.path_info = PathInfo(value) if value else None
@classmethod
def supported(cls, config):
return True
@cached_property
def cache_path(self):
return os.path.abspath(self.cache_dir)
def checksum_to_path(self, checksum):
# NOTE: `self.cache_path` is already normalized so we can simply use
# `os.sep` instead of `os.path.join`. This results in this helper
# being ~5.5 times faster.
return (
f"{self.cache_path}{os.sep}{checksum[0:2]}{os.sep}{checksum[2:]}"
)
def checksums_exist(self, checksums, jobs=None, name=None):
return [
checksum
for checksum in Tqdm(
@@ -216,28 +387,23 @@ class RemoteLOCAL(RemoteBASE):
if not self.changed_cache_file(checksum)
]
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
makedirs(to_info.parent, exist_ok=True)
def already_cached(self, path_info):
assert path_info.scheme in ["", "local"]
tmp_file = tmp_fname(to_info)
copyfile(
from_file, tmp_file, name=name, no_progress_bar=no_progress_bar
)
os.rename(tmp_file, fspath_py35(to_info))
current_md5 = self.get_checksum(path_info)
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
):
copyfile(
from_info, to_file, no_progress_bar=no_progress_bar, name=name
)
if not current_md5:
return False
@staticmethod
def open(path_info, mode="r", encoding=None):
return open(fspath_py35(path_info), mode=mode, encoding=encoding)
return not self.changed_cache(current_md5)
def _verify_link(self, path_info, link_type):
if link_type == "hardlink" and self.tree.getsize(path_info) == 0:
return
super()._verify_link(path_info, link_type)
@index_locked
def status(
self,
named_cache,
@@ -246,37 +412,130 @@ class RemoteLOCAL(RemoteBASE):
show_checksums=False,
download=False,
):
logger.debug(
"Preparing to collect status from {}".format(remote.path_info)
# Return flattened dict containing all status info
dir_status, file_status, _ = self._status(
named_cache,
remote,
jobs=jobs,
show_checksums=show_checksums,
download=download,
)
md5s = list(named_cache[self.scheme])
return dict(dir_status, **file_status)
def _status(
self,
named_cache,
remote,
jobs=None,
show_checksums=False,
download=False,
):
"""Return a tuple of (dir_status_info, file_status_info, dir_contents).
dir_status_info contains status for .dir files, file_status_info
contains status for all other files, and dir_contents is a dict of
{dir_checksum: set(file_checksum, ...)} which can be used to map
a .dir file to its file contents.
"""
logger.debug(f"Preparing to collect status from {remote.path_info}")
md5s = set(named_cache.scheme_keys(self.scheme))
logger.debug("Collecting information from local cache...")
local_exists = self.cache_exists(md5s, jobs=jobs, name=self.cache_dir)
local_exists = frozenset(
self.checksums_exist(md5s, jobs=jobs, name=self.cache_dir)
)
# This is a performance optimization. We can safely assume that,
# if the resources that we want to fetch are already cached,
# there's no need to check the remote storage for the existence of
# those files.
if download and sorted(local_exists) == sorted(md5s):
if download and local_exists == md5s:
remote_exists = local_exists
else:
logger.debug("Collecting information from remote cache...")
remote_exists = list(
remote.cache_exists(
md5s, jobs=jobs, name=str(remote.path_info)
remote_exists = set()
dir_md5s = set(named_cache.dir_keys(self.scheme))
if dir_md5s:
remote_exists.update(
self._indexed_dir_checksums(named_cache, remote, dir_md5s)
)
md5s.difference_update(remote_exists)
if md5s:
remote_exists.update(
remote.checksums_exist(
md5s, jobs=jobs, name=str(remote.path_info)
)
)
return self._make_status(
named_cache, show_checksums, local_exists, remote_exists
)
def _make_status(
self, named_cache, show_checksums, local_exists, remote_exists
):
def make_names(checksum, names):
return {"name": checksum if show_checksums else " ".join(names)}
dir_status = {}
file_status = {}
dir_contents = {}
for checksum, item in named_cache[self.scheme].items():
if item.children:
dir_status[checksum] = make_names(checksum, item.names)
dir_contents[checksum] = set()
for child_checksum, child in item.children.items():
file_status[child_checksum] = make_names(
child_checksum, child.names
)
dir_contents[checksum].add(child_checksum)
else:
file_status[checksum] = make_names(checksum, item.names)
self._fill_statuses(dir_status, local_exists, remote_exists)
self._fill_statuses(file_status, local_exists, remote_exists)
self._log_missing_caches(dict(dir_status, **file_status))
return dir_status, file_status, dir_contents
def _indexed_dir_checksums(self, named_cache, remote, dir_md5s):
# Validate our index by verifying all indexed .dir checksums
# still exist on the remote
indexed_dirs = set(remote.index.dir_checksums())
indexed_dir_exists = set()
if indexed_dirs:
indexed_dir_exists.update(
remote.tree.list_checksums_exists(indexed_dirs)
)
missing_dirs = indexed_dirs.difference(indexed_dir_exists)
if missing_dirs:
logger.debug(
"Remote cache missing indexed .dir checksums '{}', "
"clearing remote index".format(", ".join(missing_dirs))
)
remote.index.clear()
ret = {
checksum: {"name": checksum if show_checksums else " ".join(names)}
for checksum, names in named_cache[self.scheme].items()
}
self._fill_statuses(ret, local_exists, remote_exists)
# Check if non-indexed (new) dir checksums exist on remote
dir_exists = dir_md5s.intersection(indexed_dir_exists)
dir_exists.update(
remote.tree.list_checksums_exists(dir_md5s - dir_exists)
)
self._log_missing_caches(ret)
return ret
# If .dir checksum exists on the remote, assume directory contents
# still exists on the remote
for dir_checksum in dir_exists:
file_checksums = list(
named_cache.child_keys(self.scheme, dir_checksum)
)
if dir_checksum not in remote.index:
logger.debug(
"Indexing new .dir '{}' with '{}' nested files".format(
dir_checksum, len(file_checksums)
)
)
remote.index.update([dir_checksum], file_checksums)
yield dir_checksum
yield from file_checksums
@staticmethod
def _fill_statuses(checksum_info_dir, local_exists, remote_exists):
@@ -292,6 +551,7 @@ class RemoteLOCAL(RemoteBASE):
cache = []
path_infos = []
names = []
checksums = []
for md5, info in Tqdm(
status_info.items(), desc="Analysing status", unit="file"
):
@@ -299,6 +559,7 @@ class RemoteLOCAL(RemoteBASE):
cache.append(self.checksum_to_path_info(md5))
path_infos.append(remote.checksum_to_path_info(md5))
names.append(info["name"])
checksums.append(md5)
if download:
to_infos = cache
@@ -307,7 +568,7 @@ class RemoteLOCAL(RemoteBASE):
to_infos = path_infos
from_infos = cache
return from_infos, to_infos, names
return from_infos, to_infos, names, checksums
def _process(
self,
@@ -326,19 +587,21 @@ class RemoteLOCAL(RemoteBASE):
if download:
func = partial(
remote.download,
dir_mode=self._dir_mode,
file_mode=self._file_mode,
_log_exceptions(remote.tree.download, "download"),
dir_mode=self.tree.dir_mode,
file_mode=self.tree.file_mode,
)
status = STATUS_DELETED
desc = "Downloading"
else:
func = remote.upload
func = _log_exceptions(remote.tree.upload, "upload")
status = STATUS_NEW
desc = "Uploading"
if jobs is None:
jobs = remote.JOBS
jobs = remote.tree.JOBS
status_info = self.status(
dir_status, file_status, dir_contents = self._status(
named_cache,
remote,
jobs=jobs,
@@ -346,24 +609,91 @@ class RemoteLOCAL(RemoteBASE):
download=download,
)
plans = self._get_plans(download, remote, status_info, status)
dir_plans = self._get_plans(download, remote, dir_status, status)
file_plans = self._get_plans(download, remote, file_status, status)
if len(plans[0]) == 0:
total = len(dir_plans[0]) + len(file_plans[0])
if total == 0:
return 0
if jobs > 1:
with Tqdm(total=total, unit="file", desc=desc) as pbar:
func = pbar.wrap_fn(func)
with ThreadPoolExecutor(max_workers=jobs) as executor:
fails = sum(executor.map(func, *plans))
else:
fails = sum(map(func, *plans))
if download:
from_infos, to_infos, names, _ = (
d + f for d, f in zip(dir_plans, file_plans)
)
fails = sum(
executor.map(func, from_infos, to_infos, names)
)
else:
# for uploads, push files first, and any .dir files last
file_futures = {}
for from_info, to_info, name, checksum in zip(*file_plans):
file_futures[checksum] = executor.submit(
func, from_info, to_info, name
)
dir_futures = {}
for from_info, to_info, name, dir_checksum in zip(
*dir_plans
):
wait_futures = {
future
for file_checksum, future in file_futures.items()
if file_checksum in dir_contents[dir_checksum]
}
dir_futures[dir_checksum] = executor.submit(
self._dir_upload,
func,
wait_futures,
from_info,
to_info,
name,
)
fails = sum(
future.result()
for future in concat(
file_futures.values(), dir_futures.values()
)
)
if fails:
if download:
remote.index.clear()
raise DownloadError(fails)
raise UploadError(fails)
return len(plans[0])
if not download:
# index successfully pushed dirs
for dir_checksum, future in dir_futures.items():
if future.result() == 0:
file_checksums = dir_contents[dir_checksum]
logger.debug(
"Indexing pushed dir '{}' with "
"'{}' nested files".format(
dir_checksum, len(file_checksums)
)
)
remote.index.update([dir_checksum], file_checksums)
return len(dir_plans[0]) + len(file_plans[0])
@staticmethod
def _dir_upload(func, futures, from_info, to_info, name):
for future in as_completed(futures):
if future.result():
# do not upload this .dir file if any file in this
# directory failed to upload
logger.debug(
"failed to upload full contents of '{}', "
"aborting .dir file upload".format(name)
)
logger.error(f"failed to upload '{from_info}' to '{to_info}'")
return 1
return func(from_info, to_info, name)
@index_locked
def push(self, named_cache, remote, jobs=None, show_checksums=False):
return self._process(
named_cache,
@@ -373,6 +703,7 @@ class RemoteLOCAL(RemoteBASE):
download=False,
)
@index_locked
def pull(self, named_cache, remote, jobs=None, show_checksums=False):
return self._process(
named_cache,
@@ -399,127 +730,3 @@ class RemoteLOCAL(RemoteBASE):
"nor on remote. Missing cache files: {}".format(missing_desc)
)
logger.warning(msg)
def _unprotect_file(self, path):
if System.is_symlink(path) or System.is_hardlink(path):
logger.debug("Unprotecting '{}'".format(path))
tmp = os.path.join(os.path.dirname(path), "." + uuid())
# The operations order is important here - if some application
# would access the file during the process of copyfile then it
# would get only the part of file. So, at first, the file should be
# copied with the temporary name, and then original file should be
# replaced by new.
copyfile(path, tmp, name="Unprotecting '{}'".format(relpath(path)))
remove(path)
os.rename(tmp, path)
else:
logger.debug(
"Skipping copying for '{}', since it is not "
"a symlink or a hardlink.".format(path)
)
os.chmod(path, self._file_mode)
def _unprotect_dir(self, path):
assert is_working_tree(self.repo.tree)
for fname in self.repo.tree.walk_files(path):
self._unprotect_file(fname)
def unprotect(self, path_info):
path = path_info.fspath
if not os.path.exists(path):
raise DvcException(
"can't unprotect non-existing data '{}'".format(path)
)
if os.path.isdir(path):
self._unprotect_dir(path)
else:
self._unprotect_file(path)
def protect(self, path_info):
path = fspath_py35(path_info)
mode = self.CACHE_MODE
try:
os.chmod(path, mode)
except OSError as exc:
# In share cache scenario, we might not own the cache file, so we
# need to check if cache file is already protected.
if exc.errno not in [errno.EPERM, errno.EACCES]:
raise
actual = os.stat(path).st_mode
if actual & mode != mode:
raise
def _get_unpacked_dir_path_info(self, checksum):
info = self.checksum_to_path_info(checksum)
return info.with_name(info.name + self.UNPACKED_DIR_SUFFIX)
def _remove_unpacked_dir(self, checksum):
path_info = self._get_unpacked_dir_path_info(checksum)
self.remove(path_info)
def _path_info_changed(self, path_info):
if self.exists(path_info) and self.state.get(path_info):
return False
return True
def _update_unpacked_dir(self, checksum):
unpacked_dir_info = self._get_unpacked_dir_path_info(checksum)
if not self._path_info_changed(unpacked_dir_info):
return
self.remove(unpacked_dir_info)
try:
dir_info = self.get_dir_cache(checksum)
self._create_unpacked_dir(checksum, dir_info, unpacked_dir_info)
except DvcException:
logger.warning("Could not create '{}'".format(unpacked_dir_info))
self.remove(unpacked_dir_info)
def _create_unpacked_dir(self, checksum, dir_info, unpacked_dir_info):
self.makedirs(unpacked_dir_info)
for entry in Tqdm(dir_info, desc="Creating unpacked dir", unit="file"):
entry_cache_info = self.checksum_to_path_info(
entry[self.PARAM_CHECKSUM]
)
relative_path = entry[self.PARAM_RELPATH]
# In shared cache mode some cache files might not be owned by the
# user, so we need to use symlinks because, unless
# /proc/sys/fs/protected_hardlinks is disabled, the user is not
# allowed to create hardlinks to files that he doesn't own.
link_types = ["hardlink", "symlink"]
self._link(
entry_cache_info, unpacked_dir_info / relative_path, link_types
)
self.state.save(unpacked_dir_info, checksum)
def _changed_unpacked_dir(self, checksum):
status_unpacked_dir_info = self._get_unpacked_dir_path_info(checksum)
return not self.state.get(status_unpacked_dir_info)
def _get_unpacked_dir_names(self, checksums):
unpacked = set()
for c in checksums:
if self.is_dir_checksum(c):
unpacked.add(c + self.UNPACKED_DIR_SUFFIX)
return unpacked
def is_protected(self, path_info):
if not self.exists(path_info):
return False
mode = os.stat(fspath_py35(path_info)).st_mode
return stat.S_IMODE(mode) == self.CACHE_MODE
+33 -27
View File
@@ -6,14 +6,13 @@ from funcy import cached_property, wrap_prop
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import RemoteBASE
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
class RemoteOSS(RemoteBASE):
class OSSRemoteTree(BaseRemoteTree):
"""
oss2 document:
https://www.alibabacloud.com/help/doc-detail/32026.htm
@@ -33,16 +32,17 @@ class RemoteOSS(RemoteBASE):
"""
scheme = Schemes.OSS
path_cls = CloudURLInfo
PATH_CLS = CloudURLInfo
REQUIRES = {"oss2": "oss2"}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
LIST_OBJECT_PAGE_SIZE = 100
def __init__(self, repo, config):
super().__init__(repo, config)
url = config.get("url")
self.path_info = self.path_cls(url) if url else None
self.path_info = self.PATH_CLS(url) if url else None
self.endpoint = config.get("oss_endpoint") or os.getenv("OSS_ENDPOINT")
@@ -63,9 +63,9 @@ class RemoteOSS(RemoteBASE):
def oss_service(self):
import oss2
logger.debug("URL: {}".format(self.path_info))
logger.debug("key id: {}".format(self.key_id))
logger.debug("key secret: {}".format(self.key_secret))
logger.debug(f"URL: {self.path_info}")
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.path_info.bucket)
@@ -82,22 +82,37 @@ class RemoteOSS(RemoteBASE):
)
return bucket
def _generate_download_url(self, path_info, expires=3600):
assert path_info.bucket == self.path_info.bucket
return self.oss_service.sign_url("GET", path_info.path, expires)
def exists(self, path_info):
paths = self._list_paths(path_info)
return any(path_info.path == path for path in paths)
def _list_paths(self, path_info):
import oss2
for blob in oss2.ObjectIterator(
self.oss_service, prefix=path_info.path
):
yield blob.key
def walk_files(self, path_info, **kwargs):
for fname in self._list_paths(path_info):
if fname.endswith("/"):
continue
yield path_info.replace(path=fname)
def remove(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
logger.debug("Removing oss://{}".format(path_info))
logger.debug(f"Removing oss://{path_info}")
self.oss_service.delete_object(path_info.path)
def _list_paths(self, prefix):
import oss2
for blob in oss2.ObjectIterator(self.oss_service, prefix=prefix):
yield blob.key
def list_cache_paths(self):
return self._list_paths(self.path_info.path)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
):
@@ -113,12 +128,3 @@ class RemoteOSS(RemoteBASE):
self.oss_service.get_object_to_file(
from_info.path, to_file, progress_callback=pbar.update_to
)
def _generate_download_url(self, path_info, expires=3600):
assert path_info.bucket == self.path_info.bucket
return self.oss_service.sign_url("GET", path_info.path, expires)
def exists(self, path_info):
paths = self._list_paths(path_info.path)
return any(path_info.path == path for path in paths)
+2 -2
View File
@@ -1,6 +1,6 @@
import threading
from collections import deque
from contextlib import contextmanager
import threading
from funcy import memoize, wrap_with
@@ -30,7 +30,7 @@ def close_pools():
get_pool.memory.clear()
class Pool(object):
class Pool:
def __init__(self, conn_func, *conn_args, **conn_kwargs):
self._conn_func = conn_func
self._conn_args = conn_args
+148 -146
View File
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import logging
import os
import threading
@@ -7,19 +5,18 @@ import threading
from funcy import cached_property, wrap_prop
from dvc.config import ConfigError
from dvc.exceptions import DvcException
from dvc.exceptions import ETagMismatchError
from dvc.exceptions import DvcException, ETagMismatchError
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import RemoteBASE
from dvc.remote.base import BaseRemoteTree
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
class RemoteS3(RemoteBASE):
class S3RemoteTree(BaseRemoteTree):
scheme = Schemes.S3
path_cls = CloudURLInfo
PATH_CLS = CloudURLInfo
REQUIRES = {"boto3": "boto3"}
PARAM_CHECKSUM = "etag"
@@ -27,7 +24,7 @@ class RemoteS3(RemoteBASE):
super().__init__(repo, config)
url = config.get("url", "s3://")
self.path_info = self.path_cls(url)
self.path_info = self.PATH_CLS(url)
self.region = config.get("region")
self.profile = config.get("profile")
@@ -46,6 +43,10 @@ class RemoteS3(RemoteBASE):
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
@@ -75,20 +76,145 @@ class RemoteS3(RemoteBASE):
return obj["ETag"].strip('"')
def get_file_checksum(self, path_info):
return self.get_etag(self.s3, path_info.bucket, path_info.path)
@staticmethod
def get_head_object(s3, bucket, path, *args, **kwargs):
try:
obj = s3.head_object(Bucket=bucket, Key=path, *args, **kwargs)
except Exception as exc:
raise DvcException(
"s3://{}/{} does not exist".format(bucket, path)
) from exc
raise DvcException(f"s3://{bucket}/{path} does not exist") from exc
return obj
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}
return self.s3.generate_presigned_url(
ClientMethod="get_object", Params=params, ExpiresIn=int(expires)
)
def exists(self, path_info):
"""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):
from botocore.exceptions import ClientError
if path_info.path.endswith("/"):
return False
try:
self.s3.head_object(Bucket=path_info.bucket, Key=path_info.path)
except ClientError as exc:
if exc.response["Error"]["Code"] != "404":
raise
return False
return True
def _list_objects(self, path_info, max_items=None):
""" Read config for list object api, paginate through list objects."""
kwargs = {
"Bucket": path_info.bucket,
"Prefix": path_info.path,
"PaginationConfig": {"MaxItems": max_items},
}
paginator = self.s3.get_paginator(self.list_objects_api)
for page in paginator.paginate(**kwargs):
contents = page.get("Contents", ())
yield from contents
def _list_paths(self, path_info, max_items=None):
return (
item["Key"] for item in self._list_objects(path_info, max_items)
)
def walk_files(self, path_info, **kwargs):
for fname in self._list_paths(path_info / "", **kwargs):
if fname.endswith("/"):
continue
yield path_info.replace(path=fname)
def remove(self, path_info):
if path_info.scheme != "s3":
raise NotImplementedError
logger.debug(f"Removing {path_info}")
self.s3.delete_object(Bucket=path_info.bucket, Key=path_info.path)
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 / ""
self.s3.put_object(Bucket=path_info.bucket, Key=dir_path.path, Body="")
def copy(self, from_info, to_info):
self._copy(self.s3, from_info, to_info, self.extra_args)
@classmethod
def _copy_multipart(
cls, s3, from_info, to_info, size, n_parts, extra_args
@@ -101,7 +227,7 @@ class RemoteS3(RemoteBASE):
parts = []
byte_position = 0
for i in range(1, n_parts + 1):
obj = cls.get_head_object(
obj = S3RemoteTree.get_head_object(
s3, from_info.bucket, from_info.path, PartNumber=i
)
part_size = obj["ContentLength"]
@@ -109,7 +235,7 @@ class RemoteS3(RemoteBASE):
if lastbyte > size:
lastbyte = size - 1
srange = "bytes={}-{}".format(byte_position, lastbyte)
srange = f"bytes={byte_position}-{lastbyte}"
part = s3.upload_part_copy(
Bucket=to_info.bucket,
@@ -156,7 +282,9 @@ class RemoteS3(RemoteBASE):
# object is transfered in the same chunks as it was originally.
from boto3.s3.transfer import TransferConfig
obj = cls.get_head_object(s3, from_info.bucket, from_info.path)
obj = S3RemoteTree.get_head_object(
s3, from_info.bucket, from_info.path
)
etag = obj["ETag"].strip('"')
size = obj["ContentLength"]
@@ -176,94 +304,12 @@ class RemoteS3(RemoteBASE):
Config=TransferConfig(multipart_threshold=size + 1),
)
cached_etag = cls.get_etag(s3, to_info.bucket, to_info.path)
cached_etag = S3RemoteTree.get_etag(s3, to_info.bucket, to_info.path)
if etag != cached_etag:
raise ETagMismatchError(etag, cached_etag)
def copy(self, from_info, to_info):
self._copy(self.s3, from_info, to_info, self.extra_args)
def remove(self, path_info):
if path_info.scheme != "s3":
raise NotImplementedError
logger.debug("Removing {}".format(path_info))
self.s3.delete_object(Bucket=path_info.bucket, Key=path_info.path)
def _list_objects(self, path_info, max_items=None):
""" Read config for list object api, paginate through list objects."""
kwargs = {
"Bucket": path_info.bucket,
"Prefix": path_info.path,
"PaginationConfig": {"MaxItems": max_items},
}
paginator = self.s3.get_paginator(self.list_objects_api)
for page in paginator.paginate(**kwargs):
yield from page.get("Contents", ())
def _list_paths(self, path_info, max_items=None):
return (
item["Key"] for item in self._list_objects(path_info, max_items)
)
def list_cache_paths(self):
return self._list_paths(self.path_info)
def isfile(self, path_info):
from botocore.exceptions import ClientError
if path_info.path.endswith("/"):
return False
try:
self.s3.head_object(Bucket=path_info.bucket, Key=path_info.path)
except ClientError as exc:
if exc.response["Error"]["Code"] != "404":
raise
return False
return True
def exists(self, path_info):
"""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 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.
dir_path = path_info / ""
self.s3.put_object(Bucket=path_info.bucket, Key=dir_path.path, Body="")
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 get_file_checksum(self, path_info):
return self.get_etag(self.s3, path_info.bucket, path_info.path)
def _upload(self, from_file, to_info, name=None, no_progress_bar=False):
total = os.path.getsize(from_file)
@@ -291,47 +337,3 @@ class RemoteS3(RemoteBASE):
self.s3.download_file(
from_info.bucket, from_info.path, to_file, Callback=pbar.update
)
def _generate_download_url(self, path_info, expires=3600):
params = {"Bucket": path_info.bucket, "Key": path_info.path}
return self.s3.generate_presigned_url(
ClientMethod="get_object", Params=params, ExpiresIn=int(expires)
)
def walk_files(self, path_info, max_items=None):
for fname in self._list_paths(path_info / "", max_items):
if fname.endswith("/"):
continue
yield path_info.replace(path=fname)
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)
+1 -2
View File
@@ -1,10 +1,9 @@
import logging
import sys
import time
from functools import wraps
import colorama
import colorama
logger = logging.getLogger(__name__)
this = sys.modules[__name__]

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