Skip to content

Commit 33d89be

Browse files
gegnewzbjornson
authored andcommitted
feat: Add apply_tailoring function
1 parent 6099511 commit 33d89be

File tree

5 files changed

+137
-12
lines changed

5 files changed

+137
-12
lines changed

cellengine/resources/gate.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from __future__ import annotations
22
import importlib
3+
from cellengine.utils.types import (
4+
ApplyTailoringInsert,
5+
ApplyTailoringUpdate,
6+
)
7+
from collections import defaultdict
38
from math import pi
49
from operator import itemgetter
510
from typing import Any, Dict, List, Optional, Union, Tuple, overload
@@ -13,7 +18,6 @@
1318
from numpy import array, mean, stack
1419

1520
import cellengine as ce
16-
from cellengine.resources.fcs_file import FcsFile
1721
from cellengine.resources.population import Population
1822
from cellengine.utils import parse_fcs_file_args
1923
from cellengine.utils import converter, generate_id, readonly
@@ -138,10 +142,26 @@ def update_gate_family(experiment_id: str, gid: str, body: Dict) -> None:
138142
if res["nModified"] < 1:
139143
raise Warning("No gates updated.")
140144

141-
def tailor_to(self, fcs_file: FcsFile):
142-
self.tailored_per_file = True
143-
self.fcs_file_id = fcs_file._id
144-
self.update()
145+
def apply_tailoring(self, fcs_file_ids: List[str]) -> Dict[str, List[Gate]]:
146+
"""Tailor this gate to a specific FCS file or files."""
147+
payload = ce.APIClient().apply_tailoring(self.experiment_id, self, fcs_file_ids)
148+
149+
ret = defaultdict(list)
150+
for k, v in payload.items():
151+
if v:
152+
if k == "deleted":
153+
[ret[k].append(i["_id"]) for i in v] # type: ignore
154+
else:
155+
[ret[k].append(self._synthesize_gate(i)) for i in v] # type: ignore
156+
return dict(ret)
157+
158+
def _synthesize_gate(
159+
self,
160+
payload: Union[ApplyTailoringInsert, ApplyTailoringUpdate],
161+
):
162+
gate = self.to_dict()
163+
gate.update(payload)
164+
return Gate.from_dict(gate)
145165

146166

147167
class RectangleGate(Gate):

cellengine/utils/api_client/APIClient.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
from cellengine.utils.types import ApplyTailoringRes
23
from functools import lru_cache
34
from getpass import getpass
45
import importlib
@@ -603,12 +604,15 @@ def update_gate_family(self, experiment_id, gid, body: dict = None) -> dict:
603604
json=body,
604605
)
605606

606-
def tailor_to(self, experiment_id, gate_id, fcs_file_id):
607-
"""Tailor a gate to a file."""
608-
gate = self.get_gate(experiment_id, gate_id, as_dict=True)
609-
gate["tailoredPerFile"] = True
610-
gate["fcsFileId"] = fcs_file_id
611-
return self.update_entity(experiment_id, gate_id, "gates", gate)
607+
def apply_tailoring(
608+
self, experiment_id: str, gate: Gate, fcs_file_ids: List[str]
609+
) -> ApplyTailoringRes:
610+
"""Tailor a gate to a file or files."""
611+
return self._post(
612+
f"{self.base_url}/experiments/{experiment_id}/gates/applyTailored",
613+
params={"gid": gate.gid},
614+
json={"gate": gate.to_dict(), "fcsFileIds": fcs_file_ids},
615+
)
612616

613617
def get_plot(
614618
self,

cellengine/utils/types.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
import sys
3+
4+
if sys.version_info >= (3, 8):
5+
from typing import TypedDict
6+
else:
7+
from typing_extensions import TypedDict
8+
9+
ApplyTailoringInsert = TypedDict("inserted", {"_id": str, "fcsFileId": str})
10+
ApplyTailoringUpdate = TypedDict("updated", {"_id": str, "fcsFileId": str})
11+
ApplyTailoringDelete = TypedDict("deleted", {"_id": str, "fcsFileId": str})
12+
ApplyTailoringRes = TypedDict(
13+
"ApplyTailoringRes",
14+
{
15+
"inserted": ApplyTailoringInsert,
16+
"updated": ApplyTailoringUpdate,
17+
"deleted": ApplyTailoringDelete,
18+
},
19+
)

tests/unit/resources/test_gates.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,3 +744,49 @@ def test_create_split_gate(client, ENDPOINT_BASE, experiment, scalesets, split_g
744744
]
745745
assert split_gate.names == ["my gate (L)", "my gate (R)"]
746746
assert split_gate.model["labels"] == [[-199.9, 0.916], [0.9, 0.916]]
747+
748+
749+
@responses.activate
750+
def test_apply_tailoring(client, monkeypatch, ENDPOINT_BASE, rectangle_gate, fcs_files):
751+
payload = {
752+
"updated": [
753+
{"_id": "updatedGateId1", "fcsFileId": "fileId1"},
754+
{"_id": "updatedGateId2", "fcsFileId": "fileId2"},
755+
],
756+
"inserted": [],
757+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
758+
}
759+
760+
responses.add(
761+
responses.POST,
762+
f"{ENDPOINT_BASE}/experiments/{EXP_ID}/gates",
763+
status=201,
764+
json=rectangle_gate,
765+
)
766+
gate = RectangleGate.create(
767+
EXP_ID,
768+
x_channel="FSC-A",
769+
y_channel="FSC-W",
770+
name="my gate",
771+
x1=60000,
772+
x2=200000,
773+
y1=75000,
774+
y2=215000,
775+
create_population=False
776+
)
777+
778+
responses.add(
779+
responses.POST,
780+
f"{ENDPOINT_BASE}/experiments/{EXP_ID}/gates/applyTailored?gid={gate.gid}",
781+
json=payload,
782+
)
783+
784+
res = gate.apply_tailoring([fcs_files[0]["_id"]])
785+
786+
assert res["deleted"] == ["deletedGateId"]
787+
assert isinstance(res["updated"], list)
788+
assert len(res["updated"]) == 2
789+
assert res["updated"][0]._id == "updatedGateId1"
790+
assert res["updated"][1]._id == "updatedGateId2"
791+
assert res["updated"][0].fcs_file_id == "fileId1"
792+
assert res["updated"][1].fcs_file_id == "fileId2"

tests/unit/utils/test_api_client.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from cellengine.resources.compensation import Compensation
88
from cellengine.resources.experiment import Experiment
99
from cellengine.resources.fcs_file import FcsFile
10-
from cellengine.resources.gate import Gate
10+
from cellengine.resources.gate import Gate, RectangleGate
1111
from cellengine.resources.plot import Plot
1212
from cellengine.resources.population import Population
1313
from cellengine.resources.scaleset import ScaleSet
@@ -317,3 +317,39 @@ def test_clone_experiment_send_correct_body(client, ENDPOINT_BASE, experiments):
317317

318318
client.clone_experiment(EXP_ID, props={"name": "new exp name"})
319319
assert responses.calls[0].request.body == b'{"name": "new exp name"}'
320+
321+
322+
@responses.activate
323+
def test_apply_tailoring(client, ENDPOINT_BASE, gates):
324+
responses.add(
325+
responses.POST,
326+
f"{ENDPOINT_BASE}/experiments/{EXP_ID}/gates",
327+
json=gates[0],
328+
)
329+
gate = RectangleGate.create(
330+
EXP_ID,
331+
x_channel="FSC-A",
332+
y_channel="FSC-W",
333+
name="my gate",
334+
x1=60000,
335+
x2=200000,
336+
y1=75000,
337+
y2=215000,
338+
)
339+
payload = {
340+
"updated": [{"_id": "updatedGateId", "fcsFileId": "fileId"}],
341+
"inserted": [],
342+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
343+
}
344+
responses.add(
345+
responses.POST,
346+
f"{ENDPOINT_BASE}/experiments/{EXP_ID}/gates/applyTailored?gid={gate.gid}",
347+
json=payload,
348+
)
349+
350+
res = client.apply_tailoring(EXP_ID, gate, ["fileID"])
351+
assert res == {
352+
"updated": [{"_id": "updatedGateId", "fcsFileId": "fileId"}],
353+
"inserted": [],
354+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
355+
}

0 commit comments

Comments
 (0)