Skip to content

Commit d84f4a0

Browse files
committed
Refactor the authentication code; add an "interactive=False" option for using the client in headless scripts where we can't prompt for passwords
1 parent 23e56bd commit d84f4a0

File tree

4 files changed

+88
-67
lines changed

4 files changed

+88
-67
lines changed

codemeta.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"@type": "SoftwareSourceCode",
44
"license": "https://spdx.org/licenses/BSD-3-Clause.html",
55
"codeRepository": "https://github.com/HumanBrainProject/ebrains-validation-client",
6-
"dateModified": "2024-10-09",
7-
"downloadUrl": "https://files.pythonhosted.org/packages/be/17/e483a75b4b94dcc69e8b1be21ef11667ec74c8d8d1e7858bbeee71d62d1e/ebrains_validation_framework-0.9.1.tar.gz",
6+
"dateModified": "2024-10-25",
7+
"downloadUrl": null,
88
"issueTracker": "https://github.com/HumanBrainProject/ebrains-validation-client/issues",
99
"name": "ebrains-validation-framework",
10-
"version": "0.9.1",
11-
"identifier": "https://pypi.org/project/ebrains-validation-framework/0.9.1/",
10+
"version": "0.9.2",
11+
"identifier": "https://pypi.org/project/ebrains-validation-framework/0.9.2/",
1212
"description": "Python client for the EBRAINS Validation Framework web services.",
1313
"applicationCategory": "neuroscience",
1414
"funding": "https://cordis.europa.eu/project/id/945539",

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
# The short X.Y version.
6767
version = "0.9"
6868
# The full version, including alpha/beta/rc tags.
69-
release = "0.9.1"
69+
release = "0.9.2"
7070

7171
# The language for content autogenerated by Sphinx. Refer to documentation
7272
# for a list of supported languages.

ebrains_validation_framework/__init__.py

Lines changed: 82 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
have_collab_token_handler = False
3636

3737

38-
__version__ = "0.9.1"
38+
__version__ = "0.9.2"
3939

4040

4141
TOKENFILE = os.path.expanduser("~/.ebrainstoken")
@@ -90,7 +90,7 @@ class BaseClient(object):
9090

9191
__test__ = False
9292

93-
def __init__(self, username=None, password=None, environment="production", token=None):
93+
def __init__(self, username=None, password=None, environment="production", token=None, interactive=True):
9494
self.username = username
9595
self.verify = True
9696
self.environment = environment
@@ -118,79 +118,85 @@ def __init__(self, username=None, password=None, environment="production", token
118118
raise KeyError(f"{err_msg_base} does not contain environment = {environment}")
119119
else:
120120
raise IOError(f"{err_msg_base} not found in the current directory.")
121-
if self.token:
122-
pass
123-
elif password is None:
124-
self.token = None
121+
self._authenticate(password, interactive)
122+
123+
def _authenticate(self, password, interactive):
124+
# If a token is provided, we try using it.
125+
# If not, we try to get a token from the environment
126+
# or from a local cache
127+
if not self.token:
125128
if have_collab_token_handler:
126129
# if are we running in a Jupyter notebook within the Collaboratory
127130
# the token is already available
128131
self.token = oauth.get_token()
129132
elif os.path.exists(TOKENFILE):
130-
if username:
131-
# check for a stored token
133+
if self.username:
132134
with open(TOKENFILE) as fp:
133-
data = json.load(fp).get(username, None)
135+
data = json.load(fp).get(self.username, None)
134136
if data and "access_token" in data:
135137
self.token = data["access_token"]
136-
if not self._check_token_valid():
137-
print(
138-
"EBRAINS authentication token is invalid or has expired. "
139-
"Will need to re-authenticate."
140-
)
141-
self.token = None
142138
else:
143-
print(f"EBRAINS authentication token file not having required JSON data. data = {data}")
139+
print(f"No token for {self.username} found in {TOKENFILE}")
144140
else:
145141
print("Authentication token file found, but you have not provided your username.")
146142
else:
147143
print("EBRAINS authentication token file not found locally.")
148144

149-
if self.token is None:
150-
if not username:
151-
print("\n==============================================")
152-
print("Please enter your EBRAINS username.")
153-
username = input("EBRAINS Username: ")
154-
155-
password = os.environ.get("EBRAINS_PASS")
156-
if password is not None:
157-
try:
158-
self._ebrains_auth(username, password)
159-
except Exception:
160-
print(
161-
"Authentication Failure. "
162-
"Possibly incorrect EBRAINS password saved in environment variable 'EBRAINS_PASS'."
163-
)
164-
if not hasattr(self, "config"):
145+
# If we don't have a token, we try to authenticate with username and password
146+
if not self._check_token_valid():
147+
print(
148+
"EBRAINS authentication token is invalid or has expired. "
149+
"Will need to re-authenticate."
150+
)
151+
if (not self.username) and interactive:
152+
print("\n==============================================")
153+
print("Please enter your EBRAINS username.")
154+
self.username = input("EBRAINS Username: ")
155+
156+
if self.username:
157+
password = password or os.environ.get("EBRAINS_PASS")
158+
if (not password) and interactive:
159+
# prompt for password
160+
print("Please enter your EBRAINS password: ")
161+
password = getpass.getpass()
162+
163+
if password:
165164
try:
166-
# prompt for password
167-
print("Please enter your EBRAINS password: ")
168-
password = getpass.getpass()
169-
self._ebrains_auth(username, password)
165+
self._ebrains_auth(self.username, password)
170166
except Exception:
171167
print("Authentication Failure! Password entered is possibly incorrect.")
172168
raise
173-
with open(TOKENFILE, "w") as fp:
174-
json.dump({username: {"access_token": self.config["access_token"]}}, fp)
175-
os.chmod(TOKENFILE, 0o600)
176-
else:
177-
try:
178-
self._ebrains_auth(username, password)
179-
except Exception:
180-
print("Authentication Failure! Password entered is possibly incorrect.")
181-
raise
169+
170+
if self.token:
171+
self.auth = EBRAINSAuth(self.token)
172+
173+
# store token in local cache
174+
if os.path.exists(TOKENFILE):
175+
with open(TOKENFILE, "r") as fp:
176+
token_data = json.load(fp)
177+
else:
178+
token_data = {}
179+
token_data[self.username] = {"access_token": self.token}
180+
182181
with open(TOKENFILE, "w") as fp:
183-
json.dump({username: {"access_token": self.config["access_token"]}}, fp)
182+
json.dump(token_data, fp)
183+
fp.write("\n")
184184
os.chmod(TOKENFILE, 0o600)
185-
self.auth = EBRAINSAuth(self.token)
185+
else:
186+
self.auth = None
186187

187188
def _check_token_valid(self):
188-
url = "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/userinfo"
189-
data = requests.get(url, auth=EBRAINSAuth(self.token), verify=self.verify)
190-
if data.status_code == 200:
191-
return True
192-
else:
193-
return False
189+
if self.token:
190+
url = "https://iam.ebrains.eu/auth/realms/hbp/protocol/openid-connect/userinfo"
191+
data = requests.get(url, auth=EBRAINSAuth(self.token), verify=self.verify)
192+
if data.status_code == 200:
193+
remote_username = data.json()["preferred_username"]
194+
if self.username and self.username != remote_username:
195+
raise Exception("Username does not match token")
196+
else:
197+
self.username = remote_username
198+
return True
199+
return False
194200

195201
def _format_people_name(self, names):
196202
# converts a string of people names separated by semi-colons
@@ -447,7 +453,7 @@ class TestLibrary(BaseClient):
447453
}
448454
449455
token : string, optional
450-
You may directly input a valid authenticated token from Collaboratory v1 or v2.
456+
You may directly input a valid authenticated EBRAINS access token.
451457
Note: you should use the `access_token` and NOT `refresh_token`.
452458
453459
Examples
@@ -460,8 +466,8 @@ class TestLibrary(BaseClient):
460466

461467
__test__ = False
462468

463-
def __init__(self, username=None, password=None, environment="production", token=None):
464-
super(TestLibrary, self).__init__(username, password, environment, token)
469+
def __init__(self, username=None, password=None, environment="production", token=None, interactive=True):
470+
super(TestLibrary, self).__init__(username, password, environment, token, interactive)
465471
self._set_app_info()
466472

467473
def _set_app_info(self):
@@ -1580,8 +1586,13 @@ def register_result(self, test_result, data_store=None, collab_id=None):
15801586
files_to_upload.extend(test_result.related_data["figures"])
15811587
if files_to_upload:
15821588
list_dict_files_uploaded = [
1583-
{"download_url": f["filepath"], "size": f["filesize"]}
1584-
for f in data_store.upload_data(files_to_upload)
1589+
{
1590+
"download_url": ftu["filepath"],
1591+
"size": ftu["filesize"],
1592+
"hash": ftu["hash"],
1593+
"local_path": ftu["local_path"]
1594+
}
1595+
for ftu in data_store.upload_data(files_to_upload)
15851596
]
15861597
results_storage.extend(list_dict_files_uploaded)
15871598

@@ -1729,8 +1740,8 @@ class ModelCatalog(BaseClient):
17291740

17301741
__test__ = False
17311742

1732-
def __init__(self, username=None, password=None, environment="production", token=None):
1733-
super(ModelCatalog, self).__init__(username, password, environment, token)
1743+
def __init__(self, username=None, password=None, environment="production", token=None, interactive=True):
1744+
super(ModelCatalog, self).__init__(username, password, environment, token, interactive)
17341745
self._set_app_info()
17351746

17361747
def _set_app_info(self):
@@ -2541,6 +2552,7 @@ def add_model_instance(
25412552
hash="",
25422553
morphology="",
25432554
license="",
2555+
collab_id=None
25442556
):
25452557
"""Register a new model instance.
25462558
@@ -2569,6 +2581,10 @@ def add_model_instance(
25692581
URL(s) to the morphology file(s) employed in this model.
25702582
license : string
25712583
Indicates the license applicable for this model instance.
2584+
collab_id : string
2585+
Specifies the ID of the host collab in the EBRAINS Collaboratory
2586+
(the model instance would belong to this collab).
2587+
If not provided, defaults to the collab of the parent model project.
25722588
25732589
Returns
25742590
-------
@@ -2590,6 +2606,7 @@ def add_model_instance(
25902606

25912607
instance_data = locals()
25922608
instance_data.pop("self")
2609+
instance_data["project_id"] = instance_data.pop("collab_id")
25932610

25942611
for key, val in instance_data.items():
25952612
if val == "":
@@ -2614,7 +2631,7 @@ def add_model_instance(
26142631
else:
26152632
handle_response_error("Error in adding model instance", response)
26162633

2617-
def find_model_instance_else_add(self, model_obj):
2634+
def find_model_instance_else_add(self, model_obj, collab_id=None):
26182635
"""Find existing model instance; else create a new instance
26192636
26202637
This checks if the input model object has an associated model instance.
@@ -2624,6 +2641,9 @@ def find_model_instance_else_add(self, model_obj):
26242641
----------
26252642
model_obj : object
26262643
Python object representing a model.
2644+
collab_id : str
2645+
In case of adding a new model instance, add it to this collab.
2646+
If None, add to the same collab as the parent model.
26272647
26282648
Returns
26292649
-------
@@ -2664,6 +2684,7 @@ def find_model_instance_else_add(self, model_obj):
26642684
source=getattr(model_obj, "remote_url", ""),
26652685
version=model_obj.model_version,
26662686
parameters=getattr(model_obj, "parameters", ""),
2687+
collab_id=collab_id or getattr(model_obj, "collab_id", None)
26672688
)
26682689
else:
26692690
model_instance = self.get_model_instance(instance_id=model_obj.model_instance_uuid)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "ebrains_validation_framework"
3-
version = "0.9.1"
3+
version = "0.9.2"
44
description = "Python client for the EBRAINS Validation Framework web services."
55
readme = "README.md"
66
authors = [

0 commit comments

Comments
 (0)