API Reference¶
This document provides detailed API documentation for Zaojun. The API is designed for programmatic use and integration with other Python tools, including the new caching system for improved performance.
Overview¶
Zaojun provides a simple Python API for checking dependency versions. The main functionality is available through the check() function, but individual components can also be used separately.
Main Functions¶
check()¶
The main entry point for checking dependencies.
def check(
pyproject_toml: Path = Path("./pyproject.toml"),
short: bool = False,
groups: bool = False,
compat_ok: bool = False,
cache: bool = False,
clear_cache: bool = False,
cache_stats: bool = False,
) -> None:
"""Check listed dependencies against latest versions available on Pypi.org.
Args:
pyproject_toml: Full path to pyproject.toml file to check.
short: Should only the shortest possible output be produced.
groups: Should dependencies in groups be checked.
compat_ok: Compatible versions are considered OK and won't cause an exit code of 1.
cache: Enable caching of PyPI API responses (default: False).
clear_cache: Clear all cache entries before checking.
cache_stats: Show cache statistics after checking.
Returns:
None, but exits with code 0 (success) or 1 (updates needed).
Raises:
FileNotFoundError: If pyproject_toml file doesn't exist.
tomllib.TOMLDecodeError: If pyproject_toml contains invalid TOML.
ValueError: If dependencies have invalid format.
"""
Example Usage:
from pathlib import Path
from zaojun import check
# Basic check
check()
# Check with all options
check(
pyproject_toml=Path("/path/to/project/pyproject.toml"),
short=True,
groups=True,
compat_ok=True,
cache=True,
clear_cache=False,
cache_stats=True,
)
process_dependencies()¶
Process a list of dependencies and check each one.
def process_dependencies(
dependencies: list[Any],
short: bool,
compat_ok: bool,
client: httpx.Client,
cache: PyPICache | None = None,
) -> bool:
"""Check all dependencies for updates.
Args:
dependencies: List of dependency strings to check.
short: Should only the shortest possible output be produced.
compat_ok: Compatible versions are considered OK.
client: HTTP client for making requests to PyPI.
cache: Optional PyPICache instance for caching PyPI responses.
Returns:
True if incompatible update is available or if compat_ok is False
on any update available.
"""
Example Usage:
import httpx
from zaojun import process_dependencies
client = httpx.Client()
dependencies = ["httpx~=0.28.1", "packaging~=26.0"]
needs_update = process_dependencies(
dependencies=dependencies,
short=False,
compat_ok=False,
client=client,
cache=None,
)
client.close()
check_dependency()¶
Check if a single dependency needs an update.
def check_dependency(
dependency: str,
short: bool,
compat_ok: bool,
client: httpx.Client,
cache: PyPICache | None = None,
) -> bool:
"""Check if a dependency needs an update.
Args:
dependency: Dependency string to check.
short: Should only the shortest possible output be produced.
compat_ok: Compatible versions are considered OK.
client: HTTP client for making requests to PyPI.
cache: Optional PyPICache instance for caching PyPI responses.
Returns:
True if newer version of dependency is available and that version
is outside version spec or compat_ok is set to false and newer
version is within version spec.
Raises:
ValueError: If dependency has invalid format.
httpx.HTTPStatusError: If PyPI API request fails.
httpx.RequestError: If network error occurs.
"""
Example Usage:
import httpx
from zaojun import check_dependency
client = httpx.Client()
# Check a single dependency
needs_update = check_dependency(
dependency="httpx~=0.28.1",
short=False,
compat_ok=False,
client=client,
cache=None,
)
client.close()
parse_dependency()¶
Parse a dependency string into its components.
def parse_dependency(dependency: str) -> tuple[str, str, str | None]:
"""Parse a dependency string into (package_name, version_constraint, environment_marker).
Args:
dependency: Dependency string to parse.
Returns:
Tuple containing:
- package_name: Name of the package
- version_constraint: Version specification string
- environment_marker: Environment marker or None
Raises:
ValueError: If dependency has invalid format.
"""
Example Usage:
from zaojun import parse_dependency
# Parse a simple dependency
package_name, version_spec, marker = parse_dependency("httpx~=0.28.1")
# Returns: ("httpx", "~=0.28.1", None)
# Parse with environment marker
package_name, version_spec, marker = parse_dependency(
"package>=1.0; python_version>='3.8'"
)
# Returns: ("package", ">=1.0", "python_version>='3.8'")
is_version_compatible()¶
Check if a version is compatible with constraints.
def is_version_compatible(current_spec: str, latest_version: str) -> bool:
"""Check if the latest version satisfies the current version constraints.
Args:
current_spec: Version constraint string.
latest_version: Latest version to check.
Returns:
True if latest_version satisfies current_spec, False otherwise.
"""
Example Usage:
from zaojun import is_version_compatible
# Check compatibility
compatible = is_version_compatible("~=1.2.3", "1.2.5")
# Returns: True (1.2.5 satisfies ~=1.2.3)
compatible = is_version_compatible("==1.2.3", "1.2.5")
# Returns: False (1.2.5 doesn't satisfy ==1.2.3)
is_latest_version()¶
Check if the latest version has been specified.
def is_latest_version(current_spec: str, latest_version: str) -> bool:
"""Check if the latest version has been specified.
Args:
current_spec: Version constraint string.
latest_version: Latest version to check.
Returns:
True if latest_version is explicitly specified in current_spec.
"""
Example Usage:
from zaojun import is_latest_version
# Check if latest is specified
is_latest = is_latest_version("==1.2.3", "1.2.3")
# Returns: True (1.2.3 is explicitly specified)
is_latest = is_latest_version(">=1.2.3", "1.2.5")
# Returns: False (1.2.5 is not explicitly specified)
is_local_dependency()¶
Check if a dependency is local or URL-based.
def is_local_dependency(dependency: str) -> bool:
"""Check if dependency is a local path or URL.
Args:
dependency: Dependency string to check.
Returns:
True if dependency is local or URL-based, False otherwise.
"""
Example Usage:
from zaojun import is_local_dependency
# Check local dependencies
is_local = is_local_dependency("./local-package")
# Returns: True
is_local = is_local_dependency("package @ https://example.com/pkg.zip")
# Returns: True
is_local = is_local_dependency("package==1.0.0")
# Returns: False
get_latest_pypi_version()¶
Get the latest version of a package from PyPI.
def get_latest_pypi_version(package_name: str, client: httpx.Client) -> str:
"""Get latest version number of a package on Pypi.org.
Args:
package_name: Name of the package to check.
client: HTTP client for making requests to PyPI.
Returns:
Latest version string from PyPI.
Raises:
httpx.HTTPStatusError: If PyPI API request fails.
httpx.RequestError: If network error occurs.
"""
Example Usage:
import httpx
from zaojun import get_latest_pypi_version
client = httpx.Client()
# Get latest version
latest_version = get_latest_pypi_version("httpx", client, cache=None)
# Returns: e.g., "0.28.1"
client.close()
CLI Interface¶
main()¶
The CLI entry point.
Example Usage:
from zaojun import main
# This is equivalent to running `zaojun` from command line
if __name__ == "__main__":
main()
app¶
The Cyclopts application instance.
Example Usage:
Cache API¶
PyPICache Class¶
The PyPICache class provides caching functionality for PyPI API responses to reduce network calls and improve performance.
class PyPICache:
"""Cache for PyPI API responses with TTL-based expiration.
Args:
cache_dir: Directory to store cache files. If None, uses
platform-specific cache directory.
ttl_hours: Time-to-live for cache entries in hours.
enabled: Whether caching is enabled.
"""
def __init__(
self,
cache_dir: Path | None = None,
ttl_hours: int = 24,
enabled: bool = True,
):
"""Initialize the PyPI cache."""
def get(self, package_name: str) -> dict | None:
"""Get cached data for a package.
Args:
package_name: Name of the package.
Returns:
Cached data dictionary or None if not found/expired.
"""
def set(self, package_name: str, data: dict) -> bool:
"""Cache data for a package.
Args:
package_name: Name of the package.
data: Data to cache.
Returns:
True if successful, False otherwise.
"""
def clear(self, package_name: str | None = None) -> int:
"""Clear cache entries.
Args:
package_name: If specified, clear only this package's cache.
If None, clear all cache entries.
Returns:
Number of cache entries cleared.
"""
def get_stats(self) -> dict:
"""Get cache statistics.
Returns:
Dictionary with cache statistics.
"""
def is_expired(self, package_name: str) -> bool:
"""Check if a cache entry exists and is expired.
Args:
package_name: Name of the package.
Returns:
True if cache entry exists and is expired, False otherwise.
"""
get_default_cache()¶
def get_default_cache() -> PyPICache:
"""Get the default cache instance.
Returns:
Default PyPICache instance.
"""
Example Usage:
from zaojun.cache import PyPICache, get_default_cache
# Get default cache
cache = get_default_cache()
# Or create custom cache
custom_cache = PyPICache(
cache_dir=Path("/tmp/my-cache"),
ttl_hours=12,
enabled=True,
)
# Use cache with API functions
from zaojun import get_latest_pypi_version
import httpx
client = httpx.Client()
latest_version = get_latest_pypi_version("httpx", client, cache=cache)
client.close()
# Check cache statistics
stats = cache.get_stats()
print(f"Cache hits: {stats['hits']}, misses: {stats['misses']}")
# Clear cache
cleared = cache.clear()
print(f"Cleared {cleared} cache entries")
Types and Constants¶
Import Statements¶
from pathlib import Path
from typing import Annotated, Any
import httpx
import tomllib
from cyclopts import App, Parameter
from packaging.requirements import InvalidRequirement, Requirement
from packaging.specifiers import SpecifierSet
from packaging.version import InvalidVersion, parse as parse_version
from .cache import PyPICache
Type Aliases¶
The API uses standard Python types:
- Path for file paths
- bool for flags
- str for strings
- list[Any] for dependency lists
- tuple[str, str, str | None] for parsed dependencies
- dict for cache data and statistics
- PyPICache | None for optional cache instances
Error Handling¶
Common Exceptions¶
ValueError: Raised for invalid dependency formatsFileNotFoundError: Raised when pyproject.toml doesn't existtomllib.TOMLDecodeError: Raised for invalid TOMLhttpx.HTTPStatusError: Raised for HTTP errors from PyPIhttpx.RequestError: Raised for network errors
Error Recovery¶
import httpx
from zaojun import check_dependency
client = httpx.Client()
try:
needs_update = check_dependency(
dependency="invalid-package-format",
short=False,
compat_ok=False,
client=client,
cache=None,
)
except ValueError as e:
print(f"Invalid dependency: {e}")
except httpx.HTTPStatusError as e:
print(f"PyPI API error: {e}")
except httpx.RequestError as e:
print(f"Network error: {e}")
finally:
client.close()
Cache-Specific Error Handling¶
from zaojun.cache import PyPICache
cache = PyPICache()
try:
# Cache operations
cache.set("package", {"version": "1.0.0"})
data = cache.get("package")
cleared = cache.clear()
except (OSError, json.JSONDecodeError) as e:
print(f"Cache error: {e}")
# Cache automatically handles corrupted files
Cache Performance Considerations¶
Cache Configuration¶
from zaojun.cache import PyPICache
from pathlib import Path
# Custom cache configuration
cache = PyPICache(
cache_dir=Path.home() / ".myapp" / "cache", # Custom location
ttl_hours=48, # Longer TTL for stable packages
enabled=True, # Enable caching
)
# Monitor cache effectiveness
stats = cache.get_stats()
if stats["hits"] > stats["misses"] * 2:
print("Cache is effective (more hits than misses)")
else:
print("Consider adjusting TTL or cache strategy")
Cache Integration Example¶
"""Advanced cache integration example."""
import httpx
from pathlib import Path
from zaojun.cache import PyPICache
from zaojun import get_latest_pypi_version
class CachedDependencyChecker:
"""Dependency checker with intelligent caching."""
def __init__(self, cache_dir: Path | None = None):
self.cache = PyPICache(cache_dir=cache_dir, ttl_hours=24)
self.client = httpx.Client()
def get_version_with_cache(self, package_name: str) -> str:
"""Get package version with cache-first strategy."""
# Try cache first
cached = self.cache.get(package_name)
if cached and "version" in cached:
print(f"Cache hit for {package_name}")
return cached["version"]
# Fetch from PyPI
print(f"Cache miss for {package_name}, fetching from PyPI")
version = get_latest_pypi_version(
package_name,
self.client,
cache=self.cache
)
return version
def check_packages(self, packages: list[str]):
"""Check multiple packages with cache statistics."""
for package in packages:
version = self.get_version_with_cache(package)
print(f"{package}: {version}")
# Show cache performance
stats = self.cache.get_stats()
print(f"\nCache Statistics:")
print(f" Hits: {stats['hits']}")
print(f" Misses: {stats['misses']}")
print(f" Hit rate: {stats['hits'] / (stats['hits'] + stats['misses']):.1%}")
def close(self):
"""Clean up resources."""
self.client.close()
# Usage
checker = CachedDependencyChecker()
try:
checker.check_packages(["httpx", "requests", "packaging"])
finally:
checker.close()
## Integration Examples
### Custom Script
```python
#!/usr/bin/env python3
"""Custom dependency checker using Zaojun API."""
import sys
from pathlib import Path
import httpx
from zaojun import process_dependencies, parse_dependency
def check_custom_dependencies(dependencies_file: Path) -> bool:
"""Check dependencies from a custom file format."""
# Read custom dependencies file
with open(dependencies_file) as f:
dependencies = [line.strip() for line in f if line.strip()]
# Check each dependency
client = httpx.Client()
try:
needs_update = process_dependencies(
dependencies=dependencies,
short=True,
compat_ok=False,
client=client,
cache=None,
)
return needs_update
finally:
client.close()
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: check-deps.py <dependencies-file>")
sys.exit(1)
deps_file = Path(sys.argv[1])
if not deps_file.exists():
print(f"File not found: {deps_file}")
sys.exit(1)
needs_update = check_custom_dependencies(deps_file)
sys.exit(1 if needs_update else 0)
Integration with Other Tools¶
"""Integrate Zaojun with a build system."""
from pathlib import Path
import httpx
from zaojun import get_latest_pypi_version, parse_dependency
class DependencyManager:
"""Custom dependency manager using Zaojun components."""
def __init__(self):
self.client = httpx.Client()
def check_and_update(self, pyproject_path: Path):
"""Check dependencies and suggest updates."""
import tomllib
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)
dependencies = data.get("project", {}).get("dependencies", [])
for dep in dependencies:
try:
package_name, version_spec, _ = parse_dependency(dep)
latest = get_latest_pypi_version(package_name, self.client, cache=None)
print(f"{package_name}: {version_spec} -> Latest: {latest}")
except Exception as e:
print(f"Error checking {dep}: {e}")
def close(self):
"""Clean up resources."""
self.client.close()
# Usage
manager = DependencyManager()
try:
manager.check_and_update(Path("pyproject.toml"))
finally:
manager.close()
Performance Considerations¶
HTTP Client Reuse¶
Always reuse HTTP clients when making multiple requests:
# Good: Reuse client
client = httpx.Client()
for package in packages:
version = get_latest_pypi_version(package, client)
client.close()
# Bad: Create new client for each request
for package in packages:
client = httpx.Client()
version = get_latest_pypi_version(package, client)
client.close()
Error Handling in Loops¶
Handle errors gracefully when processing multiple dependencies:
client = httpx.Client()
for dependency in dependencies:
try:
needs_update = check_dependency(
dependency=dependency,
short=True,
compat_ok=False,
client=client,
cache=None,
)
except Exception as e:
print(f"Skipping {dependency}: {e}")
continue
client.close()
Testing the API¶
Unit Testing¶
"""Example unit tests for Zaojun API."""
from unittest.mock import Mock, patch
import httpx
from zaojun import check_dependency, parse_dependency
from zaojun.cache import PyPICache
def test_parse_dependency():
"""Test dependency parsing."""
package, version, marker = parse_dependency("package==1.0.0")
assert package == "package"
assert version == "==1.0.0"
assert marker is None
def test_check_dependency():
"""Test dependency checking with mock."""
mock_client = Mock(spec=httpx.Client)
mock_response = Mock()
mock_response.json.return_value = {"info": {"version": "2.0.0"}}
mock_client.get.return_value = mock_response
with patch("zaojun.parse_dependency") as mock_parse:
mock_parse.return_value = ("package", "==1.0.0", None)
needs_update = check_dependency(
dependency="package==1.0.0",
short=False,
compat_ok=False,
client=mock_client,
cache=None,
)
assert needs_update is True
def test_cache_integration():
"""Test dependency checking with cache."""
mock_client = Mock(spec=httpx.Client)
mock_cache = Mock(spec=PyPICache)
mock_cache.enabled = True
mock_cache.get.return_value = {"version": "1.0.0"}
with patch("zaojun.parse_dependency") as mock_parse:
mock_parse.return_value = ("package", "==1.0.0", None)
needs_update = check_dependency(
dependency="package==1.0.0",
short=False,
compat_ok=False,
client=mock_client,
cache=mock_cache,
)
# Verify cache was used
mock_cache.get.assert_called_once_with("package")
assert needs_update is False # Version matches cache
Cache Testing¶
"""Example unit tests for PyPICache."""
import tempfile
import time
from pathlib import Path
from zaojun.cache import PyPICache
def test_cache_hit():
"""Test cache hit scenario."""
with tempfile.TemporaryDirectory() as temp_dir:
cache_dir = Path(temp_dir)
cache = PyPICache(cache_dir=cache_dir)
# Set cache entry
cache.set("test-package", {"version": "1.0.0"})
# Get cache entry (should hit)
data = cache.get("test-package")
assert data == {"version": "1.0.0"}
stats = cache.get_stats()
assert stats["hits"] == 1
assert stats["misses"] == 0
def test_cache_expiration():
"""Test cache expiration."""
with tempfile.TemporaryDirectory() as temp_dir:
cache_dir = Path(temp_dir)
cache = PyPICache(cache_dir=cache_dir, ttl_hours=1)
# Manually create expired cache entry
import json
cache_file = cache._get_cache_path("test-package")
cache_file.parent.mkdir(parents=True, exist_ok=True)
expired_data = {
"timestamp": time.time() - 7200, # 2 hours ago
"package_name": "test-package",
"data": {"version": "1.0.0"},
}
with open(cache_file, "w") as f:
json.dump(expired_data, f)
# Get should return None (expired)
data = cache.get("test-package")
assert data is None
assert cache.get_stats()["expired"] == 1
Cache Implementation Details¶
Cache Storage Format¶
Cache files are stored as JSON with the following structure:
{
"timestamp": 1678886400.123456,
"package_name": "package-name",
"data": {
"version": "1.0.0",
"package_info": {
"summary": "Package description",
"author": "Author Name",
"home_page": "https://example.com"
}
}
}
Cache Key Generation¶
Cache keys are generated using: - Normalized package names (lowercase, underscores to hyphens) - MD5 hash for filesystem safety (not cryptographic security) - JSON file extension
Thread Safety¶
The PyPICache class is designed for single-threaded use. For multi-threaded applications:
- Create separate cache instances per thread
- Or implement external locking mechanisms
- Atomic file operations prevent corruption
Platform Compatibility¶
The cache system works on all major platforms:
- Linux/Unix: Uses ~/.cache/zaojun/pypi_cache/ (XDG-compliant)
- macOS: Uses ~/Library/Caches/zaojun/pypi_cache/
- Windows: Uses %LOCALAPPDATA%\zaojun\pypi_cache\
See Also¶
- Command Reference - CLI usage documentation including cache options
- Basic Usage - Usage patterns and examples with caching
- Installation - Installation instructions
- Cache Design - Technical details of cache implementation