22
33import logging
44import urllib .parse
5- from collections .abc import Mapping
65from dataclasses import dataclass
76from functools import partial
8- from typing import Any
7+ from typing import Any , Self
98from uuid import UUID
109
1110import httpx
3635)
3736from models_library .api_schemas_webserver .wallets import WalletGet
3837from models_library .generics import Envelope
38+ from models_library .products import ProductName
3939from models_library .projects import ProjectID
4040from models_library .projects_nodes_io import NodeID
4141from models_library .rest_pagination import Page , PageLimitInt , PageOffsetInt
42+ from models_library .users import UserID
4243from models_library .utils .fastapi_encoders import jsonable_encoder
4344from pydantic import PositiveInt
4445from servicelib .common_headers import (
4546 X_SIMCORE_PARENT_NODE_ID ,
4647 X_SIMCORE_PARENT_PROJECT_UUID ,
4748)
4849from servicelib .long_running_tasks .models import TaskStatus
50+ from servicelib .rest_constants import X_PRODUCT_NAME_HEADER
4951from settings_library .tracing import TracingSettings
5052from tenacity import TryAgain , retry_if_exception_type
5153from tenacity .asyncio import AsyncRetrying
@@ -128,7 +130,7 @@ class LongRunningTasksClient(BaseServiceClientApi):
128130 "Client for requesting status and results of long running tasks"
129131
130132
131- @dataclass
133+ @dataclass ( frozen = True )
132134class AuthSession :
133135 """
134136 - wrapper around thin-client to simplify webserver's API
@@ -140,8 +142,12 @@ class AuthSession:
140142 SEE services/api-server/src/simcore_service_api_server/api/dependencies/webserver.py
141143 """
142144
145+ _product_name : ProductName
146+ _user_id : UserID
147+
143148 _api : WebserverApi
144149 _long_running_task_client : LongRunningTasksClient
150+
145151 vtag : str
146152 session_cookies : dict | None = None
147153
@@ -150,26 +156,45 @@ def create(
150156 cls ,
151157 app : FastAPI ,
152158 session_cookies : dict ,
153- product_extra_headers : Mapping [str , str ],
154- ) -> "AuthSession" :
159+ user_id : UserID ,
160+ product_name : ProductName ,
161+ ) -> Self :
162+
163+ # WARNING: this client lifespan is tied to the app
155164 api = WebserverApi .get_instance (app )
156165 assert api # nosec
157166 assert isinstance (api , WebserverApi ) # nosec
158167
159- api . client . headers = product_extra_headers # type: ignore[assignment]
168+ # WARNING: this client lifespan is tied to the app
160169 long_running_tasks_client = LongRunningTasksClient .get_instance (app = app )
161-
162170 assert long_running_tasks_client # nosec
163171 assert isinstance (long_running_tasks_client , LongRunningTasksClient ) # nosec
164172
165- long_running_tasks_client .client .headers = product_extra_headers # type: ignore[assignment]
166173 return cls (
174+ _product_name = product_name ,
175+ _user_id = user_id ,
167176 _api = api ,
168177 _long_running_task_client = long_running_tasks_client ,
169178 vtag = app .state .settings .API_SERVER_WEBSERVER .WEBSERVER_VTAG ,
170179 session_cookies = session_cookies ,
171180 )
172181
182+ def _get_session_headers (
183+ self ,
184+ * ,
185+ parent_project_uuid : ProjectID | None = None ,
186+ parent_node_id : NodeID | None = None ,
187+ ) -> dict [str , str ]:
188+ headers = {X_PRODUCT_NAME_HEADER : self ._product_name }
189+
190+ if parent_project_uuid is not None :
191+ headers [X_SIMCORE_PARENT_PROJECT_UUID ] = str (parent_project_uuid )
192+
193+ if parent_node_id is not None :
194+ headers [X_SIMCORE_PARENT_NODE_ID ] = str (parent_node_id )
195+
196+ return headers
197+
173198 # OPERATIONS
174199
175200 @property
@@ -212,6 +237,7 @@ async def _page_projects(
212237 ** optional ,
213238 },
214239 cookies = self .session_cookies ,
240+ headers = self ._get_session_headers (),
215241 )
216242 resp .raise_for_status ()
217243
@@ -230,7 +256,9 @@ async def _wait_for_long_running_task_results(self, lrt_response: httpx.Response
230256 ):
231257 with attempt :
232258 get_response = await self .long_running_task_client .get (
233- url = status_url , cookies = self .session_cookies
259+ url = status_url ,
260+ cookies = self .session_cookies ,
261+ headers = self ._get_session_headers (),
234262 )
235263 get_response .raise_for_status (
236264 # NOTE: stops retrying if the response in not 2xx
@@ -244,7 +272,9 @@ async def _wait_for_long_running_task_results(self, lrt_response: httpx.Response
244272 raise TryAgain (msg )
245273
246274 result_response = await self .long_running_task_client .get (
247- f"{ result_url } " , cookies = self .session_cookies
275+ f"{ result_url } " ,
276+ cookies = self .session_cookies ,
277+ headers = self ._get_session_headers (),
248278 )
249279 result_response .raise_for_status ()
250280 return Envelope .model_validate_json (result_response .text ).data
@@ -253,7 +283,11 @@ async def _wait_for_long_running_task_results(self, lrt_response: httpx.Response
253283
254284 @_exception_mapper (http_status_map = _PROFILE_STATUS_MAP )
255285 async def get_me (self ) -> Profile :
256- response = await self .client .get ("/me" , cookies = self .session_cookies )
286+ response = await self .client .get (
287+ "/me" ,
288+ cookies = self .session_cookies ,
289+ headers = self ._get_session_headers (),
290+ )
257291 response .raise_for_status ()
258292
259293 got : WebProfileGet | None = (
@@ -284,6 +318,7 @@ async def update_me(self, *, profile_update: ProfileUpdate) -> Profile:
284318 "/me" ,
285319 json = update .model_dump (exclude_unset = True ),
286320 cookies = self .session_cookies ,
321+ headers = self ._get_session_headers (),
287322 )
288323 response .raise_for_status ()
289324 profile : Profile = await self .get_me ()
@@ -302,17 +337,15 @@ async def create_project(
302337 ) -> ProjectGet :
303338 # POST /projects --> 202 Accepted
304339 query_params = {"hidden" : is_hidden }
305- headers = {
306- X_SIMCORE_PARENT_PROJECT_UUID : parent_project_uuid ,
307- X_SIMCORE_PARENT_NODE_ID : parent_node_id ,
308- }
309340
310341 response = await self .client .post (
311342 "/projects" ,
312343 params = query_params ,
313- headers = {k : f"{ v } " for k , v in headers .items () if v is not None },
314344 json = jsonable_encoder (project , by_alias = True , exclude = {"state" }),
315345 cookies = self .session_cookies ,
346+ headers = self ._get_session_headers (
347+ parent_project_uuid = parent_project_uuid , parent_node_id = parent_node_id
348+ ),
316349 )
317350 response .raise_for_status ()
318351 result = await self ._wait_for_long_running_task_results (response )
@@ -329,16 +362,14 @@ async def clone_project(
329362 ) -> ProjectGet :
330363 # POST /projects --> 202 Accepted
331364 query_params = {"from_study" : project_id , "hidden" : hidden }
332- _headers = {
333- X_SIMCORE_PARENT_PROJECT_UUID : parent_project_uuid ,
334- X_SIMCORE_PARENT_NODE_ID : parent_node_id ,
335- }
336365
337366 response = await self .client .post (
338367 "/projects" ,
339368 cookies = self .session_cookies ,
340369 params = query_params ,
341- headers = {k : f"{ v } " for k , v in _headers .items () if v is not None },
370+ headers = self ._get_session_headers (
371+ parent_project_uuid = parent_project_uuid , parent_node_id = parent_node_id
372+ ),
342373 )
343374 response .raise_for_status ()
344375 result = await self ._wait_for_long_running_task_results (response )
@@ -349,6 +380,7 @@ async def get_project(self, *, project_id: UUID) -> ProjectGet:
349380 response = await self .client .get (
350381 f"/projects/{ project_id } " ,
351382 cookies = self .session_cookies ,
383+ headers = self ._get_session_headers (),
352384 )
353385 response .raise_for_status ()
354386 data = Envelope [ProjectGet ].model_validate_json (response .text ).data
@@ -379,6 +411,7 @@ async def delete_project(self, *, project_id: ProjectID) -> None:
379411 response = await self .client .delete (
380412 f"/projects/{ project_id } " ,
381413 cookies = self .session_cookies ,
414+ headers = self ._get_session_headers (),
382415 )
383416 response .raise_for_status ()
384417
@@ -395,6 +428,7 @@ async def get_project_metadata_ports(
395428 response = await self .client .get (
396429 f"/projects/{ project_id } /metadata/ports" ,
397430 cookies = self .session_cookies ,
431+ headers = self ._get_session_headers (),
398432 )
399433 response .raise_for_status ()
400434 data = Envelope [list [StudyPort ]].model_validate_json (response .text ).data
@@ -411,6 +445,7 @@ async def get_project_metadata(
411445 response = await self .client .get (
412446 f"/projects/{ project_id } /metadata" ,
413447 cookies = self .session_cookies ,
448+ headers = self ._get_session_headers (),
414449 )
415450 response .raise_for_status ()
416451 data = Envelope [ProjectMetadataGet ].model_validate_json (response .text ).data
@@ -422,6 +457,7 @@ async def patch_project(self, *, project_id: UUID, patch_params: ProjectPatch):
422457 response = await self .client .patch (
423458 f"/projects/{ project_id } " ,
424459 cookies = self .session_cookies ,
460+ headers = self ._get_session_headers (),
425461 json = jsonable_encoder (patch_params , exclude_unset = True ),
426462 )
427463 response .raise_for_status ()
@@ -435,6 +471,7 @@ async def update_project_metadata(
435471 response = await self .client .patch (
436472 f"/projects/{ project_id } /metadata" ,
437473 cookies = self .session_cookies ,
474+ headers = self ._get_session_headers (),
438475 json = jsonable_encoder (ProjectMetadataUpdate (custom = metadata )),
439476 )
440477 response .raise_for_status ()
@@ -451,6 +488,7 @@ async def get_project_node_pricing_unit(
451488 response = await self .client .get (
452489 f"/projects/{ project_id } /nodes/{ node_id } /pricing-unit" ,
453490 cookies = self .session_cookies ,
491+ headers = self ._get_session_headers (),
454492 )
455493
456494 response .raise_for_status ()
@@ -472,6 +510,7 @@ async def connect_pricing_unit_to_project_node(
472510 response = await self .client .put (
473511 f"/projects/{ project_id } /nodes/{ node_id } /pricing-plan/{ pricing_plan } /pricing-unit/{ pricing_unit } " ,
474512 cookies = self .session_cookies ,
513+ headers = self ._get_session_headers (),
475514 )
476515 response .raise_for_status ()
477516
@@ -494,6 +533,7 @@ async def start_project(
494533 response = await self .client .post (
495534 f"/computations/{ project_id } :start" ,
496535 cookies = self .session_cookies ,
536+ headers = self ._get_session_headers (),
497537 json = jsonable_encoder (body , exclude_unset = True , exclude_defaults = True ),
498538 )
499539 response .raise_for_status ()
@@ -508,6 +548,7 @@ async def update_project_inputs(
508548 response = await self .client .patch (
509549 f"/projects/{ project_id } /inputs" ,
510550 cookies = self .session_cookies ,
551+ headers = self ._get_session_headers (),
511552 json = jsonable_encoder (new_inputs ),
512553 )
513554 response .raise_for_status ()
@@ -526,6 +567,7 @@ async def get_project_inputs(
526567 response = await self .client .get (
527568 f"/projects/{ project_id } /inputs" ,
528569 cookies = self .session_cookies ,
570+ headers = self ._get_session_headers (),
529571 )
530572
531573 response .raise_for_status ()
@@ -547,6 +589,7 @@ async def get_project_outputs(
547589 response = await self .client .get (
548590 f"/projects/{ project_id } /outputs" ,
549591 cookies = self .session_cookies ,
592+ headers = self ._get_session_headers (),
550593 )
551594
552595 response .raise_for_status ()
@@ -566,6 +609,7 @@ async def update_node_outputs(
566609 response = await self .client .patch (
567610 f"/projects/{ project_id } /nodes/{ node_id } /outputs" ,
568611 cookies = self .session_cookies ,
612+ headers = self ._get_session_headers (),
569613 json = jsonable_encoder (new_node_outputs ),
570614 )
571615 response .raise_for_status ()
@@ -577,6 +621,7 @@ async def get_default_wallet(self) -> WalletGetWithAvailableCreditsLegacy:
577621 response = await self .client .get (
578622 "/wallets/default" ,
579623 cookies = self .session_cookies ,
624+ headers = self ._get_session_headers (),
580625 )
581626 response .raise_for_status ()
582627 data = (
@@ -594,6 +639,7 @@ async def get_wallet(
594639 response = await self .client .get (
595640 f"/wallets/{ wallet_id } " ,
596641 cookies = self .session_cookies ,
642+ headers = self ._get_session_headers (),
597643 )
598644 response .raise_for_status ()
599645 data = (
@@ -609,6 +655,7 @@ async def get_project_wallet(self, *, project_id: ProjectID) -> WalletGet:
609655 response = await self .client .get (
610656 f"/projects/{ project_id } /wallet" ,
611657 cookies = self .session_cookies ,
658+ headers = self ._get_session_headers (),
612659 )
613660 response .raise_for_status ()
614661 data = Envelope [WalletGet ].model_validate_json (response .text ).data
@@ -624,6 +671,7 @@ async def get_product_price(self) -> GetCreditPriceLegacy:
624671 response = await self .client .get (
625672 "/credits-price" ,
626673 cookies = self .session_cookies ,
674+ headers = self ._get_session_headers (),
627675 )
628676 response .raise_for_status ()
629677 data = Envelope [GetCreditPriceLegacy ].model_validate_json (response .text ).data
@@ -643,6 +691,7 @@ async def get_service_pricing_plan(
643691 response = await self .client .get (
644692 f"/catalog/services/{ service_key } /{ version } /pricing-plan" ,
645693 cookies = self .session_cookies ,
694+ headers = self ._get_session_headers (),
646695 )
647696 response .raise_for_status ()
648697 pricing_plan_get = (
0 commit comments