44Handles resolution and downloading of NeqSim Java dependencies from GitHub releases.
55"""
66
7- import json
87import logging
98import urllib .error
109import urllib .request
1110from pathlib import Path
1211from typing import Optional
13- from urllib .parse import urlparse
1412
1513import yaml
1614
15+ from .jar_cache import JARCacheManager
16+
1717
1818class 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