11import asyncio
2- import json
2+ import orjson # Added for faster JSON processing
33import time
44import os
55from pathlib import Path
88import aiohttp
99
1010from .exceptions import InvalidRequestError , VNDBAPIError
11+ from .methods .fetch import _fetch_api # Ensure this import is present
1112
1213# Forward declaration for type hinting
1314if "VNDB" not in globals ():
@@ -19,31 +20,89 @@ class SchemaCache:
1920 """
2021 Manages the download, caching, and retrieval of the VNDB API schema.
2122 """
23+
2224 def __init__ (self , cache_dir : str = ".veedb_cache" , cache_filename : str = "schema.json" , ttl_hours : float = 24.0 , local_schema_path : Optional [str ] = None ):
23- self .cache_dir = Path (cache_dir )
24- self .cache_file = self .cache_dir / cache_filename
25+ # Use string paths initially to avoid any Path recursion issues
26+ self ._cache_dir_str = str (cache_dir ) if cache_dir else ".veedb_cache"
27+ self ._cache_filename_str = str (cache_filename ) if cache_filename else "schema.json"
28+ self ._local_schema_path_str = str (local_schema_path ) if local_schema_path else None
29+
30+ # Create Path objects only when needed and with error handling
31+ self ._cache_dir = None
32+ self ._cache_file = None
33+ self ._local_schema_path = None
34+
2535 self .ttl_seconds = ttl_hours * 3600
2636 self ._schema_data : Optional [Dict [str , Any ]] = None
27- self .local_schema_path = Path (local_schema_path ) if local_schema_path else None
37+
38+ @property
39+ def cache_dir (self ) -> Path :
40+ """Safely get the cache directory Path object."""
41+ if self ._cache_dir is None :
42+ try :
43+ self ._cache_dir = Path (self ._cache_dir_str )
44+ except Exception :
45+ self ._cache_dir = Path (".veedb_cache" )
46+ return self ._cache_dir
47+
48+ @property
49+ def cache_file (self ) -> Path :
50+ """Safely get the cache file Path object."""
51+ if self ._cache_file is None :
52+ try :
53+ self ._cache_file = self .cache_dir / self ._cache_filename_str
54+ except Exception :
55+ self ._cache_file = Path (".veedb_cache" ) / "schema.json"
56+ return self ._cache_file
57+
58+ @property
59+ def local_schema_path (self ) -> Optional [Path ]:
60+ """Safely get the local schema path Path object."""
61+ if self ._local_schema_path is None and self ._local_schema_path_str :
62+ try :
63+ self ._local_schema_path = Path (self ._local_schema_path_str )
64+ except Exception :
65+ self ._local_schema_path = None
66+ return self ._local_schema_path
2867
2968 def is_cached (self ) -> bool :
3069 """Check if the schema file exists in the cache or if a local path is provided."""
31- if self .local_schema_path and self .local_schema_path .is_file ():
32- return True
33- return self .cache_file .is_file ()
70+ if self ._local_schema_path_str :
71+ try :
72+ # Use os.path.isfile instead of Path.is_file() to avoid recursion
73+ return os .path .isfile (self ._local_schema_path_str )
74+ except (RecursionError , OSError , Exception ):
75+ pass
76+
77+ try :
78+ # Use os.path.exists instead of Path.exists() to avoid recursion
79+ cache_path = os .path .join (self ._cache_dir_str , self ._cache_filename_str )
80+ return os .path .exists (cache_path )
81+ except (RecursionError , OSError , Exception ):
82+ return False
3483
3584 def get_cache_age (self ) -> float :
3685 """Get the age of the cache file in seconds. Returns 0 if using local_schema_path."""
37- if self .local_schema_path and self .local_schema_path .is_file ():
38- # Treat local schema as always up-to-date unless explicitly updated
39- return 0.0
40- if not self .cache_file .is_file (): # Changed from self.is_cached() to self.cache_file.is_file()
86+ if self ._local_schema_path_str :
87+ try :
88+ # Use os.path.isfile instead of Path methods to avoid recursion
89+ if os .path .isfile (self ._local_schema_path_str ):
90+ # Treat local schema as always up-to-date unless explicitly updated
91+ return 0.0
92+ except (RecursionError , OSError , Exception ):
93+ pass
94+
95+ try :
96+ cache_path = os .path .join (self ._cache_dir_str , self ._cache_filename_str )
97+ if not os .path .exists (cache_path ):
98+ return float ('inf' )
99+ return time .time () - os .path .getmtime (cache_path )
100+ except (RecursionError , OSError , Exception ):
41101 return float ('inf' )
42- return time .time () - self .cache_file .stat ().st_mtime
43102
44103 def is_cache_expired (self ) -> bool :
45104 """Check if the cached schema has expired. Local schema path is never considered expired by this check."""
46- if self .local_schema_path and self . local_schema_path . is_file ( ):
105+ if self ._local_schema_path_str and os . path . isfile ( self . _local_schema_path_str ):
47106 return False # Local schema is not subject to TTL expiration, only manual updates
48107 return self .get_cache_age () > self .ttl_seconds
49108
@@ -57,24 +116,35 @@ def save_schema(self, schema_data: Dict[str, Any], to_local_path: bool = False):
57116 target_dir = target_path .parent
58117 target_dir .mkdir (parents = True , exist_ok = True )
59118
60- with open (target_path , 'w' , encoding = 'utf-8' ) as f :
61- json .dump (schema_data , f , indent = 2 )
119+ # Use orjson for writing
120+ with open (target_path , 'wb' ) as f : # Open in binary mode for orjson
121+ f .write (orjson .dumps (schema_data , option = orjson .OPT_INDENT_2 ))
62122 self ._schema_data = schema_data # Update in-memory cache as well
63-
123+
64124 def load_schema (self ) -> Optional [Dict [str , Any ]]:
65125 """Load the schema data from the local_schema_path (if provided) or the cache file."""
66- if self .local_schema_path and self . local_schema_path . is_file ( ):
126+ if self ._local_schema_path_str and os . path . isfile ( self . _local_schema_path_str ):
67127 try :
68- with open (self .local_schema_path , 'r' , encoding = 'utf-8' ) as f :
69- return json .load (f )
128+ # Use orjson for reading
129+ with open (self ._local_schema_path_str , 'rb' ) as f : # Open in binary mode for orjson
130+ return orjson .loads (f .read ())
70131 except Exception :
71132 # If local schema fails to load, fall back to cache or download
72133 pass
73134
74- if self .cache_file .is_file (): # Changed from self.is_cached()
135+ # Use os.path instead of Path methods to avoid recursion issues
136+ try :
137+ cache_path_str = os .path .join (self ._cache_dir_str , self ._cache_filename_str )
138+ cache_file_exists = os .path .isfile (cache_path_str ) # Changed from os.path.exists to os.path.isfile
139+ except (RecursionError , OSError , Exception ):
140+ # If there's any issue with the cache_file path, return None
141+ return None
142+
143+ if cache_file_exists :
75144 try :
76- with open (self .cache_file , 'r' , encoding = 'utf-8' ) as f :
77- return json .load (f )
145+ # Use orjson for reading
146+ with open (cache_path_str , 'rb' ) as f : # Open in binary mode for orjson
147+ return orjson .loads (f .read ())
78148 except Exception :
79149 pass
80150 return None
@@ -83,8 +153,9 @@ def invalidate_cache(self):
83153 """Remove the cache file. Does not remove user-provided local_schema_path."""
84154 self ._schema_data = None
85155 try :
86- if self .cache_file .exists ():
87- os .remove (self .cache_file )
156+ cache_path = os .path .join (self ._cache_dir_str , self ._cache_filename_str )
157+ if os .path .exists (cache_path ):
158+ os .remove (cache_path )
88159 except FileNotFoundError :
89160 pass
90161
@@ -124,16 +195,61 @@ async def update_local_schema_from_api(self, client: 'VNDB') -> Dict[str, Any]:
124195 return await self .get_schema (client , force_download = True )
125196
126197 async def _download_schema (self , client : 'VNDB' ) -> Dict [str , Any ]:
127- """Fetch the schema from the VNDB API."""
198+ """Fetch the schema from the VNDB API directly ."""
128199 try :
129- return await client .get_schema ()
130- except VNDBAPIError as e :
131- # If download fails but a stale cache exists, use it as a fallback
132- if self .is_cached ():
133- schema = self .load_schema ()
134- if schema :
135- return schema
136- raise e # Re-raise if there's no cache at all
200+ # Call the API directly to avoid recursion - do NOT call client.get_schema()
201+ url = f"{ client .base_url } /schema"
202+ session = client ._get_session ()
203+
204+ # Use the imported _fetch_api
205+ # The schema endpoint typically does not require a token.
206+ response_data = await _fetch_api (
207+ session = session ,
208+ method = "GET" ,
209+ url = url ,
210+ token = None # Explicitly None for public schema endpoint
211+ )
212+ if not isinstance (response_data , dict ):
213+ raise VNDBAPIError (f"Schema download did not return a valid JSON object. Received type: { type (response_data )} " )
214+ return response_data
215+ except aiohttp .ClientError as e :
216+ raise VNDBAPIError (f"Failed to download schema due to network/HTTP error: { e } " ) from e
217+ except Exception as e :
218+ # Catch other potential errors during the fetch or processing
219+ raise VNDBAPIError (f"An unexpected error occurred while downloading schema: { e } " ) from e
220+
221+ async def update_local_schema_from_api (self , client : 'VNDB' ) -> Dict [str , Any ]:
222+ """Forces a download of the schema and saves it to local_schema_path if configured, else to cache."""
223+ if not self .local_schema_path :
224+ # If no specific local path, update the default cache file.
225+ # Or, one might choose to raise an error if this method is called without a local_schema_path configured.
226+ # For now, let's assume it updates the primary schema location (local if set, else cache).
227+ pass # Fall through to get_schema with force_download
228+ return await self .get_schema (client , force_download = True )
229+
230+ async def _download_schema (self , client : 'VNDB' ) -> Dict [str , Any ]:
231+ """Fetch the schema from the VNDB API directly."""
232+ try :
233+ # Call the API directly to avoid recursion - do NOT call client.get_schema()
234+ url = f"{ client .base_url } /schema"
235+ session = client ._get_session ()
236+
237+ # Use the imported _fetch_api
238+ # The schema endpoint typically does not require a token.
239+ response_data = await _fetch_api (
240+ session = session ,
241+ method = "GET" ,
242+ url = url ,
243+ token = None # Explicitly None for public schema endpoint
244+ )
245+ if not isinstance (response_data , dict ):
246+ raise VNDBAPIError (f"Schema download did not return a valid JSON object. Received type: { type (response_data )} " )
247+ return response_data
248+ except aiohttp .ClientError as e :
249+ raise VNDBAPIError (f"Failed to download schema due to network/HTTP error: { e } " ) from e
250+ except Exception as e :
251+ # Catch other potential errors during the fetch or processing
252+ raise VNDBAPIError (f"An unexpected error occurred while downloading schema: { e } " ) from e
137253
138254class FilterValidator :
139255 """
0 commit comments