Skip to content

Utilities

API reference for internal utilities.

_utils

Internal utilities for owi.metadatabase package.

Classes

APIConnectionError

APIConnectionError(message, response=None)

Bases: APIException

Exception raised when the API cannot be reached or returns a failure.

This exception is raised when there are network issues, server errors, or other connection-related problems during API communication.

Parameters:

Name Type Description Default
message str

Human-readable error message.

required
response Response or None

The HTTP response object if available, default is None.

None

Examples:

>>> exc = APIConnectionError("Network timeout")
>>> str(exc)
'Network timeout'
>>> exc.message
'Network timeout'
Source code in src/owi/metadatabase/_utils/exceptions.py
def __init__(self, message: str, response: Optional[requests.Response] = None) -> None:
    self.response = response
    super().__init__(message)

DataProcessingError

DataProcessingError(
    message="Error during data processing.",
)

Bases: APIException

Exception raised when there is a problem while processing the data.

This exception indicates that data was retrieved successfully from the API but could not be processed, transformed, or validated correctly.

Parameters:

Name Type Description Default
message str

Human-readable error message, default is "Error during data processing."

'Error during data processing.'

Examples:

>>> exc = DataProcessingError()
>>> str(exc)
'Error during data processing.'
>>> exc = DataProcessingError("Cannot parse geometry coordinates")
>>> str(exc)
'Cannot parse geometry coordinates'
Source code in src/owi/metadatabase/_utils/exceptions.py
def __init__(self, message: str = "Error during data processing.") -> None:
    super().__init__(message)

InvalidParameterError

InvalidParameterError(
    message="Invalid or missing parameters for the request.",
)

Bases: APIException

Exception raised when query parameters are invalid or missing.

This exception is raised before making API requests when the provided parameters fail validation checks or are inconsistent.

Parameters:

Name Type Description Default
message str

Human-readable error message, default is "Invalid or missing parameters for the request."

'Invalid or missing parameters for the request.'

Examples:

>>> exc = InvalidParameterError()
>>> str(exc)
'Invalid or missing parameters for the request.'
>>> exc = InvalidParameterError("Project name required")
>>> str(exc)
'Project name required'
Source code in src/owi/metadatabase/_utils/exceptions.py
def __init__(self, message: str = "Invalid or missing parameters for the request.") -> None:
    super().__init__(message)

Functions

deepcompare

deepcompare(a, b, tol=1e-05)

Compare two complicated objects recursively.

Compares two complicated (potentially nested) objects recursively and returns a result and a message.

Parameters:

Name Type Description Default
a Any

First object to be compared.

required
b Any

Second object to be compared.

required
tol float

Tolerance for the comparison, default is 1e-5.

1e-05

Returns:

Type Description
tuple

Tuple with a result of comparison and a message if different.

Examples:

>>> deepcompare({"a": 1.0}, {"a": 1.0})[0]
True
>>> deepcompare([1, 2], [1, 3])[0]
False
>>> deepcompare(np.nan, np.nan)[0]
True
Source code in src/owi/metadatabase/_utils/utils.py
def deepcompare(a: Any, b: Any, tol: float = 1e-5) -> tuple[bool, None | str]:
    """
    Compare two complicated objects recursively.

    Compares two complicated (potentially nested) objects recursively
    and returns a result and a message.

    Parameters
    ----------
    a : Any
        First object to be compared.
    b : Any
        Second object to be compared.
    tol : float, optional
        Tolerance for the comparison, default is 1e-5.

    Returns
    -------
    tuple
        Tuple with a result of comparison and a message if different.

    Examples
    --------
    >>> deepcompare({"a": 1.0}, {"a": 1.0})[0]
    True
    >>> deepcompare([1, 2], [1, 3])[0]
    False
    >>> deepcompare(np.nan, np.nan)[0]
    True
    """
    if type(a) != type(b):  # noqa: E721
        if hasattr(a, "__dict__") and isinstance(b, dict):
            return deepcompare(a.__dict__, b, tol)
        elif hasattr(b, "__dict__") and isinstance(a, dict):
            return deepcompare(a, b.__dict__, tol)
        elif isinstance(a, (float, np.floating)) and isinstance(b, (float, np.floating)):
            return deepcompare(np.float64(a), np.float64(b), tol)
        return (
            False,
            f"Types of {a} and {b} are different: {type(a).__name__} and {type(b).__name__}.",
        )
    elif isinstance(a, dict):
        if a.keys() != b.keys():
            return (
                False,
                f"Dictionary keys {a.keys()} and {b.keys()} are different.",
            )
        compare = [deepcompare(a[key], b[key], tol)[0] for key in a]
        assertion = all(compare)
        if assertion:
            message = None
        else:
            keys = [key for key, val in zip(a.keys(), compare) if val is False]
            message = f"Dictionary values are different for {a} and {b}, for keys: {keys}."
        return assertion, message
    elif isinstance(a, (list, tuple)):
        if len(a) != len(b):
            return (
                False,
                f"Lists/tuples {a} and {b} are of different length: {len(a)} and {len(b)}.",
            )
        compare = [deepcompare(i, j, tol)[0] for i, j in zip(a, b)]
        assertion = all(compare)
        if assertion:
            message = None
        else:
            inds = [ind for ind, val in zip(range(len(compare)), compare) if val is False]
            message = f"Lists/tuples are different for {a} and {b}, for indices: {inds}."
        return assertion, message
    elif hasattr(a, "__dict__") and not isinstance(a, pd.DataFrame):
        return deepcompare(a.__dict__, b.__dict__, tol)
    elif isinstance(a, pd.DataFrame):
        assertion = check_df_eq(a, b, tol)
        message = None if assertion else f"Dataframes {a} and {b} are different for {a.compare(b)}."
        return assertion, message
    else:
        return compare_if_simple_close(a, b, tol)

Exceptions

exceptions

Custom exceptions for the API client. These exceptions encapsulate common errors that can occur during API calls and data post-processing.

Classes

APIException
APIException(message)

Bases: Exception

Base exception for all errors raised by API.

This is the parent class for all custom exceptions in the package. It provides a consistent interface for error handling across API operations.

Parameters:

Name Type Description Default
message str

Human-readable error message describing what went wrong.

required

Examples:

>>> exc = APIException("Something went wrong")
>>> str(exc)
'Something went wrong'
>>> exc.message
'Something went wrong'
Source code in src/owi/metadatabase/_utils/exceptions.py
def __init__(self, message: str) -> None:
    self.message = message
    super().__init__(self.message)
APIConnectionError
APIConnectionError(message, response=None)

Bases: APIException

Exception raised when the API cannot be reached or returns a failure.

This exception is raised when there are network issues, server errors, or other connection-related problems during API communication.

Parameters:

Name Type Description Default
message str

Human-readable error message.

required
response Response or None

The HTTP response object if available, default is None.

None

Examples:

>>> exc = APIConnectionError("Network timeout")
>>> str(exc)
'Network timeout'
>>> exc.message
'Network timeout'
Source code in src/owi/metadatabase/_utils/exceptions.py
def __init__(self, message: str, response: Optional[requests.Response] = None) -> None:
    self.response = response
    super().__init__(message)
DataNotFoundError
DataNotFoundError(
    message="No data found for the given search criteria.",
)

Bases: APIException

Exception raised when no data is found for the given query parameters.

This exception indicates that the API request was successful but returned no results matching the search criteria.

Parameters:

Name Type Description Default
message str

Human-readable error message, default is "No data found for the given search criteria."

'No data found for the given search criteria.'

Examples:

>>> exc = DataNotFoundError()
>>> str(exc)
'No data found for the given search criteria.'
>>> exc = DataNotFoundError("No turbine T99 in project")
>>> str(exc)
'No turbine T99 in project'
Source code in src/owi/metadatabase/_utils/exceptions.py
def __init__(self, message: str = "No data found for the given search criteria.") -> None:
    super().__init__(message)
DataProcessingError
DataProcessingError(
    message="Error during data processing.",
)

Bases: APIException

Exception raised when there is a problem while processing the data.

This exception indicates that data was retrieved successfully from the API but could not be processed, transformed, or validated correctly.

Parameters:

Name Type Description Default
message str

Human-readable error message, default is "Error during data processing."

'Error during data processing.'

Examples:

>>> exc = DataProcessingError()
>>> str(exc)
'Error during data processing.'
>>> exc = DataProcessingError("Cannot parse geometry coordinates")
>>> str(exc)
'Cannot parse geometry coordinates'
Source code in src/owi/metadatabase/_utils/exceptions.py
def __init__(self, message: str = "Error during data processing.") -> None:
    super().__init__(message)
InvalidParameterError
InvalidParameterError(
    message="Invalid or missing parameters for the request.",
)

Bases: APIException

Exception raised when query parameters are invalid or missing.

This exception is raised before making API requests when the provided parameters fail validation checks or are inconsistent.

Parameters:

Name Type Description Default
message str

Human-readable error message, default is "Invalid or missing parameters for the request."

'Invalid or missing parameters for the request.'

Examples:

>>> exc = InvalidParameterError()
>>> str(exc)
'Invalid or missing parameters for the request.'
>>> exc = InvalidParameterError("Project name required")
>>> str(exc)
'Project name required'
Source code in src/owi/metadatabase/_utils/exceptions.py
def __init__(self, message: str = "Invalid or missing parameters for the request.") -> None:
    super().__init__(message)

Helper Functions

utils

Utility functions for the owimetadatabase_preprocessor package.

Functions

custom_formatwarning
custom_formatwarning(
    message, category, filename, lineno, line=None
)

Return customized warning.

Parameters:

Name Type Description Default
message str

Warning message.

required
category type

Warning category.

required
filename str

Filename where warning occurred.

required
lineno int

Line number where warning occurred.

required
line str

Line text where warning occurred.

None

Returns:

Type Description
str

Formatted warning string.

Examples:

>>> print(custom_formatwarning("warn", UserWarning, "file.py", 10), end="")
UserWarning: warn
Source code in src/owi/metadatabase/_utils/utils.py
def custom_formatwarning(message, category, filename, lineno, line=None):
    """
    Return customized warning.

    Parameters
    ----------
    message : str
        Warning message.
    category : type
        Warning category.
    filename : str
        Filename where warning occurred.
    lineno : int
        Line number where warning occurred.
    line : str, optional
        Line text where warning occurred.

    Returns
    -------
    str
        Formatted warning string.

    Examples
    --------
    >>> print(custom_formatwarning("warn", UserWarning, "file.py", 10), end="")
    UserWarning: warn
    """
    return f"{category.__name__}: {message}\n"
dict_generator
dict_generator(dict_, keys_=None, method_='exclude')

Generate a dictionary with the specified keys.

Parameters:

Name Type Description Default
dict_ dict

Dictionary to be filtered.

required
keys_ Sequence of str

List of keys to be included or excluded.

None
method_ str

Method to be used for filtering. Options are "exclude" and "include", default is "exclude".

'exclude'

Returns:

Type Description
dict

Filtered dictionary.

Raises:

Type Description
ValueError

If method is not recognized.

Examples:

>>> dict_generator({"a": 1, "b": 2}, keys_=["a"])
{'b': 2}
>>> dict_generator({"a": 1, "b": 2}, keys_=["a"], method_="include")
{'a': 1}
>>> dict_generator({"a": 1}, method_="unknown")
Traceback (most recent call last):
    ...
ValueError: Method not recognized!
Source code in src/owi/metadatabase/_utils/utils.py
def dict_generator(
    dict_: dict[str, Any],
    keys_: Sequence[str] | None = None,
    method_: str = "exclude",
) -> dict[str, Any]:
    """
    Generate a dictionary with the specified keys.

    Parameters
    ----------
    dict_ : dict
        Dictionary to be filtered.
    keys_ : Sequence of str, optional
        List of keys to be included or excluded.
    method_ : str, optional
        Method to be used for filtering. Options are "exclude" and
        "include", default is "exclude".

    Returns
    -------
    dict
        Filtered dictionary.

    Raises
    ------
    ValueError
        If method is not recognized.

    Examples
    --------
    >>> dict_generator({"a": 1, "b": 2}, keys_=["a"])
    {'b': 2}
    >>> dict_generator({"a": 1, "b": 2}, keys_=["a"], method_="include")
    {'a': 1}
    >>> dict_generator({"a": 1}, method_="unknown")  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
        ...
    ValueError: Method not recognized!
    """
    if keys_ is None:
        keys_ = []
    if method_ == "exclude":
        return {k: dict_[k] for k in dict_ if k not in keys_}
    elif method_ == "include":
        return {k: dict_[k] for k in dict_ if k in keys_}
    else:
        raise ValueError("Method not recognized!")
compare_if_simple_close
compare_if_simple_close(a, b, tol=1e-09)

Compare two values and return a boolean and a message.

Parameters:

Name Type Description Default
a Any

First value to be compared.

required
b Any

Second value to be compared.

required
tol float

Tolerance for the comparison, default is 1e-9.

1e-09

Returns:

Type Description
tuple

Tuple with a result of comparison and a message if different.

Examples:

>>> compare_if_simple_close(1.0, 1.0)
(True, None)
>>> compare_if_simple_close(1.0, 2.0)
(False, 'Values of 1.0 and 2.0 are different.')
>>> compare_if_simple_close(np.nan, np.nan)
(True, None)
Source code in src/owi/metadatabase/_utils/utils.py
def compare_if_simple_close(a: Any, b: Any, tol: float = 1e-9) -> tuple[bool, None | str]:
    """
    Compare two values and return a boolean and a message.

    Parameters
    ----------
    a : Any
        First value to be compared.
    b : Any
        Second value to be compared.
    tol : float, optional
        Tolerance for the comparison, default is 1e-9.

    Returns
    -------
    tuple
        Tuple with a result of comparison and a message if different.

    Examples
    --------
    >>> compare_if_simple_close(1.0, 1.0)
    (True, None)
    >>> compare_if_simple_close(1.0, 2.0)
    (False, 'Values of 1.0 and 2.0 are different.')
    >>> compare_if_simple_close(np.nan, np.nan)
    (True, None)
    """
    if isinstance(a, (float, np.floating)) and isinstance(b, (float, np.floating)):
        if np.isnan(a) and np.isnan(b):
            return True, None
        assertion = math.isclose(a, b, rel_tol=tol)
        messsage = None if assertion else f"Values of {a} and {b} are different."
        return assertion, messsage
    assertion = a == b
    messsage = None if assertion else f"Values of {a} and {b} are different."
    return assertion, messsage
check_df_eq
check_df_eq(df1, df2, tol=1e-09)

Check if two dataframes are equal.

Parameters:

Name Type Description Default
df1 DataFrame

First dataframe to be compared.

required
df2 DataFrame

Second dataframe to be compared.

required
tol float

Tolerance for the comparison, default is 1e-9.

1e-09

Returns:

Type Description
bool

Boolean indicating if the dataframes are equal.

Examples:

>>> df1 = pd.DataFrame({"a": [1.0, 2.0], "b": ["x", "y"]})
>>> df2 = pd.DataFrame({"a": [1.0, 2.0], "b": ["x", "y"]})
>>> check_df_eq(df1, df2)
True
>>> check_df_eq(pd.DataFrame(), pd.DataFrame())
True
Source code in src/owi/metadatabase/_utils/utils.py
def check_df_eq(df1: pd.DataFrame, df2: pd.DataFrame, tol: float = 1e-9) -> bool:
    """
    Check if two dataframes are equal.

    Parameters
    ----------
    df1 : pd.DataFrame
        First dataframe to be compared.
    df2 : pd.DataFrame
        Second dataframe to be compared.
    tol : float, optional
        Tolerance for the comparison, default is 1e-9.

    Returns
    -------
    bool
        Boolean indicating if the dataframes are equal.

    Examples
    --------
    >>> df1 = pd.DataFrame({"a": [1.0, 2.0], "b": ["x", "y"]})
    >>> df2 = pd.DataFrame({"a": [1.0, 2.0], "b": ["x", "y"]})
    >>> check_df_eq(df1, df2)
    True
    >>> check_df_eq(pd.DataFrame(), pd.DataFrame())
    True
    """
    if df1.empty and df2.empty:
        return True
    elif (df1.empty and not df2.empty) or (not df1.empty and df2.empty):
        return False
    if df1.shape != df2.shape:
        return False
    num_cols_eq = np.allclose(
        df1.select_dtypes(include="number"),
        df2.select_dtypes(include="number"),
        rtol=tol,
        atol=tol,
        equal_nan=True,
    )
    non_num_cols_eq = (
        df1.select_dtypes(exclude="number").astype(object).equals(df2.select_dtypes(exclude="number").astype(object))
    )
    return num_cols_eq and non_num_cols_eq
deepcompare
deepcompare(a, b, tol=1e-05)

Compare two complicated objects recursively.

Compares two complicated (potentially nested) objects recursively and returns a result and a message.

Parameters:

Name Type Description Default
a Any

First object to be compared.

required
b Any

Second object to be compared.

required
tol float

Tolerance for the comparison, default is 1e-5.

1e-05

Returns:

Type Description
tuple

Tuple with a result of comparison and a message if different.

Examples:

>>> deepcompare({"a": 1.0}, {"a": 1.0})[0]
True
>>> deepcompare([1, 2], [1, 3])[0]
False
>>> deepcompare(np.nan, np.nan)[0]
True
Source code in src/owi/metadatabase/_utils/utils.py
def deepcompare(a: Any, b: Any, tol: float = 1e-5) -> tuple[bool, None | str]:
    """
    Compare two complicated objects recursively.

    Compares two complicated (potentially nested) objects recursively
    and returns a result and a message.

    Parameters
    ----------
    a : Any
        First object to be compared.
    b : Any
        Second object to be compared.
    tol : float, optional
        Tolerance for the comparison, default is 1e-5.

    Returns
    -------
    tuple
        Tuple with a result of comparison and a message if different.

    Examples
    --------
    >>> deepcompare({"a": 1.0}, {"a": 1.0})[0]
    True
    >>> deepcompare([1, 2], [1, 3])[0]
    False
    >>> deepcompare(np.nan, np.nan)[0]
    True
    """
    if type(a) != type(b):  # noqa: E721
        if hasattr(a, "__dict__") and isinstance(b, dict):
            return deepcompare(a.__dict__, b, tol)
        elif hasattr(b, "__dict__") and isinstance(a, dict):
            return deepcompare(a, b.__dict__, tol)
        elif isinstance(a, (float, np.floating)) and isinstance(b, (float, np.floating)):
            return deepcompare(np.float64(a), np.float64(b), tol)
        return (
            False,
            f"Types of {a} and {b} are different: {type(a).__name__} and {type(b).__name__}.",
        )
    elif isinstance(a, dict):
        if a.keys() != b.keys():
            return (
                False,
                f"Dictionary keys {a.keys()} and {b.keys()} are different.",
            )
        compare = [deepcompare(a[key], b[key], tol)[0] for key in a]
        assertion = all(compare)
        if assertion:
            message = None
        else:
            keys = [key for key, val in zip(a.keys(), compare) if val is False]
            message = f"Dictionary values are different for {a} and {b}, for keys: {keys}."
        return assertion, message
    elif isinstance(a, (list, tuple)):
        if len(a) != len(b):
            return (
                False,
                f"Lists/tuples {a} and {b} are of different length: {len(a)} and {len(b)}.",
            )
        compare = [deepcompare(i, j, tol)[0] for i, j in zip(a, b)]
        assertion = all(compare)
        if assertion:
            message = None
        else:
            inds = [ind for ind, val in zip(range(len(compare)), compare) if val is False]
            message = f"Lists/tuples are different for {a} and {b}, for indices: {inds}."
        return assertion, message
    elif hasattr(a, "__dict__") and not isinstance(a, pd.DataFrame):
        return deepcompare(a.__dict__, b.__dict__, tol)
    elif isinstance(a, pd.DataFrame):
        assertion = check_df_eq(a, b, tol)
        message = None if assertion else f"Dataframes {a} and {b} are different for {a.compare(b)}."
        return assertion, message
    else:
        return compare_if_simple_close(a, b, tol)
fix_nan
fix_nan(obj)

Replace "nan" strings with None.

Parameters:

Name Type Description Default
obj Any

Object to be fixed.

required

Returns:

Type Description
Any

Fixed object.

Examples:

>>> fixed = fix_nan({"a": "NaN", "b": ["nan", "ok"]})
>>> bool(np.isnan(fixed["a"]))
True
>>> bool(np.isnan(fixed["b"][0]))
True
Source code in src/owi/metadatabase/_utils/utils.py
def fix_nan(obj: Any) -> Any:
    """
    Replace "nan" strings with None.

    Parameters
    ----------
    obj : Any
        Object to be fixed.

    Returns
    -------
    Any
        Fixed object.

    Examples
    --------
    >>> fixed = fix_nan({"a": "NaN", "b": ["nan", "ok"]})
    >>> bool(np.isnan(fixed["a"]))
    True
    >>> bool(np.isnan(fixed["b"][0]))
    True
    """
    if isinstance(obj, dict):
        for k, v in obj.items():
            obj[k] = fix_nan(v)
    elif isinstance(obj, list):
        for i in range(len(obj)):
            obj[i] = fix_nan(obj[i])
    elif isinstance(obj, str) and obj.lower() == "nan":
        # obj = None
        obj = np.nan
    return obj
fix_outline
fix_outline(data)

Fix the outline attribute in the data.

Parameters:

Name Type Description Default
data Any

Data to be fixed.

required

Returns:

Type Description
Any

Fixed data.

Raises:

Type Description
ValueError

If data type is not supported.

Examples:

>>> fix_outline({"outline": [[0, 1], [2, 3]]})
{'outline': ([0, 1], [2, 3])}
Source code in src/owi/metadatabase/_utils/utils.py
def fix_outline(data: Any) -> Any:
    """
    Fix the outline attribute in the data.

    Parameters
    ----------
    data : Any
        Data to be fixed.

    Returns
    -------
    Any
        Fixed data.

    Raises
    ------
    ValueError
        If data type is not supported.

    Examples
    --------
    >>> fix_outline({"outline": [[0, 1], [2, 3]]})
    {'outline': ([0, 1], [2, 3])}
    """
    if isinstance(data, list):
        for i in range(len(data)):
            if "outline" in data[i] and data[i]["outline"] is not None:
                data[i]["outline"] = tuple(data[i]["outline"])
    elif isinstance(data, dict):
        if "outline" in data and data["outline"] is not None:
            data["outline"] = tuple(data["outline"])
    else:
        raise ValueError("Not supported data type.")
    return data
hex_to_dec
hex_to_dec(value: str) -> list[float]
hex_to_dec(value: list[str]) -> list[list[float]]
hex_to_dec(value: tuple[str, ...]) -> list[list[float]]
hex_to_dec(value)

Convert hex color(s) to normalized [r, g, b, a] floats.

Accepts 6-digit (#rrggbb) or 8-digit (#rrggbbaa) hex strings, with or without leading '#'. - If value is a string: returns [r, g, b, a] - If value is a list of strings: returns [[r, g, b, a], ...]

Parameters:

Name Type Description Default
value str or Sequence of str

Hex color string or list of hex color strings.

required

Returns:

Type Description
list of float or list of list of float

Normalized RGBA list or list of such lists.

Raises:

Type Description
ValueError

If the hex string length is not 6 or 8, or if the input type is not supported.

Examples:

>>> hex_to_dec("#ff0000")
[1.0, 0.0, 0.0, 1.0]
>>> hex_to_dec(["#000000", "ffffff"])
[[0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0, 1.0]]
Source code in src/owi/metadatabase/_utils/utils.py
def hex_to_dec(value: str | Sequence[str]) -> list[float] | list[list[float]]:
    """
    Convert hex color(s) to normalized [r, g, b, a] floats.

    Accepts 6-digit (#rrggbb) or 8-digit (#rrggbbaa) hex strings, with
    or without leading '#'.
    - If `value` is a string: returns [r, g, b, a]
    - If `value` is a list of strings: returns [[r, g, b, a], ...]

    Parameters
    ----------
    value : str or Sequence of str
        Hex color string or list of hex color strings.

    Returns
    -------
    list of float or list of list of float
        Normalized RGBA list or list of such lists.

    Raises
    ------
    ValueError
        If the hex string length is not 6 or 8, or if the input type
        is not supported.

    Examples
    --------
    >>> hex_to_dec("#ff0000")
    [1.0, 0.0, 0.0, 1.0]
    >>> hex_to_dec(["#000000", "ffffff"])
    [[0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0, 1.0]]
    """

    def _hex_to_dec(s: str) -> list[float]:
        s = s.lstrip("#") if s.startswith("#") else s
        if len(s) not in (6, 8):
            raise ValueError("Length of the color string must be 6 or 8 (excluding #)")
        r = int(s[0:2], 16) / 255.0
        g = int(s[2:4], 16) / 255.0
        b = int(s[4:6], 16) / 255.0
        a = int(s[6:8], 16) / 255.0 if len(s) == 8 else 1.0
        return [r, g, b, a]

    if isinstance(value, str):
        return _hex_to_dec(value)
    elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
        return [_hex_to_dec(v) for v in value]
    raise ValueError("Value must be a string or a list of strings.")
load_token_from_env_file
load_token_from_env_file(
    env_file, env_var="OWI_METADATABASE_API_TOKEN"
)

Load a token from an environment file.

Parameters:

Name Type Description Default
env_file Path

Path to the environment file.

required
env_var str

Environment variable name, by default "OWI_METADATABASE_API_TOKEN"

'OWI_METADATABASE_API_TOKEN'

Returns:

Type Description
str | None

The token value if found, otherwise None.

Source code in src/owi/metadatabase/_utils/utils.py
def load_token_from_env_file(env_file: Path, env_var: str = "OWI_METADATABASE_API_TOKEN") -> str | None:
    """Load a token from an environment file.

    Parameters
    ----------
    env_file : Path
        Path to the environment file.
    env_var : str, optional
        Environment variable name, by default "OWI_METADATABASE_API_TOKEN"

    Returns
    -------
    str | None
        The token value if found, otherwise None.
    """
    if not env_file.exists():
        return None
    for line in env_file.read_text(encoding="utf-8").splitlines():
        if line.startswith(f"{env_var}="):
            token = line.split("=", 1)[1].strip()
            return token or None
    return None