ONE-api 3.0b1__py3-none-any.whl → 3.0b4__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.
Files changed (33) hide show
  1. {ONE_api-3.0b1.dist-info → ONE_api-3.0b4.dist-info}/LICENSE +21 -21
  2. {ONE_api-3.0b1.dist-info → ONE_api-3.0b4.dist-info}/METADATA +115 -115
  3. ONE_api-3.0b4.dist-info/RECORD +37 -0
  4. one/__init__.py +2 -2
  5. one/alf/__init__.py +1 -1
  6. one/alf/cache.py +640 -653
  7. one/alf/exceptions.py +105 -105
  8. one/alf/io.py +876 -876
  9. one/alf/path.py +1450 -1450
  10. one/alf/spec.py +519 -504
  11. one/api.py +2949 -2973
  12. one/converters.py +850 -850
  13. one/params.py +414 -414
  14. one/registration.py +845 -845
  15. one/remote/__init__.py +1 -1
  16. one/remote/aws.py +313 -313
  17. one/remote/base.py +142 -142
  18. one/remote/globus.py +1254 -1254
  19. one/tests/fixtures/params/.caches +6 -6
  20. one/tests/fixtures/params/.test.alyx.internationalbrainlab.org +8 -8
  21. one/tests/fixtures/rest_responses/1f187d80fd59677b395fcdb18e68e4401bfa1cc9 +1 -1
  22. one/tests/fixtures/rest_responses/47893cf67c985e6361cdee009334963f49fb0746 +1 -1
  23. one/tests/fixtures/rest_responses/535d0e9a1e2c1efbdeba0d673b131e00361a2edb +1 -1
  24. one/tests/fixtures/rest_responses/6dc96f7e9bcc6ac2e7581489b9580a6cd3f28293 +1 -1
  25. one/tests/fixtures/rest_responses/db1731fb8df0208944ae85f76718430813a8bf50 +1 -1
  26. one/tests/fixtures/rest_responses/dcce48259bb929661f60a02a48563f70aa6185b3 +1 -1
  27. one/tests/fixtures/rest_responses/f530d6022f61cdc9e38cc66beb3cb71f3003c9a1 +1 -1
  28. one/tests/fixtures/test_dbs.json +14 -14
  29. one/util.py +524 -524
  30. one/webclient.py +1366 -1354
  31. ONE_api-3.0b1.dist-info/RECORD +0 -37
  32. {ONE_api-3.0b1.dist-info → ONE_api-3.0b4.dist-info}/WHEEL +0 -0
  33. {ONE_api-3.0b1.dist-info → ONE_api-3.0b4.dist-info}/top_level.txt +0 -0
one/params.py CHANGED
@@ -1,414 +1,414 @@
1
- """Functions for modifying, loading and saving ONE and Alyx database parameters.
2
-
3
- Scenarios:
4
-
5
- - Load ONE with a cache dir: tries to load the Web client params from the dir
6
- - Load ONE with http address - gets cache dir from the URL map
7
-
8
- The ONE params comprise two files: a caches file that contains a map of Alyx db URLs to cache
9
- directories, and a separate parameter file for each url containing the client parameters. The
10
- caches file also sets the default client for when no url is provided.
11
- """
12
- import re
13
- import shutil
14
- import warnings
15
-
16
- from iblutil.io import params as iopar
17
- from getpass import getpass
18
- from pathlib import Path
19
- from urllib.parse import urlsplit
20
- import unicodedata
21
-
22
- _PAR_ID_STR = 'one'
23
- _CLIENT_ID_STR = 'caches'
24
- CACHE_DIR_DEFAULT = str(Path.home() / 'Downloads' / 'ONE')
25
- """str: The default data download location"""
26
-
27
-
28
- def default():
29
- """Default Web client parameters."""
30
- par = {'ALYX_URL': 'https://openalyx.internationalbrainlab.org',
31
- 'ALYX_LOGIN': 'intbrainlab',
32
- 'HTTP_DATA_SERVER': 'https://ibl.flatironinstitute.org/public',
33
- 'HTTP_DATA_SERVER_LOGIN': None,
34
- 'HTTP_DATA_SERVER_PWD': None}
35
- return iopar.from_dict(par)
36
-
37
-
38
- def _get_current_par(k, par_current):
39
- """Return the current parameter value or the default.
40
-
41
- Parameters
42
- ----------
43
- k : str
44
- The parameter key lookup.
45
- par_current : IBLParams
46
- The current parameter set.
47
-
48
- Returns
49
- -------
50
- any
51
- The current parameter value or default if None or not set.
52
-
53
- """
54
- cpar = getattr(par_current, k, None)
55
- if cpar is None:
56
- cpar = getattr(default(), k, None)
57
- return cpar
58
-
59
-
60
- def _key_from_url(url: str) -> str:
61
- """Convert a URL str to one valid for use as a file name or dict key.
62
-
63
- URL Protocols are removed entirely.
64
- The returned string will have characters in the set [a-zA-Z.-_].
65
-
66
- Parameters
67
- ----------
68
- url : str
69
- A URL string.
70
-
71
- Returns
72
- -------
73
- str
74
- A filename-safe string.
75
-
76
- Example
77
- -------
78
- >>> url = _key_from_url('http://test.alyx.internationalbrainlab.org/')
79
- 'test.alyx.internationalbrainlab.org'
80
-
81
- """
82
- url = unicodedata.normalize('NFKC', url) # Ensure ASCII
83
- url = re.sub('^https?://', '', url).strip('/') # Remove protocol and trialing slashes
84
- url = re.sub(r'[^.\w\s-]', '_', url.lower()) # Convert non word chars to underscore
85
- return re.sub(r'[-\s]+', '-', url) # Convert spaces to hyphens
86
-
87
-
88
- def setup(client=None, silent=False, make_default=None, username=None, cache_dir=None):
89
- """Set up ONE parameters.
90
-
91
- If a client (i.e. Alyx database URL) is provided, settings for that instance will be set.
92
- If silent, the user will be prompted to input each parameter value. Pressing return will use
93
- either current parameter or the default.
94
-
95
- Parameters
96
- ----------
97
- client : str
98
- An Alyx database URL. If None, the user will be prompted to input one.
99
- silent : bool
100
- If True, user is not prompted for any input.
101
- make_default : bool
102
- If True, client is set as the default and will be returned when calling `get` with no
103
- arguments.
104
- username : str, optional
105
- The Alyx username to store in the params.
106
- cache_dir : str, pathlib.Path
107
- The default cache directory to store in the params.
108
-
109
- Returns
110
- -------
111
- IBLParams
112
- An updated cache map.
113
-
114
- """
115
- # First get default parameters
116
- par_default = default()
117
- default_url = par_default.ALYX_URL
118
- client_key = _key_from_url(client or default_url)
119
-
120
- # If a client URL has been provided, set it as the default URL
121
- par_default = par_default.set('ALYX_URL', client or default_url)
122
-
123
- # When silent=True, if setting up default database use default parameters
124
- # instead of current ones to reset credentials
125
- if silent and client_key == _key_from_url(default_url):
126
- par_current = par_default
127
- else:
128
- par_current = iopar.read(f'{_PAR_ID_STR}/{client_key}', par_default)
129
- if username:
130
- par_current = par_current.set('ALYX_LOGIN', username)
131
-
132
- # Load the db URL map
133
- cache_map = iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {'CLIENT_MAP': dict()})
134
-
135
- if not silent:
136
- prompt = 'Param %s, current value is ["%s"]:'
137
- par = iopar.as_dict(par_default)
138
- quotes = '"\'`'
139
- # Iterate through non-password pars
140
- for k in filter(lambda k: 'PWD' not in k, par.keys()):
141
- cpar = _get_current_par(k, par_current)
142
- # Prompt for database and FI URL
143
- if 'URL' in k:
144
- if k == 'ALYX_URL' and client:
145
- continue # skip if client url already provided
146
- par[k] = input(prompt % (k, cpar)).strip().rstrip('/') or cpar
147
- if '://' not in par[k]:
148
- par[k] = 'https://' + par[k]
149
- url_parsed = urlsplit(par[k])
150
- if not (url_parsed.netloc and re.match('https?', url_parsed.scheme)):
151
- raise ValueError(f'{k} must be valid HTTP URL')
152
- else:
153
- par[k] = input(prompt % (k, cpar)).strip() or cpar
154
- # Check whether user erroneously entered quotation marks
155
- # Prompting the user here (hopefully) corrects them before they input a password
156
- # where the use of quotation marks may be legitimate
157
- if par[k] and len(par[k]) >= 2 and par[k][0] in quotes and par[k][-1] in quotes:
158
- warnings.warn('Do not use quotation marks with input answers', UserWarning)
159
- ans = input('Strip quotation marks from response? [Y/n]:').strip() or 'y'
160
- if ans.casefold()[0] == 'y':
161
- par[k] = par[k].strip(quotes)
162
- if k == 'ALYX_URL':
163
- client = par[k]
164
-
165
- cpar = _get_current_par('HTTP_DATA_SERVER_PWD', par_current)
166
- prompt = f'Enter the FlatIron HTTP password for {par["HTTP_DATA_SERVER_LOGIN"]} '\
167
- '(leave empty to keep current): '
168
- par['HTTP_DATA_SERVER_PWD'] = getpass(prompt) or cpar
169
-
170
- if 'ALYX_PWD' in par_current.as_dict():
171
- # Only store plain text password if user manually added it to params JSON file
172
- cpar = _get_current_par('ALYX_PWD', par_current)
173
- prompt = (f'Enter the Alyx password for {par["ALYX_LOGIN"]} '
174
- '(leave empty to keep current):')
175
- par['ALYX_PWD'] = getpass(prompt) or cpar
176
-
177
- par = iopar.from_dict(par)
178
-
179
- # Prompt for cache directory (default may have changed after prompt)
180
- client_key = _key_from_url(par.ALYX_URL)
181
- def_cache_dir = cache_map.CLIENT_MAP.get(client_key) or Path(CACHE_DIR_DEFAULT, client_key)
182
- cache_dir = cache_dir or def_cache_dir
183
- prompt = f'Enter the location of the download cache, current value is ["{cache_dir}"]:'
184
- cache_dir = input(prompt) or cache_dir
185
-
186
- # Check if directory already used by another instance
187
- in_use = [v for k, v in cache_map.CLIENT_MAP.items() if k != client_key]
188
- while str(cache_dir) in in_use:
189
- answer = input(
190
- 'Warning: the directory provided is already a cache for another URL. '
191
- 'This may cause conflicts. Would you like to change the cache location? [Y/n]')
192
- if answer and answer[0].casefold() == 'n':
193
- break
194
- cache_dir = input(prompt) or cache_dir # Prompt for another directory
195
-
196
- if make_default is None:
197
- answer = input('Would you like to set this URL as the default one? [Y/n]')
198
- make_default = (answer or 'y')[0].casefold() == 'y'
199
-
200
- # Verify setup pars
201
- answer = input('Are the above settings correct? [Y/n]')
202
- if answer and answer.casefold()[0] == 'n':
203
- print('SETUP ABANDONED. Please re-run.')
204
- return par_current
205
- else:
206
- # Precedence: user provided cache_dir; previously defined; the default location
207
- default_cache_dir = Path(CACHE_DIR_DEFAULT, client_key)
208
- cache_dir = cache_dir or cache_map.CLIENT_MAP.get(client_key, default_cache_dir)
209
- # Use current params but drop any extras (such as the TOKEN or ALYX_PWD field)
210
- keep_keys = par_default.as_dict().keys()
211
- par = iopar.from_dict({k: v for k, v in par_current.as_dict().items() if k in keep_keys})
212
- if any(v for k, v in cache_map.CLIENT_MAP.items() if k != client_key and v == cache_dir):
213
- warnings.warn('Warning: the directory provided is already a cache for another URL.')
214
-
215
- # Update and save parameters
216
- Path(cache_dir).mkdir(exist_ok=True, parents=True)
217
- cache_map.CLIENT_MAP[client_key] = str(cache_dir)
218
- if make_default or 'DEFAULT' not in cache_map.as_dict():
219
- cache_map = cache_map.set('DEFAULT', client_key)
220
-
221
- iopar.write(f'{_PAR_ID_STR}/{client_key}', par) # Client params
222
- iopar.write(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', cache_map)
223
-
224
- if not silent:
225
- print('ONE Parameter files location: ' + iopar.getfile(_PAR_ID_STR))
226
- return cache_map
227
-
228
-
229
- def get(client=None, silent=False, username=None):
230
- """Returns the AlyxClient parameters.
231
-
232
- Parameters
233
- ----------
234
- silent : bool
235
- If true, defaults are chosen if no parameters found.
236
- client : str
237
- The database URL to retrieve parameters for. If None, the default is loaded.
238
- username : str
239
- The username to use. If None, the default is loaded.
240
-
241
- Returns
242
- -------
243
- IBLParams
244
- A Params object for the AlyxClient.
245
-
246
- """
247
- client = client or get_default_client(include_schema=True)
248
- client_key = _key_from_url(client) if client else None
249
- cache_map = iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {})
250
- # If there are no params for this client, run setup routine
251
- if not cache_map or (client_key and client_key not in cache_map.CLIENT_MAP):
252
- cache_map = setup(client=client, silent=silent, username=username)
253
- cache = cache_map.CLIENT_MAP[client_key or cache_map.DEFAULT]
254
- pars = iopar.read(f'{_PAR_ID_STR}/{client_key or cache_map.DEFAULT}').set('CACHE_DIR', cache)
255
- if username:
256
- pars = pars.set('ALYX_LOGIN', username)
257
- return _patch_params(pars)
258
-
259
-
260
- def get_default_client(include_schema=True) -> str:
261
- """Returns the default AlyxClient URL, or None if no default is set.
262
-
263
- Parameters
264
- ----------
265
- include_schema : bool
266
- When True, the URL schema is included (i.e. http(s)://). Set to False to return the URL
267
- as a client key.
268
-
269
- Returns
270
- -------
271
- str
272
- The default database URL with or without the schema, or None if no default is set
273
-
274
- """
275
- cache_map = iopar.as_dict(iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {})) or {}
276
- client_key = cache_map.get('DEFAULT', None)
277
- if not client_key or include_schema is False:
278
- return client_key
279
- return get(client_key).ALYX_URL
280
-
281
-
282
- def save(par, client):
283
- """Save a set of parameters for a given client.
284
-
285
- Parameters
286
- ----------
287
- par : dict, IBLParams
288
- A set of Web client parameters to save
289
- client : str
290
- The Alyx URL that corresponds to these parameters
291
-
292
- """
293
- # Remove cache dir variable before saving
294
- par = {k: v for k, v in iopar.as_dict(par).items() if 'CACHE_DIR' not in k}
295
- iopar.write(f'{_PAR_ID_STR}/{_key_from_url(client)}', par)
296
-
297
-
298
- def get_cache_dir(client=None) -> Path:
299
- """Return the download directory for a given client.
300
-
301
- If no client is set up, the default download location is returned.
302
-
303
- Parameters
304
- ----------
305
- client : str
306
- The client to return cache dir from. If None, the default client is used.
307
-
308
- Returns
309
- -------
310
- pathlib.Path
311
- The download cache path
312
-
313
- """
314
- cache_map = iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {})
315
- client = _key_from_url(client) if client else getattr(cache_map, 'DEFAULT', None)
316
- cache_dir = Path(cache_map.CLIENT_MAP[client] if cache_map else CACHE_DIR_DEFAULT)
317
- cache_dir.mkdir(exist_ok=True, parents=True)
318
- return cache_dir
319
-
320
-
321
- def get_params_dir() -> Path:
322
- """Return the path to the root ONE parameters directory.
323
-
324
- Returns
325
- -------
326
- pathlib.Path
327
- The root ONE parameters directory
328
-
329
- """
330
- return Path(iopar.getfile(_PAR_ID_STR))
331
-
332
-
333
- def check_cache_conflict(cache_dir):
334
- """Assert that a given directory is not currently used as a cache directory.
335
-
336
- This function checks whether a given directory is used as a cache directory for an Alyx
337
- Web client. This function is called by the ONE factory to determine whether to return an
338
- OneAlyx object or not. It is also used when setting up params for a new client.
339
-
340
- Parameters
341
- ----------
342
- cache_dir : str, pathlib.Path
343
- A directory to check.
344
-
345
- Raises
346
- ------
347
- AssertionError
348
- The directory is set as a cache for a Web client
349
-
350
- """
351
- cache_map = getattr(iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {}), 'CLIENT_MAP', None)
352
- if cache_map:
353
- assert not any(x == str(cache_dir) for x in cache_map.values())
354
-
355
-
356
- def delete_params(base_url=None):
357
- """Delete parameter files.
358
-
359
- This will fully reset the ONE database and remote client parameters.
360
-
361
- Parameters
362
- ----------
363
- base_url : str, optional
364
- If provided, delete specific database parameters. If None, all parameters are removed.
365
-
366
- """
367
- if base_url:
368
- client_key = _key_from_url(base_url)
369
- params_file = Path(iopar.getfile(f'{_PAR_ID_STR}/{client_key}'))
370
- if params_file.exists():
371
- params_file.unlink()
372
- else:
373
- warnings.warn(f'{base_url}: params file not found')
374
- else:
375
- if (params_dir := get_params_dir()).exists():
376
- shutil.rmtree(params_dir)
377
-
378
-
379
- def _patch_params(par):
380
- """Patch previous version of parameters, if required.
381
-
382
- Parameters
383
- ----------
384
- par : IBLParams
385
- The old parameters object.
386
-
387
- Returns
388
- -------
389
- IBLParams
390
- New parameters object containing the previous parameters.
391
-
392
- """
393
- # Patch the URL of data server, if database is OpenAlyx.
394
- # The data location is in /public, however this path is no longer in the cache table
395
- if 'openalyx' in par.ALYX_URL and 'public' not in par.HTTP_DATA_SERVER:
396
- par = par.set('HTTP_DATA_SERVER', default().HTTP_DATA_SERVER)
397
- save(par, par.ALYX_URL)
398
-
399
- # Move old REST data
400
- rest_dir = get_params_dir() / '.rest'
401
- scheme, loc, *_ = urlsplit(par.ALYX_URL)
402
- rest_dir /= Path(loc.replace(':', '_'), scheme)
403
- new_rest_dir = Path(par.CACHE_DIR, '.rest')
404
-
405
- if rest_dir.exists() and any(x for x in rest_dir.glob('*') if x.is_file()):
406
- if not new_rest_dir.exists():
407
- shutil.move(str(rest_dir), str(new_rest_dir))
408
- from iblutil.io.params import set_hidden
409
- set_hidden(new_rest_dir, True)
410
- shutil.rmtree(rest_dir.parent)
411
- if not any(get_params_dir().joinpath('.rest').glob('*')):
412
- get_params_dir().joinpath('.rest').rmdir()
413
-
414
- return par
1
+ """Functions for modifying, loading and saving ONE and Alyx database parameters.
2
+
3
+ Scenarios:
4
+
5
+ - Load ONE with a cache dir: tries to load the Web client params from the dir
6
+ - Load ONE with http address - gets cache dir from the URL map
7
+
8
+ The ONE params comprise two files: a caches file that contains a map of Alyx db URLs to cache
9
+ directories, and a separate parameter file for each url containing the client parameters. The
10
+ caches file also sets the default client for when no url is provided.
11
+ """
12
+ import re
13
+ import shutil
14
+ import warnings
15
+
16
+ from iblutil.io import params as iopar
17
+ from getpass import getpass
18
+ from pathlib import Path
19
+ from urllib.parse import urlsplit
20
+ import unicodedata
21
+
22
+ _PAR_ID_STR = 'one'
23
+ _CLIENT_ID_STR = 'caches'
24
+ CACHE_DIR_DEFAULT = str(Path.home() / 'Downloads' / 'ONE')
25
+ """str: The default data download location"""
26
+
27
+
28
+ def default():
29
+ """Default Web client parameters."""
30
+ par = {'ALYX_URL': 'https://openalyx.internationalbrainlab.org',
31
+ 'ALYX_LOGIN': 'intbrainlab',
32
+ 'HTTP_DATA_SERVER': 'https://ibl.flatironinstitute.org/public',
33
+ 'HTTP_DATA_SERVER_LOGIN': None,
34
+ 'HTTP_DATA_SERVER_PWD': None}
35
+ return iopar.from_dict(par)
36
+
37
+
38
+ def _get_current_par(k, par_current):
39
+ """Return the current parameter value or the default.
40
+
41
+ Parameters
42
+ ----------
43
+ k : str
44
+ The parameter key lookup.
45
+ par_current : IBLParams
46
+ The current parameter set.
47
+
48
+ Returns
49
+ -------
50
+ any
51
+ The current parameter value or default if None or not set.
52
+
53
+ """
54
+ cpar = getattr(par_current, k, None)
55
+ if cpar is None:
56
+ cpar = getattr(default(), k, None)
57
+ return cpar
58
+
59
+
60
+ def _key_from_url(url: str) -> str:
61
+ """Convert a URL str to one valid for use as a file name or dict key.
62
+
63
+ URL Protocols are removed entirely.
64
+ The returned string will have characters in the set [a-zA-Z.-_].
65
+
66
+ Parameters
67
+ ----------
68
+ url : str
69
+ A URL string.
70
+
71
+ Returns
72
+ -------
73
+ str
74
+ A filename-safe string.
75
+
76
+ Example
77
+ -------
78
+ >>> url = _key_from_url('http://test.alyx.internationalbrainlab.org/')
79
+ 'test.alyx.internationalbrainlab.org'
80
+
81
+ """
82
+ url = unicodedata.normalize('NFKC', url) # Ensure ASCII
83
+ url = re.sub('^https?://', '', url).strip('/') # Remove protocol and trialing slashes
84
+ url = re.sub(r'[^.\w\s-]', '_', url.lower()) # Convert non word chars to underscore
85
+ return re.sub(r'[-\s]+', '-', url) # Convert spaces to hyphens
86
+
87
+
88
+ def setup(client=None, silent=False, make_default=None, username=None, cache_dir=None):
89
+ """Set up ONE parameters.
90
+
91
+ If a client (i.e. Alyx database URL) is provided, settings for that instance will be set.
92
+ If silent, the user will be prompted to input each parameter value. Pressing return will use
93
+ either current parameter or the default.
94
+
95
+ Parameters
96
+ ----------
97
+ client : str
98
+ An Alyx database URL. If None, the user will be prompted to input one.
99
+ silent : bool
100
+ If True, user is not prompted for any input.
101
+ make_default : bool
102
+ If True, client is set as the default and will be returned when calling `get` with no
103
+ arguments.
104
+ username : str, optional
105
+ The Alyx username to store in the params.
106
+ cache_dir : str, pathlib.Path
107
+ The default cache directory to store in the params.
108
+
109
+ Returns
110
+ -------
111
+ IBLParams
112
+ An updated cache map.
113
+
114
+ """
115
+ # First get default parameters
116
+ par_default = default()
117
+ default_url = par_default.ALYX_URL
118
+ client_key = _key_from_url(client or default_url)
119
+
120
+ # If a client URL has been provided, set it as the default URL
121
+ par_default = par_default.set('ALYX_URL', client or default_url)
122
+
123
+ # When silent=True, if setting up default database use default parameters
124
+ # instead of current ones to reset credentials
125
+ if silent and client_key == _key_from_url(default_url):
126
+ par_current = par_default
127
+ else:
128
+ par_current = iopar.read(f'{_PAR_ID_STR}/{client_key}', par_default)
129
+ if username:
130
+ par_current = par_current.set('ALYX_LOGIN', username)
131
+
132
+ # Load the db URL map
133
+ cache_map = iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {'CLIENT_MAP': dict()})
134
+
135
+ if not silent:
136
+ prompt = 'Param %s, current value is ["%s"]:'
137
+ par = iopar.as_dict(par_default)
138
+ quotes = '"\'`'
139
+ # Iterate through non-password pars
140
+ for k in filter(lambda k: 'PWD' not in k, par.keys()):
141
+ cpar = _get_current_par(k, par_current)
142
+ # Prompt for database and FI URL
143
+ if 'URL' in k:
144
+ if k == 'ALYX_URL' and client:
145
+ continue # skip if client url already provided
146
+ par[k] = input(prompt % (k, cpar)).strip().rstrip('/') or cpar
147
+ if '://' not in par[k]:
148
+ par[k] = 'https://' + par[k]
149
+ url_parsed = urlsplit(par[k])
150
+ if not (url_parsed.netloc and re.match('https?', url_parsed.scheme)):
151
+ raise ValueError(f'{k} must be valid HTTP URL')
152
+ else:
153
+ par[k] = input(prompt % (k, cpar)).strip() or cpar
154
+ # Check whether user erroneously entered quotation marks
155
+ # Prompting the user here (hopefully) corrects them before they input a password
156
+ # where the use of quotation marks may be legitimate
157
+ if par[k] and len(par[k]) >= 2 and par[k][0] in quotes and par[k][-1] in quotes:
158
+ warnings.warn('Do not use quotation marks with input answers', UserWarning)
159
+ ans = input('Strip quotation marks from response? [Y/n]:').strip() or 'y'
160
+ if ans.casefold()[0] == 'y':
161
+ par[k] = par[k].strip(quotes)
162
+ if k == 'ALYX_URL':
163
+ client = par[k]
164
+
165
+ cpar = _get_current_par('HTTP_DATA_SERVER_PWD', par_current)
166
+ prompt = f'Enter the FlatIron HTTP password for {par["HTTP_DATA_SERVER_LOGIN"]} '\
167
+ '(leave empty to keep current): '
168
+ par['HTTP_DATA_SERVER_PWD'] = getpass(prompt) or cpar
169
+
170
+ if 'ALYX_PWD' in par_current.as_dict():
171
+ # Only store plain text password if user manually added it to params JSON file
172
+ cpar = _get_current_par('ALYX_PWD', par_current)
173
+ prompt = (f'Enter the Alyx password for {par["ALYX_LOGIN"]} '
174
+ '(leave empty to keep current):')
175
+ par['ALYX_PWD'] = getpass(prompt) or cpar
176
+
177
+ par = iopar.from_dict(par)
178
+
179
+ # Prompt for cache directory (default may have changed after prompt)
180
+ client_key = _key_from_url(par.ALYX_URL)
181
+ def_cache_dir = cache_map.CLIENT_MAP.get(client_key) or Path(CACHE_DIR_DEFAULT, client_key)
182
+ cache_dir = cache_dir or def_cache_dir
183
+ prompt = f'Enter the location of the download cache, current value is ["{cache_dir}"]:'
184
+ cache_dir = input(prompt) or cache_dir
185
+
186
+ # Check if directory already used by another instance
187
+ in_use = [v for k, v in cache_map.CLIENT_MAP.items() if k != client_key]
188
+ while str(cache_dir) in in_use:
189
+ answer = input(
190
+ 'Warning: the directory provided is already a cache for another URL. '
191
+ 'This may cause conflicts. Would you like to change the cache location? [Y/n]')
192
+ if answer and answer[0].casefold() == 'n':
193
+ break
194
+ cache_dir = input(prompt) or cache_dir # Prompt for another directory
195
+
196
+ if make_default is None:
197
+ answer = input('Would you like to set this URL as the default one? [Y/n]')
198
+ make_default = (answer or 'y')[0].casefold() == 'y'
199
+
200
+ # Verify setup pars
201
+ answer = input('Are the above settings correct? [Y/n]')
202
+ if answer and answer.casefold()[0] == 'n':
203
+ print('SETUP ABANDONED. Please re-run.')
204
+ return par_current
205
+ else:
206
+ # Precedence: user provided cache_dir; previously defined; the default location
207
+ default_cache_dir = Path(CACHE_DIR_DEFAULT, client_key)
208
+ cache_dir = cache_dir or cache_map.CLIENT_MAP.get(client_key, default_cache_dir)
209
+ # Use current params but drop any extras (such as the TOKEN or ALYX_PWD field)
210
+ keep_keys = par_default.as_dict().keys()
211
+ par = iopar.from_dict({k: v for k, v in par_current.as_dict().items() if k in keep_keys})
212
+ if any(v for k, v in cache_map.CLIENT_MAP.items() if k != client_key and v == cache_dir):
213
+ warnings.warn('Warning: the directory provided is already a cache for another URL.')
214
+
215
+ # Update and save parameters
216
+ Path(cache_dir).mkdir(exist_ok=True, parents=True)
217
+ cache_map.CLIENT_MAP[client_key] = str(cache_dir)
218
+ if make_default or 'DEFAULT' not in cache_map.as_dict():
219
+ cache_map = cache_map.set('DEFAULT', client_key)
220
+
221
+ iopar.write(f'{_PAR_ID_STR}/{client_key}', par) # Client params
222
+ iopar.write(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', cache_map)
223
+
224
+ if not silent:
225
+ print('ONE Parameter files location: ' + iopar.getfile(_PAR_ID_STR))
226
+ return cache_map
227
+
228
+
229
+ def get(client=None, silent=False, username=None):
230
+ """Returns the AlyxClient parameters.
231
+
232
+ Parameters
233
+ ----------
234
+ silent : bool
235
+ If true, defaults are chosen if no parameters found.
236
+ client : str
237
+ The database URL to retrieve parameters for. If None, the default is loaded.
238
+ username : str
239
+ The username to use. If None, the default is loaded.
240
+
241
+ Returns
242
+ -------
243
+ IBLParams
244
+ A Params object for the AlyxClient.
245
+
246
+ """
247
+ client = client or get_default_client(include_schema=True)
248
+ client_key = _key_from_url(client) if client else None
249
+ cache_map = iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {})
250
+ # If there are no params for this client, run setup routine
251
+ if not cache_map or (client_key and client_key not in cache_map.CLIENT_MAP):
252
+ cache_map = setup(client=client, silent=silent, username=username)
253
+ cache = cache_map.CLIENT_MAP[client_key or cache_map.DEFAULT]
254
+ pars = iopar.read(f'{_PAR_ID_STR}/{client_key or cache_map.DEFAULT}').set('CACHE_DIR', cache)
255
+ if username:
256
+ pars = pars.set('ALYX_LOGIN', username)
257
+ return _patch_params(pars)
258
+
259
+
260
+ def get_default_client(include_schema=True) -> str:
261
+ """Returns the default AlyxClient URL, or None if no default is set.
262
+
263
+ Parameters
264
+ ----------
265
+ include_schema : bool
266
+ When True, the URL schema is included (i.e. http(s)://). Set to False to return the URL
267
+ as a client key.
268
+
269
+ Returns
270
+ -------
271
+ str
272
+ The default database URL with or without the schema, or None if no default is set
273
+
274
+ """
275
+ cache_map = iopar.as_dict(iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {})) or {}
276
+ client_key = cache_map.get('DEFAULT', None)
277
+ if not client_key or include_schema is False:
278
+ return client_key
279
+ return get(client_key).ALYX_URL
280
+
281
+
282
+ def save(par, client):
283
+ """Save a set of parameters for a given client.
284
+
285
+ Parameters
286
+ ----------
287
+ par : dict, IBLParams
288
+ A set of Web client parameters to save
289
+ client : str
290
+ The Alyx URL that corresponds to these parameters
291
+
292
+ """
293
+ # Remove cache dir variable before saving
294
+ par = {k: v for k, v in iopar.as_dict(par).items() if 'CACHE_DIR' not in k}
295
+ iopar.write(f'{_PAR_ID_STR}/{_key_from_url(client)}', par)
296
+
297
+
298
+ def get_cache_dir(client=None) -> Path:
299
+ """Return the download directory for a given client.
300
+
301
+ If no client is set up, the default download location is returned.
302
+
303
+ Parameters
304
+ ----------
305
+ client : str
306
+ The client to return cache dir from. If None, the default client is used.
307
+
308
+ Returns
309
+ -------
310
+ pathlib.Path
311
+ The download cache path
312
+
313
+ """
314
+ cache_map = iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {})
315
+ client = _key_from_url(client) if client else getattr(cache_map, 'DEFAULT', None)
316
+ cache_dir = Path(cache_map.CLIENT_MAP[client] if cache_map else CACHE_DIR_DEFAULT)
317
+ cache_dir.mkdir(exist_ok=True, parents=True)
318
+ return cache_dir
319
+
320
+
321
+ def get_params_dir() -> Path:
322
+ """Return the path to the root ONE parameters directory.
323
+
324
+ Returns
325
+ -------
326
+ pathlib.Path
327
+ The root ONE parameters directory
328
+
329
+ """
330
+ return Path(iopar.getfile(_PAR_ID_STR))
331
+
332
+
333
+ def check_cache_conflict(cache_dir):
334
+ """Assert that a given directory is not currently used as a cache directory.
335
+
336
+ This function checks whether a given directory is used as a cache directory for an Alyx
337
+ Web client. This function is called by the ONE factory to determine whether to return an
338
+ OneAlyx object or not. It is also used when setting up params for a new client.
339
+
340
+ Parameters
341
+ ----------
342
+ cache_dir : str, pathlib.Path
343
+ A directory to check.
344
+
345
+ Raises
346
+ ------
347
+ AssertionError
348
+ The directory is set as a cache for a Web client
349
+
350
+ """
351
+ cache_map = getattr(iopar.read(f'{_PAR_ID_STR}/{_CLIENT_ID_STR}', {}), 'CLIENT_MAP', None)
352
+ if cache_map:
353
+ assert not any(x == str(cache_dir) for x in cache_map.values())
354
+
355
+
356
+ def delete_params(base_url=None):
357
+ """Delete parameter files.
358
+
359
+ This will fully reset the ONE database and remote client parameters.
360
+
361
+ Parameters
362
+ ----------
363
+ base_url : str, optional
364
+ If provided, delete specific database parameters. If None, all parameters are removed.
365
+
366
+ """
367
+ if base_url:
368
+ client_key = _key_from_url(base_url)
369
+ params_file = Path(iopar.getfile(f'{_PAR_ID_STR}/{client_key}'))
370
+ if params_file.exists():
371
+ params_file.unlink()
372
+ else:
373
+ warnings.warn(f'{base_url}: params file not found')
374
+ else:
375
+ if (params_dir := get_params_dir()).exists():
376
+ shutil.rmtree(params_dir)
377
+
378
+
379
+ def _patch_params(par):
380
+ """Patch previous version of parameters, if required.
381
+
382
+ Parameters
383
+ ----------
384
+ par : IBLParams
385
+ The old parameters object.
386
+
387
+ Returns
388
+ -------
389
+ IBLParams
390
+ New parameters object containing the previous parameters.
391
+
392
+ """
393
+ # Patch the URL of data server, if database is OpenAlyx.
394
+ # The data location is in /public, however this path is no longer in the cache table
395
+ if 'openalyx' in par.ALYX_URL and 'public' not in par.HTTP_DATA_SERVER:
396
+ par = par.set('HTTP_DATA_SERVER', default().HTTP_DATA_SERVER)
397
+ save(par, par.ALYX_URL)
398
+
399
+ # Move old REST data
400
+ rest_dir = get_params_dir() / '.rest'
401
+ scheme, loc, *_ = urlsplit(par.ALYX_URL)
402
+ rest_dir /= Path(loc.replace(':', '_'), scheme)
403
+ new_rest_dir = Path(par.CACHE_DIR, '.rest')
404
+
405
+ if rest_dir.exists() and any(x for x in rest_dir.glob('*') if x.is_file()):
406
+ if not new_rest_dir.exists():
407
+ shutil.move(str(rest_dir), str(new_rest_dir))
408
+ from iblutil.io.params import set_hidden
409
+ set_hidden(new_rest_dir, True)
410
+ shutil.rmtree(rest_dir.parent)
411
+ if not any(get_params_dir().joinpath('.rest').glob('*')):
412
+ get_params_dir().joinpath('.rest').rmdir()
413
+
414
+ return par