Skip to content

Commit 5ca3f0d

Browse files
committed
global accessor works, but namespaces still a problem
1 parent 085a477 commit 5ca3f0d

File tree

5 files changed

+179
-34
lines changed

5 files changed

+179
-34
lines changed

cellengine/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,28 @@
22
import requests
33
from requests_toolbelt import sessions
44

5-
BASE_URL = os.environ.get('CELLENGINE_DEVELOPMENT', 'https://cellengine.com/api/v1/')
5+
BASE_URL = os.environ.get("CELLENGINE_DEVELOPMENT", "https://cellengine.com/api/v1/")
66
ID_INDEX = 0
77

88
session = sessions.BaseUrlSession(base_url=BASE_URL)
9-
session.headers.update({'User-Agent': "CellEngine Python API Toolkit/0.1.1 requests/{0}".format(requests.__version__)})
9+
session.headers.update(
10+
{
11+
"User-Agent": "CellEngine Python API Toolkit/0.1.1 requests/{0}".format(
12+
requests.__version__
13+
)
14+
}
15+
)
1016

1117
from .client import Client
1218
from .experiment import Experiment
1319
from .population import Population
1420
from .compensation import Compensation
1521
from .fcsfile import FcsFile
1622
from .gate import Gate
17-
from .loader import Loader
23+
24+
# from .loader import Loader
25+
# from .byname import by_name
26+
from .loader import by_name
27+
28+
cache_info = by_name.cache_info
29+
clear_cache = by_name.cache_clear

cellengine/client.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Client(object):
2929
Returns:
3030
client: Authenticated client object
3131
"""
32+
3233
username = attr.ib(default=None)
3334
password = attr.ib(default=None, repr=False)
3435
token = attr.ib(default=None, repr=False)
@@ -40,14 +41,13 @@ def __attrs_post_init__(self):
4041
if self.password is None:
4142
self.password = getpass()
4243

43-
req = session.post("signin", {
44-
"username": self.username,
45-
"password": self.password
46-
})
44+
req = session.post(
45+
"signin", {"username": self.username, "password": self.password}
46+
)
4747
req.raise_for_status()
4848

4949
if req.status_code == 200:
50-
print('Authentication successful.')
50+
print("Authentication successful.")
5151

5252
elif self.token is not None:
5353
session.cookies.update({"token": "{0}".format(self.token)})
@@ -68,4 +68,4 @@ def get_experiment(self, _id=None, name=None):
6868
@property
6969
def experiments(self):
7070
"""Return a list of Experiment objects for all experiments on client"""
71-
return _helpers.base_list('experiments', Experiment)
71+
return _helpers.base_list("experiments", Experiment)

cellengine/loader.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import attr
22
from functools import lru_cache
3+
34
# TODO: sort out these imports
45
from .experiment import Experiment
56
from .fcsfile import FcsFile
67
from cellengine import _helpers
78

89

9-
@lru_cache(maxsize=None)
10-
def lookup_by_name_or_cached_id(path, query, name):
11-
"""Cache the ID of an item looked up by name."""
12-
url = "{0}?query=eq({1},\"{2}\")&limit=2".format(path, query, name)
10+
@lru_cache(maxsize=2048)
11+
def by_name(path, query, name):
12+
"""Look up an item by name and cache it's ID for future requests."""
13+
url = '{0}?query=eq({1},"{2}")&limit=2'.format(path, query, name)
1314
content = handle_response(_helpers.base_get(url))
14-
return content['_id']
15+
return content["_id"]
1516

1617

1718
def handle_response(response):
19+
print(by_name.cache_info())
1820
if type(response) is list:
1921
handle_list(response)
2022
else:
@@ -38,17 +40,23 @@ class Loader(object):
3840
made. To improve performance, the resource's id is then cached and a
3941
subsequent request is made with the id rather than the name.
4042
41-
42-
Resources such as files that exist within an experiment are cached within
43-
the experiment's scope. That is, the following is safe, even though the FCS
43+
Resources such as files that exist within an experiment are NOT cached within
44+
the experiment's scope. That is, the following is NOT safe, because FCS
4445
files have the same name:
4546
4647
exp.get_fcsfile(name="fcsfile1.fcs")
4748
exp2.get_fcsfile(name="fcsfile1.fcs")
49+
50+
TODO: I would like the above to read:
51+
52+
'Resources such as files that exist within an experiment are cached within
53+
the experiment's scope. That is, the following is safe, even though the FCS
54+
files have the same name:'
4855
"""
56+
4957
_id = attr.ib(kw_only=True, default=None)
5058
name = attr.ib(kw_only=True, default=None)
51-
path = attr.ib(init=False, default='experiments')
59+
path = attr.ib(init=False, default="experiments")
5260

5361
def load(self):
5462
if self._id:
@@ -65,7 +73,7 @@ def load_by_name(self):
6573
return self.make_class(content)
6674

6775
def lookup_and_cache_id(self, path, query, name):
68-
_id = lookup_by_name_or_cached_id(path, query, name)
76+
_id = by_name(path, query, name)
6977
url = "/".join([path, _id])
7078
return _helpers.base_get(url)
7179

@@ -79,21 +87,21 @@ def make_class(self, content):
7987
return [self.classname(properties=item) for item in content]
8088

8189
def lookup_item_by_query(self, path, query, name):
82-
url = "{0}?query=eq({1},\"{2}\")&limit=2".format(path, query, name)
90+
url = '{0}?query=eq({1},"{2}")&limit=2'.format(path, query, name)
8391
return _helpers.base_get(url)
8492

8593

8694
@attr.s
8795
class ExperimentLoader(Loader):
8896
classname = attr.ib(kw_only=True, default=Experiment, repr=False)
89-
query = attr.ib(kw_only=True, default='name')
97+
query = attr.ib(kw_only=True, default="name")
9098

9199

92100
@attr.s
93101
class FcsFileLoader(Loader):
94102
experiment_id = attr.ib(kw_only=True, default=None)
95103
classname = attr.ib(kw_only=True, default=FcsFile)
96-
query = attr.ib(kw_only=True, default='filename')
104+
query = attr.ib(kw_only=True, default="filename")
97105
path = attr.ib(init=False)
98106

99107
@path.default

tests/unit/test_lru_cache.py

Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,75 @@
11
import os
22
import responses
3-
from cellengine import _helpers
3+
import cellengine
44
from cellengine import loader
55

6+
# TODO: pull the double-responses from a name request into a fixture
7+
# see the `responses` documentation on pytest fixtures
8+
69

710
base_url = os.environ.get("CELLENGINE_DEVELOPMENT", "https://cellengine.com/api/v1/")
811

912

1013
@responses.activate
11-
def test_get_experiment_cache(client, experiments, fcsfiles, experiment):
14+
def test_lru_cache(client, experiments):
1215
"""Test to see if the LRU cache added hits. Multiple queries of the same
1316
object should add hits and new queries should add a miss."""
17+
loader.by_name.cache_clear()
18+
19+
responses.add(responses.GET, base_url + "experiments", json=experiments[0])
20+
responses.add(
21+
responses.GET,
22+
base_url + "experiments/5d38a6f79fae87499999a74b",
23+
json=experiments[0],
24+
)
25+
exp1 = client.get_experiment(name="test_experiment")
26+
assert loader.by_name.cache_info().misses == 1
27+
28+
responses.add(responses.GET, base_url + "experiments", json=experiments[0])
29+
responses.add(
30+
responses.GET,
31+
base_url + "experiments/5d38a6f79fae87499999a74b",
32+
json=experiments[0],
33+
)
34+
exp1 = client.get_experiment(name="test_experiment")
35+
assert loader.by_name.cache_info().misses == 1
36+
assert loader.by_name.cache_info().hits == 1
37+
38+
responses.add(responses.GET, base_url + "experiments", json=experiments[1])
39+
responses.add(
40+
responses.GET,
41+
base_url + "experiments/5d38a6f79fae87499999a74b",
42+
json=experiments[1],
43+
)
44+
exp2 = client.get_experiment(name="test_experiment-1")
45+
assert loader.by_name.cache_info().misses == 2
46+
47+
48+
@responses.activate
49+
def test_lru_cache_paths(client, experiments, fcsfiles, experiment):
50+
"""Test whether the cache returns a url with a name query on the first
51+
request and then an ID on the second request."""
52+
loader.by_name.cache_clear()
53+
1454
responses.add(responses.GET, base_url + "experiments", json=experiments[0])
1555
responses.add(
1656
responses.GET,
1757
base_url + "experiments/5d38a6f79fae87499999a74b",
1858
json=experiments[0],
1959
)
20-
loader.lookup_by_name_or_cached_id.cache_clear()
2160

2261
exp = client.get_experiment(name="test_experiment")
2362
assert (
2463
responses.calls[0].request.url
2564
== base_url + "experiments?query=eq(name,%22test_experiment%22)&limit=2"
2665
)
27-
assert loader.lookup_by_name_or_cached_id.cache_info().misses == 1
2866

2967
responses.add(responses.GET, base_url + "experiments", json=experiments[0])
3068
exp_copy = client.get_experiment(name="test_experiment")
31-
assert loader.lookup_by_name_or_cached_id.cache_info().hits == 1
32-
33-
responses.add(responses.GET, base_url + "experiments", json=experiments[0])
34-
exp_copy2 = client.get_experiment(name="test_experiment")
35-
assert loader.lookup_by_name_or_cached_id.cache_info().hits == 2
69+
assert (
70+
responses.calls[1].request.url
71+
== base_url + "experiments/5d38a6f79fae87499999a74b"
72+
)
3673

3774
responses.add(responses.GET, base_url + "experiments", json=[experiments[1]])
3875
responses.add(
@@ -41,6 +78,85 @@ def test_get_experiment_cache(client, experiments, fcsfiles, experiment):
4178
json=experiments[1],
4279
)
4380
different_exp = client.get_experiment(name="pytest_experiment")
44-
assert different_exp.name == "pytest_experiment"
45-
assert loader.lookup_by_name_or_cached_id.cache_info().misses == 2
46-
assert loader.lookup_by_name_or_cached_id.cache_info().hits == 2
81+
assert (
82+
responses.calls[2].request.url
83+
== base_url + "experiments/5d38a6f79fae87499999a74b"
84+
)
85+
86+
87+
@responses.activate
88+
def test_different_item_cache_accessor(client, experiments, fcsfiles):
89+
"""Test to see if the LRU cache adds hits. Multiple queries of the same
90+
object should add hits and new queries should add a miss.
91+
92+
Multiple queries of files of the same name in different experiments should
93+
register as different queries."""
94+
loader.by_name.cache_clear()
95+
96+
responses.add(responses.GET, base_url + "experiments", json=experiments[0])
97+
responses.add(
98+
responses.GET,
99+
base_url + "experiments/5d38a6f79fae87499999a74b",
100+
json=experiments[0],
101+
)
102+
exp1 = client.get_experiment(name="test_experiment")
103+
104+
responses.add(
105+
responses.GET,
106+
base_url + "experiments/5d38a6f79fae87499999a74b/fcsfiles",
107+
json=fcsfiles[0],
108+
)
109+
responses.add(
110+
responses.GET,
111+
base_url
112+
+ "experiments/5d38a6f79fae87499999a74b/fcsfiles/5d64abe2ca9df61349ed8e79",
113+
json=fcsfiles[0],
114+
)
115+
file1 = exp1.get_fcsfile(name="Specimen_001_A12_A12.fcs")
116+
assert loader.by_name.cache_info().misses == 2
117+
118+
responses.add(responses.GET, base_url + "experiments", json=experiments[0])
119+
responses.add(
120+
responses.GET,
121+
base_url + "experiments/5d38a6f79fae87499999a74b",
122+
json=experiments[0],
123+
)
124+
exp2 = client.get_experiment(name="test_experiment-1")
125+
assert loader.by_name.cache_info().misses == 3
126+
127+
responses.add(
128+
responses.GET,
129+
base_url + "experiments/5d38a6f79fae87499999a74b/fcsfiles",
130+
json=fcsfiles[0],
131+
)
132+
responses.add(
133+
responses.GET,
134+
base_url
135+
+ "experiments/5d38a6f79fae87499999a74b/fcsfiles/5d64abe2ca9df61349ed8e79",
136+
json=fcsfiles[0],
137+
)
138+
file2 = exp2.get_fcsfile(name="Specimen_001_A12_A12.fcs")
139+
assert (
140+
loader.by_name.cache_info().misses == 4
141+
) # not 3 because this is a different experiment
142+
143+
144+
@responses.activate
145+
def test_global_cache_accessor(client, experiments):
146+
loader.by_name.cache_clear()
147+
assert cellengine.cache_info().misses == 0
148+
assert loader.by_name.cache_info().misses == 0
149+
150+
responses.add(responses.GET, base_url + "experiments", json=experiments[0])
151+
responses.add(
152+
responses.GET,
153+
base_url + "experiments/5d38a6f79fae87499999a74b",
154+
json=experiments[0],
155+
)
156+
exp1 = client.get_experiment(name="test_experiment")
157+
assert cellengine.cache_info().misses == 1
158+
assert loader.by_name.cache_info().misses == 1
159+
160+
cellengine.clear_cache()
161+
assert cellengine.cache_info().misses == 0
162+
assert loader.by_name.cache_info().misses == 0
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# WIP
2+
3+
4+
# def test_passes_when_client_activated(client, experiment):
5+
# exp1 = client.get_experiment(name="test_experiment")
6+
7+
8+
# def test_fails_when_client_not_activated(client, experiment):
9+
# exp1 = client.get_experiment(name="test_experiment")

0 commit comments

Comments
 (0)