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.
- {upd_cli-0.0.25 → upd_cli-0.0.26}/CHANGELOG.md +7 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/Cargo.lock +1 -1
- {upd_cli-0.0.25 → upd_cli-0.0.26}/Cargo.toml +1 -1
- {upd_cli-0.0.25 → upd_cli-0.0.26}/PKG-INFO +1 -1
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/pypi.rs +126 -27
- {upd_cli-0.0.25 → upd_cli-0.0.26}/.mise.toml +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/.pre-commit-config.yaml +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/.pre-commit-hooks.yaml +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/.rumdl.toml +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/LICENSE +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/Makefile +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/README.md +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/assets/logo-wide.svg +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/assets/logo.svg +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/pyproject.toml +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/python/upd_cli/__init__.py +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/python/upd_cli/__main__.py +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/python/upd_cli/py.typed +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/rust-toolchain.toml +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/align.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/audit.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/cache.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/cli.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/config.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/interactive.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/lib.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/lockfile.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/main.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/crates_io.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/github_releases.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/go_proxy.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/mock.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/mod.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/npm.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/nuget.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/rubygems.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/terraform.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/registry/utils.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/cargo_toml.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/csproj.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/gemfile.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/github_actions.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/go_mod.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/mise.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/mod.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/package_json.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/pre_commit.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/pyproject.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/requirements.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/updater/terraform.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/version/mod.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/version/pep440.rs +0 -0
- {upd_cli-0.0.25 → upd_cli-0.0.26}/src/version/semver_util.rs +0 -0
- {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
|
|
@@ -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
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
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=">=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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
567
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
589
|
+
// The opening tag ends when we see a bare `>`.
|
|
590
|
+
// `>` in attribute values (e.g. data-requires-python=">=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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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=">=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=">=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=">=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
|
|
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
|