Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/msgpack/README.md
Original file line number Diff line number Diff line change
@@ -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.
75 changes: 75 additions & 0 deletions examples/msgpack/custom_types_with_ext/code.py
Original file line number Diff line number Diff line change
@@ -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: <code>default</code> "
"translates unknown objects to ExtType on the way out, and "
"<code>ext_hook</code> 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: <strong>{len(packed)} bytes</strong>.")

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"<pre>subtotal * tax_rate = "
f"{restored['subtotal'] * tax_as_decimal}</pre>"
), append=True)
1 change: 1 addition & 0 deletions examples/msgpack/custom_types_with_ext/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["msgpack"]
20 changes: 20 additions & 0 deletions examples/msgpack/custom_types_with_ext/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

5 changes: 5 additions & 0 deletions examples/msgpack/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"pack_and_unpack",
"streaming_unpacker",
"custom_types_with_ext"
]
59 changes: 59 additions & 0 deletions examples/msgpack/pack_and_unpack/code.py
Original file line number Diff line number Diff line change
@@ -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"<pre>type: {type(packed).__name__}<br>"
f"bytes: {packed!r}</pre>"), 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? <strong>{same == reading}</strong>")
1 change: 1 addition & 0 deletions examples/msgpack/pack_and_unpack/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["msgpack"]
41 changes: 41 additions & 0 deletions examples/msgpack/pack_and_unpack/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
66 changes: 66 additions & 0 deletions examples/msgpack/streaming_unpacker/code.py
Original file line number Diff line number Diff line change
@@ -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: <strong>{buffer.tell()} bytes</strong> "
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)
1 change: 1 addition & 0 deletions examples/msgpack/streaming_unpacker/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["msgpack"]
20 changes: 20 additions & 0 deletions examples/msgpack/streaming_unpacker/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)