brainstem-python-api-tools 0.2.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 +3 -1
- brainstem_api_tools/brainstem_api_client.py +1206 -213
- brainstem_api_tools/cli.py +255 -0
- 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-0.2.0.dist-info → brainstem_python_api_tools-2.1.0.dist-info}/WHEEL +1 -1
- brainstem_python_api_tools-2.1.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.1.0.dist-info/licenses}/LICENSE +0 -0
- {brainstem_python_api_tools-0.2.0.dist-info → brainstem_python_api_tools-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Command-line interface for BrainSTEM API tools.
|
|
2
|
+
|
|
3
|
+
Usage examples
|
|
4
|
+
--------------
|
|
5
|
+
brainstem login
|
|
6
|
+
brainstem login --headless
|
|
7
|
+
brainstem login --url http://127.0.0.1:8000/
|
|
8
|
+
brainstem logout
|
|
9
|
+
brainstem load session
|
|
10
|
+
brainstem load session --portal public --filters name.icontains=rat --sort -name
|
|
11
|
+
brainstem load session --id <uuid>
|
|
12
|
+
brainstem load session --limit 20 --offset 40
|
|
13
|
+
brainstem load session --per-page 20 --page 3
|
|
14
|
+
brainstem load session --all
|
|
15
|
+
brainstem save session --data '{"name":"New","projects":["<uuid>"]}'
|
|
16
|
+
brainstem save session --id <uuid> --data '{"description":"updated"}'
|
|
17
|
+
brainstem delete session --id <uuid>
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
import requests
|
|
26
|
+
|
|
27
|
+
from .brainstem_api_client import (
|
|
28
|
+
AuthenticationError, BrainstemClient, _MODEL_TO_APP, _TOKEN_FILE,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _common_flags(suppress_defaults: bool = False) -> argparse.ArgumentParser:
|
|
33
|
+
"""Shared flags available before and after the subcommand.
|
|
34
|
+
|
|
35
|
+
The subcommand copies use ``argparse.SUPPRESS`` defaults so they do not
|
|
36
|
+
overwrite a flag given before the subcommand (``brainstem --url X load``).
|
|
37
|
+
"""
|
|
38
|
+
def default(value):
|
|
39
|
+
return argparse.SUPPRESS if suppress_defaults else value
|
|
40
|
+
|
|
41
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
42
|
+
common.add_argument(
|
|
43
|
+
"--token",
|
|
44
|
+
default=default(os.environ.get("BRAINSTEM_API_TOKEN")),
|
|
45
|
+
help="API token. Defaults to $BRAINSTEM_API_TOKEN or the cached token.",
|
|
46
|
+
)
|
|
47
|
+
common.add_argument(
|
|
48
|
+
"--headless",
|
|
49
|
+
action="store_true",
|
|
50
|
+
default=default(False),
|
|
51
|
+
help="Print verification URL + code instead of opening a browser.",
|
|
52
|
+
)
|
|
53
|
+
common.add_argument(
|
|
54
|
+
"--url",
|
|
55
|
+
default=default(os.environ.get("BRAINSTEM_API_URL")),
|
|
56
|
+
help="Base URL of the BrainSTEM server. Defaults to $BRAINSTEM_API_URL "
|
|
57
|
+
"or https://www.brainstem.org/.",
|
|
58
|
+
)
|
|
59
|
+
return common
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
63
|
+
common = _common_flags(suppress_defaults=True)
|
|
64
|
+
|
|
65
|
+
parser = argparse.ArgumentParser(
|
|
66
|
+
prog="brainstem",
|
|
67
|
+
description="BrainSTEM command-line API client.",
|
|
68
|
+
parents=[_common_flags()],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
72
|
+
|
|
73
|
+
# ---- login ------------------------------------------------------
|
|
74
|
+
sub.add_parser(
|
|
75
|
+
"login",
|
|
76
|
+
parents=[common],
|
|
77
|
+
help="Authenticate and cache your API token.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# ---- logout -----------------------------------------------------
|
|
81
|
+
sub.add_parser(
|
|
82
|
+
"logout",
|
|
83
|
+
parents=[common],
|
|
84
|
+
help="Remove the cached API token.",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# ---- load -------------------------------------------------------
|
|
88
|
+
p_load = sub.add_parser("load", parents=[common], help="Load records from BrainSTEM.")
|
|
89
|
+
p_load.add_argument("model", choices=sorted(_MODEL_TO_APP), help="Model name.")
|
|
90
|
+
p_load.add_argument("--portal", default="private", help="'private' or 'public'.")
|
|
91
|
+
p_load.add_argument("--id", help="UUID of a specific record.")
|
|
92
|
+
p_load.add_argument(
|
|
93
|
+
"--filters",
|
|
94
|
+
nargs="+",
|
|
95
|
+
metavar="FIELD=VALUE",
|
|
96
|
+
help="Filter expressions, e.g. name.icontains=rat",
|
|
97
|
+
)
|
|
98
|
+
p_load.add_argument(
|
|
99
|
+
"--sort",
|
|
100
|
+
nargs="+",
|
|
101
|
+
metavar="FIELD",
|
|
102
|
+
help="Sort fields. Prefix with '-' for descending.",
|
|
103
|
+
)
|
|
104
|
+
p_load.add_argument(
|
|
105
|
+
"--include",
|
|
106
|
+
nargs="+",
|
|
107
|
+
metavar="RELATION",
|
|
108
|
+
help="Related models to embed.",
|
|
109
|
+
)
|
|
110
|
+
p_load.add_argument("--limit", type=int,
|
|
111
|
+
help="Records per page (sent as per_page; default 100).")
|
|
112
|
+
p_load.add_argument("--offset", type=int,
|
|
113
|
+
help="Records to skip; must be a multiple of the page size.")
|
|
114
|
+
p_load.add_argument("--per-page", type=int, help="Records per page (default 100).")
|
|
115
|
+
p_load.add_argument("--page", type=int, help="Page number, starting at 1.")
|
|
116
|
+
p_load.add_argument(
|
|
117
|
+
"--all",
|
|
118
|
+
action="store_true",
|
|
119
|
+
help="Follow pagination and print all records merged into one JSON object.",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# ---- save -------------------------------------------------------
|
|
123
|
+
p_save = sub.add_parser("save", parents=[common], help="Create or update a record.")
|
|
124
|
+
p_save.add_argument("model", choices=sorted(_MODEL_TO_APP), help="Model name.")
|
|
125
|
+
p_save.add_argument("--portal", default="private")
|
|
126
|
+
p_save.add_argument("--id", help="UUID of the record to update (omit to create).")
|
|
127
|
+
p_save.add_argument(
|
|
128
|
+
"--data",
|
|
129
|
+
required=True,
|
|
130
|
+
help="JSON string of fields to submit, e.g. '{\"name\":\"x\"}'.",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# ---- delete -----------------------------------------------------
|
|
134
|
+
p_delete = sub.add_parser("delete", parents=[common], help="Delete a record by ID.")
|
|
135
|
+
p_delete.add_argument("model", choices=sorted(_MODEL_TO_APP), help="Model name.")
|
|
136
|
+
p_delete.add_argument("--portal", default="private")
|
|
137
|
+
p_delete.add_argument("--id", required=True, help="UUID of the record to delete.")
|
|
138
|
+
|
|
139
|
+
# ---- reference --------------------------------------------------
|
|
140
|
+
p_reference = sub.add_parser(
|
|
141
|
+
"reference", parents=[common],
|
|
142
|
+
help="Print a page of the server's agent reference (API and data model description).",
|
|
143
|
+
)
|
|
144
|
+
p_reference.add_argument(
|
|
145
|
+
"page", nargs="?", default="index.md",
|
|
146
|
+
help="Page path, e.g. rules.md, resources/session.md, guides/query-planning.md (default: index.md).",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return parser
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main():
|
|
153
|
+
parser = _build_parser()
|
|
154
|
+
args = parser.parse_args()
|
|
155
|
+
|
|
156
|
+
if args.command == "logout":
|
|
157
|
+
if _TOKEN_FILE.exists():
|
|
158
|
+
_TOKEN_FILE.unlink()
|
|
159
|
+
print(f"Logged out. Token removed from {_TOKEN_FILE}")
|
|
160
|
+
else:
|
|
161
|
+
print("No cached token found.")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
if args.command == "reference":
|
|
165
|
+
# The reference is public: never start a login for it.
|
|
166
|
+
client = BrainstemClient(anonymous=True, url=getattr(args, "url", None))
|
|
167
|
+
try:
|
|
168
|
+
print(client.agent_reference(args.page))
|
|
169
|
+
except (ValueError, requests.HTTPError) as exc:
|
|
170
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
client = BrainstemClient(
|
|
175
|
+
token=args.token,
|
|
176
|
+
headless=getattr(args, "headless", False),
|
|
177
|
+
url=getattr(args, "url", None),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if args.command == "login":
|
|
181
|
+
if args.token:
|
|
182
|
+
print("Logged in with the token from --token / $BRAINSTEM_API_TOKEN "
|
|
183
|
+
"(not cached).")
|
|
184
|
+
else:
|
|
185
|
+
print(f"Logged in. Token cached at {_TOKEN_FILE}")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if args.command == "load":
|
|
189
|
+
filters = {}
|
|
190
|
+
for expr in (args.filters or []):
|
|
191
|
+
if "=" not in expr:
|
|
192
|
+
parser.error(f"Invalid filter '{expr}': expected FIELD=VALUE")
|
|
193
|
+
k, v = expr.split("=", 1)
|
|
194
|
+
filters[k] = v
|
|
195
|
+
|
|
196
|
+
limit, offset = args.limit, args.offset
|
|
197
|
+
if args.per_page is not None or args.page is not None:
|
|
198
|
+
if limit is not None or offset is not None:
|
|
199
|
+
parser.error("Use either --per-page/--page or --limit/--offset, not both.")
|
|
200
|
+
if (args.per_page is not None and args.per_page < 1) or \
|
|
201
|
+
(args.page is not None and args.page < 1):
|
|
202
|
+
parser.error("--per-page and --page must be at least 1.")
|
|
203
|
+
limit = args.per_page
|
|
204
|
+
if args.page is not None:
|
|
205
|
+
offset = (args.page - 1) * (args.per_page or 100)
|
|
206
|
+
|
|
207
|
+
load_kwargs = dict(
|
|
208
|
+
portal=args.portal,
|
|
209
|
+
id=args.id,
|
|
210
|
+
filters=filters or None,
|
|
211
|
+
sort=args.sort,
|
|
212
|
+
include=args.include,
|
|
213
|
+
limit=limit,
|
|
214
|
+
offset=offset,
|
|
215
|
+
)
|
|
216
|
+
if args.all:
|
|
217
|
+
try:
|
|
218
|
+
result = client.load(args.model, load_all=True, **load_kwargs)
|
|
219
|
+
except (ValueError, AuthenticationError, requests.HTTPError) as exc:
|
|
220
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
print(json.dumps(result, indent=2))
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
resp = client.load(args.model, **load_kwargs)
|
|
227
|
+
except ValueError as exc:
|
|
228
|
+
parser.error(str(exc))
|
|
229
|
+
|
|
230
|
+
elif args.command == "save":
|
|
231
|
+
try:
|
|
232
|
+
data = json.loads(args.data)
|
|
233
|
+
except json.JSONDecodeError as exc:
|
|
234
|
+
parser.error(f"Invalid JSON for --data: {exc}")
|
|
235
|
+
|
|
236
|
+
resp = client.save(args.model, portal=args.portal, id=args.id, data=data)
|
|
237
|
+
|
|
238
|
+
elif args.command == "delete":
|
|
239
|
+
resp = client.delete(args.model, portal=args.portal, id=args.id)
|
|
240
|
+
|
|
241
|
+
# Output
|
|
242
|
+
if resp.status_code == 204:
|
|
243
|
+
print("Deleted successfully.")
|
|
244
|
+
else:
|
|
245
|
+
try:
|
|
246
|
+
print(json.dumps(resp.json(), indent=2))
|
|
247
|
+
except Exception:
|
|
248
|
+
print(resp.text)
|
|
249
|
+
|
|
250
|
+
if not resp.ok:
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
main()
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brainstem_python_api_tools
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: A Python toolset for interacting with the BrainSTEM API.
|
|
5
|
+
Author-email: BrainSTEM Team <petersen.peter@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/brainstem-org/brainstem_python_api_tools
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: requests>=2.28
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# BrainSTEM Python API Tools
|
|
18
|
+
|
|
19
|
+
The `brainstem_python_api_tools` is a Python toolset for interacting with the [BrainSTEM](https://www.brainstem.org/) API, designed for researchers and developers working with neuroscience data.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
You can install the package using `pip`:
|
|
23
|
+
```bash
|
|
24
|
+
pip install brainstem_python_api_tools
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Authentication
|
|
28
|
+
|
|
29
|
+
Authentication uses a **browser-based device authorization flow** that supports two-factor authentication (2FA). No credentials are entered into the tool itself.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from brainstem_api_tools import BrainstemClient
|
|
33
|
+
|
|
34
|
+
# First run: opens a browser window for secure login.
|
|
35
|
+
# The token is cached at ~/.config/brainstem/token and reused automatically.
|
|
36
|
+
client = BrainstemClient()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
To skip the browser flow, pass a token directly:
|
|
40
|
+
```python
|
|
41
|
+
client = BrainstemClient(token="YOUR_TOKEN")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For headless environments (no browser available):
|
|
45
|
+
```python
|
|
46
|
+
client = BrainstemClient(headless=True) # prints a URL + code to enter manually
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To read public data without logging in (no token is sent; only `portal='public'` works):
|
|
50
|
+
```python
|
|
51
|
+
client = BrainstemClient(anonymous=True)
|
|
52
|
+
client.load("project", portal="public", load_all=True)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
To connect to a different server (e.g. a local development instance):
|
|
56
|
+
```python
|
|
57
|
+
client = BrainstemClient(url="http://127.0.0.1:8000/")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The client and the CLI also read two environment variables when no `token` / `url` is given:
|
|
61
|
+
|
|
62
|
+
- `BRAINSTEM_API_TOKEN`: API token, used before the cached token.
|
|
63
|
+
- `BRAINSTEM_API_URL`: server base URL (default `https://www.brainstem.org/`).
|
|
64
|
+
|
|
65
|
+
> **Security note:** The cached token file is stored with owner-read-only permissions (`0600`). Treat it like a password and do not commit it to version control.
|
|
66
|
+
|
|
67
|
+
## Getting Started
|
|
68
|
+
To get started with the BrainSTEM API tools, please refer to the tutorial script provided:
|
|
69
|
+
|
|
70
|
+
- **Tutorial:** `brainstem_api_tutorial.py`
|
|
71
|
+
|
|
72
|
+
The tutorial demonstrates how to:
|
|
73
|
+
|
|
74
|
+
- **Authenticate:** Load the client and authenticate via your browser.
|
|
75
|
+
- **Load Data:** Load sessions and filter data using flexible options.
|
|
76
|
+
- **Paginate:** Retrieve large datasets page by page (`limit` / `offset`) or all at once (`load_all=True`).
|
|
77
|
+
- **Convenience Loaders:** Use `load_session()`, `load_subject()`, etc. for common queries.
|
|
78
|
+
- **Update Entries:** Modify existing records and update them in the database.
|
|
79
|
+
- **Create Entries:** Submit new data entries with required fields.
|
|
80
|
+
- **Delete Entries:** Remove records by ID.
|
|
81
|
+
- **Load Public Data:** Access public projects and data using the public portal.
|
|
82
|
+
|
|
83
|
+
## Example Usage
|
|
84
|
+
```python
|
|
85
|
+
from brainstem_api_tools import BrainstemClient
|
|
86
|
+
|
|
87
|
+
client = BrainstemClient()
|
|
88
|
+
|
|
89
|
+
# Load sessions sorted by name
|
|
90
|
+
response = client.load('session', sort=['name'])
|
|
91
|
+
print(response.json())
|
|
92
|
+
|
|
93
|
+
# Filter and include related data
|
|
94
|
+
response = client.load(
|
|
95
|
+
'session',
|
|
96
|
+
filters={'name.icontains': 'Rat'},
|
|
97
|
+
sort=['-name'],
|
|
98
|
+
include=['projects'],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Paginate results (manual). The API pages with page/per_page: limit is sent
|
|
102
|
+
# as per_page (1 to 500, default 100) and offset is converted to a page number,
|
|
103
|
+
# so offset must be a multiple of limit. Here: page 2 of 20 records.
|
|
104
|
+
page2 = client.load('session', limit=20, offset=20).json()
|
|
105
|
+
print(page2['meta']) # {'page': 2, 'per_page': 20, 'total_results': ..., 'total_pages': ...}
|
|
106
|
+
|
|
107
|
+
# Auto-paginate — fetches every page and returns a merged dict. Records of
|
|
108
|
+
# sideloaded relations (include=[...]) are merged per key as well.
|
|
109
|
+
all_sessions = client.load('session', load_all=True)
|
|
110
|
+
|
|
111
|
+
# Convenience loaders — sensible defaults with named filter kwargs. Their
|
|
112
|
+
# filters send the bare field (filter{projects}=<uuid>), which every server accepts.
|
|
113
|
+
# load_session sideloads dataacquisition, behaviors, manipulations, epochs
|
|
114
|
+
sessions = client.load_session(name='Rat', load_all=True)
|
|
115
|
+
|
|
116
|
+
# load_subject sideloads procedures and subjectlogs
|
|
117
|
+
subjects = client.load_subject(sex='M', projects='<project-uuid>', load_all=True)
|
|
118
|
+
|
|
119
|
+
# load_project sideloads sessions, subjects, collections, cohorts
|
|
120
|
+
projects = client.load_project(name='MyProject')
|
|
121
|
+
|
|
122
|
+
# load_behavior / load_dataacquisition / load_manipulation scope by session UUID
|
|
123
|
+
behaviors = client.load_behavior(session='<session-uuid>', load_all=True)
|
|
124
|
+
|
|
125
|
+
# load_procedure / load_subjectlog scope by subject UUID; load_procedurelog by
|
|
126
|
+
# procedure, load_equipment by setup and load_strain by species UUID.
|
|
127
|
+
procedures = client.load_procedure(subject='<subject-uuid>', load_all=True)
|
|
128
|
+
|
|
129
|
+
# Create a record
|
|
130
|
+
client.save('session', data={'name': 'New session', 'projects': ['<project-uuid>']})
|
|
131
|
+
|
|
132
|
+
# Update a record
|
|
133
|
+
client.save('session', id='<session-uuid>', data={'description': 'updated'})
|
|
134
|
+
|
|
135
|
+
# Sending a list field replaces the whole list. Lists read with include (and
|
|
136
|
+
# the convenience loaders' default includes) hold only records you may read,
|
|
137
|
+
# so read the record without include before changing one:
|
|
138
|
+
session = client.load('session', id='<session-uuid>').json()['session']
|
|
139
|
+
client.save('session', id='<session-uuid>',
|
|
140
|
+
data={'projects': session['projects'] + ['<project-uuid>']})
|
|
141
|
+
|
|
142
|
+
# Delete a record
|
|
143
|
+
client.delete('session', id='<session-uuid>')
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Specimens, interfaces and materials
|
|
147
|
+
|
|
148
|
+
Specimen sets and specimens (`stem`), experimental interfaces, material usages and specimen procedures/logs (`modules`), and anatomical regions (`taxonomies`) load like any other model:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
specimen_sets = client.load('specimenset', filters={'projects': '<project-uuid>'}, load_all=True)
|
|
152
|
+
interfaces = client.load('experimentalinterface', load_all=True)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Species, strains and brain regions
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# Search species and strains by common name as well as scientific name
|
|
159
|
+
mice = client.load_species(popular_name='mouse').json()
|
|
160
|
+
strains = client.load_strain(popular_name='black 6').json()
|
|
161
|
+
|
|
162
|
+
# Brain regions are anatomical regions; find one by acronym, then list its subregions
|
|
163
|
+
ca1 = client.load_anatomicalregion(acronym='CA1').json()['anatomical_regions'][0]
|
|
164
|
+
subregions = client.load_anatomicalregion_descendants(ca1['id']).json()['anatomical_regions']
|
|
165
|
+
for region in subregions:
|
|
166
|
+
print(region['depth'], region['acronym'], region['name'])
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`brainregion` and `load_brainregion()` are deprecated names for `anatomicalregion` and `load_anatomicalregion()`. They still work, with a `FutureWarning`, and load anatomical regions.
|
|
170
|
+
|
|
171
|
+
### Known limitations
|
|
172
|
+
|
|
173
|
+
- Specimen-era models (specimen sets, specimens, experimental interfaces, material usages, anatomical regions, ...), `popular_name` and region descendants need a server with the specimen release. Older servers answer 404 for those models and 400 for a `popular_name` filter.
|
|
174
|
+
- `offset` must be a multiple of the page size (`limit`, default 100), and a page holds at most 500 records. Use `load_all=True` to get everything.
|
|
175
|
+
- Lists read with `include` show only the related records you may read; see the note on `save` above.
|
|
176
|
+
|
|
177
|
+
## Exporting projects and sessions
|
|
178
|
+
|
|
179
|
+
`export_project()` returns a whole project as one JSON-serializable dict. It is the same file as the JSON download on the BrainSTEM website (**Project → Download → JSON**). Use it for backups, NWB conversion, external integrations or analysis pipelines. You need read access to the project.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
import json
|
|
183
|
+
|
|
184
|
+
export = client.export_project('<project-uuid>')
|
|
185
|
+
|
|
186
|
+
print(export['export_info']['format_version'], export['export_info']['counts'])
|
|
187
|
+
for record in export['records']['stem.session']:
|
|
188
|
+
print(record['pk'], record['fields']['name'])
|
|
189
|
+
|
|
190
|
+
with open('project_backup.json', 'w') as f:
|
|
191
|
+
json.dump(export, f)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Session exports
|
|
195
|
+
|
|
196
|
+
`export_session()` exports one session and everything it touches: the content of one NWB file. You need read access to one of the session's projects.
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
export = client.export_session('<session-uuid>')
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The file has the same format as a project export, with `export_info['scope'] == 'session'`. It contains:
|
|
203
|
+
|
|
204
|
+
- the session with its data acquisitions, manipulations, behaviors and epochs;
|
|
205
|
+
- the subjects and specimen sets linked to the session, with their full procedure and log history;
|
|
206
|
+
- the session's projects you can read (`export_info['project_ids']`; a session can belong to several);
|
|
207
|
+
- the lab resources, catalog entries and users these records reference.
|
|
208
|
+
|
|
209
|
+
A subject's procedure history is complete, including procedures after the session, so filter by date where that matters.
|
|
210
|
+
|
|
211
|
+
### Large exports
|
|
212
|
+
|
|
213
|
+
Large exports can take longer than the server's 30-second request limit. For those, pass `background=True`: the server builds the export as a job, the client waits for it and downloads the result, and you also get an email when it is ready.
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
export = client.export_project('<project-uuid>', background=True)
|
|
217
|
+
export = client.export_session('<session-uuid>', background=True)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Format
|
|
221
|
+
|
|
222
|
+
Both exports have this structure:
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
{
|
|
226
|
+
"export_info": {
|
|
227
|
+
"format_version": "2.1",
|
|
228
|
+
"exported_at": "<ISO 8601 UTC timestamp>",
|
|
229
|
+
"scope": "project", # or "session"
|
|
230
|
+
"project_id": "<project uuid>", # null for a session export
|
|
231
|
+
"project_ids": ["<project uuid>", ...],
|
|
232
|
+
"session_id": null, # the session's uuid for a session export
|
|
233
|
+
"lab_resources": "<which lab resources are included>",
|
|
234
|
+
"restricted": {"personal_attributes.datastorage": [{"pk": "<uuid>", "name": "Lab server"}], ...},
|
|
235
|
+
"record_count": 2344,
|
|
236
|
+
"counts": {"stem.project": 1, "stem.subject": 21, ...}
|
|
237
|
+
},
|
|
238
|
+
"records": {
|
|
239
|
+
"stem.project": [{"model": "stem.project", "pk": "<uuid>", "fields": {...}}],
|
|
240
|
+
"stem.subject": [...],
|
|
241
|
+
"stem.session": [...],
|
|
242
|
+
...
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
- `records` is keyed by model label (`app_label.model_name`). Every label is always present, with an empty list when there are no such records.
|
|
248
|
+
- Links to other records are primary keys. Many-to-many links are sorted lists of primary keys. Referenced catalog entries (species, brain regions, suppliers, ...) and users (names only) are included in the file.
|
|
249
|
+
- Lab resources (setups, data storages, equipment, protocols, ...) are included when you can read them and the exported records use them. Two people exporting the same project can therefore get different lab resources. Referenced lab resources you cannot read are listed by name in `export_info['restricted']`.
|
|
250
|
+
- A minor format version (2.1, 2.2, ...) only adds labels and keys, so ignore any you don't recognise.
|
|
251
|
+
|
|
252
|
+
Problems raise `ExportError` with a readable message, for example when the project or session doesn't exist or you can't read it, a background job fails, or the server doesn't support exports yet.
|
|
253
|
+
|
|
254
|
+
## Agent reference
|
|
255
|
+
|
|
256
|
+
The server describes its own API and data model (resources, fields, relationships, JSON schemas, guides) at
|
|
257
|
+
`/reference/`. It is written for AI agents, public, and needs no token:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
print(client.agent_reference()) # index of all pages
|
|
261
|
+
print(client.agent_reference("resources/session.md")) # one resource
|
|
262
|
+
print(client.agent_reference("guides/query-planning.md")) # a guide
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
brainstem reference rules.md
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Command-line Interface
|
|
270
|
+
|
|
271
|
+
After installation a `brainstem` command is available in your shell.
|
|
272
|
+
|
|
273
|
+
### Authentication
|
|
274
|
+
```bash
|
|
275
|
+
# Authenticate (opens browser) and cache token
|
|
276
|
+
brainstem login
|
|
277
|
+
|
|
278
|
+
# Headless — prints URL + code instead of opening browser
|
|
279
|
+
brainstem login --headless
|
|
280
|
+
|
|
281
|
+
# Connect to a local dev server
|
|
282
|
+
brainstem login --url http://127.0.0.1:8000/
|
|
283
|
+
|
|
284
|
+
# Remove cached token
|
|
285
|
+
brainstem logout
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Loading data
|
|
289
|
+
```bash
|
|
290
|
+
# Load all sessions (private portal)
|
|
291
|
+
brainstem load session
|
|
292
|
+
|
|
293
|
+
# Filter, sort and embed related data
|
|
294
|
+
brainstem load session --filters name.icontains=Rat --sort -name --include projects
|
|
295
|
+
|
|
296
|
+
# Load a single record by UUID
|
|
297
|
+
brainstem load session --id <uuid>
|
|
298
|
+
|
|
299
|
+
# Manual pagination (page 2, 20 records per page)
|
|
300
|
+
brainstem load session --per-page 20 --page 2
|
|
301
|
+
brainstem load session --limit 20 --offset 20 # equivalent
|
|
302
|
+
|
|
303
|
+
# Fetch every page and print one merged JSON object
|
|
304
|
+
brainstem load session --all
|
|
305
|
+
|
|
306
|
+
# Public portal
|
|
307
|
+
brainstem load project --portal public
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Creating and updating records
|
|
311
|
+
```bash
|
|
312
|
+
# Create a new session
|
|
313
|
+
brainstem save session --data '{"name":"New session","projects":["<uuid>"]}'
|
|
314
|
+
|
|
315
|
+
# Update an existing record
|
|
316
|
+
brainstem save session --id <uuid> --data '{"description":"updated"}'
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Deleting records
|
|
320
|
+
```bash
|
|
321
|
+
brainstem delete session --id <uuid>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
All subcommands accept `--token`, `--headless`, and `--url` to override defaults (`--token` and `--url` default to `$BRAINSTEM_API_TOKEN` and `$BRAINSTEM_API_URL`). `brainstem login` never prints the token; it reports where it is cached.
|
|
325
|
+
|
|
326
|
+
## Contributing
|
|
327
|
+
Contributions are welcome! Feel free to open issues or submit pull requests on GitHub.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
This project is licensed under the MIT License. See the `LICENSE` file for more details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
brainstem_api_tools/__init__.py,sha256=irr6k2YpKq4f01ev4pFZELoEgpekmjle2sSjPWM4t5s,130
|
|
2
|
+
brainstem_api_tools/brainstem_api_client.py,sha256=OxV0DvfPMbNpf9-s6djlnAw60TaAvbmFYTtFGtzuZDc,52359
|
|
3
|
+
brainstem_api_tools/cli.py,sha256=2-6mM7j508hlBDZU0pDqekVP5beTbc31_tWD_XTtG9M,8886
|
|
4
|
+
brainstem_python_api_tools-2.1.0.dist-info/licenses/LICENSE,sha256=I4iwel7RmYcQggAinCAw6Gxcup59frUNXxFKf9hXCoc,1092
|
|
5
|
+
brainstem_python_api_tools-2.1.0.dist-info/METADATA,sha256=WquaSvbN2AfbW8WziiYOdcX9EiktDMsN9K2Bct3WCOE,13232
|
|
6
|
+
brainstem_python_api_tools-2.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
brainstem_python_api_tools-2.1.0.dist-info/entry_points.txt,sha256=o78cX6aY5foCZ6xskNVWc_Stjxi3dfYn_8l_kszjfZ4,59
|
|
8
|
+
brainstem_python_api_tools-2.1.0.dist-info/top_level.txt,sha256=66OUGJs8UdzQdEqtx32kt84z2x-IpYEHmI1lLGlEwPA,20
|
|
9
|
+
brainstem_python_api_tools-2.1.0.dist-info/RECORD,,
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: brainstem_python_api_tools
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Summary: A Python toolset for interacting with API of BrainSTEM.
|
|
5
|
-
Home-page: https://github.com/brainstem-org/brainstem_python_api_tools
|
|
6
|
-
Author: BrainSTEM Team
|
|
7
|
-
Author-email: petersen.peter@gmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.7
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
|
-
Requires-Dist: requests
|
|
16
|
-
Requires-Dist: jupyter
|
|
17
|
-
|
|
18
|
-
# BrainSTEM Python API Tools
|
|
19
|
-
|
|
20
|
-
The `brainstem_python_api_tools` is a Python toolset for interacting with the [BrainSTEM](https://www.brainstem.org/) API, designed for researchers and developers working with neuroscience data.
|
|
21
|
-
|
|
22
|
-
## Installation
|
|
23
|
-
You can install the package using `pip`:
|
|
24
|
-
```bash
|
|
25
|
-
pip install brainstem_python_api_tools
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Getting Started
|
|
29
|
-
To get started with the BrainSTEM API tools, please refer to the tutorial script provided:
|
|
30
|
-
|
|
31
|
-
- **Tutorial:** `brainstem_api_tutorial.py`
|
|
32
|
-
|
|
33
|
-
The tutorial demonstrates how to:
|
|
34
|
-
|
|
35
|
-
- **Authenticate:** Load the client and authenticate using your credentials.
|
|
36
|
-
- **Loading Data:** Load sessions and filter data using flexible options.
|
|
37
|
-
- **Updating Entries:** Modify existing models and update them in the database.
|
|
38
|
-
- **Creating Entries:** Submit new data entries with required fields.
|
|
39
|
-
- **Loading Public Data:** Access public projects and data using the public portal.
|
|
40
|
-
|
|
41
|
-
## Example Usage
|
|
42
|
-
```python
|
|
43
|
-
from brainstem_api_tools import BrainstemClient
|
|
44
|
-
|
|
45
|
-
client = BrainstemClient()
|
|
46
|
-
response = client.load_model('session', sort=['name'])
|
|
47
|
-
print(response.json())
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Contributing
|
|
51
|
-
Contributions are welcome! Feel free to open issues or submit pull requests on [GitHub](https://github.com/brainstem-org/brainstem_python_api_tools).
|
|
52
|
-
|
|
53
|
-
## License
|
|
54
|
-
This project is licensed under the MIT License. See the `LICENSE` file for more details.
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
brainstem_api_tools/__init__.py,sha256=mADe7kLRh26hHSsFeqH60SVsJgzDb6PpoSRsmx0bKyU,73
|
|
2
|
-
brainstem_api_tools/brainstem_api_client.py,sha256=TCruvRkMG4RpioxA-IrZZAOAnj8IXvzYK0Z7qEr_8ns,7787
|
|
3
|
-
brainstem_python_api_tools-0.2.0.dist-info/LICENSE,sha256=I4iwel7RmYcQggAinCAw6Gxcup59frUNXxFKf9hXCoc,1092
|
|
4
|
-
brainstem_python_api_tools-0.2.0.dist-info/METADATA,sha256=NzJQpgIBJq3EG_V0gw9Fl4Q55ffY_gO9Uk3WLp9BYDE,1925
|
|
5
|
-
brainstem_python_api_tools-0.2.0.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
|
6
|
-
brainstem_python_api_tools-0.2.0.dist-info/top_level.txt,sha256=66OUGJs8UdzQdEqtx32kt84z2x-IpYEHmI1lLGlEwPA,20
|
|
7
|
-
brainstem_python_api_tools-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|