asanypath-native 2.1.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.
@@ -96,7 +96,7 @@ dependencies = [
96
96
 
97
97
  [[package]]
98
98
  name = "asanypath-native"
99
- version = "2.1.0"
99
+ version = "2.3.0"
100
100
  dependencies = [
101
101
  "anyhow",
102
102
  "aws-config",
@@ -114,6 +114,7 @@ dependencies = [
114
114
  "quick-xml",
115
115
  "reqwest",
116
116
  "ring",
117
+ "rusqlite",
117
118
  "russh",
118
119
  "russh-sftp",
119
120
  "scraper",
@@ -122,6 +123,7 @@ dependencies = [
122
123
  "sonic-rs",
123
124
  "ssh2-config",
124
125
  "tokio",
126
+ "tokio-util",
125
127
  "url",
126
128
  "urlencoding",
127
129
  ]
@@ -311,9 +313,9 @@ dependencies = [
311
313
 
312
314
  [[package]]
313
315
  name = "aws-sigv4"
314
- version = "1.5.3"
316
+ version = "1.6.0"
315
317
  source = "registry+https://github.com/rust-lang/crates.io-index"
316
- checksum = "31d955e76ff96acd555bf06fa0fa6d5bf9335fa84ae7c64481b20ae61d231f70"
318
+ checksum = "2312577f088c9fbf4206dfdb884cf1de9407b43e1a923cbed5237775116fc24b"
317
319
  dependencies = [
318
320
  "aws-credential-types",
319
321
  "aws-smithy-http",
@@ -448,15 +450,14 @@ dependencies = [
448
450
 
449
451
  [[package]]
450
452
  name = "aws-smithy-runtime-api"
451
- version = "1.16.2"
453
+ version = "1.19.0"
452
454
  source = "registry+https://github.com/rust-lang/crates.io-index"
453
- checksum = "6d881a7b7ad179fd6611680c9de89f716fb00ab40299a9a7b8c6913e8f7511a8"
455
+ checksum = "c0730c16f91124c6a2abb4932c77e299288b3dd9f967ea2e9ec48cc6731e87a4"
454
456
  dependencies = [
455
457
  "aws-smithy-async",
456
458
  "aws-smithy-runtime-api-macros",
457
459
  "aws-smithy-types",
458
460
  "bytes",
459
- "http 0.2.12",
460
461
  "http 1.5.0",
461
462
  "pin-project-lite",
462
463
  "tokio",
@@ -488,9 +489,9 @@ dependencies = [
488
489
 
489
490
  [[package]]
490
491
  name = "aws-smithy-types"
491
- version = "1.6.4"
492
+ version = "1.8.1"
492
493
  source = "registry+https://github.com/rust-lang/crates.io-index"
493
- checksum = "0b791f3ac597193fe1d08b82366986eb1f5bc31f2ac6c194c0855276116c76cd"
494
+ checksum = "69bb407740a197147da48238ecc94498493c9e85445732360cec180296ca45f1"
494
495
  dependencies = [
495
496
  "base64-simd",
496
497
  "bytes",
@@ -1197,6 +1198,18 @@ dependencies = [
1197
1198
  "windows-sys 0.61.2",
1198
1199
  ]
1199
1200
 
1201
+ [[package]]
1202
+ name = "fallible-iterator"
1203
+ version = "0.3.0"
1204
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1205
+ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
1206
+
1207
+ [[package]]
1208
+ name = "fallible-streaming-iterator"
1209
+ version = "0.1.9"
1210
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1211
+ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
1212
+
1200
1213
  [[package]]
1201
1214
  name = "fastrand"
1202
1215
  version = "2.5.0"
@@ -1254,6 +1267,12 @@ version = "1.0.7"
1254
1267
  source = "registry+https://github.com/rust-lang/crates.io-index"
1255
1268
  checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
1256
1269
 
1270
+ [[package]]
1271
+ name = "foldhash"
1272
+ version = "0.2.0"
1273
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1274
+ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
1275
+
1257
1276
  [[package]]
1258
1277
  name = "form_urlencoded"
1259
1278
  version = "1.2.2"
@@ -1493,11 +1512,32 @@ version = "0.14.5"
1493
1512
  source = "registry+https://github.com/rust-lang/crates.io-index"
1494
1513
  checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
1495
1514
 
1515
+ [[package]]
1516
+ name = "hashbrown"
1517
+ version = "0.16.1"
1518
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1519
+ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
1520
+ dependencies = [
1521
+ "foldhash",
1522
+ ]
1523
+
1496
1524
  [[package]]
1497
1525
  name = "hashbrown"
1498
1526
  version = "0.17.1"
1499
1527
  source = "registry+https://github.com/rust-lang/crates.io-index"
1500
1528
  checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
1529
+ dependencies = [
1530
+ "foldhash",
1531
+ ]
1532
+
1533
+ [[package]]
1534
+ name = "hashlink"
1535
+ version = "0.12.2"
1536
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1537
+ checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb"
1538
+ dependencies = [
1539
+ "hashbrown 0.17.1",
1540
+ ]
1501
1541
 
1502
1542
  [[package]]
1503
1543
  name = "heck"
@@ -1895,6 +1935,17 @@ dependencies = [
1895
1935
  "libc",
1896
1936
  ]
1897
1937
 
1938
+ [[package]]
1939
+ name = "libsqlite3-sys"
1940
+ version = "0.38.2"
1941
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1942
+ checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8"
1943
+ dependencies = [
1944
+ "cc",
1945
+ "pkg-config",
1946
+ "vcpkg",
1947
+ ]
1948
+
1898
1949
  [[package]]
1899
1950
  name = "litemap"
1900
1951
  version = "0.8.3"
@@ -2695,6 +2746,7 @@ dependencies = [
2695
2746
  "base64 0.22.1",
2696
2747
  "bytes",
2697
2748
  "futures-core",
2749
+ "futures-util",
2698
2750
  "h2",
2699
2751
  "http 1.5.0",
2700
2752
  "http-body 1.1.0",
@@ -2715,12 +2767,14 @@ dependencies = [
2715
2767
  "sync_wrapper",
2716
2768
  "tokio",
2717
2769
  "tokio-rustls",
2770
+ "tokio-util",
2718
2771
  "tower",
2719
2772
  "tower-http",
2720
2773
  "tower-service",
2721
2774
  "url",
2722
2775
  "wasm-bindgen",
2723
2776
  "wasm-bindgen-futures",
2777
+ "wasm-streams",
2724
2778
  "web-sys",
2725
2779
  "webpki-roots",
2726
2780
  ]
@@ -2797,6 +2851,31 @@ dependencies = [
2797
2851
  "zeroize",
2798
2852
  ]
2799
2853
 
2854
+ [[package]]
2855
+ name = "rsqlite-vfs"
2856
+ version = "0.1.1"
2857
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2858
+ checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
2859
+ dependencies = [
2860
+ "hashbrown 0.16.1",
2861
+ "thiserror",
2862
+ ]
2863
+
2864
+ [[package]]
2865
+ name = "rusqlite"
2866
+ version = "0.40.2"
2867
+ source = "registry+https://github.com/rust-lang/crates.io-index"
2868
+ checksum = "23f2a97da3e3873c73cb2a2e71b35c40ff95e0b1eefa8d72d8499a6928c3b5b3"
2869
+ dependencies = [
2870
+ "bitflags",
2871
+ "fallible-iterator",
2872
+ "fallible-streaming-iterator",
2873
+ "hashlink",
2874
+ "libsqlite3-sys",
2875
+ "smallvec",
2876
+ "sqlite-wasm-rs",
2877
+ ]
2878
+
2800
2879
  [[package]]
2801
2880
  name = "russh"
2802
2881
  version = "0.63.3"
@@ -3360,6 +3439,18 @@ version = "0.1.0"
3360
3439
  source = "registry+https://github.com/rust-lang/crates.io-index"
3361
3440
  checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620"
3362
3441
 
3442
+ [[package]]
3443
+ name = "sqlite-wasm-rs"
3444
+ version = "0.5.5"
3445
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3446
+ checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
3447
+ dependencies = [
3448
+ "cc",
3449
+ "js-sys",
3450
+ "rsqlite-vfs",
3451
+ "wasm-bindgen",
3452
+ ]
3453
+
3363
3454
  [[package]]
3364
3455
  name = "ssh-cipher"
3365
3456
  version = "0.3.0"
@@ -3794,6 +3885,12 @@ dependencies = [
3794
3885
  "wasm-bindgen",
3795
3886
  ]
3796
3887
 
3888
+ [[package]]
3889
+ name = "vcpkg"
3890
+ version = "0.2.15"
3891
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3892
+ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
3893
+
3797
3894
  [[package]]
3798
3895
  name = "version_check"
3799
3896
  version = "0.9.5"
@@ -3885,6 +3982,19 @@ dependencies = [
3885
3982
  "unicode-ident",
3886
3983
  ]
3887
3984
 
3985
+ [[package]]
3986
+ name = "wasm-streams"
3987
+ version = "0.4.2"
3988
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3989
+ checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
3990
+ dependencies = [
3991
+ "futures-util",
3992
+ "js-sys",
3993
+ "wasm-bindgen",
3994
+ "wasm-bindgen-futures",
3995
+ "web-sys",
3996
+ ]
3997
+
3888
3998
  [[package]]
3889
3999
  name = "web-sys"
3890
4000
  version = "0.3.105"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "asanypath-native"
3
- version = "2.1.0"
3
+ version = "2.3.0"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  publish = false
@@ -12,9 +12,9 @@ crate-type = ["cdylib"]
12
12
  [dependencies]
13
13
  pyo3 = { version = "0.29", features = ["extension-module"] }
14
14
  pyo3-async-runtimes = { version = "0.29", features = ["tokio-runtime"] }
15
- tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
15
+ tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] }
16
16
  aws-config = { version = "1", features = ["behavior-version-latest"] }
17
- reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "http2"] }
17
+ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "http2", "stream"] }
18
18
  futures = "0.3"
19
19
  aws-sigv4 = "1"
20
20
  aws-credential-types = { version = "1", features = ["hardcoded-credentials"] }
@@ -34,8 +34,10 @@ scraper = { version = "0.27", default-features = false }
34
34
  url = "2"
35
35
  russh = "0.63"
36
36
  russh-sftp = "3"
37
+ tokio-util = { version = "0.7", features = ["io"] }
37
38
  anyhow = "1"
38
39
  ssh2-config = { version = "0.8", default-features = false, features = ["nolog"] }
40
+ rusqlite = { version = "0.40", features = ["bundled"] }
39
41
 
40
42
  [profile.release]
41
43
  strip = "symbols"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asanypath-native
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: Native cloud acceleration for asanypath (Rust/tokio/reqwest)
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -0,0 +1,146 @@
1
+ # Copyright (C) 2026 Axis Communications AB, Lund, Sweden
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+
7
+ """Type stub for the Rust-native asanypath_native extension module."""
8
+
9
+ from typing import Any
10
+
11
+ class DownloadCache:
12
+ def __init__(self, directory: str, size_limit: int) -> None: ...
13
+ @property
14
+ def size_limit(self) -> int: ...
15
+ def get(self, key: str) -> bytes | None: ...
16
+ def set(self, key: str, value: bytes, expire: float | None = None) -> None: ...
17
+ def delete(self, key: str) -> None: ...
18
+ def clear(self) -> None: ...
19
+ def close(self) -> None: ...
20
+
21
+ # -- S3 --
22
+ async def s3_get_batch(*args: Any, **kwargs: Any) -> Any: ...
23
+ async def s3_put_batch(*args: Any, **kwargs: Any) -> Any: ...
24
+ async def s3_create_multipart(*args: Any, **kwargs: Any) -> Any: ...
25
+ async def s3_upload_part(*args: Any, **kwargs: Any) -> Any: ...
26
+ async def s3_complete_multipart(*args: Any, **kwargs: Any) -> Any: ...
27
+ async def s3_abort_multipart(*args: Any, **kwargs: Any) -> Any: ...
28
+ async def s3_copy_batch(*args: Any, **kwargs: Any) -> Any: ...
29
+ async def s3_delete_batch(*args: Any, **kwargs: Any) -> Any: ...
30
+ async def s3_exists_batch(*args: Any, **kwargs: Any) -> Any: ...
31
+ async def s3_list_batch(*args: Any, **kwargs: Any) -> Any: ...
32
+ async def s3_head_batch(*args: Any, **kwargs: Any) -> Any: ...
33
+ async def s3_is_dir(*args: Any, **kwargs: Any) -> Any: ...
34
+ async def s3_is_dir_batch(*args: Any, **kwargs: Any) -> Any: ...
35
+ async def s3_list_buckets(*args: Any, **kwargs: Any) -> Any: ...
36
+ async def s3_get_acl(*args: Any, **kwargs: Any) -> Any: ...
37
+ async def s3_put_acl(*args: Any, **kwargs: Any) -> Any: ...
38
+ async def s3_get_credentials(
39
+ region_override: str | None,
40
+ access_key: str | None,
41
+ secret_key: str | None,
42
+ session_token: str | None,
43
+ profile: str | None = None,
44
+ ) -> tuple[str, str, str | None, str, str | None]: ...
45
+ def s3_presign(
46
+ endpoint: str,
47
+ bucket: str,
48
+ key: str,
49
+ region: str,
50
+ access_key: str,
51
+ secret_key: str,
52
+ session_token: str | None = None,
53
+ method: str | None = None,
54
+ expires: int | None = None,
55
+ ) -> str: ...
56
+
57
+ # -- GCS --
58
+ async def gcs_get_batch(*args: Any, **kwargs: Any) -> Any: ...
59
+ async def gcs_put_batch(*args: Any, **kwargs: Any) -> Any: ...
60
+ async def gcs_start_resumable(*args: Any, **kwargs: Any) -> Any: ...
61
+ async def gcs_upload_chunk(*args: Any, **kwargs: Any) -> Any: ...
62
+ async def gcs_copy_batch(*args: Any, **kwargs: Any) -> Any: ...
63
+ async def gcs_delete_batch(*args: Any, **kwargs: Any) -> Any: ...
64
+ async def gcs_exists_batch(*args: Any, **kwargs: Any) -> Any: ...
65
+ async def gcs_list_batch(*args: Any, **kwargs: Any) -> Any: ...
66
+ async def gcs_head(*args: Any, **kwargs: Any) -> Any: ...
67
+ async def gcs_head_batch(*args: Any, **kwargs: Any) -> Any: ...
68
+ async def gcs_is_dir(*args: Any, **kwargs: Any) -> Any: ...
69
+ async def gcs_is_dir_batch(*args: Any, **kwargs: Any) -> Any: ...
70
+ async def gcs_list_buckets(*args: Any, **kwargs: Any) -> Any: ...
71
+ def gcs_presign(
72
+ bucket: str,
73
+ object_path: str,
74
+ service_account_json: str,
75
+ method: str | None = None,
76
+ expires: int | None = None,
77
+ ) -> str: ...
78
+
79
+ # -- Azure --
80
+ async def az_get_batch(*args: Any, **kwargs: Any) -> Any: ...
81
+ async def az_put_batch(*args: Any, **kwargs: Any) -> Any: ...
82
+ async def az_put_block(*args: Any, **kwargs: Any) -> Any: ...
83
+ async def az_put_block_list(*args: Any, **kwargs: Any) -> Any: ...
84
+ async def az_copy_batch(*args: Any, **kwargs: Any) -> Any: ...
85
+ async def az_delete_batch(*args: Any, **kwargs: Any) -> Any: ...
86
+ async def az_exists_batch(*args: Any, **kwargs: Any) -> Any: ...
87
+ async def az_list_batch(*args: Any, **kwargs: Any) -> Any: ...
88
+ async def az_head_batch(*args: Any, **kwargs: Any) -> Any: ...
89
+ async def az_is_dir(*args: Any, **kwargs: Any) -> Any: ...
90
+ async def az_is_dir_batch(*args: Any, **kwargs: Any) -> Any: ...
91
+ async def az_list_containers(*args: Any, **kwargs: Any) -> Any: ...
92
+ def az_presign(
93
+ endpoint: str,
94
+ container: str,
95
+ blob_path: str,
96
+ account_name: str,
97
+ account_key: str | None = None,
98
+ sas_token: str | None = None,
99
+ method: str | None = None,
100
+ expires: int | None = None,
101
+ ) -> str: ...
102
+
103
+ # -- Artifactory --
104
+ async def art_get_batch(*args: Any, **kwargs: Any) -> Any: ...
105
+ async def art_put_batch(*args: Any, **kwargs: Any) -> Any: ...
106
+ async def art_put_stream(*args: Any, **kwargs: Any) -> Any: ...
107
+ async def art_copy_batch(*args: Any, **kwargs: Any) -> Any: ...
108
+ async def art_delete_batch(*args: Any, **kwargs: Any) -> Any: ...
109
+ async def art_exists_batch(*args: Any, **kwargs: Any) -> Any: ...
110
+ async def art_list_batch(*args: Any, **kwargs: Any) -> Any: ...
111
+ async def art_list_repos(*args: Any, **kwargs: Any) -> Any: ...
112
+ async def art_storage_info(*args: Any, **kwargs: Any) -> Any: ...
113
+
114
+ # -- HTTP --
115
+ async def http_get(*args: Any, **kwargs: Any) -> Any: ...
116
+ async def http_put(*args: Any, **kwargs: Any) -> Any: ...
117
+ async def http_post(*args: Any, **kwargs: Any) -> Any: ...
118
+ async def http_delete(*args: Any, **kwargs: Any) -> Any: ...
119
+ async def http_head(*args: Any, **kwargs: Any) -> Any: ...
120
+ async def http_patch(*args: Any, **kwargs: Any) -> Any: ...
121
+ async def http_options(*args: Any, **kwargs: Any) -> Any: ...
122
+ async def http_exists(*args: Any, **kwargs: Any) -> Any: ...
123
+ async def http_scrape_links(*args: Any, **kwargs: Any) -> Any: ...
124
+ async def http_request(*args: Any, **kwargs: Any) -> Any: ...
125
+
126
+ # -- Unified --
127
+ async def range_read(path: str, start: int, end: int, **kwargs: Any) -> bytes: ...
128
+
129
+ # -- SSH / SFTP --
130
+ async def ssh_read(*args: Any, **kwargs: Any) -> bytes: ...
131
+ async def ssh_read_range(*args: Any, **kwargs: Any) -> bytes: ...
132
+ async def ssh_write(*args: Any, **kwargs: Any) -> int: ...
133
+ async def ssh_write_chunk(*args: Any, **kwargs: Any) -> int: ...
134
+ async def ssh_exists(*args: Any, **kwargs: Any) -> bool: ...
135
+ async def ssh_stat(*args: Any, **kwargs: Any) -> Any: ...
136
+ async def ssh_lstat(*args: Any, **kwargs: Any) -> Any: ...
137
+ async def ssh_setstat(*args: Any, **kwargs: Any) -> None: ...
138
+ async def ssh_list(*args: Any, **kwargs: Any) -> Any: ...
139
+ async def ssh_mkdir(*args: Any, **kwargs: Any) -> None: ...
140
+ async def ssh_rmdir(*args: Any, **kwargs: Any) -> None: ...
141
+ async def ssh_unlink(*args: Any, **kwargs: Any) -> None: ...
142
+ async def ssh_rename(*args: Any, **kwargs: Any) -> None: ...
143
+ def ssh_resolve_config(
144
+ host: str, config_paths: list[str]
145
+ ) -> tuple[str | None, int | None, str | None, str | None]: ...
146
+ def ssh_disconnect_all() -> None: ...
@@ -19,7 +19,7 @@ use pyo3::prelude::*;
19
19
  use serde::Deserialize;
20
20
  use std::collections::HashMap;
21
21
 
22
- use crate::http::{bearer_headers, do_request_light, status_to_pyerr};
22
+ use crate::http::{bearer_headers, do_request_light, do_request_stream_file, status_to_pyerr};
23
23
 
24
24
  // ---------------------------------------------------------------------------
25
25
  // JSON response types for Artifactory Storage API
@@ -465,6 +465,44 @@ pub fn art_put_batch<'py>(
465
465
  })
466
466
  }
467
467
 
468
+ /// Stream-upload a single file by PUT (body framed from disk; bounded memory).
469
+ /// Artifactory has no multipart API, so a large object is sent as one streamed PUT.
470
+ #[pyfunction]
471
+ #[pyo3(signature = (base_url, key, file_path, size, token, options_json=None, use_h2=None))]
472
+ pub fn art_put_stream<'py>(
473
+ py: Python<'py>,
474
+ base_url: String,
475
+ key: String,
476
+ file_path: String,
477
+ size: u64,
478
+ token: String,
479
+ options_json: Option<String>,
480
+ use_h2: Option<bool>,
481
+ ) -> PyResult<Bound<'py, PyAny>> {
482
+ let h2 = use_h2.unwrap_or(false);
483
+ pyo3_async_runtimes::tokio::future_into_py(py, async move {
484
+ let options: crate::backend::UploadOptions =
485
+ sonic_rs::from_str(&options_json.unwrap_or_else(|| "{}".to_string()))
486
+ .map_err(|e| PyValueError::new_err(format!("invalid backend_options: {e}")))?;
487
+ crate::backend::require_empty_provider(&options)?;
488
+ let creds = art_creds!(base_url, token);
489
+ let url = crate::backend::append_query(&creds.content_url(&key), &options.query);
490
+ let mut headers = creds.auth_headers();
491
+ headers.push((
492
+ "content-type".to_string(),
493
+ "application/octet-stream".to_string(),
494
+ ));
495
+ crate::backend::merge_user_headers(&mut headers, &options)?;
496
+ let (status, body) = do_request_stream_file("PUT", &url, &headers, &file_path, size, h2)
497
+ .await
498
+ .map_err(PyRuntimeError::new_err)?;
499
+ if !status.is_success() {
500
+ return Err(status_to_pyerr(status, &creds.error_path(&key), &body));
501
+ }
502
+ Ok(())
503
+ })
504
+ }
505
+
468
506
  /// Copy multiple files concurrently (server-side). Pairs are `(src, dst)`.
469
507
  #[pyfunction]
470
508
  pub fn art_copy_batch<'py>(
@@ -943,6 +943,97 @@ pub fn az_put_batch<'py>(
943
943
  })
944
944
  }
945
945
 
946
+ // ---------------------------------------------------------------------------
947
+ // Block blob upload (orchestrated from Python): stage blocks, then commit list
948
+ // ---------------------------------------------------------------------------
949
+
950
+ /// Stage one block for a block blob. `block_id` is the raw (pre-URL-encode) id.
951
+ #[pyfunction]
952
+ #[pyo3(signature = (endpoint, container, blob, block_id, data, account_name, account_key=None, sas_token=None, use_h2=None))]
953
+ pub fn az_put_block<'py>(
954
+ py: Python<'py>,
955
+ endpoint: String,
956
+ container: String,
957
+ blob: String,
958
+ block_id: String,
959
+ data: Vec<u8>,
960
+ account_name: String,
961
+ account_key: Option<String>,
962
+ sas_token: Option<String>,
963
+ use_h2: Option<bool>,
964
+ ) -> PyResult<Bound<'py, PyAny>> {
965
+ let h2 = use_h2.unwrap_or(false);
966
+ pyo3_async_runtimes::tokio::future_into_py(py, async move {
967
+ let creds = az_creds!(endpoint, container, account_name, account_key, sas_token);
968
+ let base = creds.blob_url(&blob);
969
+ let sep = if base.contains('?') { "&" } else { "?" };
970
+ let url = format!(
971
+ "{}{}comp=block&blockid={}",
972
+ base,
973
+ sep,
974
+ urlencoding::encode(&block_id)
975
+ );
976
+ let mut headers = vec![("content-length".to_string(), data.len().to_string())];
977
+ let params = [("blockid", block_id.as_str()), ("comp", "block")];
978
+ creds.sign("PUT", &url, &mut headers, Some(&params));
979
+ let (status, body) = do_request_light("PUT", &url, &headers, Some(Bytes::from(data)), h2)
980
+ .await
981
+ .map_err(PyRuntimeError::new_err)?;
982
+ if !status.is_success() {
983
+ return Err(status_to_pyerr(status, &creds.error_path(&blob), &body));
984
+ }
985
+ Ok(())
986
+ })
987
+ }
988
+
989
+ /// Commit a block blob from the staged block ids (in order).
990
+ #[pyfunction]
991
+ #[pyo3(signature = (endpoint, container, blob, block_ids, account_name, account_key=None, sas_token=None, options_json=None, use_h2=None))]
992
+ pub fn az_put_block_list<'py>(
993
+ py: Python<'py>,
994
+ endpoint: String,
995
+ container: String,
996
+ blob: String,
997
+ block_ids: Vec<String>,
998
+ account_name: String,
999
+ account_key: Option<String>,
1000
+ sas_token: Option<String>,
1001
+ options_json: Option<String>,
1002
+ use_h2: Option<bool>,
1003
+ ) -> PyResult<Bound<'py, PyAny>> {
1004
+ let h2 = use_h2.unwrap_or(false);
1005
+ pyo3_async_runtimes::tokio::future_into_py(py, async move {
1006
+ let options: crate::backend::UploadOptions =
1007
+ sonic_rs::from_str(&options_json.unwrap_or_else(|| "{}".to_string()))
1008
+ .map_err(|e| PyValueError::new_err(format!("invalid backend_options: {e}")))?;
1009
+ crate::backend::require_empty_provider(&options)?;
1010
+ let creds = az_creds!(endpoint, container, account_name, account_key, sas_token);
1011
+ let base = creds.blob_url(&blob);
1012
+ let sep = if base.contains('?') { "&" } else { "?" };
1013
+ let url = format!("{}{}comp=blocklist", base, sep);
1014
+ let mut xml = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?><BlockList>");
1015
+ for id in &block_ids {
1016
+ xml.push_str(&format!("<Latest>{}</Latest>", id));
1017
+ }
1018
+ xml.push_str("</BlockList>");
1019
+ let body = Bytes::from(xml.into_bytes());
1020
+ let mut headers = vec![
1021
+ ("content-length".to_string(), body.len().to_string()),
1022
+ ("content-type".to_string(), "application/xml".to_string()),
1023
+ ];
1024
+ crate::backend::merge_user_headers(&mut headers, &options)?;
1025
+ let params = [("comp", "blocklist")];
1026
+ creds.sign("PUT", &url, &mut headers, Some(&params));
1027
+ let (status, resp) = do_request_light("PUT", &url, &headers, Some(body), h2)
1028
+ .await
1029
+ .map_err(PyRuntimeError::new_err)?;
1030
+ if !status.is_success() {
1031
+ return Err(status_to_pyerr(status, &creds.error_path(&blob), &resp));
1032
+ }
1033
+ Ok(())
1034
+ })
1035
+ }
1036
+
946
1037
  #[pyfunction]
947
1038
  pub fn az_copy_batch<'py>(
948
1039
  py: Python<'py>,