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..3acfa98
--- /dev/null
+++ b/examples/msgpack/custom_types_with_ext/code.py
@@ -0,0 +1,75 @@
+# ---------------------------------------------------------------------
+# 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(
+ "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."
+)
+tax_as_decimal = Decimal(restored['tax_rate'].numerator) / Decimal(restored['tax_rate'].denominator)
+display(HTML(
+ f"
subtotal * tax_rate = "
+ f"{restored['subtotal'] * tax_as_decimal}"
+), 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..c985a6d
--- /dev/null
+++ b/examples/msgpack/custom_types_with_ext/setup.py
@@ -0,0 +1,20 @@
+"""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) + 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..918efc6 --- /dev/null +++ b/examples/msgpack/pack_and_unpack/code.py @@ -0,0 +1,59 @@ +""" +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 + +import msgpack +import json + + +# 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..b4f3ee1
--- /dev/null
+++ b/examples/msgpack/pack_and_unpack/setup.py
@@ -0,0 +1,41 @@
+"""
+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) diff --git a/examples/msgpack/streaming_unpacker/code.py b/examples/msgpack/streaming_unpacker/code.py new file mode 100644 index 0000000..2745f6e --- /dev/null +++ b/examples/msgpack/streaming_unpacker/code.py @@ -0,0 +1,66 @@ +# --------------------------------------------------------------------- +# Streaming: many small messages concatenated into one byte stream. +# --------------------------------------------------------------------- +import msgpack +from io import BytesIO + + +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..a89f700 --- /dev/null +++ b/examples/msgpack/streaming_unpacker/setup.py @@ -0,0 +1,20 @@ +"""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) +