plugin-kit-ai 2.0.0__tar.gz → 2.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugin-kit-ai
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Thin Python launcher for the plugin-kit-ai CLI
5
5
  Author: plugin-kit-ai
6
6
  License: Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """plugin-kit-ai Python wrapper package."""
2
2
 
3
- __version__ = "2.0.0"
3
+ __version__ = "2.0.2"
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import json
7
7
  import os
8
+ import re
8
9
  import shutil
9
10
  import stat
10
11
  import subprocess
@@ -25,10 +26,27 @@ PLACEHOLDER_VERSION = "0.0.0.dev0"
25
26
 
26
27
 
27
28
  def normalize_tag(raw: str) -> str:
28
- value = str(raw or "").strip()
29
+ value = str(raw or "")
29
30
  if not value or value == "latest":
30
31
  return ""
31
- return value if value.startswith("v") else f"v{value}"
32
+ match = re.fullmatch(r"(plugin-kit-ai-v|v)?((0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*))", value)
33
+ if not match:
34
+ raise ValueError(f"invalid plugin-kit-ai release tag: {value}")
35
+ prefix, version, major = match.group(1), match.group(2), int(match.group(3))
36
+ if major >= 2:
37
+ if prefix == "v":
38
+ raise ValueError("plugin-kit-ai v2+ requires a product-prefixed release tag")
39
+ return f"plugin-kit-ai-v{version}"
40
+ return f"{prefix or 'v'}{version}"
41
+
42
+
43
+ def version_from_tag(tag: str) -> str:
44
+ normalized = normalize_tag(tag)
45
+ if not normalized:
46
+ raise ValueError("an exact plugin-kit-ai release tag is required")
47
+ if normalized.startswith("plugin-kit-ai-v"):
48
+ return normalized[len("plugin-kit-ai-v") :]
49
+ return normalized[1:]
32
50
 
33
51
 
34
52
  def derive_release_base(api_base: str, override: str) -> str:
@@ -79,7 +97,7 @@ def fetch_text(url: str, accept_json: bool = False) -> str:
79
97
  def latest_tag(api_base: str, repository: str) -> str:
80
98
  clean_base = str(api_base or DEFAULT_API_BASE).strip().rstrip("/")
81
99
  payload = json.loads(fetch_text(f"{clean_base}/repos/{repository}/releases/latest", accept_json=True))
82
- tag_name = str(payload.get("tag_name", "")).strip()
100
+ tag_name = str(payload.get("tag_name", ""))
83
101
  if not tag_name:
84
102
  raise RuntimeError(f"could not resolve latest release tag from {clean_base}")
85
103
  return normalize_tag(tag_name)
@@ -142,7 +160,7 @@ def ensure_installed(*, quiet: bool = False) -> Dict[str, str]:
142
160
  tag = resolve_requested_tag()
143
161
  if not tag:
144
162
  tag = latest_tag(api_base, repository)
145
- version = tag[1:]
163
+ version = version_from_tag(tag)
146
164
  asset_name = asset_name_for_version(version, platform_info)
147
165
 
148
166
  cache_root = default_cache_root()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugin-kit-ai
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Thin Python launcher for the plugin-kit-ai CLI
5
5
  Author: plugin-kit-ai
6
6
  License: Apache-2.0
@@ -9,4 +9,5 @@ src/plugin_kit_ai.egg-info/PKG-INFO
9
9
  src/plugin_kit_ai.egg-info/SOURCES.txt
10
10
  src/plugin_kit_ai.egg-info/dependency_links.txt
11
11
  src/plugin_kit_ai.egg-info/entry_points.txt
12
- src/plugin_kit_ai.egg-info/top_level.txt
12
+ src/plugin_kit_ai.egg-info/top_level.txt
13
+ tests/test_install.py
@@ -0,0 +1,79 @@
1
+ """Cold-cache wrapper regressions, without network or user cache access."""
2
+ import hashlib
3
+ import io
4
+ import os
5
+ from pathlib import Path
6
+ import tarfile
7
+ import tempfile
8
+ import unittest
9
+ from unittest.mock import patch
10
+
11
+ from plugin_kit_ai import install
12
+
13
+
14
+ class ReleaseTags(unittest.TestCase):
15
+ def test_numeric_and_product_tags(self):
16
+ for raw, tag, version in [
17
+ ('2.0.2', 'plugin-kit-ai-v2.0.2', '2.0.2'),
18
+ ('plugin-kit-ai-v2.0.2', 'plugin-kit-ai-v2.0.2', '2.0.2'),
19
+ ('1.2.4', 'v1.2.4', '1.2.4'),
20
+ ('v1.2.4', 'v1.2.4', '1.2.4'),
21
+ ('plugin-kit-ai-v1.2.4', 'plugin-kit-ai-v1.2.4', '1.2.4'),
22
+ ]:
23
+ with self.subTest(raw=raw):
24
+ self.assertEqual(install.normalize_tag(raw), tag)
25
+ self.assertEqual(install.version_from_tag(tag), version)
26
+
27
+ def test_reject_wrong_product_and_malformed_tags(self):
28
+ for tag in ['agentplugins-v0.1.62', 'v2.0.2', '2.00.2', '2.0',
29
+ 'plugin-kit-ai-plugin-kit-ai-v2.0.2', '2.0.2-rc.1',
30
+ '2.0.2+build', '../2.0.2', 'vlatest', '2.0.2\n',
31
+ ' plugin-kit-ai-v2.0.2']:
32
+ with self.subTest(tag=tag), self.assertRaises(ValueError):
33
+ install.normalize_tag(tag)
34
+
35
+ def test_packaged_version_and_latest_product_validation(self):
36
+ with patch.dict(os.environ, {}, clear=True), patch.object(install, '__version__', '2.0.2'):
37
+ self.assertEqual(install.resolve_requested_tag(), 'plugin-kit-ai-v2.0.2')
38
+ with patch.object(install, 'fetch_text', return_value='{"tag_name":"agentplugins-v0.1.62"}'):
39
+ with self.assertRaises(ValueError):
40
+ install.latest_tag(install.DEFAULT_API_BASE, install.DEFAULT_REPOSITORY)
41
+
42
+ def test_cold_cache_and_checksum(self):
43
+ platform = install.detect_platform()
44
+ stream = io.BytesIO()
45
+ binary = b'fixture executable bytes'
46
+ with tarfile.open(fileobj=stream, mode='w:gz') as archive:
47
+ member = tarfile.TarInfo(platform.binary_name)
48
+ member.size = len(binary)
49
+ archive.addfile(member, io.BytesIO(binary))
50
+ body = stream.getvalue()
51
+ for raw in ['2.0.2', 'plugin-kit-ai-v2.0.2', '1.2.4', 'v1.2.4']:
52
+ for bad_checksum in [False, True]:
53
+ with self.subTest(raw=raw, bad_checksum=bad_checksum), tempfile.TemporaryDirectory() as root:
54
+ tag = install.normalize_tag(raw)
55
+ version = install.version_from_tag(tag)
56
+ asset = install.asset_name_for_version(version, platform)
57
+ base = f'https://github.com/{install.DEFAULT_REPOSITORY}/releases/download/{tag}'
58
+ checksum = '0' * 64 if bad_checksum else hashlib.sha256(body).hexdigest()
59
+ def fetch(url, accept_json=False):
60
+ self.assertIn(url, [f'{base}/checksums.txt', f'{base}/{asset}'])
61
+ return f'{checksum} {asset}\n'.encode() if url.endswith('/checksums.txt') else body
62
+ with patch.dict(os.environ, {'PLUGIN_KIT_AI_VERSION': raw,
63
+ 'PLUGIN_KIT_AI_CACHE_DIR': root}, clear=True), \
64
+ patch.object(install, 'fetch_bytes', side_effect=fetch) as downloaded:
65
+ if bad_checksum:
66
+ with self.assertRaisesRegex(RuntimeError, 'checksum mismatch'):
67
+ install.ensure_installed(quiet=True)
68
+ self.assertFalse((Path(root) / tag / platform.binary_name).exists())
69
+ else:
70
+ result = install.ensure_installed(quiet=True)
71
+ self.assertEqual(result['version'], version)
72
+ self.assertEqual(Path(result['installed_binary']).read_bytes(), binary)
73
+ self.assertEqual(downloaded.call_count, 2)
74
+ install.ensure_installed(quiet=True)
75
+ self.assertEqual(downloaded.call_count, 2)
76
+
77
+
78
+ if __name__ == '__main__':
79
+ unittest.main()
File without changes
File without changes
File without changes