brainstem-python-api-tools 0.1.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.
@@ -1,43 +1,104 @@
1
1
  import os
2
- from getpass import getpass
3
- import requests
4
- from requests.models import Response
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
- class ModelType(Enum):
10
- project = "project"
11
- subject = "subject"
12
- session = "session"
13
- collection = "collection"
14
- cohort = "cohort"
15
- procedure = "procedure"
16
- behavior = "behavior"
17
- dataacquisition = "dataacquisition"
18
- manipulation = "manipulation"
19
- equipment = "equipment"
20
- consumablestock = "consumablestock"
21
- procedurelog = "procedurelog"
22
- subjectlog = "subjectlog"
23
- behavioralparadigm = "behavioralparadigm"
24
- datastorage = "datastorage"
25
- inventory = "inventory"
26
- setup = "setup"
27
- consumable = "consumable"
28
- hardwaredevice = "hardwaredevice"
29
- supplier = "supplier"
30
- brainregion = "brainregion"
31
- setuptype = "setuptype"
32
- species = "species"
33
- strain = "strain"
34
- strainapproval = "strainapproval"
35
- journal = "journal"
36
- laboratory = "laboratory"
37
- publication = "publication"
38
- journalapproval = "journalapproval"
39
- user = "user"
40
- group = "group"
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
- class LoginError(Exception):
50
- def __str__(self):
51
- return f'User/password combination incorrect or user does not exist'
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
- def __init__(self,
57
- token: str = None) -> None:
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
- # Server path
60
- self._address = 'https://www.brainstem.org/api/'
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
- username = input("Please enter your username:")
66
- password = getpass("Please enter your password:")
67
-
68
- self._token = self.__set_token_authentication(
69
- url=self._address + "token/",
70
- username=username,
71
- password=password,
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
- print("Your authorization token is:\n", self._token)
75
- print("\nPlease keep it in a safe place.")
76
-
77
- def __set_token_authentication(self,
78
- url: str,
79
- username: str,
80
- password: str) -> str:
81
-
82
- headers = {
83
- "accept": "application/json",
84
- "Content-Type": "application/json"
85
- }
86
- params = {
87
- "username": username,
88
- "password": password
89
- }
90
- resp = requests.post(url, headers=headers, json=params)
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
- query_parameters = id + "/" + options
153
- else:
154
- query_parameters = ""
155
-
156
- if filters:
157
- for key in filters.keys():
158
- if query_parameters == "":
159
- prefix = "?"
160
- else:
161
- prefix = "&"
162
- query_parameters += (prefix
163
- + "filter{" + key + "}="
164
- + filters[key])
165
- if sort:
166
- for elem in sort:
167
- if query_parameters == "":
168
- prefix = "?"
169
- else:
170
- prefix = "&"
171
- query_parameters += prefix + "sort[]=" + elem
172
-
173
- if include:
174
- for elem in include:
175
- if query_parameters == "":
176
- prefix = "?"
177
- else:
178
- prefix = "&"
179
- query_parameters += prefix + "include[]=" + elem + ".*"
180
-
181
- request_url = (self._address + portal
182
- + "/" + app + "/" + model
183
- + "/" + query_parameters)
184
-
185
- resp = requests.get(request_url,
186
- headers={"Authorization": "Bearer %s"
187
- % self._token})
188
- return resp
189
-
190
- def save_model(self,
191
- model: ModelType,
192
- portal: PortalType = "private",
193
- id: str = None,
194
- data: dict = None,
195
- options: str = None) -> Response:
196
-
197
- app = self.__get_app_from_model(model)
198
- if app is None:
199
- resp = Response()
200
- resp.status_code = 404
201
- return resp
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
- # This is an update request
212
- request_url = (self._address + portal + "/" + app + "/" + model
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
- # This is a create request
220
- request_url = (self._address + portal + "/" + app + "/"
221
- + model + "/" + options)
222
- resp = requests.post(request_url, json=data,
223
- headers={"Authorization": "Bearer %s"
224
- % self._token})
225
-
226
- return resp
227
-
228
- def delete_model(self,
229
- model: ModelType,
230
- portal: PortalType = "private",
231
- id: str = None):
232
-
233
- app = self.__get_app_from_model(model)
234
- if app is None:
235
- resp = Response()
236
- resp.status_code = 404
237
- return resp
238
-
239
- # Check if entry already exists
240
- if id is not None:
241
- request_url = (self._address + portal + "/" + app + "/" + model
242
- + "/" + id + "/")
243
- resp = requests.delete(request_url,
244
- headers={"Authorization": "Bearer %s"
245
- % self._token})
246
-
247
- return resp
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
+ )