sclab 0.1.7__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.
@@ -0,0 +1,7 @@
1
+ from ._basic_processor_step import BasicProcessorStep
2
+ from ._processor_step_base import ProcessorStepBase
3
+
4
+ __all__ = [
5
+ "BasicProcessorStep",
6
+ "ProcessorStepBase",
7
+ ]
@@ -0,0 +1,109 @@
1
+ import traceback
2
+ from typing import Any
3
+
4
+ from IPython.display import display
5
+ from ipywidgets.widgets import (
6
+ HTML,
7
+ Button,
8
+ Output,
9
+ VBox,
10
+ )
11
+ from ipywidgets.widgets.valuewidget import ValueWidget
12
+ from ipywidgets.widgets.widget_description import DescriptionWidget
13
+
14
+ from ....event import EventClient
15
+ from .._processor import Processor
16
+
17
+
18
+ class BasicProcessorStep(EventClient):
19
+ events: list[str] = None
20
+ parent: Processor
21
+ description: str
22
+ function_name: str
23
+ function: callable
24
+ fixed_params: dict[str, Any]
25
+ variable_controls: dict[str, DescriptionWidget | ValueWidget]
26
+ output: Output
27
+ run_button: Button
28
+ controls: VBox
29
+
30
+ def __init__(
31
+ self,
32
+ parent: Processor,
33
+ description: str,
34
+ function: callable,
35
+ fixed_params: dict[str, Any] = {},
36
+ variable_controls: dict[str, DescriptionWidget | ValueWidget] = {},
37
+ use_run_button: bool = True,
38
+ ):
39
+ self.parent = parent
40
+ self.description = description
41
+ self.function = function
42
+ self.function_name = function.__name__
43
+
44
+ self.events = [
45
+ f"step_{self.function_name}_started",
46
+ f"step_{self.function_name}_ended",
47
+ ]
48
+
49
+ self.fixed_params = fixed_params
50
+ self.variable_controls = variable_controls
51
+ self.output = Output()
52
+
53
+ controls = []
54
+ for control in self.variable_controls.values():
55
+ control.layout.width = "98%"
56
+ self.parent.all_controls_list.append(control)
57
+ controls.append(control)
58
+
59
+ self.use_run_button = use_run_button
60
+ if use_run_button:
61
+ self.run_button = Button(description="Run", button_style="primary")
62
+ self.run_button.on_click(self.callback)
63
+ controls.append(self.run_button)
64
+
65
+ controls.append(self.output)
66
+ self.controls = VBox(controls)
67
+ super().__init__(parent.broker)
68
+
69
+ @property
70
+ def variable_params(self):
71
+ return {key: control.value for key, control in self.variable_controls.items()}
72
+
73
+ def callback(self, _: Button | None = None):
74
+ self.run()
75
+
76
+ def run(self, **extra_params):
77
+ try:
78
+ # extra params will override fixed and variable params
79
+ params = {**self.fixed_params, **self.variable_params, **extra_params}
80
+
81
+ if self.use_run_button:
82
+ self.run_button.disabled = True
83
+ self.run_button.button_style = "warning"
84
+ self.run_button.description = "Running..."
85
+ self.broker.publish(f"step_{self.function_name}_started")
86
+
87
+ self.function(**params)
88
+ self.parent.append_to_step_history(self.description, params)
89
+
90
+ if self.use_run_button:
91
+ self.run_button.button_style = "success"
92
+
93
+ self.broker.publish(f"step_{self.function_name}_ended", status="success")
94
+
95
+ except Exception as e:
96
+ if self.use_run_button:
97
+ self.run_button.button_style = "danger"
98
+
99
+ info = dict(status="failed", error=e, traceback=traceback.format_exc())
100
+ self.broker.publish(f"step_{self.function_name}_ended", **info)
101
+ self.broker.exceptions_log.append(traceback.format_exc())
102
+
103
+ with self.broker.exceptions_output.output:
104
+ display(HTML(f"<pre>{traceback.format_exc()}</pre>"))
105
+
106
+ finally:
107
+ if self.use_run_button:
108
+ self.run_button.description = "Run"
109
+ self.run_button.disabled = False
@@ -0,0 +1,120 @@
1
+ import traceback
2
+ from typing import Any
3
+
4
+ from IPython.display import HTML, Markdown, display
5
+ from ipywidgets import Button, Output, VBox
6
+ from ipywidgets.widgets.valuewidget import ValueWidget
7
+ from ipywidgets.widgets.widget_description import DescriptionWidget
8
+
9
+ from ....event import EventClient
10
+ from .._processor import Processor
11
+
12
+
13
+ class ProcessorStepBase(EventClient):
14
+ events: list[str] = None
15
+ parent: Processor
16
+ name: str
17
+ description: str
18
+ fixed_params: dict[str, Any]
19
+ variable_controls: dict[str, DescriptionWidget | ValueWidget]
20
+ output: Output
21
+ run_button: Button
22
+ controls_list: list[DescriptionWidget | ValueWidget | Button]
23
+ controls: VBox
24
+
25
+ run_button_description = "Run"
26
+
27
+ def __init__(
28
+ self,
29
+ parent: Processor,
30
+ name: str,
31
+ description: str,
32
+ fixed_params: dict[str, Any],
33
+ variable_controls: dict[str, DescriptionWidget | ValueWidget],
34
+ ):
35
+ self.parent = parent
36
+ self.name = name
37
+ self.description = description
38
+ self.fixed_params = fixed_params
39
+ self.variable_controls = variable_controls
40
+
41
+ self.events = [
42
+ f"step_{self.name}_started",
43
+ f"step_{self.name}_ended",
44
+ ]
45
+
46
+ self.output = Output()
47
+ self.run_button = Button(
48
+ description=self.run_button_description, button_style="primary"
49
+ )
50
+ self.run_button.on_click(self.button_callback)
51
+
52
+ self.controls_list = [
53
+ *self.variable_controls.values(),
54
+ self.run_button,
55
+ self.output,
56
+ ]
57
+ self.make_controls()
58
+
59
+ super().__init__(parent.broker)
60
+
61
+ def make_controls(self):
62
+ for control in self.controls_list:
63
+ control.layout.width = "98%"
64
+ self.parent.all_controls_list.append(control)
65
+
66
+ self.controls = VBox(children=self.controls_list)
67
+
68
+ @property
69
+ def variable_params(self):
70
+ return {key: control.value for key, control in self.variable_controls.items()}
71
+
72
+ def button_callback(self, _: Button | None = None):
73
+ self.run()
74
+
75
+ def function(self, *pargs, **kwargs):
76
+ raise NotImplementedError
77
+
78
+ def run(self, **extra_params):
79
+ self.output.clear_output(wait=False)
80
+ try:
81
+ # extra params will override fixed and variable params
82
+ params = {**self.fixed_params, **self.variable_params, **extra_params}
83
+
84
+ self.run_button.disabled = True
85
+ self.run_button.button_style = "warning"
86
+ self.run_button.description = "..."
87
+
88
+ self.broker.publish(f"step_{self.name}_started")
89
+ self.function(**params)
90
+
91
+ self.parent.append_to_step_history(self.description, params)
92
+ info = dict(status="success")
93
+ self.run_button.button_style = "success"
94
+
95
+ except Exception as e:
96
+ self.run_button.button_style = "danger"
97
+
98
+ info = dict(status="failed", error=e, traceback=traceback.format_exc())
99
+ self.broker.exceptions_log.append(traceback.format_exc())
100
+ with self.broker.exceptions_output.output:
101
+ display(HTML(f"<pre>{traceback.format_exc()}</pre>"))
102
+ self.update_output(f"{type(e)}: {e}")
103
+
104
+ finally:
105
+ self.run_button.description = self.run_button_description
106
+ self.run_button.disabled = False
107
+ self.broker.publish(f"step_{self.name}_ended", **info)
108
+
109
+ def update_output(self, message: str | Any | None, clear: bool = True):
110
+ if clear:
111
+ self.output.clear_output(wait=True)
112
+
113
+ if isinstance(message, str):
114
+ message = Markdown(message)
115
+
116
+ elif message is None:
117
+ message = Markdown("")
118
+
119
+ with self.output:
120
+ display(message)
@@ -0,0 +1,7 @@
1
+ from ._broker import EventBroker
2
+ from ._client import EventClient
3
+
4
+ __all__ = [
5
+ "EventBroker",
6
+ "EventClient",
7
+ ]
sclab/event/_broker.py ADDED
@@ -0,0 +1,201 @@
1
+ import traceback
2
+ from contextlib import contextmanager
3
+ from uuid import uuid4
4
+
5
+ from IPython.display import HTML, display
6
+ from ipywidgets.widgets import Layout, Output, Tab
7
+
8
+ from ._client import EventClient
9
+ from ._utils import LogOutput
10
+
11
+
12
+ class EventBroker:
13
+ """Simple event broker for publishing and subscribing to events.
14
+
15
+ Example:
16
+ ```
17
+ broker = EventBroker()
18
+
19
+ def callback(*args, **kwargs):
20
+ print(args, kwargs)
21
+
22
+ broker.subscribe("event", callback)
23
+ broker.publish("event", 1, 2, 3, a=4, b=5)
24
+ ```
25
+ """
26
+
27
+ clients: list[EventClient]
28
+ available_events: set[str]
29
+ preemptions: dict[str, list[str]]
30
+ _subscribers: dict[str, list[callable]]
31
+ _disabled_events: list[str]
32
+
33
+ def __init__(self):
34
+ self.clients = []
35
+ self.available_events = set()
36
+ self.preemptions = {}
37
+ self._subscribers = {}
38
+ self._disabled_events = []
39
+ self.depth = 0
40
+ self.std_output = Output(layout=Layout(width="auto", height="500px"))
41
+ self.execution_log = []
42
+ self.execution_output = LogOutput()
43
+ self.exceptions_log = []
44
+ self.exceptions_output = LogOutput()
45
+ self.id = uuid4()
46
+ self.event_log: Output = Output(
47
+ layout=Layout(height="200px"),
48
+ # style={"overflow-y": "scroll"}, TODO: it seems it the way to set this has changed
49
+ )
50
+ self.logs_tab = Tab(
51
+ [
52
+ self.std_output,
53
+ self.execution_output,
54
+ self.exceptions_output,
55
+ ],
56
+ titles=[
57
+ "Standard Output",
58
+ "Events",
59
+ "Exceptions",
60
+ ],
61
+ )
62
+
63
+ def register_client(self, client: EventClient):
64
+ if not isinstance(client, EventClient):
65
+ raise TypeError("client must be an instance of EventClient")
66
+
67
+ if client in self.clients:
68
+ return
69
+
70
+ self.clients.append(client)
71
+ self.available_events.update(client.events)
72
+ self.preemptions.update(client.preemptions)
73
+ client.broker = self
74
+
75
+ def unregister_client(self, client: EventClient):
76
+ # TODO: ensure full cleanup of subscriptions
77
+
78
+ if client in self.clients:
79
+ self.clients.remove(client)
80
+
81
+ self.available_events.difference_update(client.events)
82
+
83
+ self._disabled_events = [
84
+ e for e in self._disabled_events if e not in client.events
85
+ ]
86
+
87
+ for event in client.events:
88
+ self._subscribers.pop(event, None)
89
+ self.preemptions.pop(event, None)
90
+
91
+ def update_subscriptions(self):
92
+ for client in self.clients:
93
+ for event in self.available_events:
94
+ if callback := getattr(client, f"{event}_callback", None):
95
+ self.subscribe(event, callback)
96
+ client.subscriptions[event] = callback
97
+
98
+ def subscribe(self, event: str, callback: callable):
99
+ if event not in self._subscribers:
100
+ self._subscribers[event] = []
101
+
102
+ if callback not in self._subscribers[event]:
103
+ # Prevent duplicate subscriptions
104
+ self._subscribers[event].append(callback)
105
+
106
+ def unsubscribe(self, event: str, callback: callable):
107
+ if event in self._subscribers and callback in self._subscribers[event]:
108
+ self._subscribers[event].remove(callback)
109
+
110
+ def publish(self, event: str, *args, **kwargs):
111
+ if event not in self.available_events:
112
+ raise ValueError(f"Event '{event}' is not available.")
113
+
114
+ try:
115
+ self.depth += 1
116
+ tab = " " * self.depth * 4
117
+ txt_args = []
118
+ for arg in args:
119
+ if isinstance(arg, str | float | int | bool | tuple | Exception | None):
120
+ txt_args.append(arg)
121
+ else:
122
+ txt_args.append(type(arg))
123
+ txt_kwargs = {}
124
+ for k, v in kwargs.items():
125
+ if isinstance(v, str | float | int | bool | tuple | Exception | None):
126
+ txt_kwargs[k] = v
127
+ else:
128
+ txt_kwargs[k] = type(v)
129
+
130
+ if event in self._disabled_events:
131
+ msg = f"{tab}Disabled Event: {event}."
132
+ self.execution_log.append(msg)
133
+ with self.execution_output.output:
134
+ display(HTML(f"<pre>{msg}</pre>"))
135
+ return
136
+
137
+ msg = f"{tab}{event}. Args: {tuple(txt_args)}. Kwargs: {txt_kwargs}"
138
+ self.execution_log.append(msg)
139
+ with self.execution_output.output:
140
+ display(HTML(f"<pre>{msg}</pre>"))
141
+
142
+ preempt = self.preemptions.get(event, [])
143
+ with self.disable(preempt):
144
+ if event in self._subscribers:
145
+ for callback in self._subscribers[event]:
146
+ if hasattr(callback, "__self__"):
147
+ parent_class = callback.__self__.__class__.__name__
148
+ else:
149
+ parent_class = "<local context>"
150
+ msg = f"{tab}{event} --> {parent_class}.{callback.__name__}()"
151
+ self.execution_log.append(msg)
152
+ with self.execution_output.output:
153
+ display(HTML(f"<pre>{msg}</pre>"))
154
+
155
+ callback(*args, **kwargs)
156
+ except Exception as e:
157
+ msg = f"{tab} Exception: {e}"
158
+ self.execution_log.append(msg)
159
+ with self.execution_output.output:
160
+ display(HTML(f"<pre>{msg}</pre>"))
161
+
162
+ msg = f"{msg}{tab} Exception: {e}"
163
+ msg += f"\n\n{traceback.format_exc()}"
164
+ self.exceptions_log.append(msg)
165
+
166
+ with self.exceptions_output.output:
167
+ display(HTML(f"<pre>{traceback.format_exc()}</pre>"))
168
+
169
+ finally:
170
+ self.depth -= 1
171
+
172
+ def disable_event(self, event: str):
173
+ self._disabled_events.append(event)
174
+
175
+ def enable_event(self, event: str):
176
+ if event in self._disabled_events:
177
+ self._disabled_events.remove(event)
178
+
179
+ @contextmanager
180
+ def disable(self, events: str | list[str]):
181
+ if isinstance(events, str):
182
+ events = [events]
183
+
184
+ try:
185
+ for event in events:
186
+ self.disable_event(event)
187
+ yield
188
+ finally:
189
+ for event in events:
190
+ self.enable_event(event)
191
+
192
+ @contextmanager
193
+ def delay(self, events: str | list[str]):
194
+ if isinstance(events, str):
195
+ events = [events]
196
+
197
+ # TODO: implement delay context manager
198
+ # the idea is to catch these events and execute them after the current event is done
199
+ # if the delayed event is duplicated, it should be executed only once
200
+ # we could generate a execution tree and execute the events in the correct order
201
+ pass
sclab/event/_client.py ADDED
@@ -0,0 +1,81 @@
1
+ from uuid import uuid4
2
+
3
+ from ipywidgets.widgets import Button, Output
4
+
5
+
6
+ # forward declaration
7
+ class EventBroker:
8
+ clients: list["EventClient"]
9
+ available_events: set[str]
10
+ preemptions: dict[str, list[str]]
11
+ _subscribers: dict[str, list[callable]]
12
+ _disabled_events: list[str]
13
+ std_output: Output
14
+
15
+ def register_client(self, client: "EventClient"):
16
+ ...
17
+
18
+ def unregister_client(self, client: "EventClient"):
19
+ ...
20
+
21
+ def update_subscriptions(self):
22
+ ...
23
+
24
+ def subscribe(self, event: str, callback: callable):
25
+ ...
26
+
27
+ def unsubscribe(self, event: str, callback: callable):
28
+ ...
29
+
30
+ def publish(self, event: str, *args, **kwargs):
31
+ ...
32
+
33
+ def disable_event(self, event: str):
34
+ ...
35
+
36
+ def enable_event(self, event: str):
37
+ ...
38
+
39
+
40
+ class EventClient:
41
+ subscriptions: dict[str, callable]
42
+ preemptions: dict[str, list[str]] = {} # override this in subclasses if needed
43
+ broker: "EventBroker"
44
+ events: list[str]
45
+
46
+ def __init__(self, broker: "EventBroker"):
47
+ self.uuid = uuid4().hex
48
+ self.subscriptions = {}
49
+ broker.register_client(self)
50
+ broker.update_subscriptions()
51
+
52
+ def button_click_event_publisher(self, control_prefix: str, control_name: str):
53
+ event_str = f"{control_prefix}_{control_name}_click"
54
+ assert (
55
+ event_str in self.events
56
+ ), f"Event {event_str} not in {self.__class__.__name__}.events list."
57
+
58
+ def publisher(_: Button):
59
+ self.broker.publish(event_str)
60
+
61
+ return publisher
62
+
63
+ def control_value_change_event_publisher(
64
+ self, control_prefix: str, control_name: str
65
+ ):
66
+ event_str = f"{control_prefix}_{control_name}_change"
67
+ assert (
68
+ event_str in self.events
69
+ ), f"Event {event_str} not in {self.__class__.__name__}.events list."
70
+
71
+ def publisher(event: dict):
72
+ if event["type"] != "change":
73
+ raise ValueError("Event type must be 'change'")
74
+ new_value = event["new"]
75
+ self.broker.publish(event_str, new_value=new_value)
76
+
77
+ return publisher
78
+
79
+ def __del__(self):
80
+ for event, callback in self.subscriptions.items():
81
+ self.broker.unsubscribe(event, callback)
sclab/event/_utils.py ADDED
@@ -0,0 +1,14 @@
1
+ from ipywidgets.widgets import Button, Layout, Output, VBox
2
+
3
+
4
+ class LogOutput(VBox):
5
+ def __init__(self):
6
+ self.clear_button = Button(description="Clear", button_style="warning")
7
+ self.output = Output(layout=Layout(width="auto", height="500px"))
8
+
9
+ super().__init__(children=[self.clear_button, self.output])
10
+
11
+ self.clear_button.on_click(self._clear)
12
+
13
+ def _clear(self, _):
14
+ self.output.clear_output()
@@ -0,0 +1,5 @@
1
+ from . import processor_steps
2
+
3
+ __all__ = [
4
+ "processor_steps",
5
+ ]
@@ -0,0 +1,15 @@
1
+ from ._cluster import Cluster
2
+ from ._neighbors import Neighbors
3
+ from ._pca import PCA
4
+ from ._preprocess import Preprocess
5
+ from ._qc import QC
6
+ from ._umap import UMAP
7
+
8
+ __all__ = [
9
+ "QC",
10
+ "Preprocess",
11
+ "PCA",
12
+ "Neighbors",
13
+ "UMAP",
14
+ "Cluster",
15
+ ]
@@ -0,0 +1,37 @@
1
+ from ipywidgets import FloatSlider
2
+
3
+ from sclab.dataset.processor import Processor
4
+ from sclab.dataset.processor.step import ProcessorStepBase
5
+
6
+
7
+ class Cluster(ProcessorStepBase):
8
+ parent: Processor
9
+
10
+ def __init__(self, parent: Processor) -> None:
11
+ try:
12
+ import scanpy as sc # noqa: F401
13
+ except ImportError:
14
+ raise ImportError("Please install scanpy: `pip install scanpy`")
15
+
16
+ variable_controls = dict(
17
+ resolution=FloatSlider(
18
+ value=1.0, min=0.1, max=10.0, step=0.1, description="Resolution"
19
+ )
20
+ )
21
+
22
+ super().__init__(
23
+ parent=parent,
24
+ name="cluster",
25
+ description="Cluster",
26
+ fixed_params={},
27
+ variable_controls=variable_controls,
28
+ )
29
+
30
+ def function(self, resolution: float = 1.0):
31
+ import scanpy as sc
32
+
33
+ dataset = self.parent.dataset
34
+ adata = self.parent.dataset.adata
35
+ sc.tl.leiden(adata, resolution=resolution)
36
+
37
+ self.broker.publish("dset_metadata_change", dataset.metadata, "leiden")
@@ -0,0 +1,72 @@
1
+ from ipywidgets import Dropdown, IntText
2
+
3
+ from sclab.dataset.processor import Processor
4
+ from sclab.dataset.processor.step import ProcessorStepBase
5
+
6
+
7
+ class Neighbors(ProcessorStepBase):
8
+ parent: Processor
9
+
10
+ def __init__(self, parent: Processor) -> None:
11
+ try:
12
+ import scanpy as sc # noqa: F401
13
+ except ImportError:
14
+ raise ImportError("Please install scanpy: `pip install scanpy`")
15
+
16
+ variable_controls = dict(
17
+ use_rep=Dropdown(
18
+ options=tuple(parent.dataset.adata.obsm.keys()),
19
+ value=None,
20
+ description="Use rep.",
21
+ ),
22
+ n_neighbors=IntText(value=20, description="N neighbors"),
23
+ n_dims=IntText(value=10, description="N Dims"),
24
+ metric=Dropdown(
25
+ options=["euclidean", "cosine"],
26
+ value="euclidean",
27
+ description="Metric",
28
+ ),
29
+ **parent.make_groupbybatch_checkbox(),
30
+ )
31
+
32
+ super().__init__(
33
+ parent=parent,
34
+ name="neighbors",
35
+ description="Neighbors",
36
+ fixed_params={},
37
+ variable_controls=variable_controls,
38
+ )
39
+
40
+ def function(
41
+ self,
42
+ n_neighbors: int = 20,
43
+ use_rep: str = "X_pca",
44
+ n_dims: int = 10,
45
+ metric: str = "euclidean",
46
+ group_by_batch: bool = False,
47
+ ):
48
+ import scanpy as sc
49
+
50
+ adata = self.parent.dataset.adata
51
+
52
+ if group_by_batch and self.parent.batch_key:
53
+ group_by = self.parent.batch_key
54
+ sc.external.pp.bbknn(
55
+ adata,
56
+ batch_key=group_by,
57
+ use_rep=use_rep,
58
+ n_pcs=n_dims,
59
+ use_annoy=False,
60
+ metric=metric,
61
+ pynndescent_n_neighbors=n_neighbors,
62
+ )
63
+ else:
64
+ sc.pp.neighbors(
65
+ adata,
66
+ n_neighbors=n_neighbors,
67
+ use_rep=use_rep,
68
+ n_pcs=n_dims,
69
+ metric=metric,
70
+ )
71
+
72
+ self.broker.publish("dset_anndata_neighbors_change")