brainstem-python-api-tools 0.2.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- brainstem_api_tools/__init__.py +3 -1
- brainstem_api_tools/brainstem_api_client.py +867 -219
- brainstem_api_tools/cli.py +180 -0
- brainstem_python_api_tools-2.0.0.dist-info/METADATA +179 -0
- brainstem_python_api_tools-2.0.0.dist-info/RECORD +9 -0
- {brainstem_python_api_tools-0.2.0.dist-info → brainstem_python_api_tools-2.0.0.dist-info}/WHEEL +1 -1
- brainstem_python_api_tools-2.0.0.dist-info/entry_points.txt +2 -0
- brainstem_python_api_tools-0.2.0.dist-info/METADATA +0 -54
- brainstem_python_api_tools-0.2.0.dist-info/RECORD +0 -7
- {brainstem_python_api_tools-0.2.0.dist-info → brainstem_python_api_tools-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {brainstem_python_api_tools-0.2.0.dist-info → brainstem_python_api_tools-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,43 +1,104 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import json
|
|
2
|
+
import stat
|
|
3
|
+
import time
|
|
4
|
+
import webbrowser
|
|
6
5
|
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Union
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
9
|
+
import requests
|
|
10
|
+
from requests.adapters import HTTPAdapter
|
|
11
|
+
from requests.models import Response
|
|
12
|
+
from urllib3.util.retry import Retry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Model → app routing table (single source of truth)
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
_MODEL_TO_APP: dict = {
|
|
20
|
+
# stem
|
|
21
|
+
"project": "stem",
|
|
22
|
+
"subject": "stem",
|
|
23
|
+
"session": "stem",
|
|
24
|
+
"collection": "stem",
|
|
25
|
+
"cohort": "stem",
|
|
26
|
+
"breeding": "stem",
|
|
27
|
+
"project_membership_invitation": "stem",
|
|
28
|
+
"project_group_membership_invitation": "stem",
|
|
29
|
+
# modules
|
|
30
|
+
"procedure": "modules",
|
|
31
|
+
"behavior": "modules",
|
|
32
|
+
"dataacquisition": "modules",
|
|
33
|
+
"manipulation": "modules",
|
|
34
|
+
"equipment": "modules",
|
|
35
|
+
"consumablestock": "modules",
|
|
36
|
+
"procedurelog": "modules",
|
|
37
|
+
"subjectlog": "modules",
|
|
38
|
+
# personal_attributes
|
|
39
|
+
"behavioralassay": "personal_attributes",
|
|
40
|
+
"datastorage": "personal_attributes",
|
|
41
|
+
"inventory": "personal_attributes",
|
|
42
|
+
"license": "personal_attributes",
|
|
43
|
+
"protocol": "personal_attributes",
|
|
44
|
+
"setup": "personal_attributes",
|
|
45
|
+
# resources
|
|
46
|
+
"consumable": "resources",
|
|
47
|
+
"hardwaredevice": "resources",
|
|
48
|
+
"supplier": "resources",
|
|
49
|
+
# taxonomies
|
|
50
|
+
"behavioralcategory": "taxonomies",
|
|
51
|
+
"behavioralparadigm": "taxonomies",
|
|
52
|
+
"brainregion": "taxonomies",
|
|
53
|
+
"regulatoryauthority": "taxonomies",
|
|
54
|
+
"setuptype": "taxonomies",
|
|
55
|
+
"species": "taxonomies",
|
|
56
|
+
"strain": "taxonomies",
|
|
57
|
+
"strainapproval": "taxonomies",
|
|
58
|
+
# dissemination
|
|
59
|
+
"journal": "dissemination",
|
|
60
|
+
"journalapproval": "dissemination",
|
|
61
|
+
"publication": "dissemination",
|
|
62
|
+
# users
|
|
63
|
+
"group": "users",
|
|
64
|
+
"group_membership_invitation": "users",
|
|
65
|
+
"group_membership_request": "users",
|
|
66
|
+
"laboratory": "users",
|
|
67
|
+
"user": "users",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_TOKEN_FILE = Path.home() / ".config" / "brainstem" / "token"
|
|
71
|
+
|
|
72
|
+
# Derived from _MODEL_TO_APP so there is exactly one source of truth.
|
|
73
|
+
ModelType = Enum("ModelType", {k: k for k in _MODEL_TO_APP}) # type: ignore[misc]
|
|
74
|
+
|
|
75
|
+
_VALID_PORTALS = {"private", "public", "super"}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_model(model) -> str:
|
|
79
|
+
"""Accept a plain string or a ModelType member and return the string value."""
|
|
80
|
+
if isinstance(model, ModelType):
|
|
81
|
+
return model.value
|
|
82
|
+
model = str(model)
|
|
83
|
+
if model not in _MODEL_TO_APP:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"Unknown model '{model}'. Valid models are: "
|
|
86
|
+
+ ", ".join(sorted(_MODEL_TO_APP))
|
|
87
|
+
)
|
|
88
|
+
return model
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _resolve_portal(portal) -> str:
|
|
92
|
+
"""Accept a plain string or a PortalType member and return the string value."""
|
|
93
|
+
if isinstance(portal, Enum):
|
|
94
|
+
portal = portal.value
|
|
95
|
+
portal = str(portal)
|
|
96
|
+
if portal not in _VALID_PORTALS:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Unknown portal '{portal}'. Valid portals are: "
|
|
99
|
+
+ ", ".join(sorted(_VALID_PORTALS))
|
|
100
|
+
)
|
|
101
|
+
return portal
|
|
41
102
|
|
|
42
103
|
|
|
43
104
|
class PortalType(Enum):
|
|
@@ -46,202 +107,789 @@ class PortalType(Enum):
|
|
|
46
107
|
super = "super"
|
|
47
108
|
|
|
48
109
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# Exceptions
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
52
113
|
|
|
114
|
+
class AuthenticationError(Exception):
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Client
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
53
121
|
|
|
54
122
|
class BrainstemClient:
|
|
55
123
|
|
|
56
|
-
|
|
57
|
-
|
|
124
|
+
BASE_URL = "https://www.brainstem.org/"
|
|
125
|
+
DEFAULT_TIMEOUT: int = 30 # seconds; applied to all HTTP calls
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
token: str = None,
|
|
130
|
+
headless: bool = False,
|
|
131
|
+
url: str = None,
|
|
132
|
+
) -> None:
|
|
133
|
+
base = (url.rstrip("/") + "/") if url else self.BASE_URL
|
|
134
|
+
self._address = base + "api/"
|
|
135
|
+
self._session = requests.Session()
|
|
58
136
|
|
|
59
|
-
#
|
|
60
|
-
|
|
137
|
+
# Automatically retry transient server errors
|
|
138
|
+
_retry = Retry(total=3, backoff_factor=0.5, status_forcelist={502, 503, 504})
|
|
139
|
+
self._session.mount("https://", HTTPAdapter(max_retries=_retry))
|
|
140
|
+
self._session.mount("http://", HTTPAdapter(max_retries=_retry))
|
|
61
141
|
|
|
62
142
|
if token:
|
|
63
143
|
self._token = token
|
|
64
144
|
else:
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
145
|
+
self._token = self._load_cached_token()
|
|
146
|
+
if not self._token:
|
|
147
|
+
self._token = self._device_auth_flow(headless=headless)
|
|
148
|
+
self._save_token(self._token)
|
|
149
|
+
|
|
150
|
+
self._session.headers.update({"Authorization": f"Bearer {self._token}"})
|
|
151
|
+
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
# Token management
|
|
154
|
+
# ------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def _load_cached_token(self) -> str:
|
|
157
|
+
"""Return the cached token from disk, or None if not present."""
|
|
158
|
+
if _TOKEN_FILE.exists():
|
|
159
|
+
return _TOKEN_FILE.read_text().strip() or None
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def _save_token(self, token: str) -> None:
|
|
163
|
+
"""Persist the token to disk with owner-read-only permissions."""
|
|
164
|
+
_TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
_TOKEN_FILE.write_text(token)
|
|
166
|
+
_TOKEN_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0o600
|
|
167
|
+
|
|
168
|
+
def _device_auth_flow(self, headless: bool = False, max_wait: int = 900) -> str:
|
|
169
|
+
"""Run the BrainSTEM device authorization flow and return a token.
|
|
170
|
+
|
|
171
|
+
The user authenticates in their browser (supports 2FA). No
|
|
172
|
+
credentials are sent by this tool.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
headless : If ``True``, print the verification URI and user code
|
|
177
|
+
instead of opening a browser window.
|
|
178
|
+
max_wait : Maximum seconds to wait for browser approval (default: 900).
|
|
179
|
+
"""
|
|
180
|
+
# Step 1 — initiate a device session
|
|
181
|
+
resp = requests.post(
|
|
182
|
+
self._address + "auth/device/",
|
|
183
|
+
json={"client_name": "brainstem-python"},
|
|
184
|
+
timeout=10,
|
|
185
|
+
)
|
|
186
|
+
resp.raise_for_status()
|
|
187
|
+
data = resp.json()
|
|
188
|
+
|
|
189
|
+
device_code = data["device_code"]
|
|
190
|
+
interval = int(data.get("interval", 5))
|
|
191
|
+
|
|
192
|
+
# Step 2 — open browser or print instructions
|
|
193
|
+
if headless:
|
|
194
|
+
print(f"Open {data['verification_uri']} and enter: {data['user_code']}")
|
|
195
|
+
else:
|
|
196
|
+
webbrowser.open(data["verification_uri_complete"])
|
|
197
|
+
print("Waiting for browser approval...")
|
|
198
|
+
|
|
199
|
+
# Step 3 — poll until resolved (timeout after max_wait seconds)
|
|
200
|
+
deadline = time.monotonic() + max_wait
|
|
201
|
+
while True:
|
|
202
|
+
if time.monotonic() > deadline:
|
|
203
|
+
raise AuthenticationError(
|
|
204
|
+
f"Device authorization timed out after {max_wait} seconds."
|
|
205
|
+
)
|
|
206
|
+
time.sleep(interval)
|
|
207
|
+
poll = requests.post(
|
|
208
|
+
self._address + "auth/device/token/",
|
|
209
|
+
json={"device_code": device_code},
|
|
210
|
+
timeout=10,
|
|
72
211
|
)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if resp.status_code != 200:
|
|
93
|
-
raise LoginError()
|
|
94
|
-
|
|
95
|
-
token = json.loads(resp.text)['token']
|
|
96
|
-
return token
|
|
97
|
-
|
|
98
|
-
def __get_app_from_model(self, modelname: str) -> str:
|
|
99
|
-
app = None
|
|
100
|
-
|
|
101
|
-
if modelname in ['project', 'subject', 'session', 'collection', 'cohort',
|
|
102
|
-
'projectmembershipinvitation',
|
|
103
|
-
'projectgroupmembershipinvitation']:
|
|
104
|
-
app = 'stem'
|
|
105
|
-
|
|
106
|
-
elif modelname in ['procedure', 'behavior', 'dataacquisition', 'manipulation',
|
|
107
|
-
'equipment', 'consumablestock', 'procedurelog', 'subjectlog']:
|
|
108
|
-
app = 'modules'
|
|
109
|
-
|
|
110
|
-
elif modelname in ['behavioralparadigm', 'datastorage', 'inventory',
|
|
111
|
-
'setup']:
|
|
112
|
-
app = 'personal_attributes'
|
|
113
|
-
|
|
114
|
-
elif any([x in modelname for x in
|
|
115
|
-
['consumable', 'hardwaredevice', 'supplier']]):
|
|
116
|
-
app = 'resources'
|
|
117
|
-
|
|
118
|
-
elif any([x in modelname for x in
|
|
119
|
-
['brainregion', 'setuptype',
|
|
120
|
-
'species', 'strain']]):
|
|
121
|
-
app = 'taxonomies'
|
|
122
|
-
|
|
123
|
-
elif any([x in modelname for x in
|
|
124
|
-
['journal', 'laboratory', 'publication']]):
|
|
125
|
-
app = 'attributes'
|
|
126
|
-
|
|
127
|
-
elif modelname in ['user', 'group', 'groupmembershipinvitation',
|
|
128
|
-
'groupmembershiprequest']:
|
|
129
|
-
app = 'users'
|
|
130
|
-
|
|
131
|
-
return app
|
|
132
|
-
|
|
133
|
-
def load_model(self,
|
|
134
|
-
model: ModelType,
|
|
135
|
-
portal: PortalType = "private",
|
|
136
|
-
id: str = None,
|
|
137
|
-
options: str = None,
|
|
138
|
-
filters: dict = None,
|
|
139
|
-
sort: list = None,
|
|
140
|
-
include: list = None) -> Response:
|
|
141
|
-
|
|
142
|
-
app = self.__get_app_from_model(model)
|
|
143
|
-
if app is None:
|
|
144
|
-
resp = Response()
|
|
145
|
-
resp.status_code = 404
|
|
146
|
-
return resp
|
|
147
|
-
|
|
148
|
-
if options is None:
|
|
149
|
-
options = ""
|
|
150
|
-
|
|
212
|
+
poll.raise_for_status()
|
|
213
|
+
result = poll.json()
|
|
214
|
+
|
|
215
|
+
if result.get("status") == "success":
|
|
216
|
+
return result["token"]
|
|
217
|
+
elif result.get("status") == "authorization_pending":
|
|
218
|
+
continue
|
|
219
|
+
else:
|
|
220
|
+
raise AuthenticationError(
|
|
221
|
+
f"Authorization failed: {result.get('error')}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
# Internal helpers
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
def _build_url(self, portal: str, app: str, model: str,
|
|
229
|
+
id: str = None, options: str = None) -> str:
|
|
230
|
+
url = f"{self._address}{portal}/{app}/{model}/"
|
|
151
231
|
if id:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
232
|
+
url += f"{id}/"
|
|
233
|
+
if options:
|
|
234
|
+
url += options
|
|
235
|
+
return url
|
|
236
|
+
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
# Public API
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def load(self,
|
|
242
|
+
model,
|
|
243
|
+
portal="private",
|
|
244
|
+
id: str = None,
|
|
245
|
+
options: str = None,
|
|
246
|
+
filters: dict = None,
|
|
247
|
+
sort: list = None,
|
|
248
|
+
include: list = None,
|
|
249
|
+
limit: int = None,
|
|
250
|
+
offset: int = None,
|
|
251
|
+
load_all: bool = False) -> Union[Response, dict]:
|
|
252
|
+
"""Load one or more records of *model*.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
model : Model name string or ``ModelType`` member, e.g. ``'session'``.
|
|
257
|
+
portal : ``'private'`` (default) or ``'public'``, or ``PortalType`` member.
|
|
258
|
+
id : UUID of a specific record. Returns a single object when set.
|
|
259
|
+
filters : Dict of field filters, e.g. ``{'name.icontains': 'rat'}``.
|
|
260
|
+
Supports ``.icontains``, ``.startswith``, ``.endswith``,
|
|
261
|
+
``.gt``, ``.gte``, ``.lt``, ``.lte``.
|
|
262
|
+
sort : List of fields to sort by. Prefix with ``'-'`` for descending.
|
|
263
|
+
include : Related models to embed, e.g. ``['dataacquisition', 'behaviors']``.
|
|
264
|
+
limit : Max records per page (API maximum: 100).
|
|
265
|
+
offset : Number of records to skip (for pagination).
|
|
266
|
+
load_all : When ``True``, automatically follow pagination and return a
|
|
267
|
+
combined ``dict`` with all records merged under the model key.
|
|
268
|
+
When ``False`` (default), returns the raw ``Response``.
|
|
269
|
+
"""
|
|
270
|
+
model = _resolve_model(model)
|
|
271
|
+
portal = _resolve_portal(portal)
|
|
272
|
+
app = _MODEL_TO_APP[model]
|
|
273
|
+
url = self._build_url(portal, app, model, id, options)
|
|
274
|
+
|
|
275
|
+
params = {}
|
|
276
|
+
if not id:
|
|
277
|
+
for key, val in (filters or {}).items():
|
|
278
|
+
params[f"filter{{{key}}}"] = val
|
|
279
|
+
for field in (sort or []):
|
|
280
|
+
params.setdefault("sort[]", []).append(field)
|
|
281
|
+
for rel in (include or []):
|
|
282
|
+
params.setdefault("include[]", []).append(f"{rel}.*")
|
|
283
|
+
if limit is not None:
|
|
284
|
+
params["limit"] = limit
|
|
285
|
+
if offset is not None:
|
|
286
|
+
params["offset"] = offset
|
|
287
|
+
|
|
288
|
+
if not load_all:
|
|
289
|
+
return self._session.get(url, params=params, timeout=self.DEFAULT_TIMEOUT)
|
|
202
290
|
|
|
291
|
+
if id is not None:
|
|
292
|
+
raise ValueError("load_all=True cannot be used together with id.")
|
|
293
|
+
|
|
294
|
+
# --- auto-paginate and merge all pages ---
|
|
295
|
+
page_size = limit or 100
|
|
296
|
+
params["limit"] = page_size
|
|
297
|
+
params.setdefault("offset", offset or 0)
|
|
298
|
+
|
|
299
|
+
combined: dict = {}
|
|
300
|
+
records_key: str = None
|
|
301
|
+
|
|
302
|
+
while True:
|
|
303
|
+
resp = self._session.get(url, params=params, timeout=self.DEFAULT_TIMEOUT)
|
|
304
|
+
if resp.status_code == 401:
|
|
305
|
+
raise AuthenticationError(
|
|
306
|
+
"API token is invalid or expired. Run `brainstem login` to re-authenticate."
|
|
307
|
+
)
|
|
308
|
+
resp.raise_for_status()
|
|
309
|
+
data = resp.json()
|
|
310
|
+
|
|
311
|
+
if records_key is None:
|
|
312
|
+
# Detect the list key (e.g. 'sessions', 'projects')
|
|
313
|
+
records_key = next(
|
|
314
|
+
(k for k, v in data.items() if isinstance(v, list)), None
|
|
315
|
+
)
|
|
316
|
+
if records_key is None:
|
|
317
|
+
raise ValueError(
|
|
318
|
+
"load_all=True requires a paginated list response, but the API "
|
|
319
|
+
"returned no list-valued key. Use load_all=False for single-object endpoints."
|
|
320
|
+
)
|
|
321
|
+
combined = {k: v for k, v in data.items() if k != records_key}
|
|
322
|
+
combined[records_key] = []
|
|
323
|
+
|
|
324
|
+
combined[records_key].extend(data.get(records_key, []))
|
|
325
|
+
total = data.get("count", len(combined[records_key]))
|
|
326
|
+
|
|
327
|
+
if len(combined[records_key]) >= total:
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
params["offset"] = params.get("offset", 0) + page_size
|
|
331
|
+
|
|
332
|
+
return combined
|
|
333
|
+
|
|
334
|
+
def save(self,
|
|
335
|
+
model,
|
|
336
|
+
portal="private",
|
|
337
|
+
id: str = None,
|
|
338
|
+
data: dict = None,
|
|
339
|
+
options: str = None) -> Response:
|
|
340
|
+
"""Create or update a record.
|
|
341
|
+
|
|
342
|
+
When *id* is provided the record is **updated** (PATCH).
|
|
343
|
+
When *id* is omitted a **new record** is created (POST).
|
|
344
|
+
|
|
345
|
+
Parameters
|
|
346
|
+
----------
|
|
347
|
+
model : Model name string or ``ModelType`` member, e.g. ``'session'``.
|
|
348
|
+
portal : ``'private'`` (default) or ``'public'``, or ``PortalType`` member.
|
|
349
|
+
id : UUID of the record to update. Omit to create a new record.
|
|
350
|
+
data : Dict of fields to submit.
|
|
351
|
+
"""
|
|
352
|
+
model = _resolve_model(model)
|
|
353
|
+
portal = _resolve_portal(portal)
|
|
354
|
+
app = _MODEL_TO_APP[model]
|
|
203
355
|
if data is None:
|
|
204
356
|
data = {}
|
|
205
357
|
|
|
206
|
-
if options is None:
|
|
207
|
-
options = ""
|
|
208
|
-
|
|
209
|
-
# Check if entry already exists
|
|
210
358
|
if id is not None:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
+ "/" + id + "/" + options)
|
|
214
|
-
resp = requests.patch(request_url, json=data,
|
|
215
|
-
headers={"Authorization": "Bearer %s"
|
|
216
|
-
% self._token})
|
|
217
|
-
|
|
359
|
+
url = self._build_url(portal, app, model, id, options)
|
|
360
|
+
return self._session.patch(url, json=data, timeout=self.DEFAULT_TIMEOUT)
|
|
218
361
|
else:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
362
|
+
url = self._build_url(portal, app, model, options=options)
|
|
363
|
+
return self._session.post(url, json=data, timeout=self.DEFAULT_TIMEOUT)
|
|
364
|
+
|
|
365
|
+
def delete(self,
|
|
366
|
+
model,
|
|
367
|
+
portal="private",
|
|
368
|
+
id: str = None) -> Response:
|
|
369
|
+
"""Delete a record by ID.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
model : Model name string or ``ModelType`` member, e.g. ``'session'``.
|
|
374
|
+
portal : ``'private'`` (default) or ``'public'``, or ``PortalType`` member.
|
|
375
|
+
id : UUID of the record to delete (required).
|
|
376
|
+
"""
|
|
377
|
+
if id is None:
|
|
378
|
+
raise ValueError("'id' is required to delete a record.")
|
|
379
|
+
model = _resolve_model(model)
|
|
380
|
+
portal = _resolve_portal(portal)
|
|
381
|
+
app = _MODEL_TO_APP[model]
|
|
382
|
+
url = self._build_url(portal, app, model, id)
|
|
383
|
+
return self._session.delete(url, timeout=self.DEFAULT_TIMEOUT)
|
|
384
|
+
|
|
385
|
+
def __enter__(self):
|
|
386
|
+
return self
|
|
387
|
+
|
|
388
|
+
def __exit__(self, *args):
|
|
389
|
+
self._session.close()
|
|
390
|
+
|
|
391
|
+
# ------------------------------------------------------------------
|
|
392
|
+
# Convenience loaders (mirror the MATLAB load_* helpers)
|
|
393
|
+
# ------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
def _convenience_load(self, model, default_include, filter_map,
|
|
396
|
+
portal, id, filters, sort, include, limit, offset,
|
|
397
|
+
load_all, **field_kwargs):
|
|
398
|
+
"""Shared implementation for all convenience loaders."""
|
|
399
|
+
unknown = set(field_kwargs) - set(filter_map)
|
|
400
|
+
if unknown:
|
|
401
|
+
raise TypeError(
|
|
402
|
+
f"Unexpected keyword argument(s): {', '.join(sorted(unknown))}"
|
|
403
|
+
)
|
|
404
|
+
merged_filters = dict(filters or {})
|
|
405
|
+
for kwarg, api_field in filter_map.items():
|
|
406
|
+
value = field_kwargs.get(kwarg)
|
|
407
|
+
if value is not None:
|
|
408
|
+
merged_filters[api_field] = value
|
|
409
|
+
return self.load(
|
|
410
|
+
model,
|
|
411
|
+
portal=portal,
|
|
412
|
+
id=id,
|
|
413
|
+
filters=merged_filters or None,
|
|
414
|
+
sort=sort,
|
|
415
|
+
include=include if include is not None else default_include,
|
|
416
|
+
limit=limit,
|
|
417
|
+
offset=offset,
|
|
418
|
+
load_all=load_all,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def load_project(self, portal="private", id: str = None,
|
|
422
|
+
name: str = None, sessions: str = None,
|
|
423
|
+
subjects: str = None, tags: str = None,
|
|
424
|
+
filters: dict = None, sort: list = None,
|
|
425
|
+
include: list = None, limit: int = None,
|
|
426
|
+
offset: int = None, load_all: bool = False):
|
|
427
|
+
"""Load project(s). Embeds sessions, subjects, collections and cohorts by default.
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
name : Filter by project name (case-insensitive contains).
|
|
432
|
+
sessions : Filter by session UUID.
|
|
433
|
+
subjects : Filter by subject UUID.
|
|
434
|
+
tags : Filter by tag.
|
|
435
|
+
"""
|
|
436
|
+
return self._convenience_load(
|
|
437
|
+
"project",
|
|
438
|
+
default_include=["sessions", "subjects", "collections", "cohorts"],
|
|
439
|
+
filter_map={"name": "name.icontains", "sessions": "sessions.id",
|
|
440
|
+
"subjects": "subjects.id", "tags": "tags"},
|
|
441
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
442
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
443
|
+
name=name, sessions=sessions, subjects=subjects, tags=tags,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def load_subject(self, portal="private", id: str = None,
|
|
447
|
+
name: str = None, projects: str = None,
|
|
448
|
+
strain: str = None, sex: str = None, tags: str = None,
|
|
449
|
+
filters: dict = None, sort: list = None,
|
|
450
|
+
include: list = None, limit: int = None,
|
|
451
|
+
offset: int = None, load_all: bool = False):
|
|
452
|
+
"""Load subject(s). Embeds procedures and subjectlogs by default.
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
name : Filter by subject name (case-insensitive contains).
|
|
457
|
+
projects : Filter by project UUID.
|
|
458
|
+
strain : Filter by strain UUID.
|
|
459
|
+
sex : Filter by sex (``'M'``, ``'F'``, or ``'U'``).
|
|
460
|
+
tags : Filter by tag.
|
|
461
|
+
"""
|
|
462
|
+
return self._convenience_load(
|
|
463
|
+
"subject",
|
|
464
|
+
default_include=["procedures", "subjectlogs"],
|
|
465
|
+
filter_map={"name": "name.icontains", "projects": "projects.id",
|
|
466
|
+
"strain": "strain.id", "sex": "sex", "tags": "tags"},
|
|
467
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
468
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
469
|
+
name=name, projects=projects, strain=strain, sex=sex, tags=tags,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def load_session(self, portal="private", id: str = None,
|
|
473
|
+
name: str = None, projects: str = None,
|
|
474
|
+
datastorage: str = None, tags: str = None,
|
|
475
|
+
filters: dict = None, sort: list = None,
|
|
476
|
+
include: list = None, limit: int = None,
|
|
477
|
+
offset: int = None, load_all: bool = False):
|
|
478
|
+
"""Load session(s). Embeds dataacquisition, behaviors, manipulations and epochs by default.
|
|
479
|
+
|
|
480
|
+
Parameters
|
|
481
|
+
----------
|
|
482
|
+
name : Filter by session name (case-insensitive contains).
|
|
483
|
+
projects : Filter by project UUID.
|
|
484
|
+
datastorage : Filter by data storage UUID.
|
|
485
|
+
tags : Filter by tag.
|
|
486
|
+
"""
|
|
487
|
+
return self._convenience_load(
|
|
488
|
+
"session",
|
|
489
|
+
default_include=["dataacquisition", "behaviors", "manipulations", "epochs"],
|
|
490
|
+
filter_map={"name": "name.icontains", "projects": "projects.id",
|
|
491
|
+
"datastorage": "datastorage.id", "tags": "tags"},
|
|
492
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
493
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
494
|
+
name=name, projects=projects, datastorage=datastorage, tags=tags,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
def load_collection(self, portal="private", id: str = None,
|
|
498
|
+
name: str = None, tags: str = None,
|
|
499
|
+
filters: dict = None, sort: list = None,
|
|
500
|
+
include: list = None, limit: int = None,
|
|
501
|
+
offset: int = None, load_all: bool = False):
|
|
502
|
+
"""Load collection(s). Embeds sessions by default.
|
|
503
|
+
|
|
504
|
+
Parameters
|
|
505
|
+
----------
|
|
506
|
+
name : Filter by collection name (case-insensitive contains).
|
|
507
|
+
tags : Filter by tag.
|
|
508
|
+
"""
|
|
509
|
+
return self._convenience_load(
|
|
510
|
+
"collection",
|
|
511
|
+
default_include=["sessions"],
|
|
512
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
513
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
514
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
515
|
+
name=name, tags=tags,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
def load_cohort(self, portal="private", id: str = None,
|
|
519
|
+
name: str = None, tags: str = None,
|
|
520
|
+
filters: dict = None, sort: list = None,
|
|
521
|
+
include: list = None, limit: int = None,
|
|
522
|
+
offset: int = None, load_all: bool = False):
|
|
523
|
+
"""Load cohort(s). Embeds subjects by default.
|
|
524
|
+
|
|
525
|
+
Parameters
|
|
526
|
+
----------
|
|
527
|
+
name : Filter by cohort name (case-insensitive contains).
|
|
528
|
+
tags : Filter by tag.
|
|
529
|
+
"""
|
|
530
|
+
return self._convenience_load(
|
|
531
|
+
"cohort",
|
|
532
|
+
default_include=["subjects"],
|
|
533
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
534
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
535
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
536
|
+
name=name, tags=tags,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
def load_behavior(self, portal="private", id: str = None,
|
|
540
|
+
session: str = None, tags: str = None,
|
|
541
|
+
filters: dict = None, sort: list = None,
|
|
542
|
+
include: list = None, limit: int = None,
|
|
543
|
+
offset: int = None, load_all: bool = False):
|
|
544
|
+
"""Load behavior record(s).
|
|
545
|
+
|
|
546
|
+
Parameters
|
|
547
|
+
----------
|
|
548
|
+
session : Filter by session UUID.
|
|
549
|
+
tags : Filter by tag.
|
|
550
|
+
"""
|
|
551
|
+
return self._convenience_load(
|
|
552
|
+
"behavior",
|
|
553
|
+
default_include=[],
|
|
554
|
+
filter_map={"session": "session.id", "tags": "tags"},
|
|
555
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
556
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
557
|
+
session=session, tags=tags,
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
def load_dataacquisition(self, portal="private", id: str = None,
|
|
561
|
+
session: str = None, tags: str = None,
|
|
562
|
+
filters: dict = None, sort: list = None,
|
|
563
|
+
include: list = None, limit: int = None,
|
|
564
|
+
offset: int = None, load_all: bool = False):
|
|
565
|
+
"""Load data acquisition record(s).
|
|
566
|
+
|
|
567
|
+
Parameters
|
|
568
|
+
----------
|
|
569
|
+
session : Filter by session UUID.
|
|
570
|
+
tags : Filter by tag.
|
|
571
|
+
"""
|
|
572
|
+
return self._convenience_load(
|
|
573
|
+
"dataacquisition",
|
|
574
|
+
default_include=[],
|
|
575
|
+
filter_map={"session": "session.id", "tags": "tags"},
|
|
576
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
577
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
578
|
+
session=session, tags=tags,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def load_manipulation(self, portal="private", id: str = None,
|
|
582
|
+
session: str = None, tags: str = None,
|
|
583
|
+
filters: dict = None, sort: list = None,
|
|
584
|
+
include: list = None, limit: int = None,
|
|
585
|
+
offset: int = None, load_all: bool = False):
|
|
586
|
+
"""Load manipulation record(s).
|
|
587
|
+
|
|
588
|
+
Parameters
|
|
589
|
+
----------
|
|
590
|
+
session : Filter by session UUID.
|
|
591
|
+
tags : Filter by tag.
|
|
592
|
+
"""
|
|
593
|
+
return self._convenience_load(
|
|
594
|
+
"manipulation",
|
|
595
|
+
default_include=[],
|
|
596
|
+
filter_map={"session": "session.id", "tags": "tags"},
|
|
597
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
598
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
599
|
+
session=session, tags=tags,
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
def load_procedure(self, portal="private", id: str = None,
|
|
603
|
+
subject: str = None, tags: str = None,
|
|
604
|
+
filters: dict = None, sort: list = None,
|
|
605
|
+
include: list = None, limit: int = None,
|
|
606
|
+
offset: int = None, load_all: bool = False):
|
|
607
|
+
"""Load procedure record(s).
|
|
608
|
+
|
|
609
|
+
Parameters
|
|
610
|
+
----------
|
|
611
|
+
subject : Filter by subject UUID.
|
|
612
|
+
tags : Filter by tag.
|
|
613
|
+
"""
|
|
614
|
+
return self._convenience_load(
|
|
615
|
+
"procedure",
|
|
616
|
+
default_include=[],
|
|
617
|
+
filter_map={"subject": "subject.id", "tags": "tags"},
|
|
618
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
619
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
620
|
+
subject=subject, tags=tags,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
def load_procedurelog(self, portal="private", id: str = None,
|
|
624
|
+
procedure: str = None, tags: str = None,
|
|
625
|
+
filters: dict = None, sort: list = None,
|
|
626
|
+
include: list = None, limit: int = None,
|
|
627
|
+
offset: int = None, load_all: bool = False):
|
|
628
|
+
"""Load procedure log record(s).
|
|
629
|
+
|
|
630
|
+
Parameters
|
|
631
|
+
----------
|
|
632
|
+
procedure : Filter by procedure UUID.
|
|
633
|
+
tags : Filter by tag.
|
|
634
|
+
"""
|
|
635
|
+
return self._convenience_load(
|
|
636
|
+
"procedurelog",
|
|
637
|
+
default_include=[],
|
|
638
|
+
filter_map={"procedure": "procedure.id", "tags": "tags"},
|
|
639
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
640
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
641
|
+
procedure=procedure, tags=tags,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def load_subjectlog(self, portal="private", id: str = None,
|
|
645
|
+
subject: str = None, tags: str = None,
|
|
646
|
+
filters: dict = None, sort: list = None,
|
|
647
|
+
include: list = None, limit: int = None,
|
|
648
|
+
offset: int = None, load_all: bool = False):
|
|
649
|
+
"""Load subject log record(s).
|
|
650
|
+
|
|
651
|
+
Parameters
|
|
652
|
+
----------
|
|
653
|
+
subject : Filter by subject UUID.
|
|
654
|
+
tags : Filter by tag.
|
|
655
|
+
"""
|
|
656
|
+
return self._convenience_load(
|
|
657
|
+
"subjectlog",
|
|
658
|
+
default_include=[],
|
|
659
|
+
filter_map={"subject": "subject.id", "tags": "tags"},
|
|
660
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
661
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
662
|
+
subject=subject, tags=tags,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
def load_equipment(self, portal="private", id: str = None,
|
|
666
|
+
name: str = None, setup: str = None, tags: str = None,
|
|
667
|
+
filters: dict = None, sort: list = None,
|
|
668
|
+
include: list = None, limit: int = None,
|
|
669
|
+
offset: int = None, load_all: bool = False):
|
|
670
|
+
"""Load equipment record(s).
|
|
671
|
+
|
|
672
|
+
Parameters
|
|
673
|
+
----------
|
|
674
|
+
name : Filter by equipment name (case-insensitive contains).
|
|
675
|
+
setup : Filter by setup UUID.
|
|
676
|
+
tags : Filter by tag.
|
|
677
|
+
"""
|
|
678
|
+
return self._convenience_load(
|
|
679
|
+
"equipment",
|
|
680
|
+
default_include=[],
|
|
681
|
+
filter_map={"name": "name.icontains", "setup": "setup.id", "tags": "tags"},
|
|
682
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
683
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
684
|
+
name=name, setup=setup, tags=tags,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
def load_consumablestock(self, portal="private", id: str = None,
|
|
688
|
+
tags: str = None,
|
|
689
|
+
filters: dict = None, sort: list = None,
|
|
690
|
+
include: list = None, limit: int = None,
|
|
691
|
+
offset: int = None, load_all: bool = False):
|
|
692
|
+
"""Load consumable stock record(s).
|
|
693
|
+
|
|
694
|
+
Parameters
|
|
695
|
+
----------
|
|
696
|
+
tags : Filter by tag.
|
|
697
|
+
"""
|
|
698
|
+
return self._convenience_load(
|
|
699
|
+
"consumablestock",
|
|
700
|
+
default_include=[],
|
|
701
|
+
filter_map={"tags": "tags"},
|
|
702
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
703
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
704
|
+
tags=tags,
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
def load_behavioralassay(self, portal="private", id: str = None,
|
|
708
|
+
name: str = None, tags: str = None,
|
|
709
|
+
filters: dict = None, sort: list = None,
|
|
710
|
+
include: list = None, limit: int = None,
|
|
711
|
+
offset: int = None, load_all: bool = False):
|
|
712
|
+
"""Load behavioral assay record(s).
|
|
713
|
+
|
|
714
|
+
Parameters
|
|
715
|
+
----------
|
|
716
|
+
name : Filter by name (case-insensitive contains).
|
|
717
|
+
tags : Filter by tag.
|
|
718
|
+
"""
|
|
719
|
+
return self._convenience_load(
|
|
720
|
+
"behavioralassay",
|
|
721
|
+
default_include=[],
|
|
722
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
723
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
724
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
725
|
+
name=name, tags=tags,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
def load_datastorage(self, portal="private", id: str = None,
|
|
729
|
+
name: str = None, tags: str = None,
|
|
730
|
+
filters: dict = None, sort: list = None,
|
|
731
|
+
include: list = None, limit: int = None,
|
|
732
|
+
offset: int = None, load_all: bool = False):
|
|
733
|
+
"""Load data storage record(s).
|
|
734
|
+
|
|
735
|
+
Parameters
|
|
736
|
+
----------
|
|
737
|
+
name : Filter by name (case-insensitive contains).
|
|
738
|
+
tags : Filter by tag.
|
|
739
|
+
"""
|
|
740
|
+
return self._convenience_load(
|
|
741
|
+
"datastorage",
|
|
742
|
+
default_include=[],
|
|
743
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
744
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
745
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
746
|
+
name=name, tags=tags,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
def load_setup(self, portal="private", id: str = None,
|
|
750
|
+
name: str = None, tags: str = None,
|
|
751
|
+
filters: dict = None, sort: list = None,
|
|
752
|
+
include: list = None, limit: int = None,
|
|
753
|
+
offset: int = None, load_all: bool = False):
|
|
754
|
+
"""Load setup record(s).
|
|
755
|
+
|
|
756
|
+
Parameters
|
|
757
|
+
----------
|
|
758
|
+
name : Filter by name (case-insensitive contains).
|
|
759
|
+
tags : Filter by tag.
|
|
760
|
+
"""
|
|
761
|
+
return self._convenience_load(
|
|
762
|
+
"setup",
|
|
763
|
+
default_include=[],
|
|
764
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
765
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
766
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
767
|
+
name=name, tags=tags,
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
def load_hardwaredevice(self, portal="private", id: str = None,
|
|
771
|
+
name: str = None, tags: str = None,
|
|
772
|
+
filters: dict = None, sort: list = None,
|
|
773
|
+
include: list = None, limit: int = None,
|
|
774
|
+
offset: int = None, load_all: bool = False):
|
|
775
|
+
"""Load hardware device record(s).
|
|
776
|
+
|
|
777
|
+
Parameters
|
|
778
|
+
----------
|
|
779
|
+
name : Filter by name (case-insensitive contains).
|
|
780
|
+
tags : Filter by tag.
|
|
781
|
+
"""
|
|
782
|
+
return self._convenience_load(
|
|
783
|
+
"hardwaredevice",
|
|
784
|
+
default_include=[],
|
|
785
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
786
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
787
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
788
|
+
name=name, tags=tags,
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
def load_brainregion(self, portal="private", id: str = None,
|
|
792
|
+
name: str = None, tags: str = None,
|
|
793
|
+
filters: dict = None, sort: list = None,
|
|
794
|
+
include: list = None, limit: int = None,
|
|
795
|
+
offset: int = None, load_all: bool = False):
|
|
796
|
+
"""Load brain region record(s).
|
|
797
|
+
|
|
798
|
+
Parameters
|
|
799
|
+
----------
|
|
800
|
+
name : Filter by name (case-insensitive contains).
|
|
801
|
+
tags : Filter by tag.
|
|
802
|
+
"""
|
|
803
|
+
return self._convenience_load(
|
|
804
|
+
"brainregion",
|
|
805
|
+
default_include=[],
|
|
806
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
807
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
808
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
809
|
+
name=name, tags=tags,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
def load_species(self, portal="private", id: str = None,
|
|
813
|
+
name: str = None, tags: str = None,
|
|
814
|
+
filters: dict = None, sort: list = None,
|
|
815
|
+
include: list = None, limit: int = None,
|
|
816
|
+
offset: int = None, load_all: bool = False):
|
|
817
|
+
"""Load species record(s).
|
|
818
|
+
|
|
819
|
+
Parameters
|
|
820
|
+
----------
|
|
821
|
+
name : Filter by name (case-insensitive contains).
|
|
822
|
+
tags : Filter by tag.
|
|
823
|
+
"""
|
|
824
|
+
return self._convenience_load(
|
|
825
|
+
"species",
|
|
826
|
+
default_include=[],
|
|
827
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
828
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
829
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
830
|
+
name=name, tags=tags,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
def load_strain(self, portal="private", id: str = None,
|
|
834
|
+
name: str = None, species: str = None, tags: str = None,
|
|
835
|
+
filters: dict = None, sort: list = None,
|
|
836
|
+
include: list = None, limit: int = None,
|
|
837
|
+
offset: int = None, load_all: bool = False):
|
|
838
|
+
"""Load strain record(s).
|
|
839
|
+
|
|
840
|
+
Parameters
|
|
841
|
+
----------
|
|
842
|
+
name : Filter by name (case-insensitive contains).
|
|
843
|
+
species : Filter by species UUID.
|
|
844
|
+
tags : Filter by tag.
|
|
845
|
+
"""
|
|
846
|
+
return self._convenience_load(
|
|
847
|
+
"strain",
|
|
848
|
+
default_include=[],
|
|
849
|
+
filter_map={"name": "name.icontains", "species": "species.id", "tags": "tags"},
|
|
850
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
851
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
852
|
+
name=name, species=species, tags=tags,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
def load_publication(self, portal="private", id: str = None,
|
|
856
|
+
name: str = None, tags: str = None,
|
|
857
|
+
filters: dict = None, sort: list = None,
|
|
858
|
+
include: list = None, limit: int = None,
|
|
859
|
+
offset: int = None, load_all: bool = False):
|
|
860
|
+
"""Load publication record(s).
|
|
861
|
+
|
|
862
|
+
Parameters
|
|
863
|
+
----------
|
|
864
|
+
name : Filter by name (case-insensitive contains).
|
|
865
|
+
tags : Filter by tag.
|
|
866
|
+
"""
|
|
867
|
+
return self._convenience_load(
|
|
868
|
+
"publication",
|
|
869
|
+
default_include=[],
|
|
870
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
871
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
872
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
873
|
+
name=name, tags=tags,
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
def load_laboratory(self, portal="private", id: str = None,
|
|
877
|
+
name: str = None, tags: str = None,
|
|
878
|
+
filters: dict = None, sort: list = None,
|
|
879
|
+
include: list = None, limit: int = None,
|
|
880
|
+
offset: int = None, load_all: bool = False):
|
|
881
|
+
"""Load laboratory record(s).
|
|
882
|
+
|
|
883
|
+
Parameters
|
|
884
|
+
----------
|
|
885
|
+
name : Filter by name (case-insensitive contains).
|
|
886
|
+
tags : Filter by tag.
|
|
887
|
+
"""
|
|
888
|
+
return self._convenience_load(
|
|
889
|
+
"laboratory",
|
|
890
|
+
default_include=[],
|
|
891
|
+
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
892
|
+
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
893
|
+
limit=limit, offset=offset, load_all=load_all,
|
|
894
|
+
name=name, tags=tags,
|
|
895
|
+
)
|