asanypath 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {asanypath-2.2.0 → asanypath-2.3.0}/PKG-INFO +59 -12
- {asanypath-2.2.0 → asanypath-2.3.0}/README.md +57 -6
- {asanypath-2.2.0 → asanypath-2.3.0}/pyproject.toml +1 -6
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/__init__.py +2 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/__init__.pyi +1 -0
- asanypath-2.3.0/src/asanypath/_backend_opts.py +61 -0
- asanypath-2.3.0/src/asanypath/_transfer.py +425 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/_version.py +2 -2
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/artifactory.py +5 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/azure.py +7 -1
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/cache.py +16 -20
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/cli.py +133 -32
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/cloud.py +48 -11
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/gcs.py +6 -1
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/http.py +49 -1
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/local.py +17 -8
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/s3.py +13 -1
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/ssh.py +1 -0
- asanypath-2.3.0/tests/test_backend_options.py +109 -0
- asanypath-2.3.0/tests/test_cache.py +497 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_cli.py +135 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_cloudpathmixin.py +346 -4
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_ftp.py +19 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_http.py +93 -1
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_s3.py +20 -1
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_ssh.py +29 -0
- asanypath-2.2.0/src/asanypath/_transfer.py +0 -224
- asanypath-2.2.0/tests/test_cache.py +0 -242
- {asanypath-2.2.0 → asanypath-2.3.0}/.copilot/skills/async-python-lib/SKILL.md +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.copilot/skills/gitguro/SKILL.md +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.github/CODEOWNERS +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.github/dependabot.yml +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.github/workflows/publish.yml +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.github/workflows/unittests.yml +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.gitignore +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/.pre-commit-config.yaml +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/CODE_OF_CONDUCT.md +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/CONTRIBUTING.md +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/LICENSE +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/SECURITY.md +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/asanypath.png +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/compose.yml +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/hatch_build.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/scripts/bench.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/_credstore.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/_resume.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/asanypath.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/asanypath.pyi +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/batcher.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/common.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/exceptions.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/ftp.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/options.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/py.typed +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/sync.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/src/asanypath/unsupported.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/__init__.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/conftest.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_artifactory.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_asanypath.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_azure.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_batcher.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_commonpathmixin.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_credstore.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_gcs.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_http_auth.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_local.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_options.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_py_typed.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_resume.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_sync.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_syncpath.py +0 -0
- {asanypath-2.2.0 → asanypath-2.3.0}/tests/test_unsupported_path.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: asanypath
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Async pathlib for local and cloud storage
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -19,12 +19,11 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Requires-Dist: anyio>=4
|
|
22
|
-
Requires-Dist: asanypath-native~=2.
|
|
22
|
+
Requires-Dist: asanypath-native~=2.3.0
|
|
23
23
|
Requires-Dist: msgspec>=0.22.0
|
|
24
24
|
Requires-Dist: yarl>=1.2
|
|
25
25
|
Provides-Extra: all
|
|
26
26
|
Requires-Dist: aioftp>=0.21; extra == 'all'
|
|
27
|
-
Requires-Dist: diskcache>=5.6; extra == 'all'
|
|
28
27
|
Requires-Dist: keyring>=24; extra == 'all'
|
|
29
28
|
Provides-Extra: bench
|
|
30
29
|
Requires-Dist: adlfs; extra == 'bench'
|
|
@@ -38,15 +37,12 @@ Requires-Dist: gcloud-aio-storage; extra == 'bench'
|
|
|
38
37
|
Requires-Dist: gcsfs; extra == 'bench'
|
|
39
38
|
Requires-Dist: requests; extra == 'bench'
|
|
40
39
|
Requires-Dist: s3fs; extra == 'bench'
|
|
41
|
-
Provides-Extra: cache
|
|
42
|
-
Requires-Dist: diskcache>=5.6; extra == 'cache'
|
|
43
40
|
Provides-Extra: cli
|
|
44
41
|
Requires-Dist: rich-click>=1.9.7; extra == 'cli'
|
|
45
42
|
Provides-Extra: credstore
|
|
46
43
|
Requires-Dist: keyring>=24; extra == 'credstore'
|
|
47
44
|
Provides-Extra: dev
|
|
48
45
|
Requires-Dist: aioftp>=0.21; extra == 'dev'
|
|
49
|
-
Requires-Dist: diskcache>=5.6; extra == 'dev'
|
|
50
46
|
Requires-Dist: keyring>=24; extra == 'dev'
|
|
51
47
|
Requires-Dist: pre-commit>=4; extra == 'dev'
|
|
52
48
|
Requires-Dist: pytest-asyncio>=1.3; extra == 'dev'
|
|
@@ -149,6 +145,46 @@ await src.copy(dst, recursive=True)
|
|
|
149
145
|
await path.rmdir(recursive=True)
|
|
150
146
|
```
|
|
151
147
|
|
|
148
|
+
Copies automatically use spare transfer concurrency for large ranged reads.
|
|
149
|
+
The default target is 8 streams, with an 8 MiB minimum range size and at most
|
|
150
|
+
8 range streams per file. Files smaller than 16 MiB stay single-stream. When
|
|
151
|
+
enough files are active to meet the target, automatic copies use one request
|
|
152
|
+
per file instead of adding ranges. The target is best-effort, not a minimum
|
|
153
|
+
number of requests that must always be launched.
|
|
154
|
+
|
|
155
|
+
S3, GCS, Azure, Artifactory, SSH, and range-capable HTTP/HTTPS sources use the
|
|
156
|
+
shared reader for local downloads and source reads in cross-backend copies.
|
|
157
|
+
FTP/FTPS, sources without size metadata, and HTTP servers without advertised
|
|
158
|
+
range support retain single-stream downloads. Local-only copies and native
|
|
159
|
+
server-side copy/rename operations retain their existing implementations.
|
|
160
|
+
Uploads remain single writes; this does not introduce multipart uploads.
|
|
161
|
+
|
|
162
|
+
Concurrent library copies share a default budget within the event loop. To
|
|
163
|
+
configure a batch explicitly:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
import asyncio
|
|
167
|
+
from asanypath import TransferBudget
|
|
168
|
+
|
|
169
|
+
with TransferBudget(floor=8, ceiling=32, min_range_size=8 * 1024 * 1024, per_file_cap=8):
|
|
170
|
+
await asyncio.gather(*(src.copy(dst) for src, dst in pairs))
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The ceiling bounds concurrent reads in this budget, not metadata requests or
|
|
174
|
+
other processes. Parallel range buffers are bounded by the active streams
|
|
175
|
+
times the range size; cross-backend destinations still buffer the complete
|
|
176
|
+
object for their existing write API. `floor=1` disables automatic splitting.
|
|
177
|
+
Explicit `chunk_size` requests retain sequential streaming when splitting
|
|
178
|
+
would be inappropriate, and `chunk_size=0` disables streaming to local paths.
|
|
179
|
+
|
|
180
|
+
S3/Azure ETags, GCS generations, and HTTP conditional requests pin the source
|
|
181
|
+
version when available. Other sources are checked against fresh metadata
|
|
182
|
+
after the download, which cannot guarantee a snapshot if the backend does
|
|
183
|
+
not expose a reliable change token. Use `atomic=True` to keep an existing
|
|
184
|
+
local destination intact on failure. Moves remove the source only after a
|
|
185
|
+
successful copy. The updated Python package requires the matching native
|
|
186
|
+
extension for conditional cloud reads.
|
|
187
|
+
|
|
152
188
|
For synchronous code (no `await` needed):
|
|
153
189
|
|
|
154
190
|
```python
|
|
@@ -219,9 +255,8 @@ Reading the same object or URL twice always re-downloads it — there is **no
|
|
|
219
255
|
cache by default**, and the read path is unchanged unless you opt in, so
|
|
220
256
|
non-caching callers pay zero overhead.
|
|
221
257
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
zero-dependency store). Opt in per read with `use_cache=True`:
|
|
258
|
+
Caching is included in the native extension with no extra dependencies or
|
|
259
|
+
installation extras. Opt in per read with `use_cache=True`:
|
|
225
260
|
|
|
226
261
|
```python
|
|
227
262
|
from asanypath import AsAnyPath
|
|
@@ -240,7 +275,7 @@ from asanypath import configure_cache
|
|
|
240
275
|
|
|
241
276
|
configure_cache(
|
|
242
277
|
directory="/var/cache/asanypath", # default: ~/.cache/asanypath
|
|
243
|
-
size_limit=2 * 1024**3, #
|
|
278
|
+
size_limit=2 * 1024**3, # stored key + payload budget (default 1 GiB, LRU)
|
|
244
279
|
ttl=3600, # per-entry expiry in seconds (default: none)
|
|
245
280
|
enabled=True, # make use_cache default to True process-wide
|
|
246
281
|
)
|
|
@@ -255,9 +290,15 @@ Behaviour and trade-offs:
|
|
|
255
290
|
default flips process-wide; an explicit `use_cache=False` always bypasses.
|
|
256
291
|
- **Works for every remote backend** (S3, GCS, Azure, Artifactory, HTTP, SSH,
|
|
257
292
|
FTP). Local paths are never cached.
|
|
258
|
-
- **
|
|
293
|
+
- **Size-bounded, LRU-evicted;** disk I/O runs in a worker thread so the
|
|
259
294
|
event loop stays responsive. Concurrent reads of the same key collapse to a
|
|
260
|
-
single fetch.
|
|
295
|
+
single fetch within a cache instance. The byte budget counts UTF-8 keys and
|
|
296
|
+
payloads; SQLite metadata and transaction journals add filesystem overhead.
|
|
297
|
+
Objects larger than the budget are returned without being cached.
|
|
298
|
+
- **Byte-only native storage:** SQLite stores raw bytes, never Python pickles.
|
|
299
|
+
The database lives under `native-v1/` in the configured cache directory.
|
|
300
|
+
Legacy DiskCache files are ignored, not migrated or deleted. Multiple
|
|
301
|
+
processes can share the new store; use the same size limit across them.
|
|
261
302
|
- **Staleness is on you.** Entries are keyed by URL with no revalidation — a
|
|
262
303
|
cached object that changes upstream is served stale until it expires (`ttl`)
|
|
263
304
|
or is evicted. Prefer caching immutable/versioned objects, or set a `ttl`.
|
|
@@ -356,6 +397,12 @@ asanypath cp /local/file.txt s3://bucket/file.txt
|
|
|
356
397
|
# Copy cloud → local
|
|
357
398
|
asanypath cp s3://bucket/file.txt /local/file.txt
|
|
358
399
|
|
|
400
|
+
# Allow 32 concurrent files; fill spare capacity toward 8 streams for large files
|
|
401
|
+
asanypath cp -r s3://bucket/data/ ./data/ -j 32 --concurrency-floor 8
|
|
402
|
+
|
|
403
|
+
# Disable automatic range splitting (also supported by mv and sync)
|
|
404
|
+
asanypath cp s3://bucket/file.txt ./file.txt --concurrency-floor 1
|
|
405
|
+
|
|
359
406
|
# Delete a file
|
|
360
407
|
asanypath rm s3://bucket/file.txt --force
|
|
361
408
|
|
|
@@ -90,6 +90,46 @@ await src.copy(dst, recursive=True)
|
|
|
90
90
|
await path.rmdir(recursive=True)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
Copies automatically use spare transfer concurrency for large ranged reads.
|
|
94
|
+
The default target is 8 streams, with an 8 MiB minimum range size and at most
|
|
95
|
+
8 range streams per file. Files smaller than 16 MiB stay single-stream. When
|
|
96
|
+
enough files are active to meet the target, automatic copies use one request
|
|
97
|
+
per file instead of adding ranges. The target is best-effort, not a minimum
|
|
98
|
+
number of requests that must always be launched.
|
|
99
|
+
|
|
100
|
+
S3, GCS, Azure, Artifactory, SSH, and range-capable HTTP/HTTPS sources use the
|
|
101
|
+
shared reader for local downloads and source reads in cross-backend copies.
|
|
102
|
+
FTP/FTPS, sources without size metadata, and HTTP servers without advertised
|
|
103
|
+
range support retain single-stream downloads. Local-only copies and native
|
|
104
|
+
server-side copy/rename operations retain their existing implementations.
|
|
105
|
+
Uploads remain single writes; this does not introduce multipart uploads.
|
|
106
|
+
|
|
107
|
+
Concurrent library copies share a default budget within the event loop. To
|
|
108
|
+
configure a batch explicitly:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import asyncio
|
|
112
|
+
from asanypath import TransferBudget
|
|
113
|
+
|
|
114
|
+
with TransferBudget(floor=8, ceiling=32, min_range_size=8 * 1024 * 1024, per_file_cap=8):
|
|
115
|
+
await asyncio.gather(*(src.copy(dst) for src, dst in pairs))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The ceiling bounds concurrent reads in this budget, not metadata requests or
|
|
119
|
+
other processes. Parallel range buffers are bounded by the active streams
|
|
120
|
+
times the range size; cross-backend destinations still buffer the complete
|
|
121
|
+
object for their existing write API. `floor=1` disables automatic splitting.
|
|
122
|
+
Explicit `chunk_size` requests retain sequential streaming when splitting
|
|
123
|
+
would be inappropriate, and `chunk_size=0` disables streaming to local paths.
|
|
124
|
+
|
|
125
|
+
S3/Azure ETags, GCS generations, and HTTP conditional requests pin the source
|
|
126
|
+
version when available. Other sources are checked against fresh metadata
|
|
127
|
+
after the download, which cannot guarantee a snapshot if the backend does
|
|
128
|
+
not expose a reliable change token. Use `atomic=True` to keep an existing
|
|
129
|
+
local destination intact on failure. Moves remove the source only after a
|
|
130
|
+
successful copy. The updated Python package requires the matching native
|
|
131
|
+
extension for conditional cloud reads.
|
|
132
|
+
|
|
93
133
|
For synchronous code (no `await` needed):
|
|
94
134
|
|
|
95
135
|
```python
|
|
@@ -160,9 +200,8 @@ Reading the same object or URL twice always re-downloads it — there is **no
|
|
|
160
200
|
cache by default**, and the read path is unchanged unless you opt in, so
|
|
161
201
|
non-caching callers pay zero overhead.
|
|
162
202
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
zero-dependency store). Opt in per read with `use_cache=True`:
|
|
203
|
+
Caching is included in the native extension with no extra dependencies or
|
|
204
|
+
installation extras. Opt in per read with `use_cache=True`:
|
|
166
205
|
|
|
167
206
|
```python
|
|
168
207
|
from asanypath import AsAnyPath
|
|
@@ -181,7 +220,7 @@ from asanypath import configure_cache
|
|
|
181
220
|
|
|
182
221
|
configure_cache(
|
|
183
222
|
directory="/var/cache/asanypath", # default: ~/.cache/asanypath
|
|
184
|
-
size_limit=2 * 1024**3, #
|
|
223
|
+
size_limit=2 * 1024**3, # stored key + payload budget (default 1 GiB, LRU)
|
|
185
224
|
ttl=3600, # per-entry expiry in seconds (default: none)
|
|
186
225
|
enabled=True, # make use_cache default to True process-wide
|
|
187
226
|
)
|
|
@@ -196,9 +235,15 @@ Behaviour and trade-offs:
|
|
|
196
235
|
default flips process-wide; an explicit `use_cache=False` always bypasses.
|
|
197
236
|
- **Works for every remote backend** (S3, GCS, Azure, Artifactory, HTTP, SSH,
|
|
198
237
|
FTP). Local paths are never cached.
|
|
199
|
-
- **
|
|
238
|
+
- **Size-bounded, LRU-evicted;** disk I/O runs in a worker thread so the
|
|
200
239
|
event loop stays responsive. Concurrent reads of the same key collapse to a
|
|
201
|
-
single fetch.
|
|
240
|
+
single fetch within a cache instance. The byte budget counts UTF-8 keys and
|
|
241
|
+
payloads; SQLite metadata and transaction journals add filesystem overhead.
|
|
242
|
+
Objects larger than the budget are returned without being cached.
|
|
243
|
+
- **Byte-only native storage:** SQLite stores raw bytes, never Python pickles.
|
|
244
|
+
The database lives under `native-v1/` in the configured cache directory.
|
|
245
|
+
Legacy DiskCache files are ignored, not migrated or deleted. Multiple
|
|
246
|
+
processes can share the new store; use the same size limit across them.
|
|
202
247
|
- **Staleness is on you.** Entries are keyed by URL with no revalidation — a
|
|
203
248
|
cached object that changes upstream is served stale until it expires (`ttl`)
|
|
204
249
|
or is evicted. Prefer caching immutable/versioned objects, or set a `ttl`.
|
|
@@ -297,6 +342,12 @@ asanypath cp /local/file.txt s3://bucket/file.txt
|
|
|
297
342
|
# Copy cloud → local
|
|
298
343
|
asanypath cp s3://bucket/file.txt /local/file.txt
|
|
299
344
|
|
|
345
|
+
# Allow 32 concurrent files; fill spare capacity toward 8 streams for large files
|
|
346
|
+
asanypath cp -r s3://bucket/data/ ./data/ -j 32 --concurrency-floor 8
|
|
347
|
+
|
|
348
|
+
# Disable automatic range splitting (also supported by mv and sync)
|
|
349
|
+
asanypath cp s3://bucket/file.txt ./file.txt --concurrency-floor 1
|
|
350
|
+
|
|
300
351
|
# Delete a file
|
|
301
352
|
asanypath rm s3://bucket/file.txt --force
|
|
302
353
|
|
|
@@ -58,7 +58,6 @@ dev = [
|
|
|
58
58
|
cli = ["rich-click>=1.9.7"]
|
|
59
59
|
ftp = ["aioftp>=0.21"]
|
|
60
60
|
credstore = ["keyring>=24"]
|
|
61
|
-
cache = ["diskcache>=5.6"]
|
|
62
61
|
bench = [
|
|
63
62
|
"asyncssh>=2.14",
|
|
64
63
|
"fsspec",
|
|
@@ -72,7 +71,7 @@ bench = [
|
|
|
72
71
|
"aiofiles",
|
|
73
72
|
"requests",
|
|
74
73
|
]
|
|
75
|
-
all = ["asanypath[credstore,ftp
|
|
74
|
+
all = ["asanypath[credstore,ftp]"]
|
|
76
75
|
[project.scripts]
|
|
77
76
|
asanypath = "asanypath.cli:main"
|
|
78
77
|
|
|
@@ -151,10 +150,6 @@ disable_error_code = ["no-any-return", "misc", "union-attr", "type-var", "attr-d
|
|
|
151
150
|
module = "asanypath.batcher"
|
|
152
151
|
disallow_untyped_defs = false
|
|
153
152
|
|
|
154
|
-
[[tool.mypy.overrides]]
|
|
155
|
-
module = "diskcache.*"
|
|
156
|
-
ignore_missing_imports = true
|
|
157
|
-
|
|
158
153
|
[tool.pytest.ini_options]
|
|
159
154
|
asyncio_mode = "auto"
|
|
160
155
|
testpaths = ["tests"]
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# license that can be found in the LICENSE file or at
|
|
5
5
|
# https://opensource.org/licenses/MIT.
|
|
6
6
|
|
|
7
|
+
from asanypath._transfer import TransferBudget
|
|
7
8
|
from asanypath.artifactory import ArtifactoryPath
|
|
8
9
|
from asanypath.asanypath import (
|
|
9
10
|
PROTOCOL_MAP,
|
|
@@ -53,6 +54,7 @@ __all__ = [
|
|
|
53
54
|
"AsAnyPath",
|
|
54
55
|
"AsyncPath",
|
|
55
56
|
"SyncPath",
|
|
57
|
+
"TransferBudget",
|
|
56
58
|
"BackendOptions",
|
|
57
59
|
"AccessGrant",
|
|
58
60
|
"AccessPolicy",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""CLI backend options: ``scheme.key=value`` mapped onto path constructor kwargs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Callable, Iterable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from asanypath.asanypath import PROTOCOL_MAP, _match_base_url_flavor
|
|
11
|
+
|
|
12
|
+
Options = dict[str, dict[str, Any]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def option_keys(cls: type) -> dict[str, Callable[[str], Any]]:
|
|
16
|
+
"""Return ``{key: coercer}`` for the keyword arguments a path class accepts."""
|
|
17
|
+
keys: dict[str, Callable[[str], Any]] = {}
|
|
18
|
+
for name, param in inspect.signature(cls.__init__).parameters.items():
|
|
19
|
+
if name == "self" or param.kind in (
|
|
20
|
+
param.VAR_POSITIONAL,
|
|
21
|
+
param.VAR_KEYWORD,
|
|
22
|
+
param.POSITIONAL_ONLY,
|
|
23
|
+
):
|
|
24
|
+
continue
|
|
25
|
+
ann = str(param.annotation)
|
|
26
|
+
keys[name] = int if re.search(r"\bint\b", ann) and "str" not in ann else str
|
|
27
|
+
return keys
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_options(specs: Iterable[str]) -> Options:
|
|
31
|
+
"""Parse ``scheme.key=value`` specs; raise ValueError on anything unsupported."""
|
|
32
|
+
parsed: Options = {}
|
|
33
|
+
for spec in specs:
|
|
34
|
+
lhs, eq, value = spec.partition("=")
|
|
35
|
+
scheme, dot, key = lhs.partition(".")
|
|
36
|
+
if not eq or not dot or not key:
|
|
37
|
+
raise ValueError(f"invalid option {spec!r}; expected SCHEME.KEY=VALUE")
|
|
38
|
+
scheme = scheme.lower()
|
|
39
|
+
if scheme not in PROTOCOL_MAP:
|
|
40
|
+
raise ValueError(f"unknown protocol {scheme!r} in option {spec!r}")
|
|
41
|
+
keys = option_keys(PROTOCOL_MAP[scheme])
|
|
42
|
+
if key not in keys:
|
|
43
|
+
valid = ", ".join(sorted(keys)) or "none"
|
|
44
|
+
raise ValueError(f"{scheme} does not support option {key!r} (valid: {valid})")
|
|
45
|
+
try:
|
|
46
|
+
parsed.setdefault(scheme, {})[key] = keys[key](value)
|
|
47
|
+
except ValueError:
|
|
48
|
+
raise ValueError(f"invalid value {value!r} for {scheme}.{key}") from None
|
|
49
|
+
return parsed
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def kwargs_for(path: str, options: Options) -> dict[str, Any]:
|
|
53
|
+
"""Return the options that apply to the backend serving ``path``."""
|
|
54
|
+
if not options:
|
|
55
|
+
return {}
|
|
56
|
+
flavor = _match_base_url_flavor(path)
|
|
57
|
+
merged: dict[str, Any] = {}
|
|
58
|
+
for scheme, values in options.items():
|
|
59
|
+
if PROTOCOL_MAP[scheme] is flavor:
|
|
60
|
+
merged.update(values)
|
|
61
|
+
return merged
|