Repository: treeverse/lakeFS
Pull request #9039: Python Wrapper IAM-ish support
Author: login: Jonathan-Rosenberg
Labels: name: include-changelog
description: PR description should be included in next release changelog
name: python-wrapper
Created: 2025-05-11T15:37:22Z | Merged: 2025-05-18T09:19:18Z

Closes #9051 

Look at the issue for more details.

## How was this tested?

Added **auth unit test** and tested manually the three authentication options (including precedence testing): 
1. Session token
2. Credentials (API key and secret)
3. Preconfigured IAM


Linked issue #9051: Python SDK Support for AWS IAM Role Authentication
Support authentication to lakeFS with AWS IAM Roles in Python SDK

Diff:
diff --git a/clients/python-wrapper/lakefs/auth.py b/clients/python-wrapper/lakefs/auth.py
new file mode 100644
index 00000000000..e3b4b73aade
--- /dev/null
+++ b/clients/python-wrapper/lakefs/auth.py
@@ -0,0 +1,144 @@
+"""
+lakeFS Authentication Module
+
+Includes authentication util functions
+"""
+
+import base64
+import datetime
+import json
+from typing import Any, Optional, TYPE_CHECKING
+from urllib.parse import urlparse, parse_qs
+
+from lakefs_sdk import ExternalLoginInformation
+from lakefs_sdk.client import LakeFSClient
+
+from lakefs.config import ClientConfig
+from lakefs.exceptions import api_exception_handler
+
+DEFAULT_AWS_REGION = "us-east-1"
+
+if TYPE_CHECKING:
+    import boto3
+
+def access_token_from_aws_iam_role(sdk_client: LakeFSClient,
+                                   lakefs_host: str,
+                                   boto3_session: "boto3.Session",
+                                   aws_provider_auth_params: ClientConfig.AWSIAMProviderConfig) -> tuple[Any, datetime]:
+    """
+    Generate an access token for lakeFS authentication using AWS IAM role.
+    :param sdk_client: LakeFSClient
+    :param lakefs_host: LakeFS API URL
+    :param boto3_session: Session
+    :param aws_provider_auth_params: ClientConfig.AWSIAMProviderConfig
+    :return: An access token for lakeFS authentication.
+    """
+    presigned_ttl = aws_provider_auth_params.url_presign_ttl_seconds
+    token_ttl_seconds = aws_provider_auth_params.token_ttl_seconds
+    token_req_headers = aws_provider_auth_params.token_request_headers
+
+    identity_token = _get_identity_token(boto3_session, lakefs_host, presign_expiry=presigned_ttl,
+                                         additional_headers=token_req_headers)
+    external_login_information = ExternalLoginInformation(
+        token_expiration_duration=token_ttl_seconds,
+        identity_request={
+            "identity_token": identity_token
+        }
+    )
+
+    with api_exception_handler():
+        auth_token = sdk_client.auth_api.external_principal_login(external_login_information)
+    expiration_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=token_ttl_seconds)
+    reset_token_time = expiration_time - datetime.timedelta(minutes=5)
+    return auth_token.token, reset_token_time
+
+def _get_identity_token(
+        session: "boto3.Session",
+        lakefs_host: str,
+        additional_headers: Optional[dict[str, str]],
+        presign_expiry
+) -> str:
+    """
+   Generate the identity token required for lakeFS authentication from an AWS session.
+
+   This function uses the STS client to generate a presigned URL for the `get_caller_identity` action,
+    extracts the required values from the URL,
+   and creates a base64-encoded JSON object with these values.
+
+   :param session: A boto3 session object with the necessary AWS credentials and region information.
+   :return: A base64-encoded JSON string containing the required authentication information.
+   :raises ValueError: If the session does not have a region name set.
+   """
+
+    # this method should only be called when installing the aws-iam additional requirement
+    from botocore.client import Config  # pylint: disable=import-outside-toplevel, import-error
+    from botocore.signers import RequestSigner  # pylint: disable=import-outside-toplevel, import-error
+
+    sts_client = session.client('sts', config=Config(signature_version='v4'))
+    endpoint = sts_client.meta.endpoint_url
+    service_id = sts_client.meta.service_model.service_id
+    region = _extract_region_from_endpoint(endpoint)
+    # signer is used because the presigned URL generated by the STS does not support additional headers
+    signer = RequestSigner(
+        service_id,
+        region,
+        'sts',
+        'v4',
+        session.get_credentials(),
+        session.events
+    )
+    endpoint_with_params = f"{endpoint}/?Action=GetCallerIdentity&Version=2011-06-15"
+    if additional_headers is None:
+        additional_headers = {
+            'X-LakeFS-Server-ID': lakefs_host,
+        }
+    params = {
+        'method': 'POST',
+        'url': endpoint_with_params,
+        'body': {},
+        'headers': additional_headers,
+        'context': {}
+    }
+    presigned_url = signer.generate_presigned_url(
+        params,
+        region_name=region,
+        expires_in=presign_expiry,
+        operation_name=''
+    )
+    parsed_url = urlparse(presigned_url)
+    query_params = parse_qs(parsed_url.query)
+
+    # Extract values from query parameters
+    json_object = {
+        "method": "POST",
+        "host": parsed_url.hostname,
+        "region": region,
+        "action": query_params['Action'][0],
+        "date": query_params['X-Amz-Date'][0],
+        "expiration_duration": query_params['X-Amz-Expires'][0],
+        "access_key_id": query_params['X-Amz-Credential'][0].split('/')[0],
+        "signature": query_params['X-Amz-Signature'][0],
+        "signed_headers": query_params.get('X-Amz-SignedHeaders', [''])[0].split(';'),
+        "version": query_params['Version'][0],
+        "algorithm": query_params['X-Amz-Algorithm'][0],
+        "security_token": query_params.get('X-Amz-Security-Token', [None])[0]
+    }
+    json_string = json.dumps(json_object)
+    return base64.b64encode(json_string.encode('utf-8')).decode('utf-8')
+
+def _extract_region_from_endpoint(endpoint):
+    """
+    Extract the region name from an STS endpoint URL.
+    for example: https://sts.eu-central-1.amazonaws.com/ -> eu-central-1
+    and for example: https://sts.amazonaws.com/ -> DEFAULT_REGION
+
+    :param endpoint: The endpoint URL of the STS client.
+    :return: The region name extracted from the endpoint URL.
+    """
+
+    parts = endpoint.split('.')
+    if len(parts) == 4:
+        return parts[1]
+    if len(parts) > 4:
+        return parts[2]
+    return DEFAULT_AWS_REGION
diff --git a/clients/python-wrapper/lakefs/client.py b/clients/python-wrapper/lakefs/client.py
index 31eee1d039d..48a8edb21fe 100644
--- a/clients/python-wrapper/lakefs/client.py
+++ b/clients/python-wrapper/lakefs/client.py
@@ -6,25 +6,24 @@
 
 from __future__ import annotations
 
-import base64
-import json
+import datetime
 from threading import Lock
 from typing import Optional
 from typing import TYPE_CHECKING
-from urllib.parse import urlparse, parse_qs
+from urllib.parse import urlparse
 
 import lakefs_sdk
-from lakefs_sdk import ExternalLoginInformation
 from lakefs_sdk.client import LakeFSClient
 
 from lakefs.config import ClientConfig
 from lakefs.exceptions import NotAuthorizedException, ServerException, api_exception_handler
 from lakefs.models import ServerStorageConfiguration
+from lakefs.auth import access_token_from_aws_iam_role
+
 
 if TYPE_CHECKING:
     import boto3
 
-DEFAULT_REGION = "us-east-1"
 SINGLE_STORAGE_ID = ""
 
 class ServerConfiguration:
@@ -93,6 +92,48 @@ def __init__(self, **kwargs):
         self._conf = ClientConfig(**kwargs)
         self._client = LakeFSClient(self._conf, header_name='X-Lakefs-Client',
                                     header_value='python-lakefs')
+        self._server_conf = None
+        self._reset_token_time = None
+        self._session = None
+
+        # Initialize auth if using IAM provider
+        if self._conf.get_auth_type() is ClientConfig.AuthType.IAM:
+            iam_provider = self._conf.iam_provider
+            if iam_provider.type is ClientConfig.ProviderType.AWS_IAM:
+                # boto3 session lazy loading (only if an AWS IAM provider is used)
+                import boto3 # pylint: disable=import-outside-toplevel, import-error
+                self._session = boto3.Session()
+                lakefs_host = urlparse(self._conf.host).hostname
+                self._conf.access_token, self._reset_token_time = access_token_from_aws_iam_role(
+                    self._client,
+                    lakefs_host,
+                    self._session,
+                    iam_provider.aws_iam
+                )
+
+    def __getattribute__(self, name):
+        if name == "sdk_client":
+            object.__getattribute__(self, "_refresh_token_if_necessary")()
+        return object.__getattribute__(self, name)
+
+    def _refresh_token_if_necessary(self):
+        """
+        Refresh the token if necessary
+        """
+        current_time = datetime.datetime.now(datetime.timezone.utc)
+        if (self._conf.get_auth_type() is ClientConfig.AuthType.IAM and
+                self._reset_token_time is not None and
+                current_time >= self._reset_token_time):
+            # Refresh token:
+            iam_provider = self._conf.iam_provider
+            if iam_provider.type == ClientConfig.ProviderType.AWS_IAM:
+                lakefs_host = urlparse(self._conf.host).hostname
+                self._conf.access_token, self._reset_token_time = access_token_from_aws_iam_role(
+                    self._client,
+                    lakefs_host,
+                    self._session,
+                    iam_provider.aws_iam
+                )
 
     @property
     def config(self):
@@ -115,6 +156,17 @@ def storage_config(self):
         """
         return self.storage_config_by_id()
 
+    @property
+    def reset_time(self):
+        """
+        The time when the access token will expire.
+        """
+        return self._reset_token_time
+
+    @reset_time.setter
+    def reset_time(self, time: datetime):
+        self._reset_token_time = time
+
     def storage_config_by_id(self, storage_id=SINGLE_STORAGE_ID):
         """
         Returns lakeFS SDK storage config object, defaults to a single storage ID.
@@ -133,101 +185,6 @@ def version(self) -> str:
         return self._server_conf.version
 
 
-def _extract_region_from_endpoint(endpoint):
-    """
-    Extract the region name from an STS endpoint URL.
-    for example: https://sts.eu-central-1.amazonaws.com/ -> eu-central-1
-    and for example: https://sts.amazonaws.com/ -> DEFAULT_REGION
-
-    :param endpoint: The endpoint URL of the STS client.
-    :return: The region name extracted from the endpoint URL.
-    """
-
-    parts = endpoint.split('.')
-    if len(parts) == 4:
-        return parts[1]
-    if len(parts) > 4:
-        return parts[2]
-    return DEFAULT_REGION
-
-
-def _get_identity_token(
-        session: boto3.Session,
-        lakefs_host: str,
-        additional_headers: dict[str, str],
-        presign_expiry
-) -> str:
-    """
-   Generate the identity token required for lakeFS authentication from an AWS session.
-
-   This function uses the STS client to generate a presigned URL for the `get_caller_identity` action,
-    extracts the required values from the URL,
-   and creates a base64-encoded JSON object with these values.
-
-   :param session: A boto3 session object with the necessary AWS credentials and region information.
-   :return: A base64-encoded JSON string containing the required authentication information.
-   :raises ValueError: If the session does not have a region name set.
-   """
-
-    # this method should only be called when installing the aws-iam additional requirement
-    from botocore.client import Config  # pylint: disable=import-outside-toplevel, import-error
-    from botocore.signers import RequestSigner  # pylint: disable=import-outside-toplevel, import-error
-
-    sts_client = session.client('sts', config=Config(signature_version='v4'))
-    endpoint = sts_client.meta.endpoint_url
-    service_id = sts_client.meta.service_model.service_id
-    region = _extract_region_from_endpoint(endpoint)
-    # signer is used because the presigned URL generated by the STS does not support additional headers
-    signer = RequestSigner(
-        service_id,
-        region,
-        'sts',
-        'v4',
-        session.get_credentials(),
-        session.events
-    )
-    endpoint_with_params = f"{endpoint}/?Action=GetCallerIdentity&Version=2011-06-15"
-    if additional_headers is None:
-        additional_headers = {
-            'X-LakeFS-Server-ID': lakefs_host,
-        }
-    params = {
-        'method': 'POST',
-        'url': endpoint_with_params,
-        'body': {},
-        'headers': additional_headers,
-        'context': {}
-    }
-
-    presigned_url = signer.generate_presigned_url(
-        params,
-        region_name=region,
-        expires_in=presign_expiry,
-        operation_name=''
-    )
-    parsed_url = urlparse(presigned_url)
-    query_params = parse_qs(parsed_url.query)
-
-    # Extract values from query parameters
-    json_object = {
-        "method": "POST",
-        "host": parsed_url.hostname,
-        "region": region,
-        "action": query_params['Action'][0],
-        "date": query_params['X-Amz-Date'][0],
-        "expiration_duration": query_params['X-Amz-Expires'][0],
-        "access_key_id": query_params['X-Amz-Credential'][0].split('/')[0],
-        "signature": query_params['X-Amz-Signature'][0],
-        "signed_headers": query_params.get('X-Amz-SignedHeaders', [''])[0].split(';'),
-        "version": query_params['Version'][0],
-        "algorithm": query_params['X-Amz-Algorithm'][0],
-        "security_token": query_params.get('X-Amz-Security-Token', [None])[0]
-    }
-
-    json_string = json.dumps(json_object)
-    return base64.b64encode(json_string.encode('utf-8')).decode('utf-8')
-
-
 def from_aws_role(
         session: boto3.Session,
         ttl_seconds: int = 3600,
@@ -243,22 +200,23 @@ def from_aws_role(
     :param kwargs: The arguments to pass to the client.
     :return: A lakeFS client.
     """
-
     client = Client(**kwargs)
     lakefs_host = urlparse(client.config.host).hostname
-    identity_token = _get_identity_token(session, lakefs_host, presign_expiry=presigned_ttl,
-                                         additional_headers=additional_headers)
-    external_login_information = ExternalLoginInformation(token_expiration_duration=ttl_seconds, identity_request={
-        "identity_token": identity_token
-    })
-
-    with api_exception_handler():
-        auth_token = client.sdk_client.auth_api.external_principal_login(external_login_information)
-
-    client.config.access_token = auth_token.token
+    aws_provider_pros = ClientConfig.AWSIAMProviderConfig(
+        token_ttl_seconds=ttl_seconds,
+        url_presign_ttl_seconds=presigned_ttl,
+        token_request_headers=additional_headers
+    )
+    access_token, reset_time = access_token_from_aws_iam_role(
+        client.sdk_client,
+        lakefs_host,
+        session,
+        aws_provider_pros
+    )
+    client.config.access_token = access_token
+    client.reset_time = reset_time
     return client
 
-
 def from_web_identity(code: str, state: str, redirect_uri: str, ttl_seconds: int = 3600, **kwargs) -> Client:
     """
     Authenticate against lakeFS using a code received from an identity provider
diff --git a/clients/python-wrapper/lakefs/config.py b/clients/python-wrapper/lakefs/config.py
index fd9d76f46c3..c2cb9e625ac 100644
--- a/clients/python-wrapper/lakefs/config.py
+++ b/clients/python-wrapper/lakefs/config.py
@@ -4,22 +4,42 @@
 
 from __future__ import annotations
 
+import dataclasses
+import json
 import os
+from enum import Enum
+from json import JSONDecodeError
 from pathlib import Path
-from typing import Optional
+from typing import Optional, Dict
 
 import yaml
 from lakefs_sdk import Configuration
-from lakefs.exceptions import NoAuthenticationFound
+from lakefs.exceptions import NoAuthenticationFound, UnsupportedCredentialsProviderType, InvalidEnvVarFormat
 from lakefs.namedtuple import LenientNamedTuple
 
 _LAKECTL_YAML_PATH = os.path.join(Path.home(), ".lakectl.yaml")
-_LAKECTL_ENDPOINT_ENV = "LAKECTL_SERVER_ENDPOINT_URL"
+_LAKECTL_ENDPOINT_URL_ENV = "LAKECTL_SERVER_ENDPOINT_URL"
 _LAKECTL_ACCESS_KEY_ID_ENV = "LAKECTL_CREDENTIALS_ACCESS_KEY_ID"
 _LAKECTL_SECRET_ACCESS_KEY_ENV = "LAKECTL_CREDENTIALS_SECRET_ACCESS_KEY"
 # lakefs access token, used for authentication when logging in with an IAM role
 _LAKECTL_CREDENTIALS_SESSION_TOKEN = "LAKECTL_CREDENTIALS_SESSION_TOKEN"
+_LAKECTL_CREDENTIALS_PROVIDER_TYPE = "LAKECTL_CREDENTIALS_PROVIDER_TYPE"
+_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS = "LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS"
+_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_PRESIGNED_URL_TTL_SECONDS = \
+    "LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_PRESIGNED_URL_TTL_SECONDS"
+_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS = \
+    "LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS"
 
+# Defaults
+_DEFAULT_IAM_TOKEN_TTL_SECONDS = 3600
+_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS = 60
+
+TOKEN_TTL_SECONDS_CONFIG = "token_ttl_seconds"
+URL_PRESIGN_TTL_SECONDS_CONFIG = "url_presign_ttl_seconds"
+TOKEN_REQUEST_HEADERS_CONFIG = "token_request_headers"
+
+AWS_IAM_PROVIDER_TYPE = "aws_iam"
+SUPPORTED_IAM_PROVIDERS = [AWS_IAM_PROVIDER_TYPE]
 
 class ClientConfig(Configuration):
     """
@@ -48,8 +68,44 @@ class Credentials(LenientNamedTuple):
         access_key_id: str
         secret_access_key: str
 
-    server: Server
-    credentials: Credentials
+    class AuthType(Enum):
+        """
+        Enum for the supported authentication types
+        """
+        SESSION_TOKEN = 1
+        CREDENTIALS = 2
+        IAM = 3
+
+    class ProviderType(Enum):
+        """
+        Enum for the supported authentication provider types
+        """
+        AWS_IAM = "aws_iam"
+        UNKNOWN = "unknown"
+
+    @dataclasses.dataclass
+    class AWSIAMProviderConfig:
+        """
+        lakectl configuration's credentials block
+        """
+        token_ttl_seconds: int
+        url_presign_ttl_seconds: int
+        token_request_headers: Optional[Dict]
+
+    @dataclasses.dataclass
+    class IAMProvider:
+        """
+        An IAM authentication provider
+        """
+        type: ClientConfig.ProviderType
+        aws_iam: Optional[ClientConfig.AWSIAMProviderConfig]
+
+    server = Server(endpoint_url="")
+    credentials = Credentials(access_key_id="", secret_access_key="")
+    username = None
+    password = None
+    access_token = None
+    _iam_provider = None
 
     def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = None, **kwargs):
         super().__init__(**kwargs)
@@ -60,35 +116,201 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
-                data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                config_data = yaml.load(fd, Loader=yaml.Loader)
+                if "server" in config_data:
+                    self.server = ClientConfig.Server(**config_data["server"])
+                if "credentials" in config_data:
+                    self.credentials = ClientConfig.Credentials(**config_data["credentials"])
+                    self.username = self.credentials.access_key_id
+                    self.password = self.credentials.secret_access_key
+
+                if self.username is None or self.password is None:
+                    self._set_iam_provider_from_config_file(config_data)
+        except FileNotFoundError:
+            pass
+
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
+        # Session token takes precedence over basic credentials and IAM provider. If specified, set it and override
+        # all others. Currently, session token setting is only available through environment variables.
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self.credentials = None
+            self._iam_provider = None
+            return
+        # Credentials take precedence over IAM provider. If specified, set it and override the IAM provider
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+            self._iam_provider = None
+            return
+        # If no other method was specified, try to set IAM provider from env vars
+        if self.username is None or self.password is None:
+            self._set_iam_provider_from_env_vars()
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
 
-        if not found:
-            raise NoAuthenticationFound
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
+
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        ORDER MATTERS! SessionToken > Credentials > IAMProvider. self._iam_provider will be none if Session Token auth
+        is used. self.access_token will be populated for both Session Token and IAMProvider auth, therefore it's tested
+        after self._iam_provider.
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
+
+    @property
+    def iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        return self._iam_provider
+
+    def _set_iam_provider_from_config_file(self, config_data: Dict):
+        """
+        Set the IAM provider from the configuration file.
+        """
+        provider_type = _get_provider_type_from_config_file(config_data)
+        if provider_type is None:
+            self._iam_provider = None
+        elif provider_type not in SUPPORTED_IAM_PROVIDERS:
+            raise UnsupportedCredentialsProviderType(provider_type)
+        else:
+            provider_config = _get_provider_config_from_config_data(config_data, provider_type)
+            if provider_config is not None:
+                if provider_type == AWS_IAM_PROVIDER_TYPE:
+                    aws_iam_provider_config = _generate_aws_iam_provider_config(provider_config)
+                    self._iam_provider = ClientConfig.IAMProvider(
+                        type=ClientConfig.ProviderType.AWS_IAM,
+                        aws_iam=aws_iam_provider_config
+                    )
+
+    def _set_iam_provider_from_env_vars(self):
+        """
+        Set the IAM provider from environment variables.
+        """
+        provider_type = _get_iam_provider_type_from_env_vars()
+        if provider_type is None:
+            return
+        if provider_type == AWS_IAM_PROVIDER_TYPE:
+            if self._iam_provider is None:
+                self._iam_provider = ClientConfig.IAMProvider(
+                    type=ClientConfig.ProviderType.AWS_IAM,
+                    aws_iam=ClientConfig.AWSIAMProviderConfig(
+                        token_ttl_seconds=_DEFAULT_IAM_TOKEN_TTL_SECONDS,
+                        url_presign_ttl_seconds=_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS,
+                        token_request_headers=None
+                    )
+                )
+            env_token_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS,
+                                      self._iam_provider.aws_iam.token_ttl_seconds)
+            self._iam_provider.aws_iam.token_ttl_seconds = int(env_token_ttl)
+            env_presign_url_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_PRESIGNED_URL_TTL_SECONDS,
+                                            self._iam_provider.aws_iam.url_presign_ttl_seconds)
+            self._iam_provider.aws_iam.url_presign_ttl_seconds= int(env_presign_url_ttl)
+            env_headers = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS, None)
+            if env_headers is not None:
+                try:
+                    token_request_headers = json.loads(env_headers)
+                    self._iam_provider.aws_iam.token_request_headers = token_request_headers
+                except JSONDecodeError as e:
+                    raise InvalidEnvVarFormat(
+                        f"Invalid format for {env_headers} environment variable. Expected JSON format."
+                    ) from e
+        else:
+            raise UnsupportedCredentialsProviderType(provider_type)
+
+def _get_provider_type_from_config_file(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    if data is not None:
+        try:
+            return data['credentials']['provider']['type']
+        except KeyError:
+            return None
+    return None
+
+def _get_iam_provider_type_from_env_vars() -> Optional[str]:
+    """
+    Get IAM provider configuration from environment variables.
+
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type is not None and provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+    return provider_type
+
+
+def _safe_int_or_default(value: Optional[str], default: int) -> int:
+    """
+    Safely convert a value to an int, returning a default if conversion fails.
+    """
+    try:
+        return int(value)
+    except (ValueError, TypeError):
+        return default
+
+def _get_provider_config_from_config_data(data: Optional[Dict], provider: str) -> Optional[Dict]:
+    if data is not None:
+        try:
+            return data['credentials']['provider'][provider]
+        except KeyError:
+            return None
+    return None
+
+def _generate_aws_iam_provider_config(aws_config: Dict) -> ClientConfig.AWSIAMProviderConfig:
+    token_ttl_seconds = _safe_int_or_default(aws_config.get(TOKEN_TTL_SECONDS_CONFIG), _DEFAULT_IAM_TOKEN_TTL_SECONDS)
+    url_presign_ttl_seconds = _safe_int_or_default(
+        aws_config.get(URL_PRESIGN_TTL_SECONDS_CONFIG), _DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS)
+    token_request_headers = None
+    if TOKEN_REQUEST_HEADERS_CONFIG in aws_config:
+        token_request_headers = aws_config[TOKEN_REQUEST_HEADERS_CONFIG]
+    return ClientConfig.AWSIAMProviderConfig(
+            token_ttl_seconds=token_ttl_seconds,
+            url_presign_ttl_seconds=url_presign_ttl_seconds,
+            token_request_headers=token_request_headers
+    )
diff --git a/clients/python-wrapper/lakefs/exceptions.py b/clients/python-wrapper/lakefs/exceptions.py
index ceb5722767b..f921ce9138e 100644
--- a/clients/python-wrapper/lakefs/exceptions.py
+++ b/clients/python-wrapper/lakefs/exceptions.py
@@ -56,6 +56,15 @@ class NoAuthenticationFound(LakeFSException):
     Raised when no authentication method could be found on Client instantiation
     """
 
+class UnsupportedCredentialsProviderType(LakeFSException):
+    """
+    Raised when the credentials provider type is not supported
+    """
+
+class InvalidEnvVarFormat(LakeFSException):
+    """
+    Raised when the passed env var is not of expected format
+    """
 
 class BadRequestException(ServerException):
     """
diff --git a/clients/python-wrapper/requirements.txt b/clients/python-wrapper/requirements.txt
index a3bdcf2362a..8d6120294a6 100644
--- a/clients/python-wrapper/requirements.txt
+++ b/clients/python-wrapper/requirements.txt
@@ -13,4 +13,4 @@ pandas~=2.1.4
 pyarrow~=14.0.1
 pillow~=10.3.0
 setuptools>=70.0.0
-boto3>=1.26.0
\ No newline at end of file
+boto3>=1.26.0
diff --git a/clients/python-wrapper/setup.py b/clients/python-wrapper/setup.py
index 26661c46e1a..1007a0d1b1b 100644
--- a/clients/python-wrapper/setup.py
+++ b/clients/python-wrapper/setup.py
@@ -1,4 +1,8 @@
-from setuptools import setup, find_packages
+from typing import Any
+
+from setuptools import setup, find_packages, Command, Distribution, glob
+import os
+import shutil
 
 NAME = "lakefs"
 VERSION = "0.10.0"
@@ -24,7 +28,6 @@
 
 with open('README.md') as f:
     long_description = f.read()
-
 setup(
     name=NAME,
     version=VERSION,
@@ -42,6 +45,7 @@
     long_description=long_description,
     long_description_content_type='text/markdown',
     extras_require={
+        # Install using: `pip install "lakefs-<version>-py3-none-any.whl[all,aws-iam]"`
         "all": ["boto3 >= 1.26.0"],
         "aws-iam": ["boto3 >= 1.26.0"],
     },
diff --git a/clients/python-wrapper/tests/utests/test_auth.py b/clients/python-wrapper/tests/utests/test_auth.py
new file mode 100644
index 00000000000..d6bba50d62d
--- /dev/null
+++ b/clients/python-wrapper/tests/utests/test_auth.py
@@ -0,0 +1,362 @@
+import datetime
+from unittest.mock import MagicMock, patch, PropertyMock
+
+import sys
+from lakefs_sdk import AuthenticationToken
+from pydantic import StrictInt, StrictStr
+
+# Mock boto3 if it's not installed
+try:
+    import boto3
+except ImportError:
+    boto3 = MagicMock()
+    sys.modules['boto3'] = boto3
+
+from lakefs.config import ClientConfig
+from lakefs.exceptions import NoAuthenticationFound
+from tests.utests.common import (
+    lakectl_test_config_context,
+    lakectl_no_config_context,
+    env_var_context,
+    expect_exception_context,
+    TEST_SERVER, TEST_ENDPOINT_PATH,
+)
+
+MOCK_PRESIGNED_URL = """
+https://my-bucket.s3.amazonaws.com/my-object.txt?
+Action=GetObject&
+Version=2012-10-17&
+X-Amz-Algorithm=AWS4-HMAC-SHA256&
+X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20250506%2Fus-east-1%2Fs3%2Faws4_request&
+X-Amz-Date=20250506T114100Z&
+X-Amz-Expires=3600&
+X-Amz-SignedHeaders=host&
+X-Amz-Signature=abcd1234example5678
+"""
+
+class TestAuthenticationFlow:
+    """Test suite for authentication flow in lakeFS client"""
+
+    def test_client_auth_type_detection(self, monkeypatch, tmp_path):
+        """Test that client correctly detects authentication type"""
+        with lakectl_test_config_context(monkeypatch, tmp_path) as client:
+            # Test with credentials
+            clt = client.Client(username="user", password="pass")
+            assert clt.config.get_auth_type() == ClientConfig.AuthType.CREDENTIALS
+
+            # Test with session token
+            clt = client.Client(access_token="token123")
+            assert clt.config.get_auth_type() == ClientConfig.AuthType.SESSION_TOKEN
+
+            # Test with IAM provider
+            iam_provider = ClientConfig.IAMProvider(
+                type=ClientConfig.ProviderType.AWS_IAM,
+                aws_iam=ClientConfig.AWSIAMProviderConfig(
+                    token_ttl_seconds=3600,
+                    url_presign_ttl_seconds=60,
+                    token_request_headers=None
+                )
+            )
+            clt = client.Client()
+            clt.config._iam_provider = iam_provider
+            assert clt.config.get_auth_type() == ClientConfig.AuthType.IAM
+
+    @patch('lakefs.auth.access_token_from_aws_iam_role')
+    def test_client_iam_auth_initialization(self, mock_access_token, monkeypatch, tmp_path):
+        """Test client initialization with IAM auth"""
+        # Mock response from access_token_from_aws_iam_role
+        mock_token = "iam_token_123"
+        reset_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)
+        mock_access_token.return_value = (mock_token, reset_time)
+
+        with lakectl_test_config_context(monkeypatch, tmp_path) as client_module:
+            # Set up IAM provider configuration
+            with patch.object(ClientConfig, 'get_auth_type', return_value=ClientConfig.AuthType.IAM):
+                with patch('lakefs.client.ClientConfig.iam_provider', new_callable=PropertyMock) as mock_iam_provider:
+                    mock_iam_provider.return_value = ClientConfig.IAMProvider(
+                        type=ClientConfig.ProviderType.AWS_IAM,
+                        aws_iam=ClientConfig.AWSIAMProviderConfig(
+                            token_ttl_seconds=3600,
+                            url_presign_ttl_seconds=60,
+                            token_request_headers=None
+                        )
+                    )
+                    with patch('boto3.Session'):
+                        # Initialize client with IAM authentication
+                        clt = client_module.Client(host=TEST_SERVER)
+
+                        # Verify that access_token_from_aws_iam_role was called
+                        mock_access_token.assert_called_once()
+
+                        # Verify token was set properly
+                        assert clt._conf.access_token == mock_token
+                        assert clt._reset_token_time == reset_time
+
+    @patch('lakefs.auth.access_token_from_aws_iam_role')
+    def test_token_refresh_on_expiry(self, mock_access_token, monkeypatch, tmp_path):
+        """Test token refresh when token expires"""
+        # First token and expiry time
+        initial_token = "initial_token"
+        initial_expiry = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=5)  # Already expired
+
+        # Second token and expiry time (for refresh)
+        refreshed_token = "refreshed_token"
+        refreshed_expiry = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)
+
+        # Mock to return different values on successive calls
+        mock_access_token.side_effect = [(initial_token, initial_expiry), (refreshed_token, refreshed_expiry)]
+
+        with lakectl_test_config_context(monkeypatch, tmp_path) as client_module:
+            # Set up for IAM provider
+            with patch.object(ClientConfig, 'get_auth_type', return_value=ClientConfig.AuthType.IAM):
+                with patch('lakefs.client.ClientConfig.iam_provider', new_callable=PropertyMock) as mock_iam_provider:
+                    mock_iam_provider.return_value = ClientConfig.IAMProvider(
+                        type=ClientConfig.ProviderType.AWS_IAM,
+                        aws_iam=ClientConfig.AWSIAMProviderConfig(
+                            token_ttl_seconds=3600,
+                            url_presign_ttl_seconds=60,
+                            token_request_headers=None
+                        )
+                    )
+                    with patch('boto3.Session'):
+                        # Initialize client
+                        clt = client_module.Client(host=TEST_SERVER)
+                        # First token setup
+                        assert clt.config.access_token == initial_token
+                        assert clt._reset_token_time == initial_expiry
+
+                        # This should trigger a token refresh since the token is expired
+                        _ = clt.sdk_client
+
+                        # Verify token was refreshed
+                        assert clt.config.access_token == refreshed_token
+                        assert clt._reset_token_time == refreshed_expiry
+
+    def test_no_token_refresh_for_non_iam_auth(self, monkeypatch, tmp_path):
+        """Test that token refresh is not triggered for non-IAM auth methods"""
+        with lakectl_test_config_context(monkeypatch, tmp_path) as client_module:
+            # Create client with session token auth
+            with patch.object(ClientConfig, 'get_auth_type', return_value=ClientConfig.AuthType.SESSION_TOKEN):
+                clt = client_module.Client(access_token="static_token")
+
+                # Mock _reset_token_time to appear expired
+                expired_time = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(minutes=5)
+                clt._reset_token_time = expired_time
+
+                # This should NOT trigger a refresh for SESSION_TOKEN auth
+                with patch('lakefs.auth.access_token_from_aws_iam_role') as mock_access_token:
+                    _ = clt.sdk_client
+                    mock_access_token.assert_not_called()
+
+    @patch('lakefs.auth.access_token_from_aws_iam_role')
+    @patch('botocore.signers.RequestSigner.generate_presigned_url')
+    def test_from_aws_role_factory_method(self, mock_request_signer, mock_access_token, monkeypatch, tmp_path):
+        """Test the from_aws_role factory method for creating client"""
+        with patch('lakefs_sdk.api.auth_api.AuthApi.external_principal_login') as mock_external_principal_login:
+            test_token = "aws_role_token"
+            test_reset_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)
+            mock_access_token.return_value = (test_token, test_reset_time)
+
+            mock_request_signer.return_value = MOCK_PRESIGNED_URL
+            mock_external_principal_login.return_value = (
+                AuthenticationToken(token=StrictStr("test_token"), token_expiration=StrictInt(1))
+            )
+            with lakectl_test_config_context(monkeypatch, tmp_path) as client_module:
+                # Create mock session
+                mock_session = MagicMock(spec=boto3.Session)
+
+                # Create client using factory method
+                clt = client_module.from_aws_role(
+                    session=mock_session,
+                    ttl_seconds=7200,  # Custom TTL
+                    presigned_ttl=120,  # Custom presigned TTL
+                    additional_headers={"X-Custom-Header": "value"},
+                    host=TEST_SERVER
+                )
+
+                # Verify client was created with correct parameters
+                mock_access_token.assert_called_once()
+                args, kwargs = mock_access_token.call_args
+
+                # Verify the AWS provider config was passed correctly
+                aws_provider = kwargs.get('aws_provider_auth_params', None)
+                if not aws_provider:
+                    aws_provider = args[3]  # Fallback to positional args
+
+                assert aws_provider.token_ttl_seconds == 7200
+                assert aws_provider.url_presign_ttl_seconds == 120
+                assert aws_provider.token_request_headers == {"X-Custom-Header": "value"}
+
+                # Verify token was set in client
+                assert clt.config.access_token == test_token
+                assert clt._reset_token_time == test_reset_time
+
+    def test_environment_variables_auth(self, monkeypatch):
+        """Test authentication using environment variables"""
+        with lakectl_no_config_context(monkeypatch):
+            with env_var_context():
+                # Set environment variables
+                monkeypatch.setenv("LAKECTL_SERVER_ENDPOINT_URL", TEST_SERVER)
+                monkeypatch.setenv("LAKECTL_CREDENTIALS_ACCESS_KEY_ID", "env_access_key")
+                monkeypatch.setenv("LAKECTL_CREDENTIALS_SECRET_ACCESS_KEY", "env_secret_key")
+
+                from lakefs import client
+                # Create client using environment vars
+                clt = client.Client()
+
+                # Verify credentials from environment
+                assert clt.config.host == TEST_SERVER + TEST_ENDPOINT_PATH
+                assert clt.config.username == "env_access_key"
+                assert clt.config.password == "env_secret_key"
+                assert clt.config.get_auth_type() == ClientConfig.AuthType.CREDENTIALS
+
+    def test_environment_variables_with_session_token(self, monkeypatch):
+        """Test authentication using environment variables with session token"""
+        with lakectl_no_config_context(monkeypatch):
+            with env_var_context():
+                # Set environment variables
+                monkeypatch.setenv("LAKECTL_SERVER_ENDPOINT_URL", TEST_SERVER)
+                monkeypatch.setenv("LAKECTL_CREDENTIALS_SESSION_TOKEN", "env_session_token")
+
+                from lakefs import client
+                # Create client using environment vars
+                clt = client.Client()
+
+                # Verify session token from environment
+                assert clt.config.host == TEST_SERVER + TEST_ENDPOINT_PATH
+                assert clt.config.access_token == "env_session_token"
+                assert clt.config.get_auth_type() == ClientConfig.AuthType.SESSION_TOKEN
+
+    @patch('botocore.signers.RequestSigner.generate_presigned_url')
+    def test_environment_variables_iam_provider(self, mock_request_signer, monkeypatch):
+        """Test authentication using environment variables with IAM provider"""
+        # Clear any cached modules or state
+        if 'lakefs.client' in sys.modules:
+            monkeypatch.delattr(sys.modules['lakefs.client'], '_client_instance', raising=False)
+        mock_request_signer.return_value = MOCK_PRESIGNED_URL
+        # Use context manager for all patches to ensure proper cleanup
+        with lakectl_no_config_context(monkeypatch):
+            with env_var_context():
+                # Set environment variables with properly formatted data
+                monkeypatch.setenv("LAKECTL_SERVER_ENDPOINT_URL", TEST_SERVER)
+                monkeypatch.setenv("LAKECTL_CREDENTIALS_PROVIDER_TYPE", "aws_iam")
+                monkeypatch.setenv("LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS", "7200")
+                monkeypatch.setenv("LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_URL_PRESIGN_TTL_SECONDS", "60")
+
+                # Create independent patches with context managers
+                with patch('lakefs_sdk.api.auth_api.AuthApi.external_principal_login') as mock_external_principal_login:
+                    # Set up mock return value
+                    test_token = "env_iam_token"
+                    test_reset_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=2)
+                    mock_external_principal_login.return_value = AuthenticationToken(
+                        token=StrictStr(test_token),
+                        token_expiration=StrictInt(int(test_reset_time.timestamp()))
+                    )
+
+                    # Patch boto3 before importing client
+                    with patch('boto3.Session'):
+                        # Dynamic import to ensure we get a fresh instance
+                        import importlib
+                        import lakefs.client
+                        importlib.reload(lakefs.client)
+                        from lakefs.client import Client
+
+                        # Create client - should trigger external_principal_login
+                        clt = Client()
+
+                        # Verify the mock was called
+                        mock_external_principal_login.assert_called_once()
+
+                        # Verify the token was set
+                        assert clt.config.access_token == test_token
+
+    @patch('lakefs.auth.access_token_from_aws_iam_role')
+    def test_token_request_headers(self, mock_access_token, monkeypatch, tmp_path):
+        """Test that token request headers are passed correctly"""
+        test_token = "token_with_headers"
+        test_reset_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=1)
+        mock_access_token.return_value = (test_token, test_reset_time)
+
+        with lakectl_test_config_context(monkeypatch, tmp_path) as client_module:
+            # Custom headers
+            headers = {"X-Custom-1": "value1", "X-Custom-2": "value2"}
+
+            # Create client using factory method
+            mock_session = MagicMock(spec=boto3.Session)
+            client_module.from_aws_role(
+                session=mock_session,
+                additional_headers=headers,
+                host=TEST_SERVER
+            )
+
+            # Verify headers were passed to access_token_from_aws_iam_role
+            mock_access_token.assert_called_once()
+            args, kwargs = mock_access_token.call_args
+
+            # Check provider config contains headers
+            aws_provider = kwargs.get('aws_provider_auth_params', None)
+            if not aws_provider:
+                aws_provider = args[3]  # Fallback to positional args
+
+            assert aws_provider.token_request_headers == headers
+
+    def test_handling_missing_authentication(self, monkeypatch):
+        """Test proper error handling when no authentication method is available"""
+        with lakectl_no_config_context(monkeypatch):
+            with env_var_context():
+                # No environment variables set
+
+                from lakefs import client
+                # Attempt to create client without authentication
+                with expect_exception_context(NoAuthenticationFound):
+                    client.Client()
+
+    def test_region_extraction(self, monkeypatch):
+        """Test region extraction from STS endpoint"""
+        from lakefs.auth import _extract_region_from_endpoint
+
+        # Test standard regional endpoint
+        assert _extract_region_from_endpoint("https://sts.eu-central-1.amazonaws.com/") == "eu-central-1"
+
+        # Test global endpoint
+        assert _extract_region_from_endpoint("https://sts.amazonaws.com/") == "us-east-1"
+
+        # Test custom endpoint with more segments
+        assert _extract_region_from_endpoint("https://prefix.sts.us-west-2.amazonaws.com/") == "us-west-2"
+
+    @patch('lakefs.client.access_token_from_aws_iam_role')
+    def test_from_web_identity(self, mock_access_token, monkeypatch, tmp_path):
+        """Test creating client from web identity token"""
+        with lakectl_test_config_context(monkeypatch, tmp_path) as client_module:
+            # Mock web identity login
+            mock_client = MagicMock()
+            mock_auth_token = MagicMock()
+            mock_auth_token.token = "web_identity_token"
+            mock_client.experimental_api.sts_login.return_value = mock_auth_token
+
+            # Mock Client creation
+            with patch.object(client_module, 'Client') as mock_client_class:
+                mock_client_instance = MagicMock()
+                mock_client_instance.sdk_client = mock_client
+                mock_client_class.return_value = mock_client_instance
+
+                # Call from_web_identity
+                client_module.from_web_identity(
+                    code="auth_code",
+                    state="state_token",
+                    redirect_uri="https://redirect.example.com",
+                    ttl_seconds=3600,
+                    host=TEST_SERVER
+                )
+
+                # Verify sts_login was called with correct parameters
+                mock_client.experimental_api.sts_login.assert_called_once()
+                args, _ = mock_client.experimental_api.sts_login.call_args
+                sts_request = args[0]
+                assert sts_request.code == "auth_code"
+                assert sts_request.state == "state_token"
+                assert sts_request.redirect_uri == "https://redirect.example.com"
+                assert sts_request.ttl_seconds == 3600
+
+                # Verify token was set in client
+                assert mock_client_instance.config.access_token == "web_identity_token"
diff --git a/clients/python-wrapper/tests/utests/test_repository.py b/clients/python-wrapper/tests/utests/test_repository.py
index 442b3261688..a11e74856a1 100644
--- a/clients/python-wrapper/tests/utests/test_repository.py
+++ b/clients/python-wrapper/tests/utests/test_repository.py
@@ -124,7 +124,7 @@ def test_create_repository_no_authentication(monkeypatch):
         with env_var_context():
             from lakefs import config as client_config
             # Create a new client with env vars
-            os.environ[client_config._LAKECTL_ENDPOINT_ENV] = "endpoint"
+            os.environ[client_config._LAKECTL_ENDPOINT_URL_ENV] = "endpoint"
             os.environ[client_config._LAKECTL_SECRET_ACCESS_KEY_ENV] = "secret"
             os.environ[client_config._LAKECTL_ACCESS_KEY_ID_ENV] = "key"
             repo.create(storage_namespace=TEST_REPO_ARGS.storage_namespace,


Reviews:
user: login: N-o-Z
body: Thanks, 
I stopped the review pretty much in the beginning.
The main and biggest issue is with the addition of the boto package as a dependency to the lakefs package.
The HL Python SDK is supposed to be an alternative to using boto3 and therefore putting it as a dependency in the package defeats the purpose.
That is the reason why originally the boto3 package was not directly imported into lakeFS and we added a custom installation configuration for the AWS IAM role flow (see setup.py and client.py).
There are ways to achieve what you want to without explicitly importing the boto3 package into lakefs and breaking this principle
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-12T15:22:51Z
user: login: Jonathan-Rosenberg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:02:52Z
user: login: Jonathan-Rosenberg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:03:51Z
user: login: Jonathan-Rosenberg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:03:59Z
user: login: N-o-Z
body: I think we can start with this 😃 
Some general comments:
* I think it's great you split the config init logic to provide more cleaner approach. However I felt like the way you split it was more confusing to me.
* Adding additional unittest package for testing instead of using the current test infra (pytest and monkeypatch functionality)
* Could have split the changes in the setup.py to a different PR. They are not required for the task and this could have helped reduce the scope and allow to focus on the feature changes
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-13T18:37:58Z
user: login: Jonathan-Rosenberg
body: PTALNOZ
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T11:50:51Z
user: login: N-o-Z
body: Approving - please address the comments.

Also - my intention is that we don't need to use the unittest package at all.
Anything you can do with that (patch, MagicMock) you can do with monkey patching
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-15T20:05:59Z
user: login: Jonathan-Rosenberg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T08:44:38Z

Review comments:
path: clients/python-wrapper/lakefs/auth_utils.py
user: login: N-o-Z
body: suggest to call module name simply `auth` instead of `auth_utils`
created_at: 2025-05-12T15:16:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,143 @@
+"""
+lakeFS Authentication Utils Module
+
+Includes authentication util functions
+"""
+
+import base64
+import datetime
+import json
+from typing import Any, Optional, Dict
+from urllib.parse import urlparse, parse_qs
+
+import boto3
+from boto3 import Session
path: clients/python-wrapper/lakefs/auth_utils.py
user: login: N-o-Z
body: Please don't import boto3 into the lakefs package. We don't want it to be part of the lakefs package dependency.
See example in the client module of how we did it
created_at: 2025-05-12T15:17:47Z
author_association: MEMBER
diff_hunk: @@ -6,25 +6,25 @@
 
 from __future__ import annotations
 
-import base64
-import json
+import datetime
 from threading import Lock
 from typing import Optional
 from typing import TYPE_CHECKING
-from urllib.parse import urlparse, parse_qs
+from urllib.parse import urlparse
+from boto3 import Session
path: clients/python-wrapper/lakefs/client.py
user: login: N-o-Z
body: See previous comment in auth_utils
created_at: 2025-05-12T15:18:38Z
author_association: MEMBER
path: clients/python-wrapper/lakefs/auth_utils.py
user: login: Jonathan-Rosenberg
body: suggestion accepted
created_at: 2025-05-13T12:02:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,25 +6,25 @@
 
 from __future__ import annotations
 
-import base64
-import json
+import datetime
 from threading import Lock
 from typing import Optional
 from typing import TYPE_CHECKING
-from urllib.parse import urlparse, parse_qs
+from urllib.parse import urlparse
+from boto3 import Session
path: clients/python-wrapper/lakefs/client.py
user: login: Jonathan-Rosenberg
body: done
created_at: 2025-05-13T15:03:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,143 @@
+"""
+lakeFS Authentication Utils Module
+
+Includes authentication util functions
+"""
+
+import base64
+import datetime
+import json
+from typing import Any, Optional, Dict
+from urllib.parse import urlparse, parse_qs
+
+import boto3
+from boto3 import Session
path: clients/python-wrapper/lakefs/auth_utils.py
user: login: Jonathan-Rosenberg
body: done
created_at: 2025-05-13T15:03:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,8 +125,12 @@
     license="Apache 2.0",
     long_description=long_description,
     long_description_content_type='text/markdown',
+    # Install using: `pip install "lakefs-<version>-py3-none-any.whl[all,aws-iam]"` (or just one of them)
path: clients/python-wrapper/setup.py
user: login: N-o-Z
body: or none of them
created_at: 2025-05-13T17:04:06Z
author_association: MEMBER
diff_hunk: @@ -25,6 +29,86 @@
 with open('README.md') as f:
     long_description = f.read()
 
+# to use the `clean` command, run: python -m build clean --all
+class CleanCommand(Command):
path: clients/python-wrapper/setup.py
user: login: N-o-Z
body: Not sure why we need all of this code and all these clean options?
created_at: 2025-05-13T17:04:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,144 @@
+"""
+lakeFS Authentication Utils Module
path: clients/python-wrapper/lakefs/auth.py
user: login: N-o-Z
body: ```suggestion
lakeFS Authentication Module
```
created_at: 2025-05-13T17:08:03Z
author_association: MEMBER
diff_hunk: @@ -30,6 +42,7 @@ class ClientConfig(Configuration):
     2. Use LAKECTL_SERVER_ENDPOINT_URL, LAKECTL_ACCESS_KEY_ID and LAKECTL_ACCESS_SECRET_KEY if set
     3. Try to read ~/.lakectl.yaml if exists
     4. Use IAM role from current machine (using AWS IAM role will work only with enterprise/cloud)
+    5. If the credentials provider type is set to aws_iam, use the credentials from the machine's AWS profile
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: I can't really understand the difference between 4 and 5. Can you explain?
created_at: 2025-05-13T17:49:44Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: You should be very careful in python with these statements. They can mean different things
```suggestion
    if data is not None and data.get("credentials", {}).get("provider", {}).get("type"):
```
created_at: 2025-05-13T17:53:35Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
+    """Build AWS IAM provider configuration from environment and config file."""
+    env_token_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS)
+    env_url_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_URL_PRESIGN_TTL_SECONDS)
+    env_headers = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS)
+
+    # Default values
+    token_ttl_seconds = int(_DEFAULT_IAM_TOKEN_TTL_SECONDS)
+    url_presign_ttl_seconds = int(_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS)
+    token_request_headers = None
+
+    if (data and "credentials" in data and
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: Same as previous comment
```suggestion
    if (data is not None and "credentials" in data and
```
created_at: 2025-05-13T17:54:12Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
+    """Build AWS IAM provider configuration from environment and config file."""
+    env_token_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS)
+    env_url_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_URL_PRESIGN_TTL_SECONDS)
+    env_headers = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS)
+
+    # Default values
+    token_ttl_seconds = int(_DEFAULT_IAM_TOKEN_TTL_SECONDS)
+    url_presign_ttl_seconds = int(_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS)
+    token_request_headers = None
+
+    if (data and "credentials" in data and
+            "provider" in data["credentials"] and
+            "aws_iam" in data["credentials"]["provider"]):
+        file_config = data["credentials"]["provider"]["aws_iam"]
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: Use [this](https://stackoverflow.com/a/43491315) approach and you wont have to deal with the file_config check
created_at: 2025-05-13T17:57:12Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
+    """Build AWS IAM provider configuration from environment and config file."""
+    env_token_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS)
+    env_url_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_URL_PRESIGN_TTL_SECONDS)
+    env_headers = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS)
+
+    # Default values
+    token_ttl_seconds = int(_DEFAULT_IAM_TOKEN_TTL_SECONDS)
+    url_presign_ttl_seconds = int(_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS)
+    token_request_headers = None
+
+    if (data and "credentials" in data and
+            "provider" in data["credentials"] and
+            "aws_iam" in data["credentials"]["provider"]):
+        file_config = data["credentials"]["provider"]["aws_iam"]
+        if file_config:
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: `is not None`
created_at: 2025-05-13T17:57:22Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
+    """Build AWS IAM provider configuration from environment and config file."""
+    env_token_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS)
+    env_url_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_URL_PRESIGN_TTL_SECONDS)
+    env_headers = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS)
+
+    # Default values
+    token_ttl_seconds = int(_DEFAULT_IAM_TOKEN_TTL_SECONDS)
+    url_presign_ttl_seconds = int(_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS)
+    token_request_headers = None
+
+    if (data and "credentials" in data and
+            "provider" in data["credentials"] and
+            "aws_iam" in data["credentials"]["provider"]):
+        file_config = data["credentials"]["provider"]["aws_iam"]
+        if file_config:
+            token_ttl_seconds = _safe_int_or_default(file_config.get("token_ttl_seconds"), token_ttl_seconds)
+            url_presign_ttl_seconds = _safe_int_or_default(
+                file_config.get("url_presign_ttl_seconds"), url_presign_ttl_seconds)
+            if "token_request_headers" in file_config:
+                token_request_headers = file_config["token_request_headers"]
+
+    # Environment variables override
+    token_ttl_seconds = _safe_int_or_default(env_token_ttl, token_ttl_seconds)
+    url_presign_ttl_seconds = _safe_int_or_default(env_url_ttl, url_presign_ttl_seconds)
+
+    if env_headers is not None:
+        token_request_headers = env_headers
+
+    return ClientConfig.AWSIAMProviderConfig(
+        token_ttl_seconds=token_ttl_seconds,
+        url_presign_ttl_seconds=url_presign_ttl_seconds,
+        token_request_headers=token_request_headers
+    )
+
+def _safe_int_or_default(value: Optional[str], default: int) -> int:
+    if value is None:
+        return default
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: You're already catching TypeError, this check is redundant (also please document)
created_at: 2025-05-13T17:59:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,144 @@
+"""
+lakeFS Authentication Utils Module
+
+Includes authentication util functions
+"""
+
+import base64
+import datetime
+import json
+from typing import Any, Optional, TYPE_CHECKING
+from urllib.parse import urlparse, parse_qs
+
+from lakefs_sdk import ExternalLoginInformation
+from lakefs_sdk.client import LakeFSClient
+
+from lakefs.config import ClientConfig
+from lakefs.exceptions import api_exception_handler
+
+DEFAULT_AWS_REGION = "us-east-1"
+
+if TYPE_CHECKING:
+    import boto3
+
+def access_token_from_aws_iam_role(sdk_client: LakeFSClient,
+                                   lakefs_host: str,
+                                   boto3_session: "boto3.Session",
+                                   aws_provider_auth_params: ClientConfig.AWSIAMProviderConfig) -> tuple[Any, datetime]:
+    """
+    Generate an access token for lakeFS authentication using AWS IAM role.
+    :param sdk_client: LakeFSClient
+    :param lakefs_host: LakeFS API URL
+    :param boto3_session: Session
+    :param aws_provider_auth_params: ClientConfig.AWSIAMProviderConfig
+    :return: An access token for lakeFS authentication.
+    """
+    presigned_ttl = int(aws_provider_auth_params.url_presign_ttl_seconds)
+    token_ttl_seconds = int(aws_provider_auth_params.token_ttl_seconds)
path: clients/python-wrapper/lakefs/auth.py
user: login: N-o-Z
body: Why are you casting? Its already an int
created_at: 2025-05-13T18:00:21Z
author_association: MEMBER
diff_hunk: @@ -48,8 +61,42 @@ class Credentials(LenientNamedTuple):
         access_key_id: str
         secret_access_key: str
 
-    server: Server
-    credentials: Credentials
+    class AuthType(Enum):
+        """
+        Enum for the supported authentication types
+        """
+        SESSION_TOKEN = 1
+        CREDENTIALS = 2
+        IAM = 3
+
+    class ProviderType(Enum):
+        """
+        Enum for the supported authentication provider types
+        """
+        AWS_IAM = "aws_iam"
+        UNKNOWN = "unknown"
+
+    class AWSIAMProviderConfig(LenientNamedTuple):
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: Any reason why this is a `LenientNamedTuple`?
This class is used to be able to initialize structs using `**kwargs` without having to worry about it failing due to unknown keys
created_at: 2025-05-13T18:01:26Z
author_association: MEMBER
diff_hunk: @@ -48,8 +61,42 @@ class Credentials(LenientNamedTuple):
         access_key_id: str
         secret_access_key: str
 
-    server: Server
-    credentials: Credentials
+    class AuthType(Enum):
+        """
+        Enum for the supported authentication types
+        """
+        SESSION_TOKEN = 1
+        CREDENTIALS = 2
+        IAM = 3
+
+    class ProviderType(Enum):
+        """
+        Enum for the supported authentication provider types
+        """
+        AWS_IAM = "aws_iam"
+        UNKNOWN = "unknown"
+
+    class AWSIAMProviderConfig(LenientNamedTuple):
+        """
+        lakectl configuration's credentials block
+        """
+        token_ttl_seconds: int
+        url_presign_ttl_seconds: int
+        token_request_headers: dict[str, str]
+
+    class IAMProvider(LenientNamedTuple):
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: Any reason why this is a `LenientNamedTuple`?
created_at: 2025-05-13T18:01:55Z
author_association: MEMBER
path: clients/python-wrapper/tests/utests/test_auth_flow.py
user: login: N-o-Z
body: `test_auth.py`
created_at: 2025-05-13T18:05:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,361 @@
+import datetime
+from unittest.mock import MagicMock, patch
path: clients/python-wrapper/tests/utests/test_auth_flow.py
user: login: N-o-Z
body: I would much rather prefer to continue using monkeypatching than introducing another unit test library to the code.
created_at: 2025-05-13T18:06:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,361 @@
+import datetime
+from unittest.mock import MagicMock, patch
+
+import sys
+from lakefs_sdk import AuthenticationToken
+from pydantic import StrictInt, StrictStr
+
+# Mock boto3 if it's not installed
+try:
+    import boto3
+except ImportError:
+    boto3 = MagicMock()
+    sys.modules['boto3'] = boto3
+
+from lakefs.config import ClientConfig
+from lakefs.exceptions import NoAuthenticationFound
+from tests.utests.common import (
+    lakectl_test_config_context,
+    lakectl_no_config_context,
+    env_var_context,
+    expect_exception_context,
+    TEST_SERVER, TEST_ENDPOINT_PATH,
+)
+
+MOCK_PRESIGNED_URL = """
+https://my-bucket.s3.amazonaws.com/my-object.txt?
+Action=GetObject&
+Version=2012-10-17&
+X-Amz-Algorithm=AWS4-HMAC-SHA256&
+X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20250506%2Fus-east-1%2Fs3%2Faws4_request&
+X-Amz-Date=20250506T114100Z&
+X-Amz-Expires=3600&
+X-Amz-SignedHeaders=host&
+X-Amz-Signature=abcd1234example5678
+"""
+
+class TestAuthenticationFlow:
+    """Test suite for authentication flow in lakeFS client"""
+
+    def test_client_auth_type_detection(self, monkeypatch, tmp_path):
+        """Test that client correctly detects authentication type"""
+        with lakectl_test_config_context(monkeypatch, tmp_path) as client:
+            # Test with credentials
+            clt = client.Client(username="user", password="pass")
+            assert clt.config.get_auth_type() == ClientConfig.AuthType.CREDENTIALS
+
+            # Test with session token
+            clt = client.Client(access_token="token123")
+            assert clt.config.get_auth_type() == ClientConfig.AuthType.SESSION_TOKEN
+
+            # Test with IAM provider
+            iam_provider = ClientConfig.IAMProvider(
+                type=ClientConfig.ProviderType.AWS_IAM,
+                aws_iam=ClientConfig.AWSIAMProviderConfig(
+                    token_ttl_seconds=3600,
+                    url_presign_ttl_seconds=60,
+                    token_request_headers=None
+                )
+            )
+            clt = client.Client()
+            clt.config._iam_provider = iam_provider
+            assert clt.config.get_auth_type() == ClientConfig.AuthType.IAM
+
+    @patch('lakefs.auth.access_token_from_aws_iam_role')
path: clients/python-wrapper/tests/utests/test_auth_flow.py
user: login: N-o-Z
body: You can achieve the same with the pytest and monkeypatch functionality without having to import another package. See example of other test modules
created_at: 2025-05-13T18:07:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,361 @@
+import datetime
+from unittest.mock import MagicMock, patch
+
+import sys
+from lakefs_sdk import AuthenticationToken
+from pydantic import StrictInt, StrictStr
+
+# Mock boto3 if it's not installed
+try:
+    import boto3
+except ImportError:
+    boto3 = MagicMock()
+    sys.modules['boto3'] = boto3
+
+from lakefs.config import ClientConfig
+from lakefs.exceptions import NoAuthenticationFound
+from tests.utests.common import (
+    lakectl_test_config_context,
+    lakectl_no_config_context,
+    env_var_context,
+    expect_exception_context,
+    TEST_SERVER, TEST_ENDPOINT_PATH,
+)
path: clients/python-wrapper/tests/utests/test_auth_flow.py
user: login: N-o-Z
body: Either one per line or arbitrary line break:
```suggestion
from tests.utests.common import (
    lakectl_test_config_context,
    lakectl_no_config_context,
    env_var_context,
    expect_exception_context,
    TEST_SERVER,
    TEST_ENDPOINT_PATH,
)
```
created_at: 2025-05-13T18:10:48Z
author_association: MEMBER
diff_hunk: @@ -8,9 +8,10 @@ Sphinx~=7.2.6
 sphinx-autodoc-typehints~=1.25.2
 myst-parser~=2.0.0
 pytest~=7.4.3
+pytest-mock
path: clients/python-wrapper/requirements.txt
user: login: N-o-Z
body: I don't believe it's actually required
created_at: 2025-05-13T18:11:23Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: This flow is really hard to follow. I'm sure you can organize it better so it will be more streamlined
created_at: 2025-05-13T18:13:34Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: I'll tell you why I'm confused:
On the one hand you create a load from config and load from environment and split the code in a way where you load the configuration based on the source.
But then you create functions that get dedicated information get iam provider / get aws iam profile which provide specific configuration information but go over all sources.
For clarity - I think we should choose one approach for collecting configuration information.
I have to admit I got dizzy trying to read this
created_at: 2025-05-13T18:17:06Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: When will you get a TypeError?
Credentials is of type LenientNamedTuple it should not throw TypeError
created_at: 2025-05-13T18:19:47Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
+    """Build AWS IAM provider configuration from environment and config file."""
+    env_token_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS)
+    env_url_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_URL_PRESIGN_TTL_SECONDS)
+    env_headers = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS)
+
+    # Default values
+    token_ttl_seconds = int(_DEFAULT_IAM_TOKEN_TTL_SECONDS)
+    url_presign_ttl_seconds = int(_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS)
+    token_request_headers = None
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: You have defaults: https://docs.python.org/3.10/library/os.html#os.getenv

created_at: 2025-05-13T18:22:36Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: You have code paths in your init where the _config_data is not initialized explicitly
I don't see it being used outside the scope of the init func so I'm wondering why is it a data member of the instance - use a local variable instead
created_at: 2025-05-13T18:25:19Z
author_association: MEMBER
diff_hunk: @@ -48,8 +61,42 @@ class Credentials(LenientNamedTuple):
         access_key_id: str
         secret_access_key: str
 
-    server: Server
-    credentials: Credentials
+    class AuthType(Enum):
+        """
+        Enum for the supported authentication types
+        """
+        SESSION_TOKEN = 1
+        CREDENTIALS = 2
+        IAM = 3
+
+    class ProviderType(Enum):
+        """
+        Enum for the supported authentication provider types
+        """
+        AWS_IAM = "aws_iam"
+        UNKNOWN = "unknown"
+
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: Should probably take them out of the ClientConfig class
created_at: 2025-05-13T18:29:56Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: Please add more documentation for posterity - does order matter?
created_at: 2025-05-13T18:32:33Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: Redundant - if self._iam_provider == None you are returning explicit None??
This method can be a [property](https://www.programiz.com/python-programming/property) instead
created_at: 2025-05-13T18:34:08Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: Also same comment as before you can use try-except here
created_at: 2025-05-13T18:35:24Z
author_association: MEMBER
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: Why is this method public? someone else uses it?
created_at: 2025-05-13T18:36:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,144 @@
+"""
+lakeFS Authentication Utils Module
path: clients/python-wrapper/lakefs/auth.py
user: login: Jonathan-Rosenberg
body: Done
created_at: 2025-05-14T11:20:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,144 @@
+"""
+lakeFS Authentication Utils Module
+
+Includes authentication util functions
+"""
+
+import base64
+import datetime
+import json
+from typing import Any, Optional, TYPE_CHECKING
+from urllib.parse import urlparse, parse_qs
+
+from lakefs_sdk import ExternalLoginInformation
+from lakefs_sdk.client import LakeFSClient
+
+from lakefs.config import ClientConfig
+from lakefs.exceptions import api_exception_handler
+
+DEFAULT_AWS_REGION = "us-east-1"
+
+if TYPE_CHECKING:
+    import boto3
+
+def access_token_from_aws_iam_role(sdk_client: LakeFSClient,
+                                   lakefs_host: str,
+                                   boto3_session: "boto3.Session",
+                                   aws_provider_auth_params: ClientConfig.AWSIAMProviderConfig) -> tuple[Any, datetime]:
+    """
+    Generate an access token for lakeFS authentication using AWS IAM role.
+    :param sdk_client: LakeFSClient
+    :param lakefs_host: LakeFS API URL
+    :param boto3_session: Session
+    :param aws_provider_auth_params: ClientConfig.AWSIAMProviderConfig
+    :return: An access token for lakeFS authentication.
+    """
+    presigned_ttl = int(aws_provider_auth_params.url_presign_ttl_seconds)
+    token_ttl_seconds = int(aws_provider_auth_params.token_ttl_seconds)
path: clients/python-wrapper/lakefs/auth.py
user: login: Jonathan-Rosenberg
body: it's very plausible that it wasn't an integer on a previous development phase... Changing.
created_at: 2025-05-14T11:22:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +42,7 @@ class ClientConfig(Configuration):
     2. Use LAKECTL_SERVER_ENDPOINT_URL, LAKECTL_ACCESS_KEY_ID and LAKECTL_ACCESS_SECRET_KEY if set
     3. Try to read ~/.lakectl.yaml if exists
     4. Use IAM role from current machine (using AWS IAM role will work only with enterprise/cloud)
+    5. If the credentials provider type is set to aws_iam, use the credentials from the machine's AWS profile
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: I think that I wanted to differentiate between functionally asking to use the IAM role (as it was until this PR), and the config-specified IAM support implemented in this PR.
It's unnecessary...
Changing.
created_at: 2025-05-14T11:26:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,8 +61,42 @@ class Credentials(LenientNamedTuple):
         access_key_id: str
         secret_access_key: str
 
-    server: Server
-    credentials: Credentials
+    class AuthType(Enum):
+        """
+        Enum for the supported authentication types
+        """
+        SESSION_TOKEN = 1
+        CREDENTIALS = 2
+        IAM = 3
+
+    class ProviderType(Enum):
+        """
+        Enum for the supported authentication provider types
+        """
+        AWS_IAM = "aws_iam"
+        UNKNOWN = "unknown"
+
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: Why? It's part of the configurations.
created_at: 2025-05-14T11:28:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,8 +61,42 @@ class Credentials(LenientNamedTuple):
         access_key_id: str
         secret_access_key: str
 
-    server: Server
-    credentials: Credentials
+    class AuthType(Enum):
+        """
+        Enum for the supported authentication types
+        """
+        SESSION_TOKEN = 1
+        CREDENTIALS = 2
+        IAM = 3
+
+    class ProviderType(Enum):
+        """
+        Enum for the supported authentication provider types
+        """
+        AWS_IAM = "aws_iam"
+        UNKNOWN = "unknown"
+
+    class AWSIAMProviderConfig(LenientNamedTuple):
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: To keep the configurations-related classes consistent. I can change it to `@dataclass` 
created_at: 2025-05-14T11:30:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,8 +61,42 @@ class Credentials(LenientNamedTuple):
         access_key_id: str
         secret_access_key: str
 
-    server: Server
-    credentials: Credentials
+    class AuthType(Enum):
+        """
+        Enum for the supported authentication types
+        """
+        SESSION_TOKEN = 1
+        CREDENTIALS = 2
+        IAM = 3
+
+    class ProviderType(Enum):
+        """
+        Enum for the supported authentication provider types
+        """
+        AWS_IAM = "aws_iam"
+        UNKNOWN = "unknown"
+
+    class AWSIAMProviderConfig(LenientNamedTuple):
+        """
+        lakectl configuration's credentials block
+        """
+        token_ttl_seconds: int
+        url_presign_ttl_seconds: int
+        token_request_headers: dict[str, str]
+
+    class IAMProvider(LenientNamedTuple):
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: same as above
created_at: 2025-05-14T11:30:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: I wanted to test your stamina. Given you haven't fainted, you've passed the test!
Now I'll change to the actual easy-to-comprehend version. 
created_at: 2025-05-15T09:18:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: ```python
class LenientNamedTuple:
    """
    Class which provides the NamedTuple functionality but allows initializing with unknown fields.
    The unknown fields will be ignored and mandatory fields are enforced
    """

    __initialized: bool = False
    unknown: dict = {}

    def __init__(self, **kwargs):
        fields = list(self.__class__.__dict__["__annotations__"].keys())
        for k, v in kwargs.items():
            if k in fields:
                setattr(self, k, v)
                fields.remove(k)
            else:
                self.unknown[k] = v

        if len(fields) > 0:
            raise TypeError(f"missing {len(fields)} required arguments: {fields}")
....
```

I saw that there's a scenario that a `TypeError` might be thrown, so I covered it.
created_at: 2025-05-15T10:35:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: Done
created_at: 2025-05-15T10:39:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: I'm doing this one only because you used the word "posterity"
created_at: 2025-05-15T10:40:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: property it is
created_at: 2025-05-15T10:45:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: what method? 😉 🤣 
created_at: 2025-05-15T10:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: done
created_at: 2025-05-15T10:47:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
+    """Build AWS IAM provider configuration from environment and config file."""
+    env_token_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS)
+    env_url_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_URL_PRESIGN_TTL_SECONDS)
+    env_headers = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS)
+
+    # Default values
+    token_ttl_seconds = int(_DEFAULT_IAM_TOKEN_TTL_SECONDS)
+    url_presign_ttl_seconds = int(_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS)
+    token_request_headers = None
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: Yep, it didn't fit in the previous implementation, but now it will.
created_at: 2025-05-15T10:51:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
+    """Build AWS IAM provider configuration from environment and config file."""
+    env_token_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS)
+    env_url_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_URL_PRESIGN_TTL_SECONDS)
+    env_headers = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS)
+
+    # Default values
+    token_ttl_seconds = int(_DEFAULT_IAM_TOKEN_TTL_SECONDS)
+    url_presign_ttl_seconds = int(_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS)
+    token_request_headers = None
+
+    if (data and "credentials" in data and
+            "provider" in data["credentials"] and
+            "aws_iam" in data["credentials"]["provider"]):
+        file_config = data["credentials"]["provider"]["aws_iam"]
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: Noice
created_at: 2025-05-15T10:56:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,6 +29,86 @@
 with open('README.md') as f:
     long_description = f.read()
 
+# to use the `clean` command, run: python -m build clean --all
+class CleanCommand(Command):
path: clients/python-wrapper/setup.py
user: login: Jonathan-Rosenberg
body: removed the command for now
created_at: 2025-05-15T11:01:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,9 +8,10 @@ Sphinx~=7.2.6
 sphinx-autodoc-typehints~=1.25.2
 myst-parser~=2.0.0
 pytest~=7.4.3
+pytest-mock
path: clients/python-wrapper/requirements.txt
user: login: Jonathan-Rosenberg
body: done
created_at: 2025-05-15T11:47:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,8 +125,12 @@
     license="Apache 2.0",
     long_description=long_description,
     long_description_content_type='text/markdown',
+    # Install using: `pip install "lakefs-<version>-py3-none-any.whl[all,aws-iam]"` (or just one of them)
path: clients/python-wrapper/setup.py
user: login: Jonathan-Rosenberg
body: moved into the context of extras_require
created_at: 2025-05-15T11:48:02Z
author_association: CONTRIBUTOR
path: clients/python-wrapper/tests/utests/test_auth_flow.py
user: login: Jonathan-Rosenberg
body: you got it
created_at: 2025-05-15T11:48:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
+    """Build AWS IAM provider configuration from environment and config file."""
+    env_token_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_TTL_SECONDS)
+    env_url_ttl = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_URL_PRESIGN_TTL_SECONDS)
+    env_headers = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_AWS_IAM_TOKEN_REQUEST_HEADERS)
+
+    # Default values
+    token_ttl_seconds = int(_DEFAULT_IAM_TOKEN_TTL_SECONDS)
+    url_presign_ttl_seconds = int(_DEFAULT_IAM_URL_PRESIGN_TTL_SECONDS)
+    token_request_headers = None
+
+    if (data and "credentials" in data and
+            "provider" in data["credentials"] and
+            "aws_iam" in data["credentials"]["provider"]):
+        file_config = data["credentials"]["provider"]["aws_iam"]
+        if file_config:
+            token_ttl_seconds = _safe_int_or_default(file_config.get("token_ttl_seconds"), token_ttl_seconds)
+            url_presign_ttl_seconds = _safe_int_or_default(
+                file_config.get("url_presign_ttl_seconds"), url_presign_ttl_seconds)
+            if "token_request_headers" in file_config:
+                token_request_headers = file_config["token_request_headers"]
+
+    # Environment variables override
+    token_ttl_seconds = _safe_int_or_default(env_token_ttl, token_ttl_seconds)
+    url_presign_ttl_seconds = _safe_int_or_default(env_url_ttl, url_presign_ttl_seconds)
+
+    if env_headers is not None:
+        token_request_headers = env_headers
+
+    return ClientConfig.AWSIAMProviderConfig(
+        token_ttl_seconds=token_ttl_seconds,
+        url_presign_ttl_seconds=url_presign_ttl_seconds,
+        token_request_headers=token_request_headers
+    )
+
+def _safe_int_or_default(value: Optional[str], default: int) -> int:
+    if value is None:
+        return default
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: cool
created_at: 2025-05-15T11:48:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
+                        pass
+                self._config_data = data
+        except FileNotFoundError:
+            self._config_data = None
+
+    def _load_from_environment(self):
+        """Load configuration from environment variables, which take precedence"""
+        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_URL_ENV)
+        if endpoint_env is not None:
+            self.host = endpoint_env
+        elif hasattr(self, 'server') and self.server:
+            self.host = self.server.endpoint_url
         key_env = os.getenv(_LAKECTL_ACCESS_KEY_ID_ENV)
         secret_env = os.getenv(_LAKECTL_SECRET_ACCESS_KEY_ENV)
+        if key_env is not None and secret_env is not None:
+            self.username = key_env
+            self.password = secret_env
+            self.credentials = ClientConfig.Credentials(
+                access_key_id=key_env,
+                secret_access_key=secret_env
+            )
+        # Session token takes precedence over basic credentials
+        token_env = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+        if token_env is not None:
+            self.access_token = token_env
+            self.username = None
+            self.password = None
+            self._iam_provider = None
 
-        self.host = endpoint_env if endpoint_env is not None else self.server.endpoint_url
-        self.username = key_env if key_env is not None else self.credentials.access_key_id
-        self.password = secret_env if secret_env is not None else self.credentials.secret_access_key
-        self.access_token = os.getenv(_LAKECTL_CREDENTIALS_SESSION_TOKEN)
+    def _has_valid_authentication(self) -> bool:
+        """Check if we have valid authentication credentials"""
+        if self.access_token is not None:
+            return True
+        if (self.username is not None and len(self.username) > 0 and
+                self.password is not None and len(self.password) > 0):
+            return True
+        if self._iam_provider is not None:
+            return True
+        return False
 
-        if self.access_token is not None or len(self.username) > 0 and len(self.password) > 0:
-            found = True
+    def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
+        """
+        Returns the type of authentication used: either SessionToken, Credentials, or IAMProvider
+        :return: ClientConfig.AuthType
+        """
+        if self._iam_provider is not None:
+            return ClientConfig.AuthType.IAM
+        if self.access_token is not None:
+            return ClientConfig.AuthType.SESSION_TOKEN
+        if self.credentials is not None:
+            return ClientConfig.AuthType.CREDENTIALS
+        return None
 
-        if not found:
-            raise NoAuthenticationFound
+    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+        """
+        Returns the IAM provider used for authentication.
+        :return: ClientConfig.IAMProvider
+        """
+        if self._iam_provider is not None:
+            return self._iam_provider
+        return None
+
+def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    """
+    Get IAM provider configuration from environment variables (primary) or config file (fallback).
+
+    :param data: Optional config data from YAML file
+    :return: IAMProvider if configured, None otherwise
+    """
+    provider_type = _get_provider_type(data)
+    if not provider_type:
+        return None
+
+    if provider_type not in SUPPORTED_IAM_PROVIDERS:
+        raise UnsupportedCredentialsProviderType(provider_type)
+
+    if provider_type == "aws_iam":
+        aws_config = _get_aws_iam_config(data)
+        return ClientConfig.IAMProvider(
+            type=ClientConfig.ProviderType.AWS_IAM,
+            aws_iam=aws_config
+        )
+
+    return None
+
+def _get_provider_type(data: Optional[Dict] = None) -> Optional[str]:
+    """Extract provider type from environment or config data."""
+    provider_type = os.getenv(_LAKECTL_CREDENTIALS_PROVIDER_TYPE)
+    if provider_type:
+        return provider_type
+    if data and data.get("credentials", {}).get("provider", {}).get("type"):
+        return data["credentials"]["provider"]["type"]
+
+    return None
+
+def _get_aws_iam_config(data: Optional[Dict] = None) -> ClientConfig.AWSIAMProviderConfig:
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: done
created_at: 2025-05-15T11:49:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: The only reason we get TypeError in this case is when config_data["credentials"] doesn't contain all the fields that are defined in the Credentials class.
Please add description for why we pass in that case
created_at: 2025-05-15T19:21:08Z
author_association: MEMBER
diff_hunk: @@ -182,87 +205,114 @@ def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
             return ClientConfig.AuthType.CREDENTIALS
         return None
 
-    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+    @property
+    def iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
         """
         Returns the IAM provider used for authentication.
         :return: ClientConfig.IAMProvider
         """
-        if self._iam_provider is not None:
-            return self._iam_provider
-        return None
+        return self._iam_provider
 
-def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    def _set_iam_provider_from_config_file(self, config_data: Dict):
+        """
+        Set the IAM provider from the configuration file.
+        """
+        provider_type = _get_provider_type_from_config_file(config_data)
+        if provider_type is None:
+            self._iam_provider = None
path: clients/python-wrapper/lakefs/config.py
user: login: N-o-Z
body: return?
or if else?
created_at: 2025-05-15T20:03:43Z
author_association: MEMBER
diff_hunk: @@ -182,87 +205,114 @@ def get_auth_type(self) -> Optional[ClientConfig.AuthType]:
             return ClientConfig.AuthType.CREDENTIALS
         return None
 
-    def get_iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
+    @property
+    def iam_provider(self) -> Optional[ClientConfig.IAMProvider]:
         """
         Returns the IAM provider used for authentication.
         :return: ClientConfig.IAMProvider
         """
-        if self._iam_provider is not None:
-            return self._iam_provider
-        return None
+        return self._iam_provider
 
-def get_iam_provider_from_env_or_file(data: Optional[Dict] = None) -> Optional[ClientConfig.IAMProvider]:
+    def _set_iam_provider_from_config_file(self, config_data: Dict):
+        """
+        Set the IAM provider from the configuration file.
+        """
+        provider_type = _get_provider_type_from_config_file(config_data)
+        if provider_type is None:
+            self._iam_provider = None
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: done
created_at: 2025-05-18T08:32:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -60,35 +107,162 @@ def __init__(self, verify_ssl: Optional[bool] = None, proxy: Optional[str] = Non
 
         if kwargs:
             return
+        self._load_from_config_file()
+        self._load_from_environment()
+        # Check for IAM provider if no session token and no credentials
+        if self.access_token is None and self.username is None and self.password is None:
+            self._iam_provider = get_iam_provider_from_env_or_file(self._config_data)
 
-        found = False
+        if not self._has_valid_authentication():
+            raise NoAuthenticationFound
 
-        # Get credentials from lakectl
+    def _load_from_config_file(self):
+        """Load configuration from .lakectl.yaml file if it exists"""
         try:
             with open(_LAKECTL_YAML_PATH, encoding="utf-8") as fd:
                 data = yaml.load(fd, Loader=yaml.Loader)
-                self.server = (ClientConfig.Server(**data["server"])
-                               if "server" in data
-                               else ClientConfig.Server(endpoint_url=""))
-                self.credentials = (ClientConfig.Credentials(**data["credentials"])
-                                   if "credentials" in data
-                                   else ClientConfig.Credentials(access_key_id="", secret_access_key=""))
-                found = True
-        except FileNotFoundError:  # File not found, fallback to env variables
-            self.server = ClientConfig.Server(endpoint_url="")
-            self.credentials = ClientConfig.Credentials(access_key_id="", secret_access_key="")
-
-        endpoint_env = os.getenv(_LAKECTL_ENDPOINT_ENV)
+                if "server" in data:
+                    self.server = ClientConfig.Server(**data["server"])
+                if "credentials" in data:
+                    try:
+                        self.credentials = ClientConfig.Credentials(**data["credentials"])
+                        self.username = self.credentials.access_key_id
+                        self.password = self.credentials.secret_access_key
+                    except TypeError:
path: clients/python-wrapper/lakefs/config.py
user: login: Jonathan-Rosenberg
body: So I removed the check. Thanks!
created_at: 2025-05-18T08:44:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-11T15:42:51Z
body: 
  # E2E Test Results - DynamoDB Local - Local Block Adapter

  <img src="https://svg.test-summary.com/dashboard.svg?p=13&f=0&s=1" alt="13 passed, 1 skipped">

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-11T15:43:13Z
body: 
  # E2E Test Results - Quickstart

  <img src="https://svg.test-summary.com/dashboard.svg?p=12&f=0&s=0" alt="12 passed">

author_association: NONE
user: login: Jonathan-Rosenberg
created_at: 2025-05-12T19:01:05Z
body: Damn @N-o-Z ... that's some harsh truth...
Ok will change that ☺️
author_association: CONTRIBUTOR
user: login: Jonathan-Rosenberg
created_at: 2025-05-18T08:43:48Z
body: Monkey patching not done since I don't understand why it's is better than using the standard framework for Python unit testing.
author_association: CONTRIBUTOR