summaryrefslogtreecommitdiff
path: root/src/teradata/cache/file.py
blob: ebb9bda7c0a3c7f1b0815e219b49ee4d7e41582d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
lazy import ast
lazy from collections.abc import Callable
lazy from functools import wraps
lazy from inspect import getsource, signature, unwrap
lazy from pathlib import Path
lazy from sys import version_info
lazy from textwrap import dedent
lazy from types import FunctionType
lazy from uuid import uuid4

lazy import dill
lazy from blake3 import blake3
lazy from loguru import logger

BASE_FOLDER = Path(
    f"./.cache/{version_info.major}.{version_info.minor}.{version_info.micro}/"
)


def _function_hash[**P, R](fn: Callable[P, R]) -> str:
    fn = unwrap(fn)

    if not isinstance(fn, FunctionType):
        raise ValueError(f"fn {fn} is not an instance of types.FunctionType")

    source = getsource(fn)
    parsed = ast.parse(dedent(source))
    code = ast.dump(parsed)
    sig = signature(fn)
    name: str = fn.__qualname__

    hasher = blake3()
    hasher.update(name.encode("utf-8"))
    hasher.update(sig.format().encode("utf-8"))
    hasher.update(code.encode("utf-8"))

    return hasher.hexdigest()


def _parameter_hash[**P, R](
    fn: Callable[P, R], *args: P.args, **kwargs: P.kwargs
) -> str:
    fn = unwrap(fn)
    sig = signature(fn)

    bound = sig.bind(*args, **kwargs)
    bound.apply_defaults()

    serialized = dill.dumps(bound.arguments)
    hasher = blake3(serialized)

    return hasher.hexdigest()


def persistent[**P, R](func: Callable[P, R]) -> Callable[P, R]:
    fn_hash = _function_hash(func)

    folder = BASE_FOLDER / fn_hash
    folder.mkdir(parents=True, exist_ok=True)

    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        param_hash = _parameter_hash(func, *args, **kwargs)

        path = folder / f"{param_hash}.dill"

        if not path.exists():
            logger.opt(colors=True).debug(
                f"<white>[<cyan>CACHE</cyan>] [<red>MISS</red>] {func.__qualname__}({args}, {kwargs})</white>"
            )
            result = func(*args, **kwargs)
            payload = dill.dumps(result)

            temp_path = path.with_suffix(f".tmp.{uuid4().hex}")
            temp_path.write_bytes(payload)
            temp_path.replace(path)
        else:
            logger.opt(colors=True).debug(
                f"<white>[<cyan>CACHE</cyan>] [<green>HIT</green>] {func.__qualname__}({args}, {kwargs})</white>"
            )
            payload = path.read_bytes()
            result = dill.loads(payload)

        return result

    return wrapper