siibra 1.0a1__1-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.

Potentially problematic release.


This version of siibra might be problematic. Click here for more details.

Files changed (84) hide show
  1. siibra/VERSION +1 -0
  2. siibra/__init__.py +164 -0
  3. siibra/commons.py +823 -0
  4. siibra/configuration/__init__.py +17 -0
  5. siibra/configuration/configuration.py +189 -0
  6. siibra/configuration/factory.py +589 -0
  7. siibra/core/__init__.py +16 -0
  8. siibra/core/assignment.py +110 -0
  9. siibra/core/atlas.py +239 -0
  10. siibra/core/concept.py +308 -0
  11. siibra/core/parcellation.py +387 -0
  12. siibra/core/region.py +1223 -0
  13. siibra/core/space.py +131 -0
  14. siibra/core/structure.py +111 -0
  15. siibra/exceptions.py +63 -0
  16. siibra/experimental/__init__.py +19 -0
  17. siibra/experimental/contour.py +61 -0
  18. siibra/experimental/cortical_profile_sampler.py +57 -0
  19. siibra/experimental/patch.py +98 -0
  20. siibra/experimental/plane3d.py +256 -0
  21. siibra/explorer/__init__.py +17 -0
  22. siibra/explorer/url.py +222 -0
  23. siibra/explorer/util.py +87 -0
  24. siibra/features/__init__.py +117 -0
  25. siibra/features/anchor.py +224 -0
  26. siibra/features/connectivity/__init__.py +33 -0
  27. siibra/features/connectivity/functional_connectivity.py +57 -0
  28. siibra/features/connectivity/regional_connectivity.py +494 -0
  29. siibra/features/connectivity/streamline_counts.py +27 -0
  30. siibra/features/connectivity/streamline_lengths.py +27 -0
  31. siibra/features/connectivity/tracing_connectivity.py +30 -0
  32. siibra/features/dataset/__init__.py +17 -0
  33. siibra/features/dataset/ebrains.py +90 -0
  34. siibra/features/feature.py +970 -0
  35. siibra/features/image/__init__.py +27 -0
  36. siibra/features/image/image.py +115 -0
  37. siibra/features/image/sections.py +26 -0
  38. siibra/features/image/volume_of_interest.py +88 -0
  39. siibra/features/tabular/__init__.py +24 -0
  40. siibra/features/tabular/bigbrain_intensity_profile.py +77 -0
  41. siibra/features/tabular/cell_density_profile.py +298 -0
  42. siibra/features/tabular/cortical_profile.py +322 -0
  43. siibra/features/tabular/gene_expression.py +257 -0
  44. siibra/features/tabular/layerwise_bigbrain_intensities.py +62 -0
  45. siibra/features/tabular/layerwise_cell_density.py +95 -0
  46. siibra/features/tabular/receptor_density_fingerprint.py +192 -0
  47. siibra/features/tabular/receptor_density_profile.py +110 -0
  48. siibra/features/tabular/regional_timeseries_activity.py +294 -0
  49. siibra/features/tabular/tabular.py +139 -0
  50. siibra/livequeries/__init__.py +19 -0
  51. siibra/livequeries/allen.py +352 -0
  52. siibra/livequeries/bigbrain.py +197 -0
  53. siibra/livequeries/ebrains.py +145 -0
  54. siibra/livequeries/query.py +49 -0
  55. siibra/locations/__init__.py +91 -0
  56. siibra/locations/boundingbox.py +454 -0
  57. siibra/locations/location.py +115 -0
  58. siibra/locations/point.py +344 -0
  59. siibra/locations/pointcloud.py +349 -0
  60. siibra/retrieval/__init__.py +27 -0
  61. siibra/retrieval/cache.py +233 -0
  62. siibra/retrieval/datasets.py +389 -0
  63. siibra/retrieval/exceptions/__init__.py +27 -0
  64. siibra/retrieval/repositories.py +769 -0
  65. siibra/retrieval/requests.py +659 -0
  66. siibra/vocabularies/__init__.py +45 -0
  67. siibra/vocabularies/gene_names.json +29176 -0
  68. siibra/vocabularies/receptor_symbols.json +210 -0
  69. siibra/vocabularies/region_aliases.json +460 -0
  70. siibra/volumes/__init__.py +23 -0
  71. siibra/volumes/parcellationmap.py +1279 -0
  72. siibra/volumes/providers/__init__.py +20 -0
  73. siibra/volumes/providers/freesurfer.py +113 -0
  74. siibra/volumes/providers/gifti.py +165 -0
  75. siibra/volumes/providers/neuroglancer.py +736 -0
  76. siibra/volumes/providers/nifti.py +266 -0
  77. siibra/volumes/providers/provider.py +107 -0
  78. siibra/volumes/sparsemap.py +468 -0
  79. siibra/volumes/volume.py +892 -0
  80. siibra-1.0.0a1.dist-info/LICENSE +201 -0
  81. siibra-1.0.0a1.dist-info/METADATA +160 -0
  82. siibra-1.0.0a1.dist-info/RECORD +84 -0
  83. siibra-1.0.0a1.dist-info/WHEEL +5 -0
  84. siibra-1.0.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,659 @@
1
+ # Copyright 2018-2024
2
+ # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Request files with decoders, lazy loading, and caching."""
16
+
17
+ from .cache import CACHE, cache_user_fn
18
+ from .exceptions import EbrainsAuthenticationError
19
+ from ..commons import (
20
+ logger,
21
+ HBP_AUTH_TOKEN,
22
+ KEYCLOAK_CLIENT_ID,
23
+ KEYCLOAK_CLIENT_SECRET,
24
+ siibra_tqdm,
25
+ SIIBRA_USE_LOCAL_SNAPSPOT,
26
+ )
27
+ from .. import __version__
28
+
29
+ import json
30
+ from zipfile import ZipFile
31
+ import requests
32
+ import os
33
+ from nibabel import Nifti1Image, GiftiImage, streamlines, freesurfer
34
+ from skimage import io as skimage_io
35
+ import gzip
36
+ from io import BytesIO
37
+ import urllib.parse
38
+ import pandas as pd
39
+ import numpy as np
40
+ from typing import List, Callable, TYPE_CHECKING
41
+ from enum import Enum
42
+ from functools import wraps
43
+ from time import sleep
44
+ import sys
45
+ from filelock import FileLock as Lock
46
+ if TYPE_CHECKING:
47
+ from .repositories import GitlabConnector
48
+
49
+ USER_AGENT_HEADER = {"User-Agent": f"siibra-python/{__version__}"}
50
+
51
+
52
+ def read_as_bytesio(function: Callable, suffix: str, bytesio: BytesIO):
53
+ """
54
+ Helper method to provide BytesIO to methods that only takes file path and
55
+ cannot handle BytesIO normally (e.g., `nibabel.freesurfer.read_annot()`).
56
+
57
+ Writes the bytes to a temporary file on cache and reads with the
58
+ original function.
59
+
60
+ Parameters
61
+ ----------
62
+ function : Callable
63
+ suffix : str
64
+ Must match the suffix expected by the function provided.
65
+ bytesio : BytesIO
66
+
67
+ Returns
68
+ -------
69
+ Return type of the provided function.
70
+ """
71
+ tempfile = CACHE.build_filename(f"temp_{suffix}") + suffix
72
+ with open(tempfile, "wb") as bf:
73
+ bf.write(bytesio.getbuffer())
74
+ result = function(tempfile)
75
+ os.remove(tempfile)
76
+ return result
77
+
78
+
79
+ DECODERS = {
80
+ ".nii": lambda b: Nifti1Image.from_bytes(b),
81
+ ".gii": lambda b: GiftiImage.from_bytes(b),
82
+ ".json": lambda b: json.loads(b.decode()),
83
+ ".tck": lambda b: streamlines.load(BytesIO(b)),
84
+ ".csv": lambda b: pd.read_csv(BytesIO(b)),
85
+ ".tsv": lambda b: pd.read_csv(BytesIO(b), delimiter="\t").dropna(axis=0, how="all"),
86
+ ".txt": lambda b: pd.read_csv(BytesIO(b), delimiter=" ", header=None),
87
+ ".zip": lambda b: ZipFile(BytesIO(b)),
88
+ ".png": lambda b: skimage_io.imread(BytesIO(b)),
89
+ ".npy": lambda b: np.load(BytesIO(b)),
90
+ ".annot": lambda b: read_as_bytesio(freesurfer.read_annot, '.annot', BytesIO(b)),
91
+ }
92
+
93
+
94
+ def find_suitiable_decoder(url: str) -> Callable:
95
+ """
96
+ By supplying a url or a filename, obtain a suitable decoder function
97
+ for siibra to digest based on predifined DECODERS. An extra layer of
98
+ gzip decompresser automatically added for gzipped files.
99
+
100
+ Parameters
101
+ ----------
102
+ url : str
103
+ The url or filename with extension.
104
+
105
+ Returns
106
+ -------
107
+ Callable or None
108
+ """
109
+ urlpath = urllib.parse.urlsplit(url).path
110
+ if urlpath.endswith(".gz"):
111
+ dec = find_suitiable_decoder(urlpath[:-3])
112
+ if dec is None:
113
+ return lambda b: gzip.decompress(b)
114
+ else:
115
+ return lambda b: dec(gzip.decompress(b))
116
+
117
+ suitable_decoders = [
118
+ dec for sfx, dec in DECODERS.items() if urlpath.endswith(sfx)
119
+ ]
120
+ if len(suitable_decoders) == 1:
121
+ return suitable_decoders[0]
122
+ else:
123
+ return None
124
+
125
+
126
+ class SiibraHttpRequestError(Exception):
127
+ def __init__(self, url: str, status_code: int, msg="Cannot execute http request."):
128
+ self.url = url
129
+ self.status_code = status_code
130
+ self.msg = msg
131
+ Exception.__init__(self)
132
+
133
+ def __str__(self):
134
+ return f"{self.msg}\n\tStatus code: {self.status_code}\n\tUrl: {self.url:76.76}"
135
+
136
+
137
+ class HttpRequest:
138
+ def __init__(
139
+ self,
140
+ url: str,
141
+ func: Callable = None,
142
+ msg_if_not_cached: str = None,
143
+ refresh=False,
144
+ post=False,
145
+ **kwargs,
146
+ ):
147
+ """
148
+ Initialize a cached http data loader.
149
+ It takes a URL and optional data conversion function.
150
+ For loading, the http request is only performed if the
151
+ result is not yet available in the disk cache.
152
+ Leaves the interpretation of the returned content to the caller.
153
+
154
+ Parameters
155
+ ----------
156
+ url : string, or None
157
+ URL for loading raw data, which is then fed into `func`
158
+ for creating the output.
159
+ If None, `func` will be called without arguments.
160
+ func : function pointer
161
+ Function for constructing the output data
162
+ (called on the data retrieved from `url`, if supplied)
163
+ refresh : bool, default: False
164
+ If True, a possibly cached content will be ignored and refreshed
165
+ post: bool, default: False
166
+ perform a post instead of get
167
+ """
168
+ assert url is not None
169
+ self.url = url
170
+ self._set_decoder_func(func)
171
+ self.kwargs = kwargs
172
+ self.cachefile = CACHE.build_filename(self.url + json.dumps(kwargs))
173
+ self.msg_if_not_cached = msg_if_not_cached
174
+ self.refresh = refresh
175
+ self.post = post
176
+
177
+ def _set_decoder_func(self, func: Callable = None):
178
+ """
179
+ Sets the decoder function of the HttpRequest. If `func` is None,
180
+ it will try to find a suitable decoder.
181
+
182
+ Parameters
183
+ ----------
184
+ func : Callable, default: None
185
+ """
186
+ self.func = func or find_suitiable_decoder(self.url)
187
+
188
+ @property
189
+ def cached(self):
190
+ return os.path.isfile(self.cachefile)
191
+
192
+ def _retrieve(self, block_size=1024, min_bytesize_with_no_progress_info=2e8):
193
+ """
194
+ Populates the file cache with the data from http if required.
195
+ noop if 1/ data is already cached and 2/ refresh flag not set
196
+ The caller should load the cachefile after _retrieve successfuly executes
197
+ """
198
+ if self.cached and not self.refresh:
199
+ return
200
+
201
+ # not yet in cache, perform http request.
202
+ if self.msg_if_not_cached is not None:
203
+ logger.debug(self.msg_if_not_cached)
204
+
205
+ headers = self.kwargs.get("headers", {})
206
+ other_kwargs = {
207
+ key: self.kwargs[key] for key in self.kwargs if key != "headers"
208
+ }
209
+
210
+ http_method = requests.post if self.post else requests.get
211
+ r = http_method(
212
+ self.url,
213
+ headers={
214
+ **USER_AGENT_HEADER,
215
+ **headers,
216
+ },
217
+ **other_kwargs,
218
+ stream=True,
219
+ )
220
+
221
+ if not r.ok:
222
+ raise SiibraHttpRequestError(status_code=r.status_code, url=self.url)
223
+
224
+ size_bytes = int(r.headers.get("content-length", 0))
225
+ if size_bytes > min_bytesize_with_no_progress_info:
226
+ progress_bar = siibra_tqdm(
227
+ total=size_bytes,
228
+ unit="iB",
229
+ unit_scale=True,
230
+ position=0,
231
+ leave=True,
232
+ desc=f"Downloading {os.path.split(self.url)[-1]} ({size_bytes / 1024**2:.1f} MiB)",
233
+ )
234
+ temp_cachefile = f"{self.cachefile}_temp"
235
+ lock = Lock(f"{temp_cachefile}.lock")
236
+
237
+ with lock:
238
+ with open(temp_cachefile, "wb") as f:
239
+ for data in r.iter_content(block_size):
240
+ if size_bytes > min_bytesize_with_no_progress_info:
241
+ progress_bar.update(len(data))
242
+ f.write(data)
243
+ if size_bytes > min_bytesize_with_no_progress_info:
244
+ progress_bar.close()
245
+ if self.refresh and os.path.isfile(self.cachefile):
246
+ os.remove(self.cachefile)
247
+ self.refresh = False
248
+ os.rename(temp_cachefile, self.cachefile)
249
+
250
+ def get(self):
251
+ self._retrieve()
252
+ with open(self.cachefile, "rb") as f:
253
+ data = f.read()
254
+ try:
255
+ return data if self.func is None else self.func(data)
256
+ except Exception as e:
257
+ # if network error results in bad cache, it may get raised here
258
+ # e.g. BadZipFile("File is not a zip file")
259
+ # if that happens, remove cachefile and
260
+ try:
261
+ os.unlink(self.cachefile)
262
+ except Exception:
263
+ pass
264
+ raise e
265
+
266
+ @property
267
+ def data(self):
268
+ # for backward compatibility with old LazyHttpRequest class
269
+ return self.get()
270
+
271
+
272
+ class FileLoader(HttpRequest):
273
+ """
274
+ Just a loads a local file, but mimics the behaviour
275
+ of cached http requests used in other connectors.
276
+ """
277
+ def __init__(self, filepath, func=None):
278
+ HttpRequest.__init__(
279
+ self, filepath, refresh=False,
280
+ func=func or find_suitiable_decoder(filepath)
281
+ )
282
+ self.cachefile = filepath
283
+
284
+ def _retrieve(self, **kwargs):
285
+ if kwargs:
286
+ logger.info(f"Keywords {list(kwargs.keys())} are supplied but won't be used.")
287
+ assert os.path.isfile(self.cachefile)
288
+
289
+
290
+ class ZipfileRequest(HttpRequest):
291
+ def __init__(self, url, filename, func=None, refresh=False):
292
+ HttpRequest.__init__(
293
+ self, url, refresh=refresh,
294
+ func=func or find_suitiable_decoder(filename)
295
+ )
296
+ self.filename = filename
297
+
298
+ def get(self):
299
+ self._retrieve()
300
+ zipfile = ZipFile(self.cachefile)
301
+ filenames = zipfile.namelist()
302
+ matches = [fn for fn in filenames if fn.endswith(self.filename)]
303
+ if len(matches) == 0:
304
+ raise RuntimeError(
305
+ f"Requested filename {self.filename} not found in archive at {self.url}"
306
+ )
307
+ if len(matches) > 1:
308
+ raise RuntimeError(
309
+ f'Requested filename {self.filename} was not unique in archive at {self.url}. Candidates were: {", ".join(matches)}'
310
+ )
311
+ with zipfile.open(matches[0]) as f:
312
+ data = f.read()
313
+ return data if self.func is None else self.func(data)
314
+
315
+
316
+ class EbrainsRequest(HttpRequest):
317
+ """
318
+ Implements lazy loading of HTTP Knowledge graph queries.
319
+ """
320
+
321
+ _KG_API_TOKEN: str = None
322
+ _IAM_ENDPOINT: str = "https://iam.ebrains.eu/auth/realms/hbp"
323
+ _IAM_DEVICE_ENDPOINT: str = None
324
+ _IAM_DEVICE_MAXTRIES = 12
325
+ _IAM_DEVICE_POLLING_INTERVAL_SEC = 5
326
+ _IAM_DEVICE_FLOW_CLIENTID = "siibra"
327
+
328
+ keycloak_endpoint = (
329
+ "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/token"
330
+ )
331
+
332
+ def __init__(
333
+ self, url, decoder=None, params={}, msg_if_not_cached=None, post=False
334
+ ):
335
+ """Construct an EBRAINS request."""
336
+ # NOTE: we do not pass params and header here,
337
+ # since we want to evaluate them late in the get() method.
338
+ # This is nice because it allows to set env. variable KG_TOKEN only when
339
+ # really needed, and not necessarily on package initialization.
340
+ self.params = params
341
+ HttpRequest.__init__(self, url, decoder, msg_if_not_cached, post=post)
342
+
343
+ @classmethod
344
+ def init_oidc(cls):
345
+ resp = requests.get(f"{cls._IAM_ENDPOINT}/.well-known/openid-configuration")
346
+ json_resp = resp.json()
347
+ if "token_endpoint" in json_resp:
348
+ logger.debug(
349
+ f"token_endpoint exists in .well-known/openid-configuration. Setting _IAM_TOKEN_ENDPOINT to {json_resp.get('token_endpoint')}"
350
+ )
351
+ cls._IAM_TOKEN_ENDPOINT = json_resp.get("token_endpoint")
352
+ else:
353
+ logger.warning(
354
+ "expect token endpoint in .well-known/openid-configuration, but was not present"
355
+ )
356
+
357
+ if "device_authorization_endpoint" in json_resp:
358
+ logger.debug(
359
+ f"device_authorization_endpoint exists in .well-known/openid-configuration. setting _IAM_DEVICE_ENDPOINT to {json_resp.get('device_authorization_endpoint')}"
360
+ )
361
+ cls._IAM_DEVICE_ENDPOINT = json_resp.get("device_authorization_endpoint")
362
+ else:
363
+ logger.warning(
364
+ "expected device_authorization_endpoint in .well-known/openid-configuration, but was not present"
365
+ )
366
+
367
+ @classmethod
368
+ def fetch_token(cls, **kwargs):
369
+ """
370
+ Fetch an EBRAINS token using commandline-supplied username/password
371
+ using the data proxy endpoint.
372
+
373
+
374
+ :ref:`Details on how to access EBRAINS are here.<accessEBRAINS>`
375
+ """
376
+ cls.device_flow(**kwargs)
377
+
378
+ @classmethod
379
+ def device_flow(cls, **kwargs):
380
+ if all(
381
+ [
382
+ not sys.__stdout__.isatty(), # if is tty, do not raise
383
+ not any(
384
+ k in ["JPY_INTERRUPT_EVENT", "JPY_PARENT_PID"] for k in os.environ
385
+ ), # if is notebook environment, do not raise
386
+ not os.getenv(
387
+ "SIIBRA_ENABLE_DEVICE_FLOW"
388
+ ), # if explicitly enabled by env var, do not raise
389
+ ]
390
+ ):
391
+ raise EbrainsAuthenticationError(
392
+ "sys.stdout is not tty, SIIBRA_ENABLE_DEVICE_FLOW is not set,"
393
+ "and not running in a notebook. Are you running in batch mode?"
394
+ )
395
+
396
+ cls.init_oidc()
397
+
398
+ def get_scope() -> str:
399
+ scope = kwargs.get("scope")
400
+ if not scope:
401
+ return None
402
+ if not isinstance(scope, list):
403
+ logger.warning("scope needs to be a list, is but is not... skipping")
404
+ return None
405
+ if not all(isinstance(scope, str) for scope in scope):
406
+ logger.warning("scope needs to be all str, but is not")
407
+ return None
408
+ if len(scope) == 0:
409
+ logger.warning("provided empty list as scope... skipping")
410
+ return None
411
+ return "+".join(scope)
412
+
413
+ scope = get_scope()
414
+
415
+ data = {"client_id": cls._IAM_DEVICE_FLOW_CLIENTID}
416
+
417
+ if scope:
418
+ data["scope"] = scope
419
+
420
+ resp = requests.post(url=cls._IAM_DEVICE_ENDPOINT, data=data)
421
+ resp.raise_for_status()
422
+ resp_json = resp.json()
423
+ logger.debug("device flow, request full json:", resp_json)
424
+
425
+ assert "verification_uri_complete" in resp_json
426
+ assert "device_code" in resp_json
427
+
428
+ device_code = resp_json.get("device_code")
429
+
430
+ print("***")
431
+ print(f"To continue, please go to {resp_json.get('verification_uri_complete')}")
432
+ print("***")
433
+
434
+ attempt_number = 0
435
+ sleep_timer = cls._IAM_DEVICE_POLLING_INTERVAL_SEC
436
+ while True:
437
+ # TODO the polling is a little busted at the moment.
438
+ # need to speak to axel to shorten the polling duration
439
+ sleep(sleep_timer)
440
+
441
+ logger.debug("Calling endpoint")
442
+ if attempt_number > cls._IAM_DEVICE_MAXTRIES:
443
+ message = (
444
+ f"exceeded max attempts: {cls._IAM_DEVICE_MAXTRIES}, aborting..."
445
+ )
446
+ logger.error(message)
447
+ raise EbrainsAuthenticationError(message)
448
+ attempt_number += 1
449
+ resp = requests.post(
450
+ url=cls._IAM_TOKEN_ENDPOINT,
451
+ data={
452
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
453
+ "client_id": cls._IAM_DEVICE_FLOW_CLIENTID,
454
+ "device_code": device_code,
455
+ },
456
+ )
457
+
458
+ if resp.status_code == 200:
459
+ json_resp = resp.json()
460
+ logger.debug("Device flow sucessful:", json_resp)
461
+ cls._KG_API_TOKEN = json_resp.get("access_token")
462
+ print("ebrains token successfuly set.")
463
+ break
464
+
465
+ if resp.status_code == 400:
466
+ json_resp = resp.json()
467
+ error = json_resp.get("error")
468
+ if error == "slow_down":
469
+ sleep_timer += 1
470
+ logger.debug(f"400 error: {resp.content}")
471
+ continue
472
+
473
+ raise EbrainsAuthenticationError(resp.content)
474
+
475
+ @classmethod
476
+ def set_token(cls, token):
477
+ logger.info(f"Setting EBRAINS Knowledge Graph authentication token: {token}")
478
+ cls._KG_API_TOKEN = token
479
+
480
+ @property
481
+ def kg_token(self):
482
+ # token is available, return it
483
+ if self.__class__._KG_API_TOKEN is not None:
484
+ return self.__class__._KG_API_TOKEN
485
+
486
+ # See if a token is directly provided in $HBP_AUTH_TOKEN
487
+ if HBP_AUTH_TOKEN:
488
+ self.__class__._KG_API_TOKEN = HBP_AUTH_TOKEN
489
+ return self.__class__._KG_API_TOKEN
490
+
491
+ # try KEYCLOAK. Requires the following environment variables set:
492
+ # KEYCLOAK_ENDPOINT, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET
493
+
494
+ if KEYCLOAK_CLIENT_ID is not None and KEYCLOAK_CLIENT_SECRET is not None:
495
+ logger.info("Getting an EBRAINS token via keycloak client configuration...")
496
+ result = requests.post(
497
+ self.__class__._IAM_TOKEN_ENDPOINT,
498
+ data=(
499
+ f"grant_type=client_credentials&client_id={KEYCLOAK_CLIENT_ID}"
500
+ f"&client_secret={KEYCLOAK_CLIENT_SECRET}"
501
+ "&scope=kg-nexus-role-mapping%20kg-nexus-service-account-mock"
502
+ ),
503
+ headers={
504
+ "content-type": "application/x-www-form-urlencoded",
505
+ **USER_AGENT_HEADER,
506
+ },
507
+ )
508
+ try:
509
+ content = json.loads(result.content.decode("utf-8"))
510
+ except json.JSONDecodeError as error:
511
+ logger.error(f"Invalid json from keycloak:{error}")
512
+ self.__class__._KG_API_TOKEN = None
513
+ if "error" in content:
514
+ logger.error(content["error_description"])
515
+ self.__class__._KG_API_TOKEN = None
516
+ self.__class__._KG_API_TOKEN = content["access_token"]
517
+
518
+ if self.__class__._KG_API_TOKEN is None:
519
+ # No success getting the token
520
+ raise RuntimeError(
521
+ "No access token for EBRAINS Knowledge Graph found. "
522
+ "If you do not have an EBRAINS account, please first register at "
523
+ "https://ebrains.eu/register. Then, use one of the following option: "
524
+ "\n 1. Let siibra get you a token by using siibra.fetch_ebrains_token() and follow the prompt."
525
+ "\n 2. If you know how to get a token yourself, set it as $HBP_AUTH_TOKEN or siibra.set_ebrains_token()"
526
+ "\n 3. If you are an application developer, you might configure keycloak access by setting $KEYCLOAK_CLIENT_ID"
527
+ "and $KEYCLOAK_CLIENT_SECRET."
528
+ )
529
+
530
+ return self.__class__._KG_API_TOKEN
531
+
532
+ @property
533
+ def auth_headers(self):
534
+ return {
535
+ "Content-Type": "application/json",
536
+ "Authorization": f"Bearer {self.kg_token}",
537
+ }
538
+
539
+ def get(self):
540
+ """Evaluate KG Token is evaluated only on execution of the request."""
541
+ self.kwargs = {"headers": self.auth_headers, "params": self.params}
542
+ return super().get()
543
+
544
+
545
+ def try_all_connectors():
546
+ def outer(fn):
547
+ @wraps(fn)
548
+ def inner(self: "GitlabProxyEnum", *args, **kwargs):
549
+ exceptions = []
550
+ for connector in self.connectors:
551
+ try:
552
+ return fn(self, *args, connector=connector, **kwargs)
553
+ except Exception as e:
554
+ exceptions.append(e)
555
+ else:
556
+ for exc in exceptions:
557
+ logger.error(exc)
558
+ raise Exception("try_all_connectors failed")
559
+
560
+ return inner
561
+
562
+ return outer
563
+
564
+
565
+ class GitlabProxyEnum(Enum):
566
+ DATASET_V1 = "DATASET_V1"
567
+ PARCELLATIONREGION_V1 = "PARCELLATIONREGION_V1"
568
+ DATASET_V3 = "DATASET_V3"
569
+ DATASETVERSION_V3 = "DATASETVERSION_V3"
570
+
571
+ @property
572
+ def connectors(self) -> List["GitlabConnector"]:
573
+ servers = [
574
+ ("https://jugit.fz-juelich.de", 7846),
575
+ ("https://gitlab.ebrains.eu", 421),
576
+ ]
577
+ from .repositories import GitlabConnector, LocalFileRepository
578
+
579
+ if SIIBRA_USE_LOCAL_SNAPSPOT:
580
+ logger.info(f"Using localsnapshot at {SIIBRA_USE_LOCAL_SNAPSPOT}")
581
+ return [LocalFileRepository(SIIBRA_USE_LOCAL_SNAPSPOT)]
582
+ else:
583
+ return [
584
+ GitlabConnector(server[0], server[1], "master", archive_mode=True)
585
+ for server in servers
586
+ ]
587
+
588
+ @try_all_connectors()
589
+ def search_files(
590
+ self,
591
+ folder: str,
592
+ suffix=None,
593
+ recursive=True,
594
+ *,
595
+ connector: "GitlabConnector" = None,
596
+ ) -> List[str]:
597
+ assert connector
598
+ return connector.search_files(folder, suffix=suffix, recursive=recursive)
599
+
600
+ @try_all_connectors()
601
+ def get(self, filename, decode_func=None, *, connector: "GitlabConnector" = None):
602
+ assert connector
603
+ return connector.get(filename, "", decode_func)
604
+
605
+
606
+ class GitlabProxy(HttpRequest):
607
+ folder_dict = {
608
+ GitlabProxyEnum.DATASET_V1: "ebrainsquery/v1/dataset",
609
+ GitlabProxyEnum.DATASET_V3: "ebrainsquery/v3/Dataset",
610
+ GitlabProxyEnum.DATASETVERSION_V3: "ebrainsquery/v3/DatasetVersion",
611
+ GitlabProxyEnum.PARCELLATIONREGION_V1: "ebrainsquery/v1/parcellationregions",
612
+ }
613
+
614
+ def __init__(
615
+ self,
616
+ flavour: GitlabProxyEnum,
617
+ instance_id=None,
618
+ ):
619
+ if flavour not in GitlabProxyEnum:
620
+ raise RuntimeError("Can only proxy enum members")
621
+
622
+ self.flavour = flavour
623
+ self.folder = self.folder_dict[flavour]
624
+ self.instance_id = instance_id
625
+ self.get = cache_user_fn(self.get)
626
+
627
+ def get(self):
628
+ if self.instance_id:
629
+ return self.flavour.get(f"{self.folder}/{self.instance_id}.json")
630
+ return {
631
+ "results": self.flavour.get(f"{self.folder}/_all.json")
632
+ }
633
+
634
+
635
+ class MultiSourceRequestException(Exception):
636
+ pass
637
+
638
+
639
+ class MultiSourcedRequest:
640
+ requests: List[HttpRequest] = []
641
+
642
+ def __init__(self, requests: List[HttpRequest]) -> None:
643
+ self.requests = requests
644
+
645
+ def get(self):
646
+ exceptions = []
647
+ for req in self.requests:
648
+ try:
649
+ return req.get()
650
+ except Exception as e:
651
+ exceptions.append(e)
652
+ else:
653
+ raise MultiSourceRequestException(
654
+ "All requests failed:\n" + "\n".join(str(exc) for exc in exceptions)
655
+ )
656
+
657
+ @property
658
+ def data(self):
659
+ return self.get()
@@ -0,0 +1,45 @@
1
+ # Copyright 2018-2024
2
+ # Institute of Neuroscience and Medicine (INM-1), Forschungszentrum Jülich GmbH
3
+
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Abbreviations and aliases."""
16
+
17
+ from ..commons import InstanceTable
18
+
19
+ import json
20
+ from os import path
21
+
22
+
23
+ RT_DIR = path.dirname(__file__)
24
+
25
+
26
+ def runtime_path(fname: str):
27
+ return path.join(RT_DIR, fname)
28
+
29
+
30
+ with open(runtime_path('gene_names.json'), 'r') as f:
31
+ _gene_names = json.load(f)
32
+ GENE_NAMES = InstanceTable(
33
+ elements={
34
+ k: {'symbol': k, 'description': v}
35
+ for k, v in _gene_names.items()
36
+ }
37
+ )
38
+
39
+
40
+ with open(runtime_path('receptor_symbols.json'), 'r') as f:
41
+ RECEPTOR_SYMBOLS = json.load(f)
42
+
43
+
44
+ with open(runtime_path('region_aliases.json'), 'r') as f:
45
+ REGION_ALIASES = json.load(f)