lorax-arg 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.
Files changed (66) hide show
  1. lorax/buffer.py +43 -0
  2. lorax/cache/__init__.py +43 -0
  3. lorax/cache/csv_tree_graph.py +59 -0
  4. lorax/cache/disk.py +467 -0
  5. lorax/cache/file_cache.py +142 -0
  6. lorax/cache/file_context.py +72 -0
  7. lorax/cache/lru.py +90 -0
  8. lorax/cache/tree_graph.py +293 -0
  9. lorax/cli.py +312 -0
  10. lorax/cloud/__init__.py +0 -0
  11. lorax/cloud/gcs_utils.py +205 -0
  12. lorax/constants.py +66 -0
  13. lorax/context.py +80 -0
  14. lorax/csv/__init__.py +7 -0
  15. lorax/csv/config.py +250 -0
  16. lorax/csv/layout.py +182 -0
  17. lorax/csv/newick_tree.py +234 -0
  18. lorax/handlers.py +998 -0
  19. lorax/lineage.py +456 -0
  20. lorax/loaders/__init__.py +0 -0
  21. lorax/loaders/csv_loader.py +10 -0
  22. lorax/loaders/loader.py +31 -0
  23. lorax/loaders/tskit_loader.py +119 -0
  24. lorax/lorax_app.py +75 -0
  25. lorax/manager.py +58 -0
  26. lorax/metadata/__init__.py +0 -0
  27. lorax/metadata/loader.py +426 -0
  28. lorax/metadata/mutations.py +146 -0
  29. lorax/modes.py +190 -0
  30. lorax/pg.py +183 -0
  31. lorax/redis_utils.py +30 -0
  32. lorax/routes.py +137 -0
  33. lorax/session_manager.py +206 -0
  34. lorax/sockets/__init__.py +55 -0
  35. lorax/sockets/connection.py +99 -0
  36. lorax/sockets/debug.py +47 -0
  37. lorax/sockets/decorators.py +112 -0
  38. lorax/sockets/file_ops.py +200 -0
  39. lorax/sockets/lineage.py +307 -0
  40. lorax/sockets/metadata.py +232 -0
  41. lorax/sockets/mutations.py +154 -0
  42. lorax/sockets/node_search.py +535 -0
  43. lorax/sockets/tree_layout.py +117 -0
  44. lorax/sockets/utils.py +10 -0
  45. lorax/tree_graph/__init__.py +12 -0
  46. lorax/tree_graph/tree_graph.py +689 -0
  47. lorax/utils.py +124 -0
  48. lorax_app/__init__.py +4 -0
  49. lorax_app/app.py +159 -0
  50. lorax_app/cli.py +114 -0
  51. lorax_app/static/X.png +0 -0
  52. lorax_app/static/assets/index-BCEGlUFi.js +2361 -0
  53. lorax_app/static/assets/index-iKjzUpA9.css +1 -0
  54. lorax_app/static/assets/localBackendWorker-BaWwjSV_.js +2 -0
  55. lorax_app/static/assets/renderDataWorker-BKLdiU7J.js +2 -0
  56. lorax_app/static/gestures/gesture-flick.ogv +0 -0
  57. lorax_app/static/gestures/gesture-two-finger-scroll.ogv +0 -0
  58. lorax_app/static/index.html +14 -0
  59. lorax_app/static/logo.png +0 -0
  60. lorax_app/static/lorax-logo.png +0 -0
  61. lorax_app/static/vite.svg +1 -0
  62. lorax_arg-0.1.dist-info/METADATA +131 -0
  63. lorax_arg-0.1.dist-info/RECORD +66 -0
  64. lorax_arg-0.1.dist-info/WHEEL +5 -0
  65. lorax_arg-0.1.dist-info/entry_points.txt +4 -0
  66. lorax_arg-0.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,200 @@
1
+ """
2
+ File operation event handlers for Lorax Socket.IO.
3
+
4
+ Handles load_file, details, and query events.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import asyncio
10
+ from pathlib import Path
11
+
12
+ from lorax.context import session_manager, BUCKET_NAME, tree_graph_cache, csv_tree_graph_cache
13
+ from lorax.modes import CURRENT_MODE
14
+ from lorax.constants import (
15
+ UPLOADS_DIR, ERROR_SESSION_NOT_FOUND, ERROR_MISSING_SESSION, ERROR_NO_FILE_LOADED,
16
+ )
17
+ from lorax.cloud.gcs_utils import download_gcs_file
18
+ from lorax.handlers import handle_upload, handle_details
19
+ from lorax.sockets.decorators import require_session
20
+ from lorax.sockets.utils import is_csv_session_file
21
+
22
+ UPLOAD_DIR = Path(UPLOADS_DIR)
23
+ UPLOAD_DIR.mkdir(exist_ok=True)
24
+
25
+
26
+ def register_file_events(sio):
27
+ """Register file operation socket events."""
28
+
29
+ async def background_load_file(sid, data):
30
+ try:
31
+ lorax_sid = data.get("lorax_sid")
32
+ share_sid = data.get("share_sid")
33
+
34
+ if not lorax_sid:
35
+ print(f"⚠️ Missing lorax_sid")
36
+ await sio.emit("error", {
37
+ "code": ERROR_MISSING_SESSION,
38
+ "message": "Session ID is missing."
39
+ }, to=sid)
40
+ return
41
+
42
+ session = await session_manager.get_session(lorax_sid)
43
+ if not session:
44
+ print(f"⚠️ Unknown sid {lorax_sid}")
45
+ await sio.emit("error", {
46
+ "code": ERROR_SESSION_NOT_FOUND,
47
+ "message": "Session expired. Please refresh the page."
48
+ }, to=sid)
49
+ return
50
+
51
+ if share_sid and share_sid != lorax_sid:
52
+ print(f"⚠️ share_sid denied for sid={lorax_sid} target={share_sid}")
53
+ await sio.emit("error", {
54
+ "code": "share_sid_denied",
55
+ "message": "Access denied for shared upload."
56
+ }, to=sid)
57
+ return
58
+
59
+ project = str(data.get("project"))
60
+ filename = str(data.get("file"))
61
+
62
+ # Extract genomic coordinates from client if provided
63
+ genomiccoordstart = data.get("genomiccoordstart")
64
+ genomiccoordend = data.get("genomiccoordend")
65
+ print("lorax_sid", lorax_sid, project, filename)
66
+ if not filename:
67
+ print("Missing file param")
68
+ return
69
+
70
+ if project == 'Uploads':
71
+ target_sid = share_sid if share_sid else lorax_sid
72
+ if CURRENT_MODE == "local":
73
+ file_path = UPLOAD_DIR / project / filename
74
+ blob_path = f"{project}/{filename}"
75
+ else:
76
+ file_path = UPLOAD_DIR / project / target_sid / filename
77
+ blob_path = f"{project}/{target_sid}/{filename}"
78
+ else:
79
+ file_path = UPLOAD_DIR / project / filename
80
+ blob_path = f"{project}/{filename}"
81
+
82
+ if BUCKET_NAME and CURRENT_MODE != "local":
83
+ if file_path.exists():
84
+ print(f"File {file_path} already exists, skipping download.")
85
+ else:
86
+ print(f"Downloading file {file_path} from {BUCKET_NAME}")
87
+ await download_gcs_file(BUCKET_NAME, f"{blob_path}", str(file_path))
88
+ else:
89
+ print("local mode or no bucket; using local uploads only")
90
+
91
+ if not file_path.exists():
92
+ print("File not found")
93
+ return
94
+
95
+ # Clear TreeGraph cache when loading a new file
96
+ await tree_graph_cache.clear_session(lorax_sid)
97
+ await csv_tree_graph_cache.clear_session(lorax_sid)
98
+
99
+ session.file_path = str(file_path)
100
+ await session_manager.save_session(session)
101
+
102
+ print("loading file", file_path, os.getpid())
103
+ ctx = await handle_upload(str(file_path), str(UPLOAD_DIR))
104
+
105
+ await sio.emit("status", {
106
+ "status": "processing-file",
107
+ "message": "Processing file...",
108
+ "filename": filename,
109
+ "project": project
110
+ }, to=sid)
111
+
112
+ # Config is already computed and cached in FileContext
113
+ config = ctx.config
114
+
115
+ if config is None:
116
+ await sio.emit("error", {"message": "Failed to load file configuration"}, to=sid)
117
+ return
118
+
119
+ # Override initial_position if client provided genomic coordinates
120
+ if genomiccoordstart is not None and genomiccoordend is not None:
121
+ try:
122
+ config['initial_position'] = [int(genomiccoordstart), int(genomiccoordend)]
123
+ print(f"Using client-provided coordinates: [{genomiccoordstart}, {genomiccoordend}]")
124
+ except (ValueError, TypeError) as e:
125
+ print(f"Invalid coordinates, using computed: {e}")
126
+
127
+ owner_sid = share_sid if share_sid else lorax_sid
128
+ await sio.emit("load-file-result", {
129
+ "message": "File loaded",
130
+ "sid": sid,
131
+ "filename": filename,
132
+ "config": config,
133
+ "owner_sid": owner_sid
134
+ }, to=sid)
135
+
136
+ except Exception as e:
137
+ print(f"Load file error: {e}")
138
+ await sio.emit("error", {"message": str(e)}, to=sid)
139
+
140
+ @sio.event
141
+ async def load_file(sid, data):
142
+ asyncio.create_task(background_load_file(sid, data))
143
+
144
+ @sio.event
145
+ async def details(sid, data):
146
+ try:
147
+ lorax_sid = data.get("lorax_sid")
148
+ session = await require_session(lorax_sid, sid, sio)
149
+ if not session:
150
+ return
151
+
152
+ if not session.file_path:
153
+ print(f"⚠️ No file loaded for session {lorax_sid}")
154
+ await sio.emit("error", {
155
+ "code": ERROR_NO_FILE_LOADED,
156
+ "message": "No file loaded. Please load a file first."
157
+ }, to=sid)
158
+ return
159
+
160
+ if is_csv_session_file(session.file_path):
161
+ await sio.emit("details-result", {
162
+ "data": {"error": "Details are not supported for CSV yet."}
163
+ }, to=sid)
164
+ return
165
+
166
+ print("fetch details in ", session.sid, os.getpid())
167
+
168
+ result = await handle_details(session.file_path, data)
169
+ await sio.emit("details-result", {"data": json.loads(result)}, to=sid)
170
+ except Exception as e:
171
+ print(f"❌ Details error: {e}")
172
+ await sio.emit("details-result", {"error": str(e)}, to=sid)
173
+
174
+ @sio.event
175
+ async def query(sid, data):
176
+ """Socket event to query tree nodes."""
177
+ try:
178
+ lorax_sid = data.get("lorax_sid")
179
+ session = await require_session(lorax_sid, sid, sio)
180
+ if not session:
181
+ return
182
+
183
+ if not session.file_path:
184
+ print(f"⚠️ No file loaded for session {lorax_sid}")
185
+ await sio.emit("error", {
186
+ "code": ERROR_NO_FILE_LOADED,
187
+ "message": "No file loaded. Please load a file first."
188
+ }, to=sid)
189
+ return
190
+
191
+ value = data.get("value")
192
+ local_trees = data.get("localTrees", [])
193
+
194
+ # Acknowledge the query - the actual tree data is processed by the frontend worker
195
+ await sio.emit("query-result", {
196
+ "data": {"value": value, "localTrees": local_trees}
197
+ }, to=sid)
198
+ except Exception as e:
199
+ print(f"❌ Query error: {e}")
200
+ await sio.emit("query-result", {"error": str(e)}, to=sid)
@@ -0,0 +1,307 @@
1
+ """
2
+ Lineage event handlers for Lorax Socket.IO.
3
+
4
+ Handles ancestry and descendant operations:
5
+ - get_ancestors_event
6
+ - get_descendants_event
7
+ - search_nodes_by_criteria_event
8
+ - get_subtree_event
9
+ - get_mrca_event
10
+ """
11
+
12
+ from lorax.context import tree_graph_cache
13
+ from lorax.handlers import get_or_construct_tree_graph
14
+ from lorax.lineage import (
15
+ get_ancestors, get_descendants, search_nodes_by_criteria,
16
+ get_subtree, get_mrca
17
+ )
18
+ from lorax.sockets.decorators import require_session
19
+ from lorax.sockets.utils import is_csv_session_file
20
+
21
+
22
+ def register_lineage_events(sio):
23
+ """Register lineage-related socket events."""
24
+
25
+ @sio.event
26
+ async def get_ancestors_event(sid, data):
27
+ """Socket event to get ancestors (path to root) for a node.
28
+
29
+ Requires the tree to be cached first (call cache_trees or process a layout).
30
+
31
+ data: {
32
+ lorax_sid: str,
33
+ tree_index: int,
34
+ node_id: int
35
+ }
36
+
37
+ Returns: {
38
+ ancestors: [int], # Node IDs from node to root
39
+ path: [{node_id, time, x, y}], # Path coordinates for visualization
40
+ tree_index: int,
41
+ query_node: int
42
+ }
43
+ """
44
+ try:
45
+ lorax_sid = data.get("lorax_sid")
46
+ session = await require_session(lorax_sid, sid, sio)
47
+ if not session:
48
+ return {"error": "Session not found", "ancestors": [], "path": []}
49
+
50
+ if not session.file_path:
51
+ return {"error": "No file loaded", "ancestors": [], "path": []}
52
+
53
+ if is_csv_session_file(session.file_path):
54
+ return {"error": "Lineage not supported for CSV", "ancestors": [], "path": []}
55
+
56
+ tree_index = data.get("tree_index")
57
+ node_id = data.get("node_id")
58
+
59
+ if tree_index is None or node_id is None:
60
+ return {"error": "Missing tree_index or node_id", "ancestors": [], "path": []}
61
+
62
+ # Ensure tree is cached
63
+ await get_or_construct_tree_graph(
64
+ session.file_path,
65
+ int(tree_index),
66
+ lorax_sid,
67
+ tree_graph_cache
68
+ )
69
+
70
+ result = await get_ancestors(
71
+ tree_graph_cache,
72
+ lorax_sid,
73
+ int(tree_index),
74
+ int(node_id)
75
+ )
76
+ return result
77
+ except Exception as e:
78
+ print(f"❌ Get ancestors error: {e}")
79
+ return {"error": str(e), "ancestors": [], "path": []}
80
+
81
+ @sio.event
82
+ async def get_descendants_event(sid, data):
83
+ """Socket event to get all descendants of a node.
84
+
85
+ data: {
86
+ lorax_sid: str,
87
+ tree_index: int,
88
+ node_id: int,
89
+ include_tips_only: bool # Optional, default False
90
+ }
91
+
92
+ Returns: {
93
+ descendants: [int],
94
+ tips: [int],
95
+ total_descendants: int,
96
+ tree_index: int,
97
+ query_node: int
98
+ }
99
+ """
100
+ try:
101
+ lorax_sid = data.get("lorax_sid")
102
+ session = await require_session(lorax_sid, sid, sio)
103
+ if not session:
104
+ return {"error": "Session not found", "descendants": [], "tips": []}
105
+
106
+ if not session.file_path:
107
+ return {"error": "No file loaded", "descendants": [], "tips": []}
108
+
109
+ if is_csv_session_file(session.file_path):
110
+ return {"error": "Lineage not supported for CSV", "descendants": [], "tips": []}
111
+
112
+ tree_index = data.get("tree_index")
113
+ node_id = data.get("node_id")
114
+ include_tips_only = data.get("include_tips_only", False)
115
+
116
+ if tree_index is None or node_id is None:
117
+ return {"error": "Missing tree_index or node_id", "descendants": [], "tips": []}
118
+
119
+ # Ensure tree is cached
120
+ await get_or_construct_tree_graph(
121
+ session.file_path,
122
+ int(tree_index),
123
+ lorax_sid,
124
+ tree_graph_cache
125
+ )
126
+
127
+ result = await get_descendants(
128
+ tree_graph_cache,
129
+ lorax_sid,
130
+ int(tree_index),
131
+ int(node_id),
132
+ include_tips_only=include_tips_only
133
+ )
134
+ return result
135
+ except Exception as e:
136
+ print(f"❌ Get descendants error: {e}")
137
+ return {"error": str(e), "descendants": [], "tips": []}
138
+
139
+ @sio.event
140
+ async def search_nodes_by_criteria_event(sid, data):
141
+ """Socket event to search nodes by criteria (time, tip status, etc).
142
+
143
+ data: {
144
+ lorax_sid: str,
145
+ tree_index: int,
146
+ criteria: {
147
+ min_time: float, # Optional
148
+ max_time: float, # Optional
149
+ is_tip: bool, # Optional: True for tips, False for internal
150
+ has_children: bool, # Optional: inverse of is_tip
151
+ node_ids: [int] # Optional: filter to these nodes
152
+ }
153
+ }
154
+
155
+ Returns: {
156
+ matches: [int],
157
+ positions: [{node_id, x, y, time}],
158
+ total_matches: int
159
+ }
160
+ """
161
+ try:
162
+ lorax_sid = data.get("lorax_sid")
163
+ session = await require_session(lorax_sid, sid, sio)
164
+ if not session:
165
+ return {"error": "Session not found", "matches": [], "positions": []}
166
+
167
+ if not session.file_path:
168
+ return {"error": "No file loaded", "matches": [], "positions": []}
169
+
170
+ if is_csv_session_file(session.file_path):
171
+ return {"error": "Search not supported for CSV", "matches": [], "positions": []}
172
+
173
+ tree_index = data.get("tree_index")
174
+ criteria = data.get("criteria", {})
175
+
176
+ if tree_index is None:
177
+ return {"error": "Missing tree_index", "matches": [], "positions": []}
178
+
179
+ # Ensure tree is cached
180
+ await get_or_construct_tree_graph(
181
+ session.file_path,
182
+ int(tree_index),
183
+ lorax_sid,
184
+ tree_graph_cache
185
+ )
186
+
187
+ result = await search_nodes_by_criteria(
188
+ tree_graph_cache,
189
+ lorax_sid,
190
+ int(tree_index),
191
+ criteria
192
+ )
193
+ return result
194
+ except Exception as e:
195
+ print(f"❌ Search nodes error: {e}")
196
+ return {"error": str(e), "matches": [], "positions": []}
197
+
198
+ @sio.event
199
+ async def get_subtree_event(sid, data):
200
+ """Socket event to get the complete subtree rooted at a node.
201
+
202
+ data: {
203
+ lorax_sid: str,
204
+ tree_index: int,
205
+ root_node_id: int
206
+ }
207
+
208
+ Returns: {
209
+ nodes: [{node_id, parent_id, x, y, time, is_tip}],
210
+ edges: [{parent, child}],
211
+ total_nodes: int
212
+ }
213
+ """
214
+ try:
215
+ lorax_sid = data.get("lorax_sid")
216
+ session = await require_session(lorax_sid, sid, sio)
217
+ if not session:
218
+ return {"error": "Session not found", "nodes": [], "edges": []}
219
+
220
+ if not session.file_path:
221
+ return {"error": "No file loaded", "nodes": [], "edges": []}
222
+
223
+ if is_csv_session_file(session.file_path):
224
+ return {"error": "Subtree not supported for CSV", "nodes": [], "edges": []}
225
+
226
+ tree_index = data.get("tree_index")
227
+ root_node_id = data.get("root_node_id")
228
+
229
+ if tree_index is None or root_node_id is None:
230
+ return {"error": "Missing tree_index or root_node_id", "nodes": [], "edges": []}
231
+
232
+ # Ensure tree is cached
233
+ await get_or_construct_tree_graph(
234
+ session.file_path,
235
+ int(tree_index),
236
+ lorax_sid,
237
+ tree_graph_cache
238
+ )
239
+
240
+ result = await get_subtree(
241
+ tree_graph_cache,
242
+ lorax_sid,
243
+ int(tree_index),
244
+ int(root_node_id)
245
+ )
246
+ return result
247
+ except Exception as e:
248
+ print(f"❌ Get subtree error: {e}")
249
+ return {"error": str(e), "nodes": [], "edges": []}
250
+
251
+ @sio.event
252
+ async def get_mrca_event(sid, data):
253
+ """Socket event to find the Most Recent Common Ancestor of multiple nodes.
254
+
255
+ data: {
256
+ lorax_sid: str,
257
+ tree_index: int,
258
+ node_ids: [int] # At least 2 nodes
259
+ }
260
+
261
+ Returns: {
262
+ mrca: int, # Node ID of MRCA
263
+ mrca_time: float,
264
+ mrca_position: {x, y},
265
+ tree_index: int,
266
+ query_nodes: [int]
267
+ }
268
+ """
269
+ try:
270
+ lorax_sid = data.get("lorax_sid")
271
+ session = await require_session(lorax_sid, sid, sio)
272
+ if not session:
273
+ return {"error": "Session not found", "mrca": None}
274
+
275
+ if not session.file_path:
276
+ return {"error": "No file loaded", "mrca": None}
277
+
278
+ if is_csv_session_file(session.file_path):
279
+ return {"error": "MRCA not supported for CSV", "mrca": None}
280
+
281
+ tree_index = data.get("tree_index")
282
+ node_ids = data.get("node_ids", [])
283
+
284
+ if tree_index is None:
285
+ return {"error": "Missing tree_index", "mrca": None}
286
+
287
+ if len(node_ids) < 2:
288
+ return {"error": "Need at least 2 nodes", "mrca": None}
289
+
290
+ # Ensure tree is cached
291
+ await get_or_construct_tree_graph(
292
+ session.file_path,
293
+ int(tree_index),
294
+ lorax_sid,
295
+ tree_graph_cache
296
+ )
297
+
298
+ result = await get_mrca(
299
+ tree_graph_cache,
300
+ lorax_sid,
301
+ int(tree_index),
302
+ [int(n) for n in node_ids]
303
+ )
304
+ return result
305
+ except Exception as e:
306
+ print(f"❌ Get MRCA error: {e}")
307
+ return {"error": str(e), "mrca": None}