Coverage for C:\Python311\Lib\site-packages\persist_cache\caching.py: 81%
43 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-05-06 20:49 +1000
« prev ^ index » next coverage.py v7.3.2, created at 2024-05-06 20:49 +1000
1import os
2import shutil
3from datetime import datetime, timedelta
4from typing import Any, Union
6from filelock import FileLock
7from xxhash import xxh3_64_hexdigest, xxh3_128_hexdigest
9from .serialization import deserialize, serialize
11NOT_IN_CACHE = object()
12"""A sentinel object that flags that a key is not in the cache."""
14def set(key: str, value: Any, dir: str) -> None:
15 """Set the given key of the provided cache to the specified value."""
17 path = f'{dir}/{key}.msgpack'
19 # Lock the entry before writing to it.
20 with FileLock(f'{path}.lock'), \
21 open(path, 'wb') as file:
22 file.write(serialize(value))
24def get(key: str, dir: str, expiry: Union[int, float, timedelta, None] = None) -> Any:
25 """Get the value of the given key from the provided cache if it is not expired."""
27 path = f'{dir}/{key}.msgpack'
29 # If the key does not exist in the cache, return `NOT_IN_CACHE`.
30 if not os.path.exists(path):
31 return NOT_IN_CACHE
33 # Lock the entry.
34 with FileLock(f'{path}.lock'):
35 # Handle expiry if necessary.
36 if expiry is not None:
37 # Get the time at which the key was last set.
38 timestamp = os.path.getmtime(path)
40 # If the entry is expired, remove it from the cache and return `NOT_IN_CACHE`.
41 if isinstance(expiry, timedelta) and datetime.fromtimestamp(timestamp) + expiry < datetime.now() \
42 or timestamp + expiry < datetime.now().timestamp():
43 # Remove the entry.
44 os.remove(path)
46 return NOT_IN_CACHE
48 # Read, deserialize and return the value.
49 with open(path, 'rb') as file:
50 return deserialize(file.read())
52def hash(data: Any) -> str:
53 """Hash the given data."""
55 # Serialise the data.
56 data = serialize(data)
58 # Hash the data and affix its length, preceded by a hyphen (to reduce the likelihood of collisions).
59 return f'{xxh3_128_hexdigest(data)}{len(data)}'
61def shorthash(data: Any) -> str:
62 """Hash the given data."""
64 # Serialise the data.
65 data = serialize(data)
67 # Hash the data and affix its length, preceded by a hyphen (to reduce the likelihood of collisions).
68 return f'{xxh3_64_hexdigest(data)}{len(data)}'
70def delete(dir: str) -> None:
71 """Delete the provided cache."""
73 # Remove the cache directory and all its contents.
74 shutil.rmtree(dir, ignore_errors=True)
76def clear(dir: str) -> None:
77 """Clear the provided cache."""
79 # Delete the cache.
80 delete(dir)
82 # Recreate the cache directory.
83 os.makedirs(dir, exist_ok=True)
85def flush(dir: str, expiry: Union[int, float, timedelta, None]) -> None:
86 """Flush expired keys from the provided cache."""
88 # Iterate over keys in the cache.
89 for file in os.listdir(dir):
90 path = f'{dir}/{file}'
92 # Lock the entry before reading it.
93 with FileLock(f'{path}.lock'):
94 # Get the time at which the key was last set.
95 timestamp = os.path.getmtime(path)
97 # If the entry is expired, remove it from the cache.
98 if (isinstance(expiry, timedelta) and datetime.fromtimestamp(timestamp) + expiry < datetime.now()) \
99 or (timestamp + expiry < datetime.now().timestamp()):
100 os.remove(path)