import%20marimo%0A%0A__generated_with%20%3D%20%220.17.6%22%0Aapp%20%3D%20marimo.App(width%3D%22medium%22)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Serializing%20and%20Reloading%20Loman%20Computations%0A%0A%20%20%20%20Loman%20computations%20can%20be%20saved%20to%20a%20JSON%20file%20and%20reloaded%20later%20%E2%80%94%20useful%20for%3A%0A%0A%20%20%20%20-%20**Post-mortem%20debugging**%3A%20save%20a%20batch%20run%20in%20full%20detail%20so%20you%20can%20inspect%20every%20intermediate%20value%20if%20something%20goes%20wrong.%0A%20%20%20%20-%20**Checkpoint%20%2F%20resume**%3A%20persist%20a%20partially-completed%20computation%20and%20pick%20up%20where%20you%20left%20off.%0A%20%20%20%20-%20**Reproducibility**%3A%20store%20the%20exact%20inputs%20and%20results%20alongside%20the%20code%20that%20produced%20them.%0A%0A%20%20%20%20This%20notebook%20walks%20through%20the%20key%20features%20of%20%60write_json%60%20%2F%20%60read_json%60%3A%0A%0A%20%20%20%201.%20Basic%20round-trip%0A%20%20%20%202.%20Excluding%20nodes%20with%20%60serialize%3DFalse%60%0A%20%20%20%203.%20Handling%20lambdas%20with%20%60ComputationSerializer(use_dill_for_functions%3DTrue)%60%0A%20%20%20%204.%20Preserving%20%60PINNED%60%20state%0A%20%20%20%205.%20Post-mortem%20inspection%20of%20%60ERROR%60%20nodes%0A%20%20%20%206.%20Pandas%20DataFrames%20as%20node%20values%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%201.%20Basic%20round-trip%0A%0A%20%20%20%20We%20start%20with%20a%20small%20computation%3A%20three%20nodes%20where%20%60c%60%20depends%20on%20%60a%60%20and%20%60b%60.%0A%20%20%20%20All%20three%20nodes%20use%20importable%20module-level%20functions%2C%20so%20they%20serialise%20cleanly.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20io%0A%20%20%20%20import%20json%0A%20%20%20%20import%20math%0A%0A%20%20%20%20from%20loman%20import%20Computation%2C%20ComputationSerializer%2C%20States%0A%0A%20%20%20%20def%20square(x)%3A%0A%20%20%20%20%20%20%20%20return%20x**2%0A%0A%20%20%20%20def%20hypotenuse(a%2C%20b)%3A%0A%20%20%20%20%20%20%20%20return%20math.sqrt(a%20%2B%20b)%0A%0A%20%20%20%20comp%20%3D%20Computation()%0A%20%20%20%20comp.add_node(%22a%22%2C%20value%3D3.0)%0A%20%20%20%20comp.add_node(%22b%22%2C%20value%3D4.0)%0A%20%20%20%20comp.add_node(%22a_sq%22%2C%20square%2C%20kwds%3D%7B%22x%22%3A%20%22a%22%7D)%0A%20%20%20%20comp.add_node(%22b_sq%22%2C%20square%2C%20kwds%3D%7B%22x%22%3A%20%22b%22%7D)%0A%20%20%20%20comp.add_node(%22c%22%2C%20hypotenuse%2C%20kwds%3D%7B%22a%22%3A%20%22a_sq%22%2C%20%22b%22%3A%20%22b_sq%22%7D)%0A%20%20%20%20comp.compute_all()%0A%20%20%20%20comp.to_dict()%0A%20%20%20%20return%20Computation%2C%20ComputationSerializer%2C%20States%2C%20comp%2C%20io%2C%20json%0A%0A%0A%40app.cell%0Adef%20_(comp)%3A%0A%20%20%20%20comp%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Save%20to%20an%20in-memory%20buffer%20and%20reload%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20comp%2C%20io)%3A%0A%20%20%20%20buf%20%3D%20io.StringIO()%0A%20%20%20%20comp.write_json(buf)%0A%20%20%20%20buf.seek(0)%0A%20%20%20%20comp_loaded%20%3D%20Computation.read_json(buf)%0A%20%20%20%20comp_loaded.to_dict()%0A%20%20%20%20return%20(comp_loaded%2C)%0A%0A%0A%40app.cell%0Adef%20_(comp_loaded)%3A%0A%20%20%20%20comp_loaded.src%5B%22c%22%5D%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20output%20is%20plain%20JSON%20text%20%E2%80%94%20open%20it%20in%20any%20text%20editor%20to%20inspect%20it.%0A%20%20%20%20Here%20is%20what%20the%20file%20actually%20looks%20like%20for%20this%20computation%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp%2C%20io%2C%20json)%3A%0A%20%20%20%20_buf%20%3D%20io.StringIO()%0A%20%20%20%20comp.write_json(_buf)%0A%20%20%20%20print(json.dumps(json.loads(_buf.getvalue())%2C%20indent%3D2))%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Each%20node%20carries%20its%20%60state%60%2C%20encoded%20%60value%60%2C%20and%20(where%20applicable)%20a%20%60func%60%0A%20%20%20%20reference%20stored%20as%20%60%7B%22type%22%3A%20%22func_ref%22%2C%20%22module%22%3A%20%22...%22%2C%20%22qualname%22%3A%20%22...%22%7D%60.%0A%20%20%20%20Edges%20record%20the%20dependency%20wiring%20including%20parameter%20names.%0A%0A%20%20%20%20The%20reloaded%20computation%20has%20the%20same%20values%20and%20states.%20The%20function%20references%20are%0A%20%20%20%20also%20preserved%20%E2%80%94%20we%20can%20update%20an%20input%20and%20recompute%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20reloaded%20computation%20has%20the%20same%20values%20and%20states.%20The%20function%20references%20are%0A%20%20%20%20also%20preserved%20%E2%80%94%20we%20can%20update%20an%20input%20and%20recompute%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp_loaded)%3A%0A%20%20%20%20comp_loaded.insert(%22a%22%2C%205.0)%0A%20%20%20%20comp_loaded.compute_all()%0A%20%20%20%20comp_loaded.to_dict()%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(comp_loaded)%3A%0A%20%20%20%20comp_loaded%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%202.%20Excluding%20nodes%20with%20%60serialize%3DFalse%60%0A%0A%20%20%20%20Some%20nodes%20hold%20values%20that%20should%20not%20be%20saved%20%E2%80%94%20database%20connections%2C%20licensed%0A%20%20%20%20data%2C%20or%20objects%20that%20cannot%20be%20serialised.%20Pass%20%60serialize%3DFalse%60%20when%20adding%20the%0A%20%20%20%20node%3A%20it%20will%20be%20stored%20as%20%60UNINITIALIZED%60%20in%20the%20file%20with%20no%20value.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20io)%3A%0A%20%20%20%20def%20_expensive_db_fetch()%3A%0A%20%20%20%20%20%20%20%20%23%20Pretend%20this%20returns%20data%20from%20a%20live%20database.%0A%20%20%20%20%20%20%20%20return%20%7B%22price%22%3A%2042.0%7D%0A%0A%20%20%20%20comp_skip%20%3D%20Computation()%0A%20%20%20%20comp_skip.add_node(%22db_conn%22%2C%20value%3Dobject()%2C%20serialize%3DFalse)%20%20%23%20not%20saved%0A%20%20%20%20comp_skip.add_node(%22raw_data%22%2C%20value%3D%7B%22price%22%3A%2042.0%7D)%20%20%23%20saved%0A%20%20%20%20comp_skip.add_node(%22result%22%2C%20value%3D42.0%20*%201.1)%20%20%23%20saved%0A%0A%20%20%20%20buf_skip%20%3D%20io.StringIO()%0A%20%20%20%20comp_skip.write_json(buf_skip)%0A%20%20%20%20buf_skip.seek(0)%0A%0A%20%20%20%20comp_skip2%20%3D%20Computation.read_json(buf_skip)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%22db_conn%22%3A%20comp_skip2.state(%22db_conn%22)%2C%20%20%23%20UNINITIALIZED%20%E2%80%94%20not%20restored%0A%20%20%20%20%20%20%20%20%22raw_data%22%3A%20comp_skip2.value(%22raw_data%22)%2C%0A%20%20%20%20%20%20%20%20%22result%22%3A%20comp_skip2.value(%22result%22)%2C%0A%20%20%20%20%7D%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%60db_conn%60%20comes%20back%20as%20%60UNINITIALIZED%60%20%E2%80%94%20exactly%20as%20if%20the%20node%20had%20never%20been%0A%20%20%20%20given%20a%20value%20%E2%80%94%20while%20%60raw_data%60%20and%20%60result%60%20round-trip%20perfectly.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%203.%20Lambdas%20and%20closures%0A%0A%20%20%20%20By%20default%2C%20%60write_json%60%20raises%20a%20%60SerializationError%60%20when%20a%20node's%20function%20is%20a%0A%20%20%20%20lambda%2C%20because%20lambdas%20have%20no%20importable%20module%20path%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20io)%3A%0A%20%20%20%20from%20loman%20import%20SerializationError%20as%20_SerializationError%0A%0A%20%20%20%20comp_lambda%20%3D%20Computation()%0A%20%20%20%20comp_lambda.add_node(%22x%22%2C%20value%3D5)%0A%20%20%20%20comp_lambda.add_node(%22y%22%2C%20lambda%20x%3A%20x**2)%0A%20%20%20%20comp_lambda.compute_all()%0A%0A%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20comp_lambda.write_json(io.StringIO())%0A%20%20%20%20%20%20%20%20error_msg%20%3D%20None%0A%20%20%20%20except%20_SerializationError%20as%20e%3A%0A%20%20%20%20%20%20%20%20error_msg%20%3D%20str(e)%0A%0A%20%20%20%20error_msg%0A%20%20%20%20return%20(comp_lambda%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20error%20message%20points%20to%20the%20fix%3A%20pass%20%60use_dill_for_functions%3DTrue%60%20to%0A%20%20%20%20%60ComputationSerializer%60.%20This%20encodes%20the%20callable%20as%20a%20base64%20%5Bdill%5D(https%3A%2F%2Fgithub.com%2Fuqfoundation%2Fdill)%0A%20%20%20%20blob%20inside%20the%20JSON%2C%20so%20lambdas%20and%20closures%20%E2%80%94%20including%20ones%20that%20capture%20local%0A%20%20%20%20variables%20%E2%80%94%20round-trip%20intact%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20ComputationSerializer%2C%20comp_lambda%2C%20io)%3A%0A%20%20%20%20s_dill%20%3D%20ComputationSerializer(use_dill_for_functions%3DTrue)%0A%0A%20%20%20%20buf_lambda%20%3D%20io.StringIO()%0A%20%20%20%20comp_lambda.write_json(buf_lambda%2C%20serializer%3Ds_dill)%0A%20%20%20%20buf_lambda.seek(0)%0A%20%20%20%20comp_lambda2%20%3D%20Computation.read_json(buf_lambda%2C%20serializer%3Ds_dill)%0A%0A%20%20%20%20%23%20Value%20is%20restored%20%E2%80%A6%0A%20%20%20%20print(%22Loaded%20value%20of%20y%3A%22%2C%20comp_lambda2.value(%22y%22))%0A%0A%20%20%20%20%23%20%E2%80%A6%20and%20the%20function%20is%20live%20%E2%80%94%20we%20can%20recompute%20after%20changing%20x.%0A%20%20%20%20comp_lambda2.insert(%22x%22%2C%2012)%0A%20%20%20%20comp_lambda2.compute_all()%0A%20%20%20%20print(%22Recomputed%20y%20after%20x%3D12%3A%22%2C%20comp_lambda2.value(%22y%22))%0A%20%20%20%20return%20(buf_lambda%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20The%20lambda%20is%20stored%20as%20a%20%60%22dill_func%22%60%20object%20in%20the%20JSON.%20The%20%60blob%60%20field%20is%20a%0A%20%20%20%20base64-encoded%20dill%20byte%20string%20%E2%80%94%20here%20is%20what%20the%20%60func%60%20field%20looks%20like%0A%20%20%20%20(blob%20truncated%20for%20readability)%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(buf_lambda%2C%20json)%3A%0A%20%20%20%20from%20pprint%20import%20pprint%0A%0A%20%20%20%20_raw%20%3D%20json.loads(buf_lambda.getvalue())%0A%0A%20%20%20%20pprint(_raw)%0A%20%20%20%20return%20(pprint%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Closures%20that%20capture%20variables%20from%20an%20enclosing%20scope%20also%20work%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20ComputationSerializer%2C%20io%2C%20json%2C%20pprint)%3A%0A%20%20%20%20scale%20%3D%202.5%0A%0A%20%20%20%20def%20scale_up(x)%3A%0A%20%20%20%20%20%20%20%20return%20x%20*%20scale%20%20%23%20captures%20%60scale%60%20from%20the%20enclosing%20scope%0A%0A%20%20%20%20comp_closure%20%3D%20Computation()%0A%20%20%20%20comp_closure.add_node(%22x%22%2C%20value%3D4)%0A%20%20%20%20comp_closure.add_node(%22y%22%2C%20scale_up)%0A%20%20%20%20comp_closure.compute_all()%0A%0A%20%20%20%20s2%20%3D%20ComputationSerializer(use_dill_for_functions%3DTrue)%0A%20%20%20%20buf_closure%20%3D%20io.StringIO()%0A%20%20%20%20comp_closure.write_json(buf_closure%2C%20serializer%3Ds2)%0A%20%20%20%20buf_closure.seek(0)%0A%20%20%20%20comp_closure2%20%3D%20Computation.read_json(buf_closure%2C%20serializer%3Ds2)%0A%0A%20%20%20%20comp_closure2.insert(%22x%22%2C%2010)%0A%20%20%20%20comp_closure2.compute_all()%0A%20%20%20%20print(%22y%20after%20reload%20with%20x%3D10%3A%22%2C%20comp_closure2.value(%22y%22))%20%20%23%2025.0%0A%20%20%20%20print(%22%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%22)%0A%20%20%20%20pprint(json.loads(buf_closure.getvalue()))%0A%20%20%20%20comp_closure2%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%3E%20**Note%3A**%20The%20dill%20blob%20is%20not%20portable%20across%20Python%20versions.%20Prefer%20named%0A%20%20%20%20%3E%20module-level%20functions%20when%20portability%20matters%3B%20use%20%60use_dill_for_functions%3DTrue%60%0A%20%20%20%20%3E%20when%20convenience%20is%20more%20important.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%204.%20Preserving%20PINNED%20state%0A%0A%20%20%20%20A%20%60PINNED%60%20node's%20value%20is%20locked%20%E2%80%94%20downstream%20recalculations%20use%20it%20but%20it%20is%0A%20%20%20%20never%20overwritten%20by%20%60compute_all%60.%20The%20%60PINNED%60%20state%20survives%20a%20round-trip%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20States%2C%20io)%3A%0A%20%20%20%20comp_pin%20%3D%20Computation()%0A%20%20%20%20comp_pin.add_node(%22rate%22%2C%20value%3D0.05)%0A%20%20%20%20comp_pin.add_node(%22principal%22%2C%20value%3D1000.0)%0A%0A%20%20%20%20def%20calc_interest(rate%2C%20principal)%3A%0A%20%20%20%20%20%20%20%20return%20rate%20*%20principal%0A%0A%20%20%20%20comp_pin.add_node(%22interest%22%2C%20calc_interest)%0A%20%20%20%20comp_pin.compute_all()%0A%0A%20%20%20%20%23%20Pin%20the%20rate%20so%20that%20even%20if%20we%20reload%20and%20change%20inputs%2C%20it%20stays%20fixed.%0A%20%20%20%20comp_pin.pin(%22rate%22)%0A%20%20%20%20print(%22State%20of%20rate%20before%20save%3A%22%2C%20comp_pin.state(%22rate%22))%0A%0A%20%20%20%20buf_pin%20%3D%20io.StringIO()%0A%20%20%20%20comp_pin.write_json(buf_pin)%0A%20%20%20%20buf_pin.seek(0)%0A%20%20%20%20comp_pin2%20%3D%20Computation.read_json(buf_pin)%0A%0A%20%20%20%20print(%22State%20of%20rate%20after%20reload%3A%22%2C%20comp_pin2.state(%22rate%22))%0A%20%20%20%20assert%20comp_pin2.state(%22rate%22)%20%3D%3D%20States.PINNED%0A%20%20%20%20print(%22Value%20of%20rate%20after%20reload%3A%22%2C%20comp_pin2.value(%22rate%22))%0A%20%20%20%20return%20(comp_pin%2C)%0A%0A%0A%40app.cell%0Adef%20_(comp_pin)%3A%0A%20%20%20%20comp_pin%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%205.%20Post-mortem%20inspection%20of%20ERROR%20nodes%0A%0A%20%20%20%20When%20a%20node%20raises%20an%20exception%20during%20%60compute_all%60%2C%20it%20enters%20%60ERROR%60%20state.%0A%20%20%20%20The%20exception's%20type%2C%20message%20and%20traceback%20are%20all%20preserved.%20A%20builtin%0A%20%20%20%20exception%20type%20is%20rebuilt%20as%20itself%2C%20so%20%60except%20ValueError%60%20still%20matches%20after%0A%20%20%20%20a%20reload%3B%20anything%20else%20becomes%20a%20%60DeserializedError%60%20carrying%20the%20original%20type%0A%20%20%20%20name%2C%20because%20rebuilding%20it%20for%20real%20would%20mean%20importing%20whatever%20module%20the%0A%20%20%20%20file%20names%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20States%2C%20io)%3A%0A%20%20%20%20def%20bad_calc(x)%3A%0A%20%20%20%20%20%20%20%20msg%20%3D%20f%22unexpected%20value%3A%20%7Bx!r%7D%22%0A%20%20%20%20%20%20%20%20raise%20ValueError(msg)%0A%0A%20%20%20%20comp_err%20%3D%20Computation()%0A%20%20%20%20comp_err.add_node(%22x%22%2C%20value%3D-1)%0A%20%20%20%20comp_err.add_node(%22result%22%2C%20bad_calc)%0A%20%20%20%20comp_err.compute_all()%0A%0A%20%20%20%20print(%22State%20of%20result%3A%22%2C%20comp_err.state(%22result%22))%0A%0A%20%20%20%20buf_err%20%3D%20io.StringIO()%0A%20%20%20%20comp_err.write_json(buf_err)%0A%20%20%20%20buf_err.seek(0)%0A%20%20%20%20comp_err2%20%3D%20Computation.read_json(buf_err)%0A%0A%20%20%20%20print(%22State%20after%20reload%3A%22%2C%20comp_err2.state(%22result%22))%0A%20%20%20%20assert%20comp_err2.state(%22result%22)%20%3D%3D%20States.ERROR%0A%0A%20%20%20%20err_val%20%3D%20comp_err2.value(%22result%22)%0A%20%20%20%20print(%22Exception%20message%3A%22%2C%20err_val.exception)%0A%20%20%20%20print(%22Traceback%20(first%20line)%3A%22%2C%20err_val.traceback.splitlines()%5B0%5D)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%206.%20Pandas%20DataFrames%20as%20node%20values%0A%0A%20%20%20%20DataFrames%20and%20Series%20are%20serialised%20automatically%20using%20a%20JSON%20split-orientation%0A%20%20%20%20format.%20No%20extra%20configuration%20needed%3A%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20io)%3A%0A%20%20%20%20import%20pandas%20as%20pd%0A%0A%20%20%20%20def%20enrich(raw)%3A%0A%20%20%20%20%20%20%20%20df%20%3D%20raw.copy()%0A%20%20%20%20%20%20%20%20df%5B%22value_eur%22%5D%20%3D%20df%5B%22qty%22%5D%20*%20df%5B%22price_usd%22%5D%20*%200.92%0A%20%20%20%20%20%20%20%20return%20df%0A%0A%20%20%20%20prices%20%3D%20pd.DataFrame(%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22ticker%22%3A%20%5B%22AAPL%22%2C%20%22MSFT%22%2C%20%22GOOG%22%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22qty%22%3A%20%5B10%2C%2020%2C%205%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22price_usd%22%3A%20%5B182.5%2C%20375.2%2C%20140.8%5D%2C%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20)%0A%0A%20%20%20%20comp_df%20%3D%20Computation()%0A%20%20%20%20comp_df.add_node(%22raw%22%2C%20value%3Dprices)%0A%20%20%20%20comp_df.add_node(%22enriched%22%2C%20enrich)%0A%20%20%20%20comp_df.compute_all()%0A%0A%20%20%20%20buf_df%20%3D%20io.StringIO()%0A%20%20%20%20comp_df.write_json(buf_df)%0A%20%20%20%20buf_df.seek(0)%0A%20%20%20%20comp_df2%20%3D%20Computation.read_json(buf_df)%0A%20%20%20%20comp_df2.value(%22enriched%22)%0A%20%20%20%20return%20buf_df%2C%20pd%0A%0A%0A%40app.cell%0Adef%20_(buf_df%2C%20json%2C%20pprint)%3A%0A%20%20%20%20pprint(json.loads(buf_df.getvalue()))%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%207.%20%60save%60%20and%20%60load%60%3A%20the%20%60.loman%60%20container%0A%0A%20%20%20%20Everything%20above%20uses%20%60write_json%60%2C%20which%20writes%20one%20JSON%20document.%20That%20is%0A%20%20%20%20readable%20and%20diffable%2C%20but%20every%20number%20is%20stored%20as%20text%3A%20a%20100k-row%20frame%0A%20%20%20%20becomes%20tens%20of%20megabytes%20of%20decimal%20digits.%0A%0A%20%20%20%20%60save%60%20writes%20a%20%60.loman%60%20file%20instead%20%E2%80%94%20a%20zip%20holding%20the%20same%20manifest%2C%20with%0A%20%20%20%20large%20values%20stored%20beside%20it%20as%20compressed%20binary.%20The%20manifest%20still%20records%0A%20%20%20%20each%20value's%20shape%2C%20dtype%20and%20index%2C%20so%20a%20saved%20run%20can%20be%20inspected%20without%0A%20%20%20%20decoding%20any%20data.%0A%0A%20%20%20%20Both%20remain%20available.%20%60write_json%60%20is%20the%20right%20choice%20when%20you%20want%20a%20text%0A%20%20%20%20file%20to%20diff%3B%20%60save%60%20is%20the%20right%20choice%20for%20everything%20else.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20json%2C%20pd)%3A%0A%20%20%20%20import%20tempfile%0A%20%20%20%20import%20zipfile%0A%20%20%20%20from%20pathlib%20import%20Path%0A%0A%20%20%20%20import%20numpy%20as%20np%0A%0A%20%20%20%20tmp%20%3D%20Path(tempfile.mkdtemp())%0A%0A%20%20%20%20def%20make_prices(n%3D100_000%2C%20rounded%3DTrue)%3A%0A%20%20%20%20%20%20%20%20%22%22%22A%20price%20series%3A%20a%20random%20walk%2C%20rounded%20as%20real%20prices%20are.%22%22%22%0A%20%20%20%20%20%20%20%20rng%20%3D%20np.random.default_rng(0)%0A%20%20%20%20%20%20%20%20walk%20%3D%20100%20%2B%20np.cumsum(rng.standard_normal(n)%20*%200.01)%0A%20%20%20%20%20%20%20%20return%20pd.DataFrame(%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%22px%22%3A%20np.round(walk%2C%202)%20if%20rounded%20else%20walk%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20index%3Dpd.date_range(%222020-01-01%22%2C%20periods%3Dn%2C%20freq%3D%22min%22)%2C%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20def%20manifest_of(path)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Read%20a%20container's%20manifest%20without%20decoding%20any%20of%20its%20data.%22%22%22%0A%20%20%20%20%20%20%20%20path%20%3D%20Path(path)%0A%20%20%20%20%20%20%20%20if%20path.is_dir()%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20json.loads((path%20%2F%20%22manifest.json%22).read_text())%0A%20%20%20%20%20%20%20%20if%20path.suffix%20%3D%3D%20%22.json%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20json.loads(path.read_text())%0A%20%20%20%20%20%20%20%20return%20json.loads(zipfile.ZipFile(path).read(%22manifest.json%22))%0A%0A%20%20%20%20def%20size_of(path)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Total%20bytes%20on%20disk%2C%20whether%20one%20file%20or%20a%20directory.%22%22%22%0A%20%20%20%20%20%20%20%20path%20%3D%20Path(path)%0A%20%20%20%20%20%20%20%20if%20path.is_dir()%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20sum(f.stat().st_size%20for%20f%20in%20path.rglob(%22*%22)%20if%20f.is_file())%0A%20%20%20%20%20%20%20%20return%20path.stat().st_size%0A%0A%20%20%20%20px_frame%20%3D%20make_prices()%0A%20%20%20%20comp_big%20%3D%20Computation()%0A%20%20%20%20comp_big.add_node(%22prices%22%2C%20value%3Dpx_frame)%0A%0A%20%20%20%20comp_big.save(str(tmp%20%2F%20%22run.loman%22))%0A%20%20%20%20comp_big.save(str(tmp%20%2F%20%22run.json%22))%0A%0A%20%20%20%20print(f%22save('run.loman')%3A%20%7Bsize_of(tmp%20%2F%20'run.loman')%3A%3E12%2C%7D%20bytes%22)%0A%20%20%20%20print(f%22save('run.json')%20%3A%20%7Bsize_of(tmp%20%2F%20'run.json')%3A%3E12%2C%7D%20bytes%22)%0A%20%20%20%20print(f%22exact%20round-trip%20%3A%20%7BComputation.load(str(tmp%20%2F%20'run.loman')).v.prices.equals(px_frame)%7D%22)%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20Path%2C%0A%20%20%20%20%20%20%20%20comp_big%2C%0A%20%20%20%20%20%20%20%20make_prices%2C%0A%20%20%20%20%20%20%20%20manifest_of%2C%0A%20%20%20%20%20%20%20%20np%2C%0A%20%20%20%20%20%20%20%20px_frame%2C%0A%20%20%20%20%20%20%20%20size_of%2C%0A%20%20%20%20%20%20%20%20tmp%2C%0A%20%20%20%20%20%20%20%20zipfile%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20The%20manifest%20still%20describes%20everything%0A%0A%20%20%20%20Moving%20the%20data%20out%20of%20line%20does%20not%20make%20the%20file%20opaque.%20Shape%2C%20dtype%2C%0A%20%20%20%20column%20names%20and%20index%20type%20stay%20inline%20%E2%80%94%20only%20the%20bulk%20numbers%20move%20%E2%80%94%20so%20you%0A%20%20%20%20can%20see%20what%20a%20saved%20run%20contains%20without%20reading%20a%20single%20blob.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(json%2C%20manifest_of%2C%20tmp)%3A%0A%20%20%20%20_manifest%20%3D%20manifest_of(tmp%20%2F%20%22run.loman%22)%0A%20%20%20%20_value%20%3D%20_manifest%5B%22nodes%22%5D%5B0%5D%5B%22value%22%5D%0A%0A%20%20%20%20print(f%22manifest%20size%20%3A%20%7Blen(json.dumps(_manifest))%3A%2C%7D%20bytes%20(for%20a%20100k-row%20frame)%22)%0A%20%20%20%20print(f%22index%20%20%20%20%20%20%20%20%20%3A%20%7Bjson.dumps(%7Bk%3A%20v%20for%20k%2C%20v%20in%20_value%5B'index'%5D.items()%20if%20k%20!%3D%20'values'%7D)%7D%22)%0A%20%20%20%20print(f%22columns%20%20%20%20%20%20%20%3A%20%7B_value%5B'columns'%5D%5B'values'%5D%7D%22)%0A%20%20%20%20print(%22blob%20table%20%20%20%20%3A%22)%0A%20%20%20%20for%20_entry%20in%20_manifest%5B%22blobs%22%5D%3A%0A%20%20%20%20%20%20%20%20print(f%22%20%20%7B_entry%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%208.%20Profiles%20and%20containers%0A%0A%20%20%20%20Two%20independent%20choices%2C%20and%20keeping%20them%20independent%20is%20deliberate.%0A%0A%20%20%20%20The%20**profile**%20decides%20how%20a%20value%20is%20encoded%20%E2%80%94%20%60%22readable%22%60%20keeps%20everything%0A%20%20%20%20inline%20as%20JSON%2C%20%60%22efficient%22%60%20(the%20default)%20writes%20large%20values%20out%20of%20line.%0A%0A%20%20%20%20The%20**container**%20decides%20where%20the%20bytes%20land%20%E2%80%94%20%60%22zip%22%60%20(a%20%60.loman%60%20file%2C%0A%20%20%20%20the%20default)%2C%20%60%22dir%22%60%20(the%20same%20layout%20unzipped)%2C%20or%20%60%22json%22%60%20(one%20document).%0A%0A%20%20%20%20Collapsing%20them%20would%20rule%20out%20combinations%20people%20actually%20want%2C%20such%20as%20a%0A%20%20%20%20readable%20manifest%20inside%20a%20zip.%20Only%20one%20pairing%20is%20impossible%3A%20%60efficient%60%0A%20%20%20%20with%20a%20single%20JSON%20document%2C%20which%20has%20nowhere%20to%20put%20the%20bytes.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20comp_big%2C%20px_frame%2C%20size_of%2C%20tmp)%3A%0A%20%20%20%20_rows%20%3D%20%5B%5D%0A%20%20%20%20for%20_label%2C%20_name%2C%20_kwargs%20in%20%5B%0A%20%20%20%20%20%20%20%20(%22efficient%20%2B%20zip%20(default)%22%2C%20%22m_ez.loman%22%2C%20%7B%7D)%2C%0A%20%20%20%20%20%20%20%20(%22readable%20%20%2B%20zip%22%2C%20%22m_rz.loman%22%2C%20%7B%22profile%22%3A%20%22readable%22%7D)%2C%0A%20%20%20%20%20%20%20%20(%22efficient%20%2B%20dir%22%2C%20%22m_ed%22%2C%20%7B%22container%22%3A%20%22dir%22%7D)%2C%0A%20%20%20%20%20%20%20%20(%22readable%20%20%2B%20json%22%2C%20%22m_rj.json%22%2C%20%7B%7D)%2C%0A%20%20%20%20%5D%3A%0A%20%20%20%20%20%20%20%20_path%20%3D%20tmp%20%2F%20_name%0A%20%20%20%20%20%20%20%20comp_big.save(str(_path)%2C%20**_kwargs)%0A%20%20%20%20%20%20%20%20_ok%20%3D%20Computation.load(str(_path)).v.prices.equals(px_frame)%0A%20%20%20%20%20%20%20%20_rows.append((_label%2C%20size_of(_path)%2C%20_ok))%0A%0A%20%20%20%20print(f%22%7B''%3A28s%7D%20%7B'bytes'%3A%3E12s%7D%20%20exact%22)%0A%20%20%20%20for%20_label%2C%20_bytes%2C%20_ok%20in%20_rows%3A%0A%20%20%20%20%20%20%20%20print(f%22%7B_label%3A28s%7D%20%7B_bytes%3A%3E12%2C%7D%20%20%7B_ok%7D%22)%0A%0A%20%20%20%20print(%22%5CnThe%20one%20impossible%20combination%3A%22)%0A%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20comp_big.save(str(tmp%20%2F%20%22nope.json%22)%2C%20profile%3D%22efficient%22)%0A%20%20%20%20except%20ValueError%20as%20_exc%3A%0A%20%20%20%20%20%20%20%20print(f%22%20%20%7Bstr(_exc)%5B%3A150%5D%7D...%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20Which%20container%3F%0A%0A%20%20%20%20%60.loman%60%20is%20one%20file%2C%20which%20is%20what%20you%20want%20for%20handing%20a%20run%20to%20a%20colleague%0A%20%20%20%20or%20uploading%20it.%0A%0A%20%20%20%20Use%20%60container%3D%22dir%22%60%20when%20you%20save%20**repeatedly**%20%E2%80%94%20a%20checkpointing%20loop.%0A%20%20%20%20Updating%20one%20value%20in%20a%20zip%20rewrites%20the%20whole%20archive%20at%20a%20cost%20that%20grows%0A%20%20%20%20with%20its%20size%2C%20while%20a%20directory%20rewrites%20only%20the%20file%20that%20changed.%20The%0A%20%20%20%20layout%20is%20identical%20either%20way%3B%20a%20%60.loman%60%20is%20simply%20that%20directory%20zipped.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Path%2C%20tmp)%3A%0A%20%20%20%20_dir%20%3D%20Path(tmp%20%2F%20%22m_ed%22)%0A%20%20%20%20print(%22directory%20container%20layout%3A%22)%0A%20%20%20%20for%20_f%20in%20sorted(_dir.rglob(%22*%22))%3A%0A%20%20%20%20%20%20%20%20print(f%22%20%20%7B_f.relative_to(_dir)%7D%20%20(%7B_f.stat().st_size%3A%2C%7D%20bytes)%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%209.%20Compression%0A%0A%20%20%20%20Blobs%20are%20compressed%20with%20**zstd%20at%20level%201**%20by%20default.%20The%20codec%20is%20your%0A%20%20%20%20choice%3B%20nothing%20is%20inferred%20from%20the%20data.%0A%0A%20%20%20%20That%20it%20is%20on%20by%20default%20rests%20on%20one%20measurement%3A%20zstd%20rejects%20data%20it%0A%20%20%20%20cannot%20compress%20at%20about%201%20GB%2Fs%2C%20roughly%2025%20times%20faster%20than%20zlib%2C%20while%0A%20%20%20%20also%20compressing%20real%20data%20better%20and%20faster.%20Compressing%20to%20find%20out%20is%0A%20%20%20%20therefore%20a%20non-event%2C%20which%20is%20why%20zstandard%20is%20a%20required%20dependency.%0A%0A%20%20%20%20After%20compressing%2C%20the%20two%20sizes%20are%20compared%20and%20**whichever%20is%20smaller%20is%0A%20%20%20%20stored**.%20A%20blob%20that%20did%20not%20shrink%20is%20written%20exactly%20as%20it%20came%20and%0A%20%20%20%20recorded%20as%20%60%22none%22%60%2C%20so%20nothing%20is%20ever%20stored%20larger%20than%20it%20started%20and%20no%0A%20%20%20%20future%20read%20pays%20a%20decompression%20step%20for%20nothing.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20make_prices%2C%20manifest_of%2C%20np%2C%20size_of%2C%20tmp)%3A%0A%20%20%20%20import%20os%20as%20_os%0A%0A%20%20%20%20_cases%20%3D%20%5B%0A%20%20%20%20%20%20%20%20(%22rounded%20prices%22%2C%20make_prices(200_000%2C%20rounded%3DTrue)%5B%22px%22%5D.to_numpy())%2C%0A%20%20%20%20%20%20%20%20(%22random%20float64%22%2C%20np.random.default_rng(0).standard_normal(200_000))%2C%0A%20%20%20%20%20%20%20%20(%22random%20bytes%22%2C%20np.frombuffer(_os.urandom(1_600_000)%2C%20dtype%3Dnp.uint8))%2C%0A%20%20%20%20%5D%0A%20%20%20%20for%20_label%2C%20_values%20in%20_cases%3A%0A%20%20%20%20%20%20%20%20_comp%20%3D%20Computation()%0A%20%20%20%20%20%20%20%20_comp.add_node(%22v%22%2C%20value%3D_values)%0A%20%20%20%20%20%20%20%20_path%20%3D%20tmp%20%2F%20f%22cmp_%7B_label.split()%5B1%5D%7D.loman%22%0A%20%20%20%20%20%20%20%20_comp.save(str(_path))%0A%0A%20%20%20%20%20%20%20%20_entry%20%3D%20manifest_of(_path)%5B%22blobs%22%5D%5B0%5D%0A%20%20%20%20%20%20%20%20_ratio%20%3D%20_entry%5B%22size%22%5D%20%2F%20_entry.get(%22stored_size%22%2C%20_entry%5B%22size%22%5D)%0A%20%20%20%20%20%20%20%20print(f%22%7B_label%3A16s%7D%20stored%20as%20%7B_entry%5B'compression'%5D%3A8s%7D%20%7Bsize_of(_path)%3A%3E10%2C%7D%20bytes%20%20(%7B_ratio%3A.1f%7Dx)%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Note%20the%20third%20row%3A%20genuinely%20random%20bytes%20cannot%20be%20compressed%2C%20so%20the%0A%20%20%20%20attempt%20is%20discarded%20and%20the%20blob%20is%20stored%20raw.%20Random%20%60float64%60%20is%20*not*%20a%0A%20%20%20%20good%20example%20of%20incompressible%20data%20%E2%80%94%20zstd%20still%20finds%20about%204%25%20in%20the%0A%20%20%20%20exponent%20bytes.%0A%0A%20%20%20%20To%20choose%20something%20else%2C%20pass%20a%20profile.%20%60compression%60%20accepts%20%60%22none%22%60%2C%20or%0A%20%20%20%20a%20codec%20and%20optional%20level%3A%20%60%22zstd%3A1%22%60%E2%80%93%60%22zstd%3A22%22%60%2C%20%60%22zlib%3A1%22%60%E2%80%93%60%22zlib%3A9%22%60.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20make_prices%2C%20size_of%2C%20tmp)%3A%0A%20%20%20%20from%20loman%20import%20SerializationProfile%0A%0A%20%20%20%20_values%20%3D%20make_prices(200_000)%5B%22px%22%5D.to_numpy()%0A%20%20%20%20_comp%20%3D%20Computation()%0A%20%20%20%20_comp.add_node(%22v%22%2C%20value%3D_values)%0A%0A%20%20%20%20for%20_spec%20in%20%5B%22none%22%2C%20%22zstd%3A1%22%2C%20%22zstd%3A19%22%2C%20%22zlib%3A9%22%5D%3A%0A%20%20%20%20%20%20%20%20_profile%20%3D%20SerializationProfile(%22demo%22%2C%20inline_max_bytes%3D8192%2C%20compression%3D_spec)%0A%20%20%20%20%20%20%20%20_path%20%3D%20tmp%20%2F%20f%22lvl_%7B_spec.replace('%3A'%2C%20'')%7D.loman%22%0A%20%20%20%20%20%20%20%20_comp.save(str(_path)%2C%20profile%3D_profile)%0A%20%20%20%20%20%20%20%20print(f%22compression%3D%7B_spec%3A8s%7D%20%7Bsize_of(_path)%3A%3E10%2C%7D%20bytes%22)%0A%20%20%20%20return%20(SerializationProfile%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%2010.%20Bring%20your%20own%20storage%0A%0A%20%20%20%20Sometimes%20a%20value%20does%20not%20belong%20in%20the%20saved%20file%20at%20all%20%E2%80%94%20a%20frame%20that%0A%20%20%20%20should%20land%20in%20a%20bucket%2C%20or%20a%20result%20that%20belongs%20in%20a%20warehouse%20table.%0A%0A%20%20%20%20Mark%20the%20node%20with%20a%20store%20name%2C%20and%20supply%20the%20implementation%20when%20you%20save%0A%20%20%20%20and%20load.%20A%20store%20is%20**two%20methods**%3A%20compression%2C%20deduplication%2C%20checksums%2C%0A%20%20%20%20blob%20ids%20and%20the%20blob%20table%20are%20all%20handled%20for%20you.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20from%20loman.serialization%20import%20BlobStore%0A%0A%20%20%20%20class%20BucketStore(BlobStore)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Stands%20in%20for%20S3%20or%20a%20database.%20A%20real%20one%20would%20talk%20to%20a%20client.%22%22%22%0A%0A%20%20%20%20%20%20%20%20def%20__init__(self%2C%20bucket)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.bucket%20%3D%20bucket%0A%20%20%20%20%20%20%20%20%20%20%20%20self.objects%20%3D%20%7B%7D%0A%0A%20%20%20%20%20%20%20%20def%20write_blob(self%2C%20key%2C%20data)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20self.objects%5Bf%22s3%3A%2F%2F%7Bself.bucket%7D%2F%7Bkey%7D%22%5D%20%3D%20data%0A%0A%20%20%20%20%20%20%20%20def%20read_blob(self%2C%20key)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20self.objects%5Bf%22s3%3A%2F%2F%7Bself.bucket%7D%2F%7Bkey%7D%22%5D%0A%20%20%20%20return%20(BucketStore%2C)%0A%0A%0A%40app.cell%0Adef%20_(BucketStore%2C%20Computation%2C%20manifest_of%2C%20px_frame%2C%20size_of%2C%20tmp%2C%20zipfile)%3A%0A%20%20%20%20comp_remote%20%3D%20Computation()%0A%20%20%20%20comp_remote.add_node(%22prices%22%2C%20value%3Dpx_frame%2C%20store%3D%22warehouse%22)%0A%20%20%20%20comp_remote.add_node(%22summary%22%2C%20value%3D%7B%22rows%22%3A%20len(px_frame)%7D)%0A%0A%20%20%20%20bucket%20%3D%20BucketStore(%22quant-data%22)%0A%20%20%20%20comp_remote.save(str(tmp%20%2F%20%22remote.loman%22)%2C%20stores%3D%7B%22warehouse%22%3A%20bucket%7D)%0A%0A%20%20%20%20print(f%22file%20on%20disk%20%20%20%20%3A%20%7Bsize_of(tmp%20%2F%20'remote.loman')%3A%2C%7D%20bytes%22)%0A%20%20%20%20print(f%22archive%20members%20%3A%20%7Bzipfile.ZipFile(tmp%20%2F%20'remote.loman').namelist()%7D%22)%0A%20%20%20%20print(f%22in%20the%20bucket%20%20%20%3A%20%7Blist(bucket.objects)%7D%22)%0A%20%20%20%20print(f%22%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7Bsum(len(v)%20for%20v%20in%20bucket.objects.values())%3A%2C%7D%20bytes%22)%0A%0A%20%20%20%20_back%20%3D%20Computation.load(str(tmp%20%2F%20%22remote.loman%22)%2C%20stores%3D%7B%22warehouse%22%3A%20bucket%7D)%0A%20%20%20%20print(f%22reload%20exact%20%20%20%20%3A%20%7B_back.v.prices.equals(px_frame)%7D%22)%0A%20%20%20%20print(f%22blob%20entry%20%20%20%20%20%20%3A%20%7Bmanifest_of(tmp%20%2F%20'remote.loman')%5B'blobs'%5D%5B0%5D%7D%22)%0A%20%20%20%20return%20(comp_remote%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20The%20file%20never%20holds%20your%20credentials%0A%0A%20%20%20%20A%20manifest%20records%20a%20store's%20**name**%2C%20never%20its%20configuration.%20No%20bucket%2C%20no%0A%20%20%20%20endpoint%2C%20no%20key%20reaches%20the%20file.%0A%0A%20%20%20%20The%20consequence%20is%20deliberate%3A%20a%20file%20with%20external%20values%20cannot%20resolve%20them%0A%20%20%20%20on%20its%20own%2C%20so%20whoever%20loads%20it%20supplies%20the%20matching%20store.%20If%20they%20don't%2C%0A%20%20%20%20the%20error%20says%20which%20store%20is%20missing%20and%20for%20which%20node.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20comp_remote%2C%20json%2C%20manifest_of%2C%20tmp)%3A%0A%20%20%20%20print(%22bucket%20name%20anywhere%20in%20the%20file%3A%22%2C%20%22quant-data%22%20in%20json.dumps(manifest_of(tmp%20%2F%20%22remote.loman%22)))%0A%0A%20%20%20%20print(%22%5CnForgetting%20the%20store%2C%20at%20each%20end%3A%22)%0A%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20comp_remote.save(str(tmp%20%2F%20%22oops.loman%22))%0A%20%20%20%20except%20Exception%20as%20_exc%3A%0A%20%20%20%20%20%20%20%20print(f%22%20%20save%20-%3E%20%7Btype(_exc).__name__%7D%3A%20%7Bstr(_exc)%5B%3A110%5D%7D%22)%0A%0A%20%20%20%20try%3A%0A%20%20%20%20%20%20%20%20Computation.load(str(tmp%20%2F%20%22remote.loman%22))%0A%20%20%20%20except%20Exception%20as%20_exc%3A%0A%20%20%20%20%20%20%20%20print(f%22%20%20load%20-%3E%20%7Btype(_exc).__name__%7D%3A%20%7Bstr(_exc)%5B%3A110%5D%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Note%20that%20a%20missing%20store%20is%20an%20**error**%2C%20not%20a%20quiet%20fallback%20to%20writing%20the%0A%20%20%20%20data%20into%20the%20file.%20Believing%20your%20data%20went%20to%20a%20bucket%20when%20it%20is%20actually%0A%20%20%20%20sitting%20in%20the%20archive%20is%20a%20worse%20outcome%20than%20a%20failed%20save.%0A%0A%20%20%20%20%23%23%23%20Routing%20is%20a%20default%2C%20not%20a%20fixture%0A%0A%20%20%20%20The%20store%20named%20on%20a%20node%20is%20what%20that%20node%20*is*%3B%20a%20profile%20override%20says%0A%20%20%20%20where%20that%20kind%20of%20thing%20goes%20*today*.%20So%20the%20same%20computation%20can%20go%20to%20a%0A%20%20%20%20bucket%20in%20production%20and%20to%20a%20plain%20container%20in%20a%20test%2C%20with%20no%20edit%20to%20the%0A%20%20%20%20graph.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20SerializationProfile%2C%20comp_remote%2C%20px_frame%2C%20size_of%2C%20tmp)%3A%0A%20%20%20%20_local%20%3D%20SerializationProfile(%0A%20%20%20%20%20%20%20%20%22local%22%2C%0A%20%20%20%20%20%20%20%20inline_max_bytes%3D8192%2C%0A%20%20%20%20%20%20%20%20compression%3D%22zstd%3A1%22%2C%0A%20%20%20%20%20%20%20%20overrides%3D%7B%22prices%22%3A%20%7B%22store%22%3A%20None%7D%7D%2C%0A%20%20%20%20)%0A%20%20%20%20comp_remote.save(str(tmp%20%2F%20%22local.loman%22)%2C%20profile%3D_local)%0A%0A%20%20%20%20print(f%22same%20computation%2C%20no%20bucket%3A%20%7Bsize_of(tmp%20%2F%20'local.loman')%3A%2C%7D%20bytes%22)%0A%20%20%20%20print(f%22loads%20with%20no%20stores%20%20%20%20%20%20%20%3A%20%7BComputation.load(str(tmp%20%2F%20'local.loman')).v.prices.equals(px_frame)%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20Selectors%20match%20node-key%20globs%20(%60%22market%2F*%22%60)%20or%20tags%20(%60%22tag%3Abulky%22%60)%2C%20so%20you%0A%20%20%20%20can%20tag%20nodes%20by%20*what%20they%20are*%20and%20let%20each%20save%20decide%20where%20that%20kind%20of%0A%20%20%20%20thing%20goes.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20BucketStore%2C%0A%20%20%20%20Computation%2C%0A%20%20%20%20SerializationProfile%2C%0A%20%20%20%20manifest_of%2C%0A%20%20%20%20px_frame%2C%0A%20%20%20%20tmp%2C%0A)%3A%0A%20%20%20%20_comp%20%3D%20Computation()%0A%20%20%20%20_comp.add_node(%22prices%22%2C%20value%3Dpx_frame%2C%20tags%3D%5B%22bulky%22%5D)%0A%20%20%20%20_comp.add_node(%22note%22%2C%20value%3D%22small%20enough%20to%20stay%20inline%22)%0A%0A%20%20%20%20_tagged%20%3D%20SerializationProfile(%0A%20%20%20%20%20%20%20%20%22tagged%22%2C%0A%20%20%20%20%20%20%20%20inline_max_bytes%3D8192%2C%0A%20%20%20%20%20%20%20%20compression%3D%22zstd%3A1%22%2C%0A%20%20%20%20%20%20%20%20overrides%3D%7B%22tag%3Abulky%22%3A%20%7B%22store%22%3A%20%22warehouse%22%7D%7D%2C%0A%20%20%20%20)%0A%20%20%20%20_bucket%20%3D%20BucketStore(%22tagged-data%22)%0A%20%20%20%20_comp.save(str(tmp%20%2F%20%22tagged.loman%22)%2C%20profile%3D_tagged%2C%20stores%3D%7B%22warehouse%22%3A%20_bucket%7D)%0A%0A%20%20%20%20print(%22routed%20by%20tag%2C%20not%20by%20name%3A%22)%0A%20%20%20%20for%20_entry%20in%20manifest_of(tmp%20%2F%20%22tagged.loman%22)%5B%22blobs%22%5D%3A%0A%20%20%20%20%20%20%20%20print(f%22%20%20node%20%7B_entry%5B'node'%5D!r%7D%20-%3E%20store%20%7B_entry.get('store'%2C%20'container')!r%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%23%20A%20readable%20manifest%20with%20the%20data%20elsewhere%0A%0A%20%20%20%20Because%20an%20external%20store%20is%20independent%20of%20the%20container%2C%20the%20two%20combine%3A%0A%20%20%20%20a%20manifest%20you%20can%20open%20in%20a%20text%20editor%2C%20describing%20values%20held%20in%20a%20bucket.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(BucketStore%2C%20Computation%2C%20SerializationProfile%2C%20px_frame%2C%20size_of%2C%20tmp)%3A%0A%20%20%20%20_bucket%20%3D%20BucketStore(%22hybrid%22)%0A%20%20%20%20_profile%20%3D%20SerializationProfile(%22hybrid%22%2C%20inline_max_bytes%3D8192%2C%20compression%3D%22zstd%3A1%22)%0A%20%20%20%20_comp%20%3D%20Computation()%0A%20%20%20%20_comp.add_node(%22prices%22%2C%20value%3Dpx_frame%2C%20store%3D%22warehouse%22)%0A%20%20%20%20_comp.save(str(tmp%20%2F%20%22hybrid.json%22)%2C%20profile%3D_profile%2C%20stores%3D%7B%22warehouse%22%3A%20_bucket%7D)%0A%0A%20%20%20%20print(f%22manifest%20is%20plain%20JSON%20%3A%20%7Bsize_of(tmp%20%2F%20'hybrid.json')%3A%2C%7D%20bytes%22)%0A%20%20%20%20print(f%22data%20in%20the%20bucket%20%20%20%20%20%3A%20%7Bsum(len(v)%20for%20v%20in%20_bucket.objects.values())%3A%2C%7D%20bytes%22)%0A%20%20%20%20_back%20%3D%20Computation.load(str(tmp%20%2F%20%22hybrid.json%22)%2C%20stores%3D%7B%22warehouse%22%3A%20_bucket%7D)%0A%20%20%20%20print(f%22reload%20exact%20%20%20%20%20%20%20%20%20%20%20%3A%20%7B_back.v.prices.equals(px_frame)%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%2011.%20Loading%20files%20you%20do%20not%20trust%0A%0A%20%20%20%20Loading%20restores%20node%20functions%2C%20which%20means%20importing%20the%20modules%20the%20file%0A%20%20%20%20names%20%E2%80%94%20or%20unpickling%20a%20dill%20blob%20out%20of%20it.%20**Both%20run%20code%20the%20file%20chose.**%0A%20%20%20%20This%20format%20is%20not%20safe%20against%20a%20hostile%20file%20and%20never%20was.%0A%0A%20%20%20%20%60allow_code%3DFalse%60%20skips%20resolving%20callables%20entirely.%20Values%2C%20structure%2C%0A%20%20%20%20states%20and%20tags%20still%20load%2C%20so%20a%20graph%20can%20be%20inspected%20but%20not%20recalculated.%0A%20%20%20%20Treat%20it%20as%20a%20mitigation%2C%20not%20a%20security%20boundary.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.function%0Adef%20increment_for_demo(a)%3A%0A%20%20%20%20%22%22%22Module-level%20so%20it%20is%20importable%2C%20and%20so%20the%20demo%20is%20not%20vacuous.%22%22%22%0A%20%20%20%20return%20a%20%2B%201%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20tmp)%3A%0A%20%20%20%20from%20loman.consts%20import%20NodeAttributes%0A%20%20%20%20from%20loman.nodekey%20import%20parse_nodekey%0A%0A%20%20%20%20_comp%20%3D%20Computation()%0A%20%20%20%20_comp.add_node(%22a%22%2C%20value%3D1)%0A%20%20%20%20_comp.add_node(%22b%22%2C%20increment_for_demo)%0A%20%20%20%20_comp.compute_all()%0A%20%20%20%20_comp.save(str(tmp%20%2F%20%22untrusted.loman%22))%0A%0A%20%20%20%20safe%20%3D%20Computation.load(str(tmp%20%2F%20%22untrusted.loman%22)%2C%20allow_code%3DFalse)%0A%20%20%20%20print(f%22values%20still%20load%20%20%20%20%20%3A%20a%3D%7Bsafe.v.a%7D%2C%20b%3D%7Bsafe.v.b%7D%22)%0A%20%20%20%20print(f%22structure%20still%20loads%20%3A%20%7Blen(safe.dag.edges())%7D%20edge(s)%22)%0A%20%20%20%20normal%20%3D%20Computation.load(str(tmp%20%2F%20%22untrusted.loman%22))%0A%20%20%20%20print(f%22function%2C%20normal%20load%20%3A%20%7Bnormal.dag.nodes%5Bparse_nodekey('b')%5D%5BNodeAttributes.FUNC%5D%7D%22)%0A%20%20%20%20print(f%22function%2C%20allow_code%3DFalse%3A%20%7Bsafe.dag.nodes%5Bparse_nodekey('b')%5D%5BNodeAttributes.FUNC%5D%7D%22)%0A%20%20%20%20return%20normal%2C%20safe%0A%0A%0A%40app.cell%0Adef%20_(safe)%3A%0A%20%20%20%20safe.src%5B%22b%22%5D%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(normal)%3A%0A%20%20%20%20normal.src%5B%22b%22%5D%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%2012.%20Durability%2C%20and%20pandas%202%20versus%203%0A%0A%20%20%20%20**Values%20are%20durable.**%20The%20format%20carries%20a%20version%2C%20changes%20to%20it%20are%0A%20%20%20%20additive%2C%20and%20files%20written%20by%20every%20earlier%20version%20are%20held%20as%20fixtures%20in%0A%20%20%20%20the%20test%20suite%20and%20asserted%20to%20still%20load%20*and%20still%20recompute*.%0A%0A%20%20%20%20**Functions%20are%20only%20as%20durable%20as%20your%20code.**%20A%20node's%20function%20is%20stored%20as%0A%20%20%20%20a%20module%20path%20and%20a%20qualified%20name%2C%20so%20it%20resolves%20only%20while%20that%20module%0A%20%20%20%20still%20exports%20that%20name.%20A%20dill-serialized%20one%20is%20worse%3A%20not%20portable%20across%0A%20%20%20%20Python%20versions.%0A%0A%20%20%20%20So%20a%20saved%20computation%20is%20a%20durable%20record%20of%20*what*%20was%20computed%2C%20and%20a%0A%20%20%20%20best-effort%20record%20of%20*how*.%0A%0A%20%20%20%20On%20pandas%3A%20version%202%20defaults%20datetimes%20to%20nanoseconds%20and%20version%203%20to%0A%20%20%20%20microseconds.%20Each%20value's%20resolution%20is%20therefore%20**recorded**%20rather%20than%0A%20%20%20%20assumed%2C%20so%20a%20file%20written%20under%20one%20loads%20correctly%20under%20the%20other.%20That%20is%0A%20%20%20%20why%20loman%20requires%20pandas%202.0%20or%20later%20%E2%80%94%20the%20APIs%20that%20read%20and%20restore%0A%20%20%20%20resolution%20arrived%20there%2C%20and%20below%20that%20the%20values%20would%20be%20silently%20wrong%0A%20%20%20%20rather%20than%20raising.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(Computation%2C%20pd%2C%20tmp)%3A%0A%20%20%20%20print(f%22running%20pandas%20%7Bpd.__version__%7D%5Cn%22)%0A%20%20%20%20for%20_unit%20in%20%5B%22s%22%2C%20%22ms%22%2C%20%22us%22%2C%20%22ns%22%5D%3A%0A%20%20%20%20%20%20%20%20_index%20%3D%20pd.date_range(%222020-01-01%22%2C%20periods%3D3%2C%20freq%3D%22min%22).as_unit(_unit)%0A%20%20%20%20%20%20%20%20_frame%20%3D%20pd.DataFrame(%7B%22a%22%3A%20%5B1.0%2C%202.0%2C%203.0%5D%7D%2C%20index%3D_index)%0A%0A%20%20%20%20%20%20%20%20_comp%20%3D%20Computation()%0A%20%20%20%20%20%20%20%20_comp.add_node(%22frame%22%2C%20value%3D_frame)%0A%20%20%20%20%20%20%20%20_comp.save(str(tmp%20%2F%20f%22unit_%7B_unit%7D.loman%22))%0A%0A%20%20%20%20%20%20%20%20_back%20%3D%20Computation.load(str(tmp%20%2F%20f%22unit_%7B_unit%7D.loman%22)).v.frame%0A%20%20%20%20%20%20%20%20print(f%22%20%20resolution%20%7B_unit%3A%3E2s%7D%20-%3E%20%7B_back.index.unit%3A%3E2s%7D%20%20exact%3A%20%7B_back.equals(_frame)%7D%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Summary%0A%0A%20%20%20%20%7C%20Scenario%20%7C%20How%20%7C%0A%20%20%20%20%7C---%7C---%7C%0A%20%20%20%20%7C%20Basic%20round-trip%20%7C%20%60comp.save(path)%60%20%2F%20%60Computation.load(path)%60%20%7C%0A%20%20%20%20%7C%20A%20readable%20text%20file%20%7C%20%60comp.write_json(path)%60%20%2F%20%60Computation.read_json(path)%60%20%7C%0A%20%20%20%20%7C%20Exclude%20a%20node%20%7C%20%60add_node(...%2C%20serialize%3DFalse)%60%20%7C%0A%20%20%20%20%7C%20Lambda%20%2F%20closure%20%7C%20%60ComputationSerializer(use_dill_for_functions%3DTrue)%60%20%7C%0A%20%20%20%20%7C%20PINNED%20state%20%7C%20Preserved%20automatically%20%7C%0A%20%20%20%20%7C%20ERROR%20state%20%7C%20Builtin%20types%20rebuilt%3B%20others%20become%20%60DeserializedError%60%20%7C%0A%20%20%20%20%7C%20Pandas%20%2F%20NumPy%20%7C%20Handled%20automatically%2C%20at%20any%20resolution%20%7C%0A%20%20%20%20%7C%20Large%20values%20%7C%20Out%20of%20line%20and%20compressed%2C%20decided%20per%20value%20%7C%0A%20%20%20%20%7C%20Repeated%20checkpoints%20%7C%20%60container%3D%22dir%22%60%20%7C%0A%20%20%20%20%7C%20Values%20in%20a%20bucket%20or%20database%20%7C%20%60add_node(store%3D...)%60%20%2B%20%60save(stores%3D%7B...%7D)%60%20%7C%0A%20%20%20%20%7C%20Untrusted%20file%20%7C%20%60load(path%2C%20allow_code%3DFalse)%60%20%7C%0A%20%20%20%20%7C%20Custom%20type%20%7C%20Register%20a%20%60CustomTransformer%60%20%7C%0A%20%20%20%20%7C%20Custom%20destination%20%7C%20Subclass%20%60BlobStore%60%20%E2%80%94%20two%20methods%20%7C%0A%0A%20%20%20%20Nothing%20here%20removed%20or%20changed%20an%20existing%20API%3A%20%60write_json%60%2C%20%60read_json%60%2C%0A%20%20%20%20%60write_dill%60%20and%20%60serialize%3DFalse%60%20all%20behave%20as%20they%20did%2C%20and%20every%20new%0A%20%20%20%20parameter%20is%20keyword-only%20with%20a%20default.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%20%20%20%20return%20(mo%2C)%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
3b7ff019275ba8b46ab92bf6eed9c1e7dbffd9abeba4e46d922fc460b7b17cb4