Compare commits

...
65 Commits
Author SHA1 Message Date
Ruslan Kuprieiev e18fd34b2c testing: bench: use --dataset instead of --size (#10134)
This is no longer just about size (e.g. we have mnist/openorca/used-cars),
so the --dataset makes more sense.
2023-12-04 14:43:37 +02:00
Ruslan Kuprieiev c0b64b0a10 testing: bench: add openorca (#10133)
Need to move the data into dvc at some point.
2023-12-04 12:43:50 +02:00
Ruslan Kuprieievandpre-commit-ci[bot] 3927e75ef1 testing: bench: add pytest_report_header (#10132)
* testing: bench: add pytest_report_header

Useful for sanity checks in logs.

* Update dvc/testing/benchmarks/plugin.py

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-04 11:23:12 +02:00
skshetry e5ae5ef24b add security-extend to codeql queries (#10131) 2023-12-04 13:29:29 +05:45
skshetry e187508f40 database: fix ui issues (#10129) 2023-12-04 13:27:05 +05:45
skshetry 8768b50aed experimental: support connection strings for databases (#10125)
* experimental: support connection strings for databases using connectorx

This adds an experimental support for connector strings using connectorx.

User has to provide config for the connection strings with:

Example:
```console
dvc config --local db_connection.pg postgres://user:pass@localhost:5432/database
```

And then, they can import using:
```console
dvc import-db --sql 'query' --conn pg
```

Check supported databases and how to configure in https://sfu-db.github.io/connector-x/databases.html.
This `db_connection` config is subject to change.

connectorx still has the same problem with dbt as before: no support for streaming,
but it'll be a bit more simpler to configure with the connection strings.

* add pandas to deps

* sqlalchemy

* dbt streaming sql

* fix test

* remove connectorx

* remove check in set

* use mode='wb' for serialization
2023-12-03 19:23:09 +05:45
skshetry 7c61463d2d tests: do not setup pytest plugin entrypoint (#10121)
We should not register a pytest plugin automaticallly, as we may
have fixtures that might override user's fixtures. Also, it might
raise import errors as in https://github.com/iterative/dvc/issues/10118.

We can still use this as a plugin ourselves by setting the following
in `conftest.py` file.
```python
pytest_plugins = ['dvc.testing.plugin']
```
2023-11-29 12:05:46 +05:45
Peter Rowlands b0d0315689 import/get: support pre-fetching LFS objects from Git-LFS repos 2023-11-29 14:39:54 +09:00
Peter Rowlands 7edcb6b1e5 deps: bump scmrepo minver to 1.5.0 2023-11-29 14:39:54 +09:00
Ruslan Kuprieiev b6d231f9cf testing: import virtualenv lazily
Fixes https://github.com/iterative/dvc/issues/10118
2023-11-28 22:20:34 +02:00
Ruslan Kuprieiev 322fcec5ca bench: test_sharing: use fetch instead of pull 2023-11-28 19:31:32 +02:00
Ruslan Kuprieiev 1e5b3603cc bench: test fetch instead of pull
`pull` is literally `fetch` + `checkout` and our main interest there is downloading, so let's just test fetch instead.
2023-11-28 19:31:32 +02:00
Ruslan Kuprieiev ffa630e8d5 push: handle missing/failed version_aware file
Previously we used recorded index which had stub entries
for missing/failed outputs, but now we actually collect
everything ourselves and those are missing.
2023-11-28 18:52:46 +02:00
Ruslan Kuprieiev 1e9b60dc48 deps: bump dvc-data to >=2.22.1 2023-11-27 16:17:42 +02:00
skshetry 83dd2b2a7b test: import-db: test for external model (#10112)
* test: import-db: test for external model

* close repo
2023-11-27 16:13:44 +05:45
Ivan Shcheklein d238350a2d fix(config): respect global config env variable 2023-11-23 20:51:52 +02:00
skshetry 06b9196371 pytest.ini_options: add --benchmark-skip to suppress pytest-benchmark warning with xdist (#10106)
We don't run benchmarks here, and benchmarking should always be a separate command than regular test run.
2023-11-23 19:55:23 +05:45
skshetry b4f42242c9 tmp_dir: fix object initialization for Python 3.12 (#10105) 2023-11-23 16:00:30 +05:45
skshetryandpre-commit-ci[bot] fbdb566079 remove pytest-virtualenv (#10103)
* remove pytest-virtualenv

It does not look like it's being actively maintained.

* force virtualenv

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-11-23 14:53:39 +05:45
skshetry 3d7bbb8492 tests: add functional test for db (#10101)
* tests: add functional test for db

* fix test
2023-11-22 16:15:07 +05:45
skshetry fd7c891217 implement import-db command (#10040)
* implement import-db command

* generate .dvc file

* update

* fix tests

* pin dbt-fal

* try to run on ci

* use os.chdir

* fix

* revert

* require dbt-core>=1.5

* add db config schema

* remove platformdirs constraints

* use logger.getChild

* improve error messaging

* rename cli flag --export-format to --output-format

* use feature flag for db config

* hide command from dvc commands list

* refactor dependency._get

* remove todo

* refactor dependency._get

* fix dbt installation check
2023-11-21 19:38:50 +05:45
Ruslan Kuprieiev 2b3b15f767 deps: bump dvc-data to 2.22.0
Related https://github.com/iterative/dvc/issues/10059
2023-11-17 20:01:57 +02:00
Ruslan Kuprieiev 8c9358957c push: fix cloud versioning 2023-11-15 16:28:17 +02:00
Ivan Shcheklein 86dd0785a0 add tests for the index param keys 2023-11-14 13:21:44 +02:00
Ivan Shcheklein a8aec51c35 add params collection to index 2023-11-14 13:21:44 +02:00
Ruslan Kuprieiev e9f2da4451 repo: drop_data_index: delete all first level nodes 2023-11-13 11:23:08 +02:00
dependabot[bot]andRuslan Kuprieiev 8d23a5e65a build(deps-dev): bump mypy from 1.6.1 to 1.7.0 (#10088)
* build(deps-dev): bump mypy from 1.6.1 to 1.7.0

Bumps [mypy](https://github.com/python/mypy) from 1.6.1 to 1.7.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.6.1...v1.7.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

* fix misc mypy errors

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2023-11-13 10:59:32 +02:00
skshetry b2fc194e2b fix name of the loggers used (#10085)
__name__ looks something like `dvc.commands.diff`, but if we are already instantiating
root logger with `dvc`, it adds up to create something like `dvc.dvc.commands.diff`.

We should be using a root logger here instead.
2023-11-10 15:54:08 +05:45
dependabot[bot]andSaugat Pachhai 0def298b8c build(deps-dev): bump mypy from 1.5.1 to 1.6.1 (#10035)
* build(deps-dev): bump mypy from 1.5.1 to 1.6.1

Bumps [mypy](https://github.com/python/mypy) from 1.5.1 to 1.6.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.5.1...v1.6.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

* pre-commit autoupdate

* mypy fixes

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2023-11-10 15:40:57 +05:45
skshetry d678773b53 remove more pylint suppression comments (#10084) 2023-11-10 15:19:52 +05:45
skshetry 519cce3fe4 logger: type-safe logger.trace (#10083)
* use logger.getChild from `dvc.log` instead of `logging.getLogger`

I got tired adding `# type: ignore[attr-defined]` while using `logger.trace`.
So, now you need to import logger from `from dvc.log import logger` and
create a child logger using `logger.getChild(__name__)`. `logger.getChild`
will return a logger type-hinted as a logger having a `trace` method.

So, from now on, instead of:
```python
import logging

logger = logging.getogger(__name__)
```

you'll have to do following:
```python
from dvc.log import logger

logger = logger.getChild(__name__)
```

* remove all dvc imports from dvc.logger

* use Tqdm back
2023-11-10 12:55:49 +05:45
skshetry cbc111b519 remove flake8/bandit (#10082)
ruff already runs most of the flake8/bandit checks
2023-11-09 21:26:37 +05:45
skshetry 4ee8f554a5 use ruff formatter (#10081)
Replaces black with ruff-formatter, and isort with ruff.
2023-11-09 20:58:58 +05:45
skshetry 06565e18b4 remove pylint suppression comments (#10080) 2023-11-09 20:31:50 +05:45
skshetry 2085bc8053 drop pylint from linter checks (#10079)
drop pylint
2023-11-09 19:25:28 +05:45
Peter Rowlands 14cbf6470d artifacts get: fix issue where global/system config could be ignored 2023-11-09 16:19:02 +09:00
Ivan Shcheklein 36fa116e75 chore(deps): bump scmerepo to 1.4.1+
After this fix https://github.com/iterative/scmrepo/issues/280, to make sure that we don't install by mistake an older version in Studio.
2023-11-06 15:23:18 +09:00
Ivan Shcheklein 97f3d07480 fix(readme): link to VS Code extension (#10063) 2023-11-02 20:37:32 -07:00
dberenbaum 4a2c4f76e5 fix for DVCFileSystem().ls() 2023-10-31 23:38:21 +02:00
dberenbaum eb8999a86b gc: force no prompts during dry run 2023-10-31 10:24:01 -05:00
dberenbaum db870335e3 gc: don't prompt when for dry run 2023-10-31 10:24:01 -05:00
Piotr Bielak 23d2ed3d59 Unroll dict-comprehension in matrix test 2023-10-31 08:05:38 -05:00
skshetry 46db3ac419 Update dvc/parsing/__init__.py 2023-10-31 08:05:38 -05:00
Piotr Bielak fcc69ab6eb parsing: Add key to matrix stages
This commit adds the `key` property to the templaing context of `matrix`
stages. Similarly to `foreach` stages, the `key` is available using
`${key}` substitution.

Fixes #10043
2023-10-31 08:05:38 -05:00
Ruslan Kuprieiev d60557dd08 deps: bump dvc-data to >=2.20.0 2023-10-31 04:51:30 +02:00
dberenbaum fc5147767f api: make exp_show return float instead of None for zeroes 2023-10-30 13:54:31 -05:00
skshetry 89b4beb7a4 daemon: ipc to log pid from the child (#10058)
* daemon: ipc to log pid from the child

* fix tests to use pid

* also test for updater/analytics keyword
2023-10-30 20:47:52 +05:45
skshetry 48f1f52c1b refactor: dvc.daemon (#10045)
* minor refactor to dvc.daemon

* fix tests

* run daemon only inside a subprocess
2023-10-30 11:09:55 +05:45
Ruslan Kuprieiev 3fee81f1be push: use index push 2023-10-26 20:58:45 +03:00
Peter Rowlands 0bfce0b643 exp apply: use force dvc-checkout by default 2023-10-26 14:48:41 +09:00
Peter Rowlands d9b2e67a0b Revert "Enable forcing exp apply (unsaved files in the workspace)"
This reverts commit 3661b8998b.
2023-10-26 14:48:41 +09:00
Ruslan Kuprieiev 4a0d56a793 cli: DictAction: set default metavar (#10047) 2023-10-20 16:46:14 +03:00
skshetry 825c823a37 avoid failing on standard streams that are None. (#10041)
See https://docs.python.org/3/library/sys.html#sys.__stdin__.

> [!Note]
> Under some conditions stdin, stdout and stderr as well as the original values __stdin__, __stdout__ and __stderr__ can be None. It is usually the case for Windows GUI apps that aren’t connected to a console and Python apps started with pythonw.

This is a best-effort solution. External libraries (eg: `tqdm`) and future code that is unaware of this stuff may still fail.
As a console application, `stdout`/`stderr` being `None` is not really something that we should support in general.
Regarding the failure of the daemon, it has been fixed so that it'll always have a stream attached (if not, it falls back to
/dev/null). Today, this is a problem in daemon only if the main process is run with no streams attached, which is not
something that we support or go on length to support.

It is tricky to write tests for this case, and I could not come up with one. Also decided not to test daemon as it should no longer be a problem.
2023-10-19 22:16:26 +05:45
skshetry b23ba58cd6 tests: daemon: log body of the request (#10039)
log body of the request
2023-10-19 17:16:34 +05:45
skshetry 8fb67b71a5 expose test webserver as an executable for easier testing (#10037) 2023-10-19 16:28:47 +05:45
skshetry 5693775459 refactor: use int as the factory (#10036)
makes it thread-safe if we need in the future. Also looks a bit clean.
2023-10-19 15:31:33 +05:45
skshetry 17f5508f49 daemon: add logging support, tests and generalize popen based runs for future (#10033)
* use popen to run updater/analytics in detached mode

subprocess does not provide a way to avoid a zombie process. Not to confuse with orphaned processes, which will be `wait`-ed by `pid 1`.

It might not work for long-running processes, but I don't think zombie processes are going to be a problem for us, as the parent process immediately exits after spawning a child process.

Even in case of `queue` based experiment runner, parent will exit before than the child process. So I don't think it's a requirement here. But it does feel wrong to leak the processes, so I have to sleep on that.

---

This PR:

1. makes `daemonized` process inherit the logging verbosity, so if the parent process used `-vv`, the daemonized process will run the same way.
2. Add `DVC_DAEMON_LOGFILE` envvar, that the daemon will log to the file. By default, the verbosity is the same as running parent processes. Without `DVC_DAEMON_LOGFILE`, the output will be redirected to `/dev/null`.
3. Adds debug/trace for updater/analytics.

I hope by doing this, we can make our life easier on debugging and make daemon more transparent/observable.

* add tests

* fixes

* use kwargs

* pass all os.environ with some modified
2023-10-19 15:14:41 +05:45
skshetry c39ce6157d redirect fds rather than closing (#10028) 2023-10-18 22:29:45 +05:45
skshetry d59231febf log exit code and pid on verbose-verbose logging (#10031)
log exit code and pid on verbose logging

Is useful for debugging on daemon.
2023-10-18 17:11:07 +05:45
Ruslan Kuprieiev b35b9420a1 config: azure: expose timeouts (#10027) 2023-10-17 19:10:08 +03:00
Ruslan Kuprieiev 46c65fdba2 worktree: don't modify index entries in place (#10024) 2023-10-17 16:30:19 +03:00
Matt Seddon 5f8afb911e Remove VegaConverter TODO and update misleading comment 2023-10-16 20:42:48 -04:00
Dave Berenbaum ef226a96cd analytics: fix mac not sending reports (#10026) 2023-10-16 23:28:29 +03:00
Heinz-Alexander Fuetterer 8ea9d3edef chore: fix typos (#10022) 2023-10-14 12:54:57 +05:45
dependabot[bot] cafcefa758 build(deps-dev): bump pylint from 2.17.6 to 3.0.1
Bumps [pylint](https://github.com/pylint-dev/pylint) from 2.17.6 to 3.0.1.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v2.17.6...v3.0.1)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-10-13 23:50:16 +03:00
256 changed files with 2945 additions and 1276 deletions
-20
View File
@@ -1,20 +0,0 @@
[flake8]
max_line_length = 88
extend-ignore=
# Whitespace before ':'
E203
# Line break occurred before a binary operator
W503
# unindexed parameters in the str.format, see:
# https://pypi.org/project/flake8-string-format/
P1
max-complexity = 10
show_source = true
count = true
min_python_version = 3.8.0
per-file-ignores =
dvc/testing/benchmarks/conftest.py:F401
dvc/testing/conftest.py:F401
dvc/testing/plugin.py:F401
tests/conftest.py:F401
tests/func/experiments/executor/test_ssh.py:F401
+1
View File
@@ -34,6 +34,7 @@ jobs:
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: security-extended
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+5 -35
View File
@@ -2,11 +2,11 @@ default_language_version:
python: python3
ci:
skip: [mypy, pylint, dvc-pre-commit]
skip: [mypy, dvc-pre-commit]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
@@ -17,44 +17,21 @@ repos:
args: ['--assume-in-merge']
- id: check-toml
- id: check-yaml
- id: debug-statements
exclude: "dvc/_debug.py"
- id: end-of-file-fixer
- id: mixed-line-ending
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 23.9.1
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.0.291'
rev: 'v0.1.5'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/codespell-project/codespell
rev: v2.2.5
rev: v2.2.6
hooks:
- id: codespell
additional_dependencies: ["tomli"]
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
additional_dependencies:
- flake8-string-format
- flake8-typing-imports
- repo: https://github.com/PyCQA/bandit
rev: '1.7.5'
hooks:
- id: bandit
args: ["-c", "pyproject.toml", "--quiet", "--format=custom"]
files: ^dvc/
additional_dependencies: [".[toml]"]
- repo: local
hooks:
- id: mypy
@@ -64,13 +41,6 @@ repos:
language: system
types: [python]
require_serial: true
- id: pylint
name: pylint
entry: pylint
files: ^dvc/
language: system
types: [python]
require_serial: true
- hooks:
- args:
- git-hook
+2 -2
View File
@@ -90,8 +90,8 @@ Their results can be filtered and compared based on hyperparameters and metrics,
.. _`VS Code Extension`:
Visual Studio Code Extension
============================
VS Code Extension
=================
|VS Code|
+1 -1
View File
@@ -1,5 +1,5 @@
# ruff: noqa: N999
# pylint: disable=import-error
from PyInstaller.utils.hooks import collect_submodules, is_module_or_submodule
# Celery dynamically imports most celery internals at runtime
+1 -1
View File
@@ -1,5 +1,5 @@
# ruff: noqa: N999
from PyInstaller.utils.hooks import copy_metadata # pylint:disable=import-error
from PyInstaller.utils.hooks import copy_metadata
# needed for `dvc doctor` to show dep versions
datas = copy_metadata("adlfs", recursive=True)
+1 -1
View File
@@ -1,4 +1,4 @@
# ruff: noqa: N999
from PyInstaller.utils.hooks import copy_metadata # pylint:disable=import-error
from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata("flatten-dict")
+1 -1
View File
@@ -1,5 +1,5 @@
# ruff: noqa: N999
# pylint: disable=import-error
from PyInstaller.utils.hooks import collect_submodules
hiddenimports = collect_submodules("dvc_task")
+1 -1
View File
@@ -1,4 +1,4 @@
# ruff: noqa: N999
from PyInstaller.utils.hooks import copy_metadata # pylint:disable=import-error
from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata("pydrive2")
+9 -8
View File
@@ -14,7 +14,7 @@ def viztracer_profile(
log_async: bool = False,
):
try:
import viztracer # pylint: disable=import-error
import viztracer
except ImportError:
print("Failed to run profiler, viztracer is not installed") # noqa: T201
yield
@@ -36,7 +36,7 @@ def yappi_profile(
separate_threads: Optional[bool] = False,
):
try:
import yappi # pylint: disable=import-error
import yappi
except ImportError:
print("Failed to run profiler, yappi is not installed") # noqa: T201
yield
@@ -65,9 +65,9 @@ def yappi_profile(
else:
if ctx_id is not None:
print(f"\nThread {ctx_id}") # noqa: T201
st.print_all() # pylint:disable=no-member
st.print_all()
if ctx_id is None:
threads.print_all() # pylint:disable=no-member
threads.print_all()
yappi.clear_stats()
@@ -76,7 +76,7 @@ def yappi_profile(
def instrument(html_output=False):
"""Run a statistical profiler"""
try:
from pyinstrument import Profiler # pylint: disable=import-error
from pyinstrument import Profiler
except ImportError:
print("Failed to run profiler, pyinstrument is not installed") # noqa: T201
yield
@@ -115,9 +115,9 @@ def profile(dump_path: Optional[str] = None):
def debug():
try:
yield
except Exception: # pylint: disable=broad-except
except Exception:
try:
import ipdb as pdb # noqa: T100, pylint: disable=import-error
import ipdb as pdb # noqa: T100
except ImportError:
import pdb # type: ignore[no-redef] # noqa: T100
pdb.post_mortem()
@@ -145,7 +145,8 @@ def show_stack():
signal.signal(signal.SIGQUIT, _sigshow)
try:
signal.signal(signal.SIGINFO, _sigshow) # only available on macOS
# only available on macOS
signal.signal(signal.SIGINFO, _sigshow) # type: ignore[attr-defined]
except AttributeError:
pass
yield
+14 -6
View File
@@ -1,10 +1,11 @@
import json
import logging
import os
from .env import DVC_NO_ANALYTICS
from dvc.log import logger
logger = logging.getLogger(__name__)
from .env import DVC_ANALYTICS_ENDPOINT, DVC_NO_ANALYTICS
logger = logger.getChild(__name__)
def collect_and_send_report(args=None, return_code=None):
@@ -34,6 +35,8 @@ def collect_and_send_report(args=None, return_code=None):
with tempfile.NamedTemporaryFile(delete=False, mode="w") as fobj:
json.dump(report, fobj)
logger.trace("Saving analytics report to %s", fobj.name)
daemon(["analytics", fobj.name])
@@ -65,7 +68,7 @@ def send(path):
"""
import requests
url = "https://analytics.dvc.org"
url = os.environ.get(DVC_ANALYTICS_ENDPOINT, "https://analytics.dvc.org")
headers = {"content-type": "application/json"}
with open(path, encoding="utf-8") as fobj:
@@ -73,11 +76,16 @@ def send(path):
report.update(_runtime_info())
logger.debug("uploading report to %s", url)
logger.trace("Sending %s to %s", report, url)
try:
requests.post(url, json=report, headers=headers, timeout=5)
except requests.exceptions.RequestException:
logger.debug("failed to send analytics report", exc_info=True)
except requests.exceptions.RequestException as e:
logger.trace("", exc_info=True)
logger.debug("failed to send analytics report %s", str(e))
logger.trace("removing report %s", path)
os.remove(path)
+1 -1
View File
@@ -46,7 +46,7 @@ ANNOTATION_SCHEMA = {
Annotation.PARAM_LABELS: [str],
Annotation.PARAM_META: object,
}
ARTIFACT_SCHEMA = {
ARTIFACT_SCHEMA: Dict[Any, Any] = {
Required(Artifact.PARAM_PATH): str,
**ANNOTATION_SCHEMA, # type: ignore[arg-type]
}
+1 -2
View File
@@ -1,8 +1,7 @@
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
from .artifacts import artifacts_show
from .data import open # pylint: disable=redefined-builtin
from .data import get_url, read
from .data import get_url, open, read
from .experiments import exp_save, exp_show
from .scm import all_branches, all_commits, all_tags
from .show import metrics_show, params_show
+2 -2
View File
@@ -55,7 +55,7 @@ def get_url(path, repo=None, rev=None, remote=None):
class _OpenContextManager(GCM):
def __init__(self, func, args, kwds): # pylint: disable=super-init-not-called
def __init__(self, func, args, kwds):
self.gen = func(*args, **kwds)
self.func, self.args, self.kwds = ( # type: ignore[assignment]
func,
@@ -67,7 +67,7 @@ class _OpenContextManager(GCM):
raise AttributeError("dvc.api.open() should be used in a with statement.")
def open( # noqa: A001, pylint: disable=redefined-builtin
def open( # noqa: A001
path: str,
repo: Optional[str] = None,
rev: Optional[str] = None,
+1 -1
View File
@@ -53,7 +53,7 @@ def _postprocess(exp_rows):
except ValueError:
exp_row[k] = v_str
if not exp_row[k]:
elif not exp_row[k]:
exp_row[k] = None
return exp_rows
-1
View File
@@ -2,7 +2,6 @@ from typing import Optional
try:
# file is created during dvc build
# pylint:disable=unused-import
from . import _build # type: ignore[attr-defined, import]
PKG: Optional[str] = _build.PKG # type: ignore[assignment]
+1 -1
View File
@@ -137,7 +137,7 @@ def migrate_2_to_3(repo: "Repo", dry: bool = False):
src = repo.cache.legacy
dest = repo.cache.local
if dry:
oids = list(src._list_oids()) # pylint: disable=protected-access
oids = list(src._list_oids())
ui.write(
f"{len(oids)} files will be re-hashed and migrated to the DVC 3.0 cache "
"location."
+15 -10
View File
@@ -2,15 +2,17 @@
import logging
import sys
from typing import Optional
from dvc.log import logger
# Workaround for CPython bug. See [1] and [2] for more info.
# [1] https://github.com/aws/aws-cli/blob/1.16.277/awscli/clidriver.py#L55
# [2] https://bugs.python.org/issue29288
from typing import Optional
"".encode("idna")
logger = logging.getLogger("dvc")
logger = logger.getChild(__name__)
class DvcParserError(Exception):
@@ -165,18 +167,18 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
# the copied parent's standard file descriptors. If we make any logging
# calls in this state it will cause an exception due to writing to a closed
# file descriptor.
if sys.stderr.closed: # pylint: disable=using-constant-test
if not sys.stderr or sys.stderr.closed:
logging.disable()
elif sys.stdout.closed: # pylint: disable=using-constant-test
elif not sys.stdout or sys.stdout.closed:
logging.disable(logging.INFO)
args = None
outer_log_level = logger.level
level = None
try:
args = parse_args(argv)
level = None
if args.quiet:
level = logging.CRITICAL
elif args.verbose == 1:
@@ -197,9 +199,9 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
logger.debug("v%s%s, %s on %s", __version__, pkg, pyv, platform())
logger.debug("command: %s", " ".join(argv or sys.argv))
logger.trace(args) # type: ignore[attr-defined]
logger.trace(args)
if not sys.stdout.closed and not args.quiet:
if sys.stdout and not sys.stdout.closed and not args.quiet:
from dvc.ui import ui
ui.enable()
@@ -230,16 +232,19 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
logger.exception("")
except DvcParserError:
ret = 254
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
# pylint: disable=no-member
except Exception as exc: # noqa: BLE001
ret = _log_exceptions(exc) or 255
try:
import os
from dvc import analytics
if analytics.is_enabled():
analytics.collect_and_send_report(args, ret)
logger.trace("Process %s exiting with %s", os.getpid(), ret)
return ret
finally:
logger.setLevel(outer_log_level)
+4 -3
View File
@@ -1,12 +1,13 @@
import logging
import os
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from dvc.log import logger
if TYPE_CHECKING:
from dvc.config import Config
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdBase(ABC):
@@ -31,7 +32,7 @@ class CmdBase(ABC):
class CmdBaseNoRepo(CmdBase):
def __init__(self, args): # pylint: disable=super-init-not-called
def __init__(self, args):
self.args = args
os.chdir(args.cd)
+7 -5
View File
@@ -1,6 +1,5 @@
"""Main parser for the dvc cli."""
import argparse
import logging
import os
from functools import lru_cache
@@ -27,6 +26,7 @@ from dvc.commands import (
get_url,
git_hook,
imp,
imp_db,
imp_url,
init,
install,
@@ -46,10 +46,11 @@ from dvc.commands import (
update,
version,
)
from dvc.log import logger
from . import DvcParserError
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
COMMANDS = [
init,
@@ -65,6 +66,7 @@ COMMANDS = [
data_sync,
gc,
imp,
imp_db,
imp_url,
config,
checkout,
@@ -95,12 +97,12 @@ COMMANDS = [
def _find_parser(parser, cmd_cls):
defaults = parser._defaults # pylint: disable=protected-access
defaults = parser._defaults
if not cmd_cls or cmd_cls == defaults.get("func"):
parser.print_help()
raise DvcParserError
actions = parser._actions # pylint: disable=protected-access
actions = parser._actions
for action in actions:
if not isinstance(action.choices, dict):
# NOTE: we are only interested in subparsers
@@ -112,7 +114,7 @@ def _find_parser(parser, cmd_cls):
class DvcParser(argparse.ArgumentParser):
"""Custom parser class for dvc CLI."""
def error(self, message, cmd_cls=None): # pylint: disable=arguments-differ
def error(self, message, cmd_cls=None):
logger.error(message)
_find_parser(self, cmd_cls)
+4
View File
@@ -2,6 +2,10 @@ import argparse
class DictAction(argparse.Action):
def __init__(self, *args, **kwargs):
kwargs.setdefault("metavar", "<name>=<value>")
super().__init__(*args, **kwargs)
def __call__(self, parser, args, values, option_string=None): # noqa: ARG002
d = getattr(args, self.dest) or {}
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdAdd(CmdBase):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import DictAction, append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdArtifactsGet(CmdBaseNoRepo):
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdCommit(CmdBase):
+3 -3
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.completion import PREAMBLE
from dvc.cli.utils import append_doc_link
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
SUPPORTED_SHELLS = ["bash", "zsh"]
@@ -18,7 +18,7 @@ class CmdCompletion(CmdBaseNoRepo):
shell = self.args.shell
parser = self.args.parser
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE) # nosec B604
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
ui.write(script, force=True)
return 0
+23 -21
View File
@@ -1,14 +1,16 @@
import argparse
import logging
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
from dvc.ui import ui
logger = logging.getLogger(__name__)
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")
+6
View File
@@ -1,6 +1,9 @@
from dvc.cli import completion
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import fix_subparsers
from dvc.log import logger
logger = logger.getChild(__name__)
class CmdDaemonBase(CmdBaseNoRepo):
@@ -21,6 +24,8 @@ class CmdDaemonUpdater(CmdDaemonBase):
config = Config(dvc_dir, validate=False)
hardlink_lock = config.get("core", {}).get("hardlink_lock", False)
updater = Updater(tmp_dir, hardlink_lock=hardlink_lock)
logger.info("Starting updater to fetch the latest version")
updater.fetch(detach=False)
return 0
@@ -30,6 +35,7 @@ class CmdDaemonAnalytics(CmdDaemonBase):
def run(self):
from dvc import analytics
logger.info("Sending analytics")
analytics.send(self.args.target)
return 0
+1 -1
View File
@@ -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)): # noqa: B301
for out in outs_trie.itervalues(prefix=repo.fs.path.parts(path)):
targets.extend(str(out))
return targets
+3 -5
View File
@@ -1,11 +1,11 @@
import argparse
import logging
from typing import TYPE_CHECKING
from funcy import chunks, compact, log_durations
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.log import logger
from dvc.ui import ui
from dvc.utils import colorize
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
from dvc.repo.data import Status as DataStatus
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdDataStatus(CmdBase):
@@ -106,9 +106,7 @@ class CmdDataStatus(CmdBase):
return 0
def run(self) -> int:
with log_durations(
logger.trace, "in data_status" # type: ignore[attr-defined]
):
with log_durations(logger.trace, "in data_status"):
status = self.repo.data_status(
granular=self.args.granular,
untracked_files=self.args.untracked_files,
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdDataBase(CmdBase):
+2 -2
View File
@@ -1,10 +1,10 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdDestroy(CmdBase):
+2 -2
View File
@@ -1,13 +1,13 @@
import argparse
import logging
import os
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 = logging.getLogger(__name__)
logger = logger.getChild(__name__)
def _digest(checksum):
+15 -8
View File
@@ -1,16 +1,24 @@
import argparse
import logging
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 = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsApply(CmdBase):
def run(self):
self.repo.experiments.apply(self.args.experiment, force=self.args.force)
if not self.args.force:
ui.write(
"The --no-force option is deprecated and will be removed in a future"
" DVC release. To revert the result of 'exp apply', run:\n"
"\n\tgit reset --hard\n"
"\tgit stash apply refs/exps/apply/stash\n"
)
self.repo.experiments.apply(self.args.experiment)
return 0
@@ -25,11 +33,10 @@ def add_parser(experiments_subparsers, parent_parser):
formatter_class=argparse.RawDescriptionHelpFormatter,
)
experiments_apply_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Do not prompt when removing working directory files.",
"--no-force",
action="store_false",
dest="force",
help="Fail if this command would overwrite conflicting changes.",
)
experiments_apply_parser.add_argument(
"experiment", help="Experiment to be applied."
+2 -2
View File
@@ -1,10 +1,10 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsBranch(CmdBase):
+2 -2
View File
@@ -1,10 +1,10 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsClean(CmdBase):
+2 -2
View File
@@ -1,14 +1,14 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.commands.metrics import DEFAULT_PRECISION
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsDiff(CmdBase):
+2 -3
View File
@@ -1,8 +1,7 @@
import logging
from dvc.cli.command import CmdBaseNoRepo
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExecutorRun(CmdBaseNoRepo):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import InvalidArgumentError
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsList(CmdBase):
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
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 = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsPull(CmdBase):
+2 -2
View File
@@ -1,13 +1,13 @@
import argparse
import logging
from typing import Any, Dict
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 = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsPush(CmdBase):
+2 -3
View File
@@ -1,8 +1,7 @@
import logging
from dvc.cli.command import CmdBase
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdQueueWorker(CmdBase):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import InvalidArgumentError
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsRemove(CmdBase):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import InvalidArgumentError
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsRename(CmdBase):
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
from dvc.cli.utils import append_doc_link
from dvc.commands.repro import CmdRepro
from dvc.commands.repro import add_arguments as add_repro_arguments
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsRun(CmdRepro):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdExperimentsSave(CmdBase):
+3 -3
View File
@@ -1,5 +1,4 @@
import argparse
import logging
import re
from datetime import date, datetime
from typing import TYPE_CHECKING, Dict, Iterable
@@ -10,6 +9,7 @@ from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.commands.metrics import DEFAULT_PRECISION
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.ui import ui
from dvc.utils.serialize import encode_exception
@@ -21,7 +21,7 @@ FILL_VALUE = "-"
FILL_VALUE_ERRORED = "!"
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
experiment_types = {
@@ -54,7 +54,7 @@ def baseline_styler(typ):
return {"style": "bold"} if typ == "baseline" else {}
def show_experiments( # noqa: C901
def show_experiments(
td: "TabularData",
headers: Dict[str, Iterable[str]],
keep=None,
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdFreezeBase(CmdBase):
+6 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
import os
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 = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdGC(CmdBase):
@@ -26,6 +26,10 @@ class CmdGC(CmdBase):
not_in_remote=self.args.not_in_remote,
)
# Don't prompt during dry run
if self.args.dry:
self.args.force = True
if self.args.rev:
self.args.num = self.args.num or 1
+2 -2
View File
@@ -1,12 +1,12 @@
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.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdGet(CmdBaseNoRepo):
+2 -2
View File
@@ -1,12 +1,12 @@
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.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdGetUrl(CmdBaseNoRepo):
+2 -2
View File
@@ -1,11 +1,11 @@
import logging
import os
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import fix_subparsers
from dvc.exceptions import NotDvcRepoError
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdHookBase(CmdBaseNoRepo):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import DictAction, append_doc_link
from dvc.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdImport(CmdBase):
+167
View File
@@ -0,0 +1,167 @@
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_adapter
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_adapter(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):
raise argparse.ArgumentTypeError("Either of --sql or --model is required.")
self.repo.imp_db(
url=self.args.url,
rev=self.args.rev,
project_dir=self.args.project_dir,
sql=self.args.sql,
model=self.args.model,
profile=self.args.profile,
target=self.args.target,
output_format=self.args.output_format,
out=self.args.out,
force=self.args.force,
connection=self.args.connection,
)
return 0
def add_parser(subparsers, parent_parser):
IMPORT_HELP = (
"Download file or directory tracked by DVC or by Git "
"into the workspace, and track it."
)
import_parser = subparsers.add_parser(
"import-db",
parents=[parent_parser],
description=append_doc_link(IMPORT_HELP, "import"),
add_help=False,
)
import_parser.add_argument(
"--url", help="Location of DVC or Git repository to download from"
)
import_parser.add_argument(
"--rev",
nargs="?",
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
import_parser.add_argument(
"--project-dir", nargs="?", help="Subdirectory to the dbt project location"
)
group = import_parser.add_mutually_exclusive_group()
group.add_argument(
"--sql",
help="SQL query",
)
group.add_argument(
"--model",
help="Model name to download",
)
import_parser.add_argument("--profile", help="Profile to use")
import_parser.add_argument("--target", help="Target to use")
import_parser.add_argument(
"--output-format",
default="csv",
const="csv",
nargs="?",
choices=["csv", "json"],
help="Export format",
)
import_parser.add_argument(
"-o",
"--out",
nargs="?",
help="Destination path to download files to",
metavar="<path>",
).complete = completion.FILE
import_parser.add_argument(
"-f",
"--force",
action="store_true",
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)
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import DictAction, append_doc_link
from dvc.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdImportUrl(CmdBase):
+2 -2
View File
@@ -1,15 +1,15 @@
import argparse
import logging
import colorama
from dvc import analytics
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.log import logger
from dvc.utils import boxify
from dvc.utils import format_link as fmt_link
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
def _welcome_message():
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdInstall(CmdBase):
+2 -2
View File
@@ -1,14 +1,14 @@
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.commands.ls.ls_colors import LsColors
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
def _format_entry(entry, fmt):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import DictAction, append_doc_link
from dvc.log import logger
from .ls import show_entries
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdListUrl(CmdBaseNoRepo):
+2 -2
View File
@@ -1,13 +1,13 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.log import logger
from dvc.ui import ui
from dvc.utils.serialize import encode_exception
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
DEFAULT_PRECISION = 5
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdMove(CmdBase):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdParamsDiff(CmdBase):
+2 -2
View File
@@ -1,5 +1,4 @@
import argparse
import logging
import os
from typing import TYPE_CHECKING, Dict, List, Optional
@@ -9,6 +8,7 @@ from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.ui import ui
from dvc.utils import format_link
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
from dvc.render.match import RendererWithErrors
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
def _show_json(
+2 -2
View File
@@ -1,10 +1,10 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdQueueKill(CmdBase):
+2 -2
View File
@@ -1,10 +1,10 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdQueueLogs(CmdBase):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import InvalidArgumentError
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdQueueRemove(CmdBase):
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
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 = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdQueueStart(CmdBase):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.compare import TabularData
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdQueueStatus(CmdBase):
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
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 = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdQueueStop(CmdBase):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdRemove(CmdBase):
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.log import logger
from dvc.utils import relpath
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdRoot(CmdBaseNoRepo):
+3 -4
View File
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Dict, Iterable, List
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.log import logger
from dvc.utils.cli_parse import parse_params
from dvc.utils.humanize import truncate_text
@@ -14,7 +15,7 @@ if TYPE_CHECKING:
from dvc.output import Output
from dvc.stage import Stage
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
MAX_TEXT_LENGTH = 80
@@ -67,9 +68,7 @@ class CmdStageList(CmdBase):
def _get_stages(self) -> Iterable["Stage"]:
if self.args.all:
stages: List["Stage"] = self.repo.index.stages
logger.trace( # type: ignore[attr-defined]
"%d no. of stages found", len(stages)
)
logger.trace("%d no. of stages found", len(stages))
return stages
# removing duplicates while maintaining order
+2 -3
View File
@@ -1,11 +1,10 @@
import logging
from dvc.commands.data_sync import CmdDataBase
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.ui import ui
from dvc.utils import format_link
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdDataStatus(CmdDataBase):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdUnprotect(CmdBase):
+2 -2
View File
@@ -1,12 +1,12 @@
import argparse
import logging
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.exceptions import DvcException
from dvc.log import logger
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdUpdate(CmdBase):
+2 -2
View File
@@ -1,11 +1,11 @@
import argparse
import logging
from dvc.cli.command import CmdBaseNoRepo
from dvc.cli.utils import append_doc_link
from dvc.log import logger
from dvc.ui import ui
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class CmdVersion(CmdBaseNoRepo):
+6 -6
View File
@@ -1,5 +1,4 @@
"""DVC config objects."""
import logging
import os
import re
from contextlib import contextmanager
@@ -9,6 +8,7 @@ from typing import TYPE_CHECKING, Dict, Optional
from funcy import compact, memoize, re_find
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc.log import logger
from .utils.objects import cached_property
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
from dvc.fs import FileSystem
from dvc.types import DictStrAny, StrPath
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class ConfigError(DvcException):
@@ -96,7 +96,7 @@ class Config(dict):
config: Optional["DictStrAny"] = None,
remote: Optional[str] = None,
remote_config: Optional["DictStrAny"] = None,
): # pylint: disable=super-init-not-called
):
from dvc.fs import LocalFileSystem
dvc_dir = os.fspath(dvc_dir) if dvc_dir else None
@@ -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)
+15 -2
View File
@@ -1,4 +1,3 @@
import logging
import os
from urllib.parse import urlparse
@@ -16,7 +15,9 @@ from voluptuous import (
Schema,
)
logger = logging.getLogger(__name__)
from dvc.log import logger
logger = logger.getChild(__name__)
Bool = All(
Lower,
@@ -256,6 +257,9 @@ SCHEMA = {
"exclude_shared_token_cache_credential": Bool,
"exclude_managed_identity_credential": Bool,
Optional("verify", default=False): Bool,
"timeout": int,
"read_timeout": int,
"connection_timeout": int,
**REMOTE_COMMON,
},
"oss": {
@@ -314,6 +318,8 @@ SCHEMA = {
"feature": FeatureSchema(
{
Optional("machine", default=False): Bool,
"dbt_profile": str,
"dbt_target": str,
},
),
"plots": {
@@ -346,4 +352,11 @@ SCHEMA = {
"repo_url": str,
Optional("offline", default=False): Bool,
},
"db": {
str: {
"url": str,
"username": str,
"password": str,
},
},
}
+125 -78
View File
@@ -3,138 +3,185 @@
import inspect
import logging
import os
import platform
import subprocess
import sys
from subprocess import Popen # nosec B404
from typing import List
from contextlib import nullcontext
from typing import TYPE_CHECKING, Any, List, Mapping, Optional, Sequence, Union
from dvc.env import DVC_DAEMON
from dvc.log import logger
if TYPE_CHECKING:
from typing import ContextManager
from dvc.env import DVC_DAEMON, DVC_DAEMON_LOGFILE
from dvc.utils import fix_env, is_binary
from dvc.utils.collections import ensure_list
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
def _suppress_resource_warning(popen: Popen):
def _suppress_resource_warning(popen: subprocess.Popen) -> None:
"""Sets the returncode to avoid ResourceWarning when popen is garbage collected."""
# only use for daemon processes.
# See https://bugs.python.org/issue38890.
popen.returncode = 0
def _popen(cmd, **kwargs) -> Popen:
prefix = [sys.executable]
if not is_binary():
main_entrypoint = os.path.join(
os.path.abspath(os.path.dirname(__file__)), "__main__.py"
)
prefix += [main_entrypoint]
return Popen(
prefix + cmd, close_fds=True, shell=False, **kwargs # nosec B603 # noqa: S603
def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
assert os.name == "nt"
from subprocess import ( # type: ignore[attr-defined]
CREATE_NEW_PROCESS_GROUP,
CREATE_NO_WINDOW,
STARTF_USESHOWWINDOW,
STARTUPINFO,
)
# https://stackoverflow.com/a/7006424
# https://bugs.python.org/issue41619
creationflags = CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
def _spawn_windows(cmd, env):
if sys.platform == "win32":
from subprocess import ( # nosec B404
CREATE_NEW_PROCESS_GROUP,
CREATE_NO_WINDOW,
STARTF_USESHOWWINDOW,
STARTUPINFO,
)
# https://stackoverflow.com/a/7006424
# https://bugs.python.org/issue41619
creationflags = CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW
startupinfo = STARTUPINFO()
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
popen = _popen(
cmd, env=env, creationflags=creationflags, startupinfo=startupinfo
)
_suppress_resource_warning(popen)
startupinfo = STARTUPINFO()
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
popen = subprocess.Popen(
args,
close_fds=True,
shell=False, # noqa: S603
startupinfo=startupinfo,
creationflags=creationflags,
**kwargs,
)
_suppress_resource_warning(popen)
return popen.pid
def _spawn_posix(cmd, env):
from dvc.cli import main
def _get_dvc_args() -> List[str]:
args = [sys.executable]
if not is_binary():
root_dir = os.path.abspath(os.path.dirname(__file__))
main_entrypoint = os.path.join(root_dir, "__main__.py")
args.append(main_entrypoint)
return args
# `fork` will copy buffers, so we need to flush them before forking.
# Otherwise, we will get duplicated outputs.
if sys.stdout and not sys.stdout.closed:
sys.stdout.flush()
if sys.stderr and not sys.stderr.closed:
sys.stderr.flush()
def _fork_process() -> int:
assert os.name == "posix"
# NOTE: using os._exit instead of sys.exit, because dvc built
# with PyInstaller has trouble with SystemExit exception and throws
# errors such as "[26338] Failed to execute script __main__"
try:
# pylint: disable-next=no-member
pid = os.fork() # type: ignore[attr-defined]
if pid > 0:
return
return pid
except OSError:
logger.exception("failed at first fork")
os._exit(1) # pylint: disable=protected-access
os._exit(1)
os.setsid() # type: ignore[attr-defined] # pylint: disable=no-member
os.setsid() # type: ignore[attr-defined]
try:
# pylint: disable-next=no-member
pid = os.fork() # type: ignore[attr-defined]
if pid > 0:
os._exit(0) # pylint: disable=protected-access
os._exit(0)
except OSError:
logger.exception("failed at second fork")
os._exit(1) # pylint: disable=protected-access
os._exit(1)
sys.stdin.close()
sys.stdout.close()
sys.stderr.close()
os.closerange(0, 3)
if platform.system() == "Darwin":
# workaround for MacOS bug
# https://github.com/iterative/dvc/issues/4294
_popen(cmd, env=env).communicate()
else:
os.environ.update(env)
main(cmd)
os._exit(0) # pylint: disable=protected-access
# disconnect from the terminal
fd = os.open(os.devnull, os.O_RDWR)
for fd2 in range(3):
os.dup2(fd, fd2)
os.close(fd)
return pid
def _spawn(cmd, env):
logger.debug("Trying to spawn '%s'", cmd)
def _posix_detached_subprocess(args: Sequence[str], **kwargs) -> int:
# double fork and execute a subprocess so that there are no zombies
read_end, write_end = os.pipe()
pid = _fork_process()
if pid > 0: # in parent
os.close(write_end)
pid_str = os.read(read_end, 32).decode("utf8")
os.close(read_end)
return int(pid_str)
proc = subprocess.Popen(
args,
shell=False, # noqa: S603
close_fds=True,
**kwargs,
)
os.close(read_end)
os.write(write_end, str(proc.pid).encode("utf8"))
os.close(write_end)
exit_code = proc.wait()
os._exit(exit_code)
def _detached_subprocess(args: Sequence[str], **kwargs) -> int:
"""Run in a detached subprocess."""
kwargs.setdefault("stdin", subprocess.DEVNULL)
kwargs.setdefault("stdout", subprocess.DEVNULL)
kwargs.setdefault("stderr", subprocess.DEVNULL)
if os.name == "nt":
_spawn_windows(cmd, env)
elif os.name == "posix":
_spawn_posix(cmd, env)
else:
raise NotImplementedError
logger.debug("Spawned '%s'", cmd)
return _win_detached_subprocess(args, **kwargs)
return _posix_detached_subprocess(args, **kwargs)
def daemon(args):
def _map_log_level_to_flag() -> Optional[str]:
flags = {logging.DEBUG: "-v", logging.TRACE: "-vv"} # type: ignore[attr-defined]
return flags.get(logger.getEffectiveLevel())
def daemon(args: List[str]) -> None:
"""Launch a `dvc daemon` command in a detached process.
Args:
args (list): list of arguments to append to `dvc daemon` command.
"""
daemonize(["daemon", "-q", *args])
if flag := _map_log_level_to_flag():
args = [*args, flag]
daemonize(["daemon", *args])
def daemonize(cmd: List[str]):
def _spawn(
args: List[str],
executable: Optional[Union[str, List[str]]] = None,
env: Optional[Mapping[str, str]] = None,
output_file: Optional[str] = None,
) -> int:
file: "ContextManager[Any]" = nullcontext()
kwargs = {}
if output_file:
file = open(output_file, "ab") # noqa: SIM115
kwargs = {"stdout": file, "stderr": file}
if executable is None:
executable = _get_dvc_args()
else:
executable = ensure_list(executable)
with file:
return _detached_subprocess(executable + args, env=env, **kwargs)
def daemonize(args: List[str], executable: Union[None, str, List[str]] = None) -> None:
if os.name not in ("posix", "nt"):
return None
if os.environ.get(DVC_DAEMON):
logger.debug("skipping launching a new daemon.")
return
env = fix_env()
env[DVC_DAEMON] = "1"
if not is_binary():
file_path = os.path.abspath(inspect.stack()[0][1])
env["PYTHONPATH"] = os.path.dirname(os.path.dirname(file_path))
env[DVC_DAEMON] = "1"
_spawn(cmd, env)
logger.debug("Trying to spawn %r", args)
pid = _spawn(args, executable, env, output_file=env.get(DVC_DAEMON_LOGFILE))
logger.debug("Spawned %r with pid %s", args, pid)
+9 -11
View File
@@ -1,6 +1,5 @@
"""Draws DAG in ASCII."""
import logging
import math
import os
@@ -8,7 +7,9 @@ from grandalf.graphs import Edge, Graph, Vertex
from grandalf.layouts import SugiyamaLayout
from grandalf.routing import EdgeViewer, route_with_lines
logger = logging.getLogger(__name__)
from dvc.log import logger
logger = logger.getChild(__name__)
class VertexViewer:
@@ -22,17 +23,16 @@ class VertexViewer:
HEIGHT = 3 # top and bottom box edges + text
def __init__(self, name):
# pylint: disable=invalid-name
self._h = self.HEIGHT # top and bottom box edges + text
self._w = len(name) + 2 # right and left bottom edges + text
@property
def h(self): # pylint: disable=invalid-name
def h(self):
"""Height of the box."""
return self._h
@property
def w(self): # pylint: disable=invalid-name
def w(self):
"""Width of the box."""
return self._w
@@ -90,7 +90,6 @@ class AsciiCanvas:
y1 (int): y coordinate where the line should end.
char (str): character to draw the line with.
"""
# pylint: disable=too-many-arguments, too-many-branches
if x0 > x1:
x1, x0 = x0, x1
y1, y0 = y0, y1
@@ -234,11 +233,11 @@ def draw(vertices, edges):
| 1 |
+---+
"""
# pylint: disable=too-many-locals
# NOTE: coordinates might me negative, so we need to shift
# everything to the positive plane before we actually draw it.
Xs = [] # noqa: N806, pylint: disable=invalid-name
Ys = [] # noqa: N806, pylint: disable=invalid-name
Xs = [] # noqa: N806
Ys = [] # noqa: N806
sug = _build_sugiyama_layout(vertices, edges)
@@ -250,7 +249,7 @@ def draw(vertices, edges):
Ys.append(vertex.view.xy[1] + vertex.view.h)
for edge in sug.g.sE:
for x, y in edge.view._pts: # pylint: disable=protected-access
for x, y in edge.view._pts:
Xs.append(x)
Ys.append(y)
@@ -266,7 +265,6 @@ def draw(vertices, edges):
# NOTE: first draw edges so that node boxes could overwrite them
for edge in sug.g.sE:
# pylint: disable=protected-access
assert len(edge.view._pts) > 1
for index in range(1, len(edge.view._pts)):
start = edge.view._pts[index - 1]
+2 -2
View File
@@ -1,10 +1,10 @@
"""Manages dvc remotes that user can use with push/pull/status commands."""
import logging
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
from dvc_data.hashfile.transfer import TransferResult
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.status import CompareStatusResult
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class Remote:
+22
View File
@@ -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
Adapter = Union[sqla.SQLAlchemyAdapter, dbt_query.DbtAdapter]
def get_adapter(
config,
project_dir: Optional[str] = None,
profile: Optional[str] = None,
target: Optional[str] = None,
**kwargs: Any,
) -> "ContextManager[Adapter]":
if config:
return sqla.adapter(config, **kwargs)
return dbt_query.adapter(project_dir=project_dir, profile=profile, target=target)
__all__ = ["export", "get_adapter", "get_model", "Adapter"]
+40
View File
@@ -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)
+88
View File
@@ -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 DbtAdapter:
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 adapter(
project_dir: Optional[str] = None,
profiles_dir: Optional[str] = None,
profile: Optional[str] = None,
target: Optional[str] = None,
) -> Iterator["DbtAdapter"]:
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 DbtAdapter(adapter, creds)
+61
View File
@@ -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
+100
View File
@@ -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)
+54
View File
@@ -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)
+73
View File
@@ -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 SQLAlchemyAdapter:
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 adapter(
url_or_config: Union[str, URL, Dict[str, str]], **engine_kwargs: Any
) -> Iterator[SQLAlchemyAdapter]:
url = url_from_config(url_or_config)
with handle_error(url):
engine = create_engine(url, **engine_kwargs)
try:
yield SQLAlchemyAdapter(engine)
finally:
engine.dispose()
+17 -6
View File
@@ -4,6 +4,7 @@ from typing import Any, Dict, List, Mapping, Set
from dvc.output import ARTIFACT_SCHEMA, DIR_FILES_SCHEMA, Output
from .base import Dependency
from .db import DB_SCHEMA, PARAM_DB, DbDependency, DbtDependency
from .param import ParamsDependency
from .repo import RepoDependency
@@ -14,20 +15,28 @@ from .repo import RepoDependency
SCHEMA: Mapping[str, Any] = {
**ARTIFACT_SCHEMA,
**RepoDependency.REPO_SCHEMA,
**DB_SCHEMA,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
Output.PARAM_FS_CONFIG: dict,
}
def _get(stage, p, info, **kwargs):
if info and info.get(RepoDependency.PARAM_REPO):
repo = info.pop(RepoDependency.PARAM_REPO)
return RepoDependency(repo, stage, p, info)
d = info or {}
db = d.get(PARAM_DB, {})
params = d.pop(ParamsDependency.PARAM_PARAMS, None)
repo = d.pop(RepoDependency.PARAM_REPO, None)
if info and info.get(ParamsDependency.PARAM_PARAMS):
params = info.pop(ParamsDependency.PARAM_PARAMS)
if params:
return ParamsDependency(stage, p, params)
if DbDependency.PARAM_QUERY in db:
return DbDependency(stage, info)
if db:
return DbtDependency(repo, stage, info)
assert p
if repo:
return RepoDependency(repo, stage, p, info)
return Dependency(stage, p, info, **kwargs)
@@ -44,9 +53,11 @@ def loadd_from(stage, d_list):
return ret
def loads_from(stage, s_list, erepo=None, fs_config=None):
def loads_from(stage, s_list, erepo=None, fs_config=None, db=None):
assert isinstance(s_list, list)
info = {RepoDependency.PARAM_REPO: erepo} if erepo else {}
if db:
info.update({"db": db})
return [
_get(
stage,
+324
View File
@@ -0,0 +1,324 @@
import os
from contextlib import contextmanager, nullcontext
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Union
from funcy import compact, log_durations
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.scm import SCM
from .base import Dependency
if TYPE_CHECKING:
from rich.status import Status
from dvc.output import Output
from dvc.stage import Stage
logger = logger.getChild(__name__)
PARAM_DB = "db"
PARAM_PROFILE = "profile"
PARAM_FILE_FORMAT = "file_format"
def _get_dbt_config(config: Dict) -> Dict:
conf = config.get("feature", {})
pref = "dbt_"
return {k.lstrip(pref): v for k, v in conf.items() if k.startswith(pref)}
@contextmanager
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), status or ui.status(msg) as st:
st.update(msg)
yield st
@contextmanager
def chdir(path):
wdir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(wdir)
@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: Dict[str, Any], *args, **kwargs):
self.repo = stage.repo
self.stage = stage
self.fs = None
self.fs_path = None
self.def_path = None # type: ignore[assignment]
self.info = info or {}
class DbDependency(AbstractDependency):
PARAM_QUERY = "query"
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)
self.target = None
def __repr__(self):
return "{}:{}".format(
self.__class__.__name__, "".join(f"{k}=={v}" for k, v in self.info.items())
)
def __str__(self):
from dvc.utils.humanize import truncate_text
db_info = self.info.get(PARAM_DB, {})
query = db_info.get(self.PARAM_QUERY, "[query]")
return truncate_text(query, 50)
def workspace_status(self):
return False # no workspace to check
def status(self):
return self.workspace_status()
def save(self):
"""nothing to save."""
def dumpd(self, **kwargs):
db_info = compact(self.info.get(PARAM_DB, {}))
return {PARAM_DB: db_info} if db_info else {}
def update(self, rev=None):
"""nothing to update."""
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_adapter
db_info = self.info.get(PARAM_DB, {})
query = db_info.get(self.PARAM_QUERY)
if not query:
raise DvcException("Cannot download: no query specified")
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")
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")
project_dir = self.repo.root_dir
with get_adapter(
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):
PARAM_MODEL = "model"
PARAM_VERSION = "version"
PARAM_PROJECT_DIR = "project_dir"
DBT_SCHEMA = {
PARAM_MODEL: str,
PARAM_VERSION: str,
PARAM_PROJECT_DIR: str,
}
def __init__(self, def_repo: Dict[str, Any], stage: "Stage", info, *args, **kwargs):
self.def_repo = def_repo or {}
self.target = None
super().__init__(stage, info, *args, **kwargs)
def __repr__(self):
return "{}:{}".format(
self.__class__.__name__,
"".join(f"{k}=={v}" for k, v in {**self.def_repo, **self.info}.items()),
)
def __str__(self):
from .repo import RepoDependency
repo = self.def_repo.get(RepoDependency.PARAM_URL)
rev = self.def_repo.get(RepoDependency.PARAM_REV)
db_info = self.info.get(PARAM_DB, {})
db = db_info.get(self.PARAM_MODEL, "")
project_dir = db_info.get(self.PARAM_PROJECT_DIR, "")
repo_info = ""
if repo:
repo_info += repo
if rev:
repo_info += f"@{rev}"
if project_dir:
repo_info += f":/{project_dir}"
return db + (f"({repo_info})" if repo_info else "")
@property
def locked_rev(self):
from .repo import RepoDependency
return self.def_repo.get(RepoDependency.PARAM_REV_LOCK)
@property
def rev(self):
from .repo import RepoDependency
return self.def_repo.get(RepoDependency.PARAM_REV)
def workspace_status(self):
if not self.def_repo:
return
current = self._get_clone(self.locked_rev or self.rev).get_rev()
updated = self._get_clone(self.rev).get_rev()
if current != updated:
return {str(self): "update available"}
return {}
def status(self):
return self.workspace_status()
def save(self):
from .repo import RepoDependency
if not self.def_repo:
return
rev = self._get_clone(self.locked_rev or self.rev).get_rev()
if self.def_repo.get(RepoDependency.PARAM_REV_LOCK) is None:
self.def_repo[RepoDependency.PARAM_REV_LOCK] = rev
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
from .repo import RepoDependency
def_repo = {}
if self.def_repo:
def_repo = RepoDependency._dump_def_repo(self.def_repo)
db_info = compact(self.info.get(PARAM_DB, {}))
return compact({RepoDependency.PARAM_REPO: def_repo, PARAM_DB: db_info})
def _get_clone(self, rev):
from dvc.repo.open_repo import _cached_clone
from .repo import RepoDependency
url = self.def_repo.get(RepoDependency.PARAM_URL)
repo_root = self.repo.root_dir if self.repo else os.getcwd()
return SCM(_cached_clone(url, rev) if url else repo_root)
def update(self, rev=None):
from .repo import RepoDependency
if not self.def_repo:
return
if rev:
self.def_repo[RepoDependency.PARAM_REV] = rev
else:
rev = self.rev
self.def_repo[RepoDependency.PARAM_REV_LOCK] = self._get_clone(rev).get_rev()
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, "")
if self.def_repo:
ctx = repo = self._get_clone(self.locked_rev or self.rev)
self.def_repo[RepoDependency.PARAM_REV_LOCK] = repo.get_rev()
root = wdir = repo.root_dir
else:
ctx = nullcontext()
root = self.repo.root_dir
wdir = self.stage.wdir
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)
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")
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"):
serializer = get_model(
model, version=version, profile=profile, target=target
)
# NOTE: we keep everything in memory, and then export it out later.
with download_progress(to) as progress:
export(serializer, to.fs_path, format=file_format, progress=progress)
DB_SCHEMA = {
PARAM_DB: {
PARAM_PROFILE: str,
PARAM_FILE_FORMAT: str,
**DbDependency.QUERY_SCHEMA,
**DbtDependency.DBT_SCHEMA,
},
}
+3 -3
View File
@@ -1,4 +1,3 @@
import logging
import os
import typing
from collections import defaultdict
@@ -7,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
import dpath
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.utils.serialize import ParseError, load_path
from dvc_data.hashfile.hash_info import HashInfo
@@ -15,7 +15,7 @@ from .base import Dependency
if TYPE_CHECKING:
from dvc.fs import FileSystem
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class MissingParamsError(DvcException):
@@ -132,8 +132,8 @@ class ParamsDependency(Dependency):
from funcy import ldistinct
status: Dict[str, Any] = defaultdict(dict)
assert isinstance(self.hash_info.value, dict)
info = self.hash_info.value if self.hash_info else {}
assert isinstance(info, dict)
actual = self.read_params()
# NOTE: we want to preserve the order of params as specified in the
+19 -16
View File
@@ -1,7 +1,7 @@
from copy import deepcopy
from typing import TYPE_CHECKING, Dict, Optional, Union
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from voluptuous import Any, Required
import voluptuous as vol
from dvc.utils import as_posix
@@ -22,11 +22,11 @@ class RepoDependency(Dependency):
REPO_SCHEMA = {
PARAM_REPO: {
Required(PARAM_URL): str,
vol.Required(PARAM_URL): str,
PARAM_REV: str,
PARAM_REV_LOCK: str,
PARAM_CONFIG: Any(str, dict),
PARAM_REMOTE: Any(str, dict),
PARAM_CONFIG: vol.Any(str, dict),
PARAM_REMOTE: vol.Any(str, dict),
}
}
@@ -64,28 +64,31 @@ class RepoDependency(Dependency):
if self.def_repo.get(self.PARAM_REV_LOCK) is None:
self.def_repo[self.PARAM_REV_LOCK] = rev
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
repo = {self.PARAM_URL: self.def_repo[self.PARAM_URL]}
@classmethod
def _dump_def_repo(cls, def_repo) -> Dict[str, str]:
repo = {cls.PARAM_URL: def_repo[cls.PARAM_URL]}
rev = self.def_repo.get(self.PARAM_REV)
rev = def_repo.get(cls.PARAM_REV)
if rev:
repo[self.PARAM_REV] = self.def_repo[self.PARAM_REV]
repo[cls.PARAM_REV] = def_repo[cls.PARAM_REV]
rev_lock = self.def_repo.get(self.PARAM_REV_LOCK)
rev_lock = def_repo.get(cls.PARAM_REV_LOCK)
if rev_lock:
repo[self.PARAM_REV_LOCK] = rev_lock
repo[cls.PARAM_REV_LOCK] = rev_lock
config = self.def_repo.get(self.PARAM_CONFIG)
config = def_repo.get(cls.PARAM_CONFIG)
if config:
repo[self.PARAM_CONFIG] = config
repo[cls.PARAM_CONFIG] = config
remote = self.def_repo.get(self.PARAM_REMOTE)
remote = def_repo.get(cls.PARAM_REMOTE)
if remote:
repo[self.PARAM_REMOTE] = remote
repo[cls.PARAM_REMOTE] = remote
return repo
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
return {
self.PARAM_PATH: self.def_path,
self.PARAM_REPO: repo,
self.PARAM_REPO: self._dump_def_repo(self.def_repo),
}
def update(self, rev: Optional[str] = None):
+1 -1
View File
@@ -15,7 +15,7 @@ def system_config_dir():
def global_config_dir():
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.user_config_dir(
return os.getenv(env.DVC_GLOBAL_CONFIG_DIR) or platformdirs.user_config_dir(
APPNAME, APPAUTHOR
)
+3 -7
View File
@@ -1,5 +1,4 @@
import contextlib
import logging
import os
from typing import (
TYPE_CHECKING,
@@ -14,6 +13,7 @@ from typing import (
)
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.stage import serialize
from dvc.stage.exceptions import (
StageFileBadNameError,
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
from .parsing import DataResolver
from .stage import Stage
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
_T = TypeVar("_T")
DVC_FILE_SUFFIX = ".dvc"
@@ -166,7 +166,6 @@ class FileMixin:
**kwargs,
)
# pylint: disable-next=unused-argument
def remove(self, force=False): # noqa: ARG002
with contextlib.suppress(FileNotFoundError):
os.unlink(self.path)
@@ -208,7 +207,6 @@ class SingleStageFile(FileMixin):
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
self.repo.scm_context.track_file(self.relpath)
# pylint: disable-next=unused-argument
def remove_stage(self, stage): # noqa: ARG002
self.remove()
@@ -237,9 +235,7 @@ class ProjectFile(FileMixin):
self.__dict__.pop("resolver", None)
self.__dict__.pop("stages", None)
def dump(
self, stage, update_pipeline=True, update_lock=True, **kwargs
): # pylint: disable=arguments-differ
def dump(self, stage, update_pipeline=True, update_lock=True, **kwargs):
"""Dumps given stage appropriately in the dvcfile."""
from dvc.stage import PipelineStage
+4 -1
View File
@@ -1,4 +1,6 @@
DVC_ANALYTICS_ENDPOINT = "DVC_ANALYTICS_ENDPOINT"
DVC_DAEMON = "DVC_DAEMON"
DVC_DAEMON_LOGFILE = "DVC_DAEMON_LOGFILE"
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
@@ -12,6 +14,7 @@ DVC_SHOW_TRACEBACK = "DVC_SHOW_TRACEBACK"
DVC_SITE_CACHE_DIR = "DVC_SITE_CACHE_DIR"
DVC_STUDIO_OFFLINE = "DVC_STUDIO_OFFLINE"
DVC_STUDIO_REPO_URL = "DVC_STUDIO_REPO_URL"
DVC_STUDIO_TOKEN = "DVC_STUDIO_TOKEN" # noqa: S105 # nosec B105
DVC_STUDIO_TOKEN = "DVC_STUDIO_TOKEN" # noqa: S105
DVC_STUDIO_URL = "DVC_STUDIO_URL"
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
DVC_UPDATER_ENDPOINT = "DVC_UPDATER_ENDPOINT"
+1 -1
View File
@@ -301,7 +301,7 @@ class URLMissingError(DvcException):
super().__init__(f"The path '{url}' does not exist")
class IsADirectoryError(DvcException): # noqa: A001,pylint:disable=redefined-builtin
class IsADirectoryError(DvcException): # noqa: A001
"""Raised when a file operation is requested on a directory."""
+7 -6
View File
@@ -29,7 +29,7 @@ from dvc_objects.fs.path import Path # noqa: F401
from .callbacks import Callback
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(
@@ -46,12 +46,11 @@ known_implementations.update(
)
# pylint: enable=unused-import
def download(
fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
) -> int:
from dvc.scm import lfs_prefetch
with Callback.as_tqdm_callback(
desc=f"Downloading {fs.path.name(fs_path)}",
unit="files",
@@ -75,6 +74,8 @@ def download(
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(
@@ -166,8 +167,8 @@ def get_cloud_fs(repo_config, **kwargs):
# should be treated as being a root path.
fs_path = cls.root_marker
else:
fs_path = cls._strip_protocol(url) # pylint:disable=protected-access
fs_path = cls._strip_protocol(url)
extras = cls._get_kwargs_from_urls(url) # pylint:disable=protected-access
extras = cls._get_kwargs_from_urls(url)
conf = {**extras, **remote_conf} # remote config takes priority
return cls, conf, fs_path
+1 -2
View File
@@ -1,4 +1,3 @@
# pylint: disable=unused-import
from contextlib import ExitStack
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
@@ -72,7 +71,7 @@ class RichCallback(Callback):
value: int = 0,
progress: Optional["RichTransferProgress"] = None,
desc: Optional[str] = None,
bytes: bool = False, # noqa: A002, pylint: disable=redefined-builtin
bytes: bool = False, # noqa: A002
unit: Optional[str] = None,
disable: bool = False,
transient: bool = True,
+3 -3
View File
@@ -1,8 +1,8 @@
import functools
import logging
import os
from typing import TYPE_CHECKING
from dvc.log import logger
from dvc.utils import as_posix
from dvc_objects.fs.base import FileSystem
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
from dvc_data.fs import DataFileSystem as _DataFileSystem
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
class DataFileSystem(FileSystem):
@@ -22,7 +22,7 @@ class DataFileSystem(FileSystem):
return config
@functools.cached_property
def fs( # pylint: disable=invalid-overridden-method
def fs(
self,
) -> "_DataFileSystem":
from dvc_data.fs import DataFileSystem as _DataFileSystem
+13 -21
View File
@@ -1,6 +1,5 @@
import errno
import functools
import logging
import ntpath
import os
import posixpath
@@ -11,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Un
from fsspec.spec import AbstractFileSystem
from funcy import wrap_with
from dvc.log import logger
from dvc_objects.fs.base import FileSystem
from dvc_objects.fs.path import Path
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.types import DictStrAny, StrPath
logger = logging.getLogger(__name__)
logger = logger.getChild(__name__)
RepoFactory = Union[Callable[..., "Repo"], Type["Repo"]]
Key = Tuple[str, ...]
@@ -72,7 +72,7 @@ def _get_dvc_path(dvc_fs, subkey):
return dvc_fs.path.join(*subkey) if subkey else ""
class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
class _DVCFileSystem(AbstractFileSystem):
cachable = False
root_marker = "/"
@@ -84,9 +84,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
subrepos: bool = False,
repo_factory: Optional[RepoFactory] = None,
fo: Optional[str] = None,
# pylint:disable-next=unused-argument
target_options: Optional[Dict[str, Any]] = None, # noqa: ARG002
# pylint:disable-next=unused-argument
target_protocol: Optional[str] = None, # noqa: ARG002
config: Optional["DictStrAny"] = None,
remote: Optional[str] = None,
@@ -146,7 +144,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
remote_config=remote_config,
)
assert repo is not None
# pylint: disable=protected-access
repo_factory = repo._fs_conf["repo_factory"]
self._repo_stack.enter_context(repo)
@@ -282,9 +280,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
dvc_fs = self._datafss.get(repo_key)
return repo, dvc_fs, subkey
def _open(
self, path, mode="rb", **kwargs
): # pylint: disable=arguments-renamed, arguments-differ
def _open(self, path, mode="rb", **kwargs):
if mode != "rb":
raise OSError(errno.EROFS, os.strerror(errno.EROFS))
@@ -307,9 +303,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
except FileNotFoundError:
return False
def ls( # pylint: disable=arguments-differ # noqa: C901
self, path, detail=True, dvc_only=False, **kwargs
):
def ls(self, path, detail=True, dvc_only=False, **kwargs):
key = self._get_key_from_relative(path)
repo, dvc_fs, subkey = self._get_subrepo_info(key)
@@ -320,7 +314,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
with suppress(FileNotFoundError, NotADirectoryError):
for info in dvc_fs.ls(dvc_path, detail=True):
dvc_infos[dvc_fs.path.name(info["name"])] = info
dvc_exists = bool(dvc_infos) or dvc_fs.exists(dvc_path)
dvc_exists = True
fs_exists = False
fs_infos = {}
@@ -333,10 +327,13 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
fs, fs_path, detail=True, ignore_subrepos=ignore_subrepos
):
fs_infos[fs.path.name(info["name"])] = info
fs_exists = True
except (FileNotFoundError, NotADirectoryError):
pass
fs_exists = bool(fs_infos) or fs.exists(fs_path)
if not (dvc_exists or fs_exists):
# broken symlink or TreeError
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
dvcfiles = kwargs.get("dvcfiles", False)
@@ -344,10 +341,6 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
paths = []
names = set(dvc_infos.keys()) | set(fs_infos.keys())
if not names and (dvc_exists or fs_exists):
# broken symlink or TreeError
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
for name in names:
if not dvcfiles and _is_dvc_file(name):
continue
@@ -415,7 +408,7 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
info["name"] = path
return info
def get_file(self, rpath, lpath, **kwargs): # pylint: disable=arguments-differ
def get_file(self, rpath, lpath, **kwargs):
key = self._get_key_from_relative(rpath)
fs_path = self._from_key(key)
try:
@@ -440,7 +433,6 @@ class DVCFileSystem(FileSystem):
return config
@functools.cached_property
# pylint: disable-next=invalid-overridden-method
def fs(self) -> "_DVCFileSystem":
return _DVCFileSystem(**self.fs_args)
@@ -452,7 +444,7 @@ class DVCFileSystem(FileSystem):
return self.fs.isdvc(path, **kwargs)
@property
def path(self) -> Path: # pylint: disable=invalid-overridden-method
def path(self) -> Path:
return self.fs.path
@property
+3 -3
View File
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
from dvc.scm import Git
class GitFileSystem(FileSystem): # pylint:disable=abstract-method
class GitFileSystem(FileSystem):
"""Proxies the repo file access methods to Git objects"""
protocol = "local"
@@ -39,7 +39,7 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
)
@functools.cached_property
def fs( # pylint: disable=invalid-overridden-method
def fs(
self,
) -> "FsspecGitFileSystem":
from scmrepo.fs import GitFileSystem as FsspecGitFileSystem
@@ -47,7 +47,7 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
return FsspecGitFileSystem(**self.fs_args)
@functools.cached_property
def path(self): # pylint: disable=invalid-overridden-method
def path(self):
return self.fs.path
@property

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