mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1b36e36ca | ||
|
|
6ec66ab3de | ||
|
|
7c13897674 | ||
|
|
1db4637f2d | ||
|
|
ca3757b45b | ||
|
|
4e792ae61c | ||
|
|
6b13f76fdb | ||
|
|
53e9460c65 | ||
|
|
dafe6e991c | ||
|
|
09946847d2 | ||
|
|
6e5e62857b | ||
|
|
b7c6df0ece | ||
|
|
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 |
@@ -0,0 +1 @@
|
||||
* @iterative/dvc
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
--cov-report=xml --cov-report=term
|
||||
--all ${{ env.extra_test_args }}
|
||||
- name: upload coverage report
|
||||
uses: codecov/[email protected].0
|
||||
uses: codecov/[email protected].2
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Notification
|
||||
uses: rtCamp/action-slack-notify@v2.1.3
|
||||
uses: rtCamp/action-slack-notify@v2.2.0
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
|
||||
@@ -6,12 +6,12 @@ repos:
|
||||
- id: black
|
||||
language_version: python3
|
||||
repo: https://github.com/ambv/black
|
||||
rev: 21.5b1
|
||||
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
|
||||
@@ -26,8 +26,13 @@ repos:
|
||||
- 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
|
||||
|
||||
@@ -204,6 +204,10 @@ Comparison to related technologies
|
||||
#. `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
|
||||
============
|
||||
|
||||
|
||||
+1
-1
@@ -134,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc import 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")
|
||||
|
||||
+24
-24
@@ -14,6 +14,7 @@ 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:
|
||||
@@ -450,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,
|
||||
@@ -494,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
|
||||
|
||||
@@ -502,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)
|
||||
@@ -537,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,
|
||||
@@ -554,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
|
||||
|
||||
@@ -608,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
|
||||
|
||||
|
||||
@@ -662,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
|
||||
@@ -689,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
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dvc import 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(
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+4
-2
@@ -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):
|
||||
|
||||
+2
-4
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
@@ -188,6 +189,7 @@ SCHEMA = {
|
||||
"tenant_id": str,
|
||||
"client_id": str,
|
||||
"client_secret": str,
|
||||
"allow_anonymous_login": bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"oss": {
|
||||
|
||||
+53
-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:
|
||||
@@ -48,76 +57,88 @@ class DataCloud:
|
||||
|
||||
return get_remote(self.repo, name=name)
|
||||
|
||||
def push(self, cache, jobs=None, remote=None, show_checksums=False):
|
||||
def push(
|
||||
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):
|
||||
def pull(
|
||||
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))
|
||||
|
||||
+110
-46
@@ -1,9 +1,17 @@
|
||||
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 .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.objects.db.base import ObjectDB
|
||||
from dvc.objects.file import HashFile
|
||||
|
||||
|
||||
class RepoDependency(Dependency):
|
||||
PARAM_REPO = "repo"
|
||||
@@ -21,6 +29,7 @@ class RepoDependency(Dependency):
|
||||
|
||||
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_info):
|
||||
@@ -30,37 +39,12 @@ class RepoDependency(Dependency):
|
||||
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,
|
||||
).hash_info
|
||||
|
||||
def workspace_status(self):
|
||||
current = self._get_hash(locked=True)
|
||||
updated = self._get_hash(locked=False)
|
||||
current = self.get_obj(locked=True).hash_info
|
||||
updated = self.get_obj(locked=False).hash_info
|
||||
|
||||
if current != updated:
|
||||
return {str(self): "update available"}
|
||||
@@ -78,31 +62,21 @@ class RepoDependency(Dependency):
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
save(odb, obj, jobs=jobs)
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
if not isinstance(odb, GitObjectDB):
|
||||
fetch_from_odb(self.repo, odb, objs, jobs=jobs)
|
||||
|
||||
obj = self.get_obj()
|
||||
save(self.repo.odb.local, obj, jobs=jobs)
|
||||
checkout(
|
||||
to.path_info,
|
||||
to.fs,
|
||||
obj,
|
||||
odb,
|
||||
self.repo.odb.local,
|
||||
dvcignore=None,
|
||||
state=self.repo.state,
|
||||
)
|
||||
@@ -110,7 +84,6 @@ class RepoDependency(Dependency):
|
||||
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()
|
||||
|
||||
@@ -119,3 +92,94 @@ class RepoDependency(Dependency):
|
||||
# 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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -384,3 +384,11 @@ class CacheLinkError(DvcException):
|
||||
)
|
||||
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}'."
|
||||
)
|
||||
|
||||
+9
-3
@@ -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 = {
|
||||
@@ -106,4 +105,11 @@ def get_cloud_fs(repo, **kwargs):
|
||||
if cls == GDriveFileSystem and repo:
|
||||
remote_conf["gdrive_credentials_tmp_dir"] = repo.tmp_dir
|
||||
|
||||
return cls, remote_conf
|
||||
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
|
||||
|
||||
+33
-27
@@ -5,7 +5,8 @@ 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
|
||||
@@ -67,6 +68,19 @@ class AzureAuthError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
@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
|
||||
@@ -79,17 +93,21 @@ class AzureFileSystem(ObjectFSWrapper):
|
||||
"azure-identity": "azure.identity",
|
||||
}
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**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)
|
||||
bucket = _az_config().get("storage", "container_name", None)
|
||||
return f"azure://{bucket}"
|
||||
|
||||
if not self.path_info.bucket:
|
||||
container = self._az_config.get("storage", "container_name", None)
|
||||
url = f"azure://{container}"
|
||||
|
||||
self.path_info = self.PATH_CLS(url)
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
ops = infer_storage_options(urlpath)
|
||||
if "host" in ops:
|
||||
return {"bucket": ops["host"]}
|
||||
return {}
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
@@ -100,16 +118,16 @@ class AzureFileSystem(ObjectFSWrapper):
|
||||
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")
|
||||
@@ -129,7 +147,7 @@ class AzureFileSystem(ObjectFSWrapper):
|
||||
if (
|
||||
login_info["account_name"]
|
||||
and not any_secondary
|
||||
and not config.get("allow_anonymous_login")
|
||||
and not config.get("allow_anonymous_login", False)
|
||||
):
|
||||
login_info["credential"] = DefaultAzureCredential(
|
||||
exclude_interactive_browser_credential=False
|
||||
@@ -157,18 +175,6 @@ class AzureFileSystem(ObjectFSWrapper):
|
||||
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):
|
||||
|
||||
+21
-6
@@ -55,11 +55,17 @@ class BaseFileSystem:
|
||||
def __init__(self, **kwargs):
|
||||
self._check_requires(**kwargs)
|
||||
|
||||
self.path_info = None
|
||||
|
||||
self.jobs = kwargs.get("jobs") or self._JOBS
|
||||
self.hash_jobs = kwargs.get("checksum_jobs") or self.HASH_JOBS
|
||||
|
||||
@classmethod
|
||||
def _strip_protocol(cls, path: str):
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath): # pylint:disable=unused-argument
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def get_missing_deps(cls):
|
||||
import importlib
|
||||
@@ -164,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):
|
||||
@@ -229,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,
|
||||
|
||||
@@ -96,7 +96,8 @@ 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:
|
||||
@@ -105,7 +106,7 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
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):
|
||||
@@ -117,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))
|
||||
@@ -155,6 +156,8 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
|
||||
# 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
|
||||
@@ -169,9 +172,16 @@ class ObjectFSWrapper(FSSpecWrapper):
|
||||
and entry["name"].endswith("/")
|
||||
)
|
||||
|
||||
def find(self, path_info, detail=False):
|
||||
path = self._with_bucket(path_info)
|
||||
files = self.fs.find(path, detail=detail)
|
||||
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()
|
||||
|
||||
|
||||
+13
-4
@@ -142,6 +142,10 @@ class GDriveFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
return {"url": urlpath}
|
||||
|
||||
def _validate_config(self):
|
||||
# Validate Service Account configuration
|
||||
if (
|
||||
@@ -534,7 +538,7 @@ class GDriveFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
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()
|
||||
|
||||
@@ -614,22 +618,27 @@ class GDriveFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
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):
|
||||
|
||||
@@ -17,12 +17,6 @@ class GSFileSystem(ObjectFSWrapper):
|
||||
PARAM_CHECKSUM = "etag"
|
||||
DETAIL_FIELDS = frozenset(("etag", "size"))
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
url = config.get("url", "gs://")
|
||||
self.path_info = self.PATH_CLS(url)
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
login_info = {"consistency": None}
|
||||
login_info["project"] = config.get("projectname")
|
||||
|
||||
+11
-17
@@ -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
|
||||
@@ -68,24 +67,19 @@ class HDFSFileSystem(BaseFileSystem):
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+7
-12
@@ -2,6 +2,7 @@ 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
|
||||
|
||||
@@ -39,13 +40,7 @@ class HTTPFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
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,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
+1
-3
@@ -25,8 +25,6 @@ class LocalFileSystem(BaseFileSystem):
|
||||
|
||||
super().__init__(**config)
|
||||
self.fs = LocalFS()
|
||||
url = config.get("url")
|
||||
self.path_info = self.PATH_CLS(url) if url else None
|
||||
|
||||
@staticmethod
|
||||
def open(path_info, mode="r", encoding=None, **kwargs):
|
||||
@@ -105,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:
|
||||
|
||||
+1
-4
@@ -42,9 +42,6 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
url = config.get("url")
|
||||
self.path_info = self.PATH_CLS(url) if url else None
|
||||
|
||||
self.endpoint = config.get("oss_endpoint") or os.getenv("OSS_ENDPOINT")
|
||||
|
||||
self.key_id = (
|
||||
@@ -107,7 +104,7 @@ class OSSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
logger.debug(f"Removing oss://{path_info}")
|
||||
self._get_bucket(path_info.bucket).delete_object(path_info.path)
|
||||
|
||||
def _upload_fobj(self, fobj, to_info):
|
||||
def _upload_fobj(self, fobj, to_info, **kwargs):
|
||||
self._get_bucket(to_info.bucket).put_object(to_info.path, fobj)
|
||||
|
||||
def _upload(
|
||||
|
||||
@@ -64,6 +64,12 @@ class RepoFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self._dvcfss[repo.root_dir] = DvcFileSystem(repo=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.
|
||||
|
||||
|
||||
@@ -29,12 +29,6 @@ class BaseS3FileSystem(ObjectFSWrapper):
|
||||
"grant_write_acp": "GrantWriteACP",
|
||||
}
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
url = config.get("url", "s3://")
|
||||
self.path_info = self.PATH_CLS(url)
|
||||
|
||||
_TRANSFER_CONFIG_ALIASES = {
|
||||
"max_queue_size": "max_io_queue",
|
||||
"max_concurrent_requests": "max_concurrency",
|
||||
|
||||
+26
-36
@@ -5,7 +5,6 @@ import os
|
||||
import shutil
|
||||
import threading
|
||||
from contextlib import closing, contextmanager
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import first, memoize, silent, wrap_with
|
||||
|
||||
@@ -48,34 +47,19 @@ class SSHFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
url = config.get("url")
|
||||
if url:
|
||||
parsed = urlparse(url)
|
||||
user_ssh_config = self._load_user_ssh_config(parsed.hostname)
|
||||
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): # pylint:disable=abstract-method
|
||||
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): # pylint:disable=abstract-method
|
||||
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
|
||||
|
||||
@@ -247,7 +237,7 @@ class SSHFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
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)
|
||||
|
||||
+64
-233
@@ -1,270 +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, **config):
|
||||
# Call BaseFileSystem constructor
|
||||
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)
|
||||
|
||||
# Whether to ask for password if it is not set
|
||||
self.ask_password = config.get("ask_password", False)
|
||||
|
||||
# Use token for webdav auth
|
||||
self.token = config.get("token", None)
|
||||
|
||||
# Path to certificate
|
||||
self.cert_path = config.get("cert_path", None)
|
||||
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
|
||||
# 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,
|
||||
@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,
|
||||
}
|
||||
|
||||
client = Client(options)
|
||||
def _prepare_credentials(self, **config):
|
||||
user = config.get("user", None)
|
||||
password = config.get("password", None)
|
||||
|
||||
# Check whether client options are valid
|
||||
if not client.valid():
|
||||
raise ConfigError(
|
||||
f"Configuration for WebDAV {self.hostname} is invalid."
|
||||
)
|
||||
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)
|
||||
|
||||
# Check whether connection is valid (root should always exist)
|
||||
if not client.check(self.path_info.path):
|
||||
raise WebDAVConnectionError(self.hostname)
|
||||
return {"headers": headers, "auth": auth}
|
||||
|
||||
return client
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self):
|
||||
from webdav4.fsspec import WebdavFileSystem
|
||||
|
||||
def open(self, path_info, mode="r", encoding=None, **kwargs):
|
||||
from webdav3.exceptions import RemoteResourceNotFound
|
||||
return WebdavFileSystem(**self.fs_args)
|
||||
|
||||
assert mode in {"r", "rt", "rb"}
|
||||
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
|
||||
)
|
||||
|
||||
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) -> bool:
|
||||
# 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
|
||||
+14
-14
@@ -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
|
||||
|
||||
@@ -40,23 +39,24 @@ class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
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):
|
||||
@@ -141,7 +141,7 @@ class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
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 {}
|
||||
|
||||
+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, **config):
|
||||
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, **config)
|
||||
return LocalObjectDB(fs, path_info, **config)
|
||||
|
||||
if fs.scheme == Schemes.SSH:
|
||||
return SSHObjectDB(fs, **config)
|
||||
return SSHObjectDB(fs, path_info, **config)
|
||||
|
||||
if fs.scheme == Schemes.GDRIVE:
|
||||
return GDriveObjectDB(fs, **config)
|
||||
return GDriveObjectDB(fs, path_info, **config)
|
||||
|
||||
return ObjectDB(fs, **config)
|
||||
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
|
||||
|
||||
cls, config = get_cloud_fs(repo, **settings)
|
||||
return get_odb(cls(**config), state=repo.state, **config)
|
||||
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()
|
||||
|
||||
+34
-7
@@ -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, **config):
|
||||
def __init__(self, fs, path_info, **config):
|
||||
from dvc.state import StateNoop
|
||||
|
||||
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)
|
||||
@@ -87,7 +106,7 @@ class ObjectDB:
|
||||
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):
|
||||
@@ -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_):
|
||||
|
||||
@@ -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,9 +22,9 @@ class LocalObjectDB(ObjectDB):
|
||||
CACHE_MODE = 0o444
|
||||
UNPACKED_DIR_SUFFIX = ".unpacked"
|
||||
|
||||
def __init__(self, fs, **config):
|
||||
super().__init__(fs, **config)
|
||||
self.cache_dir = config.get("url")
|
||||
def __init__(self, fs, path_info, **config):
|
||||
super().__init__(fs, path_info, **config)
|
||||
self.cache_dir = path_info.fspath
|
||||
|
||||
shared = config.get("shared")
|
||||
if shared:
|
||||
@@ -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:
|
||||
|
||||
@@ -75,10 +75,10 @@ class SSHObjectDB(ObjectDB):
|
||||
|
||||
def _list_paths(self, prefix=None, progress_callback=None):
|
||||
if prefix:
|
||||
root = posixpath.join(self.fs.path_info.path, prefix[:2])
|
||||
root = posixpath.join(self.path_info.path, prefix[:2])
|
||||
else:
|
||||
root = self.fs.path_info.path
|
||||
with self.fs.ssh(self.fs.path_info) as ssh:
|
||||
root = self.path_info.path
|
||||
with self.fs.ssh(self.path_info) as ssh:
|
||||
if prefix and not ssh.exists(root):
|
||||
return
|
||||
# If we simply return an iterator then with above closes instantly
|
||||
|
||||
+23
-1
@@ -1,17 +1,30 @@
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from .errors import ObjectFormatError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.types import DvcPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HashFile:
|
||||
def __init__(self, path_info, fs, hash_info):
|
||||
def __init__(
|
||||
self,
|
||||
path_info: Optional["DvcPath"],
|
||||
fs: Optional["BaseFileSystem"],
|
||||
hash_info: "HashInfo",
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
self.path_info = path_info
|
||||
self.fs = fs
|
||||
self.hash_info = hash_info
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
@@ -37,6 +50,15 @@ class HashFile:
|
||||
and self.hash_info == other.hash_info
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(
|
||||
(
|
||||
self.hash_info,
|
||||
self.path_info,
|
||||
self.fs.scheme if self.fs else None,
|
||||
)
|
||||
)
|
||||
|
||||
def check(self, odb, check_hash=True):
|
||||
from .stage import get_file_hash
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ def _upload_file(path_info, fs, odb):
|
||||
from dvc.utils import tmp_fname
|
||||
from dvc.utils.stream import HashedStreamReader
|
||||
|
||||
tmp_info = odb.fs.path_info / tmp_fname()
|
||||
tmp_info = odb.path_info / tmp_fname()
|
||||
with fs.open(path_info, mode="rb", chunk_size=fs.CHUNK_SIZE) as stream:
|
||||
stream = HashedStreamReader(stream)
|
||||
odb.fs.upload_fobj(
|
||||
stream, tmp_info, desc=path_info.name, total=fs.getsize(path_info)
|
||||
stream, tmp_info, desc=path_info.name, size=fs.getsize(path_info)
|
||||
)
|
||||
|
||||
obj = HashFile(tmp_info, odb.fs, stream.hash_info)
|
||||
@@ -52,11 +52,6 @@ def get_file_hash(path_info, fs, name, state=None):
|
||||
if hash_info:
|
||||
return hash_info
|
||||
|
||||
if not fs.exists(path_info):
|
||||
raise FileNotFoundError(
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), path_info
|
||||
)
|
||||
|
||||
hash_info = _get_file_hash(path_info, fs, name)
|
||||
|
||||
if state:
|
||||
|
||||
+30
-5
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
import posixpath
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
@@ -8,6 +9,9 @@ from .errors import ObjectFormatError
|
||||
from .file import HashFile
|
||||
from .stage import get_file_hash
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .db.base import ObjectDB
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -115,24 +119,45 @@ class Tree(HashFile):
|
||||
tree = cls.from_list(raw)
|
||||
tree.path_info = obj.path_info
|
||||
tree.fs = obj.fs
|
||||
for _, entry_obj in tree:
|
||||
entry_obj.fs = obj.fs
|
||||
tree.hash_info = hash_info
|
||||
|
||||
return tree
|
||||
|
||||
def filter(self, odb, prefix):
|
||||
def filter(
|
||||
self, odb: "ObjectDB", prefix: Tuple[str], copy: bool = False
|
||||
) -> Optional[HashFile]:
|
||||
"""Return filter object(s) for this tree.
|
||||
|
||||
If copy is True, returned object will be a Tree containing
|
||||
filtered entries, but with hash_info copied from the original tree.
|
||||
|
||||
If copy is False, returned object will be a raw HashFile or Tree with
|
||||
newly computed hash_info for the filtered object.
|
||||
"""
|
||||
obj = self._dict.get(prefix)
|
||||
if obj:
|
||||
if copy:
|
||||
tree = Tree(self.path_info, self.fs, self.hash_info)
|
||||
tree.add(prefix, obj)
|
||||
return tree
|
||||
return obj
|
||||
|
||||
depth = len(prefix)
|
||||
tree = Tree(None, None, None)
|
||||
if copy:
|
||||
tree = Tree(self.path_info, self.fs, self.hash_info)
|
||||
depth = 0
|
||||
else:
|
||||
tree = Tree(None, None, None)
|
||||
depth = len(prefix)
|
||||
try:
|
||||
for key, obj in self.trie.items(prefix):
|
||||
tree.add(key[depth:], obj)
|
||||
except KeyError:
|
||||
return None
|
||||
tree.digest()
|
||||
odb.add(tree.path_info, tree.fs, tree.hash_info)
|
||||
if not copy:
|
||||
tree.digest()
|
||||
odb.add(tree.path_info, tree.fs, tree.hash_info)
|
||||
return tree
|
||||
|
||||
|
||||
|
||||
+57
-72
@@ -2,7 +2,7 @@ import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set, Type
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import collecting, project
|
||||
@@ -24,14 +24,18 @@ from .fs.local import LocalFileSystem
|
||||
from .fs.s3 import S3FileSystem
|
||||
from .hash_info import HashInfo
|
||||
from .istextfile import istextfile
|
||||
from .objects import Tree
|
||||
from .objects import save as osave
|
||||
from .objects.db import NamedCache
|
||||
from .objects.errors import ObjectFormatError
|
||||
from .objects.stage import stage as ostage
|
||||
from .scheme import Schemes
|
||||
from .utils import relpath
|
||||
from .utils.fs import path_isin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .objects.db.base import ObjectDB
|
||||
from .objects.file import HashFile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -41,18 +45,24 @@ CHECKSUM_SCHEMA = Any(
|
||||
And(Any(str, And(int, Coerce(str))), Length(min=3), Lower),
|
||||
)
|
||||
|
||||
CASE_SENSITIVE_CHECKSUM_SCHEMA = Any(
|
||||
None,
|
||||
And(str, Length(max=0), SetTo(None)),
|
||||
And(Any(str, And(int, Coerce(str))), Length(min=3)),
|
||||
)
|
||||
|
||||
# NOTE: currently there are only 3 possible checksum names:
|
||||
#
|
||||
# 1) md5 (LOCAL, SSH);
|
||||
# 2) etag (S3);
|
||||
# 2) etag (S3, GS, OSS, AZURE, HTTP);
|
||||
# 3) checksum (HDFS);
|
||||
#
|
||||
# so when a few types of outputs share the same name, we only need
|
||||
# specify it once.
|
||||
CHECKSUMS_SCHEMA = {
|
||||
LocalFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3FileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
HDFSFileSystem.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3FileSystem.PARAM_CHECKSUM: CASE_SENSITIVE_CHECKSUM_SCHEMA,
|
||||
}
|
||||
|
||||
|
||||
@@ -279,9 +289,8 @@ class Output:
|
||||
):
|
||||
self.repo = stage.repo if stage else None
|
||||
|
||||
fs_cls, fs_config = get_cloud_fs(self.repo, url=path)
|
||||
fs_cls, fs_config, path_info = get_cloud_fs(self.repo, url=path)
|
||||
self.fs = fs_cls(**fs_config)
|
||||
path_info = self.fs.path_info
|
||||
|
||||
if (
|
||||
self.fs.scheme == "local"
|
||||
@@ -320,6 +329,8 @@ class Output:
|
||||
self.obj = None
|
||||
self.isexec = False if self.IS_DEPENDENCY else isexec
|
||||
|
||||
self.def_remote = None
|
||||
|
||||
def _parse_path(self, fs, path_info):
|
||||
if fs.scheme != "local":
|
||||
return path_info
|
||||
@@ -648,7 +659,7 @@ class Output:
|
||||
def download(self, to, jobs=None):
|
||||
self.fs.download(self.path_info, to.path_info, jobs=jobs)
|
||||
|
||||
def get_obj(self, filter_info=None):
|
||||
def get_obj(self, filter_info=None, **kwargs):
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
elif self.hash_info:
|
||||
@@ -661,7 +672,7 @@ class Output:
|
||||
|
||||
if filter_info and filter_info != self.path_info:
|
||||
prefix = filter_info.relative_to(self.path_info).parts
|
||||
obj = obj.filter(self.odb, prefix)
|
||||
obj = obj.filter(self.odb, prefix, **kwargs)
|
||||
|
||||
return obj
|
||||
|
||||
@@ -741,9 +752,8 @@ class Output:
|
||||
if odb is None:
|
||||
odb = self.odb
|
||||
|
||||
cls, config = get_cloud_fs(self.repo, url=source)
|
||||
cls, config, from_info = get_cloud_fs(self.repo, url=source)
|
||||
from_fs = cls(**config)
|
||||
from_info = from_fs.path_info
|
||||
|
||||
# When running import-url --to-remote / add --to-remote/-o ... we
|
||||
# assume that it is unlikely that the odb will contain majority of the
|
||||
@@ -788,18 +798,14 @@ class Output:
|
||||
self.odb.unprotect(self.path_info)
|
||||
|
||||
def get_dir_cache(self, **kwargs):
|
||||
|
||||
if not self.is_dir_checksum:
|
||||
raise DvcException("cannot get dir cache for file checksum")
|
||||
|
||||
obj = self.odb.get(self.hash_info)
|
||||
try:
|
||||
objects.check(self.odb, self.odb.get(self.hash_info))
|
||||
objects.check(self.odb, obj)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
self.repo.cloud.pull(
|
||||
NamedCache.make("local", self.hash_info.value, str(self)),
|
||||
show_checksums=False,
|
||||
**kwargs,
|
||||
)
|
||||
self.repo.cloud.pull([obj], show_checksums=False, **kwargs)
|
||||
|
||||
try:
|
||||
self.obj = objects.load(self.odb, self.hash_info)
|
||||
@@ -810,27 +816,8 @@ class Output:
|
||||
|
||||
def collect_used_dir_cache(
|
||||
self, remote=None, force=False, jobs=None, filter_info=None
|
||||
):
|
||||
"""Get a list of `info`s related to the given directory.
|
||||
|
||||
- Pull the directory entry from the remote cache if it was changed.
|
||||
|
||||
Example:
|
||||
|
||||
Given the following commands:
|
||||
|
||||
$ echo "foo" > directory/foo
|
||||
$ echo "bar" > directory/bar
|
||||
$ dvc add directory
|
||||
|
||||
It will return a NamedCache like:
|
||||
|
||||
nc = NamedCache()
|
||||
nc.add(self.scheme, 'c157a79031e1', 'directory/foo')
|
||||
nc.add(self.scheme, 'd3b07384d113', 'directory/bar')
|
||||
"""
|
||||
|
||||
cache = NamedCache()
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]:
|
||||
"""Fetch dir cache and return used objects for this out."""
|
||||
|
||||
try:
|
||||
self.get_dir_cache(jobs=jobs, remote=remote)
|
||||
@@ -850,38 +837,22 @@ class Output:
|
||||
"unable to fully collect used cache"
|
||||
" without cache for directory '{}'".format(self)
|
||||
)
|
||||
return cache
|
||||
return {}
|
||||
|
||||
path = str(self.path_info)
|
||||
filter_path = str(filter_info) if filter_info else None
|
||||
for entry_key, entry_obj in self.obj:
|
||||
entry_path = os.path.join(path, *entry_key)
|
||||
if (
|
||||
not filter_path
|
||||
or entry_path == filter_path
|
||||
or entry_path.startswith(filter_path + os.sep)
|
||||
):
|
||||
cache.add(self.scheme, entry_obj.hash_info.value, entry_path)
|
||||
obj = self.get_obj(filter_info=filter_info, copy=True)
|
||||
self._set_obj_names(obj)
|
||||
return {None: {obj}}
|
||||
|
||||
return cache
|
||||
|
||||
def get_used_cache(self, **kwargs):
|
||||
"""Get a dumpd of the given `out`, with an entry including the branch.
|
||||
|
||||
The `used_cache` of an output is no more than its `info`.
|
||||
|
||||
In case that the given output is a directory, it will also
|
||||
include the `info` of its files.
|
||||
"""
|
||||
def get_used_objs(
|
||||
self, **kwargs
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]:
|
||||
"""Return filtered set of used objects for this out."""
|
||||
|
||||
if not self.use_cache:
|
||||
return NamedCache()
|
||||
return {}
|
||||
|
||||
if self.stage.is_repo_import:
|
||||
cache = NamedCache()
|
||||
(dep,) = self.stage.deps
|
||||
cache.external[dep.repo_pair].add(dep.def_path)
|
||||
return cache
|
||||
return self.get_used_external(**kwargs)
|
||||
|
||||
if not self.hash_info:
|
||||
msg = (
|
||||
@@ -900,18 +871,32 @@ class Output:
|
||||
)
|
||||
)
|
||||
logger.warning(msg)
|
||||
return NamedCache()
|
||||
return {}
|
||||
|
||||
ret = NamedCache.make(self.scheme, self.hash_info.value, str(self))
|
||||
if self.is_dir_checksum:
|
||||
return self.collect_used_dir_cache(**kwargs)
|
||||
|
||||
if not self.is_dir_checksum:
|
||||
return ret
|
||||
obj = self.get_obj(filter_info=kwargs.get("filter_info"))
|
||||
if not obj:
|
||||
obj = self.odb.get(self.hash_info)
|
||||
self._set_obj_names(obj)
|
||||
|
||||
ret.add_child_cache(
|
||||
self.hash_info.value, self.collect_used_dir_cache(**kwargs)
|
||||
)
|
||||
return {None: {obj}}
|
||||
|
||||
return ret
|
||||
def _set_obj_names(self, obj):
|
||||
obj.name = str(self)
|
||||
if isinstance(obj, Tree):
|
||||
for key, entry_obj in obj:
|
||||
entry_obj.name = os.path.join(str(self), *key)
|
||||
|
||||
def get_used_external(
|
||||
self, **kwargs
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]:
|
||||
if not self.use_cache or not self.stage.is_repo_import:
|
||||
return {}
|
||||
|
||||
(dep,) = self.stage.deps
|
||||
return dep.get_used_objs()
|
||||
|
||||
def _validate_output_path(self, path, stage=None):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ from threading import RLock
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from dvc.env import DVC_IGNORE_ISATTY
|
||||
from dvc.utils import env2bool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -77,7 +78,7 @@ class Tqdm(tqdm):
|
||||
# auto-disable based on TTY
|
||||
if (
|
||||
not disable
|
||||
and not env2bool("DVC_IGNORE_ISATTY")
|
||||
and not env2bool(DVC_IGNORE_ISATTY)
|
||||
and hasattr(file, "isatty")
|
||||
):
|
||||
disable = not file.isatty()
|
||||
|
||||
@@ -4,8 +4,8 @@ from .local import LocalRemote
|
||||
|
||||
|
||||
def get_remote(repo, **kwargs):
|
||||
cls, config = get_cloud_fs(repo, **kwargs)
|
||||
cls, config, path_info = get_cloud_fs(repo, **kwargs)
|
||||
fs = cls(**config)
|
||||
if fs.scheme == "local":
|
||||
return LocalRemote(fs, repo.tmp_dir, **config)
|
||||
return Remote(fs, repo.tmp_dir, **config)
|
||||
return LocalRemote(fs, path_info, repo.tmp_dir, **config)
|
||||
return Remote(fs, path_info, repo.tmp_dir, **config)
|
||||
|
||||
+92
-49
@@ -8,9 +8,10 @@ from functools import partial, wraps
|
||||
|
||||
from dvc.exceptions import DownloadError, UploadError
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.objects.tree import Tree
|
||||
|
||||
from ..progress import Tqdm
|
||||
from .index import RemoteIndex, RemoteIndexNoop
|
||||
from .index import RemoteIndex
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -67,25 +68,39 @@ class Remote:
|
||||
|
||||
INDEX_CLS = RemoteIndex
|
||||
|
||||
def __init__(self, fs, tmp_dir, **config):
|
||||
def __init__(self, fs, path_info, tmp_dir, **config):
|
||||
from dvc.objects.db import get_odb
|
||||
|
||||
self.fs = fs
|
||||
self.odb = get_odb(self.fs, **config)
|
||||
config["tmp_dir"] = tmp_dir
|
||||
self.odb = get_odb(self.fs, path_info, **config)
|
||||
|
||||
url = config.get("url")
|
||||
if url:
|
||||
index_name = hashlib.sha256(url.encode("utf-8")).hexdigest()
|
||||
self.index = self.INDEX_CLS(
|
||||
tmp_dir, index_name, dir_suffix=self.fs.CHECKSUM_DIR_SUFFIX
|
||||
)
|
||||
else:
|
||||
self.index = RemoteIndexNoop()
|
||||
url = getattr(path_info, "fspath", path_info.url)
|
||||
index_name = hashlib.sha256(url.encode("utf-8")).hexdigest()
|
||||
self.index = self.INDEX_CLS(
|
||||
tmp_dir, index_name, dir_suffix=self.fs.CHECKSUM_DIR_SUFFIX
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_name}: '{path_info}'".format(
|
||||
class_name=type(self).__name__,
|
||||
path_info=self.fs.path_info or "No path",
|
||||
path_info=self.odb.path_info or "No path",
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.odb == other.odb
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.odb)
|
||||
|
||||
@classmethod
|
||||
def from_odb(cls, odb):
|
||||
config = odb.config
|
||||
return cls(
|
||||
odb.fs,
|
||||
odb.path_info,
|
||||
config.pop("tmp_dir"),
|
||||
**config,
|
||||
)
|
||||
|
||||
@index_locked
|
||||
@@ -101,7 +116,7 @@ class Remote:
|
||||
def status(
|
||||
self,
|
||||
cache,
|
||||
named_cache,
|
||||
objs,
|
||||
jobs=None,
|
||||
show_checksums=False,
|
||||
download=False,
|
||||
@@ -110,7 +125,7 @@ class Remote:
|
||||
# Return flattened dict containing all status info
|
||||
dir_status, file_status, _ = self._status(
|
||||
cache,
|
||||
named_cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
show_checksums=show_checksums,
|
||||
download=download,
|
||||
@@ -129,10 +144,10 @@ class Remote:
|
||||
|
||||
return indexed_hashes + self.odb.hashes_exist(list(hashes), **kwargs)
|
||||
|
||||
def _status(
|
||||
def _status( # pylint: disable=unused-argument
|
||||
self,
|
||||
cache,
|
||||
named_cache,
|
||||
objs,
|
||||
jobs=None,
|
||||
show_checksums=False,
|
||||
download=False,
|
||||
@@ -145,8 +160,18 @@ class Remote:
|
||||
{dir_hash: set(file_hash, ...)} which can be used to map
|
||||
a .dir file to its file contents.
|
||||
"""
|
||||
logger.debug(f"Preparing to collect status from {self.fs.path_info}")
|
||||
md5s = set(named_cache.scheme_keys(cache.fs.scheme))
|
||||
|
||||
logger.debug(f"Preparing to collect status from {self.odb.path_info}")
|
||||
md5s = set()
|
||||
dir_objs = {}
|
||||
for obj in objs:
|
||||
if obj.fs.scheme != cache.fs.scheme:
|
||||
continue
|
||||
md5s.add(obj.hash_info.value)
|
||||
if isinstance(obj, Tree):
|
||||
dir_objs[obj.hash_info.value] = obj
|
||||
for _, entry_obj in obj:
|
||||
md5s.add(entry_obj.hash_info.value)
|
||||
|
||||
logger.debug("Collecting information from local cache...")
|
||||
local_exists = frozenset(
|
||||
@@ -162,21 +187,18 @@ class Remote:
|
||||
else:
|
||||
logger.debug("Collecting information from remote cache...")
|
||||
remote_exists = set()
|
||||
dir_md5s = set(named_cache.dir_keys(cache.fs.scheme))
|
||||
if dir_md5s:
|
||||
remote_exists.update(
|
||||
self._indexed_dir_hashes(cache, named_cache, dir_md5s)
|
||||
)
|
||||
if dir_objs:
|
||||
remote_exists.update(self._indexed_dir_hashes(dir_objs))
|
||||
md5s.difference_update(remote_exists)
|
||||
if md5s:
|
||||
remote_exists.update(
|
||||
self.hashes_exist(
|
||||
md5s, jobs=jobs, name=str(self.fs.path_info)
|
||||
md5s, jobs=jobs, name=str(self.odb.path_info)
|
||||
)
|
||||
)
|
||||
return self._make_status(
|
||||
cache,
|
||||
named_cache,
|
||||
objs,
|
||||
show_checksums,
|
||||
local_exists,
|
||||
remote_exists,
|
||||
@@ -186,29 +208,36 @@ class Remote:
|
||||
def _make_status(
|
||||
self,
|
||||
cache,
|
||||
named_cache,
|
||||
objs,
|
||||
show_checksums,
|
||||
local_exists,
|
||||
remote_exists,
|
||||
log_missing,
|
||||
):
|
||||
def make_names(hash_, names):
|
||||
return {"name": hash_ if show_checksums else " ".join(names)}
|
||||
def make_names(obj):
|
||||
if show_checksums or obj.name is None:
|
||||
name = obj.hash_info.value
|
||||
else:
|
||||
name = obj.name
|
||||
return {"name": name}
|
||||
|
||||
dir_status = {}
|
||||
file_status = {}
|
||||
dir_contents = {}
|
||||
for hash_, item in named_cache[cache.fs.scheme].items():
|
||||
if item.children:
|
||||
dir_status[hash_] = make_names(hash_, item.names)
|
||||
for obj in objs:
|
||||
if obj.fs.scheme != cache.fs.scheme:
|
||||
continue
|
||||
|
||||
hash_ = obj.hash_info.value
|
||||
if isinstance(obj, Tree):
|
||||
dir_status[hash_] = make_names(obj)
|
||||
dir_contents[hash_] = set()
|
||||
for child_hash, child in item.children.items():
|
||||
file_status[child_hash] = make_names(
|
||||
child_hash, child.names
|
||||
)
|
||||
for _, child_obj in obj:
|
||||
child_hash = child_obj.hash_info.value
|
||||
file_status[child_hash] = make_names(child_obj)
|
||||
dir_contents[hash_].add(child_hash)
|
||||
else:
|
||||
file_status[hash_] = make_names(hash_, item.names)
|
||||
file_status[hash_] = make_names(obj)
|
||||
|
||||
self._fill_statuses(dir_status, local_exists, remote_exists)
|
||||
self._fill_statuses(file_status, local_exists, remote_exists)
|
||||
@@ -218,9 +247,10 @@ class Remote:
|
||||
|
||||
return dir_status, file_status, dir_contents
|
||||
|
||||
def _indexed_dir_hashes(self, cache, named_cache, dir_md5s):
|
||||
def _indexed_dir_hashes(self, dir_objs):
|
||||
# Validate our index by verifying all indexed .dir hashes
|
||||
# still exist on the remote
|
||||
dir_md5s = set(dir_objs.keys())
|
||||
indexed_dirs = set(self.index.dir_hashes())
|
||||
indexed_dir_exists = set()
|
||||
if indexed_dirs:
|
||||
@@ -242,9 +272,9 @@ class Remote:
|
||||
# If .dir hash exists on the remote, assume directory contents
|
||||
# still exists on the remote
|
||||
for dir_hash in dir_exists:
|
||||
file_hashes = list(
|
||||
named_cache.child_keys(cache.fs.scheme, dir_hash)
|
||||
)
|
||||
file_hashes = [
|
||||
obj.hash_info.value for _, obj in dir_objs[dir_hash]
|
||||
]
|
||||
if dir_hash not in self.index:
|
||||
logger.debug(
|
||||
"Indexing new .dir '{}' with '{}' nested files".format(
|
||||
@@ -294,7 +324,7 @@ class Remote:
|
||||
def _process(
|
||||
self,
|
||||
cache,
|
||||
named_cache,
|
||||
objs,
|
||||
jobs=None,
|
||||
show_checksums=False,
|
||||
download=False,
|
||||
@@ -302,7 +332,7 @@ class Remote:
|
||||
logger.debug(
|
||||
"Preparing to {} '{}'".format(
|
||||
"download data from" if download else "upload data to",
|
||||
self.fs.path_info,
|
||||
self.odb.path_info,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -320,7 +350,7 @@ class Remote:
|
||||
|
||||
dir_status, file_status, dir_contents = self._status(
|
||||
cache,
|
||||
named_cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
show_checksums=show_checksums,
|
||||
download=download,
|
||||
@@ -460,17 +490,20 @@ class Remote:
|
||||
self.index.update([dir_hash], file_hashes)
|
||||
|
||||
@index_locked
|
||||
def push(self, cache, named_cache, jobs=None, show_checksums=False):
|
||||
def push(self, cache, objs, jobs=None, show_checksums=False):
|
||||
ret = self._process(
|
||||
cache,
|
||||
named_cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
show_checksums=show_checksums,
|
||||
download=False,
|
||||
)
|
||||
|
||||
if self.fs.scheme == "local":
|
||||
for checksum in named_cache.scheme_keys("local"):
|
||||
for obj in objs:
|
||||
if obj.fs.scheme != "local":
|
||||
continue
|
||||
checksum = obj.hash_info.value
|
||||
cache_file = self.odb.hash_to_path_info(checksum)
|
||||
if self.fs.exists(cache_file):
|
||||
hash_info = HashInfo(self.fs.PARAM_CHECKSUM, checksum)
|
||||
@@ -480,17 +513,19 @@ class Remote:
|
||||
return ret
|
||||
|
||||
@index_locked
|
||||
def pull(self, cache, named_cache, jobs=None, show_checksums=False):
|
||||
def pull(self, cache, objs, jobs=None, show_checksums=False):
|
||||
ret = self._process(
|
||||
cache,
|
||||
named_cache,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
show_checksums=show_checksums,
|
||||
download=True,
|
||||
)
|
||||
|
||||
if not self.odb.verify:
|
||||
for checksum in named_cache.scheme_keys("local"):
|
||||
|
||||
def save_state(obj):
|
||||
checksum = obj.hash_info.value
|
||||
cache_file = cache.hash_to_path_info(checksum)
|
||||
if cache.fs.exists(cache_file):
|
||||
# We can safely save here, as existing corrupted files
|
||||
@@ -501,6 +536,14 @@ class Remote:
|
||||
cache.state.save(cache_file, cache.fs, hash_info)
|
||||
cache.protect(cache_file)
|
||||
|
||||
for obj in objs:
|
||||
if obj.fs.scheme != "local":
|
||||
continue
|
||||
save_state(obj)
|
||||
if isinstance(obj, Tree):
|
||||
for _, entry_obj in obj:
|
||||
save_state(entry_obj)
|
||||
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
|
||||
+25
-14
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
@@ -340,7 +341,7 @@ class Repo:
|
||||
existing_stages = self.stages if old_stages is None else old_stages
|
||||
build_graph(existing_stages + new_stages)
|
||||
|
||||
def used_cache(
|
||||
def used_objs(
|
||||
self,
|
||||
targets=None,
|
||||
all_branches=False,
|
||||
@@ -366,13 +367,22 @@ class Repo:
|
||||
the scope.
|
||||
|
||||
Returns:
|
||||
A dictionary with Schemes (representing output's location) mapped
|
||||
to items containing the output's `dumpd` names and the output's
|
||||
children (if the given output is a directory).
|
||||
A dict mapping (remote) ODB instances to sets of objects that
|
||||
belong to each ODB. If the ODB instance is None, the objects
|
||||
are naive and do not belong to a specific remote ODB.
|
||||
"""
|
||||
from dvc.objects.db import NamedCache
|
||||
used = defaultdict(set)
|
||||
|
||||
cache = NamedCache()
|
||||
def _add_suffix(objs, suffix):
|
||||
from dvc.objects.tree import Tree
|
||||
|
||||
for obj in objs:
|
||||
if obj.name is not None:
|
||||
obj.name += suffix
|
||||
if isinstance(obj, Tree):
|
||||
for _, entry_obj in obj:
|
||||
if entry_obj.name is not None:
|
||||
entry_obj.name += suffix
|
||||
|
||||
for branch in self.brancher(
|
||||
revs=revs,
|
||||
@@ -390,23 +400,24 @@ class Repo:
|
||||
for target in targets
|
||||
)
|
||||
|
||||
suffix = f"({branch})" if branch else ""
|
||||
for stage, filter_info in pairs:
|
||||
used_cache = stage.get_used_cache(
|
||||
for odb, objs in stage.get_used_objs(
|
||||
remote=remote,
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
filter_info=filter_info,
|
||||
)
|
||||
cache.update(used_cache, suffix=suffix)
|
||||
).items():
|
||||
if branch:
|
||||
_add_suffix(objs, f" ({branch})")
|
||||
used[odb].update(objs)
|
||||
|
||||
if used_run_cache:
|
||||
used_cache = self.stage_cache.get_used_cache(
|
||||
for odb, objs in self.stage_cache.get_used_objs(
|
||||
used_run_cache, remote=remote, force=force, jobs=jobs
|
||||
)
|
||||
cache.update(used_cache)
|
||||
).items():
|
||||
used[odb].update(objs)
|
||||
|
||||
return cache
|
||||
return used
|
||||
|
||||
@cached_property
|
||||
def outs_trie(self):
|
||||
|
||||
+74
-44
@@ -1,9 +1,9 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import DownloadError, NoOutputOrStageError
|
||||
from dvc.exceptions import DownloadError
|
||||
|
||||
from ..scheme import Schemes
|
||||
from . import locked
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -36,11 +36,12 @@ def fetch(
|
||||
config.NoRemoteError: thrown when downloading only local files and no
|
||||
remote is configured
|
||||
"""
|
||||
from dvc.objects.db.git import GitObjectDB
|
||||
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
|
||||
used = self.used_cache(
|
||||
used = self.used_objs(
|
||||
targets,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
@@ -59,17 +60,39 @@ def fetch(
|
||||
try:
|
||||
if run_cache:
|
||||
self.stage_cache.pull(remote)
|
||||
downloaded += self.cloud.pull(
|
||||
used, jobs, remote=remote, show_checksums=show_checksums
|
||||
)
|
||||
except NoRemoteError:
|
||||
if not used.external and used["local"]:
|
||||
raise
|
||||
except DownloadError as exc:
|
||||
failed += exc.amount
|
||||
|
||||
for (repo_url, repo_rev), files in used.external.items():
|
||||
d, f = _fetch_external(self, repo_url, repo_rev, files, jobs)
|
||||
external = set()
|
||||
for odb, objs in used.items():
|
||||
if odb is None:
|
||||
# objs contains naive objects to be pulled from specified remote
|
||||
d, f = _fetch_naive_objs(
|
||||
self,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
remote=remote,
|
||||
show_checksums=show_checksums,
|
||||
)
|
||||
downloaded += d
|
||||
failed += f
|
||||
elif isinstance(odb, GitObjectDB):
|
||||
# objs contains staged import objects which should be saved
|
||||
# last (after all other objects have been pulled)
|
||||
external.update(objs)
|
||||
else:
|
||||
d, f = fetch_from_odb(
|
||||
self,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
show_checksums=show_checksums,
|
||||
)
|
||||
downloaded += d
|
||||
failed += f
|
||||
|
||||
if external:
|
||||
d, f = _fetch_external(self, external, jobs=jobs)
|
||||
downloaded += d
|
||||
failed += f
|
||||
|
||||
@@ -79,44 +102,51 @@ def fetch(
|
||||
return downloaded
|
||||
|
||||
|
||||
def _fetch_external(self, repo_url, repo_rev, files, jobs):
|
||||
from dvc.external_repo import external_repo
|
||||
from dvc.objects import save
|
||||
from dvc.objects.stage import stage
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.scm.base import CloneError
|
||||
|
||||
def _fetch_naive_objs(repo, objs, **kwargs):
|
||||
# objs contains naive objects to be pulled from specified remote
|
||||
downloaded = 0
|
||||
failed = 0
|
||||
try:
|
||||
downloaded += repo.cloud.pull(objs, **kwargs)
|
||||
except NoRemoteError:
|
||||
if any(obj.fs.scheme == Schemes.LOCAL for obj in objs):
|
||||
raise
|
||||
except DownloadError as exc:
|
||||
failed += exc.amount
|
||||
return downloaded, failed
|
||||
|
||||
|
||||
def fetch_from_odb(repo, odb, objs, **kwargs):
|
||||
from dvc.remote.base import Remote
|
||||
|
||||
downloaded = 0
|
||||
failed = 0
|
||||
remote = Remote.from_odb(odb)
|
||||
try:
|
||||
downloaded += remote.pull(
|
||||
repo.odb.local,
|
||||
objs,
|
||||
**kwargs,
|
||||
)
|
||||
except DownloadError as exc:
|
||||
failed += exc.amount
|
||||
return downloaded, failed
|
||||
|
||||
|
||||
def _fetch_external(repo, objs, **kwargs):
|
||||
from dvc.objects import save
|
||||
from dvc.objects.errors import ObjectError
|
||||
|
||||
results = []
|
||||
failed = 0
|
||||
|
||||
def cb(result):
|
||||
def callback(result):
|
||||
results.append(result)
|
||||
|
||||
odb = self.odb.local
|
||||
try:
|
||||
with external_repo(
|
||||
repo_url, repo_rev, cache_dir=odb.cache_dir
|
||||
) as repo:
|
||||
root = PathInfo(repo.root_dir)
|
||||
for path in files:
|
||||
path_info = root / path
|
||||
try:
|
||||
used = repo.used_cache(
|
||||
[os.fspath(path_info)],
|
||||
force=True,
|
||||
jobs=jobs,
|
||||
recursive=True,
|
||||
)
|
||||
cb(repo.cloud.pull(used, jobs))
|
||||
except (NoOutputOrStageError, NoRemoteError):
|
||||
pass
|
||||
obj = stage(odb, path_info, repo.repo_fs, "md5", jobs=jobs)
|
||||
save(odb, obj, jobs=jobs, download_callback=cb)
|
||||
except CloneError:
|
||||
failed += 1
|
||||
logger.exception(
|
||||
"failed to fetch data for '{}'".format(", ".join(files))
|
||||
)
|
||||
for obj in objs:
|
||||
try:
|
||||
save(repo.odb.local, obj, download_callback=callback, **kwargs)
|
||||
except ObjectError:
|
||||
failed += 1
|
||||
|
||||
return sum(results), failed
|
||||
|
||||
+24
-16
@@ -1,11 +1,14 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.objects.db import NamedCache
|
||||
|
||||
from ..scheme import Schemes
|
||||
from . import locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.objects.file import HashFile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -52,30 +55,32 @@ def gc(
|
||||
repos = []
|
||||
all_repos = [Repo(path) for path in repos]
|
||||
|
||||
used_objs: Set["HashFile"] = set()
|
||||
with ExitStack() as stack:
|
||||
for repo in all_repos:
|
||||
stack.enter_context(repo.lock)
|
||||
|
||||
used = NamedCache()
|
||||
for repo in all_repos + [self]:
|
||||
used.update(
|
||||
repo.used_cache(
|
||||
all_branches=all_branches,
|
||||
with_deps=with_deps,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
all_experiments=all_experiments,
|
||||
remote=remote,
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
)
|
||||
)
|
||||
for objs in repo.used_objs(
|
||||
all_branches=all_branches,
|
||||
with_deps=with_deps,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
all_experiments=all_experiments,
|
||||
remote=remote,
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
).values():
|
||||
used_objs.update(objs)
|
||||
|
||||
for scheme, odb in self.odb.by_scheme():
|
||||
if not odb:
|
||||
continue
|
||||
|
||||
removed = odb.gc(set(used.scheme_keys(scheme)), jobs=jobs)
|
||||
removed = odb.gc(
|
||||
{obj for obj in used_objs if obj.fs.scheme == scheme},
|
||||
jobs=jobs,
|
||||
)
|
||||
if not removed:
|
||||
logger.info(f"No unused '{scheme}' cache to remove.")
|
||||
|
||||
@@ -83,6 +88,9 @@ def gc(
|
||||
return
|
||||
|
||||
remote = self.cloud.get_remote(remote, "gc -c")
|
||||
removed = remote.gc(set(used.scheme_keys(Schemes.LOCAL)), jobs=jobs)
|
||||
removed = remote.gc(
|
||||
{obj for obj in used_objs if obj.fs.scheme == Schemes.LOCAL},
|
||||
jobs=jobs,
|
||||
)
|
||||
if not removed:
|
||||
logger.info("No unused cache to remove from remote.")
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ def ls(url, path=None, rev=None, recursive=None, dvc_only=False):
|
||||
"""
|
||||
from . import Repo
|
||||
|
||||
with Repo.open(url, rev, subrepos=True, uninitialized=True) as repo:
|
||||
with Repo.open(url, rev=rev, subrepos=True, uninitialized=True) as repo:
|
||||
path_info = PathInfo(repo.root_dir)
|
||||
if path:
|
||||
path_info /= path
|
||||
|
||||
+5
-1
@@ -62,7 +62,11 @@ def move(self, from_path, to_path):
|
||||
)
|
||||
to_path = os.path.relpath(to_path, new_wdir)
|
||||
new_stage = self.stage.create(
|
||||
single_stage=True, fname=new_fname, wdir=new_wdir, outs=[to_path]
|
||||
single_stage=True,
|
||||
fname=new_fname,
|
||||
wdir=new_wdir,
|
||||
outs=[to_path],
|
||||
meta=stage.meta,
|
||||
)
|
||||
|
||||
os.unlink(stage.path)
|
||||
|
||||
+6
-2
@@ -24,7 +24,7 @@ def push(
|
||||
|
||||
expanded_targets = glob_targets(targets, glob=glob)
|
||||
|
||||
used = self.used_cache(
|
||||
used = self.used_objs(
|
||||
expanded_targets,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
@@ -38,4 +38,8 @@ def push(
|
||||
revs=revs,
|
||||
)
|
||||
|
||||
return len(used_run_cache) + self.cloud.push(used, jobs, remote=remote)
|
||||
pushed = len(used_run_cache)
|
||||
for odb, objs in used.items():
|
||||
if odb is None:
|
||||
pushed += self.cloud.push(objs, jobs, remote=remote)
|
||||
return pushed
|
||||
|
||||
+18
-14
@@ -78,7 +78,7 @@ def _cloud_status(
|
||||
"""
|
||||
import dvc.remote.base as cloud
|
||||
|
||||
used = self.used_cache(
|
||||
used = self.used_objs(
|
||||
targets,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
@@ -91,22 +91,26 @@ def _cloud_status(
|
||||
)
|
||||
|
||||
ret = {}
|
||||
status_info = self.cloud.status(
|
||||
used, jobs, remote=remote, log_missing=False
|
||||
)
|
||||
for info in status_info.values():
|
||||
name = info["name"]
|
||||
status_ = info["status"]
|
||||
if status_ == cloud.STATUS_OK:
|
||||
for odb, objs in used.items():
|
||||
if odb is not None:
|
||||
# ignore imported objects
|
||||
continue
|
||||
status_info = self.cloud.status(
|
||||
objs, jobs, remote=remote, log_missing=False
|
||||
)
|
||||
for info in status_info.values():
|
||||
name = info["name"]
|
||||
status_ = info["status"]
|
||||
if status_ == cloud.STATUS_OK:
|
||||
continue
|
||||
|
||||
prefix_map = {
|
||||
cloud.STATUS_DELETED: "deleted",
|
||||
cloud.STATUS_NEW: "new",
|
||||
cloud.STATUS_MISSING: "missing",
|
||||
}
|
||||
prefix_map = {
|
||||
cloud.STATUS_DELETED: "deleted",
|
||||
cloud.STATUS_NEW: "new",
|
||||
cloud.STATUS_MISSING: "missing",
|
||||
}
|
||||
|
||||
ret[name] = prefix_map[status_]
|
||||
ret[name] = prefix_map[status_]
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
+11
-8
@@ -4,7 +4,7 @@ import string
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set
|
||||
|
||||
from funcy import cached_property, project
|
||||
|
||||
@@ -36,6 +36,8 @@ from .utils import (
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.dvcfile import DVCFile
|
||||
from dvc.objects.db.base import ObjectDB
|
||||
from dvc.objects.file import HashFile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# Disallow all punctuation characters except hyphen and underscore
|
||||
@@ -622,14 +624,15 @@ class Stage(params.StageParams):
|
||||
for out in self.filter_outs(filter_info)
|
||||
)
|
||||
|
||||
def get_used_cache(self, *args, **kwargs):
|
||||
from dvc.objects.db import NamedCache
|
||||
|
||||
cache = NamedCache()
|
||||
def get_used_objs(
|
||||
self, *args, **kwargs
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashFile"]]:
|
||||
"""Return set of objects used by this stage."""
|
||||
used_objs = defaultdict(set)
|
||||
for out in self.filter_outs(kwargs.get("filter_info")):
|
||||
cache.update(out.get_used_cache(*args, **kwargs))
|
||||
|
||||
return cache
|
||||
for odb, objs in out.get_used_objs(*args, **kwargs).items():
|
||||
used_objs[odb].update(objs)
|
||||
return used_objs
|
||||
|
||||
@staticmethod
|
||||
def _check_can_merge(stage, ancestor_out=None):
|
||||
|
||||
+18
-14
@@ -196,7 +196,8 @@ class StageCache:
|
||||
cached_stage = self._create_stage(cache, wdir=stage.wdir)
|
||||
|
||||
if pull:
|
||||
self.repo.cloud.pull(cached_stage.get_used_cache())
|
||||
for objs in cached_stage.get_used_objs().values():
|
||||
self.repo.cloud.pull(objs)
|
||||
|
||||
if not cached_stage.outs_cached():
|
||||
raise RunCacheNotFoundError(stage)
|
||||
@@ -212,15 +213,17 @@ class StageCache:
|
||||
ret = []
|
||||
|
||||
runs = from_remote.path_info / "runs"
|
||||
if not from_remote.exists(runs):
|
||||
if not from_remote.fs.exists(runs):
|
||||
return []
|
||||
|
||||
for src in from_remote.walk_files(runs):
|
||||
for src in from_remote.fs.walk_files(runs):
|
||||
rel = src.relative_to(from_remote.path_info)
|
||||
dst = to_remote.path_info / rel
|
||||
key = dst.parent
|
||||
# check if any build cache already exists for this key
|
||||
if to_remote.exists(key) and first(to_remote.walk_files(key)):
|
||||
if to_remote.fs.exists(key) and first(
|
||||
to_remote.fs.walk_files(key)
|
||||
):
|
||||
continue
|
||||
func(src, dst)
|
||||
ret.append((src.parent.name, src.name))
|
||||
@@ -233,8 +236,8 @@ class StageCache:
|
||||
remote = self.repo.cloud.get_remote(remote)
|
||||
return self._transfer(
|
||||
_log_exceptions(remote.fs.upload, "upload"),
|
||||
self.repo.odb.local.fs,
|
||||
remote.fs,
|
||||
self.repo.odb.local,
|
||||
remote.odb,
|
||||
)
|
||||
|
||||
def pull(self, remote):
|
||||
@@ -243,19 +246,20 @@ class StageCache:
|
||||
remote = self.repo.cloud.get_remote(remote)
|
||||
return self._transfer(
|
||||
_log_exceptions(remote.fs.download, "download"),
|
||||
remote.fs,
|
||||
self.repo.odb.local.fs,
|
||||
remote.odb,
|
||||
self.repo.odb.local,
|
||||
)
|
||||
|
||||
def get_used_cache(self, used_run_cache, *args, **kwargs):
|
||||
from dvc.objects.db import NamedCache
|
||||
|
||||
cache = NamedCache()
|
||||
def get_used_objs(self, used_run_cache, *args, **kwargs):
|
||||
"""Return used cache for the specified run-cached stages."""
|
||||
from collections import defaultdict
|
||||
|
||||
used_objs = defaultdict(set)
|
||||
for key, value in used_run_cache:
|
||||
entry = self._load_cache(key, value)
|
||||
if not entry:
|
||||
continue
|
||||
stage = self._create_stage(entry)
|
||||
cache.update(stage.get_used_cache(*args, **kwargs))
|
||||
return cache
|
||||
for odb, objs in stage.get_used_objs(*args, **kwargs).items():
|
||||
used_objs[odb].update(objs)
|
||||
return used_objs
|
||||
|
||||
@@ -75,6 +75,7 @@ class CheckpointTask(MonitorTask):
|
||||
def _run_callback(stage, callback_func):
|
||||
stage.save(allow_missing=True)
|
||||
stage.commit(allow_missing=True)
|
||||
stage.unprotect_outs()
|
||||
logger.debug("Running checkpoint callback for stage '%s'", stage)
|
||||
callback_func()
|
||||
|
||||
|
||||
+11
-6
@@ -1,5 +1,4 @@
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -7,20 +6,21 @@ from typing import (
|
||||
Iterable,
|
||||
Optional,
|
||||
Sequence,
|
||||
TextIO,
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils import colorize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.ui.table import Headers, Styles, TableData
|
||||
|
||||
|
||||
class Formatter:
|
||||
def __init__(self, theme: Dict = None, defaults: Dict = None) -> None:
|
||||
from collections import defaultdict
|
||||
|
||||
theme = theme or {
|
||||
"success": {"color": "green", "style": "bold"},
|
||||
"warn": {"color": "yellow"},
|
||||
@@ -29,6 +29,8 @@ class Formatter:
|
||||
self.theme = defaultdict(lambda: defaults or {}, theme)
|
||||
|
||||
def format(self, message: str, style: str = None, **kwargs) -> str:
|
||||
from dvc.utils import colorize
|
||||
|
||||
return colorize(message, **self.theme[style])
|
||||
|
||||
|
||||
@@ -75,6 +77,7 @@ class Console:
|
||||
stderr: bool = False,
|
||||
force: bool = False,
|
||||
styled: bool = False,
|
||||
file: TextIO = None,
|
||||
) -> None:
|
||||
sep = " " if sep is None else sep
|
||||
end = "\n" if end is None else end
|
||||
@@ -85,12 +88,14 @@ class Console:
|
||||
console = self.error_console if stderr else self.rich_console
|
||||
return console.print(*objects, sep=sep, end=end)
|
||||
|
||||
file = sys.stderr if stderr else sys.stdout
|
||||
file = file or (sys.stderr if stderr else sys.stdout)
|
||||
values = (self.formatter.format(obj, style=style) for obj in objects)
|
||||
return print(*values, sep=sep, end=end, file=file)
|
||||
|
||||
@staticmethod
|
||||
def progress(*args, **kwargs) -> Tqdm:
|
||||
def progress(*args, **kwargs) -> "Tqdm":
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
return Tqdm(*args, **kwargs)
|
||||
|
||||
def prompt(
|
||||
|
||||
+13
-4
@@ -319,10 +319,19 @@ def relpath(path, start=os.curdir):
|
||||
start = os.path.abspath(os.fspath(start))
|
||||
|
||||
# Windows path on different drive than curdir doesn't have relpath
|
||||
if os.name == "nt" and not os.path.commonprefix(
|
||||
[start, os.path.abspath(path)]
|
||||
):
|
||||
return path
|
||||
if os.name == "nt":
|
||||
# Since python 3.8 os.realpath resolves network shares to their UNC
|
||||
# path. So, to be certain that relative paths correctly captured,
|
||||
# we need to resolve to UNC path first. We resolve only the drive
|
||||
# name so that we don't follow any 'real' symlinks on the path
|
||||
def resolve_network_drive_windows(path_to_resolve):
|
||||
drive, tail = os.path.splitdrive(path_to_resolve)
|
||||
return os.path.join(os.path.realpath(drive), tail)
|
||||
|
||||
path = resolve_network_drive_windows(os.path.abspath(path))
|
||||
start = resolve_network_drive_windows(start)
|
||||
if not os.path.commonprefix([start, path]):
|
||||
return path
|
||||
return os.path.relpath(path, start)
|
||||
|
||||
|
||||
|
||||
+10
-6
@@ -4,11 +4,15 @@ import os
|
||||
import shutil
|
||||
import stat
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.system import System
|
||||
from dvc.utils import dict_md5
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LOCAL_CHUNK_SIZE = 2 ** 20 # 1 MB
|
||||
@@ -73,9 +77,9 @@ class BasePathNotInCheckedPathException(DvcException):
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
def contains_symlink_up_to(path, base_path):
|
||||
base_path = os.fspath(base_path)
|
||||
path = os.fspath(path)
|
||||
def contains_symlink_up_to(path: "StrPath", base_path: "StrPath"):
|
||||
base_path = os.path.normcase(os.fspath(base_path))
|
||||
path = os.path.normcase(os.fspath(path))
|
||||
|
||||
if base_path not in path:
|
||||
raise BasePathNotInCheckedPathException(path, base_path)
|
||||
@@ -144,11 +148,11 @@ def remove(path):
|
||||
raise
|
||||
|
||||
|
||||
def path_isin(child, parent):
|
||||
def path_isin(child: "StrPath", parent: "StrPath"):
|
||||
"""Check if given `child` path is inside `parent`."""
|
||||
|
||||
def normalize_path(path):
|
||||
return os.path.normpath(path)
|
||||
def normalize_path(path) -> str:
|
||||
return os.path.normcase(os.path.normpath(path))
|
||||
|
||||
parent = os.path.join(normalize_path(parent), "")
|
||||
child = normalize_path(child)
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Any, Callable, ContextManager, Dict, Union
|
||||
|
||||
from funcy import reraise
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
@@ -42,13 +43,24 @@ class ParseError(DvcException):
|
||||
from dvc.utils import relpath
|
||||
|
||||
path = relpath(path)
|
||||
self.path = path
|
||||
super().__init__(f"unable to read: '{path}', {message}")
|
||||
|
||||
|
||||
class EncodingError(ParseError):
|
||||
"""We could not read a file with the given encoding"""
|
||||
|
||||
def __init__(self, path: "AnyPath", encoding: str):
|
||||
self.encoding = encoding
|
||||
super().__init__(path, f"is not valid {encoding}")
|
||||
|
||||
|
||||
def _load_data(path: "AnyPath", parser: ParserFn, fs: "BaseFileSystem" = None):
|
||||
open_fn = fs.open if fs else open
|
||||
with open_fn(path, encoding="utf-8") as fd: # type: ignore
|
||||
return parser(fd.read(), path)
|
||||
encoding = "utf-8"
|
||||
with open_fn(path, encoding=encoding) as fd: # type: ignore
|
||||
with reraise(UnicodeDecodeError, EncodingError(path, encoding)):
|
||||
return parser(fd.read(), path)
|
||||
|
||||
|
||||
def _dump_data(path, data: Any, dumper: DumperFn, fs: "BaseFileSystem" = None):
|
||||
|
||||
+5
-1
@@ -70,6 +70,7 @@ class HashedStreamReader(io.IOBase):
|
||||
def __init__(self, fobj):
|
||||
self.fobj = fobj
|
||||
self.md5 = hashlib.md5()
|
||||
self.total_read = 0
|
||||
self.is_text_file = None
|
||||
super().__init__()
|
||||
|
||||
@@ -95,9 +96,12 @@ class HashedStreamReader(io.IOBase):
|
||||
else:
|
||||
data = chunk
|
||||
self.md5.update(data)
|
||||
self.total_read += len(data)
|
||||
|
||||
return chunk
|
||||
|
||||
@property
|
||||
def hash_info(self):
|
||||
return HashInfo(self.PARAM_CHECKSUM, self.md5.hexdigest(), nfiles=1)
|
||||
return HashInfo(
|
||||
self.PARAM_CHECKSUM, self.md5.hexdigest(), size=self.total_read
|
||||
)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
_BASE_VERSION = "2.3.0"
|
||||
_BASE_VERSION = "2.4.2"
|
||||
|
||||
|
||||
def _generate_version(base_version):
|
||||
|
||||
@@ -88,16 +88,16 @@ install_requires = [
|
||||
"python-benedict>=0.21.1",
|
||||
"pyparsing==2.4.7",
|
||||
"typing_extensions>=3.7.4",
|
||||
"fsspec==2021.5.0",
|
||||
"fsspec==2021.6.1",
|
||||
"diskcache>=5.2.1",
|
||||
]
|
||||
|
||||
|
||||
# Extra dependencies for remote integrations
|
||||
|
||||
gs = ["gcsfs==2021.5.0"]
|
||||
gs = ["gcsfs==2021.6.1"]
|
||||
gdrive = ["pydrive2>=1.8.1", "six >= 1.13.0"]
|
||||
s3 = ["s3fs==2021.5.0", "aiobotocore[boto3]==1.3.0"]
|
||||
s3 = ["s3fs==2021.6.1", "aiobotocore[boto3]==1.3.0"]
|
||||
azure = ["adlfs==0.7.1", "azure-identity>=1.4.0", "knack"]
|
||||
# https://github.com/Legrandin/pycryptodome/issues/465
|
||||
oss = ["oss2==2.6.1", "pycryptodome>=3.10"]
|
||||
@@ -106,7 +106,7 @@ ssh = ["paramiko[invoke]>=2.7.0"]
|
||||
# Remove the env marker if/when pyarrow is available for Python3.9
|
||||
hdfs = ["pyarrow>=2.0.0"]
|
||||
webhdfs = ["hdfs==2.5.8"]
|
||||
webdav = ["webdavclient3>=3.14.5"]
|
||||
webdav = ["webdav4>=0.8.1"]
|
||||
# gssapi should not be included in all_remotes, because it doesn't have wheels
|
||||
# for linux and mac, so it will fail to compile if user doesn't have all the
|
||||
# requirements, including kerberos itself. Once all the wheels are available,
|
||||
|
||||
+10
-6
@@ -3,11 +3,11 @@ wheel==0.36.2
|
||||
# Test requirements
|
||||
pytest==6.2.4
|
||||
pytest-cov==2.12.1
|
||||
pytest-xdist==2.2.1
|
||||
pytest-xdist==2.3.0
|
||||
pytest-mock==3.6.1
|
||||
pytest-lazy-fixture==0.6.3
|
||||
pytest-timeout==1.4.2
|
||||
pytest-docker==0.10.1
|
||||
pytest-docker==0.10.3
|
||||
|
||||
flaky==3.7.0
|
||||
mock==4.0.3
|
||||
@@ -17,12 +17,12 @@ beautifulsoup4==4.9.3
|
||||
wget==3.2
|
||||
filelock==3.0.12
|
||||
wsgidav==3.1.0
|
||||
crc32c==2.2
|
||||
crc32c==2.2.post0
|
||||
xmltodict==0.12.0
|
||||
google-compute-engine==2.8.13
|
||||
google-cloud-storage==1.38.0
|
||||
google-cloud-storage==1.39.0
|
||||
|
||||
urllib3==1.26.5
|
||||
urllib3==1.26.6
|
||||
|
||||
# required by collective.checkdocs
|
||||
Pygments==2.9.0
|
||||
@@ -39,7 +39,11 @@ pylint-pytest==1.0.3
|
||||
pylint-plugin-utils==0.6
|
||||
|
||||
# type-checking
|
||||
mypy==0.812
|
||||
mypy==0.910
|
||||
types-requests
|
||||
types-paramiko
|
||||
types-tabulate
|
||||
types-toml
|
||||
|
||||
# pypi doesn't allow for direct dependencies
|
||||
# gdrivefs @ git+https://github.com/intake/gdrivefs.git
|
||||
|
||||
@@ -7,9 +7,13 @@ from dvc.repo.experiments.base import EXEC_APPLY, EXEC_CHECKPOINT
|
||||
|
||||
|
||||
@pytest.mark.parametrize("workspace", [True, False])
|
||||
@pytest.mark.parametrize("links", ["reflink,copy", "hardlink,symlink"])
|
||||
def test_new_checkpoint(
|
||||
tmp_dir, scm, dvc, checkpoint_stage, mocker, workspace
|
||||
tmp_dir, scm, dvc, checkpoint_stage, mocker, workspace, links
|
||||
):
|
||||
with dvc.config.edit() as conf:
|
||||
conf["cache"]["type"] = links
|
||||
|
||||
new_mock = mocker.spy(dvc.experiments, "new")
|
||||
results = dvc.experiments.run(
|
||||
checkpoint_stage.addressing, params=["foo=2"], tmp_dir=not workspace
|
||||
|
||||
@@ -683,17 +683,18 @@ def test_show_no_repo(tmp_dir):
|
||||
dvc.plots.show(["metric.json"])
|
||||
|
||||
|
||||
def test_show_from_subdir(tmp_dir, dvc, caplog):
|
||||
def test_show_from_subdir(tmp_dir, dvc, capsys):
|
||||
subdir = tmp_dir / "subdir"
|
||||
|
||||
subdir.mkdir()
|
||||
metric = [{"first_val": 100, "val": 2}, {"first_val": 200, "val": 3}]
|
||||
_write_json(subdir, metric, "metric.json")
|
||||
|
||||
with subdir.chdir(), caplog.at_level(logging.INFO, "dvc"):
|
||||
with subdir.chdir():
|
||||
assert main(["plots", "show", "metric.json"]) == 0
|
||||
|
||||
assert subdir.as_uri() in caplog.text
|
||||
out, _ = capsys.readouterr()
|
||||
assert subdir.as_uri() in out
|
||||
assert (subdir / "plots.html").exists()
|
||||
|
||||
|
||||
|
||||
@@ -1024,6 +1024,7 @@ def test_add_to_remote(tmp_dir, dvc, local_cloud, local_remote):
|
||||
|
||||
hash_info = stage.outs[0].hash_info
|
||||
assert local_remote.hash_to_path_info(hash_info.value).read_text() == "foo"
|
||||
assert hash_info.size == len("foo")
|
||||
|
||||
|
||||
def test_add_to_remote_absolute(tmp_dir, make_tmp_dir, dvc, local_remote):
|
||||
@@ -1067,6 +1068,8 @@ def test_add_to_cache_dir(tmp_dir, dvc, local_cloud):
|
||||
(stage,) = dvc.add(str(local_cloud / "data"), out="data")
|
||||
assert len(stage.deps) == 0
|
||||
assert len(stage.outs) == 1
|
||||
assert stage.outs[0].hash_info.size == len("foo") + len("bar")
|
||||
assert stage.outs[0].hash_info.nfiles == 2
|
||||
|
||||
data = tmp_dir / "data"
|
||||
assert data.read_text() == {"foo": "foo", "bar": "bar"}
|
||||
|
||||
+30
-29
@@ -98,7 +98,7 @@ def test_config_set_local(tmp_dir, dvc):
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_config_get(tmp_dir, dvc, caplog, args, ret, msg):
|
||||
def test_config_get(tmp_dir, dvc, capsys, caplog, args, ret, msg):
|
||||
(tmp_dir / ".dvc" / "config").write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
@@ -123,12 +123,12 @@ def test_config_get(tmp_dir, dvc, caplog, args, ret, msg):
|
||||
)
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
assert main(["config"] + args) == ret
|
||||
assert msg in caplog.text
|
||||
text = caplog.text if ret else capsys.readouterr()[0]
|
||||
assert msg in text
|
||||
|
||||
|
||||
def test_config_list(tmp_dir, dvc, caplog):
|
||||
def test_config_list(tmp_dir, dvc, capsys):
|
||||
(tmp_dir / ".dvc" / "config").write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
@@ -155,17 +155,18 @@ def test_config_list(tmp_dir, dvc, caplog):
|
||||
)
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
assert main(["config", "--list"]) == 0
|
||||
assert "remote.myremote.url=s3://bucket/path" in caplog.text
|
||||
assert "remote.myremote.region=us-east-2" in caplog.text
|
||||
assert "remote.myremote.profile=iterative" in caplog.text
|
||||
assert "remote.myremote.access_key_id=abcde" in caplog.text
|
||||
assert "remote.myremote.secret_access_key=123456" in caplog.text
|
||||
assert "remote.other.url=gs://bucket/path" in caplog.text
|
||||
assert "core.analytics=False" in caplog.text
|
||||
assert "core.no_scm=true" in caplog.text
|
||||
assert "core.remote=myremote" in caplog.text
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "remote.myremote.url=s3://bucket/path" in out
|
||||
assert "remote.myremote.region=us-east-2" in out
|
||||
assert "remote.myremote.profile=iterative" in out
|
||||
assert "remote.myremote.access_key_id=abcde" in out
|
||||
assert "remote.myremote.secret_access_key=123456" in out
|
||||
assert "remote.other.url=gs://bucket/path" in out
|
||||
assert "core.analytics=False" in out
|
||||
assert "core.no_scm=true" in out
|
||||
assert "core.remote=myremote" in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -241,57 +242,56 @@ def test_load_relative_paths(dvc, field, remote_url):
|
||||
)
|
||||
|
||||
|
||||
def test_config_remote(tmp_dir, dvc, caplog):
|
||||
def test_config_remote(tmp_dir, dvc, capsys):
|
||||
(tmp_dir / ".dvc" / "config").write_text(
|
||||
"['remote \"myremote\"']\n"
|
||||
" url = s3://bucket/path\n"
|
||||
" region = myregion\n"
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
assert main(["config", "remote.myremote.url"]) == 0
|
||||
assert "s3://bucket/path" in caplog.text
|
||||
out, _ = capsys.readouterr()
|
||||
assert "s3://bucket/path" in out
|
||||
|
||||
caplog.clear()
|
||||
assert main(["config", "remote.myremote.region"]) == 0
|
||||
assert "myregion" in caplog.text
|
||||
out, _ = capsys.readouterr()
|
||||
assert "myregion" in out
|
||||
|
||||
|
||||
def test_config_show_origin_single(tmp_dir, dvc, caplog):
|
||||
def test_config_show_origin_single(tmp_dir, dvc, capsys):
|
||||
(tmp_dir / ".dvc" / "config").write_text(
|
||||
"['remote \"myremote\"']\n"
|
||||
" url = s3://bucket/path\n"
|
||||
" region = myregion\n"
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
assert (
|
||||
main(["config", "--show-origin", "--project", "remote.myremote.url"])
|
||||
== 0
|
||||
)
|
||||
out, _ = capsys.readouterr()
|
||||
assert (
|
||||
"{}\t{}\n".format(os.path.join(".dvc", "config"), "s3://bucket/path")
|
||||
in caplog.text
|
||||
in out
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
assert (
|
||||
main(["config", "--show-origin", "--local", "remote.myremote.url"])
|
||||
== 251
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
assert main(["config", "--list", "--project", "--show-origin"]) == 0
|
||||
out, _ = capsys.readouterr()
|
||||
assert (
|
||||
"{}\t{}\n".format(
|
||||
os.path.join(".dvc", "config"),
|
||||
"remote.myremote.url=s3://bucket/path",
|
||||
)
|
||||
in caplog.text
|
||||
in out
|
||||
)
|
||||
|
||||
|
||||
def test_config_show_origin_merged(tmp_dir, dvc, caplog):
|
||||
def test_config_show_origin_merged(tmp_dir, dvc, capsys):
|
||||
(tmp_dir / ".dvc" / "config").write_text(
|
||||
"['remote \"myremote\"']\n"
|
||||
" url = s3://bucket/path\n"
|
||||
@@ -302,18 +302,19 @@ def test_config_show_origin_merged(tmp_dir, dvc, caplog):
|
||||
"['remote \"myremote\"']\n timeout = 100\n"
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
assert main(["config", "--list", "--show-origin"]) == 0
|
||||
out, _ = capsys.readouterr()
|
||||
assert (
|
||||
"{}\t{}\n".format(
|
||||
os.path.join(".dvc", "config"),
|
||||
"remote.myremote.url=s3://bucket/path",
|
||||
)
|
||||
in caplog.text
|
||||
in out
|
||||
)
|
||||
|
||||
assert (
|
||||
"{}\t{}\n".format(
|
||||
os.path.join(".dvc", "config.local"), "remote.myremote.timeout=100"
|
||||
)
|
||||
in caplog.text
|
||||
in out
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ from flaky.flaky_decorator import flaky
|
||||
import dvc as dvc_module
|
||||
from dvc.external_repo import clean_repos
|
||||
from dvc.main import main
|
||||
from dvc.objects.db import NamedCache
|
||||
from dvc.remote.base import (
|
||||
STATUS_DELETED,
|
||||
STATUS_MISSING,
|
||||
@@ -57,7 +56,7 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
|
||||
out = stage.outs[0]
|
||||
cache = out.cache_path
|
||||
md5 = out.hash_info.value
|
||||
info = out.get_used_cache()
|
||||
foo_objs = out.get_used_objs().get(None, set())
|
||||
|
||||
(stage_dir,) = tmp_dir.dvc_gen(
|
||||
{
|
||||
@@ -70,29 +69,39 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
|
||||
)
|
||||
out_dir = stage_dir.outs[0]
|
||||
cache_dir = out_dir.cache_path
|
||||
name_dir = str(out_dir)
|
||||
md5_dir = out_dir.hash_info.value
|
||||
info_dir = NamedCache.make(out_dir.scheme, md5_dir, name_dir)
|
||||
dir_objs = {out_dir.obj}
|
||||
entry_md5s = [entry_obj.hash_info.value for _, entry_obj in out_dir.obj]
|
||||
|
||||
def _make_dir_status(status):
|
||||
expected = {md5_dir: {"name": md5_dir, "status": status}}
|
||||
expected.update(
|
||||
{
|
||||
entry_md5: {"name": entry_md5, "status": status}
|
||||
for entry_md5 in entry_md5s
|
||||
}
|
||||
)
|
||||
return expected
|
||||
|
||||
# Check status
|
||||
status = dvc.cloud.status(info, show_checksums=True)
|
||||
status = dvc.cloud.status(foo_objs, show_checksums=True)
|
||||
expected = {md5: {"name": md5, "status": STATUS_NEW}}
|
||||
assert status == expected
|
||||
|
||||
status_dir = dvc.cloud.status(info_dir, show_checksums=True)
|
||||
expected = {md5_dir: {"name": md5_dir, "status": STATUS_NEW}}
|
||||
status_dir = dvc.cloud.status(dir_objs, show_checksums=True)
|
||||
expected = _make_dir_status(STATUS_NEW)
|
||||
assert status_dir == expected
|
||||
|
||||
# Move cache and check status
|
||||
# See issue https://github.com/iterative/dvc/issues/4383 for details
|
||||
backup_dir = dvc.odb.local.cache_dir + ".backup"
|
||||
move(dvc.odb.local.cache_dir, backup_dir)
|
||||
status = dvc.cloud.status(info, show_checksums=True)
|
||||
status = dvc.cloud.status(foo_objs, show_checksums=True)
|
||||
expected = {md5: {"name": md5, "status": STATUS_MISSING}}
|
||||
assert status == expected
|
||||
|
||||
status_dir = dvc.cloud.status(info_dir, show_checksums=True)
|
||||
expected = {md5_dir: {"name": md5_dir, "status": STATUS_MISSING}}
|
||||
status_dir = dvc.cloud.status(dir_objs, show_checksums=True)
|
||||
expected = _make_dir_status(STATUS_MISSING)
|
||||
assert status_dir == expected
|
||||
|
||||
# Restore original cache:
|
||||
@@ -100,48 +109,48 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
|
||||
move(backup_dir, dvc.odb.local.cache_dir)
|
||||
|
||||
# Push and check status
|
||||
dvc.cloud.push(info)
|
||||
dvc.cloud.push(foo_objs)
|
||||
assert os.path.exists(cache)
|
||||
assert os.path.isfile(cache)
|
||||
|
||||
dvc.cloud.push(info_dir)
|
||||
dvc.cloud.push(dir_objs)
|
||||
assert os.path.isfile(cache_dir)
|
||||
|
||||
status = dvc.cloud.status(info, show_checksums=True)
|
||||
status = dvc.cloud.status(foo_objs, show_checksums=True)
|
||||
expected = {md5: {"name": md5, "status": STATUS_OK}}
|
||||
assert status == expected
|
||||
|
||||
status_dir = dvc.cloud.status(info_dir, show_checksums=True)
|
||||
expected = {md5_dir: {"name": md5_dir, "status": STATUS_OK}}
|
||||
status_dir = dvc.cloud.status(dir_objs, show_checksums=True)
|
||||
expected = _make_dir_status(STATUS_OK)
|
||||
assert status_dir == expected
|
||||
|
||||
# Remove and check status
|
||||
remove(dvc.odb.local.cache_dir)
|
||||
|
||||
status = dvc.cloud.status(info, show_checksums=True)
|
||||
status = dvc.cloud.status(foo_objs, show_checksums=True)
|
||||
expected = {md5: {"name": md5, "status": STATUS_DELETED}}
|
||||
assert status == expected
|
||||
|
||||
status_dir = dvc.cloud.status(info_dir, show_checksums=True)
|
||||
expected = {md5_dir: {"name": md5_dir, "status": STATUS_DELETED}}
|
||||
status_dir = dvc.cloud.status(dir_objs, show_checksums=True)
|
||||
expected = _make_dir_status(STATUS_DELETED)
|
||||
assert status_dir == expected
|
||||
|
||||
# Pull and check status
|
||||
dvc.cloud.pull(info)
|
||||
dvc.cloud.pull(foo_objs)
|
||||
assert os.path.exists(cache)
|
||||
assert os.path.isfile(cache)
|
||||
with open(cache) as fd:
|
||||
assert fd.read() == "foo"
|
||||
|
||||
dvc.cloud.pull(info_dir)
|
||||
dvc.cloud.pull(dir_objs)
|
||||
assert os.path.isfile(cache_dir)
|
||||
|
||||
status = dvc.cloud.status(info, show_checksums=True)
|
||||
status = dvc.cloud.status(foo_objs, show_checksums=True)
|
||||
expected = {md5: {"name": md5, "status": STATUS_OK}}
|
||||
assert status == expected
|
||||
|
||||
status_dir = dvc.cloud.status(info_dir, show_checksums=True)
|
||||
expected = {md5_dir: {"name": md5_dir, "status": STATUS_OK}}
|
||||
status_dir = dvc.cloud.status(dir_objs, show_checksums=True)
|
||||
expected = _make_dir_status(STATUS_OK)
|
||||
assert status_dir == expected
|
||||
|
||||
|
||||
@@ -239,7 +248,7 @@ def test_warn_on_outdated_stage(tmp_dir, dvc, local_remote, caplog):
|
||||
|
||||
def test_hash_recalculation(mocker, dvc, tmp_dir, local_remote):
|
||||
tmp_dir.gen({"foo": "foo"})
|
||||
test_get_file_hash = mocker.spy(dvc_module.objects.stage, "_get_file_hash")
|
||||
test_file_md5 = mocker.spy(dvc_module.objects.stage, "file_md5")
|
||||
ret = main(["config", "cache.type", "hardlink"])
|
||||
assert ret == 0
|
||||
ret = main(["add", "foo"])
|
||||
@@ -248,7 +257,7 @@ def test_hash_recalculation(mocker, dvc, tmp_dir, local_remote):
|
||||
assert ret == 0
|
||||
ret = main(["run", "--single-stage", "-d", "foo", "echo foo"])
|
||||
assert ret == 0
|
||||
assert test_get_file_hash.mock.call_count == 1
|
||||
assert test_file_md5.mock.call_count == 1
|
||||
|
||||
|
||||
def test_missing_cache(tmp_dir, dvc, local_remote, caplog):
|
||||
@@ -277,7 +286,10 @@ def test_missing_cache(tmp_dir, dvc, local_remote, caplog):
|
||||
assert bar in caplog.text
|
||||
|
||||
caplog.clear()
|
||||
assert dvc.status(cloud=True) == {"bar": "missing", "foo": "missing"}
|
||||
assert dvc.status(cloud=True) == {
|
||||
"bar": "missing",
|
||||
"foo": "missing",
|
||||
}
|
||||
assert header not in caplog.text
|
||||
assert foo not in caplog.text
|
||||
assert bar not in caplog.text
|
||||
@@ -295,7 +307,7 @@ def test_verify_hashes(
|
||||
remove("dir")
|
||||
remove(dvc.odb.local.cache_dir)
|
||||
|
||||
hash_spy = mocker.spy(dvc_module.objects.stage, "_get_file_hash")
|
||||
hash_spy = mocker.spy(dvc_module.objects.stage, "file_md5")
|
||||
|
||||
dvc.pull()
|
||||
assert hash_spy.call_count == 0
|
||||
@@ -463,13 +475,12 @@ def test_pipeline_file_target_ops(tmp_dir, dvc, run_copy, local_remote):
|
||||
({}, "Everything is up to date"),
|
||||
],
|
||||
)
|
||||
def test_push_stats(tmp_dir, dvc, fs, msg, caplog, local_remote):
|
||||
def test_push_stats(tmp_dir, dvc, fs, msg, capsys, local_remote):
|
||||
tmp_dir.dvc_gen(fs)
|
||||
|
||||
caplog.clear()
|
||||
with caplog.at_level(level=logging.INFO, logger="dvc"):
|
||||
main(["push"])
|
||||
assert msg in caplog.text
|
||||
main(["push"])
|
||||
out, _ = capsys.readouterr()
|
||||
assert msg in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -480,17 +491,17 @@ def test_push_stats(tmp_dir, dvc, fs, msg, caplog, local_remote):
|
||||
({}, "Everything is up to date."),
|
||||
],
|
||||
)
|
||||
def test_fetch_stats(tmp_dir, dvc, fs, msg, caplog, local_remote):
|
||||
def test_fetch_stats(tmp_dir, dvc, fs, msg, capsys, local_remote):
|
||||
tmp_dir.dvc_gen(fs)
|
||||
dvc.push()
|
||||
clean(list(fs.keys()), dvc)
|
||||
caplog.clear()
|
||||
with caplog.at_level(level=logging.INFO, logger="dvc"):
|
||||
main(["fetch"])
|
||||
assert msg in caplog.text
|
||||
|
||||
main(["fetch"])
|
||||
out, _ = capsys.readouterr()
|
||||
assert msg in out
|
||||
|
||||
|
||||
def test_pull_stats(tmp_dir, dvc, capsys, caplog, local_remote):
|
||||
def test_pull_stats(tmp_dir, dvc, capsys, local_remote):
|
||||
tmp_dir.dvc_gen({"foo": "foo", "bar": "bar"})
|
||||
dvc.push()
|
||||
clean(["foo", "bar"], dvc)
|
||||
@@ -501,12 +512,13 @@ def test_pull_stats(tmp_dir, dvc, capsys, caplog, local_remote):
|
||||
out, _ = capsys.readouterr()
|
||||
assert "M\tbar".expandtabs() in out
|
||||
assert "A\tfoo".expandtabs() in out
|
||||
assert "2 files fetched" in caplog.text
|
||||
assert "1 file added" in caplog.text
|
||||
assert "1 file modified" in caplog.text
|
||||
with caplog.at_level(level=logging.INFO, logger="dvc"):
|
||||
main(["pull"])
|
||||
assert "Everything is up to date." in caplog.text
|
||||
assert "2 files fetched" in out
|
||||
assert "1 file added" in out
|
||||
assert "1 file modified" in out
|
||||
|
||||
main(["pull"])
|
||||
out, _ = capsys.readouterr()
|
||||
assert "Everything is up to date." in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
+8
-11
@@ -244,9 +244,8 @@ def test_walk_dont_ignore_subrepos(tmp_dir, scm, dvc):
|
||||
)
|
||||
def test_fs_getsize(dvc, cloud):
|
||||
cloud.gen({"data": {"foo": "foo"}, "baz": "baz baz"})
|
||||
cls, config = get_cloud_fs(dvc, **cloud.config)
|
||||
cls, config, path_info = get_cloud_fs(dvc, **cloud.config)
|
||||
fs = cls(**config)
|
||||
path_info = fs.path_info
|
||||
|
||||
assert fs.getsize(path_info / "baz") == 7
|
||||
assert fs.getsize(path_info / "data" / "foo") == 3
|
||||
@@ -270,11 +269,11 @@ def test_fs_getsize(dvc, cloud):
|
||||
)
|
||||
def test_fs_upload_fobj(dvc, tmp_dir, cloud):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
cls, config = get_cloud_fs(dvc, **cloud.config)
|
||||
cls, config, path_info = get_cloud_fs(dvc, **cloud.config)
|
||||
fs = cls(**config)
|
||||
|
||||
from_info = tmp_dir / "foo"
|
||||
to_info = fs.path_info / "foo"
|
||||
to_info = path_info / "foo"
|
||||
|
||||
with open(from_info, "rb") as stream:
|
||||
fs.upload_fobj(stream, to_info)
|
||||
@@ -297,9 +296,9 @@ def test_fs_ls(dvc, cloud):
|
||||
}
|
||||
}
|
||||
)
|
||||
cls, config = get_cloud_fs(dvc, **cloud.config)
|
||||
cls, config, path_info = get_cloud_fs(dvc, **cloud.config)
|
||||
fs = cls(**config)
|
||||
path_info = cloud / "directory"
|
||||
path_info /= "directory"
|
||||
|
||||
assert {os.path.basename(file_key) for file_key in fs.ls(path_info)} == {
|
||||
"foo",
|
||||
@@ -327,9 +326,8 @@ def test_fs_ls(dvc, cloud):
|
||||
)
|
||||
def test_fs_find_recursive(dvc, cloud):
|
||||
cloud.gen({"data": {"foo": "foo", "bar": {"baz": "baz"}, "quux": "quux"}})
|
||||
cls, config = get_cloud_fs(dvc, **cloud.config)
|
||||
cls, config, path_info = get_cloud_fs(dvc, **cloud.config)
|
||||
fs = cls(**config)
|
||||
path_info = fs.path_info
|
||||
|
||||
assert {
|
||||
os.path.basename(file_key) for file_key in fs.find(path_info / "data")
|
||||
@@ -349,9 +347,8 @@ def test_fs_find_recursive(dvc, cloud):
|
||||
)
|
||||
def test_fs_find_with_etag(dvc, cloud):
|
||||
cloud.gen({"data": {"foo": "foo", "bar": {"baz": "baz"}, "quux": "quux"}})
|
||||
cls, config = get_cloud_fs(dvc, **cloud.config)
|
||||
cls, config, path_info = get_cloud_fs(dvc, **cloud.config)
|
||||
fs = cls(**config)
|
||||
path_info = fs.path_info
|
||||
|
||||
for details in fs.find(path_info / "data", detail=True):
|
||||
assert (
|
||||
@@ -371,7 +368,7 @@ def test_fs_find_with_etag(dvc, cloud):
|
||||
)
|
||||
def test_fs_fsspec_path_management(dvc, cloud):
|
||||
cloud.gen({"foo": "foo", "data": {"bar": "bar", "baz": {"foo": "foo"}}})
|
||||
cls, config = get_cloud_fs(dvc, **cloud.config)
|
||||
cls, config, _ = get_cloud_fs(dvc, **cloud.config)
|
||||
fs = cls(**config)
|
||||
|
||||
root = cloud.parents[len(cloud.parents) - 1]
|
||||
|
||||
@@ -530,7 +530,7 @@ def test_import_with_no_exec(tmp_dir, dvc, erepo_dir):
|
||||
|
||||
|
||||
def test_import_with_jobs(mocker, dvc, erepo_dir):
|
||||
from dvc.data_cloud import DataCloud
|
||||
import dvc as dvc_module
|
||||
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.dvc_gen(
|
||||
@@ -545,7 +545,57 @@ def test_import_with_jobs(mocker, dvc, erepo_dir):
|
||||
commit="init",
|
||||
)
|
||||
|
||||
spy = mocker.spy(DataCloud, "pull")
|
||||
spy = mocker.spy(dvc_module.objects, "save")
|
||||
dvc.imp(os.fspath(erepo_dir), "dir1", jobs=3)
|
||||
run_jobs = tuple(spy.call_args_list[0])[1].get("jobs")
|
||||
assert run_jobs == 3
|
||||
|
||||
|
||||
def test_chained_import(tmp_dir, dvc, make_tmp_dir, erepo_dir, local_cloud):
|
||||
erepo_dir.add_remote(config=local_cloud.config)
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="init")
|
||||
erepo_dir.dvc.push()
|
||||
remove(erepo_dir.dvc.odb.local.cache_dir)
|
||||
remove(os.fspath(erepo_dir / "dir"))
|
||||
|
||||
erepo2 = make_tmp_dir("erepo2", scm=True, dvc=True)
|
||||
with erepo2.chdir():
|
||||
erepo2.dvc.imp(os.fspath(erepo_dir), "dir")
|
||||
erepo2.scm.add("dir.dvc")
|
||||
erepo2.scm.commit("import")
|
||||
remove(erepo2.dvc.odb.local.cache_dir)
|
||||
remove(os.fspath(erepo2 / "dir"))
|
||||
|
||||
dvc.imp(os.fspath(erepo2), "dir", "dir_imported")
|
||||
dst = tmp_dir / "dir_imported"
|
||||
assert (dst / "foo").read_text() == "foo"
|
||||
assert (dst / "bar").read_text() == "bar"
|
||||
|
||||
remove(dvc.odb.local.cache_dir)
|
||||
remove("dir_imported")
|
||||
|
||||
# pulled objects should come from the original upstream repo's remote,
|
||||
# no cache or remote should be needed from the intermediate repo
|
||||
dvc.pull(["dir_imported.dvc"])
|
||||
assert not os.path.exists(erepo_dir.dvc.odb.local.cache_dir)
|
||||
assert not os.path.exists(erepo2.dvc.odb.local.cache_dir)
|
||||
assert (dst / "foo").read_text() == "foo"
|
||||
assert (dst / "bar").read_text() == "bar"
|
||||
|
||||
|
||||
def test_circular_import(tmp_dir, dvc, scm, erepo_dir):
|
||||
from dvc.exceptions import CircularImportError
|
||||
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.dvc_gen({"dir": {"foo": "foo", "bar": "bar"}}, commit="init")
|
||||
|
||||
dvc.imp(os.fspath(erepo_dir), "dir", "dir_imported")
|
||||
scm.add("dir_imported.dvc")
|
||||
scm.commit("import")
|
||||
|
||||
with erepo_dir.chdir():
|
||||
with pytest.raises(CircularImportError):
|
||||
erepo_dir.dvc.imp(
|
||||
os.fspath(tmp_dir), "dir_imported", "circular_import"
|
||||
)
|
||||
|
||||
@@ -123,6 +123,7 @@ def test_import_url_with_no_exec(tmp_dir, dvc, erepo_dir):
|
||||
[
|
||||
pytest.lazy_fixture("local_cloud"),
|
||||
pytest.lazy_fixture("s3"),
|
||||
pytest.lazy_fixture("azure"),
|
||||
pytest.param(
|
||||
pytest.lazy_fixture("gs"), marks=pytest.mark.needs_internet
|
||||
),
|
||||
@@ -282,6 +283,7 @@ def test_import_url_to_remote_single_file(
|
||||
|
||||
hash_info = stage.outs[0].hash_info
|
||||
assert local_remote.hash_to_path_info(hash_info.value).read_text() == "foo"
|
||||
assert hash_info.size == len("foo")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -273,6 +273,24 @@ def test_ls_repo_with_removed_dvc_dir_with_path_file(tmp_dir, dvc, scm):
|
||||
match_files(files, ((("file",), True),))
|
||||
|
||||
|
||||
def test_ls_repo_with_rev(erepo_dir):
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.scm_gen(FS_STRUCTURE, commit="init")
|
||||
erepo_dir.dvc_gen(DVC_STRUCTURE, commit="dvc")
|
||||
|
||||
rev = erepo_dir.scm.list_all_commits()[1]
|
||||
files = Repo.ls(os.fspath(erepo_dir), rev=rev)
|
||||
match_files(
|
||||
files,
|
||||
(
|
||||
((".dvcignore",), False),
|
||||
((".gitignore",), False),
|
||||
(("README.md",), False),
|
||||
(("model",), False),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_ls_remote_repo(erepo_dir):
|
||||
with erepo_dir.chdir():
|
||||
erepo_dir.scm_gen(FS_STRUCTURE, commit="init")
|
||||
|
||||
+23
-1
@@ -1,11 +1,12 @@
|
||||
import os
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.dvcfile import DVC_FILE_SUFFIX
|
||||
from dvc.exceptions import DvcException, MoveNotDataSourceError
|
||||
from dvc.main import main
|
||||
from dvc.utils.serialize import load_yaml
|
||||
from dvc.utils.serialize import dump_yaml, load_yaml
|
||||
from tests.basic_env import TestDvc, TestDvcGit
|
||||
from tests.func.test_repro import TestRepro
|
||||
from tests.utils import cd
|
||||
@@ -244,3 +245,24 @@ def test_move_output_overlap(tmp_dir, dvc):
|
||||
assert (tmp_dir / "foo.dvc").exists()
|
||||
assert not (tmp_dir / "dir" / "foo").exists()
|
||||
assert not (tmp_dir / "dir" / "foo.dvc").exists()
|
||||
|
||||
|
||||
def test_move_meta(tmp_dir, dvc):
|
||||
(stage,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
data = load_yaml(stage.path)
|
||||
data["meta"] = {"custom_key": 42}
|
||||
dump_yaml(stage.path, data)
|
||||
|
||||
dvc.move("foo", "bar")
|
||||
res = (tmp_dir / "bar.dvc").read_text()
|
||||
print(res)
|
||||
assert res == textwrap.dedent(
|
||||
"""\
|
||||
outs:
|
||||
- md5: acbd18db4cc2f85cedef654fccc4a4d8
|
||||
size: 3
|
||||
path: bar
|
||||
meta:
|
||||
custom_key: 42
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestExternalCacheDir(TestDvc):
|
||||
|
||||
self.dvc.__init__()
|
||||
|
||||
assert self.dvc.odb.ssh.fs.path_info == ssh_url + "/tmp"
|
||||
assert self.dvc.odb.ssh.path_info == ssh_url + "/tmp"
|
||||
|
||||
|
||||
class TestSharedCacheDir(TestDir):
|
||||
|
||||
@@ -204,6 +204,7 @@ def test_partial_push_n_pull(tmp_dir, dvc, tmp_path_factory, local_remote):
|
||||
dvc.push()
|
||||
remove(dvc.odb.local.cache_dir)
|
||||
|
||||
baz.collect_used_dir_cache()
|
||||
with patch.object(LocalFileSystem, "_download", side_effect=Exception):
|
||||
with pytest.raises(DownloadError) as download_error_info:
|
||||
dvc.pull()
|
||||
@@ -430,17 +431,22 @@ def test_push_incomplete_dir(tmp_dir, dvc, mocker, local_remote):
|
||||
remote = dvc.cloud.get_remote("upstream")
|
||||
|
||||
odb = dvc.odb.local
|
||||
dir_hash = stage.outs[0].hash_info.value
|
||||
used = stage.get_used_cache(remote=remote)
|
||||
out = stage.outs[0]
|
||||
file_objs = [entry_obj for _, entry_obj in out.obj]
|
||||
|
||||
# remove one of the cache files for directory
|
||||
file_hashes = list(used.child_keys(odb.fs.scheme, dir_hash))
|
||||
remove(odb.hash_to_path_info(file_hashes[0]))
|
||||
remove(odb.hash_to_path_info(file_objs[0].hash_info.value))
|
||||
|
||||
dvc.push()
|
||||
assert not remote.fs.exists(remote.odb.hash_to_path_info(dir_hash))
|
||||
assert not remote.fs.exists(remote.odb.hash_to_path_info(file_hashes[0]))
|
||||
assert remote.fs.exists(remote.odb.hash_to_path_info(file_hashes[1]))
|
||||
assert not remote.fs.exists(
|
||||
remote.odb.hash_to_path_info(out.hash_info.value)
|
||||
)
|
||||
assert not remote.fs.exists(
|
||||
remote.odb.hash_to_path_info(file_objs[0].hash_info.value)
|
||||
)
|
||||
assert remote.fs.exists(
|
||||
remote.odb.hash_to_path_info(file_objs[1].hash_info.value)
|
||||
)
|
||||
|
||||
|
||||
def test_upload_exists(tmp_dir, dvc, local_remote):
|
||||
@@ -451,7 +457,7 @@ def test_upload_exists(tmp_dir, dvc, local_remote):
|
||||
remote.fs.CACHE_MODE = 0o644
|
||||
|
||||
from_info = PathInfo(tmp_dir / "foo")
|
||||
to_info = remote.fs.path_info / "foo"
|
||||
to_info = remote.odb.path_info / "foo"
|
||||
remote.fs.upload(from_info, to_info)
|
||||
assert remote.fs.exists(to_info)
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from dvc.main import main
|
||||
|
||||
|
||||
def test_root(tmp_dir, dvc, caplog):
|
||||
def test_root(tmp_dir, dvc, capsys):
|
||||
assert main(["root"]) == 0
|
||||
assert ".\n" in caplog.text
|
||||
assert "." in capsys.readouterr()[0]
|
||||
|
||||
|
||||
def test_root_locked(tmp_dir, dvc, caplog):
|
||||
def test_root_locked(tmp_dir, dvc, capsys):
|
||||
# NOTE: check that `dvc root` is not blocked with dvc lock
|
||||
with dvc.lock:
|
||||
assert main(["root"]) == 0
|
||||
assert ".\n" in caplog.text
|
||||
assert "." in capsys.readouterr()[0]
|
||||
|
||||
@@ -4,13 +4,14 @@ from dvc.main import main
|
||||
from tests.unit.test_info import PYTHON_VERSION_REGEX
|
||||
|
||||
|
||||
def test_(tmp_dir, dvc, scm, caplog):
|
||||
def test_(tmp_dir, dvc, scm, capsys):
|
||||
assert main(["version"]) == 0
|
||||
|
||||
assert re.search(r"DVC version: \d+\.\d+\.\d+.*", caplog.text)
|
||||
assert re.search(f"Platform: {PYTHON_VERSION_REGEX} on .*", caplog.text)
|
||||
assert re.search(r"Supports: .*", caplog.text)
|
||||
assert re.search(r"Cache types: .*", caplog.text)
|
||||
assert re.search(r"Caches: local", caplog.text)
|
||||
assert re.search(r"Remotes: None", caplog.text)
|
||||
assert "Repo: dvc, git" in caplog.text
|
||||
out, _ = capsys.readouterr()
|
||||
assert re.search(r"DVC version: \d+\.\d+\.\d+.*", out)
|
||||
assert re.search(f"Platform: {PYTHON_VERSION_REGEX} on .*", out)
|
||||
assert re.search(r"Supports: .*", out)
|
||||
assert re.search(r"Cache types: .*", out)
|
||||
assert re.search(r"Caches: local", out)
|
||||
assert re.search(r"Remotes: None", out)
|
||||
assert "Repo: dvc, git" in out
|
||||
|
||||
@@ -21,25 +21,25 @@ class Webdav(Base, WebDAVURLInfo):
|
||||
|
||||
@cached_property
|
||||
def client(self):
|
||||
from webdav3.client import Client
|
||||
from webdav4.client import Client
|
||||
|
||||
user, secrets = first(AUTH.items())
|
||||
return Client(
|
||||
{
|
||||
"webdav_hostname": self.replace(path="").url,
|
||||
"webdav_login": user,
|
||||
"webdav_password": secrets["password"],
|
||||
}
|
||||
self.replace(path="").url, auth=(user, secrets["password"])
|
||||
)
|
||||
|
||||
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
|
||||
assert mode == 0o777
|
||||
parent_dirs = list(reversed(self.parents))[1:] if parents else []
|
||||
for d in parent_dirs + [self]:
|
||||
self.client.mkdir(d.path) # pylint: disable=no-member
|
||||
path = d.path # pylint: disable=no-member
|
||||
if not self.client.exists(path):
|
||||
self.client.mkdir(path)
|
||||
|
||||
def write_bytes(self, contents):
|
||||
self.client.upload_to(contents, self.path)
|
||||
from io import BytesIO
|
||||
|
||||
self.client.upload_fileobj(BytesIO(contents), self.path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
|
||||
import mock
|
||||
@@ -21,7 +20,7 @@ def test_digest(checksum, expected):
|
||||
assert expected == _digest(checksum)
|
||||
|
||||
|
||||
def test_default(mocker, caplog):
|
||||
def test_default(mocker, capsys):
|
||||
args = parse_args(["diff"])
|
||||
cmd = args.func(args)
|
||||
diff = {
|
||||
@@ -51,10 +50,10 @@ def test_default(mocker, caplog):
|
||||
"\n"
|
||||
"files summary: 1 added, 0 deleted, "
|
||||
"1 renamed, 0 modified, 0 not in cache"
|
||||
).format(sep=os.path.sep) in caplog.text
|
||||
).format(sep=os.path.sep) in capsys.readouterr()[0]
|
||||
|
||||
|
||||
def test_show_hash(mocker, caplog):
|
||||
def test_show_hash(mocker, capsys):
|
||||
args = parse_args(["diff", "--show-hash"])
|
||||
cmd = args.func(args)
|
||||
diff = {
|
||||
@@ -81,6 +80,8 @@ def test_show_hash(mocker, caplog):
|
||||
}
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
assert 0 == cmd.run()
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert (
|
||||
"Deleted:\n"
|
||||
" XXXXXXXX " + os.path.join("data", "") + "\n"
|
||||
@@ -100,10 +101,10 @@ def test_show_hash(mocker, caplog):
|
||||
"\n"
|
||||
"files summary: 0 added, 2 deleted, "
|
||||
"1 renamed, 2 modified, 0 not in cache"
|
||||
) in caplog.text
|
||||
) in out
|
||||
|
||||
|
||||
def test_show_json(mocker, caplog):
|
||||
def test_show_json(mocker, capsys):
|
||||
args = parse_args(["diff", "--show-json"])
|
||||
cmd = args.func(args)
|
||||
diff = {
|
||||
@@ -118,13 +119,14 @@ def test_show_json(mocker, caplog):
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert '"added": [{"path": "file1"}, {"path": "file2"}]' in caplog.text
|
||||
assert '"deleted": []' in caplog.text
|
||||
assert '"modified": []' in caplog.text
|
||||
assert '"not in cache": []' in caplog.text
|
||||
out, _ = capsys.readouterr()
|
||||
assert '"added": [{"path": "file1"}, {"path": "file2"}]' in out
|
||||
assert '"deleted": []' in out
|
||||
assert '"modified": []' in out
|
||||
assert '"not in cache": []' in out
|
||||
|
||||
|
||||
def test_show_json_and_hash(mocker, caplog):
|
||||
def test_show_json_and_hash(mocker, capsys):
|
||||
args = parse_args(["diff", "--show-json", "--show-hash"])
|
||||
cmd = args.func(args)
|
||||
|
||||
@@ -147,20 +149,21 @@ def test_show_json_and_hash(mocker, caplog):
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
|
||||
assert 0 == cmd.run()
|
||||
out, _ = capsys.readouterr()
|
||||
assert (
|
||||
'"added": [{"path": "file1", "hash": "11111111"}, '
|
||||
'{"path": "file2", "hash": "22222222"}]' in caplog.text
|
||||
'{"path": "file2", "hash": "22222222"}]' in out
|
||||
)
|
||||
assert '"deleted": []' in caplog.text
|
||||
assert '"modified": []' in caplog.text
|
||||
assert '"deleted": []' in out
|
||||
assert '"modified": []' in out
|
||||
assert (
|
||||
'"renamed": [{"path": {"old": "file_old", '
|
||||
'"new": "file_new"}, "hash": "11111111"}]' in caplog.text
|
||||
'"new": "file_new"}, "hash": "11111111"}]' in out
|
||||
)
|
||||
assert '"not in cache": []' in caplog.text
|
||||
assert '"not in cache": []' in out
|
||||
|
||||
|
||||
def test_show_json_hide_missing(mocker, caplog):
|
||||
def test_show_json_hide_missing(mocker, capsys):
|
||||
args = parse_args(["diff", "--show-json", "--hide-missing"])
|
||||
cmd = args.func(args)
|
||||
diff = {
|
||||
@@ -181,19 +184,17 @@ def test_show_json_hide_missing(mocker, caplog):
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert '"added": [{"path": "file1"}, {"path": "file2"}]' in caplog.text
|
||||
assert '"deleted": []' in caplog.text
|
||||
assert (
|
||||
'"renamed": [{"path": {"old": "file_old", "new": "file_new"}'
|
||||
in caplog.text
|
||||
)
|
||||
assert '"modified": []' in caplog.text
|
||||
assert '"not in cache": []' not in caplog.text
|
||||
out, _ = capsys.readouterr()
|
||||
assert '"added": [{"path": "file1"}, {"path": "file2"}]' in out
|
||||
assert '"deleted": []' in out
|
||||
assert '"renamed": [{"path": {"old": "file_old", "new": "file_new"}' in out
|
||||
assert '"modified": []' in out
|
||||
assert '"not in cache": []' not in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("show_hash", [None, True, False])
|
||||
@mock.patch("dvc.command.diff._show_md")
|
||||
def test_diff_show_md_and_hash(mock_show_md, mocker, caplog, show_hash):
|
||||
def test_diff_show_md_and_hash(mock_show_md, mocker, show_hash):
|
||||
options = ["diff", "--show-md"] + (["--show-hash"] if show_hash else [])
|
||||
args = parse_args(options)
|
||||
cmd = args.func(args)
|
||||
@@ -206,27 +207,20 @@ def test_diff_show_md_and_hash(mock_show_md, mocker, caplog, show_hash):
|
||||
mock_show_md.assert_called_once_with(diff, show_hash, False)
|
||||
|
||||
|
||||
def test_no_changes(mocker, caplog):
|
||||
def test_no_changes(mocker, capsys):
|
||||
args = parse_args(["diff", "--show-json"])
|
||||
cmd = args.func(args)
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value={})
|
||||
|
||||
def info():
|
||||
return [
|
||||
msg
|
||||
for name, level, msg in caplog.record_tuples
|
||||
if name.startswith("dvc") and level == logging.INFO
|
||||
]
|
||||
|
||||
assert 0 == cmd.run()
|
||||
assert ["{}"] == info()
|
||||
|
||||
caplog.clear()
|
||||
out, _ = capsys.readouterr()
|
||||
assert "{}" in out
|
||||
|
||||
args = parse_args(["diff"])
|
||||
cmd = args.func(args)
|
||||
assert 0 == cmd.run()
|
||||
assert not info()
|
||||
out, _ = capsys.readouterr()
|
||||
assert not out
|
||||
|
||||
|
||||
def test_show_md(capsys):
|
||||
@@ -330,7 +324,7 @@ def test_show_md_hide_missing(capsys):
|
||||
).format(sep=os.path.sep)
|
||||
|
||||
|
||||
def test_hide_missing(mocker, caplog):
|
||||
def test_hide_missing(mocker, capsys):
|
||||
args = parse_args(["diff", "--hide-missing"])
|
||||
cmd = args.func(args)
|
||||
diff = {
|
||||
@@ -348,6 +342,7 @@ def test_hide_missing(mocker, caplog):
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff)
|
||||
|
||||
assert 0 == cmd.run()
|
||||
out, _ = capsys.readouterr()
|
||||
assert (
|
||||
"Added:\n"
|
||||
" file\n"
|
||||
@@ -356,5 +351,5 @@ def test_hide_missing(mocker, caplog):
|
||||
" file_old -> file_new\n"
|
||||
"\n"
|
||||
"files summary: 1 added, 0 deleted, 1 renamed, 0 modified"
|
||||
) in caplog.text
|
||||
assert "not in cache" not in caplog.text
|
||||
) in out
|
||||
assert "not in cache" not in out
|
||||
|
||||
@@ -28,7 +28,7 @@ def test_get(mocker):
|
||||
)
|
||||
|
||||
|
||||
def test_get_url(mocker, caplog):
|
||||
def test_get_url(mocker, capsys):
|
||||
cli_args = parse_args(
|
||||
["get", "repo_url", "src", "--rev", "version", "--show-url"]
|
||||
)
|
||||
@@ -38,6 +38,7 @@ def test_get_url(mocker, caplog):
|
||||
m = mocker.patch("dvc.api.get_url", return_value="resource_url")
|
||||
|
||||
assert cmd.run() == 0
|
||||
assert "resource_url" in caplog.text
|
||||
out, _ = capsys.readouterr()
|
||||
assert "resource_url" in out
|
||||
|
||||
m.assert_called_once_with("src", repo="repo_url", rev="version")
|
||||
|
||||
@@ -50,7 +50,7 @@ def test_metrics_diff(dvc, mocker, capsys):
|
||||
)
|
||||
|
||||
|
||||
def test_metrics_diff_json(dvc, mocker, caplog, capsys):
|
||||
def test_metrics_diff_json(dvc, mocker, capsys):
|
||||
cli_args = parse_args(
|
||||
[
|
||||
"metrics",
|
||||
@@ -79,6 +79,7 @@ def test_metrics_diff_json(dvc, mocker, caplog, capsys):
|
||||
show_diff_mock = mocker.patch("dvc.compare.show_diff")
|
||||
|
||||
assert cmd.run() == 0
|
||||
out, _ = capsys.readouterr()
|
||||
metrics_diff.assert_called_once_with(
|
||||
cmd.repo,
|
||||
targets=["target1", "target2"],
|
||||
@@ -88,8 +89,7 @@ def test_metrics_diff_json(dvc, mocker, caplog, capsys):
|
||||
all=True,
|
||||
)
|
||||
show_diff_mock.assert_not_called()
|
||||
assert json.dumps(diff) in caplog.text
|
||||
assert capsys.readouterr() == ("", "")
|
||||
assert json.dumps(diff) in out
|
||||
|
||||
|
||||
def test_metrics_show(dvc, mocker):
|
||||
@@ -134,7 +134,7 @@ def test_metrics_show(dvc, mocker):
|
||||
)
|
||||
|
||||
|
||||
def test_metrics_show_json(dvc, mocker, caplog, capsys):
|
||||
def test_metrics_show_json(dvc, mocker, capsys):
|
||||
cli_args = parse_args(
|
||||
[
|
||||
"metrics",
|
||||
@@ -161,7 +161,7 @@ def test_metrics_show_json(dvc, mocker, caplog, capsys):
|
||||
show_metrics_mock = mocker.patch("dvc.compare.show_metrics")
|
||||
|
||||
assert cmd.run() == 0
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
metrics_show.assert_called_once_with(
|
||||
cmd.repo,
|
||||
["target1", "target2"],
|
||||
@@ -171,5 +171,4 @@ def test_metrics_show_json(dvc, mocker, caplog, capsys):
|
||||
all_commits=True,
|
||||
)
|
||||
show_metrics_mock.assert_not_called()
|
||||
assert json.dumps(d) in caplog.text
|
||||
assert capsys.readouterr() == ("", "")
|
||||
assert json.dumps(d) in out
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
|
||||
from dvc.cli import parse_args
|
||||
from dvc.command.params import CmdParamsDiff
|
||||
|
||||
@@ -57,7 +55,7 @@ def test_params_diff_from_cli(dvc, mocker):
|
||||
)
|
||||
|
||||
|
||||
def test_params_diff_show_json(dvc, mocker, caplog, capsys):
|
||||
def test_params_diff_show_json(dvc, mocker, capsys):
|
||||
cli_args = parse_args(
|
||||
["params", "diff", "HEAD~10", "HEAD~1", "--show-json"]
|
||||
)
|
||||
@@ -67,9 +65,7 @@ def test_params_diff_show_json(dvc, mocker, caplog, capsys):
|
||||
)
|
||||
show_diff_mock = mocker.patch("dvc.compare.show_diff")
|
||||
|
||||
with caplog.at_level(logging.INFO, logger="dvc"):
|
||||
assert cmd.run() == 0
|
||||
assert '{"params.yaml": {"a": "b"}}\n' in caplog.text
|
||||
|
||||
assert cmd.run() == 0
|
||||
out, _ = capsys.readouterr()
|
||||
assert '{"params.yaml": {"a": "b"}}\n' in out
|
||||
show_diff_mock.assert_not_called()
|
||||
assert capsys.readouterr() == ("", "")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
|
||||
@@ -91,7 +90,7 @@ def test_plots_show_vega(dvc, mocker):
|
||||
)
|
||||
|
||||
|
||||
def test_plots_diff_vega(dvc, mocker, caplog):
|
||||
def test_plots_diff_vega(dvc, mocker, capsys):
|
||||
cli_args = parse_args(
|
||||
[
|
||||
"plots",
|
||||
@@ -108,10 +107,12 @@ def test_plots_diff_vega(dvc, mocker, caplog):
|
||||
"dvc.repo.plots.diff.diff", return_value={"plots.csv": "plothtml"}
|
||||
)
|
||||
assert cmd.run() == 0
|
||||
assert "plothtml" in caplog.text
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert "plothtml" in out
|
||||
|
||||
|
||||
def test_plots_diff_open(tmp_dir, dvc, mocker, caplog):
|
||||
def test_plots_diff_open(tmp_dir, dvc, mocker, capsys):
|
||||
mocked_open = mocker.patch("webbrowser.open", return_value=True)
|
||||
cli_args = parse_args(["plots", "diff", "--targets", "datafile", "--open"])
|
||||
cmd = cli_args.func(cli_args)
|
||||
@@ -123,10 +124,12 @@ def test_plots_diff_open(tmp_dir, dvc, mocker, caplog):
|
||||
mocked_open.assert_called_once_with("plots.html")
|
||||
|
||||
expected_url = posixpath.join(tmp_dir.as_uri(), "plots.html")
|
||||
assert expected_url in caplog.text
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert expected_url in out
|
||||
|
||||
|
||||
def test_plots_diff_open_failed(tmp_dir, dvc, mocker, caplog):
|
||||
def test_plots_diff_open_failed(tmp_dir, dvc, mocker, capsys):
|
||||
mocked_open = mocker.patch("webbrowser.open", return_value=False)
|
||||
cli_args = parse_args(["plots", "diff", "--targets", "datafile", "--open"])
|
||||
cmd = cli_args.func(cli_args)
|
||||
@@ -140,10 +143,9 @@ def test_plots_diff_open_failed(tmp_dir, dvc, mocker, caplog):
|
||||
error_message = "Failed to open. Please try opening it manually."
|
||||
expected_url = posixpath.join(tmp_dir.as_uri(), "plots.html")
|
||||
|
||||
assert caplog.record_tuples == [
|
||||
("dvc.command.plots", logging.INFO, expected_url),
|
||||
("dvc.command.plots", logging.ERROR, error_message),
|
||||
]
|
||||
out, err = capsys.readouterr()
|
||||
assert expected_url in out
|
||||
assert error_message in err
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -155,7 +157,7 @@ def test_plots_diff_open_failed(tmp_dir, dvc, mocker, caplog):
|
||||
ids=["quote", "resolve"],
|
||||
)
|
||||
def test_plots_path_is_quoted_and_resolved_properly(
|
||||
tmp_dir, dvc, mocker, caplog, output, expected_url_path
|
||||
tmp_dir, dvc, mocker, capsys, output, expected_url_path
|
||||
):
|
||||
cli_args = parse_args(
|
||||
["plots", "diff", "--targets", "datafile", "--out", output]
|
||||
@@ -167,4 +169,6 @@ def test_plots_path_is_quoted_and_resolved_properly(
|
||||
|
||||
assert cmd.run() == 0
|
||||
expected_url = posixpath.join(tmp_dir.as_uri(), expected_url_path)
|
||||
assert expected_url in caplog.text
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert expected_url in out
|
||||
|
||||
@@ -45,16 +45,17 @@ def test_cloud_status(tmp_dir, dvc, mocker):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [{}, {"a": "b", "c": [1, 2, 3]}, [1, 2, 3]])
|
||||
def test_status_show_json(dvc, mocker, caplog, status):
|
||||
def test_status_show_json(dvc, mocker, capsys, status):
|
||||
cli_args = parse_args(["status", "--show-json"])
|
||||
assert cli_args.func == CmdDataStatus
|
||||
|
||||
cmd = cli_args.func(cli_args)
|
||||
|
||||
mocker.patch.object(cmd.repo, "status", autospec=True, return_value=status)
|
||||
caplog.clear()
|
||||
|
||||
assert cmd.run() == 0
|
||||
assert caplog.messages == [json.dumps(status)]
|
||||
out, _ = capsys.readouterr()
|
||||
assert json.dumps(status) in out
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -15,21 +15,27 @@ connection_string = (
|
||||
)
|
||||
|
||||
|
||||
def test_init_env_var(monkeypatch, dvc):
|
||||
def test_strip_protocol_env_var(monkeypatch, dvc):
|
||||
monkeypatch.setenv("AZURE_STORAGE_CONTAINER_NAME", container_name)
|
||||
monkeypatch.setenv("AZURE_STORAGE_CONNECTION_STRING", connection_string)
|
||||
|
||||
config = {"url": "azure://"}
|
||||
fs = AzureFileSystem(**config)
|
||||
assert fs.path_info == "azure://" + container_name
|
||||
assert (
|
||||
AzureFileSystem._strip_protocol("azure://")
|
||||
== f"azure://{container_name}"
|
||||
)
|
||||
|
||||
|
||||
def test_strip_protocol(dvc):
|
||||
assert (
|
||||
AzureFileSystem._strip_protocol(f"azure://{container_name}")
|
||||
== f"azure://{container_name}"
|
||||
)
|
||||
|
||||
|
||||
def test_init(dvc):
|
||||
prefix = "some/prefix"
|
||||
url = f"azure://{container_name}/{prefix}"
|
||||
config = {"url": url, "connection_string": connection_string}
|
||||
config = {"connection_string": connection_string}
|
||||
fs = AzureFileSystem(**config)
|
||||
assert fs.path_info == url
|
||||
assert fs.fs_args["connection_string"] == connection_string
|
||||
|
||||
|
||||
def test_info(tmp_dir, azure):
|
||||
|
||||
@@ -26,13 +26,6 @@ def grants():
|
||||
}
|
||||
|
||||
|
||||
def test_init(dvc):
|
||||
config = {"url": url}
|
||||
fs = S3FileSystem(**config)
|
||||
|
||||
assert fs.path_info == url
|
||||
|
||||
|
||||
def test_verify_ssl_default_param(dvc):
|
||||
config = {"url": url}
|
||||
fs = S3FileSystem(**config)
|
||||
|
||||
@@ -16,12 +16,12 @@ def test_get_cloud_fs(tmp_dir, dvc):
|
||||
first = base / "first"
|
||||
second = first / "second"
|
||||
|
||||
cls, config = get_cloud_fs(dvc, name="base")
|
||||
assert cls(**config).path_info == base
|
||||
cls, config = get_cloud_fs(dvc, name="first")
|
||||
assert cls(**config).path_info == first
|
||||
cls, config = get_cloud_fs(dvc, name="second")
|
||||
assert cls(**config).path_info == second
|
||||
_, _, path_info = get_cloud_fs(dvc, name="base")
|
||||
assert path_info == base
|
||||
_, _, path_info = get_cloud_fs(dvc, name="first")
|
||||
assert path_info == first
|
||||
_, _, path_info = get_cloud_fs(dvc, name="second")
|
||||
assert path_info == second
|
||||
|
||||
|
||||
def test_get_cloud_fs_validate(tmp_dir, dvc):
|
||||
@@ -39,11 +39,9 @@ def test_get_cloud_fs_validate(tmp_dir, dvc):
|
||||
default=False,
|
||||
)
|
||||
|
||||
assert get_cloud_fs(dvc, name="base")[1] == {
|
||||
"url": "ssh://example.com/path"
|
||||
}
|
||||
assert get_cloud_fs(dvc, name="base")[1] == {"host": "example.com"}
|
||||
assert get_cloud_fs(dvc, name="first")[1] == {
|
||||
"url": "ssh://example.com/path/first",
|
||||
"host": "example.com",
|
||||
"type": ["symlink"],
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user