From c3d8bc4ca0dd4064cbd50c88e3d75df16350b256 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:10 +0100 Subject: [PATCH 1/2] Add PyScript examples for msgpack Generated by apply_llm_response.py from prompts/msgpack/response.toml. Examples included: - pack_and_unpack: Pack and unpack with MessagePack - streaming_unpacker: Stream many messages with Unpacker - custom_types_with_ext: Custom types with default and ext_hook Generated-By: apply_llm_response.py --- examples/msgpack/README.md | 18 +++++ .../msgpack/custom_types_with_ext/code.py | 70 +++++++++++++++++++ .../msgpack/custom_types_with_ext/config.toml | 1 + .../msgpack/custom_types_with_ext/setup.py | 24 +++++++ examples/msgpack/order.json | 5 ++ examples/msgpack/pack_and_unpack/code.py | 55 +++++++++++++++ examples/msgpack/pack_and_unpack/config.toml | 1 + examples/msgpack/pack_and_unpack/setup.py | 45 ++++++++++++ examples/msgpack/streaming_unpacker/code.py | 63 +++++++++++++++++ .../msgpack/streaming_unpacker/config.toml | 1 + examples/msgpack/streaming_unpacker/setup.py | 23 ++++++ 11 files changed, 306 insertions(+) create mode 100644 examples/msgpack/README.md create mode 100644 examples/msgpack/custom_types_with_ext/code.py create mode 100644 examples/msgpack/custom_types_with_ext/config.toml create mode 100644 examples/msgpack/custom_types_with_ext/setup.py create mode 100644 examples/msgpack/order.json create mode 100644 examples/msgpack/pack_and_unpack/code.py create mode 100644 examples/msgpack/pack_and_unpack/config.toml create mode 100644 examples/msgpack/pack_and_unpack/setup.py create mode 100644 examples/msgpack/streaming_unpacker/code.py create mode 100644 examples/msgpack/streaming_unpacker/config.toml create mode 100644 examples/msgpack/streaming_unpacker/setup.py diff --git a/examples/msgpack/README.md b/examples/msgpack/README.md new file mode 100644 index 0000000..1ba70e9 --- /dev/null +++ b/examples/msgpack/README.md @@ -0,0 +1,18 @@ +# msgpack Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/msgpack/custom_types_with_ext/code.py b/examples/msgpack/custom_types_with_ext/code.py new file mode 100644 index 0000000..152de76 --- /dev/null +++ b/examples/msgpack/custom_types_with_ext/code.py @@ -0,0 +1,70 @@ +# --------------------------------------------------------------------- +# Teaching msgpack about types it doesn't know natively. +# --------------------------------------------------------------------- + +heading("Custom types: Decimal and Fraction via ExtType") +note( + "MessagePack natively handles ints, floats, strings, bytes, lists, " + "and maps. For other types we register two callbacks: default " + "translates unknown objects to ExtType on the way out, and " + "ext_hook rebuilds them on the way back in. Each ExtType " + "carries a small integer code so we can tell types apart." +) + +# Pick stable codes for our two custom types. Codes 0..127 are free +# for application use. +CODE_DECIMAL = 1 +CODE_FRACTION = 2 + + +def encode_custom(obj): + """Serialize unknown objects into ExtType bytes.""" + if isinstance(obj, Decimal): + return msgpack.ExtType(CODE_DECIMAL, str(obj).encode("utf-8")) + if isinstance(obj, Fraction): + payload = f"{obj.numerator}/{obj.denominator}".encode("utf-8") + return msgpack.ExtType(CODE_FRACTION, payload) + raise TypeError(f"Cannot serialize {type(obj).__name__}") + + +def decode_custom(code, data): + """Rebuild objects from their ExtType representation.""" + if code == CODE_DECIMAL: + return Decimal(data.decode("utf-8")) + if code == CODE_FRACTION: + numerator, denominator = data.decode("utf-8").split("/") + return Fraction(int(numerator), int(denominator)) + # Unknown codes: hand the raw ExtType back so callers can inspect it. + return msgpack.ExtType(code, data) + + +# An invoice mixing native types with our custom numeric types. +invoice = { + "invoice_no": "INV-2026-0042", + "subtotal": Decimal("199.95"), + "tax_rate": Fraction(1, 5), # exactly 20%, no float drift + "total": Decimal("239.94"), + "paid": False, +} + +packed = msgpack.packb(invoice, default=encode_custom) +note(f"Packed size: {len(packed)} bytes.") + +restored = msgpack.unpackb(packed, ext_hook=decode_custom, raw=False) + +note("Restored invoice (note that types are preserved exactly):") +display(restored, append=True) + +types_seen = {key: type(value).__name__ for key, value in restored.items()} +note("Types after round-trip:") +display(types_seen, append=True) + +heading("Sanity check") +note( + "Decimal and Fraction came back as themselves, not as floats or " + "strings, so arithmetic still works as intended." +) +display(HTML( + f"
subtotal * tax_rate = "
+    f"{restored['subtotal'] * restored['tax_rate']}
" +), append=True) diff --git a/examples/msgpack/custom_types_with_ext/config.toml b/examples/msgpack/custom_types_with_ext/config.toml new file mode 100644 index 0000000..0c91421 --- /dev/null +++ b/examples/msgpack/custom_types_with_ext/config.toml @@ -0,0 +1 @@ +packages = ["msgpack"] diff --git a/examples/msgpack/custom_types_with_ext/setup.py b/examples/msgpack/custom_types_with_ext/setup.py new file mode 100644 index 0000000..baad549 --- /dev/null +++ b/examples/msgpack/custom_types_with_ext/setup.py @@ -0,0 +1,24 @@ +"""Setup for the custom-types example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import msgpack +from decimal import Decimal +from fractions import Fraction diff --git a/examples/msgpack/order.json b/examples/msgpack/order.json new file mode 100644 index 0000000..b53421d --- /dev/null +++ b/examples/msgpack/order.json @@ -0,0 +1,5 @@ +[ + "pack_and_unpack", + "streaming_unpacker", + "custom_types_with_ext" +] diff --git a/examples/msgpack/pack_and_unpack/code.py b/examples/msgpack/pack_and_unpack/code.py new file mode 100644 index 0000000..68a7aa1 --- /dev/null +++ b/examples/msgpack/pack_and_unpack/code.py @@ -0,0 +1,55 @@ +""" +A first look at msgpack: a compact, fast binary serialization format +that's a drop-in alternative to JSON for many use cases. + +Docs: https://msgpack-python.readthedocs.io/ +""" +from IPython.core.display import display, HTML + +# A small payload that might fly between two services: a sensor reading +# from a fictional weather station. +reading = { + "station_id": "WX-204", + "location": [52.52, 13.405], + "temperature_c": 18.7, + "humidity_pct": 64, + "active": True, + "tags": ["calibrated", "outdoor"], +} + +heading("1. Pack a Python object into bytes") +note( + "msgpack.packb turns Python data into a compact bytes object. " + "msgpack.unpackb is the inverse." +) + +packed = msgpack.packb(reading) +display(HTML(f"
type: {type(packed).__name__}
" + f"bytes: {packed!r}
"), append=True) + +restored = msgpack.unpackb(packed) +note("Round-tripped back to a Python dict:") +display(restored, append=True) + +heading("2. Compared with JSON") +note( + "For the same data, MessagePack is typically smaller than the " + "equivalent JSON text, and faster to parse. Sizes for our reading:" +) + +json_bytes = json.dumps(reading).encode("utf-8") +sizes = { + "json (utf-8 bytes)": len(json_bytes), + "msgpack (bytes)": len(packed), + "msgpack savings": f"{100 * (1 - len(packed) / len(json_bytes)):.1f}%", +} +display(sizes, append=True) + +heading("3. Aliases for the json/pickle crowd") +note( + "msgpack.dumps and msgpack.loads are aliases for packb and unpackb, " + "so the API feels familiar." +) + +same = msgpack.loads(msgpack.dumps(reading)) +note(f"Round-trip equal to the original? {same == reading}") diff --git a/examples/msgpack/pack_and_unpack/config.toml b/examples/msgpack/pack_and_unpack/config.toml new file mode 100644 index 0000000..0c91421 --- /dev/null +++ b/examples/msgpack/pack_and_unpack/config.toml @@ -0,0 +1 @@ +packages = ["msgpack"] diff --git a/examples/msgpack/pack_and_unpack/setup.py b/examples/msgpack/pack_and_unpack/setup.py new file mode 100644 index 0000000..12b2149 --- /dev/null +++ b/examples/msgpack/pack_and_unpack/setup.py @@ -0,0 +1,45 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + """Wrap pyscript.display so output lands in the example target.""" + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import msgpack +import json diff --git a/examples/msgpack/streaming_unpacker/code.py b/examples/msgpack/streaming_unpacker/code.py new file mode 100644 index 0000000..2b400f6 --- /dev/null +++ b/examples/msgpack/streaming_unpacker/code.py @@ -0,0 +1,63 @@ +# --------------------------------------------------------------------- +# Streaming: many small messages concatenated into one byte stream. +# --------------------------------------------------------------------- + +heading("Streaming unpack: a log of trades") +note( + "Real systems often append many MessagePack messages back-to-back " + "into a file or socket. msgpack.Unpacker reads them one at a time, " + "either from a file-like object or via its feed() method." +) + +# A small ledger of trades. We pack each row independently and write +# the bytes into one buffer, the way a log file would grow over time. +trades = [ + {"id": 1001, "symbol": "ACME", "qty": 50, "price": 12.40}, + {"id": 1002, "symbol": "GLOBO", "qty": 10, "price": 88.10}, + {"id": 1003, "symbol": "ACME", "qty": 25, "price": 12.55}, + {"id": 1004, "symbol": "INITECH", "qty": 5, "price": 305.00}, + {"id": 1005, "symbol": "GLOBO", "qty": 40, "price": 87.95}, +] + +buffer = BytesIO() +for trade in trades: + buffer.write(msgpack.packb(trade)) + +note(f"Total stream size: {buffer.tell()} bytes " + f"for {len(trades)} messages.") + +# Iterate the Unpacker like a generator: each iteration yields the +# next fully-decoded message from the stream. +buffer.seek(0) +unpacker = msgpack.Unpacker(buffer, raw=False) + +decoded = [] +for trade in unpacker: + decoded.append(trade) + +note("Decoded trades, one message at a time:") +display(decoded, append=True) + +heading("Feeding bytes incrementally") +note( + "When bytes arrive in chunks (a network socket, a slow file), " + "feed() lets you push partial data and pull whole messages out " + "as they become available." +) + +# Simulate a chunked arrival by splitting the buffer at an arbitrary +# midpoint that probably falls inside a message. +all_bytes = buffer.getvalue() +split = len(all_bytes) // 2 +chunks = [all_bytes[:split], all_bytes[split:]] + +incremental = msgpack.Unpacker(raw=False) +results = [] +for i, chunk in enumerate(chunks, start=1): + incremental.feed(chunk) + # Drain any complete messages that the new chunk made available. + for msg in incremental: + results.append((f"after chunk {i}", msg["id"], msg["symbol"])) + +note("Messages surfaced as each chunk was fed in:") +display(results, append=True) diff --git a/examples/msgpack/streaming_unpacker/config.toml b/examples/msgpack/streaming_unpacker/config.toml new file mode 100644 index 0000000..0c91421 --- /dev/null +++ b/examples/msgpack/streaming_unpacker/config.toml @@ -0,0 +1 @@ +packages = ["msgpack"] diff --git a/examples/msgpack/streaming_unpacker/setup.py b/examples/msgpack/streaming_unpacker/setup.py new file mode 100644 index 0000000..483bf3c --- /dev/null +++ b/examples/msgpack/streaming_unpacker/setup.py @@ -0,0 +1,23 @@ +"""Setup for the streaming Unpacker example.""" +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import msgpack +from io import BytesIO From a0896c698df32a763db6968fa9b0c1dc421793b9 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 4 Jun 2026 17:43:56 +0100 Subject: [PATCH 2/2] Fix imports. --- examples/msgpack/custom_types_with_ext/code.py | 7 ++++++- examples/msgpack/custom_types_with_ext/setup.py | 4 ---- examples/msgpack/pack_and_unpack/code.py | 4 ++++ examples/msgpack/pack_and_unpack/setup.py | 4 ---- examples/msgpack/streaming_unpacker/code.py | 3 +++ examples/msgpack/streaming_unpacker/setup.py | 3 --- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/msgpack/custom_types_with_ext/code.py b/examples/msgpack/custom_types_with_ext/code.py index 152de76..3acfa98 100644 --- a/examples/msgpack/custom_types_with_ext/code.py +++ b/examples/msgpack/custom_types_with_ext/code.py @@ -1,6 +1,10 @@ # --------------------------------------------------------------------- # Teaching msgpack about types it doesn't know natively. # --------------------------------------------------------------------- +import msgpack +from decimal import Decimal +from fractions import Fraction + heading("Custom types: Decimal and Fraction via ExtType") note( @@ -64,7 +68,8 @@ def decode_custom(code, data): "Decimal and Fraction came back as themselves, not as floats or " "strings, so arithmetic still works as intended." ) +tax_as_decimal = Decimal(restored['tax_rate'].numerator) / Decimal(restored['tax_rate'].denominator) display(HTML( f"
subtotal * tax_rate = "
-    f"{restored['subtotal'] * restored['tax_rate']}
" + f"{restored['subtotal'] * tax_as_decimal}" ), append=True) diff --git a/examples/msgpack/custom_types_with_ext/setup.py b/examples/msgpack/custom_types_with_ext/setup.py index baad549..c985a6d 100644 --- a/examples/msgpack/custom_types_with_ext/setup.py +++ b/examples/msgpack/custom_types_with_ext/setup.py @@ -18,7 +18,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -import msgpack -from decimal import Decimal -from fractions import Fraction diff --git a/examples/msgpack/pack_and_unpack/code.py b/examples/msgpack/pack_and_unpack/code.py index 68a7aa1..918efc6 100644 --- a/examples/msgpack/pack_and_unpack/code.py +++ b/examples/msgpack/pack_and_unpack/code.py @@ -6,6 +6,10 @@ """ from IPython.core.display import display, HTML +import msgpack +import json + + # A small payload that might fly between two services: a sensor reading # from a fictional weather station. reading = { diff --git a/examples/msgpack/pack_and_unpack/setup.py b/examples/msgpack/pack_and_unpack/setup.py index 12b2149..b4f3ee1 100644 --- a/examples/msgpack/pack_and_unpack/setup.py +++ b/examples/msgpack/pack_and_unpack/setup.py @@ -39,7 +39,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - - -import msgpack -import json diff --git a/examples/msgpack/streaming_unpacker/code.py b/examples/msgpack/streaming_unpacker/code.py index 2b400f6..2745f6e 100644 --- a/examples/msgpack/streaming_unpacker/code.py +++ b/examples/msgpack/streaming_unpacker/code.py @@ -1,6 +1,9 @@ # --------------------------------------------------------------------- # Streaming: many small messages concatenated into one byte stream. # --------------------------------------------------------------------- +import msgpack +from io import BytesIO + heading("Streaming unpack: a log of trades") note( diff --git a/examples/msgpack/streaming_unpacker/setup.py b/examples/msgpack/streaming_unpacker/setup.py index 483bf3c..a89f700 100644 --- a/examples/msgpack/streaming_unpacker/setup.py +++ b/examples/msgpack/streaming_unpacker/setup.py @@ -18,6 +18,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -import msgpack -from io import BytesIO