Skip to content

Commit e84a6f2

Browse files
committed
feat(apiclient, gate): Add apply_tailoring function
1 parent a7907fe commit e84a6f2

File tree

5 files changed

+116
-11
lines changed

5 files changed

+116
-11
lines changed

cellengine/resources/gate.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import annotations
2+
from cellengine.utils.types import TailoringDict
3+
from collections import defaultdict
24
from math import pi
35
from typing import Dict, List, Optional
46

57
from attr import define, field
68
import numpy
79

810
import cellengine as ce
9-
from cellengine.resources.fcs_file import FcsFile
1011
from cellengine.utils import parse_fcs_file_args
1112
from cellengine.utils import converter, generate_id, is_valid_id, readonly
1213

@@ -89,10 +90,25 @@ def update_family(experiment_id, gid: str, body: Dict):
8990
if res["nModified"] < 1:
9091
raise Warning("No gates updated.")
9192

92-
def tailor_to(self, fcs_file: FcsFile):
93-
self.tailored_per_file = True
94-
self.fcs_file_id = fcs_file._id
95-
self.update()
93+
def apply_tailoring(self, fcs_file_ids: List[str], **kwargs):
94+
"""Tailor this gate to a specific FCS file or files."""
95+
res = ce.APIClient().apply_tailoring(self.experiment_id, self, fcs_file_ids)
96+
return self._make_tailored_gates(res)
97+
98+
def _make_tailored_gates(self, payload: TailoringDict) -> Dict[str, List[Gate]]:
99+
ret = defaultdict(list)
100+
for k, v in payload.items():
101+
if v:
102+
if k == "deleted":
103+
[ret[k].append(i["_id"]) for i in v]
104+
else:
105+
[
106+
ret[k].append(
107+
ce.APIClient().get_gate(self.experiment_id, i["_id"])
108+
)
109+
for i in v
110+
]
111+
return dict(ret)
96112

97113

98114
@define(repr=False)

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 TailoringDict
23
from functools import lru_cache
34
from getpass import getpass
45
import json
@@ -513,12 +514,15 @@ def update_gate_family(self, experiment_id, gid, body: dict = None) -> dict:
513514
json=body,
514515
)
515516

516-
def tailor_to(self, experiment_id, gate_id, fcs_file_id):
517-
"""Tailor a gate to a file."""
518-
gate = self.get_gate(experiment_id, gate_id, as_dict=True)
519-
gate["tailoredPerFile"] = True
520-
gate["fcsFileId"] = fcs_file_id
521-
return self.update_entity(experiment_id, gate_id, "gates", gate)
517+
def apply_tailoring(
518+
self, experiment_id: str, gate: Gate, fcs_file_ids: List[str]
519+
) -> TailoringDict:
520+
"""Tailor a gate to a file or files."""
521+
return self._post(
522+
f"{self.base_url}/experiments/{experiment_id}/gates/applyTailored",
523+
params={"gid": gate.gid},
524+
json={"gate": gate.to_dict(), "fcsFileIds": fcs_file_ids},
525+
)
522526

523527
def get_plot(
524528
self,

cellengine/utils/types.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from __future__ import annotations
2+
3+
from typing import Dict, List, TypedDict
4+
5+
TailoringDict = TypedDict(
6+
"TailoringDict",
7+
{
8+
"inserted": List[Dict[str, str]],
9+
"updated": List[Dict[str, str]],
10+
"deleted": List[Dict[str, str]],
11+
},
12+
)

tests/unit/resources/test_gates.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,54 @@ def test_create_split_gate(client, ENDPOINT_BASE, experiment, scalesets, split_g
444444
]
445445
assert split_gate.names == ["my gate (L)", "my gate (R)"]
446446
assert split_gate.model["labels"] == [[-199.9, 0.916], [0.9, 0.916]]
447+
448+
449+
def test_make_tailored_gates(client, monkeypatch, rectangle_gate):
450+
def mock_get_gate(exp_id, _id):
451+
return "Gate"
452+
453+
monkeypatch.setattr(client, "get_gate", mock_get_gate)
454+
455+
gate = Gate.factory(rectangle_gate)
456+
457+
payload = {
458+
"updated": [{"_id": "updatedGateId", "fcsFileId": "fileId"}],
459+
"inserted": [],
460+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
461+
}
462+
463+
res = gate._make_tailored_gates(payload)
464+
assert res == {
465+
"updated": ["Gate"],
466+
"deleted": ["deletedGateId"],
467+
}
468+
469+
470+
@responses.activate
471+
def test_apply_tailoring(client, monkeypatch, ENDPOINT_BASE, rectangle_gate, fcs_files):
472+
def mock_get_gate(exp_id, _id):
473+
return "Gate"
474+
475+
monkeypatch.setattr(client, "get_gate", mock_get_gate)
476+
477+
payload = {
478+
"updated": [
479+
{"_id": "updatedGateId", "fcsFileId": "fileId"},
480+
{"_id": "updatedGateId", "fcsFileId": "fileId"},
481+
],
482+
"inserted": [],
483+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
484+
}
485+
gate = Gate.factory(rectangle_gate)
486+
487+
responses.add(
488+
responses.POST,
489+
f"{ENDPOINT_BASE}/experiments/{EXP_ID}/gates/applyTailored?gid={gate.gid}",
490+
json=payload,
491+
)
492+
493+
res = gate.apply_tailoring([fcs_files[0]["_id"]])
494+
assert res == {
495+
"updated": ["Gate", "Gate"],
496+
"deleted": ["deletedGateId"],
497+
}

tests/unit/utils/test_api_client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,25 @@ def test_should_get_statistics(client, ENDPOINT_BASE, statistics):
291291
population_ids="some population id",
292292
)
293293
assert set(expected_query_body) == set(json.loads(responses.calls[0].request.body))
294+
295+
296+
@responses.activate
297+
def test_apply_tailoring(client, ENDPOINT_BASE, rectangle_gate):
298+
gate = Gate.factory(rectangle_gate)
299+
payload = {
300+
"updated": [{"_id": "updatedGateId", "fcsFileId": "fileId"}],
301+
"inserted": [],
302+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
303+
}
304+
responses.add(
305+
responses.POST,
306+
f"{ENDPOINT_BASE}/experiments/{EXP_ID}/gates/applyTailored?gid={gate.gid}",
307+
json=payload,
308+
)
309+
310+
res = client.apply_tailoring(EXP_ID, gate, ["fileID"])
311+
assert res == {
312+
"updated": [{"_id": "updatedGateId", "fcsFileId": "fileId"}],
313+
"inserted": [],
314+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
315+
}

0 commit comments

Comments
 (0)