Skip to content

CLI

Constants

consts

CLI constants and environment variable keys for t3api_utils configuration.

Defines default values and the :class:EnvKeys enum whose members map to environment variables (and .t3.env file keys) that control authentication, HTTP behaviour, performance tuning, and output settings.

DEFAULT_OTP_WHITELIST module-attribute

DEFAULT_OTP_WHITELIST = {'mi.metrc.com'}

Metrc hostnames that require a one-time password for authentication.

DEFAULT_CREDENTIAL_EMAIL_WHITELIST module-attribute

DEFAULT_CREDENTIAL_EMAIL_WHITELIST = {'co.metrc.com'}

Metrc hostnames that require an email address for authentication.

DEFAULT_T3_API_HOST module-attribute

DEFAULT_T3_API_HOST = 'https://api.trackandtrace.tools'

Default base URL for the T3 API when no override is configured.

DEFAULT_ENV_PATH module-attribute

DEFAULT_ENV_PATH: Final[str] = '.t3.env'

Default path to the dotenv file where credentials and settings are persisted.

EnvKeys

Bases: str, Enum

Environment variable keys recognised by :class:t3api_utils.cli.utils.ConfigManager.

Each member's value is the literal env-var / dotenv key string. Members are grouped by concern: authentication, connection, performance, hostname behaviour, development, and output.

METRC_HOSTNAME class-attribute instance-attribute

METRC_HOSTNAME = 'METRC_HOSTNAME'

Metrc state hostname (e.g. ca.metrc.com).

METRC_USERNAME class-attribute instance-attribute

METRC_USERNAME = 'METRC_USERNAME'

Metrc account username.

METRC_PASSWORD class-attribute instance-attribute

METRC_PASSWORD = 'METRC_PASSWORD'

Metrc account password.

METRC_EMAIL class-attribute instance-attribute

METRC_EMAIL = 'METRC_EMAIL'

Email address for hostnames that require it (see :data:DEFAULT_CREDENTIAL_EMAIL_WHITELIST).

JWT_TOKEN class-attribute instance-attribute

JWT_TOKEN = 'JWT_TOKEN'

Pre-issued JWT bearer token for direct authentication.

API_KEY class-attribute instance-attribute

API_KEY = 'API_KEY'

T3 API key for API-key-based authentication.

API_STATE_CODE class-attribute instance-attribute

API_STATE_CODE = 'API_STATE_CODE'

Two-letter US state code associated with the API key.

T3_API_HOST class-attribute instance-attribute

T3_API_HOST = 'T3_API_HOST'

Base URL of the T3 API (overrides :data:DEFAULT_T3_API_HOST).

HTTP_TIMEOUT class-attribute instance-attribute

HTTP_TIMEOUT = 'HTTP_TIMEOUT'

Overall HTTP request timeout in seconds.

HTTP_CONNECT_TIMEOUT class-attribute instance-attribute

HTTP_CONNECT_TIMEOUT = 'HTTP_CONNECT_TIMEOUT'

TCP connection timeout in seconds.

HTTP_READ_TIMEOUT class-attribute instance-attribute

HTTP_READ_TIMEOUT = 'HTTP_READ_TIMEOUT'

HTTP response read timeout in seconds.

VERIFY_SSL class-attribute instance-attribute

VERIFY_SSL = 'VERIFY_SSL'

Whether to verify TLS certificates ("true" / "false").

MAX_WORKERS class-attribute instance-attribute

MAX_WORKERS = 'MAX_WORKERS'

Maximum number of parallel workers for concurrent data fetching.

BATCH_SIZE class-attribute instance-attribute

BATCH_SIZE = 'BATCH_SIZE'

Number of pages to fetch per batch in async parallel loading.

RATE_LIMIT_RPS class-attribute instance-attribute

RATE_LIMIT_RPS = 'RATE_LIMIT_RPS'

Maximum requests per second for the rate limiter.

RATE_LIMIT_BURST class-attribute instance-attribute

RATE_LIMIT_BURST = 'RATE_LIMIT_BURST'

Maximum burst size allowed by the rate limiter.

RETRY_MAX_ATTEMPTS class-attribute instance-attribute

RETRY_MAX_ATTEMPTS = 'RETRY_MAX_ATTEMPTS'

Maximum number of retry attempts for failed HTTP requests.

RETRY_BACKOFF_FACTOR class-attribute instance-attribute

RETRY_BACKOFF_FACTOR = 'RETRY_BACKOFF_FACTOR'

Exponential backoff multiplier between retries.

RETRY_MIN_WAIT class-attribute instance-attribute

RETRY_MIN_WAIT = 'RETRY_MIN_WAIT'

Minimum wait time in seconds between retries.

OTP_WHITELIST class-attribute instance-attribute

OTP_WHITELIST = 'OTP_WHITELIST'

Comma-separated list of hostnames requiring OTP authentication.

EMAIL_WHITELIST class-attribute instance-attribute

EMAIL_WHITELIST = 'EMAIL_WHITELIST'

Comma-separated list of hostnames requiring an email address.

OTP_SEED class-attribute instance-attribute

OTP_SEED = 'OTP_SEED'

TOTP seed for automatic one-time password generation.

LOG_LEVEL class-attribute instance-attribute

LOG_LEVEL = 'LOG_LEVEL'

Logging level (e.g. DEBUG, INFO, WARNING).

LOG_FORMAT class-attribute instance-attribute

LOG_FORMAT = 'LOG_FORMAT'

Custom log format string.

DEBUG_MODE class-attribute instance-attribute

DEBUG_MODE = 'DEBUG_MODE'

Enable debug mode ("true" / "false").

CACHE_RESPONSES class-attribute instance-attribute

CACHE_RESPONSES = 'CACHE_RESPONSES'

Cache API responses locally ("true" / "false").

T3_LOG_HTTP class-attribute instance-attribute

T3_LOG_HTTP = 'T3_LOG_HTTP'

Enable verbose HTTP request/response logging ("true" / "false").

T3_LOG_HEADERS class-attribute instance-attribute

T3_LOG_HEADERS = 'T3_LOG_HEADERS'

Log request headers when HTTP logging is enabled ("true" / "false", default "true").

T3_LOG_BODY class-attribute instance-attribute

T3_LOG_BODY = 'T3_LOG_BODY'

Log request body/payload when HTTP logging is enabled ("true" / "false", default "true").

T3_LOG_FILE class-attribute instance-attribute

T3_LOG_FILE = 'T3_LOG_FILE'

File path for HTTP debug logs. File is truncated on each run. Empty to disable file logging.

OUTPUT_DIR class-attribute instance-attribute

OUTPUT_DIR = 'OUTPUT_DIR'

Directory for CSV/JSON output files.

TEMP_DIR class-attribute instance-attribute

TEMP_DIR = 'TEMP_DIR'

Directory for temporary files.

AUTO_OPEN_FILES class-attribute instance-attribute

AUTO_OPEN_FILES = 'AUTO_OPEN_FILES'

Automatically open exported files after saving ("true" / "false").

STRIP_EMPTY_COLUMNS class-attribute instance-attribute

STRIP_EMPTY_COLUMNS = 'STRIP_EMPTY_COLUMNS'

Remove columns that are entirely empty from CSV output ("true" / "false").

DEFAULT_FILE_FORMAT class-attribute instance-attribute

DEFAULT_FILE_FORMAT = 'DEFAULT_FILE_FORMAT'

Default export format ("csv" or "json").

Utilities

utils

CLI utility functions for configuration management and authentication.

Provides the ConfigManager class for reading and auto-generating the .t3.env configuration file, as well as helper functions for loading, prompting, and persisting Metrc credentials.

DEFAULT_ENV_PATH module-attribute

DEFAULT_ENV_PATH: Final[str] = '.t3.env'

Default path to the dotenv file where credentials and settings are persisted.

ConfigManager

ConfigManager(config_path: str = DEFAULT_ENV_PATH)

Centralized configuration manager for .t3.env file.

Handles auto-generation, validation, and reading of the comprehensive .t3.env configuration file with all t3api-utils settings.

Initialize the configuration manager.

Parameters:

Name Type Description Default
config_path str

Filesystem path to the .t3.env configuration file. Defaults to DEFAULT_ENV_PATH.

DEFAULT_ENV_PATH
Source code in t3api_utils/cli/utils.py
def __init__(self, config_path: str = DEFAULT_ENV_PATH):
    """Initialize the configuration manager.

    Args:
        config_path: Filesystem path to the ``.t3.env`` configuration
            file. Defaults to ``DEFAULT_ENV_PATH``.
    """
    self.config_path = config_path
    self._config_cache: Optional[Dict[str, Any]] = None

ensure_config_exists

ensure_config_exists() -> None

Ensure .t3.env exists and is complete, auto-generating if needed.

Creates the configuration file when it is missing or regenerates it when required keys are absent. Prints an info message when the file is generated or updated.

Source code in t3api_utils/cli/utils.py
def ensure_config_exists(self) -> None:
    """Ensure ``.t3.env`` exists and is complete, auto-generating if needed.

    Creates the configuration file when it is missing or regenerates it
    when required keys are absent. Prints an info message when the file
    is generated or updated.
    """
    if not self._config_exists() or self._needs_update():
        self._generate_config_file()
        print_info(f"Generated/updated {self.config_path} configuration file")

get_config_value

get_config_value(key: EnvKeys, default: Any = None) -> Any

Get a configuration value with automatic type conversion.

Loads the configuration file, reads the requested key, and converts the raw string to the appropriate Python type (bool, float, int, or set) based on the key.

Parameters:

Name Type Description Default
key EnvKeys

The EnvKeys member identifying the configuration value.

required
default Any

Fallback value returned when the key is missing or type conversion fails.

None

Returns:

Type Description
Any

The converted configuration value, or default if the key is

Any

absent or conversion fails.

Source code in t3api_utils/cli/utils.py
def get_config_value(self, key: EnvKeys, default: Any = None) -> Any:
    """Get a configuration value with automatic type conversion.

    Loads the configuration file, reads the requested key, and converts
    the raw string to the appropriate Python type (bool, float, int, or
    set) based on the key.

    Args:
        key: The ``EnvKeys`` member identifying the configuration value.
        default: Fallback value returned when the key is missing or
            type conversion fails.

    Returns:
        The converted configuration value, or ``default`` if the key is
        absent or conversion fails.
    """
    load_dotenv(dotenv_path=self.config_path)
    value = os.getenv(key.value)

    if value is None:
        return default

    # Type conversion based on key
    if key in [EnvKeys.DEBUG_MODE, EnvKeys.VERIFY_SSL, EnvKeys.AUTO_OPEN_FILES,
               EnvKeys.STRIP_EMPTY_COLUMNS, EnvKeys.CACHE_RESPONSES,
               EnvKeys.T3_LOG_HTTP, EnvKeys.T3_LOG_HEADERS, EnvKeys.T3_LOG_BODY]:
        return value.lower() in ['true', '1', 'yes', 'on']
    elif key in [EnvKeys.HTTP_TIMEOUT, EnvKeys.HTTP_CONNECT_TIMEOUT, EnvKeys.HTTP_READ_TIMEOUT,
                 EnvKeys.RETRY_BACKOFF_FACTOR, EnvKeys.RETRY_MIN_WAIT]:
        try:
            return float(value)
        except ValueError:
            return default
    elif key in [EnvKeys.MAX_WORKERS, EnvKeys.BATCH_SIZE, EnvKeys.RATE_LIMIT_RPS,
                 EnvKeys.RATE_LIMIT_BURST, EnvKeys.RETRY_MAX_ATTEMPTS]:
        try:
            return int(value)
        except ValueError:
            return default
    elif key in [EnvKeys.OTP_WHITELIST, EnvKeys.EMAIL_WHITELIST]:
        # Convert comma-separated string to set
        return {hostname.strip() for hostname in value.split(',') if hostname.strip()}

    return value

get_otp_whitelist

get_otp_whitelist() -> Set[str]

Get the set of hostnames that require OTP authentication.

Returns:

Type Description
Set[str]

Set of Metrc hostnames requiring OTP, falling back to

Set[str]

DEFAULT_OTP_WHITELIST when unconfigured.

Source code in t3api_utils/cli/utils.py
def get_otp_whitelist(self) -> Set[str]:
    """Get the set of hostnames that require OTP authentication.

    Returns:
        Set of Metrc hostnames requiring OTP, falling back to
        ``DEFAULT_OTP_WHITELIST`` when unconfigured.
    """
    whitelist = self.get_config_value(EnvKeys.OTP_WHITELIST)
    return whitelist if whitelist else DEFAULT_OTP_WHITELIST

get_email_whitelist

get_email_whitelist() -> Set[str]

Get the set of hostnames that require an email during authentication.

Returns:

Type Description
Set[str]

Set of Metrc hostnames requiring email, falling back to

Set[str]

DEFAULT_CREDENTIAL_EMAIL_WHITELIST when unconfigured.

Source code in t3api_utils/cli/utils.py
def get_email_whitelist(self) -> Set[str]:
    """Get the set of hostnames that require an email during authentication.

    Returns:
        Set of Metrc hostnames requiring email, falling back to
        ``DEFAULT_CREDENTIAL_EMAIL_WHITELIST`` when unconfigured.
    """
    whitelist = self.get_config_value(EnvKeys.EMAIL_WHITELIST)
    return whitelist if whitelist else DEFAULT_CREDENTIAL_EMAIL_WHITELIST

get_api_host

get_api_host() -> str

Get the T3 API base URL.

Returns:

Type Description
str

The configured API host string, falling back to

str

DEFAULT_T3_API_HOST when unconfigured.

Source code in t3api_utils/cli/utils.py
def get_api_host(self) -> str:
    """Get the T3 API base URL.

    Returns:
        The configured API host string, falling back to
        ``DEFAULT_T3_API_HOST`` when unconfigured.
    """
    host = self.get_config_value(EnvKeys.T3_API_HOST)
    return host if host else DEFAULT_T3_API_HOST

get_otp_seed

get_otp_seed() -> Optional[str]

Get the Base32-encoded OTP seed for TOTP generation.

Returns:

Type Description
Optional[str]

The trimmed seed string, or None if the seed is not

Optional[str]

configured or is empty.

Source code in t3api_utils/cli/utils.py
def get_otp_seed(self) -> Optional[str]:
    """Get the Base32-encoded OTP seed for TOTP generation.

    Returns:
        The trimmed seed string, or ``None`` if the seed is not
        configured or is empty.
    """
    seed = self.get_config_value(EnvKeys.OTP_SEED)
    return seed.strip() if seed else None

generate_otp_from_seed

generate_otp_from_seed() -> Optional[str]

Generate a 6-digit TOTP code from the OTP_SEED environment variable.

Returns:

Type Description
Optional[str]

6-digit OTP string if OTP_SEED is configured, None otherwise.

Raises:

Type Description
AuthenticationError

If OTP_SEED is malformed or TOTP generation fails.

Source code in t3api_utils/cli/utils.py
def generate_otp_from_seed() -> Optional[str]:
    """
    Generate a 6-digit TOTP code from the OTP_SEED environment variable.

    Returns:
        6-digit OTP string if OTP_SEED is configured, None otherwise.

    Raises:
        AuthenticationError: If OTP_SEED is malformed or TOTP generation fails.
    """
    otp_seed = config_manager.get_otp_seed()
    if not otp_seed:
        return None

    try:
        totp = pyotp.TOTP(otp_seed)
        otp_code = totp.now()

        # Ensure it's always 6 digits (pad with zeros if needed)
        return f"{int(otp_code):06d}"

    except Exception as e:
        raise AuthenticationError(f"Failed to generate OTP from seed: {e}")

load_credentials_from_env

load_credentials_from_env() -> Dict[str, str]

Load credential values from the .t3.env environment file.

Reads all available authentication values including Metrc credentials, JWT tokens, and API key credentials. Only non-empty values are included in the returned dictionary.

Returns:

Type Description
Dict[str, str]

Dictionary mapping credential names (e.g. "hostname",

Dict[str, str]

"username", "jwt_token") to their string values.

Dict[str, str]

Keys with empty or missing values are omitted.

Source code in t3api_utils/cli/utils.py
def load_credentials_from_env() -> Dict[str, str]:
    """Load credential values from the ``.t3.env`` environment file.

    Reads all available authentication values including Metrc credentials,
    JWT tokens, and API key credentials. Only non-empty values are included
    in the returned dictionary.

    Returns:
        Dictionary mapping credential names (e.g. ``"hostname"``,
        ``"username"``, ``"jwt_token"``) to their string values.
        Keys with empty or missing values are omitted.
    """
    load_dotenv(dotenv_path=DEFAULT_ENV_PATH)

    creds = {}

    # Metrc credentials
    hostname = (os.getenv(EnvKeys.METRC_HOSTNAME.value) or "").strip()
    username = (os.getenv(EnvKeys.METRC_USERNAME.value) or "").strip()
    password = (os.getenv(EnvKeys.METRC_PASSWORD.value) or "").strip()
    email = (os.getenv(EnvKeys.METRC_EMAIL.value) or "").strip()

    # Alternative authentication methods
    jwt_token = (os.getenv(EnvKeys.JWT_TOKEN.value) or "").strip()
    api_key = (os.getenv(EnvKeys.API_KEY.value) or "").strip()
    api_state_code = (os.getenv(EnvKeys.API_STATE_CODE.value) or "").strip()

    if hostname:
        creds["hostname"] = hostname
    if username:
        creds["username"] = username
    if password:
        creds["password"] = password
    if email:
        creds["email"] = email
    if jwt_token:
        creds["jwt_token"] = jwt_token
    if api_key:
        creds["api_key"] = api_key
    if api_state_code:
        creds["api_state_code"] = api_state_code

    return creds

offer_to_save_credentials

offer_to_save_credentials(*, credentials: T3Credentials) -> None

Offer to save credentials to the .t3.env file.

If the environment file does not exist, the user is prompted to create it. If it exists but any credential values differ from those provided, the user is prompted to update the file.

Parameters:

Name Type Description Default
credentials T3Credentials

The T3Credentials dictionary containing the current authentication values to compare and potentially save.

required
Source code in t3api_utils/cli/utils.py
def offer_to_save_credentials(*, credentials: T3Credentials) -> None:
    """Offer to save credentials to the ``.t3.env`` file.

    If the environment file does not exist, the user is prompted to create
    it. If it exists but any credential values differ from those provided,
    the user is prompted to update the file.

    Args:
        credentials: The ``T3Credentials`` dictionary containing the
            current authentication values to compare and potentially save.
    """
    load_dotenv(dotenv_path=DEFAULT_ENV_PATH)
    env_exists = os.path.exists(DEFAULT_ENV_PATH)

    current_hostname = os.getenv(EnvKeys.METRC_HOSTNAME.value, "").strip()
    current_username = os.getenv(EnvKeys.METRC_USERNAME.value, "").strip()
    current_password = os.getenv(EnvKeys.METRC_PASSWORD.value, "").strip()
    current_email = os.getenv(EnvKeys.METRC_EMAIL.value, "").strip()

    hostname_differs = credentials["hostname"] != current_hostname
    username_differs = credentials["username"] != current_username
    password_differs = credentials["password"] != current_password

    # Only check email differences if the hostname requires email
    email_differs = False
    if credentials["hostname"] in config_manager.get_email_whitelist():
        email_differs = credentials.get("email") != current_email

    if not env_exists:
        if typer.confirm(
            f"No credentials file found. Save these values to {DEFAULT_ENV_PATH}?",
            default=True,
        ):
            logger.info("[green]Saving credentials to new environment file.[/green]")
            set_key(
                DEFAULT_ENV_PATH, EnvKeys.METRC_HOSTNAME.value, credentials["hostname"]
            )
            set_key(
                DEFAULT_ENV_PATH, EnvKeys.METRC_USERNAME.value, credentials["username"]
            )
            set_key(
                DEFAULT_ENV_PATH, EnvKeys.METRC_PASSWORD.value, credentials["password"]
            )
            email_value = credentials.get("email")
            if email_value:
                set_key(
                    DEFAULT_ENV_PATH, EnvKeys.METRC_EMAIL.value, email_value
                )
    elif hostname_differs or username_differs or password_differs or email_differs:
        if typer.confirm(
            f"Some credential values differ from those in {DEFAULT_ENV_PATH}. Update them?",
            default=True,
        ):
            logger.info("[cyan]Updating credentials in environment file.[/cyan]")
            set_key(
                DEFAULT_ENV_PATH, EnvKeys.METRC_HOSTNAME.value, credentials["hostname"]
            )
            set_key(
                DEFAULT_ENV_PATH, EnvKeys.METRC_USERNAME.value, credentials["username"]
            )
            set_key(
                DEFAULT_ENV_PATH, EnvKeys.METRC_PASSWORD.value, credentials["password"]
            )
            email_value = credentials.get("email")
            if email_value:
                set_key(
                    DEFAULT_ENV_PATH, EnvKeys.METRC_EMAIL.value, email_value
                )

offer_to_save_jwt_token

offer_to_save_jwt_token(*, jwt_token: str) -> None

Offer to save a JWT token to the .t3.env file.

If the environment file does not exist, the user is prompted to create it. If it exists but the stored token differs, the user is prompted to update the file.

Parameters:

Name Type Description Default
jwt_token str

The JWT token string to compare and potentially save.

required
Source code in t3api_utils/cli/utils.py
def offer_to_save_jwt_token(*, jwt_token: str) -> None:
    """Offer to save a JWT token to the ``.t3.env`` file.

    If the environment file does not exist, the user is prompted to create
    it. If it exists but the stored token differs, the user is prompted to
    update the file.

    Args:
        jwt_token: The JWT token string to compare and potentially save.
    """
    load_dotenv(dotenv_path=DEFAULT_ENV_PATH)
    env_exists = os.path.exists(DEFAULT_ENV_PATH)

    current_jwt = os.getenv(EnvKeys.JWT_TOKEN.value, "").strip()

    if not env_exists:
        if typer.confirm(
            f"No credentials file found. Save JWT token to {DEFAULT_ENV_PATH}?",
            default=True,
        ):
            logger.info("[green]Saving JWT token to new environment file.[/green]")
            set_key(DEFAULT_ENV_PATH, EnvKeys.JWT_TOKEN.value, jwt_token)
    elif jwt_token != current_jwt:
        if typer.confirm(
            f"JWT token differs from the one in {DEFAULT_ENV_PATH}. Update it?",
            default=True,
        ):
            logger.info("[cyan]Updating JWT token in environment file.[/cyan]")
            set_key(DEFAULT_ENV_PATH, EnvKeys.JWT_TOKEN.value, jwt_token)

offer_to_save_api_key

offer_to_save_api_key(*, api_key: str, state_code: str) -> None

Offer to save an API key and state code to the .t3.env file.

If the environment file does not exist, the user is prompted to create it. If it exists but either the API key or state code differs, the user is prompted to update the file.

Parameters:

Name Type Description Default
api_key str

The API key string to compare and potentially save.

required
state_code str

The two-letter state code to compare and potentially save.

required
Source code in t3api_utils/cli/utils.py
def offer_to_save_api_key(*, api_key: str, state_code: str) -> None:
    """Offer to save an API key and state code to the ``.t3.env`` file.

    If the environment file does not exist, the user is prompted to create
    it. If it exists but either the API key or state code differs, the user
    is prompted to update the file.

    Args:
        api_key: The API key string to compare and potentially save.
        state_code: The two-letter state code to compare and potentially save.
    """
    load_dotenv(dotenv_path=DEFAULT_ENV_PATH)
    env_exists = os.path.exists(DEFAULT_ENV_PATH)

    current_api_key = os.getenv(EnvKeys.API_KEY.value, "").strip()
    current_state_code = os.getenv(EnvKeys.API_STATE_CODE.value, "").strip()

    if not env_exists:
        if typer.confirm(
            f"No credentials file found. Save API key to {DEFAULT_ENV_PATH}?",
            default=True,
        ):
            logger.info("[green]Saving API key to new environment file.[/green]")
            set_key(DEFAULT_ENV_PATH, EnvKeys.API_KEY.value, api_key)
            set_key(DEFAULT_ENV_PATH, EnvKeys.API_STATE_CODE.value, state_code)
    elif api_key != current_api_key or state_code != current_state_code:
        if typer.confirm(
            f"API key or state code differs from {DEFAULT_ENV_PATH}. Update them?",
            default=True,
        ):
            logger.info("[cyan]Updating API key in environment file.[/cyan]")
            set_key(DEFAULT_ENV_PATH, EnvKeys.API_KEY.value, api_key)
            set_key(DEFAULT_ENV_PATH, EnvKeys.API_STATE_CODE.value, state_code)

prompt_for_credentials_or_error

prompt_for_credentials_or_error(**kwargs: object) -> T3Credentials

Prompt for any missing credentials, using provided values if available.

Pre-populated values (typically loaded from the environment) are used without prompting. Missing values are requested interactively. OTP and email prompts are shown only when the hostname requires them.

Parameters:

Name Type Description Default
**kwargs object

Optional pre-populated credential values. Recognized keys are hostname, username, password, and email.

{}

Returns:

Type Description
T3Credentials

A fully-populated T3Credentials dictionary.

Raises:

Type Description
AuthenticationError

If a required credential is missing or empty, the OTP code is invalid, or the email address is malformed.

Source code in t3api_utils/cli/utils.py
def prompt_for_credentials_or_error(**kwargs: object) -> T3Credentials:
    """Prompt for any missing credentials, using provided values if available.

    Pre-populated values (typically loaded from the environment) are used
    without prompting. Missing values are requested interactively. OTP and
    email prompts are shown only when the hostname requires them.

    Args:
        **kwargs: Optional pre-populated credential values. Recognized keys
            are ``hostname``, ``username``, ``password``, and ``email``.

    Returns:
        A fully-populated ``T3Credentials`` dictionary.

    Raises:
        AuthenticationError: If a required credential is missing or empty,
            the OTP code is invalid, or the email address is malformed.
    """
    hostname = str(kwargs.get("hostname", "")) if kwargs.get("hostname") else None
    username = str(kwargs.get("username", "")) if kwargs.get("username") else None
    password = str(kwargs.get("password", "")) if kwargs.get("password") else None
    email = str(kwargs.get("email", "")) if kwargs.get("email") else None

    if hostname:
        print_info(f"Using stored value for hostname: {hostname}")
    else:
        hostname = typer.prompt("Enter Metrc hostname (e.g., mo.metrc.com)")

    if username:
        print_info(f"Using stored value for username: {username}")
    else:
        username = typer.prompt("Enter Metrc username")

    if password:
        print_info("Using stored value for password.")
    else:
        password = typer.prompt("Enter Metrc password", hide_input=True)

    credentials: T3Credentials = {
        "hostname": hostname or "",
        "username": username or "",
        "password": password or "",
        "otp": None,
        "email": None,
    }

    if hostname in config_manager.get_otp_whitelist():
        # Try to generate OTP from seed first, otherwise prompt user
        otp = generate_otp_from_seed()
        if otp:
            print_info("Using OTP generated from configured seed")
            credentials["otp"] = otp
        else:
            otp = typer.prompt("Enter 6-digit Metrc 2-factor authentication code")
            if not otp or len(otp) != 6 or not otp.isdigit():
                print_error("Invalid 2-factor authentication entered.")
                raise AuthenticationError(f"Invalid 2-factor authentication: {otp}")
            credentials["otp"] = otp

    if hostname in config_manager.get_email_whitelist():
        if email:
            print_info(f"Using stored value for email: {email}")
            credentials["email"] = email
        else:
            email_input = typer.prompt("Enter Metrc email address")
            if not email_input or "@" not in email_input:
                print_error("Invalid email address entered.")
                raise AuthenticationError(f"Invalid email address: {email_input}")
            credentials["email"] = email_input

    for key, value in credentials.items():
        if key not in ("otp", "email") and (not isinstance(value, str) or not value.strip()):
            print_error(f"Missing or empty credential: {key}")
            raise AuthenticationError(f"Missing or empty credential: {key}")

    return credentials

resolve_auth_inputs_or_error

resolve_auth_inputs_or_error() -> T3Credentials

Resolve authentication credentials from env and/or interactive prompt.

Loads any stored credentials from the environment file, prompts the user for missing values, and offers to persist the final set back to the .t3.env file.

Returns:

Type Description
T3Credentials

A fully-populated T3Credentials dictionary ready for use with

T3Credentials

the authentication client.

Raises:

Type Description
AuthenticationError

If required credentials cannot be resolved (propagated from prompt_for_credentials_or_error).

Source code in t3api_utils/cli/utils.py
def resolve_auth_inputs_or_error() -> T3Credentials:
    """Resolve authentication credentials from env and/or interactive prompt.

    Loads any stored credentials from the environment file, prompts the
    user for missing values, and offers to persist the final set back to
    the ``.t3.env`` file.

    Returns:
        A fully-populated ``T3Credentials`` dictionary ready for use with
        the authentication client.

    Raises:
        AuthenticationError: If required credentials cannot be resolved
            (propagated from ``prompt_for_credentials_or_error``).
    """
    print_subheader("Authentication Required")
    stored_credentials = load_credentials_from_env()
    credentials = prompt_for_credentials_or_error(**stored_credentials)
    offer_to_save_credentials(credentials=credentials)
    return credentials