vantage6-algorithm-store 5.0.2rc2__tar.gz → 5.0.3__tar.gz
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.
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/PKG-INFO +4 -4
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/pyproject.toml +3 -3
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/base/unittest_base.py +25 -3
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/resources/test_algorithm.py +74 -2
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/__init__.py +17 -4
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/link_algorithms.py +1 -1
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/algorithm.py +3 -5
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/base.py +1 -1
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/policy.py +1 -3
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/review.py +1 -3
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/permission.py +1 -4
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/__init__.py +47 -9
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/algorithm.py +16 -4
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/policy.py +1 -1
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/review.py +7 -7
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/role.py +1 -2
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/rule.py +1 -1
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/schema/input_schema.py +24 -20
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/schema/output_schema.py +9 -9
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/user.py +1 -2
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/resource/version.py +1 -1
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/wsgi.py +1 -1
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/.gitignore +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/Makefile +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/README.md +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/dev_store.sh +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/server.sh +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/__init__.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/base/__init__.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/resources/__init__.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/resources/test_policy.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/resources/test_review.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/resources/test_role.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/resources/test_rule.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/resources/test_user.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/resources/test_version.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/test___init__.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/test_argument_schema.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/test_link_algorithms.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/_data/unittest_config.yaml +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/_data/unittest_fixtures.yaml +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/algorithm_create.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/db.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/default_roles.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/globals.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/init_db.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/__init__.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/allowed_argument_value.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/argument.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/common/enums.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/common/ui_visualization_schemas.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/database.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/function.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/permission.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/role.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/role_rule_association.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/rule.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/ui_visualization.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/model/user.py +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/static/robots.txt +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/templates/mail/algorithm_review_finalized.html +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/templates/mail/algorithm_review_finalized.txt +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/templates/mail/new_algorithm.html +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/templates/mail/new_algorithm.txt +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/templates/mail/new_review.html +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/templates/mail/new_review.txt +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/templates/mail/review_no_longer_required.html +0 -0
- {vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/templates/mail/review_no_longer_required.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: vantage6-algorithm-store
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.3
|
|
4
4
|
Summary: Vantage6 algorithm store
|
|
5
5
|
Author: Vantage6 Team
|
|
6
6
|
Maintainer-email: Frank Martin <f.martin@iknl.nl>, Bart van Beusekom <b.vanbeusekom@iknl.nl>
|
|
@@ -18,8 +18,8 @@ Requires-Dist: python-keycloak==5.5.0
|
|
|
18
18
|
Requires-Dist: requests==2.33.1
|
|
19
19
|
Requires-Dist: schema==0.7.5
|
|
20
20
|
Requires-Dist: sqlalchemy==2.0.37
|
|
21
|
-
Requires-Dist: vantage6-common==5.0.
|
|
22
|
-
Requires-Dist: vantage6==5.0.
|
|
21
|
+
Requires-Dist: vantage6-common==5.0.3
|
|
22
|
+
Requires-Dist: vantage6==5.0.3
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: black; extra == 'dev'
|
|
25
25
|
Requires-Dist: coverage==7.10.2; extra == 'dev'
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vantage6-algorithm-store"
|
|
7
|
-
version = "5.0.
|
|
7
|
+
version = "5.0.3"
|
|
8
8
|
description = "Vantage6 algorithm store"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -30,8 +30,8 @@ dependencies = [
|
|
|
30
30
|
"requests==2.33.1",
|
|
31
31
|
"schema==0.7.5",
|
|
32
32
|
"sqlalchemy==2.0.37",
|
|
33
|
-
"vantage6-common==5.0.
|
|
34
|
-
"vantage6==5.0.
|
|
33
|
+
"vantage6-common==5.0.3",
|
|
34
|
+
"vantage6==5.0.3",
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
[project.optional-dependencies]
|
|
@@ -2,7 +2,9 @@ import os
|
|
|
2
2
|
import unittest
|
|
3
3
|
from http import HTTPStatus
|
|
4
4
|
from unittest.mock import Mock, patch
|
|
5
|
+
from uuid import uuid1
|
|
5
6
|
|
|
7
|
+
import jwt
|
|
6
8
|
from cryptography.hazmat.backends import default_backend
|
|
7
9
|
from cryptography.hazmat.primitives import serialization
|
|
8
10
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
@@ -107,10 +109,10 @@ class TestResources(unittest.TestCase):
|
|
|
107
109
|
def register_user(
|
|
108
110
|
self,
|
|
109
111
|
username: str = "test_user",
|
|
110
|
-
user_roles: list[Role] = None,
|
|
111
|
-
user_rules: list[Rule] = None,
|
|
112
|
+
user_roles: list[Role] | None = None,
|
|
113
|
+
user_rules: list[Rule] | None = None,
|
|
112
114
|
organization_id: int = 1,
|
|
113
|
-
authenticate_mock: Mock = None,
|
|
115
|
+
authenticate_mock: Mock | None = None,
|
|
114
116
|
auth: bool = True,
|
|
115
117
|
) -> User:
|
|
116
118
|
user = User(username=username, organization_id=organization_id)
|
|
@@ -147,3 +149,23 @@ class TestResources(unittest.TestCase):
|
|
|
147
149
|
rules=role["rules"],
|
|
148
150
|
)
|
|
149
151
|
new_role.save()
|
|
152
|
+
|
|
153
|
+
def login_node(self) -> dict:
|
|
154
|
+
"""
|
|
155
|
+
Build headers with a real node JWT (mirrors vantage6-hq's `login_node` test
|
|
156
|
+
helper), for testing endpoints that recognize nodes via the
|
|
157
|
+
`vantage6_client_type` claim rather than a registered store `User`.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
dict
|
|
162
|
+
Authorization header with a valid node bearer token.
|
|
163
|
+
"""
|
|
164
|
+
mock_claims = {
|
|
165
|
+
"sub": str(uuid1()),
|
|
166
|
+
"vantage6_client_type": "node",
|
|
167
|
+
"exp": 9999999999,
|
|
168
|
+
"iat": 0,
|
|
169
|
+
}
|
|
170
|
+
mock_token = jwt.encode(mock_claims, MOCK_PRIVATE_KEY_PEM, algorithm="RS256")
|
|
171
|
+
return {"Authorization": f"Bearer {mock_token}"}
|
|
@@ -136,6 +136,63 @@ class TestAlgorithmResources(TestResources):
|
|
|
136
136
|
policy.delete()
|
|
137
137
|
user.delete()
|
|
138
138
|
|
|
139
|
+
def test_view_algorithm_decorator_node(self):
|
|
140
|
+
"""
|
|
141
|
+
Test that a node JWT is always allowed to list approved algorithms
|
|
142
|
+
(`allow_node=True` on the decorator), even under the store's most
|
|
143
|
+
restrictive algorithm_view policy - and that this does not extend to
|
|
144
|
+
non-approved algorithms.
|
|
145
|
+
"""
|
|
146
|
+
# even the most restrictive policy should not block a node
|
|
147
|
+
policy = Policy(
|
|
148
|
+
key=StorePolicies.ALGORITHM_VIEW.value,
|
|
149
|
+
value=AlgorithmViewPolicies.ONLY_WITH_EXPLICIT_PERMISSION.value,
|
|
150
|
+
)
|
|
151
|
+
policy.save()
|
|
152
|
+
|
|
153
|
+
node_headers = self.login_node()
|
|
154
|
+
|
|
155
|
+
# a node with a valid token can list approved algorithms
|
|
156
|
+
rv = self.app.get("/api/algorithm", headers=node_headers)
|
|
157
|
+
self.assertEqual(rv.status_code, HTTPStatus.OK)
|
|
158
|
+
|
|
159
|
+
# a node cannot use this to see non-approved algorithms
|
|
160
|
+
for arg in [
|
|
161
|
+
"awaiting_reviewer_assignment",
|
|
162
|
+
"under_review",
|
|
163
|
+
"in_review_process",
|
|
164
|
+
"invalidated",
|
|
165
|
+
]:
|
|
166
|
+
rv = self.app.get(f"/api/algorithm?{arg}=1", headers=node_headers)
|
|
167
|
+
self.assertEqual(rv.status_code, HTTPStatus.UNAUTHORIZED)
|
|
168
|
+
|
|
169
|
+
# this is not a blanket bypass: a request without a token is still governed
|
|
170
|
+
# by the normal policy
|
|
171
|
+
rv = self.app.get("/api/algorithm")
|
|
172
|
+
self.assertEqual(rv.status_code, HTTPStatus.UNAUTHORIZED)
|
|
173
|
+
|
|
174
|
+
# cleanup
|
|
175
|
+
policy.delete()
|
|
176
|
+
|
|
177
|
+
def test_view_algorithm_single_not_affected_by_node_bypass(self):
|
|
178
|
+
"""
|
|
179
|
+
Test that the node bypass on the algorithm list endpoint does not extend to
|
|
180
|
+
/api/algorithm/<id>, which was deliberately left on the un-modified
|
|
181
|
+
decorator.
|
|
182
|
+
"""
|
|
183
|
+
algorithm = Algorithm(
|
|
184
|
+
name="test_algorithm", status=AlgorithmStatus.APPROVED.value
|
|
185
|
+
)
|
|
186
|
+
algorithm.save()
|
|
187
|
+
|
|
188
|
+
# no policy is defined, so the default (AUTHENTICATED) applies - and a node
|
|
189
|
+
# token is not looked up as a store User, so it is rejected here
|
|
190
|
+
rv = self.app.get(f"/api/algorithm/{algorithm.id}", headers=self.login_node())
|
|
191
|
+
self.assertEqual(rv.status_code, HTTPStatus.UNAUTHORIZED)
|
|
192
|
+
|
|
193
|
+
# cleanup
|
|
194
|
+
algorithm.delete()
|
|
195
|
+
|
|
139
196
|
@patch("vantage6.algorithm.store.resource._authenticate")
|
|
140
197
|
def test_algorithm_view_multi(self, authenticate_mock):
|
|
141
198
|
"""Test GET /api/algorithm"""
|
|
@@ -544,9 +601,24 @@ class TestAlgorithmResources(TestResources):
|
|
|
544
601
|
self.assertEqual(response.status_code, HTTPStatus.OK)
|
|
545
602
|
self.assertEqual(response.json["description"], "new_description")
|
|
546
603
|
|
|
604
|
+
# check that another developer cannot update this algorithm - they are
|
|
605
|
+
# not its owner
|
|
606
|
+
self.register_user(
|
|
607
|
+
username="other_developer",
|
|
608
|
+
user_rules=[Rule.get_by_("algorithm", Operation.EDIT)],
|
|
609
|
+
authenticate_mock=authenticate_mock,
|
|
610
|
+
)
|
|
611
|
+
response = self.app.patch(
|
|
612
|
+
f"/api/algorithm/{algorithm.id}", json={"description": "other_description"}
|
|
613
|
+
)
|
|
614
|
+
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)
|
|
615
|
+
|
|
616
|
+
# switch back to the algorithm's own developer for the remaining checks
|
|
617
|
+
self._mock_auth(user, authenticate_mock, True)
|
|
618
|
+
|
|
547
619
|
# check that algorithm cannot be updated if it is approved
|
|
548
620
|
algorithm.status = AlgorithmStatus.APPROVED.value
|
|
549
|
-
algorithm.approved_at = datetime.datetime.now(datetime.
|
|
621
|
+
algorithm.approved_at = datetime.datetime.now(datetime.UTC)
|
|
550
622
|
algorithm.save()
|
|
551
623
|
response = self.app.patch(
|
|
552
624
|
f"/api/algorithm/{algorithm.id}", json={"description": "new_description"}
|
|
@@ -554,7 +626,7 @@ class TestAlgorithmResources(TestResources):
|
|
|
554
626
|
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)
|
|
555
627
|
|
|
556
628
|
# check that algorithm cannot be updated if it is invalidated
|
|
557
|
-
algorithm.invalidated_at = datetime.datetime.now(datetime.
|
|
629
|
+
algorithm.invalidated_at = datetime.datetime.now(datetime.UTC)
|
|
558
630
|
algorithm.save()
|
|
559
631
|
response = self.app.patch(
|
|
560
632
|
f"/api/algorithm/{algorithm.id}", json={"description": "new_description"}
|
|
@@ -26,6 +26,7 @@ from flask_principal import Identity, identity_changed
|
|
|
26
26
|
|
|
27
27
|
from vantage6.common import logger_name
|
|
28
28
|
from vantage6.common.enum import StorePolicies
|
|
29
|
+
from vantage6.common.exceptions import AuthenticationException
|
|
29
30
|
from vantage6.common.globals import DEFAULT_API_PATH
|
|
30
31
|
|
|
31
32
|
from vantage6.cli.context.algorithm_store import AlgorithmStoreContext
|
|
@@ -100,7 +101,7 @@ class AlgorithmStoreApp(Vantage6App):
|
|
|
100
101
|
"""Configure JWT authentication."""
|
|
101
102
|
|
|
102
103
|
@self.jwt.user_lookup_loader
|
|
103
|
-
def user_lookup_loader(jwt_payload: dict, jwt_headers: dict) -> db.User:
|
|
104
|
+
def user_lookup_loader(jwt_payload: dict, jwt_headers: dict) -> db.User | str:
|
|
104
105
|
"""
|
|
105
106
|
Load the user, node or container instance from the JWT payload.
|
|
106
107
|
|
|
@@ -113,15 +114,27 @@ class AlgorithmStoreApp(Vantage6App):
|
|
|
113
114
|
|
|
114
115
|
Returns
|
|
115
116
|
-------
|
|
116
|
-
db.User:
|
|
117
|
-
The user identity
|
|
117
|
+
db.User | str:
|
|
118
|
+
The user identity, or the keycloak id if it is a node that is
|
|
119
|
+
authenticating.
|
|
120
|
+
|
|
121
|
+
Raises
|
|
122
|
+
------
|
|
123
|
+
AuthenticationException
|
|
124
|
+
If no store `User` is registered for this (non-node) identity.
|
|
118
125
|
"""
|
|
119
126
|
identity = jwt_headers["sub"]
|
|
127
|
+
|
|
128
|
+
if jwt_headers.get("vantage6_client_type") == "node":
|
|
129
|
+
return identity
|
|
130
|
+
|
|
120
131
|
auth_identity = Identity(identity)
|
|
121
132
|
|
|
122
133
|
user = db.User.get_by_keycloak_id(identity)
|
|
123
134
|
if not user:
|
|
124
|
-
raise
|
|
135
|
+
raise AuthenticationException(
|
|
136
|
+
f"No user found for keycloak id {identity}"
|
|
137
|
+
)
|
|
125
138
|
|
|
126
139
|
# add role permissions
|
|
127
140
|
for role in user.roles:
|
|
@@ -213,7 +213,7 @@ def link_algorithms_from_config(config: dict) -> None:
|
|
|
213
213
|
developer = db.User.get_first_user()
|
|
214
214
|
if not developer:
|
|
215
215
|
log.warning(
|
|
216
|
-
"link_algorithms: No users found; skipping seeding",
|
|
216
|
+
"link_algorithms: No users found (root_username=%s); skipping seeding",
|
|
217
217
|
root_username,
|
|
218
218
|
)
|
|
219
219
|
return
|
|
@@ -60,9 +60,7 @@ class Algorithm(Base):
|
|
|
60
60
|
partitioning = Column(String)
|
|
61
61
|
vantage6_version = Column(String)
|
|
62
62
|
digest = Column(String)
|
|
63
|
-
submitted_at = Column(
|
|
64
|
-
DateTime, default=datetime.datetime.now(datetime.timezone.utc)
|
|
65
|
-
)
|
|
63
|
+
submitted_at = Column(DateTime, default=datetime.datetime.now(datetime.UTC))
|
|
66
64
|
approved_at = Column(DateTime)
|
|
67
65
|
invalidated_at = Column(DateTime)
|
|
68
66
|
developer_id = Column(Integer, ForeignKey("user.id"))
|
|
@@ -96,14 +94,14 @@ class Algorithm(Base):
|
|
|
96
94
|
bool
|
|
97
95
|
True if all reviews are approved, False otherwise
|
|
98
96
|
"""
|
|
99
|
-
return all(
|
|
97
|
+
return all(review.status == ReviewStatus.APPROVED for review in self.reviews)
|
|
100
98
|
|
|
101
99
|
def approve(self) -> None:
|
|
102
100
|
"""
|
|
103
101
|
Approve the algorithm, and invalidate all other algorithms with the same image.
|
|
104
102
|
"""
|
|
105
103
|
self.status = AlgorithmStatus.APPROVED.value
|
|
106
|
-
self.approved_at = datetime.datetime.now(datetime.
|
|
104
|
+
self.approved_at = datetime.datetime.now(datetime.UTC)
|
|
107
105
|
self.save()
|
|
108
106
|
|
|
109
107
|
for other_version in self.get_by_image(self.image):
|
|
@@ -32,9 +32,7 @@ class Review(Base):
|
|
|
32
32
|
algorithm_id = Column(Integer, ForeignKey("algorithm.id"))
|
|
33
33
|
reviewer_id = Column(Integer, ForeignKey("user.id"))
|
|
34
34
|
requested_by_id = Column(Integer, ForeignKey("user.id"))
|
|
35
|
-
requested_at = Column(
|
|
36
|
-
DateTime, default=datetime.datetime.now(datetime.timezone.utc)
|
|
37
|
-
)
|
|
35
|
+
requested_at = Column(DateTime, default=datetime.datetime.now(datetime.UTC))
|
|
38
36
|
submitted_at = Column(DateTime)
|
|
39
37
|
status = Column(Text, default=ReviewStatus.UNDER_REVIEW.value)
|
|
40
38
|
comment = Column(Text)
|
|
@@ -7,7 +7,7 @@ from http import HTTPStatus
|
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
9
|
from flask import g, request
|
|
10
|
-
from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request
|
|
10
|
+
from flask_jwt_extended import get_jwt, get_jwt_identity, verify_jwt_in_request
|
|
11
11
|
from flask_mail import Mail
|
|
12
12
|
from flask_restful import Api
|
|
13
13
|
|
|
@@ -105,6 +105,26 @@ def _authorize_user(
|
|
|
105
105
|
return None, None
|
|
106
106
|
|
|
107
107
|
|
|
108
|
+
def _is_node_request() -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Check whether the current request carries a valid node JWT.
|
|
111
|
+
|
|
112
|
+
Nodes are never registered as a store `User` - unlike `_authenticate`, this does
|
|
113
|
+
not do a database lookup. It only relies on the `vantage6_client_type` claim set
|
|
114
|
+
by the shared vantage6 Keycloak realm.
|
|
115
|
+
|
|
116
|
+
Note that this function does not raise if no token is present at all, so it is safe
|
|
117
|
+
to call unconditionally before any policy-based access checks.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
bool
|
|
122
|
+
Whether the request is authenticated as a node.
|
|
123
|
+
"""
|
|
124
|
+
verify_jwt_in_request(optional=True)
|
|
125
|
+
return get_jwt().get("vantage6_client_type") == "node"
|
|
126
|
+
|
|
127
|
+
|
|
108
128
|
def with_authentication() -> Callable:
|
|
109
129
|
"""
|
|
110
130
|
Decorator to verify that the user is authenticated from the linked Keycloak service.
|
|
@@ -169,10 +189,21 @@ def with_permission(resource: str, operation: Operation) -> Callable:
|
|
|
169
189
|
return protection_decorator
|
|
170
190
|
|
|
171
191
|
|
|
172
|
-
def with_permission_to_view_algorithms() -> Callable:
|
|
192
|
+
def with_permission_to_view_algorithms(allow_node: bool = False) -> Callable:
|
|
173
193
|
"""
|
|
174
194
|
Decorator to verify that the user has as a permission on a resource.
|
|
175
195
|
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
allow_node : bool, optional
|
|
199
|
+
If True, also allow requests authenticated as a node (see `_is_node_request`)
|
|
200
|
+
to view approved algorithms, regardless of the store's `algorithm_view` policy.
|
|
201
|
+
This lets nodes independently re-verify - for their own
|
|
202
|
+
`allowed_algorithm_stores` policy - that an image is a registered, approved
|
|
203
|
+
algorithm in this store, without requiring the node to be registered as a
|
|
204
|
+
store `User`. Node access is still restricted to approved algorithms only.
|
|
205
|
+
Default is False.
|
|
206
|
+
|
|
176
207
|
Returns
|
|
177
208
|
-------
|
|
178
209
|
Callable
|
|
@@ -183,13 +214,6 @@ def with_permission_to_view_algorithms() -> Callable:
|
|
|
183
214
|
def protection_decorator(fn):
|
|
184
215
|
@wraps(fn)
|
|
185
216
|
def decorator(self, *args, **kwargs):
|
|
186
|
-
policies = Policy.get_as_dict()
|
|
187
|
-
# check if everyone has permission to view algorithms
|
|
188
|
-
algorithm_view_policy = policies.get(
|
|
189
|
-
StorePolicies.ALGORITHM_VIEW.value,
|
|
190
|
-
DefaultStorePolicies.ALGORITHM_VIEW.value,
|
|
191
|
-
)
|
|
192
|
-
|
|
193
217
|
# check if user is trying to view algorithms that are not approved by review
|
|
194
218
|
# or have been invalidated - these algorithms always require authentication
|
|
195
219
|
# even when algorithms are open to all
|
|
@@ -201,6 +225,20 @@ def with_permission_to_view_algorithms() -> Callable:
|
|
|
201
225
|
or request_args.get("invalidated")
|
|
202
226
|
)
|
|
203
227
|
|
|
228
|
+
# nodes re-verifying an image against this store's catalog are always
|
|
229
|
+
# allowed to see approved algorithms, independent of the store's
|
|
230
|
+
# algorithm_view policy - they are never allowed to see non-approved ones
|
|
231
|
+
if allow_node and request_approved and _is_node_request():
|
|
232
|
+
g.node = True
|
|
233
|
+
return fn(self, *args, **kwargs)
|
|
234
|
+
|
|
235
|
+
policies = Policy.get_as_dict()
|
|
236
|
+
# check if everyone has permission to view algorithms
|
|
237
|
+
algorithm_view_policy = policies.get(
|
|
238
|
+
StorePolicies.ALGORITHM_VIEW.value,
|
|
239
|
+
DefaultStorePolicies.ALGORITHM_VIEW.value,
|
|
240
|
+
)
|
|
241
|
+
|
|
204
242
|
# if the algorithm is public and approved, allow access
|
|
205
243
|
if (
|
|
206
244
|
algorithm_view_policy == AlgorithmViewPolicies.PUBLIC
|
|
@@ -67,7 +67,7 @@ def setup(api: Api, api_base: str, services: dict) -> None:
|
|
|
67
67
|
services : dict
|
|
68
68
|
Dictionary with services required for the resource endpoints
|
|
69
69
|
"""
|
|
70
|
-
path = "/"
|
|
70
|
+
path = f"{api_base}/{module_name}"
|
|
71
71
|
log.info('Setting up "%s" and subdirectories', path)
|
|
72
72
|
|
|
73
73
|
api.add_resource(
|
|
@@ -148,7 +148,7 @@ class AlgorithmBaseResource(AlgorithmStoreResources):
|
|
|
148
148
|
class Algorithms(AlgorithmBaseResource):
|
|
149
149
|
"""Resource for /algorithm"""
|
|
150
150
|
|
|
151
|
-
@with_permission_to_view_algorithms()
|
|
151
|
+
@with_permission_to_view_algorithms(allow_node=True)
|
|
152
152
|
def get(self):
|
|
153
153
|
"""List algorithms
|
|
154
154
|
---
|
|
@@ -159,6 +159,12 @@ class Algorithms(AlgorithmBaseResource):
|
|
|
159
159
|
algorithms, set the 'awaiting_reviewer_assignment', 'under_review' or
|
|
160
160
|
'invalidated' parameter to True.
|
|
161
161
|
|
|
162
|
+
Nodes are always allowed to list approved algorithms (e.g. filtered by
|
|
163
|
+
'image'), regardless of this store's algorithm_view policy - this lets a node
|
|
164
|
+
independently verify that an image is a registered, approved algorithm for
|
|
165
|
+
its own allowed_algorithm_stores policy. Nodes cannot use the non-approved
|
|
166
|
+
filters.
|
|
167
|
+
|
|
162
168
|
parameters:
|
|
163
169
|
- in: query
|
|
164
170
|
name: name
|
|
@@ -863,6 +869,12 @@ class Algorithm(AlgorithmBaseResource):
|
|
|
863
869
|
if not algorithm:
|
|
864
870
|
return {"msg": "Algorithm not found"}, HTTPStatus.NOT_FOUND
|
|
865
871
|
|
|
872
|
+
# only the algorithm's own developer may edit it
|
|
873
|
+
if algorithm.developer_id != g.user.id:
|
|
874
|
+
return {
|
|
875
|
+
"msg": "You can only edit algorithms you submitted yourself."
|
|
876
|
+
}, HTTPStatus.FORBIDDEN
|
|
877
|
+
|
|
866
878
|
data = request.get_json(silent=True)
|
|
867
879
|
|
|
868
880
|
# validate the request body
|
|
@@ -887,7 +899,7 @@ class Algorithm(AlgorithmBaseResource):
|
|
|
887
899
|
"algorithm and go through the review process if you want to update it."
|
|
888
900
|
}, HTTPStatus.FORBIDDEN
|
|
889
901
|
elif algorithm.reviews and any(
|
|
890
|
-
|
|
902
|
+
r.is_review_finished() for r in algorithm.reviews
|
|
891
903
|
):
|
|
892
904
|
return {
|
|
893
905
|
"msg": "This algorithm has at least one submitted review, and can "
|
|
@@ -1043,7 +1055,7 @@ class AlgorithmInvalidate(AlgorithmStoreResources):
|
|
|
1043
1055
|
return {"msg": "Algorithm not found"}, HTTPStatus.NOT_FOUND
|
|
1044
1056
|
|
|
1045
1057
|
# invalidate the algorithm
|
|
1046
|
-
algorithm.invalidated_at = datetime.datetime.now(datetime.
|
|
1058
|
+
algorithm.invalidated_at = datetime.datetime.now(datetime.UTC)
|
|
1047
1059
|
algorithm.status = AlgorithmStatus.REMOVED.value
|
|
1048
1060
|
algorithm.save()
|
|
1049
1061
|
|
|
@@ -38,7 +38,7 @@ def setup(api: Api, api_base: str, services: dict) -> None:
|
|
|
38
38
|
services : dict
|
|
39
39
|
Dictionary with services required for the resource endpoints
|
|
40
40
|
"""
|
|
41
|
-
path = "/"
|
|
41
|
+
path = f"{api_base}/{module_name}"
|
|
42
42
|
log.info('Setting up "%s" and subdirectories', path)
|
|
43
43
|
|
|
44
44
|
api.add_resource(
|
|
@@ -52,7 +52,7 @@ def setup(api: Api, api_base: str, services: dict) -> None:
|
|
|
52
52
|
Dictionary with services required for the resource endpoints
|
|
53
53
|
"""
|
|
54
54
|
|
|
55
|
-
path = "/"
|
|
55
|
+
path = f"{api_base}/{module_name}"
|
|
56
56
|
log.info(f'Setting up "{path}" and subdirectories')
|
|
57
57
|
|
|
58
58
|
api.add_resource(
|
|
@@ -138,7 +138,7 @@ class ReviewBase(AlgorithmStoreResources):
|
|
|
138
138
|
True if the reviewers assignment is finished, False otherwise
|
|
139
139
|
"""
|
|
140
140
|
# get the number of unique organizations assigned to review the algorithm.
|
|
141
|
-
current_orgs = len(
|
|
141
|
+
current_orgs = len({(rev.reviewer.organization_id) for rev in reviews})
|
|
142
142
|
|
|
143
143
|
return (
|
|
144
144
|
current_orgs >= Policy.get_minimum_reviewing_orgs()
|
|
@@ -565,12 +565,12 @@ class Review(ReviewBase):
|
|
|
565
565
|
if not algorithm.is_review_finished():
|
|
566
566
|
# if number of reviews after deletion is less than the minium,
|
|
567
567
|
# new reviewers should be assigned
|
|
568
|
-
other_reviews = [r for r in algorithm.reviews if
|
|
568
|
+
other_reviews = [r for r in algorithm.reviews if r.id != review.id]
|
|
569
569
|
if not other_reviews or not self._is_reviewers_assignment_finished(
|
|
570
570
|
other_reviews
|
|
571
571
|
):
|
|
572
572
|
algorithm.status = AlgorithmStatus.AWAITING_REVIEWER_ASSIGNMENT.value
|
|
573
|
-
elif all(
|
|
573
|
+
elif all(r.status == ReviewStatus.APPROVED for r in other_reviews):
|
|
574
574
|
# if this was the last remaining review that needed to be approved, but
|
|
575
575
|
# it is now deleted, the algorithm should be approved
|
|
576
576
|
algorithm.status = AlgorithmStatus.APPROVED.value
|
|
@@ -747,7 +747,7 @@ class ReviewApprove(ReviewUpdateResources):
|
|
|
747
747
|
|
|
748
748
|
# update the review status to 'approved'
|
|
749
749
|
review.status = ReviewStatus.APPROVED.value
|
|
750
|
-
review.submitted_at = datetime.datetime.now(datetime.
|
|
750
|
+
review.submitted_at = datetime.datetime.now(datetime.UTC)
|
|
751
751
|
if comment := data.get("comment"):
|
|
752
752
|
review.comment = comment
|
|
753
753
|
review.save()
|
|
@@ -842,7 +842,7 @@ class ReviewReject(ReviewUpdateResources):
|
|
|
842
842
|
|
|
843
843
|
# update the review status to 'rejected'
|
|
844
844
|
review.status = ReviewStatus.REJECTED.value
|
|
845
|
-
review.submitted_at = datetime.datetime.now(datetime.
|
|
845
|
+
review.submitted_at = datetime.datetime.now(datetime.UTC)
|
|
846
846
|
if comment := data.get("comment"):
|
|
847
847
|
review.comment = comment
|
|
848
848
|
review.save()
|
|
@@ -850,7 +850,7 @@ class ReviewReject(ReviewUpdateResources):
|
|
|
850
850
|
# also update the algorithm status to 'rejected'
|
|
851
851
|
algorithm: db.Algorithm = review.algorithm
|
|
852
852
|
algorithm.status = AlgorithmStatus.REJECTED.value
|
|
853
|
-
algorithm.invalidated_at = datetime.datetime.now(datetime.
|
|
853
|
+
algorithm.invalidated_at = datetime.datetime.now(datetime.UTC)
|
|
854
854
|
algorithm.save()
|
|
855
855
|
|
|
856
856
|
# the other reviews of this algorithm are dropped as they are no longer required
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
1
|
import logging
|
|
3
2
|
from http import HTTPStatus
|
|
4
3
|
|
|
@@ -50,7 +49,7 @@ def setup(api: Api, api_base: str, services: dict) -> None:
|
|
|
50
49
|
services : dict
|
|
51
50
|
Dictionary with services required for the resource endpoints
|
|
52
51
|
"""
|
|
53
|
-
path = "/"
|
|
52
|
+
path = f"{api_base}/{module_name}"
|
|
54
53
|
log.info(f'Setting up "{path}" and subdirectories')
|
|
55
54
|
|
|
56
55
|
api.add_resource(
|
|
@@ -35,7 +35,7 @@ def setup(api: Api, api_base: str, services: dict) -> None:
|
|
|
35
35
|
services : dict
|
|
36
36
|
Dictionary with services required for the resource endpoints
|
|
37
37
|
"""
|
|
38
|
-
path = "/"
|
|
38
|
+
path = f"{api_base}/{module_name}"
|
|
39
39
|
log.info(f'Setting up "{path}" and subdirectories')
|
|
40
40
|
|
|
41
41
|
api.add_resource(
|
|
@@ -4,9 +4,8 @@ Marshmallow schemas for validating input data for the API.
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
|
|
7
|
-
import marshmallow.validate as validate
|
|
8
7
|
from jsonschema import validate as json_validate
|
|
9
|
-
from marshmallow import Schema, ValidationError, fields, validates_schema
|
|
8
|
+
from marshmallow import Schema, ValidationError, fields, validate, validates_schema
|
|
10
9
|
|
|
11
10
|
from vantage6.common.enum import (
|
|
12
11
|
AlgorithmArgumentType,
|
|
@@ -177,14 +176,16 @@ class FunctionInputSchema(_NameDescriptionSchema):
|
|
|
177
176
|
f"float, while the conditional argument '{conditional_on}' "
|
|
178
177
|
"requires a float"
|
|
179
178
|
) from exc
|
|
180
|
-
elif
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
elif (
|
|
180
|
+
conditional_type == AlgorithmArgumentType.BOOLEAN
|
|
181
|
+
and conditional_value.lower() not in ["true", "false", "1", "0"]
|
|
182
|
+
):
|
|
183
|
+
raise ValidationError(
|
|
184
|
+
f"Conditional value '{conditional_value}' is not a valid "
|
|
185
|
+
"boolean, while the conditional argument "
|
|
186
|
+
f"'{conditional_on}' requires a boolean. Please use 'true',"
|
|
187
|
+
" 'false', '1', or '0'"
|
|
188
|
+
)
|
|
188
189
|
|
|
189
190
|
@staticmethod
|
|
190
191
|
def _check_circular_conditions(arguments: list[dict]) -> None:
|
|
@@ -244,7 +245,7 @@ class _MixedBaseTypeField(fields.Field):
|
|
|
244
245
|
"""
|
|
245
246
|
|
|
246
247
|
def _deserialize(self, value, attr, data, **kwargs):
|
|
247
|
-
if isinstance(value, str
|
|
248
|
+
if isinstance(value, (str, int, float)):
|
|
248
249
|
return value
|
|
249
250
|
else:
|
|
250
251
|
raise ValidationError("Values should be str, int or float")
|
|
@@ -381,8 +382,8 @@ class ArgumentInputSchema(_NameDescriptionSchema):
|
|
|
381
382
|
try:
|
|
382
383
|
json_list = json.loads(default)
|
|
383
384
|
if not isinstance(json_list, list):
|
|
384
|
-
raise
|
|
385
|
-
except ValueError as exc:
|
|
385
|
+
raise TypeError(f"Not a list: {json_list}")
|
|
386
|
+
except (ValueError, TypeError) as exc:
|
|
386
387
|
raise ValidationError(
|
|
387
388
|
f"Default value '{default}' is not a valid JSON array, while "
|
|
388
389
|
f"the argument type {type_} requires a JSON array"
|
|
@@ -394,10 +395,10 @@ class ArgumentInputSchema(_NameDescriptionSchema):
|
|
|
394
395
|
try:
|
|
395
396
|
json_list = json.loads(default)
|
|
396
397
|
if not isinstance(json_list, list):
|
|
397
|
-
raise
|
|
398
|
+
raise TypeError(f"Not a list: {json_list}")
|
|
398
399
|
for value in json_list:
|
|
399
400
|
int(value)
|
|
400
|
-
except ValueError as exc:
|
|
401
|
+
except (ValueError, TypeError) as exc:
|
|
401
402
|
raise ValidationError(
|
|
402
403
|
f"Default value '{default}' is not a valid JSON array of "
|
|
403
404
|
f"integers, while the argument type {type_} requires a JSON "
|
|
@@ -407,10 +408,10 @@ class ArgumentInputSchema(_NameDescriptionSchema):
|
|
|
407
408
|
try:
|
|
408
409
|
json_list = json.loads(default)
|
|
409
410
|
if not isinstance(json_list, list):
|
|
410
|
-
raise
|
|
411
|
+
raise TypeError(f"Not a list: {json_list}")
|
|
411
412
|
for value in json_list:
|
|
412
413
|
float(value)
|
|
413
|
-
except ValueError as exc:
|
|
414
|
+
except (ValueError, TypeError) as exc:
|
|
414
415
|
raise ValidationError(
|
|
415
416
|
f"Default value '{default}' is not a valid JSON array of "
|
|
416
417
|
f"floats, while the argument type {type_} requires a JSON array"
|
|
@@ -436,9 +437,12 @@ class ArgumentInputSchema(_NameDescriptionSchema):
|
|
|
436
437
|
|
|
437
438
|
# if there are both allowed values and a default value, validate that the
|
|
438
439
|
# default value is one of the allowed values
|
|
439
|
-
if
|
|
440
|
-
|
|
441
|
-
|
|
440
|
+
if (
|
|
441
|
+
data.get("default_value")
|
|
442
|
+
and data.get("allowed_values")
|
|
443
|
+
and data.get("default_value") not in data.get("allowed_values")
|
|
444
|
+
):
|
|
445
|
+
raise ValidationError("Default value is not one of the allowed values")
|
|
442
446
|
|
|
443
447
|
|
|
444
448
|
class UIVisualizationInputSchema(_NameDescriptionSchema):
|
|
@@ -27,15 +27,15 @@ class HATEOASModelSchema(BaseHATEOASModelSchema):
|
|
|
27
27
|
def __init__(self, *args, **kwargs) -> None:
|
|
28
28
|
# set lambda functions to create links for one to one relationship
|
|
29
29
|
# TODO check if all below are used
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
self.algorithm = lambda obj: self.create_hateoas("algorithm", obj)
|
|
31
|
+
self.function = lambda obj: self.create_hateoas("function", obj)
|
|
32
|
+
self.database = lambda obj: self.create_hateoas("database", obj)
|
|
33
|
+
self.argument = lambda obj: self.create_hateoas("argument", obj)
|
|
34
|
+
self.rule = lambda obj: self.create_hateoas("rule", obj)
|
|
35
|
+
self.role = lambda obj: self.create_hateoas("role", obj)
|
|
36
|
+
self.user = lambda obj: self.create_hateoas("user", obj)
|
|
37
|
+
self.review = lambda obj: self.create_hateoas("review", obj)
|
|
38
|
+
self.hq = lambda obj: self.create_hateoas("hq", obj)
|
|
39
39
|
|
|
40
40
|
# call super class. Do this after setting the attributes above, because
|
|
41
41
|
# the super class initializer will call the attributes.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
1
|
import logging
|
|
3
2
|
from http import HTTPStatus
|
|
4
3
|
|
|
@@ -49,7 +48,7 @@ def setup(api: Api, api_base: str, services: dict) -> None:
|
|
|
49
48
|
Dictionary with services required for the resource endpoints
|
|
50
49
|
"""
|
|
51
50
|
|
|
52
|
-
path = "/"
|
|
51
|
+
path = f"{api_base}/{module_name}"
|
|
53
52
|
log.info(f'Setting up "{path}" and subdirectories')
|
|
54
53
|
|
|
55
54
|
api.add_resource(
|
|
@@ -31,7 +31,7 @@ def setup(api: Api, api_base: str, services: dict) -> None:
|
|
|
31
31
|
services : dict
|
|
32
32
|
Dictionary with services required for the resource endpoints
|
|
33
33
|
"""
|
|
34
|
-
path = "/"
|
|
34
|
+
path = f"{api_base}/{module_name}"
|
|
35
35
|
log.info('Setting up "%s" and subdirectories', path)
|
|
36
36
|
|
|
37
37
|
api.add_resource(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/__init__.py
RENAMED
|
File without changes
|
{vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/base/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/tests_store/test___init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vantage6_algorithm_store-5.0.2rc2 → vantage6_algorithm_store-5.0.3}/vantage6/algorithm/store/db.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|