Skip to content

Commit 4b45368

Browse files
authored
Merge pull request #123 from kikkomep/feat/extend-caching-support
feat: extend and improve caching support
2 parents 99f9b0d + ee6a223 commit 4b45368

4 files changed

Lines changed: 109 additions & 23 deletions

File tree

rocrate_validator/cli/commands/validate.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from rich.rule import Rule
2626

2727
from rocrate_validator.utils import log as logging
28-
from rocrate_validator import services
28+
from rocrate_validator import constants, services
2929
from rocrate_validator.cli.commands.errors import handle_error
3030
from rocrate_validator.cli.main import cli
3131
from rocrate_validator.cli.ui.text.validate import ValidationCommandView
@@ -205,6 +205,29 @@ def validate_uri(ctx, param, value):
205205
show_default=True,
206206
help="Width of the output line",
207207
)
208+
@click.option(
209+
'--cache-max-age',
210+
type=click.INT,
211+
default=constants.DEFAULT_HTTP_CACHE_MAX_AGE,
212+
show_default=True,
213+
help="Maximum age of the HTTP cache in seconds ([bold green]-1[/bold green] for no expiration)",
214+
)
215+
@click.option(
216+
'--cache-path',
217+
type=click.Path(),
218+
default=None,
219+
show_default=True,
220+
help="Path to the HTTP cache directory",
221+
)
222+
@click.option(
223+
'-nc',
224+
'--no-cache',
225+
is_flag=True,
226+
help="Disable the HTTP cache",
227+
default=False,
228+
show_default=True,
229+
hidden=True
230+
)
208231
@click.pass_context
209232
def validate(ctx,
210233
profiles_path: Path = DEFAULT_PROFILES_PATH,
@@ -223,7 +246,10 @@ def validate(ctx,
223246
verbose: bool = False,
224247
output_format: str = "text",
225248
output_file: Optional[Path] = None,
226-
output_line_width: Optional[int] = None):
249+
output_line_width: Optional[int] = None,
250+
cache_max_age: int = constants.DEFAULT_HTTP_CACHE_MAX_AGE,
251+
cache_path: Optional[Path] = None,
252+
no_cache: bool = False):
227253
"""
228254
[magenta]rocrate-validator:[/magenta] Validate a RO-Crate against a profile
229255
"""
@@ -247,6 +273,11 @@ def validate(ctx,
247273
logger.debug("fail_fast: %s", fail_fast)
248274
logger.debug("no fail fast: %s", not fail_fast)
249275

276+
# Cache settings
277+
logger.debug("cache_max_age: %s", cache_max_age)
278+
logger.debug("cache_path: %s", os.path.abspath(cache_path) if cache_path else None)
279+
logger.debug("no_cache: %s", no_cache)
280+
250281
if rocrate_uri:
251282
logger.debug("rocrate_path: %s", os.path.abspath(rocrate_uri))
252283

@@ -282,7 +313,9 @@ def validate(ctx,
282313
"rocrate_relative_root_path": relative_root_path,
283314
"abort_on_first": fail_fast,
284315
"skip_checks": skip_checks_list,
285-
"metadata_only": metadata_only
316+
"metadata_only": metadata_only,
317+
"cache_max_age": cache_max_age if not no_cache else -1,
318+
"cache_path": cache_path
286319
}
287320

288321
# Print the application header

rocrate_validator/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,5 @@
8787
JSON_OUTPUT_FORMAT_VERSION = "0.2"
8888

8989
# Http Cache Settings
90-
DEFAULT_HTTP_CACHE_TIMEOUT = 60
90+
DEFAULT_HTTP_CACHE_MAX_AGE = 300 # in seconds
9191
DEFAULT_HTTP_CACHE_PATH_PREFIX = '/tmp/rocrate_validator_cache'

rocrate_validator/models.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@
3131
import enum_tools
3232
from rdflib import RDF, RDFS, Graph, Namespace, URIRef
3333

34-
from rocrate_validator.utils import log as logging
3534
from rocrate_validator import __version__
36-
from rocrate_validator.constants import (DEFAULT_ONTOLOGY_FILE,
35+
from rocrate_validator.constants import (DEFAULT_HTTP_CACHE_MAX_AGE,
36+
DEFAULT_ONTOLOGY_FILE,
3737
DEFAULT_PROFILE_IDENTIFIER,
3838
DEFAULT_PROFILE_README_FILE,
3939
IGNORED_PROFILE_DIRECTORIES,
@@ -48,11 +48,13 @@
4848
ROCrateMetadataNotFoundError)
4949
from rocrate_validator.events import Event, EventType, Publisher, Subscriber
5050
from rocrate_validator.rocrate import ROCrate
51-
from rocrate_validator.utils.collections import (MapIndex)
51+
from rocrate_validator.utils import log as logging
52+
from rocrate_validator.utils.collections import MapIndex, MultiIndexMap
53+
from rocrate_validator.utils.http import HttpRequester
5254
from rocrate_validator.utils.paths import get_profiles_path
53-
from rocrate_validator.utils.python_helpers import get_requirement_name_from_file
55+
from rocrate_validator.utils.python_helpers import \
56+
get_requirement_name_from_file
5457
from rocrate_validator.utils.uri import URI
55-
from rocrate_validator.utils.collections import MultiIndexMap
5658

5759
# set the default profiles path
5860
DEFAULT_PROFILES_PATH = get_profiles_path()
@@ -2388,11 +2390,19 @@ class ValidationSettings:
23882390
metadata_dict: dict = None
23892391
#: Verbose output
23902392
verbose: bool = False
2393+
#: Cache max age in seconds
2394+
cache_max_age: Optional[int] = DEFAULT_HTTP_CACHE_MAX_AGE
2395+
#: Cache path
2396+
cache_path: Optional[Path] = None
23912397

23922398
def __post_init__(self):
23932399
# if requirement_severity is a str, convert to Severity
23942400
if isinstance(self.requirement_severity, str):
23952401
self.requirement_severity = Severity[self.requirement_severity]
2402+
# initialize the HTTP cache
2403+
HttpRequester.initialize_cache(cache_path=self.cache_path, cache_max_age=self.cache_max_age)
2404+
logger.debug("HTTP cache initialized at %s with max age %s seconds",
2405+
self.cache_path, self.cache_max_age)
23962406

23972407
def to_dict(self):
23982408
"""

rocrate_validator/utils/http.py

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import atexit
1618
import os
1719
import random
1820
import string
1921
import threading
22+
from typing import Optional
2023

2124
import requests
2225

@@ -34,8 +37,9 @@ class HttpRequester:
3437
_instance = None
3538
_lock = threading.Lock()
3639

37-
def __new__(cls):
40+
def __new__(cls, *args, **kwargs) -> HttpRequester:
3841
if cls._instance is None:
42+
logger.debug(f"Creating instance of {cls.__name__} with args: {args}, kwargs: {kwargs}")
3943
with cls._lock:
4044
if cls._instance is None:
4145
logger.debug(f"Creating instance of {cls.__name__}")
@@ -44,40 +48,59 @@ def __new__(cls):
4448
logger.debug(f"Instance created: {cls._instance.__class__.__name__}")
4549
return cls._instance
4650

47-
def __init__(self):
51+
def __init__(self,
52+
cache_max_age: int = constants.DEFAULT_HTTP_CACHE_MAX_AGE,
53+
cache_path: Optional[str] = None):
54+
logger.debug(f"Initializing instance of {self.__class__.__name__} {self}")
4855
# check if the instance is already initialized
4956
if not hasattr(self, "_initialized"):
5057
# check if the instance is already initialized
5158
with self._lock:
5259
if not getattr(self, "_initialized", False):
5360
# set the initialized flag
5461
self._initialized = False
62+
# store the parameters
63+
try:
64+
logger.debug(f"Setting cache_max_age to {cache_max_age}")
65+
self.cache_max_age = int(cache_max_age)
66+
except ValueError:
67+
raise TypeError("cache_max_age must be an integer")
68+
self.cache_path_prefix = cache_path
69+
# flag to indicate if the cache is permanent or temporary
70+
self.permanent_cache = cache_path is not None
5571
# initialize the session
56-
self.__initialize_session__()
72+
self.__initialize_session__(cache_max_age, cache_path)
5773
# set the initialized flag
5874
self._initialized = True
5975
else:
6076
logger.debug(f"Instance of {self} already initialized")
6177

62-
def __initialize_session__(self):
78+
def __initialize_session__(self, cache_max_age: int, cache_path: Optional[str] = None):
6379
# initialize the session
6480
self.session = None
6581
logger.debug(f"Initializing instance of {self.__class__.__name__}")
6682
assert not self._initialized, "Session already initialized"
6783
# check if requests_cache is installed
6884
# and set up the cached session
6985
try:
70-
if constants.DEFAULT_HTTP_CACHE_TIMEOUT > 0:
86+
if cache_max_age >= 0:
7187
from requests_cache import CachedSession
7288

73-
# Generate a random path for the cache
74-
# to avoid conflicts with other instances
75-
random_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
89+
# If cache_path is not provided, use the default path prefix
90+
if not cache_path:
91+
# Generate a random path for the cache
92+
# to avoid conflicts with other instances
93+
random_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=8))
94+
cache_path = constants.DEFAULT_HTTP_CACHE_PATH_PREFIX + f"_{random_suffix}"
95+
logger.debug(f"Using default cache path: {cache_path}")
96+
else:
97+
logger.debug(f"Using provided cache path: {cache_path}")
98+
self.permanent_cache = True
7699
# Initialize the session with a cache
77100
self.session = CachedSession(
78101
# Cache name with random suffix
79-
cache_name=f"{constants.DEFAULT_HTTP_CACHE_PATH_PREFIX}_{random_suffix}",
80-
expire_after=constants.DEFAULT_HTTP_CACHE_TIMEOUT, # Cache expiration time in seconds
102+
cache_name=cache_path,
103+
expire_after=cache_max_age, # Cache expiration time in seconds
81104
backend='sqlite', # Use SQLite backend
82105
allowable_methods=('GET',), # Cache GET
83106
allowable_codes=(200, 302, 404) # Cache responses with these status codes
@@ -86,15 +109,23 @@ def __initialize_session__(self):
86109
logger.warning("requests_cache is not installed. Using requests instead.")
87110
except Exception as e:
88111
logger.error("Error initializing requests_cache: %s", e)
89-
logger.warning("Using requests instead of requests_cache")
90-
# if requests_cache is not installed or an error occurred, use requests
91-
# instead of requests_cache
112+
113+
# if requests_cache is not installed or an error occurred,
114+
# use requests instead of requests_cache
92115
# and create a new session
93116
if not self.session:
94-
logger.debug("Using requests instead of requests_cache")
117+
logger.debug("Cache disabled: using requests instead of requests_cache")
95118
self.session = requests.Session()
96119

97120
def __del__(self):
121+
"""
122+
Destructor to clean up the cache file used by CachedSession.
123+
"""
124+
logger.debug(f"Deleting instance of {self.__class__.__name__}")
125+
if hasattr(self, "permanent_cache") and not self.permanent_cache:
126+
self.cleanup()
127+
128+
def cleanup(self):
98129
"""
99130
Destructor to clean up the cache file used by CachedSession.
100131
"""
@@ -119,3 +150,15 @@ def __getattr__(self, name):
119150
if name.upper() in {"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"}:
120151
return getattr(self.session, name.lower())
121152
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
153+
154+
@classmethod
155+
def initialize_cache(cls,
156+
cache_max_age: int = constants.DEFAULT_HTTP_CACHE_MAX_AGE,
157+
cache_path: Optional[str] = None) -> HttpRequester:
158+
"""
159+
Initialize the HttpRequester singleton with cache settings.
160+
161+
:param max_age: The maximum age of the cache in seconds.
162+
:param cache_path: The path to the cache directory.
163+
"""
164+
return cls(cache_max_age=cache_max_age, cache_path=cache_path)

0 commit comments

Comments
 (0)