loom-client 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loom_client/__init__.py +149 -0
- loom_client/client.py +176 -0
- loom_client/exceptions.py +10 -0
- loom_client/models.py +526 -0
- loom_client-0.0.1.dist-info/METADATA +227 -0
- loom_client-0.0.1.dist-info/RECORD +8 -0
- loom_client-0.0.1.dist-info/WHEEL +5 -0
- loom_client-0.0.1.dist-info/top_level.txt +1 -0
loom_client/__init__.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""loom_client — Python WebSocket client for the Loom Genome Browser."""
|
|
2
|
+
|
|
3
|
+
from .client import LoomClient
|
|
4
|
+
from .exceptions import CommandException
|
|
5
|
+
from .models import (
|
|
6
|
+
AddROIAction,
|
|
7
|
+
AddROIResult,
|
|
8
|
+
AddTrackAction,
|
|
9
|
+
AnnotationFeatureSummary,
|
|
10
|
+
ClearROIAction,
|
|
11
|
+
ClearROIResult,
|
|
12
|
+
CommandArgs,
|
|
13
|
+
DataSourceConfig,
|
|
14
|
+
ErrorDetail,
|
|
15
|
+
ErrorMessage,
|
|
16
|
+
EventMessage,
|
|
17
|
+
ExportViewArgs,
|
|
18
|
+
FindROIAtLocusAction,
|
|
19
|
+
FindROIAtLocusResult,
|
|
20
|
+
FindTrackAction,
|
|
21
|
+
GetBrowserStateArgs,
|
|
22
|
+
GetVisibleROIAction,
|
|
23
|
+
GetVisibleROIResult,
|
|
24
|
+
InteractionFeatureSummary,
|
|
25
|
+
ListROIAction,
|
|
26
|
+
ListROIResult,
|
|
27
|
+
ListROISetsAction,
|
|
28
|
+
ListROISetsResult,
|
|
29
|
+
Locus,
|
|
30
|
+
LocusChangeData,
|
|
31
|
+
ManageROIsArgs,
|
|
32
|
+
ManageROIsResult,
|
|
33
|
+
ModifyTrackAction,
|
|
34
|
+
ModifyTrackActionResult,
|
|
35
|
+
ModifyTracksArgs,
|
|
36
|
+
ModifyTracksResult,
|
|
37
|
+
NavigateArgs,
|
|
38
|
+
OutboundMessage,
|
|
39
|
+
ProjectedState,
|
|
40
|
+
QueryFeaturesArgs,
|
|
41
|
+
QueryFeaturesResult,
|
|
42
|
+
ROI,
|
|
43
|
+
ROIAction,
|
|
44
|
+
ROIEventData,
|
|
45
|
+
ROIInteractionEventData,
|
|
46
|
+
ROISetConfig,
|
|
47
|
+
RemoveROIAction,
|
|
48
|
+
RemoveROIResult,
|
|
49
|
+
RemoveTrackAction,
|
|
50
|
+
RequestMessage,
|
|
51
|
+
ResponseMessage,
|
|
52
|
+
SessionConfig,
|
|
53
|
+
SetLayoutArgs,
|
|
54
|
+
SetLayoutResult,
|
|
55
|
+
SignalRange,
|
|
56
|
+
SingleTrackEventData,
|
|
57
|
+
SubscribeEventsArgs,
|
|
58
|
+
SubscribeEventsResult,
|
|
59
|
+
TrackErrorData,
|
|
60
|
+
TrackFeatureSummary,
|
|
61
|
+
TrackInteractionData,
|
|
62
|
+
TrackOrderChangedData,
|
|
63
|
+
TrackSessionConfig,
|
|
64
|
+
TrackSummary,
|
|
65
|
+
UpdateMetadataAction,
|
|
66
|
+
UpdateROIAction,
|
|
67
|
+
UpdateROIResult,
|
|
68
|
+
WigFeatureSummary,
|
|
69
|
+
WireTrackSelector,
|
|
70
|
+
ZoomLevel,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"LoomClient",
|
|
75
|
+
"CommandException",
|
|
76
|
+
# Wire messages
|
|
77
|
+
"RequestMessage",
|
|
78
|
+
"ResponseMessage",
|
|
79
|
+
"ErrorDetail",
|
|
80
|
+
"ErrorMessage",
|
|
81
|
+
"EventMessage",
|
|
82
|
+
"OutboundMessage",
|
|
83
|
+
# Core genomic types
|
|
84
|
+
"Locus",
|
|
85
|
+
"ROI",
|
|
86
|
+
"ROISetConfig",
|
|
87
|
+
# Track session configs
|
|
88
|
+
"DataSourceConfig",
|
|
89
|
+
"TrackSessionConfig",
|
|
90
|
+
"SessionConfig",
|
|
91
|
+
# State projection
|
|
92
|
+
"ZoomLevel",
|
|
93
|
+
"SignalRange",
|
|
94
|
+
"WigFeatureSummary",
|
|
95
|
+
"AnnotationFeatureSummary",
|
|
96
|
+
"InteractionFeatureSummary",
|
|
97
|
+
"TrackFeatureSummary",
|
|
98
|
+
"TrackSummary",
|
|
99
|
+
"ProjectedState",
|
|
100
|
+
# Track selector
|
|
101
|
+
"WireTrackSelector",
|
|
102
|
+
# Command args
|
|
103
|
+
"CommandArgs",
|
|
104
|
+
"GetBrowserStateArgs",
|
|
105
|
+
"NavigateArgs",
|
|
106
|
+
"AddTrackAction",
|
|
107
|
+
"RemoveTrackAction",
|
|
108
|
+
"FindTrackAction",
|
|
109
|
+
"UpdateMetadataAction",
|
|
110
|
+
"ModifyTrackAction",
|
|
111
|
+
"ModifyTracksArgs",
|
|
112
|
+
"QueryFeaturesArgs",
|
|
113
|
+
"SetLayoutArgs",
|
|
114
|
+
"ExportViewArgs",
|
|
115
|
+
"AddROIAction",
|
|
116
|
+
"RemoveROIAction",
|
|
117
|
+
"UpdateROIAction",
|
|
118
|
+
"ClearROIAction",
|
|
119
|
+
"ListROIAction",
|
|
120
|
+
"ListROISetsAction",
|
|
121
|
+
"FindROIAtLocusAction",
|
|
122
|
+
"GetVisibleROIAction",
|
|
123
|
+
"ROIAction",
|
|
124
|
+
"ManageROIsArgs",
|
|
125
|
+
"SubscribeEventsArgs",
|
|
126
|
+
# Command results
|
|
127
|
+
"ModifyTrackActionResult",
|
|
128
|
+
"ModifyTracksResult",
|
|
129
|
+
"QueryFeaturesResult",
|
|
130
|
+
"SetLayoutResult",
|
|
131
|
+
"AddROIResult",
|
|
132
|
+
"RemoveROIResult",
|
|
133
|
+
"UpdateROIResult",
|
|
134
|
+
"ClearROIResult",
|
|
135
|
+
"ListROIResult",
|
|
136
|
+
"ListROISetsResult",
|
|
137
|
+
"FindROIAtLocusResult",
|
|
138
|
+
"GetVisibleROIResult",
|
|
139
|
+
"ManageROIsResult",
|
|
140
|
+
"SubscribeEventsResult",
|
|
141
|
+
# Event data
|
|
142
|
+
"LocusChangeData",
|
|
143
|
+
"SingleTrackEventData",
|
|
144
|
+
"TrackOrderChangedData",
|
|
145
|
+
"TrackErrorData",
|
|
146
|
+
"TrackInteractionData",
|
|
147
|
+
"ROIEventData",
|
|
148
|
+
"ROIInteractionEventData",
|
|
149
|
+
]
|
loom_client/client.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
import websockets
|
|
9
|
+
|
|
10
|
+
from .exceptions import CommandException
|
|
11
|
+
from .models import (
|
|
12
|
+
AddTrackAction,
|
|
13
|
+
CommandArgs,
|
|
14
|
+
ErrorDetail,
|
|
15
|
+
ExportViewArgs,
|
|
16
|
+
FindTrackAction,
|
|
17
|
+
GetBrowserStateArgs,
|
|
18
|
+
ManageROIsArgs,
|
|
19
|
+
ModifyTrackAction,
|
|
20
|
+
ModifyTracksArgs,
|
|
21
|
+
ModifyTracksResult,
|
|
22
|
+
NavigateArgs,
|
|
23
|
+
ProjectedState,
|
|
24
|
+
QueryFeaturesArgs,
|
|
25
|
+
QueryFeaturesResult,
|
|
26
|
+
ROIAction,
|
|
27
|
+
RequestMessage,
|
|
28
|
+
SessionConfig,
|
|
29
|
+
SetLayoutArgs,
|
|
30
|
+
SetLayoutResult,
|
|
31
|
+
SubscribeEventsArgs,
|
|
32
|
+
SubscribeEventsResult,
|
|
33
|
+
TrackSessionConfig,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LoomClient:
|
|
38
|
+
"""Async WebSocket client for the Loom Genome Browser."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, url: str) -> None:
|
|
41
|
+
self._url = url
|
|
42
|
+
self._ws = None
|
|
43
|
+
self._pending: dict[str, asyncio.Future] = {}
|
|
44
|
+
self._event_handlers: dict[str, list[Callable]] = {}
|
|
45
|
+
self._listener_task: asyncio.Task | None = None
|
|
46
|
+
|
|
47
|
+
# ── Connection management ─────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
async def connect(self) -> None:
|
|
50
|
+
"""Open the WebSocket connection and start the background listener."""
|
|
51
|
+
self._ws = await websockets.connect(self._url)
|
|
52
|
+
self._listener_task = asyncio.create_task(self._listen())
|
|
53
|
+
|
|
54
|
+
async def close(self) -> None:
|
|
55
|
+
"""Cancel the listener and close the WebSocket connection."""
|
|
56
|
+
if self._listener_task:
|
|
57
|
+
self._listener_task.cancel()
|
|
58
|
+
if self._ws:
|
|
59
|
+
await self._ws.close()
|
|
60
|
+
|
|
61
|
+
# ── Message routing ───────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
async def _listen(self) -> None:
|
|
64
|
+
async for raw in self._ws:
|
|
65
|
+
msg = json.loads(raw)
|
|
66
|
+
|
|
67
|
+
if "event" in msg:
|
|
68
|
+
# Unsolicited server-pushed event
|
|
69
|
+
event_name = msg["event"]
|
|
70
|
+
for handler in self._event_handlers.get(event_name, []):
|
|
71
|
+
handler(msg["data"])
|
|
72
|
+
for handler in self._event_handlers.get("*", []):
|
|
73
|
+
handler(event_name, msg["data"])
|
|
74
|
+
|
|
75
|
+
elif "id" in msg:
|
|
76
|
+
future = self._pending.pop(msg["id"], None)
|
|
77
|
+
if future is None:
|
|
78
|
+
continue
|
|
79
|
+
if "error" in msg:
|
|
80
|
+
err = ErrorDetail.model_validate(msg["error"])
|
|
81
|
+
future.set_exception(CommandException(err.code, err.message))
|
|
82
|
+
else:
|
|
83
|
+
future.set_result(msg["result"])
|
|
84
|
+
|
|
85
|
+
# ── Sending commands ──────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
async def send(self, args: CommandArgs) -> Any:
|
|
88
|
+
"""Send a command and await the correlated response."""
|
|
89
|
+
req = RequestMessage(command=args.command)
|
|
90
|
+
payload = args.model_dump(by_alias=True, exclude_none=True)
|
|
91
|
+
payload.pop("command")
|
|
92
|
+
if payload:
|
|
93
|
+
req.args = payload
|
|
94
|
+
|
|
95
|
+
loop = asyncio.get_event_loop()
|
|
96
|
+
future: asyncio.Future = loop.create_future()
|
|
97
|
+
self._pending[req.id] = future
|
|
98
|
+
await self._ws.send(req.model_dump_json())
|
|
99
|
+
return await future
|
|
100
|
+
|
|
101
|
+
# ── Typed command methods ─────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
async def get_browser_state(self, *, record: str | None = None) -> ProjectedState:
|
|
104
|
+
"""Return the current projected browser state."""
|
|
105
|
+
result = await self.send(GetBrowserStateArgs(record=record))
|
|
106
|
+
return ProjectedState.model_validate(result)
|
|
107
|
+
|
|
108
|
+
async def navigate(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
locus: str | None = None,
|
|
112
|
+
zoom: Literal["in", "out"] | None = None,
|
|
113
|
+
factor: float | None = None,
|
|
114
|
+
) -> bool:
|
|
115
|
+
"""Navigate the viewport to a locus or zoom in/out."""
|
|
116
|
+
return await self.send(NavigateArgs(locus=locus, zoom=zoom, factor=factor))
|
|
117
|
+
|
|
118
|
+
async def modify_tracks(
|
|
119
|
+
self, actions: list[ModifyTrackAction]
|
|
120
|
+
) -> ModifyTracksResult:
|
|
121
|
+
"""Apply one or more track modification actions."""
|
|
122
|
+
result = await self.send(ModifyTracksArgs(actions=actions))
|
|
123
|
+
return ModifyTracksResult.model_validate(result)
|
|
124
|
+
|
|
125
|
+
async def query_features(
|
|
126
|
+
self, track_id: str, *, summarize: bool | None = None
|
|
127
|
+
) -> QueryFeaturesResult:
|
|
128
|
+
"""Query features for a specific track."""
|
|
129
|
+
result = await self.send(
|
|
130
|
+
QueryFeaturesArgs(trackId=track_id, summarize=summarize)
|
|
131
|
+
)
|
|
132
|
+
return QueryFeaturesResult.model_validate(result)
|
|
133
|
+
|
|
134
|
+
async def set_layout(
|
|
135
|
+
self,
|
|
136
|
+
tracks: list[TrackSessionConfig],
|
|
137
|
+
*,
|
|
138
|
+
locus: str | None = None,
|
|
139
|
+
) -> SetLayoutResult:
|
|
140
|
+
"""Replace the current track layout."""
|
|
141
|
+
result = await self.send(SetLayoutArgs(tracks=tracks, locus=locus))
|
|
142
|
+
return SetLayoutResult.model_validate(result)
|
|
143
|
+
|
|
144
|
+
async def export_view(
|
|
145
|
+
self,
|
|
146
|
+
format: Literal["svg", "png", "session"],
|
|
147
|
+
*,
|
|
148
|
+
width: int | None = None,
|
|
149
|
+
) -> str | SessionConfig:
|
|
150
|
+
"""Export the current view as SVG, PNG, or a session config."""
|
|
151
|
+
result = await self.send(ExportViewArgs(format=format, width=width))
|
|
152
|
+
if format == "session":
|
|
153
|
+
return SessionConfig.model_validate(result)
|
|
154
|
+
return result # SVG/PNG as string
|
|
155
|
+
|
|
156
|
+
async def manage_rois(self, action: ROIAction) -> Any:
|
|
157
|
+
"""Perform a ROI management action."""
|
|
158
|
+
result = await self.send(ManageROIsArgs(action=action))
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
async def subscribe_events(self, events: list[str]) -> SubscribeEventsResult:
|
|
162
|
+
"""Subscribe to browser events. Use ``["*"]`` for all, ``[]`` to unsubscribe."""
|
|
163
|
+
result = await self.send(SubscribeEventsArgs(events=events))
|
|
164
|
+
return SubscribeEventsResult.model_validate(result)
|
|
165
|
+
|
|
166
|
+
# ── Event subscription ────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
def on(self, event: str, handler: Callable) -> None:
|
|
169
|
+
"""Register an event handler. Use ``'*'`` for all events."""
|
|
170
|
+
self._event_handlers.setdefault(event, []).append(handler)
|
|
171
|
+
|
|
172
|
+
def off(self, event: str, handler: Callable) -> None:
|
|
173
|
+
"""Unregister a previously registered event handler."""
|
|
174
|
+
handlers = self._event_handlers.get(event, [])
|
|
175
|
+
if handler in handlers:
|
|
176
|
+
handlers.remove(handler)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CommandException(Exception):
|
|
5
|
+
"""Raised when the server returns an error response."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, code: str, message: str) -> None:
|
|
8
|
+
self.code = code
|
|
9
|
+
self.message = message
|
|
10
|
+
super().__init__(f"[{code}] {message}")
|
loom_client/models.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Annotated, Any, Literal, Union
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ── Wire messages ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
class RequestMessage(BaseModel):
|
|
13
|
+
"""Client → Server request."""
|
|
14
|
+
|
|
15
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
16
|
+
command: str
|
|
17
|
+
args: dict[str, Any] | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ResponseMessage(BaseModel):
|
|
21
|
+
"""Server → Client success response."""
|
|
22
|
+
|
|
23
|
+
id: str
|
|
24
|
+
result: Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ErrorDetail(BaseModel):
|
|
28
|
+
code: str
|
|
29
|
+
message: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ErrorMessage(BaseModel):
|
|
33
|
+
"""Server → Client error response."""
|
|
34
|
+
|
|
35
|
+
id: str
|
|
36
|
+
error: ErrorDetail
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EventMessage(BaseModel):
|
|
40
|
+
"""Server → Client unsolicited event."""
|
|
41
|
+
|
|
42
|
+
event: str
|
|
43
|
+
data: Any
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Discriminated parsing of inbound server messages
|
|
47
|
+
OutboundMessage = Annotated[
|
|
48
|
+
Union[ResponseMessage, ErrorMessage, EventMessage],
|
|
49
|
+
Field(discriminator=None), # manually discriminate on field presence
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── Core genomic types ────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
class Locus(BaseModel):
|
|
56
|
+
chr: str
|
|
57
|
+
start: int
|
|
58
|
+
end: int
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ROI(BaseModel):
|
|
62
|
+
"""Region of Interest."""
|
|
63
|
+
|
|
64
|
+
id: str
|
|
65
|
+
chr: str
|
|
66
|
+
start: int # 0-based
|
|
67
|
+
end: int
|
|
68
|
+
name: str | None = None
|
|
69
|
+
description: str | None = None
|
|
70
|
+
color: str | None = None # CSS color with alpha
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ROISetConfig(BaseModel):
|
|
74
|
+
name: str
|
|
75
|
+
color: str | None = None
|
|
76
|
+
is_user_defined: bool | None = Field(None, alias="isUserDefined")
|
|
77
|
+
features: list[ROI]
|
|
78
|
+
|
|
79
|
+
model_config = {"populate_by_name": True}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Track session configs ─────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
class DataSourceConfig(BaseModel):
|
|
85
|
+
"""Discriminated union — use the `type` field."""
|
|
86
|
+
|
|
87
|
+
type: str
|
|
88
|
+
url: str | None = None
|
|
89
|
+
genome: str | None = None
|
|
90
|
+
track: str | None = None
|
|
91
|
+
experiment_id: str | None = Field(None, alias="experimentId")
|
|
92
|
+
window_function: str | None = Field(None, alias="windowFunction")
|
|
93
|
+
format: str | None = None
|
|
94
|
+
index_url: str | None = Field(None, alias="indexURL")
|
|
95
|
+
indexed: bool | None = None
|
|
96
|
+
|
|
97
|
+
model_config = {"populate_by_name": True}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TrackSessionConfig(BaseModel):
|
|
101
|
+
"""
|
|
102
|
+
Discriminated union on ``type``. Valid types:
|
|
103
|
+
``'wig'``, ``'annotation'``, ``'ruler'``, ``'sequence'``, ``'interact'``
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
type: str
|
|
107
|
+
id: str | None = None
|
|
108
|
+
name: str | None = None
|
|
109
|
+
order: int | None = None
|
|
110
|
+
metadata: dict[str, str] | None = None
|
|
111
|
+
data_source: DataSourceConfig | None = Field(None, alias="dataSource")
|
|
112
|
+
config: dict[str, Any] | None = None
|
|
113
|
+
|
|
114
|
+
model_config = {"populate_by_name": True}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SessionConfig(BaseModel):
|
|
118
|
+
version: str
|
|
119
|
+
locus: Locus
|
|
120
|
+
viewport_width: int | None = Field(None, alias="viewportWidth")
|
|
121
|
+
theme: dict[str, Any] | None = None
|
|
122
|
+
genome: dict[str, Any] | None = None
|
|
123
|
+
tracks: list[TrackSessionConfig]
|
|
124
|
+
rois: list[ROISetConfig] | None = None
|
|
125
|
+
|
|
126
|
+
model_config = {"populate_by_name": True}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ── State projection (get_browser_state response) ─────────────────────────────
|
|
130
|
+
|
|
131
|
+
class ZoomLevel(str, Enum):
|
|
132
|
+
BASE = "base"
|
|
133
|
+
ELEMENT = "element"
|
|
134
|
+
GENE = "gene"
|
|
135
|
+
REGION = "region"
|
|
136
|
+
CHROMOSOME = "chromosome"
|
|
137
|
+
GENOME = "genome"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class SignalRange(BaseModel):
|
|
141
|
+
min: float
|
|
142
|
+
max: float
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class WigFeatureSummary(BaseModel):
|
|
146
|
+
feature_count: int = Field(alias="featureCount")
|
|
147
|
+
signal_range: SignalRange = Field(alias="signalRange")
|
|
148
|
+
|
|
149
|
+
model_config = {"populate_by_name": True}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class AnnotationFeatureSummary(BaseModel):
|
|
153
|
+
feature_count: int = Field(alias="featureCount")
|
|
154
|
+
feature_names: list[str] = Field(alias="featureNames")
|
|
155
|
+
packing_depth: int = Field(alias="packingDepth")
|
|
156
|
+
|
|
157
|
+
model_config = {"populate_by_name": True}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class InteractionFeatureSummary(BaseModel):
|
|
161
|
+
feature_count: int = Field(alias="featureCount")
|
|
162
|
+
value_range: SignalRange = Field(alias="valueRange")
|
|
163
|
+
|
|
164
|
+
model_config = {"populate_by_name": True}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
TrackFeatureSummary = WigFeatureSummary | AnnotationFeatureSummary | InteractionFeatureSummary
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TrackSummary(BaseModel):
|
|
171
|
+
id: str
|
|
172
|
+
type: str
|
|
173
|
+
name: str | None = None
|
|
174
|
+
metadata: dict[str, str] | None = None
|
|
175
|
+
loading: bool | None = None
|
|
176
|
+
feature_summary: dict[str, Any] | None = Field(None, alias="featureSummary")
|
|
177
|
+
|
|
178
|
+
model_config = {"populate_by_name": True}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ProjectedState(BaseModel):
|
|
182
|
+
locus: Locus
|
|
183
|
+
locus_string: str = Field(alias="locusString")
|
|
184
|
+
span: int
|
|
185
|
+
zoom_level: ZoomLevel = Field(alias="zoomLevel")
|
|
186
|
+
genome: str | None = None
|
|
187
|
+
tracks: list[TrackSummary]
|
|
188
|
+
rois: list[ROISetConfig]
|
|
189
|
+
|
|
190
|
+
model_config = {"populate_by_name": True}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ── Track selector ────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
class WireTrackSelector(BaseModel):
|
|
196
|
+
"""
|
|
197
|
+
JSON-safe track selector. Fields are AND-combined.
|
|
198
|
+
|
|
199
|
+
- id: exact match on track ID
|
|
200
|
+
- name: case-insensitive substring match
|
|
201
|
+
- name_regex: regex pattern (takes precedence over name), matched case-insensitive
|
|
202
|
+
- type: exact match on track type (``'wig'``, ``'annotation'``, etc.)
|
|
203
|
+
- where: AND semantics on metadata key-value pairs
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
id: str | None = None
|
|
207
|
+
name: str | None = None
|
|
208
|
+
name_regex: str | None = Field(None, alias="nameRegex")
|
|
209
|
+
type: str | None = None
|
|
210
|
+
where: dict[str, str] | None = None
|
|
211
|
+
|
|
212
|
+
model_config = {"populate_by_name": True}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ── Command argument models ───────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
class GetBrowserStateArgs(BaseModel):
|
|
218
|
+
command: Literal["get_browser_state"] = "get_browser_state"
|
|
219
|
+
record: str | None = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class NavigateArgs(BaseModel):
|
|
223
|
+
command: Literal["navigate"] = "navigate"
|
|
224
|
+
locus: str | None = None
|
|
225
|
+
zoom: Literal["in", "out"] | None = None
|
|
226
|
+
factor: float | None = None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ── modify_tracks ─────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
class AddTrackAction(BaseModel):
|
|
232
|
+
action: Literal["add"] = "add"
|
|
233
|
+
config: TrackSessionConfig
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class RemoveTrackAction(BaseModel):
|
|
237
|
+
action: Literal["remove"] = "remove"
|
|
238
|
+
selector: WireTrackSelector
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class FindTrackAction(BaseModel):
|
|
242
|
+
action: Literal["find"] = "find"
|
|
243
|
+
selector: WireTrackSelector
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class UpdateMetadataAction(BaseModel):
|
|
247
|
+
action: Literal["update_metadata"] = "update_metadata"
|
|
248
|
+
selector: WireTrackSelector
|
|
249
|
+
metadata: dict[str, str]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
ModifyTrackAction = AddTrackAction | RemoveTrackAction | FindTrackAction | UpdateMetadataAction
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ModifyTracksArgs(BaseModel):
|
|
256
|
+
command: Literal["modify_tracks"] = "modify_tracks"
|
|
257
|
+
actions: list[ModifyTrackAction]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ── query_features ────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
class QueryFeaturesArgs(BaseModel):
|
|
263
|
+
command: Literal["query_features"] = "query_features"
|
|
264
|
+
track_id: str = Field(alias="trackId")
|
|
265
|
+
summarize: bool | None = None
|
|
266
|
+
|
|
267
|
+
model_config = {"populate_by_name": True}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ── set_layout ────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
class SetLayoutArgs(BaseModel):
|
|
273
|
+
command: Literal["set_layout"] = "set_layout"
|
|
274
|
+
tracks: list[TrackSessionConfig]
|
|
275
|
+
locus: str | None = None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ── export_view ───────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
class ExportViewArgs(BaseModel):
|
|
281
|
+
command: Literal["export_view"] = "export_view"
|
|
282
|
+
format: Literal["svg", "png", "session"]
|
|
283
|
+
width: int | None = None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ── manage_rois ───────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
class AddROIAction(BaseModel):
|
|
289
|
+
action: Literal["add"] = "add"
|
|
290
|
+
roi: ROI
|
|
291
|
+
set_name: str | None = Field(None, alias="setName")
|
|
292
|
+
|
|
293
|
+
model_config = {"populate_by_name": True}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class RemoveROIAction(BaseModel):
|
|
297
|
+
action: Literal["remove"] = "remove"
|
|
298
|
+
roi_id: str = Field(alias="roiId")
|
|
299
|
+
|
|
300
|
+
model_config = {"populate_by_name": True}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class UpdateROIAction(BaseModel):
|
|
304
|
+
action: Literal["update"] = "update"
|
|
305
|
+
roi_id: str = Field(alias="roiId")
|
|
306
|
+
changes: dict[str, Any]
|
|
307
|
+
|
|
308
|
+
model_config = {"populate_by_name": True}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class ClearROIAction(BaseModel):
|
|
312
|
+
action: Literal["clear"] = "clear"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ListROIAction(BaseModel):
|
|
316
|
+
action: Literal["list"] = "list"
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ListROISetsAction(BaseModel):
|
|
320
|
+
action: Literal["list_sets"] = "list_sets"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class FindROIAtLocusAction(BaseModel):
|
|
324
|
+
action: Literal["find_at_locus"] = "find_at_locus"
|
|
325
|
+
chr: str
|
|
326
|
+
start: int
|
|
327
|
+
end: int
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class GetVisibleROIAction(BaseModel):
|
|
331
|
+
action: Literal["get_visible"] = "get_visible"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
ROIAction = (
|
|
335
|
+
AddROIAction
|
|
336
|
+
| RemoveROIAction
|
|
337
|
+
| UpdateROIAction
|
|
338
|
+
| ClearROIAction
|
|
339
|
+
| ListROIAction
|
|
340
|
+
| ListROISetsAction
|
|
341
|
+
| FindROIAtLocusAction
|
|
342
|
+
| GetVisibleROIAction
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class ManageROIsArgs(BaseModel):
|
|
347
|
+
command: Literal["manage_rois"] = "manage_rois"
|
|
348
|
+
action: ROIAction
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ── subscribe_events ──────────────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
class SubscribeEventsArgs(BaseModel):
|
|
354
|
+
"""Subscribe to browser events. Use ``["*"]`` for all events, ``[]`` to unsubscribe."""
|
|
355
|
+
|
|
356
|
+
command: Literal["subscribe_events"] = "subscribe_events"
|
|
357
|
+
events: list[str]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ── Union of all commands ─────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
CommandArgs = (
|
|
363
|
+
GetBrowserStateArgs
|
|
364
|
+
| NavigateArgs
|
|
365
|
+
| ModifyTracksArgs
|
|
366
|
+
| QueryFeaturesArgs
|
|
367
|
+
| SetLayoutArgs
|
|
368
|
+
| ExportViewArgs
|
|
369
|
+
| ManageROIsArgs
|
|
370
|
+
| SubscribeEventsArgs
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ── Command result models ─────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
class ModifyTrackActionResult(BaseModel):
|
|
377
|
+
action: str
|
|
378
|
+
success: bool
|
|
379
|
+
track_id: str | None = Field(None, alias="trackId")
|
|
380
|
+
tracks: list[TrackSummary] | None = None
|
|
381
|
+
count: int | None = None
|
|
382
|
+
error: str | None = None
|
|
383
|
+
|
|
384
|
+
model_config = {"populate_by_name": True}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class ModifyTracksResult(BaseModel):
|
|
388
|
+
results: list[ModifyTrackActionResult]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class QueryFeaturesResult(BaseModel):
|
|
392
|
+
track_id: str = Field(alias="trackId")
|
|
393
|
+
feature_count: int = Field(alias="featureCount")
|
|
394
|
+
features: list[Any] | None = None
|
|
395
|
+
summary: Any | None = None
|
|
396
|
+
|
|
397
|
+
model_config = {"populate_by_name": True}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
class SetLayoutResult(BaseModel):
|
|
401
|
+
added: list[str]
|
|
402
|
+
removed: list[str]
|
|
403
|
+
unchanged: list[str]
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# ManageROIs results — discriminated on ``action``
|
|
407
|
+
|
|
408
|
+
class AddROIResult(BaseModel):
|
|
409
|
+
action: Literal["add"] = "add"
|
|
410
|
+
roi: ROI
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class RemoveROIResult(BaseModel):
|
|
414
|
+
action: Literal["remove"] = "remove"
|
|
415
|
+
success: bool
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class UpdateROIResult(BaseModel):
|
|
419
|
+
action: Literal["update"] = "update"
|
|
420
|
+
roi: ROI | None = None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class ClearROIResult(BaseModel):
|
|
424
|
+
action: Literal["clear"] = "clear"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class ListROIResult(BaseModel):
|
|
428
|
+
action: Literal["list"] = "list"
|
|
429
|
+
rois: list[ROI]
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class ListROISetsResult(BaseModel):
|
|
433
|
+
action: Literal["list_sets"] = "list_sets"
|
|
434
|
+
sets: list[ROISetConfig]
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class FindROIAtLocusResult(BaseModel):
|
|
438
|
+
action: Literal["find_at_locus"] = "find_at_locus"
|
|
439
|
+
rois: list[ROI]
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class GetVisibleROIResult(BaseModel):
|
|
443
|
+
action: Literal["get_visible"] = "get_visible"
|
|
444
|
+
rois: list[ROI]
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
ManageROIsResult = (
|
|
448
|
+
AddROIResult
|
|
449
|
+
| RemoveROIResult
|
|
450
|
+
| UpdateROIResult
|
|
451
|
+
| ClearROIResult
|
|
452
|
+
| ListROIResult
|
|
453
|
+
| ListROISetsResult
|
|
454
|
+
| FindROIAtLocusResult
|
|
455
|
+
| GetVisibleROIResult
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class SubscribeEventsResult(BaseModel):
|
|
460
|
+
subscribed: list[str]
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# ── Event data models ─────────────────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
class LocusChangeData(BaseModel):
|
|
466
|
+
locus: Locus
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class SingleTrackEventData(BaseModel):
|
|
470
|
+
"""Used for: trackadded, trackremoved, dataloaded"""
|
|
471
|
+
|
|
472
|
+
track_id: str = Field(alias="trackId")
|
|
473
|
+
track_type: str = Field(alias="trackType")
|
|
474
|
+
track_name: str | None = Field(None, alias="trackName")
|
|
475
|
+
|
|
476
|
+
model_config = {"populate_by_name": True}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class TrackOrderChangedData(BaseModel):
|
|
480
|
+
track_ids: list[str] = Field(alias="trackIds")
|
|
481
|
+
|
|
482
|
+
model_config = {"populate_by_name": True}
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class TrackErrorData(BaseModel):
|
|
486
|
+
"""Used for: dataerror, rendererror"""
|
|
487
|
+
|
|
488
|
+
track_id: str = Field(alias="trackId")
|
|
489
|
+
track_type: str = Field(alias="trackType")
|
|
490
|
+
track_name: str | None = Field(None, alias="trackName")
|
|
491
|
+
error: str
|
|
492
|
+
|
|
493
|
+
model_config = {"populate_by_name": True}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class TrackInteractionData(BaseModel):
|
|
497
|
+
"""Used for: trackclick, trackhover, trackcontextmenu"""
|
|
498
|
+
|
|
499
|
+
track_id: str = Field(alias="trackId")
|
|
500
|
+
track_type: str = Field(alias="trackType")
|
|
501
|
+
track_name: str | None = Field(None, alias="trackName")
|
|
502
|
+
genomic_location: int = Field(alias="genomicLocation")
|
|
503
|
+
feature_count: int = Field(alias="featureCount")
|
|
504
|
+
|
|
505
|
+
model_config = {"populate_by_name": True}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class ROIEventData(BaseModel):
|
|
509
|
+
"""Used for: roiadded, roiremoved, roichanged"""
|
|
510
|
+
|
|
511
|
+
roi: ROI
|
|
512
|
+
set_name: str | None = Field(None, alias="setName")
|
|
513
|
+
changes: dict[str, Any] | None = None
|
|
514
|
+
|
|
515
|
+
model_config = {"populate_by_name": True}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class ROIInteractionEventData(BaseModel):
|
|
519
|
+
"""Used for: roiclick, roicontextmenu"""
|
|
520
|
+
|
|
521
|
+
roi: ROI
|
|
522
|
+
genomic_location: int = Field(alias="genomicLocation")
|
|
523
|
+
x: float
|
|
524
|
+
y: float
|
|
525
|
+
|
|
526
|
+
model_config = {"populate_by_name": True}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: loom-client
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python WebSocket client for the Loom Genome Browser
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pydantic>=2.0
|
|
8
|
+
Requires-Dist: websockets>=12.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# loom-client
|
|
14
|
+
|
|
15
|
+
Python WebSocket client for the [Loom Genome Browser](https://github.com/nicholasrq/loom). 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.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install loom-client
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or install from source:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git clone https://github.com/riyavsinha/loom-client.git
|
|
27
|
+
cd loom-client
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Python >= 3.11.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
from loom_client import LoomClient
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
client = LoomClient("ws://localhost:8080")
|
|
41
|
+
await client.connect()
|
|
42
|
+
|
|
43
|
+
# Get current browser state
|
|
44
|
+
state = await client.get_browser_state()
|
|
45
|
+
print(state.locus_string) # e.g. "chr1:1,000-2,000"
|
|
46
|
+
print(state.zoom_level) # e.g. ZoomLevel.GENE
|
|
47
|
+
|
|
48
|
+
# Navigate to a locus
|
|
49
|
+
await client.navigate(locus="chr17:7565097-7590856") # TP53
|
|
50
|
+
|
|
51
|
+
# Zoom out 2x
|
|
52
|
+
await client.navigate(zoom="out", factor=2.0)
|
|
53
|
+
|
|
54
|
+
await client.close()
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API reference
|
|
60
|
+
|
|
61
|
+
### Connection
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
client = LoomClient(url) # Create client
|
|
65
|
+
await client.connect() # Open WebSocket + start listener
|
|
66
|
+
await client.close() # Tear down
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Commands
|
|
70
|
+
|
|
71
|
+
All command methods are async and return typed Pydantic models.
|
|
72
|
+
|
|
73
|
+
#### `get_browser_state(record=None) -> ProjectedState`
|
|
74
|
+
|
|
75
|
+
Returns the current viewport state including locus, zoom level, loaded tracks, and ROIs.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
state = await client.get_browser_state()
|
|
79
|
+
for track in state.tracks:
|
|
80
|
+
print(f"{track.name} ({track.type})")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `navigate(locus=None, zoom=None, factor=None) -> bool`
|
|
84
|
+
|
|
85
|
+
Move the viewport to a genomic locus or zoom in/out.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
await client.navigate(locus="chr1:1000-2000")
|
|
89
|
+
await client.navigate(zoom="in", factor=3.0)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### `modify_tracks(actions) -> ModifyTracksResult`
|
|
93
|
+
|
|
94
|
+
Add, remove, find, or update tracks in a single batch.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from loom_client import AddTrackAction, RemoveTrackAction, TrackSessionConfig, DataSourceConfig, WireTrackSelector
|
|
98
|
+
|
|
99
|
+
# Add a BigWig track
|
|
100
|
+
result = await client.modify_tracks([
|
|
101
|
+
AddTrackAction(config=TrackSessionConfig(
|
|
102
|
+
type="wig",
|
|
103
|
+
name="H3K27ac",
|
|
104
|
+
data_source=DataSourceConfig(
|
|
105
|
+
type="bigwig",
|
|
106
|
+
url="https://example.com/h3k27ac.bw",
|
|
107
|
+
),
|
|
108
|
+
))
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
# Remove by name pattern
|
|
112
|
+
result = await client.modify_tracks([
|
|
113
|
+
RemoveTrackAction(selector=WireTrackSelector(name_regex="H3K.*"))
|
|
114
|
+
])
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### `query_features(track_id, summarize=None) -> QueryFeaturesResult`
|
|
118
|
+
|
|
119
|
+
Query features for a loaded track.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
result = await client.query_features("track-id-123", summarize=True)
|
|
123
|
+
print(result.feature_count)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### `set_layout(tracks, locus=None) -> SetLayoutResult`
|
|
127
|
+
|
|
128
|
+
Replace the entire track layout at once.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
result = await client.set_layout(
|
|
132
|
+
tracks=[
|
|
133
|
+
TrackSessionConfig(type="ruler"),
|
|
134
|
+
TrackSessionConfig(type="sequence"),
|
|
135
|
+
TrackSessionConfig(
|
|
136
|
+
type="annotation",
|
|
137
|
+
name="GENCODE",
|
|
138
|
+
data_source=DataSourceConfig(type="gencode", genome="hg38"),
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
locus="chr1:1000-5000",
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### `export_view(format, width=None) -> str | SessionConfig`
|
|
146
|
+
|
|
147
|
+
Export the current view as SVG, PNG (returned as strings), or a session config object.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
svg = await client.export_view("svg", width=1200)
|
|
151
|
+
session = await client.export_view("session") # returns SessionConfig
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### `manage_rois(action) -> ROI result`
|
|
155
|
+
|
|
156
|
+
Manage regions of interest. The action type determines the operation.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from loom_client import AddROIAction, ROI, ListROIAction, FindROIAtLocusAction
|
|
160
|
+
|
|
161
|
+
# Add a region of interest
|
|
162
|
+
result = await client.manage_rois(AddROIAction(
|
|
163
|
+
roi=ROI(id="roi-1", chr="chr17", start=7565097, end=7590856,
|
|
164
|
+
name="TP53", color="rgba(255,0,0,0.3)"),
|
|
165
|
+
set_name="Genes of interest",
|
|
166
|
+
))
|
|
167
|
+
|
|
168
|
+
# List all ROIs
|
|
169
|
+
result = await client.manage_rois(ListROIAction())
|
|
170
|
+
|
|
171
|
+
# Find ROIs overlapping a locus
|
|
172
|
+
result = await client.manage_rois(
|
|
173
|
+
FindROIAtLocusAction(chr="chr17", start=7500000, end=7600000)
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### `subscribe_events(events) -> SubscribeEventsResult`
|
|
178
|
+
|
|
179
|
+
Subscribe to server-pushed events. Use `["*"]` for all events or `[]` to unsubscribe.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
await client.subscribe_events(["locuschange", "trackadded", "dataloaded"])
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Event handling
|
|
186
|
+
|
|
187
|
+
Register callbacks for browser events:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
def on_locus_change(data):
|
|
191
|
+
locus = data["locus"]
|
|
192
|
+
print(f"Moved to {locus['chr']}:{locus['start']}-{locus['end']}")
|
|
193
|
+
|
|
194
|
+
client.on("locuschange", on_locus_change)
|
|
195
|
+
|
|
196
|
+
# Wildcard handler receives (event_name, data)
|
|
197
|
+
client.on("*", lambda name, data: print(f"Event: {name}"))
|
|
198
|
+
|
|
199
|
+
# Unregister
|
|
200
|
+
client.off("locuschange", on_locus_change)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Available events:** `locuschange`, `trackadded`, `trackremoved`, `dataloaded`, `dataerror`, `rendererror`, `trackclick`, `trackhover`, `trackcontextmenu`, `trackorderchanged`, `roiadded`, `roiremoved`, `roichanged`, `roiclick`, `roicontextmenu`
|
|
204
|
+
|
|
205
|
+
### Error handling
|
|
206
|
+
|
|
207
|
+
Server errors raise `CommandException`:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from loom_client import CommandException
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
await client.navigate(locus="invalid")
|
|
214
|
+
except CommandException as e:
|
|
215
|
+
print(e.code, e.message)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pip install -e ".[dev]"
|
|
222
|
+
pytest
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
loom_client/__init__.py,sha256=uJTC_QMv7UiznSFdS4k5KT0WloBYX-CxYetBiEQs8hY,3330
|
|
2
|
+
loom_client/client.py,sha256=INP8FMcj7F9cTwXFRtRR-PF-nF95dR2n_jlURLMp8cg,6807
|
|
3
|
+
loom_client/exceptions.py,sha256=ArcoUrn1dTG-iYFYa6gfhJk2ZZHdnufjuUZ99f0WSGk,294
|
|
4
|
+
loom_client/models.py,sha256=5Kqt-97ClFOVgitdPG8l901Wn1lZaNUHn3wlEmVGumM,14774
|
|
5
|
+
loom_client-0.0.1.dist-info/METADATA,sha256=h5hyR6J3NQOsqLfn_bYTDhWdnMEFv_Qtti8aRYN7SSE,5673
|
|
6
|
+
loom_client-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
loom_client-0.0.1.dist-info/top_level.txt,sha256=CDXwXEAiCy3zLc_Rsk69qCErhtdh5_BmXRUOEVX2D-g,12
|
|
8
|
+
loom_client-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
loom_client
|