Compare commits

...
60 Commits
Author SHA1 Message Date
Ruslan Kuprieiev f2e479422f index: mark remote as read_only for imports
Fixes #10206
2023-12-28 08:08:45 +02:00
Ruslan Kuprieiev 73820cc596 push: don't try to update meta for non versioned remotes
Per https://github.com/iterative/dvc/pull/10191#issuecomment-1870716262
2023-12-28 06:11:16 +02:00
skshetry e7b695dbef fix partial fetch (#10205)
Closes #10199. Bumps dvc-data to >=3.4.
2023-12-27 19:05:33 +05:45
Guido Diepenandskshetry 27a8222e2a webhdfs: implement support for basic auth and proxy (#10075)
* remote: add user key to config schema webhdfs

This was already supported according to documentation, but missing in
code

Fixes: 10062

* remote: add password key to support Basic Auth

* remote: add data_proxy_target for webhdfs remote

This key is needed to construct a potential rewrite dictionary in
webhdfs to deal with HDFS behind High Availability Proxy servers

* bump dvc-webhdfs

---------

Co-authored-by: skshetry <[email protected]>
2023-12-26 08:37:07 +05:45
pre-commit-ci[bot] 5850639a97 [pre-commit.ci] pre-commit autoupdate (#10200)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.8 → v0.1.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.8...v0.1.9)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-26 08:36:30 +05:45
dberenbaum f2b56bbb8a drop lock from params_show and metrics_show 2023-12-25 10:15:38 +02:00
dependabot[bot] cf8195c829 build(deps-dev): bump mypy from 1.7.1 to 1.8.0 (#10189)
Bumps [mypy](https://github.com/python/mypy) from 1.7.1 to 1.8.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.7.1...v1.8.0)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-24 15:31:31 +05:45
Ruslan Kuprieiev 402c107823 ui: push: use blank pbar for the header
Same as in `dvc fetch`.
2023-12-23 04:40:23 +02:00
Ruslan Kuprieiev b73754edd1 deps: bump dvc-data to >=3.3 2023-12-23 01:19:21 +02:00
Ruslan Kuprieiev 333f2f04f0 push: set desc explicitly
Leftover from https://github.com/iterative/dvc/pull/10190
2023-12-22 08:03:29 +02:00
Ruslan Kuprieiev 470c50a22b push: use set_size instead of set_total
From https://github.com/iterative/dvc/pull/10191
2023-12-22 07:55:52 +02:00
Ruslan Kuprieiev 01f49b7f24 push: only collect needed files when updating version_ids 2023-12-22 07:38:26 +02:00
Ruslan Kuprieiev 19f2246bee push: leave ui messages
Same as we do in pull/fetch/checkout.
2023-12-22 05:51:54 +02:00
Ruslan Kuprieiev 6a44783d72 index: load meta from outs.files 2023-12-22 05:26:18 +02:00
Ruslan Kuprieiev 98a286d88d repo: don't forget to close data_index when dropping 2023-12-22 05:26:18 +02:00
Ruslan Kuprieiev 499c2dda01 testing: don't forget to check dvc.lock for cloud versioned dirs 2023-12-21 01:05:08 +02:00
Ruslan Kuprieiev 5f2615ff53 deps: bump dvc-data to >=0.3.1 2023-12-21 01:05:08 +02:00
dberenbaum 457d615a51 gc: fix test_gc_no_dir_cache 2023-12-20 00:21:23 +02:00
dberenbaum 827f9a7f03 gc: add --skip-failed 2023-12-20 00:21:23 +02:00
pre-commit-ci[bot] 7821e126fe [pre-commit.ci] pre-commit autoupdate (#10183)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.7 → v0.1.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.7...v0.1.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-19 15:31:12 +05:45
Helio Machadoandpre-commit-ci[bot] 72ab5a12df param: optimize fix for exponential memory allocation (#10180)
* param: simplify fix for exponential memory allocation

* Apply suggestions from code review

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

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

* Remove `deepcopy` again

* Restore deepcopy

* [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-18 08:37:31 +05:45
Helio Machadoandpre-commit-ci[bot] abcd70b9cf param: fix exponential memory allocation (#10178)
* param: fix exponential memory allocation

* [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-16 20:08:44 +05:45
Ruslan Kuprieiev 3e4e143bee deps: update dvc-data to >=3 2023-12-15 08:02:40 +02:00
dependabot[bot] 766b43bc2d build(deps): bump actions/upload-artifact from 3 to 4 (#10170)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-15 10:11:19 +05:45
dependabot[bot] 92c66b2331 build(deps): bump actions/download-artifact from 3 to 4 (#10171)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-15 10:11:06 +05:45
Ruslan Kuprieiev b1211366cd dvcfs: make repo creation lazy 2023-12-14 21:43:24 +02:00
Ruslan Kuprieiev 92823f1d84 tests: repro: don't depend on particular order 2023-12-14 21:43:24 +02:00
dependabot[bot] f3fa67440d build(deps): bump github/codeql-action from 2 to 3
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-12-14 06:04:54 +02:00
Ruslan Kuprieiev 3275f1ebf5 deps: bump dvc-data to >= 2.25.0 2023-12-14 05:04:36 +02:00
Ruslan Kuprieiev 4392af6d95 cli: debug: finalize profiler reports on errors
Even if a command failed, it might still be useful to look at the profiler report.
2023-12-13 20:34:30 +02:00
pre-commit-ci[bot] a02c5a7405 [pre-commit.ci] pre-commit autoupdate (#10153)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.1.7)

* [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-13 19:56:38 +05:45
skshetry dec1ac7346 deps: update dvc-data to >=2.24 (#10158) 2023-12-13 16:55:45 +05:45
Ruslan Kuprieiev 8ccae0a086 dvcfs: implement basic du 2023-12-12 23:56:19 +02:00
skshetry 3c31234151 unprotect: add progress bar (#10152)
* unprotect: add progress bar

* Update pyproject.toml
2023-12-12 09:47:35 +05:45
skshetry 77b451d118 database: rename Adapter to Client (#10151) 2023-12-09 11:28:47 +05:45
Ruslan Kuprieiev 1d47b03cd5 bench: mark test_get/import as flaky (#10148)
Sometimes seeing those error out in 2.x and early 3.x with:

```
ERROR: unexpected error - [Errno 95] no more link types left to try out: [Errno 95] Operation not supported
```

It seems like we've fixed it at some point, but I was not able to quickly find a link.
2023-12-08 17:15:57 +02:00
Dave Berenbaum 7c898cfad7 exp: ignore workspace errors during push/pull (#10128)
* exp: ignore workspace errors during push/pull

* refactor fetch revs warning

* mention failed revs are skipped

* add workspace arg to brancher

* pass workspace=False only for dvc exp push/pull
2023-12-07 13:46:18 +02:00
dependabot[bot] 1f07023d17 build(deps): bump actions/setup-python from 4 to 5 (#10145)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-07 09:06:35 +05:45
Ruslan Kuprieiev 1e6b0242aa deps: bump dvc-data to 2.22.6 (#10144) 2023-12-06 18:41:52 +02:00
skshetry 3c1e9ee9be run test on 3.12 (#10140) 2023-12-06 21:47:50 +05:45
Ruslan Kuprieiev 9622157565 deps: bump dvc-data to >=2.22.5 (#10141) 2023-12-06 13:32:37 +02:00
Ruslan Kuprieiev 6ff4342f02 deps: bump dvc-data to >=2.22.4 (#10139)
* deps: bump dvc-data to >=2.22.4

* Update test_odb.py
2023-12-06 11:51:05 +02:00
Matt Seddon 5bb72b36cb plots: standardise across DVC/Studio/VS Code (#9931)
* standardise plot data output

* add data required for new anchors to renderer properties

* feed dvc-render updates into API output

* update --json --split api with new data types (send split anchor definitions as strings)

* accomodate zoom and pan anchor

* switch anchor_revs to revs_with_datapoints

* accomodate height and width anchors

* drop terrible idea of holding all data as strings

* fix plot having multiple x fields

* move inferred properties off vega converter class
2023-12-06 11:25:13 +11:00
Ruslan Kuprieiev 1aed230cd0 deps: bump dvc-oss to >=2.22.0
First release with unpinned fsspec version.
2023-12-05 18:48:36 +02:00
pre-commit-ci[bot] c50b2e1e71 [pre-commit.ci] pre-commit autoupdate (#10100)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.5 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.5...v0.1.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-05 21:36:22 +05:45
dependabot[bot] 34bc410075 build(deps-dev): bump mypy from 1.7.0 to 1.7.1 (#10108)
Bumps [mypy](https://github.com/python/mypy) from 1.7.0 to 1.7.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.7.0...v1.7.1)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 21:23:17 +05:45
47ff40da0c studio: Add Studio authentication to DVC (#10074)
* Add Studio authentication to DVC

Implemented a new feature to authenticate DVC with Iterative Studio. This includes generating an access token, starting device login, checking if user code is authorized, and handling any request exceptions. Furthermore, the commit creates a new authentication command for DVC. This feature was added to allow DVC users to authenticate with Studio, which is necessary for functionality such as sharing live experiments and notifying Studio about pushed experiments.

* Add authentication validation and error handling to DVC Studio

This commit introduces validation for scopes used in DVC Studio's authentication process. If an unsupported scope is passed, the user will receive an error message listing the invalid scopes and the program will return an error status. Additionally, this commit changes the program's exit behavior when device code authorization fails or authentication process is successful, replacing 'sys.exit' with appropriate returned values.

* Add logout functionality to DVC Studio commands

This commit introduces the logout command for DVC Studio. It allows users to manually log out from DVC Studio via the command line interface. The logout command only removes the user's token if it exists, otherwise, it will print an error message and return an error status. This adds an extra layer of control for the users and enhances the security for the usage of DVC Studio.

* Improve User Authentication process for DVC Studio

Implemented a new 'token' command facilitating users to check their authentication token while operating with DVC Studio. This implementation enhances user experience by providing the ability to validate logged-in status. If not logged in, it rightly prompts an error, ensuring secure access.

* Add unit tests for auth commands

Unit tests are added for the authentication commands used in DVC Studio. The modifications include tests for login with invalid scope, standard login, logout, and token commands. The tests verify successful execution and appropriate responses for different user authentication scenarios, thereby increasing the robustness and reliability of the auth module.

* Update unit tests for auth commands

Unit tests for DVC Studio authentication commands have been updated. These checks verify successful execution and appropriate responses under various authentication scenarios, including login with invalid scope, standard login, logout, and token commands in order to enhance the robustness and reliability of the auth module.

* Add unit tests and modify authentication function in studio utils

This commit adds several new unit tests in test_studio.py. Tests for start_device_login and check_token_authorization functionality are included to verify the correct behavior of these methods. Moreover, it also modifies the authentication function in studio.py for improving the robustness. Changes in the function aimed to handle unexpected error scenarios and make the code more resilient.

* Refactor mock_response usage in tests and add test_auth.py

This commit refactors the way mock responses are used in test_studio.py to follow DRY principles, changing the private _mock_response to a public reusable function. It also adds a new file, test_auth.py, that provides functional tests for authorisation methods, ensuring their functionality. The patches in test_auth.py helps test different cases like expired authorization and successful authorization. Furthermore, test_auth.py also tests if correct configuration is set on successful authorization. Modifications on the method test_auth_login_arguments in test_auth.py were made to refine test cases, thus making the tests more precise and straightforward.

* Refactor error handling in `dvc/utils/studio.py`

Refactored error handling by removing redundant code blocks and introducing more detailed logging for request exceptions. This makes the failure cases handled explicitly and streamlines the traceability. Also, tests for authorization have been refined and more specific cases like 'expired authorization' have been included.

* Replace `auth` module with `studio` in DVC

Renamed all instances of `auth` to `studio`, as we are moving the authentication process into a new module `studio`. This includes changes to command names, classes, and test file names to reflect the updated module. It improves the organization of the authentication features related to DVC Studio, separating them from other types of authorization.

* Remove device login methods in utils/studio

Removed methods related to device login and token authorization in `utils/studio.py` and its corresponding test cases. These functionalities have been moved to the `dvc-studio-client` module to decouple the login process and make the code maintainable. Updated the dependencies in `pyproject.toml` to use the new `dvc-studio-client` module. Also, updated the command/studio to use the `dvc-studio-client` module for auth functionalities.

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

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

* Update dvc-studio-client dependency in pyproject.toml

The "dvc-studio-client" dependency in pyproject.toml is updated from a specific git commit to v0.16.0, removing the TODO comment. This change was made because the necessary features from the PR linked in the TODO comment have been merged and released in v0.16.0, making the git commit unnecessary. Now the code is cleaner and easier to track with versioned dependency.

* Update DVC Studio components and simplify authentication process

This commit modifies several parts of the ‘dvc/commands/studio.py’ and updates DVC Studio Client's version in 'pyproject.toml' for compatibility with the changes.

Primarily, the authentication process has been simplified. The unnecessary generation of a random token name is removed while initiating authorization in the run method of CmdStudioLogin class, enhancing the code quality.

Changes made in 'dvc/commands/studio.py' are mainly restructuring the code to adapt to the newer version of DVC Studio Client, including changes to the scopes and methods used from this client.

The corresponding changes were also reflected in unit tests. The test files 'tests/func/test_studio.py' and 'tests/unit/command/test_studio.py' are updated in accordance with the approach now used in 'dvc/commands/studio.py'.

* Remove unnecessary tests and update StudioClient methods

Removed functional test file `test_studio.py`from DVC to move it to studio client. The new mock response no longer needs a full testing file, simplifying our test environment.

In `dvc/commands/studio.py`, adjusted function imports to align with new StudioClient function names, which better indicate their purpose: `initiate_authorization` changed to `get_access_token`.

Additionally, `DEFAULT_SCOPES` was removed from both `dvc/commands/studio.py` and `tests/unit/command/test_studio.py`, simplifying the token acquisition process and removing unnecessary global variable. The `--scopes` command, if needed, can be passed directly without requiring a default fallback.

The corresponding changes were reflected in unit tests : the mock functions in `tests/unit/command/test_studio.py` were changed to align with the new authentication method, and updated the usage of the `get_access_token` function.

* Update token success message in dvc/commands/studio.py

This commit replaces f-string (formatted string literal) with a regular string for the success message after token authentication. The change was necessary as the variable `token_name` is already being passed as a formatted string, thus no need for f-string in the success message sentence. This makes code cleaner and more consistent.

* Refactor tests in `test_studio.py` for better readability

* Updated terminology in `studio.py` and `test_studio.py`

Changes terminology from 'authorize' to 'authenticate' to better align with common practices and improve understanding. Also fixed the `get_access_token` function in 'test_studio.py' where 'AuthorizationExpired' was replaced with 'AuthenticationExpired' for better error handling.

* Change 'dvc' to 'DVC' in `studio.py`

Updates all instances of 'dvc' to 'DVC' in `studio.py` for enhanced consistency with the rest of the project. This change aims to standardize the usage of the term across all project files.

* Improve Studio commands information clarity

Updated descriptions in various commands on 'dvc/commands/studio.py' focused on improving clarity and context. Authentication message, command helper, and descriptions have been expanded to provide better understanding. Also, 'dvc' has been changed to 'DVC' in 'studio.py' for consistency with the rest of the project files.

* Not relevant anymore

* Change argument name in studio login

Renamed the argument from "--use-device-code" to "--no-open" in the login function under the Studio authentication. The option's name was changed to make it more intuitive and better align with its actual effect, avoiding potential user confusion. Corresponding changes have also been made in unit tests.

* Modify flag for Studio login function

Updated the argument name from "-d" to "-o" in `studio.py` for the Studio login function. The change makes the argument's intention clearer for users. This is significant in improving user interaction with the feature and mitigating potential user misinterpretation.

* Apply suggestions from code review

---------

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

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