Skip to content

Utils

The helpers/utils subpackage provides utility classes with static methods for common operations including date/time handling, string manipulation, file I/O, JWT tokens, passwords, TOTP, Keycloak integration, OpenTelemetry, and application-level utilities.

Base Utils

Base utility class providing foundational helpers shared across other utility classes.

Shared base utility helpers.

archipy.helpers.utils.base_utils.IRANIAN_MOBILE_LOCAL_LEN module-attribute

IRANIAN_MOBILE_LOCAL_LEN = 10

archipy.helpers.utils.base_utils.IRANIAN_NATIONAL_CODE_LEN module-attribute

IRANIAN_NATIONAL_CODE_LEN = 10

archipy.helpers.utils.base_utils.NATIONAL_CODE_CHECKSUM_THRESHOLD module-attribute

NATIONAL_CODE_CHECKSUM_THRESHOLD = 2

archipy.helpers.utils.base_utils.NATIONAL_CODE_MODULUS module-attribute

NATIONAL_CODE_MODULUS = 11

archipy.helpers.utils.base_utils.BaseUtils

Bases: ErrorUtils, DatetimeUtils, PasswordUtils, JWTUtils, TOTPUtils, FileUtils, StringUtils

A utility class that combines multiple utility functionalities into a single class.

This class inherits from various utility classes to provide a centralized place for common utility methods.

Source code in archipy/helpers/utils/base_utils.py
class BaseUtils(ErrorUtils, DatetimeUtils, PasswordUtils, JWTUtils, TOTPUtils, FileUtils, StringUtils):
    """A utility class that combines multiple utility functionalities into a single class.

    This class inherits from various utility classes to provide a centralized place for common utility methods.
    """

    @staticmethod
    def sanitize_iranian_landline_or_phone_number(landline_or_phone_number: str) -> str:
        """Sanitize an Iranian landline or mobile phone number.

        Removes non-numeric characters and standardizes the format.

        Args:
            landline_or_phone_number (str): The phone number to sanitize.

        Returns:
            str: The sanitized phone number in a standardized format.
        """
        # Remove non-numeric characters
        cleaned_number = re.sub(r"\D", "", landline_or_phone_number)

        # Standardize international format to local Iran format
        if cleaned_number.startswith("0098"):  # Handles "0098"
            cleaned_number = "0" + cleaned_number[4:]  # Replace "0098" with "0"
        elif cleaned_number.startswith("98"):  # Handles "+98"
            cleaned_number = "0" + cleaned_number[2:]  # Replace "98" with "0"

        # Ensure mobile numbers start with '09'
        if len(cleaned_number) == IRANIAN_MOBILE_LOCAL_LEN and cleaned_number.startswith("9"):
            cleaned_number = "0" + cleaned_number  # Convert "9123456789" → "09123456789"

        return cleaned_number

    @classmethod
    def validate_iranian_phone_number(cls, phone_number: str) -> None:
        """Validates an Iranian mobile phone number.

        Args:
            phone_number (str): The phone number to validate.

        Raises:
            InvalidPhoneNumberError: If the phone number is invalid.
        """
        # Sanitize the input to remove spaces, dashes, or other non-numeric characters
        sanitized_number = cls.sanitize_iranian_landline_or_phone_number(phone_number)
        # Define the regular expression pattern for Iranian phone numbers
        iranian_mobile_pattern = re.compile(r"^09\d{9}$")  # Mobile numbers

        # Check if the phone number matches either mobile or landline pattern
        if not iranian_mobile_pattern.match(sanitized_number):
            raise InvalidPhoneNumberError(phone_number)

    @classmethod
    def validate_iranian_landline_number(cls, landline_number: str) -> None:
        """Validates an Iranian landline number.

        Args:
            landline_number (str): The landline number to validate.

        Raises:
            InvalidLandlineNumberError: If the landline number is invalid.
        """
        # Sanitize the input to remove spaces, dashes, or other non-numeric characters
        sanitized_number = cls.sanitize_iranian_landline_or_phone_number(landline_number)
        # Landline examples: `0` + 2 to 4-digit area code + 7 to 8-digit local number
        iranian_landline_pattern = re.compile(r"^0\d{2,4}\d{7,8}$")

        if not iranian_landline_pattern.match(sanitized_number):
            raise InvalidLandlineNumberError(landline_number)

    @classmethod
    def validate_iranian_national_code_pattern(cls, national_code: str) -> None:
        """Validates an Iranian National ID number using the official algorithm.

        To see how the algorithm works, see http://www.aliarash.com/article/codemeli/codemeli.htm

        The algorithm works by:
        1. Checking if the ID is exactly 10 digits
        2. Multiplying each digit (except the last) by its position weight
        3. Summing these products
        4. Calculating the remainder when divided by 11
        5. Comparing the check digit based on specific rules

        Args:
            national_code (str): A string containing the national ID to validate.

        Raises:
            InvalidNationalCodeError: If the ID is invalid due to length or checksum.
        """

        def _validate_length(national_code: str) -> None:
            """Validates that the national code is exactly 10 digits long.

            Args:
                national_code (str): The national code to validate.

            Raises:
                InvalidNationalCodeError: If the code is not exactly 10 ASCII digits, or is a single repeated digit.
            """
            if not len(national_code) == IRANIAN_NATIONAL_CODE_LEN:
                raise InvalidNationalCodeError(national_code)
            if not (national_code.isascii() and national_code.isdigit()):
                raise InvalidNationalCodeError(national_code)
            # Codes made of one repeated digit satisfy the checksum but are not issued
            if len(set(national_code)) == 1:
                raise InvalidNationalCodeError(national_code)

        def _calculate_weighted_sum(national_code: str) -> int:
            """Calculates the weighted sum of the national code digits.

            Args:
                national_code (str): The national code to calculate the weighted sum for.

            Returns:
                int: The weighted sum of the national code digits.
            """
            return sum(int(digit) * (10 - i) for i, digit in enumerate(national_code[:-1]))

        def _get_checksums(national_code: str) -> tuple[int, int]:
            """Calculates the expected and actual checksums for the national code.

            Args:
                national_code (str): The national code to calculate checksums for.

            Returns:
                tuple[int, int]: A tuple containing the calculated checksum and the actual checksum.
            """
            weighted_sum = _calculate_weighted_sum(national_code)
            remainder = weighted_sum % 11

            calculated_checksum = (
                remainder if remainder < NATIONAL_CODE_CHECKSUM_THRESHOLD else NATIONAL_CODE_MODULUS - remainder
            )
            actual_checksum = int(national_code[-1])

            return calculated_checksum, actual_checksum

        _validate_length(national_code)
        calculated_checksum, actual_checksum = _get_checksums(national_code)
        if calculated_checksum != actual_checksum:
            raise InvalidNationalCodeError(national_code)

archipy.helpers.utils.base_utils.BaseUtils.arabic_vowel_translate_table class-attribute instance-attribute

arabic_vowel_translate_table = str.maketrans(
    dict.fromkeys("ًٌَُِّْٓءٍٰۖۗۘۙۚۛ", "")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_akoolad_alef_translate_table class-attribute instance-attribute

alphabet_akoolad_alef_translate_table = str.maketrans(
    dict.fromkeys("ﺁ", "آ")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_alef_translate_table class-attribute instance-attribute

alphabet_alef_translate_table = str.maketrans(
    dict.fromkeys("ﺎٲٱإﺍأٵٳ", "ا")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_be_translate_table class-attribute instance-attribute

alphabet_be_translate_table = str.maketrans(
    dict.fromkeys("ﺐﺏﺑٻٮ", "ب")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_pe_translate_table class-attribute instance-attribute

alphabet_pe_translate_table = str.maketrans(
    dict.fromkeys("ﭖﭗﭙﺒﭘڀݐݒݕ", "پ")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_te_translate_table class-attribute instance-attribute

alphabet_te_translate_table = str.maketrans(
    dict.fromkeys("ﭡٺٹﭞٿټﺕﺗﺖﺘݓ", "ت")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_se_translate_table class-attribute instance-attribute

alphabet_se_translate_table = str.maketrans(
    dict.fromkeys("ﺙﺛٽﺚﺜ", "ث")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_jim_translate_table class-attribute instance-attribute

alphabet_jim_translate_table = str.maketrans(
    dict.fromkeys("ﺝﺠﺟﺞۚ", "ج")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_che_translate_table class-attribute instance-attribute

alphabet_che_translate_table = str.maketrans(
    dict.fromkeys("ڃﭽﭼڇڄݘڿﭻ", "چ")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_he_translate_table class-attribute instance-attribute

alphabet_he_translate_table = str.maketrans(
    dict.fromkeys("ﺢﺤڅځﺣﺡ", "ح")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_khe_translate_table class-attribute instance-attribute

alphabet_khe_translate_table = str.maketrans(
    dict.fromkeys("ﺥﺦﺨﺧڂݗ", "خ")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_dal_translate_table class-attribute instance-attribute

alphabet_dal_translate_table = str.maketrans(
    dict.fromkeys("ډﺪﺩڊڈڍܥ", "د")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_zal_translate_table class-attribute instance-attribute

alphabet_zal_translate_table = str.maketrans(
    dict.fromkeys("ﺫﺬڐڏڎڌ", "ذ")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_re_translate_table class-attribute instance-attribute

alphabet_re_translate_table = str.maketrans(
    dict.fromkeys("ڗڒڑڕﺭﺮږڔړڒڑۯ", "ر")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_ze_translate_table class-attribute instance-attribute

alphabet_ze_translate_table = str.maketrans(
    dict.fromkeys("ﺰﺯ", "ز")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_zhe_translate_table class-attribute instance-attribute

alphabet_zhe_translate_table = str.maketrans(
    dict.fromkeys("ﮊڙﮋ", "ژ")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_sin_translate_table class-attribute instance-attribute

alphabet_sin_translate_table = str.maketrans(
    dict.fromkeys("ݭݜﺱﺲﺴﺳڛښۣݾݽ", "س")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_shin_translate_table class-attribute instance-attribute

alphabet_shin_translate_table = str.maketrans(
    dict.fromkeys("ﺵﺶﺸﺷڜۺ", "ش")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_sad_translate_table class-attribute instance-attribute

alphabet_sad_translate_table = str.maketrans(
    dict.fromkeys("ﺹﺺﺼﺻڝ", "ص")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_zad_translate_table class-attribute instance-attribute

alphabet_zad_translate_table = str.maketrans(
    dict.fromkeys("ﺽﺾﺿﻀۻڞ", "ض")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_ta_translate_table class-attribute instance-attribute

alphabet_ta_translate_table = str.maketrans(
    dict.fromkeys("ﻁﻂﻃﻄ", "ط")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_za_translate_table class-attribute instance-attribute

alphabet_za_translate_table = str.maketrans(
    dict.fromkeys("ﻆﻇﻈڟﻅ", "ظ")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_eyn_translate_table class-attribute instance-attribute

alphabet_eyn_translate_table = str.maketrans(
    dict.fromkeys("ڠﻉﻊﻋﻌ", "ع")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_gheyn_translate_table class-attribute instance-attribute

alphabet_gheyn_translate_table = str.maketrans(
    dict.fromkeys("ﻎۼﻍﻐﻏݝݞݟ", "غ")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_fe_translate_table class-attribute instance-attribute

alphabet_fe_translate_table = str.maketrans(
    dict.fromkeys("ﻒﻑﻔﻓڡڥڦڤڣڢ", "ف")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_ghaf_translate_table class-attribute instance-attribute

alphabet_ghaf_translate_table = str.maketrans(
    dict.fromkeys("ﻕﻖﻗڧڨﻘ", "ق")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_kaf_translate_table class-attribute instance-attribute

alphabet_kaf_translate_table = str.maketrans(
    dict.fromkeys("ڭﻚﮎﻜﮏګﻛﮑﮐڪكݢݣݤڬڮݿﻙ", "ک")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_gaf_translate_table class-attribute instance-attribute

alphabet_gaf_translate_table = str.maketrans(
    dict.fromkeys("ﮚﮒﮓﮕﮔڱڰڲڳڴ", "گ")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_lam_translate_table class-attribute instance-attribute

alphabet_lam_translate_table = str.maketrans(
    dict.fromkeys("ﻟﻝﻞﻠݪڷڸڶڵ", "ل")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_mim_translate_table class-attribute instance-attribute

alphabet_mim_translate_table = str.maketrans(
    dict.fromkeys("ﻡﻤﻢﻣݦݥ", "م")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_nun_translate_table class-attribute instance-attribute

alphabet_nun_translate_table = str.maketrans(
    dict.fromkeys("ڼﻦﻥﻧﻨݩݨݧڻڽںڹ", "ن")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_vav_translate_table class-attribute instance-attribute

alphabet_vav_translate_table = str.maketrans(
    dict.fromkeys("ވﯙۈۋﺆۊۇۏۅۉﻭﻮؤۆۄ", "و")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_ha_translate_table class-attribute instance-attribute

alphabet_ha_translate_table = str.maketrans(
    dict.fromkeys("ﺓﮭﺔﻬھﻩﻫﻪۀەةہܣܤܝ", "ه")
)

archipy.helpers.utils.base_utils.BaseUtils.alphabet_ye_translate_table class-attribute instance-attribute

alphabet_ye_translate_table = str.maketrans(
    dict.fromkeys("ﯨﭛﻯۍﻰﻱﻲﻳﻴﯼېﯽﯾﯿێےىيٸۑؽؾؿﺉﺋﺌ", "ی")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table1 class-attribute instance-attribute

punctuation_translate_table1 = str.maketrans(
    dict.fromkeys("¬", " ")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table2 class-attribute instance-attribute

punctuation_translate_table2 = str.maketrans(
    dict.fromkeys("•·●·・∙。ⴰ", ".")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table3 class-attribute instance-attribute

punctuation_translate_table3 = str.maketrans(
    dict.fromkeys("٬٫‚,", "،")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table4 class-attribute instance-attribute

punctuation_translate_table4 = str.maketrans(
    dict.fromkeys("ʕ⁉�", "؟")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table5 class-attribute instance-attribute

punctuation_translate_table5 = str.maketrans(
    dict.fromkeys("‼❕", "!")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table7 class-attribute instance-attribute

punctuation_translate_table7 = str.maketrans(
    dict.fromkeys("-━−‐‑–—─−ー⁃", "-")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table8 class-attribute instance-attribute

punctuation_translate_table8 = str.maketrans(
    dict.fromkeys("‹《﴾", "«")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table9 class-attribute instance-attribute

punctuation_translate_table9 = str.maketrans(
    dict.fromkeys("›》﴿", "»")
)

archipy.helpers.utils.base_utils.BaseUtils.ascii_punctuation_patterns class-attribute instance-attribute

ascii_punctuation_patterns: list[
    tuple[Pattern[str], str]
] = compile_patterns(
    [
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff]),|,(?=\\s*[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])",
            "،",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff\\u061f])\\?",
            "؟",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff\\u061b]);",
            "؛",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])%",
            "٪",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])_(?=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])",
            "ـ",
        ),
    ]
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table12 class-attribute instance-attribute

punctuation_translate_table12 = str.maketrans(
    dict.fromkeys("ˈ‘’“”", "'")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_translate_table13 class-attribute instance-attribute

punctuation_translate_table13 = str.maketrans(
    dict.fromkeys(":", ":")
)

archipy.helpers.utils.base_utils.BaseUtils.character_refinement_patterns class-attribute instance-attribute

character_refinement_patterns: list[
    tuple[Pattern[str], str]
] = compile_patterns(
    [(" +", " "), ("\\n\\n+", "\n"), (" ?\\.\\.\\.", " …")]
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_after class-attribute instance-attribute

punctuation_after = '\\.:!،؛؟»\\]\\)\\}'

archipy.helpers.utils.base_utils.BaseUtils.punctuation_before class-attribute instance-attribute

punctuation_before = '«\\[\\(\\{'

archipy.helpers.utils.base_utils.BaseUtils.punctuation_spacing_patterns class-attribute instance-attribute

punctuation_spacing_patterns = compile_patterns(
    [
        (f" ([{punctuation_after}])", "\\1"),
        (f"([{punctuation_before}]) ", "\\1"),
        (
            f"(?<![A-Za-z0-9])([{punctuation_after[:3]}])([^ {punctuation_after}"
            + "\\d])",
            "\\1 \\2",
        ),
        (
            f"([{punctuation_after[3:]}])([^ {punctuation_after}])",
            "\\1 \\2",
        ),
        (
            f"([^ {punctuation_before}])([{punctuation_before}])",
            "\\1 \\2",
        ),
    ]
)

archipy.helpers.utils.base_utils.BaseUtils.number_zero_translate_table class-attribute instance-attribute

number_zero_translate_table = str.maketrans(
    dict.fromkeys("۰٠", "0")
)

archipy.helpers.utils.base_utils.BaseUtils.number_one_translate_table class-attribute instance-attribute

number_one_translate_table = str.maketrans(
    dict.fromkeys("۱١", "1")
)

archipy.helpers.utils.base_utils.BaseUtils.number_two_translate_table class-attribute instance-attribute

number_two_translate_table = str.maketrans(
    dict.fromkeys("۲٢", "2")
)

archipy.helpers.utils.base_utils.BaseUtils.number_three_translate_table class-attribute instance-attribute

number_three_translate_table = str.maketrans(
    dict.fromkeys("۳٣", "3")
)

archipy.helpers.utils.base_utils.BaseUtils.number_four_translate_table class-attribute instance-attribute

number_four_translate_table = str.maketrans(
    dict.fromkeys("۴٤", "4")
)

archipy.helpers.utils.base_utils.BaseUtils.number_five_translate_table class-attribute instance-attribute

number_five_translate_table = str.maketrans(
    dict.fromkeys("۵٥", "5")
)

archipy.helpers.utils.base_utils.BaseUtils.number_six_translate_table class-attribute instance-attribute

number_six_translate_table = str.maketrans(
    dict.fromkeys("۶٦", "6")
)

archipy.helpers.utils.base_utils.BaseUtils.number_seven_translate_table class-attribute instance-attribute

number_seven_translate_table = str.maketrans(
    dict.fromkeys("۷٧", "7")
)

archipy.helpers.utils.base_utils.BaseUtils.number_eight_translate_table class-attribute instance-attribute

number_eight_translate_table = str.maketrans(
    dict.fromkeys("۸٨", "8")
)

archipy.helpers.utils.base_utils.BaseUtils.number_nine_translate_table class-attribute instance-attribute

number_nine_translate_table = str.maketrans(
    dict.fromkeys("۹٩", "9")
)

archipy.helpers.utils.base_utils.BaseUtils.punctuation_persian_marks_to_space_translate_table class-attribute instance-attribute

punctuation_persian_marks_to_space_translate_table = (
    str.maketrans(
        dict.fromkeys(".:!،؛؟»])}«[({-ـ٪!'\"#+/,?;%_", " ")
    )
)

archipy.helpers.utils.base_utils.BaseUtils.sanitize_iranian_landline_or_phone_number staticmethod

sanitize_iranian_landline_or_phone_number(
    landline_or_phone_number: str,
) -> str

Sanitize an Iranian landline or mobile phone number.

Removes non-numeric characters and standardizes the format.

Parameters:

Name Type Description Default
landline_or_phone_number str

The phone number to sanitize.

required

Returns:

Name Type Description
str str

The sanitized phone number in a standardized format.

Source code in archipy/helpers/utils/base_utils.py
@staticmethod
def sanitize_iranian_landline_or_phone_number(landline_or_phone_number: str) -> str:
    """Sanitize an Iranian landline or mobile phone number.

    Removes non-numeric characters and standardizes the format.

    Args:
        landline_or_phone_number (str): The phone number to sanitize.

    Returns:
        str: The sanitized phone number in a standardized format.
    """
    # Remove non-numeric characters
    cleaned_number = re.sub(r"\D", "", landline_or_phone_number)

    # Standardize international format to local Iran format
    if cleaned_number.startswith("0098"):  # Handles "0098"
        cleaned_number = "0" + cleaned_number[4:]  # Replace "0098" with "0"
    elif cleaned_number.startswith("98"):  # Handles "+98"
        cleaned_number = "0" + cleaned_number[2:]  # Replace "98" with "0"

    # Ensure mobile numbers start with '09'
    if len(cleaned_number) == IRANIAN_MOBILE_LOCAL_LEN and cleaned_number.startswith("9"):
        cleaned_number = "0" + cleaned_number  # Convert "9123456789" → "09123456789"

    return cleaned_number

archipy.helpers.utils.base_utils.BaseUtils.validate_iranian_phone_number classmethod

validate_iranian_phone_number(phone_number: str) -> None

Validates an Iranian mobile phone number.

Parameters:

Name Type Description Default
phone_number str

The phone number to validate.

required

Raises:

Type Description
InvalidPhoneNumberError

If the phone number is invalid.

Source code in archipy/helpers/utils/base_utils.py
@classmethod
def validate_iranian_phone_number(cls, phone_number: str) -> None:
    """Validates an Iranian mobile phone number.

    Args:
        phone_number (str): The phone number to validate.

    Raises:
        InvalidPhoneNumberError: If the phone number is invalid.
    """
    # Sanitize the input to remove spaces, dashes, or other non-numeric characters
    sanitized_number = cls.sanitize_iranian_landline_or_phone_number(phone_number)
    # Define the regular expression pattern for Iranian phone numbers
    iranian_mobile_pattern = re.compile(r"^09\d{9}$")  # Mobile numbers

    # Check if the phone number matches either mobile or landline pattern
    if not iranian_mobile_pattern.match(sanitized_number):
        raise InvalidPhoneNumberError(phone_number)

archipy.helpers.utils.base_utils.BaseUtils.validate_iranian_landline_number classmethod

validate_iranian_landline_number(
    landline_number: str,
) -> None

Validates an Iranian landline number.

Parameters:

Name Type Description Default
landline_number str

The landline number to validate.

required

Raises:

Type Description
InvalidLandlineNumberError

If the landline number is invalid.

Source code in archipy/helpers/utils/base_utils.py
@classmethod
def validate_iranian_landline_number(cls, landline_number: str) -> None:
    """Validates an Iranian landline number.

    Args:
        landline_number (str): The landline number to validate.

    Raises:
        InvalidLandlineNumberError: If the landline number is invalid.
    """
    # Sanitize the input to remove spaces, dashes, or other non-numeric characters
    sanitized_number = cls.sanitize_iranian_landline_or_phone_number(landline_number)
    # Landline examples: `0` + 2 to 4-digit area code + 7 to 8-digit local number
    iranian_landline_pattern = re.compile(r"^0\d{2,4}\d{7,8}$")

    if not iranian_landline_pattern.match(sanitized_number):
        raise InvalidLandlineNumberError(landline_number)

archipy.helpers.utils.base_utils.BaseUtils.validate_iranian_national_code_pattern classmethod

validate_iranian_national_code_pattern(
    national_code: str,
) -> None

Validates an Iranian National ID number using the official algorithm.

To see how the algorithm works, see http://www.aliarash.com/article/codemeli/codemeli.htm

The algorithm works by: 1. Checking if the ID is exactly 10 digits 2. Multiplying each digit (except the last) by its position weight 3. Summing these products 4. Calculating the remainder when divided by 11 5. Comparing the check digit based on specific rules

Parameters:

Name Type Description Default
national_code str

A string containing the national ID to validate.

required

Raises:

Type Description
InvalidNationalCodeError

If the ID is invalid due to length or checksum.

Source code in archipy/helpers/utils/base_utils.py
@classmethod
def validate_iranian_national_code_pattern(cls, national_code: str) -> None:
    """Validates an Iranian National ID number using the official algorithm.

    To see how the algorithm works, see http://www.aliarash.com/article/codemeli/codemeli.htm

    The algorithm works by:
    1. Checking if the ID is exactly 10 digits
    2. Multiplying each digit (except the last) by its position weight
    3. Summing these products
    4. Calculating the remainder when divided by 11
    5. Comparing the check digit based on specific rules

    Args:
        national_code (str): A string containing the national ID to validate.

    Raises:
        InvalidNationalCodeError: If the ID is invalid due to length or checksum.
    """

    def _validate_length(national_code: str) -> None:
        """Validates that the national code is exactly 10 digits long.

        Args:
            national_code (str): The national code to validate.

        Raises:
            InvalidNationalCodeError: If the code is not exactly 10 ASCII digits, or is a single repeated digit.
        """
        if not len(national_code) == IRANIAN_NATIONAL_CODE_LEN:
            raise InvalidNationalCodeError(national_code)
        if not (national_code.isascii() and national_code.isdigit()):
            raise InvalidNationalCodeError(national_code)
        # Codes made of one repeated digit satisfy the checksum but are not issued
        if len(set(national_code)) == 1:
            raise InvalidNationalCodeError(national_code)

    def _calculate_weighted_sum(national_code: str) -> int:
        """Calculates the weighted sum of the national code digits.

        Args:
            national_code (str): The national code to calculate the weighted sum for.

        Returns:
            int: The weighted sum of the national code digits.
        """
        return sum(int(digit) * (10 - i) for i, digit in enumerate(national_code[:-1]))

    def _get_checksums(national_code: str) -> tuple[int, int]:
        """Calculates the expected and actual checksums for the national code.

        Args:
            national_code (str): The national code to calculate checksums for.

        Returns:
            tuple[int, int]: A tuple containing the calculated checksum and the actual checksum.
        """
        weighted_sum = _calculate_weighted_sum(national_code)
        remainder = weighted_sum % 11

        calculated_checksum = (
            remainder if remainder < NATIONAL_CODE_CHECKSUM_THRESHOLD else NATIONAL_CODE_MODULUS - remainder
        )
        actual_checksum = int(national_code[-1])

        return calculated_checksum, actual_checksum

    _validate_length(national_code)
    calculated_checksum, actual_checksum = _get_checksums(national_code)
    if calculated_checksum != actual_checksum:
        raise InvalidNationalCodeError(national_code)

archipy.helpers.utils.base_utils.BaseUtils.remove_arabic_vowels classmethod

remove_arabic_vowels(text: str) -> str

Removes Arabic vowels (tashkeel) from the text.

Parameters:

Name Type Description Default
text str

The input text containing Arabic vowels.

required

Returns:

Name Type Description
str str

The text with Arabic vowels removed.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def remove_arabic_vowels(cls, text: str) -> str:
    """Removes Arabic vowels (tashkeel) from the text.

    Args:
        text (str): The input text containing Arabic vowels.

    Returns:
        str: The text with Arabic vowels removed.
    """
    return text.translate(cls.arabic_vowel_translate_table)

archipy.helpers.utils.base_utils.BaseUtils.normalize_persian_chars classmethod

normalize_persian_chars(text: str) -> str

Normalizes Persian characters to their standard forms.

Parameters:

Name Type Description Default
text str

The input text containing Persian characters.

required

Returns:

Name Type Description
str str

The text with Persian characters normalized.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_persian_chars(cls, text: str) -> str:
    """Normalizes Persian characters to their standard forms.

    Args:
        text (str): The input text containing Persian characters.

    Returns:
        str: The text with Persian characters normalized.
    """
    text = text.translate(cls.alphabet_akoolad_alef_translate_table)
    text = text.translate(cls.alphabet_alef_translate_table)
    text = text.translate(cls.alphabet_be_translate_table)
    text = text.translate(cls.alphabet_pe_translate_table)
    text = text.translate(cls.alphabet_te_translate_table)
    text = text.translate(cls.alphabet_se_translate_table)
    text = text.translate(cls.alphabet_jim_translate_table)
    text = text.translate(cls.alphabet_che_translate_table)
    text = text.translate(cls.alphabet_he_translate_table)
    text = text.translate(cls.alphabet_khe_translate_table)
    text = text.translate(cls.alphabet_dal_translate_table)
    text = text.translate(cls.alphabet_zal_translate_table)
    text = text.translate(cls.alphabet_re_translate_table)
    text = text.translate(cls.alphabet_ze_translate_table)
    text = text.translate(cls.alphabet_zhe_translate_table)
    text = text.translate(cls.alphabet_sin_translate_table)
    text = text.translate(cls.alphabet_shin_translate_table)
    text = text.translate(cls.alphabet_sad_translate_table)
    text = text.translate(cls.alphabet_zad_translate_table)
    text = text.translate(cls.alphabet_ta_translate_table)
    text = text.translate(cls.alphabet_za_translate_table)
    text = text.translate(cls.alphabet_eyn_translate_table)
    text = text.translate(cls.alphabet_gheyn_translate_table)
    text = text.translate(cls.alphabet_fe_translate_table)
    text = text.translate(cls.alphabet_ghaf_translate_table)
    text = text.translate(cls.alphabet_kaf_translate_table)
    text = text.translate(cls.alphabet_gaf_translate_table)
    text = text.translate(cls.alphabet_lam_translate_table)
    text = text.translate(cls.alphabet_mim_translate_table)
    text = text.translate(cls.alphabet_nun_translate_table)
    text = text.translate(cls.alphabet_vav_translate_table)
    text = text.translate(cls.alphabet_ha_translate_table)
    return text.translate(cls.alphabet_ye_translate_table)

archipy.helpers.utils.base_utils.BaseUtils.normalize_punctuation classmethod

normalize_punctuation(text: str) -> str

Normalizes punctuation marks in the text.

ASCII ,, ?, ;, % and _ are converted to their Persian forms only next to Persian text, so numbers (1,000), URLs, e-mails and identifiers are left intact.

Parameters:

Name Type Description Default
text str

The input text containing punctuation marks.

required

Returns:

Name Type Description
str str

The text with punctuation marks normalized.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_punctuation(cls, text: str) -> str:
    """Normalizes punctuation marks in the text.

    ASCII ``,``, ``?``, ``;``, ``%`` and ``_`` are converted to their Persian forms only next to Persian text, so
    numbers (``1,000``), URLs, e-mails and identifiers are left intact.

    Args:
        text (str): The input text containing punctuation marks.

    Returns:
        str: The text with punctuation marks normalized.
    """
    for pattern, repl in cls.ascii_punctuation_patterns:
        text = pattern.sub(repl, text)
    text = text.translate(cls.punctuation_translate_table1)
    text = text.translate(cls.punctuation_translate_table2)
    text = text.translate(cls.punctuation_translate_table3)
    text = text.translate(cls.punctuation_translate_table4)
    text = text.translate(cls.punctuation_translate_table5)
    text = text.translate(cls.punctuation_translate_table7)
    text = text.translate(cls.punctuation_translate_table8)
    text = text.translate(cls.punctuation_translate_table9)
    text = text.translate(cls.punctuation_translate_table12)
    return text.translate(cls.punctuation_translate_table13)

archipy.helpers.utils.base_utils.BaseUtils.normalize_numbers classmethod

normalize_numbers(text: str) -> str

Normalizes numbers in the text to English format.

Parameters:

Name Type Description Default
text str

The input text containing numbers.

required

Returns:

Name Type Description
str str

The text with numbers normalized to English format.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_numbers(cls, text: str) -> str:
    """Normalizes numbers in the text to English format.

    Args:
        text (str): The input text containing numbers.

    Returns:
        str: The text with numbers normalized to English format.
    """
    text = text.translate(cls.number_zero_translate_table)
    text = text.translate(cls.number_one_translate_table)
    text = text.translate(cls.number_two_translate_table)
    text = text.translate(cls.number_three_translate_table)
    text = text.translate(cls.number_four_translate_table)
    text = text.translate(cls.number_five_translate_table)
    text = text.translate(cls.number_six_translate_table)
    text = text.translate(cls.number_seven_translate_table)
    text = text.translate(cls.number_eight_translate_table)
    return text.translate(cls.number_nine_translate_table)

archipy.helpers.utils.base_utils.BaseUtils.clean_spacing classmethod

clean_spacing(text: str, *, keep_zwnj: bool = False) -> str

Cleans up spacing issues in the text, such as non-breaking spaces and zero-width non-joiners.

Parameters:

Name Type Description Default
text str

The input text with spacing issues.

required
keep_zwnj bool

Keep zero-width non-joiners, which Persian words such as "می‌خواهم" need. When False (the default) they are turned into spaces.

False

Returns:

Name Type Description
str str

The text with spacing cleaned up.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def clean_spacing(cls, text: str, *, keep_zwnj: bool = False) -> str:
    """Cleans up spacing issues in the text, such as non-breaking spaces and zero-width non-joiners.

    Args:
        text (str): The input text with spacing issues.
        keep_zwnj (bool): Keep zero-width non-joiners, which Persian words such as "می‌خواهم" need. When
            `False` (the default) they are turned into spaces.

    Returns:
        str: The text with spacing cleaned up.
    """
    if not keep_zwnj:
        text = text.replace("\u200c", " ")  # ZWNJ
    text = text.replace("\xa0", " ")  # NBSP

    for pattern, repl in cls.character_refinement_patterns:
        text = pattern.sub(repl, text)

    return text

archipy.helpers.utils.base_utils.BaseUtils.normalize_punctuation_spacing classmethod

normalize_punctuation_spacing(text: str) -> str

Applies proper spacing around punctuation marks.

Parameters:

Name Type Description Default
text str

The input text with punctuation spacing issues.

required

Returns:

Name Type Description
str str

The text with proper spacing around punctuation marks.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_punctuation_spacing(cls, text: str) -> str:
    """Applies proper spacing around punctuation marks.

    Args:
        text (str): The input text with punctuation spacing issues.

    Returns:
        str: The text with proper spacing around punctuation marks.
    """
    for pattern, repl in cls.punctuation_spacing_patterns:
        text = pattern.sub(repl, text)
    return text

archipy.helpers.utils.base_utils.BaseUtils.remove_punctuation_marks classmethod

remove_punctuation_marks(text: str) -> str

Removes punctuation marks from the text.

Parameters:

Name Type Description Default
text str

The input text containing punctuation marks.

required

Returns:

Name Type Description
str str

The text with punctuation marks removed.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def remove_punctuation_marks(cls, text: str) -> str:
    """Removes punctuation marks from the text.

    Args:
        text (str): The input text containing punctuation marks.

    Returns:
        str: The text with punctuation marks removed.
    """
    return text.translate(cls.punctuation_persian_marks_to_space_translate_table)

archipy.helpers.utils.base_utils.BaseUtils.mask_urls classmethod

mask_urls(text: str, mask: str | None = None) -> str

Masks URLs in the text with a specified mask.

Parameters:

Name Type Description Default
text str

The input text containing URLs.

required
mask str | None

The mask to replace URLs with. Defaults to "MASK_URL".

None

Returns:

Name Type Description
str str

The text with URLs masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def mask_urls(cls, text: str, mask: str | None = None) -> str:
    """Masks URLs in the text with a specified mask.

    Args:
        text (str): The input text containing URLs.
        mask (str | None): The mask to replace URLs with. Defaults to "MASK_URL".

    Returns:
        str: The text with URLs masked.
    """
    mask = mask or "MASK_URL"
    return re_compile(r"https?://\S+|www\.\S+").sub(f" {mask} ", text)

archipy.helpers.utils.base_utils.BaseUtils.mask_emails classmethod

mask_emails(text: str, mask: str | None = None) -> str

Masks email addresses in the text with a specified mask.

Parameters:

Name Type Description Default
text str

The input text containing email addresses.

required
mask str | None

The mask to replace emails with. Defaults to "MASK_EMAIL".

None

Returns:

Name Type Description
str str

The text with email addresses masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def mask_emails(cls, text: str, mask: str | None = None) -> str:
    """Masks email addresses in the text with a specified mask.

    Args:
        text (str): The input text containing email addresses.
        mask (str | None): The mask to replace emails with. Defaults to "MASK_EMAIL".

    Returns:
        str: The text with email addresses masked.
    """
    mask = mask or "MASK_EMAIL"
    return re_compile(r"\S+@\S+\.\S+").sub(f" {mask} ", text)

archipy.helpers.utils.base_utils.BaseUtils.mask_phones classmethod

mask_phones(text: str, mask: str | None = None) -> str

Masks phone numbers in the text with a specified mask.

Parameters:

Name Type Description Default
text str

The input text containing phone numbers.

required
mask str | None

The mask to replace phone numbers with. Defaults to "MASK_PHONE".

None

Returns:

Name Type Description
str str

The text with phone numbers masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def mask_phones(cls, text: str, mask: str | None = None) -> str:
    """Masks phone numbers in the text with a specified mask.

    Args:
        text (str): The input text containing phone numbers.
        mask (str | None): The mask to replace phone numbers with. Defaults to "MASK_PHONE".

    Returns:
        str: The text with phone numbers masked.
    """
    mask = mask or "MASK_PHONE"
    return re_compile(r"(?:\+98|0)?(?:\d{3}\s*?\d{3}\s*?\d{4})").sub(f" {mask} ", text)

archipy.helpers.utils.base_utils.BaseUtils.convert_english_number_to_persian classmethod

convert_english_number_to_persian(text: str) -> str

Converts English numbers to Persian numbers in the text.

Parameters:

Name Type Description Default
text str

The input text containing English numbers.

required

Returns:

Name Type Description
str str

The text with English numbers converted to Persian numbers.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def convert_english_number_to_persian(cls, text: str) -> str:
    """Converts English numbers to Persian numbers in the text.

    Args:
        text (str): The input text containing English numbers.

    Returns:
        str: The text with English numbers converted to Persian numbers.
    """
    return text.translate(ENGLISH_TO_PERSIAN_DIGITS_TABLE)

archipy.helpers.utils.base_utils.BaseUtils.convert_numbers_to_english classmethod

convert_numbers_to_english(text: str) -> str

Converts Persian/Arabic numbers to English numbers in the text.

Parameters:

Name Type Description Default
text str

The input text containing Persian/Arabic numbers.

required

Returns:

Name Type Description
str str

The text with Persian/Arabic numbers converted to English numbers.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def convert_numbers_to_english(cls, text: str) -> str:
    """Converts Persian/Arabic numbers to English numbers in the text.

    Args:
        text (str): The input text containing Persian/Arabic numbers.

    Returns:
        str: The text with Persian/Arabic numbers converted to English numbers.
    """
    table = {
        1776: 48,  # 0
        1777: 49,  # 1
        1778: 50,  # 2
        1779: 51,  # 3
        1780: 52,  # 4
        1781: 53,  # 5
        1782: 54,  # 6
        1783: 55,  # 7
        1784: 56,  # 8
        1785: 57,  # 9
        1632: 48,  # 0
        1633: 49,  # 1
        1634: 50,  # 2
        1635: 51,  # 3
        1636: 52,  # 4
        1637: 53,  # 5
        1638: 54,  # 6
        1639: 55,  # 7
        1640: 56,  # 8
        1641: 57,  # 9
    }
    return text.translate(table)

archipy.helpers.utils.base_utils.BaseUtils.convert_add_3digit_delimiter classmethod

convert_add_3digit_delimiter(value: int) -> str

Adds thousand separators to numbers.

Parameters:

Name Type Description Default
value int

The number to format.

required

Returns:

Name Type Description
str str

The formatted number with thousand separators. Non-integer values are returned as str(value).

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def convert_add_3digit_delimiter(cls, value: int) -> str:
    """Adds thousand separators to numbers.

    Args:
        value (int): The number to format.

    Returns:
        str: The formatted number with thousand separators. Non-integer values are returned as `str(value)`.
    """
    return f"{value:,}" if isinstance(value, int) else str(value)

archipy.helpers.utils.base_utils.BaseUtils.remove_emoji classmethod

remove_emoji(text: str) -> str

Removes emoji characters from the text.

Parameters:

Name Type Description Default
text str

The input text containing emojis.

required

Returns:

Name Type Description
str str

The text with emojis removed.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def remove_emoji(cls, text: str) -> str:
    """Removes emoji characters from the text.

    Args:
        text (str): The input text containing emojis.

    Returns:
        str: The text with emojis removed.
    """
    emoji_pattern = re.compile(
        r"["
        r"\U0001F600-\U0001F64F"  # emoticons
        r"\U0001F300-\U0001F5FF"  # symbols & pictographs
        r"\U0001F680-\U0001F6FF"  # transport & map symbols
        r"\U0001F1E0-\U0001F1FF"  # flags
        r"\U0001F900-\U0001F9FF"  # supplemental symbols and pictographs
        r"\U0001FA00-\U0001FA6F"  # symbols and pictographs extended-A
        r"\U00002600-\U000026FF"  # miscellaneous symbols (some are emojis)
        r"\U00002700-\U000027BF"  # dingbats (some are emojis)
        r"\U00002190-\U000021FF"  # arrows (some are emojis)
        r"]+",
        re.UNICODE,
    )
    return emoji_pattern.sub(r"", text)

archipy.helpers.utils.base_utils.BaseUtils.replace_currencies_with_mask classmethod

replace_currencies_with_mask(
    text: str, mask: str | None = None
) -> str

Masks currency symbols in the text (the amounts next to them are not masked).

Parameters:

Name Type Description Default
text str

The input text containing currency symbols.

required
mask str | None

The mask to replace currency symbols with. Defaults to "MASK_CURRENCIES".

None

Returns:

Name Type Description
str str

The text with currency symbols masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def replace_currencies_with_mask(cls, text: str, mask: str | None = None) -> str:
    """Masks currency symbols in the text (the amounts next to them are not masked).

    Args:
        text (str): The input text containing currency symbols.
        mask (str | None): The mask to replace currency symbols with. Defaults to "MASK_CURRENCIES".

    Returns:
        str: The text with currency symbols masked.
    """
    mask = mask or "MASK_CURRENCIES"
    currency_pattern = re_compile(r"(zł|£|\$|₡|₦|¥|₩|₪|₫|€|₱|₲|₴|₹|﷼)+")
    return currency_pattern.sub(f" {mask} ", text)

archipy.helpers.utils.base_utils.BaseUtils.replace_numbers_with_mask classmethod

replace_numbers_with_mask(
    text: str, mask: str | None = None
) -> str

Masks numbers in the text.

Parameters:

Name Type Description Default
text str

The input text containing numbers.

required
mask str | None

The mask to replace numbers with. Defaults to "MASK_NUMBERS".

None

Returns:

Name Type Description
str str

The text with numbers masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def replace_numbers_with_mask(cls, text: str, mask: str | None = None) -> str:
    """Masks numbers in the text.

    Args:
        text (str): The input text containing numbers.
        mask (str | None): The mask to replace numbers with. Defaults to "MASK_NUMBERS".

    Returns:
        str: The text with numbers masked.
    """
    mask = mask or "MASK_NUMBERS"
    # A single pass, so a mask that itself contains digits (e.g. "NUM1") is never rewritten
    return re.sub("[0-9]+", lambda _: f" {mask} ", str(text))

archipy.helpers.utils.base_utils.BaseUtils.is_string_none_or_empty classmethod

is_string_none_or_empty(text: str | None) -> bool

Checks if a string is None or empty (after stripping whitespace).

Parameters:

Name Type Description Default
text str | None

The input string to check.

required

Returns:

Name Type Description
bool bool

True if the string is None or empty, False otherwise.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def is_string_none_or_empty(cls, text: str | None) -> bool:
    """Checks if a string is `None` or empty (after stripping whitespace).

    Args:
        text (str | None): The input string to check.

    Returns:
        bool: `True` if the string is `None` or empty, `False` otherwise.
    """
    return text is None or (isinstance(text, str) and not text.strip())

archipy.helpers.utils.base_utils.BaseUtils.normalize_persian_text classmethod

normalize_persian_text(
    text: str,
    *,
    remove_vowels: bool = True,
    normalize_punctuation: bool = True,
    normalize_numbers: bool = True,
    normalize_persian_chars: bool = True,
    mask_urls: bool = False,
    mask_emails: bool = False,
    mask_phones: bool = False,
    mask_currencies: bool = False,
    mask_all_numbers: bool = False,
    remove_emojis: bool = False,
    url_mask: str | None = None,
    email_mask: str | None = None,
    phone_mask: str | None = None,
    currency_mask: str | None = None,
    number_mask: str | None = None,
    clean_spacing: bool = True,
    remove_punctuation: bool = False,
    normalize_punctuation_spacing: bool = False,
    keep_zwnj: bool = False,
) -> str

Normalizes Persian text with configurable options.

Parameters:

Name Type Description Default
text str

The input text to normalize.

required
remove_vowels bool

Whether to remove Arabic vowels. Defaults to True.

True
normalize_punctuation bool

Whether to normalize punctuation marks. Defaults to True.

True
normalize_numbers bool

Whether to normalize numbers to English format. Defaults to True.

True
normalize_persian_chars bool

Whether to normalize Persian characters. Defaults to True.

True
mask_urls bool

Whether to mask URLs. Defaults to False.

False
mask_emails bool

Whether to mask email addresses. Defaults to False.

False
mask_phones bool

Whether to mask phone numbers. Defaults to False.

False
mask_currencies bool

Whether to mask currency symbols and amounts. Defaults to False.

False
mask_all_numbers bool

Whether to mask all numbers. Defaults to False.

False
remove_emojis bool

Whether to remove emojis. Defaults to False.

False
url_mask str | None

The mask to replace URLs with. Defaults to None.

None
email_mask str | None

The mask to replace email addresses with. Defaults to None.

None
phone_mask str | None

The mask to replace phone numbers with. Defaults to None.

None
currency_mask str | None

The mask to replace currency symbols and amounts with. Defaults to None.

None
number_mask str | None

The mask to replace numbers with. Defaults to None.

None
clean_spacing bool

Whether to clean up spacing issues. Defaults to True.

True
remove_punctuation bool

Whether to remove punctuation marks. Defaults to False.

False
normalize_punctuation_spacing bool

Whether to apply proper spacing around punctuation marks. Defaults to False.

False
keep_zwnj bool

Whether to keep zero-width non-joiners when cleaning spacing instead of turning them into spaces. Defaults to False.

False

Returns:

Name Type Description
str str

The normalized text.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_persian_text(
    cls,
    text: str,
    *,
    remove_vowels: bool = True,
    normalize_punctuation: bool = True,
    normalize_numbers: bool = True,
    normalize_persian_chars: bool = True,
    mask_urls: bool = False,
    mask_emails: bool = False,
    mask_phones: bool = False,
    mask_currencies: bool = False,
    mask_all_numbers: bool = False,
    remove_emojis: bool = False,
    url_mask: str | None = None,
    email_mask: str | None = None,
    phone_mask: str | None = None,
    currency_mask: str | None = None,
    number_mask: str | None = None,
    clean_spacing: bool = True,
    remove_punctuation: bool = False,
    normalize_punctuation_spacing: bool = False,
    keep_zwnj: bool = False,
) -> str:
    """Normalizes Persian text with configurable options.

    Args:
        text (str): The input text to normalize.
        remove_vowels (bool): Whether to remove Arabic vowels. Defaults to `True`.
        normalize_punctuation (bool): Whether to normalize punctuation marks. Defaults to `True`.
        normalize_numbers (bool): Whether to normalize numbers to English format. Defaults to `True`.
        normalize_persian_chars (bool): Whether to normalize Persian characters. Defaults to `True`.
        mask_urls (bool): Whether to mask URLs. Defaults to `False`.
        mask_emails (bool): Whether to mask email addresses. Defaults to `False`.
        mask_phones (bool): Whether to mask phone numbers. Defaults to `False`.
        mask_currencies (bool): Whether to mask currency symbols and amounts. Defaults to `False`.
        mask_all_numbers (bool): Whether to mask all numbers. Defaults to `False`.
        remove_emojis (bool): Whether to remove emojis. Defaults to `False`.
        url_mask (str | None): The mask to replace URLs with. Defaults to `None`.
        email_mask (str | None): The mask to replace email addresses with. Defaults to `None`.
        phone_mask (str | None): The mask to replace phone numbers with. Defaults to `None`.
        currency_mask (str | None): The mask to replace currency symbols and amounts with. Defaults to `None`.
        number_mask (str | None): The mask to replace numbers with. Defaults to `None`.
        clean_spacing (bool): Whether to clean up spacing issues. Defaults to `True`.
        remove_punctuation (bool): Whether to remove punctuation marks. Defaults to `False`.
        normalize_punctuation_spacing (bool): Whether to apply proper spacing around
            punctuation marks. Defaults to `False`.
        keep_zwnj (bool): Whether to keep zero-width non-joiners when cleaning spacing instead of turning them
            into spaces. Defaults to `False`.

    Returns:
        str: The normalized text.
    """
    if not text:
        return text

    if remove_emojis:
        text = cls.remove_emoji(text)

    text = cls._apply_persian_text_normalizations(
        text,
        remove_vowels=remove_vowels,
        normalize_persian_chars=normalize_persian_chars,
        normalize_punctuation=normalize_punctuation,
        remove_punctuation=remove_punctuation,
        normalize_numbers=normalize_numbers,
    )

    text = cls._apply_persian_text_masks(
        text,
        mask_urls=mask_urls,
        mask_emails=mask_emails,
        mask_phones=mask_phones,
        mask_currencies=mask_currencies,
        mask_all_numbers=mask_all_numbers,
        url_mask=url_mask,
        email_mask=email_mask,
        phone_mask=phone_mask,
        currency_mask=currency_mask,
        number_mask=number_mask,
    )

    if clean_spacing:
        text = cls.clean_spacing(text, keep_zwnj=keep_zwnj)
    if normalize_punctuation_spacing:
        text = cls.normalize_punctuation_spacing(text)

    return text.strip()

archipy.helpers.utils.base_utils.BaseUtils.snake_to_camel_case classmethod

snake_to_camel_case(text: str) -> str

Converts snake_case to camelCase.

Parameters:

Name Type Description Default
text str

The input text in snake_case format.

required

Returns:

Name Type Description
str str

The text converted to camelCase format.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def snake_to_camel_case(cls, text: str) -> str:
    """Converts snake_case to camelCase.

    Args:
        text (str): The input text in snake_case format.

    Returns:
        str: The text converted to camelCase format.
    """
    if cls.is_string_none_or_empty(text):
        return text

    components = text.split("_")
    # First component remains lowercase, the rest get capitalized
    return components[0] + "".join(x[:1].upper() + x[1:] for x in components[1:])

archipy.helpers.utils.base_utils.BaseUtils.camel_to_snake_case classmethod

camel_to_snake_case(text: str) -> str

Converts camelCase to snake_case.

Parameters:

Name Type Description Default
text str

The input text in camelCase format.

required

Returns:

Name Type Description
str str

The text converted to snake_case format.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def camel_to_snake_case(cls, text: str) -> str:
    """Converts camelCase to snake_case.

    Args:
        text (str): The input text in camelCase format.

    Returns:
        str: The text converted to snake_case format.
    """
    if cls.is_string_none_or_empty(text):
        return text

    # Add underscore before each capital letter and convert to lowercase
    s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
    return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
create_secure_link(
    path: str,
    minutes: int | None = None,
    file_config: FileConfig | None = None,
) -> str

Creates a secure link with expiration for file access.

Parameters:

Name Type Description Default
path str

The file path to create a secure link for.

required
minutes int | None

Number of minutes until link expiration. Defaults to the config's DEFAULT_EXPIRY_MINUTES.

None
file_config FileConfig | None

Optional file configuration object. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

A secure link with a hash and expiration timestamp.

Raises:

Type Description
InvalidArgumentError

If the path is empty.

OutOfRangeError

If minutes is less than 1.

Source code in archipy/helpers/utils/file_utils.py
@classmethod
def create_secure_link(
    cls,
    path: str,
    minutes: int | None = None,
    file_config: FileConfig | None = None,
) -> str:
    """Creates a secure link with expiration for file access.

    Args:
        path (str): The file path to create a secure link for.
        minutes (int | None): Number of minutes until link expiration.
            Defaults to the config's `DEFAULT_EXPIRY_MINUTES`.
        file_config (FileConfig | None): Optional file configuration object.
            If not provided, uses the global config.

    Returns:
        str: A secure link with a hash and expiration timestamp.

    Raises:
        InvalidArgumentError: If the `path` is empty.
        OutOfRangeError: If `minutes` is less than 1.
    """
    if not path:
        raise InvalidArgumentError(argument_name="path")

    configs: FileConfig = file_config or BaseConfig.global_config().FILE
    expiry_minutes: int = minutes if minutes is not None else configs.DEFAULT_EXPIRY_MINUTES

    if expiry_minutes < 1:
        raise OutOfRangeError(field_name="minutes")

    expires_at = int(DatetimeUtils.get_datetime_after_given_datetime_or_now(minutes=expiry_minutes).timestamp())
    secure_link_hash = cls._create_secure_link_hash(path, expires_at, file_config)

    return f"{path}?md5={secure_link_hash}&expires_at={expires_at}"

archipy.helpers.utils.base_utils.BaseUtils.validate_file_name classmethod

validate_file_name(
    file_name: str, file_config: FileConfig | None = None
) -> bool

Validates a file name based on allowed extensions.

Parameters:

Name Type Description Default
file_name str

The file name to validate.

required
file_config FileConfig | None

Optional file configuration object. If not provided, uses the global config.

None

Returns:

Name Type Description
bool bool

True if the file name has an allowed extension, False otherwise.

Raises:

Type Description
InvalidArgumentError

If file_name is not a string or allowed_extensions is not a list.

Source code in archipy/helpers/utils/file_utils.py
@classmethod
def validate_file_name(
    cls,
    file_name: str,
    file_config: FileConfig | None = None,
) -> bool:
    """Validates a file name based on allowed extensions.

    Args:
        file_name (str): The file name to validate.
        file_config (FileConfig | None): Optional file configuration object.
            If not provided, uses the global config.

    Returns:
        bool: `True` if the file name has an allowed extension, `False` otherwise.

    Raises:
        InvalidArgumentError: If `file_name` is not a string or `allowed_extensions` is not a list.
    """
    configs: FileConfig = file_config or BaseConfig.global_config().FILE
    allowed_extensions: list[str] = configs.ALLOWED_EXTENSIONS

    if not isinstance(file_name, str):
        raise InvalidArgumentError(argument_name="file_name")

    if not allowed_extensions:
        raise InvalidArgumentError(argument_name="allowed_extensions")

    file_path = Path(file_name)
    ext = file_path.suffix[1:].lower()
    # Compare case-insensitively and accept entries written as ".pdf" or "PDF"
    normalized_allowed = {allowed.lstrip(".").lower() for allowed in allowed_extensions}
    return bool(ext) and ext in normalized_allowed

archipy.helpers.utils.base_utils.BaseUtils.generate_totp classmethod

generate_totp(
    secret: str | UUID,
    auth_config: AuthConfig | None = None,
) -> tuple[str, datetime]

Generates a TOTP code using the configured hash algorithm.

Parameters:

Name Type Description Default
secret str | UUID

The secret key used to generate the TOTP code.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
str

A tuple containing the generated TOTP code and its expiration time. The expiration

datetime

is TOTP_EXPIRES_IN from now, capped at the moment verify_totp stops accepting the code. A code

tuple[str, datetime]

stays valid for that whole period: this helper keeps no state, so callers must enforce single use and

tuple[str, datetime]

attempt limits (TOTP_MAX_ATTEMPTS, TOTP_LOCKOUT_TIME) themselves, for example with a cache.

Raises:

Type Description
InvalidArgumentError

If the secret is invalid or empty.

Source code in archipy/helpers/utils/totp_utils.py
@classmethod
def generate_totp(cls, secret: str | UUID, auth_config: AuthConfig | None = None) -> tuple[str, datetime]:
    """Generates a TOTP code using the configured hash algorithm.

    Args:
        secret: The secret key used to generate the TOTP code.
        auth_config: Optional auth configuration override. If not provided, uses the global config.

    Returns:
        A tuple containing the generated TOTP code and its expiration time. The expiration
        is ``TOTP_EXPIRES_IN`` from now, capped at the moment ``verify_totp`` stops accepting the code. A code
        stays valid for that whole period: this helper keeps no state, so callers must enforce single use and
        attempt limits (``TOTP_MAX_ATTEMPTS``, ``TOTP_LOCKOUT_TIME``) themselves, for example with a cache.

    Raises:
        InvalidArgumentError: If the secret is invalid or empty.
    """
    if not secret:
        raise InvalidArgumentError(
            argument_name="secret",
        )

    configs = auth_config or BaseConfig.global_config().AUTH

    # Get current timestamp and calculate time step
    current_time = DatetimeUtils.get_epoch_time_now()
    time_step_counter = int(current_time / configs.TOTP_TIME_STEP)

    # Use the dedicated TOTP hash algorithm from config, with fallback to SHA1
    hash_algo = getattr(configs, "TOTP_HASH_ALGORITHM", "SHA1")
    totp_code = _compute_totp(_secret_to_key(secret), time_step_counter, hash_algo, configs.TOTP_LENGTH)

    # The code is accepted until the last step of the verification window ends, so never report a later expiry.
    valid_until = (time_step_counter + configs.TOTP_VERIFICATION_WINDOW + 1) * configs.TOTP_TIME_STEP
    lifetime = min(configs.TOTP_EXPIRES_IN, max(valid_until - current_time, 0))
    expires_in = DatetimeUtils.get_datetime_after_given_datetime_or_now(seconds=lifetime)

    return totp_code, expires_in

archipy.helpers.utils.base_utils.BaseUtils.verify_totp classmethod

verify_totp(
    secret: str | UUID,
    totp_code: str,
    auth_config: AuthConfig | None = None,
    replay_guard: Callable[[int], bool] | None = None,
) -> bool

Verifies a TOTP code against the provided secret.

This helper keeps no state. Two protections therefore belong to the caller: counting failed attempts (TOTP_MAX_ATTEMPTS / TOTP_LOCKOUT_TIME are not enforced here) and rejecting a code that was already used. For the second one pass replay_guard.

Parameters:

Name Type Description Default
secret str | UUID

The secret key used to generate the TOTP code.

required
totp_code str

The TOTP code to verify.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None
replay_guard Callable[[int], bool] | None

Optional callback invoked with the matched time step (an int counter) once the code is valid. Return True to accept the code, False to reject it as already used. Typical use is an atomic "store the step if it is greater than the last accepted one" on the user record or in Redis (RFC 6238 section 5.2). It is not called for invalid codes.

None

Returns:

Type Description
bool

True if the TOTP code is valid, False otherwise (including a code whose length differs from

bool

TOTP_LENGTH).

Raises:

Type Description
InvalidArgumentError

If the secret is invalid or empty.

InvalidTokenError

If the TOTP code format is invalid.

Source code in archipy/helpers/utils/totp_utils.py
@classmethod
def verify_totp(
    cls,
    secret: str | UUID,
    totp_code: str,
    auth_config: AuthConfig | None = None,
    replay_guard: Callable[[int], bool] | None = None,
) -> bool:
    """Verifies a TOTP code against the provided secret.

    This helper keeps no state. Two protections therefore belong to the caller: counting failed attempts
    (``TOTP_MAX_ATTEMPTS`` / ``TOTP_LOCKOUT_TIME`` are not enforced here) and rejecting a code that was already
    used. For the second one pass ``replay_guard``.

    Args:
        secret: The secret key used to generate the TOTP code.
        totp_code: The TOTP code to verify.
        auth_config: Optional auth configuration override. If not provided, uses the global config.
        replay_guard: Optional callback invoked with the matched time step (an ``int`` counter) once the code
            is valid. Return ``True`` to accept the code, ``False`` to reject it as already used. Typical use is
            an atomic "store the step if it is greater than the last accepted one" on the user record or in
            Redis (RFC 6238 section 5.2). It is not called for invalid codes.

    Returns:
        `True` if the TOTP code is valid, `False` otherwise (including a code whose length differs from
        ``TOTP_LENGTH``).

    Raises:
        InvalidArgumentError: If the secret is invalid or empty.
        InvalidTokenError: If the TOTP code format is invalid.
    """
    if not secret:
        raise InvalidArgumentError(
            argument_name="secret",
        )

    if not totp_code:
        raise InvalidArgumentError(
            argument_name="totp_code",
        )

    if not (totp_code.isascii() and totp_code.isdigit()):
        raise InvalidTokenError

    configs = auth_config or BaseConfig.global_config().AUTH

    # The expected length comes from the config, never from the caller's input.
    if len(totp_code) != configs.TOTP_LENGTH:
        return False

    current_time = DatetimeUtils.get_epoch_time_now()

    # Use the dedicated TOTP hash algorithm from config, with fallback to SHA1
    hash_algo = getattr(configs, "TOTP_HASH_ALGORITHM", "SHA1")

    key = _secret_to_key(secret)
    current_step = int(current_time / configs.TOTP_TIME_STEP)

    # Check every step in the window without early exit so timing does not reveal which step matched
    matched_step: int | None = None
    for i in range(-configs.TOTP_VERIFICATION_WINDOW, configs.TOTP_VERIFICATION_WINDOW + 1):
        computed_totp = _compute_totp(key, current_step + i, hash_algo, configs.TOTP_LENGTH)
        if hmac.compare_digest(totp_code, computed_totp):
            matched_step = current_step + i

    if matched_step is None:
        return False
    return replay_guard is None or bool(replay_guard(matched_step))

archipy.helpers.utils.base_utils.BaseUtils.generate_secret_key_for_totp staticmethod

generate_secret_key_for_totp(
    auth_config: AuthConfig | None = None,
) -> str

Generates a random secret key for TOTP initialization.

Parameters:

Name Type Description Default
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
str

A base32-encoded secret key for TOTP initialization.

Raises:

Type Description
InvalidArgumentError

If the TOTP_SECRET_KEY is not configured.

InternalError

If there is an error generating the secret key.

Source code in archipy/helpers/utils/totp_utils.py
@staticmethod
def generate_secret_key_for_totp(auth_config: AuthConfig | None = None) -> str:
    """Generates a random secret key for TOTP initialization.

    Args:
        auth_config: Optional auth configuration override. If not provided, uses the global config.

    Returns:
        A base32-encoded secret key for TOTP initialization.

    Raises:
        InvalidArgumentError: If the TOTP_SECRET_KEY is not configured.
        InternalError: If there is an error generating the secret key.
    """
    try:
        configs = auth_config or BaseConfig.global_config().AUTH

        # Use secrets module instead of random for better security
        random_bytes = secrets.token_bytes(configs.SALT_LENGTH)

        # Check if TOTP secret key is configured
        if not configs.TOTP_SECRET_KEY:
            _raise_missing_totp_secret()

        master_key = configs.TOTP_SECRET_KEY.get_secret_value().encode("utf-8")

        # Use the dedicated TOTP hash algorithm from config, with fallback to SHA1
        hash_algo = getattr(configs, "TOTP_HASH_ALGORITHM", "SHA1")

        # Use HMAC with master key for additional security
        hmac_obj = hmac.new(master_key, random_bytes, hash_algo)
        return base64.b32encode(hmac_obj.digest()).decode("utf-8")
    except BaseError:
        raise
    except Exception as e:
        # Convert any other errors to our custom errors
        raise InternalError(additional_data={"operation": "generate_secret_key_for_totp"}) from e

archipy.helpers.utils.base_utils.BaseUtils.create_token classmethod

create_token(
    data: Mapping[str, object],
    expires_in: int,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str

Creates a JWT token with enhanced security features.

Parameters:

Name Type Description Default
data Mapping[str, object]

Base claims data to include in the token.

required
expires_in int

Token expiration time in seconds.

required
additional_claims Mapping[str, object] | None

Optional additional claims to include in the token. They cannot redefine iss, aud, jti or any key already present in data (for example sub, type and token_version of access and refresh tokens); time claims may be overridden.

None
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

The encoded JWT token.

Raises:

Type Description
InvalidArgumentError

If data is empty, the expiration is invalid, additional_claims collides with reserved claims, the secret key is missing or too short for the algorithm (HS256 needs 32 bytes, HS384 48, HS512 64), or the algorithm is not configured.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def create_token(
    cls,
    data: Mapping[str, object],
    expires_in: int,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str:
    """Creates a JWT token with enhanced security features.

    Args:
        data (Mapping[str, object]): Base claims data to include in the token.
        expires_in (int): Token expiration time in seconds.
        additional_claims (Mapping[str, object] | None): Optional additional claims to include in the token. They
            cannot redefine ``iss``, ``aud``, ``jti`` or any key already present in ``data`` (for example
            ``sub``, ``type`` and ``token_version`` of access and refresh tokens); time claims may be overridden.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: The encoded JWT token.

    Raises:
        InvalidArgumentError: If data is empty, the expiration is invalid, ``additional_claims`` collides with
            reserved claims, the secret key is missing or too short for the algorithm (HS256 needs 32 bytes,
            HS384 48, HS512 64), or the algorithm is not configured.
    """
    import jwt

    configs = auth_config or BaseConfig.global_config().AUTH
    current_time = DatetimeUtils.get_datetime_utc_now()

    # Define argument names
    arg_data = "data"
    arg_expires_in = "expires_in"

    if not data:
        raise InvalidArgumentError(arg_data)
    if expires_in <= 0:
        raise InvalidArgumentError(arg_expires_in)
    if additional_claims and (reserved := (_PROTECTED_CLAIMS | data.keys()) & additional_claims.keys()):
        raise InvalidArgumentError("additional_claims", additional_data={"reserved_claims": sorted(reserved)})

    to_encode: dict[str, object] = dict(data)
    expire = DatetimeUtils.get_datetime_after_given_datetime_or_now(seconds=expires_in, datetime_given=current_time)

    # Add standard claims
    to_encode.update(
        {
            # Registered claims (RFC 7519)
            "iss": configs.JWT_ISSUER,
            "aud": configs.JWT_AUDIENCE,
            "exp": expire,
            "iat": current_time,
            "nbf": current_time,
        },
    )

    # Add JWT ID if enabled
    if configs.ENABLE_JTI_CLAIM:
        to_encode["jti"] = str(uuid4())

    # Add additional claims
    if additional_claims:
        to_encode.update(additional_claims)

    # Validate SECRET_KEY
    algorithm = _validated_algorithm(configs)
    return jwt.encode(to_encode, _validated_secret(configs), algorithm=algorithm)

archipy.helpers.utils.base_utils.BaseUtils.create_access_token classmethod

create_access_token(
    user_uuid: UUID,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str

Creates an access token for a user.

Parameters:

Name Type Description Default
user_uuid UUID

The user's UUID to include in the token.

required
additional_claims Mapping[str, object] | None

Optional additional claims to include in the token.

None
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

The encoded access token.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def create_access_token(
    cls,
    user_uuid: UUID,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str:
    """Creates an access token for a user.

    Args:
        user_uuid (UUID): The user's UUID to include in the token.
        additional_claims (Mapping[str, object] | None): Optional additional claims to include in the token.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: The encoded access token.
    """
    configs = auth_config or BaseConfig.global_config().AUTH

    return cls.create_token(
        data={
            "sub": str(user_uuid),
            "type": "access",
            "token_version": configs.TOKEN_VERSION,
        },
        expires_in=configs.ACCESS_TOKEN_EXPIRES_IN,
        additional_claims=additional_claims,
        auth_config=configs,
    )

archipy.helpers.utils.base_utils.BaseUtils.create_refresh_token classmethod

create_refresh_token(
    user_uuid: UUID,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str

Creates a refresh token for a user.

Parameters:

Name Type Description Default
user_uuid UUID

The user's UUID to include in the token.

required
additional_claims Mapping[str, object] | None

Optional additional claims to include in the token.

None
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

The encoded refresh token.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def create_refresh_token(
    cls,
    user_uuid: UUID,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str:
    """Creates a refresh token for a user.

    Args:
        user_uuid (UUID): The user's UUID to include in the token.
        additional_claims (Mapping[str, object] | None): Optional additional claims to include in the token.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: The encoded refresh token.
    """
    configs = auth_config or BaseConfig.global_config().AUTH

    return cls.create_token(
        data={
            "sub": str(user_uuid),
            "type": "refresh",
            "token_version": configs.TOKEN_VERSION,
        },
        expires_in=configs.REFRESH_TOKEN_EXPIRES_IN,
        additional_claims=additional_claims,
        auth_config=configs,
    )

archipy.helpers.utils.base_utils.BaseUtils.decode_token classmethod

decode_token(
    token: str,
    verify_type: str | None = None,
    auth_config: AuthConfig | None = None,
) -> dict[str, object]

Decodes and verifies a JWT token with enhanced security checks.

Parameters:

Name Type Description Default
token str

The JWT token to decode.

required
verify_type str | None

Optional token type to verify (e.g., "access" or "refresh").

None
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
dict[str, object]

dict[str, object]: The decoded token payload.

Raises:

Type Description
TokenExpiredError

If the token has expired.

InvalidTokenError

If the token is invalid (e.g., invalid signature, audience, issuer, or type).

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def decode_token(
    cls,
    token: str,
    verify_type: str | None = None,
    auth_config: AuthConfig | None = None,
) -> dict[str, object]:
    """Decodes and verifies a JWT token with enhanced security checks.

    Args:
        token (str): The JWT token to decode.
        verify_type (str | None): Optional token type to verify (e.g., "access" or "refresh").
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        dict[str, object]: The decoded token payload.

    Raises:
        TokenExpiredError: If the token has expired.
        InvalidTokenError: If the token is invalid (e.g., invalid signature, audience, issuer, or type).
    """
    import jwt
    from jwt.exceptions import ExpiredSignatureError, InvalidTokenError as JWTInvalidTokenError

    configs = auth_config or BaseConfig.global_config().AUTH
    required_claims = ["exp", "iat", "nbf", "aud", "iss", "sub", "type", "token_version"]
    if configs.ENABLE_JTI_CLAIM:
        required_claims.append("jti")

    try:
        # Validate SECRET_KEY
        algorithm = _validated_algorithm(configs)
        payload = jwt.decode(
            token,
            _validated_secret(configs),
            algorithms=[algorithm],
            options={
                "verify_signature": True,
                "verify_exp": True,
                "verify_nbf": True,
                "verify_iat": True,
                "verify_aud": True,
                "verify_iss": True,
                "require": required_claims,
            },
            audience=configs.JWT_AUDIENCE,
            issuer=configs.JWT_ISSUER,
            leeway=configs.JWT_LEEWAY,
        )

        # Verify token type
        if verify_type is not None and payload.get("type") != verify_type:
            raise InvalidTokenError

        # Verify token version
        if payload.get("token_version") != configs.TOKEN_VERSION:
            raise InvalidTokenError

        # Ensure the return type is dict[str, object] as declared
        return dict(payload)

    except ExpiredSignatureError as exception:
        raise TokenExpiredError from exception
    except JWTInvalidTokenError as exception:
        # Signature, audience, issuer, nbf/iat and malformed-token errors all derive from this class
        raise InvalidTokenError from exception

archipy.helpers.utils.base_utils.BaseUtils.verify_access_token classmethod

verify_access_token(
    token: str, auth_config: AuthConfig | None = None
) -> dict[str, object]

Verifies an access token.

Parameters:

Name Type Description Default
token str

The access token to verify.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
dict[str, object]

dict[str, object]: The decoded access token payload.

Raises:

Type Description
InvalidTokenError

If the token is invalid or not an access token.

TokenExpiredError

If the token has expired.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def verify_access_token(cls, token: str, auth_config: AuthConfig | None = None) -> dict[str, object]:
    """Verifies an access token.

    Args:
        token (str): The access token to verify.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        dict[str, object]: The decoded access token payload.

    Raises:
        InvalidTokenError: If the token is invalid or not an access token.
        TokenExpiredError: If the token has expired.
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    return cls.decode_token(token, verify_type="access", auth_config=configs)

archipy.helpers.utils.base_utils.BaseUtils.verify_refresh_token classmethod

verify_refresh_token(
    token: str, auth_config: AuthConfig | None = None
) -> dict[str, object]

Verifies a refresh token.

Parameters:

Name Type Description Default
token str

The refresh token to verify.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
dict[str, object]

dict[str, object]: The decoded refresh token payload.

Raises:

Type Description
InvalidTokenError

If the token is invalid or not a refresh token.

TokenExpiredError

If the token has expired.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def verify_refresh_token(cls, token: str, auth_config: AuthConfig | None = None) -> dict[str, object]:
    """Verifies a refresh token.

    Args:
        token (str): The refresh token to verify.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        dict[str, object]: The decoded refresh token payload.

    Raises:
        InvalidTokenError: If the token is invalid or not a refresh token.
        TokenExpiredError: If the token has expired.
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    return cls.decode_token(token, verify_type="refresh", auth_config=configs)

archipy.helpers.utils.base_utils.BaseUtils.extract_user_uuid staticmethod

extract_user_uuid(payload: Mapping[str, object]) -> UUID

Extracts the user UUID from the token payload.

Parameters:

Name Type Description Default
payload Mapping[str, object]

The decoded token payload.

required

Returns:

Name Type Description
UUID UUID

The user's UUID.

Raises:

Type Description
InvalidTokenError

If the user identifier is invalid or missing.

Source code in archipy/helpers/utils/jwt_utils.py
@staticmethod
def extract_user_uuid(payload: Mapping[str, object]) -> UUID:
    """Extracts the user UUID from the token payload.

    Args:
        payload (Mapping[str, object]): The decoded token payload.

    Returns:
        UUID: The user's UUID.

    Raises:
        InvalidTokenError: If the user identifier is invalid or missing.
    """
    try:
        subject = payload["sub"]
    except KeyError as exception:
        raise InvalidTokenError from exception
    if not isinstance(subject, str):
        raise InvalidTokenError
    try:
        return UUID(subject)
    except ValueError as exception:
        raise InvalidTokenError from exception

archipy.helpers.utils.base_utils.BaseUtils.get_token_expiry classmethod

get_token_expiry(
    token: str, auth_config: AuthConfig | None = None
) -> int

Gets the token expiry timestamp.

Parameters:

Name Type Description Default
token str

The JWT token.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
int int

The token expiry timestamp in seconds.

Raises:

Type Description
InvalidTokenError

If the token is invalid.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def get_token_expiry(cls, token: str, auth_config: AuthConfig | None = None) -> int:
    """Gets the token expiry timestamp.

    Args:
        token (str): The JWT token.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        int: The token expiry timestamp in seconds.

    Raises:
        InvalidTokenError: If the token is invalid.
    """
    payload = cls.decode_token(token, auth_config=auth_config)
    # PyJWT has already checked that ``exp`` converts to an integer
    expiry = payload["exp"]
    if not isinstance(expiry, int | float | str):
        raise InvalidTokenError
    return int(expiry)

archipy.helpers.utils.base_utils.BaseUtils.hash_password staticmethod

hash_password(
    password: str, auth_config: AuthConfig | None = None
) -> str

Hashes a password using PBKDF2 with SHA256.

Parameters:

Name Type Description Default
password str

The password to hash.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

A self-describing string in the format pbkdf2_sha256$<iterations>$<salt>$<hash> where salt and hash are base64-encoded. The iteration count is stored with the hash, so changing HASH_ITERATIONS or SALT_LENGTH later does not invalidate existing hashes.

Source code in archipy/helpers/utils/password_utils.py
@staticmethod
def hash_password(password: str, auth_config: AuthConfig | None = None) -> str:
    """Hashes a password using PBKDF2 with SHA256.

    Args:
        password (str): The password to hash.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: A self-describing string in the format ``pbkdf2_sha256$<iterations>$<salt>$<hash>`` where salt and
            hash are base64-encoded. The iteration count is stored with the hash, so changing
            ``HASH_ITERATIONS`` or ``SALT_LENGTH`` later does not invalidate existing hashes.
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    salt = os.urandom(configs.SALT_LENGTH)
    iterations = configs.HASH_ITERATIONS
    pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)

    return _HASH_SEPARATOR.join(
        (_HASH_SCHEME, str(iterations), b64encode(salt).decode("utf-8"), b64encode(pw_hash).decode("utf-8")),
    )

archipy.helpers.utils.base_utils.BaseUtils.verify_password staticmethod

verify_password(
    password: str,
    stored_password: str,
    auth_config: AuthConfig | None = None,
) -> bool

Verifies a password against a stored hash.

Both the current pbkdf2_sha256$... format and the legacy format (base64 of salt + hash, written by earlier versions) are accepted. Legacy hashes carry no parameters, so they are tried with the configured HASH_ITERATIONS and, if that differs, with the former default of 100000.

Parameters:

Name Type Description Default
password str

The password to verify.

required
stored_password str

The stored password hash to compare against.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
bool bool

True if the password matches the stored hash, False otherwise.

Source code in archipy/helpers/utils/password_utils.py
@staticmethod
def verify_password(password: str, stored_password: str, auth_config: AuthConfig | None = None) -> bool:
    """Verifies a password against a stored hash.

    Both the current ``pbkdf2_sha256$...`` format and the legacy format (base64 of ``salt + hash``, written by
    earlier versions) are accepted. Legacy hashes carry no parameters, so they are tried with the configured
    ``HASH_ITERATIONS`` and, if that differs, with the former default of 100000.

    Args:
        password (str): The password to verify.
        stored_password (str): The stored password hash to compare against.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        bool: True if the password matches the stored hash, False otherwise.
    """
    try:
        configs = auth_config or BaseConfig.global_config().AUTH

        if stored_password.startswith(_HASH_SCHEME + _HASH_SEPARATOR):
            _, iterations, salt_b64, hash_b64 = stored_password.split(_HASH_SEPARATOR)
            salt = b64decode(salt_b64.encode("utf-8"), validate=True)
            stored_hash = b64decode(hash_b64.encode("utf-8"), validate=True)
            return PasswordUtils._matches(password, salt, stored_hash, int(iterations))

        # Legacy format: base64(salt + hash)
        decoded = b64decode(stored_password.encode("utf-8"))
        salt = decoded[: configs.SALT_LENGTH]
        stored_hash = decoded[configs.SALT_LENGTH :]
        candidates = dict.fromkeys((configs.HASH_ITERATIONS, _LEGACY_DEFAULT_ITERATIONS))
        return any(PasswordUtils._matches(password, salt, stored_hash, count) for count in candidates)
    except ValueError, TypeError, IndexError:
        # Catch specific exceptions that could occur during decoding or comparison
        return False

archipy.helpers.utils.base_utils.BaseUtils.validate_password staticmethod

validate_password(
    password: str, auth_config: AuthConfig | None = None
) -> None

Validates a password against the password policy.

Parameters:

Name Type Description Default
password str

The password to validate.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Raises:

Type Description
InvalidPasswordError

If the password does not meet the policy requirements.

Source code in archipy/helpers/utils/password_utils.py
@staticmethod
def validate_password(
    password: str,
    auth_config: AuthConfig | None = None,
) -> None:
    """Validates a password against the password policy.

    Args:
        password (str): The password to validate.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Raises:
        InvalidPasswordError: If the password does not meet the policy requirements.
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    errors = []

    if len(password) < configs.MIN_LENGTH:
        errors.append(f"Password must be at least {configs.MIN_LENGTH} characters long.")

    if configs.REQUIRE_DIGIT and not any(char.isdigit() for char in password):
        errors.append("Password must contain at least one digit.")

    if configs.REQUIRE_LOWERCASE and not any(char.islower() for char in password):
        errors.append("Password must contain at least one lowercase letter.")

    if configs.REQUIRE_UPPERCASE and not any(char.isupper() for char in password):
        errors.append("Password must contain at least one uppercase letter.")

    if configs.REQUIRE_SPECIAL and not any(char in configs.SPECIAL_CHARACTERS for char in password):
        errors.append(f"Password must contain at least one special character: {configs.SPECIAL_CHARACTERS}")

    if errors:
        raise InvalidPasswordError(requirements=errors)

archipy.helpers.utils.base_utils.BaseUtils.generate_password staticmethod

generate_password(
    auth_config: AuthConfig | None = None,
) -> str

Generates a random password that meets the policy requirements.

Parameters:

Name Type Description Default
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

A randomly generated password that meets the policy requirements.

Source code in archipy/helpers/utils/password_utils.py
@staticmethod
def generate_password(auth_config: AuthConfig | None = None) -> str:
    """Generates a random password that meets the policy requirements.

    Args:
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: A randomly generated password that meets the policy requirements.
    """
    configs = auth_config or BaseConfig.global_config().AUTH

    lowercase_chars = string.ascii_lowercase
    uppercase_chars = string.ascii_uppercase
    digit_chars = string.digits
    special_chars = "".join(configs.SPECIAL_CHARACTERS)

    # Initialize with required characters
    password_chars = []
    if configs.REQUIRE_LOWERCASE:
        password_chars.append(secrets.choice(lowercase_chars))
    if configs.REQUIRE_UPPERCASE:
        password_chars.append(secrets.choice(uppercase_chars))
    if configs.REQUIRE_DIGIT:
        password_chars.append(secrets.choice(digit_chars))
    if configs.REQUIRE_SPECIAL:
        password_chars.append(secrets.choice(special_chars))

    # Calculate remaining length
    remaining_length = max(0, configs.MIN_LENGTH - len(password_chars))

    # Add random characters to meet minimum length
    all_chars = lowercase_chars + uppercase_chars + digit_chars + special_chars
    password_chars.extend(secrets.choice(all_chars) for _ in range(remaining_length))

    # Shuffle the password characters
    shuffled = list(password_chars)
    secrets.SystemRandom().shuffle(shuffled)

    return "".join(shuffled)

archipy.helpers.utils.base_utils.BaseUtils.validate_password_history classmethod

validate_password_history(
    new_password: str,
    password_history: list[str],
    auth_config: AuthConfig | None = None,
    lang: LanguageType | None = None,
) -> None

Validates a new password against the password history.

Parameters:

Name Type Description Default
new_password str

The new password to validate.

required
password_history list[str]

A list of previous password hashes.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None
lang LanguageType

The language to use for error messages. Defaults to Persian.

None

Raises:

Type Description
InvalidPasswordError

If the new password has been used recently or does not meet the policy requirements.

Source code in archipy/helpers/utils/password_utils.py
@classmethod
def validate_password_history(
    cls,
    new_password: str,
    password_history: list[str],
    auth_config: AuthConfig | None = None,
    lang: LanguageType | None = None,
) -> None:
    """Validates a new password against the password history.

    Args:
        new_password (str): The new password to validate.
        password_history (list[str]): A list of previous password hashes.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.
        lang (LanguageType): The language to use for error messages. Defaults to Persian.

    Raises:
        InvalidPasswordError: If the new password has been used recently or does not meet the policy requirements.
    """
    configs = auth_config or BaseConfig.global_config().AUTH

    # First validate against password policy
    cls.validate_password(new_password, configs)

    # Check password history (a size of 0 disables the check)
    history_size = max(configs.PASSWORD_HISTORY_SIZE, 0)
    if history_size and any(
        cls.verify_password(new_password, old_password, configs)
        for old_password in password_history[-history_size:]
    ):
        raise InvalidPasswordError(requirements=["Password has been used recently"], lang=lang)

archipy.helpers.utils.base_utils.BaseUtils.convert_to_jalali staticmethod

convert_to_jalali(target_date: date) -> jdatetime.date

Converts a Gregorian date to a Jalali (Persian) date.

Parameters:

Name Type Description Default
target_date date

The Gregorian date to convert.

required

Returns:

Type Description
date

jdatetime.date: The corresponding Jalali date.

Source code in archipy/helpers/utils/datetime_utils.py
@staticmethod
def convert_to_jalali(target_date: date) -> jdatetime.date:
    """Converts a Gregorian date to a Jalali (Persian) date.

    Args:
        target_date (date): The Gregorian date to convert.

    Returns:
        jdatetime.date: The corresponding Jalali date.
    """
    return jdatetime.date.fromgregorian(date=target_date)

archipy.helpers.utils.base_utils.BaseUtils.is_holiday_in_iran classmethod

is_holiday_in_iran(target_date: date) -> bool

Determines if the target date is a holiday in Iran.

This method leverages caching and an external API to check if the given date is a holiday.

Parameters:

Name Type Description Default
target_date date

The date to check for holiday status.

required

Returns:

Name Type Description
bool bool

True if the date is a holiday, False otherwise.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def is_holiday_in_iran(cls, target_date: date) -> bool:
    """Determines if the target date is a holiday in Iran.

    This method leverages caching and an external API to check if the given date is a holiday.

    Args:
        target_date (date): The date to check for holiday status.

    Returns:
        bool: True if the date is a holiday, False otherwise.
    """
    # Convert to Jalali date first
    jalali_date = cls.convert_to_jalali(target_date)
    date_str = target_date.strftime("%Y-%m-%d")
    current_time = cls.get_datetime_utc_now()

    # Check cache first
    is_cached, is_holiday = cls._check_cache(date_str, current_time)
    if is_cached:
        return is_holiday

    # Fetch holiday status and cache it
    return cls._fetch_and_cache_holiday_status(jalali_date, date_str, current_time)

archipy.helpers.utils.base_utils.BaseUtils.ensure_timezone_aware classmethod

ensure_timezone_aware(dt: datetime) -> datetime

Ensures a datetime object is timezone-aware, converting it to UTC if necessary.

Parameters:

Name Type Description Default
dt datetime

The datetime object to make timezone-aware.

required

Returns:

Name Type Description
datetime datetime

The timezone-aware datetime object.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def ensure_timezone_aware(cls, dt: datetime) -> datetime:
    """Ensures a datetime object is timezone-aware, converting it to UTC if necessary.

    Args:
        dt (datetime): The datetime object to make timezone-aware.

    Returns:
        datetime: The timezone-aware datetime object.
    """
    if dt.tzinfo is None:
        return dt.replace(tzinfo=UTC)
    return dt

archipy.helpers.utils.base_utils.BaseUtils.daterange classmethod

daterange(
    start_date: datetime, end_date: datetime
) -> Generator[date]

Generates a range of dates from start_date to end_date, exclusive of end_date.

Parameters:

Name Type Description Default
start_date datetime

The start date of the range.

required
end_date datetime

The end date of the range.

required

Yields:

Name Type Description
date Generator[date]

Each date in the range.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def daterange(cls, start_date: datetime, end_date: datetime) -> Generator[date]:
    """Generates a range of dates from start_date to end_date, exclusive of end_date.

    Args:
        start_date (datetime): The start date of the range.
        end_date (datetime): The end date of the range.

    Yields:
        date: Each date in the range.
    """
    # Compare calendar dates (naive values are read as UTC) so a range that crosses midnight is not skipped.
    first_day = cls.ensure_timezone_aware(start_date).date()
    last_day = cls.ensure_timezone_aware(end_date).date()
    for n in range((last_day - first_day).days):
        yield first_day + timedelta(n)

archipy.helpers.utils.base_utils.BaseUtils.get_string_datetime_from_datetime classmethod

get_string_datetime_from_datetime(
    dt: datetime, format_: str | None = None
) -> str

Converts a datetime object to a formatted string. Default format is ISO 8601.

Parameters:

Name Type Description Default
dt datetime

The datetime object to format.

required
format_ str | None

The format string. If None, uses ISO 8601.

None

Returns:

Name Type Description
str str

The formatted datetime string.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_string_datetime_from_datetime(cls, dt: datetime, format_: str | None = None) -> str:
    """Converts a datetime object to a formatted string. Default format is ISO 8601.

    Args:
        dt (datetime): The datetime object to format.
        format_ (str | None): The format string. If None, uses ISO 8601.

    Returns:
        str: The formatted datetime string.
    """
    format_ = format_ or "%Y-%m-%dT%H:%M:%S.%f"
    return dt.strftime(format_)

archipy.helpers.utils.base_utils.BaseUtils.standardize_string_datetime classmethod

standardize_string_datetime(date_string: str) -> str

Standardizes a datetime string to the default format.

Parameters:

Name Type Description Default
date_string str

The datetime string to standardize.

required

Returns:

Name Type Description
str str

The standardized datetime string.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def standardize_string_datetime(cls, date_string: str) -> str:
    """Standardizes a datetime string to the default format.

    Args:
        date_string (str): The datetime string to standardize.

    Returns:
        str: The standardized datetime string.
    """
    datetime_ = cls.get_datetime_from_string_datetime(date_string)
    return cls.get_string_datetime_from_datetime(datetime_)

archipy.helpers.utils.base_utils.BaseUtils.get_datetime_from_string_datetime classmethod

get_datetime_from_string_datetime(
    date_string: str, format_: str | None = None
) -> datetime

Parses a string to a datetime object using the given format, or ISO 8601 by default.

Parameters:

Name Type Description Default
date_string str

The datetime string to parse.

required
format_ str | None

The format string. If None, uses ISO 8601.

None

Returns:

Name Type Description
datetime datetime

The parsed timezone-aware datetime. Strings without an offset are read as UTC.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_from_string_datetime(cls, date_string: str, format_: str | None = None) -> datetime:
    """Parses a string to a datetime object using the given format, or ISO 8601 by default.

    Args:
        date_string (str): The datetime string to parse.
        format_ (str | None): The format string. If None, uses ISO 8601.

    Returns:
        datetime: The parsed timezone-aware datetime. Strings without an offset are read as UTC.
    """
    dt = datetime.fromisoformat(date_string) if format_ is None else datetime.strptime(date_string, format_)

    # Values without an offset are read as UTC; an offset parsed from the string (e.g. ``%z``) is kept
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=UTC)

    return dt

archipy.helpers.utils.base_utils.BaseUtils.get_string_datetime_now classmethod

get_string_datetime_now() -> str

Gets the current datetime as a formatted string. Default format is ISO 8601.

Returns:

Name Type Description
str str

The formatted datetime string.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_string_datetime_now(cls) -> str:
    """Gets the current datetime as a formatted string. Default format is ISO 8601.

    Returns:
        str: The formatted datetime string.
    """
    return cls.get_string_datetime_from_datetime(cls.get_datetime_now())

archipy.helpers.utils.base_utils.BaseUtils.get_datetime_now classmethod

get_datetime_now() -> datetime

Gets the current local datetime.

Returns:

Name Type Description
datetime datetime

The current local datetime.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_now(cls) -> datetime:
    """Gets the current local datetime.

    Returns:
        datetime: The current local datetime.
    """
    return datetime.now()

archipy.helpers.utils.base_utils.BaseUtils.get_datetime_utc_now classmethod

get_datetime_utc_now() -> datetime

Gets the current UTC datetime.

Returns:

Name Type Description
datetime datetime

The current UTC datetime.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_utc_now(cls) -> datetime:
    """Gets the current UTC datetime.

    Returns:
        datetime: The current UTC datetime.
    """
    return datetime.now(UTC)

archipy.helpers.utils.base_utils.BaseUtils.get_epoch_time_now classmethod

get_epoch_time_now() -> int

Gets the current time in seconds since the epoch.

Returns:

Name Type Description
int int

The current epoch time.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_epoch_time_now(cls) -> int:
    """Gets the current time in seconds since the epoch.

    Returns:
        int: The current epoch time.
    """
    return int(time.time())

archipy.helpers.utils.base_utils.BaseUtils.get_datetime_before_given_datetime_or_now classmethod

get_datetime_before_given_datetime_or_now(
    weeks: int = 0,
    days: int = 0,
    hours: int = 0,
    minutes: int = 0,
    seconds: int = 0,
    datetime_given: datetime | None = None,
) -> datetime

Subtracts time from a given datetime or the current datetime if not specified.

Parameters:

Name Type Description Default
weeks int

The number of weeks to subtract.

0
days int

The number of days to subtract.

0
hours int

The number of hours to subtract.

0
minutes int

The number of minutes to subtract.

0
seconds int

The number of seconds to subtract.

0
datetime_given datetime | None

The datetime to subtract from. If None, uses the current datetime.

None

Returns:

Name Type Description
datetime datetime

The resulting datetime after subtraction.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_before_given_datetime_or_now(
    cls,
    weeks: int = 0,
    days: int = 0,
    hours: int = 0,
    minutes: int = 0,
    seconds: int = 0,
    datetime_given: datetime | None = None,
) -> datetime:
    """Subtracts time from a given datetime or the current datetime if not specified.

    Args:
        weeks (int): The number of weeks to subtract.
        days (int): The number of days to subtract.
        hours (int): The number of hours to subtract.
        minutes (int): The number of minutes to subtract.
        seconds (int): The number of seconds to subtract.
        datetime_given (datetime | None): The datetime to subtract from. If None, uses the current datetime.

    Returns:
        datetime: The resulting datetime after subtraction.
    """
    datetime_given = datetime_given or cls.get_datetime_now()
    return datetime_given - timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)

archipy.helpers.utils.base_utils.BaseUtils.get_datetime_after_given_datetime_or_now classmethod

get_datetime_after_given_datetime_or_now(
    weeks: int = 0,
    days: int = 0,
    hours: int = 0,
    minutes: int = 0,
    seconds: int = 0,
    datetime_given: datetime | None = None,
) -> datetime

Adds time to a given datetime or the current datetime if not specified.

Parameters:

Name Type Description Default
weeks int

The number of weeks to add.

0
days int

The number of days to add.

0
hours int

The number of hours to add.

0
minutes int

The number of minutes to add.

0
seconds int

The number of seconds to add.

0
datetime_given datetime | None

The datetime to add to. If None, uses the current datetime.

None

Returns:

Name Type Description
datetime datetime

The resulting datetime after addition.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_after_given_datetime_or_now(
    cls,
    weeks: int = 0,
    days: int = 0,
    hours: int = 0,
    minutes: int = 0,
    seconds: int = 0,
    datetime_given: datetime | None = None,
) -> datetime:
    """Adds time to a given datetime or the current datetime if not specified.

    Args:
        weeks (int): The number of weeks to add.
        days (int): The number of days to add.
        hours (int): The number of hours to add.
        minutes (int): The number of minutes to add.
        seconds (int): The number of seconds to add.
        datetime_given (datetime | None): The datetime to add to. If None, uses the current datetime.

    Returns:
        datetime: The resulting datetime after addition.
    """
    datetime_given = datetime_given or cls.get_datetime_now()
    return datetime_given + timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)

archipy.helpers.utils.base_utils.BaseUtils.capture_exception staticmethod

capture_exception(exception: BaseException) -> None

Deprecated: use archipy.helpers.errors.capture_exception.

Parameters:

Name Type Description Default
exception BaseException

The exception to capture and report.

required
Source code in archipy/helpers/utils/error_utils.py
@staticmethod
def capture_exception(exception: BaseException) -> None:
    """Deprecated: use ``archipy.helpers.errors.capture_exception``.

    Args:
        exception (BaseException): The exception to capture and report.
    """
    warn_deprecated_name("BaseUtils.capture_exception", "archipy.helpers.errors.capture_exception")
    capture_exception(exception)

archipy.helpers.utils.base_utils.BaseUtils.async_handle_fastapi_exception async staticmethod

async_handle_fastapi_exception(
    _request: Request, exception: BaseError
) -> JSONResponse

Deprecated: use archipy.helpers.errors.fastapi.handle_fastapi_exception.

Parameters:

Name Type Description Default
_request Request

The incoming FastAPI request.

required
exception BaseError

The exception to handle.

required

Returns:

Name Type Description
JSONResponse JSONResponse

A JSON response containing the exception details.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
async def async_handle_fastapi_exception(_request: Request, exception: BaseError) -> JSONResponse:
    """Deprecated: use ``archipy.helpers.errors.fastapi.handle_fastapi_exception``.

    Args:
        _request (Request): The incoming FastAPI request.
        exception (BaseError): The exception to handle.

    Returns:
        JSONResponse: A JSON response containing the exception details.
    """
    from archipy.helpers.errors.fastapi import handle_fastapi_exception

    warn_deprecated_name(
        "BaseUtils.async_handle_fastapi_exception",
        "archipy.helpers.errors.fastapi.handle_fastapi_exception",
    )
    return await handle_fastapi_exception(_request, exception)

archipy.helpers.utils.base_utils.BaseUtils.get_fastapi_exception_responses staticmethod

get_fastapi_exception_responses(
    exceptions: list[type[BaseError]],
) -> dict[int, dict[str, object]]

Deprecated: use archipy.helpers.errors.fastapi.get_fastapi_exception_responses.

Parameters:

Name Type Description Default
exceptions list[type[BaseError]]

A list of exception types to generate responses for.

required

Returns:

Type Description
dict[int, dict[str, object]]

dict[int, dict[str, object]]: A dictionary mapping HTTP status codes to their corresponding response schemas.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
def get_fastapi_exception_responses(exceptions: list[type[BaseError]]) -> dict[int, dict[str, object]]:
    """Deprecated: use ``archipy.helpers.errors.fastapi.get_fastapi_exception_responses``.

    Args:
        exceptions (list[type[BaseError]]): A list of exception types to generate responses for.

    Returns:
        dict[int, dict[str, object]]: A dictionary mapping HTTP status codes to their corresponding response
            schemas.
    """
    from archipy.helpers.errors.fastapi import get_fastapi_exception_responses

    warn_deprecated_name(
        "BaseUtils.get_fastapi_exception_responses",
        "archipy.helpers.errors.fastapi.get_fastapi_exception_responses",
    )
    return get_fastapi_exception_responses(exceptions)

options: show_root_toc_entry: false heading_level: 3

App Utils

Application-level utilities for runtime environment inspection and process management.

Application bootstrap and lifecycle utilities.

archipy.helpers.utils.app_utils.CreateGrpcServerType module-attribute

CreateGrpcServerType = Callable[..., GrpcAioServer]

archipy.helpers.utils.app_utils.logger module-attribute

logger = logging.getLogger(__name__)

archipy.helpers.utils.app_utils.create_grpc_server module-attribute

create_grpc_server: CreateGrpcServerType = grpc_aio.server

archipy.helpers.utils.app_utils.GRPC_APP module-attribute

GRPC_APP = True

archipy.helpers.utils.app_utils.FASTAPI_APP module-attribute

FASTAPI_APP = True

archipy.helpers.utils.app_utils.FastAPIUtils

Utility class for FastAPI configuration and setup.

Source code in archipy/helpers/utils/app_utils.py
class FastAPIUtils:
    """Utility class for FastAPI configuration and setup."""

    @staticmethod
    def custom_generate_unique_id(route: APIRoute) -> str:
        """Generates a unique ID for API routes.

        Args:
            route (APIRoute): The route for which to generate a unique ID.

        Returns:
            str: A unique ID for the route.
        """
        tags = getattr(route, "tags", [])
        return f"{tags[0]}-{route.name}" if tags else route.name

    @staticmethod
    def setup_cors(app: FastAPI, config: BaseConfig) -> None:
        """Configures CORS middleware.

        Args:
            app (FastAPI): The FastAPI application instance.
            config (BaseConfig): The configuration object containing CORS settings.
        """
        origins = [str(origin).strip("/") for origin in config.FASTAPI.CORS_MIDDLEWARE_ALLOW_ORIGINS]
        # Use app.add_middleware with CORSMiddleware directly
        # CORSMiddleware is compatible with FastAPI's middleware system at runtime
        app.add_middleware(
            CORSMiddleware,
            allow_origins=origins,
            allow_credentials=config.FASTAPI.CORS_MIDDLEWARE_ALLOW_CREDENTIALS,
            allow_methods=config.FASTAPI.CORS_MIDDLEWARE_ALLOW_METHODS,
            allow_headers=config.FASTAPI.CORS_MIDDLEWARE_ALLOW_HEADERS,
            allow_origin_regex=config.FASTAPI.CORS_MIDDLEWARE_ALLOW_ORIGIN_REGEX,
            expose_headers=config.FASTAPI.CORS_MIDDLEWARE_EXPOSE_HEADERS,
            max_age=config.FASTAPI.CORS_MIDDLEWARE_MAX_AGE,
        )

    @staticmethod
    def setup_gzip(app: FastAPI, config: BaseConfig) -> None:
        """Configures GZip response compression middleware if enabled.

        Args:
            app (FastAPI): The FastAPI application instance.
            config (BaseConfig): The configuration object containing GZip middleware settings.
        """
        if not config.FASTAPI.GZIP_MIDDLEWARE_IS_ENABLED:
            return

        app.add_middleware(
            GZipMiddleware,
            minimum_size=config.FASTAPI.GZIP_MIDDLEWARE_MINIMUM_SIZE,
            compresslevel=config.FASTAPI.GZIP_MIDDLEWARE_COMPRESSLEVEL,
        )

    @staticmethod
    def setup_trusted_host(app: FastAPI, config: BaseConfig) -> None:
        """Configures TrustedHost middleware if enabled.

        Args:
            app (FastAPI): The FastAPI application instance.
            config (BaseConfig): The configuration object containing TrustedHost middleware settings.
        """
        if not config.FASTAPI.TRUSTED_HOST_MIDDLEWARE_IS_ENABLED:
            return

        allowed_hosts = config.FASTAPI.TRUSTED_HOST_MIDDLEWARE_ALLOWED_HOSTS
        if not allowed_hosts:
            logger.warning(
                "TrustedHost middleware enabled but TRUSTED_HOST_MIDDLEWARE_ALLOWED_HOSTS is empty; skipping",
            )
            return

        app.add_middleware(
            TrustedHostMiddleware,
            allowed_hosts=allowed_hosts,
            www_redirect=config.FASTAPI.TRUSTED_HOST_MIDDLEWARE_WWW_REDIRECT,
        )

    @staticmethod
    def setup_https_redirect(app: FastAPI, config: BaseConfig) -> None:
        """Configures HTTPS redirect middleware if enabled.

        Args:
            app (FastAPI): The FastAPI application instance.
            config (BaseConfig): The configuration object containing HTTPS redirect middleware settings.
        """
        if not config.FASTAPI.HTTPS_REDIRECT_MIDDLEWARE_IS_ENABLED:
            return

        app.add_middleware(HTTPSRedirectMiddleware)

    @staticmethod
    def _fastapi_otel_excluded(patterns: str | None) -> Callable[[Scope], bool] | None:
        """Build a native-telemetry ``exclude`` callable from comma-separated URL patterns.

        Args:
            patterns: Comma-separated regular expressions searched against the request path.

        Returns:
            A callable receiving the ASGI scope, or ``None`` when no patterns are configured.
        """
        compiled = [re.compile(item.strip()) for item in (patterns or "").split(",") if item.strip()]
        if not compiled:
            return None

        def exclude(scope: Scope) -> bool:
            path = scope.get("path", "")
            return any(pattern.search(path) for pattern in compiled)

        return exclude

    @staticmethod
    def _resolve_otel_providers(
        config: BaseConfig,
    ) -> tuple[OwnedTracerProvider | None, OwnedMeterProvider | None, OwnedLoggerProvider | None]:
        """Return the tracer, meter and logger providers FastAPI should use.

        A provider is ``None`` when its signal is disabled or ``OtelUtils`` has none; an enabled
        signal without a provider logs a warning.

        Args:
            config: Application configuration containing OTel settings.

        Returns:
            Tuple of ``(tracer_provider, meter_provider, logger_provider)``.
        """
        from archipy.helpers.utils.otel_utils import OtelUtils

        tracer_provider = OtelUtils.tracer_provider() if config.OTEL.TRACES_ENABLED else None
        meter_provider = OtelUtils.meter_provider() if config.OTEL.METRICS_ENABLED else None
        logger_provider = OtelUtils.logger_provider() if config.OTEL.FASTAPI_LOGS_ENABLED else None
        if config.OTEL.TRACES_ENABLED and tracer_provider is None:
            logger.warning("OTEL traces enabled but no tracer provider is available; skipping FastAPI tracing")
        if config.OTEL.METRICS_ENABLED and meter_provider is None:
            logger.warning("OTEL metrics enabled but no meter provider is available; skipping FastAPI metrics")
        if config.OTEL.FASTAPI_LOGS_ENABLED and logger_provider is None:
            logger.warning("OTEL FastAPI logs enabled but no logger provider is available; skipping FastAPI logs")
        return tracer_provider, meter_provider, logger_provider

    @staticmethod
    def build_otel_telemetry_config(config: BaseConfig) -> TelemetryConfig:
        """Build the ``telemetry`` argument for ``FastAPI(...)`` from ArchiPy OTel settings.

        FastAPI instruments requests natively, so the config must be passed to the constructor.
        Providers come from ``OtelUtils`` and are never replaced by globals. Signals without a
        real provider are disabled. ``auto_configure`` is off because ArchiPy owns the exporters.
        FastAPI log records (which carry stack traces) are opt-in via ``OTEL.FASTAPI_LOGS_ENABLED``.

        Args:
            config: Application configuration containing OTel settings.

        Returns:
            Telemetry settings for ``FastAPI(telemetry=...)``.
        """
        from archipy.helpers.utils.otel_utils import OTEL_FASTAPI_INSTALL_HINT, OtelUtils

        disabled: TelemetryConfig = {
            "tracing": False,
            "metrics": False,
            "logs": False,
            "operation_spans": False,
            "auto_configure": False,
        }
        if not config.OTEL.IS_ENABLED:
            return disabled

        try:
            OtelUtils.init_otel_if_needed(config)
            if OtelUtils.import_failed():
                return disabled
            tracer_provider, meter_provider, logger_provider = FastAPIUtils._resolve_otel_providers(config)
        except ImportError:
            logger.warning("%s", OTEL_FASTAPI_INSTALL_HINT)
            return disabled
        # Observability is optional: a failed init must not stop the app.
        except Exception:
            logger.exception("Failed to initialize OpenTelemetry for FastAPI")
            return disabled

        telemetry: TelemetryConfig = {
            "tracing": tracer_provider is not None,
            "metrics": meter_provider is not None,
            "logs": logger_provider is not None,
            "operation_spans": tracer_provider is not None and config.OTEL.FASTAPI_OPERATION_SPANS_ENABLED,
            "auto_configure": False,
        }
        if tracer_provider is not None:
            telemetry["tracer_provider"] = tracer_provider
        if meter_provider is not None:
            telemetry["meter_provider"] = meter_provider
        if logger_provider is not None:
            telemetry["logger_provider"] = logger_provider
        exclude = FastAPIUtils._fastapi_otel_excluded(config.OTEL.FASTAPI_EXCLUDED_URLS)
        if exclude is not None:
            telemetry["exclude"] = exclude
        return telemetry

archipy.helpers.utils.app_utils.FastAPIUtils.custom_generate_unique_id staticmethod

custom_generate_unique_id(route: APIRoute) -> str

Generates a unique ID for API routes.

Parameters:

Name Type Description Default
route APIRoute

The route for which to generate a unique ID.

required

Returns:

Name Type Description
str str

A unique ID for the route.

Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def custom_generate_unique_id(route: APIRoute) -> str:
    """Generates a unique ID for API routes.

    Args:
        route (APIRoute): The route for which to generate a unique ID.

    Returns:
        str: A unique ID for the route.
    """
    tags = getattr(route, "tags", [])
    return f"{tags[0]}-{route.name}" if tags else route.name

archipy.helpers.utils.app_utils.FastAPIUtils.setup_cors staticmethod

setup_cors(app: FastAPI, config: BaseConfig) -> None

Configures CORS middleware.

Parameters:

Name Type Description Default
app FastAPI

The FastAPI application instance.

required
config BaseConfig

The configuration object containing CORS settings.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_cors(app: FastAPI, config: BaseConfig) -> None:
    """Configures CORS middleware.

    Args:
        app (FastAPI): The FastAPI application instance.
        config (BaseConfig): The configuration object containing CORS settings.
    """
    origins = [str(origin).strip("/") for origin in config.FASTAPI.CORS_MIDDLEWARE_ALLOW_ORIGINS]
    # Use app.add_middleware with CORSMiddleware directly
    # CORSMiddleware is compatible with FastAPI's middleware system at runtime
    app.add_middleware(
        CORSMiddleware,
        allow_origins=origins,
        allow_credentials=config.FASTAPI.CORS_MIDDLEWARE_ALLOW_CREDENTIALS,
        allow_methods=config.FASTAPI.CORS_MIDDLEWARE_ALLOW_METHODS,
        allow_headers=config.FASTAPI.CORS_MIDDLEWARE_ALLOW_HEADERS,
        allow_origin_regex=config.FASTAPI.CORS_MIDDLEWARE_ALLOW_ORIGIN_REGEX,
        expose_headers=config.FASTAPI.CORS_MIDDLEWARE_EXPOSE_HEADERS,
        max_age=config.FASTAPI.CORS_MIDDLEWARE_MAX_AGE,
    )

archipy.helpers.utils.app_utils.FastAPIUtils.setup_gzip staticmethod

setup_gzip(app: FastAPI, config: BaseConfig) -> None

Configures GZip response compression middleware if enabled.

Parameters:

Name Type Description Default
app FastAPI

The FastAPI application instance.

required
config BaseConfig

The configuration object containing GZip middleware settings.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_gzip(app: FastAPI, config: BaseConfig) -> None:
    """Configures GZip response compression middleware if enabled.

    Args:
        app (FastAPI): The FastAPI application instance.
        config (BaseConfig): The configuration object containing GZip middleware settings.
    """
    if not config.FASTAPI.GZIP_MIDDLEWARE_IS_ENABLED:
        return

    app.add_middleware(
        GZipMiddleware,
        minimum_size=config.FASTAPI.GZIP_MIDDLEWARE_MINIMUM_SIZE,
        compresslevel=config.FASTAPI.GZIP_MIDDLEWARE_COMPRESSLEVEL,
    )

archipy.helpers.utils.app_utils.FastAPIUtils.setup_trusted_host staticmethod

setup_trusted_host(
    app: FastAPI, config: BaseConfig
) -> None

Configures TrustedHost middleware if enabled.

Parameters:

Name Type Description Default
app FastAPI

The FastAPI application instance.

required
config BaseConfig

The configuration object containing TrustedHost middleware settings.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_trusted_host(app: FastAPI, config: BaseConfig) -> None:
    """Configures TrustedHost middleware if enabled.

    Args:
        app (FastAPI): The FastAPI application instance.
        config (BaseConfig): The configuration object containing TrustedHost middleware settings.
    """
    if not config.FASTAPI.TRUSTED_HOST_MIDDLEWARE_IS_ENABLED:
        return

    allowed_hosts = config.FASTAPI.TRUSTED_HOST_MIDDLEWARE_ALLOWED_HOSTS
    if not allowed_hosts:
        logger.warning(
            "TrustedHost middleware enabled but TRUSTED_HOST_MIDDLEWARE_ALLOWED_HOSTS is empty; skipping",
        )
        return

    app.add_middleware(
        TrustedHostMiddleware,
        allowed_hosts=allowed_hosts,
        www_redirect=config.FASTAPI.TRUSTED_HOST_MIDDLEWARE_WWW_REDIRECT,
    )

archipy.helpers.utils.app_utils.FastAPIUtils.setup_https_redirect staticmethod

setup_https_redirect(
    app: FastAPI, config: BaseConfig
) -> None

Configures HTTPS redirect middleware if enabled.

Parameters:

Name Type Description Default
app FastAPI

The FastAPI application instance.

required
config BaseConfig

The configuration object containing HTTPS redirect middleware settings.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_https_redirect(app: FastAPI, config: BaseConfig) -> None:
    """Configures HTTPS redirect middleware if enabled.

    Args:
        app (FastAPI): The FastAPI application instance.
        config (BaseConfig): The configuration object containing HTTPS redirect middleware settings.
    """
    if not config.FASTAPI.HTTPS_REDIRECT_MIDDLEWARE_IS_ENABLED:
        return

    app.add_middleware(HTTPSRedirectMiddleware)

archipy.helpers.utils.app_utils.FastAPIUtils.build_otel_telemetry_config staticmethod

build_otel_telemetry_config(
    config: BaseConfig,
) -> TelemetryConfig

Build the telemetry argument for FastAPI(...) from ArchiPy OTel settings.

FastAPI instruments requests natively, so the config must be passed to the constructor. Providers come from OtelUtils and are never replaced by globals. Signals without a real provider are disabled. auto_configure is off because ArchiPy owns the exporters. FastAPI log records (which carry stack traces) are opt-in via OTEL.FASTAPI_LOGS_ENABLED.

Parameters:

Name Type Description Default
config BaseConfig

Application configuration containing OTel settings.

required

Returns:

Type Description
TelemetryConfig

Telemetry settings for FastAPI(telemetry=...).

Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def build_otel_telemetry_config(config: BaseConfig) -> TelemetryConfig:
    """Build the ``telemetry`` argument for ``FastAPI(...)`` from ArchiPy OTel settings.

    FastAPI instruments requests natively, so the config must be passed to the constructor.
    Providers come from ``OtelUtils`` and are never replaced by globals. Signals without a
    real provider are disabled. ``auto_configure`` is off because ArchiPy owns the exporters.
    FastAPI log records (which carry stack traces) are opt-in via ``OTEL.FASTAPI_LOGS_ENABLED``.

    Args:
        config: Application configuration containing OTel settings.

    Returns:
        Telemetry settings for ``FastAPI(telemetry=...)``.
    """
    from archipy.helpers.utils.otel_utils import OTEL_FASTAPI_INSTALL_HINT, OtelUtils

    disabled: TelemetryConfig = {
        "tracing": False,
        "metrics": False,
        "logs": False,
        "operation_spans": False,
        "auto_configure": False,
    }
    if not config.OTEL.IS_ENABLED:
        return disabled

    try:
        OtelUtils.init_otel_if_needed(config)
        if OtelUtils.import_failed():
            return disabled
        tracer_provider, meter_provider, logger_provider = FastAPIUtils._resolve_otel_providers(config)
    except ImportError:
        logger.warning("%s", OTEL_FASTAPI_INSTALL_HINT)
        return disabled
    # Observability is optional: a failed init must not stop the app.
    except Exception:
        logger.exception("Failed to initialize OpenTelemetry for FastAPI")
        return disabled

    telemetry: TelemetryConfig = {
        "tracing": tracer_provider is not None,
        "metrics": meter_provider is not None,
        "logs": logger_provider is not None,
        "operation_spans": tracer_provider is not None and config.OTEL.FASTAPI_OPERATION_SPANS_ENABLED,
        "auto_configure": False,
    }
    if tracer_provider is not None:
        telemetry["tracer_provider"] = tracer_provider
    if meter_provider is not None:
        telemetry["meter_provider"] = meter_provider
    if logger_provider is not None:
        telemetry["logger_provider"] = logger_provider
    exclude = FastAPIUtils._fastapi_otel_excluded(config.OTEL.FASTAPI_EXCLUDED_URLS)
    if exclude is not None:
        telemetry["exclude"] = exclude
    return telemetry

archipy.helpers.utils.app_utils.AsyncGrpcAPIUtils

async grpc api utilities.

Source code in archipy/helpers/utils/app_utils.py
class AsyncGrpcAPIUtils:
    """async grpc api utilities."""

    @staticmethod
    def setup_otel_interceptor(config: BaseConfig, interceptors: list[Any]) -> None:
        """Configure OpenTelemetry server interceptor for an async gRPC server.

        Inserts the OTel interceptor at position 0 so it wraps later interceptors.

        Args:
            config: Application configuration containing OTel settings.
            interceptors: Mutable list of gRPC interceptors.
        """
        _install_otel_grpc_interceptor(config, interceptors, async_mode=True)

    @staticmethod
    def setup_rate_limit_interceptor(config: BaseConfig, interceptors: list[Any]) -> None:
        """Configures rate-limit interceptor for async gRPC server when enabled.

        Args:
            config (BaseConfig): The configuration object containing gRPC rate-limit settings.
            interceptors (List): List of gRPC interceptors to add the rate-limit interceptor to.
        """
        if not config.GRPC_RATE_LIMIT.IS_ENABLED:
            return

        try:
            from archipy.helpers.interceptors.grpc.rate_limit.grpc_rate_limit_interceptor import (
                AsyncGrpcServerRateLimitInterceptor,
            )

            interceptors.append(AsyncGrpcServerRateLimitInterceptor(rate_limit_config=config.GRPC_RATE_LIMIT))
        # A fail-open limiter must not stop startup; a fail-closed one must not leave the server unprotected.
        except Exception:
            logger.exception("Failed to initialize Rate Limit Interceptor")
            if config.GRPC_RATE_LIMIT.FAIL_CLOSED:
                raise

archipy.helpers.utils.app_utils.AsyncGrpcAPIUtils.setup_otel_interceptor staticmethod

setup_otel_interceptor(
    config: BaseConfig, interceptors: list[Any]
) -> None

Configure OpenTelemetry server interceptor for an async gRPC server.

Inserts the OTel interceptor at position 0 so it wraps later interceptors.

Parameters:

Name Type Description Default
config BaseConfig

Application configuration containing OTel settings.

required
interceptors list[Any]

Mutable list of gRPC interceptors.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_otel_interceptor(config: BaseConfig, interceptors: list[Any]) -> None:
    """Configure OpenTelemetry server interceptor for an async gRPC server.

    Inserts the OTel interceptor at position 0 so it wraps later interceptors.

    Args:
        config: Application configuration containing OTel settings.
        interceptors: Mutable list of gRPC interceptors.
    """
    _install_otel_grpc_interceptor(config, interceptors, async_mode=True)

archipy.helpers.utils.app_utils.AsyncGrpcAPIUtils.setup_rate_limit_interceptor staticmethod

setup_rate_limit_interceptor(
    config: BaseConfig, interceptors: list[Any]
) -> None

Configures rate-limit interceptor for async gRPC server when enabled.

Parameters:

Name Type Description Default
config BaseConfig

The configuration object containing gRPC rate-limit settings.

required
interceptors List

List of gRPC interceptors to add the rate-limit interceptor to.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_rate_limit_interceptor(config: BaseConfig, interceptors: list[Any]) -> None:
    """Configures rate-limit interceptor for async gRPC server when enabled.

    Args:
        config (BaseConfig): The configuration object containing gRPC rate-limit settings.
        interceptors (List): List of gRPC interceptors to add the rate-limit interceptor to.
    """
    if not config.GRPC_RATE_LIMIT.IS_ENABLED:
        return

    try:
        from archipy.helpers.interceptors.grpc.rate_limit.grpc_rate_limit_interceptor import (
            AsyncGrpcServerRateLimitInterceptor,
        )

        interceptors.append(AsyncGrpcServerRateLimitInterceptor(rate_limit_config=config.GRPC_RATE_LIMIT))
    # A fail-open limiter must not stop startup; a fail-closed one must not leave the server unprotected.
    except Exception:
        logger.exception("Failed to initialize Rate Limit Interceptor")
        if config.GRPC_RATE_LIMIT.FAIL_CLOSED:
            raise

archipy.helpers.utils.app_utils.GrpcAPIUtils

grpc api utilities.

Source code in archipy/helpers/utils/app_utils.py
class GrpcAPIUtils:
    """grpc api utilities."""

    @staticmethod
    def setup_otel_interceptor(config: BaseConfig, interceptors: list[Any]) -> None:
        """Configure OpenTelemetry server interceptor for a sync gRPC server.

        Inserts the OTel interceptor at position 0 so it wraps later interceptors.

        Args:
            config: Application configuration containing OTel settings.
            interceptors: Mutable list of gRPC interceptors.
        """
        _install_otel_grpc_interceptor(config, interceptors, async_mode=False)

    @staticmethod
    def setup_rate_limit_interceptor(config: BaseConfig, interceptors: list[Any]) -> None:
        """Configures rate-limit interceptor for gRPC server when enabled.

        Args:
            config (BaseConfig): The configuration object containing gRPC rate-limit settings.
            interceptors (List): List of gRPC interceptors to add the rate-limit interceptor to.
        """
        if not config.GRPC_RATE_LIMIT.IS_ENABLED:
            return

        try:
            from archipy.helpers.interceptors.grpc.rate_limit.grpc_rate_limit_interceptor import (
                GrpcServerRateLimitInterceptor,
            )

            interceptors.append(GrpcServerRateLimitInterceptor(rate_limit_config=config.GRPC_RATE_LIMIT))
        # A fail-open limiter must not stop startup; a fail-closed one must not leave the server unprotected.
        except Exception:
            logger.exception("Failed to initialize Rate Limit Interceptor")
            if config.GRPC_RATE_LIMIT.FAIL_CLOSED:
                raise

archipy.helpers.utils.app_utils.GrpcAPIUtils.setup_otel_interceptor staticmethod

setup_otel_interceptor(
    config: BaseConfig, interceptors: list[Any]
) -> None

Configure OpenTelemetry server interceptor for a sync gRPC server.

Inserts the OTel interceptor at position 0 so it wraps later interceptors.

Parameters:

Name Type Description Default
config BaseConfig

Application configuration containing OTel settings.

required
interceptors list[Any]

Mutable list of gRPC interceptors.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_otel_interceptor(config: BaseConfig, interceptors: list[Any]) -> None:
    """Configure OpenTelemetry server interceptor for a sync gRPC server.

    Inserts the OTel interceptor at position 0 so it wraps later interceptors.

    Args:
        config: Application configuration containing OTel settings.
        interceptors: Mutable list of gRPC interceptors.
    """
    _install_otel_grpc_interceptor(config, interceptors, async_mode=False)

archipy.helpers.utils.app_utils.GrpcAPIUtils.setup_rate_limit_interceptor staticmethod

setup_rate_limit_interceptor(
    config: BaseConfig, interceptors: list[Any]
) -> None

Configures rate-limit interceptor for gRPC server when enabled.

Parameters:

Name Type Description Default
config BaseConfig

The configuration object containing gRPC rate-limit settings.

required
interceptors List

List of gRPC interceptors to add the rate-limit interceptor to.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_rate_limit_interceptor(config: BaseConfig, interceptors: list[Any]) -> None:
    """Configures rate-limit interceptor for gRPC server when enabled.

    Args:
        config (BaseConfig): The configuration object containing gRPC rate-limit settings.
        interceptors (List): List of gRPC interceptors to add the rate-limit interceptor to.
    """
    if not config.GRPC_RATE_LIMIT.IS_ENABLED:
        return

    try:
        from archipy.helpers.interceptors.grpc.rate_limit.grpc_rate_limit_interceptor import (
            GrpcServerRateLimitInterceptor,
        )

        interceptors.append(GrpcServerRateLimitInterceptor(rate_limit_config=config.GRPC_RATE_LIMIT))
    # A fail-open limiter must not stop startup; a fail-closed one must not leave the server unprotected.
    except Exception:
        logger.exception("Failed to initialize Rate Limit Interceptor")
        if config.GRPC_RATE_LIMIT.FAIL_CLOSED:
            raise

archipy.helpers.utils.app_utils.AppUtils

Utility class for creating and configuring FastAPI applications.

Source code in archipy/helpers/utils/app_utils.py
class AppUtils:
    """Utility class for creating and configuring FastAPI applications."""

    @staticmethod
    def _compose_otel_lifespan(
        config: BaseConfig,
        lifespan: Callable[..., AbstractAsyncContextManager] | None,
    ) -> Callable[..., AbstractAsyncContextManager] | None:
        """Wrap a FastAPI lifespan so OTel force-flushes on exit.

        Args:
            config: Application configuration.
            lifespan: Optional caller-provided lifespan context manager factory.

        Returns:
            A lifespan factory that preserves the user lifespan and runs
            ``force_flush`` afterward, or ``None`` when OTel is off and no user
            lifespan was provided. Provider shutdown remains on process atexit.
        """
        if not config.OTEL.IS_ENABLED:
            return lifespan

        from contextlib import asynccontextmanager

        from archipy.helpers.utils.otel_utils import OtelUtils

        @asynccontextmanager
        async def otel_lifespan(app: FastAPI) -> AsyncGenerator[None]:
            try:
                if lifespan is not None:
                    async with lifespan(app):
                        yield
                else:
                    yield
            finally:
                # Flush only — do not shutdown here. Multiple apps / TestClient
                # share process-wide OtelUtils; full shutdown stays on atexit.
                try:
                    OtelUtils.force_flush()
                # Best-effort flush on shutdown: never block app teardown.
                except Exception:
                    logger.debug("Error during OTel force_flush in FastAPI lifespan", exc_info=True)

        return otel_lifespan

    @classmethod
    def create_fastapi_app(
        cls,
        config: BaseConfig | None = None,
        *,
        configure_exception_handlers: bool = True,
        include_common_responses: bool = True,
        lifespan: Callable[..., AbstractAsyncContextManager] | None = None,
    ) -> FastAPI:
        """Create and configure a FastAPI application.

        Args:
            config (BaseConfig | None, optional): Custom configuration. If not provided, uses global config.
            configure_exception_handlers (bool, optional): Whether to configure exception handlers.
                Defaults to True.
            include_common_responses (bool, optional): Whether to configure common response definitions
                for all endpoints. Defaults to True.
            lifespan (Callable[..., AbstractAsyncContextManager] | None, optional): Custom lifespan
                context manager for the app. Defaults to None.

        Returns:
            FastAPI: The configured FastAPI application instance.
        """
        from archipy.helpers.errors.fastapi import get_fastapi_exception_responses, setup_exception_handlers

        config = config or BaseConfig.global_config()

        # Define common responses for all endpoints
        common_responses = get_fastapi_exception_responses(
            [UnknownError, UnavailableError, InvalidArgumentError],
        )
        # Convert dict[int, ...] to dict[int | str, ...] for FastAPI compatibility
        responses_dict: dict[int | str, dict[str, object]] | None = None
        if include_common_responses and common_responses:
            responses_dict = dict(common_responses.items())

        resolved_lifespan = cls._compose_otel_lifespan(config, lifespan)
        app = FastAPI(
            title=config.FASTAPI.PROJECT_NAME,
            openapi_url=config.FASTAPI.OPENAPI_URL,
            generate_unique_id_function=FastAPIUtils.custom_generate_unique_id,
            swagger_ui_parameters=config.FASTAPI.SWAGGER_UI_PARAMS,
            docs_url=config.FASTAPI.DOCS_URL,
            redoc_url=config.FASTAPI.RE_DOC_URL,
            responses=responses_dict,
            lifespan=resolved_lifespan,
            telemetry=FastAPIUtils.build_otel_telemetry_config(config),
        )

        FastAPIUtils.setup_cors(app, config)
        FastAPIUtils.setup_gzip(app, config)
        FastAPIUtils.setup_https_redirect(app, config)
        FastAPIUtils.setup_trusted_host(app, config)

        if configure_exception_handlers:
            setup_exception_handlers(app)

        return app

    @staticmethod
    def _ordered_interceptors(customized_interceptors: Sequence[object] | AbstractSet[object]) -> list[object]:
        """Return custom interceptors in the order they run.

        Pass a ``list`` or ``tuple`` to control the order (first runs outermost). A ``set`` is still accepted for
        backward compatibility, but its iteration order is arbitrary.

        Args:
            customized_interceptors: The interceptors supplied by the caller.

        Returns:
            list[object]: The interceptors as a list.
        """
        return list(customized_interceptors)

    @classmethod
    def create_async_grpc_app(
        cls,
        config: BaseConfig,
        customized_interceptors: Sequence[object] | AbstractSet[object] | None = None,
        compression: grpc.Compression | None = None,
        *,
        validation_errors_as_internal: bool = False,
    ) -> GrpcAioServer:
        """Create and configure an async gRPC application.

        Args:
            config: The application config.
            customized_interceptors: Extra interceptors appended after the built-in ones.
            compression: The server compression.
            validation_errors_as_internal: Report a pydantic ``ValidationError`` raised by a handler as INTERNAL
                instead of INVALID_ARGUMENT. Defaults to False.
        """
        from archipy.helpers.errors.grpc import GrpcExceptionInterceptor

        # grpc-stubs want grpc.aio.ServerInterceptor items, which the GrpcExceptionInterceptor factory and the
        # untyped custom interceptors are not declared as
        async_interceptors: list[Any] = [
            GrpcExceptionInterceptor(aio=True, validation_errors_as_internal=validation_errors_as_internal),
        ]

        # OTel inserts at 0 → order: OTel → exception → rate-limit → custom
        AsyncGrpcAPIUtils.setup_otel_interceptor(config, async_interceptors)
        AsyncGrpcAPIUtils.setup_rate_limit_interceptor(config, async_interceptors)

        if customized_interceptors:
            async_interceptors.extend(cls._ordered_interceptors(customized_interceptors))

        if create_grpc_server is None:
            raise ConfigurationError(operation="import", reason="grpc_aio_extra_required")
        return create_grpc_server(
            futures.ThreadPoolExecutor(max_workers=config.GRPC.THREAD_WORKER_COUNT),
            interceptors=async_interceptors,
            compression=compression,
            options=config.GRPC.SERVER_OPTIONS_CONFIG_LIST,
            maximum_concurrent_rpcs=config.GRPC.MAX_CONCURRENT_RPCS,
        )

    @classmethod
    def create_grpc_app(
        cls,
        config: BaseConfig,
        customized_interceptors: Sequence[object] | AbstractSet[object] | None = None,
        compression: grpc.Compression | None = None,
        *,
        validation_errors_as_internal: bool = False,
    ) -> grpc.Server:
        """Create and configure a synchronous gRPC server.

        Args:
            config: The application config.
            customized_interceptors: Extra interceptors appended after the built-in ones.
            compression: The server compression.
            validation_errors_as_internal: Report a pydantic ``ValidationError`` raised by a handler as INTERNAL
                instead of INVALID_ARGUMENT. Defaults to False.
        """
        from archipy.helpers.errors.grpc import GrpcExceptionInterceptor

        # grpc-stubs want grpc.ServerInterceptor items, which the GrpcExceptionInterceptor factory and the
        # untyped custom interceptors are not declared as
        interceptors: list[Any] = [
            GrpcExceptionInterceptor(validation_errors_as_internal=validation_errors_as_internal),
        ]

        # OTel inserts at 0 → order: OTel → exception → rate-limit → custom
        GrpcAPIUtils.setup_otel_interceptor(config, interceptors)
        GrpcAPIUtils.setup_rate_limit_interceptor(config, interceptors)
        if customized_interceptors:
            interceptors.extend(cls._ordered_interceptors(customized_interceptors))

        return grpc.server(
            futures.ThreadPoolExecutor(max_workers=config.GRPC.THREAD_WORKER_COUNT),
            interceptors=interceptors,
            compression=compression,
            options=config.GRPC.SERVER_OPTIONS_CONFIG_LIST,
            maximum_concurrent_rpcs=config.GRPC.MAX_CONCURRENT_RPCS,
        )

archipy.helpers.utils.app_utils.AppUtils.create_fastapi_app classmethod

create_fastapi_app(
    config: BaseConfig | None = None,
    *,
    configure_exception_handlers: bool = True,
    include_common_responses: bool = True,
    lifespan: Callable[..., AbstractAsyncContextManager]
    | None = None,
) -> FastAPI

Create and configure a FastAPI application.

Parameters:

Name Type Description Default
config BaseConfig | None

Custom configuration. If not provided, uses global config.

None
configure_exception_handlers bool

Whether to configure exception handlers. Defaults to True.

True
include_common_responses bool

Whether to configure common response definitions for all endpoints. Defaults to True.

True
lifespan Callable[..., AbstractAsyncContextManager] | None

Custom lifespan context manager for the app. Defaults to None.

None

Returns:

Name Type Description
FastAPI FastAPI

The configured FastAPI application instance.

Source code in archipy/helpers/utils/app_utils.py
@classmethod
def create_fastapi_app(
    cls,
    config: BaseConfig | None = None,
    *,
    configure_exception_handlers: bool = True,
    include_common_responses: bool = True,
    lifespan: Callable[..., AbstractAsyncContextManager] | None = None,
) -> FastAPI:
    """Create and configure a FastAPI application.

    Args:
        config (BaseConfig | None, optional): Custom configuration. If not provided, uses global config.
        configure_exception_handlers (bool, optional): Whether to configure exception handlers.
            Defaults to True.
        include_common_responses (bool, optional): Whether to configure common response definitions
            for all endpoints. Defaults to True.
        lifespan (Callable[..., AbstractAsyncContextManager] | None, optional): Custom lifespan
            context manager for the app. Defaults to None.

    Returns:
        FastAPI: The configured FastAPI application instance.
    """
    from archipy.helpers.errors.fastapi import get_fastapi_exception_responses, setup_exception_handlers

    config = config or BaseConfig.global_config()

    # Define common responses for all endpoints
    common_responses = get_fastapi_exception_responses(
        [UnknownError, UnavailableError, InvalidArgumentError],
    )
    # Convert dict[int, ...] to dict[int | str, ...] for FastAPI compatibility
    responses_dict: dict[int | str, dict[str, object]] | None = None
    if include_common_responses and common_responses:
        responses_dict = dict(common_responses.items())

    resolved_lifespan = cls._compose_otel_lifespan(config, lifespan)
    app = FastAPI(
        title=config.FASTAPI.PROJECT_NAME,
        openapi_url=config.FASTAPI.OPENAPI_URL,
        generate_unique_id_function=FastAPIUtils.custom_generate_unique_id,
        swagger_ui_parameters=config.FASTAPI.SWAGGER_UI_PARAMS,
        docs_url=config.FASTAPI.DOCS_URL,
        redoc_url=config.FASTAPI.RE_DOC_URL,
        responses=responses_dict,
        lifespan=resolved_lifespan,
        telemetry=FastAPIUtils.build_otel_telemetry_config(config),
    )

    FastAPIUtils.setup_cors(app, config)
    FastAPIUtils.setup_gzip(app, config)
    FastAPIUtils.setup_https_redirect(app, config)
    FastAPIUtils.setup_trusted_host(app, config)

    if configure_exception_handlers:
        setup_exception_handlers(app)

    return app

archipy.helpers.utils.app_utils.AppUtils.create_async_grpc_app classmethod

create_async_grpc_app(
    config: BaseConfig,
    customized_interceptors: Sequence[object]
    | Set[object]
    | None = None,
    compression: Compression | None = None,
    *,
    validation_errors_as_internal: bool = False,
) -> GrpcAioServer

Create and configure an async gRPC application.

Parameters:

Name Type Description Default
config BaseConfig

The application config.

required
customized_interceptors Sequence[object] | Set[object] | None

Extra interceptors appended after the built-in ones.

None
compression Compression | None

The server compression.

None
validation_errors_as_internal bool

Report a pydantic ValidationError raised by a handler as INTERNAL instead of INVALID_ARGUMENT. Defaults to False.

False
Source code in archipy/helpers/utils/app_utils.py
@classmethod
def create_async_grpc_app(
    cls,
    config: BaseConfig,
    customized_interceptors: Sequence[object] | AbstractSet[object] | None = None,
    compression: grpc.Compression | None = None,
    *,
    validation_errors_as_internal: bool = False,
) -> GrpcAioServer:
    """Create and configure an async gRPC application.

    Args:
        config: The application config.
        customized_interceptors: Extra interceptors appended after the built-in ones.
        compression: The server compression.
        validation_errors_as_internal: Report a pydantic ``ValidationError`` raised by a handler as INTERNAL
            instead of INVALID_ARGUMENT. Defaults to False.
    """
    from archipy.helpers.errors.grpc import GrpcExceptionInterceptor

    # grpc-stubs want grpc.aio.ServerInterceptor items, which the GrpcExceptionInterceptor factory and the
    # untyped custom interceptors are not declared as
    async_interceptors: list[Any] = [
        GrpcExceptionInterceptor(aio=True, validation_errors_as_internal=validation_errors_as_internal),
    ]

    # OTel inserts at 0 → order: OTel → exception → rate-limit → custom
    AsyncGrpcAPIUtils.setup_otel_interceptor(config, async_interceptors)
    AsyncGrpcAPIUtils.setup_rate_limit_interceptor(config, async_interceptors)

    if customized_interceptors:
        async_interceptors.extend(cls._ordered_interceptors(customized_interceptors))

    if create_grpc_server is None:
        raise ConfigurationError(operation="import", reason="grpc_aio_extra_required")
    return create_grpc_server(
        futures.ThreadPoolExecutor(max_workers=config.GRPC.THREAD_WORKER_COUNT),
        interceptors=async_interceptors,
        compression=compression,
        options=config.GRPC.SERVER_OPTIONS_CONFIG_LIST,
        maximum_concurrent_rpcs=config.GRPC.MAX_CONCURRENT_RPCS,
    )

archipy.helpers.utils.app_utils.AppUtils.create_grpc_app classmethod

create_grpc_app(
    config: BaseConfig,
    customized_interceptors: Sequence[object]
    | Set[object]
    | None = None,
    compression: Compression | None = None,
    *,
    validation_errors_as_internal: bool = False,
) -> grpc.Server

Create and configure a synchronous gRPC server.

Parameters:

Name Type Description Default
config BaseConfig

The application config.

required
customized_interceptors Sequence[object] | Set[object] | None

Extra interceptors appended after the built-in ones.

None
compression Compression | None

The server compression.

None
validation_errors_as_internal bool

Report a pydantic ValidationError raised by a handler as INTERNAL instead of INVALID_ARGUMENT. Defaults to False.

False
Source code in archipy/helpers/utils/app_utils.py
@classmethod
def create_grpc_app(
    cls,
    config: BaseConfig,
    customized_interceptors: Sequence[object] | AbstractSet[object] | None = None,
    compression: grpc.Compression | None = None,
    *,
    validation_errors_as_internal: bool = False,
) -> grpc.Server:
    """Create and configure a synchronous gRPC server.

    Args:
        config: The application config.
        customized_interceptors: Extra interceptors appended after the built-in ones.
        compression: The server compression.
        validation_errors_as_internal: Report a pydantic ``ValidationError`` raised by a handler as INTERNAL
            instead of INVALID_ARGUMENT. Defaults to False.
    """
    from archipy.helpers.errors.grpc import GrpcExceptionInterceptor

    # grpc-stubs want grpc.ServerInterceptor items, which the GrpcExceptionInterceptor factory and the
    # untyped custom interceptors are not declared as
    interceptors: list[Any] = [
        GrpcExceptionInterceptor(validation_errors_as_internal=validation_errors_as_internal),
    ]

    # OTel inserts at 0 → order: OTel → exception → rate-limit → custom
    GrpcAPIUtils.setup_otel_interceptor(config, interceptors)
    GrpcAPIUtils.setup_rate_limit_interceptor(config, interceptors)
    if customized_interceptors:
        interceptors.extend(cls._ordered_interceptors(customized_interceptors))

    return grpc.server(
        futures.ThreadPoolExecutor(max_workers=config.GRPC.THREAD_WORKER_COUNT),
        interceptors=interceptors,
        compression=compression,
        options=config.GRPC.SERVER_OPTIONS_CONFIG_LIST,
        maximum_concurrent_rpcs=config.GRPC.MAX_CONCURRENT_RPCS,
    )

options: show_root_toc_entry: false heading_level: 3

Datetime Utils

Utilities for timezone-aware date and time operations with microsecond precision.

Date and time utility helpers.

archipy.helpers.utils.datetime_utils.DatetimeUtils

A utility class for handling date and time operations, including conversions, caching, and API integrations.

This class provides methods for working with both Gregorian and Jalali (Persian) calendars, as well as utility functions for timezone-aware datetime objects, date ranges, and string formatting.

Source code in archipy/helpers/utils/datetime_utils.py
class DatetimeUtils:
    """A utility class for handling date and time operations, including conversions, caching, and API integrations.

    This class provides methods for working with both Gregorian and Jalali (Persian) calendars, as well as
    utility functions for timezone-aware datetime objects, date ranges, and string formatting.
    """

    """A class-level cache for storing holiday statuses to avoid redundant API calls."""
    _holiday_cache: ClassVar[dict[str, tuple[bool, datetime]]] = {}
    _holiday_cache_lock: ClassVar[threading.Lock] = threading.Lock()
    _HOLIDAY_CACHE_MAX_SIZE: ClassVar[int] = 4096

    @staticmethod
    def convert_to_jalali(target_date: date) -> jdatetime.date:
        """Converts a Gregorian date to a Jalali (Persian) date.

        Args:
            target_date (date): The Gregorian date to convert.

        Returns:
            jdatetime.date: The corresponding Jalali date.
        """
        return jdatetime.date.fromgregorian(date=target_date)

    @classmethod
    def is_holiday_in_iran(cls, target_date: date) -> bool:
        """Determines if the target date is a holiday in Iran.

        This method leverages caching and an external API to check if the given date is a holiday.

        Args:
            target_date (date): The date to check for holiday status.

        Returns:
            bool: True if the date is a holiday, False otherwise.
        """
        # Convert to Jalali date first
        jalali_date = cls.convert_to_jalali(target_date)
        date_str = target_date.strftime("%Y-%m-%d")
        current_time = cls.get_datetime_utc_now()

        # Check cache first
        is_cached, is_holiday = cls._check_cache(date_str, current_time)
        if is_cached:
            return is_holiday

        # Fetch holiday status and cache it
        return cls._fetch_and_cache_holiday_status(jalali_date, date_str, current_time)

    @classmethod
    def _check_cache(cls, date_str: str, current_time: datetime) -> tuple[bool, bool]:
        """Checks the cache for holiday status to avoid redundant API calls.

        Args:
            date_str (str): The date string to check in the cache.
            current_time (datetime): The current time to compare against cache expiration.

        Returns:
            tuple[bool, bool]: A tuple where the first element indicates if the cache was hit,
                               and the second element is the cached holiday status.
        """
        with cls._holiday_cache_lock:
            cached_data = cls._holiday_cache.get(date_str)
            if cached_data:
                is_holiday, expiry_time = cached_data
                if current_time < expiry_time:
                    return True, is_holiday

                # Remove expired cache entry
                cls._holiday_cache.pop(date_str, None)

        return False, False

    @classmethod
    def _store_in_cache(cls, date_str: str, is_holiday: bool, expiry_time: datetime, current_time: datetime) -> None:
        """Stores a holiday status in the cache, evicting expired (then oldest) entries when the cache is full."""
        with cls._holiday_cache_lock:
            if date_str not in cls._holiday_cache and len(cls._holiday_cache) >= cls._HOLIDAY_CACHE_MAX_SIZE:
                for key in [k for k, (_, expiry) in cls._holiday_cache.items() if expiry <= current_time]:
                    cls._holiday_cache.pop(key, None)
                while len(cls._holiday_cache) >= cls._HOLIDAY_CACHE_MAX_SIZE:
                    cls._holiday_cache.pop(next(iter(cls._holiday_cache)), None)
            cls._holiday_cache[date_str] = (is_holiday, expiry_time)

    @classmethod
    def _fetch_and_cache_holiday_status(
        cls,
        jalali_date: jdatetime.date,
        date_str: str,
        current_time: datetime,
    ) -> bool:
        """Fetches holiday status from the API and caches the result.

        This method calls an external API to determine if the given Jalali date is a holiday.
        If the API call is successful, the result is cached with an expiration time to avoid
        redundant API calls. If the API call fails, an `UnknownError` is raised.

        Args:
            jalali_date (jdatetime.date): The Jalali date to check for holiday status.
            date_str (str): The date string to use as a cache key.
            current_time (datetime): The current time to set cache expiration.

        Returns:
            bool: True if the date is a holiday, False otherwise.

        Raises:
            UnknownError: If the API request fails due to a network issue, or its response is not valid JSON.
        """
        try:
            config = BaseConfig.global_config()
            response = cls._call_holiday_api(jalali_date)
            is_holiday = cls._parse_holiday_response(response, jalali_date)

            # Determine cache TTL based on whether the date is historical
            target_date = datetime.strptime(date_str, "%Y-%m-%d").replace(tzinfo=UTC).date()
            is_historical = target_date <= current_time.date()
            cache_ttl = config.DATETIME.HISTORICAL_CACHE_TTL if is_historical else config.DATETIME.CACHE_TTL

            # Cache the result with appropriate expiration
            expiry_time = current_time + timedelta(seconds=cache_ttl)
            cls._store_in_cache(date_str, is_holiday, expiry_time, current_time)
        except (httpx2.HTTPError, ValueError) as exception:
            raise UnknownError from exception

        return is_holiday

    @staticmethod
    @trace_span(name="datetime.get_holiday_api")
    def _call_holiday_api(jalali_date: jdatetime.date) -> _HolidayResponse:
        """Calls the Time.ir API to fetch holiday data for the given Jalali date.

        Args:
            jalali_date (jdatetime.date): The Jalali date to fetch data for.

        Returns:
            _HolidayResponse: The JSON response from the API.

        Raises:
            httpx2.HTTPError: If the API request fails.
        """
        config = BaseConfig.global_config()
        transport = httpx2.HTTPTransport(retries=config.DATETIME.MAX_RETRIES)
        url = DatetimeUtils._build_api_url(jalali_date)
        api_key = config.DATETIME.TIME_IR_API_KEY
        headers = {"x-api-key": api_key} if api_key else {}
        with httpx2.Client(transport=transport, timeout=config.DATETIME.REQUEST_TIMEOUT) as client:
            response = client.get(url, headers=headers)
            response.raise_for_status()
            result: _HolidayResponse = response.json()
            return result

    @staticmethod
    def _build_api_url(jalali_date: jdatetime.date) -> str:
        """Builds the API URL with Jalali date parameters.

        Args:
            jalali_date (jdatetime.date): The Jalali date to include in the URL.

        Returns:
            str: The constructed API URL.
        """
        config = BaseConfig.global_config()
        base_url = config.DATETIME.TIME_IR_API_ENDPOINT
        return f"{base_url}?year={jalali_date.year}&month={jalali_date.month}&day={jalali_date.day}"

    @staticmethod
    def _parse_holiday_response(response_data: _HolidayResponse, jalali_date: jdatetime.date) -> bool:
        """Parses the API response to extract and return the holiday status.

        Args:
            response_data (_HolidayResponse): The JSON response from the API.
            jalali_date (jdatetime.date): The Jalali date to check.

        Returns:
            bool: True if the date is a holiday, False otherwise.
        """
        event_list = response_data.get("data", {}).get("event_list", [])
        for event_info in event_list:
            if (
                event_info.get("jalali_year") == jalali_date.year
                and event_info.get("jalali_month") == jalali_date.month
                and event_info.get("jalali_day") == jalali_date.day
            ):
                is_holiday = event_info.get("is_holiday", False)
                return bool(is_holiday)
        return False

    @classmethod
    def ensure_timezone_aware(cls, dt: datetime) -> datetime:
        """Ensures a datetime object is timezone-aware, converting it to UTC if necessary.

        Args:
            dt (datetime): The datetime object to make timezone-aware.

        Returns:
            datetime: The timezone-aware datetime object.
        """
        if dt.tzinfo is None:
            return dt.replace(tzinfo=UTC)
        return dt

    @classmethod
    def daterange(cls, start_date: datetime, end_date: datetime) -> Generator[date]:
        """Generates a range of dates from start_date to end_date, exclusive of end_date.

        Args:
            start_date (datetime): The start date of the range.
            end_date (datetime): The end date of the range.

        Yields:
            date: Each date in the range.
        """
        # Compare calendar dates (naive values are read as UTC) so a range that crosses midnight is not skipped.
        first_day = cls.ensure_timezone_aware(start_date).date()
        last_day = cls.ensure_timezone_aware(end_date).date()
        for n in range((last_day - first_day).days):
            yield first_day + timedelta(n)

    @classmethod
    def get_string_datetime_from_datetime(cls, dt: datetime, format_: str | None = None) -> str:
        """Converts a datetime object to a formatted string. Default format is ISO 8601.

        Args:
            dt (datetime): The datetime object to format.
            format_ (str | None): The format string. If None, uses ISO 8601.

        Returns:
            str: The formatted datetime string.
        """
        format_ = format_ or "%Y-%m-%dT%H:%M:%S.%f"
        return dt.strftime(format_)

    @classmethod
    def standardize_string_datetime(cls, date_string: str) -> str:
        """Standardizes a datetime string to the default format.

        Args:
            date_string (str): The datetime string to standardize.

        Returns:
            str: The standardized datetime string.
        """
        datetime_ = cls.get_datetime_from_string_datetime(date_string)
        return cls.get_string_datetime_from_datetime(datetime_)

    @classmethod
    def get_datetime_from_string_datetime(cls, date_string: str, format_: str | None = None) -> datetime:
        """Parses a string to a datetime object using the given format, or ISO 8601 by default.

        Args:
            date_string (str): The datetime string to parse.
            format_ (str | None): The format string. If None, uses ISO 8601.

        Returns:
            datetime: The parsed timezone-aware datetime. Strings without an offset are read as UTC.
        """
        dt = datetime.fromisoformat(date_string) if format_ is None else datetime.strptime(date_string, format_)

        # Values without an offset are read as UTC; an offset parsed from the string (e.g. ``%z``) is kept
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=UTC)

        return dt

    @classmethod
    def get_string_datetime_now(cls) -> str:
        """Gets the current datetime as a formatted string. Default format is ISO 8601.

        Returns:
            str: The formatted datetime string.
        """
        return cls.get_string_datetime_from_datetime(cls.get_datetime_now())

    @classmethod
    def get_datetime_now(cls) -> datetime:
        """Gets the current local datetime.

        Returns:
            datetime: The current local datetime.
        """
        return datetime.now()

    @classmethod
    def get_datetime_utc_now(cls) -> datetime:
        """Gets the current UTC datetime.

        Returns:
            datetime: The current UTC datetime.
        """
        return datetime.now(UTC)

    @classmethod
    def get_epoch_time_now(cls) -> int:
        """Gets the current time in seconds since the epoch.

        Returns:
            int: The current epoch time.
        """
        return int(time.time())

    @classmethod
    def get_datetime_before_given_datetime_or_now(
        cls,
        weeks: int = 0,
        days: int = 0,
        hours: int = 0,
        minutes: int = 0,
        seconds: int = 0,
        datetime_given: datetime | None = None,
    ) -> datetime:
        """Subtracts time from a given datetime or the current datetime if not specified.

        Args:
            weeks (int): The number of weeks to subtract.
            days (int): The number of days to subtract.
            hours (int): The number of hours to subtract.
            minutes (int): The number of minutes to subtract.
            seconds (int): The number of seconds to subtract.
            datetime_given (datetime | None): The datetime to subtract from. If None, uses the current datetime.

        Returns:
            datetime: The resulting datetime after subtraction.
        """
        datetime_given = datetime_given or cls.get_datetime_now()
        return datetime_given - timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)

    @classmethod
    def get_datetime_after_given_datetime_or_now(
        cls,
        weeks: int = 0,
        days: int = 0,
        hours: int = 0,
        minutes: int = 0,
        seconds: int = 0,
        datetime_given: datetime | None = None,
    ) -> datetime:
        """Adds time to a given datetime or the current datetime if not specified.

        Args:
            weeks (int): The number of weeks to add.
            days (int): The number of days to add.
            hours (int): The number of hours to add.
            minutes (int): The number of minutes to add.
            seconds (int): The number of seconds to add.
            datetime_given (datetime | None): The datetime to add to. If None, uses the current datetime.

        Returns:
            datetime: The resulting datetime after addition.
        """
        datetime_given = datetime_given or cls.get_datetime_now()
        return datetime_given + timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)

archipy.helpers.utils.datetime_utils.DatetimeUtils.convert_to_jalali staticmethod

convert_to_jalali(target_date: date) -> jdatetime.date

Converts a Gregorian date to a Jalali (Persian) date.

Parameters:

Name Type Description Default
target_date date

The Gregorian date to convert.

required

Returns:

Type Description
date

jdatetime.date: The corresponding Jalali date.

Source code in archipy/helpers/utils/datetime_utils.py
@staticmethod
def convert_to_jalali(target_date: date) -> jdatetime.date:
    """Converts a Gregorian date to a Jalali (Persian) date.

    Args:
        target_date (date): The Gregorian date to convert.

    Returns:
        jdatetime.date: The corresponding Jalali date.
    """
    return jdatetime.date.fromgregorian(date=target_date)

archipy.helpers.utils.datetime_utils.DatetimeUtils.is_holiday_in_iran classmethod

is_holiday_in_iran(target_date: date) -> bool

Determines if the target date is a holiday in Iran.

This method leverages caching and an external API to check if the given date is a holiday.

Parameters:

Name Type Description Default
target_date date

The date to check for holiday status.

required

Returns:

Name Type Description
bool bool

True if the date is a holiday, False otherwise.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def is_holiday_in_iran(cls, target_date: date) -> bool:
    """Determines if the target date is a holiday in Iran.

    This method leverages caching and an external API to check if the given date is a holiday.

    Args:
        target_date (date): The date to check for holiday status.

    Returns:
        bool: True if the date is a holiday, False otherwise.
    """
    # Convert to Jalali date first
    jalali_date = cls.convert_to_jalali(target_date)
    date_str = target_date.strftime("%Y-%m-%d")
    current_time = cls.get_datetime_utc_now()

    # Check cache first
    is_cached, is_holiday = cls._check_cache(date_str, current_time)
    if is_cached:
        return is_holiday

    # Fetch holiday status and cache it
    return cls._fetch_and_cache_holiday_status(jalali_date, date_str, current_time)

archipy.helpers.utils.datetime_utils.DatetimeUtils.ensure_timezone_aware classmethod

ensure_timezone_aware(dt: datetime) -> datetime

Ensures a datetime object is timezone-aware, converting it to UTC if necessary.

Parameters:

Name Type Description Default
dt datetime

The datetime object to make timezone-aware.

required

Returns:

Name Type Description
datetime datetime

The timezone-aware datetime object.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def ensure_timezone_aware(cls, dt: datetime) -> datetime:
    """Ensures a datetime object is timezone-aware, converting it to UTC if necessary.

    Args:
        dt (datetime): The datetime object to make timezone-aware.

    Returns:
        datetime: The timezone-aware datetime object.
    """
    if dt.tzinfo is None:
        return dt.replace(tzinfo=UTC)
    return dt

archipy.helpers.utils.datetime_utils.DatetimeUtils.daterange classmethod

daterange(
    start_date: datetime, end_date: datetime
) -> Generator[date]

Generates a range of dates from start_date to end_date, exclusive of end_date.

Parameters:

Name Type Description Default
start_date datetime

The start date of the range.

required
end_date datetime

The end date of the range.

required

Yields:

Name Type Description
date Generator[date]

Each date in the range.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def daterange(cls, start_date: datetime, end_date: datetime) -> Generator[date]:
    """Generates a range of dates from start_date to end_date, exclusive of end_date.

    Args:
        start_date (datetime): The start date of the range.
        end_date (datetime): The end date of the range.

    Yields:
        date: Each date in the range.
    """
    # Compare calendar dates (naive values are read as UTC) so a range that crosses midnight is not skipped.
    first_day = cls.ensure_timezone_aware(start_date).date()
    last_day = cls.ensure_timezone_aware(end_date).date()
    for n in range((last_day - first_day).days):
        yield first_day + timedelta(n)

archipy.helpers.utils.datetime_utils.DatetimeUtils.get_string_datetime_from_datetime classmethod

get_string_datetime_from_datetime(
    dt: datetime, format_: str | None = None
) -> str

Converts a datetime object to a formatted string. Default format is ISO 8601.

Parameters:

Name Type Description Default
dt datetime

The datetime object to format.

required
format_ str | None

The format string. If None, uses ISO 8601.

None

Returns:

Name Type Description
str str

The formatted datetime string.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_string_datetime_from_datetime(cls, dt: datetime, format_: str | None = None) -> str:
    """Converts a datetime object to a formatted string. Default format is ISO 8601.

    Args:
        dt (datetime): The datetime object to format.
        format_ (str | None): The format string. If None, uses ISO 8601.

    Returns:
        str: The formatted datetime string.
    """
    format_ = format_ or "%Y-%m-%dT%H:%M:%S.%f"
    return dt.strftime(format_)

archipy.helpers.utils.datetime_utils.DatetimeUtils.standardize_string_datetime classmethod

standardize_string_datetime(date_string: str) -> str

Standardizes a datetime string to the default format.

Parameters:

Name Type Description Default
date_string str

The datetime string to standardize.

required

Returns:

Name Type Description
str str

The standardized datetime string.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def standardize_string_datetime(cls, date_string: str) -> str:
    """Standardizes a datetime string to the default format.

    Args:
        date_string (str): The datetime string to standardize.

    Returns:
        str: The standardized datetime string.
    """
    datetime_ = cls.get_datetime_from_string_datetime(date_string)
    return cls.get_string_datetime_from_datetime(datetime_)

archipy.helpers.utils.datetime_utils.DatetimeUtils.get_datetime_from_string_datetime classmethod

get_datetime_from_string_datetime(
    date_string: str, format_: str | None = None
) -> datetime

Parses a string to a datetime object using the given format, or ISO 8601 by default.

Parameters:

Name Type Description Default
date_string str

The datetime string to parse.

required
format_ str | None

The format string. If None, uses ISO 8601.

None

Returns:

Name Type Description
datetime datetime

The parsed timezone-aware datetime. Strings without an offset are read as UTC.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_from_string_datetime(cls, date_string: str, format_: str | None = None) -> datetime:
    """Parses a string to a datetime object using the given format, or ISO 8601 by default.

    Args:
        date_string (str): The datetime string to parse.
        format_ (str | None): The format string. If None, uses ISO 8601.

    Returns:
        datetime: The parsed timezone-aware datetime. Strings without an offset are read as UTC.
    """
    dt = datetime.fromisoformat(date_string) if format_ is None else datetime.strptime(date_string, format_)

    # Values without an offset are read as UTC; an offset parsed from the string (e.g. ``%z``) is kept
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=UTC)

    return dt

archipy.helpers.utils.datetime_utils.DatetimeUtils.get_string_datetime_now classmethod

get_string_datetime_now() -> str

Gets the current datetime as a formatted string. Default format is ISO 8601.

Returns:

Name Type Description
str str

The formatted datetime string.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_string_datetime_now(cls) -> str:
    """Gets the current datetime as a formatted string. Default format is ISO 8601.

    Returns:
        str: The formatted datetime string.
    """
    return cls.get_string_datetime_from_datetime(cls.get_datetime_now())

archipy.helpers.utils.datetime_utils.DatetimeUtils.get_datetime_now classmethod

get_datetime_now() -> datetime

Gets the current local datetime.

Returns:

Name Type Description
datetime datetime

The current local datetime.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_now(cls) -> datetime:
    """Gets the current local datetime.

    Returns:
        datetime: The current local datetime.
    """
    return datetime.now()

archipy.helpers.utils.datetime_utils.DatetimeUtils.get_datetime_utc_now classmethod

get_datetime_utc_now() -> datetime

Gets the current UTC datetime.

Returns:

Name Type Description
datetime datetime

The current UTC datetime.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_utc_now(cls) -> datetime:
    """Gets the current UTC datetime.

    Returns:
        datetime: The current UTC datetime.
    """
    return datetime.now(UTC)

archipy.helpers.utils.datetime_utils.DatetimeUtils.get_epoch_time_now classmethod

get_epoch_time_now() -> int

Gets the current time in seconds since the epoch.

Returns:

Name Type Description
int int

The current epoch time.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_epoch_time_now(cls) -> int:
    """Gets the current time in seconds since the epoch.

    Returns:
        int: The current epoch time.
    """
    return int(time.time())

archipy.helpers.utils.datetime_utils.DatetimeUtils.get_datetime_before_given_datetime_or_now classmethod

get_datetime_before_given_datetime_or_now(
    weeks: int = 0,
    days: int = 0,
    hours: int = 0,
    minutes: int = 0,
    seconds: int = 0,
    datetime_given: datetime | None = None,
) -> datetime

Subtracts time from a given datetime or the current datetime if not specified.

Parameters:

Name Type Description Default
weeks int

The number of weeks to subtract.

0
days int

The number of days to subtract.

0
hours int

The number of hours to subtract.

0
minutes int

The number of minutes to subtract.

0
seconds int

The number of seconds to subtract.

0
datetime_given datetime | None

The datetime to subtract from. If None, uses the current datetime.

None

Returns:

Name Type Description
datetime datetime

The resulting datetime after subtraction.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_before_given_datetime_or_now(
    cls,
    weeks: int = 0,
    days: int = 0,
    hours: int = 0,
    minutes: int = 0,
    seconds: int = 0,
    datetime_given: datetime | None = None,
) -> datetime:
    """Subtracts time from a given datetime or the current datetime if not specified.

    Args:
        weeks (int): The number of weeks to subtract.
        days (int): The number of days to subtract.
        hours (int): The number of hours to subtract.
        minutes (int): The number of minutes to subtract.
        seconds (int): The number of seconds to subtract.
        datetime_given (datetime | None): The datetime to subtract from. If None, uses the current datetime.

    Returns:
        datetime: The resulting datetime after subtraction.
    """
    datetime_given = datetime_given or cls.get_datetime_now()
    return datetime_given - timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)

archipy.helpers.utils.datetime_utils.DatetimeUtils.get_datetime_after_given_datetime_or_now classmethod

get_datetime_after_given_datetime_or_now(
    weeks: int = 0,
    days: int = 0,
    hours: int = 0,
    minutes: int = 0,
    seconds: int = 0,
    datetime_given: datetime | None = None,
) -> datetime

Adds time to a given datetime or the current datetime if not specified.

Parameters:

Name Type Description Default
weeks int

The number of weeks to add.

0
days int

The number of days to add.

0
hours int

The number of hours to add.

0
minutes int

The number of minutes to add.

0
seconds int

The number of seconds to add.

0
datetime_given datetime | None

The datetime to add to. If None, uses the current datetime.

None

Returns:

Name Type Description
datetime datetime

The resulting datetime after addition.

Source code in archipy/helpers/utils/datetime_utils.py
@classmethod
def get_datetime_after_given_datetime_or_now(
    cls,
    weeks: int = 0,
    days: int = 0,
    hours: int = 0,
    minutes: int = 0,
    seconds: int = 0,
    datetime_given: datetime | None = None,
) -> datetime:
    """Adds time to a given datetime or the current datetime if not specified.

    Args:
        weeks (int): The number of weeks to add.
        days (int): The number of days to add.
        hours (int): The number of hours to add.
        minutes (int): The number of minutes to add.
        seconds (int): The number of seconds to add.
        datetime_given (datetime | None): The datetime to add to. If None, uses the current datetime.

    Returns:
        datetime: The resulting datetime after addition.
    """
    datetime_given = datetime_given or cls.get_datetime_now()
    return datetime_given + timedelta(weeks=weeks, days=days, hours=hours, minutes=minutes, seconds=seconds)

options: show_root_toc_entry: false heading_level: 3

String Utils

Utilities for string manipulation including slugification, truncation, random string generation, and HTML sanitization.

String manipulation utility helpers.

archipy.helpers.utils.string_utils.StringUtils

Bases: StringUtilsConstants

String utilities for text normalization, cleaning, and masking.

This class provides methods for handling Persian and Arabic text, including normalization, punctuation cleaning, number conversion, and masking of sensitive information like URLs, emails, and phone numbers.

Source code in archipy/helpers/utils/string_utils.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
class StringUtils(StringUtilsConstants):
    """String utilities for text normalization, cleaning, and masking.

    This class provides methods for handling Persian and Arabic text, including normalization,
    punctuation cleaning, number conversion, and masking of sensitive information like URLs,
    emails, and phone numbers.
    """

    @classmethod
    def remove_arabic_vowels(cls, text: str) -> str:
        """Removes Arabic vowels (tashkeel) from the text.

        Args:
            text (str): The input text containing Arabic vowels.

        Returns:
            str: The text with Arabic vowels removed.
        """
        return text.translate(cls.arabic_vowel_translate_table)

    @classmethod
    def normalize_persian_chars(cls, text: str) -> str:
        """Normalizes Persian characters to their standard forms.

        Args:
            text (str): The input text containing Persian characters.

        Returns:
            str: The text with Persian characters normalized.
        """
        text = text.translate(cls.alphabet_akoolad_alef_translate_table)
        text = text.translate(cls.alphabet_alef_translate_table)
        text = text.translate(cls.alphabet_be_translate_table)
        text = text.translate(cls.alphabet_pe_translate_table)
        text = text.translate(cls.alphabet_te_translate_table)
        text = text.translate(cls.alphabet_se_translate_table)
        text = text.translate(cls.alphabet_jim_translate_table)
        text = text.translate(cls.alphabet_che_translate_table)
        text = text.translate(cls.alphabet_he_translate_table)
        text = text.translate(cls.alphabet_khe_translate_table)
        text = text.translate(cls.alphabet_dal_translate_table)
        text = text.translate(cls.alphabet_zal_translate_table)
        text = text.translate(cls.alphabet_re_translate_table)
        text = text.translate(cls.alphabet_ze_translate_table)
        text = text.translate(cls.alphabet_zhe_translate_table)
        text = text.translate(cls.alphabet_sin_translate_table)
        text = text.translate(cls.alphabet_shin_translate_table)
        text = text.translate(cls.alphabet_sad_translate_table)
        text = text.translate(cls.alphabet_zad_translate_table)
        text = text.translate(cls.alphabet_ta_translate_table)
        text = text.translate(cls.alphabet_za_translate_table)
        text = text.translate(cls.alphabet_eyn_translate_table)
        text = text.translate(cls.alphabet_gheyn_translate_table)
        text = text.translate(cls.alphabet_fe_translate_table)
        text = text.translate(cls.alphabet_ghaf_translate_table)
        text = text.translate(cls.alphabet_kaf_translate_table)
        text = text.translate(cls.alphabet_gaf_translate_table)
        text = text.translate(cls.alphabet_lam_translate_table)
        text = text.translate(cls.alphabet_mim_translate_table)
        text = text.translate(cls.alphabet_nun_translate_table)
        text = text.translate(cls.alphabet_vav_translate_table)
        text = text.translate(cls.alphabet_ha_translate_table)
        return text.translate(cls.alphabet_ye_translate_table)

    @classmethod
    def normalize_punctuation(cls, text: str) -> str:
        """Normalizes punctuation marks in the text.

        ASCII ``,``, ``?``, ``;``, ``%`` and ``_`` are converted to their Persian forms only next to Persian text, so
        numbers (``1,000``), URLs, e-mails and identifiers are left intact.

        Args:
            text (str): The input text containing punctuation marks.

        Returns:
            str: The text with punctuation marks normalized.
        """
        for pattern, repl in cls.ascii_punctuation_patterns:
            text = pattern.sub(repl, text)
        text = text.translate(cls.punctuation_translate_table1)
        text = text.translate(cls.punctuation_translate_table2)
        text = text.translate(cls.punctuation_translate_table3)
        text = text.translate(cls.punctuation_translate_table4)
        text = text.translate(cls.punctuation_translate_table5)
        text = text.translate(cls.punctuation_translate_table7)
        text = text.translate(cls.punctuation_translate_table8)
        text = text.translate(cls.punctuation_translate_table9)
        text = text.translate(cls.punctuation_translate_table12)
        return text.translate(cls.punctuation_translate_table13)

    @classmethod
    def normalize_numbers(cls, text: str) -> str:
        """Normalizes numbers in the text to English format.

        Args:
            text (str): The input text containing numbers.

        Returns:
            str: The text with numbers normalized to English format.
        """
        text = text.translate(cls.number_zero_translate_table)
        text = text.translate(cls.number_one_translate_table)
        text = text.translate(cls.number_two_translate_table)
        text = text.translate(cls.number_three_translate_table)
        text = text.translate(cls.number_four_translate_table)
        text = text.translate(cls.number_five_translate_table)
        text = text.translate(cls.number_six_translate_table)
        text = text.translate(cls.number_seven_translate_table)
        text = text.translate(cls.number_eight_translate_table)
        return text.translate(cls.number_nine_translate_table)

    @classmethod
    def clean_spacing(cls, text: str, *, keep_zwnj: bool = False) -> str:
        """Cleans up spacing issues in the text, such as non-breaking spaces and zero-width non-joiners.

        Args:
            text (str): The input text with spacing issues.
            keep_zwnj (bool): Keep zero-width non-joiners, which Persian words such as "می‌خواهم" need. When
                `False` (the default) they are turned into spaces.

        Returns:
            str: The text with spacing cleaned up.
        """
        if not keep_zwnj:
            text = text.replace("\u200c", " ")  # ZWNJ
        text = text.replace("\xa0", " ")  # NBSP

        for pattern, repl in cls.character_refinement_patterns:
            text = pattern.sub(repl, text)

        return text

    @classmethod
    def normalize_punctuation_spacing(cls, text: str) -> str:
        """Applies proper spacing around punctuation marks.

        Args:
            text (str): The input text with punctuation spacing issues.

        Returns:
            str: The text with proper spacing around punctuation marks.
        """
        for pattern, repl in cls.punctuation_spacing_patterns:
            text = pattern.sub(repl, text)
        return text

    @classmethod
    def remove_punctuation_marks(cls, text: str) -> str:
        """Removes punctuation marks from the text.

        Args:
            text (str): The input text containing punctuation marks.

        Returns:
            str: The text with punctuation marks removed.
        """
        return text.translate(cls.punctuation_persian_marks_to_space_translate_table)

    @classmethod
    def mask_urls(cls, text: str, mask: str | None = None) -> str:
        """Masks URLs in the text with a specified mask.

        Args:
            text (str): The input text containing URLs.
            mask (str | None): The mask to replace URLs with. Defaults to "MASK_URL".

        Returns:
            str: The text with URLs masked.
        """
        mask = mask or "MASK_URL"
        return re_compile(r"https?://\S+|www\.\S+").sub(f" {mask} ", text)

    @classmethod
    def mask_emails(cls, text: str, mask: str | None = None) -> str:
        """Masks email addresses in the text with a specified mask.

        Args:
            text (str): The input text containing email addresses.
            mask (str | None): The mask to replace emails with. Defaults to "MASK_EMAIL".

        Returns:
            str: The text with email addresses masked.
        """
        mask = mask or "MASK_EMAIL"
        return re_compile(r"\S+@\S+\.\S+").sub(f" {mask} ", text)

    @classmethod
    def mask_phones(cls, text: str, mask: str | None = None) -> str:
        """Masks phone numbers in the text with a specified mask.

        Args:
            text (str): The input text containing phone numbers.
            mask (str | None): The mask to replace phone numbers with. Defaults to "MASK_PHONE".

        Returns:
            str: The text with phone numbers masked.
        """
        mask = mask or "MASK_PHONE"
        return re_compile(r"(?:\+98|0)?(?:\d{3}\s*?\d{3}\s*?\d{4})").sub(f" {mask} ", text)

    @classmethod
    def convert_english_number_to_persian(cls, text: str) -> str:
        """Converts English numbers to Persian numbers in the text.

        Args:
            text (str): The input text containing English numbers.

        Returns:
            str: The text with English numbers converted to Persian numbers.
        """
        return text.translate(ENGLISH_TO_PERSIAN_DIGITS_TABLE)

    @classmethod
    def convert_numbers_to_english(cls, text: str) -> str:
        """Converts Persian/Arabic numbers to English numbers in the text.

        Args:
            text (str): The input text containing Persian/Arabic numbers.

        Returns:
            str: The text with Persian/Arabic numbers converted to English numbers.
        """
        table = {
            1776: 48,  # 0
            1777: 49,  # 1
            1778: 50,  # 2
            1779: 51,  # 3
            1780: 52,  # 4
            1781: 53,  # 5
            1782: 54,  # 6
            1783: 55,  # 7
            1784: 56,  # 8
            1785: 57,  # 9
            1632: 48,  # 0
            1633: 49,  # 1
            1634: 50,  # 2
            1635: 51,  # 3
            1636: 52,  # 4
            1637: 53,  # 5
            1638: 54,  # 6
            1639: 55,  # 7
            1640: 56,  # 8
            1641: 57,  # 9
        }
        return text.translate(table)

    @classmethod
    def convert_add_3digit_delimiter(cls, value: int) -> str:
        """Adds thousand separators to numbers.

        Args:
            value (int): The number to format.

        Returns:
            str: The formatted number with thousand separators. Non-integer values are returned as `str(value)`.
        """
        return f"{value:,}" if isinstance(value, int) else str(value)

    @classmethod
    def remove_emoji(cls, text: str) -> str:
        """Removes emoji characters from the text.

        Args:
            text (str): The input text containing emojis.

        Returns:
            str: The text with emojis removed.
        """
        emoji_pattern = re.compile(
            r"["
            r"\U0001F600-\U0001F64F"  # emoticons
            r"\U0001F300-\U0001F5FF"  # symbols & pictographs
            r"\U0001F680-\U0001F6FF"  # transport & map symbols
            r"\U0001F1E0-\U0001F1FF"  # flags
            r"\U0001F900-\U0001F9FF"  # supplemental symbols and pictographs
            r"\U0001FA00-\U0001FA6F"  # symbols and pictographs extended-A
            r"\U00002600-\U000026FF"  # miscellaneous symbols (some are emojis)
            r"\U00002700-\U000027BF"  # dingbats (some are emojis)
            r"\U00002190-\U000021FF"  # arrows (some are emojis)
            r"]+",
            re.UNICODE,
        )
        return emoji_pattern.sub(r"", text)

    @classmethod
    def replace_currencies_with_mask(cls, text: str, mask: str | None = None) -> str:
        """Masks currency symbols in the text (the amounts next to them are not masked).

        Args:
            text (str): The input text containing currency symbols.
            mask (str | None): The mask to replace currency symbols with. Defaults to "MASK_CURRENCIES".

        Returns:
            str: The text with currency symbols masked.
        """
        mask = mask or "MASK_CURRENCIES"
        currency_pattern = re_compile(r"(zł|£|\$|₡|₦|¥|₩|₪|₫|€|₱|₲|₴|₹|﷼)+")
        return currency_pattern.sub(f" {mask} ", text)

    @classmethod
    def replace_numbers_with_mask(cls, text: str, mask: str | None = None) -> str:
        """Masks numbers in the text.

        Args:
            text (str): The input text containing numbers.
            mask (str | None): The mask to replace numbers with. Defaults to "MASK_NUMBERS".

        Returns:
            str: The text with numbers masked.
        """
        mask = mask or "MASK_NUMBERS"
        # A single pass, so a mask that itself contains digits (e.g. "NUM1") is never rewritten
        return re.sub("[0-9]+", lambda _: f" {mask} ", str(text))

    @classmethod
    def is_string_none_or_empty(cls, text: str | None) -> bool:
        """Checks if a string is `None` or empty (after stripping whitespace).

        Args:
            text (str | None): The input string to check.

        Returns:
            bool: `True` if the string is `None` or empty, `False` otherwise.
        """
        return text is None or (isinstance(text, str) and not text.strip())

    @classmethod
    def _apply_persian_text_normalizations(
        cls,
        text: str,
        *,
        remove_vowels: bool,
        normalize_persian_chars: bool,
        normalize_punctuation: bool,
        remove_punctuation: bool,
        normalize_numbers: bool,
    ) -> str:
        """Apply character, punctuation, and number normalizations."""
        if remove_vowels:
            text = cls.remove_arabic_vowels(text)
        if normalize_persian_chars:
            text = cls.normalize_persian_chars(text)
        if normalize_punctuation:
            text = cls.normalize_punctuation(text)
        if remove_punctuation:
            text = cls.remove_punctuation_marks(text)
        if normalize_numbers:
            text = cls.normalize_numbers(text)
        return text

    @classmethod
    def _apply_persian_text_masks(
        cls,
        text: str,
        *,
        mask_urls: bool,
        mask_emails: bool,
        mask_phones: bool,
        mask_currencies: bool,
        mask_all_numbers: bool,
        url_mask: str | None,
        email_mask: str | None,
        phone_mask: str | None,
        currency_mask: str | None,
        number_mask: str | None,
    ) -> str:
        """Apply optional masking transforms to Persian text."""
        if mask_urls:
            text = cls.mask_urls(text, mask=url_mask)
        if mask_emails:
            text = cls.mask_emails(text, mask=email_mask)
        if mask_phones:
            text = cls.mask_phones(text, mask=phone_mask)
        if mask_currencies:
            text = cls.replace_currencies_with_mask(text, mask=currency_mask)
        if mask_all_numbers:
            text = cls.replace_numbers_with_mask(text, mask=number_mask)
        return text

    @classmethod
    def normalize_persian_text(
        cls,
        text: str,
        *,
        remove_vowels: bool = True,
        normalize_punctuation: bool = True,
        normalize_numbers: bool = True,
        normalize_persian_chars: bool = True,
        mask_urls: bool = False,
        mask_emails: bool = False,
        mask_phones: bool = False,
        mask_currencies: bool = False,
        mask_all_numbers: bool = False,
        remove_emojis: bool = False,
        url_mask: str | None = None,
        email_mask: str | None = None,
        phone_mask: str | None = None,
        currency_mask: str | None = None,
        number_mask: str | None = None,
        clean_spacing: bool = True,
        remove_punctuation: bool = False,
        normalize_punctuation_spacing: bool = False,
        keep_zwnj: bool = False,
    ) -> str:
        """Normalizes Persian text with configurable options.

        Args:
            text (str): The input text to normalize.
            remove_vowels (bool): Whether to remove Arabic vowels. Defaults to `True`.
            normalize_punctuation (bool): Whether to normalize punctuation marks. Defaults to `True`.
            normalize_numbers (bool): Whether to normalize numbers to English format. Defaults to `True`.
            normalize_persian_chars (bool): Whether to normalize Persian characters. Defaults to `True`.
            mask_urls (bool): Whether to mask URLs. Defaults to `False`.
            mask_emails (bool): Whether to mask email addresses. Defaults to `False`.
            mask_phones (bool): Whether to mask phone numbers. Defaults to `False`.
            mask_currencies (bool): Whether to mask currency symbols and amounts. Defaults to `False`.
            mask_all_numbers (bool): Whether to mask all numbers. Defaults to `False`.
            remove_emojis (bool): Whether to remove emojis. Defaults to `False`.
            url_mask (str | None): The mask to replace URLs with. Defaults to `None`.
            email_mask (str | None): The mask to replace email addresses with. Defaults to `None`.
            phone_mask (str | None): The mask to replace phone numbers with. Defaults to `None`.
            currency_mask (str | None): The mask to replace currency symbols and amounts with. Defaults to `None`.
            number_mask (str | None): The mask to replace numbers with. Defaults to `None`.
            clean_spacing (bool): Whether to clean up spacing issues. Defaults to `True`.
            remove_punctuation (bool): Whether to remove punctuation marks. Defaults to `False`.
            normalize_punctuation_spacing (bool): Whether to apply proper spacing around
                punctuation marks. Defaults to `False`.
            keep_zwnj (bool): Whether to keep zero-width non-joiners when cleaning spacing instead of turning them
                into spaces. Defaults to `False`.

        Returns:
            str: The normalized text.
        """
        if not text:
            return text

        if remove_emojis:
            text = cls.remove_emoji(text)

        text = cls._apply_persian_text_normalizations(
            text,
            remove_vowels=remove_vowels,
            normalize_persian_chars=normalize_persian_chars,
            normalize_punctuation=normalize_punctuation,
            remove_punctuation=remove_punctuation,
            normalize_numbers=normalize_numbers,
        )

        text = cls._apply_persian_text_masks(
            text,
            mask_urls=mask_urls,
            mask_emails=mask_emails,
            mask_phones=mask_phones,
            mask_currencies=mask_currencies,
            mask_all_numbers=mask_all_numbers,
            url_mask=url_mask,
            email_mask=email_mask,
            phone_mask=phone_mask,
            currency_mask=currency_mask,
            number_mask=number_mask,
        )

        if clean_spacing:
            text = cls.clean_spacing(text, keep_zwnj=keep_zwnj)
        if normalize_punctuation_spacing:
            text = cls.normalize_punctuation_spacing(text)

        return text.strip()

    @classmethod
    def snake_to_camel_case(cls, text: str) -> str:
        """Converts snake_case to camelCase.

        Args:
            text (str): The input text in snake_case format.

        Returns:
            str: The text converted to camelCase format.
        """
        if cls.is_string_none_or_empty(text):
            return text

        components = text.split("_")
        # First component remains lowercase, the rest get capitalized
        return components[0] + "".join(x[:1].upper() + x[1:] for x in components[1:])

    @classmethod
    def camel_to_snake_case(cls, text: str) -> str:
        """Converts camelCase to snake_case.

        Args:
            text (str): The input text in camelCase format.

        Returns:
            str: The text converted to snake_case format.
        """
        if cls.is_string_none_or_empty(text):
            return text

        # Add underscore before each capital letter and convert to lowercase
        s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
        return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()

archipy.helpers.utils.string_utils.StringUtils.arabic_vowel_translate_table class-attribute instance-attribute

arabic_vowel_translate_table = str.maketrans(
    dict.fromkeys("ًٌَُِّْٓءٍٰۖۗۘۙۚۛ", "")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_akoolad_alef_translate_table class-attribute instance-attribute

alphabet_akoolad_alef_translate_table = str.maketrans(
    dict.fromkeys("ﺁ", "آ")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_alef_translate_table class-attribute instance-attribute

alphabet_alef_translate_table = str.maketrans(
    dict.fromkeys("ﺎٲٱإﺍأٵٳ", "ا")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_be_translate_table class-attribute instance-attribute

alphabet_be_translate_table = str.maketrans(
    dict.fromkeys("ﺐﺏﺑٻٮ", "ب")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_pe_translate_table class-attribute instance-attribute

alphabet_pe_translate_table = str.maketrans(
    dict.fromkeys("ﭖﭗﭙﺒﭘڀݐݒݕ", "پ")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_te_translate_table class-attribute instance-attribute

alphabet_te_translate_table = str.maketrans(
    dict.fromkeys("ﭡٺٹﭞٿټﺕﺗﺖﺘݓ", "ت")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_se_translate_table class-attribute instance-attribute

alphabet_se_translate_table = str.maketrans(
    dict.fromkeys("ﺙﺛٽﺚﺜ", "ث")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_jim_translate_table class-attribute instance-attribute

alphabet_jim_translate_table = str.maketrans(
    dict.fromkeys("ﺝﺠﺟﺞۚ", "ج")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_che_translate_table class-attribute instance-attribute

alphabet_che_translate_table = str.maketrans(
    dict.fromkeys("ڃﭽﭼڇڄݘڿﭻ", "چ")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_he_translate_table class-attribute instance-attribute

alphabet_he_translate_table = str.maketrans(
    dict.fromkeys("ﺢﺤڅځﺣﺡ", "ح")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_khe_translate_table class-attribute instance-attribute

alphabet_khe_translate_table = str.maketrans(
    dict.fromkeys("ﺥﺦﺨﺧڂݗ", "خ")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_dal_translate_table class-attribute instance-attribute

alphabet_dal_translate_table = str.maketrans(
    dict.fromkeys("ډﺪﺩڊڈڍܥ", "د")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_zal_translate_table class-attribute instance-attribute

alphabet_zal_translate_table = str.maketrans(
    dict.fromkeys("ﺫﺬڐڏڎڌ", "ذ")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_re_translate_table class-attribute instance-attribute

alphabet_re_translate_table = str.maketrans(
    dict.fromkeys("ڗڒڑڕﺭﺮږڔړڒڑۯ", "ر")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_ze_translate_table class-attribute instance-attribute

alphabet_ze_translate_table = str.maketrans(
    dict.fromkeys("ﺰﺯ", "ز")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_zhe_translate_table class-attribute instance-attribute

alphabet_zhe_translate_table = str.maketrans(
    dict.fromkeys("ﮊڙﮋ", "ژ")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_sin_translate_table class-attribute instance-attribute

alphabet_sin_translate_table = str.maketrans(
    dict.fromkeys("ݭݜﺱﺲﺴﺳڛښۣݾݽ", "س")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_shin_translate_table class-attribute instance-attribute

alphabet_shin_translate_table = str.maketrans(
    dict.fromkeys("ﺵﺶﺸﺷڜۺ", "ش")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_sad_translate_table class-attribute instance-attribute

alphabet_sad_translate_table = str.maketrans(
    dict.fromkeys("ﺹﺺﺼﺻڝ", "ص")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_zad_translate_table class-attribute instance-attribute

alphabet_zad_translate_table = str.maketrans(
    dict.fromkeys("ﺽﺾﺿﻀۻڞ", "ض")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_ta_translate_table class-attribute instance-attribute

alphabet_ta_translate_table = str.maketrans(
    dict.fromkeys("ﻁﻂﻃﻄ", "ط")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_za_translate_table class-attribute instance-attribute

alphabet_za_translate_table = str.maketrans(
    dict.fromkeys("ﻆﻇﻈڟﻅ", "ظ")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_eyn_translate_table class-attribute instance-attribute

alphabet_eyn_translate_table = str.maketrans(
    dict.fromkeys("ڠﻉﻊﻋﻌ", "ع")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_gheyn_translate_table class-attribute instance-attribute

alphabet_gheyn_translate_table = str.maketrans(
    dict.fromkeys("ﻎۼﻍﻐﻏݝݞݟ", "غ")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_fe_translate_table class-attribute instance-attribute

alphabet_fe_translate_table = str.maketrans(
    dict.fromkeys("ﻒﻑﻔﻓڡڥڦڤڣڢ", "ف")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_ghaf_translate_table class-attribute instance-attribute

alphabet_ghaf_translate_table = str.maketrans(
    dict.fromkeys("ﻕﻖﻗڧڨﻘ", "ق")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_kaf_translate_table class-attribute instance-attribute

alphabet_kaf_translate_table = str.maketrans(
    dict.fromkeys("ڭﻚﮎﻜﮏګﻛﮑﮐڪكݢݣݤڬڮݿﻙ", "ک")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_gaf_translate_table class-attribute instance-attribute

alphabet_gaf_translate_table = str.maketrans(
    dict.fromkeys("ﮚﮒﮓﮕﮔڱڰڲڳڴ", "گ")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_lam_translate_table class-attribute instance-attribute

alphabet_lam_translate_table = str.maketrans(
    dict.fromkeys("ﻟﻝﻞﻠݪڷڸڶڵ", "ل")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_mim_translate_table class-attribute instance-attribute

alphabet_mim_translate_table = str.maketrans(
    dict.fromkeys("ﻡﻤﻢﻣݦݥ", "م")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_nun_translate_table class-attribute instance-attribute

alphabet_nun_translate_table = str.maketrans(
    dict.fromkeys("ڼﻦﻥﻧﻨݩݨݧڻڽںڹ", "ن")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_vav_translate_table class-attribute instance-attribute

alphabet_vav_translate_table = str.maketrans(
    dict.fromkeys("ވﯙۈۋﺆۊۇۏۅۉﻭﻮؤۆۄ", "و")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_ha_translate_table class-attribute instance-attribute

alphabet_ha_translate_table = str.maketrans(
    dict.fromkeys("ﺓﮭﺔﻬھﻩﻫﻪۀەةہܣܤܝ", "ه")
)

archipy.helpers.utils.string_utils.StringUtils.alphabet_ye_translate_table class-attribute instance-attribute

alphabet_ye_translate_table = str.maketrans(
    dict.fromkeys("ﯨﭛﻯۍﻰﻱﻲﻳﻴﯼېﯽﯾﯿێےىيٸۑؽؾؿﺉﺋﺌ", "ی")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table1 class-attribute instance-attribute

punctuation_translate_table1 = str.maketrans(
    dict.fromkeys("¬", " ")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table2 class-attribute instance-attribute

punctuation_translate_table2 = str.maketrans(
    dict.fromkeys("•·●·・∙。ⴰ", ".")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table3 class-attribute instance-attribute

punctuation_translate_table3 = str.maketrans(
    dict.fromkeys("٬٫‚,", "،")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table4 class-attribute instance-attribute

punctuation_translate_table4 = str.maketrans(
    dict.fromkeys("ʕ⁉�", "؟")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table5 class-attribute instance-attribute

punctuation_translate_table5 = str.maketrans(
    dict.fromkeys("‼❕", "!")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table7 class-attribute instance-attribute

punctuation_translate_table7 = str.maketrans(
    dict.fromkeys("-━−‐‑–—─−ー⁃", "-")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table8 class-attribute instance-attribute

punctuation_translate_table8 = str.maketrans(
    dict.fromkeys("‹《﴾", "«")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table9 class-attribute instance-attribute

punctuation_translate_table9 = str.maketrans(
    dict.fromkeys("›》﴿", "»")
)

archipy.helpers.utils.string_utils.StringUtils.ascii_punctuation_patterns class-attribute instance-attribute

ascii_punctuation_patterns: list[
    tuple[Pattern[str], str]
] = compile_patterns(
    [
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff]),|,(?=\\s*[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])",
            "،",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff\\u061f])\\?",
            "؟",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff\\u061b]);",
            "؛",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])%",
            "٪",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])_(?=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])",
            "ـ",
        ),
    ]
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table12 class-attribute instance-attribute

punctuation_translate_table12 = str.maketrans(
    dict.fromkeys("ˈ‘’“”", "'")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table13 class-attribute instance-attribute

punctuation_translate_table13 = str.maketrans(
    dict.fromkeys(":", ":")
)

archipy.helpers.utils.string_utils.StringUtils.character_refinement_patterns class-attribute instance-attribute

character_refinement_patterns: list[
    tuple[Pattern[str], str]
] = compile_patterns(
    [(" +", " "), ("\\n\\n+", "\n"), (" ?\\.\\.\\.", " …")]
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_after class-attribute instance-attribute

punctuation_after = '\\.:!،؛؟»\\]\\)\\}'

archipy.helpers.utils.string_utils.StringUtils.punctuation_before class-attribute instance-attribute

punctuation_before = '«\\[\\(\\{'

archipy.helpers.utils.string_utils.StringUtils.punctuation_spacing_patterns class-attribute instance-attribute

punctuation_spacing_patterns = compile_patterns(
    [
        (f" ([{punctuation_after}])", "\\1"),
        (f"([{punctuation_before}]) ", "\\1"),
        (
            f"(?<![A-Za-z0-9])([{punctuation_after[:3]}])([^ {punctuation_after}"
            + "\\d])",
            "\\1 \\2",
        ),
        (
            f"([{punctuation_after[3:]}])([^ {punctuation_after}])",
            "\\1 \\2",
        ),
        (
            f"([^ {punctuation_before}])([{punctuation_before}])",
            "\\1 \\2",
        ),
    ]
)

archipy.helpers.utils.string_utils.StringUtils.number_zero_translate_table class-attribute instance-attribute

number_zero_translate_table = str.maketrans(
    dict.fromkeys("۰٠", "0")
)

archipy.helpers.utils.string_utils.StringUtils.number_one_translate_table class-attribute instance-attribute

number_one_translate_table = str.maketrans(
    dict.fromkeys("۱١", "1")
)

archipy.helpers.utils.string_utils.StringUtils.number_two_translate_table class-attribute instance-attribute

number_two_translate_table = str.maketrans(
    dict.fromkeys("۲٢", "2")
)

archipy.helpers.utils.string_utils.StringUtils.number_three_translate_table class-attribute instance-attribute

number_three_translate_table = str.maketrans(
    dict.fromkeys("۳٣", "3")
)

archipy.helpers.utils.string_utils.StringUtils.number_four_translate_table class-attribute instance-attribute

number_four_translate_table = str.maketrans(
    dict.fromkeys("۴٤", "4")
)

archipy.helpers.utils.string_utils.StringUtils.number_five_translate_table class-attribute instance-attribute

number_five_translate_table = str.maketrans(
    dict.fromkeys("۵٥", "5")
)

archipy.helpers.utils.string_utils.StringUtils.number_six_translate_table class-attribute instance-attribute

number_six_translate_table = str.maketrans(
    dict.fromkeys("۶٦", "6")
)

archipy.helpers.utils.string_utils.StringUtils.number_seven_translate_table class-attribute instance-attribute

number_seven_translate_table = str.maketrans(
    dict.fromkeys("۷٧", "7")
)

archipy.helpers.utils.string_utils.StringUtils.number_eight_translate_table class-attribute instance-attribute

number_eight_translate_table = str.maketrans(
    dict.fromkeys("۸٨", "8")
)

archipy.helpers.utils.string_utils.StringUtils.number_nine_translate_table class-attribute instance-attribute

number_nine_translate_table = str.maketrans(
    dict.fromkeys("۹٩", "9")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_persian_marks_to_space_translate_table class-attribute instance-attribute

punctuation_persian_marks_to_space_translate_table = (
    str.maketrans(
        dict.fromkeys(".:!،؛؟»])}«[({-ـ٪!'\"#+/,?;%_", " ")
    )
)

archipy.helpers.utils.string_utils.StringUtils.remove_arabic_vowels classmethod

remove_arabic_vowels(text: str) -> str

Removes Arabic vowels (tashkeel) from the text.

Parameters:

Name Type Description Default
text str

The input text containing Arabic vowels.

required

Returns:

Name Type Description
str str

The text with Arabic vowels removed.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def remove_arabic_vowels(cls, text: str) -> str:
    """Removes Arabic vowels (tashkeel) from the text.

    Args:
        text (str): The input text containing Arabic vowels.

    Returns:
        str: The text with Arabic vowels removed.
    """
    return text.translate(cls.arabic_vowel_translate_table)

archipy.helpers.utils.string_utils.StringUtils.normalize_persian_chars classmethod

normalize_persian_chars(text: str) -> str

Normalizes Persian characters to their standard forms.

Parameters:

Name Type Description Default
text str

The input text containing Persian characters.

required

Returns:

Name Type Description
str str

The text with Persian characters normalized.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_persian_chars(cls, text: str) -> str:
    """Normalizes Persian characters to their standard forms.

    Args:
        text (str): The input text containing Persian characters.

    Returns:
        str: The text with Persian characters normalized.
    """
    text = text.translate(cls.alphabet_akoolad_alef_translate_table)
    text = text.translate(cls.alphabet_alef_translate_table)
    text = text.translate(cls.alphabet_be_translate_table)
    text = text.translate(cls.alphabet_pe_translate_table)
    text = text.translate(cls.alphabet_te_translate_table)
    text = text.translate(cls.alphabet_se_translate_table)
    text = text.translate(cls.alphabet_jim_translate_table)
    text = text.translate(cls.alphabet_che_translate_table)
    text = text.translate(cls.alphabet_he_translate_table)
    text = text.translate(cls.alphabet_khe_translate_table)
    text = text.translate(cls.alphabet_dal_translate_table)
    text = text.translate(cls.alphabet_zal_translate_table)
    text = text.translate(cls.alphabet_re_translate_table)
    text = text.translate(cls.alphabet_ze_translate_table)
    text = text.translate(cls.alphabet_zhe_translate_table)
    text = text.translate(cls.alphabet_sin_translate_table)
    text = text.translate(cls.alphabet_shin_translate_table)
    text = text.translate(cls.alphabet_sad_translate_table)
    text = text.translate(cls.alphabet_zad_translate_table)
    text = text.translate(cls.alphabet_ta_translate_table)
    text = text.translate(cls.alphabet_za_translate_table)
    text = text.translate(cls.alphabet_eyn_translate_table)
    text = text.translate(cls.alphabet_gheyn_translate_table)
    text = text.translate(cls.alphabet_fe_translate_table)
    text = text.translate(cls.alphabet_ghaf_translate_table)
    text = text.translate(cls.alphabet_kaf_translate_table)
    text = text.translate(cls.alphabet_gaf_translate_table)
    text = text.translate(cls.alphabet_lam_translate_table)
    text = text.translate(cls.alphabet_mim_translate_table)
    text = text.translate(cls.alphabet_nun_translate_table)
    text = text.translate(cls.alphabet_vav_translate_table)
    text = text.translate(cls.alphabet_ha_translate_table)
    return text.translate(cls.alphabet_ye_translate_table)

archipy.helpers.utils.string_utils.StringUtils.normalize_punctuation classmethod

normalize_punctuation(text: str) -> str

Normalizes punctuation marks in the text.

ASCII ,, ?, ;, % and _ are converted to their Persian forms only next to Persian text, so numbers (1,000), URLs, e-mails and identifiers are left intact.

Parameters:

Name Type Description Default
text str

The input text containing punctuation marks.

required

Returns:

Name Type Description
str str

The text with punctuation marks normalized.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_punctuation(cls, text: str) -> str:
    """Normalizes punctuation marks in the text.

    ASCII ``,``, ``?``, ``;``, ``%`` and ``_`` are converted to their Persian forms only next to Persian text, so
    numbers (``1,000``), URLs, e-mails and identifiers are left intact.

    Args:
        text (str): The input text containing punctuation marks.

    Returns:
        str: The text with punctuation marks normalized.
    """
    for pattern, repl in cls.ascii_punctuation_patterns:
        text = pattern.sub(repl, text)
    text = text.translate(cls.punctuation_translate_table1)
    text = text.translate(cls.punctuation_translate_table2)
    text = text.translate(cls.punctuation_translate_table3)
    text = text.translate(cls.punctuation_translate_table4)
    text = text.translate(cls.punctuation_translate_table5)
    text = text.translate(cls.punctuation_translate_table7)
    text = text.translate(cls.punctuation_translate_table8)
    text = text.translate(cls.punctuation_translate_table9)
    text = text.translate(cls.punctuation_translate_table12)
    return text.translate(cls.punctuation_translate_table13)

archipy.helpers.utils.string_utils.StringUtils.normalize_numbers classmethod

normalize_numbers(text: str) -> str

Normalizes numbers in the text to English format.

Parameters:

Name Type Description Default
text str

The input text containing numbers.

required

Returns:

Name Type Description
str str

The text with numbers normalized to English format.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_numbers(cls, text: str) -> str:
    """Normalizes numbers in the text to English format.

    Args:
        text (str): The input text containing numbers.

    Returns:
        str: The text with numbers normalized to English format.
    """
    text = text.translate(cls.number_zero_translate_table)
    text = text.translate(cls.number_one_translate_table)
    text = text.translate(cls.number_two_translate_table)
    text = text.translate(cls.number_three_translate_table)
    text = text.translate(cls.number_four_translate_table)
    text = text.translate(cls.number_five_translate_table)
    text = text.translate(cls.number_six_translate_table)
    text = text.translate(cls.number_seven_translate_table)
    text = text.translate(cls.number_eight_translate_table)
    return text.translate(cls.number_nine_translate_table)

archipy.helpers.utils.string_utils.StringUtils.clean_spacing classmethod

clean_spacing(text: str, *, keep_zwnj: bool = False) -> str

Cleans up spacing issues in the text, such as non-breaking spaces and zero-width non-joiners.

Parameters:

Name Type Description Default
text str

The input text with spacing issues.

required
keep_zwnj bool

Keep zero-width non-joiners, which Persian words such as "می‌خواهم" need. When False (the default) they are turned into spaces.

False

Returns:

Name Type Description
str str

The text with spacing cleaned up.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def clean_spacing(cls, text: str, *, keep_zwnj: bool = False) -> str:
    """Cleans up spacing issues in the text, such as non-breaking spaces and zero-width non-joiners.

    Args:
        text (str): The input text with spacing issues.
        keep_zwnj (bool): Keep zero-width non-joiners, which Persian words such as "می‌خواهم" need. When
            `False` (the default) they are turned into spaces.

    Returns:
        str: The text with spacing cleaned up.
    """
    if not keep_zwnj:
        text = text.replace("\u200c", " ")  # ZWNJ
    text = text.replace("\xa0", " ")  # NBSP

    for pattern, repl in cls.character_refinement_patterns:
        text = pattern.sub(repl, text)

    return text

archipy.helpers.utils.string_utils.StringUtils.normalize_punctuation_spacing classmethod

normalize_punctuation_spacing(text: str) -> str

Applies proper spacing around punctuation marks.

Parameters:

Name Type Description Default
text str

The input text with punctuation spacing issues.

required

Returns:

Name Type Description
str str

The text with proper spacing around punctuation marks.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_punctuation_spacing(cls, text: str) -> str:
    """Applies proper spacing around punctuation marks.

    Args:
        text (str): The input text with punctuation spacing issues.

    Returns:
        str: The text with proper spacing around punctuation marks.
    """
    for pattern, repl in cls.punctuation_spacing_patterns:
        text = pattern.sub(repl, text)
    return text

archipy.helpers.utils.string_utils.StringUtils.remove_punctuation_marks classmethod

remove_punctuation_marks(text: str) -> str

Removes punctuation marks from the text.

Parameters:

Name Type Description Default
text str

The input text containing punctuation marks.

required

Returns:

Name Type Description
str str

The text with punctuation marks removed.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def remove_punctuation_marks(cls, text: str) -> str:
    """Removes punctuation marks from the text.

    Args:
        text (str): The input text containing punctuation marks.

    Returns:
        str: The text with punctuation marks removed.
    """
    return text.translate(cls.punctuation_persian_marks_to_space_translate_table)

archipy.helpers.utils.string_utils.StringUtils.mask_urls classmethod

mask_urls(text: str, mask: str | None = None) -> str

Masks URLs in the text with a specified mask.

Parameters:

Name Type Description Default
text str

The input text containing URLs.

required
mask str | None

The mask to replace URLs with. Defaults to "MASK_URL".

None

Returns:

Name Type Description
str str

The text with URLs masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def mask_urls(cls, text: str, mask: str | None = None) -> str:
    """Masks URLs in the text with a specified mask.

    Args:
        text (str): The input text containing URLs.
        mask (str | None): The mask to replace URLs with. Defaults to "MASK_URL".

    Returns:
        str: The text with URLs masked.
    """
    mask = mask or "MASK_URL"
    return re_compile(r"https?://\S+|www\.\S+").sub(f" {mask} ", text)

archipy.helpers.utils.string_utils.StringUtils.mask_emails classmethod

mask_emails(text: str, mask: str | None = None) -> str

Masks email addresses in the text with a specified mask.

Parameters:

Name Type Description Default
text str

The input text containing email addresses.

required
mask str | None

The mask to replace emails with. Defaults to "MASK_EMAIL".

None

Returns:

Name Type Description
str str

The text with email addresses masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def mask_emails(cls, text: str, mask: str | None = None) -> str:
    """Masks email addresses in the text with a specified mask.

    Args:
        text (str): The input text containing email addresses.
        mask (str | None): The mask to replace emails with. Defaults to "MASK_EMAIL".

    Returns:
        str: The text with email addresses masked.
    """
    mask = mask or "MASK_EMAIL"
    return re_compile(r"\S+@\S+\.\S+").sub(f" {mask} ", text)

archipy.helpers.utils.string_utils.StringUtils.mask_phones classmethod

mask_phones(text: str, mask: str | None = None) -> str

Masks phone numbers in the text with a specified mask.

Parameters:

Name Type Description Default
text str

The input text containing phone numbers.

required
mask str | None

The mask to replace phone numbers with. Defaults to "MASK_PHONE".

None

Returns:

Name Type Description
str str

The text with phone numbers masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def mask_phones(cls, text: str, mask: str | None = None) -> str:
    """Masks phone numbers in the text with a specified mask.

    Args:
        text (str): The input text containing phone numbers.
        mask (str | None): The mask to replace phone numbers with. Defaults to "MASK_PHONE".

    Returns:
        str: The text with phone numbers masked.
    """
    mask = mask or "MASK_PHONE"
    return re_compile(r"(?:\+98|0)?(?:\d{3}\s*?\d{3}\s*?\d{4})").sub(f" {mask} ", text)

archipy.helpers.utils.string_utils.StringUtils.convert_english_number_to_persian classmethod

convert_english_number_to_persian(text: str) -> str

Converts English numbers to Persian numbers in the text.

Parameters:

Name Type Description Default
text str

The input text containing English numbers.

required

Returns:

Name Type Description
str str

The text with English numbers converted to Persian numbers.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def convert_english_number_to_persian(cls, text: str) -> str:
    """Converts English numbers to Persian numbers in the text.

    Args:
        text (str): The input text containing English numbers.

    Returns:
        str: The text with English numbers converted to Persian numbers.
    """
    return text.translate(ENGLISH_TO_PERSIAN_DIGITS_TABLE)

archipy.helpers.utils.string_utils.StringUtils.convert_numbers_to_english classmethod

convert_numbers_to_english(text: str) -> str

Converts Persian/Arabic numbers to English numbers in the text.

Parameters:

Name Type Description Default
text str

The input text containing Persian/Arabic numbers.

required

Returns:

Name Type Description
str str

The text with Persian/Arabic numbers converted to English numbers.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def convert_numbers_to_english(cls, text: str) -> str:
    """Converts Persian/Arabic numbers to English numbers in the text.

    Args:
        text (str): The input text containing Persian/Arabic numbers.

    Returns:
        str: The text with Persian/Arabic numbers converted to English numbers.
    """
    table = {
        1776: 48,  # 0
        1777: 49,  # 1
        1778: 50,  # 2
        1779: 51,  # 3
        1780: 52,  # 4
        1781: 53,  # 5
        1782: 54,  # 6
        1783: 55,  # 7
        1784: 56,  # 8
        1785: 57,  # 9
        1632: 48,  # 0
        1633: 49,  # 1
        1634: 50,  # 2
        1635: 51,  # 3
        1636: 52,  # 4
        1637: 53,  # 5
        1638: 54,  # 6
        1639: 55,  # 7
        1640: 56,  # 8
        1641: 57,  # 9
    }
    return text.translate(table)

archipy.helpers.utils.string_utils.StringUtils.convert_add_3digit_delimiter classmethod

convert_add_3digit_delimiter(value: int) -> str

Adds thousand separators to numbers.

Parameters:

Name Type Description Default
value int

The number to format.

required

Returns:

Name Type Description
str str

The formatted number with thousand separators. Non-integer values are returned as str(value).

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def convert_add_3digit_delimiter(cls, value: int) -> str:
    """Adds thousand separators to numbers.

    Args:
        value (int): The number to format.

    Returns:
        str: The formatted number with thousand separators. Non-integer values are returned as `str(value)`.
    """
    return f"{value:,}" if isinstance(value, int) else str(value)

archipy.helpers.utils.string_utils.StringUtils.remove_emoji classmethod

remove_emoji(text: str) -> str

Removes emoji characters from the text.

Parameters:

Name Type Description Default
text str

The input text containing emojis.

required

Returns:

Name Type Description
str str

The text with emojis removed.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def remove_emoji(cls, text: str) -> str:
    """Removes emoji characters from the text.

    Args:
        text (str): The input text containing emojis.

    Returns:
        str: The text with emojis removed.
    """
    emoji_pattern = re.compile(
        r"["
        r"\U0001F600-\U0001F64F"  # emoticons
        r"\U0001F300-\U0001F5FF"  # symbols & pictographs
        r"\U0001F680-\U0001F6FF"  # transport & map symbols
        r"\U0001F1E0-\U0001F1FF"  # flags
        r"\U0001F900-\U0001F9FF"  # supplemental symbols and pictographs
        r"\U0001FA00-\U0001FA6F"  # symbols and pictographs extended-A
        r"\U00002600-\U000026FF"  # miscellaneous symbols (some are emojis)
        r"\U00002700-\U000027BF"  # dingbats (some are emojis)
        r"\U00002190-\U000021FF"  # arrows (some are emojis)
        r"]+",
        re.UNICODE,
    )
    return emoji_pattern.sub(r"", text)

archipy.helpers.utils.string_utils.StringUtils.replace_currencies_with_mask classmethod

replace_currencies_with_mask(
    text: str, mask: str | None = None
) -> str

Masks currency symbols in the text (the amounts next to them are not masked).

Parameters:

Name Type Description Default
text str

The input text containing currency symbols.

required
mask str | None

The mask to replace currency symbols with. Defaults to "MASK_CURRENCIES".

None

Returns:

Name Type Description
str str

The text with currency symbols masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def replace_currencies_with_mask(cls, text: str, mask: str | None = None) -> str:
    """Masks currency symbols in the text (the amounts next to them are not masked).

    Args:
        text (str): The input text containing currency symbols.
        mask (str | None): The mask to replace currency symbols with. Defaults to "MASK_CURRENCIES".

    Returns:
        str: The text with currency symbols masked.
    """
    mask = mask or "MASK_CURRENCIES"
    currency_pattern = re_compile(r"(zł|£|\$|₡|₦|¥|₩|₪|₫|€|₱|₲|₴|₹|﷼)+")
    return currency_pattern.sub(f" {mask} ", text)

archipy.helpers.utils.string_utils.StringUtils.replace_numbers_with_mask classmethod

replace_numbers_with_mask(
    text: str, mask: str | None = None
) -> str

Masks numbers in the text.

Parameters:

Name Type Description Default
text str

The input text containing numbers.

required
mask str | None

The mask to replace numbers with. Defaults to "MASK_NUMBERS".

None

Returns:

Name Type Description
str str

The text with numbers masked.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def replace_numbers_with_mask(cls, text: str, mask: str | None = None) -> str:
    """Masks numbers in the text.

    Args:
        text (str): The input text containing numbers.
        mask (str | None): The mask to replace numbers with. Defaults to "MASK_NUMBERS".

    Returns:
        str: The text with numbers masked.
    """
    mask = mask or "MASK_NUMBERS"
    # A single pass, so a mask that itself contains digits (e.g. "NUM1") is never rewritten
    return re.sub("[0-9]+", lambda _: f" {mask} ", str(text))

archipy.helpers.utils.string_utils.StringUtils.is_string_none_or_empty classmethod

is_string_none_or_empty(text: str | None) -> bool

Checks if a string is None or empty (after stripping whitespace).

Parameters:

Name Type Description Default
text str | None

The input string to check.

required

Returns:

Name Type Description
bool bool

True if the string is None or empty, False otherwise.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def is_string_none_or_empty(cls, text: str | None) -> bool:
    """Checks if a string is `None` or empty (after stripping whitespace).

    Args:
        text (str | None): The input string to check.

    Returns:
        bool: `True` if the string is `None` or empty, `False` otherwise.
    """
    return text is None or (isinstance(text, str) and not text.strip())

archipy.helpers.utils.string_utils.StringUtils.normalize_persian_text classmethod

normalize_persian_text(
    text: str,
    *,
    remove_vowels: bool = True,
    normalize_punctuation: bool = True,
    normalize_numbers: bool = True,
    normalize_persian_chars: bool = True,
    mask_urls: bool = False,
    mask_emails: bool = False,
    mask_phones: bool = False,
    mask_currencies: bool = False,
    mask_all_numbers: bool = False,
    remove_emojis: bool = False,
    url_mask: str | None = None,
    email_mask: str | None = None,
    phone_mask: str | None = None,
    currency_mask: str | None = None,
    number_mask: str | None = None,
    clean_spacing: bool = True,
    remove_punctuation: bool = False,
    normalize_punctuation_spacing: bool = False,
    keep_zwnj: bool = False,
) -> str

Normalizes Persian text with configurable options.

Parameters:

Name Type Description Default
text str

The input text to normalize.

required
remove_vowels bool

Whether to remove Arabic vowels. Defaults to True.

True
normalize_punctuation bool

Whether to normalize punctuation marks. Defaults to True.

True
normalize_numbers bool

Whether to normalize numbers to English format. Defaults to True.

True
normalize_persian_chars bool

Whether to normalize Persian characters. Defaults to True.

True
mask_urls bool

Whether to mask URLs. Defaults to False.

False
mask_emails bool

Whether to mask email addresses. Defaults to False.

False
mask_phones bool

Whether to mask phone numbers. Defaults to False.

False
mask_currencies bool

Whether to mask currency symbols and amounts. Defaults to False.

False
mask_all_numbers bool

Whether to mask all numbers. Defaults to False.

False
remove_emojis bool

Whether to remove emojis. Defaults to False.

False
url_mask str | None

The mask to replace URLs with. Defaults to None.

None
email_mask str | None

The mask to replace email addresses with. Defaults to None.

None
phone_mask str | None

The mask to replace phone numbers with. Defaults to None.

None
currency_mask str | None

The mask to replace currency symbols and amounts with. Defaults to None.

None
number_mask str | None

The mask to replace numbers with. Defaults to None.

None
clean_spacing bool

Whether to clean up spacing issues. Defaults to True.

True
remove_punctuation bool

Whether to remove punctuation marks. Defaults to False.

False
normalize_punctuation_spacing bool

Whether to apply proper spacing around punctuation marks. Defaults to False.

False
keep_zwnj bool

Whether to keep zero-width non-joiners when cleaning spacing instead of turning them into spaces. Defaults to False.

False

Returns:

Name Type Description
str str

The normalized text.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def normalize_persian_text(
    cls,
    text: str,
    *,
    remove_vowels: bool = True,
    normalize_punctuation: bool = True,
    normalize_numbers: bool = True,
    normalize_persian_chars: bool = True,
    mask_urls: bool = False,
    mask_emails: bool = False,
    mask_phones: bool = False,
    mask_currencies: bool = False,
    mask_all_numbers: bool = False,
    remove_emojis: bool = False,
    url_mask: str | None = None,
    email_mask: str | None = None,
    phone_mask: str | None = None,
    currency_mask: str | None = None,
    number_mask: str | None = None,
    clean_spacing: bool = True,
    remove_punctuation: bool = False,
    normalize_punctuation_spacing: bool = False,
    keep_zwnj: bool = False,
) -> str:
    """Normalizes Persian text with configurable options.

    Args:
        text (str): The input text to normalize.
        remove_vowels (bool): Whether to remove Arabic vowels. Defaults to `True`.
        normalize_punctuation (bool): Whether to normalize punctuation marks. Defaults to `True`.
        normalize_numbers (bool): Whether to normalize numbers to English format. Defaults to `True`.
        normalize_persian_chars (bool): Whether to normalize Persian characters. Defaults to `True`.
        mask_urls (bool): Whether to mask URLs. Defaults to `False`.
        mask_emails (bool): Whether to mask email addresses. Defaults to `False`.
        mask_phones (bool): Whether to mask phone numbers. Defaults to `False`.
        mask_currencies (bool): Whether to mask currency symbols and amounts. Defaults to `False`.
        mask_all_numbers (bool): Whether to mask all numbers. Defaults to `False`.
        remove_emojis (bool): Whether to remove emojis. Defaults to `False`.
        url_mask (str | None): The mask to replace URLs with. Defaults to `None`.
        email_mask (str | None): The mask to replace email addresses with. Defaults to `None`.
        phone_mask (str | None): The mask to replace phone numbers with. Defaults to `None`.
        currency_mask (str | None): The mask to replace currency symbols and amounts with. Defaults to `None`.
        number_mask (str | None): The mask to replace numbers with. Defaults to `None`.
        clean_spacing (bool): Whether to clean up spacing issues. Defaults to `True`.
        remove_punctuation (bool): Whether to remove punctuation marks. Defaults to `False`.
        normalize_punctuation_spacing (bool): Whether to apply proper spacing around
            punctuation marks. Defaults to `False`.
        keep_zwnj (bool): Whether to keep zero-width non-joiners when cleaning spacing instead of turning them
            into spaces. Defaults to `False`.

    Returns:
        str: The normalized text.
    """
    if not text:
        return text

    if remove_emojis:
        text = cls.remove_emoji(text)

    text = cls._apply_persian_text_normalizations(
        text,
        remove_vowels=remove_vowels,
        normalize_persian_chars=normalize_persian_chars,
        normalize_punctuation=normalize_punctuation,
        remove_punctuation=remove_punctuation,
        normalize_numbers=normalize_numbers,
    )

    text = cls._apply_persian_text_masks(
        text,
        mask_urls=mask_urls,
        mask_emails=mask_emails,
        mask_phones=mask_phones,
        mask_currencies=mask_currencies,
        mask_all_numbers=mask_all_numbers,
        url_mask=url_mask,
        email_mask=email_mask,
        phone_mask=phone_mask,
        currency_mask=currency_mask,
        number_mask=number_mask,
    )

    if clean_spacing:
        text = cls.clean_spacing(text, keep_zwnj=keep_zwnj)
    if normalize_punctuation_spacing:
        text = cls.normalize_punctuation_spacing(text)

    return text.strip()

archipy.helpers.utils.string_utils.StringUtils.snake_to_camel_case classmethod

snake_to_camel_case(text: str) -> str

Converts snake_case to camelCase.

Parameters:

Name Type Description Default
text str

The input text in snake_case format.

required

Returns:

Name Type Description
str str

The text converted to camelCase format.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def snake_to_camel_case(cls, text: str) -> str:
    """Converts snake_case to camelCase.

    Args:
        text (str): The input text in snake_case format.

    Returns:
        str: The text converted to camelCase format.
    """
    if cls.is_string_none_or_empty(text):
        return text

    components = text.split("_")
    # First component remains lowercase, the rest get capitalized
    return components[0] + "".join(x[:1].upper() + x[1:] for x in components[1:])

archipy.helpers.utils.string_utils.StringUtils.camel_to_snake_case classmethod

camel_to_snake_case(text: str) -> str

Converts camelCase to snake_case.

Parameters:

Name Type Description Default
text str

The input text in camelCase format.

required

Returns:

Name Type Description
str str

The text converted to snake_case format.

Source code in archipy/helpers/utils/string_utils.py
@classmethod
def camel_to_snake_case(cls, text: str) -> str:
    """Converts camelCase to snake_case.

    Args:
        text (str): The input text in camelCase format.

    Returns:
        str: The text converted to snake_case format.
    """
    if cls.is_string_none_or_empty(text):
        return text

    # Add underscore before each capital letter and convert to lowercase
    s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
    return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()

options: show_root_toc_entry: false heading_level: 3

String Utils Constants

Constants used by string_utils for character sets, patterns, and limits.

Constants for string utility helpers.

archipy.helpers.utils.string_utils_constants.StringUtilsConstants

Constants for string utility operations including translation tables and regex patterns.

Source code in archipy/helpers/utils/string_utils_constants.py
class StringUtilsConstants:
    """Constants for string utility operations including translation tables and regex patterns."""

    arabic_vowel_translate_table = str.maketrans(
        dict.fromkeys(
            "\u064e\u064f\u0650\u0652\u0651\u0653\u064b\u064c\u0621\u064d\u0670"  # Normal vowels (Fatha, Damma, etc)
            "\u06d6\u06d7\u06d8\u06d9\u06da\u06db",  # Quranic marks
            "",
        ),
    )

    # replace 'آ|ﺁ' with 'آ'
    alphabet_akoolad_alef_translate_table = str.maketrans(dict.fromkeys("\ufe81", "\u0622"))

    # replace 'ٳ|ٲ|ٱ|إ|ﺍ|أ|ٵ | ﺎ' with 'ا'
    alphabet_alef_translate_table = str.maketrans(
        dict.fromkeys("\ufe8e\u0672\u0671\u0625\ufe8d\u0623\u0675\u0673", "\u0627"),
    )

    # replace 'ٮ|ݕ|ٻ|ﺐ|ﺏ|ﺑ' with "ب"
    alphabet_be_translate_table = str.maketrans(dict.fromkeys("\ufe90\ufe8f\ufe91\u067b\u066e", "\u0628"))

    # replace 'ݕ|ݒ|ݐ|ڀ|ﭖ|ﭗ|ﭙ|ﺒ|ﭘ' with "پ"
    alphabet_pe_translate_table = str.maketrans(
        dict.fromkeys("\ufb56\ufb57\ufb59\ufe92\ufb58\u0680\u0750\u0752\u0755", "\u067e"),
    )

    # replace 'ﭡ|ٺ|ٹ|ﭞ|ٿ|ټ|ﺕ|ﺗ|ﺖ|ﺘ|ݓ' with "ت"
    alphabet_te_translate_table = str.maketrans(
        dict.fromkeys("\ufb61\u067a\u0679\ufb5e\u067f\u067c\ufe95\ufe97\ufe96\ufe98\u0753", "\u062a"),
    )
    # replace ﺙ|ﺛ|ٽ|ﺚ|ﺜ with "ث"
    alphabet_se_translate_table = str.maketrans(dict.fromkeys("\ufe99\ufe9b\u067d\ufe9a\ufe9c", "\u062b"))

    # replace "ﺞ|ﺝ|ﺠ|ﺟ| " with "ج"
    alphabet_jim_translate_table = str.maketrans(dict.fromkeys("\ufe9d\ufea0\ufe9f\ufe9e\u06da", "\u062c"))

    # replace "ﭻ|ڿ|ݘ|ڄ|ڇ|ڃ|ﭽ|ﭼ" with "چ"
    alphabet_che_translate_table = str.maketrans(
        dict.fromkeys("\u0683\ufb7d\ufb7c\u0687\u0684\u0758\u06bf\ufb7b", "\u0686"),
    )

    # replace "ﺡ|ﺢ|ﺤ|څ|ځ|ﺣ" with "ح"
    alphabet_he_translate_table = str.maketrans(dict.fromkeys("\ufea2\ufea4\u0685\u0681\ufea3\ufea1", "\u062d"))

    # replace "ݗ|څ|ڂ|ﺥ|ﺦ|ﺨ|ﺧ" with "خ"
    alphabet_khe_translate_table = str.maketrans(dict.fromkeys("\ufea5\ufea6\ufea8\ufea7\u0682\u0757", "\u062e"))

    # replace "ܥ|ڍ|ڈ|ڊ|ﺪ|ﺩ|ډ" with "د"
    alphabet_dal_translate_table = str.maketrans(dict.fromkeys("\u0689\ufeaa\ufea9\u068a\u0688\u068d\u0725", "\u062f"))

    # replace "ڌ|ڎ|ڏ|ڐ|ﺫ|ﺬ" with "ذ"
    alphabet_zal_translate_table = str.maketrans(dict.fromkeys("\ufeab\ufeac\u0690\u068f\u068e\u068c", "\u0630"))

    # replace "ۯ|ڑ|ڒ|ړ|ڔ|ڕ|ږ|ڒ|ڑ|ڕ|ﺭ|ﺮ|ڗ" with "ر"
    alphabet_re_translate_table = str.maketrans(
        dict.fromkeys("\u0697\u0692\u0691\u0695\ufead\ufeae\u0696\u0694\u0693\u0692\u0691\u06ef", "\u0631"),
    )

    # replace "ﺰ|ﺯ" with "ز"
    alphabet_ze_translate_table = str.maketrans(dict.fromkeys("\ufeb0\ufeaf", "\u0632"))

    # replace "ﮋ|ڙ|ﮊ" with "ژ"
    alphabet_zhe_translate_table = str.maketrans(dict.fromkeys("\ufb8a\u0699\ufb8b", "\u0698"))

    # replace  r"ۣښ|ݭ|ݜ|ﺱ|ﺲ|ښ|ﺴ|ﺳ|ڛ|ۣ	"ݽ|ݾ|with r "س"
    alphabet_sin_translate_table = str.maketrans(
        dict.fromkeys("\u076d\u075c\ufeb1\ufeb2\ufeb4\ufeb3\u069b\u069a\u06e3\u077e\u077d", "\u0633"),
    )

    # replace "ۺ|ڜ|ﺵ|ﺶ|ﺸ|ﺷ" with "ش"
    alphabet_shin_translate_table = str.maketrans(dict.fromkeys("\ufeb5\ufeb6\ufeb8\ufeb7\u069c\u06fa", "\u0634"))

    # replace "ڝ|ﺺ|ﺼ|ﺻ |ﺹ" with "ص"
    alphabet_sad_translate_table = str.maketrans(dict.fromkeys("\ufeb9\ufeba\ufebc\ufebb\u069d", "\u0635"))

    # replace "ڞ|ۻ|ﺽ|ﺾ|ﺿ|ﻀ"  with "ض"
    alphabet_zad_translate_table = str.maketrans(dict.fromkeys("\ufebd\ufebe\ufebf\ufec0\u06fb\u069e", "\u0636"))

    # replace "ﻁ|ﻂ|ﻃ|ﻄ" with "ط"
    alphabet_ta_translate_table = str.maketrans(dict.fromkeys("\ufec1\ufec2\ufec3\ufec4", "\u0637"))

    # replace "ڟ|ﻆ|ﻇ|ﻈ|ﻅ" with "ظ"
    alphabet_za_translate_table = str.maketrans(dict.fromkeys("\ufec6\ufec7\ufec8\u069f\ufec5", "\u0638"))

    # replace "ڠ|ﻉ|ﻊ|ﻋ|ﻌ" with "ع"
    alphabet_eyn_translate_table = str.maketrans(dict.fromkeys("\u06a0\ufec9\ufeca\ufecb\ufecc", "\u0639"))

    # replace "ݞ|ݝ|ﻎ|ۼ|ﻍ|ﻐ|ﻏ|ݟ" with "غ"
    alphabet_gheyn_translate_table = str.maketrans(
        dict.fromkeys("\ufece\u06fc\ufecd\ufed0\ufecf\u075d\u075e\u075f", "\u063a"),
    )

    # replace "ڢ|ڣ|ڢ|ڣ|ڤ|ڦ|ڥ|ڡ|ﻒ|ﻑ|ﻔ|ﻓ" with "ف"
    alphabet_fe_translate_table = str.maketrans(
        dict.fromkeys("\ufed2\ufed1\ufed4\ufed3\u06a1\u06a5\u06a6\u06a4\u06a3\u06a2", "\u0641"),
    )

    # replace "ڧ|ڨ|ﻕ|ﻖ|ﻗ|ﻘ" with "ق"
    alphabet_ghaf_translate_table = str.maketrans(dict.fromkeys("\ufed5\ufed6\ufed7\u06a7\u06a8\ufed8", "\u0642"))

    # replace "ݿ|ڮ|ڬ|ݤ|ݣ|ݢ|ڭ|ﻚ|ﮎ|ﻜ|ﮏ|ګ|ﻛ|ﮑ|ﮐ|ڪ|ك|ﻙ" with "ک"
    alphabet_kaf_translate_table = str.maketrans(
        dict.fromkeys(
            "\u06ad\ufeda\ufb8e\ufedc\ufb8f\u06ab\ufedb"
            "\ufb91\ufb90\u06aa\u0643\u0762\u0763\u0764"
            "\u06ac\u06ae\u077f\ufed9",
            "\u06a9",
        ),
    )
    # replace  "ڴ|ڳ|ڲ|ڰ|ڱ|ﮚ|ﮒ|ﮓ|ﮕ|ﮔ" with "گ"
    alphabet_gaf_translate_table = str.maketrans(
        dict.fromkeys("\ufb9a\ufb92\ufb93\ufb95\ufb94\u06b1\u06b0\u06b2\u06b3\u06b4", "\u06af"),
    )

    # replace "ڵ|ڶ|ڸ|ڷ|ݪ|ﻝ|ﻞ|ﻠ|ڵ | ﻟ" with "ل"
    alphabet_lam_translate_table = str.maketrans(
        dict.fromkeys("\ufedf\ufedd\ufede\ufee0\u076a\u06b7\u06b8\u06b6\u06b5", "\u0644"),
    )

    # replace "ݥ|ݦ|ﻡ|ﻤ|ﻢ|ﻣ" with "م"
    alphabet_mim_translate_table = str.maketrans(dict.fromkeys("\ufee1\ufee4\ufee2\ufee3\u0766\u0765", "\u0645"))

    # replace "ڹ|ں|ڽ|ڻ|ݧ|ݨ|ݩ|ڼ|ﻦ|ﻥ|ﻧ|ﻨ" with "ن"
    alphabet_nun_translate_table = str.maketrans(
        dict.fromkeys("\u06bc\ufee6\ufee5\ufee7\ufee8\u0769\u0768\u0767\u06bb\u06bd\u06ba\u06b9", "\u0646"),
    )

    # replace "ۄ|ۆ|ۊ|ވ|ﯙ|ۈ|ۋ|ﺆ|ۊ|ۇ|ۏ|ۅ|ۉ|ﻭ|ﻮ|ؤ" with "و"
    alphabet_vav_translate_table = str.maketrans(
        dict.fromkeys(
            "\u0788\ufbd9\u06c8\u06cb\ufe86\u06ca\u06c7\u06cf\u06c5\u06c9\ufeed\ufeee\u0624\u06c6\u06c4",
            "\u0648",
        ),
    )
    # replace "ܝ|ܤ|ܣ|ﺔ|ﻬ|ھ|ﻩ|ﻫ|ﻪ|ۀ|ە|ة|ہ|ﮭ|ﺓ" with "ه"
    alphabet_ha_translate_table = str.maketrans(
        dict.fromkeys(
            "\ufe93\ufbad\ufe94\ufeec\u06be\ufee9\ufeeb\ufeea\u06c0\u06d5\u0629\u06c1\u0723\u0724\u071d",
            "\u0647",
        ),
    )

    # replace "ﺋ|ؿ|ؾ|ؽ|ۑ|ٸ|ﭛ|ﻯ|ۍ|ﻰ|ﻱ|ﻲ|ﻳ|ﻴ|ﯼ|ې|ﯽ|ﯾ|ﯿ|ێ|ے|ى|ي|ﺉ|ﺌ |ﯨ" with "ی"
    alphabet_ye_translate_table = str.maketrans(
        dict.fromkeys(
            "\ufbe8\ufb5b\ufeef\u06cd\ufef0\ufef1\ufef2\ufef3\ufef4\ufbfc"
            "\u06d0\ufbfd\ufbfe\ufbff\u06ce\u06d2\u0649\u064a\u0678"
            "\u06d1\u063d\u063e\u063f\ufe89\ufe8b\ufe8c",
            "\u06cc",
        ),
    )
    # replace '¬' with ' '
    punctuation_translate_table1 = str.maketrans(dict.fromkeys("\u00ac", "\u0020"))

    # replace '•|·|●|·|・|∙|。|ⴰ' with '.'
    punctuation_translate_table2 = str.maketrans(
        dict.fromkeys("\u2022\u00b7\u25cf\u0387\u30fb\u2219\uff61\u2d30", "\u002e"),
    )

    # replace '٬|٫|‚|,' with '،' (the ASCII ',' is handled by ``ascii_punctuation_patterns``)
    punctuation_translate_table3 = str.maketrans(dict.fromkeys("\u066c\u066b\u201a\uff0c", "\u060c"))

    # replace 'ʕ | ⁉ | � ' with '؟' (the ASCII '?' is handled by ``ascii_punctuation_patterns``)
    punctuation_translate_table4 = str.maketrans(dict.fromkeys("\u0295\u2049\ufffd", "\u061f"))

    # replace '‼ | ❕ ' with '!'
    punctuation_translate_table5 = str.maketrans(dict.fromkeys("\u203c\u2755", "\u0021"))

    # replace ' - | ━ | − | ‐ | ‑ | – | — | ─ | − | ー | ⁃ (hyphen bullet : not supported by pycharm) |  ' with '-'
    punctuation_translate_table7 = str.maketrans(
        dict.fromkeys("\uff0d\u2501\u2212\u2010\u2011\u2013\u2014\u2500\u2212\u30fc\u2043", "\u002d"),
    )

    # replace '‹ |《 | ﴾ ' with '«'
    punctuation_translate_table8 = str.maketrans(dict.fromkeys("\u2039\u300a\ufd3e", "\u00ab"))

    # replace '› | 》| ﴿ ' with '»'
    punctuation_translate_table9 = str.maketrans(dict.fromkeys("\u203a\u300b\ufd3f", "\u00bb"))

    # ASCII ',', '?', ';', '%' and '_' also occur in numbers, URLs, e-mails and identifiers, so they are converted
    # to their Persian forms only next to Persian text.
    ascii_punctuation_patterns: list[tuple[re.Pattern[str], str]] = compile_patterns(
        [
            (
                r"(?<=[\u0600-\u06ff\ufb50-\ufdff\ufe70-\ufeff]),|,(?=\s*[\u0600-\u06ff\ufb50-\ufdff\ufe70-\ufeff])",
                "\u060c",
            ),
            (r"(?<=[\u0600-\u06ff\ufb50-\ufdff\ufe70-\ufeff\u061f])\?", "\u061f"),
            (r"(?<=[\u0600-\u06ff\ufb50-\ufdff\ufe70-\ufeff\u061b]);", "\u061b"),
            (r"(?<=[\u0600-\u06ff\ufb50-\ufdff\ufe70-\ufeff])%", "\u066a"),
            (r"(?<=[\u0600-\u06ff\ufb50-\ufdff\ufe70-\ufeff])_(?=[\u0600-\u06ff\ufb50-\ufdff\ufe70-\ufeff])", "\u0640"),
        ],
    )

    # replace "  ˈ | ‘ | ’ | “ | ”  " with " ' "
    punctuation_translate_table12 = str.maketrans(dict.fromkeys("\u02c8\u2018\u2019\u201c\u201d", "\u0027"))

    # replace ':' with ': '
    punctuation_translate_table13 = str.maketrans(dict.fromkeys("\uff1a", "\u003a"))

    character_refinement_patterns: list[tuple[re.Pattern[str], str]] = compile_patterns(
        [
            (r" +", " "),  # remove extra spaces
            (r"\n\n+", "\n"),  # remove extra newlines
            (r" ?\.\.\.", " …"),  # replace 3 dots
        ],
    )

    punctuation_after = r"\.:!،؛؟»\]\)\}"
    punctuation_before = r"«\[\(\{"

    punctuation_spacing_patterns = compile_patterns(
        [
            (f" ([{punctuation_after}])", r"\1"),
            (f"([{punctuation_before}]) ", r"\1"),
            (
                # not after an ASCII word character, so "file.txt" and "example.com" stay intact
                f"(?<![A-Za-z0-9])([{punctuation_after[:3]}])([^ {punctuation_after}" + r"\d])",
                r"\1 \2",
            ),
            (
                f"([{punctuation_after[3:]}])([^ {punctuation_after}])",
                r"\1 \2",
            ),
            (f"([^ {punctuation_before}])([{punctuation_before}])", r"\1 \2"),
        ],
    )

    # replace '۰|٠' with '0'
    number_zero_translate_table = str.maketrans(dict.fromkeys("\u06f0\u0660", "\u0030"))

    # replace '۱|١' with '1'
    number_one_translate_table = str.maketrans(dict.fromkeys("\u06f1\u0661", "\u0031"))

    # replace '۲|٢' with '2'
    number_two_translate_table = str.maketrans(dict.fromkeys("\u06f2\u0662", "\u0032"))

    # replace '۳|٣' with '3'
    number_three_translate_table = str.maketrans(dict.fromkeys("\u06f3\u0663", "\u0033"))

    # replace '۴|٤' with '4'
    number_four_translate_table = str.maketrans(dict.fromkeys("\u06f4\u0664", "\u0034"))

    # replace '۵|٥' with '5'
    number_five_translate_table = str.maketrans(dict.fromkeys("\u06f5\u0665", "\u0035"))

    # replace '۶|٦' with '6'
    number_six_translate_table = str.maketrans(dict.fromkeys("\u06f6\u0666", "\u0036"))

    # replace '۷|٧' with '7'
    number_seven_translate_table = str.maketrans(dict.fromkeys("\u06f7\u0667", "\u0037"))

    # replace '۸|٨' with '8'
    number_eight_translate_table = str.maketrans(dict.fromkeys("\u06f8\u0668", "\u0038"))

    # replace '۹|٩' with '9'
    number_nine_translate_table = str.maketrans(dict.fromkeys("\u06f9\u0669", "\u0039"))

    # replace ' «|» | . | : | ، | ؛ | ؟ | [|] | (|) | {|} | - | ـ | ٪ | ! | ' | " | # | + | / |' with ' '
    punctuation_persian_marks_to_space_translate_table = str.maketrans(
        dict.fromkeys(
            "\u002e\u003a\u0021\u060c\u061b\u061f\u00bb\u005d"
            "\u0029\u007d\u00ab\u005b\u0028\u007b\u002d\u0640\u066a\u0021\u0027\u0022\u0023"
            "\u002b\u002f\u002c\u003f\u003b\u0025\u005f",
            "\u0020",
        ),
    )

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.arabic_vowel_translate_table class-attribute instance-attribute

arabic_vowel_translate_table = str.maketrans(
    dict.fromkeys("ًٌَُِّْٓءٍٰۖۗۘۙۚۛ", "")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_akoolad_alef_translate_table class-attribute instance-attribute

alphabet_akoolad_alef_translate_table = str.maketrans(
    dict.fromkeys("ﺁ", "آ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_alef_translate_table class-attribute instance-attribute

alphabet_alef_translate_table = str.maketrans(
    dict.fromkeys("ﺎٲٱإﺍأٵٳ", "ا")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_be_translate_table class-attribute instance-attribute

alphabet_be_translate_table = str.maketrans(
    dict.fromkeys("ﺐﺏﺑٻٮ", "ب")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_pe_translate_table class-attribute instance-attribute

alphabet_pe_translate_table = str.maketrans(
    dict.fromkeys("ﭖﭗﭙﺒﭘڀݐݒݕ", "پ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_te_translate_table class-attribute instance-attribute

alphabet_te_translate_table = str.maketrans(
    dict.fromkeys("ﭡٺٹﭞٿټﺕﺗﺖﺘݓ", "ت")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_se_translate_table class-attribute instance-attribute

alphabet_se_translate_table = str.maketrans(
    dict.fromkeys("ﺙﺛٽﺚﺜ", "ث")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_jim_translate_table class-attribute instance-attribute

alphabet_jim_translate_table = str.maketrans(
    dict.fromkeys("ﺝﺠﺟﺞۚ", "ج")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_che_translate_table class-attribute instance-attribute

alphabet_che_translate_table = str.maketrans(
    dict.fromkeys("ڃﭽﭼڇڄݘڿﭻ", "چ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_he_translate_table class-attribute instance-attribute

alphabet_he_translate_table = str.maketrans(
    dict.fromkeys("ﺢﺤڅځﺣﺡ", "ح")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_khe_translate_table class-attribute instance-attribute

alphabet_khe_translate_table = str.maketrans(
    dict.fromkeys("ﺥﺦﺨﺧڂݗ", "خ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_dal_translate_table class-attribute instance-attribute

alphabet_dal_translate_table = str.maketrans(
    dict.fromkeys("ډﺪﺩڊڈڍܥ", "د")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_zal_translate_table class-attribute instance-attribute

alphabet_zal_translate_table = str.maketrans(
    dict.fromkeys("ﺫﺬڐڏڎڌ", "ذ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_re_translate_table class-attribute instance-attribute

alphabet_re_translate_table = str.maketrans(
    dict.fromkeys("ڗڒڑڕﺭﺮږڔړڒڑۯ", "ر")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_ze_translate_table class-attribute instance-attribute

alphabet_ze_translate_table = str.maketrans(
    dict.fromkeys("ﺰﺯ", "ز")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_zhe_translate_table class-attribute instance-attribute

alphabet_zhe_translate_table = str.maketrans(
    dict.fromkeys("ﮊڙﮋ", "ژ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_sin_translate_table class-attribute instance-attribute

alphabet_sin_translate_table = str.maketrans(
    dict.fromkeys("ݭݜﺱﺲﺴﺳڛښۣݾݽ", "س")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_shin_translate_table class-attribute instance-attribute

alphabet_shin_translate_table = str.maketrans(
    dict.fromkeys("ﺵﺶﺸﺷڜۺ", "ش")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_sad_translate_table class-attribute instance-attribute

alphabet_sad_translate_table = str.maketrans(
    dict.fromkeys("ﺹﺺﺼﺻڝ", "ص")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_zad_translate_table class-attribute instance-attribute

alphabet_zad_translate_table = str.maketrans(
    dict.fromkeys("ﺽﺾﺿﻀۻڞ", "ض")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_ta_translate_table class-attribute instance-attribute

alphabet_ta_translate_table = str.maketrans(
    dict.fromkeys("ﻁﻂﻃﻄ", "ط")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_za_translate_table class-attribute instance-attribute

alphabet_za_translate_table = str.maketrans(
    dict.fromkeys("ﻆﻇﻈڟﻅ", "ظ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_eyn_translate_table class-attribute instance-attribute

alphabet_eyn_translate_table = str.maketrans(
    dict.fromkeys("ڠﻉﻊﻋﻌ", "ع")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_gheyn_translate_table class-attribute instance-attribute

alphabet_gheyn_translate_table = str.maketrans(
    dict.fromkeys("ﻎۼﻍﻐﻏݝݞݟ", "غ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_fe_translate_table class-attribute instance-attribute

alphabet_fe_translate_table = str.maketrans(
    dict.fromkeys("ﻒﻑﻔﻓڡڥڦڤڣڢ", "ف")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_ghaf_translate_table class-attribute instance-attribute

alphabet_ghaf_translate_table = str.maketrans(
    dict.fromkeys("ﻕﻖﻗڧڨﻘ", "ق")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_kaf_translate_table class-attribute instance-attribute

alphabet_kaf_translate_table = str.maketrans(
    dict.fromkeys("ڭﻚﮎﻜﮏګﻛﮑﮐڪكݢݣݤڬڮݿﻙ", "ک")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_gaf_translate_table class-attribute instance-attribute

alphabet_gaf_translate_table = str.maketrans(
    dict.fromkeys("ﮚﮒﮓﮕﮔڱڰڲڳڴ", "گ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_lam_translate_table class-attribute instance-attribute

alphabet_lam_translate_table = str.maketrans(
    dict.fromkeys("ﻟﻝﻞﻠݪڷڸڶڵ", "ل")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_mim_translate_table class-attribute instance-attribute

alphabet_mim_translate_table = str.maketrans(
    dict.fromkeys("ﻡﻤﻢﻣݦݥ", "م")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_nun_translate_table class-attribute instance-attribute

alphabet_nun_translate_table = str.maketrans(
    dict.fromkeys("ڼﻦﻥﻧﻨݩݨݧڻڽںڹ", "ن")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_vav_translate_table class-attribute instance-attribute

alphabet_vav_translate_table = str.maketrans(
    dict.fromkeys("ވﯙۈۋﺆۊۇۏۅۉﻭﻮؤۆۄ", "و")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_ha_translate_table class-attribute instance-attribute

alphabet_ha_translate_table = str.maketrans(
    dict.fromkeys("ﺓﮭﺔﻬھﻩﻫﻪۀەةہܣܤܝ", "ه")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.alphabet_ye_translate_table class-attribute instance-attribute

alphabet_ye_translate_table = str.maketrans(
    dict.fromkeys("ﯨﭛﻯۍﻰﻱﻲﻳﻴﯼېﯽﯾﯿێےىيٸۑؽؾؿﺉﺋﺌ", "ی")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table1 class-attribute instance-attribute

punctuation_translate_table1 = str.maketrans(
    dict.fromkeys("¬", " ")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table2 class-attribute instance-attribute

punctuation_translate_table2 = str.maketrans(
    dict.fromkeys("•·●·・∙。ⴰ", ".")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table3 class-attribute instance-attribute

punctuation_translate_table3 = str.maketrans(
    dict.fromkeys("٬٫‚,", "،")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table4 class-attribute instance-attribute

punctuation_translate_table4 = str.maketrans(
    dict.fromkeys("ʕ⁉�", "؟")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table5 class-attribute instance-attribute

punctuation_translate_table5 = str.maketrans(
    dict.fromkeys("‼❕", "!")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table7 class-attribute instance-attribute

punctuation_translate_table7 = str.maketrans(
    dict.fromkeys("-━−‐‑–—─−ー⁃", "-")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table8 class-attribute instance-attribute

punctuation_translate_table8 = str.maketrans(
    dict.fromkeys("‹《﴾", "«")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table9 class-attribute instance-attribute

punctuation_translate_table9 = str.maketrans(
    dict.fromkeys("›》﴿", "»")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.ascii_punctuation_patterns class-attribute instance-attribute

ascii_punctuation_patterns: list[
    tuple[Pattern[str], str]
] = compile_patterns(
    [
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff]),|,(?=\\s*[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])",
            "،",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff\\u061f])\\?",
            "؟",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff\\u061b]);",
            "؛",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])%",
            "٪",
        ),
        (
            "(?<=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])_(?=[\\u0600-\\u06ff\\ufb50-\\ufdff\\ufe70-\\ufeff])",
            "ـ",
        ),
    ]
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table12 class-attribute instance-attribute

punctuation_translate_table12 = str.maketrans(
    dict.fromkeys("ˈ‘’“”", "'")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table13 class-attribute instance-attribute

punctuation_translate_table13 = str.maketrans(
    dict.fromkeys(":", ":")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.character_refinement_patterns class-attribute instance-attribute

character_refinement_patterns: list[
    tuple[Pattern[str], str]
] = compile_patterns(
    [(" +", " "), ("\\n\\n+", "\n"), (" ?\\.\\.\\.", " …")]
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_after class-attribute instance-attribute

punctuation_after = '\\.:!،؛؟»\\]\\)\\}'

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_before class-attribute instance-attribute

punctuation_before = '«\\[\\(\\{'

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_spacing_patterns class-attribute instance-attribute

punctuation_spacing_patterns = compile_patterns(
    [
        (f" ([{punctuation_after}])", "\\1"),
        (f"([{punctuation_before}]) ", "\\1"),
        (
            f"(?<![A-Za-z0-9])([{punctuation_after[:3]}])([^ {punctuation_after}"
            + "\\d])",
            "\\1 \\2",
        ),
        (
            f"([{punctuation_after[3:]}])([^ {punctuation_after}])",
            "\\1 \\2",
        ),
        (
            f"([^ {punctuation_before}])([{punctuation_before}])",
            "\\1 \\2",
        ),
    ]
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_zero_translate_table class-attribute instance-attribute

number_zero_translate_table = str.maketrans(
    dict.fromkeys("۰٠", "0")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_one_translate_table class-attribute instance-attribute

number_one_translate_table = str.maketrans(
    dict.fromkeys("۱١", "1")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_two_translate_table class-attribute instance-attribute

number_two_translate_table = str.maketrans(
    dict.fromkeys("۲٢", "2")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_three_translate_table class-attribute instance-attribute

number_three_translate_table = str.maketrans(
    dict.fromkeys("۳٣", "3")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_four_translate_table class-attribute instance-attribute

number_four_translate_table = str.maketrans(
    dict.fromkeys("۴٤", "4")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_five_translate_table class-attribute instance-attribute

number_five_translate_table = str.maketrans(
    dict.fromkeys("۵٥", "5")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_six_translate_table class-attribute instance-attribute

number_six_translate_table = str.maketrans(
    dict.fromkeys("۶٦", "6")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_seven_translate_table class-attribute instance-attribute

number_seven_translate_table = str.maketrans(
    dict.fromkeys("۷٧", "7")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_eight_translate_table class-attribute instance-attribute

number_eight_translate_table = str.maketrans(
    dict.fromkeys("۸٨", "8")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.number_nine_translate_table class-attribute instance-attribute

number_nine_translate_table = str.maketrans(
    dict.fromkeys("۹٩", "9")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_persian_marks_to_space_translate_table class-attribute instance-attribute

punctuation_persian_marks_to_space_translate_table = (
    str.maketrans(
        dict.fromkeys(".:!،؛؟»])}«[({-ـ٪!'\"#+/,?;%_", " ")
    )
)

archipy.helpers.utils.string_utils_constants.compile_patterns

compile_patterns(
    patterns: list[tuple[str, str]],
) -> list[tuple[re.Pattern[str], str]]

Compile regex patterns with their replacement strings.

Parameters:

Name Type Description Default
patterns list[tuple[str, str]]

List of tuples containing (pattern, replacement) pairs.

required

Returns:

Type Description
list[tuple[Pattern[str], str]]

List of tuples containing (compiled_pattern, replacement) pairs.

Source code in archipy/helpers/utils/string_utils_constants.py
def compile_patterns(patterns: list[tuple[str, str]]) -> list[tuple[re.Pattern[str], str]]:
    """Compile regex patterns with their replacement strings.

    Args:
        patterns: List of tuples containing (pattern, replacement) pairs.

    Returns:
        List of tuples containing (compiled_pattern, replacement) pairs.
    """
    return [(re_compile(pattern), repl) for pattern, repl in patterns]

options: show_root_toc_entry: false heading_level: 3

File Utils

Utilities for file operations including reading, writing, hashing, and type validation.

File system utility helpers.

archipy.helpers.utils.file_utils.FileUtils

A utility class for handling file-related operations, such as creating secure links and validating file names.

Source code in archipy/helpers/utils/file_utils.py
class FileUtils:
    """A utility class for handling file-related operations, such as creating secure links and validating file names."""

    @staticmethod
    def _create_secure_link_hash(path: str, expires_at: float, file_config: FileConfig | None = None) -> str:
        """Generates a secure hash for a file link based on the file path, expiration timestamp, and secret key.

        Args:
            path (str): The file path to generate the hash for.
            expires_at (float): The expiration timestamp for the link.
            file_config (FileConfig | None): Optional file configuration object.
                If not provided, uses the global config.

        Returns:
            str: A base64-encoded secure hash for the file link.

        Raises:
            InvalidArgumentError: If the `SECRET_KEY` in the configuration is `None`.
        """
        configs: FileConfig = file_config or BaseConfig.global_config().FILE
        secret: str | None = configs.SECRET_KEY
        if secret is None:
            raise InvalidArgumentError(argument_name="SECRET_KEY")
        _input = f"{expires_at}{path} {secret}"
        # nginx secure_link requires MD5; this hash is for protocol compatibility, not cryptographic security.
        hash_object = hashlib.md5(_input.encode("utf8"), usedforsecurity=False)
        return base64.urlsafe_b64encode(hash_object.digest()).decode("utf-8").rstrip("=")

    @classmethod
    def create_secure_link(
        cls,
        path: str,
        minutes: int | None = None,
        file_config: FileConfig | None = None,
    ) -> str:
        """Creates a secure link with expiration for file access.

        Args:
            path (str): The file path to create a secure link for.
            minutes (int | None): Number of minutes until link expiration.
                Defaults to the config's `DEFAULT_EXPIRY_MINUTES`.
            file_config (FileConfig | None): Optional file configuration object.
                If not provided, uses the global config.

        Returns:
            str: A secure link with a hash and expiration timestamp.

        Raises:
            InvalidArgumentError: If the `path` is empty.
            OutOfRangeError: If `minutes` is less than 1.
        """
        if not path:
            raise InvalidArgumentError(argument_name="path")

        configs: FileConfig = file_config or BaseConfig.global_config().FILE
        expiry_minutes: int = minutes if minutes is not None else configs.DEFAULT_EXPIRY_MINUTES

        if expiry_minutes < 1:
            raise OutOfRangeError(field_name="minutes")

        expires_at = int(DatetimeUtils.get_datetime_after_given_datetime_or_now(minutes=expiry_minutes).timestamp())
        secure_link_hash = cls._create_secure_link_hash(path, expires_at, file_config)

        return f"{path}?md5={secure_link_hash}&expires_at={expires_at}"

    @classmethod
    def validate_file_name(
        cls,
        file_name: str,
        file_config: FileConfig | None = None,
    ) -> bool:
        """Validates a file name based on allowed extensions.

        Args:
            file_name (str): The file name to validate.
            file_config (FileConfig | None): Optional file configuration object.
                If not provided, uses the global config.

        Returns:
            bool: `True` if the file name has an allowed extension, `False` otherwise.

        Raises:
            InvalidArgumentError: If `file_name` is not a string or `allowed_extensions` is not a list.
        """
        configs: FileConfig = file_config or BaseConfig.global_config().FILE
        allowed_extensions: list[str] = configs.ALLOWED_EXTENSIONS

        if not isinstance(file_name, str):
            raise InvalidArgumentError(argument_name="file_name")

        if not allowed_extensions:
            raise InvalidArgumentError(argument_name="allowed_extensions")

        file_path = Path(file_name)
        ext = file_path.suffix[1:].lower()
        # Compare case-insensitively and accept entries written as ".pdf" or "PDF"
        normalized_allowed = {allowed.lstrip(".").lower() for allowed in allowed_extensions}
        return bool(ext) and ext in normalized_allowed
create_secure_link(
    path: str,
    minutes: int | None = None,
    file_config: FileConfig | None = None,
) -> str

Creates a secure link with expiration for file access.

Parameters:

Name Type Description Default
path str

The file path to create a secure link for.

required
minutes int | None

Number of minutes until link expiration. Defaults to the config's DEFAULT_EXPIRY_MINUTES.

None
file_config FileConfig | None

Optional file configuration object. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

A secure link with a hash and expiration timestamp.

Raises:

Type Description
InvalidArgumentError

If the path is empty.

OutOfRangeError

If minutes is less than 1.

Source code in archipy/helpers/utils/file_utils.py
@classmethod
def create_secure_link(
    cls,
    path: str,
    minutes: int | None = None,
    file_config: FileConfig | None = None,
) -> str:
    """Creates a secure link with expiration for file access.

    Args:
        path (str): The file path to create a secure link for.
        minutes (int | None): Number of minutes until link expiration.
            Defaults to the config's `DEFAULT_EXPIRY_MINUTES`.
        file_config (FileConfig | None): Optional file configuration object.
            If not provided, uses the global config.

    Returns:
        str: A secure link with a hash and expiration timestamp.

    Raises:
        InvalidArgumentError: If the `path` is empty.
        OutOfRangeError: If `minutes` is less than 1.
    """
    if not path:
        raise InvalidArgumentError(argument_name="path")

    configs: FileConfig = file_config or BaseConfig.global_config().FILE
    expiry_minutes: int = minutes if minutes is not None else configs.DEFAULT_EXPIRY_MINUTES

    if expiry_minutes < 1:
        raise OutOfRangeError(field_name="minutes")

    expires_at = int(DatetimeUtils.get_datetime_after_given_datetime_or_now(minutes=expiry_minutes).timestamp())
    secure_link_hash = cls._create_secure_link_hash(path, expires_at, file_config)

    return f"{path}?md5={secure_link_hash}&expires_at={expires_at}"

archipy.helpers.utils.file_utils.FileUtils.validate_file_name classmethod

validate_file_name(
    file_name: str, file_config: FileConfig | None = None
) -> bool

Validates a file name based on allowed extensions.

Parameters:

Name Type Description Default
file_name str

The file name to validate.

required
file_config FileConfig | None

Optional file configuration object. If not provided, uses the global config.

None

Returns:

Name Type Description
bool bool

True if the file name has an allowed extension, False otherwise.

Raises:

Type Description
InvalidArgumentError

If file_name is not a string or allowed_extensions is not a list.

Source code in archipy/helpers/utils/file_utils.py
@classmethod
def validate_file_name(
    cls,
    file_name: str,
    file_config: FileConfig | None = None,
) -> bool:
    """Validates a file name based on allowed extensions.

    Args:
        file_name (str): The file name to validate.
        file_config (FileConfig | None): Optional file configuration object.
            If not provided, uses the global config.

    Returns:
        bool: `True` if the file name has an allowed extension, `False` otherwise.

    Raises:
        InvalidArgumentError: If `file_name` is not a string or `allowed_extensions` is not a list.
    """
    configs: FileConfig = file_config or BaseConfig.global_config().FILE
    allowed_extensions: list[str] = configs.ALLOWED_EXTENSIONS

    if not isinstance(file_name, str):
        raise InvalidArgumentError(argument_name="file_name")

    if not allowed_extensions:
        raise InvalidArgumentError(argument_name="allowed_extensions")

    file_path = Path(file_name)
    ext = file_path.suffix[1:].lower()
    # Compare case-insensitively and accept entries written as ".pdf" or "PDF"
    normalized_allowed = {allowed.lstrip(".").lower() for allowed in allowed_extensions}
    return bool(ext) and ext in normalized_allowed

options: show_root_toc_entry: false heading_level: 3

Error Utils

Deprecated wrappers (capture_exception, async_handle_fastapi_exception, get_fastapi_exception_responses) that redirect to archipy.helpers.errors. They will be removed in 6.0.0.

Deprecated error handling helpers; use archipy.helpers.errors instead.

archipy.helpers.utils.error_utils.ErrorUtils

Deprecated wrappers kept for BaseUtils callers; each method names its replacement in the warning.

Source code in archipy/helpers/utils/error_utils.py
class ErrorUtils:
    """Deprecated wrappers kept for ``BaseUtils`` callers; each method names its replacement in the warning."""

    @staticmethod
    def capture_exception(exception: BaseException) -> None:
        """Deprecated: use ``archipy.helpers.errors.capture_exception``.

        Args:
            exception (BaseException): The exception to capture and report.
        """
        warn_deprecated_name("BaseUtils.capture_exception", "archipy.helpers.errors.capture_exception")
        capture_exception(exception)

    @staticmethod
    async def async_handle_fastapi_exception(_request: Request, exception: BaseError) -> JSONResponse:
        """Deprecated: use ``archipy.helpers.errors.fastapi.handle_fastapi_exception``.

        Args:
            _request (Request): The incoming FastAPI request.
            exception (BaseError): The exception to handle.

        Returns:
            JSONResponse: A JSON response containing the exception details.
        """
        from archipy.helpers.errors.fastapi import handle_fastapi_exception

        warn_deprecated_name(
            "BaseUtils.async_handle_fastapi_exception",
            "archipy.helpers.errors.fastapi.handle_fastapi_exception",
        )
        return await handle_fastapi_exception(_request, exception)

    @staticmethod
    def get_fastapi_exception_responses(exceptions: list[type[BaseError]]) -> dict[int, dict[str, object]]:
        """Deprecated: use ``archipy.helpers.errors.fastapi.get_fastapi_exception_responses``.

        Args:
            exceptions (list[type[BaseError]]): A list of exception types to generate responses for.

        Returns:
            dict[int, dict[str, object]]: A dictionary mapping HTTP status codes to their corresponding response
                schemas.
        """
        from archipy.helpers.errors.fastapi import get_fastapi_exception_responses

        warn_deprecated_name(
            "BaseUtils.get_fastapi_exception_responses",
            "archipy.helpers.errors.fastapi.get_fastapi_exception_responses",
        )
        return get_fastapi_exception_responses(exceptions)

archipy.helpers.utils.error_utils.ErrorUtils.capture_exception staticmethod

capture_exception(exception: BaseException) -> None

Deprecated: use archipy.helpers.errors.capture_exception.

Parameters:

Name Type Description Default
exception BaseException

The exception to capture and report.

required
Source code in archipy/helpers/utils/error_utils.py
@staticmethod
def capture_exception(exception: BaseException) -> None:
    """Deprecated: use ``archipy.helpers.errors.capture_exception``.

    Args:
        exception (BaseException): The exception to capture and report.
    """
    warn_deprecated_name("BaseUtils.capture_exception", "archipy.helpers.errors.capture_exception")
    capture_exception(exception)

archipy.helpers.utils.error_utils.ErrorUtils.async_handle_fastapi_exception async staticmethod

async_handle_fastapi_exception(
    _request: Request, exception: BaseError
) -> JSONResponse

Deprecated: use archipy.helpers.errors.fastapi.handle_fastapi_exception.

Parameters:

Name Type Description Default
_request Request

The incoming FastAPI request.

required
exception BaseError

The exception to handle.

required

Returns:

Name Type Description
JSONResponse JSONResponse

A JSON response containing the exception details.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
async def async_handle_fastapi_exception(_request: Request, exception: BaseError) -> JSONResponse:
    """Deprecated: use ``archipy.helpers.errors.fastapi.handle_fastapi_exception``.

    Args:
        _request (Request): The incoming FastAPI request.
        exception (BaseError): The exception to handle.

    Returns:
        JSONResponse: A JSON response containing the exception details.
    """
    from archipy.helpers.errors.fastapi import handle_fastapi_exception

    warn_deprecated_name(
        "BaseUtils.async_handle_fastapi_exception",
        "archipy.helpers.errors.fastapi.handle_fastapi_exception",
    )
    return await handle_fastapi_exception(_request, exception)

archipy.helpers.utils.error_utils.ErrorUtils.get_fastapi_exception_responses staticmethod

get_fastapi_exception_responses(
    exceptions: list[type[BaseError]],
) -> dict[int, dict[str, object]]

Deprecated: use archipy.helpers.errors.fastapi.get_fastapi_exception_responses.

Parameters:

Name Type Description Default
exceptions list[type[BaseError]]

A list of exception types to generate responses for.

required

Returns:

Type Description
dict[int, dict[str, object]]

dict[int, dict[str, object]]: A dictionary mapping HTTP status codes to their corresponding response schemas.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
def get_fastapi_exception_responses(exceptions: list[type[BaseError]]) -> dict[int, dict[str, object]]:
    """Deprecated: use ``archipy.helpers.errors.fastapi.get_fastapi_exception_responses``.

    Args:
        exceptions (list[type[BaseError]]): A list of exception types to generate responses for.

    Returns:
        dict[int, dict[str, object]]: A dictionary mapping HTTP status codes to their corresponding response
            schemas.
    """
    from archipy.helpers.errors.fastapi import get_fastapi_exception_responses

    warn_deprecated_name(
        "BaseUtils.get_fastapi_exception_responses",
        "archipy.helpers.errors.fastapi.get_fastapi_exception_responses",
    )
    return get_fastapi_exception_responses(exceptions)

options: show_root_toc_entry: false heading_level: 3

JWT Utils

Utilities for JWT generation, verification, and decoding with configurable signing algorithms and expiration.

Utility module for JWT token operations with enhanced security and datetime handling.

This module provides a robust JWT handling implementation with support for access and refresh tokens, cryptographic security, token validation, and comprehensive error handling.

archipy.helpers.utils.jwt_utils.JWTUtils

Utility class for JWT token operations with enhanced security and datetime handling.

Source code in archipy/helpers/utils/jwt_utils.py
class JWTUtils:
    """Utility class for JWT token operations with enhanced security and datetime handling."""

    @classmethod
    def create_token(
        cls,
        data: Mapping[str, object],
        expires_in: int,
        additional_claims: Mapping[str, object] | None = None,
        auth_config: AuthConfig | None = None,
    ) -> str:
        """Creates a JWT token with enhanced security features.

        Args:
            data (Mapping[str, object]): Base claims data to include in the token.
            expires_in (int): Token expiration time in seconds.
            additional_claims (Mapping[str, object] | None): Optional additional claims to include in the token. They
                cannot redefine ``iss``, ``aud``, ``jti`` or any key already present in ``data`` (for example
                ``sub``, ``type`` and ``token_version`` of access and refresh tokens); time claims may be overridden.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            str: The encoded JWT token.

        Raises:
            InvalidArgumentError: If data is empty, the expiration is invalid, ``additional_claims`` collides with
                reserved claims, the secret key is missing or too short for the algorithm (HS256 needs 32 bytes,
                HS384 48, HS512 64), or the algorithm is not configured.
        """
        import jwt

        configs = auth_config or BaseConfig.global_config().AUTH
        current_time = DatetimeUtils.get_datetime_utc_now()

        # Define argument names
        arg_data = "data"
        arg_expires_in = "expires_in"

        if not data:
            raise InvalidArgumentError(arg_data)
        if expires_in <= 0:
            raise InvalidArgumentError(arg_expires_in)
        if additional_claims and (reserved := (_PROTECTED_CLAIMS | data.keys()) & additional_claims.keys()):
            raise InvalidArgumentError("additional_claims", additional_data={"reserved_claims": sorted(reserved)})

        to_encode: dict[str, object] = dict(data)
        expire = DatetimeUtils.get_datetime_after_given_datetime_or_now(seconds=expires_in, datetime_given=current_time)

        # Add standard claims
        to_encode.update(
            {
                # Registered claims (RFC 7519)
                "iss": configs.JWT_ISSUER,
                "aud": configs.JWT_AUDIENCE,
                "exp": expire,
                "iat": current_time,
                "nbf": current_time,
            },
        )

        # Add JWT ID if enabled
        if configs.ENABLE_JTI_CLAIM:
            to_encode["jti"] = str(uuid4())

        # Add additional claims
        if additional_claims:
            to_encode.update(additional_claims)

        # Validate SECRET_KEY
        algorithm = _validated_algorithm(configs)
        return jwt.encode(to_encode, _validated_secret(configs), algorithm=algorithm)

    @classmethod
    def create_access_token(
        cls,
        user_uuid: UUID,
        additional_claims: Mapping[str, object] | None = None,
        auth_config: AuthConfig | None = None,
    ) -> str:
        """Creates an access token for a user.

        Args:
            user_uuid (UUID): The user's UUID to include in the token.
            additional_claims (Mapping[str, object] | None): Optional additional claims to include in the token.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            str: The encoded access token.
        """
        configs = auth_config or BaseConfig.global_config().AUTH

        return cls.create_token(
            data={
                "sub": str(user_uuid),
                "type": "access",
                "token_version": configs.TOKEN_VERSION,
            },
            expires_in=configs.ACCESS_TOKEN_EXPIRES_IN,
            additional_claims=additional_claims,
            auth_config=configs,
        )

    @classmethod
    def create_refresh_token(
        cls,
        user_uuid: UUID,
        additional_claims: Mapping[str, object] | None = None,
        auth_config: AuthConfig | None = None,
    ) -> str:
        """Creates a refresh token for a user.

        Args:
            user_uuid (UUID): The user's UUID to include in the token.
            additional_claims (Mapping[str, object] | None): Optional additional claims to include in the token.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            str: The encoded refresh token.
        """
        configs = auth_config or BaseConfig.global_config().AUTH

        return cls.create_token(
            data={
                "sub": str(user_uuid),
                "type": "refresh",
                "token_version": configs.TOKEN_VERSION,
            },
            expires_in=configs.REFRESH_TOKEN_EXPIRES_IN,
            additional_claims=additional_claims,
            auth_config=configs,
        )

    @classmethod
    def decode_token(
        cls,
        token: str,
        verify_type: str | None = None,
        auth_config: AuthConfig | None = None,
    ) -> dict[str, object]:
        """Decodes and verifies a JWT token with enhanced security checks.

        Args:
            token (str): The JWT token to decode.
            verify_type (str | None): Optional token type to verify (e.g., "access" or "refresh").
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            dict[str, object]: The decoded token payload.

        Raises:
            TokenExpiredError: If the token has expired.
            InvalidTokenError: If the token is invalid (e.g., invalid signature, audience, issuer, or type).
        """
        import jwt
        from jwt.exceptions import ExpiredSignatureError, InvalidTokenError as JWTInvalidTokenError

        configs = auth_config or BaseConfig.global_config().AUTH
        required_claims = ["exp", "iat", "nbf", "aud", "iss", "sub", "type", "token_version"]
        if configs.ENABLE_JTI_CLAIM:
            required_claims.append("jti")

        try:
            # Validate SECRET_KEY
            algorithm = _validated_algorithm(configs)
            payload = jwt.decode(
                token,
                _validated_secret(configs),
                algorithms=[algorithm],
                options={
                    "verify_signature": True,
                    "verify_exp": True,
                    "verify_nbf": True,
                    "verify_iat": True,
                    "verify_aud": True,
                    "verify_iss": True,
                    "require": required_claims,
                },
                audience=configs.JWT_AUDIENCE,
                issuer=configs.JWT_ISSUER,
                leeway=configs.JWT_LEEWAY,
            )

            # Verify token type
            if verify_type is not None and payload.get("type") != verify_type:
                raise InvalidTokenError

            # Verify token version
            if payload.get("token_version") != configs.TOKEN_VERSION:
                raise InvalidTokenError

            # Ensure the return type is dict[str, object] as declared
            return dict(payload)

        except ExpiredSignatureError as exception:
            raise TokenExpiredError from exception
        except JWTInvalidTokenError as exception:
            # Signature, audience, issuer, nbf/iat and malformed-token errors all derive from this class
            raise InvalidTokenError from exception

    @classmethod
    def verify_access_token(cls, token: str, auth_config: AuthConfig | None = None) -> dict[str, object]:
        """Verifies an access token.

        Args:
            token (str): The access token to verify.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            dict[str, object]: The decoded access token payload.

        Raises:
            InvalidTokenError: If the token is invalid or not an access token.
            TokenExpiredError: If the token has expired.
        """
        configs = auth_config or BaseConfig.global_config().AUTH
        return cls.decode_token(token, verify_type="access", auth_config=configs)

    @classmethod
    def verify_refresh_token(cls, token: str, auth_config: AuthConfig | None = None) -> dict[str, object]:
        """Verifies a refresh token.

        Args:
            token (str): The refresh token to verify.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            dict[str, object]: The decoded refresh token payload.

        Raises:
            InvalidTokenError: If the token is invalid or not a refresh token.
            TokenExpiredError: If the token has expired.
        """
        configs = auth_config or BaseConfig.global_config().AUTH
        return cls.decode_token(token, verify_type="refresh", auth_config=configs)

    @staticmethod
    def extract_user_uuid(payload: Mapping[str, object]) -> UUID:
        """Extracts the user UUID from the token payload.

        Args:
            payload (Mapping[str, object]): The decoded token payload.

        Returns:
            UUID: The user's UUID.

        Raises:
            InvalidTokenError: If the user identifier is invalid or missing.
        """
        try:
            subject = payload["sub"]
        except KeyError as exception:
            raise InvalidTokenError from exception
        if not isinstance(subject, str):
            raise InvalidTokenError
        try:
            return UUID(subject)
        except ValueError as exception:
            raise InvalidTokenError from exception

    @classmethod
    def get_token_expiry(cls, token: str, auth_config: AuthConfig | None = None) -> int:
        """Gets the token expiry timestamp.

        Args:
            token (str): The JWT token.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            int: The token expiry timestamp in seconds.

        Raises:
            InvalidTokenError: If the token is invalid.
        """
        payload = cls.decode_token(token, auth_config=auth_config)
        # PyJWT has already checked that ``exp`` converts to an integer
        expiry = payload["exp"]
        if not isinstance(expiry, int | float | str):
            raise InvalidTokenError
        return int(expiry)

archipy.helpers.utils.jwt_utils.JWTUtils.create_token classmethod

create_token(
    data: Mapping[str, object],
    expires_in: int,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str

Creates a JWT token with enhanced security features.

Parameters:

Name Type Description Default
data Mapping[str, object]

Base claims data to include in the token.

required
expires_in int

Token expiration time in seconds.

required
additional_claims Mapping[str, object] | None

Optional additional claims to include in the token. They cannot redefine iss, aud, jti or any key already present in data (for example sub, type and token_version of access and refresh tokens); time claims may be overridden.

None
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

The encoded JWT token.

Raises:

Type Description
InvalidArgumentError

If data is empty, the expiration is invalid, additional_claims collides with reserved claims, the secret key is missing or too short for the algorithm (HS256 needs 32 bytes, HS384 48, HS512 64), or the algorithm is not configured.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def create_token(
    cls,
    data: Mapping[str, object],
    expires_in: int,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str:
    """Creates a JWT token with enhanced security features.

    Args:
        data (Mapping[str, object]): Base claims data to include in the token.
        expires_in (int): Token expiration time in seconds.
        additional_claims (Mapping[str, object] | None): Optional additional claims to include in the token. They
            cannot redefine ``iss``, ``aud``, ``jti`` or any key already present in ``data`` (for example
            ``sub``, ``type`` and ``token_version`` of access and refresh tokens); time claims may be overridden.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: The encoded JWT token.

    Raises:
        InvalidArgumentError: If data is empty, the expiration is invalid, ``additional_claims`` collides with
            reserved claims, the secret key is missing or too short for the algorithm (HS256 needs 32 bytes,
            HS384 48, HS512 64), or the algorithm is not configured.
    """
    import jwt

    configs = auth_config or BaseConfig.global_config().AUTH
    current_time = DatetimeUtils.get_datetime_utc_now()

    # Define argument names
    arg_data = "data"
    arg_expires_in = "expires_in"

    if not data:
        raise InvalidArgumentError(arg_data)
    if expires_in <= 0:
        raise InvalidArgumentError(arg_expires_in)
    if additional_claims and (reserved := (_PROTECTED_CLAIMS | data.keys()) & additional_claims.keys()):
        raise InvalidArgumentError("additional_claims", additional_data={"reserved_claims": sorted(reserved)})

    to_encode: dict[str, object] = dict(data)
    expire = DatetimeUtils.get_datetime_after_given_datetime_or_now(seconds=expires_in, datetime_given=current_time)

    # Add standard claims
    to_encode.update(
        {
            # Registered claims (RFC 7519)
            "iss": configs.JWT_ISSUER,
            "aud": configs.JWT_AUDIENCE,
            "exp": expire,
            "iat": current_time,
            "nbf": current_time,
        },
    )

    # Add JWT ID if enabled
    if configs.ENABLE_JTI_CLAIM:
        to_encode["jti"] = str(uuid4())

    # Add additional claims
    if additional_claims:
        to_encode.update(additional_claims)

    # Validate SECRET_KEY
    algorithm = _validated_algorithm(configs)
    return jwt.encode(to_encode, _validated_secret(configs), algorithm=algorithm)

archipy.helpers.utils.jwt_utils.JWTUtils.create_access_token classmethod

create_access_token(
    user_uuid: UUID,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str

Creates an access token for a user.

Parameters:

Name Type Description Default
user_uuid UUID

The user's UUID to include in the token.

required
additional_claims Mapping[str, object] | None

Optional additional claims to include in the token.

None
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

The encoded access token.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def create_access_token(
    cls,
    user_uuid: UUID,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str:
    """Creates an access token for a user.

    Args:
        user_uuid (UUID): The user's UUID to include in the token.
        additional_claims (Mapping[str, object] | None): Optional additional claims to include in the token.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: The encoded access token.
    """
    configs = auth_config or BaseConfig.global_config().AUTH

    return cls.create_token(
        data={
            "sub": str(user_uuid),
            "type": "access",
            "token_version": configs.TOKEN_VERSION,
        },
        expires_in=configs.ACCESS_TOKEN_EXPIRES_IN,
        additional_claims=additional_claims,
        auth_config=configs,
    )

archipy.helpers.utils.jwt_utils.JWTUtils.create_refresh_token classmethod

create_refresh_token(
    user_uuid: UUID,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str

Creates a refresh token for a user.

Parameters:

Name Type Description Default
user_uuid UUID

The user's UUID to include in the token.

required
additional_claims Mapping[str, object] | None

Optional additional claims to include in the token.

None
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

The encoded refresh token.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def create_refresh_token(
    cls,
    user_uuid: UUID,
    additional_claims: Mapping[str, object] | None = None,
    auth_config: AuthConfig | None = None,
) -> str:
    """Creates a refresh token for a user.

    Args:
        user_uuid (UUID): The user's UUID to include in the token.
        additional_claims (Mapping[str, object] | None): Optional additional claims to include in the token.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: The encoded refresh token.
    """
    configs = auth_config or BaseConfig.global_config().AUTH

    return cls.create_token(
        data={
            "sub": str(user_uuid),
            "type": "refresh",
            "token_version": configs.TOKEN_VERSION,
        },
        expires_in=configs.REFRESH_TOKEN_EXPIRES_IN,
        additional_claims=additional_claims,
        auth_config=configs,
    )

archipy.helpers.utils.jwt_utils.JWTUtils.decode_token classmethod

decode_token(
    token: str,
    verify_type: str | None = None,
    auth_config: AuthConfig | None = None,
) -> dict[str, object]

Decodes and verifies a JWT token with enhanced security checks.

Parameters:

Name Type Description Default
token str

The JWT token to decode.

required
verify_type str | None

Optional token type to verify (e.g., "access" or "refresh").

None
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
dict[str, object]

dict[str, object]: The decoded token payload.

Raises:

Type Description
TokenExpiredError

If the token has expired.

InvalidTokenError

If the token is invalid (e.g., invalid signature, audience, issuer, or type).

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def decode_token(
    cls,
    token: str,
    verify_type: str | None = None,
    auth_config: AuthConfig | None = None,
) -> dict[str, object]:
    """Decodes and verifies a JWT token with enhanced security checks.

    Args:
        token (str): The JWT token to decode.
        verify_type (str | None): Optional token type to verify (e.g., "access" or "refresh").
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        dict[str, object]: The decoded token payload.

    Raises:
        TokenExpiredError: If the token has expired.
        InvalidTokenError: If the token is invalid (e.g., invalid signature, audience, issuer, or type).
    """
    import jwt
    from jwt.exceptions import ExpiredSignatureError, InvalidTokenError as JWTInvalidTokenError

    configs = auth_config or BaseConfig.global_config().AUTH
    required_claims = ["exp", "iat", "nbf", "aud", "iss", "sub", "type", "token_version"]
    if configs.ENABLE_JTI_CLAIM:
        required_claims.append("jti")

    try:
        # Validate SECRET_KEY
        algorithm = _validated_algorithm(configs)
        payload = jwt.decode(
            token,
            _validated_secret(configs),
            algorithms=[algorithm],
            options={
                "verify_signature": True,
                "verify_exp": True,
                "verify_nbf": True,
                "verify_iat": True,
                "verify_aud": True,
                "verify_iss": True,
                "require": required_claims,
            },
            audience=configs.JWT_AUDIENCE,
            issuer=configs.JWT_ISSUER,
            leeway=configs.JWT_LEEWAY,
        )

        # Verify token type
        if verify_type is not None and payload.get("type") != verify_type:
            raise InvalidTokenError

        # Verify token version
        if payload.get("token_version") != configs.TOKEN_VERSION:
            raise InvalidTokenError

        # Ensure the return type is dict[str, object] as declared
        return dict(payload)

    except ExpiredSignatureError as exception:
        raise TokenExpiredError from exception
    except JWTInvalidTokenError as exception:
        # Signature, audience, issuer, nbf/iat and malformed-token errors all derive from this class
        raise InvalidTokenError from exception

archipy.helpers.utils.jwt_utils.JWTUtils.verify_access_token classmethod

verify_access_token(
    token: str, auth_config: AuthConfig | None = None
) -> dict[str, object]

Verifies an access token.

Parameters:

Name Type Description Default
token str

The access token to verify.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
dict[str, object]

dict[str, object]: The decoded access token payload.

Raises:

Type Description
InvalidTokenError

If the token is invalid or not an access token.

TokenExpiredError

If the token has expired.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def verify_access_token(cls, token: str, auth_config: AuthConfig | None = None) -> dict[str, object]:
    """Verifies an access token.

    Args:
        token (str): The access token to verify.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        dict[str, object]: The decoded access token payload.

    Raises:
        InvalidTokenError: If the token is invalid or not an access token.
        TokenExpiredError: If the token has expired.
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    return cls.decode_token(token, verify_type="access", auth_config=configs)

archipy.helpers.utils.jwt_utils.JWTUtils.verify_refresh_token classmethod

verify_refresh_token(
    token: str, auth_config: AuthConfig | None = None
) -> dict[str, object]

Verifies a refresh token.

Parameters:

Name Type Description Default
token str

The refresh token to verify.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
dict[str, object]

dict[str, object]: The decoded refresh token payload.

Raises:

Type Description
InvalidTokenError

If the token is invalid or not a refresh token.

TokenExpiredError

If the token has expired.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def verify_refresh_token(cls, token: str, auth_config: AuthConfig | None = None) -> dict[str, object]:
    """Verifies a refresh token.

    Args:
        token (str): The refresh token to verify.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        dict[str, object]: The decoded refresh token payload.

    Raises:
        InvalidTokenError: If the token is invalid or not a refresh token.
        TokenExpiredError: If the token has expired.
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    return cls.decode_token(token, verify_type="refresh", auth_config=configs)

archipy.helpers.utils.jwt_utils.JWTUtils.extract_user_uuid staticmethod

extract_user_uuid(payload: Mapping[str, object]) -> UUID

Extracts the user UUID from the token payload.

Parameters:

Name Type Description Default
payload Mapping[str, object]

The decoded token payload.

required

Returns:

Name Type Description
UUID UUID

The user's UUID.

Raises:

Type Description
InvalidTokenError

If the user identifier is invalid or missing.

Source code in archipy/helpers/utils/jwt_utils.py
@staticmethod
def extract_user_uuid(payload: Mapping[str, object]) -> UUID:
    """Extracts the user UUID from the token payload.

    Args:
        payload (Mapping[str, object]): The decoded token payload.

    Returns:
        UUID: The user's UUID.

    Raises:
        InvalidTokenError: If the user identifier is invalid or missing.
    """
    try:
        subject = payload["sub"]
    except KeyError as exception:
        raise InvalidTokenError from exception
    if not isinstance(subject, str):
        raise InvalidTokenError
    try:
        return UUID(subject)
    except ValueError as exception:
        raise InvalidTokenError from exception

archipy.helpers.utils.jwt_utils.JWTUtils.get_token_expiry classmethod

get_token_expiry(
    token: str, auth_config: AuthConfig | None = None
) -> int

Gets the token expiry timestamp.

Parameters:

Name Type Description Default
token str

The JWT token.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
int int

The token expiry timestamp in seconds.

Raises:

Type Description
InvalidTokenError

If the token is invalid.

Source code in archipy/helpers/utils/jwt_utils.py
@classmethod
def get_token_expiry(cls, token: str, auth_config: AuthConfig | None = None) -> int:
    """Gets the token expiry timestamp.

    Args:
        token (str): The JWT token.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        int: The token expiry timestamp in seconds.

    Raises:
        InvalidTokenError: If the token is invalid.
    """
    payload = cls.decode_token(token, auth_config=auth_config)
    # PyJWT has already checked that ``exp`` converts to an integer
    expiry = payload["exp"]
    if not isinstance(expiry, int | float | str):
        raise InvalidTokenError
    return int(expiry)

options: show_root_toc_entry: false heading_level: 3

Password Utils

Utilities for secure password hashing, verification, generation, and strength validation with timing-attack protection.

Password hashing and validation utilities.

archipy.helpers.utils.password_utils.PasswordUtils

A utility class for handling password-related operations, such as hashing, verification, and validation.

Source code in archipy/helpers/utils/password_utils.py
class PasswordUtils:
    """A utility class for handling password-related operations, such as hashing, verification, and validation."""

    @staticmethod
    def hash_password(password: str, auth_config: AuthConfig | None = None) -> str:
        """Hashes a password using PBKDF2 with SHA256.

        Args:
            password (str): The password to hash.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            str: A self-describing string in the format ``pbkdf2_sha256$<iterations>$<salt>$<hash>`` where salt and
                hash are base64-encoded. The iteration count is stored with the hash, so changing
                ``HASH_ITERATIONS`` or ``SALT_LENGTH`` later does not invalidate existing hashes.
        """
        configs = auth_config or BaseConfig.global_config().AUTH
        salt = os.urandom(configs.SALT_LENGTH)
        iterations = configs.HASH_ITERATIONS
        pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)

        return _HASH_SEPARATOR.join(
            (_HASH_SCHEME, str(iterations), b64encode(salt).decode("utf-8"), b64encode(pw_hash).decode("utf-8")),
        )

    @staticmethod
    def verify_password(password: str, stored_password: str, auth_config: AuthConfig | None = None) -> bool:
        """Verifies a password against a stored hash.

        Both the current ``pbkdf2_sha256$...`` format and the legacy format (base64 of ``salt + hash``, written by
        earlier versions) are accepted. Legacy hashes carry no parameters, so they are tried with the configured
        ``HASH_ITERATIONS`` and, if that differs, with the former default of 100000.

        Args:
            password (str): The password to verify.
            stored_password (str): The stored password hash to compare against.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            bool: True if the password matches the stored hash, False otherwise.
        """
        try:
            configs = auth_config or BaseConfig.global_config().AUTH

            if stored_password.startswith(_HASH_SCHEME + _HASH_SEPARATOR):
                _, iterations, salt_b64, hash_b64 = stored_password.split(_HASH_SEPARATOR)
                salt = b64decode(salt_b64.encode("utf-8"), validate=True)
                stored_hash = b64decode(hash_b64.encode("utf-8"), validate=True)
                return PasswordUtils._matches(password, salt, stored_hash, int(iterations))

            # Legacy format: base64(salt + hash)
            decoded = b64decode(stored_password.encode("utf-8"))
            salt = decoded[: configs.SALT_LENGTH]
            stored_hash = decoded[configs.SALT_LENGTH :]
            candidates = dict.fromkeys((configs.HASH_ITERATIONS, _LEGACY_DEFAULT_ITERATIONS))
            return any(PasswordUtils._matches(password, salt, stored_hash, count) for count in candidates)
        except ValueError, TypeError, IndexError:
            # Catch specific exceptions that could occur during decoding or comparison
            return False

    @staticmethod
    def _matches(password: str, salt: bytes, stored_hash: bytes, iterations: int) -> bool:
        """Hash ``password`` with the given parameters and compare in constant time."""
        if iterations < 1:
            return False
        pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)
        return hmac.compare_digest(pw_hash, stored_hash)

    @staticmethod
    def validate_password(
        password: str,
        auth_config: AuthConfig | None = None,
    ) -> None:
        """Validates a password against the password policy.

        Args:
            password (str): The password to validate.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Raises:
            InvalidPasswordError: If the password does not meet the policy requirements.
        """
        configs = auth_config or BaseConfig.global_config().AUTH
        errors = []

        if len(password) < configs.MIN_LENGTH:
            errors.append(f"Password must be at least {configs.MIN_LENGTH} characters long.")

        if configs.REQUIRE_DIGIT and not any(char.isdigit() for char in password):
            errors.append("Password must contain at least one digit.")

        if configs.REQUIRE_LOWERCASE and not any(char.islower() for char in password):
            errors.append("Password must contain at least one lowercase letter.")

        if configs.REQUIRE_UPPERCASE and not any(char.isupper() for char in password):
            errors.append("Password must contain at least one uppercase letter.")

        if configs.REQUIRE_SPECIAL and not any(char in configs.SPECIAL_CHARACTERS for char in password):
            errors.append(f"Password must contain at least one special character: {configs.SPECIAL_CHARACTERS}")

        if errors:
            raise InvalidPasswordError(requirements=errors)

    @staticmethod
    def generate_password(auth_config: AuthConfig | None = None) -> str:
        """Generates a random password that meets the policy requirements.

        Args:
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.

        Returns:
            str: A randomly generated password that meets the policy requirements.
        """
        configs = auth_config or BaseConfig.global_config().AUTH

        lowercase_chars = string.ascii_lowercase
        uppercase_chars = string.ascii_uppercase
        digit_chars = string.digits
        special_chars = "".join(configs.SPECIAL_CHARACTERS)

        # Initialize with required characters
        password_chars = []
        if configs.REQUIRE_LOWERCASE:
            password_chars.append(secrets.choice(lowercase_chars))
        if configs.REQUIRE_UPPERCASE:
            password_chars.append(secrets.choice(uppercase_chars))
        if configs.REQUIRE_DIGIT:
            password_chars.append(secrets.choice(digit_chars))
        if configs.REQUIRE_SPECIAL:
            password_chars.append(secrets.choice(special_chars))

        # Calculate remaining length
        remaining_length = max(0, configs.MIN_LENGTH - len(password_chars))

        # Add random characters to meet minimum length
        all_chars = lowercase_chars + uppercase_chars + digit_chars + special_chars
        password_chars.extend(secrets.choice(all_chars) for _ in range(remaining_length))

        # Shuffle the password characters
        shuffled = list(password_chars)
        secrets.SystemRandom().shuffle(shuffled)

        return "".join(shuffled)

    @classmethod
    def validate_password_history(
        cls,
        new_password: str,
        password_history: list[str],
        auth_config: AuthConfig | None = None,
        lang: LanguageType | None = None,
    ) -> None:
        """Validates a new password against the password history.

        Args:
            new_password (str): The new password to validate.
            password_history (list[str]): A list of previous password hashes.
            auth_config (AuthConfig | None): Optional auth configuration override.
                If not provided, uses the global config.
            lang (LanguageType): The language to use for error messages. Defaults to Persian.

        Raises:
            InvalidPasswordError: If the new password has been used recently or does not meet the policy requirements.
        """
        configs = auth_config or BaseConfig.global_config().AUTH

        # First validate against password policy
        cls.validate_password(new_password, configs)

        # Check password history (a size of 0 disables the check)
        history_size = max(configs.PASSWORD_HISTORY_SIZE, 0)
        if history_size and any(
            cls.verify_password(new_password, old_password, configs)
            for old_password in password_history[-history_size:]
        ):
            raise InvalidPasswordError(requirements=["Password has been used recently"], lang=lang)

archipy.helpers.utils.password_utils.PasswordUtils.hash_password staticmethod

hash_password(
    password: str, auth_config: AuthConfig | None = None
) -> str

Hashes a password using PBKDF2 with SHA256.

Parameters:

Name Type Description Default
password str

The password to hash.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

A self-describing string in the format pbkdf2_sha256$<iterations>$<salt>$<hash> where salt and hash are base64-encoded. The iteration count is stored with the hash, so changing HASH_ITERATIONS or SALT_LENGTH later does not invalidate existing hashes.

Source code in archipy/helpers/utils/password_utils.py
@staticmethod
def hash_password(password: str, auth_config: AuthConfig | None = None) -> str:
    """Hashes a password using PBKDF2 with SHA256.

    Args:
        password (str): The password to hash.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: A self-describing string in the format ``pbkdf2_sha256$<iterations>$<salt>$<hash>`` where salt and
            hash are base64-encoded. The iteration count is stored with the hash, so changing
            ``HASH_ITERATIONS`` or ``SALT_LENGTH`` later does not invalidate existing hashes.
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    salt = os.urandom(configs.SALT_LENGTH)
    iterations = configs.HASH_ITERATIONS
    pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)

    return _HASH_SEPARATOR.join(
        (_HASH_SCHEME, str(iterations), b64encode(salt).decode("utf-8"), b64encode(pw_hash).decode("utf-8")),
    )

archipy.helpers.utils.password_utils.PasswordUtils.verify_password staticmethod

verify_password(
    password: str,
    stored_password: str,
    auth_config: AuthConfig | None = None,
) -> bool

Verifies a password against a stored hash.

Both the current pbkdf2_sha256$... format and the legacy format (base64 of salt + hash, written by earlier versions) are accepted. Legacy hashes carry no parameters, so they are tried with the configured HASH_ITERATIONS and, if that differs, with the former default of 100000.

Parameters:

Name Type Description Default
password str

The password to verify.

required
stored_password str

The stored password hash to compare against.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
bool bool

True if the password matches the stored hash, False otherwise.

Source code in archipy/helpers/utils/password_utils.py
@staticmethod
def verify_password(password: str, stored_password: str, auth_config: AuthConfig | None = None) -> bool:
    """Verifies a password against a stored hash.

    Both the current ``pbkdf2_sha256$...`` format and the legacy format (base64 of ``salt + hash``, written by
    earlier versions) are accepted. Legacy hashes carry no parameters, so they are tried with the configured
    ``HASH_ITERATIONS`` and, if that differs, with the former default of 100000.

    Args:
        password (str): The password to verify.
        stored_password (str): The stored password hash to compare against.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        bool: True if the password matches the stored hash, False otherwise.
    """
    try:
        configs = auth_config or BaseConfig.global_config().AUTH

        if stored_password.startswith(_HASH_SCHEME + _HASH_SEPARATOR):
            _, iterations, salt_b64, hash_b64 = stored_password.split(_HASH_SEPARATOR)
            salt = b64decode(salt_b64.encode("utf-8"), validate=True)
            stored_hash = b64decode(hash_b64.encode("utf-8"), validate=True)
            return PasswordUtils._matches(password, salt, stored_hash, int(iterations))

        # Legacy format: base64(salt + hash)
        decoded = b64decode(stored_password.encode("utf-8"))
        salt = decoded[: configs.SALT_LENGTH]
        stored_hash = decoded[configs.SALT_LENGTH :]
        candidates = dict.fromkeys((configs.HASH_ITERATIONS, _LEGACY_DEFAULT_ITERATIONS))
        return any(PasswordUtils._matches(password, salt, stored_hash, count) for count in candidates)
    except ValueError, TypeError, IndexError:
        # Catch specific exceptions that could occur during decoding or comparison
        return False

archipy.helpers.utils.password_utils.PasswordUtils.validate_password staticmethod

validate_password(
    password: str, auth_config: AuthConfig | None = None
) -> None

Validates a password against the password policy.

Parameters:

Name Type Description Default
password str

The password to validate.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Raises:

Type Description
InvalidPasswordError

If the password does not meet the policy requirements.

Source code in archipy/helpers/utils/password_utils.py
@staticmethod
def validate_password(
    password: str,
    auth_config: AuthConfig | None = None,
) -> None:
    """Validates a password against the password policy.

    Args:
        password (str): The password to validate.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Raises:
        InvalidPasswordError: If the password does not meet the policy requirements.
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    errors = []

    if len(password) < configs.MIN_LENGTH:
        errors.append(f"Password must be at least {configs.MIN_LENGTH} characters long.")

    if configs.REQUIRE_DIGIT and not any(char.isdigit() for char in password):
        errors.append("Password must contain at least one digit.")

    if configs.REQUIRE_LOWERCASE and not any(char.islower() for char in password):
        errors.append("Password must contain at least one lowercase letter.")

    if configs.REQUIRE_UPPERCASE and not any(char.isupper() for char in password):
        errors.append("Password must contain at least one uppercase letter.")

    if configs.REQUIRE_SPECIAL and not any(char in configs.SPECIAL_CHARACTERS for char in password):
        errors.append(f"Password must contain at least one special character: {configs.SPECIAL_CHARACTERS}")

    if errors:
        raise InvalidPasswordError(requirements=errors)

archipy.helpers.utils.password_utils.PasswordUtils.generate_password staticmethod

generate_password(
    auth_config: AuthConfig | None = None,
) -> str

Generates a random password that meets the policy requirements.

Parameters:

Name Type Description Default
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Name Type Description
str str

A randomly generated password that meets the policy requirements.

Source code in archipy/helpers/utils/password_utils.py
@staticmethod
def generate_password(auth_config: AuthConfig | None = None) -> str:
    """Generates a random password that meets the policy requirements.

    Args:
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.

    Returns:
        str: A randomly generated password that meets the policy requirements.
    """
    configs = auth_config or BaseConfig.global_config().AUTH

    lowercase_chars = string.ascii_lowercase
    uppercase_chars = string.ascii_uppercase
    digit_chars = string.digits
    special_chars = "".join(configs.SPECIAL_CHARACTERS)

    # Initialize with required characters
    password_chars = []
    if configs.REQUIRE_LOWERCASE:
        password_chars.append(secrets.choice(lowercase_chars))
    if configs.REQUIRE_UPPERCASE:
        password_chars.append(secrets.choice(uppercase_chars))
    if configs.REQUIRE_DIGIT:
        password_chars.append(secrets.choice(digit_chars))
    if configs.REQUIRE_SPECIAL:
        password_chars.append(secrets.choice(special_chars))

    # Calculate remaining length
    remaining_length = max(0, configs.MIN_LENGTH - len(password_chars))

    # Add random characters to meet minimum length
    all_chars = lowercase_chars + uppercase_chars + digit_chars + special_chars
    password_chars.extend(secrets.choice(all_chars) for _ in range(remaining_length))

    # Shuffle the password characters
    shuffled = list(password_chars)
    secrets.SystemRandom().shuffle(shuffled)

    return "".join(shuffled)

archipy.helpers.utils.password_utils.PasswordUtils.validate_password_history classmethod

validate_password_history(
    new_password: str,
    password_history: list[str],
    auth_config: AuthConfig | None = None,
    lang: LanguageType | None = None,
) -> None

Validates a new password against the password history.

Parameters:

Name Type Description Default
new_password str

The new password to validate.

required
password_history list[str]

A list of previous password hashes.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None
lang LanguageType

The language to use for error messages. Defaults to Persian.

None

Raises:

Type Description
InvalidPasswordError

If the new password has been used recently or does not meet the policy requirements.

Source code in archipy/helpers/utils/password_utils.py
@classmethod
def validate_password_history(
    cls,
    new_password: str,
    password_history: list[str],
    auth_config: AuthConfig | None = None,
    lang: LanguageType | None = None,
) -> None:
    """Validates a new password against the password history.

    Args:
        new_password (str): The new password to validate.
        password_history (list[str]): A list of previous password hashes.
        auth_config (AuthConfig | None): Optional auth configuration override.
            If not provided, uses the global config.
        lang (LanguageType): The language to use for error messages. Defaults to Persian.

    Raises:
        InvalidPasswordError: If the new password has been used recently or does not meet the policy requirements.
    """
    configs = auth_config or BaseConfig.global_config().AUTH

    # First validate against password policy
    cls.validate_password(new_password, configs)

    # Check password history (a size of 0 disables the check)
    history_size = max(configs.PASSWORD_HISTORY_SIZE, 0)
    if history_size and any(
        cls.verify_password(new_password, old_password, configs)
        for old_password in password_history[-history_size:]
    ):
        raise InvalidPasswordError(requirements=["Password has been used recently"], lang=lang)

options: show_root_toc_entry: false heading_level: 3

TOTP Utils

Utilities for TOTP (Time-based One-Time Password) generation, verification, and QR code URI construction.

Utility module for TOTP (Time-based One-Time Password) operations.

This module provides functionality for generating and verifying TOTP codes that are commonly used for multi-factor authentication.

archipy.helpers.utils.totp_utils.TOTPUtils

Utility class for TOTP (Time-based One-Time Password) operations.

This class provides methods for generating and verifying TOTP codes, as well as generating secure secret keys for TOTP initialization.

Uses the following configuration parameters from AuthConfig: - TOTP_SECRET_KEY: Master secret key for generating TOTP secrets - TOTP_HASH_ALGORITHM: Hash algorithm used for TOTP generation (default: SHA1) - TOTP_LENGTH: Number of digits in generated TOTP codes - TOTP_TIME_STEP: Time step in seconds between TOTP code changes - TOTP_EXPIRES_IN: TOTP validity period in seconds - TOTP_VERIFICATION_WINDOW: Number of time steps to check before/after - SALT_LENGTH: Length of random bytes for secure key generation

Source code in archipy/helpers/utils/totp_utils.py
class TOTPUtils:
    """Utility class for TOTP (Time-based One-Time Password) operations.

    This class provides methods for generating and verifying TOTP codes, as well as generating
    secure secret keys for TOTP initialization.

    Uses the following configuration parameters from AuthConfig:
    - TOTP_SECRET_KEY: Master secret key for generating TOTP secrets
    - TOTP_HASH_ALGORITHM: Hash algorithm used for TOTP generation (default: SHA1)
    - TOTP_LENGTH: Number of digits in generated TOTP codes
    - TOTP_TIME_STEP: Time step in seconds between TOTP code changes
    - TOTP_EXPIRES_IN: TOTP validity period in seconds
    - TOTP_VERIFICATION_WINDOW: Number of time steps to check before/after
    - SALT_LENGTH: Length of random bytes for secure key generation
    """

    @classmethod
    def generate_totp(cls, secret: str | UUID, auth_config: AuthConfig | None = None) -> tuple[str, datetime]:
        """Generates a TOTP code using the configured hash algorithm.

        Args:
            secret: The secret key used to generate the TOTP code.
            auth_config: Optional auth configuration override. If not provided, uses the global config.

        Returns:
            A tuple containing the generated TOTP code and its expiration time. The expiration
            is ``TOTP_EXPIRES_IN`` from now, capped at the moment ``verify_totp`` stops accepting the code. A code
            stays valid for that whole period: this helper keeps no state, so callers must enforce single use and
            attempt limits (``TOTP_MAX_ATTEMPTS``, ``TOTP_LOCKOUT_TIME``) themselves, for example with a cache.

        Raises:
            InvalidArgumentError: If the secret is invalid or empty.
        """
        if not secret:
            raise InvalidArgumentError(
                argument_name="secret",
            )

        configs = auth_config or BaseConfig.global_config().AUTH

        # Get current timestamp and calculate time step
        current_time = DatetimeUtils.get_epoch_time_now()
        time_step_counter = int(current_time / configs.TOTP_TIME_STEP)

        # Use the dedicated TOTP hash algorithm from config, with fallback to SHA1
        hash_algo = getattr(configs, "TOTP_HASH_ALGORITHM", "SHA1")
        totp_code = _compute_totp(_secret_to_key(secret), time_step_counter, hash_algo, configs.TOTP_LENGTH)

        # The code is accepted until the last step of the verification window ends, so never report a later expiry.
        valid_until = (time_step_counter + configs.TOTP_VERIFICATION_WINDOW + 1) * configs.TOTP_TIME_STEP
        lifetime = min(configs.TOTP_EXPIRES_IN, max(valid_until - current_time, 0))
        expires_in = DatetimeUtils.get_datetime_after_given_datetime_or_now(seconds=lifetime)

        return totp_code, expires_in

    @classmethod
    def verify_totp(
        cls,
        secret: str | UUID,
        totp_code: str,
        auth_config: AuthConfig | None = None,
        replay_guard: Callable[[int], bool] | None = None,
    ) -> bool:
        """Verifies a TOTP code against the provided secret.

        This helper keeps no state. Two protections therefore belong to the caller: counting failed attempts
        (``TOTP_MAX_ATTEMPTS`` / ``TOTP_LOCKOUT_TIME`` are not enforced here) and rejecting a code that was already
        used. For the second one pass ``replay_guard``.

        Args:
            secret: The secret key used to generate the TOTP code.
            totp_code: The TOTP code to verify.
            auth_config: Optional auth configuration override. If not provided, uses the global config.
            replay_guard: Optional callback invoked with the matched time step (an ``int`` counter) once the code
                is valid. Return ``True`` to accept the code, ``False`` to reject it as already used. Typical use is
                an atomic "store the step if it is greater than the last accepted one" on the user record or in
                Redis (RFC 6238 section 5.2). It is not called for invalid codes.

        Returns:
            `True` if the TOTP code is valid, `False` otherwise (including a code whose length differs from
            ``TOTP_LENGTH``).

        Raises:
            InvalidArgumentError: If the secret is invalid or empty.
            InvalidTokenError: If the TOTP code format is invalid.
        """
        if not secret:
            raise InvalidArgumentError(
                argument_name="secret",
            )

        if not totp_code:
            raise InvalidArgumentError(
                argument_name="totp_code",
            )

        if not (totp_code.isascii() and totp_code.isdigit()):
            raise InvalidTokenError

        configs = auth_config or BaseConfig.global_config().AUTH

        # The expected length comes from the config, never from the caller's input.
        if len(totp_code) != configs.TOTP_LENGTH:
            return False

        current_time = DatetimeUtils.get_epoch_time_now()

        # Use the dedicated TOTP hash algorithm from config, with fallback to SHA1
        hash_algo = getattr(configs, "TOTP_HASH_ALGORITHM", "SHA1")

        key = _secret_to_key(secret)
        current_step = int(current_time / configs.TOTP_TIME_STEP)

        # Check every step in the window without early exit so timing does not reveal which step matched
        matched_step: int | None = None
        for i in range(-configs.TOTP_VERIFICATION_WINDOW, configs.TOTP_VERIFICATION_WINDOW + 1):
            computed_totp = _compute_totp(key, current_step + i, hash_algo, configs.TOTP_LENGTH)
            if hmac.compare_digest(totp_code, computed_totp):
                matched_step = current_step + i

        if matched_step is None:
            return False
        return replay_guard is None or bool(replay_guard(matched_step))

    @staticmethod
    def generate_secret_key_for_totp(auth_config: AuthConfig | None = None) -> str:
        """Generates a random secret key for TOTP initialization.

        Args:
            auth_config: Optional auth configuration override. If not provided, uses the global config.

        Returns:
            A base32-encoded secret key for TOTP initialization.

        Raises:
            InvalidArgumentError: If the TOTP_SECRET_KEY is not configured.
            InternalError: If there is an error generating the secret key.
        """
        try:
            configs = auth_config or BaseConfig.global_config().AUTH

            # Use secrets module instead of random for better security
            random_bytes = secrets.token_bytes(configs.SALT_LENGTH)

            # Check if TOTP secret key is configured
            if not configs.TOTP_SECRET_KEY:
                _raise_missing_totp_secret()

            master_key = configs.TOTP_SECRET_KEY.get_secret_value().encode("utf-8")

            # Use the dedicated TOTP hash algorithm from config, with fallback to SHA1
            hash_algo = getattr(configs, "TOTP_HASH_ALGORITHM", "SHA1")

            # Use HMAC with master key for additional security
            hmac_obj = hmac.new(master_key, random_bytes, hash_algo)
            return base64.b32encode(hmac_obj.digest()).decode("utf-8")
        except BaseError:
            raise
        except Exception as e:
            # Convert any other errors to our custom errors
            raise InternalError(additional_data={"operation": "generate_secret_key_for_totp"}) from e

archipy.helpers.utils.totp_utils.TOTPUtils.generate_totp classmethod

generate_totp(
    secret: str | UUID,
    auth_config: AuthConfig | None = None,
) -> tuple[str, datetime]

Generates a TOTP code using the configured hash algorithm.

Parameters:

Name Type Description Default
secret str | UUID

The secret key used to generate the TOTP code.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
str

A tuple containing the generated TOTP code and its expiration time. The expiration

datetime

is TOTP_EXPIRES_IN from now, capped at the moment verify_totp stops accepting the code. A code

tuple[str, datetime]

stays valid for that whole period: this helper keeps no state, so callers must enforce single use and

tuple[str, datetime]

attempt limits (TOTP_MAX_ATTEMPTS, TOTP_LOCKOUT_TIME) themselves, for example with a cache.

Raises:

Type Description
InvalidArgumentError

If the secret is invalid or empty.

Source code in archipy/helpers/utils/totp_utils.py
@classmethod
def generate_totp(cls, secret: str | UUID, auth_config: AuthConfig | None = None) -> tuple[str, datetime]:
    """Generates a TOTP code using the configured hash algorithm.

    Args:
        secret: The secret key used to generate the TOTP code.
        auth_config: Optional auth configuration override. If not provided, uses the global config.

    Returns:
        A tuple containing the generated TOTP code and its expiration time. The expiration
        is ``TOTP_EXPIRES_IN`` from now, capped at the moment ``verify_totp`` stops accepting the code. A code
        stays valid for that whole period: this helper keeps no state, so callers must enforce single use and
        attempt limits (``TOTP_MAX_ATTEMPTS``, ``TOTP_LOCKOUT_TIME``) themselves, for example with a cache.

    Raises:
        InvalidArgumentError: If the secret is invalid or empty.
    """
    if not secret:
        raise InvalidArgumentError(
            argument_name="secret",
        )

    configs = auth_config or BaseConfig.global_config().AUTH

    # Get current timestamp and calculate time step
    current_time = DatetimeUtils.get_epoch_time_now()
    time_step_counter = int(current_time / configs.TOTP_TIME_STEP)

    # Use the dedicated TOTP hash algorithm from config, with fallback to SHA1
    hash_algo = getattr(configs, "TOTP_HASH_ALGORITHM", "SHA1")
    totp_code = _compute_totp(_secret_to_key(secret), time_step_counter, hash_algo, configs.TOTP_LENGTH)

    # The code is accepted until the last step of the verification window ends, so never report a later expiry.
    valid_until = (time_step_counter + configs.TOTP_VERIFICATION_WINDOW + 1) * configs.TOTP_TIME_STEP
    lifetime = min(configs.TOTP_EXPIRES_IN, max(valid_until - current_time, 0))
    expires_in = DatetimeUtils.get_datetime_after_given_datetime_or_now(seconds=lifetime)

    return totp_code, expires_in

archipy.helpers.utils.totp_utils.TOTPUtils.verify_totp classmethod

verify_totp(
    secret: str | UUID,
    totp_code: str,
    auth_config: AuthConfig | None = None,
    replay_guard: Callable[[int], bool] | None = None,
) -> bool

Verifies a TOTP code against the provided secret.

This helper keeps no state. Two protections therefore belong to the caller: counting failed attempts (TOTP_MAX_ATTEMPTS / TOTP_LOCKOUT_TIME are not enforced here) and rejecting a code that was already used. For the second one pass replay_guard.

Parameters:

Name Type Description Default
secret str | UUID

The secret key used to generate the TOTP code.

required
totp_code str

The TOTP code to verify.

required
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None
replay_guard Callable[[int], bool] | None

Optional callback invoked with the matched time step (an int counter) once the code is valid. Return True to accept the code, False to reject it as already used. Typical use is an atomic "store the step if it is greater than the last accepted one" on the user record or in Redis (RFC 6238 section 5.2). It is not called for invalid codes.

None

Returns:

Type Description
bool

True if the TOTP code is valid, False otherwise (including a code whose length differs from

bool

TOTP_LENGTH).

Raises:

Type Description
InvalidArgumentError

If the secret is invalid or empty.

InvalidTokenError

If the TOTP code format is invalid.

Source code in archipy/helpers/utils/totp_utils.py
@classmethod
def verify_totp(
    cls,
    secret: str | UUID,
    totp_code: str,
    auth_config: AuthConfig | None = None,
    replay_guard: Callable[[int], bool] | None = None,
) -> bool:
    """Verifies a TOTP code against the provided secret.

    This helper keeps no state. Two protections therefore belong to the caller: counting failed attempts
    (``TOTP_MAX_ATTEMPTS`` / ``TOTP_LOCKOUT_TIME`` are not enforced here) and rejecting a code that was already
    used. For the second one pass ``replay_guard``.

    Args:
        secret: The secret key used to generate the TOTP code.
        totp_code: The TOTP code to verify.
        auth_config: Optional auth configuration override. If not provided, uses the global config.
        replay_guard: Optional callback invoked with the matched time step (an ``int`` counter) once the code
            is valid. Return ``True`` to accept the code, ``False`` to reject it as already used. Typical use is
            an atomic "store the step if it is greater than the last accepted one" on the user record or in
            Redis (RFC 6238 section 5.2). It is not called for invalid codes.

    Returns:
        `True` if the TOTP code is valid, `False` otherwise (including a code whose length differs from
        ``TOTP_LENGTH``).

    Raises:
        InvalidArgumentError: If the secret is invalid or empty.
        InvalidTokenError: If the TOTP code format is invalid.
    """
    if not secret:
        raise InvalidArgumentError(
            argument_name="secret",
        )

    if not totp_code:
        raise InvalidArgumentError(
            argument_name="totp_code",
        )

    if not (totp_code.isascii() and totp_code.isdigit()):
        raise InvalidTokenError

    configs = auth_config or BaseConfig.global_config().AUTH

    # The expected length comes from the config, never from the caller's input.
    if len(totp_code) != configs.TOTP_LENGTH:
        return False

    current_time = DatetimeUtils.get_epoch_time_now()

    # Use the dedicated TOTP hash algorithm from config, with fallback to SHA1
    hash_algo = getattr(configs, "TOTP_HASH_ALGORITHM", "SHA1")

    key = _secret_to_key(secret)
    current_step = int(current_time / configs.TOTP_TIME_STEP)

    # Check every step in the window without early exit so timing does not reveal which step matched
    matched_step: int | None = None
    for i in range(-configs.TOTP_VERIFICATION_WINDOW, configs.TOTP_VERIFICATION_WINDOW + 1):
        computed_totp = _compute_totp(key, current_step + i, hash_algo, configs.TOTP_LENGTH)
        if hmac.compare_digest(totp_code, computed_totp):
            matched_step = current_step + i

    if matched_step is None:
        return False
    return replay_guard is None or bool(replay_guard(matched_step))

archipy.helpers.utils.totp_utils.TOTPUtils.generate_secret_key_for_totp staticmethod

generate_secret_key_for_totp(
    auth_config: AuthConfig | None = None,
) -> str

Generates a random secret key for TOTP initialization.

Parameters:

Name Type Description Default
auth_config AuthConfig | None

Optional auth configuration override. If not provided, uses the global config.

None

Returns:

Type Description
str

A base32-encoded secret key for TOTP initialization.

Raises:

Type Description
InvalidArgumentError

If the TOTP_SECRET_KEY is not configured.

InternalError

If there is an error generating the secret key.

Source code in archipy/helpers/utils/totp_utils.py
@staticmethod
def generate_secret_key_for_totp(auth_config: AuthConfig | None = None) -> str:
    """Generates a random secret key for TOTP initialization.

    Args:
        auth_config: Optional auth configuration override. If not provided, uses the global config.

    Returns:
        A base32-encoded secret key for TOTP initialization.

    Raises:
        InvalidArgumentError: If the TOTP_SECRET_KEY is not configured.
        InternalError: If there is an error generating the secret key.
    """
    try:
        configs = auth_config or BaseConfig.global_config().AUTH

        # Use secrets module instead of random for better security
        random_bytes = secrets.token_bytes(configs.SALT_LENGTH)

        # Check if TOTP secret key is configured
        if not configs.TOTP_SECRET_KEY:
            _raise_missing_totp_secret()

        master_key = configs.TOTP_SECRET_KEY.get_secret_value().encode("utf-8")

        # Use the dedicated TOTP hash algorithm from config, with fallback to SHA1
        hash_algo = getattr(configs, "TOTP_HASH_ALGORITHM", "SHA1")

        # Use HMAC with master key for additional security
        hmac_obj = hmac.new(master_key, random_bytes, hash_algo)
        return base64.b32encode(hmac_obj.digest()).decode("utf-8")
    except BaseError:
        raise
    except Exception as e:
        # Convert any other errors to our custom errors
        raise InternalError(additional_data={"operation": "generate_secret_key_for_totp"}) from e

options: show_root_toc_entry: false heading_level: 3

Keycloak Utils

The Keycloak authentication guards moved to the adapters layer; see Keycloak. archipy.helpers.utils.keycloak_utils still resolves the old names and emits a DeprecationWarning; the old module will be removed in 6.0.0.

OpenTelemetry Utils

Utilities for OpenTelemetry provider lifecycle, library instrumentation, status mapping, and gRPC client interceptors.

OpenTelemetry utilities for provider lifecycle, instrumentation, and status mapping.

archipy.helpers.utils.otel_utils.DURATION_HISTOGRAM_BUCKETS_S module-attribute

DURATION_HISTOGRAM_BUCKETS_S: tuple[float, ...] = (
    0.005,
    0.01,
    0.025,
    0.05,
    0.075,
    0.1,
    0.25,
    0.5,
    0.75,
    1.0,
    2.5,
    5.0,
    7.5,
    10.0,
)

archipy.helpers.utils.otel_utils.OTEL_FASTAPI_INSTALL_HINT module-attribute

OTEL_FASTAPI_INSTALL_HINT = _OTEL_FASTAPI_HINT

archipy.helpers.utils.otel_utils.OTEL_GRPC_INSTALL_HINT module-attribute

OTEL_GRPC_INSTALL_HINT = _OTEL_GRPC_HINT

archipy.helpers.utils.otel_utils.OTEL_INSTALL_HINT module-attribute

OTEL_INSTALL_HINT = _OTEL_INSTALL_HINT

archipy.helpers.utils.otel_utils.OtelRuntime

Idempotent OpenTelemetry provider management and helpers for one runtime.

Owns TracerProvider / MeterProvider / LoggerProvider references and passes them explicitly to instrumentors. Globals are set once for third-party interop (e.g. Temporal TracingInterceptor), but internal callers use get_tracer / get_meter against owned or adopted providers.

Providers are built programmatically from BaseConfig.OTEL. The SDK's OTEL_* autoconfiguration is not used; with OTEL.USE_STANDARD_ENV the standard variables fill in the settings the config leaves unset. On init, ArchiPy installs the composite textmap propagator named by OTEL.PROPAGATORS (W3C TraceContext + Baggage by default, idempotent) so cross-service carriers stay locked to the intended ones.

Initialization is transactional: providers are published only after all enabled signals succeed. Pre-existing concrete global providers are adopted (not replaced) so ArchiPy and third-party libraries share context. Only ArchiPy-owned providers are shut down.

Source code in archipy/helpers/utils/_otel/runtime.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
class OtelRuntime:
    """Idempotent OpenTelemetry provider management and helpers for one runtime.

    Owns ``TracerProvider`` / ``MeterProvider`` / ``LoggerProvider`` references and
    passes them explicitly to instrumentors. Globals are set once for third-party
    interop (e.g. Temporal ``TracingInterceptor``), but internal callers use
    ``get_tracer`` / ``get_meter`` against owned or adopted providers.

    Providers are built programmatically from ``BaseConfig.OTEL``. The SDK's ``OTEL_*``
    autoconfiguration is not used; with ``OTEL.USE_STANDARD_ENV`` the standard variables fill in
    the settings the config leaves unset. On init, ArchiPy installs the composite textmap
    propagator named by ``OTEL.PROPAGATORS`` (W3C TraceContext + Baggage by default,
    idempotent) so cross-service carriers stay locked to the intended ones.

    Initialization is transactional: providers are published only after all enabled
    signals succeed. Pre-existing concrete global providers are adopted (not replaced)
    so ArchiPy and third-party libraries share context. Only ArchiPy-owned providers
    are shut down.
    """

    def __init__(self) -> None:
        self._initialized = False
        self._logging_handler_attached = False
        self._atexit_registered = False
        self._import_failed = False
        self._tracer_provider: OwnedTracerProvider | None = None
        self._meter_provider: OwnedMeterProvider | None = None
        self._logger_provider: OwnedLoggerProvider | None = None
        self._owns_tracer = False
        self._owns_meter = False
        self._owns_logger = False
        self._logging_handler: logging.Handler | None = None
        self._init_pid: int | None = None
        self._init_failed_at: float | None = None
        self._metrics = MetricsExport()

    @property
    def _lock(self) -> threading.Lock:
        return _ProcessState.lock

    @property
    def _globals_set(self) -> bool:
        return _ProcessState.globals_set

    @_globals_set.setter
    def _globals_set(self, value: bool) -> None:
        _ProcessState.globals_set = value

    @property
    def _textmap_propagator_installed(self) -> bool:
        return _ProcessState.propagator_installed

    @_textmap_propagator_installed.setter
    def _textmap_propagator_installed(self, value: bool) -> None:
        _ProcessState.propagator_installed = value

    @property
    def _shutdown_providers(self) -> weakref.WeakSet[OwnedTracerProvider | OwnedMeterProvider | OwnedLoggerProvider]:
        return _ProcessState.shutdown_providers

    @property
    def _instrumented_libraries(self) -> set[str]:
        return Instrumentation.libraries

    @staticmethod
    def duration_histogram_buckets() -> tuple[float, ...]:
        """Return the explicit bucket boundaries, in seconds, of the duration histograms ArchiPy records.

        Reads ``OTEL.METRICS_DURATION_BUCKETS_S`` from the global config, and falls back to
        ``DURATION_HISTOGRAM_BUCKETS_S`` while no global config is set.

        Returns:
            The bucket boundaries.
        """
        from archipy.configs.base_config import BaseConfig

        try:
            return tuple(BaseConfig.global_config().OTEL.METRICS_DURATION_BUCKETS_S)
        except AssertionError:
            return DURATION_HISTOGRAM_BUCKETS_S

    @staticmethod
    def error_type_for_exception(exception: BaseException) -> str:
        """Return the ``error.type`` attribute value of a failure (``BaseError.code`` or the exception class name)."""
        return error_type_for_exception(exception)

    @staticmethod
    def is_otel_enabled(config: BaseConfig) -> bool:
        """Return True when the OTel master switch is enabled.

        Args:
            config: Application configuration.

        Returns:
            True if ``config.OTEL.IS_ENABLED`` is True.
        """
        return bool(config.OTEL.IS_ENABLED)

    @staticmethod
    def is_traces_enabled(config: BaseConfig) -> bool:
        """Return True when tracing should be active.

        Args:
            config: Application configuration.

        Returns:
            True if the master switch and ``TRACES_ENABLED`` are both True.
        """
        return bool(config.OTEL.IS_ENABLED and config.OTEL.TRACES_ENABLED)

    @staticmethod
    def is_metrics_enabled(config: BaseConfig) -> bool:
        """Return True when metrics should be active.

        Args:
            config: Application configuration.

        Returns:
            True if the master switch and ``METRICS_ENABLED`` are both True.
        """
        return bool(config.OTEL.IS_ENABLED and config.OTEL.METRICS_ENABLED)

    @staticmethod
    def is_logs_enabled(config: BaseConfig) -> bool:
        """Return True when log export should be active.

        Args:
            config: Application configuration.

        Returns:
            True if the master switch and ``LOGS_ENABLED`` are both True.
        """
        return bool(config.OTEL.IS_ENABLED and config.OTEL.LOGS_ENABLED)

    def import_failed(self) -> bool:
        """Return True when OTel initialization failed due to missing packages."""
        return self._import_failed

    def get_tracer(self, name: str) -> Tracer | _NoOpTracer:
        """Return a tracer from the owned tracer provider (or a no-op tracer).

        Args:
            name: Instrumentation scope name.

        Returns:
            An OpenTelemetry ``Tracer`` instance, or a no-op stub when the
            ``opentelemetry`` package is not installed.
        """
        try:
            from opentelemetry import trace
        except ImportError:
            return _NoOpTracer()

        if self._tracer_provider is not None:
            return self._tracer_provider.get_tracer(name)
        return trace.get_tracer(name)

    def get_meter(self, name: str) -> Meter | _NoOpMeter:
        """Return a meter from the owned meter provider (or a no-op meter).

        Args:
            name: Instrumentation scope name.

        Returns:
            An OpenTelemetry ``Meter`` instance, or a no-op stub when the
            ``opentelemetry`` package is not installed.
        """
        try:
            from opentelemetry import metrics
        except ImportError:
            return _NoOpMeter()

        if self._meter_provider is not None:
            return self._meter_provider.get_meter(name)
        return metrics.get_meter(name)

    def tracer_provider(self) -> OwnedTracerProvider | None:
        """Return the owned or adopted tracer provider, if initialized."""
        return self._tracer_provider

    def meter_provider(self) -> OwnedMeterProvider | None:
        """Return the owned or adopted meter provider, if initialized."""
        return self._meter_provider

    def logger_provider(self) -> OwnedLoggerProvider | None:
        """Return the owned or adopted logger provider, if initialized."""
        return self._logger_provider

    def metrics_registry(self) -> CollectorRegistry | None:
        """Return the Prometheus ``CollectorRegistry`` for pull/pushgateway metrics.

        Returns:
            The registry when ``METRICS_EXPORTER`` is ``pull`` or ``pushgateway``
            and providers were built, otherwise ``None``.
        """
        return self._metrics.registry

    @staticmethod
    def _truncate_status_description(exception: BaseException) -> str:
        """Return a bounded status description for span status."""
        return truncate_status_description(exception)

    @staticmethod
    def status_for_exception(exception: BaseException) -> Status | None:
        """Map an exception to an OpenTelemetry ``Status``, or ``None`` for UNSET.

        ``BaseError`` with ``http_status`` below 500 leaves status UNSET (handled client error). All other exceptions
        become ``StatusCode.ERROR``.

        Args:
            exception: The exception raised during a span.

        Returns:
            An OpenTelemetry ``Status`` instance, or ``None`` to leave status UNSET.
        """
        return status_for_exception(exception)

    @staticmethod
    def metric_status_for_exception(exception: BaseException) -> str:
        """Map an exception to a metric ``status`` attribute value (``"ok"`` or ``"error"``).

        Aligns with ``status_for_exception``: handled client ``BaseError`` values record as ``ok``. Callers that catch
        ``asyncio.CancelledError`` should record ``cancelled`` before invoking this helper.

        Args:
            exception: The exception raised during the instrumented call.

        Returns:
            ``"ok"`` or ``"error"``.
        """
        return metric_status_for_exception(exception)

    @staticmethod
    def status_for_cancellation() -> Status:
        """Return an ERROR status for asyncio task cancellation.

        Returns:
            An OpenTelemetry ``Status`` with ``StatusCode.ERROR``.
        """
        return status_for_cancellation()

    def init_otel_if_needed(self, config: BaseConfig) -> None:
        """Initialize OTel providers once (idempotent, thread-safe, fork-aware).

        Args:
            config: Application configuration.
        """
        if not config.OTEL.IS_ENABLED or self._import_failed or sdk_disabled(config.OTEL):
            return

        current_pid = os.getpid()
        if self._initialized and self._init_pid == current_pid:
            return
        if self._init_failed_recently():
            return

        with _ProcessState.lock:
            if not config.OTEL.IS_ENABLED or self._import_failed:
                return
            if self._initialized and self._init_pid == current_pid:
                return
            if self._init_failed_recently():
                return
            if self._initialized and self._init_pid is not None and self._init_pid != current_pid:
                self._reset_after_fork()
            try:
                self._install_textmap_propagator_unlocked(apply_standard_env(config.OTEL).PROPAGATORS)
                self._build_providers(config)
                self._instrument_installed_libraries(config)
                self._register_atexit()
                self._initialized = True
                self._init_pid = current_pid
                self._init_failed_at = None
            except ImportError:
                self._import_failed = True
                logger.warning(
                    "OTEL.IS_ENABLED is True but OpenTelemetry is not installed; telemetry disabled. %s",
                    _OTEL_INSTALL_HINT,
                )
            except Exception:
                self._metrics.stop_pushgateway_unlocked()
                self._metrics.stop_pull_scrape_unlocked()
                self._init_failed_at = time.monotonic()
                logger.exception("Failed to initialize OpenTelemetry")

    def _init_failed_recently(self) -> bool:
        """Return True while a failed initialization is still inside its retry back-off window."""
        failed_at = self._init_failed_at
        return failed_at is not None and time.monotonic() - failed_at < _INIT_RETRY_BACKOFF_SECONDS

    def force_flush(self, timeout_millis: int = _DEFAULT_FLUSH_TIMEOUT_MS) -> bool:
        """Force-flush all known providers.

        Args:
            timeout_millis: Maximum time to wait per provider.

        Returns:
            True when every provider flushed successfully (or none exist).
        """
        # Flush outside the lock: a slow exporter must not block initialization or shutdown on other threads.
        with _ProcessState.lock:
            known = (self._tracer_provider, self._meter_provider, self._logger_provider)
        return provider_lifecycle.flush_providers(known, timeout_millis)

    def shutdown(self) -> None:
        """Flush and shut down ArchiPy-owned providers; detach logging handler.

        Adopted (borrowed) providers are left running. Idempotent.
        """
        with _ProcessState.lock:
            self._force_flush_unlocked(_DEFAULT_FLUSH_TIMEOUT_MS)
            self._detach_logging_handler_unlocked()
            self._metrics.stop_pushgateway_unlocked()
            self._metrics.stop_pull_scrape_unlocked()
            self._shutdown_owned_providers_unlocked()
            self._tracer_provider = None
            self._meter_provider = None
            self._logger_provider = None
            self._owns_tracer = False
            self._owns_meter = False
            self._owns_logger = False
            self._initialized = False
            self._init_pid = None
            self._init_failed_at = None
            Instrumentation.libraries.clear()
            self._clear_metric_instrument_caches()

    def configure_for_testing(
        self,
        span_exporter: SpanExporter | None = None,
        metric_reader: MetricReader | None = None,
        log_exporter: LogRecordExporter | None = None,
        *,
        service_name: str = "archipy-test",
    ) -> None:
        """Swap in in-memory providers for BDD / unit tests.

        Args:
            span_exporter: Optional span exporter (e.g. ``InMemorySpanExporter``).
            metric_reader: Optional metric reader (e.g. ``InMemoryMetricReader``).
            log_exporter: Optional log exporter (e.g. ``InMemoryLogExporter``).
            service_name: Resource service name for the test providers.
        """
        from opentelemetry import metrics, trace
        from opentelemetry.sdk.resources import Resource
        from opentelemetry.sdk.trace.export import SimpleSpanProcessor

        with _ProcessState.lock:
            self._install_textmap_propagator_unlocked(_DEFAULT_PROPAGATORS)
            resource = Resource.create({"service.name": service_name})
            tracer_provider = temporal_providers.new_tracer_provider(resource)
            if span_exporter is not None:
                tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
            self._tracer_provider = tracer_provider
            self._owns_tracer = True
            if not _ProcessState.globals_set:
                trace.set_tracer_provider(tracer_provider)

            if metric_reader is not None:
                from opentelemetry.sdk.metrics import MeterProvider

                meter_provider = temporal_providers.wrap_meter_provider(
                    MeterProvider(resource=resource, metric_readers=[metric_reader]),
                )
                self._meter_provider = meter_provider
                self._owns_meter = True
                if not _ProcessState.globals_set:
                    metrics.set_meter_provider(meter_provider)

            if log_exporter is not None:
                from opentelemetry._logs import set_logger_provider
                from opentelemetry.sdk._logs import LoggerProvider
                from opentelemetry.sdk._logs.export import SimpleLogRecordProcessor

                sdk_logger_provider = LoggerProvider(resource=resource)
                sdk_logger_provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter))
                logger_provider = temporal_providers.wrap_logger_provider(sdk_logger_provider)
                set_logger_provider(logger_provider)
                self._logger_provider = logger_provider
                self._owns_logger = True
                self._attach_logging_handler_unlocked(logging.INFO)

            _ProcessState.globals_set = True
            self._initialized = True
            self._init_pid = os.getpid()
            self._import_failed = False
            self._init_failed_at = None

    def reset_for_testing(self) -> None:
        """Reset owned providers for the next BDD scenario.

        Does not replace OTel globals (first-call-wins); subsequent scenarios
        reuse ``configure_for_testing`` which overwrites class attributes and
        rebuilds processors/readers on the existing global providers when possible.
        """
        with _ProcessState.lock:
            self._detach_logging_handler_unlocked()
            self._metrics.stop_pushgateway_unlocked()
            self._metrics.stop_pull_scrape_unlocked()
            self._shutdown_owned_providers_unlocked()
            self._tracer_provider = None
            self._meter_provider = None
            self._logger_provider = None
            self._owns_tracer = False
            self._owns_meter = False
            self._owns_logger = False
            self._initialized = False
            self._import_failed = False
            self._init_failed_at = None
            self._init_pid = None
            Instrumentation.libraries.clear()
            self._clear_metric_instrument_caches()
            # Keep global textmap propagator; first-call-wins across scenarios.

    def _install_textmap_propagator_unlocked(self, names: Sequence[str]) -> None:
        """Install the named propagators as the global textmap propagator.

        Idempotent. Locks ArchiPy's intended carriers (W3C ``traceparent``, ``tracestate`` and ``baggage`` by default)
        so another library cannot silently replace the default composite before ``init_otel_if_needed``.

        Args:
            names: Propagator names from ``OTEL.PROPAGATORS``.
        """
        if _ProcessState.propagator_installed:
            return
        from opentelemetry.propagate import set_global_textmap

        set_global_textmap(create_propagator(names))
        _ProcessState.propagator_installed = True

    def grpc_client_interceptors(self) -> list[OpenTelemetryClientInterceptor]:
        """Return sync gRPC client interceptors for outbound trace propagation.

        Returns:
            A list containing the contrib client interceptor.

        Raises:
            ImportError: If ``archipy[otel-grpc]`` is not installed.
        """
        try:
            from opentelemetry.instrumentation.grpc import client_interceptor
        except ImportError as exc:
            raise ImportError(_OTEL_GRPC_HINT) from exc
        return [client_interceptor(tracer_provider=self._tracer_provider)]

    def async_grpc_client_interceptors(self) -> list[grpc.aio.ClientInterceptor]:
        """Return async gRPC client interceptors for outbound trace propagation.

        Returns:
            A list of contrib aio client interceptors.

        Raises:
            ImportError: If ``archipy[otel-grpc]`` is not installed.
        """
        try:
            from opentelemetry.instrumentation.grpc import aio_client_interceptors
        except ImportError as exc:
            raise ImportError(_OTEL_GRPC_HINT) from exc
        return list(aio_client_interceptors(tracer_provider=self._tracer_provider))

    def _reset_after_fork(self) -> None:
        """Drop provider references after fork without shutting down parent threads."""
        logger.warning(
            "Process forked after OpenTelemetry init (parent_pid=%s, child_pid=%s); "
            "rebuilding ArchiPy-owned providers. Third-party code using OTel globals may "
            "need re-initialization in the child.",
            self._init_pid,
            os.getpid(),
        )
        self._detach_logging_handler_unlocked()
        # Do not call shutdown() — exporter threads belong to the parent process.
        # Drop scrape/push handles without stopping the parent's workers.
        self._metrics.drop_handles()
        self._tracer_provider = None
        self._meter_provider = None
        self._logger_provider = None
        self._owns_tracer = False
        self._owns_meter = False
        self._owns_logger = False
        self._initialized = False
        _ProcessState.globals_set = False
        self._init_pid = None
        Instrumentation.libraries.clear()
        self._clear_metric_instrument_caches()

    def _clear_metric_instrument_caches(self) -> None:
        """Clear decorator and gRPC metric instrument caches after provider reset."""
        from archipy.helpers.decorators.metrics import clear_instrument_caches
        from archipy.helpers.interceptors.grpc.otel_metrics.server_interceptor import (
            _RpcInstruments,
        )

        clear_instrument_caches()
        _RpcInstruments.clear()

    def _force_flush_unlocked(self, timeout_millis: int) -> bool:
        return provider_lifecycle.flush_providers(
            (self._tracer_provider, self._meter_provider, self._logger_provider),
            timeout_millis,
        )

    def _shutdown_owned_providers_unlocked(self) -> None:
        provider_lifecycle.shutdown_owned(
            (
                (self._tracer_provider, self._owns_tracer),
                (self._meter_provider, self._owns_meter),
                (self._logger_provider, self._owns_logger),
            ),
        )

    def _shutdown_partial(
        self,
        tracer: OwnedTracerProvider | None,
        owns_tracer: bool,
        meter: OwnedMeterProvider | None,
        owns_meter: bool,
        log_provider: OwnedLoggerProvider | None,
        owns_logger: bool,
    ) -> None:
        """Shut down providers built during a failed initialization attempt."""
        provider_lifecycle.shutdown_owned(
            ((tracer, owns_tracer), (meter, owns_meter), (log_provider, owns_logger)),
            "Error shutting down partial OTel provider",
        )

    @staticmethod
    def _is_concrete_provider[T](provider: object, sdk_type: type[T]) -> TypeIs[T]:
        return provider_lifecycle.is_concrete_provider(provider, sdk_type)

    def _is_usable_provider(self, provider: object) -> bool:
        """Return False for providers that were shut down or are otherwise unusable."""
        return provider_lifecycle.is_usable_provider(provider)

    def _mark_provider_shutdown(
        self,
        provider: OwnedTracerProvider | OwnedMeterProvider | OwnedLoggerProvider | None,
    ) -> None:
        provider_lifecycle.mark_provider_shutdown(provider)

    def _existing_concrete_tracer_provider(self) -> OwnedTracerProvider | None:
        return provider_lifecycle.existing_concrete_tracer_provider()

    def _existing_concrete_meter_provider(self) -> OwnedMeterProvider | None:
        return provider_lifecycle.existing_concrete_meter_provider()

    def _existing_concrete_logger_provider(self) -> OwnedLoggerProvider | None:
        return provider_lifecycle.existing_concrete_logger_provider()

    def _create_resource(self, otel: OpentelemetryConfig) -> Resource:
        return create_resource(otel)

    def _acquire_provider[P](
        self,
        *,
        signal_name: str,
        existing: P | None,
        builder: Callable[[], P],
    ) -> tuple[P, bool]:
        """Adopt an existing provider or build a new owned one (see ``provider_lifecycle.acquire_provider``)."""
        return provider_lifecycle.acquire_provider(signal_name=signal_name, existing=existing, builder=builder)

    def _publish_trace_global(
        self,
        provider: OwnedTracerProvider | None,
        owns: bool,
    ) -> tuple[OwnedTracerProvider | None, bool]:
        return provider_lifecycle.publish_trace_global(provider, owns)

    def _publish_metric_global(
        self,
        provider: OwnedMeterProvider | None,
        owns: bool,
    ) -> tuple[OwnedMeterProvider | None, bool]:
        return provider_lifecycle.publish_metric_global(provider, owns)

    def _build_providers(self, config: BaseConfig) -> None:
        """Build providers transactionally and publish only on full success."""
        from opentelemetry._logs import set_logger_provider

        otel = apply_standard_env(config.OTEL)
        resource = self._create_resource(otel)

        new_tracer: OwnedTracerProvider | None = None
        new_meter: OwnedMeterProvider | None = None
        new_logger: OwnedLoggerProvider | None = None
        owns_tracer = False
        owns_meter = False
        owns_logger = False

        try:
            if otel.TRACES_ENABLED:
                new_tracer, owns_tracer = self._acquire_provider(
                    signal_name="trace",
                    existing=self._existing_concrete_tracer_provider(),
                    builder=lambda: self._build_tracer_provider(otel, resource),
                )
            if otel.METRICS_ENABLED:
                new_meter, owns_meter = self._acquire_provider(
                    signal_name="metric",
                    existing=self._existing_concrete_meter_provider(),
                    builder=lambda: self._build_meter_provider(otel, resource),
                )
            if otel.LOGS_ENABLED:
                new_logger, owns_logger = self._acquire_provider(
                    signal_name="log",
                    existing=self._existing_concrete_logger_provider(),
                    builder=lambda: self._build_logger_provider(otel, resource),
                )
        except Exception:
            self._shutdown_partial(new_tracer, owns_tracer, new_meter, owns_meter, new_logger, owns_logger)
            raise

        new_tracer, owns_tracer = self._publish_trace_global(new_tracer, owns_tracer)
        new_meter, owns_meter = self._publish_metric_global(new_meter, owns_meter)
        if owns_logger and new_logger is not None:
            set_logger_provider(new_logger)

        self._tracer_provider = new_tracer
        self._meter_provider = new_meter
        self._logger_provider = new_logger
        self._owns_tracer = owns_tracer
        self._owns_meter = owns_meter
        self._owns_logger = owns_logger
        if new_tracer is not None or new_meter is not None or new_logger is not None:
            _ProcessState.globals_set = True

        self._metrics.maybe_start_pull_scrape_unlocked(
            otel,
            owns_meter=owns_meter,
            new_meter=new_meter,
        )
        self._metrics.maybe_start_pushgateway_unlocked(
            otel,
            owns_meter=owns_meter,
            new_meter=new_meter,
        )

        if otel.LOGS_ENABLED and new_logger is not None:
            self._attach_logging_handler_unlocked(getattr(logging, otel.LOGS_LEVEL.upper(), logging.INFO))

    def _build_tracer_provider(self, otel: OpentelemetryConfig, resource: Resource) -> OwnedTracerProvider:
        return providers.build_tracer_provider(otel, resource, self._create_span_exporter)

    def _build_meter_provider(self, otel: OpentelemetryConfig, resource: Resource) -> OwnedMeterProvider:
        provider, registry = providers.build_meter_provider(otel, resource, self._create_metric_exporter)
        if registry is not None:
            self._metrics.registry = registry
        return provider

    def _build_logger_provider(self, otel: OpentelemetryConfig, resource: Resource) -> OwnedLoggerProvider:
        return providers.build_logger_provider(
            otel,
            resource,
            self._create_console_log_exporter,
            self._create_otlp_log_exporter,
        )

    @staticmethod
    def _create_console_log_exporter() -> LogRecordExporter:
        """Return a console exporter that splits INFO/DEBUG to stdout and WARNING+ to stderr."""
        return create_console_log_exporter()

    @staticmethod
    def _resolve_otlp_endpoint(
        otel: OpentelemetryConfig,
        signal: str,
        override: object | None,
    ) -> str:
        """Resolve the OTLP endpoint for a signal.

        Prefer a per-signal override. For ``http/protobuf``, append ``/v1/{signal}`` when the base URL has no path
        (or only ``/``). gRPC uses the base endpoint as-is.

        Args:
            otel: OpenTelemetry config section.
            signal: One of ``traces``, ``metrics``, or ``logs``.
            override: Optional per-signal endpoint override.

        Returns:
            The resolved endpoint URL.
        """
        return resolve_otlp_endpoint(otel, signal, override)

    def resolve_metrics_endpoint(self, otel: OpentelemetryConfig) -> str:
        """Return the resolved OTLP metrics endpoint for Temporal / callers.

        Args:
            otel: OpenTelemetry config section.

        Returns:
            The resolved metrics endpoint URL.
        """
        return resolve_metrics_endpoint(otel)

    def _create_span_exporter(self, otel: OpentelemetryConfig) -> SpanExporter:
        return create_span_exporter(otel)

    def _create_metric_exporter(self, otel: OpentelemetryConfig) -> MetricExporter:
        return create_metric_exporter(otel)

    def _create_otlp_log_exporter(self, otel: OpentelemetryConfig) -> LogRecordExporter:
        return create_otlp_log_exporter(otel)

    def _attach_logging_handler_unlocked(self, level: int) -> None:
        if self._logging_handler_attached or self._logger_provider is None:
            return
        self._logging_handler = attach_logging_handler(level, self._logger_provider)
        self._logging_handler_attached = True

    def _detach_logging_handler_unlocked(self) -> None:
        handler = self._logging_handler
        if self._logging_handler_attached and handler is not None:
            detach_logging_handler(handler)
        self._logging_handler = None
        self._logging_handler_attached = False

    def _instrument_installed_libraries(self, config: BaseConfig) -> None:
        """Best-effort auto-instrumentation of installed contrib packages with ArchiPy's providers."""
        Instrumentation.instrument_installed_libraries(
            config,
            tracer_provider=self._tracer_provider,
            meter_provider=self._meter_provider,
        )

    def _guard_redis_search_attributes(self) -> None:
        """Keep ``opentelemetry-instrumentation-redis`` from raising into the application on a RESP3 search reply."""
        Instrumentation.guard_redis_search_attributes()

    def restore_redis_search_attributes(self) -> None:
        """Undo :meth:`_guard_redis_search_attributes` (used by tests that need the upstream behaviour)."""
        Instrumentation.restore_redis_search_attributes()

    def _register_atexit(self) -> None:
        if self._atexit_registered:
            return

        def _shutdown() -> None:
            try:
                self.shutdown()
            except Exception:
                logger.debug("Error during OTel atexit shutdown", exc_info=True)

        atexit.register(_shutdown)
        self._atexit_registered = True

archipy.helpers.utils.otel_utils.OtelRuntime.duration_histogram_buckets staticmethod

duration_histogram_buckets() -> tuple[float, ...]

Return the explicit bucket boundaries, in seconds, of the duration histograms ArchiPy records.

Reads OTEL.METRICS_DURATION_BUCKETS_S from the global config, and falls back to DURATION_HISTOGRAM_BUCKETS_S while no global config is set.

Returns:

Type Description
tuple[float, ...]

The bucket boundaries.

Source code in archipy/helpers/utils/_otel/runtime.py
@staticmethod
def duration_histogram_buckets() -> tuple[float, ...]:
    """Return the explicit bucket boundaries, in seconds, of the duration histograms ArchiPy records.

    Reads ``OTEL.METRICS_DURATION_BUCKETS_S`` from the global config, and falls back to
    ``DURATION_HISTOGRAM_BUCKETS_S`` while no global config is set.

    Returns:
        The bucket boundaries.
    """
    from archipy.configs.base_config import BaseConfig

    try:
        return tuple(BaseConfig.global_config().OTEL.METRICS_DURATION_BUCKETS_S)
    except AssertionError:
        return DURATION_HISTOGRAM_BUCKETS_S

archipy.helpers.utils.otel_utils.OtelRuntime.error_type_for_exception staticmethod

error_type_for_exception(exception: BaseException) -> str

Return the error.type attribute value of a failure (BaseError.code or the exception class name).

Source code in archipy/helpers/utils/_otel/runtime.py
@staticmethod
def error_type_for_exception(exception: BaseException) -> str:
    """Return the ``error.type`` attribute value of a failure (``BaseError.code`` or the exception class name)."""
    return error_type_for_exception(exception)

archipy.helpers.utils.otel_utils.OtelRuntime.is_otel_enabled staticmethod

is_otel_enabled(config: BaseConfig) -> bool

Return True when the OTel master switch is enabled.

Parameters:

Name Type Description Default
config BaseConfig

Application configuration.

required

Returns:

Type Description
bool

True if config.OTEL.IS_ENABLED is True.

Source code in archipy/helpers/utils/_otel/runtime.py
@staticmethod
def is_otel_enabled(config: BaseConfig) -> bool:
    """Return True when the OTel master switch is enabled.

    Args:
        config: Application configuration.

    Returns:
        True if ``config.OTEL.IS_ENABLED`` is True.
    """
    return bool(config.OTEL.IS_ENABLED)

archipy.helpers.utils.otel_utils.OtelRuntime.is_traces_enabled staticmethod

is_traces_enabled(config: BaseConfig) -> bool

Return True when tracing should be active.

Parameters:

Name Type Description Default
config BaseConfig

Application configuration.

required

Returns:

Type Description
bool

True if the master switch and TRACES_ENABLED are both True.

Source code in archipy/helpers/utils/_otel/runtime.py
@staticmethod
def is_traces_enabled(config: BaseConfig) -> bool:
    """Return True when tracing should be active.

    Args:
        config: Application configuration.

    Returns:
        True if the master switch and ``TRACES_ENABLED`` are both True.
    """
    return bool(config.OTEL.IS_ENABLED and config.OTEL.TRACES_ENABLED)

archipy.helpers.utils.otel_utils.OtelRuntime.is_metrics_enabled staticmethod

is_metrics_enabled(config: BaseConfig) -> bool

Return True when metrics should be active.

Parameters:

Name Type Description Default
config BaseConfig

Application configuration.

required

Returns:

Type Description
bool

True if the master switch and METRICS_ENABLED are both True.

Source code in archipy/helpers/utils/_otel/runtime.py
@staticmethod
def is_metrics_enabled(config: BaseConfig) -> bool:
    """Return True when metrics should be active.

    Args:
        config: Application configuration.

    Returns:
        True if the master switch and ``METRICS_ENABLED`` are both True.
    """
    return bool(config.OTEL.IS_ENABLED and config.OTEL.METRICS_ENABLED)

archipy.helpers.utils.otel_utils.OtelRuntime.is_logs_enabled staticmethod

is_logs_enabled(config: BaseConfig) -> bool

Return True when log export should be active.

Parameters:

Name Type Description Default
config BaseConfig

Application configuration.

required

Returns:

Type Description
bool

True if the master switch and LOGS_ENABLED are both True.

Source code in archipy/helpers/utils/_otel/runtime.py
@staticmethod
def is_logs_enabled(config: BaseConfig) -> bool:
    """Return True when log export should be active.

    Args:
        config: Application configuration.

    Returns:
        True if the master switch and ``LOGS_ENABLED`` are both True.
    """
    return bool(config.OTEL.IS_ENABLED and config.OTEL.LOGS_ENABLED)

archipy.helpers.utils.otel_utils.OtelRuntime.import_failed

import_failed() -> bool

Return True when OTel initialization failed due to missing packages.

Source code in archipy/helpers/utils/_otel/runtime.py
def import_failed(self) -> bool:
    """Return True when OTel initialization failed due to missing packages."""
    return self._import_failed

archipy.helpers.utils.otel_utils.OtelRuntime.get_tracer

get_tracer(name: str) -> Tracer | _NoOpTracer

Return a tracer from the owned tracer provider (or a no-op tracer).

Parameters:

Name Type Description Default
name str

Instrumentation scope name.

required

Returns:

Type Description
Tracer | _NoOpTracer

An OpenTelemetry Tracer instance, or a no-op stub when the

Tracer | _NoOpTracer

opentelemetry package is not installed.

Source code in archipy/helpers/utils/_otel/runtime.py
def get_tracer(self, name: str) -> Tracer | _NoOpTracer:
    """Return a tracer from the owned tracer provider (or a no-op tracer).

    Args:
        name: Instrumentation scope name.

    Returns:
        An OpenTelemetry ``Tracer`` instance, or a no-op stub when the
        ``opentelemetry`` package is not installed.
    """
    try:
        from opentelemetry import trace
    except ImportError:
        return _NoOpTracer()

    if self._tracer_provider is not None:
        return self._tracer_provider.get_tracer(name)
    return trace.get_tracer(name)

archipy.helpers.utils.otel_utils.OtelRuntime.get_meter

get_meter(name: str) -> Meter | _NoOpMeter

Return a meter from the owned meter provider (or a no-op meter).

Parameters:

Name Type Description Default
name str

Instrumentation scope name.

required

Returns:

Type Description
Meter | _NoOpMeter

An OpenTelemetry Meter instance, or a no-op stub when the

Meter | _NoOpMeter

opentelemetry package is not installed.

Source code in archipy/helpers/utils/_otel/runtime.py
def get_meter(self, name: str) -> Meter | _NoOpMeter:
    """Return a meter from the owned meter provider (or a no-op meter).

    Args:
        name: Instrumentation scope name.

    Returns:
        An OpenTelemetry ``Meter`` instance, or a no-op stub when the
        ``opentelemetry`` package is not installed.
    """
    try:
        from opentelemetry import metrics
    except ImportError:
        return _NoOpMeter()

    if self._meter_provider is not None:
        return self._meter_provider.get_meter(name)
    return metrics.get_meter(name)

archipy.helpers.utils.otel_utils.OtelRuntime.tracer_provider

tracer_provider() -> OwnedTracerProvider | None

Return the owned or adopted tracer provider, if initialized.

Source code in archipy/helpers/utils/_otel/runtime.py
def tracer_provider(self) -> OwnedTracerProvider | None:
    """Return the owned or adopted tracer provider, if initialized."""
    return self._tracer_provider

archipy.helpers.utils.otel_utils.OtelRuntime.meter_provider

meter_provider() -> OwnedMeterProvider | None

Return the owned or adopted meter provider, if initialized.

Source code in archipy/helpers/utils/_otel/runtime.py
def meter_provider(self) -> OwnedMeterProvider | None:
    """Return the owned or adopted meter provider, if initialized."""
    return self._meter_provider

archipy.helpers.utils.otel_utils.OtelRuntime.logger_provider

logger_provider() -> OwnedLoggerProvider | None

Return the owned or adopted logger provider, if initialized.

Source code in archipy/helpers/utils/_otel/runtime.py
def logger_provider(self) -> OwnedLoggerProvider | None:
    """Return the owned or adopted logger provider, if initialized."""
    return self._logger_provider

archipy.helpers.utils.otel_utils.OtelRuntime.metrics_registry

metrics_registry() -> CollectorRegistry | None

Return the Prometheus CollectorRegistry for pull/pushgateway metrics.

Returns:

Type Description
CollectorRegistry | None

The registry when METRICS_EXPORTER is pull or pushgateway

CollectorRegistry | None

and providers were built, otherwise None.

Source code in archipy/helpers/utils/_otel/runtime.py
def metrics_registry(self) -> CollectorRegistry | None:
    """Return the Prometheus ``CollectorRegistry`` for pull/pushgateway metrics.

    Returns:
        The registry when ``METRICS_EXPORTER`` is ``pull`` or ``pushgateway``
        and providers were built, otherwise ``None``.
    """
    return self._metrics.registry

archipy.helpers.utils.otel_utils.OtelRuntime.status_for_exception staticmethod

status_for_exception(
    exception: BaseException,
) -> Status | None

Map an exception to an OpenTelemetry Status, or None for UNSET.

BaseError with http_status below 500 leaves status UNSET (handled client error). All other exceptions become StatusCode.ERROR.

Parameters:

Name Type Description Default
exception BaseException

The exception raised during a span.

required

Returns:

Type Description
Status | None

An OpenTelemetry Status instance, or None to leave status UNSET.

Source code in archipy/helpers/utils/_otel/runtime.py
@staticmethod
def status_for_exception(exception: BaseException) -> Status | None:
    """Map an exception to an OpenTelemetry ``Status``, or ``None`` for UNSET.

    ``BaseError`` with ``http_status`` below 500 leaves status UNSET (handled client error). All other exceptions
    become ``StatusCode.ERROR``.

    Args:
        exception: The exception raised during a span.

    Returns:
        An OpenTelemetry ``Status`` instance, or ``None`` to leave status UNSET.
    """
    return status_for_exception(exception)

archipy.helpers.utils.otel_utils.OtelRuntime.metric_status_for_exception staticmethod

metric_status_for_exception(
    exception: BaseException,
) -> str

Map an exception to a metric status attribute value ("ok" or "error").

Aligns with status_for_exception: handled client BaseError values record as ok. Callers that catch asyncio.CancelledError should record cancelled before invoking this helper.

Parameters:

Name Type Description Default
exception BaseException

The exception raised during the instrumented call.

required

Returns:

Type Description
str

"ok" or "error".

Source code in archipy/helpers/utils/_otel/runtime.py
@staticmethod
def metric_status_for_exception(exception: BaseException) -> str:
    """Map an exception to a metric ``status`` attribute value (``"ok"`` or ``"error"``).

    Aligns with ``status_for_exception``: handled client ``BaseError`` values record as ``ok``. Callers that catch
    ``asyncio.CancelledError`` should record ``cancelled`` before invoking this helper.

    Args:
        exception: The exception raised during the instrumented call.

    Returns:
        ``"ok"`` or ``"error"``.
    """
    return metric_status_for_exception(exception)

archipy.helpers.utils.otel_utils.OtelRuntime.status_for_cancellation staticmethod

status_for_cancellation() -> Status

Return an ERROR status for asyncio task cancellation.

Returns:

Type Description
Status

An OpenTelemetry Status with StatusCode.ERROR.

Source code in archipy/helpers/utils/_otel/runtime.py
@staticmethod
def status_for_cancellation() -> Status:
    """Return an ERROR status for asyncio task cancellation.

    Returns:
        An OpenTelemetry ``Status`` with ``StatusCode.ERROR``.
    """
    return status_for_cancellation()

archipy.helpers.utils.otel_utils.OtelRuntime.init_otel_if_needed

init_otel_if_needed(config: BaseConfig) -> None

Initialize OTel providers once (idempotent, thread-safe, fork-aware).

Parameters:

Name Type Description Default
config BaseConfig

Application configuration.

required
Source code in archipy/helpers/utils/_otel/runtime.py
def init_otel_if_needed(self, config: BaseConfig) -> None:
    """Initialize OTel providers once (idempotent, thread-safe, fork-aware).

    Args:
        config: Application configuration.
    """
    if not config.OTEL.IS_ENABLED or self._import_failed or sdk_disabled(config.OTEL):
        return

    current_pid = os.getpid()
    if self._initialized and self._init_pid == current_pid:
        return
    if self._init_failed_recently():
        return

    with _ProcessState.lock:
        if not config.OTEL.IS_ENABLED or self._import_failed:
            return
        if self._initialized and self._init_pid == current_pid:
            return
        if self._init_failed_recently():
            return
        if self._initialized and self._init_pid is not None and self._init_pid != current_pid:
            self._reset_after_fork()
        try:
            self._install_textmap_propagator_unlocked(apply_standard_env(config.OTEL).PROPAGATORS)
            self._build_providers(config)
            self._instrument_installed_libraries(config)
            self._register_atexit()
            self._initialized = True
            self._init_pid = current_pid
            self._init_failed_at = None
        except ImportError:
            self._import_failed = True
            logger.warning(
                "OTEL.IS_ENABLED is True but OpenTelemetry is not installed; telemetry disabled. %s",
                _OTEL_INSTALL_HINT,
            )
        except Exception:
            self._metrics.stop_pushgateway_unlocked()
            self._metrics.stop_pull_scrape_unlocked()
            self._init_failed_at = time.monotonic()
            logger.exception("Failed to initialize OpenTelemetry")

archipy.helpers.utils.otel_utils.OtelRuntime.force_flush

force_flush(
    timeout_millis: int = _DEFAULT_FLUSH_TIMEOUT_MS,
) -> bool

Force-flush all known providers.

Parameters:

Name Type Description Default
timeout_millis int

Maximum time to wait per provider.

_DEFAULT_FLUSH_TIMEOUT_MS

Returns:

Type Description
bool

True when every provider flushed successfully (or none exist).

Source code in archipy/helpers/utils/_otel/runtime.py
def force_flush(self, timeout_millis: int = _DEFAULT_FLUSH_TIMEOUT_MS) -> bool:
    """Force-flush all known providers.

    Args:
        timeout_millis: Maximum time to wait per provider.

    Returns:
        True when every provider flushed successfully (or none exist).
    """
    # Flush outside the lock: a slow exporter must not block initialization or shutdown on other threads.
    with _ProcessState.lock:
        known = (self._tracer_provider, self._meter_provider, self._logger_provider)
    return provider_lifecycle.flush_providers(known, timeout_millis)

archipy.helpers.utils.otel_utils.OtelRuntime.shutdown

shutdown() -> None

Flush and shut down ArchiPy-owned providers; detach logging handler.

Adopted (borrowed) providers are left running. Idempotent.

Source code in archipy/helpers/utils/_otel/runtime.py
def shutdown(self) -> None:
    """Flush and shut down ArchiPy-owned providers; detach logging handler.

    Adopted (borrowed) providers are left running. Idempotent.
    """
    with _ProcessState.lock:
        self._force_flush_unlocked(_DEFAULT_FLUSH_TIMEOUT_MS)
        self._detach_logging_handler_unlocked()
        self._metrics.stop_pushgateway_unlocked()
        self._metrics.stop_pull_scrape_unlocked()
        self._shutdown_owned_providers_unlocked()
        self._tracer_provider = None
        self._meter_provider = None
        self._logger_provider = None
        self._owns_tracer = False
        self._owns_meter = False
        self._owns_logger = False
        self._initialized = False
        self._init_pid = None
        self._init_failed_at = None
        Instrumentation.libraries.clear()
        self._clear_metric_instrument_caches()

archipy.helpers.utils.otel_utils.OtelRuntime.configure_for_testing

configure_for_testing(
    span_exporter: SpanExporter | None = None,
    metric_reader: MetricReader | None = None,
    log_exporter: LogRecordExporter | None = None,
    *,
    service_name: str = "archipy-test",
) -> None

Swap in in-memory providers for BDD / unit tests.

Parameters:

Name Type Description Default
span_exporter SpanExporter | None

Optional span exporter (e.g. InMemorySpanExporter).

None
metric_reader MetricReader | None

Optional metric reader (e.g. InMemoryMetricReader).

None
log_exporter LogRecordExporter | None

Optional log exporter (e.g. InMemoryLogExporter).

None
service_name str

Resource service name for the test providers.

'archipy-test'
Source code in archipy/helpers/utils/_otel/runtime.py
def configure_for_testing(
    self,
    span_exporter: SpanExporter | None = None,
    metric_reader: MetricReader | None = None,
    log_exporter: LogRecordExporter | None = None,
    *,
    service_name: str = "archipy-test",
) -> None:
    """Swap in in-memory providers for BDD / unit tests.

    Args:
        span_exporter: Optional span exporter (e.g. ``InMemorySpanExporter``).
        metric_reader: Optional metric reader (e.g. ``InMemoryMetricReader``).
        log_exporter: Optional log exporter (e.g. ``InMemoryLogExporter``).
        service_name: Resource service name for the test providers.
    """
    from opentelemetry import metrics, trace
    from opentelemetry.sdk.resources import Resource
    from opentelemetry.sdk.trace.export import SimpleSpanProcessor

    with _ProcessState.lock:
        self._install_textmap_propagator_unlocked(_DEFAULT_PROPAGATORS)
        resource = Resource.create({"service.name": service_name})
        tracer_provider = temporal_providers.new_tracer_provider(resource)
        if span_exporter is not None:
            tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
        self._tracer_provider = tracer_provider
        self._owns_tracer = True
        if not _ProcessState.globals_set:
            trace.set_tracer_provider(tracer_provider)

        if metric_reader is not None:
            from opentelemetry.sdk.metrics import MeterProvider

            meter_provider = temporal_providers.wrap_meter_provider(
                MeterProvider(resource=resource, metric_readers=[metric_reader]),
            )
            self._meter_provider = meter_provider
            self._owns_meter = True
            if not _ProcessState.globals_set:
                metrics.set_meter_provider(meter_provider)

        if log_exporter is not None:
            from opentelemetry._logs import set_logger_provider
            from opentelemetry.sdk._logs import LoggerProvider
            from opentelemetry.sdk._logs.export import SimpleLogRecordProcessor

            sdk_logger_provider = LoggerProvider(resource=resource)
            sdk_logger_provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter))
            logger_provider = temporal_providers.wrap_logger_provider(sdk_logger_provider)
            set_logger_provider(logger_provider)
            self._logger_provider = logger_provider
            self._owns_logger = True
            self._attach_logging_handler_unlocked(logging.INFO)

        _ProcessState.globals_set = True
        self._initialized = True
        self._init_pid = os.getpid()
        self._import_failed = False
        self._init_failed_at = None

archipy.helpers.utils.otel_utils.OtelRuntime.reset_for_testing

reset_for_testing() -> None

Reset owned providers for the next BDD scenario.

Does not replace OTel globals (first-call-wins); subsequent scenarios reuse configure_for_testing which overwrites class attributes and rebuilds processors/readers on the existing global providers when possible.

Source code in archipy/helpers/utils/_otel/runtime.py
def reset_for_testing(self) -> None:
    """Reset owned providers for the next BDD scenario.

    Does not replace OTel globals (first-call-wins); subsequent scenarios
    reuse ``configure_for_testing`` which overwrites class attributes and
    rebuilds processors/readers on the existing global providers when possible.
    """
    with _ProcessState.lock:
        self._detach_logging_handler_unlocked()
        self._metrics.stop_pushgateway_unlocked()
        self._metrics.stop_pull_scrape_unlocked()
        self._shutdown_owned_providers_unlocked()
        self._tracer_provider = None
        self._meter_provider = None
        self._logger_provider = None
        self._owns_tracer = False
        self._owns_meter = False
        self._owns_logger = False
        self._initialized = False
        self._import_failed = False
        self._init_failed_at = None
        self._init_pid = None
        Instrumentation.libraries.clear()
        self._clear_metric_instrument_caches()

archipy.helpers.utils.otel_utils.OtelRuntime.grpc_client_interceptors

grpc_client_interceptors() -> list[
    OpenTelemetryClientInterceptor
]

Return sync gRPC client interceptors for outbound trace propagation.

Returns:

Type Description
list[OpenTelemetryClientInterceptor]

A list containing the contrib client interceptor.

Raises:

Type Description
ImportError

If archipy[otel-grpc] is not installed.

Source code in archipy/helpers/utils/_otel/runtime.py
def grpc_client_interceptors(self) -> list[OpenTelemetryClientInterceptor]:
    """Return sync gRPC client interceptors for outbound trace propagation.

    Returns:
        A list containing the contrib client interceptor.

    Raises:
        ImportError: If ``archipy[otel-grpc]`` is not installed.
    """
    try:
        from opentelemetry.instrumentation.grpc import client_interceptor
    except ImportError as exc:
        raise ImportError(_OTEL_GRPC_HINT) from exc
    return [client_interceptor(tracer_provider=self._tracer_provider)]

archipy.helpers.utils.otel_utils.OtelRuntime.async_grpc_client_interceptors

async_grpc_client_interceptors() -> list[
    grpc.aio.ClientInterceptor
]

Return async gRPC client interceptors for outbound trace propagation.

Returns:

Type Description
list[ClientInterceptor]

A list of contrib aio client interceptors.

Raises:

Type Description
ImportError

If archipy[otel-grpc] is not installed.

Source code in archipy/helpers/utils/_otel/runtime.py
def async_grpc_client_interceptors(self) -> list[grpc.aio.ClientInterceptor]:
    """Return async gRPC client interceptors for outbound trace propagation.

    Returns:
        A list of contrib aio client interceptors.

    Raises:
        ImportError: If ``archipy[otel-grpc]`` is not installed.
    """
    try:
        from opentelemetry.instrumentation.grpc import aio_client_interceptors
    except ImportError as exc:
        raise ImportError(_OTEL_GRPC_HINT) from exc
    return list(aio_client_interceptors(tracer_provider=self._tracer_provider))

archipy.helpers.utils.otel_utils.OtelRuntime.resolve_metrics_endpoint

resolve_metrics_endpoint(otel: OpentelemetryConfig) -> str

Return the resolved OTLP metrics endpoint for Temporal / callers.

Parameters:

Name Type Description Default
otel OpentelemetryConfig

OpenTelemetry config section.

required

Returns:

Type Description
str

The resolved metrics endpoint URL.

Source code in archipy/helpers/utils/_otel/runtime.py
def resolve_metrics_endpoint(self, otel: OpentelemetryConfig) -> str:
    """Return the resolved OTLP metrics endpoint for Temporal / callers.

    Args:
        otel: OpenTelemetry config section.

    Returns:
        The resolved metrics endpoint URL.
    """
    return resolve_metrics_endpoint(otel)

archipy.helpers.utils.otel_utils.OtelRuntime.restore_redis_search_attributes

restore_redis_search_attributes() -> None

Undo :meth:_guard_redis_search_attributes (used by tests that need the upstream behaviour).

Source code in archipy/helpers/utils/_otel/runtime.py
def restore_redis_search_attributes(self) -> None:
    """Undo :meth:`_guard_redis_search_attributes` (used by tests that need the upstream behaviour)."""
    Instrumentation.restore_redis_search_attributes()

archipy.helpers.utils.otel_utils.OtelUtils

Idempotent OpenTelemetry provider management and helpers.

A facade over the default OtelRuntime, which owns the TracerProvider / MeterProvider / LoggerProvider references and passes them explicitly to instrumentors. Globals are set once for third-party interop (e.g. Temporal TracingInterceptor), but internal callers use get_tracer / get_meter against owned or adopted providers. Everything process-wide (the OpenTelemetry globals, the propagator, the instrumented libraries) is shared by every runtime; set_runtime swaps the object that owns the providers, for example to give a test its own.

Providers are built programmatically from BaseConfig.OTEL. The SDK's OTEL_* autoconfiguration is not used; with OTEL.USE_STANDARD_ENV the standard variables fill in the settings the config leaves unset. On init, ArchiPy installs the composite textmap propagator named by OTEL.PROPAGATORS (W3C TraceContext + Baggage by default, idempotent) so cross-service carriers stay locked to the intended ones.

Initialization is transactional: providers are published only after all enabled signals succeed. Pre-existing concrete global providers are adopted (not replaced) so ArchiPy and third-party libraries share context. Only ArchiPy-owned providers are shut down.

Source code in archipy/helpers/utils/otel_utils.py
class OtelUtils(metaclass=_OtelUtilsMeta):
    """Idempotent OpenTelemetry provider management and helpers.

    A facade over the default ``OtelRuntime``, which owns the ``TracerProvider`` / ``MeterProvider`` /
    ``LoggerProvider`` references and passes them explicitly to instrumentors. Globals are set once for third-party
    interop (e.g. Temporal ``TracingInterceptor``), but internal callers use ``get_tracer`` / ``get_meter`` against
    owned or adopted providers. Everything process-wide (the OpenTelemetry globals, the propagator, the instrumented
    libraries) is shared by every runtime; ``set_runtime`` swaps the object that owns the providers, for example to
    give a test its own.

    Providers are built programmatically from ``BaseConfig.OTEL``. The SDK's ``OTEL_*`` autoconfiguration is not used;
    with ``OTEL.USE_STANDARD_ENV`` the standard variables fill in the settings the config leaves unset. On init,
    ArchiPy installs the composite textmap propagator named by ``OTEL.PROPAGATORS`` (W3C TraceContext + Baggage by
    default, idempotent) so cross-service carriers stay locked to the intended ones.

    Initialization is transactional: providers are published only after all enabled signals succeed. Pre-existing
    concrete global providers are adopted (not replaced) so ArchiPy and third-party libraries share context. Only
    ArchiPy-owned providers are shut down.
    """

    _runtime: ClassVar[OtelRuntime] = OtelRuntime()

    @classmethod
    def runtime(cls) -> OtelRuntime:
        """Return the runtime the facade currently delegates to."""
        return cls._runtime

    @classmethod
    def set_runtime(cls, runtime: OtelRuntime) -> OtelRuntime:
        """Make ``runtime`` the one the facade delegates to.

        Args:
            runtime: The runtime to use from now on.

        Returns:
            The runtime that was in use before, so the caller can put it back.
        """
        previous = cls._runtime
        type.__setattr__(cls, "_runtime", runtime)
        return previous

    @staticmethod
    def duration_histogram_buckets() -> tuple[float, ...]:
        """Return the explicit bucket boundaries, in seconds, of the duration histograms ArchiPy records."""
        return OtelRuntime.duration_histogram_buckets()

    @staticmethod
    def error_type_for_exception(exception: BaseException) -> str:
        """Return the ``error.type`` attribute value of a failure (``BaseError.code`` or the exception class name)."""
        return OtelRuntime.error_type_for_exception(exception)

    @staticmethod
    def is_otel_enabled(config: BaseConfig) -> bool:
        """Return whether OpenTelemetry is enabled in ``config``."""
        return OtelRuntime.is_otel_enabled(config)

    @staticmethod
    def is_traces_enabled(config: BaseConfig) -> bool:
        """Return whether OpenTelemetry and its traces are enabled in ``config``."""
        return OtelRuntime.is_traces_enabled(config)

    @staticmethod
    def is_metrics_enabled(config: BaseConfig) -> bool:
        """Return whether OpenTelemetry and its metrics are enabled in ``config``."""
        return OtelRuntime.is_metrics_enabled(config)

    @staticmethod
    def is_logs_enabled(config: BaseConfig) -> bool:
        """Return whether OpenTelemetry and its logs are enabled in ``config``."""
        return OtelRuntime.is_logs_enabled(config)

    @classmethod
    def import_failed(cls) -> bool:
        """Return whether OpenTelemetry could not be imported when initialization was attempted."""
        return cls._runtime.import_failed()

    @classmethod
    def get_tracer(cls, name: str) -> Tracer | _NoOpTracer:
        """Return a tracer from the owned tracer provider (or a no-op tracer)."""
        return cls._runtime.get_tracer(name)

    @classmethod
    def get_meter(cls, name: str) -> Meter | _NoOpMeter:
        """Return a meter from the owned meter provider (or a no-op meter)."""
        return cls._runtime.get_meter(name)

    @classmethod
    def tracer_provider(cls) -> OwnedTracerProvider | None:
        """Return the owned or adopted tracer provider, if any."""
        return cls._runtime.tracer_provider()

    @classmethod
    def meter_provider(cls) -> OwnedMeterProvider | None:
        """Return the owned or adopted meter provider, if any."""
        return cls._runtime.meter_provider()

    @classmethod
    def logger_provider(cls) -> OwnedLoggerProvider | None:
        """Return the owned or adopted logger provider, if any."""
        return cls._runtime.logger_provider()

    @classmethod
    def metrics_registry(cls) -> CollectorRegistry | None:
        """Return the Prometheus registry of the pull or Pushgateway metrics exporter, if any."""
        return cls._runtime.metrics_registry()

    @staticmethod
    def status_for_exception(exception: BaseException) -> Status | None:
        """Map an exception to an OpenTelemetry ``Status``, or ``None`` for UNSET."""
        return OtelRuntime.status_for_exception(exception)

    @staticmethod
    def metric_status_for_exception(exception: BaseException) -> str:
        """Map an exception to a metric ``status`` attribute value (``"ok"`` or ``"error"``)."""
        return OtelRuntime.metric_status_for_exception(exception)

    @staticmethod
    def status_for_cancellation() -> Status:
        """Return an ERROR status for asyncio task cancellation."""
        return OtelRuntime.status_for_cancellation()

    @classmethod
    def init_otel_if_needed(cls, config: BaseConfig) -> None:
        """Build and publish the providers once per process, as ``config`` asks."""
        cls._runtime.init_otel_if_needed(config)

    @classmethod
    def force_flush(cls, timeout_millis: int = 30_000) -> bool:
        """Flush the owned providers; return whether every flush succeeded."""
        return cls._runtime.force_flush(timeout_millis)

    @classmethod
    def shutdown(cls) -> None:
        """Shut down the providers this runtime owns and stop its metrics workers."""
        cls._runtime.shutdown()

    @classmethod
    def configure_for_testing(
        cls,
        span_exporter: SpanExporter | None = None,
        metric_reader: MetricReader | None = None,
        log_exporter: LogRecordExporter | None = None,
        *,
        service_name: str = "archipy-test",
    ) -> None:
        """Install in-memory providers for a test."""
        cls._runtime.configure_for_testing(span_exporter, metric_reader, log_exporter, service_name=service_name)

    @classmethod
    def reset_for_testing(cls) -> None:
        """Reset the owned providers for the next test scenario."""
        cls._runtime.reset_for_testing()

    @classmethod
    def grpc_client_interceptors(cls) -> list[OpenTelemetryClientInterceptor]:
        """Return sync gRPC client interceptors for outbound trace propagation."""
        return cls._runtime.grpc_client_interceptors()

    @classmethod
    def async_grpc_client_interceptors(cls) -> list[grpc.aio.ClientInterceptor]:
        """Return async gRPC client interceptors for outbound trace propagation."""
        return cls._runtime.async_grpc_client_interceptors()

    @classmethod
    def resolve_metrics_endpoint(cls, otel: OpentelemetryConfig) -> str:
        """Return the resolved OTLP metrics endpoint for Temporal / callers."""
        return cls._runtime.resolve_metrics_endpoint(otel)

    @classmethod
    def restore_redis_search_attributes(cls) -> None:
        """Undo the guard around the Redis instrumentor's search helper (for tests)."""
        cls._runtime.restore_redis_search_attributes()

archipy.helpers.utils.otel_utils.OtelUtils.runtime classmethod

runtime() -> OtelRuntime

Return the runtime the facade currently delegates to.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def runtime(cls) -> OtelRuntime:
    """Return the runtime the facade currently delegates to."""
    return cls._runtime

archipy.helpers.utils.otel_utils.OtelUtils.set_runtime classmethod

set_runtime(runtime: OtelRuntime) -> OtelRuntime

Make runtime the one the facade delegates to.

Parameters:

Name Type Description Default
runtime OtelRuntime

The runtime to use from now on.

required

Returns:

Type Description
OtelRuntime

The runtime that was in use before, so the caller can put it back.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def set_runtime(cls, runtime: OtelRuntime) -> OtelRuntime:
    """Make ``runtime`` the one the facade delegates to.

    Args:
        runtime: The runtime to use from now on.

    Returns:
        The runtime that was in use before, so the caller can put it back.
    """
    previous = cls._runtime
    type.__setattr__(cls, "_runtime", runtime)
    return previous

archipy.helpers.utils.otel_utils.OtelUtils.duration_histogram_buckets staticmethod

duration_histogram_buckets() -> tuple[float, ...]

Return the explicit bucket boundaries, in seconds, of the duration histograms ArchiPy records.

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def duration_histogram_buckets() -> tuple[float, ...]:
    """Return the explicit bucket boundaries, in seconds, of the duration histograms ArchiPy records."""
    return OtelRuntime.duration_histogram_buckets()

archipy.helpers.utils.otel_utils.OtelUtils.error_type_for_exception staticmethod

error_type_for_exception(exception: BaseException) -> str

Return the error.type attribute value of a failure (BaseError.code or the exception class name).

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def error_type_for_exception(exception: BaseException) -> str:
    """Return the ``error.type`` attribute value of a failure (``BaseError.code`` or the exception class name)."""
    return OtelRuntime.error_type_for_exception(exception)

archipy.helpers.utils.otel_utils.OtelUtils.is_otel_enabled staticmethod

is_otel_enabled(config: BaseConfig) -> bool

Return whether OpenTelemetry is enabled in config.

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def is_otel_enabled(config: BaseConfig) -> bool:
    """Return whether OpenTelemetry is enabled in ``config``."""
    return OtelRuntime.is_otel_enabled(config)

archipy.helpers.utils.otel_utils.OtelUtils.is_traces_enabled staticmethod

is_traces_enabled(config: BaseConfig) -> bool

Return whether OpenTelemetry and its traces are enabled in config.

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def is_traces_enabled(config: BaseConfig) -> bool:
    """Return whether OpenTelemetry and its traces are enabled in ``config``."""
    return OtelRuntime.is_traces_enabled(config)

archipy.helpers.utils.otel_utils.OtelUtils.is_metrics_enabled staticmethod

is_metrics_enabled(config: BaseConfig) -> bool

Return whether OpenTelemetry and its metrics are enabled in config.

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def is_metrics_enabled(config: BaseConfig) -> bool:
    """Return whether OpenTelemetry and its metrics are enabled in ``config``."""
    return OtelRuntime.is_metrics_enabled(config)

archipy.helpers.utils.otel_utils.OtelUtils.is_logs_enabled staticmethod

is_logs_enabled(config: BaseConfig) -> bool

Return whether OpenTelemetry and its logs are enabled in config.

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def is_logs_enabled(config: BaseConfig) -> bool:
    """Return whether OpenTelemetry and its logs are enabled in ``config``."""
    return OtelRuntime.is_logs_enabled(config)

archipy.helpers.utils.otel_utils.OtelUtils.import_failed classmethod

import_failed() -> bool

Return whether OpenTelemetry could not be imported when initialization was attempted.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def import_failed(cls) -> bool:
    """Return whether OpenTelemetry could not be imported when initialization was attempted."""
    return cls._runtime.import_failed()

archipy.helpers.utils.otel_utils.OtelUtils.get_tracer classmethod

get_tracer(name: str) -> Tracer | _NoOpTracer

Return a tracer from the owned tracer provider (or a no-op tracer).

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def get_tracer(cls, name: str) -> Tracer | _NoOpTracer:
    """Return a tracer from the owned tracer provider (or a no-op tracer)."""
    return cls._runtime.get_tracer(name)

archipy.helpers.utils.otel_utils.OtelUtils.get_meter classmethod

get_meter(name: str) -> Meter | _NoOpMeter

Return a meter from the owned meter provider (or a no-op meter).

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def get_meter(cls, name: str) -> Meter | _NoOpMeter:
    """Return a meter from the owned meter provider (or a no-op meter)."""
    return cls._runtime.get_meter(name)

archipy.helpers.utils.otel_utils.OtelUtils.tracer_provider classmethod

tracer_provider() -> OwnedTracerProvider | None

Return the owned or adopted tracer provider, if any.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def tracer_provider(cls) -> OwnedTracerProvider | None:
    """Return the owned or adopted tracer provider, if any."""
    return cls._runtime.tracer_provider()

archipy.helpers.utils.otel_utils.OtelUtils.meter_provider classmethod

meter_provider() -> OwnedMeterProvider | None

Return the owned or adopted meter provider, if any.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def meter_provider(cls) -> OwnedMeterProvider | None:
    """Return the owned or adopted meter provider, if any."""
    return cls._runtime.meter_provider()

archipy.helpers.utils.otel_utils.OtelUtils.logger_provider classmethod

logger_provider() -> OwnedLoggerProvider | None

Return the owned or adopted logger provider, if any.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def logger_provider(cls) -> OwnedLoggerProvider | None:
    """Return the owned or adopted logger provider, if any."""
    return cls._runtime.logger_provider()

archipy.helpers.utils.otel_utils.OtelUtils.metrics_registry classmethod

metrics_registry() -> CollectorRegistry | None

Return the Prometheus registry of the pull or Pushgateway metrics exporter, if any.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def metrics_registry(cls) -> CollectorRegistry | None:
    """Return the Prometheus registry of the pull or Pushgateway metrics exporter, if any."""
    return cls._runtime.metrics_registry()

archipy.helpers.utils.otel_utils.OtelUtils.status_for_exception staticmethod

status_for_exception(
    exception: BaseException,
) -> Status | None

Map an exception to an OpenTelemetry Status, or None for UNSET.

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def status_for_exception(exception: BaseException) -> Status | None:
    """Map an exception to an OpenTelemetry ``Status``, or ``None`` for UNSET."""
    return OtelRuntime.status_for_exception(exception)

archipy.helpers.utils.otel_utils.OtelUtils.metric_status_for_exception staticmethod

metric_status_for_exception(
    exception: BaseException,
) -> str

Map an exception to a metric status attribute value ("ok" or "error").

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def metric_status_for_exception(exception: BaseException) -> str:
    """Map an exception to a metric ``status`` attribute value (``"ok"`` or ``"error"``)."""
    return OtelRuntime.metric_status_for_exception(exception)

archipy.helpers.utils.otel_utils.OtelUtils.status_for_cancellation staticmethod

status_for_cancellation() -> Status

Return an ERROR status for asyncio task cancellation.

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def status_for_cancellation() -> Status:
    """Return an ERROR status for asyncio task cancellation."""
    return OtelRuntime.status_for_cancellation()

archipy.helpers.utils.otel_utils.OtelUtils.init_otel_if_needed classmethod

init_otel_if_needed(config: BaseConfig) -> None

Build and publish the providers once per process, as config asks.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def init_otel_if_needed(cls, config: BaseConfig) -> None:
    """Build and publish the providers once per process, as ``config`` asks."""
    cls._runtime.init_otel_if_needed(config)

archipy.helpers.utils.otel_utils.OtelUtils.force_flush classmethod

force_flush(timeout_millis: int = 30000) -> bool

Flush the owned providers; return whether every flush succeeded.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def force_flush(cls, timeout_millis: int = 30_000) -> bool:
    """Flush the owned providers; return whether every flush succeeded."""
    return cls._runtime.force_flush(timeout_millis)

archipy.helpers.utils.otel_utils.OtelUtils.shutdown classmethod

shutdown() -> None

Shut down the providers this runtime owns and stop its metrics workers.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def shutdown(cls) -> None:
    """Shut down the providers this runtime owns and stop its metrics workers."""
    cls._runtime.shutdown()

archipy.helpers.utils.otel_utils.OtelUtils.configure_for_testing classmethod

configure_for_testing(
    span_exporter: SpanExporter | None = None,
    metric_reader: MetricReader | None = None,
    log_exporter: LogRecordExporter | None = None,
    *,
    service_name: str = "archipy-test",
) -> None

Install in-memory providers for a test.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def configure_for_testing(
    cls,
    span_exporter: SpanExporter | None = None,
    metric_reader: MetricReader | None = None,
    log_exporter: LogRecordExporter | None = None,
    *,
    service_name: str = "archipy-test",
) -> None:
    """Install in-memory providers for a test."""
    cls._runtime.configure_for_testing(span_exporter, metric_reader, log_exporter, service_name=service_name)

archipy.helpers.utils.otel_utils.OtelUtils.reset_for_testing classmethod

reset_for_testing() -> None

Reset the owned providers for the next test scenario.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def reset_for_testing(cls) -> None:
    """Reset the owned providers for the next test scenario."""
    cls._runtime.reset_for_testing()

archipy.helpers.utils.otel_utils.OtelUtils.grpc_client_interceptors classmethod

grpc_client_interceptors() -> list[
    OpenTelemetryClientInterceptor
]

Return sync gRPC client interceptors for outbound trace propagation.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def grpc_client_interceptors(cls) -> list[OpenTelemetryClientInterceptor]:
    """Return sync gRPC client interceptors for outbound trace propagation."""
    return cls._runtime.grpc_client_interceptors()

archipy.helpers.utils.otel_utils.OtelUtils.async_grpc_client_interceptors classmethod

async_grpc_client_interceptors() -> list[
    grpc.aio.ClientInterceptor
]

Return async gRPC client interceptors for outbound trace propagation.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def async_grpc_client_interceptors(cls) -> list[grpc.aio.ClientInterceptor]:
    """Return async gRPC client interceptors for outbound trace propagation."""
    return cls._runtime.async_grpc_client_interceptors()

archipy.helpers.utils.otel_utils.OtelUtils.resolve_metrics_endpoint classmethod

resolve_metrics_endpoint(otel: OpentelemetryConfig) -> str

Return the resolved OTLP metrics endpoint for Temporal / callers.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def resolve_metrics_endpoint(cls, otel: OpentelemetryConfig) -> str:
    """Return the resolved OTLP metrics endpoint for Temporal / callers."""
    return cls._runtime.resolve_metrics_endpoint(otel)

archipy.helpers.utils.otel_utils.OtelUtils.restore_redis_search_attributes classmethod

restore_redis_search_attributes() -> None

Undo the guard around the Redis instrumentor's search helper (for tests).

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def restore_redis_search_attributes(cls) -> None:
    """Undo the guard around the Redis instrumentor's search helper (for tests)."""
    cls._runtime.restore_redis_search_attributes()

options: show_root_toc_entry: false heading_level: 3

Rate Limit Utils

Utilities for building rate-limit window DTOs and reading decorator metadata from gRPC servicer methods.

Shared rate-limit window helpers.

archipy.helpers.utils.rate_limit_utils.RateLimitUtils

Utility class for building and reading rate-limit window metadata.

Source code in archipy/helpers/utils/rate_limit_utils.py
class RateLimitUtils:
    """Utility class for building and reading rate-limit window metadata."""

    ARCHIPY_RATE_LIMIT_WINDOWS_ATTR = "__archipy_rate_limit_windows__"

    @classmethod
    def compute_rate_limit_window(
        cls,
        calls_count: int = 1,
        milliseconds: int = 0,
        seconds: int = 0,
        minutes: int = 0,
        hours: int = 0,
        days: int = 0,
    ) -> RateLimitWindowDTO:
        """Build a validated rate-limit window from time unit parameters.

        Args:
            calls_count: Maximum allowed requests within the window.
            milliseconds: Milliseconds component of the window.
            seconds: Seconds component of the window.
            minutes: Minutes component of the window.
            hours: Hours component of the window.
            days: Days component of the window.

        Returns:
            A ``RateLimitWindowDTO`` with the combined window in milliseconds.

        Raises:
            InvalidArgumentError: If ``calls_count`` is below 1, a window component is negative, or the window is zero.
        """
        if calls_count < 1:
            raise InvalidArgumentError(additional_data={"detail": "calls_count must be at least 1"})
        if min(milliseconds, seconds, minutes, hours, days) < 0:
            raise InvalidArgumentError(additional_data={"detail": "Rate limit window components must not be negative"})

        window_ms = (
            milliseconds + 1000 * seconds + 60 * 1000 * minutes + 60 * 60 * 1000 * hours + 24 * 60 * 60 * 1000 * days
        )
        if window_ms <= 0:
            raise InvalidArgumentError(
                additional_data={"detail": "Rate limit window must be greater than 0 milliseconds"},
            )

        return RateLimitWindowDTO(calls_count=calls_count, window_ms=window_ms)

    @classmethod
    def get_rate_limit_windows_from_callable(cls, method: object) -> tuple[RateLimitWindowDTO, ...]:
        """Return stacked rate-limit windows attached to a callable or bound method.

        Args:
            method: A function, bound method, or other callable inspected for decorator metadata.

        Returns:
            Tuple of ``RateLimitWindowDTO`` instances in declaration order.
        """
        target = getattr(method, "__func__", method)
        windows = getattr(target, cls.ARCHIPY_RATE_LIMIT_WINDOWS_ATTR, ())
        if not windows:
            return ()
        return tuple(RateLimitWindowDTO.model_validate(window) for window in windows)

archipy.helpers.utils.rate_limit_utils.RateLimitUtils.ARCHIPY_RATE_LIMIT_WINDOWS_ATTR class-attribute instance-attribute

ARCHIPY_RATE_LIMIT_WINDOWS_ATTR = (
    "__archipy_rate_limit_windows__"
)

archipy.helpers.utils.rate_limit_utils.RateLimitUtils.compute_rate_limit_window classmethod

compute_rate_limit_window(
    calls_count: int = 1,
    milliseconds: int = 0,
    seconds: int = 0,
    minutes: int = 0,
    hours: int = 0,
    days: int = 0,
) -> RateLimitWindowDTO

Build a validated rate-limit window from time unit parameters.

Parameters:

Name Type Description Default
calls_count int

Maximum allowed requests within the window.

1
milliseconds int

Milliseconds component of the window.

0
seconds int

Seconds component of the window.

0
minutes int

Minutes component of the window.

0
hours int

Hours component of the window.

0
days int

Days component of the window.

0

Returns:

Type Description
RateLimitWindowDTO

A RateLimitWindowDTO with the combined window in milliseconds.

Raises:

Type Description
InvalidArgumentError

If calls_count is below 1, a window component is negative, or the window is zero.

Source code in archipy/helpers/utils/rate_limit_utils.py
@classmethod
def compute_rate_limit_window(
    cls,
    calls_count: int = 1,
    milliseconds: int = 0,
    seconds: int = 0,
    minutes: int = 0,
    hours: int = 0,
    days: int = 0,
) -> RateLimitWindowDTO:
    """Build a validated rate-limit window from time unit parameters.

    Args:
        calls_count: Maximum allowed requests within the window.
        milliseconds: Milliseconds component of the window.
        seconds: Seconds component of the window.
        minutes: Minutes component of the window.
        hours: Hours component of the window.
        days: Days component of the window.

    Returns:
        A ``RateLimitWindowDTO`` with the combined window in milliseconds.

    Raises:
        InvalidArgumentError: If ``calls_count`` is below 1, a window component is negative, or the window is zero.
    """
    if calls_count < 1:
        raise InvalidArgumentError(additional_data={"detail": "calls_count must be at least 1"})
    if min(milliseconds, seconds, minutes, hours, days) < 0:
        raise InvalidArgumentError(additional_data={"detail": "Rate limit window components must not be negative"})

    window_ms = (
        milliseconds + 1000 * seconds + 60 * 1000 * minutes + 60 * 60 * 1000 * hours + 24 * 60 * 60 * 1000 * days
    )
    if window_ms <= 0:
        raise InvalidArgumentError(
            additional_data={"detail": "Rate limit window must be greater than 0 milliseconds"},
        )

    return RateLimitWindowDTO(calls_count=calls_count, window_ms=window_ms)

archipy.helpers.utils.rate_limit_utils.RateLimitUtils.get_rate_limit_windows_from_callable classmethod

get_rate_limit_windows_from_callable(
    method: object,
) -> tuple[RateLimitWindowDTO, ...]

Return stacked rate-limit windows attached to a callable or bound method.

Parameters:

Name Type Description Default
method object

A function, bound method, or other callable inspected for decorator metadata.

required

Returns:

Type Description
tuple[RateLimitWindowDTO, ...]

Tuple of RateLimitWindowDTO instances in declaration order.

Source code in archipy/helpers/utils/rate_limit_utils.py
@classmethod
def get_rate_limit_windows_from_callable(cls, method: object) -> tuple[RateLimitWindowDTO, ...]:
    """Return stacked rate-limit windows attached to a callable or bound method.

    Args:
        method: A function, bound method, or other callable inspected for decorator metadata.

    Returns:
        Tuple of ``RateLimitWindowDTO`` instances in declaration order.
    """
    target = getattr(method, "__func__", method)
    windows = getattr(target, cls.ARCHIPY_RATE_LIMIT_WINDOWS_ATTR, ())
    if not windows:
        return ()
    return tuple(RateLimitWindowDTO.model_validate(window) for window in windows)

options: show_root_toc_entry: false heading_level: 3