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.
Files changed (40) hide show
  1. {upd_cli-0.0.12 → upd_cli-0.0.14}/Cargo.lock +1 -1
  2. {upd_cli-0.0.12 → upd_cli-0.0.14}/Cargo.toml +1 -1
  3. {upd_cli-0.0.12 → upd_cli-0.0.14}/PKG-INFO +1 -1
  4. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/pypi.rs +225 -11
  5. {upd_cli-0.0.12 → upd_cli-0.0.14}/.mise.toml +0 -0
  6. {upd_cli-0.0.12 → upd_cli-0.0.14}/.pre-commit-config.yaml +0 -0
  7. {upd_cli-0.0.12 → upd_cli-0.0.14}/.rumdl.toml +0 -0
  8. {upd_cli-0.0.12 → upd_cli-0.0.14}/CHANGELOG.md +0 -0
  9. {upd_cli-0.0.12 → upd_cli-0.0.14}/LICENSE +0 -0
  10. {upd_cli-0.0.12 → upd_cli-0.0.14}/Makefile +0 -0
  11. {upd_cli-0.0.12 → upd_cli-0.0.14}/README.md +0 -0
  12. {upd_cli-0.0.12 → upd_cli-0.0.14}/assets/logo-wide.svg +0 -0
  13. {upd_cli-0.0.12 → upd_cli-0.0.14}/assets/logo.svg +0 -0
  14. {upd_cli-0.0.12 → upd_cli-0.0.14}/pyproject.toml +0 -0
  15. {upd_cli-0.0.12 → upd_cli-0.0.14}/python/upd_cli/__init__.py +0 -0
  16. {upd_cli-0.0.12 → upd_cli-0.0.14}/python/upd_cli/__main__.py +0 -0
  17. {upd_cli-0.0.12 → upd_cli-0.0.14}/python/upd_cli/py.typed +0 -0
  18. {upd_cli-0.0.12 → upd_cli-0.0.14}/rust-toolchain.toml +0 -0
  19. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/align.rs +0 -0
  20. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/audit.rs +0 -0
  21. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/cache.rs +0 -0
  22. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/cli.rs +0 -0
  23. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/interactive.rs +0 -0
  24. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/lib.rs +0 -0
  25. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/main.rs +0 -0
  26. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/crates_io.rs +0 -0
  27. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/go_proxy.rs +0 -0
  28. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/mock.rs +0 -0
  29. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/mod.rs +0 -0
  30. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/npm.rs +0 -0
  31. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/registry/utils.rs +0 -0
  32. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/cargo_toml.rs +0 -0
  33. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/go_mod.rs +0 -0
  34. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/mod.rs +0 -0
  35. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/package_json.rs +0 -0
  36. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/pyproject.rs +0 -0
  37. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/updater/requirements.rs +0 -0
  38. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/version/mod.rs +0 -0
  39. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/version/pep440.rs +0 -0
  40. {upd_cli-0.0.12 → upd_cli-0.0.14}/src/version/semver_util.rs +0 -0
@@ -1730,7 +1730,7 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1730
1730
 
1731
1731
  [[package]]
1732
1732
  name = "upd"
1733
- version = "0.0.12"
1733
+ version = "0.0.14"
1734
1734
  dependencies = [
1735
1735
  "anyhow",
1736
1736
  "async-trait",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "upd"
3
- version = "0.0.12"
3
+ version = "0.0.14"
4
4
  edition = "2024"
5
5
  rust-version = "1.91.1"
6
6
  description = "A fast dependency updater for Python, Node.js, Rust, and Go projects"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upd-cli
3
- Version: 0.0.12
3
+ Version: 0.0.14
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -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
- let response = self.get_with_retry(&url).await?;
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 !response.status().is_success() {
261
- return Err(anyhow!(
262
- "Failed to fetch package '{}': HTTP {}",
263
- package,
264
- response.status()
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
- let data: PyPiResponse = response.json().await?;
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/hda-pypi/simple");
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/hda-pypi"
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