Skip to content

Commit d5fcb84

Browse files
committed
added by_name loader with LRU cache
added lru_caching to by-name lookups global accessor works, but namespaces still a problem cache works for different namespaces, all tests pass
1 parent c99a606 commit d5fcb84

17 files changed

+674
-284
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,12 @@ experiments = client.experiments
1717
exp = cellengine.Experiment(name="160311-96plex-4dye")
1818
```
1919

20+
##Developer Notes
21+
- `id` is a python builtin, which causes some confusion. We use `_id` to indicate
22+
the ID of an API object, but the `attrs` package does not accept leading
23+
underscores (i.e. an `_id = attr.ib()` in a class is treated by `attrs` as the
24+
string "id"). Practically, this means:
25+
- pass `_id` to functions that take an ID as an argument.
26+
- pass `id` to `attrs` classes when instantiating them.
27+
- pass `properties` to `attrs` classes when instantiating them.
2028

cellengine/Gates/gate_util.py

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
from cellengine import loader
12
import re
2-
3-
from ..fcsfile import FcsFile
43
from .. import gate # circular import here
54
from .. import _helpers
65

6+
cellengine = __import__(__name__.split(".")[0])
7+
78

8-
def common_gate_create(experiment_id, body, tailored_per_file, fcs_file_id,
9-
fcs_file, create_population):
9+
def common_gate_create(
10+
experiment_id, body, tailored_per_file, fcs_file_id, fcs_file, create_population
11+
):
1012
"""
1113
<Description>
1214
@@ -46,40 +48,37 @@ def common_gate_create(experiment_id, body, tailored_per_file, fcs_file_id,
4648
Example:
4749
<Example>
4850
"""
49-
body = parse_fcs_file_args(experiment_id, body, tailored_per_file,
50-
fcs_file_id, fcs_file)
51-
52-
body = _helpers.convert_dict(body, 'snake_to_camel')
53-
res = _helpers.base_create(gate.Gate,
54-
url="experiments/{0}/gates".format(experiment_id),
55-
expected_status=201,
56-
json=body, params={'createPopulation':
57-
create_population}
58-
)
51+
body = parse_fcs_file_args(
52+
experiment_id, body, tailored_per_file, fcs_file_id, fcs_file
53+
)
54+
55+
body = _helpers.convert_dict(body, "snake_to_camel")
56+
res = _helpers.base_create(
57+
gate.Gate,
58+
url="experiments/{0}/gates".format(experiment_id),
59+
expected_status=201,
60+
json=body,
61+
params={"createPopulation": create_population},
62+
)
5963
return res
6064

6165

62-
def parse_fcs_file_args(experiment_id, body, tailored_per_file, fcs_file_id,
63-
fcs_file):
66+
def parse_fcs_file_args(experiment_id, body, tailored_per_file, fcs_file_id, fcs_file):
6467
"""Find the fcs file ID if 'tailored_per_file' and either 'fcs_file' or
6568
'fcs_file_id' are specified."""
6669
if fcs_file is not None and fcs_file_id is not None:
6770
raise ValueError("Please specify only 'fcs_file' or 'fcs_file_id'.")
6871
if fcs_file is not None and tailored_per_file is True: # lookup by name
6972
_file = get_fcsfile(experiment_id, name=fcs_file)
7073
fcs_file_id = _file._id
71-
body['tailoredPerFile'] = tailored_per_file
72-
body['fcsFileId'] = fcs_file_id
74+
body["tailoredPerFile"] = tailored_per_file
75+
body["fcsFileId"] = fcs_file_id
7376
return body
7477

7578

7679
def get_fcsfile(experiment_id, _id=None, name=None):
77-
if _id:
78-
content = _helpers.base_get("experiments/{0}/fcsfiles/{1}".format(experiment_id, _id))
79-
content = FcsFile(properties=content)
80-
else:
81-
content = _helpers.load_fcsfile_by_name(experiment_id, name)
82-
return content
80+
fcs_loader = loader.FcsFileLoader(experiment_id=experiment_id, id=_id, name=name)
81+
return fcs_loader.load()
8382

8483

8584
def create_gates(experiment_id=None, gates=None, create_all_populations=True):
@@ -108,8 +107,9 @@ def create_gates(experiment_id=None, gates=None, create_all_populations=True):
108107
``help(cellengine.Gate.<Gate Type>)``.
109108
fcs_file_id (optional): ``str``: ID of FCS file, if tailored per file. Use
110109
``None`` for the global gate in the tailored gate group.
111-
tailored_per_file (optional): ``bool``: Whether this gate is tailored per FCS file.
112-
names (optional): ``list(str)``: For compound gates, a list of gate names.
110+
tailored_per_file (optional): ``bool``: Whether this gate is
111+
tailored per FCS file. names (optional): ``list(str)``: For
112+
compound gates, a list of gate names.
113113
create_population (optional): Whether to create populations for each gate.
114114
create_populations: Whether to create populations for all gates. If set
115115
to False, ``create_population`` may be specified for each gate.
@@ -119,23 +119,25 @@ def create_gates(experiment_id=None, gates=None, create_all_populations=True):
119119
"""
120120
prepared_gates = []
121121
for g in gates:
122-
g.update({'_id': _helpers.generate_id()})
123-
if 'gid' not in g.keys():
124-
g.update({'gid': _helpers.generate_id()})
122+
g.update({"_id": _helpers.generate_id()})
123+
if "gid" not in g.keys():
124+
g.update({"gid": _helpers.generate_id()})
125125
prepared_gates.append(g)
126126

127127
new_gates = []
128128
for each_gate in prepared_gates:
129129
if create_all_populations is False:
130-
create_populations = each_gate.get('create_population', False)
130+
create_populations = each_gate.get("create_population", False)
131131
else:
132132
create_populations = create_all_populations
133-
new_gate = common_gate_create(experiment_id=each_gate.get('experiment_id', experiment_id),
134-
body=each_gate,
135-
tailored_per_file=each_gate.get('tailored_per_file', False),
136-
fcs_file_id=each_gate.get('fcs_file_id', None),
137-
fcs_file=each_gate.get('fcs_file', None),
138-
create_population=create_populations)
133+
new_gate = common_gate_create(
134+
experiment_id=each_gate.get("experiment_id", experiment_id),
135+
body=each_gate,
136+
tailored_per_file=each_gate.get("tailored_per_file", False),
137+
fcs_file_id=each_gate.get("fcs_file_id", None),
138+
fcs_file=each_gate.get("fcs_file", None),
139+
create_population=create_populations,
140+
)
139141
new_gates.append(new_gate)
140142

141143
return new_gates
@@ -175,12 +177,14 @@ def delete_gates(experiment_id, _id=None, gid=None, exclude=None):
175177

176178

177179
def gate_style(prnt_doc, child_doc):
178-
desc = child_doc[:child_doc.index('Args')].strip()
179-
args = child_doc[child_doc.index('Args')+5:child_doc.index('Returns')].strip()
180-
args = re.sub('\n', '\n ', args)
181-
returns = child_doc[child_doc.index('Returns')+10:child_doc.index('Example')].strip()
182-
example = child_doc[child_doc.index('Example')+10:].strip()
183-
keys = ['<Description>', '<Gate Args>', '<Returns>', '<Example>']
180+
desc = child_doc[: child_doc.index("Args")].strip()
181+
args = child_doc[child_doc.index("Args") + 5 : child_doc.index("Returns")].strip()
182+
args = re.sub("\n", "\n ", args)
183+
returns = child_doc[
184+
child_doc.index("Returns") + 10 : child_doc.index("Example")
185+
].strip()
186+
example = child_doc[child_doc.index("Example") + 10 :].strip()
187+
keys = ["<Description>", "<Gate Args>", "<Returns>", "<Example>"]
184188
sections = [desc, args, returns, example]
185189
docs = prnt_doc
186190
for key, section in zip(keys, sections):

cellengine/__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,26 @@
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
23+
24+
from .loader import by_name
25+
26+
cache_info = by_name.cache_info
27+
clear_cache = by_name.cache_clear

cellengine/_helpers.py

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
import time
44
import binascii
5+
from functools import lru_cache
56
from .client import session
67
from datetime import datetime
78
from cellengine import ID_INDEX
@@ -95,57 +96,6 @@ def today_timestamp():
9596
def created(self):
9697
return timestamp_to_datetime(self._properties.get("created"))
9798

98-
def load(self, path, query='name'):
99-
if self._id is None:
100-
load_by_name(self, query)
101-
else:
102-
content = base_get(path)
103-
if len(content) == 0:
104-
ValueError("Failed to load object from {0}".format(self.path))
105-
else:
106-
self._properties = content
107-
self.__dict__.update(self._properties)
108-
109-
110-
def load_by_id(_id):
111-
content = base_get("experiments/{0}".format(_id))
112-
return content
113-
114-
115-
def load_by_name(self, query):
116-
# TODO does requests encode URI components for us?
117-
url = "{0}?query=eq({1},\"{2}\")&limit=2".format(self.path, query, self.name)
118-
content = base_get(url)
119-
if len(content) == 0:
120-
raise RuntimeError("No objects found with the name {0}.".format(self.name))
121-
elif len(content) > 1:
122-
raise RuntimeError("Multiple objects found with the name {0}, use _id to query instead.".format(self.name))
123-
else:
124-
self._properties = content[0]
125-
self._id = self._properties.get("_id")
126-
self.__dict__.update(self._properties)
127-
128-
129-
def load_experiment_by_name(name):
130-
content = base_get("experiments?query=eq(name, \"{0}\")&limit=2".format(name))
131-
return load_object_by_name('__import__("cellengine").Experiment', content)
132-
133-
134-
def load_fcsfile_by_name(experiment_id, name=None):
135-
content = base_get("experiments/{0}/fcsfiles?query=eq(filename, \"{1}\")&limit=2".format(experiment_id, name))
136-
return load_object_by_name('__import__("cellengine").FcsFile', content)
137-
138-
139-
def load_object_by_name(classname, content):
140-
if len(content) == 0:
141-
raise RuntimeError("No objects found.")
142-
elif len(content) > 1:
143-
raise RuntimeError("Multiple objects found; use _id to query instead.")
144-
elif type(content) is list:
145-
return make_class(classname, content[0])
146-
else:
147-
return make_class(classname, content)
148-
14999

150100
def generate_id():
151101
"""Generates a hexadecimal ID based on a mongoDB ObjectId"""

cellengine/client.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from getpass import getpass
33
from . import session
44
from . import _helpers
5+
from .loader import ExperimentLoader
56
from .experiment import Experiment
67

78

@@ -28,6 +29,7 @@ class Client(object):
2829
Returns:
2930
client: Authenticated client object
3031
"""
32+
3133
username = attr.ib(default=None)
3234
password = attr.ib(default=None, repr=False)
3335
token = attr.ib(default=None, repr=False)
@@ -39,14 +41,13 @@ def __attrs_post_init__(self):
3941
if self.password is None:
4042
self.password = getpass()
4143

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

4849
if req.status_code == 200:
49-
print('Authentication successful.')
50+
print("Authentication successful.")
5051

5152
elif self.token is not None:
5253
session.cookies.update({"token": "{0}".format(self.token)})
@@ -55,14 +56,16 @@ def __attrs_post_init__(self):
5556
raise RuntimeError("Username or token must be provided")
5657

5758
def get_experiment(self, _id=None, name=None):
58-
if _id:
59-
content = _helpers.base_get("experiments/{0}".format(_id))
60-
content = Experiment(properties=content)
61-
else:
62-
content = _helpers.load_experiment_by_name(name)
63-
return content
59+
loader = ExperimentLoader(id=_id, name=name)
60+
return loader.load()
61+
# if _id:
62+
# content = _helpers.base_get("experiments/{0}".format(_id))
63+
# content = Experiment(properties=content)
64+
# else:
65+
# content = _helpers.load_experiment_by_name(name)
66+
# return content
6467

6568
@property
6669
def experiments(self):
6770
"""Return a list of Experiment objects for all experiments on client"""
68-
return _helpers.base_list('experiments', Experiment)
71+
return _helpers.base_list("experiments", Experiment)

0 commit comments

Comments
 (0)