[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
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.
[6]:
start = time.time()
long_running_calculation(100)
end = time.time()
print(f'Second call took {end - start:.2f} seconds.')
Running calculation for 100...
Second call took 2.00 seconds.
As you can see, the second call returns almost instantly, because the result was cached. As soon as the argument changes, fleche runs the original fucntion again.
Recursive function
[7]:
@fleche
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
[8]:
start = time.time()
fib(20)
end = time.time()
print(f'fib(20) took {end - start:.4f} seconds with caching.')
fib(20) took 0.0067 seconds with caching.
Without caching, this would be much slower as each call to fib would be recomputed.
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.
[9]:
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
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.
Query returns an iterator object, that also defines additional utilities, e.g. to create a table of queried calls, do
[10]:
long_running_calculation.query().table()
[10]:
| name | module | timestart | timestop | walltime | |
|---|---|---|---|---|---|
| 4435e2927c194eff1bb90270ab3f353f0db1a8b50ec04932de88f7a4e5745953 | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000136 |
| cb9db168b7a8eb99bda637a54241ad5d4e2726b5cd67ad4cfb21baa5874c2b9e | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000227 |
| a7ce6824785adc406ca3561dcf98b3c64ddf1539d2467e1b9e6318e4a97368e8 | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000230 |
The table includes the call digest as an index and the metadata associated with the call.
Arguments can be selectively included in the table via the arguments. You only pay the loading cost for the specified arguments, not for arguments that are not included in the table.
[11]:
long_running_calculation.query().table(arguments=['x'])
[11]:
| name | module | x | timestart | timestop | walltime | |
|---|---|---|---|---|---|---|
| 4435e2927c194eff1bb90270ab3f353f0db1a8b50ec04932de88f7a4e5745953 | long_running_calculation | __main__ | 2 | 1.774227e+09 | 1.774227e+09 | 2.000136 |
| cb9db168b7a8eb99bda637a54241ad5d4e2726b5cd67ad4cfb21baa5874c2b9e | long_running_calculation | __main__ | 100 | 1.774227e+09 | 1.774227e+09 | 2.000227 |
| a7ce6824785adc406ca3561dcf98b3c64ddf1539d2467e1b9e6318e4a97368e8 | long_running_calculation | __main__ | 10 | 1.774227e+09 | 1.774227e+09 | 2.000230 |
[12]:
long_running_calculation.query().table(arguments=['x'], results=True)
[12]:
| name | module | result | x | timestart | timestop | walltime | |
|---|---|---|---|---|---|---|---|
| 4435e2927c194eff1bb90270ab3f353f0db1a8b50ec04932de88f7a4e5745953 | long_running_calculation | __main__ | 4 | 2 | 1.774227e+09 | 1.774227e+09 | 2.000136 |
| cb9db168b7a8eb99bda637a54241ad5d4e2726b5cd67ad4cfb21baa5874c2b9e | long_running_calculation | __main__ | 10000 | 100 | 1.774227e+09 | 1.774227e+09 | 2.000227 |
| a7ce6824785adc406ca3561dcf98b3c64ddf1539d2467e1b9e6318e4a97368e8 | long_running_calculation | __main__ | 100 | 10 | 1.774227e+09 | 1.774227e+09 | 2.000230 |
via Cache
[13]:
from fleche.call import QueryCall
[14]:
cache().query(QueryCall(module="__main__")).table().head()
[14]:
| name | module | timestart | timestop | walltime | |
|---|---|---|---|---|---|
| 4435e2927c194eff1bb90270ab3f353f0db1a8b50ec04932de88f7a4e5745953 | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000136 |
| cb9db168b7a8eb99bda637a54241ad5d4e2726b5cd67ad4cfb21baa5874c2b9e | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000227 |
| 698e29f05ba00ee23503848cd166215f62cd976d54431594036979d8d56f254f | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000004 |
| 405dfbadf453a9d5bbe3482fbb05251a6fc18b90045f8b4edeafb1c6f236fc80 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000002 |
| 24231d9bc47f7abc0ea485d178fc8457dce8082790f8c948b936ebe906352225 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000838 |
[15]:
cache().query(QueryCall(name="fib")).table().head()
[15]:
| name | module | timestart | timestop | walltime | |
|---|---|---|---|---|---|
| 698e29f05ba00ee23503848cd166215f62cd976d54431594036979d8d56f254f | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000004 |
| 405dfbadf453a9d5bbe3482fbb05251a6fc18b90045f8b4edeafb1c6f236fc80 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000002 |
| 24231d9bc47f7abc0ea485d178fc8457dce8082790f8c948b936ebe906352225 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000838 |
| dabebddba19859bdb1a075e422b1842dde433c7d6ca51c2b9a284dc1bb743d79 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.001454 |
| e2b3a4eaf9036f83560677b9bad64cbb8263cf3496ba6faeefc15b4231178912 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.001772 |
[16]:
cache().query(QueryCall(arguments={"x": 100})).table()
[16]:
| name | module | timestart | timestop | walltime | |
|---|---|---|---|---|---|
| cb9db168b7a8eb99bda637a54241ad5d4e2726b5cd67ad4cfb21baa5874c2b9e | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000227 |
| 3fb9bd0ba9c388530e4a0f5fbb789aaf7b5c598c1fae748de84aad8f244a2d8f | double | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000019 |
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.
[17]:
@fleche
def another_calculation(a, b):
return a + b
[18]:
with tags(project='my_project', category='testing'):
another_calculation(1, 2)
another_calculation(3, 4)
This metadata is stored alongside the cached result. This metadata can be used to query the cache as well.
[19]:
# 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 LazyArguments({'a': 'da217d50752f3371d9f8b62a3e72409592bd34b74e14fdac43e2b137bd59f21f', 'b': '92a9b214d814a4a7b5f9ba52e2248c6d16ec0196d8cd7798b345471118bf0c67'}) {'project': 'my_project', 'category': 'testing'}
LazyArguments({'a': '65d52a82c5a72f12ca0499522dc9274a0e6822e1038630ba68f94400b3e4c98f', 'b': '83ada2198553b88cb3d0882f7fca8c4e9531049b978df3e9e3b5d6301c6c0bfa'}) 7
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.
[20]:
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
[21]:
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.
[22]:
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.
Gotcha: Helper Methods on Decorated Methods
When @fleche decorates a class method, the helper methods (.call, .digest, .query, .load, .contains, .rerun) do not automatically bind self. Accessing obj.compute.query returns the same unbound helper as MyClass.compute.query.
You must pass the instance explicitly:
# Pass self as the first positional argument
obj.compute.query(obj, x=5)
obj.compute.contains(obj, x=5)
obj.compute.digest(obj, x=5)
# Or as a keyword argument
obj.compute.query(self=obj, x=5)
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.
[23]:
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))}")
Value Digest: 60079f7901a9295349d1796c037afc132e81286f785ddeeb763104ef02363102
Short digest: 60079f79
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.
[24]:
@fleche
def another_calculation(a, b):
return a + b
[25]:
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.
[26]:
cache().table()
[26]:
| name | module | timestart | timestop | walltime | project | category | |
|---|---|---|---|---|---|---|---|
| 4435e2927c194eff1bb90270ab3f353f0db1a8b50ec04932de88f7a4e5745953 | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000136 | NaN | NaN |
| cb9db168b7a8eb99bda637a54241ad5d4e2726b5cd67ad4cfb21baa5874c2b9e | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000227 | NaN | NaN |
| 698e29f05ba00ee23503848cd166215f62cd976d54431594036979d8d56f254f | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000004 | NaN | NaN |
| 405dfbadf453a9d5bbe3482fbb05251a6fc18b90045f8b4edeafb1c6f236fc80 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000002 | NaN | NaN |
| 24231d9bc47f7abc0ea485d178fc8457dce8082790f8c948b936ebe906352225 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000838 | NaN | NaN |
| dabebddba19859bdb1a075e422b1842dde433c7d6ca51c2b9a284dc1bb743d79 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.001454 | NaN | NaN |
| e2b3a4eaf9036f83560677b9bad64cbb8263cf3496ba6faeefc15b4231178912 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.001772 | NaN | NaN |
| 981a54293b3027931a47d21c275955d6dab2fe6d8fa4c2f4fcd3190b6187b0ac | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.002040 | NaN | NaN |
| fdb0a9b8b1df91293924ad8fc03bcf041f3e50f924e9d199b8606eed60579d91 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.002305 | NaN | NaN |
| db6b5632c77a01a692e13c486c54536a0c46907972d93900a60a1eab59110b93 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.002581 | NaN | NaN |
| cd94bda61e484ca82c470b6ab50c0f1b98055e6a905d82f18b57abc5e07b6457 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.002940 | NaN | NaN |
| f4efb0e68400d195d8d57051088964d98b9ae4f7dbb78dad4feb055b56fd47de | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.003206 | NaN | NaN |
| bc7f8b67d293d9fbdec343f49c44d46e655516537cc492ba3a990edb7f86f176 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.003480 | NaN | NaN |
| 6fbe26ceafe80276c7714b2aae14c25bd38e24683d6624f01322946f5e53e7cf | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.003752 | NaN | NaN |
| f3cefd42fd07119792c38084ecbb33f1955ccd83d8021e1bd078db3377cca4c3 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.004020 | NaN | NaN |
| 2b7c9e8c5e0a73afddb28a49d47415462b3c84adc05c35567b81514b90c053f2 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.004293 | NaN | NaN |
| 60bf26e7cdec62d1a8f9ebfbebb336850cf588d547badc7a18a0001fd0b65cf9 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.004573 | NaN | NaN |
| 9b23a905e639149d35169118ffefadec03a181cbd290006620e671655b1499c3 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.004852 | NaN | NaN |
| 18e77dab7a4a0385a66512547336e7a02ee62803634fd801e6dd49c6e5444e2f | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.005127 | NaN | NaN |
| e1a78503325bcbb94116e03febed590b14bdd7f955c4f15b8649e613fbd6bf5c | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.005413 | NaN | NaN |
| 34814b3262b7920c7cef43eaf8a1e86e849611ed0d0317c4342b29231a8213ea | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.005702 | NaN | NaN |
| 9214caae9e9819230882f7e9f0f1970fd5f2fc354c3404d1fd7ef30aa2c75873 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.005992 | NaN | NaN |
| c5a5bb6cf5b69c7eb5e244ed540af44391dff12a77fcecf19505cc660b50e6d9 | fib | __main__ | 1.774227e+09 | 1.774227e+09 | 0.006301 | NaN | NaN |
| a7ce6824785adc406ca3561dcf98b3c64ddf1539d2467e1b9e6318e4a97368e8 | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000230 | NaN | NaN |
| 3fb9bd0ba9c388530e4a0f5fbb789aaf7b5c598c1fae748de84aad8f244a2d8f | double | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000019 | NaN | NaN |
| a8a3061653183cff08e5c414f9a4087550ab5e33cc186ae7a2e91aa1fbac8c80 | another_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000011 | my_project | testing |
| 1e352b538b9219d4e5fcaf429882a4b3587f3bc6358ef0feca453b301b40156a | another_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000025 | my_project | testing |
| 81d4df70836ded7d8a0dd70348976c431210ff824d870acb2932b984e7936e14 | compute | __main__ | 1.774227e+09 | 1.774227e+09 | 1.000169 | NaN | NaN |
| e9f9ca077d0566f335e54df480d202815d7b5b5ac5e7ccf7ba47c5bf2fa219ae | compute | __main__ | 1.774227e+09 | 1.774227e+09 | 1.000242 | NaN | NaN |
Filtering
The metadata table is just pandas so you can query and filter as you like.
[27]:
cache().table().query('name!="fib"')
[27]:
| name | module | timestart | timestop | walltime | project | category | |
|---|---|---|---|---|---|---|---|
| 4435e2927c194eff1bb90270ab3f353f0db1a8b50ec04932de88f7a4e5745953 | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000136 | NaN | NaN |
| cb9db168b7a8eb99bda637a54241ad5d4e2726b5cd67ad4cfb21baa5874c2b9e | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000227 | NaN | NaN |
| a7ce6824785adc406ca3561dcf98b3c64ddf1539d2467e1b9e6318e4a97368e8 | long_running_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 2.000230 | NaN | NaN |
| 3fb9bd0ba9c388530e4a0f5fbb789aaf7b5c598c1fae748de84aad8f244a2d8f | double | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000019 | NaN | NaN |
| a8a3061653183cff08e5c414f9a4087550ab5e33cc186ae7a2e91aa1fbac8c80 | another_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000011 | my_project | testing |
| 1e352b538b9219d4e5fcaf429882a4b3587f3bc6358ef0feca453b301b40156a | another_calculation | __main__ | 1.774227e+09 | 1.774227e+09 | 0.000025 | my_project | testing |
| 81d4df70836ded7d8a0dd70348976c431210ff824d870acb2932b984e7936e14 | compute | __main__ | 1.774227e+09 | 1.774227e+09 | 1.000169 | NaN | NaN |
| e9f9ca077d0566f335e54df480d202815d7b5b5ac5e7ccf7ba47c5bf2fa219ae | compute | __main__ | 1.774227e+09 | 1.774227e+09 | 1.000242 | NaN | NaN |
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:
[28]:
# 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 LazyArguments({'a': 'da217d50752f3371d9f8b62a3e72409592bd34b74e14fdac43e2b137bd59f21f', 'b': '92a9b214d814a4a7b5f9ba52e2248c6d16ec0196d8cd7798b345471118bf0c67'}) {'project': 'my_project', 'category': 'testing'}
LazyArguments({'a': '65d52a82c5a72f12ca0499522dc9274a0e6822e1038630ba68f94400b3e4c98f', 'b': '83ada2198553b88cb3d0882f7fca8c4e9531049b978df3e9e3b5d6301c6c0bfa'}) 7
Lazy Loading
When you load a call from the cache, fleche returns it as a LazyCall by default. Arguments and results are only fetched from storage when you actually access them — so iterating over a large cache or inspecting metadata stays fast even when individual results are huge.
[29]:
# Default load: returns a LazyCall — no deserialization yet
key = fib.digest(20)
lazy_call = cache().load(key)
print(f"Got a {type(lazy_call).__name__} for {lazy_call.name}(20)")
# Accessing .result triggers the actual load from storage
print(f"Result: {lazy_call.result}")
Obtained LazyCall for fib(20)
Accessing result now (triggers load)...
Result: 6765
To load everything upfront, call .fetch() on a lazy call you already have.
[ ]:
# Fetch everything upfront from a lazy call:
lazy_call.fetch()