brainstem-python-api-tools 2.0.0__py3-none-any.whl → 2.1.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 +2 -2
- brainstem_api_tools/brainstem_api_client.py +489 -144
- brainstem_api_tools/cli.py +90 -15
- brainstem_python_api_tools-2.1.0.dist-info/METADATA +330 -0
- brainstem_python_api_tools-2.1.0.dist-info/RECORD +9 -0
- {brainstem_python_api_tools-2.0.0.dist-info → brainstem_python_api_tools-2.1.0.dist-info}/WHEEL +1 -1
- brainstem_python_api_tools-2.0.0.dist-info/METADATA +0 -179
- brainstem_python_api_tools-2.0.0.dist-info/RECORD +0 -9
- {brainstem_python_api_tools-2.0.0.dist-info → brainstem_python_api_tools-2.1.0.dist-info}/entry_points.txt +0 -0
- {brainstem_python_api_tools-2.0.0.dist-info → brainstem_python_api_tools-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {brainstem_python_api_tools-2.0.0.dist-info → brainstem_python_api_tools-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import gzip
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
1
4
|
import os
|
|
2
5
|
import stat
|
|
3
6
|
import time
|
|
7
|
+
import warnings
|
|
4
8
|
import webbrowser
|
|
5
9
|
from enum import Enum
|
|
6
10
|
from pathlib import Path
|
|
@@ -18,68 +22,134 @@ from urllib3.util.retry import Retry
|
|
|
18
22
|
|
|
19
23
|
_MODEL_TO_APP: dict = {
|
|
20
24
|
# stem
|
|
25
|
+
"breeding": "stem",
|
|
26
|
+
"cohort": "stem",
|
|
27
|
+
"cohortsubject": "stem",
|
|
28
|
+
"collection": "stem",
|
|
29
|
+
"collectionsession": "stem",
|
|
30
|
+
"epoch": "stem",
|
|
21
31
|
"project": "stem",
|
|
22
|
-
"
|
|
32
|
+
"projectgroupmembershipinvitation": "stem",
|
|
33
|
+
"projectmembershipinvitation": "stem",
|
|
23
34
|
"session": "stem",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"project_group_membership_invitation": "stem",
|
|
35
|
+
"sessiondatastorage": "stem",
|
|
36
|
+
"specimen": "stem",
|
|
37
|
+
"specimenset": "stem",
|
|
38
|
+
"subject": "stem",
|
|
29
39
|
# modules
|
|
30
|
-
"procedure": "modules",
|
|
31
40
|
"behavior": "modules",
|
|
41
|
+
"consumablestock": "modules",
|
|
32
42
|
"dataacquisition": "modules",
|
|
33
|
-
"
|
|
43
|
+
"dataacquisitioninterface": "modules",
|
|
44
|
+
"dataacquisitionprocedure": "modules",
|
|
45
|
+
"dataacquisitiontemplate": "modules",
|
|
46
|
+
"episodepersonnel": "modules",
|
|
34
47
|
"equipment": "modules",
|
|
35
|
-
"
|
|
48
|
+
"experimentalinterface": "modules",
|
|
49
|
+
"experimentalinterfacetemplate": "modules",
|
|
50
|
+
"manipulation": "modules",
|
|
51
|
+
"manipulationinterface": "modules",
|
|
52
|
+
"manipulationtemplate": "modules",
|
|
53
|
+
"materialusage": "modules",
|
|
54
|
+
"materialusagetemplate": "modules",
|
|
55
|
+
"procedure": "modules",
|
|
56
|
+
"procedureepisode": "modules",
|
|
57
|
+
"procedureepisodetemplate": "modules",
|
|
36
58
|
"procedurelog": "modules",
|
|
59
|
+
"proceduretemplate": "modules",
|
|
60
|
+
"specimenlog": "modules",
|
|
61
|
+
"specimenprocedure": "modules",
|
|
62
|
+
"specimenproceduretemplate": "modules",
|
|
37
63
|
"subjectlog": "modules",
|
|
38
64
|
# personal_attributes
|
|
39
65
|
"behavioralassay": "personal_attributes",
|
|
66
|
+
"behavioralassaygroupmembershipinvitation": "personal_attributes",
|
|
67
|
+
"behavioralassaymembershipinvitation": "personal_attributes",
|
|
40
68
|
"datastorage": "personal_attributes",
|
|
69
|
+
"datastoragegroupmembershipinvitation": "personal_attributes",
|
|
70
|
+
"datastoragemembershipinvitation": "personal_attributes",
|
|
41
71
|
"inventory": "personal_attributes",
|
|
72
|
+
"inventorygroupmembershipinvitation": "personal_attributes",
|
|
73
|
+
"inventorymembershipinvitation": "personal_attributes",
|
|
42
74
|
"license": "personal_attributes",
|
|
75
|
+
"licensegroupmembershipinvitation": "personal_attributes",
|
|
76
|
+
"licensemembershipinvitation": "personal_attributes",
|
|
43
77
|
"protocol": "personal_attributes",
|
|
78
|
+
"protocolgroupmembershipinvitation": "personal_attributes",
|
|
79
|
+
"protocolmembershipinvitation": "personal_attributes",
|
|
44
80
|
"setup": "personal_attributes",
|
|
81
|
+
"setupgroupmembershipinvitation": "personal_attributes",
|
|
82
|
+
"setupmembershipinvitation": "personal_attributes",
|
|
45
83
|
# resources
|
|
46
84
|
"consumable": "resources",
|
|
85
|
+
"consumableapproval": "resources",
|
|
47
86
|
"hardwaredevice": "resources",
|
|
87
|
+
"hardwaredeviceapproval": "resources",
|
|
48
88
|
"supplier": "resources",
|
|
89
|
+
"supplierapproval": "resources",
|
|
49
90
|
# taxonomies
|
|
91
|
+
"anatomicalreference": "taxonomies",
|
|
92
|
+
"anatomicalregion": "taxonomies",
|
|
50
93
|
"behavioralcategory": "taxonomies",
|
|
94
|
+
"behavioralcategoryapproval": "taxonomies",
|
|
51
95
|
"behavioralparadigm": "taxonomies",
|
|
52
|
-
"
|
|
96
|
+
"behavioralparadigmapproval": "taxonomies",
|
|
97
|
+
"coordinatetransform": "taxonomies",
|
|
53
98
|
"regulatoryauthority": "taxonomies",
|
|
99
|
+
"regulatoryauthorityapproval": "taxonomies",
|
|
54
100
|
"setuptype": "taxonomies",
|
|
101
|
+
"setuptypeapproval": "taxonomies",
|
|
55
102
|
"species": "taxonomies",
|
|
103
|
+
"speciesapproval": "taxonomies",
|
|
56
104
|
"strain": "taxonomies",
|
|
57
105
|
"strainapproval": "taxonomies",
|
|
58
106
|
# dissemination
|
|
59
107
|
"journal": "dissemination",
|
|
60
108
|
"journalapproval": "dissemination",
|
|
61
109
|
"publication": "dissemination",
|
|
110
|
+
"publicationapproval": "dissemination",
|
|
62
111
|
# users
|
|
112
|
+
"admingroup": "users",
|
|
63
113
|
"group": "users",
|
|
64
|
-
"
|
|
65
|
-
"
|
|
114
|
+
"groupmembershipinvitation": "users",
|
|
115
|
+
"groupmembershiprequest": "users",
|
|
66
116
|
"laboratory": "users",
|
|
117
|
+
"pendinginvitation": "users",
|
|
67
118
|
"user": "users",
|
|
68
119
|
}
|
|
69
120
|
|
|
70
121
|
_TOKEN_FILE = Path.home() / ".config" / "brainstem" / "token"
|
|
71
122
|
|
|
123
|
+
# Deprecated model names, resolved to the server's current name with a warning:
|
|
124
|
+
# the underscored names earlier versions of this client used (they never matched
|
|
125
|
+
# a server route), and "brainregion", renamed "anatomicalregion".
|
|
126
|
+
_MODEL_ALIASES: dict = {
|
|
127
|
+
"brainregion": "anatomicalregion",
|
|
128
|
+
"project_membership_invitation": "projectmembershipinvitation",
|
|
129
|
+
"project_group_membership_invitation": "projectgroupmembershipinvitation",
|
|
130
|
+
"group_membership_invitation": "groupmembershipinvitation",
|
|
131
|
+
"group_membership_request": "groupmembershiprequest",
|
|
132
|
+
}
|
|
133
|
+
|
|
72
134
|
# Derived from _MODEL_TO_APP so there is exactly one source of truth.
|
|
73
|
-
ModelType = Enum(
|
|
135
|
+
ModelType = Enum( # type: ignore[misc]
|
|
136
|
+
"ModelType", {k: k for k in [*_MODEL_TO_APP, *_MODEL_ALIASES]}
|
|
137
|
+
)
|
|
74
138
|
|
|
75
139
|
_VALID_PORTALS = {"private", "public", "super"}
|
|
76
140
|
|
|
77
141
|
|
|
78
142
|
def _resolve_model(model) -> str:
|
|
79
|
-
"""Accept a plain string or a ModelType member and return the
|
|
143
|
+
"""Accept a plain string or a ModelType member and return the server's resource name."""
|
|
80
144
|
if isinstance(model, ModelType):
|
|
81
|
-
|
|
145
|
+
model = model.value
|
|
82
146
|
model = str(model)
|
|
147
|
+
if model in _MODEL_ALIASES:
|
|
148
|
+
warnings.warn(
|
|
149
|
+
f"Model name '{model}' is deprecated; use '{_MODEL_ALIASES[model]}'.",
|
|
150
|
+
FutureWarning, stacklevel=3,
|
|
151
|
+
)
|
|
152
|
+
return _MODEL_ALIASES[model]
|
|
83
153
|
if model not in _MODEL_TO_APP:
|
|
84
154
|
raise ValueError(
|
|
85
155
|
f"Unknown model '{model}'. Valid models are: "
|
|
@@ -115,6 +185,10 @@ class AuthenticationError(Exception):
|
|
|
115
185
|
pass
|
|
116
186
|
|
|
117
187
|
|
|
188
|
+
class ExportError(Exception):
|
|
189
|
+
"""A project export could not be produced or downloaded."""
|
|
190
|
+
|
|
191
|
+
|
|
118
192
|
# ---------------------------------------------------------------------------
|
|
119
193
|
# Client
|
|
120
194
|
# ---------------------------------------------------------------------------
|
|
@@ -123,14 +197,34 @@ class BrainstemClient:
|
|
|
123
197
|
|
|
124
198
|
BASE_URL = "https://www.brainstem.org/"
|
|
125
199
|
DEFAULT_TIMEOUT: int = 30 # seconds; applied to all HTTP calls
|
|
200
|
+
MAX_PAGE_SIZE: int = 500 # the API returns at most this many records per page
|
|
126
201
|
|
|
127
202
|
def __init__(
|
|
128
203
|
self,
|
|
129
204
|
token: str = None,
|
|
130
205
|
headless: bool = False,
|
|
131
206
|
url: str = None,
|
|
207
|
+
anonymous: bool = False,
|
|
132
208
|
) -> None:
|
|
209
|
+
"""Create a client.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
token : API token. Defaults to ``$BRAINSTEM_API_TOKEN``, then the
|
|
214
|
+
cached token, then the browser device authorization flow.
|
|
215
|
+
headless : Print the verification URL and code instead of opening a browser.
|
|
216
|
+
url : Base URL of the server. Defaults to ``$BRAINSTEM_API_URL``,
|
|
217
|
+
then ``https://www.brainstem.org/``.
|
|
218
|
+
anonymous : Send no token and never start a login. Only the public
|
|
219
|
+
portal (``portal='public'``) is readable.
|
|
220
|
+
"""
|
|
221
|
+
if url is None:
|
|
222
|
+
url = os.environ.get("BRAINSTEM_API_URL")
|
|
223
|
+
if token is None:
|
|
224
|
+
token = os.environ.get("BRAINSTEM_API_TOKEN")
|
|
225
|
+
|
|
133
226
|
base = (url.rstrip("/") + "/") if url else self.BASE_URL
|
|
227
|
+
self._base_url = base
|
|
134
228
|
self._address = base + "api/"
|
|
135
229
|
self._session = requests.Session()
|
|
136
230
|
|
|
@@ -139,6 +233,10 @@ class BrainstemClient:
|
|
|
139
233
|
self._session.mount("https://", HTTPAdapter(max_retries=_retry))
|
|
140
234
|
self._session.mount("http://", HTTPAdapter(max_retries=_retry))
|
|
141
235
|
|
|
236
|
+
if anonymous:
|
|
237
|
+
self._token = None
|
|
238
|
+
return
|
|
239
|
+
|
|
142
240
|
if token:
|
|
143
241
|
self._token = token
|
|
144
242
|
else:
|
|
@@ -209,8 +307,18 @@ class BrainstemClient:
|
|
|
209
307
|
json={"device_code": device_code},
|
|
210
308
|
timeout=10,
|
|
211
309
|
)
|
|
212
|
-
|
|
213
|
-
|
|
310
|
+
# The server answers 400 (invalid_device_code) or 500 (token_error)
|
|
311
|
+
# with a JSON "error" key; report those as authentication failures
|
|
312
|
+
# rather than bare HTTP errors.
|
|
313
|
+
try:
|
|
314
|
+
result = poll.json()
|
|
315
|
+
except ValueError:
|
|
316
|
+
result = {}
|
|
317
|
+
if not poll.ok:
|
|
318
|
+
error = result.get("error") if isinstance(result, dict) else None
|
|
319
|
+
if error:
|
|
320
|
+
raise AuthenticationError(f"Authorization failed: {error}")
|
|
321
|
+
poll.raise_for_status()
|
|
214
322
|
|
|
215
323
|
if result.get("status") == "success":
|
|
216
324
|
return result["token"]
|
|
@@ -260,11 +368,20 @@ class BrainstemClient:
|
|
|
260
368
|
Supports ``.icontains``, ``.startswith``, ``.endswith``,
|
|
261
369
|
``.gt``, ``.gte``, ``.lt``, ``.lte``.
|
|
262
370
|
sort : List of fields to sort by. Prefix with ``'-'`` for descending.
|
|
263
|
-
include : Related
|
|
264
|
-
|
|
265
|
-
|
|
371
|
+
include : Related fields to sideload, e.g. ``['dataacquisition', 'behaviors']``.
|
|
372
|
+
Their records are returned as extra lists next to the main
|
|
373
|
+
one, and only records you may read are included. See ``save``
|
|
374
|
+
before writing back a list field read this way.
|
|
375
|
+
limit : Records per page, 1 to 500 (default 100). Sent to the API as
|
|
376
|
+
``per_page``. With *load_all*, a larger value is reduced to
|
|
377
|
+
500, which returns the same records in more pages.
|
|
378
|
+
offset : Number of records to skip. The API pages with ``page`` and
|
|
379
|
+
``per_page``, so *offset* must be a multiple of the page size
|
|
380
|
+
(*limit*, or 100 if *limit* is not set); it is sent as
|
|
381
|
+
``page = offset // page_size + 1``.
|
|
266
382
|
load_all : When ``True``, automatically follow pagination and return a
|
|
267
|
-
combined ``dict`` with all records merged under the model key
|
|
383
|
+
combined ``dict`` with all records merged under the model key,
|
|
384
|
+
starting at the page selected by *offset*.
|
|
268
385
|
When ``False`` (default), returns the raw ``Response``.
|
|
269
386
|
"""
|
|
270
387
|
model = _resolve_model(model)
|
|
@@ -280,54 +397,88 @@ class BrainstemClient:
|
|
|
280
397
|
params.setdefault("sort[]", []).append(field)
|
|
281
398
|
for rel in (include or []):
|
|
282
399
|
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)
|
|
290
400
|
|
|
291
|
-
if id is not None:
|
|
401
|
+
if load_all and id is not None:
|
|
292
402
|
raise ValueError("load_all=True cannot be used together with id.")
|
|
293
403
|
|
|
294
|
-
#
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
404
|
+
# The API paginates with page/per_page (it also accepts limit/offset
|
|
405
|
+
# as aliases under the same multiple-of-page-size rule); translate
|
|
406
|
+
# limit/offset here so the request is explicit and validated early.
|
|
407
|
+
page_size = 100 if limit is None else limit
|
|
408
|
+
if load_all:
|
|
409
|
+
page_size = min(page_size, self.MAX_PAGE_SIZE)
|
|
410
|
+
if not id and not 1 <= page_size <= self.MAX_PAGE_SIZE:
|
|
411
|
+
raise ValueError(
|
|
412
|
+
f"limit ({limit}) must be between 1 and {self.MAX_PAGE_SIZE}, "
|
|
413
|
+
"the API's maximum page size."
|
|
414
|
+
)
|
|
415
|
+
if not id and offset and offset % page_size:
|
|
416
|
+
raise ValueError(
|
|
417
|
+
f"offset ({offset}) must be a multiple of limit (the page size, "
|
|
418
|
+
f"{page_size}): the API pages with page/per_page."
|
|
419
|
+
)
|
|
420
|
+
start_page = (offset or 0) // page_size + 1
|
|
421
|
+
|
|
422
|
+
if load_all:
|
|
423
|
+
return self._load_all_pages(url, params, page_size, start_page=start_page)
|
|
424
|
+
|
|
425
|
+
if not id and (limit is not None or offset is not None):
|
|
426
|
+
params["per_page"] = page_size
|
|
427
|
+
params["page"] = start_page
|
|
428
|
+
return self._session.get(url, params=params, timeout=self.DEFAULT_TIMEOUT)
|
|
429
|
+
|
|
430
|
+
def _get_json(self, url: str, params: dict = None) -> dict:
|
|
431
|
+
"""GET *url* and return the decoded JSON, raising on HTTP errors."""
|
|
432
|
+
resp = self._session.get(url, params=params, timeout=self.DEFAULT_TIMEOUT)
|
|
433
|
+
if resp.status_code == 401:
|
|
434
|
+
raise AuthenticationError(
|
|
435
|
+
"API token is invalid or expired. Run `brainstem login` to re-authenticate."
|
|
436
|
+
)
|
|
437
|
+
resp.raise_for_status()
|
|
438
|
+
return resp.json()
|
|
439
|
+
|
|
440
|
+
def _load_all_pages(self, url: str, params: dict,
|
|
441
|
+
page_size: int = 100, start_page: int = 1) -> dict:
|
|
442
|
+
"""Follow pagination and merge every page into a single dict.
|
|
298
443
|
|
|
444
|
+
All list-valued keys are concatenated across pages: the primary
|
|
445
|
+
records as well as any sideloaded related records (de-duplicated by
|
|
446
|
+
``id``). Non-list keys are taken from the first page.
|
|
447
|
+
"""
|
|
448
|
+
params = dict(params, per_page=page_size)
|
|
449
|
+
page = start_page
|
|
299
450
|
combined: dict = {}
|
|
300
|
-
|
|
451
|
+
seen_ids: dict = {}
|
|
301
452
|
|
|
302
453
|
while True:
|
|
303
|
-
|
|
304
|
-
|
|
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()
|
|
454
|
+
data = self._get_json(url, dict(params, page=page))
|
|
455
|
+
lists = {k: v for k, v in data.items() if isinstance(v, list)}
|
|
310
456
|
|
|
311
|
-
if
|
|
312
|
-
|
|
313
|
-
records_key = next(
|
|
314
|
-
(k for k, v in data.items() if isinstance(v, list)), None
|
|
315
|
-
)
|
|
316
|
-
if records_key is None:
|
|
457
|
+
if not combined:
|
|
458
|
+
if not lists:
|
|
317
459
|
raise ValueError(
|
|
318
460
|
"load_all=True requires a paginated list response, but the API "
|
|
319
461
|
"returned no list-valued key. Use load_all=False for single-object endpoints."
|
|
320
462
|
)
|
|
321
|
-
combined = {k: v for k, v in data.items() if k
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
463
|
+
combined = {k: v for k, v in data.items() if k not in lists}
|
|
464
|
+
|
|
465
|
+
for key, records in lists.items():
|
|
466
|
+
bucket = combined.setdefault(key, [])
|
|
467
|
+
ids = seen_ids.setdefault(key, set())
|
|
468
|
+
for record in records:
|
|
469
|
+
rid = record.get("id") if isinstance(record, dict) else None
|
|
470
|
+
if rid is not None:
|
|
471
|
+
if rid in ids:
|
|
472
|
+
continue
|
|
473
|
+
ids.add(rid)
|
|
474
|
+
bucket.append(record)
|
|
475
|
+
|
|
476
|
+
total_pages = (data.get("meta") or {}).get("total_pages")
|
|
477
|
+
if total_pages is None and "count" in data:
|
|
478
|
+
total_pages = -(-data["count"] // page_size) # ceil division
|
|
479
|
+
if total_pages is None or page >= total_pages or not any(lists.values()):
|
|
328
480
|
break
|
|
329
|
-
|
|
330
|
-
params["offset"] = params.get("offset", 0) + page_size
|
|
481
|
+
page += 1
|
|
331
482
|
|
|
332
483
|
return combined
|
|
333
484
|
|
|
@@ -342,6 +493,14 @@ class BrainstemClient:
|
|
|
342
493
|
When *id* is provided the record is **updated** (PATCH).
|
|
343
494
|
When *id* is omitted a **new record** is created (POST).
|
|
344
495
|
|
|
496
|
+
.. warning::
|
|
497
|
+
Sending a list field (``projects``, ``subjects``, ``behaviors``, ...)
|
|
498
|
+
replaces the whole list. A list read with ``include`` (including the
|
|
499
|
+
convenience loaders' default includes) holds only the records you may
|
|
500
|
+
read, so writing it back drops links to records you cannot see. Read
|
|
501
|
+
the record without ``include`` first, e.g.
|
|
502
|
+
``client.load('session', id=uid).json()``, and change that list.
|
|
503
|
+
|
|
345
504
|
Parameters
|
|
346
505
|
----------
|
|
347
506
|
model : Model name string or ``ModelType`` member, e.g. ``'session'``.
|
|
@@ -382,6 +541,159 @@ class BrainstemClient:
|
|
|
382
541
|
url = self._build_url(portal, app, model, id)
|
|
383
542
|
return self._session.delete(url, timeout=self.DEFAULT_TIMEOUT)
|
|
384
543
|
|
|
544
|
+
# ------------------------------------------------------------------
|
|
545
|
+
# Export
|
|
546
|
+
# ------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
EXPORT_TIMEOUT: int = 90 # seconds; the server builds the whole export per request
|
|
549
|
+
|
|
550
|
+
def export_project(self, project_id: str, background: bool = False,
|
|
551
|
+
poll_interval: float = 5.0, timeout: float = 3600) -> dict:
|
|
552
|
+
"""Export a project as the server's versioned JSON export.
|
|
553
|
+
|
|
554
|
+
Returns the same payload as the JSON download on the BrainSTEM website
|
|
555
|
+
(format documented in the server's ``docs/project-export-format.md``)::
|
|
556
|
+
|
|
557
|
+
{
|
|
558
|
+
"export_info": {"format_version", "exported_at", "scope",
|
|
559
|
+
"project_id", "project_ids", "session_id",
|
|
560
|
+
"lab_resources", "restricted", "record_count",
|
|
561
|
+
"counts"},
|
|
562
|
+
"records": {"stem.project": [...], "stem.subject": [...], ...}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
Each record has the shape ``{"model", "pk", "fields"}``. Exporting
|
|
566
|
+
requires read access to the project.
|
|
567
|
+
|
|
568
|
+
Parameters
|
|
569
|
+
----------
|
|
570
|
+
project_id : UUID of the project to export.
|
|
571
|
+
background : When ``True``, queue the export as a server-side job,
|
|
572
|
+
wait for it to finish and download the result. Use this
|
|
573
|
+
for large projects, which can exceed the server's
|
|
574
|
+
30-second request limit. The server also emails you
|
|
575
|
+
when the job finishes.
|
|
576
|
+
poll_interval : Seconds between job status checks (background only).
|
|
577
|
+
timeout : Maximum seconds to wait for the job (background only).
|
|
578
|
+
"""
|
|
579
|
+
return self._export("project", project_id, background, poll_interval, timeout)
|
|
580
|
+
|
|
581
|
+
def export_session(self, session_id: str, background: bool = False,
|
|
582
|
+
poll_interval: float = 5.0, timeout: float = 3600) -> dict:
|
|
583
|
+
"""Export one session and everything it touches: the content of one NWB file.
|
|
584
|
+
|
|
585
|
+
Returns the same format as ``export_project`` (``export_info.scope`` is
|
|
586
|
+
``"session"``), holding the session with its data acquisitions,
|
|
587
|
+
manipulations, behaviors and epochs; the subjects and specimen sets
|
|
588
|
+
linked to it with their full procedure and log history; the session's
|
|
589
|
+
projects; and the lab resources, catalog entries and users these
|
|
590
|
+
reference. Only records you can read are included, in any of the
|
|
591
|
+
session's projects.
|
|
592
|
+
|
|
593
|
+
A subject's procedure history is complete, including procedures after
|
|
594
|
+
the session, so filter by date where that matters. Exporting requires
|
|
595
|
+
read access to one of the session's projects.
|
|
596
|
+
|
|
597
|
+
Parameters are the same as for ``export_project``, with
|
|
598
|
+
``session_id`` the UUID of the session to export.
|
|
599
|
+
"""
|
|
600
|
+
return self._export("session", session_id, background, poll_interval, timeout)
|
|
601
|
+
|
|
602
|
+
def _export(self, model: str, record_id: str, background: bool,
|
|
603
|
+
poll_interval: float, timeout: float) -> dict:
|
|
604
|
+
"""Run a direct or background export of a project or session record."""
|
|
605
|
+
if not record_id:
|
|
606
|
+
raise ValueError(f"'{model}_id' is required to export a {model}.")
|
|
607
|
+
url = self._build_url("private", "stem", model, record_id, "export/")
|
|
608
|
+
|
|
609
|
+
if not background:
|
|
610
|
+
# Bypass the session's automatic retries: a timed-out export would be
|
|
611
|
+
# rebuilt up to three more times, each counting against the export rate limit.
|
|
612
|
+
resp = requests.get(url, headers=self._session.headers, timeout=self.EXPORT_TIMEOUT)
|
|
613
|
+
if resp.status_code in (503, 504):
|
|
614
|
+
raise ExportError(
|
|
615
|
+
"The export did not finish within the server's request time limit. "
|
|
616
|
+
f"Use export_{model}(..., background=True) for large exports."
|
|
617
|
+
)
|
|
618
|
+
self._check_export_response(resp, model)
|
|
619
|
+
return resp.json()
|
|
620
|
+
|
|
621
|
+
resp = self._session.post(url, timeout=self.DEFAULT_TIMEOUT)
|
|
622
|
+
self._check_export_response(resp, model)
|
|
623
|
+
job = resp.json()["export_job"]
|
|
624
|
+
job_url = self._build_url("private", "stem", model, record_id,
|
|
625
|
+
f"export-jobs/{job['id']}/")
|
|
626
|
+
|
|
627
|
+
deadline = time.monotonic() + timeout
|
|
628
|
+
while job["status"] in ("pending", "running"):
|
|
629
|
+
if time.monotonic() > deadline:
|
|
630
|
+
raise ExportError(
|
|
631
|
+
f"Export job {job['id']} did not finish within {timeout} seconds "
|
|
632
|
+
f"(status: {job['status']}, {job.get('progress_percent', 0)}%)."
|
|
633
|
+
)
|
|
634
|
+
time.sleep(poll_interval)
|
|
635
|
+
job = self._get_json(job_url)["export_job"]
|
|
636
|
+
|
|
637
|
+
if job["status"] != "completed":
|
|
638
|
+
raise ExportError(job.get("error_message") or f"Export job ended as '{job['status']}'.")
|
|
639
|
+
|
|
640
|
+
resp = self._session.get(self._base_url + job["download_url"].lstrip("/"),
|
|
641
|
+
timeout=self.EXPORT_TIMEOUT)
|
|
642
|
+
self._check_export_response(resp, model)
|
|
643
|
+
content = resp.content
|
|
644
|
+
if content[:2] == b"\x1f\x8b": # gzip magic number
|
|
645
|
+
expected = job.get("file_sha256")
|
|
646
|
+
if expected and hashlib.sha256(content).hexdigest() != expected:
|
|
647
|
+
raise ExportError(f"Export job {job['id']}: downloaded file failed its checksum.")
|
|
648
|
+
content = gzip.decompress(content)
|
|
649
|
+
return json.loads(content)
|
|
650
|
+
|
|
651
|
+
def _check_export_response(self, resp: Response, model: str) -> None:
|
|
652
|
+
"""Turn export endpoint errors into clear exceptions."""
|
|
653
|
+
if resp.ok:
|
|
654
|
+
return
|
|
655
|
+
if resp.status_code == 401:
|
|
656
|
+
raise AuthenticationError(
|
|
657
|
+
"API token is invalid or expired. Run `brainstem login` to re-authenticate."
|
|
658
|
+
)
|
|
659
|
+
is_json = "json" in resp.headers.get("Content-Type", "")
|
|
660
|
+
if resp.status_code == 404:
|
|
661
|
+
if not is_json:
|
|
662
|
+
raise ExportError(f"This BrainSTEM server does not support {model} export yet.")
|
|
663
|
+
# The API answers 404, not 403, when the record exists but you cannot read it.
|
|
664
|
+
raise ExportError(f"The {model} was not found, or you do not have read access to it.")
|
|
665
|
+
if resp.status_code == 403:
|
|
666
|
+
raise ExportError(f"You do not have permission to export this {model}.")
|
|
667
|
+
if resp.status_code in (400, 409, 410, 429) and is_json:
|
|
668
|
+
raise ExportError(resp.json().get("detail", resp.reason))
|
|
669
|
+
resp.raise_for_status()
|
|
670
|
+
|
|
671
|
+
# ------------------------------------------------------------------
|
|
672
|
+
# Agent reference
|
|
673
|
+
# ------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
def agent_reference(self, page: str = "index.md") -> str:
|
|
676
|
+
"""Return one page of the server's agent reference as text.
|
|
677
|
+
|
|
678
|
+
The server describes its own API and data model at ``/reference/`` (Markdown
|
|
679
|
+
pages such as ``rules.md``, ``resources/session.md`` or
|
|
680
|
+
``guides/query-planning.md``, and JSON documents under ``data/``). It is
|
|
681
|
+
public, so this works with an anonymous client too.
|
|
682
|
+
|
|
683
|
+
Parameters
|
|
684
|
+
----------
|
|
685
|
+
page : Page path relative to the reference root. Defaults to the index.
|
|
686
|
+
"""
|
|
687
|
+
url = self._base_url + "reference/" + page.lstrip("/")
|
|
688
|
+
resp = self._session.get(url, timeout=self.DEFAULT_TIMEOUT)
|
|
689
|
+
if resp.status_code == 404:
|
|
690
|
+
raise ValueError(
|
|
691
|
+
f"No agent reference page '{page}' on this server. The page does not exist, "
|
|
692
|
+
"or the server predates the agent reference."
|
|
693
|
+
)
|
|
694
|
+
resp.raise_for_status()
|
|
695
|
+
return resp.text
|
|
696
|
+
|
|
385
697
|
def __enter__(self):
|
|
386
698
|
return self
|
|
387
699
|
|
|
@@ -431,13 +743,13 @@ class BrainstemClient:
|
|
|
431
743
|
name : Filter by project name (case-insensitive contains).
|
|
432
744
|
sessions : Filter by session UUID.
|
|
433
745
|
subjects : Filter by subject UUID.
|
|
434
|
-
tags : Filter by tag.
|
|
746
|
+
tags : Filter by tag name (sent as ``filter{tags.name}``; records return tags as names).
|
|
435
747
|
"""
|
|
436
748
|
return self._convenience_load(
|
|
437
749
|
"project",
|
|
438
750
|
default_include=["sessions", "subjects", "collections", "cohorts"],
|
|
439
|
-
filter_map={"name": "name.icontains", "sessions": "sessions
|
|
440
|
-
"subjects": "subjects
|
|
751
|
+
filter_map={"name": "name.icontains", "sessions": "sessions",
|
|
752
|
+
"subjects": "subjects", "tags": "tags.name"},
|
|
441
753
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
442
754
|
limit=limit, offset=offset, load_all=load_all,
|
|
443
755
|
name=name, sessions=sessions, subjects=subjects, tags=tags,
|
|
@@ -457,13 +769,13 @@ class BrainstemClient:
|
|
|
457
769
|
projects : Filter by project UUID.
|
|
458
770
|
strain : Filter by strain UUID.
|
|
459
771
|
sex : Filter by sex (``'M'``, ``'F'``, or ``'U'``).
|
|
460
|
-
tags : Filter by tag.
|
|
772
|
+
tags : Filter by tag name (sent as ``filter{tags.name}``; records return tags as names).
|
|
461
773
|
"""
|
|
462
774
|
return self._convenience_load(
|
|
463
775
|
"subject",
|
|
464
776
|
default_include=["procedures", "subjectlogs"],
|
|
465
|
-
filter_map={"name": "name.icontains", "projects": "projects
|
|
466
|
-
"strain": "strain
|
|
777
|
+
filter_map={"name": "name.icontains", "projects": "projects",
|
|
778
|
+
"strain": "strain", "sex": "sex", "tags": "tags.name"},
|
|
467
779
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
468
780
|
limit=limit, offset=offset, load_all=load_all,
|
|
469
781
|
name=name, projects=projects, strain=strain, sex=sex, tags=tags,
|
|
@@ -482,13 +794,13 @@ class BrainstemClient:
|
|
|
482
794
|
name : Filter by session name (case-insensitive contains).
|
|
483
795
|
projects : Filter by project UUID.
|
|
484
796
|
datastorage : Filter by data storage UUID.
|
|
485
|
-
tags : Filter by tag.
|
|
797
|
+
tags : Filter by tag name (sent as ``filter{tags.name}``; records return tags as names).
|
|
486
798
|
"""
|
|
487
799
|
return self._convenience_load(
|
|
488
800
|
"session",
|
|
489
801
|
default_include=["dataacquisition", "behaviors", "manipulations", "epochs"],
|
|
490
|
-
filter_map={"name": "name.icontains", "projects": "projects
|
|
491
|
-
"datastorage": "datastorage
|
|
802
|
+
filter_map={"name": "name.icontains", "projects": "projects",
|
|
803
|
+
"datastorage": "datastorage", "tags": "tags.name"},
|
|
492
804
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
493
805
|
limit=limit, offset=offset, load_all=load_all,
|
|
494
806
|
name=name, projects=projects, datastorage=datastorage, tags=tags,
|
|
@@ -504,12 +816,12 @@ class BrainstemClient:
|
|
|
504
816
|
Parameters
|
|
505
817
|
----------
|
|
506
818
|
name : Filter by collection name (case-insensitive contains).
|
|
507
|
-
tags : Filter by tag.
|
|
819
|
+
tags : Filter by tag name (sent as ``filter{tags.name}``; records return tags as names).
|
|
508
820
|
"""
|
|
509
821
|
return self._convenience_load(
|
|
510
822
|
"collection",
|
|
511
823
|
default_include=["sessions"],
|
|
512
|
-
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
824
|
+
filter_map={"name": "name.icontains", "tags": "tags.name"},
|
|
513
825
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
514
826
|
limit=limit, offset=offset, load_all=load_all,
|
|
515
827
|
name=name, tags=tags,
|
|
@@ -525,19 +837,19 @@ class BrainstemClient:
|
|
|
525
837
|
Parameters
|
|
526
838
|
----------
|
|
527
839
|
name : Filter by cohort name (case-insensitive contains).
|
|
528
|
-
tags : Filter by tag.
|
|
840
|
+
tags : Filter by tag name (sent as ``filter{tags.name}``; records return tags as names).
|
|
529
841
|
"""
|
|
530
842
|
return self._convenience_load(
|
|
531
843
|
"cohort",
|
|
532
844
|
default_include=["subjects"],
|
|
533
|
-
filter_map={"name": "name.icontains", "tags": "tags"},
|
|
845
|
+
filter_map={"name": "name.icontains", "tags": "tags.name"},
|
|
534
846
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
535
847
|
limit=limit, offset=offset, load_all=load_all,
|
|
536
848
|
name=name, tags=tags,
|
|
537
849
|
)
|
|
538
850
|
|
|
539
851
|
def load_behavior(self, portal="private", id: str = None,
|
|
540
|
-
session: str = None,
|
|
852
|
+
session: str = None,
|
|
541
853
|
filters: dict = None, sort: list = None,
|
|
542
854
|
include: list = None, limit: int = None,
|
|
543
855
|
offset: int = None, load_all: bool = False):
|
|
@@ -546,19 +858,18 @@ class BrainstemClient:
|
|
|
546
858
|
Parameters
|
|
547
859
|
----------
|
|
548
860
|
session : Filter by session UUID.
|
|
549
|
-
tags : Filter by tag.
|
|
550
861
|
"""
|
|
551
862
|
return self._convenience_load(
|
|
552
863
|
"behavior",
|
|
553
864
|
default_include=[],
|
|
554
|
-
filter_map={"session": "session
|
|
865
|
+
filter_map={"session": "session"},
|
|
555
866
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
556
867
|
limit=limit, offset=offset, load_all=load_all,
|
|
557
|
-
session=session,
|
|
868
|
+
session=session,
|
|
558
869
|
)
|
|
559
870
|
|
|
560
871
|
def load_dataacquisition(self, portal="private", id: str = None,
|
|
561
|
-
session: str = None,
|
|
872
|
+
session: str = None,
|
|
562
873
|
filters: dict = None, sort: list = None,
|
|
563
874
|
include: list = None, limit: int = None,
|
|
564
875
|
offset: int = None, load_all: bool = False):
|
|
@@ -567,19 +878,18 @@ class BrainstemClient:
|
|
|
567
878
|
Parameters
|
|
568
879
|
----------
|
|
569
880
|
session : Filter by session UUID.
|
|
570
|
-
tags : Filter by tag.
|
|
571
881
|
"""
|
|
572
882
|
return self._convenience_load(
|
|
573
883
|
"dataacquisition",
|
|
574
884
|
default_include=[],
|
|
575
|
-
filter_map={"session": "session
|
|
885
|
+
filter_map={"session": "session"},
|
|
576
886
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
577
887
|
limit=limit, offset=offset, load_all=load_all,
|
|
578
|
-
session=session,
|
|
888
|
+
session=session,
|
|
579
889
|
)
|
|
580
890
|
|
|
581
891
|
def load_manipulation(self, portal="private", id: str = None,
|
|
582
|
-
session: str = None,
|
|
892
|
+
session: str = None,
|
|
583
893
|
filters: dict = None, sort: list = None,
|
|
584
894
|
include: list = None, limit: int = None,
|
|
585
895
|
offset: int = None, load_all: bool = False):
|
|
@@ -588,19 +898,18 @@ class BrainstemClient:
|
|
|
588
898
|
Parameters
|
|
589
899
|
----------
|
|
590
900
|
session : Filter by session UUID.
|
|
591
|
-
tags : Filter by tag.
|
|
592
901
|
"""
|
|
593
902
|
return self._convenience_load(
|
|
594
903
|
"manipulation",
|
|
595
904
|
default_include=[],
|
|
596
|
-
filter_map={"session": "session
|
|
905
|
+
filter_map={"session": "session"},
|
|
597
906
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
598
907
|
limit=limit, offset=offset, load_all=load_all,
|
|
599
|
-
session=session,
|
|
908
|
+
session=session,
|
|
600
909
|
)
|
|
601
910
|
|
|
602
911
|
def load_procedure(self, portal="private", id: str = None,
|
|
603
|
-
subject: str = None,
|
|
912
|
+
subject: str = None,
|
|
604
913
|
filters: dict = None, sort: list = None,
|
|
605
914
|
include: list = None, limit: int = None,
|
|
606
915
|
offset: int = None, load_all: bool = False):
|
|
@@ -609,19 +918,18 @@ class BrainstemClient:
|
|
|
609
918
|
Parameters
|
|
610
919
|
----------
|
|
611
920
|
subject : Filter by subject UUID.
|
|
612
|
-
tags : Filter by tag.
|
|
613
921
|
"""
|
|
614
922
|
return self._convenience_load(
|
|
615
923
|
"procedure",
|
|
616
924
|
default_include=[],
|
|
617
|
-
filter_map={"subject": "subject
|
|
925
|
+
filter_map={"subject": "subject"},
|
|
618
926
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
619
927
|
limit=limit, offset=offset, load_all=load_all,
|
|
620
|
-
subject=subject,
|
|
928
|
+
subject=subject,
|
|
621
929
|
)
|
|
622
930
|
|
|
623
931
|
def load_procedurelog(self, portal="private", id: str = None,
|
|
624
|
-
procedure: str = None,
|
|
932
|
+
procedure: str = None,
|
|
625
933
|
filters: dict = None, sort: list = None,
|
|
626
934
|
include: list = None, limit: int = None,
|
|
627
935
|
offset: int = None, load_all: bool = False):
|
|
@@ -630,19 +938,18 @@ class BrainstemClient:
|
|
|
630
938
|
Parameters
|
|
631
939
|
----------
|
|
632
940
|
procedure : Filter by procedure UUID.
|
|
633
|
-
tags : Filter by tag.
|
|
634
941
|
"""
|
|
635
942
|
return self._convenience_load(
|
|
636
943
|
"procedurelog",
|
|
637
944
|
default_include=[],
|
|
638
|
-
filter_map={"procedure": "procedure
|
|
945
|
+
filter_map={"procedure": "procedure"},
|
|
639
946
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
640
947
|
limit=limit, offset=offset, load_all=load_all,
|
|
641
|
-
procedure=procedure,
|
|
948
|
+
procedure=procedure,
|
|
642
949
|
)
|
|
643
950
|
|
|
644
951
|
def load_subjectlog(self, portal="private", id: str = None,
|
|
645
|
-
subject: str = None,
|
|
952
|
+
subject: str = None,
|
|
646
953
|
filters: dict = None, sort: list = None,
|
|
647
954
|
include: list = None, limit: int = None,
|
|
648
955
|
offset: int = None, load_all: bool = False):
|
|
@@ -651,19 +958,18 @@ class BrainstemClient:
|
|
|
651
958
|
Parameters
|
|
652
959
|
----------
|
|
653
960
|
subject : Filter by subject UUID.
|
|
654
|
-
tags : Filter by tag.
|
|
655
961
|
"""
|
|
656
962
|
return self._convenience_load(
|
|
657
963
|
"subjectlog",
|
|
658
964
|
default_include=[],
|
|
659
|
-
filter_map={"subject": "subject
|
|
965
|
+
filter_map={"subject": "subject"},
|
|
660
966
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
661
967
|
limit=limit, offset=offset, load_all=load_all,
|
|
662
|
-
subject=subject,
|
|
968
|
+
subject=subject,
|
|
663
969
|
)
|
|
664
970
|
|
|
665
971
|
def load_equipment(self, portal="private", id: str = None,
|
|
666
|
-
name: str = None, setup: str = None,
|
|
972
|
+
name: str = None, setup: str = None,
|
|
667
973
|
filters: dict = None, sort: list = None,
|
|
668
974
|
include: list = None, limit: int = None,
|
|
669
975
|
offset: int = None, load_all: bool = False):
|
|
@@ -673,19 +979,18 @@ class BrainstemClient:
|
|
|
673
979
|
----------
|
|
674
980
|
name : Filter by equipment name (case-insensitive contains).
|
|
675
981
|
setup : Filter by setup UUID.
|
|
676
|
-
tags : Filter by tag.
|
|
677
982
|
"""
|
|
678
983
|
return self._convenience_load(
|
|
679
984
|
"equipment",
|
|
680
985
|
default_include=[],
|
|
681
|
-
filter_map={"name": "name.icontains", "setup": "setup
|
|
986
|
+
filter_map={"name": "name.icontains", "setup": "setup"},
|
|
682
987
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
683
988
|
limit=limit, offset=offset, load_all=load_all,
|
|
684
|
-
name=name, setup=setup,
|
|
989
|
+
name=name, setup=setup,
|
|
685
990
|
)
|
|
686
991
|
|
|
687
992
|
def load_consumablestock(self, portal="private", id: str = None,
|
|
688
|
-
|
|
993
|
+
inventory: str = None,
|
|
689
994
|
filters: dict = None, sort: list = None,
|
|
690
995
|
include: list = None, limit: int = None,
|
|
691
996
|
offset: int = None, load_all: bool = False):
|
|
@@ -693,19 +998,19 @@ class BrainstemClient:
|
|
|
693
998
|
|
|
694
999
|
Parameters
|
|
695
1000
|
----------
|
|
696
|
-
|
|
1001
|
+
inventory : Filter by inventory UUID.
|
|
697
1002
|
"""
|
|
698
1003
|
return self._convenience_load(
|
|
699
1004
|
"consumablestock",
|
|
700
1005
|
default_include=[],
|
|
701
|
-
filter_map={"
|
|
1006
|
+
filter_map={"inventory": "inventory"},
|
|
702
1007
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
703
1008
|
limit=limit, offset=offset, load_all=load_all,
|
|
704
|
-
|
|
1009
|
+
inventory=inventory,
|
|
705
1010
|
)
|
|
706
1011
|
|
|
707
1012
|
def load_behavioralassay(self, portal="private", id: str = None,
|
|
708
|
-
name: str = None,
|
|
1013
|
+
name: str = None,
|
|
709
1014
|
filters: dict = None, sort: list = None,
|
|
710
1015
|
include: list = None, limit: int = None,
|
|
711
1016
|
offset: int = None, load_all: bool = False):
|
|
@@ -714,19 +1019,18 @@ class BrainstemClient:
|
|
|
714
1019
|
Parameters
|
|
715
1020
|
----------
|
|
716
1021
|
name : Filter by name (case-insensitive contains).
|
|
717
|
-
tags : Filter by tag.
|
|
718
1022
|
"""
|
|
719
1023
|
return self._convenience_load(
|
|
720
1024
|
"behavioralassay",
|
|
721
1025
|
default_include=[],
|
|
722
|
-
filter_map={"name": "name.icontains"
|
|
1026
|
+
filter_map={"name": "name.icontains"},
|
|
723
1027
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
724
1028
|
limit=limit, offset=offset, load_all=load_all,
|
|
725
|
-
name=name,
|
|
1029
|
+
name=name,
|
|
726
1030
|
)
|
|
727
1031
|
|
|
728
1032
|
def load_datastorage(self, portal="private", id: str = None,
|
|
729
|
-
name: str = None,
|
|
1033
|
+
name: str = None,
|
|
730
1034
|
filters: dict = None, sort: list = None,
|
|
731
1035
|
include: list = None, limit: int = None,
|
|
732
1036
|
offset: int = None, load_all: bool = False):
|
|
@@ -735,19 +1039,18 @@ class BrainstemClient:
|
|
|
735
1039
|
Parameters
|
|
736
1040
|
----------
|
|
737
1041
|
name : Filter by name (case-insensitive contains).
|
|
738
|
-
tags : Filter by tag.
|
|
739
1042
|
"""
|
|
740
1043
|
return self._convenience_load(
|
|
741
1044
|
"datastorage",
|
|
742
1045
|
default_include=[],
|
|
743
|
-
filter_map={"name": "name.icontains"
|
|
1046
|
+
filter_map={"name": "name.icontains"},
|
|
744
1047
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
745
1048
|
limit=limit, offset=offset, load_all=load_all,
|
|
746
|
-
name=name,
|
|
1049
|
+
name=name,
|
|
747
1050
|
)
|
|
748
1051
|
|
|
749
1052
|
def load_setup(self, portal="private", id: str = None,
|
|
750
|
-
name: str = None,
|
|
1053
|
+
name: str = None,
|
|
751
1054
|
filters: dict = None, sort: list = None,
|
|
752
1055
|
include: list = None, limit: int = None,
|
|
753
1056
|
offset: int = None, load_all: bool = False):
|
|
@@ -756,19 +1059,18 @@ class BrainstemClient:
|
|
|
756
1059
|
Parameters
|
|
757
1060
|
----------
|
|
758
1061
|
name : Filter by name (case-insensitive contains).
|
|
759
|
-
tags : Filter by tag.
|
|
760
1062
|
"""
|
|
761
1063
|
return self._convenience_load(
|
|
762
1064
|
"setup",
|
|
763
1065
|
default_include=[],
|
|
764
|
-
filter_map={"name": "name.icontains"
|
|
1066
|
+
filter_map={"name": "name.icontains"},
|
|
765
1067
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
766
1068
|
limit=limit, offset=offset, load_all=load_all,
|
|
767
|
-
name=name,
|
|
1069
|
+
name=name,
|
|
768
1070
|
)
|
|
769
1071
|
|
|
770
1072
|
def load_hardwaredevice(self, portal="private", id: str = None,
|
|
771
|
-
name: str = None,
|
|
1073
|
+
name: str = None,
|
|
772
1074
|
filters: dict = None, sort: list = None,
|
|
773
1075
|
include: list = None, limit: int = None,
|
|
774
1076
|
offset: int = None, load_all: bool = False):
|
|
@@ -777,83 +1079,128 @@ class BrainstemClient:
|
|
|
777
1079
|
Parameters
|
|
778
1080
|
----------
|
|
779
1081
|
name : Filter by name (case-insensitive contains).
|
|
780
|
-
tags : Filter by tag.
|
|
781
1082
|
"""
|
|
782
1083
|
return self._convenience_load(
|
|
783
1084
|
"hardwaredevice",
|
|
784
1085
|
default_include=[],
|
|
785
|
-
filter_map={"name": "name.icontains"
|
|
1086
|
+
filter_map={"name": "name.icontains"},
|
|
786
1087
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
787
1088
|
limit=limit, offset=offset, load_all=load_all,
|
|
788
|
-
name=name,
|
|
1089
|
+
name=name,
|
|
789
1090
|
)
|
|
790
1091
|
|
|
791
1092
|
def load_brainregion(self, portal="private", id: str = None,
|
|
792
|
-
name: str = None,
|
|
1093
|
+
name: str = None,
|
|
793
1094
|
filters: dict = None, sort: list = None,
|
|
794
1095
|
include: list = None, limit: int = None,
|
|
795
1096
|
offset: int = None, load_all: bool = False):
|
|
796
|
-
"""
|
|
1097
|
+
"""Deprecated: use ``load_anatomicalregion``, which this calls.
|
|
1098
|
+
|
|
1099
|
+
Brain regions are ``anatomicalregion`` records; the response holds them
|
|
1100
|
+
under ``anatomical_regions``.
|
|
797
1101
|
|
|
798
1102
|
Parameters
|
|
799
1103
|
----------
|
|
800
1104
|
name : Filter by name (case-insensitive contains).
|
|
801
|
-
|
|
1105
|
+
"""
|
|
1106
|
+
warnings.warn(
|
|
1107
|
+
"load_brainregion() is deprecated; use load_anatomicalregion().",
|
|
1108
|
+
FutureWarning, stacklevel=2,
|
|
1109
|
+
)
|
|
1110
|
+
return self.load_anatomicalregion(
|
|
1111
|
+
portal=portal, id=id, name=name, filters=filters, sort=sort,
|
|
1112
|
+
include=include, limit=limit, offset=offset, load_all=load_all,
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
def load_anatomicalregion(self, portal="private", id: str = None,
|
|
1116
|
+
name: str = None, acronym: str = None,
|
|
1117
|
+
reference: str = None,
|
|
1118
|
+
filters: dict = None, sort: list = None,
|
|
1119
|
+
include: list = None, limit: int = None,
|
|
1120
|
+
offset: int = None, load_all: bool = False):
|
|
1121
|
+
"""Load anatomical (brain) region record(s).
|
|
1122
|
+
|
|
1123
|
+
Parameters
|
|
1124
|
+
----------
|
|
1125
|
+
name : Filter by name (case-insensitive contains).
|
|
1126
|
+
acronym : Filter by acronym (case-insensitive exact match), e.g. ``'CA1'``.
|
|
1127
|
+
reference : Filter by anatomical reference (atlas) UUID.
|
|
802
1128
|
"""
|
|
803
1129
|
return self._convenience_load(
|
|
804
|
-
"
|
|
1130
|
+
"anatomicalregion",
|
|
805
1131
|
default_include=[],
|
|
806
|
-
filter_map={"name": "name.icontains", "
|
|
1132
|
+
filter_map={"name": "name.icontains", "acronym": "acronym.iexact",
|
|
1133
|
+
"reference": "reference"},
|
|
807
1134
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
808
1135
|
limit=limit, offset=offset, load_all=load_all,
|
|
809
|
-
name=name,
|
|
1136
|
+
name=name, acronym=acronym, reference=reference,
|
|
810
1137
|
)
|
|
811
1138
|
|
|
1139
|
+
def load_anatomicalregion_descendants(self, id: str, portal="private") -> Response:
|
|
1140
|
+
"""Load every region below anatomical region *id* in its atlas.
|
|
1141
|
+
|
|
1142
|
+
Returns the raw ``Response``; its JSON is
|
|
1143
|
+
``{"anatomical_regions": [{"id", "name", "acronym", "parent_acronym",
|
|
1144
|
+
"is_obsolete", "depth"}, ...]}``, ordered by depth (1 = direct child).
|
|
1145
|
+
Atlases without a hierarchy return an empty list.
|
|
1146
|
+
"""
|
|
1147
|
+
if not id:
|
|
1148
|
+
raise ValueError("'id' is required to load the descendants of a region.")
|
|
1149
|
+
url = self._build_url(_resolve_portal(portal), "taxonomies", "anatomicalregion",
|
|
1150
|
+
id, "descendants/")
|
|
1151
|
+
return self._session.get(url, timeout=self.DEFAULT_TIMEOUT)
|
|
1152
|
+
|
|
812
1153
|
def load_species(self, portal="private", id: str = None,
|
|
813
|
-
name: str = None,
|
|
1154
|
+
name: str = None,
|
|
814
1155
|
filters: dict = None, sort: list = None,
|
|
815
1156
|
include: list = None, limit: int = None,
|
|
816
|
-
offset: int = None, load_all: bool = False
|
|
1157
|
+
offset: int = None, load_all: bool = False,
|
|
1158
|
+
popular_name: str = None):
|
|
817
1159
|
"""Load species record(s).
|
|
818
1160
|
|
|
819
1161
|
Parameters
|
|
820
1162
|
----------
|
|
821
|
-
name
|
|
822
|
-
|
|
1163
|
+
name : Filter by scientific name (case-insensitive contains).
|
|
1164
|
+
popular_name : Filter by common name (case-insensitive contains), e.g.
|
|
1165
|
+
``'mouse'``. Needs a server with the specimen release.
|
|
823
1166
|
"""
|
|
824
1167
|
return self._convenience_load(
|
|
825
1168
|
"species",
|
|
826
1169
|
default_include=[],
|
|
827
|
-
filter_map={"name": "name.icontains",
|
|
1170
|
+
filter_map={"name": "name.icontains",
|
|
1171
|
+
"popular_name": "popular_name.icontains"},
|
|
828
1172
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
829
1173
|
limit=limit, offset=offset, load_all=load_all,
|
|
830
|
-
name=name,
|
|
1174
|
+
name=name, popular_name=popular_name,
|
|
831
1175
|
)
|
|
832
1176
|
|
|
833
1177
|
def load_strain(self, portal="private", id: str = None,
|
|
834
|
-
name: str = None, species: str = None,
|
|
1178
|
+
name: str = None, species: str = None,
|
|
835
1179
|
filters: dict = None, sort: list = None,
|
|
836
1180
|
include: list = None, limit: int = None,
|
|
837
|
-
offset: int = None, load_all: bool = False
|
|
1181
|
+
offset: int = None, load_all: bool = False,
|
|
1182
|
+
popular_name: str = None):
|
|
838
1183
|
"""Load strain record(s).
|
|
839
1184
|
|
|
840
1185
|
Parameters
|
|
841
1186
|
----------
|
|
842
|
-
name
|
|
843
|
-
species
|
|
844
|
-
|
|
1187
|
+
name : Filter by name (case-insensitive contains).
|
|
1188
|
+
species : Filter by species UUID.
|
|
1189
|
+
popular_name : Filter by common name (case-insensitive contains).
|
|
1190
|
+
Needs a server with the specimen release.
|
|
845
1191
|
"""
|
|
846
1192
|
return self._convenience_load(
|
|
847
1193
|
"strain",
|
|
848
1194
|
default_include=[],
|
|
849
|
-
filter_map={"name": "name.icontains", "species": "species
|
|
1195
|
+
filter_map={"name": "name.icontains", "species": "species",
|
|
1196
|
+
"popular_name": "popular_name.icontains"},
|
|
850
1197
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
851
1198
|
limit=limit, offset=offset, load_all=load_all,
|
|
852
|
-
name=name, species=species,
|
|
1199
|
+
name=name, species=species, popular_name=popular_name,
|
|
853
1200
|
)
|
|
854
1201
|
|
|
855
1202
|
def load_publication(self, portal="private", id: str = None,
|
|
856
|
-
name: str = None,
|
|
1203
|
+
name: str = None,
|
|
857
1204
|
filters: dict = None, sort: list = None,
|
|
858
1205
|
include: list = None, limit: int = None,
|
|
859
1206
|
offset: int = None, load_all: bool = False):
|
|
@@ -862,19 +1209,18 @@ class BrainstemClient:
|
|
|
862
1209
|
Parameters
|
|
863
1210
|
----------
|
|
864
1211
|
name : Filter by name (case-insensitive contains).
|
|
865
|
-
tags : Filter by tag.
|
|
866
1212
|
"""
|
|
867
1213
|
return self._convenience_load(
|
|
868
1214
|
"publication",
|
|
869
1215
|
default_include=[],
|
|
870
|
-
filter_map={"name": "name.icontains"
|
|
1216
|
+
filter_map={"name": "name.icontains"},
|
|
871
1217
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
872
1218
|
limit=limit, offset=offset, load_all=load_all,
|
|
873
|
-
name=name,
|
|
1219
|
+
name=name,
|
|
874
1220
|
)
|
|
875
1221
|
|
|
876
1222
|
def load_laboratory(self, portal="private", id: str = None,
|
|
877
|
-
name: str = None,
|
|
1223
|
+
name: str = None,
|
|
878
1224
|
filters: dict = None, sort: list = None,
|
|
879
1225
|
include: list = None, limit: int = None,
|
|
880
1226
|
offset: int = None, load_all: bool = False):
|
|
@@ -883,13 +1229,12 @@ class BrainstemClient:
|
|
|
883
1229
|
Parameters
|
|
884
1230
|
----------
|
|
885
1231
|
name : Filter by name (case-insensitive contains).
|
|
886
|
-
tags : Filter by tag.
|
|
887
1232
|
"""
|
|
888
1233
|
return self._convenience_load(
|
|
889
1234
|
"laboratory",
|
|
890
1235
|
default_include=[],
|
|
891
|
-
filter_map={"name": "name.icontains"
|
|
1236
|
+
filter_map={"name": "name.icontains"},
|
|
892
1237
|
portal=portal, id=id, filters=filters, sort=sort, include=include,
|
|
893
1238
|
limit=limit, offset=offset, load_all=load_all,
|
|
894
|
-
name=name,
|
|
1239
|
+
name=name,
|
|
895
1240
|
)
|