<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: wright-benjamin-1701</title>
    <description>The latest articles on DEV Community by wright-benjamin-1701 (@wrightbenjamin1701).</description>
    <link>https://dev.to/wrightbenjamin1701</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4139373%2F484e161f-aa9d-4f18-8778-021d9f5ecb9f.png</url>
      <title>DEV Community: wright-benjamin-1701</title>
      <link>https://dev.to/wrightbenjamin1701</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wrightbenjamin1701"/>
    <language>en</language>
    <item>
      <title>Building a Software KVM in Python with Clean Architecture and AI</title>
      <dc:creator>wright-benjamin-1701</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:21:51 +0000</pubDate>
      <link>https://dev.to/wrightbenjamin1701/building-a-software-kvm-in-python-with-clean-architecture-and-ai-3g18</link>
      <guid>https://dev.to/wrightbenjamin1701/building-a-software-kvm-in-python-with-clean-architecture-and-ai-3g18</guid>
      <description>&lt;h2&gt;
  
  
  What is mouse-share?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;mouse-share&lt;/strong&gt; is a software KVM — it lets you share a single keyboard and mouse across multiple computers on your LAN. Move your cursor off the edge of one screen and it appears on the next machine, exactly like a multi-monitor setup but across separate PCs.&lt;/p&gt;

&lt;p&gt;If you've used Barrier or Synergy, you know the concept. I built mouse-share because the existing options are either abandoned, commercial, or carry decades of C++ complexity. I wanted something lightweight, strongly typed, and written in modern Python — with gaming-grade latency to boot.&lt;/p&gt;

&lt;p&gt;The result is a ~3,500-line Python project with clean architecture, protocol-based dependency injection, platform-specific backends for Linux and Windows, a binary UDP event protocol, and a 310-line ops runbook. This article walks through the architecture and how AI accelerated the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The project follows a layered architecture that keeps platform-specific code isolated from business logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/mouse_share/
├── event/                 # Domain — platform-agnostic events &amp;amp; codec
│   ├── types.py           #   Frozen InputEvent dataclasses
│   ├── codec.py           #   Binary UDP wire format
│   ├── held.py            #   Tracked key/button state
│   └── scancode.py        #   HID ↔ platform scancode maps
│
├── edge/                  # Domain — screen geometry &amp;amp; crossing
│   ├── detector.py        #   Cursor tracking state machine
│   └── layout.py          #   Multi-screen adjacency graph
│
├── capture/               # Infrastructure — OS input capture
│   ├── base.py            #   CaptureBackend protocol
│   ├── linux_x11.py       #   X11 XRecord
│   └── windows.py         #   Win32 low-level hooks
│
├── emulate/               # Infrastructure — OS input injection
│   ├── base.py            #   EmulateBackend protocol
│   ├── linux_x11.py       #   X11 XTest
│   └── windows.py         #   Win32 SendInput
│
├── net/                   # Infrastructure — networking
│   ├── protocol.py        #   Message framing &amp;amp; constants
│   ├── control.py         #   TCP control channel
│   ├── transport.py       #   UDP event transport + batching
│   ├── link_selector.py   #   Fastest-NIC latency probing
│   └── sockopt.py         #   Low-latency socket tuning
│
├── clipboard/             # Infrastructure — clipboard sync
│   ├── base.py            #   ClipboardBackend protocol
│   └── qt.py              #   Qt-based implementation
│
├── platform.py            # Factory — platform detection &amp;amp; DI
├── app.py                 # Application — orchestrator &amp;amp; state machine
├── config.py              # Application — config persistence
├── hotkey.py              # Application — escape combo detector
└── screen.py              # Application — multi-monitor detection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the centre is a state machine that drives the entire application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IDLE → CONNECTING → ACTIVE_LOCAL → ACTIVE_REMOTE
                                 ↘ CONTROLLED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you're using your local machine, you're in &lt;code&gt;ACTIVE_LOCAL&lt;/code&gt;. Move the cursor off-screen and focus switches to the remote peer (&lt;code&gt;ACTIVE_REMOTE&lt;/code&gt;). The remote peer enters &lt;code&gt;CONTROLLED&lt;/code&gt;, injecting your input. Push the cursor back across the edge and focus returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain Layer — Platform-Agnostic Events
&lt;/h2&gt;

&lt;p&gt;Every input event in mouse-share is a frozen dataclass using USB HID scancodes as the universal keycode space. This means the domain layer has zero platform dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/mouse_share/event/types.py
&lt;/span&gt;
&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MouseMove&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MOUSE_MOVE&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KeyPress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scancode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KEY_PRESS&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KeyRelease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;scancode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KEY_RELEASE&lt;/span&gt;

&lt;span class="n"&gt;InputEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MouseMove&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;MouseButton&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;MouseScroll&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;KeyPress&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;KeyRelease&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;InputEvent&lt;/code&gt; union type gives exhaustive pattern matching everywhere events are consumed. Frozen dataclasses make events hashable and safe to pass between threads. Slots keep memory tight — these objects are created at up to 1,000 Hz on a busy mouse.&lt;/p&gt;

&lt;p&gt;HID scancodes were the right choice for the canonical keycode space. Every platform captures in its native codes (Win32 virtual keys, X11 keysyms) and the capture backend translates to HID at the boundary. The emulation backend translates back on the receiving end. This keeps all the cross-platform mapping logic in exactly two places per platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protocol-Based Dependency Injection
&lt;/h2&gt;

&lt;p&gt;Instead of ABC inheritance, mouse-share uses Python's &lt;code&gt;@runtime_checkable&lt;/code&gt; Protocols for all platform contracts. Here's the capture backend interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/mouse_share/capture/base.py
&lt;/span&gt;
&lt;span class="nd"&gt;@runtime_checkable&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CaptureBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_suppressing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suppressing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_escape_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cursor_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the emulation backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/mouse_share/emulate/base.py
&lt;/span&gt;
&lt;span class="nd"&gt;@runtime_checkable&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmulateBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InputEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;warp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cursor_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why Protocols over ABCs? Three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structural typing.&lt;/strong&gt; A class satisfies the protocol by having the right methods — no inheritance required. Platform backends don't need to import or subclass anything from the core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime checking.&lt;/strong&gt; &lt;code&gt;@runtime_checkable&lt;/code&gt; lets you do &lt;code&gt;isinstance(obj, CaptureBackend)&lt;/code&gt; for defensive assertions without paying the ABC tax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test doubles for free.&lt;/strong&gt; Any object with the right shape works, which makes mock creation trivial in the 20-module test suite.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Platform Factory — Targeting Linux and Windows
&lt;/h2&gt;

&lt;p&gt;Platform-specific modules are lazily imported through factory functions. This avoids import errors on the wrong OS — you can develop the Windows backend on a Linux machine without import failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/mouse_share/platform.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_capture_backend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CaptureBackend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;win32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mouse_share.capture.windows&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WindowsCapture&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;WindowsCapture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;linux&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;session_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;XDG_SESSION_TYPE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wayland&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wayland capture is not yet supported. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run under X11 or Xwayland (XDG_SESSION_TYPE=x11).&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mouse_share.capture.linux_x11&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;X11Capture&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;X11Capture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;darwin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;macOS capture is not yet implemented.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unsupported platform: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pyproject.toml&lt;/code&gt; uses environment markers to keep platform dependencies separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: &lt;code&gt;evdev &amp;gt;= 1.7&lt;/code&gt; + &lt;code&gt;python-xlib &amp;gt;= 0.33&lt;/code&gt; (X11 XRecord/XTest)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt;: pure ctypes — zero extra dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: stubs ready for &lt;code&gt;pyobjc-framework-Quartz&lt;/code&gt; + &lt;code&gt;pyobjc-framework-Cocoa&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each platform implementation is radically different under the hood. The Windows capture backend runs a Win32 message pump with &lt;code&gt;SetWindowsHookEx&lt;/code&gt; for &lt;code&gt;WH_MOUSE_LL&lt;/code&gt; and &lt;code&gt;WH_KEYBOARD_LL&lt;/code&gt; in a dedicated thread. The Linux backend uses XRecord to tap the X11 event stream. But from the orchestrator's perspective, they're identical — just a &lt;code&gt;CaptureBackend&lt;/code&gt; that calls &lt;code&gt;on_event&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking for Low Latency
&lt;/h2&gt;

&lt;p&gt;A software KVM lives or dies by its latency. mouse-share uses a dual-channel architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TCP (port 24800)&lt;/strong&gt;: Control channel — length-prefixed JSON messages for handshake, screen config, clipboard sync, and focus switching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UDP (port 24801)&lt;/strong&gt;: Event transport — binary-encoded input events batched and flushed at 125 Hz.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The binary codec keeps events tiny:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Header (4 bytes): version:u8 | flags:u8 | count:u16 LE

MouseMove:    0x01 | dx:i16 | dy:i16        → 5 bytes
MouseButton:  0x03 | button:u8 | pressed:u8 → 3 bytes
KeyPress:     0x10 | scancode:u16           → 3 bytes
KeyRelease:   0x11 | scancode:u16           → 3 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pre-compiled &lt;code&gt;struct.Struct&lt;/code&gt; objects eliminate per-call overhead in the codec. A typical batch of 8 mouse moves fits in a single 44-byte UDP datagram — well under the MTU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mouse move coalescing&lt;/strong&gt; is critical. At 1,000 Hz polling, the mouse generates far more events than we need to send. Consecutive &lt;code&gt;MouseMove&lt;/code&gt; events are collapsed into a single event with accumulated deltas before each flush. This cuts traffic dramatically without adding any perceptible latency.&lt;/p&gt;

&lt;p&gt;On the socket side, every knob is tuned for LAN latency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DSCP/TOS&lt;/strong&gt; marking (&lt;code&gt;0xB8&lt;/code&gt; — Expedited Forwarding) so managed switches prioritise our traffic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TCP_NODELAY&lt;/strong&gt; to disable Nagle's algorithm on the control channel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SO_BUSY_POLL&lt;/strong&gt; (Linux) — kernel spins for 50μs instead of sleeping, shaving ~45μs off receive latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SO_PRIORITY&lt;/strong&gt; (Linux) — elevated kernel packet priority&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SO_BINDTODEVICE&lt;/strong&gt; — NIC pinning when multiple interfaces are present&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For multi-NIC setups, a &lt;strong&gt;link selector&lt;/strong&gt; probes all local×remote IP pairs with timed UDP round-trips and picks the path with the lowest median RTT. This means if you have both Wi-Fi and a direct ethernet cable to your other machine, mouse-share automatically uses the cable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge Detection &amp;amp; Focus Switching
&lt;/h2&gt;

&lt;p&gt;The edge detector tracks the cursor position in layout coordinates. When the cursor reaches the edge of a screen and the next delta pushes it outward, a crossing event fires. The orchestrator then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Records held keys and buttons via &lt;code&gt;HeldInput&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sends synthetic release events for all held inputs on the source machine (preventing stuck keys)&lt;/li&gt;
&lt;li&gt;Switches focus to the target peer via a &lt;code&gt;SWITCH_FOCUS&lt;/code&gt; control message&lt;/li&gt;
&lt;li&gt;Warps the cursor to the entry point on the target screen (proportionally mapped along the shared edge)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If things go wrong — say the remote machine crashes — pressing &lt;strong&gt;Ctrl+Alt+Escape&lt;/strong&gt; immediately breaks out of remote mode and returns control to the local machine. The hotkey detector runs inside the capture backend itself, so it works even when events are being suppressed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI Accelerated Development
&lt;/h2&gt;

&lt;p&gt;I used Claude Code throughout this project as a pair-programming partner. Here's where AI made the biggest difference:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaffolding the architecture.&lt;/strong&gt; The Protocol-based DI pattern emerged from an early conversation about how to keep platform code isolated. I described the constraints (multiple OS backends, no framework dependencies, easy to test) and the AI suggested &lt;code&gt;@runtime_checkable&lt;/code&gt; Protocols with factory functions — a pattern I hadn't used at this scale before. It was the right call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform-specific low-level code.&lt;/strong&gt; Writing X11 XRecord capture code or Win32 &lt;code&gt;SetWindowsHookEx&lt;/code&gt; message pumps is not something most Python developers do daily. The documentation for these APIs is sparse, scattered across man pages and decades-old MSDN articles. AI was invaluable here — it could synthesize working ctypes bindings and XRecord setup code that would have taken hours of documentation archaeology to piece together manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comprehensive test suite.&lt;/strong&gt; The project has 20 unit test modules covering everything from the binary codec to platform-specific capture and emulation backends. AI generated the initial test scaffolding and helped ensure edge cases were covered — things like truncated datagrams, scancode boundary values, and multi-monitor geometry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The runbook.&lt;/strong&gt; The 310-line operations guide covering installation, firewall rules, auto-start configuration (systemd, launchd, Task Scheduler), and troubleshooting was drafted collaboratively. AI knew the exact &lt;code&gt;ufw&lt;/code&gt; commands, &lt;code&gt;launchd&lt;/code&gt; plist format, and PowerShell &lt;code&gt;Register-ScheduledTask&lt;/code&gt; invocations, saving significant research time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What AI didn't do:&lt;/strong&gt; replace understanding. Every suggestion was reviewed, tested, and often adapted. The architecture decisions were mine. The AI accelerated the implementation — especially in domains where documentation is sparse — but it didn't write the project for me. It's a research accelerator, not a code generator you trust blindly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Clean architecture pays off for multi-platform projects.&lt;/strong&gt; The upfront cost of defining protocols and factories is trivial compared to the cost of entangling platform code with business logic. When I added Windows support, I didn't touch a single line in the domain or application layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protocols + factory functions = lightweight DI without frameworks.&lt;/strong&gt; No dependency injection container, no decorators, no magic. Just interfaces defined as Protocols and a factory function that returns the right implementation. It's simple enough to fit in your head and powerful enough to support multiple platforms and a full test suite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI is best used as a research accelerator for obscure APIs.&lt;/strong&gt; The places where AI saved the most time weren't the high-level architecture (that requires human judgement about trade-offs) but the low-level platform glue: ctypes struct definitions, X11 extension setup, Win32 message pump boilerplate. For these tasks, AI is dramatically faster than reading sparse documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;mouse-share is open source and available on GitHub: &lt;a href="https://github.com/wright-benjamin-1701/mouse-share" rel="noopener noreferrer"&gt;github.com/wright-benjamin-1701/mouse-share&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project ships with a comprehensive &lt;a href="https://github.com/wright-benjamin-1701/mouse-share/blob/main/docs/runbook.md" rel="noopener noreferrer"&gt;runbook&lt;/a&gt; covering installation, firewall setup, and auto-start configuration for all supported platforms.&lt;/p&gt;

&lt;p&gt;Contributions welcome — especially macOS support (the protocol stubs are ready) and Wayland capture via the input-capture portal.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>python</category>
      <category>cleanarchitecture</category>
      <category>ai</category>
    </item>
    <item>
      <title>Scrape Wikipedia in 4m♭ (four minutes flat)</title>
      <dc:creator>wright-benjamin-1701</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:08:07 +0000</pubDate>
      <link>https://dev.to/wrightbenjamin1701/scrape-wikipedia-in-4m-four-minutes-flat-gg8</link>
      <guid>https://dev.to/wrightbenjamin1701/scrape-wikipedia-in-4m-four-minutes-flat-gg8</guid>
      <description>&lt;h1&gt;
  
  
  I Process All of English Wikipedia in Under 4 Minutes
&lt;/h1&gt;

&lt;p&gt;After a one-time 25 GB download, every subsequent extraction — jazz musicians, sci-fi novels, literary authors, whatever you want — runs against all 25.8 million Wikipedia pages in about three and a half minutes. Here's how I got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;Transform a corpus of semi-structured Wikipedia data into searchable, structured database tables &lt;strong&gt;as fast as possible&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Previously, I've done &lt;a href="https://benwright.blog/the-new-jazz-album-data-visualization-portfolio-edition/" rel="noopener noreferrer"&gt;data visualization projects&lt;/a&gt; with data scraped from Wikipedia pages. I used asynchronous, depth-first page crawling that extracted key data related to jazz music from artist, album, and genre pages.&lt;/p&gt;

&lt;p&gt;After fetching a page, a top-level switch classified each one as a jazz artist, jazz album, or jazz-related genre. This determined which shape of &lt;a href="https://beautiful-soup-4.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;Beautiful Soup&lt;/a&gt;-based extractor to run on that page, each one suited to target a specific table in the extracted database (artist, album, or genre). To turn a Wikipedia page into a database row, the extractor had to identify the relevant data for each column on that page. In my original architecture, this just meant writing a function to query the DOM for each data element I wanted — such as "name", "instruments", or "years active".&lt;/p&gt;

&lt;p&gt;For each data element, I just had to inspect the DOM, write one Beautiful Soup query to grab its value, and cast/map it to the right column. Even accounting for the messiness of real-world wiki page data (DOM structure may not always be the same, or time ranges may look like "1967-2000", "1999-&lt;em&gt;present&lt;/em&gt;", or "'62-'89"), this worked fairly well. The vast majority of pages identified at the top level as jazz-related processed without skipping due to parsing errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  What didn't work
&lt;/h2&gt;

&lt;p&gt;Every time I wanted to change the extract functions or schema to accommodate messier data or new fields, the web crawl could take &lt;em&gt;hours&lt;/em&gt; to re-populate the extract database due to network speed and rate limiting.&lt;/p&gt;

&lt;p&gt;Even if I could reload already-fetched pages from disk instead of the web, what if I wanted to run extracts on science fiction books instead of jazz music? Caching wouldn't help scale extracts to new subjects.&lt;/p&gt;

&lt;p&gt;What could I do to speed things up? Parallelize? Yet, depth-first search is &lt;a href="https://link.springer.com/article/10.1007/BF01389000" rel="noopener noreferrer"&gt;notoriously difficult&lt;/a&gt; to parallelize — &lt;em&gt;especially&lt;/em&gt; considering the strong coupling of my web-crawling, extraction, and database writing operations.&lt;/p&gt;

&lt;p&gt;And yet, &lt;em&gt;most of my system was idling throughout the extract process&lt;/em&gt;. That meant there was room to rearchitect and improve. I had proved that I could generate structured data and insights from Wikipedia articles, but how could I design the system to be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extendable&lt;/strong&gt; to new topics without re-downloading anything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalable&lt;/strong&gt; horizontally, to make full use of my hardware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; enough that development iterations don't take hours&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How I redesigned it
&lt;/h2&gt;

&lt;p&gt;The biggest inspiration came when I noticed my extract jobs were roughly deterministic — I was downloading the same data from the same pages over and over again. This was acceptable for exploratory data analysis, but when it turned into a bottleneck for both the program and my development iterations, I asked: what are the tradeoffs for simply downloading a full dump of wiki data ahead of time?&lt;/p&gt;

&lt;p&gt;That question evolved into the architecture below.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pipeline
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph LR
    F["Dev&amp;lt;br/&amp;gt;writes a job class&amp;lt;br/&amp;gt;(or asks an LLM)"]
    G["Dev&amp;lt;br/&amp;gt;queries the DB&amp;lt;br/&amp;gt;SELECT ..."]
    A["Wikipedia&amp;lt;br/&amp;gt;~25 GB dump"] --&amp;gt; B["Download&amp;lt;br/&amp;gt;(async, one-time)"]
    B --&amp;gt; C["Preprocess:&amp;lt;br/&amp;gt;decompress once&amp;lt;br/&amp;gt;~109 GB raw"]
    C --&amp;gt; D["N workers&amp;lt;br/&amp;gt;parse + match"]
    D --&amp;gt; E["Extracted DB&amp;lt;br/&amp;gt;jazz.db&amp;lt;br/&amp;gt;scifi.db&amp;lt;br/&amp;gt;..."]
    F -.-&amp;gt;|"defines tables &amp;amp;&amp;lt;br/&amp;gt;page-to-row mapping"| D
    E -.-&amp;gt;|"SQL / pandas"| G&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Data from Wikipedia is downloaded from &lt;a href="https://dumps.wikimedia.org" rel="noopener noreferrer"&gt;dumps.wikimedia.org&lt;/a&gt;, decompressed, and preprocessed as a one-time job. The download takes a couple of hours, but you only repeat it as often as you need fresh wiki data. (Whether or not &lt;em&gt;jazz is dead&lt;/em&gt;, I don't need real-time streaming data on the latest album releases.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Why multiprocessing, not async
&lt;/h3&gt;

&lt;p&gt;The old architecture was I/O-bound — waiting on network requests. The new architecture is CPU-bound — parsing megabytes of wikitext per second. Python's GIL means threading won't help for CPU-bound work, so I use &lt;code&gt;ProcessPoolExecutor&lt;/code&gt; to fan out across all available cores.&lt;/p&gt;

&lt;p&gt;The key enabler is Wikipedia's &lt;strong&gt;multistream dump format&lt;/strong&gt;. Instead of one monolithic bz2 archive, the dump is split into ~258,000 independently-addressable compressed blocks of ~100 pages each. After a one-time decompression to a raw file, each worker can seek to any block and parse it independently — no coordination, no shared state, no locks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jobs are just Python files
&lt;/h3&gt;

&lt;p&gt;Adding a new extraction topic means dropping a single &lt;code&gt;.py&lt;/code&gt; file into the &lt;code&gt;jobs/&lt;/code&gt; directory. Each job implements a simple protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JazzJob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jazz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;default_db_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite:///data/databases/jazz.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WikiPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Does this Wikipedia page belong to my topic?&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WikiPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Parse the page and buffer a database row.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Write buffered rows to the database.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create tables from ORM models.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No registration, no config files. The CLI auto-discovers every job in the directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ wiki extract list
┌─────────────────┬─────────────────────────────────────────────┐
│ Name            │ Description                                 │
├─────────────────┼─────────────────────────────────────────────┤
│ jazz            │ Jazz artists, albums, and genres             │
│ scifi           │ Sci-fi novels, films, and authors            │
│ literature      │ Literary novels, authors/poets, and poetry   │
│ classical_music │ Classical composers, compositions, and operas│
│ philosophy      │ Philosophers, works, and traditions          │
│ ...             │ (10 jobs total)                              │
└─────────────────┴─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;Here's what actually happens when you run extractions against the full English Wikipedia dump (August 2026, 25.8 million pages) on a &lt;strong&gt;Ryzen 9 5950X&lt;/strong&gt; (16 cores / 32 threads) with 64 GB DDR4 RAM and AIO liquid cooling. With thermal headroom to spare, all 32 threads push to 99–100% utilization for the entire duration of an extract — this is a genuinely CPU-saturated workload:&lt;/p&gt;

&lt;h3&gt;
  
  
  Extraction timing
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Pages Scanned&lt;/th&gt;
&lt;th&gt;Matched&lt;/th&gt;
&lt;th&gt;Wall Clock&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;jazz&lt;/td&gt;
&lt;td&gt;25,792,184&lt;/td&gt;
&lt;td&gt;12,441&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3m 23s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~196K pages/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scifi&lt;/td&gt;
&lt;td&gt;25,792,184&lt;/td&gt;
&lt;td&gt;13,021&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3m 22s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~197K pages/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;literature&lt;/td&gt;
&lt;td&gt;25,792,184&lt;/td&gt;
&lt;td&gt;149,812&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5m 04s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~111K pages/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For lightweight jobs (jazz, scifi), the system sustains roughly &lt;strong&gt;195,000 pages per second&lt;/strong&gt; across 32 workers. Literature is slower because it matches 12x more pages — 150K vs 12–13K — so the wikitext parsing and database flush overhead dominates.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you get
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Jazz&lt;/strong&gt; — 10,967 artists, 1,408 albums, 66 genres:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Artists&lt;/th&gt;
&lt;th&gt;Albums&lt;/th&gt;
&lt;th&gt;Genres&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;title&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;name / artist&lt;/td&gt;
&lt;td&gt;73.5%&lt;/td&gt;
&lt;td&gt;97.0%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;birth / release year&lt;/td&gt;
&lt;td&gt;66.0%&lt;/td&gt;
&lt;td&gt;93.0%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;genres&lt;/td&gt;
&lt;td&gt;67.1%&lt;/td&gt;
&lt;td&gt;93.8%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;instruments&lt;/td&gt;
&lt;td&gt;53.4%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;labels&lt;/td&gt;
&lt;td&gt;35.9%&lt;/td&gt;
&lt;td&gt;95.6%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;personnel&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;80.3%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;description&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Sci-Fi&lt;/strong&gt; — 3,233 authors, 6,372 novels, 3,416 films:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Authors&lt;/th&gt;
&lt;th&gt;Novels&lt;/th&gt;
&lt;th&gt;Films&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;title&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;name / author&lt;/td&gt;
&lt;td&gt;80.3%&lt;/td&gt;
&lt;td&gt;79.3%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;birth / pub / release year&lt;/td&gt;
&lt;td&gt;68.4%&lt;/td&gt;
&lt;td&gt;78.0%&lt;/td&gt;
&lt;td&gt;82.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;genres&lt;/td&gt;
&lt;td&gt;41.6%&lt;/td&gt;
&lt;td&gt;74.5%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;director&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;78.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;description&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Literature&lt;/strong&gt; — 81,514 authors, 66,168 novels, 2,130 poetry collections.&lt;/p&gt;

&lt;p&gt;The fill rates reflect the reality of Wikipedia: well-known albums almost always have a label and release year, but many jazz musicians' pages lack a structured infobox for instruments or active years. That's useful signal in itself — it tells you where the data is rich and where you might want to augment it from other sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before and after
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph TB
    subgraph before ["Before: Web Crawling"]
        direction TB
        B1["Fetch page (network)"] --&amp;gt; B2["Parse HTML (Beautiful Soup)"]
        B2 --&amp;gt; B3["Write row to DB"]
        B3 --&amp;gt; B1
        B4["Hours per run"]
        B5["Single topic"]
        B6["Sequential"]
    end

    subgraph after ["After: Dump Processing"]
        direction TB
        A1["Download dump (one-time)"] --&amp;gt; A2["Preprocess (one-time)"]
        A2 --&amp;gt; A3["N workers parse in parallel"]
        A3 --&amp;gt; A4["Per-topic DBs"]
        A5["~3 min per run"]
        A6["Any topic"]
        A7["32 cores"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Web Crawling&lt;/th&gt;
&lt;th&gt;Dump Processing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time per run&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;~3 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bottleneck&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Network I/O&lt;/td&gt;
&lt;td&gt;CPU (wikitext parsing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New topic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;New crawler + hours of fetching&lt;/td&gt;
&lt;td&gt;One Python file + 3 min extract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parallelism&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Difficult (coupled I/O + parsing)&lt;/td&gt;
&lt;td&gt;Trivial (independent blocks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data freshness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time&lt;/td&gt;
&lt;td&gt;Monthly/bi-monthly snapshots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tradeoff is data freshness: dumps are snapshots, not live data. For my use case — historical jazz discographies, literary canons, sci-fi filmographies — monthly snapshots are more than sufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This architecture is modular enough to support several natural extensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incremental updates&lt;/strong&gt;: after a full extract, monitor a targeted subset of pages for changes between dumps rather than re-processing everything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Richer extraction&lt;/strong&gt;: use NLP or entity linking to fill in the gaps that simple infobox parsing misses (those red cells in the fill-rate tables)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More topics&lt;/strong&gt;: the same dump already supports 10 extraction jobs; adding more is a single file drop-in&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Clone the repo, download a dump, and you'll have a queryable database in minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/wright-benjamin-1701/wiki-crawler
&lt;span class="nb"&gt;cd &lt;/span&gt;wiki-crawler
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run wiki dump download &lt;span class="nt"&gt;--lang&lt;/span&gt; en
uv run wiki dump preprocess
uv run wiki extract run jazz       &lt;span class="c"&gt;# ~3 min on modern hardware&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then query it however you like — SQL, pandas, whatever. The data is yours.&lt;/p&gt;

&lt;p&gt;See the full code on &lt;a href="https://github.com/wright-benjamin-1701/wiki-crawler" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>performance</category>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
