The trick i learned was just to add a last updated timestamp to the sqlalchemy model. You'll still incur the full overhead of the full db query (don't try to return just the timestamp field, maybe different in your case but when i test i find the overhead of RTT'ing to the database twice completely negates any benefit - maybe if i had memcached or redis in my stack that would make a better place for the timestamp, but i always just have a postgres and nothing else)
With that last updated time as a seed for the etag value for dynamic pages, it means when the cache control public header times out, and a request arrives then in the request handler i can do the orm query, and then short circuit out before template rendering or page transfer if the last updated vs etag match.
I hope that rubbish attempt at describing made sense :-) basically i like this approach because it works even for logged in per-user pages that don't change much. I know there are slightly faster approaches for non-logged in pages but i just handle everything with the same pattern.