Skip to content

Commit 19b692f

Browse files
johnugeorgearistizabal95hasan7n
authored
Add versioning support for MedPerf APIs (#354)
* Add versioning support for MedPerf APIs * Fix flake8 issues * Fix CI tests * Support versioning from the CLI * Ignore lint error for __init__ * Adapt tests to changes * Fix linter error * Moving API version to common settings * Fix flake8 * Move full url logic to utils function * Pass base url to full_url * Move full url logic to comms * Use API version from settings * Remove deprecated import * Remove deprecated import * make parse_url a class method * Use correct decorator order * use version 0.1.0 * fix version in integration tests --------- Co-authored-by: Alejandro Aristizábal <alejandro.aristizabal24@gmail.com> Co-authored-by: hasan7n <hasankassim7@hotmail.com>
1 parent 96377ee commit 19b692f

File tree

32 files changed

+260
-156
lines changed

32 files changed

+260
-156
lines changed

cli/cli_tests.sh

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ CERT_FILE="${AUTH_CERT:-$(realpath server/cert.crt)}"
1818
MEDPERF_STORAGE=~/.medperf
1919
MEDPERF_SUBSTORAGE="$MEDPERF_STORAGE/$(echo $SERVER_URL | cut -d '/' -f 3 | sed -e 's/[.:]/_/g')"
2020
MEDPERF_LOG_STORAGE="$MEDPERF_SUBSTORAGE/logs/medperf.log"
21+
VERSION_PREFIX="/api/v0"
2122

2223
echo "Server URL: $SERVER_URL"
2324
echo "Storage location: $MEDPERF_SUBSTORAGE"
@@ -83,15 +84,15 @@ METRIC_PARAMS="$ASSETS_URL/metrics/mlcube/workspace/parameters.yaml"
8384
METRICS_SING_IMAGE="$ASSETS_URL/metrics/mlcube/workspace/.image/image.tar.gz"
8485

8586
# admin token
86-
ADMIN_TOKEN=$(curl -sk -X POST https://127.0.0.1:8000/auth-token/ -d '{"username": "admin", "password": "admin"}' -H 'Content-Type: application/json' | jq -r '.token')
87+
ADMIN_TOKEN=$(curl -sk -X POST $SERVER_URL$VERSION_PREFIX/auth-token/ -d '{"username": "admin", "password": "admin"}' -H 'Content-Type: application/json' | jq -r '.token')
8788

8889
# create users
8990
MODELOWNER="mockmodelowner"
9091
DATAOWNER="mockdataowner"
9192
BENCHMARKOWNER="mockbenchmarkowner"
92-
curl -sk -X POST https://127.0.0.1:8000/users/ -d '{"first_name": "model", "last_name": "owner", "username": "'"$MODELOWNER"'", "password": "test", "email": "model@owner.com"}' -H 'Content-Type: application/json' -H "Authorization: Token $ADMIN_TOKEN"
93-
curl -sk -X POST https://127.0.0.1:8000/users/ -d '{"first_name": "bmk", "last_name": "owner", "username": "'"$BENCHMARKOWNER"'", "password": "test", "email": "bmk@owner.com"}' -H 'Content-Type: application/json' -H "Authorization: Token $ADMIN_TOKEN"
94-
curl -sk -X POST https://127.0.0.1:8000/users/ -d '{"first_name": "data", "last_name": "owner", "username": "'"$DATAOWNER"'", "password": "test", "email": "data@owner.com"}' -H 'Content-Type: application/json' -H "Authorization: Token $ADMIN_TOKEN"
93+
curl -sk -X POST $SERVER_URL$VERSION_PREFIX/users/ -d '{"first_name": "model", "last_name": "owner", "username": "'"$MODELOWNER"'", "password": "test", "email": "model@owner.com"}' -H 'Content-Type: application/json' -H "Authorization: Token $ADMIN_TOKEN"
94+
curl -sk -X POST $SERVER_URL$VERSION_PREFIX/users/ -d '{"first_name": "bmk", "last_name": "owner", "username": "'"$BENCHMARKOWNER"'", "password": "test", "email": "bmk@owner.com"}' -H 'Content-Type: application/json' -H "Authorization: Token $ADMIN_TOKEN"
95+
curl -sk -X POST $SERVER_URL$VERSION_PREFIX/users/ -d '{"first_name": "data", "last_name": "owner", "username": "'"$DATAOWNER"'", "password": "test", "email": "data@owner.com"}' -H 'Content-Type: application/json' -H "Authorization: Token $ADMIN_TOKEN"
9596

9697
##########################################################
9798
################### Start Testing ########################
@@ -184,7 +185,7 @@ medperf benchmark submit --name bmk --description bmk --demo-url $DEMO_URL --dat
184185
checkFailed "Benchmark submission failed"
185186
BMK_UID=$(medperf benchmark ls | tail -n 1 | tr -s ' ' | cut -d ' ' -f 2)
186187

187-
curl -sk -X PUT https://127.0.0.1:8000/benchmarks/$BMK_UID/ -d '{"approval_status": "APPROVED"}' -H 'Content-Type: application/json' -H "Authorization: Token $ADMIN_TOKEN"
188+
curl -sk -X PUT $SERVER_URL$VERSION_PREFIX/benchmarks/$BMK_UID/ -d '{"approval_status": "APPROVED"}' -H 'Content-Type: application/json' -H "Authorization: Token $ADMIN_TOKEN"
188189
checkFailed "Benchmark approval failed"
189190
##########################################################
190191

cli/medperf/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from ._version import __version__ # noqa

cli/medperf/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import logging.handlers
44
from os.path import expanduser, abspath
55

6+
from medperf import __version__
67
import medperf.config as config
78
from medperf.ui.factory import UIFactory
89
from medperf.decorators import clean_except, configurable
@@ -173,11 +174,12 @@ def main(ctx: typer.Context):
173174
log = config.loglevel.upper()
174175
log_lvl = getattr(logging, log)
175176
setup_logging(log_lvl)
177+
logging.info(f"Running MedPerf v{__version__} on {log_lvl} logging level")
176178

177179
config.ui = UIFactory.create_ui(config.ui)
178180
config.comms = CommsFactory.create_comms(config.comms, config.server)
179181

180-
config.ui.print(f"MedPerf {config.version}")
182+
config.ui.print(f"MedPerf {__version__}")
181183

182184

183185
if __name__ == "__main__":

cli/medperf/_version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1.0"

cli/medperf/comms/interface.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ def __init__(self, source: str, ui: UI, token: str = None):
1515
token (str, Optional): authentication token to be used throughout communication. Defaults to None.
1616
"""
1717

18+
@classmethod
19+
@abstractmethod
20+
def parse_url(self, url: str) -> str:
21+
"""Parse the source URL so that it can be used by the comms implementation.
22+
It should handle protocols and versioning to be able to communicate with the API.
23+
24+
Args:
25+
url (str): base URL
26+
27+
Returns:
28+
str: parsed URL with protocol and version
29+
"""
30+
1831
@abstractmethod
1932
def login(self, ui: UI):
2033
"""Authenticate the comms instance for further interactions

cli/medperf/comms/rest.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,31 @@ def log_response_error(res, warn=False):
4040

4141
class REST(Comms):
4242
def __init__(self, source: str, token=None):
43-
self.server_url = self.__parse_url(source)
43+
self.server_url = self.parse_url(source)
4444
self.token = token
4545
self.cert = config.certificate
4646
if self.cert is None:
4747
# No certificate provided, default to normal verification
4848
self.cert = True
4949

50-
def __parse_url(self, url):
50+
@classmethod
51+
def parse_url(cls, url: str) -> str:
52+
"""Parse the source URL so that it can be used by the comms implementation.
53+
It should handle protocols and versioning to be able to communicate with the API.
54+
55+
Args:
56+
url (str): base URL
57+
58+
Returns:
59+
str: parsed URL with protocol and version
60+
"""
5161
url_sections = url.split("://")
62+
api_path = f"/api/v{config.major_version}"
5263
# Remove protocol if passed
5364
if len(url_sections) > 1:
5465
url = "".join(url_sections[1:])
5566

56-
return f"https://{url}"
67+
return f"https://{url}{api_path}"
5768

5869
def login(self, user: str, pwd: str):
5970
"""Authenticates the user with the server. Required for most endpoints

cli/medperf/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from ._version import __version__
12
from os.path import expanduser, abspath
23

3-
version = "0.0.0"
4+
major_version, minor_version, patch_version = __version__.split(".")
5+
46
server = "https://api.medperf.org"
57
certificate = None
68

cli/medperf/setup.py

Lines changed: 0 additions & 24 deletions
This file was deleted.

cli/medperf/tests/comms/test_rest.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from medperf.tests.mocks import MockResponse
1111

1212
url = "https://mock.url"
13+
full_url = REST.parse_url(url)
1314
patch_server = "medperf.comms.rest.{}"
1415

1516

@@ -22,24 +23,24 @@ def server(mocker, ui):
2223
@pytest.mark.parametrize(
2324
"method_params",
2425
[
25-
("get_benchmark", "get", 200, [1], {}, (f"{url}/benchmarks/1",), {}),
26+
("get_benchmark", "get", 200, [1], {}, (f"{full_url}/benchmarks/1",), {}),
2627
(
2728
"get_benchmark_models",
2829
"get_list",
2930
200,
3031
[1],
3132
[],
32-
(f"{url}/benchmarks/1/models",),
33+
(f"{full_url}/benchmarks/1/models",),
3334
{},
3435
),
35-
("get_cube_metadata", "get", 200, [1], {}, (f"{url}/mlcubes/1/",), {}),
36+
("get_cube_metadata", "get", 200, [1], {}, (f"{full_url}/mlcubes/1/",), {}),
3637
(
3738
"upload_dataset",
3839
"post",
3940
201,
4041
[{}],
4142
{"id": 1},
42-
(f"{url}/datasets/",),
43+
(f"{full_url}/datasets/",),
4344
{"json": {}},
4445
),
4546
(
@@ -48,7 +49,7 @@ def server(mocker, ui):
4849
201,
4950
[{}],
5051
{"id": 1},
51-
(f"{url}/results/",),
52+
(f"{full_url}/results/",),
5253
{"json": {}},
5354
),
5455
(
@@ -57,7 +58,7 @@ def server(mocker, ui):
5758
201,
5859
[1, 1],
5960
{},
60-
(f"{url}/datasets/benchmarks/",),
61+
(f"{full_url}/datasets/benchmarks/",),
6162
{
6263
"json": {
6364
"benchmark": 1,
@@ -71,18 +72,18 @@ def server(mocker, ui):
7172
"_REST__set_approval_status",
7273
"put",
7374
200,
74-
[f"{url}/mlcubes/1/benchmarks/1", Status.APPROVED.value],
75+
[f"{full_url}/mlcubes/1/benchmarks/1", Status.APPROVED.value],
7576
{},
76-
(f"{url}/mlcubes/1/benchmarks/1",),
77+
(f"{full_url}/mlcubes/1/benchmarks/1",),
7778
{"json": {"approval_status": Status.APPROVED.value}},
7879
),
7980
(
8081
"_REST__set_approval_status",
8182
"put",
8283
200,
83-
[f"{url}/mlcubes/1/benchmarks/1", Status.REJECTED.value],
84+
[f"{full_url}/mlcubes/1/benchmarks/1", Status.REJECTED.value],
8485
{},
85-
(f"{url}/mlcubes/1/benchmarks/1",),
86+
(f"{full_url}/mlcubes/1/benchmarks/1",),
8687
{"json": {"approval_status": Status.REJECTED.value}},
8788
),
8889
(
@@ -91,7 +92,7 @@ def server(mocker, ui):
9192
200,
9293
["pwd"],
9394
{},
94-
(f"{url}/me/password/",),
95+
(f"{full_url}/me/password/",),
9596
{"json": {"password": "pwd"}},
9697
),
9798
],
@@ -149,7 +150,7 @@ def test_login_with_user_and_pwd(mocker, server, ui, uname, pwd):
149150
res = MockResponse({"token": ""}, 200)
150151
spy = mocker.patch("requests.post", return_value=res)
151152
exp_body = {"username": uname, "password": pwd}
152-
exp_path = f"{url}/auth-token/"
153+
exp_path = f"{full_url}/auth-token/"
153154
cert_verify = config.certificate or True
154155

155156
# Act
@@ -249,12 +250,12 @@ def test__req_sanitizes_json(mocker, server):
249250
def test__get_list_uses_default_page_size(mocker, server):
250251
# Arrange
251252
exp_page_size = config.default_page_size
252-
exp_url = f"{url}?limit={exp_page_size}&offset=0"
253+
exp_url = f"{full_url}?limit={exp_page_size}&offset=0"
253254
ret_body = MockResponse({"count": 1, "next": None, "results": []}, 200)
254255
spy = mocker.patch.object(server, "_REST__auth_get", return_value=ret_body)
255256

256257
# Act
257-
server._REST__get_list(url)
258+
server._REST__get_list(full_url)
258259

259260
# Assert
260261
spy.assert_called_once_with(exp_url)
@@ -336,7 +337,7 @@ def test_get_benchmarks_calls_benchmarks_path(mocker, server, body):
336337
bmarks = server.get_benchmarks()
337338

338339
# Assert
339-
spy.assert_called_once_with(f"{url}/benchmarks/")
340+
spy.assert_called_once_with(f"{full_url}/benchmarks/")
340341
assert bmarks == [body]
341342

342343

@@ -367,7 +368,7 @@ def test_get_user_benchmarks_calls_auth_get_for_expected_path(mocker, server):
367368
server.get_user_benchmarks()
368369

369370
# Assert
370-
spy.assert_called_once_with(f"{url}/me/benchmarks/")
371+
spy.assert_called_once_with(f"{full_url}/me/benchmarks/")
371372

372373

373374
def test_get_user_benchmarks_returns_benchmarks(mocker, server):
@@ -394,7 +395,7 @@ def test_get_mlcubes_calls_mlcubes_path(mocker, server, body):
394395
cubes = server.get_cubes()
395396

396397
# Assert
397-
spy.assert_called_once_with(f"{url}/mlcubes/")
398+
spy.assert_called_once_with(f"{full_url}/mlcubes/")
398399
assert cubes == [body]
399400

400401

@@ -484,7 +485,7 @@ def test_get_user_cubes_calls_auth_get_for_expected_path(mocker, server):
484485
server.get_user_cubes()
485486

486487
# Assert
487-
spy.assert_called_once_with(f"{url}/me/mlcubes/")
488+
spy.assert_called_once_with(f"{full_url}/me/mlcubes/")
488489

489490

490491
def test_get_cube_file_calls_download_direct_link_method(mocker, server):
@@ -512,7 +513,7 @@ def test_get_datasets_calls_datasets_path(mocker, server, body):
512513
dsets = server.get_datasets()
513514

514515
# Assert
515-
spy.assert_called_once_with(f"{url}/datasets/")
516+
spy.assert_called_once_with(f"{full_url}/datasets/")
516517
assert dsets == [body]
517518

518519

@@ -527,7 +528,7 @@ def test_get_dataset_calls_specific_dataset_path(mocker, server, uid, body):
527528
dset = server.get_dataset(uid)
528529

529530
# Assert
530-
spy.assert_called_once_with(f"{url}/datasets/{uid}/")
531+
spy.assert_called_once_with(f"{full_url}/datasets/{uid}/")
531532
assert dset == body
532533

533534

@@ -543,7 +544,7 @@ def test_get_user_datasets_calls_auth_get_for_expected_path(mocker, server):
543544
server.get_user_datasets()
544545

545546
# Assert
546-
spy.assert_called_once_with(f"{url}/me/datasets/")
547+
spy.assert_called_once_with(f"{full_url}/me/datasets/")
547548

548549

549550
@pytest.mark.parametrize("body", [{"mlcube": 1}, {}, {"test": "test"}])
@@ -616,7 +617,7 @@ def test_set_dataset_association_approval_sets_approval(
616617
spy = mocker.patch(
617618
patch_server.format("REST._REST__set_approval_status"), return_value=res
618619
)
619-
exp_url = f"{url}/datasets/{dataset_uid}/benchmarks/{benchmark_uid}/"
620+
exp_url = f"{full_url}/datasets/{dataset_uid}/benchmarks/{benchmark_uid}/"
620621

621622
# Act
622623
server.set_dataset_association_approval(benchmark_uid, dataset_uid, status)
@@ -636,7 +637,7 @@ def test_set_mlcube_association_approval_sets_approval(
636637
spy = mocker.patch(
637638
patch_server.format("REST._REST__set_approval_status"), return_value=res
638639
)
639-
exp_url = f"{url}/mlcubes/{mlcube_uid}/benchmarks/{benchmark_uid}/"
640+
exp_url = f"{full_url}/mlcubes/{mlcube_uid}/benchmarks/{benchmark_uid}/"
640641

641642
# Act
642643
server.set_mlcube_association_approval(benchmark_uid, mlcube_uid, status)
@@ -648,7 +649,7 @@ def test_set_mlcube_association_approval_sets_approval(
648649
def test_get_datasets_associations_gets_associations(mocker, server):
649650
# Arrange
650651
spy = mocker.patch(patch_server.format("REST._REST__get_list"), return_value=[])
651-
exp_path = f"{url}/me/datasets/associations/"
652+
exp_path = f"{full_url}/me/datasets/associations/"
652653

653654
# Act
654655
server.get_datasets_associations()
@@ -660,7 +661,7 @@ def test_get_datasets_associations_gets_associations(mocker, server):
660661
def test_get_cubes_associations_gets_associations(mocker, server):
661662
# Arrange
662663
spy = mocker.patch(patch_server.format("REST._REST__get_list"), return_value=[])
663-
exp_path = f"{url}/me/mlcubes/associations/"
664+
exp_path = f"{full_url}/me/mlcubes/associations/"
664665

665666
# Act
666667
server.get_cubes_associations()
@@ -675,7 +676,7 @@ def test_get_result_calls_specified_path(mocker, server, uid, body):
675676
# Arrange
676677
res = MockResponse(body, 200)
677678
spy = mocker.patch(patch_server.format("REST._REST__auth_get"), return_value=res)
678-
exp_path = f"{url}/results/{uid}/"
679+
exp_path = f"{full_url}/results/{uid}/"
679680

680681
# Act
681682
result = server.get_result(uid)
@@ -707,7 +708,7 @@ def test_set_mlcube_association_priority_sets_priority(
707708
# Arrange
708709
res = MockResponse({}, 200)
709710
spy = mocker.patch(patch_server.format("REST._REST__auth_put"), return_value=res)
710-
exp_url = f"{url}/mlcubes/{mlcube_uid}/benchmarks/{benchmark_uid}/"
711+
exp_url = f"{full_url}/mlcubes/{mlcube_uid}/benchmarks/{benchmark_uid}/"
711712

712713
# Act
713714
server.set_mlcube_association_priority(benchmark_uid, mlcube_uid, priority)

cli/medperf/utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def setup_logging(log_lvl):
4141
requests_logger = logging.getLogger("requests")
4242
requests_logger.addHandler(handler)
4343
requests_logger.setLevel(log_lvl)
44-
logging.info(f"Running MedPerf v{config.version} on {log_lvl} logging level")
4544

4645

4746
def delete_credentials():

0 commit comments

Comments
 (0)