Skip to content

Commit 7a6c383

Browse files
authored
Merge pull request #49 from HumanBrainProject/fix/bucketClient
fix: ebrains bucket client
2 parents 2e63c71 + c962b21 commit 7a6c383

File tree

4 files changed

+142
-28
lines changed

4 files changed

+142
-28
lines changed

.github/workflows/tests.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Tests
2+
3+
# Configure the events that are going to trigger tha automated update of the mirror
4+
on:
5+
push:
6+
branches: [master]
7+
pull_request:
8+
9+
# Configure what will be updated
10+
jobs:
11+
# set the job name
12+
unit-tests:
13+
runs-on: ubuntu-latest
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
python-version:
18+
- "3.13"
19+
- "3.12"
20+
- "3.11"
21+
- "3.10"
22+
- "3.9"
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Set up Python ${{ matrix.python-version }}
27+
uses: actions/setup-python@v4
28+
with:
29+
python-version: ${{ matrix.python-version }}
30+
31+
- name: Install dependencies
32+
run: |
33+
python -m pip install --upgrade pip
34+
pip install .[test]
35+
36+
- name: Run tests
37+
run: pytest tests
38+
39+
e2e-tests:
40+
runs-on: ubuntu-latest
41+
strategy:
42+
fail-fast: false
43+
matrix:
44+
python-version:
45+
# - "3.13"
46+
# - "3.12"
47+
# - "3.11"
48+
# - "3.10"
49+
- "3.9"
50+
51+
steps:
52+
- uses: actions/checkout@v4
53+
54+
- name: Set up Python ${{ matrix.python-version }}
55+
uses: actions/setup-python@v4
56+
with:
57+
python-version: ${{ matrix.python-version }}
58+
59+
- name: Install dependencies
60+
run: |
61+
python -m pip install --upgrade pip
62+
pip install .[test]
63+
64+
- name: Run tests
65+
run: pytest e2e/ --durations=50

e2e/test_bucket_get.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import json
2+
3+
import pytest
4+
import requests
5+
6+
from ebrains_drive import BucketApiClient
7+
8+
@pytest.fixture
9+
def bucket_client():
10+
yield BucketApiClient()
11+
12+
def test_get(bucket_client):
13+
url = "https://data-proxy.ebrains.eu/api/v1/buckets/reference-atlas-data/precomputed/BigBrainRelease.2015/8bit/info"
14+
bucket = bucket_client.buckets.get_bucket("reference-atlas-data")
15+
file = bucket.get_file("precomputed/BigBrainRelease.2015/8bit/info")
16+
file_json = json.loads(file.get_content())
17+
resp = requests.get(url)
18+
resp.raise_for_status()
19+
assert resp.json() == json.loads(file.get_content())
20+
assert file_json["type"] == "image"
21+

ebrains_drive/client.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from getpass import getpass
2-
import requests
32
from abc import ABC
43
import base64
54
import json
65
import time
7-
from copy import copy, deepcopy
6+
from copy import copy
7+
from typing import Callable
8+
from functools import wraps
9+
10+
import requests
11+
812
from ebrains_drive.utils import on_401_raise_unauthorized
913
from ebrains_drive.exceptions import ClientHttpError, TokenExpired, Unauthorized
1014
from ebrains_drive.repos import Repos
@@ -71,18 +75,6 @@ def put(self, *args, **kwargs):
7175
def delete(self, *args, **kwargs):
7276
return self.send_request("DELETE", *args, **kwargs)
7377

74-
def _exchange_oidc_for_seafile_token(self):
75-
url = self.server.rstrip("/") + "/api2/account/token/"
76-
headers = {"Authorization": f"Bearer {self._token}"}
77-
78-
resp = self.session.get(url, headers=headers)
79-
80-
if resp.status_code != 200:
81-
raise Exception(f"Failed to exchange OIDC token for Seafile token: {resp.status_code} {resp.text}")
82-
83-
self._seafile_token = resp.text.strip()
84-
return self._seafile_token
85-
8678
def send_request(self, method: str, url: str, *args, **kwargs):
8779
if not url.startswith("http"):
8880
# sanity checks.
@@ -94,31 +86,62 @@ def send_request(self, method: str, url: str, *args, **kwargs):
9486
# We cannot deepcopy the whole thing, because some values (e.g. BufferedReader objects)
9587
# cannot be pickled
9688
kwargs = copy(kwargs)
97-
headers = kwargs.pop("headers", {}).copy()
89+
headers: dict = kwargs.pop("headers", {}).copy()
90+
token_auth = kwargs.pop("token_auth", None)
9891

99-
if self._seafile_token:
100-
headers.setdefault("Authorization", "Token " + self._seafile_token)
101-
else:
102-
headers.setdefault("Authorization", "Bearer " + self._token)
92+
auth_header = f"Token {token_auth}" if token_auth else f"Bearer {self._token}"
93+
headers.setdefault("Authorization", auth_header)
10394

10495
expected = kwargs.pop("expected", 200)
10596
if not hasattr(expected, "__iter__"):
10697
expected = (expected,)
10798

10899
resp = self.session.request(method, url, headers=headers, *args, **kwargs)
109100

110-
if resp.status_code == 401 and not self._seafile_token:
111-
self._seafile_token = self._exchange_oidc_for_seafile_token()
112-
113-
headers["Authorization"] = "Token " + self._seafile_token
114-
resp = self.session.request(method, url, headers=headers, *args, **kwargs)
115-
116101
if resp.status_code not in expected:
117102
msg = f"Expected {expected}, but got {resp.status_code}"
118103
raise ClientHttpError(resp.status_code, msg)
119104

120105
return resp
121106

107+
108+
def wrap_exchange_seafile_token():
109+
def exchange_oidc_for_seafile(self: "DriveApiClient"):
110+
111+
url = self.server.rstrip("/") + "/api2/account/token/"
112+
headers = {"Authorization": f"Bearer {self._token}"}
113+
114+
resp = self.session.get(url, headers=headers)
115+
resp.raise_for_status()
116+
117+
return resp.text.strip()
118+
119+
def outer(fn: Callable):
120+
@wraps(fn)
121+
def inner(self, *args, **kwargs):
122+
assert isinstance(self, DriveApiClient), f"seafile exchange can only decorate DriveApiClient"
123+
124+
kwargs = copy(kwargs)
125+
126+
if self._seafile_token is None:
127+
self._seafile_token = exchange_oidc_for_seafile(self)
128+
129+
retry_counter = 1
130+
while retry_counter >= 0:
131+
try:
132+
kwargs["token_auth"] = self._seafile_token
133+
return fn(self, *args, **kwargs)
134+
except ClientHttpError as e:
135+
if e.code == 401:
136+
self._seafile_token = exchange_oidc_for_seafile(self)
137+
retry_counter -= 1
138+
continue
139+
raise e from e
140+
141+
return inner
142+
return outer
143+
144+
122145
class DriveApiClient(ClientBase):
123146
"""Wraps seafile web api"""
124147

@@ -152,6 +175,7 @@ def __str__(self):
152175

153176
__repr__ = __str__
154177

178+
@wrap_exchange_seafile_token()
155179
def send_request(self, method: str, url: str, *args, **kwargs):
156180
if not url.startswith("http"):
157181
assert not self.server.endswith("/")
@@ -162,7 +186,7 @@ def send_request(self, method: str, url: str, *args, **kwargs):
162186
return super().send_request(method, url, *args, **kwargs)
163187

164188

165-
_I_AM_A_PUBLIC_BUCKET = "_I_AM_A_PUBLIC_BUCKET"
189+
_I_AM_A_PUBLIC_BUCKET = object()
166190

167191

168192
class BucketApiClient(ClientBase):
@@ -235,7 +259,7 @@ def delete_bucket(self, bucket_name: str, *, delete_wiki=False):
235259

236260
def send_request(self, method: str, url: str, *args, **kwargs):
237261

238-
if self._token != _I_AM_A_PUBLIC_BUCKET:
262+
if self._token is not _I_AM_A_PUBLIC_BUCKET:
239263
hdr, info, sig = self._token.split(".")
240264
info_json = base64.b64decode(info + "==").decode("utf-8")
241265

@@ -246,7 +270,7 @@ def send_request(self, method: str, url: str, *args, **kwargs):
246270
if now_tc_seconds > exp_utc_seconds:
247271
raise TokenExpired
248272

249-
if self._token == _I_AM_A_PUBLIC_BUCKET:
273+
if self._token is _I_AM_A_PUBLIC_BUCKET:
250274
headers = kwargs.get("headers", {})
251275
headers["Authorization"] = None
252276
kwargs["headers"] = headers

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,7 @@ addopts = "-s -v --doctest-modules --ignore=build --ignore=dist --ignore=ebrains
4747

4848
[tool.black]
4949
line-length = 119
50+
51+
[tool.setuptools.packages.find]
52+
where = ["."]
53+
include = ["ebrains_drive*"]

0 commit comments

Comments
 (0)