Source code for acro.utils
"""ACRO: Utility Functions."""
from __future__ import annotations
import logging
import os
from inspect import FrameInfo, getframeinfo
import numpy as np
import pandas as pd
from tabulate import tabulate
from .constants import ARTIFACTS_DIR
logger = logging.getLogger("acro")
# Allowed values for the disclosure-control ``mitigation`` field.
# Lives here so both :mod:`acro.acro_tables` and :mod:`acro.acro_stata_parser`
# can share a single source of truth.
ALLOWED_MITIGATIONS: frozenset[str] = frozenset({"none", "suppress", "round"})
[docs]
def is_blocked_extension(filename: str, blocked_extensions: list[str]) -> bool:
"""Return True and log a warning if the file's extension is blocked."""
_, ext = os.path.splitext(filename)
if ext.lower() in blocked_extensions:
logger.warning(
"Blocked file extension %s. Files with extension %s are not allowed.",
filename,
ext,
)
return True
return False
[docs]
def get_command(default: str, stack_list: list[FrameInfo]) -> str:
"""Return the calling source line as a string.
Parameters
----------
default : str
Default string to return if unable to extract the stack.
stack_list : list[tuple]
A list of frame records for the caller's stack. The first entry in the
returned list represents the caller; the last entry represents the
outermost call on the stack.
Returns
-------
str
The calling source line.
"""
command: str = default
if len(stack_list) > 1:
code: list[str] | None = getframeinfo(stack_list[1][0]).code_context
if code is not None:
command = "\n".join(code).strip()
logger.debug("command: %s", command)
return command
[docs]
def prettify_table_string(table: pd.DataFrame, separator: str | None = None) -> str:
"""Add delimiters to table.to_string() to improve readability for onscreen display.
Splits fields on whitespace unless an optional separator is provided e.g. ',' for csv.
"""
mytable = table.copy()
# be mindful that everything in thr join has to be a string
mytable.columns = [
"\n".join(map(str, col)) if isinstance(col, tuple) else col
for col in mytable.columns
]
# other way
# table.columns= [' '.join(col).strip()for col in table.columns.values]
mytable.reset_index(inplace=True)
return tabulate(
mytable, headers="keys", showindex=False, tablefmt="rounded_outline"
)
[docs]
def get_unique_artefact_filename(filename: str) -> str:
"""Return a unique filename from a proposed string."""
# CREATE artifacts DIRECTORY to save plot in
try:
os.makedirs(ARTIFACTS_DIR)
logger.debug("Directory %s created successfully", ARTIFACTS_DIR)
except FileExistsError: # pragma: no cover
logger.debug("Directory %s already exists", ARTIFACTS_DIR)
# CREATE UNIQUE FILENAME to avoid overwrite
filename, extension = os.path.splitext(filename)
if not extension: # pragma: no cover
logger.info("Please provide a valid file extension")
return "None"
increment_number = 0
while os.path.exists(
f"{ARTIFACTS_DIR}/{filename}_{increment_number}{extension}"
): # pragma: no cover
increment_number += 1
unique_filename = f"{ARTIFACTS_DIR}/{filename}_{increment_number}{extension}"
return unique_filename
[docs]
def get_catdtype(series: pd.Series) -> pd.CategoricalDtype:
"""Get info for pandas datatype to convert series to CategoricalDtype."""
ordered = True if series.astype(int, errors="ignore").dtype == "int64" else False
categories = np.sort(series.dropna().explode().unique())
cat_type = pd.CategoricalDtype(categories, ordered)
return cat_type