Skip to content

Commit c04e853

Browse files
committed
Merge branch 'master' into 8065-instrument-asyncpg
2 parents fbc0aee + 7aec02e commit c04e853

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+486
-343
lines changed

api/specs/web-server/_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from models_library.rest_error import EnvelopedError, Log
1616
from pydantic import BaseModel, Field, confloat
1717
from simcore_service_webserver._meta import API_VTAG
18-
from simcore_service_webserver.login._controller.rest.auth import (
18+
from simcore_service_webserver.login._controller.rest.auth_schemas import (
1919
LoginBody,
2020
LoginNextPage,
2121
LoginTwoFactorAuthBody,
@@ -30,7 +30,7 @@
3030
PhoneConfirmationBody,
3131
ResetPasswordConfirmation,
3232
)
33-
from simcore_service_webserver.login._controller.rest.registration import (
33+
from simcore_service_webserver.login._controller.rest.registration_schemas import (
3434
InvitationCheck,
3535
InvitationInfo,
3636
RegisterBody,

packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
from aiohttp.test_utils import TestClient
77
from servicelib.aiohttp import status
8-
from simcore_service_webserver.login._constants import MSG_LOGGED_IN
98
from simcore_service_webserver.login._invitations_service import create_invitation_token
109
from simcore_service_webserver.login._login_repository_legacy import (
1110
get_plugin_storage,
1211
)
12+
from simcore_service_webserver.login.constants import MSG_LOGGED_IN
1313
from simcore_service_webserver.security import security_service
1414
from yarl import URL
1515

services/director-v2/src/simcore_service_director_v2/api/routes/computations.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ async def _check_pipeline_not_running_or_raise_409(
9797
computation: ComputationCreate,
9898
) -> None:
9999
with contextlib.suppress(ComputationalRunNotFoundError):
100-
last_run = await comp_runs_repo.get(
101-
user_id=computation.user_id, project_id=computation.project_id
100+
last_run = await comp_runs_repo.get_latest_run_by_project(
101+
project_id=computation.project_id
102102
)
103103
pipeline_state = last_run.result
104104

@@ -367,8 +367,8 @@ async def create_or_update_or_start_computation( # noqa: PLR0913 # pylint: disa
367367
last_run: CompRunsAtDB | None = None
368368
pipeline_state = RunningState.NOT_STARTED
369369
with contextlib.suppress(ComputationalRunNotFoundError):
370-
last_run = await comp_runs_repo.get(
371-
user_id=computation.user_id, project_id=computation.project_id
370+
last_run = await comp_runs_repo.get_latest_run_by_project(
371+
project_id=computation.project_id
372372
)
373373
pipeline_state = last_run.result
374374

@@ -467,7 +467,7 @@ async def get_computation(
467467
last_run: CompRunsAtDB | None = None
468468
pipeline_state = RunningState.NOT_STARTED
469469
with contextlib.suppress(ComputationalRunNotFoundError):
470-
last_run = await comp_runs_repo.get(user_id=user_id, project_id=project_id)
470+
last_run = await comp_runs_repo.get_latest_run_by_project(project_id=project_id)
471471
pipeline_state = last_run.result
472472

473473
_logger.debug(
@@ -542,8 +542,8 @@ async def stop_computation(
542542
last_run: CompRunsAtDB | None = None
543543
pipeline_state = RunningState.UNKNOWN
544544
with contextlib.suppress(ComputationalRunNotFoundError):
545-
last_run = await comp_runs_repo.get(
546-
user_id=computation_stop.user_id, project_id=project_id
545+
last_run = await comp_runs_repo.get_latest_run_by_project(
546+
project_id=project_id
547547
)
548548
pipeline_state = last_run.result
549549
if utils.is_pipeline_running(last_run.result):
@@ -601,8 +601,8 @@ async def delete_computation(
601601
# check if current state allow to stop the computation
602602
pipeline_state = RunningState.UNKNOWN
603603
with contextlib.suppress(ComputationalRunNotFoundError):
604-
last_run = await comp_runs_repo.get(
605-
user_id=computation_stop.user_id, project_id=project_id
604+
last_run = await comp_runs_repo.get_latest_run_by_project(
605+
project_id=project_id
606606
)
607607
pipeline_state = last_run.result
608608
if utils.is_pipeline_running(pipeline_state):
@@ -636,8 +636,8 @@ def return_last_value(retry_state: Any) -> Any:
636636
before_sleep=before_sleep_log(_logger, logging.INFO),
637637
)
638638
async def check_pipeline_stopped() -> bool:
639-
last_run = await comp_runs_repo.get(
640-
user_id=computation_stop.user_id, project_id=project_id
639+
last_run = await comp_runs_repo.get_latest_run_by_project(
640+
project_id=project_id
641641
)
642642
pipeline_state = last_run.result
643643
return utils.is_pipeline_stopped(pipeline_state)

services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_runs.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,22 @@ async def get(
145145
raise ComputationalRunNotFoundError
146146
return CompRunsAtDB.model_validate(row)
147147

148+
async def get_latest_run_by_project(
149+
self,
150+
project_id: ProjectID,
151+
) -> CompRunsAtDB:
152+
async with pass_or_acquire_connection(self.db_engine) as conn:
153+
result = await conn.execute(
154+
sa.select(comp_runs)
155+
.where(comp_runs.c.project_uuid == f"{project_id}")
156+
.order_by(desc(comp_runs.c.run_id))
157+
.limit(1)
158+
)
159+
row = result.one_or_none()
160+
if not row:
161+
raise ComputationalRunNotFoundError
162+
return CompRunsAtDB.model_validate(row)
163+
148164
async def list_(
149165
self,
150166
*,

services/director-v2/tests/unit/with_dbs/comp_scheduler/test_db_repositories_comp_runs.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,3 +1245,78 @@ async def test_list_group_by_collection_run_id_state_priority_precedence(
12451245
assert len(items) == 1
12461246
collection_item = items[0]
12471247
assert collection_item.state == RunningState.FAILED
1248+
1249+
1250+
async def test_get_latest_run_by_project(
1251+
sqlalchemy_async_engine: AsyncEngine,
1252+
run_metadata: RunMetadataDict,
1253+
faker: Faker,
1254+
publish_project: Callable[[], Awaitable[PublishedProject]],
1255+
create_registered_user: Callable[..., dict[str, Any]],
1256+
with_product: dict[str, Any],
1257+
):
1258+
"""Test that get() with user_id=None retrieves the latest run regardless of user"""
1259+
published_project = await publish_project()
1260+
1261+
# Create a second user
1262+
second_user = create_registered_user()
1263+
1264+
# Create comp runs for the original user
1265+
comp_run_user1_iter1 = await CompRunsRepository(sqlalchemy_async_engine).create(
1266+
user_id=published_project.user["id"],
1267+
project_id=published_project.project.uuid,
1268+
iteration=None,
1269+
metadata=run_metadata,
1270+
use_on_demand_clusters=faker.pybool(),
1271+
dag_adjacency_list=published_project.pipeline.dag_adjacency_list,
1272+
collection_run_id=CollectionRunID(faker.uuid4()),
1273+
)
1274+
1275+
# Create comp runs for the second user (this should increment iteration)
1276+
comp_run_user2_iter2 = await CompRunsRepository(sqlalchemy_async_engine).create(
1277+
user_id=second_user["id"],
1278+
project_id=published_project.project.uuid,
1279+
iteration=None,
1280+
metadata=run_metadata,
1281+
use_on_demand_clusters=faker.pybool(),
1282+
dag_adjacency_list=published_project.pipeline.dag_adjacency_list,
1283+
collection_run_id=CollectionRunID(faker.uuid4()),
1284+
)
1285+
1286+
# Create another run for the first user (should be iteration 3)
1287+
comp_run_user1_iter3 = await CompRunsRepository(sqlalchemy_async_engine).create(
1288+
user_id=published_project.user["id"],
1289+
project_id=published_project.project.uuid,
1290+
iteration=None,
1291+
metadata=run_metadata,
1292+
use_on_demand_clusters=faker.pybool(),
1293+
dag_adjacency_list=published_project.pipeline.dag_adjacency_list,
1294+
collection_run_id=CollectionRunID(faker.uuid4()),
1295+
)
1296+
1297+
# Verify iterations are correct
1298+
assert comp_run_user1_iter1.iteration == 1
1299+
assert comp_run_user2_iter2.iteration == 1
1300+
assert comp_run_user1_iter3.iteration == 2
1301+
1302+
# Test get with user_id=None should return the latest run (highest iteration)
1303+
latest_run = await CompRunsRepository(
1304+
sqlalchemy_async_engine
1305+
).get_latest_run_by_project(
1306+
project_id=published_project.project.uuid,
1307+
)
1308+
assert latest_run == comp_run_user1_iter3
1309+
assert latest_run.iteration == 2
1310+
1311+
# Test get with specific user_id still works
1312+
user1_latest = await CompRunsRepository(sqlalchemy_async_engine).get(
1313+
user_id=published_project.user["id"],
1314+
project_id=published_project.project.uuid,
1315+
)
1316+
assert user1_latest == comp_run_user1_iter3
1317+
1318+
user2_latest = await CompRunsRepository(sqlalchemy_async_engine).get(
1319+
user_id=second_user["id"],
1320+
project_id=published_project.project.uuid,
1321+
)
1322+
assert user2_latest == comp_run_user2_iter2
Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +0,0 @@
1-
""" webserver's login subsystem
2-
3-
4-
This sub-package is based on aiohttp-login https://github.com/imbolc/aiohttp-login
5-
"""

services/web/server/src/simcore_service_webserver/login/_auth_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from ..products.models import Product
1313
from ..security import security_service
1414
from . import _login_service
15-
from ._constants import MSG_UNKNOWN_EMAIL, MSG_WRONG_PASSWORD
1615
from ._login_repository_legacy import AsyncpgStorage, get_plugin_storage
16+
from .constants import MSG_UNKNOWN_EMAIL, MSG_WRONG_PASSWORD
1717

1818

1919
async def get_user_by_email(app: web.Application, *, email: str) -> dict[str, Any]:

services/web/server/src/simcore_service_webserver/login/_controller/rest/_rest_exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
to_exceptions_handlers_map,
99
)
1010
from ....users.exceptions import AlreadyPreRegisteredError
11-
from ..._constants import MSG_2FA_UNAVAILABLE
11+
from ...constants import MSG_2FA_UNAVAILABLE
1212
from ...errors import SendingVerificationEmailError, SendingVerificationSmsError
1313

1414
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {

services/web/server/src/simcore_service_webserver/login/_controller/rest/auth.py

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
from aiohttp import web
44
from aiohttp.web import RouteTableDef
55
from models_library.authentification import TwoFactorAuthentificationMethod
6-
from models_library.emails import LowerCaseEmailStr
7-
from pydantic import BaseModel, Field, PositiveInt, SecretStr, TypeAdapter
6+
from pydantic import TypeAdapter
87
from servicelib.aiohttp import status
98
from servicelib.aiohttp.requests_validation import parse_request_body_as
109
from servicelib.logging_utils import get_log_record_extra, log_context
@@ -21,9 +20,9 @@
2120
session_access_required,
2221
)
2322
from ....users import preferences_api as user_preferences_api
24-
from ....utils_aiohttp import NextPage
23+
from ....web_utils import envelope_response, flash_response
2524
from ... import _auth_service, _login_service, _security_service, _twofa_service
26-
from ..._constants import (
25+
from ...constants import (
2726
CODE_2FA_EMAIL_CODE_REQUIRED,
2827
CODE_2FA_SMS_CODE_REQUIRED,
2928
CODE_PHONE_NUMBER_REQUIRED,
@@ -37,31 +36,17 @@
3736
MSG_WRONG_2FA_CODE__EXPIRED,
3837
MSG_WRONG_2FA_CODE__INVALID,
3938
)
40-
from ..._models import InputSchema
4139
from ...decorators import login_required
4240
from ...settings import LoginSettingsForProduct, get_plugin_settings
4341
from ._rest_exceptions import handle_rest_requests_exceptions
42+
from .auth_schemas import LoginBody, LoginTwoFactorAuthBody, LogoutBody
4443

4544
log = logging.getLogger(__name__)
4645

4746

4847
routes = RouteTableDef()
4948

5049

51-
class LoginBody(InputSchema):
52-
email: LowerCaseEmailStr
53-
password: SecretStr
54-
55-
56-
class CodePageParams(BaseModel):
57-
message: str
58-
expiration_2fa: PositiveInt | None = None
59-
next_url: str | None = None
60-
61-
62-
class LoginNextPage(NextPage[CodePageParams]): ...
63-
64-
6550
@routes.post(f"/{API_VTAG}/auth/login", name="auth_login")
6651
@on_success_grant_session_access_to(
6752
name="auth_register_phone",
@@ -134,7 +119,7 @@ async def login(request: web.Request):
134119
user_2fa_authentification_method == TwoFactorAuthentificationMethod.SMS
135120
and not user["phone"]
136121
):
137-
return _login_service.envelope_response(
122+
return envelope_response(
138123
# LoginNextPage
139124
{
140125
"name": CODE_PHONE_NUMBER_REQUIRED,
@@ -169,7 +154,7 @@ async def login(request: web.Request):
169154
user_id=user["id"],
170155
)
171156

172-
return _login_service.envelope_response(
157+
return envelope_response(
173158
# LoginNextPage
174159
{
175160
"name": CODE_2FA_SMS_CODE_REQUIRED,
@@ -196,7 +181,7 @@ async def login(request: web.Request):
196181
product=product,
197182
user_id=user["id"],
198183
)
199-
return _login_service.envelope_response(
184+
return envelope_response(
200185
{
201186
"name": CODE_2FA_EMAIL_CODE_REQUIRED,
202187
"parameters": {
@@ -208,11 +193,6 @@ async def login(request: web.Request):
208193
)
209194

210195

211-
class LoginTwoFactorAuthBody(InputSchema):
212-
email: LowerCaseEmailStr
213-
code: SecretStr
214-
215-
216196
@routes.post(f"/{API_VTAG}/auth/validate-code-login", name="auth_login_2fa")
217197
@session_access_required(
218198
"auth_login_2fa",
@@ -259,12 +239,6 @@ async def login_2fa(request: web.Request):
259239
return await _security_service.login_granted_response(request, user=dict(user))
260240

261241

262-
class LogoutBody(InputSchema):
263-
client_session_id: str | None = Field(
264-
None, examples=["5ac57685-c40f-448f-8711-70be1936fd63"]
265-
)
266-
267-
268242
@routes.post(f"/{API_VTAG}/auth/logout", name="auth_logout")
269243
@login_required
270244
@handle_rest_requests_exceptions
@@ -282,7 +256,7 @@ async def logout(request: web.Request) -> web.Response:
282256
f"{logout_.client_session_id=}",
283257
extra=get_log_record_extra(user_id=user_id),
284258
):
285-
response = _login_service.flash_response(MSG_LOGGED_OUT, "INFO")
259+
response = flash_response(MSG_LOGGED_OUT, "INFO")
286260
await _login_service.notify_user_logout(
287261
request.app, user_id, logout_.client_session_id
288262
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from aiohttp.web import RouteTableDef
2+
from models_library.emails import LowerCaseEmailStr
3+
from pydantic import BaseModel, Field, PositiveInt, SecretStr
4+
5+
from ....utils_aiohttp import NextPage
6+
from ..._models import InputSchema
7+
8+
routes = RouteTableDef()
9+
10+
11+
class LoginBody(InputSchema):
12+
email: LowerCaseEmailStr
13+
password: SecretStr
14+
15+
16+
class CodePageParams(BaseModel):
17+
message: str
18+
expiration_2fa: PositiveInt | None = None
19+
next_url: str | None = None
20+
21+
22+
class LoginNextPage(NextPage[CodePageParams]): ...
23+
24+
25+
class LoginTwoFactorAuthBody(InputSchema):
26+
email: LowerCaseEmailStr
27+
code: SecretStr
28+
29+
30+
class LogoutBody(InputSchema):
31+
client_session_id: str | None = Field(
32+
None, examples=["5ac57685-c40f-448f-8711-70be1936fd63"]
33+
)

0 commit comments

Comments
 (0)