[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.

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 instead, pass lazy=False or call .fetch() on a lazy call you already have.

[30]:
# Both of these produce a fully-loaded Call:
cache().load(key, lazy=False) == lazy_call.fetch()

[30]:
True