Coretex
cache.py
1 # Copyright (C) 2023 Coretex LLC
2 
3 # This file is part of Coretex.ai
4 
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
14 
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <https://www.gnu.org/licenses/>.
17 
18 from typing import Any, Union
19 from pathlib import Path
20 
21 import os
22 import shutil
23 import pickle
24 import hashlib
25 
26 import requests
27 
28 from ._folder_manager import folder_manager
29 
30 
31 class CacheException(Exception):
32 
33  """
34  Exception which is raised due to any unexpected behaviour with Cache module
35  """
36 
37  pass
38 
39 
40 def _hashCacheKey(key: str) -> str:
41  return hashlib.sha256(key.encode("UTF-8")).hexdigest()
42 
43 
44 MAX_RETRY_COUNT = 3
45 
46 
47 def _download(source: str, destination: Path, retryCount: int = 0) -> Path:
48  if destination.exists():
49  raise FileExistsError(destination)
50 
51  try:
52  with requests.get(source, stream = True) as r:
53  r.raise_for_status()
54 
55  with destination.open("wb") as destinationFile:
56  for chunk in r.iter_content(chunk_size = 8192):
57  destinationFile.write(chunk)
58  except:
59  if retryCount >= MAX_RETRY_COUNT:
60  raise
61 
62  destination.unlink(missing_ok = True)
63  return _download(source, destination, retryCount + 1)
64 
65  return destination
66 
67 
68 def getPath(key: str) -> Path:
69  """
70  Retrieves the path of the cache
71 
72  Parameters
73  ----------
74  key : str
75  key to which the cache item was linked
76 
77  Returns
78  -------
79  pathlib.Path -> path of the cached item
80 
81  Raises
82  ------
83  CacheException -> if something went wrong
84 
85  Example
86  -------
87  >>> from coretex import cache
88  \b
89  >>> key = "dummyFile"
90  >>> print(cache.getPath(key))
91  Path("/Users/dummyUser/.coretex/cache/c147efcfc2d7ea666a9e4f5187b115c90903f0fc896a56df9a6ef5d8f3fc9f31.zip")
92  """
93 
94  if not exists(key):
95  raise CacheException(">> [Coretex] Cache with given key doesn't exist.")
96 
97  return folder_manager.cache / _hashCacheKey(key)
98 
99 
100 def exists(key: str) -> bool:
101  """
102  Checks if the cache item exists
103 
104  Parameters
105  ----------
106  key : str
107  key to which the cache item was linked
108 
109  Returns
110  -------
111  bool -> True if it exists, False otherwise
112 
113  Example
114  -------
115  >>> from coretex import cache
116  \b
117  >>> key = "dummyFile"
118  >>> print(cache.exists(key))
119  True
120  """
121 
122  return folder_manager.cache.joinpath(_hashCacheKey(key)).exists()
123 
124 
125 def remove(key: str) -> None:
126  """
127  Removes cached item from the cache
128 
129  Parameters
130  ----------
131  key : str -> Key to which the cache item was linked
132 
133  Raises
134  ------
135  CacheException -> if the cache item does not exist
136 
137  Example
138  -------
139  >>> from coretex import cache
140  \b
141  >>> cache.remove("dummyFile")
142  """
143 
144  if not exists(key):
145  raise CacheException(">> [Coretex] Cache with given key doesn't exist.")
146 
147  getPath(key).unlink()
148 
149 
150 def clear() -> None:
151  """
152  Clears all cached items
153 
154  Example
155  -------
156  >>> from coretex import cache
157  \b
158  >>> cache.clear()
159  """
160 
161  shutil.rmtree(folder_manager.cache)
162 
163 
164 def storeObject(key: str, object: Any, override: bool = False) -> None:
165  """
166  Caches the specified object using pickle
167 
168  Parameters
169  ----------
170  key : str
171  key to which the cached object will be linked
172  object : Any
173  object which supports being pickled
174  override : bool
175  should the cache be overriden if it exists or not
176 
177  Raises
178  ------
179  CacheException -> if something went wrong
180 
181  Example
182  -------
183  >>> from coretex import cache
184  \b
185  >>> dummyObject = {"name": "John", "age": 24, "gender": "Male"}
186  >>> cache.storeObject("Person", dummyObject)
187  """
188 
189  if not override and exists(key):
190  raise CacheException(">> [Coretex] Cache with given key already exists. Set \"override\" to \"True\" if you want to override existing cache.")
191 
192  if override and exists(key):
193  getPath(key).unlink()
194 
195  cachePath = folder_manager.cache / _hashCacheKey(key)
196  with cachePath.open("wb") as cacheFile:
197  pickle.dump(object, cacheFile)
198 
199 
200 def storeFile(key: str, source: Union[Path, str], override: bool = False) -> None:
201  """
202  Caches the specified file
203 
204  Parameters
205  ----------
206  key : str
207  key to which the cached file will be linked
208  source : str
209  path to the file which will be cached
210  override : bool
211  should the cache be overriden if it exists or not
212 
213  Raises
214  ------
215  CacheException -> if something went wrong
216 
217  Example
218  -------
219  >>> from coretex import cache
220  \b
221  >>> filePath = "path/to/file"
222  >>> cache.storeFile("dummyFile", filePath)
223  """
224 
225  if not isinstance(source, Path):
226  source = Path(source)
227 
228  if not override and exists(key):
229  raise CacheException(">> [Coretex] Cache with given key already exists. Set \"override\" to \"True\" if you want to override existing cache.")
230 
231  if override and exists(key):
232  getPath(key).unlink()
233 
234  cachePath = folder_manager.cache / _hashCacheKey(key)
235 
236  # Hardlink the file to the cache directory
237  os.link(source, cachePath)
238 
239 
240 def storeUrl(key: str, url: str, override: bool = False) -> None:
241  """
242  Downloads and caches file from the specified URL
243 
244  Parameters
245  ----------
246  url : str
247  URL of the file which is downloaded
248  fileName : str
249  Name of the downloaded file with extension - used as a key for cache
250 
251  Returns
252  -------
253  Tuple[Path, str] -> Path of the cached file and the name of the cached file
254 
255  Raises
256  ------
257  CacheException -> if something went wrong
258 
259  Example
260  -------
261  >>> from coretex import cache
262  \b
263  >>> url = "https://dummy_url.com/download"
264  >>> fileName = "dummyFile.ext"
265  >>> cache.storeUrl(url, fileName)
266  """
267 
268  if not override and exists(key):
269  raise CacheException(">> [Coretex] Cache with given key already exists. Set \"override\" to \"True\" if you want to override existing cache.")
270 
271  if override and exists(key):
272  getPath(key).unlink()
273 
274  cachePath = folder_manager.cache / _hashCacheKey(key)
275  _download(url, cachePath)
276 
277 
278 def loadObject(key: str) -> Any:
279  """
280  Loads the object cached with storeObject function
281 
282  Parameters
283  ----------
284  key : str
285  key to which the object was linked when cached
286 
287  Returns
288  -------
289  Any -> unpickled object
290 
291  Raises
292  ------
293  CacheException -> if something went wrong
294 
295  Example
296  -------
297  >>> from coretex import cache
298  \b
299  >>> loadedObject = cache.load("dummyObject")
300  >>> print(loadedObject)
301  {"name": "John", "age": 24, "gender": "Male"}
302  """
303 
304  if not exists(key):
305  raise CacheException(">> [Coretex] Cache with given key doesn't exist.")
306 
307  with getPath(key).open("rb") as pickleFile:
308  return pickle.load(pickleFile)