Skip to content

Commit 91ce248

Browse files
committed
refactor(gate.py): Synthesize gates from self
Instead of requesting gates in a loop, per #140 (comment)
1 parent 602c716 commit 91ce248

File tree

3 files changed

+33
-66
lines changed

3 files changed

+33
-66
lines changed

cellengine/resources/gate.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import annotations
22
import importlib
3-
from cellengine.utils.types import ApplyTailoringRes
3+
from cellengine.utils.types import (
4+
ApplyTailoringInsert,
5+
ApplyTailoringUpdate,
6+
)
47
from collections import defaultdict
58
from math import pi
69
from operator import itemgetter
@@ -127,26 +130,27 @@ def update_gate_family(experiment_id: str, gid: str, body: Dict) -> None:
127130
if res["nModified"] < 1:
128131
raise Warning("No gates updated.")
129132

130-
def apply_tailoring(self, fcs_file_ids: List[str]):
133+
def apply_tailoring(self, fcs_file_ids: List[str]) -> Dict[str, List[Gate]]:
131134
"""Tailor this gate to a specific FCS file or files."""
132-
res = ce.APIClient().apply_tailoring(self.experiment_id, self, fcs_file_ids)
133-
return self._make_tailored_gates(res)
135+
payload = ce.APIClient().apply_tailoring(self.experiment_id, self, fcs_file_ids)
134136

135-
def _make_tailored_gates(self, payload: ApplyTailoringRes) -> Dict[str, List[Gate]]:
136137
ret = defaultdict(list)
137138
for k, v in payload.items():
138139
if v:
139140
if k == "deleted":
140-
[ret[k].append(i["_id"]) for i in v]
141+
[ret[k].append(i["_id"]) for i in v] # type: ignore
141142
else:
142-
[
143-
ret[k].append(
144-
ce.APIClient().get_gate(self.experiment_id, i["_id"])
145-
)
146-
for i in v
147-
]
143+
[ret[k].append(self._synthesize_gate(i)) for i in v] # type: ignore
148144
return dict(ret)
149145

146+
def _synthesize_gate(
147+
self,
148+
payload: Union[ApplyTailoringInsert, ApplyTailoringUpdate],
149+
):
150+
gate = self.to_dict()
151+
gate.update(payload)
152+
return Gate.from_dict(gate)
153+
150154

151155
class RectangleGate(Gate):
152156
@classmethod

cellengine/utils/types.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from __future__ import annotations
22
import sys
3-
from typing import Dict, List
43

54
if sys.version_info >= (3, 8):
65
from typing import TypedDict
76
else:
87
from typing_extensions import TypedDict
98

9+
ApplyTailoringInsert = TypedDict("inserted", {"_id": str, "fcsFileId": str})
10+
ApplyTailoringUpdate = TypedDict("updated", {"_id": str, "fcsFileId": str})
11+
ApplyTailoringDelete = TypedDict("deleted", {"_id": str, "fcsFileId": str})
1012
ApplyTailoringRes = TypedDict(
1113
"ApplyTailoringRes",
1214
{
13-
"inserted": TypedDict("inserted", {"_id": str, "fcsFileId": str}),
14-
"updated": TypedDict("updated", {"_id": str, "fcsFileId": str}),
15-
"deleted": TypedDict("deleted", {"_id": str, "fcsFileId": str}),
15+
"inserted": ApplyTailoringInsert,
16+
"updated": ApplyTailoringUpdate,
17+
"deleted": ApplyTailoringDelete,
1618
},
1719
)

tests/unit/resources/test_gates.py

Lines changed: 11 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import json
22
import pytest
33
import responses
4-
from numpy import array
54

65
from cellengine.resources.gate import (
76
Gate,
@@ -12,7 +11,7 @@
1211
RangeGate,
1312
SplitGate,
1413
)
15-
from cellengine.utils.helpers import is_valid_id, normalize
14+
from cellengine.utils.helpers import is_valid_id
1615
from tests.unit.resources.test_population import population_tester
1716
from numpy import mean
1817

@@ -731,54 +730,12 @@ def test_create_split_gate(client, ENDPOINT_BASE, experiment, scalesets, split_g
731730
assert split_gate.model["labels"] == [[-199.9, 0.916], [0.9, 0.916]]
732731

733732

734-
@responses.activate
735-
def test_make_tailored_gates(client, ENDPOINT_BASE, monkeypatch, rectangle_gate):
736-
def mock_get_gate(exp_id, _id):
737-
return "Gate"
738-
739-
monkeypatch.setattr(client, "get_gate", mock_get_gate)
740-
741-
responses.add(
742-
responses.POST,
743-
f"{ENDPOINT_BASE}/experiments/{EXP_ID}/gates",
744-
status=201,
745-
json=rectangle_gate,
746-
)
747-
gate = RectangleGate.create(
748-
experiment_id=EXP_ID,
749-
x_channel="FSC-A",
750-
y_channel="FSC-W",
751-
name="my gate",
752-
x1=60000,
753-
x2=200000,
754-
y1=75000,
755-
y2=215000,
756-
)
757-
758-
payload = {
759-
"updated": [{"_id": "updatedGateId", "fcsFileId": "fileId"}],
760-
"inserted": [],
761-
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
762-
}
763-
764-
res = gate._make_tailored_gates(payload)
765-
assert res == {
766-
"updated": ["Gate"],
767-
"deleted": ["deletedGateId"],
768-
}
769-
770-
771733
@responses.activate
772734
def test_apply_tailoring(client, monkeypatch, ENDPOINT_BASE, rectangle_gate, fcs_files):
773-
def mock_get_gate(exp_id, _id):
774-
return "Gate"
775-
776-
monkeypatch.setattr(client, "get_gate", mock_get_gate)
777-
778735
payload = {
779736
"updated": [
780-
{"_id": "updatedGateId", "fcsFileId": "fileId"},
781-
{"_id": "updatedGateId", "fcsFileId": "fileId"},
737+
{"_id": "updatedGateId1", "fcsFileId": "fileId1"},
738+
{"_id": "updatedGateId2", "fcsFileId": "fileId2"},
782739
],
783740
"inserted": [],
784741
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
@@ -808,7 +765,11 @@ def mock_get_gate(exp_id, _id):
808765
)
809766

810767
res = gate.apply_tailoring([fcs_files[0]["_id"]])
811-
assert res == {
812-
"updated": ["Gate", "Gate"],
813-
"deleted": ["deletedGateId"],
814-
}
768+
769+
assert res["deleted"] == ["deletedGateId"]
770+
assert isinstance(res["updated"], list)
771+
assert len(res["updated"]) == 2
772+
assert res["updated"][0]._id == "updatedGateId1"
773+
assert res["updated"][1]._id == "updatedGateId2"
774+
assert res["updated"][0].fcs_file_id == "fileId1"
775+
assert res["updated"][1].fcs_file_id == "fileId2"

0 commit comments

Comments
 (0)