upd-cli 0.0.25__tar.gz → 0.0.26__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.
Files changed (54) hide show
  1. {upd_cli-0.0.25 → upd_cli-0.0.26}/CHANGELOG.md +7 -0
  2. {upd_cli-0.0.25 → upd_cli-0.0.26}/Cargo.lock +1 -1
  3. {upd_cli-0.0.25 → upd_cli-0.0.26}/Cargo.toml +1 -1
  4. {upd_cli-0.0.25 → upd_cli-0.0.26}/PKG-INFO +1 -1
  5. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/pypi.rs +126 -27
  6. {upd_cli-0.0.25 → upd_cli-0.0.26}/.mise.toml +0 -0
  7. {upd_cli-0.0.25 → upd_cli-0.0.26}/.pre-commit-config.yaml +0 -0
  8. {upd_cli-0.0.25 → upd_cli-0.0.26}/.pre-commit-hooks.yaml +0 -0
  9. {upd_cli-0.0.25 → upd_cli-0.0.26}/.rumdl.toml +0 -0
  10. {upd_cli-0.0.25 → upd_cli-0.0.26}/LICENSE +0 -0
  11. {upd_cli-0.0.25 → upd_cli-0.0.26}/Makefile +0 -0
  12. {upd_cli-0.0.25 → upd_cli-0.0.26}/README.md +0 -0
  13. {upd_cli-0.0.25 → upd_cli-0.0.26}/assets/logo-wide.svg +0 -0
  14. {upd_cli-0.0.25 → upd_cli-0.0.26}/assets/logo.svg +0 -0
  15. {upd_cli-0.0.25 → upd_cli-0.0.26}/pyproject.toml +0 -0
  16. {upd_cli-0.0.25 → upd_cli-0.0.26}/python/upd_cli/__init__.py +0 -0
  17. {upd_cli-0.0.25 → upd_cli-0.0.26}/python/upd_cli/__main__.py +0 -0
  18. {upd_cli-0.0.25 → upd_cli-0.0.26}/python/upd_cli/py.typed +0 -0
  19. {upd_cli-0.0.25 → upd_cli-0.0.26}/rust-toolchain.toml +0 -0
  20. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/align.rs +0 -0
  21. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/audit.rs +0 -0
  22. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/cache.rs +0 -0
  23. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/cli.rs +0 -0
  24. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/config.rs +0 -0
  25. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/interactive.rs +0 -0
  26. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/lib.rs +0 -0
  27. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/lockfile.rs +0 -0
  28. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/main.rs +0 -0
  29. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/crates_io.rs +0 -0
  30. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/github_releases.rs +0 -0
  31. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/go_proxy.rs +0 -0
  32. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/mock.rs +0 -0
  33. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/mod.rs +0 -0
  34. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/npm.rs +0 -0
  35. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/nuget.rs +0 -0
  36. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/rubygems.rs +0 -0
  37. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/terraform.rs +0 -0
  38. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/utils.rs +0 -0
  39. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/cargo_toml.rs +0 -0
  40. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/csproj.rs +0 -0
  41. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/gemfile.rs +0 -0
  42. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/github_actions.rs +0 -0
  43. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/go_mod.rs +0 -0
  44. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/mise.rs +0 -0
  45. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/mod.rs +0 -0
  46. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/package_json.rs +0 -0
  47. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/pre_commit.rs +0 -0
  48. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/pyproject.rs +0 -0
  49. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/requirements.rs +0 -0
  50. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/terraform.rs +0 -0
  51. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/version/mod.rs +0 -0
  52. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/version/pep440.rs +0 -0
  53. {upd_cli-0.0.25 → upd_cli-0.0.26}/src/version/semver_util.rs +0 -0
  54. {upd_cli-0.0.25 → upd_cli-0.0.26}/vership.toml +0 -0
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+
10
+ ## [0.0.26](https://github.com/rvben/upd/compare/v0.0.25...v0.0.26) - 2026-04-15
11
+
12
+ ### Fixed
13
+
14
+ - **pypi**: rewrite HTML Simple API parser to handle multi-line anchor tags ([f9c937b](https://github.com/rvben/upd/commit/f9c937be297112e0556cae205f8c0f3ce54997f4))
15
+
9
16
  ## [0.0.25](https://github.com/rvben/upd/compare/v0.0.24...v0.0.25) - 2026-04-15
10
17
 
11
18
  ### Fixed
@@ -1780,7 +1780,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1780
1780
 
1781
1781
  [[package]]
1782
1782
  name = "upd"
1783
- version = "0.0.25"
1783
+ version = "0.0.26"
1784
1784
  dependencies = [
1785
1785
  "anyhow",
1786
1786
  "async-trait",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "upd"
3
- version = "0.0.25"
3
+ version = "0.0.26"
4
4
  edition = "2024"
5
5
  rust-version = "1.91.1"
6
6
  description = "A fast dependency updater for Python, Node.js, Rust, Go, Ruby, Terraform, GitHub Actions, pre-commit, and Mise projects"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upd-cli
3
- Version: 0.0.25
3
+ Version: 0.0.26
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -542,38 +542,88 @@ impl PyPiRegistry {
542
542
  let mut versions: Vec<(Version, String)> = Vec::new();
543
543
  let normalized = package.to_lowercase().replace('_', "-");
544
544
 
545
- // Extract versions from href attributes in anchor tags
546
- // Format: <a href="...">package-version.tar.gz</a> or package-version-py3-none-any.whl
547
- // Yanked packages have data-yanked attribute: <a href="..." data-yanked="">...</a>
545
+ // Use a state machine to collect multi-line <a> opening tags.
546
+ //
547
+ // Some registries (e.g. Nexus Repository Manager) spread attributes
548
+ // across multiple lines:
549
+ // <a href="...pkg-1.0.0.whl#sha256=..."
550
+ // rel="internal"
551
+ // data-requires-python="&gt;=3.10"
552
+ // >
553
+ // pkg-1.0.0.whl
554
+ // </a>
555
+ //
556
+ // We extract the filename from the `href` URL (the last path segment
557
+ // before the `#sha256=...` fragment) rather than the link text, so
558
+ // both single-line and multi-line formats are handled uniformly.
559
+ let mut collecting = false;
560
+ let mut is_yanked = false;
561
+ let mut href_value: Option<String> = None;
562
+
548
563
  for line in html.lines() {
549
- // Skip yanked packages (marked with data-yanked attribute)
550
- if line.contains("data-yanked") {
551
- continue;
564
+ let trimmed = line.trim();
565
+
566
+ // Detect start of an anchor element
567
+ if trimmed.starts_with("<a ") || trimmed.starts_with("<a\t") {
568
+ collecting = true;
569
+ is_yanked = false;
570
+ href_value = None;
552
571
  }
553
572
 
554
- let Some(start) = line.find('>') else {
555
- continue;
556
- };
557
- let Some(end) = line[start..].find('<') else {
558
- continue;
559
- };
560
- let filename = &line[start + 1..start + end];
561
- let Some(version_str) = Self::extract_version_from_filename(filename, &normalized)
562
- else {
563
- continue;
564
- };
573
+ if collecting {
574
+ // Check for yanked attribute (may be on any line of the element)
575
+ if trimmed.contains("data-yanked") {
576
+ is_yanked = true;
577
+ }
565
578
 
566
- if !include_prereleases && !Self::is_stable_version(&version_str) {
567
- continue;
568
- }
579
+ // Extract href value from whichever line carries it
580
+ if href_value.is_none()
581
+ && let Some(href_start) = trimmed.find("href=\"")
582
+ {
583
+ let after = &trimmed[href_start + 6..];
584
+ if let Some(href_end) = after.find('"') {
585
+ href_value = Some(after[..href_end].to_string());
586
+ }
587
+ }
569
588
 
570
- let Ok(version) = version_str.parse::<Version>() else {
571
- continue;
572
- };
589
+ // The opening tag ends when we see a bare `>`.
590
+ // `&gt;` in attribute values (e.g. data-requires-python="&gt;=3.10")
591
+ // is the HTML entity and does NOT contain a literal `>` byte in the
592
+ // raw HTTP response, so it won't trigger this check.
593
+ if trimmed.contains('>') {
594
+ collecting = false;
573
595
 
574
- // Avoid duplicates
575
- if !versions.iter().any(|(_, v)| v == &version_str) {
576
- versions.push((version, version_str));
596
+ if is_yanked {
597
+ continue;
598
+ }
599
+
600
+ let Some(href) = href_value.take() else {
601
+ continue;
602
+ };
603
+
604
+ // Filename is the last URL path segment, before any `#fragment`
605
+ let url_path = href.split('#').next().unwrap_or(&href);
606
+ let filename = url_path.split('/').next_back().unwrap_or("");
607
+
608
+ let Some(version_str) =
609
+ Self::extract_version_from_filename(filename, &normalized)
610
+ else {
611
+ continue;
612
+ };
613
+
614
+ if !include_prereleases && !Self::is_stable_version(&version_str) {
615
+ continue;
616
+ }
617
+
618
+ let Ok(version) = version_str.parse::<Version>() else {
619
+ continue;
620
+ };
621
+
622
+ // Avoid duplicates (e.g. both .whl and .tar.gz for the same version)
623
+ if !versions.iter().any(|(_, v)| v == &version_str) {
624
+ versions.push((version, version_str));
625
+ }
626
+ }
577
627
  }
578
628
  }
579
629
 
@@ -952,6 +1002,56 @@ mod tests {
952
1002
  assert_eq!(versions[1].1, "1.0.0");
953
1003
  }
954
1004
 
1005
+ #[test]
1006
+ fn test_parse_simple_api_response_multiline_anchor_format() {
1007
+ // Some registries (e.g. Nexus Repository Manager) spread <a> attributes
1008
+ // across multiple lines, with the filename text on a separate line after
1009
+ // the closing '>'. Versions must be extracted from the href URL, not the
1010
+ // link text.
1011
+ let registry = PyPiRegistry::new();
1012
+ let html = r#"
1013
+ <!DOCTYPE html>
1014
+ <html lang="en">
1015
+ <head><title>Links for my-package</title>
1016
+ <meta name="api-version" value="2"/>
1017
+ </head>
1018
+ <body>
1019
+ <h1>Links for my-package</h1>
1020
+ <a href="../../packages/my-package/2.0.0/my_package-2.0.0.tar.gz#sha256=abc123"
1021
+ rel="internal"
1022
+ data-requires-python="&gt;=3.10"
1023
+
1024
+ >
1025
+ my_package-2.0.0.tar.gz
1026
+ </a>
1027
+ <br/>
1028
+ <a href="../../packages/my-package/2.0.0/my_package-2.0.0-py3-none-any.whl#sha256=def456"
1029
+ rel="internal"
1030
+ data-requires-python="&gt;=3.10"
1031
+
1032
+ >
1033
+ my_package-2.0.0-py3-none-any.whl
1034
+ </a>
1035
+ <br/>
1036
+ <a href="../../packages/my-package/1.9.0/my_package-1.9.0-py3-none-any.whl#sha256=ghi789"
1037
+ rel="internal"
1038
+ data-requires-python="&gt;=3.10"
1039
+ data-yanked="superseded"
1040
+ >
1041
+ my_package-1.9.0-py3-none-any.whl
1042
+ </a>
1043
+ </body>
1044
+ </html>
1045
+ "#;
1046
+ let versions = registry
1047
+ .parse_simple_api_response(html, "my-package", false)
1048
+ .unwrap();
1049
+ // 2.0.0 appears twice (tar.gz + whl) but should be deduplicated
1050
+ // 1.9.0 is yanked and should be excluded
1051
+ assert_eq!(versions.len(), 1);
1052
+ assert_eq!(versions[0].1, "2.0.0");
1053
+ }
1054
+
955
1055
  #[test]
956
1056
  fn test_parse_simple_api_json_response() {
957
1057
  let registry = PyPiRegistry::new();
@@ -1901,7 +2001,6 @@ mod tests {
1901
2001
  let mock_server = MockServer::start().await;
1902
2002
 
1903
2003
  // Server responds with HTML (no PEP 691 support)
1904
- // Each <a> tag needs to be on its own line for the parser
1905
2004
  let html = r#"<!DOCTYPE html>
1906
2005
  <html>
1907
2006
  <body>
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