dnastack-client-library 3.1.144__py3-none-any.whl → 3.1.159__py3-none-any.whl

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.
Files changed (24) hide show
  1. dnastack/__main__.py +20 -2
  2. dnastack/cli/commands/auth/commands.py +6 -4
  3. dnastack/cli/commands/config/contexts.py +2 -2
  4. dnastack/client/service_registry/helper.py +2 -0
  5. dnastack/client/workbench/samples/models.py +0 -1
  6. dnastack/common/auth_manager.py +8 -1
  7. dnastack/constants.py +1 -1
  8. dnastack/context/manager.py +45 -2
  9. dnastack/context/models.py +4 -1
  10. dnastack/http/authenticators/abstract.py +1 -1
  11. dnastack/http/authenticators/oauth2.py +80 -38
  12. dnastack/http/authenticators/oauth2_adapter/client_credential.py +7 -0
  13. dnastack/http/authenticators/oauth2_adapter/cloud_providers.py +122 -0
  14. dnastack/http/authenticators/oauth2_adapter/device_code_flow.py +6 -0
  15. dnastack/http/authenticators/oauth2_adapter/factory.py +2 -0
  16. dnastack/http/authenticators/oauth2_adapter/models.py +9 -1
  17. dnastack/http/authenticators/oauth2_adapter/token_exchange.py +142 -0
  18. dnastack/http/session_info.py +4 -0
  19. {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.159.dist-info}/METADATA +10 -1
  20. {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.159.dist-info}/RECORD +24 -22
  21. {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.159.dist-info}/WHEEL +0 -0
  22. {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.159.dist-info}/entry_points.txt +0 -0
  23. {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.159.dist-info}/licenses/LICENSE +0 -0
  24. {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.159.dist-info}/top_level.txt +0 -0
dnastack/__main__.py CHANGED
@@ -71,12 +71,30 @@ def version():
71
71
  type=bool,
72
72
  required=False,
73
73
  hidden=True,
74
+ ),
75
+ ArgumentSpec(
76
+ name='platform_credentials',
77
+ arg_names=['--platform-credentials'],
78
+ help='Use platform-specific credentials (IMDS) for authentication',
79
+ type=bool,
80
+ required=False,
81
+ hidden=True,
82
+ ),
83
+ ArgumentSpec(
84
+ name='subject_token',
85
+ arg_names=['--subject-token'],
86
+ help='Subject token for token exchange authentication',
87
+ type=str,
88
+ required=False,
89
+ hidden=True,
74
90
  )
75
91
  ]
76
92
  )
77
93
  def use(registry_hostname_or_url: str,
78
94
  context_name: Optional[str] = None,
79
- no_auth: bool = False):
95
+ no_auth: bool = False,
96
+ platform_credentials: bool = False,
97
+ subject_token: Optional[str] = None):
80
98
  """
81
99
  Import a configuration from host's service registry (if available) or the corresponding public configuration from
82
100
  cloud storage. If "--no-auth" is not defined, it will automatically initiate all authentication.
@@ -85,7 +103,7 @@ def use(registry_hostname_or_url: str,
85
103
 
86
104
  This is a shortcut to dnastack config contexts use".
87
105
  """
88
- _context_command_handler.use(registry_hostname_or_url, context_name=context_name, no_auth=no_auth)
106
+ _context_command_handler.use(registry_hostname_or_url, context_name=context_name, no_auth=no_auth, platform_credentials=platform_credentials, subject_token=subject_token)
89
107
 
90
108
 
91
109
  # noinspection PyTypeChecker
@@ -50,8 +50,7 @@ def init_auth_commands(group: Group):
50
50
  handler.initiate_authentications(endpoint_ids=[endpoint_id] if endpoint_id else [],
51
51
  force_refresh=force_refresh,
52
52
  revoke_existing=revoke_existing)
53
-
54
-
53
+
55
54
  @formatted_command(
56
55
  group=group,
57
56
  name='status',
@@ -94,6 +93,8 @@ def init_auth_commands(group: Group):
94
93
  handler.revoke([endpoint_id] if endpoint_id else [], force)
95
94
 
96
95
 
96
+
97
+
97
98
  class AuthCommandHandler:
98
99
  def __init__(self, context_name: Optional[str] = None):
99
100
  self._logger = get_logger(type(self).__name__)
@@ -126,7 +127,7 @@ class AuthCommandHandler:
126
127
  echo_header('Summary')
127
128
 
128
129
  if affected_endpoint_ids:
129
- echo_list('The client is no longer authenticated to the follow endpoints:',
130
+ echo_list('The client is no longer authenticated to the following endpoints:',
130
131
  affected_endpoint_ids)
131
132
  else:
132
133
  click.echo('No changes')
@@ -157,4 +158,5 @@ class AuthCommandHandler:
157
158
  auth_manager.events.on('no-refresh-token', handle_no_refresh_token)
158
159
  auth_manager.events.on('refresh-skipped', handle_refresh_skipped)
159
160
 
160
- auth_manager.initiate_authentications(endpoint_ids, force_refresh, revoke_existing)
161
+ auth_manager.initiate_authentications(endpoint_ids, force_refresh, revoke_existing, allow_token_exchange=False)
162
+
@@ -170,9 +170,9 @@ class ContextCommandHandler:
170
170
  def manager(self):
171
171
  return self._context_manager
172
172
 
173
- def use(self, registry_hostname_or_url: str, context_name: Optional[str] = None, no_auth: bool = False):
173
+ def use(self, registry_hostname_or_url: str, context_name: Optional[str] = None, no_auth: bool = False, platform_credentials: bool = False, subject_token: Optional[str] = None):
174
174
  echo_result('Context', 'blue', 'syncing', registry_hostname_or_url)
175
- self._context_manager.use(registry_hostname_or_url, context_name=context_name, no_auth=no_auth)
175
+ self._context_manager.use(registry_hostname_or_url, context_name=context_name, no_auth=no_auth, platform_credentials=platform_credentials, subject_token=subject_token)
176
176
  echo_result('Context', 'green', 'use', registry_hostname_or_url)
177
177
 
178
178
  def __handle_sync_event(self, event: Event):
@@ -2,6 +2,7 @@ from typing import Any, Dict, Optional
2
2
 
3
3
  from dnastack.client.models import ServiceEndpoint
4
4
  from dnastack.client.service_registry.models import Service
5
+ from dnastack.http.authenticators.oauth2_adapter.models import GRANT_TYPE_TOKEN_EXCHANGE
5
6
 
6
7
 
7
8
  def parse_ga4gh_service_info(service: Service, alternate_service_id: Optional[str] = None) -> ServiceEndpoint:
@@ -36,4 +37,5 @@ def _parse_authentication_info(auth_info: Dict[str, Any]) -> Dict[str, Any]:
36
37
  resource_url=auth_info.get('resource'),
37
38
  scope=auth_info.get('scope'),
38
39
  token_endpoint=auth_info.get('accessTokenUrl'),
40
+ platform_credentials=auth_info.get('grantType') == GRANT_TYPE_TOKEN_EXCHANGE,
39
41
  )
@@ -26,7 +26,6 @@ class Sample(BaseModel):
26
26
  id: str
27
27
  created_at: Optional[datetime]
28
28
  last_updated_at: Optional[datetime]
29
- files: Optional[List[SampleFile]]
30
29
 
31
30
 
32
31
  class SampleListResponse(PaginatedResource):
@@ -10,6 +10,7 @@ from dnastack.common.tracing import Span
10
10
  from dnastack.context.models import Context
11
11
  from dnastack.http.authenticators.abstract import Authenticator, AuthStateStatus, AuthState
12
12
  from dnastack.http.authenticators.factory import HttpAuthenticatorFactory
13
+ from dnastack.http.authenticators.oauth2_adapter.models import GRANT_TYPE_TOKEN_EXCHANGE
13
14
 
14
15
 
15
16
  class ExtendedAuthState(AuthState):
@@ -127,7 +128,8 @@ class AuthManager:
127
128
  def initiate_authentications(self,
128
129
  endpoint_ids: List[str] = None,
129
130
  force_refresh: bool = False,
130
- revoke_existing: bool = False):
131
+ revoke_existing: bool = False,
132
+ allow_token_exchange: bool = False):
131
133
  trace = Span(origin=self)
132
134
 
133
135
  authenticators = self.get_authenticators(endpoint_ids)
@@ -137,6 +139,11 @@ class AuthManager:
137
139
 
138
140
  for authenticator in authenticators:
139
141
  state = authenticator.get_state()
142
+ if not allow_token_exchange and state.auth_info.get('grant_type') == GRANT_TYPE_TOKEN_EXCHANGE:
143
+ self._logger.debug(f'Skipping token exchange authenticator {authenticator.session_id} (allow_token_exchange=False)')
144
+ index += 1
145
+ continue
146
+
140
147
  basic_event_info = dict(session_id=authenticator.session_id,
141
148
  state=state,
142
149
  index=index,
dnastack/constants.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import os
2
2
 
3
- __version__ = "v3.1.144"
3
+ __version__ = "v3.1.159"
4
4
 
5
5
  LOCAL_STORAGE_DIRECTORY = os.path.join(os.path.expanduser("~"), '.dnastack')
@@ -17,6 +17,7 @@ from dnastack.common.events import EventSource, Event
17
17
  from dnastack.common.logger import get_logger
18
18
  from dnastack.configuration.manager import ConfigurationManager
19
19
  from dnastack.context.models import Context
20
+ from dnastack.http.authenticators.oauth2_adapter.models import GRANT_TYPE_TOKEN_EXCHANGE
20
21
  from dnastack.http.client_factory import HttpClientFactory
21
22
 
22
23
 
@@ -252,7 +253,9 @@ class BaseContextManager:
252
253
  def use(self,
253
254
  registry_hostname_or_url: str,
254
255
  context_name: Optional[str] = None,
255
- no_auth: Optional[bool] = False) -> EndpointRepository:
256
+ no_auth: Optional[bool] = False,
257
+ platform_credentials: Optional[bool] = False,
258
+ subject_token: Optional[str] = None) -> EndpointRepository:
256
259
  target_hostname = self._get_hostname(registry_hostname_or_url)
257
260
  context_name = context_name or target_hostname
258
261
 
@@ -312,6 +315,11 @@ class BaseContextManager:
312
315
  self._logger.debug(f'Syncing: {reg_endpoint.url}')
313
316
  reg_manager.synchronize_endpoints(reg_endpoint.id)
314
317
 
318
+ if platform_credentials:
319
+ self._filter_endpoints_for_token_exchange(context)
320
+ if subject_token:
321
+ context.platform_subject_token = subject_token
322
+
315
323
  # Set the current context.
316
324
  self._contexts.set_current_context_name(context_name)
317
325
  self._contexts.set(context_name, context)
@@ -328,7 +336,7 @@ class BaseContextManager:
328
336
  for event_type in self.__propagated_auth_event_types:
329
337
  self.events.relay_from(auth_manager.events, event_type)
330
338
 
331
- auth_manager.initiate_authentications()
339
+ auth_manager.initiate_authentications(allow_token_exchange=platform_credentials)
332
340
  del auth_manager
333
341
 
334
342
  # Then, return the repository.
@@ -404,6 +412,41 @@ class BaseContextManager:
404
412
  return None
405
413
  # end: if
406
414
 
415
+ def _filter_endpoints_for_token_exchange(self, context: Context):
416
+ """Filter endpoint authentication methods to only include token-exchange grant types
417
+ Raises error if no endpoints support token exchange authentication"""
418
+ has_any_token_exchange = False
419
+
420
+ for endpoint in context.endpoints:
421
+ if endpoint.type == STANDARD_SERVICE_REGISTRY_TYPE_V1_0:
422
+ continue
423
+
424
+ all_auths = []
425
+ if endpoint.authentication:
426
+ all_auths.append(endpoint.authentication)
427
+ if endpoint.fallback_authentications:
428
+ all_auths.extend(endpoint.fallback_authentications)
429
+
430
+ token_exchange_auths = [
431
+ auth for auth in all_auths
432
+ if auth.get('grant_type') == GRANT_TYPE_TOKEN_EXCHANGE
433
+ ]
434
+
435
+ if token_exchange_auths:
436
+ endpoint.authentication = token_exchange_auths[0]
437
+ endpoint.fallback_authentications = token_exchange_auths[1:] or None
438
+ has_any_token_exchange = True
439
+ else:
440
+ endpoint.authentication = None
441
+ endpoint.fallback_authentications = None
442
+
443
+ if not has_any_token_exchange:
444
+ raise InvalidServiceRegistryError(
445
+ "Platform credentials (--platform-credentials) requested, but no endpoints "
446
+ "support token exchange authentication. Either remove --platform-credentials "
447
+ "or ensure the service registry includes token exchange authentication methods."
448
+ )
449
+
407
450
 
408
451
  @service.registered()
409
452
  class InMemoryContextManager(BaseContextManager):
@@ -1,4 +1,4 @@
1
- from typing import Dict, List
1
+ from typing import Dict, List, Optional
2
2
  from uuid import uuid4
3
3
 
4
4
  from pydantic import BaseModel, Field
@@ -16,3 +16,6 @@ class Context(BaseModel):
16
16
  defaults: Dict[str, str] = Field(default_factory=lambda: dict())
17
17
 
18
18
  endpoints: List[Endpoint] = Field(default_factory=lambda: list())
19
+
20
+ # Store subject token for token exchange authentication if provided
21
+ platform_subject_token: Optional[str] = Field(default=None)
@@ -147,7 +147,7 @@ class Authenticator(AuthBase, ABC):
147
147
  logger.debug('initialize: Restoring the session...')
148
148
  info = self.restore_session()
149
149
  if not info:
150
- logger.debug('initialize: Session is UNAVALIABLE.')
150
+ logger.debug('initialize: Session is UNAVAILABLE.')
151
151
  raise AuthenticationRequired('Session is not available')
152
152
  elif not info.is_valid():
153
153
  logger.debug(f'initialize: Session is INVALID ({"expired" if info.access_token else "token revoked"}).')
@@ -17,7 +17,8 @@ from dnastack.http.authenticators.abstract import Authenticator, AuthenticationR
17
17
  AuthStateStatus
18
18
  from dnastack.http.authenticators.constants import get_authenticator_log_level
19
19
  from dnastack.http.authenticators.oauth2_adapter.factory import OAuth2AdapterFactory
20
- from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication
20
+ from dnastack.http.authenticators.oauth2_adapter.token_exchange import TokenExchangeAdapter
21
+ from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication, GRANT_TYPE_TOKEN_EXCHANGE
21
22
  from dnastack.http.client_factory import HttpClientFactory
22
23
  from dnastack.http.session_info import SessionInfo, SessionManager, SessionInfoHandler
23
24
 
@@ -26,9 +27,18 @@ class OAuth2MisconfigurationError(RuntimeError):
26
27
  pass
27
28
 
28
29
 
30
+ def _is_token_exchange_session(session_info: SessionInfo) -> bool:
31
+ """Check if this session was created via token exchange"""
32
+ if not session_info.handler or not session_info.handler.auth_info:
33
+ return False
34
+
35
+ grant_type = session_info.handler.auth_info.get('grant_type')
36
+ return grant_type == GRANT_TYPE_TOKEN_EXCHANGE
37
+
38
+
29
39
  class OAuth2Authenticator(Authenticator):
30
40
  def __init__(self,
31
- endpoint: ServiceEndpoint,
41
+ endpoint: Optional[ServiceEndpoint],
32
42
  auth_info: Dict[str, Any],
33
43
  session_manager: Optional[SessionManager] = None,
34
44
  adapter_factory: Optional[OAuth2AdapterFactory] = None,
@@ -105,6 +115,12 @@ class OAuth2Authenticator(Authenticator):
105
115
  self.events.dispatch('authentication-before', event_details)
106
116
 
107
117
  auth_info = OAuth2Authentication(**self._auth_info)
118
+
119
+ use_platform_credentials = self._auth_info.get('platform_credentials', False) is True
120
+
121
+ if auth_info.grant_type == GRANT_TYPE_TOKEN_EXCHANGE or use_platform_credentials:
122
+ return self._authenticate_token_exchange(auth_info, trace_context)
123
+
108
124
  adapter = self._adapter_factory.get_from(auth_info)
109
125
 
110
126
  if adapter:
@@ -131,7 +147,7 @@ class OAuth2Authenticator(Authenticator):
131
147
  return self._session_info
132
148
 
133
149
  def refresh(self, trace_context: Optional[Span] = None) -> SessionInfo:
134
- """ Refresh the session using a refresh token. """
150
+ """ Refresh the session using a refresh token or re-authenticate for token exchange. """
135
151
  trace_context = trace_context or Span(origin='OAuth2Authenticator.refresh')
136
152
  logger = trace_context.create_span_logger(self._logger)
137
153
 
@@ -158,6 +174,15 @@ class OAuth2Authenticator(Authenticator):
158
174
  raise ReauthenticationRequired(f'The stored session information does not provide enough information to '
159
175
  f'refresh token. (given: {session_info})')
160
176
 
177
+ if _is_token_exchange_session(session_info):
178
+ try:
179
+ return self._reauthenticate_token_exchange(session_info, trace_context)
180
+ except Exception as e:
181
+ logger.error(f'refresh: Token exchange re-authentication failed: {e}')
182
+ event_details['reason'] = f'Token exchange re-authentication failed: {e}'
183
+ self.events.dispatch('refresh-failure', event_details)
184
+ raise ReauthenticationRequired(f'Token exchange re-authentication failed: {e}')
185
+
161
186
  if not session_info.refresh_token:
162
187
  logger.debug('refresh: Cannot refresh the tokens as the refresh token is not provided.')
163
188
 
@@ -209,7 +234,7 @@ class OAuth2Authenticator(Authenticator):
209
234
 
210
235
  if updated_session_info.scope:
211
236
  session_info.scope = updated_session_info.scope
212
-
237
+
213
238
  if updated_session_info.refresh_token:
214
239
  # NOTE: The refresh token may not be available in the response.
215
240
  session_info.refresh_token = updated_session_info.refresh_token
@@ -276,9 +301,7 @@ class OAuth2Authenticator(Authenticator):
276
301
  # Clear the local cache
277
302
  self._session_info = None
278
303
  self._session_manager.delete(session_id)
279
-
280
304
  self._logger.debug(f'Revoked Session {session_id}')
281
-
282
305
  self.events.dispatch('session-revoked', dict(session_id=session_id))
283
306
 
284
307
  def clear_access_token(self):
@@ -296,13 +319,11 @@ class OAuth2Authenticator(Authenticator):
296
319
 
297
320
  def restore_session(self) -> Optional[SessionInfo]:
298
321
  logger = self._logger
299
-
300
322
  session_id = self.session_id
301
- event_details = dict(cached=self._session_info is not None,
302
- cache_hash=session_id)
303
-
304
- session: SessionInfo = self._session_info
323
+ event_details = dict(cached=self._session_info is not None, cache_hash=session_id)
305
324
 
325
+ # Try to get session (in-memory, then stored)
326
+ session = self._session_info
306
327
  if session:
307
328
  logger.debug(f'In-memory Session Info: {session}')
308
329
  else:
@@ -312,46 +333,37 @@ class OAuth2Authenticator(Authenticator):
312
333
  if not session:
313
334
  event_details['reason'] = 'No session available'
314
335
  self.events.dispatch('session-not-restored', event_details)
315
-
316
336
  logger.debug(f'Require RE-AUTH -- event details = {event_details}')
317
-
318
337
  raise AuthenticationRequired('No session available')
319
- elif session.is_valid():
320
- logger.debug('The session is valid.')
321
-
322
- current_auth_info = OAuth2Authentication(**self._auth_info)
323
- current_config_hash = current_auth_info.get_content_hash()
324
- stored_config_hash = session.config_hash
325
338
 
326
- if current_config_hash == stored_config_hash:
327
- return session
328
- else:
329
- event_details['reason'] = 'Authentication information has changed and the session is invalidated.'
330
- self.events.dispatch('session-not-restored', event_details)
331
-
332
- logger.debug(f'Require RE-AUTH -- event details = {event_details}')
333
-
334
- raise ReauthenticationRequiredDueToConfigChange(
335
- 'The session is invalidated as the endpoint configuration has changed.'
336
- )
337
- else:
339
+ if not session.is_valid():
338
340
  logger.debug(f'The session is INVALID ({"expired" if session.access_token else "token revoked"}).')
339
341
 
340
342
  if session.refresh_token:
341
343
  event_details['reason'] = 'The session is invalid but it can be refreshed.'
342
344
  self.events.dispatch('session-not-restored', event_details)
343
-
344
345
  logger.debug(f'Require REFRESH -- event details = {event_details}')
345
-
346
346
  raise RefreshRequired(session)
347
347
  else:
348
348
  event_details['reason'] = 'The session is invalid. Require re-authentication.'
349
349
  self.events.dispatch('session-not-restored', event_details)
350
-
351
350
  logger.debug(f'Require RE-AUTH -- event details = {event_details}')
352
-
353
351
  raise ReauthenticationRequired('The session is invalid and refreshing tokens is not possible.')
354
352
 
353
+ current_auth_info = OAuth2Authentication(**self._auth_info)
354
+ current_config_hash = current_auth_info.get_content_hash()
355
+ stored_config_hash = session.config_hash
356
+
357
+ if current_config_hash == stored_config_hash:
358
+ return session
359
+ else:
360
+ event_details['reason'] = 'Authentication information has changed and the session is invalidated.'
361
+ self.events.dispatch('session-not-restored', event_details)
362
+ logger.debug(f'Require RE-AUTH -- event details = {event_details}')
363
+ raise ReauthenticationRequiredDueToConfigChange(
364
+ 'The session is invalidated as the endpoint configuration has changed.'
365
+ )
366
+
355
367
  def _convert_token_response_to_session(self,
356
368
  authentication: Dict[str, Any],
357
369
  response: Dict[str, Any]):
@@ -359,8 +371,8 @@ class OAuth2Authenticator(Authenticator):
359
371
 
360
372
  created_time = time()
361
373
  expiry_time = created_time + response['expires_in']
362
-
363
374
  current_auth_info = OAuth2Authentication(**self._auth_info)
375
+ self._logger.debug(f'Creating session: created_time={created_time}, expires_in={response["expires_in"]}, expiry_time={expiry_time}')
364
376
 
365
377
  return SessionInfo(
366
378
  model_version=4,
@@ -369,8 +381,8 @@ class OAuth2Authenticator(Authenticator):
369
381
  refresh_token=response.get('refresh_token'),
370
382
  scope=response.get('scope'),
371
383
  token_type=response['token_type'],
372
- issued_at=created_time,
373
- valid_until=expiry_time,
384
+ issued_at=int(created_time),
385
+ valid_until=int(expiry_time),
374
386
  handler=SessionInfoHandler(auth_info=authentication)
375
387
  )
376
388
 
@@ -385,3 +397,33 @@ class OAuth2Authenticator(Authenticator):
385
397
  @classmethod
386
398
  def make(cls, endpoint: ServiceEndpoint, auth_info: Dict[str, Any]):
387
399
  return cls(endpoint, auth_info)
400
+
401
+ def _reauthenticate_token_exchange(self, session_info: SessionInfo, trace_context: Span) -> SessionInfo:
402
+ """
403
+ Re-authenticate a token exchange session by performing the exchange again.
404
+ Assumes we're in a cloud environment and attempts to fetch new identity token.
405
+ """
406
+ if not session_info.handler or not session_info.handler.auth_info:
407
+ raise ReauthenticationRequired('Cannot re-authenticate: missing authentication configuration')
408
+ auth_info_dict = session_info.handler.auth_info.copy()
409
+ auth_info_dict['subject_token'] = None # Force cloud metadata fetch
410
+
411
+ auth_info = OAuth2Authentication(**auth_info_dict)
412
+ return self._authenticate_token_exchange(auth_info, trace_context)
413
+
414
+ def _authenticate_token_exchange(self, auth_info: OAuth2Authentication, trace_context: Span) -> SessionInfo:
415
+ """Handle token exchange authentication flow"""
416
+ session_id = self.session_id
417
+ event_details = dict(session_id=session_id, auth_info=self._auth_info)
418
+ adapter = TokenExchangeAdapter(auth_info)
419
+ for auth_event_type in ['blocking-response-required', 'blocking-response-ok', 'blocking-response-failed']:
420
+ self.events.relay_from(adapter.events, auth_event_type)
421
+
422
+ token_response = adapter.exchange_tokens(trace_context)
423
+ self._session_info = self._convert_token_response_to_session(auth_info.dict(), token_response)
424
+ self._session_manager.save(session_id, self._session_info)
425
+
426
+ event_details['session_info'] = self._session_info
427
+ self.events.dispatch('authentication-ok', event_details)
428
+
429
+ return self._session_info
@@ -2,12 +2,19 @@ from typing import Dict, Any, List
2
2
 
3
3
  from dnastack.common.tracing import Span
4
4
  from dnastack.http.authenticators.oauth2_adapter.abstract import OAuth2Adapter, AuthException
5
+ from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication
5
6
  from dnastack.http.client_factory import HttpClientFactory
6
7
 
7
8
 
8
9
  class ClientCredentialAdapter(OAuth2Adapter):
9
10
  __grant_type = 'client_credentials'
10
11
 
12
+ @classmethod
13
+ def is_compatible_with(cls, auth_info: OAuth2Authentication) -> bool:
14
+ if auth_info.grant_type != cls.__grant_type:
15
+ return False
16
+ return super().is_compatible_with(auth_info)
17
+
11
18
  @staticmethod
12
19
  def get_expected_auth_info_fields() -> List[str]:
13
20
  return [
@@ -0,0 +1,122 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+ from enum import Enum
4
+ from pydantic import BaseModel, Field
5
+ import logging
6
+
7
+ from dnastack.common.tracing import Span
8
+ from dnastack.http.client_factory import HttpClientFactory
9
+
10
+
11
+ class CloudProvider(str, Enum):
12
+ GCP = "gcp"
13
+
14
+
15
+ class CloudMetadataProvider(ABC):
16
+ """Abstract base class for cloud metadata providers."""
17
+
18
+ timeout: int
19
+ _logger: logging.Logger
20
+
21
+ def __init__(self, timeout: int = 5):
22
+ self.timeout = timeout
23
+ self._logger = logging.getLogger(type(self).__name__)
24
+
25
+ @abstractmethod
26
+ def is_available(self) -> bool:
27
+ """Check if this cloud provider's metadata service is available."""
28
+ pass
29
+
30
+ @abstractmethod
31
+ def get_identity_token(self, audience: str, trace_context: Span) -> Optional[str]:
32
+ """Fetch an identity token from the cloud metadata service."""
33
+ pass
34
+
35
+ @property
36
+ @abstractmethod
37
+ def name(self) -> str:
38
+ """Return the name of this cloud provider."""
39
+ pass
40
+
41
+
42
+ class GCPMetadataProvider(CloudMetadataProvider):
43
+ """Google Cloud Platform metadata provider."""
44
+
45
+ _METADATA_BASE_URL = 'http://metadata.google.internal/computeMetadata/v1'
46
+ _IDENTITY_ENDPOINT = '/instance/service-accounts/default/identity'
47
+ _METADATA_FLAVOR = 'Google'
48
+
49
+ @property
50
+ def name(self) -> str:
51
+ return CloudProvider.GCP.value
52
+
53
+ def is_available(self) -> bool:
54
+ """Check if GCP metadata service is available."""
55
+ try:
56
+ with HttpClientFactory.make() as http_session:
57
+ response = http_session.get(
58
+ f'{self._METADATA_BASE_URL}/project/project-id',
59
+ headers={'Metadata-Flavor': self._METADATA_FLAVOR},
60
+ timeout=1
61
+ )
62
+ return response.ok
63
+ except Exception:
64
+ return False
65
+
66
+ def get_identity_token(self, audience: str, trace_context: Span) -> Optional[str]:
67
+ """Fetch GCP identity token from metadata service.
68
+ '&format=full' ensures we get email in response"""
69
+ url = f'{self._METADATA_BASE_URL}{self._IDENTITY_ENDPOINT}?audience={audience}&format=full'
70
+
71
+ try:
72
+ with HttpClientFactory.make() as http_session:
73
+ response = http_session.get(
74
+ url,
75
+ headers={'Metadata-Flavor': self._METADATA_FLAVOR},
76
+ timeout=self.timeout
77
+ )
78
+
79
+ if response.ok:
80
+ token = response.text.strip()
81
+ self._logger.debug(f'Successfully fetched GCP identity token for audience: {audience}')
82
+ return token
83
+ else:
84
+ self._logger.warning(f'GCP metadata service returned {response.status_code}: {response.text}')
85
+ return None
86
+
87
+ except Exception as e:
88
+ self._logger.warning(f'Failed to fetch GCP identity token: {e}')
89
+ return None
90
+
91
+
92
+ class CloudMetadataConfig(BaseModel):
93
+ """Configuration model for cloud metadata provider."""
94
+ timeout: int = Field(5, ge=1, le=30, description="Timeout for metadata service request (1-30 seconds).")
95
+
96
+
97
+ class CloudProviderFactory:
98
+ """Factory for creating cloud metadata providers."""
99
+ _providers = {
100
+ CloudProvider.GCP: GCPMetadataProvider,
101
+ }
102
+
103
+ @classmethod
104
+ def create(cls, provider: CloudProvider, config: CloudMetadataConfig) -> CloudMetadataProvider:
105
+ """Create a cloud metadata provider instance."""
106
+ provider_class = cls._providers.get(provider)
107
+ if not provider_class:
108
+ raise ValueError(f'Unsupported cloud provider: {provider}')
109
+ return provider_class(timeout=config.timeout)
110
+
111
+ @classmethod
112
+ def detect_provider(cls, config: CloudMetadataConfig) -> Optional[CloudMetadataProvider]:
113
+ """Auto-detect the current cloud provider by checking all available providers."""
114
+ for provider_type in cls._providers.keys():
115
+ try:
116
+ provider = cls.create(provider_type, config)
117
+ if provider.is_available():
118
+ return provider
119
+ except Exception:
120
+ # Skip providers that fail to initialize or check availability
121
+ continue
122
+ return None
@@ -18,6 +18,12 @@ class DeviceCodeFlowAdapter(OAuth2Adapter):
18
18
  super(DeviceCodeFlowAdapter, self).__init__(auth_info)
19
19
  self.__console: Console = container.get(Console)
20
20
 
21
+ @classmethod
22
+ def is_compatible_with(cls, auth_info: OAuth2Authentication) -> bool:
23
+ if auth_info.grant_type != cls.__grant_type:
24
+ return False
25
+ return super().is_compatible_with(auth_info)
26
+
21
27
  @staticmethod
22
28
  def get_expected_auth_info_fields() -> List[str]:
23
29
  return [
@@ -5,6 +5,7 @@ from imagination.decorator import service
5
5
  from dnastack.http.authenticators.oauth2_adapter.abstract import OAuth2Adapter
6
6
  from dnastack.http.authenticators.oauth2_adapter.client_credential import ClientCredentialAdapter
7
7
  from dnastack.http.authenticators.oauth2_adapter.device_code_flow import DeviceCodeFlowAdapter
8
+ from dnastack.http.authenticators.oauth2_adapter.token_exchange import TokenExchangeAdapter
8
9
  from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication
9
10
 
10
11
 
@@ -14,6 +15,7 @@ class OAuth2AdapterFactory:
14
15
  __supported_auth_adapter_classes = [
15
16
  DeviceCodeFlowAdapter,
16
17
  ClientCredentialAdapter,
18
+ TokenExchangeAdapter,
17
19
  ]
18
20
 
19
21
  def get_from(self, auth_info: OAuth2Authentication) -> Optional[OAuth2Adapter]:
@@ -5,6 +5,9 @@ from pydantic import BaseModel
5
5
  from dnastack.common.model_mixin import JsonModelMixin as HashableModel
6
6
 
7
7
 
8
+ GRANT_TYPE_TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange'
9
+
10
+
8
11
  class OAuth2Authentication(BaseModel, HashableModel):
9
12
  """OAuth2 Authentication Information"""
10
13
  authorization_endpoint: Optional[str]
@@ -19,4 +22,9 @@ class OAuth2Authentication(BaseModel, HashableModel):
19
22
  resource_url: str
20
23
  scope: Optional[str]
21
24
  token_endpoint: Optional[str]
22
- type: str = 'oauth2'
25
+ type: str = 'oauth2'
26
+ subject_token: Optional[str]
27
+ subject_token_type: Optional[str]
28
+ requested_token_type: Optional[str]
29
+ audience: Optional[str]
30
+ cloud_provider: Optional[str] # Currently supported: 'gcp'
@@ -0,0 +1,142 @@
1
+ from typing import Dict, Any, List, Optional
2
+
3
+ from imagination import container
4
+
5
+ from dnastack.common.tracing import Span
6
+ from dnastack.http.authenticators.oauth2_adapter.abstract import OAuth2Adapter, AuthException
7
+ from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication, GRANT_TYPE_TOKEN_EXCHANGE
8
+ from dnastack.http.authenticators.oauth2_adapter.cloud_providers import (
9
+ CloudProviderFactory, CloudMetadataProvider, CloudMetadataConfig
10
+ )
11
+ from dnastack.http.client_factory import HttpClientFactory
12
+
13
+
14
+ class TokenExchangeAdapter(OAuth2Adapter):
15
+ __grant_type = GRANT_TYPE_TOKEN_EXCHANGE
16
+ __subject_token_type = 'urn:ietf:params:oauth:token-type:jwt'
17
+ __METADATA_TIMEOUT = 10
18
+
19
+ def __init__(self, auth_info: OAuth2Authentication):
20
+ super().__init__(auth_info)
21
+ self._cloud_provider: Optional[CloudMetadataProvider] = None
22
+
23
+ @classmethod
24
+ def is_compatible_with(cls, auth_info: OAuth2Authentication) -> bool:
25
+ if auth_info.grant_type != cls.__grant_type:
26
+ return False
27
+ required_fields = ['token_endpoint', 'resource_url']
28
+ return all(getattr(auth_info, field, None) for field in required_fields)
29
+
30
+ @staticmethod
31
+ def get_expected_auth_info_fields() -> List[str]:
32
+ return [
33
+ 'grant_type',
34
+ 'resource_url',
35
+ 'token_endpoint',
36
+ ]
37
+
38
+ def _get_subject_token(self, trace_context: Span) -> str:
39
+ """
40
+ Get ID token from cloud metadata service or use provided token.
41
+ For re-authentication, always tries cloud metadata fetch.
42
+ """
43
+ if self._auth_info.subject_token:
44
+ return self._auth_info.subject_token
45
+
46
+ context_subject_token = self._get_and_clear_context_subject_token()
47
+ if context_subject_token:
48
+ return context_subject_token
49
+
50
+ audience = self._auth_info.audience or self._auth_info.client_id or self._auth_info.resource_url
51
+ token = self._fetch_cloud_identity_token(audience, trace_context)
52
+ if token:
53
+ return token
54
+
55
+ raise AuthException(
56
+ 'No subject token provided and unable to fetch from cloud. '
57
+ 'Please provide a subject token or run from a supported cloud environment.'
58
+ )
59
+
60
+ def _fetch_cloud_identity_token(self, audience: str, trace_context: Span) -> Optional[str]:
61
+ """Fetch identity token from cloud metadata service."""
62
+ logger = trace_context.create_span_logger(self._logger)
63
+
64
+ if self._cloud_provider:
65
+ logger.debug(f'Attempting to fetch identity token from {self._cloud_provider.name}')
66
+ token = self._cloud_provider.get_identity_token(audience, trace_context)
67
+ if token:
68
+ return token
69
+ logger.error(f'Failed to fetch token from configured provider: {self._cloud_provider.name}')
70
+
71
+ logger.info('Auto-detecting cloud provider...')
72
+ config = CloudMetadataConfig(timeout=self.__METADATA_TIMEOUT)
73
+ detected_provider = CloudProviderFactory.detect_provider(config)
74
+ if detected_provider:
75
+ logger.info(f'Detected cloud provider: {detected_provider.name}')
76
+ self._cloud_provider = detected_provider
77
+ token = detected_provider.get_identity_token(audience, trace_context)
78
+ if token:
79
+ return token
80
+ logger.error(f'Failed to fetch token from detected provider: {detected_provider.name}')
81
+ else:
82
+ logger.error('No cloud provider detected')
83
+
84
+ return None
85
+
86
+ def _get_and_clear_context_subject_token(self) -> Optional[str]:
87
+ """Get subject token from current context if available and clear it after use"""
88
+ from dnastack.context.manager import ContextManager
89
+ context_manager = container.get(ContextManager)
90
+ current_context = context_manager.contexts.current_context
91
+ if current_context and current_context.platform_subject_token:
92
+ token = current_context.platform_subject_token
93
+ current_context.platform_subject_token = None
94
+ context_manager.contexts.set(context_manager.contexts.current_context_name, current_context)
95
+ return token
96
+ return None
97
+
98
+ def exchange_tokens(self, trace_context: Span) -> Dict[str, Any]:
99
+ logger = trace_context.create_span_logger(self._logger)
100
+ auth_info = self._auth_info
101
+ resource_urls = self._prepare_resource_urls_for_request(auth_info.resource_url)
102
+ subject_token = self._get_subject_token(trace_context)
103
+ client_id = auth_info.client_id
104
+ client_secret = auth_info.client_secret
105
+
106
+ trace_info = dict(
107
+ oauth='token-exchange',
108
+ token_url=auth_info.token_endpoint,
109
+ client_id=client_id,
110
+ grant_type=self.__grant_type,
111
+ resource_urls=resource_urls,
112
+ subject_token_type=self.__subject_token_type,
113
+ cloud_provider=self._cloud_provider.name if self._cloud_provider else 'none',
114
+ )
115
+ logger.debug(f'exchange_token: Authenticating with {trace_info}')
116
+ auth_params = {
117
+ 'grant_type': self.__grant_type,
118
+ 'subject_token_type': self.__subject_token_type,
119
+ 'subject_token': subject_token,
120
+ 'resource': resource_urls,
121
+ **({'requested_token_type': self._auth_info.requested_token_type} if self._auth_info.requested_token_type else {}),
122
+ **({'scope': auth_info.scope} if auth_info.scope else {})
123
+ }
124
+
125
+ with trace_context.new_span(metadata=trace_info) as sub_span:
126
+ with HttpClientFactory.make() as http_session:
127
+ span_headers = sub_span.create_http_headers()
128
+ response = http_session.post(
129
+ auth_info.token_endpoint,
130
+ data=auth_params,
131
+ headers=span_headers,
132
+ auth=(client_id, client_secret)
133
+ )
134
+
135
+ if not response.ok:
136
+ raise AuthException(
137
+ f'Failed to perform token exchange for {client_id} as the server responds with HTTP {response.status_code}:'
138
+ f'\n\n{response.text}\n',
139
+ resource_urls
140
+ )
141
+
142
+ return response.json()
@@ -97,6 +97,7 @@ class BaseSessionStorage(ABC):
97
97
  def __delitem__(self, id: str):
98
98
  raise NotImplementedError()
99
99
 
100
+
100
101
  def __str__(self):
101
102
  return f'{type(self).__module__}.{type(self).__name__}'
102
103
 
@@ -125,6 +126,7 @@ class InMemorySessionStorage(BaseSessionStorage):
125
126
  del self.__cache_map[id]
126
127
 
127
128
 
129
+
128
130
  @service.registered(
129
131
  params=[
130
132
  EnvironmentVariable('DNASTACK_SESSION_DIR',
@@ -174,6 +176,7 @@ class FileSessionStorage(BaseSessionStorage):
174
176
  final_file_path = self.__get_file_path(id)
175
177
  os.unlink(final_file_path)
176
178
 
179
+
177
180
  def __get_file_path(self, id: str) -> str:
178
181
  path_blocks = []
179
182
 
@@ -262,6 +265,7 @@ class SessionManager:
262
265
  finally:
263
266
  del self.__change_locks[id]
264
267
 
268
+
265
269
  def __lock(self, id) -> Lock:
266
270
  if id not in self.__change_locks:
267
271
  self.__change_locks[id] = Lock()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dnastack-client-library
3
- Version: 3.1.144
3
+ Version: 3.1.159
4
4
  Summary: DNAstack's GA4GH library and CLI
5
5
  Author-email: DNAstack <devs@dnastack.com>
6
6
  License: Apache License, Version 2.0
@@ -27,6 +27,15 @@ Requires-Dist: kotoba
27
27
  Requires-Dist: imagination>=3.3.1
28
28
  Requires-Dist: requests-toolbelt<1,>=0.9.1
29
29
  Requires-Dist: httpie>=3.2.1
30
+ Requires-Dist: beautifulsoup4>=4.10.0
31
+ Requires-Dist: flask~=2.1
32
+ Requires-Dist: google-cloud-secret-manager
33
+ Requires-Dist: google-crc32c
34
+ Requires-Dist: pandas
35
+ Requires-Dist: python-dotenv
36
+ Requires-Dist: pip>=21.3.1
37
+ Requires-Dist: packaging>=21.3
38
+ Requires-Dist: selenium>=4.1.0
30
39
  Provides-Extra: test
31
40
  Requires-Dist: selenium>=3.141.0; extra == "test"
32
41
  Requires-Dist: pyjwt>=2.1.0; extra == "test"
@@ -1,6 +1,6 @@
1
1
  dnastack/__init__.py,sha256=mslf7se8vBSK_HkqWTGPdibeVhT4xyKXgzQBV7dEK1M,333
2
- dnastack/__main__.py,sha256=0R4pq-kVx2TjcGT_bUXiqKj91ba3Op8I6w1pqiaxR10,3682
3
- dnastack/constants.py,sha256=jaK6QFWKaJVTqeMQ6KIkcEk6jNZF9eYkteiTPeHFOXA,114
2
+ dnastack/__main__.py,sha256=EKmtIs4TBseQJi-OT_U6LqRyKLiyrGTBuTQg9zE-G2I,4376
3
+ dnastack/constants.py,sha256=Lgz298BXYq_2gOGdhlAdlDkzJjgpA-c6hddW5d5bRuQ,114
4
4
  dnastack/feature_flags.py,sha256=RK_V_Ovncoe6NeTheAA_frP-kYkZC1fDlTbbup2KYG4,1419
5
5
  dnastack/json_path.py,sha256=TyghhDf7nGQmnsUWBhenU_fKsE_Ez-HLVER6HgH5-hU,2700
6
6
  dnastack/omics_cli.py,sha256=ZppKZTHv_XjUUZyRIzSkx0Ug5ODAYrCOTsU0ezCOVrA,3694
@@ -31,7 +31,7 @@ dnastack/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  dnastack/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  dnastack/cli/commands/utils.py,sha256=ZwKTyyvqBTrt27cF9wF6SglE01t7NkHWLMjXegi_6iA,574
33
33
  dnastack/cli/commands/auth/__init__.py,sha256=SGnt7w8ccS1ED6EQzBq4PaH3kqoLuXC4wrFMa7P_Vg4,313
34
- dnastack/cli/commands/auth/commands.py,sha256=ds-Rj8KtmS567DbJLdZSXTiPbycc1rjzq6b8BT1-z8c,6469
34
+ dnastack/cli/commands/auth/commands.py,sha256=FIQLegV5h6T2IP-fh4xcp7aItC4OkKSOHIlFCBQK1bw,6498
35
35
  dnastack/cli/commands/auth/event_handlers.py,sha256=g6FPChlBZZr7M6hejGWT6Z3THNZr7AtbV0S5PJEtU_o,3627
36
36
  dnastack/cli/commands/collections/__init__.py,sha256=hrsjIkOvqOnYUN4xwoSB3iTCLrbX6qG-XeafVzoGleI,535
37
37
  dnastack/cli/commands/collections/commands.py,sha256=yawGsTZfgkTYvyCIyRaup9mEQOrr_nfE7Cj6lPcEaIk,8978
@@ -39,7 +39,7 @@ dnastack/cli/commands/collections/tables.py,sha256=31D3QuI7VQp9V7M4EeoiTzwf3lg1d
39
39
  dnastack/cli/commands/collections/utils.py,sha256=o59nT42PACZiGo-dgo5Qjg8UDWgvcjmgb4fDGWpjm7w,6225
40
40
  dnastack/cli/commands/config/__init__.py,sha256=xBBLgV5tvRr45gWgZhGHLxB12fNMF8LDqeBvMXCt_sk,706
41
41
  dnastack/cli/commands/config/commands.py,sha256=ANyepqMCCwL2qtbrlS4OxYyGzbMQLF_P7iiKnACIsj8,813
42
- dnastack/cli/commands/config/contexts.py,sha256=OcTd_XhTyDf0-CyLnmdYbD7dlO8r1peprTtQV-A-JEc,6095
42
+ dnastack/cli/commands/config/contexts.py,sha256=Dhl3C90D1cuO_LgO0zZIwZtfX3yB3WqhIX3Nu2Bsr78,6240
43
43
  dnastack/cli/commands/config/endpoints.py,sha256=2R6bw7cx32iJ_zzu4C7nUsQiDFsu0tvj_PplmpgsA0E,23906
44
44
  dnastack/cli/commands/config/registries.py,sha256=PCSA05mLuG8_4XR6Gl6yjZy6wPXT4BLhTRMIjO6Zq0I,6422
45
45
  dnastack/cli/commands/dataconnect/__init__.py,sha256=pmSnzfQQkhupJ-ORfTNVo3ev9ASpOGk5Cv4kMZA7zQ8,521
@@ -130,7 +130,7 @@ dnastack/client/explorer/models.py,sha256=vrltbcb4qAx6z1oGXG8ufw2kZ36dDiwU5QGqOo
130
130
  dnastack/client/service_registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
131
131
  dnastack/client/service_registry/client.py,sha256=r7D8CnPJLbNkc03g2PYHt880Ba1oPW2d8B0ShP0p4Eo,1131
132
132
  dnastack/client/service_registry/factory.py,sha256=MKidmvDuIdnz9jsqm6yDjImldcNajKxopwmKFFCTN4U,10751
133
- dnastack/client/service_registry/helper.py,sha256=CxWsov0WCk5weYj-dYP4gE136nILQTyshYrJl2hpQRw,1326
133
+ dnastack/client/service_registry/helper.py,sha256=9zJLsgEN1kwEZJs9MdXQbcqRikXHf-uXHHyKcYUxBwo,1501
134
134
  dnastack/client/service_registry/manager.py,sha256=o_Eqg1GRj8x2q-duWozVge8JRFXUEGhV5-jbkcATFw8,9311
135
135
  dnastack/client/service_registry/models.py,sha256=X0vf1sv1f5sZ_7wZMmynjPF57ZrKHZ8FDXV5iHxv7b4,1513
136
136
  dnastack/client/workbench/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -141,7 +141,7 @@ dnastack/client/workbench/ewes/client.py,sha256=yIqjwyyY9q0NrxpTX6LrnlnjavHoa6Fo
141
141
  dnastack/client/workbench/ewes/models.py,sha256=wnzthvBjzG_Zq1lyVJBTN5gWgT9gO1sZ0-Q91isy7FA,8214
142
142
  dnastack/client/workbench/samples/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
143
143
  dnastack/client/workbench/samples/client.py,sha256=2X34SYTjV6n4yZz0q7Kaa4NPWDHRi2ut0uJWL3zXZWA,5901
144
- dnastack/client/workbench/samples/models.py,sha256=qGR--2b-Z71PlNXZt8Sg5LbrDNICFV54l3IWetwCTw8,1504
144
+ dnastack/client/workbench/samples/models.py,sha256=g_04aDltLVRVCstOGkINqJNo1XSKB2aXWwnMfDEhC0Y,1466
145
145
  dnastack/client/workbench/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
146
146
  dnastack/client/workbench/storage/client.py,sha256=uMr0mtwMj07TKhS2_IHIKoF-JkrEUiFdGpijVHP-vb4,4080
147
147
  dnastack/client/workbench/storage/models.py,sha256=S5P1m-blJH5x4glmIcu1KTDoJEjt8Qfp-lEeBW9I7PI,2219
@@ -153,7 +153,7 @@ dnastack/client/workbench/workflow/client.py,sha256=ZbQAzJ1DPoPcMGkJEKSyV37pEpSk
153
153
  dnastack/client/workbench/workflow/models.py,sha256=xokiS_Kyet3zbyB8i5Z3Uq3wmvcgPMkTQ2WGWhFrexw,4481
154
154
  dnastack/client/workbench/workflow/utils.py,sha256=Yw9X-Gtu5lYPDCZjimFJMhrib9ELl07YyD4A-L8Y7pE,4661
155
155
  dnastack/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
156
- dnastack/common/auth_manager.py,sha256=N5lVkM94KGSh8Lje6os9v6rQC082vVJiRwe33US517g,8587
156
+ dnastack/common/auth_manager.py,sha256=AFMDIR01AQ2EPNyUZ7RMS8A8FvdRUMEhLtFjTd5Mdqw,9051
157
157
  dnastack/common/class_decorator.py,sha256=vu-TytoFW06IZwuTl54oR-YKqftAPeyIGJeqSG3WvO4,2470
158
158
  dnastack/common/console.py,sha256=edvhWiXtPPXg508RCcfuWKElpLdRu2A9p1kLhdsTtSw,602
159
159
  dnastack/common/environments.py,sha256=VDSSxvtxd60IxAyjgwx9YJuAwWkorhnAMN-XaV087gw,2529
@@ -175,26 +175,28 @@ dnastack/configuration/wrapper.py,sha256=qLmI8bgwTTflym_e0HC85IwdoCuLfazJt_O8mJJ
175
175
  dnastack/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
176
176
  dnastack/context/context_wraper.py,sha256=G88LciVBW5HJkvhi3oYS6RlrH_-l0H2CSYSU8nS33_0,315
177
177
  dnastack/context/helper.py,sha256=GOlmi2ddkstSRBo3sG6yB8_2oMFqN1o6yI8Ny-gpkbQ,1475
178
- dnastack/context/manager.py,sha256=5rwQGmV4Em3RyJZNvVIgi8xc2ANfTIGP7v7a6gqowZU,15340
179
- dnastack/context/models.py,sha256=1tVkBiQQf252VS5YmLqSNTt0McbitC3Oe2_C7dH-OQg,520
178
+ dnastack/context/manager.py,sha256=4Z_KhDhIClkUOiT605yi9SwehdqiJwYth4T3G3Lb4rM,17395
179
+ dnastack/context/models.py,sha256=BXSJZSwiopNZ_l2nY8uzofoYebLYwu0BLbirSBIY110,671
180
180
  dnastack/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
181
181
  dnastack/http/client_factory.py,sha256=HdRZpTEnFQcHEdVbdYl7AbCcRUv3K3kVyznIxEWIqvA,650
182
182
  dnastack/http/session.py,sha256=R-rsE_TTswlTTlqJW7a6_O-sVMmbrnEMnKBf7m_5I8U,15058
183
- dnastack/http/session_info.py,sha256=ae9MnzMxOoPanGrxec5oIlnv2cizx5bT_h0wclHkAJg,9362
183
+ dnastack/http/session_info.py,sha256=I0m8Z17MCtsWgMCmlzhtOzsXZfUddBAXp0XSOLWfgrM,9366
184
184
  dnastack/http/authenticators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
185
- dnastack/http/authenticators/abstract.py,sha256=xaTA_3Wmc24x7tCvUcb5daUcC8sALMQoOx6iyTECZts,9241
185
+ dnastack/http/authenticators/abstract.py,sha256=b5zXqSdCasiy6Bz5LYTr7e_p7l4__Mq5shgTV6bBGnk,9241
186
186
  dnastack/http/authenticators/constants.py,sha256=mSpBnm5lMMmMJwr13KIfCoOXXJLP4qGDkFprYXALu8o,1278
187
187
  dnastack/http/authenticators/factory.py,sha256=PZhE7c59rDtmtUx3PcX_NzarSeRpoPCD6oexDE6Z05I,2062
188
- dnastack/http/authenticators/oauth2.py,sha256=GxS7ynHHH-76RUbxZqdqb9qagJavvLWPwfOEiQf51Nk,17030
188
+ dnastack/http/authenticators/oauth2.py,sha256=wPTrDzR8Jzk6ugn9yLt9HBcFae9bH6tgSRhScVQcglU,20043
189
189
  dnastack/http/authenticators/oauth2_adapter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
190
190
  dnastack/http/authenticators/oauth2_adapter/abstract.py,sha256=Tm4Nnroo5_vp0UgZHhcEDVRRbhIrvVdfPr8nTyihoH4,2832
191
- dnastack/http/authenticators/oauth2_adapter/client_credential.py,sha256=Lu22mTT1kjnfLbRPjNVoJ95vySK9G9FItlrnn1g4xgE,2516
192
- dnastack/http/authenticators/oauth2_adapter/device_code_flow.py,sha256=j0EODp-O8DCCqh9kOGi5MDOdVXkQO14Q7ZJe4ZrzPCY,6527
193
- dnastack/http/authenticators/oauth2_adapter/factory.py,sha256=r8K6swt5zhraP74KhTL2K4sQ71HWAMLM0oHg8qQT4BA,965
194
- dnastack/http/authenticators/oauth2_adapter/models.py,sha256=U11r8DZsWvjIRNCJE1mmQMuprZw3fpFwFBg7vmI5w48,660
195
- dnastack_client_library-3.1.144.dist-info/licenses/LICENSE,sha256=uwybO-wUbQhxkosgjhJlxmYATMy-AzoULFO9FUedE34,11580
196
- dnastack_client_library-3.1.144.dist-info/METADATA,sha256=VGEBV4ScOMv2xQbFzn8Pp0Ejo2d3DeOGJuf6W26Mu90,1490
197
- dnastack_client_library-3.1.144.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
198
- dnastack_client_library-3.1.144.dist-info/entry_points.txt,sha256=Y6OeicsiyGn3-8D-SiV4NiKlJgXfkSqK88kFBR6R1rY,89
199
- dnastack_client_library-3.1.144.dist-info/top_level.txt,sha256=P2RgRyqJ7hfNy1wLVRoVLJYEppUVkCX3syGK9zBqkt8,9
200
- dnastack_client_library-3.1.144.dist-info/RECORD,,
191
+ dnastack/http/authenticators/oauth2_adapter/client_credential.py,sha256=wWHk6-l-GIxG5_MtcjLvu6sU6wugAFudaBUH33BkXPQ,2823
192
+ dnastack/http/authenticators/oauth2_adapter/cloud_providers.py,sha256=UHQ-YHHr5ipqSQVzCfr95Uv3zkFcop_RCpK4q6a2yJg,4317
193
+ dnastack/http/authenticators/oauth2_adapter/device_code_flow.py,sha256=dXI5CyUcsqYg6gf5vDC_3eY6Cc-H1C8W7FeD_24j92A,6750
194
+ dnastack/http/authenticators/oauth2_adapter/factory.py,sha256=ZtNXOklWEim-26ooNoPp3ji_hRg1vf4fHHnY94F0wLI,1087
195
+ dnastack/http/authenticators/oauth2_adapter/models.py,sha256=iY7asrSElyjubInrGV5rJKKZAxJWeq7csnaj-EqMq00,943
196
+ dnastack/http/authenticators/oauth2_adapter/token_exchange.py,sha256=nSuAsSKWa_UNqHSbPMOEk4komaFITYAnE04Sk5WOrLc,6332
197
+ dnastack_client_library-3.1.159.dist-info/licenses/LICENSE,sha256=uwybO-wUbQhxkosgjhJlxmYATMy-AzoULFO9FUedE34,11580
198
+ dnastack_client_library-3.1.159.dist-info/METADATA,sha256=8WpjZzXD8-Zq7hzlzrm953TLgASaQrScgbOPFqKldYg,1766
199
+ dnastack_client_library-3.1.159.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
200
+ dnastack_client_library-3.1.159.dist-info/entry_points.txt,sha256=Y6OeicsiyGn3-8D-SiV4NiKlJgXfkSqK88kFBR6R1rY,89
201
+ dnastack_client_library-3.1.159.dist-info/top_level.txt,sha256=P2RgRyqJ7hfNy1wLVRoVLJYEppUVkCX3syGK9zBqkt8,9
202
+ dnastack_client_library-3.1.159.dist-info/RECORD,,