mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 23:09:28 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6779d3d431 | ||
|
|
5844a0cd5b | ||
|
|
ce3a3ab100 | ||
|
|
d1a219df0a | ||
|
|
476af100de | ||
|
|
f2998630a3 | ||
|
|
9a7b6d2a1a | ||
|
|
1d3524f144 | ||
|
|
1a5fa228ab | ||
|
|
adeb9ef63e | ||
|
|
3e93bc286f | ||
|
|
ea89f4a102 | ||
|
|
78299ad345 | ||
|
|
b9df4451b1 | ||
|
|
df67dc2db8 | ||
|
|
37fb73606c | ||
|
|
0deddf81cf | ||
|
|
429d54e146 | ||
|
|
32859763be | ||
|
|
5012494213 | ||
|
|
57c728269d | ||
|
|
3ca18cb400 | ||
|
|
1335c7f5e4 | ||
|
|
8b9b2f6907 | ||
|
|
2f8cf8d7d3 | ||
|
|
bf5c28b71a | ||
|
|
ed2a0b4282 | ||
|
|
eb46c03c6c | ||
|
|
5fb181fcf8 | ||
|
|
0e3db0d323 | ||
|
|
5e4de841d1 | ||
|
|
9e822edafc | ||
|
|
9d8b0b80a0 | ||
|
|
59cfc2c4a4 | ||
|
|
c7a3a000e0 | ||
|
|
782e2aba87 | ||
|
|
488a6f8076 | ||
|
|
1fbf0da3df | ||
|
|
bf5deb08e4 | ||
|
|
04349a50eb | ||
|
|
a172067c7d | ||
|
|
7700d55529 | ||
|
|
a55c47e89f | ||
|
|
efda015de4 | ||
|
|
a74855b9f7 | ||
|
|
63dba45c14 | ||
|
|
e96f0cfed5 | ||
|
|
d818e70bf3 | ||
|
|
e28726c057 | ||
|
|
8a7ccab82f | ||
|
|
50ec64b841 | ||
|
|
aa672c5108 | ||
|
|
9dda6bd32c | ||
|
|
bbe06df18e | ||
|
|
960ece926c | ||
|
|
c6b4c94fde | ||
|
|
b9000ed926 | ||
|
|
58c465ef13 | ||
|
|
789faa4f11 | ||
|
|
9a5b7251b3 | ||
|
|
aab5dec7f5 | ||
|
|
e6a2b8a2ad | ||
|
|
9c4a8c384a | ||
|
|
71ea12877d | ||
|
|
c6f49bd23d | ||
|
|
a5b080645c | ||
|
|
6d941e6cb5 | ||
|
|
da0f0406fb | ||
|
|
89b40afee7 | ||
|
|
0ca08d2fea | ||
|
|
14e1511eee | ||
|
|
b349b39d62 | ||
|
|
25eb9ad8c1 | ||
|
|
eab8146b76 | ||
|
|
e7571ab325 | ||
|
|
fc4e31bbf8 | ||
|
|
674693bb86 | ||
|
|
0ad1408da7 | ||
|
|
19f2fe52e6 | ||
|
|
77be01ee2d | ||
|
|
eabf6ddf27 | ||
|
|
597f730e7d | ||
|
|
3670bf4fc3 | ||
|
|
c2f1a1e162 | ||
|
|
a234f53b76 | ||
|
|
0a0d146386 | ||
|
|
d4a3df73df | ||
|
|
256cb06921 | ||
|
|
ff56379f8a | ||
|
|
4733757bb1 | ||
|
|
3e9fd100ea | ||
|
|
c1a49b440d | ||
|
|
c2734a2cd7 | ||
|
|
71ab15771a | ||
|
|
53414a0618 | ||
|
|
708587a159 |
@@ -7,7 +7,7 @@ on:
|
||||
types: [released, prereleased]
|
||||
jobs:
|
||||
deb:
|
||||
runs-on: ubuntu-16.04
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/[email protected]
|
||||
- name: Set up Python 3.8
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/setup-ruby@v1
|
||||
uses: actions/setup-ruby@v1.1.3
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
asset_name: dvc_${{ github.event.release.tag_name }}_amd64.deb
|
||||
asset_content_type: binary/octet-stream
|
||||
rpm:
|
||||
runs-on: ubuntu-16.04
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/[email protected]
|
||||
- name: Set up Python 3.8
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/setup-ruby@v1
|
||||
uses: actions/setup-ruby@v1.1.3
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Set up Ruby 2.4
|
||||
uses: actions/setup-ruby@v1
|
||||
uses: actions/setup-ruby@v1.1.3
|
||||
with:
|
||||
ruby-version: 2.4
|
||||
- name: Install deps
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
asset_name: dvc-${{ github.event.release.tag_name }}.exe
|
||||
asset_content_type: binary/octet-stream
|
||||
pip:
|
||||
runs-on: ubuntu-16.04
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/[email protected]
|
||||
- name: Set up Python 3.7
|
||||
|
||||
@@ -7,6 +7,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '5 1 * * *' # every day at 01:05
|
||||
|
||||
env:
|
||||
DVC_TEST: "true"
|
||||
@@ -60,7 +62,7 @@ jobs:
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
- name: set pip cache
|
||||
id: pip-cache
|
||||
uses: actions/[email protected].5
|
||||
uses: actions/[email protected].6
|
||||
with:
|
||||
path: ${{ steps.pip-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
|
||||
@@ -87,14 +89,38 @@ jobs:
|
||||
run: |
|
||||
mkdir -p scripts/ci
|
||||
echo "$GS_CREDS_JSON" > scripts/ci/gcp-creds.json
|
||||
- name: disable gdrive tests
|
||||
if: >-
|
||||
github.event_name != 'schedule' &&
|
||||
(matrix.pyv != '3.8' || matrix.os != 'ubuntu-18.04')
|
||||
run: |
|
||||
echo "extra_test_args=--disable-gdrive" >> $GITHUB_ENV
|
||||
- name: run tests
|
||||
env:
|
||||
GDRIVE_CREDENTIALS_DATA: ${{ secrets.GDRIVE_CREDENTIALS_DATA }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
run: python -m tests --all --cov-report=xml --cov-report=term -n=4
|
||||
run: >-
|
||||
python -m tests -n=4
|
||||
--cov-report=xml --cov-report=term
|
||||
--all ${{ env.extra_test_args }}
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v1.4.1
|
||||
uses: codecov/codecov-action@v1.5.2
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
notify:
|
||||
if: github.ref == 'refs/heads/master' && failure()
|
||||
needs:
|
||||
- lint
|
||||
- tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Notification
|
||||
uses: rtCamp/[email protected]
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_MESSAGE: 'CI Failed on master :boom:'
|
||||
SLACK_TITLE: CI Status
|
||||
SLACK_USERNAME: DVC-CI
|
||||
|
||||
+13
-5
@@ -1,14 +1,17 @@
|
||||
ci:
|
||||
skip: [pylint, dvc-pre-commit]
|
||||
|
||||
repos:
|
||||
- hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
repo: https://github.com/ambv/black
|
||||
rev: 19.10b0
|
||||
rev: 21.6b0
|
||||
- hooks:
|
||||
- id: isort
|
||||
language_version: python3
|
||||
repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.8.0
|
||||
rev: 5.9.1
|
||||
- hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
@@ -18,13 +21,18 @@ repos:
|
||||
- flake8-debugger
|
||||
- flake8-string-format
|
||||
repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.9.0
|
||||
rev: 3.9.2
|
||||
- hooks:
|
||||
- id: mypy
|
||||
language_version: python3
|
||||
files: ^dvc/
|
||||
additional_dependencies:
|
||||
- types-requests
|
||||
- types-paramiko
|
||||
- types-tabulate
|
||||
- types-toml
|
||||
repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: 'v0.812'
|
||||
rev: 'v0.902'
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
@@ -39,7 +47,7 @@ repos:
|
||||
id: beautysh
|
||||
language_version: python3
|
||||
repo: https://github.com/lovesegfault/beautysh
|
||||
rev: 6.0.1
|
||||
rev: v6.1.0
|
||||
- hooks:
|
||||
- args:
|
||||
- git-hook
|
||||
|
||||
@@ -313,6 +313,7 @@ missing-member-hint-distance=1
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
signature-mutators=funcy.decorators.decorator,
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
|
||||
+12
-1
@@ -182,6 +182,12 @@ Fedora / CentOS (rpm)
|
||||
Comparison to related technologies
|
||||
==================================
|
||||
|
||||
#. *Data Engineering* tools such as `AirFlow <https://airflow.apache.org/>`_, `Luigi <https://github.com/spotify/luigi>`_ and
|
||||
others - in DVC data, model and ML pipelines represent a single ML project and focuses on data scientist' experience while
|
||||
data engineering pipeline orchestrates multiple data projects and focuses on efficent execution. DVC project can be used
|
||||
from data pipeline as a single execution step. `AirFlow DVC <https://github.com/covid-genomics/airflow-dvc>`_ is an example
|
||||
of such integration.
|
||||
|
||||
#. `Git-annex <https://git-annex.branchable.com/>`_ - DVC uses the idea of storing the content of large files (which should
|
||||
not be in a Git repository) in a local key-value store, and uses file hardlinks/symlinks instead of
|
||||
copying/duplicating files.
|
||||
@@ -195,7 +201,12 @@ Comparison to related technologies
|
||||
#. `Workflow Management Systems <https://en.wikipedia.org/wiki/Workflow_management_system>`_ - DVC is a workflow
|
||||
management system designed specifically to manage machine learning experiments. DVC is built on top of Git.
|
||||
|
||||
#. `DAGsHub <https://dagshub.com/>`_ - This is a Github equivalent for DVC. Pushing Git+DVC based repositories to DAGsHub will produce in a high level project dashboard; including DVC pipelines and metrics visualizations, as well as links to any DVC-managed files present in cloud storage.
|
||||
#. `DAGsHub <https://dagshub.com/>`_ - online service of Git+DVC repositories with pipeline and metrics visualization and
|
||||
DVC-specific cloud storage.
|
||||
|
||||
#. `DVC Studio <https://studio.iterative.ai/>`_ - online service of DVC repositories visualisation from DVC team. Also,
|
||||
integrated with `CML (CI/CD for ML) <https://cml.dev/>`_ for training models in clouds and Kubernetes.
|
||||
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
+4
-4
@@ -77,15 +77,15 @@ def add_debugging_flags(parser):
|
||||
from argparse import SUPPRESS
|
||||
|
||||
parser.add_argument(
|
||||
"--cprofile", action="store_true", default=False, help=SUPPRESS,
|
||||
"--cprofile", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument("--cprofile-dump", help=SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--pdb", action="store_true", default=False, help=SUPPRESS,
|
||||
"--pdb", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instrument", action="store_true", default=False, help=SUPPRESS,
|
||||
"--instrument", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument(
|
||||
"--instrument-open", action="store_true", default=False, help=SUPPRESS,
|
||||
"--instrument-open", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
|
||||
+67
-19
@@ -2,19 +2,21 @@ import logging
|
||||
|
||||
from shortuuid import uuid
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc import prompt
|
||||
from dvc.exceptions import (
|
||||
CacheLinkError,
|
||||
CheckoutError,
|
||||
ConfirmRemoveError,
|
||||
DvcException,
|
||||
)
|
||||
from dvc.ignore import DvcIgnoreFilter
|
||||
from dvc.objects import check, load
|
||||
from dvc.objects.errors import ObjectFormatError
|
||||
from dvc.objects.stage import stage
|
||||
from dvc.remote.slow_link_detection import ( # type: ignore[attr-defined]
|
||||
slow_link_guard,
|
||||
)
|
||||
from dvc.types import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -95,7 +97,7 @@ def _do_link(cache, from_info, to_info, link_method):
|
||||
link_method(from_info, to_info)
|
||||
|
||||
logger.debug(
|
||||
"Created '%s': %s -> %s", cache.cache_types[0], from_info, to_info,
|
||||
"Created '%s': %s -> %s", cache.cache_types[0], from_info, to_info
|
||||
)
|
||||
|
||||
|
||||
@@ -132,7 +134,7 @@ def _cache_is_copy(cache, path_info):
|
||||
return True
|
||||
|
||||
workspace_file = path_info.with_name("." + uuid())
|
||||
test_cache_file = cache.fs.path_info / ".cache_type_test_file"
|
||||
test_cache_file = cache.path_info / ".cache_type_test_file"
|
||||
if not cache.fs.exists(test_cache_file):
|
||||
cache.makedirs(test_cache_file.parent)
|
||||
with cache.fs.open(test_cache_file, "wb") as fobj:
|
||||
@@ -148,7 +150,14 @@ def _cache_is_copy(cache, path_info):
|
||||
|
||||
|
||||
def _checkout_file(
|
||||
path_info, fs, obj, cache, force, progress_callback=None, relink=False,
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
state=None,
|
||||
):
|
||||
"""The file is changed we need to checkout a new copy"""
|
||||
modified = False
|
||||
@@ -168,15 +177,22 @@ def _checkout_file(
|
||||
_link(cache, cache_info, path_info)
|
||||
modified = True
|
||||
|
||||
fs.repo.state.save(path_info, fs, obj.hash_info)
|
||||
if state:
|
||||
state.save(path_info, fs, obj.hash_info)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(str(path_info))
|
||||
|
||||
return modified
|
||||
|
||||
|
||||
def _remove_redundant_files(path_info, fs, obj, cache, force):
|
||||
existing_files = set(fs.walk_files(path_info))
|
||||
def _remove_redundant_files(
|
||||
path_info, fs, obj, cache, force, dvcignore: Optional[DvcIgnoreFilter]
|
||||
):
|
||||
if dvcignore:
|
||||
existing_files = set(dvcignore.walk_files(fs, path_info))
|
||||
else:
|
||||
existing_files = set(fs.walk_files(path_info))
|
||||
|
||||
needed_files = {path_info.joinpath(*key) for key, _ in obj}
|
||||
redundant_files = existing_files - needed_files
|
||||
@@ -187,9 +203,17 @@ def _remove_redundant_files(path_info, fs, obj, cache, force):
|
||||
|
||||
|
||||
def _checkout_dir(
|
||||
path_info, fs, obj, cache, force, progress_callback=None, relink=False,
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
modified = False, False
|
||||
modified = False
|
||||
# Create dir separately so that dir is created
|
||||
# even if there are no files in it
|
||||
if not fs.exists(path_info):
|
||||
@@ -207,15 +231,20 @@ def _checkout_dir(
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
state=None,
|
||||
)
|
||||
if entry_modified:
|
||||
modified = True
|
||||
|
||||
modified = (
|
||||
_remove_redundant_files(path_info, fs, obj, cache, force) or modified
|
||||
_remove_redundant_files(
|
||||
path_info, fs, obj, cache, force, dvcignore=dvcignore
|
||||
)
|
||||
or modified
|
||||
)
|
||||
|
||||
fs.repo.state.save(path_info, fs, obj.hash_info)
|
||||
if state:
|
||||
state.save(path_info, fs, obj.hash_info)
|
||||
|
||||
# relink is not modified, assume it as nochange
|
||||
return modified and not relink
|
||||
@@ -229,17 +258,28 @@ def _checkout(
|
||||
force=False,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
if not obj.hash_info.isdir:
|
||||
ret = _checkout_file(
|
||||
path_info, fs, obj, cache, force, progress_callback, relink
|
||||
path_info, fs, obj, cache, force, progress_callback, relink, state
|
||||
)
|
||||
else:
|
||||
ret = _checkout_dir(
|
||||
path_info, fs, obj, cache, force, progress_callback, relink,
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
dvcignore=dvcignore,
|
||||
state=state,
|
||||
)
|
||||
|
||||
fs.repo.state.save_link(path_info, fs)
|
||||
if state:
|
||||
state.save_link(path_info, fs)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -253,6 +293,8 @@ def checkout(
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
quiet=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
if path_info.scheme not in ["local", cache.fs.scheme]:
|
||||
raise NotImplementedError
|
||||
@@ -269,7 +311,7 @@ def checkout(
|
||||
failed = path_info
|
||||
|
||||
elif not relink and not _changed(path_info, fs, obj, cache):
|
||||
logger.trace("Data '%s' didn't change.", path_info)
|
||||
logger.trace("Data '%s' didn't change.", path_info) # type: ignore
|
||||
skip = True
|
||||
else:
|
||||
try:
|
||||
@@ -286,9 +328,7 @@ def checkout(
|
||||
|
||||
if failed or skip:
|
||||
if progress_callback and obj:
|
||||
progress_callback(
|
||||
str(path_info), len(obj),
|
||||
)
|
||||
progress_callback(str(path_info), len(obj))
|
||||
if failed:
|
||||
raise CheckoutError([failed])
|
||||
return
|
||||
@@ -296,5 +336,13 @@ def checkout(
|
||||
logger.debug("Checking out '%s' with cache '%s'.", path_info, obj)
|
||||
|
||||
return _checkout(
|
||||
path_info, fs, obj, cache, force, progress_callback, relink,
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
dvcignore=dvcignore,
|
||||
state=state,
|
||||
)
|
||||
|
||||
+1
-1
@@ -117,6 +117,6 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add.",
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
parser.set_defaults(func=CmdAdd)
|
||||
|
||||
+11
-4
@@ -7,11 +7,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def fix_subparsers(subparsers):
|
||||
"""Workaround for bug in Python 3. See more info at:
|
||||
https://bugs.python.org/issue16308
|
||||
https://github.com/iterative/dvc/issues/769
|
||||
https://bugs.python.org/issue16308
|
||||
https://github.com/iterative/dvc/issues/769
|
||||
|
||||
Args:
|
||||
subparsers: subparsers to fix.
|
||||
Args:
|
||||
subparsers: subparsers to fix.
|
||||
"""
|
||||
subparsers.required = True
|
||||
subparsers.dest = "cmd"
|
||||
@@ -45,6 +45,10 @@ class CmdBase(ABC):
|
||||
updater = Updater(self.repo.tmp_dir, hardlink_lock=hardlink_lock)
|
||||
updater.check()
|
||||
|
||||
def do_run(self):
|
||||
with self.repo:
|
||||
return self.run()
|
||||
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
pass
|
||||
@@ -55,3 +59,6 @@ class CmdBaseNoRepo(CmdBase):
|
||||
self.args = args
|
||||
|
||||
os.chdir(args.cd)
|
||||
|
||||
def do_run(self):
|
||||
return self.run()
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import append_doc_link, fix_subparsers
|
||||
from dvc.command.config import CmdConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from dvc.ui import ui
|
||||
|
||||
|
||||
class CmdCacheDir(CmdConfig):
|
||||
@@ -17,7 +15,7 @@ class CmdCacheDir(CmdConfig):
|
||||
# Use merged config with default values
|
||||
conf = self.config
|
||||
self._check(conf, False, "cache", "dir")
|
||||
logger.info(conf["cache"]["dir"])
|
||||
ui.write(conf["cache"]["dir"])
|
||||
return 0
|
||||
with self.config.edit(level=self.args.level) as conf:
|
||||
if self.args.unset:
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from dvc.ui import ui
|
||||
|
||||
|
||||
class CmdCheckIgnore(CmdBase):
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
self.ignore_filter = self.repo.fs.dvcignore
|
||||
self.ignore_filter = self.repo.dvcignore
|
||||
|
||||
def _show_results(self, result):
|
||||
if not result.match and not self.args.non_matching:
|
||||
@@ -22,9 +20,9 @@ class CmdCheckIgnore(CmdBase):
|
||||
patterns = patterns[-1:]
|
||||
|
||||
for pattern in patterns:
|
||||
logger.info("{}\t{}".format(pattern, result.file))
|
||||
ui.write(pattern, result.file, sep="\t")
|
||||
else:
|
||||
logger.info(result.file)
|
||||
ui.write(result.file)
|
||||
|
||||
def _check_one_file(self, target):
|
||||
result = self.ignore_filter.check_ignore(target)
|
||||
@@ -122,6 +120,6 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Read paths from standard input instead of providing `targets`.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"targets", nargs="*", help="File or directory paths to check",
|
||||
"targets", nargs="*", help="File or directory paths to check"
|
||||
).complete = completion.FILE
|
||||
parser.set_defaults(func=CmdCheckIgnore)
|
||||
|
||||
+10
-20
@@ -1,38 +1,28 @@
|
||||
import argparse
|
||||
import logging
|
||||
import operator
|
||||
|
||||
import colorama
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import CheckoutError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from dvc.ui import ui
|
||||
|
||||
|
||||
def log_changes(stats):
|
||||
colors = [
|
||||
("modified", colorama.Fore.YELLOW,),
|
||||
("added", colorama.Fore.GREEN),
|
||||
("deleted", colorama.Fore.RED,),
|
||||
]
|
||||
colors = {
|
||||
"modified": "yellow",
|
||||
"added": "green",
|
||||
"deleted": "red",
|
||||
}
|
||||
|
||||
for state, color in colors:
|
||||
for state, color in colors.items():
|
||||
entries = stats.get(state)
|
||||
|
||||
if not entries:
|
||||
continue
|
||||
|
||||
for entry in entries:
|
||||
logger.info(
|
||||
"{color}{state}{nc}{spacing}{entry}".format(
|
||||
color=color,
|
||||
state=state[0].upper(),
|
||||
nc=colorama.Fore.RESET,
|
||||
spacing="\t",
|
||||
entry=entry,
|
||||
)
|
||||
ui.write(
|
||||
f"[{color}]{state[0].upper()}", entry, styled=True, sep="\t"
|
||||
)
|
||||
|
||||
|
||||
@@ -58,7 +48,7 @@ class CmdCheckout(CmdBase):
|
||||
msg = get_summary(
|
||||
sorted(stats.items(), key=operator.itemgetter(0))
|
||||
)
|
||||
logger.info(msg or default_message)
|
||||
ui.write(msg or default_message)
|
||||
else:
|
||||
log_changes(stats)
|
||||
|
||||
|
||||
@@ -71,5 +71,5 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="*",
|
||||
help="stages or .dvc files to commit. Optional. "
|
||||
"(Finds all DVC files in the workspace by default.)",
|
||||
).complete = completion.DVC_FILE
|
||||
).complete = completion.DVCFILES_AND_STAGE
|
||||
commit_parser.set_defaults(func=CmdCommit)
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
import shtab
|
||||
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
FILE = shtab.FILE
|
||||
@@ -67,25 +68,16 @@ _dvc_compadd_exps() {
|
||||
""",
|
||||
}
|
||||
|
||||
DVC_FILE = {
|
||||
"bash": "_dvc_compgen_DVCFiles",
|
||||
"zsh": "_dvc_compadd_DVCFiles",
|
||||
}
|
||||
DVC_FILE = {"bash": "_dvc_compgen_DVCFiles", "zsh": "_dvc_compadd_DVCFiles"}
|
||||
|
||||
STAGE = {
|
||||
"bash": "_dvc_compgen_stages",
|
||||
"zsh": "_dvc_compadd_stages",
|
||||
}
|
||||
STAGE = {"bash": "_dvc_compgen_stages", "zsh": "_dvc_compadd_stages"}
|
||||
|
||||
DVCFILES_AND_STAGE = {
|
||||
"bash": "_dvc_compgen_stages_and_files",
|
||||
"zsh": "_dvc_compadd_stages_and_files",
|
||||
}
|
||||
|
||||
EXPERIMENT = {
|
||||
"bash": "_dvc_compgen_exps",
|
||||
"zsh": "_dvc_compadd_exps",
|
||||
}
|
||||
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
|
||||
|
||||
|
||||
class CmdCompletion(CmdBaseNoRepo):
|
||||
@@ -95,7 +87,7 @@ class CmdCompletion(CmdBaseNoRepo):
|
||||
parser = get_main_parser()
|
||||
shell = self.args.shell
|
||||
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
|
||||
print(script)
|
||||
ui.write(script, force=True)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import os
|
||||
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -71,7 +72,9 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
prefix = self._config_file_prefix(
|
||||
self.args.show_origin, self.config, level
|
||||
)
|
||||
logger.info("\n".join(self._format_config(conf, prefix)))
|
||||
configs = list(self._format_config(conf, prefix))
|
||||
if configs:
|
||||
ui.write("\n".join(configs))
|
||||
|
||||
return 0
|
||||
|
||||
@@ -96,7 +99,7 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
prefix = self._config_file_prefix(
|
||||
self.args.show_origin, self.config, level
|
||||
)
|
||||
logger.info("{}{}".format(prefix, conf[section][opt]))
|
||||
ui.write(prefix, conf[section][opt], sep="")
|
||||
break
|
||||
|
||||
return 0
|
||||
|
||||
@@ -67,6 +67,6 @@ def add_parser(subparsers, parent_parser):
|
||||
help=DAEMON_ANALYTICS_HELP,
|
||||
)
|
||||
daemon_analytics_parser.add_argument(
|
||||
"target", help="Analytics file.",
|
||||
"target", help="Analytics file."
|
||||
).complete = completion.FILE
|
||||
daemon_analytics_parser.set_defaults(func=CmdDaemonAnalytics)
|
||||
|
||||
+2
-4
@@ -1,9 +1,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from dvc.ui import ui
|
||||
|
||||
|
||||
def _show_ascii(G):
|
||||
@@ -101,7 +99,7 @@ class CmdDAG(CmdBase):
|
||||
)
|
||||
|
||||
if self.args.dot:
|
||||
logger.info(_show_dot(G))
|
||||
ui.write(_show_dot(G))
|
||||
else:
|
||||
from dvc.utils.pager import pager
|
||||
|
||||
|
||||
@@ -9,10 +9,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class CmdDataBase(CmdBase):
|
||||
def log_summary(self, stats):
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.humanize import get_summary
|
||||
|
||||
default_msg = "Everything is up to date."
|
||||
logger.info(get_summary(stats.items()) or default_msg)
|
||||
ui.write(get_summary(stats.items()) or default_msg)
|
||||
|
||||
|
||||
class CmdDataPull(CmdDataBase):
|
||||
@@ -139,7 +140,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"-r", "--remote", help="Remote storage to pull from", metavar="<name>",
|
||||
"-r", "--remote", help="Remote storage to pull from", metavar="<name>"
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"-a",
|
||||
@@ -208,7 +209,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"-r", "--remote", help="Remote storage to push to", metavar="<name>",
|
||||
"-r", "--remote", help="Remote storage to push to", metavar="<name>"
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"-a",
|
||||
@@ -272,10 +273,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"-r",
|
||||
"--remote",
|
||||
help="Remote storage to fetch from",
|
||||
metavar="<name>",
|
||||
"-r", "--remote", help="Remote storage to fetch from", metavar="<name>"
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"-a",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -10,6 +9,7 @@ logger = logging.getLogger(__name__)
|
||||
class CmdDestroy(CmdBase):
|
||||
def run(self):
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
try:
|
||||
statement = (
|
||||
@@ -19,7 +19,7 @@ class CmdDestroy(CmdBase):
|
||||
"Are you sure you want to continue?"
|
||||
)
|
||||
|
||||
if not self.args.force and not prompt.confirm(statement):
|
||||
if not self.args.force and not ui.confirm(statement):
|
||||
raise DvcException(
|
||||
"cannot destroy without a confirmation from the user."
|
||||
" Use `-f` to force."
|
||||
|
||||
+16
-28
@@ -3,10 +3,9 @@ import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import colorama
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,13 +17,12 @@ def _digest(checksum):
|
||||
|
||||
|
||||
def _show_md(diff, show_hash=False, hide_missing=False):
|
||||
from dvc.ui import ui
|
||||
|
||||
headers = ["Status", "Hash", "Path"] if show_hash else ["Status", "Path"]
|
||||
rows = []
|
||||
statuses = ["added", "deleted", "renamed", "modified"]
|
||||
if not hide_missing:
|
||||
statuses.append("not in cache")
|
||||
|
||||
for status in statuses:
|
||||
entries = diff.get(status, [])
|
||||
if not entries:
|
||||
@@ -44,7 +42,7 @@ def _show_md(diff, show_hash=False, hide_missing=False):
|
||||
|
||||
class CmdDiff(CmdBase):
|
||||
@staticmethod
|
||||
def _format(diff, hide_missing=False):
|
||||
def _show_diff(diff, hide_missing=False):
|
||||
"""
|
||||
Given a diff structure, generate a string of paths separated
|
||||
by new lines and grouped together by their state.
|
||||
@@ -71,15 +69,14 @@ class CmdDiff(CmdBase):
|
||||
"""
|
||||
|
||||
colors = {
|
||||
"added": colorama.Fore.GREEN,
|
||||
"modified": colorama.Fore.YELLOW,
|
||||
"deleted": colorama.Fore.RED,
|
||||
"renamed": colorama.Fore.GREEN,
|
||||
"not in cache": colorama.Fore.YELLOW,
|
||||
"added": "green",
|
||||
"modified": "yellow",
|
||||
"deleted": "red",
|
||||
"renamed": "green",
|
||||
"not in cache": "yellow",
|
||||
}
|
||||
|
||||
summary = {}
|
||||
groups = []
|
||||
|
||||
states = ["added", "deleted", "renamed", "modified"]
|
||||
if not hide_missing:
|
||||
@@ -91,7 +88,8 @@ class CmdDiff(CmdBase):
|
||||
if not entries:
|
||||
continue
|
||||
|
||||
content = []
|
||||
header = state.capitalize()
|
||||
ui.write(f"[{colors[state]}]{header}[/]:", styled=True)
|
||||
|
||||
for entry in entries:
|
||||
path = entry["path"]
|
||||
@@ -99,7 +97,7 @@ class CmdDiff(CmdBase):
|
||||
path = f"{path['old']} -> {path['new']}"
|
||||
checksum = entry.get("hash")
|
||||
summary[state] += 1 if not path.endswith(os.sep) else 0
|
||||
content.append(
|
||||
ui.write(
|
||||
"{space}{checksum}{separator}{path}".format(
|
||||
space=" ",
|
||||
checksum=_digest(checksum) if checksum else "",
|
||||
@@ -108,14 +106,7 @@ class CmdDiff(CmdBase):
|
||||
)
|
||||
)
|
||||
|
||||
groups.append(
|
||||
"{color}{header}{nc}:\n{content}".format(
|
||||
color=colors[state],
|
||||
header=state.capitalize(),
|
||||
nc=colorama.Fore.RESET,
|
||||
content="\n".join(content),
|
||||
)
|
||||
)
|
||||
ui.write()
|
||||
|
||||
if not sum(summary.values()):
|
||||
return None
|
||||
@@ -126,9 +117,8 @@ class CmdDiff(CmdBase):
|
||||
)
|
||||
if not hide_missing:
|
||||
fmt += ", {not in cache} not in cache"
|
||||
groups.append(fmt.format_map(summary))
|
||||
|
||||
return "\n\n".join(groups)
|
||||
ui.write(fmt.format_map(summary))
|
||||
|
||||
def run(self):
|
||||
from dvc.exceptions import DvcException
|
||||
@@ -155,13 +145,11 @@ class CmdDiff(CmdBase):
|
||||
diff[key] = entries
|
||||
|
||||
if self.args.show_json:
|
||||
logger.info(json.dumps(diff))
|
||||
ui.write(json.dumps(diff))
|
||||
elif self.args.show_md:
|
||||
logger.info(_show_md(diff, show_hash, hide_missing))
|
||||
_show_md(diff, show_hash, hide_missing)
|
||||
elif diff:
|
||||
output = self._format(diff, hide_missing)
|
||||
if output:
|
||||
logger.info(output)
|
||||
self._show_diff(diff, hide_missing)
|
||||
|
||||
except DvcException:
|
||||
logger.exception("failed to get diff")
|
||||
|
||||
+83
-70
@@ -5,18 +5,21 @@ from datetime import date, datetime
|
||||
from fnmatch import fnmatch
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, Optional
|
||||
|
||||
from funcy import constantly, iffy, lmap
|
||||
from funcy import lmap
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc import prompt
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.command.metrics import DEFAULT_PRECISION
|
||||
from dvc.command.repro import CmdRepro
|
||||
from dvc.command.repro import add_arguments as add_repro_arguments
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.flatten import flatten
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
|
||||
@@ -116,6 +119,16 @@ def _collect_names(all_experiments, **kwargs):
|
||||
return metric_names, param_names
|
||||
|
||||
|
||||
experiment_types = {
|
||||
"checkpoint_tip": "│ ╓",
|
||||
"checkpoint_commit": "│ ╟",
|
||||
"checkpoint_base": "├─╨",
|
||||
"branch_commit": "├──",
|
||||
"branch_base": "└──",
|
||||
"baseline": "",
|
||||
}
|
||||
|
||||
|
||||
def _collect_rows(
|
||||
base_rev,
|
||||
experiments,
|
||||
@@ -147,13 +160,6 @@ def _collect_rows(
|
||||
name_rev = rev[:7]
|
||||
|
||||
exp_name = exp.get("name", "")
|
||||
if is_baseline:
|
||||
name = exp_name or name_rev
|
||||
elif exp_name:
|
||||
name = f"{rev[:7]} [[bold]{exp['name']}[/]]"
|
||||
else:
|
||||
name = name_rev
|
||||
|
||||
tip = exp.get("checkpoint_tip")
|
||||
|
||||
parent_rev = exp.get("checkpoint_parent", "")
|
||||
@@ -162,27 +168,31 @@ def _collect_rows(
|
||||
|
||||
parent = ""
|
||||
if is_baseline:
|
||||
tree = ""
|
||||
typ = "baseline"
|
||||
elif tip:
|
||||
if tip == parent_tip:
|
||||
tree = "│ ╓" if new_checkpoint else "│ ╟"
|
||||
typ = (
|
||||
"checkpoint_tip" if new_checkpoint else "checkpoint_commit"
|
||||
)
|
||||
elif parent_rev == base_rev:
|
||||
tree = "├─╨"
|
||||
typ = "checkpoint_base"
|
||||
else:
|
||||
tree = "│ ╟"
|
||||
typ = "checkpoint_commit"
|
||||
parent = parent_rev[:7]
|
||||
elif i < len(experiments) - 1:
|
||||
typ = "branch_commit"
|
||||
else:
|
||||
tree = "├──" if i < len(experiments) - 1 else "└──"
|
||||
typ = "branch_base"
|
||||
|
||||
if not is_baseline:
|
||||
new_checkpoint = not (tip and tip == parent_tip)
|
||||
|
||||
row = [
|
||||
name,
|
||||
exp_name,
|
||||
name_rev,
|
||||
queued,
|
||||
tree,
|
||||
typ,
|
||||
_format_time(exp.get("timestamp")),
|
||||
rev == "baseline",
|
||||
parent,
|
||||
]
|
||||
_extend_row(
|
||||
@@ -295,14 +305,7 @@ def experiments_table(
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
headers = [
|
||||
"Experiment",
|
||||
"queued",
|
||||
"ident_guide",
|
||||
"Created",
|
||||
"is_baseline",
|
||||
"parent",
|
||||
]
|
||||
headers = ["Experiment", "rev", "queued", "typ", "Created", "parent"]
|
||||
td = TabularData(
|
||||
lconcat(headers, metric_headers, param_headers), fill_value=FILL_VALUE
|
||||
)
|
||||
@@ -321,17 +324,30 @@ def experiments_table(
|
||||
return td
|
||||
|
||||
|
||||
def prepare_exp_id(kwargs):
|
||||
def prepare_exp_id(kwargs) -> "Text":
|
||||
from rich.text import Text
|
||||
|
||||
exp_name = kwargs["Experiment"]
|
||||
ident = kwargs.get("ident_guide")
|
||||
queued = kwargs.get("queued")
|
||||
rev = kwargs["rev"]
|
||||
typ = kwargs.get("typ", "baseline")
|
||||
|
||||
if typ == "baseline" or not exp_name:
|
||||
text = Text(exp_name or rev)
|
||||
else:
|
||||
text = Text.assemble(rev, " [", (exp_name, "bold"), "]")
|
||||
|
||||
parent = kwargs.get("parent")
|
||||
return "{}{}{}{}".format(
|
||||
f"{ident} " if ident else "",
|
||||
"*" if queued else "",
|
||||
exp_name,
|
||||
f" ({parent})" if parent else "",
|
||||
)
|
||||
suff = f" ({parent})" if parent else ""
|
||||
text.append(suff)
|
||||
|
||||
tree = experiment_types[typ]
|
||||
queued = "*" if kwargs.get("queued") else ""
|
||||
pref = (f"{tree} " if tree else "") + queued
|
||||
return Text(pref) + text
|
||||
|
||||
|
||||
def baseline_styler(typ):
|
||||
return {"style": "bold"} if typ == "baseline" else {}
|
||||
|
||||
|
||||
def show_experiments(
|
||||
@@ -366,11 +382,9 @@ def show_experiments(
|
||||
if no_timestamp:
|
||||
td.drop("Created")
|
||||
|
||||
baseline_styler = iffy(constantly({"style": "bold"}), default={})
|
||||
row_styles = lmap(baseline_styler, td.column("is_baseline"))
|
||||
td.drop("is_baseline")
|
||||
row_styles = lmap(baseline_styler, td.column("typ"))
|
||||
|
||||
merge_headers = ["Experiment", "queued", "ident_guide", "parent"]
|
||||
merge_headers = ["Experiment", "rev", "queued", "typ", "parent"]
|
||||
td.column("Experiment")[:] = map(prepare_exp_id, td.as_dict(merge_headers))
|
||||
td.drop(*merge_headers[1:])
|
||||
|
||||
@@ -437,7 +451,7 @@ class CmdExperimentsShow(CmdBase):
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(all_experiments, default=_format_json))
|
||||
ui.write(json.dumps(all_experiments, default=_format_json))
|
||||
else:
|
||||
show_experiments(
|
||||
all_experiments,
|
||||
@@ -481,7 +495,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(diff))
|
||||
ui.write(json.dumps(diff))
|
||||
else:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
@@ -489,8 +503,6 @@ class CmdExperimentsDiff(CmdBase):
|
||||
diffs = [("metrics", "Metric"), ("params", "Param")]
|
||||
for idx, (key, title) in enumerate(diffs):
|
||||
if idx:
|
||||
from dvc.ui import ui
|
||||
|
||||
# we are printing tables even in `--quiet` mode
|
||||
# so we should also be printing the "table" separator
|
||||
ui.write(force=True)
|
||||
@@ -524,7 +536,7 @@ class CmdExperimentsRun(CmdRepro):
|
||||
)
|
||||
|
||||
if self.args.reset:
|
||||
logger.info("Any existing checkpoints will be reset and re-run.")
|
||||
ui.write("Any existing checkpoints will be reset and re-run.")
|
||||
|
||||
results = self.repo.experiments.run(
|
||||
name=self.args.name,
|
||||
@@ -541,7 +553,7 @@ class CmdExperimentsRun(CmdRepro):
|
||||
if self.args.metrics and results:
|
||||
metrics = self.repo.metrics.show(revs=list(results))
|
||||
metrics.pop("workspace", None)
|
||||
logger.info(show_metrics(metrics))
|
||||
show_metrics(metrics)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -595,12 +607,13 @@ class CmdExperimentsGC(CmdRepro):
|
||||
)
|
||||
|
||||
if removed:
|
||||
logger.info(
|
||||
f"Removed {removed} experiments. To remove unused cache files "
|
||||
"use 'dvc gc'."
|
||||
ui.write(
|
||||
f"Removed {removed} experiments.",
|
||||
"To remove unused cache files",
|
||||
"use 'dvc gc'.",
|
||||
)
|
||||
else:
|
||||
logger.info("No experiments to remove.")
|
||||
ui.write("No experiments to remove.")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -649,15 +662,15 @@ class CmdExperimentsPush(CmdBase):
|
||||
run_cache=self.args.run_cache,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Pushed experiment '%s' to Git remote '%s'.",
|
||||
self.args.experiment,
|
||||
self.args.git_remote,
|
||||
ui.write(
|
||||
f"Pushed experiment '{self.args.experiment}'"
|
||||
f"to Git remote '{self.args.git_remote}'."
|
||||
)
|
||||
if not self.args.push_cache:
|
||||
logger.info(
|
||||
"To push cached outputs for this experiment to DVC remote "
|
||||
"storage, re-run this command without '--no-cache'."
|
||||
ui.write(
|
||||
"To push cached outputs",
|
||||
"for this experiment to DVC remote storage,"
|
||||
"re-run this command without '--no-cache'.",
|
||||
)
|
||||
|
||||
return 0
|
||||
@@ -676,15 +689,15 @@ class CmdExperimentsPull(CmdBase):
|
||||
run_cache=self.args.run_cache,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Pulled experiment '%s' from Git remote '%s'. ",
|
||||
self.args.experiment,
|
||||
self.args.git_remote,
|
||||
ui.write(
|
||||
f"Pulled experiment '{self.args.experiment}'",
|
||||
f"from Git remote '{self.args.git_remote}'.",
|
||||
)
|
||||
if not self.args.pull_cache:
|
||||
logger.info(
|
||||
"To pull cached outputs for this experiment from DVC remote "
|
||||
"storage, re-run this command without '--no-cache'."
|
||||
ui.write(
|
||||
"To pull cached outputs for this experiment"
|
||||
"from DVC remote storage,"
|
||||
"re-run this command without '--no-cache'."
|
||||
)
|
||||
|
||||
return 0
|
||||
@@ -694,7 +707,7 @@ class CmdExperimentsRemove(CmdBase):
|
||||
def run(self):
|
||||
|
||||
self.repo.experiments.remove(
|
||||
exp_names=self.args.experiment, queue=self.args.queue,
|
||||
exp_names=self.args.experiment, queue=self.args.queue
|
||||
)
|
||||
|
||||
return 0
|
||||
@@ -855,7 +868,7 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Fail if this command would overwrite conflicting changes.",
|
||||
)
|
||||
experiments_apply_parser.add_argument(
|
||||
"experiment", help="Experiment to be applied.",
|
||||
"experiment", help="Experiment to be applied."
|
||||
).complete = completion.EXPERIMENT
|
||||
experiments_apply_parser.set_defaults(func=CmdExperimentsApply)
|
||||
|
||||
@@ -1020,10 +1033,10 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
experiments_branch_parser.add_argument(
|
||||
"experiment", help="Experiment to be promoted.",
|
||||
"experiment", help="Experiment to be promoted."
|
||||
)
|
||||
experiments_branch_parser.add_argument(
|
||||
"branch", help="Git branch name to use.",
|
||||
"branch", help="Git branch name to use."
|
||||
)
|
||||
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
|
||||
|
||||
@@ -1046,7 +1059,7 @@ def add_parser(subparsers, parent_parser):
|
||||
metavar="<rev>",
|
||||
)
|
||||
experiments_list_parser.add_argument(
|
||||
"--all", action="store_true", help="List all experiments.",
|
||||
"--all", action="store_true", help="List all experiments."
|
||||
)
|
||||
experiments_list_parser.add_argument(
|
||||
"--names-only",
|
||||
@@ -1118,7 +1131,7 @@ def add_parser(subparsers, parent_parser):
|
||||
metavar="<git_remote>",
|
||||
)
|
||||
experiments_push_parser.add_argument(
|
||||
"experiment", help="Experiment to push.", metavar="<experiment>",
|
||||
"experiment", help="Experiment to push.", metavar="<experiment>"
|
||||
).complete = completion.EXPERIMENT
|
||||
experiments_push_parser.set_defaults(func=CmdExperimentsPush)
|
||||
|
||||
@@ -1174,7 +1187,7 @@ def add_parser(subparsers, parent_parser):
|
||||
metavar="<git_remote>",
|
||||
)
|
||||
experiments_pull_parser.add_argument(
|
||||
"experiment", help="Experiment to pull.", metavar="<experiment>",
|
||||
"experiment", help="Experiment to pull.", metavar="<experiment>"
|
||||
)
|
||||
experiments_pull_parser.set_defaults(func=CmdExperimentsPull)
|
||||
|
||||
@@ -1187,7 +1200,7 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
experiments_remove_parser.add_argument(
|
||||
"--queue", action="store_true", help="Remove all queued experiments.",
|
||||
"--queue", action="store_true", help="Remove all queued experiments."
|
||||
)
|
||||
experiments_remove_parser.add_argument(
|
||||
"experiment",
|
||||
|
||||
@@ -40,7 +40,7 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
freeze_parser.add_argument(
|
||||
"targets", nargs="+", help="Stages or .dvc files to freeze",
|
||||
"targets", nargs="+", help="Stages or .dvc files to freeze"
|
||||
).complete = completion.DVC_FILE
|
||||
freeze_parser.set_defaults(func=CmdFreeze)
|
||||
|
||||
@@ -53,6 +53,6 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
unfreeze_parser.add_argument(
|
||||
"targets", nargs="+", help="Stages or .dvc files to unfreeze",
|
||||
"targets", nargs="+", help="Stages or .dvc files to unfreeze"
|
||||
).complete = completion.DVC_FILE
|
||||
unfreeze_parser.set_defaults(func=CmdUnfreeze)
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,7 +44,7 @@ class CmdGC(CmdBase):
|
||||
logger.warning(msg)
|
||||
|
||||
msg = "Are you sure you want to proceed?"
|
||||
if not self.args.force and not prompt.confirm(msg):
|
||||
if not self.args.force and not ui.confirm(msg):
|
||||
return 1
|
||||
|
||||
self.repo.gc(
|
||||
|
||||
+3
-2
@@ -12,12 +12,13 @@ logger = logging.getLogger(__name__)
|
||||
class CmdGet(CmdBaseNoRepo):
|
||||
def _show_url(self):
|
||||
from dvc.api import get_url
|
||||
from dvc.ui import ui
|
||||
|
||||
try:
|
||||
url = get_url(
|
||||
self.args.path, repo=self.args.url, rev=self.args.rev
|
||||
)
|
||||
logger.info(url)
|
||||
ui.write(url, force=True)
|
||||
except DvcException:
|
||||
logger.exception("failed to show URL")
|
||||
return 1
|
||||
@@ -64,7 +65,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"url", help="Location of DVC or Git repository to download from"
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"path", help="Path to a file or directory within the repository",
|
||||
"path", help="Path to a file or directory within the repository"
|
||||
).complete = completion.FILE
|
||||
get_parser.add_argument(
|
||||
"-o",
|
||||
|
||||
@@ -34,7 +34,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"url", help="See `dvc import-url -h` for full list of supported URLs."
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"out", nargs="?", help="Destination path to put data to.",
|
||||
"out", nargs="?", help="Destination path to put data to."
|
||||
).complete = completion.DIR
|
||||
get_parser.add_argument(
|
||||
"-j",
|
||||
|
||||
@@ -105,7 +105,7 @@ def add_parser(subparsers, parent_parser):
|
||||
help=PRE_COMMIT_HELP,
|
||||
)
|
||||
pre_commit_parser.add_argument(
|
||||
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
|
||||
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool."
|
||||
)
|
||||
pre_commit_parser.set_defaults(func=CmdPreCommit)
|
||||
|
||||
@@ -117,7 +117,7 @@ def add_parser(subparsers, parent_parser):
|
||||
help=POST_CHECKOUT_HELP,
|
||||
)
|
||||
post_checkout_parser.add_argument(
|
||||
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
|
||||
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool."
|
||||
)
|
||||
post_checkout_parser.set_defaults(func=CmdPostCheckout)
|
||||
|
||||
@@ -129,7 +129,7 @@ def add_parser(subparsers, parent_parser):
|
||||
help=PRE_PUSH_HELP,
|
||||
)
|
||||
pre_push_parser.add_argument(
|
||||
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
|
||||
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool."
|
||||
)
|
||||
pre_push_parser.set_defaults(func=CmdPrePush)
|
||||
|
||||
@@ -146,9 +146,7 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Ancestor's version of the conflicting file.",
|
||||
)
|
||||
merge_driver_parser.add_argument(
|
||||
"--our",
|
||||
required=True,
|
||||
help="Current version of the conflicting file.",
|
||||
"--our", required=True, help="Current version of the conflicting file."
|
||||
)
|
||||
merge_driver_parser.add_argument(
|
||||
"--their",
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"url", help="Location of DVC or Git repository to download from"
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"path", help="Path to a file or directory within the repository",
|
||||
"path", help="Path to a file or directory within the repository"
|
||||
).complete = completion.FILE
|
||||
import_parser.add_argument(
|
||||
"-o",
|
||||
|
||||
@@ -58,7 +58,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"remote://remote_name/path/to/file/or/dir (see `dvc remote`)",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"out", nargs="?", help="Destination path to put files to.",
|
||||
"out", nargs="?", help="Destination path to put files to."
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"--file",
|
||||
|
||||
+8
-6
@@ -12,8 +12,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _welcome_message():
|
||||
from dvc.ui import ui
|
||||
|
||||
if analytics.is_enabled():
|
||||
logger.info(
|
||||
ui.write(
|
||||
boxify(
|
||||
"DVC has enabled anonymous aggregate usage analytics.\n"
|
||||
"Read the analytics documentation (and how to opt-out) here:\n"
|
||||
@@ -30,7 +32,7 @@ def _welcome_message():
|
||||
f"- Star us on GitHub: {fmt_link('https://github.com/iterative/dvc')}"
|
||||
).format(yellow=colorama.Fore.YELLOW, nc=colorama.Fore.RESET)
|
||||
|
||||
logger.info(msg)
|
||||
ui.write(msg)
|
||||
|
||||
|
||||
class CmdInit(CmdBaseNoRepo):
|
||||
@@ -39,14 +41,14 @@ class CmdInit(CmdBaseNoRepo):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
self.repo = Repo.init(
|
||||
with Repo.init(
|
||||
".",
|
||||
no_scm=self.args.no_scm,
|
||||
force=self.args.force,
|
||||
subdir=self.args.subdir,
|
||||
)
|
||||
self.config = self.repo.config
|
||||
_welcome_message()
|
||||
) as repo:
|
||||
self.config = repo.config
|
||||
_welcome_message()
|
||||
except InitError:
|
||||
logger.exception("failed to initiate DVC")
|
||||
return 1
|
||||
|
||||
+3
-5
@@ -1,11 +1,9 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, fix_subparsers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from dvc.ui import ui
|
||||
|
||||
|
||||
class CmdLive(CmdBase):
|
||||
@@ -17,7 +15,7 @@ class CmdLive(CmdBase):
|
||||
html_path = self.args.target + ".html"
|
||||
self.repo.plots.write_html(html_path, plots, metrics)
|
||||
|
||||
logger.info(f"\nfile://{os.path.abspath(html_path)}")
|
||||
ui.write("\nfile://", os.path.abspath(html_path), sep="")
|
||||
|
||||
return 0
|
||||
|
||||
@@ -35,7 +33,7 @@ class CmdLiveDiff(CmdLive):
|
||||
def shared_parent_parser():
|
||||
parent_parser = argparse.ArgumentParser(add_help=False)
|
||||
parent_parser.add_argument(
|
||||
"target", help="Logs dir to produce summary from",
|
||||
"target", help="Logs dir to produce summary from"
|
||||
).complete = completion.DIR
|
||||
parent_parser.add_argument(
|
||||
"-o",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
from dvc.command.ls.ls_colors import LsColors
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,10 +37,10 @@ class CmdList(CmdBaseNoRepo):
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(entries))
|
||||
ui.write(json.dumps(entries))
|
||||
elif entries:
|
||||
entries = _prettify(entries, sys.stdout.isatty())
|
||||
logger.info("\n".join(entries))
|
||||
entries = _prettify(entries, with_color=True)
|
||||
ui.write("\n".join(entries))
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception(f"failed to list '{self.args.url}'")
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import BadMetricError, DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,7 +33,7 @@ class CmdMetricsShow(CmdMetricsBase):
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(metrics))
|
||||
ui.write(json.dumps(metrics))
|
||||
else:
|
||||
from dvc.compare import show_metrics
|
||||
|
||||
@@ -73,7 +74,7 @@ class CmdMetricsDiff(CmdMetricsBase):
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(diff))
|
||||
ui.write(json.dumps(diff))
|
||||
else:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
|
||||
+2
-2
@@ -37,9 +37,9 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
move_parser.add_argument(
|
||||
"src", help="Source path to a data file or directory.",
|
||||
"src", help="Source path to a data file or directory."
|
||||
).complete = completion.FILE
|
||||
move_parser.add_argument(
|
||||
"dst", help="Destination path.",
|
||||
"dst", help="Destination path."
|
||||
).complete = completion.FILE
|
||||
move_parser.set_defaults(func=CmdMove)
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,7 +28,7 @@ class CmdParamsDiff(CmdBase):
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(diff))
|
||||
ui.write(json.dumps(diff))
|
||||
else:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,7 +39,7 @@ class CmdPlots(CmdBase):
|
||||
|
||||
if self.args.show_vega:
|
||||
target = self.args.targets[0]
|
||||
logger.info(plots[target])
|
||||
ui.write(plots[target])
|
||||
return 0
|
||||
|
||||
rel: str = self.args.out or "plots.html"
|
||||
@@ -53,13 +54,15 @@ class CmdPlots(CmdBase):
|
||||
|
||||
assert path.is_absolute() # as_uri throws ValueError if not absolute
|
||||
url = path.as_uri()
|
||||
logger.info(url)
|
||||
ui.write(url)
|
||||
if self.args.open:
|
||||
import webbrowser
|
||||
|
||||
opened = webbrowser.open(rel)
|
||||
if not opened:
|
||||
logger.error("Failed to open. Please try opening it manually.")
|
||||
ui.error_write(
|
||||
"Failed to open. Please try opening it manually."
|
||||
)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
@@ -87,7 +90,7 @@ class CmdPlotsDiff(CmdPlots):
|
||||
class CmdPlotsModify(CmdPlots):
|
||||
def run(self):
|
||||
self.repo.plots.modify(
|
||||
self.args.target, props=self._props(), unset=self.args.unset,
|
||||
self.args.target, props=self._props(), unset=self.args.unset
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -160,7 +163,7 @@ def add_parser(subparsers, parent_parser):
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"revisions", nargs="*", default=None, help="Git commits to plot from",
|
||||
"revisions", nargs="*", default=None, help="Git commits to plot from"
|
||||
)
|
||||
_add_props_arguments(plots_diff_parser)
|
||||
_add_output_arguments(plots_diff_parser)
|
||||
@@ -175,7 +178,7 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_modify_parser.add_argument(
|
||||
"target", help="Metric file to set properties to",
|
||||
"target", help="Metric file to set properties to"
|
||||
).complete = completion.FILE
|
||||
_add_props_arguments(plots_modify_parser)
|
||||
plots_modify_parser.add_argument(
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link, fix_subparsers
|
||||
from dvc.command.config import CmdConfig
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdRemote(CmdConfig):
|
||||
def __init__(self, args):
|
||||
@@ -27,7 +25,7 @@ class CmdRemoteAdd(CmdRemote):
|
||||
from dvc.config import ConfigError
|
||||
|
||||
if self.args.default:
|
||||
logger.info(f"Setting '{self.args.name}' as a default remote.")
|
||||
ui.write(f"Setting '{self.args.name}' as a default remote.")
|
||||
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
if self.args.name in conf["remote"] and not self.args.force:
|
||||
@@ -90,7 +88,7 @@ class CmdRemoteDefault(CmdRemote):
|
||||
try:
|
||||
print(conf["core"]["remote"])
|
||||
except KeyError:
|
||||
logger.info("No default remote set")
|
||||
ui.write("No default remote set")
|
||||
return 1
|
||||
else:
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
@@ -116,7 +114,7 @@ class CmdRemoteList(CmdRemote):
|
||||
def run(self):
|
||||
conf = self.config.read(self.args.level)
|
||||
for name, conf in conf["remote"].items():
|
||||
logger.info("{}\t{}".format(name, conf["url"]))
|
||||
ui.write(name, conf["url"], sep="\t")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.command.status import CmdDataStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdRepro(CmdBase):
|
||||
def run(self):
|
||||
from dvc.ui import ui
|
||||
|
||||
stages = self.repo.reproduce(**self._repro_kwargs)
|
||||
if len(stages) == 0:
|
||||
logger.info(CmdDataStatus.UP_TO_DATE_MSG)
|
||||
ui.write(CmdDataStatus.UP_TO_DATE_MSG)
|
||||
else:
|
||||
logger.info(
|
||||
ui.write(
|
||||
"Use `dvc push` to send your updates to " "remote storage."
|
||||
)
|
||||
|
||||
|
||||
+2
-1
@@ -10,8 +10,9 @@ logger = logging.getLogger(__name__)
|
||||
class CmdRoot(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
from dvc.repo import Repo
|
||||
from dvc.ui import ui
|
||||
|
||||
logger.info(relpath(Repo.find_root()))
|
||||
ui.write(relpath(Repo.find_root()))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+12
-33
@@ -33,36 +33,17 @@ class CmdRun(CmdBase):
|
||||
)
|
||||
return 1
|
||||
|
||||
kwargs = vars(self.args)
|
||||
kwargs.update(
|
||||
{
|
||||
"cmd": parse_cmd(self.args.cmd),
|
||||
"fname": kwargs.pop("file"),
|
||||
"no_exec": (self.args.no_exec or bool(self.args.checkpoints)),
|
||||
"run_cache": not kwargs.pop("no_run_cache"),
|
||||
}
|
||||
)
|
||||
try:
|
||||
self.repo.run(
|
||||
cmd=parse_cmd(self.args.cmd),
|
||||
outs=self.args.outs,
|
||||
outs_no_cache=self.args.outs_no_cache,
|
||||
metrics=self.args.metrics,
|
||||
metrics_no_cache=self.args.metrics_no_cache,
|
||||
plots=self.args.plots,
|
||||
plots_no_cache=self.args.plots_no_cache,
|
||||
live=self.args.live,
|
||||
live_no_cache=self.args.live_no_cache,
|
||||
live_no_summary=self.args.live_no_summary,
|
||||
live_no_html=self.args.live_no_html,
|
||||
deps=self.args.deps,
|
||||
params=self.args.params,
|
||||
fname=self.args.file,
|
||||
wdir=self.args.wdir,
|
||||
no_exec=(self.args.no_exec or bool(self.args.checkpoints)),
|
||||
force=self.args.force,
|
||||
run_cache=not self.args.no_run_cache,
|
||||
no_commit=self.args.no_commit,
|
||||
outs_persist=self.args.outs_persist,
|
||||
outs_persist_no_cache=self.args.outs_persist_no_cache,
|
||||
checkpoints=self.args.checkpoints,
|
||||
always_changed=self.args.always_changed,
|
||||
name=self.args.name,
|
||||
single_stage=self.args.single_stage,
|
||||
external=self.args.external,
|
||||
desc=self.args.desc,
|
||||
)
|
||||
self.repo.run(**kwargs)
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
@@ -83,11 +64,9 @@ def add_parser(subparsers, parent_parser):
|
||||
help=RUN_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
run_parser.add_argument("-n", "--name", help="Stage name.")
|
||||
run_parser.add_argument(
|
||||
"-n", "--name", help="Stage name.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--file", metavar="<filename>", help=argparse.SUPPRESS,
|
||||
"--file", metavar="<filename>", help=argparse.SUPPRESS
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--no-exec",
|
||||
|
||||
+10
-9
@@ -9,7 +9,7 @@ from dvc.utils.cli_parse import parse_params
|
||||
from dvc.utils.humanize import truncate_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.output.base import BaseOutput
|
||||
from dvc.output import Output
|
||||
from dvc.stage import Stage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -18,7 +18,7 @@ MAX_TEXT_LENGTH = 80
|
||||
|
||||
|
||||
def generate_description(stage: "Stage") -> str:
|
||||
def part_desc(outs: Iterable["BaseOutput"]) -> str:
|
||||
def part_desc(outs: Iterable["Output"]) -> str:
|
||||
return ", ".join(out.def_path for out in outs)
|
||||
|
||||
if not stage.deps and not stage.outs:
|
||||
@@ -27,7 +27,7 @@ def generate_description(stage: "Stage") -> str:
|
||||
if not stage.outs and stage.deps:
|
||||
return "Depends on " + part_desc(stage.deps)
|
||||
|
||||
def is_plot_or_metric(out: "BaseOutput"):
|
||||
def is_plot_or_metric(out: "Output"):
|
||||
return bool(out.plot) or bool(out.metric)
|
||||
|
||||
desc: List[str] = []
|
||||
@@ -75,9 +75,7 @@ class CmdStageList(CmdBase):
|
||||
# removing duplicates while maintaining order
|
||||
collected = chain.from_iterable(
|
||||
self.repo.stage.collect(
|
||||
target=target,
|
||||
recursive=self.args.recursive,
|
||||
accept_group=True,
|
||||
target=target, recursive=self.args.recursive, accept_group=True
|
||||
)
|
||||
for target in self.args.targets
|
||||
)
|
||||
@@ -115,8 +113,11 @@ def parse_cmd(commands: List[str]) -> str:
|
||||
"""
|
||||
|
||||
def quote_argument(arg: str):
|
||||
should_quote = " " in arg and '"' not in arg
|
||||
return f'"{arg}"' if should_quote else arg
|
||||
if not arg:
|
||||
return '""'
|
||||
if " " in arg and '"' not in arg:
|
||||
return f'"{arg}"'
|
||||
return arg
|
||||
|
||||
if len(commands) < 2:
|
||||
return " ".join(commands)
|
||||
@@ -201,7 +202,7 @@ def _add_common_args(parser):
|
||||
metavar="<path>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live", help="Declare output as dvclive.", metavar="<path>",
|
||||
"--live", help="Declare output as dvclive.", metavar="<path>"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live-no-cache",
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
from dvc.command.data_sync import CmdDataBase
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,7 +27,7 @@ class CmdDataStatus(CmdDataBase):
|
||||
ind = indent * self.STATUS_INDENT
|
||||
|
||||
if isinstance(status, str):
|
||||
logger.info(f"{ind}{status}")
|
||||
ui.write(f"{ind}{status}")
|
||||
return
|
||||
|
||||
if isinstance(status, list):
|
||||
@@ -38,14 +39,13 @@ class CmdDataStatus(CmdDataBase):
|
||||
|
||||
for key, value in status.items():
|
||||
if isinstance(value, str):
|
||||
logger.info("{}{}{}".format(ind, self._normalize(value), key))
|
||||
ui.write("{}{}{}".format(ind, self._normalize(value), key))
|
||||
elif value:
|
||||
logger.info(f"{ind}{key}:")
|
||||
ui.write(f"{ind}{key}:")
|
||||
self._show(value, indent + 1)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo import lock_repo
|
||||
from dvc.ui import ui
|
||||
|
||||
indent = 1 if self.args.cloud else 0
|
||||
|
||||
@@ -72,7 +72,7 @@ class CmdDataStatus(CmdDataBase):
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(st))
|
||||
ui.write(json.dumps(st))
|
||||
return 0
|
||||
|
||||
if st:
|
||||
|
||||
@@ -33,6 +33,6 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
unprotect_parser.add_argument(
|
||||
"targets", nargs="+", help="Data files/directories to unprotect.",
|
||||
"targets", nargs="+", help="Data files/directories to unprotect."
|
||||
).complete = completion.FILE
|
||||
unprotect_parser.set_defaults(func=CmdUnprotect)
|
||||
|
||||
@@ -36,7 +36,7 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"targets", nargs="+", help=".dvc files to update.",
|
||||
"targets", nargs="+", help=".dvc files to update."
|
||||
).complete = completion.DVC_FILE
|
||||
update_parser.add_argument(
|
||||
"--rev",
|
||||
|
||||
@@ -2,6 +2,7 @@ import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -11,7 +12,7 @@ class CmdVersion(CmdBaseNoRepo):
|
||||
from dvc.info import get_dvc_info
|
||||
|
||||
dvc_info = get_dvc_info()
|
||||
logger.info(dvc_info)
|
||||
ui.write(dvc_info, force=True)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+8
-8
@@ -1,4 +1,4 @@
|
||||
from collections.abc import Mapping
|
||||
from collections import abc
|
||||
from itertools import chain, repeat, zip_longest
|
||||
from operator import itemgetter
|
||||
from typing import (
|
||||
@@ -9,6 +9,7 @@ from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Mapping,
|
||||
MutableSequence,
|
||||
Sequence,
|
||||
Tuple,
|
||||
@@ -149,7 +150,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
self._columns[name] = Column([self._fill_value] * len(self))
|
||||
self._keys.append(name)
|
||||
|
||||
def row_from_dict(self, d: Dict[str, "CellT"]) -> None:
|
||||
def row_from_dict(self, d: Mapping[str, "CellT"]) -> None:
|
||||
keys = self.keys()
|
||||
for key in d:
|
||||
if key not in keys:
|
||||
@@ -165,7 +166,9 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
|
||||
ui.table(self, headers=self.keys(), **kwargs)
|
||||
|
||||
def as_dict(self, cols: Iterable[str] = None) -> Iterable[Dict[str, str]]:
|
||||
def as_dict(
|
||||
self, cols: Iterable[str] = None
|
||||
) -> Iterable[Dict[str, "CellT"]]:
|
||||
keys = self.keys() if cols is None else set(cols)
|
||||
return [
|
||||
{k: self._columns[k][i] for k in keys} for i in range(len(self))
|
||||
@@ -183,7 +186,7 @@ def _format_field(
|
||||
if isinstance(_val, float) and precision:
|
||||
func = round if round_digits else _normalize_float
|
||||
return func(_val, precision)
|
||||
if isinstance(_val, Mapping):
|
||||
if isinstance(_val, abc.Mapping):
|
||||
return {k: _format(v) for k, v in _val.items()}
|
||||
if isinstance(_val, list):
|
||||
return [_format(x) for x in _val]
|
||||
@@ -275,10 +278,7 @@ def metrics_table(
|
||||
|
||||
for branch, val in metrics.items():
|
||||
for fname, metric in val.items():
|
||||
row_data: Dict[str, str] = {
|
||||
"Revision": branch,
|
||||
"Path": fname,
|
||||
}
|
||||
row_data: Dict[str, str] = {"Revision": branch, "Path": fname}
|
||||
flattened = (
|
||||
flatten(format_dict(metric))
|
||||
if isinstance(metric, dict)
|
||||
|
||||
+3
-3
@@ -63,7 +63,7 @@ class Config(dict):
|
||||
CONFIG_LOCAL = "config.local"
|
||||
|
||||
def __init__(
|
||||
self, dvc_dir=None, validate=True, fs=None, config=None,
|
||||
self, dvc_dir=None, validate=True, fs=None, config=None
|
||||
): # pylint: disable=super-init-not-called
|
||||
from dvc.fs.local import LocalFileSystem
|
||||
|
||||
@@ -79,7 +79,7 @@ class Config(dict):
|
||||
else:
|
||||
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
|
||||
|
||||
self.wfs = LocalFileSystem(None, {"url": self.dvc_dir})
|
||||
self.wfs = LocalFileSystem(url=self.dvc_dir)
|
||||
self.fs = fs or self.wfs
|
||||
|
||||
self.load(validate=validate, config=config)
|
||||
@@ -154,7 +154,7 @@ class Config(dict):
|
||||
filename = self.files[level]
|
||||
fs = self._get_fs(level)
|
||||
|
||||
if fs.exists(filename, use_dvcignore=False):
|
||||
if fs.exists(filename):
|
||||
with fs.open(filename) as fobj:
|
||||
conf_obj = ConfigObj(fobj)
|
||||
else:
|
||||
|
||||
@@ -93,7 +93,7 @@ HTTP_COMMON = {
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
"ssl_verify": Bool,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"method": str,
|
||||
}
|
||||
WEBDAV_COMMON = {
|
||||
@@ -104,6 +104,7 @@ WEBDAV_COMMON = {
|
||||
"cert_path": str,
|
||||
"key_path": str,
|
||||
"timeout": Coerce(int),
|
||||
"ssl_verify": Any(Bool, str),
|
||||
}
|
||||
|
||||
SCHEMA = {
|
||||
@@ -145,7 +146,7 @@ SCHEMA = {
|
||||
"session_token": str,
|
||||
Optional("listobjects", default=False): Bool, # obsoleted
|
||||
Optional("use_ssl", default=True): Bool,
|
||||
Optional("ssl_verify", default=True): Bool,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"sse": str,
|
||||
"sse_kms_key_id": str,
|
||||
"acl": str,
|
||||
@@ -188,6 +189,7 @@ SCHEMA = {
|
||||
"tenant_id": str,
|
||||
"client_id": str,
|
||||
"client_secret": str,
|
||||
"allow_anonymous_login": bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"oss": {
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ def _spawn_windows(cmd, env):
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
|
||||
_popen(
|
||||
cmd, env=env, creationflags=creationflags, startupinfo=startupinfo,
|
||||
cmd, env=env, creationflags=creationflags, startupinfo=startupinfo
|
||||
).communicate()
|
||||
|
||||
|
||||
|
||||
+49
-32
@@ -1,6 +1,11 @@
|
||||
"""Manages dvc remotes that user can use with push/pull/status commands."""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.objects.file import HashFile
|
||||
from dvc.remote.base import Remote
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,7 +24,11 @@ class DataCloud:
|
||||
def __init__(self, repo):
|
||||
self.repo = repo
|
||||
|
||||
def get_remote(self, name=None, command="<command>"):
|
||||
def get_remote(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
command: str = "<command>",
|
||||
) -> "Remote":
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
if not name:
|
||||
@@ -49,79 +58,87 @@ class DataCloud:
|
||||
return get_remote(self.repo, name=name)
|
||||
|
||||
def push(
|
||||
self, cache, jobs=None, remote=None, show_checksums=False,
|
||||
self,
|
||||
objs: Iterable["HashFile"],
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
show_checksums: bool = False,
|
||||
):
|
||||
"""Push data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
cache (NamedCache): named checksums to push to the cloud.
|
||||
jobs (int): number of jobs that can be running simultaneously.
|
||||
remote (dvc.remote.base.BaseRemote): optional remote to push to.
|
||||
objs: objects to push to the cloud.
|
||||
jobs: number of jobs that can be running simultaneously.
|
||||
remote: optional remote to push to.
|
||||
By default remote from core.remote config option is used.
|
||||
show_checksums (bool): show checksums instead of file names in
|
||||
show_checksums: show checksums instead of file names in
|
||||
information messages.
|
||||
"""
|
||||
remote = self.get_remote(remote, "push")
|
||||
remote_obj = self.get_remote(remote, "push")
|
||||
|
||||
return remote.push(
|
||||
return remote_obj.push(
|
||||
self.repo.odb.local,
|
||||
cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
show_checksums=show_checksums,
|
||||
)
|
||||
|
||||
def pull(
|
||||
self, cache, jobs=None, remote=None, show_checksums=False,
|
||||
self,
|
||||
objs: Iterable["HashFile"],
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
show_checksums: bool = False,
|
||||
):
|
||||
"""Pull data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
cache (NamedCache): named checksums to pull from the cloud.
|
||||
jobs (int): number of jobs that can be running simultaneously.
|
||||
remote (dvc.remote.base.BaseRemote): optional remote to pull from.
|
||||
objs: objects to pull from the cloud.
|
||||
jobs: number of jobs that can be running simultaneously.
|
||||
remote: optional remote to pull from.
|
||||
By default remote from core.remote config option is used.
|
||||
show_checksums (bool): show checksums instead of file names in
|
||||
show_checksums: show checksums instead of file names in
|
||||
information messages.
|
||||
"""
|
||||
remote = self.get_remote(remote, "pull")
|
||||
remote_obj = self.get_remote(remote, "pull")
|
||||
|
||||
return remote.pull(
|
||||
return remote_obj.pull(
|
||||
self.repo.odb.local,
|
||||
cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
show_checksums=show_checksums,
|
||||
)
|
||||
|
||||
def status(
|
||||
self,
|
||||
cache,
|
||||
jobs=None,
|
||||
remote=None,
|
||||
show_checksums=False,
|
||||
log_missing=True,
|
||||
objs: Iterable["HashFile"],
|
||||
jobs: Optional[int] = None,
|
||||
remote: Optional[str] = None,
|
||||
show_checksums: bool = False,
|
||||
log_missing: bool = True,
|
||||
):
|
||||
"""Check status of data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
cache (NamedCache): named checksums to check status for.
|
||||
jobs (int): number of jobs that can be running simultaneously.
|
||||
remote (dvc.remote.base.BaseRemote): optional remote to compare
|
||||
objs: objects to check status for.
|
||||
jobs: number of jobs that can be running simultaneously.
|
||||
remote: optional remote to compare
|
||||
cache to. By default remote from core.remote config option
|
||||
is used.
|
||||
show_checksums (bool): show checksums instead of file names in
|
||||
show_checksums: show checksums instead of file names in
|
||||
information messages.
|
||||
log_missing (bool): log warning messages if file doesn't exist
|
||||
log_missing: log warning messages if file doesn't exist
|
||||
neither in cache, neither in cloud.
|
||||
"""
|
||||
remote = self.get_remote(remote, "status")
|
||||
return remote.status(
|
||||
remote_obj = self.get_remote(remote, "status")
|
||||
return remote_obj.status(
|
||||
self.repo.odb.local,
|
||||
cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
show_checksums=show_checksums,
|
||||
log_missing=log_missing,
|
||||
)
|
||||
|
||||
def get_url_for(self, remote, checksum):
|
||||
remote = self.get_remote(remote)
|
||||
return str(remote.odb.hash_to_path_info(checksum))
|
||||
remote_obj = self.get_remote(remote)
|
||||
return str(remote_obj.odb.hash_to_path_info(checksum))
|
||||
|
||||
+11
-46
@@ -1,58 +1,24 @@
|
||||
from collections import defaultdict
|
||||
from urllib.parse import urlparse
|
||||
from typing import Any, Mapping
|
||||
|
||||
import dvc.output as output
|
||||
from dvc.dependency.azure import AzureDependency
|
||||
from dvc.dependency.gs import GSDependency
|
||||
from dvc.dependency.hdfs import HDFSDependency
|
||||
from dvc.dependency.http import HTTPDependency
|
||||
from dvc.dependency.https import HTTPSDependency
|
||||
from dvc.dependency.local import LocalDependency
|
||||
from dvc.dependency.param import ParamsDependency
|
||||
from dvc.dependency.s3 import S3Dependency
|
||||
from dvc.dependency.ssh import SSHDependency
|
||||
from dvc.dependency.webdav import WebDAVDependency
|
||||
from dvc.dependency.webdavs import WebDAVSDependency
|
||||
from dvc.dependency.webhdfs import WebHDFSDependency
|
||||
from dvc.output.base import BaseOutput
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.output import ARTIFACT_SCHEMA, Output
|
||||
|
||||
from ..fs import get_cloud_fs
|
||||
from .base import Dependency
|
||||
from .param import ParamsDependency
|
||||
from .repo import RepoDependency
|
||||
|
||||
DEP_MAP = {
|
||||
Schemes.LOCAL: LocalDependency,
|
||||
Schemes.SSH: SSHDependency,
|
||||
Schemes.S3: S3Dependency,
|
||||
Schemes.AZURE: AzureDependency,
|
||||
Schemes.GS: GSDependency,
|
||||
Schemes.HDFS: HDFSDependency,
|
||||
Schemes.HTTP: HTTPDependency,
|
||||
Schemes.HTTPS: HTTPSDependency,
|
||||
Schemes.WEBDAV: WebDAVDependency,
|
||||
Schemes.WEBDAVS: WebDAVSDependency,
|
||||
Schemes.WEBHDFS: WebHDFSDependency,
|
||||
}
|
||||
|
||||
|
||||
# NOTE: schema for dependencies is basically the same as for outputs, but
|
||||
# without output-specific entries like 'cache' (whether or not output is
|
||||
# cached, see -o and -O flags for `dvc run`) and 'metric' (whether or not
|
||||
# output is a metrics file and how to parse it, see `-M` flag for `dvc run`).
|
||||
SCHEMA = output.SCHEMA.copy()
|
||||
del SCHEMA[BaseOutput.PARAM_CACHE]
|
||||
del SCHEMA[BaseOutput.PARAM_METRIC]
|
||||
del SCHEMA[BaseOutput.PARAM_DESC]
|
||||
SCHEMA.update(RepoDependency.REPO_SCHEMA)
|
||||
SCHEMA.update(ParamsDependency.PARAM_SCHEMA)
|
||||
SCHEMA: Mapping[str, Any] = {
|
||||
**ARTIFACT_SCHEMA,
|
||||
**RepoDependency.REPO_SCHEMA,
|
||||
**ParamsDependency.PARAM_SCHEMA,
|
||||
}
|
||||
|
||||
|
||||
def _get(stage, p, info):
|
||||
parsed = urlparse(p) if p else None
|
||||
if parsed and parsed.scheme == "remote":
|
||||
fs = get_cloud_fs(stage.repo, name=parsed.netloc)
|
||||
return DEP_MAP[fs.scheme](stage, p, info, fs=fs)
|
||||
|
||||
if info and info.get(RepoDependency.PARAM_REPO):
|
||||
repo = info.pop(RepoDependency.PARAM_REPO)
|
||||
return RepoDependency(repo, stage, p, info)
|
||||
@@ -61,14 +27,13 @@ def _get(stage, p, info):
|
||||
params = info.pop(ParamsDependency.PARAM_PARAMS)
|
||||
return ParamsDependency(stage, p, params)
|
||||
|
||||
dep_cls = DEP_MAP.get(parsed.scheme, LocalDependency)
|
||||
return dep_cls(stage, p, info)
|
||||
return Dependency(stage, p, info)
|
||||
|
||||
|
||||
def loadd_from(stage, d_list):
|
||||
ret = []
|
||||
for d in d_list:
|
||||
p = d.pop(BaseOutput.PARAM_PATH, None)
|
||||
p = d.pop(Output.PARAM_PATH, None)
|
||||
ret.append(_get(stage, p, d))
|
||||
return ret
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
from ..fs.azure import AzureFileSystem
|
||||
|
||||
|
||||
class AzureDependency(BaseDependency, BaseOutput):
|
||||
FS_CLS = AzureFileSystem
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Type
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.output import Output
|
||||
|
||||
|
||||
class DependencyDoesNotExistError(DvcException):
|
||||
@@ -20,7 +21,7 @@ class DependencyIsStageFileError(DvcException):
|
||||
super().__init__(f"DVC file '{path}' cannot be a dependency.")
|
||||
|
||||
|
||||
class BaseDependency:
|
||||
class Dependency(Output):
|
||||
IS_DEPENDENCY = True
|
||||
|
||||
DoesNotExistError = DependencyDoesNotExistError # type: Type[DvcException]
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
from ..fs.gs import GSFileSystem
|
||||
|
||||
|
||||
class GSDependency(BaseDependency, BaseOutput):
|
||||
FS_CLS = GSFileSystem
|
||||
@@ -1,6 +0,0 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.hdfs import HDFSOutput
|
||||
|
||||
|
||||
class HDFSDependency(BaseDependency, HDFSOutput):
|
||||
pass
|
||||
@@ -1,8 +0,0 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
from ..fs.http import HTTPFileSystem
|
||||
|
||||
|
||||
class HTTPDependency(BaseDependency, BaseOutput):
|
||||
FS_CLS = HTTPFileSystem
|
||||
@@ -1,6 +0,0 @@
|
||||
from ..fs.https import HTTPSFileSystem
|
||||
from .http import HTTPDependency
|
||||
|
||||
|
||||
class HTTPSDependency(HTTPDependency):
|
||||
FS_CLS = HTTPSFileSystem
|
||||
@@ -1,6 +0,0 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.local import LocalOutput
|
||||
|
||||
|
||||
class LocalDependency(BaseDependency, LocalOutput):
|
||||
pass
|
||||
@@ -4,11 +4,12 @@ from collections import defaultdict
|
||||
import dpath.util
|
||||
from voluptuous import Any
|
||||
|
||||
from dvc.dependency.local import LocalDependency
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.utils.serialize import LOADERS, ParseError
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
|
||||
class MissingParamsError(DvcException):
|
||||
pass
|
||||
@@ -18,7 +19,7 @@ class BadParamFileError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class ParamsDependency(LocalDependency):
|
||||
class ParamsDependency(Dependency):
|
||||
PARAM_PARAMS = "params"
|
||||
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
|
||||
DEFAULT_PARAMS_FILE = "params.yaml"
|
||||
@@ -39,7 +40,6 @@ class ParamsDependency(LocalDependency):
|
||||
path
|
||||
or os.path.join(stage.repo.root_dir, self.DEFAULT_PARAMS_FILE),
|
||||
info=info,
|
||||
fs=stage.repo.fs,
|
||||
)
|
||||
|
||||
def dumpd(self):
|
||||
@@ -128,7 +128,7 @@ class ParamsDependency(LocalDependency):
|
||||
if missing_params:
|
||||
raise MissingParamsError(
|
||||
"Parameters '{}' are missing from '{}'.".format(
|
||||
", ".join(missing_params), self,
|
||||
", ".join(missing_params), self
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+120
-55
@@ -1,11 +1,19 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set
|
||||
|
||||
from voluptuous import Required
|
||||
|
||||
from dvc.path_info import PathInfo
|
||||
|
||||
from .local import LocalDependency
|
||||
from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.objects.db.base import ObjectDB
|
||||
from dvc.objects.file import HashFile
|
||||
|
||||
|
||||
class RepoDependency(LocalDependency):
|
||||
class RepoDependency(Dependency):
|
||||
PARAM_REPO = "repo"
|
||||
PARAM_URL = "url"
|
||||
PARAM_REV = "rev"
|
||||
@@ -21,47 +29,22 @@ class RepoDependency(LocalDependency):
|
||||
|
||||
def __init__(self, def_repo, stage, *args, **kwargs):
|
||||
self.def_repo = def_repo
|
||||
self._staged_objs: Dict[str, "HashFile"] = {}
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
def _parse_path(self, fs, path):
|
||||
def _parse_path(self, fs, path_info):
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_in_repo(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def repo_pair(self):
|
||||
d = self.def_repo
|
||||
rev = d.get(self.PARAM_REV_LOCK) or d.get(self.PARAM_REV)
|
||||
return d[self.PARAM_URL], rev
|
||||
|
||||
def __str__(self):
|
||||
return "{} ({})".format(self.def_path, self.def_repo[self.PARAM_URL])
|
||||
|
||||
def _make_repo(self, *, locked=True, **kwargs):
|
||||
from dvc.external_repo import external_repo
|
||||
|
||||
d = self.def_repo
|
||||
rev = (d.get("rev_lock") if locked else None) or d.get("rev")
|
||||
return external_repo(d["url"], rev=rev, **kwargs)
|
||||
|
||||
def _get_hash(self, locked=True):
|
||||
from dvc.objects.stage import stage
|
||||
|
||||
with self._make_repo(locked=locked) as repo:
|
||||
path_info = PathInfo(repo.root_dir) / self.def_path
|
||||
return stage(
|
||||
self.repo.odb.local,
|
||||
path_info,
|
||||
repo.repo_fs,
|
||||
self.repo.odb.local.fs.PARAM_CHECKSUM,
|
||||
follow_subrepos=False,
|
||||
).hash_info
|
||||
|
||||
def workspace_status(self):
|
||||
current = self._get_hash(locked=True)
|
||||
updated = self._get_hash(locked=False)
|
||||
current = self.get_obj(locked=True).hash_info
|
||||
updated = self.get_obj(locked=False).hash_info
|
||||
|
||||
if current != updated:
|
||||
return {str(self): "update available"}
|
||||
@@ -79,37 +62,28 @@ class RepoDependency(LocalDependency):
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc.checkout import checkout
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputOrStageError
|
||||
from dvc.objects import save
|
||||
from dvc.objects.stage import stage
|
||||
from dvc.objects.db.git import GitObjectDB
|
||||
from dvc.repo.fetch import fetch_from_odb
|
||||
|
||||
odb = self.repo.odb.local
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
if not isinstance(odb, GitObjectDB):
|
||||
fetch_from_odb(self.repo, odb, objs, jobs=jobs)
|
||||
|
||||
with self._make_repo(cache_dir=odb.cache_dir) as repo:
|
||||
if self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
|
||||
path_info = PathInfo(repo.root_dir) / self.def_path
|
||||
try:
|
||||
repo.fetch([path_info.fspath], jobs=jobs, recursive=True)
|
||||
except (NoOutputOrStageError, NoRemoteError):
|
||||
pass
|
||||
obj = stage(
|
||||
odb,
|
||||
path_info,
|
||||
repo.repo_fs,
|
||||
odb.fs.PARAM_CHECKSUM,
|
||||
jobs=jobs,
|
||||
follow_subrepos=False,
|
||||
)
|
||||
save(odb, obj, jobs=jobs)
|
||||
|
||||
checkout(to.path_info, to.fs, obj, odb)
|
||||
obj = self.get_obj()
|
||||
save(self.repo.odb.local, obj, jobs=jobs)
|
||||
checkout(
|
||||
to.path_info,
|
||||
to.fs,
|
||||
obj,
|
||||
self.repo.odb.local,
|
||||
dvcignore=None,
|
||||
state=self.repo.state,
|
||||
)
|
||||
|
||||
def update(self, rev=None):
|
||||
if rev:
|
||||
self.def_repo[self.PARAM_REV] = rev
|
||||
|
||||
with self._make_repo(locked=False) as repo:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
|
||||
|
||||
@@ -118,3 +92,94 @@ class RepoDependency(LocalDependency):
|
||||
# origin project url and rev_lock, and it makes RepoDependency
|
||||
# immutable, hence its impossible for checksum to change.
|
||||
return False
|
||||
|
||||
def get_used_objs(
|
||||
self, **kwargs
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]:
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputOrStageError
|
||||
from dvc.objects.db.git import GitObjectDB
|
||||
from dvc.objects.stage import stage
|
||||
|
||||
local_odb = self.repo.odb.local
|
||||
locked = kwargs.pop("locked", True)
|
||||
with self._make_repo(
|
||||
locked=locked, cache_dir=local_odb.cache_dir
|
||||
) as repo:
|
||||
used_objs = defaultdict(set)
|
||||
rev = repo.get_rev()
|
||||
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = rev
|
||||
|
||||
path_info = PathInfo(repo.root_dir) / str(self.def_path)
|
||||
try:
|
||||
for odb, objs in repo.used_objs(
|
||||
[os.fspath(path_info)],
|
||||
force=True,
|
||||
jobs=kwargs.get("jobs"),
|
||||
recursive=True,
|
||||
).items():
|
||||
if odb is None:
|
||||
odb = repo.cloud.get_remote().odb
|
||||
self._check_circular_import(odb)
|
||||
used_objs[odb].update(objs)
|
||||
except (NoRemoteError, NoOutputOrStageError):
|
||||
pass
|
||||
|
||||
staged_obj = stage(
|
||||
local_odb,
|
||||
path_info,
|
||||
repo.repo_fs,
|
||||
local_odb.fs.PARAM_CHECKSUM,
|
||||
)
|
||||
self._staged_objs[rev] = staged_obj
|
||||
git_odb = GitObjectDB(repo.repo_fs, repo.root_dir)
|
||||
used_objs[git_odb].add(staged_obj)
|
||||
return used_objs
|
||||
|
||||
def _check_circular_import(self, odb):
|
||||
from dvc.exceptions import CircularImportError
|
||||
from dvc.fs.repo import RepoFileSystem
|
||||
|
||||
if not odb or not isinstance(odb.fs, RepoFileSystem):
|
||||
return
|
||||
|
||||
self_url = self.repo.url or self.repo.root_dir
|
||||
if odb.fs.repo_url is not None and odb.fs.repo_url == self_url:
|
||||
raise CircularImportError(self, odb.fs.repo_url, self_url)
|
||||
|
||||
def get_obj(self, filter_info=None, **kwargs):
|
||||
from dvc.objects.stage import stage
|
||||
|
||||
odb = self.repo.odb.local
|
||||
locked = kwargs.pop("locked", True)
|
||||
with self._make_repo(locked=locked, cache_dir=odb.cache_dir) as repo:
|
||||
rev = repo.get_rev()
|
||||
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = rev
|
||||
obj = self._staged_objs.get(rev)
|
||||
if obj is not None:
|
||||
return obj
|
||||
|
||||
path_info = PathInfo(repo.root_dir) / str(self.def_path)
|
||||
obj = stage(
|
||||
odb,
|
||||
path_info,
|
||||
repo.repo_fs,
|
||||
odb.fs.PARAM_CHECKSUM,
|
||||
)
|
||||
self._staged_objs[rev] = obj
|
||||
return obj
|
||||
|
||||
def _make_repo(self, locked=True, **kwargs):
|
||||
from dvc.external_repo import external_repo
|
||||
|
||||
d = self.def_repo
|
||||
rev = self._get_rev(locked=locked)
|
||||
return external_repo(d[self.PARAM_URL], rev=rev, **kwargs)
|
||||
|
||||
def _get_rev(self, locked=True):
|
||||
d = self.def_repo
|
||||
return (d.get(self.PARAM_REV_LOCK) if locked else None) or d.get(
|
||||
self.PARAM_REV
|
||||
)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.s3 import S3Output
|
||||
|
||||
|
||||
class S3Dependency(BaseDependency, S3Output):
|
||||
pass
|
||||
@@ -1,6 +0,0 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.ssh import SSHOutput
|
||||
|
||||
|
||||
class SSHDependency(BaseDependency, SSHOutput):
|
||||
pass
|
||||
@@ -1,8 +0,0 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
from ..fs.webdav import WebDAVFileSystem
|
||||
|
||||
|
||||
class WebDAVDependency(BaseDependency, BaseOutput):
|
||||
FS_CLS = WebDAVFileSystem
|
||||
@@ -1,6 +0,0 @@
|
||||
from ..fs.webdavs import WebDAVSFileSystem
|
||||
from .webdav import WebDAVDependency
|
||||
|
||||
|
||||
class WebDAVSDependency(WebDAVDependency):
|
||||
FS_CLS = WebDAVSFileSystem
|
||||
@@ -1,6 +0,0 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.webhdfs import WebHDFSOutput
|
||||
|
||||
|
||||
class WebHDFSDependency(BaseDependency, WebHDFSOutput):
|
||||
pass
|
||||
+32
-14
@@ -37,8 +37,12 @@ PIPELINE_LOCK = "dvc.lock"
|
||||
|
||||
|
||||
class FileIsGitIgnored(DvcException):
|
||||
def __init__(self, path):
|
||||
super().__init__(f"'{path}' is git-ignored.")
|
||||
def __init__(self, path, pipeline_file=False):
|
||||
super().__init__(
|
||||
"{}'{}' is git-ignored.".format(
|
||||
"bad DVC file name " if pipeline_file else "", path
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class LockfileCorruptedError(DvcException):
|
||||
@@ -66,7 +70,19 @@ def is_lock_file(path):
|
||||
return os.path.basename(path) == PIPELINE_LOCK
|
||||
|
||||
|
||||
def check_dvc_filename(path):
|
||||
def is_git_ignored(repo, path):
|
||||
from dvc.fs.local import LocalFileSystem
|
||||
from dvc.scm.base import NoSCMError
|
||||
|
||||
try:
|
||||
return isinstance(repo.fs, LocalFileSystem) and repo.scm.is_ignored(
|
||||
path
|
||||
)
|
||||
except NoSCMError:
|
||||
return False
|
||||
|
||||
|
||||
def check_dvcfile_path(repo, path):
|
||||
if not is_valid_filename(path):
|
||||
raise StageFileBadNameError(
|
||||
"bad DVC file name '{}'. DVC files should be named "
|
||||
@@ -75,6 +91,9 @@ def check_dvc_filename(path):
|
||||
)
|
||||
)
|
||||
|
||||
if is_git_ignored(repo, path):
|
||||
raise FileIsGitIgnored(relpath(path), True)
|
||||
|
||||
|
||||
class FileMixin:
|
||||
SCHEMA = None
|
||||
@@ -105,18 +124,15 @@ class FileMixin:
|
||||
return relpath(self.path)
|
||||
|
||||
def exists(self):
|
||||
return self.repo.fs.exists(self.path)
|
||||
is_ignored = self.repo.dvcignore.is_ignored_file(self.path)
|
||||
return self.repo.fs.exists(self.path) and not is_ignored
|
||||
|
||||
def _is_git_ignored(self):
|
||||
from dvc.fs.local import LocalFileSystem
|
||||
|
||||
return isinstance(
|
||||
self.repo.fs, LocalFileSystem
|
||||
) and self.repo.scm.is_ignored(self.path)
|
||||
return is_git_ignored(self.repo, self.path)
|
||||
|
||||
def _verify_filename(self):
|
||||
if self.verify:
|
||||
check_dvc_filename(self.path)
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
|
||||
def _check_gitignored(self):
|
||||
if self._is_git_ignored():
|
||||
@@ -129,8 +145,10 @@ class FileMixin:
|
||||
# 3. path doesn't represent a regular file
|
||||
# 4. when the file is git ignored
|
||||
if not self.exists():
|
||||
is_ignored = self.repo.fs.exists(self.path, use_dvcignore=False)
|
||||
raise StageFileDoesNotExistError(self.path, dvc_ignored=is_ignored)
|
||||
dvc_ignored = self.repo.dvcignore.is_ignored_file(self.path)
|
||||
raise StageFileDoesNotExistError(
|
||||
self.path, dvc_ignored=dvc_ignored
|
||||
)
|
||||
|
||||
self._verify_filename()
|
||||
if not self.repo.fs.isfile(self.path):
|
||||
@@ -181,7 +199,7 @@ class SingleStageFile(FileMixin):
|
||||
|
||||
assert not isinstance(stage, PipelineStage)
|
||||
if self.verify:
|
||||
check_dvc_filename(self.path)
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
logger.debug(
|
||||
"Saving information to '{file}'.".format(file=relpath(self.path))
|
||||
)
|
||||
@@ -218,7 +236,7 @@ class PipelineFile(FileMixin):
|
||||
|
||||
assert isinstance(stage, PipelineStage)
|
||||
if self.verify:
|
||||
check_dvc_filename(self.path)
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
|
||||
if update_pipeline and not stage.is_data_source:
|
||||
self._dump_pipeline_file(stage)
|
||||
|
||||
@@ -6,3 +6,4 @@ DVCLIVE_PATH = "DVCLIVE_PATH"
|
||||
DVCLIVE_SUMMARY = "DVCLIVE_SUMMARY"
|
||||
DVCLIVE_HTML = "DVCLIVE_HTML"
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
|
||||
+11
-3
@@ -35,7 +35,7 @@ class OutputDuplicationError(DvcException):
|
||||
)
|
||||
else:
|
||||
msg = "output '{}' is already specified in stages:\n{}".format(
|
||||
output, "\n".join(f"\t- {s.addressing}" for s in stages),
|
||||
output, "\n".join(f"\t- {s.addressing}" for s in stages)
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.stages = stages
|
||||
@@ -283,7 +283,7 @@ class CheckoutError(DvcException):
|
||||
m = (
|
||||
"Checkout failed for following targets:\n{}\nIs your "
|
||||
"cache up to date?\n{}".format(
|
||||
"\n".join(targets), error_link("missing-files"),
|
||||
"\n".join(targets), error_link("missing-files")
|
||||
)
|
||||
)
|
||||
super().__init__(m)
|
||||
@@ -380,7 +380,15 @@ class CacheLinkError(DvcException):
|
||||
|
||||
def __init__(self, path_infos):
|
||||
msg = "No possible cache link types for '{}'. {}".format(
|
||||
", ".join([str(path) for path in path_infos]), self.SUPPORT_LINK,
|
||||
", ".join([str(path) for path in path_infos]), self.SUPPORT_LINK
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.path_infos = path_infos
|
||||
|
||||
|
||||
class CircularImportError(DvcException):
|
||||
def __init__(self, dep, a, b):
|
||||
super().__init__(
|
||||
f"'{dep}' contains invalid circular import. "
|
||||
f"DVC repo '{a}' already imports from '{b}'."
|
||||
)
|
||||
|
||||
@@ -36,10 +36,7 @@ def external_repo(
|
||||
rev = rev or "refs/remotes/origin/HEAD"
|
||||
|
||||
cache_config = {
|
||||
"cache": {
|
||||
"dir": cache_dir or _get_cache_dir(url),
|
||||
"type": cache_types,
|
||||
}
|
||||
"cache": {"dir": cache_dir or _get_cache_dir(url), "type": cache_types}
|
||||
}
|
||||
|
||||
config = _get_remote_config(url) if os.path.isdir(url) else {}
|
||||
|
||||
+29
-4
@@ -12,8 +12,7 @@ from .local import LocalFileSystem
|
||||
from .oss import OSSFileSystem
|
||||
from .s3 import S3FileSystem
|
||||
from .ssh import SSHFileSystem
|
||||
from .webdav import WebDAVFileSystem
|
||||
from .webdavs import WebDAVSFileSystem
|
||||
from .webdav import WebDAVFileSystem, WebDAVSFileSystem
|
||||
from .webhdfs import WebHDFSFileSystem
|
||||
|
||||
FS_MAP = {
|
||||
@@ -82,9 +81,35 @@ def get_cloud_fs(repo, **kwargs):
|
||||
from dvc.config import ConfigError
|
||||
from dvc.config_schema import SCHEMA, Invalid
|
||||
|
||||
remote_conf = get_fs_config(repo.config, **kwargs)
|
||||
repo_config = repo.config if repo else {}
|
||||
core_config = repo_config.get("core", {})
|
||||
|
||||
remote_conf = get_fs_config(repo_config, **kwargs)
|
||||
try:
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf)
|
||||
except Invalid as exc:
|
||||
raise ConfigError(str(exc)) from None
|
||||
return get_fs_cls(remote_conf)(repo, remote_conf)
|
||||
|
||||
if "jobs" not in remote_conf:
|
||||
jobs = core_config.get("jobs")
|
||||
if jobs:
|
||||
remote_conf["jobs"] = jobs
|
||||
|
||||
if "checksum_jobs" not in remote_conf:
|
||||
checksum_jobs = core_config.get("checksum_jobs")
|
||||
if checksum_jobs:
|
||||
remote_conf["checksum_jobs"] = checksum_jobs
|
||||
|
||||
cls = get_fs_cls(remote_conf)
|
||||
|
||||
if cls == GDriveFileSystem and repo:
|
||||
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
|
||||
|
||||
url = remote_conf.pop("url")
|
||||
path_info = cls.PATH_CLS(
|
||||
cls._strip_protocol(url) # pylint:disable=protected-access
|
||||
)
|
||||
|
||||
extras = cls._get_kwargs_from_urls(url) # pylint:disable=protected-access
|
||||
conf = {**extras, **remote_conf} # remote config takes priority
|
||||
return cls, conf, path_info
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from dvc.output import BaseOutput
|
||||
from dvc.output import Output
|
||||
from dvc.path_info import PathInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -33,7 +33,7 @@ class Metadata:
|
||||
# optional fields
|
||||
isdir: bool = False # is it a directory?
|
||||
is_exec: bool = False # is it an executable?
|
||||
outs: List[BaseOutput] = field(default_factory=list) # list of outputs
|
||||
outs: List[Output] = field(default_factory=list) # list of outputs
|
||||
|
||||
def __post_init__(self):
|
||||
self.output_exists = bool(self.outs)
|
||||
|
||||
+102
-37
@@ -1,15 +1,19 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
from fsspec.utils import infer_storage_options
|
||||
from funcy import cached_property, memoize, wrap_prop
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.utils import format_link
|
||||
|
||||
from .fsspec_wrapper import FSSpecWrapper
|
||||
from .fsspec_wrapper import ObjectFSWrapper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_DEFAULT_CREDS_STEPS = (
|
||||
@@ -19,11 +23,66 @@ _DEFAULT_CREDS_STEPS = (
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _temp_event_loop():
|
||||
"""When trying to initalize azure filesystem instances
|
||||
with DefaultCredentials, the authentication process requires
|
||||
to have an access to a separate event loop. The normal calls
|
||||
are run in a separate loop managed by the fsspec, but the
|
||||
DefaultCredentials assumes that the callee is managing their
|
||||
own event loop. This function checks whether is there any
|
||||
event loop set, and if not it creates a temporary event loop
|
||||
and set it. After the context is finalized, it restores the
|
||||
original event loop back (if there is any)."""
|
||||
|
||||
try:
|
||||
original_loop = asyncio.get_event_loop()
|
||||
original_policy = asyncio.get_event_loop_policy()
|
||||
except RuntimeError:
|
||||
original_loop = None
|
||||
original_policy = None
|
||||
|
||||
# From 3.8>= and onwards, asyncio changed the default
|
||||
# loop policy for windows to use proactor loops instead
|
||||
# of selector based ones. Due to that, proxied connections
|
||||
# doesn't work with the aiohttp and this is most likely an
|
||||
# upstream bug that needs to be solved outside of DVC. Until
|
||||
# such issue is resolved, we need to manage this;
|
||||
# https://github.com/aio-libs/aiohttp/issues/4536
|
||||
if sys.version_info >= (3, 8) and os.name == "nt":
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
loop = original_loop or asyncio.new_event_loop()
|
||||
|
||||
try:
|
||||
asyncio.set_event_loop(loop)
|
||||
yield
|
||||
finally:
|
||||
if original_loop is None:
|
||||
loop.close()
|
||||
asyncio.set_event_loop(original_loop)
|
||||
asyncio.set_event_loop_policy(original_policy)
|
||||
|
||||
|
||||
class AzureAuthError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class AzureFileSystem(FSSpecWrapper):
|
||||
@memoize
|
||||
def _az_config():
|
||||
# NOTE: ideally we would've used get_default_cli().config from
|
||||
# azure.cli.core, but azure-cli-core has a lot of conflicts with other
|
||||
# dependencies. So instead we are just use knack directly
|
||||
from knack.config import CLIConfig
|
||||
|
||||
config_dir = os.getenv(
|
||||
"AZURE_CONFIG_DIR", os.path.expanduser(os.path.join("~", ".azure"))
|
||||
)
|
||||
return CLIConfig(config_dir=config_dir, config_env_var_prefix="AZURE")
|
||||
|
||||
|
||||
# pylint:disable=abstract-method
|
||||
class AzureFileSystem(ObjectFSWrapper):
|
||||
scheme = Schemes.AZURE
|
||||
PATH_CLS = CloudURLInfo
|
||||
PARAM_CHECKSUM = "etag"
|
||||
@@ -34,21 +93,23 @@ class AzureFileSystem(FSSpecWrapper):
|
||||
"azure-identity": "azure.identity",
|
||||
}
|
||||
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str):
|
||||
bucket = infer_storage_options(path).get("host")
|
||||
if bucket:
|
||||
return path
|
||||
|
||||
url = config.get("url")
|
||||
self.path_info = self.PATH_CLS(url)
|
||||
self.bucket = self.path_info.bucket
|
||||
bucket = _az_config().get("storage", "container_name", None)
|
||||
return f"azure://{bucket}"
|
||||
|
||||
if not self.bucket:
|
||||
container = self._az_config.get("storage", "container_name", None)
|
||||
url = f"azure://{container}"
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
ops = infer_storage_options(urlpath)
|
||||
if "host" in ops:
|
||||
return {"bucket": ops["host"]}
|
||||
return {}
|
||||
|
||||
self.path_info = self.PATH_CLS(url)
|
||||
self.bucket = self.path_info.bucket
|
||||
|
||||
def _prepare_credentials(self, config):
|
||||
def _prepare_credentials(self, **config):
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
|
||||
# Disable spam from failed cred types for DefaultAzureCredential
|
||||
@@ -57,23 +118,36 @@ class AzureFileSystem(FSSpecWrapper):
|
||||
login_info = {}
|
||||
login_info["connection_string"] = config.get(
|
||||
"connection_string",
|
||||
self._az_config.get("storage", "connection_string", None),
|
||||
_az_config().get("storage", "connection_string", None),
|
||||
)
|
||||
login_info["account_name"] = config.get(
|
||||
"account_name", self._az_config.get("storage", "account", None)
|
||||
"account_name", _az_config().get("storage", "account", None)
|
||||
)
|
||||
login_info["account_key"] = config.get(
|
||||
"account_key", self._az_config.get("storage", "key", None)
|
||||
"account_key", _az_config().get("storage", "key", None)
|
||||
)
|
||||
login_info["sas_token"] = config.get(
|
||||
"sas_token", self._az_config.get("storage", "sas_token", None)
|
||||
"sas_token", _az_config().get("storage", "sas_token", None)
|
||||
)
|
||||
login_info["tenant_id"] = config.get("tenant_id")
|
||||
login_info["client_id"] = config.get("client_id")
|
||||
login_info["client_secret"] = config.get("client_secret")
|
||||
|
||||
if not any(
|
||||
if not (login_info["account_name"] or login_info["connection_string"]):
|
||||
raise AzureAuthError(
|
||||
"Authentication to Azure Blob Storage requires either "
|
||||
"account_name or connection_string.\nLearn more about "
|
||||
"configuration settings at "
|
||||
+ format_link("https://man.dvc.org/remote/modify")
|
||||
)
|
||||
|
||||
any_secondary = any(
|
||||
value for key, value in login_info.items() if key != "account_name"
|
||||
)
|
||||
if (
|
||||
login_info["account_name"]
|
||||
and not any_secondary
|
||||
and not config.get("allow_anonymous_login", False)
|
||||
):
|
||||
login_info["credential"] = DefaultAzureCredential(
|
||||
exclude_interactive_browser_credential=False
|
||||
@@ -101,18 +175,6 @@ class AzureFileSystem(FSSpecWrapper):
|
||||
self.login_method = login_method
|
||||
return login_info
|
||||
|
||||
@cached_property
|
||||
def _az_config(self):
|
||||
# NOTE: ideally we would've used get_default_cli().config from
|
||||
# azure.cli.core, but azure-cli-core has a lot of conflicts with other
|
||||
# dependencies. So instead we are just use knack directly
|
||||
from knack.config import CLIConfig
|
||||
|
||||
config_dir = os.getenv(
|
||||
"AZURE_CONFIG_DIR", os.path.expanduser(os.path.join("~", ".azure"))
|
||||
)
|
||||
return CLIConfig(config_dir=config_dir, config_env_var_prefix="AZURE")
|
||||
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self):
|
||||
@@ -120,11 +182,8 @@ class AzureFileSystem(FSSpecWrapper):
|
||||
from azure.core.exceptions import AzureError
|
||||
|
||||
try:
|
||||
file_system = AzureBlobFileSystem(**self.fs_args)
|
||||
if self.bucket not in [
|
||||
container.rstrip("/") for container in file_system.ls("/")
|
||||
]:
|
||||
file_system.mkdir(self.bucket)
|
||||
with _temp_event_loop():
|
||||
file_system = AzureBlobFileSystem(**self.fs_args)
|
||||
except (ValueError, AzureError) as e:
|
||||
raise AzureAuthError(
|
||||
f"Authentication to Azure Blob Storage via {self.login_method}"
|
||||
@@ -133,3 +192,9 @@ class AzureFileSystem(FSSpecWrapper):
|
||||
) from e
|
||||
|
||||
return file_system
|
||||
|
||||
def open(
|
||||
self, path_info, mode="r", **kwargs
|
||||
): # pylint: disable=arguments-differ
|
||||
with _temp_event_loop():
|
||||
return self.fs.open(self._with_bucket(path_info), mode=mode)
|
||||
|
||||
+36
-39
@@ -4,8 +4,6 @@ from functools import partial, partialmethod
|
||||
from multiprocessing import cpu_count
|
||||
from typing import Any, ClassVar, Dict, FrozenSet, Optional
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.path_info import URLInfo
|
||||
from dvc.progress import Tqdm
|
||||
@@ -54,29 +52,19 @@ class BaseFileSystem:
|
||||
PARAM_CHECKSUM: ClassVar[Optional[str]] = None
|
||||
DETAIL_FIELDS: FrozenSet[str] = frozenset()
|
||||
|
||||
def __init__(self, repo, config):
|
||||
self.repo = repo
|
||||
self.config = config
|
||||
def __init__(self, **kwargs):
|
||||
self._check_requires(**kwargs)
|
||||
|
||||
self._check_requires()
|
||||
self.jobs = kwargs.get("jobs") or self._JOBS
|
||||
self.hash_jobs = kwargs.get("checksum_jobs") or self.HASH_JOBS
|
||||
|
||||
self.path_info = None
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str):
|
||||
return path
|
||||
|
||||
@cached_property
|
||||
def jobs(self):
|
||||
return (
|
||||
self.config.get("jobs")
|
||||
or (self.repo and self.repo.config["core"].get("jobs"))
|
||||
or self._JOBS
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def hash_jobs(self):
|
||||
return (
|
||||
self.config.get("checksum_jobs")
|
||||
or (self.repo and self.repo.config["core"].get("checksum_jobs"))
|
||||
or self.HASH_JOBS
|
||||
)
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath): # pylint:disable=unused-argument
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def get_missing_deps(cls):
|
||||
@@ -91,7 +79,7 @@ class BaseFileSystem:
|
||||
|
||||
return missing
|
||||
|
||||
def _check_requires(self):
|
||||
def _check_requires(self, **kwargs):
|
||||
from ..scheme import Schemes
|
||||
from ..utils import format_link
|
||||
from ..utils.pkg import PKG
|
||||
@@ -100,7 +88,7 @@ class BaseFileSystem:
|
||||
if not missing:
|
||||
return
|
||||
|
||||
url = self.config.get("url", f"{self.scheme}://")
|
||||
url = kwargs.get("url", f"{self.scheme}://")
|
||||
|
||||
scheme = self.scheme
|
||||
if scheme == Schemes.WEBDAVS:
|
||||
@@ -139,7 +127,7 @@ class BaseFileSystem:
|
||||
|
||||
raise RemoteActionNotImplemented("open", self.scheme)
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True) -> bool:
|
||||
def exists(self, path_info) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@@ -166,6 +154,10 @@ class BaseFileSystem:
|
||||
"""Check if this file is an independent copy."""
|
||||
return False # We can't be sure by default
|
||||
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
"""Return a generator with (root, dirs, files)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
"""Return a generator with `PathInfo`s to all the files.
|
||||
|
||||
@@ -178,7 +170,7 @@ class BaseFileSystem:
|
||||
def ls(self, path_info, detail=False):
|
||||
raise RemoteActionNotImplemented("ls", self.scheme)
|
||||
|
||||
def find(self, path_info, detail=False):
|
||||
def find(self, path_info, detail=False, prefix=None):
|
||||
raise RemoteActionNotImplemented("find", self.scheme)
|
||||
|
||||
def is_empty(self, path_info):
|
||||
@@ -222,9 +214,7 @@ class BaseFileSystem:
|
||||
return False
|
||||
return hash_.endswith(cls.CHECKSUM_DIR_SUFFIX)
|
||||
|
||||
def upload(
|
||||
self, from_info, to_info, name=None, no_progress_bar=False,
|
||||
):
|
||||
def upload(self, from_info, to_info, name=None, no_progress_bar=False):
|
||||
if not hasattr(self, "_upload"):
|
||||
raise RemoteActionNotImplemented("upload", self.scheme)
|
||||
|
||||
@@ -245,14 +235,23 @@ class BaseFileSystem:
|
||||
no_progress_bar=no_progress_bar,
|
||||
)
|
||||
|
||||
def upload_fobj(self, fobj, to_info, no_progress_bar=False, **pbar_args):
|
||||
def upload_fobj(
|
||||
self, fobj, to_info, no_progress_bar=False, size=None, **pbar_args
|
||||
):
|
||||
if not hasattr(self, "_upload_fobj"):
|
||||
raise RemoteActionNotImplemented("upload_fobj", self.scheme)
|
||||
|
||||
with Tqdm.wrapattr(
|
||||
fobj, "read", disable=no_progress_bar, bytes=True, **pbar_args
|
||||
fobj,
|
||||
"read",
|
||||
disable=no_progress_bar,
|
||||
bytes=True,
|
||||
total=size,
|
||||
**pbar_args,
|
||||
) as wrapped:
|
||||
self._upload_fobj(wrapped, to_info) # pylint: disable=no-member
|
||||
self._upload_fobj( # pylint: disable=no-member
|
||||
wrapped, to_info, size=size
|
||||
)
|
||||
|
||||
def download(
|
||||
self,
|
||||
@@ -279,14 +278,14 @@ class BaseFileSystem:
|
||||
|
||||
if not _only_file and self.isdir(from_info):
|
||||
return self._download_dir(
|
||||
from_info, to_info, name, no_progress_bar, jobs, **kwargs,
|
||||
from_info, to_info, name, no_progress_bar, jobs, **kwargs
|
||||
)
|
||||
return self._download_file(from_info, to_info, name, no_progress_bar,)
|
||||
return self._download_file(from_info, to_info, name, no_progress_bar)
|
||||
|
||||
download_file = partialmethod(download, _only_file=True)
|
||||
|
||||
def _download_dir(
|
||||
self, from_info, to_info, name, no_progress_bar, jobs, **kwargs,
|
||||
self, from_info, to_info, name, no_progress_bar, jobs, **kwargs
|
||||
):
|
||||
from_infos = list(self.walk_files(from_info, **kwargs))
|
||||
if not from_infos:
|
||||
@@ -303,7 +302,7 @@ class BaseFileSystem:
|
||||
disable=no_progress_bar,
|
||||
) as pbar:
|
||||
download_files = pbar.wrap_fn(
|
||||
partial(self._download_file, name=name, no_progress_bar=True,)
|
||||
partial(self._download_file, name=name, no_progress_bar=True)
|
||||
)
|
||||
max_workers = jobs or self.jobs
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
@@ -327,9 +326,7 @@ class BaseFileSystem:
|
||||
entry.cancel()
|
||||
raise exc
|
||||
|
||||
def _download_file(
|
||||
self, from_info, to_info, name, no_progress_bar,
|
||||
):
|
||||
def _download_file(self, from_info, to_info, name, no_progress_bar):
|
||||
makedirs(to_info.parent, exist_ok=True)
|
||||
|
||||
logger.debug("Downloading '%s' to '%s'", from_info, to_info)
|
||||
|
||||
+11
-6
@@ -10,7 +10,7 @@ from ._metadata import Metadata
|
||||
from .base import BaseFileSystem
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from dvc.output.base import BaseOutput
|
||||
from dvc.output import Output
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,8 +26,9 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
scheme = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
def __init__(self, repo):
|
||||
super().__init__(repo, {"url": repo.root_dir})
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.repo = kwargs["repo"]
|
||||
|
||||
def _find_outs(self, path, *args, **kwargs):
|
||||
outs = self.repo.find_outs_by_path(path, *args, **kwargs)
|
||||
@@ -42,7 +43,7 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
return outs
|
||||
|
||||
def _get_granular_hash(
|
||||
self, path_info: PathInfo, out: "BaseOutput", remote=None
|
||||
self, path_info: PathInfo, out: "Output", remote=None
|
||||
):
|
||||
assert isinstance(path_info, PathInfo)
|
||||
# NOTE: use string paths here for performance reasons
|
||||
@@ -67,13 +68,17 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
raise IsADirectoryError
|
||||
|
||||
out = outs[0]
|
||||
|
||||
if not out.hash_info:
|
||||
raise FileNotFoundError
|
||||
|
||||
if out.changed_cache(filter_info=path):
|
||||
from dvc.config import NoRemoteError
|
||||
|
||||
try:
|
||||
remote_obj = self.repo.cloud.get_remote(remote)
|
||||
except NoRemoteError:
|
||||
raise FileNotFoundError
|
||||
except NoRemoteError as exc:
|
||||
raise FileNotFoundError from exc
|
||||
if out.is_dir_checksum:
|
||||
checksum = self._get_granular_hash(path, out).value
|
||||
else:
|
||||
|
||||
+71
-36
@@ -2,6 +2,8 @@ import os
|
||||
import shutil
|
||||
from functools import lru_cache
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
from .base import BaseFileSystem
|
||||
@@ -9,11 +11,14 @@ from .base import BaseFileSystem
|
||||
|
||||
# pylint: disable=no-member
|
||||
class FSSpecWrapper(BaseFileSystem):
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
self.fs_args = {"skip_instance_cache": True}
|
||||
self.fs_args.update(self._prepare_credentials(config))
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.fs_args = {"skip_instance_cache": True}
|
||||
self.fs_args.update(self._prepare_credentials(**kwargs))
|
||||
|
||||
@cached_property
|
||||
def fs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -47,33 +52,28 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
entries within info() and ls(detail=True) calls"""
|
||||
return entry
|
||||
|
||||
def _prepare_credentials(self, config): # pylint: disable=unused-argument
|
||||
def _prepare_credentials(
|
||||
self, **config
|
||||
): # pylint: disable=unused-argument
|
||||
"""Prepare the arguments for authentication to the
|
||||
host filesystem"""
|
||||
return {}
|
||||
|
||||
def isdir(self, path_info):
|
||||
if not self.exists(path_info):
|
||||
return False
|
||||
def _isdir(self, path_info):
|
||||
return self.fs.isdir(self._with_bucket(path_info))
|
||||
|
||||
# Directory in object storages are interpreted differently
|
||||
# among different fsspec providers, so this logic is a temporary
|
||||
# measure for us to adapt as of now. It checks whether it is a
|
||||
# directory (as in a prefix with contents) or whether it is an empty
|
||||
# file where it's name ends with a forward slash
|
||||
entry = self.info(path_info)
|
||||
return entry["type"] == "directory" or (
|
||||
entry["size"] == 0
|
||||
and entry["type"] == "file"
|
||||
and entry["name"].endswith("/")
|
||||
)
|
||||
def isdir(self, path_info):
|
||||
try:
|
||||
return self._isdir(path_info)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def isfile(self, path_info):
|
||||
if not self.exists(path_info):
|
||||
try:
|
||||
return not self._isdir(path_info)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
return not self.isdir(path_info)
|
||||
|
||||
def is_empty(self, path_info):
|
||||
entry = self.info(path_info)
|
||||
if entry["type"] == "directory":
|
||||
@@ -88,7 +88,7 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
def copy(self, from_info, to_info):
|
||||
self.fs.copy(self._with_bucket(from_info), self._with_bucket(to_info))
|
||||
|
||||
def exists(self, path_info, use_dvcignore=False):
|
||||
def exists(self, path_info) -> bool:
|
||||
return self.fs.exists(self._with_bucket(path_info))
|
||||
|
||||
def ls(self, path_info, detail=False):
|
||||
@@ -96,24 +96,17 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
files = self.fs.ls(path, detail=detail)
|
||||
yield from self._strip_buckets(files, detail=detail)
|
||||
|
||||
def find(self, path_info, detail=False):
|
||||
# pylint: disable=unused-argument
|
||||
def find(self, path_info, detail=False, prefix=None):
|
||||
path = self._with_bucket(path_info)
|
||||
files = self.fs.find(path, detail=detail)
|
||||
if detail:
|
||||
files = files.values()
|
||||
|
||||
# When calling find() on a file, it returns the same file in a list.
|
||||
# For object-based storages, the same behavior applies to empty
|
||||
# directories since they are represented as files. This condition
|
||||
# checks whether we should yield an empty list (if it is an empty
|
||||
# directory) or just yield the file itself.
|
||||
if len(files) == 1 and files[0] == path and self.isdir(path_info):
|
||||
return None
|
||||
|
||||
yield from self._strip_buckets(files, detail=detail)
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for file in self.find(path_info):
|
||||
for file in self.find(path_info, **kwargs):
|
||||
yield path_info.replace(path=file)
|
||||
|
||||
def remove(self, path_info):
|
||||
@@ -125,20 +118,20 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
info["name"] = self._strip_bucket(info["name"])
|
||||
return info
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
def _upload_fobj(self, fobj, to_info, size=None):
|
||||
with self.open(to_info, "wb") as fdest:
|
||||
shutil.copyfileobj(fobj, fdest, length=fdest.blocksize)
|
||||
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **kwargs
|
||||
):
|
||||
total = os.path.getsize(from_file)
|
||||
size = os.path.getsize(from_file)
|
||||
with open(from_file, "rb") as fobj:
|
||||
self.upload_fobj(
|
||||
fobj,
|
||||
self._with_bucket(to_info),
|
||||
size=size,
|
||||
desc=name,
|
||||
total=total,
|
||||
no_progress_bar=no_progress_bar,
|
||||
)
|
||||
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
|
||||
@@ -159,3 +152,45 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
) as wrapped:
|
||||
with open(to_file, "wb") as fdest:
|
||||
shutil.copyfileobj(wrapped, fdest, length=fobj.blocksize)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class ObjectFSWrapper(FSSpecWrapper):
|
||||
TRAVERSE_PREFIX_LEN = 3
|
||||
|
||||
def _isdir(self, path_info):
|
||||
# Directory in object storages are interpreted differently
|
||||
# among different fsspec providers, so this logic is a temporary
|
||||
# measure for us to adapt as of now. It checks whether it is a
|
||||
# directory (as in a prefix with contents) or whether it is an empty
|
||||
# file where it's name ends with a forward slash
|
||||
|
||||
entry = self.info(path_info)
|
||||
return entry["type"] == "directory" or (
|
||||
entry["size"] == 0
|
||||
and entry["type"] == "file"
|
||||
and entry["name"].endswith("/")
|
||||
)
|
||||
|
||||
def find(self, path_info, detail=False, prefix=None):
|
||||
if prefix is not None:
|
||||
path = self._with_bucket(path_info.parent)
|
||||
files = self.fs.find(
|
||||
path, detail=detail, prefix=path_info.parts[-1]
|
||||
)
|
||||
else:
|
||||
path = self._with_bucket(path_info)
|
||||
files = self.fs.find(path, detail=detail)
|
||||
|
||||
if detail:
|
||||
files = files.values()
|
||||
|
||||
# When calling find() on a file, it returns the same file in a list.
|
||||
# For object-based storages, the same behavior applies to empty
|
||||
# directories since they are represented as files. This condition
|
||||
# checks whether we should yield an empty list (if it is an empty
|
||||
# directory) or just yield the file itself.
|
||||
if len(files) == 1 and files[0] == path and self.isdir(path_info):
|
||||
return None
|
||||
|
||||
yield from self._strip_buckets(files, detail=detail)
|
||||
|
||||
+24
-13
@@ -85,7 +85,7 @@ class GDriveURLInfo(CloudURLInfo):
|
||||
self._spath = re.sub("/{2,}", "/", self._spath.rstrip("/"))
|
||||
|
||||
|
||||
class GDriveFileSystem(BaseFileSystem):
|
||||
class GDriveFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
scheme = Schemes.GDRIVE
|
||||
PATH_CLS = GDriveURLInfo
|
||||
PARAM_CHECKSUM = "checksum"
|
||||
@@ -100,8 +100,8 @@ class GDriveFileSystem(BaseFileSystem):
|
||||
DEFAULT_GDRIVE_CLIENT_ID = "710796635688-iivsgbgsb6uv1fap6635dhvuei09o66c.apps.googleusercontent.com" # noqa: E501
|
||||
DEFAULT_GDRIVE_CLIENT_SECRET = "a1Fz59uTpVNeG_VGuSKDLJXv"
|
||||
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
self.path_info = self.PATH_CLS(config["url"])
|
||||
|
||||
@@ -126,20 +126,26 @@ class GDriveFileSystem(BaseFileSystem):
|
||||
self._client_id = config.get("gdrive_client_id")
|
||||
self._client_secret = config.get("gdrive_client_secret")
|
||||
self._validate_config()
|
||||
|
||||
tmp_dir = config["gdrive_credentials_tmp_dir"]
|
||||
assert tmp_dir
|
||||
|
||||
self._gdrive_service_credentials_path = tmp_fname(
|
||||
os.path.join(self.repo.tmp_dir, "")
|
||||
os.path.join(tmp_dir, "")
|
||||
)
|
||||
self._gdrive_user_credentials_path = (
|
||||
tmp_fname(os.path.join(self.repo.tmp_dir, ""))
|
||||
tmp_fname(os.path.join(tmp_dir, ""))
|
||||
if os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA)
|
||||
else config.get(
|
||||
"gdrive_user_credentials_file",
|
||||
os.path.join(
|
||||
self.repo.tmp_dir, self.DEFAULT_USER_CREDENTIALS_FILE,
|
||||
),
|
||||
os.path.join(tmp_dir, self.DEFAULT_USER_CREDENTIALS_FILE),
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
return {"url": urlpath}
|
||||
|
||||
def _validate_config(self):
|
||||
# Validate Service Account configuration
|
||||
if (
|
||||
@@ -517,7 +523,7 @@ class GDriveFileSystem(BaseFileSystem):
|
||||
assert not create
|
||||
raise FileMissingError(path_info, hint)
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
def exists(self, path_info) -> bool:
|
||||
try:
|
||||
self._get_item_id(path_info)
|
||||
except FileMissingError:
|
||||
@@ -532,7 +538,7 @@ class GDriveFileSystem(BaseFileSystem):
|
||||
query = f"({query}) and trashed=false"
|
||||
return self._gdrive_list(query)
|
||||
|
||||
def find(self, path_info, detail=False):
|
||||
def find(self, path_info, detail=False, prefix=None):
|
||||
root_path = path_info.path
|
||||
seen_paths = set()
|
||||
|
||||
@@ -612,22 +618,27 @@ class GDriveFileSystem(BaseFileSystem):
|
||||
gdrive_file.FetchMetadata(fields="fileSize")
|
||||
return {"size": gdrive_file.get("fileSize")}
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
def _upload_fobj(self, fobj, to_info, **kwargs):
|
||||
dirname = to_info.parent
|
||||
assert dirname
|
||||
parent_id = self._get_item_id(dirname, create=True)
|
||||
self._gdrive_upload_fobj(fobj, parent_id, to_info.name)
|
||||
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
|
||||
self,
|
||||
from_file,
|
||||
to_info,
|
||||
name=None,
|
||||
no_progress_bar=False,
|
||||
**_kwargs,
|
||||
):
|
||||
with open(from_file, "rb") as fobj:
|
||||
self.upload_fobj(
|
||||
fobj,
|
||||
to_info,
|
||||
size=os.path.getsize(from_file),
|
||||
no_progress_bar=no_progress_bar,
|
||||
desc=name or to_info.name,
|
||||
total=os.path.getsize(from_file),
|
||||
)
|
||||
|
||||
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
|
||||
|
||||
+20
-66
@@ -1,8 +1,6 @@
|
||||
import errno
|
||||
import os
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.utils import is_exec, relpath
|
||||
|
||||
from .base import BaseFileSystem
|
||||
@@ -11,43 +9,29 @@ from .base import BaseFileSystem
|
||||
class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
"""Proxies the repo file access methods to Git objects"""
|
||||
|
||||
def __init__(
|
||||
self, root_dir, trie, use_dvcignore=False, dvcignore_root=None
|
||||
):
|
||||
super().__init__(None, {})
|
||||
self._fs_root = root_dir
|
||||
scheme = "local"
|
||||
|
||||
def __init__(self, root_dir, trie):
|
||||
super().__init__()
|
||||
self._root = root_dir
|
||||
self.trie = trie
|
||||
self.use_dvcignore = use_dvcignore
|
||||
self.dvcignore_root = dvcignore_root
|
||||
|
||||
@property
|
||||
def rev(self):
|
||||
return self.trie.rev
|
||||
|
||||
@property
|
||||
def fs_root(self):
|
||||
return self._fs_root
|
||||
|
||||
def _get_key(self, path):
|
||||
if isinstance(path, str):
|
||||
if not os.path.isabs(path):
|
||||
relparts = path.split(os.sep)
|
||||
else:
|
||||
relparts = relpath(path, self.fs_root).split(os.sep)
|
||||
relparts = relpath(path, self._root).split(os.sep)
|
||||
else:
|
||||
relparts = path.relative_to(self.fs_root).parts
|
||||
relparts = path.relative_to(self._root).parts
|
||||
if relparts == ["."]:
|
||||
return ()
|
||||
return tuple(relparts)
|
||||
|
||||
@cached_property
|
||||
def dvcignore(self):
|
||||
from dvc.ignore import DvcIgnoreFilter, DvcIgnoreFilterNoop
|
||||
|
||||
root = self.dvcignore_root or self.fs_root
|
||||
cls = DvcIgnoreFilter if self.use_dvcignore else DvcIgnoreFilterNoop
|
||||
return cls(self, root)
|
||||
|
||||
def open(
|
||||
self, path, mode="r", encoding=None
|
||||
): # pylint: disable=arguments-differ
|
||||
@@ -66,50 +50,27 @@ class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
errno.EISDIR, os.strerror(errno.EISDIR), path
|
||||
) from exc
|
||||
|
||||
def exists(
|
||||
self, path, use_dvcignore=True
|
||||
): # pylint: disable=arguments-differ
|
||||
def _is_ignored(path):
|
||||
return self.dvcignore.is_ignored_file(
|
||||
path
|
||||
) or self.dvcignore.is_ignored_dir(path)
|
||||
|
||||
if use_dvcignore and _is_ignored(path):
|
||||
return False
|
||||
|
||||
key = self._get_key(path)
|
||||
def exists(self, path_info) -> bool:
|
||||
key = self._get_key(path_info)
|
||||
return self.trie.exists(key)
|
||||
|
||||
def isdir(
|
||||
self, path, use_dvcignore=True
|
||||
): # pylint: disable=arguments-differ
|
||||
if use_dvcignore and self.dvcignore.is_ignored_dir(path):
|
||||
return False
|
||||
key = self._get_key(path)
|
||||
def isdir(self, path_info) -> bool:
|
||||
key = self._get_key(path_info)
|
||||
return self.trie.isdir(key)
|
||||
|
||||
def isfile(self, path): # pylint: disable=arguments-differ
|
||||
if self.dvcignore.is_ignored_file(path):
|
||||
return False
|
||||
key = self._get_key(path)
|
||||
def isfile(self, path_info) -> bool:
|
||||
key = self._get_key(path_info)
|
||||
return self.trie.isfile(key)
|
||||
|
||||
def walk(
|
||||
self,
|
||||
top,
|
||||
topdown=True,
|
||||
onerror=None,
|
||||
use_dvcignore=True,
|
||||
ignore_subrepos=True,
|
||||
):
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
"""Directory tree generator.
|
||||
|
||||
See `os.walk` for the docs. Differences:
|
||||
- no support for symlinks
|
||||
"""
|
||||
if not self.isdir(top, use_dvcignore=use_dvcignore):
|
||||
if not self.isdir(top):
|
||||
if onerror:
|
||||
if self.exists(top, use_dvcignore=use_dvcignore):
|
||||
if self.exists(top):
|
||||
exc = NotADirectoryError(
|
||||
errno.ENOTDIR, os.strerror(errno.ENOTDIR), top
|
||||
)
|
||||
@@ -123,13 +84,9 @@ class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
key = self._get_key(top)
|
||||
for prefix, dirs, files in self.trie.walk(key, topdown=topdown):
|
||||
if prefix:
|
||||
root = os.path.join(self.fs_root, os.sep.join(prefix))
|
||||
root = os.path.join(self._root, os.sep.join(prefix))
|
||||
else:
|
||||
root = self.fs_root
|
||||
if use_dvcignore:
|
||||
dirs[:], files[:] = self.dvcignore(
|
||||
root, dirs, files, ignore_subrepos=ignore_subrepos,
|
||||
)
|
||||
root = self._root
|
||||
yield root, dirs, files
|
||||
|
||||
def isexec(self, path_info):
|
||||
@@ -158,11 +115,8 @@ class GitFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), path
|
||||
)
|
||||
|
||||
def walk_files(self, top): # pylint: disable=arguments-differ
|
||||
for root, _, files in self.walk(top):
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for root, _, files in self.walk(path_info, **kwargs):
|
||||
for file in files:
|
||||
# NOTE: os.path.join is ~5.5 times slower
|
||||
yield f"{root}{os.sep}{file}"
|
||||
|
||||
def _reset(self):
|
||||
return self.__dict__.pop("dvcignore", None)
|
||||
|
||||
+4
-9
@@ -6,23 +6,18 @@ from funcy import cached_property, wrap_prop
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .fsspec_wrapper import FSSpecWrapper
|
||||
from .fsspec_wrapper import ObjectFSWrapper
|
||||
|
||||
|
||||
class GSFileSystem(FSSpecWrapper):
|
||||
# pylint:disable=abstract-method
|
||||
class GSFileSystem(ObjectFSWrapper):
|
||||
scheme = Schemes.GS
|
||||
PATH_CLS = CloudURLInfo
|
||||
REQUIRES = {"gcsfs": "gcsfs"}
|
||||
PARAM_CHECKSUM = "etag"
|
||||
DETAIL_FIELDS = frozenset(("etag", "size"))
|
||||
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
|
||||
url = config.get("url", "gs://")
|
||||
self.path_info = self.PATH_CLS(url)
|
||||
|
||||
def _prepare_credentials(self, config):
|
||||
def _prepare_credentials(self, **config):
|
||||
login_info = {"consistency": None}
|
||||
login_info["project"] = config.get("projectname")
|
||||
login_info["token"] = config.get("credentialpath")
|
||||
|
||||
+19
-25
@@ -6,7 +6,6 @@ import shutil
|
||||
import subprocess
|
||||
from collections import deque
|
||||
from contextlib import closing, contextmanager
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.progress import Tqdm
|
||||
@@ -65,27 +64,22 @@ class HDFSFileSystem(BaseFileSystem):
|
||||
PARAM_CHECKSUM = "checksum"
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
self.path_info = None
|
||||
url = config.get("url")
|
||||
if not url:
|
||||
return
|
||||
|
||||
parsed = urlparse(url)
|
||||
user = parsed.username or config.get("user")
|
||||
|
||||
self.path_info = self.PATH_CLS.from_parts(
|
||||
scheme=self.scheme,
|
||||
host=parsed.hostname,
|
||||
user=user,
|
||||
port=parsed.port,
|
||||
path=parsed.path,
|
||||
)
|
||||
self.host = config["host"]
|
||||
self.user = config.get("user")
|
||||
self.port = config.get("port")
|
||||
|
||||
self.kerb_ticket = config.get("kerb_ticket")
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
from fsspec.implementations.hdfs import PyArrowHDFS
|
||||
|
||||
# pylint:disable=protected-access
|
||||
return PyArrowHDFS._get_kwargs_from_urls(urlpath)
|
||||
|
||||
def hdfs(self, path_info):
|
||||
import pyarrow.fs
|
||||
|
||||
@@ -123,7 +117,7 @@ class HDFSFileSystem(BaseFileSystem):
|
||||
raise FileNotFoundError(*e.args)
|
||||
raise
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
def exists(self, path_info) -> bool:
|
||||
assert not isinstance(path_info, list)
|
||||
assert path_info.scheme == "hdfs"
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
@@ -159,15 +153,15 @@ class HDFSFileSystem(BaseFileSystem):
|
||||
if not topdown:
|
||||
yield root, dirs, nondirs
|
||||
|
||||
def walk(self, path_info, **kwargs):
|
||||
if not self.isdir(path_info):
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
if not self.isdir(top):
|
||||
return
|
||||
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
with self.hdfs(top) as hdfs:
|
||||
for root, dnames, fnames in self._walk(
|
||||
hdfs, path_info.path, **kwargs
|
||||
hdfs, top.path, topdown=topdown
|
||||
):
|
||||
yield path_info.replace(path=root), dnames, fnames
|
||||
yield top.replace(path=root), dnames, fnames
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for root, _, fnames in self.walk(path_info):
|
||||
@@ -236,7 +230,7 @@ class HDFSFileSystem(BaseFileSystem):
|
||||
size=self.getsize(path_info),
|
||||
)
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
def _upload_fobj(self, fobj, to_info, **kwargs):
|
||||
with self.hdfs(to_info) as hdfs:
|
||||
with hdfs.open_output_stream(to_info.path) as fdest:
|
||||
shutil.copyfileobj(fobj, fdest)
|
||||
|
||||
+11
-16
@@ -2,10 +2,11 @@ import logging
|
||||
import os.path
|
||||
import threading
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import cached_property, memoize, wrap_prop, wrap_with
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc import prompt
|
||||
from dvc.exceptions import DvcException, HTTPError
|
||||
from dvc.path_info import HTTPURLInfo
|
||||
from dvc.progress import Tqdm
|
||||
@@ -36,16 +37,10 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
REQUEST_TIMEOUT = 60
|
||||
CHUNK_SIZE = 2 ** 16
|
||||
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
url = config.get("url")
|
||||
if url:
|
||||
self.path_info = self.PATH_CLS(url)
|
||||
self.user = config.get("user", None)
|
||||
self.host = self.path_info.host
|
||||
else:
|
||||
self.path_info = None
|
||||
self.user = config.get("user", None)
|
||||
|
||||
self.auth = config.get("auth", None)
|
||||
self.custom_auth_header = config.get("custom_auth_header", None)
|
||||
@@ -55,12 +50,12 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
self.ssl_verify = config.get("ssl_verify", True)
|
||||
self.method = config.get("method", "POST")
|
||||
|
||||
def _auth_method(self):
|
||||
def _auth_method(self, url):
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
|
||||
if self.auth:
|
||||
if self.ask_password and self.password is None:
|
||||
self.password = ask_password(self.host, self.user)
|
||||
self.password = ask_password(urlparse(url).hostname, self.user)
|
||||
if self.auth == "basic":
|
||||
return HTTPBasicAuth(self.user, self.password)
|
||||
if self.auth == "digest":
|
||||
@@ -103,7 +98,7 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
res = self._session.request(
|
||||
method,
|
||||
url,
|
||||
auth=self._auth_method(),
|
||||
auth=self._auth_method(url),
|
||||
headers=self.headers,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -138,7 +133,7 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
return response
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
def exists(self, path_info) -> bool:
|
||||
res = self._head(path_info.url)
|
||||
if res.status_code == 404:
|
||||
return False
|
||||
@@ -152,7 +147,7 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
size = self._content_length(resp)
|
||||
return {"etag": etag, "size": size}
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
def _upload_fobj(self, fobj, to_info, **kwargs):
|
||||
def chunks(fobj):
|
||||
while True:
|
||||
chunk = fobj.read(self.CHUNK_SIZE)
|
||||
@@ -189,9 +184,9 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
self.upload_fobj(
|
||||
fobj,
|
||||
to_info,
|
||||
size=None if no_progress_bar else os.path.getsize(from_file),
|
||||
no_progress_bar=no_progress_bar,
|
||||
desc=name or to_info.url,
|
||||
total=None if no_progress_bar else os.path.getsize(from_file),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
+13
-67
@@ -2,8 +2,6 @@ import logging
|
||||
import os
|
||||
import stat
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.system import System
|
||||
@@ -22,70 +20,32 @@ class LocalFileSystem(BaseFileSystem):
|
||||
PARAM_PATH = "path"
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
def __init__(self, repo, config, use_dvcignore=False, dvcignore_root=None):
|
||||
super().__init__(repo, config)
|
||||
url = config.get("url")
|
||||
self.path_info = self.PATH_CLS(url) if url else None
|
||||
self.use_dvcignore = use_dvcignore
|
||||
self.dvcignore_root = dvcignore_root
|
||||
def __init__(self, **config):
|
||||
from fsspec.implementations.local import LocalFileSystem as LocalFS
|
||||
|
||||
@property
|
||||
def fs_root(self):
|
||||
return self.config.get("url")
|
||||
|
||||
@cached_property
|
||||
def dvcignore(self):
|
||||
from dvc.ignore import DvcIgnoreFilter, DvcIgnoreFilterNoop
|
||||
|
||||
root = self.dvcignore_root or self.fs_root
|
||||
cls = DvcIgnoreFilter if self.use_dvcignore else DvcIgnoreFilterNoop
|
||||
return cls(self, root)
|
||||
super().__init__(**config)
|
||||
self.fs = LocalFS()
|
||||
|
||||
@staticmethod
|
||||
def open(path_info, mode="r", encoding=None, **kwargs):
|
||||
return open(path_info, mode=mode, encoding=encoding)
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
def exists(self, path_info) -> bool:
|
||||
assert isinstance(path_info, str) or path_info.scheme == "local"
|
||||
if self.repo:
|
||||
ret = os.path.lexists(path_info)
|
||||
else:
|
||||
ret = os.path.exists(path_info)
|
||||
if not ret:
|
||||
return False
|
||||
if not use_dvcignore:
|
||||
return True
|
||||
return self.fs.exists(path_info)
|
||||
|
||||
return not self.dvcignore.is_ignored_file(
|
||||
path_info
|
||||
) and not self.dvcignore.is_ignored_dir(path_info)
|
||||
def isfile(self, path_info) -> bool:
|
||||
return os.path.isfile(path_info)
|
||||
|
||||
def isfile(self, path_info):
|
||||
if not os.path.isfile(path_info):
|
||||
return False
|
||||
|
||||
return not self.dvcignore.is_ignored_file(path_info)
|
||||
|
||||
def isdir(
|
||||
self, path_info, use_dvcignore=True
|
||||
): # pylint: disable=arguments-differ
|
||||
if not os.path.isdir(path_info):
|
||||
return False
|
||||
return not (use_dvcignore and self.dvcignore.is_ignored_dir(path_info))
|
||||
def isdir(self, path_info) -> bool:
|
||||
return os.path.isdir(path_info)
|
||||
|
||||
def iscopy(self, path_info):
|
||||
return not (
|
||||
System.is_symlink(path_info) or System.is_hardlink(path_info)
|
||||
)
|
||||
|
||||
def walk(
|
||||
self,
|
||||
top,
|
||||
topdown=True,
|
||||
onerror=None,
|
||||
use_dvcignore=True,
|
||||
ignore_subrepos=True,
|
||||
):
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
"""Directory fs generator.
|
||||
|
||||
See `os.walk` for the docs. Differences:
|
||||
@@ -94,14 +54,6 @@ class LocalFileSystem(BaseFileSystem):
|
||||
for root, dirs, files in os.walk(
|
||||
top, topdown=topdown, onerror=onerror
|
||||
):
|
||||
if use_dvcignore:
|
||||
dirs[:], files[:] = self.dvcignore(
|
||||
os.path.abspath(root),
|
||||
dirs,
|
||||
files,
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
)
|
||||
|
||||
yield os.path.normpath(root), dirs, files
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
@@ -133,9 +85,6 @@ class LocalFileSystem(BaseFileSystem):
|
||||
return is_exec(mode)
|
||||
|
||||
def stat(self, path):
|
||||
if self.dvcignore.is_ignored(path):
|
||||
raise FileNotFoundError
|
||||
|
||||
return os.stat(path)
|
||||
|
||||
def move(self, from_info, to_info):
|
||||
@@ -154,7 +103,7 @@ class LocalFileSystem(BaseFileSystem):
|
||||
self.remove(tmp_info)
|
||||
raise
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
def _upload_fobj(self, fobj, to_info, **kwargs):
|
||||
self.makedirs(to_info.parent)
|
||||
tmp_info = to_info.parent / tmp_fname("")
|
||||
try:
|
||||
@@ -210,7 +159,7 @@ class LocalFileSystem(BaseFileSystem):
|
||||
}
|
||||
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs,
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
|
||||
):
|
||||
makedirs(to_info.parent, exist_ok=True)
|
||||
|
||||
@@ -227,6 +176,3 @@ class LocalFileSystem(BaseFileSystem):
|
||||
copyfile(
|
||||
from_info, to_file, no_progress_bar=no_progress_bar, name=name
|
||||
)
|
||||
|
||||
def _reset(self):
|
||||
return self.__dict__.pop("dvcignore", None)
|
||||
|
||||
+3
-3
@@ -5,14 +5,14 @@ class MemoryFileSystem(BaseFileSystem):
|
||||
scheme = "local"
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
def __init__(self, repo, config):
|
||||
def __init__(self, **kwargs):
|
||||
from fsspec.implementations.memory import MemoryFileSystem as MemFS
|
||||
|
||||
super().__init__(repo, config)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.fs = MemFS()
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
def exists(self, path_info) -> bool:
|
||||
return self.fs.exists(path_info.fspath)
|
||||
|
||||
def open(self, path_info, mode="r", encoding=None, **kwargs):
|
||||
|
||||
+19
-29
@@ -39,13 +39,9 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
COPY_POLL_SECONDS = 5
|
||||
LIST_OBJECT_PAGE_SIZE = 100
|
||||
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
url = config.get("url")
|
||||
self.path_info = self.PATH_CLS(url) if url else None
|
||||
|
||||
self.bucket = self.path_info.bucket
|
||||
self.endpoint = config.get("oss_endpoint") or os.getenv("OSS_ENDPOINT")
|
||||
|
||||
self.key_id = (
|
||||
@@ -68,27 +64,19 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
logger.debug(f"key id: {self.key_id}")
|
||||
logger.debug(f"key secret: {self.key_secret}")
|
||||
|
||||
auth = oss2.Auth(self.key_id, self.key_secret)
|
||||
bucket = oss2.Bucket(auth, self.endpoint, self.bucket)
|
||||
return oss2.Auth(self.key_id, self.key_secret)
|
||||
|
||||
# Ensure bucket exists
|
||||
try:
|
||||
bucket.get_bucket_info()
|
||||
except oss2.exceptions.NoSuchBucket:
|
||||
bucket.create_bucket(
|
||||
oss2.BUCKET_ACL_PUBLIC_READ,
|
||||
oss2.models.BucketCreateConfig(
|
||||
oss2.BUCKET_STORAGE_CLASS_STANDARD
|
||||
),
|
||||
)
|
||||
return bucket
|
||||
def _get_bucket(self, bucket):
|
||||
import oss2
|
||||
|
||||
return oss2.Bucket(self.oss_service, self.endpoint, bucket)
|
||||
|
||||
def _generate_download_url(self, path_info, expires=3600):
|
||||
assert path_info.bucket == self.bucket
|
||||
return self._get_bucket(path_info.bucket).sign_url(
|
||||
"GET", path_info.path, expires
|
||||
)
|
||||
|
||||
return self.oss_service.sign_url("GET", path_info.path, expires)
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
def exists(self, path_info) -> bool:
|
||||
paths = self._list_paths(path_info)
|
||||
return any(path_info.path == path for path in paths)
|
||||
|
||||
@@ -96,7 +84,7 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
import oss2
|
||||
|
||||
for blob in oss2.ObjectIterator(
|
||||
self.oss_service, prefix=path_info.path
|
||||
self._get_bucket(path_info.bucket), prefix=path_info.path
|
||||
):
|
||||
yield blob.key
|
||||
|
||||
@@ -114,16 +102,17 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
raise NotImplementedError
|
||||
|
||||
logger.debug(f"Removing oss://{path_info}")
|
||||
self.oss_service.delete_object(path_info.path)
|
||||
self._get_bucket(path_info.bucket).delete_object(path_info.path)
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
self.oss_service.put_object(to_info.path, fobj)
|
||||
def _upload_fobj(self, fobj, to_info, **kwargs):
|
||||
self._get_bucket(to_info.bucket).put_object(to_info.path, fobj)
|
||||
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
|
||||
):
|
||||
with Tqdm(desc=name, disable=no_progress_bar, bytes=True) as pbar:
|
||||
self.oss_service.put_object_from_file(
|
||||
bucket = self._get_bucket(to_info.bucket)
|
||||
bucket.put_object_from_file(
|
||||
to_info.path, from_file, progress_callback=pbar.update_to
|
||||
)
|
||||
|
||||
@@ -133,8 +122,9 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
with Tqdm(desc=name, disable=no_progress_bar, bytes=True) as pbar:
|
||||
import oss2
|
||||
|
||||
bucket = self._get_bucket(from_info.bucket)
|
||||
oss2.resumable_download(
|
||||
self.oss_service,
|
||||
bucket,
|
||||
from_info.path,
|
||||
to_file,
|
||||
progress_callback=pbar.update_to,
|
||||
|
||||
+47
-47
@@ -35,9 +35,9 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
def __init__(
|
||||
self, repo, subrepos=False, repo_factory: RepoFactory = None,
|
||||
self, repo=None, subrepos=False, repo_factory: RepoFactory = None
|
||||
):
|
||||
super().__init__(repo, {"url": repo.root_dir})
|
||||
super().__init__()
|
||||
|
||||
from dvc.utils.collections import PathStringTrie
|
||||
|
||||
@@ -49,6 +49,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
self.repo_factory = repo_factory
|
||||
|
||||
self._main_repo = repo
|
||||
self.hash_jobs = repo.fs.hash_jobs
|
||||
self.root_dir = repo.root_dir
|
||||
self._traverse_subrepos = subrepos
|
||||
|
||||
@@ -61,9 +62,15 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
"""Keep a dvcfs instance of each repo."""
|
||||
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self._dvcfss[repo.root_dir] = DvcFileSystem(repo)
|
||||
self._dvcfss[repo.root_dir] = DvcFileSystem(repo=repo)
|
||||
|
||||
def _get_repo(self, path) -> Optional["Repo"]:
|
||||
@property
|
||||
def repo_url(self):
|
||||
if self._main_repo is None:
|
||||
return None
|
||||
return self._main_repo.url
|
||||
|
||||
def _get_repo(self, path: str) -> Optional["Repo"]:
|
||||
"""Returns repo that the path falls in, using prefix.
|
||||
|
||||
If the path is already tracked/collected, it just returns the repo.
|
||||
@@ -93,11 +100,11 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
if self._is_dvc_repo(d):
|
||||
repo = self.repo_factory(
|
||||
d,
|
||||
scm=self.repo.scm,
|
||||
rev=self.repo.get_rev(),
|
||||
scm=self._main_repo.scm,
|
||||
rev=self._main_repo.get_rev(),
|
||||
repo_factory=self.repo_factory,
|
||||
)
|
||||
self._dvcfss[repo.root_dir] = DvcFileSystem(repo)
|
||||
self._dvcfss[repo.root_dir] = DvcFileSystem(repo=repo)
|
||||
self._subrepos_trie[d] = repo
|
||||
|
||||
def _is_dvc_repo(self, dir_path):
|
||||
@@ -108,8 +115,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
from dvc.repo import Repo
|
||||
|
||||
repo_path = os.path.join(dir_path, Repo.DVC_DIR)
|
||||
# dvcignore will ignore subrepos, therefore using `use_dvcignore=False`
|
||||
return self._main_repo.fs.isdir(repo_path, use_dvcignore=False)
|
||||
return self._main_repo.fs.isdir(repo_path)
|
||||
|
||||
def _get_fs_pair(
|
||||
self, path
|
||||
@@ -142,19 +148,20 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
return dvc_fs.open(path_info, mode=mode, encoding=encoding, **kwargs)
|
||||
|
||||
def exists(
|
||||
self, path, use_dvcignore=True
|
||||
): # pylint: disable=arguments-differ
|
||||
fs, dvc_fs = self._get_fs_pair(path)
|
||||
def exists(self, path_info) -> bool:
|
||||
fs, dvc_fs = self._get_fs_pair(path_info)
|
||||
|
||||
if not dvc_fs:
|
||||
return fs.exists(path)
|
||||
return fs.exists(path_info)
|
||||
|
||||
if fs.exists(path):
|
||||
if dvc_fs.repo.dvcignore.is_ignored(fs, path_info):
|
||||
return False
|
||||
|
||||
if fs.exists(path_info):
|
||||
return True
|
||||
|
||||
try:
|
||||
meta = dvc_fs.metadata(path)
|
||||
meta = dvc_fs.metadata(path_info)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
@@ -167,6 +174,9 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
def isdir(self, path): # pylint: disable=arguments-differ
|
||||
fs, dvc_fs = self._get_fs_pair(path)
|
||||
|
||||
if dvc_fs and dvc_fs.repo.dvcignore.is_ignored_dir(path):
|
||||
return False
|
||||
|
||||
try:
|
||||
st = fs.stat(path)
|
||||
return stat.S_ISDIR(st.st_mode)
|
||||
@@ -195,6 +205,9 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
def isfile(self, path): # pylint: disable=arguments-differ
|
||||
fs, dvc_fs = self._get_fs_pair(path)
|
||||
|
||||
if dvc_fs and dvc_fs.repo.dvcignore.is_ignored_file(path):
|
||||
return False
|
||||
|
||||
try:
|
||||
st = fs.stat(path)
|
||||
return stat.S_ISREG(st.st_mode)
|
||||
@@ -238,13 +251,11 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
def _subrepo_walk(self, dir_path, **kwargs):
|
||||
"""Walk into a new repo.
|
||||
|
||||
NOTE: subrepo will only be discovered when walking if
|
||||
ignore_subrepos is set to False.
|
||||
NOTE: subrepo will only be discovered when walking if
|
||||
ignore_subrepos is set to False.
|
||||
"""
|
||||
fs, dvc_fs = self._get_fs_pair(dir_path)
|
||||
fs_walk = fs.walk(
|
||||
dir_path, topdown=True, ignore_subrepos=not self._traverse_subrepos
|
||||
)
|
||||
fs_walk = fs.walk(dir_path, topdown=True)
|
||||
if dvc_fs:
|
||||
dvc_walk = dvc_fs.walk(dir_path, topdown=True, **kwargs)
|
||||
else:
|
||||
@@ -306,15 +317,7 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
elif dirname in repo_set:
|
||||
yield from self._walk(repo_walk, None, dvcfiles=dvcfiles)
|
||||
|
||||
def walk(
|
||||
self,
|
||||
top,
|
||||
topdown=True,
|
||||
onerror=None,
|
||||
dvcfiles=False,
|
||||
follow_subrepos=None,
|
||||
**kwargs
|
||||
): # pylint: disable=arguments-differ
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
"""Walk and merge both DVC and repo fss.
|
||||
|
||||
Args:
|
||||
@@ -340,17 +343,14 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
onerror(NotADirectoryError(top))
|
||||
return
|
||||
|
||||
ignore_subrepos = not self._traverse_subrepos
|
||||
if follow_subrepos is not None:
|
||||
ignore_subrepos = not follow_subrepos
|
||||
repo = self._get_repo(os.path.abspath(top))
|
||||
dvcfiles = kwargs.pop("dvcfiles", False)
|
||||
|
||||
fs, dvc_fs = self._get_fs_pair(top)
|
||||
repo_exists = fs.exists(top)
|
||||
repo_walk = fs.walk(
|
||||
top,
|
||||
topdown=topdown,
|
||||
onerror=onerror,
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
|
||||
repo_walk = repo.dvcignore.walk(
|
||||
fs, top, topdown=topdown, onerror=onerror, **kwargs
|
||||
)
|
||||
|
||||
if not dvc_fs or (repo_exists and dvc_fs.isdvc(top)):
|
||||
@@ -358,16 +358,20 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
return
|
||||
|
||||
if not repo_exists:
|
||||
yield from dvc_fs.walk(top, topdown=topdown, **kwargs)
|
||||
yield from dvc_fs.walk(
|
||||
top, topdown=topdown, onerror=onerror, **kwargs
|
||||
)
|
||||
|
||||
dvc_walk = None
|
||||
if dvc_fs.exists(top):
|
||||
dvc_walk = dvc_fs.walk(top, topdown=topdown, **kwargs)
|
||||
dvc_walk = dvc_fs.walk(
|
||||
top, topdown=topdown, onerror=onerror, **kwargs
|
||||
)
|
||||
|
||||
yield from self._walk(repo_walk, dvc_walk, dvcfiles=dvcfiles)
|
||||
|
||||
def walk_files(self, top, **kwargs): # pylint: disable=arguments-differ
|
||||
for root, _, files in self.walk(top, **kwargs):
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for root, _, files in self.walk(path_info, **kwargs):
|
||||
for fname in files:
|
||||
yield PathInfo(root) / fname
|
||||
|
||||
@@ -380,15 +384,11 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
with open(to_file, "wb+") as to_fobj:
|
||||
with Tqdm.wrapattr(
|
||||
to_fobj, "write", desc=name, disable=no_progress_bar,
|
||||
to_fobj, "write", desc=name, disable=no_progress_bar
|
||||
) as wrapped:
|
||||
with self.open(from_info, "rb", **kwargs) as from_fobj:
|
||||
shutil.copyfileobj(from_fobj, wrapped)
|
||||
|
||||
@property
|
||||
def hash_jobs(self): # pylint: disable=invalid-overridden-method
|
||||
return self._main_repo.fs.hash_jobs
|
||||
|
||||
def metadata(self, path):
|
||||
abspath = os.path.abspath(path)
|
||||
path_info = PathInfo(abspath)
|
||||
|
||||
+157
-387
@@ -1,75 +1,34 @@
|
||||
import logging
|
||||
import functools
|
||||
import os
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from collections import defaultdict
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.config import ConfigError
|
||||
from dvc.exceptions import DvcException, ETagMismatchError
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.utils import conversions, error_link
|
||||
|
||||
from .base import BaseFileSystem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from .fsspec_wrapper import ObjectFSWrapper
|
||||
|
||||
_AWS_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".aws", "config")
|
||||
|
||||
|
||||
class S3FileSystem(BaseFileSystem):
|
||||
# pylint:disable=abstract-method
|
||||
class BaseS3FileSystem(ObjectFSWrapper):
|
||||
scheme = Schemes.S3
|
||||
PATH_CLS = CloudURLInfo
|
||||
REQUIRES = {"boto3": "boto3"}
|
||||
REQUIRES = {"s3fs": "s3fs", "boto3": "boto3"}
|
||||
PARAM_CHECKSUM = "etag"
|
||||
DETAIL_FIELDS = frozenset(("etag", "size"))
|
||||
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
_GRANTS = {
|
||||
"grant_full_control": "GrantFullControl",
|
||||
"grant_read": "GrantRead",
|
||||
"grant_read_acp": "GrantReadACP",
|
||||
"grant_write_acp": "GrantWriteACP",
|
||||
}
|
||||
|
||||
url = config.get("url", "s3://")
|
||||
self.path_info = self.PATH_CLS(url)
|
||||
|
||||
self.region = config.get("region")
|
||||
self.profile = config.get("profile")
|
||||
self.endpoint_url = config.get("endpointurl")
|
||||
|
||||
self.use_ssl = config.get("use_ssl", True)
|
||||
self.ssl_verify = config.get("ssl_verify", True)
|
||||
|
||||
self.extra_args = {}
|
||||
|
||||
self.sse = config.get("sse")
|
||||
if self.sse:
|
||||
self.extra_args["ServerSideEncryption"] = self.sse
|
||||
|
||||
self.sse_kms_key_id = config.get("sse_kms_key_id")
|
||||
if self.sse_kms_key_id:
|
||||
self.extra_args["SSEKMSKeyId"] = self.sse_kms_key_id
|
||||
|
||||
self.acl = config.get("acl")
|
||||
if self.acl:
|
||||
self.extra_args["ACL"] = self.acl
|
||||
|
||||
self._append_aws_grants_to_extra_args(config)
|
||||
|
||||
self.access_key_id = config.get("access_key_id")
|
||||
self.secret_access_key = config.get("secret_access_key")
|
||||
self.session_token = config.get("session_token")
|
||||
|
||||
shared_creds = config.get("credentialpath")
|
||||
if shared_creds:
|
||||
os.environ.setdefault("AWS_SHARED_CREDENTIALS_FILE", shared_creds)
|
||||
|
||||
config_path = config.get("configpath")
|
||||
if config_path:
|
||||
os.environ.setdefault("AWS_CONFIG_FILE", config_path)
|
||||
self._transfer_config = None
|
||||
|
||||
# https://github.com/aws/aws-cli/blob/0376c6262d6b15dc36c82e6da6e1aad10249cc8c/awscli/customizations/s3/transferconfig.py#L107-L113
|
||||
_TRANSFER_CONFIG_ALIASES = {
|
||||
"max_queue_size": "max_io_queue",
|
||||
"max_concurrent_requests": "max_concurrency",
|
||||
@@ -77,11 +36,15 @@ class S3FileSystem(BaseFileSystem):
|
||||
"multipart_chunksize": "multipart_chunksize",
|
||||
}
|
||||
|
||||
def _transform_config(self, s3_config):
|
||||
def _split_s3_config(self, s3_config):
|
||||
"""Splits the general s3 config into 2 different config
|
||||
objects, one for transfer.TransferConfig and other is the
|
||||
general session config"""
|
||||
|
||||
from boto3.s3.transfer import TransferConfig
|
||||
|
||||
from dvc.utils import conversions
|
||||
|
||||
config, transfer_config = {}, {}
|
||||
for key, value in s3_config.items():
|
||||
if key in self._TRANSFER_CONFIG_ALIASES:
|
||||
@@ -94,367 +57,174 @@ class S3FileSystem(BaseFileSystem):
|
||||
else:
|
||||
config[key] = value
|
||||
|
||||
return config, transfer_config
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self._transfer_config = TransferConfig(**transfer_config)
|
||||
return config
|
||||
|
||||
def _process_config(self):
|
||||
from boto3.s3.transfer import TransferConfig
|
||||
def _load_aws_config_file(self, profile):
|
||||
from botocore.configloader import load_config
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self._transfer_config = None
|
||||
config_path = os.environ.get("AWS_CONFIG_FILE", _AWS_CONFIG_PATH)
|
||||
if not os.path.exists(config_path):
|
||||
return None
|
||||
|
||||
config = load_config(config_path)
|
||||
profile = config["profiles"].get(self.profile or "default")
|
||||
if not profile:
|
||||
profile_config = config["profiles"].get(profile or "default")
|
||||
if not profile_config:
|
||||
return None
|
||||
|
||||
s3_config = profile.get("s3", {})
|
||||
s3_config, transfer_config = self._transform_config(s3_config)
|
||||
self._transfer_config = TransferConfig(**transfer_config)
|
||||
return s3_config
|
||||
s3_config = profile_config.get("s3", {})
|
||||
return self._split_s3_config(s3_config)
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
from dvc.config import ConfigError
|
||||
from dvc.utils.flatten import flatten, unflatten
|
||||
|
||||
login_info = defaultdict(dict)
|
||||
|
||||
# credentials
|
||||
login_info["key"] = config.get("access_key_id")
|
||||
login_info["secret"] = config.get("secret_access_key")
|
||||
login_info["token"] = config.get("session_token")
|
||||
|
||||
# session configuration
|
||||
login_info["profile"] = config.get("profile")
|
||||
login_info["use_ssl"] = config.get("use_ssl", True)
|
||||
|
||||
# extra client configuration
|
||||
client = login_info["client_kwargs"]
|
||||
client["region_name"] = config.get("region")
|
||||
client["endpoint_url"] = config.get("endpointurl")
|
||||
client["verify"] = config.get("ssl_verify")
|
||||
|
||||
# encryptions
|
||||
additional = login_info["s3_additional_kwargs"]
|
||||
additional["ServerSideEncryption"] = config.get("sse")
|
||||
additional["SSEKMSKeyId"] = config.get("sse_kms_key_id")
|
||||
additional["ACL"] = config.get("acl")
|
||||
for grant_option, grant_key in self._GRANTS.items():
|
||||
if config.get(grant_option):
|
||||
if additional["ACL"]:
|
||||
raise ConfigError(
|
||||
"`acl` and `grant_*` AWS S3 config options "
|
||||
"are mutually exclusive"
|
||||
)
|
||||
additional[grant_key] = config[grant_option]
|
||||
|
||||
# config kwargs
|
||||
session_config = login_info["config_kwargs"]
|
||||
session_config["s3"] = self._load_aws_config_file(
|
||||
login_info["profile"]
|
||||
)
|
||||
|
||||
shared_creds = config.get("credentialpath")
|
||||
if shared_creds:
|
||||
os.environ.setdefault("AWS_SHARED_CREDENTIALS_FILE", shared_creds)
|
||||
|
||||
config_path = config.get("configpath")
|
||||
if config_path:
|
||||
os.environ.setdefault("AWS_CONFIG_FILE", config_path)
|
||||
|
||||
return unflatten(
|
||||
{
|
||||
key: value
|
||||
for key, value in flatten(login_info).items()
|
||||
if value is not None
|
||||
}
|
||||
)
|
||||
|
||||
def _entry_hook(self, entry):
|
||||
entry = entry.copy()
|
||||
if "ETag" in entry:
|
||||
entry["etag"] = entry["ETag"].strip('"')
|
||||
return entry
|
||||
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self):
|
||||
from s3fs import S3FileSystem as _S3FileSystem
|
||||
|
||||
return _S3FileSystem(**self.fs_args)
|
||||
|
||||
|
||||
def _translate_exceptions(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
except Exception as exc:
|
||||
from s3fs.errors import translate_boto_error
|
||||
|
||||
raise translate_boto_error(exc)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class S3FileSystem(BaseS3FileSystem): # pylint:disable=abstract-method
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def s3(self):
|
||||
import boto3
|
||||
|
||||
login_info = self.fs_args
|
||||
client_kwargs = login_info.get("client_kwargs", {})
|
||||
session_opts = {
|
||||
"profile_name": self.profile,
|
||||
"region_name": self.region,
|
||||
"profile_name": login_info.get("profile"),
|
||||
"region_name": client_kwargs.get("region_name"),
|
||||
}
|
||||
|
||||
if self.access_key_id:
|
||||
session_opts["aws_access_key_id"] = self.access_key_id
|
||||
if self.secret_access_key:
|
||||
session_opts["aws_secret_access_key"] = self.secret_access_key
|
||||
if self.session_token:
|
||||
session_opts["aws_session_token"] = self.session_token
|
||||
if "key" in login_info:
|
||||
session_opts["aws_access_key_id"] = login_info["key"]
|
||||
if "secret" in login_info:
|
||||
session_opts["aws_secret_access_key"] = login_info["secret"]
|
||||
if "token" in login_info:
|
||||
session_opts["aws_session_token"] = login_info["token"]
|
||||
|
||||
session = boto3.session.Session(**session_opts)
|
||||
s3_config = self._process_config()
|
||||
|
||||
return session.resource(
|
||||
"s3",
|
||||
endpoint_url=self.endpoint_url,
|
||||
verify=self.ssl_verify,
|
||||
use_ssl=self.use_ssl,
|
||||
config=boto3.session.Config(
|
||||
signature_version="s3v4", s3=s3_config
|
||||
),
|
||||
endpoint_url=client_kwargs.get("endpoint_url"),
|
||||
use_ssl=login_info["use_ssl"],
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def _get_s3(self):
|
||||
from botocore.exceptions import (
|
||||
EndpointConnectionError,
|
||||
NoCredentialsError,
|
||||
)
|
||||
|
||||
try:
|
||||
yield self.s3
|
||||
except NoCredentialsError as exc:
|
||||
link = error_link("no-credentials")
|
||||
raise DvcException(
|
||||
f"Unable to find AWS credentials. {link}"
|
||||
) from exc
|
||||
except EndpointConnectionError as exc:
|
||||
link = error_link("connection-error")
|
||||
name = self.endpoint_url or "AWS S3"
|
||||
raise DvcException(
|
||||
f"Unable to connect to '{name}'. {link}"
|
||||
) from exc
|
||||
|
||||
@contextmanager
|
||||
def _get_bucket(self, bucket):
|
||||
with self._get_s3() as s3:
|
||||
try:
|
||||
yield s3.Bucket(bucket)
|
||||
except s3.meta.client.exceptions.NoSuchBucket as exc:
|
||||
link = error_link("no-bucket")
|
||||
raise DvcException(
|
||||
f"Bucket '{bucket}' does not exist. {link}"
|
||||
) from exc
|
||||
|
||||
@contextmanager
|
||||
def _get_obj(self, path_info):
|
||||
with self._get_bucket(path_info.bucket) as bucket:
|
||||
try:
|
||||
yield bucket.Object(path_info.path)
|
||||
except bucket.meta.client.exceptions.NoSuchKey as exc:
|
||||
raise DvcException(f"{path_info.url} does not exist") from exc
|
||||
|
||||
def _append_aws_grants_to_extra_args(self, config):
|
||||
# Keys for extra_args can be one of the following list:
|
||||
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/customizations/s3.html#boto3.s3.transfer.S3Transfer.ALLOWED_UPLOAD_ARGS
|
||||
"""
|
||||
ALLOWED_UPLOAD_ARGS = [
|
||||
'ACL', 'CacheControl', 'ContentDisposition', 'ContentEncoding',
|
||||
'ContentLanguage', 'ContentType', 'Expires', 'GrantFullControl',
|
||||
'GrantRead', 'GrantReadACP', 'GrantWriteACP', 'Metadata',
|
||||
'RequestPayer', 'ServerSideEncryption', 'StorageClass',
|
||||
'SSECustomerAlgorithm', 'SSECustomerKey', 'SSECustomerKeyMD5',
|
||||
'SSEKMSKeyId', 'WebsiteRedirectLocation'
|
||||
]
|
||||
"""
|
||||
|
||||
grants = {
|
||||
"grant_full_control": "GrantFullControl",
|
||||
"grant_read": "GrantRead",
|
||||
"grant_read_acp": "GrantReadACP",
|
||||
"grant_write_acp": "GrantWriteACP",
|
||||
}
|
||||
|
||||
for grant_option, extra_args_key in grants.items():
|
||||
if config.get(grant_option):
|
||||
if self.acl:
|
||||
raise ConfigError(
|
||||
"`acl` and `grant_*` AWS S3 config options "
|
||||
"are mutually exclusive"
|
||||
)
|
||||
|
||||
self.extra_args[extra_args_key] = config.get(grant_option)
|
||||
|
||||
def _generate_download_url(self, path_info, expires=3600):
|
||||
params = {"Bucket": path_info.bucket, "Key": path_info.path}
|
||||
with self._get_s3() as s3:
|
||||
return s3.meta.client.generate_presigned_url(
|
||||
ClientMethod="get_object",
|
||||
Params=params,
|
||||
ExpiresIn=int(expires),
|
||||
)
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
"""Check if the blob exists. If it does not exist,
|
||||
it could be a part of a directory path.
|
||||
|
||||
eg: if `data/file.txt` exists, check for `data` should return True
|
||||
"""
|
||||
return self.isfile(path_info) or self.isdir(path_info)
|
||||
|
||||
def isdir(self, path_info):
|
||||
# S3 doesn't have a concept for directories.
|
||||
#
|
||||
# Using `head_object` with a path pointing to a directory
|
||||
# will throw a 404 error.
|
||||
#
|
||||
# A reliable way to know if a given path is a directory is by
|
||||
# checking if there are more files sharing the same prefix
|
||||
# with a `list_objects` call.
|
||||
#
|
||||
# We need to make sure that the path ends with a forward slash,
|
||||
# since we can end with false-positives like the following example:
|
||||
#
|
||||
# bucket
|
||||
# └── data
|
||||
# ├── alice
|
||||
# └── alpha
|
||||
#
|
||||
# Using `data/al` as prefix will return `[data/alice, data/alpha]`,
|
||||
# While `data/al/` will return nothing.
|
||||
#
|
||||
dir_path = path_info / ""
|
||||
return bool(list(self._list_paths(dir_path, max_items=1)))
|
||||
|
||||
def isfile(self, path_info):
|
||||
if path_info.path.endswith("/"):
|
||||
return False
|
||||
|
||||
return path_info.path in self._list_paths(path_info)
|
||||
|
||||
def info(self, path_info):
|
||||
# temporary workaround, will be removed when migrating to s3fs
|
||||
if self.isdir(path_info):
|
||||
return {"type": "directory"}
|
||||
|
||||
with self._get_obj(path_info) as obj:
|
||||
return {
|
||||
"type": "file",
|
||||
"etag": obj.e_tag.strip('"'),
|
||||
"size": obj.content_length,
|
||||
}
|
||||
|
||||
def _list_paths(self, path_info, max_items=None):
|
||||
with self._get_bucket(path_info.bucket) as bucket:
|
||||
obj_summaries = bucket.objects.filter(Prefix=path_info.path)
|
||||
if max_items is not None:
|
||||
obj_summaries = obj_summaries.page_size(max_items).limit(
|
||||
max_items
|
||||
)
|
||||
for obj_summary in obj_summaries:
|
||||
yield obj_summary.key
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
if not kwargs.pop("prefix", False):
|
||||
path_info = path_info / ""
|
||||
for fname in self._list_paths(path_info, **kwargs):
|
||||
if fname.endswith("/"):
|
||||
continue
|
||||
|
||||
yield path_info.replace(path=fname)
|
||||
|
||||
def find(self, path_info, detail=False):
|
||||
with self._get_bucket(path_info.bucket) as bucket:
|
||||
for obj_summary in bucket.objects.filter(Prefix=path_info.path):
|
||||
if detail:
|
||||
yield {
|
||||
"type": "file",
|
||||
"name": obj_summary.key,
|
||||
"size": obj_summary.size,
|
||||
"etag": obj_summary.e_tag.strip('"'),
|
||||
}
|
||||
else:
|
||||
yield obj_summary.key
|
||||
|
||||
def remove(self, path_info):
|
||||
if path_info.scheme != "s3":
|
||||
raise NotImplementedError
|
||||
|
||||
logger.debug(f"Removing {path_info}")
|
||||
with self._get_obj(path_info) as obj:
|
||||
obj.delete()
|
||||
|
||||
def makedirs(self, path_info):
|
||||
# We need to support creating empty directories, which means
|
||||
# creating an object with an empty body and a trailing slash `/`.
|
||||
#
|
||||
# We are not creating directory objects for every parent prefix,
|
||||
# as it is not required.
|
||||
if not path_info.path:
|
||||
return
|
||||
|
||||
dir_path = path_info / ""
|
||||
with self._get_obj(dir_path) as obj:
|
||||
obj.put(Body="")
|
||||
|
||||
def copy(self, from_info, to_info):
|
||||
with self._get_s3() as s3:
|
||||
self._copy(s3.meta.client, from_info, to_info, self.extra_args)
|
||||
|
||||
@classmethod
|
||||
def _copy_multipart(
|
||||
cls, s3, from_info, to_info, size, n_parts, extra_args
|
||||
):
|
||||
mpu = s3.create_multipart_upload(
|
||||
Bucket=to_info.bucket, Key=to_info.path, **extra_args
|
||||
)
|
||||
mpu_id = mpu["UploadId"]
|
||||
|
||||
parts = []
|
||||
byte_position = 0
|
||||
for i in range(1, n_parts + 1):
|
||||
obj = s3.head_object(
|
||||
Bucket=from_info.bucket, Key=from_info.path, PartNumber=i
|
||||
)
|
||||
part_size = obj["ContentLength"]
|
||||
lastbyte = byte_position + part_size - 1
|
||||
if lastbyte > size:
|
||||
lastbyte = size - 1
|
||||
|
||||
srange = f"bytes={byte_position}-{lastbyte}"
|
||||
|
||||
part = s3.upload_part_copy(
|
||||
Bucket=to_info.bucket,
|
||||
Key=to_info.path,
|
||||
PartNumber=i,
|
||||
UploadId=mpu_id,
|
||||
CopySourceRange=srange,
|
||||
CopySource={"Bucket": from_info.bucket, "Key": from_info.path},
|
||||
)
|
||||
parts.append(
|
||||
{"PartNumber": i, "ETag": part["CopyPartResult"]["ETag"]}
|
||||
)
|
||||
byte_position += part_size
|
||||
|
||||
assert n_parts == len(parts)
|
||||
|
||||
s3.complete_multipart_upload(
|
||||
Bucket=to_info.bucket,
|
||||
Key=to_info.path,
|
||||
UploadId=mpu_id,
|
||||
MultipartUpload={"Parts": parts},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _copy(cls, s3, from_info, to_info, extra_args):
|
||||
# NOTE: object's etag depends on the way it was uploaded to s3 or the
|
||||
# way it was copied within the s3. More specifically, it depends on
|
||||
# the chunk size that was used to transfer it, which would affect
|
||||
# whether an object would be uploaded as a single part or as a
|
||||
# multipart.
|
||||
#
|
||||
# If an object's etag looks like '8978c98bb5a48c2fb5f2c4c905768afa',
|
||||
# then it was transferred as a single part, which means that the chunk
|
||||
# size used to transfer it was greater or equal to the ContentLength
|
||||
# of that object. So to preserve that tag over the next transfer, we
|
||||
# could use any value >= ContentLength.
|
||||
#
|
||||
# If an object's etag looks like '50d67013a5e1a4070bef1fc8eea4d5f9-13',
|
||||
# then it was transferred as a multipart, which means that the chunk
|
||||
# size used to transfer it was less than ContentLength of that object.
|
||||
# Unfortunately, in general, it doesn't mean that the chunk size was
|
||||
# the same throughout the transfer, so it means that in order to
|
||||
# preserve etag, we need to transfer each part separately, so the
|
||||
# object is transfered in the same chunks as it was originally.
|
||||
from boto3.s3.transfer import TransferConfig
|
||||
|
||||
obj = s3.head_object(Bucket=from_info.bucket, Key=from_info.path)
|
||||
etag = obj["ETag"].strip('"')
|
||||
size = obj["ContentLength"]
|
||||
|
||||
_, _, parts_suffix = etag.partition("-")
|
||||
if parts_suffix:
|
||||
n_parts = int(parts_suffix)
|
||||
cls._copy_multipart(
|
||||
s3, from_info, to_info, size, n_parts, extra_args=extra_args
|
||||
)
|
||||
else:
|
||||
source = {"Bucket": from_info.bucket, "Key": from_info.path}
|
||||
s3.copy(
|
||||
source,
|
||||
to_info.bucket,
|
||||
to_info.path,
|
||||
ExtraArgs=extra_args,
|
||||
Config=TransferConfig(multipart_threshold=size + 1),
|
||||
)
|
||||
|
||||
cached_etag = s3.head_object(Bucket=to_info.bucket, Key=to_info.path)[
|
||||
"ETag"
|
||||
].strip('"')
|
||||
if etag != cached_etag:
|
||||
raise ETagMismatchError(etag, cached_etag)
|
||||
|
||||
def etag(self, path_info):
|
||||
with self._get_obj(path_info) as obj:
|
||||
return HashInfo("etag", size=obj.content_length,)
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
with self._get_obj(to_info) as obj:
|
||||
obj.upload_fileobj(fobj, Config=self._transfer_config)
|
||||
bucket = self.s3.Bucket(path_info.bucket)
|
||||
return bucket.Object(path_info.path)
|
||||
|
||||
@_translate_exceptions
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **pbar_args
|
||||
):
|
||||
with self._get_obj(to_info) as obj:
|
||||
total = os.path.getsize(from_file)
|
||||
with Tqdm(
|
||||
disable=no_progress_bar, total=total, bytes=True, desc=name
|
||||
) as pbar:
|
||||
obj.upload_file(
|
||||
from_file,
|
||||
Callback=pbar.update,
|
||||
ExtraArgs=self.extra_args,
|
||||
Config=self._transfer_config,
|
||||
)
|
||||
total = os.path.getsize(from_file)
|
||||
with Tqdm(
|
||||
disable=no_progress_bar,
|
||||
total=total,
|
||||
bytes=True,
|
||||
desc=name,
|
||||
**pbar_args,
|
||||
) as pbar:
|
||||
obj = self._get_obj(to_info)
|
||||
obj.upload_file(
|
||||
from_file,
|
||||
Callback=pbar.update,
|
||||
ExtraArgs=self.fs_args.get("s3_additional_kwargs"),
|
||||
Config=self._transfer_config,
|
||||
)
|
||||
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
|
||||
|
||||
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
|
||||
with self._get_obj(from_info) as obj:
|
||||
with Tqdm(
|
||||
disable=no_progress_bar,
|
||||
total=obj.content_length,
|
||||
bytes=True,
|
||||
desc=name,
|
||||
) as pbar:
|
||||
obj.download_file(
|
||||
to_file, Callback=pbar.update, Config=self._transfer_config
|
||||
)
|
||||
@_translate_exceptions
|
||||
def _download(
|
||||
self, from_info, to_file, name=None, no_progress_bar=False, **pbar_args
|
||||
):
|
||||
obj = self._get_obj(from_info)
|
||||
with Tqdm(
|
||||
disable=no_progress_bar,
|
||||
total=obj.content_length,
|
||||
bytes=True,
|
||||
desc=name,
|
||||
**pbar_args,
|
||||
) as pbar:
|
||||
obj.download_file(to_file, Callback=pbar.update)
|
||||
|
||||
+31
-41
@@ -5,11 +5,10 @@ import os
|
||||
import shutil
|
||||
import threading
|
||||
from contextlib import closing, contextmanager
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import first, memoize, silent, wrap_with
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc import prompt
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
@@ -30,7 +29,7 @@ def ask_password(host, user, port):
|
||||
)
|
||||
|
||||
|
||||
class SSHFileSystem(BaseFileSystem):
|
||||
class SSHFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
scheme = Schemes.SSH
|
||||
REQUIRES = {"paramiko": "paramiko"}
|
||||
_JOBS = 4
|
||||
@@ -46,36 +45,21 @@ class SSHFileSystem(BaseFileSystem):
|
||||
DEFAULT_PORT = 22
|
||||
TIMEOUT = 1800
|
||||
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
url = config.get("url")
|
||||
if url:
|
||||
parsed = urlparse(url)
|
||||
user_ssh_config = self._load_user_ssh_config(parsed.hostname)
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
user_ssh_config = self._load_user_ssh_config(config["host"])
|
||||
|
||||
host = user_ssh_config.get("hostname", parsed.hostname)
|
||||
user = (
|
||||
config.get("user")
|
||||
or parsed.username
|
||||
or user_ssh_config.get("user")
|
||||
or getpass.getuser()
|
||||
)
|
||||
port = (
|
||||
config.get("port")
|
||||
or parsed.port
|
||||
or self._try_get_ssh_config_port(user_ssh_config)
|
||||
or self.DEFAULT_PORT
|
||||
)
|
||||
self.path_info = self.PATH_CLS.from_parts(
|
||||
scheme=self.scheme,
|
||||
host=host,
|
||||
user=user,
|
||||
port=port,
|
||||
path=parsed.path,
|
||||
)
|
||||
else:
|
||||
self.path_info = None
|
||||
user_ssh_config = {}
|
||||
self.host = user_ssh_config.get("hostname", config["host"])
|
||||
self.user = (
|
||||
config.get("user")
|
||||
or user_ssh_config.get("user")
|
||||
or getpass.getuser()
|
||||
)
|
||||
self.port = (
|
||||
config.get("port")
|
||||
or self._try_get_ssh_config_port(user_ssh_config)
|
||||
or self.DEFAULT_PORT
|
||||
)
|
||||
|
||||
self.keyfile = config.get(
|
||||
"keyfile"
|
||||
@@ -93,6 +77,16 @@ class SSHFileSystem(BaseFileSystem):
|
||||
self.sock = None
|
||||
self.allow_agent = config.get("allow_agent", True)
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
from fsspec.implementations.sftp import SFTPFileSystem
|
||||
|
||||
# pylint:disable=protected-access
|
||||
kwargs = SFTPFileSystem._get_kwargs_from_urls(urlpath)
|
||||
if "username" in kwargs:
|
||||
kwargs["user"] = kwargs.pop("username")
|
||||
return kwargs
|
||||
|
||||
@staticmethod
|
||||
def ssh_config_filename():
|
||||
return os.path.expanduser(os.path.join("~", ".ssh", "config"))
|
||||
@@ -120,17 +114,13 @@ class SSHFileSystem(BaseFileSystem):
|
||||
def _try_get_ssh_config_keyfile(user_ssh_config):
|
||||
return first(user_ssh_config.get("identityfile") or ())
|
||||
|
||||
def ensure_credentials(self, path_info=None):
|
||||
if path_info is None:
|
||||
path_info = self.path_info
|
||||
|
||||
def ensure_credentials(self):
|
||||
# NOTE: we use the same password regardless of the server :(
|
||||
if self.ask_password and self.password is None:
|
||||
host, user, port = path_info.host, path_info.user, path_info.port
|
||||
self.password = ask_password(host, user, port)
|
||||
self.password = ask_password(self.host, self.user, self.port)
|
||||
|
||||
def ssh(self, path_info):
|
||||
self.ensure_credentials(path_info)
|
||||
self.ensure_credentials()
|
||||
|
||||
from .connection import SSHConnection
|
||||
|
||||
@@ -159,7 +149,7 @@ class SSHFileSystem(BaseFileSystem):
|
||||
else:
|
||||
yield io.TextIOWrapper(fd, encoding=encoding)
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
def exists(self, path_info) -> bool:
|
||||
with self.ssh(path_info) as ssh:
|
||||
return ssh.exists(path_info.path)
|
||||
|
||||
@@ -247,7 +237,7 @@ class SSHFileSystem(BaseFileSystem):
|
||||
with self.ssh(path_info) as ssh:
|
||||
return ssh.info(path_info.path)
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
def _upload_fobj(self, fobj, to_info, **kwargs):
|
||||
self.makedirs(to_info.parent)
|
||||
with self.open(to_info, mode="wb") as fdest:
|
||||
shutil.copyfileobj(fobj, fdest)
|
||||
|
||||
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix="B"):
|
||||
""" Convert number of bytes to human-readable string """
|
||||
"""Convert number of bytes to human-readable string"""
|
||||
for unit in ["", "K", "M", "G", "T", "P", "E", "Z"]:
|
||||
if abs(num) < 1024.0:
|
||||
return f"{num:3.1f}{unit}{suffix}"
|
||||
|
||||
+65
-237
@@ -1,273 +1,101 @@
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from collections import deque
|
||||
from functools import lru_cache
|
||||
|
||||
from funcy import cached_property, nullcontext, wrap_prop
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.config import ConfigError
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.path_info import HTTPURLInfo, WebDAVURLInfo
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.path_info import WebDAVURLInfo
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .base import BaseFileSystem
|
||||
from .fsspec_wrapper import FSSpecWrapper
|
||||
from .http import ask_password
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebDAVConnectionError(DvcException):
|
||||
def __init__(self, host):
|
||||
super().__init__(f"Unable to connect to WebDAV {host}.")
|
||||
|
||||
|
||||
class WebDAVFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
# Use webdav scheme
|
||||
class WebDAVFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
||||
scheme = Schemes.WEBDAV
|
||||
|
||||
# URLInfo for Webdav ~ replaces webdav -> http
|
||||
PATH_CLS = WebDAVURLInfo
|
||||
|
||||
# Traversable as walk_files is implemented
|
||||
CAN_TRAVERSE = True
|
||||
|
||||
# Length of walk_files prefix
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
# Implementation based on webdav3.client
|
||||
REQUIRES = {"webdavclient3": "webdav3.client"}
|
||||
|
||||
# Chunk size for buffered upload/download with progress bar
|
||||
CHUNK_SIZE = 2 ** 16
|
||||
|
||||
REQUIRES = {"webdav4": "webdav4"}
|
||||
PARAM_CHECKSUM = "etag"
|
||||
DETAIL_FIELDS = frozenset(("etag", "size"))
|
||||
|
||||
# Constructor
|
||||
def __init__(self, repo, config):
|
||||
# Call BaseFileSystem constructor
|
||||
super().__init__(repo, config)
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
# Get username from configuration
|
||||
self.user = config.get("user", None)
|
||||
cert_path = config.get("cert_path", None)
|
||||
key_path = config.get("key_path", None)
|
||||
self.prefix = config.get("prefix", "")
|
||||
self.fs_args.update(
|
||||
{
|
||||
"base_url": config["url"],
|
||||
"cert": cert_path if not key_path else (cert_path, key_path),
|
||||
"verify": config.get("ssl_verify", True),
|
||||
"timeout": config.get("timeout", 30),
|
||||
}
|
||||
)
|
||||
|
||||
# Get password from configuration (might be None ~ not set)
|
||||
self.password = config.get("password", None)
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
path_info = WebDAVURLInfo(urlpath)
|
||||
return {
|
||||
"prefix": path_info.path.rstrip("/"),
|
||||
"host": path_info.replace(path="").url,
|
||||
"url": path_info.url.rstrip("/"),
|
||||
"user": path_info.user,
|
||||
}
|
||||
|
||||
# Whether to ask for password if it is not set
|
||||
self.ask_password = config.get("ask_password", False)
|
||||
def _prepare_credentials(self, **config):
|
||||
user = config.get("user", None)
|
||||
password = config.get("password", None)
|
||||
|
||||
# Use token for webdav auth
|
||||
self.token = config.get("token", None)
|
||||
headers = {}
|
||||
token = config.get("token")
|
||||
auth = None
|
||||
if token:
|
||||
headers.update({"Authorization": f"Bearer {token}"})
|
||||
elif user:
|
||||
if not password and config.get("ask_password"):
|
||||
password = ask_password(config["host"], user)
|
||||
auth = (user, password)
|
||||
|
||||
# Path to certificate
|
||||
self.cert_path = config.get("cert_path", None)
|
||||
return {"headers": headers, "auth": auth}
|
||||
|
||||
# Path to private key
|
||||
self.key_path = config.get("key_path", None)
|
||||
|
||||
# Connection timeout
|
||||
self.timeout = config.get("timeout", 30)
|
||||
|
||||
# Get URL from configuration
|
||||
self.url = config.get("url", None)
|
||||
|
||||
# If URL in config parse path_info
|
||||
if self.url:
|
||||
self.path_info = self.PATH_CLS(self.url)
|
||||
|
||||
# If username not specified try to use from URL
|
||||
if self.user is None and self.path_info.user is not None:
|
||||
self.user = self.path_info.user
|
||||
|
||||
# Construct hostname from path_info by stripping path
|
||||
http_info = HTTPURLInfo(self.path_info.url)
|
||||
self.hostname = http_info.replace(path="").url
|
||||
|
||||
# Webdav client
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def _client(self):
|
||||
from webdav3.client import Client
|
||||
def fs(self):
|
||||
from webdav4.fsspec import WebdavFileSystem
|
||||
|
||||
# Set password or ask for it
|
||||
if self.ask_password and self.password is None and self.token is None:
|
||||
self.password = ask_password(self.hostname, self.user)
|
||||
return WebdavFileSystem(**self.fs_args)
|
||||
|
||||
# Setup webdav client options dictionary
|
||||
options = {
|
||||
"webdav_hostname": self.hostname,
|
||||
"webdav_login": self.user,
|
||||
"webdav_password": self.password,
|
||||
"webdav_token": self.token,
|
||||
"webdav_cert_path": self.cert_path,
|
||||
"webdav_key_path": self.key_path,
|
||||
"webdav_timeout": self.timeout,
|
||||
"webdav_chunk_size": self.CHUNK_SIZE,
|
||||
}
|
||||
def _upload_fobj(self, fobj, to_info, size: int = None):
|
||||
rpath = self.translate_path_info(to_info)
|
||||
self.makedirs(os.path.dirname(rpath))
|
||||
# using upload_fileobj to directly upload fileobj rather than buffering
|
||||
# and using overwrite=True to avoid check for an extra exists call,
|
||||
# as caller should ensure that the file does not exist beforehand.
|
||||
return self.fs.client.upload_fileobj(
|
||||
fobj, rpath, overwrite=True, size=size
|
||||
)
|
||||
|
||||
client = Client(options)
|
||||
|
||||
# Check whether client options are valid
|
||||
if not client.valid():
|
||||
raise ConfigError(
|
||||
f"Configuration for WebDAV {self.hostname} is invalid."
|
||||
)
|
||||
|
||||
# Check whether connection is valid (root should always exist)
|
||||
if not client.check(self.path_info.path):
|
||||
raise WebDAVConnectionError(self.hostname)
|
||||
|
||||
return client
|
||||
|
||||
def open(self, path_info, mode="r", encoding=None, **kwargs):
|
||||
from webdav3.exceptions import RemoteResourceNotFound
|
||||
|
||||
assert mode in {"r", "rt", "rb"}
|
||||
|
||||
fobj = io.BytesIO()
|
||||
|
||||
try:
|
||||
self._client.download_from(buff=fobj, remote_path=path_info.path)
|
||||
except RemoteResourceNotFound as exc:
|
||||
raise FileNotFoundError from exc
|
||||
|
||||
fobj.seek(0)
|
||||
|
||||
if "mode" == "rb":
|
||||
return fobj
|
||||
|
||||
return io.TextIOWrapper(fobj, encoding=encoding)
|
||||
|
||||
# Checks whether file/directory exists at remote
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
# Use webdav check to test for file existence
|
||||
return self._client.check(path_info.path)
|
||||
|
||||
# Checks whether path points to directory
|
||||
def isdir(self, path_info):
|
||||
# Use webdav is_dir to test whether path points to a directory
|
||||
return self._client.is_dir(path_info.path)
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
if not self.exists(path_info):
|
||||
return
|
||||
|
||||
for file in self.find(path_info):
|
||||
yield path_info.replace(path=file)
|
||||
|
||||
def ls(self, path_info, detail=False):
|
||||
for entry in self._client.list(path_info.path):
|
||||
path = entry["path"]
|
||||
if detail:
|
||||
if entry["isdir"]:
|
||||
yield {"type": "directory", "name": path}
|
||||
else:
|
||||
yield {
|
||||
"type": "file",
|
||||
"name": path,
|
||||
"size": entry["size"],
|
||||
"etag": entry["etag"],
|
||||
}
|
||||
else:
|
||||
yield path
|
||||
|
||||
def find(self, path_info, detail=False):
|
||||
dirs = deque([path_info.path])
|
||||
|
||||
while dirs:
|
||||
for entry in self._client.list(dirs.pop(), get_info=True):
|
||||
path = entry["path"]
|
||||
if entry["isdir"]:
|
||||
dirs.append(path)
|
||||
continue
|
||||
|
||||
if detail:
|
||||
yield {
|
||||
"type": "file",
|
||||
"name": path,
|
||||
"size": entry["size"],
|
||||
"etag": entry["etag"],
|
||||
}
|
||||
else:
|
||||
yield path
|
||||
|
||||
# Removes file/directory
|
||||
def remove(self, path_info):
|
||||
# Use webdav client clean (DELETE) method to remove file/directory
|
||||
self._client.clean(path_info.path)
|
||||
|
||||
# Creates directories
|
||||
def makedirs(self, path_info):
|
||||
# Terminate recursion
|
||||
if path_info.path == self.path_info.path or self.exists(path_info):
|
||||
return
|
||||
path = self.translate_path_info(path_info)
|
||||
return self.fs.makedirs(path, exist_ok=True)
|
||||
|
||||
# Recursively descent to root
|
||||
self.makedirs(path_info.parent)
|
||||
@lru_cache(512)
|
||||
def translate_path_info(self, path):
|
||||
if isinstance(path, self.PATH_CLS):
|
||||
return path.path[len(self.prefix) :].lstrip("/")
|
||||
return path
|
||||
|
||||
# Construct directory at current recursion depth
|
||||
self._client.mkdir(path_info.path)
|
||||
_with_bucket = translate_path_info
|
||||
|
||||
# Moves file/directory at remote
|
||||
def move(self, from_info, to_info):
|
||||
# Webdav client move
|
||||
self._client.move(from_info.path, to_info.path)
|
||||
def _strip_bucket(self, entry):
|
||||
return entry
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
# In contrast to other upload_fobj implementations, this one does not
|
||||
# exactly do a chunked-upload but rather put everything in one request.
|
||||
self.makedirs(to_info.parent)
|
||||
self._client.upload_to(buff=fobj, remote_path=to_info.path)
|
||||
|
||||
# Downloads file from remote to file
|
||||
def _download(self, from_info, to_file, name=None, no_progress_bar=False):
|
||||
# Progress from HTTPFileSystem
|
||||
with open(to_file, "wb") as fd:
|
||||
with Tqdm.wrapattr(
|
||||
fd,
|
||||
"write",
|
||||
total=None if no_progress_bar else self.getsize(from_info),
|
||||
leave=False,
|
||||
desc=from_info.url if name is None else name,
|
||||
disable=no_progress_bar,
|
||||
) as fd_wrapped:
|
||||
# Download from WebDAV via buffer
|
||||
self._client.download_from(
|
||||
buff=fd_wrapped, remote_path=from_info.path
|
||||
)
|
||||
|
||||
# Uploads file to remote
|
||||
def _upload(
|
||||
self, from_file, to_info, name=None, no_progress_bar=False, **_kwargs,
|
||||
):
|
||||
# First try to create parent directories
|
||||
self.makedirs(to_info.parent)
|
||||
|
||||
file_size = os.path.getsize(from_file)
|
||||
with open(from_file, "rb") as fd:
|
||||
progress_context = (
|
||||
nullcontext(fd)
|
||||
if file_size == 0
|
||||
else Tqdm.wrapattr(
|
||||
fd,
|
||||
"read",
|
||||
total=None if no_progress_bar else file_size,
|
||||
leave=False,
|
||||
desc=to_info.url if name is None else name,
|
||||
disable=no_progress_bar,
|
||||
)
|
||||
)
|
||||
with progress_context as fd_wrapped:
|
||||
self._client.upload_to(
|
||||
buff=fd_wrapped, remote_path=to_info.path
|
||||
)
|
||||
|
||||
def info(self, path_info):
|
||||
info = self._client.info(path_info.path)
|
||||
return {
|
||||
"size": int(info["size"]),
|
||||
"etag": info["etag"],
|
||||
}
|
||||
class WebDAVSFileSystem(WebDAVFileSystem): # pylint:disable=abstract-method
|
||||
scheme = Schemes.WEBDAVS
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .webdav import WebDAVFileSystem
|
||||
|
||||
|
||||
class WebDAVSFileSystem(WebDAVFileSystem): # pylint:disable=abstract-method
|
||||
scheme = Schemes.WEBDAVS
|
||||
+18
-18
@@ -4,7 +4,6 @@ import posixpath
|
||||
import shutil
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
@@ -30,33 +29,34 @@ def update_pbar(pbar, total):
|
||||
return update
|
||||
|
||||
|
||||
class WebHDFSFileSystem(BaseFileSystem):
|
||||
class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
scheme = Schemes.WEBHDFS
|
||||
PATH_CLS = CloudURLInfo
|
||||
REQUIRES = {"hdfs": "hdfs"}
|
||||
PARAM_CHECKSUM = "checksum"
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
def __init__(self, repo, config):
|
||||
super().__init__(repo, config)
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
self.path_info = None
|
||||
url = config.get("url")
|
||||
if not url:
|
||||
return
|
||||
|
||||
self.path_info = self.PATH_CLS(url)
|
||||
|
||||
parsed = urlparse(url)
|
||||
|
||||
self.host = parsed.hostname
|
||||
self.user = parsed.username or config.get("user")
|
||||
self.port = parsed.port
|
||||
self.host = config["host"]
|
||||
self.user = config.get("user")
|
||||
self.port = config.get("port")
|
||||
|
||||
self.hdfscli_config = config.get("hdfscli_config")
|
||||
self.token = config.get("webhdfs_token")
|
||||
self.alias = config.get("webhdfs_alias")
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
from fsspec.implementations.webhdfs import WebHDFS
|
||||
|
||||
return (
|
||||
WebHDFS._get_kwargs_from_urls( # pylint:disable=protected-access
|
||||
urlpath
|
||||
)
|
||||
)
|
||||
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def hdfs_client(self):
|
||||
@@ -114,7 +114,7 @@ class WebHDFSFileSystem(BaseFileSystem):
|
||||
|
||||
self.hdfs_client.delete(path_info.path)
|
||||
|
||||
def exists(self, path_info, use_dvcignore=True):
|
||||
def exists(self, path_info) -> bool:
|
||||
assert not isinstance(path_info, list)
|
||||
assert path_info.scheme == "webhdfs"
|
||||
|
||||
@@ -141,7 +141,7 @@ class WebHDFSFileSystem(BaseFileSystem):
|
||||
self.hdfs_client.makedirs(to_info.parent.path)
|
||||
self.hdfs_client.rename(from_info.path, to_info.path)
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
def _upload_fobj(self, fobj, to_info, **kwargs):
|
||||
with self.hdfs_client.write(to_info.path) as fdest:
|
||||
shutil.copyfileobj(fobj, fdest)
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ class HashInfo:
|
||||
def __str__(self):
|
||||
return f"{self.name}: {self.value}"
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.value))
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
_d = d.copy() if d else {}
|
||||
|
||||
+115
-82
@@ -7,9 +7,12 @@ from itertools import groupby, takewhile
|
||||
from pathspec.patterns import GitWildMatchPattern
|
||||
from pathspec.util import normalize_file
|
||||
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.pathspec_math import PatternInfo, merge_patterns
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.system import System
|
||||
from dvc.types import AnyPath, List, Optional
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.collections import PathStringTrie
|
||||
|
||||
@@ -49,16 +52,12 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_files(cls, ignore_file_path, fs):
|
||||
assert os.path.isabs(ignore_file_path)
|
||||
dirname = os.path.normpath(os.path.dirname(ignore_file_path))
|
||||
ignore_file_rel_path = os.path.relpath(ignore_file_path, fs.fs_root)
|
||||
with fs.open(ignore_file_path, encoding="utf-8") as fobj:
|
||||
def from_file(cls, path, fs, name):
|
||||
assert os.path.isabs(path)
|
||||
dirname = os.path.normpath(os.path.dirname(path))
|
||||
with fs.open(path, encoding="utf-8") as fobj:
|
||||
path_spec_lines = [
|
||||
PatternInfo(
|
||||
line,
|
||||
"{}:{}:{}".format(ignore_file_rel_path, line_no + 1, line),
|
||||
)
|
||||
PatternInfo(line, "{}:{}:{}".format(name, line_no + 1, line))
|
||||
for line_no, line in enumerate(
|
||||
map(str.strip, fobj.readlines())
|
||||
)
|
||||
@@ -67,7 +66,7 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
|
||||
return cls(path_spec_lines, dirname)
|
||||
|
||||
def __call__(self, root, dirs, files):
|
||||
def __call__(self, root: List[str], dirs: List[str], files: List[str]):
|
||||
files = [f for f in files if not self.matches(root, f)]
|
||||
dirs = [d for d in dirs if not self.matches(root, d, True)]
|
||||
|
||||
@@ -159,26 +158,6 @@ def _no_match(path):
|
||||
return CheckIgnoreResult(path, False, ["::"])
|
||||
|
||||
|
||||
class DvcIgnoreFilterNoop:
|
||||
def __init__(self, fs, root_dir):
|
||||
pass
|
||||
|
||||
def __call__(self, root, dirs, files, **kwargs):
|
||||
return dirs, files
|
||||
|
||||
def is_ignored_dir(self, _):
|
||||
return False
|
||||
|
||||
def is_ignored_file(self, _):
|
||||
return False
|
||||
|
||||
def check_ignore(self, path):
|
||||
return _no_match(path)
|
||||
|
||||
def is_ignored(self, _):
|
||||
return False
|
||||
|
||||
|
||||
class DvcIgnoreFilter:
|
||||
def __init__(self, fs, root_dir):
|
||||
from dvc.repo import Repo
|
||||
@@ -193,27 +172,34 @@ class DvcIgnoreFilter:
|
||||
self.fs = fs
|
||||
self.root_dir = root_dir
|
||||
self.ignores_trie_fs = PathStringTrie()
|
||||
self._ignores_trie_subrepos = PathStringTrie()
|
||||
self.ignores_trie_fs[root_dir] = DvcIgnorePatterns(
|
||||
default_ignore_patterns, root_dir
|
||||
)
|
||||
self._ignored_subrepos = PathStringTrie()
|
||||
self._update(self.root_dir)
|
||||
self._ignores_trie_subrepos[root_dir] = self.ignores_trie_fs[root_dir]
|
||||
self._update(
|
||||
self.root_dir,
|
||||
self._ignores_trie_subrepos,
|
||||
dnames=None,
|
||||
ignore_subrepos=False,
|
||||
)
|
||||
self._update(
|
||||
self.root_dir,
|
||||
self.ignores_trie_fs,
|
||||
dnames=None,
|
||||
ignore_subrepos=True,
|
||||
)
|
||||
|
||||
def _update(self, dirname):
|
||||
self._update_sub_repo(dirname)
|
||||
|
||||
old_pattern = self.ignores_trie_fs.longest_prefix(dirname).value
|
||||
def _update_trie(self, dirname: str, trie: PathStringTrie) -> None:
|
||||
old_pattern = trie.longest_prefix(dirname).value
|
||||
matches = old_pattern.matches(dirname, DvcIgnore.DVCIGNORE_FILE, False)
|
||||
|
||||
ignore_file_path = os.path.join(dirname, DvcIgnore.DVCIGNORE_FILE)
|
||||
if not matches and self.fs.exists(
|
||||
ignore_file_path, use_dvcignore=False
|
||||
):
|
||||
new_pattern = DvcIgnorePatterns.from_files(
|
||||
ignore_file_path, self.fs
|
||||
)
|
||||
path = os.path.join(dirname, DvcIgnore.DVCIGNORE_FILE)
|
||||
if not matches and self.fs.exists(path):
|
||||
name = os.path.relpath(path, self.root_dir)
|
||||
new_pattern = DvcIgnorePatterns.from_file(path, self.fs, name)
|
||||
if old_pattern:
|
||||
self.ignores_trie_fs[dirname] = DvcIgnorePatterns(
|
||||
trie[dirname] = DvcIgnorePatterns(
|
||||
*merge_patterns(
|
||||
old_pattern.pattern_list,
|
||||
old_pattern.dirname,
|
||||
@@ -222,11 +208,32 @@ class DvcIgnoreFilter:
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.ignores_trie_fs[dirname] = new_pattern
|
||||
trie[dirname] = new_pattern
|
||||
elif old_pattern:
|
||||
self.ignores_trie_fs[dirname] = old_pattern
|
||||
trie[dirname] = old_pattern
|
||||
|
||||
def _update_sub_repo(self, path):
|
||||
def _update(
|
||||
self,
|
||||
dirname: str,
|
||||
ignore_trie: PathStringTrie,
|
||||
dnames: Optional["List"],
|
||||
ignore_subrepos: bool,
|
||||
) -> None:
|
||||
self._update_trie(dirname, ignore_trie)
|
||||
|
||||
if ignore_subrepos:
|
||||
if dnames is None:
|
||||
try:
|
||||
_, dnames, _ = next(self.fs.walk(dirname))
|
||||
except StopIteration:
|
||||
dnames = []
|
||||
|
||||
for dname in dnames:
|
||||
self._update_sub_repo(
|
||||
os.path.join(dirname, dname), ignore_trie
|
||||
)
|
||||
|
||||
def _update_sub_repo(self, path, ignore_trie: PathStringTrie):
|
||||
from dvc.repo import Repo
|
||||
|
||||
if path == self.root_dir:
|
||||
@@ -237,14 +244,11 @@ class DvcIgnoreFilter:
|
||||
return
|
||||
|
||||
root, dname = os.path.split(path)
|
||||
self._ignored_subrepos[root] = self._ignored_subrepos.get(
|
||||
root, set()
|
||||
) | {dname}
|
||||
pattern_info = PatternInfo(f"/{dname}/", f"in sub_repo:{dname}")
|
||||
new_pattern = DvcIgnorePatterns([pattern_info], root)
|
||||
old_pattern = self.ignores_trie_fs.longest_prefix(root).value
|
||||
old_pattern = ignore_trie.longest_prefix(root).value
|
||||
if old_pattern:
|
||||
self.ignores_trie_fs[root] = DvcIgnorePatterns(
|
||||
ignore_trie[root] = DvcIgnorePatterns(
|
||||
*merge_patterns(
|
||||
old_pattern.pattern_list,
|
||||
old_pattern.dirname,
|
||||
@@ -253,25 +257,50 @@ class DvcIgnoreFilter:
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.ignores_trie_fs[root] = new_pattern
|
||||
ignore_trie[root] = new_pattern
|
||||
|
||||
def __call__(self, root, dirs, files, ignore_subrepos=True):
|
||||
for dname in dirs:
|
||||
self._update_sub_repo(os.path.join(root, dname))
|
||||
|
||||
ignore_pattern = self._get_trie_pattern(root)
|
||||
abs_root = os.path.abspath(root)
|
||||
ignore_pattern = self._get_trie_pattern(
|
||||
abs_root, dnames=dirs, ignore_subrepos=ignore_subrepos
|
||||
)
|
||||
if ignore_pattern:
|
||||
dirs, files = ignore_pattern(root, dirs, files)
|
||||
if not ignore_subrepos:
|
||||
dirs.extend(self._ignored_subrepos.get(root, []))
|
||||
dirs, files = ignore_pattern(abs_root, dirs, files)
|
||||
return dirs, files
|
||||
|
||||
def _get_trie_pattern(self, dirname):
|
||||
ignore_pattern = self.ignores_trie_fs.get(dirname)
|
||||
def walk(self, fs: BaseFileSystem, path_info: AnyPath, **kwargs):
|
||||
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
|
||||
if fs.scheme == Schemes.LOCAL:
|
||||
for root, dirs, files in fs.walk(path_info, **kwargs):
|
||||
dirs[:], files[:] = self(
|
||||
root, dirs, files, ignore_subrepos=ignore_subrepos
|
||||
)
|
||||
yield root, dirs, files
|
||||
else:
|
||||
yield from fs.walk(path_info, **kwargs)
|
||||
|
||||
def walk_files(self, fs: BaseFileSystem, path_info: AnyPath, **kwargs):
|
||||
if fs.scheme == Schemes.LOCAL:
|
||||
for root, _, files in self.walk(fs, path_info, **kwargs):
|
||||
for file in files:
|
||||
# NOTE: os.path.join is ~5.5 times slower
|
||||
yield PathInfo(f"{root}{os.sep}{file}")
|
||||
else:
|
||||
yield from fs.walk_files(path_info)
|
||||
|
||||
def _get_trie_pattern(
|
||||
self, dirname, dnames: Optional["List"] = None, ignore_subrepos=True
|
||||
) -> Optional["DvcIgnorePatterns"]:
|
||||
if ignore_subrepos:
|
||||
ignores_trie = self.ignores_trie_fs
|
||||
else:
|
||||
ignores_trie = self._ignores_trie_subrepos
|
||||
|
||||
ignore_pattern = ignores_trie.get(dirname)
|
||||
if ignore_pattern:
|
||||
return ignore_pattern
|
||||
|
||||
prefix = self.ignores_trie_fs.longest_prefix(dirname).key
|
||||
prefix = ignores_trie.longest_prefix(dirname).key
|
||||
if not prefix:
|
||||
# outside of the repo
|
||||
return None
|
||||
@@ -286,33 +315,31 @@ class DvcIgnoreFilter:
|
||||
dirs.append(dirname)
|
||||
|
||||
for parent in dirs:
|
||||
self._update(parent)
|
||||
self._update(parent, ignores_trie, dnames, ignore_subrepos)
|
||||
|
||||
return self.ignores_trie_fs.get(dirname)
|
||||
return ignores_trie.get(dirname)
|
||||
|
||||
def _is_ignored(self, path, is_dir=False):
|
||||
def _is_ignored(
|
||||
self, path: str, is_dir: bool = False, ignore_subrepos: bool = True
|
||||
):
|
||||
if self._outside_repo(path):
|
||||
return False
|
||||
dirname, basename = os.path.split(os.path.normpath(path))
|
||||
ignore_pattern = self._get_trie_pattern(dirname)
|
||||
ignore_pattern = self._get_trie_pattern(dirname, None, ignore_subrepos)
|
||||
if ignore_pattern:
|
||||
return ignore_pattern.matches(dirname, basename, is_dir)
|
||||
return False
|
||||
|
||||
def _is_subrepo(self, path):
|
||||
dirname, basename = os.path.split(os.path.normpath(path))
|
||||
return basename in self._ignored_subrepos.get(dirname, set())
|
||||
|
||||
def is_ignored_dir(self, path, ignore_subrepos=True):
|
||||
def is_ignored_dir(self, path: str, ignore_subrepos: bool = True) -> bool:
|
||||
"Only used in LocalFileSystem"
|
||||
path = os.path.abspath(path)
|
||||
if not ignore_subrepos:
|
||||
return not self._is_subrepo(path)
|
||||
if path == self.root_dir:
|
||||
return False
|
||||
|
||||
return self._is_ignored(path, True)
|
||||
return self._is_ignored(path, True, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def is_ignored_file(self, path):
|
||||
def is_ignored_file(self, path: str) -> bool:
|
||||
"Only used in LocalFileSystem"
|
||||
path = os.path.abspath(path)
|
||||
return self._is_ignored(path, False)
|
||||
|
||||
@@ -339,21 +366,27 @@ class DvcIgnoreFilter:
|
||||
pattern = self._get_trie_pattern(dirname)
|
||||
if pattern:
|
||||
matches = pattern.matches(
|
||||
dirname, basename, os.path.isdir(full_target), True,
|
||||
dirname, basename, os.path.isdir(full_target), True
|
||||
)
|
||||
|
||||
if matches:
|
||||
return CheckIgnoreResult(target, True, matches)
|
||||
return _no_match(target)
|
||||
|
||||
def is_ignored(self, path):
|
||||
def is_ignored(
|
||||
self, fs: BaseFileSystem, path: str, ignore_subrepos: bool = True
|
||||
) -> bool:
|
||||
# NOTE: can't use self.check_ignore(path).match for now, see
|
||||
# https://github.com/iterative/dvc/issues/4555
|
||||
if os.path.isfile(path):
|
||||
if fs.scheme != Schemes.LOCAL:
|
||||
return False
|
||||
if fs.isfile(path):
|
||||
return self.is_ignored_file(path)
|
||||
if os.path.isdir(path):
|
||||
return self.is_ignored_dir(path)
|
||||
return self.is_ignored_file(path) or self.is_ignored_dir(path)
|
||||
if fs.isdir(path):
|
||||
return self.is_ignored_dir(path, ignore_subrepos)
|
||||
return self.is_ignored_file(path) or self.is_ignored_dir(
|
||||
path, ignore_subrepos
|
||||
)
|
||||
|
||||
|
||||
def init(path):
|
||||
|
||||
+20
-22
@@ -31,34 +31,32 @@ def get_dvc_info():
|
||||
]
|
||||
|
||||
try:
|
||||
repo = Repo()
|
||||
with Repo() as repo:
|
||||
# cache_dir might not exist yet (e.g. after `dvc init`), and we
|
||||
# can't auto-create it, as it might cause issues if the user
|
||||
# later decides to enable shared cache mode with
|
||||
# `dvc config cache.shared group`.
|
||||
if os.path.exists(repo.odb.local.cache_dir):
|
||||
info.append(
|
||||
"Cache types: {}".format(_get_linktype_support_info(repo))
|
||||
)
|
||||
fs_type = get_fs_type(repo.odb.local.cache_dir)
|
||||
info.append(f"Cache directory: {fs_type}")
|
||||
else:
|
||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||
|
||||
# cache_dir might not exist yet (e.g. after `dvc init`), and we
|
||||
# can't auto-create it, as it might cause issues if the user
|
||||
# later decides to enable shared cache mode with
|
||||
# `dvc config cache.shared group`.
|
||||
if os.path.exists(repo.odb.local.cache_dir):
|
||||
info.append(
|
||||
"Cache types: {}".format(_get_linktype_support_info(repo))
|
||||
)
|
||||
fs_type = get_fs_type(repo.odb.local.cache_dir)
|
||||
info.append(f"Cache directory: {fs_type}")
|
||||
else:
|
||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||
|
||||
info.append(f"Caches: {_get_caches(repo.odb)}")
|
||||
|
||||
info.append(f"Remotes: {_get_remotes(repo.config)}")
|
||||
info.append(f"Caches: {_get_caches(repo.odb)}")
|
||||
info.append(f"Remotes: {_get_remotes(repo.config)}")
|
||||
|
||||
root_directory = repo.root_dir
|
||||
fs_root = get_fs_type(os.path.abspath(root_directory))
|
||||
info.append(f"Workspace directory: {fs_root}")
|
||||
info.append("Repo: {}".format(_get_dvc_repo_info(repo)))
|
||||
except NotDvcRepoError:
|
||||
pass
|
||||
except SCMError:
|
||||
info.append("Repo: dvc, git (broken)")
|
||||
else:
|
||||
root_directory = repo.root_dir
|
||||
fs_root = get_fs_type(os.path.abspath(root_directory))
|
||||
info.append(f"Workspace directory: {fs_root}")
|
||||
info.append("Repo: {}".format(_get_dvc_repo_info(repo)))
|
||||
|
||||
return "\n".join(info)
|
||||
|
||||
|
||||
|
||||
+4
-4
@@ -23,10 +23,10 @@ def istextblock(block):
|
||||
|
||||
|
||||
def istextfile(fname, fs, blocksize=512):
|
||||
""" Uses heuristics to guess whether the given file is text or binary,
|
||||
by reading a single block of bytes from the file.
|
||||
If more than 30% of the chars in the block are non-text, or there
|
||||
are NUL ('\x00') bytes in the block, assume this is a binary file.
|
||||
"""Uses heuristics to guess whether the given file is text or binary,
|
||||
by reading a single block of bytes from the file.
|
||||
If more than 30% of the chars in the block are non-text, or there
|
||||
are NUL ('\x00') bytes in the block, assume this is a binary file.
|
||||
"""
|
||||
with fs.open(fname, "rb") as fobj:
|
||||
block = fobj.read(blocksize)
|
||||
|
||||
+14
-3
@@ -188,8 +188,8 @@ def _stack_trace(exc_info):
|
||||
|
||||
def disable_other_loggers():
|
||||
logging.captureWarnings(True)
|
||||
root = logging.root
|
||||
for (logger_name, logger) in root.manager.loggerDict.items():
|
||||
loggerDict = logging.root.manager.loggerDict # pylint: disable=no-member
|
||||
for logger_name, logger in loggerDict.items():
|
||||
if logger_name != "dvc" and not logger_name.startswith("dvc."):
|
||||
logger.disabled = True
|
||||
|
||||
@@ -197,6 +197,17 @@ def disable_other_loggers():
|
||||
def setup(level=logging.INFO):
|
||||
colorama.init()
|
||||
|
||||
if level >= logging.DEBUG:
|
||||
# Unclosed session errors for asyncio/aiohttp are only available
|
||||
# on the tracing mode for extensive debug purposes. They are really
|
||||
# noisy, and this is potentially somewhere in the client library
|
||||
# not managing their own session. Even though it is the best practice
|
||||
# for them to do so, we can be assured that these errors raised when
|
||||
# the object is getting deallocated, so no need to take any extensive
|
||||
# action.
|
||||
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("aiohttp").setLevel(logging.CRITICAL)
|
||||
|
||||
addLoggingLevel("TRACE", logging.DEBUG - 5)
|
||||
logging.config.dictConfig(
|
||||
{
|
||||
@@ -245,7 +256,7 @@ def setup(level=logging.INFO):
|
||||
"console_trace",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
"disable_existing_loggers": False,
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ def main(argv=None): # noqa: C901
|
||||
|
||||
with debugtools(args):
|
||||
cmd = args.func(args)
|
||||
ret = cmd.run()
|
||||
ret = cmd.do_run()
|
||||
except ConfigError:
|
||||
logger.exception("configuration error")
|
||||
ret = 251
|
||||
|
||||
+12
-2
@@ -1,14 +1,24 @@
|
||||
import logging
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
from .tree import Tree
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .db.base import ObjectDB
|
||||
from .file import HashFile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def save(odb, obj, jobs=None, **kwargs):
|
||||
def save(
|
||||
odb: "ObjectDB",
|
||||
obj: "HashFile",
|
||||
jobs: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if isinstance(obj, Tree):
|
||||
with ThreadPoolExecutor(max_workers=jobs) as executor:
|
||||
for future in Tqdm(
|
||||
@@ -18,7 +28,7 @@ def save(odb, obj, jobs=None, **kwargs):
|
||||
entry.path_info,
|
||||
entry.fs,
|
||||
entry.hash_info,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
for _, entry in obj
|
||||
),
|
||||
|
||||
+7
-105
@@ -1,24 +1,22 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
|
||||
def get_odb(fs):
|
||||
def get_odb(fs, path_info, **config):
|
||||
from .base import ObjectDB
|
||||
from .gdrive import GDriveObjectDB
|
||||
from .local import LocalObjectDB
|
||||
from .ssh import SSHObjectDB
|
||||
|
||||
if fs.scheme == Schemes.LOCAL:
|
||||
return LocalObjectDB(fs)
|
||||
return LocalObjectDB(fs, path_info, **config)
|
||||
|
||||
if fs.scheme == Schemes.SSH:
|
||||
return SSHObjectDB(fs)
|
||||
return SSHObjectDB(fs, path_info, **config)
|
||||
|
||||
if fs.scheme == Schemes.GDRIVE:
|
||||
return GDriveObjectDB(fs)
|
||||
return GDriveObjectDB(fs, path_info, **config)
|
||||
|
||||
return ObjectDB(fs)
|
||||
return ObjectDB(fs, path_info, **config)
|
||||
|
||||
|
||||
def _get_odb(repo, settings):
|
||||
@@ -27,8 +25,8 @@ def _get_odb(repo, settings):
|
||||
if not settings:
|
||||
return None
|
||||
|
||||
fs = get_cloud_fs(repo, **settings)
|
||||
return get_odb(fs)
|
||||
cls, config, path_info = get_cloud_fs(repo, **settings)
|
||||
return get_odb(cls(**config), path_info, state=repo.state, **config)
|
||||
|
||||
|
||||
class ODBManager:
|
||||
@@ -80,99 +78,3 @@ class ODBManager:
|
||||
def by_scheme(self):
|
||||
self._init_odb(self.CLOUD_SCHEMES)
|
||||
yield from self._odb.items()
|
||||
|
||||
|
||||
class NamedCacheItem:
|
||||
def __init__(self):
|
||||
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:
|
||||
# pylint: disable=protected-access
|
||||
def __init__(self):
|
||||
self._items = defaultdict(lambda: defaultdict(NamedCacheItem))
|
||||
self.external = defaultdict(set)
|
||||
|
||||
@classmethod
|
||||
def make(cls, scheme, checksum, name):
|
||||
cache = cls()
|
||||
cache.add(scheme, checksum, name)
|
||||
return cache
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._items[key]
|
||||
|
||||
def add(self, scheme, checksum, 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)
|
||||
|
||||
def update(self, cache, suffix=""):
|
||||
for scheme, src in cache._items.items():
|
||||
dst = self._items[scheme]
|
||||
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()
|
||||
|
||||
+52
-18
@@ -7,6 +7,7 @@ from typing import Optional
|
||||
|
||||
from dvc.objects.errors import ObjectFormatError
|
||||
from dvc.objects.file import HashFile
|
||||
from dvc.objects.tree import Tree
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -18,15 +19,33 @@ class ObjectDB:
|
||||
DEFAULT_CACHE_TYPES = ["copy"]
|
||||
CACHE_MODE: Optional[int] = None
|
||||
|
||||
def __init__(self, fs):
|
||||
self.fs = fs
|
||||
self.repo = fs.repo
|
||||
def __init__(self, fs, path_info, **config):
|
||||
from dvc.state import StateNoop
|
||||
|
||||
self.verify = fs.config.get("verify", self.DEFAULT_VERIFY)
|
||||
self.cache_types = fs.config.get("type") or copy(
|
||||
self.DEFAULT_CACHE_TYPES
|
||||
)
|
||||
self.fs = fs
|
||||
self.path_info = path_info
|
||||
self.state = config.get("state", StateNoop())
|
||||
self.verify = config.get("verify", self.DEFAULT_VERIFY)
|
||||
self.cache_types = config.get("type") or copy(self.DEFAULT_CACHE_TYPES)
|
||||
self.cache_type_confirmed = False
|
||||
self.slow_link_warning = config.get("slow_link_warning", True)
|
||||
self.tmp_dir = config.get("tmp_dir")
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
return {
|
||||
"state": self.state,
|
||||
"verify": self.verify,
|
||||
"type": self.cache_types,
|
||||
"slow_link_warning": self.slow_link_warning,
|
||||
"tmp_dir": self.tmp_dir,
|
||||
}
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.fs == other.fs and self.path_info == other.path_info
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.fs.scheme, self.path_info))
|
||||
|
||||
def move(self, from_info, to_info):
|
||||
self.fs.move(from_info, to_info)
|
||||
@@ -35,7 +54,7 @@ class ObjectDB:
|
||||
self.fs.makedirs(path_info)
|
||||
|
||||
def get(self, hash_info):
|
||||
""" get raw object """
|
||||
"""get raw object"""
|
||||
return HashFile(
|
||||
# Prefer string path over PathInfo when possible due to performance
|
||||
self.hash_to_path(hash_info.value),
|
||||
@@ -80,14 +99,14 @@ class ObjectDB:
|
||||
raise
|
||||
|
||||
self.protect(cache_info)
|
||||
self.fs.repo.state.save(cache_info, self.fs, hash_info)
|
||||
self.state.save(cache_info, self.fs, hash_info)
|
||||
|
||||
callback = kwargs.get("download_callback")
|
||||
if callback:
|
||||
callback(1)
|
||||
|
||||
def hash_to_path_info(self, hash_):
|
||||
return self.fs.path_info / hash_[0:2] / hash_[2:]
|
||||
return self.path_info / hash_[0:2] / hash_[2:]
|
||||
|
||||
# Override to return path as a string instead of PathInfo for clouds
|
||||
# which support string paths (see local)
|
||||
@@ -143,12 +162,12 @@ class ObjectDB:
|
||||
def _list_paths(self, prefix=None, progress_callback=None):
|
||||
if prefix:
|
||||
if len(prefix) > 2:
|
||||
path_info = self.fs.path_info / prefix[:2] / prefix[2:]
|
||||
path_info = self.path_info / prefix[:2] / prefix[2:]
|
||||
else:
|
||||
path_info = self.fs.path_info / prefix[:2]
|
||||
path_info = self.path_info / prefix[:2]
|
||||
prefix = True
|
||||
else:
|
||||
path_info = self.fs.path_info
|
||||
path_info = self.path_info
|
||||
prefix = False
|
||||
if progress_callback:
|
||||
for file_info in self.fs.walk_files(path_info, prefix=prefix):
|
||||
@@ -289,7 +308,7 @@ class ObjectDB:
|
||||
with ThreadPoolExecutor(
|
||||
max_workers=jobs or self.fs.jobs
|
||||
) as executor:
|
||||
in_remote = executor.map(list_with_update, traverse_prefixes,)
|
||||
in_remote = executor.map(list_with_update, traverse_prefixes)
|
||||
yield from itertools.chain.from_iterable(in_remote)
|
||||
|
||||
def all(self, jobs=None, name=None):
|
||||
@@ -316,14 +335,22 @@ class ObjectDB:
|
||||
pass
|
||||
|
||||
def gc(self, used, jobs=None):
|
||||
used_hashes = set()
|
||||
for obj in used:
|
||||
used_hashes.add(obj.hash_info.value)
|
||||
if isinstance(obj, Tree):
|
||||
used_hashes.update(
|
||||
entry_obj.hash_info.value for _, entry_obj in obj
|
||||
)
|
||||
|
||||
removed = False
|
||||
# hashes must be sorted to ensure we always remove .dir files first
|
||||
for hash_ in sorted(
|
||||
self.all(jobs, str(self.fs.path_info)),
|
||||
self.all(jobs, str(self.path_info)),
|
||||
key=self.fs.is_dir_hash,
|
||||
reverse=True,
|
||||
):
|
||||
if hash_ in used:
|
||||
if hash_ in used_hashes:
|
||||
continue
|
||||
path_info = self.hash_to_path_info(hash_)
|
||||
if self.fs.is_dir_hash(hash_):
|
||||
@@ -399,8 +426,15 @@ class ObjectDB:
|
||||
# hashes_exist() (see ssh, local)
|
||||
assert self.fs.TRAVERSE_PREFIX_LEN >= 2
|
||||
|
||||
# During the tests, for ensuring that the traverse behavior
|
||||
# is working we turn on this option. It will ensure the
|
||||
# list_hashes_traverse() is called.
|
||||
always_traverse = getattr(self.fs, "_ALWAYS_TRAVERSE", False)
|
||||
|
||||
hashes = set(hashes)
|
||||
if len(hashes) == 1 or not self.fs.CAN_TRAVERSE:
|
||||
if (
|
||||
len(hashes) == 1 or not self.fs.CAN_TRAVERSE
|
||||
) and not always_traverse:
|
||||
remote_hashes = self.list_hashes_exists(hashes, jobs, name)
|
||||
return remote_hashes
|
||||
|
||||
@@ -418,7 +452,7 @@ class ObjectDB:
|
||||
)
|
||||
else:
|
||||
traverse_weight = traverse_pages
|
||||
if len(hashes) < traverse_weight:
|
||||
if len(hashes) < traverse_weight and not always_traverse:
|
||||
logger.debug(
|
||||
"Large remote ('{}' hashes < '{}' traverse weight), "
|
||||
"using object_exists for remaining hashes".format(
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import logging
|
||||
|
||||
from .base import ObjectDB
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GitObjectDB(ObjectDB):
|
||||
"""Dummy read-only ODB for uncached objects in external Git repos."""
|
||||
|
||||
def __init__(self, fs, path_info, **config):
|
||||
from dvc.fs.repo import RepoFileSystem
|
||||
|
||||
assert isinstance(fs, RepoFileSystem)
|
||||
super().__init__(fs, path_info)
|
||||
|
||||
def get(self, hash_info):
|
||||
raise NotImplementedError
|
||||
|
||||
def add(self, path_info, fs, hash_info, move=True, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def gc(self, used, jobs=None):
|
||||
raise NotImplementedError
|
||||
@@ -22,11 +22,11 @@ class LocalObjectDB(ObjectDB):
|
||||
CACHE_MODE = 0o444
|
||||
UNPACKED_DIR_SUFFIX = ".unpacked"
|
||||
|
||||
def __init__(self, fs):
|
||||
super().__init__(fs)
|
||||
self.cache_dir = fs.config.get("url")
|
||||
def __init__(self, fs, path_info, **config):
|
||||
super().__init__(fs, path_info, **config)
|
||||
self.cache_dir = path_info.fspath
|
||||
|
||||
shared = fs.config.get("shared")
|
||||
shared = config.get("shared")
|
||||
if shared:
|
||||
self._file_mode = 0o664
|
||||
self._dir_mode = 0o2775
|
||||
@@ -36,11 +36,11 @@ class LocalObjectDB(ObjectDB):
|
||||
|
||||
@property
|
||||
def cache_dir(self):
|
||||
return self.fs.path_info.fspath if self.fs.path_info else None
|
||||
return self.path_info.fspath if self.path_info else None
|
||||
|
||||
@cache_dir.setter
|
||||
def cache_dir(self, value):
|
||||
self.fs.path_info = PathInfo(value) if value else None
|
||||
self.path_info = PathInfo(value) if value else None
|
||||
|
||||
@cached_property
|
||||
def cache_path(self):
|
||||
@@ -81,13 +81,13 @@ class LocalObjectDB(ObjectDB):
|
||||
return ret
|
||||
|
||||
def _list_paths(self, prefix=None, progress_callback=None):
|
||||
assert self.fs.path_info is not None
|
||||
assert self.path_info is not None
|
||||
if prefix:
|
||||
path_info = self.fs.path_info / prefix[:2]
|
||||
path_info = self.path_info / prefix[:2]
|
||||
if not self.fs.exists(path_info):
|
||||
return
|
||||
else:
|
||||
path_info = self.fs.path_info
|
||||
path_info = self.path_info
|
||||
# NOTE: use utils.fs walk_files since fs.walk_files will not follow
|
||||
# symlinks
|
||||
if progress_callback:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user