Compare commits

...
54 Commits
Author SHA1 Message Date
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
Ruslan Kuprieiev ffa630e8d5 push: handle missing/failed version_aware file
Previously we used recorded index which had stub entries
for missing/failed outputs, but now we actually collect
everything ourselves and those are missing.
2023-11-28 18:52:46 +02:00
Ruslan Kuprieiev 1e9b60dc48 deps: bump dvc-data to >=2.22.1 2023-11-27 16:17:42 +02:00
skshetry 83dd2b2a7b test: import-db: test for external model (#10112)
* test: import-db: test for external model

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

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

* force virtualenv

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

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

---------

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

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

* generate .dvc file

* update

* fix tests

* pin dbt-fal

* try to run on ci

* use os.chdir

* fix

* revert

* require dbt-core>=1.5

* add db config schema

* remove platformdirs constraints

* use logger.getChild

* improve error messaging

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

* use feature flag for db config

* hide command from dvc commands list

* refactor dependency._get

* remove todo

* refactor dependency._get

* fix dbt installation check
2023-11-21 19:38:50 +05:45
Ruslan Kuprieiev 2b3b15f767 deps: bump dvc-data to 2.22.0
Related https://github.com/iterative/dvc/issues/10059
2023-11-17 20:01:57 +02:00
Ruslan Kuprieiev 8c9358957c push: fix cloud versioning 2023-11-15 16:28:17 +02:00
Ivan Shcheklein 86dd0785a0 add tests for the index param keys 2023-11-14 13:21:44 +02:00
Ivan Shcheklein a8aec51c35 add params collection to index 2023-11-14 13:21:44 +02:00
118 changed files with 3163 additions and 907 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.8'
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):
+6
View File
@@ -19,6 +19,7 @@ from dvc.commands import (
data_sync,
destroy,
diff,
du,
experiments,
freeze,
gc,
@@ -26,6 +27,7 @@ from dvc.commands import (
get_url,
git_hook,
imp,
imp_db,
imp_url,
init,
install,
@@ -41,6 +43,7 @@ from dvc.commands import (
repro,
root,
stage,
studio,
unprotect,
update,
version,
@@ -65,6 +68,7 @@ COMMANDS = [
data_sync,
gc,
imp,
imp_db,
imp_url,
config,
checkout,
@@ -91,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)
+167
View File
@@ -0,0 +1,167 @@
import argparse
from funcy import compact, merge
from dvc.cli import completion
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
from dvc.log import logger
from dvc.ui import ui
logger = logger.getChild(__name__)
class CmdTestDb(CmdBase):
def run(self):
from dvc.database import get_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):
raise argparse.ArgumentTypeError("Either of --sql or --model is required.")
self.repo.imp_db(
url=self.args.url,
rev=self.args.rev,
project_dir=self.args.project_dir,
sql=self.args.sql,
model=self.args.model,
profile=self.args.profile,
target=self.args.target,
output_format=self.args.output_format,
out=self.args.out,
force=self.args.force,
connection=self.args.connection,
)
return 0
def add_parser(subparsers, parent_parser):
IMPORT_HELP = (
"Download file or directory tracked by DVC or by Git "
"into the workspace, and track it."
)
import_parser = subparsers.add_parser(
"import-db",
parents=[parent_parser],
description=append_doc_link(IMPORT_HELP, "import"),
add_help=False,
)
import_parser.add_argument(
"--url", help="Location of DVC or Git repository to download from"
)
import_parser.add_argument(
"--rev",
nargs="?",
help="Git revision (e.g. SHA, branch, tag)",
metavar="<commit>",
)
import_parser.add_argument(
"--project-dir", nargs="?", help="Subdirectory to the dbt project location"
)
group = import_parser.add_mutually_exclusive_group()
group.add_argument(
"--sql",
help="SQL query",
)
group.add_argument(
"--model",
help="Model name to download",
)
import_parser.add_argument("--profile", help="Profile to use")
import_parser.add_argument("--target", help="Target to use")
import_parser.add_argument(
"--output-format",
default="csv",
const="csv",
nargs="?",
choices=["csv", "json"],
help="Export format",
)
import_parser.add_argument(
"-o",
"--out",
nargs="?",
help="Destination path to download files to",
metavar="<path>",
).complete = completion.FILE
import_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Override destination file or folder if exists.",
)
import_parser.add_argument(
"--conn",
dest="connection",
nargs="?",
help="Database connection to use, needs to be set in config",
)
import_parser.set_defaults(func=CmdImportDb)
TEST_DB_HELP = "Test the database connection"
test_db_parser = subparsers.add_parser(
"test-db",
parents=[parent_parser],
description=append_doc_link(TEST_DB_HELP, "test-db"),
add_help=False,
)
test_db_parser.add_argument("--conn", dest="connection")
test_db_parser.add_argument("--url")
test_db_parser.add_argument("--password")
test_db_parser.add_argument("--username")
test_db_parser.add_argument("--profile")
test_db_parser.add_argument("--target")
test_db_parser.set_defaults(func=CmdTestDb)
+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)
+9
View File
@@ -318,6 +318,8 @@ SCHEMA = {
"feature": FeatureSchema(
{
Optional("machine", default=False): Bool,
"dbt_profile": str,
"dbt_target": str,
},
),
"plots": {
@@ -350,4 +352,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()
+17 -6
View File
@@ -4,6 +4,7 @@ from typing import Any, Dict, List, Mapping, Set
from dvc.output import ARTIFACT_SCHEMA, DIR_FILES_SCHEMA, Output
from .base import Dependency
from .db import DB_SCHEMA, PARAM_DB, DbDependency, DbtDependency
from .param import ParamsDependency
from .repo import RepoDependency
@@ -14,20 +15,28 @@ from .repo import RepoDependency
SCHEMA: Mapping[str, Any] = {
**ARTIFACT_SCHEMA,
**RepoDependency.REPO_SCHEMA,
**DB_SCHEMA,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
Output.PARAM_FS_CONFIG: dict,
}
def _get(stage, p, info, **kwargs):
if info and info.get(RepoDependency.PARAM_REPO):
repo = info.pop(RepoDependency.PARAM_REPO)
return RepoDependency(repo, stage, p, info)
d = info or {}
db = d.get(PARAM_DB, {})
params = d.pop(ParamsDependency.PARAM_PARAMS, None)
repo = d.pop(RepoDependency.PARAM_REPO, None)
if info and info.get(ParamsDependency.PARAM_PARAMS):
params = info.pop(ParamsDependency.PARAM_PARAMS)
if params:
return ParamsDependency(stage, p, params)
if DbDependency.PARAM_QUERY in db:
return DbDependency(stage, info)
if db:
return DbtDependency(repo, stage, info)
assert p
if repo:
return RepoDependency(repo, stage, p, info)
return Dependency(stage, p, info, **kwargs)
@@ -44,9 +53,11 @@ def loadd_from(stage, d_list):
return ret
def loads_from(stage, s_list, erepo=None, fs_config=None):
def loads_from(stage, s_list, erepo=None, fs_config=None, db=None):
assert isinstance(s_list, list)
info = {RepoDependency.PARAM_REPO: erepo} if erepo else {}
if db:
info.update({"db": db})
return [
_get(
stage,
+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:
+324
View File
@@ -0,0 +1,324 @@
import os
from contextlib import contextmanager, nullcontext
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Union
from funcy import compact, log_durations
from dvc.exceptions import DvcException
from dvc.log import logger
from dvc.scm import SCM
from .base import Dependency
if TYPE_CHECKING:
from rich.status import Status
from dvc.output import Output
from dvc.stage import Stage
logger = logger.getChild(__name__)
PARAM_DB = "db"
PARAM_PROFILE = "profile"
PARAM_FILE_FORMAT = "file_format"
def _get_dbt_config(config: Dict) -> Dict:
conf = config.get("feature", {})
pref = "dbt_"
return {k.lstrip(pref): v for k, v in conf.items() if k.startswith(pref)}
@contextmanager
def log_status(
msg, status: Optional["Status"] = None, log=logger.debug
) -> Iterator["Status"]:
from funcy import log_durations
from dvc.ui import ui
with log_durations(log, msg), status or ui.status(msg) as st:
st.update(msg)
yield st
@contextmanager
def chdir(path):
wdir = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(wdir)
@contextmanager
def download_progress(to: "Output") -> Iterator[Callable[[int], Any]]:
from dvc.ui import ui
from dvc.ui._rich_progress import DbDownloadProgress
with log_durations(logger.debug, f"Saving to {to}"), DbDownloadProgress(
console=ui.error_console,
) as progress:
task = progress.add_task("Saving", total=None, output=to)
yield lambda n: progress.advance(task, advance=n)
progress.update(task, description="Saved", total=0)
class AbstractDependency(Dependency):
"""Dependency without workspace/fs/fs_path"""
def __init__(self, stage: "Stage", info: Dict[str, Any], *args, **kwargs):
self.repo = stage.repo
self.stage = stage
self.fs = None
self.fs_path = None
self.def_path = None # type: ignore[assignment]
self.info = info or {}
class DbDependency(AbstractDependency):
PARAM_QUERY = "query"
PARAM_CONNECTION = "connection"
QUERY_SCHEMA = {PARAM_QUERY: str, PARAM_CONNECTION: str}
def __init__(self, stage: "Stage", info, *args, **kwargs):
super().__init__(stage, info, *args, **kwargs)
self.target = None
def __repr__(self):
return "{}:{}".format(
self.__class__.__name__, "".join(f"{k}=={v}" for k, v in self.info.items())
)
def __str__(self):
from dvc.utils.humanize import truncate_text
db_info = self.info.get(PARAM_DB, {})
query = db_info.get(self.PARAM_QUERY, "[query]")
return truncate_text(query, 50)
def workspace_status(self):
return False # no workspace to check
def status(self):
return self.workspace_status()
def save(self):
"""nothing to save."""
def dumpd(self, **kwargs):
db_info = compact(self.info.get(PARAM_DB, {}))
return {PARAM_DB: db_info} if db_info else {}
def update(self, rev=None):
"""nothing to update."""
def download(
self,
to: "Output",
jobs: Optional[int] = None, # noqa: ARG002
file_format: Optional[str] = None,
**kwargs: Any,
) -> None:
from dvc.database import export, get_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")
dbt_config = _get_dbt_config(self.repo.config)
profile = db_info.get(PARAM_PROFILE) or dbt_config.get(PARAM_PROFILE)
target = self.target or dbt_config.get("target")
connection = db_info.get(self.PARAM_CONNECTION)
db_config = self.repo.config.get("db", {})
config = db_config.get(connection)
if connection and not config:
raise DvcException(f"connection {connection} not found in config")
project_dir = self.repo.root_dir
with get_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):
PARAM_MODEL = "model"
PARAM_VERSION = "version"
PARAM_PROJECT_DIR = "project_dir"
DBT_SCHEMA = {
PARAM_MODEL: str,
PARAM_VERSION: str,
PARAM_PROJECT_DIR: str,
}
def __init__(self, def_repo: Dict[str, Any], stage: "Stage", info, *args, **kwargs):
self.def_repo = def_repo or {}
self.target = None
super().__init__(stage, info, *args, **kwargs)
def __repr__(self):
return "{}:{}".format(
self.__class__.__name__,
"".join(f"{k}=={v}" for k, v in {**self.def_repo, **self.info}.items()),
)
def __str__(self):
from .repo import RepoDependency
repo = self.def_repo.get(RepoDependency.PARAM_URL)
rev = self.def_repo.get(RepoDependency.PARAM_REV)
db_info = self.info.get(PARAM_DB, {})
db = db_info.get(self.PARAM_MODEL, "")
project_dir = db_info.get(self.PARAM_PROJECT_DIR, "")
repo_info = ""
if repo:
repo_info += repo
if rev:
repo_info += f"@{rev}"
if project_dir:
repo_info += f":/{project_dir}"
return db + (f"({repo_info})" if repo_info else "")
@property
def locked_rev(self):
from .repo import RepoDependency
return self.def_repo.get(RepoDependency.PARAM_REV_LOCK)
@property
def rev(self):
from .repo import RepoDependency
return self.def_repo.get(RepoDependency.PARAM_REV)
def workspace_status(self):
if not self.def_repo:
return
current = self._get_clone(self.locked_rev or self.rev).get_rev()
updated = self._get_clone(self.rev).get_rev()
if current != updated:
return {str(self): "update available"}
return {}
def status(self):
return self.workspace_status()
def save(self):
from .repo import RepoDependency
if not self.def_repo:
return
rev = self._get_clone(self.locked_rev or self.rev).get_rev()
if self.def_repo.get(RepoDependency.PARAM_REV_LOCK) is None:
self.def_repo[RepoDependency.PARAM_REV_LOCK] = rev
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
from .repo import RepoDependency
def_repo = {}
if self.def_repo:
def_repo = RepoDependency._dump_def_repo(self.def_repo)
db_info = compact(self.info.get(PARAM_DB, {}))
return compact({RepoDependency.PARAM_REPO: def_repo, PARAM_DB: db_info})
def _get_clone(self, rev):
from dvc.repo.open_repo import _cached_clone
from .repo import RepoDependency
url = self.def_repo.get(RepoDependency.PARAM_URL)
repo_root = self.repo.root_dir if self.repo else os.getcwd()
return SCM(_cached_clone(url, rev) if url else repo_root)
def update(self, rev=None):
from .repo import RepoDependency
if not self.def_repo:
return
if rev:
self.def_repo[RepoDependency.PARAM_REV] = rev
else:
rev = self.rev
self.def_repo[RepoDependency.PARAM_REV_LOCK] = self._get_clone(rev).get_rev()
def download(
self,
to: "Output",
jobs: Optional[int] = None, # noqa: ARG002
file_format: Optional[str] = None,
) -> None:
from .repo import RepoDependency
project_dir = self.info.get(PARAM_DB, {}).get(self.PARAM_PROJECT_DIR, "")
if self.def_repo:
ctx = repo = self._get_clone(self.locked_rev or self.rev)
self.def_repo[RepoDependency.PARAM_REV_LOCK] = repo.get_rev()
root = wdir = repo.root_dir
else:
ctx = nullcontext()
root = self.repo.root_dir
wdir = self.stage.wdir
project_path = os.path.join(wdir, project_dir) if project_dir else root
with ctx, chdir(project_path):
self._download_db(to, file_format=file_format)
def _download_db(
self,
to: "Output",
version: Optional[int] = None,
file_format: Optional[str] = None,
) -> None:
from dvc.database import export, get_model
db_info = self.info.get(PARAM_DB, {})
model = db_info.get(self.PARAM_MODEL)
version = version or db_info.get(self.PARAM_VERSION)
file_format = file_format or db_info.get(PARAM_FILE_FORMAT) or "csv"
assert file_format
if not model:
raise DvcException("Cannot download, no model specified")
dbt_config = _get_dbt_config(self.repo.config)
profile = db_info.get(PARAM_PROFILE) or dbt_config.get(PARAM_PROFILE)
target = self.target or db_info.get("target") or dbt_config.get("target")
with log_status("Downloading model"):
serializer = get_model(
model, version=version, profile=profile, target=target
)
# NOTE: we keep everything in memory, and then export it out later.
with download_progress(to) as progress:
export(serializer, to.fs_path, format=file_format, progress=progress)
DB_SCHEMA = {
PARAM_DB: {
PARAM_PROFILE: str,
PARAM_FILE_FORMAT: str,
**DbDependency.QUERY_SCHEMA,
**DbtDependency.DBT_SCHEMA,
},
}
+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
+14 -11
View File
@@ -64,28 +64,31 @@ class RepoDependency(Dependency):
if self.def_repo.get(self.PARAM_REV_LOCK) is None:
self.def_repo[self.PARAM_REV_LOCK] = rev
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
repo = {self.PARAM_URL: self.def_repo[self.PARAM_URL]}
@classmethod
def _dump_def_repo(cls, def_repo) -> Dict[str, str]:
repo = {cls.PARAM_URL: def_repo[cls.PARAM_URL]}
rev = self.def_repo.get(self.PARAM_REV)
rev = def_repo.get(cls.PARAM_REV)
if rev:
repo[self.PARAM_REV] = self.def_repo[self.PARAM_REV]
repo[cls.PARAM_REV] = def_repo[cls.PARAM_REV]
rev_lock = self.def_repo.get(self.PARAM_REV_LOCK)
rev_lock = def_repo.get(cls.PARAM_REV_LOCK)
if rev_lock:
repo[self.PARAM_REV_LOCK] = rev_lock
repo[cls.PARAM_REV_LOCK] = rev_lock
config = self.def_repo.get(self.PARAM_CONFIG)
config = def_repo.get(cls.PARAM_CONFIG)
if config:
repo[self.PARAM_CONFIG] = config
repo[cls.PARAM_CONFIG] = config
remote = self.def_repo.get(self.PARAM_REMOTE)
remote = def_repo.get(cls.PARAM_REMOTE)
if remote:
repo[self.PARAM_REMOTE] = remote
repo[cls.PARAM_REMOTE] = remote
return repo
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
return {
self.PARAM_PATH: self.def_path,
self.PARAM_REPO: repo,
self.PARAM_REPO: self._dump_def_repo(self.def_repo),
}
def update(self, rev: Optional[str] = None):
+1 -1
View File
@@ -15,7 +15,7 @@ def system_config_dir():
def global_config_dir():
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.user_config_dir(
return os.getenv(env.DVC_GLOBAL_CONFIG_DIR) or platformdirs.user_config_dir(
APPNAME, APPAUTHOR
)
+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
+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(
+31 -34
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:
@@ -1523,7 +1520,7 @@ CLOUD_SCHEMA = vol.All({str: {**META_SCHEMA, **CHECKSUMS_SCHEMA}}, vol.Length(ma
ARTIFACT_SCHEMA: Dict[Any, Any] = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
vol.Required(Output.PARAM_PATH): str,
Output.PARAM_PATH: str,
Output.PARAM_PERSIST: bool,
Output.PARAM_CLOUD: CLOUD_SCHEMA,
Output.PARAM_HASH: str,
+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
+23 -17
View File
@@ -70,12 +70,14 @@ 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]
from dvc.repo.get import get as _get # type: ignore[misc]
from dvc.repo.get_url import get_url as _get_url # type: ignore[misc]
from dvc.repo.imp import imp # type: ignore[misc]
from dvc.repo.imp_db import imp_db # type: ignore[misc]
from dvc.repo.imp_url import imp_url # type: ignore[misc]
from dvc.repo.install import install # type: ignore[misc]
from dvc.repo.ls import ls as _ls # type: ignore[misc]
@@ -92,6 +94,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)
@@ -111,7 +114,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
@@ -204,7 +207,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,
@@ -248,7 +251,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:
@@ -260,10 +263,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,
@@ -392,7 +395,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]
@@ -408,18 +411,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
@@ -437,7 +440,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":
@@ -446,7 +449,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"]]
@@ -536,19 +542,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
@@ -560,7 +566,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
@@ -607,10 +613,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):
+25 -4
View File
@@ -1,4 +1,4 @@
from typing import List, Tuple
from typing import TYPE_CHECKING, List, Tuple
from dvc.exceptions import DownloadError
from dvc.log import logger
@@ -7,6 +7,10 @@ from dvc_data.index import DataIndex, FileStorage
from . import locked
if TYPE_CHECKING:
from dvc.output import Output
from dvc.stage import Stage
logger = logger.getChild(__name__)
@@ -28,10 +32,12 @@ def _collect_indexes( # noqa: PLR0913
recursive=False,
all_commits=False,
revs=None,
workspace=True,
max_size=None,
types=None,
config=None,
onerror=None,
push=False,
):
indexes = {}
collection_exc = None
@@ -42,11 +48,22 @@ def _collect_indexes( # noqa: PLR0913
core["remote"] = remote
config["core"] = core
def stage_filter(stage: "Stage") -> bool:
if push and stage.is_repo_import:
return False
return True
def outs_filter(out: "Output") -> bool:
if push and not out.can_push:
return False
return True
for rev in repo.brancher(
revs=revs,
all_branches=all_branches,
all_tags=all_tags,
all_commits=all_commits,
workspace=workspace,
):
try:
repo.config.merge(config)
@@ -57,16 +74,18 @@ def _collect_indexes( # noqa: PLR0913
recursive=recursive,
max_size=max_size,
types=types,
stage_filter=stage_filter,
outs_filter=outs_filter,
)
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
@@ -87,6 +106,7 @@ def fetch( # noqa: PLR0913
all_commits=False,
run_cache=False,
revs=None,
workspace=True,
max_size=None,
types=None,
config=None,
@@ -131,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,
@@ -198,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()
+7 -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))
@@ -135,9 +135,11 @@ def build_graph(stages, outs_trie=None):
for stage in stages:
if stage.is_repo_import:
continue
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))
@@ -160,8 +162,10 @@ def build_outs_graph(graph, outs_trie):
for stage in graph.nodes():
if stage.is_repo_import:
continue
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))
+81
View File
@@ -0,0 +1,81 @@
from typing import TYPE_CHECKING, Any, Dict, Optional
from dvc.exceptions import OutputDuplicationError
from dvc.repo.scm_context import scm_context
from dvc.ui import ui
from dvc.utils import resolve_output, resolve_paths
if TYPE_CHECKING:
from . import Repo
from . import locked
@locked
@scm_context
def imp_db( # noqa: PLR0913
self: "Repo",
url: Optional[str] = None,
rev: Optional[str] = None,
project_dir: Optional[str] = None,
sql: Optional[str] = None,
model: Optional[str] = None,
version: Optional[int] = None,
frozen: bool = True,
profile: Optional[str] = None,
target: Optional[str] = None,
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(
"The functionality may change or break without notice, "
"which could lead to unexpected behavior."
)
erepo = None
if model and url:
erepo = {"url": url}
if rev:
erepo["rev"] = rev
assert output_format in ("csv", "json")
db: Dict[str, Any] = {"file_format": output_format}
if profile:
db["profile"] = profile
if model:
out = out or f"{model}.{output_format}"
db.update({"model": model, "version": version, "project_dir": project_dir})
else:
out = out or f"results.{output_format}"
db.update({"query": sql, "connection": connection})
out = resolve_output(url or ".", out, force=force)
path, wdir, out = resolve_paths(self, out, always_local=True)
stage = self.stage.create(
single_stage=True,
validate=False,
fname=path,
wdir=wdir,
deps=[url],
outs=[out],
erepo=erepo,
fs_config=None,
db=db,
)
stage.deps[0].target = target
try:
self.check_graph(stages={stage})
except OutputDuplicationError as exc:
raise OutputDuplicationError( # noqa: B904
exc.output, set(exc.stages) - {stage}
)
stage.run()
stage.frozen = frozen
stage.dump()
return stage
+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:
+41 -12
View File
@@ -2,6 +2,7 @@ import logging
import time
from collections import defaultdict
from functools import partial
from itertools import chain
from typing import (
TYPE_CHECKING,
Any,
@@ -19,6 +20,7 @@ from typing import (
from funcy.debug import format_time
from dvc.dependency import ParamsDependency
from dvc.fs import LocalFileSystem
from dvc.fs.callbacks import DEFAULT_CALLBACK
from dvc.log import logger
@@ -28,7 +30,7 @@ if TYPE_CHECKING:
from networkx import DiGraph
from pygtrie import Trie
from dvc.dependency import Dependency, ParamsDependency
from dvc.dependency import Dependency
from dvc.fs.callbacks import Callback
from dvc.output import Output
from dvc.repo import Repo
@@ -95,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:
@@ -133,7 +135,11 @@ def _load_data_from_outs(index, prefix, outs):
hash_info=out.hash_info,
)
if out.stage.is_import and not out.stage.is_repo_import:
if (
out.stage.is_import
and not out.stage.is_repo_import
and not out.stage.is_db_import
):
dep = out.stage.deps[0]
entry.meta = dep.meta
if out.hash_info:
@@ -181,6 +187,9 @@ def _load_storage_from_out(storage_map, key, out):
except NoRemoteError:
pass
if out.stage.is_db_import:
return
if out.stage.is_import:
dep = out.stage.deps[0]
if not out.hash_info:
@@ -192,7 +201,7 @@ 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)
),
)
@@ -405,7 +414,25 @@ 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)
@cached_property
def param_keys(self) -> Dict[str, Set["DataIndexKey"]]:
from .params.show import _collect_top_level_params
by_workspace: Dict[str, Set["DataIndexKey"]] = defaultdict(set)
by_workspace["repo"] = set()
param_paths = _collect_top_level_params(self.repo)
default_file: str = ParamsDependency.DEFAULT_PARAMS_FILE
if self.repo.fs.exists(f"{self.repo.fs.root_marker}{default_file}"):
param_paths = chain(param_paths, [default_file])
for path in param_paths:
key = self.repo.fs.relparts(path, self.repo.root_dir)
by_workspace["repo"].add(key)
return dict(by_workspace)
@@ -424,7 +451,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)
@@ -550,6 +577,8 @@ class Index:
keys = self.plot_keys
elif typ == "metrics":
keys = self.metric_keys
elif typ == "params":
keys = self.param_keys
else:
raise ValueError(f"unsupported type {typ}")
@@ -672,8 +701,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)
@@ -689,8 +718,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)
@@ -753,7 +782,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])
@@ -798,7 +827,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 -7
View File
@@ -35,10 +35,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 +101,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 +128,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 +156,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
+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 -6
View File
@@ -21,10 +21,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 +55,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 +93,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 +138,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
+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,
+9
View File
@@ -46,6 +46,7 @@ def push( # noqa: PLR0913
all_commits=False,
run_cache=False,
revs=None,
workspace=True,
glob=False,
):
from fsspec.utils import tokenize
@@ -87,6 +88,8 @@ def push( # noqa: PLR0913
recursive=recursive,
all_commits=all_commits,
revs=revs,
workspace=workspace,
push=True,
)
cache_key = (
@@ -107,6 +110,7 @@ def push( # noqa: PLR0913
push=True,
)
push_transferred, push_failed = 0, 0
try:
with ui.progress(
desc="Pushing",
@@ -131,6 +135,11 @@ def push( # noqa: PLR0913
for fs_index in data:
fs_index.close()
if push_transferred:
# NOTE: dropping cached index to force reloading from newly saved
# metadata from version-aware remotes
self.drop_data_index()
transferred_count += push_transferred
failed_count += push_failed
if failed_count:
+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:
+16 -13
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
@@ -101,7 +100,7 @@ def _get_remote(
return repo.cloud.get_remote(name, command)
def _merge_push_meta(
def _merge_push_meta( # noqa: C901
out: "Output",
index: Union["DataIndex", "DataIndexView"],
remote: Optional[str] = None,
@@ -116,7 +115,11 @@ def _merge_push_meta(
from dvc_data.index.save import build_tree
_, key = out.index_key
entry = index[key]
entry = index.get(key)
if entry is None:
return
repo = out.stage.repo
if out.isdir():
old_tree = out.get_obj()
@@ -128,10 +131,11 @@ def _merge_push_meta(
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
@@ -243,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:
@@ -261,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,
@@ -352,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,
)
+22 -3
View File
@@ -105,7 +105,7 @@ def create_stage(cls: Type[_T], repo, path, **kwargs) -> _T:
fill_stage_outputs(stage, **kwargs)
check_no_externals(stage)
fill_stage_dependencies(
stage, **project(kwargs, ["deps", "erepo", "params", "fs_config"])
stage, **project(kwargs, ["deps", "erepo", "params", "fs_config", "db"])
)
check_circular_dependency(stage)
check_duplicated_arguments(stage)
@@ -281,9 +281,24 @@ class Stage(params.StageParams):
return isinstance(self.deps[0], RepoDependency)
@property
def is_db_import(self) -> bool:
if not self.is_import:
return False
from dvc.dependency import DbDependency, DbtDependency
return isinstance(self.deps[0], (DbDependency, DbtDependency))
@property
def is_versioned_import(self) -> bool:
return self.is_import and self.deps[0].fs.version_aware
from dvc.dependency import DbDependency, DbtDependency
return (
self.is_import
and not isinstance(self.deps[0], (DbDependency, DbtDependency))
and self.deps[0].fs.version_aware
)
def short_description(self) -> Optional["str"]:
desc: Optional["str"] = None
@@ -446,6 +461,9 @@ class Stage(params.StageParams):
) -> None:
if not (self.is_repo_import or self.is_import):
raise StageUpdateError(self.relpath)
# always force update DbDep/DbtDep since we don't know if it's changed
force = self.is_db_import
update_import(
self,
rev=rev,
@@ -453,6 +471,7 @@ class Stage(params.StageParams):
remote=remote,
no_download=no_download,
jobs=jobs,
force=force,
)
def reload(self) -> "Stage":
@@ -637,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 -1
View File
@@ -7,6 +7,7 @@ from funcy import decorator
def rwlocked(call, read=None, write=None):
import sys
from dvc.dependency.db import AbstractDependency
from dvc.dependency.repo import RepoDependency
from dvc.rwlock import rwlock
@@ -27,7 +28,7 @@ def rwlocked(call, read=None, write=None):
for item in getattr(stage, attr)
# There is no need to lock RepoDependency deps, as there is no
# corresponding OutputREPO, so we can't even write it.
if not isinstance(item, RepoDependency)
if not isinstance(item, (RepoDependency, AbstractDependency))
]
cmd = " ".join(sys.argv)
+9 -3
View File
@@ -18,7 +18,13 @@ def _update_import_on_remote(stage, remote, jobs):
def update_import(
stage, rev=None, to_remote=False, remote=None, no_download=None, jobs=None
stage,
rev=None,
to_remote=False,
remote=None,
no_download=None,
jobs=None,
force=False,
):
stage.deps[0].update(rev=rev)
@@ -30,7 +36,7 @@ def update_import(
if to_remote:
_update_import_on_remote(stage, remote, jobs)
else:
stage.reproduce(no_download=no_download, jobs=jobs)
stage.reproduce(no_download=no_download, jobs=jobs, force=force)
finally:
if no_download and changed:
# Avoid retaining stale information
@@ -55,7 +61,7 @@ def sync_import(
else:
stage.save_deps()
if no_download:
if stage.is_repo_import:
if stage.is_repo_import or stage.is_db_import:
stage.deps[0].update()
else:
stage.deps[0].download(
+6 -4
View File
@@ -77,12 +77,14 @@ def fill_stage_outputs(stage, **kwargs):
)
def fill_stage_dependencies(stage, deps=None, erepo=None, params=None, fs_config=None):
def fill_stage_dependencies(
stage, deps=None, erepo=None, params=None, fs_config=None, db=None
):
from dvc.dependency import loads_from, loads_params
assert not stage.deps
stage.deps = []
stage.deps += loads_from(stage, deps or [], erepo=erepo, fs_config=fs_config)
stage.deps += loads_from(stage, deps or [], erepo=erepo, fs_config=fs_config, db=db)
stage.deps += loads_params(stage, params or [])
@@ -182,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")
+30 -8
View File
@@ -2,26 +2,48 @@ import os
import shutil
from pathlib import Path
from subprocess import check_output
from typing import Dict, Optional
import pytest
from dulwich.porcelain import clone
from funcy import first
from packaging import version
from dvc.types import StrPath
@pytest.fixture(scope="session")
def bench_config(request):
return request.config.bench_config
class VirtualEnv:
def __init__(self, path: StrPath) -> None:
self.path = Path(path)
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:
exe = self.which(cmd)
check_output([exe, *args], env=env) # noqa: S603
def which(self, cmd: str) -> str:
assert self.bin.exists()
return shutil.which(cmd, path=self.bin) or cmd
@pytest.fixture(scope="session")
def make_dvc_venv(tmp_path_factory):
def _make_dvc_venv(name):
from pytest_virtualenv import VirtualEnv
name = _sanitize_venv_name(name)
venv_dir = tmp_path_factory.mktemp(f"dvc-venv-{name}")
return VirtualEnv(workspace=venv_dir)
venv = VirtualEnv(venv_dir)
venv.create()
return venv
return _make_dvc_venv
@@ -74,16 +96,16 @@ def make_dvc_bin(
venv = dvc_venvs.get(dvc_rev)
if not venv:
venv = make_dvc_venv(dvc_rev)
venv.run("pip install -U pip")
venv.run("pip", "install", "-U", "pip")
if bench_config.dvc_install_deps:
egg = f"dvc[{bench_config.dvc_install_deps}]"
else:
egg = "dvc"
venv.run(f"pip install git+file://{dvc_git_repo}@{dvc_rev}#egg={egg}")
venv.run("pip", "install", f"git+file://{dvc_git_repo}@{dvc_rev}#egg={egg}")
if dvc_rev in ["2.18.1", "2.11.0", "2.6.3"]:
venv.run("pip install fsspec==2022.11.0")
venv.run("pip", "install", "fsspec==2022.11.0")
dvc_venvs[dvc_rev] = venv
dvc_bin = venv.virtualenv / "bin" / "dvc"
dvc_bin = venv.which("dvc")
else:
dvc_bin = bench_config.dvc_bin
@@ -171,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(
+14
View File
@@ -172,6 +172,8 @@ class TestRemoteVersionAware:
run_copy("foo", "foo_copy", name="copy")
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()
assert "version_id" in foo_dvc
stage = stage.reload()
@@ -181,18 +183,24 @@ class TestRemoteVersionAware:
remove(dvc.cache.local.path)
remove(tmp_dir / "foo")
remove(tmp_dir / "foo_copy")
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 (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 (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
@@ -241,6 +249,12 @@ class TestRemoteVersionAware:
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()
assert (remote_version_aware / "data_dir").exists()
assert (remote_version_aware / "data_dir" / "data").exists()
class TestRemoteWorktree:
def test_file(self, tmp_dir, dvc, remote_worktree):
+14 -10
View File
@@ -70,16 +70,20 @@ class TmpDir(pathlib.Path):
def __new__(cls, *args, **kwargs):
if cls is TmpDir:
cls = WindowsTmpDir if os.name == "nt" else PosixTmpDir
# init parameter and `_init` method has been removed in Python 3.10.
kw = {"init": False} if sys.version_info < (3, 10) else {}
self = cls._from_parts(args, **kw) # type: ignore[attr-defined]
if not self._flavour.is_supported:
raise NotImplementedError(
f"cannot instantiate {cls.__name__!r} on your system"
)
if sys.version_info < (3, 10):
self._init()
return self
if sys.version_info >= (3, 12):
return object.__new__(cls)
else: # noqa: RET505
# init parameter and `_init` method has been removed in Python 3.10.
kw = {"init": False} if sys.version_info < (3, 10) else {}
self = cls._from_parts(args, **kw) # type: ignore[attr-defined]
if not self._flavour.is_supported:
raise NotImplementedError(
f"cannot instantiate {cls.__name__!r} on your system"
)
if sys.version_info < (3, 10):
self._init()
return self
def init(self, *, scm=False, dvc=False, subdir=False):
from dvc.repo import Repo
+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)
+32
View File
@@ -0,0 +1,32 @@
import logging
from funcy import once_per_args
from dvc.log import logger
logger = logger.getChild(__name__)
@once_per_args
def check_required_version(pkg: str, dist: str = "dvc", log_level=logging.WARNING):
from importlib import metadata
from packaging.requirements import InvalidRequirement, Requirement
try:
reqs = {
r.name: r.specifier for r in map(Requirement, metadata.requires(dist) or [])
}
version = metadata.version(pkg)
except (metadata.PackageNotFoundError, InvalidRequirement):
return
specifier = reqs.get(pkg)
if specifier and version and version not in specifier:
logger.log(
log_level,
"%s%s is required, but you have %r installed which is incompatible.",
pkg,
specifier,
version,
)
+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)
+27 -27
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.20.0,<2.21.0",
"dvc-data>=3,<3.1",
"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.7.1",
"pandas-stubs",
"types-colorama",
"types-psutil",
"types-pyinstaller",
@@ -85,14 +87,14 @@ 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-test-utils",
"pytest-benchmark[histogram]",
"pytest-virtualenv",
"pytest-test-utils",
"virtualenv",
]
tests = [
"dvc[testing]",
@@ -110,10 +112,13 @@ tests = [
"pytest-xdist>=3.2",
'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,<4"]
webhdfs_kerberos = ["dvc-webhdfs[kerberos]>=3,<4"]
[project.urls]
Documentation = "https://dvc.org/doc"
@@ -126,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"
@@ -144,7 +146,7 @@ namespaces = false
write_to = "dvc/_dvc_version.py"
[tool.pytest.ini_options]
addopts = "-ra --cov-config pyproject.toml --dist worksteal"
addopts = "-ra --cov-config pyproject.toml --dist worksteal --benchmark-skip"
filterwarnings = [
"error::ResourceWarning",
"error::pytest.PytestUnraisableExceptionWarning",
@@ -155,8 +157,6 @@ filterwarnings = [
# google.logging: https://github.com/googleapis/python-logging/issues/730
# Also happens with `zc.lockfile`.
"ignore:Deprecated call to `pkg_resources.declare_namespace:DeprecationWarning",
# tpi imports pkg_resources
"ignore:pkg_resources is deprecated as an API:DeprecationWarning",
# see https://github.com/networkx/networkx/issues/5723.
"ignore:nx.nx_pydot.* depends on the pydot package, which has.*known issues and is not actively maintained:DeprecationWarning",
]
@@ -207,6 +207,7 @@ warn_unused_configs = true
[[tool.mypy.overrides]]
ignore_missing_imports = true
module = [
"agate.*",
"celery.*",
"configobj.*",
"dpath.*",
@@ -228,12 +229,11 @@ module = [
"pyparsing",
"pytest_benchmark.*",
"pytest_docker.plugin",
"pytest_virtualenv.*",
"ruamel.*",
"ruamel.yaml.*",
"shortuuid",
"shtab",
"tpi.*",
"virtualenv",
"viztracer",
"voluptuous",
"yappi",
+1 -1
View File
@@ -41,7 +41,7 @@ def test_open_external(tmp_dir, erepo_dir, cloud):
# NOTE: need file to be other size for Mac
erepo_dir.dvc_gen("version", "branchver", commit="add version")
erepo_dir.dvc.push(all_branches=True)
assert erepo_dir.dvc.push(all_branches=True) == 2
# Remove cache to force download
remove(erepo_dir.dvc.cache.local.path)
+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
+1 -1
View File
@@ -486,7 +486,7 @@ def test_pull_partial(tmp_dir, dvc, local_remote):
clean(["foo"], dvc)
stats = dvc.pull(os.path.join("foo", "bar"))
assert stats["fetched"] == 2
assert stats["fetched"] == 3
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)]

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