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.
Files changed (55) hide show
  1. {upd_cli-0.0.24 → upd_cli-0.0.26}/CHANGELOG.md +14 -0
  2. {upd_cli-0.0.24 → upd_cli-0.0.26}/Cargo.lock +1 -1
  3. {upd_cli-0.0.24 → upd_cli-0.0.26}/Cargo.toml +1 -1
  4. upd_cli-0.0.26/Makefile +57 -0
  5. {upd_cli-0.0.24 → upd_cli-0.0.26}/PKG-INFO +4 -6
  6. {upd_cli-0.0.24 → upd_cli-0.0.26}/README.md +3 -5
  7. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/pypi.rs +273 -50
  8. upd_cli-0.0.26/vership.toml +2 -0
  9. upd_cli-0.0.24/Makefile +0 -127
  10. {upd_cli-0.0.24 → upd_cli-0.0.26}/.mise.toml +0 -0
  11. {upd_cli-0.0.24 → upd_cli-0.0.26}/.pre-commit-config.yaml +0 -0
  12. {upd_cli-0.0.24 → upd_cli-0.0.26}/.pre-commit-hooks.yaml +0 -0
  13. {upd_cli-0.0.24 → upd_cli-0.0.26}/.rumdl.toml +0 -0
  14. {upd_cli-0.0.24 → upd_cli-0.0.26}/LICENSE +0 -0
  15. {upd_cli-0.0.24 → upd_cli-0.0.26}/assets/logo-wide.svg +0 -0
  16. {upd_cli-0.0.24 → upd_cli-0.0.26}/assets/logo.svg +0 -0
  17. {upd_cli-0.0.24 → upd_cli-0.0.26}/pyproject.toml +0 -0
  18. {upd_cli-0.0.24 → upd_cli-0.0.26}/python/upd_cli/__init__.py +0 -0
  19. {upd_cli-0.0.24 → upd_cli-0.0.26}/python/upd_cli/__main__.py +0 -0
  20. {upd_cli-0.0.24 → upd_cli-0.0.26}/python/upd_cli/py.typed +0 -0
  21. {upd_cli-0.0.24 → upd_cli-0.0.26}/rust-toolchain.toml +0 -0
  22. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/align.rs +0 -0
  23. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/audit.rs +0 -0
  24. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/cache.rs +0 -0
  25. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/cli.rs +0 -0
  26. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/config.rs +0 -0
  27. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/interactive.rs +0 -0
  28. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/lib.rs +0 -0
  29. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/lockfile.rs +0 -0
  30. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/main.rs +0 -0
  31. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/crates_io.rs +0 -0
  32. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/github_releases.rs +0 -0
  33. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/go_proxy.rs +0 -0
  34. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/mock.rs +0 -0
  35. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/mod.rs +0 -0
  36. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/npm.rs +0 -0
  37. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/nuget.rs +0 -0
  38. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/rubygems.rs +0 -0
  39. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/terraform.rs +0 -0
  40. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/registry/utils.rs +0 -0
  41. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/cargo_toml.rs +0 -0
  42. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/csproj.rs +0 -0
  43. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/gemfile.rs +0 -0
  44. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/github_actions.rs +0 -0
  45. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/go_mod.rs +0 -0
  46. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/mise.rs +0 -0
  47. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/mod.rs +0 -0
  48. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/package_json.rs +0 -0
  49. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/pre_commit.rs +0 -0
  50. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/pyproject.rs +0 -0
  51. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/requirements.rs +0 -0
  52. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/updater/terraform.rs +0 -0
  53. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/version/mod.rs +0 -0
  54. {upd_cli-0.0.24 → upd_cli-0.0.26}/src/version/pep440.rs +0 -0
  55. {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
@@ -1780,7 +1780,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1780
1780
 
1781
1781
  [[package]]
1782
1782
  name = "upd"
1783
- version = "0.0.24"
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.24"
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"
@@ -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.24
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 # Use the latest version
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 and trigger when dependency files change.
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 # Use the latest version
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 and trigger when dependency files change.
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: Option<bool>,
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.unwrap_or(false) {
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
- // Extract versions from href attributes in anchor tags
507
- // Format: <a href="...">package-version.tar.gz</a> or package-version-py3-none-any.whl
508
- // 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
+
509
563
  for line in html.lines() {
510
- // Skip yanked packages (marked with data-yanked attribute)
511
- if line.contains("data-yanked") {
512
- 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;
513
571
  }
514
572
 
515
- let Some(start) = line.find('>') else {
516
- continue;
517
- };
518
- let Some(end) = line[start..].find('<') else {
519
- continue;
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
- if !include_prereleases && !Self::is_stable_version(&version_str) {
528
- continue;
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
- let Ok(version) = version_str.parse::<Version>() else {
532
- continue;
533
- };
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;
534
595
 
535
- // Avoid duplicates
536
- if !versions.iter().any(|(_, v)| v == &version_str) {
537
- 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
+ }
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="&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
+
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: Some(false),
1062
+ yanked: false,
924
1063
  },
925
1064
  SimpleApiFile {
926
1065
  filename: "my_package-1.1.0.tar.gz".to_string(),
927
- yanked: None,
1066
+ yanked: false,
928
1067
  },
929
1068
  SimpleApiFile {
930
1069
  filename: "my_package-1.2.0-py3-none-any.whl".to_string(),
931
- yanked: Some(false),
1070
+ yanked: false,
932
1071
  },
933
1072
  SimpleApiFile {
934
1073
  filename: "my_package-2.0.0a1.tar.gz".to_string(),
935
- yanked: Some(false),
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: Some(false),
1103
+ yanked: false,
965
1104
  },
966
1105
  SimpleApiFile {
967
1106
  filename: "my_package-1.1.0.tar.gz".to_string(),
968
- yanked: Some(true), // Yanked
1107
+ yanked: true, // Yanked
969
1108
  },
970
1109
  SimpleApiFile {
971
1110
  filename: "my_package-1.2.0.tar.gz".to_string(),
972
- yanked: Some(true), // Yanked
1111
+ yanked: true, // Yanked
973
1112
  },
974
1113
  SimpleApiFile {
975
1114
  filename: "my_package-1.3.0.tar.gz".to_string(),
976
- yanked: Some(false),
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 return 404
1528
- Mock::given(method("GET"))
1529
- .and(path("/testpkg/json"))
1530
- .respond_with(ResponseTemplate::new(404))
1531
- .mount(&mock_server1)
1532
- .await;
1533
-
1534
- Mock::given(method("GET"))
1535
- .and(path("/testpkg/json"))
1536
- .respond_with(ResponseTemplate::new(404))
1537
- .mount(&mock_server2)
1538
- .await;
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>
@@ -0,0 +1,2 @@
1
+ [hooks]
2
+ post-push = "gh api repos/rvben/upd-pre-commit/dispatches -f event_type=pypi_release"
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