Compare commits

..
941 Commits
Author SHA1 Message Date
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
Ruslan Kuprieiev a1fe6c6f44 dvc: bump to 0.90.2 2020-03-18 23:11:17 +02:00
Ruslan Kuprieiev 7edee656da pipeline: fix --outs output (#3507)
Fixes #3497
2020-03-18 23:03:41 +02:00
Ruslan Kuprieiev 498ee509da dvc: use consistent messages for InvalidArgumentError (#3506)
Per https://discordapp.com/channels/485586884165107732/563406153334128681/689896480898810034
2020-03-18 21:34:46 +02:00
Ruslan Kuprieiev 55f86df5b1 dvc: bump to 0.90.1 2020-03-18 19:20:22 +02:00
Ruslan Kuprieiev eae432818a git: tree: use item.path instead of item.name (#3505)
We are currently using this [0] hack to work around the fact that
item.name
is not always a basename. Even though `IndexObject` has it defined that
way
[1], but `Submodule` - doesn't [2]. So using `item.name` might cause
issues
such as [3], because GitPython doesn't pass name parameter when simply
going through objects [4]. In other places, when working specifically
with
submodules, it sets `_name` attribute explicitly [5].

Note that we still don't walk into submodules when walking through a git
repo. Might want to reconsider that later as for now we haven't heard
anything about it from actual users.

Fixes #3481

[0] https://github.com/iterative/dvc/blob/0.90.0/dvc/scm/git/tree.py#L15
[1]
https://github.com/gitpython-developers/GitPython/blob/3.1.0/git/objects/base.py#L170
[2]
https://github.com/gitpython-developers/GitPython/blob/3.1.0/git/objects/submodule/base.py#L1123
[3] https://github.com/gitpython-developers/GitPython/issues/597
[4]
https://github.com/gitpython-developers/GitPython/blob/3.1.0/git/objects/tree.py#L237
[5]
https://github.com/gitpython-developers/GitPython/blob/3.1.0/git/objects/submodule/base.py#L357
2020-03-18 19:18:58 +02:00
Ruslan Kuprieiev aef3dfba13 dvc: bump to 0.90.0 2020-03-18 01:24:19 +02:00
Ruslan Kuprieiev 42c8fab47f version: add supported remotes (#3498)
Since our remotes have optional dependencies, it is really useful to be
able to see what is currently supported. Also helps debug issues with
packages such as brew/conda/etc.
2020-03-18 01:14:59 +02:00
Casper da Costa-Luis f19d800f43 import-url: allow queries in URL (#3432)
* import-url: allow queries in URL

Fixes #3424

* Revert "import-url: allow queries in URL"

This reverts commit 48495d027963073fe260f596d85c3c179fee037f.

* path_info: add HTTPURLInfo base

* path_info: HTTPURLInfo complete

* remote: http(s): support params, queries, fragments

Fixes #3424
Closes #3432

* tests: path_info: HTTPURLInfo

* lint

* tests: path_info: HTTPURLInfo

Slightly more thorough testing
2020-03-17 19:23:56 +02:00
Casper da Costa-LuisandJorge Orpinel 137373c165 template: pr update (#3495)
* template: pr update

* Update .github/PULL_REQUEST_TEMPLATE.md

* Update .github/PULL_REQUEST_TEMPLATE.md

* Update .github/PULL_REQUEST_TEMPLATE.md

Co-authored-by: Jorge Orpinel <[email protected]>
2020-03-17 19:18:25 +02:00
Ruslan Kuprieiev 6559b9d222 dvc: use protected mode by default (#3472)
* dvc: use protected mode by default

Fixes #3261
Fixes #2041

* unprotect: adjust help message

https://github.com/iterative/dvc.org/pull/1058/files/21aab371a487acf6f6e6201b29cd832e7c55ed23#r393324668
2020-03-17 19:16:21 +02:00
Saugat Pachhai 4b5d4d759a gc: do not work without specifier when using -c flag (#3493)
* gc: ask for specifier when using -c flag

* tests: add -w flag with gc -c
2020-03-17 18:12:42 +02:00
Saugat Pachhai e1334448ea completion: add --summary flag on checkout (#3491) 2020-03-17 14:04:12 +02:00
Ruslan Kuprieiev 02daaf391a dvc: bump to 0.89.0 2020-03-17 02:08:56 +02:00
Ruslan Kuprieiev 2ffebb0b5e logger: use standard lazy formatting (#3487)
Unfortunately overriding `format` is not enough in order to use our own
custom formatting in logger. `Record` uses percent formatting that is
hardcoded inside, so we would also have to override it, which is just
way too much effort that will inevitably break as we discover other
methods that are not ready for non-% formatting. So we are just giving
up and sticking to default %-formatting for lazy log messages.

Fixes #3397
2020-03-17 00:38:00 +02:00
Paweł Redzyński 87338c994f cmd: diff: don't load dir cache for diff (#3468) 2020-03-16 21:39:34 +02:00
Saugat PachhaiandCasper da Costa-Luis 109ded4068 dvc: support Python3.8 (#3463)
* py38: make wheels

- ci: set ARROW_LIBHDFS_DIR to load native library
- deps: pin pyarrow to 0.15.1 for py<=3.7
- pin deps for python<3.8
- pin deps for python==3.8 as well
- add windows Py38 on travis
- do not use speedcopy for Python3.8 and Windows

* use realpath to resolve symbolic links

* do not build pkgs for Windows and Linux on Python3.8

* reorder

* pyarrow: document regression fix

Co-authored-by: Casper da Costa-Luis <[email protected]>
2020-03-16 19:18:33 +00:00
Ruslan Kuprieiev 3f3384b7c4 pre-commit: use official dvc repo 2020-03-16 20:41:29 +02:00
Ruslan Kuprieiev b223696079 install: use hooks from master
Same as with shell completion, it is better to use the newest hooks.
2020-03-16 20:40:02 +02:00
c4164b3497 Use pre-commit for Git hooks (#3406)
* Initial design of using pre-commit for Git hooks

* Refactor to reuse existing code and provide merge capabilities

* Linter fixes

* More cleanup

* scm: make use_precommit_tool optional

* dvc: install our own hooks

* install: fix typo

* install: fix bugs in pre-commit integration

* install: don't forget 'always_run' for post-checkout

* install: no need to run pre-commit install if conf file doesn't exist

* install: fix bugs

* install: use python hooks

* add .pre-commit-hooks.yaml

Co-authored-by: Andrew Hare <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-03-16 20:34:01 +02:00
Guro Bokum dcf5af647e list: rename target param to path, update docs (#3462)
Close #3381
2020-03-16 20:22:48 +02:00
Saugat Pachhai 1b21789444 exceptions: use semantic exceptions (#3430) 2020-03-16 19:39:59 +02:00
Saugat PachhaiandJorge Orpinel 066ccea733 checkout: show summary by default and introduce --show-changes flag (#3401)
* checkout: implement --show-changes flag to show detailed checkout details
Show summary by default on checkout

checkout: show directory modified even when files are removed from inside it

checkout: refactor template logic

checkout: change output format

checkout: do not show summary if nothing's changed

checkout: do not show summary if show_changes is specified

checkout: show '/' at the end if it's a directory

rename variable name

test: cover stats for checkout

checkout: show relink as not modified

checkout: remove unwanted message during empty checkout

pull: show summary on checkout

import/get: change external_repo to catch CheckoutError

fix tests and check if path is url

checkout: support external deps/outputs

tests: checkout on external outputs

* fix tests

* checkout: make show changes default and introduce summary flag to show summary

* address suggestions

* exceptions: remove extra spacing on erorr

* Update dvc/command/checkout.py

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

Co-authored-by: Jorge Orpinel <[email protected]>
2020-03-16 16:29:36 +02:00
Saugat Pachhai d2734dfe1e tests: remove use of mocker as context-manager (#3480) 2020-03-16 08:25:22 +05:45
Alexander Schepanovski e69b44d1ed dvc: optimize all target specific commands to not build graph (#3459) 2020-03-15 21:53:38 +02:00
Paweł Redzyński 767433d7a4 tests: add .gitignore files upon dvc_add (#3455) 2020-03-13 17:19:35 +02:00
Saugat Pachhai 682275dc0a add: do not verify hardlink if file is empty (#3428)
* add: do not verify hardlink if file is empty

Fixes #3390

* tests: spy

* fixup! tests: spy
2020-03-11 21:56:12 +02:00
Jorge OrpinelandRuslan Kuprieiev 9944efbd0f api: docstring updates (2) (#3426)
* api: update to latest definitions to match iterative/dvc.org/pull/908

* Update dvc/api.py

* metrics show: update -h output to match docs
per https://github.com/iterative/dvc.org/pull/1014/commits/2c3452119545987f77b764e4aa6fc94d051092a7

* api: update docstrings to match iterative/dvc.org/pull/908

* api: refactor DVC repo check in get_url, and document it

* dvc: cosmetic edits as I explored exceptions that api functions may raise

* api: copy default info to read() docstring from open()
per https://github.com/iterative/dvc/pull/3426#pullrequestreview-366949501

* api: improve open() docstring for clarity and add example
per https://github.com/iterative/dvc/pull/3426#pullrequestreview-366956856

* api: remove unnecessary info from get_url docstring
per https://github.com/iterative/dvc/pull/3426#discussion_r386203064

* api: produces->generated in open() docstring

* api: simplify open docstring
per https://github.com/iterative/dvc/pull/3426#discussion_r388218147

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-03-11 20:45:29 +02:00
Casper da Costa-Luis 7923446219 completion: bash: misc fixes and updates (#3467)
* completion: bash: fix, update and sort

* completion: bash: fix missing `compgen`s

* completion: bash: remove deprecated/hidden options

* completion: bash: fix extra parenthesis

- fixes metrics show -t

* completion: bash: re-add `gc --all-commits`

Due to #3429 superceding #2681
2020-03-11 15:56:20 +02:00
Max Risuhin 3ff804b3b2 gdrive: Google Service account support (#3269) 2020-03-11 15:49:33 +02:00
Ruslan Kuprieiev ef96e6b8ff dvc: bump to 0.88.0 2020-03-10 22:47:01 +02:00
Saugat Pachhai 7f94518008 deps: upgrade jsonpath_ng to latest version (#3464)
Fix #3456
2020-03-10 11:18:52 +02:00
Ruslan Kuprieiev 40519005f5 travis: hadoop: update tar location 2020-03-10 10:20:34 +02:00
Saugat Pachhai ea981ae955 gc: make --all-commits flag visible (#3429)
* gc: make --all-commits visible

* gc: fix help message and completion scripts

* bash: add longform --workspace flag
2020-03-10 08:29:07 +05:45
Ivan Shcheklein 4bceb9d9cd gdrive: share drive vs just folder id when we set corpora param (#3458) 2020-03-09 19:25:08 +02:00
Ruslan Kuprieiev 37e4a5dcd4 travis: use us-west-2 aws s3 region 2020-03-09 19:10:47 +02:00
Ruslan Kuprieiev 6479adb1a3 test: s3: move to dvc-temp bucket 2020-03-09 18:45:59 +02:00
Guro Bokum 02cd520acd api: colorize dvc list (#3351)
Close #3347
2020-03-09 14:07:23 +02:00
Alexander Schepanovski 357a6dd467 dvc: optimize building graph (#3443)
Use trie structure to efficiently check out, dep and stage paths being
prefixes of each other.
2020-03-09 12:13:07 +02:00
Ruslan Kuprieiev f05e6e0529 metrics show: update -h output to match docs (#3420)
per https://github.com/iterative/dvc.org/pull/1014/commits/2c3452119545987f77b764e4aa6fc94d051092a7
2020-03-05 16:11:43 +02:00
Ruslan Kuprieiev 06c634adee pipelines: fix edges gathering (#3421) 2020-03-05 14:16:57 +02:00
Alexander Schepanovski 05cc023164 test: check that no remote backend modules are loaded by default (#3400) 2020-02-28 14:45:51 +02:00
Ruslan Kuprieiev abc4e3c2d0 dvc: bump to 0.87.0 2020-02-28 02:21:39 +02:00
Casper da Costa-Luis 35a6e0fedd docs: update badges (#3380)
* docs: add and update badges

Add |Choco|
Add |Brew|
Fix style for |Conda|
Add |PyPI|
Add |Packages|
Fixes #3233
Related https://github.com/iterative/dvc.org/issues/904

* docs: add badge logos

* docs: minor badge tidy

* docs: add choco install, add OS, update order
2020-02-28 01:32:10 +02:00
ttekampeandGuro Bokum de453ab06d dont disable existing loggers when dvc is set up (#3345)
* dont disable existing loggers when dvc is set up

* corrected code style

* dont disable existing loggers, fixes

Co-authored-by: Guro Bokum <[email protected]>
2020-02-27 07:02:06 +02:00
Alexander Schepanovski d8778e7ca9 erepo: set up more logging (#3403) 2020-02-26 22:39:13 +02:00
Bruno Deferrari b136eb5151 erepo: fix relative url on default remote (#2756) (#3378)
* erepo: fix relative urls for remotes (#2756)

* erepo: add test case for relative path remotes (#2756)

* Remove unused argument

* erepo: move handling of relative remotes to a separate method (#2756)

* erepo: don't manually build paths on test (#2756)

* erepo: refactor handling of missing or relative upstream (#2756)

* erepo: move handling of source repo resource to `_fix_upstream` (#2756)

* erepo: improve relative remote tests (#2756)

- use `tmp_dir` fixture
- commit config file changes

* erepo: Remove comment (#2756)

* erepo: use scm_add to commit too, no need for separate scm.commit call (#2756)

* erepo: minor refactoring in `_fix_upstream` (#2756)

* erepo: add comment explaining that old relative paths are restored (#2756)
2020-02-26 22:37:29 +02:00
rxxg d7d5d8fcca Remove Linux pyfastcopy (#3412)
Fixes #3395
2020-02-26 22:35:48 +02:00
Casper da Costa-Luis 68a2f34a46 pull: fetch: external: respect jobs (#3413)
Fixes #3396
2020-02-26 22:35:19 +02:00
Paweł Redzyński a9bc65ee1f analytics: fix resolving used scm (#3405) 2020-02-26 02:29:46 +02:00
Ruslan Kuprieiev 432e399f44 deps: remove pyfastcopy
https://github.com/iterative/dvc/issues/3395
2020-02-25 15:53:43 +02:00
Alexander Schepanovski a81ab5e26b gdrive: do not import pydrive2 on module load (#3399) 2020-02-25 14:52:40 +02:00
Paweł Redzyński 8d04d6d3ce SCM: allow multiple DVC repos inside single SCM repo (#3257) 2020-02-25 14:40:20 +02:00
Jorge OrpinelandRuslan Kuprieiev f673144b19 api: docstring updates (#3352)
* api: update to latest definitions to match iterative/dvc.org/pull/908

* Update dvc/api.py

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-02-23 06:41:18 +02:00
Ivan Shcheklein eebbf3f812 change diff checksum arg name to hash (#3384) 2020-02-23 06:32:35 +02:00
Alexander Schepanovski 3a5e588561 gdrive: a pair of cleanup refactors (upstream branch) (#3382)
* gdrive: minor clean using funcy 1.14 features

Employ @retry(filter_errors) and @wrap_prop() to pack things up a bit.

* gdrive: refactor dirs/ids/root_id cache and their uses

- cache root transparently
- make cache dirs and ids symmetric, i.e. both using full paths
- implement .list_cache_paths() instead of .all() like in the rest of
the remotes
2020-02-23 06:11:25 +02:00
Saugat Pachhai 5101bf1253 gc: make it safer by only activating by use of certain flags (#3363)
* gc: make it safer by only activating by use of certain flags

Previous plain `gc` is now not supported. The same behavior can be trigger by
`gc -w` or `gc --workspace`. When `--all-{tags, commits, branches}` are
specified, it will work as if `-w` is also specified (same as before).

* tests: adjust gc to use workspace on test_repro
2020-02-21 12:39:59 +02:00
Paweł Redzyński bd33b1ae30 Stage: get checksum from repo if no checksum in stage file (#3365) 2020-02-21 12:10:33 +02:00
68e543f7e1 remotes.http: Support dvc push for http remotes (#3343)
* remote.http: support _upload() via HTTP POST

- uploaded files are sent as chunked encoding POST data

Fixes #3247

* remote.http: add support for HTTP basic and digest auth

- username, password, ask_password options work the same way as for
  ssh remotes
- basic_auth (bool) and digest_auth (bool) options added for http(s)
  remotes
- digest_auth takes precedence over basic_auth if both are enabled

* remote.http: auth_method() unit tests

* tests: Add functional tests for HTTP remotes

- HTTP remotes now tested locally using a SimpleHTTPServer instance that
  allows reading/writing to a temp directory

* tests: fix http tests on py3.5, 3.6

* remote.http: add custom auth method

* s/custom_header/custom_auth_header/

* Apply suggestions from code review

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

Co-authored-by: Saugat Pachhai <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-02-21 12:05:55 +02:00
Casper da Costa-Luis 411057d1b4 completion: bash: properly recurse targets (#3374)
* completion: bash: properly recurse commands

TODO: subcommands

* completion: bash: add subcommand path completion

* completion: bash: comments
2020-02-21 04:03:23 +02:00
Casper da Costa-Luis ae99e1b02b metadata: add Zenodo, tidy badges and keywords (#3373)
* docs: add Zenodo badge

* docs: fix rst image syntax

* docs: collect and re-use badges

* metadata: add zenodo

Fixes #2978

* metadata: sync keywords

* docs: add citation in readme

* docs: one-line badges
2020-02-21 04:01:59 +02:00
Ruslan Kuprieiev 2c46bddcfa dvc: bump to 0.86.5 2020-02-21 00:11:41 +02:00
Ruslan Kuprieiev e4bf5c1ddb dvc: bump to 0.86.4 2020-02-20 03:46:51 +02:00
Ruslan Kuprieiev 6474725d4f test: repro: prevent dvc.stages resetting (#3368) 2020-02-20 03:45:28 +02:00
Ruslan Kuprieiev c5677decd3 dvc: bump to 0.86.3 2020-02-20 02:40:14 +02:00
Casper da Costa-Luis 2820aedb87 completion: bash: allow multi-arg completion, minor tidy (#3367) 2020-02-19 23:41:48 +02:00
Paweł Redzyński 9b2ef002ea Stage: create: reset repo after new stage creation (#3349) 2020-02-19 20:25:04 +02:00
Casper da Costa-Luis a9161af593 Fix bash completion (#3360)
* completion: safer function names

* completion: fix missing expansions

Fixes #1923

* complete: bash: all filenames

* complete: bash: DVC-files

* complete: bash: safer and quicker opts
2020-02-19 04:16:09 +02:00
Casper da Costa-Luis 37de65fe48 ui: fix missing progress & tidy (#3335)
* ui: show missing progress

Fixes #3332

* ui: more verbose progress

* remote: base: back to no_progres_bar=True
2020-02-19 04:08:29 +02:00
Ruslan Kuprieiev 6dba652b25 dvc: bump to 0.86.2 2020-02-17 20:30:51 +02:00
Ruslan Kuprieiev d2a6c1fc62 external_repo: fix bug in path resolution (#3350) 2020-02-17 20:30:04 +02:00
Ruslan Kuprieiev 130180cc5c setup.py: remove gitdb2 dependency and update gitpython 2020-02-17 18:12:30 +02:00
Ruslan Kuprieiev 72f1313cc3 setup: fix gitpython version 2020-02-17 17:54:53 +02:00
Jorge OrpinelandRuslan Kuprieiev a663971ce4 get/import: updates output strings, et al. (#3308)
* get/import: update cmd and url arg descs with correct behavior

* get/import: update path arg desc as well (See previous commit)

* get/imp: udpate help output with simpler terms
per https://github.com/iterative/dvc/pull/3277#pullrequestreview-352650016

* api: url->URL in a docstring

* metrics show: update help output per
per https://github.com/iterative/dvc.org/pull/933#pullrequestreview-351862579

* diff: update `diff` and `metrics diff` help desc to match docs
per https://github.com/iterative/dvc.org/pull/933#pullrequestreview-356890960

* diff: update autocomplete scripts
per https://github.com/iterative/dvc/pull/3308#issuecomment-585220300

* diff: update help output again
per https://github.com/iterative/dvc.org/pull/933#pullrequestreview-356890960

* diff/metrics diff: update help output yet again
per https://github.com/iterative/dvc.org/pull/933#discussion_r378654316

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-02-17 17:27:09 +02:00
Jorge Orpinel b23f43a7e7 term: avoid "under X control" and other help output updates (#3322)
* get/import: update cmd and url arg descs with correct behavior

* get/import: update path arg desc as well (See previous commit)

* get/imp: udpate help output with simpler terms
per https://github.com/iterative/dvc/pull/3277#pullrequestreview-352650016

* api: url->URL in a docstring

* metrics show: update help output per
per https://github.com/iterative/dvc.org/pull/933#pullrequestreview-351862579

* diff: update `diff` and `metrics diff` help desc to match docs
per https://github.com/iterative/dvc.org/pull/933#pullrequestreview-356890960

* diff: update autocomplete scripts
per https://github.com/iterative/dvc/pull/3308#issuecomment-585220300

* diff: update help output again
per https://github.com/iterative/dvc.org/pull/933#pullrequestreview-356890960

* diff/metrics diff: update help output yet again
per https://github.com/iterative/dvc.org/pull/933#discussion_r378654316

* term" under X control -> tracked by X
2020-02-17 17:24:30 +02:00
Ruslan Kuprieiev a90d7009f5 dvc: bump to 0.86.1 2020-02-17 17:08:37 +02:00
Ruslan Kuprieiev 4986395998 setup.py: limit gitpython version
https://dev.azure.com/conda-forge/feedstock-builds/_build/results?buildId=122147&view=logs&jobId=4f922444-fdfe-5dcf-b824-02f86439ef14&j=4f922444-fdfe-5dcf-b824-02f86439ef14&t=937c195f-508d-5135-dc9f-d4b5730df0f7
2020-02-17 17:03:33 +02:00
Ruslan Kuprieiev 19edc1ab1d dvc: bump to 0.86.0 2020-02-17 12:44:49 +02:00
Mr. Outis 57d02195b5 get/import: retrieve files inside directory outs (#3309)
* get/import: retrieve files inside directory outs

Close #2458

* fs: move, fspath_py35 -> fspath
2020-02-17 03:24:02 +02:00
Mr. Outis 8492786ae4 logger: refactor, remove dead code (#3306)
* Remove dead code
* Rename _walk_exc to _causes, simplify it
* Split up stack trace and remove extra \n
* Import traceback only when needed
2020-02-17 03:19:37 +02:00
Saugat Pachhai 46e5053705 update: support specifying revision when updating (#3337)
* update: work with rev

* completion: update --rev completion scripts

* test: fix unit test assumption

* test: use fspath_py35
2020-02-15 17:55:41 +02:00
Ruslan Kuprieiev 4087b020db dvc: bump to 0.85.0 2020-02-15 01:14:37 +02:00
Ruslan Kuprieiev 8a2699dfbb schema: use a more flexible hash schema (#3333)
Fixes #3331
2020-02-15 01:13:45 +02:00
Casper da Costa-Luis 276a2b02a3 ui: make progress bars respect --quiet (#3316)
* ui: default no_progress_bar=None instead of False

Fixes #3314

* ui: catch remaining missing auto-quiet progress

* progress: treat disable=False as None

* test: progress.Tqdm(disable=False)

* revert no_progress_bar=None <= False

partially reverts e0342a8641f0101d
2020-02-15 00:53:52 +02:00
Saugat Pachhai 77f24af727 dvc: do not set cache dir if not a dvc repo (#3329) 2020-02-14 19:07:23 +02:00
Casper da Costa-Luis 6932cf6c8d snap: test: add build and deploy checks (#3330)
* snap: deploy: include review-tools

Enables pre-deployment checks

* snap: build: include snapd

Enables enhanced build-time checks
2020-02-14 19:05:23 +02:00
Paweł Redzyński 5b737a0a0b imp_url: make default stage fname accompany import target (#3312) 2020-02-14 17:05:08 +02:00
Saugat Pachhai a317e59df1 tests: fix typo on gdrive_client_secret (#3327) 2020-02-14 17:04:27 +02:00
Saugat Pachhai 0daf835ae9 checkout: change level to debug and fix message (#3326) 2020-02-14 15:44:16 +02:00
Saugat Pachhai 17c7b72cb0 logger: log datetime when on debug mode (#3325) 2020-02-14 13:58:15 +02:00
Paweł Redzyński 0c87771064 daemon config fix (#3324) 2020-02-14 13:13:04 +02:00
Alexander Schepanovski 7df06b278a erepo: fix known sha handling (#3323)
Closes #3280.
2020-02-14 12:59:31 +02:00
Gaurav Rai 51a5a4ec3b repo: Prevent repo from setting log level (#3321)
* Removed setting log level

* removed loglevel schema
2020-02-13 20:45:43 +02:00
Alexander Schepanovski 6ccae711fe dvc: refactor config (#3298)
* dvc: refactor config

Advantages:
- read like a dict
- modify as a dict for testing or manipulation purposes
- wrap modifications into context manager to save them:

    with config.edit("local") as conf:
        conf["core"]["remote"] = "new-default"

Design decisions:
- dropped string constants
- remote sections are parsed into a simple subdict upon load
- remote config schema is now defined by url scheme
- relative paths are handled transparently upon load/save
- user config and remote manipulations are moved to commands
- same dev manipulations are done by simply modifying a config dict

* test: do not set cache.type for remotes that doesn't support it

* dvc: fix windows absolute paths validation in config

* test: fix windows separate drive test

* dvc: stylistic fixes in config
2020-02-13 19:35:37 +02:00
Paweł Redzyński ccca126749 add: meaningful message upon adding overlapping paths (#3296) 2020-02-13 05:31:57 +02:00
Guro Bokum 7e6e8ee169 api: add list command (#3246)
Close #2509
2020-02-12 22:10:48 +02:00
Mr. Outis 753aef4910 black: bump version 19.3b0 -> 19.10b0 (#3307) 2020-02-12 02:41:24 +02:00
Ruslan Kuprieiev 9a78c0e2fd dvc: bump to 0.84.0 2020-02-11 22:09:59 +02:00
Ruslan Kuprieiev 7200bddd09 dvc: bump to 0.83.1 2020-02-11 22:08:30 +02:00
Alexander Schepanovski 47081cb118 erepo: cache all read only external repos by hexsha (#3286)
* erepo: cache all read only external repos by hexsha

So we have 3 things cached now separately:
- clean clones, to not ask for creds repatedly
- cache dirs, also shared between erepos with same origin
- checked out clones if they are read only, addressed by (url, hexsha)

Several additions to `Git` along the way:
- Git.is_sha() static method
- .pull() and .push() work with multiple returned records correctly
- .get_rev() and .resolve_rev() work faster
- .resolve_rev() looks for remote branches
- .has_rev()

Fixes #3280.

* git: improve .resolve_rev()

It follows `git checkout` logic now - if name can be unambiguously
resolved across known remotes then it's done.
2020-02-11 19:43:15 +02:00
Ivan Shcheklein 5e98a702a2 GDrive improvements and code cleanup (#3294)
* cleanup gdrive code, fix remote ids resolving

* fixes after testing GDrive cleanup

* address CodeClimate complexity, simplify a litte bit

* address CodeClimate complexity, further simplify complexity

* further refactoring need to fix GDrive tests

* tests: minor setup gdrive cloud simplification
2020-02-11 19:37:03 +02:00
Ruslan Kuprieiev 2361bceb70 repro: fix bug in _get_active_graph (#3301)
We are currently removing all of the deps stages from the DAG even if
those deps are referenced by some parallel branches of execution.
The new approach is to remove edges and then gradually remove nodes if
there are no more `in` edges left.

Discord context:
https://discordapp.com/channels/485586884165107732/485596304961962003/676464313942409265
2020-02-11 01:45:43 +02:00
Ruslan Kuprieiev 0c8897949e diff: use rev instead of ref (#3299)
Fixes #3245
2020-02-11 00:13:23 +02:00
Saugat Pachhai 8610c0c95e repo: raise exception if root_dir is not a directory path (#3295)
* repo: throw if root_dir in Repo.__init__() is not a directory path

When Repo is instantiated with invalid urls such as 'http://github.com/iterative/dvc.git',
it does os.path.realpath which is not intelligent enough and just concatenates with current
working directory, which is then traversed outward. It might happen that the outward folder
is a dvc directory which is not really what we want.

So, a check for root to be a directory is added. Otherwise, NotDvcRepoError is thrown with
a custom message.

* repo: set message from raise for NotDvcRepoError
2020-02-10 20:57:34 +02:00
Jorge Orpinel 7821fa6866 get/import: they don't work with any DVC project, only DVC repos (as well as Git repos) (#3277)
* get/import: update cmd and url arg descs with correct behavior

* get/import: update path arg desc as well (See previous commit)

* get/imp: udpate help output with simpler terms
per https://github.com/iterative/dvc/pull/3277#pullrequestreview-352650016
2020-02-10 19:43:00 +02:00
Casper da Costa-Luis 3d5b8ecdd5 add: auto convert absolute_path => relative (#2975)
* add: auto convert absolute_path => relative

- fixes #2955

* use path_isin

* catch-all absolute paths in repo

* unneeded leftover

* revert realtive imports for later

* output: move abs2relpath from Base to Local

* output:local:fix parse_path

* remove path_info

* test:local versus base abs2relpath

* test:fix remaining abs2relpath test

* output: local path relative to wdir

* output: local: modify path before inheritance

* output: local: minor inheritance fix

* test: local: split output test

* test: local: positively explicit
2020-02-10 18:35:24 +02:00
Ruslan Kuprieiev 65ee446642 dvc: bump to 0.83.0 2020-02-10 01:42:31 +02:00
Ruslan Kuprieiev d305803015 analytics: ignore request.post errors (#3292)
Users have been complaining about dvc analytics errors when they are not
connected to the internet.
2020-02-10 01:41:42 +02:00
Ruslan Kuprieiev 2786a4e59b logger: allow exc_info in all record types (#3290)
This better reflects the original logger formatter.

This also fixes annoying redundant newline between traceback and "need
any help" footer.
2020-02-09 22:22:20 +02:00
Ruslan Kuprieiev afcbc7e17a deps: update pydrive2 version 2020-02-09 18:48:38 +02:00
Ruslan Kuprieiev 0bb5a8210a dvc: bump to 0.82.10 2020-02-09 18:47:24 +02:00
Max Risuhin 90ee067972 gdrive: add gc command support (#3288) 2020-02-09 18:46:05 +02:00
Aljoscha SteffensandRuslan Kuprieiev e7b3297c2d check root .gitignore in '_ignored' (#3284)
* made '_ignored' check if the given file is already ignored by the root .gitignore. Added corresponding functional test

* code formatting

* Changed method input, adjusted tests, formatted code

* typo

* tests: use git logger fix in conftest

See https://github.com/iterative/dvc/issues/3167

* scm: git: remove redundant comments

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-02-06 19:52:27 +02:00
Alexander Schepanovski e28371f53a Fix blockers list in rwlock error message (#3285) 2020-02-06 16:25:17 +02:00
Mr. OutisandRuslan Kuprieiev 509ce3e16e rwlock: require info keys to be present in schema (#3283)
Co-authored-by: Ruslan Kuprieiev <[email protected]>

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2020-02-05 19:24:41 +02:00
Ruslan Kuprieiev d9ed98b951 Merge pull request #3270 from mroutis/fix-3164
rwlock: include actionable in error message
2020-02-05 13:05:49 +02:00
Mr. Outis e6ed5cc6cf rwlock: check blockers on a single method 2020-02-04 20:41:56 -06:00
Ruslan Kuprieiev cb06a91368 dvc: bump to 0.82.9 2020-02-05 03:20:52 +02:00
Ruslan Kuprieiev 93d1b6dcb0 Merge pull request #3217 from fabiosantoscode/feature/1744-pipeline-eliminate-extra-connection
pipeline: show: outs: eliminate extra edges in DAG
2020-02-04 02:02:31 +02:00
Ruslan Kuprieiev ed04a9f5ff Merge pull request #3276 from fabiosantoscode/patch-1
commit: add missing space
2020-02-04 01:49:27 +02:00
Fábio Santos 758205a52a PR feedback 2020-02-03 18:45:44 +00:00
Fábio Santos 11ad0a9803 remove __ 2020-02-03 18:41:55 +00:00
Fábio Santos 2e091f67b5 commit: add missing space
The prompt shown when committing without `-f` had a missing space between sentences.
2020-02-03 17:30:18 +00:00
Ruslan Kuprieiev de1f67afbd Merge pull request #3244 from jorgeorpinel/master
diff/metics diff: update argument descriptions, et al.
2020-02-03 02:15:54 +02:00
Ruslan Kuprieiev 633e126ba9 dvc: bump to 0.82.8 2020-02-02 17:59:01 +02:00
Ruslan Kuprieiev 82eaa52fab Merge pull request #3274 from efiop/3248
remote: bring back manual close_pools()
2020-02-02 17:56:14 +02:00
Ruslan Kuprieiev 06ca3b7f82 remote: bring back manual close_pools()
We used to have this line previously, thinking that it was only a py2
issue, but it turned out that it happens on py3 as well. So bring this
line back for now.
2020-02-02 16:57:23 +02:00
Mr. Outis 1c17d02f57 rwlock: include actionable in error message
Fix: #3164
2020-02-01 20:22:20 -06:00
Jorge Orpinel c27f6efa65 diff: remove backquotes around "HEAD"
per https://github.com/iterative/dvc/pull/3244#pullrequestreview-351263562
2020-01-31 20:05:47 -06:00
Jorge Orpinel e70d5fdbc3 remove unnecessary line from PR template 2020-01-31 19:59:57 -06:00
Ruslan Kuprieiev 945a517f6c Merge pull request #3260 from mroutis/change-diff-format
diff: format summary to make it more human readable
2020-02-01 01:11:09 +02:00
Ruslan Kuprieiev 2674689442 dvc: bump to 0.82.7 2020-01-31 23:30:32 +02:00
Ruslan Kuprieiev fd6bd29303 setup: move pydot to install_requires
https://github.com/iterative/dvc/issues/3267
2020-01-31 22:51:41 +02:00
Ruslan Kuprieiev 92bbd437f3 Merge pull request #3268 from Suor/isdir
dvc: do not check for isdir on recursive out collect
2020-01-31 22:45:35 +02:00
Ruslan Kuprieiev 2613e122ff Merge pull request #3263 from casperdcl/shfmt
lint:beautysh pre-commit hook
2020-01-31 22:44:30 +02:00
Casper da Costa-Luis b0b69ce819 lint/pre-commit/ci: replace shfmt => beautysh 2020-01-31 19:14:51 +00:00
Casper da Costa-Luis 4e87bf9e0e try beautysh 2020-01-31 19:07:53 +00:00
Alexander Schepanovski 6545d2178b dvc: do not check for isdir on recursive out collect
The things is we create dirs as needed, so if outs are not checked out a
directory contraining nothing but outs may be absent.
2020-01-31 19:36:36 +07:00
Jorge Orpinel 6984546ae2 diff/metrics diff: simplify b_ref desc
per https://github.com/iterative/dvc/pull/3244#pullrequestreview-351111761
2020-01-30 18:25:11 -06:00
Jorge Orpinel 611c1d600f scripts: update autocomplete per diff changes
per https://github.com/iterative/dvc/pull/3244#pullrequestreview-351108115
2020-01-30 18:23:58 -06:00
Jorge Orpinel 5019fe147c term: use "hash" instead of "checksum" in a few files
per https://github.com/iterative/dvc/pull/3244#pullrequestreview-351108115
2020-01-30 18:13:36 -06:00
Casper da Costa-Luis 0f13f47269 ci:fix silly go check 2020-01-30 23:42:29 +00:00
Casper da Costa-Luis 605c4af9c6 ci:split checks into py & go jobs 2020-01-30 23:37:53 +00:00
Casper da Costa-Luis cf257f9617 ci:still try to use go 2020-01-30 23:25:57 +00:00
Casper da Costa-Luis 493a340531 ci:attempt python=>go for checks 2020-01-30 23:17:59 +00:00
Casper da Costa-Luis 7a9512d02e ci:fix go version 2020-01-30 22:55:19 +00:00
Casper da Costa-Luis 2734f49b9e ci:check:fix go install 2020-01-30 22:50:41 +00:00
Casper da Costa-Luis 22dd0d47eb lint:pre-commit shfmt defaults 2020-01-30 22:40:56 +00:00
Casper da Costa-Luis f8c01e7fb3 lint:pre-commit shfmt hook 2020-01-30 22:37:31 +00:00
Casper da Costa-Luis 98d2ca00c0 lint: shfmt with -ci 2020-01-30 22:35:48 +00:00
Casper da Costa-Luis 10c5103ed7 lint: run shfmt -i 2 2020-01-30 22:26:49 +00:00
Casper da Costa-Luis 2e79ce3318 lint: run shfmt
Related to #2834 and #2969.
2020-01-30 21:22:12 +00:00
Mr. Outisand@dmpetrov e5145e6c55 diff: format summary to make it more human readable
Co-authored-by: @dmpetrov
2020-01-30 13:15:49 -06:00
Jorge Orpinel 039f196dd8 Update Dmitry's email address in several places
per https://github.com/iterative/dvc/pull/3244#pullrequestreview-349888033
2020-01-30 12:25:40 -06:00
Jorge Orpinel 4403d66ac6 metrics diff: use same param desc as in simple diff 2020-01-30 12:24:31 -06:00
Jorge Orpinel aa849ab2d5 Merge branch 'master' of github.com:iterative/dvc 2020-01-30 12:24:22 -06:00
Ruslan Kuprieiev d864caf79b Merge pull request #3251 from Suor/summon-publish
api: prepare for dvcx summon/publish
2020-01-30 19:56:43 +02:00
Ruslan Kuprieiev fd1e6f457e Merge pull request #3229 from mroutis/enhance-diff
diff: reimplement interface and tests from scratch
2020-01-30 19:24:17 +02:00
Alexander Schepanovski f8c4001e35 scm: add git pull/push to Git 2020-01-31 00:03:41 +07:00
Alexander Schepanovski b4b5781a14 dvc: support inheritance in Repo.__repr__ 2020-01-31 00:02:49 +07:00
Alexander Schepanovski 0eb4e7e15f api: move summon out
Will be added to dvcx instead.
2020-01-31 00:02:28 +07:00
Mr. Outis 673afe20b1 diff: be windows friendly at file counting 2020-01-30 10:28:02 -06:00
Mr. Outis 793a069644 tests/diff: order dictionary (second try) 2020-01-30 10:00:56 -06:00
Mr. Outisand@jorgeorpinel 2c2295700d wording: update command help
Co-authored-by: @jorgeorpinel <[email protected]>
2020-01-30 09:52:53 -06:00
Mr. Outis e9bd9599a2 tests/diff: dont care about sorted jsons 2020-01-30 09:26:35 -06:00
Ruslan Kuprieiev b78ef78507 dvc: bump to 0.82.6 2020-01-30 12:03:23 +02:00
Ruslan Kuprieiev 14075c4658 Merge pull request #3253 from efiop/3241
install: fix post-checkout hook
2020-01-30 12:02:55 +02:00
Mr. Outis e501549bd7 diff: add a summary at the bottom 2020-01-29 18:54:45 -06:00
Ruslan Kuprieiev 13ed7a1c32 install: fix post-checkout hook
We should skip running post-checkout hook on rebases and merges to not
accidentally break them in the middle of the process. Git doesn't
provide any native mechanisms for hooks to know whether or not this is a
rebase/merge that we are going through, but we can use
`.git/rebase-merge` existance as a marker.

Previous approach has only accounted for branches and was causing issues
when checking-out tags.

Fixes #3241
2020-01-30 02:19:13 +02:00
Mr. Outis debf6cb510 scm: dead code 2020-01-29 18:00:23 -06:00
Mr. Outis 8e9d8145cd diff: remove deadcode, replace diffable with dicts 2020-01-29 17:42:35 -06:00
Ruslan Kuprieiev 90773ed4f3 dvc: bump to 0.82.5 2020-01-29 22:08:41 +02:00
Ruslan Kuprieiev 3d62db5a02 Merge pull request #3252 from iterative/corpora-issue
gdrive: Init drive if corpora property value requested.
2020-01-29 22:07:59 +02:00
Max Risuhin 8f3a7ff342 gdrive: Init drive if corpora property value requested. 2020-01-29 11:43:43 -08:00
Mr. Outis 76f5514a92 refactor 2020-01-29 13:38:03 -06:00
Alexander Schepanovski 614515e631 api: prepare for dvcx summon/publish
Also removed type python summons from `dvc.api`
2020-01-30 01:15:47 +07:00
Ruslan Kuprieiev 57dae7a0c1 Merge pull request #2846 from casperdcl/add
UI progress cleanup
2020-01-29 19:38:05 +02:00
Jorge Orpinel 1672cdfb06 Update Dmitry's email address ) 2020-01-29 00:39:46 -06:00
Jorge Orpinel d0139a7826 term: review "revision", "reference", and "hash", "SHA" terms 2020-01-29 00:38:41 -06:00
Jorge Orpinel b3ff72c983 term: review usage of "version" in strings 2020-01-29 00:21:30 -06:00
Jorge Orpinel b10057a1b2 metrics diff: update help desc.
per iterative/dvc.org/pull/933
2020-01-29 00:12:16 -06:00
Jorge Orpinel 2430cb5dea diff: remve unused DIFF_HELP 2020-01-29 00:08:12 -06:00
Dmitry Petrov e6d0d291e2 Code review feedback and change schema to dict of dvc-objects 2020-01-28 18:44:36 -08:00
Ruslan Kuprieiev 12cae708a8 dvc: bump to 0.82.4 2020-01-29 00:46:11 +02:00
Ruslan Kuprieiev d16e71d767 Merge pull request #3239 from iterative/fix-shared-drives-support
[WIP] Set corpora and driveId params values based on type of remote disk - shared or user's.
2020-01-29 00:44:41 +02:00
Max Risuhin 7b0e44e293 gdrive: support corpora and driveId 2020-01-28 11:50:21 -08:00
Fábio Santos a7bad88159 remove unnecessary chdir 2020-01-28 11:49:24 +00:00
Mr. Outis f41ceb51da tests: adjust doc 2020-01-27 17:03:50 -06:00
Mr. Outis 7f6324e298 diff: remove --target 2020-01-27 16:06:46 -06:00
Mr. Outis 3c6b52bb91 diff: improve RevError message 2020-01-27 14:27:56 -06:00
Mr. Outis a12d81fd0a tests/diff: fix windows compat issues 2020-01-27 14:13:54 -06:00
Mr. Outis 423a6163bc diff: make --checksums work with --json-show 2020-01-27 14:10:37 -06:00
Jorge Orpinel bfa1d3e188 diff/metics diff: update argument descriptions
to match https://github.com/iterative/dvc.org/pull/933
2020-01-27 13:43:43 -06:00
Mr. Outis fced1718cc diff: --json -> --show-json 2020-01-27 12:53:24 -06:00
Mr. Outis 2370cb12b3 diff: use logger instead of print 2020-01-27 12:33:24 -06:00
Mr. Outis 9171b9e0ad 💅 add reference explaining git revisions 2020-01-27 12:07:28 -06:00
Mr. Outis 9d14acd87f 💅 remove excesive parenthesis 2020-01-27 11:48:02 -06:00
Ruslan Kuprieiev 242b5b18f6 Merge pull request #3193 from mroutis/fix-3192
config: verify before saving a modification
2020-01-27 18:08:53 +02:00
Ruslan Kuprieiev 230e5c86d8 config: use validate() more explicitly 2020-01-27 16:38:46 +02:00
Ruslan Kuprieiev c38589bb0d Merge pull request #3234 from Suor/git-pull-p2
erepo: make it better
2020-01-27 14:19:59 +02:00
Alexander Schepanovski 4e27dedeb4 erepo: make it better
- code moving during git pull won't affect any erepos
- source tag move will work as expected
- source cache dir setting will never break havoc
- cache is reused between all clones from same source
2020-01-27 18:58:30 +07:00
Mr. Outis 10c0ff22d6 diff: remove extra sorting 2020-01-27 01:40:24 -06:00
Mr. Outis e6a957a426 tests/diff: add directory to no_cache_entry 2020-01-26 23:24:24 -06:00
Mr. Outis 27ada0210d scripts: update completion scripts for diff 2020-01-26 22:51:29 -06:00
Mr. Outis bb22acd4c9 diff: enable using diff when there is no cache 2020-01-26 22:45:35 -06:00
Mr. Outis 6ef90f5acf diff: add option to display checksums 2020-01-26 20:22:53 -06:00
Mr. Outis fe4eba5941 diff: handle exceptions 2020-01-26 17:50:47 -06:00
Casper da Costa-Luis ab115dfe74 Recursing => Searching 2020-01-26 23:34:57 +00:00
Casper da Costa-Luis b49a61d247 minor message rename
Responding to review request
2020-01-26 23:34:56 +00:00
Casper da Costa-Luis 4ae9b7584b minor import tidy 2020-01-26 23:34:42 +00:00
Casper da Costa-Luis 61a1e7f774 backtrack pbars after failing when adding nonexistent files 2020-01-26 23:34:42 +00:00
Casper da Costa-Luis 7fa5e694b1 silly omission 2020-01-26 23:34:41 +00:00
Casper da Costa-Luis 6de48d30ed replace funcy.merge with py3 syntax 2020-01-26 23:34:41 +00:00
Casper da Costa-Luis 528dedda03 minor import tidy 2020-01-26 23:34:41 +00:00
Casper da Costa-Luis 660fcfd10b fix disable logic 2020-01-26 23:34:40 +00:00
Casper da Costa-Luis a66cc0e503 add saving progress for files in dirs 2020-01-26 23:34:40 +00:00
Casper da Costa-Luis 25557c5882 remove potential single-iteration bar 2020-01-26 23:34:40 +00:00
Casper da Costa-Luis aa3ff022f3 minor comment 2020-01-26 23:34:40 +00:00
Casper da Costa-Luis 2a4c30892e tidy close logic 2020-01-26 23:34:36 +00:00
Casper da Costa-Luis 866b3cd0ae add progress for save/commit/dump stages 2020-01-26 23:33:39 +00:00
Casper da Costa-Luis 21c1b266cf add progress for stages; also may need reverting 2020-01-26 23:33:39 +00:00
Casper da Costa-Luis 3b65181e01 remove unneeded <??:?? for unknown ETA 2020-01-26 23:33:39 +00:00
Casper da Costa-Luis 80e8270954 add recursive file find progress
for profiling; may need to revert
2020-01-26 23:33:39 +00:00
Casper da Costa-Luis 0293f7128c minor comment and force refresh 2020-01-26 23:33:38 +00:00
Casper da Costa-Luis dd34100310 replace super bar with intermediate "Adding..." for single files 2020-01-26 23:33:38 +00:00
Casper da Costa-Luis 426972e55f disable super progress bar for single-files
TODO: actually disable sub-bars instead
2020-01-26 23:33:38 +00:00
Casper da Costa-Luis dee51991c3 black 2020-01-26 23:33:34 +00:00
Casper da Costa-Luis eb05f8bd0c clean output cleaning (meta) 2020-01-26 23:13:02 +00:00
Casper da Costa-Luis c6c8b80219 no unit scaling for small totals 2020-01-26 23:13:02 +00:00
Casper da Costa-Luis 5d10606e09 remove ETA on completion, minor tidy 2020-01-26 23:13:02 +00:00
Mr. Outis bd1adf2c23 diff: add CLI output 2020-01-26 15:12:30 -06:00
Mr. Outis a4ade13c0a diff: add JSON output 2020-01-26 13:25:38 -06:00
Mr. Outis b251682aec diff: document diffables_from_output 2020-01-26 12:55:17 -06:00
Dmitry Petrov d5a3b6f4f6 Summon dataframe support 2020-01-26 02:50:42 -08:00
Mr. Outis 3db983fc2e tests/diff: use os.path.join instead of implying separator 2020-01-26 02:00:38 -06:00
Mr. Outis 5ace747c05 diff: group by change state and support dirs 2020-01-26 01:41:48 -06:00
Mr. Outis 07abd1b41b diff: add support for specifying a target 2020-01-25 11:00:36 -06:00
Mr. Outis 576a27667f diff: fix some tests and adjust result for modify 2020-01-24 19:05:23 -06:00
Dmitry Petrov 8f799210d3 Extract Summon class 2020-01-24 16:35:17 -08:00
Ruslan Kuprieiev a42d4f2c90 dvc: bump to 0.82.3 2020-01-24 23:36:32 +02:00
Ruslan Kuprieiev 4d5faab19c Merge pull request #3213 from fabiosantoscode/feature/3121-push-no-remote-specified-hint
push: improve 'no remote specified' hint
2020-01-24 22:46:55 +02:00
Mr. Outis bc8871eccd diff: adjust some wording 2020-01-24 13:48:54 -06:00
Mr. Outis f791371224 diff: change structure to be indexed by path 2020-01-24 13:29:29 -06:00
Mr. Outis b2de740556 tests: add result examples 2020-01-24 10:30:15 -06:00
Mr. Outis ebd5a5a7f3 diff: address comments from review
* OSError -> FileNotFoundError
* Make `a_ref` optional
* Use absolute imports for unrelated packages
* Relpath: `str(output.path_info)` -> `str(output)`
* `a_ref` defaults to `HEAD` at the signature and parser level
2020-01-24 10:05:44 -06:00
Ruslan Kuprieiev 0bd0c7a61e Merge pull request #3232 from iterative/2865
Remove usage of google-api-python-client deps
2020-01-24 18:02:24 +02:00
Fábio Santos 2fb6b338bd move message creation outside of exception 2020-01-24 15:13:20 +00:00
Ruslan Kuprieiev c2def08551 Merge pull request #3228 from efiop/3107
install: post-checkout: run only on branches/tags
2020-01-24 17:02:53 +02:00
Max Risuhin 10399e8eb8 Remove usage of google-api-python-client deps 2020-01-24 05:51:33 -08:00
Ruslan Kuprieiev 4457df334b Merge pull request #3225 from pared/2647_no_cache_external_dir
remote: verify cache exists on dir checksum
2020-01-24 15:21:36 +02:00
Paweł Redzyński 6557fed55d remote: verify cache exists on dir checksum 2020-01-24 13:37:33 +01:00
Ruslan Kuprieiev a65b6c7a2b Merge pull request #3231 from pared/link_fixes
some doc link fixes
2020-01-24 13:33:16 +02:00
Paweł Redzyński 44b7216861 some doc link fixes 2020-01-24 11:57:17 +01:00
Ruslan Kuprieiev 6718622549 Merge pull request #3227 from Suor/git-pull
erepo: pull each time
2020-01-24 10:14:43 +02:00
Ruslan Kuprieiev 2f1224b27b setup: update pydrive2 version 2020-01-24 09:47:52 +02:00
Mr. Outis 198bf55aaf diff: reimplement interface and tests from scratch 2020-01-24 00:56:05 -06:00
Ruslan Kuprieiev a11b5f4377 install: post-checkout: run only on branches/tags
We shouldn't try to automatically checkout in detached HEAD state, as we
might break the rebase process.

Fixes #3107
2020-01-23 23:10:41 +02:00
Fábio Santos 233790ced5 add test and traverse nodes when showing outs 2020-01-23 17:54:00 +00:00
Alexander Schepanovski 7168a9c50a erepo: pull each time
Fixes #3062.
2020-01-23 23:09:57 +07:00
Ruslan Kuprieiev 1bc39e7e99 dvc: bump to 0.82.2 2020-01-23 16:36:42 +02:00
Ruslan Kuprieiev 4f61932c71 Merge pull request #3224 from pared/fix_import_url_test
import-url: test: prevent in-repo testing
2020-01-23 15:59:20 +02:00
Fábio Santos d827fef8c4 add missing nodes 2020-01-23 12:39:07 +00:00
Fábio Santos 84999f590e convert to bool 2020-01-23 12:24:30 +00:00
Fábio Santos 2742a58226 move remote checking option to within NoRemoteError 2020-01-23 12:23:16 +00:00
Fábio Santos b635fdddbd connect outs to deps 2020-01-23 11:54:15 +00:00
Fábio Santos a299fba15d connect deps to outs 2020-01-23 11:47:37 +00:00
Paweł Redzyński 48f8f2402a import-url: test: prevent in-repo testing 2020-01-23 12:40:21 +01:00
Ruslan Kuprieiev bbb3a831b5 Merge pull request #3216 from MaxRis/3157
Update Google Project credentials to run tests
2020-01-23 13:25:25 +02:00
Ruslan Kuprieiev 1a7eb8fdec Merge pull request #3222 from skshetry/fallback-external-repo
api: fallback to external_repo if not a DVC repository
2020-01-23 13:24:33 +02:00
Ruslan Kuprieiev a0419173ba Merge pull request #3088 from efiop/3080
dvc: add sanity checks for hard/symlinks
2020-01-23 11:48:19 +02:00
Saugat Pachhai 2e0b143856 api: use current directory for repo if not specified 2020-01-23 15:28:22 +05:45
Saugat Pachhai 003b5c392a api: fallback to external_repo on error 2020-01-23 14:44:38 +05:45
Ruslan Kuprieiev 49039968f7 Merge pull request #3208 from fabiosantoscode/feature/3198-remove-spy-method
tests: replace spy() with pytest-mock spy
2020-01-23 10:52:43 +02:00
Ruslan Kuprieiev ad6e58a457 Merge pull request #3223 from efiop/3107
hooks: checkout: fix incorrect dvc repo detection
2020-01-23 10:41:27 +02:00
Ruslan Kuprieiev f6f931be87 hooks: checkout: fix incorrect dvc repo detection
When running from non-root directory `git ls-files .dvc` will treat it
as a relative path to the current location, so if you run `git checkout`
from a directory that is not a git/dvc root, our hook will think that
it is being run in a non-dvc repo and will do nothing. To fix that, need
to use `--full-name` flag.
2020-01-23 10:21:26 +02:00
Ruslan Kuprieiev da6399dc3c Merge pull request #3220 from jorgeorpinel/master
get: copy edits aroud --show-url, "dvc", et al.
2020-01-23 09:14:00 +02:00
Jorge Orpinel 69c1aa5bbf dvc: revert capitalization of logger.exception strings
per https://github.com/iterative/dvc/pull/3220#pullrequestreview-347083819
2020-01-23 00:50:24 -06:00
Jorge Orpinel 6364c9ee62 get: refine --show-url description
per https://github.com/iterative/dvc/pull/3220#pullrequestreview-347038086
2020-01-23 00:39:10 -06:00
Jorge Orpinel d06bd6abd8 tests: match recent commits and
other misc. language improvements
2020-01-23 00:35:15 -06:00
Jorge Orpinel ec0c39a28e term: review capitalization and backquotes around "dvc"
rel: https://github.com/iterative/dvc/pull/3156#discussion_r369897073
2020-01-22 20:27:01 -06:00
Jorge Orpinel ee0193ccd3 get: rewrite --show-url description
per https://github.com/iterative/dvc.org/pull/936#discussion_r368680473
2020-01-22 20:12:19 -06:00
Jorge Orpinel ae3455141c get: language feedback on --show-url PR
per https://github.com/iterative/dvc/pull/3156#pullrequestreview-345562608
2020-01-22 19:47:57 -06:00
Fábio Santos 058e19fed0 pipeline: show: outs: eliminate extra nodes in DAG 2020-01-22 21:30:12 +00:00
Ruslan Kuprieiev e9caa44b55 Merge pull request #3214 from jorgeorpinel/master
get/imp: update help output descriptions
2020-01-22 23:03:07 +02:00
Max Risuhin cb38b1a0f1 Update Google Project credentials to run tests 2020-01-22 12:19:21 -08:00
Ruslan Kuprieiev 000c524983 Merge pull request #3212 from casperdcl/snap-deploy
another attempt at travis dpl snap
2020-01-22 21:01:56 +02:00
Jorge Orpinel 6e61236fb7 get/imp: update help output descriptions
per https://github.com/iterative/dvc/issues/3196#issuecomment-577306219
2020-01-22 12:00:06 -06:00
Fábio Santos fc92266114 address comments 2020-01-22 17:31:46 +00:00
Fábio Santos abbe29e58e push: improve 'no remote specified' hint
Fixes #3121
2020-01-22 17:09:00 +00:00
Ruslan Kuprieiev d863c46859 dvc: add sanity checks for hard/symlinks
Fixed #3080
2020-01-22 18:47:37 +02:00
Casper da Costa-Luis 66ad722964 Revert "debugging (to revert)"
This reverts commit 555258c75d.
2020-01-22 16:28:43 +00:00
Casper da Costa-Luis 7526eb1a2f fix before_install checks 2020-01-22 16:08:24 +00:00
Casper da Costa-Luis f02c804cad tidy up 2020-01-22 16:04:42 +00:00
Casper da Costa-Luis a4a31b3113 fix missing env vars 2020-01-22 16:00:48 +00:00
Casper da Costa-Luis 555258c75d debugging (to revert) 2020-01-22 15:52:33 +00:00
Casper da Costa-Luis ba2c84bade attempt to fix travis snap dpl 2020-01-22 15:23:57 +00:00
Fábio Santos e41aa00c20 convert tests to pytest function tests 2020-01-22 14:04:52 +00:00
Ruslan Kuprieiev b99c5e4aad Merge pull request #3211 from skshetry/fix-brancher-noscm
brancher: do not get tree if there's no revs
2020-01-22 14:09:53 +02:00
Saugat Pachhai 5f8b1f7f80 brancher: do not get tree if there's no revs 2020-01-22 17:22:54 +05:45
Ruslan Kuprieiev d2d887bb50 Merge pull request #3200 from pared/2676_trust_checksum
fetch: trust remote checksums
2020-01-22 12:36:29 +02:00
Paweł Redzyński 482dfaf998 data cloud: refactor verify checksums test 2020-01-22 10:54:07 +01:00
pawel e36cc8c9d2 data cloud: add explanaiton to pulled checksums save 2020-01-22 10:54:07 +01:00
pawel a148d16a95 data cloud: verify is config option now 2020-01-22 10:54:07 +01:00
pawel f4e0079ec1 data sync: add command unit test 2020-01-22 10:54:07 +01:00
pawel b451de5496 remote: tests: refactor verify test 2020-01-22 10:54:07 +01:00
pawel ef1b06ddf9 test_remote: use mocker.spy instead of spy 2020-01-22 10:54:07 +01:00
pawel 0b1d1f5ce4 data cloud: pull: trust remotes md5 on proper flag 2020-01-22 10:54:07 +01:00
Ruslan Kuprieiev d942542c84 tests: fix index.remove misuse 2020-01-22 10:53:48 +02:00
Ruslan Kuprieiev a99fa4e4e5 Merge pull request #3209 from casperdcl/snap-deploy
fix snap stable & edge release errors
2020-01-22 08:26:02 +02:00
Casper da Costa-Luis a6d55e3252 ensure correct snap version dirtyness 2020-01-21 22:38:09 +00:00
Casper da Costa-Luis 8a560ecaba pin snap version to base version 2020-01-21 22:31:40 +00:00
Fábio Santos bdf665a34a don't use patch.object() 2020-01-21 20:52:48 +00:00
Casper da Costa-Luis c13b24fb40 fix master/edge release provider argument conflicts 2020-01-21 19:50:22 +00:00
Ruslan Kuprieiev aa86eab2cc Merge pull request #3206 from Suor/tests
test: add git_dir
2020-01-21 20:19:35 +02:00
Fábio Santos 5dfcda54f9 tests: replace spy() with pytest-mock spy
Fixes #3198
2020-01-21 14:47:49 +00:00
Alexander Schepanovski 5af7a9d850 test: add git_dir 2020-01-21 20:56:04 +07:00
Ruslan Kuprieiev 6958901d98 Merge pull request #3204 from efiop/3087
erepo: fix DependencyREPO schema
2020-01-21 15:31:22 +02:00
Ruslan Kuprieiev cb64491512 build: don't upgrade pip 2020-01-21 14:41:59 +02:00
Ruslan Kuprieiev 8d8a1c0f0c travis: don't upgrade pip 2020-01-21 14:14:54 +02:00
Ruslan Kuprieiev 7c9e0345a0 Merge pull request #3205 from skshetry/fix-spell
exception: fix spelling
2020-01-21 13:56:59 +02:00
Saugat Pachhai df1ba08b94 exception: fix spelling 2020-01-21 17:24:21 +05:45
Ruslan Kuprieiev 2305549460 erepo: fix DependencyREPO schema
URL should be required and rev or rev_lock should be optional.

Related to #3087
2020-01-21 12:58:33 +02:00
Ruslan Kuprieiev 10a7b9d9a8 readme: add a release label
To avoid accidentally looking like an unstable project.
2020-01-21 10:03:16 +02:00
Ruslan Kuprieiev 04161ad6f2 dvc: bump to 0.82.1 2020-01-21 00:41:45 +02:00
Ruslan Kuprieiev 503401dde5 Merge pull request #3202 from efiop/flatten
dvc: replace flatten_dict with flatten_json
2020-01-21 00:38:19 +02:00
Ruslan Kuprieiev 81029f8f75 Merge pull request #3201 from efiop/traverse_gdrive
gdrive: allow configuring no_traverse
2020-01-21 00:19:11 +02:00
Ruslan Kuprieiev 1eb82ac707 dvc: replace flatten_dict with flatten_json
flatten_json has a conda package, while flatten_dict doesn't and I'm not
willing to maintain yet another conda package :)
2020-01-21 00:14:22 +02:00
Ruslan Kuprieiev e848bcb9d6 gdrive: allow configuring no_traverse 2020-01-20 23:49:49 +02:00
Ruslan Kuprieiev 39bf7c0d43 Merge pull request #3199 from skshetry/remove-use-six
py2: remove use of `six`
2020-01-20 19:19:23 +02:00
Saugat Pachhai 8dd2dbb46e py2: remove use of six 2020-01-20 21:47:56 +05:45
Ruslan Kuprieiev 0be5046a72 Merge pull request #3197 from Suor/refactor-tmpdir
test: refactor tmp_dir/erepo_dir
2020-01-20 16:02:13 +02:00
Alexander Schepanovski 91e024c8fa test: do not commit copy.py to git 2020-01-20 20:35:10 +07:00
Ruslan Kuprieiev b2b3587c0b Merge pull request #3190 from Suor/refactor-erepo
external: refactor external_repo() and its users
2020-01-20 14:49:36 +02:00
Alexander Schepanovski 1d891a6e77 test: provide make_tmp_dir fixture 2020-01-20 11:57:54 +07:00
Alexander Schepanovski 4117206c67 test: drop repo_template fixture 2020-01-20 11:49:13 +07:00
Ruslan Kuprieiev ec2c8ad8bc Merge pull request #2956 from casperdcl/snap
add snap tests
2020-01-19 21:36:47 +02:00
Casper da Costa-Luis f80626b5d0 recompress conditional snap channel 2020-01-19 19:10:25 +00:00
Casper da Costa-Luis de358d661a explicitly spilt snap stable/edge deployments
- (re) fix #3152
2020-01-19 19:10:05 +00:00
Casper da Costa-Luis 22ed9cb5dc Remove travis-incompatible tests
- reverts
  + 205077e3a0.
  + 46e0616614.
2020-01-19 18:27:59 +00:00
Casper da Costa-Luis 205077e3a0 debugging 2020-01-19 18:19:01 +00:00
Casper da Costa-Luis 2fe9b3a967 connectivity debugging 2020-01-19 18:19:01 +00:00
Casper da Costa-Luis 5ac849377a minor comment update 2020-01-19 18:19:01 +00:00
Casper da Costa-Luis 0a0b4e019e debugging 2020-01-19 18:19:01 +00:00
Casper da Costa-Luis d020c3d8b0 test snaps even where tag is undefined 2020-01-19 18:19:01 +00:00
Casper da Costa-Luis b756f77787 revert whitespace changes, but keep stderr errors 2020-01-19 18:19:01 +00:00
Casper da Costa-Luis 46e0616614 promote snap tests for debugging 2020-01-19 18:19:01 +00:00
Casper da Costa-Luis 39ef3dba1a add snap tests
- fixes #2954
2020-01-19 18:17:16 +00:00
Ruslan Kuprieiev 430b9bbdc0 dvc: bump to 0.82.0 2020-01-19 20:04:41 +02:00
Alexander Schepanovski 72812b519e api: fix get_url() non-dvc repo error message 2020-01-19 23:01:53 +07:00
Alexander Schepanovski eb97e0b393 external: do not fail with no remote if cache is available 2020-01-19 21:39:32 +07:00
Alexander Schepanovski ee022eada0 api: adjust get_url requiring dvc repo to the new reality 2020-01-19 21:38:37 +07:00
Alexander Schepanovski 9de0643d97 api: allow rev on local repos 2020-01-19 21:38:30 +07:00
Alexander Schepanovski 033ef88a4e external: refactor external_repo() and its users
Some things are fixed along the way:
- no cache shared between incompatible funcs
- some incosistent exceptions are now consistent
- import updates for git files now works both for dvc and git repos
- "auto-generated-upstream already exists" fixed
- dvc.api.open()/read() works with git repos now

Fixes #3124, #2976. Closes #3172.
2020-01-19 21:38:06 +07:00
Ruslan Kuprieiev 7d65002bf0 Merge pull request #3178 from efiop/1843
logger: remote: use lazy formatting
2020-01-19 12:16:01 +02:00
Ruslan Kuprieiev 19acded31f logger: utils: fs: use lazy formatting 2020-01-19 11:53:29 +02:00
Ruslan Kuprieiev 65c0c7e60d logger: state: use lazy formatting 2020-01-19 11:53:29 +02:00
Ruslan Kuprieiev c2dae4b49c logger: remote: use lazy formatting
Part of #1843

As our previous investigations showed, path stringification and stuff
like relpath are taking a very large chunk of time when working with
giant directories. This patch removes `format()`s and uses lazy
formatting provided by logger instead, so stuff like path_info is not
stringified until actually needed (e.g. on `-v`).
2020-01-19 11:53:29 +02:00
Ruslan Kuprieiev 6f8ac0c1b1 logger: support {} formatting 2020-01-19 11:53:29 +02:00
Mr. Outis 566cc6c862 tests: move tests from remote to config 2020-01-19 02:29:07 -06:00
Ruslan Kuprieiev 3987086275 Merge pull request #3169 from fabiosantoscode/feature/2963-dvc-pull-of-s3-remote-with-https-url
pull: treat HTTP redirects without Location header as error
2020-01-19 10:00:05 +02:00
Mr. Outis efcddb6f5b remote: fix merging two different config levels 2020-01-19 01:56:24 -06:00
Ruslan Kuprieiev 74c5af1a84 Merge pull request #3130 from jorgeorpinel/master
api: better code documentation
2020-01-19 09:05:52 +02:00
Ruslan Kuprieiev cf013576d4 Merge pull request #3120 from sharidas/no_remote_issue
Fix import issue without remote config in the target
2020-01-19 09:03:38 +02:00
Ruslan Kuprieiev 1fb79fa6e9 Merge pull request #3188 from mroutis/fix-3184
completion: get --show-url
2020-01-19 08:55:48 +02:00
Ruslan Kuprieiev 3c73e94b4e Merge pull request #3195 from mroutis/fix-boto3-import
s3: import boto3 inside _copy instead of module level
2020-01-19 08:29:52 +02:00
Sujith H 1b7eda2745 Fix import issue without remote config in the target
Fix the import issue when target does not have
remote config.

Signed-off-by: Sujith H <[email protected]>
2020-01-19 11:42:05 +05:30
Mr. Outis de1de68f90 s3: import boto3 inside _copy instead of module level
Kudos to @skshetry for bringing this out during a review
2020-01-18 23:01:50 -06:00
Mr. Outis 263e57e5e6 config: verify before saving a modification
Close #3192
2020-01-18 21:36:24 -06:00
Mr. Outis 6ad55ec3b0 completion: get --show-url 2020-01-17 18:50:08 -06:00
Jorge Orpinel 2b818e203f api: note get_url doesn't chekc file existence
per https://github.com/iterative/dvc/pull/3130#discussion_r367359439
2020-01-17 17:40:02 -06:00
Jorge Orpinel 4d05ad2789 Merge branch 'master' of github.com:iterative/dvc 2020-01-17 17:39:51 -06:00
Ruslan Kuprieiev 7f5e4a9fe0 Merge pull request #3187 from iterative/pull-3186-restyled
Restyle s3: copy large objects without chunking
2020-01-17 20:38:36 +02:00
Restyled.io c3628c87d5 Restyled by black 2020-01-17 18:00:19 +00:00
Andy Sarroff 0bd433d153 s3: copy large objects without chunking
When executing `boto3.s3.Client.Copy`, large files are chunked into
multiple parts. The default chunk threshold and size is 8MB. This
caused an issue when caching an object larger than 8M, where the
original object had not been uploaded in multiple parts. The original
object and the cached object would have different ETags, causing an
exception. This fixes the issue by dynamically setting the chunk
threshold to be one byte larger than the size of the original object
therefore avoiding multipart copies.

Fixes #3174
2020-01-17 12:00:42 -05:00
Ruslan Kuprieiev 4d49b6ba1d Merge pull request #3170 from pared/3105
utils: resolve_output: normpath before extracting basename
2020-01-17 12:41:25 +02:00
Paweł Redzyński cf08529362 utils: add tests for resolve out 2020-01-17 09:51:07 +01:00
Paweł Redzyński 34d3261e58 utils: resolve_output: normpath before extracting basename 2020-01-17 09:29:52 +01:00
Ruslan Kuprieiev bdd1d61803 Merge pull request #3156 from skshetry/get-url-from-cache
get: implement --show-url to display only url/path to remote
2020-01-17 10:15:06 +02:00
Ruslan Kuprieiev 6ebd053c0f Merge pull request #3165 from mroutis/remove-py2-compat
setup: remove py2 backports
2020-01-17 04:28:22 +02:00
Ruslan Kuprieiev 422ff579f4 Merge pull request #3173 from casperdcl/snap-deploy
minor comments
2020-01-17 01:11:49 +02:00
Casper da Costa-Luis d34aa4471b minor comments
- explains review concerns in #3158
2020-01-16 20:36:42 +00:00
Fábio Santos df22dd4def specifically look for Location header in response 2020-01-16 17:58:44 +00:00
Saugat PachhaiandPaweł Redzyński 98f6d56e8c test: refactor, apply suggestion from @pared
Co-Authored-By: Paweł Redzyński <[email protected]>
2020-01-16 22:59:58 +05:45
Saugat Pachhai 02a73a0d9b unit: test: check output of get --show-url command 2020-01-16 22:52:35 +05:45
Ruslan Kuprieiev 74bbc3bc04 Merge pull request #3135 from rxxg/master
Perf improvement for file copies
2020-01-16 18:46:58 +02:00
rxxg 083a630b44 Fallback to standard Python code in case of missing packages 2020-01-16 17:08:07 +01:00
Saugat Pachhai 50206d44dc test: fix test related to path on windows
On windows, `str(exc_info)` is returning escaped path, eg:
'<ExceptionInfo UrlNotDvcRepoError("URL \'C:\\\\Users\\\\travis\\\\AppData\\\\Local\\\\Temp\\\\pytest-of-travis\\\\pytest-0\\\\popen-gw2\\\\test_get_url_git_only_repo_thr0\' is not a dvc repository.") tblen=2>'

This commit just replaces the double slashes with single one. Bit of a hack, I know.
2020-01-16 21:06:52 +05:45
Ruslan Kuprieiev d081627501 Merge pull request #3150 from fabiosantoscode/feature/2959-status-of-imported-files
status: implement support for imported files
2020-01-16 17:11:31 +02:00
Fábio Santos fcb7681683 take exception transformation into account 2020-01-16 14:49:51 +00:00
Fábio Santos cd3ef1850d simplify control flow 2020-01-16 14:44:09 +00:00
Fábio Santos c5eb13c114 update redirect check 2020-01-16 14:34:09 +00:00
Fábio Santos d3f925ae63 add missing return 2020-01-16 14:30:03 +00:00
Fábio Santos 968e74890d pull: treat HTTP redirects without Location header as error
Fixes #2963
2020-01-16 14:21:59 +00:00
Saugat Pachhai aa7119a741 get: test get_url raises exception if not a dvc repo 2020-01-16 19:28:14 +05:45
Saugat Pachhai f130e18e2c get: use new exception from public api 2020-01-16 19:28:14 +05:45
Saugat Pachhai fadf60eea6 api: throw exception by get_url if not a dvc repo 2020-01-16 19:28:14 +05:45
Saugat Pachhai eb252e055b get: split command for getting url and getting files from url functions 2020-01-16 19:28:14 +05:45
Saugat Pachhai 4509f82bc3 get: implement --show-url to display only url/path to remote 2020-01-16 19:28:14 +05:45
Fábio Santos a9bdaec63c rework code path; remove extraneous context generator 2020-01-16 13:37:13 +00:00
Fábio Santos 5883d3dabf reorder imports 2020-01-16 12:54:05 +00:00
Fábio Santos a0bdaa4c9e use checksum facilities 2020-01-16 12:51:20 +00:00
Ruslan Kuprieiev 1f1fb03138 Merge pull request #3142 from pared/remove_fixtures
conftest: remove dvc_repo, erepo, git, git_erepo
2020-01-16 13:20:45 +02:00
Ruslan Kuprieiev d160f83717 Merge pull request #3017 from pared/errno_24_message
point to troubleshooting guide on too many open files error
2020-01-16 12:56:14 +02:00
Paweł Redzyński 36c64c66db test_get_from_non_dvc_master: add comment about git logs 2020-01-16 11:49:03 +01:00
Ruslan Kuprieiev d1f172f332 Merge pull request #3158 from casperdcl/snap-deploy
major deploy tidy
2020-01-16 12:23:22 +02:00
Paweł Redzyński 18446996ab get: tes_get_from_non_dvc_master: capture git logs at INFO 2020-01-16 10:43:48 +01:00
pawel c2f5e35f1d conftest: remove repo_dir 2020-01-16 10:43:48 +01:00
pawel a4b55a00ec conftest: remove dvc_repo, erepo, git, git_erepo 2020-01-16 10:43:48 +01:00
Ruslan Kuprieiev 7d249d23e1 Merge pull request #3166 from mroutis/fix-3111
api: identify local repos with os.path.exists
2020-01-16 05:51:55 +02:00
Mr. Outis 388fcd6d32 api: identify local repos with os.path.exists
Close #3111
2020-01-15 21:31:09 -06:00
Mr. Outis 48326620ec setup: remove py2 backports 2020-01-15 19:13:01 -06:00
Ruslan Kuprieiev 6753e7b1e6 Merge pull request #3133 from JIoJIaJIu/core_checksum
remote: config checksum_jobs
2020-01-15 23:36:33 +02:00
Ruslan Kuprieiev ed0bc38e15 Merge pull request #3163 from MaxRis/3162
Fix gdrive multiple parents file list query
2020-01-15 23:32:57 +02:00
Max Risuhin 2d2ba0addf Fix gdrive multiple parents file list query 2020-01-15 13:10:55 -08:00
Ruslan Kuprieiev d2f81da696 Merge pull request #3161 from efiop/pydrive2
dvc: use pydrive2 instead of pydrive
2020-01-15 20:34:25 +02:00
Ruslan Kuprieiev 97a29a1f7e dvc: use pydrive2 instead of pydrive
PyDrive2 is our maintained fork.
2020-01-15 19:13:29 +02:00
Casper da Costa-Luis d8e6e34c34 minor use of new public API env var 2020-01-15 16:23:59 +00:00
Casper da Costa-Luis 4b16252c48 tidy deploy_condition 2020-01-15 15:41:02 +00:00
Casper da Costa-Luis 7b94181a0d deploy only on push 2020-01-15 15:26:29 +00:00
Casper da Costa-Luis d603048698 capitalisation 2020-01-15 15:20:09 +00:00
Casper da Costa-Luis 930b6cf45e fix pkg deploy condition 2020-01-15 15:16:29 +00:00
Casper da Costa-Luis 5bddfa7a92 major deploy tidy
- fixes #3152
2020-01-15 15:10:56 +00:00
Guro Bokum 4918c56ca3 remote: config checksum_jobs
Close #2920
2020-01-15 22:23:09 +08:00
pawel 0fcbfd04ce main: point to error.dvc.org after too many open files error 2020-01-15 15:14:24 +01:00
pawel d386237d86 main: format link to troubleshooting guide 2020-01-15 15:14:24 +01:00
pawel bc9a94b5ee point to troubleshooting guide on too many open files error 2020-01-15 15:14:24 +01:00
Fábio Santos 4ada384fcd remove trace statement 2020-01-15 12:43:14 +00:00
Fábio Santos 008a531535 support non-DVC repositories 2020-01-15 12:39:15 +00:00
rxxg e170852e97 Merge branch 'master' into master 2020-01-15 12:53:40 +01:00
rxxg 8fca139246 Use released version of speedcopy library 2020-01-15 12:50:43 +01:00
Fábio Santos 289797e7eb can now get whether an item changed, but not when an item changes from non-DVC to DVC 2020-01-15 11:33:26 +00:00
Fábio Santos 799d902c98 add test that checks that a file can be upgraded from git tracking to DVC tracking 2020-01-14 23:41:48 +00:00
Fábio Santos 1a26d3b60f Merge remote-tracking branch 'upstream/master' into feature/2959-status-of-imported-files 2020-01-14 23:08:43 +00:00
Ruslan Kuprieiev 0be0931fcd Merge pull request #3151 from efiop/3087
import: don't forget to set rev_lock for git files
2020-01-15 01:06:04 +02:00
Fábio Santos 3228b476fa compare checksums instead of file bytes 2020-01-14 22:58:11 +00:00
Ruslan Kuprieiev 410a8f8aeb travis: don't deploy snap on PRs 2020-01-15 00:55:34 +02:00
Ruslan Kuprieiev 7d6ecd1ccb import: don't forget to set rev_lock for git files
Related to #3087
2020-01-15 00:38:55 +02:00
Fábio Santos e5d35cb3ce reduce cognitive complexity of _local_status 2020-01-14 20:32:46 +00:00
Fábio Santos cfbdccdbed update test 2020-01-14 19:49:21 +00:00
Fábio Santos 8f464c2a07 status: don't clone more than once for plain files 2020-01-14 19:00:46 +00:00
Fábio Santos c9b0593220 status: enable status of files imported from remote git repos 2020-01-14 18:51:56 +00:00
Ruslan Kuprieiev b9e221a49c Merge pull request #3145 from iterative/efiop-patch-1
travis: build snap in parallel with other packages
2020-01-14 20:29:51 +02:00
Ruslan Kuprieiev 42784816c3 Merge pull request #3149 from skshetry/refactor-remote
test: use helper remotes in external repro
2020-01-14 20:27:25 +02:00
Fábio Santos f949b31976 status: implement support for imported files
Fixes #2959
2020-01-14 17:54:20 +00:00
Saugat Pachhai 7993600019 test: use helper remotes in external repro 2020-01-14 23:31:44 +05:45
Ruslan Kuprieiev fe6d51fa77 Merge pull request #3148 from skshetry/refactor-datacloud-clibase
test: use helper remotes in child classes of TestDataCloudCLIBase
2020-01-14 19:35:03 +02:00
Ruslan Kuprieiev b865a48b92 Merge pull request #3146 from pared/2896_fs_repo
fs: tests: remove repo_dir
2020-01-14 16:49:23 +02:00
Saugat Pachhai 45b97b14c4 test: use helper remotes in child of TestDataCloudCLIBase 2020-01-14 20:29:29 +05:45
Ruslan Kuprieiev 9c934c8abf Merge pull request #3147 from pared/2896_ssh_utils
ssh: test: unit, utils: test: unit: remove repo_dir
2020-01-14 16:38:08 +02:00
pawel cb5b5ed334 ssh: test: unit, utils: test: unit: remove repo_dir 2020-01-14 15:32:48 +01:00
Ruslan Kuprieiev b0f83f1486 travis: deploy snap on tag or master 2020-01-14 16:28:03 +02:00
pawel 144f10d8cb fs: tests: remove repo_dir 2020-01-14 15:25:55 +01:00
Ruslan Kuprieiev 6bd53273da Merge pull request #3143 from pared/2896_connection_unit
connection: test: unit: remove repo_dir
2020-01-14 16:25:11 +02:00
Ruslan Kuprieiev d5290e2a5c travis: build snap in parallel with other packages 2020-01-14 16:21:05 +02:00
Ruslan Kuprieiev 2d1e32748c Merge pull request #3136 from skshetry/refactor-remotes-inherit
test: use helper remotes in TestCases
2020-01-14 16:14:29 +02:00
pawel acd60ac1cf connection: test: unit: remove repo_dir 2020-01-14 14:52:31 +01:00
Saugat Pachhai 37b9d32e04 test: refactor: use helper remotes in TestCases 2020-01-14 19:25:19 +05:45
Ruslan Kuprieiev 88fb097275 Merge pull request #3138 from pared/2896_repro_cmd
tests: repro: unit: migrate to dir helpers
2020-01-14 15:29:03 +02:00
Ruslan Kuprieiev 6869ee65e0 Merge pull request #3139 from pared/2896_run_cmd
tests: run: unit: migrate to dir helpers
2020-01-14 15:28:56 +02:00
Ruslan Kuprieiev 63a3c678cf Merge pull request #3141 from pared/2896_update_cmd
update: tests: unit: migrate to dir helpers
2020-01-14 15:28:47 +02:00
Ruslan Kuprieiev e7626b8500 Merge pull request #3140 from pared/2896_stage_unit
stage: tests: unit: migrate to dir helpers
2020-01-14 15:27:14 +02:00
Ruslan Kuprieiev 6781a534f9 Merge pull request #3137 from pared/2896_unit_local
tests: local: unit: migrate to dir helpers
2020-01-14 15:10:13 +02:00
pawel 26c4845157 tests: run: unit: migrate to dir helpers 2020-01-14 14:02:26 +01:00
pawel ba63900b29 tests: repro: unit: migrate to dir helpers 2020-01-14 13:56:56 +01:00
pawel cabe619d97 update: tests: unit: migrate to dir helpers 2020-01-14 13:55:02 +01:00
pawel 8e756f0452 stage: tests: unit: migrate to dir helpers 2020-01-14 13:51:40 +01:00
pawel de9ff82b9c tests: local: unit: migrate to dir helpers 2020-01-14 13:40:44 +01:00
Ruslan Kuprieiev 9cafef1fdd Merge pull request #3051 from efiop/2995
metrics: introduce `diff`
2020-01-14 14:18:40 +02:00
Ruslan Kuprieiev 482473aa69 dvc: bump to 0.81.3 2020-01-14 11:23:10 +02:00
Ruslan Kuprieiev f18bfdc674 setup: fix typo 2020-01-14 11:22:29 +02:00
Ruslan Kuprieiev 8da898d483 dvc: bump to 0.81.2 2020-01-14 11:15:36 +02:00
Ruslan Kuprieiev ebb0243e87 setup: rollback pydrive dep
pypi doesn't allow direct github deps https://travis-ci.com/iterative/dvc/jobs/275282583
2020-01-14 11:15:11 +02:00
Ruslan Kuprieiev 58cc31cbe4 Merge pull request #3134 from skshetry/refactor-local
test: merge local related function in Local helper class
2020-01-14 10:56:01 +02:00
rxxg 674541f922 Use pyfastcopy for non-Windows systems using Python < 3.8 2020-01-14 09:32:09 +01:00
rxxg 811fc5317c Use speedcopy module with bugfix 2020-01-14 09:32:09 +01:00
rxxg 09223442f5 Use speedcopy module for faster copies across network boundaries 2020-01-14 09:32:09 +01:00
Ruslan Kuprieiev a09bfaf9db dvc: bump to 0.81.1 2020-01-14 10:29:01 +02:00
Ruslan Kuprieiev 876b659c2c remote: config: add a comment about section existence check
https://github.com/iterative/dvc/pull/3132#discussion_r366202037
2020-01-14 10:27:29 +02:00
Ruslan Kuprieiev 69e91f2bf9 Merge pull request #3132 from mroutis/fix-3131
remote: error out when modifying a non-existing remote
2020-01-14 10:26:16 +02:00
Ruslan Kuprieiev 563f75ebf7 Merge pull request #3129 from mroutis/fix-version
version: decode correctly commands output
2020-01-14 10:20:12 +02:00
Saugat Pachhai a27910d71d test: merge local related function in Local helper class 2020-01-14 14:04:02 +05:45
Ruslan Kuprieiev efa655ecf1 Merge pull request #3128 from skshetry/refactor-hdfs-gcp
test: move hdfs and gcp functions into helper classes
2020-01-14 10:16:57 +02:00
Mr. Outis ff094b2560 remote: error out when modifying a non-existing remote
Close #3131
2020-01-13 18:29:40 -06:00
Jorge Orpinel f65237761f api: improve existing docstrings
per https://github.com/iterative/dvc/issues/3092#issuecomment-572394156
2020-01-13 14:27:04 -06:00
Mr. Outis 2c9b763906 version: decode correctly commands output 2020-01-13 12:36:47 -06:00
Saugat Pachhai 26d85f1a52 test: merge gcp related functions to GCP helper class 2020-01-13 23:49:22 +05:45
Ruslan Kuprieiev fe635a5040 dvc: bump to 0.81.0 2020-01-13 16:59:05 +02:00
Ruslan Kuprieiev 0ead08805e Merge pull request #3116 from efiop/2896-scm
test: scm: convert to dir helpers
2020-01-13 16:13:22 +02:00
Ruslan Kuprieiev 0710dc2453 Merge pull request #3114 from iterative/efiop-patch-2
get: refactor handling of git files and git-only repos
2020-01-13 15:38:27 +02:00
Ruslan Kuprieiev e9dee8a103 Merge pull request #3115 from efiop/2896
test: get_url: convert to dir helpers
2020-01-13 15:38:02 +02:00
Ruslan Kuprieiev 92f3f8a624 Merge pull request #3117 from efiop/2896-init
test: init: convert to dir helpers
2020-01-13 15:37:17 +02:00
Ruslan Kuprieiev 7d2fa8885a test: scm: convert to dir helpers
Related to #2896
2020-01-13 15:34:27 +02:00
Ruslan Kuprieiev e833fd22d3 Merge pull request #3118 from efiop/2896-external-repo
tests: external_repo: convert to dir helpers
2020-01-13 15:21:53 +02:00
Ruslan Kuprieiev af821444f7 get: remove unused var 2020-01-13 13:39:45 +02:00
Ruslan Kuprieiev 64431f8930 test: get_url: convert to dir helpers
Related to #2896
2020-01-13 13:34:33 +02:00
Ruslan Kuprieiev 3e3869ffd0 Merge pull request #3126 from pared/fix_import_help
add space for import help text
2020-01-13 12:52:45 +02:00
pawel 28639c1d13 add space for import help text 2020-01-13 11:46:16 +01:00
Saugat Pachhai d55427dbd9 test: refactor hdfs related functions in HDFS test helpers 2020-01-13 16:30:15 +05:45
Ruslan Kuprieiev 2ab6860b90 Update tests/func/test_init.py 2020-01-13 11:25:37 +02:00
Ruslan Kuprieiev 028d30e22d Merge pull request #3123 from skshetry/refactor-ssh
test: refactor ssh related functions in test helper classes
2020-01-13 11:14:26 +02:00
Ruslan Kuprieiev 100dde1c9a Merge pull request #3101 from Suor/prepare-summon
summon: fixes and dvcx prereq
2020-01-13 10:51:27 +02:00
Saugat Pachhai 141f308a6a test: refactor ssh related functions in SSH and SSHMocked test helpers 2020-01-13 14:23:42 +05:45
Alexander Schepanovski be90c38182 summon: nice up summon code
- proper doc-string for prepare_summon()
- summon -> summon_dict in summon() to not overwrite a glo bal name.
This was not a bug, but a bad practice, which might have caused some
issues in the future.
- better name for _get_object_spec()
2020-01-13 15:22:16 +07:00
Alexander Schepanovski d0de9d0ed0 summon: revert dvc.api._invoke_mehtod() considerations
It's now thread-safe, but the rest is still relevant.
2020-01-13 15:19:54 +07:00
Ruslan Kuprieiev e393e29ef5 metrics: introduce diff
This first implementation is based on existing `dvc metrics show`
functionality, but is also able to auto-detect json to properly
show diff for it.
2020-01-13 06:45:48 +02:00
Ruslan Kuprieiev d831044cb6 tests: external_repo: convert to dir helpers
Related to #2896
2020-01-13 04:15:27 +02:00
Ruslan Kuprieiev 537a0af5f7 travis: update mac machine 2020-01-13 03:56:31 +02:00
Ruslan Kuprieiev 21c75502c5 test: init: convert to dir helpers 2020-01-12 06:07:21 +02:00
Ruslan Kuprieiev 5b5810e1c0 Merge pull request #3016 from efiop/3015
gdrive: add support for teamDriveId
2020-01-12 04:52:01 +02:00
Ruslan Kuprieiev a79f04a28c get: refactor handling of git files and git-only repos 2020-01-12 03:05:20 +02:00
Ruslan Kuprieiev f920afd5dd travis: fix aws config condition
Close #3113
2020-01-12 02:23:15 +02:00
Ruslan Kuprieiev 3e97886ccc Merge pull request #3076 from efiop/rwlock_sync
rwlock: flush and fsync
2020-01-12 01:35:13 +02:00
Max Risuhin 57111a7788 gdrive: add support for teamDriveId 2020-01-11 07:41:12 -08:00
Ruslan Kuprieiev b7f9e7365d Merge pull request #3097 from fabiosantoscode/feature/3089-get-non-dvc-repositories
get: handle non-DVC repositories
2020-01-11 07:05:35 +02:00
Ruslan Kuprieiev 9e9f37e06a Merge pull request #3094 from pared/2896_run
run: tests: migrate to dir helpers
2020-01-11 05:36:07 +02:00
Ruslan Kuprieiev d6092c1c95 Merge pull request #3096 from pared/2896_state
state: tests: migrate to dir helpers
2020-01-11 05:35:32 +02:00
Ruslan Kuprieiev 62d0fa1afa Merge pull request #3100 from pared/2896_update
update: tests: migrate to dir helpers
2020-01-11 05:34:52 +02:00
Ruslan Kuprieiev 7f9bbd94a5 Merge pull request #3099 from pared/2896_stage
stage: tests: migrate to dir helpers
2020-01-11 05:33:39 +02:00
Ruslan Kuprieiev 5b85db3e04 Merge pull request #3104 from efiop/ssh_dir_out
tests: remove redundant native ssh tests
2020-01-11 05:20:21 +02:00
Ruslan Kuprieiev bc619f1c41 pyinstaller: remove Crypto hook 2020-01-11 04:55:34 +02:00
Ruslan Kuprieiev c064865c80 build: use build-requirements.txt 2020-01-11 04:55:34 +02:00
Ruslan Kuprieiev 795e4ce6b8 pyinstaller: delete oss hook
It is already available in the standard hooks.
2020-01-11 04:38:57 +02:00
Ruslan Kuprieiev 2a9d19f116 tests: use mocked ssh 2020-01-11 03:14:45 +02:00
Ruslan Kuprieiev f6a00f35ae ssh: fix bug in remove() 2020-01-11 03:14:45 +02:00
Ruslan Kuprieiev 8154c89c3f tests: remove redundant native ssh tests
We already have mocked ones, which are more reliable as they use their
own paramiko server instead of relying on the sshd running on the test
machine. Native tests are also quite flaky and fail sometimes due to
network issues.
2020-01-11 03:14:45 +02:00
Ruslan Kuprieiev 01103585d7 Merge pull request #3090 from mroutis/py3-system
py3: use links from os module
2020-01-11 02:45:10 +02:00
Mr. Outis df97a2a7de don't reimport DvcException 2020-01-10 18:32:34 -06:00
Mr. Outis b8dd4225d2 py3: fspath source and link_name 2020-01-10 17:40:59 -06:00
Mr. Outis 6eac40d226 system: catch OSError instead of Exception 2020-01-10 17:35:54 -06:00
Fábio Santos de303d45ba test removing a file outside a raw git repo 2020-01-10 23:30:18 +00:00
Fábio Santos f3d234ec9c deduplicate code 2020-01-10 23:26:37 +00:00
Fábio Santos 1f2eed80f8 don't manually clean up git repository 2020-01-10 23:16:19 +00:00
Fábio Santos 9f279b3968 forbid absolute paths for plain git repositories 2020-01-10 23:15:03 +00:00
Fábio Santos 0e2ef470e8 remove blank line 2020-01-10 22:53:31 +00:00
Fábio Santos 5fb6640867 run black 2020-01-10 22:29:27 +00:00
Fábio Santos f5e40b2a47 get: leverage external_repo() context manager for DVC repositories 2020-01-10 22:28:24 +00:00
Fábio Santos c16f8825dc import: update documentation
Fixes #3077
2020-01-10 22:28:24 +00:00
Fábio SantosandJorge Orpinel 5b36ffd60e Update dvc/command/get.py
Co-Authored-By: Jorge Orpinel <[email protected]>
2020-01-10 22:28:24 +00:00
Fábio SantosandJorge Orpinel 9a98622020 Update dvc/command/get.py
Co-Authored-By: Jorge Orpinel <[email protected]>
2020-01-10 22:28:24 +00:00
Fábio SantosandJorge Orpinel 04619d559b Update dvc/command/get.py
Co-Authored-By: Jorge Orpinel <[email protected]>
2020-01-10 22:28:24 +00:00
Fábio SantosandJorge Orpinel 51f2fa184f Update dvc/command/get.py
Co-Authored-By: Jorge Orpinel <[email protected]>
2020-01-10 22:28:24 +00:00
Fábio Santos 6a4f4031b0 change command doc string 2020-01-10 22:28:24 +00:00
Fábio Santos 7873abcade remove unused exception class 2020-01-10 22:28:24 +00:00
Fábio Santos 6cb7308cdb Revert "get: recoup cache optimal location"
This reverts commit 8e4922800f6ce47210976764af1c05e31e90deac.
2020-01-10 22:28:24 +00:00
Fábio SantosandRuslan Kuprieiev a01f81ed09 Update dvc/repo/get.py
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2020-01-10 22:28:24 +00:00
Fábio Santos 86b9a1cfe4 get: recoup cache optimal location 2020-01-10 22:28:24 +00:00
Fábio Santos f126f2483b doc: change get documentation so that it doesn't imply the target must be DVC enabled 2020-01-10 22:28:24 +00:00
Fábio Santos 07637a6f8f style: improve code flow and move comments 2020-01-10 22:28:24 +00:00
Fábio Santos 0dd5647d3c get: handle non-DVC repositories
Allows us to `dvc get` from non-DVC source repositories.

Fixes #3089
2020-01-10 22:28:24 +00:00
Ruslan Kuprieiev 84ec0d49a3 Merge pull request #3091 from pared/2896_repro
repro: tests: migrate to dir helpers
2020-01-10 18:38:30 +02:00
Paweł Redzyński 23288aa568 run: tests: remove deterministic_run fixture 2020-01-10 17:18:12 +01:00
Paweł Redzyński f62aeae6d1 run: tests: clumsy deterministic_run migration 2020-01-10 17:18:11 +01:00
Paweł Redzyński 6d61e627cb run: tests: migrate to helpers all besides deterministic 2020-01-10 17:18:11 +01:00
Alexander Schepanovski 4a3dd63720 summon: separate prepare_summon() 2020-01-10 22:02:58 +07:00
Alexander Schepanovski c548b0792d summon: fix open() shadowing 2020-01-10 21:59:58 +07:00
pawel ab1fab5b3e update: tests: migrate to dir helpers 2020-01-10 15:33:12 +01:00
Alexander Schepanovski afd29e5dad summon: protect chdir/import/call with a thread lock 2020-01-10 20:55:28 +07:00
pawel b6edeb4914 stage: tests: migrate to dir helpers 2020-01-10 14:44:29 +01:00
pawel 57685399a6 state: tests: migrate to dir helpers 2020-01-10 13:06:15 +01:00
Ruslan Kuprieiev 90152ea8b8 Merge pull request #3053 from Suor/is-tracked
dvc: optimize Git.is_tracked()
2020-01-10 06:34:30 +02:00
Ruslan Kuprieiev 095464d851 Merge pull request #3093 from iterative/move-fs-utilities
Move fs utilities to fs.py from __init__.py
2020-01-09 17:25:58 +02:00
Ruslan Kuprieiev 238b4e8e7e Merge pull request #3072 from pared/2896_import_url
import url: test: migrate to dir helpers
2020-01-09 16:33:57 +02:00
Paweł Redzyński 0554cbfe9c repro: tests: use Path-like objects in repro_dir fixture 2020-01-09 11:41:44 +01:00
Paweł Redzyński 93c54763fe repro: tests: yield dict of stages from repro_dir 2020-01-09 11:10:41 +01:00
Aman Sharma 74850fe728 Move walk_files to fs.py 2020-01-09 14:55:49 +05:30
Paweł RedzyńskiandRuslan Kuprieiev ad1450635b Update tests/func/test_import_url.py
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2020-01-09 10:00:02 +01:00
Aman Sharma a79816920d Move copyfile to fs.py 2020-01-09 04:57:53 +05:30
Aman Sharma eca5d2dde4 Move makedirs to fs.py 2020-01-09 04:30:51 +05:30
Paweł RedzyńskiandRuslan Kuprieiev b9e700105e Update tests/func/test_import_url.py
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2020-01-08 22:21:02 +01:00
Paweł RedzyńskiandRuslan Kuprieiev 7e12c7104d Update tests/func/test_import_url.py
Co-Authored-By: Ruslan Kuprieiev <[email protected]>
2020-01-08 22:20:50 +01:00
Paweł Redzyński 25437ca600 repro: tests: refactor for readibility 2020-01-08 21:46:39 +01:00
Paweł Redzyński e93504668d repro: tests: migrate to dir helpers 2020-01-08 21:35:29 +01:00
Ruslan Kuprieiev 277d28c6cb Merge pull request #3083 from pared/2896_output
output: tests: migrate to dir helpers
2020-01-08 22:14:07 +02:00
Mr. Outis e5bee243a3 py3: use links from os module 2020-01-08 14:09:22 -06:00
Ruslan Kuprieiev 049367e475 Merge pull request #3034 from mroutis/remove-py2-comments
py3: remove code related to py2 limitations
2020-01-08 20:14:40 +02:00
Ruslan Kuprieiev 82cdce0934 Merge pull request #3086 from pared/2896_remote
remote: test: migrate to dir helpers
2020-01-08 19:16:30 +02:00
Ruslan Kuprieiev 27774705cf Merge pull request #3085 from pared/2896_pipeline
pipeline: test: migrate to dir helpers
2020-01-08 19:14:21 +02:00
Paweł Redzyński 04912f2c86 remote: test: migrate to dir helpers 2020-01-08 12:21:33 +01:00
Paweł Redzyński 945eb181bc pipeline: test: migrate to dir helpers 2020-01-08 11:23:43 +01:00
Paweł Redzyński f074d00269 output: tests: migrate to dir helpers 2020-01-08 11:11:34 +01:00
pawel 62d6f4dbdb import url: test: migrate to dir helpers 2020-01-08 10:30:33 +01:00
Ruslan Kuprieiev 2966924e47 Merge pull request #3074 from pared/2896_install
install: tests: migrate to dir helpers
2020-01-08 06:20:19 +02:00
Ruslan Kuprieiev fb37cd0ab2 Merge pull request #3082 from efiop/reqs
setup: get rid of py2 reqs
2020-01-08 02:42:19 +02:00
Ruslan Kuprieiev ba45c9cd5b setup: get rid of py2 reqs 2020-01-08 02:16:06 +02:00
Ruslan Kuprieiev e8b0e22a6b Merge pull request #3078 from mroutis/py3-logger
py3: use format_exc instead of print_exception
2020-01-08 01:27:07 +02:00
Ruslan Kuprieiev ee0a8fa92d Merge pull request #3075 from efiop/assert_stage
rwlock: check that .dvc/lock is locked
2020-01-08 00:44:32 +02:00
Ruslan Kuprieiev 249f6c5634 Merge pull request #3027 from Suor/status-noop
ui: warn about ignored options for `dvc status` and a grammar fix
2020-01-07 20:30:05 +02:00
Alexander Schepanovski bafac6dec3 ui: fix grammar in the error message 2020-01-07 17:26:01 +07:00
Alexander Schepanovski 1b1f8f9d22 ui: error out for meaningless options for non-cloud dvc status 2020-01-07 17:25:55 +07:00
Paweł Redzyński 1d00560612 fixup 2020-01-07 10:16:32 +01:00
pawel 7f2a196c5c install: tests: migrate to dir helpers 2020-01-07 10:12:01 +01:00
Mr. Outis f942e26453 py3: remove tb_list 2020-01-07 00:13:51 -06:00
Mr. Outis 7ed4fd3840 py3: use format_exc instead of print_exception 2020-01-06 23:40:00 -06:00
Mr. Outis 71b15d259a py3: remove code related to py2 limitations 2020-01-06 23:03:33 -06:00
Ruslan Kuprieiev 11fa9204f8 Merge pull request #3058 from efiop/too_many_files
ui: don't show exception for "Too many files" error
2020-01-07 06:51:29 +02:00
Ruslan Kuprieiev df214fa9d4 Merge pull request #3035 from iterative/test-walk_files
Write test for `walk_files` to check if it accepts Path-like and str objects
2020-01-07 05:33:08 +02:00
Aman Sharma 13ca5bdf28 Write test for walk_files to check if it accepts Path-like and str objects 2020-01-07 05:50:48 +05:30
Ruslan Kuprieiev c1906436c2 rwlock: flush and fsync
It is important that we ensure that rwlocked contents are flushed to
disk, because otherwise other rwlock readers might get outdated state.

Kudos @shcheklein for catching this one.
2020-01-06 22:37:21 +02:00
Ruslan Kuprieiev 1dcce1d8b0 rwlock: check that .dvc/lock is locked
Mostly a sanity check, but we did have a potential bug in `destroy`,
whcih this PR fixes along the way.

Kudos @shcheklein for catching this one.
2020-01-06 22:34:34 +02:00
Ruslan Kuprieiev e2efb7b6e4 Merge pull request #3064 from casperdcl/snap-deploy
fix snap deploy stage
2020-01-06 22:26:00 +02:00
Casper da Costa-Luis 5753ea5dae snap deploy master edge
partially reverts f21931cca0
2020-01-07 00:40:43 +05:30
Ruslan Kuprieiev 498dcabb94 Merge pull request #3054 from sharidas/fix-gc-local
dvc: update gc to remove unpacked dir
2020-01-06 21:05:13 +02:00
Ruslan Kuprieiev f21931cca0 travis: deploy snap only on tags. 2020-01-06 20:53:49 +02:00
Ruslan Kuprieiev aaec1837bb Merge pull request #3073 from skshetry/refactor-remote-s3
test: merge _should_test_aws and _get_aws_url inside S3 test helper
2020-01-06 20:43:18 +02:00
Ruslan Kuprieiev 4f312f69a9 Merge branch 'master' into refactor-remote-s3 2020-01-06 20:43:10 +02:00
Ruslan Kuprieiev 0f08b7ed79 Merge pull request #3071 from skshetry/refactor-remote-oss
test: merge _should_test_oss and _get_oss_url inside OSS test helper
2020-01-06 20:39:20 +02:00
Ruslan Kuprieiev 40449c49b1 Merge pull request #3070 from pared/2896_http_imp
http/ imp tests - migrate do dir helpers
2020-01-06 20:38:37 +02:00
Ruslan Kuprieiev 9f0f1c4212 Merge pull request #3068 from pared/2896_dependency
dependency: test: migrate to dir helpers
2020-01-06 20:37:39 +02:00
Ruslan Kuprieiev e5ac017957 Merge pull request #3067 from pared/2896_cache
cache: test: migrate to dir helpers
2020-01-06 20:37:12 +02:00
Sujith H 1e3abe2f38 dvc: update gc to remove unpacked dir
In local remote its found that gc does not
remove the unpacked dir. This change set
helps to remove it.

Signed-off-by: Sujith H <[email protected]>
2020-01-06 23:58:06 +05:30
Ruslan Kuprieiev cf37fe2c56 travis: deploy snap only on master 2020-01-06 20:16:22 +02:00
Ruslan Kuprieiev ec574a0055 Merge pull request #3066 from pared/2896_analytics
analytics: tests: migrate to dir helpers
2020-01-06 19:42:11 +02:00
Ruslan Kuprieiev 46379c8591 config: update command for setting default remote 2020-01-06 18:58:10 +02:00
Saugat Pachhai 78dd4896ca test: merge _should_test_aws and _get_aws_url inside S3 test helper 2020-01-06 22:08:28 +05:45
pawel e1176bf42d imp/imp_url cmd test: remove unnecessary dvc fixture 2020-01-06 16:40:15 +01:00
pawel a620658884 http/ imp tests - migrate do dir helpers 2020-01-06 16:18:01 +01:00
pawel c2b3921af3 dependency: test: migrate to dir helpers 2020-01-06 16:06:52 +01:00
pawel d2859a311d cache: test: migrate to dir helpers 2020-01-06 16:02:04 +01:00
pawel bb72ce8a50 analytics: tests: migrate to dir helpers 2020-01-06 15:49:26 +01:00
Ruslan Kuprieiev 8041d70552 Merge pull request #3044 from pared/2896_api
tests: api: convert to dir helpers
2020-01-06 16:21:31 +02:00
Ruslan Kuprieiev 0ad3548fca Merge pull request #3063 from casperdcl/snap-classic
Snap classic deployment
2020-01-06 12:47:01 +02:00
Casper da Costa-Luis be762e70c2 remove unneeded stage 2020-01-06 14:56:16 +05:30
Casper da Costa-Luis 55f9a54bff integrate snap deploy 2020-01-06 14:32:39 +05:30
Casper da Costa-Luis 30354f4c8c Snap classic approval!
- approval: https://forum.snapcraft.io/t/classic-confinement-request-for-dvc/14124
- partially reverts commit a0ac129d5f.
2020-01-06 14:14:44 +05:30
Ruslan Kuprieiev 19f408b0e6 Merge pull request #3061 from mroutis/fix-zsh
completion: fix --relink problem
2020-01-06 04:57:21 +02:00
Ruslan Kuprieiev 6411a3bb32 ui: don't show exception for "Too many files" error
https://github.com/iterative/dvc.org/pull/875#discussion_r362920851

Stops showing the exception in non-verbose mode, but shows full
traceback in verbose mode.

Introducing our custom parameter `tb_only` through standard `extra`
logging parameter.
2020-01-06 01:02:59 +02:00
Ruslan Kuprieiev 29afb380fd Merge pull request #3018 from pared/2914_3
local-related methods should verify that WorkingTree is in use
2020-01-06 00:06:35 +02:00
Mr. Outis 8712ecc1e1 completion: fix --relink problem
Close #2983
2020-01-05 02:05:24 -06:00
Ruslan Kuprieiev 7f92cc3be3 Merge pull request #3049 from pared/2896_remote_local
tests: remote: local: migrate to dir fixtures
2020-01-05 07:19:44 +02:00
Ruslan Kuprieiev f93574895f Merge pull request #3020 from chatcannon/dvc-import-regular-git
import: allow importing from non-DVC git repositories
2020-01-05 07:18:03 +02:00
Saugat Pachhai 96c87d5643 test: merge _should_oss_azure and _get_oss_url inside OSS class 2020-01-05 10:25:01 +05:45
Ruslan Kuprieiev 54faf1b795 Merge pull request #3055 from skshetry/refactor-remote-azure
test: Merge _should_test_azure and _get_azure_url inside Azure test helper
2020-01-05 04:25:09 +02:00
Ruslan Kuprieiev 60e512112e Merge branch 'master' into refactor-remote-azure 2020-01-05 04:24:59 +02:00
Ruslan Kuprieiev b6c77f7668 Merge pull request #3056 from skshetry/refactor-gdrive
test: add _should_test_gdrive and _get_gdrive_url inside GDrive
2020-01-04 20:18:49 +02:00
Ruslan Kuprieiev f39d8eb3d1 Merge pull request #3052 from Suor/yield-from
py3: use yield from where appropriate
2020-01-04 18:21:12 +02:00
Ruslan Kuprieiev f3a7ac3756 Merge pull request #3045 from Suor/erepo-template
test: do not bootstrap repo template and version files in erepo_dir
2020-01-04 18:20:55 +02:00
Saugat Pachhai c0616fcee2 test: add _should_test_gdrive and _get_gdrive_url inside GDrive 2020-01-04 22:03:49 +05:45
Saugat Pachhai c4783f4930 test: merge _should_test_azure and _get_azure_url inside Azure class 2020-01-04 21:54:12 +05:45
Alexander Schepanovski a19deb2076 dvc: optimize Git.is_tracked()
Closes #3000
2020-01-04 11:32:16 +07:00
Alexander Schepanovski 0352adc639 py3: use yield from where appropriate 2020-01-04 11:03:30 +07:00
Alexander Schepanovski 5dd2d7b192 test: do not use repo template in erepo_dir 2020-01-04 10:47:13 +07:00
Alexander Schepanovski b194bfa1ff test: do not bootstrap versions in erepo_dir 2020-01-04 10:47:13 +07:00
pawel af425318aa tests: api: roll back erepo usage 2020-01-04 03:10:43 +01:00
pawel 020c0496a6 tests: api: convert to dir helpers 2020-01-04 03:08:51 +01:00
pawel d9eaabe9bf some fixmes 2020-01-04 03:08:51 +01:00
pawel 39f010e50a api: open: fix open scm controlled test 2020-01-04 03:08:51 +01:00
pawel ce0eb08ba6 tests: remote: local: migrate to dir fixtures 2020-01-04 02:47:58 +01:00
Ruslan Kuprieiev 85c35f894c Merge pull request #3048 from Suor/optimize-stages
dvc: optimize and clean up stages collection
2020-01-03 23:26:42 +02:00
Ruslan Kuprieiev bdcc995ae5 Merge pull request #3047 from Suor/tree-autoclean
dvc: autowrap repo tree into CleanTree
2020-01-03 23:23:05 +02:00
Alexander Schepanovski b2d083fd88 dvc: optimize and clean up stages collection 2020-01-03 22:06:52 +07:00
Alexander Schepanovski 5e92a079b8 dvc: autowrap repo tree into CleanTree 2020-01-03 21:46:05 +07:00
Ruslan Kuprieiev 45933113df Merge pull request #3043 from Suor/chdir
test: add @TmpDir.chdir()
2020-01-03 16:06:52 +02:00
Ruslan Kuprieiev 11a627d336 Merge pull request #3046 from Suor/clean-brancher
dvc: clean brancher
2020-01-03 14:42:51 +02:00
Alexander Schepanovski ecd511fc69 dvc: safely return to saved tree in brancher 2020-01-03 19:09:34 +07:00
Alexander Schepanovski 77b937b001 dvc: drop unused params in Repo.brancher() 2020-01-03 19:05:54 +07:00
Alexander Schepanovski 1e5fb102a2 test: add @TmpDir.chdir() 2020-01-03 16:58:32 +07:00
pawel 36427387c0 get_mtime_and_size: move working tree assertion after veryfing dir 2020-01-03 10:32:32 +01:00
pawel 1e1a9cf774 tree: introduce is_working_tree 2020-01-03 10:03:47 +01:00
pawel eb53c030c6 local-related methods should verify that WorkingTree is in use 2020-01-03 09:46:04 +01:00
Ruslan Kuprieiev 705fbd24c4 Merge pull request #3042 from mroutis/refactor-exceptions
py3: refactor exceptions
2020-01-03 05:44:52 +02:00
Mr. Outis 5611ccb43a exceptions: remove cause 2020-01-02 20:43:24 -06:00
Mr. Outis aed04e7cc9 exceptions: remove noop code 2020-01-02 16:59:56 -06:00
Ruslan Kuprieiev 0a2e699115 Merge pull request #3039 from mroutis/refactor-super
py3: use new magic from PEP 3135
2020-01-03 00:53:09 +02:00
Ruslan Kuprieiev ca06698dc2 Merge pull request #3041 from iterative/pull-3040-restyled
Restyle fix for base None value
2020-01-03 00:46:54 +02:00
Restyled.io bb54232921 Restyled by black 2020-01-02 22:43:26 +00:00
msatlihan 70146efc7c fix for None value 2020-01-03 01:39:24 +03:00
Mr. Outis fd82402d34 py3: use new magic from PEP 3135 2020-01-02 16:24:31 -06:00
Ruslan Kuprieiev a377f9e08f Merge pull request #3037 from iterative/test-relpath
Ensure `relpath` accepts str and Path-like objects
2020-01-02 16:46:06 +02:00
Ruslan Kuprieiev 8a2e375d6b Merge pull request #3025 from Suor/dirty-metrics
metrics: don't show working tree if it is clean
2020-01-02 02:14:20 +02:00
Aman Sharma f08c90fc6e Ensure relpath accepts str and Path-like objects 2020-01-02 02:17:02 +05:30
Ruslan Kuprieiev f6162fee47 Merge pull request #3026 from Suor/relink-autocompl
ui: add checkout --relink to autocomplete scripts
2020-01-01 17:34:46 +02:00
Ruslan Kuprieiev a5ba9707a9 Merge pull request #3032 from mroutis/migrate-utils-compat
py3: migrate utils/compat.py to compat.py
2020-01-01 17:09:32 +02:00
Ruslan Kuprieiev cba4c017a7 Merge pull request #3031 from mroutis/remove-imports
py3: remove imports from utils.compat
2020-01-01 17:08:46 +02:00
Ruslan Kuprieiev fd319ab8a9 Merge pull request #3030 from mroutis/remove-utils-compat
py3: remove csv_reader and makedirs from utils/compat.py
2020-01-01 17:06:33 +02:00
Ruslan Kuprieiev a507a19a34 Merge pull request #3029 from mroutis/compat-casts
py3: remove methods for casting bytes in python 2
2020-01-01 17:04:25 +02:00
Mr. Outis 55e90b7bdf py3: ignore_file_not_found -> contextlib.suppress 2019-12-31 23:42:40 -06:00
Mr. Outis 086acf6404 py3: dvc/utils/compat.py -> dvc/compat.py 2019-12-31 23:42:07 -06:00
Mr. Outis 27f581cf4e py3: remove imports from utils.compat 2019-12-31 23:16:11 -06:00
Mr. Outis 9c42809587 py3: _makedirs -> os.makedirs 2019-12-31 22:19:07 -06:00
Mr. Outis 999130b029 py3: remove compat csv_reader 2019-12-31 22:17:02 -06:00
Mr. Outis 1ef466b849 py3: remove convert_to_unicode noop 2019-12-31 22:14:54 -06:00
Alexander Schepanovski b8cb6d72a9 metrics: don't show working tree if it is clean 2020-01-01 10:59:13 +07:00
Mr. Outis b844aea42c py3: remove cast_bytes method 2019-12-31 20:14:25 -06:00
Mr. Outis 326ba21ed0 py3: remove unneeded string casts 2019-12-31 19:43:15 -06:00
Ruslan Kuprieiev 35f4ee8a53 dvc: bump to 0.80.0 2020-01-01 03:24:03 +02:00
Chris Kerr 3ceb1db5a8 import: Allow importing from non-DVC git repos
Fixes #2977

Refactor `_copy_if_git_file` to use `cached_clone` directly, so
that copying still works even if the source repo does not have a
'.dvc' subdirectory.
2019-12-31 13:19:33 +00:00
Chris Kerr fb300e1f9f Refactor cached_clone function out of _external_repo
Since this function does not create a `Repo` class, it can be called
on a git repo with no '.dvc' directory
2019-12-31 13:13:29 +00:00
Chris Kerr c393e8dcbe import: Test for #2977: Handle non-DVC Git repositories 2019-12-31 13:11:28 +00:00
Ruslan Kuprieiev cbc7e21593 utils: don't import colorama again 2019-12-31 13:06:36 +02:00
Ruslan Kuprieiev 32ee19778f Merge pull request #3024 from mroutis/unicode-str
py3: remove string compat related code
2019-12-31 13:05:57 +02:00
Alexander Schepanovski bd0b28d7c2 ui: add checkout --relink to autocomplete scripts 2019-12-31 14:04:51 +07:00
Mr. Outis 6d36b85736 py3: remove string compat related code 2019-12-30 23:49:19 -06:00
Ruslan Kuprieiev dd5e0589db Merge pull request #3021 from mroutis/remove-utils-compat
py3: get rid of is_py checks
2019-12-31 04:11:00 +02:00
Ruslan Kuprieiev 381b279e53 Merge pull request #3022 from mroutis/remove-future-imports
py3: remove future imports
2019-12-31 04:08:22 +02:00
Mr. Outis 2827146e55 py3: adapt TEXT_CHARS on istextfile.py 2019-12-30 19:45:51 -06:00
Ruslan Kuprieiev e599a3ad57 Merge pull request #3019 from mroutis/deprecating-27
py3: deprecate 2.7
2019-12-31 03:36:39 +02:00
Mr. Outis 94a0914bba py3: get rid of future imports 2019-12-30 19:35:48 -06:00
Mr. Outis c61254dc75 py3: get rid of py2 on CI scripts 2019-12-30 19:16:10 -06:00
Mr. Outis 886afdb203 py3: get rid of is_py checks 2019-12-30 18:53:36 -06:00
Mr. Outis d3d6f679be py3: modify python_requires to support 3.5 onwards 2019-12-30 13:10:28 -06:00
Ruslan Kuprieiev 21808112df Merge pull request #3014 from efiop/test
tests: func: convert test_get.py test_utils.py to new fixtures
2019-12-30 18:31:53 +02:00
Ruslan Kuprieiev df5fda272e Merge pull request #2974 from pared/2914
repo: move dvcignore from repo to tree
2019-12-30 18:31:23 +02:00
pawel 1880656579 CleanTree: conform to snake_case naming style 2019-12-30 17:05:45 +01:00
pawel 691d771d56 remove CleanTree from GitTree completely 2019-12-30 16:50:43 +01:00
pawel e1eab9405c tree: check that repo.tree is CleanTree when using walk_files 2019-12-30 14:19:43 +01:00
pawel e225601031 reintroduce dvcignore collection test 2019-12-30 14:19:43 +01:00
pawel cf2d349bd7 tree: move CleanTree to ignore 2019-12-30 14:19:43 +01:00
pawel 56ed5351ef ignore: test: remove ignore collection test 2019-12-30 14:19:43 +01:00
pawel d8813dc5ca remove dvc_walk, skip .dvcignore gathering test 2019-12-30 14:19:43 +01:00
pawel d8c0ae6909 remove most occurences of dvc_walk 2019-12-30 14:19:43 +01:00
pawel cf2fac5787 working tree: adjust walk behaviour to os.walk 2019-12-30 14:19:43 +01:00
pawel aa613f9e6f dvcignore loading back in constructor 2019-12-30 14:19:43 +01:00
pawel f4ee01cffe no need for dvcignore in walk args 2019-12-30 14:19:43 +01:00
Paweł Redzyński 956fec02ce repo: move dvcignore from repo to tree 2019-12-30 14:19:43 +01:00
Ruslan Kuprieiev fe7fa1424b tests: func: convert test_get.py test_utils.py to new fixtures 2019-12-30 09:38:48 +02:00
Ruslan Kuprieiev 0a3a9bf58c Merge pull request #3012 from Suor/simplify-graph
dvc: make graph consist of stages not their paths
2019-12-30 08:07:34 +02:00
Ruslan Kuprieiev 13d9af581f Merge branch 'master' into simplify-graph 2019-12-30 07:29:19 +02:00
Ruslan Kuprieiev ef6018fc2f Merge pull request #3002 from efiop/2458
dvc: support granularity for fetch/pull/push/status/checkout
2019-12-30 07:26:57 +02:00
Alexander Schepanovski b6b07dde8b dvc: make graph consist of stages not their paths 2019-12-30 12:07:52 +07:00
Ruslan Kuprieiev 3fcd758304 travis: fix stages 2019-12-30 00:41:28 +02:00
Ruslan Kuprieiev 054163f850 travis: disable py2 builds 2019-12-30 00:38:57 +02:00
Ruslan Kuprieiev a07bc20c37 Merge pull request #2991 from MaxRis/gdrive-tests
Improve Google Drive auth failure error reporting
2019-12-29 04:35:29 +02:00
Ruslan Kuprieiev c81795c047 dvc: support granularity for fetch/pull/push/status/checkout
Using output path or a subdir/subfile path within an output now works
with `dvc push/pull/fetch/status -c` commands. Other commands don't
support the same logic for now, as there are some questions about what
should commands like `dvc remove` do when given a specific output path.

Example:
    dvc add data
    dvc pull data/subdir # will only pull files within data/subdir

Related to #2458
2019-12-29 01:31:08 +02:00
Max Risuhin 4727cb7893 Improve Google Drive auth failure error reporting 2019-12-28 05:22:37 -08:00
Ruslan Kuprieiev 1038cfc993 Merge pull request #2971 from mroutis/summon
summon: first draft with python specific implementation
2019-12-27 19:57:03 +02:00
Ruslan Kuprieiev a6f023cc4e Merge pull request #3006 from Suor/version-newline
ui: fix extra newline in dvc version
2019-12-27 18:40:10 +02:00
Mr. Outis dad224ef42 summon: os.curdir -> os.getcwd 2019-12-27 10:09:13 -06:00
Mr. Outis e8dd2358f5 summon: fix path popping operation 2019-12-27 10:05:41 -06:00
Alexander Schepanovski 322b354dd5 test: fix and migrate version tests 2019-12-27 22:07:46 +07:00
Ruslan Kuprieiev 5a589c5219 Merge pull request #3003 from Suor/ignore-nuances
test: test ignore nuances
2019-12-27 11:42:13 +02:00
Alexander Schepanovski 5f3e416e60 test: fix test_ignore_external in Python 3.5 2019-12-27 15:51:07 +07:00
Alexander Schepanovski 094482249a ui: fix extra newline in dvc version
Additionally don't try to detect cache fs type unless cache dir exists.
2019-12-27 15:45:41 +07:00
Mr. Outis b97747179b Merge branch 'master' into summon 2019-12-25 14:40:34 -06:00
Mr. Outis 49264b55ac py2: fix problem with ruamel.yaml and unicode 2019-12-25 13:07:40 -06:00
Ruslan Kuprieiev d23ffcf338 dvc: bump to 0.78.1 2019-12-25 16:14:05 +02:00
Ruslan Kuprieiev f1b7f86dbf setup: fix inflect version requirements 2019-12-25 16:04:23 +02:00
Mr. Outis 4a25612282 💅 refactor 2019-12-25 01:04:52 -06:00
Mr. Outis 75ad14aaa2 💅 black 2019-12-25 00:43:33 -06:00
Mr. Outis 0658622ca2 summon: pair-programming adjustments 2019-12-25 00:42:55 -06:00
Mr. Outis 7a58901ec6 💅 black & flake 2019-12-24 20:21:03 -06:00
Mr. Outis 1e65c76ef1 summon: raise SummonError appropiately 2019-12-24 20:20:03 -06:00
Mr. Outis fbb7aebd31 tests/summon: use different args for non-canon.yml 2019-12-24 19:50:43 -06:00
Mr. Outis d45f07cbec summon: make schema more consistent
* Mark optional keywords
* Allow extra entries
* Include objects into the schema
2019-12-24 13:41:57 -06:00
Mr. Outis 1c22cd5af4 summon: adjust comments 2019-12-24 02:20:08 -06:00
Mr. Outis 7dd340aeeb summon: 💅 refactoring/reordering 2019-12-24 02:13:11 -06:00
Mr. Outis 542f4c3c18 summon: wrap chdir_and_syspath in a try/finally 2019-12-24 00:42:13 -06:00
Mr. Outis 6fd5eb4185 summon: use slice instead of get 2019-12-24 00:40:07 -06:00
Mr. Outis 7ece84b840 summon: prevent modifying the yaml args 2019-12-24 00:39:24 -06:00
Alexander Schepanovski e8c321e9a3 test: test ignore nuances
- nested files are ignored
- external dirs are not affected
2019-12-24 12:46:32 +07:00
Mr. Outis 4ad7cb3961 summon: define yaml file schema 2019-12-23 23:45:13 -06:00
Mr. Outis addc56c232 summon: allow different YAML file names 2019-12-18 23:38:52 -06:00
Mr. Outis c852d3316d 💅 black 2019-12-18 23:34:43 -06:00
Mr. Outis ee72280e60 summon: left a comment about chdir and syspath 2019-12-18 23:34:00 -06:00
Mr. Outis 4273ff49b3 summon: params -> args; use summon key in yaml 2019-12-18 23:32:28 -06:00
Mr. Outis 29339d29a2 summon: file & method -> call 2019-12-18 23:25:45 -06:00
Mr. Outis 1b3c771df5 summon: artifacs.yaml -> dvcsummon.yaml; artifact -> object 2019-12-18 22:42:55 -06:00
Mr. Outis 0a54683f91 💅 black 2019-12-18 22:30:22 -06:00
Mr. Outis 32f35ea686 summon: remove scm.add, *_gen already handles it 2019-12-18 22:29:44 -06:00
Mr. Outis 4ce1113ccf summon: use find_out_by_relpath 2019-12-18 22:18:37 -06:00
Mr. Outis fea3d1400a summon: return empty generator for object.deps 2019-12-18 22:15:54 -06:00
Mr. Outis 5d66a572d4 summon: support overriding parameters 2019-12-18 17:20:07 -06:00
Mr. Outis d0344ce975 summon: grimoire/spells -> artifacts 2019-12-18 17:00:39 -06:00
Mr. Outis 789db9169a summon: support checking out dependencies 2019-12-18 16:41:03 -06:00
Mr. Outis b921730f37 summon: safely get a module; change naming 2019-12-18 13:32:23 -06:00
Mr. Outis 3d133186d4 summon: ugly hack 2019-12-18 00:48:10 -06:00
342 changed files with 21594 additions and 11462 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
+3 -4
View File
@@ -1,8 +1,7 @@
* [ ] ❗ Have you followed the guidelines in the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) list?
* [ ] ❗ I have followed the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) checklist.
* [ ] 📖 Check this box if this PR **does not** require [documentation](https://dvc.org/doc) updates, or if it does **and** you have created a separate PR in [dvc.org](https://github.com/iterative/dvc.org) with such updates (or at least opened an issue about it in that repo). Please link below to your PR (or issue) in the [dvc.org](https://github.com/iterative/dvc.org) repo.
* [ ] 📖 If this PR requires [documentation](https://dvc.org/doc) updates, I have created a separate PR (or issue, at least) in [dvc.org](https://github.com/iterative/dvc.org) and linked it here. If the CLI API is changed, I have updated [tab completion scripts](https://github.com/iterative/dvc/tree/master/scripts/completion).
* [ ] ❌ Have you checked DeepSource, CodeClimate, and other sanity checks below? We consider their findings recommendatory and don't expect everything to be addressed. Please review them carefully and fix those that actually improve code or fix bugs.
* [ ] ❌ I will check DeepSource, CodeClimate, and other sanity checks below. (We consider them recommendatory and don't expect everything to be addressed. Please fix things that actually improve code or fix bugs.)
Thank you for the contribution - we'll try to review it as soon as possible. 🙏
+1 -1
View File
@@ -26,7 +26,7 @@ innosetup/config.ini
.coverage
.coverage.*
*.swp
*.sw?
pip-wheel-metadata/
.vscode/
+42 -9
View File
@@ -1,11 +1,44 @@
repos:
- repo: https://github.com/ambv/black
rev: 19.3b0
- hooks:
- id: black
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: black
language_version: python3
- repo: https://gitlab.com/pycqa/flake8
rev: master
hooks:
- id: flake8
language_version: python3
- 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: 3.7.9
- hooks:
- args:
- -i
- '2'
id: beautysh
language_version: python3
repo: https://github.com/lovesegfault/beautysh
rev: master
- hooks:
- id: dvc-pre-commit
language_version: python3
stages:
- commit
- id: dvc-pre-push
language_version: python3
stages:
- push
- always_run: true
id: dvc-post-checkout
language_version: python3
stages:
- post-checkout
repo: https://github.com/iterative/dvc
rev: master
+33
View File
@@ -0,0 +1,33 @@
- args:
- git-hook
- pre-commit
entry: dvc
id: dvc-pre-commit
language: python
language_version: python3
name: DVC pre-commit
stages:
- commit
verbose: true
- args:
- git-hook
- pre-push
entry: dvc
id: dvc-pre-push
language: python
language_version: python3
name: DVC pre-push
stages:
- push
- always_run: true
args:
- git-hook
- post-checkout
entry: dvc
id: dvc-post-checkout
language: python
language_version: python3
minimum_pre_commit_version: 2.2.0
name: DVC post-checkout
stages:
- post-checkout
+10 -1
View File
@@ -1,7 +1,7 @@
---
restylers:
- name: black
image: restyled/restyler-black:v19.3b0
image: restyled/restyler-black:v19.10b0
command:
- black
arguments: []
@@ -9,3 +9,12 @@ restylers:
- "**/*.py"
interpreters:
- python
- name: isort
image: restyled/restyler-isort:v4.3.21
command:
- isort
arguments: []
include:
- "**/*.py"
interpreters:
- python
+48 -48
View File
@@ -13,8 +13,6 @@ stages:
- check
- test
- build
- name: deploy
if: (branch = master OR tag is present) AND repo = iterative/dvc
# Travis doesn't support testing python on Mac yet, so we need
# to workaround it by installing it directly with brew.
#
@@ -35,37 +33,31 @@ jobs:
install:
script: ./scripts/ci/check_patch.sh
# test jobs
- name: "2.7 on Windows"
- name: "3.7 on Windows"
stage: test
os: windows # Windows 10.0.17134 N/A Build 17134
language: shell # 'language: python' is an error on Travis CI Windows
env:
- PY2=1
- name: "3.7 on Windows"
- PYTHON_VERSION=3.7.5
- PATH=/c/Python37:/c/Python37/Scripts:$PATH
- name: "3.8 on Windows"
os: windows # Windows 10.0.17134 N/A Build 17134
language: shell # 'language: python' is an error on Travis CI Windows
- name: "2.7 on Linux"
language: python
python: 2.7
env:
- PY2=1
- name: "3.5 on Linux"
language: python
python: 3.5
- PYTHON_VERSION=3.8.0
- PATH=/c/Python38:/c/Python38/Scripts:$PATH
- name: "3.6 on Linux"
language: python
python: 3.6
- name: "3.7 on Linux"
language: python
python: 3.7
- name: "2.7 on Mac"
- name: "3.8 on Linux"
language: python
python: 3.8
- name: "3.7 on Mac"
os: osx
osx_image: xcode9.4
env:
- PY2=1
- name: "3.6 on Mac"
os: osx
osx_image: xcode9.4
osx_image: xcode11.3
# build jobs
- name: "Mac pkg"
stage: build
@@ -80,57 +72,42 @@ jobs:
language: shell
install:
script: ./scripts/build_windows.cmd
env:
- PYTHON_VERSION=3.7.5
- PATH=/c/Python37:/c/Python37/Scripts:$PATH
- name: "Linux pkgs"
language: python
python: 3.7
before_install:
install:
script: ./scripts/build_posix.sh
- name: "Pypi pkgs"
- name: "PyPI pkgs"
language: python
python: 3.7
python: 3.8
script: ./scripts/build_package.sh
- name: Snapcraft snap
language: python
python: 3.8
addons:
snaps:
- name: snapcraft
channel: stable
confinement: classic
- name: review-tools
channel: stable
- name: lxd
channel: stable
env:
- SNAPCRAFT_IMAGE_INFO: '{"build_url": "$TRAVIS_BUILD_URL"}'
before_install:
install:
- sudo /snap/bin/lxd.migrate -yes
- sudo /snap/bin/lxd waitready
- sudo /snap/bin/lxd init --auto
install:
- git fetch --tags
script:
- sudo snapcraft --use-lxd
- ./scripts/build_snap.sh
after_failure:
- sudo journalctl -u snapd
# deploy jobs
- name: "Pypi pkgs"
if: tag is present
stage: deploy
language: python
python: 3.7
script: ./scripts/build_package.sh
deploy:
- provider: pypi
user:
secure: "IOzNfgPoG3ICg3+s4AuZCCnawgd/KqMzeiAB9R0B9Xb6ProPQJp9O3GBZrtg5OJMHdWQQtwULeOh/LWk4sFdXMcX1iNEgm5Tb7DwO7TDtltW14jUgN9AqfY2twj4UIsW/ON8w0P8v36ZoQq1kO6Pfgjijdm3w+9h6rpps8rJ6jXYpVZVO+SSzzijWddanMkEz4GgctZ2r7U5MWlmm1UogF60kEZIZCQykz0yJnhTpP9nMFDKw6Maki/mcfVwTQ/v3RqjzttHei/aaEMGQXYr3TOX8NPYJXerBpjShDZqcgcCfqPzhi7uCwVYaPvRsMMYH5jgtMz/SO3J6ToeGKT0f1F1ihRyCdusg325TMK7Vv8yHJzL4ngeUbb/oqxWtxhLY7f77Lkx6hBwe6ROpTqiQHIfZad5qyIHihKcDcIHo4FwLHVzxR4y+Ds2js5dXwBkUBaEAx0Dmc6ZdgXqxd815GDwv+VVpzXJhtO6qXmKE36F/Oe6eCNG3dk5MdOK4C2DrM9M8MXllqeQtX6JrM2stWlBalAc9Jn4b0xxAoghX3x3G3hNsLc9Uist1VinY2+IcPozusqj66c+Vbi5YngWSbICOjlIcq2GaOlBi3xioGts396442ztf1eLjMVuvykCI3b4Yz/vJYZOZjUvG/6nLfaFQTJLSXkTvI4do3foeY4="
password:
secure: "ts/3FSPL+w4euXryip9wCJWUqUwuJ2CqHi0nEpYVM10KL0cp9aMkQXN/7C8ih+j/JZ25kaO+tis1nx0JERVnKrg/1F/fJhxHTpakCP4OSIlhKRXDt7yUUHcKKt5QdM2XCKhApptP6RTpWL1vlc36CMrhdvTcMYp7hlDOiWjdhhim24FGQzabL42Ko59ttyyCROqgcnjHqj+WlhJl7iYO+lv2PLOtslpkIYY5EMjgBLuGyOvxTqZsAXtworsASdB2CrsTE6iMRcrM1afgkyBlUqDezoiv9TwfZrZ85oRoAxPB7su4QWK7lEP0wGQV/SDz/RL7c+cNfRaauxUKVcC8I2LfaEpudv8vi205+v/08wzPbS4xSOnf89irUJnyGxUy+bYLoXPPVPMyzcQRAUnrQl+ENCmN62HU+/2pkIyiUJvt0wYYP0vp+3pIbXbYQGmAIMq7nZJd7K3rikfnQIGKI03xZsFe3PEdjzhDI9Ej/tRUbJx4pgg084QuYYca6MSYn/zJ5pqIG4A7ngCSxoxoWZT0duo8PrUWQeT17L73Q86HeEv3OzZhWfEF2fNDN3Qbsu4CllSZnbE7DXgAuysTwTYHS/na6lp7wejOqdRUXrHbidVla9Mwxyc9JdWNWzUDxALvNxAdNbV6l85mO8izy8wjQF6M1O7idqTfjQeR4pc="
skip_existing: true
file_glob: true
file:
- dist/dvc*.whl
- dist/dvc*.tar.gz
skip_cleanup: true
on:
tags: true
repo: iterative/dvc
before_install:
- bash ./scripts/ci/before_install.sh
@@ -142,7 +119,6 @@ script:
- bash ./scripts/ci/script.sh
deploy:
- provider: releases
condition: "[[ $(./scripts/ci/deploy_condition.sh) == 'true' ]]"
api_key:
secure: nyo17gF4Xl6DFL/KVkMewC805Cv+eK99L7SiYCU+WUAYDIzzFL3YJm7fm8bznPeor7DmpO7uSLSveMnJETBtdurW6JWhCTUss4yD66yFzpk6Z5cvHf/1UA5pdMHkkMM54fRaTRt6L2Yal7RPwGEQTBECLXW1Xmf78Y6+0l8lTvXQ9jXOQSRO/63VDKI5Zww2+XytVicUFlr0hLMGKUuaCjjXcQlAPzBgQz3/5Iz6EtzeTf2o+gTPjocbRbSFPikfiBXUn8XlJ8jBj7i114XJRJffw59KxYGDzxp+enfLBULGNoW0Np2hZqkKkPUIdB08C8XVbRXlFYX8cEYBczjqhv8A05p5F5CnltnqQ0KazcoUP2awPVqn68dWNRF+kqrnMBus8euYKIZ4dy/6bVEKJRa84ESJXZVDcwXTAXRqyovSxOlXqg9ZgAImMKBfKn/fpDX2xIvQPaXcnJxKbmcePfdbC1d0HYJ5jkbNaxKE/T15VvhzIho7mJ2WAv1r8QouQUuzqtpzIGbRn/KXHRUzCtyP0k4+eMKMLFOPzg848WsDarP3v8VYiSy4nFY7ZP7DRd3KttvW5bgVbSeO4jPmTgS6TnnpRwual9BJXSG7bjbfpIMii3bNZ7jWL4zpKT9Jfm8w9RGKLcX9HqkZ3+y60+LtYdhEfBvqsIlWQoIJBXg=
file_glob: true
@@ -151,9 +127,33 @@ deploy:
- dvc*.deb
- dvc*.pkg
- dvc*.exe
- dvc*.snap
skip_cleanup: true
on:
condition: "$(./scripts/ci/deploy_condition.sh dvc*.rpm dvc*.deb dvc*.pkg dvc*.exe)"
tags: true
repo: iterative/dvc
stage: build
- provider: pypi
user:
secure: "IOzNfgPoG3ICg3+s4AuZCCnawgd/KqMzeiAB9R0B9Xb6ProPQJp9O3GBZrtg5OJMHdWQQtwULeOh/LWk4sFdXMcX1iNEgm5Tb7DwO7TDtltW14jUgN9AqfY2twj4UIsW/ON8w0P8v36ZoQq1kO6Pfgjijdm3w+9h6rpps8rJ6jXYpVZVO+SSzzijWddanMkEz4GgctZ2r7U5MWlmm1UogF60kEZIZCQykz0yJnhTpP9nMFDKw6Maki/mcfVwTQ/v3RqjzttHei/aaEMGQXYr3TOX8NPYJXerBpjShDZqcgcCfqPzhi7uCwVYaPvRsMMYH5jgtMz/SO3J6ToeGKT0f1F1ihRyCdusg325TMK7Vv8yHJzL4ngeUbb/oqxWtxhLY7f77Lkx6hBwe6ROpTqiQHIfZad5qyIHihKcDcIHo4FwLHVzxR4y+Ds2js5dXwBkUBaEAx0Dmc6ZdgXqxd815GDwv+VVpzXJhtO6qXmKE36F/Oe6eCNG3dk5MdOK4C2DrM9M8MXllqeQtX6JrM2stWlBalAc9Jn4b0xxAoghX3x3G3hNsLc9Uist1VinY2+IcPozusqj66c+Vbi5YngWSbICOjlIcq2GaOlBi3xioGts396442ztf1eLjMVuvykCI3b4Yz/vJYZOZjUvG/6nLfaFQTJLSXkTvI4do3foeY4="
password:
secure: "ts/3FSPL+w4euXryip9wCJWUqUwuJ2CqHi0nEpYVM10KL0cp9aMkQXN/7C8ih+j/JZ25kaO+tis1nx0JERVnKrg/1F/fJhxHTpakCP4OSIlhKRXDt7yUUHcKKt5QdM2XCKhApptP6RTpWL1vlc36CMrhdvTcMYp7hlDOiWjdhhim24FGQzabL42Ko59ttyyCROqgcnjHqj+WlhJl7iYO+lv2PLOtslpkIYY5EMjgBLuGyOvxTqZsAXtworsASdB2CrsTE6iMRcrM1afgkyBlUqDezoiv9TwfZrZ85oRoAxPB7su4QWK7lEP0wGQV/SDz/RL7c+cNfRaauxUKVcC8I2LfaEpudv8vi205+v/08wzPbS4xSOnf89irUJnyGxUy+bYLoXPPVPMyzcQRAUnrQl+ENCmN62HU+/2pkIyiUJvt0wYYP0vp+3pIbXbYQGmAIMq7nZJd7K3rikfnQIGKI03xZsFe3PEdjzhDI9Ej/tRUbJx4pgg084QuYYca6MSYn/zJ5pqIG4A7ngCSxoxoWZT0duo8PrUWQeT17L73Q86HeEv3OzZhWfEF2fNDN3Qbsu4CllSZnbE7DXgAuysTwTYHS/na6lp7wejOqdRUXrHbidVla9Mwxyc9JdWNWzUDxALvNxAdNbV6l85mO8izy8wjQF6M1O7idqTfjQeR4pc="
skip_existing: true
file_glob: true
file:
- dist/dvc*.whl
- dist/dvc*.tar.gz
skip_cleanup: true
on:
condition: "$(./scripts/ci/deploy_condition.sh dist/dvc*.whl dist/dvc*.tar.gz)"
tags: true
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 || echo timed out"
on:
all_branches: true
condition: "$(./scripts/ci/deploy_condition.sh dvc_*.snap) && ($TRAVIS_BRANCH = master || -n $TRAVIS_TAG)"
repo: iterative/dvc
stage: build
+8
View File
@@ -0,0 +1,8 @@
{
"title": "DVC: Data Version Control - Git for Data & Models",
"keywords": [
"data-science", "data-version-control", "machine-learning", "git",
"developer-tools", "reproducibility", "collaboration", "ai", "python"],
"contributors": [
{"name": "DVC team", "type": "Other", "affiliation": "Iterative"}]
}
+132 -76
View File
@@ -1,6 +1,4 @@
.. image:: https://dvc.org/static/img/logo-github-readme.png
:target: https://dvc.org
:alt: DVC logo
|Banner|
`Website <https://dvc.org>`_
• `Docs <https://dvc.org/doc>`_
@@ -10,29 +8,9 @@
• `Tutorial <https://dvc.org/doc/get-started>`_
• `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
.. image:: https://travis-ci.com/iterative/dvc.svg?branch=master
:target: https://travis-ci.com/iterative/dvc
:alt: Travis
|Release| |CI| |Maintainability| |Coverage| |Donate| |DOI|
.. image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
:target: https://codeclimate.com/github/iterative/dvc
:alt: Code Climate
.. image:: https://codecov.io/gh/iterative/dvc/branch/master/graph/badge.svg
:target: https://codecov.io/gh/iterative/dvc
:alt: Codecov
.. image:: https://img.shields.io/badge/patreon-donate-green.svg
:target: https://www.patreon.com/DVCorg/overview
:alt: Donate
.. image:: https://anaconda.org/conda-forge/dvc/badges/version.svg
:target: https://anaconda.org/conda-forge/dvc
:alt: Conda-forge
.. image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
:target: https://snapcraft.io/dvc
:alt: Snapcraft
|Snap| |Choco| |Brew| |Conda| |PyPI| |Packages|
|
@@ -75,9 +53,7 @@ to store data and model files seamlessly out of Git, while preserving almost the
were stored in Git itself. To store and share the data cache, DVC supports multiple remotes - any cloud (S3, Azure,
Google Cloud, etc) or any on-premise network storage (via SSH, for example).
.. image:: https://dvc.org/static/img/flow.gif
:target: https://dvc.org/static/img/flow.gif
:alt: how_dvc_works
|Flowchart|
The DVC pipelines (computational graph) feature connects code and data together. It is possible to explicitly
specify all steps required to produce a model: input dependencies including data, commands to run,
@@ -115,9 +91,52 @@ Installation
There are four options to install DVC: ``pip``, Homebrew, Conda (Anaconda) or an OS-specific package.
Full instructions are `available here <https://dvc.org/doc/get-started/install>`_.
Snap (Snapcraft/Linux)
----------------------
|Snap|
.. code-block:: bash
snap install dvc --classic
This corresponds to the latest tagged release.
Add ``--edge`` for the latest ``master`` version.
Choco (Chocolatey/Windows)
--------------------------
|Choco|
.. code-block:: bash
choco install dvc
Brew (Homebrew/Mac OS)
----------------------
|Brew|
.. code-block:: bash
brew install dvc
Conda (Anaconda)
----------------
|Conda|
.. code-block:: bash
conda install -c conda-forge dvc
Currently, this includes support for Python versions 2.7, 3.6 and 3.7.
pip (PyPI)
----------
|PyPI|
.. code-block:: bash
pip install dvc
@@ -133,41 +152,11 @@ To install the development version, run:
pip install git+git://github.com/iterative/dvc
Homebrew
--------
.. code-block:: bash
brew install dvc
Conda (Anaconda)
----------------
.. code-block:: bash
conda install -c conda-forge dvc
Currently, this includes support for Python versions 2.7, 3.6 and 3.7.
Snap (Snapcraft)
----------------
Download the latest ``dvc_*.snap`` from the
GitHub `releases page <https://github.com/iterative/dvc/releases>`_.
.. code-block:: bash
snap install dvc_*.snap --dangerous --classic
Once ``dvc`` is released on the snap store
(`pending request <https://forum.snapcraft.io/t/classic-confinement-request-for-dvc/14124>`_)
there will be no need to download ``dvc_*.snap`` or use ``--dangerous``
(``snap install dvc --classic`` would suffice).
Package
-------
|Packages|
Self-contained packages for Linux, Windows, and Mac are available. The latest version of the packages
can be found on the GitHub `releases page <https://github.com/iterative/dvc/releases>`_.
@@ -207,40 +196,43 @@ Comparison to related technologies
Contributing
============
|Maintainability| |Donate|
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more
details.
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/0
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/0
:alt: 0
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/0
:alt: 0
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/1
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/1
:alt: 1
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/1
:alt: 1
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/2
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/2
:alt: 2
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/2
:alt: 2
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/3
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/3
:alt: 3
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/3
:alt: 3
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/4
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/4
:alt: 4
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/4
:alt: 4
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/5
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/5
:alt: 5
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/5
:alt: 5
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/6
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/6
:alt: 6
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/6
:alt: 6
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/7
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/7
:alt: 7
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/7
:alt: 7
Mailing List
============
@@ -254,3 +246,67 @@ This project is distributed under the Apache license version 2.0 (see the LICENS
By submitting a pull request to this project, you agree to license your contribution under the Apache license version
2.0 to this project.
Citation
========
|DOI|
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/img/logo-github-readme.png
:target: https://dvc.org
:alt: DVC logo
.. |Release| image:: https://img.shields.io/badge/release-ok-brightgreen
:target: https://travis-ci.com/iterative/dvc/branches
:alt: Release
.. |CI| image:: https://img.shields.io/travis/com/iterative/dvc/master?label=dev&logo=travis
:target: https://travis-ci.com/iterative/dvc/builds
:alt: Travis dev branch
.. |Maintainability| image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
:target: https://codeclimate.com/github/iterative/dvc
:alt: Code Climate
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/master/graph/badge.svg
:target: https://codecov.io/gh/iterative/dvc
:alt: Codecov
.. |Donate| image:: https://img.shields.io/badge/patreon-donate-green.svg?logo=patreon
:target: https://www.patreon.com/DVCorg/overview
:alt: Donate
.. |Snap| image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
:target: https://snapcraft.io/dvc
:alt: Snapcraft
.. |Choco| image:: https://img.shields.io/chocolatey/v/dvc?label=choco
:target: https://chocolatey.org/packages/dvc
:alt: Chocolatey
.. |Brew| image:: https://img.shields.io/homebrew/v/dvc?label=brew
:target: https://formulae.brew.sh/formula/dvc
:alt: Homebrew
.. |Conda| image:: https://img.shields.io/conda/v/conda-forge/dvc.svg?label=conda&logo=conda-forge
:target: https://anaconda.org/conda-forge/dvc
:alt: Conda-forge
.. |PyPI| image:: https://img.shields.io/pypi/v/dvc.svg?label=pip&logo=PyPI&logoColor=white
:target: https://pypi.org/project/dvc
:alt: PyPI
.. |Packages| image:: https://img.shields.io/github/v/release/iterative/dvc?label=deb|pkg|rpm|exe&logo=GitHub
:target: https://github.com/iterative/dvc/releases/latest
:alt: deb|pkg|rpm|exe
.. |DOI| image:: https://img.shields.io/badge/DOI-10.5281/zenodo.3677553-blue.svg
:target: https://doi.org/10.5281/zenodo.3677553
:alt: DOI
.. |Flowchart| image:: https://dvc.org/img/flow.gif
:target: https://dvc.org/img/flow.gif
:alt: how_dvc_works
+1 -1
View File
@@ -2,4 +2,4 @@ dvc (0.75.b1) experimental; urgency=low
* First native Debian package version.
-- Dmitry Petrov <dmitry@dataversioncontrol.com> Sat, 07 Dec 2019 09:49:28 +0300
-- Dmitry Petrov <dmitry@dvc.org> Sat, 07 Dec 2019 09:49:28 +0300
+1 -1
View File
@@ -1,5 +1,5 @@
Source: dvc
Maintainer: Dmitry Petrov <dmitry@dataversioncontrol.com>
Maintainer: Dmitry Petrov <dmitry@dvc.org>
Section: python
Priority: optional
Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9)
+2 -2
View File
@@ -1,10 +1,10 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: DVC
Upstream-Contact: Dmitry Petrov <dmitry@dataversioncontrol.com>
Upstream-Contact: Dmitry Petrov <dmitry@dvc.org>
Source: https://github.com/iterative/dvc
Files: *
Copyright: Dmitry Petrov <dmitry@dataversioncontrol.com>
Copyright: Dmitry Petrov <dmitry@dvc.org>
License: Apache 2.0
On Debian systems, the full text of the Apache 2.0
License can be found in the file
-12
View File
@@ -3,19 +3,7 @@ DVC
----
Make your data science projects reproducible and shareable.
"""
from __future__ import unicode_literals
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")
+1 -3
View File
@@ -1,6 +1,4 @@
"""Main entry point for dvc command line tool."""
from __future__ import unicode_literals
"""Main entry point for DVC command line tool."""
import sys
from dvc.main import main
+23 -19
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
@@ -15,10 +15,10 @@ from dvc.daemon import daemon
from dvc.exceptions import NotDvcRepoError
from dvc.lock import Lock, LockError
from dvc.repo import Repo
from dvc.scm import SCM
from dvc.utils import env2bool, is_binary, makedirs
from dvc.utils.compat import str, FileNotFoundError
from dvc.scm import SCM, NoSCM
from dvc.scm.base import SCMError
from dvc.utils import env2bool, is_binary
from dvc.utils.fs import makedirs
logger = logging.getLogger(__name__)
@@ -35,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"):
@@ -54,17 +54,14 @@ def is_enabled():
return False
enabled = to_bool(
Config(validate=False)
.config.get(Config.SECTION_CORE, {})
.get(Config.SECTION_CORE_ANALYTICS, "true")
Config(validate=False).get("core", {}).get("analytics", "true")
)
logger.debug("Analytics is {}abled.".format("en" if enabled else "dis"))
return enabled
def send(report):
def send(path):
"""
Side effect: Removes the report after sending it.
@@ -75,16 +72,25 @@ def send(report):
url = "https://analytics.dvc.org"
headers = {"content-type": "application/json"}
with open(report, "rb") as fobj:
requests.post(url, data=fobj, headers=headers, timeout=5)
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():
try:
scm = SCM(root_dir=Repo.find_root())
return type(scm).__name__
except SCMError:
return NoSCM.__name__
except NotDvcRepoError:
pass
@@ -141,7 +147,7 @@ def _find_or_create_user_id():
IDs are generated randomly with UUID.
"""
config_dir = Config.get_global_config_dir()
config_dir = Config.get_dir("global")
fname = os.path.join(config_dir, "user_id")
lockfile = os.path.join(config_dir, "user_id.lock")
@@ -152,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):
@@ -164,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}'")
+53 -23
View File
@@ -1,27 +1,51 @@
import os
from contextlib import _GeneratorContextManager as GCM
from contextlib import contextmanager
try:
from contextlib import _GeneratorContextManager as GCM
except ImportError:
from contextlib import GeneratorContextManager as GCM
from dvc.utils.compat import urlparse
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__(f"'{url}' is not a DVC repository.")
def get_url(path, repo=None, rev=None, remote=None):
"""Returns an url of a resource specified by path in repo"""
"""
Returns the URL to the storage location of a data file or directory tracked
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
supplied. The default remote is tried unless a remote argument is supplied.
Raises UrlNotDvcRepoError if repo is not a DVC project.
NOTE: This function does not check for the actual existence of the file or
directory in the remote storage.
"""
with _make_repo(repo, rev=rev) as _repo:
abspath = os.path.join(_repo.root_dir, path)
out, = _repo.find_outs_by_path(abspath)
if not isinstance(_repo, Repo):
raise UrlNotDvcRepoError(_repo.url)
out = _repo.find_out_by_relpath(path)
remote_obj = _repo.cloud.get_remote(remote)
return str(remote_obj.checksum_to_path_info(out.checksum))
def open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
"""Opens a specified resource as a file descriptor"""
"""
Open file in the supplied path tracked in a repo (both DVC projects and
plain Git repos are supported). For Git repos, HEAD is used unless a rev
argument is supplied. The default remote is tried unless a remote argument
is supplied. It may only be used as a context manager:
with dvc.api.open(
'path/to/file',
repo='https://example.com/url/to/repo'
) as fd:
# ... Handle file object fd
"""
args = (path,)
kwargs = {
"repo": repo,
@@ -40,21 +64,24 @@ class _OpenContextManager(GCM):
def __getattr__(self, name):
raise AttributeError(
"dvc.api.open() should be used in a with statement"
"dvc.api.open() should be used in a with statement."
)
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
with _make_repo(repo, rev=rev) as _repo:
abspath = os.path.join(_repo.root_dir, path)
with _repo.open(
abspath, remote=remote, mode=mode, encoding=encoding
with _repo.open_by_relpath(
path, remote=remote, mode=mode, encoding=encoding
) as fd:
yield fd
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
"""Read a specified resource into string"""
"""
Returns the contents of a tracked file (by DVC or Git). For Git repos, HEAD
is used unless a rev argument is supplied. The default remote is tried
unless a remote argument is supplied.
"""
with open(
path, repo=repo, rev=rev, remote=remote, mode=mode, encoding=encoding
) as fd:
@@ -62,10 +89,13 @@ def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
@contextmanager
def _make_repo(repo_url, rev=None):
if not repo_url or urlparse(repo_url).scheme == "":
assert rev is None, "Custom revision is not supported for local repo"
yield Repo(repo_url)
else:
with external_repo(url=repo_url, rev=rev) as repo:
yield repo
def _make_repo(repo_url=None, rev=None):
repo_url = repo_url or os.getcwd()
if rev is None and os.path.exists(repo_url):
try:
yield Repo(repo_url)
return
except NotDvcRepoError:
pass # fallthrough to external_repo
with external_repo(url=repo_url, rev=rev) as repo:
yield repo
+83 -56
View File
@@ -1,28 +1,8 @@
"""Manages cache of a dvc repo."""
from __future__ import unicode_literals
import os
"""Manages cache of a DVC repo."""
from collections import defaultdict
from funcy import cached_property
from dvc.config import Config
from dvc.utils.compat import builtin_str
class CacheConfig(object):
def __init__(self, config):
self.config = config
def set_dir(self, dname, level=None):
from dvc.remote.config import RemoteConfig
configobj = self.config.get_configobj(level)
path = RemoteConfig.resolve_path(dname, configobj.filename)
self.config.set(
Config.SECTION_CACHE, Config.SECTION_CACHE_DIR, path, level=level
)
def _make_remote_property(name):
"""
@@ -56,12 +36,12 @@ def _make_remote_property(name):
return Remote(self.repo, name=remote)
getter.__name__ = builtin_str(name)
getter.__name__ = name
return cached_property(getter)
class Cache(object):
"""Class that manages cache locations of a dvc repo.
class Cache:
"""Class that manages cache locations of a DVC repo.
Args:
repo (dvc.repo.Repo): repo instance that this cache belongs to.
@@ -73,42 +53,57 @@ class Cache(object):
from dvc.remote import Remote
self.repo = repo
self.config = config = repo.config["cache"]
self.config = config = repo.config.config[Config.SECTION_CACHE]
local = config.get(Config.SECTION_CACHE_LOCAL)
local = config.get("local")
if local:
name = Config.SECTION_REMOTE_FMT.format(local)
settings = repo.config.config[name]
settings = {"name": local}
else:
default_cache_dir = os.path.join(repo.dvc_dir, self.CACHE_DIR)
cache_dir = config.get(Config.SECTION_CACHE_DIR, default_cache_dir)
cache_type = config.get(Config.SECTION_CACHE_TYPE)
protected = config.get(Config.SECTION_CACHE_PROTECTED)
shared = config.get(Config.SECTION_CACHE_SHARED)
settings = {
Config.PRIVATE_CWD: config.get(
Config.PRIVATE_CWD, repo.dvc_dir
),
Config.SECTION_REMOTE_URL: cache_dir,
Config.SECTION_CACHE_TYPE: cache_type,
Config.SECTION_CACHE_PROTECTED: protected,
Config.SECTION_CACHE_SHARED: shared,
}
settings = {**config, "url": config["dir"]}
self.local = Remote(repo, **settings)
s3 = _make_remote_property(Config.SECTION_CACHE_S3)
gs = _make_remote_property(Config.SECTION_CACHE_GS)
ssh = _make_remote_property(Config.SECTION_CACHE_SSH)
hdfs = _make_remote_property(Config.SECTION_CACHE_HDFS)
azure = _make_remote_property(Config.SECTION_CACHE_AZURE)
s3 = _make_remote_property("s3")
gs = _make_remote_property("gs")
ssh = _make_remote_property("ssh")
hdfs = _make_remote_property("hdfs")
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
@@ -121,7 +116,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)
@@ -129,11 +135,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()
+43 -39
View File
@@ -1,43 +1,44 @@
"""Command line interface for dvc."""
from __future__ import print_function
from __future__ import unicode_literals
"""DVC command line interface"""
import argparse
import logging
import sys
import dvc.command.add as add
import dvc.command.cache as cache
import dvc.command.checkout as checkout
import dvc.command.commit as commit
import dvc.command.config as config
import dvc.command.daemon as daemon
import dvc.command.data_sync as data_sync
import dvc.command.destroy as destroy
import dvc.command.diff as diff
import dvc.command.gc as gc
import dvc.command.get as get
import dvc.command.get_url as get_url
import dvc.command.imp as imp
import dvc.command.imp_url as imp_url
import dvc.command.init as init
import dvc.command.install as install
import dvc.command.lock as lock
import dvc.command.metrics as metrics
import dvc.command.move as move
import dvc.command.pipeline as pipeline
import dvc.command.remote as remote
import dvc.command.remove as remove
import dvc.command.repro as repro
import dvc.command.root as root
import dvc.command.run as run
import dvc.command.tag as tag
import dvc.command.unprotect as unprotect
import dvc.command.update as update
import dvc.command.version as version
from dvc.command.base import fix_subparsers
from dvc.exceptions import DvcParserError
from .command import (
add,
cache,
checkout,
commit,
config,
daemon,
data_sync,
destroy,
diff,
gc,
get,
get_url,
git_hook,
imp,
imp_url,
init,
install,
lock,
ls,
metrics,
move,
params,
pipeline,
plots,
remote,
remove,
repro,
root,
run,
unprotect,
update,
version,
)
from .command.base import fix_subparsers
from .exceptions import DvcParserError
logger = logging.getLogger(__name__)
@@ -61,16 +62,19 @@ COMMANDS = [
remote,
cache,
metrics,
params,
install,
root,
ls,
lock,
pipeline,
daemon,
commit,
tag,
diff,
version,
update,
git_hook,
plots,
]
@@ -92,7 +96,7 @@ class DvcParser(argparse.ArgumentParser):
class VersionAction(argparse.Action): # pragma: no cover
# pylint: disable=too-few-public-methods
"""Shows dvc version and exits."""
"""Shows DVC version and exits."""
def __call__(self, parser, namespace, values, option_string=None):
from dvc import __version__
@@ -173,7 +177,7 @@ def parse_args(argv=None):
title="Available Commands",
metavar="COMMAND",
dest="cmd",
help="Use dvc COMMAND --help for command-specific help.",
help="Use `dvc COMMAND --help` for command-specific help.",
)
fix_subparsers(subparsers)
+3 -7
View File
@@ -1,13 +1,8 @@
from __future__ import unicode_literals
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.exceptions import DvcException
from dvc.exceptions import RecursiveAddingWhileUsingFilename
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException, RecursiveAddingWhileUsingFilename
logger = logging.getLogger(__name__)
@@ -58,6 +53,7 @@ def add_parser(subparsers, parent_parser):
"-f",
"--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."
+9 -16
View File
@@ -1,8 +1,5 @@
from __future__ import unicode_literals
import logging
logger = logging.getLogger(__name__)
@@ -14,11 +11,8 @@ def fix_subparsers(subparsers):
Args:
subparsers: subparsers to fix.
"""
from dvc.utils.compat import is_py3
if is_py3: # pragma: no cover
subparsers.required = True
subparsers.dest = "cmd"
subparsers.required = True
subparsers.dest = "cmd"
def append_doc_link(help_message, path):
@@ -32,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
@@ -40,18 +34,17 @@ class CmdBase(object):
self.repo = Repo()
self.config = self.repo.config
self.args = args
hardlink_lock = self.config.config["core"].get("hardlink_lock", False)
updater = Updater(self.repo.dvc_dir, hardlink_lock=hardlink_lock)
hardlink_lock = self.config["core"].get("hardlink_lock", False)
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):
+5 -11
View File
@@ -1,20 +1,13 @@
from __future__ import unicode_literals
import argparse
from dvc.cache import CacheConfig
from dvc.command.base import append_doc_link
from dvc.command.base import fix_subparsers
from dvc.command.base import append_doc_link, fix_subparsers
from dvc.command.config import CmdConfig
class CmdCacheDir(CmdConfig):
def __init__(self, args):
super(CmdCacheDir, self).__init__(args)
self.cache_config = CacheConfig(self.config)
def run(self):
self.cache_config.set_dir(self.args.value, level=self.args.level)
with self.config.edit(level=self.args.level) as edit:
edit["cache"]["dir"] = self.args.value
return 0
@@ -32,7 +25,8 @@ def add_parser(subparsers, parent_parser):
)
cache_subparsers = cache_parser.add_subparsers(
dest="cmd", help="Use dvc cache CMD --help for command-specific help."
dest="cmd",
help="Use `dvc cache CMD --help` for command-specific " "help.",
)
fix_subparsers(cache_subparsers)
+64 -11
View File
@@ -1,20 +1,67 @@
from __future__ import unicode_literals
import argparse
import logging
import operator
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
import colorama
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 log_changes(stats):
colors = [
("modified", colorama.Fore.YELLOW,),
("added", colorama.Fore.GREEN),
("deleted", colorama.Fore.RED,),
]
for state, color in colors:
entries = stats.get(state)
if not entries:
continue
for entry in entries:
logger.info(
"{color}{state}{nc}{spacing}{entry}".format(
color=color,
state=state[0].upper(),
nc=colorama.Fore.RESET,
spacing="\t",
entry=entry,
)
)
class CmdCheckout(CmdBase):
def run(self):
self.repo.checkout(
targets=self.args.targets,
with_deps=self.args.with_deps,
force=self.args.force,
relink=self.args.relink,
recursive=self.args.recursive,
)
stats, exc = None, None
try:
stats = self.repo.checkout(
targets=self.args.targets,
with_deps=self.args.with_deps,
force=self.args.force,
relink=self.args.relink,
recursive=self.args.recursive,
)
except CheckoutError as _exc:
exc = _exc
stats = exc.stats
if self.args.summary:
default_message = "No changes."
msg = get_summary(
sorted(stats.items(), key=operator.itemgetter(0))
)
logger.info(msg or default_message)
else:
log_changes(stats)
if exc:
raise exc
return 0
@@ -28,6 +75,12 @@ def add_parser(subparsers, parent_parser):
help=CHECKOUT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
checkout_parser.add_argument(
"--summary",
action="store_true",
default=False,
help="Show summary of the changes.",
)
checkout_parser.add_argument(
"-d",
"--with-deps",
+2 -6
View File
@@ -1,13 +1,9 @@
from __future__ import unicode_literals
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__)
@@ -49,7 +45,7 @@ def add_parser(subparsers, parent_parser):
"--force",
action="store_true",
default=False,
help="Commit even if checksums for dependencies/outputs changed.",
help="Commit even if hash value for dependencies/outputs changed.",
)
commit_parser.add_argument(
"-d",
+30 -21
View File
@@ -1,36 +1,36 @@
from __future__ import unicode_literals
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBaseNoRepo
from dvc.config import Config
from dvc.command.base import CmdBaseNoRepo, append_doc_link
from dvc.config import Config, ConfigError
logger = logging.getLogger(__name__)
class CmdConfig(CmdBaseNoRepo):
def __init__(self, args):
super(CmdConfig, self).__init__(args)
super().__init__(args)
self.config = Config(validate=False)
def run(self):
section, opt = self.args.name.lower().strip().split(".", 1)
if self.args.unset:
self.config.unset(section, opt, level=self.args.level)
elif self.args.value is None:
logger.info(self.config.get(section, opt, level=self.args.level))
else:
self.config.set(
section, opt, self.args.value, level=self.args.level
)
if self.args.value is None and not self.args.unset:
conf = self.config.load_one(self.args.level)
self._check(conf, section, opt)
logger.info(conf[section][opt])
return 0
is_write = self.args.unset or self.args.value is not None
if is_write and self.args.name == "cache.type":
with self.config.edit(self.args.level) as conf:
if self.args.unset:
self._check(conf, section, opt)
del conf[section][opt]
else:
self._check(conf, section)
conf[section][opt] = self.args.value
if self.args.name == "cache.type":
logger.warning(
"You have changed the 'cache.type' option. This doesn't update"
" any existing workspace file links, but it can be done with:"
@@ -39,30 +39,39 @@ class CmdConfig(CmdBaseNoRepo):
return 0
def _check(self, conf, section, opt=None):
if section not in conf:
msg = "section {} doesn't exist"
raise ConfigError(msg.format(self.args.name))
if opt and opt not in conf[section]:
msg = "option {} doesn't exist"
raise ConfigError(msg.format(self.args.name))
parent_config_parser = argparse.ArgumentParser(add_help=False)
parent_config_parser.add_argument(
"--global",
dest="level",
action="store_const",
const=Config.LEVEL_GLOBAL,
const="global",
help="Use global config.",
)
parent_config_parser.add_argument(
"--system",
dest="level",
action="store_const",
const=Config.LEVEL_SYSTEM,
const="system",
help="Use system config.",
)
parent_config_parser.add_argument(
"--local",
dest="level",
action="store_const",
const=Config.LEVEL_LOCAL,
const="local",
help="Use local config.",
)
parent_config_parser.set_defaults(level=Config.LEVEL_REPO)
parent_config_parser.set_defaults(level="repo")
def add_parser(subparsers, parent_parser):
+6 -9
View File
@@ -1,7 +1,4 @@
from __future__ import unicode_literals
from dvc.command.base import CmdBaseNoRepo
from dvc.command.base import fix_subparsers
from dvc.command.base import CmdBaseNoRepo, fix_subparsers
class CmdDaemonBase(CmdBaseNoRepo):
@@ -17,11 +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.config.get("core", {}).get(
"hardlink_lock", False
)
updater = Updater(dvc_dir, hardlink_lock=hardlink_lock)
hardlink_lock = config.get("core", {}).get("hardlink_lock", False)
updater = Updater(tmp_dir, hardlink_lock=hardlink_lock)
updater.fetch(detach=False)
return 0
@@ -46,7 +42,8 @@ def add_parser(subparsers, parent_parser):
)
daemon_subparsers = daemon_parser.add_subparsers(
dest="cmd", help="Use dvc daemon CMD --help for command-specific help."
dest="cmd",
help="Use `dvc daemon CMD --help` for command-specific " "help.",
)
fix_subparsers(daemon_subparsers)
+101 -28
View File
@@ -1,40 +1,45 @@
from __future__ import unicode_literals
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import CmdBase
from dvc.exceptions import DvcException
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:
processed_files_count = self.repo.pull(
stats = self.repo.pull(
targets=self.args.targets,
jobs=self.args.jobs,
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,
)
except DvcException:
self.log_summary(stats)
except (CheckoutError, DvcException) as exc:
self.log_summary(getattr(exc, "stats", {}))
logger.exception("failed to pull data from the cloud")
return 1
self.check_up_to_date(processed_files_count)
return 0
@@ -47,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
@@ -66,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
@@ -80,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",
@@ -110,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",
@@ -126,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",
@@ -147,10 +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 = "Push data files to a DVC remote storage."
PUSH_HELP = "Upload tracked files or directories to remote storage."
push_parser = subparsers.add_parser(
"push",
@@ -160,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",
@@ -176,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",
@@ -188,12 +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 = "Fetch data files from a DVC remote storage."
FETCH_HELP = (
"Get tracked files or directories from remote storage"
" into the cache."
)
fetch_parser = subparsers.add_parser(
"fetch",
@@ -203,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",
@@ -219,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",
@@ -233,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
@@ -266,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",
@@ -284,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",
@@ -291,4 +356,12 @@ 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.set_defaults(func=CmdDataStatus)
+1 -5
View File
@@ -1,14 +1,10 @@
from __future__ import unicode_literals
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__)
+134 -133
View File
@@ -1,172 +1,173 @@
from __future__ import unicode_literals
import argparse
import json
import logging
import os
import humanize
import inflect
from funcy import compact
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 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 _print_size(size):
if size < 0:
change = "decreased by {}"
elif size > 0:
change = "increased by {}"
else:
change = "not changed"
natur_size = humanize.naturalsize(abs(size))
return change.format(natur_size)
def _format(diff):
"""
Given a diff structure, generate a string of paths separated
by new lines and grouped together by their state.
@staticmethod
def _get_md5_string(sign, file_name, checksum):
sample_msg = ""
if file_name:
sample_msg = "{}{} with md5 {}\n"
sample_msg = sample_msg.format(sign, file_name, checksum)
return sample_msg
A group's header is colored and its entries are sorted to enhance
readability, for example:
@classmethod
def _get_dir_changes(cls, dct):
import dvc.repo.diff as diff
Added:
another_file.txt
backup.tar
dir/
dir/1
engine = inflect.engine()
changes_msg = (
"{} {} untouched, {} {} modified, {} {} added, "
"{} {} deleted, size was {}"
An example of a diff formatted when entries contain hash:
Added:
d3b07384 foo
Modified:
c157a790..f98bf6f1 bar
If a group has no entries, it won't be included in the result.
At the bottom, include a summary with the number of files per state.
"""
def _digest(checksum):
if type(checksum) is str:
return checksum[0:8]
return "{}..{}".format(checksum["old"][0:8], checksum["new"][0:8])
colors = {
"added": colorama.Fore.GREEN,
"modified": colorama.Fore.YELLOW,
"deleted": colorama.Fore.RED,
}
summary = {}
groups = []
for state in ["added", "deleted", "modified"]:
summary[state] = 0
entries = diff[state]
if not entries:
continue
content = []
for entry in entries:
path = entry["path"]
checksum = entry.get("hash")
summary[state] += 1 if not path.endswith(os.sep) else 0
content.append(
"{space}{checksum}{separator}{path}".format(
space=" ",
checksum=_digest(checksum) if checksum else "",
separator=" " if checksum else "",
path=entry["path"],
)
)
groups.append(
"{color}{header}{nc}:\n{content}".format(
color=colors[state],
header=state.capitalize(),
nc=colorama.Fore.RESET,
content="\n".join(content),
)
)
groups.append(
"files summary: {added} added, {deleted} deleted,"
" {modified} modified".format_map(summary)
)
changes_msg = changes_msg.format(
dct[diff.DIFF_IDENT],
engine.plural("file", dct[diff.DIFF_IDENT]),
dct[diff.DIFF_CHANGE],
engine.plural("file", dct[diff.DIFF_CHANGE]),
dct[diff.DIFF_NEW],
engine.plural("file", dct[diff.DIFF_NEW]),
dct[diff.DIFF_DEL],
engine.plural("file", dct[diff.DIFF_DEL]),
cls._print_size(dct[diff.DIFF_SIZE]),
)
return changes_msg
@classmethod
def _get_file_changes(cls, dct):
import dvc.repo.diff as diff
if (
dct.get(diff.DIFF_OLD_FILE)
and dct.get(diff.DIFF_NEW_FILE)
and dct[diff.DIFF_SIZE] == 0
):
msg = "file size was not changed"
elif dct.get(diff.DIFF_NEW_FILE):
msg = "added file with size {}".format(
humanize.naturalsize(dct[diff.DIFF_SIZE])
)
elif dct.get(diff.DIFF_OLD_FILE):
msg = "deleted file with size {}".format(
humanize.naturalsize(abs(dct[diff.DIFF_SIZE]))
)
else:
msg = "file was modified, file size {}".format(
cls._print_size(dct[diff.DIFF_SIZE])
)
return msg
@classmethod
def _get_royal_changes(cls, dct):
import dvc.repo.diff as diff
if dct[diff.DIFF_SIZE] != diff.DIFF_SIZE_UNKNOWN:
if dct.get("is_dir"):
return cls._get_dir_changes(dct)
else:
return cls._get_file_changes(dct)
return "size is ?"
@classmethod
def _show(cls, diff_dct):
import dvc.repo.diff as diff
msg = "dvc diff from {} to {}".format(
diff_dct[diff.DIFF_A_REF], diff_dct[diff.DIFF_B_REF]
)
if diff_dct.get(diff.DIFF_EQUAL):
logger.info(msg)
return
for dct in diff_dct[diff.DIFF_LIST]:
msg += "\n\ndiff for '{}'\n".format(dct[diff.DIFF_TARGET])
msg += cls._get_md5_string(
"-",
dct.get(diff.DIFF_OLD_FILE),
dct.get(diff.DIFF_OLD_CHECKSUM),
)
msg += cls._get_md5_string(
"+",
dct.get(diff.DIFF_NEW_FILE),
dct.get(diff.DIFF_NEW_CHECKSUM),
)
msg += "\n"
msg += cls._get_royal_changes(dct)
logger.info(msg)
return msg
return "\n\n".join(groups)
def run(self):
try:
msg = self.repo.diff(
self.args.a_ref, target=self.args.target, b_ref=self.args.b_ref
)
self._show(msg)
diff = self.repo.diff(self.args.a_rev, self.args.b_rev)
if not self.args.show_hash:
for _, entries in diff.items():
for entry in entries:
del entry["hash"]
if self.args.show_json:
logger.info(json.dumps(diff))
elif self.args.show_md:
logger.info(_show_md(diff))
elif diff:
logger.info(self._format(diff))
except DvcException:
msg = "failed to get 'diff {}'"
args = " ".join(
compact([self.args.target, self.args.a_ref, self.args.b_ref])
)
msg = msg.format(args)
logger.exception(msg)
logger.exception("failed to get diff")
return 1
return 0
def add_parser(subparsers, parent_parser):
DIFF_DESCRIPTION = (
"Show diff of a data file or a directory that is under DVC control.\n"
"Some basic statistics summary, how many files were deleted/changed."
"Show added, modified, or deleted data between commits in the DVC"
" repository, or between a commit and the workspace."
)
DIFF_HELP = "Show a diff of a DVC controlled data file or a directory."
diff_parser = subparsers.add_parser(
"diff",
parents=[parent_parser],
description=append_doc_link(DIFF_DESCRIPTION, "diff"),
help=DIFF_HELP,
help=DIFF_DESCRIPTION,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
diff_parser.add_argument(
"-t",
"--target",
help=(
"Source path to a data file or directory. Default None. "
"If not specified, compares all files and directories "
"that are under DVC control in the current working space."
),
"a_rev",
help="Old Git commit to compare (defaults to HEAD)",
nargs="?",
default="HEAD",
)
diff_parser.add_argument(
"a_ref", help="Git reference from which diff calculates"
)
diff_parser.add_argument(
"b_ref",
help=(
"Git reference until which diff calculates, if omitted "
"diff shows the difference between current HEAD and a_ref"
),
"b_rev",
help=("New Git commit to compare (defaults to the current workspace)"),
nargs="?",
)
diff_parser.add_argument(
"--show-json",
help="Format the output into a JSON",
action="store_true",
default=False,
)
diff_parser.add_argument(
"--show-hash",
help="Display hash value for each entry",
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)
+34 -13
View File
@@ -1,19 +1,24 @@
from __future__ import unicode_literals
import argparse
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__)
class CmdGC(CmdBase):
def run(self):
from dvc.repo.gc import _raise_error_if_all_disabled
_raise_error_if_all_disabled(
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
workspace=self.args.workspace,
)
msg = "This will remove all cache except items used in "
msg += "the working tree"
@@ -49,15 +54,16 @@ class CmdGC(CmdBase):
force=self.args.force,
jobs=self.args.jobs,
repos=self.args.repos,
workspace=self.args.workspace,
)
return 0
def add_parser(subparsers, parent_parser):
GC_HELP = "Collect unused data from DVC cache or a remote storage."
GC_HELP = "Garbage collect unused objects from cache or remote storage."
GC_DESCRIPTION = (
"Deletes all files in the cache or a remote which are not in\n"
"use by the specified git references (defaults to just HEAD)."
"Removes all files in the cache or a remote which are not in\n"
"use by the specified Git revisions (defaults to just HEAD)."
)
gc_parser = subparsers.add_parser(
"gc",
@@ -66,25 +72,32 @@ def add_parser(subparsers, parent_parser):
help=GC_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
gc_parser.add_argument(
"-w",
"--workspace",
action="store_true",
default=False,
help="Keep data files used in the current workspace (working tree).",
)
gc_parser.add_argument(
"-a",
"--all-branches",
action="store_true",
default=False,
help="Keep data files for the tips of all git branches.",
help="Keep data files for the tips of all Git branches.",
)
gc_parser.add_argument(
"-T",
"--all-tags",
action="store_true",
default=False,
help="Keep data files for all git tags.",
help="Keep data files for all Git tags.",
)
gc_parser.add_argument(
"--all-commits",
action="store_true",
default=False,
help=argparse.SUPPRESS,
help="Keep data files for all Git commits.",
)
gc_parser.add_argument(
"-c",
@@ -94,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",
@@ -104,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",
@@ -115,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)
+36 -9
View File
@@ -1,18 +1,35 @@
from __future__ import unicode_literals
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__)
class CmdGet(CmdBaseNoRepo):
def _show_url(self):
from dvc.api import get_url
try:
url = get_url(
self.args.path, repo=self.args.url, rev=self.args.rev
)
logger.info(url)
except DvcException:
logger.exception("failed to show URL")
return 1
return 0
def run(self):
if self.args.show_url:
return self._show_url()
return self._get_file_from_repo()
def _get_file_from_repo(self):
from dvc.repo import Repo
try:
@@ -33,7 +50,7 @@ class CmdGet(CmdBaseNoRepo):
def add_parser(subparsers, parent_parser):
GET_HELP = "Download/copy files or directories from DVC repository."
GET_HELP = "Download file or directory tracked by DVC or by Git."
get_parser = subparsers.add_parser(
"get",
parents=[parent_parser],
@@ -42,18 +59,28 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
get_parser.add_argument(
"url", help="URL of Git repository with DVC project to download from."
"url", help="Location of DVC or Git repository to download from"
)
get_parser.add_argument(
"path", help="Path to a file or directory within a DVC repository."
"path", help="Path to a file or directory within the repository"
)
get_parser.add_argument(
"-o",
"--out",
nargs="?",
help="Destination path to copy/download files to.",
help="Destination path to download files to",
metavar="<path>",
)
get_parser.add_argument(
"--rev", nargs="?", help="DVC repository git revision."
"--rev",
nargs="?",
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
get_parser.add_argument(
"--show-url",
action="store_true",
help="Print the storage location (URL) the target data would be "
"downloaded from, and exit.",
)
get_parser.set_defaults(func=CmdGet)
+2 -5
View File
@@ -1,12 +1,9 @@
from __future__ import unicode_literals
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__)
@@ -19,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
+112
View File
@@ -0,0 +1,112 @@
import logging
import os
from dvc.command.base import CmdBaseNoRepo, fix_subparsers
from dvc.exceptions import NotDvcRepoError
logger = logging.getLogger(__name__)
class CmdHookBase(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
try:
repo = Repo()
repo.close()
except NotDvcRepoError:
return 0
return self._run()
class CmdPreCommit(CmdHookBase):
def _run(self):
from dvc.main import main
return main(["status"])
class CmdPostCheckout(CmdHookBase):
def run(self):
# when we are running from pre-commit tool, it doesn't provide CLI
# flags, but instead provides respective env vars that we could use.
flag = os.environ.get("PRE_COMMIT_CHECKOUT_TYPE")
if flag is None and len(self.args.args) >= 3:
# see https://git-scm.com/docs/githooks#_post_checkout
flag = self.args.args[2]
# checking out some reference and not specific file.
if flag != "1":
return 0
# make sure we are not in the middle of a rebase/merge, so we
# don't accidentally break it with an unsuccessful checkout.
# Note that git hooks are always running in repo root.
if os.path.isdir(os.path.join(".git", "rebase-merge")):
return 0
from dvc.main import main
return main(["checkout"])
class CmdPrePush(CmdHookBase):
def run(self):
from dvc.main import main
return main(["push"])
def add_parser(subparsers, parent_parser):
GIT_HOOK_HELP = "Run GIT hook."
git_hook_parser = subparsers.add_parser(
"git-hook",
parents=[parent_parser],
description=GIT_HOOK_HELP,
add_help=False,
)
git_hook_subparsers = git_hook_parser.add_subparsers(
dest="cmd",
help="Use `dvc daemon CMD --help` for command-specific help.",
)
fix_subparsers(git_hook_subparsers)
PRE_COMMIT_HELP = "Run pre-commit GIT hook."
pre_commit_parser = git_hook_subparsers.add_parser(
"pre-commit",
parents=[parent_parser],
description=PRE_COMMIT_HELP,
help=PRE_COMMIT_HELP,
)
pre_commit_parser.add_argument(
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
)
pre_commit_parser.set_defaults(func=CmdPreCommit)
POST_CHECKOUT_HELP = "Run post-checkout GIT hook."
post_checkout_parser = git_hook_subparsers.add_parser(
"post-checkout",
parents=[parent_parser],
description=POST_CHECKOUT_HELP,
help=POST_CHECKOUT_HELP,
)
post_checkout_parser.add_argument(
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
)
post_checkout_parser.set_defaults(func=CmdPostCheckout)
PRE_PUSH_HELP = "Run pre-push GIT hook."
pre_push_parser = git_hook_subparsers.add_parser(
"pre-push",
parents=[parent_parser],
description=PRE_PUSH_HELP,
help=PRE_PUSH_HELP,
)
pre_push_parser.add_argument(
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
)
pre_push_parser.set_defaults(func=CmdPrePush)
+14 -10
View File
@@ -1,13 +1,9 @@
from __future__ import unicode_literals
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__)
@@ -32,7 +28,8 @@ class CmdImport(CmdBase):
def add_parser(subparsers, parent_parser):
IMPORT_HELP = (
"Download data from DVC repository and take it under DVC control."
"Download file or directory tracked by DVC or by Git "
"into the workspace, and track it."
)
import_parser = subparsers.add_parser(
@@ -43,15 +40,22 @@ def add_parser(subparsers, parent_parser):
formatter_class=argparse.RawTextHelpFormatter,
)
import_parser.add_argument(
"url", help="URL of Git repository with DVC project to download from."
"url", help="Location of DVC or Git repository to download from"
)
import_parser.add_argument(
"path", help="Path to data within DVC repository."
"path", help="Path to a file or directory within the repository"
)
import_parser.add_argument(
"-o", "--out", nargs="?", help="Destination path to put data to."
"-o",
"--out",
nargs="?",
help="Destination path to download files to",
metavar="<path>",
)
import_parser.add_argument(
"--rev", nargs="?", help="DVC repository git revision."
"--rev",
nargs="?",
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
import_parser.set_defaults(func=CmdImport)
+5 -6
View File
@@ -1,13 +1,9 @@
from __future__ import unicode_literals
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__)
@@ -61,6 +57,9 @@ 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."
"-f",
"--file",
help="Specify name of the DVC-file this command will generate.",
metavar="<filename>",
)
import_parser.set_defaults(func=CmdImportUrl)
+17 -9
View File
@@ -1,11 +1,7 @@
from __future__ import unicode_literals
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__)
@@ -17,11 +13,14 @@ class CmdInit(CmdBaseNoRepo):
try:
self.repo = Repo.init(
".", no_scm=self.args.no_scm, force=self.args.force
".",
no_scm=self.args.no_scm,
force=self.args.force,
subdir=self.args.subdir,
)
self.config = self.repo.config
except InitError:
logger.exception("failed to initiate dvc")
logger.exception("failed to initiate DVC")
return 1
return 0
@@ -45,8 +44,8 @@ def add_parser(subparsers, parent_parser):
"--no-scm",
action="store_true",
default=False,
help="Initiate dvc in directory that is "
"not tracked by any scm tool (e.g. git).",
help="Initiate DVC in directory that is "
"not tracked by any SCM tool (e.g. Git).",
)
init_parser.add_argument(
"-f",
@@ -58,4 +57,13 @@ def add_parser(subparsers, parent_parser):
"This operation removes local cache."
),
)
init_parser.add_argument(
"--subdir",
action="store_true",
default=False,
help=(
"Necessary for running this command inside a subdirectory of a "
"parent SCM repository."
),
)
init_parser.set_defaults(func=CmdInit)
+10 -7
View File
@@ -1,11 +1,7 @@
from __future__ import unicode_literals
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__)
@@ -13,9 +9,9 @@ logger = logging.getLogger(__name__)
class CmdInstall(CmdBase):
def run(self):
try:
self.repo.install()
self.repo.install(self.args.use_pre_commit_tool)
except Exception:
logger.exception("failed to install dvc hooks")
logger.exception("failed to install DVC Git hooks")
return 1
return 0
@@ -29,4 +25,11 @@ def add_parser(subparsers, parent_parser):
help=INSTALL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
install_parser.add_argument(
"--use-pre-commit-tool",
action="store_true",
default=False,
help="Install DVC hooks using pre-commit "
"(https://pre-commit.com) if it is installed.",
)
install_parser.set_defaults(func=CmdInstall)
+1 -5
View File
@@ -1,13 +1,9 @@
from __future__ import unicode_literals
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__)
+78
View File
@@ -0,0 +1,78 @@
import argparse
import logging
import sys
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__)
def _prettify(entries, with_color=False):
if with_color:
ls_colors = LsColors()
fmt = ls_colors.format
else:
def fmt(entry):
return entry["path"]
return [fmt(entry) for entry in entries]
class CmdList(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
try:
entries = Repo.ls(
self.args.url,
self.args.path,
rev=self.args.rev,
recursive=self.args.recursive,
outs_only=self.args.outs_only,
)
if entries:
entries = _prettify(entries, sys.stdout.isatty())
logger.info("\n".join(entries))
return 0
except DvcException:
logger.exception(f"failed to list '{self.args.url}'")
return 1
def add_parser(subparsers, parent_parser):
LIST_HELP = (
"List repository contents, including files"
" and directories tracked by DVC and by Git."
)
list_parser = subparsers.add_parser(
"list",
parents=[parent_parser],
description=append_doc_link(LIST_HELP, "list"),
help=LIST_HELP,
formatter_class=argparse.RawTextHelpFormatter,
)
list_parser.add_argument("url", help="Location of DVC repository to list")
list_parser.add_argument(
"-R",
"--recursive",
action="store_true",
help="Recursively list files.",
)
list_parser.add_argument(
"--outs-only", action="store_true", help="Show only DVC outputs."
)
list_parser.add_argument(
"--rev",
nargs="?",
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
list_parser.add_argument(
"path",
nargs="?",
help="Path to directory within the repository to list outputs for",
)
list_parser.set_defaults(func=CmdList)
+48
View File
@@ -0,0 +1,48 @@
import os
class LsColors:
default = "rs=0:di=01;34:ex=01;32"
def __init__(self, lscolors=None):
self._extensions = {}
self._codes = {}
self._load(lscolors or os.environ.get("LS_COLORS") or LsColors.default)
def _load(self, lscolors):
for item in lscolors.split(":"):
try:
code, color = item.split("=", 1)
except ValueError:
continue
if code.startswith("*."):
self._extensions[code[1:]] = color
else:
self._codes[code] = color
def format(self, entry):
text = entry["path"]
if entry.get("isout", False) and "out" in self._codes:
return self._format(text, code="out")
if entry.get("isdir", False):
return self._format(text, code="di")
if entry.get("isexec", False):
return self._format(text, code="ex")
_, ext = os.path.splitext(text)
return self._format(text, ext=ext)
def _format(self, text, code=None, ext=None):
val = None
if ext:
val = self._extensions.get(ext, None)
if code:
val = self._codes.get(code, None)
if not val:
return text
rs = self._codes.get("rs", 0)
return f"\033[{val}m{text}\033[{rs}m"
+157 -92
View File
@@ -1,43 +1,34 @@
from __future__ import unicode_literals
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`
"""
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():
lines = metric if type(metric) is list else 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)
@@ -48,14 +39,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
@@ -63,27 +63,12 @@ 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
return 0
class CmdMetricsAdd(CmdBase):
def run(self):
try:
self.repo.metrics.add(
self.args.path, self.args.type, self.args.xpath
)
self.repo.metrics.add(self.args.path)
except DvcException:
msg = "failed to add metric file '{}'".format(self.args.path)
msg = f"failed to add metric file '{self.args.path}'"
logger.exception(msg)
return 1
@@ -95,15 +80,63 @@ class CmdMetricsRemove(CmdBase):
try:
self.repo.metrics.remove(self.args.path)
except DvcException:
msg = "failed to remove metric file '{}'".format(self.args.path)
msg = f"failed to remove metric file '{self.args.path}'"
logger.exception(msg)
return 1
return 0
def _show_diff(diff, markdown=False):
from collections import OrderedDict
from dvc.utils.diff import table
rows = []
for fname, mdiff in diff.items():
sorted_mdiff = OrderedDict(sorted(mdiff.items()))
for metric, change in sorted_mdiff.items():
rows.append(
[
fname,
metric,
change["new"],
change.get("diff", "diff not supported"),
]
)
return table(["Path", "Metric", "Value", "Change"], rows, markdown)
class CmdMetricsDiff(CmdBase):
def run(self):
try:
diff = self.repo.metrics.diff(
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
targets=self.args.targets,
recursive=self.args.recursive,
all=self.args.all,
)
if self.args.show_json:
import json
logger.info(json.dumps(diff))
else:
table = _show_diff(diff, self.args.show_md)
if table:
logger.info(table)
except DvcException:
logger.exception("failed to show metrics diff")
return 1
return 0
def add_parser(subparsers, parent_parser):
METRICS_HELP = "Commands to add, manage, collect and display metrics."
METRICS_HELP = "Commands to add, manage, collect, and display metrics."
metrics_parser = subparsers.add_parser(
"metrics",
@@ -115,12 +148,23 @@ def add_parser(subparsers, parent_parser):
metrics_subparsers = metrics_parser.add_subparsers(
dest="cmd",
help="Use dvc metrics CMD --help to display command-specific help.",
help="Use `dvc metrics CMD --help` to display command-specific help.",
)
fix_subparsers(metrics_subparsers)
METRICS_SHOW_HELP = "Output metric values."
METRICS_ADD_HELP = "Mark a DVC-tracked file as a metric."
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_add_parser.add_argument("path", help="Path to a metric file.")
metrics_add_parser.set_defaults(func=CmdMetricsAdd)
METRICS_SHOW_HELP = "Print metrics, with optional formatting."
metrics_show_parser = metrics_subparsers.add_parser(
"show",
parents=[parent_parser],
@@ -131,20 +175,7 @@ def add_parser(subparsers, parent_parser):
metrics_show_parser.add_argument(
"targets",
nargs="*",
help="Metric files or directories (see -R) to show "
"(leave empty to display all)",
)
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."
),
)
metrics_show_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
help="Metric files or directories (see -R) to show",
)
metrics_show_parser.add_argument(
"-a",
@@ -160,6 +191,12 @@ 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(
"-R",
"--recursive",
@@ -170,43 +207,71 @@ def add_parser(subparsers, parent_parser):
"metric files."
),
)
metrics_show_parser.add_argument(
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
metrics_show_parser.set_defaults(func=CmdMetricsShow)
METRICS_ADD_HELP = "Tag file as a metric file."
metrics_add_parser = metrics_subparsers.add_parser(
"add",
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],
description=append_doc_link(METRICS_ADD_HELP, "metrics/add"),
help=METRICS_ADD_HELP,
description=append_doc_link(METRICS_DIFF_HELP, "metrics/diff"),
help=METRICS_DIFF_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_add_parser.add_argument(
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
metrics_diff_parser.add_argument(
"a_rev", nargs="?", help="Old Git commit to compare (defaults to HEAD)"
)
metrics_add_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
metrics_diff_parser.add_argument(
"b_rev",
nargs="?",
help=("New Git commit to compare (defaults to the current workspace)"),
)
metrics_add_parser.add_argument("path", help="Path to a metric file.")
metrics_add_parser.set_defaults(func=CmdMetricsAdd)
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."
),
metavar="<paths>",
)
metrics_diff_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help=(
"If any target is a directory, recursively search and process "
"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.set_defaults(func=CmdMetricsDiff)
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_HELP = "Remove metric mark on a DVC-tracked file."
metrics_remove_parser = metrics_subparsers.add_parser(
"remove",
parents=[parent_parser],
+1 -5
View File
@@ -1,13 +1,9 @@
from __future__ import unicode_literals
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__)
+103
View File
@@ -0,0 +1,103 @@
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):
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():
rows.append([fname, param, change["old"], change["new"]])
return table(["Path", "Param", "Old", "New"], 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)
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.set_defaults(func=CmdParamsDiff)
+89 -83
View File
@@ -1,15 +1,8 @@
from __future__ import unicode_literals
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.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
from dvc.utils import relpath
from dvc.utils.compat import str
logger = logging.getLogger(__name__)
@@ -17,73 +10,89 @@ logger = logging.getLogger(__name__)
class CmdPipelineShow(CmdBase):
def _show(self, target, commands, outs, locked):
import networkx
from dvc.stage import Stage
from dvc import dvcfile
from dvc.utils import parse_target
stage = Stage.load(self.repo, target)
path, name = parse_target(target)
stage = dvcfile.Dvcfile(self.repo, path).stages[name]
G = self.repo.graph
stages = networkx.get_node_attributes(G, "stage")
node = relpath(stage.path, self.repo.root_dir)
nodes = networkx.dfs_postorder_nodes(G, node)
stages = networkx.dfs_postorder_nodes(G, stage)
if locked:
nodes = [n for n in nodes if stages[n].locked]
stages = [s for s in stages if s.locked]
for n in nodes:
if commands:
if stages[n].cmd is None:
continue
logger.info(stages[n].cmd)
elif outs:
for out in stages[n].outs:
logger.info(str(out))
else:
logger.info(n)
def __build_graph(self, target, commands, outs):
import networkx
from dvc.stage import Stage
from dvc.repo.graph import get_pipeline
stage = Stage.load(self.repo, target)
node = relpath(stage.path, self.repo.root_dir)
G = get_pipeline(self.repo.pipelines, node)
stages = networkx.get_node_attributes(G, "stage")
nodes = []
for n in G:
stage = stages[n]
for stage in stages:
if commands:
if stage.cmd is None:
continue
nodes.append(stage.cmd)
logger.info(stage.cmd)
elif outs:
for out in stage.outs:
nodes.append(str(out))
logger.info(str(out))
else:
nodes.append(stage.relpath)
logger.info(stage.addressing)
@staticmethod
def _build_output_graph(G, target_stage):
import networkx
from itertools import product
nodes = {str(out) for out in target_stage.outs}
edges = []
for from_stage, to_stage in networkx.edge_dfs(G, target_stage):
from_stage_deps = {dep.path_info.parts for dep in from_stage.deps}
to_outs = {
to_out
for to_out in to_stage.outs
if to_out.path_info.parts in from_stage_deps
}
from_outs = {
from_out
for from_out in from_stage.outs
if str(from_out) in nodes
}
nodes |= {str(to_out) for to_out in to_outs}
for from_out, to_out in product(from_outs, to_outs):
edges.append((str(from_out), str(to_out)))
return nodes, edges
def _build_graph(self, target, commands=False, outs=False):
import networkx
from dvc import dvcfile
from dvc.repo.graph import get_pipeline
from dvc.utils import parse_target
path, name = parse_target(target)
target_stage = dvcfile.Dvcfile(self.repo, path).stages[name]
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 not outs:
nodes.add(stage.addressing)
edges = []
for e in G.edges():
from_stage = stages[e[0]]
to_stage = stages[e[1]]
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))
elif not outs:
edges.append((from_stage.addressing, to_stage.addressing))
return nodes, edges, networkx.is_tree(G)
if outs:
nodes, edges = self._build_output_graph(G, target_stage)
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)
nodes, edges, _ = self._build_graph(target, commands, outs)
if not nodes:
return
@@ -93,7 +102,7 @@ class CmdPipelineShow(CmdBase):
def _show_dependencies_tree(self, target, commands, outs):
from treelib import Tree
nodes, edges, is_tree = self.__build_graph(target, commands, outs)
nodes, edges, is_tree = self._build_graph(target, commands, outs)
if not nodes:
return
if not is_tree:
@@ -114,24 +123,26 @@ class CmdPipelineShow(CmdBase):
observe_list.pop(0)
tree.show()
def __write_dot(self, target, commands, outs):
from dvc.utils.compat import StringIO
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, _ = 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 = StringIO()
dot_file = io.StringIO()
write_dot(simple_g, dot_file)
logger.info(dot_file.getvalue())
def run(self):
from dvc.dvcfile import DVC_FILE
if not self.args.targets:
self.args.targets = self.default_targets
self.args.targets = [DVC_FILE]
for target in self.args.targets:
try:
@@ -140,9 +151,7 @@ class CmdPipelineShow(CmdBase):
target, self.args.commands, self.args.outs
)
elif self.args.dot:
self.__write_dot(
target, self.args.commands, self.args.outs
)
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
@@ -155,7 +164,7 @@ class CmdPipelineShow(CmdBase):
self.args.locked,
)
except DvcException:
msg = "failed to show pipeline for '{}'".format(target)
msg = f"failed to show pipeline for '{target}'"
logger.exception(msg)
return 1
return 0
@@ -163,14 +172,11 @@ class CmdPipelineShow(CmdBase):
class CmdPipelineList(CmdBase):
def run(self):
import networkx
pipelines = self.repo.pipelines
for p in pipelines:
stages = networkx.get_node_attributes(p, "stage")
for stage in stages:
logger.info(stage)
if len(stages) != 0:
for pipeline in pipelines:
for stage in pipeline:
logger.info(stage.addressing)
if len(pipeline) != 0:
logger.info("=" * 80)
logger.info("{} pipelines total".format(len(pipelines)))
@@ -189,12 +195,22 @@ def add_parser(subparsers, parent_parser):
pipeline_subparsers = pipeline_parser.add_subparsers(
dest="cmd",
help="Use dvc pipeline CMD --help for command-specific help.",
help="Use `dvc pipeline CMD --help` for command-specific help.",
)
fix_subparsers(pipeline_subparsers)
PIPELINE_SHOW_HELP = "Show pipelines."
PIPELINE_LIST_HELP = "List connected groups of stages (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)
PIPELINE_SHOW_HELP = "Show stages in a pipeline."
pipeline_show_parser = pipeline_subparsers.add_parser(
"show",
parents=[parent_parser],
@@ -249,13 +265,3 @@ def add_parser(subparsers, parent_parser):
"(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)
+229
View File
@@ -0,0 +1,229 @@
import argparse
import logging
import os
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
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 run(self):
fields = None
jsonpath = None
if self.args.select:
if self.args.select.startswith("$"):
jsonpath = self.args.select
else:
fields = set(self.args.select.split(","))
try:
plots = self._func(
targets=self.args.targets,
template=self.args.template,
fields=fields,
x_field=self.args.x,
y_field=self.args.y,
path=jsonpath,
csv_header=not self.args.no_csv_header,
title=self.args.title,
x_title=self.args.xlab,
y_title=self.args.ylab,
)
if self.args.show_json:
import json
logger.info(json.dumps(plots))
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)
def add_parser(subparsers, parent_parser):
PLOTS_HELP = (
"Generating plots for metrics stored in structured files "
"(JSON, 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 a plots image file from a metrics file."
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(
"-t",
"--template",
nargs="?",
default=None,
help="File to be injected with data.",
)
plots_show_parser.add_argument(
"-o", "--out", default=None, help="Destination path to save plots to.",
)
plots_show_parser.add_argument(
"-s",
"--select",
default=None,
help="Choose which field(s) or JSONPath to include in the plots.",
)
plots_show_parser.add_argument(
"-x", default=None, help="Field name for x axis."
)
plots_show_parser.add_argument(
"-y", default=None, help="Field name for y axis."
)
plots_show_parser.add_argument(
"--no-csv-header",
action="store_true",
default=False,
help="Required when CSV or TSV datafile does not have a header.",
)
plots_show_parser.add_argument(
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
plots_show_parser.add_argument("--title", default=None, help="Plot title.")
plots_show_parser.add_argument(
"--xlab", default=None, help="X axis title."
)
plots_show_parser.add_argument(
"--ylab", default=None, help="Y axis title."
)
plots_show_parser.add_argument(
"targets",
nargs="*",
help="Metrics files to visualize. Shows all plots by default.",
)
plots_show_parser.set_defaults(func=CmdPlotsShow)
PLOTS_DIFF_HELP = (
"Plot differences in metrics between commits in the DVC "
"repository, or between the last commit and the workspace."
)
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(
"-t",
"--template",
nargs="?",
default=None,
help="File to be injected with data.",
)
plots_diff_parser.add_argument(
"--targets",
nargs="*",
help="Metrics file to visualize. Shows all plots by default.",
)
plots_diff_parser.add_argument(
"-o", "--out", default=None, help="Destination path to save plots to.",
)
plots_diff_parser.add_argument(
"-s",
"--select",
default=None,
help="Choose which field(s) or JSONPath to include in the plot.",
)
plots_diff_parser.add_argument(
"-x", default=None, help="Field name for x axis."
)
plots_diff_parser.add_argument(
"-y", default=None, help="Field name for y axis."
)
plots_diff_parser.add_argument(
"--no-csv-header",
action="store_true",
default=False,
help="Provided CSV ot TSV datafile does not have a header.",
)
plots_diff_parser.add_argument(
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
plots_diff_parser.add_argument("--title", default=None, help="Plot title.")
plots_diff_parser.add_argument(
"--xlab", default=None, help="X axis title."
)
plots_diff_parser.add_argument(
"--ylab", default=None, help="Y axis title."
)
plots_diff_parser.add_argument(
"revisions", nargs="*", default=None, help="Git commits to plot from",
)
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
+134 -59
View File
@@ -1,80 +1,142 @@
from __future__ import unicode_literals
import argparse
import logging
from dvc.command.base import append_doc_link
from dvc.command.base import fix_subparsers
from dvc.command.base import append_doc_link, fix_subparsers
from dvc.command.config import CmdConfig
from dvc.remote.config import RemoteConfig
from dvc.config import ConfigError
from dvc.utils import format_link
logger = logging.getLogger(__name__)
class CmdRemoteConfig(CmdConfig):
class CmdRemote(CmdConfig):
def __init__(self, args):
super(CmdRemoteConfig, self).__init__(args)
self.remote_config = RemoteConfig(self.config)
super().__init__(args)
if getattr(self.args, "name", None):
self.args.name = self.args.name.lower()
def _check_exists(self, conf):
if self.args.name not in conf["remote"]:
raise ConfigError(f"remote '{self.args.name}' doesn't exists.")
class CmdRemoteAdd(CmdRemoteConfig):
class CmdRemoteAdd(CmdRemote):
def run(self):
if self.args.default:
logger.info(
"Setting '{}' as a default remote.".format(self.args.name)
)
self.remote_config.add(
self.args.name,
self.args.url,
force=self.args.force,
default=self.args.default,
level=self.args.level,
)
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:
raise ConfigError(
"remote '{}' already exists. Use `-f|--force` to "
"overwrite it.".format(self.args.name)
)
conf["remote"][self.args.name] = {"url": self.args.url}
if self.args.default:
conf["core"]["remote"] = self.args.name
return 0
class CmdRemoteRemove(CmdRemoteConfig):
class CmdRemoteRemove(CmdRemote):
def run(self):
self.remote_config.remove(self.args.name, level=self.args.level)
with self.config.edit(self.args.level) as conf:
self._check_exists(conf)
del conf["remote"][self.args.name]
# Remove core.remote refs to this remote in any shadowing configs
for level in reversed(self.config.LEVELS):
with self.config.edit(level) as conf:
if conf["core"].get("remote") == self.args.name:
del conf["core"]["remote"]
if level == self.args.level:
break
return 0
class CmdRemoteModify(CmdRemoteConfig):
class CmdRemoteModify(CmdRemote):
def run(self):
self.remote_config.modify(
self.args.name,
self.args.option,
self.args.value,
level=self.args.level,
)
with self.config.edit(self.args.level) as conf:
self._check_exists(conf)
conf["remote"][self.args.name][self.args.option] = self.args.value
return 0
class CmdRemoteDefault(CmdRemoteConfig):
class CmdRemoteDefault(CmdRemote):
def run(self):
if self.args.name is None and not self.args.unset:
name = self.remote_config.get_default(level=self.args.level)
print(name)
conf = self.config.load_one(self.args.level)
try:
print(conf["core"]["remote"])
except KeyError:
logger.info("No default remote set")
return 1
else:
self.remote_config.set_default(
self.args.name, unset=self.args.unset, level=self.args.level
)
with self.config.edit(self.args.level) as conf:
if self.args.unset:
conf["core"].pop("remote", None)
else:
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
class CmdRemoteList(CmdRemoteConfig):
class CmdRemoteList(CmdRemote):
def run(self):
for name, url in self.remote_config.list(
level=self.args.level
).items():
logger.info("{}\t{}".format(name, url))
conf = self.config.load_one(self.args.level)
for name, conf in conf["remote"].items():
logger.info("{}\t{}".format(name, conf["url"]))
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],
@@ -85,12 +147,12 @@ def add_parser(subparsers, parent_parser):
remote_subparsers = remote_parser.add_subparsers(
dest="cmd",
help="Use dvc remote CMD --help for " "command-specific help.",
help="Use `dvc remote CMD --help` for " "command-specific help.",
)
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],
@@ -101,7 +163,9 @@ def add_parser(subparsers, parent_parser):
remote_add_parser.add_argument("name", help="Name of the remote")
remote_add_parser.add_argument(
"url",
help="URL. See full list of supported urls at " "man.dvc.org/remote",
help="URL. See full list of supported urls at {}".format(
format_link("https://man.dvc.org/remote")
),
)
remote_add_parser.add_argument(
"-d",
@@ -119,7 +183,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],
@@ -139,20 +203,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],
@@ -176,7 +227,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],
@@ -185,3 +236,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)
+3 -7
View File
@@ -1,14 +1,10 @@
from __future__ import unicode_literals
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__)
@@ -38,13 +34,13 @@ class CmdRemove(CmdBase):
outs_only = self._is_outs_only(target)
self.repo.remove(target, outs_only=outs_only)
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 DVC-tracked files or directories."
remove_parser = subparsers.add_parser(
"remove",
parents=[parent_parser],
+17 -8
View File
@@ -1,16 +1,12 @@
from __future__ import unicode_literals
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.exceptions import DvcException
logger = logging.getLogger(__name__)
@@ -37,10 +33,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:
@@ -59,7 +56,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],
@@ -92,6 +91,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",
@@ -137,7 +137,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 -5
View File
@@ -1,10 +1,7 @@
from __future__ import unicode_literals
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__)
@@ -19,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],
+61 -33
View File
@@ -1,20 +1,14 @@
from __future__ import unicode_literals
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,
@@ -22,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
@@ -41,21 +38,24 @@ 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,
overwrite=self.args.overwrite_dvcfile,
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,
)
except DvcException:
logger.exception("failed to run command")
logger.exception("")
return 1
return 0
@@ -79,7 +79,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):
@@ -97,6 +97,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",
@@ -104,6 +108,7 @@ def add_parser(subparsers, parent_parser):
action="append",
default=[],
help="Declare output file or directory.",
metavar="<filename>",
)
run_parser.add_argument(
"-O",
@@ -112,6 +117,15 @@ 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",
@@ -119,6 +133,7 @@ def add_parser(subparsers, parent_parser):
action="append",
default=[],
help="Declare output metric file or directory.",
metavar="<path>",
)
run_parser.add_argument(
"-M",
@@ -127,17 +142,33 @@ def add_parser(subparsers, parent_parser):
default=[],
help="Declare output metric file or directory "
"(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(
"-f",
"--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",
@@ -145,13 +176,6 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Only create stage file without actually running it.",
)
run_parser.add_argument(
"-y",
"--yes",
action="store_true",
default=False,
help="Deprecated, use --overwrite-dvcfile instead",
)
run_parser.add_argument(
"--overwrite-dvcfile",
action="store_true",
@@ -159,17 +183,13 @@ def add_parser(subparsers, parent_parser):
help="Overwrite existing DVC-file without asking for confirmation.",
)
run_parser.add_argument(
"--ignore-build-cache",
"--no-run-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",
@@ -183,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",
@@ -190,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",
@@ -197,6 +219,12 @@ 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(
"command", nargs=argparse.REMAINDER, help="Command to execute."
)
+4 -6
View File
@@ -1,10 +1,6 @@
from __future__ import unicode_literals
import logging
from dvc.command.data_sync import CmdDataBase
from dvc.utils.compat import str
logger = logging.getLogger(__name__)
@@ -23,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):
@@ -37,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):
@@ -50,7 +46,9 @@ 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:
-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)
+6 -7
View File
@@ -1,13 +1,9 @@
from __future__ import unicode_literals
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__)
@@ -17,14 +13,17 @@ 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
def add_parser(subparsers, parent_parser):
UNPROTECT_HELP = "Unprotect data files or directories."
UNPROTECT_HELP = (
"Unprotect tracked files or directories (when hardlinks or symlinks "
"have been enabled with `dvc config cache.type`)"
)
unprotect_parser = subparsers.add_parser(
"unprotect",
parents=[parent_parser],
+23 -11
View File
@@ -1,25 +1,24 @@
from __future__ import unicode_literals
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)
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
@@ -35,4 +34,17 @@ def add_parser(subparsers, parent_parser):
update_parser.add_argument(
"targets", nargs="+", help="DVC-files to update."
)
update_parser.add_argument(
"--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)
+65 -49
View File
@@ -1,24 +1,24 @@
from __future__ import unicode_literals
import argparse
import itertools
import logging
import os
import pathlib
import platform
import uuid
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.utils.compat import pathlib
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__)
@@ -27,23 +27,14 @@ class CmdVersion(CmdBaseNoRepo):
def run(self):
from dvc.repo import Repo
dvc_version = __version__
python_version = platform.python_version()
platform_type = platform.platform()
binary = is_binary()
info = (
"DVC version: {dvc_version}\n"
"Python version: {python_version}\n"
"Platform: {platform_type}\n"
"Binary: {binary}\n"
"Package: {package}\n"
).format(
dvc_version=dvc_version,
python_version=python_version,
platform_type=platform_type,
binary=binary,
package=PKG,
)
info = [
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:
repo = Repo()
@@ -54,9 +45,14 @@ class CmdVersion(CmdBaseNoRepo):
# later decides to enable shared cache mode with
# `dvc config cache.shared group`.
if os.path.exists(repo.cache.local.cache_dir):
info += "Cache: {cache}\n".format(
cache=self.get_linktype_support_info(repo)
info.append(
"Cache: {}".format(self.get_linktype_support_info(repo))
)
if psutil:
fs_type = self.get_fs_type(repo.cache.local.cache_dir)
info.append(
f"Filesystem type (cache directory): {fs_type}"
)
else:
logger.warning(
"Unable to detect supported link types, as cache "
@@ -66,19 +62,19 @@ class CmdVersion(CmdBaseNoRepo):
"check.".format(relpath(repo.cache.local.cache_dir))
)
if psutil:
info += (
"Filesystem type (cache directory): {fs_cache}\n"
).format(fs_cache=self.get_fs_type(repo.cache.local.cache_dir))
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:
info += ("Filesystem type (workspace): {fs_root}").format(
fs_root=self.get_fs_type(os.path.abspath(root_directory))
)
fs_root = self.get_fs_type(os.path.abspath(root_directory))
info.append(f"Filesystem type (workspace): {fs_root}")
logger.info(info)
logger.info("\n".join(info))
return 0
@staticmethod
@@ -98,9 +94,9 @@ class CmdVersion(CmdBaseNoRepo):
@staticmethod
def get_linktype_support_info(repo):
links = {
"reflink": System.reflink,
"hardlink": System.hardlink,
"symlink": System.symlink,
"reflink": (System.reflink, None),
"hardlink": (System.hardlink, System.is_hardlink),
"symlink": (System.symlink, System.is_symlink),
}
fname = "." + str(uuid.uuid4())
@@ -110,26 +106,46 @@ class CmdVersion(CmdBaseNoRepo):
cache = []
for name, link in links.items():
for name, (link, is_link) in links.items():
try:
link(src, dst)
status = "supported"
if is_link and not is_link(dst):
status = "broken"
os.unlink(dst)
supported = True
except DvcException:
supported = False
cache.append(
"{name} - {supported}".format(
name=name, supported=True if supported else False
)
)
status = "not supported"
cache.append(f"{name} - {status}")
os.remove(src)
return ", ".join(cache)
@staticmethod
def get_supported_remotes():
from dvc.remote import REMOTES
supported_remotes = []
for remote in REMOTES:
if not remote.get_missing_deps():
supported_remotes.append(remote.scheme)
return ", ".join(supported_remotes)
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 = "Show DVC version and system/environment information."
VERSION_HELP = (
"Display the DVC version and system/environment information."
)
version_parser = subparsers.add_parser(
"version",
parents=[parent_parser],
+300 -416
View File
@@ -1,47 +1,41 @@
"""DVC config objects."""
from __future__ import unicode_literals
import copy
import errno
import logging
import os
import re
from contextlib import contextmanager
from functools import partial
from urllib.parse import urlparse
import configobj
from voluptuous import Schema, Required, Optional, Invalid
from voluptuous import All, Any, Lower, Range, Coerce, Match
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
from dvc.exceptions import NotDvcRepoError
from dvc.utils.compat import open, str
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc.path_info import PathInfo
from dvc.utils import relpath
logger = logging.getLogger(__name__)
class ConfigError(DvcException):
"""DVC config exception.
"""DVC config exception."""
Args:
msg (str): error message.
cause (Exception): optional exception that has caused this error.
"""
def __init__(self, msg, cause=None):
super(ConfigError, self).__init__(
"config file error: {}".format(msg), cause=cause
)
def __init__(self, msg):
super().__init__(f"config file error: {msg}")
class NoRemoteError(ConfigError):
def __init__(self, command, cause=None):
msg = (
"no remote specified. Setup default remote with\n"
" dvc config core.remote <name>\n"
"or use:\n"
" dvc {} -r <name>\n".format(command)
)
super(NoRemoteError, self).__init__(msg, cause=cause)
pass
def supported_cache_type(types):
@@ -65,13 +59,12 @@ def supported_cache_type(types):
# Checks that value is either true or false and converts it to bool
Bool = All(
to_bool = Bool = All(
Lower,
Any("true", "false"),
lambda v: v == "true",
msg="expected true or false",
)
to_bool = Schema(Bool)
def Choices(*choices):
@@ -84,8 +77,139 @@ def Choices(*choices):
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))
class Config(object): # pylint: disable=too-many-instance-attributes
"""Class that manages configuration files for a dvc repo.
def ByUrl(mapping):
schemas = walk_values(Schema, mapping)
def validate(data):
if "url" not in data:
raise Invalid("expected 'url'")
parsed = urlparse(data["url"])
# Windows absolute paths should really have scheme == "" (local)
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.netloc == "":
return schemas[""](data)
if parsed.scheme not in schemas:
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
return schemas[parsed.scheme](data)
return validate
class RelPath(str):
pass
REMOTE_COMMON = {
"url": str,
"checksum_jobs": All(Coerce(int), Range(1)),
Optional("no_traverse"): Bool, # obsoleted
"verify": Bool,
}
LOCAL_COMMON = {
"type": supported_cache_type,
Optional("protected", default=False): Bool, # obsoleted
"shared": All(Lower, Choices("group")),
Optional("slow_link_warning", default=True): Bool,
}
HTTP_COMMON = {
"auth": All(Lower, Choices("basic", "digest", "custom")),
"custom_auth_header": str,
"user": str,
"password": str,
"ask_password": Bool,
}
SCHEMA = {
"core": {
"remote": Lower,
"checksum_jobs": All(Coerce(int), Range(1)),
Optional("interactive", default=False): Bool,
Optional("analytics", default=True): Bool,
Optional("hardlink_lock", default=False): Bool,
Optional("no_scm", default=False): Bool,
},
"cache": {
"local": str,
"s3": str,
"gs": str,
"hdfs": str,
"ssh": str,
"azure": str,
# This is for default local cache
"dir": str,
**LOCAL_COMMON,
},
"remote": {
str: ByUrl(
{
"": {**LOCAL_COMMON, **REMOTE_COMMON},
"s3": {
"region": str,
"profile": str,
"credentialpath": str,
"endpointurl": str,
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,
"grant_write_acp": str,
"grant_full_control": str,
**REMOTE_COMMON,
},
"gs": {
"projectname": str,
"credentialpath": str,
**REMOTE_COMMON,
},
"ssh": {
"type": supported_cache_type,
"port": Coerce(int),
"user": str,
"password": str,
"ask_password": Bool,
"keyfile": str,
"timeout": Coerce(int),
"gss_auth": Bool,
**REMOTE_COMMON,
},
"hdfs": {"user": str, **REMOTE_COMMON},
"azure": {"connection_string": str, **REMOTE_COMMON},
"oss": {
"oss_key_id": str,
"oss_key_secret": str,
"oss_endpoint": str,
**REMOTE_COMMON,
},
"gdrive": {
"gdrive_use_service_account": Bool,
"gdrive_client_id": str,
"gdrive_client_secret": str,
"gdrive_user_credentials_file": str,
"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},
"https": {**HTTP_COMMON, **REMOTE_COMMON},
"remote": {str: object}, # Any of the above options are valid
}
)
},
"state": {
"row_limit": All(Coerce(int), Range(1)),
"row_cleanup_quota": All(Coerce(int), Range(0, 100)),
},
}
COMPILED_SCHEMA = Schema(SCHEMA)
class Config(dict):
"""Class that manages configuration files for a DVC repo.
Args:
dvc_dir (str): optional path to `.dvc` directory, that is used to
@@ -94,172 +218,21 @@ class Config(object): # pylint: disable=too-many-instance-attributes
validate (bool): optional flag to tell dvc if it should validate the
config or just load it as is. 'True' by default.
Raises:
ConfigError: thrown when config has an invalid format.
ConfigError: thrown if config has an invalid format.
"""
APPNAME = "dvc"
APPAUTHOR = "iterative"
# NOTE: used internally in RemoteLOCAL to know config
# location, that url should resolved relative to.
PRIVATE_CWD = "_cwd"
# In the order they shadow each other
LEVELS = ("system", "global", "repo", "local")
CONFIG = "config"
CONFIG_LOCAL = "config.local"
CREDENTIALPATH = "credentialpath"
LEVEL_LOCAL = 0
LEVEL_REPO = 1
LEVEL_GLOBAL = 2
LEVEL_SYSTEM = 3
SECTION_CORE = "core"
SECTION_CORE_LOGLEVEL = "loglevel"
SECTION_CORE_LOGLEVEL_SCHEMA = All(
Lower, Choices("info", "debug", "warning", "error")
)
SECTION_CORE_REMOTE = "remote"
SECTION_CORE_INTERACTIVE = "interactive"
SECTION_CORE_ANALYTICS = "analytics"
SECTION_CORE_CHECKSUM_JOBS = "checksum_jobs"
SECTION_CORE_HARDLINK_LOCK = "hardlink_lock"
SECTION_CACHE = "cache"
SECTION_CACHE_DIR = "dir"
SECTION_CACHE_TYPE = "type"
SECTION_CACHE_PROTECTED = "protected"
SECTION_CACHE_SHARED = "shared"
SECTION_CACHE_SHARED_SCHEMA = All(Lower, Choices("group"))
SECTION_CACHE_LOCAL = "local"
SECTION_CACHE_S3 = "s3"
SECTION_CACHE_GS = "gs"
SECTION_CACHE_SSH = "ssh"
SECTION_CACHE_HDFS = "hdfs"
SECTION_CACHE_AZURE = "azure"
SECTION_CACHE_SLOW_LINK_WARNING = "slow_link_warning"
SECTION_CACHE_SCHEMA = {
SECTION_CACHE_LOCAL: str,
SECTION_CACHE_S3: str,
SECTION_CACHE_GS: str,
SECTION_CACHE_HDFS: str,
SECTION_CACHE_SSH: str,
SECTION_CACHE_AZURE: str,
SECTION_CACHE_DIR: str,
SECTION_CACHE_TYPE: supported_cache_type,
Optional(SECTION_CACHE_PROTECTED, default=False): Bool,
SECTION_CACHE_SHARED: SECTION_CACHE_SHARED_SCHEMA,
PRIVATE_CWD: str,
Optional(SECTION_CACHE_SLOW_LINK_WARNING, default=True): Bool,
}
SECTION_CORE_SCHEMA = {
SECTION_CORE_LOGLEVEL: SECTION_CORE_LOGLEVEL_SCHEMA,
SECTION_CORE_REMOTE: Lower,
Optional(SECTION_CORE_INTERACTIVE, default=False): Bool,
Optional(SECTION_CORE_ANALYTICS, default=True): Bool,
SECTION_CORE_CHECKSUM_JOBS: All(Coerce(int), Range(1)),
Optional(SECTION_CORE_HARDLINK_LOCK, default=False): Bool,
}
# aws specific options
SECTION_AWS_CREDENTIALPATH = CREDENTIALPATH
SECTION_AWS_ENDPOINT_URL = "endpointurl"
SECTION_AWS_LIST_OBJECTS = "listobjects"
SECTION_AWS_REGION = "region"
SECTION_AWS_PROFILE = "profile"
SECTION_AWS_USE_SSL = "use_ssl"
SECTION_AWS_SSE = "sse"
SECTION_AWS_ACL = "acl"
SECTION_AWS_GRANT_READ = "grant_read"
SECTION_AWS_GRANT_READ_ACP = "grant_read_acp"
SECTION_AWS_GRANT_WRITE_ACP = "grant_write_acp"
SECTION_AWS_GRANT_FULL_CONTROL = "grant_full_control"
# gcp specific options
SECTION_GCP_CREDENTIALPATH = CREDENTIALPATH
SECTION_GCP_PROJECTNAME = "projectname"
# azure specific option
SECTION_AZURE_CONNECTION_STRING = "connection_string"
# Alibabacloud oss options
SECTION_OSS_ACCESS_KEY_ID = "oss_key_id"
SECTION_OSS_ACCESS_KEY_SECRET = "oss_key_secret"
SECTION_OSS_ENDPOINT = "oss_endpoint"
# GDrive options
SECTION_GDRIVE_CLIENT_ID = "gdrive_client_id"
SECTION_GDRIVE_CLIENT_SECRET = "gdrive_client_secret"
SECTION_GDRIVE_USER_CREDENTIALS_FILE = "gdrive_user_credentials_file"
SECTION_REMOTE_REGEX = r'^\s*remote\s*"(?P<name>.*)"\s*$'
SECTION_REMOTE_FMT = 'remote "{}"'
SECTION_REMOTE_URL = "url"
SECTION_REMOTE_USER = "user"
SECTION_REMOTE_PORT = "port"
SECTION_REMOTE_KEY_FILE = "keyfile"
SECTION_REMOTE_TIMEOUT = "timeout"
SECTION_REMOTE_PASSWORD = "password"
SECTION_REMOTE_ASK_PASSWORD = "ask_password"
SECTION_REMOTE_GSS_AUTH = "gss_auth"
SECTION_REMOTE_NO_TRAVERSE = "no_traverse"
SECTION_REMOTE_SCHEMA = {
Required(SECTION_REMOTE_URL): str,
SECTION_AWS_REGION: str,
SECTION_AWS_PROFILE: str,
SECTION_AWS_CREDENTIALPATH: str,
SECTION_AWS_ENDPOINT_URL: str,
Optional(SECTION_AWS_LIST_OBJECTS, default=False): Bool,
Optional(SECTION_AWS_USE_SSL, default=True): Bool,
SECTION_AWS_SSE: str,
SECTION_AWS_ACL: str,
SECTION_AWS_GRANT_READ: str,
SECTION_AWS_GRANT_READ_ACP: str,
SECTION_AWS_GRANT_WRITE_ACP: str,
SECTION_AWS_GRANT_FULL_CONTROL: str,
SECTION_GCP_PROJECTNAME: str,
SECTION_CACHE_TYPE: supported_cache_type,
Optional(SECTION_CACHE_PROTECTED, default=False): Bool,
SECTION_REMOTE_USER: str,
SECTION_REMOTE_PORT: Coerce(int),
SECTION_REMOTE_KEY_FILE: str,
SECTION_REMOTE_TIMEOUT: Coerce(int),
SECTION_REMOTE_PASSWORD: str,
SECTION_REMOTE_ASK_PASSWORD: Bool,
SECTION_REMOTE_GSS_AUTH: Bool,
SECTION_AZURE_CONNECTION_STRING: str,
SECTION_OSS_ACCESS_KEY_ID: str,
SECTION_OSS_ACCESS_KEY_SECRET: str,
SECTION_OSS_ENDPOINT: str,
SECTION_GDRIVE_CLIENT_ID: str,
SECTION_GDRIVE_CLIENT_SECRET: str,
SECTION_GDRIVE_USER_CREDENTIALS_FILE: str,
PRIVATE_CWD: str,
Optional(SECTION_REMOTE_NO_TRAVERSE, default=True): Bool,
}
SECTION_STATE = "state"
SECTION_STATE_ROW_LIMIT = "row_limit"
SECTION_STATE_ROW_CLEANUP_QUOTA = "row_cleanup_quota"
SECTION_STATE_SCHEMA = {
SECTION_STATE_ROW_LIMIT: All(Coerce(int), Range(1)),
SECTION_STATE_ROW_CLEANUP_QUOTA: All(Coerce(int), Range(0, 100)),
}
SCHEMA = {
Optional(SECTION_CORE, default={}): SECTION_CORE_SCHEMA,
Match(SECTION_REMOTE_REGEX): SECTION_REMOTE_SCHEMA,
Optional(SECTION_CACHE, default={}): SECTION_CACHE_SCHEMA,
Optional(SECTION_STATE, default={}): SECTION_STATE_SCHEMA,
}
COMPILED_SCHEMA = Schema(SCHEMA)
def __init__(self, dvc_dir=None, validate=True):
self.dvc_dir = dvc_dir
self.validate = validate
if not dvc_dir:
try:
@@ -271,33 +244,31 @@ class Config(object): # pylint: disable=too-many-instance-attributes
else:
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
self.load()
self.load(validate=validate)
@staticmethod
def get_global_config_dir():
"""Returns global config location. E.g. ~/.config/dvc/config.
@classmethod
def get_dir(cls, level):
from appdirs import user_config_dir, site_config_dir
Returns:
str: path to the global config directory.
"""
from appdirs import user_config_dir
assert level in ("global", "system")
return user_config_dir(
appname=Config.APPNAME, appauthor=Config.APPAUTHOR
)
if level == "global":
return user_config_dir(cls.APPNAME, cls.APPAUTHOR)
if level == "system":
return site_config_dir(cls.APPNAME, cls.APPAUTHOR)
@staticmethod
def get_system_config_dir():
"""Returns system config location. E.g. /etc/dvc.conf.
@cached_property
def files(self):
files = {
level: os.path.join(self.get_dir(level), self.CONFIG)
for level in ("system", "global")
}
Returns:
str: path to the system config directory.
"""
from appdirs import site_config_dir
if self.dvc_dir is not None:
files["repo"] = os.path.join(self.dvc_dir, self.CONFIG)
files["local"] = os.path.join(self.dvc_dir, self.CONFIG_LOCAL)
return site_config_dir(
appname=Config.APPNAME, appauthor=Config.APPAUTHOR
)
return files
@staticmethod
def init(dvc_dir):
@@ -313,243 +284,156 @@ class Config(object): # pylint: disable=too-many-instance-attributes
open(config_file, "w+").close()
return Config(dvc_dir)
def _resolve_cache_path(self, config):
cache = config.get(self.SECTION_CACHE)
if cache is None:
return
cache_dir = cache.get(self.SECTION_CACHE_DIR)
if cache_dir is None:
return
cache[self.PRIVATE_CWD] = os.path.dirname(config.filename)
def _resolve_paths(self, config):
if config.filename is None:
return config
ret = copy.deepcopy(config)
self._resolve_cache_path(ret)
for section in ret.values():
if self.SECTION_REMOTE_URL not in section.keys():
continue
section[self.PRIVATE_CWD] = os.path.dirname(ret.filename)
return ret
def _load_configs(self):
system_config_file = os.path.join(
self.get_system_config_dir(), self.CONFIG
)
global_config_file = os.path.join(
self.get_global_config_dir(), self.CONFIG
)
self._system_config = configobj.ConfigObj(system_config_file)
self._global_config = configobj.ConfigObj(global_config_file)
self._repo_config = configobj.ConfigObj()
self._local_config = configobj.ConfigObj()
if not self.dvc_dir:
return
config_file = os.path.join(self.dvc_dir, self.CONFIG)
config_local_file = os.path.join(self.dvc_dir, self.CONFIG_LOCAL)
self._repo_config = configobj.ConfigObj(config_file)
self._local_config = configobj.ConfigObj(config_local_file)
@property
def config_local_file(self):
return self._local_config.filename
@property
def config_file(self):
return self._repo_config.filename
def load(self):
def load(self, validate=True):
"""Loads config from all the config files.
Raises:
dvc.config.ConfigError: thrown if config has invalid format.
ConfigError: thrown if config has an invalid format.
"""
self._load_configs()
conf = self.load_config_to_level()
self.config = configobj.ConfigObj()
for c in [
self._system_config,
self._global_config,
self._repo_config,
self._local_config,
]:
c = self._resolve_paths(c)
c = self._lower(c)
self.config.merge(c)
if validate:
conf = self.validate(conf)
if not self.validate:
return
self.clear()
self.update(conf)
d = self.config.dict()
try:
d = self.COMPILED_SCHEMA(d)
except Invalid as exc:
raise ConfigError(str(exc), cause=exc)
self.config = configobj.ConfigObj(d, write_empty_values=True)
# Add resolved default cache.dir
if not self["cache"].get("dir") and self.dvc_dir:
self["cache"]["dir"] = os.path.join(self.dvc_dir, "cache")
def save(self, config=None):
"""Saves config to config files.
def load_one(self, level):
conf = _load_config(self.files[level])
conf = self._load_paths(conf, self.files[level])
Raises:
dvc.config.ConfigError: thrown if failed to write config file.
"""
if config is not None:
clist = [config]
else:
clist = [
self._system_config,
self._global_config,
self._repo_config,
self._local_config,
]
# Auto-verify sections
for key in COMPILED_SCHEMA.schema:
conf.setdefault(key, {})
for conf in clist:
self._save(conf)
return conf
@staticmethod
def _load_paths(conf, filename):
abs_conf_dir = os.path.abspath(os.path.dirname(filename))
def resolve(path):
if os.path.isabs(path) or re.match(r"\w+://", path):
return path
return RelPath(os.path.join(abs_conf_dir, path))
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)
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, "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:
raise ConfigError("Not inside a DVC repo")
conf = self.load_one(level)
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()
@staticmethod
def _save(config):
if config.filename is None:
return
logger.debug("Writing '{}'.".format(config.filename))
dname = os.path.dirname(os.path.abspath(config.filename))
def validate(data):
try:
os.makedirs(dname)
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
config.write()
return COMPILED_SCHEMA(data)
except Invalid as exc:
raise ConfigError(str(exc)) from None
def unset(self, section, opt=None, level=None, force=False):
"""Unsets specified option and/or section in the config.
Args:
section (str): section name.
opt (str): optional option name.
level (int): config level to use.
force (bool): don't error-out even if section doesn't exist. False
by default.
def _load_config(filename):
conf_obj = configobj.ConfigObj(filename)
return _parse_remotes(_lower_keys(conf_obj.dict()))
Raises:
dvc.config.ConfigError: thrown if section doesn't exist and
`force != True`.
"""
config = self.get_configobj(level)
if section not in config.keys():
if force:
return
raise ConfigError("section '{}' doesn't exist".format(section))
def _save_config(filename, conf_dict):
logger.debug(f"Writing '{filename}'.")
os.makedirs(os.path.dirname(filename), exist_ok=True)
if opt:
if opt not in config[section].keys():
if force:
return
raise ConfigError(
"option '{}.{}' doesn't exist".format(section, opt)
)
del config[section][opt]
config = configobj.ConfigObj(_pack_remotes(conf_dict))
config.filename = filename
config.write()
if not config[section]:
del config[section]
def _parse_remotes(conf):
result = {"remote": {}}
for section, val in conf.items():
name = re_find(r'^\s*remote\s*"(.*)"\s*$', section)
if name:
result["remote"][name] = val
else:
del config[section]
result[section] = val
self.save(config)
return result
def set(self, section, opt, value, level=None, force=True):
"""Sets specified option in the config.
Args:
section (str): section name.
opt (str): option name.
value: value to set option to.
level (int): config level to use.
force (bool): set option even if section already exists. True by
default.
def _pack_remotes(conf):
# Drop empty sections
result = compact(conf)
Raises:
dvc.config.ConfigError: thrown if section already exists and
`force != True`.
# Transform remote.name -> 'remote "name"'
for name, val in conf["remote"].items():
result[f'remote "{name}"'] = val
result.pop("remote", None)
"""
config = self.get_configobj(level)
return result
if section not in config.keys():
config[section] = {}
elif not force:
raise ConfigError(
"Section '{}' already exists. Use `-f|--force` to overwrite "
"section with new value.".format(section)
)
config[section][opt] = value
self.save(config)
def _merge(into, update):
"""Merges second dict into first recursively"""
for key, val in update.items():
if isinstance(into.get(key), dict) and isinstance(val, dict):
_merge(into[key], val)
else:
into[key] = val
def get(self, section, opt=None, level=None):
"""Return option value from the config.
Args:
section (str): section name.
opt (str): option name.
level (int): config level to use.
Returns:
value (str, int): option value.
"""
config = self.get_configobj(level)
if section not in config.keys():
raise ConfigError("section '{}' doesn't exist".format(section))
if opt not in config[section].keys():
raise ConfigError(
"option '{}.{}' doesn't exist".format(section, opt)
)
return config[section][opt]
@staticmethod
def _lower(config):
new_config = configobj.ConfigObj()
for s_key, s_value in config.items():
new_s = {}
for key, value in s_value.items():
new_s[key.lower()] = str(value)
new_config[s_key.lower()] = new_s
return new_config
def get_configobj(self, level):
configs = {
self.LEVEL_LOCAL: self._local_config,
self.LEVEL_REPO: self._repo_config,
self.LEVEL_GLOBAL: self._global_config,
self.LEVEL_SYSTEM: self._system_config,
}
return configs.get(level, self._repo_config)
def list_options(self, section_regex, option, level=None):
ret = {}
config = self.get_configobj(level)
for section in config.keys():
r = re.match(section_regex, section)
if r:
name = r.group("name")
value = config[section].get(option, "")
ret[name] = value
return ret
def _lower_keys(data):
return {
k.lower(): _lower_keys(v) if isinstance(v, dict) else v
for k, v in data.items()
}
+5 -11
View File
@@ -1,5 +1,4 @@
"""Launch `dvc daemon` command in a separate detached process."""
from __future__ import unicode_literals
import inspect
import logging
@@ -8,10 +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.compat import cast_bytes_py2
from dvc.utils import fix_env, is_binary
logger = logging.getLogger(__name__)
@@ -70,7 +66,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)
@@ -79,7 +75,7 @@ def _spawn(cmd, env):
else:
raise NotImplementedError
logger.debug("Spawned '{}'".format(cmd))
logger.debug(f"Spawned '{cmd}'")
def daemon(args):
@@ -99,9 +95,7 @@ def daemon(args):
env = fix_env()
file_path = os.path.abspath(inspect.stack()[0][1])
env[cast_bytes_py2("PYTHONPATH")] = cast_bytes_py2(
os.path.dirname(os.path.dirname(file_path))
)
env[cast_bytes_py2(DVC_DAEMON)] = cast_bytes_py2("1")
env["PYTHONPATH"] = os.path.dirname(os.path.dirname(file_path))
env[DVC_DAEMON] = "1"
_spawn(cmd, env)
+14 -17
View File
@@ -1,6 +1,4 @@
"""Draws DAG in ASCII."""
from __future__ import print_function
from __future__ import unicode_literals
import logging
import math
@@ -8,15 +6,12 @@ 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
logger = logging.getLogger(__name__)
@@ -42,17 +37,19 @@ 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(
"Unable to find `less` in the PATH. Check out "
"man.dvc.org/doc/command-reference/pipeline/show for more info."
"{} for more info.".format(
format_link("https://man.dvc.org/pipeline/show")
)
)
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,7 +92,7 @@ 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."""
@@ -148,21 +145,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 +213,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()
+52 -33
View File
@@ -1,17 +1,14 @@
"""Manages dvc remotes that user can use with push/pull/status commands."""
from __future__ import unicode_literals
import logging
from dvc.config import Config
from dvc.config import NoRemoteError
from dvc.remote import Remote
logger = logging.getLogger(__name__)
class DataCloud(object):
class DataCloud:
"""Class that manages dvc remotes.
Args:
@@ -25,69 +22,91 @@ class DataCloud(object):
def __init__(self, repo):
self.repo = repo
@property
def _config(self):
return self.repo.config.config
def get_remote(self, name=None, command="<command>"):
if not name:
name = self.repo.config["core"].get("remote")
@property
def _core(self):
return self._config.get(Config.SECTION_CORE, {})
if name:
return self._init_remote(name)
def get_remote(self, remote=None, command="<command>"):
if not remote:
remote = self._core.get(Config.SECTION_CORE_REMOTE)
if bool(self.repo.config["remote"]):
error_msg = (
"no remote specified. Setup default remote with\n"
" dvc remote default <remote name>\n"
"or use:\n"
" dvc {} -r <remote name>".format(command)
)
else:
error_msg = (
"no remote specified. Create a default remote with\n"
" dvc remote add -d <remote name> <remote url>"
)
if remote:
return self._init_remote(remote)
raise NoRemoteError(error_msg)
raise NoRemoteError(command)
def _init_remote(self, name):
return Remote(self.repo, name=name)
def _init_remote(self, remote):
return Remote(self.repo, name=remote)
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.
"""
return self.repo.cache.local.pull(
cache,
jobs=jobs,
remote=self.get_remote(remote, "pull"),
show_checksums=show_checksums,
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:
self._save_pulled_checksums(cache)
return downloaded_items_num
def _save_pulled_checksums(self, cache):
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()`)
self.repo.state.save(cache_file, checksum)
def status(self, cache, jobs=None, remote=None, show_checksums=False):
"""Check status of data items in a cloud-agnostic way.
Args:
cache (NamedCache): named checksums to check status for.
jobs (int): number of jobs that can be running simultaneously.
remote (dvc.remote.base.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
+67 -38
View File
@@ -1,37 +1,39 @@
from __future__ import unicode_literals
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.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 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
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.GS: GSDependency,
Schemes.HDFS: HDFSDependency,
Schemes.HTTP: HTTPDependency,
Schemes.HTTPS: HTTPSDependency,
}
@@ -40,34 +42,36 @@ 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):
from dvc.utils.compat import urlparse
parsed = urlparse(p)
if parsed.scheme == "remote":
parsed = urlparse(p) if p else None
if parsed and parsed.scheme == "remote":
remote = 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
@@ -75,6 +79,31 @@ def loadd_from(stage, d_list):
def loads_from(stage, s_list, erepo=None):
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 _parse_params(path_params):
path, _, params_str = path_params.rpartition(":")
params = params_str.split(",")
return path, params
def loads_params(stage, s_list):
# Creates an object for each unique file that is referenced in the list
params_by_path = defaultdict(list)
for s in s_list:
path, params = _parse_params(s)
params_by_path[path].extend(params)
d_list = []
for path, params in params_by_path.items():
d_list.append(
{
BaseOutput.PARAM_PATH: path,
ParamsDependency.PARAM_PARAMS: params,
}
)
return loadd_from(stage, d_list)
+7 -11
View File
@@ -1,33 +1,29 @@
from __future__ import unicode_literals
from dvc.exceptions import DvcException
class DependencyDoesNotExistError(DvcException):
def __init__(self, path):
msg = "dependency '{}' does not exist".format(path)
super(DependencyDoesNotExistError, self).__init__(msg)
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)
super(DependencyIsNotFileOrDirError, self).__init__(msg)
msg = f"dependency '{path}' is not a file or directory"
super().__init__(msg)
class DependencyIsStageFileError(DvcException):
def __init__(self, path):
super(DependencyIsStageFileError, self).__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
IsNotFileOrDirError = DependencyIsNotFileOrDirError
IsStageFileError = DependencyIsStageFileError
def update(self):
def update(self, rev=None):
pass
+3 -5
View File
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
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 -5
View File
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
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 -7
View File
@@ -1,9 +1,7 @@
from __future__ import unicode_literals
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 HTTPRemote
class DependencyHTTP(DependencyBase, OutputBase):
REMOTE = RemoteHTTP
class HTTPDependency(BaseDependency, BaseOutput):
REMOTE = HTTPRemote
+4 -5
View File
@@ -1,8 +1,7 @@
from __future__ import unicode_literals
from dvc.remote.https import HTTPSRemote
from .http import DependencyHTTP
from dvc.remote.https import RemoteHTTPS
from .http import HTTPDependency
class DependencyHTTPS(DependencyHTTP):
REMOTE = RemoteHTTPS
class HTTPSDependency(HTTPDependency):
REMOTE = HTTPSRemote
+3 -5
View File
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
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 _dyn_load(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
+45 -73
View File
@@ -1,31 +1,30 @@
from __future__ import unicode_literals
import copy
import os
from contextlib import contextmanager
from dvc.utils.compat import FileNotFoundError
from funcy import merge
from voluptuous import Required
from .local import DependencyLOCAL
from dvc.external_repo import external_repo
from dvc.utils.compat import str
from dvc.exceptions import OutputNotFoundError
from dvc.exceptions import PathMissingError
from dvc.utils.fs import fs_copy
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 = {PARAM_URL: str, PARAM_REV: str, PARAM_REV_LOCK: str}
REPO_SCHEMA = {
PARAM_REPO: {
Required(PARAM_URL): str,
PARAM_REV: str,
PARAM_REV_LOCK: str,
}
}
def __init__(self, def_repo, stage, *args, **kwargs):
self.def_repo = def_repo
super(DependencyREPO, self).__init__(stage, *args, **kwargs)
super().__init__(stage, *args, **kwargs)
def _parse_path(self, remote, path):
return None
@@ -37,24 +36,33 @@ class DependencyREPO(DependencyLOCAL):
@property
def repo_pair(self):
d = self.def_repo
return d[self.PARAM_URL], d[self.PARAM_REV_LOCK] or d[self.PARAM_REV]
rev = d.get(self.PARAM_REV_LOCK) or d.get(self.PARAM_REV)
return d[self.PARAM_URL], rev
def __str__(self):
return "{} ({})".format(self.def_path, self.def_repo[self.PARAM_URL])
@contextmanager
def _make_repo(self, **overrides):
with external_repo(**merge(self.def_repo, overrides)) as repo:
yield repo
def _make_repo(self, *, locked=True):
from dvc.external_repo import external_repo
d = self.def_repo
rev = (d.get("rev_lock") if locked else None) or d.get("rev")
return external_repo(d["url"], rev=rev)
def _get_checksum(self, locked=True):
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 are polluting our repo cache with some dir listing here
return self.repo.cache.local.get_checksum(path)
def status(self):
with self._make_repo() as repo:
current = repo.find_out_by_relpath(self.def_path).info
current_checksum = self._get_checksum(locked=True)
updated_checksum = self._get_checksum(locked=False)
with self._make_repo(rev_lock=None) as repo:
updated = repo.find_out_by_relpath(self.def_path).info
if current != updated:
if current_checksum != updated_checksum:
return {str(self): "update available"}
return {}
@@ -65,55 +73,19 @@ class DependencyREPO(DependencyLOCAL):
def dumpd(self):
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
def fetch(self):
with self._make_repo(
cache_dir=self.repo.cache.local.cache_dir
) as repo:
self.def_repo[self.PARAM_REV_LOCK] = repo.scm.get_rev()
out = repo.find_out_by_relpath(self.def_path)
with repo.state:
repo.cloud.pull(out.get_used_cache())
return out
@staticmethod
def _is_git_file(repo, path):
if not os.path.isabs(path):
try:
output = repo.find_out_by_relpath(path)
if not output.use_cache:
return True
except OutputNotFoundError:
return True
return False
def _copy_if_git_file(self, to_path):
src_path = self.def_path
with self._make_repo(
cache_dir=self.repo.cache.local.cache_dir
) as repo:
if not self._is_git_file(repo, src_path):
return False
src_full_path = os.path.join(repo.root_dir, src_path)
dst_full_path = os.path.abspath(to_path)
fs_copy(src_full_path, dst_full_path)
return True
def download(self, to):
try:
if self._copy_if_git_file(to.fspath):
return
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()
out = self.fetch()
to.info = copy.copy(out.info)
to.checkout()
except (FileNotFoundError):
raise PathMissingError(
self.def_path, self.def_repo[self.PARAM_URL]
)
if hasattr(repo, "cache"):
repo.cache.local.cache_dir = self.repo.cache.local.cache_dir
def update(self):
with self._make_repo(rev_lock=None) as repo:
repo.pull_to(self.def_path, to.path_info)
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()
+3 -5
View File
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
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 -5
View File
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
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
+270
View File
@@ -0,0 +1,270 @@
import contextlib
import logging
import os
from voluptuous import MultipleInvalid
import dvc.prompt as prompt
from dvc import serialize
from dvc.exceptions import DvcException
from dvc.stage.exceptions import (
StageFileAlreadyExistsError,
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.stage import (
dump_stage_file,
parse_stage,
parse_stage_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_stage(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_with_prompt(self, force=False):
raise NotImplementedError
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_stage_file(self.path, serialize.to_single_stage_file(stage))
self.repo.scm.track_file(relpath(self.path))
def remove_with_prompt(self, force=False):
if not self.exists():
return
msg = (
"'{}' already exists. Do you wish to run the command and "
"overwrite it?".format(relpath(self.path))
)
if not (force or prompt.confirm(msg)):
raise StageFileAlreadyExistsError(self.path)
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, **kwargs):
"""Dumps given stage appropriately in the dvcfile."""
from dvc.stage import PipelineStage
assert isinstance(stage, PipelineStage)
check_dvc_filename(self.path)
self._dump_lockfile(stage)
if update_pipeline and not stage.is_data_source:
self._dump_pipeline_file(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_stage_for_update(fd.read(), self.path)
else:
open(self.path, "w+").close()
data["stages"] = data.get("stages", {})
stage_data = serialize.to_pipeline_file(stage)
if data["stages"].get(stage.name):
orig_stage_data = data["stages"][stage.name]
apply_diff(stage_data[stage.name], orig_stage_data)
else:
data["stages"].update(stage_data)
dump_stage_file(self.path, data)
self.repo.scm.track_file(relpath(self.path))
@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()
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_stage(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():
data = stage_data
open(self.path, "w+").close()
else:
with self.repo.tree.open(self.path, "r") as fd:
data = parse_stage_for_update(fd.read(), self.path)
data.update(stage_data)
dump_stage_file(self.path, data)
self.repo.scm.track_file(relpath(self.path))
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)
+116 -143
View File
@@ -1,32 +1,19 @@
"""Exceptions raised by the dvc."""
from __future__ import unicode_literals
from funcy import first
import traceback
from dvc.utils import relpath
from dvc.utils.compat import builtin_str
from dvc.utils.compat import str
from dvc.utils import format_link, relpath
class DvcException(Exception):
"""Base class for all dvc exceptions.
"""Base class for all dvc exceptions."""
Args:
msg (unicode): message for this exception.
cause (Exception): optional cause exception.
"""
def __init__(self, msg, *args):
assert msg
super().__init__(msg, *args)
def __init__(self, msg, cause=None):
# NOTE: unlike python 3, python 2 doesn't have built-in support
# for chained exceptions, so we are using our own implementation.
self.cause = cause
self.cause_tb = None
if cause:
try:
self.cause_tb = traceback.format_exc()
except AttributeError: # pragma: no cover
pass
super(DvcException, self).__init__(msg)
class InvalidArgumentError(ValueError, DvcException):
"""Thrown if arguments are invalid."""
class OutputDuplicationError(DvcException):
@@ -39,18 +26,19 @@ class OutputDuplicationError(DvcException):
"""
def __init__(self, output, stages):
assert isinstance(output, str) or isinstance(output, builtin_str)
assert isinstance(stages, list)
assert all(
isinstance(stage, str) or isinstance(stage, builtin_str)
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(stages))
super(OutputDuplicationError, self).__init__(msg)
assert isinstance(output, str)
assert all(hasattr(stage, "relpath") for stage 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):
@@ -63,8 +51,8 @@ class OutputNotFoundError(DvcException):
def __init__(self, output, repo=None):
self.output = output
self.repo = repo
super(OutputNotFoundError, self).__init__(
"unable to find DVC-file with output '{path}'".format(
super().__init__(
"Unable to find DVC-file with output '{path}'".format(
path=relpath(self.output)
)
)
@@ -75,21 +63,17 @@ class StagePathAsOutputError(DvcException):
an output of another stage.
Args:
cwd (str): path to the directory.
fname (str): path to the DVC-file that has cwd specified as an
output.
stage (Stage): a stage that is in some other stages output
output (str): an output covering the stage above
"""
def __init__(self, wdir, fname):
assert isinstance(wdir, str) or isinstance(wdir, builtin_str)
assert isinstance(fname, str) or isinstance(fname, builtin_str)
msg = (
"current working directory '{cwd}' is specified as an output in "
"'{fname}'. Use another CWD to prevent any data removal.".format(
cwd=wdir, fname=fname
def __init__(self, stage, output):
assert isinstance(output, str)
super().__init__(
"{stage} is within an output '{output}' of another stage".format(
stage=stage, output=output
)
)
super(StagePathAsOutputError, self).__init__(msg)
class CircularDependencyError(DvcException):
@@ -101,13 +85,10 @@ class CircularDependencyError(DvcException):
"""
def __init__(self, dependency):
assert isinstance(dependency, (str, builtin_str))
assert isinstance(dependency, str)
msg = (
"file/directory '{}' is specified as an output and as a "
"dependency."
)
super(CircularDependencyError, self).__init__(msg.format(dependency))
msg = "'{}' is specified as an output and as a dependency."
super().__init__(msg.format(dependency))
class ArgumentDuplicationError(DvcException):
@@ -119,9 +100,8 @@ class ArgumentDuplicationError(DvcException):
"""
def __init__(self, path):
assert isinstance(path, (str, builtin_str))
msg = "file '{}' is specified more than once."
super(ArgumentDuplicationError, self).__init__(msg.format(path))
assert isinstance(path, str)
super().__init__(f"file '{path}' is specified more than once.")
class MoveNotDataSourceError(DvcException):
@@ -139,157 +119,122 @@ class MoveNotDataSourceError(DvcException):
"it by hand, or remove '{path}' and create a new one at the "
"desired location."
)
super(MoveNotDataSourceError, self).__init__(msg.format(path=path))
super().__init__(msg.format(path=path))
class NotDvcRepoError(DvcException):
"""Thrown if a directory is not a dvc repo.
Args:
root (str): path to the directory.
"""
def __init__(self, root):
msg = (
"you are not inside of a dvc repository "
"(checked up to mount point '{}')"
)
super(NotDvcRepoError, self).__init__(msg.format(root))
"""Thrown if a directory is not a DVC repo"""
class DvcParserError(DvcException):
"""Base class for CLI parser errors."""
def __init__(self):
super(DvcParserError, self).__init__("parser error")
super().__init__("parser error")
class CyclicGraphError(DvcException):
def __init__(self, stages):
assert isinstance(stages, list)
stages = "\n".join("\t- {}".format(stage) for stage in stages)
msg = (
"you've introduced a cycle in your pipeline that involves "
"the following stages:"
"\n"
"{stages}".format(stages=stages)
msg = "Pipeline has a cycle involving: {}.".format(
", ".join(s.addressing for s in stages)
)
super(CyclicGraphError, self).__init__(msg)
super().__init__(msg)
class ConfirmRemoveError(DvcException):
def __init__(self, path):
super(ConfirmRemoveError, self).__init__(
"unable to remove '{}' without a confirmation from the user. Use "
super().__init__(
"unable to remove '{}' without a confirmation. Use "
"`-f` to force.".format(path)
)
class InitError(DvcException):
def __init__(self, msg):
super(InitError, self).__init__(msg)
pass
class ReproductionError(DvcException):
def __init__(self, dvc_file_name, ex):
def __init__(self, dvc_file_name):
self.path = dvc_file_name
msg = "failed to reproduce '{}'".format(dvc_file_name)
super(ReproductionError, self).__init__(msg, cause=ex)
super().__init__(f"failed to reproduce '{dvc_file_name}'")
class BadMetricError(DvcException):
def __init__(self, paths):
super(BadMetricError, self).__init__(
"the following metrics do not exists, "
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)
)
)
class NoMetricsError(DvcException):
def __init__(self):
super(NoMetricsError, self).__init__(
super().__init__(
"no metric files in this repository. "
"Use `dvc metrics add` to add a metric file to track."
)
class StageFileCorruptedError(DvcException):
def __init__(self, path, cause=None):
def __init__(self, path):
path = relpath(path)
super(StageFileCorruptedError, self).__init__(
super().__init__(
"unable to read DVC-file: {} "
"YAML file structure is corrupted".format(path),
cause=cause,
"YAML file structure is corrupted".format(path)
)
class RecursiveAddingWhileUsingFilename(DvcException):
def __init__(self):
super(RecursiveAddingWhileUsingFilename, self).__init__(
super().__init__(
"cannot use `fname` with multiple targets or `-R|--recursive`"
)
class OverlappingOutputPathsError(DvcException):
def __init__(self, out_1, out_2):
super(OverlappingOutputPathsError, self).__init__(
"Paths for outs:\n'{}'('{}')\n'{}'('{}')\noverlap. To avoid "
"unpredictable behaviour, rerun command with non overlapping outs "
"paths.".format(
str(out_1),
out_1.stage.relpath,
str(out_2),
out_2.stage.relpath,
)
)
def __init__(self, parent, overlapping_out, message):
self.parent = parent
self.overlapping_out = overlapping_out
super().__init__(message)
class CheckoutErrorSuggestGit(DvcException):
def __init__(self, target, cause):
super(CheckoutErrorSuggestGit, self).__init__(
"Did you mean `git checkout {}`?".format(target), cause=cause
)
def __init__(self, target):
super().__init__(f"Did you mean `git checkout {target}`?")
class ETagMismatchError(DvcException):
def __init__(self, etag, cached_etag):
super(ETagMismatchError, self).__init__(
super().__init__(
"ETag mismatch detected when copying file to cache! "
"(expected: '{}', actual: '{}')".format(etag, cached_etag)
)
class FileMissingError(DvcException):
def __init__(self, path, cause=None):
super(FileMissingError, self).__init__(
"Can't find '{}' neither locally nor on remote".format(path),
cause=cause,
)
def __init__(self, path):
self.path = path
super().__init__(f"Can't find '{path}' neither locally nor on remote")
class DvcIgnoreInCollectedDirError(DvcException):
def __init__(self, ignore_dirname):
super(DvcIgnoreInCollectedDirError, self).__init__(
super().__init__(
".dvcignore file should not be in collected dir path: "
"'{}'".format(ignore_dirname)
)
class UrlNotDvcRepoError(DvcException):
def __init__(self, url):
super(UrlNotDvcRepoError, self).__init__(
"URL '{}' is not a dvc repository.".format(url)
)
class GitHookAlreadyExistsError(DvcException):
def __init__(self, hook_name):
super(GitHookAlreadyExistsError, self).__init__(
"Hook '{}' already exists. Please refer to "
"https://man.dvc.org/install "
"for more info.".format(hook_name)
super().__init__(
"Hook '{}' already exists. Please refer to {} for more "
"info.".format(
hook_name, format_link("https://man.dvc.org/install")
)
)
@@ -297,28 +242,26 @@ class DownloadError(DvcException):
def __init__(self, amount):
self.amount = amount
super(DownloadError, self).__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(UploadError, self).__init__(
"{amount} files failed to upload".format(amount=amount)
)
super().__init__(f"{amount} files failed to upload")
class CheckoutError(DvcException):
def __init__(self, target_infos):
def __init__(self, target_infos, stats=None):
self.target_infos = target_infos
self.stats = stats
targets = [str(t) for t in target_infos]
m = (
"Checkout failed for following targets:\n {}\nDid you "
"Checkout failed for following targets:\n{}\nDid you "
"forget to fetch?".format("\n".join(targets))
)
super(CheckoutError, self).__init__(m)
super().__init__(m)
class CollectCacheError(DvcException):
@@ -327,14 +270,14 @@ class CollectCacheError(DvcException):
class NoRemoteInExternalRepoError(DvcException):
def __init__(self, url):
super(NoRemoteInExternalRepoError, self).__init__(
"No DVC remote is specified in target repository '{}'.".format(url)
super().__init__(
f"No DVC remote is specified in target repository '{url}'."
)
class NoOutputInExternalRepoError(DvcException):
def __init__(self, path, external_repo_path, external_repo_url):
super(NoOutputInExternalRepoError, self).__init__(
super().__init__(
"Output '{}' not found in target repository '{}'".format(
relpath(path, external_repo_path), external_repo_url
)
@@ -343,13 +286,43 @@ class NoOutputInExternalRepoError(DvcException):
class HTTPError(DvcException):
def __init__(self, code, reason):
super(HTTPError, self).__init__("'{} {}'".format(code, reason))
super().__init__(f"'{code} {reason}'")
class PathMissingError(DvcException):
def __init__(self, path, repo):
default_msg = (
"The path '{}' does not exist in the target repository '{}'"
" neither as a DVC output nor as a Git-tracked file."
)
default_msg_output_only = (
"The path '{}' does not exist in the target repository '{}'"
" as an DVC output."
)
def __init__(self, path, repo, output_only=False):
msg = (
"The path '{}' does not exist in the target repository '{}'"
" neighther as an output nor a git-handled file."
self.default_msg
if not output_only
else self.default_msg_output_only
)
super(PathMissingError, self).__init__(msg.format(path, repo))
super().__init__(msg.format(path, repo))
self.output_only = output_only
class RemoteCacheRequiredError(DvcException):
def __init__(self, path_info):
super().__init__(
(
"Current operation was unsuccessful because '{}' requires "
"existing cache on '{}' remote. See {} for information on how "
"to set up remote cache."
).format(
path_info,
path_info.scheme,
format_link("https://man.dvc.org/config#cache"),
)
)
class IsADirectoryError(DvcException):
"""Raised when a file operation is requested on a directory."""
+240 -110
View File
@@ -1,29 +1,39 @@
from __future__ import unicode_literals
import logging
import os
import tempfile
import threading
from contextlib import contextmanager
from distutils.dir_util import copy_tree
from funcy import retry
from funcy import cached_property, retry, suppress, wrap_with
from dvc.config import NoRemoteError, ConfigError
from dvc.exceptions import NoRemoteInExternalRepoError
from dvc.remote import RemoteConfig
from dvc.exceptions import NoOutputInExternalRepoError
from dvc.exceptions import OutputNotFoundError
from dvc.utils.fs import remove
from dvc.config import NoRemoteError, NotDvcRepoError
from dvc.exceptions import (
CheckoutError,
FileMissingError,
NoOutputInExternalRepoError,
NoRemoteInExternalRepoError,
OutputNotFoundError,
PathMissingError,
)
from dvc.path_info import PathInfo
from dvc.repo import Repo
from dvc.scm.git import Git
from dvc.utils import tmp_fname
from dvc.utils.fs import fs_copy, move, remove
REPO_CACHE = {}
logger = logging.getLogger(__name__)
@contextmanager
def external_repo(url=None, rev=None, rev_lock=None, cache_dir=None):
from dvc.repo import Repo
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)
try:
repo = ExternalRepo(path, url)
except NotDvcRepoError:
repo = ExternalGitRepo(path, url)
path = _external_repo(url=url, rev=rev_lock or rev, cache_dir=cache_dir)
repo = Repo(path)
try:
yield repo
except NoRemoteError:
@@ -32,90 +42,233 @@ def external_repo(url=None, rev=None, rev_lock=None, cache_dir=None):
if exc.repo is repo:
raise NoOutputInExternalRepoError(exc.output, repo.root_dir, url)
raise
repo.close()
def _external_repo(url=None, rev=None, cache_dir=None):
from dvc.config import Config
from dvc.cache import CacheConfig
from dvc.repo import Repo
key = (url, rev, cache_dir)
if key in REPO_CACHE:
return REPO_CACHE[key]
new_path = tempfile.mkdtemp("dvc-erepo")
# Copy and adjust existing clone
if (url, None, None) in REPO_CACHE:
old_path = REPO_CACHE[url, None, None]
# This one unlike shutil.copytree() works with an existing dir
copy_tree(old_path, new_path)
else:
# Create a new clone
_clone_repo(url, new_path)
# Save clean clone dir so that we will have access to a default branch
clean_clone_path = tempfile.mkdtemp("dvc-erepo")
copy_tree(new_path, clean_clone_path)
REPO_CACHE[url, None, None] = clean_clone_path
# Adjust new clone/copy to fit rev and cache_dir
# Checkout needs to be done first because current branch might not be
# DVC repository
if rev is not None:
_git_checkout(new_path, rev)
repo = Repo(new_path)
try:
# check if the URL is local and no default remote is present
# add default remote pointing to the original repo's cache location
if os.path.isdir(url):
rconfig = RemoteConfig(repo.config)
if not _default_remote_set(rconfig):
original_repo = Repo(url)
try:
rconfig.add(
"auto-generated-upstream",
original_repo.cache.local.cache_dir,
default=True,
level=Config.LEVEL_LOCAL,
)
finally:
original_repo.close()
if cache_dir is not None:
cache_config = CacheConfig(repo.config)
cache_config.set_dir(cache_dir, level=Config.LEVEL_LOCAL)
except FileMissingError as exc:
raise PathMissingError(exc.path, url)
finally:
# Need to close/reopen repo to force config reread
repo.close()
REPO_CACHE[key] = new_path
return new_path
if for_write:
_remove(path)
def _git_checkout(repo_path, revision):
from dvc.scm import Git
git = Git(repo_path)
try:
git.checkout(revision)
finally:
git.close()
CLONES = {}
CACHE_DIRS = {}
def clean_repos():
# Outside code should not see cache while we are removing
repo_paths = list(REPO_CACHE.values())
REPO_CACHE.clear()
paths = list(CLONES.values()) + list(CACHE_DIRS.values())
CLONES.clear()
CACHE_DIRS.clear()
for path in repo_paths:
for path in paths:
_remove(path)
class ExternalRepo(Repo):
def __init__(self, root_dir, url):
super().__init__(root_dir)
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(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(path_info, 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:
cache_dir = CACHE_DIRS[self.url]
except KeyError:
cache_dir = CACHE_DIRS[self.url] = tempfile.mkdtemp("dvc-cache")
self.cache.local.cache_dir = cache_dir
def _fix_upstream(self):
if not os.path.isdir(self.url):
return
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:
self._add_upstream(src_repo)
finally:
src_repo.close()
def _fix_local_remote(self, src_repo, remote_name):
# If a remote URL is relative to the source repo,
# it will have changed upon config load and made
# relative to this new repo. Restore the old one here.
new_remote = self.config["remote"][remote_name]
old_remote = src_repo.config["remote"][remote_name]
if new_remote["url"] != old_remote["url"]:
new_remote["url"] = old_remote["url"]
def _add_upstream(self, src_repo):
# Fill the empty upstream entry with a new remote pointing to the
# original repo's cache location.
cache_dir = src_repo.cache.local.cache_dir
self.config["remote"]["auto-generated-upstream"] = {"url": cache_dir}
self.config["core"]["remote"] = "auto-generated-upstream"
class ExternalGitRepo:
def __init__(self, root_dir, url):
self.root_dir = root_dir
self.url = url
@cached_property
def scm(self):
return Git(self.root_dir)
def close(self):
if "scm" in self.__dict__:
self.scm.close()
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), 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."""
try:
abs_path = os.path.join(self.root_dir, path)
with open(abs_path, mode, encoding=encoding) as fd:
yield fd
except FileNotFoundError:
raise PathMissingError(path, self.url)
def _cached_clone(url, rev, for_write=False):
"""Clone an external git repo to a temporary directory.
Returns the path to a local temporary directory with the specified
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]
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]
# Copy to a new dir to keep the clone clean
repo_path = tempfile.mkdtemp("dvc-erepo")
logger.debug("erepo: making a copy of %s clone", url)
copy_tree(clone_path, repo_path)
# Check out the specified revision
if rev is not None:
_git_checkout(repo_path, rev)
if not for_write:
CLONES[url, rev_sha] = repo_path
return repo_path
@wrap_with(threading.Lock())
def _clone_default_branch(url, rev):
"""Get or create a clean clone of the url.
The cloned is reactualized with git pull unless rev is a known sha.
"""
clone_path = CLONES.get(url)
git = None
try:
if clone_path:
git = Git(clone_path)
# Do not pull for known shas, branches and tags might move
if not Git.is_sha(rev) or not git.has_rev(rev):
logger.debug("erepo: git pull %s", url)
git.pull()
else:
logger.debug("erepo: git clone %s to a temporary dir", url)
clone_path = tempfile.mkdtemp("dvc-clone")
git = Git.clone(url, clone_path)
CLONES[url] = clone_path
finally:
if git:
git.close()
return clone_path
def _git_checkout(repo_path, rev):
logger.debug("erepo: git checkout %s@%s", repo_path, rev)
git = Git(repo_path)
try:
git.checkout(rev)
finally:
git.close()
def _remove(path):
if os.name == "nt":
# git.exe may hang for a while not permitting to remove temp dir
@@ -123,26 +276,3 @@ def _remove(path):
os_retry(remove)(path)
else:
remove(path)
def _clone_repo(url, path):
from dvc.scm.git import Git
git = Git.clone(url, path)
git.close()
def _default_remote_set(rconfig):
"""
Checks if default remote config is present.
Args:
rconfig: a remote config
Returns:
True if the default remote config is set, else False
"""
try:
rconfig.get_default()
return True
except ConfigError:
return False
+61 -10
View File
@@ -1,17 +1,17 @@
from __future__ import unicode_literals
import logging
import os
from funcy import cached_property
from pathspec import PathSpec
from pathspec.patterns import GitWildMatchPattern
from dvc.scm.tree import BaseTree
from dvc.utils import relpath
logger = logging.getLogger(__name__)
class DvcIgnore(object):
class DvcIgnore:
DVCIGNORE_FILE = ".dvcignore"
def __call__(self, root, dirs, files):
@@ -71,14 +71,28 @@ class DvcIgnoreDirs(DvcIgnore):
return self.basenames == other.basenames
class DvcIgnoreFilter(object):
def __init__(self, root_dir, tree):
class DvcIgnoreRepo(DvcIgnore):
def __call__(self, root, dirs, files):
def is_dvc_repo(directory):
from dvc.repo import Repo
return os.path.isdir(os.path.join(directory, Repo.DVC_DIR))
dirs = [d for d in dirs if not is_dvc_repo(d)]
return dirs, files
class DvcIgnoreFilter:
def __init__(self, tree):
self.tree = tree
self.ignores = {DvcIgnoreDirs([".git", ".hg", ".dvc"])}
self._update(root_dir)
for root, dirs, _ in self.tree.walk(root_dir, dvcignore=self):
for d in dirs:
self._update(os.path.join(root, d))
self.ignores = {
DvcIgnoreDirs([".git", ".hg", ".dvc"]),
DvcIgnoreRepo(),
}
for root, dirs, files in self.tree.walk(self.tree.tree_root):
self._update(root)
dirs[:], files[:] = self(root, dirs, files)
def _update(self, dirname):
ignore_file_path = os.path.join(dirname, DvcIgnore.DVCIGNORE_FILE)
@@ -90,3 +104,40 @@ class DvcIgnoreFilter(object):
dirs, files = ignore(root, dirs, files)
return dirs, files
class CleanTree(BaseTree):
def __init__(self, tree):
self.tree = tree
@cached_property
def dvcignore(self):
return DvcIgnoreFilter(self.tree)
@property
def tree_root(self):
return self.tree.tree_root
def open(self, path, mode="r", encoding="utf-8"):
return self.tree.open(path, mode, encoding)
def exists(self, path):
return self.tree.exists(path)
def isdir(self, path):
return self.tree.isdir(path)
def isfile(self, path):
return self.tree.isfile(path)
def isexec(self, path):
return self.tree.isexec(path)
def walk(self, top, topdown=True):
for root, dirs, files in self.tree.walk(top, topdown):
dirs[:], files[:] = self.dvcignore(root, dirs, files)
yield root, dirs, files
def stat(self, path):
return self.tree.stat(path)
+1 -15
View File
@@ -1,24 +1,10 @@
"""Use heuristics to guess if it is a text file or a binary file."""
from __future__ import unicode_literals
from dvc.utils.compat import is_py3
from dvc.utils.compat import open
# Based on https://eli.thegreenplace.net/2011/10/19/
# perls-guess-if-file-is-text-or-binary-implemented-in-python
# A function that takes an integer in the 8-bit range and returns
# a single-character byte object in py3 / a single-character string
# in py2.
#
def _int2byte(i):
if is_py3:
return bytes((i,))
return chr(i)
TEXT_CHARS = b"".join(_int2byte(i) for i in range(32, 127)) + b"\n\r\t\f\b"
TEXT_CHARS = bytes(range(32, 127)) + b"\n\r\t\f\b"
def istextfile(fname, blocksize=512):
+63 -73
View File
@@ -1,33 +1,32 @@
"""Manages dvc lock file."""
from __future__ import unicode_literals
import hashlib
import os
import time
from datetime import timedelta
import flufl.lock
import zc.lockfile
from dvc.exceptions import DvcException
from dvc.utils import format_link
from dvc.utils.compat import is_py3, is_py2
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."
)
class LockError(DvcException):
"""Thrown when unable to acquire the lock for dvc repo."""
"""Thrown when unable to acquire the lock for DVC repo."""
class Lock(object):
"""Class for dvc repo lock.
class Lock:
"""Class for DVC repo lock.
Uses zc.lockfile as backend.
"""
@@ -48,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")
)
),
):
@@ -69,6 +68,10 @@ class Lock(object):
self._lock.close()
self._lock = None
@property
def is_locked(self):
return bool(self._lock)
def __enter__(self):
self.lock()
@@ -76,83 +79,70 @@ class Lock(object):
self.unlock()
if is_py3:
import flufl.lock
class HardlinkLock(flufl.lock.Lock):
"""Class for DVC repo lock.
class HardlinkLock(flufl.lock.Lock):
"""Class for dvc repo lock.
Args:
lockfile (str): the lock filename
in.
tmp_dir (str): a directory to store claim files.
"""
Args:
lockfile (str): the lock filename
in.
tmp_dir (str): a directory to store claim files.
"""
def __init__(self, lockfile, tmp_dir=None, **kwargs):
import socket
def __init__(self, lockfile, tmp_dir=None, **kwargs):
import socket
self._tmp_dir = tmp_dir
self._tmp_dir = tmp_dir
# NOTE: this is basically Lock.__init__ copy-paste, except that
# instead of using `socket.getfqdn()` we use `socket.gethostname()`
# to speed this up. We've seen [1] `getfqdn()` take ~5sec to return
# anything, which is way too slow. `gethostname()` is actually a
# fallback for `getfqdn()` when it is not able to resolve a
# canonical hostname through network. The claimfile that uses
# `self._hostname` is still usable, as it uses `pid` and random
# number to generate the resulting lock file name, which is unique
# enough for our application.
#
# [1] https://github.com/iterative/dvc/issues/2582
self._hostname = socket.gethostname()
# NOTE: this is basically Lock.__init__ copy-paste, except that
# instead of using `socket.getfqdn()` we use `socket.gethostname()`
# to speed this up. We've seen [1] `getfqdn()` take ~5sec to return
# anything, which is way too slow. `gethostname()` is actually a
# fallback for `getfqdn()` when it is not able to resolve a
# canonical hostname through network. The claimfile that uses
# `self._hostname` is still usable, as it uses `pid` and random
# number to generate the resulting lock file name, which is unique
# enough for our application.
#
# [1] https://github.com/iterative/dvc/issues/2582
self._hostname = socket.gethostname()
self._lockfile = lockfile
self._lifetime = timedelta(days=365) # Lock for good by default
self._separator = flufl.lock.SEP
self._set_claimfile()
self._owned = True
self._retry_errnos = []
self._lockfile = lockfile
self._lifetime = timedelta(days=365) # Lock for good by default
self._separator = flufl.lock.SEP
self._set_claimfile()
self._owned = True
self._retry_errnos = []
@property
def lockfile(self):
return self._lockfile
@property
def lockfile(self):
return self._lockfile
def lock(self):
try:
super().lock(timedelta(seconds=DEFAULT_TIMEOUT))
except flufl.lock.TimeOutError:
raise LockError(FAILED_TO_LOCK_MESSAGE)
def lock(self):
try:
super(Lock, self).lock(timedelta(seconds=DEFAULT_TIMEOUT))
except flufl.lock.TimeOutError:
raise LockError(FAILED_TO_LOCK_MESSAGE)
def _set_claimfile(self, pid=None):
super()._set_claimfile(pid)
def _set_claimfile(self, pid=None):
super(Lock, self)._set_claimfile(pid)
if self._tmp_dir is not None:
# Under Windows file path length is limited so we hash it
filename = hashlib.md5(self._claimfile.encode()).hexdigest()
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
if self._tmp_dir is not None:
# Under Windows file path length is limited so we hash it
filename = hashlib.md5(self._claimfile.encode()).hexdigest()
self._claimfile = os.path.join(
self._tmp_dir, filename + ".lock"
)
# Fix for __del__ bug in flufl.lock [1] which is causing errors on
# Python shutdown [2].
# [1] https://gitlab.com/warsaw/flufl.lock/issues/7
# [2] https://github.com/iterative/dvc/issues/2573
def __del__(self):
try:
if self._owned:
self.finalize()
except ImportError:
pass
# Fix for __del__ bug in flufl.lock [1] which is causing errors on
# Python shutdown [2].
# [1] https://gitlab.com/warsaw/flufl.lock/issues/7
# [2] https://github.com/iterative/dvc/issues/2573
def __del__(self):
try:
if self._owned:
self.finalize()
except ImportError:
pass
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False):
if hardlink_lock and is_py2:
raise DvcException(
"Hardlink locks are not supported on Python <3.5. "
"See `hardlink_lock` in {}".format(
format_link("man.dvc.org/config#core")
)
)
cls = HardlinkLock if hardlink_lock else Lock
return cls(lockfile, tmp_dir=tmp_dir, friendly=friendly)
+72 -105
View File
@@ -1,5 +1,4 @@
"""Manages logging configuration for dvc repo."""
from __future__ import unicode_literals
"""Manages logging configuration for DVC repo."""
import logging.config
import logging.handlers
@@ -7,10 +6,6 @@ import logging.handlers
import colorama
from dvc.progress import Tqdm
from dvc.utils.compat import RecursionError
from dvc.utils.compat import str
from dvc.utils.compat import StringIO
FOOTER = (
"\n{yellow}Having any troubles?{nc}"
@@ -26,7 +21,7 @@ FOOTER = (
class LoggingException(Exception):
def __init__(self, record):
msg = "failed to log {}".format(str(record))
super(LoggingException, self).__init__(msg)
super().__init__(msg)
class ExcludeErrorsFilter(logging.Filter):
@@ -40,117 +35,70 @@ class ExcludeInfoFilter(logging.Filter):
class ColorFormatter(logging.Formatter):
"""Enable color support when logging to a terminal that supports it.
"""Spit out colored text in supported terminals.
Color support on Windows versions that do not support ANSI color codes is
enabled by use of the colorama__ library.
colorama__ makes ANSI escape character sequences work under Windows.
See the colorama documentation for details.
__ https://pypi.python.org/pypi/colorama
For records containing `exc_info`, it will use a custom `_walk_exc` to
retrieve the whole traceback.
If record has an extra `tb_only` attribute, it will not show the
exception cause, just the message and the traceback.
"""
color_code = {
"DEBUG": colorama.Fore.BLUE,
"INFO": "",
"WARNING": colorama.Fore.YELLOW,
"ERROR": colorama.Fore.RED,
"CRITICAL": colorama.Fore.RED,
}
def format(self, record):
record.message = record.getMessage()
msg = self.formatMessage(record)
if record.levelname == "INFO":
return record.msg
return msg
if record.levelname == "ERROR" or record.levelname == "CRITICAL":
exception, stack_trace = self._parse_exc(record.exc_info)
if record.exc_info:
if getattr(record, "tb_only", False):
cause = ""
else:
cause = ": ".join(_iter_causes(record.exc_info[1]))
return (
"{color}{levelname}{nc}: {description}" "{stack_trace}\n"
).format(
color=self.color_code.get(record.levelname, ""),
nc=colorama.Fore.RESET,
levelname=record.levelname,
description=self._description(record.msg, exception),
msg=record.msg,
stack_trace=stack_trace,
msg = "{message}{separator}{cause}".format(
message=msg or "",
separator=" - " if msg and cause else "",
cause=cause,
)
return "{color}{levelname}{nc}: {msg}".format(
color=self.color_code.get(record.levelname, ""),
if _is_verbose():
msg += _stack_trace(record.exc_info)
return "{asctime}{color}{levelname}{nc}: {msg}".format(
asctime=self.formatTime(record, self.datefmt),
color=self.color_code[record.levelname],
nc=colorama.Fore.RESET,
levelname=record.levelname,
msg=record.msg,
msg=msg,
)
def _current_level(self):
return logging.getLogger("dvc").getEffectiveLevel()
def formatTime(self, record, datefmt=None):
# only show if current level is set to DEBUG
# also, skip INFO as it is used for UI
if not _is_verbose() or record.levelno == logging.INFO:
return ""
def _is_visible(self, record):
return record.levelno >= self._current_level()
def _description(self, message, exception):
description = ""
if exception and message:
description = "{message} - {exception}"
elif exception:
description = "{exception}"
elif message:
description = "{message}"
return description.format(message=message, exception=exception)
def _walk_exc(self, exc_info):
import traceback
buffer = StringIO()
traceback.print_exception(*exc_info, file=buffer)
exc = exc_info[1]
tb = buffer.getvalue()
exc_list = [str(exc)]
tb_list = [tb]
# NOTE: parsing chained exceptions. See dvc/exceptions.py for more info
while hasattr(exc, "cause") and exc.cause:
exc_list.append(str(exc.cause))
if hasattr(exc, "cause_tb") and exc.cause_tb:
tb_list.insert(0, str(exc.cause_tb))
exc = exc.cause
return exc_list, tb_list
def _parse_exc(self, exc_info):
if not exc_info:
return (None, "")
exc_list, tb_list = self._walk_exc(exc_info)
exception = ": ".join(exc_list)
if self._current_level() == logging.DEBUG:
stack_trace = (
"\n" "{red}{line}{nc}\n" "{stack_trace}" "{red}{line}{nc}"
).format(
red=colorama.Fore.RED,
nc=colorama.Fore.RESET,
line="-" * 60,
stack_trace="\n".join(tb_list),
)
else:
stack_trace = ""
return (exception, stack_trace)
return "{green}{date}{nc} ".format(
green=colorama.Fore.GREEN,
date=super().formatTime(record, datefmt),
nc=colorama.Fore.RESET,
)
class LoggerHandler(logging.StreamHandler):
def handleError(self, record):
super(LoggerHandler, self).handleError(record)
super().handleError(record)
raise LoggingException(record)
def emit(self, record):
@@ -167,6 +115,40 @@ class LoggerHandler(logging.StreamHandler):
self.handleError(record)
def _is_verbose():
return logging.getLogger("dvc").getEffectiveLevel() == logging.DEBUG
def _iter_causes(exc):
while exc:
yield str(exc)
exc = exc.__cause__
def _stack_trace(exc_info):
import traceback
return (
"\n"
"{red}{line}{nc}\n"
"{trace}"
"{red}{line}{nc}".format(
red=colorama.Fore.RED,
line="-" * 60,
trace="".join(traceback.format_exception(*exc_info)),
nc=colorama.Fore.RESET,
)
)
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."):
logger.disabled = True
def setup(level=logging.INFO):
colorama.init()
@@ -209,22 +191,7 @@ def setup(level=logging.INFO):
"console_errors",
],
},
"paramiko": {
"level": logging.CRITICAL,
"handlers": [
"console_info",
"console_debug",
"console_errors",
],
},
"flufl.lock": {
"level": logging.CRITICAL,
"handlers": [
"console_info",
"console_debug",
"console_errors",
],
},
},
"disable_existing_loggers": False,
}
)
+18 -16
View File
@@ -1,5 +1,4 @@
"""Main entry point for dvc CLI."""
from __future__ import unicode_literals
import errno
import logging
@@ -7,17 +6,16 @@ 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 FOOTER
from dvc.logger import FOOTER, disable_other_loggers
from dvc.remote.pool import close_pools
from dvc.utils.compat import is_py2
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
# [2] https://bugs.python.org/issue29288
"".encode("idna")
@@ -34,7 +32,7 @@ def main(argv=None):
int: command's return code.
"""
args = None
cmd = None
disable_other_loggers()
outerLogLevel = logger.level
try:
@@ -59,15 +57,18 @@ 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, UnicodeError) and is_py2:
if isinstance(exc, OSError) and exc.errno == errno.EMFILE:
logger.exception(
"unicode is not supported in DVC for Python 2 "
"(end-of-life January 1, 2020), please upgrade to Python 3"
)
elif isinstance(exc, OSError) and exc.errno == errno.EMFILE:
logger.exception(
"too many open files, please increase your `ulimit`"
"too many open files, please visit "
"{} to see how to handle this "
"problem".format(
format_link("https://error.dvc.org/many-files")
),
extra={"tb_only": True},
)
else:
logger.exception("unexpected error")
@@ -75,8 +76,9 @@ def main(argv=None):
finally:
logger.setLevel(outerLogLevel)
# Python 2 fails to close these clean occasionally and users see
# weird error messages, so we do it manually
# Closing pools by-hand to prevent wierd messages when closing SSH
# connections. See https://github.com/iterative/dvc/issues/3248 for
# more info.
close_pools()
# Remove cached repos in the end of the call, these are anonymous
+62 -59
View File
@@ -1,37 +1,41 @@
from __future__ import unicode_literals
from urllib.parse import urlparse
from voluptuous import Any, Required
from voluptuous import And, Any, Coerce, Length, Lower, Required, 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.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 Remote
from dvc.remote.hdfs import RemoteHDFS
from dvc.remote.local import RemoteLOCAL
from dvc.remote.s3 import RemoteS3
from dvc.remote.hdfs import HDFSRemote
from dvc.remote.local import LocalRemote
from dvc.remote.s3 import S3Remote
from dvc.scheme import Schemes
from dvc.utils.compat import str
from dvc.utils.compat import urlparse
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(
None,
And(str, Length(max=0), SetTo(None)),
And(Any(str, And(int, Coerce(str))), Length(min=3), Lower),
)
# NOTE: currently there are only 3 possible checksum names:
#
# 1) md5 (LOCAL, SSH, GS);
@@ -40,23 +44,21 @@ OUTS_MAP = {
#
# so when a few types of outputs share the same name, we only need
# specify it once.
CHECKSUM_SCHEMA = {
RemoteLOCAL.PARAM_CHECKSUM: Any(str, None),
RemoteS3.PARAM_CHECKSUM: Any(str, None),
RemoteHDFS.PARAM_CHECKSUM: Any(str, None),
CHECKSUMS_SCHEMA = {
LocalRemote.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
S3Remote.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
HDFSRemote.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
}
TAGS_SCHEMA = {str: CHECKSUM_SCHEMA}
SCHEMA = CHECKSUM_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 = CHECKSUMS_SCHEMA.copy()
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, cache, metric, plot=False, persist=False):
parsed = urlparse(p)
if parsed.scheme == "remote":
@@ -68,8 +70,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:
@@ -81,29 +83,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,
@@ -111,24 +113,25 @@ 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
]
+119 -74
View File
@@ -1,57 +1,57 @@
from __future__ import unicode_literals
import logging
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
from dvc.exceptions import DvcException
from dvc.remote.base import RemoteBASE
from dvc.utils.compat import str
from dvc.utils.compat import urlparse
from dvc.exceptions import (
CollectCacheError,
DvcException,
RemoteCacheRequiredError,
)
from dvc.remote.base import BaseRemote
logger = logging.getLogger(__name__)
class OutputDoesNotExistError(DvcException):
def __init__(self, path):
msg = "output '{}' does not exist".format(path)
super(OutputDoesNotExistError, self).__init__(msg)
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)
super(OutputIsNotFileOrDirError, self).__init__(msg)
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)
super(OutputAlreadyTrackedError, self).__init__(msg)
msg = f"output '{path}' is already tracked by SCM (e.g. Git)"
super().__init__(msg)
class OutputIsStageFileError(DvcException):
def __init__(self, path):
super(OutputIsStageFileError, self).__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 = BaseRemote
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_PERSIST = "persist"
METRIC_SCHEMA = Any(
@@ -63,8 +63,6 @@ class OutputBase(object):
},
)
PARAM_TAGS = "tags"
DoesNotExistError = OutputDoesNotExistError
IsNotFileOrDirError = OutputIsNotFileOrDirError
IsStageFileError = OutputIsStageFileError
@@ -79,8 +77,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
@@ -100,17 +98,12 @@ class OutputBase(object):
self.remote = remote or self.REMOTE(self.repo, {})
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
self.plot = False if self.IS_DEPENDENCY else plot
self.persist = persist
self.tags = None if self.IS_DEPENDENCY else (tags or {})
if self.use_cache and self.cache is None:
raise DvcException(
"no cache location setup for '{}' outputs.".format(
self.REMOTE.scheme
)
)
self.path_info = self._parse_path(remote, path)
if self.use_cache and self.cache is None:
raise RemoteCacheRequiredError(self.path_info)
def _parse_path(self, remote, path):
if remote:
@@ -157,10 +150,21 @@ class OutputBase(object):
def cache_path(self):
return self.cache.checksum_to_path_info(self.checksum).url
@property
def checksum_type(self):
return self.remote.PARAM_CHECKSUM
@property
def checksum(self):
return self.info.get(self.remote.PARAM_CHECKSUM)
@checksum.setter
def checksum(self, checksum):
self.info[self.remote.PARAM_CHECKSUM] = checksum
def get_checksum(self):
return self.remote.get_checksum(self.path_info)
@property
def is_dir_checksum(self):
return self.remote.is_dir_checksum(self.checksum)
@@ -169,14 +173,17 @@ class OutputBase(object):
def exists(self):
return self.remote.exists(self.path_info)
def changed_checksum(self):
return self.checksum != self.remote.get_checksum(self.path_info)
def save_info(self):
return self.remote.save_info(self.path_info)
def changed_cache(self):
def changed_checksum(self):
return self.checksum != self.get_checksum()
def changed_cache(self, filter_info=None):
if not self.use_cache or not self.checksum:
return True
return self.cache.changed_cache(self.checksum)
return self.cache.changed_cache(self.checksum, filter_info=filter_info)
def status(self):
if self.checksum and self.use_cache and self.changed_cache():
@@ -208,6 +215,15 @@ class OutputBase(object):
def isfile(self):
return self.remote.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:
raise self.DoesNotExistError(self)
@@ -216,17 +232,13 @@ 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.repo.scm.ignore(self.fspath)
self.ignore()
if not self.use_cache:
self.info = self.remote.save_info(self.path_info)
if self.metric:
self.info = self.save_info()
if self.metric or self.plot:
self.verify_metric()
if not self.IS_DEPENDENCY:
logger.info(
@@ -239,12 +251,10 @@ class OutputBase(object):
assert not self.IS_DEPENDENCY
if not self.changed():
logger.info(
"Output '{}' didn't change. Skipping saving.".format(self)
)
logger.info(f"Output '{self}' didn't change. Skipping saving.")
return
self.info = self.remote.save_info(self.path_info)
self.info = self.save_info()
def commit(self):
if self.use_cache:
@@ -267,40 +277,41 @@ class OutputBase(object):
del self.metric[self.PARAM_METRIC_XPATH]
ret[self.PARAM_METRIC] = self.metric
ret[self.PARAM_PERSIST] = self.persist
if self.tags:
ret[self.PARAM_TAGS] = self.tags
if self.plot:
ret[self.PARAM_PLOT] = self.plot
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)
def checkout(
self, force=False, progress_callback=None, tag=None, relink=False
self,
force=False,
progress_callback=None,
relink=False,
filter_info=None,
):
if not self.use_cache:
if progress_callback:
progress_callback(str(self.path_info), self.get_files_number())
progress_callback(
str(self.path_info), self.get_files_number(filter_info)
)
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,
filter_info=filter_info,
)
def remove(self, ignore_remove=False):
@@ -324,17 +335,21 @@ class OutputBase(object):
if self.scheme == "local" and self.use_scm_ignore:
self.repo.scm.ignore(self.fspath)
def get_files_number(self):
def get_files_number(self, filter_info=None):
if not self.use_cache:
return 0
return self.cache.get_files_number(self.checksum)
return self.cache.get_files_number(
self.path_info, self.checksum, filter_info
)
def unprotect(self):
if self.exists:
self.remote.unprotect(self.path_info)
def _collect_used_dir_cache(self, remote=None, force=False, jobs=None):
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.
- Pull the directory entry from the remote cache if it was changed.
@@ -365,7 +380,7 @@ class OutputBase(object):
show_checksums=False,
)
except DvcException:
logger.debug("failed to pull cache for '{}'".format(self))
logger.debug(f"failed to pull cache for '{self}'")
if self.cache.changed_cache_file(self.checksum):
msg = (
@@ -381,10 +396,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]
path_info = self.path_info / entry[self.remote.PARAM_RELPATH]
cache.add(self.scheme, checksum, str(path_info))
entry_relpath = entry[self.remote.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
@@ -400,12 +426,29 @@ class OutputBase(object):
if not self.use_cache:
return NamedCache()
if not self.info:
logger.warning(
"Output '{}'({}) is missing version info. Cache for it will "
"not be collected. Use dvc repro to get your pipeline up to "
"date.".format(self, self.stage)
if self.stage.is_repo_import:
cache = NamedCache()
(dep,) = self.stage.deps
cache.external[dep.repo_pair].add(dep.def_path)
return cache
if not self.checksum:
msg = (
"Output '{}'({}) is missing version info. "
"Cache for it will not be collected. "
"Use `dvc repro` to get your pipeline up to date.".format(
self, self.stage
)
)
if self.exists:
msg += (
"\n"
"You can also use `dvc commit {stage.addressing}` "
"to associate existing '{out}' with {stage}.".format(
out=self, stage=self.stage
)
)
logger.warning(msg)
return NamedCache()
ret = NamedCache.make(self.scheme, self.checksum, str(self))
@@ -413,13 +456,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 -6
View File
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
from dvc.output.s3 import OutputS3
from dvc.remote.gs import RemoteGS
from dvc.output.s3 import S3Output
from dvc.remote.gs import GSRemote
class OutputGS(OutputS3):
REMOTE = RemoteGS
class GSOutput(S3Output):
REMOTE = GSRemote
+4 -6
View File
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
from dvc.output.base import OutputBase
from dvc.remote.hdfs import RemoteHDFS
from dvc.output.base import BaseOutput
from dvc.remote.hdfs import HDFSRemote
class OutputHDFS(OutputBase):
REMOTE = RemoteHDFS
class HDFSOutput(BaseOutput):
REMOTE = HDFSRemote
+20 -18
View File
@@ -1,26 +1,27 @@
from __future__ import unicode_literals
import logging
import os
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
from dvc.utils import relpath
from dvc.utils.compat import fspath_py35
from dvc.utils.compat import str
from dvc.utils.compat import urlparse
from dvc.utils.fs import path_isin
logger = logging.getLogger(__name__)
class OutputLOCAL(OutputBase):
REMOTE = RemoteLOCAL
class LocalOutput(BaseOutput):
REMOTE = LocalRemote
sep = os.sep
def __init__(self, stage, path, *args, **kwargs):
if stage and path_isin(path, stage.repo.root_dir):
path = relpath(path, stage.wdir)
super().__init__(stage, path, *args, **kwargs)
def _parse_path(self, remote, path):
parsed = urlparse(path)
if parsed.scheme == "remote":
@@ -36,7 +37,7 @@ class OutputLOCAL(OutputBase):
if not p.is_absolute():
p = self.stage.wdir / p
abs_p = os.path.abspath(os.path.normpath(fspath_py35(p)))
abs_p = os.path.abspath(os.path.normpath(p))
return self.REMOTE.path_cls(abs_p)
def __str__(self):
@@ -59,7 +60,7 @@ class OutputLOCAL(OutputBase):
return def_scheme != "remote" and not os.path.isabs(self.def_path)
def dumpd(self):
ret = super(OutputLOCAL, self).dumpd()
ret = super().dumpd()
if self.is_in_repo:
path = self.path_info.relpath(self.stage.wdir).as_posix()
else:
@@ -70,17 +71,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 DvcException(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 -6
View File
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
from dvc.output.base import OutputBase
from dvc.remote.s3 import RemoteS3
from dvc.output.base import BaseOutput
from dvc.remote.s3 import S3Remote
class OutputS3(OutputBase):
REMOTE = RemoteS3
class S3Output(BaseOutput):
REMOTE = S3Remote
+4 -6
View File
@@ -1,8 +1,6 @@
from __future__ import unicode_literals
from dvc.output.base import OutputBase
from dvc.remote.ssh import RemoteSSH
from dvc.output.base import BaseOutput
from dvc.remote.ssh import SSHRemote
class OutputSSH(OutputBase):
REMOTE = RemoteSSH
class SSHOutput(BaseOutput):
REMOTE = SSHRemote
+99 -69
View File
@@ -1,35 +1,23 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import os
import pathlib
import posixpath
import sys
from urllib.parse import urlparse
from funcy import cached_property
from dvc.utils import relpath
from dvc.utils.compat import basestring
from dvc.utils.compat import builtin_str
from dvc.utils.compat import is_py2
from dvc.utils.compat import pathlib
from dvc.utils.compat import str
from dvc.utils.compat import urlparse
# On Python 2.7/Windows sys.getfilesystemencoding() is set to mbcs,
# which is lossy, thus we can't use that,
# see https://github.com/mcmtroffaes/pathlib2/issues/56.
if is_py2:
fs_encoding = "utf-8"
class _BasePath(object):
class _BasePath:
def overlaps(self, other):
if isinstance(other, basestring):
if isinstance(other, (str, bytes)):
other = self.__class__(other)
elif self.__class__ != other.__class__:
return False
return self == other or self.isin(other) or other.isin(self)
return self.isin_or_eq(other) or other.isin(self)
def isin_or_eq(self, other):
return self == other or self.isin(other)
class PathInfo(pathlib.PurePath, _BasePath):
@@ -45,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
@@ -63,10 +47,9 @@ class PathInfo(pathlib.PurePath, _BasePath):
return relpath(path)
def __repr__(self):
return builtin_str("{}: '{}'").format(type(self).__name__, self)
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)
@@ -80,7 +63,7 @@ class PathInfo(pathlib.PurePath, _BasePath):
return self.__class__(relpath(self, other))
def isin(self, other):
if isinstance(other, basestring):
if isinstance(other, (str, bytes)):
other = self.__class__(other)
elif self.__class__ != other.__class__:
return False
@@ -88,34 +71,6 @@ class PathInfo(pathlib.PurePath, _BasePath):
n = len(other._cparts)
return len(self._cparts) > n and self._cparts[:n] == other._cparts
# pathlib2 uses bytes internally in Python 2, and we use unicode everywhere
# for paths in both pythons, thus we need this glue.
if is_py2:
__unicode__ = __str__
def __str__(self):
return self.__unicode__().encode(sys.getfilesystemencoding())
@classmethod
def _parse_args(cls, args):
args = [
a.encode(fs_encoding)
if isinstance(a, unicode) # noqa: F821
else a
for a in args
]
return super(PathInfo, cls)._parse_args(args)
@property
def name(self):
return super(PathInfo, self).name.decode(fs_encoding)
def __fspath__(self): # noqa: F811
return pathlib.PurePath.__str__(self).decode(fs_encoding)
def with_name(self, name):
return pathlib.PurePath.with_name(self, name.encode(fs_encoding))
class WindowsPathInfo(PathInfo, pathlib.PureWindowsPath):
pass
@@ -132,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
@@ -144,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):
@@ -155,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(
@@ -164,21 +119,21 @@ 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, (basestring, _URLPathInfo))
assert isinstance(path, (str, bytes, _URLPathInfo))
self.scheme, self.host, self.user = scheme, host, user
self.port = int(port) if port else self.DEFAULT_PORTS.get(self.scheme)
if isinstance(path, _URLPathInfo):
self._spath = builtin_str(path)
self._spath = str(path)
self._path = path
else:
if path and path[0] != "/":
@@ -198,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
@@ -207,7 +162,7 @@ class URLInfo(_BasePath):
return "{}: '{}'".format(type(self).__name__, self)
def __eq__(self, other):
if isinstance(other, basestring):
if isinstance(other, (str, bytes)):
other = self.__class__(other)
return (
self.__class__ == other.__class__
@@ -257,18 +212,18 @@ class URLInfo(_BasePath):
return _URLPathParents(self)
def relative_to(self, other):
if isinstance(other, basestring):
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)
def isin(self, other):
if isinstance(other, basestring):
if isinstance(other, (str, bytes)):
other = self.__class__(other)
elif self.__class__ != other.__class__:
return False
@@ -281,3 +236,78 @@ class CloudURLInfo(URLInfo):
@property
def path(self):
return self._spath.lstrip("/")
class HTTPURLInfo(URLInfo):
__hash__ = URLInfo.__hash__
def __init__(self, url):
p = urlparse(url)
stripped = p._replace(params=None, query=None, fragment=None)
super().__init__(stripped.geturl())
self.params = p.params
self.query = p.query
self.fragment = p.fragment
@classmethod
def from_parts(
cls,
scheme=None,
host=None,
user=None,
port=None,
path="",
netloc=None,
params=None,
query=None,
fragment=None,
):
assert bool(host) ^ bool(netloc)
if netloc is not None:
return cls(
"{}://{}{}{}{}{}".format(
scheme,
netloc,
path,
(";" + params) if params else "",
("?" + query) if query else "",
("#" + fragment) if fragment else "",
)
)
obj = cls.__new__(cls)
obj._fill_parts(scheme, host, user, port, path)
obj.params = params
obj.query = query
obj.fragment = fragment
return obj
@property
def _extra_parts(self):
return (self.params, self.query, self.fragment)
@property
def parts(self):
return self._base_parts + self._path.parts + self._extra_parts
@cached_property
def url(self):
return "{}://{}{}{}{}{}".format(
self.scheme,
self.netloc,
self._spath,
(";" + self.params) if self.params else "",
("?" + self.query) if self.query else "",
("#" + self.fragment) if self.fragment else "",
)
def __eq__(self, other):
if isinstance(other, (str, bytes)):
other = self.__class__(other)
return (
self.__class__ == other.__class__
and self._base_parts == other._base_parts
and self._path == other._path
and self._extra_parts == other._extra_parts
)
+53 -30
View File
@@ -1,11 +1,9 @@
"""Manages progress bars for dvc repo."""
from __future__ import print_function
"""Manages progress bars for DVC repo."""
import logging
import sys
from threading import RLock
from funcy import merge
from tqdm import tqdm
from dvc.utils import env2bool
@@ -21,18 +19,20 @@ 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
)
def __init__(
@@ -45,6 +45,8 @@ class Tqdm(tqdm):
bar_format=None,
bytes=False, # pylint: disable=W0622
file=None,
total=None,
postfix=None,
**kwargs
):
"""
@@ -53,23 +55,21 @@ class Tqdm(tqdm):
desc : persists after `close()`
level : effective logging level for determining `disable`;
used only if `disable` is unspecified
disable : If (default: None), will be determined by logging level.
disable : If (default: None) or False,
will be determined by logging level.
May be overridden to `True` due to non-TTY status.
Skip override by specifying env var `DVC_IGNORE_ISATTY`.
kwargs : anything accepted by `tqdm.tqdm()`
"""
kwargs = kwargs.copy()
kwargs.setdefault("unit_scale", True)
if bytes:
bytes_defaults = dict(
unit="B", unit_scale=True, unit_divisor=1024, miniters=1
)
kwargs = merge(bytes_defaults, kwargs)
kwargs = {**self.BYTES_DEFAULTS, **kwargs}
else:
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 disable is None:
if not disable:
disable = logger.getEffectiveLevel() > level
# auto-disable based on TTY
if (
@@ -78,15 +78,17 @@ class Tqdm(tqdm):
and hasattr(file, "isatty")
):
disable = not file.isatty()
super(Tqdm, self).__init__(
super().__init__(
iterable=iterable,
disable=disable,
leave=leave,
desc=desc,
bar_format="!",
lock_args=(False,),
total=total,
**kwargs
)
self.postfix = postfix or {"info": ""}
if bar_format is None:
if self.__len__():
self.bar_format = (
@@ -100,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):
@@ -112,22 +114,43 @@ 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)
super(Tqdm, self).close()
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
def format_dict(self):
"""inject `ncols_desc` to fill the display width (`ncols`)"""
d = super(Tqdm, self).format_dict
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 -7
View File
@@ -1,14 +1,9 @@
"""Manages user prompts."""
from __future__ import print_function
from __future__ import unicode_literals
import logging
import sys
from getpass import getpass
from dvc.utils.compat import input
logger = logging.getLogger(__name__)
@@ -43,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")
@@ -57,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("")
+60 -30
View File
@@ -1,44 +1,74 @@
from __future__ import unicode_literals
from .config import RemoteConfig
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
import posixpath
from urllib.parse import urlparse
from dvc.remote.azure import AzureRemote
from dvc.remote.gdrive import GDriveRemote
from dvc.remote.gs import GSRemote
from dvc.remote.hdfs import HDFSRemote
from dvc.remote.http import HTTPRemote
from dvc.remote.https import HTTPSRemote
from dvc.remote.local import LocalRemote
from dvc.remote.oss import OSSRemote
from dvc.remote.s3 import S3Remote
from dvc.remote.ssh import SSHRemote
REMOTES = [
RemoteAZURE,
RemoteGDrive,
RemoteGS,
RemoteHDFS,
RemoteHTTP,
RemoteHTTPS,
RemoteS3,
RemoteSSH,
RemoteOSS,
# NOTE: RemoteLOCAL is the default
AzureRemote,
GDriveRemote,
GSRemote,
HDFSRemote,
HTTPRemote,
HTTPSRemote,
S3Remote,
SSHRemote,
OSSRemote,
# NOTE: LocalRemote is the default
]
def _get(config):
def _get(remote_conf):
for remote in REMOTES:
if remote.supported(config):
if remote.supported(remote_conf):
return remote
return RemoteLOCAL
return LocalRemote
def Remote(repo, **kwargs):
name = kwargs.get("name")
if name:
remote_config = RemoteConfig(repo.config)
settings = remote_config.get_settings(name)
remote_conf = repo.config["remote"][name.lower()]
else:
settings = kwargs
return _get(settings)(repo, settings)
remote_conf = kwargs
remote_conf = _resolve_remote_refs(repo.config, remote_conf)
return _get(remote_conf)(repo, remote_conf)
def _resolve_remote_refs(config, remote_conf):
# Support for cross referenced remotes.
# This will merge the settings, shadowing base ref with remote_conf.
# For example, having:
#
# dvc remote add server ssh://localhost
# dvc remote modify server user root
# dvc remote modify server ask_password true
#
# dvc remote add images remote://server/tmp/pictures
# dvc remote modify images user alice
# dvc remote modify images ask_password false
# dvc remote modify images password asdf1234
#
# Results on a config dictionary like:
#
# {
# "url": "ssh://localhost/tmp/pictures",
# "user": "alice",
# "password": "asdf1234",
# "ask_password": False,
# }
parsed = urlparse(remote_conf["url"])
if parsed.scheme != "remote":
return remote_conf
base = config["remote"][parsed.netloc]
url = posixpath.join(base["url"], parsed.path.lstrip("/"))
return {**base, **remote_conf, "url": url}
+26 -19
View File
@@ -1,26 +1,22 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import logging
import os
import posixpath
import re
from datetime import datetime, timedelta
import threading
from datetime import datetime, timedelta
from urllib.parse import urlparse
from funcy import cached_property, wrap_prop
from dvc.config import Config
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import RemoteBASE
from dvc.remote.base import BaseRemote
from dvc.scheme import Schemes
from dvc.utils.compat import urlparse
logger = logging.getLogger(__name__)
class RemoteAZURE(RemoteBASE):
class AzureRemote(BaseRemote):
scheme = Schemes.AZURE
path_cls = CloudURLInfo
REGEX = (
@@ -32,11 +28,12 @@ class RemoteAZURE(RemoteBASE):
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(RemoteAZURE, self).__init__(repo, config)
super().__init__(repo, config)
url = config.get(Config.SECTION_REMOTE_URL, "azure://")
url = config.get("url", "azure://")
match = re.match(self.REGEX, url) # backward compatibility
path = match.group("path")
@@ -47,7 +44,7 @@ class RemoteAZURE(RemoteBASE):
)
self.connection_string = (
config.get(Config.SECTION_AZURE_CONNECTION_STRING)
config.get("connection_string")
or match.group("connection_string") # backward compatibility
or os.getenv("AZURE_STORAGE_CONNECTION_STRING")
)
@@ -70,12 +67,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
@@ -88,10 +85,10 @@ class RemoteAZURE(RemoteBASE):
if path_info.scheme != self.scheme:
raise NotImplementedError
logger.debug("Removing {}".format(path_info))
logger.debug(f"Removing {path_info}")
self.blob_service.delete_blob(path_info.bucket, path_info.path)
def _list_paths(self, bucket, prefix):
def _list_paths(self, bucket, prefix, progress_callback=None):
blob_service = self.blob_service
next_marker = None
while True:
@@ -100,6 +97,8 @@ class RemoteAZURE(RemoteBASE):
)
for blob in blobs:
if progress_callback:
progress_callback()
yield blob.name
if not blobs.next_marker:
@@ -107,8 +106,16 @@ 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 list_cache_paths(self, prefix=None, progress_callback=None):
if prefix:
prefix = posixpath.join(
self.path_info.path, prefix[:2], prefix[2:]
)
else:
prefix = self.path_info.path
return self._list_paths(
self.path_info.bucket, prefix, progress_callback
)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
+509 -176
View File
File diff suppressed because it is too large Load Diff
-157
View File
@@ -1,157 +0,0 @@
import logging
import os
import posixpath
from dvc.config import Config
from dvc.config import ConfigError
from dvc.utils import relpath
from dvc.utils.compat import urlparse
logger = logging.getLogger(__name__)
class RemoteConfig(object):
def __init__(self, config):
self.config = config
def get_settings(self, name):
"""
Args:
name (str): The name of the remote that we want to retrieve
Returns:
dict: The content beneath the given remote name.
Example:
>>> config = {'remote "server"': {'url': 'ssh://localhost/'}}
>>> get_settings("server")
{'url': 'ssh://localhost/'}
"""
settings = self.config.config.get(
Config.SECTION_REMOTE_FMT.format(name.lower())
)
if settings is None:
raise ConfigError(
"unable to find remote section '{}'".format(name)
)
parsed = urlparse(settings["url"])
# Support for cross referenced remotes.
# This will merge the settings, giving priority to the outer reference.
# For example, having:
#
# dvc remote add server ssh://localhost
# dvc remote modify server user root
# dvc remote modify server ask_password true
#
# dvc remote add images remote://server/tmp/pictures
# dvc remote modify images user alice
# dvc remote modify images ask_password false
# dvc remote modify images password asdf1234
#
# Results on a config dictionary like:
#
# {
# "url": "ssh://localhost/tmp/pictures",
# "user": "alice",
# "password": "asdf1234",
# "ask_password": False,
# }
#
if parsed.scheme == "remote":
reference = self.get_settings(parsed.netloc)
url = posixpath.join(reference["url"], parsed.path.lstrip("/"))
merged = reference.copy()
merged.update(settings)
merged["url"] = url
return merged
return settings
@staticmethod
def resolve_path(path, config_file):
"""Resolve path relative to config file location.
Args:
path: Path to be resolved.
config_file: Path to config file, which `path` is specified
relative to.
Returns:
Path relative to the `config_file` location. If `path` is an
absolute path then it will be returned without change.
"""
if os.path.isabs(path):
return path
return relpath(path, os.path.dirname(config_file))
def add(self, name, url, default=False, force=False, level=None):
from dvc.remote import _get, RemoteLOCAL
configobj = self.config.get_configobj(level)
remote = _get({Config.SECTION_REMOTE_URL: url})
if remote == RemoteLOCAL and not url.startswith("remote://"):
url = self.resolve_path(url, configobj.filename)
self.config.set(
Config.SECTION_REMOTE_FMT.format(name),
Config.SECTION_REMOTE_URL,
url,
force=force,
level=level,
)
if default:
self.config.set(
Config.SECTION_CORE,
Config.SECTION_CORE_REMOTE,
name,
level=level,
)
def remove(self, name, level=None):
self.config.unset(Config.SECTION_REMOTE_FMT.format(name), level=level)
if level is None:
level = Config.LEVEL_REPO
for lev in [
Config.LEVEL_LOCAL,
Config.LEVEL_REPO,
Config.LEVEL_GLOBAL,
Config.LEVEL_SYSTEM,
]:
self.config.unset(
Config.SECTION_CORE,
Config.SECTION_CORE_REMOTE,
level=lev,
force=True,
)
if lev == level:
break
def modify(self, name, option, value, level=None):
self.config.set(
Config.SECTION_REMOTE_FMT.format(name), option, value, level=level
)
def list(self, level=None):
return self.config.list_options(
Config.SECTION_REMOTE_REGEX, Config.SECTION_REMOTE_URL, level=level
)
def set_default(self, name, unset=False, level=None):
if unset:
self.config.unset(Config.SECTION_CORE, Config.SECTION_CORE_REMOTE)
return
self.config.set(
Config.SECTION_CORE, Config.SECTION_CORE_REMOTE, name, level=level
)
def get_default(self, level=None):
return self.config.get(
Config.SECTION_CORE, Config.SECTION_CORE_REMOTE, level=level
)
+482 -227
View File
@@ -1,99 +1,363 @@
from __future__ import unicode_literals
import logging
import os
import posixpath
import logging
import re
import threading
from collections import defaultdict
from urllib.parse import urlparse
from funcy import retry, compose, decorator, wrap_with
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.path_info import CloudURLInfo
from dvc.remote.base import RemoteBASE
from dvc.config import Config
from dvc.exceptions import DvcException
from dvc.utils import tmp_fname
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import BaseRemote
from dvc.scheme import Schemes
from dvc.utils import format_link, tmp_fname
logger = logging.getLogger(__name__)
FOLDER_MIME_TYPE = "application/vnd.google-apps.folder"
class GDriveRetriableError(DvcException):
def __init__(self, msg):
super(GDriveRetriableError, self).__init__(msg)
class GDrivePathNotFound(DvcException):
def __init__(self, path_info, hint):
hint = "" if hint is None else f" {hint}"
super().__init__(f"GDrive path '{path_info}' not found.{hint}")
@decorator
def _wrap_pydrive_retriable(call):
from apiclient import errors
from pydrive.files import ApiRequestError
class GDriveAuthError(DvcException):
def __init__(self, cred_location):
try:
result = call()
except (ApiRequestError, errors.HttpError) as exception:
retry_codes = ["403", "500", "502", "503", "504"]
if any(
"HttpError {}".format(code) in str(exception)
for code in retry_codes
):
raise GDriveRetriableError(msg="Google API request failed")
raise
return result
if cred_location:
message = (
"GDrive remote auth failed with credentials in '{}'.\n"
"Backup first, remove of 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)
gdrive_retry = compose(
# 8 tries, start at 0.5s, multiply by golden ratio, cap at 10s
retry(
8, GDriveRetriableError, timeout=lambda a: min(0.5 * 1.618 ** a, 10)
),
_wrap_pydrive_retriable,
)
def _gdrive_retry(func):
def should_retry(exc):
from pydrive2.files import ApiRequestError
if not isinstance(exc, ApiRequestError):
return False
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(
16,
timeout=lambda a: min(0.5 * 1.618 ** a, 20),
filter_errors=should_retry,
)(func)
class RemoteGDrive(RemoteBASE):
class GDriveURLInfo(CloudURLInfo):
def __init__(self, url):
super().__init__(url)
# GDrive URL host part is case sensitive,
# we are restoring it here.
p = urlparse(url)
self.host = p.netloc
assert self.netloc == self.host
# Normalize path. Important since we have a cache (path to ID)
# and don't want to deal with different variations of path in it.
self._spath = re.sub("/{2,}", "/", self._spath.rstrip("/"))
class GDriveRemote(BaseRemote):
scheme = Schemes.GDRIVE
path_cls = CloudURLInfo
REQUIRES = {"pydrive": "pydrive"}
GDRIVE_USER_CREDENTIALS_DATA = "GDRIVE_USER_CREDENTIALS_DATA"
path_cls = GDriveURLInfo
REQUIRES = {"pydrive2": "pydrive2"}
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"
def __init__(self, repo, config):
super(RemoteGDrive, self).__init__(repo, config)
self.no_traverse = False
self.path_info = self.path_cls(config[Config.SECTION_REMOTE_URL])
self.config = config
self.init_drive()
DEFAULT_GDRIVE_CLIENT_ID = "710796635688-iivsgbgsb6uv1fap6635dhvuei09o66c.apps.googleusercontent.com" # noqa: E501
DEFAULT_GDRIVE_CLIENT_SECRET = "a1Fz59uTpVNeG_VGuSKDLJXv"
def init_drive(self):
self.client_id = self.config.get(Config.SECTION_GDRIVE_CLIENT_ID, None)
self.client_secret = self.config.get(
Config.SECTION_GDRIVE_CLIENT_SECRET, None
)
if not self.client_id or not self.client_secret:
def __init__(self, repo, config):
super().__init__(repo, config)
self.path_info = self.path_cls(config["url"])
if not self.path_info.bucket:
raise DvcException(
"Please specify Google Drive's client id and "
"secret in DVC's config. Learn more at "
"https://man.dvc.org/remote/add."
"Empty GDrive URL '{}'. Learn more at {}".format(
config["url"],
format_link("https://man.dvc.org/remote/add"),
)
)
self.gdrive_user_credentials_path = (
tmp_fname(".dvc/tmp/")
if os.getenv(RemoteGDrive.GDRIVE_USER_CREDENTIALS_DATA)
else self.config.get(
Config.SECTION_GDRIVE_USER_CREDENTIALS_FILE,
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"
)
self._service_account_user_email = config.get(
"gdrive_service_account_user_email"
)
self._service_account_p12_file_path = config.get(
"gdrive_service_account_p12_file_path"
)
self._client_id = config.get("gdrive_client_id")
self._client_secret = config.get("gdrive_client_secret")
self._validate_config()
self._gdrive_user_credentials_path = (
tmp_fname(os.path.join(self.repo.tmp_dir, ""))
if os.getenv(GDriveRemote.GDRIVE_CREDENTIALS_DATA)
else config.get(
"gdrive_user_credentials_file",
os.path.join(
self.repo.tmp_dir, self.DEFAULT_USER_CREDENTIALS_FILE
),
)
)
@gdrive_retry
def gdrive_upload_file(
self, args, no_progress_bar=True, from_file="", progress_name=""
def _validate_config(self):
# Validate Service Account configuration
if self._use_service_account and (
not self._service_account_email
or not self._service_account_p12_file_path
):
raise DvcException(
"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:
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(GDriveRemote.GDRIVE_CREDENTIALS_DATA):
return GDriveRemote.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(GDriveRemote.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 GoogleAuth
from pydrive2.drive import GoogleDrive
if os.getenv(GDriveRemote.GDRIVE_CREDENTIALS_DATA):
with open(self._gdrive_user_credentials_path, "w") as cred_file:
cred_file.write(
os.getenv(GDriveRemote.GDRIVE_CREDENTIALS_DATA)
)
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:
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:
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.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()
GDriveRemote._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 GDriveAuthError(self.credentials_location) from exc
finally:
if os.getenv(GDriveRemote.GDRIVE_CREDENTIALS_DATA):
os.remove(self._gdrive_user_credentials_path)
return GoogleDrive(gauth)
@wrap_prop(threading.RLock())
@cached_property
def _ids_cache(self):
cache = {
"dirs": defaultdict(list),
"ids": {},
"root_id": self._get_item_id(
self.path_info,
use_cache=False,
hint="Confirm the directory exists and you can access it.",
),
}
self._cache_path_id(self.path_info.path, cache["root_id"], cache)
for item in self._gdrive_list(
"'{}' in parents and trashed=false".format(cache["root_id"])
):
item_path = (self.path_info / item["title"]).path
self._cache_path_id(item_path, item["id"], cache)
return cache
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):
params = {"corpora": "default"}
if self._bucket != "root" and self._bucket != "appDataFolder":
drive_id = self._gdrive_shared_drive_id(self._bucket)
if drive_id:
logger.debug(
"GDrive remote '{}' is using shared drive id '{}'.".format(
self.path_info, drive_id
)
)
params["driveId"] = drive_id
params["corpora"] = "drive"
return params
@_gdrive_retry
def _gdrive_shared_drive_id(self, item_id):
from pydrive2.files import ApiRequestError
param = {"id": item_id}
# it does not create a file on the remote
item = self._drive.CreateFile(param)
# ID of the shared drive the item resides in.
# Only populated for items in shared drives.
try:
item.FetchMetadata("driveId")
except ApiRequestError as exc:
error_code = exc.error.get("code", 0)
if error_code == 404:
raise DvcException(
"'{}' for '{}':\n\n"
"1. Confirm the directory exists and you can access it.\n"
"2. Make sure that credentials in '{}'\n"
" are correct for this remote e.g. "
"use the `gdrive_user_credentials_file` config\n"
" option if you use multiple GDrive remotes with "
"different email accounts.\n\nDetails".format(
item_id, self.path_info, self._credentials_location
)
) from exc
raise
return item.get("driveId", None)
@_gdrive_retry
def _gdrive_upload_file(
self,
parent_id,
title,
no_progress_bar=False,
from_file="",
progress_name="",
):
item = self.drive.CreateFile(
{"title": args["title"], "parents": [{"id": args["parent_id"]}]}
item = self._drive.CreateFile(
{"title": title, "parents": [{"id": parent_id}]}
)
with open(from_file, "rb") as fobj:
@@ -112,197 +376,188 @@ 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
):
gdrive_file = self.drive.CreateFile({"id": file_id})
bar_format = (
"Donwloading {desc:{ncols_desc}.{ncols_desc}}... "
+ Tqdm.format_sizeof(int(gdrive_file["fileSize"]), "B", 1024)
)
param = {"id": item_id}
# it does not create a file on the remote
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):
file_list = self.drive.ListFile({"q": query, "maxResults": 1000})
@_gdrive_retry
def _gdrive_delete_file(self, item_id):
from pydrive2.files import ApiRequestError
# Isolate and decorate fetching of remote drive items in pages
get_list = gdrive_retry(lambda: next(file_list, None))
param = {"id": item_id}
# it does not create a file on the remote
item = self._drive.CreateFile(param)
# Fetch pages until None is received, lazily flatten the thing
try:
item.Trash() if self._trash_only else item.Delete()
except ApiRequestError as exc:
http_error_code = exc.error.get("code", 0)
if (
http_error_code == 403
and self._list_params["corpora"] == "drive"
and exc.GetField("location") == "file.permissions"
):
raise DvcException(
"Insufficient permissions to {}. You should have {} "
"access level for the used shared drive. More details "
"at {}.".format(
"move the file into Trash"
if self._trash_only
else "permanently delete the file",
"Manager or Content Manager"
if self._trash_only
else "Manager",
"https://support.google.com/a/answer/7337554",
)
) from exc
raise
def _gdrive_list(self, query):
param = {"q": query, "maxResults": 1000}
param.update(self._list_params)
file_list = self._drive.ListFile(param)
# Isolate and decorate fetching of remote drive items in pages.
get_list = _gdrive_retry(lambda: next(file_list, None))
# Fetch pages until None is received, lazily flatten the thing.
return cat(iter(get_list, None))
def cache_root_dirs(self):
cached_dirs = {}
cached_ids = {}
for dir1 in self.gdrive_list_item(
"'{}' in parents and trashed=false".format(self.root_id)
):
remote_path = posixpath.join(self.path_info.path, dir1["title"])
cached_dirs.setdefault(remote_path, []).append(dir1["id"])
cached_ids[dir1["id"]] = dir1["title"]
return cached_dirs, cached_ids
@property
def cached_dirs(self):
if not hasattr(self, "_cached_dirs"):
self.drive
return self._cached_dirs
@property
def cached_ids(self):
if not hasattr(self, "_cached_ids"):
self.drive
return self._cached_ids
@property
@wrap_with(threading.RLock())
def drive(self):
if not hasattr(self, "_gdrive"):
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
if os.getenv(RemoteGDrive.GDRIVE_USER_CREDENTIALS_DATA):
with open(
self.gdrive_user_credentials_path, "w"
) as credentials_file:
credentials_file.write(
os.getenv(RemoteGDrive.GDRIVE_USER_CREDENTIALS_DATA)
)
GoogleAuth.DEFAULT_SETTINGS["client_config_backend"] = "settings"
GoogleAuth.DEFAULT_SETTINGS["client_config"] = {
"client_id": self.client_id,
"client_secret": self.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
gauth = GoogleAuth(settings_file="")
gauth.CommandLineAuth()
if os.getenv(RemoteGDrive.GDRIVE_USER_CREDENTIALS_DATA):
os.remove(self.gdrive_user_credentials_path)
self._gdrive = GoogleDrive(gauth)
self.root_id = self.get_remote_id(self.path_info, create=True)
self._cached_dirs, self._cached_ids = self.cache_root_dirs()
return self._gdrive
@gdrive_retry
def create_remote_dir(self, parent_id, title):
item = self.drive.CreateFile(
{
"title": title,
"parents": [{"id": parent_id}],
"mimeType": FOLDER_MIME_TYPE,
}
@_gdrive_retry
def _gdrive_create_dir(self, parent_id, title):
parent = {"id": parent_id}
item = self._drive.CreateFile(
{"title": title, "parents": [parent], "mimeType": FOLDER_MIME_TYPE}
)
item.Upload()
return item
@gdrive_retry
def get_remote_item(self, name, parents_ids):
if not parents_ids:
@wrap_with(threading.RLock())
def _create_dir(self, parent_id, title, remote_path):
cached = self._ids_cache["dirs"].get(remote_path)
if cached:
return cached[0]
item = self._gdrive_create_dir(parent_id, title)
if parent_id == self._ids_cache["root_id"]:
self._cache_path_id(remote_path, item["id"])
return item["id"]
def _get_remote_item_ids(self, parent_ids, title):
if not parent_ids:
return None
query = " or ".join(
"'{}' in parents".format(parent_id) for parent_id in parents_ids
query = "trashed=false and ({})".format(
" or ".join(
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
]
# Limit found remote items count to 1 in response
item_list = self.drive.ListFile(
{"q": query, "maxResults": 1}
).GetList()
return next(iter(item_list), None)
def resolve_remote_item_from_path(self, path_parts, create):
parents_ids = ["root"]
current_path = ""
for path_part in path_parts:
current_path = posixpath.join(current_path, path_part)
remote_ids = self.get_remote_id_from_cache(current_path)
if remote_ids:
parents_ids = remote_ids
continue
item = self.get_remote_item(path_part, parents_ids)
if not item and create:
item = self.create_remote_dir(parents_ids[0], path_part)
elif not item:
return None
parents_ids = [item["id"]]
return item
def get_remote_id_from_cache(self, remote_path):
if hasattr(self, "_cached_dirs"):
return self.cached_dirs.get(remote_path, [])
def _get_cached_item_ids(self, path, use_cache):
if not path:
return [self._bucket]
if use_cache:
return self._ids_cache["dirs"].get(path, [])
return []
def get_remote_id(self, path_info, create=False):
remote_ids = self.get_remote_id_from_cache(path_info.path)
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
if remote_ids:
return remote_ids[0]
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
file1 = self.resolve_remote_item_from_path(
path_info.path.split("/"), create
return (
[self._create_dir(min(parent_ids), title, path)] if create else []
)
return file1["id"] if file1 else ""
def _get_item_id(self, path_info, create=False, use_cache=True, hint=None):
assert path_info.bucket == self._bucket
item_ids = self._path_to_item_ids(path_info.path, create, use_cache)
if item_ids:
return min(item_ids)
assert not create
raise GDrivePathNotFound(path_info, hint)
def exists(self, path_info):
return self.get_remote_id(path_info) != ""
def _upload(self, from_file, to_info, name, no_progress_bar):
dirname = to_info.parent
if dirname:
parent_id = self.get_remote_id(dirname, True)
try:
self._get_item_id(path_info)
except GDrivePathNotFound:
return False
else:
parent_id = to_info.bucket
return True
self.gdrive_upload_file(
{"title": to_info.name, "parent_id": parent_id},
no_progress_bar,
from_file,
name,
def _upload(self, from_file, to_info, name=None, no_progress_bar=False):
dirname = to_info.parent
assert dirname
parent_id = self._get_item_id(dirname, True)
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 _download(self, from_info, to_file, name=None, no_progress_bar=False):
item_id = self._get_item_id(from_info)
self._gdrive_download_file(item_id, to_file, name, no_progress_bar)
def all(self):
if not self.cached_ids:
def list_cache_paths(self, prefix=None, progress_callback=None):
if not self._ids_cache["ids"]:
return
query = " or ".join(
"'{}' in parents".format(dir_id) for dir_id in self.cached_ids
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"
query += " and trashed=false"
for file1 in self.gdrive_list_item(query):
parent_id = file1["parents"][0]["id"]
path = posixpath.join(self.cached_ids[parent_id], file1["title"])
try:
yield self.path_to_checksum(path)
except ValueError:
# We ignore all the non-cache looking files
logger.debug('Ignoring path as "non-cache looking"')
for item in self._gdrive_list(query):
if progress_callback:
progress_callback()
parent_id = item["parents"][0]["id"]
yield posixpath.join(
self._ids_cache["ids"][parent_id], item["title"]
)
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 walk_files(self, path_info):
raise NotImplementedError
+32 -23
View File
@@ -1,21 +1,17 @@
from __future__ import unicode_literals, division
import logging
import os.path
import posixpath
import threading
from datetime import timedelta
from functools import wraps
import io
import os.path
import threading
from funcy import cached_property, wrap_prop
from dvc.config import Config
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 BaseRemote
from dvc.scheme import Schemes
from dvc.utils.compat import FileNotFoundError # skipcq: PYL-W0622
logger = logging.getLogger(__name__)
@@ -55,10 +51,10 @@ def _upload_to_bucket(
to_info,
chunk_size=None,
name=None,
no_progress_bar=True,
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",
@@ -69,20 +65,20 @@ def _upload_to_bucket(
blob.upload_from_file(wrapped)
class RemoteGS(RemoteBASE):
class GSRemote(BaseRemote):
scheme = Schemes.GS
path_cls = CloudURLInfo
REQUIRES = {"google-cloud-storage": "google.cloud.storage"}
PARAM_CHECKSUM = "md5"
def __init__(self, repo, config):
super(RemoteGS, self).__init__(repo, config)
super().__init__(repo, config)
url = config.get(Config.SECTION_REMOTE_URL, "gs:///")
url = config.get("url", "gs:///")
self.path_info = self.path_cls(url)
self.projectname = config.get(Config.SECTION_GCP_PROJECTNAME, None)
self.credentialpath = config.get(Config.SECTION_GCP_CREDENTIALPATH)
self.projectname = config.get("projectname", None)
self.credentialpath = config.get("credentialpath")
@wrap_prop(threading.Lock())
@cached_property
@@ -113,7 +109,7 @@ class RemoteGS(RemoteBASE):
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)
msg = f"'{from_info.path}' doesn't exist in the cloud"
raise DvcException(msg)
to_bucket = self.gs.bucket(to_info.bucket)
@@ -123,21 +119,31 @@ class RemoteGS(RemoteBASE):
if path_info.scheme != "gs":
raise NotImplementedError
logger.debug("Removing gs://{}".format(path_info))
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 _list_paths(self, path_info, max_items=None):
def _list_paths(
self, path_info, max_items=None, prefix=None, progress_callback=None
):
if prefix:
prefix = posixpath.join(path_info.path, prefix[:2], prefix[2:])
else:
prefix = path_info.path
for blob in self.gs.bucket(path_info.bucket).list_blobs(
prefix=path_info.path, max_results=max_items
):
if progress_callback:
progress_callback()
yield blob.name
def list_cache_paths(self):
return self._list_paths(self.path_info)
def list_cache_paths(self, prefix=None, progress_callback=None):
return self._list_paths(
self.path_info, prefix=prefix, progress_callback=progress_callback
)
def walk_files(self, path_info):
for fname in self._list_paths(path_info / ""):
@@ -147,6 +153,9 @@ class RemoteGS(RemoteBASE):
yield path_info.replace(fname)
def makedirs(self, path_info):
if not path_info.path:
return
self.gs.bucket(path_info.bucket).blob(
(path_info / "").path
).upload_from_string("")
@@ -170,7 +179,7 @@ class RemoteGS(RemoteBASE):
"""
return self.isfile(path_info) or self.isdir(path_info)
def _upload(self, from_file, to_info, name=None, no_progress_bar=True):
def _upload(self, from_file, to_info, name=None, no_progress_bar=False):
bucket = self.gs.bucket(to_info.bucket)
_upload_to_bucket(
bucket,
@@ -180,10 +189,10 @@ class RemoteGS(RemoteBASE):
no_progress_bar=no_progress_bar,
)
def _download(self, from_info, to_file, name=None, no_progress_bar=True):
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",
+34 -23
View File
@@ -1,40 +1,38 @@
from __future__ import unicode_literals
from dvc.utils.compat import FileNotFoundError, urlparse
import io
import logging
import os
import posixpath
import re
import subprocess
from collections import deque
from contextlib import closing, contextmanager
import subprocess
from urllib.parse import urlparse
from .base import RemoteBASE, RemoteCmdError
from .pool import get_connection
from dvc.config import Config
from dvc.scheme import Schemes
from dvc.utils import fix_env, tmp_fname
from .base import BaseRemote, RemoteCmdError
from .pool import get_connection
logger = logging.getLogger(__name__)
class RemoteHDFS(RemoteBASE):
class HDFSRemote(BaseRemote):
scheme = Schemes.HDFS
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
PARAM_CHECKSUM = "checksum"
REQUIRES = {"pyarrow": "pyarrow"}
TRAVERSE_PREFIX_LEN = 2
def __init__(self, repo, config):
super(RemoteHDFS, self).__init__(repo, config)
super().__init__(repo, config)
self.path_info = None
url = config.get(Config.SECTION_REMOTE_URL)
url = config.get("url")
if not url:
return
parsed = urlparse(url)
user = parsed.username or config.get(Config.SECTION_REMOTE_USER)
user = parsed.username or config.get("user")
self.path_info = self.path_cls.from_parts(
scheme=self.scheme,
@@ -57,7 +55,7 @@ class RemoteHDFS(RemoteBASE):
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.
@@ -89,7 +87,7 @@ 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")
@@ -114,7 +112,7 @@ class RemoteHDFS(RemoteBASE):
raise NotImplementedError
if self.exists(path_info):
logger.debug("Removing {}".format(path_info.path))
logger.debug(f"Removing {path_info.path}")
with self.hdfs(path_info) as hdfs:
hdfs.rm(path_info.path)
@@ -149,23 +147,36 @@ class RemoteHDFS(RemoteBASE):
yield fd
else:
yield io.TextIOWrapper(fd, encoding=encoding)
except IOError as e:
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 list_cache_paths(self):
def list_cache_paths(self, prefix=None, progress_callback=None):
if not self.exists(self.path_info):
return
dirs = deque([self.path_info.path])
if prefix:
root = posixpath.join(self.path_info.path, prefix[:2])
else:
root = self.path_info.path
dirs = deque([root])
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
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":
if progress_callback:
progress_callback()
yield urlparse(entry["name"]).path
except OSError as e:
# When searching for a specific prefix pyarrow raises an
# exception if the specified cache dir does not exist
if not prefix:
raise e
+107 -28
View File
@@ -1,56 +1,96 @@
from __future__ import unicode_literals
import logging
import os.path
import threading
from funcy import cached_property, wrap_prop
from funcy import cached_property, memoize, wrap_prop, wrap_with
from dvc.config import Config
from dvc.config import ConfigError
import dvc.prompt as prompt
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 BaseRemote
from dvc.scheme import Schemes
from dvc.utils.compat import open
logger = logging.getLogger(__name__)
class RemoteHTTP(RemoteBASE):
@wrap_with(threading.Lock())
@memoize
def ask_password(host, user):
return prompt.password(
"Enter a password for "
"host '{host}' user '{user}'".format(host=host, user=user)
)
class HTTPRemote(BaseRemote):
scheme = Schemes.HTTP
path_cls = HTTPURLInfo
SESSION_RETRIES = 5
SESSION_BACKOFF_FACTOR = 0.1
REQUEST_TIMEOUT = 10
CHUNK_SIZE = 2 ** 16
PARAM_CHECKSUM = "etag"
CAN_TRAVERSE = False
def __init__(self, repo, config):
super(RemoteHTTP, self).__init__(repo, config)
super().__init__(repo, config)
url = config.get(Config.SECTION_REMOTE_URL)
self.path_info = self.path_cls(url) if url else None
url = config.get("url")
if 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:
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.write(chunk)
pbar.update(len(chunk))
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)
def exists(self, path_info):
return bool(self._request("HEAD", path_info.url))
@@ -78,6 +118,24 @@ class RemoteHTTP(RemoteBASE):
return etag
def auth_method(self, path_info=None):
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
if path_info is None:
path_info = self.path_info
if self.auth:
if self.ask_password and self.password is None:
host, user = path_info.host, path_info.user
self.password = ask_password(host, user)
if self.auth == "basic":
return HTTPBasicAuth(path_info.user, self.password)
if self.auth == "digest":
return HTTPDigestAuth(path_info.user, self.password)
if self.auth == "custom" and self.custom_auth_header:
self.headers.update({self.custom_auth_header: self.password})
return None
@wrap_prop(threading.Lock())
@cached_property
def _session(self):
@@ -104,9 +162,30 @@ class RemoteHTTP(RemoteBASE):
kwargs.setdefault("timeout", self.REQUEST_TIMEOUT)
try:
return self._session.request(method, url, **kwargs)
res = self._session.request(
method,
url,
auth=self.auth_method(),
headers=self.headers,
**kwargs,
)
redirect_no_location = (
kwargs["allow_redirects"]
and res.status_code in (301, 302)
and "location" not in res.headers
)
if redirect_no_location:
# AWS s3 doesn't like to add a location header to its redirects
# from https://s3.amazonaws.com/<bucket name>/* type URLs.
# This should be treated as an error
raise requests.exceptions.RequestException
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
+3 -4
View File
@@ -1,8 +1,7 @@
from __future__ import unicode_literals
from .http import RemoteHTTP
from dvc.scheme import Schemes
from .http import HTTPRemote
class RemoteHTTPS(RemoteHTTP):
class HTTPSRemote(HTTPRemote):
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
+309 -109
View File
@@ -1,72 +1,52 @@
from __future__ import unicode_literals
import errno
import logging
import os
import stat
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor, as_completed
from functools import partial
from funcy import cached_property, concat
from shortuuid import uuid
from dvc.config import Config
from dvc.exceptions import DownloadError
from dvc.exceptions import DvcException
from dvc.exceptions import 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
from dvc.remote.base import STATUS_DELETED
from dvc.remote.base import STATUS_MAP
from dvc.remote.base import STATUS_MISSING
from dvc.remote.base import STATUS_NEW
from dvc.remote.base import (
STATUS_DELETED,
STATUS_MAP,
STATUS_MISSING,
STATUS_NEW,
BaseRemote,
index_locked,
)
from dvc.remote.index import RemoteIndexNoop
from dvc.scheme import Schemes
from dvc.scm.tree import is_working_tree
from dvc.system import System
from dvc.utils import copyfile
from dvc.utils import file_md5
from dvc.utils import makedirs
from dvc.utils import relpath
from dvc.utils import tmp_fname
from dvc.utils import walk_files
from dvc.utils.compat import fspath_py35
from dvc.utils.compat import open
from dvc.utils.compat import str
from dvc.utils.fs import move
from dvc.utils.fs import remove
from dvc.utils import file_md5, relpath, tmp_fname
from dvc.utils.fs import copyfile, makedirs, move, remove, walk_files
logger = logging.getLogger(__name__)
class RemoteLOCAL(RemoteBASE):
class LocalRemote(BaseRemote):
scheme = Schemes.LOCAL
path_cls = PathInfo
PARAM_CHECKSUM = "md5"
PARAM_PATH = "path"
TRAVERSE_PREFIX_LEN = 2
INDEX_CLS = RemoteIndexNoop
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(RemoteLOCAL, self).__init__(repo, config)
self.protected = config.get(Config.SECTION_CACHE_PROTECTED, False)
shared = config.get(Config.SECTION_CACHE_SHARED)
self._file_mode, self._dir_mode = self.SHARED_MODE_MAP[shared]
if self.protected:
# cache files are set to be read-only for everyone
self._file_mode = stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
cache_dir = config.get(Config.SECTION_REMOTE_URL)
if cache_dir is not None and not os.path.isabs(cache_dir):
cwd = config[Config.PRIVATE_CWD]
cache_dir = os.path.abspath(os.path.join(cwd, cache_dir))
self.path_info = PathInfo(cache_dir) if cache_dir else None
super().__init__(repo, config)
self.cache_dir = config.get("url")
self._dir_info = {}
@property
@@ -77,13 +57,33 @@ class RemoteLOCAL(RemoteBASE):
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):
@cached_property
def cache_path(self):
return os.path.abspath(self.cache_dir)
def checksum_to_path(self, checksum):
return os.path.join(self.cache_path, checksum[0:2], checksum[2:])
def list_cache_paths(self, prefix=None, progress_callback=None):
assert self.path_info is not None
return walk_files(self.path_info, None)
if prefix:
path_info = self.path_info / prefix[:2]
else:
path_info = self.path_info
if progress_callback:
for path in walk_files(path_info):
progress_callback()
yield path
else:
yield from walk_files(path_info)
def get(self, md5):
if not md5:
@@ -91,10 +91,10 @@ class RemoteLOCAL(RemoteBASE):
return self.checksum_to_path_info(md5).url
@staticmethod
def exists(path_info):
assert path_info.scheme == "local"
return os.path.lexists(fspath_py35(path_info))
def exists(self, path_info):
assert is_working_tree(self.repo.tree)
assert isinstance(path_info, str) or path_info.scheme == "local"
return self.repo.tree.exists(path_info)
def makedirs(self, path_info):
makedirs(path_info, exist_ok=True, mode=self._dir_mode)
@@ -109,6 +109,12 @@ class RemoteLOCAL(RemoteBASE):
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 is_empty(self, path_info):
path = path_info.fspath
@@ -122,11 +128,11 @@ class RemoteLOCAL(RemoteBASE):
@staticmethod
def isfile(path_info):
return os.path.isfile(fspath_py35(path_info))
return os.path.isfile(path_info)
@staticmethod
def isdir(path_info):
return os.path.isdir(fspath_py35(path_info))
return os.path.isdir(path_info)
def iscopy(self, path_info):
return not (
@@ -135,32 +141,39 @@ class RemoteLOCAL(RemoteBASE):
@staticmethod
def getsize(path_info):
return os.path.getsize(fspath_py35(path_info))
return os.path.getsize(path_info)
def walk_files(self, path_info):
for fname in walk_files(path_info, self.repo.dvcignore):
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 move(self, from_info, to_info):
def move(self, from_info, to_info, mode=None):
if from_info.scheme != "local" or to_info.scheme != "local":
raise NotImplementedError
self.makedirs(to_info.parent)
if self.isfile(from_info):
mode = self._file_mode
else:
mode = self._dir_mode
if mode is None:
if self.isfile(from_info):
mode = self._file_mode
else:
mode = self._dir_mode
move(from_info, to_info, mode=mode)
@@ -168,7 +181,8 @@ class RemoteLOCAL(RemoteBASE):
tmp_info = to_info.parent / tmp_fname(to_info.name)
try:
System.copy(from_info, tmp_info)
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
@@ -177,6 +191,10 @@ class RemoteLOCAL(RemoteBASE):
def symlink(from_info, to_info):
System.symlink(from_info, to_info)
@staticmethod
def is_symlink(path_info):
return System.is_symlink(path_info)
def hardlink(self, from_info, to_info):
# If there are a lot of empty files (which happens a lot in datasets),
# and the cache type is `hardlink`, we might reach link limits and
@@ -204,8 +222,16 @@ class RemoteLOCAL(RemoteBASE):
System.hardlink(from_info, to_info)
@staticmethod
def reflink(from_info, to_info):
System.reflink(from_info, to_info)
def is_hardlink(path_info):
return System.is_hardlink(path_info)
def reflink(self, from_info, to_info):
tmp_info = to_info.parent / tmp_fname(to_info.name)
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(tmp_info, self._file_mode)
os.rename(tmp_info, to_info)
def cache_exists(self, checksums, jobs=None, name=None):
return [
@@ -228,7 +254,7 @@ class RemoteLOCAL(RemoteBASE):
copyfile(
from_file, tmp_file, name=name, no_progress_bar=no_progress_bar
)
os.rename(tmp_file, fspath_py35(to_info))
os.rename(tmp_file, to_info)
def _download(
self, from_info, to_file, name=None, no_progress_bar=False, **_kwargs
@@ -239,8 +265,9 @@ class RemoteLOCAL(RemoteBASE):
@staticmethod
def open(path_info, mode="r", encoding=None):
return open(fspath_py35(path_info), mode=mode, encoding=encoding)
return open(path_info, mode=mode, encoding=encoding)
@index_locked
def status(
self,
named_cache,
@@ -249,37 +276,128 @@ 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.cache_exists(md5s, jobs=jobs, name=self.cache_dir)
)
# This is a performance optimization. We can safely assume that,
# if the resources that we want to fetch are already cached,
# 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.cache_exists(
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._cache_object_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._cache_object_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):
@@ -295,6 +413,7 @@ class RemoteLOCAL(RemoteBASE):
cache = []
path_infos = []
names = []
checksums = []
for md5, info in Tqdm(
status_info.items(), desc="Analysing status", unit="file"
):
@@ -302,6 +421,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
@@ -310,7 +430,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,
@@ -334,14 +454,16 @@ class RemoteLOCAL(RemoteBASE):
file_mode=self._file_mode,
)
status = STATUS_DELETED
desc = "Downloading"
else:
func = remote.upload
status = STATUS_NEW
desc = "Uploading"
if jobs is None:
jobs = remote.JOBS
status_info = self.status(
dir_status, file_status, dir_contents = self._status(
named_cache,
remote,
jobs=jobs,
@@ -349,24 +471,87 @@ 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:
fails = sum(executor.map(func, *dir_plans))
fails += sum(executor.map(func, *file_plans))
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,
@@ -376,6 +561,7 @@ class RemoteLOCAL(RemoteBASE):
download=False,
)
@index_locked
def pull(self, named_cache, remote, jobs=None, show_checksums=False):
return self._process(
named_cache,
@@ -403,11 +589,10 @@ class RemoteLOCAL(RemoteBASE):
)
logger.warning(msg)
@staticmethod
def _unprotect_file(path):
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), "." + str(uuid()))
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
@@ -424,45 +609,52 @@ class RemoteLOCAL(RemoteBASE):
"a symlink or a hardlink.".format(path)
)
os.chmod(path, os.stat(path).st_mode | stat.S_IWRITE)
os.chmod(path, self._file_mode)
def _unprotect_dir(self, path):
for fname in walk_files(path, self.repo.dvcignore):
RemoteLOCAL._unprotect_file(fname)
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)
)
raise DvcException(f"can't unprotect non-existing data '{path}'")
if os.path.isdir(path):
self._unprotect_dir(path)
else:
RemoteLOCAL._unprotect_file(path)
self._unprotect_file(path)
@staticmethod
def protect(path_info):
path = fspath_py35(path_info)
mode = stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH
def protect(self, path_info):
path = os.fspath(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
# 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 = os.stat(path).st_mode
if actual & mode != mode:
actual = stat.S_IMODE(os.stat(path).st_mode)
if actual != 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
@@ -480,7 +672,7 @@ class RemoteLOCAL(RemoteBASE):
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))
logger.warning(f"Could not create '{unpacked_dir_info}'")
self.remove(unpacked_dir_info)
@@ -514,3 +706,11 @@ class RemoteLOCAL(RemoteBASE):
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(path_info).st_mode
return stat.S_IMODE(mode) == self.CACHE_MODE
+24 -21
View File
@@ -1,23 +1,19 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import logging
import os
import posixpath
import threading
from funcy import cached_property, wrap_prop
from dvc.config import Config
from dvc.path_info import CloudURLInfo
from dvc.progress import Tqdm
from dvc.remote.base import RemoteBASE
from dvc.remote.base import BaseRemote
from dvc.scheme import Schemes
logger = logging.getLogger(__name__)
class RemoteOSS(RemoteBASE):
class OSSRemote(BaseRemote):
"""
oss2 document:
https://www.alibabacloud.com/help/doc-detail/32026.htm
@@ -41,25 +37,24 @@ class RemoteOSS(RemoteBASE):
REQUIRES = {"oss2": "oss2"}
PARAM_CHECKSUM = "etag"
COPY_POLL_SECONDS = 5
LIST_OBJECT_PAGE_SIZE = 100
def __init__(self, repo, config):
super(RemoteOSS, self).__init__(repo, config)
super().__init__(repo, config)
url = config.get(Config.SECTION_REMOTE_URL)
url = config.get("url")
self.path_info = self.path_cls(url) if url else None
self.endpoint = config.get(Config.SECTION_OSS_ENDPOINT) or os.getenv(
"OSS_ENDPOINT"
)
self.endpoint = config.get("oss_endpoint") or os.getenv("OSS_ENDPOINT")
self.key_id = (
config.get(Config.SECTION_OSS_ACCESS_KEY_ID)
config.get("oss_key_id")
or os.getenv("OSS_ACCESS_KEY_ID")
or "defaultId"
)
self.key_secret = (
config.get(Config.SECTION_OSS_ACCESS_KEY_SECRET)
config.get("oss_key_secret")
or os.getenv("OSS_ACCESS_KEY_SECRET")
or "defaultSecret"
)
@@ -69,9 +64,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)
@@ -92,17 +87,25 @@ class RemoteOSS(RemoteBASE):
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):
def _list_paths(self, prefix, progress_callback=None):
import oss2
for blob in oss2.ObjectIterator(self.oss_service, prefix=prefix):
if progress_callback:
progress_callback()
yield blob.key
def list_cache_paths(self):
return self._list_paths(self.path_info.path)
def list_cache_paths(self, prefix=None, progress_callback=None):
if prefix:
prefix = posixpath.join(
self.path_info.path, prefix[:2], prefix[2:]
)
else:
prefix = self.path_info.path
return self._list_paths(prefix, progress_callback)
def _upload(
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs

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