mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56e5982951 | ||
|
|
defe4062aa | ||
|
|
f74c1c0e70 | ||
|
|
b5dede14f2 | ||
|
|
8d37f966c3 | ||
|
|
95165cc39a | ||
|
|
ccb72126ea | ||
|
|
d5315c3827 | ||
|
|
8131c32c33 | ||
|
|
0013af99d4 | ||
|
|
06ff81cd7f | ||
|
|
e19b0874c0 | ||
|
|
b290eaea43 | ||
|
|
2a2b543dd6 | ||
|
|
356dfa0327 | ||
|
|
0f4e8a23c8 | ||
|
|
333edb2cbf | ||
|
|
a65523a86a | ||
|
|
fb42429107 | ||
|
|
e40e378fc9 | ||
|
|
b1aaeb94c0 | ||
|
|
dcf6f48fb3 | ||
|
|
c6c680d2b2 | ||
|
|
1e8962bf0c | ||
|
|
0f2361f218 | ||
|
|
e9c0a99083 | ||
|
|
3c71fdbb0d | ||
|
|
04cbd40df5 | ||
|
|
5725780790 | ||
|
|
2dc6261a66 | ||
|
|
0f73c0a725 | ||
|
|
ff8752c3dc | ||
|
|
dbc7531f0d | ||
|
|
d4594c749c | ||
|
|
e062f545c9 | ||
|
|
3c77443696 | ||
|
|
b5555b7285 | ||
|
|
fbdcb6e140 | ||
|
|
7ed8d896df | ||
|
|
9fb761772c | ||
|
|
f523105219 | ||
|
|
e1ccc2c7c5 | ||
|
|
a6594e0cc9 | ||
|
|
796903a14b | ||
|
|
62ae466af2 | ||
|
|
e6dba6ef91 | ||
|
|
be4fd3b197 | ||
|
|
f05211113a | ||
|
|
9b42dc534e | ||
|
|
01b0f3bb52 | ||
|
|
f342cc7db5 | ||
|
|
0eb279ea35 | ||
|
|
d086bbbd3a | ||
|
|
046d2fd72e | ||
|
|
cb96cf1487 | ||
|
|
cf5168af74 | ||
|
|
dc123102cc | ||
|
|
438259cc70 | ||
|
|
7b978fec5b | ||
|
|
9f901e57a2 | ||
|
|
ab5ac17925 | ||
|
|
72abd013cf | ||
|
|
fa5c33ed24 | ||
|
|
9dda30b7a0 | ||
|
|
19c146e392 | ||
|
|
749c22bbcb | ||
|
|
19a0bf48f1 | ||
|
|
bda614baa8 | ||
|
|
d6fa8b67ff | ||
|
|
498a4d5d3a | ||
|
|
b74711d5fe | ||
|
|
61d5de54e5 | ||
|
|
ca7454980d | ||
|
|
8912ca7d2b | ||
|
|
f13ae75a45 | ||
|
|
aa090ecf8d | ||
|
|
0c4a5cbbb7 | ||
|
|
2b19c724c5 | ||
|
|
28b92c8b03 | ||
|
|
cd740fed42 | ||
|
|
5f31ed2b08 | ||
|
|
6e896d396c | ||
|
|
336c8695da | ||
|
|
3915fa26aa | ||
|
|
114fd8075c | ||
|
|
b39a888948 | ||
|
|
c1af66c0a6 | ||
|
|
75c5d17d1e | ||
|
|
ea9dd5fc3e | ||
|
|
763cbf6bd1 | ||
|
|
6f4b8c98fc | ||
|
|
a8bda305cd | ||
|
|
4f4c5b1d01 | ||
|
|
76cfe92960 | ||
|
|
79454557bb | ||
|
|
85046843ef | ||
|
|
e10b27acbd | ||
|
|
5a86586f3a | ||
|
|
d142d0d6af | ||
|
|
9b5ee43ce1 | ||
|
|
6e223633e7 | ||
|
|
29386a0dbb | ||
|
|
e027bc6a87 | ||
|
|
82efd3e780 | ||
|
|
a68dda4164 | ||
|
|
4b1adfd646 | ||
|
|
748c45655e | ||
|
|
225bfa6a19 | ||
|
|
09079d50ff | ||
|
|
5485e5662e | ||
|
|
e619b6dccd | ||
|
|
396e71f30c | ||
|
|
c6a393e3fe | ||
|
|
a72402c843 | ||
|
|
08f3197cdd | ||
|
|
1f325bc21e | ||
|
|
ca73078b80 | ||
|
|
fade6c5991 | ||
|
|
446a59c806 | ||
|
|
a370f80f90 | ||
|
|
dc13186759 | ||
|
|
ba1281a41c | ||
|
|
abb5977a38 | ||
|
|
50487e931f | ||
|
|
397f4c6f28 | ||
|
|
1f5bace250 | ||
|
|
5a68e821f2 | ||
|
|
b4d8690799 | ||
|
|
7f0c835eed | ||
|
|
c2506abf74 | ||
|
|
d938a1c700 | ||
|
|
a0b6b12a4f | ||
|
|
1b9b1f870a | ||
|
|
be1b529bdf | ||
|
|
da55c0da1f | ||
|
|
a23810f0b5 | ||
|
|
0adf6679d9 | ||
|
|
75083cc472 | ||
|
|
493279c7e8 | ||
|
|
95c5ea81e0 | ||
|
|
3aca0edfce | ||
|
|
5bec7d1bb9 | ||
|
|
f3836ab29a | ||
|
|
e278c9cdf4 | ||
|
|
aac32e1b64 | ||
|
|
45fd1025e2 | ||
|
|
c7c7ba69fb | ||
|
|
346b1c064e | ||
|
|
b89cb893a1 | ||
|
|
549821e6be | ||
|
|
bf26126e37 | ||
|
|
8f1c4424d3 | ||
|
|
5fb8986dbd | ||
|
|
18177a6579 | ||
|
|
ea02a18b23 | ||
|
|
e04f1499a3 | ||
|
|
488c30d1ea | ||
|
|
88c3103cf3 | ||
|
|
c37bef7add | ||
|
|
7ea1daa2b8 | ||
|
|
fd181c78d2 | ||
|
|
1de7f89af8 | ||
|
|
b7e13dbd07 | ||
|
|
dc513a98e4 | ||
|
|
9908ca7f8e | ||
|
|
0eebeb9900 | ||
|
|
415d4bc980 | ||
|
|
f3be7d761f | ||
|
|
4bde0e754e | ||
|
|
88f1ada4bf | ||
|
|
bd65c205df | ||
|
|
e6ce5f825d | ||
|
|
d30d66c1d3 | ||
|
|
19f31d7cac | ||
|
|
99d9ca714d | ||
|
|
a3b6722523 | ||
|
|
b824bc4db7 | ||
|
|
6cc349edb4 | ||
|
|
e455ee69aa | ||
|
|
31270c14e3 | ||
|
|
28b7c8b723 | ||
|
|
1d6ea68133 | ||
|
|
25d2638a57 | ||
|
|
0b03714097 | ||
|
|
9392bbdd4b | ||
|
|
c4b18b710a | ||
|
|
da1e0b57b8 | ||
|
|
b2a873d13b | ||
|
|
89ef3463dc | ||
|
|
7d14acb5a7 | ||
|
|
dd501554fe | ||
|
|
54acfda687 | ||
|
|
e70a1e9bfc | ||
|
|
aea0e6b6cc | ||
|
|
2c6b917fc5 | ||
|
|
a43c1b31f7 | ||
|
|
683a9f09bd | ||
|
|
53a7230da0 | ||
|
|
f078a572dd | ||
|
|
007041bfe9 | ||
|
|
965dbea75e | ||
|
|
f5cbf97eea | ||
|
|
198c8f2ffd | ||
|
|
368c785410 | ||
|
|
64ccd9cfdb | ||
|
|
d38b2dbcb8 | ||
|
|
8cb0f02bf9 | ||
|
|
7e75b0616f | ||
|
|
fb24775bc0 | ||
|
|
82e2446eb4 | ||
|
|
1e08cc5da3 | ||
|
|
e56a729e3b | ||
|
|
9a050684f7 | ||
|
|
2431ec6189 | ||
|
|
70fc4e6cda | ||
|
|
bbc01b40d2 | ||
|
|
6302e5f1d3 | ||
|
|
df5429f7d8 | ||
|
|
f9583f0c3d | ||
|
|
9e26a46323 | ||
|
|
746f028f6c | ||
|
|
3651939e11 | ||
|
|
d236667048 | ||
|
|
d22a68f7a8 | ||
|
|
683ae7a951 | ||
|
|
d5054ce861 | ||
|
|
e884f1b8f0 | ||
|
|
9b5772fab8 | ||
|
|
c1aaeffc2d | ||
|
|
ad70dd322b | ||
|
|
72b6971730 | ||
|
|
ee1864af8e | ||
|
|
52877485d1 | ||
|
|
a923f110e3 | ||
|
|
b8266ed15f | ||
|
|
2c871ba432 | ||
|
|
32d11f1e05 | ||
|
|
f56343d6ae | ||
|
|
7b8922b20f | ||
|
|
436a2ac793 | ||
|
|
eec3d48731 | ||
|
|
5417ccd7b6 | ||
|
|
f2b7b00af5 | ||
|
|
4f3fb15f2e | ||
|
|
016f285fe3 | ||
|
|
f6dd641855 | ||
|
|
3f2584d155 | ||
|
|
705b7bbea6 | ||
|
|
26ad01bec0 | ||
|
|
e76aaeac57 | ||
|
|
6f24d04b5e | ||
|
|
2dcaea8119 | ||
|
|
81db9b4572 | ||
|
|
98dde6cd27 | ||
|
|
e42d094310 | ||
|
|
38ee0a01f7 | ||
|
|
d494126cc5 | ||
|
|
fd3b43df05 | ||
|
|
b9bcb45aaa | ||
|
|
7bca71db2e | ||
|
|
52405a0e10 | ||
|
|
c514b0ea31 | ||
|
|
853116a0e3 | ||
|
|
5e4daf6cfb | ||
|
|
991a816995 | ||
|
|
9735b5aaa9 | ||
|
|
fa317d5b1d | ||
|
|
1b0fb6487b | ||
|
|
43e6229308 | ||
|
|
1bef0da4fb | ||
|
|
284d83f390 | ||
|
|
417e6d4b76 | ||
|
|
248ba3b6b6 | ||
|
|
8a91c2b303 | ||
|
|
544dad75e9 | ||
|
|
8f50f2509f | ||
|
|
cdc1857559 | ||
|
|
358dd163da | ||
|
|
aae1a93cb5 | ||
|
|
c9b7218659 | ||
|
|
88c4b9525d | ||
|
|
69d36eeb06 | ||
|
|
3201794553 | ||
|
|
4464f8b4b9 | ||
|
|
efc7e05893 | ||
|
|
a555c3d29a | ||
|
|
676645b1ca | ||
|
|
93c3100bad | ||
|
|
c74156288d | ||
|
|
0e38ccc031 | ||
|
|
b69b368ffe | ||
|
|
6cc80fdeaa | ||
|
|
8f8d49d023 | ||
|
|
83ec952da1 | ||
|
|
c0d7e2a8de | ||
|
|
9aad09164c | ||
|
|
1f7733fec1 | ||
|
|
856c902b65 | ||
|
|
ebb459f9e2 | ||
|
|
2e0b024efe | ||
|
|
135fa26515 | ||
|
|
ef507efa04 | ||
|
|
05db79e7cf | ||
|
|
0a813bbcbc | ||
|
|
e82356ba92 | ||
|
|
5bfacec974 | ||
|
|
1848074ac9 | ||
|
|
a7c0f6ef39 | ||
|
|
0a8a9deceb | ||
|
|
c5d98681c0 | ||
|
|
15e05e4b3e | ||
|
|
7df8b0c09b | ||
|
|
2e1faf2979 | ||
|
|
cbaeff2ea1 | ||
|
|
132e625ae8 | ||
|
|
8517cccff4 | ||
|
|
c52a25e011 | ||
|
|
c4678736b0 | ||
|
|
ec92cc14d1 | ||
|
|
e6048287cb | ||
|
|
72a353d8b9 | ||
|
|
dd2b515c97 | ||
|
|
c223cdaecd | ||
|
|
5379aa2348 | ||
|
|
3375a7c39f | ||
|
|
519c47f5fc | ||
|
|
6f6038888f | ||
|
|
e223f51e7e | ||
|
|
fb628bf9ea | ||
|
|
4f4e977e53 | ||
|
|
8b1f8847fe | ||
|
|
c62a0380e9 | ||
|
|
8d8939fb67 | ||
|
|
f7a54bf2d0 | ||
|
|
1a9f5d3aa8 | ||
|
|
017a510b83 | ||
|
|
a80d224c58 | ||
|
|
49709a4810 | ||
|
|
7df58ce404 | ||
|
|
827e58f305 | ||
|
|
8bc6ba7cd5 | ||
|
|
a96a446742 | ||
|
|
837c8e8de4 | ||
|
|
603da1ca1e | ||
|
|
377d0c9d58 | ||
|
|
6bdf41a217 | ||
|
|
1aeb8750b6 | ||
|
|
5aae934379 | ||
|
|
de1338caaf | ||
|
|
9957410998 | ||
|
|
cf9855af45 | ||
|
|
0001cc3a4b | ||
|
|
440e1491f0 | ||
|
|
b0384f8f51 | ||
|
|
cd1e228e55 | ||
|
|
3b2e031e60 | ||
|
|
e5ee82bc99 | ||
|
|
bc95b650a4 | ||
|
|
257ea5f4cb | ||
|
|
5f7acd4cbe | ||
|
|
13325ea89b | ||
|
|
fd71434df5 | ||
|
|
2ff4ff2daa | ||
|
|
0ee6d6eba8 | ||
|
|
7c34370464 | ||
|
|
b67b254287 | ||
|
|
212928a1fa | ||
|
|
5be23cfd4e | ||
|
|
4d3a631e60 | ||
|
|
491155cdb0 | ||
|
|
9bb4501bf0 | ||
|
|
4cb658f96c | ||
|
|
9f66897bf7 | ||
|
|
84a1750013 | ||
|
|
6cb37ac27b | ||
|
|
f38490c897 | ||
|
|
34d80fbfce | ||
|
|
89537a77b5 | ||
|
|
c72e1fb0b8 | ||
|
|
9fbe5d929d | ||
|
|
0635b0ca15 | ||
|
|
06abf73c22 | ||
|
|
5e2c441d28 | ||
|
|
6efd699267 | ||
|
|
f639ab50ab | ||
|
|
953ae56536 | ||
|
|
ac0397e536 | ||
|
|
b243ccab1f | ||
|
|
c31a5d9df9 | ||
|
|
44b78b81b9 | ||
|
|
d25cb79e0a | ||
|
|
79238fd2a3 | ||
|
|
2bf7bd990a | ||
|
|
68b9ecb95c | ||
|
|
594716ec79 |
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@ $ dvc doctor
|
|||||||
**Additional Information (if any):**
|
**Additional Information (if any):**
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Please check https://github.com/iterative/dvc/wiki/Debugging-DVC on ways to gather more information regarding the issue.
|
Please check https://github.com/treeverse/dvc/wiki/Debugging-DVC on ways to gather more information regarding the issue.
|
||||||
|
|
||||||
If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
|
If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
|
||||||
If the issue is regarding the performance, please attach the profiling information and the benchmark comparisons.
|
If the issue is regarding the performance, please attach the profiling information and the benchmark comparisons.
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
name: Epic/Story Issue Template
|
|
||||||
about: Template for "top" level issues - Epics (>2 weeks) / Stories (<2 weeks)
|
|
||||||
title: 'Epic: New Feature'
|
|
||||||
labels: epic
|
|
||||||
assignees:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary / Background
|
|
||||||
What do you want to achieve, why? business context
|
|
||||||
...
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
What will be impacted and what won't be?
|
|
||||||
What needs implementation and what is invariant?
|
|
||||||
e.g.
|
|
||||||
- user should be able to run workflow X from UI
|
|
||||||
- enable workflow Y, Z from CLI
|
|
||||||
|
|
||||||
## Assumptions
|
|
||||||
Product / UX assumptions as well as technical assumptions / limitations
|
|
||||||
e.g.
|
|
||||||
* Support only Python Runtime
|
|
||||||
* Focus on DVC experiments only
|
|
||||||
* Deployment environments don't change often and can be picked up from shared configuration
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
e.g.
|
|
||||||
- How should access control work for shared artifacts (workflow X)
|
|
||||||
- Python runtime assumption - is it really valid? in light of <...>
|
|
||||||
|
|
||||||
## Blockers / Dependencies
|
|
||||||
List issues or other conditions / blockers
|
|
||||||
|
|
||||||
## General Approach
|
|
||||||
Invocation example:
|
|
||||||
```shell
|
|
||||||
$ mapper-run task.tar.gz --ray-cluster <ip>:<port>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
### Must have (p1)
|
|
||||||
- [ ] subissue2
|
|
||||||
- [ ] step 2
|
|
||||||
- info
|
|
||||||
- info
|
|
||||||
|
|
||||||
### Optional / followup (p2)
|
|
||||||
- [ ] ⌛ step 3 wip
|
|
||||||
- [ ] step 4
|
|
||||||
|
|
||||||
## Timelines
|
|
||||||
|
|
||||||
Put your estimations here. Update once certainty changes
|
|
||||||
- end of week (Feb 3) for prototype with workflows X, Y
|
|
||||||
- Feb 15 - MVP in production
|
|
||||||
- Low priority followups can be done later
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
* [ ] ❗ I have followed the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) checklist.
|
* [ ] ❗ I have followed the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) checklist.
|
||||||
|
|
||||||
* [ ] 📖 If this PR requires [documentation](https://dvc.org/doc) updates, I have created a separate PR (or issue, at least) in [dvc.org](https://github.com/iterative/dvc.org) and linked it here.
|
* [ ] 📖 If this PR requires [documentation](https://dvc.org/doc) updates, I have created a separate PR (or issue, at least) in [dvc.org](https://github.com/treeverse/dvc.org) and linked it here.
|
||||||
|
|
||||||
Thank you for the contribution - we'll try to review it as soon as possible. 🙏
|
Thank you for the contribution - we'll try to review it as soon as possible. 🙏
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
pull_request_rules:
|
|
||||||
- name: backport patches to 2.x
|
|
||||||
conditions:
|
|
||||||
- base=main
|
|
||||||
- label=backport
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- "2.x"
|
|
||||||
title: ({{ destination_branch }}) {{ title }}
|
|
||||||
@@ -5,14 +5,40 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_COLOR: "1"
|
||||||
|
PY_COLORS: "1"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bench:
|
bench:
|
||||||
name: run benchmarks
|
name: run benchmarks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.14"
|
||||||
- uses: iterative/dvc-bench@main
|
|
||||||
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
pytest_options: "-k 'test_init or test_help'"
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: astral-sh/setup-uv@v7
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
cache-dependency-glob: pyproject.toml
|
||||||
|
|
||||||
|
- run: uv pip install '.[tests]' --system
|
||||||
|
- name: run benchmarks on base branch
|
||||||
|
run: pytest --benchmark-autosave dvc/testing/benchmarks/ -k 'test_init or test_help'
|
||||||
|
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
clean: false
|
||||||
|
- run: uv pip install '.[tests]' --system
|
||||||
|
- name: run benchmarks for PR
|
||||||
|
run: >
|
||||||
|
pytest --benchmark-compare --benchmark-compare-fail=min:5%
|
||||||
|
--benchmark-group-by name
|
||||||
|
dvc/testing/benchmarks/ -k 'test_init or test_help'
|
||||||
|
|||||||
@@ -19,46 +19,49 @@ jobs:
|
|||||||
name: Build & Verify package
|
name: Build & Verify package
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: 3.12
|
python-version: "3.14"
|
||||||
|
|
||||||
- run: python -m pip install --upgrade pip setuptools_scm build twine
|
- uses: astral-sh/setup-uv@v7
|
||||||
|
|
||||||
- name: Force version for Test PyPI upload
|
- name: Omit local version for Test PyPI upload
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
run: echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
run: echo SETUPTOOLS_SCM_OVERRIDES_FOR_DVC='{local_scheme="no-local-version"}' >> $GITHUB_ENV
|
||||||
|
|
||||||
- run: echo 'PKG = "pip"'>dvc/_build.py
|
- name: Build Python Package
|
||||||
- run: python -m build
|
run: |
|
||||||
env:
|
echo 'PKG = "pip"'>dvc/_build.py
|
||||||
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DVC: ${{ env.version }}
|
uv build
|
||||||
- run: twine check --strict dist/*
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- name: Check dist
|
||||||
|
run: uv tool run twine check --strict dist/*
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: Packages
|
name: Packages
|
||||||
path: dist/
|
path: dist/
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
test-pypi-publish:
|
test-pypi-publish:
|
||||||
name: Publish dev package to test.pypi.org
|
name: Publish dev package to test.pypi.org
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
if: ${{ github.event.action == 'published' || (github.repository == 'iterative/dvc' && github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
if: ${{ github.event.action == 'published' || (github.repository == 'treeverse/dvc' && github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
name: test-pypi
|
name: test-pypi
|
||||||
url: https://test.pypi.org/p/dvc
|
url: https://test.pypi.org/p/dvc/${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: Packages
|
name: Packages
|
||||||
path: dist
|
path: dist
|
||||||
@@ -77,13 +80,13 @@ jobs:
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
name: pypi
|
name: pypi
|
||||||
url: https://pypi.org/p/dvc
|
url: https://pypi.org/p/dvc/${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v8
|
||||||
with:
|
with:
|
||||||
name: Packages
|
name: Packages
|
||||||
path: dist
|
path: dist
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main", "2.x"]
|
branches: ["main"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
schedule:
|
schedule:
|
||||||
@@ -28,13 +28,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4.37.4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: security-extended
|
queries: security-extended
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4.37.4
|
||||||
|
|||||||
@@ -18,35 +18,36 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04]
|
os: [ubuntu-latest]
|
||||||
pyv: ["3.10"]
|
pyv: ["3.14"]
|
||||||
plugin: ["dvc-s3"]
|
plugin: ["dvc-s3"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
path: dvc
|
path: dvc
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
repository: iterative/${{ matrix.plugin }}
|
repository: treeverse/${{ matrix.plugin }}
|
||||||
ref: main
|
ref: main
|
||||||
path: ${{ matrix.plugin }}
|
path: ${{ matrix.plugin }}
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.pyv }}
|
python-version: ${{ matrix.pyv }}
|
||||||
cache: pip
|
|
||||||
cache-dependency-path: |
|
- uses: astral-sh/setup-uv@v7
|
||||||
dvc/pyproject.toml
|
with:
|
||||||
${{ matrix.plugin }}/setup.cfg
|
enable-cache: true
|
||||||
|
cache-dependency-glob: "**/pyproject.toml"
|
||||||
|
cache-suffix: ${{ matrix.pyv }}
|
||||||
|
|
||||||
- name: Install plugin + DVC@PR
|
- name: Install plugin + DVC@PR
|
||||||
run: |
|
run: |
|
||||||
pip install --upgrade pip wheel
|
uv pip install "./dvc[testing]" --system
|
||||||
pip install "./dvc[testing]"
|
uv pip install -e "./${{ matrix.plugin }}[tests]" --system
|
||||||
pip install -e "./${{ matrix.plugin }}[tests]"
|
|
||||||
|
|
||||||
- name: Run plugin tests
|
- name: Run plugin tests
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
|
|||||||
+125
-21
@@ -2,7 +2,7 @@ name: Tests
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, 2.x]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '5 1 * * *' # every day at 01:05
|
- cron: '5 1 * * *' # every day at 01:05
|
||||||
@@ -16,42 +16,146 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
lint:
|
||||||
|
timeout-minutes: 10
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
DVC_NO_ANALYTICS: true
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v7
|
||||||
|
with:
|
||||||
|
python-version: '3.14'
|
||||||
|
- uses: astral-sh/setup-uv@v7
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
cache-dependency-glob: pyproject.toml
|
||||||
|
- run: uv pip install -e ".[dev]" --system
|
||||||
|
env:
|
||||||
|
UV_LINK_MODE: ${{ runner.os == 'Windows' && 'symlink' || 'hardlink' }}
|
||||||
|
|
||||||
|
- name: Cache mypy
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: .mypy_cache
|
||||||
|
key: mypy-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}
|
||||||
|
- uses: pre-commit/[email protected]
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
timeout-minutes: 50
|
timeout-minutes: 20
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
pyv: ["3.9", "3.10", "3.11", "3.12"]
|
pyv: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.9"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.9"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.9"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.9"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.10"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.10"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.10"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.10"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.11"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.11"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.11"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.11"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.12"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.12"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.12"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.12"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.13"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.13"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.13"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.13"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.14"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.14"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.14"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||||
|
- os: windows-latest
|
||||||
|
pyv: "3.14"
|
||||||
|
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v7
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.pyv }}
|
python-version: ${{ matrix.pyv }}
|
||||||
cache: pip
|
- uses: astral-sh/setup-uv@v7
|
||||||
cache-dependency-path: pyproject.toml
|
with:
|
||||||
- name: install
|
enable-cache: true
|
||||||
run: |
|
cache-dependency-glob: pyproject.toml
|
||||||
pip install --upgrade pip wheel
|
cache-suffix: ${{ matrix.pyv }}
|
||||||
pip install -e ".[dev]"
|
- run: uv pip install -e ".[dev]" pytest-split --system
|
||||||
- uses: pre-commit/[email protected]
|
env:
|
||||||
|
UV_LINK_MODE: ${{ runner.os == 'Windows' && 'symlink' || 'hardlink' }}
|
||||||
|
|
||||||
- name: run tests
|
- name: run tests
|
||||||
timeout-minutes: 40
|
timeout-minutes: 40
|
||||||
env:
|
env:
|
||||||
|
COVERAGE_CORE: sysmon
|
||||||
PYTHONUTF8: 1
|
PYTHONUTF8: 1
|
||||||
run: >
|
run: >
|
||||||
pytest -n=logical --timeout=300 --durations=100
|
pytest ${{ matrix.pytestargs }} -n=logical --dist=worksteal --timeout=300 --durations=0
|
||||||
--cov --cov-report=xml --cov-report=term
|
--cov --cov-report=xml --cov-report=term --durations-path=./.github/.test_durations
|
||||||
- name: upload coverage report
|
- name: upload coverage report
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v7
|
||||||
with:
|
|
||||||
file: ./coverage.xml
|
|
||||||
fail_ci_if_error: false
|
|
||||||
check:
|
check:
|
||||||
if: always()
|
if: always()
|
||||||
needs: [tests]
|
needs: [tests]
|
||||||
@@ -66,7 +170,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Slack Notification
|
- name: Slack Notification
|
||||||
uses: rtCamp/action-slack-notify@v2.2.1
|
uses: rtCamp/action-slack-notify@v2.4.0
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||||
SLACK_COLOR: ${{ job.status }}
|
SLACK_COLOR: ${{ job.status }}
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ scripts/ci/gcp-creds.json
|
|||||||
.gcp-creds.json
|
.gcp-creds.json
|
||||||
|
|
||||||
*~
|
*~
|
||||||
/dvc/_dvc_version.py
|
/dvc/_version.py
|
||||||
|
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ ci:
|
|||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
@@ -22,13 +22,13 @@ repos:
|
|||||||
- id: sort-simple-yaml
|
- id: sort-simple-yaml
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: 'v0.1.15'
|
rev: 'v0.15.20'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff-check
|
||||||
args: [--fix, --exit-non-zero-on-fix]
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.2.6
|
rev: v2.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
additional_dependencies: ["tomli"]
|
additional_dependencies: ["tomli"]
|
||||||
@@ -50,7 +50,7 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
name: DVC pre-commit
|
name: DVC pre-commit
|
||||||
stages:
|
stages:
|
||||||
- commit
|
- pre-commit
|
||||||
verbose: true
|
verbose: true
|
||||||
require_serial: true
|
require_serial: true
|
||||||
- args:
|
- args:
|
||||||
@@ -61,7 +61,7 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
name: DVC pre-push
|
name: DVC pre-push
|
||||||
stages:
|
stages:
|
||||||
- push
|
- pre-push
|
||||||
require_serial: true
|
require_serial: true
|
||||||
- always_run: true
|
- always_run: true
|
||||||
args:
|
args:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
name: DVC pre-commit
|
name: DVC pre-commit
|
||||||
require_serial: true
|
require_serial: true
|
||||||
stages:
|
stages:
|
||||||
- commit
|
- pre-commit
|
||||||
verbose: true
|
verbose: true
|
||||||
- args:
|
- args:
|
||||||
- git-hook
|
- git-hook
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
name: DVC pre-push
|
name: DVC pre-push
|
||||||
require_serial: true
|
require_serial: true
|
||||||
stages:
|
stages:
|
||||||
- push
|
- pre-push
|
||||||
- always_run: true
|
- always_run: true
|
||||||
args:
|
args:
|
||||||
- git-hook
|
- git-hook
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
id: dvc-post-checkout
|
id: dvc-post-checkout
|
||||||
language: python
|
language: python
|
||||||
language_version: python3
|
language_version: python3
|
||||||
minimum_pre_commit_version: 2.2.0
|
minimum_pre_commit_version: 3.2.0
|
||||||
name: DVC post-checkout
|
name: DVC post-checkout
|
||||||
require_serial: true
|
require_serial: true
|
||||||
stages:
|
stages:
|
||||||
|
|||||||
+1
-1
@@ -4,5 +4,5 @@
|
|||||||
"data-science", "data-version-control", "machine-learning", "git",
|
"data-science", "data-version-control", "machine-learning", "git",
|
||||||
"developer-tools", "reproducibility", "collaboration", "ai", "python"],
|
"developer-tools", "reproducibility", "collaboration", "ai", "python"],
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{"name": "DVC team", "type": "Other", "affiliation": "Iterative"}]
|
{"name": "DVC team", "type": "Other", "affiliation": "Treeverse"}]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
cff-version: 1.2.0
|
||||||
|
title: 'DVC: Data Version Control - Git for Data & Models"'
|
||||||
|
message: >-
|
||||||
|
If you use this software as part of a publication and wish
|
||||||
|
to cite it, please use the metadata from this file.
|
||||||
|
type: software
|
||||||
|
authors:
|
||||||
|
- name: The DVC team and contributors
|
||||||
|
website: 'https://github.com/treeverse'
|
||||||
|
repository-code: 'https://github.com/treeverse/dvc'
|
||||||
|
url: 'https://dvc.org'
|
||||||
|
abstract: >-
|
||||||
|
DVC is a tool for data science that takes advantage of existing software engineering toolset.
|
||||||
|
It helps machine learning teams manage large datasets, make projects reproducible, and collaborate better.
|
||||||
|
keywords:
|
||||||
|
- ai
|
||||||
|
- collaboration
|
||||||
|
- data-science
|
||||||
|
- data-version-control
|
||||||
|
- developer-tools
|
||||||
|
- git
|
||||||
|
- machine-learning
|
||||||
|
- python
|
||||||
|
- reproducibility
|
||||||
|
license: Apache-2.0
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2017-2021 Iterative, Inc.
|
Copyright 2025 Treeverse.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+11
-17
@@ -167,7 +167,7 @@ To install the development version, run:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install git+git://github.com/iterative/dvc
|
pip install "dvc @ git+https://github.com/treeverse/dvc"
|
||||||
|
|
||||||
Package (Platform-specific)
|
Package (Platform-specific)
|
||||||
---------------------------
|
---------------------------
|
||||||
@@ -175,7 +175,7 @@ Package (Platform-specific)
|
|||||||
|Packages|
|
|Packages|
|
||||||
|
|
||||||
Self-contained packages for Linux, Windows, and Mac are available.
|
Self-contained packages for Linux, Windows, and Mac are available.
|
||||||
The latest version of the packages can be found on the GitHub `releases page <https://github.com/iterative/dvc/releases>`_.
|
The latest version of the packages can be found on the GitHub `releases page <https://github.com/treeverse/dvc/releases>`_.
|
||||||
|
|
||||||
Ubuntu / Debian (deb)
|
Ubuntu / Debian (deb)
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
@@ -198,8 +198,6 @@ Fedora / CentOS (rpm)
|
|||||||
Contributing
|
Contributing
|
||||||
============
|
============
|
||||||
|
|
||||||
|Maintainability|
|
|
||||||
|
|
||||||
Contributions are welcome!
|
Contributions are welcome!
|
||||||
Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more details.
|
Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more details.
|
||||||
Thanks to all our contributors!
|
Thanks to all our contributors!
|
||||||
@@ -213,7 +211,7 @@ Community and Support
|
|||||||
* `Forum <https://discuss.dvc.org/>`_
|
* `Forum <https://discuss.dvc.org/>`_
|
||||||
* `Discord Chat <https://dvc.org/chat>`_
|
* `Discord Chat <https://dvc.org/chat>`_
|
||||||
* `Email <mailto:[email protected]>`_
|
* `Email <mailto:[email protected]>`_
|
||||||
* `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
|
* `Mailing List <https://dvc.org/community#subscribe>`_
|
||||||
|
|
||||||
Copyright
|
Copyright
|
||||||
=========
|
=========
|
||||||
@@ -237,23 +235,19 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
|||||||
:target: https://dvc.org
|
:target: https://dvc.org
|
||||||
:alt: DVC logo
|
:alt: DVC logo
|
||||||
|
|
||||||
.. |VS Code Extension Overview| image:: https://raw.githubusercontent.com/iterative/vscode-dvc/main/extension/docs/overview.gif
|
.. |VS Code Extension Overview| image:: https://raw.githubusercontent.com/treeverse/vscode-dvc/main/extension/docs/overview.gif
|
||||||
:alt: DVC Extension for VS Code
|
:alt: DVC Extension for VS Code
|
||||||
|
|
||||||
.. |CI| image:: https://github.com/iterative/dvc/workflows/Tests/badge.svg?branch=main
|
.. |CI| image:: https://github.com/treeverse/dvc/actions/workflows/tests.yaml/badge.svg
|
||||||
:target: https://github.com/iterative/dvc/actions
|
:target: https://github.com/treeverse/dvc/actions/workflows/tests.yaml
|
||||||
:alt: GHA Tests
|
:alt: GHA Tests
|
||||||
|
|
||||||
.. |Maintainability| image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
|
|
||||||
:target: https://codeclimate.com/github/iterative/dvc
|
|
||||||
:alt: Code Climate
|
|
||||||
|
|
||||||
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/dvc
|
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/dvc
|
||||||
:target: https://pypi.org/project/dvc
|
:target: https://pypi.org/project/dvc
|
||||||
:alt: Python Version
|
:alt: Python Version
|
||||||
|
|
||||||
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/main/graph/badge.svg
|
.. |Coverage| image:: https://codecov.io/gh/treeverse/dvc/branch/main/graph/badge.svg
|
||||||
:target: https://codecov.io/gh/iterative/dvc
|
:target: https://codecov.io/gh/treeverse/dvc
|
||||||
:alt: Codecov
|
:alt: Codecov
|
||||||
|
|
||||||
.. |Snap| image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
|
.. |Snap| image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
|
||||||
@@ -268,7 +262,7 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
|||||||
:target: https://formulae.brew.sh/formula/dvc
|
:target: https://formulae.brew.sh/formula/dvc
|
||||||
:alt: Homebrew
|
:alt: Homebrew
|
||||||
|
|
||||||
.. |Conda| image:: https://img.shields.io/conda/v/conda-forge/dvc.svg?label=conda&logo=conda-forge
|
.. |Conda| image:: https://anaconda.org/conda-forge/dvc/badges/version.svg
|
||||||
:target: https://anaconda.org/conda-forge/dvc
|
:target: https://anaconda.org/conda-forge/dvc
|
||||||
:alt: Conda-forge
|
:alt: Conda-forge
|
||||||
|
|
||||||
@@ -292,8 +286,8 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
|||||||
:target: https://dvc.org/img/flow.gif
|
:target: https://dvc.org/img/flow.gif
|
||||||
:alt: how_dvc_works
|
:alt: how_dvc_works
|
||||||
|
|
||||||
.. |Contribs| image:: https://contrib.rocks/image?repo=iterative/dvc
|
.. |Contribs| image:: https://contrib.rocks/image?repo=treeverse/dvc
|
||||||
:target: https://github.com/iterative/dvc/graphs/contributors
|
:target: https://github.com/treeverse/dvc/graphs/contributors
|
||||||
:alt: Contributors
|
:alt: Contributors
|
||||||
|
|
||||||
.. |VS Code| image:: https://img.shields.io/visual-studio-marketplace/v/Iterative.dvc?color=blue&label=VSCode&logo=visualstudiocode&logoColor=blue
|
.. |VS Code| image:: https://img.shields.io/visual-studio-marketplace/v/Iterative.dvc?color=blue&label=VSCode&logo=visualstudiocode&logoColor=blue
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ DVC
|
|||||||
----
|
----
|
||||||
Make your data science projects reproducible and shareable.
|
Make your data science projects reproducible and shareable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import dvc.logger
|
import dvc.logger
|
||||||
from dvc.build import PKG # noqa: F401
|
from dvc.build import PKG # noqa: F401
|
||||||
from dvc.version import __version__, version_tuple # noqa: F401
|
from dvc.version import __version__, version_tuple # noqa: F401
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Main entry point for DVC command line tool."""
|
"""Main entry point for DVC command line tool."""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from dvc.cli import main
|
from dvc.cli import main
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from PyInstaller.utils.hooks import collect_submodules, is_module_or_submodule
|
|||||||
_EXCLUDES = ("celery.bin", "celery.contrib")
|
_EXCLUDES = ("celery.bin", "celery.contrib")
|
||||||
hiddenimports = collect_submodules(
|
hiddenimports = collect_submodules(
|
||||||
"celery",
|
"celery",
|
||||||
filter=lambda name: not any(
|
filter=lambda name: (
|
||||||
is_module_or_submodule(name, module) for module in _EXCLUDES
|
not any(is_module_or_submodule(name, module) for module in _EXCLUDES)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ datas += copy_metadata("gcsfs")
|
|||||||
datas += copy_metadata("pyarrow")
|
datas += copy_metadata("pyarrow")
|
||||||
datas += copy_metadata("pydrive2")
|
datas += copy_metadata("pydrive2")
|
||||||
datas += copy_metadata("s3fs", recursive=True)
|
datas += copy_metadata("s3fs", recursive=True)
|
||||||
datas += copy_metadata("boto3")
|
|
||||||
datas += copy_metadata("ossfs")
|
datas += copy_metadata("ossfs")
|
||||||
datas += copy_metadata("sshfs")
|
datas += copy_metadata("sshfs")
|
||||||
datas += copy_metadata("webdav4")
|
datas += copy_metadata("webdav4")
|
||||||
|
|||||||
+6
-6
@@ -14,7 +14,7 @@ def viztracer_profile(
|
|||||||
log_async: bool = False,
|
log_async: bool = False,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
import viztracer
|
import viztracer # ty: ignore[unresolved-import]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Failed to run profiler, viztracer is not installed") # noqa: T201
|
print("Failed to run profiler, viztracer is not installed") # noqa: T201
|
||||||
yield
|
yield
|
||||||
@@ -37,7 +37,7 @@ def yappi_profile(
|
|||||||
separate_threads: Optional[bool] = False,
|
separate_threads: Optional[bool] = False,
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
import yappi
|
import yappi # ty: ignore[unresolved-import]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Failed to run profiler, yappi is not installed") # noqa: T201
|
print("Failed to run profiler, yappi is not installed") # noqa: T201
|
||||||
yield
|
yield
|
||||||
@@ -79,7 +79,7 @@ def yappi_profile(
|
|||||||
def instrument(html_output=False):
|
def instrument(html_output=False):
|
||||||
"""Run a statistical profiler"""
|
"""Run a statistical profiler"""
|
||||||
try:
|
try:
|
||||||
from pyinstrument import Profiler
|
from pyinstrument import Profiler # ty: ignore[unresolved-import]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Failed to run profiler, pyinstrument is not installed") # noqa: T201
|
print("Failed to run profiler, pyinstrument is not installed") # noqa: T201
|
||||||
yield
|
yield
|
||||||
@@ -123,7 +123,7 @@ def debug():
|
|||||||
yield
|
yield
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
import ipdb as pdb # noqa: T100
|
import ipdb as pdb # noqa: T100 # ty: ignore[unresolved-import]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import pdb # type: ignore[no-redef] # noqa: T100
|
import pdb # type: ignore[no-redef] # noqa: T100
|
||||||
pdb.post_mortem()
|
pdb.post_mortem()
|
||||||
@@ -162,7 +162,7 @@ def _get_path_func(tool: str, ext: str):
|
|||||||
fmt = f"{tool}.dvc-{{now:%Y%m%d}}_{{now:%H%M%S}}.{ext}"
|
fmt = f"{tool}.dvc-{{now:%Y%m%d}}_{{now:%H%M%S}}.{ext}"
|
||||||
|
|
||||||
def func(now: Optional["datetime"] = None) -> str:
|
def func(now: Optional["datetime"] = None) -> str:
|
||||||
return fmt.format(now=now or datetime.now())
|
return fmt.format(now=now or datetime.now()) # noqa: DTZ005
|
||||||
|
|
||||||
return func
|
return func
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ def add_debugging_flags(parser):
|
|||||||
from argparse import SUPPRESS
|
from argparse import SUPPRESS
|
||||||
|
|
||||||
# For detailed info see:
|
# For detailed info see:
|
||||||
# https://github.com/iterative/dvc/wiki/Debugging,-Profiling-and-Benchmarking-DVC
|
# https://github.com/treeverse/dvc/wiki/Debugging,-Profiling-and-Benchmarking-DVC
|
||||||
args, _ = parser.parse_known_args()
|
args, _ = parser.parse_known_args()
|
||||||
verbose = args.verbose
|
verbose = args.verbose
|
||||||
|
|
||||||
|
|||||||
+73
-13
@@ -1,10 +1,14 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
|
|
||||||
from .env import DVC_ANALYTICS_ENDPOINT, DVC_NO_ANALYTICS
|
from .env import DVC_ANALYTICS_ENDPOINT, DVC_NO_ANALYTICS
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dvc.scm import Base
|
||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +71,7 @@ def send(path):
|
|||||||
thus, the need of removing such file afterwards.
|
thus, the need of removing such file afterwards.
|
||||||
"""
|
"""
|
||||||
import requests
|
import requests
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
url = os.environ.get(DVC_ANALYTICS_ENDPOINT, "https://analytics.dvc.org")
|
url = os.environ.get(DVC_ANALYTICS_ENDPOINT, "https://analytics.dvc.org")
|
||||||
headers = {"content-type": "application/json"}
|
headers = {"content-type": "application/json"}
|
||||||
@@ -81,7 +86,7 @@ def send(path):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
requests.post(url, json=report, headers=headers, timeout=5)
|
requests.post(url, json=report, headers=headers, timeout=5)
|
||||||
except requests.exceptions.RequestException as e:
|
except RequestException as e:
|
||||||
logger.trace("", exc_info=True)
|
logger.trace("", exc_info=True)
|
||||||
logger.debug("failed to send analytics report %s", str(e))
|
logger.debug("failed to send analytics report %s", str(e))
|
||||||
|
|
||||||
@@ -89,20 +94,62 @@ def send(path):
|
|||||||
os.remove(path)
|
os.remove(path)
|
||||||
|
|
||||||
|
|
||||||
def _scm_in_use():
|
def _git_remote_url(scm: Optional["Base"]) -> Optional[str]:
|
||||||
from dvc.exceptions import NotDvcRepoError
|
from dvc.scm import Git
|
||||||
from dvc.repo import Repo
|
|
||||||
from dvc.scm import NoSCM
|
|
||||||
|
|
||||||
from .scm import SCM, SCMError
|
if not isinstance(scm, Git):
|
||||||
|
return None
|
||||||
|
|
||||||
|
from dulwich.porcelain import get_remote_repo
|
||||||
|
|
||||||
|
dulwich_repo = scm.dulwich.repo
|
||||||
|
try:
|
||||||
|
_remote, url = get_remote_repo(dulwich_repo)
|
||||||
|
except IndexError:
|
||||||
|
# IndexError happens when the head is detached
|
||||||
|
_remote, url = get_remote_repo(dulwich_repo, b"origin")
|
||||||
|
# Dulwich returns (None, "origin") if no remote set
|
||||||
|
if (_remote, url) == (None, "origin"):
|
||||||
|
return None
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def _scm_in_use(scm: Optional["Base"]) -> Optional[str]:
|
||||||
|
return type(scm).__name__ if scm else None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_git_remote_path(remote_url: str) -> str:
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from scmrepo.urls import is_scp_style_url
|
||||||
|
|
||||||
|
parsed = urlparse(remote_url)
|
||||||
|
# Windows Path also gets parsed with a drive letter as scheme
|
||||||
|
# https://github.com/python/cpython/issues/86381
|
||||||
|
if parsed.scheme and parsed.scheme in ("http", "https", "git", "ssh"):
|
||||||
|
return parsed.path.strip("/")
|
||||||
|
|
||||||
|
if is_scp_style_url(remote_url):
|
||||||
|
# handle scp-style URL
|
||||||
|
parts = remote_url.split(":", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
_, path = parts
|
||||||
|
return path.rstrip("/")
|
||||||
|
return remote_url
|
||||||
|
|
||||||
|
|
||||||
|
def _git_remote_path_hash(scm: Optional["Base"]) -> Optional[str]:
|
||||||
|
"""Return a hash of the git remote path."""
|
||||||
|
import hashlib
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scm = SCM(root_dir=Repo.find_root())
|
if remote_url := _git_remote_url(scm):
|
||||||
return type(scm).__name__
|
path = _parse_git_remote_path(remote_url)
|
||||||
except SCMError:
|
h = hashlib.md5(path.encode("utf-8"), usedforsecurity=False) # for FIPS
|
||||||
return NoSCM.__name__
|
return h.hexdigest()
|
||||||
except NotDvcRepoError:
|
except Exception:
|
||||||
pass
|
logger.debug("Failed to get git remote path", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _runtime_info():
|
def _runtime_info():
|
||||||
@@ -112,6 +159,8 @@ def _runtime_info():
|
|||||||
from iterative_telemetry import _generate_ci_id, find_or_create_user_id
|
from iterative_telemetry import _generate_ci_id, find_or_create_user_id
|
||||||
|
|
||||||
from dvc import __version__
|
from dvc import __version__
|
||||||
|
from dvc.info import _get_remotes
|
||||||
|
from dvc.repo import Repo
|
||||||
from dvc.utils import is_binary
|
from dvc.utils import is_binary
|
||||||
|
|
||||||
ci_id = _generate_ci_id()
|
ci_id = _generate_ci_id()
|
||||||
@@ -120,13 +169,24 @@ def _runtime_info():
|
|||||||
else:
|
else:
|
||||||
group_id, user_id = None, find_or_create_user_id()
|
group_id, user_id = None, find_or_create_user_id()
|
||||||
|
|
||||||
|
scm = None
|
||||||
|
remotes = None
|
||||||
|
try:
|
||||||
|
repo = Repo()
|
||||||
|
scm = repo.scm
|
||||||
|
remotes = _get_remotes(repo.config)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
logger.debug("failed to open repo: %s", exc)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"dvc_version": __version__,
|
"dvc_version": __version__,
|
||||||
"is_binary": is_binary(),
|
"is_binary": is_binary(),
|
||||||
"scm_class": _scm_in_use(),
|
"scm_class": _scm_in_use(scm),
|
||||||
"system_info": _system_info(),
|
"system_info": _system_info(),
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"group_id": group_id,
|
"group_id": group_id,
|
||||||
|
"remotes": remotes,
|
||||||
|
"git_remote_hash": _git_remote_path_hash(scm),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -1,7 +1,8 @@
|
|||||||
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||||
|
|
||||||
from .artifacts import artifacts_show
|
from .artifacts import artifacts_show
|
||||||
from .data import get_url, open, read
|
from .data import get_url, open, read # noqa: A004
|
||||||
|
from .dataset import get as get_dataset
|
||||||
from .experiments import exp_save, exp_show
|
from .experiments import exp_save, exp_show
|
||||||
from .scm import all_branches, all_commits, all_tags
|
from .scm import all_branches, all_commits, all_tags
|
||||||
from .show import metrics_show, params_show
|
from .show import metrics_show, params_show
|
||||||
@@ -14,6 +15,7 @@ __all__ = [
|
|||||||
"artifacts_show",
|
"artifacts_show",
|
||||||
"exp_save",
|
"exp_save",
|
||||||
"exp_show",
|
"exp_show",
|
||||||
|
"get_dataset",
|
||||||
"get_url",
|
"get_url",
|
||||||
"metrics_show",
|
"metrics_show",
|
||||||
"open",
|
"open",
|
||||||
|
|||||||
+13
-2
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from dvc.repo import Repo
|
from dvc.repo import Repo
|
||||||
@@ -36,12 +37,22 @@ def artifacts_show(
|
|||||||
if version and stage:
|
if version and stage:
|
||||||
raise ValueError("Artifact version and stage are mutually exclusive.")
|
raise ValueError("Artifact version and stage are mutually exclusive.")
|
||||||
|
|
||||||
|
from dvc.repo.artifacts import Artifacts
|
||||||
|
from dvc.utils import as_posix
|
||||||
|
|
||||||
repo_kwargs: dict[str, Any] = {
|
repo_kwargs: dict[str, Any] = {
|
||||||
"subrepos": True,
|
"subrepos": True,
|
||||||
"uninitialized": True,
|
"uninitialized": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dirname, _ = Artifacts.parse_path(name)
|
||||||
with Repo.open(repo, **repo_kwargs) as _repo:
|
with Repo.open(repo, **repo_kwargs) as _repo:
|
||||||
rev = _repo.artifacts.get_rev(name, version=version, stage=stage)
|
rev = _repo.artifacts.get_rev(name, version=version, stage=stage)
|
||||||
with _repo.switch(rev):
|
with _repo.switch(rev):
|
||||||
path = _repo.artifacts.get_path(name)
|
root = _repo.fs.root_marker
|
||||||
return {"rev": rev, "path": path}
|
_dirname = _repo.fs.join(root, dirname) if dirname else root
|
||||||
|
with Repo(_dirname, fs=_repo.fs, scm=_repo.scm) as r:
|
||||||
|
path = r.artifacts.get_path(name)
|
||||||
|
path = _repo.fs.join(_repo.fs.root_marker, as_posix(path))
|
||||||
|
parts = _repo.fs.relparts(path, _repo.root_dir)
|
||||||
|
return {"rev": rev, "path": os.path.join(*parts)}
|
||||||
|
|||||||
+42
-6
@@ -25,7 +25,14 @@ def _wrap_exceptions(repo, url):
|
|||||||
raise PathMissingError(exc.path, url) from exc
|
raise PathMissingError(exc.path, url) from exc
|
||||||
|
|
||||||
|
|
||||||
def get_url(path, repo=None, rev=None, remote=None):
|
def get_url(
|
||||||
|
path: str,
|
||||||
|
repo: Optional[str] = None,
|
||||||
|
rev: Optional[str] = None,
|
||||||
|
remote: Optional[str] = None,
|
||||||
|
config: Optional[dict[str, Any]] = None,
|
||||||
|
remote_config: Optional[dict[str, Any]] = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Returns the URL to the storage location of a data file or directory tracked
|
Returns the URL to the storage location of a data file or directory tracked
|
||||||
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
|
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
|
||||||
@@ -35,15 +42,44 @@ def get_url(path, repo=None, rev=None, remote=None):
|
|||||||
|
|
||||||
NOTE: This function does not check for the actual existence of the file or
|
NOTE: This function does not check for the actual existence of the file or
|
||||||
directory in the remote storage.
|
directory in the remote storage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): location and file name of the target, relative to the root
|
||||||
|
of `repo`.
|
||||||
|
repo (str, optional): location of the DVC project or Git Repo.
|
||||||
|
Defaults to the current DVC project (found by walking up from the
|
||||||
|
current working directory tree).
|
||||||
|
It can be a URL or a file system path.
|
||||||
|
Both HTTP and SSH protocols are supported for online Git repos
|
||||||
|
(e.g. [user@]server:project.git).
|
||||||
|
rev (str, optional): Any `Git revision`_ such as a branch or tag name,
|
||||||
|
a commit hash or a dvc experiment name.
|
||||||
|
Defaults to HEAD.
|
||||||
|
If `repo` is not a Git repo, this option is ignored.
|
||||||
|
remote (str, optional): Name of the `DVC remote`_ used to form the
|
||||||
|
returned URL string.
|
||||||
|
Defaults to the `default remote`_ of `repo`.
|
||||||
|
For local projects, the cache is tried before the default remote.
|
||||||
|
config(dict, optional): config to be passed to the DVC repository.
|
||||||
|
Defaults to None.
|
||||||
|
remote_config(dict, optional): remote config to be passed to the DVC
|
||||||
|
repository.
|
||||||
|
Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: URL to the file or directory.
|
||||||
"""
|
"""
|
||||||
from dvc.config import NoRemoteError
|
from dvc.config import NoRemoteError
|
||||||
from dvc_data.index import StorageKeyError
|
from dvc_data.index import StorageKeyError
|
||||||
|
|
||||||
repo_kwargs: dict[str, Any] = {}
|
|
||||||
if remote:
|
|
||||||
repo_kwargs["config"] = {"core": {"remote": remote}}
|
|
||||||
with Repo.open(
|
with Repo.open(
|
||||||
repo, rev=rev, subrepos=True, uninitialized=True, **repo_kwargs
|
repo,
|
||||||
|
rev=rev,
|
||||||
|
subrepos=True,
|
||||||
|
uninitialized=True,
|
||||||
|
remote=remote,
|
||||||
|
config=config,
|
||||||
|
remote_config=remote_config,
|
||||||
) as _repo:
|
) as _repo:
|
||||||
index, entry = _repo.get_data_index_entry(path)
|
index, entry = _repo.get_data_index_entry(path)
|
||||||
with reraise(
|
with reraise(
|
||||||
@@ -249,7 +285,7 @@ def _open(
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dvc.fs import FileSystem
|
from dvc.fs import FileSystem
|
||||||
|
|
||||||
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
|
fs: Union[FileSystem, DataFileSystem, DVCFileSystem]
|
||||||
if os.path.isabs(path):
|
if os.path.isabs(path):
|
||||||
fs = DataFileSystem(index=_repo.index.data["local"])
|
fs = DataFileSystem(index=_repo.index.data["local"])
|
||||||
fs_path = path
|
fs_path = path
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from typing import Literal, TypedDict, Union
|
||||||
|
|
||||||
|
|
||||||
|
class DatachainDataset(TypedDict):
|
||||||
|
type: Literal["dc"]
|
||||||
|
name: str
|
||||||
|
version: int
|
||||||
|
|
||||||
|
|
||||||
|
class DVCDataset(TypedDict):
|
||||||
|
type: Literal["dvc"]
|
||||||
|
url: str
|
||||||
|
path: str
|
||||||
|
sha: str
|
||||||
|
|
||||||
|
|
||||||
|
class URLDataset(TypedDict):
|
||||||
|
type: Literal["url"]
|
||||||
|
files: list[str]
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
def get(name: str) -> Union[DatachainDataset, DVCDataset, URLDataset]:
|
||||||
|
from difflib import get_close_matches
|
||||||
|
|
||||||
|
from dvc.fs import get_cloud_fs
|
||||||
|
from dvc.repo import Repo, datasets
|
||||||
|
|
||||||
|
repo = Repo()
|
||||||
|
try:
|
||||||
|
dataset = repo.datasets[name]
|
||||||
|
except datasets.DatasetNotFoundError as e:
|
||||||
|
add_note = getattr(e, "add_note", lambda _: None)
|
||||||
|
if matches := get_close_matches(name, repo.datasets):
|
||||||
|
add_note(f"Did you mean: {matches[0]!r}?")
|
||||||
|
raise
|
||||||
|
|
||||||
|
if dataset._invalidated:
|
||||||
|
raise ValueError(f"dataset not in sync. Sync with 'dvc ds update {name}'.")
|
||||||
|
if not dataset.lock:
|
||||||
|
raise ValueError("missing lock information")
|
||||||
|
if dataset.type == "dvc":
|
||||||
|
return DVCDataset(
|
||||||
|
type="dvc",
|
||||||
|
url=dataset.lock.url,
|
||||||
|
path=dataset.lock.path,
|
||||||
|
sha=dataset.lock.rev_lock,
|
||||||
|
)
|
||||||
|
if dataset.type == "dc":
|
||||||
|
return DatachainDataset(
|
||||||
|
type="dc", name=dataset.name_version[0], version=dataset.lock.version
|
||||||
|
)
|
||||||
|
if dataset.type == "url":
|
||||||
|
fs_cls, _, path = get_cloud_fs(repo.config, url=dataset.lock.url)
|
||||||
|
assert fs_cls
|
||||||
|
join_version = getattr(fs_cls, "join_version", lambda path, _: path)
|
||||||
|
protocol = fs_cls.protocol
|
||||||
|
versioned_path = join_version(path, dataset.lock.meta.version_id)
|
||||||
|
versioned_path = f"{protocol}://{versioned_path}"
|
||||||
|
files = [
|
||||||
|
join_version(
|
||||||
|
fs_cls.join(versioned_path, file.relpath), file.meta.version_id
|
||||||
|
)
|
||||||
|
for file in dataset.lock.files
|
||||||
|
]
|
||||||
|
return URLDataset(type="url", files=files, path=versioned_path)
|
||||||
|
raise AssertionError("unreachable")
|
||||||
+2
-3
@@ -25,10 +25,9 @@ def _postprocess(results):
|
|||||||
(k if counts[k] == 1 else f"{file_name}:{k}"): v
|
(k if counts[k] == 1 else f"{file_name}:{k}"): v
|
||||||
for k, v in file_data["data"].items()
|
for k, v in file_data["data"].items()
|
||||||
}
|
}
|
||||||
processed[rev] = {**processed[rev], **to_merge}
|
processed[rev] = processed[rev] | to_merge
|
||||||
|
|
||||||
if "workspace" in processed:
|
processed.pop("workspace", None)
|
||||||
del processed["workspace"]
|
|
||||||
|
|
||||||
return processed
|
return processed
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -86,7 +86,7 @@ class CacheManager:
|
|||||||
return FileStorage(
|
return FileStorage(
|
||||||
key=(),
|
key=(),
|
||||||
fs=self.local.fs,
|
fs=self.local.fs,
|
||||||
path=self.local.fs.join(self.default_local_cache_dir, self.FS_DIR),
|
path=self.local.fs.join(self.local_cache_dir, self.FS_DIR),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _init_odb(self, schemes):
|
def _init_odb(self, schemes):
|
||||||
|
|||||||
+7
-6
@@ -96,7 +96,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
|||||||
f"See {link} for more info."
|
f"See {link} for more info."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
link = format_link("https://github.com/iterative/dvc/issues")
|
link = format_link("https://github.com/treeverse/dvc/issues")
|
||||||
hint = f"\nPlease report this bug to {link}. Thank you!"
|
hint = f"\nPlease report this bug to {link}. Thank you!"
|
||||||
|
|
||||||
logger.exception(
|
logger.exception(
|
||||||
@@ -157,6 +157,8 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
|||||||
Returns:
|
Returns:
|
||||||
int: command's return code.
|
int: command's return code.
|
||||||
"""
|
"""
|
||||||
|
from contextlib import ExitStack
|
||||||
|
|
||||||
from dvc._debug import debugtools
|
from dvc._debug import debugtools
|
||||||
from dvc.config import ConfigError
|
from dvc.config import ConfigError
|
||||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||||
@@ -173,12 +175,11 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
|||||||
logging.disable(logging.INFO)
|
logging.disable(logging.INFO)
|
||||||
|
|
||||||
args = None
|
args = None
|
||||||
|
stack = ExitStack()
|
||||||
outer_log_level = logger.level
|
|
||||||
level = None
|
|
||||||
try:
|
try:
|
||||||
args = parse_args(argv)
|
args = parse_args(argv)
|
||||||
|
|
||||||
|
level = None
|
||||||
if args.quiet:
|
if args.quiet:
|
||||||
level = logging.CRITICAL
|
level = logging.CRITICAL
|
||||||
elif args.verbose == 1:
|
elif args.verbose == 1:
|
||||||
@@ -187,7 +188,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
|||||||
level = logging.TRACE # type: ignore[attr-defined]
|
level = logging.TRACE # type: ignore[attr-defined]
|
||||||
|
|
||||||
if level is not None:
|
if level is not None:
|
||||||
set_loggers_level(level)
|
stack.enter_context(set_loggers_level(level))
|
||||||
|
|
||||||
if level and level <= logging.DEBUG:
|
if level and level <= logging.DEBUG:
|
||||||
from platform import platform, python_implementation, python_version
|
from platform import platform, python_implementation, python_version
|
||||||
@@ -247,7 +248,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
finally:
|
finally:
|
||||||
logger.setLevel(outer_log_level)
|
stack.close()
|
||||||
|
|
||||||
from dvc.repo.open_repo import clean_repos
|
from dvc.repo.open_repo import clean_repos
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -18,8 +18,11 @@ class CmdBase(ABC):
|
|||||||
|
|
||||||
os.chdir(args.cd)
|
os.chdir(args.cd)
|
||||||
|
|
||||||
self.repo: "Repo" = Repo(uninitialized=self.UNINITIALIZED)
|
self.repo: Repo = Repo(
|
||||||
self.config: "Config" = self.repo.config
|
uninitialized=self.UNINITIALIZED,
|
||||||
|
_wait_for_lock=args.wait_for_lock,
|
||||||
|
)
|
||||||
|
self.config: Config = self.repo.config
|
||||||
self.args = args
|
self.args = args
|
||||||
|
|
||||||
def do_run(self):
|
def do_run(self):
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ _dvc_compgen_exps() {
|
|||||||
local _dvc_exps=($(dvc exp list -q --all-commits --names-only))
|
local _dvc_exps=($(dvc exp list -q --all-commits --names-only))
|
||||||
compgen -W "${_dvc_exps[*]}" -- $1
|
compgen -W "${_dvc_exps[*]}" -- $1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_dvc_compgen_remotes() {
|
||||||
|
local _dvc_remotes=($(dvc remote list | cut -d' ' -f1))
|
||||||
|
compgen -W "${_dvc_remotes[*]}" -- $1
|
||||||
|
}
|
||||||
|
|
||||||
|
_dvc_compgen_config_vars() {
|
||||||
|
compgen -W "${_dvc_config_vars[*]}" -- $1
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ZSH_PREAMBLE = """
|
ZSH_PREAMBLE = """
|
||||||
@@ -55,6 +64,14 @@ _dvc_compadd_stages_and_files() {
|
|||||||
_dvc_compadd_exps() {
|
_dvc_compadd_exps() {
|
||||||
_describe 'experiments' "($(dvc exp list -q --all-commits --names-only))"
|
_describe 'experiments' "($(dvc exp list -q --all-commits --names-only))"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_dvc_compadd_remotes() {
|
||||||
|
_describe 'remotes' "($(dvc remote list | cut -d' ' -f1))"
|
||||||
|
}
|
||||||
|
|
||||||
|
_dvc_compadd_config_vars() {
|
||||||
|
_describe 'config_vars' _dvc_config_vars
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PREAMBLE = {
|
PREAMBLE = {
|
||||||
@@ -71,3 +88,22 @@ DVCFILES_AND_STAGE = {
|
|||||||
"zsh": "_dvc_compadd_stages_and_files",
|
"zsh": "_dvc_compadd_stages_and_files",
|
||||||
}
|
}
|
||||||
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
|
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
|
||||||
|
REMOTE = {"bash": "_dvc_compgen_remotes", "zsh": "_dvc_compadd_remotes"}
|
||||||
|
CONFIG_VARS = {"bash": "_dvc_compgen_config_vars", "zsh": "_dvc_compadd_config_vars"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_preamble() -> dict[str, str]:
|
||||||
|
from dvc.config_schema import config_vars_for_completion
|
||||||
|
|
||||||
|
ret: dict[str, str] = {}
|
||||||
|
config_vars = list(config_vars_for_completion())
|
||||||
|
|
||||||
|
nl = "\n\t".expandtabs(4)
|
||||||
|
config_vars_arr = f"""
|
||||||
|
_dvc_config_vars=(
|
||||||
|
{nl.join(config_vars)}
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
for shell, preamble in PREAMBLE.items():
|
||||||
|
ret[shell] = config_vars_arr + preamble
|
||||||
|
return ret
|
||||||
|
|||||||
+42
-31
@@ -1,4 +1,5 @@
|
|||||||
"""Main parser for the dvc cli."""
|
"""Main parser for the dvc cli."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
@@ -17,6 +18,7 @@ from dvc.commands import (
|
|||||||
dag,
|
dag,
|
||||||
data,
|
data,
|
||||||
data_sync,
|
data_sync,
|
||||||
|
dataset,
|
||||||
destroy,
|
destroy,
|
||||||
diff,
|
diff,
|
||||||
du,
|
du,
|
||||||
@@ -55,48 +57,49 @@ from . import DvcParserError, formatter
|
|||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
COMMANDS = [
|
COMMANDS = [
|
||||||
init,
|
add,
|
||||||
queue,
|
artifacts,
|
||||||
|
cache,
|
||||||
|
check_ignore,
|
||||||
|
checkout,
|
||||||
|
commit,
|
||||||
|
completion,
|
||||||
|
config,
|
||||||
|
daemon,
|
||||||
|
dag,
|
||||||
|
data,
|
||||||
|
data_sync,
|
||||||
|
dataset,
|
||||||
|
destroy,
|
||||||
|
diff,
|
||||||
|
du,
|
||||||
|
experiments,
|
||||||
|
freeze,
|
||||||
|
gc,
|
||||||
get,
|
get,
|
||||||
get_url,
|
get_url,
|
||||||
destroy,
|
git_hook,
|
||||||
add,
|
|
||||||
remove,
|
|
||||||
move,
|
|
||||||
unprotect,
|
|
||||||
repro,
|
|
||||||
data_sync,
|
|
||||||
gc,
|
|
||||||
imp,
|
imp,
|
||||||
imp_db,
|
imp_db,
|
||||||
imp_url,
|
imp_url,
|
||||||
config,
|
init,
|
||||||
checkout,
|
|
||||||
remote,
|
|
||||||
cache,
|
|
||||||
metrics,
|
|
||||||
params,
|
|
||||||
install,
|
install,
|
||||||
root,
|
|
||||||
ls,
|
ls,
|
||||||
ls_url,
|
ls_url,
|
||||||
freeze,
|
metrics,
|
||||||
dag,
|
move,
|
||||||
daemon,
|
params,
|
||||||
commit,
|
|
||||||
completion,
|
|
||||||
diff,
|
|
||||||
version,
|
|
||||||
update,
|
|
||||||
git_hook,
|
|
||||||
plots,
|
plots,
|
||||||
|
queue,
|
||||||
|
remote,
|
||||||
|
remove,
|
||||||
|
repro,
|
||||||
|
root,
|
||||||
stage,
|
stage,
|
||||||
experiments,
|
|
||||||
check_ignore,
|
|
||||||
data,
|
|
||||||
artifacts,
|
|
||||||
studio,
|
studio,
|
||||||
du,
|
unprotect,
|
||||||
|
update,
|
||||||
|
version,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -197,6 +200,14 @@ def get_main_parser():
|
|||||||
type=str,
|
type=str,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--wait-for-lock",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Wait for the lock if it is already held by another process, instead of"
|
||||||
|
" failing immediately.",
|
||||||
|
)
|
||||||
|
|
||||||
# Sub commands
|
# Sub commands
|
||||||
subparsers = parser.add_subparsers(
|
subparsers = parser.add_subparsers(
|
||||||
title="Available Commands",
|
title="Available Commands",
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ def hide_subparsers_from_help(subparsers):
|
|||||||
# from the 'positional arguments' choices list
|
# from the 'positional arguments' choices list
|
||||||
# see: https://bugs.python.org/issue22848
|
# see: https://bugs.python.org/issue22848
|
||||||
# Need to set `add_help=False`, but avoid setting `help`
|
# Need to set `add_help=False`, but avoid setting `help`
|
||||||
# (not even to `argparse.SUPPPRESS`).
|
# (not even to `argparse.SUPPRESS`).
|
||||||
# NOTE: The argument is the parent subparser, not the subcommand parser.
|
# NOTE: The argument is the parent subparser, not the subcommand parser.
|
||||||
cmds = [cmd for cmd, parser in subparsers.choices.items() if parser.add_help]
|
cmds = [cmd for cmd, parser in subparsers.choices.items() if parser.add_help]
|
||||||
subparsers.metavar = "{{{}}}".format(",".join(cmds))
|
subparsers.metavar = "{{{}}}".format(",".join(cmds))
|
||||||
|
|||||||
+9
-1
@@ -51,6 +51,7 @@ class CmdAdd(CmdBase):
|
|||||||
to_remote=self.args.to_remote,
|
to_remote=self.args.to_remote,
|
||||||
remote_jobs=self.args.remote_jobs,
|
remote_jobs=self.args.remote_jobs,
|
||||||
force=self.args.force,
|
force=self.args.force,
|
||||||
|
relink=self.args.relink,
|
||||||
)
|
)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.exception("")
|
logger.exception("")
|
||||||
@@ -100,7 +101,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"--remote",
|
"--remote",
|
||||||
help="Remote storage to download to",
|
help="Remote storage to download to",
|
||||||
metavar="<name>",
|
metavar="<name>",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--remote-jobs",
|
"--remote-jobs",
|
||||||
type=int,
|
type=int,
|
||||||
@@ -119,6 +120,13 @@ def add_parser(subparsers, parent_parser):
|
|||||||
default=False,
|
default=False,
|
||||||
help="Override local file or folder if exists.",
|
help="Override local file or folder if exists.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-relink",
|
||||||
|
dest="relink",
|
||||||
|
action="store_false",
|
||||||
|
help="Don't recreate links from cache to workspace.",
|
||||||
|
)
|
||||||
|
parser.set_defaults(relink=True)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"targets", nargs="+", help="Input files/directories to add."
|
"targets", nargs="+", help="Input files/directories to add."
|
||||||
).complete = completion.FILE
|
).complete = completion.FILE
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ class CmdArtifactsGet(CmdBaseNoRepo):
|
|||||||
from dvc.scm import CloneError
|
from dvc.scm import CloneError
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
if self.args.show_url:
|
||||||
|
return self._show_url()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
count, out = Artifacts.get(
|
count, out = Artifacts.get(
|
||||||
self.args.url,
|
self.args.url,
|
||||||
@@ -36,6 +39,28 @@ class CmdArtifactsGet(CmdBaseNoRepo):
|
|||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
def _show_url(self):
|
||||||
|
from dvc.api import artifacts_show, get_url
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
artifact = artifacts_show(
|
||||||
|
self.args.name,
|
||||||
|
version=self.args.rev,
|
||||||
|
stage=self.args.stage,
|
||||||
|
repo=self.args.url,
|
||||||
|
)
|
||||||
|
|
||||||
|
url = get_url(
|
||||||
|
artifact["path"],
|
||||||
|
repo=self.args.url,
|
||||||
|
rev=artifact["rev"],
|
||||||
|
remote=self.args.remote,
|
||||||
|
remote_config=self.args.remote_config,
|
||||||
|
)
|
||||||
|
ui.write(url, force=True)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
ARTIFACTS_HELP = "DVC model registry artifact commands."
|
ARTIFACTS_HELP = "DVC model registry artifact commands."
|
||||||
@@ -84,6 +109,14 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help="Destination path to download artifact to",
|
help="Destination path to download artifact to",
|
||||||
metavar="<path>",
|
metavar="<path>",
|
||||||
).complete = completion.DIR
|
).complete = completion.DIR
|
||||||
|
get_parser.add_argument(
|
||||||
|
"--show-url",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Print the storage location (URL) the target data would be "
|
||||||
|
"downloaded from, and exit."
|
||||||
|
),
|
||||||
|
)
|
||||||
get_parser.add_argument(
|
get_parser.add_argument(
|
||||||
"-j",
|
"-j",
|
||||||
"--jobs",
|
"--jobs",
|
||||||
@@ -116,7 +149,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"Remote name to set as a default in the target repository "
|
"Remote name to set as a default in the target repository "
|
||||||
"(only applicable when downloading from DVC remote)."
|
"(only applicable when downloading from DVC remote)."
|
||||||
),
|
),
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
get_parser.add_argument(
|
get_parser.add_argument(
|
||||||
"--remote-config",
|
"--remote-config",
|
||||||
type=str,
|
type=str,
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dvc.cli import completion, formatter
|
from dvc.cli import completion, formatter
|
||||||
from dvc.cli.command import CmdBase
|
from dvc.cli.command import CmdBase
|
||||||
from dvc.cli.utils import append_doc_link
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dvc.ignore import CheckIgnoreResult
|
||||||
|
|
||||||
|
|
||||||
class CmdCheckIgnore(CmdBase):
|
class CmdCheckIgnore(CmdBase):
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
super().__init__(args)
|
super().__init__(args)
|
||||||
self.ignore_filter = self.repo.dvcignore
|
self.ignore_filter = self.repo.dvcignore
|
||||||
|
|
||||||
def _show_results(self, result):
|
def _show_results(self, result: "CheckIgnoreResult"):
|
||||||
if not result.match and not self.args.non_matching:
|
if not result.match and not self.args.details:
|
||||||
return
|
return None
|
||||||
|
|
||||||
if self.args.details:
|
if self.args.details:
|
||||||
patterns = result.patterns
|
pattern_infos = result.pattern_infos
|
||||||
|
patterns = [str(pi) for pi in pattern_infos]
|
||||||
|
if not patterns and self.args.non_matching:
|
||||||
|
patterns = ["::"]
|
||||||
if not self.args.all:
|
if not self.args.all:
|
||||||
patterns = patterns[-1:]
|
patterns = patterns[-1:]
|
||||||
|
|
||||||
@@ -22,13 +30,11 @@ class CmdCheckIgnore(CmdBase):
|
|||||||
ui.write(pattern, result.file, sep="\t")
|
ui.write(pattern, result.file, sep="\t")
|
||||||
else:
|
else:
|
||||||
ui.write(result.file)
|
ui.write(result.file)
|
||||||
|
return bool(result.pattern_infos)
|
||||||
|
|
||||||
def _check_one_file(self, target):
|
def _check_one_file(self, target):
|
||||||
result = self.ignore_filter.check_ignore(target)
|
result = self.ignore_filter.check_ignore(target)
|
||||||
self._show_results(result)
|
return bool(self._show_results(result))
|
||||||
if result.match:
|
|
||||||
return 0
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def _interactive_mode(self):
|
def _interactive_mode(self):
|
||||||
ret = 1
|
ret = 1
|
||||||
@@ -39,14 +45,14 @@ class CmdCheckIgnore(CmdBase):
|
|||||||
break
|
break
|
||||||
if not target:
|
if not target:
|
||||||
break
|
break
|
||||||
if not self._check_one_file(target):
|
if self._check_one_file(target):
|
||||||
ret = 0
|
ret = 0
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _normal_mode(self):
|
def _normal_mode(self):
|
||||||
ret = 1
|
ret = 1
|
||||||
for target in self.args.targets:
|
for target in self.args.targets:
|
||||||
if not self._check_one_file(target):
|
if self._check_one_file(target):
|
||||||
ret = 0
|
ret = 0
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -76,6 +82,8 @@ class CmdCheckIgnore(CmdBase):
|
|||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
|
import argparse
|
||||||
|
|
||||||
ADD_HELP = "Check whether files or directories are excluded due to `.dvcignore`."
|
ADD_HELP = "Check whether files or directories are excluded due to `.dvcignore`."
|
||||||
|
|
||||||
parser = subparsers.add_parser(
|
parser = subparsers.add_parser(
|
||||||
@@ -93,14 +101,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help="Show the exclude patterns along with each target path.",
|
help="Show the exclude patterns along with each target path.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-a",
|
"-a", "--all", action="store_true", default=False, help=argparse.SUPPRESS
|
||||||
"--all",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help=(
|
|
||||||
"Include the target paths which don't match any pattern "
|
|
||||||
"in the `--details` list."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-n",
|
"-n",
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import operator
|
|
||||||
|
|
||||||
from dvc.cli import completion, formatter
|
from dvc.cli import completion, formatter
|
||||||
from dvc.cli.command import CmdBase
|
from dvc.cli.command import CmdBase
|
||||||
from dvc.cli.utils import append_doc_link
|
from dvc.cli.utils import append_doc_link
|
||||||
@@ -30,7 +28,7 @@ class CmdCheckout(CmdBase):
|
|||||||
|
|
||||||
stats, exc = None, None
|
stats, exc = None, None
|
||||||
try:
|
try:
|
||||||
stats = self.repo.checkout(
|
result = self.repo.checkout(
|
||||||
targets=self.args.targets,
|
targets=self.args.targets,
|
||||||
with_deps=self.args.with_deps,
|
with_deps=self.args.with_deps,
|
||||||
force=self.args.force,
|
force=self.args.force,
|
||||||
@@ -40,14 +38,17 @@ class CmdCheckout(CmdBase):
|
|||||||
)
|
)
|
||||||
except CheckoutError as _exc:
|
except CheckoutError as _exc:
|
||||||
exc = _exc
|
exc = _exc
|
||||||
stats = exc.stats
|
result = exc.result
|
||||||
|
|
||||||
if self.args.summary:
|
if self.args.summary:
|
||||||
default_message = "No changes."
|
default_message = "No changes."
|
||||||
msg = get_summary(sorted(stats.items(), key=operator.itemgetter(0)))
|
stats = result["stats"]
|
||||||
|
assert isinstance(stats, dict)
|
||||||
|
msg = get_summary(stats.items())
|
||||||
ui.write(msg or default_message)
|
ui.write(msg or default_message)
|
||||||
else:
|
else:
|
||||||
log_changes(stats)
|
result.pop("stats", {})
|
||||||
|
log_changes(result)
|
||||||
|
|
||||||
if exc:
|
if exc:
|
||||||
raise exc
|
raise exc
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class CmdCommit(CmdBase):
|
|||||||
with_deps=self.args.with_deps,
|
with_deps=self.args.with_deps,
|
||||||
recursive=self.args.recursive,
|
recursive=self.args.recursive,
|
||||||
force=self.args.force,
|
force=self.args.force,
|
||||||
|
relink=self.args.relink,
|
||||||
)
|
)
|
||||||
except DvcException:
|
except DvcException:
|
||||||
logger.exception("failed to commit%s", (" " + target) if target else "")
|
logger.exception("failed to commit%s", (" " + target) if target else "")
|
||||||
@@ -64,6 +65,13 @@ def add_parser(subparsers, parent_parser):
|
|||||||
default=False,
|
default=False,
|
||||||
help="Commit cache for subdirectories of the specified directory.",
|
help="Commit cache for subdirectories of the specified directory.",
|
||||||
)
|
)
|
||||||
|
commit_parser.add_argument(
|
||||||
|
"--no-relink",
|
||||||
|
dest="relink",
|
||||||
|
action="store_false",
|
||||||
|
help="Don't recreate links from cache to workspace.",
|
||||||
|
)
|
||||||
|
commit_parser.set_defaults(relink=True)
|
||||||
commit_parser.add_argument(
|
commit_parser.add_argument(
|
||||||
"targets",
|
"targets",
|
||||||
nargs="*",
|
nargs="*",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from dvc.cli import formatter
|
from dvc.cli import formatter
|
||||||
from dvc.cli.command import CmdBaseNoRepo
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
from dvc.cli.completion import PREAMBLE
|
from dvc.cli.completion import get_preamble
|
||||||
from dvc.cli.utils import append_doc_link
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
@@ -17,7 +17,7 @@ class CmdCompletion(CmdBaseNoRepo):
|
|||||||
|
|
||||||
shell = self.args.shell
|
shell = self.args.shell
|
||||||
parser = self.args.parser
|
parser = self.args.parser
|
||||||
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
|
script = shtab.complete(parser, shell=shell, preamble=get_preamble())
|
||||||
ui.write(script, force=True)
|
ui.write(script, force=True)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
|
|
||||||
from funcy import set_in
|
from funcy import set_in
|
||||||
|
|
||||||
from dvc.cli import formatter
|
from dvc.cli import completion, formatter
|
||||||
from dvc.cli.command import CmdBaseNoRepo
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
from dvc.cli.utils import append_doc_link
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
@@ -224,7 +224,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
nargs="?",
|
nargs="?",
|
||||||
type=_name_type,
|
type=_name_type,
|
||||||
help="Option name (section.option or remote.name.option).",
|
help="Option name (section.option or remote.name.option).",
|
||||||
)
|
).complete = completion.CONFIG_VARS
|
||||||
config_parser.add_argument("value", nargs="?", help="Option value.")
|
config_parser.add_argument("value", nargs="?", help="Option value.")
|
||||||
config_parser.add_argument(
|
config_parser.add_argument(
|
||||||
"-l",
|
"-l",
|
||||||
|
|||||||
+72
-3
@@ -17,7 +17,7 @@ def _show_ascii(graph: "DiGraph"):
|
|||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
for pipeline in pipelines:
|
for pipeline in pipelines:
|
||||||
ret.append(draw(pipeline.nodes, pipeline.edges))
|
ret.append(draw(pipeline.nodes, pipeline.edges)) # noqa: PERF401
|
||||||
|
|
||||||
return "\n".join(ret)
|
return "\n".join(ret)
|
||||||
|
|
||||||
@@ -127,19 +127,80 @@ def _filter(graph, targets, full):
|
|||||||
return new_graph
|
return new_graph
|
||||||
|
|
||||||
|
|
||||||
def _build(repo, target=None, full=False, outs=False):
|
def _is_foreach_matrix_stage(node, join_string):
|
||||||
|
if node.endswith(".dvc"):
|
||||||
|
return False
|
||||||
|
return join_string in node
|
||||||
|
|
||||||
|
|
||||||
|
def _collapse_foreach_matrix_get_nodes(graph):
|
||||||
|
from dvc.parsing import JOIN
|
||||||
|
|
||||||
|
new_nodes = set()
|
||||||
|
nodes_to_remove = set()
|
||||||
|
for _node in list(graph.nodes):
|
||||||
|
if not _is_foreach_matrix_stage(_node, JOIN):
|
||||||
|
continue
|
||||||
|
nodes_to_remove.add(_node)
|
||||||
|
new_nodes.add(_node.split(JOIN)[0])
|
||||||
|
return new_nodes, nodes_to_remove
|
||||||
|
|
||||||
|
|
||||||
|
def _collapse_foreach_matrix_get_edges(graph):
|
||||||
|
from dvc.parsing import JOIN
|
||||||
|
|
||||||
|
new_edges = set()
|
||||||
|
edges_to_remove = set()
|
||||||
|
for _e1, _e2 in list(graph.edges):
|
||||||
|
_replace = False
|
||||||
|
_new_e1 = _e1
|
||||||
|
_new_e2 = _e2
|
||||||
|
if _is_foreach_matrix_stage(_e1, JOIN):
|
||||||
|
_new_e1 = _e1.split(JOIN)[0]
|
||||||
|
_replace = True
|
||||||
|
if _is_foreach_matrix_stage(_e2, JOIN):
|
||||||
|
_new_e2 = _e2.split(JOIN)[0]
|
||||||
|
_replace = True
|
||||||
|
if _replace:
|
||||||
|
edges_to_remove.add((_e1, _e2))
|
||||||
|
new_edges.add((_new_e1, _new_e2))
|
||||||
|
return new_edges, edges_to_remove
|
||||||
|
|
||||||
|
|
||||||
|
def _collapse_foreach_matrix(graph):
|
||||||
|
new_nodes, nodes_to_remove = _collapse_foreach_matrix_get_nodes(graph)
|
||||||
|
new_edges, edges_to_remove = _collapse_foreach_matrix_get_edges(graph)
|
||||||
|
new_graph = graph.copy()
|
||||||
|
new_graph.remove_edges_from(edges_to_remove)
|
||||||
|
new_graph.add_nodes_from(new_nodes)
|
||||||
|
new_graph.add_edges_from(new_edges)
|
||||||
|
new_graph.remove_nodes_from(nodes_to_remove)
|
||||||
|
return new_graph
|
||||||
|
|
||||||
|
|
||||||
|
def _build(repo, target=None, full=False, outs=False, collapse_foreach_matrix=False):
|
||||||
targets = _collect_targets(repo, target, outs)
|
targets = _collect_targets(repo, target, outs)
|
||||||
graph = _transform(repo.index, outs)
|
graph = _transform(repo.index, outs)
|
||||||
return _filter(graph, targets, full)
|
filtered_graph = _filter(graph, targets, full)
|
||||||
|
if collapse_foreach_matrix:
|
||||||
|
filtered_graph = _collapse_foreach_matrix(filtered_graph)
|
||||||
|
return filtered_graph
|
||||||
|
|
||||||
|
|
||||||
class CmdDAG(CmdBase):
|
class CmdDAG(CmdBase):
|
||||||
def run(self):
|
def run(self):
|
||||||
|
from dvc.exceptions import InvalidArgumentError
|
||||||
|
|
||||||
|
if self.args.outs and self.args.collapse_foreach_matrix:
|
||||||
|
raise InvalidArgumentError(
|
||||||
|
"`--outs` and `--collapse-foreach-matrix` are mutually exclusive"
|
||||||
|
)
|
||||||
graph = _build(
|
graph = _build(
|
||||||
self.repo,
|
self.repo,
|
||||||
target=self.args.target,
|
target=self.args.target,
|
||||||
full=self.args.full,
|
full=self.args.full,
|
||||||
outs=self.args.outs,
|
outs=self.args.outs,
|
||||||
|
collapse_foreach_matrix=self.args.collapse_foreach_matrix,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.args.dot:
|
if self.args.dot:
|
||||||
@@ -181,6 +242,14 @@ def add_parser(subparsers, parent_parser):
|
|||||||
dest="markdown",
|
dest="markdown",
|
||||||
help="Print DAG with mermaid format wrapped in Markdown block.",
|
help="Print DAG with mermaid format wrapped in Markdown block.",
|
||||||
)
|
)
|
||||||
|
dag_parser.add_argument(
|
||||||
|
"--collapse-foreach-matrix",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=(
|
||||||
|
"Collapse stages from each foreach/matrix definition into a single node."
|
||||||
|
),
|
||||||
|
)
|
||||||
dag_parser.add_argument(
|
dag_parser.add_argument(
|
||||||
"--full",
|
"--full",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
+36
-12
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, ClassVar
|
|||||||
|
|
||||||
from funcy import chunks, compact, log_durations
|
from funcy import chunks, compact, log_durations
|
||||||
|
|
||||||
from dvc.cli import formatter
|
from dvc.cli import completion, formatter
|
||||||
from dvc.cli.command import CmdBase
|
from dvc.cli.command import CmdBase
|
||||||
from dvc.cli.utils import append_doc_link
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
@@ -10,6 +10,7 @@ from dvc.ui import ui
|
|||||||
from dvc.utils import colorize
|
from dvc.utils import colorize
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from dvc.repo.data import GitInfo
|
||||||
from dvc.repo.data import Status as DataStatus
|
from dvc.repo.data import Status as DataStatus
|
||||||
|
|
||||||
|
|
||||||
@@ -55,20 +56,15 @@ class CmdDataStatus(CmdBase):
|
|||||||
def _process_status(status: "DataStatus"):
|
def _process_status(status: "DataStatus"):
|
||||||
"""Flatten stage status, and filter empty stage status contents."""
|
"""Flatten stage status, and filter empty stage status contents."""
|
||||||
for stage, stage_status in status.items():
|
for stage, stage_status in status.items():
|
||||||
items = stage_status
|
if not stage_status or (
|
||||||
if isinstance(stage_status, dict):
|
isinstance(stage_status, dict) and not any(stage_status.values())
|
||||||
items = {
|
):
|
||||||
file: state
|
|
||||||
for state, files in stage_status.items()
|
|
||||||
for file in files
|
|
||||||
}
|
|
||||||
if not items:
|
|
||||||
continue
|
continue
|
||||||
yield stage, items
|
yield stage, stage_status
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _show_status(cls, status: "DataStatus") -> int: # noqa: C901
|
def _show_status(cls, status: "DataStatus") -> int: # noqa: C901
|
||||||
git_info = status.pop("git") # type: ignore[misc]
|
git_info: GitInfo = status.pop("git") # type: ignore[misc]
|
||||||
result = dict(cls._process_status(status))
|
result = dict(cls._process_status(status))
|
||||||
if not result:
|
if not result:
|
||||||
no_changes = "No changes"
|
no_changes = "No changes"
|
||||||
@@ -90,7 +86,16 @@ class CmdDataStatus(CmdBase):
|
|||||||
ui.write(f" ({hint})")
|
ui.write(f" ({hint})")
|
||||||
|
|
||||||
if isinstance(stage_status, dict):
|
if isinstance(stage_status, dict):
|
||||||
items = [f"{state}: {file}" for file, state in stage_status.items()]
|
items = [
|
||||||
|
f"{state}: "
|
||||||
|
+ (
|
||||||
|
" -> ".join(change.values())
|
||||||
|
if isinstance(change, dict)
|
||||||
|
else change
|
||||||
|
)
|
||||||
|
for state, changes in stage_status.items()
|
||||||
|
for change in changes
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
items = stage_status
|
items = stage_status
|
||||||
|
|
||||||
@@ -108,8 +113,10 @@ class CmdDataStatus(CmdBase):
|
|||||||
def run(self) -> int:
|
def run(self) -> int:
|
||||||
with log_durations(logger.trace, "in data_status"):
|
with log_durations(logger.trace, "in data_status"):
|
||||||
status = self.repo.data_status(
|
status = self.repo.data_status(
|
||||||
|
targets=self.args.targets,
|
||||||
granular=self.args.granular,
|
granular=self.args.granular,
|
||||||
untracked_files=self.args.untracked_files,
|
untracked_files=self.args.untracked_files,
|
||||||
|
remote=self.args.remote,
|
||||||
not_in_remote=self.args.not_in_remote,
|
not_in_remote=self.args.not_in_remote,
|
||||||
remote_refresh=self.args.remote_refresh,
|
remote_refresh=self.args.remote_refresh,
|
||||||
)
|
)
|
||||||
@@ -126,9 +133,12 @@ class CmdDataStatus(CmdBase):
|
|||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
|
data_help = "Commands related to data management."
|
||||||
data_parser = subparsers.add_parser(
|
data_parser = subparsers.add_parser(
|
||||||
"data",
|
"data",
|
||||||
parents=[parent_parser],
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(data_help, "data/status"),
|
||||||
|
help=data_help,
|
||||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
data_subparsers = data_parser.add_subparsers(
|
data_subparsers = data_parser.add_subparsers(
|
||||||
@@ -147,6 +157,14 @@ def add_parser(subparsers, parent_parser):
|
|||||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||||
help=DATA_STATUS_HELP,
|
help=DATA_STATUS_HELP,
|
||||||
)
|
)
|
||||||
|
data_status_parser.add_argument(
|
||||||
|
"targets",
|
||||||
|
nargs="*",
|
||||||
|
help=(
|
||||||
|
"Limit command scope to these tracked files/directories, "
|
||||||
|
".dvc files and stage names."
|
||||||
|
),
|
||||||
|
).complete = completion.FILE # type: ignore[attr-defined]
|
||||||
data_status_parser.add_argument(
|
data_status_parser.add_argument(
|
||||||
"--json",
|
"--json",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -173,6 +191,12 @@ def add_parser(subparsers, parent_parser):
|
|||||||
nargs="?",
|
nargs="?",
|
||||||
help="Show untracked files.",
|
help="Show untracked files.",
|
||||||
)
|
)
|
||||||
|
data_status_parser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--remote",
|
||||||
|
help="Remote storage to check (only applicable with --not-in-remote).",
|
||||||
|
metavar="<name>",
|
||||||
|
).complete = completion.REMOTE
|
||||||
data_status_parser.add_argument(
|
data_status_parser.add_argument(
|
||||||
"--not-in-remote",
|
"--not-in-remote",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
+20
-13
@@ -22,17 +22,18 @@ class CmdDataBase(CmdBase):
|
|||||||
|
|
||||||
|
|
||||||
class CmdDataPull(CmdDataBase):
|
class CmdDataPull(CmdDataBase):
|
||||||
def log_summary(self, stats):
|
def log_summary(self, result):
|
||||||
from dvc.commands.checkout import log_changes
|
from dvc.commands.checkout import log_changes
|
||||||
|
|
||||||
log_changes(stats)
|
stats = result.pop("stats", {})
|
||||||
|
log_changes(result)
|
||||||
super().log_summary(stats)
|
super().log_summary(stats)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
from dvc.exceptions import CheckoutError, DvcException
|
from dvc.exceptions import CheckoutError, DvcException
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stats = self.repo.pull(
|
result = self.repo.pull(
|
||||||
targets=self.args.targets,
|
targets=self.args.targets,
|
||||||
jobs=self.args.jobs,
|
jobs=self.args.jobs,
|
||||||
remote=self.args.remote,
|
remote=self.args.remote,
|
||||||
@@ -46,10 +47,10 @@ class CmdDataPull(CmdDataBase):
|
|||||||
glob=self.args.glob,
|
glob=self.args.glob,
|
||||||
allow_missing=self.args.allow_missing,
|
allow_missing=self.args.allow_missing,
|
||||||
)
|
)
|
||||||
self.log_summary(stats)
|
self.log_summary(result)
|
||||||
except (CheckoutError, DvcException) as exc:
|
except (CheckoutError, DvcException) as exc:
|
||||||
if stats := getattr(exc, "stats", {}):
|
if result := getattr(exc, "result", {}):
|
||||||
self.log_summary(stats)
|
self.log_summary(result)
|
||||||
logger.exception("failed to pull data from the cloud")
|
logger.exception("failed to pull data from the cloud")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ def add_parser(subparsers, _parent_parser):
|
|||||||
)
|
)
|
||||||
pull_parser.add_argument(
|
pull_parser.add_argument(
|
||||||
"-r", "--remote", help="Remote storage to pull from", metavar="<name>"
|
"-r", "--remote", help="Remote storage to pull from", metavar="<name>"
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
pull_parser.add_argument(
|
pull_parser.add_argument(
|
||||||
"-a",
|
"-a",
|
||||||
"--all-branches",
|
"--all-branches",
|
||||||
@@ -194,7 +195,7 @@ def add_parser(subparsers, _parent_parser):
|
|||||||
)
|
)
|
||||||
pull_parser.add_argument(
|
pull_parser.add_argument(
|
||||||
"--run-cache",
|
"--run-cache",
|
||||||
action="store_true",
|
action=argparse.BooleanOptionalAction,
|
||||||
default=False,
|
default=False,
|
||||||
help="Fetch run history for all stages.",
|
help="Fetch run history for all stages.",
|
||||||
)
|
)
|
||||||
@@ -224,7 +225,7 @@ def add_parser(subparsers, _parent_parser):
|
|||||||
)
|
)
|
||||||
push_parser.add_argument(
|
push_parser.add_argument(
|
||||||
"-r", "--remote", help="Remote storage to push to", metavar="<name>"
|
"-r", "--remote", help="Remote storage to push to", metavar="<name>"
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
push_parser.add_argument(
|
push_parser.add_argument(
|
||||||
"-a",
|
"-a",
|
||||||
"--all-branches",
|
"--all-branches",
|
||||||
@@ -262,7 +263,7 @@ def add_parser(subparsers, _parent_parser):
|
|||||||
)
|
)
|
||||||
push_parser.add_argument(
|
push_parser.add_argument(
|
||||||
"--run-cache",
|
"--run-cache",
|
||||||
action="store_true",
|
action=argparse.BooleanOptionalAction,
|
||||||
default=False,
|
default=False,
|
||||||
help="Push run history for all stages.",
|
help="Push run history for all stages.",
|
||||||
)
|
)
|
||||||
@@ -286,7 +287,7 @@ def add_parser(subparsers, _parent_parser):
|
|||||||
)
|
)
|
||||||
fetch_parser.add_argument(
|
fetch_parser.add_argument(
|
||||||
"-r", "--remote", help="Remote storage to fetch from", metavar="<name>"
|
"-r", "--remote", help="Remote storage to fetch from", metavar="<name>"
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
fetch_parser.add_argument(
|
fetch_parser.add_argument(
|
||||||
"-a",
|
"-a",
|
||||||
"--all-branches",
|
"--all-branches",
|
||||||
@@ -324,7 +325,7 @@ def add_parser(subparsers, _parent_parser):
|
|||||||
)
|
)
|
||||||
fetch_parser.add_argument(
|
fetch_parser.add_argument(
|
||||||
"--run-cache",
|
"--run-cache",
|
||||||
action="store_true",
|
action=argparse.BooleanOptionalAction,
|
||||||
default=False,
|
default=False,
|
||||||
help="Fetch run history for all stages.",
|
help="Fetch run history for all stages.",
|
||||||
)
|
)
|
||||||
@@ -379,7 +380,7 @@ def add_parser(subparsers, _parent_parser):
|
|||||||
"--remote",
|
"--remote",
|
||||||
help="Remote storage to compare local cache to",
|
help="Remote storage to compare local cache to",
|
||||||
metavar="<name>",
|
metavar="<name>",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
status_parser.add_argument(
|
status_parser.add_argument(
|
||||||
"-a",
|
"-a",
|
||||||
"--all-branches",
|
"--all-branches",
|
||||||
@@ -429,5 +430,11 @@ def add_parser(subparsers, _parent_parser):
|
|||||||
default=False,
|
default=False,
|
||||||
help="Show status in JSON format.",
|
help="Show status in JSON format.",
|
||||||
)
|
)
|
||||||
|
status_parser.add_argument(
|
||||||
|
"--no-updates",
|
||||||
|
dest="check_updates",
|
||||||
|
action="store_false",
|
||||||
|
help="Ignore updates to imported data.",
|
||||||
|
)
|
||||||
|
|
||||||
status_parser.set_defaults(func=CmdDataStatus)
|
status_parser.set_defaults(func=CmdDataStatus)
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from dvc.cli import formatter
|
||||||
|
from dvc.cli.command import CmdBase
|
||||||
|
from dvc.cli.utils import append_doc_link
|
||||||
|
from dvc.exceptions import DvcException
|
||||||
|
from dvc.log import logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from dvc.repo.datasets import Dataset, FileInfo
|
||||||
|
|
||||||
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def diff_files(old: list["FileInfo"], new: list["FileInfo"]) -> dict[str, list[str]]:
|
||||||
|
old_files = {d.relpath: d for d in old}
|
||||||
|
new_files = {d.relpath: d for d in new}
|
||||||
|
rest = old_files.keys() & new_files.keys()
|
||||||
|
return {
|
||||||
|
"added": list(new_files.keys() - old_files.keys()),
|
||||||
|
"deleted": list(old_files.keys() - new_files.keys()),
|
||||||
|
"modified": [p for p in rest if new_files[p] != old_files[p]],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CmdDatasetAdd(CmdBase):
|
||||||
|
@classmethod
|
||||||
|
def display(cls, name: str, dataset: "Dataset", action: str = "Adding"):
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
assert dataset.lock
|
||||||
|
|
||||||
|
url = dataset.spec.url
|
||||||
|
ver: str = ""
|
||||||
|
if dataset.type == "dc":
|
||||||
|
ver = f"v{dataset.lock.version}"
|
||||||
|
if dataset.type == "dvc":
|
||||||
|
if dataset.lock.path:
|
||||||
|
url = f"{url}:/{dataset.lock.path.lstrip('/')}"
|
||||||
|
if rev := dataset.lock.rev:
|
||||||
|
ver = rev
|
||||||
|
|
||||||
|
ver_part: Optional[Text] = None
|
||||||
|
if ver:
|
||||||
|
ver_part = ui.rich_text.assemble(" @ ", (ver, "repr.number"))
|
||||||
|
text = ui.rich_text.assemble("(", (url, "repr.url"), ver_part or "", ")")
|
||||||
|
ui.write(action, ui.rich_text(name, "cyan"), text, styled=True)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if not self.args.dvc and self.args.rev:
|
||||||
|
raise DvcException("--rev can't be used without --dvc")
|
||||||
|
if not self.args.dvc and self.args.path:
|
||||||
|
raise DvcException("--path can't be used without --dvc")
|
||||||
|
|
||||||
|
d = vars(self.args)
|
||||||
|
for key in ["dvc", "dc", "url"]:
|
||||||
|
if url := d.pop(key, None):
|
||||||
|
d.update({"type": key, "url": url})
|
||||||
|
break
|
||||||
|
|
||||||
|
existing = self.repo.datasets.get(self.args.name)
|
||||||
|
with self.repo.scm_context:
|
||||||
|
if not self.args.force and existing:
|
||||||
|
path = self.repo.fs.relpath(existing.manifest_path)
|
||||||
|
raise DvcException(
|
||||||
|
f"{self.args.name} already exists in {path}, "
|
||||||
|
"use the --force to overwrite"
|
||||||
|
)
|
||||||
|
dataset = self.repo.datasets.add(**d)
|
||||||
|
self.display(self.args.name, dataset)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class CmdDatasetUpdate(CmdBase):
|
||||||
|
def display(self, name: str, dataset: "Dataset", new: "Dataset"):
|
||||||
|
from dvc.commands.checkout import log_changes
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
action = "Updating"
|
||||||
|
if not dataset.lock:
|
||||||
|
return CmdDatasetAdd.display(name, new, action)
|
||||||
|
if dataset == new:
|
||||||
|
ui.write("[yellow]Nothing to update[/]", styled=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
assert new.lock
|
||||||
|
|
||||||
|
v: Optional[tuple[str, str]] = None
|
||||||
|
if dataset.type == "dc":
|
||||||
|
assert new.type == "dc"
|
||||||
|
if new.lock.version < dataset.lock.version:
|
||||||
|
action = "Downgrading"
|
||||||
|
|
||||||
|
v = (f"v{dataset.lock.version}", f"v{new.lock.version}")
|
||||||
|
if dataset.type == "dvc":
|
||||||
|
assert new.type == "dvc"
|
||||||
|
v = (f"{dataset.lock.rev_lock[:9]}", f"{new.lock.rev_lock[:9]}")
|
||||||
|
|
||||||
|
if v:
|
||||||
|
part = ui.rich_text.assemble(
|
||||||
|
(v[0], "repr.number"),
|
||||||
|
" -> ",
|
||||||
|
(v[1], "repr.number"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
part = ui.rich_text(dataset.spec.url, "repr.url")
|
||||||
|
changes = ui.rich_text.assemble("(", part, ")")
|
||||||
|
ui.write(action, ui.rich_text(name, "cyan"), changes, styled=True)
|
||||||
|
if dataset.type == "url":
|
||||||
|
assert new.type == "url"
|
||||||
|
stats = diff_files(dataset.lock.files, new.lock.files)
|
||||||
|
log_changes(stats)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
from difflib import get_close_matches
|
||||||
|
|
||||||
|
from dvc.repo.datasets import DatasetNotFoundError
|
||||||
|
from dvc.ui import ui
|
||||||
|
|
||||||
|
version = None
|
||||||
|
if self.args.rev:
|
||||||
|
try:
|
||||||
|
version = int(self.args.rev.lstrip("v"))
|
||||||
|
except ValueError:
|
||||||
|
version = self.args.rev
|
||||||
|
|
||||||
|
d = vars(self.args) | {"version": version}
|
||||||
|
with self.repo.scm_context:
|
||||||
|
try:
|
||||||
|
dataset, new = self.repo.datasets.update(**d)
|
||||||
|
except DatasetNotFoundError:
|
||||||
|
logger.exception("")
|
||||||
|
if matches := get_close_matches(self.args.name, self.repo.datasets):
|
||||||
|
ui.write(
|
||||||
|
"did you mean?",
|
||||||
|
ui.rich_text(matches[0], "cyan"),
|
||||||
|
stderr=True,
|
||||||
|
styled=True,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
self.display(self.args.name, dataset, new)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def add_parser(subparsers, parent_parser):
|
||||||
|
ds_parser = subparsers.add_parser(
|
||||||
|
"dataset",
|
||||||
|
aliases=["ds"],
|
||||||
|
parents=[parent_parser],
|
||||||
|
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
ds_subparsers = ds_parser.add_subparsers(
|
||||||
|
dest="cmd",
|
||||||
|
help="Use `dvc dataset CMD --help` to display command-specific help.",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
dataset_add_help = "Add a dataset."
|
||||||
|
ds_add_parser = ds_subparsers.add_parser(
|
||||||
|
"add",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(dataset_add_help, "dataset/add"),
|
||||||
|
formatter_class=formatter.RawTextHelpFormatter,
|
||||||
|
help=dataset_add_help,
|
||||||
|
)
|
||||||
|
|
||||||
|
url_exclusive_group = ds_add_parser.add_mutually_exclusive_group(required=True)
|
||||||
|
url_exclusive_group.add_argument(
|
||||||
|
"--dc", metavar="name", help="Name of the DataChain dataset to track"
|
||||||
|
)
|
||||||
|
url_exclusive_group.add_argument(
|
||||||
|
"--dvc",
|
||||||
|
help="Path or URL to a Git/DVC repository to track",
|
||||||
|
metavar="url",
|
||||||
|
)
|
||||||
|
url_exclusive_group.add_argument(
|
||||||
|
"--url",
|
||||||
|
help="""\
|
||||||
|
URL of a cloud-versioned remote to track. Supported URLs:
|
||||||
|
|
||||||
|
s3://bucket/key/path
|
||||||
|
gs://bucket/path/to/file/or/dir
|
||||||
|
azure://mycontainer/path
|
||||||
|
remote://remote_name/path/to/file/or/dir (see `dvc remote`)
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
ds_add_parser.add_argument("name", help="Name of the dataset to add")
|
||||||
|
ds_add_parser.add_argument(
|
||||||
|
"--rev",
|
||||||
|
help="Git revision, e.g. SHA, branch, tag (only applicable with --dvc)",
|
||||||
|
metavar="<commit>",
|
||||||
|
)
|
||||||
|
ds_add_parser.add_argument(
|
||||||
|
"--path",
|
||||||
|
help="Path to a file or a directory within a git repository "
|
||||||
|
"(only applicable with --dvc)",
|
||||||
|
)
|
||||||
|
ds_add_parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Overwrite existing dataset",
|
||||||
|
)
|
||||||
|
ds_add_parser.set_defaults(func=CmdDatasetAdd)
|
||||||
|
|
||||||
|
dataset_update_help = "Update a dataset."
|
||||||
|
ds_update_parser = ds_subparsers.add_parser(
|
||||||
|
"update",
|
||||||
|
parents=[parent_parser],
|
||||||
|
description=append_doc_link(dataset_update_help, "dataset/update"),
|
||||||
|
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||||
|
help=dataset_update_help,
|
||||||
|
)
|
||||||
|
ds_update_parser.add_argument("name", help="Name of the dataset to update")
|
||||||
|
ds_update_parser.add_argument(
|
||||||
|
"--rev",
|
||||||
|
"--version",
|
||||||
|
nargs="?",
|
||||||
|
help="DataChain dataset version or Git revision (e.g. SHA, branch, tag)",
|
||||||
|
metavar="<version>",
|
||||||
|
)
|
||||||
|
ds_update_parser.set_defaults(func=CmdDatasetUpdate)
|
||||||
+1
-1
@@ -60,7 +60,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"--remote",
|
"--remote",
|
||||||
type=str,
|
type=str,
|
||||||
help="Remote name to set as a default in the target repository.",
|
help="Remote name to set as a default in the target repository.",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
du_parser.add_argument(
|
du_parser.add_argument(
|
||||||
"--remote-config",
|
"--remote-config",
|
||||||
type=str,
|
type=str,
|
||||||
|
|||||||
@@ -58,6 +58,15 @@ def add_parser(subparsers, parent_parser):
|
|||||||
hide_subparsers_from_help(experiments_subparsers)
|
hide_subparsers_from_help(experiments_subparsers)
|
||||||
|
|
||||||
|
|
||||||
|
def add_keep_selection_flag(experiments_subcmd_parser):
|
||||||
|
experiments_subcmd_parser.add_argument(
|
||||||
|
"--keep",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Keep the selected experiments instead of removing them.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_rev_selection_flags(
|
def add_rev_selection_flags(
|
||||||
experiments_subcmd_parser, command: str, default: bool = True
|
experiments_subcmd_parser, command: str, default: bool = True
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from dvc.cli import formatter
|
import argparse
|
||||||
|
|
||||||
|
from dvc.cli import completion, formatter
|
||||||
from dvc.cli.command import CmdBase
|
from dvc.cli.command import CmdBase
|
||||||
from dvc.cli.utils import append_doc_link
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
@@ -69,7 +71,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
|||||||
dest="dvc_remote",
|
dest="dvc_remote",
|
||||||
metavar="<name>",
|
metavar="<name>",
|
||||||
help="Name of the DVC remote to use when pulling cached outputs.",
|
help="Name of the DVC remote to use when pulling cached outputs.",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
experiments_pull_parser.add_argument(
|
experiments_pull_parser.add_argument(
|
||||||
"-j",
|
"-j",
|
||||||
"--jobs",
|
"--jobs",
|
||||||
@@ -81,7 +83,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
|||||||
)
|
)
|
||||||
experiments_pull_parser.add_argument(
|
experiments_pull_parser.add_argument(
|
||||||
"--run-cache",
|
"--run-cache",
|
||||||
action="store_true",
|
action=argparse.BooleanOptionalAction,
|
||||||
default=False,
|
default=False,
|
||||||
help="Pull run history for all stages.",
|
help="Pull run history for all stages.",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import argparse
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from dvc.cli import completion, formatter
|
from dvc.cli import completion, formatter
|
||||||
@@ -110,7 +111,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
|||||||
dest="dvc_remote",
|
dest="dvc_remote",
|
||||||
metavar="<name>",
|
metavar="<name>",
|
||||||
help="Name of the DVC remote to use when pushing cached outputs.",
|
help="Name of the DVC remote to use when pushing cached outputs.",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
experiments_push_parser.add_argument(
|
experiments_push_parser.add_argument(
|
||||||
"-j",
|
"-j",
|
||||||
"--jobs",
|
"--jobs",
|
||||||
@@ -120,7 +121,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
|||||||
)
|
)
|
||||||
experiments_push_parser.add_argument(
|
experiments_push_parser.add_argument(
|
||||||
"--run-cache",
|
"--run-cache",
|
||||||
action="store_true",
|
action=argparse.BooleanOptionalAction,
|
||||||
default=False,
|
default=False,
|
||||||
help="Push run history for all stages.",
|
help="Push run history for all stages.",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class CmdExperimentsRemove(CmdBase):
|
|||||||
num=self.args.num,
|
num=self.args.num,
|
||||||
queue=self.args.queue,
|
queue=self.args.queue,
|
||||||
git_remote=self.args.git_remote,
|
git_remote=self.args.git_remote,
|
||||||
|
keep=self.args.keep,
|
||||||
)
|
)
|
||||||
if removed:
|
if removed:
|
||||||
ui.write(f"Removed experiments: {humanize.join(map(repr, removed))}")
|
ui.write(f"Removed experiments: {humanize.join(map(repr, removed))}")
|
||||||
@@ -44,7 +45,7 @@ class CmdExperimentsRemove(CmdBase):
|
|||||||
|
|
||||||
|
|
||||||
def add_parser(experiments_subparsers, parent_parser):
|
def add_parser(experiments_subparsers, parent_parser):
|
||||||
from . import add_rev_selection_flags
|
from . import add_keep_selection_flag, add_rev_selection_flags
|
||||||
|
|
||||||
EXPERIMENTS_REMOVE_HELP = "Remove experiments."
|
EXPERIMENTS_REMOVE_HELP = "Remove experiments."
|
||||||
experiments_remove_parser = experiments_subparsers.add_parser(
|
experiments_remove_parser = experiments_subparsers.add_parser(
|
||||||
@@ -57,6 +58,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
|||||||
)
|
)
|
||||||
remove_group = experiments_remove_parser.add_mutually_exclusive_group()
|
remove_group = experiments_remove_parser.add_mutually_exclusive_group()
|
||||||
add_rev_selection_flags(experiments_remove_parser, "Remove", False)
|
add_rev_selection_flags(experiments_remove_parser, "Remove", False)
|
||||||
|
add_keep_selection_flag(experiments_remove_parser)
|
||||||
remove_group.add_argument(
|
remove_group.add_argument(
|
||||||
"--queue", action="store_true", help="Remove all queued experiments."
|
"--queue", action="store_true", help="Remove all queued experiments."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class CmdExperimentsRun(CmdRepro):
|
|||||||
tmp_dir=self.args.tmp_dir,
|
tmp_dir=self.args.tmp_dir,
|
||||||
copy_paths=self.args.copy_paths,
|
copy_paths=self.args.copy_paths,
|
||||||
message=self.args.message,
|
message=self.args.message,
|
||||||
|
no_hydra=self.args.no_hydra,
|
||||||
**self._common_kwargs,
|
**self._common_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,4 +108,14 @@ def _add_run_common(parser):
|
|||||||
default=None,
|
default=None,
|
||||||
help="Custom commit message to use when committing the experiment.",
|
help="Custom commit message to use when committing the experiment.",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-hydra",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=(
|
||||||
|
"Disables automatically updating `params.yaml` with Hydra configuration. "
|
||||||
|
" You can still use `--set-param` to update individual params if needed."
|
||||||
|
" Default is False."
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument("-M", dest="message", help=argparse.SUPPRESS) # obsolete
|
parser.add_argument("-M", dest="message", help=argparse.SUPPRESS) # obsolete
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
from collections.abc import Iterable
|
from collections.abc import Collection, Iterable
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -59,17 +59,16 @@ def baseline_styler(typ):
|
|||||||
def show_experiments(
|
def show_experiments(
|
||||||
td: "TabularData",
|
td: "TabularData",
|
||||||
headers: dict[str, Iterable[str]],
|
headers: dict[str, Iterable[str]],
|
||||||
keep=None,
|
keep: Collection[str] = (),
|
||||||
drop=None,
|
drop: Collection[str] = (),
|
||||||
pager=True,
|
pager=True,
|
||||||
csv=False,
|
csv=False,
|
||||||
markdown=False,
|
markdown=False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
if keep:
|
if keep:
|
||||||
for col in td.keys(): # noqa: SIM118
|
keep_re = re.compile("|".join(keep))
|
||||||
if re.match(keep, col):
|
td.protect(*(col for col in td.keys() if keep_re.match(col))) # noqa: SIM118
|
||||||
td.protect(col)
|
|
||||||
|
|
||||||
for col in ("State", "Executor"):
|
for col in ("State", "Executor"):
|
||||||
if td.is_empty(col):
|
if td.is_empty(col):
|
||||||
@@ -110,8 +109,9 @@ def show_experiments(
|
|||||||
td.drop_duplicates("cols", ignore_empty=False)
|
td.drop_duplicates("cols", ignore_empty=False)
|
||||||
|
|
||||||
cols_to_drop = set()
|
cols_to_drop = set()
|
||||||
if drop is not None:
|
if drop:
|
||||||
cols_to_drop = {col for col in td.keys() if re.match(drop, col)} # noqa: SIM118
|
drop_re = re.compile("|".join(drop))
|
||||||
|
cols_to_drop = {col for col in td.keys() if drop_re.match(col)} # noqa: SIM118
|
||||||
td.drop(*cols_to_drop)
|
td.drop(*cols_to_drop)
|
||||||
|
|
||||||
td.render(
|
td.render(
|
||||||
@@ -150,6 +150,7 @@ class CmdExperimentsShow(CmdBase):
|
|||||||
all_commits=self.args.all_commits,
|
all_commits=self.args.all_commits,
|
||||||
hide_queued=self.args.hide_queued,
|
hide_queued=self.args.hide_queued,
|
||||||
hide_failed=self.args.hide_failed,
|
hide_failed=self.args.hide_failed,
|
||||||
|
hide_workspace=self.args.hide_workspace,
|
||||||
revs=self.args.rev,
|
revs=self.args.rev,
|
||||||
num=self.args.num,
|
num=self.args.num,
|
||||||
sha_only=self.args.sha,
|
sha_only=self.args.sha,
|
||||||
@@ -236,11 +237,15 @@ def add_parser(experiments_subparsers, parent_parser):
|
|||||||
)
|
)
|
||||||
experiments_show_parser.add_argument(
|
experiments_show_parser.add_argument(
|
||||||
"--drop",
|
"--drop",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
help="Remove the columns matching the specified regex pattern.",
|
help="Remove the columns matching the specified regex pattern.",
|
||||||
metavar="<regex_pattern>",
|
metavar="<regex_pattern>",
|
||||||
)
|
)
|
||||||
experiments_show_parser.add_argument(
|
experiments_show_parser.add_argument(
|
||||||
"--keep",
|
"--keep",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
help="Preserve the columns matching the specified regex pattern.",
|
help="Preserve the columns matching the specified regex pattern.",
|
||||||
metavar="<regex_pattern>",
|
metavar="<regex_pattern>",
|
||||||
)
|
)
|
||||||
@@ -279,6 +284,12 @@ def add_parser(experiments_subparsers, parent_parser):
|
|||||||
default=False,
|
default=False,
|
||||||
help="Hide queued experiments in the table.",
|
help="Hide queued experiments in the table.",
|
||||||
)
|
)
|
||||||
|
experiments_show_parser.add_argument(
|
||||||
|
"--hide-workspace",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Hide workspace row in the table.",
|
||||||
|
)
|
||||||
experiments_show_parser.add_argument(
|
experiments_show_parser.add_argument(
|
||||||
"--json",
|
"--json",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
+15
-8
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from dvc.cli import formatter
|
from dvc.cli import completion, formatter
|
||||||
from dvc.cli.command import CmdBase
|
from dvc.cli.command import CmdBase
|
||||||
from dvc.cli.utils import append_doc_link
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
@@ -40,9 +40,16 @@ class CmdGC(CmdBase):
|
|||||||
msg += " and all git commits"
|
msg += " and all git commits"
|
||||||
else:
|
else:
|
||||||
if self.args.all_branches and self.args.all_tags:
|
if self.args.all_branches and self.args.all_tags:
|
||||||
msg += " and all git branches and tags"
|
if self.args.num:
|
||||||
|
msg += f" and last {self.args.num} commits from all git branches"
|
||||||
|
msg += " and all git tags"
|
||||||
|
else:
|
||||||
|
msg += " and all git branches and tags"
|
||||||
elif self.args.all_branches:
|
elif self.args.all_branches:
|
||||||
msg += " and all git branches"
|
if self.args.num:
|
||||||
|
msg += f" and last {self.args.num} commits from all git branches"
|
||||||
|
else:
|
||||||
|
msg += " and all git branches"
|
||||||
elif self.args.all_tags:
|
elif self.args.all_tags:
|
||||||
msg += " and all git tags"
|
msg += " and all git tags"
|
||||||
if self.args.commit_date:
|
if self.args.commit_date:
|
||||||
@@ -54,13 +61,13 @@ class CmdGC(CmdBase):
|
|||||||
msg += " and all experiments"
|
msg += " and all experiments"
|
||||||
|
|
||||||
if self.args.not_in_remote:
|
if self.args.not_in_remote:
|
||||||
msg += " that are present in the DVC remote"
|
msg += " that are not present in the DVC remote"
|
||||||
|
|
||||||
if self.args.repos:
|
if self.args.repos:
|
||||||
msg += " of the current and the following repos:"
|
msg += " of the current and the following repos:"
|
||||||
|
|
||||||
for repo_path in self.args.repos:
|
for repo_path in self.args.repos:
|
||||||
msg += "\n - %s" % os.path.abspath(repo_path)
|
msg += f"\n - {os.path.abspath(repo_path)}"
|
||||||
else:
|
else:
|
||||||
msg += " of the current repo."
|
msg += " of the current repo."
|
||||||
|
|
||||||
@@ -126,8 +133,8 @@ def add_parser(subparsers, parent_parser):
|
|||||||
metavar="<num>",
|
metavar="<num>",
|
||||||
help=(
|
help=(
|
||||||
"Keep data files used in the last `num` commits "
|
"Keep data files used in the last `num` commits "
|
||||||
"starting from the `--rev` <commit>. "
|
"starting from each selected revision root. "
|
||||||
"Only used if `--rev` is also provided. "
|
"Can be used with `--rev` and `--all-branches`. "
|
||||||
"Defaults to `1`."
|
"Defaults to `1`."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -188,7 +195,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"--remote",
|
"--remote",
|
||||||
help="Remote storage to collect garbage in",
|
help="Remote storage to collect garbage in",
|
||||||
metavar="<name>",
|
metavar="<name>",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
gc_parser.add_argument(
|
gc_parser.add_argument(
|
||||||
"--skip-failed",
|
"--skip-failed",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
+8
-2
@@ -12,7 +12,13 @@ class CmdGet(CmdBaseNoRepo):
|
|||||||
from dvc.api import get_url
|
from dvc.api import get_url
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
|
|
||||||
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
|
url = get_url(
|
||||||
|
self.args.path,
|
||||||
|
repo=self.args.url,
|
||||||
|
rev=self.args.rev,
|
||||||
|
remote=self.args.remote,
|
||||||
|
remote_config=self.args.remote_config,
|
||||||
|
)
|
||||||
ui.write(url, force=True)
|
ui.write(url, force=True)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -115,7 +121,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"--remote",
|
"--remote",
|
||||||
type=str,
|
type=str,
|
||||||
help="Remote name to set as a default in the target repository.",
|
help="Remote name to set as a default in the target repository.",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
get_parser.add_argument(
|
get_parser.add_argument(
|
||||||
"--remote-config",
|
"--remote-config",
|
||||||
type=str,
|
type=str,
|
||||||
|
|||||||
+1
-1
@@ -115,7 +115,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"--remote",
|
"--remote",
|
||||||
type=str,
|
type=str,
|
||||||
help="Remote name to set as a default in the target repository.",
|
help="Remote name to set as a default in the target repository.",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
import_parser.add_argument(
|
import_parser.add_argument(
|
||||||
"--remote-config",
|
"--remote-config",
|
||||||
type=str,
|
type=str,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"--remote",
|
"--remote",
|
||||||
help="Remote storage to download to",
|
help="Remote storage to download to",
|
||||||
metavar="<name>",
|
metavar="<name>",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||||
no_download_exec_group.add_argument(
|
no_download_exec_group.add_argument(
|
||||||
"--no-exec",
|
"--no-exec",
|
||||||
|
|||||||
+14
-5
@@ -29,7 +29,7 @@ def _welcome_message():
|
|||||||
"{yellow}------------{nc}\n"
|
"{yellow}------------{nc}\n"
|
||||||
f"- Check out the documentation: {fmt_link('https://dvc.org/doc')}\n"
|
f"- Check out the documentation: {fmt_link('https://dvc.org/doc')}\n"
|
||||||
f"- Get help and share ideas: {fmt_link('https://dvc.org/chat')}\n"
|
f"- Get help and share ideas: {fmt_link('https://dvc.org/chat')}\n"
|
||||||
f"- Star us on GitHub: {fmt_link('https://github.com/iterative/dvc')}"
|
f"- Star us on GitHub: {fmt_link('https://github.com/treeverse/dvc')}"
|
||||||
).format(yellow=colorama.Fore.YELLOW, nc=colorama.Fore.RESET)
|
).format(yellow=colorama.Fore.YELLOW, nc=colorama.Fore.RESET)
|
||||||
|
|
||||||
ui.write(msg)
|
ui.write(msg)
|
||||||
@@ -42,7 +42,7 @@ class CmdInit(CmdBaseNoRepo):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with Repo.init(
|
with Repo.init(
|
||||||
".",
|
self.args.directory,
|
||||||
no_scm=self.args.no_scm,
|
no_scm=self.args.no_scm,
|
||||||
force=self.args.force,
|
force=self.args.force,
|
||||||
subdir=self.args.subdir,
|
subdir=self.args.subdir,
|
||||||
@@ -57,10 +57,11 @@ class CmdInit(CmdBaseNoRepo):
|
|||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
"""Setup parser for `dvc init`."""
|
"""Setup parser for `dvc init`."""
|
||||||
INIT_HELP = "Initialize DVC in the current directory."
|
INIT_HELP = "Initialize DVC repository."
|
||||||
INIT_DESCRIPTION = (
|
INIT_DESCRIPTION = (
|
||||||
"Initialize DVC in the current directory. Expects directory\n"
|
"Initialize DVC repository in the given directory (defaults to the current "
|
||||||
"to be a Git repository unless --no-scm option is specified."
|
"working directory).\n"
|
||||||
|
"Expects directory to be a Git repository unless --no-scm option is specified."
|
||||||
)
|
)
|
||||||
|
|
||||||
init_parser = subparsers.add_parser(
|
init_parser = subparsers.add_parser(
|
||||||
@@ -70,6 +71,14 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=INIT_HELP,
|
help=INIT_HELP,
|
||||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
|
init_parser.add_argument(
|
||||||
|
"directory",
|
||||||
|
nargs="?",
|
||||||
|
default=".",
|
||||||
|
help=(
|
||||||
|
"Directory to initialize DVC in. Defaults to the current working directory."
|
||||||
|
),
|
||||||
|
)
|
||||||
init_parser.add_argument(
|
init_parser.add_argument(
|
||||||
"--no-scm",
|
"--no-scm",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|||||||
+173
-33
@@ -1,3 +1,5 @@
|
|||||||
|
from typing import Callable
|
||||||
|
|
||||||
from dvc.cli import completion, formatter
|
from dvc.cli import completion, formatter
|
||||||
from dvc.cli.command import CmdBaseNoRepo
|
from dvc.cli.command import CmdBaseNoRepo
|
||||||
from dvc.cli.utils import DictAction, append_doc_link
|
from dvc.cli.utils import DictAction, append_doc_link
|
||||||
@@ -9,54 +11,173 @@ from dvc.ui import ui
|
|||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _format_entry(entry, fmt):
|
def _get_formatter(with_color: bool = False) -> Callable[[dict], str]:
|
||||||
from dvc.utils.humanize import naturalsize
|
def fmt(entry: dict) -> str:
|
||||||
|
return entry["path"]
|
||||||
|
|
||||||
size = entry.get("size")
|
|
||||||
if size is None:
|
|
||||||
size = ""
|
|
||||||
else:
|
|
||||||
size = naturalsize(size)
|
|
||||||
return size, fmt(entry)
|
|
||||||
|
|
||||||
|
|
||||||
def show_entries(entries, with_color=False, with_size=False):
|
|
||||||
if with_color:
|
if with_color:
|
||||||
ls_colors = LsColors()
|
ls_colors = LsColors()
|
||||||
fmt = ls_colors.format
|
return ls_colors.format
|
||||||
else:
|
|
||||||
|
|
||||||
def fmt(entry):
|
return fmt
|
||||||
return entry["path"]
|
|
||||||
|
|
||||||
|
|
||||||
|
def _format_entry(entry, name, with_size=True, with_hash=False):
|
||||||
|
from dvc.utils.humanize import naturalsize
|
||||||
|
|
||||||
|
ret = []
|
||||||
if with_size:
|
if with_size:
|
||||||
ui.table([_format_entry(entry, fmt) for entry in entries])
|
size = entry.get("size")
|
||||||
|
if size is None or (size <= 0 and entry.get("isdir")):
|
||||||
|
size = ""
|
||||||
|
else:
|
||||||
|
size = naturalsize(size)
|
||||||
|
ret.append(size)
|
||||||
|
if with_hash:
|
||||||
|
md5 = entry.get("md5", "")
|
||||||
|
ret.append(md5)
|
||||||
|
ret.append(name)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def show_entries(entries, with_color=False, with_size=False, with_hash=False):
|
||||||
|
fmt = _get_formatter(with_color)
|
||||||
|
if with_size or with_hash:
|
||||||
|
colalign = ("right",) if with_size else None
|
||||||
|
ui.table(
|
||||||
|
[
|
||||||
|
_format_entry(
|
||||||
|
entry,
|
||||||
|
fmt(entry),
|
||||||
|
with_size=with_size,
|
||||||
|
with_hash=with_hash,
|
||||||
|
)
|
||||||
|
for entry in entries
|
||||||
|
],
|
||||||
|
colalign=colalign,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# NOTE: this is faster than ui.table for very large number of entries
|
# NOTE: this is faster than ui.table for very large number of entries
|
||||||
ui.write("\n".join(fmt(entry) for entry in entries))
|
ui.write("\n".join(fmt(entry) for entry in entries))
|
||||||
|
|
||||||
|
|
||||||
|
class TreePart:
|
||||||
|
Edge = "├── "
|
||||||
|
Line = "│ "
|
||||||
|
Corner = "└── "
|
||||||
|
Blank = " "
|
||||||
|
|
||||||
|
|
||||||
|
def _build_tree_structure(
|
||||||
|
entries, with_color=False, with_size=False, with_hash=False, _depth=0, _prefix=""
|
||||||
|
):
|
||||||
|
rows = []
|
||||||
|
fmt = _get_formatter(with_color)
|
||||||
|
|
||||||
|
num_entries = len(entries)
|
||||||
|
for i, (name, entry) in enumerate(entries.items()):
|
||||||
|
entry["path"] = name
|
||||||
|
is_last = i >= num_entries - 1
|
||||||
|
tree_part = ""
|
||||||
|
if _depth > 0:
|
||||||
|
tree_part = TreePart.Corner if is_last else TreePart.Edge
|
||||||
|
|
||||||
|
row = _format_entry(
|
||||||
|
entry,
|
||||||
|
_prefix + tree_part + fmt(entry),
|
||||||
|
with_size=with_size,
|
||||||
|
with_hash=with_hash,
|
||||||
|
)
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
if contents := entry.get("contents"):
|
||||||
|
new_prefix = _prefix
|
||||||
|
if _depth > 0:
|
||||||
|
new_prefix += TreePart.Blank if is_last else TreePart.Line
|
||||||
|
new_rows = _build_tree_structure(
|
||||||
|
contents,
|
||||||
|
with_color=with_color,
|
||||||
|
with_size=with_size,
|
||||||
|
with_hash=with_hash,
|
||||||
|
_depth=_depth + 1,
|
||||||
|
_prefix=new_prefix,
|
||||||
|
)
|
||||||
|
rows.extend(new_rows)
|
||||||
|
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def show_tree(entries, with_color=False, with_size=False, with_hash=False):
|
||||||
|
rows = _build_tree_structure(
|
||||||
|
entries,
|
||||||
|
with_color=with_color,
|
||||||
|
with_size=with_size,
|
||||||
|
with_hash=with_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
colalign = ("right",) if with_size else None
|
||||||
|
ui.table(rows, colalign=colalign, preserve_whitespace=True)
|
||||||
|
|
||||||
|
|
||||||
class CmdList(CmdBaseNoRepo):
|
class CmdList(CmdBaseNoRepo):
|
||||||
def run(self):
|
def _show_tree(self):
|
||||||
|
from dvc.repo.ls import ls_tree
|
||||||
|
|
||||||
|
entries = ls_tree(
|
||||||
|
self.args.url,
|
||||||
|
self.args.path,
|
||||||
|
rev=self.args.rev,
|
||||||
|
dvc_only=self.args.dvc_only,
|
||||||
|
config=self.args.config,
|
||||||
|
remote=self.args.remote,
|
||||||
|
remote_config=self.args.remote_config,
|
||||||
|
maxdepth=self.args.level,
|
||||||
|
)
|
||||||
|
show_tree(
|
||||||
|
entries,
|
||||||
|
with_color=True,
|
||||||
|
with_size=self.args.size,
|
||||||
|
with_hash=self.args.show_hash,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _show_list(self):
|
||||||
from dvc.repo import Repo
|
from dvc.repo import Repo
|
||||||
|
|
||||||
try:
|
entries = Repo.ls(
|
||||||
entries = Repo.ls(
|
self.args.url,
|
||||||
self.args.url,
|
self.args.path,
|
||||||
self.args.path,
|
rev=self.args.rev,
|
||||||
rev=self.args.rev,
|
recursive=self.args.recursive,
|
||||||
recursive=self.args.recursive,
|
dvc_only=self.args.dvc_only,
|
||||||
dvc_only=self.args.dvc_only,
|
config=self.args.config,
|
||||||
config=self.args.config,
|
remote=self.args.remote,
|
||||||
remote=self.args.remote,
|
remote_config=self.args.remote_config,
|
||||||
remote_config=self.args.remote_config,
|
maxdepth=self.args.level,
|
||||||
|
)
|
||||||
|
if self.args.json:
|
||||||
|
ui.write_json(entries)
|
||||||
|
elif entries:
|
||||||
|
show_entries(
|
||||||
|
entries,
|
||||||
|
with_color=True,
|
||||||
|
with_size=self.args.size,
|
||||||
|
with_hash=self.args.show_hash,
|
||||||
)
|
)
|
||||||
if self.args.json:
|
return 0
|
||||||
ui.write_json(entries)
|
|
||||||
elif entries:
|
def run(self):
|
||||||
show_entries(entries, with_color=True, with_size=self.args.size)
|
if self.args.tree and self.args.json:
|
||||||
return 0
|
raise DvcException("Cannot use --tree and --json options together.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.args.tree:
|
||||||
|
return self._show_tree()
|
||||||
|
return self._show_list()
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.exception("")
|
||||||
|
return 1
|
||||||
except DvcException:
|
except DvcException:
|
||||||
logger.exception("failed to list '%s'", self.args.url)
|
logger.exception("failed to list '%s'", self.args.url)
|
||||||
return 1
|
return 1
|
||||||
@@ -82,6 +203,19 @@ def add_parser(subparsers, parent_parser):
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Recursively list files.",
|
help="Recursively list files.",
|
||||||
)
|
)
|
||||||
|
list_parser.add_argument(
|
||||||
|
"-T",
|
||||||
|
"--tree",
|
||||||
|
action="store_true",
|
||||||
|
help="Recurse into directories as a tree.",
|
||||||
|
)
|
||||||
|
list_parser.add_argument(
|
||||||
|
"-L",
|
||||||
|
"--level",
|
||||||
|
metavar="depth",
|
||||||
|
type=int,
|
||||||
|
help="Limit the depth of recursion.",
|
||||||
|
)
|
||||||
list_parser.add_argument(
|
list_parser.add_argument(
|
||||||
"--dvc-only", action="store_true", help="Show only DVC outputs."
|
"--dvc-only", action="store_true", help="Show only DVC outputs."
|
||||||
)
|
)
|
||||||
@@ -108,7 +242,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"--remote",
|
"--remote",
|
||||||
type=str,
|
type=str,
|
||||||
help="Remote name to set as a default in the target repository.",
|
help="Remote name to set as a default in the target repository.",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
list_parser.add_argument(
|
list_parser.add_argument(
|
||||||
"--remote-config",
|
"--remote-config",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -120,6 +254,12 @@ def add_parser(subparsers, parent_parser):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
list_parser.add_argument("--size", action="store_true", help="Show sizes.")
|
list_parser.add_argument("--size", action="store_true", help="Show sizes.")
|
||||||
|
list_parser.add_argument(
|
||||||
|
"--show-hash",
|
||||||
|
help="Display hash value for each item.",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
list_parser.add_argument(
|
list_parser.add_argument(
|
||||||
"path",
|
"path",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ class LsColors:
|
|||||||
if entry.get("isexec", False):
|
if entry.get("isexec", False):
|
||||||
return self._format(text, code="ex")
|
return self._format(text, code="ex")
|
||||||
|
|
||||||
_, ext = os.path.splitext(text)
|
stem, ext = os.path.splitext(text)
|
||||||
|
if not ext and stem.startswith("."):
|
||||||
|
ext = stem
|
||||||
return self._format(text, ext=ext)
|
return self._format(text, ext=ext)
|
||||||
|
|
||||||
def _format(self, text, code=None, ext=None):
|
def _format(self, text, code=None, ext=None):
|
||||||
|
|||||||
+36
-4
@@ -3,26 +3,45 @@ from dvc.cli.command import CmdBaseNoRepo
|
|||||||
from dvc.cli.utils import DictAction, append_doc_link
|
from dvc.cli.utils import DictAction, append_doc_link
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
|
|
||||||
from .ls import show_entries
|
from .ls import show_entries, show_tree
|
||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CmdListUrl(CmdBaseNoRepo):
|
class CmdListUrl(CmdBaseNoRepo):
|
||||||
def run(self):
|
def _show_tree(self, config):
|
||||||
from dvc.config import Config
|
from dvc.fs import parse_external_url
|
||||||
|
from dvc.repo.ls import _ls_tree
|
||||||
|
|
||||||
|
fs, fs_path = parse_external_url(
|
||||||
|
self.args.url, fs_config=self.args.fs_config, config=config
|
||||||
|
)
|
||||||
|
entries = _ls_tree(fs, fs_path, maxdepth=self.args.level)
|
||||||
|
show_tree(entries, with_color=True, with_size=self.args.size)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _show_list(self, config):
|
||||||
from dvc.repo import Repo
|
from dvc.repo import Repo
|
||||||
|
|
||||||
entries = Repo.ls_url(
|
entries = Repo.ls_url(
|
||||||
self.args.url,
|
self.args.url,
|
||||||
recursive=self.args.recursive,
|
recursive=self.args.recursive,
|
||||||
|
maxdepth=self.args.level,
|
||||||
fs_config=self.args.fs_config,
|
fs_config=self.args.fs_config,
|
||||||
config=Config.from_cwd(),
|
config=config,
|
||||||
)
|
)
|
||||||
if entries:
|
if entries:
|
||||||
show_entries(entries, with_color=True, with_size=self.args.size)
|
show_entries(entries, with_color=True, with_size=self.args.size)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
from dvc.config import Config
|
||||||
|
|
||||||
|
config = Config.from_cwd()
|
||||||
|
if self.args.tree:
|
||||||
|
return self._show_tree(config=config)
|
||||||
|
return self._show_list(config=config)
|
||||||
|
|
||||||
|
|
||||||
def add_parser(subparsers, parent_parser):
|
def add_parser(subparsers, parent_parser):
|
||||||
LS_HELP = "List directory contents from URL."
|
LS_HELP = "List directory contents from URL."
|
||||||
@@ -40,6 +59,19 @@ def add_parser(subparsers, parent_parser):
|
|||||||
lsurl_parser.add_argument(
|
lsurl_parser.add_argument(
|
||||||
"-R", "--recursive", action="store_true", help="Recursively list files."
|
"-R", "--recursive", action="store_true", help="Recursively list files."
|
||||||
)
|
)
|
||||||
|
lsurl_parser.add_argument(
|
||||||
|
"-T",
|
||||||
|
"--tree",
|
||||||
|
action="store_true",
|
||||||
|
help="Recurse into directories as a tree.",
|
||||||
|
)
|
||||||
|
lsurl_parser.add_argument(
|
||||||
|
"-L",
|
||||||
|
"--level",
|
||||||
|
metavar="depth",
|
||||||
|
type=int,
|
||||||
|
help="Limit the depth of recursion.",
|
||||||
|
)
|
||||||
lsurl_parser.add_argument("--size", action="store_true", help="Show sizes.")
|
lsurl_parser.add_argument("--size", action="store_true", help="Show sizes.")
|
||||||
lsurl_parser.add_argument(
|
lsurl_parser.add_argument(
|
||||||
"--fs-config",
|
"--fs-config",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class CmdPlots(CmdBase):
|
|||||||
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
|
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
|
||||||
return html_template_path
|
return html_template_path
|
||||||
|
|
||||||
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
|
def run(self) -> int: # noqa: C901, PLR0911
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dvc.render.match import match_defs_renderers
|
from dvc.render.match import match_defs_renderers
|
||||||
|
|||||||
+19
-5
@@ -1,4 +1,4 @@
|
|||||||
from dvc.cli import formatter
|
from dvc.cli import completion, formatter
|
||||||
from dvc.cli.utils import append_doc_link
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.commands.config import CmdConfig
|
from dvc.commands.config import CmdConfig
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
@@ -110,8 +110,16 @@ class CmdRemoteDefault(CmdRemote):
|
|||||||
class CmdRemoteList(CmdRemote):
|
class CmdRemoteList(CmdRemote):
|
||||||
def run(self):
|
def run(self):
|
||||||
conf = self.config.read(self.args.level)
|
conf = self.config.read(self.args.level)
|
||||||
|
default_remote = conf["core"].get("remote")
|
||||||
|
|
||||||
for name, remote_conf in conf["remote"].items():
|
for name, remote_conf in conf["remote"].items():
|
||||||
ui.write(name, remote_conf["url"], sep="\t")
|
if name == default_remote:
|
||||||
|
text = f"{name}\t{remote_conf['url']}\t(default)"
|
||||||
|
color = "green"
|
||||||
|
else:
|
||||||
|
text = f"{name}\t{remote_conf['url']}"
|
||||||
|
color = ""
|
||||||
|
ui.write(ui.rich_text(text, style=color), styled=True)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -202,7 +210,9 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=REMOTE_DEFAULT_HELP,
|
help=REMOTE_DEFAULT_HELP,
|
||||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
remote_default_parser.add_argument("name", nargs="?", help="Name of the remote")
|
remote_default_parser.add_argument(
|
||||||
|
"name", nargs="?", help="Name of the remote"
|
||||||
|
).complete = completion.REMOTE
|
||||||
remote_default_parser.add_argument(
|
remote_default_parser.add_argument(
|
||||||
"-u",
|
"-u",
|
||||||
"--unset",
|
"--unset",
|
||||||
@@ -220,7 +230,9 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=REMOTE_MODIFY_HELP,
|
help=REMOTE_MODIFY_HELP,
|
||||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
remote_modify_parser.add_argument("name", help="Name of the remote")
|
remote_modify_parser.add_argument(
|
||||||
|
"name", help="Name of the remote"
|
||||||
|
).complete = completion.REMOTE
|
||||||
remote_modify_parser.add_argument("option", help="Name of the option to modify.")
|
remote_modify_parser.add_argument("option", help="Name of the option to modify.")
|
||||||
remote_modify_parser.add_argument(
|
remote_modify_parser.add_argument(
|
||||||
"value", nargs="?", help="(optional) Value of the option."
|
"value", nargs="?", help="(optional) Value of the option."
|
||||||
@@ -252,7 +264,9 @@ def add_parser(subparsers, parent_parser):
|
|||||||
help=REMOTE_REMOVE_HELP,
|
help=REMOTE_REMOVE_HELP,
|
||||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
remote_remove_parser.add_argument("name", help="Name of the remote to remove.")
|
remote_remove_parser.add_argument(
|
||||||
|
"name", help="Name of the remote to remove."
|
||||||
|
).complete = completion.REMOTE
|
||||||
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
|
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
|
||||||
REMOTE_RENAME_HELP = "Rename a DVC remote"
|
REMOTE_RENAME_HELP = "Rename a DVC remote"
|
||||||
remote_rename_parser = remote_subparsers.add_parser(
|
remote_rename_parser = remote_subparsers.add_parser(
|
||||||
|
|||||||
@@ -117,10 +117,7 @@ and then the stage name name.
|
|||||||
"--pull",
|
"--pull",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help=(
|
help="Try automatically pulling missing data.",
|
||||||
"Try automatically pulling missing cache for outputs restored "
|
|
||||||
"from the run-cache."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
repro_parser.add_argument(
|
repro_parser.add_argument(
|
||||||
"--allow-missing",
|
"--allow-missing",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def prepare_stages_data(
|
|||||||
class CmdStageList(CmdBase):
|
class CmdStageList(CmdBase):
|
||||||
def _get_stages(self) -> Iterable["Stage"]:
|
def _get_stages(self) -> Iterable["Stage"]:
|
||||||
if self.args.all:
|
if self.args.all:
|
||||||
stages: list["Stage"] = self.repo.index.stages
|
stages: list[Stage] = self.repo.index.stages
|
||||||
logger.trace("%d no. of stages found", len(stages))
|
logger.trace("%d no. of stages found", len(stages))
|
||||||
return stages
|
return stages
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class CmdDataStatus(CmdDataBase):
|
|||||||
all_commits=self.args.all_commits,
|
all_commits=self.args.all_commits,
|
||||||
with_deps=self.args.with_deps,
|
with_deps=self.args.with_deps,
|
||||||
recursive=self.args.recursive,
|
recursive=self.args.recursive,
|
||||||
|
check_updates=self.args.check_updates,
|
||||||
)
|
)
|
||||||
except DvcException:
|
except DvcException:
|
||||||
logger.exception("")
|
logger.exception("")
|
||||||
@@ -70,7 +71,7 @@ class CmdDataStatus(CmdDataBase):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
if self.args.quiet:
|
if self.args.quiet:
|
||||||
return bool(st)
|
return int(bool(st))
|
||||||
|
|
||||||
if st:
|
if st:
|
||||||
self._show(st, indent)
|
self._show(st, indent)
|
||||||
|
|||||||
+37
-12
@@ -5,6 +5,7 @@ from funcy import get_in
|
|||||||
from dvc.cli import formatter
|
from dvc.cli import formatter
|
||||||
from dvc.cli.utils import append_doc_link
|
from dvc.cli.utils import append_doc_link
|
||||||
from dvc.commands.config import CmdConfig
|
from dvc.commands.config import CmdConfig
|
||||||
|
from dvc.exceptions import DvcException
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
@@ -12,22 +13,35 @@ logger = logger.getChild(__name__)
|
|||||||
|
|
||||||
class CmdStudioLogin(CmdConfig):
|
class CmdStudioLogin(CmdConfig):
|
||||||
def run(self):
|
def run(self):
|
||||||
from dvc_studio_client.auth import StudioAuthError, get_access_token
|
|
||||||
|
|
||||||
from dvc.env import DVC_STUDIO_URL
|
from dvc.env import DVC_STUDIO_URL
|
||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
from dvc.utils.studio import STUDIO_URL
|
from dvc.utils.studio import STUDIO_URL
|
||||||
|
from dvc_studio_client.auth import StudioAuthError, get_access_token
|
||||||
|
|
||||||
|
studio = self.config.get("studio", {})
|
||||||
name = self.args.name
|
name = self.args.name
|
||||||
hostname = self.args.hostname or os.environ.get(DVC_STUDIO_URL) or STUDIO_URL
|
hostname = (
|
||||||
|
self.args.hostname
|
||||||
|
or os.environ.get(DVC_STUDIO_URL)
|
||||||
|
or studio.get("url")
|
||||||
|
or STUDIO_URL
|
||||||
|
)
|
||||||
scopes = self.args.scopes
|
scopes = self.args.scopes
|
||||||
|
|
||||||
|
if studio.get("url", hostname) == hostname and "token" in studio:
|
||||||
|
raise DvcException(
|
||||||
|
"Token already exists. "
|
||||||
|
"To login with a different token, "
|
||||||
|
"logout using 'dvc studio logout'."
|
||||||
|
)
|
||||||
|
|
||||||
|
open_browser = not self.args.no_open
|
||||||
try:
|
try:
|
||||||
token_name, access_token = get_access_token(
|
_, access_token = get_access_token(
|
||||||
token_name=name,
|
token_name=name,
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
scopes=scopes,
|
scopes=scopes,
|
||||||
use_device_code=self.args.no_open,
|
open_browser=open_browser,
|
||||||
client_name="DVC",
|
client_name="DVC",
|
||||||
)
|
)
|
||||||
except StudioAuthError as e:
|
except StudioAuthError as e:
|
||||||
@@ -35,17 +49,26 @@ class CmdStudioLogin(CmdConfig):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
self.save_config(hostname, access_token)
|
self.save_config(hostname, access_token)
|
||||||
ui.write(
|
|
||||||
"Authentication has been successfully completed."
|
if not self.config["exp"].get("auto_push", True):
|
||||||
"The generated token will now be accessible as"
|
from dvc.ui import ui
|
||||||
f" {token_name} in the user's Studio profile."
|
|
||||||
)
|
ui.warn(
|
||||||
|
"exp.auto_push is disabled. \n"
|
||||||
|
"Enable with 'dvc config exp.auto_push true' "
|
||||||
|
"to automatically push experiments to Studio."
|
||||||
|
)
|
||||||
|
|
||||||
|
config_path = self.config.files["global"]
|
||||||
|
ui.write(f"Authentication complete. Saved token to {config_path}.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def save_config(self, hostname, token):
|
def save_config(self, hostname, token):
|
||||||
with self.config.edit("global") as conf:
|
with self.config.edit("global") as conf:
|
||||||
conf["studio"]["token"] = token
|
conf["studio"]["token"] = token
|
||||||
conf["studio"]["url"] = hostname
|
conf["studio"]["url"] = hostname
|
||||||
|
if "auto_push" not in conf["exp"]:
|
||||||
|
conf["exp"]["auto_push"] = True
|
||||||
|
|
||||||
|
|
||||||
class CmdStudioLogout(CmdConfig):
|
class CmdStudioLogout(CmdConfig):
|
||||||
@@ -54,12 +77,14 @@ class CmdStudioLogout(CmdConfig):
|
|||||||
|
|
||||||
with self.config.edit("global") as conf:
|
with self.config.edit("global") as conf:
|
||||||
if not get_in(conf, ["studio", "token"]):
|
if not get_in(conf, ["studio", "token"]):
|
||||||
ui.error_write("Not logged in to Studio.")
|
ui.error_write(
|
||||||
|
"Not logged in to Studio. Log in with 'dvc studio login'."
|
||||||
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
del conf["studio"]["token"]
|
del conf["studio"]["token"]
|
||||||
|
|
||||||
ui.write("Logged out from Studio")
|
ui.write("Logged out from Studio (you can log back in with 'dvc studio login')")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ def add_parser(subparsers, parent_parser):
|
|||||||
"--remote",
|
"--remote",
|
||||||
help="Remote storage to perform updates to",
|
help="Remote storage to perform updates to",
|
||||||
metavar="<name>",
|
metavar="<name>",
|
||||||
)
|
).complete = completion.REMOTE
|
||||||
update_parser.add_argument(
|
update_parser.add_argument(
|
||||||
"-j",
|
"-j",
|
||||||
"--jobs",
|
"--jobs",
|
||||||
|
|||||||
+3
-5
@@ -88,12 +88,10 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
return list(it)
|
return list(it)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __setitem__(self, item: int, value: Sequence["CellT"]) -> None:
|
def __setitem__(self, item: int, value: Sequence["CellT"]) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __setitem__(self, item: slice, value: Iterable[Sequence["CellT"]]) -> None:
|
def __setitem__(self, item: slice, value: Iterable[Sequence["CellT"]]) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
def __setitem__(self, item, value) -> None:
|
def __setitem__(self, item, value) -> None:
|
||||||
it = value
|
it = value
|
||||||
@@ -162,7 +160,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
|||||||
if key not in keys:
|
if key not in keys:
|
||||||
self.add_column(key)
|
self.add_column(key)
|
||||||
|
|
||||||
row: list["CellT"] = [
|
row: list[CellT] = [
|
||||||
with_value(d.get(key), self._fill_value) for key in self.keys()
|
with_value(d.get(key), self._fill_value) for key in self.keys()
|
||||||
]
|
]
|
||||||
self.append(row)
|
self.append(row)
|
||||||
|
|||||||
+13
-12
@@ -1,5 +1,8 @@
|
|||||||
"""DVC config objects."""
|
"""DVC config objects."""
|
||||||
|
|
||||||
|
import ntpath
|
||||||
import os
|
import os
|
||||||
|
import posixpath
|
||||||
import re
|
import re
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -38,18 +41,6 @@ class RemoteNotFoundError(RemoteConfigError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MachineConfigError(ConfigError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NoMachineError(MachineConfigError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MachineNotFoundError(MachineConfigError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@memoize
|
@memoize
|
||||||
def get_compiled_schema():
|
def get_compiled_schema():
|
||||||
from voluptuous import Schema
|
from voluptuous import Schema
|
||||||
@@ -136,6 +127,7 @@ class Config(dict):
|
|||||||
return global_config_dir()
|
return global_config_dir()
|
||||||
if level == "system":
|
if level == "system":
|
||||||
return system_config_dir()
|
return system_config_dir()
|
||||||
|
return None
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def files(self) -> dict[str, str]:
|
def files(self) -> dict[str, str]:
|
||||||
@@ -267,12 +259,17 @@ class Config(dict):
|
|||||||
if re.match(r"\w+://", path):
|
if re.match(r"\w+://", path):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
if os.name == "nt" and posixpath.isabs(path) and ntpath.sep not in path:
|
||||||
|
return path
|
||||||
|
|
||||||
if os.path.isabs(path):
|
if os.path.isabs(path):
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# on windows convert slashes to backslashes
|
# on windows convert slashes to backslashes
|
||||||
# to have path compatible with abs_conf_dir
|
# to have path compatible with abs_conf_dir
|
||||||
if os.path.sep == "\\" and "/" in path:
|
if os.path.sep == "\\" and "/" in path:
|
||||||
|
if path.startswith("/"):
|
||||||
|
path = path.replace("/", "\\\\", 1)
|
||||||
path = path.replace("/", "\\")
|
path = path.replace("/", "\\")
|
||||||
|
|
||||||
expanded = os.path.expanduser(path)
|
expanded = os.path.expanduser(path)
|
||||||
@@ -304,6 +301,9 @@ class Config(dict):
|
|||||||
if os.path.expanduser(path) != path:
|
if os.path.expanduser(path) != path:
|
||||||
return localfs.as_posix(path)
|
return localfs.as_posix(path)
|
||||||
|
|
||||||
|
if os.name == "nt" and posixpath.isabs(path) and ntpath.sep not in path:
|
||||||
|
return path
|
||||||
|
|
||||||
if isinstance(path, RelPath) or not os.path.isabs(path):
|
if isinstance(path, RelPath) or not os.path.isabs(path):
|
||||||
path = relpath(path, conf_dir)
|
path = relpath(path, conf_dir)
|
||||||
return localfs.as_posix(path)
|
return localfs.as_posix(path)
|
||||||
@@ -329,6 +329,7 @@ class Config(dict):
|
|||||||
"gdrive_user_credentials_file": func,
|
"gdrive_user_credentials_file": func,
|
||||||
"gdrive_service_account_json_file_path": func,
|
"gdrive_service_account_json_file_path": func,
|
||||||
"credentialpath": func,
|
"credentialpath": func,
|
||||||
|
"configpath": func,
|
||||||
"keyfile": func,
|
"keyfile": func,
|
||||||
"cert_path": func,
|
"cert_path": func,
|
||||||
"key_path": func,
|
"key_path": func,
|
||||||
|
|||||||
+175
-134
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from funcy import once, walk_values
|
from funcy import once, walk_values
|
||||||
@@ -10,6 +11,7 @@ from voluptuous import (
|
|||||||
Exclusive,
|
Exclusive,
|
||||||
Invalid,
|
Invalid,
|
||||||
Lower,
|
Lower,
|
||||||
|
Marker,
|
||||||
Optional,
|
Optional,
|
||||||
Range,
|
Range,
|
||||||
Schema,
|
Schema,
|
||||||
@@ -17,6 +19,9 @@ from voluptuous import (
|
|||||||
|
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
Bool = All(
|
Bool = All(
|
||||||
@@ -112,17 +117,19 @@ class FeatureSchema(Schema):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
DEPRECATED = "==DEPRECATED=="
|
||||||
|
|
||||||
REMOTE_COMMON = {
|
REMOTE_COMMON = {
|
||||||
"url": str,
|
"url": str,
|
||||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||||
"jobs": All(Coerce(int), Range(1)),
|
"jobs": All(Coerce(int), Range(1)),
|
||||||
Optional("worktree"): Bool,
|
Optional("worktree"): Bool,
|
||||||
Optional("no_traverse"): Bool, # obsoleted
|
Optional("no_traverse", description=DEPRECATED): Bool, # obsoleted
|
||||||
Optional("version_aware"): Bool,
|
Optional("version_aware"): Bool,
|
||||||
}
|
}
|
||||||
LOCAL_COMMON = {
|
LOCAL_COMMON = {
|
||||||
"type": supported_cache_type,
|
"type": supported_cache_type,
|
||||||
Optional("protected", default=False): Bool, # obsoleted
|
Optional("protected", default=False, description=DEPRECATED): Bool, # obsoleted
|
||||||
"shared": All(Lower, Choices("group")),
|
"shared": All(Lower, Choices("group")),
|
||||||
Optional("slow_link_warning", default=True): Bool,
|
Optional("slow_link_warning", default=True): Bool,
|
||||||
Optional("verify", default=False): Bool,
|
Optional("verify", default=False): Bool,
|
||||||
@@ -144,6 +151,7 @@ WEBDAV_COMMON = {
|
|||||||
"password": str,
|
"password": str,
|
||||||
"ask_password": Bool,
|
"ask_password": Bool,
|
||||||
"token": str,
|
"token": str,
|
||||||
|
"bearer_token_command": str,
|
||||||
"custom_auth_header": str,
|
"custom_auth_header": str,
|
||||||
"cert_path": str,
|
"cert_path": str,
|
||||||
"key_path": str,
|
"key_path": str,
|
||||||
@@ -152,6 +160,127 @@ WEBDAV_COMMON = {
|
|||||||
Optional("verify", default=False): Bool,
|
Optional("verify", default=False): Bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
REMOTE_SCHEMAS = {
|
||||||
|
"": LOCAL_COMMON | REMOTE_COMMON,
|
||||||
|
"s3": {
|
||||||
|
"region": str,
|
||||||
|
"profile": str,
|
||||||
|
"credentialpath": str,
|
||||||
|
"configpath": str,
|
||||||
|
"endpointurl": str,
|
||||||
|
"access_key_id": str,
|
||||||
|
"secret_access_key": str,
|
||||||
|
"session_token": str,
|
||||||
|
Optional(
|
||||||
|
"listobjects", default=False, description=DEPRECATED
|
||||||
|
): Bool, # obsoleted
|
||||||
|
Optional("use_ssl", default=True): Bool,
|
||||||
|
Optional("allow_anonymous_login", default=False): Bool,
|
||||||
|
"ssl_verify": Any(Bool, str),
|
||||||
|
"sse": str,
|
||||||
|
"sse_kms_key_id": str,
|
||||||
|
"sse_customer_algorithm": str,
|
||||||
|
"sse_customer_key": str,
|
||||||
|
"acl": str,
|
||||||
|
"grant_read": str,
|
||||||
|
"grant_read_acp": str,
|
||||||
|
"grant_write_acp": str,
|
||||||
|
"grant_full_control": str,
|
||||||
|
"cache_regions": bool,
|
||||||
|
"read_timeout": Coerce(int),
|
||||||
|
"connect_timeout": Coerce(int),
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
|
**REMOTE_COMMON,
|
||||||
|
},
|
||||||
|
"gs": {
|
||||||
|
"projectname": str,
|
||||||
|
"credentialpath": str,
|
||||||
|
"endpointurl": str,
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
|
Optional("allow_anonymous_login", default=False): Bool,
|
||||||
|
**REMOTE_COMMON,
|
||||||
|
},
|
||||||
|
"ssh": {
|
||||||
|
"type": supported_cache_type,
|
||||||
|
"port": Coerce(int),
|
||||||
|
"user": str,
|
||||||
|
"password": str,
|
||||||
|
"ask_password": Bool,
|
||||||
|
"passphrase": str,
|
||||||
|
"ask_passphrase": Bool,
|
||||||
|
"keyfile": str,
|
||||||
|
"timeout": Coerce(int),
|
||||||
|
"gss_auth": Bool,
|
||||||
|
"allow_agent": Bool,
|
||||||
|
"max_sessions": Coerce(int),
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
|
**REMOTE_COMMON,
|
||||||
|
},
|
||||||
|
"hdfs": {
|
||||||
|
"user": str,
|
||||||
|
"kerb_ticket": str,
|
||||||
|
"replication": int,
|
||||||
|
**REMOTE_COMMON,
|
||||||
|
},
|
||||||
|
"webhdfs": {
|
||||||
|
"kerberos": Bool,
|
||||||
|
"kerberos_principal": str,
|
||||||
|
"proxy_to": str,
|
||||||
|
"ssl_verify": Any(Bool, str),
|
||||||
|
"token": str,
|
||||||
|
"use_https": Bool,
|
||||||
|
"user": str,
|
||||||
|
"password": str,
|
||||||
|
"data_proxy_target": str,
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
|
**REMOTE_COMMON,
|
||||||
|
},
|
||||||
|
"azure": {
|
||||||
|
"connection_string": str,
|
||||||
|
"sas_token": str,
|
||||||
|
"account_name": str,
|
||||||
|
"account_key": str,
|
||||||
|
"tenant_id": str,
|
||||||
|
"client_id": str,
|
||||||
|
"client_secret": str,
|
||||||
|
"allow_anonymous_login": Bool,
|
||||||
|
"exclude_environment_credential": Bool,
|
||||||
|
"exclude_visual_studio_code_credential": Bool,
|
||||||
|
"exclude_shared_token_cache_credential": Bool,
|
||||||
|
"exclude_managed_identity_credential": Bool,
|
||||||
|
Optional("verify", default=False): Bool,
|
||||||
|
"timeout": Coerce(int),
|
||||||
|
"read_timeout": Coerce(int),
|
||||||
|
"connection_timeout": Coerce(int),
|
||||||
|
**REMOTE_COMMON,
|
||||||
|
},
|
||||||
|
"oss": {
|
||||||
|
"oss_key_id": str,
|
||||||
|
"oss_key_secret": str,
|
||||||
|
"oss_endpoint": str,
|
||||||
|
Optional("verify", default=True): Bool,
|
||||||
|
**REMOTE_COMMON,
|
||||||
|
},
|
||||||
|
"gdrive": {
|
||||||
|
"profile": str,
|
||||||
|
"gdrive_use_service_account": Bool,
|
||||||
|
"gdrive_client_id": str,
|
||||||
|
"gdrive_client_secret": str,
|
||||||
|
"gdrive_user_credentials_file": str,
|
||||||
|
"gdrive_service_account_user_email": str,
|
||||||
|
"gdrive_service_account_json_file_path": str,
|
||||||
|
Optional("gdrive_trash_only", default=False): Bool,
|
||||||
|
Optional("gdrive_acknowledge_abuse", default=False): Bool,
|
||||||
|
Optional("verify", default=True): Bool,
|
||||||
|
**REMOTE_COMMON,
|
||||||
|
},
|
||||||
|
"http": HTTP_COMMON | REMOTE_COMMON,
|
||||||
|
"https": HTTP_COMMON | REMOTE_COMMON,
|
||||||
|
"webdav": WEBDAV_COMMON | REMOTE_COMMON,
|
||||||
|
"webdavs": WEBDAV_COMMON | REMOTE_COMMON,
|
||||||
|
"remote": {str: object}, # Any of the above options are valid
|
||||||
|
}
|
||||||
|
|
||||||
SCHEMA = {
|
SCHEMA = {
|
||||||
"core": {
|
"core": {
|
||||||
"remote": Lower,
|
"remote": Lower,
|
||||||
@@ -161,145 +290,37 @@ SCHEMA = {
|
|||||||
Optional("hardlink_lock", default=False): Bool,
|
Optional("hardlink_lock", default=False): Bool,
|
||||||
Optional("no_scm", default=False): Bool,
|
Optional("no_scm", default=False): Bool,
|
||||||
Optional("autostage", default=False): Bool,
|
Optional("autostage", default=False): Bool,
|
||||||
Optional("experiments"): Bool, # obsoleted
|
Optional("experiments", description=DEPRECATED): Bool, # obsoleted
|
||||||
Optional("check_update", default=True): Bool,
|
Optional("check_update", default=True): Bool,
|
||||||
"site_cache_dir": str,
|
"site_cache_dir": str,
|
||||||
"machine": Lower,
|
"machine": Lower,
|
||||||
},
|
},
|
||||||
"cache": {
|
"cache": {
|
||||||
"local": str, # obsoleted
|
Marker("local", description=DEPRECATED): str, # obsoleted
|
||||||
"s3": str, # obsoleted
|
Marker("s3", description=DEPRECATED): str, # obsoleted
|
||||||
"gs": str, # obsoleted
|
Marker("gs", description=DEPRECATED): str, # obsoleted
|
||||||
"hdfs": str, # obsoleted
|
Marker("hdfs", description=DEPRECATED): str, # obsoleted
|
||||||
"webhdfs": str, # obsoleted
|
Marker("webhdfs", description=DEPRECATED): str, # obsoleted
|
||||||
"ssh": str, # obsoleted
|
Marker("ssh", description=DEPRECATED): str, # obsoleted
|
||||||
"azure": str, # obsoleted
|
Marker("azure", description=DEPRECATED): str, # obsoleted
|
||||||
# This is for default local cache
|
# This is for default local cache
|
||||||
"dir": str,
|
"dir": str,
|
||||||
**LOCAL_COMMON,
|
**LOCAL_COMMON,
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
str: ByUrl(
|
str: ByUrl(REMOTE_SCHEMAS),
|
||||||
{
|
|
||||||
"": {**LOCAL_COMMON, **REMOTE_COMMON},
|
|
||||||
"s3": {
|
|
||||||
"region": str,
|
|
||||||
"profile": str,
|
|
||||||
"credentialpath": str,
|
|
||||||
"configpath": str,
|
|
||||||
"endpointurl": str,
|
|
||||||
"access_key_id": str,
|
|
||||||
"secret_access_key": str,
|
|
||||||
"session_token": str,
|
|
||||||
Optional("listobjects", default=False): Bool, # obsoleted
|
|
||||||
Optional("use_ssl", default=True): Bool,
|
|
||||||
"ssl_verify": Any(Bool, str),
|
|
||||||
"sse": str,
|
|
||||||
"sse_kms_key_id": str,
|
|
||||||
"sse_customer_algorithm": str,
|
|
||||||
"sse_customer_key": str,
|
|
||||||
"acl": str,
|
|
||||||
"grant_read": str,
|
|
||||||
"grant_read_acp": str,
|
|
||||||
"grant_write_acp": str,
|
|
||||||
"grant_full_control": str,
|
|
||||||
"cache_regions": bool,
|
|
||||||
"read_timeout": Coerce(int),
|
|
||||||
"connect_timeout": Coerce(int),
|
|
||||||
Optional("verify", default=False): Bool,
|
|
||||||
**REMOTE_COMMON,
|
|
||||||
},
|
|
||||||
"gs": {
|
|
||||||
"projectname": str,
|
|
||||||
"credentialpath": str,
|
|
||||||
"endpointurl": str,
|
|
||||||
Optional("verify", default=False): Bool,
|
|
||||||
**REMOTE_COMMON,
|
|
||||||
},
|
|
||||||
"ssh": {
|
|
||||||
"type": supported_cache_type,
|
|
||||||
"port": Coerce(int),
|
|
||||||
"user": str,
|
|
||||||
"password": str,
|
|
||||||
"ask_password": Bool,
|
|
||||||
"passphrase": str,
|
|
||||||
"ask_passphrase": Bool,
|
|
||||||
"keyfile": str,
|
|
||||||
"timeout": Coerce(int),
|
|
||||||
"gss_auth": Bool,
|
|
||||||
"allow_agent": Bool,
|
|
||||||
"max_sessions": Coerce(int),
|
|
||||||
Optional("verify", default=False): Bool,
|
|
||||||
**REMOTE_COMMON,
|
|
||||||
},
|
|
||||||
"hdfs": {"user": str, "kerb_ticket": str, **REMOTE_COMMON},
|
|
||||||
"webhdfs": {
|
|
||||||
"kerberos": Bool,
|
|
||||||
"kerberos_principal": str,
|
|
||||||
"proxy_to": str,
|
|
||||||
"ssl_verify": Any(Bool, str),
|
|
||||||
"token": str,
|
|
||||||
"use_https": Bool,
|
|
||||||
"user": str,
|
|
||||||
"password": str,
|
|
||||||
"data_proxy_target": str,
|
|
||||||
Optional("verify", default=False): Bool,
|
|
||||||
**REMOTE_COMMON,
|
|
||||||
},
|
|
||||||
"azure": {
|
|
||||||
"connection_string": str,
|
|
||||||
"sas_token": str,
|
|
||||||
"account_name": str,
|
|
||||||
"account_key": str,
|
|
||||||
"tenant_id": str,
|
|
||||||
"client_id": str,
|
|
||||||
"client_secret": str,
|
|
||||||
"allow_anonymous_login": Bool,
|
|
||||||
"exclude_environment_credential": Bool,
|
|
||||||
"exclude_visual_studio_code_credential": Bool,
|
|
||||||
"exclude_shared_token_cache_credential": Bool,
|
|
||||||
"exclude_managed_identity_credential": Bool,
|
|
||||||
Optional("verify", default=False): Bool,
|
|
||||||
"timeout": Coerce(int),
|
|
||||||
"read_timeout": Coerce(int),
|
|
||||||
"connection_timeout": Coerce(int),
|
|
||||||
**REMOTE_COMMON,
|
|
||||||
},
|
|
||||||
"oss": {
|
|
||||||
"oss_key_id": str,
|
|
||||||
"oss_key_secret": str,
|
|
||||||
"oss_endpoint": str,
|
|
||||||
Optional("verify", default=True): Bool,
|
|
||||||
**REMOTE_COMMON,
|
|
||||||
},
|
|
||||||
"gdrive": {
|
|
||||||
"profile": str,
|
|
||||||
"gdrive_use_service_account": Bool,
|
|
||||||
"gdrive_client_id": str,
|
|
||||||
"gdrive_client_secret": str,
|
|
||||||
"gdrive_user_credentials_file": str,
|
|
||||||
"gdrive_service_account_user_email": str,
|
|
||||||
"gdrive_service_account_json_file_path": str,
|
|
||||||
Optional("gdrive_trash_only", default=False): Bool,
|
|
||||||
Optional("gdrive_acknowledge_abuse", default=False): Bool,
|
|
||||||
Optional("verify", default=True): Bool,
|
|
||||||
**REMOTE_COMMON,
|
|
||||||
},
|
|
||||||
"http": {**HTTP_COMMON, **REMOTE_COMMON},
|
|
||||||
"https": {**HTTP_COMMON, **REMOTE_COMMON},
|
|
||||||
"webdav": {**WEBDAV_COMMON, **REMOTE_COMMON},
|
|
||||||
"webdavs": {**WEBDAV_COMMON, **REMOTE_COMMON},
|
|
||||||
"remote": {str: object}, # Any of the above options are valid
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"dir": str, # obsoleted
|
Marker("dir", description=DEPRECATED): str, # obsoleted
|
||||||
"row_limit": All(Coerce(int), Range(1)), # obsoleted
|
Marker("row_limit", description=DEPRECATED): All(
|
||||||
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
|
Coerce(int), Range(1)
|
||||||
|
), # obsoleted
|
||||||
|
Marker("row_cleanup_quota", description=DEPRECATED): All(
|
||||||
|
Coerce(int), Range(0, 100)
|
||||||
|
), # obsoleted
|
||||||
},
|
},
|
||||||
"index": {
|
"index": {
|
||||||
"dir": str, # obsoleted
|
Marker("dir", description=DEPRECATED): str, # obsoleted
|
||||||
},
|
},
|
||||||
"machine": {
|
"machine": {
|
||||||
str: {
|
str: {
|
||||||
@@ -329,13 +350,15 @@ SCHEMA = {
|
|||||||
"out_dir": str,
|
"out_dir": str,
|
||||||
},
|
},
|
||||||
"exp": {
|
"exp": {
|
||||||
"code": str,
|
Marker("code", description=DEPRECATED): str,
|
||||||
"data": str,
|
Marker("data", description=DEPRECATED): str,
|
||||||
"models": str,
|
Marker("models", description=DEPRECATED): str,
|
||||||
"metrics": str,
|
Marker("metrics", description=DEPRECATED): str,
|
||||||
"params": str,
|
Marker("params", description=DEPRECATED): str,
|
||||||
"plots": str,
|
Marker("plots", description=DEPRECATED): str,
|
||||||
"live": str,
|
Marker("live", description=DEPRECATED): str,
|
||||||
|
"auto_push": Bool,
|
||||||
|
"git_remote": str,
|
||||||
},
|
},
|
||||||
"parsing": {
|
"parsing": {
|
||||||
"bool": All(Lower, Choices("store_true", "boolean_optional")),
|
"bool": All(Lower, Choices("store_true", "boolean_optional")),
|
||||||
@@ -362,3 +385,21 @@ SCHEMA = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def config_vars_for_completion(d: dict = SCHEMA, path: str = "") -> "Iterator[str]":
|
||||||
|
for k, v in d.items():
|
||||||
|
if k in ("machine", "feature"):
|
||||||
|
continue
|
||||||
|
if isinstance(k, Marker):
|
||||||
|
if k.description == DEPRECATED:
|
||||||
|
continue
|
||||||
|
k = k.schema
|
||||||
|
if not isinstance(k, str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
keypath = path + k
|
||||||
|
if isinstance(v, dict):
|
||||||
|
yield from config_vars_for_completion(v, keypath + ".")
|
||||||
|
else:
|
||||||
|
yield keypath
|
||||||
|
|||||||
+8
-8
@@ -32,10 +32,10 @@ def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
|
|||||||
assert os.name == "nt"
|
assert os.name == "nt"
|
||||||
|
|
||||||
from subprocess import ( # type: ignore[attr-defined]
|
from subprocess import ( # type: ignore[attr-defined]
|
||||||
CREATE_NEW_PROCESS_GROUP,
|
CREATE_NEW_PROCESS_GROUP, # ty: ignore[unresolved-import]
|
||||||
CREATE_NO_WINDOW,
|
CREATE_NO_WINDOW, # ty: ignore[unresolved-import]
|
||||||
STARTF_USESHOWWINDOW,
|
STARTF_USESHOWWINDOW, # ty: ignore[unresolved-import]
|
||||||
STARTUPINFO,
|
STARTUPINFO, # ty: ignore[unresolved-import]
|
||||||
)
|
)
|
||||||
|
|
||||||
# https://stackoverflow.com/a/7006424
|
# https://stackoverflow.com/a/7006424
|
||||||
@@ -44,10 +44,10 @@ def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
|
|||||||
|
|
||||||
startupinfo = STARTUPINFO()
|
startupinfo = STARTUPINFO()
|
||||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||||
popen = subprocess.Popen(
|
popen = subprocess.Popen( # noqa: S603
|
||||||
args,
|
args,
|
||||||
close_fds=True,
|
close_fds=True,
|
||||||
shell=False, # noqa: S603
|
shell=False,
|
||||||
startupinfo=startupinfo,
|
startupinfo=startupinfo,
|
||||||
creationflags=creationflags,
|
creationflags=creationflags,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -149,7 +149,7 @@ def _spawn(
|
|||||||
env: Optional[Mapping[str, str]] = None,
|
env: Optional[Mapping[str, str]] = None,
|
||||||
output_file: Optional[str] = None,
|
output_file: Optional[str] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
file: "AbstractContextManager[Any]" = nullcontext()
|
file: AbstractContextManager[Any] = nullcontext()
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if output_file:
|
if output_file:
|
||||||
file = open(output_file, "ab") # noqa: SIM115
|
file = open(output_file, "ab") # noqa: SIM115
|
||||||
@@ -164,7 +164,7 @@ def _spawn(
|
|||||||
return _detached_subprocess(executable + args, env=env, **kwargs)
|
return _detached_subprocess(executable + args, env=env, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def daemonize(args: list[str], executable: Union[None, str, list[str]] = None) -> None:
|
def daemonize(args: list[str], executable: Union[str, list[str], None] = None) -> None:
|
||||||
if os.name not in ("posix", "nt"):
|
if os.name not in ("posix", "nt"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
+13
-13
@@ -104,21 +104,21 @@ class AsciiCanvas:
|
|||||||
if dx == 0:
|
if dx == 0:
|
||||||
y = y0
|
y = y0
|
||||||
else:
|
else:
|
||||||
y = y0 + int(round((x - x0) * dy / float(dx)))
|
y = y0 + round((x - x0) * dy / float(dx))
|
||||||
self.point(x, y, char)
|
self.point(x, y, char)
|
||||||
elif y0 < y1:
|
elif y0 < y1:
|
||||||
for y in range(y0, y1 + 1):
|
for y in range(y0, y1 + 1):
|
||||||
if dy == 0:
|
if dy == 0:
|
||||||
x = x0
|
x = x0
|
||||||
else:
|
else:
|
||||||
x = x0 + int(round((y - y0) * dx / float(dy)))
|
x = x0 + round((y - y0) * dx / float(dy))
|
||||||
self.point(x, y, char)
|
self.point(x, y, char)
|
||||||
else:
|
else:
|
||||||
for y in range(y1, y0 + 1):
|
for y in range(y1, y0 + 1):
|
||||||
if dy == 0:
|
if dy == 0:
|
||||||
x = x0
|
x = x0
|
||||||
else:
|
else:
|
||||||
x = x1 + int(round((y - y1) * dx / float(dy)))
|
x = x1 + round((y - y1) * dx / float(dy))
|
||||||
self.point(x, y, char)
|
self.point(x, y, char)
|
||||||
|
|
||||||
def text(self, x, y, text):
|
def text(self, x, y, text):
|
||||||
@@ -195,7 +195,7 @@ def _build_sugiyama_layout(vertices, edges):
|
|||||||
|
|
||||||
sug.yspace = VertexViewer.HEIGHT
|
sug.yspace = VertexViewer.HEIGHT
|
||||||
sug.xspace = minw
|
sug.xspace = minw
|
||||||
sug.route_edge = route_with_lines
|
sug.route_edge = route_with_lines # ty: ignore[unresolved-attribute]
|
||||||
|
|
||||||
sug.draw()
|
sug.draw()
|
||||||
|
|
||||||
@@ -258,8 +258,8 @@ def draw(vertices, edges):
|
|||||||
maxx = max(Xs)
|
maxx = max(Xs)
|
||||||
maxy = max(Ys)
|
maxy = max(Ys)
|
||||||
|
|
||||||
canvas_cols = int(math.ceil(math.ceil(maxx) - math.floor(minx))) + 1
|
canvas_cols = math.ceil(math.ceil(maxx) - math.floor(minx)) + 1
|
||||||
canvas_lines = int(round(maxy - miny))
|
canvas_lines = round(maxy - miny)
|
||||||
|
|
||||||
canvas = AsciiCanvas(canvas_cols, canvas_lines)
|
canvas = AsciiCanvas(canvas_cols, canvas_lines)
|
||||||
|
|
||||||
@@ -270,10 +270,10 @@ def draw(vertices, edges):
|
|||||||
start = edge.view._pts[index - 1]
|
start = edge.view._pts[index - 1]
|
||||||
end = edge.view._pts[index]
|
end = edge.view._pts[index]
|
||||||
|
|
||||||
start_x = int(round(start[0] - minx))
|
start_x = round(start[0] - minx)
|
||||||
start_y = int(round(start[1] - miny))
|
start_y = round(start[1] - miny)
|
||||||
end_x = int(round(end[0] - minx))
|
end_x = round(end[0] - minx)
|
||||||
end_y = int(round(end[1] - miny))
|
end_y = round(end[1] - miny)
|
||||||
|
|
||||||
assert start_x >= 0
|
assert start_x >= 0
|
||||||
assert start_y >= 0
|
assert start_y >= 0
|
||||||
@@ -288,12 +288,12 @@ def draw(vertices, edges):
|
|||||||
y = vertex.view.xy[1]
|
y = vertex.view.xy[1]
|
||||||
|
|
||||||
canvas.box(
|
canvas.box(
|
||||||
int(round(x - minx)),
|
round(x - minx),
|
||||||
int(round(y - miny)),
|
round(y - miny),
|
||||||
vertex.view.w,
|
vertex.view.w,
|
||||||
vertex.view.h,
|
vertex.view.h,
|
||||||
)
|
)
|
||||||
|
|
||||||
canvas.text(int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data)
|
canvas.text(round(x - minx) + 1, round(y - miny) + 1, vertex.data)
|
||||||
|
|
||||||
return canvas.draw()
|
return canvas.draw()
|
||||||
|
|||||||
+5
-5
@@ -5,9 +5,9 @@ from dataclasses import dataclass
|
|||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine # type: ignore[import]
|
||||||
from sqlalchemy.engine import make_url as _make_url
|
from sqlalchemy.engine import make_url as _make_url # type: ignore[import]
|
||||||
from sqlalchemy.exc import NoSuchModuleError
|
from sqlalchemy.exc import NoSuchModuleError # type: ignore[import]
|
||||||
|
|
||||||
from dvc import env
|
from dvc import env
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
@@ -17,7 +17,7 @@ from dvc.utils import env2bool
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sqlalchemy.engine import URL, Connectable, Engine
|
from sqlalchemy.engine import URL, Connectable, Engine
|
||||||
from sqlalchemy.sql.expression import Selectable
|
from sqlalchemy.sql.expression import Selectable # type: ignore[import]
|
||||||
|
|
||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
@@ -84,7 +84,7 @@ class Client:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if callable(onerror):
|
if callable(onerror):
|
||||||
onerror()
|
onerror()
|
||||||
logger.exception(
|
logger.exception( # noqa: LOG007
|
||||||
"Could not connect to the database. "
|
"Could not connect to the database. "
|
||||||
"Check your database credentials and try again.",
|
"Check your database credentials and try again.",
|
||||||
exc_info=False,
|
exc_info=False,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def _get(stage, p, info, **kwargs):
|
|||||||
return Dependency(stage, p, info, **kwargs)
|
return Dependency(stage, p, info, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def loadd_from(stage, d_list):
|
def loadd_from(stage, d_list) -> list[Dependency]:
|
||||||
ret = []
|
ret = []
|
||||||
for d in d_list:
|
for d in d_list:
|
||||||
p = d.pop(Output.PARAM_PATH, None)
|
p = d.pop(Output.PARAM_PATH, None)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class Dependency(Output):
|
|||||||
self.fs_path = self.fs.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):
|
def download(self, to, jobs=None):
|
||||||
fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
|
return fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
super().save()
|
super().save()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
from funcy import compact, merge
|
from funcy import compact, merge
|
||||||
|
|
||||||
|
from dvc.exceptions import DvcException
|
||||||
from dvc_data.hashfile.hash_info import HashInfo
|
from dvc_data.hashfile.hash_info import HashInfo
|
||||||
|
|
||||||
from .db import AbstractDependency
|
from .db import AbstractDependency
|
||||||
@@ -47,9 +48,15 @@ class DatasetDependency(AbstractDependency):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def workspace_status(self):
|
def workspace_status(self):
|
||||||
registered = self.repo.index.datasets.get(self.name, {})
|
ds = self.repo.datasets[self.name]
|
||||||
|
if not ds.lock:
|
||||||
|
return {str(self): "not in sync"}
|
||||||
|
|
||||||
info: dict[str, Any] = self.hash_info.value if self.hash_info else {} # type: ignore[assignment]
|
info: dict[str, Any] = self.hash_info.value if self.hash_info else {} # type: ignore[assignment]
|
||||||
if info != registered:
|
lock = self.repo.datasets._lock_from_info(info)
|
||||||
|
if not lock:
|
||||||
|
return {str(self): "new"}
|
||||||
|
if lock != ds.lock:
|
||||||
return {str(self): "modified"}
|
return {str(self): "modified"}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -57,7 +64,15 @@ class DatasetDependency(AbstractDependency):
|
|||||||
return self.workspace_status()
|
return self.workspace_status()
|
||||||
|
|
||||||
def get_hash(self):
|
def get_hash(self):
|
||||||
return HashInfo(self.PARAM_DATASET, self.repo.index.datasets.get(self.name, {}))
|
ds = self.repo.datasets[self.name]
|
||||||
|
if not ds.lock:
|
||||||
|
if ds._invalidated:
|
||||||
|
raise DvcException(
|
||||||
|
"Dataset information is not in sync. "
|
||||||
|
f"Run 'dvc ds update {self.name}' to sync."
|
||||||
|
)
|
||||||
|
raise DvcException("Dataset information missing from dvc.lock file")
|
||||||
|
return HashInfo(self.PARAM_DATASET, ds.lock.to_dict()) # type: ignore[arg-type]
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
self.hash_info = self.get_hash()
|
self.hash_info = self.get_hash()
|
||||||
|
|||||||
+14
-6
@@ -11,6 +11,7 @@ from .base import Dependency
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dvc.output import Output
|
from dvc.output import Output
|
||||||
|
from dvc.repo import Repo
|
||||||
from dvc.stage import Stage
|
from dvc.stage import Stage
|
||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
@@ -21,9 +22,12 @@ def download_progress(to: "Output") -> Iterator[Callable[[int], Any]]:
|
|||||||
from dvc.ui import ui
|
from dvc.ui import ui
|
||||||
from dvc.ui._rich_progress import DbDownloadProgress
|
from dvc.ui._rich_progress import DbDownloadProgress
|
||||||
|
|
||||||
with log_durations(logger.debug, f"Saving to {to}"), DbDownloadProgress(
|
with (
|
||||||
console=ui.error_console,
|
log_durations(logger.debug, f"Saving to {to}"),
|
||||||
) as progress:
|
DbDownloadProgress(
|
||||||
|
console=ui.error_console,
|
||||||
|
) as progress,
|
||||||
|
):
|
||||||
task = progress.add_task("Saving", total=None, output=to)
|
task = progress.add_task("Saving", total=None, output=to)
|
||||||
yield lambda n: progress.advance(task, advance=n)
|
yield lambda n: progress.advance(task, advance=n)
|
||||||
progress.update(task, description="Saved", total=0)
|
progress.update(task, description="Saved", total=0)
|
||||||
@@ -33,13 +37,17 @@ class AbstractDependency(Dependency):
|
|||||||
"""Dependency without workspace/fs/fs_path"""
|
"""Dependency without workspace/fs/fs_path"""
|
||||||
|
|
||||||
def __init__(self, stage: "Stage", info: dict[str, Any], *args, **kwargs):
|
def __init__(self, stage: "Stage", info: dict[str, Any], *args, **kwargs):
|
||||||
self.repo = stage.repo
|
self.repo: Repo = stage.repo
|
||||||
self.stage = stage
|
self.stage = stage
|
||||||
self.fs = None
|
self.fs = None
|
||||||
self.fs_path = None
|
self.fs_path = None
|
||||||
self.def_path = None # type: ignore[assignment]
|
self.def_path = None # type: ignore[assignment]
|
||||||
self.info = info or {}
|
self.info = info or {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_in_repo(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class DbDependency(AbstractDependency):
|
class DbDependency(AbstractDependency):
|
||||||
PARAM_CONNECTION = "connection"
|
PARAM_CONNECTION = "connection"
|
||||||
@@ -66,8 +74,8 @@ class DbDependency(AbstractDependency):
|
|||||||
return self.db_info.get(self.PARAM_QUERY) or self.db_info.get(self.PARAM_TABLE)
|
return self.db_info.get(self.PARAM_QUERY) or self.db_info.get(self.PARAM_TABLE)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "{}:{}".format(
|
return "{}: {}".format(
|
||||||
self.__class__.__name__, "".join(f"{k}=={v}" for k, v in self.info.items())
|
self.__class__.__name__, "".join(f"{k}={v}" for k, v in self.info.items())
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class ParamsDependency(Dependency):
|
|||||||
except MissingParamsFile:
|
except MissingParamsFile:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
assert self.repo
|
||||||
try:
|
try:
|
||||||
return read_param_file(
|
return read_param_file(
|
||||||
self.repo.fs,
|
self.repo.fs,
|
||||||
|
|||||||
+30
-2
@@ -9,7 +9,9 @@ from .base import Dependency
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dvc.fs import DVCFileSystem
|
from dvc.fs import DVCFileSystem
|
||||||
|
from dvc.output import Output
|
||||||
from dvc.stage import Stage
|
from dvc.stage import Stage
|
||||||
|
from dvc_data.hashfile.hash_info import HashInfo
|
||||||
|
|
||||||
|
|
||||||
class RepoDependency(Dependency):
|
class RepoDependency(Dependency):
|
||||||
@@ -35,7 +37,7 @@ class RepoDependency(Dependency):
|
|||||||
super().__init__(stage, *args, **kwargs)
|
super().__init__(stage, *args, **kwargs)
|
||||||
|
|
||||||
self.fs = self._make_fs()
|
self.fs = self._make_fs()
|
||||||
self.fs_path = as_posix(self.def_path)
|
self.fs_path = as_posix(self.fs.normpath(self.def_path))
|
||||||
|
|
||||||
def _parse_path(self, fs, fs_path): # noqa: ARG002
|
def _parse_path(self, fs, fs_path): # noqa: ARG002
|
||||||
return None
|
return None
|
||||||
@@ -91,6 +93,27 @@ class RepoDependency(Dependency):
|
|||||||
self.PARAM_REPO: self._dump_def_repo(self.def_repo),
|
self.PARAM_REPO: self._dump_def_repo(self.def_repo),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def download(self, to: "Output", jobs: Optional[int] = None):
|
||||||
|
from dvc.fs import LocalFileSystem
|
||||||
|
|
||||||
|
files = super().download(to=to, jobs=jobs)
|
||||||
|
if not isinstance(to.fs, LocalFileSystem):
|
||||||
|
return
|
||||||
|
|
||||||
|
hashes: list[tuple[str, HashInfo, dict[str, Any]]] = []
|
||||||
|
for src_path, dest_path, maybe_info in files:
|
||||||
|
try:
|
||||||
|
info = maybe_info or self.fs.info(src_path)
|
||||||
|
hash_info = info["dvc_info"]["entry"].hash_info
|
||||||
|
dest_info = to.fs.info(dest_path)
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
# If no hash info found, just keep going and output will be hashed later
|
||||||
|
continue
|
||||||
|
if hash_info:
|
||||||
|
hashes.append((dest_path, hash_info, dest_info))
|
||||||
|
cache = to.cache if to.use_cache else to.local_cache
|
||||||
|
cache.state.save_many(hashes, to.fs)
|
||||||
|
|
||||||
def update(self, rev: Optional[str] = None):
|
def update(self, rev: Optional[str] = None):
|
||||||
if rev:
|
if rev:
|
||||||
self.def_repo[self.PARAM_REV] = rev
|
self.def_repo[self.PARAM_REV] = rev
|
||||||
@@ -123,11 +146,16 @@ class RepoDependency(Dependency):
|
|||||||
else:
|
else:
|
||||||
config = Config.load_file(conf)
|
config = Config.load_file(conf)
|
||||||
|
|
||||||
|
# Setup config to the new DVCFileSystem to use the remote repo, but rely on the
|
||||||
|
# local cache instead of the remote's cache. This avoids re-streaming of data,
|
||||||
|
# but messes up the call to `_get_remote_config()` downstream, which will need
|
||||||
|
# to ignore cache parameters.
|
||||||
|
assert self.repo
|
||||||
config["cache"] = self.repo.config["cache"]
|
config["cache"] = self.repo.config["cache"]
|
||||||
config["cache"]["dir"] = self.repo.cache.local_cache_dir
|
config["cache"]["dir"] = self.repo.cache.local_cache_dir
|
||||||
|
|
||||||
return DVCFileSystem(
|
return DVCFileSystem(
|
||||||
url=self.def_repo[self.PARAM_URL],
|
repo=self.def_repo[self.PARAM_URL],
|
||||||
rev=rev or self._get_rev(locked=locked),
|
rev=rev or self._get_rev(locked=locked),
|
||||||
subrepos=True,
|
subrepos=True,
|
||||||
config=config,
|
config=config,
|
||||||
|
|||||||
+22
-4
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import platformdirs
|
import platformdirs
|
||||||
|
|
||||||
@@ -20,7 +21,24 @@ def global_config_dir():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def site_cache_dir():
|
def site_cache_dir(config_site_cache_dir: Optional[str] = None):
|
||||||
return os.getenv(env.DVC_SITE_CACHE_DIR) or platformdirs.site_cache_dir(
|
from platformdirs import PlatformDirs
|
||||||
APPNAME, APPAUTHOR, opinion=True
|
from platformdirs.unix import Unix
|
||||||
)
|
|
||||||
|
if dvc_site_cache_dir := os.getenv(env.DVC_SITE_CACHE_DIR):
|
||||||
|
return dvc_site_cache_dir
|
||||||
|
|
||||||
|
if config_site_cache_dir is not None:
|
||||||
|
return config_site_cache_dir
|
||||||
|
|
||||||
|
if issubclass(Unix, PlatformDirs):
|
||||||
|
# Return the cache directory shared by users, e.g. `/var/tmp/$appname`
|
||||||
|
# NOTE: platformdirs>=5 changed `site_cache_dir` to return /var/cache/$appname.
|
||||||
|
# as the following path is considered insecure.
|
||||||
|
# For details, see: https://github.com/tox-dev/platformdirs/pull/239
|
||||||
|
|
||||||
|
# FIXME: keeping the old behavior temporarily to avoid dependency conflict.
|
||||||
|
# In the future, consider migrating to a more secure directory.
|
||||||
|
return f"/var/tmp/{APPNAME}" # noqa: S108
|
||||||
|
|
||||||
|
return platformdirs.site_cache_dir(APPNAME, APPAUTHOR, opinion=True)
|
||||||
|
|||||||
+117
-40
@@ -68,10 +68,9 @@ def is_git_ignored(repo, path):
|
|||||||
def check_dvcfile_path(repo, path):
|
def check_dvcfile_path(repo, path):
|
||||||
if not is_valid_filename(path):
|
if not is_valid_filename(path):
|
||||||
raise StageFileBadNameError(
|
raise StageFileBadNameError(
|
||||||
"bad DVC file name '{}'. DVC files should be named "
|
f"bad DVC file name '{relpath(path)}'. DVC files should be named "
|
||||||
"'{}' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
|
f"'{PROJECT_FILE}' or have a '.dvc' suffix "
|
||||||
relpath(path), PROJECT_FILE, os.path.basename(path)
|
f"(e.g. '{os.path.basename(path)}.dvc')."
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_git_ignored(repo, path):
|
if is_git_ignored(repo, path):
|
||||||
@@ -163,6 +162,9 @@ class FileMixin:
|
|||||||
def dump(self, stage, **kwargs):
|
def dump(self, stage, **kwargs):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def dump_stages(self, stages, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def merge(self, ancestor, other, allowed=None):
|
def merge(self, ancestor, other, allowed=None):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -172,6 +174,7 @@ class SingleStageFile(FileMixin):
|
|||||||
from dvc.stage.loader import SingleStageLoader as LOADER # noqa: N814
|
from dvc.stage.loader import SingleStageLoader as LOADER # noqa: N814
|
||||||
|
|
||||||
datasets: ClassVar[list[dict[str, Any]]] = []
|
datasets: ClassVar[list[dict[str, Any]]] = []
|
||||||
|
datasets_lock: ClassVar[list[dict[str, Any]]] = []
|
||||||
metrics: ClassVar[list[str]] = []
|
metrics: ClassVar[list[str]] = []
|
||||||
plots: ClassVar[Any] = {}
|
plots: ClassVar[Any] = {}
|
||||||
params: ClassVar[list[str]] = []
|
params: ClassVar[list[str]] = []
|
||||||
@@ -198,6 +201,13 @@ class SingleStageFile(FileMixin):
|
|||||||
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
|
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
|
||||||
self.repo.scm_context.track_file(self.relpath)
|
self.repo.scm_context.track_file(self.relpath)
|
||||||
|
|
||||||
|
def dump_stages(self, stages, **kwargs) -> None:
|
||||||
|
if not stages:
|
||||||
|
return None
|
||||||
|
|
||||||
|
assert len(stages) == 1, "SingleStageFile can only dump one stage."
|
||||||
|
return self.dump(stages[0], **kwargs)
|
||||||
|
|
||||||
def remove_stage(self, stage): # noqa: ARG002
|
def remove_stage(self, stage): # noqa: ARG002
|
||||||
self.remove()
|
self.remove()
|
||||||
|
|
||||||
@@ -228,44 +238,79 @@ class ProjectFile(FileMixin):
|
|||||||
|
|
||||||
def dump(self, stage, update_pipeline=True, update_lock=True, **kwargs):
|
def dump(self, stage, update_pipeline=True, update_lock=True, **kwargs):
|
||||||
"""Dumps given stage appropriately in the dvcfile."""
|
"""Dumps given stage appropriately in the dvcfile."""
|
||||||
|
return self.dump_stages(
|
||||||
|
[stage], update_pipeline=update_pipeline, update_lock=update_lock, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def dump_stages(self, stages, update_pipeline=True, update_lock=True, **kwargs):
|
||||||
from dvc.stage import PipelineStage
|
from dvc.stage import PipelineStage
|
||||||
|
|
||||||
assert isinstance(stage, PipelineStage)
|
if not stages:
|
||||||
|
return
|
||||||
|
|
||||||
|
for stage in stages:
|
||||||
|
assert isinstance(stage, PipelineStage)
|
||||||
|
|
||||||
if self.verify:
|
if self.verify:
|
||||||
check_dvcfile_path(self.repo, self.path)
|
check_dvcfile_path(self.repo, self.path)
|
||||||
|
|
||||||
if update_pipeline and not stage.is_data_source:
|
if update_pipeline:
|
||||||
self._dump_pipeline_file(stage)
|
self._dump_pipeline_file(stages)
|
||||||
|
|
||||||
if update_lock:
|
if update_lock:
|
||||||
self._dump_lockfile(stage, **kwargs)
|
self._dump_lockfile(stages, **kwargs)
|
||||||
|
|
||||||
def _dump_lockfile(self, stage, **kwargs):
|
def dump_dataset(self, dataset):
|
||||||
self._lockfile.dump(stage, **kwargs)
|
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||||
|
parsed = self.datasets if data else []
|
||||||
|
raw = data.setdefault("datasets", [])
|
||||||
|
loc = next(
|
||||||
|
(i for i, ds in enumerate(parsed) if ds["name"] == dataset["name"]),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if loc is not None:
|
||||||
|
if raw[loc] != parsed[loc]:
|
||||||
|
raise ParametrizedDumpError(
|
||||||
|
"cannot update a parametrized dataset entry"
|
||||||
|
)
|
||||||
|
|
||||||
|
apply_diff(dataset, raw[loc])
|
||||||
|
raw[loc] = dataset
|
||||||
|
else:
|
||||||
|
raw.append(dataset)
|
||||||
|
self.repo.scm_context.track_file(self.relpath)
|
||||||
|
|
||||||
|
def _dump_lockfile(self, stages, **kwargs):
|
||||||
|
self._lockfile.dump_stages(stages, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _check_if_parametrized(stage, action: str = "dump") -> None:
|
def _check_if_parametrized(stage, action: str = "dump") -> None:
|
||||||
if stage.raw_data.parametrized:
|
if stage.raw_data.parametrized:
|
||||||
raise ParametrizedDumpError(f"cannot {action} a parametrized {stage}")
|
raise ParametrizedDumpError(f"cannot {action} a parametrized {stage}")
|
||||||
|
|
||||||
def _dump_pipeline_file(self, stage):
|
def _dump_pipeline_file(self, stages):
|
||||||
self._check_if_parametrized(stage)
|
stages = stages if isinstance(stages, list) else [stages]
|
||||||
stage_data = serialize.to_pipeline_file(stage)
|
if not stages:
|
||||||
|
return
|
||||||
|
|
||||||
|
for stage in stages:
|
||||||
|
self._check_if_parametrized(stage)
|
||||||
|
|
||||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||||
if not data:
|
if not data:
|
||||||
logger.info("Creating '%s'", self.relpath)
|
logger.info("Creating '%s'", self.relpath)
|
||||||
|
|
||||||
data["stages"] = data.get("stages", {})
|
data["stages"] = data.get("stages", {})
|
||||||
existing_entry = stage.name in data["stages"]
|
for stage in stages:
|
||||||
action = "Modifying" if existing_entry else "Adding"
|
stage_data = serialize.to_pipeline_file(stage)
|
||||||
logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
|
existing_entry = stage.name in data["stages"]
|
||||||
|
action = "Modifying" if existing_entry else "Adding"
|
||||||
if existing_entry:
|
logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
|
||||||
orig_stage_data = data["stages"][stage.name]
|
if existing_entry:
|
||||||
apply_diff(stage_data[stage.name], orig_stage_data)
|
orig_stage_data = data["stages"][stage.name]
|
||||||
else:
|
apply_diff(stage_data[stage.name], orig_stage_data)
|
||||||
data["stages"].update(stage_data)
|
else:
|
||||||
|
data["stages"].update(stage_data)
|
||||||
|
|
||||||
self.repo.scm_context.track_file(self.relpath)
|
self.repo.scm_context.track_file(self.relpath)
|
||||||
|
|
||||||
@@ -293,24 +338,28 @@ class ProjectFile(FileMixin):
|
|||||||
return self.LOADER(self, self.contents, self.lockfile_contents)
|
return self.LOADER(self, self.contents, self.lockfile_contents)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metrics(self) -> list[str]:
|
def artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||||
return self.contents.get("metrics", [])
|
return self.resolver.resolve_artifacts()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plots(self) -> Any:
|
def metrics(self) -> list[str]:
|
||||||
return self.contents.get("plots", {})
|
return self.resolver.resolve_metrics()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def params(self) -> list[str]:
|
def params(self) -> list[str]:
|
||||||
return self.contents.get("params", [])
|
return self.resolver.resolve_params()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plots(self) -> list[Any]:
|
||||||
|
return self.resolver.resolve_plots()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def datasets(self) -> list[dict[str, Any]]:
|
def datasets(self) -> list[dict[str, Any]]:
|
||||||
return self.contents.get("datasets", [])
|
return self.resolver.resolve_datasets()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
|
def datasets_lock(self) -> list[dict[str, Any]]:
|
||||||
return self.contents.get("artifacts", {})
|
return self.lockfile_contents.get("datasets", [])
|
||||||
|
|
||||||
def remove(self, force=False):
|
def remove(self, force=False):
|
||||||
if not force:
|
if not force:
|
||||||
@@ -357,9 +406,30 @@ class Lockfile(FileMixin):
|
|||||||
self._check_gitignored()
|
self._check_gitignored()
|
||||||
return {}, ""
|
return {}, ""
|
||||||
|
|
||||||
def dump(self, stage, **kwargs):
|
def dump_dataset(self, dataset: dict):
|
||||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||||
|
data.update({"schema": "2.0"})
|
||||||
|
if not data:
|
||||||
|
logger.info("Generating lock file '%s'", self.relpath)
|
||||||
|
|
||||||
|
datasets: list[dict] = data.setdefault("datasets", [])
|
||||||
|
loc = next(
|
||||||
|
(i for i, ds in enumerate(datasets) if ds["name"] == dataset["name"]),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if loc is not None:
|
||||||
|
datasets[loc] = dataset
|
||||||
|
else:
|
||||||
|
datasets.append(dataset)
|
||||||
|
data.setdefault("stages", {})
|
||||||
|
self.repo.scm_context.track_file(self.relpath)
|
||||||
|
|
||||||
|
def dump_stages(self, stages, **kwargs):
|
||||||
|
if not stages:
|
||||||
|
return
|
||||||
|
|
||||||
|
is_modified = False
|
||||||
|
log_updated = False
|
||||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||||
if not data:
|
if not data:
|
||||||
data.update({"schema": "2.0"})
|
data.update({"schema": "2.0"})
|
||||||
@@ -367,17 +437,24 @@ class Lockfile(FileMixin):
|
|||||||
logger.info("Generating lock file '%s'", self.relpath)
|
logger.info("Generating lock file '%s'", self.relpath)
|
||||||
|
|
||||||
data["stages"] = data.get("stages", {})
|
data["stages"] = data.get("stages", {})
|
||||||
modified = data["stages"].get(stage.name, {}) != stage_data.get(
|
for stage in stages:
|
||||||
stage.name, {}
|
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||||
)
|
modified = data["stages"].get(stage.name, {}) != stage_data.get(
|
||||||
if modified:
|
stage.name, {}
|
||||||
logger.info("Updating lock file '%s'", self.relpath)
|
)
|
||||||
|
if modified:
|
||||||
|
is_modified = True
|
||||||
|
if not log_updated:
|
||||||
|
logger.info("Updating lock file '%s'", self.relpath)
|
||||||
|
log_updated = True
|
||||||
|
data["stages"].update(stage_data)
|
||||||
|
|
||||||
data["stages"].update(stage_data)
|
if is_modified:
|
||||||
|
|
||||||
if modified:
|
|
||||||
self.repo.scm_context.track_file(self.relpath)
|
self.repo.scm_context.track_file(self.relpath)
|
||||||
|
|
||||||
|
def dump(self, stage, **kwargs):
|
||||||
|
self.dump_stages([stage], **kwargs)
|
||||||
|
|
||||||
def remove_stage(self, stage):
|
def remove_stage(self, stage):
|
||||||
if not self.exists():
|
if not self.exists():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ DVC_STUDIO_URL = "DVC_STUDIO_URL"
|
|||||||
DVC_SQLALCHEMY_ECHO = "DVC_SQLALCHEMY_ECHO"
|
DVC_SQLALCHEMY_ECHO = "DVC_SQLALCHEMY_ECHO"
|
||||||
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
|
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
|
||||||
DVC_UPDATER_ENDPOINT = "DVC_UPDATER_ENDPOINT"
|
DVC_UPDATER_ENDPOINT = "DVC_UPDATER_ENDPOINT"
|
||||||
|
DVC_STAGE = "DVC_STAGE"
|
||||||
|
|||||||
+9
-9
@@ -1,4 +1,5 @@
|
|||||||
"""Exceptions raised by the dvc."""
|
"""Exceptions raised by the dvc."""
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ if TYPE_CHECKING:
|
|||||||
from dvc.stage import Stage
|
from dvc.stage import Stage
|
||||||
|
|
||||||
|
|
||||||
class DvcException(Exception):
|
class DvcException(Exception): # noqa: N818
|
||||||
"""Base class for all dvc exceptions."""
|
"""Base class for all dvc exceptions."""
|
||||||
|
|
||||||
def __init__(self, msg, *args):
|
def __init__(self, msg, *args):
|
||||||
@@ -132,11 +133,11 @@ class MoveNotDataSourceError(DvcException):
|
|||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
msg = (
|
msg = (
|
||||||
"move is not permitted for stages that are not data sources. "
|
"move is not permitted for stages that are not data sources. "
|
||||||
"You need to either move '{path}' to a new location and edit "
|
f"You need to either move {path!r} to a new location and edit "
|
||||||
"it by hand, or remove '{path}' and create a new one at the "
|
f"it by hand, or remove {path!r} and create a new one at the "
|
||||||
"desired location."
|
"desired location."
|
||||||
)
|
)
|
||||||
super().__init__(msg.format(path=path))
|
super().__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
class NotDvcRepoError(DvcException):
|
class NotDvcRepoError(DvcException):
|
||||||
@@ -241,11 +242,11 @@ class UploadError(FileTransferError):
|
|||||||
|
|
||||||
|
|
||||||
class CheckoutError(DvcException):
|
class CheckoutError(DvcException):
|
||||||
def __init__(self, target_infos: list[str], stats: dict[str, list[str]]):
|
def __init__(self, target_infos: list[str], result: dict):
|
||||||
from dvc.utils import error_link
|
from dvc.utils import error_link
|
||||||
|
|
||||||
self.target_infos = target_infos
|
self.target_infos = target_infos
|
||||||
self.stats = stats
|
self.result = result
|
||||||
targets = [str(t) for t in target_infos]
|
targets = [str(t) for t in target_infos]
|
||||||
m = (
|
m = (
|
||||||
"Checkout failed for following targets:\n{}\nIs your "
|
"Checkout failed for following targets:\n{}\nIs your "
|
||||||
@@ -270,9 +271,8 @@ class NoOutputInExternalRepoError(DvcException):
|
|||||||
from dvc.utils import relpath
|
from dvc.utils import relpath
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
"Output '{}' not found in target repository '{}'".format(
|
f"Output {relpath(path, external_repo_path)!r} "
|
||||||
relpath(path, external_repo_path), external_repo_url
|
f"not found in target repository '{external_repo_url}'"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-9
@@ -1,10 +1,11 @@
|
|||||||
|
import glob
|
||||||
|
from itertools import repeat
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
|
||||||
|
|
||||||
from dvc.config import ConfigError as RepoConfigError
|
from dvc.config import ConfigError as RepoConfigError
|
||||||
from dvc.config_schema import SCHEMA, Invalid
|
from dvc.config_schema import SCHEMA, Invalid
|
||||||
|
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
from dvc_objects.fs import ( # noqa: F401
|
from dvc_objects.fs import ( # noqa: F401
|
||||||
@@ -19,7 +20,7 @@ from dvc_objects.fs import ( # noqa: F401
|
|||||||
system,
|
system,
|
||||||
utils,
|
utils,
|
||||||
)
|
)
|
||||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401, TCH001
|
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401, TC001
|
||||||
from dvc_objects.fs.errors import ( # noqa: F401
|
from dvc_objects.fs.errors import ( # noqa: F401
|
||||||
AuthError,
|
AuthError,
|
||||||
ConfigError,
|
ConfigError,
|
||||||
@@ -47,12 +48,24 @@ known_implementations.update(
|
|||||||
|
|
||||||
def download(
|
def download(
|
||||||
fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
|
fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
|
||||||
) -> int:
|
) -> list[tuple[str, str, Optional[dict]]]:
|
||||||
from dvc.scm import lfs_prefetch
|
from dvc.scm import lfs_prefetch
|
||||||
|
|
||||||
from .callbacks import TqdmCallback
|
from .callbacks import TqdmCallback
|
||||||
|
|
||||||
with TqdmCallback(desc=f"Downloading {fs.name(fs_path)}", unit="files") as cb:
|
with TqdmCallback(desc=f"Downloading {fs.name(fs_path)}", unit="files") as cb:
|
||||||
|
if isinstance(fs, DVCFileSystem):
|
||||||
|
lfs_prefetch(
|
||||||
|
fs,
|
||||||
|
[
|
||||||
|
f"{fs.normpath(glob.escape(fs_path))}/**"
|
||||||
|
if fs.isdir(fs_path)
|
||||||
|
else glob.escape(fs_path)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if not glob.has_magic(fs_path):
|
||||||
|
return fs._get(fs_path, to, batch_size=jobs, callback=cb)
|
||||||
|
|
||||||
# NOTE: We use dvc-objects generic.copy over fs.get since it makes file
|
# NOTE: We use dvc-objects generic.copy over fs.get since it makes file
|
||||||
# download atomic and avoids fsspec glob/regex path expansion.
|
# download atomic and avoids fsspec glob/regex path expansion.
|
||||||
if fs.isdir(fs_path):
|
if fs.isdir(fs_path):
|
||||||
@@ -61,7 +74,7 @@ def download(
|
|||||||
]
|
]
|
||||||
if not from_infos:
|
if not from_infos:
|
||||||
localfs.makedirs(to, exist_ok=True)
|
localfs.makedirs(to, exist_ok=True)
|
||||||
return 0
|
return []
|
||||||
to_infos = [
|
to_infos = [
|
||||||
localfs.join(to, *fs.relparts(info, fs_path)) for info in from_infos
|
localfs.join(to, *fs.relparts(info, fs_path)) for info in from_infos
|
||||||
]
|
]
|
||||||
@@ -69,12 +82,10 @@ def download(
|
|||||||
from_infos = [fs_path]
|
from_infos = [fs_path]
|
||||||
to_infos = [to]
|
to_infos = [to]
|
||||||
|
|
||||||
if isinstance(fs, DVCFileSystem):
|
|
||||||
lfs_prefetch(fs, from_infos)
|
|
||||||
cb.set_size(len(from_infos))
|
cb.set_size(len(from_infos))
|
||||||
jobs = jobs or fs.jobs
|
jobs = jobs or fs.jobs
|
||||||
generic.copy(fs, from_infos, localfs, to_infos, callback=cb, batch_size=jobs)
|
generic.copy(fs, from_infos, localfs, to_infos, callback=cb, batch_size=jobs)
|
||||||
return len(to_infos)
|
return list(zip(from_infos, to_infos, repeat(None)))
|
||||||
|
|
||||||
|
|
||||||
def parse_external_url(url, fs_config=None, config=None):
|
def parse_external_url(url, fs_config=None, config=None):
|
||||||
@@ -158,5 +169,5 @@ def get_cloud_fs(repo_config, **kwargs):
|
|||||||
fs_path = cls._strip_protocol(url)
|
fs_path = cls._strip_protocol(url)
|
||||||
|
|
||||||
extras = cls._get_kwargs_from_urls(url)
|
extras = cls._get_kwargs_from_urls(url)
|
||||||
conf = {**extras, **remote_conf} # remote config takes priority
|
conf = extras | remote_conf # remote config takes priority
|
||||||
return cls, conf, fs_path
|
return cls, conf, fs_path
|
||||||
|
|||||||
+18
-57
@@ -1,67 +1,38 @@
|
|||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from typing import TYPE_CHECKING, Any, BinaryIO, Optional, Union
|
from typing import TYPE_CHECKING, BinaryIO, Optional, Union
|
||||||
|
|
||||||
|
from fsspec.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
|
||||||
|
from fsspec.callbacks import TqdmCallback as _TqdmCallback
|
||||||
|
|
||||||
from dvc.progress import Tqdm
|
from dvc.progress import Tqdm
|
||||||
from dvc.utils.objects import cached_property
|
from dvc.utils.objects import cached_property
|
||||||
from dvc_objects.fs.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from rich.progress import TaskID
|
from rich.progress import TaskID
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
from dvc.ui._rich_progress import RichTransferProgress
|
from dvc.ui._rich_progress import RichTransferProgress
|
||||||
|
|
||||||
|
|
||||||
# NOTE: this is very similar to dvc-objects.fs.callbacks.TqdmCallback,
|
class TqdmCallback(_TqdmCallback):
|
||||||
# but it works with our own Tqdm instance.
|
|
||||||
class TqdmCallback(Callback):
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
size: Optional[int] = None,
|
size: Optional[int] = None,
|
||||||
value: int = 0,
|
value: int = 0,
|
||||||
progress_bar: Optional["Tqdm"] = None,
|
progress_bar: Optional["tqdm"] = None,
|
||||||
|
tqdm_cls: Optional[type["tqdm"]] = None,
|
||||||
**tqdm_kwargs,
|
**tqdm_kwargs,
|
||||||
):
|
):
|
||||||
tqdm_kwargs["total"] = size or -1
|
tqdm_kwargs.pop("total", None)
|
||||||
self._tqdm_kwargs = tqdm_kwargs
|
super().__init__(
|
||||||
self._progress_bar = progress_bar
|
tqdm_kwargs=tqdm_kwargs, tqdm_cls=tqdm_cls or Tqdm, size=size, value=value
|
||||||
self._stack = ExitStack()
|
|
||||||
super().__init__(size=size, value=value)
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def progress_bar(self):
|
|
||||||
progress_bar = (
|
|
||||||
self._progress_bar
|
|
||||||
if self._progress_bar is not None
|
|
||||||
else Tqdm(**self._tqdm_kwargs)
|
|
||||||
)
|
)
|
||||||
return self._stack.enter_context(progress_bar)
|
if progress_bar is not None:
|
||||||
|
self.tqdm = progress_bar
|
||||||
|
|
||||||
def __enter__(self):
|
def branched(self, path_1: "Union[str, BinaryIO]", path_2: str, **kwargs):
|
||||||
return self
|
desc = path_1 if isinstance(path_1, str) else path_2
|
||||||
|
return TqdmCallback(bytes=True, desc=desc)
|
||||||
def close(self):
|
|
||||||
self._stack.close()
|
|
||||||
|
|
||||||
def set_size(self, size):
|
|
||||||
# Tqdm tries to be smart when to refresh,
|
|
||||||
# so we try to force it to re-render.
|
|
||||||
super().set_size(size)
|
|
||||||
self.progress_bar.refresh()
|
|
||||||
|
|
||||||
def call(self, hook_name=None, **kwargs): # noqa: ARG002
|
|
||||||
self.progress_bar.update_to(self.value, total=self.size)
|
|
||||||
|
|
||||||
def branch(
|
|
||||||
self,
|
|
||||||
path_1: "Union[str, BinaryIO]",
|
|
||||||
path_2: str,
|
|
||||||
kwargs: dict[str, Any],
|
|
||||||
child: Optional[Callback] = None,
|
|
||||||
):
|
|
||||||
child = child or TqdmCallback(
|
|
||||||
bytes=True, desc=path_1 if isinstance(path_1, str) else path_2
|
|
||||||
)
|
|
||||||
return super().branch(path_1, path_2, kwargs, child=child)
|
|
||||||
|
|
||||||
|
|
||||||
class RichCallback(Callback):
|
class RichCallback(Callback):
|
||||||
@@ -110,9 +81,6 @@ class RichCallback(Callback):
|
|||||||
def task(self) -> "TaskID":
|
def task(self) -> "TaskID":
|
||||||
return self.progress.add_task(**self._task_kwargs) # type: ignore[arg-type]
|
return self.progress.add_task(**self._task_kwargs) # type: ignore[arg-type]
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self._transient:
|
if self._transient:
|
||||||
self.progress.clear_task(self.task)
|
self.progress.clear_task(self.task)
|
||||||
@@ -126,17 +94,10 @@ class RichCallback(Callback):
|
|||||||
visible=not self.disable,
|
visible=not self.disable,
|
||||||
)
|
)
|
||||||
|
|
||||||
def branch(
|
def branched(self, path_1: Union[str, BinaryIO], path_2: str, **kwargs):
|
||||||
self,
|
return RichCallback(
|
||||||
path_1: Union[str, BinaryIO],
|
|
||||||
path_2: str,
|
|
||||||
kwargs: dict[str, Any],
|
|
||||||
child: Optional["Callback"] = None,
|
|
||||||
):
|
|
||||||
child = child or RichCallback(
|
|
||||||
progress=self.progress,
|
progress=self.progress,
|
||||||
desc=path_1 if isinstance(path_1, str) else path_2,
|
desc=path_1 if isinstance(path_1, str) else path_2,
|
||||||
bytes=True,
|
bytes=True,
|
||||||
transient=self._transient,
|
transient=self._transient,
|
||||||
)
|
)
|
||||||
return super().branch(path_1, path_2, kwargs, child=child)
|
|
||||||
|
|||||||
+213
-30
@@ -4,22 +4,28 @@ import ntpath
|
|||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
import threading
|
import threading
|
||||||
from collections import deque
|
from collections import defaultdict, deque
|
||||||
from contextlib import ExitStack, suppress
|
from contextlib import ExitStack, nullcontext, suppress
|
||||||
|
from glob import has_magic
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
||||||
|
|
||||||
from fsspec.spec import AbstractFileSystem
|
from fsspec.spec import DEFAULT_CALLBACK, AbstractFileSystem
|
||||||
from funcy import wrap_with
|
from funcy import wrap_with
|
||||||
|
|
||||||
from dvc.log import logger
|
from dvc.log import logger
|
||||||
from dvc_objects.fs.base import FileSystem
|
from dvc.utils.threadpool import ThreadPoolExecutor
|
||||||
|
from dvc_objects.fs.base import AnyFSPath, FileSystem
|
||||||
|
|
||||||
from .data import DataFileSystem
|
from .data import DataFileSystem
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from contextlib import AbstractContextManager
|
||||||
|
|
||||||
from dvc.repo import Repo
|
from dvc.repo import Repo
|
||||||
from dvc.types import DictStrAny, StrPath
|
from dvc.types import DictStrAny, StrPath
|
||||||
|
|
||||||
|
from .callbacks import Callback
|
||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
RepoFactory = Union[Callable[..., "Repo"], type["Repo"]]
|
RepoFactory = Union[Callable[..., "Repo"], type["Repo"]]
|
||||||
@@ -77,11 +83,10 @@ class _DVCFileSystem(AbstractFileSystem):
|
|||||||
cachable = False
|
cachable = False
|
||||||
root_marker = "/"
|
root_marker = "/"
|
||||||
|
|
||||||
def __init__( # noqa: PLR0913
|
def __init__(
|
||||||
self,
|
self,
|
||||||
url: Optional[str] = None,
|
repo: Union["Repo", os.PathLike[str], str, None] = None,
|
||||||
rev: Optional[str] = None,
|
rev: Optional[str] = None,
|
||||||
repo: Optional["Repo"] = None,
|
|
||||||
subrepos: bool = False,
|
subrepos: bool = False,
|
||||||
repo_factory: Optional[RepoFactory] = None,
|
repo_factory: Optional[RepoFactory] = None,
|
||||||
fo: Optional[str] = None,
|
fo: Optional[str] = None,
|
||||||
@@ -95,7 +100,8 @@ class _DVCFileSystem(AbstractFileSystem):
|
|||||||
"""DVC + git-tracked files fs.
|
"""DVC + git-tracked files fs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path (str, optional): URL or path to a DVC/Git repository.
|
repo (str | os.PathLike[str] | Repo, optional): A url or a path to a DVC/Git
|
||||||
|
repository, or a `Repo` instance.
|
||||||
Defaults to a DVC repository in the current working directory.
|
Defaults to a DVC repository in the current working directory.
|
||||||
Both HTTP and SSH protocols are supported for remote Git repos
|
Both HTTP and SSH protocols are supported for remote Git repos
|
||||||
(e.g. [user@]server:project.git).
|
(e.g. [user@]server:project.git).
|
||||||
@@ -105,7 +111,6 @@ class _DVCFileSystem(AbstractFileSystem):
|
|||||||
In case of a local repository, if rev is unspecified, it will
|
In case of a local repository, if rev is unspecified, it will
|
||||||
default to the working directory.
|
default to the working directory.
|
||||||
If the repo is not a Git repo, this option is ignored.
|
If the repo is not a Git repo, this option is ignored.
|
||||||
repo (:obj:`Repo`, optional): `Repo` instance.
|
|
||||||
subrepos (bool): traverse to subrepos.
|
subrepos (bool): traverse to subrepos.
|
||||||
By default, it ignores subrepos.
|
By default, it ignores subrepos.
|
||||||
repo_factory (callable): A function to initialize subrepo with.
|
repo_factory (callable): A function to initialize subrepo with.
|
||||||
@@ -130,13 +135,23 @@ class _DVCFileSystem(AbstractFileSystem):
|
|||||||
... rev="main",
|
... rev="main",
|
||||||
... )
|
... )
|
||||||
"""
|
"""
|
||||||
|
from dvc.repo import Repo
|
||||||
|
|
||||||
|
# kwargs.get("url") is for maintaining backward compatibility
|
||||||
|
repo = repo or fo or kwargs.get("url")
|
||||||
|
if isinstance(repo, Repo):
|
||||||
|
self._repo: Optional[Repo] = repo
|
||||||
|
url = None
|
||||||
|
else:
|
||||||
|
self._repo = None
|
||||||
|
url = os.fspath(repo) if repo else None
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._repo = repo
|
|
||||||
self._repo_factory = repo_factory
|
self._repo_factory = repo_factory
|
||||||
self._traverse_subrepos = subrepos
|
self._traverse_subrepos = subrepos
|
||||||
self._repo_stack = ExitStack()
|
self._repo_stack = ExitStack()
|
||||||
self._repo_kwargs = {
|
self._repo_kwargs = {
|
||||||
"url": url if url is not None else fo,
|
"url": url,
|
||||||
"rev": rev,
|
"rev": rev,
|
||||||
"subrepos": subrepos,
|
"subrepos": subrepos,
|
||||||
"config": config,
|
"config": config,
|
||||||
@@ -360,38 +375,47 @@ class _DVCFileSystem(AbstractFileSystem):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def ls(self, path, detail=True, dvc_only=False, **kwargs):
|
def ls(self, path, detail=True, dvc_only=False, **kwargs): # noqa: C901, PLR0912
|
||||||
key = self._get_key_from_relative(path)
|
key = self._get_key_from_relative(path)
|
||||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||||
|
|
||||||
dvc_exists = False
|
|
||||||
dvc_infos = {}
|
dvc_infos = {}
|
||||||
|
dvc_info = {}
|
||||||
if dvc_fs:
|
if dvc_fs:
|
||||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||||
with suppress(FileNotFoundError, NotADirectoryError):
|
with suppress(FileNotFoundError):
|
||||||
for info in dvc_fs.ls(dvc_path, detail=True):
|
dvc_info = dvc_fs.info(dvc_path)
|
||||||
dvc_infos[dvc_fs.name(info["name"])] = info
|
if dvc_info["type"] == "file":
|
||||||
dvc_exists = True
|
dvc_infos[""] = dvc_info
|
||||||
|
else:
|
||||||
|
for info in dvc_fs.ls(dvc_path, detail=True):
|
||||||
|
dvc_infos[dvc_fs.name(info["name"])] = info
|
||||||
|
|
||||||
fs_exists = False
|
|
||||||
fs_infos = {}
|
fs_infos = {}
|
||||||
|
fs_info = {}
|
||||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||||
if not dvc_only:
|
if not dvc_only:
|
||||||
fs = self.repo.fs
|
fs = self.repo.fs
|
||||||
fs_path = self._from_key(key)
|
fs_path = self._from_key(key)
|
||||||
try:
|
try:
|
||||||
for info in repo.dvcignore.ls(
|
fs_info = fs.info(fs_path)
|
||||||
fs, fs_path, detail=True, ignore_subrepos=ignore_subrepos
|
if fs_info["type"] == "file":
|
||||||
):
|
fs_infos[""] = fs_info
|
||||||
fs_infos[fs.name(info["name"])] = info
|
else:
|
||||||
fs_exists = True
|
for info in repo.dvcignore.ls(
|
||||||
|
fs, fs_path, detail=True, ignore_subrepos=ignore_subrepos
|
||||||
|
):
|
||||||
|
fs_infos[fs.name(info["name"])] = info
|
||||||
except (FileNotFoundError, NotADirectoryError):
|
except (FileNotFoundError, NotADirectoryError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not (dvc_exists or fs_exists):
|
if not (fs_info or dvc_info):
|
||||||
# broken symlink or TreeError
|
# broken symlink or TreeError
|
||||||
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
||||||
|
|
||||||
|
if fs_info and dvc_info and dvc_info["type"] != fs_info["type"]:
|
||||||
|
dvc_infos.clear() # invalidate dvc_info if file type differs
|
||||||
|
|
||||||
dvcfiles = kwargs.get("dvcfiles", False)
|
dvcfiles = kwargs.get("dvcfiles", False)
|
||||||
|
|
||||||
infos = []
|
infos = []
|
||||||
@@ -402,7 +426,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
|||||||
if not dvcfiles and _is_dvc_file(name):
|
if not dvcfiles and _is_dvc_file(name):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entry_path = self.join(path, name)
|
entry_path = self.join(path, name) if name else path
|
||||||
info = _merge_info(
|
info = _merge_info(
|
||||||
repo, (*subkey, name), fs_infos.get(name), dvc_infos.get(name)
|
repo, (*subkey, name), fs_infos.get(name), dvc_infos.get(name)
|
||||||
)
|
)
|
||||||
@@ -420,7 +444,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
|||||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||||
|
|
||||||
def _info( # noqa: C901, PLR0912
|
def _info( # noqa: C901
|
||||||
self, key, path, ignore_subrepos=True, check_ignored=True
|
self, key, path, ignore_subrepos=True, check_ignored=True
|
||||||
):
|
):
|
||||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||||
@@ -459,15 +483,134 @@ class _DVCFileSystem(AbstractFileSystem):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not dvc_info and not fs_info:
|
if not dvc_info and not fs_info:
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
||||||
|
|
||||||
info = _merge_info(repo, subkey, fs_info, dvc_info)
|
info = _merge_info(repo, subkey, fs_info, dvc_info)
|
||||||
info["name"] = path
|
info["name"] = path
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
rpath,
|
||||||
|
lpath,
|
||||||
|
recursive=False,
|
||||||
|
callback=DEFAULT_CALLBACK,
|
||||||
|
maxdepth=None,
|
||||||
|
batch_size=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self._get(
|
||||||
|
rpath,
|
||||||
|
lpath,
|
||||||
|
recursive=recursive,
|
||||||
|
callback=callback,
|
||||||
|
maxdepth=maxdepth,
|
||||||
|
batch_size=batch_size,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get( # noqa: C901, PLR0912, PLR0915
|
||||||
|
self,
|
||||||
|
rpath,
|
||||||
|
lpath,
|
||||||
|
recursive=False,
|
||||||
|
callback=DEFAULT_CALLBACK,
|
||||||
|
maxdepth=None,
|
||||||
|
batch_size=None,
|
||||||
|
**kwargs,
|
||||||
|
) -> list[tuple[str, str, Optional[dict]]]:
|
||||||
|
if (
|
||||||
|
isinstance(rpath, list)
|
||||||
|
or isinstance(lpath, list)
|
||||||
|
or has_magic(rpath)
|
||||||
|
or not self.exists(rpath)
|
||||||
|
or not recursive
|
||||||
|
):
|
||||||
|
super().get(
|
||||||
|
rpath,
|
||||||
|
lpath,
|
||||||
|
recursive=recursive,
|
||||||
|
callback=callback,
|
||||||
|
maxdepth=maxdepth,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
if os.path.isdir(lpath) or lpath.endswith(os.path.sep):
|
||||||
|
lpath = self.join(lpath, os.path.basename(rpath))
|
||||||
|
|
||||||
|
if self.isfile(rpath):
|
||||||
|
with callback.branched(rpath, lpath) as child:
|
||||||
|
self.get_file(rpath, lpath, callback=child, **kwargs)
|
||||||
|
return [(rpath, lpath, None)]
|
||||||
|
|
||||||
|
result: list[tuple[str, str, Optional[dict]]] = []
|
||||||
|
_dirs: list[str] = []
|
||||||
|
_files: dict[FileSystem, list[tuple[str, str, Optional[dict]]]]
|
||||||
|
_files = defaultdict(list)
|
||||||
|
|
||||||
|
for root, dirs, files in self.walk(rpath, maxdepth=maxdepth, detail=True):
|
||||||
|
if files:
|
||||||
|
callback.set_size((callback.size or 0) + len(files))
|
||||||
|
|
||||||
|
parts = self.relparts(root, rpath)
|
||||||
|
if parts in ((os.curdir,), ("",)):
|
||||||
|
parts = ()
|
||||||
|
dest_root = os.path.join(lpath, *parts)
|
||||||
|
if not maxdepth or len(parts) < maxdepth - 1:
|
||||||
|
_dirs.extend(f"{dest_root}{os.path.sep}{d}" for d in dirs)
|
||||||
|
|
||||||
|
key = self._get_key_from_relative(root)
|
||||||
|
_, dvc_fs, _ = self._get_subrepo_info(key)
|
||||||
|
|
||||||
|
for name, info in files.items():
|
||||||
|
dvc_info = info.get("dvc_info")
|
||||||
|
fs_info = info.get("fs_info")
|
||||||
|
if dvc_fs and dvc_info and not fs_info:
|
||||||
|
fs = dvc_fs
|
||||||
|
fs_path = dvc_info["name"]
|
||||||
|
else:
|
||||||
|
fs = self.repo.fs
|
||||||
|
fs_path = fs_info["name"]
|
||||||
|
|
||||||
|
src_path = f"{root}{self.sep}{name}"
|
||||||
|
dest_path = f"{dest_root}{os.path.sep}{name}"
|
||||||
|
_files[fs].append((fs_path, dest_path, dvc_info))
|
||||||
|
result.append((src_path, dest_path, info))
|
||||||
|
|
||||||
|
os.makedirs(lpath, exist_ok=True)
|
||||||
|
for d in _dirs:
|
||||||
|
os.makedirs(d, exist_ok=True)
|
||||||
|
|
||||||
|
def get_file(arg: tuple[FileSystem, tuple[str, str, Optional[dict]]]):
|
||||||
|
fs, (src, dest, info) = arg
|
||||||
|
kw = kwargs
|
||||||
|
if isinstance(fs, DataFileSystem):
|
||||||
|
kw = kw | {"info": info}
|
||||||
|
with callback.branched(src, dest) as child:
|
||||||
|
fs.get_file(src, dest, callback=child, **kw)
|
||||||
|
|
||||||
|
if batch_size == 1:
|
||||||
|
ctx: AbstractContextManager = nullcontext()
|
||||||
|
map_fn: Callable = map
|
||||||
|
else:
|
||||||
|
ctx = ThreadPoolExecutor(max_workers=batch_size)
|
||||||
|
map_fn = ctx.imap_unordered
|
||||||
|
|
||||||
|
with ctx:
|
||||||
|
it = ((fs, f) for fs, files in _files.items() for f in files)
|
||||||
|
deque(callback.wrap(map_fn(get_file, it)), maxlen=0)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_file(self, rpath, lpath, **kwargs):
|
def get_file(self, rpath, lpath, **kwargs):
|
||||||
|
dvc_info = kwargs.pop("info", {}).pop("dvc_info", None)
|
||||||
key = self._get_key_from_relative(rpath)
|
key = self._get_key_from_relative(rpath)
|
||||||
fs_path = self._from_key(key)
|
fs_path = self._from_key(key)
|
||||||
|
dirpath = os.path.dirname(lpath)
|
||||||
|
if dirpath:
|
||||||
|
# makedirs raises error if the string is empty
|
||||||
|
os.makedirs(dirpath, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.repo.fs.get_file(fs_path, lpath, **kwargs)
|
return self.repo.fs.get_file(fs_path, lpath, **kwargs)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@@ -476,7 +619,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||||
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
|
return dvc_fs.get_file(dvc_path, lpath, info=dvc_info, **kwargs)
|
||||||
|
|
||||||
def du(self, path, total=True, maxdepth=None, withdirs=False, **kwargs):
|
def du(self, path, total=True, maxdepth=None, withdirs=False, **kwargs):
|
||||||
if maxdepth is not None:
|
if maxdepth is not None:
|
||||||
@@ -544,6 +687,45 @@ class DVCFileSystem(FileSystem):
|
|||||||
def getcwd(self):
|
def getcwd(self):
|
||||||
return self.fs.getcwd()
|
return self.fs.getcwd()
|
||||||
|
|
||||||
|
def _get(
|
||||||
|
self,
|
||||||
|
from_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||||
|
to_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||||
|
callback: "Callback" = DEFAULT_CALLBACK,
|
||||||
|
recursive: bool = False,
|
||||||
|
batch_size: Optional[int] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> list[tuple[str, str, Optional[dict]]]:
|
||||||
|
# FileSystem.get is non-recursive by default if arguments are lists
|
||||||
|
# otherwise, it's recursive.
|
||||||
|
recursive = not (isinstance(from_info, list) and isinstance(to_info, list))
|
||||||
|
return self.fs._get(
|
||||||
|
from_info,
|
||||||
|
to_info,
|
||||||
|
callback=callback,
|
||||||
|
recursive=recursive,
|
||||||
|
batch_size=batch_size,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
from_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||||
|
to_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||||
|
callback: "Callback" = DEFAULT_CALLBACK,
|
||||||
|
recursive: bool = False,
|
||||||
|
batch_size: Optional[int] = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
self._get(
|
||||||
|
from_info,
|
||||||
|
to_info,
|
||||||
|
callback=callback,
|
||||||
|
batch_size=batch_size,
|
||||||
|
recursive=recursive,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fsid(self) -> str:
|
def fsid(self) -> str:
|
||||||
return self.fs.fsid
|
return self.fs.fsid
|
||||||
@@ -560,9 +742,10 @@ class DVCFileSystem(FileSystem):
|
|||||||
return self.fs.repo_url
|
return self.fs.repo_url
|
||||||
|
|
||||||
def from_os_path(self, path: str) -> str:
|
def from_os_path(self, path: str) -> str:
|
||||||
if os.path.isabs(path):
|
if os.path.isabs(path) or (
|
||||||
|
os.name == "nt" and posixpath.isabs(path) and ntpath.sep not in path
|
||||||
|
):
|
||||||
path = os.path.relpath(path, self.repo.root_dir)
|
path = os.path.relpath(path, self.repo.root_dir)
|
||||||
|
|
||||||
return as_posix(path)
|
return as_posix(path)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""UPath implementation for DVCFileSystem.
|
||||||
|
|
||||||
|
This provides a `pathlib.Path` like interface to
|
||||||
|
work with DVCFileSystem.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
>>> from upath import UPath
|
||||||
|
|
||||||
|
>>> local = UPath("dvc://path/to/local/repo")
|
||||||
|
>>> https = UPath("dvc+https://github.com/iterative/example-get-started", rev="main")
|
||||||
|
>>> ssh = UPath("dvc+ssh://[email protected]:iterative/example-get-started.git")
|
||||||
|
"""
|
||||||
|
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
|
from upath import UPath # ty: ignore[unresolved-import]
|
||||||
|
|
||||||
|
|
||||||
|
class DVCPath(UPath):
|
||||||
|
@classmethod
|
||||||
|
def _transform_init_args(cls, args, protocol, storage_options):
|
||||||
|
if not args:
|
||||||
|
args = ("/",)
|
||||||
|
elif (
|
||||||
|
args
|
||||||
|
and "url" not in storage_options
|
||||||
|
and protocol in {"dvc+http", "dvc+https", "dvc+ssh"}
|
||||||
|
):
|
||||||
|
url, *rest = args
|
||||||
|
url = urlsplit(str(url))
|
||||||
|
proto = protocol.split("+")[1]
|
||||||
|
if proto == "ssh":
|
||||||
|
base_url = url.netloc + url.path
|
||||||
|
else:
|
||||||
|
base_url = url._replace(scheme=proto).geturl()
|
||||||
|
storage_options["url"] = base_url
|
||||||
|
# Assume the given path is a root url
|
||||||
|
args = ("/", *rest)
|
||||||
|
return super()._transform_init_args(args, "dvc", storage_options)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
s = super().__str__()
|
||||||
|
if url := self.storage_options.get("url"):
|
||||||
|
return s.replace("dvc://", f"dvc+{url}", 1)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def with_segments(self, *pathsegments):
|
||||||
|
obj = super().with_segments(*pathsegments)
|
||||||
|
# cache filesystem, as dvcfs does not cache filesystem
|
||||||
|
# caveat: any joinpath operation will instantiate filesystem
|
||||||
|
obj._fs_cached = self.fs
|
||||||
|
return obj
|
||||||
+232
-104
@@ -1,10 +1,19 @@
|
|||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from collections import namedtuple
|
from collections.abc import Iterable, Iterator
|
||||||
from itertools import chain, groupby, takewhile
|
from itertools import chain, groupby, takewhile
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional, Union, overload
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pathspec.patterns.gitignore.spec import ( # type: ignore[import-not-found]
|
||||||
|
GitIgnoreSpecPattern,
|
||||||
|
)
|
||||||
|
except ImportError: # pathspec<1
|
||||||
|
from pathspec.patterns import (
|
||||||
|
GitWildMatchPattern as GitIgnoreSpecPattern,
|
||||||
|
)
|
||||||
|
|
||||||
from pathspec.patterns import GitWildMatchPattern
|
|
||||||
from pathspec.util import normalize_file
|
from pathspec.util import normalize_file
|
||||||
from pygtrie import Trie
|
from pygtrie import Trie
|
||||||
|
|
||||||
@@ -13,7 +22,9 @@ from dvc.log import logger
|
|||||||
from dvc.pathspec_math import PatternInfo, merge_patterns
|
from dvc.pathspec_math import PatternInfo, merge_patterns
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dvc.fs import AnyFSPath, FileSystem
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
from dvc.fs import FileSystem
|
||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
@@ -21,36 +32,86 @@ logger = logger.getChild(__name__)
|
|||||||
class DvcIgnore:
|
class DvcIgnore:
|
||||||
DVCIGNORE_FILE = ".dvcignore"
|
DVCIGNORE_FILE = ".dvcignore"
|
||||||
|
|
||||||
def __call__(self, root, dirs, files):
|
def __call__(
|
||||||
|
self, root: str, dirs: list[str], files: list[str]
|
||||||
|
) -> tuple[list[str], list[str]]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class DvcIgnorePatterns(DvcIgnore):
|
class DvcIgnorePatterns(DvcIgnore):
|
||||||
def __init__(self, pattern_list, dirname, sep):
|
def __init__(
|
||||||
from pathspec.patterns.gitwildmatch import _DIR_MARK
|
self, pattern_list: Iterable[Union[PatternInfo, str]], dirname: str, sep: str
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
from pathspec.patterns.gitignore.spec import ( # type: ignore[import-not-found]
|
||||||
|
_DIR_MARK,
|
||||||
|
)
|
||||||
|
except ImportError: # pathspec<1
|
||||||
|
from pathspec.patterns.gitwildmatch import ( # type: ignore[attr-defined, no-redef]
|
||||||
|
_DIR_MARK,
|
||||||
|
)
|
||||||
|
|
||||||
if pattern_list and isinstance(pattern_list[0], str):
|
pattern_infos = [
|
||||||
pattern_list = [PatternInfo(pattern, "") for pattern in pattern_list]
|
pattern if isinstance(pattern, PatternInfo) else PatternInfo(pattern, "")
|
||||||
|
for pattern in pattern_list
|
||||||
self.sep = sep
|
|
||||||
self.pattern_list = pattern_list
|
|
||||||
self.dirname = dirname
|
|
||||||
|
|
||||||
self.regex_pattern_list = []
|
|
||||||
for count, pattern in enumerate(pattern_list):
|
|
||||||
pattern, group = GitWildMatchPattern.pattern_to_regex(pattern.patterns)
|
|
||||||
if pattern:
|
|
||||||
pattern = pattern.replace(f"<{_DIR_MARK}>", f"<{_DIR_MARK}{count}>")
|
|
||||||
self.regex_pattern_list.append((pattern, group))
|
|
||||||
|
|
||||||
self.ignore_spec = [
|
|
||||||
(ignore, re.compile("|".join(item[0] for item in group)))
|
|
||||||
for ignore, group in groupby(self.regex_pattern_list, lambda x: x[1])
|
|
||||||
if ignore is not None
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
self.sep = sep
|
||||||
|
self.pattern_list: list[PatternInfo] = []
|
||||||
|
self.dirname = dirname
|
||||||
|
self.find_matching_pattern = functools.cache(self._find_matching_pattern)
|
||||||
|
|
||||||
|
regex_pattern_list: list[tuple[str, bool, bool, PatternInfo]] = []
|
||||||
|
for count, pattern_info in enumerate(pattern_infos):
|
||||||
|
regex, ignore = GitIgnoreSpecPattern.pattern_to_regex(pattern_info.patterns)
|
||||||
|
if regex is not None and ignore is not None:
|
||||||
|
self.pattern_list.append(pattern_info)
|
||||||
|
regex = regex.replace(f"<{_DIR_MARK}>", f"<{_DIR_MARK}{count}>")
|
||||||
|
regex_pattern_list.append(
|
||||||
|
(regex, ignore, pattern_info.patterns.endswith("/"), pattern_info)
|
||||||
|
)
|
||||||
|
|
||||||
|
def keyfunc(item: tuple[str, bool, bool, PatternInfo]) -> tuple[bool, bool]:
|
||||||
|
_, ignore, dir_only_pattern, _ = item
|
||||||
|
return ignore, dir_only_pattern
|
||||||
|
|
||||||
|
self.ignore_spec: list[
|
||||||
|
tuple[
|
||||||
|
re.Pattern[str],
|
||||||
|
bool,
|
||||||
|
bool,
|
||||||
|
dict[Optional[str], tuple[str, PatternInfo]],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
self.ignore_spec = []
|
||||||
|
for (ignore, dir_only_pattern), group in groupby(
|
||||||
|
regex_pattern_list, key=keyfunc
|
||||||
|
):
|
||||||
|
if ignore:
|
||||||
|
# For performance, we combine all exclude patterns.
|
||||||
|
# But we still need to figure out which pattern matched which rule,
|
||||||
|
# (eg: to show in `dvc check-ignore`).
|
||||||
|
# So, we use named groups and keep a map of group name to pattern.
|
||||||
|
pattern_map: dict[Optional[str], tuple[str, PatternInfo]] = {
|
||||||
|
f"rule_{i}": (regex, pi)
|
||||||
|
for i, (regex, _, _, pi) in enumerate(group)
|
||||||
|
}
|
||||||
|
combined_regex = "|".join(
|
||||||
|
f"(?P<{name}>{regex})" for name, (regex, _) in pattern_map.items()
|
||||||
|
)
|
||||||
|
self.ignore_spec.append(
|
||||||
|
(re.compile(combined_regex), ignore, dir_only_pattern, pattern_map)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# unignored patterns are not combined with `|`.
|
||||||
|
for regex, _, _, pi in group:
|
||||||
|
pattern_map = {None: (regex, pi)}
|
||||||
|
self.ignore_spec.append(
|
||||||
|
(re.compile(regex), ignore, dir_only_pattern, pattern_map)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_file(cls, path, fs, name):
|
def from_file(cls, path: str, fs: "FileSystem", name: str) -> "Self":
|
||||||
assert fs.isabs(path)
|
assert fs.isabs(path)
|
||||||
dirname = fs.normpath(fs.dirname(path))
|
dirname = fs.normpath(fs.dirname(path))
|
||||||
with fs.open(path, encoding="utf-8") as fobj:
|
with fs.open(path, encoding="utf-8") as fobj:
|
||||||
@@ -62,13 +123,15 @@ class DvcIgnorePatterns(DvcIgnore):
|
|||||||
|
|
||||||
return cls(path_spec_lines, dirname, fs.sep)
|
return cls(path_spec_lines, dirname, fs.sep)
|
||||||
|
|
||||||
def __call__(self, root: list[str], dirs: list[str], files: list[str]):
|
def __call__(
|
||||||
|
self, root: str, dirs: list[str], files: list[str]
|
||||||
|
) -> tuple[list[str], list[str]]:
|
||||||
files = [f for f in files if not self.matches(root, f)]
|
files = [f for f in files if not self.matches(root, f)]
|
||||||
dirs = [d for d in dirs if not self.matches(root, d, True)]
|
dirs = [d for d in dirs if not self.matches(root, d, True)]
|
||||||
|
|
||||||
return dirs, files
|
return dirs, files
|
||||||
|
|
||||||
def _get_normalize_path(self, dirname, basename):
|
def _get_normalize_path(self, dirname: str, basename: str) -> Optional[str]:
|
||||||
# NOTE: `relpath` is too slow, so we have to assume that both
|
# NOTE: `relpath` is too slow, so we have to assume that both
|
||||||
# `dirname` and `self.dirname` are relative or absolute together.
|
# `dirname` and `self.dirname` are relative or absolute together.
|
||||||
|
|
||||||
@@ -81,62 +144,105 @@ class DvcIgnorePatterns(DvcIgnore):
|
|||||||
# NOTE: `os.path.join` is ~x5.5 slower
|
# NOTE: `os.path.join` is ~x5.5 slower
|
||||||
path = f"{rel}{self.sep}{basename}"
|
path = f"{rel}{self.sep}{basename}"
|
||||||
else:
|
else:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
return normalize_file(path)
|
return normalize_file(path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def matches(self, dirname, basename, is_dir=False, details: bool = False):
|
@overload
|
||||||
|
def matches(
|
||||||
|
self,
|
||||||
|
dirname: str,
|
||||||
|
basename: str,
|
||||||
|
is_dir: bool = False,
|
||||||
|
details: Literal[False] = ...,
|
||||||
|
) -> bool: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def matches(
|
||||||
|
self,
|
||||||
|
dirname: str,
|
||||||
|
basename: str,
|
||||||
|
is_dir: bool = False,
|
||||||
|
details: Literal[True] = ...,
|
||||||
|
) -> tuple[bool, list[PatternInfo]]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def matches(
|
||||||
|
self,
|
||||||
|
dirname: str,
|
||||||
|
basename: str,
|
||||||
|
is_dir: bool = False,
|
||||||
|
details: bool = False,
|
||||||
|
) -> Union[bool, tuple[bool, list[PatternInfo]]]: ...
|
||||||
|
|
||||||
|
def matches(
|
||||||
|
self,
|
||||||
|
dirname: str,
|
||||||
|
basename: str,
|
||||||
|
is_dir: bool = False,
|
||||||
|
details: bool = False,
|
||||||
|
) -> Union[bool, tuple[bool, list[PatternInfo]]]:
|
||||||
path = self._get_normalize_path(dirname, basename)
|
path = self._get_normalize_path(dirname, basename)
|
||||||
if not path:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if details:
|
|
||||||
return self._ignore_details(path, is_dir)
|
|
||||||
return self.ignore(path, is_dir)
|
|
||||||
|
|
||||||
def ignore(self, path, is_dir):
|
|
||||||
def matches(pattern, path, is_dir) -> bool:
|
|
||||||
matches_ = bool(pattern.match(path))
|
|
||||||
|
|
||||||
if is_dir:
|
|
||||||
matches_ |= bool(pattern.match(f"{path}/"))
|
|
||||||
|
|
||||||
return matches_
|
|
||||||
|
|
||||||
result = False
|
result = False
|
||||||
|
_match: list[PatternInfo] = []
|
||||||
|
if path:
|
||||||
|
result, _match = self._ignore(path, is_dir)
|
||||||
|
return (result, _match) if details else result
|
||||||
|
|
||||||
for ignore, pattern in self.ignore_spec[::-1]:
|
def _find_matching_pattern(
|
||||||
if matches(pattern, path, is_dir):
|
self, path: str, is_dir: bool
|
||||||
result = ignore
|
) -> tuple[bool, list[PatternInfo]]:
|
||||||
break
|
paths = [path]
|
||||||
return result
|
if is_dir and not path.endswith("/"):
|
||||||
|
paths.append(f"{path}/")
|
||||||
|
|
||||||
def _ignore_details(self, path, is_dir: bool):
|
for pattern, ignore, dir_only_pattern, pattern_map in reversed(
|
||||||
result = []
|
self.ignore_spec
|
||||||
for (regex, _), pattern_info in list(
|
|
||||||
zip(self.regex_pattern_list, self.pattern_list)
|
|
||||||
):
|
):
|
||||||
# skip system pattern
|
if dir_only_pattern and not is_dir:
|
||||||
if not pattern_info.file_info:
|
|
||||||
continue
|
continue
|
||||||
|
for p in paths:
|
||||||
|
match = pattern.match(p)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
if ignore:
|
||||||
|
group_name, _match = next(
|
||||||
|
(
|
||||||
|
(name, _match)
|
||||||
|
for name, _match in match.groupdict().items()
|
||||||
|
if name.startswith("rule_") and _match is not None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# unignored patterns are not combined with `|`,
|
||||||
|
# so there are no groups.
|
||||||
|
group_name = None
|
||||||
|
_regex, pattern_info = pattern_map[group_name]
|
||||||
|
return ignore, [pattern_info]
|
||||||
|
return False, []
|
||||||
|
|
||||||
regex = re.compile(regex)
|
def _ignore(self, path: str, is_dir: bool) -> tuple[bool, list[PatternInfo]]:
|
||||||
|
parts = path.split("/")
|
||||||
|
result = False
|
||||||
|
matches: list[PatternInfo] = []
|
||||||
|
for i in range(1, len(parts) + 1):
|
||||||
|
rel_path = "/".join(parts[:i])
|
||||||
|
result, _matches = self.find_matching_pattern(
|
||||||
|
rel_path, is_dir or i < len(parts)
|
||||||
|
)
|
||||||
|
if i < len(parts) and not result:
|
||||||
|
continue
|
||||||
|
matches.extend(_matches)
|
||||||
|
if result:
|
||||||
|
break
|
||||||
|
return result, matches
|
||||||
|
|
||||||
matches = bool(regex.match(path))
|
def __hash__(self) -> int:
|
||||||
if is_dir:
|
|
||||||
matches |= bool(regex.match(f"{path}/"))
|
|
||||||
|
|
||||||
if matches:
|
|
||||||
result.append(pattern_info.file_info)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.dirname + ":" + str(self.pattern_list))
|
return hash(self.dirname + ":" + str(self.pattern_list))
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other: object) -> bool:
|
||||||
if not isinstance(other, DvcIgnorePatterns):
|
if not isinstance(other, DvcIgnorePatterns):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
return (self.dirname == other.dirname) & (
|
return (self.dirname == other.dirname) & (
|
||||||
@@ -144,21 +250,18 @@ class DvcIgnorePatterns(DvcIgnore):
|
|||||||
== [pattern.patterns for pattern in other.pattern_list]
|
== [pattern.patterns for pattern in other.pattern_list]
|
||||||
)
|
)
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self) -> bool:
|
||||||
return bool(self.pattern_list)
|
return bool(self.pattern_list)
|
||||||
|
|
||||||
|
|
||||||
CheckIgnoreResult = namedtuple( # noqa: PYI024
|
class CheckIgnoreResult(NamedTuple):
|
||||||
"CheckIgnoreResult", ["file", "match", "patterns"]
|
file: str
|
||||||
)
|
match: bool
|
||||||
|
pattern_infos: list[PatternInfo]
|
||||||
|
|
||||||
def _no_match(path):
|
|
||||||
return CheckIgnoreResult(path, False, ["::"])
|
|
||||||
|
|
||||||
|
|
||||||
class DvcIgnoreFilter:
|
class DvcIgnoreFilter:
|
||||||
def __init__(self, fs, root_dir):
|
def __init__(self, fs: "FileSystem", root_dir: str) -> None:
|
||||||
from dvc.repo import Repo
|
from dvc.repo import Repo
|
||||||
|
|
||||||
default_ignore_patterns = [
|
default_ignore_patterns = [
|
||||||
@@ -193,7 +296,7 @@ class DvcIgnoreFilter:
|
|||||||
ignore_subrepos=True,
|
ignore_subrepos=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_key(self, path):
|
def _get_key(self, path: str) -> tuple[str, ...]:
|
||||||
parts = self.fs.relparts(path, self.root_dir)
|
parts = self.fs.relparts(path, self.root_dir)
|
||||||
if parts == (os.curdir,):
|
if parts == (os.curdir,):
|
||||||
return ()
|
return ()
|
||||||
@@ -241,7 +344,7 @@ class DvcIgnoreFilter:
|
|||||||
for dname in dnames:
|
for dname in dnames:
|
||||||
self._update_sub_repo(self.fs.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):
|
def _update_sub_repo(self, path: str, ignore_trie: Trie) -> None:
|
||||||
from dvc.repo import Repo
|
from dvc.repo import Repo
|
||||||
|
|
||||||
if path == self.root_dir:
|
if path == self.root_dir:
|
||||||
@@ -268,7 +371,9 @@ class DvcIgnoreFilter:
|
|||||||
else:
|
else:
|
||||||
ignore_trie[key] = new_pattern
|
ignore_trie[key] = new_pattern
|
||||||
|
|
||||||
def __call__(self, root, dirs, files, ignore_subrepos=True):
|
def __call__(
|
||||||
|
self, root: str, dirs: list[str], files: list[str], ignore_subrepos: bool = True
|
||||||
|
) -> tuple[list[str], list[str]]:
|
||||||
abs_root = self.fs.abspath(root)
|
abs_root = self.fs.abspath(root)
|
||||||
ignore_pattern = self._get_trie_pattern(
|
ignore_pattern = self._get_trie_pattern(
|
||||||
abs_root, dnames=dirs, ignore_subrepos=ignore_subrepos
|
abs_root, dnames=dirs, ignore_subrepos=ignore_subrepos
|
||||||
@@ -277,7 +382,24 @@ class DvcIgnoreFilter:
|
|||||||
dirs, files = ignore_pattern(abs_root, dirs, files)
|
dirs, files = ignore_pattern(abs_root, dirs, files)
|
||||||
return dirs, files
|
return dirs, files
|
||||||
|
|
||||||
def ls(self, fs, path, detail=True, **kwargs):
|
@overload
|
||||||
|
def ls(
|
||||||
|
self, fs: "FileSystem", path: str, detail: Literal[True], **kwargs: Any
|
||||||
|
) -> list[dict[str, Any]]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def ls(
|
||||||
|
self, fs: "FileSystem", path: str, detail: Literal[False], **kwargs
|
||||||
|
) -> list[str]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def ls(
|
||||||
|
self, fs: "FileSystem", path: str, detail: bool = True, **kwargs
|
||||||
|
) -> Union[list[str], list[dict[str, Any]]]: ...
|
||||||
|
|
||||||
|
def ls(
|
||||||
|
self, fs: "FileSystem", path: str, detail: bool = True, **kwargs: Any
|
||||||
|
) -> Union[list[str], list[dict[str, Any]]]:
|
||||||
fs_dict = {}
|
fs_dict = {}
|
||||||
dirs = []
|
dirs = []
|
||||||
nondirs = []
|
nondirs = []
|
||||||
@@ -297,22 +419,29 @@ class DvcIgnoreFilter:
|
|||||||
|
|
||||||
return [fs_dict[name] for name in chain(dirs, nondirs)]
|
return [fs_dict[name] for name in chain(dirs, nondirs)]
|
||||||
|
|
||||||
def walk(self, fs: "FileSystem", path: "AnyFSPath", **kwargs):
|
def walk(
|
||||||
|
self, fs: "FileSystem", path: str, **kwargs: Any
|
||||||
|
) -> Iterator[
|
||||||
|
Union[
|
||||||
|
tuple[str, list[str], list[str]],
|
||||||
|
tuple[str, dict[str, dict], dict[str, dict]],
|
||||||
|
]
|
||||||
|
]:
|
||||||
detail = kwargs.get("detail", False)
|
detail = kwargs.get("detail", False)
|
||||||
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
|
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
|
||||||
if fs.protocol == Schemes.LOCAL:
|
if fs.protocol == Schemes.LOCAL:
|
||||||
for root, dirs, files in fs.walk(path, **kwargs):
|
for root, dirs, files in fs.walk(path, **kwargs):
|
||||||
if detail:
|
if detail:
|
||||||
all_dnames = set(dirs.keys())
|
assert isinstance(dirs, dict)
|
||||||
all_fnames = set(files.keys())
|
assert isinstance(files, dict)
|
||||||
dnames, fnames = self(
|
dnames, fnames = self(
|
||||||
root,
|
root,
|
||||||
all_dnames,
|
list(dirs),
|
||||||
all_fnames,
|
list(files),
|
||||||
ignore_subrepos=ignore_subrepos,
|
ignore_subrepos=ignore_subrepos,
|
||||||
)
|
)
|
||||||
list(map(dirs.pop, all_dnames - set(dnames)))
|
list(map(dirs.pop, dirs.keys() - set(dnames)))
|
||||||
list(map(files.pop, all_fnames - set(fnames)))
|
list(map(files.pop, files.keys() - set(fnames)))
|
||||||
else:
|
else:
|
||||||
dirs[:], files[:] = self(
|
dirs[:], files[:] = self(
|
||||||
root, dirs, files, ignore_subrepos=ignore_subrepos
|
root, dirs, files, ignore_subrepos=ignore_subrepos
|
||||||
@@ -321,7 +450,7 @@ class DvcIgnoreFilter:
|
|||||||
else:
|
else:
|
||||||
yield from fs.walk(path, **kwargs)
|
yield from fs.walk(path, **kwargs)
|
||||||
|
|
||||||
def find(self, fs: "FileSystem", path: "AnyFSPath", **kwargs):
|
def find(self, fs: "FileSystem", path: str, **kwargs: Any) -> Iterator[str]:
|
||||||
if fs.protocol == Schemes.LOCAL:
|
if fs.protocol == Schemes.LOCAL:
|
||||||
for root, _, files in self.walk(fs, path, **kwargs):
|
for root, _, files in self.walk(fs, path, **kwargs):
|
||||||
for file in files:
|
for file in files:
|
||||||
@@ -331,7 +460,7 @@ class DvcIgnoreFilter:
|
|||||||
yield from fs.find(path)
|
yield from fs.find(path)
|
||||||
|
|
||||||
def _get_trie_pattern(
|
def _get_trie_pattern(
|
||||||
self, dirname, dnames: Optional["list"] = None, ignore_subrepos=True
|
self, dirname: str, dnames: Optional[list[str]] = None, ignore_subrepos=True
|
||||||
) -> Optional["DvcIgnorePatterns"]:
|
) -> Optional["DvcIgnorePatterns"]:
|
||||||
if ignore_subrepos:
|
if ignore_subrepos:
|
||||||
ignores_trie = self.ignores_trie_fs
|
ignores_trie = self.ignores_trie_fs
|
||||||
@@ -367,7 +496,7 @@ class DvcIgnoreFilter:
|
|||||||
|
|
||||||
def _is_ignored(
|
def _is_ignored(
|
||||||
self, path: str, is_dir: bool = False, ignore_subrepos: bool = True
|
self, path: str, is_dir: bool = False, ignore_subrepos: bool = True
|
||||||
):
|
) -> bool:
|
||||||
if self._outside_repo(path):
|
if self._outside_repo(path):
|
||||||
return False
|
return False
|
||||||
dirname, basename = self.fs.split(self.fs.normpath(path))
|
dirname, basename = self.fs.split(self.fs.normpath(path))
|
||||||
@@ -389,30 +518,29 @@ class DvcIgnoreFilter:
|
|||||||
path = self.fs.abspath(path)
|
path = self.fs.abspath(path)
|
||||||
return self._is_ignored(path, False, ignore_subrepos=ignore_subrepos)
|
return self._is_ignored(path, False, ignore_subrepos=ignore_subrepos)
|
||||||
|
|
||||||
def _outside_repo(self, path):
|
def _outside_repo(self, path: str) -> bool:
|
||||||
return not self.fs.isin_or_eq(path, self.root_dir)
|
return not self.fs.isin_or_eq(path, self.root_dir)
|
||||||
|
|
||||||
def check_ignore(self, target):
|
def check_ignore(self, target: str) -> CheckIgnoreResult:
|
||||||
# NOTE: can only be used in `dvc check-ignore`, see
|
# NOTE: can only be used in `dvc check-ignore`, see
|
||||||
# https://github.com/iterative/dvc/issues/5046
|
# https://github.com/treeverse/dvc/issues/5046
|
||||||
full_target = self.fs.abspath(target)
|
full_target = self.fs.abspath(target)
|
||||||
|
matched_patterns: list[PatternInfo] = []
|
||||||
|
ignore = False
|
||||||
if not self._outside_repo(full_target):
|
if not self._outside_repo(full_target):
|
||||||
dirname, basename = self.fs.split(self.fs.normpath(full_target))
|
dirname, basename = self.fs.split(self.fs.normpath(full_target))
|
||||||
pattern = self._get_trie_pattern(dirname)
|
pattern = self._get_trie_pattern(dirname)
|
||||||
if pattern:
|
if pattern:
|
||||||
matches = pattern.matches(
|
ignore, matched_patterns = pattern.matches(
|
||||||
dirname, basename, self.fs.isdir(full_target), True
|
dirname, basename, self.fs.isdir(full_target), details=True
|
||||||
)
|
)
|
||||||
|
return CheckIgnoreResult(target, ignore, matched_patterns)
|
||||||
if matches:
|
|
||||||
return CheckIgnoreResult(target, True, matches)
|
|
||||||
return _no_match(target)
|
|
||||||
|
|
||||||
def is_ignored(
|
def is_ignored(
|
||||||
self, fs: "FileSystem", path: str, ignore_subrepos: bool = True
|
self, fs: "FileSystem", path: str, ignore_subrepos: bool = True
|
||||||
) -> bool:
|
) -> bool:
|
||||||
# NOTE: can't use self.check_ignore(path).match for now, see
|
# NOTE: can't use self.check_ignore(path).match for now, see
|
||||||
# https://github.com/iterative/dvc/issues/4555
|
# https://github.com/treeverse/dvc/issues/4555
|
||||||
if fs.protocol != Schemes.LOCAL:
|
if fs.protocol != Schemes.LOCAL:
|
||||||
return False
|
return False
|
||||||
if fs.isfile(path):
|
if fs.isfile(path):
|
||||||
@@ -424,7 +552,7 @@ class DvcIgnoreFilter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def init(path):
|
def init(path: Union[str, os.PathLike[str]]) -> str:
|
||||||
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
|
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
|
||||||
if os.path.exists(dvcignore):
|
if os.path.exists(dvcignore):
|
||||||
return dvcignore
|
return dvcignore
|
||||||
@@ -439,7 +567,7 @@ def init(path):
|
|||||||
return dvcignore
|
return dvcignore
|
||||||
|
|
||||||
|
|
||||||
def destroy(path):
|
def destroy(path: Union[str, os.PathLike[str]]) -> None:
|
||||||
from dvc.utils.fs import remove
|
from dvc.utils.fs import remove
|
||||||
|
|
||||||
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
|
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
|
||||||
|
|||||||
+8
-7
@@ -48,7 +48,10 @@ def get_dvc_info():
|
|||||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||||
|
|
||||||
info.append(f"Caches: {_get_caches(repo.cache)}")
|
info.append(f"Caches: {_get_caches(repo.cache)}")
|
||||||
info.append(f"Remotes: {_get_remotes(repo.config)}")
|
|
||||||
|
configured_remotes = _get_remotes(repo.config)
|
||||||
|
remotes = ", ".join(configured_remotes) if configured_remotes else None
|
||||||
|
info.append(f"Remotes: {remotes}")
|
||||||
|
|
||||||
root_directory = repo.root_dir
|
root_directory = repo.root_dir
|
||||||
fs_root = _get_fs_type(os.path.abspath(root_directory))
|
fs_root = _get_fs_type(os.path.abspath(root_directory))
|
||||||
@@ -75,12 +78,10 @@ def _get_caches(cache):
|
|||||||
|
|
||||||
|
|
||||||
def _get_remotes(config):
|
def _get_remotes(config):
|
||||||
schemes = (
|
return [
|
||||||
get_fs_cls(get_fs_config(config, name=remote)).protocol
|
get_fs_cls(get_fs_config(config, name=remote)).protocol
|
||||||
for remote in config["remote"]
|
for remote in config["remote"]
|
||||||
)
|
]
|
||||||
|
|
||||||
return ", ".join(schemes) or "None"
|
|
||||||
|
|
||||||
|
|
||||||
def _get_linktype_support_info(repo):
|
def _get_linktype_support_info(repo):
|
||||||
@@ -123,7 +124,7 @@ def _get_supported_remotes():
|
|||||||
if not fs_cls.get_missing_deps():
|
if not fs_cls.get_missing_deps():
|
||||||
dependencies = []
|
dependencies = []
|
||||||
for requirement in fs_cls.REQUIRES:
|
for requirement in fs_cls.REQUIRES:
|
||||||
dependencies.append(
|
dependencies.append( # noqa: PERF401
|
||||||
f"{requirement} = {importlib_metadata.version(requirement)}"
|
f"{requirement} = {importlib_metadata.version(requirement)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ def _get_fs_type(path):
|
|||||||
except PermissionError:
|
except PermissionError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# need to follow the symlink: https://github.com/iterative/dvc/issues/5065
|
# need to follow the symlink: https://github.com/treeverse/dvc/issues/5065
|
||||||
path = pathlib.Path(path).resolve()
|
path = pathlib.Path(path).resolve()
|
||||||
|
|
||||||
for parent in itertools.chain([path], path.parents):
|
for parent in itertools.chain([path], path.parents):
|
||||||
|
|||||||
+48
-21
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
import flufl.lock
|
import flufl.lock
|
||||||
import zc.lockfile
|
import zc.lockfile
|
||||||
from funcy import retry
|
|
||||||
|
|
||||||
from dvc.exceptions import DvcException
|
from dvc.exceptions import DvcException
|
||||||
from dvc.progress import Tqdm
|
from dvc.progress import Tqdm
|
||||||
@@ -87,9 +88,10 @@ class Lock(LockBase):
|
|||||||
Uses zc.lockfile as backend.
|
Uses zc.lockfile as backend.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, lockfile, friendly=False, **kwargs):
|
def __init__(self, lockfile, friendly=False, wait=False, **kwargs):
|
||||||
super().__init__(lockfile)
|
super().__init__(lockfile)
|
||||||
self._friendly = friendly
|
self._friendly = friendly
|
||||||
|
self._wait = wait
|
||||||
self._lock = None
|
self._lock = None
|
||||||
self._lock_failed = False
|
self._lock_failed = False
|
||||||
|
|
||||||
@@ -100,23 +102,38 @@ class Lock(LockBase):
|
|||||||
def _do_lock(self):
|
def _do_lock(self):
|
||||||
try:
|
try:
|
||||||
self._lock_failed = False
|
self._lock_failed = False
|
||||||
with Tqdm(
|
self._lock = zc.lockfile.LockFile(self._lockfile)
|
||||||
bar_format="{desc}",
|
|
||||||
disable=not self._friendly,
|
|
||||||
desc="If DVC froze, see `hardlink_lock` in {}".format(
|
|
||||||
format_link("https://man.dvc.org/config#core")
|
|
||||||
),
|
|
||||||
):
|
|
||||||
self._lock = zc.lockfile.LockFile(self._lockfile)
|
|
||||||
except zc.lockfile.LockError:
|
except zc.lockfile.LockError:
|
||||||
self._lock_failed = True
|
self._lock_failed = True
|
||||||
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
|
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
|
||||||
|
|
||||||
def lock(self):
|
def lock(self):
|
||||||
retries = 6
|
"""Acquire the lock, either waiting forever, or after default_retries."""
|
||||||
delay = DEFAULT_TIMEOUT / retries
|
default_retries = 6
|
||||||
lock_retry = retry(retries, LockError, timeout=delay)(self._do_lock)
|
delay = DEFAULT_TIMEOUT / default_retries
|
||||||
lock_retry()
|
attempts = 0
|
||||||
|
|
||||||
|
max_retries = float("inf") if self._wait else default_retries
|
||||||
|
|
||||||
|
with Tqdm(
|
||||||
|
bar_format="{desc}",
|
||||||
|
disable=not self._friendly,
|
||||||
|
desc="Waiting to acquire lock. "
|
||||||
|
"If DVC froze, see `hardlink_lock` in {}.".format(
|
||||||
|
format_link("https://man.dvc.org/config#core")
|
||||||
|
),
|
||||||
|
) as pbar:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
self._do_lock()
|
||||||
|
return
|
||||||
|
except LockError:
|
||||||
|
attempts += 1
|
||||||
|
if attempts > max_retries:
|
||||||
|
raise
|
||||||
|
time.sleep(delay)
|
||||||
|
finally:
|
||||||
|
pbar.update()
|
||||||
|
|
||||||
def unlock(self):
|
def unlock(self):
|
||||||
if self._lock_failed:
|
if self._lock_failed:
|
||||||
@@ -149,7 +166,7 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
|||||||
tmp_dir (str): a directory to store claim files.
|
tmp_dir (str): a directory to store claim files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, lockfile, tmp_dir=None, **kwargs):
|
def __init__(self, lockfile, tmp_dir=None, wait=False, **kwargs):
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
self._tmp_dir = tmp_dir
|
self._tmp_dir = tmp_dir
|
||||||
@@ -165,18 +182,28 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
|||||||
# number to generate the resulting lock file name, which is unique
|
# number to generate the resulting lock file name, which is unique
|
||||||
# enough for our application.
|
# enough for our application.
|
||||||
#
|
#
|
||||||
# [1] https://github.com/iterative/dvc/issues/2582
|
# [1] https://github.com/treeverse/dvc/issues/2582
|
||||||
self._hostname = socket.gethostname()
|
self._hostname = socket.gethostname()
|
||||||
|
|
||||||
self._lifetime = timedelta(days=365) # Lock for good by default
|
self._lifetime = timedelta(days=365) # Lock for good by default
|
||||||
self._separator = flufl.lock.SEP
|
self._separator = flufl.lock.SEP
|
||||||
self._set_claimfile()
|
self._set_claimfile()
|
||||||
self._owned = True
|
self._owned = True
|
||||||
|
self._wait = wait
|
||||||
self._retry_errnos = []
|
self._retry_errnos = []
|
||||||
|
self._friendly = kwargs.get("friendly", False)
|
||||||
|
|
||||||
def lock(self):
|
def lock(self, timeout: Optional[Union[timedelta, int]] = None):
|
||||||
try:
|
try:
|
||||||
super().lock(timedelta(seconds=DEFAULT_TIMEOUT))
|
if not self._wait:
|
||||||
|
timeout = timeout or timedelta(seconds=DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
|
with Tqdm(
|
||||||
|
bar_format="{desc}",
|
||||||
|
disable=not (self._wait and self._friendly),
|
||||||
|
desc="Waiting to acquire lock",
|
||||||
|
):
|
||||||
|
super().lock(timeout)
|
||||||
except flufl.lock.TimeOutError:
|
except flufl.lock.TimeOutError:
|
||||||
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
|
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
|
||||||
|
|
||||||
@@ -185,11 +212,11 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
|||||||
|
|
||||||
if self._tmp_dir is not None:
|
if self._tmp_dir is not None:
|
||||||
# Under Windows file path length is limited so we hash it
|
# Under Windows file path length is limited so we hash it
|
||||||
hasher = hashlib.md5(self._claimfile.encode()) # noqa: S324
|
hasher = hashlib.md5(self._claimfile.encode(), usedforsecurity=False)
|
||||||
filename = hasher.hexdigest()
|
filename = hasher.hexdigest()
|
||||||
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
|
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
|
||||||
|
|
||||||
|
|
||||||
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False):
|
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False, wait=False):
|
||||||
cls = HardlinkLock if hardlink_lock else Lock
|
cls = HardlinkLock if hardlink_lock else Lock
|
||||||
return cls(lockfile, tmp_dir=tmp_dir, friendly=friendly)
|
return cls(lockfile, tmp_dir=tmp_dir, friendly=friendly, wait=wait)
|
||||||
|
|||||||
+16
-6
@@ -1,10 +1,10 @@
|
|||||||
"""Manages logging configuration for DVC repo."""
|
"""Manages logging configuration for DVC repo."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
|
||||||
import logging.handlers
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from contextlib import contextmanager
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
@@ -36,7 +36,7 @@ def add_logging_level(level_name, level_num, method_name=None):
|
|||||||
self._log(level_num, message, args, **kwargs)
|
self._log(level_num, message, args, **kwargs)
|
||||||
|
|
||||||
def log_to_root(message, *args, **kwargs):
|
def log_to_root(message, *args, **kwargs):
|
||||||
logging.log(level_num, message, *args, **kwargs)
|
logging.log(level_num, message, *args, **kwargs) # noqa: LOG015
|
||||||
|
|
||||||
# getLevelName resolves the numeric log level if already defined,
|
# getLevelName resolves the numeric log level if already defined,
|
||||||
# otherwise returns a string
|
# otherwise returns a string
|
||||||
@@ -53,7 +53,7 @@ def add_logging_level(level_name, level_num, method_name=None):
|
|||||||
setattr(logging, method_name, log_to_root)
|
setattr(logging, method_name, log_to_root)
|
||||||
|
|
||||||
|
|
||||||
class LoggingException(Exception):
|
class LoggingException(Exception): # noqa: N818
|
||||||
def __init__(self, record):
|
def __init__(self, record):
|
||||||
msg = f"failed to log {record!s}"
|
msg = f"failed to log {record!s}"
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
@@ -174,9 +174,19 @@ def _iter_causes(exc):
|
|||||||
exc = exc.__cause__
|
exc = exc.__cause__
|
||||||
|
|
||||||
|
|
||||||
def set_loggers_level(level: int = logging.INFO) -> None:
|
@contextmanager
|
||||||
|
def set_loggers_level(level: int = logging.INFO) -> Iterator[None]:
|
||||||
|
ret: dict[logging.Logger, int] = {}
|
||||||
for name in ["dvc", "dvc_objects", "dvc_data"]:
|
for name in ["dvc", "dvc_objects", "dvc_data"]:
|
||||||
logging.getLogger(name).setLevel(level)
|
_logger = logging.getLogger(name)
|
||||||
|
ret[_logger] = _logger.getEffectiveLevel()
|
||||||
|
_logger.setLevel(level)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
for _logger, old_lvl in ret.items():
|
||||||
|
_logger.setLevel(old_lvl)
|
||||||
|
|
||||||
|
|
||||||
def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
|
def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
|
||||||
|
|||||||
+82
-40
@@ -42,10 +42,11 @@ from .utils import relpath
|
|||||||
from .utils.fs import path_isin
|
from .utils.fs import path_isin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from dvc.repo import Repo
|
||||||
from dvc_data.hashfile.obj import HashFile
|
from dvc_data.hashfile.obj import HashFile
|
||||||
from dvc_data.index import DataIndexKey
|
from dvc_data.index import DataIndexKey
|
||||||
|
|
||||||
from .ignore import DvcIgnoreFilter
|
from .ignore import CheckIgnoreResult, DvcIgnoreFilter
|
||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ def _get(stage, path, **kwargs):
|
|||||||
return Output(stage, path, **kwargs)
|
return Output(stage, path, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def loadd_from(stage, d_list):
|
def loadd_from(stage, d_list) -> list["Output"]:
|
||||||
ret = []
|
ret = []
|
||||||
for d in d_list:
|
for d in d_list:
|
||||||
p = d.pop(Output.PARAM_PATH)
|
p = d.pop(Output.PARAM_PATH)
|
||||||
@@ -267,9 +268,9 @@ class OutputIsStageFileError(DvcException):
|
|||||||
|
|
||||||
|
|
||||||
class OutputIsIgnoredError(DvcException):
|
class OutputIsIgnoredError(DvcException):
|
||||||
def __init__(self, match):
|
def __init__(self, result: "CheckIgnoreResult"):
|
||||||
lines = "\n".join(match.patterns)
|
pi = result.pattern_infos[-1]
|
||||||
super().__init__(f"Path '{match.file}' is ignored by\n{lines}")
|
super().__init__(f"Path {result.file!r} is ignored by {pi!s}")
|
||||||
|
|
||||||
|
|
||||||
class CheckoutCallback(TqdmCallback):
|
class CheckoutCallback(TqdmCallback):
|
||||||
@@ -329,7 +330,7 @@ class Output:
|
|||||||
self.annot = Annotation(
|
self.annot = Annotation(
|
||||||
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
desc=desc, type=type, labels=labels or [], meta=meta or {}
|
||||||
)
|
)
|
||||||
self.repo = stage.repo if not repo and stage else repo
|
self.repo: Optional[Repo] = stage.repo if not repo and stage else repo
|
||||||
meta_d = merge_file_meta_from_cloud(info or {})
|
meta_d = merge_file_meta_from_cloud(info or {})
|
||||||
meta = Meta.from_dict(meta_d)
|
meta = Meta.from_dict(meta_d)
|
||||||
# NOTE: when version_aware is not passed into get_cloud_fs, it will be
|
# NOTE: when version_aware is not passed into get_cloud_fs, it will be
|
||||||
@@ -390,7 +391,7 @@ class Output:
|
|||||||
self.can_push = push
|
self.can_push = push
|
||||||
|
|
||||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||||
self.obj: Optional["HashFile"] = None
|
self.obj: Optional[HashFile] = None
|
||||||
|
|
||||||
self.remote = remote
|
self.remote = remote
|
||||||
|
|
||||||
@@ -431,7 +432,7 @@ class Output:
|
|||||||
self.meta.nfiles = len(self.files)
|
self.meta.nfiles = len(self.files)
|
||||||
self.meta.size = sum(filter(None, (f.get("size") for f in self.files)))
|
self.meta.size = sum(filter(None, (f.get("size") for f in self.files)))
|
||||||
self.meta.remote = first(f.get("remote") for f in self.files)
|
self.meta.remote = first(f.get("remote") for f in self.files)
|
||||||
elif self.meta.nfiles or self.hash_info and self.hash_info.isdir:
|
elif self.meta.nfiles or (self.hash_info and self.hash_info.isdir):
|
||||||
self.meta.isdir = True
|
self.meta.isdir = True
|
||||||
if not self.hash_info and self.hash_name not in ("md5", "md5-dos2unix"):
|
if not self.hash_info and self.hash_name not in ("md5", "md5-dos2unix"):
|
||||||
md5 = getattr(self.meta, "md5", None)
|
md5 = getattr(self.meta, "md5", None)
|
||||||
@@ -511,6 +512,7 @@ class Output:
|
|||||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||||
|
|
||||||
assert self.is_in_repo
|
assert self.is_in_repo
|
||||||
|
assert self.repo
|
||||||
odb_name = "legacy" if self.hash_name in LEGACY_HASH_NAMES else "repo"
|
odb_name = "legacy" if self.hash_name in LEGACY_HASH_NAMES else "repo"
|
||||||
return getattr(self.repo.cache, odb_name)
|
return getattr(self.repo.cache, odb_name)
|
||||||
|
|
||||||
@@ -518,6 +520,7 @@ class Output:
|
|||||||
def local_cache(self):
|
def local_cache(self):
|
||||||
from dvc.cachemgr import LEGACY_HASH_NAMES
|
from dvc.cachemgr import LEGACY_HASH_NAMES
|
||||||
|
|
||||||
|
assert self.repo
|
||||||
if self.hash_name in LEGACY_HASH_NAMES:
|
if self.hash_name in LEGACY_HASH_NAMES:
|
||||||
return self.repo.cache.legacy
|
return self.repo.cache.legacy
|
||||||
return self.repo.cache.local
|
return self.repo.cache.local
|
||||||
@@ -542,7 +545,9 @@ class Output:
|
|||||||
desc=f"Collecting files and computing hashes in {self}",
|
desc=f"Collecting files and computing hashes in {self}",
|
||||||
disable=no_progress_bar,
|
disable=no_progress_bar,
|
||||||
) as pb:
|
) as pb:
|
||||||
return build(*args, callback=pb.as_callback(), **kwargs)
|
kwargs["callback"] = pb.as_callback()
|
||||||
|
kwargs.setdefault("checksum_jobs", self.fs.hash_jobs)
|
||||||
|
return build(*args, **kwargs)
|
||||||
|
|
||||||
def _get_hash_meta(self):
|
def _get_hash_meta(self):
|
||||||
if self.use_cache:
|
if self.use_cache:
|
||||||
@@ -583,6 +588,7 @@ class Output:
|
|||||||
def index_key(self) -> tuple[str, "DataIndexKey"]:
|
def index_key(self) -> tuple[str, "DataIndexKey"]:
|
||||||
if self.is_in_repo:
|
if self.is_in_repo:
|
||||||
workspace = "repo"
|
workspace = "repo"
|
||||||
|
assert self.repo
|
||||||
key = self.repo.fs.relparts(self.fs_path, self.repo.root_dir)
|
key = self.repo.fs.relparts(self.fs_path, self.repo.root_dir)
|
||||||
else:
|
else:
|
||||||
workspace = self.fs.protocol
|
workspace = self.fs.protocol
|
||||||
@@ -638,6 +644,7 @@ class Output:
|
|||||||
@property
|
@property
|
||||||
def dvcignore(self) -> Optional["DvcIgnoreFilter"]:
|
def dvcignore(self) -> Optional["DvcIgnoreFilter"]:
|
||||||
if self.fs.protocol == "local":
|
if self.fs.protocol == "local":
|
||||||
|
assert self.repo
|
||||||
return self.repo.dvcignore
|
return self.repo.dvcignore
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -659,6 +666,7 @@ class Output:
|
|||||||
if not self.use_scm_ignore:
|
if not self.use_scm_ignore:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
assert self.repo
|
||||||
if self.repo.scm.is_tracked(self.fspath):
|
if self.repo.scm.is_tracked(self.fspath):
|
||||||
raise OutputAlreadyTrackedError(self)
|
raise OutputAlreadyTrackedError(self)
|
||||||
|
|
||||||
@@ -668,6 +676,7 @@ class Output:
|
|||||||
if not self.use_scm_ignore:
|
if not self.use_scm_ignore:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
assert self.repo
|
||||||
self.repo.scm_context.ignore_remove(self.fspath)
|
self.repo.scm_context.ignore_remove(self.fspath)
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
@@ -730,6 +739,7 @@ class Output:
|
|||||||
from dvc_data.hashfile.checkout import LinkError, PromptError
|
from dvc_data.hashfile.checkout import LinkError, PromptError
|
||||||
|
|
||||||
kwargs.setdefault("ignore", self.dvcignore)
|
kwargs.setdefault("ignore", self.dvcignore)
|
||||||
|
kwargs.setdefault("checksum_jobs", self.fs.hash_jobs)
|
||||||
try:
|
try:
|
||||||
return checkout(*args, **kwargs)
|
return checkout(*args, **kwargs)
|
||||||
except PromptError as exc:
|
except PromptError as exc:
|
||||||
@@ -749,11 +759,9 @@ class Output:
|
|||||||
granular = (
|
granular = (
|
||||||
self.is_dir_checksum and filter_info and filter_info != self.fs_path
|
self.is_dir_checksum and filter_info and filter_info != self.fs_path
|
||||||
)
|
)
|
||||||
# NOTE: trying to use hardlink during transfer only if we will be
|
hardlink = relink and next(iter(self.cache.cache_types), None) == "hardlink"
|
||||||
# relinking later
|
|
||||||
hardlink = relink
|
|
||||||
if granular:
|
if granular:
|
||||||
obj = self._commit_granular_dir(filter_info, hardlink)
|
obj = self._commit_granular_dir(filter_info, hardlink=hardlink)
|
||||||
else:
|
else:
|
||||||
staging, _, obj = self._build(
|
staging, _, obj = self._build(
|
||||||
self.cache,
|
self.cache,
|
||||||
@@ -775,6 +783,7 @@ class Output:
|
|||||||
callback=cb,
|
callback=cb,
|
||||||
)
|
)
|
||||||
if relink:
|
if relink:
|
||||||
|
assert self.repo
|
||||||
rel = self.fs.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:
|
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as cb:
|
||||||
self._checkout(
|
self._checkout(
|
||||||
@@ -786,6 +795,7 @@ class Output:
|
|||||||
state=self.repo.state,
|
state=self.repo.state,
|
||||||
prompt=prompt.confirm,
|
prompt=prompt.confirm,
|
||||||
progress_callback=cb,
|
progress_callback=cb,
|
||||||
|
old=obj,
|
||||||
)
|
)
|
||||||
self.set_exec()
|
self.set_exec()
|
||||||
|
|
||||||
@@ -814,7 +824,7 @@ class Output:
|
|||||||
|
|
||||||
ret: dict[str, Any] = {}
|
ret: dict[str, Any] = {}
|
||||||
with_files = (
|
with_files = (
|
||||||
(not self.IS_DEPENDENCY or self.stage.is_import)
|
(not self.IS_DEPENDENCY or kwargs.get("datasets") or self.stage.is_import)
|
||||||
and self.hash_info.isdir
|
and self.hash_info.isdir
|
||||||
and (kwargs.get("with_files") or self.files is not None)
|
and (kwargs.get("with_files") or self.files is not None)
|
||||||
)
|
)
|
||||||
@@ -902,7 +912,7 @@ class Output:
|
|||||||
def get_obj(
|
def get_obj(
|
||||||
self, filter_info: Optional[str] = None, **kwargs
|
self, filter_info: Optional[str] = None, **kwargs
|
||||||
) -> Optional["HashFile"]:
|
) -> Optional["HashFile"]:
|
||||||
obj: Optional["HashFile"] = None
|
obj: Optional[HashFile] = None
|
||||||
if self.obj:
|
if self.obj:
|
||||||
obj = self.obj
|
obj = self.obj
|
||||||
elif self.files:
|
elif self.files:
|
||||||
@@ -955,6 +965,7 @@ class Output:
|
|||||||
added = not self.exists
|
added = not self.exists
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
assert self.repo
|
||||||
modified = self._checkout(
|
modified = self._checkout(
|
||||||
filter_info or self.fs_path,
|
filter_info or self.fs_path,
|
||||||
self.fs,
|
self.fs,
|
||||||
@@ -985,18 +996,24 @@ class Output:
|
|||||||
if ignore_remove:
|
if ignore_remove:
|
||||||
self.ignore_remove()
|
self.ignore_remove()
|
||||||
|
|
||||||
def move(self, out):
|
def move(self, out: "Output") -> None:
|
||||||
|
src_exists = self.exists
|
||||||
|
if src_exists:
|
||||||
|
self.fs.move(self.fs_path, out.fs_path)
|
||||||
|
else:
|
||||||
|
logger.warning("%r missing", self.fspath)
|
||||||
|
|
||||||
if self.protocol == "local" and self.use_scm_ignore:
|
if self.protocol == "local" and self.use_scm_ignore:
|
||||||
|
assert self.repo
|
||||||
self.repo.scm_context.ignore_remove(self.fspath)
|
self.repo.scm_context.ignore_remove(self.fspath)
|
||||||
|
|
||||||
self.fs.move(self.fs_path, out.fs_path)
|
|
||||||
self.def_path = out.def_path
|
self.def_path = out.def_path
|
||||||
self.fs_path = out.fs_path
|
self.fs_path = out.fs_path
|
||||||
self.save()
|
try:
|
||||||
self.commit()
|
self.save()
|
||||||
|
self.commit()
|
||||||
if self.protocol == "local" and self.use_scm_ignore:
|
except self.DoesNotExistError:
|
||||||
self.repo.scm_context.ignore(self.fspath)
|
self.ignore()
|
||||||
|
|
||||||
def transfer(
|
def transfer(
|
||||||
self, source, odb=None, jobs=None, update=False, no_progress_bar=False
|
self, source, odb=None, jobs=None, update=False, no_progress_bar=False
|
||||||
@@ -1077,6 +1094,7 @@ class Output:
|
|||||||
if self.remote:
|
if self.remote:
|
||||||
kwargs["remote"] = self.remote
|
kwargs["remote"] = self.remote
|
||||||
with suppress(Exception):
|
with suppress(Exception):
|
||||||
|
assert self.repo
|
||||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||||
|
|
||||||
if self.obj:
|
if self.obj:
|
||||||
@@ -1158,7 +1176,7 @@ class Output:
|
|||||||
logger.warning(msg)
|
logger.warning(msg)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
obj: Optional["HashFile"]
|
obj: Optional[HashFile]
|
||||||
if self.is_dir_checksum:
|
if self.is_dir_checksum:
|
||||||
obj = self._collect_used_dir_cache(**kwargs)
|
obj = self._collect_used_dir_cache(**kwargs)
|
||||||
else:
|
else:
|
||||||
@@ -1170,6 +1188,7 @@ class Output:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
if self.remote:
|
if self.remote:
|
||||||
|
assert self.repo
|
||||||
remote_odb = self.repo.cloud.get_remote_odb(
|
remote_odb = self.repo.cloud.get_remote_odb(
|
||||||
name=self.remote, hash_name=self.hash_name
|
name=self.remote, hash_name=self.hash_name
|
||||||
)
|
)
|
||||||
@@ -1201,8 +1220,9 @@ class Output:
|
|||||||
if stage:
|
if stage:
|
||||||
abs_path = os.path.join(stage.wdir, path)
|
abs_path = os.path.join(stage.wdir, path)
|
||||||
if self._is_path_dvcignore(abs_path):
|
if self._is_path_dvcignore(abs_path):
|
||||||
check = stage.repo.dvcignore.check_ignore(abs_path)
|
check: CheckIgnoreResult = stage.repo.dvcignore.check_ignore(abs_path)
|
||||||
raise self.IsIgnoredError(check)
|
if check.match:
|
||||||
|
raise self.IsIgnoredError(check)
|
||||||
|
|
||||||
def _check_can_merge(self, out):
|
def _check_can_merge(self, out):
|
||||||
if self.protocol != out.protocol:
|
if self.protocol != out.protocol:
|
||||||
@@ -1353,7 +1373,7 @@ class Output:
|
|||||||
cache = self.cache if self.use_cache else self.local_cache
|
cache = self.cache if self.use_cache else self.local_cache
|
||||||
assert isinstance(cache, HashFileDB)
|
assert isinstance(cache, HashFileDB)
|
||||||
|
|
||||||
new: "HashFile"
|
new: HashFile
|
||||||
try:
|
try:
|
||||||
assert self.hash_name
|
assert self.hash_name
|
||||||
staging, meta, obj = self._build(
|
staging, meta, obj = self._build(
|
||||||
@@ -1365,7 +1385,7 @@ class Output:
|
|||||||
dry_run=not self.use_cache,
|
dry_run=not self.use_cache,
|
||||||
)
|
)
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
if self.fs_path == path:
|
if not self.exists:
|
||||||
raise self.DoesNotExistError(self) from exc
|
raise self.DoesNotExistError(self) from exc
|
||||||
if not self.is_dir_checksum:
|
if not self.is_dir_checksum:
|
||||||
raise
|
raise
|
||||||
@@ -1398,12 +1418,14 @@ class Output:
|
|||||||
|
|
||||||
assert staging
|
assert staging
|
||||||
assert obj.hash_info
|
assert obj.hash_info
|
||||||
|
|
||||||
|
hardlink = relink and next(iter(self.cache.cache_types), None) == "hardlink"
|
||||||
with TqdmCallback(desc=f"Adding {self} to cache", unit="file") as cb:
|
with TqdmCallback(desc=f"Adding {self} to cache", unit="file") as cb:
|
||||||
otransfer(
|
otransfer(
|
||||||
staging,
|
staging,
|
||||||
self.cache,
|
self.cache,
|
||||||
{obj.hash_info},
|
{obj.hash_info},
|
||||||
hardlink=relink,
|
hardlink=hardlink,
|
||||||
shallow=False,
|
shallow=False,
|
||||||
callback=cb,
|
callback=cb,
|
||||||
)
|
)
|
||||||
@@ -1421,6 +1443,7 @@ class Output:
|
|||||||
state=self.repo.state,
|
state=self.repo.state,
|
||||||
prompt=prompt.confirm,
|
prompt=prompt.confirm,
|
||||||
progress_callback=callback,
|
progress_callback=callback,
|
||||||
|
old=obj,
|
||||||
)
|
)
|
||||||
self.set_exec()
|
self.set_exec()
|
||||||
return obj
|
return obj
|
||||||
@@ -1447,25 +1470,44 @@ class Output:
|
|||||||
self.remote = other.remote
|
self.remote = other.remote
|
||||||
self.can_push = other.can_push
|
self.can_push = other.can_push
|
||||||
|
|
||||||
def merge_version_meta(self, other: "Output"):
|
def _get_versioned_meta(
|
||||||
|
self,
|
||||||
|
) -> Optional[
|
||||||
|
tuple["HashInfo", Optional["Meta"], Optional[Union["HashFile", "Tree"]]]
|
||||||
|
]:
|
||||||
|
if self.files is not None or (
|
||||||
|
self.meta is not None and self.meta.version_id is not None
|
||||||
|
):
|
||||||
|
old_obj = self.obj if self.obj is not None else self.get_obj()
|
||||||
|
return self.hash_info, self.meta, old_obj
|
||||||
|
return None
|
||||||
|
|
||||||
|
def merge_version_meta(
|
||||||
|
self,
|
||||||
|
old_hi: "HashInfo",
|
||||||
|
old_meta: Optional["Meta"],
|
||||||
|
old_obj: Optional[Union["HashFile", "Tree"]],
|
||||||
|
):
|
||||||
"""Merge version meta for files which are unchanged from other."""
|
"""Merge version meta for files which are unchanged from other."""
|
||||||
if not self.hash_info:
|
if not self.hash_info:
|
||||||
return
|
return None
|
||||||
if self.hash_info.isdir:
|
if self.hash_info.isdir:
|
||||||
return self._merge_dir_version_meta(other)
|
return self._merge_dir_version_meta(old_hi, old_obj)
|
||||||
if self.hash_info != other.hash_info:
|
if self.hash_info != old_hi:
|
||||||
return
|
return None
|
||||||
self.meta = other.meta
|
self.meta = old_meta
|
||||||
|
return None
|
||||||
|
|
||||||
def _merge_dir_version_meta(self, other: "Output"):
|
def _merge_dir_version_meta(
|
||||||
|
self, old_hi: "HashInfo", old_obj: Optional[Union["HashFile", "Tree"]]
|
||||||
|
):
|
||||||
from dvc_data.hashfile.tree import update_meta
|
from dvc_data.hashfile.tree import update_meta
|
||||||
|
|
||||||
if not self.obj or not other.hash_info.isdir:
|
if not self.obj or not old_hi.isdir:
|
||||||
return
|
return
|
||||||
other_obj = other.obj if other.obj is not None else other.get_obj()
|
|
||||||
assert isinstance(self.obj, Tree)
|
assert isinstance(self.obj, Tree)
|
||||||
assert isinstance(other_obj, Tree)
|
assert isinstance(old_obj, Tree)
|
||||||
updated = update_meta(self.obj, other_obj)
|
updated = update_meta(self.obj, old_obj)
|
||||||
assert updated.hash_info == self.obj.hash_info
|
assert updated.hash_info == self.obj.hash_info
|
||||||
self.obj = updated
|
self.obj = updated
|
||||||
self.files = updated.as_list(with_meta=True)
|
self.files = updated.as_list(with_meta=True)
|
||||||
@@ -1478,7 +1520,7 @@ META_SCHEMA = {
|
|||||||
Meta.PARAM_VERSION_ID: str,
|
Meta.PARAM_VERSION_ID: str,
|
||||||
}
|
}
|
||||||
|
|
||||||
CLOUD_SCHEMA = vol.All({str: {**META_SCHEMA, **CHECKSUMS_SCHEMA}}, vol.Length(max=1))
|
CLOUD_SCHEMA = vol.All({str: META_SCHEMA | CHECKSUMS_SCHEMA}, vol.Length(max=1))
|
||||||
|
|
||||||
ARTIFACT_SCHEMA: dict[Any, Any] = {
|
ARTIFACT_SCHEMA: dict[Any, Any] = {
|
||||||
**CHECKSUMS_SCHEMA,
|
**CHECKSUMS_SCHEMA,
|
||||||
|
|||||||
+92
-3
@@ -21,6 +21,7 @@ from .context import (
|
|||||||
VarsAlreadyLoaded,
|
VarsAlreadyLoaded,
|
||||||
)
|
)
|
||||||
from .interpolate import (
|
from .interpolate import (
|
||||||
|
check_expression,
|
||||||
check_recursive_parse_errors,
|
check_recursive_parse_errors,
|
||||||
is_interpolated_string,
|
is_interpolated_string,
|
||||||
recurse,
|
recurse,
|
||||||
@@ -38,10 +39,16 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = logger.getChild(__name__)
|
logger = logger.getChild(__name__)
|
||||||
|
|
||||||
STAGES_KWD = "stages"
|
|
||||||
VARS_KWD = "vars"
|
VARS_KWD = "vars"
|
||||||
WDIR_KWD = "wdir"
|
WDIR_KWD = "wdir"
|
||||||
|
|
||||||
|
ARTIFACTS_KWD = "artifacts"
|
||||||
|
DATASETS_KWD = "datasets"
|
||||||
|
METRICS_KWD = "metrics"
|
||||||
PARAMS_KWD = "params"
|
PARAMS_KWD = "params"
|
||||||
|
PLOTS_KWD = "plots"
|
||||||
|
STAGES_KWD = "stages"
|
||||||
|
|
||||||
FOREACH_KWD = "foreach"
|
FOREACH_KWD = "foreach"
|
||||||
MATRIX_KWD = "matrix"
|
MATRIX_KWD = "matrix"
|
||||||
DO_KWD = "do"
|
DO_KWD = "do"
|
||||||
@@ -163,6 +170,27 @@ class DataResolver:
|
|||||||
for name, definition in stages_data.items()
|
for name, definition in stages_data.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.artifacts = [
|
||||||
|
ArtifactDefinition(self, self.context, name, definition, ARTIFACTS_KWD)
|
||||||
|
for name, definition in d.get(ARTIFACTS_KWD, {}).items()
|
||||||
|
]
|
||||||
|
self.datasets = [
|
||||||
|
TopDefinition(self, self.context, str(i), definition, DATASETS_KWD)
|
||||||
|
for i, definition in enumerate(d.get(DATASETS_KWD, []))
|
||||||
|
]
|
||||||
|
self.metrics = [
|
||||||
|
TopDefinition(self, self.context, str(i), definition, METRICS_KWD)
|
||||||
|
for i, definition in enumerate(d.get(METRICS_KWD, []))
|
||||||
|
]
|
||||||
|
self.params = [
|
||||||
|
TopDefinition(self, self.context, str(i), definition, PARAMS_KWD)
|
||||||
|
for i, definition in enumerate(d.get(PARAMS_KWD, []))
|
||||||
|
]
|
||||||
|
self.plots = [
|
||||||
|
TopDefinition(self, self.context, str(i), definition, PLOTS_KWD)
|
||||||
|
for i, definition in enumerate(d.get(PLOTS_KWD, []))
|
||||||
|
]
|
||||||
|
|
||||||
def resolve_one(self, name: str):
|
def resolve_one(self, name: str):
|
||||||
group, key = split_group_name(name)
|
group, key = split_group_name(name)
|
||||||
|
|
||||||
@@ -186,6 +214,27 @@ class DataResolver:
|
|||||||
logger.trace("Resolved dvc.yaml:\n%s", data)
|
logger.trace("Resolved dvc.yaml:\n%s", data)
|
||||||
return {STAGES_KWD: data}
|
return {STAGES_KWD: data}
|
||||||
|
|
||||||
|
# Top-level sections are eagerly evaluated, whereas stages are lazily evaluated,
|
||||||
|
# one-by-one.
|
||||||
|
|
||||||
|
def resolve_artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||||
|
d: dict[str, Optional[dict[str, Any]]] = {}
|
||||||
|
for item in self.artifacts:
|
||||||
|
d.update(item.resolve())
|
||||||
|
return d
|
||||||
|
|
||||||
|
def resolve_datasets(self) -> list[dict[str, Any]]:
|
||||||
|
return [item.resolve() for item in self.datasets]
|
||||||
|
|
||||||
|
def resolve_metrics(self) -> list[str]:
|
||||||
|
return [item.resolve() for item in self.metrics]
|
||||||
|
|
||||||
|
def resolve_params(self) -> list[str]:
|
||||||
|
return [item.resolve() for item in self.params]
|
||||||
|
|
||||||
|
def resolve_plots(self) -> list[Any]:
|
||||||
|
return [item.resolve() for item in self.plots]
|
||||||
|
|
||||||
def has_key(self, key: str):
|
def has_key(self, key: str):
|
||||||
return self._has_group_and_key(*split_group_name(key))
|
return self._has_group_and_key(*split_group_name(key))
|
||||||
|
|
||||||
@@ -514,10 +563,10 @@ class MatrixDefinition:
|
|||||||
iterable = self.resolved_iterable
|
iterable = self.resolved_iterable
|
||||||
assert isinstance(iterable, Mapping)
|
assert isinstance(iterable, Mapping)
|
||||||
|
|
||||||
ret: dict[str, "DictStrAny"] = {}
|
ret: dict[str, DictStrAny] = {}
|
||||||
matrix = {key: enumerate(v) for key, v in iterable.items()}
|
matrix = {key: enumerate(v) for key, v in iterable.items()}
|
||||||
for combination in product(*matrix.values()):
|
for combination in product(*matrix.values()):
|
||||||
d: "DictStrAny" = {}
|
d: DictStrAny = {}
|
||||||
fragments: list[str] = []
|
fragments: list[str] = []
|
||||||
for k, (i, v) in zip(matrix.keys(), combination):
|
for k, (i, v) in zip(matrix.keys(), combination):
|
||||||
d[k] = v
|
d[k] = v
|
||||||
@@ -565,3 +614,43 @@ class MatrixDefinition:
|
|||||||
return entry.resolve_stage(skip_checks=True)
|
return entry.resolve_stage(skip_checks=True)
|
||||||
except ContextError as exc:
|
except ContextError as exc:
|
||||||
format_and_raise(exc, f"stage '{generated}'", self.relpath)
|
format_and_raise(exc, f"stage '{generated}'", self.relpath)
|
||||||
|
|
||||||
|
|
||||||
|
class TopDefinition:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
resolver: DataResolver,
|
||||||
|
context: Context,
|
||||||
|
name: str,
|
||||||
|
definition: "Any",
|
||||||
|
where: str,
|
||||||
|
):
|
||||||
|
self.resolver = resolver
|
||||||
|
self.context = context
|
||||||
|
self.name = name
|
||||||
|
self.definition = definition
|
||||||
|
self.where = where
|
||||||
|
self.relpath = self.resolver.relpath
|
||||||
|
|
||||||
|
def resolve(self):
|
||||||
|
try:
|
||||||
|
check_recursive_parse_errors(self.definition)
|
||||||
|
return self.context.resolve(self.definition)
|
||||||
|
except (ParseError, ContextError) as exc:
|
||||||
|
format_and_raise(exc, f"'{self.where}.{self.name}'", self.relpath)
|
||||||
|
|
||||||
|
|
||||||
|
class ArtifactDefinition(TopDefinition):
|
||||||
|
def resolve(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||||
|
try:
|
||||||
|
check_expression(self.name)
|
||||||
|
name = self.context.resolve(self.name)
|
||||||
|
if not isinstance(name, str):
|
||||||
|
typ = type(name).__name__
|
||||||
|
raise ResolveError(
|
||||||
|
f"failed to resolve '{self.where}.{self.name}'"
|
||||||
|
f" in '{self.relpath}': expected str, got " + typ
|
||||||
|
)
|
||||||
|
except (ParseError, ContextError) as exc:
|
||||||
|
format_and_raise(exc, f"'{self.where}.{self.name}'", self.relpath)
|
||||||
|
return {name: super().resolve()}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class ParamsLoadError(ContextError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class KeyNotInContext(ContextError, KeyError):
|
class KeyNotInContext(ContextError, KeyError): # noqa: N818
|
||||||
def __init__(self, key: str) -> None:
|
def __init__(self, key: str) -> None:
|
||||||
self.key: str = key
|
self.key: str = key
|
||||||
super().__init__(f"Could not find '{key}'")
|
super().__init__(f"Could not find '{key}'")
|
||||||
@@ -75,7 +75,7 @@ class KeyNotInContext(ContextError, KeyError):
|
|||||||
return self.msg
|
return self.msg
|
||||||
|
|
||||||
|
|
||||||
class VarsAlreadyLoaded(ContextError):
|
class VarsAlreadyLoaded(ContextError): # noqa: N818
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class Meta:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_path(meta: "Meta", path: Union[str, int]):
|
def update_path(meta: "Meta", path: Union[str, int]):
|
||||||
dpaths = meta.dpaths[:] + [str(path)]
|
dpaths = [*meta.dpaths, str(path)]
|
||||||
return replace(meta, dpaths=dpaths)
|
return replace(meta, dpaths=dpaths)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -155,7 +155,7 @@ class Value(Node):
|
|||||||
PRIMITIVES = (int, float, str, bytes, bool)
|
PRIMITIVES = (int, float, str, bytes, bool)
|
||||||
|
|
||||||
|
|
||||||
class Container(Node, ABC):
|
class Container(Node, ABC): # noqa: PLW1641
|
||||||
meta: Meta
|
meta: Meta
|
||||||
data: Union[list, dict]
|
data: Union[list, dict]
|
||||||
_key_transform = staticmethod(identity)
|
_key_transform = staticmethod(identity)
|
||||||
@@ -176,8 +176,9 @@ class Container(Node, ABC):
|
|||||||
return value
|
return value
|
||||||
if isinstance(value, (list, dict)):
|
if isinstance(value, (list, dict)):
|
||||||
assert meta
|
assert meta
|
||||||
container = CtxDict if isinstance(value, dict) else CtxList
|
if isinstance(value, dict):
|
||||||
return container(value, meta=meta)
|
return CtxDict(value, meta=meta)
|
||||||
|
return CtxList(value, meta=meta)
|
||||||
msg = f"Unsupported value of type '{type(value).__name__}' in '{meta}'"
|
msg = f"Unsupported value of type '{type(value).__name__}' in '{meta}'"
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
@@ -271,7 +272,7 @@ class CtxDict(Container, MutableMapping):
|
|||||||
if not isinstance(key, str):
|
if not isinstance(key, str):
|
||||||
# limitation for the interpolation
|
# limitation for the interpolation
|
||||||
# ignore other kinds of keys
|
# ignore other kinds of keys
|
||||||
return
|
return None
|
||||||
return super().__setitem__(key, value)
|
return super().__setitem__(key, value)
|
||||||
|
|
||||||
def merge_update(self, other, overwrite=False):
|
def merge_update(self, other, overwrite=False):
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ KEYCRE = re.compile(
|
|||||||
def get_parser():
|
def get_parser():
|
||||||
from pyparsing import CharsNotIn, ParserElement, Suppress, ZeroOrMore
|
from pyparsing import CharsNotIn, ParserElement, Suppress, ZeroOrMore
|
||||||
|
|
||||||
ParserElement.enablePackrat()
|
ParserElement.enable_packrat()
|
||||||
|
|
||||||
word = CharsNotIn(f"{PERIOD}{LBRACK}{RBRACK}")
|
word = CharsNotIn(f"{PERIOD}{LBRACK}{RBRACK}")
|
||||||
idx = Suppress(LBRACK) + word + Suppress(RBRACK)
|
idx = Suppress(LBRACK) + word + Suppress(RBRACK)
|
||||||
attr = Suppress(PERIOD) + word
|
attr = Suppress(PERIOD) + word
|
||||||
parser = word + ZeroOrMore(attr ^ idx)
|
parser = word + ZeroOrMore(attr ^ idx)
|
||||||
parser.setParseAction(PERIOD.join)
|
parser.set_parse_action(PERIOD.join)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ def parse_expr(s: str):
|
|||||||
from pyparsing import ParseException
|
from pyparsing import ParseException
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = get_parser().parseString(s, parseAll=True)
|
result = get_parser().parse_string(s, parse_all=True)
|
||||||
except ParseException as exc:
|
except ParseException as exc:
|
||||||
format_and_raise_parse_error(exc)
|
format_and_raise_parse_error(exc)
|
||||||
raise AssertionError("unreachable") # noqa: B904
|
raise AssertionError("unreachable") # noqa: B904
|
||||||
@@ -202,6 +202,7 @@ def validate_value(value, key):
|
|||||||
if isinstance(value, dict) and key == "cmd":
|
if isinstance(value, dict) and key == "cmd":
|
||||||
return True
|
return True
|
||||||
raise ParseError(f"Cannot interpolate data of type '{type(value).__name__}'")
|
raise ParseError(f"Cannot interpolate data of type '{type(value).__name__}'")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def str_interpolate(
|
def str_interpolate(
|
||||||
|
|||||||
+10
-6
@@ -2,13 +2,19 @@
|
|||||||
# Including changing base dir of path specification patterns and merging
|
# Including changing base dir of path specification patterns and merging
|
||||||
# of two path specification patterns with different base
|
# of two path specification patterns with different base
|
||||||
# All the operations follow the documents of `gitignore`
|
# All the operations follow the documents of `gitignore`
|
||||||
from collections import namedtuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
from pathspec.util import normalize_file
|
from pathspec.util import normalize_file
|
||||||
|
|
||||||
from dvc.utils import relpath
|
from dvc.utils import relpath
|
||||||
|
|
||||||
PatternInfo = namedtuple("PatternInfo", ["patterns", "file_info"]) # noqa: PYI024
|
|
||||||
|
class PatternInfo(NamedTuple):
|
||||||
|
patterns: str
|
||||||
|
file_info: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.file_info or f":{self.patterns}"
|
||||||
|
|
||||||
|
|
||||||
def _not_ignore(rule):
|
def _not_ignore(rule):
|
||||||
@@ -27,11 +33,9 @@ def _remove_slash(rule):
|
|||||||
|
|
||||||
def _match_all_level(rule):
|
def _match_all_level(rule):
|
||||||
if rule[:-1].find("/") >= 0 and not rule.startswith("**/"):
|
if rule[:-1].find("/") >= 0 and not rule.startswith("**/"):
|
||||||
if rule.startswith("/"):
|
rule = rule.removeprefix("/")
|
||||||
rule = rule[1:]
|
|
||||||
return False, rule
|
return False, rule
|
||||||
if rule.startswith("**/"):
|
rule = rule.removeprefix("**/")
|
||||||
rule = rule[3:]
|
|
||||||
return True, rule
|
return True, rule
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -1,4 +1,5 @@
|
|||||||
"""Manages progress bars for DVC repo."""
|
"""Manages progress bars for DVC repo."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
@@ -40,7 +41,7 @@ class Tqdm(tqdm):
|
|||||||
"miniters": 1,
|
"miniters": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__( # noqa: PLR0913
|
def __init__(
|
||||||
self,
|
self,
|
||||||
iterable=None,
|
iterable=None,
|
||||||
disable=None,
|
disable=None,
|
||||||
@@ -68,7 +69,7 @@ class Tqdm(tqdm):
|
|||||||
"""
|
"""
|
||||||
kwargs = kwargs.copy()
|
kwargs = kwargs.copy()
|
||||||
if bytes:
|
if bytes:
|
||||||
kwargs = {**self.BYTES_DEFAULTS, **kwargs}
|
kwargs = self.BYTES_DEFAULTS | kwargs
|
||||||
else:
|
else:
|
||||||
kwargs.setdefault("unit_scale", total > 999 if total else True)
|
kwargs.setdefault("unit_scale", total > 999 if total else True)
|
||||||
if file is None:
|
if file is None:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def _lists(blob: Union[dict, list]) -> Iterable[list]:
|
|||||||
if isinstance(blob, list):
|
if isinstance(blob, list):
|
||||||
yield blob
|
yield blob
|
||||||
else:
|
else:
|
||||||
for _, value in blob.items():
|
for value in blob.values():
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
yield from _lists(value)
|
yield from _lists(value)
|
||||||
elif isinstance(value, list):
|
elif isinstance(value, list):
|
||||||
@@ -167,7 +167,7 @@ class VegaConverter(Converter):
|
|||||||
if isinstance(y, list):
|
if isinstance(y, list):
|
||||||
return "y"
|
return "y"
|
||||||
if not isinstance(y, dict):
|
if not isinstance(y, dict):
|
||||||
return
|
return None
|
||||||
|
|
||||||
fields = {field for _, field in _file_field(y)}
|
fields = {field for _, field in _file_field(y)}
|
||||||
if len(fields) == 1:
|
if len(fields) == 1:
|
||||||
@@ -285,7 +285,7 @@ class VegaConverter(Converter):
|
|||||||
if not all_datapoints:
|
if not all_datapoints:
|
||||||
return [], {}
|
return [], {}
|
||||||
|
|
||||||
properties = {**properties, **props_update}
|
properties = properties | props_update
|
||||||
|
|
||||||
return all_datapoints, properties
|
return all_datapoints, properties
|
||||||
|
|
||||||
@@ -298,7 +298,7 @@ class VegaConverter(Converter):
|
|||||||
inferred_properties = self._infer_x_y()
|
inferred_properties = self._infer_x_y()
|
||||||
|
|
||||||
datapoints = self._find_datapoints()
|
datapoints = self._find_datapoints()
|
||||||
properties = {**self.properties, **inferred_properties}
|
properties = self.properties | inferred_properties
|
||||||
|
|
||||||
properties["y_label"] = self.infer_y_label(properties)
|
properties["y_label"] = self.infer_y_label(properties)
|
||||||
properties["x_label"] = self.infer_x_label(properties)
|
properties["x_label"] = self.infer_x_label(properties)
|
||||||
|
|||||||
+1
-2
@@ -2,7 +2,6 @@ import os
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import TYPE_CHECKING, NamedTuple, Optional
|
from typing import TYPE_CHECKING, NamedTuple, Optional
|
||||||
|
|
||||||
import dpath
|
|
||||||
import dpath.options
|
import dpath.options
|
||||||
from funcy import get_in, last
|
from funcy import get_in, last
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ def _squash_plots_properties(data: list) -> dict:
|
|||||||
configs = [last(group) for group in data]
|
configs = [last(group) for group in data]
|
||||||
resolved: dict = {}
|
resolved: dict = {}
|
||||||
for config in reversed(configs):
|
for config in reversed(configs):
|
||||||
resolved = {**resolved, **config}
|
resolved = resolved | config
|
||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+27
-16
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
|
|||||||
from dvc.fs.data import DataFileSystem
|
from dvc.fs.data import DataFileSystem
|
||||||
from dvc.fs.dvc import DVCFileSystem
|
from dvc.fs.dvc import DVCFileSystem
|
||||||
from dvc.lock import LockBase
|
from dvc.lock import LockBase
|
||||||
|
from dvc.output import Output
|
||||||
from dvc.scm import Git, NoSCM
|
from dvc.scm import Git, NoSCM
|
||||||
from dvc.stage import Stage
|
from dvc.stage import Stage
|
||||||
from dvc.types import DictStrAny
|
from dvc.types import DictStrAny
|
||||||
@@ -144,12 +145,14 @@ class Repo:
|
|||||||
scm: Optional[Union["Git", "NoSCM"]] = None,
|
scm: Optional[Union["Git", "NoSCM"]] = None,
|
||||||
remote: Optional[str] = None,
|
remote: Optional[str] = None,
|
||||||
remote_config: Optional["DictStrAny"] = None,
|
remote_config: Optional["DictStrAny"] = None,
|
||||||
|
_wait_for_lock: bool = False,
|
||||||
):
|
):
|
||||||
from dvc.cachemgr import CacheManager
|
from dvc.cachemgr import CacheManager
|
||||||
from dvc.data_cloud import DataCloud
|
from dvc.data_cloud import DataCloud
|
||||||
from dvc.fs import GitFileSystem, LocalFileSystem, localfs
|
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||||
from dvc.lock import LockNoop, make_lock
|
from dvc.lock import LockNoop, make_lock
|
||||||
from dvc.repo.artifacts import Artifacts
|
from dvc.repo.artifacts import Artifacts
|
||||||
|
from dvc.repo.datasets import Datasets
|
||||||
from dvc.repo.metrics import Metrics
|
from dvc.repo.metrics import Metrics
|
||||||
from dvc.repo.params import Params
|
from dvc.repo.params import Params
|
||||||
from dvc.repo.plots import Plots
|
from dvc.repo.plots import Plots
|
||||||
@@ -160,12 +163,13 @@ class Repo:
|
|||||||
|
|
||||||
self.url = url
|
self.url = url
|
||||||
self._fs_conf = {"repo_factory": repo_factory}
|
self._fs_conf = {"repo_factory": repo_factory}
|
||||||
self._fs = fs or localfs
|
self._fs = fs or LocalFileSystem()
|
||||||
self._scm = scm
|
self._scm = scm
|
||||||
self._config = config
|
self._config = config
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self._remote_config = remote_config
|
self._remote_config = remote_config
|
||||||
self._data_index = None
|
self._data_index: Optional[DataIndex] = None
|
||||||
|
self._wait_for_lock = _wait_for_lock
|
||||||
|
|
||||||
if rev and not fs:
|
if rev and not fs:
|
||||||
self._scm = scm = SCM(root_dir or os.curdir)
|
self._scm = scm = SCM(root_dir or os.curdir)
|
||||||
@@ -183,12 +187,12 @@ class Repo:
|
|||||||
# used by DVCFileSystem to determine if it should traverse subrepos
|
# used by DVCFileSystem to determine if it should traverse subrepos
|
||||||
self.subrepos = subrepos
|
self.subrepos = subrepos
|
||||||
|
|
||||||
self.cloud: "DataCloud" = DataCloud(self)
|
self.cloud: DataCloud = DataCloud(self)
|
||||||
self.stage: "StageLoad" = StageLoad(self)
|
self.stage: StageLoad = StageLoad(self)
|
||||||
|
|
||||||
self.lock: "LockBase"
|
self.lock: LockBase
|
||||||
self.cache: CacheManager
|
self.cache: CacheManager
|
||||||
self.state: "StateBase"
|
self.state: StateBase
|
||||||
if isinstance(self.fs, GitFileSystem) or not self.dvc_dir:
|
if isinstance(self.fs, GitFileSystem) or not self.dvc_dir:
|
||||||
self.lock = LockNoop()
|
self.lock = LockNoop()
|
||||||
self.state = StateNoop()
|
self.state = StateNoop()
|
||||||
@@ -203,8 +207,14 @@ class Repo:
|
|||||||
tmp_dir=self.tmp_dir,
|
tmp_dir=self.tmp_dir,
|
||||||
hardlink_lock=self.config["core"].get("hardlink_lock", False),
|
hardlink_lock=self.config["core"].get("hardlink_lock", False),
|
||||||
friendly=True,
|
friendly=True,
|
||||||
|
wait=self._wait_for_lock,
|
||||||
)
|
)
|
||||||
os.makedirs(self.site_cache_dir, exist_ok=True)
|
os.makedirs(self.site_cache_dir, exist_ok=True)
|
||||||
|
if not fs and (
|
||||||
|
checksum_jobs := self.config["core"].get("checksum_jobs")
|
||||||
|
):
|
||||||
|
self.fs.hash_jobs = checksum_jobs
|
||||||
|
|
||||||
self.state = State(self.root_dir, self.site_cache_dir, self.dvcignore)
|
self.state = State(self.root_dir, self.site_cache_dir, self.dvcignore)
|
||||||
else:
|
else:
|
||||||
self.lock = LockNoop()
|
self.lock = LockNoop()
|
||||||
@@ -220,6 +230,7 @@ class Repo:
|
|||||||
self.plots: Plots = Plots(self)
|
self.plots: Plots = Plots(self)
|
||||||
self.params: Params = Params(self)
|
self.params: Params = Params(self)
|
||||||
self.artifacts: Artifacts = Artifacts(self)
|
self.artifacts: Artifacts = Artifacts(self)
|
||||||
|
self.datasets: Datasets = Datasets(self)
|
||||||
|
|
||||||
self.stage_collection_error_handler: Optional[
|
self.stage_collection_error_handler: Optional[
|
||||||
Callable[[str, Exception], None]
|
Callable[[str, Exception], None]
|
||||||
@@ -534,7 +545,9 @@ class Repo:
|
|||||||
|
|
||||||
return used
|
return used
|
||||||
|
|
||||||
def find_outs_by_path(self, path, outs=None, recursive=False, strict=True):
|
def find_outs_by_path(
|
||||||
|
self, path, outs=None, recursive=False, strict=True
|
||||||
|
) -> list["Output"]:
|
||||||
# using `outs_graph` to ensure graph checks are run
|
# using `outs_graph` to ensure graph checks are run
|
||||||
outs = outs or self.index.outs_graph
|
outs = outs or self.index.outs_graph
|
||||||
|
|
||||||
@@ -549,11 +562,7 @@ class Repo:
|
|||||||
|
|
||||||
if out.protocol == "local" and match(fs_path, out.fs_path):
|
if out.protocol == "local" and match(fs_path, out.fs_path):
|
||||||
return True
|
return True
|
||||||
|
return recursive and out.fs.isin(out.fs_path, fs_path)
|
||||||
if recursive and out.fs.isin(out.fs_path, fs_path):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
matched = list(filter(func, outs))
|
matched = list(filter(func, outs))
|
||||||
if not matched:
|
if not matched:
|
||||||
@@ -606,7 +615,7 @@ class Repo:
|
|||||||
from dvc.fs import GitFileSystem
|
from dvc.fs import GitFileSystem
|
||||||
from dvc.version import version_tuple
|
from dvc.version import version_tuple
|
||||||
|
|
||||||
cache_dir = self.config["core"].get("site_cache_dir") or site_cache_dir()
|
cache_dir = site_cache_dir(self.config["core"].get("site_cache_dir"))
|
||||||
|
|
||||||
subdir = None
|
subdir = None
|
||||||
if isinstance(self.fs, GitFileSystem):
|
if isinstance(self.fs, GitFileSystem):
|
||||||
@@ -635,10 +644,11 @@ class Repo:
|
|||||||
# that just happened to be at the same path as old deleted ones.
|
# that just happened to be at the same path as old deleted ones.
|
||||||
btime = self._btime or getattr(os.stat(root_dir), "st_birthtime", None)
|
btime = self._btime or getattr(os.stat(root_dir), "st_birthtime", None)
|
||||||
|
|
||||||
md5 = hashlib.md5( # noqa: S324
|
md5 = hashlib.md5(
|
||||||
str(
|
str(
|
||||||
(root_dir, subdir, btime, getpass.getuser(), version_tuple[0], salt)
|
(root_dir, subdir, btime, getpass.getuser(), version_tuple[0], salt)
|
||||||
).encode()
|
).encode(),
|
||||||
|
usedforsecurity=False,
|
||||||
)
|
)
|
||||||
repo_token = md5.hexdigest()
|
repo_token = md5.hexdigest()
|
||||||
return os.path.join(repos_dir, repo_token)
|
return os.path.join(repos_dir, repo_token)
|
||||||
@@ -653,6 +663,7 @@ class Repo:
|
|||||||
|
|
||||||
def _reset(self):
|
def _reset(self):
|
||||||
self.scm._reset()
|
self.scm._reset()
|
||||||
|
self.datasets._reset()
|
||||||
self.state.close()
|
self.state.close()
|
||||||
if "dvcfs" in self.__dict__:
|
if "dvcfs" in self.__dict__:
|
||||||
self.dvcfs.close()
|
self.dvcfs.close()
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user