[1]:
!rm .fleche -rf
Getting Started with Fleche
This notebook demonstrates the main features of the fleche library, a caching library for Python.
Long-running calculation
[2]:
import time
from fleche import fleche, cache, tags, project
from fleche.digest import Digest
No config file found. Using default memory cache.
[3]:
@fleche
def long_running_calculation(x):
print(f'Running calculation for {x}...')
time.sleep(2)
return x * x
[4]:
start = time.time()
long_running_calculation(2)
end = time.time()
print(f'First call took {end - start:.2f} seconds.')
Running calculation for 2...
First call took 2.00 seconds.
[5]:
start = time.time()
long_running_calculation(2)
end = time.time()
print(f'Second call took {end - start:.2f} seconds.')
Second call took 0.00 seconds.
As you can see, the second call returns almost instantly, because the result was cached.
Recursive function
[6]:
@fleche
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
[7]:
start = time.time()
fib(20)
end = time.time()
print(f'fib(20) took {end - start:.4f} seconds with caching.')
fib(20) took 0.0127 seconds with caching.
Without caching, this would be much slower as each call to fib would be recomputed.
Caching Methods of User-defined Types
fleche can also cache methods of classes. For this to work, the class must be “digest-compatible”. You can make a class digest-compatible by implementing a __digest__ method or by using a dataclass.
[8]:
class MyClass:
def __init__(self, val):
self.val = val
def __digest__(self):
# The digest defines how the instance is identified in the cache
return Digest(str(self.val))
@fleche
def compute(self, x):
print(f"Computing {self.val} + {x}...")
time.sleep(1)
return self.val + x
[9]:
obj = MyClass(10)
start = time.time()
print(f"Result: {obj.compute(5)}")
print(f"First call took {time.time() - start:.2f} seconds.")
start = time.time()
print(f"Result: {obj.compute(5)}")
print(f"Second call (same instance) took {time.time() - start:.2f} seconds.")
Computing 10 + 5...
Result: 15
First call took 1.00 seconds.
Result: 15
Second call (same instance) took 0.00 seconds.
If you mutate the instance such that its digest changes, the cache will be missed.
[10]:
obj.val = 20
start = time.time()
print(f"Result: {obj.compute(5)}")
print(f"Call after mutation took {time.time() - start:.2f} seconds.")
Computing 20 + 5...
Result: 25
Call after mutation took 1.00 seconds.
Passing Digests as Arguments
fleche supports passing Digest objects directly to cached functions. When a function receives a Digest, fleche automatically expands it to its actual value from the cache before executing the function. You can use the convenience wrapper D to mark a string as a digest.
[11]:
from fleche import D
from fleche.digest import digest as value_digest
@fleche
def double(x):
print(f"Doubling {x}...")
return x * 2
# 1. Run the calculation to ensure it is cached
long_running_calculation(10)
# 2. Compute the value digest for 100 (the cached result)
v = long_running_calculation(10)
val_dig = value_digest(v)
print(f"Value Digest: {val_dig}")
# 3. Pass a short digest prefix of the value to double(); it will expand to 100.
short = str(val_dig)[:8]
print(f"Short digest: {short}")
print(f"Result: {double(D(short))}")
Running calculation for 10...
Value Digest: 60079f7901a9295349d1796c037afc132e81286f785ddeeb763104ef02363102
Short digest: 60079f79
Doubling 100...
Result: 200
Metadata
fleche allows you to add metadata to your cached functions using the tags context manager. This can be useful for organizing and querying your results.
[12]:
@fleche
def another_calculation(a, b):
return a + b
[13]:
with tags(project='my_project', category='testing'):
another_calculation(1, 2)
another_calculation(3, 4)
This metadata is stored alongside the cached result. You can then use the metadata.table method to view the metadata for all cached results.
[14]:
cache().table()
[14]:
| name | module | timestart | timestop | walltime | project | category | |
|---|---|---|---|---|---|---|---|
| 4435e2927c194eff1bb90270ab3f353f0db1a8b50ec04932de88f7a4e5745953 | long_running_calculation | __main__ | 1.773968e+09 | 1.773968e+09 | 2.000198 | NaN | NaN |
| 698e29f05ba00ee23503848cd166215f62cd976d54431594036979d8d56f254f | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.000009 | NaN | NaN |
| 405dfbadf453a9d5bbe3482fbb05251a6fc18b90045f8b4edeafb1c6f236fc80 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.000007 | NaN | NaN |
| 24231d9bc47f7abc0ea485d178fc8457dce8082790f8c948b936ebe906352225 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.002490 | NaN | NaN |
| dabebddba19859bdb1a075e422b1842dde433c7d6ca51c2b9a284dc1bb743d79 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.003173 | NaN | NaN |
| e2b3a4eaf9036f83560677b9bad64cbb8263cf3496ba6faeefc15b4231178912 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.003788 | NaN | NaN |
| 981a54293b3027931a47d21c275955d6dab2fe6d8fa4c2f4fcd3190b6187b0ac | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.004265 | NaN | NaN |
| fdb0a9b8b1df91293924ad8fc03bcf041f3e50f924e9d199b8606eed60579d91 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.004732 | NaN | NaN |
| db6b5632c77a01a692e13c486c54536a0c46907972d93900a60a1eab59110b93 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.005261 | NaN | NaN |
| cd94bda61e484ca82c470b6ab50c0f1b98055e6a905d82f18b57abc5e07b6457 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.005755 | NaN | NaN |
| f4efb0e68400d195d8d57051088964d98b9ae4f7dbb78dad4feb055b56fd47de | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.006238 | NaN | NaN |
| bc7f8b67d293d9fbdec343f49c44d46e655516537cc492ba3a990edb7f86f176 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.006701 | NaN | NaN |
| 6fbe26ceafe80276c7714b2aae14c25bd38e24683d6624f01322946f5e53e7cf | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.007159 | NaN | NaN |
| f3cefd42fd07119792c38084ecbb33f1955ccd83d8021e1bd078db3377cca4c3 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.007635 | NaN | NaN |
| 2b7c9e8c5e0a73afddb28a49d47415462b3c84adc05c35567b81514b90c053f2 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.008112 | NaN | NaN |
| 60bf26e7cdec62d1a8f9ebfbebb336850cf588d547badc7a18a0001fd0b65cf9 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.008859 | NaN | NaN |
| 9b23a905e639149d35169118ffefadec03a181cbd290006620e671655b1499c3 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.009603 | NaN | NaN |
| 18e77dab7a4a0385a66512547336e7a02ee62803634fd801e6dd49c6e5444e2f | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.010176 | NaN | NaN |
| e1a78503325bcbb94116e03febed590b14bdd7f955c4f15b8649e613fbd6bf5c | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.010721 | NaN | NaN |
| 34814b3262b7920c7cef43eaf8a1e86e849611ed0d0317c4342b29231a8213ea | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.011218 | NaN | NaN |
| 9214caae9e9819230882f7e9f0f1970fd5f2fc354c3404d1fd7ef30aa2c75873 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.011720 | NaN | NaN |
| c5a5bb6cf5b69c7eb5e244ed540af44391dff12a77fcecf19505cc660b50e6d9 | fib | __main__ | 1.773968e+09 | 1.773968e+09 | 0.012221 | NaN | NaN |
| 81d4df70836ded7d8a0dd70348976c431210ff824d870acb2932b984e7936e14 | compute | __main__ | 1.773968e+09 | 1.773968e+09 | 1.000190 | NaN | NaN |
| e9f9ca077d0566f335e54df480d202815d7b5b5ac5e7ccf7ba47c5bf2fa219ae | compute | __main__ | 1.773968e+09 | 1.773968e+09 | 1.000223 | NaN | NaN |
| a7ce6824785adc406ca3561dcf98b3c64ddf1539d2467e1b9e6318e4a97368e8 | long_running_calculation | __main__ | 1.773968e+09 | 1.773968e+09 | 2.000249 | NaN | NaN |
| dda964d172cde9eb85bc22b048444a149e7536453b23c8a90155a6c9dcc6f035 | double | __main__ | 1.773968e+09 | 1.773968e+09 | 0.000051 | NaN | NaN |
| a8a3061653183cff08e5c414f9a4087550ab5e33cc186ae7a2e91aa1fbac8c80 | another_calculation | __main__ | 1.773968e+09 | 1.773968e+09 | 0.000013 | my_project | testing |
| 1e352b538b9219d4e5fcaf429882a4b3587f3bc6358ef0feca453b301b40156a | another_calculation | __main__ | 1.773968e+09 | 1.773968e+09 | 0.000011 | my_project | testing |
Filtering
The metadata table is just pandas so you can query and filter as you like.
[15]:
cache().table().query('name!="fib"')
[15]:
| name | module | timestart | timestop | walltime | project | category | |
|---|---|---|---|---|---|---|---|
| 4435e2927c194eff1bb90270ab3f353f0db1a8b50ec04932de88f7a4e5745953 | long_running_calculation | __main__ | 1.773968e+09 | 1.773968e+09 | 2.000198 | NaN | NaN |
| 81d4df70836ded7d8a0dd70348976c431210ff824d870acb2932b984e7936e14 | compute | __main__ | 1.773968e+09 | 1.773968e+09 | 1.000190 | NaN | NaN |
| e9f9ca077d0566f335e54df480d202815d7b5b5ac5e7ccf7ba47c5bf2fa219ae | compute | __main__ | 1.773968e+09 | 1.773968e+09 | 1.000223 | NaN | NaN |
| a7ce6824785adc406ca3561dcf98b3c64ddf1539d2467e1b9e6318e4a97368e8 | long_running_calculation | __main__ | 1.773968e+09 | 1.773968e+09 | 2.000249 | NaN | NaN |
| dda964d172cde9eb85bc22b048444a149e7536453b23c8a90155a6c9dcc6f035 | double | __main__ | 1.773968e+09 | 1.773968e+09 | 0.000051 | NaN | NaN |
| a8a3061653183cff08e5c414f9a4087550ab5e33cc186ae7a2e91aa1fbac8c80 | another_calculation | __main__ | 1.773968e+09 | 1.773968e+09 | 0.000013 | my_project | testing |
| 1e352b538b9219d4e5fcaf429882a4b3587f3bc6358ef0feca453b301b40156a | another_calculation | __main__ | 1.773968e+09 | 1.773968e+09 | 0.000011 | my_project | testing |
Querying Cached Calls via Function Wrapper
You can retrieve previously cached calls that match some of your function’s arguments and metadata using the function wrapper’s query method. Any field left as None is treated as a wildcard. Arguments and result are compared by digest internally, but the wrapper decodes them back to Python objects when returning matches.
Example using the another_calculation wrapper we created above:
[16]:
# Query by metadata presence (tags) and a specific key-value filter
for call in another_calculation.query(1, 2, metadata={"tags": {}}):
# presence-only: any call with 'tags'
print(call.name, call.arguments, call.metadata.get("tags"))
for call in another_calculation.query(3, 4, metadata={"tags": {"project": "my_project"}}):
# equality filter on metadata
assert call.metadata["tags"]["project"] == "my_project"
# arguments and result are decoded if they were stored as digests
print(call.arguments, call.result)
another_calculation {'a': 1, 'b': 2} {'project': 'my_project', 'category': 'testing'}
{'a': 3, 'b': 4} 7
Lazy Loading
When dealing with large results or many cached calls, you might not want to load everything at once. fleche supports “lazy loading”, where arguments and results are only fetched from the cache when you actually access them.
[17]:
from fleche.call import LazyCall
# 1. Get the digest for a known call
key = fib.digest(20)
# 2. Load it lazily
lazy_call = cache().load(key, lazy=True)
print(f"Obtained {type(lazy_call).__name__} for {lazy_call.name}(20)")
# 3. Accessing .result or .arguments will now trigger the load
print("Accessing result now (triggers load)...")
print(f"Result: {lazy_call.result}")
Obtained LazyCall for fib(20)
Accessing result now (triggers load)...
Result: 6765
This is on by default. You can request to load everything at once using lazy=False, or the .fetch method on a lazily loaded call.
[18]:
cache().load(key).fetch() == cache().load(key, lazy=False)
[18]:
True