upd-cli 0.0.12__tar.gz → 0.0.14__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.
- {upd_cli-0.0.12 → upd_cli-0.0.14}/Cargo.lock +1 -1
- {upd_cli-0.0.12 → upd_cli-0.0.14}/Cargo.toml +1 -1
- {upd_cli-0.0.12 → upd_cli-0.0.14}/PKG-INFO +1 -1
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/pypi.rs +225 -11
- {upd_cli-0.0.12 → upd_cli-0.0.14}/.mise.toml +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/.pre-commit-config.yaml +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/.rumdl.toml +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/CHANGELOG.md +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/LICENSE +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/Makefile +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/README.md +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/assets/logo-wide.svg +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/assets/logo.svg +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/pyproject.toml +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/python/upd_cli/__init__.py +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/python/upd_cli/__main__.py +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/python/upd_cli/py.typed +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/rust-toolchain.toml +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/align.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/audit.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/cache.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/cli.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/interactive.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/lib.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/main.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/crates_io.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/go_proxy.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/mock.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/mod.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/npm.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/utils.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/cargo_toml.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/go_mod.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/mod.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/package_json.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/pyproject.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/requirements.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/version/mod.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/version/pep440.rs +0 -0
- {upd_cli-0.0.12 → upd_cli-0.0.14}/src/version/semver_util.rs +0 -0
|
@@ -247,26 +247,46 @@ impl PyPiRegistry {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
/// Internal method to fetch versions with optional pre-release inclusion
|
|
250
|
+
/// Tries JSON API first, falls back to Simple API for private registries
|
|
250
251
|
async fn fetch_versions_internal(
|
|
251
252
|
&self,
|
|
252
253
|
package: &str,
|
|
253
254
|
include_prereleases: bool,
|
|
254
255
|
) -> Result<Vec<(Version, String)>> {
|
|
255
256
|
let normalized = package.to_lowercase().replace('_', "-");
|
|
256
|
-
let url = format!("{}/{}/json", self.index_url, normalized);
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
// Try JSON API first (PyPI.org style)
|
|
259
|
+
let json_url = format!("{}/{}/json", self.index_url, normalized);
|
|
260
|
+
let response = self.get_with_retry(&json_url).await?;
|
|
259
261
|
|
|
260
|
-
if
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
if response.status().is_success() {
|
|
263
|
+
// JSON API succeeded
|
|
264
|
+
let data: PyPiResponse = response.json().await?;
|
|
265
|
+
return self.parse_json_response(data, include_prereleases);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// JSON API failed - try Simple API (private registries like Nexus)
|
|
269
|
+
let simple_url = format!("{}/simple/{}/", self.index_url, normalized);
|
|
270
|
+
let simple_response = self.get_with_retry(&simple_url).await?;
|
|
271
|
+
|
|
272
|
+
if simple_response.status().is_success() {
|
|
273
|
+
let html = simple_response.text().await?;
|
|
274
|
+
return self.parse_simple_api_response(&html, package, include_prereleases);
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
|
|
277
|
+
Err(anyhow!(
|
|
278
|
+
"Failed to fetch package '{}': HTTP {}",
|
|
279
|
+
package,
|
|
280
|
+
simple_response.status()
|
|
281
|
+
))
|
|
282
|
+
}
|
|
269
283
|
|
|
284
|
+
/// Parse JSON API response from PyPI
|
|
285
|
+
fn parse_json_response(
|
|
286
|
+
&self,
|
|
287
|
+
data: PyPiResponse,
|
|
288
|
+
include_prereleases: bool,
|
|
289
|
+
) -> Result<Vec<(Version, String)>> {
|
|
270
290
|
let mut versions: Vec<(Version, String)> = data
|
|
271
291
|
.releases
|
|
272
292
|
.iter()
|
|
@@ -288,6 +308,100 @@ impl PyPiRegistry {
|
|
|
288
308
|
versions.sort_by(|a, b| b.0.cmp(&a.0));
|
|
289
309
|
Ok(versions)
|
|
290
310
|
}
|
|
311
|
+
|
|
312
|
+
/// Parse Simple API HTML response (for private registries)
|
|
313
|
+
/// Extracts versions from package filenames in anchor tags
|
|
314
|
+
fn parse_simple_api_response(
|
|
315
|
+
&self,
|
|
316
|
+
html: &str,
|
|
317
|
+
package: &str,
|
|
318
|
+
include_prereleases: bool,
|
|
319
|
+
) -> Result<Vec<(Version, String)>> {
|
|
320
|
+
let mut versions: Vec<(Version, String)> = Vec::new();
|
|
321
|
+
let normalized = package.to_lowercase().replace('_', "-");
|
|
322
|
+
|
|
323
|
+
// Extract versions from href attributes in anchor tags
|
|
324
|
+
// Format: <a href="...">package-version.tar.gz</a> or package-version-py3-none-any.whl
|
|
325
|
+
// Yanked packages have data-yanked attribute: <a href="..." data-yanked="">...</a>
|
|
326
|
+
for line in html.lines() {
|
|
327
|
+
// Skip yanked packages (marked with data-yanked attribute)
|
|
328
|
+
if line.contains("data-yanked") {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let Some(start) = line.find('>') else {
|
|
333
|
+
continue;
|
|
334
|
+
};
|
|
335
|
+
let Some(end) = line[start..].find('<') else {
|
|
336
|
+
continue;
|
|
337
|
+
};
|
|
338
|
+
let filename = &line[start + 1..start + end];
|
|
339
|
+
let Some(version_str) = Self::extract_version_from_filename(filename, &normalized)
|
|
340
|
+
else {
|
|
341
|
+
continue;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
if !include_prereleases && !Self::is_stable_version(&version_str) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let Ok(version) = version_str.parse::<Version>() else {
|
|
349
|
+
continue;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Avoid duplicates
|
|
353
|
+
if !versions.iter().any(|(_, v)| v == &version_str) {
|
|
354
|
+
versions.push((version, version_str));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if versions.is_empty() {
|
|
359
|
+
return Err(anyhow!("No versions found for package '{}'", package));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
versions.sort_by(|a, b| b.0.cmp(&a.0));
|
|
363
|
+
Ok(versions)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// Extract version from a package filename
|
|
367
|
+
/// Handles: package-1.0.0.tar.gz, package-1.0.0-py3-none-any.whl, etc.
|
|
368
|
+
fn extract_version_from_filename(filename: &str, normalized_package: &str) -> Option<String> {
|
|
369
|
+
// Remove file extension
|
|
370
|
+
let name = filename
|
|
371
|
+
.trim_end_matches(".tar.gz")
|
|
372
|
+
.trim_end_matches(".zip")
|
|
373
|
+
.trim_end_matches(".whl")
|
|
374
|
+
.trim_end_matches(".egg");
|
|
375
|
+
|
|
376
|
+
// Package name can have - or _ replaced, normalize for matching
|
|
377
|
+
let name_lower = name.to_lowercase();
|
|
378
|
+
let pkg_with_dash = normalized_package;
|
|
379
|
+
let pkg_with_underscore = normalized_package.replace('-', "_");
|
|
380
|
+
|
|
381
|
+
// Find where the version starts (after package name and separator)
|
|
382
|
+
let version_start = if name_lower.starts_with(&format!("{}-", pkg_with_dash)) {
|
|
383
|
+
Some(pkg_with_dash.len() + 1)
|
|
384
|
+
} else if name_lower.starts_with(&format!("{}-", pkg_with_underscore)) {
|
|
385
|
+
Some(pkg_with_underscore.len() + 1)
|
|
386
|
+
} else {
|
|
387
|
+
None
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if let Some(start) = version_start {
|
|
391
|
+
let rest = &name[start..];
|
|
392
|
+
// For wheel files, version ends at first '-' after version
|
|
393
|
+
// For source dists, version is the rest of the name
|
|
394
|
+
let version = if filename.ends_with(".whl") {
|
|
395
|
+
// Wheel format: {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
|
|
396
|
+
rest.split('-').next().unwrap_or(rest)
|
|
397
|
+
} else {
|
|
398
|
+
rest
|
|
399
|
+
};
|
|
400
|
+
Some(version.to_string())
|
|
401
|
+
} else {
|
|
402
|
+
None
|
|
403
|
+
}
|
|
404
|
+
}
|
|
291
405
|
}
|
|
292
406
|
|
|
293
407
|
impl Default for PyPiRegistry {
|
|
@@ -500,6 +614,106 @@ mod tests {
|
|
|
500
614
|
assert!(!PyPiRegistry::is_stable_version("1.0.0.dev1"));
|
|
501
615
|
}
|
|
502
616
|
|
|
617
|
+
#[test]
|
|
618
|
+
fn test_extract_version_from_filename() {
|
|
619
|
+
// Source distributions
|
|
620
|
+
assert_eq!(
|
|
621
|
+
PyPiRegistry::extract_version_from_filename("my-package-1.2.3.tar.gz", "my-package"),
|
|
622
|
+
Some("1.2.3".to_string())
|
|
623
|
+
);
|
|
624
|
+
assert_eq!(
|
|
625
|
+
PyPiRegistry::extract_version_from_filename("my_package-1.2.3.tar.gz", "my-package"),
|
|
626
|
+
Some("1.2.3".to_string())
|
|
627
|
+
);
|
|
628
|
+
assert_eq!(
|
|
629
|
+
PyPiRegistry::extract_version_from_filename("requests-2.31.0.tar.gz", "requests"),
|
|
630
|
+
Some("2.31.0".to_string())
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
// Wheel files
|
|
634
|
+
assert_eq!(
|
|
635
|
+
PyPiRegistry::extract_version_from_filename(
|
|
636
|
+
"my_package-1.2.3-py3-none-any.whl",
|
|
637
|
+
"my-package"
|
|
638
|
+
),
|
|
639
|
+
Some("1.2.3".to_string())
|
|
640
|
+
);
|
|
641
|
+
assert_eq!(
|
|
642
|
+
PyPiRegistry::extract_version_from_filename(
|
|
643
|
+
"requests-2.31.0-py3-none-any.whl",
|
|
644
|
+
"requests"
|
|
645
|
+
),
|
|
646
|
+
Some("2.31.0".to_string())
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// Pre-release versions
|
|
650
|
+
assert_eq!(
|
|
651
|
+
PyPiRegistry::extract_version_from_filename("mypackage-1.0.0a1.tar.gz", "mypackage"),
|
|
652
|
+
Some("1.0.0a1".to_string())
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
// Non-matching package
|
|
656
|
+
assert_eq!(
|
|
657
|
+
PyPiRegistry::extract_version_from_filename("other-package-1.0.0.tar.gz", "mypackage"),
|
|
658
|
+
None
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
#[test]
|
|
663
|
+
fn test_parse_simple_api_response() {
|
|
664
|
+
let registry = PyPiRegistry::new();
|
|
665
|
+
let html = r#"
|
|
666
|
+
<!DOCTYPE html>
|
|
667
|
+
<html>
|
|
668
|
+
<head><title>Links for my-package</title></head>
|
|
669
|
+
<body>
|
|
670
|
+
<a href="../../packages/my_package-1.0.0.tar.gz">my_package-1.0.0.tar.gz</a>
|
|
671
|
+
<a href="../../packages/my_package-1.1.0.tar.gz">my_package-1.1.0.tar.gz</a>
|
|
672
|
+
<a href="../../packages/my_package-1.2.0-py3-none-any.whl">my_package-1.2.0-py3-none-any.whl</a>
|
|
673
|
+
<a href="../../packages/my_package-2.0.0a1.tar.gz">my_package-2.0.0a1.tar.gz</a>
|
|
674
|
+
</body>
|
|
675
|
+
</html>
|
|
676
|
+
"#;
|
|
677
|
+
// Stable versions only
|
|
678
|
+
let versions = registry
|
|
679
|
+
.parse_simple_api_response(html, "my-package", false)
|
|
680
|
+
.unwrap();
|
|
681
|
+
assert_eq!(versions.len(), 3);
|
|
682
|
+
assert_eq!(versions[0].1, "1.2.0"); // Highest stable
|
|
683
|
+
assert_eq!(versions[1].1, "1.1.0");
|
|
684
|
+
assert_eq!(versions[2].1, "1.0.0");
|
|
685
|
+
|
|
686
|
+
// Including prereleases
|
|
687
|
+
let versions_with_pre = registry
|
|
688
|
+
.parse_simple_api_response(html, "my-package", true)
|
|
689
|
+
.unwrap();
|
|
690
|
+
assert_eq!(versions_with_pre.len(), 4);
|
|
691
|
+
assert_eq!(versions_with_pre[0].1, "2.0.0a1"); // Highest including prerelease
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
#[test]
|
|
695
|
+
fn test_parse_simple_api_response_skips_yanked() {
|
|
696
|
+
let registry = PyPiRegistry::new();
|
|
697
|
+
let html = r#"
|
|
698
|
+
<!DOCTYPE html>
|
|
699
|
+
<html>
|
|
700
|
+
<body>
|
|
701
|
+
<a href="../../packages/my_package-1.0.0.tar.gz">my_package-1.0.0.tar.gz</a>
|
|
702
|
+
<a href="../../packages/my_package-1.1.0.tar.gz" data-yanked="">my_package-1.1.0.tar.gz</a>
|
|
703
|
+
<a href="../../packages/my_package-1.2.0.tar.gz" data-yanked="security issue">my_package-1.2.0.tar.gz</a>
|
|
704
|
+
<a href="../../packages/my_package-1.3.0.tar.gz">my_package-1.3.0.tar.gz</a>
|
|
705
|
+
</body>
|
|
706
|
+
</html>
|
|
707
|
+
"#;
|
|
708
|
+
let versions = registry
|
|
709
|
+
.parse_simple_api_response(html, "my-package", false)
|
|
710
|
+
.unwrap();
|
|
711
|
+
// Should only have 1.0.0 and 1.3.0 (1.1.0 and 1.2.0 are yanked)
|
|
712
|
+
assert_eq!(versions.len(), 2);
|
|
713
|
+
assert_eq!(versions[0].1, "1.3.0");
|
|
714
|
+
assert_eq!(versions[1].1, "1.0.0");
|
|
715
|
+
}
|
|
716
|
+
|
|
503
717
|
#[test]
|
|
504
718
|
fn test_base64_encode() {
|
|
505
719
|
assert_eq!(base64_encode("hello"), "aGVsbG8=");
|
|
@@ -625,10 +839,10 @@ mod tests {
|
|
|
625
839
|
fn test_from_url_nexus_style() {
|
|
626
840
|
// Nexus Repository Manager style URL
|
|
627
841
|
let registry =
|
|
628
|
-
PyPiRegistry::from_url("https://nexus.example.com/repository/
|
|
842
|
+
PyPiRegistry::from_url("https://nexus.example.com/repository/pypi-private/simple");
|
|
629
843
|
assert_eq!(
|
|
630
844
|
registry.index_url(),
|
|
631
|
-
"https://nexus.example.com/repository/
|
|
845
|
+
"https://nexus.example.com/repository/pypi-private"
|
|
632
846
|
);
|
|
633
847
|
}
|
|
634
848
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|