Skip to content

Commit 6100c02

Browse files
committed
chore: Implement cache to aviod rate limiting.
1 parent 8dd2606 commit 6100c02

16 files changed

+848
-394
lines changed

.github/workflows/publish.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,25 @@ jobs:
5050
- name: Fixup module naming
5151
run: find jneqsim/neqsim -type f -name "*.pyi" -print0 | xargs -0 sed -i '' -e 's/neqsim./jneqsim.neqsim./g' || true
5252

53+
- name: Update NeqSim dependency version
54+
run: |
55+
NEQSIM_VERSION="${{ inputs.version }}"
56+
echo "Updating NeqSim dependency to version '$NEQSIM_VERSION'"
57+
58+
# Get the current version to use as fallback
59+
CURRENT_VERSION=$(grep -o 'version: "[^"]*"' jneqsim/dependencies.yaml | head -1 | sed 's/version: "\(.*\)"/\1/')
60+
echo "Current version (will become fallback): '$CURRENT_VERSION'"
61+
echo "New version: '$NEQSIM_VERSION'"
62+
63+
# Update the main version to the new version
64+
sed -i 's/version: "[^"]*"/version: "'"$NEQSIM_VERSION"'"/' jneqsim/dependencies.yaml
65+
66+
# Set fallback version to the previous version (current before update)
67+
sed -i 's/fallback_version: "[^"]*"/fallback_version: "'"$CURRENT_VERSION"'"/' jneqsim/dependencies.yaml
68+
69+
echo "Updated dependencies.yaml:"
70+
grep -A 10 -B 5 "version:" jneqsim/dependencies.yaml
71+
5372
- name: Bump version
5473
run: |
5574
VERSION="${{ inputs.pypi_version || inputs.version }}"

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
needs: build
6767
strategy:
6868
matrix:
69-
java: [ '11', '17', '21' ]
69+
java: [ '8', '11', '21' ]
7070
name: Java ${{ matrix.Java }}
7171
steps:
7272
- uses: actions/checkout@v4

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,40 @@ def pressurize_gas():
4949

5050
- [jpype](https://jpype.readthedocs.io/en/latest/index.html#)
5151

52+
## Version Management
53+
54+
jneqsim uses a controlled release process to ensure compatibility:
55+
56+
- **Pinned Versions**: Each jneqsim release is pinned to a specific, tested NeqSim JAR version
57+
- **Automated Updates**: The nightly CI workflow automatically checks for new NeqSim releases and publishes updated jneqsim packages when available
58+
- **No Auto-Updates**: Applications using jneqsim will not automatically download newer NeqSim versions - they use the tested version that comes with their installed jneqsim package
59+
- **Cache Management**: Downloaded JARs are cached locally in `~/.jneqsim/cache` for faster subsequent usage
60+
61+
## Development
62+
63+
### Running Tests
64+
65+
Quick testing options:
66+
67+
```bash
68+
# Install test dependencies
69+
pip install pytest pytest-mock
70+
71+
# Run unit tests (fast)
72+
make test-unit
73+
# or: pytest -v -m "not slow" tests/
74+
75+
# Run all tests (may download JARs)
76+
make test-all
77+
# or: pytest -v tests/
78+
79+
# Run with coverage
80+
make coverage
81+
# or: pytest --cov=jneqsim --cov-report=html tests/
82+
```
83+
84+
See [TESTING.md](TESTING.md) for detailed testing documentation.
85+
5286

5387
<a id="Contributing"></a>
5488

jneqsim/dependencies.yaml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
# This file defines how to resolve NeqSim Java dependencies
33

44
neqsim:
5-
# Version to use ("latest" or specific version like "2.5.25")
6-
version: "latest"
5+
# Version to use (specific version that has been tested with this jneqsim release)
6+
# This version is automatically updated by the CI/CD pipeline when a new NeqSim release is published
7+
version: "3.0.43"
8+
9+
# Fallback version when GitHub API is unavailable (rate limited, network issues)
10+
# This should be the previous tested version, providing a safety fallback mechanism
11+
# The CI/CD pipeline automatically sets this to the previous version when updating
12+
fallback_version: "3.0.42"
713

814
# GitHub Releases source
915
sources:
@@ -17,10 +23,14 @@ neqsim:
1723
assets:
1824
java8: "neqsim-{version}-Java8.jar"
1925
java11: "neqsim-{version}.jar" # Default Java 11+ version
20-
java17: "neqsim-{version}-Java17.jar"
2126
java21: "neqsim-{version}-Java21.jar"
2227

2328
# Logging configuration
2429
logging:
2530
level: "INFO" # DEBUG, INFO, WARNING, ERROR
26-
show_progress: true
31+
show_progress: true
32+
33+
cache:
34+
enabled: true
35+
verify_integrity: true
36+
# Cache stores one JAR file per version/Java version combination

jneqsim/dependency_manager.py

Lines changed: 46 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,43 @@
44
Handles resolution and downloading of NeqSim Java dependencies from GitHub releases.
55
"""
66

7-
import json
87
import logging
98
import urllib.error
109
import urllib.request
1110
from pathlib import Path
1211
from typing import Optional
13-
from urllib.parse import urlparse
1412

1513
import yaml
1614

15+
from .jar_cache import JARCacheManager
16+
1717

1818
class NeqSimDependencyManager:
1919
"""Manages NeqSim JAR dependencies from GitHub releases"""
2020

21-
def __init__(self, config_path: Optional[Path] = None):
21+
def __init__(self, config_path: Optional[Path] = None, cache_dir: Optional[Path] = None):
2222
"""
2323
Initialize dependency manager
2424
2525
Args:
2626
config_path: Path to dependencies.yaml, defaults to package config
27+
cache_dir: Directory for caching version info, defaults to ~/.jneqsim/cache
2728
"""
2829
if config_path is None:
2930
config_path = Path(__file__).parent / "dependencies.yaml"
3031

32+
if cache_dir is None:
33+
cache_dir = Path.home() / ".jneqsim" / "cache"
34+
35+
self.cache_dir = cache_dir
36+
self.cache_dir.mkdir(parents=True, exist_ok=True)
37+
3138
self.config = self._load_config(config_path)
3239
self.logger = self._setup_logging()
3340

41+
# Initialize cache manager
42+
self.cache_manager = JARCacheManager(self.cache_dir, self.config, self.logger)
43+
3444
def _load_config(self, config_path: Path) -> dict:
3545
"""Load configuration from YAML file"""
3646
with open(config_path) as f:
@@ -50,43 +60,6 @@ def _setup_logging(self) -> logging.Logger:
5060

5161
return logger
5262

53-
def _validate_url(self, url: str, allowed_hosts: set[str]) -> None:
54-
"""
55-
Validate URL for security - ensures HTTPS and trusted hosts only
56-
57-
Args:
58-
url: URL to validate
59-
allowed_hosts: Set of allowed hostnames
60-
61-
Raises:
62-
ValueError: If URL is invalid or from untrusted host
63-
"""
64-
parsed = urlparse(url)
65-
66-
if parsed.scheme != "https":
67-
raise ValueError(f"Only HTTPS URLs are allowed, got: {parsed.scheme}")
68-
69-
if parsed.hostname not in allowed_hosts:
70-
raise ValueError(f"Untrusted host: {parsed.hostname}. Allowed hosts: {allowed_hosts}")
71-
72-
def get_latest_version(self) -> str:
73-
"""Get latest NeqSim version from GitHub API"""
74-
repo = self.config["neqsim"]["sources"]["github"]["repository"]
75-
url = f"https://api.github.com/repos/{repo}/releases/latest"
76-
77-
# Validate URL for security
78-
self._validate_url(url, {"api.github.com"})
79-
80-
try:
81-
with urllib.request.urlopen(url) as response: # noqa: S310
82-
data = json.loads(response.read())
83-
version = data["tag_name"].lstrip("v")
84-
self.logger.debug(f"Latest version: {version}")
85-
return version
86-
except Exception as e:
87-
self.logger.error(f"Failed to get latest version: {e}")
88-
raise RuntimeError(f"Could not determine latest NeqSim version: {e}") from e
89-
9063
def _get_jar_patterns(self, java_version: int) -> list[str]:
9164
"""Get list of JAR filename patterns to try for a given Java version.
9265
@@ -101,40 +74,41 @@ def _get_jar_patterns(self, java_version: int) -> list[str]:
10174
github_config["assets"]["java8"], # Standard pattern
10275
github_config["assets"]["java11"], # Fallback
10376
]
104-
elif java_version >= 21:
77+
elif 11 <= java_version < 21:
10578
return [
106-
"neqsim-{version}-Java21-Java21.jar", # Newer pattern
107-
github_config["assets"]["java21"], # Standard pattern
79+
"neqsim-{version}.jar", # Standard pattern
10880
github_config["assets"]["java11"], # Fallback to default
10981
]
110-
elif java_version >= 17:
82+
elif java_version >= 21:
11183
return [
112-
"neqsim-{version}-Java17-Java17.jar", # Newer pattern
113-
"neqsim-{version}-Java17.jar", # Standard pattern
84+
"neqsim-{version}-Java21-Java21.jar", # Newer pattern
85+
github_config["assets"]["java21"], # Standard pattern
11486
github_config["assets"]["java11"], # Fallback to default
11587
]
11688
else:
11789
return [github_config["assets"]["java11"]]
11890

119-
def _get_from_github(self, version: str, java_version: int) -> Path:
120-
"""Download JAR from GitHub releases with fallback support"""
91+
def _get_JAR_from_github(self, version: str, java_version: int) -> Path:
92+
"""Download JAR from GitHub releases with fallback support and caching"""
12193
github_config = self.config["neqsim"]["sources"]["github"]
122-
patterns_to_try = self._get_jar_patterns(java_version)
12394

124-
# Create temporary directory for download
125-
import tempfile
95+
# Check cache first
96+
cached_jar = self.cache_manager.get_cached_jar(version, java_version)
97+
if cached_jar:
98+
return cached_jar
12699

127-
temp_dir = Path(tempfile.mkdtemp(prefix="jneqsim_"))
100+
patterns_to_try = self._get_jar_patterns(java_version)
128101

129102
# Try each pattern until one succeeds
130103
last_error = None
131104
for i, asset_pattern in enumerate(patterns_to_try):
132105
jar_filename = asset_pattern.format(version=version)
133106
url = f"{github_config['base_url']}/v{version}/{jar_filename}"
134107

135-
# Validate URL for security
136-
self._validate_url(url, {"github.com"})
108+
# Create temporary directory for download
109+
import tempfile
137110

111+
temp_dir = Path(tempfile.mkdtemp(prefix="jneqsim_"))
138112
downloaded_jar = temp_dir / jar_filename
139113

140114
try:
@@ -158,7 +132,9 @@ def _get_from_github(self, version: str, java_version: int) -> Path:
158132
else:
159133
self.logger.info(f"Downloaded from GitHub: {downloaded_jar.name}")
160134

161-
return downloaded_jar
135+
# Cache the downloaded JAR
136+
cached_jar = self.cache_manager.cache_jar(downloaded_jar, version, java_version)
137+
return cached_jar
162138

163139
except urllib.error.HTTPError as e:
164140
if e.code == 404:
@@ -180,38 +156,31 @@ def _get_from_github(self, version: str, java_version: int) -> Path:
180156
self.logger.error(error_msg)
181157
raise RuntimeError(error_msg) from last_error
182158

183-
def resolve_dependency(self, version: Optional[str] = None, java_version: Optional[int] = None) -> Path:
159+
def resolve_dependency(self, java_version: Optional[int] = None) -> Path:
184160
"""
185161
Resolve NeqSim dependency
186162
187163
Args:
188-
version: NeqSim version, defaults to config or latest
164+
version: Specific NeqSim version to use. If None, uses the version
165+
configured in dependencies.yaml. Note: The config should specify
166+
a pinned version that has been tested with this jneqsim release.
189167
java_version: Java version, auto-detected if None
190168
191169
Returns:
192170
Path to resolved JAR file
193171
"""
194-
# Determine version and java version
195-
version = self._resolve_version(version)
172+
173+
neqsim_version = self.config["neqsim"]["version"]
174+
if neqsim_version is None:
175+
raise ValueError("NeqSim version must be specified either as an argument or in dependencies.yaml")
176+
196177
java_version = self._resolve_java_version(java_version)
197178

198179
# Download dependency
199-
jar_path = self._get_from_github(version, java_version)
180+
jar_path = self._get_JAR_from_github(neqsim_version, java_version)
200181

201182
return jar_path
202183

203-
def _resolve_version(self, version: Optional[str]) -> str:
204-
"""Resolve the NeqSim version to use"""
205-
if version is None:
206-
version = self.config["neqsim"]["version"]
207-
if version == "latest":
208-
version = self.get_latest_version()
209-
210-
if version is None:
211-
raise RuntimeError("Could not determine NeqSim version")
212-
213-
return version
214-
215184
def _resolve_java_version(self, java_version: Optional[int]) -> int:
216185
"""Resolve the Java version to use"""
217186
if java_version is not None:
@@ -226,3 +195,8 @@ def _resolve_java_version(self, java_version: Optional[int]) -> int:
226195
return 11 # Default fallback
227196
except ImportError:
228197
return 11 # Default fallback
198+
199+
@property
200+
def jar_cache_dir(self) -> Path:
201+
"""Access to JAR cache directory for backward compatibility"""
202+
return self.cache_manager.jar_cache_dir

0 commit comments

Comments
 (0)