python-cachier / cachier

Persistent, stale-free, local and cross-machine caching for Python functions.
MIT License
527 stars 59 forks source link

Notify successful fresh cache retrieval #46

Open kitmonisit opened 3 years ago

kitmonisit commented 3 years ago

Hi, I'd like to propose an enhancement similar to verbose_cache.

I will call the new keyword argument notify_fresh. The wrapped function will return a tuple where the second value indicates if the value was fresh from cache.

This will enable logic outside the scope of the cached function to make decisions based on whether the data came from the cache.

For example, let's pretend we have a snobby server that hates being asked too many questions in rapid succession. If we get our answer fresh from the cache, then we can ask the next question right away:

@cachier()
def small_talk_to_snobby_server(query):
    response = "I'm fine, thank you!"
    return response

if __name__ == "__main__":
    response, is_fresh = small_talk_to_snobby_server(query="How are you?", notify_fresh=True)
    if not is_fresh:
        sleep(60)
    response = small_talk_to_snobby_server(query="What are you up to these days?")

Some self-critique

I find it rather clunky and API-breaking to make functions return a different data type than expected. I do not know any other way around this.

shaypal5 commented 3 years ago

Hey Kit! :)

Thank you for making this suggestion, and offering a PR and being active! The use case sounds solid, but it's definitely not the right implementation.

I would suggest providing the notify_fresh keyword with a "listener" object, in which the information you want to get will be stored by cachier. Than, you can check it's value after the value returns to get more information about the cache process involved in fetching said value.

For example:

@cachier()
def small_talk_to_snobby_server(query):
    response = "I'm fine, thank you!"
    return response

if __name__ == "__main__":
    freshness = Freshness()
    response = small_talk_to_snobby_server(query="How are you?", notify_fresh=freshness)
    if not freshness.value:
        sleep(60)
    response = small_talk_to_snobby_server(query="What are you up to these days?")

We will also want a generalized design, to ride the same API for more information, so this is better, I think:

if __name__ == "__main__":
    cinfo = CachierInfo()
    response = small_talk_to_snobby_server(query="How are you?", info=cinfo)
    if not cinfo.fresh:
        sleep(60)
    response = small_talk_to_snobby_server(query="What are you up to these days?")

What say you?

kitmonisit commented 3 years ago

Hi @shaypal5 thanks for the encouragement. And sorry for the delay, found the time to look at this just now.

That's a neat idea. I haven't dug deep into the code yet. Question, though: Certainly, when querying CachierInfo object for its fresh attribute, it needs to know which function I am referring to. So I imagine it stores the hash of the function call that's using it and associates it with a freshness boolean.

I'm not really familiar with function call hashes (other than that I've used function names as dict keys). A cursory glance of cachier's code base shows you make good use of hashes.

Am I on the right track?