Hi all. First, thanks for an awesome piece of soft...
# hamilton-help
g
Hi all. First, thanks for an awesome piece of software. It has been a pleasure to work with Hamilton. Question: how can I retrieve cached artifacts after a run, from a different python process? More context in the thread.
I don't have access to the driver that generated the cached artifacts anymore (it was created by a separate python process). If I instantiate a driver, like in the caching tutorial, it won't see the runs (in fact
dr.cache.run_ids()
will be an empty list). I have already discovered
hamilton.caching.cache_key.create_cache_key
and
hamilton.caching.stores.sqlite.SQLiteMetadataStore
but as you can see, the SQLite metadata store, via this API, does not give me all the info I need to build the correct cache keys (it's missing the
dependencies_data_versions
info)
Copy code
[ins] In [81]: cm = hamilton.caching.stores.sqlite.SQLiteMetadataStore('.hamilton_cache')

[ins] In [82]: cm.get_last_run()
Out[82]:
[{'node_name': 'unpack_pull_config_params',
  'code_version': 'e12571afc3d1a8c1b1e45a83b51928d400092ce807294425d739e945c9aefc00',
  'data_version': 'sZbMajJ4izr9OBqiaGl_oFdxK_3sLLtcd6oDwQ=='},
 {'node_name': 'earliest_transaction_date',
  'code_version': 'e12571afc3d1a8c1b1e45a83b51928d400092ce807294425d739e945c9aefc00',
  'data_version': '-Bglmx2Xj0-HtonjDSQ7uQ=='},
 {'node_name': 'latest_transaction_date',
  'code_version': 'e12571afc3d1a8c1b1e45a83b51928d400092ce807294425d739e945c9aefc00',
  'data_version': 'HrKZKK-9DWjxMxLOqJmQTw=='},
 {'node_name': 'run_date',
  'code_version': 'e12571afc3d1a8c1b1e45a83b51928d400092ce807294425d739e945c9aefc00',
  'data_version': 'hirf8ZYkDNoT7i2WK8vUiQ=='},
 {'node_name': 'rcc_transactions',
  'code_version': '4d862064d9deb8e0aa8a8fb8e1e578e0c7a62fcbaf4c947015fcc1345ba240e9',
  'data_version': 'BgHnGxP--F2Qt5eIpXSnT-gHERzRW9T2Yc0AAg=='}]
image.png
More context: the cache was generated by a pytest run (the tests call hamilton to generate the pytest fixtures). So after the test runs, I may want to interact with the data used in that last test run (say to explore something, debug, etc. in a notebook). So I want to access that cached artifact.
s
okay thanks for the context. let look at the code to see if what I want to do will work.
g
What code do you want to see? Everything that led to the generation of the cached artifacts?
s
sorry I’m looking at internal code
👍 1
okay right now I don’t think we expose enough functionality to make this easy. CC @Thierry Jean
@Guilherme Pereira de Freitas so thought as a work around: 1.
cm.get_run_ids()
should list all runs -> there is a way to log a run_id when the test runs. 2. then
cm.get_run(run_id)
will give you the data structure above 3. default store is the filesystem — the file by default will be a pickle and the name of the file is the data version value (we will likely change this in future) 4. you now have node name -> data version mapping. So you could manually then do the following:
Copy code
from hamilton.caching.stores.file import FileResultStore

data_version = ... # from metadatastore result
result_store = FileResultStore(...)
result = result_store.get(data_version)
Otherwise let’s create an issue here to make this more ergonomic.
t
Hi @Guilherme Pereira de Freitas, if you have a DAG visualization, that could help ground the discussion! For DAG, with nodes
A -> B; A -> C; B -> C
After the first `.execute()`: • the
MetadataStore
will have an entry for
A, B, C
• the
ResultStore
will have an entry for
A, B, C
• an entry in the
MetadataStore
, must minimally store the
cache_key
and the
data_version
for each node. They may contain other metadata ◦ as you've found the
SQLiteMetadataStore
, there's currently a bug. It doesn't actually return the
cache_key
value. You can still use the
data_version
for retrieval though. I have fix planned for this! • to get the
dependencies_data_versions
, you can use
hamilton.caching.cache_key.decode_key()
. You will probably find the Internals section of the tutorial notebook informative When you don't have access to a driver
Copy code
from hamilton.caching.stores.sqlite import SQLiteMetadataStore
from hamilton.caching.cache_key import decode_key

metadata_store = SQLiteMetadataStore(...)
run_ids = metadata_stores.get_run_ids()
run_id = run_ids[2]

# run_info is a list of {node_name: ..., code_version: ..., data_version: ...}
run_info = metadata_store.get_run(run_id)
Then, you can use the
data_version
(for the current node or from a dependency via the decoded
cache_key
)
Copy code
from hamilton.caching.stores.file import FileResultStore

# select the right entry
node_name = "foo"
node_foo = next(n for n in run_info if n["node_name"] == node_name)

data_version = node_foo["data_version"]
result_store = FileResultStore(...)
result = result_store.get(data_version)
👍 1
g
I'll try this in a bit, will get back here later.
s
ah cool — I missed the fileresultstore API.
t
I reread your initial question, and trimmed my reply. If your goal is only to access store results/values, you probably don't have to interact with
cache_key
. The relevant value is
data_version
, which can be retrieved from
MetadataStore.get_run()
and then used with
ResultStore.get(data_version)
If you have questions about the
cache_key
, I'm happy to help too! random: it's a side-effect and this may change, but currently values for
code_version
,
data_version
, and
cache_key
should be distinctive enough •
code_version
is a hash converted to a string (fixed length); it only includes alphanumeric characters ◦
c9e3377d6c5044944bd89eeb7073c730ee8707627c39906b4156c6411f056f00
•
data_version
uses base64 encoding (compressed, but variable length), so you'll fin distinctive trailing
==
◦
t-BDcMLikFSNdn4piUKy1mBcKPoEsnsYjUNzWg==
•
cache_key
is in 3 parts (and can be very long):
f"{node_name}-{code_version}-{encoding(dependencies_data_versions)}
◦
raw_data-9d727859b9fd883247c3379d4d25a35af4a56df9d9fde20c75c6375dde631c68-eF6zycvPS7UDAAeiAis=
g
The result store worked.
🙌 1
Yeah, I think I saw the cache key in the sqlite db itself when I was poking around.
I noticed the various keys/version/"signatures" do look different.
Thank you so much for your help, @Thierry Jean!
t
My pleasure! Caching is a newer feature that we're excited to expand upon, so let us know if you hit other edge cases. Happy to learn more about your use case too!
g
By the way, if anyone is interested in seeing how I'm creating pytest fixtures automatically from hamilton, happy to show that.
👀 1
It's really handy. The current implementation does rely on caching crucially though, for efficiency.
👍 1
s
contribution and blog post please! 🙂
g
The heart of the code is this fixture (I'm setting the driver
ham_driver
outside of that reading in all the modules with dag nodes) :
Copy code
@pytest.fixture()
def hamilton_fixtures(request, inputs):
    """Returns a dict of hamilton DAG fixtures.
    """
    # load the fixtures
    hamilton_vars = set(node.name for node in ham_driver.list_available_variables())
    requested_fixtures = set(request.fixturenames)
    hamilton_fixtures = hamilton_vars.intersection(requested_fixtures)

    <http://logger.info|logger.info>("Loading hamilton_fixtures...")

    # fixtures is a dictionary
    fixtures = ham_driver.execute(final_vars=hamilton_fixtures, inputs=inputs)

    logger.debug(f"Non-NULL hamilton fixtures: {[k for k in fixtures if fixtures[k] is not None]}")
    logger.debug(fixtures)
    <http://logger.info|logger.info>("[DONE] Loading hamilton_fixtures.")

    return fixtures
And then in
conftest.py
Copy code
this_module = sys.modules[__name__]
for node in ham_driver.list_available_variables():
    def fixture_wrapper(varname=node.name):
        @pytest.fixture
        def hamfix(hamilton_fixtures):
            return hamilton_fixtures.get(varname)

        return hamfix

    setattr(this_module, node.name, fixture_wrapper(node.name))
So then you can just go write a
test_func(node_A, node_B)
and jump straight into the asserts for
node_A
and
node_B
.
Happy to collaborate on that blog post!
The main problem is that I don't know a way to get pytest to tell me "all the fixtures needed for all tests", bu tonly "all the fixtures needed for a given test" (via the builtin
request
fixture). So we "compute" the fixtures for every test separately, which is inefficient. However, with caching, that is not so bad.
If anyone has an idea of how to get a list of all the fixtures needed for that whole pytest session, that will be awesome. I guess I should ask the pytest folks about it.
I'll try to do a better writeup later that I can send to you guys. Again, thanks for the help!
(and for Hamilton!)
@Thierry Jean I described the use-case at a high level here: https://hamilton-opensource.slack.com/archives/C03M55D7MK6/p1729657437801359 And in more detail in the long message above. Happy to schedule a call or something if you guys want to see how I'm using hamilton for this use-case. I'd rather do that than right a document that may or may not be used. If you guys show interest in the use-case, I can talk to my boss about writing a blog post for this.
s
@Guilherme Pereira de Freitas we have office hours most Tuesdays at 9:30am PT if you can make that — can chat there.
I haven’t looked into pytest too much, but it’s pretty extensible, and I think there must be a way to figure out all fixtures for a session.
g
Yeah, I can make Tuesday 9:30pm. See you guys soon.
👍 1
🙌 1
s
@Guilherme Pereira de Freitas it’s 9:30*am* (just confirming pointing that out)