Package Version Checker Design Document¶
Specification for a reusable package version checking utility with flexible strategies and clear reporting.
Overview¶
This document outlines the design for a robust, reusable package version checking utility that can verify versions for any Python package against minimum requirements, latest versions, or specific version constraints.
The utility provides a simple, flexible API for checking package versions with configurable behavior for different compatibility requirements. It's designed to support both simple version checks and sophisticated version difference monitoring.
Context¶
Modern Python applications often depend on rapidly evolving libraries. Ensuring compatibility with these dependencies requires monitoring version differences and enforcing version requirements. This is especially important for libraries with frequent updates and API changes, such as OpenAI's client libraries.
Existing approaches often lack flexibility in defining what constitutes a "compatible" version, particularly when dealing with semantic versioning differences at various levels (major, minor, patch).
Goals¶
- Create a modular, extensible version checking system that works with any Python package
- Provide both programmatic and CLI interfaces
- Support multiple checking strategies (minimum version, exact version, version range)
- Include robust error handling with configurable failure modes
- Maintain clean separation of concerns
- Support caching to minimize network requests
- Provide clear, actionable feedback to users
- Allow fine-grained control over version difference thresholds
Design¶
Core Components¶
classDiagram
class PackageVersionChecker {
+check_version(package_name, config) Result
}
class VersionCheckerConfig {
+fail_on_error: bool
+cache_duration: int
+network_timeout: int
+strategy: VersionStrategy
+requirement: str
}
class VersionStrategy {
<<enumeration>>
MINIMUM
EXACT
LATEST
RANGE
VERSION_DIFF
}
class PackageInfo {
+name: str
+installed_version: Version
+required_version: Version
+latest_version: Version
}
class Result {
+is_compatible: bool
+needs_update: bool
+package_info: PackageInfo
+error: Optional[str]
+get_upgrade_command() str
}
class VersionProvider {
<<interface>>
+get_installed_version(package_name) Version
+get_latest_version(package_name) Version
}
class StandardVersionProvider {
+get_installed_version(package_name) Version
+get_latest_version(package_name) Version
}
class VersionCache {
+get(package_name) CachedVersion
+set(package_name, version) None
+is_valid(package_name) bool
}
PackageVersionChecker --> VersionCheckerConfig : uses
PackageVersionChecker --> VersionProvider : uses
PackageVersionChecker --> Result : produces
Result *-- PackageInfo
VersionCheckerConfig *-- VersionStrategy
VersionProvider <|-- StandardVersionProvider
StandardVersionProvider --> VersionCache : uses
Key Classes¶
1. PackageVersionChecker¶
The main entry point for version checking functionality:
class PackageVersionChecker:
def __init__(self,
provider: Optional[VersionProvider] = None,
cache: Optional[VersionCache] = None):
self.provider = provider or StandardVersionProvider()
self.cache = cache or VersionCache()
def check_version(self,
package_name: str,
config: Optional[VersionCheckerConfig] = None) -> Result:
"""Check if package meets version requirements based on config."""
# Implementation
2. VersionCheckerConfig¶
Configuration for version checking behavior:
class VersionCheckerConfig:
def __init__(self,
strategy: VersionStrategy = VersionStrategy.MINIMUM,
requirement: str = "",
fail_on_error: bool = False,
cache_duration: int = 3600, # 1 hour
network_timeout: int = 5, # seconds
vdiff_warn_matrix: Optional[str] = None,
vdiff_fail_matrix: Optional[str] = None):
"""
Initialize version checker configuration.
Args:
strategy: Version checking strategy to use
requirement: Version requirement string (for MINIMUM, EXACT, RANGE strategies)
fail_on_error: Whether to raise exceptions on errors
cache_duration: How long to cache PyPI results in seconds
network_timeout: Network request timeout in seconds
vdiff_warn_matrix: Version difference matrix for warnings [major.minor.micro]
Use * for any/infinity, e.g. "1.5.*"
vdiff_fail_matrix: Version difference matrix for failures [major.minor.micro]
Use * for any/infinity, e.g. "0.20.*"
"""
self.strategy = strategy
self.requirement = requirement
self.fail_on_error = fail_on_error
self.cache_duration = cache_duration
self.network_timeout = network_timeout
# Version difference matrices
self.vdiff_warn_matrix = vdiff_warn_matrix
self.vdiff_fail_matrix = vdiff_fail_matrix
def parse_vdiff_matrix(self, matrix_str: Optional[str]) -> Tuple[Optional[int], Optional[int], Optional[int]]:
"""
Parse a version difference matrix string into component thresholds.
Args:
matrix_str: String in format "major.minor.micro" where each component
is either a number or * for infinity
Returns:
Tuple of (major_diff, minor_diff, micro_diff) thresholds
Examples:
"1.5.*" -> (1, 5, None) # Major diff > 1 or minor diff > 5, ignore micro
"0.0.0" -> (0, 0, 0) # Any difference triggers
"*.*.*" -> (None, None, None) # No difference triggers
"""
if not matrix_str:
return None, None, None
parts = matrix_str.split(".")
if len(parts) != 3:
raise ValueError(f"Invalid version difference matrix: {matrix_str}. "
f"Format should be 'major.minor.micro'")
thresholds = []
for part in parts:
if part == "*":
thresholds.append(None) # None represents infinity/any
else:
try:
thresholds.append(int(part))
except ValueError:
raise ValueError(f"Invalid version component in matrix: {part}. "
f"Must be a number or *")
return tuple(thresholds) # type: Tuple[Optional[int], Optional[int], Optional[int]]
3. Result¶
Structured result of version check operations:
class Result:
def __init__(self,
is_compatible: bool,
needs_update: bool,
package_info: PackageInfo,
error: Optional[str] = None,
warning_level: Optional[str] = None,
diff_details: Optional[Dict[str, int]] = None):
self.is_compatible = is_compatible
self.needs_update = needs_update
self.package_info = package_info
self.error = error
self.warning_level = warning_level # e.g., "MAJOR", "MINOR", "MICRO"
self.diff_details = diff_details # e.g., {"major": 1, "minor": 5}
def get_upgrade_command(self) -> str:
"""Return pip command to upgrade package."""
# Implementation
Implementation Phases¶
Phase 1: Core Implementation¶
- Define all interfaces and data classes
- Implement
StandardVersionProvider(using importlib.metadata and PyPI API) - Implement basic caching mechanism
- Implement core
PackageVersionCheckerfunctionality - Add support for minimum version strategy
Phase 2: Enhanced Functionality¶
- Add support for all version strategies
- Implement robust error handling
- Add configurable logging
- Implement cache invalidation
- Add utility functions for common use cases
Phase 3: CLI and Integration¶
- Create CLI interface
- Add support for requirements.txt parsing
- Implement environment-aware version checking
- Add documentation and examples
- Create integration tests
Implementation Plan¶
Module Structure¶
src/tnh_scholar/utils/
├── version_check/
│ ├── __init__.py
│ ├── checker.py # Main PackageVersionChecker class
│ ├── config.py # Configuration classes
│ ├── providers.py # Version provider implementations
│ ├── cache.py # Caching mechanisms
│ ├── models.py # Data models (Result, PackageInfo)
│ ├── strategies.py # Version comparison strategies
│ └── cli.py # Command-line interface
└── __init__.py
Implementation Details¶
Version Retrieval¶
We'll build on the existing implementation, but make it more generic:
def get_installed_version(package_name: str) -> Version:
"""Get installed package version."""
try:
if version_str := str(importlib.metadata.version(package_name)):
return Version(version_str)
else:
raise InvalidVersion(f"{package_name} version string is empty")
except importlib.metadata.PackageNotFoundError as e:
raise ImportError(f"{package_name} is not installed") from e
except InvalidVersion as e:
raise InvalidVersion(f"Invalid version for {package_name}: {e}") from e
Version Comparison¶
Version strategies will use comparison operators from packaging.version:
def check_minimum_version(installed: Version,
required: Version) -> bool:
"""Check if installed version meets minimum requirement."""
return installed >= required
def check_compatible_version(installed: Version,
required: Version) -> bool:
"""Check if installed version is compatible with requirement."""
# Example: versions like 2.1.x are compatible with 2.1.0
return (installed.major == required.major and
installed.minor == required.minor and
installed.micro >= required.micro)
def check_version_diff(installed: Version,
reference: Version,
vdiff_matrix: Optional[str] = None) -> Tuple[bool, Dict[str, int]]:
"""
Check if version difference is within specified limits.
Args:
installed: The installed version
reference: The reference version to compare against (typically latest)
vdiff_matrix: Version difference matrix string in format "major.minor.micro"
Returns:
Tuple of (is_within_limits, diff_details)
where diff_details is a dict with actual differences
"""
# Calculate actual differences
major_diff = abs(reference.major - installed.major)
minor_diff = abs(reference.minor - installed.minor) if reference.major == installed.major else 0
micro_diff = abs(reference.micro - installed.micro) if (reference.major == installed.major and
reference.minor == installed.minor) else 0
diff_details = {
"major": major_diff,
"minor": minor_diff,
"micro": micro_diff
}
# If no matrix provided, differences are acceptable
if not vdiff_matrix:
return True, diff_details
# Parse matrix
major_limit, minor_limit, micro_limit = parse_vdiff_matrix(vdiff_matrix)
# Check limits (None means no limit)
if major_limit is not None and major_diff > major_limit:
return False, diff_details
if minor_limit is not None and minor_diff > minor_limit:
return False, diff_details
if micro_limit is not None and micro_diff > micro_limit:
return False, diff_details
return True, diff_details
def parse_vdiff_matrix(matrix_str: str) -> Tuple[Optional[int], Optional[int], Optional[int]]:
"""
Parse a version difference matrix string.
Args:
matrix_str: String in format "major.minor.micro"
Returns:
Tuple of (major_limit, minor_limit, micro_limit)
where None represents no limit
"""
parts = matrix_str.split(".")
if len(parts) != 3:
raise ValueError(f"Invalid version difference matrix: {matrix_str}")
limits = []
for part in parts:
if part == "*":
limits.append(None) # No limit
else:
try:
limits.append(int(part))
except ValueError:
raise ValueError(f"Invalid version component: {part}")
return tuple(limits) # type: Tuple[Optional[int], Optional[int], Optional[int]]
Caching Mechanism¶
Simple time-based caching for PyPI results:
class VersionCache:
def __init__(self, cache_duration: int = 3600):
self.cache = {}
self.timestamps = {}
self.cache_duration = cache_duration
def get(self, key: str) -> Optional[Version]:
"""Get cached version if still valid."""
if not self.is_valid(key):
return None
return self.cache.get(key)
def set(self, key: str, value: Version) -> None:
"""Cache version with current timestamp."""
self.cache[key] = value
self.timestamps[key] = time.time()
def is_valid(self, key: str) -> bool:
"""Check if cached value is still valid."""
if key not in self.timestamps:
return False
age = time.time() - self.timestamps[key]
return age < self.cache_duration
Error Handling¶
Robust error handling with configurable behavior:
def handle_error(error: Exception,
config: VersionCheckerConfig,
package_name: str) -> Result:
"""Handle errors based on configuration."""
error_message = str(error)
logger.error(f"Error checking {package_name} version: {error_message}")
if config.fail_on_error:
raise error
return Result(
is_compatible=False,
needs_update=False,
package_info=PackageInfo(name=package_name),
error=error_message
)
Usage Examples¶
Basic Usage¶
from tnh_scholar.utils.version_check import PackageVersionChecker, VersionCheckerConfig, VersionStrategy
# Check if package meets minimum version
checker = PackageVersionChecker()
result = checker.check_version(
"openai",
VersionCheckerConfig(
strategy=VersionStrategy.MINIMUM,
requirement="1.70.0"
)
)
if not result.is_compatible:
print(f"openai package version {result.package_info.installed_version} "
f"does not meet minimum requirement {result.package_info.required_version}")
print(f"Upgrade with: {result.get_upgrade_command()}")
Integration with Whisper Service¶
def __init__(self, api_key: Optional[str] = None, **config_options):
# Check OpenAI package version
checker = PackageVersionChecker()
result = checker.check_version(
"openai",
VersionCheckerConfig(
strategy=VersionStrategy.MINIMUM,
requirement="1.70.0",
fail_on_error=False
)
)
if not result.is_compatible:
logger.warning(
f"OpenAI package version {result.package_info.installed_version} "
f"is older than required version {result.package_info.required_version}. "
f"Some features may not work as expected. "
f"Please upgrade using: {result.get_upgrade_command()}"
)
# Continue with initialization
self.config = WhisperConfig()
# ...
Command-line Interface¶
usage: version-check [-h] [--min MIN_VERSION] [--exact EXACT_VERSION]
[--vdiff-warn VDIFF_WARN_MATRIX]
[--vdiff-fail VDIFF_FAIL_MATRIX]
[--fail-on-error] [--timeout TIMEOUT] [--no-cache]
PACKAGE_NAME
Check if a Python package meets version requirements.
positional arguments:
PACKAGE_NAME Name of the package to check
optional arguments:
-h, --help show this help message and exit
--min MIN_VERSION Minimum required version
--exact EXACT_VERSION Exact required version
--vdiff-warn VDIFF_WARN_MATRIX
Version difference matrix for warnings in format major.minor.micro
Use * for any/infinity, e.g. "1.5.*"
--vdiff-fail VDIFF_FAIL_MATRIX
Version difference matrix for failures in format major.minor.micro
Use * for any/infinity, e.g. "0.20.*"
--fail-on-error Exit with error code on version mismatch
--timeout TIMEOUT Network timeout in seconds
--no-cache Bypass cache and check PyPI directly
Implementation Examples¶
Here's a simplified sketch of the key implementation components:
def check_version(
package_name: str,
config: VersionCheckerConfig
) -> Result:
"""Main version checking function."""
try:
installed = get_installed_version(package_name)
latest = get_latest_version(package_name, config.network_timeout)
# Determine compatibility based on strategy
is_compatible = True
needs_update = False
warning_level = None
diff_details = None
if config.strategy == VersionStrategy.MINIMUM:
required = Version(config.requirement)
is_compatible = installed >= required
needs_update = installed < latest
elif config.strategy == VersionStrategy.EXACT:
required = Version(config.requirement)
is_compatible = installed == required
needs_update = installed != required
elif config.strategy == VersionStrategy.VERSION_DIFF:
# Check warning threshold
if config.vdiff_warn_matrix:
warn_within_limits, diff_details = check_version_diff(
installed, latest, config.vdiff_warn_matrix)
if not warn_within_limits:
warning_level = determine_warning_level(diff_details)
# Check failure threshold
if config.vdiff_fail_matrix:
fail_within_limits, diff_details = check_version_diff(
installed, latest, config.vdiff_fail_matrix)
is_compatible = fail_within_limits
needs_update = installed < latest
# Create package info
package_info = PackageInfo(
name=package_name,
installed_version=str(installed),
latest_version=str(latest),
required_version=config.requirement if hasattr(config, 'requirement') else None
)
# Create and return result
return Result(
is_compatible=is_compatible,
needs_update=needs_update,
package_info=package_info,
warning_level=warning_level,
diff_details=diff_details
)
except Exception as e:
# Handle errors based on configuration
if config.fail_on_error:
raise
return Result(
is_compatible=False,
needs_update=False,
package_info=PackageInfo(name=package_name),
error=str(e)
)
Extensibility¶
The design supports these extension points:
-
New Version Strategies: Adding new comparison strategies is as simple as extending the
VersionStrategyenum and implementing the corresponding check in thecheck_versionmethod. -
Custom Version Providers: The separation of version retrieval logic allows for swapping in alternative providers for handling private repositories, alternate package indexes, or specialized version parsing.
-
Advanced Caching: The caching system can be extended to provide persistent caching, environment-aware caching, or more sophisticated invalidation strategies.
-
Result Processing: The detailed
Resultobject can be used to generate different types of reports, integrate with monitoring systems, or enforce custom policies.
Future Enhancements¶
- Support for version ranges with upper bounds
- Integration with virtual environment detection
- Cache persistence across runs
- Support for private PyPI repositories
- Batch checking of multiple packages
- Integration with dependency resolution
- Support for pre-release versions
- More sophisticated version difference metrics (e.g., weighted differences)
- Configurable version comparison for non-semver packages
- Support for checking against specific release channels (stable, beta, etc.)
- Integration with package security advisories
Conclusion¶
This version checking utility will provide a robust, flexible solution for ensuring package compatibility across the TNH Scholar project. By building on the existing YTDVersionChecker implementation but making it more generic and configurable, it will help prevent runtime errors and improve user experience.