diff --git a/examples/aiohappyeyeballs/README.md b/examples/aiohappyeyeballs/README.md
new file mode 100644
index 0000000..2837575
--- /dev/null
+++ b/examples/aiohappyeyeballs/README.md
@@ -0,0 +1,18 @@
+# aiohappyeyeballs 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/aiohappyeyeballs/addr_info_basics/code.py b/examples/aiohappyeyeballs/addr_info_basics/code.py
new file mode 100644
index 0000000..55c3e6e
--- /dev/null
+++ b/examples/aiohappyeyeballs/addr_info_basics/code.py
@@ -0,0 +1,64 @@
+"""
+A first look at aiohappyeyeballs: shaping addrinfo lists.
+
+The Happy Eyeballs algorithm (RFC 8305) races IPv6 and IPv4 connection
+attempts so dual-stack clients fall back gracefully when one family is
+slow or unreachable. The aiohappyeyeballs package provides the building
+blocks asyncio uses to do this when you already have a list of resolved
+addresses (rather than a hostname).
+
+In this first example we'll skip the network entirely and focus on the
+addrinfo helpers: the small list-manipulation utilities that let you
+prepare and tidy the input to the Happy Eyeballs machinery.
+"""
+from IPython.core.display import display, HTML
+import socket
+
+heading("A handful of resolved addresses for example.org")
+note(
+ "Normally you'd get this list from loop.getaddrinfo() or a "
+ "DNS cache. We'll build it by hand so we can see exactly what the helpers do."
+)
+
+# Each entry is the same 5-tuple shape that socket.getaddrinfo returns:
+# (family, type, proto, canonname, sockaddr).
+addr_infos = [
+ (socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2606:2800:220:1::1", 80, 0, 0)),
+ (socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2606:2800:220:1::2", 80, 0, 0)),
+ (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", 80)),
+ (socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.35", 80)),
+]
+format_addr_infos(addr_infos)
+
+heading("Adding a local bind address with addr_to_addr_infos")
+note(
+ "When you want to bind the outgoing socket to a specific local address, "
+ "start_connection expects a full addrinfo list, not a tuple. "
+ "addr_to_addr_infos does that conversion for you."
+)
+local_addr_infos = addr_to_addr_infos(("127.0.0.1", 0))
+format_addr_infos(local_addr_infos)
+
+heading("Interleaving families with pop_addr_infos_interleave")
+note(
+ "Happy Eyeballs prefers to alternate between IPv6 and IPv4. "
+ "pop_addr_infos_interleave(addr_infos, 1) removes the first "
+ "address of each family in place — handy after a successful attempt, to "
+ "skip past addresses you already tried."
+)
+working = list(addr_infos)
+pop_addr_infos_interleave(working, 1)
+note("After popping one address per family:")
+format_addr_infos(working)
+
+heading("Pruning a known-bad address with remove_addr_infos")
+note(
+ "If you discover an address is unreachable (perhaps from a previous "
+ "failure), remove_addr_infos strips every entry that matches."
+)
+working2 = list(addr_infos)
+remove_addr_infos(working2, ("93.184.216.34", 80))
+note("After removing 93.184.216.34:80")
+format_addr_infos(working2)
+
+note(f"aiohappyeyeballs version in use: {aiohappyeyeballs.__version__}")
diff --git a/examples/aiohappyeyeballs/addr_info_basics/config.toml b/examples/aiohappyeyeballs/addr_info_basics/config.toml
new file mode 100644
index 0000000..528415a
--- /dev/null
+++ b/examples/aiohappyeyeballs/addr_info_basics/config.toml
@@ -0,0 +1 @@
+packages = ["aiohappyeyeballs"]
diff --git a/examples/aiohappyeyeballs/addr_info_basics/setup.py b/examples/aiohappyeyeballs/addr_info_basics/setup.py
new file mode 100644
index 0000000..0438f29
--- /dev/null
+++ b/examples/aiohappyeyeballs/addr_info_basics/setup.py
@@ -0,0 +1,68 @@
+"""
+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) + + +import socket +import aiohappyeyeballs +from aiohappyeyeballs import ( + addr_to_addr_infos, + pop_addr_infos_interleave, + remove_addr_infos, +) + + +def format_addr_infos(addr_infos): + """Render a list of getaddrinfo-style 5-tuples as an HTML table.""" + rows = ["{sockaddr}{winner}")
+
+# Visualise the race as a Gantt-style chart.
+fig, ax = plt.subplots(figsize=(9, 3.5))
+colors = {"won": "seagreen", "cancelled": "lightgray", "failed": "indianred"}
+for i, (sockaddr, start, end, outcome) in enumerate(log):
+ ax.barh(i, end - start, left=start, color=colors[outcome],
+ edgecolor="black", linewidth=0.5)
+ ax.text(end + 0.02, i, outcome, va="center", fontsize=9)
+ax.set_yticks(range(len(log)))
+ax.set_yticklabels([str(entry[0]) for entry in log], fontsize=9)
+ax.set_xlabel("Time since start (seconds)")
+ax.set_title("Happy Eyeballs race: staggered connection attempts")
+ax.invert_yaxis()
+fig.tight_layout()
+display(fig, append=True)
+
+heading("Pruning a flaky address before the next attempt")
+note(
+ "If your application learns that an address is unhealthy, you can clean "
+ "the addrinfo list before reusing it. Here we drop the slow IPv6 address "
+ "and reorder so the IPv4 fallback is tried first."
+)
+pruned = list(addr_infos)
+remove_addr_infos(pruned, ("2001:db8::1", 80, 0, 0))
+pop_addr_infos_interleave(pruned, 0) # no-op example: keeps ordering intact
+note("Cleaned addrinfo list:")
+for entry in pruned:
+ display(HTML(f"{entry}"), append=True)
diff --git a/examples/aiohappyeyeballs/happy_eyeballs_simulation/config.toml b/examples/aiohappyeyeballs/happy_eyeballs_simulation/config.toml
new file mode 100644
index 0000000..8e42cc1
--- /dev/null
+++ b/examples/aiohappyeyeballs/happy_eyeballs_simulation/config.toml
@@ -0,0 +1 @@
+packages = ["aiohappyeyeballs", "matplotlib"]
diff --git a/examples/aiohappyeyeballs/happy_eyeballs_simulation/setup.py b/examples/aiohappyeyeballs/happy_eyeballs_simulation/setup.py
new file mode 100644
index 0000000..8c3fd64
--- /dev/null
+++ b/examples/aiohappyeyeballs/happy_eyeballs_simulation/setup.py
@@ -0,0 +1,28 @@
+"""Lightweight setup for the second example: same names as cell 1, no shim."""
+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) + + +import socket +import asyncio +import matplotlib.pyplot as plt +import aiohappyeyeballs +from aiohappyeyeballs import ( + addr_to_addr_infos, + pop_addr_infos_interleave, + remove_addr_infos, +) diff --git a/examples/aiohappyeyeballs/order.json b/examples/aiohappyeyeballs/order.json new file mode 100644 index 0000000..589e981 --- /dev/null +++ b/examples/aiohappyeyeballs/order.json @@ -0,0 +1,4 @@ +[ + "addr_info_basics", + "happy_eyeballs_simulation" +]