mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2e479422f | ||
|
|
73820cc596 | ||
|
|
e7b695dbef | ||
|
|
27a8222e2a | ||
|
|
5850639a97 | ||
|
|
f2b56bbb8a | ||
|
|
cf8195c829 | ||
|
|
402c107823 | ||
|
|
b73754edd1 | ||
|
|
333f2f04f0 | ||
|
|
470c50a22b | ||
|
|
01f49b7f24 | ||
|
|
19f2246bee | ||
|
|
6a44783d72 | ||
|
|
98a286d88d | ||
|
|
499c2dda01 | ||
|
|
5f2615ff53 | ||
|
|
457d615a51 | ||
|
|
827f9a7f03 | ||
|
|
7821e126fe | ||
|
|
72ab5a12df | ||
|
|
abcd70b9cf | ||
|
|
3e4e143bee | ||
|
|
766b43bc2d | ||
|
|
92c66b2331 | ||
|
|
b1211366cd | ||
|
|
92823f1d84 | ||
|
|
f3fa67440d | ||
|
|
3275f1ebf5 | ||
|
|
4392af6d95 | ||
|
|
a02c5a7405 | ||
|
|
dec1ac7346 | ||
|
|
8ccae0a086 | ||
|
|
3c31234151 | ||
|
|
77b451d118 | ||
|
|
1d47b03cd5 | ||
|
|
7c898cfad7 | ||
|
|
1f07023d17 | ||
|
|
1e6b0242aa | ||
|
|
3c1e9ee9be | ||
|
|
9622157565 | ||
|
|
6ff4342f02 | ||
|
|
5bb72b36cb | ||
|
|
1aed230cd0 | ||
|
|
c50b2e1e71 | ||
|
|
34bc410075 | ||
|
|
47ff40da0c | ||
|
|
ee35ec10a2 | ||
|
|
e18fd34b2c | ||
|
|
c0b64b0a10 | ||
|
|
3927e75ef1 | ||
|
|
e5ae5ef24b | ||
|
|
e187508f40 | ||
|
|
8768b50aed | ||
|
|
7c61463d2d | ||
|
|
b0d0315689 | ||
|
|
7edcb6b1e5 | ||
|
|
b6d231f9cf | ||
|
|
322fcec5ca | ||
|
|
1e5b3603cc |
@@ -10,7 +10,7 @@ jobs:
|
||||
name: run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- uses: iterative/dvc-bench@main
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DVC: ${{ env.version }}
|
||||
- run: twine check --strict dist/*
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Packages
|
||||
path: dist/
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
@@ -31,9 +31,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-extended
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
path: ${{ matrix.plugin }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
cache: pip
|
||||
|
||||
@@ -23,14 +23,14 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.8", "3.9", "3.10", "3.11"]
|
||||
pyv: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
cache: pip
|
||||
|
||||
@@ -22,7 +22,7 @@ repos:
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.1.5'
|
||||
rev: 'v0.1.9'
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
+45
-39
@@ -23,10 +23,11 @@ def viztracer_profile(
|
||||
tracer = viztracer.VizTracer(max_stack_depth=depth, log_async=log_async)
|
||||
|
||||
tracer.start()
|
||||
yield
|
||||
tracer.stop()
|
||||
|
||||
tracer.save(path() if callable(path) else path)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
tracer.stop()
|
||||
tracer.save(path() if callable(path) else path)
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -45,31 +46,33 @@ def yappi_profile(
|
||||
yappi.set_clock_type("wall" if wall_clock else "cpu")
|
||||
|
||||
yappi.start()
|
||||
yield
|
||||
yappi.stop()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
yappi.stop()
|
||||
|
||||
threads = yappi.get_thread_stats()
|
||||
stats = {}
|
||||
if separate_threads:
|
||||
for thread in threads:
|
||||
ctx_id = thread.id
|
||||
stats[ctx_id] = yappi.get_func_stats(ctx_id=ctx_id)
|
||||
else:
|
||||
stats[None] = yappi.get_func_stats()
|
||||
|
||||
fpath = path() if callable(path) else path
|
||||
for ctx_id, st in stats.items():
|
||||
if fpath:
|
||||
out = f"{fpath}-{ctx_id}" if ctx_id is not None else fpath
|
||||
st.save(out, type="callgrind")
|
||||
threads = yappi.get_thread_stats()
|
||||
stats = {}
|
||||
if separate_threads:
|
||||
for thread in threads:
|
||||
ctx_id = thread.id
|
||||
stats[ctx_id] = yappi.get_func_stats(ctx_id=ctx_id)
|
||||
else:
|
||||
if ctx_id is not None:
|
||||
print(f"\nThread {ctx_id}") # noqa: T201
|
||||
st.print_all()
|
||||
if ctx_id is None:
|
||||
threads.print_all()
|
||||
stats[None] = yappi.get_func_stats()
|
||||
|
||||
yappi.clear_stats()
|
||||
fpath = path() if callable(path) else path
|
||||
for ctx_id, st in stats.items():
|
||||
if fpath:
|
||||
out = f"{fpath}-{ctx_id}" if ctx_id is not None else fpath
|
||||
st.save(out, type="callgrind")
|
||||
else:
|
||||
if ctx_id is not None:
|
||||
print(f"\nThread {ctx_id}") # noqa: T201
|
||||
st.print_all()
|
||||
if ctx_id is None:
|
||||
threads.print_all()
|
||||
|
||||
yappi.clear_stats()
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -85,13 +88,15 @@ def instrument(html_output=False):
|
||||
profiler = Profiler()
|
||||
|
||||
profiler.start()
|
||||
yield
|
||||
profiler.stop()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
profiler.stop()
|
||||
|
||||
if html_output:
|
||||
profiler.open_in_browser()
|
||||
return
|
||||
print(profiler.output_text(unicode=True, color=True)) # noqa: T201
|
||||
if html_output:
|
||||
profiler.open_in_browser()
|
||||
else:
|
||||
print(profiler.output_text(unicode=True, color=True)) # noqa: T201
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -102,13 +107,14 @@ def profile(dump_path: Optional[str] = None):
|
||||
prof = cProfile.Profile()
|
||||
prof.enable()
|
||||
|
||||
yield
|
||||
|
||||
prof.disable()
|
||||
if not dump_path:
|
||||
prof.print_stats(sort="cumtime")
|
||||
return
|
||||
prof.dump_stats(dump_path)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
prof.disable()
|
||||
if dump_path:
|
||||
prof.dump_stats(dump_path)
|
||||
else:
|
||||
prof.print_stats(sort="cumtime")
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ def _get_odb(
|
||||
cls, config, fs_path = get_cloud_fs(repo.config, **settings)
|
||||
fs = fs or cls(**config)
|
||||
if prefix:
|
||||
fs_path = fs.path.join(fs_path, *prefix)
|
||||
fs_path = fs.join(fs_path, *prefix)
|
||||
if hash_name:
|
||||
config["hash_name"] = hash_name
|
||||
return get_odb(fs, fs_path, state=repo.state, **config)
|
||||
@@ -86,7 +86,7 @@ class CacheManager:
|
||||
return FileStorage(
|
||||
key=(),
|
||||
fs=self.local.fs,
|
||||
path=self.local.fs.path.join(self.default_local_cache_dir, self.FS_DIR),
|
||||
path=self.local.fs.join(self.default_local_cache_dir, self.FS_DIR),
|
||||
)
|
||||
|
||||
def _init_odb(self, schemes):
|
||||
|
||||
@@ -19,6 +19,7 @@ from dvc.commands import (
|
||||
data_sync,
|
||||
destroy,
|
||||
diff,
|
||||
du,
|
||||
experiments,
|
||||
freeze,
|
||||
gc,
|
||||
@@ -42,6 +43,7 @@ from dvc.commands import (
|
||||
repro,
|
||||
root,
|
||||
stage,
|
||||
studio,
|
||||
unprotect,
|
||||
update,
|
||||
version,
|
||||
@@ -93,6 +95,8 @@ COMMANDS = [
|
||||
check_ignore,
|
||||
data,
|
||||
artifacts,
|
||||
studio,
|
||||
du,
|
||||
]
|
||||
|
||||
|
||||
|
||||
+21
-19
@@ -1,6 +1,8 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from funcy import set_in
|
||||
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
@@ -8,7 +10,7 @@ from dvc.ui import ui
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
NAME_REGEX = r"^(?P<remote>remote\.)?(?P<section>[^\.]*)\.(?P<option>[^\.]*)$"
|
||||
NAME_REGEX = r"^(?P<top>(remote|db)\.)?(?P<section>[^\.]*)\.(?P<option>[^\.]*)$"
|
||||
|
||||
|
||||
def _name_type(value):
|
||||
@@ -17,10 +19,12 @@ def _name_type(value):
|
||||
match = re.match(NAME_REGEX, value)
|
||||
if not match:
|
||||
raise argparse.ArgumentTypeError(
|
||||
"name argument should look like remote.name.option or section.option"
|
||||
"name argument should look like remote.name.option or "
|
||||
"db.name.option or section.option"
|
||||
)
|
||||
top = match.group("top")
|
||||
return (
|
||||
bool(match.group("remote")),
|
||||
top.strip(".") if top else None,
|
||||
match.group("section").lower(),
|
||||
match.group("option").lower(),
|
||||
)
|
||||
@@ -49,12 +53,11 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
logger.error("name argument is required")
|
||||
return 1
|
||||
|
||||
remote, section, opt = self.args.name
|
||||
remote_or_db, section, opt = self.args.name
|
||||
|
||||
if self.args.value is None and not self.args.unset:
|
||||
return self._get(remote, section, opt)
|
||||
|
||||
return self._set(remote, section, opt)
|
||||
return self._get(remote_or_db, section, opt)
|
||||
return self._set(remote_or_db, section, opt)
|
||||
|
||||
def _list(self):
|
||||
if any((self.args.name, self.args.value, self.args.unset)):
|
||||
@@ -75,18 +78,18 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
|
||||
return 0
|
||||
|
||||
def _get(self, remote, section, opt):
|
||||
def _get(self, remote_or_db, section, opt):
|
||||
from dvc.config import ConfigError
|
||||
|
||||
levels = self._get_appropriate_levels(self.args.level)[::-1]
|
||||
|
||||
for level in levels:
|
||||
conf = self.config.read(level)
|
||||
if remote:
|
||||
conf = conf["remote"]
|
||||
if remote_or_db:
|
||||
conf = conf[remote_or_db]
|
||||
|
||||
try:
|
||||
self._check(conf, remote, section, opt)
|
||||
self._check(conf, remote_or_db, section, opt)
|
||||
except ConfigError:
|
||||
if self.args.level:
|
||||
raise
|
||||
@@ -99,16 +102,15 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
|
||||
return 0
|
||||
|
||||
def _set(self, remote, section, opt):
|
||||
def _set(self, remote_or_db, section, opt):
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
if remote:
|
||||
conf = conf["remote"]
|
||||
if remote_or_db:
|
||||
conf = conf[remote_or_db]
|
||||
if self.args.unset:
|
||||
self._check(conf, remote, section, opt)
|
||||
self._check(conf, remote_or_db, section, opt)
|
||||
del conf[section][opt]
|
||||
else:
|
||||
self._check(conf, remote, section)
|
||||
conf[section][opt] = self.args.value
|
||||
conf.update(set_in(conf, [section, opt], self.args.value))
|
||||
|
||||
if self.args.name == "cache.type":
|
||||
logger.warning(
|
||||
@@ -119,10 +121,10 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
|
||||
return 0
|
||||
|
||||
def _check(self, conf, remote, section, opt=None):
|
||||
def _check(self, conf, remote_or_db, section, opt=None):
|
||||
from dvc.config import ConfigError
|
||||
|
||||
name = "remote" if remote else "section"
|
||||
name = remote_or_db or "section"
|
||||
if section not in conf:
|
||||
raise ConfigError(f"{name} '{section}' doesn't exist")
|
||||
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ def _collect_targets(repo, target, outs):
|
||||
targets.extend([str(out) for out in stage.outs])
|
||||
continue
|
||||
|
||||
for out in outs_trie.itervalues(prefix=repo.fs.path.parts(path)):
|
||||
for out in outs_trie.itervalues(prefix=repo.fs.parts(path)):
|
||||
targets.extend(str(out))
|
||||
|
||||
return targets
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdDU(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
from dvc.repo import Repo
|
||||
from dvc.utils.humanize import naturalsize
|
||||
|
||||
entries = Repo.du(
|
||||
self.args.url,
|
||||
self.args.path,
|
||||
rev=self.args.rev,
|
||||
summarize=self.args.summarize,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
ui.table([(naturalsize(size), path) for path, size in entries])
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
DU_HELP = "Show disk usage."
|
||||
du_parser = subparsers.add_parser(
|
||||
"du",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DU_HELP, "du"),
|
||||
help=DU_HELP,
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
du_parser.add_argument("url", help="Location of DVC repository")
|
||||
du_parser.add_argument(
|
||||
"--rev",
|
||||
nargs="?",
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
du_parser.add_argument(
|
||||
"-s",
|
||||
"--summarize",
|
||||
action="store_true",
|
||||
help="Show total disk usage.",
|
||||
)
|
||||
du_parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
help=(
|
||||
"Path to a config file that will be merged with the config "
|
||||
"in the target repository."
|
||||
),
|
||||
)
|
||||
du_parser.add_argument(
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
du_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
nargs="*",
|
||||
action=DictAction,
|
||||
help=(
|
||||
"Remote config options to merge with a remote's config (default or one "
|
||||
"specified by '--remote') in the target repository."
|
||||
),
|
||||
)
|
||||
du_parser.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
help="Path to directory within the repository",
|
||||
).complete = completion.DIR
|
||||
du_parser.set_defaults(func=CmdDU)
|
||||
@@ -86,6 +86,7 @@ class CmdGC(CmdBase):
|
||||
num=self.args.num,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
dry=self.args.dry,
|
||||
skip_failed=self.args.skip_failed,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -188,6 +189,12 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Remote storage to collect garbage in",
|
||||
metavar="<name>",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"--skip-failed",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Skip revisions that fail when collected.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
|
||||
@@ -1,13 +1,68 @@
|
||||
import argparse
|
||||
|
||||
from funcy import compact, merge
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
class CmdTestDb(CmdBase):
|
||||
def run(self):
|
||||
from dvc.database import get_client
|
||||
from dvc.database.dbt_utils import DBT_PROJECT_FILE, is_dbt_project
|
||||
from dvc.dependency.db import _get_dbt_config
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
connection = self.args.connection
|
||||
|
||||
db_config = self.repo.config.get("db", {})
|
||||
config = db_config.get(connection, {})
|
||||
if connection and not config:
|
||||
raise DvcException(f"connection {connection} not found in config")
|
||||
|
||||
cli_config = compact(
|
||||
{
|
||||
"url": self.args.url,
|
||||
"username": self.args.username,
|
||||
"password": self.args.password,
|
||||
}
|
||||
)
|
||||
conn_config = merge(config, cli_config)
|
||||
|
||||
cli_dbt_config = {"profile": self.args.profile, "target": self.args.target}
|
||||
dbt_config = merge(_get_dbt_config(self.repo.config), cli_dbt_config)
|
||||
|
||||
project_dir = self.repo.root_dir
|
||||
if not (conn_config or dbt_config):
|
||||
if is_dbt_project(project_dir):
|
||||
ui.write("Using", DBT_PROJECT_FILE, "for testing", styled=True)
|
||||
else:
|
||||
raise DvcException(
|
||||
f"no config set and {DBT_PROJECT_FILE} is missing; "
|
||||
"provide arguments or set a configuration"
|
||||
)
|
||||
|
||||
adapter = get_client(conn_config, project_dir=project_dir, **dbt_config)
|
||||
with adapter as db:
|
||||
ui.write(f"Testing with {db}", styled=True)
|
||||
|
||||
creds = getattr(db, "creds", {})
|
||||
for k, v in creds.items():
|
||||
ui.write("\t", f"{k}:", v, styled=True)
|
||||
|
||||
if creds:
|
||||
ui.write()
|
||||
|
||||
db.test_connection()
|
||||
|
||||
ui.write("Connection successful", styled=True)
|
||||
|
||||
|
||||
class CmdImportDb(CmdBase):
|
||||
def run(self):
|
||||
if not (self.args.sql or self.args.model):
|
||||
@@ -24,6 +79,7 @@ class CmdImportDb(CmdBase):
|
||||
output_format=self.args.output_format,
|
||||
out=self.args.out,
|
||||
force=self.args.force,
|
||||
connection=self.args.connection,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -86,5 +142,26 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Override destination file or folder if exists.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--conn",
|
||||
dest="connection",
|
||||
nargs="?",
|
||||
help="Database connection to use, needs to be set in config",
|
||||
)
|
||||
|
||||
import_parser.set_defaults(func=CmdImportDb)
|
||||
|
||||
TEST_DB_HELP = "Test the database connection"
|
||||
test_db_parser = subparsers.add_parser(
|
||||
"test-db",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(TEST_DB_HELP, "test-db"),
|
||||
add_help=False,
|
||||
)
|
||||
test_db_parser.add_argument("--conn", dest="connection")
|
||||
test_db_parser.add_argument("--url")
|
||||
test_db_parser.add_argument("--password")
|
||||
test_db_parser.add_argument("--username")
|
||||
test_db_parser.add_argument("--profile")
|
||||
test_db_parser.add_argument("--target")
|
||||
test_db_parser.set_defaults(func=CmdTestDb)
|
||||
|
||||
+1
-45
@@ -59,49 +59,6 @@ def _show_json(
|
||||
ui.write_json(compact({"errors": all_errors, "data": data}), highlight=False)
|
||||
|
||||
|
||||
def _adjust_vega_renderers(renderers):
|
||||
from dvc.render import REVISION_FIELD, VERSION_FIELD
|
||||
from dvc_render import VegaRenderer
|
||||
|
||||
for r in renderers:
|
||||
if isinstance(r, VegaRenderer):
|
||||
if _data_versions_count(r) > 1:
|
||||
summary = _summarize_version_infos(r)
|
||||
for dp in r.datapoints:
|
||||
vi = dp.pop(VERSION_FIELD, {})
|
||||
keys = list(vi.keys())
|
||||
for key in keys:
|
||||
if not (len(summary.get(key, set())) > 1):
|
||||
vi.pop(key)
|
||||
if vi:
|
||||
dp["rev"] = "::".join(vi.values())
|
||||
else:
|
||||
for dp in r.datapoints:
|
||||
dp[REVISION_FIELD] = dp[VERSION_FIELD]["revision"]
|
||||
dp.pop(VERSION_FIELD, {})
|
||||
|
||||
|
||||
def _summarize_version_infos(renderer):
|
||||
from collections import defaultdict
|
||||
|
||||
from dvc.render import VERSION_FIELD
|
||||
|
||||
result = defaultdict(set)
|
||||
|
||||
for dp in renderer.datapoints:
|
||||
for key, value in dp.get(VERSION_FIELD, {}).items():
|
||||
result[key].add(value)
|
||||
return dict(result)
|
||||
|
||||
|
||||
def _data_versions_count(renderer):
|
||||
from itertools import product
|
||||
|
||||
summary = _summarize_version_infos(renderer)
|
||||
x = product(summary.get("filename", {None}), summary.get("field", {None}))
|
||||
return len(set(x))
|
||||
|
||||
|
||||
class CmdPlots(CmdBase):
|
||||
def _func(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
@@ -175,11 +132,10 @@ class CmdPlots(CmdBase):
|
||||
return 0
|
||||
|
||||
renderers = [r.renderer for r in renderers_with_errors]
|
||||
_adjust_vega_renderers(renderers)
|
||||
if self.args.show_vega:
|
||||
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
|
||||
if renderer:
|
||||
ui.write_json(renderer.get_filled_template(as_string=False))
|
||||
ui.write_json(renderer.get_filled_template())
|
||||
return 0
|
||||
|
||||
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from funcy import get_in
|
||||
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
from dvc.commands.config import CmdConfig
|
||||
from dvc.log import logger
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
class CmdStudioLogin(CmdConfig):
|
||||
def run(self):
|
||||
from dvc_studio_client.auth import StudioAuthError, get_access_token
|
||||
|
||||
from dvc.env import DVC_STUDIO_URL
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.studio import STUDIO_URL
|
||||
|
||||
name = self.args.name
|
||||
hostname = self.args.hostname or os.environ.get(DVC_STUDIO_URL) or STUDIO_URL
|
||||
scopes = self.args.scopes
|
||||
|
||||
try:
|
||||
token_name, access_token = get_access_token(
|
||||
token_name=name,
|
||||
hostname=hostname,
|
||||
scopes=scopes,
|
||||
use_device_code=self.args.no_open,
|
||||
client_name="DVC",
|
||||
)
|
||||
except StudioAuthError as e:
|
||||
ui.error_write(str(e))
|
||||
return 1
|
||||
|
||||
self.save_config(hostname, access_token)
|
||||
ui.write(
|
||||
"Authentication has been successfully completed."
|
||||
"The generated token will now be accessible as"
|
||||
f" {token_name} in the user's Studio profile."
|
||||
)
|
||||
return 0
|
||||
|
||||
def save_config(self, hostname, token):
|
||||
with self.config.edit("global") as conf:
|
||||
conf["studio"]["token"] = token
|
||||
conf["studio"]["url"] = hostname
|
||||
|
||||
|
||||
class CmdStudioLogout(CmdConfig):
|
||||
def run(self):
|
||||
from dvc.ui import ui
|
||||
|
||||
with self.config.edit("global") as conf:
|
||||
if not get_in(conf, ["studio", "token"]):
|
||||
ui.error_write("Not logged in to Studio.")
|
||||
return 1
|
||||
|
||||
del conf["studio"]["token"]
|
||||
|
||||
ui.write("Logged out from Studio")
|
||||
return 0
|
||||
|
||||
|
||||
class CmdStudioToken(CmdConfig):
|
||||
def run(self):
|
||||
from dvc.ui import ui
|
||||
|
||||
conf = self.config.read("global")
|
||||
token = get_in(conf, ["studio", "token"])
|
||||
if not token:
|
||||
ui.error_write("Not logged in to Studio.")
|
||||
return 1
|
||||
|
||||
ui.write(token)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
STUDIO_HELP = "Commands to authenticate DVC with Iterative Studio"
|
||||
STUDIO_DESCRIPTION = (
|
||||
"Authenticate DVC with Studio and set the token."
|
||||
" Once this token has been properly configured,\n"
|
||||
" DVC will utilize it for seamlessly sharing live experiments\n"
|
||||
" and sending notifications to Studio regarding any experiments"
|
||||
" that have been pushed."
|
||||
)
|
||||
|
||||
studio_parser = subparsers.add_parser(
|
||||
"studio",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(STUDIO_DESCRIPTION, "studio"),
|
||||
help=STUDIO_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
studio_subparser = studio_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use `DVC studio CMD --help` to display command-specific help.",
|
||||
)
|
||||
fix_subparsers(studio_subparser)
|
||||
|
||||
STUDIO_LOGIN_HELP = "Authenticate DVC with Studio host"
|
||||
STUDIO_LOGIN_DESCRIPTION = (
|
||||
"By default, this command authenticates the DVC with Studio\n"
|
||||
" using default scopes and assigns a random name as the token name."
|
||||
)
|
||||
login_parser = studio_subparser.add_parser(
|
||||
"login",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(STUDIO_LOGIN_DESCRIPTION, "studio/login"),
|
||||
help=STUDIO_LOGIN_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
login_parser.add_argument(
|
||||
"-H",
|
||||
"--hostname",
|
||||
action="store",
|
||||
default=None,
|
||||
help="The hostname of the Studio instance to authenticate with.",
|
||||
)
|
||||
login_parser.add_argument(
|
||||
"-s",
|
||||
"--scopes",
|
||||
action="store",
|
||||
default=None,
|
||||
help="The scopes for the authentication token. ",
|
||||
)
|
||||
|
||||
login_parser.add_argument(
|
||||
"-n",
|
||||
"--name",
|
||||
action="store",
|
||||
default=None,
|
||||
help="The name of the authentication token. It will be used to\n"
|
||||
"identify token shown in Studio profile.",
|
||||
)
|
||||
|
||||
login_parser.add_argument(
|
||||
"--no-open",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Use authentication flow based on user code.\n"
|
||||
"You will be presented with user code to enter in browser.\n"
|
||||
"DVC will also use this if it cannot launch browser on your behalf.",
|
||||
)
|
||||
login_parser.set_defaults(func=CmdStudioLogin)
|
||||
|
||||
STUDIO_LOGOUT_HELP = "Logout user from Studio"
|
||||
STUDIO_LOGOUT_DESCRIPTION = (
|
||||
"This removes the studio token from your global config.\n"
|
||||
)
|
||||
|
||||
logout_parser = studio_subparser.add_parser(
|
||||
"logout",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(STUDIO_LOGOUT_DESCRIPTION, "studio/logout"),
|
||||
help=STUDIO_LOGOUT_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
logout_parser.set_defaults(func=CmdStudioLogout)
|
||||
|
||||
STUDIO_TOKEN_HELP = "View the token dvc uses to contact Studio" # noqa: S105 # nosec B105
|
||||
|
||||
logout_parser = studio_subparser.add_parser(
|
||||
"token",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(STUDIO_TOKEN_HELP, "studio/token"),
|
||||
help=STUDIO_TOKEN_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
logout_parser.set_defaults(func=CmdStudioToken)
|
||||
+11
-11
@@ -14,7 +14,7 @@ from .utils.objects import cached_property
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
from dvc.types import DictStrAny, StrPath
|
||||
from dvc.types import DictStrAny
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
@@ -89,8 +89,8 @@ class Config(dict):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dvc_dir: Optional["StrPath"] = None,
|
||||
local_dvc_dir: Optional["StrPath"] = None,
|
||||
dvc_dir: Optional[str] = None,
|
||||
local_dvc_dir: Optional[str] = None,
|
||||
validate: bool = True,
|
||||
fs: Optional["FileSystem"] = None,
|
||||
config: Optional["DictStrAny"] = None,
|
||||
@@ -105,7 +105,7 @@ class Config(dict):
|
||||
self.fs = fs or self.wfs
|
||||
|
||||
if dvc_dir:
|
||||
self.dvc_dir = self.fs.path.abspath(dvc_dir)
|
||||
self.dvc_dir = self.fs.abspath(dvc_dir)
|
||||
|
||||
self.local_dvc_dir = local_dvc_dir
|
||||
if not fs and not local_dvc_dir:
|
||||
@@ -145,10 +145,10 @@ class Config(dict):
|
||||
}
|
||||
|
||||
if self.dvc_dir is not None:
|
||||
files["repo"] = self.fs.path.join(self.dvc_dir, self.CONFIG)
|
||||
files["repo"] = self.fs.join(self.dvc_dir, self.CONFIG)
|
||||
|
||||
if self.local_dvc_dir is not None:
|
||||
files["local"] = self.wfs.path.join(self.local_dvc_dir, self.CONFIG_LOCAL)
|
||||
files["local"] = self.wfs.join(self.local_dvc_dir, self.CONFIG_LOCAL)
|
||||
|
||||
return files
|
||||
|
||||
@@ -302,11 +302,11 @@ class Config(dict):
|
||||
return path.def_path
|
||||
|
||||
if os.path.expanduser(path) != path:
|
||||
return localfs.path.as_posix(path)
|
||||
return localfs.as_posix(path)
|
||||
|
||||
if isinstance(path, RelPath) or not os.path.isabs(path):
|
||||
path = relpath(path, conf_dir)
|
||||
return localfs.path.as_posix(path)
|
||||
return localfs.as_posix(path)
|
||||
|
||||
return path
|
||||
|
||||
@@ -390,10 +390,10 @@ class Config(dict):
|
||||
|
||||
|
||||
def _parse_named(conf):
|
||||
result: Dict[str, Dict] = {"remote": {}, "machine": {}}
|
||||
result: Dict[str, Dict] = {"remote": {}, "machine": {}, "db": {}}
|
||||
|
||||
for section, val in conf.items():
|
||||
match = re_find(r'^\s*(remote|machine)\s*"(.*)"\s*$', section)
|
||||
match = re_find(r'^\s*(remote|machine|db)\s*"(.*)"\s*$', section)
|
||||
if match:
|
||||
key, name = match
|
||||
result[key][name] = val
|
||||
@@ -408,7 +408,7 @@ def _pack_named(conf):
|
||||
result = compact(conf)
|
||||
|
||||
# Transform remote.name -> 'remote "name"'
|
||||
for key in ("remote", "machine"):
|
||||
for key in ("remote", "machine", "db"):
|
||||
for name, val in conf[key].items():
|
||||
result[f'{key} "{name}"'] = val
|
||||
result.pop(key, None)
|
||||
|
||||
+12
-2
@@ -240,6 +240,9 @@ SCHEMA = {
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"token": str,
|
||||
"use_https": Bool,
|
||||
"user": str,
|
||||
"password": str,
|
||||
"data_proxy_target": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
@@ -318,8 +321,8 @@ SCHEMA = {
|
||||
"feature": FeatureSchema(
|
||||
{
|
||||
Optional("machine", default=False): Bool,
|
||||
"db_profile": str,
|
||||
"db_target": str,
|
||||
"dbt_profile": str,
|
||||
"dbt_target": str,
|
||||
},
|
||||
),
|
||||
"plots": {
|
||||
@@ -352,4 +355,11 @@ SCHEMA = {
|
||||
"repo_url": str,
|
||||
Optional("offline", default=False): Bool,
|
||||
},
|
||||
"db": {
|
||||
str: {
|
||||
"url": str,
|
||||
"username": str,
|
||||
"password": str,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+8
-7
@@ -3,7 +3,6 @@
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, Set, Tuple
|
||||
|
||||
from dvc.config import NoRemoteError, RemoteConfigError
|
||||
from dvc.fs.callbacks import Callback
|
||||
from dvc.log import logger
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_data.hashfile.db import get_index
|
||||
@@ -36,11 +35,9 @@ class Remote:
|
||||
|
||||
path = self.path
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(
|
||||
path, ".dvc", CacheManager.FILES_DIR, DEFAULT_ALGORITHM
|
||||
)
|
||||
path = self.fs.join(path, ".dvc", CacheManager.FILES_DIR, DEFAULT_ALGORITHM)
|
||||
else:
|
||||
path = self.fs.path.join(path, CacheManager.FILES_DIR, DEFAULT_ALGORITHM)
|
||||
path = self.fs.join(path, CacheManager.FILES_DIR, DEFAULT_ALGORITHM)
|
||||
return get_odb(self.fs, path, hash_name=DEFAULT_ALGORITHM, **self.config)
|
||||
|
||||
@cached_property
|
||||
@@ -206,11 +203,13 @@ class DataCloud:
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
) -> "TransferResult":
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
with Callback.as_tqdm_callback(
|
||||
with TqdmCallback(
|
||||
desc=f"Pushing to {odb.fs.unstrip_protocol(odb.path)}",
|
||||
unit="file",
|
||||
) as cb:
|
||||
@@ -265,11 +264,13 @@ class DataCloud:
|
||||
jobs: Optional[int] = None,
|
||||
odb: "HashFileDB",
|
||||
) -> "TransferResult":
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
|
||||
if odb.hash_name == "md5-dos2unix":
|
||||
cache = self.repo.cache.legacy
|
||||
else:
|
||||
cache = self.repo.cache.local
|
||||
with Callback.as_tqdm_callback(
|
||||
with TqdmCallback(
|
||||
desc=f"Fetching from {odb.fs.unstrip_protocol(odb.path)}",
|
||||
unit="file",
|
||||
) as cb:
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import Any, ContextManager, Optional, Union
|
||||
|
||||
from . import dbt_query, sqla
|
||||
from .dbt_models import get_model
|
||||
from .serializer import export
|
||||
|
||||
Client = Union[sqla.SQLAlchemyClient, dbt_query.DbtClient]
|
||||
|
||||
|
||||
def get_client(
|
||||
config,
|
||||
project_dir: Optional[str] = None,
|
||||
profile: Optional[str] = None,
|
||||
target: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> "ContextManager[Client]":
|
||||
if config:
|
||||
return sqla.client(config, **kwargs)
|
||||
return dbt_query.client(project_dir=project_dir, profile=profile, target=target)
|
||||
|
||||
|
||||
__all__ = ["export", "get_client", "get_model", "Client"]
|
||||
@@ -0,0 +1,40 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from dvc.log import logger
|
||||
|
||||
from .dbt_runner import dbt_show
|
||||
from .dbt_utils import check_dbt
|
||||
from .serializer import AgateSerializer
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
def ref(
|
||||
name: str,
|
||||
package: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
) -> str:
|
||||
parts: List[str] = []
|
||||
if package:
|
||||
parts.append(repr(package))
|
||||
|
||||
parts.append(repr(name))
|
||||
if version:
|
||||
parts.append(f"{version=}")
|
||||
|
||||
inner = ",".join(parts)
|
||||
return "{{ ref(" + inner + ") }}"
|
||||
|
||||
|
||||
@check_dbt("model")
|
||||
def get_model(
|
||||
name: str,
|
||||
package: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
profile: Optional[str] = None,
|
||||
target: Optional[str] = None,
|
||||
) -> AgateSerializer:
|
||||
model = ref(name, package, version=version)
|
||||
q = f"select * from {model}" # noqa: S608
|
||||
table = dbt_show(inline=q, profile=profile, target=target)
|
||||
return AgateSerializer(table)
|
||||
@@ -0,0 +1,88 @@
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Union
|
||||
|
||||
from attrs import define, field
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
|
||||
from .dbt_utils import check_dbt
|
||||
from .serializer import AgateSerializer, PandasSQLSerializer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dbt.adapters.base.impl import BaseAdapter
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
@define
|
||||
class DbtClient:
|
||||
adapter: "BaseAdapter" = field(repr=lambda o: type(o).__qualname__)
|
||||
creds: Dict[str, Any] = field(repr=False)
|
||||
|
||||
@contextmanager
|
||||
def query(self, sql: str) -> Iterator[Union[AgateSerializer, PandasSQLSerializer]]:
|
||||
from dbt.adapters.sql import SQLAdapter
|
||||
|
||||
with self.adapter.connection_named("execute"):
|
||||
if isinstance(self.adapter, SQLAdapter):
|
||||
# try to use connection to stream results
|
||||
# otherwise fallback to fetch all at once
|
||||
try:
|
||||
connection = self.adapter.connections.get_thread_connection().handle
|
||||
except Exception: # noqa: BLE001
|
||||
logger.debug("", exc_info=True)
|
||||
else:
|
||||
# pandas raises warning that it does not support
|
||||
# dbapi2-based raw connection, but it does seem to work.
|
||||
yield PandasSQLSerializer(sql, connection)
|
||||
return
|
||||
_, table = self.adapter.execute(sql, fetch=True)
|
||||
yield AgateSerializer(table)
|
||||
|
||||
def test_connection(self, onerror: Optional[Callable[[], Any]] = None) -> None:
|
||||
with self.adapter.connection_named("debug"):
|
||||
try:
|
||||
self.adapter.debug_query()
|
||||
except Exception as exc:
|
||||
if callable(onerror):
|
||||
onerror()
|
||||
|
||||
logger.exception(
|
||||
"dvc was unable to connect to the specified database. "
|
||||
"Please check your database credentials and try again.",
|
||||
exc_info=False,
|
||||
)
|
||||
raise DvcException("The database returned the following error") from exc
|
||||
|
||||
|
||||
@contextmanager
|
||||
@check_dbt("query")
|
||||
def client(
|
||||
project_dir: Optional[str] = None,
|
||||
profiles_dir: Optional[str] = None,
|
||||
profile: Optional[str] = None,
|
||||
target: Optional[str] = None,
|
||||
) -> Iterator["DbtClient"]:
|
||||
from dbt.adapters import factory as adapters_factory
|
||||
from dbt.adapters.base.impl import BaseAdapter
|
||||
|
||||
from .dbt_utils import get_or_build_profile, get_profiles_dir, init_dbt
|
||||
|
||||
profiles_dir = profiles_dir or get_profiles_dir(project_dir)
|
||||
flags = init_dbt(profiles_dir, os.getcwd(), target=target)
|
||||
|
||||
with flags, adapters_factory.adapter_management():
|
||||
# likely invalid connection profile or no adapter
|
||||
profile_obj = get_or_build_profile(project_dir, profile, target)
|
||||
|
||||
adapters_factory.register_adapter(profile_obj) # type: ignore[arg-type]
|
||||
adapter = adapters_factory.get_adapter(profile_obj) # type: ignore[arg-type]
|
||||
assert isinstance(adapter, BaseAdapter)
|
||||
try:
|
||||
creds = dict(profile_obj.credentials.connection_info())
|
||||
except: # noqa: E722
|
||||
creds = {}
|
||||
|
||||
yield DbtClient(adapter, creds)
|
||||
@@ -0,0 +1,61 @@
|
||||
from typing import TYPE_CHECKING, List, Optional, Sequence, Union
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agate import Table
|
||||
from dbt.contracts.results import RunResult
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
class DbtInternalError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
def _kw_to_cmd_args(**kwargs: Union[None, bool, str, int]) -> List[str]:
|
||||
args: List[str] = []
|
||||
for key, value in kwargs.items():
|
||||
key = key.replace("_", "-")
|
||||
if value is None:
|
||||
continue # skip creating a flag in this case
|
||||
if value is True:
|
||||
args.append(f"--{key}")
|
||||
elif value is False:
|
||||
args.append(f"--no-{key}")
|
||||
else:
|
||||
args.extend([f"--{key}", str(value)])
|
||||
return args
|
||||
|
||||
|
||||
def _dbt_invoke(*posargs: str, quiet: bool = True, **kw: Union[None, bool, str, int]):
|
||||
from dbt.cli.main import dbtRunner
|
||||
|
||||
args = _kw_to_cmd_args(quiet=quiet or None) # global options
|
||||
args.extend([*posargs, *_kw_to_cmd_args(**kw)])
|
||||
|
||||
runner = dbtRunner()
|
||||
result = runner.invoke(args)
|
||||
if result.success:
|
||||
return result.result
|
||||
raise DbtInternalError(f"failed to run dbt {posargs[0]}") from result.exception
|
||||
|
||||
|
||||
def dbt_show(
|
||||
inline: Optional[str] = None,
|
||||
limit: int = -1,
|
||||
profile: Optional[str] = None,
|
||||
target: Optional[str] = None,
|
||||
) -> "Table":
|
||||
from dbt.contracts.results import RunExecutionResult
|
||||
|
||||
result = _dbt_invoke(
|
||||
"show", inline=inline, limit=limit, profile=profile, target=target
|
||||
)
|
||||
assert isinstance(result, RunExecutionResult)
|
||||
|
||||
run_results: Sequence["RunResult"] = result.results
|
||||
run_result, *_ = run_results
|
||||
assert run_result.agate_table is not None
|
||||
return run_result.agate_table
|
||||
@@ -0,0 +1,100 @@
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from importlib.util import find_spec
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from funcy import cut_prefix
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
from dvc.utils import packaging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dbt.config.profile import Profile
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
DBT_PROJECT_FILE = "dbt_project.yml"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def check_dbt(action: Optional[str] = None):
|
||||
if not (find_spec("dbt") and find_spec("dbt.cli")):
|
||||
action = f" {action}" if action else ""
|
||||
raise DvcException(f"Could not run{action}. dbt-core is not installed")
|
||||
yield packaging.check_required_version(pkg="dbt-core")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def init_dbt(
|
||||
profiles_dir: str,
|
||||
project_dir: str,
|
||||
target: Optional[str] = None,
|
||||
):
|
||||
from argparse import Namespace
|
||||
|
||||
from dbt import flags
|
||||
|
||||
prev = flags.get_flags()
|
||||
try:
|
||||
args = Namespace(
|
||||
use_colors=True,
|
||||
project_dir=project_dir,
|
||||
profiles_dir=profiles_dir,
|
||||
target=target,
|
||||
)
|
||||
flags.set_from_args(args, None)
|
||||
yield flags.get_flags()
|
||||
finally:
|
||||
flags.set_flags(prev)
|
||||
|
||||
|
||||
def get_profiles_dir(project_dir: Optional[str] = None) -> str:
|
||||
from dbt.cli.resolvers import default_profiles_dir
|
||||
|
||||
if profiles_dir := os.getenv("DBT_PROFILES_DIR"):
|
||||
return profiles_dir
|
||||
if project_dir and os.path.isfile(os.path.join(project_dir, "profiles.yml")):
|
||||
return project_dir
|
||||
return os.fspath(default_profiles_dir())
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _handle_profile_parsing_error():
|
||||
from dbt.exceptions import DbtRuntimeError
|
||||
|
||||
try:
|
||||
yield
|
||||
except DbtRuntimeError as e:
|
||||
cause = e.__cause__ is not None and e.__cause__.__context__
|
||||
if isinstance(cause, ModuleNotFoundError) and (
|
||||
adapter := cut_prefix(cause.name, "dbt.adapters.")
|
||||
):
|
||||
# DbtRuntimeError is very noisy, so send it to debug
|
||||
logger.debug("", exc_info=True)
|
||||
raise DvcException(f"dbt-{adapter} dependency is missing") from cause
|
||||
raise DvcException("failed to read connection profiles") from e
|
||||
|
||||
|
||||
def is_dbt_project(project_dir: str):
|
||||
return os.path.isfile(os.path.join(project_dir, DBT_PROJECT_FILE))
|
||||
|
||||
|
||||
@_handle_profile_parsing_error()
|
||||
def get_or_build_profile(
|
||||
project_dir: Optional[str], profile: Optional[str], target: Optional[str]
|
||||
) -> "Profile":
|
||||
from dbt.config.profile import Profile
|
||||
from dbt.config.renderer import ProfileRenderer
|
||||
from dbt.config.runtime import load_profile
|
||||
|
||||
if project_dir and is_dbt_project(project_dir):
|
||||
return load_profile(
|
||||
project_dir, {}, profile_name_override=profile, target_override=target
|
||||
)
|
||||
|
||||
if not profile:
|
||||
raise DvcException("No profile specified to query from.")
|
||||
|
||||
renderer = ProfileRenderer({})
|
||||
return Profile.render(renderer, profile, target_override=target)
|
||||
@@ -0,0 +1,54 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import sqlalchemy as sa
|
||||
from agate import Table
|
||||
|
||||
|
||||
def noop(_):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class PandasSQLSerializer:
|
||||
sql: "Union[sa.TextClause, str]"
|
||||
con: "sa.Connection"
|
||||
chunksize: int = 10_000
|
||||
|
||||
def to_csv(self, file: str, progress=noop) -> None:
|
||||
import pandas as pd
|
||||
|
||||
with open(file, mode="wb") as f:
|
||||
idfs = pd.read_sql_query(self.sql, self.con, chunksize=self.chunksize)
|
||||
for i, df in enumerate(idfs):
|
||||
df.to_csv(f, header=i == 0, index=False)
|
||||
progress(len(df))
|
||||
|
||||
def to_json(self, file: str, progress=noop) -> None: # noqa: ARG002
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_sql_query(self.sql, self.con)
|
||||
df.to_json(file, orient="records")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgateSerializer:
|
||||
table: "Table"
|
||||
|
||||
def to_csv(self, file: str, progress=noop) -> None: # noqa: ARG002
|
||||
return self.table.to_csv(file)
|
||||
|
||||
def to_json(self, file: str, progress=noop) -> None: # noqa: ARG002
|
||||
return self.table.to_json(file)
|
||||
|
||||
|
||||
def export(
|
||||
serializer: Union[PandasSQLSerializer, AgateSerializer],
|
||||
file: str,
|
||||
format: str = "csv", # noqa: A002
|
||||
progress=noop,
|
||||
) -> None:
|
||||
if format == "csv":
|
||||
return serializer.to_csv(file, progress=progress)
|
||||
return serializer.to_json(file, progress=progress)
|
||||
@@ -0,0 +1,73 @@
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Dict, Iterator, Optional, Union
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.engine import URL, Engine
|
||||
from sqlalchemy.engine import make_url as _make_url
|
||||
from sqlalchemy.exc import NoSuchModuleError
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
|
||||
from .serializer import PandasSQLSerializer
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
def make_url(url: Union[URL, str], **kwargs: Any) -> URL:
|
||||
return _make_url(url).set(**kwargs)
|
||||
|
||||
|
||||
def url_from_config(config: Union[str, URL, Dict[str, str]]) -> URL:
|
||||
if isinstance(config, (str, URL)):
|
||||
return make_url(config)
|
||||
return make_url(**config)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SQLAlchemyClient:
|
||||
engine: Engine
|
||||
|
||||
@contextmanager
|
||||
def query(self, sql: str) -> Iterator[PandasSQLSerializer]:
|
||||
with self.engine.connect().execution_options(stream_results=True) as conn:
|
||||
yield PandasSQLSerializer(text(sql), conn)
|
||||
|
||||
def test_connection(self, onerror: Optional[Callable[[], Any]] = None) -> None:
|
||||
try:
|
||||
with self.engine.connect() as conn:
|
||||
conn.execute(text("select 1"))
|
||||
except Exception as exc:
|
||||
if callable(onerror):
|
||||
onerror()
|
||||
logger.exception(
|
||||
"dvc was unable to connect to the specified database. "
|
||||
"Please check your database credentials and try again.",
|
||||
exc_info=False,
|
||||
)
|
||||
raise DvcException("The database returned the following error") from exc
|
||||
|
||||
|
||||
@contextmanager
|
||||
def handle_error(url: URL):
|
||||
try:
|
||||
yield
|
||||
except (ModuleNotFoundError, NoSuchModuleError) as e:
|
||||
# TODO: write installation instructions
|
||||
raise DvcException(
|
||||
f"Could not load database driver for {url.drivername!r}"
|
||||
) from e
|
||||
|
||||
|
||||
@contextmanager
|
||||
def client(
|
||||
url_or_config: Union[str, URL, Dict[str, str]], **engine_kwargs: Any
|
||||
) -> Iterator[SQLAlchemyClient]:
|
||||
url = url_from_config(url_or_config)
|
||||
with handle_error(url):
|
||||
engine = create_engine(url, **engine_kwargs)
|
||||
try:
|
||||
yield SQLAlchemyClient(engine)
|
||||
finally:
|
||||
engine.dispose()
|
||||
@@ -33,7 +33,7 @@ class Dependency(Output):
|
||||
if self.fs.version_aware:
|
||||
old_fs_path = self.fs_path
|
||||
try:
|
||||
self.fs_path = self.fs.path.version_path(self.fs_path, None)
|
||||
self.fs_path = self.fs.version_path(self.fs_path, None)
|
||||
if self.changed_meta():
|
||||
return {str(self): "update available"}
|
||||
finally:
|
||||
@@ -42,9 +42,9 @@ class Dependency(Output):
|
||||
|
||||
def update(self, rev=None):
|
||||
if self.fs.version_aware:
|
||||
self.fs_path = self.fs.path.version_path(self.fs_path, rev)
|
||||
self.fs_path = self.fs.version_path(self.fs_path, rev)
|
||||
self.meta = self.get_meta()
|
||||
self.fs_path = self.fs.path.version_path(self.fs_path, self.meta.version_id)
|
||||
self.fs_path = self.fs.version_path(self.fs_path, self.meta.version_id)
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
|
||||
@@ -52,7 +52,7 @@ class Dependency(Output):
|
||||
def save(self):
|
||||
super().save()
|
||||
if self.fs.version_aware:
|
||||
self.fs_path = self.fs.path.version_path(self.fs_path, self.meta.version_id)
|
||||
self.fs_path = self.fs.version_path(self.fs_path, self.meta.version_id)
|
||||
|
||||
def dumpd(self, **kwargs):
|
||||
if self.fs.version_aware:
|
||||
|
||||
+83
-47
@@ -1,8 +1,8 @@
|
||||
import os
|
||||
from contextlib import contextmanager, nullcontext
|
||||
from typing import TYPE_CHECKING, Any, Dict, Iterator, Union
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Union
|
||||
|
||||
from funcy import compact
|
||||
from funcy import compact, log_durations
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
@@ -11,9 +11,9 @@ from dvc.scm import SCM
|
||||
from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agate import Table
|
||||
from rich.status import Status
|
||||
|
||||
from dvc.output import Output
|
||||
from dvc.stage import Stage
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
@@ -24,20 +24,23 @@ PARAM_PROFILE = "profile"
|
||||
PARAM_FILE_FORMAT = "file_format"
|
||||
|
||||
|
||||
def _get_db_config(config: Dict) -> Dict:
|
||||
def _get_dbt_config(config: Dict) -> Dict:
|
||||
conf = config.get("feature", {})
|
||||
pref = "db_"
|
||||
pref = "dbt_"
|
||||
return {k.lstrip(pref): v for k, v in conf.items() if k.startswith(pref)}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def log_status(msg, log=logger.debug) -> Iterator["Status"]:
|
||||
def log_status(
|
||||
msg, status: Optional["Status"] = None, log=logger.debug
|
||||
) -> Iterator["Status"]:
|
||||
from funcy import log_durations
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
with log_durations(log, msg), ui.status(msg) as status:
|
||||
yield status
|
||||
with log_durations(log, msg), status or ui.status(msg) as st:
|
||||
st.update(msg)
|
||||
yield st
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -50,15 +53,23 @@ def chdir(path):
|
||||
os.chdir(wdir)
|
||||
|
||||
|
||||
def export_to(table: "Table", to: str, file_format: str = "csv") -> None:
|
||||
exporter = {"csv": table.to_csv, "json": table.to_json}
|
||||
return exporter[file_format](to)
|
||||
@contextmanager
|
||||
def download_progress(to: "Output") -> Iterator[Callable[[int], Any]]:
|
||||
from dvc.ui import ui
|
||||
from dvc.ui._rich_progress import DbDownloadProgress
|
||||
|
||||
with log_durations(logger.debug, f"Saving to {to}"), DbDownloadProgress(
|
||||
console=ui.error_console,
|
||||
) as progress:
|
||||
task = progress.add_task("Saving", total=None, output=to)
|
||||
yield lambda n: progress.advance(task, advance=n)
|
||||
progress.update(task, description="Saved", total=0)
|
||||
|
||||
|
||||
class AbstractDependency(Dependency):
|
||||
"""Dependency without workspace/fs/fs_path"""
|
||||
|
||||
def __init__(self, stage: "Stage", info, *args, **kwargs):
|
||||
def __init__(self, stage: "Stage", info: Dict[str, Any], *args, **kwargs):
|
||||
self.repo = stage.repo
|
||||
self.stage = stage
|
||||
self.fs = None
|
||||
@@ -69,7 +80,8 @@ class AbstractDependency(Dependency):
|
||||
|
||||
class DbDependency(AbstractDependency):
|
||||
PARAM_QUERY = "query"
|
||||
QUERY_SCHEMA = {PARAM_QUERY: str}
|
||||
PARAM_CONNECTION = "connection"
|
||||
QUERY_SCHEMA = {PARAM_QUERY: str, PARAM_CONNECTION: str}
|
||||
|
||||
def __init__(self, stage: "Stage", info, *args, **kwargs):
|
||||
super().__init__(stage, info, *args, **kwargs)
|
||||
@@ -103,33 +115,47 @@ class DbDependency(AbstractDependency):
|
||||
def update(self, rev=None):
|
||||
"""nothing to update."""
|
||||
|
||||
def download(self, to, jobs=None, file_format=None): # noqa: ARG002
|
||||
def download(
|
||||
self,
|
||||
to: "Output",
|
||||
jobs: Optional[int] = None, # noqa: ARG002
|
||||
file_format: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
from dvc.database import export, get_client
|
||||
|
||||
db_info = self.info.get(PARAM_DB, {})
|
||||
query = db_info.get(self.PARAM_QUERY)
|
||||
if not query:
|
||||
raise DvcException("Cannot download: no query specified")
|
||||
|
||||
from dvc.utils.db import _check_dbt, _profiles_dir, execute_sql
|
||||
dbt_config = _get_dbt_config(self.repo.config)
|
||||
profile = db_info.get(PARAM_PROFILE) or dbt_config.get(PARAM_PROFILE)
|
||||
target = self.target or dbt_config.get("target")
|
||||
|
||||
db_config = _get_db_config(self.repo.config)
|
||||
profile = db_info.get(PARAM_PROFILE) or db_config.get(PARAM_PROFILE)
|
||||
target = self.target or db_config.get("target")
|
||||
file_format = file_format or db_info.get(PARAM_FILE_FORMAT, "csv")
|
||||
connection = db_info.get(self.PARAM_CONNECTION)
|
||||
db_config = self.repo.config.get("db", {})
|
||||
config = db_config.get(connection)
|
||||
if connection and not config:
|
||||
raise DvcException(f"connection {connection} not found in config")
|
||||
|
||||
_check_dbt(self.PARAM_QUERY)
|
||||
profiles_dir = _profiles_dir(self.repo.root_dir)
|
||||
with log_status("Executing query") as status:
|
||||
table = execute_sql(
|
||||
query,
|
||||
profiles_dir,
|
||||
self.repo.root_dir,
|
||||
profile,
|
||||
target=target,
|
||||
status=status,
|
||||
)
|
||||
# NOTE: we keep everything in memory, and then export it out later.
|
||||
with log_status(f"Saving to {to}"):
|
||||
return export_to(table, to.fs_path, file_format)
|
||||
project_dir = self.repo.root_dir
|
||||
with get_client(
|
||||
config, project_dir=project_dir, profile=profile, target=target
|
||||
) as db:
|
||||
logger.debug("using adapter: %s", db)
|
||||
with log_status("Testing connection") as status:
|
||||
db.test_connection(onerror=status.stop)
|
||||
|
||||
file_format = file_format or db_info.get(PARAM_FILE_FORMAT, "csv")
|
||||
assert file_format
|
||||
with log_status("Executing query") as status, db.query(query) as serializer:
|
||||
status.stop()
|
||||
logger.debug("using serializer: %s", serializer)
|
||||
with download_progress(to) as progress:
|
||||
return export(
|
||||
serializer, to.fs_path, format=file_format, progress=progress
|
||||
)
|
||||
|
||||
|
||||
class DbtDependency(AbstractDependency):
|
||||
@@ -237,9 +263,12 @@ class DbtDependency(AbstractDependency):
|
||||
rev = self.rev
|
||||
self.def_repo[RepoDependency.PARAM_REV_LOCK] = self._get_clone(rev).get_rev()
|
||||
|
||||
def download(self, to, jobs=None, file_format=None): # noqa: ARG002
|
||||
from dvc.ui import ui
|
||||
|
||||
def download(
|
||||
self,
|
||||
to: "Output",
|
||||
jobs: Optional[int] = None, # noqa: ARG002
|
||||
file_format: Optional[str] = None,
|
||||
) -> None:
|
||||
from .repo import RepoDependency
|
||||
|
||||
project_dir = self.info.get(PARAM_DB, {}).get(self.PARAM_PROJECT_DIR, "")
|
||||
@@ -255,27 +284,34 @@ class DbtDependency(AbstractDependency):
|
||||
project_path = os.path.join(wdir, project_dir) if project_dir else root
|
||||
with ctx, chdir(project_path):
|
||||
self._download_db(to, file_format=file_format)
|
||||
ui.write(f"Saved file to {to}", styled=True)
|
||||
|
||||
def _download_db(self, to, version=None, file_format=None):
|
||||
from dvc.utils.db import get_model
|
||||
def _download_db(
|
||||
self,
|
||||
to: "Output",
|
||||
version: Optional[int] = None,
|
||||
file_format: Optional[str] = None,
|
||||
) -> None:
|
||||
from dvc.database import export, get_model
|
||||
|
||||
db_info = self.info.get(PARAM_DB, {})
|
||||
model = db_info.get(self.PARAM_MODEL)
|
||||
version = version or db_info.get(self.PARAM_VERSION)
|
||||
file_format = file_format or db_info.get(PARAM_FILE_FORMAT) or "csv"
|
||||
assert file_format
|
||||
if not model:
|
||||
raise DvcException("Cannot download, no model specified")
|
||||
|
||||
db_config = _get_db_config(self.repo.config)
|
||||
version = version or db_info.get(self.PARAM_VERSION)
|
||||
profile = db_info.get(PARAM_PROFILE) or db_config.get(PARAM_PROFILE)
|
||||
target = self.target or db_info.get("target")
|
||||
file_format = file_format or db_info.get(PARAM_FILE_FORMAT, "csv")
|
||||
dbt_config = _get_dbt_config(self.repo.config)
|
||||
profile = db_info.get(PARAM_PROFILE) or dbt_config.get(PARAM_PROFILE)
|
||||
target = self.target or db_info.get("target") or dbt_config.get("target")
|
||||
|
||||
with log_status("Downloading model"):
|
||||
table = get_model(model, version=version, profile=profile, target=target)
|
||||
serializer = get_model(
|
||||
model, version=version, profile=profile, target=target
|
||||
)
|
||||
# NOTE: we keep everything in memory, and then export it out later.
|
||||
with log_status(f"Saving to {to}"):
|
||||
export_to(table, to.fs_path, file_format=file_format)
|
||||
with download_progress(to) as progress:
|
||||
export(serializer, to.fs_path, format=file_format, progress=progress)
|
||||
|
||||
|
||||
DB_SCHEMA = {
|
||||
|
||||
@@ -54,12 +54,15 @@ def read_param_file(
|
||||
continue
|
||||
return ret
|
||||
|
||||
from dpath import merge
|
||||
from copy import deepcopy
|
||||
|
||||
for key_path in key_paths:
|
||||
from dpath import merge
|
||||
from funcy import distinct
|
||||
|
||||
for key_path in distinct(key_paths):
|
||||
merge(
|
||||
ret,
|
||||
dpath.search(config, key_path, separator="."),
|
||||
deepcopy(dpath.search(config, key_path, separator=".")),
|
||||
separator=".",
|
||||
)
|
||||
return ret
|
||||
|
||||
+1
-1
@@ -294,7 +294,7 @@ class ProjectFile(FileMixin):
|
||||
def resolver(self) -> "DataResolver":
|
||||
from .parsing import DataResolver
|
||||
|
||||
wdir = self.repo.fs.path.parent(self.path)
|
||||
wdir = self.repo.fs.parent(self.path)
|
||||
return DataResolver(self.repo, wdir, self.contents)
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -357,3 +357,15 @@ class ArtifactNotFoundError(DvcException):
|
||||
|
||||
desc = f" @ {stage or version}" if (stage or version) else ""
|
||||
super().__init__(f"Unable to find artifact '{name}{desc}'")
|
||||
|
||||
|
||||
class RevCollectionError(DvcException):
|
||||
"""Thrown if a revision failed to be collected.
|
||||
|
||||
Args:
|
||||
rev (str): revision that failed (or "workspace").
|
||||
"""
|
||||
|
||||
def __init__(self, rev):
|
||||
self.rev = rev
|
||||
super().__init__(f"Failed to collect '{rev}'")
|
||||
|
||||
+14
-10
@@ -5,6 +5,8 @@ from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
||||
|
||||
from dvc.config import ConfigError as RepoConfigError
|
||||
from dvc.config_schema import SCHEMA, Invalid
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from dvc_objects.fs import ( # noqa: F401
|
||||
LocalFileSystem,
|
||||
MemoryFileSystem,
|
||||
@@ -23,11 +25,10 @@ from dvc_objects.fs.errors import ( # noqa: F401
|
||||
ConfigError,
|
||||
RemoteMissingDepsError,
|
||||
)
|
||||
from dvc_objects.fs.path import Path # noqa: F401
|
||||
|
||||
from .callbacks import Callback
|
||||
from .callbacks import Callback # noqa: F401
|
||||
from .data import DataFileSystem # noqa: F401
|
||||
from .dvc import DVCFileSystem # noqa: F401
|
||||
from .dvc import DVCFileSystem
|
||||
from .git import GitFileSystem # noqa: F401
|
||||
|
||||
known_implementations.update(
|
||||
@@ -47,29 +48,32 @@ known_implementations.update(
|
||||
def download(
|
||||
fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
|
||||
) -> int:
|
||||
with Callback.as_tqdm_callback(
|
||||
desc=f"Downloading {fs.path.name(fs_path)}",
|
||||
from dvc.scm import lfs_prefetch
|
||||
|
||||
from .callbacks import TqdmCallback
|
||||
|
||||
with TqdmCallback(
|
||||
desc=f"Downloading {fs.name(fs_path)}",
|
||||
unit="files",
|
||||
) as cb:
|
||||
# NOTE: We use dvc-objects generic.copy over fs.get since it makes file
|
||||
# download atomic and avoids fsspec glob/regex path expansion.
|
||||
if fs.isdir(fs_path):
|
||||
from_infos = [
|
||||
path
|
||||
for path in fs.find(fs_path)
|
||||
if not path.endswith(fs.path.flavour.sep)
|
||||
path for path in fs.find(fs_path) if not path.endswith(fs.flavour.sep)
|
||||
]
|
||||
if not from_infos:
|
||||
localfs.makedirs(to, exist_ok=True)
|
||||
return 0
|
||||
to_infos = [
|
||||
localfs.path.join(to, *fs.path.relparts(info, fs_path))
|
||||
for info in from_infos
|
||||
localfs.join(to, *fs.relparts(info, fs_path)) for info in from_infos
|
||||
]
|
||||
else:
|
||||
from_infos = [fs_path]
|
||||
to_infos = [to]
|
||||
|
||||
if isinstance(fs, DVCFileSystem):
|
||||
lfs_prefetch(fs, from_infos)
|
||||
cb.set_size(len(from_infos))
|
||||
jobs = jobs or fs.jobs
|
||||
generic.copy(
|
||||
|
||||
@@ -29,6 +29,9 @@ class DataFileSystem(FileSystem):
|
||||
|
||||
return _DataFileSystem(**self.fs_args)
|
||||
|
||||
def getcwd(self):
|
||||
return self.fs.getcwd()
|
||||
|
||||
def isdvc(self, path, **kwargs):
|
||||
return self.fs.isdvc(path, **kwargs)
|
||||
|
||||
|
||||
+151
-56
@@ -4,6 +4,7 @@ import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import threading
|
||||
from collections import deque
|
||||
from contextlib import ExitStack, suppress
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Union
|
||||
|
||||
@@ -12,7 +13,6 @@ from funcy import wrap_with
|
||||
|
||||
from dvc.log import logger
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
from .data import DataFileSystem
|
||||
|
||||
@@ -60,6 +60,7 @@ def _merge_info(repo, key, fs_info, dvc_info):
|
||||
if fs_info:
|
||||
ret["type"] = fs_info["type"]
|
||||
ret["size"] = fs_info["size"]
|
||||
ret["fs_info"] = fs_info
|
||||
isexec = False
|
||||
if fs_info["type"] == "file":
|
||||
isexec = utils.is_exec(fs_info["mode"])
|
||||
@@ -69,7 +70,7 @@ def _merge_info(repo, key, fs_info, dvc_info):
|
||||
|
||||
|
||||
def _get_dvc_path(dvc_fs, subkey):
|
||||
return dvc_fs.path.join(*subkey) if subkey else ""
|
||||
return dvc_fs.join(*subkey) if subkey else ""
|
||||
|
||||
|
||||
class _DVCFileSystem(AbstractFileSystem):
|
||||
@@ -129,55 +130,88 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
... rev="main",
|
||||
... )
|
||||
"""
|
||||
from pygtrie import Trie
|
||||
|
||||
super().__init__()
|
||||
self._repo = repo
|
||||
self._repo_factory = repo_factory
|
||||
self._traverse_subrepos = subrepos
|
||||
self._repo_stack = ExitStack()
|
||||
if repo is None:
|
||||
url = url if url is not None else fo
|
||||
repo = self._make_repo(
|
||||
url=url,
|
||||
rev=rev,
|
||||
subrepos=subrepos,
|
||||
config=config,
|
||||
remote=remote,
|
||||
remote_config=remote_config,
|
||||
)
|
||||
assert repo is not None
|
||||
self._repo_kwargs = {
|
||||
"url": url if url is not None else fo,
|
||||
"rev": rev,
|
||||
"subrepos": subrepos,
|
||||
"config": config,
|
||||
"remote": remote,
|
||||
"remote_config": remote_config,
|
||||
}
|
||||
|
||||
repo_factory = repo._fs_conf["repo_factory"]
|
||||
self._repo_stack.enter_context(repo)
|
||||
def getcwd(self):
|
||||
relparts: Tuple[str, ...] = ()
|
||||
assert self.repo is not None
|
||||
if self.repo.fs.isin(self.repo.fs.getcwd(), self.repo.root_dir):
|
||||
relparts = self.repo.fs.relparts(self.repo.fs.getcwd(), self.repo.root_dir)
|
||||
return self.root_marker + self.sep.join(relparts)
|
||||
|
||||
if not repo_factory:
|
||||
@classmethod
|
||||
def join(cls, *parts: str) -> str:
|
||||
return posixpath.join(*parts)
|
||||
|
||||
@classmethod
|
||||
def parts(cls, path: str) -> Tuple[str, ...]:
|
||||
ret = []
|
||||
while True:
|
||||
path, part = posixpath.split(path)
|
||||
|
||||
if part:
|
||||
ret.append(part)
|
||||
continue
|
||||
|
||||
if path:
|
||||
ret.append(path)
|
||||
|
||||
break
|
||||
|
||||
ret.reverse()
|
||||
|
||||
return tuple(ret)
|
||||
|
||||
def normpath(self, path: str) -> str:
|
||||
return posixpath.normpath(path)
|
||||
|
||||
def abspath(self, path: str) -> str:
|
||||
if not posixpath.isabs(path):
|
||||
path = self.join(self.getcwd(), path)
|
||||
return self.normpath(path)
|
||||
|
||||
def relpath(self, path: str, start: Optional[str] = None) -> str:
|
||||
if start is None:
|
||||
start = "."
|
||||
return posixpath.relpath(self.abspath(path), start=self.abspath(start))
|
||||
|
||||
def relparts(self, path: str, start: Optional[str] = None) -> Tuple[str, ...]:
|
||||
return self.parts(self.relpath(path, start=start))
|
||||
|
||||
@functools.cached_property
|
||||
def repo(self):
|
||||
if self._repo:
|
||||
return self._repo
|
||||
|
||||
repo = self._make_repo(**self._repo_kwargs)
|
||||
|
||||
self._repo_stack.enter_context(repo)
|
||||
self._repo = repo
|
||||
return repo
|
||||
|
||||
@functools.cached_property
|
||||
def repo_factory(self):
|
||||
if self._repo_factory:
|
||||
return self._repo_factory
|
||||
|
||||
if self._repo:
|
||||
from dvc.repo import Repo
|
||||
|
||||
self.repo_factory: RepoFactory = Repo
|
||||
else:
|
||||
self.repo_factory = repo_factory
|
||||
return Repo
|
||||
|
||||
def _getcwd():
|
||||
relparts: Tuple[str, ...] = ()
|
||||
assert repo is not None
|
||||
if repo.fs.path.isin(repo.fs.path.getcwd(), repo.root_dir):
|
||||
relparts = repo.fs.path.relparts(repo.fs.path.getcwd(), repo.root_dir)
|
||||
return self.root_marker + self.sep.join(relparts)
|
||||
|
||||
self.path = Path(self.sep, getcwd=_getcwd)
|
||||
self.repo = repo
|
||||
self.hash_jobs = repo.fs.hash_jobs
|
||||
self._traverse_subrepos = subrepos
|
||||
|
||||
self._subrepos_trie = Trie()
|
||||
"""Keeps track of each and every path with the corresponding repo."""
|
||||
|
||||
key = self._get_key(self.repo.root_dir)
|
||||
self._subrepos_trie[key] = repo
|
||||
|
||||
self._datafss = {}
|
||||
"""Keep a datafs instance of each repo."""
|
||||
|
||||
if hasattr(repo, "dvc_dir"):
|
||||
self._datafss[key] = DataFileSystem(index=repo.index.data["repo"])
|
||||
return self.repo._fs_conf["repo_factory"]
|
||||
|
||||
@functools.cached_property
|
||||
def fsid(self) -> str:
|
||||
@@ -192,20 +226,43 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
|
||||
def _get_key(self, path: "StrPath") -> Key:
|
||||
path = os.fspath(path)
|
||||
parts = self.repo.fs.path.relparts(path, self.repo.root_dir)
|
||||
parts = self.repo.fs.relparts(path, self.repo.root_dir)
|
||||
if parts == (os.curdir,):
|
||||
return ()
|
||||
return parts
|
||||
|
||||
@functools.cached_property
|
||||
def _subrepos_trie(self):
|
||||
"""Keeps track of each and every path with the corresponding repo."""
|
||||
|
||||
from pygtrie import Trie
|
||||
|
||||
trie = Trie()
|
||||
key = self._get_key(self.repo.root_dir)
|
||||
trie[key] = self.repo
|
||||
return trie
|
||||
|
||||
def _get_key_from_relative(self, path) -> Key:
|
||||
path = self._strip_protocol(path)
|
||||
parts = self.path.relparts(path, self.root_marker)
|
||||
parts = self.relparts(path, self.root_marker)
|
||||
if parts and parts[0] == os.curdir:
|
||||
return parts[1:]
|
||||
return parts
|
||||
|
||||
def _from_key(self, parts: Key) -> str:
|
||||
return self.repo.fs.path.join(self.repo.root_dir, *parts)
|
||||
return self.repo.fs.join(self.repo.root_dir, *parts)
|
||||
|
||||
@functools.cached_property
|
||||
def _datafss(self):
|
||||
"""Keep a datafs instance of each repo."""
|
||||
|
||||
datafss = {}
|
||||
|
||||
if hasattr(self.repo, "dvc_dir"):
|
||||
key = self._get_key(self.repo.root_dir)
|
||||
datafss[key] = DataFileSystem(index=self.repo.index.data["repo"])
|
||||
|
||||
return datafss
|
||||
|
||||
@property
|
||||
def repo_url(self):
|
||||
@@ -259,7 +316,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
repo_path = self.repo.fs.path.join(dir_path, Repo.DVC_DIR)
|
||||
repo_path = self.repo.fs.join(dir_path, Repo.DVC_DIR)
|
||||
return self.repo.fs.isdir(repo_path)
|
||||
|
||||
def _get_subrepo_info(
|
||||
@@ -313,7 +370,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
with suppress(FileNotFoundError, NotADirectoryError):
|
||||
for info in dvc_fs.ls(dvc_path, detail=True):
|
||||
dvc_infos[dvc_fs.path.name(info["name"])] = info
|
||||
dvc_infos[dvc_fs.name(info["name"])] = info
|
||||
dvc_exists = True
|
||||
|
||||
fs_exists = False
|
||||
@@ -326,7 +383,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
for info in repo.dvcignore.ls(
|
||||
fs, fs_path, detail=True, ignore_subrepos=ignore_subrepos
|
||||
):
|
||||
fs_infos[fs.path.name(info["name"])] = info
|
||||
fs_infos[fs.name(info["name"])] = info
|
||||
fs_exists = True
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
pass
|
||||
@@ -345,7 +402,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
if not dvcfiles and _is_dvc_file(name):
|
||||
continue
|
||||
|
||||
entry_path = self.path.join(path, name)
|
||||
entry_path = self.join(path, name)
|
||||
info = _merge_info(
|
||||
repo, (*subkey, name), fs_infos.get(name), dvc_infos.get(name)
|
||||
)
|
||||
@@ -393,7 +450,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
# NOTE: if some parent in fs_path turns out to be a file, it means
|
||||
# that the whole repofs branch doesn't exist.
|
||||
if dvc_info and not fs_info:
|
||||
for parent in fs.path.parents(fs_path):
|
||||
for parent in fs.parents(fs_path):
|
||||
try:
|
||||
if fs.info(parent)["type"] != "directory":
|
||||
dvc_info = None
|
||||
@@ -421,6 +478,45 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
|
||||
|
||||
def du(self, path, total=True, maxdepth=None, withdirs=False, **kwargs):
|
||||
if maxdepth is not None:
|
||||
raise NotImplementedError
|
||||
|
||||
sizes = {}
|
||||
dus = {}
|
||||
todo = deque([self.info(path)])
|
||||
while todo:
|
||||
info = todo.popleft()
|
||||
isdir = info["type"] == "directory"
|
||||
size = info["size"] or 0
|
||||
name = info["name"]
|
||||
|
||||
if not isdir:
|
||||
sizes[name] = size
|
||||
continue
|
||||
|
||||
dvc_info = info.get("dvc_info") or {}
|
||||
fs_info = info.get("fs_info")
|
||||
entry = dvc_info.get("entry")
|
||||
if (
|
||||
dvc_info
|
||||
and not fs_info
|
||||
and entry is not None
|
||||
and entry.size is not None
|
||||
):
|
||||
dus[name] = entry.size
|
||||
continue
|
||||
|
||||
if withdirs:
|
||||
sizes[name] = size
|
||||
|
||||
todo.extend(self.ls(info["name"], detail=True))
|
||||
|
||||
if total:
|
||||
return sum(sizes.values()) + sum(dus.values())
|
||||
|
||||
return sizes
|
||||
|
||||
def close(self):
|
||||
self._repo_stack.close()
|
||||
|
||||
@@ -436,6 +532,9 @@ class DVCFileSystem(FileSystem):
|
||||
def fs(self) -> "_DVCFileSystem":
|
||||
return _DVCFileSystem(**self.fs_args)
|
||||
|
||||
def getcwd(self):
|
||||
return self.fs.getcwd()
|
||||
|
||||
@property
|
||||
def fsid(self) -> str:
|
||||
return self.fs.fsid
|
||||
@@ -443,10 +542,6 @@ class DVCFileSystem(FileSystem):
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
return self.fs.isdvc(path, **kwargs)
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return self.fs.path
|
||||
|
||||
@property
|
||||
def repo(self) -> "Repo":
|
||||
return self.fs.repo
|
||||
|
||||
+5
-3
@@ -46,9 +46,11 @@ class GitFileSystem(FileSystem):
|
||||
|
||||
return FsspecGitFileSystem(**self.fs_args)
|
||||
|
||||
@functools.cached_property
|
||||
def path(self):
|
||||
return self.fs.path
|
||||
def getcwd(self):
|
||||
return self.fs.getcwd()
|
||||
|
||||
def chdir(self, path):
|
||||
self.fs.chdir(path)
|
||||
|
||||
@property
|
||||
def rev(self) -> str:
|
||||
|
||||
+21
-21
@@ -51,8 +51,8 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path, fs, name):
|
||||
assert fs.path.isabs(path)
|
||||
dirname = fs.path.normpath(fs.path.dirname(path))
|
||||
assert fs.isabs(path)
|
||||
dirname = fs.normpath(fs.dirname(path))
|
||||
with fs.open(path, encoding="utf-8") as fobj:
|
||||
path_spec_lines = [
|
||||
PatternInfo(line, f"{name}:{line_no + 1}:{line}")
|
||||
@@ -194,7 +194,7 @@ class DvcIgnoreFilter:
|
||||
)
|
||||
|
||||
def _get_key(self, path):
|
||||
parts = self.fs.path.relparts(path, self.root_dir)
|
||||
parts = self.fs.relparts(path, self.root_dir)
|
||||
if parts == (os.curdir,):
|
||||
return ()
|
||||
return parts
|
||||
@@ -204,13 +204,13 @@ class DvcIgnoreFilter:
|
||||
old_pattern = trie.longest_prefix(key).value
|
||||
matches = old_pattern.matches(dirname, DvcIgnore.DVCIGNORE_FILE, False)
|
||||
|
||||
path = self.fs.path.join(dirname, DvcIgnore.DVCIGNORE_FILE)
|
||||
path = self.fs.join(dirname, DvcIgnore.DVCIGNORE_FILE)
|
||||
if not matches and self.fs.exists(path):
|
||||
name = self.fs.path.relpath(path, self.root_dir)
|
||||
name = self.fs.relpath(path, self.root_dir)
|
||||
new_pattern = DvcIgnorePatterns.from_file(path, self.fs, name)
|
||||
if old_pattern:
|
||||
plist, prefix = merge_patterns(
|
||||
self.fs.path.flavour,
|
||||
self.fs.flavour,
|
||||
old_pattern.pattern_list,
|
||||
old_pattern.dirname,
|
||||
new_pattern.pattern_list,
|
||||
@@ -239,7 +239,7 @@ class DvcIgnoreFilter:
|
||||
dnames = []
|
||||
|
||||
for dname in dnames:
|
||||
self._update_sub_repo(self.fs.path.join(dirname, dname), ignore_trie)
|
||||
self._update_sub_repo(self.fs.join(dirname, dname), ignore_trie)
|
||||
|
||||
def _update_sub_repo(self, path, ignore_trie: Trie):
|
||||
from dvc.repo import Repo
|
||||
@@ -247,18 +247,18 @@ class DvcIgnoreFilter:
|
||||
if path == self.root_dir:
|
||||
return
|
||||
|
||||
dvc_dir = self.fs.path.join(path, Repo.DVC_DIR)
|
||||
dvc_dir = self.fs.join(path, Repo.DVC_DIR)
|
||||
if not self.fs.exists(dvc_dir):
|
||||
return
|
||||
|
||||
root, dname = self.fs.path.split(path)
|
||||
root, dname = self.fs.split(path)
|
||||
key = self._get_key(root)
|
||||
pattern_info = PatternInfo(f"/{dname}/", f"in sub_repo:{dname}")
|
||||
new_pattern = DvcIgnorePatterns([pattern_info], root, self.fs.sep)
|
||||
old_pattern = ignore_trie.longest_prefix(key).value
|
||||
if old_pattern:
|
||||
plist, prefix = merge_patterns(
|
||||
self.fs.path.flavour,
|
||||
self.fs.flavour,
|
||||
old_pattern.pattern_list,
|
||||
old_pattern.dirname,
|
||||
new_pattern.pattern_list,
|
||||
@@ -269,7 +269,7 @@ class DvcIgnoreFilter:
|
||||
ignore_trie[key] = new_pattern
|
||||
|
||||
def __call__(self, root, dirs, files, ignore_subrepos=True):
|
||||
abs_root = self.fs.path.abspath(root)
|
||||
abs_root = self.fs.abspath(root)
|
||||
ignore_pattern = self._get_trie_pattern(
|
||||
abs_root, dnames=dirs, ignore_subrepos=ignore_subrepos
|
||||
)
|
||||
@@ -283,7 +283,7 @@ class DvcIgnoreFilter:
|
||||
nondirs = []
|
||||
|
||||
for entry in fs.ls(path, detail=True, **kwargs):
|
||||
name = fs.path.name(entry["name"])
|
||||
name = fs.name(entry["name"])
|
||||
fs_dict[name] = entry
|
||||
if entry["type"] == "directory":
|
||||
dirs.append(name)
|
||||
@@ -338,7 +338,7 @@ class DvcIgnoreFilter:
|
||||
else:
|
||||
ignores_trie = self._ignores_trie_subrepos
|
||||
|
||||
if not self.fs.path.isin_or_eq(dirname, self.root_dir):
|
||||
if not self.fs.isin_or_eq(dirname, self.root_dir):
|
||||
# outside of the repo
|
||||
return None
|
||||
|
||||
@@ -349,12 +349,12 @@ class DvcIgnoreFilter:
|
||||
return ignore_pattern
|
||||
|
||||
prefix_key = ignores_trie.longest_prefix(key).key or ()
|
||||
prefix = self.fs.path.join(self.root_dir, *prefix_key)
|
||||
prefix = self.fs.join(self.root_dir, *prefix_key)
|
||||
|
||||
dirs = list(
|
||||
takewhile(
|
||||
lambda path: path != prefix,
|
||||
(parent for parent in localfs.path.parents(dirname)),
|
||||
(parent for parent in localfs.parents(dirname)),
|
||||
)
|
||||
)
|
||||
dirs.reverse()
|
||||
@@ -370,7 +370,7 @@ class DvcIgnoreFilter:
|
||||
):
|
||||
if self._outside_repo(path):
|
||||
return False
|
||||
dirname, basename = self.fs.path.split(self.fs.path.normpath(path))
|
||||
dirname, basename = self.fs.split(self.fs.normpath(path))
|
||||
ignore_pattern = self._get_trie_pattern(dirname, None, ignore_subrepos)
|
||||
if ignore_pattern:
|
||||
return ignore_pattern.matches(dirname, basename, is_dir)
|
||||
@@ -378,7 +378,7 @@ class DvcIgnoreFilter:
|
||||
|
||||
def is_ignored_dir(self, path: str, ignore_subrepos: bool = True) -> bool:
|
||||
# only used in LocalFileSystem
|
||||
path = self.fs.path.abspath(path)
|
||||
path = self.fs.abspath(path)
|
||||
if path == self.root_dir:
|
||||
return False
|
||||
|
||||
@@ -386,18 +386,18 @@ class DvcIgnoreFilter:
|
||||
|
||||
def is_ignored_file(self, path: str, ignore_subrepos: bool = True) -> bool:
|
||||
# only used in LocalFileSystem
|
||||
path = self.fs.path.abspath(path)
|
||||
path = self.fs.abspath(path)
|
||||
return self._is_ignored(path, False, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _outside_repo(self, path):
|
||||
return not self.fs.path.isin_or_eq(path, self.root_dir)
|
||||
return not self.fs.isin_or_eq(path, self.root_dir)
|
||||
|
||||
def check_ignore(self, target):
|
||||
# NOTE: can only be used in `dvc check-ignore`, see
|
||||
# https://github.com/iterative/dvc/issues/5046
|
||||
full_target = self.fs.path.abspath(target)
|
||||
full_target = self.fs.abspath(target)
|
||||
if not self._outside_repo(full_target):
|
||||
dirname, basename = self.fs.path.split(self.fs.path.normpath(full_target))
|
||||
dirname, basename = self.fs.split(self.fs.normpath(full_target))
|
||||
pattern = self._get_trie_pattern(dirname)
|
||||
if pattern:
|
||||
matches = pattern.matches(
|
||||
|
||||
+30
-33
@@ -378,7 +378,7 @@ class Output:
|
||||
if (
|
||||
self.repo
|
||||
and self.fs.protocol == "local"
|
||||
and not self.fs.path.isabs(self.def_path)
|
||||
and not self.fs.isabs(self.def_path)
|
||||
):
|
||||
self.fs = self.repo.fs
|
||||
|
||||
@@ -411,7 +411,7 @@ class Output:
|
||||
self.remote = remote
|
||||
|
||||
if self.fs.version_aware:
|
||||
_, version_id = self.fs.path.coalesce_version(
|
||||
_, version_id = self.fs.coalesce_version(
|
||||
self.def_path, self.meta.version_id
|
||||
)
|
||||
self.meta.version_id = version_id
|
||||
@@ -465,7 +465,7 @@ class Output:
|
||||
parsed.scheme != "remote"
|
||||
and self.stage
|
||||
and self.stage.repo.fs == fs
|
||||
and not fs.path.isabs(fs_path)
|
||||
and not fs.isabs(fs_path)
|
||||
):
|
||||
# NOTE: we can path either from command line or .dvc file,
|
||||
# so we should expect both posix and windows style paths.
|
||||
@@ -473,9 +473,9 @@ class Output:
|
||||
#
|
||||
# FIXME: if we have Windows path containing / or posix one with \
|
||||
# then we have #2059 bug and can't really handle that.
|
||||
fs_path = fs.path.join(self.stage.wdir, fs_path)
|
||||
fs_path = fs.join(self.stage.wdir, fs_path)
|
||||
|
||||
return fs.path.abspath(fs.path.normpath(fs_path))
|
||||
return fs.abspath(fs.normpath(fs_path))
|
||||
|
||||
def __repr__(self):
|
||||
return f"{type(self).__name__}: {self.def_path!r}"
|
||||
@@ -491,14 +491,14 @@ class Output:
|
||||
):
|
||||
return str(self.def_path)
|
||||
|
||||
if not self.fs.path.isin(self.fs_path, self.repo.root_dir):
|
||||
if not self.fs.isin(self.fs_path, self.repo.root_dir):
|
||||
return self.fs_path
|
||||
|
||||
cur_dir = self.fs.path.getcwd()
|
||||
if self.fs.path.isin(cur_dir, self.repo.root_dir):
|
||||
return self.fs.path.relpath(self.fs_path, cur_dir)
|
||||
cur_dir = self.fs.getcwd()
|
||||
if self.fs.isin(cur_dir, self.repo.root_dir):
|
||||
return self.fs.relpath(self.fs_path, cur_dir)
|
||||
|
||||
return self.fs.path.relpath(self.fs_path, self.repo.root_dir)
|
||||
return self.fs.relpath(self.fs_path, self.repo.root_dir)
|
||||
|
||||
def clear(self):
|
||||
self.hash_info = HashInfo.from_dict({})
|
||||
@@ -515,10 +515,10 @@ class Output:
|
||||
if urlparse(self.def_path).scheme == "remote":
|
||||
return False
|
||||
|
||||
if self.fs.path.isabs(self.def_path):
|
||||
if self.fs.isabs(self.def_path):
|
||||
return False
|
||||
|
||||
return self.repo and self.fs.path.isin(
|
||||
return self.repo and self.fs.isin(
|
||||
self.fs_path,
|
||||
self.repo.root_dir,
|
||||
)
|
||||
@@ -607,11 +607,11 @@ class Output:
|
||||
def index_key(self) -> Tuple[str, "DataIndexKey"]:
|
||||
if self.is_in_repo:
|
||||
workspace = "repo"
|
||||
key = self.repo.fs.path.relparts(self.fs_path, self.repo.root_dir)
|
||||
key = self.repo.fs.relparts(self.fs_path, self.repo.root_dir)
|
||||
else:
|
||||
workspace = self.fs.protocol
|
||||
no_drive = self.fs.path.flavour.splitdrive(self.fs_path)[1]
|
||||
key = self.fs.path.parts(no_drive)[1:]
|
||||
no_drive = self.fs.flavour.splitdrive(self.fs_path)[1]
|
||||
key = self.fs.parts(no_drive)[1:]
|
||||
return workspace, key
|
||||
|
||||
def changed_checksum(self):
|
||||
@@ -786,7 +786,7 @@ class Output:
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
with Callback.as_tqdm_callback(
|
||||
with TqdmCallback(
|
||||
desc=f"Committing {self} to cache",
|
||||
unit="file",
|
||||
) as cb:
|
||||
@@ -799,7 +799,7 @@ class Output:
|
||||
callback=cb,
|
||||
)
|
||||
if relink:
|
||||
rel = self.fs.path.relpath(filter_info or self.fs_path)
|
||||
rel = self.fs.relpath(filter_info or self.fs_path)
|
||||
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as cb:
|
||||
self._checkout(
|
||||
filter_info or self.fs_path,
|
||||
@@ -814,7 +814,7 @@ class Output:
|
||||
self.set_exec()
|
||||
|
||||
def _commit_granular_dir(self, filter_info, hardlink) -> Optional["HashFile"]:
|
||||
prefix = self.fs.path.parts(self.fs.path.relpath(filter_info, self.fs_path))
|
||||
prefix = self.fs.parts(self.fs.relpath(filter_info, self.fs_path))
|
||||
staging, _, obj = self._build(
|
||||
self.cache,
|
||||
self.fs_path,
|
||||
@@ -826,7 +826,7 @@ class Output:
|
||||
save_obj = obj.filter(prefix)
|
||||
assert isinstance(save_obj, Tree)
|
||||
checkout_obj = save_obj.get_obj(self.cache, prefix)
|
||||
with Callback.as_tqdm_callback(
|
||||
with TqdmCallback(
|
||||
desc=f"Committing {self} to cache",
|
||||
unit="file",
|
||||
) as cb:
|
||||
@@ -862,7 +862,7 @@ class Output:
|
||||
ret.update(split_file_meta_from_cloud(meta_d))
|
||||
|
||||
if self.is_in_repo:
|
||||
path = self.fs.path.as_posix(relpath(self.fs_path, self.stage.wdir))
|
||||
path = self.fs.as_posix(relpath(self.fs_path, self.stage.wdir))
|
||||
else:
|
||||
path = self.def_path
|
||||
|
||||
@@ -949,7 +949,7 @@ class Output:
|
||||
return None
|
||||
|
||||
assert obj
|
||||
fs_path = self.fs.path
|
||||
fs_path = self.fs
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
prefix = fs_path.relparts(filter_info, self.fs_path)
|
||||
assert isinstance(obj, Tree)
|
||||
@@ -1059,7 +1059,7 @@ class Output:
|
||||
upload=upload,
|
||||
no_progress_bar=no_progress_bar,
|
||||
)
|
||||
with Callback.as_tqdm_callback(
|
||||
with TqdmCallback(
|
||||
desc=f"Transferring to {odb.fs.unstrip_protocol(odb.path)}",
|
||||
unit="file",
|
||||
) as cb:
|
||||
@@ -1092,7 +1092,10 @@ class Output:
|
||||
|
||||
def unprotect(self):
|
||||
if self.exists and self.use_cache:
|
||||
self.cache.unprotect(self.fs_path)
|
||||
with TqdmCallback(
|
||||
size=self.meta.nfiles or -1, desc=f"Unprotecting {self}"
|
||||
) as callback:
|
||||
self.cache.unprotect(self.fs_path, callback=callback)
|
||||
|
||||
def get_dir_cache(self, **kwargs) -> Optional["Tree"]:
|
||||
if not self.is_dir_checksum:
|
||||
@@ -1151,7 +1154,7 @@ class Output:
|
||||
assert obj is None or isinstance(obj, Tree)
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
assert obj
|
||||
prefix = self.fs.path.parts(self.fs.path.relpath(filter_info, self.fs_path))
|
||||
prefix = self.fs.parts(self.fs.relpath(filter_info, self.fs_path))
|
||||
return obj.filter(prefix)
|
||||
return obj
|
||||
|
||||
@@ -1294,10 +1297,7 @@ class Output:
|
||||
def unstage(self, path: str) -> Tuple["Meta", "Tree"]:
|
||||
from pygtrie import Trie
|
||||
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
assert isinstance(self.fs.path, Path)
|
||||
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
|
||||
rel_key = tuple(self.fs.parts(self.fs.relpath(path, self.fs_path)))
|
||||
|
||||
if self.hash_info:
|
||||
tree = self.get_dir_cache()
|
||||
@@ -1330,11 +1330,8 @@ class Output:
|
||||
) -> Tuple["Meta", "Tree"]:
|
||||
from pygtrie import Trie
|
||||
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
assert isinstance(self.fs.path, Path)
|
||||
append_only = True
|
||||
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
|
||||
rel_key = tuple(self.fs.parts(self.fs.relpath(path, self.fs_path)))
|
||||
|
||||
if self.hash_info:
|
||||
tree = self.get_dir_cache()
|
||||
@@ -1435,7 +1432,7 @@ class Output:
|
||||
|
||||
assert staging
|
||||
assert obj.hash_info
|
||||
with Callback.as_tqdm_callback(
|
||||
with TqdmCallback(
|
||||
desc=f"Adding {self} to cache",
|
||||
unit="file",
|
||||
) as cb:
|
||||
|
||||
@@ -143,11 +143,11 @@ class DataResolver:
|
||||
self.parsing_config = repo.config.get("parsing", {})
|
||||
|
||||
if os.path.isabs(wdir):
|
||||
wdir = fs.path.relpath(wdir)
|
||||
wdir = fs.relpath(wdir)
|
||||
wdir = "" if wdir == os.curdir else wdir
|
||||
|
||||
self.wdir = wdir
|
||||
self.relpath = fs.path.normpath(fs.path.join(self.wdir, "dvc.yaml"))
|
||||
self.relpath = fs.normpath(fs.join(self.wdir, "dvc.yaml"))
|
||||
|
||||
vars_ = d.get(VARS_KWD, [])
|
||||
check_interpolations(vars_, VARS_KWD, self.relpath)
|
||||
@@ -248,7 +248,7 @@ class EntryDefinition:
|
||||
wdir = to_str(context.resolve_str(wdir))
|
||||
except (ContextError, ParseError) as exc:
|
||||
format_and_raise(exc, f"'{self.where}.{name}.wdir'", self.relpath)
|
||||
return self.resolver.fs.path.join(self.wdir, wdir)
|
||||
return self.resolver.fs.join(self.wdir, wdir)
|
||||
|
||||
def resolve(self, **kwargs):
|
||||
try:
|
||||
|
||||
@@ -384,7 +384,7 @@ class Context(CtxDict):
|
||||
|
||||
def merge_from(self, fs, item: str, wdir: str, overwrite=False):
|
||||
path, _, keys_str = item.partition(":")
|
||||
path = fs.path.normpath(fs.path.join(wdir, path))
|
||||
path = fs.normpath(fs.join(wdir, path))
|
||||
|
||||
select_keys = lfilter(bool, keys_str.split(",")) if keys_str else None
|
||||
if path in self.imports:
|
||||
@@ -429,7 +429,7 @@ class Context(CtxDict):
|
||||
default: Optional[str] = None,
|
||||
):
|
||||
if default:
|
||||
to_import = fs.path.join(wdir, default)
|
||||
to_import = fs.join(wdir, default)
|
||||
if fs.exists(to_import):
|
||||
self.merge_from(fs, default, wdir)
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
INDEX_FIELD = "step"
|
||||
REVISION_FIELD = "rev"
|
||||
FILENAME_FIELD = "filename"
|
||||
VERSION_FIELD = "dvc_data_version_info"
|
||||
REVISIONS_KEY = "revisions"
|
||||
INDEX = "step"
|
||||
REVISION = "rev"
|
||||
FILENAME = "filename"
|
||||
FIELD = "field"
|
||||
REVISIONS = "revisions"
|
||||
ANCHOR_DEFINITIONS = "anchor_definitions"
|
||||
TYPE_KEY = "type"
|
||||
SRC_FIELD = "src"
|
||||
SRC = "src"
|
||||
|
||||
+18
-27
@@ -1,7 +1,6 @@
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Union
|
||||
|
||||
from dvc.render import REVISION_FIELD, REVISIONS_KEY, SRC_FIELD, TYPE_KEY, VERSION_FIELD
|
||||
from dvc.render import REVISION, REVISIONS, SRC, TYPE_KEY
|
||||
from dvc.render.converter.image import ImageConverter
|
||||
from dvc.render.converter.vega import VegaConverter
|
||||
|
||||
@@ -19,39 +18,31 @@ def _get_converter(
|
||||
raise ValueError(f"Invalid renderer class {renderer_class}")
|
||||
|
||||
|
||||
def _group_by_rev(datapoints):
|
||||
grouped = defaultdict(list)
|
||||
for datapoint in datapoints:
|
||||
rev = datapoint.get(VERSION_FIELD, {}).get("revision")
|
||||
grouped[rev].append(datapoint)
|
||||
return dict(grouped)
|
||||
|
||||
|
||||
def to_json(renderer, split: bool = False) -> List[Dict]:
|
||||
if renderer.TYPE == "vega":
|
||||
grouped = _group_by_rev(renderer.datapoints)
|
||||
if not renderer.datapoints:
|
||||
return []
|
||||
revs = renderer.get_revs()
|
||||
if split:
|
||||
content = renderer.get_filled_template(
|
||||
skip_anchors=["data"], as_string=False
|
||||
)
|
||||
content, split_content = renderer.get_partial_filled_template()
|
||||
else:
|
||||
content = renderer.get_filled_template(as_string=False)
|
||||
if grouped:
|
||||
return [
|
||||
{
|
||||
TYPE_KEY: renderer.TYPE,
|
||||
REVISIONS_KEY: sorted(grouped.keys()),
|
||||
"content": content,
|
||||
"datapoints": grouped,
|
||||
}
|
||||
]
|
||||
return []
|
||||
content = renderer.get_filled_template()
|
||||
split_content = {}
|
||||
|
||||
return [
|
||||
{
|
||||
TYPE_KEY: renderer.TYPE,
|
||||
REVISIONS: revs,
|
||||
"content": content,
|
||||
**split_content,
|
||||
}
|
||||
]
|
||||
if renderer.TYPE == "image":
|
||||
return [
|
||||
{
|
||||
TYPE_KEY: renderer.TYPE,
|
||||
REVISIONS_KEY: [datapoint.get(REVISION_FIELD)],
|
||||
"url": datapoint.get(SRC_FIELD),
|
||||
REVISIONS: [datapoint.get(REVISION)],
|
||||
"url": datapoint.get(SRC),
|
||||
}
|
||||
for datapoint in renderer.datapoints
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@ import base64
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
|
||||
|
||||
from dvc.render import FILENAME_FIELD, REVISION_FIELD, SRC_FIELD
|
||||
from dvc.render import FILENAME, REVISION, SRC
|
||||
|
||||
from . import Converter
|
||||
|
||||
@@ -58,9 +58,9 @@ class ImageConverter(Converter):
|
||||
else:
|
||||
src = self._encode_image(image_data)
|
||||
datapoint = {
|
||||
REVISION_FIELD: revision,
|
||||
FILENAME_FIELD: filename,
|
||||
SRC_FIELD: src,
|
||||
REVISION: revision,
|
||||
FILENAME: filename,
|
||||
SRC: src,
|
||||
}
|
||||
datapoints.append(datapoint)
|
||||
return datapoints, properties
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
from funcy import first, last
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.render import FILENAME_FIELD, INDEX_FIELD, VERSION_FIELD
|
||||
from dvc.render import FIELD, FILENAME, INDEX, REVISION
|
||||
|
||||
from . import Converter
|
||||
|
||||
@@ -112,7 +112,6 @@ class VegaConverter(Converter):
|
||||
):
|
||||
super().__init__(plot_id, data, properties)
|
||||
self.plot_id = plot_id
|
||||
self.inferred_properties: Dict = {}
|
||||
|
||||
def _infer_y_from_data(self):
|
||||
if self.plot_id in self.data:
|
||||
@@ -120,34 +119,38 @@ class VegaConverter(Converter):
|
||||
if all(isinstance(item, dict) for item in lst):
|
||||
datapoint = first(lst)
|
||||
field = last(datapoint.keys())
|
||||
self.inferred_properties["y"] = {self.plot_id: field}
|
||||
break
|
||||
return {self.plot_id: field}
|
||||
return None
|
||||
|
||||
def _infer_x_y(self):
|
||||
x = self.properties.get("x", None)
|
||||
y = self.properties.get("y", None)
|
||||
|
||||
inferred_properties: Dict = {}
|
||||
|
||||
# Infer x.
|
||||
if isinstance(x, str):
|
||||
self.inferred_properties["x"] = {}
|
||||
inferred_properties["x"] = {}
|
||||
# If multiple y files, duplicate x for each file.
|
||||
if isinstance(y, dict):
|
||||
for file, fields in y.items():
|
||||
# Duplicate x for each y.
|
||||
if isinstance(fields, list):
|
||||
self.inferred_properties["x"][file] = [x] * len(fields)
|
||||
inferred_properties["x"][file] = [x] * len(fields)
|
||||
else:
|
||||
self.inferred_properties["x"][file] = x
|
||||
inferred_properties["x"][file] = x
|
||||
# Otherwise use plot ID as file.
|
||||
else:
|
||||
self.inferred_properties["x"][self.plot_id] = x
|
||||
inferred_properties["x"][self.plot_id] = x
|
||||
|
||||
# Infer y.
|
||||
if y is None:
|
||||
self._infer_y_from_data()
|
||||
inferred_properties["y"] = self._infer_y_from_data()
|
||||
# If y files not provided, use plot ID as file.
|
||||
elif not isinstance(y, dict):
|
||||
self.inferred_properties["y"] = {self.plot_id: y}
|
||||
inferred_properties["y"] = {self.plot_id: y}
|
||||
|
||||
return inferred_properties
|
||||
|
||||
def _find_datapoints(self):
|
||||
result = {}
|
||||
@@ -182,7 +185,7 @@ class VegaConverter(Converter):
|
||||
|
||||
x = properties.get("x", None)
|
||||
if not isinstance(x, dict):
|
||||
return INDEX_FIELD
|
||||
return INDEX
|
||||
|
||||
fields = {field for _, field in _file_field(x)}
|
||||
if len(fields) == 1:
|
||||
@@ -192,7 +195,7 @@ class VegaConverter(Converter):
|
||||
def flat_datapoints(self, revision): # noqa: C901, PLR0912
|
||||
file2datapoints, properties = self.convert()
|
||||
|
||||
props_update = {}
|
||||
props_update: Dict[str, Union[str, List[Dict[str, str]]]] = {}
|
||||
|
||||
xs = list(_get_xs(properties, file2datapoints))
|
||||
|
||||
@@ -200,15 +203,17 @@ class VegaConverter(Converter):
|
||||
if not xs:
|
||||
x_file, x_field = (
|
||||
None,
|
||||
INDEX_FIELD,
|
||||
INDEX,
|
||||
)
|
||||
else:
|
||||
x_file, x_field = xs[0]
|
||||
props_update["x"] = x_field
|
||||
|
||||
num_xs = len(xs)
|
||||
multiple_x_fields = num_xs > 1 and len({x[1] for x in xs}) > 1
|
||||
props_update["x"] = "dvc_inferred_x_value" if multiple_x_fields else x_field
|
||||
|
||||
ys = list(_get_ys(properties, file2datapoints))
|
||||
|
||||
num_xs = len(xs)
|
||||
num_ys = len(ys)
|
||||
if num_xs > 1 and num_xs != num_ys:
|
||||
raise DvcException(
|
||||
@@ -237,6 +242,14 @@ class VegaConverter(Converter):
|
||||
else:
|
||||
common_prefix_len = 0
|
||||
|
||||
props_update["anchors_y_definitions"] = [
|
||||
{
|
||||
FILENAME: _get_short_y_file(y_file, common_prefix_len),
|
||||
FIELD: y_field,
|
||||
}
|
||||
for y_file, y_field in ys
|
||||
]
|
||||
|
||||
for i, (y_file, y_field) in enumerate(ys):
|
||||
if num_xs > 1:
|
||||
x_file, x_field = xs[i]
|
||||
@@ -249,15 +262,16 @@ class VegaConverter(Converter):
|
||||
source_field=y_field,
|
||||
)
|
||||
|
||||
if x_field == INDEX_FIELD and x_file is None:
|
||||
_update_from_index(datapoints, INDEX_FIELD)
|
||||
if x_field == INDEX and x_file is None:
|
||||
_update_from_index(datapoints, INDEX)
|
||||
else:
|
||||
x_datapoints = file2datapoints.get(x_file, [])
|
||||
try:
|
||||
_update_from_field(
|
||||
datapoints,
|
||||
field=x_field,
|
||||
field="dvc_inferred_x_value" if multiple_x_fields else x_field,
|
||||
source_datapoints=x_datapoints,
|
||||
source_field=x_field,
|
||||
)
|
||||
except IndexError:
|
||||
raise DvcException( # noqa: B904
|
||||
@@ -266,15 +280,12 @@ class VegaConverter(Converter):
|
||||
"They have to have same length."
|
||||
)
|
||||
|
||||
y_file_short = y_file[common_prefix_len:].strip("/\\")
|
||||
_update_all(
|
||||
datapoints,
|
||||
update_dict={
|
||||
VERSION_FIELD: {
|
||||
"revision": revision,
|
||||
FILENAME_FIELD: y_file_short,
|
||||
"field": y_field,
|
||||
}
|
||||
REVISION: revision,
|
||||
FILENAME: _get_short_y_file(y_file, common_prefix_len),
|
||||
FIELD: y_field,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -295,10 +306,10 @@ class VegaConverter(Converter):
|
||||
generated datapoints and updated properties. `x`, `y` values and labels
|
||||
are inferred and always provided.
|
||||
"""
|
||||
self._infer_x_y()
|
||||
inferred_properties = self._infer_x_y()
|
||||
|
||||
datapoints = self._find_datapoints()
|
||||
properties = {**self.properties, **self.inferred_properties}
|
||||
properties = {**self.properties, **inferred_properties}
|
||||
|
||||
properties["y_label"] = self.infer_y_label(properties)
|
||||
properties["x_label"] = self.infer_x_label(properties)
|
||||
@@ -306,6 +317,10 @@ class VegaConverter(Converter):
|
||||
return datapoints, properties
|
||||
|
||||
|
||||
def _get_short_y_file(y_file, common_prefix_len):
|
||||
return y_file[common_prefix_len:].strip("/\\")
|
||||
|
||||
|
||||
def _update_from_field(
|
||||
target_datapoints: List[Dict],
|
||||
field: str,
|
||||
|
||||
+12
-6
@@ -80,7 +80,7 @@ def match_defs_renderers( # noqa: C901, PLR0912
|
||||
for plot_id, group in plots_data.group_definitions().items():
|
||||
plot_datapoints: List[Dict] = []
|
||||
props = _squash_plots_properties(group)
|
||||
final_props: Dict = {}
|
||||
first_props: Dict = {}
|
||||
|
||||
def_errors: Dict[str, Exception] = {}
|
||||
src_errors: DefaultDict[str, Dict[str, Exception]] = defaultdict(dict)
|
||||
@@ -90,6 +90,7 @@ def match_defs_renderers( # noqa: C901, PLR0912
|
||||
if templates_dir is not None:
|
||||
props["template_dir"] = templates_dir
|
||||
|
||||
revs = []
|
||||
for rev, inner_id, plot_definition in group:
|
||||
plot_sources = infer_data_sources(inner_id, plot_definition)
|
||||
definitions_data = plots_data.get_definition_data(plot_sources, rev)
|
||||
@@ -109,19 +110,24 @@ def match_defs_renderers( # noqa: C901, PLR0912
|
||||
|
||||
try:
|
||||
dps, rev_props = converter.flat_datapoints(rev)
|
||||
if dps and rev not in revs:
|
||||
revs.append(rev)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("In %r, %s", rev, str(e).lower())
|
||||
def_errors[rev] = e
|
||||
continue
|
||||
|
||||
if not final_props and rev_props:
|
||||
final_props = rev_props
|
||||
if not first_props and rev_props:
|
||||
first_props = rev_props
|
||||
plot_datapoints.extend(dps)
|
||||
|
||||
if "title" not in final_props:
|
||||
final_props["title"] = renderer_id
|
||||
if "title" not in first_props:
|
||||
first_props["title"] = renderer_id
|
||||
|
||||
if revs:
|
||||
first_props["revs_with_datapoints"] = revs
|
||||
|
||||
if renderer_cls is not None:
|
||||
renderer = renderer_cls(plot_datapoints, renderer_id, **final_props)
|
||||
renderer = renderer_cls(plot_datapoints, renderer_id, **first_props)
|
||||
renderers.append(RendererWithErrors(renderer, dict(src_errors), def_errors))
|
||||
return renderers
|
||||
|
||||
+48
-30
@@ -12,7 +12,12 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc.exceptions import NotDvcRepoError, OutputNotFoundError
|
||||
from dvc.exceptions import (
|
||||
DvcException,
|
||||
NotDvcRepoError,
|
||||
OutputNotFoundError,
|
||||
RevCollectionError,
|
||||
)
|
||||
from dvc.ignore import DvcIgnoreFilter
|
||||
from dvc.log import logger
|
||||
from dvc.utils.objects import cached_property
|
||||
@@ -70,6 +75,7 @@ class Repo:
|
||||
from dvc.repo.commit import commit # type: ignore[misc]
|
||||
from dvc.repo.destroy import destroy # type: ignore[misc]
|
||||
from dvc.repo.diff import diff # type: ignore[misc]
|
||||
from dvc.repo.du import du as _du # type: ignore[misc]
|
||||
from dvc.repo.fetch import fetch # type: ignore[misc]
|
||||
from dvc.repo.freeze import freeze, unfreeze # type: ignore[misc]
|
||||
from dvc.repo.gc import gc # type: ignore[misc]
|
||||
@@ -93,6 +99,7 @@ class Repo:
|
||||
from .cache import check_missing as cache_check_missing # type: ignore[misc]
|
||||
from .data import status as data_status # type: ignore[misc]
|
||||
|
||||
du = staticmethod(_du)
|
||||
ls = staticmethod(_ls)
|
||||
ls_url = staticmethod(_ls_url)
|
||||
get = staticmethod(_get)
|
||||
@@ -112,7 +119,7 @@ class Repo:
|
||||
try:
|
||||
root_dir = self.find_root(root_dir, fs)
|
||||
fs = fs or localfs
|
||||
dvc_dir = fs.path.join(root_dir, self.DVC_DIR)
|
||||
dvc_dir = fs.join(root_dir, self.DVC_DIR)
|
||||
except NotDvcRepoError:
|
||||
if not uninitialized:
|
||||
raise
|
||||
@@ -205,7 +212,7 @@ class Repo:
|
||||
self.fs.makedirs(self.tmp_dir, exist_ok=True)
|
||||
|
||||
self.lock = make_lock(
|
||||
self.fs.path.join(self.tmp_dir, "lock"),
|
||||
self.fs.join(self.tmp_dir, "lock"),
|
||||
tmp_dir=self.tmp_dir,
|
||||
hardlink_lock=self.config["core"].get("hardlink_lock", False),
|
||||
friendly=True,
|
||||
@@ -249,7 +256,7 @@ class Repo:
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def local_dvc_dir(self):
|
||||
def local_dvc_dir(self) -> Optional[str]:
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
|
||||
if not self.dvc_dir:
|
||||
@@ -261,10 +268,10 @@ class Repo:
|
||||
if not isinstance(self.fs, GitFileSystem):
|
||||
return None
|
||||
|
||||
relparts = ()
|
||||
relparts: Tuple[str, ...] = ()
|
||||
if self.root_dir != "/":
|
||||
# subrepo
|
||||
relparts = self.fs.path.relparts(self.root_dir, "/")
|
||||
relparts = self.fs.relparts(self.root_dir, "/")
|
||||
|
||||
dvc_dir = os.path.join(
|
||||
self.scm.root_dir,
|
||||
@@ -378,6 +385,7 @@ class Repo:
|
||||
except KeyError:
|
||||
pass
|
||||
self.data_index.commit()
|
||||
self.data_index.close()
|
||||
self._reset()
|
||||
|
||||
def get_data_index_entry(
|
||||
@@ -393,7 +401,7 @@ class Repo:
|
||||
index = subrepo.index.data[workspace]
|
||||
else:
|
||||
index = self.index.data[workspace]
|
||||
key = self.fs.path.relparts(path, self.root_dir)
|
||||
key = self.fs.relparts(path, self.root_dir)
|
||||
|
||||
try:
|
||||
return index, index[key]
|
||||
@@ -409,18 +417,18 @@ class Repo:
|
||||
|
||||
fs = fs or localfs
|
||||
root = root or os.curdir
|
||||
root_dir = fs.path.abspath(root)
|
||||
root_dir = fs.abspath(root)
|
||||
|
||||
if not fs.isdir(root_dir):
|
||||
raise NotDvcRepoError(f"directory '{root}' does not exist")
|
||||
|
||||
while True:
|
||||
dvc_dir = fs.path.join(root_dir, cls.DVC_DIR)
|
||||
dvc_dir = fs.join(root_dir, cls.DVC_DIR)
|
||||
if fs.isdir(dvc_dir):
|
||||
return root_dir
|
||||
if isinstance(fs, LocalFileSystem) and os.path.ismount(root_dir):
|
||||
break
|
||||
parent = fs.path.parent(root_dir)
|
||||
parent = fs.parent(root_dir)
|
||||
if parent == root_dir:
|
||||
break
|
||||
root_dir = parent
|
||||
@@ -438,7 +446,7 @@ class Repo:
|
||||
|
||||
fs = fs or localfs
|
||||
root_dir = cls.find_root(root, fs=fs)
|
||||
return fs.path.join(root_dir, cls.DVC_DIR)
|
||||
return fs.join(root_dir, cls.DVC_DIR)
|
||||
|
||||
@staticmethod
|
||||
def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False) -> "Repo":
|
||||
@@ -447,7 +455,10 @@ class Repo:
|
||||
return init(root_dir=root_dir, no_scm=no_scm, force=force, subdir=subdir)
|
||||
|
||||
def unprotect(self, target):
|
||||
return self.cache.repo.unprotect(target)
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
|
||||
with TqdmCallback(desc=f"Unprotecting {target}") as callback:
|
||||
return self.cache.repo.unprotect(target, callback=callback)
|
||||
|
||||
def _ignore(self):
|
||||
flist = [self.config.files["local"]]
|
||||
@@ -487,6 +498,7 @@ class Repo:
|
||||
revs=None,
|
||||
num=1,
|
||||
push: bool = False,
|
||||
skip_failed: bool = False,
|
||||
):
|
||||
"""Get the stages related to the given target and collect
|
||||
the `info` of its outputs.
|
||||
@@ -505,7 +517,7 @@ class Repo:
|
||||
"""
|
||||
used = defaultdict(set)
|
||||
|
||||
for _ in self.brancher(
|
||||
for rev in self.brancher(
|
||||
revs=revs,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
@@ -514,17 +526,23 @@ class Repo:
|
||||
commit_date=commit_date,
|
||||
num=num,
|
||||
):
|
||||
for odb, objs in self.index.used_objs(
|
||||
targets,
|
||||
remote=remote,
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
recursive=recursive,
|
||||
with_deps=with_deps,
|
||||
push=push,
|
||||
).items():
|
||||
used[odb].update(objs)
|
||||
|
||||
try:
|
||||
for odb, objs in self.index.used_objs(
|
||||
targets,
|
||||
remote=remote,
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
recursive=recursive,
|
||||
with_deps=with_deps,
|
||||
push=push,
|
||||
).items():
|
||||
used[odb].update(objs)
|
||||
except DvcException as exc:
|
||||
rev = rev or "workspace"
|
||||
if skip_failed:
|
||||
logger.warning("Failed to collect '%s', skipping", rev)
|
||||
else:
|
||||
raise RevCollectionError(rev) from exc
|
||||
if used_run_cache:
|
||||
for odb, objs in self.stage_cache.get_used_objs(
|
||||
used_run_cache, remote=remote, force=force, jobs=jobs
|
||||
@@ -537,19 +555,19 @@ class Repo:
|
||||
# using `outs_graph` to ensure graph checks are run
|
||||
outs = outs or self.index.outs_graph
|
||||
|
||||
abs_path = self.fs.path.abspath(path)
|
||||
abs_path = self.fs.abspath(path)
|
||||
fs_path = abs_path
|
||||
|
||||
def func(out):
|
||||
def eq(one, two):
|
||||
return one == two
|
||||
|
||||
match = eq if strict else out.fs.path.isin_or_eq
|
||||
match = eq if strict else out.fs.isin_or_eq
|
||||
|
||||
if out.protocol == "local" and match(fs_path, out.fs_path):
|
||||
return True
|
||||
|
||||
if recursive and out.fs.path.isin(out.fs_path, fs_path):
|
||||
if recursive and out.fs.isin(out.fs_path, fs_path):
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -561,7 +579,7 @@ class Repo:
|
||||
return matched
|
||||
|
||||
def is_dvc_internal(self, path):
|
||||
path_parts = self.fs.path.normpath(path).split(self.fs.sep)
|
||||
path_parts = self.fs.normpath(path).split(self.fs.sep)
|
||||
return self.DVC_DIR in path_parts
|
||||
|
||||
@cached_property
|
||||
@@ -608,10 +626,10 @@ class Repo:
|
||||
cache_dir = self.config["core"].get("site_cache_dir") or site_cache_dir()
|
||||
|
||||
if isinstance(self.fs, GitFileSystem):
|
||||
relparts = ()
|
||||
relparts: Tuple[str, ...] = ()
|
||||
if self.root_dir != "/":
|
||||
# subrepo
|
||||
relparts = self.fs.path.relparts(self.root_dir, "/")
|
||||
relparts = self.fs.relparts(self.root_dir, "/")
|
||||
root_dir = os.path.join(self.scm.root_dir, *relparts)
|
||||
else:
|
||||
root_dir = self.root_dir
|
||||
|
||||
+1
-1
@@ -181,7 +181,7 @@ def _add_transfer(
|
||||
|
||||
def _add(stage: "Stage", source: Optional[str] = None, no_commit: bool = False) -> None:
|
||||
out = stage.outs[0]
|
||||
path = out.fs.path.abspath(source) if source else None
|
||||
path = out.fs.abspath(source) if source else None
|
||||
try:
|
||||
stage.add_outs(path, no_commit=no_commit)
|
||||
except CacheLinkError:
|
||||
|
||||
@@ -210,7 +210,8 @@ class Artifacts:
|
||||
) -> Tuple[int, str]:
|
||||
from dvc_studio_client.model_registry import get_download_uris
|
||||
|
||||
from dvc.fs import Callback, HTTPFileSystem, generic, localfs
|
||||
from dvc.fs import HTTPFileSystem, generic, localfs
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
|
||||
logger.debug("Trying to download artifact '%s' via studio", name)
|
||||
out = out or os.getcwd()
|
||||
@@ -228,7 +229,7 @@ class Artifacts:
|
||||
dvc_studio_config=dvc_studio_config,
|
||||
**kwargs,
|
||||
).items():
|
||||
to_info = localfs.path.join(out, path)
|
||||
to_info = localfs.join(out, path)
|
||||
if localfs.exists(to_info) and not force:
|
||||
hint = "\nTo override it, re-run with '--force'."
|
||||
raise FileExistsLocallyError( # noqa: TRY301
|
||||
@@ -244,7 +245,7 @@ class Artifacts:
|
||||
) from exc
|
||||
fs = HTTPFileSystem()
|
||||
jobs = jobs or fs.jobs
|
||||
with Callback.as_tqdm_callback(
|
||||
with TqdmCallback(
|
||||
desc=f"Downloading '{name}' from '{repo_url}'",
|
||||
unit="files",
|
||||
) as cb:
|
||||
@@ -253,7 +254,7 @@ class Artifacts:
|
||||
fs, from_infos, localfs, to_infos, callback=cb, batch_size=jobs
|
||||
)
|
||||
|
||||
return len(to_infos), relpath(localfs.path.commonpath(to_infos))
|
||||
return len(to_infos), relpath(localfs.commonpath(to_infos))
|
||||
|
||||
@classmethod
|
||||
def get( # noqa: PLR0913
|
||||
|
||||
+16
-13
@@ -20,6 +20,7 @@ def brancher(
|
||||
all_tags=False,
|
||||
all_commits=False,
|
||||
all_experiments=False,
|
||||
workspace=True,
|
||||
commit_date: Optional[str] = None,
|
||||
sha_only=False,
|
||||
num=1,
|
||||
@@ -31,6 +32,7 @@ def brancher(
|
||||
all_branches (bool): iterate over all available branches.
|
||||
all_commits (bool): iterate over all commits.
|
||||
all_tags (bool): iterate over all available tags.
|
||||
workspace (bool): include workspace.
|
||||
commit_date (str): Keep experiments from the commits after(include)
|
||||
a certain date. Date must match the extended
|
||||
ISO 8601 format (YYYY-MM-DD).
|
||||
@@ -58,12 +60,12 @@ def brancher(
|
||||
from dvc.fs import LocalFileSystem
|
||||
|
||||
repo_root_parts: Tuple[str, ...] = ()
|
||||
if self.fs.path.isin(self.root_dir, self.scm.root_dir):
|
||||
repo_root_parts = self.fs.path.relparts(self.root_dir, self.scm.root_dir)
|
||||
if self.fs.isin(self.root_dir, self.scm.root_dir):
|
||||
repo_root_parts = self.fs.relparts(self.root_dir, self.scm.root_dir)
|
||||
|
||||
cwd_parts: Tuple[str, ...] = ()
|
||||
if self.fs.path.isin(self.fs.path.getcwd(), self.scm.root_dir):
|
||||
cwd_parts = self.fs.path.relparts(self.fs.path.getcwd(), self.scm.root_dir)
|
||||
if self.fs.isin(self.fs.getcwd(), self.scm.root_dir):
|
||||
cwd_parts = self.fs.relparts(self.fs.getcwd(), self.scm.root_dir)
|
||||
|
||||
saved_fs = self.fs
|
||||
saved_root = self.root_dir
|
||||
@@ -73,7 +75,8 @@ def brancher(
|
||||
|
||||
logger.trace("switching fs to workspace")
|
||||
self.fs = LocalFileSystem(url=self.root_dir)
|
||||
yield "workspace"
|
||||
if workspace:
|
||||
yield "workspace"
|
||||
|
||||
revs = revs.copy() if revs else []
|
||||
if "workspace" in revs:
|
||||
@@ -122,18 +125,18 @@ def _switch_fs(
|
||||
logger.trace("switching fs to revision %s", rev[:7])
|
||||
assert isinstance(repo.scm, Git)
|
||||
fs = GitFileSystem(scm=repo.scm, rev=rev)
|
||||
root_dir = repo.fs.path.join("/", *repo_root_parts)
|
||||
root_dir = repo.fs.join("/", *repo_root_parts)
|
||||
if not fs.exists(root_dir):
|
||||
raise NotDvcRepoError(f"Commit '{rev[:7]}' does not contain a DVC repo")
|
||||
|
||||
repo.fs = fs
|
||||
repo.root_dir = root_dir
|
||||
repo.dvc_dir = fs.path.join(root_dir, repo.DVC_DIR)
|
||||
repo.dvc_dir = fs.join(root_dir, repo.DVC_DIR)
|
||||
repo._reset()
|
||||
|
||||
if cwd_parts:
|
||||
cwd = repo.fs.path.join("/", *cwd_parts)
|
||||
repo.fs.path.chdir(cwd)
|
||||
cwd = repo.fs.join("/", *cwd_parts)
|
||||
repo.fs.chdir(cwd)
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -145,12 +148,12 @@ def switch(repo: "Repo", rev: str) -> Iterator[str]:
|
||||
rev = resolve_rev(repo.scm, rev)
|
||||
|
||||
repo_root_parts: Tuple[str, ...] = ()
|
||||
if repo.fs.path.isin(repo.root_dir, repo.scm.root_dir):
|
||||
repo_root_parts = repo.fs.path.relparts(repo.root_dir, repo.scm.root_dir)
|
||||
if repo.fs.isin(repo.root_dir, repo.scm.root_dir):
|
||||
repo_root_parts = repo.fs.relparts(repo.root_dir, repo.scm.root_dir)
|
||||
|
||||
cwd_parts: Tuple[str, ...] = ()
|
||||
if repo.fs.path.isin(repo.fs.path.getcwd(), repo.scm.root_dir):
|
||||
cwd_parts = repo.fs.path.relparts(repo.fs.path.getcwd(), repo.scm.root_dir)
|
||||
if repo.fs.isin(repo.fs.getcwd(), repo.scm.root_dir):
|
||||
cwd_parts = repo.fs.relparts(repo.fs.getcwd(), repo.scm.root_dir)
|
||||
|
||||
saved_fs = repo.fs
|
||||
saved_root = repo.root_dir
|
||||
|
||||
@@ -81,7 +81,7 @@ def _check_can_delete(
|
||||
if cache_fs.exists(cache_path):
|
||||
continue
|
||||
|
||||
entry_paths.append(fs.path.join(path, *(entry.key or ())))
|
||||
entry_paths.append(fs.join(path, *(entry.key or ())))
|
||||
|
||||
if not entry_paths:
|
||||
return
|
||||
@@ -172,7 +172,7 @@ def checkout( # noqa: C901
|
||||
)
|
||||
|
||||
for out_path in out_paths:
|
||||
if self.fs.path.isin_or_eq(dest_path, out_path):
|
||||
if self.fs.isin_or_eq(dest_path, out_path):
|
||||
failed.add(out_path)
|
||||
|
||||
with ui.progress(
|
||||
@@ -195,7 +195,7 @@ def checkout( # noqa: C901
|
||||
|
||||
typ_map = {ADD: "added", DELETE: "deleted", MODIFY: "modified"}
|
||||
for key, typ in out_changes.items():
|
||||
out_path = self.fs.path.join(self.root_dir, *key)
|
||||
out_path = self.fs.join(self.root_dir, *key)
|
||||
|
||||
if out_path in failed:
|
||||
self.fs.remove(out_path, recursive=True)
|
||||
|
||||
+33
-9
@@ -1,4 +1,4 @@
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Set
|
||||
|
||||
from dvc import prompt
|
||||
|
||||
@@ -7,6 +7,7 @@ from .scm_context import scm_context
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Repo
|
||||
from .index import IndexView
|
||||
|
||||
|
||||
def _prepare_message(stage, changes):
|
||||
@@ -88,14 +89,7 @@ def commit_2_to_3(repo: "Repo", dry: bool = False):
|
||||
outs_filter=lambda o: o.hash_name == "md5-dos2unix",
|
||||
recursive=True,
|
||||
)
|
||||
migrated = set()
|
||||
for out in view.outs:
|
||||
dvcfile = out.stage.dvcfile.relpath
|
||||
if isinstance(out.stage.dvcfile, ProjectFile):
|
||||
lockfile = out.stage.dvcfile._lockfile.relpath
|
||||
migrated.add(f"{dvcfile} ({lockfile})")
|
||||
else:
|
||||
migrated.add(dvcfile)
|
||||
migrated = _migrateable_dvcfiles(view)
|
||||
if not migrated:
|
||||
ui.write("No DVC files in the repo to migrate to the 3.0 format.")
|
||||
return
|
||||
@@ -110,11 +104,41 @@ def commit_2_to_3(repo: "Repo", dry: bool = False):
|
||||
for out in stage.filter_outs(filter_info)
|
||||
if outs_filter is not None and outs_filter(out)
|
||||
}
|
||||
modified = False
|
||||
if outs:
|
||||
for out in outs:
|
||||
out.update_legacy_hash_name(force=True)
|
||||
modified = True
|
||||
deps = {dep for dep in stage.deps if not stage.is_import and dep.is_in_repo}
|
||||
if deps:
|
||||
for dep in deps:
|
||||
dep.update_legacy_hash_name(force=True)
|
||||
modified = True
|
||||
if modified:
|
||||
stage.save(allow_missing=True)
|
||||
stage.commit(allow_missing=True, relink=True)
|
||||
if not isinstance(stage.dvcfile, ProjectFile):
|
||||
ui.write(f"Updating DVC file '{stage.dvcfile.relpath}'")
|
||||
stage.dump(update_pipeline=False)
|
||||
|
||||
|
||||
def _migrateable_dvcfiles(view: "IndexView") -> Set[str]:
|
||||
from dvc.dvcfile import ProjectFile
|
||||
|
||||
migrated = set()
|
||||
for stage, filter_info in view._stage_infos:
|
||||
outs_filter = view._outs_filter
|
||||
dvcfile = stage.dvcfile.relpath
|
||||
assert outs_filter
|
||||
if any(outs_filter(out) for out in stage.filter_outs(filter_info)) or (
|
||||
not stage.is_import
|
||||
and any(
|
||||
dep.is_in_repo and dep.hash_name == "md5-dos2unix" for dep in stage.deps
|
||||
)
|
||||
):
|
||||
if isinstance(stage.dvcfile, ProjectFile):
|
||||
lockfile = stage.dvcfile._lockfile.relpath
|
||||
migrated.add(f"{dvcfile} ({lockfile})")
|
||||
else:
|
||||
migrated.add(dvcfile)
|
||||
return migrated
|
||||
|
||||
+3
-3
@@ -205,7 +205,7 @@ class Status(TypedDict):
|
||||
|
||||
def _transform_git_paths_to_dvc(repo: "Repo", files: Iterable[str]) -> List[str]:
|
||||
"""Transform files rel. to Git root to DVC root, and drop outside files."""
|
||||
rel = repo.fs.path.relpath(repo.root_dir, repo.scm.root_dir).rstrip("/")
|
||||
rel = repo.fs.relpath(repo.root_dir, repo.scm.root_dir).rstrip("/")
|
||||
|
||||
# if we have repo root in a different location than scm's root,
|
||||
# i.e. subdir repo, all git_paths need to be transformed rel. to the DVC
|
||||
@@ -215,11 +215,11 @@ def _transform_git_paths_to_dvc(repo: "Repo", files: Iterable[str]) -> List[str]
|
||||
length = len(prefix)
|
||||
files = (file[length:] for file in files if file.startswith(prefix))
|
||||
|
||||
start = repo.fs.path.relpath(repo.fs.path.getcwd(), repo.root_dir)
|
||||
start = repo.fs.relpath(repo.fs.getcwd(), repo.root_dir)
|
||||
if start in (os.curdir, ""):
|
||||
return list(files)
|
||||
# we need to convert repo relative paths to curdir relative.
|
||||
return [repo.fs.path.relpath(file, start) for file in files]
|
||||
return [repo.fs.relpath(file, start) for file in files]
|
||||
|
||||
|
||||
def status(
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
|
||||
def du(
|
||||
url: str,
|
||||
path: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
summarize: bool = False,
|
||||
config: Union[None, Dict[str, Any], str] = None,
|
||||
remote: Optional[str] = None,
|
||||
remote_config: Optional[dict] = None,
|
||||
):
|
||||
from dvc.config import Config
|
||||
|
||||
from . import Repo
|
||||
|
||||
if config and not isinstance(config, dict):
|
||||
config_dict = Config.load_file(config)
|
||||
else:
|
||||
config_dict = None
|
||||
|
||||
with Repo.open(
|
||||
url,
|
||||
rev=rev,
|
||||
subrepos=True,
|
||||
uninitialized=True,
|
||||
config=config_dict,
|
||||
remote=remote,
|
||||
remote_config=remote_config,
|
||||
) as repo:
|
||||
path = path or ""
|
||||
|
||||
fs = repo.dvcfs
|
||||
|
||||
if summarize or not fs.isdir(path):
|
||||
return [(path, fs.du(path, total=True))]
|
||||
|
||||
ret = [
|
||||
(entry_path, fs.du(entry_path, total=True)) for entry_path in fs.ls(path)
|
||||
]
|
||||
ret.append((path, sum(entry[1] for entry in ret)))
|
||||
return ret
|
||||
@@ -112,4 +112,6 @@ def _pull_cache(
|
||||
refs = [refs]
|
||||
revs = list(exp_commits(repo.scm, refs))
|
||||
logger.debug("dvc fetch experiment '%s'", refs)
|
||||
repo.fetch(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs)
|
||||
repo.fetch(
|
||||
jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs, workspace=False
|
||||
)
|
||||
|
||||
@@ -188,4 +188,6 @@ def _push_cache(
|
||||
assert isinstance(repo.scm, Git)
|
||||
revs = list(exp_commits(repo.scm, refs))
|
||||
logger.debug("dvc push experiment '%s'", refs)
|
||||
return repo.push(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs)
|
||||
return repo.push(
|
||||
jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs, workspace=False
|
||||
)
|
||||
|
||||
@@ -171,7 +171,7 @@ class ApplyStash(Stash):
|
||||
right_fs = self.scm.get_fs(right_rev)
|
||||
paths = [path for path in left_fs.find("/") if not right_fs.exists(path)]
|
||||
dest_paths = [
|
||||
localfs.path.join(self.scm.root_dir, left_fs.path.relpath(path, "/"))
|
||||
localfs.join(self.scm.root_dir, left_fs.relpath(path, "/"))
|
||||
for path in paths
|
||||
]
|
||||
for src, dest in zip(paths, dest_paths):
|
||||
|
||||
+7
-3
@@ -32,6 +32,7 @@ def _collect_indexes( # noqa: PLR0913
|
||||
recursive=False,
|
||||
all_commits=False,
|
||||
revs=None,
|
||||
workspace=True,
|
||||
max_size=None,
|
||||
types=None,
|
||||
config=None,
|
||||
@@ -62,6 +63,7 @@ def _collect_indexes( # noqa: PLR0913
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
workspace=workspace,
|
||||
):
|
||||
try:
|
||||
repo.config.merge(config)
|
||||
@@ -79,11 +81,11 @@ def _collect_indexes( # noqa: PLR0913
|
||||
idx.data["repo"].onerror = _make_index_onerror(onerror, rev)
|
||||
|
||||
indexes[rev or "workspace"] = idx
|
||||
except Exception as exc:
|
||||
except Exception as exc: # noqa: BLE001
|
||||
if onerror:
|
||||
onerror(rev, None, exc)
|
||||
collection_exc = exc
|
||||
logger.exception("failed to collect '%s'", rev or "workspace")
|
||||
logger.warning("failed to collect '%s', skipping", rev or "workspace")
|
||||
|
||||
if not indexes and collection_exc:
|
||||
raise collection_exc
|
||||
@@ -104,6 +106,7 @@ def fetch( # noqa: PLR0913
|
||||
all_commits=False,
|
||||
run_cache=False,
|
||||
revs=None,
|
||||
workspace=True,
|
||||
max_size=None,
|
||||
types=None,
|
||||
config=None,
|
||||
@@ -148,6 +151,7 @@ def fetch( # noqa: PLR0913
|
||||
recursive=recursive,
|
||||
all_commits=all_commits,
|
||||
revs=revs,
|
||||
workspace=workspace,
|
||||
max_size=max_size,
|
||||
types=types,
|
||||
config=config,
|
||||
@@ -215,7 +219,7 @@ def _log_unversioned(data: List["DataIndex"]) -> Tuple[List["DataIndex"], int]:
|
||||
index.storage_map = fs_index.storage_map
|
||||
for key, entry in fs_index.iteritems():
|
||||
if entry.meta and not entry.meta.isdir and entry.meta.version_id is None:
|
||||
unversioned.append(fs.unstrip_protocol(fs.path.join(remote.path, *key)))
|
||||
unversioned.append(fs.unstrip_protocol(fs.join(remote.path, *key)))
|
||||
else:
|
||||
index[key] = entry
|
||||
fs_index.close()
|
||||
|
||||
@@ -68,6 +68,7 @@ def gc( # noqa: PLR0913, C901
|
||||
num: Optional[int] = None,
|
||||
not_in_remote: bool = False,
|
||||
dry: bool = False,
|
||||
skip_failed: bool = False,
|
||||
):
|
||||
# require `workspace` to be true to come into effect.
|
||||
# assume `workspace` to be enabled if any of `all_tags`, `all_commits`,
|
||||
@@ -113,6 +114,7 @@ def gc( # noqa: PLR0913, C901
|
||||
jobs=jobs,
|
||||
revs=[rev] if rev else None,
|
||||
num=num or 1,
|
||||
skip_failed=skip_failed,
|
||||
).items():
|
||||
if odb not in odb_to_obj_ids:
|
||||
odb_to_obj_ids[odb] = set()
|
||||
|
||||
+3
-3
@@ -126,7 +126,7 @@ def build_graph(stages, outs_trie=None):
|
||||
outs_trie = outs_trie or build_outs_trie(stages)
|
||||
|
||||
for stage in stages:
|
||||
out = outs_trie.shortest_prefix(localfs.path.parts(stage.path)).value
|
||||
out = outs_trie.shortest_prefix(localfs.parts(stage.path)).value
|
||||
if out:
|
||||
raise StagePathAsOutputError(stage, str(out))
|
||||
|
||||
@@ -139,7 +139,7 @@ def build_graph(stages, outs_trie=None):
|
||||
continue
|
||||
|
||||
for dep in stage.deps:
|
||||
dep_key = dep.fs.path.parts(dep.fs_path)
|
||||
dep_key = dep.fs.parts(dep.fs_path)
|
||||
overlapping = [n.value for n in outs_trie.prefixes(dep_key)]
|
||||
if outs_trie.has_subtrie(dep_key):
|
||||
overlapping.extend(outs_trie.values(prefix=dep_key))
|
||||
@@ -165,7 +165,7 @@ def build_outs_graph(graph, outs_trie):
|
||||
if stage.is_db_import:
|
||||
continue
|
||||
for dep in stage.deps:
|
||||
dep_key = dep.fs.path.parts(dep.fs_path)
|
||||
dep_key = dep.fs.parts(dep.fs_path)
|
||||
overlapping = [n.value for n in outs_trie.prefixes(dep_key)]
|
||||
if outs_trie.has_subtrie(dep_key):
|
||||
overlapping.extend(outs_trie.values(prefix=dep_key))
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ def imp_db( # noqa: PLR0913
|
||||
output_format: str = "csv",
|
||||
out: Optional[str] = None,
|
||||
force: bool = False,
|
||||
connection: Optional[str] = None,
|
||||
):
|
||||
ui.warn("WARNING: import-db is an experimental feature.")
|
||||
ui.warn(
|
||||
@@ -50,7 +51,7 @@ def imp_db( # noqa: PLR0913
|
||||
db.update({"model": model, "version": version, "project_dir": project_dir})
|
||||
else:
|
||||
out = out or f"results.{output_format}"
|
||||
db["query"] = sql
|
||||
db.update({"query": sql, "connection": connection})
|
||||
|
||||
out = resolve_output(url or ".", out, force=force)
|
||||
path, wdir, out = resolve_paths(self, out, always_local=True)
|
||||
|
||||
+2
-2
@@ -100,10 +100,10 @@ def save_imports(
|
||||
Returns:
|
||||
Objects which were downloaded from source location.
|
||||
"""
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
from dvc.stage.exceptions import DataSourceChanged
|
||||
from dvc_data.index import md5, save
|
||||
from dvc_data.index.checkout import apply, compare
|
||||
from dvc_objects.fs.callbacks import Callback
|
||||
|
||||
downloaded: Set["HashInfo"] = set()
|
||||
|
||||
@@ -122,7 +122,7 @@ def save_imports(
|
||||
if not cache.fs.exists(cache.path):
|
||||
os.makedirs(cache.path)
|
||||
with TemporaryDirectory(dir=cache.path) as tmpdir:
|
||||
with Callback.as_tqdm_callback(
|
||||
with TqdmCallback(
|
||||
desc="Downloading imports from source",
|
||||
unit="files",
|
||||
) as cb:
|
||||
|
||||
+27
-12
@@ -97,7 +97,7 @@ def collect_files(
|
||||
for root, dirs, files in walk_iter:
|
||||
dvcfile_filter = partial(is_dvcfile_and_not_ignored, root)
|
||||
for file in filter(dvcfile_filter, files):
|
||||
file_path = fs.path.join(root, file)
|
||||
file_path = fs.join(root, file)
|
||||
try:
|
||||
index = Index.from_file(repo, file_path)
|
||||
except DvcException as exc:
|
||||
@@ -129,10 +129,25 @@ def _load_data_from_outs(index, prefix, outs):
|
||||
for key_len in range(1, len(key)):
|
||||
parents.add((ws, key[:key_len]))
|
||||
|
||||
loaded = None
|
||||
if out.files:
|
||||
loaded = True
|
||||
for okey, ometa, ohi in out.get_obj():
|
||||
for key_len in range(1, len(okey)):
|
||||
parents.add((ws, (*key, *okey[:key_len])))
|
||||
|
||||
fkey = (*key, *okey)
|
||||
index[(*prefix, ws, *fkey)] = DataIndexEntry(
|
||||
key=fkey,
|
||||
meta=ometa,
|
||||
hash_info=ohi,
|
||||
)
|
||||
|
||||
entry = DataIndexEntry(
|
||||
key=key,
|
||||
meta=out.meta,
|
||||
hash_info=out.hash_info,
|
||||
loaded=loaded,
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -201,12 +216,12 @@ def _load_storage_from_out(storage_map, key, out):
|
||||
FileStorage(
|
||||
key,
|
||||
fs_cache.fs,
|
||||
fs_cache.fs.path.join(
|
||||
fs_cache.fs.join(
|
||||
fs_cache.path, dep.fs.protocol, tokenize(dep.fs_path)
|
||||
),
|
||||
)
|
||||
)
|
||||
storage_map.add_remote(FileStorage(key, dep.fs, dep.fs_path))
|
||||
storage_map.add_remote(FileStorage(key, dep.fs, dep.fs_path, read_only=True))
|
||||
|
||||
|
||||
class Index:
|
||||
@@ -414,7 +429,7 @@ class Index:
|
||||
by_workspace[workspace].add(key)
|
||||
|
||||
for path in _collect_top_level_metrics(self.repo):
|
||||
key = self.repo.fs.path.relparts(path, self.repo.root_dir)
|
||||
key = self.repo.fs.relparts(path, self.repo.root_dir)
|
||||
by_workspace["repo"].add(key)
|
||||
|
||||
return dict(by_workspace)
|
||||
@@ -432,7 +447,7 @@ class Index:
|
||||
param_paths = chain(param_paths, [default_file])
|
||||
|
||||
for path in param_paths:
|
||||
key = self.repo.fs.path.relparts(path, self.repo.root_dir)
|
||||
key = self.repo.fs.relparts(path, self.repo.root_dir)
|
||||
by_workspace["repo"].add(key)
|
||||
|
||||
return dict(by_workspace)
|
||||
@@ -451,7 +466,7 @@ class Index:
|
||||
by_workspace[workspace].add(key)
|
||||
|
||||
for path in self._plot_sources:
|
||||
key = self.repo.fs.path.parts(path)
|
||||
key = self.repo.fs.parts(path)
|
||||
by_workspace["repo"].add(key)
|
||||
|
||||
return dict(by_workspace)
|
||||
@@ -701,8 +716,8 @@ class IndexView:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
workspace, key = out.index_key
|
||||
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
|
||||
key = key + out.fs.path.relparts(filter_info, out.fs_path)
|
||||
if filter_info and out.fs.isin(filter_info, out.fs_path):
|
||||
key = key + out.fs.relparts(filter_info, out.fs_path)
|
||||
entry = self._index.data[workspace].get(key)
|
||||
if entry and entry.meta and entry.meta.isdir:
|
||||
prefixes[workspace].recursive.add(key)
|
||||
@@ -718,8 +733,8 @@ class IndexView:
|
||||
continue
|
||||
|
||||
workspace, key = out.index_key
|
||||
if filter_info and out.fs.path.isin(filter_info, out.fs_path):
|
||||
key = key + out.fs.path.relparts(filter_info, out.fs_path)
|
||||
if filter_info and out.fs.isin(filter_info, out.fs_path):
|
||||
key = key + out.fs.relparts(filter_info, out.fs_path)
|
||||
ret[workspace].add(key)
|
||||
|
||||
return dict(ret)
|
||||
@@ -782,7 +797,7 @@ def build_data_index( # noqa: C901, PLR0912
|
||||
data = DataIndex()
|
||||
parents = set()
|
||||
for key in index.data_keys.get(workspace, set()):
|
||||
out_path = fs.path.join(path, *key)
|
||||
out_path = fs.join(path, *key)
|
||||
|
||||
for key_len in range(1, len(key)):
|
||||
parents.add(key[:key_len])
|
||||
@@ -827,7 +842,7 @@ def build_data_index( # noqa: C901, PLR0912
|
||||
callback.relative_update(1)
|
||||
|
||||
for key in parents:
|
||||
parent_path = fs.path.join(path, *key)
|
||||
parent_path = fs.join(path, *key)
|
||||
if not fs.exists(parent_path):
|
||||
continue
|
||||
direntry = DataIndexEntry(key=key, meta=Meta(isdir=True), loaded=True)
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ def _ls(
|
||||
if not recursive:
|
||||
files.update(dirs)
|
||||
|
||||
parts = fs.path.relparts(root, fs_path)
|
||||
parts = fs.relparts(root, fs_path)
|
||||
if parts == (".",):
|
||||
parts = ()
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ def ls_url(url, *, fs_config=None, recursive=False, config=None):
|
||||
|
||||
for info in files.values():
|
||||
ls_info = {
|
||||
"path": fs.path.relpath(info["name"], fs_path),
|
||||
"path": fs.relpath(info["name"], fs_path),
|
||||
"isdir": info["type"] == "directory",
|
||||
"size": info.get("size"),
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ from funcy import ldistinct
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.log import logger
|
||||
from dvc.repo import locked
|
||||
from dvc.scm import NoSCMError
|
||||
from dvc.utils import as_posix, expand_paths
|
||||
from dvc.utils.collections import ensure_list
|
||||
@@ -35,10 +34,10 @@ logger = logger.getChild(__name__)
|
||||
def _collect_top_level_metrics(repo: "Repo") -> Iterator[str]:
|
||||
top_metrics = repo.index._metrics
|
||||
for dvcfile, metrics in top_metrics.items():
|
||||
wdir = repo.fs.path.relpath(repo.fs.path.parent(dvcfile), repo.root_dir)
|
||||
wdir = repo.fs.relpath(repo.fs.parent(dvcfile), repo.root_dir)
|
||||
for file in metrics:
|
||||
path = repo.fs.path.join(wdir, as_posix(file))
|
||||
yield repo.fs.path.normpath(path)
|
||||
path = repo.fs.join(wdir, as_posix(file))
|
||||
yield repo.fs.normpath(path)
|
||||
|
||||
|
||||
def _extract_metrics(metrics, path: str):
|
||||
@@ -101,7 +100,7 @@ def _collect_metrics(
|
||||
|
||||
if not targets or outs_only:
|
||||
outs = metrics_from_target(repo, stages) if stages else repo.index.metrics
|
||||
relpath = repo.fs.path.relpath
|
||||
relpath = repo.fs.relpath
|
||||
metrics.extend(relpath(out.fs_path, repo.root_dir) for out in outs)
|
||||
|
||||
if not targets and not outs_only and not stages:
|
||||
@@ -128,8 +127,8 @@ class Result(TypedDict, total=False):
|
||||
|
||||
|
||||
def to_relpath(fs: "FileSystem", root_dir: str, d: Result) -> Result:
|
||||
relpath = fs.path.relpath
|
||||
cwd = fs.path.getcwd()
|
||||
relpath = fs.relpath
|
||||
cwd = fs.getcwd()
|
||||
|
||||
start = relpath(cwd, root_dir)
|
||||
data = d.get("data")
|
||||
@@ -156,7 +155,7 @@ def _gather_metrics(
|
||||
fs = repo.dvcfs
|
||||
for fs_path, result in _read_metrics(fs, files, cache=True):
|
||||
repo_path = fs_path.lstrip(fs.root_marker)
|
||||
repo_os_path = os.sep.join(fs.path.parts(repo_path))
|
||||
repo_os_path = os.sep.join(fs.parts(repo_path))
|
||||
if not isinstance(result, Exception):
|
||||
data.update({repo_os_path: FileResult(data=result)})
|
||||
continue
|
||||
@@ -185,7 +184,6 @@ def _hide_workspace(
|
||||
return res
|
||||
|
||||
|
||||
@locked
|
||||
def show(
|
||||
repo: "Repo",
|
||||
targets: Optional[List[str]] = None,
|
||||
|
||||
@@ -71,7 +71,7 @@ def erepo_factory(url, root_dir, cache_config):
|
||||
_config = cache_config.copy()
|
||||
if os.path.isdir(url):
|
||||
fs = fs or localfs
|
||||
repo_path = os.path.join(url, *fs.path.relparts(path, root_dir))
|
||||
repo_path = os.path.join(url, *fs.relparts(path, root_dir))
|
||||
_config.update(_get_remote_config(repo_path))
|
||||
return Repo(path, fs=fs, config=_config, **_kwargs)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Tuple, Un
|
||||
|
||||
from dvc.dependency.param import ParamsDependency, read_param_file
|
||||
from dvc.log import logger
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.metrics.show import FileResult, Result
|
||||
from dvc.stage import PipelineStage
|
||||
from dvc.utils import as_posix, expand_paths
|
||||
@@ -21,10 +20,10 @@ logger = logger.getChild(__name__)
|
||||
def _collect_top_level_params(repo: "Repo") -> Iterator[str]:
|
||||
top_params = repo.index._params
|
||||
for dvcfile, params in top_params.items():
|
||||
wdir = repo.fs.path.relpath(repo.fs.path.parent(dvcfile), repo.root_dir)
|
||||
wdir = repo.fs.relpath(repo.fs.parent(dvcfile), repo.root_dir)
|
||||
for file in params:
|
||||
path = repo.fs.path.join(wdir, as_posix(file))
|
||||
yield repo.fs.path.normpath(path)
|
||||
path = repo.fs.join(wdir, as_posix(file))
|
||||
yield repo.fs.normpath(path)
|
||||
|
||||
|
||||
def params_from_target(
|
||||
@@ -55,7 +54,7 @@ def _collect_params(
|
||||
|
||||
if not targets or stages:
|
||||
deps = params_from_target(repo, stages) if stages else repo.index.params
|
||||
relpath = repo.fs.path.relpath
|
||||
relpath = repo.fs.relpath
|
||||
params.extend(
|
||||
{relpath(dep.fs_path, repo.root_dir): list(dep.params)} for dep in deps
|
||||
)
|
||||
@@ -93,7 +92,7 @@ def _collect_vars(repo, params, stages=None) -> Dict:
|
||||
# to reduce noise and duplication, they are skipped
|
||||
|
||||
# `file` is relative
|
||||
abspath = repo.fs.path.abspath(file)
|
||||
abspath = repo.fs.abspath(file)
|
||||
repo_path = repo.dvcfs.from_os_path(abspath)
|
||||
if repo_path in params:
|
||||
continue
|
||||
@@ -138,7 +137,7 @@ def _gather_params(
|
||||
fs = repo.dvcfs
|
||||
for fs_path, result in _read_params(fs, files_keypaths, cache=True):
|
||||
repo_path = fs_path.lstrip(fs.root_marker)
|
||||
repo_os_path = os.sep.join(fs.path.parts(repo_path))
|
||||
repo_os_path = os.sep.join(fs.parts(repo_path))
|
||||
if not isinstance(result, Exception):
|
||||
data.update({repo_os_path: FileResult(data=result)})
|
||||
continue
|
||||
@@ -158,7 +157,6 @@ def _gather_params(
|
||||
return data
|
||||
|
||||
|
||||
@locked
|
||||
def show(
|
||||
repo: "Repo",
|
||||
targets: Optional[List[str]] = None,
|
||||
|
||||
@@ -386,7 +386,7 @@ def _matches(targets, config_file, plot_id):
|
||||
|
||||
|
||||
def _normpath(path):
|
||||
# TODO dvcfs.path.normopath normalizes to windows path on Windows
|
||||
# TODO dvcfs.normopath normalizes to windows path on Windows
|
||||
# even though other methods work as expected
|
||||
import posixpath
|
||||
|
||||
@@ -399,7 +399,7 @@ def _relpath(fs, path):
|
||||
# and invoking from some subdir `dvcfile.relpath` returns strange long
|
||||
# relative paths
|
||||
# ("../../../../../../dvc.yaml") - investigate
|
||||
return fs.path.relpath(fs.path.join("/", fs.from_os_path(path)), fs.path.getcwd())
|
||||
return fs.relpath(fs.join("/", fs.from_os_path(path)), fs.getcwd())
|
||||
|
||||
|
||||
def _collect_output_plots(repo, targets, props, onerror: Optional[Callable] = None):
|
||||
@@ -413,7 +413,7 @@ def _collect_output_plots(repo, targets, props, onerror: Optional[Callable] = No
|
||||
if _matches(targets, config_path, str(plot)):
|
||||
unpacked = unpack_if_dir(
|
||||
fs,
|
||||
_normpath(fs.path.join(wdir_relpath, plot.def_path)),
|
||||
_normpath(fs.join(wdir_relpath, plot.def_path)),
|
||||
props={**plot_props, **props},
|
||||
onerror=onerror,
|
||||
)
|
||||
@@ -438,7 +438,7 @@ def _adjust_sources(fs, plot_props, config_dir):
|
||||
old_y = new_plot_props.pop("y", {})
|
||||
new_y = {}
|
||||
for filepath, val in old_y.items():
|
||||
new_y[_normpath(fs.path.join(config_dir, filepath))] = val
|
||||
new_y[_normpath(fs.join(config_dir, filepath))] = val
|
||||
new_plot_props["y"] = new_y
|
||||
return new_plot_props
|
||||
|
||||
@@ -452,13 +452,13 @@ def _resolve_definitions(
|
||||
onerror: Optional[Callable[[Any], Any]] = None,
|
||||
):
|
||||
config_path = os.fspath(config_path)
|
||||
config_dir = fs.path.dirname(config_path)
|
||||
config_dir = fs.dirname(config_path)
|
||||
result: Dict[str, Dict] = {}
|
||||
for plot_id, plot_props in definitions.items():
|
||||
if plot_props is None:
|
||||
plot_props = {}
|
||||
if _id_is_path(plot_props):
|
||||
data_path = _normpath(fs.path.join(config_dir, plot_id))
|
||||
data_path = _normpath(fs.join(config_dir, plot_id))
|
||||
if _matches(targets, config_path, plot_id):
|
||||
unpacked = unpack_if_dir(
|
||||
fs,
|
||||
|
||||
+39
-12
@@ -6,26 +6,49 @@ from dvc.ui import ui
|
||||
from . import locked
|
||||
|
||||
|
||||
def _rebuild(idx, path, fs, cb):
|
||||
from dvc_data.index import DataIndex, DataIndexEntry, Meta
|
||||
|
||||
new = DataIndex()
|
||||
items = list(idx.items())
|
||||
|
||||
cb.set_size(len(items))
|
||||
for key, entry in items:
|
||||
if entry.meta and entry.meta.isdir:
|
||||
meta = Meta(isdir=True)
|
||||
else:
|
||||
try:
|
||||
meta = Meta.from_info(fs.info(fs.join(path, *key)), fs.protocol)
|
||||
except FileNotFoundError:
|
||||
meta = None
|
||||
|
||||
if meta:
|
||||
new.add(DataIndexEntry(key=key, meta=meta))
|
||||
|
||||
cb.relative_update(1)
|
||||
|
||||
return new
|
||||
|
||||
|
||||
def _update_meta(index, **kwargs):
|
||||
from dvc.repo.index import build_data_index
|
||||
from dvc.repo.worktree import _merge_push_meta, worktree_view_by_remotes
|
||||
|
||||
stages = set()
|
||||
for remote_name, idx in worktree_view_by_remotes(index, push=True, **kwargs):
|
||||
remote = index.repo.cloud.get_remote(remote_name)
|
||||
|
||||
with ui.progress("Collecting", unit="entry") as pb:
|
||||
new = build_data_index(
|
||||
idx,
|
||||
remote.path,
|
||||
remote.fs,
|
||||
callback=pb.as_callback(),
|
||||
)
|
||||
if not remote.fs.version_aware:
|
||||
continue
|
||||
|
||||
with ui.progress(
|
||||
desc=f"Collecting {remote.path} on {remote.fs.protocol}",
|
||||
unit="entry",
|
||||
leave=True,
|
||||
) as pb:
|
||||
cb = pb.as_callback()
|
||||
new = _rebuild(idx.data["repo"], remote.path, remote.fs, cb)
|
||||
|
||||
for out in idx.outs:
|
||||
if not remote.fs.version_aware:
|
||||
continue
|
||||
|
||||
_merge_push_meta(out, new, remote.name)
|
||||
stages.add(out.stage)
|
||||
|
||||
@@ -46,6 +69,7 @@ def push( # noqa: PLR0913
|
||||
all_commits=False,
|
||||
run_cache=False,
|
||||
revs=None,
|
||||
workspace=True,
|
||||
glob=False,
|
||||
):
|
||||
from fsspec.utils import tokenize
|
||||
@@ -87,6 +111,7 @@ def push( # noqa: PLR0913
|
||||
recursive=recursive,
|
||||
all_commits=all_commits,
|
||||
revs=revs,
|
||||
workspace=workspace,
|
||||
push=True,
|
||||
)
|
||||
|
||||
@@ -98,6 +123,7 @@ def push( # noqa: PLR0913
|
||||
with ui.progress(
|
||||
desc="Collecting",
|
||||
unit="entry",
|
||||
leave=True,
|
||||
) as pb:
|
||||
data = collect(
|
||||
[idx.data["repo"] for idx in indexes.values()],
|
||||
@@ -112,7 +138,8 @@ def push( # noqa: PLR0913
|
||||
try:
|
||||
with ui.progress(
|
||||
desc="Pushing",
|
||||
unit="file",
|
||||
bar_format="{desc}",
|
||||
leave=True,
|
||||
) as pb:
|
||||
push_transferred, push_failed = ipush(
|
||||
data,
|
||||
|
||||
+3
-3
@@ -219,7 +219,7 @@ class StageLoad:
|
||||
self, path: Optional[str] = None, name: Optional[str] = None
|
||||
) -> str:
|
||||
if path:
|
||||
return self.repo.fs.path.abspath(path)
|
||||
return self.repo.fs.abspath(path)
|
||||
|
||||
path = PROJECT_FILE
|
||||
logger.debug("Assuming '%s' to be a stage inside '%s'", name, path)
|
||||
@@ -348,7 +348,7 @@ class StageLoad:
|
||||
if recursive and self.fs.isdir(target):
|
||||
from dvc.repo.graph import collect_inside_path
|
||||
|
||||
path = self.fs.path.abspath(target)
|
||||
path = self.fs.abspath(target)
|
||||
return collect_inside_path(path, graph or self.repo.index.graph)
|
||||
|
||||
stages = self.from_target(target, glob=glob)
|
||||
@@ -393,7 +393,7 @@ class StageLoad:
|
||||
if not (recursive and self.fs.isdir(target)):
|
||||
try:
|
||||
(out,) = self.repo.find_outs_by_path(target, strict=False)
|
||||
return [StageInfo(out.stage, self.fs.path.abspath(target))]
|
||||
return [StageInfo(out.stage, self.fs.abspath(target))]
|
||||
except OutputNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ def build_outs_trie(stages):
|
||||
|
||||
for stage in stages:
|
||||
for out in stage.outs:
|
||||
out_key = out.fs.path.parts(out.fs_path)
|
||||
out_key = out.fs.parts(out.fs_path)
|
||||
|
||||
# Check for dup outs
|
||||
if out_key in outs:
|
||||
|
||||
+10
-11
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Tuple, Union
|
||||
|
||||
from funcy import first
|
||||
|
||||
from dvc.fs.callbacks import Callback
|
||||
from dvc.log import logger
|
||||
from dvc.stage.exceptions import StageUpdateError
|
||||
|
||||
@@ -132,10 +131,11 @@ def _merge_push_meta( # noqa: C901
|
||||
entries.append(entry)
|
||||
if entry.meta is not None and entry.meta.isdir:
|
||||
continue
|
||||
fs_path = repo.fs.path.join(repo.root_dir, *subkey)
|
||||
meta, hash_info = old_tree.get(
|
||||
repo.fs.path.relparts(fs_path, out.fs_path)
|
||||
) or (None, None)
|
||||
fs_path = repo.fs.join(repo.root_dir, *subkey)
|
||||
meta, hash_info = old_tree.get(repo.fs.relparts(fs_path, out.fs_path)) or (
|
||||
None,
|
||||
None,
|
||||
)
|
||||
entry.hash_info = hash_info
|
||||
if entry.meta:
|
||||
entry.meta.remote = remote
|
||||
@@ -247,11 +247,12 @@ def _fetch_out_changes(
|
||||
remote_index: Union["DataIndex", "DataIndexView"],
|
||||
remote: "Remote",
|
||||
):
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
from dvc_data.index.checkout import apply, compare
|
||||
|
||||
old, new = _get_diff_indexes(out, local_index, remote_index)
|
||||
|
||||
with Callback.as_tqdm_callback(
|
||||
with TqdmCallback(
|
||||
unit="entry",
|
||||
desc="Comparing indexes",
|
||||
) as cb:
|
||||
@@ -265,9 +266,7 @@ def _fetch_out_changes(
|
||||
)
|
||||
|
||||
total = len(new)
|
||||
with Callback.as_tqdm_callback(
|
||||
unit="file", desc=f"Updating '{out}'", disable=total == 0
|
||||
) as cb:
|
||||
with TqdmCallback(unit="file", desc=f"Updating '{out}'", disable=total == 0) as cb:
|
||||
cb.set_size(total)
|
||||
apply(
|
||||
diff,
|
||||
@@ -356,11 +355,11 @@ def _get_update_diff_index(
|
||||
# downloading
|
||||
if out.isdir():
|
||||
if not entry.meta.isdir:
|
||||
fs_path = repo.fs.path.join(repo.root_dir, *entry.key)
|
||||
fs_path = repo.fs.join(repo.root_dir, *entry.key)
|
||||
tree = out.obj
|
||||
assert isinstance(tree, Tree)
|
||||
_, entry.hash_info = tree.get( # type: ignore[misc]
|
||||
repo.fs.path.relparts(fs_path, out.fs_path)
|
||||
repo.fs.relparts(fs_path, out.fs_path)
|
||||
)
|
||||
else:
|
||||
entry.hash_info = out.hash_info
|
||||
|
||||
+3
-3
@@ -43,10 +43,10 @@ class RWLockFileFormatError(DvcException):
|
||||
|
||||
@contextmanager
|
||||
def _edit_rwlock(lock_dir, fs, hardlink):
|
||||
path = fs.path.join(lock_dir, RWLOCK_FILE)
|
||||
path = fs.join(lock_dir, RWLOCK_FILE)
|
||||
|
||||
rwlock_guard = make_lock(
|
||||
fs.path.join(lock_dir, RWLOCK_LOCK),
|
||||
fs.join(lock_dir, RWLOCK_LOCK),
|
||||
tmp_dir=lock_dir,
|
||||
hardlink_lock=hardlink,
|
||||
)
|
||||
@@ -82,7 +82,7 @@ def _check_blockers(tmp_dir, lock, info, *, mode, waiters): # noqa: C901, PLR09
|
||||
to_release = defaultdict(list)
|
||||
for path, infos in lock[mode].items():
|
||||
for waiter_path in waiters:
|
||||
if localfs.path.overlaps(waiter_path, path):
|
||||
if localfs.overlaps(waiter_path, path):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
+29
@@ -24,6 +24,8 @@ from dvc.progress import Tqdm
|
||||
if TYPE_CHECKING:
|
||||
from scmrepo.progress import GitProgressEvent
|
||||
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
|
||||
class SCMError(DvcException):
|
||||
"""Base class for source control management errors."""
|
||||
@@ -262,3 +264,30 @@ def iter_revs(
|
||||
|
||||
rev_resolver = partial(resolve_rev, scm)
|
||||
return group_by(rev_resolver, results)
|
||||
|
||||
|
||||
def lfs_prefetch(fs: "FileSystem", paths: List[str]):
|
||||
from scmrepo.git.lfs import fetch as _lfs_fetch
|
||||
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc.fs.git import GitFileSystem
|
||||
|
||||
if isinstance(fs, DVCFileSystem) and isinstance(fs.repo.fs, GitFileSystem):
|
||||
git_fs = fs.repo.fs
|
||||
scm = fs.repo.scm
|
||||
assert isinstance(scm, Git)
|
||||
else:
|
||||
return
|
||||
|
||||
try:
|
||||
if "filter=lfs" not in git_fs.open(".gitattributes").read():
|
||||
return
|
||||
except OSError:
|
||||
return
|
||||
with TqdmGit(desc="Checking for Git-LFS objects") as pbar:
|
||||
_lfs_fetch(
|
||||
scm,
|
||||
[git_fs.rev],
|
||||
include=[(path if path.startswith("/") else f"/{path}") for path in paths],
|
||||
progress=pbar.update_git,
|
||||
)
|
||||
|
||||
@@ -656,7 +656,7 @@ class Stage(params.StageParams):
|
||||
|
||||
def filter_outs(self, fs_path) -> Iterable["Output"]:
|
||||
def _func(o):
|
||||
return o.fs.path.isin_or_eq(fs_path, o.fs_path)
|
||||
return o.fs.isin_or_eq(fs_path, o.fs_path)
|
||||
|
||||
return filter(_func, self.outs) if fs_path else self.outs
|
||||
|
||||
|
||||
+11
-11
@@ -181,9 +181,9 @@ class StageCache:
|
||||
|
||||
path = self._get_cache_path(cache_key, cache_value)
|
||||
local_fs = self.repo.cache.legacy.fs
|
||||
parent = local_fs.path.parent(path)
|
||||
parent = local_fs.parent(path)
|
||||
self.repo.cache.legacy.makedirs(parent)
|
||||
tmp = local_fs.path.join(parent, fs.utils.tmp_fname())
|
||||
tmp = local_fs.join(parent, fs.utils.tmp_fname())
|
||||
assert os.path.exists(parent)
|
||||
assert os.path.isdir(parent)
|
||||
dump_yaml(tmp, cache)
|
||||
@@ -231,12 +231,12 @@ class StageCache:
|
||||
|
||||
def transfer(self, from_odb, to_odb):
|
||||
from dvc.fs import HTTPFileSystem, LocalFileSystem
|
||||
from dvc.fs.callbacks import Callback
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
|
||||
from_fs = from_odb.fs
|
||||
to_fs = to_odb.fs
|
||||
func = fs.generic.log_exceptions(fs.generic.copy)
|
||||
runs = from_fs.path.join(from_odb.path, "runs")
|
||||
runs = from_fs.join(from_odb.path, "runs")
|
||||
|
||||
http_odb = next(
|
||||
(odb for odb in (from_odb, to_odb) if isinstance(odb.fs, HTTPFileSystem)),
|
||||
@@ -252,21 +252,21 @@ class StageCache:
|
||||
return ret
|
||||
|
||||
for src in from_fs.find(runs):
|
||||
rel = from_fs.path.relpath(src, from_odb.path)
|
||||
rel = from_fs.relpath(src, from_odb.path)
|
||||
if not isinstance(to_fs, LocalFileSystem):
|
||||
rel = from_fs.path.as_posix(rel)
|
||||
rel = from_fs.as_posix(rel)
|
||||
|
||||
dst = to_fs.path.join(to_odb.path, rel)
|
||||
key = to_fs.path.parent(dst)
|
||||
dst = to_fs.join(to_odb.path, rel)
|
||||
key = to_fs.parent(dst)
|
||||
# check if any build cache already exists for this key
|
||||
# TODO: check if MaxKeys=1 or something like that applies
|
||||
# or otherwise this will take a lot of time!
|
||||
if to_fs.exists(key) and first(to_fs.find(key)):
|
||||
continue
|
||||
|
||||
src_name = from_fs.path.name(src)
|
||||
parent_name = from_fs.path.name(from_fs.path.parent(src))
|
||||
with Callback.as_tqdm_callback(
|
||||
src_name = from_fs.name(src)
|
||||
parent_name = from_fs.name(from_fs.parent(src))
|
||||
with TqdmCallback(
|
||||
desc=src_name,
|
||||
bytes=True,
|
||||
) as cb:
|
||||
|
||||
+2
-2
@@ -184,9 +184,9 @@ def resolve_wdir(wdir, path):
|
||||
|
||||
|
||||
def resolve_paths(fs, path, wdir=None):
|
||||
path = fs.path.abspath(path)
|
||||
path = fs.abspath(path)
|
||||
wdir = wdir or os.curdir
|
||||
wdir = fs.path.abspath(fs.path.join(fs.path.dirname(path), wdir))
|
||||
wdir = fs.abspath(fs.join(fs.dirname(path), wdir))
|
||||
return path, wdir
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
def test_fetch(bench_dvc, tmp_dir, dvc, make_dataset, remote):
|
||||
make_dataset(cache=False, dvcfile=True, files=False, remote=True)
|
||||
bench_dvc("fetch")
|
||||
@@ -1,3 +1,7 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.flaky(max_runs=3, min_passes=1)
|
||||
def test_get(bench_dvc, tmp_dir, scm, dvc, make_dataset, remote):
|
||||
dataset = make_dataset(
|
||||
cache=False, files=False, dvcfile=True, commit=True, remote=True
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.flaky(max_runs=3, min_passes=1)
|
||||
def test_import(bench_dvc, tmp_dir, scm, dvc, make_dataset, remote):
|
||||
dataset = make_dataset(
|
||||
cache=False, files=False, dvcfile=True, commit=True, remote=True
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
def test_pull(bench_dvc, tmp_dir, dvc, make_dataset, remote):
|
||||
make_dataset(cache=False, dvcfile=True, files=False, remote=True)
|
||||
bench_dvc("pull")
|
||||
@@ -11,7 +11,8 @@ def test_sharing(bench_dvc, tmp_dir, dvc, dataset, remote):
|
||||
shutil.rmtree(dataset)
|
||||
shutil.rmtree(tmp_dir / ".dvc" / "cache")
|
||||
|
||||
bench_dvc("pull")
|
||||
bench_dvc("pull", name="noop")
|
||||
bench_dvc("fetch")
|
||||
bench_dvc("fetch", name="noop")
|
||||
|
||||
bench_dvc("checkout")
|
||||
bench_dvc("checkout", name="noop")
|
||||
|
||||
@@ -5,7 +5,6 @@ from subprocess import check_output
|
||||
from typing import Dict, Optional
|
||||
|
||||
import pytest
|
||||
import virtualenv
|
||||
from dulwich.porcelain import clone
|
||||
from funcy import first
|
||||
from packaging import version
|
||||
@@ -24,6 +23,8 @@ class VirtualEnv:
|
||||
self.bin = self.path / ("Scripts" if os.name == "nt" else "bin")
|
||||
|
||||
def create(self) -> None:
|
||||
import virtualenv
|
||||
|
||||
virtualenv.cli_run([os.fspath(self.path)])
|
||||
|
||||
def run(self, cmd: str, *args: str, env: Optional[Dict[str, str]] = None) -> None:
|
||||
@@ -192,7 +193,7 @@ def make_dataset(request, bench_config, tmp_dir, dvc_bench_git_repo):
|
||||
|
||||
path = tmp_dir / "dataset"
|
||||
root = dvc_bench_git_repo
|
||||
src = root / "data" / bench_config.size / "dataset"
|
||||
src = root / "data" / bench_config.dataset / "dataset"
|
||||
src_dvc = src.with_suffix(".dvc")
|
||||
|
||||
dvc = Repo(root)
|
||||
|
||||
@@ -4,6 +4,11 @@ DEFAULT_DVC_BENCH_GIT_REPO = "https://github.com/iterative/dvc-bench"
|
||||
DEFAULT_PROJECT_GIT_REPO = "https://github.com/iterative/example-get-started"
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
bconf = config.bench_config
|
||||
return "dvc-bench: (" f"dataset: '{bconf.dataset}'," f"revs: '{bconf.dvc_revs}'" ")"
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
str_revs = metafunc.config.getoption("--dvc-revs")
|
||||
revs = str_revs.split(",") if str_revs else [None]
|
||||
@@ -13,7 +18,7 @@ def pytest_generate_tests(metafunc):
|
||||
|
||||
class DVCBenchConfig:
|
||||
def __init__(self):
|
||||
self.size = "small"
|
||||
self.dataset = "small"
|
||||
self.dvc_bin = DEFAULT_DVC_BIN
|
||||
self.dvc_revs = None
|
||||
self.dvc_git_repo = DEFAULT_DVC_GIT_REPO
|
||||
@@ -29,7 +34,7 @@ def pytest_configure(config):
|
||||
)
|
||||
|
||||
config.bench_config = DVCBenchConfig()
|
||||
config.bench_config.size = config.getoption("--size")
|
||||
config.bench_config.dataset = config.getoption("--dataset")
|
||||
config.bench_config.dvc_bin = config.getoption("--dvc-bin")
|
||||
config.bench_config.dvc_revs = config.getoption("--dvc-revs")
|
||||
config.bench_config.dvc_git_repo = config.getoption("--dvc-git-repo")
|
||||
@@ -41,10 +46,10 @@ def pytest_configure(config):
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--size",
|
||||
choices=["tiny", "small", "large", "mnist"],
|
||||
"--dataset",
|
||||
type=str,
|
||||
default="small",
|
||||
help="Size of the dataset/datafile to use in tests",
|
||||
help="Dataset name to use in tests (e.g. tiny/small/large/mnist/etc)",
|
||||
)
|
||||
|
||||
parser.addoption(
|
||||
|
||||
@@ -32,6 +32,14 @@ class Cloud(ABC):
|
||||
def write_bytes(self, contents):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def unlink(self, missing_ok: bool = False) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def rmdir(self, recursive: bool = True) -> None:
|
||||
pass
|
||||
|
||||
def read_text(self, encoding=None, errors=None):
|
||||
if not encoding:
|
||||
encoding = locale.getpreferredencoding(False)
|
||||
|
||||
+38
-10
@@ -171,7 +171,7 @@ class TestRemoteVersionAware:
|
||||
(stage,) = tmp_dir.dvc_gen("foo", "foo")
|
||||
run_copy("foo", "foo_copy", name="copy")
|
||||
|
||||
dvc.push()
|
||||
assert dvc.push()
|
||||
assert (remote_version_aware / "foo").read_text() == "foo"
|
||||
assert (remote_version_aware / "foo_copy").read_text() == "foo"
|
||||
foo_dvc = (tmp_dir / "foo.dvc").read_text()
|
||||
@@ -185,26 +185,26 @@ class TestRemoteVersionAware:
|
||||
remove(tmp_dir / "foo")
|
||||
remove(tmp_dir / "foo_copy")
|
||||
|
||||
dvc.pull()
|
||||
assert dvc.pull()
|
||||
assert (tmp_dir / "foo").read_text() == "foo"
|
||||
assert (tmp_dir / "foo_copy").read_text() == "foo"
|
||||
assert (tmp_dir / "foo.dvc").read_text() == foo_dvc
|
||||
assert (tmp_dir / "dvc.lock").read_text() == dvc_lock
|
||||
|
||||
dvc.push()
|
||||
assert not dvc.push()
|
||||
assert (remote_version_aware / "foo").read_text() == "foo"
|
||||
assert (remote_version_aware / "foo_copy").read_text() == "foo"
|
||||
assert (tmp_dir / "foo.dvc").read_text() == foo_dvc
|
||||
assert (tmp_dir / "dvc.lock").read_text() == dvc_lock
|
||||
|
||||
dvc.reproduce()
|
||||
dvc.push()
|
||||
assert not dvc.push()
|
||||
assert (remote_version_aware / "foo").read_text() == "foo"
|
||||
assert (remote_version_aware / "foo_copy").read_text() == "foo"
|
||||
assert (tmp_dir / "foo.dvc").read_text() == foo_dvc
|
||||
assert (tmp_dir / "dvc.lock").read_text() == dvc_lock
|
||||
|
||||
def test_dir(self, tmp_dir, dvc, run_copy, remote_version_aware):
|
||||
def test_dir(self, tmp_dir, dvc, run_copy, remote_version_aware): # noqa: PLR0915
|
||||
(stage,) = tmp_dir.dvc_gen(
|
||||
{
|
||||
"data_dir": {
|
||||
@@ -215,7 +215,8 @@ class TestRemoteVersionAware:
|
||||
}
|
||||
)
|
||||
|
||||
dvc.push()
|
||||
assert not dvc.fetch()
|
||||
assert dvc.push()
|
||||
|
||||
data_dir_dvc = (tmp_dir / "data_dir.dvc").read_text()
|
||||
assert "files" in data_dir_dvc
|
||||
@@ -230,7 +231,7 @@ class TestRemoteVersionAware:
|
||||
remove(dvc.cache.local.path)
|
||||
remove(tmp_dir / "data_dir")
|
||||
|
||||
dvc.pull()
|
||||
assert dvc.pull()
|
||||
assert (tmp_dir / "data_dir" / "data").read_text() == "data"
|
||||
assert (
|
||||
tmp_dir / "data_dir" / "data_sub_dir" / "data_sub"
|
||||
@@ -240,20 +241,47 @@ class TestRemoteVersionAware:
|
||||
run_copy("data_dir", "data_dir_copy", name="copy")
|
||||
dvc_lock = (tmp_dir / "dvc.lock").read_text()
|
||||
|
||||
dvc.push()
|
||||
assert dvc.push()
|
||||
assert (remote_version_aware / "data_dir").exists()
|
||||
assert (remote_version_aware / "data_dir" / "data").exists()
|
||||
assert (remote_version_aware / "data_dir_copy").exists()
|
||||
assert (remote_version_aware / "data_dir_copy" / "data").exists()
|
||||
assert (tmp_dir / "data_dir.dvc").read_text() == data_dir_dvc
|
||||
assert (tmp_dir / "dvc.lock").read_text() != dvc_lock
|
||||
dvc_lock = (tmp_dir / "dvc.lock").read_text()
|
||||
|
||||
dvc.push()
|
||||
assert not dvc.push()
|
||||
assert (remote_version_aware / "data_dir").exists()
|
||||
assert (remote_version_aware / "data_dir" / "data").exists()
|
||||
assert (remote_version_aware / "data_dir_copy").exists()
|
||||
assert (remote_version_aware / "data_dir_copy" / "data").exists()
|
||||
assert (tmp_dir / "data_dir.dvc").read_text() == data_dir_dvc
|
||||
assert (tmp_dir / "dvc.lock").read_text() == dvc_lock
|
||||
|
||||
dvc.cache.local.clear()
|
||||
remove(tmp_dir / "data_dir")
|
||||
dvc.push()
|
||||
remove(tmp_dir / "data_dir_copy")
|
||||
assert not dvc.push()
|
||||
assert (remote_version_aware / "data_dir").exists()
|
||||
assert (remote_version_aware / "data_dir" / "data").exists()
|
||||
assert (remote_version_aware / "data_dir_copy").exists()
|
||||
assert (remote_version_aware / "data_dir_copy" / "data").exists()
|
||||
assert (tmp_dir / "data_dir.dvc").read_text() == data_dir_dvc
|
||||
assert (tmp_dir / "dvc.lock").read_text() == dvc_lock
|
||||
|
||||
(remote_version_aware / "data_dir").rmdir()
|
||||
(remote_version_aware / "data_dir_copy").rmdir()
|
||||
assert not (remote_version_aware / "data_dir").exists()
|
||||
assert not (remote_version_aware / "data_dir_copy").exists()
|
||||
assert dvc.pull()
|
||||
assert (tmp_dir / "data_dir" / "data").read_text() == "data"
|
||||
assert (
|
||||
tmp_dir / "data_dir" / "data_sub_dir" / "data_sub"
|
||||
).read_text() == "data_sub"
|
||||
assert (tmp_dir / "data_dir_copy" / "data").read_text() == "data"
|
||||
assert (
|
||||
tmp_dir / "data_dir_copy" / "data_sub_dir" / "data_sub"
|
||||
).read_text() == "data_sub"
|
||||
|
||||
|
||||
class TestRemoteWorktree:
|
||||
|
||||
@@ -163,8 +163,8 @@ class TestImportURLVersionAware:
|
||||
|
||||
|
||||
def match_files(fs, entries, expected):
|
||||
entries_content = {(fs.path.normpath(d["path"]), d["isdir"]) for d in entries}
|
||||
expected_content = {(fs.path.normpath(d["path"]), d["isdir"]) for d in expected}
|
||||
entries_content = {(fs.normpath(d["path"]), d["isdir"]) for d in entries}
|
||||
expected_content = {(fs.normpath(d["path"]), d["isdir"]) for d in expected}
|
||||
assert entries_content == expected_content
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ class TestLsUrl:
|
||||
match_files(
|
||||
fs,
|
||||
result,
|
||||
[{"path": fs.path.join(fs_path, fname), "isdir": False}],
|
||||
[{"path": fs.join(fs_path, fname), "isdir": False}],
|
||||
)
|
||||
|
||||
def test_dir(self, cloud):
|
||||
|
||||
@@ -4,6 +4,7 @@ from rich.progress import (
|
||||
DownloadColumn,
|
||||
MofNCompleteColumn,
|
||||
Progress,
|
||||
SpinnerColumn,
|
||||
TextColumn,
|
||||
TimeElapsedColumn,
|
||||
TimeRemainingColumn,
|
||||
@@ -52,3 +53,24 @@ class RichTransferProgress(RichProgress):
|
||||
yield self.make_tasks_table(summary_tasks)
|
||||
self.columns = self.TRANSFER_COLS
|
||||
yield self.make_tasks_table(other_tasks)
|
||||
|
||||
|
||||
class DbDownloadProgress(RichProgress):
|
||||
PROGRESS_COLS = (
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
TextColumn("[progress.download]{task.completed:,} rows"),
|
||||
TextColumn("to [repr.filename]{task.fields[output]}"),
|
||||
)
|
||||
STATUS_COLS = (
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
TextColumn("to [repr.filename]{task.fields[output]}"),
|
||||
)
|
||||
|
||||
def get_renderables(self):
|
||||
if self.tasks:
|
||||
(task, *_) = self.tasks
|
||||
cols = self.PROGRESS_COLS if task.completed else self.STATUS_COLS
|
||||
self.columns = cols[1:] if task.finished else cols
|
||||
yield self.make_tasks_table(self.tasks)
|
||||
|
||||
@@ -287,7 +287,7 @@ def resolve_paths(repo, out, always_local=False):
|
||||
# urlparse interprets windows drive letters as URL scheme
|
||||
scheme = ""
|
||||
|
||||
if scheme or not localfs.path.isin_or_eq(abspath, repo.root_dir):
|
||||
if scheme or not localfs.isin_or_eq(abspath, repo.root_dir):
|
||||
wdir = os.getcwd()
|
||||
elif contains_symlink_up_to(dirname, repo.root_dir) or (
|
||||
os.path.isdir(abspath) and localfs.is_symlink(abspath)
|
||||
|
||||
-252
@@ -1,252 +0,0 @@
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from importlib.util import find_spec
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import cut_prefix, identity
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
|
||||
from . import packaging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agate import Table
|
||||
from dbt.config.profile import Profile
|
||||
from dbt.contracts.results import RunResult
|
||||
from rich.status import Status
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
class DbtInternalError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
def _check_dbt(action: Optional[str]):
|
||||
if not (find_spec("dbt") and find_spec("dbt.cli")):
|
||||
action = f" {action}" if action else ""
|
||||
raise DvcException(f"Could not run{action}. dbt-core is not installed")
|
||||
packaging.check_required_version(pkg="dbt-core")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def check_dbt(action: Optional[str]):
|
||||
_check_dbt(action)
|
||||
yield
|
||||
|
||||
|
||||
def _ref(
|
||||
name: str,
|
||||
package: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
) -> str:
|
||||
parts: List[str] = []
|
||||
if package:
|
||||
parts.append(repr(package))
|
||||
|
||||
parts.append(repr(name))
|
||||
if version:
|
||||
parts.append(f"{version=}")
|
||||
|
||||
inner = ",".join(parts)
|
||||
return "{{ ref(" + inner + ") }}"
|
||||
|
||||
|
||||
def _kw_to_cmd_args(**kwargs: Union[None, bool, str, int]) -> List[str]:
|
||||
args: List[str] = []
|
||||
for key, value in kwargs.items():
|
||||
key = key.replace("_", "-")
|
||||
if value is None:
|
||||
continue # skip creating a flag in this case
|
||||
if value is True:
|
||||
args.append(f"--{key}")
|
||||
elif value is False:
|
||||
args.append(f"--no-{key}")
|
||||
else:
|
||||
args.extend([f"--{key}", str(value)])
|
||||
return args
|
||||
|
||||
|
||||
def _dbt_invoke(*posargs: str, quiet: bool = True, **kw: Union[None, bool, str, int]):
|
||||
from dbt.cli.main import dbtRunner
|
||||
|
||||
args = _kw_to_cmd_args(quiet=quiet or None) # global options
|
||||
args.extend([*posargs, *_kw_to_cmd_args(**kw)])
|
||||
|
||||
runner = dbtRunner()
|
||||
result = runner.invoke(args)
|
||||
if result.success:
|
||||
return result.result
|
||||
raise DbtInternalError(f"failed to run dbt {posargs[0]}") from result.exception
|
||||
|
||||
|
||||
def _dbt_show(
|
||||
inline: Optional[str] = None,
|
||||
limit: int = -1,
|
||||
profile: Optional[str] = None,
|
||||
target: Optional[str] = None,
|
||||
) -> "Table":
|
||||
from dbt.contracts.results import RunExecutionResult
|
||||
|
||||
result = _dbt_invoke(
|
||||
"show",
|
||||
inline=inline,
|
||||
limit=limit,
|
||||
profile=profile,
|
||||
target=target,
|
||||
)
|
||||
assert isinstance(result, RunExecutionResult)
|
||||
|
||||
run_results: Sequence["RunResult"] = result.results
|
||||
run_result, *_ = run_results
|
||||
assert run_result.agate_table is not None
|
||||
return run_result.agate_table
|
||||
|
||||
|
||||
@check_dbt("model")
|
||||
def get_model(
|
||||
name: str,
|
||||
package: Optional[str] = None,
|
||||
version: Optional[int] = None,
|
||||
profile: Optional[str] = None,
|
||||
target: Optional[str] = None,
|
||||
) -> "Table":
|
||||
model = _ref(name, package, version=version)
|
||||
q = f"select * from {model}" # noqa: S608
|
||||
return _dbt_show(
|
||||
inline=q,
|
||||
profile=profile,
|
||||
target=target,
|
||||
)
|
||||
|
||||
|
||||
def _profiles_dir(project_dir: Optional[str] = None) -> str:
|
||||
from dbt.cli.resolvers import default_profiles_dir
|
||||
|
||||
if profiles_dir := os.getenv("DBT_PROFILES_DIR"):
|
||||
return profiles_dir
|
||||
if project_dir and os.path.isfile(os.path.join(project_dir, "profiles.yml")):
|
||||
return project_dir
|
||||
return os.fspath(default_profiles_dir())
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _global_dbt_flags(
|
||||
profiles_dir: str,
|
||||
project_dir: str,
|
||||
target: Optional[str] = None,
|
||||
):
|
||||
from argparse import Namespace
|
||||
|
||||
from dbt import flags
|
||||
|
||||
prev = flags.get_flags()
|
||||
try:
|
||||
args = Namespace(
|
||||
use_colors=True,
|
||||
project_dir=project_dir,
|
||||
profiles_dir=profiles_dir,
|
||||
target=target,
|
||||
)
|
||||
flags.set_from_args(args, None)
|
||||
yield flags.get_flags()
|
||||
finally:
|
||||
flags.set_flags(prev)
|
||||
|
||||
|
||||
def _get_profile_or(
|
||||
project_dir: Optional[str], profile: Optional[str], target: Optional[str]
|
||||
) -> "Profile":
|
||||
from dbt.config.profile import Profile
|
||||
from dbt.config.renderer import ProfileRenderer
|
||||
from dbt.config.runtime import load_profile
|
||||
|
||||
if project_dir and os.path.isfile(os.path.join(project_dir, "dbt_project.yml")):
|
||||
return load_profile(
|
||||
project_dir, {}, profile_name_override=profile, target_override=target
|
||||
)
|
||||
|
||||
if not profile:
|
||||
raise DvcException("No profile specified to query from.")
|
||||
|
||||
renderer = ProfileRenderer({})
|
||||
return Profile.render(renderer, profile, target_override=target)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _handle_profile_parsing_error():
|
||||
from dbt.exceptions import DbtRuntimeError
|
||||
|
||||
try:
|
||||
yield
|
||||
except DbtRuntimeError as e:
|
||||
cause = e.__cause__ is not None and e.__cause__.__context__
|
||||
if isinstance(cause, ModuleNotFoundError) and (
|
||||
adapter := cut_prefix(cause.name, "dbt.adapters.")
|
||||
):
|
||||
# DbtRuntimeError is very noisy, so send it to debug
|
||||
logger.debug("", exc_info=True)
|
||||
raise DvcException(f"dbt-{adapter} dependency is missing") from cause
|
||||
raise DvcException("failed to read connection profiles") from e
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _suppress_status(status: Optional["Status"]):
|
||||
if status:
|
||||
status.stop()
|
||||
|
||||
yield
|
||||
if status:
|
||||
status.start()
|
||||
|
||||
|
||||
@check_dbt("query")
|
||||
def execute_sql(
|
||||
sql: str,
|
||||
profiles_dir: str,
|
||||
project_dir: Optional[str],
|
||||
profile: Optional[str],
|
||||
target: Optional[str] = None,
|
||||
status: Optional["Status"] = None,
|
||||
) -> "Table":
|
||||
from dbt.adapters import factory as adapters_factory
|
||||
from dbt.adapters.sql import SQLAdapter
|
||||
|
||||
flags = _global_dbt_flags(profiles_dir, os.getcwd(), target=target)
|
||||
update_status = status.update if status else identity
|
||||
|
||||
with flags, adapters_factory.adapter_management():
|
||||
# likely invalid connection profile or no adapter
|
||||
with _handle_profile_parsing_error(), _suppress_status(status):
|
||||
profile_obj = _get_profile_or(project_dir, profile, target)
|
||||
|
||||
adapters_factory.register_adapter(profile_obj) # type: ignore[arg-type]
|
||||
adapter = adapters_factory.get_adapter(profile_obj) # type: ignore[arg-type]
|
||||
|
||||
assert isinstance(adapter, SQLAdapter)
|
||||
with adapter.connection_named("debug"):
|
||||
update_status("Testing Connection")
|
||||
try:
|
||||
adapter.debug_query()
|
||||
except Exception as exc:
|
||||
if status:
|
||||
status.stop()
|
||||
logger.exception(
|
||||
"dvc was unable to connect to the specified database. "
|
||||
"Please check your database credentials and try again.",
|
||||
exc_info=False,
|
||||
)
|
||||
raise DvcException("The database returned the following error") from exc
|
||||
|
||||
with adapter.connection_named("execute"):
|
||||
update_status("Executing query")
|
||||
exec_resp = adapter.execute(sql, fetch=True)
|
||||
_, table = exec_resp
|
||||
return table
|
||||
+1
-1
@@ -9,7 +9,7 @@ def get_plot_id(config_plot_id: str, config_file_path: str = ""):
|
||||
|
||||
|
||||
def group_definitions_by_id(
|
||||
definitions: Dict[str, Dict]
|
||||
definitions: Dict[str, Dict],
|
||||
) -> Dict[str, Tuple[str, Dict]]:
|
||||
"""
|
||||
Format ID and extracts plot_definition for each plot.
|
||||
|
||||
@@ -21,7 +21,7 @@ PARSERS.update(
|
||||
|
||||
|
||||
def load_path(fs_path, fs, **kwargs):
|
||||
suffix = fs.path.suffix(fs_path).lower()
|
||||
suffix = fs.suffix(fs_path).lower()
|
||||
loader = LOADERS[suffix]
|
||||
return loader(fs_path, fs=fs, **kwargs)
|
||||
|
||||
|
||||
+21
-20
@@ -29,22 +29,23 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
dependencies = [
|
||||
"colorama>=0.3.9",
|
||||
"configobj>=5.0.6",
|
||||
"distro>=1.3",
|
||||
"dpath<3,>=2.1.0",
|
||||
"dvc-data>=2.22.1,<2.23.0",
|
||||
"dvc-data>=3.5,<3.6",
|
||||
"dvc-http>=2.29.0",
|
||||
"dvc-render>=0.3.1,<1",
|
||||
"dvc-studio-client>=0.13.0,<1",
|
||||
"dvc-render>=1.0.0,<2",
|
||||
"dvc-studio-client>=0.17.1,<1",
|
||||
"dvc-task>=0.3.0,<1",
|
||||
"flatten_dict<1,>=0.4.1",
|
||||
# https://github.com/iterative/dvc/issues/9654
|
||||
"flufl.lock>=5,<8",
|
||||
"funcy>=1.14",
|
||||
"grandalf<1,>=0.7",
|
||||
"gto>=1.4.0,<2",
|
||||
"gto>=1.6.0,<2",
|
||||
"hydra-core>=1.1",
|
||||
"iterative-telemetry>=0.0.7",
|
||||
"networkx>=2.5",
|
||||
@@ -58,7 +59,7 @@ dependencies = [
|
||||
"requests>=2.22",
|
||||
"rich>=12",
|
||||
"ruamel.yaml>=0.17.11",
|
||||
"scmrepo>=1.4.1,<2",
|
||||
"scmrepo>=2.0.2,<3",
|
||||
"shortuuid>=0.5",
|
||||
"shtab<2,>=1.3.4",
|
||||
"tabulate>=0.8.7",
|
||||
@@ -70,13 +71,14 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
all = ["dvc[azure,gdrive,gs,hdfs,oss,s3,ssh,webdav,webhdfs]"]
|
||||
azure = ["dvc-azure>=2.23.0"]
|
||||
azure = ["dvc-azure>=3,<4"]
|
||||
dev = ["dvc[azure,gdrive,gs,hdfs,lint,oss,s3,ssh,tests,webdav,webhdfs]"]
|
||||
gdrive = ["dvc-gdrive==2.20"]
|
||||
gs = ["dvc-gs==2.22.1"]
|
||||
hdfs = ["dvc-hdfs==2.19"]
|
||||
gdrive = ["dvc-gdrive>=3,<4"]
|
||||
gs = ["dvc-gs>=3,<4"]
|
||||
hdfs = ["dvc-hdfs>=3,<4"]
|
||||
lint = [
|
||||
"mypy==1.7.0",
|
||||
"mypy==1.8.0",
|
||||
"pandas-stubs",
|
||||
"types-colorama",
|
||||
"types-psutil",
|
||||
"types-pyinstaller",
|
||||
@@ -85,10 +87,10 @@ lint = [
|
||||
"types-toml",
|
||||
"types-tqdm",
|
||||
]
|
||||
oss = ["dvc-oss>=2.20.0"]
|
||||
s3 = ["dvc-s3==2.23.0"]
|
||||
ssh = ["dvc-ssh>=2.22.1,<3"]
|
||||
ssh_gssapi = ["dvc-ssh[gssapi]>=2.22.1,<3"]
|
||||
oss = ["dvc-oss>=3,<4"]
|
||||
s3 = ["dvc-s3>=3,<4"]
|
||||
ssh = ["dvc-ssh>=3,<4"]
|
||||
ssh_gssapi = ["dvc-ssh[gssapi]>=3,<4"]
|
||||
testing = [
|
||||
"pytest-benchmark[histogram]",
|
||||
"pytest-test-utils",
|
||||
@@ -111,10 +113,12 @@ tests = [
|
||||
'pywin32>=225; sys_platform == "win32"', # optional test dependency
|
||||
'tzdata; sys_platform == "win32"', # for testing with celery
|
||||
"dbt-sqlite", # optional dependency for testing `import-db`
|
||||
"sqlalchemy>=1,<3", # optional dependency for `import-db`
|
||||
"pandas>=1",
|
||||
]
|
||||
webdav = ["dvc-webdav==2.19.1"]
|
||||
webhdfs = ["dvc-webhdfs==2.19"]
|
||||
webhdfs_kerberos = ["dvc-webhdfs[kerberos]==2.19"]
|
||||
webdav = ["dvc-webdav>=3,<4"]
|
||||
webhdfs = ["dvc-webhdfs>=3.1,<4"]
|
||||
webhdfs_kerberos = ["dvc-webhdfs[kerberos]>=3.1,<4"]
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://dvc.org/doc"
|
||||
@@ -127,9 +131,6 @@ dvc = "dvc.cli:main"
|
||||
[project.entry-points."fsspec.specs"]
|
||||
dvc = "dvc.api:DVCFileSystem"
|
||||
|
||||
[project.entry-points."pytest11"]
|
||||
dvc-testing = "dvc.testing.plugin"
|
||||
|
||||
[project.entry-points."pyinstaller40"]
|
||||
hook-dirs = "dvc.__pyinstaller:get_hook_dirs"
|
||||
tests = "dvc.__pyinstaller:get_PyInstaller_tests"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from funcy import first
|
||||
|
||||
@@ -360,3 +362,17 @@ def test_get(tmp_dir, scm, dvc, exp_stage, erepo_dir, use_ref):
|
||||
rev=exp_ref.name if use_ref else exp_rev,
|
||||
)
|
||||
assert (erepo_dir / "params.yaml").read_text().strip() == "foo: 2"
|
||||
|
||||
|
||||
def test_push_pull_invalid_workspace(
|
||||
tmp_dir, scm, dvc, git_upstream, exp_stage, local_remote, caplog
|
||||
):
|
||||
dvc.experiments.run()
|
||||
|
||||
with open("dvc.yaml", mode="a") as f:
|
||||
f.write("\ninvalid")
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="dvc"):
|
||||
dvc.experiments.push(git_upstream.remote, push_cache=True)
|
||||
dvc.experiments.pull(git_upstream.remote, pull_cache=True)
|
||||
assert "failed to collect" not in caplog.text
|
||||
|
||||
@@ -1331,10 +1331,10 @@ def test_repro_keep_going(mocker, tmp_dir, dvc, copy_script):
|
||||
stage1, upstream=[bar_stage], force=False, interactive=False
|
||||
)
|
||||
foo_call = mocker.call(foo_stage, upstream=[], force=False, interactive=False)
|
||||
assert spy.call_args_list in (
|
||||
[bar_call, stage1_call, foo_call],
|
||||
[foo_call, bar_call, stage1_call],
|
||||
)
|
||||
assert len(spy.call_args_list) == 3
|
||||
assert foo_call in spy.call_args_list
|
||||
assert bar_call in spy.call_args_list
|
||||
assert stage1_call in spy.call_args_list
|
||||
|
||||
|
||||
def test_repro_ignore_errors(mocker, tmp_dir, dvc, copy_script):
|
||||
@@ -1362,10 +1362,11 @@ def test_repro_ignore_errors(mocker, tmp_dir, dvc, copy_script):
|
||||
force=False,
|
||||
interactive=False,
|
||||
)
|
||||
assert spy.call_args_list in (
|
||||
[bar_call, stage1_call, foo_call, stage2_call],
|
||||
[foo_call, bar_call, stage1_call, stage2_call],
|
||||
)
|
||||
assert len(spy.call_args_list) == 4
|
||||
assert foo_call in spy.call_args_list
|
||||
assert bar_call in spy.call_args_list
|
||||
assert stage1_call in spy.call_args_list
|
||||
assert stage2_call in spy.call_args_list
|
||||
|
||||
|
||||
@pytest.mark.parametrize("persist", [True, False])
|
||||
|
||||
@@ -96,7 +96,7 @@ def test_config_unset(dvc):
|
||||
cmd = args.func(args)
|
||||
assert isinstance(cmd, CmdConfig)
|
||||
assert args.unset
|
||||
assert args.name == (False, "section", "option")
|
||||
assert args.name == (None, "section", "option")
|
||||
assert args.value == value
|
||||
|
||||
|
||||
|
||||
@@ -481,12 +481,13 @@ def test_push_pull_fetch_pipeline_stages(tmp_dir, dvc, run_copy, local_remote):
|
||||
|
||||
|
||||
def test_pull_partial(tmp_dir, dvc, local_remote):
|
||||
tmp_dir.dvc_gen({"foo": {"bar": {"baz": "baz"}, "spam": "spam"}})
|
||||
other_files = {f"spam{i}": f"spam{i}" for i in range(10)}
|
||||
tmp_dir.dvc_gen({"foo": {"bar": {"baz": "baz"}, **other_files}})
|
||||
dvc.push()
|
||||
clean(["foo"], dvc)
|
||||
|
||||
stats = dvc.pull(os.path.join("foo", "bar"))
|
||||
assert stats["fetched"] == 3
|
||||
assert stats["fetched"] == 2
|
||||
assert (tmp_dir / "foo").read_text() == {"bar": {"baz": "baz"}}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import os
|
||||
|
||||
|
||||
def test_du(tmp_dir, dvc):
|
||||
tmp_dir.gen(
|
||||
{
|
||||
"file": b"file",
|
||||
"dvcfile": b"dvcfile",
|
||||
"dir": {
|
||||
"dirfile": b"dirfile",
|
||||
"subdir": {
|
||||
"subdirfile": b"subdirfile",
|
||||
},
|
||||
"dvcsubdir": {
|
||||
"dvcsubdirfile": b"dvcsubdirfile",
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
dvc.add("dvcfile")
|
||||
dvc.add(os.path.join("dir", "dvcsubdir"))
|
||||
|
||||
assert dvc.du(".", "file") == [("file", 4)]
|
||||
assert dvc.du(".", "dvcfile") == [("dvcfile", 7)]
|
||||
assert set(dvc.du(".", "dir/subdir")) == {
|
||||
("dir/subdir/subdirfile", 10),
|
||||
("dir/subdir", 10),
|
||||
}
|
||||
assert dvc.du(".", "dir/subdir", summarize=True) == [("dir/subdir", 10)]
|
||||
assert set(dvc.du(".", "dir/dvcsubdir")) == {
|
||||
("dir/dvcsubdir/dvcsubdirfile", 13),
|
||||
("dir/dvcsubdir", 13),
|
||||
}
|
||||
assert dvc.du(".", "dir/dvcsubdir", summarize=True) == [("dir/dvcsubdir", 13)]
|
||||
assert set(dvc.du(".", "dir")) == {
|
||||
("dir/dvcsubdir", 13),
|
||||
("dir/subdir", 10),
|
||||
("dir/dirfile", 7),
|
||||
("dir", 30),
|
||||
}
|
||||
assert dvc.du(".", "dir", summarize=True) == [("dir", 30)]
|
||||
assert set(dvc.du(".", "/")) == {
|
||||
("/dvcfile", 7),
|
||||
("/dir", 30),
|
||||
("/file", 4),
|
||||
("/", 41),
|
||||
}
|
||||
assert dvc.du(".", "/", summarize=True) == [("/", 41)]
|
||||
@@ -13,15 +13,15 @@ def test_cleanfs_subrepo(tmp_dir, dvc, scm, monkeypatch):
|
||||
|
||||
path = subrepo_dir.fs_path
|
||||
|
||||
assert dvc.fs.exists(dvc.fs.path.join(path, "foo"))
|
||||
assert dvc.fs.isfile(dvc.fs.path.join(path, "foo"))
|
||||
assert dvc.fs.exists(dvc.fs.path.join(path, "dir"))
|
||||
assert dvc.fs.isdir(dvc.fs.path.join(path, "dir"))
|
||||
assert dvc.fs.exists(dvc.fs.join(path, "foo"))
|
||||
assert dvc.fs.isfile(dvc.fs.join(path, "foo"))
|
||||
assert dvc.fs.exists(dvc.fs.join(path, "dir"))
|
||||
assert dvc.fs.isdir(dvc.fs.join(path, "dir"))
|
||||
|
||||
assert subrepo.fs.exists(subrepo.fs.path.join(path, "foo"))
|
||||
assert subrepo.fs.isfile(subrepo.fs.path.join(path, "foo"))
|
||||
assert subrepo.fs.exists(subrepo.fs.path.join(path, "dir"))
|
||||
assert subrepo.fs.isdir(subrepo.fs.path.join(path, "dir"))
|
||||
assert subrepo.fs.exists(subrepo.fs.join(path, "foo"))
|
||||
assert subrepo.fs.isfile(subrepo.fs.join(path, "foo"))
|
||||
assert subrepo.fs.exists(subrepo.fs.join(path, "dir"))
|
||||
assert subrepo.fs.isdir(subrepo.fs.join(path, "dir"))
|
||||
|
||||
|
||||
def test_walk_dont_ignore_subrepos(tmp_dir, scm, dvc):
|
||||
|
||||
+13
-2
@@ -7,7 +7,7 @@ import textwrap
|
||||
import pytest
|
||||
|
||||
from dvc.cli import main
|
||||
from dvc.exceptions import CollectCacheError, InvalidArgumentError
|
||||
from dvc.exceptions import CollectCacheError, InvalidArgumentError, RevCollectionError
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc.utils.fs import remove
|
||||
from dvc_data.hashfile.db.local import LocalHashFileDB
|
||||
@@ -111,8 +111,9 @@ def test_gc_no_dir_cache(tmp_dir, dvc):
|
||||
|
||||
remove(dir_stage.outs[0].cache_path)
|
||||
|
||||
with pytest.raises(CollectCacheError):
|
||||
with pytest.raises(RevCollectionError) as exc:
|
||||
dvc.gc(workspace=True)
|
||||
assert type(exc.value.__cause__) == CollectCacheError
|
||||
|
||||
assert _count_files(dvc.cache.local.path) == 4
|
||||
dvc.gc(force=True, workspace=True)
|
||||
@@ -439,3 +440,13 @@ def test_gc_logging(caplog, dvc, good_and_bad_cache):
|
||||
assert "Removed 3 objects from repo cache." in caplog.text
|
||||
assert "No unused 'local' cache to remove." in caplog.text
|
||||
assert "No unused 'legacy' cache to remove." in caplog.text
|
||||
|
||||
|
||||
def test_gc_skip_failed(tmp_dir, dvc):
|
||||
with open("dvc.yaml", mode="w") as f:
|
||||
f.write("\ninvalid")
|
||||
|
||||
with pytest.raises(RevCollectionError):
|
||||
dvc.gc(force=True, workspace=True)
|
||||
|
||||
dvc.gc(force=True, workspace=True, skip_failed=True)
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
"""import_db tests without involving any databases."""
|
||||
|
||||
import pytest
|
||||
from agate import Table
|
||||
|
||||
from dvc.database.serializer import AgateSerializer
|
||||
|
||||
data1 = Table([(i, i) for i in range(1, 10)], ["id", "value"])
|
||||
data2 = Table([(i, i) for i in range(10, 20)], ["id", "value"])
|
||||
|
||||
serializers = AgateSerializer(data1), AgateSerializer(data2)
|
||||
|
||||
def test_sql(mocker, tmp_dir, dvc):
|
||||
mocker.patch("dvc.utils.db.execute_sql", side_effect=[data1, data2])
|
||||
|
||||
@pytest.fixture
|
||||
def client(mocker):
|
||||
m = mocker.patch("dvc.database.get_client")
|
||||
client = mocker.MagicMock()
|
||||
client.query.return_value.__enter__.side_effect = serializers
|
||||
m.return_value.__enter__.return_value = client
|
||||
return m
|
||||
|
||||
|
||||
def test_sql(client, tmp_dir, dvc):
|
||||
stage = dvc.imp_db(
|
||||
sql="select * from model", profile="profile", output_format="json"
|
||||
)
|
||||
@@ -44,8 +56,46 @@ def test_sql(mocker, tmp_dir, dvc):
|
||||
}
|
||||
|
||||
|
||||
def test_sql_conn_string(client, tmp_dir, dvc):
|
||||
with dvc.config.edit(level="local") as conf:
|
||||
conf["db"] = {"conn": {"url": "conn"}}
|
||||
|
||||
stage = dvc.imp_db(
|
||||
sql="select * from model", connection="conn", output_format="json"
|
||||
)
|
||||
db = {"file_format": "json", "connection": "conn", "query": "select * from model"}
|
||||
assert (tmp_dir / stage.relpath).parse() == {
|
||||
"md5": "f0dcca8e8411907adb25aa547d7c432c",
|
||||
"frozen": True,
|
||||
"deps": [{"db": db}],
|
||||
"outs": [
|
||||
{
|
||||
"hash": "md5",
|
||||
"md5": "56226d443067b195c90b427db557e1f2",
|
||||
"path": "results.json",
|
||||
"size": 243,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
dvc.update(stage.addressing)
|
||||
assert (tmp_dir / stage.relpath).parse() == {
|
||||
"md5": "8c0441b206d8ee7c7b9e3d54871c3cbb",
|
||||
"frozen": True,
|
||||
"deps": [{"db": db}],
|
||||
"outs": [
|
||||
{
|
||||
"hash": "md5",
|
||||
"md5": "08d806ee7e1f309c4561750805f02276",
|
||||
"path": "results.json",
|
||||
"size": 290,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_model(mocker, tmp_dir, dvc):
|
||||
mocker.patch("dvc.utils.db.get_model", side_effect=[data1, data2])
|
||||
mocker.patch("dvc.database.get_model", side_effect=serializers)
|
||||
stage = dvc.imp_db(model="model", output_format="json")
|
||||
|
||||
assert (tmp_dir / stage.relpath).parse() == {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user