Skip to content

Commit 270ef96

Browse files
committed
fix: version handling with extraction and validation utility
Now properly handles: ``` "@standardnotes/[email protected]" → "3.198.1" "[email protected]" → "1.2.3" "v1.2.3" → "1.2.3" ``` - Introduced `extract_and_validate_version` utility for consistent version extraction and validation across modules. - Updated `GitHubReleaseFetcher` to use the new utility for normalizing version tags. - Modified `CatalogInstallStrategy` and `URLInstallStrategy` to leverage the utility for extracting and validating version data. - Added comprehensive version handling functions in `utils.py`: `extract_version_from_package_string`, `sanitize_version_string`, and `validate_version_string`.
1 parent 8ed36af commit 270ef96

File tree

4 files changed

+107
-12
lines changed

4 files changed

+107
-12
lines changed

my_unicorn/github_client.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import aiohttp
1111

1212
from .auth import GitHubAuthManager, auth_manager
13+
from .utils import extract_and_validate_version
1314

1415

1516
class GitHubAsset(TypedDict):
@@ -46,16 +47,28 @@ def __init__(self, owner: str, repo: str, session: aiohttp.ClientSession) -> Non
4647
self.session = session
4748

4849
def _normalize_version(self, tag_name: str) -> str:
49-
"""Normalize version by stripping 'v' prefix.
50+
"""Normalize version by extracting and sanitizing version string.
51+
52+
Handles various formats including package@version and v-prefixed versions.
5053
5154
Args:
52-
tag_name: Version tag that may have 'v' prefix
55+
tag_name: Version tag that may have 'v' prefix or package format
5356
5457
Returns:
55-
Version string without 'v' prefix
58+
Sanitized version string
5659
5760
"""
58-
return tag_name.lstrip("v") if tag_name else ""
61+
if not tag_name:
62+
return ""
63+
64+
# Use the comprehensive version extraction and validation
65+
normalized = extract_and_validate_version(tag_name)
66+
67+
# Fall back to original logic if extraction fails
68+
if normalized is None:
69+
return tag_name.lstrip("v")
70+
71+
return normalized
5972

6073
async def fetch_latest_release(self) -> GitHubReleaseDetails:
6174
"""Fetch the latest release information from GitHub API.

my_unicorn/strategies/install_catalog.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from my_unicorn.download import IconAsset
1111

1212
from ..logger import get_logger
13+
from ..utils import extract_and_validate_version
1314
from .install_url import InstallationError, InstallStrategy, ValidationError
1415

1516
logger = get_logger(__name__)
@@ -260,7 +261,7 @@ async def _install_single_app(
260261
"path": str(final_path),
261262
"name": final_path.name,
262263
"source": "catalog",
263-
"version": release_data.get("tag_name"),
264+
"version": extract_and_validate_version(release_data.get("tag_name", "")),
264265
"icon_path": str(icon_path) if icon_path else None,
265266
}
266267

@@ -568,7 +569,8 @@ def _create_app_config(
568569
"config_version": "1.0.0",
569570
"source": "catalog",
570571
"appimage": {
571-
"version": release_data.get("tag_name", "unknown"),
572+
"version": extract_and_validate_version(release_data.get("tag_name", ""))
573+
or "unknown",
572574
"name": app_path.name,
573575
"rename": appimage_config.get("rename", app_name),
574576
"name_template": appimage_config.get("name_template", ""),

my_unicorn/strategies/install_url.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@
77
from pathlib import Path
88
from typing import Any
99

10-
from my_unicorn.download import IconAsset
11-
1210
from ..github_client import (
1311
GitHubAsset,
1412
GitHubClient,
1513
GitHubReleaseDetails,
1614
GitHubReleaseFetcher,
1715
)
1816
from ..logger import get_logger
17+
from ..utils import extract_and_validate_version
1918
from ..verify import Verifier
2019
from .install import InstallationError, InstallStrategy, ValidationError
2120

@@ -219,7 +218,7 @@ async def _install_single_repo(
219218
"path": str(final_path),
220219
"name": final_path.name,
221220
"source": "url",
222-
"version": release_data.get("tag_name"),
221+
"version": extract_and_validate_version(release_data.get("tag_name", "")),
223222
"icon_path": str(icon_path) if icon_path else None,
224223
}
225224

@@ -315,7 +314,8 @@ async def _create_app_config(
315314
"config_version": "1.0.0",
316315
"source": "url",
317316
"appimage": {
318-
"version": release_data.get("version", "unknown"),
317+
"version": extract_and_validate_version(release_data.get("version", ""))
318+
or "unknown",
319319
"name": app_path.name,
320320
"rename": app_name,
321321
"name_template": "",

my_unicorn/utils.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,73 @@ def expand_template(template: str, variables: dict[str, Any]) -> str:
130130
return template
131131

132132

133+
def extract_version_from_package_string(package_string: str) -> str | None:
134+
"""Extract version from package identifier string.
135+
136+
Handles formats like:
137+
- "@standardnotes/[email protected]" -> "3.198.1"
138+
- "[email protected]" -> "1.2.3"
139+
- "v1.2.3" -> "1.2.3"
140+
- "1.2.3" -> "1.2.3"
141+
142+
Args:
143+
package_string: Package string that may contain version
144+
145+
Returns:
146+
Extracted version string or None if not found
147+
148+
"""
149+
if not package_string:
150+
return None
151+
152+
# Handle package@version format
153+
if "@" in package_string:
154+
# Split by @ and take the last part (version)
155+
parts = package_string.split("@")
156+
if len(parts) >= 2:
157+
version_part = parts[-1]
158+
# Clean up the version part
159+
version_part = version_part.strip()
160+
if version_part:
161+
return sanitize_version_string(version_part)
162+
163+
# Handle direct version strings
164+
return sanitize_version_string(package_string)
165+
166+
167+
def sanitize_version_string(version: str) -> str:
168+
"""Sanitize version string by removing invalid characters and prefixes.
169+
170+
Args:
171+
version: Raw version string
172+
173+
Returns:
174+
Sanitized version string
175+
176+
"""
177+
if not version:
178+
return ""
179+
180+
# Remove common prefixes
181+
version = version.lstrip("v")
182+
183+
# Remove any remaining @ symbols that might be present
184+
version = version.replace("@", "")
185+
186+
# Remove quotes and other problematic characters for JSON
187+
version = version.strip("\"'")
188+
189+
# Remove any trailing/leading whitespace
190+
version = version.strip()
191+
192+
return version
193+
194+
133195
def validate_version_string(version: str) -> bool:
134196
"""Validate version string format.
135197
136198
Args:
137-
version: Version string to validate
199+
version: Version string to validate (should be pre-sanitized)
138200
139201
Returns:
140202
True if valid version format
@@ -143,14 +205,32 @@ def validate_version_string(version: str) -> bool:
143205
if not version:
144206
return False
145207

146-
# Remove common prefixes
208+
# The version should already be sanitized, but ensure no prefixes
147209
version = version.lstrip("v")
148210

149211
# Check semantic version pattern (major.minor.patch with optional pre-release)
150212
pattern = r"^\d+(\.\d+)*(-[a-zA-Z0-9.-]+)?$"
151213
return bool(re.match(pattern, version))
152214

153215

216+
def extract_and_validate_version(package_string: str) -> str | None:
217+
"""Extract and validate version from package string.
218+
219+
Combines extraction, sanitization, and validation in one function.
220+
221+
Args:
222+
package_string: Package string that may contain version
223+
224+
Returns:
225+
Valid version string or None if extraction/validation fails
226+
227+
"""
228+
extracted_version = extract_version_from_package_string(package_string)
229+
if extracted_version and validate_version_string(extracted_version):
230+
return extracted_version
231+
return None
232+
233+
154234
def create_desktop_entry_name(app_name: str) -> str:
155235
"""Create desktop entry name from app name.
156236

0 commit comments

Comments
 (0)