Skip to content

Commit ac6f89a

Browse files
committed
feat(apiclient, gate): Add apply_tailoring function
1 parent 7199a93 commit ac6f89a

File tree

5 files changed

+131
-12
lines changed

5 files changed

+131
-12
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

@@ -77,10 +78,25 @@ def update_gate_family(experiment_id, gid: str, body: Dict):
7778
if res["nModified"] < 1:
7879
raise Warning("No gates updated.")
7980

80-
def tailor_to(self, fcs_file: FcsFile):
81-
self.tailored_per_file = True
82-
self.fcs_file_id = fcs_file._id
83-
self.update()
81+
def apply_tailoring(self, fcs_file_ids: List[str]):
82+
"""Tailor this gate to a specific FCS file or files."""
83+
res = ce.APIClient().apply_tailoring(self.experiment_id, self, fcs_file_ids)
84+
return self._make_tailored_gates(res)
85+
86+
def _make_tailored_gates(self, payload: TailoringDict) -> Dict[str, List[Gate]]:
87+
ret = defaultdict(list)
88+
for k, v in payload.items():
89+
if v:
90+
if k == "deleted":
91+
[ret[k].append(i["_id"]) for i in v]
92+
else:
93+
[
94+
ret[k].append(
95+
ce.APIClient().get_gate(self.experiment_id, i["_id"])
96+
)
97+
for i in v
98+
]
99+
return dict(ret)
84100

85101

86102
@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
@@ -515,12 +516,15 @@ def update_gate_family(self, experiment_id, gid, body: dict = None) -> dict:
515516
json=body,
516517
)
517518

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

525529
def get_plot(
526530
self,

cellengine/utils/types.py

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

tests/unit/resources/test_gates.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,3 +496,54 @@ def test_create_split_gate(client, ENDPOINT_BASE, experiment, scalesets, split_g
496496
]
497497
assert split_gate.names == ["my gate (L)", "my gate (R)"]
498498
assert split_gate.model["labels"] == [[-199.9, 0.916], [0.9, 0.916]]
499+
500+
501+
def test_make_tailored_gates(client, monkeypatch, rectangle_gate):
502+
def mock_get_gate(exp_id, _id):
503+
return "Gate"
504+
505+
monkeypatch.setattr(client, "get_gate", mock_get_gate)
506+
507+
gate = Gate.factory(rectangle_gate)
508+
509+
payload = {
510+
"updated": [{"_id": "updatedGateId", "fcsFileId": "fileId"}],
511+
"inserted": [],
512+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
513+
}
514+
515+
res = gate._make_tailored_gates(payload)
516+
assert res == {
517+
"updated": ["Gate"],
518+
"deleted": ["deletedGateId"],
519+
}
520+
521+
522+
@responses.activate
523+
def test_apply_tailoring(client, monkeypatch, ENDPOINT_BASE, rectangle_gate, fcs_files):
524+
def mock_get_gate(exp_id, _id):
525+
return "Gate"
526+
527+
monkeypatch.setattr(client, "get_gate", mock_get_gate)
528+
529+
payload = {
530+
"updated": [
531+
{"_id": "updatedGateId", "fcsFileId": "fileId"},
532+
{"_id": "updatedGateId", "fcsFileId": "fileId"},
533+
],
534+
"inserted": [],
535+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
536+
}
537+
gate = Gate.factory(rectangle_gate)
538+
539+
responses.add(
540+
responses.POST,
541+
f"{ENDPOINT_BASE}/experiments/{EXP_ID}/gates/applyTailored?gid={gate.gid}",
542+
json=payload,
543+
)
544+
545+
res = gate.apply_tailoring([fcs_files[0]["_id"]])
546+
assert res == {
547+
"updated": ["Gate", "Gate"],
548+
"deleted": ["deletedGateId"],
549+
}

tests/unit/utils/test_api_client.py

Lines changed: 32 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,34 @@ 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):
324+
gate = RectangleGate.create(
325+
EXP_ID,
326+
x_channel="FSC-A",
327+
y_channel="FSC-W",
328+
name="my gate",
329+
x1=60000,
330+
x2=200000,
331+
y1=75000,
332+
y2=215000,
333+
)
334+
payload = {
335+
"updated": [{"_id": "updatedGateId", "fcsFileId": "fileId"}],
336+
"inserted": [],
337+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
338+
}
339+
responses.add(
340+
responses.POST,
341+
f"{ENDPOINT_BASE}/experiments/{EXP_ID}/gates/applyTailored?gid={gate.gid}",
342+
json=payload,
343+
)
344+
345+
res = client.apply_tailoring(EXP_ID, gate, ["fileID"])
346+
assert res == {
347+
"updated": [{"_id": "updatedGateId", "fcsFileId": "fileId"}],
348+
"inserted": [],
349+
"deleted": [{"_id": "deletedGateId", "fcsFileId": "fileId"}],
350+
}

0 commit comments

Comments
 (0)