upd-cli 0.0.24__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.24 → upd_cli-0.0.26}/CHANGELOG.md +14 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/Cargo.lock +1 -1
- {upd_cli-0.0.24 → upd_cli-0.0.26}/Cargo.toml +1 -1
- upd_cli-0.0.26/Makefile +57 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/PKG-INFO +4 -6
- {upd_cli-0.0.24 → upd_cli-0.0.26}/README.md +3 -5
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/pypi.rs +273 -50
- upd_cli-0.0.26/vership.toml +2 -0
- upd_cli-0.0.24/Makefile +0 -127
- {upd_cli-0.0.24 → upd_cli-0.0.26}/.mise.toml +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/.pre-commit-config.yaml +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/.pre-commit-hooks.yaml +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/.rumdl.toml +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/LICENSE +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/assets/logo-wide.svg +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/assets/logo.svg +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/pyproject.toml +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/python/upd_cli/__init__.py +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/python/upd_cli/__main__.py +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/python/upd_cli/py.typed +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/rust-toolchain.toml +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/align.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/audit.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/cache.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/cli.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/config.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/interactive.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/lib.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/lockfile.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/main.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/crates_io.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/github_releases.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/go_proxy.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/mock.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/mod.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/npm.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/nuget.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/rubygems.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/terraform.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/utils.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/cargo_toml.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/csproj.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/gemfile.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/github_actions.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/go_mod.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/mise.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/mod.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/package_json.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/pre_commit.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/pyproject.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/requirements.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/terraform.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/version/mod.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/version/pep440.rs +0 -0
- {upd_cli-0.0.24 → upd_cli-0.0.26}/src/version/semver_util.rs +0 -0
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
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
|
+
|
|
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
|
+
|
|
16
|
+
## [0.0.25](https://github.com/rvben/upd/compare/v0.0.24...v0.0.25) - 2026-04-15
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **pypi**: handle string-valued yanked field in PEP 691 JSON Simple API ([b17034b](https://github.com/rvben/upd/commit/b17034b540f6a5b62e446131c6e87f43695bbd9b))
|
|
21
|
+
|
|
8
22
|
## [0.0.24] - 2026-03-23
|
|
9
23
|
|
|
10
24
|
### Added
|
upd_cli-0.0.26/Makefile
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
.PHONY: build release test lint fmt check clean run install release-patch release-minor release-major
|
|
2
|
+
|
|
3
|
+
# Build debug binary
|
|
4
|
+
build:
|
|
5
|
+
cargo build
|
|
6
|
+
|
|
7
|
+
# Build release binary
|
|
8
|
+
release:
|
|
9
|
+
cargo build --release
|
|
10
|
+
|
|
11
|
+
# Run all tests
|
|
12
|
+
test:
|
|
13
|
+
cargo test
|
|
14
|
+
|
|
15
|
+
# Run tests with output
|
|
16
|
+
test-verbose:
|
|
17
|
+
cargo test -- --nocapture
|
|
18
|
+
|
|
19
|
+
# Run clippy lints
|
|
20
|
+
lint:
|
|
21
|
+
cargo clippy -- -D warnings
|
|
22
|
+
|
|
23
|
+
# Format code
|
|
24
|
+
fmt:
|
|
25
|
+
cargo fmt
|
|
26
|
+
|
|
27
|
+
# Check formatting without changing files
|
|
28
|
+
fmt-check:
|
|
29
|
+
cargo fmt -- --check
|
|
30
|
+
|
|
31
|
+
# Run all checks (format, lint, test)
|
|
32
|
+
check: fmt-check lint test
|
|
33
|
+
|
|
34
|
+
# Clean build artifacts
|
|
35
|
+
clean:
|
|
36
|
+
cargo clean
|
|
37
|
+
|
|
38
|
+
# Run debug build
|
|
39
|
+
run:
|
|
40
|
+
cargo run
|
|
41
|
+
|
|
42
|
+
# Run with arguments
|
|
43
|
+
run-release:
|
|
44
|
+
./target/release/upd
|
|
45
|
+
|
|
46
|
+
# Install to ~/.cargo/bin
|
|
47
|
+
install:
|
|
48
|
+
cargo install --path .
|
|
49
|
+
|
|
50
|
+
release-patch:
|
|
51
|
+
vership bump patch
|
|
52
|
+
|
|
53
|
+
release-minor:
|
|
54
|
+
vership bump minor
|
|
55
|
+
|
|
56
|
+
release-major:
|
|
57
|
+
vership bump major
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upd-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.26
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -595,8 +595,8 @@ Add `upd` to your `.pre-commit-config.yaml`:
|
|
|
595
595
|
|
|
596
596
|
```yaml
|
|
597
597
|
repos:
|
|
598
|
-
- repo: https://github.com/rvben/upd
|
|
599
|
-
rev: v0.0.24
|
|
598
|
+
- repo: https://github.com/rvben/upd-pre-commit
|
|
599
|
+
rev: v0.0.24
|
|
600
600
|
hooks:
|
|
601
601
|
- id: upd-check
|
|
602
602
|
# Optional: only check specific ecosystems
|
|
@@ -610,9 +610,7 @@ Available hooks:
|
|
|
610
610
|
| `upd-check` | Fail if any dependencies are outdated |
|
|
611
611
|
| `upd-check-major` | Fail only on major (breaking) updates |
|
|
612
612
|
|
|
613
|
-
Both hooks run on `pre-push` by default
|
|
614
|
-
|
|
615
|
-
**Note:** Requires `upd` to be installed and available in PATH.
|
|
613
|
+
Both hooks run on `pre-push` by default. Uses `language: python` which installs `upd-cli` from PyPI automatically — no manual installation needed.
|
|
616
614
|
|
|
617
615
|
## Development
|
|
618
616
|
|
|
@@ -572,8 +572,8 @@ Add `upd` to your `.pre-commit-config.yaml`:
|
|
|
572
572
|
|
|
573
573
|
```yaml
|
|
574
574
|
repos:
|
|
575
|
-
- repo: https://github.com/rvben/upd
|
|
576
|
-
rev: v0.0.24
|
|
575
|
+
- repo: https://github.com/rvben/upd-pre-commit
|
|
576
|
+
rev: v0.0.24
|
|
577
577
|
hooks:
|
|
578
578
|
- id: upd-check
|
|
579
579
|
# Optional: only check specific ecosystems
|
|
@@ -587,9 +587,7 @@ Available hooks:
|
|
|
587
587
|
| `upd-check` | Fail if any dependencies are outdated |
|
|
588
588
|
| `upd-check-major` | Fail only on major (breaking) updates |
|
|
589
589
|
|
|
590
|
-
Both hooks run on `pre-push` by default
|
|
591
|
-
|
|
592
|
-
**Note:** Requires `upd` to be installed and available in PATH.
|
|
590
|
+
Both hooks run on `pre-push` by default. Uses `language: python` which installs `upd-cli` from PyPI automatically — no manual installation needed.
|
|
593
591
|
|
|
594
592
|
## Development
|
|
595
593
|
|
|
@@ -60,11 +60,50 @@ struct SimpleApiResponse {
|
|
|
60
60
|
files: Vec<SimpleApiFile>,
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/// Deserialize the `yanked` field from PyPI's Simple API (PEP 592 / PEP 700).
|
|
64
|
+
/// The field can be:
|
|
65
|
+
/// - absent / `null` → not yanked (false)
|
|
66
|
+
/// - `false` → not yanked
|
|
67
|
+
/// - `true` → yanked (rare)
|
|
68
|
+
/// - a non-empty string (the yank reason) → yanked
|
|
69
|
+
fn deserialize_yanked_flag<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
|
70
|
+
where
|
|
71
|
+
D: serde::Deserializer<'de>,
|
|
72
|
+
{
|
|
73
|
+
struct YankedVisitor;
|
|
74
|
+
|
|
75
|
+
impl<'de> serde::de::Visitor<'de> for YankedVisitor {
|
|
76
|
+
type Value = bool;
|
|
77
|
+
|
|
78
|
+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
79
|
+
formatter.write_str("a boolean or yank-reason string")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn visit_bool<E: serde::de::Error>(self, v: bool) -> Result<bool, E> {
|
|
83
|
+
Ok(v)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<bool, E> {
|
|
87
|
+
Ok(!v.is_empty())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<bool, E> {
|
|
91
|
+
Ok(!v.is_empty())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn visit_unit<E: serde::de::Error>(self) -> Result<bool, E> {
|
|
95
|
+
Ok(false)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
deserializer.deserialize_any(YankedVisitor)
|
|
100
|
+
}
|
|
101
|
+
|
|
63
102
|
#[derive(Debug, Clone, Deserialize)]
|
|
64
103
|
struct SimpleApiFile {
|
|
65
104
|
filename: String,
|
|
66
|
-
#[serde(default)]
|
|
67
|
-
yanked:
|
|
105
|
+
#[serde(default, deserialize_with = "deserialize_yanked_flag")]
|
|
106
|
+
yanked: bool,
|
|
68
107
|
}
|
|
69
108
|
|
|
70
109
|
#[derive(Debug, Deserialize)]
|
|
@@ -457,7 +496,7 @@ impl PyPiRegistry {
|
|
|
457
496
|
|
|
458
497
|
for file in &data.files {
|
|
459
498
|
// Skip yanked packages
|
|
460
|
-
if file.yanked
|
|
499
|
+
if file.yanked {
|
|
461
500
|
continue;
|
|
462
501
|
}
|
|
463
502
|
|
|
@@ -503,38 +542,88 @@ impl PyPiRegistry {
|
|
|
503
542
|
let mut versions: Vec<(Version, String)> = Vec::new();
|
|
504
543
|
let normalized = package.to_lowercase().replace('_', "-");
|
|
505
544
|
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
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
|
+
|
|
509
563
|
for line in html.lines() {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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;
|
|
513
571
|
}
|
|
514
572
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
};
|
|
521
|
-
let filename = &line[start + 1..start + end];
|
|
522
|
-
let Some(version_str) = Self::extract_version_from_filename(filename, &normalized)
|
|
523
|
-
else {
|
|
524
|
-
continue;
|
|
525
|
-
};
|
|
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
|
+
}
|
|
526
578
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
}
|
|
530
588
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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;
|
|
534
595
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
}
|
|
538
627
|
}
|
|
539
628
|
}
|
|
540
629
|
|
|
@@ -913,6 +1002,56 @@ mod tests {
|
|
|
913
1002
|
assert_eq!(versions[1].1, "1.0.0");
|
|
914
1003
|
}
|
|
915
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
|
+
|
|
916
1055
|
#[test]
|
|
917
1056
|
fn test_parse_simple_api_json_response() {
|
|
918
1057
|
let registry = PyPiRegistry::new();
|
|
@@ -920,19 +1059,19 @@ mod tests {
|
|
|
920
1059
|
files: vec![
|
|
921
1060
|
SimpleApiFile {
|
|
922
1061
|
filename: "my_package-1.0.0.tar.gz".to_string(),
|
|
923
|
-
yanked:
|
|
1062
|
+
yanked: false,
|
|
924
1063
|
},
|
|
925
1064
|
SimpleApiFile {
|
|
926
1065
|
filename: "my_package-1.1.0.tar.gz".to_string(),
|
|
927
|
-
yanked:
|
|
1066
|
+
yanked: false,
|
|
928
1067
|
},
|
|
929
1068
|
SimpleApiFile {
|
|
930
1069
|
filename: "my_package-1.2.0-py3-none-any.whl".to_string(),
|
|
931
|
-
yanked:
|
|
1070
|
+
yanked: false,
|
|
932
1071
|
},
|
|
933
1072
|
SimpleApiFile {
|
|
934
1073
|
filename: "my_package-2.0.0a1.tar.gz".to_string(),
|
|
935
|
-
yanked:
|
|
1074
|
+
yanked: false,
|
|
936
1075
|
},
|
|
937
1076
|
],
|
|
938
1077
|
};
|
|
@@ -961,19 +1100,19 @@ mod tests {
|
|
|
961
1100
|
files: vec![
|
|
962
1101
|
SimpleApiFile {
|
|
963
1102
|
filename: "my_package-1.0.0.tar.gz".to_string(),
|
|
964
|
-
yanked:
|
|
1103
|
+
yanked: false,
|
|
965
1104
|
},
|
|
966
1105
|
SimpleApiFile {
|
|
967
1106
|
filename: "my_package-1.1.0.tar.gz".to_string(),
|
|
968
|
-
yanked:
|
|
1107
|
+
yanked: true, // Yanked
|
|
969
1108
|
},
|
|
970
1109
|
SimpleApiFile {
|
|
971
1110
|
filename: "my_package-1.2.0.tar.gz".to_string(),
|
|
972
|
-
yanked:
|
|
1111
|
+
yanked: true, // Yanked
|
|
973
1112
|
},
|
|
974
1113
|
SimpleApiFile {
|
|
975
1114
|
filename: "my_package-1.3.0.tar.gz".to_string(),
|
|
976
|
-
yanked:
|
|
1115
|
+
yanked: false,
|
|
977
1116
|
},
|
|
978
1117
|
],
|
|
979
1118
|
};
|
|
@@ -987,6 +1126,89 @@ mod tests {
|
|
|
987
1126
|
assert_eq!(versions[1].1, "1.0.0");
|
|
988
1127
|
}
|
|
989
1128
|
|
|
1129
|
+
#[test]
|
|
1130
|
+
fn test_parse_simple_api_json_response_string_yanked() {
|
|
1131
|
+
// PyPI can return yanked as a string (the yank reason) per PEP 592 / PEP 700.
|
|
1132
|
+
// Packages with string yanked values used to cause a serde deserialization
|
|
1133
|
+
// failure (Option<bool> vs String), which caused the entire response to be
|
|
1134
|
+
// discarded and fell through to the fallback registry with misleading 404 errors.
|
|
1135
|
+
let registry = PyPiRegistry::new();
|
|
1136
|
+
let json = r#"{
|
|
1137
|
+
"files": [
|
|
1138
|
+
{"filename": "my_package-1.0.0.tar.gz", "yanked": false},
|
|
1139
|
+
{"filename": "my_package-1.1.0.tar.gz", "yanked": "Backward compatibility bug"},
|
|
1140
|
+
{"filename": "my_package-1.2.0.tar.gz", "yanked": "security issue"},
|
|
1141
|
+
{"filename": "my_package-2.0.0.tar.gz", "yanked": false}
|
|
1142
|
+
]
|
|
1143
|
+
}"#;
|
|
1144
|
+
let data: SimpleApiResponse = serde_json::from_str(json).unwrap();
|
|
1145
|
+
let versions = registry
|
|
1146
|
+
.parse_simple_api_json_response(data, "my-package", false)
|
|
1147
|
+
.unwrap();
|
|
1148
|
+
// 1.1.0 and 1.2.0 are yanked (string reason), should be skipped
|
|
1149
|
+
assert_eq!(versions.len(), 2);
|
|
1150
|
+
assert_eq!(versions[0].1, "2.0.0");
|
|
1151
|
+
assert_eq!(versions[1].1, "1.0.0");
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
#[test]
|
|
1155
|
+
fn test_parse_simple_api_json_response_empty_string_yanked() {
|
|
1156
|
+
// yanked: "" is treated as not-yanked — an empty string carries no yank reason.
|
|
1157
|
+
// PyPI uses true (bool) for no-reason yanks; empty string is not a valid yank signal.
|
|
1158
|
+
let registry = PyPiRegistry::new();
|
|
1159
|
+
let json = r#"{
|
|
1160
|
+
"files": [
|
|
1161
|
+
{"filename": "my_package-1.0.0.tar.gz", "yanked": ""},
|
|
1162
|
+
{"filename": "my_package-2.0.0.tar.gz", "yanked": ""}
|
|
1163
|
+
]
|
|
1164
|
+
}"#;
|
|
1165
|
+
let data: SimpleApiResponse = serde_json::from_str(json).unwrap();
|
|
1166
|
+
let versions = registry
|
|
1167
|
+
.parse_simple_api_json_response(data, "my-package", false)
|
|
1168
|
+
.unwrap();
|
|
1169
|
+
assert_eq!(versions.len(), 2);
|
|
1170
|
+
assert_eq!(versions[0].1, "2.0.0");
|
|
1171
|
+
assert_eq!(versions[1].1, "1.0.0");
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
#[test]
|
|
1175
|
+
fn test_parse_simple_api_json_response_null_yanked() {
|
|
1176
|
+
// yanked: null must be treated as not-yanked (visit_unit)
|
|
1177
|
+
let registry = PyPiRegistry::new();
|
|
1178
|
+
let json = r#"{
|
|
1179
|
+
"files": [
|
|
1180
|
+
{"filename": "my_package-1.0.0.tar.gz", "yanked": null},
|
|
1181
|
+
{"filename": "my_package-2.0.0.tar.gz", "yanked": null}
|
|
1182
|
+
]
|
|
1183
|
+
}"#;
|
|
1184
|
+
let data: SimpleApiResponse = serde_json::from_str(json).unwrap();
|
|
1185
|
+
let versions = registry
|
|
1186
|
+
.parse_simple_api_json_response(data, "my-package", false)
|
|
1187
|
+
.unwrap();
|
|
1188
|
+
assert_eq!(versions.len(), 2);
|
|
1189
|
+
assert_eq!(versions[0].1, "2.0.0");
|
|
1190
|
+
assert_eq!(versions[1].1, "1.0.0");
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
#[test]
|
|
1194
|
+
fn test_parse_simple_api_json_response_absent_yanked() {
|
|
1195
|
+
// yanked field absent entirely must be treated as not-yanked (#[serde(default)])
|
|
1196
|
+
let registry = PyPiRegistry::new();
|
|
1197
|
+
let json = r#"{
|
|
1198
|
+
"files": [
|
|
1199
|
+
{"filename": "my_package-1.0.0.tar.gz"},
|
|
1200
|
+
{"filename": "my_package-2.0.0.tar.gz"}
|
|
1201
|
+
]
|
|
1202
|
+
}"#;
|
|
1203
|
+
let data: SimpleApiResponse = serde_json::from_str(json).unwrap();
|
|
1204
|
+
let versions = registry
|
|
1205
|
+
.parse_simple_api_json_response(data, "my-package", false)
|
|
1206
|
+
.unwrap();
|
|
1207
|
+
assert_eq!(versions.len(), 2);
|
|
1208
|
+
assert_eq!(versions[0].1, "2.0.0");
|
|
1209
|
+
assert_eq!(versions[1].1, "1.0.0");
|
|
1210
|
+
}
|
|
1211
|
+
|
|
990
1212
|
#[test]
|
|
991
1213
|
fn test_base64_encode() {
|
|
992
1214
|
assert_eq!(base64_encode("hello"), "aGVsbG8=");
|
|
@@ -1524,18 +1746,20 @@ mod tests {
|
|
|
1524
1746
|
let mock_server1 = MockServer::start().await;
|
|
1525
1747
|
let mock_server2 = MockServer::start().await;
|
|
1526
1748
|
|
|
1527
|
-
// Both servers
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1749
|
+
// Both servers: Simple API returns 404, JSON API also returns 404
|
|
1750
|
+
for server in [&mock_server1, &mock_server2] {
|
|
1751
|
+
Mock::given(method("GET"))
|
|
1752
|
+
.and(path("/simple/testpkg/"))
|
|
1753
|
+
.respond_with(ResponseTemplate::new(404))
|
|
1754
|
+
.mount(server)
|
|
1755
|
+
.await;
|
|
1756
|
+
|
|
1757
|
+
Mock::given(method("GET"))
|
|
1758
|
+
.and(path("/pypi/testpkg/json"))
|
|
1759
|
+
.respond_with(ResponseTemplate::new(404))
|
|
1760
|
+
.mount(server)
|
|
1761
|
+
.await;
|
|
1762
|
+
}
|
|
1539
1763
|
|
|
1540
1764
|
let primary = PyPiRegistry::with_index_url(mock_server1.uri());
|
|
1541
1765
|
let extras = vec![mock_server2.uri()];
|
|
@@ -1777,7 +2001,6 @@ mod tests {
|
|
|
1777
2001
|
let mock_server = MockServer::start().await;
|
|
1778
2002
|
|
|
1779
2003
|
// Server responds with HTML (no PEP 691 support)
|
|
1780
|
-
// Each <a> tag needs to be on its own line for the parser
|
|
1781
2004
|
let html = r#"<!DOCTYPE html>
|
|
1782
2005
|
<html>
|
|
1783
2006
|
<body>
|
upd_cli-0.0.24/Makefile
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
.PHONY: build release test lint fmt check clean run install version-get version-major version-minor version-patch version-push release-major release-minor release-patch build-wheel verify-release pre-commit-update
|
|
2
|
-
|
|
3
|
-
# Get version from Cargo.toml
|
|
4
|
-
VERSION := $(shell grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
|
|
5
|
-
|
|
6
|
-
# Verify release is ready
|
|
7
|
-
verify-release:
|
|
8
|
-
@echo "Verifying release readiness..."
|
|
9
|
-
@./scripts/verify-release-ready.sh
|
|
10
|
-
|
|
11
|
-
# Build Python wheel locally
|
|
12
|
-
build-wheel:
|
|
13
|
-
@echo "Building Python wheel..."
|
|
14
|
-
maturin build --release
|
|
15
|
-
|
|
16
|
-
# Build debug binary
|
|
17
|
-
build:
|
|
18
|
-
cargo build
|
|
19
|
-
|
|
20
|
-
# Build release binary
|
|
21
|
-
release:
|
|
22
|
-
cargo build --release
|
|
23
|
-
|
|
24
|
-
# Run all tests
|
|
25
|
-
test:
|
|
26
|
-
cargo test
|
|
27
|
-
|
|
28
|
-
# Run tests with output
|
|
29
|
-
test-verbose:
|
|
30
|
-
cargo test -- --nocapture
|
|
31
|
-
|
|
32
|
-
# Run clippy lints
|
|
33
|
-
lint:
|
|
34
|
-
cargo clippy -- -D warnings
|
|
35
|
-
|
|
36
|
-
# Format code
|
|
37
|
-
fmt:
|
|
38
|
-
cargo fmt
|
|
39
|
-
|
|
40
|
-
# Check formatting without changing files
|
|
41
|
-
fmt-check:
|
|
42
|
-
cargo fmt -- --check
|
|
43
|
-
|
|
44
|
-
# Run all checks (format, lint, test)
|
|
45
|
-
check: fmt-check lint test
|
|
46
|
-
|
|
47
|
-
# Clean build artifacts
|
|
48
|
-
clean:
|
|
49
|
-
cargo clean
|
|
50
|
-
|
|
51
|
-
# Run debug build
|
|
52
|
-
run:
|
|
53
|
-
cargo run
|
|
54
|
-
|
|
55
|
-
# Run with arguments
|
|
56
|
-
run-release:
|
|
57
|
-
./target/release/upd
|
|
58
|
-
|
|
59
|
-
# Install to ~/.cargo/bin
|
|
60
|
-
install:
|
|
61
|
-
cargo install --path .
|
|
62
|
-
|
|
63
|
-
# Show current version
|
|
64
|
-
version-get:
|
|
65
|
-
@echo "Current version: v$(VERSION)"
|
|
66
|
-
|
|
67
|
-
# Version tagging targets
|
|
68
|
-
version-major:
|
|
69
|
-
@echo "Creating new major version tag..."
|
|
70
|
-
$(eval CURRENT := $(shell git describe --tags --abbrev=0 2>/dev/null || echo v0.0.0))
|
|
71
|
-
$(eval MAJOR := $(shell echo $(CURRENT) | sed -E 's/v([0-9]+)\.[0-9]+\.[0-9]+/\1/'))
|
|
72
|
-
$(eval NEW_MAJOR := $(shell echo $$(( $(MAJOR) + 1 ))))
|
|
73
|
-
$(eval NEW_TAG := v$(NEW_MAJOR).0.0)
|
|
74
|
-
@echo "Current: $(CURRENT) -> New: $(NEW_TAG)"
|
|
75
|
-
@sed -i '' 's/^version = ".*"/version = "$(NEW_MAJOR).0.0"/' Cargo.toml
|
|
76
|
-
@cargo check --quiet
|
|
77
|
-
@git add Cargo.toml Cargo.lock
|
|
78
|
-
@git commit -m "chore: bump version to $(NEW_TAG)"
|
|
79
|
-
@git tag -a $(NEW_TAG) -m "Release $(NEW_TAG)"
|
|
80
|
-
@echo "Version $(NEW_TAG) created. Run 'make version-push' to trigger release."
|
|
81
|
-
|
|
82
|
-
version-minor:
|
|
83
|
-
@echo "Creating new minor version tag..."
|
|
84
|
-
$(eval CURRENT := $(shell git describe --tags --abbrev=0 2>/dev/null || echo v0.0.0))
|
|
85
|
-
$(eval MAJOR := $(shell echo $(CURRENT) | sed -E 's/v([0-9]+)\.[0-9]+\.[0-9]+/\1/'))
|
|
86
|
-
$(eval MINOR := $(shell echo $(CURRENT) | sed -E 's/v[0-9]+\.([0-9]+)\.[0-9]+/\1/'))
|
|
87
|
-
$(eval NEW_MINOR := $(shell echo $$(( $(MINOR) + 1 ))))
|
|
88
|
-
$(eval NEW_TAG := v$(MAJOR).$(NEW_MINOR).0)
|
|
89
|
-
@echo "Current: $(CURRENT) -> New: $(NEW_TAG)"
|
|
90
|
-
@sed -i '' 's/^version = ".*"/version = "$(MAJOR).$(NEW_MINOR).0"/' Cargo.toml
|
|
91
|
-
@cargo check --quiet
|
|
92
|
-
@git add Cargo.toml Cargo.lock
|
|
93
|
-
@git commit -m "chore: bump version to $(NEW_TAG)"
|
|
94
|
-
@git tag -a $(NEW_TAG) -m "Release $(NEW_TAG)"
|
|
95
|
-
@echo "Version $(NEW_TAG) created. Run 'make version-push' to trigger release."
|
|
96
|
-
|
|
97
|
-
version-patch:
|
|
98
|
-
@echo "Creating new patch version tag..."
|
|
99
|
-
$(eval CURRENT := $(shell git describe --tags --abbrev=0 2>/dev/null || echo v0.0.0))
|
|
100
|
-
$(eval MAJOR := $(shell echo $(CURRENT) | sed -E 's/v([0-9]+)\.[0-9]+\.[0-9]+/\1/'))
|
|
101
|
-
$(eval MINOR := $(shell echo $(CURRENT) | sed -E 's/v[0-9]+\.([0-9]+)\.[0-9]+/\1/'))
|
|
102
|
-
$(eval PATCH := $(shell echo $(CURRENT) | sed -E 's/v[0-9]+\.[0-9]+\.([0-9]+)/\1/'))
|
|
103
|
-
$(eval NEW_PATCH := $(shell echo $$(( $(PATCH) + 1 ))))
|
|
104
|
-
$(eval NEW_TAG := v$(MAJOR).$(MINOR).$(NEW_PATCH))
|
|
105
|
-
@echo "Current: $(CURRENT) -> New: $(NEW_TAG)"
|
|
106
|
-
@sed -i '' 's/^version = ".*"/version = "$(MAJOR).$(MINOR).$(NEW_PATCH)"/' Cargo.toml
|
|
107
|
-
@cargo check --quiet
|
|
108
|
-
@git add Cargo.toml Cargo.lock
|
|
109
|
-
@git commit -m "chore: bump version to $(NEW_TAG)"
|
|
110
|
-
@git tag -a $(NEW_TAG) -m "Release $(NEW_TAG)"
|
|
111
|
-
@echo "Version $(NEW_TAG) created. Run 'make version-push' to trigger release."
|
|
112
|
-
|
|
113
|
-
version-push:
|
|
114
|
-
$(eval LATEST_TAG := $(shell git describe --tags --abbrev=0))
|
|
115
|
-
@echo "Pushing latest commit and tag $(LATEST_TAG) to origin..."
|
|
116
|
-
@git push
|
|
117
|
-
@git push origin $(LATEST_TAG)
|
|
118
|
-
@echo "Release workflow triggered for $(LATEST_TAG)"
|
|
119
|
-
|
|
120
|
-
# Combined release targets
|
|
121
|
-
release-major: version-major version-push
|
|
122
|
-
release-minor: version-minor version-push
|
|
123
|
-
release-patch: version-patch version-push
|
|
124
|
-
|
|
125
|
-
# Update pre-commit hooks to latest versions
|
|
126
|
-
pre-commit-update:
|
|
127
|
-
pre-commit autoupdate
|
|
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
|