Skip to main content

loom-client

Python WebSocket client for the Loom Genome Browser. Provides a fully async, typed interface for programmatically controlling a running Loom instance -- navigating loci, managing tracks and regions of interest, exporting views, and subscribing to browser events.

Installation

pip install loom-client

Or install from source:

git clone https://github.com/riyavsinha/loom-client.git
cd loom-client
pip install -e .

Requires Python >= 3.11.

Quick start

Option 1: Connect to a Loom instance

Use this when your code initiates the WebSocket connection to a running Loom server.

import asyncio
from loom_client import LoomClient

async def main():
    client = LoomClient("ws://localhost:8080")
    await client.connect()

    # Get current browser state
    state = await client.get_browser_state()
    print(state.locus_string)   # e.g. "chr1:1,000-2,000"
    print(state.zoom_level)     # e.g. ZoomLevel.GENE

    # Navigate to a locus
    await client.navigate(locus="chr17:7565097-7590856")  # TP53

    # Zoom out 2x
    await client.navigate(zoom="out", factor=2.0)

    await client.close()

asyncio.run(main())

Option 2: Use an existing WebSocket connection

Use this when the WebSocket is already established — for example, a frontend-initiated connection handled by your backend, or when bridging through a proxy.

from loom_client import LoomClient

async def handle_connection(websocket):
    """Example: called by your WebSocket server when a client connects."""
    client = LoomClient.from_websocket(websocket)
    client.start_listening()

    state = await client.get_browser_state()
    print(state.locus_string)

    await client.close()

API reference

Connection

# Option 1: Initiate a new connection
client = LoomClient(url)   # Create client
await client.connect()     # Open WebSocket + start listener

# Option 2: Reuse an existing WebSocket
client = LoomClient.from_websocket(ws)  # Wrap existing connection
client.start_listening()                # Start listener

await client.close()       # Tear down

Commands

All command methods are async and return typed Pydantic models.

get_browser_state(record=None) -> ProjectedState

Returns the current viewport state including locus, zoom level, loaded tracks, ROIs, and annotations.

state = await client.get_browser_state()
for track in state.tracks:
    print(f"{track.name} ({track.type})")

navigate(locus=None, zoom=None, factor=None) -> bool

Move the viewport to a genomic locus or zoom in/out.

await client.navigate(locus="chr1:1000-2000")
await client.navigate(zoom="in", factor=3.0)

modify_tracks(actions) -> ModifyTracksResult

Add, remove, find, or update tracks in a single batch.

from loom_client import AddTrackAction, RemoveTrackAction, TrackSessionConfig, DataSourceConfig, WireTrackSelector

# Add a BigWig track
result = await client.modify_tracks([
    AddTrackAction(config=TrackSessionConfig(
        type="wig",
        name="H3K27ac",
        data_source=DataSourceConfig(
            type="bigwig",
            url="https://example.com/h3k27ac.bw",
        ),
    ))
])

# Remove by name pattern
result = await client.modify_tracks([
    RemoveTrackAction(selector=WireTrackSelector(name_regex="H3K.*"))
])

query_features(track_id, summarize=None) -> QueryFeaturesResult

Query features for a loaded track.

result = await client.query_features("track-id-123", summarize=True)
print(result.feature_count)

set_layout(tracks, locus=None) -> SetLayoutResult

Replace the entire track layout at once.

result = await client.set_layout(
    tracks=[
        TrackSessionConfig(type="ruler"),
        TrackSessionConfig(type="sequence"),
        TrackSessionConfig(
            type="annotation",
            name="GENCODE",
            data_source=DataSourceConfig(type="gencode", genome="hg38"),
        ),
    ],
    locus="chr1:1000-5000",
)

export_view(format, width=None) -> str | SessionConfig

Export the current view as SVG, PNG (returned as strings), or a session config object.

svg = await client.export_view("svg", width=1200)
session = await client.export_view("session")  # returns SessionConfig

capture(track_ids=None, locus=None, format=None, width=None, include_axis=None) -> CaptureResult

Image of just a fragment — a subset of tracks over a locus — re-rendered off-screen without disturbing the live view. Unlike export_view (the whole browser), this is small enough to hand to a vision model (e.g. inspect the motifs in one dynseq contribution track). png (default) needs a DOM-attached browser; svg works headlessly.

shot = await client.capture(
    track_ids=["dynseq-contrib"],       # omit for all tracks
    locus="chr1:1,000,000-1,000,300",   # omit for the current view
    format="png", width=800,
)
# shot.image → 'data:image/png;base64,…'  ·  shot.width / shot.height / shot.track_ids

manage_rois(action) -> ROI result

Manage regions of interest. The action type determines the operation.

from loom_client import AddROIAction, ROI, ListROIAction, FindROIAtLocusAction

# Add a region of interest
result = await client.manage_rois(AddROIAction(
    roi=ROI(id="roi-1", chr="chr17", start=7565097, end=7590856,
            name="TP53", color="rgba(255,0,0,0.3)"),
    set_name="Genes of interest",
))

# List all ROIs
result = await client.manage_rois(ListROIAction())

# Find ROIs overlapping a locus
result = await client.manage_rois(
    FindROIAtLocusAction(chr="chr17", start=7500000, end=7600000)
)

annotate(action) -> annotation result

Draw and manage the user/agent annotation layer — track-scoped box / point / freehand markup, distinct from ROIs (cross-track highlights). The server generates id/createdAt, so pass an AnnotationInput (only chr/start/end/trackIds are required; kind defaults to box, author to user).

from loom_client import (
    AddAnnotationAction, AnnotationInput, UpdateAnnotationAction,
    ListAnnotationAction, AnnotationFilter,
)

# Agent draws a glowing box to call attention to a region on one track
result = await client.annotate(AddAnnotationAction(
    annotation=AnnotationInput(
        kind="box", chr="chr17", start=7674200, end=7674260,
        track_ids=["h3k27ac"], text="likely active enhancer",
        author="agent", emphasis="glow",   # 'pulse' (size) | 'glow'/'flash' (color)
    ),
))
annotation_id = result["annotation"]["id"]

# Toggle emphasis later (e.g. a history stepper highlighting the current step's mark)
await client.annotate(UpdateAnnotationAction(
    annotation_id=annotation_id, changes={"emphasis": "pulse"}))

# List agent-authored annotations
result = await client.annotate(ListAnnotationAction(
    filter=AnnotationFilter(author="agent")))

# "Circle this area": an ellipse outline reads as a callout, not a BED-like box.
# outline=stroke-only (no fill); corner_radius 0..1 (1 = ellipse). box-only.
await client.annotate(AddAnnotationAction(
    annotation=AnnotationInput(
        kind="box", chr="chr17", start=7674200, end=7674260,
        track_ids=["h3k27ac"], author="agent",
        outline=True, corner_radius=1, text="candidate enhancer",
    ),
))
from loom_client import ResolveFeaturesAnnotationAction

# "What did the user circle?" — expand a freehand/box mark into the concrete
# features its span overlaps within its own tracks: "peaks A, B, C", not just a region.
result = await client.annotate(
    ResolveFeaturesAnnotationAction(annotation_id=annotation_id))
for r in result["resolved"]:
    for f in r["features"]:
        print(f["trackId"], f["chr"], f["start"], f["end"], f.get("name"))
# Omit annotation_id to resolve all (add filter=AnnotationFilter(...) to scope).
# Resolved on-demand against live feature caches — never stale, never stored on the
# annotation. Capped at max_features (default 200); "truncated": true flags a cut list.

Actions: AddAnnotationAction, RemoveAnnotationAction, UpdateAnnotationAction, ClearAnnotationAction, ListAnnotationAction, GetVisibleAnnotationAction, ResolveFeaturesAnnotationAction. A subscribed client receives annotationadded when a human draws, and every get_browser_state includes an annotations list of what's visible.

subscribe_events(events) -> SubscribeEventsResult

Subscribe to server-pushed events. Use ["*"] for all events or [] to unsubscribe.

await client.subscribe_events(["locuschange", "trackadded", "dataloaded"])

Event handling

Register callbacks for browser events:

def on_locus_change(data):
    locus = data["locus"]
    print(f"Moved to {locus['chr']}:{locus['start']}-{locus['end']}")

client.on("locuschange", on_locus_change)

# Wildcard handler receives (event_name, data)
client.on("*", lambda name, data: print(f"Event: {name}"))

# Unregister
client.off("locuschange", on_locus_change)

Available events: locuschange, trackadded, trackremoved, dataloaded, dataerror, rendererror, trackclick, trackhover, trackcontextmenu, trackorderchanged, roiadded, roiremoved, roichanged, roiclick, roicontextmenu

Error handling

Server errors raise CommandException:

from loom_client import CommandException

try:
    await client.navigate(locus="invalid")
except CommandException as e:
    print(e.code, e.message)

Development

pip install -e ".[dev]"
pytest

License

MIT

Metadata

Release files for loom-client 0.0.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for loom-client 0.0.7
File Size Uploaded
loom_client-0.0.7.tar.gz 27.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for loom-client 0.0.7
File Interpreter ABI Platform
loom_client-0.0.7-py3-none-any.whl Python 3 none any Details

Total release size: 43.1 kB

Release files / loom_client-0.0.7.tar.gz

Download URL loom_client-0.0.7.tar.gz
Size 27.4 kB
Tags Source
SHA-256 checksum
How to use checksums
37a0cdf64ff85947962627eecc453feeac9a002ec7c373da381cde1c4b6f1be4
BLAKE2b-256 checksum
How to use checksums
fab0b94d020e0be2fd58a4b042bcfb62ebbb616bfad314b471057d54ebd01070
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.7.7

Release files / loom_client-0.0.7-py3-none-any.whl

Download URL loom_client-0.0.7-py3-none-any.whl
Size 15.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2a28d367746fd43eaff61a55453355b14129c567dfb42e2ce2738bf9ff4e8597
BLAKE2b-256 checksum
How to use checksums
89e33f9ef9536adfc885882fdb7d8ca2bbf5fdfeea63f6b281e6356e6f67e39a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.7.7

Release history Release notifications | RSS feed

This release

0.0.7 This release

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page