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 length is not 10 digits.
            """
            if not len(national_code) == IRANIAN_NATIONAL_CODE_LEN:
                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("ﻒﻑﻔﻓڡڥڦڤ\u0603ڣڢ", "ف")
)

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_table6 class-attribute instance-attribute

punctuation_translate_table6 = 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.punctuation_translate_table10 class-attribute instance-attribute

punctuation_translate_table10 = str.maketrans(
    dict.fromkeys(";", "؛")
)

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

punctuation_translate_table11 = str.maketrans(
    dict.fromkeys("%", "٪")
)

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 = 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"([{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 length is not 10 digits.
        """
        if not len(national_code) == IRANIAN_NATIONAL_CODE_LEN:
            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.

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.

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

    Returns:
        str: The text with punctuation marks normalized.
    """
    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_table6)
    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_table10)
    text = text.translate(cls.punctuation_translate_table11)
    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) -> 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

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) -> 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.

    Returns:
        str: The text with spacing cleaned up.
    """
    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.
    """
    table = {
        48: 1776,  # 0
        49: 1777,  # 1
        50: 1778,  # 2
        51: 1779,  # 3
        52: 1780,  # 4
        53: 1781,  # 5
        54: 1782,  # 6
        55: 1783,  # 7
        56: 1784,  # 8
        57: 1785,  # 9
        44: 1548,  # ,
    }
    return text.translate(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.

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.
    """
    return f"{value:,}" if isinstance(value, int) else 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 and amounts in the text.

Parameters:

Name Type Description Default
text str

The input text containing currency symbols and amounts.

required
mask str | None

The mask to replace currencies with. Defaults to "MASK_CURRENCIES".

None

Returns:

Name Type Description
str str

The text with currency symbols and amounts 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 and amounts in the text.

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

    Returns:
        str: The text with currency symbols and amounts 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"
    work_text = str(text)
    numbers: list[str] = re.findall("[0-9]+", work_text)
    replacement = f" {mask} "
    for raw_number in sorted(numbers, key=len, reverse=True):
        number = str(raw_number)
        work_text = re.sub(re.escape(number), replacement, work_text)
    return work_text

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

is_string_none_or_empty(text: str) -> bool

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

Parameters:

Name Type Description Default
text str

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) -> bool:
    """Checks if a string is `None` or empty (after stripping whitespace).

    Args:
        text (str): 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,
) -> 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

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,
) -> 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`.

    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)
    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.title() 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()
    return ext in allowed_extensions and bool(ext)

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
tuple[str, datetime]

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

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.

    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

    # Convert secret to bytes if it's UUID
    if isinstance(secret, UUID):
        secret = str(secret)

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

    # Generate HMAC hash
    secret_bytes = str(secret).encode("utf-8")
    time_bytes = struct.pack(">Q", time_step_counter)

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

    hmac_obj = hmac.new(secret_bytes, time_bytes, hash_algo)
    hmac_result = hmac_obj.digest()

    # Get offset and truncate
    offset = hmac_result[-1] & 0xF
    truncated_hash = (
        ((hmac_result[offset] & 0x7F) << 24)
        | ((hmac_result[offset + 1] & 0xFF) << 16)
        | ((hmac_result[offset + 2] & 0xFF) << 8)
        | (hmac_result[offset + 3] & 0xFF)
    )

    # Generate TOTP code
    totp_code = str(truncated_hash % (10**configs.TOTP_LENGTH)).zfill(configs.TOTP_LENGTH)

    # Calculate expiration time
    expires_in = DatetimeUtils.get_datetime_after_given_datetime_or_now(seconds=configs.TOTP_EXPIRES_IN)

    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,
) -> bool

Verifies a TOTP code against the provided secret.

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

Returns:

Type Description
bool

True if the TOTP code is valid, False otherwise.

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) -> bool:
    """Verifies a TOTP code against the provided secret.

    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.

    Returns:
        `True` if the TOTP code is valid, `False` otherwise.

    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.isdigit():
        raise InvalidTokenError

    configs = auth_config or BaseConfig.global_config().AUTH

    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")

    # Check codes within verification window
    for i in range(-configs.TOTP_VERIFICATION_WINDOW, configs.TOTP_VERIFICATION_WINDOW + 1):
        time_step_counter = int(current_time / configs.TOTP_TIME_STEP) + i

        secret_bytes = str(secret).encode("utf-8")
        time_bytes = struct.pack(">Q", time_step_counter)
        hmac_obj = hmac.new(secret_bytes, time_bytes, hash_algo)
        hmac_result = hmac_obj.digest()

        offset = hmac_result[-1] & 0xF
        truncated_hash = (
            ((hmac_result[offset] & 0x7F) << 24)
            | ((hmac_result[offset + 1] & 0xFF) << 16)
            | ((hmac_result[offset + 2] & 0xFF) << 8)
            | (hmac_result[offset + 3] & 0xFF)
        )

        computed_totp = str(truncated_hash % (10 ** len(totp_code))).zfill(len(totp_code))

        if hmac.compare_digest(totp_code, computed_totp):
            return True

    return False

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 Exception as e:
        # Convert any errors to our custom errors
        raise InternalError() from e

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

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

Creates a JWT token with enhanced security features.

Parameters:

Name Type Description Default
data dict[str, Any]

Base claims data to include in the token.

required
expires_in int

Token expiration time in seconds.

required
additional_claims dict[str, Any] | 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 JWT token.

Raises:

Type Description
ValueError

If data is empty or expiration is invalid

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

    Args:
        data (dict[str, Any]): Base claims data to include in the token.
        expires_in (int): Token expiration time in seconds.
        additional_claims (dict[str, Any] | 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 JWT token.

    Raises:
        ValueError: If data is empty or expiration is invalid
    """
    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)

    to_encode = data.copy()
    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
    secret_key = configs.SECRET_KEY
    if secret_key is None:
        raise InvalidArgumentError("SECRET_KEY")
    return jwt.encode(to_encode, secret_key.get_secret_value(), algorithm=configs.HASH_ALGORITHM)

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

create_access_token(
    user_uuid: UUID,
    additional_claims: dict[str, Any] | 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 dict[str, Any] | 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: dict[str, Any] | 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 (dict[str, Any] | 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: dict[str, Any] | 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 dict[str, Any] | 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: dict[str, Any] | 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 (dict[str, Any] | 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, Any]

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, Any]

dict[str, Any]: 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, Any]:
    """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, Any]: 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,
        InvalidAudienceError,
        InvalidIssuerError,
        InvalidSignatureError,
        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
        secret_key = configs.SECRET_KEY
        if secret_key is None:
            raise InvalidArgumentError("SECRET_KEY")

        payload = jwt.decode(
            token,
            secret_key.get_secret_value(),
            algorithms=[configs.HASH_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,
        )

        # Verify token type
        if verify_type 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, Any] as declared
        return dict(payload)

    except ExpiredSignatureError as exception:
        raise TokenExpiredError from exception
    except InvalidSignatureError as exception:
        raise InvalidTokenError from exception
    except InvalidAudienceError as exception:
        raise InvalidTokenError from exception
    except InvalidIssuerError as exception:
        raise InvalidTokenError from exception
    except JWTInvalidTokenError as exception:
        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, Any]

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, Any]

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

Raises:

Type Description
InvalidTokenException

If the token is invalid or not an access token.

TokenExpiredException

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, Any]:
    """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, Any]: The decoded access token payload.

    Raises:
        InvalidTokenException: If the token is invalid or not an access token.
        TokenExpiredException: 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, Any]

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, Any]

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

Raises:

Type Description
InvalidTokenException

If the token is invalid or not a refresh token.

TokenExpiredException

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, Any]:
    """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, Any]: The decoded refresh token payload.

    Raises:
        InvalidTokenException: If the token is invalid or not a refresh token.
        TokenExpiredException: 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: dict[str, Any]) -> UUID

Extracts the user UUID from the token payload.

Parameters:

Name Type Description Default
payload dict[str, Any]

The decoded token payload.

required

Returns:

Name Type Description
UUID UUID

The user's UUID.

Raises:

Type Description
InvalidTokenException

If the user identifier is invalid or missing.

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

    Args:
        payload (dict[str, Any]): The decoded token payload.

    Returns:
        UUID: The user's UUID.

    Raises:
        InvalidTokenException: If the user identifier is invalid or missing.
    """
    try:
        return UUID(payload["sub"])
    except (KeyError, 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
InvalidTokenException

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:
        InvalidTokenException: If the token is invalid.
    """
    payload = cls.decode_token(token, auth_config=auth_config)
    return int(payload["exp"])

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 base64-encoded string containing the salt and hash in the format "salt:hash".

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 base64-encoded string containing the salt and hash in the format "salt:hash".
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    salt = os.urandom(configs.SALT_LENGTH)
    pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, configs.HASH_ITERATIONS)

    # Combine salt and hash, encode in base64
    return b64encode(salt + 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.

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.

    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

        # Decode the stored password
        decoded = b64decode(stored_password.encode("utf-8"))
        salt = decoded[: configs.SALT_LENGTH]
        stored_hash = decoded[configs.SALT_LENGTH :]

        # Hash the provided password with the same salt
        pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, configs.HASH_ITERATIONS)

        # Compare in constant time to prevent timing attacks
        return hmac.compare_digest(pw_hash, stored_hash)
    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
    if any(
        cls.verify_password(new_password, old_password, configs)
        for old_password in password_history[-configs.PASSWORD_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.
    """
    for n in range((end_date - start_date).days):
        yield (start_date + timedelta(n)).date()

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 datetime object with UTC timezone.

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 datetime object with UTC timezone.
    """
    # Parse using a single expression and immediately make timezone-aware for both cases
    dt = (
        datetime.fromisoformat(date_string)
        if format_ is None
        else datetime.strptime(date_string, format_).replace(tzinfo=UTC)
    )

    # Handle the fromisoformat case which might already have timezone info
    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.format_validation_errors staticmethod

format_validation_errors(
    validation_error: ValidationError,
    *,
    include_type: bool = False,
) -> list[dict[str, str]]

Formats Pydantic validation errors into a structured format.

Parameters:

Name Type Description Default
validation_error ValidationError

The validation error to format.

required
include_type bool

Whether to include the error type in the output. Defaults to False.

False

Returns:

Type Description
list[dict[str, str]]

list[dict[str, str]]: A list of formatted validation error details.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
def format_validation_errors(
    validation_error: ValidationError,
    *,
    include_type: bool = False,
) -> list[dict[str, str]]:
    """Formats Pydantic validation errors into a structured format.

    Args:
        validation_error (ValidationError): The validation error to format.
        include_type (bool): Whether to include the error type in the output. Defaults to False.

    Returns:
        list[dict[str, str]]: A list of formatted validation error details.
    """
    formatted_errors = []
    for error in validation_error.errors():
        error_dict = {
            "field": ".".join(str(x) for x in error["loc"]),
            "message": error["msg"],
            "value": str(error.get("input", "")),
        }
        if include_type:
            error_dict["type"] = error["type"]
        formatted_errors.append(error_dict)

    return formatted_errors

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

capture_exception(exception: BaseException) -> None

Captures an exception and records it on the current OpenTelemetry span.

Always logs locally. When OTel is enabled and a recording span is active, records the exception event and sets span status via OtelUtils.status_for_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:
    """Captures an exception and records it on the current OpenTelemetry span.

    Always logs locally. When OTel is enabled and a recording span is active,
    records the exception event and sets span status via ``OtelUtils.status_for_exception``.

    Args:
        exception (BaseException): The exception to capture and report.
    """
    # Always log the exception locally
    logger.error(
        "An exception occurred",
        exc_info=(type(exception), exception, exception.__traceback__),
    )
    config: Any = BaseConfig.global_config()

    if not config.OTEL.IS_ENABLED or not config.OTEL.TRACES_ENABLED:
        return

    try:
        from opentelemetry import trace

        from archipy.helpers.utils.otel_utils import OtelUtils

        span = trace.get_current_span()
        if span.is_recording():
            span.record_exception(exception)
            status = OtelUtils.status_for_exception(exception)
            if status is not None:
                span.set_status(status)
    except ImportError:
        logger.debug("opentelemetry is not installed, cannot record exception on span.")

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

async_handle_fastapi_exception(
    _request: RequestProtocol, exception: BaseError
) -> JSONResponseProtocol

Handles a FastAPI exception and returns a JSON response.

Parameters:

Name Type Description Default
_request Request

The incoming FastAPI request.

required
exception BaseError

The exception to handle.

required

Returns:

Name Type Description
JSONResponse JSONResponseProtocol

A JSON response containing the exception details.

Raises:

Type Description
NotImplementedError

If FastAPI is not available.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
async def async_handle_fastapi_exception(_request: RequestProtocol, exception: BaseError) -> JSONResponseProtocol:
    """Handles a FastAPI exception and returns a JSON response.

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

    Returns:
        JSONResponse: A JSON response containing the exception details.

    Raises:
        NotImplementedError: If FastAPI is not available.
    """
    if not HTTP_AVAILABLE:
        raise NotImplementedError
    return JSONResponse(
        status_code=exception.http_status or HTTPStatus.INTERNAL_SERVER_ERROR,
        content=exception.to_dict(),
    )

archipy.helpers.utils.base_utils.BaseUtils.handle_grpc_exception staticmethod

handle_grpc_exception(
    exception: BaseError,
) -> tuple[int, str]

Handles a gRPC exception and returns a tuple of status code and message.

Parameters:

Name Type Description Default
exception BaseError

The exception to handle.

required

Returns:

Type Description
tuple[int, str]

tuple[int, str]: A tuple containing the gRPC status code and error message.

Raises:

Type Description
NotImplementedError

If gRPC is not available.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
def handle_grpc_exception(exception: BaseError) -> tuple[int, str]:
    """Handles a gRPC exception and returns a tuple of status code and message.

    Args:
        exception (BaseError): The exception to handle.

    Returns:
        tuple[int, str]: A tuple containing the gRPC status code and error message.

    Raises:
        NotImplementedError: If gRPC is not available.
    """
    if not GRPC_AVAILABLE:
        raise NotImplementedError
    return exception.grpc_status or StatusCode.UNKNOWN.value[0], exception.get_message()

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

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

Generates OpenAPI response documentation for the given errors.

This method creates OpenAPI-compatible response schemas for FastAPI errors, including validation errors and custom errors.

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, Any]]

dict[int, dict[str, Any]]: 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, Any]]:
    """Generates OpenAPI response documentation for the given errors.

    This method creates OpenAPI-compatible response schemas for FastAPI errors,
    including validation errors and custom errors.

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

    Returns:
        dict[int, dict[str, Any]]: A dictionary mapping HTTP status codes to their corresponding response schemas.
    """
    responses: dict[int, dict[str, Any]] = {}

    # Add validation error response by default
    validation_error_response = ValidationErrorResponseDTO()
    if validation_error_response.status_code is not None:
        responses[validation_error_response.status_code] = validation_error_response.model

    exception_schemas = {
        "InvalidPhoneNumberError": {
            "phone_number": {"type": "string", "example": "1234567890", "description": "The invalid phone number"},
        },
        "InvalidLandlineNumberError": {
            "landline_number": {
                "type": "string",
                "example": "02112345678",
                "description": "The invalid landline number",
            },
        },
        "NotFoundError": {
            "resource_type": {
                "type": "string",
                "example": "user",
                "description": "Type of resource that was not found",
            },
        },
        "AlreadyExistsError": {
            "resource_type": {
                "type": "string",
                "example": "user",
                "description": "Type of resource that was not found",
            },
        },
        "InvalidNationalCodeError": {
            "national_code": {
                "type": "string",
                "example": "1234567890",
                "description": "The invalid national code",
            },
        },
        "InvalidArgumentError": {
            "argument": {
                "type": "string",
                "example": "mobile_number",
                "description": "Argument that was invalid",
            },
        },
    }

    for exc in exceptions:
        # Use exception class directly (error details are now class attributes)
        if exc.http_status:
            additional_properties = exception_schemas.get(exc.__name__)
            response = FastAPIErrorResponseDTO(exc, additional_properties)
            if response.status_code is not None:
                responses[response.status_code] = response.model

    return responses

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.FastAPIExceptionHandler

Handles various types of errors and converts them to appropriate JSON responses.

Source code in archipy/helpers/utils/app_utils.py
class FastAPIExceptionHandler:
    """Handles various types of errors and converts them to appropriate JSON responses."""

    @staticmethod
    def create_error_response(exception: BaseError) -> JSONResponse:
        """Creates a standardized error response.

        Args:
            exception (BaseError): The exception to be converted into a response.

        Returns:
            JSONResponse: A JSON response containing the exception details.
        """
        BaseUtils.capture_exception(exception)
        # Default to internal server error if status code is not set
        status_code = exception.http_status or HTTPStatus.INTERNAL_SERVER_ERROR.value
        return JSONResponse(status_code=status_code, content=exception.to_dict())

    @staticmethod
    async def custom_exception_handler(_request: Request, exception: BaseError) -> JSONResponse:
        """Handles custom errors.

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

        Returns:
            JSONResponse: A JSON response containing the exception details.
        """
        return FastAPIExceptionHandler.create_error_response(exception)

    @staticmethod
    async def generic_exception_handler(_request: Request, _exception: Exception) -> JSONResponse:
        """Handles generic errors.

        Args:
            _request (Request): The incoming request.
            _exception (Exception): The generic exception to handle.

        Returns:
            JSONResponse: A JSON response containing the exception details.
        """
        return FastAPIExceptionHandler.create_error_response(UnknownError())

    @staticmethod
    async def validation_exception_handler(
        _request: Request,
        exception: ValidationError,
    ) -> JSONResponse:
        """Handles validation errors.

        Args:
            _request (Request): The incoming request.
            exception (ValidationError): The validation exception to handle.

        Returns:
            JSONResponse: A JSON response containing the validation error details.
        """
        BaseUtils.capture_exception(exception)
        errors = BaseUtils.format_validation_errors(exception)
        return JSONResponse(
            status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
            content={"error": "VALIDATION_ERROR", "detail": errors},
        )

archipy.helpers.utils.app_utils.FastAPIExceptionHandler.create_error_response staticmethod

create_error_response(exception: BaseError) -> JSONResponse

Creates a standardized error response.

Parameters:

Name Type Description Default
exception BaseError

The exception to be converted into a response.

required

Returns:

Name Type Description
JSONResponse JSONResponse

A JSON response containing the exception details.

Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def create_error_response(exception: BaseError) -> JSONResponse:
    """Creates a standardized error response.

    Args:
        exception (BaseError): The exception to be converted into a response.

    Returns:
        JSONResponse: A JSON response containing the exception details.
    """
    BaseUtils.capture_exception(exception)
    # Default to internal server error if status code is not set
    status_code = exception.http_status or HTTPStatus.INTERNAL_SERVER_ERROR.value
    return JSONResponse(status_code=status_code, content=exception.to_dict())

archipy.helpers.utils.app_utils.FastAPIExceptionHandler.custom_exception_handler async staticmethod

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

Handles custom errors.

Parameters:

Name Type Description Default
_request Request

The incoming request.

required
exception BaseError

The custom exception to handle.

required

Returns:

Name Type Description
JSONResponse JSONResponse

A JSON response containing the exception details.

Source code in archipy/helpers/utils/app_utils.py
@staticmethod
async def custom_exception_handler(_request: Request, exception: BaseError) -> JSONResponse:
    """Handles custom errors.

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

    Returns:
        JSONResponse: A JSON response containing the exception details.
    """
    return FastAPIExceptionHandler.create_error_response(exception)

archipy.helpers.utils.app_utils.FastAPIExceptionHandler.generic_exception_handler async staticmethod

generic_exception_handler(
    _request: Request, _exception: Exception
) -> JSONResponse

Handles generic errors.

Parameters:

Name Type Description Default
_request Request

The incoming request.

required
_exception Exception

The generic exception to handle.

required

Returns:

Name Type Description
JSONResponse JSONResponse

A JSON response containing the exception details.

Source code in archipy/helpers/utils/app_utils.py
@staticmethod
async def generic_exception_handler(_request: Request, _exception: Exception) -> JSONResponse:
    """Handles generic errors.

    Args:
        _request (Request): The incoming request.
        _exception (Exception): The generic exception to handle.

    Returns:
        JSONResponse: A JSON response containing the exception details.
    """
    return FastAPIExceptionHandler.create_error_response(UnknownError())

archipy.helpers.utils.app_utils.FastAPIExceptionHandler.validation_exception_handler async staticmethod

validation_exception_handler(
    _request: Request, exception: ValidationError
) -> JSONResponse

Handles validation errors.

Parameters:

Name Type Description Default
_request Request

The incoming request.

required
exception ValidationError

The validation exception to handle.

required

Returns:

Name Type Description
JSONResponse JSONResponse

A JSON response containing the validation error details.

Source code in archipy/helpers/utils/app_utils.py
@staticmethod
async def validation_exception_handler(
    _request: Request,
    exception: ValidationError,
) -> JSONResponse:
    """Handles validation errors.

    Args:
        _request (Request): The incoming request.
        exception (ValidationError): The validation exception to handle.

    Returns:
        JSONResponse: A JSON response containing the validation error details.
    """
    BaseUtils.capture_exception(exception)
    errors = BaseUtils.format_validation_errors(exception)
    return JSONResponse(
        status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
        content={"error": "VALIDATION_ERROR", "detail": errors},
    )

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_instrument_kwargs(config: BaseConfig) -> dict[str, Any] | None:
        """Build FastAPIInstrumentor kwargs with real or NoOp providers.

        Returns:
            Kwargs for ``instrument_app``, or ``None`` when instrumentation
            should be skipped (no real providers available).
        """
        from opentelemetry.metrics import NoOpMeterProvider
        from opentelemetry.trace import NoOpTracerProvider

        from archipy.helpers.utils.otel_utils import OtelUtils

        instrument_kwargs: dict[str, Any] = {}
        has_real_provider = False

        if config.OTEL.TRACES_ENABLED:
            tracer_provider = OtelUtils.tracer_provider()
            if tracer_provider is None:
                logger.warning(
                    "OTEL traces enabled but no tracer provider is available; skipping FastAPI trace instrumentation",
                )
                instrument_kwargs["tracer_provider"] = NoOpTracerProvider()
            else:
                instrument_kwargs["tracer_provider"] = tracer_provider
                has_real_provider = True
        else:
            instrument_kwargs["tracer_provider"] = NoOpTracerProvider()

        if config.OTEL.METRICS_ENABLED:
            meter_provider = OtelUtils.meter_provider()
            if meter_provider is None:
                logger.warning(
                    "OTEL metrics enabled but no meter provider is available; skipping FastAPI metric instrumentation",
                )
                instrument_kwargs["meter_provider"] = NoOpMeterProvider()
            else:
                instrument_kwargs["meter_provider"] = meter_provider
                has_real_provider = True
        else:
            instrument_kwargs["meter_provider"] = NoOpMeterProvider()

        if not has_real_provider:
            return None

        if config.OTEL.FASTAPI_EXCLUDED_URLS is not None:
            instrument_kwargs["excluded_urls"] = config.OTEL.FASTAPI_EXCLUDED_URLS
        return instrument_kwargs

    @staticmethod
    def setup_otel(app: FastAPI, config: BaseConfig) -> None:
        """Configure OpenTelemetry instrumentation for a FastAPI application.

        Only passes providers for enabled signals. Never passes ``None`` providers
        (contrib instrumentors would fall back to global OTEL providers). Disabled
        signals receive explicit NoOp providers.

        Args:
            app: The FastAPI application instance.
            config: Application configuration containing OTel settings.
        """
        if not config.OTEL.IS_ENABLED:
            return
        if not config.OTEL.TRACES_ENABLED and not config.OTEL.METRICS_ENABLED:
            return

        from archipy.helpers.utils.otel_utils import OTEL_FASTAPI_INSTALL_HINT, OtelUtils

        try:
            OtelUtils.init_otel_if_needed(config)
            if OtelUtils.import_failed():
                return

            from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

            instrument_kwargs = FastAPIUtils._fastapi_otel_instrument_kwargs(config)
            if instrument_kwargs is None:
                return

            FastAPIInstrumentor.instrument_app(app, **instrument_kwargs)
        except ImportError:
            logger.warning("%s", OTEL_FASTAPI_INSTALL_HINT)
        except Exception:
            logger.exception("Failed to initialize OpenTelemetry for FastAPI")

    @staticmethod
    def setup_exception_handlers(app: FastAPI) -> None:
        """Configures exception handlers for the FastAPI application.

        Args:
            app (FastAPI): The FastAPI application instance.
        """

        # These handlers return JSONResponse which is a subclass of Response,
        # so they are compatible with FastAPI's exception handler requirements.
        # We create wrapper functions to match the expected signature
        async def validation_wrapper(request: Request, exception: Exception) -> Response:
            if isinstance(exception, ValidationError):
                return await FastAPIExceptionHandler.validation_exception_handler(request, exception)
            if isinstance(exception, RequestValidationError):
                # RequestValidationError has errors() method that returns validation errors
                # Format them directly since RequestValidationError has a similar structure
                BaseUtils.capture_exception(exception)
                formatted_errors = []
                for error in exception.errors():
                    error_dict = {
                        "field": ".".join(str(x) for x in error.get("loc", [])),
                        "message": error.get("msg", ""),
                        "value": str(error.get("input", "")),
                    }
                    if "type" in error:
                        error_dict["type"] = error["type"]
                    formatted_errors.append(error_dict)
                return JSONResponse(
                    status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
                    content={"error": "VALIDATION_ERROR", "detail": formatted_errors},
                )
            return await FastAPIExceptionHandler.generic_exception_handler(request, exception)

        async def custom_wrapper(request: Request, exception: Exception) -> Response:
            if isinstance(exception, BaseError):
                return await FastAPIExceptionHandler.custom_exception_handler(request, exception)
            return await FastAPIExceptionHandler.generic_exception_handler(request, exception)

        async def generic_wrapper(request: Request, exception: Exception) -> Response:
            return await FastAPIExceptionHandler.generic_exception_handler(request, exception)

        app.add_exception_handler(RequestValidationError, validation_wrapper)
        app.add_exception_handler(ValidationError, validation_wrapper)
        app.add_exception_handler(BaseError, custom_wrapper)
        app.add_exception_handler(Exception, generic_wrapper)

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.setup_otel staticmethod

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

Configure OpenTelemetry instrumentation for a FastAPI application.

Only passes providers for enabled signals. Never passes None providers (contrib instrumentors would fall back to global OTEL providers). Disabled signals receive explicit NoOp providers.

Parameters:

Name Type Description Default
app FastAPI

The FastAPI application instance.

required
config BaseConfig

Application configuration containing OTel settings.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_otel(app: FastAPI, config: BaseConfig) -> None:
    """Configure OpenTelemetry instrumentation for a FastAPI application.

    Only passes providers for enabled signals. Never passes ``None`` providers
    (contrib instrumentors would fall back to global OTEL providers). Disabled
    signals receive explicit NoOp providers.

    Args:
        app: The FastAPI application instance.
        config: Application configuration containing OTel settings.
    """
    if not config.OTEL.IS_ENABLED:
        return
    if not config.OTEL.TRACES_ENABLED and not config.OTEL.METRICS_ENABLED:
        return

    from archipy.helpers.utils.otel_utils import OTEL_FASTAPI_INSTALL_HINT, OtelUtils

    try:
        OtelUtils.init_otel_if_needed(config)
        if OtelUtils.import_failed():
            return

        from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

        instrument_kwargs = FastAPIUtils._fastapi_otel_instrument_kwargs(config)
        if instrument_kwargs is None:
            return

        FastAPIInstrumentor.instrument_app(app, **instrument_kwargs)
    except ImportError:
        logger.warning("%s", OTEL_FASTAPI_INSTALL_HINT)
    except Exception:
        logger.exception("Failed to initialize OpenTelemetry for FastAPI")

archipy.helpers.utils.app_utils.FastAPIUtils.setup_exception_handlers staticmethod

setup_exception_handlers(app: FastAPI) -> None

Configures exception handlers for the FastAPI application.

Parameters:

Name Type Description Default
app FastAPI

The FastAPI application instance.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_exception_handlers(app: FastAPI) -> None:
    """Configures exception handlers for the FastAPI application.

    Args:
        app (FastAPI): The FastAPI application instance.
    """

    # These handlers return JSONResponse which is a subclass of Response,
    # so they are compatible with FastAPI's exception handler requirements.
    # We create wrapper functions to match the expected signature
    async def validation_wrapper(request: Request, exception: Exception) -> Response:
        if isinstance(exception, ValidationError):
            return await FastAPIExceptionHandler.validation_exception_handler(request, exception)
        if isinstance(exception, RequestValidationError):
            # RequestValidationError has errors() method that returns validation errors
            # Format them directly since RequestValidationError has a similar structure
            BaseUtils.capture_exception(exception)
            formatted_errors = []
            for error in exception.errors():
                error_dict = {
                    "field": ".".join(str(x) for x in error.get("loc", [])),
                    "message": error.get("msg", ""),
                    "value": str(error.get("input", "")),
                }
                if "type" in error:
                    error_dict["type"] = error["type"]
                formatted_errors.append(error_dict)
            return JSONResponse(
                status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
                content={"error": "VALIDATION_ERROR", "detail": formatted_errors},
            )
        return await FastAPIExceptionHandler.generic_exception_handler(request, exception)

    async def custom_wrapper(request: Request, exception: Exception) -> Response:
        if isinstance(exception, BaseError):
            return await FastAPIExceptionHandler.custom_exception_handler(request, exception)
        return await FastAPIExceptionHandler.generic_exception_handler(request, exception)

    async def generic_wrapper(request: Request, exception: Exception) -> Response:
        return await FastAPIExceptionHandler.generic_exception_handler(request, exception)

    app.add_exception_handler(RequestValidationError, validation_wrapper)
    app.add_exception_handler(ValidationError, validation_wrapper)
    app.add_exception_handler(BaseError, custom_wrapper)
    app.add_exception_handler(Exception, generic_wrapper)

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) -> 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) -> 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))
        except Exception:
            logger.exception("Failed to initialize Rate Limit Interceptor")

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

setup_otel_interceptor(
    config: BaseConfig, interceptors: list
) -> 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

Mutable list of gRPC interceptors.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_otel_interceptor(config: BaseConfig, interceptors: list) -> 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
) -> 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) -> 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))
    except Exception:
        logger.exception("Failed to initialize Rate Limit Interceptor")

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) -> 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) -> 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))
        except Exception:
            logger.exception("Failed to initialize Rate Limit Interceptor")

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

setup_otel_interceptor(
    config: BaseConfig, interceptors: list
) -> 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

Mutable list of gRPC interceptors.

required
Source code in archipy/helpers/utils/app_utils.py
@staticmethod
def setup_otel_interceptor(config: BaseConfig, interceptors: list) -> 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
) -> 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) -> 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))
    except Exception:
        logger.exception("Failed to initialize Rate Limit Interceptor")

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) -> AsyncIterator[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()
                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.
        """
        config = config or BaseConfig.global_config()

        # Define common responses for all endpoints
        common_responses = BaseUtils.get_fastapi_exception_responses(
            [UnknownError, UnavailableError, InvalidArgumentError],
        )
        # Convert dict[int, ...] to dict[int | str, ...] for FastAPI compatibility
        responses_dict: dict[int | str, dict[str, Any]] | 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,
        )

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

        if configure_exception_handlers:
            FastAPIUtils.setup_exception_handlers(app)

        return app

    @classmethod
    def create_async_grpc_app(
        cls,
        config: BaseConfig,
        customized_interceptors: set[Any] | None = None,
        compression: grpc.Compression | None = None,
    ) -> GrpcAioServer:
        """Create and configure an async gRPC application."""
        from archipy.helpers.interceptors.grpc.exception import AsyncGrpcServerExceptionInterceptor

        async_interceptors = [AsyncGrpcServerExceptionInterceptor()]

        # 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(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: set[Any] | None = None,
        compression: grpc.Compression | None = None,
    ) -> grpc.Server:
        """Create and configure a synchronous gRPC server."""
        from archipy.helpers.interceptors.grpc.exception import GrpcServerExceptionInterceptor

        interceptors: list[grpc.ServerInterceptor] = [GrpcServerExceptionInterceptor()]

        # 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(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.
    """
    config = config or BaseConfig.global_config()

    # Define common responses for all endpoints
    common_responses = BaseUtils.get_fastapi_exception_responses(
        [UnknownError, UnavailableError, InvalidArgumentError],
    )
    # Convert dict[int, ...] to dict[int | str, ...] for FastAPI compatibility
    responses_dict: dict[int | str, dict[str, Any]] | 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,
    )

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

    if configure_exception_handlers:
        FastAPIUtils.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: set[Any] | None = None,
    compression: Compression | None = None,
) -> GrpcAioServer

Create and configure an async gRPC application.

Source code in archipy/helpers/utils/app_utils.py
@classmethod
def create_async_grpc_app(
    cls,
    config: BaseConfig,
    customized_interceptors: set[Any] | None = None,
    compression: grpc.Compression | None = None,
) -> GrpcAioServer:
    """Create and configure an async gRPC application."""
    from archipy.helpers.interceptors.grpc.exception import AsyncGrpcServerExceptionInterceptor

    async_interceptors = [AsyncGrpcServerExceptionInterceptor()]

    # 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(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: set[Any] | None = None,
    compression: Compression | None = None,
) -> grpc.Server

Create and configure a synchronous gRPC server.

Source code in archipy/helpers/utils/app_utils.py
@classmethod
def create_grpc_app(
    cls,
    config: BaseConfig,
    customized_interceptors: set[Any] | None = None,
    compression: grpc.Compression | None = None,
) -> grpc.Server:
    """Create and configure a synchronous gRPC server."""
    from archipy.helpers.interceptors.grpc.exception import GrpcServerExceptionInterceptor

    interceptors: list[grpc.ServerInterceptor] = [GrpcServerExceptionInterceptor()]

    # 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(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]]] = {}

    @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.
        """
        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
            del cls._holiday_cache[date_str]

        return False, False

    @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 other request-related errors.
        """
        try:
            config: Any = 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._holiday_cache[date_str] = (is_holiday, expiry_time)
        except httpx2.HTTPError 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) -> dict[str, Any]:
        """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:
            Dict[str, Any]: The JSON response from the API.

        Raises:
            httpx2.HTTPError: If the API request fails.
        """
        config: Any = BaseConfig.global_config()
        transport = httpx2.HTTPTransport(retries=config.DATETIME.MAX_RETRIES)
        url = DatetimeUtils._build_api_url(jalali_date)
        headers = {"x-api-key": config.DATETIME.TIME_IR_API_KEY}
        with httpx2.Client(transport=transport, timeout=config.DATETIME.REQUEST_TIMEOUT) as client:
            response = client.get(url, headers=headers)
            response.raise_for_status()
            result: dict[str, Any] = 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: Any = 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: dict[str, Any], jalali_date: jdatetime.date) -> bool:
        """Parses the API response to extract and return the holiday status.

        Args:
            response_data (Dict[str, Any]): 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.
        """
        for n in range((end_date - start_date).days):
            yield (start_date + timedelta(n)).date()

    @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 datetime object with UTC timezone.
        """
        # Parse using a single expression and immediately make timezone-aware for both cases
        dt = (
            datetime.fromisoformat(date_string)
            if format_ is None
            else datetime.strptime(date_string, format_).replace(tzinfo=UTC)
        )

        # Handle the fromisoformat case which might already have timezone info
        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.
    """
    for n in range((end_date - start_date).days):
        yield (start_date + timedelta(n)).date()

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 datetime object with UTC timezone.

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 datetime object with UTC timezone.
    """
    # Parse using a single expression and immediately make timezone-aware for both cases
    dt = (
        datetime.fromisoformat(date_string)
        if format_ is None
        else datetime.strptime(date_string, format_).replace(tzinfo=UTC)
    )

    # Handle the fromisoformat case which might already have timezone info
    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
  9
 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
512
513
514
515
516
517
518
519
520
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.

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

        Returns:
            str: The text with punctuation marks normalized.
        """
        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_table6)
        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_table10)
        text = text.translate(cls.punctuation_translate_table11)
        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) -> 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.

        Returns:
            str: The text with spacing cleaned up.
        """
        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.
        """
        table = {
            48: 1776,  # 0
            49: 1777,  # 1
            50: 1778,  # 2
            51: 1779,  # 3
            52: 1780,  # 4
            53: 1781,  # 5
            54: 1782,  # 6
            55: 1783,  # 7
            56: 1784,  # 8
            57: 1785,  # 9
            44: 1548,  # ,
        }
        return text.translate(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.
        """
        return f"{value:,}" if isinstance(value, int) else 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 and amounts in the text.

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

        Returns:
            str: The text with currency symbols and amounts 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"
        work_text = str(text)
        numbers: list[str] = re.findall("[0-9]+", work_text)
        replacement = f" {mask} "
        for raw_number in sorted(numbers, key=len, reverse=True):
            number = str(raw_number)
            work_text = re.sub(re.escape(number), replacement, work_text)
        return work_text

    @classmethod
    def is_string_none_or_empty(cls, text: str) -> bool:
        """Checks if a string is `None` or empty (after stripping whitespace).

        Args:
            text (str): 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,
    ) -> 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`.

        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)
        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.title() 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("ﻒﻑﻔﻓڡڥڦڤ\u0603ڣڢ", "ف")
)

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_table6 class-attribute instance-attribute

punctuation_translate_table6 = 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.punctuation_translate_table10 class-attribute instance-attribute

punctuation_translate_table10 = str.maketrans(
    dict.fromkeys(";", "؛")
)

archipy.helpers.utils.string_utils.StringUtils.punctuation_translate_table11 class-attribute instance-attribute

punctuation_translate_table11 = str.maketrans(
    dict.fromkeys("%", "٪")
)

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 = 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"([{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.

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.

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

    Returns:
        str: The text with punctuation marks normalized.
    """
    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_table6)
    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_table10)
    text = text.translate(cls.punctuation_translate_table11)
    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) -> 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

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) -> 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.

    Returns:
        str: The text with spacing cleaned up.
    """
    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.
    """
    table = {
        48: 1776,  # 0
        49: 1777,  # 1
        50: 1778,  # 2
        51: 1779,  # 3
        52: 1780,  # 4
        53: 1781,  # 5
        54: 1782,  # 6
        55: 1783,  # 7
        56: 1784,  # 8
        57: 1785,  # 9
        44: 1548,  # ,
    }
    return text.translate(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.

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.
    """
    return f"{value:,}" if isinstance(value, int) else 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 and amounts in the text.

Parameters:

Name Type Description Default
text str

The input text containing currency symbols and amounts.

required
mask str | None

The mask to replace currencies with. Defaults to "MASK_CURRENCIES".

None

Returns:

Name Type Description
str str

The text with currency symbols and amounts 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 and amounts in the text.

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

    Returns:
        str: The text with currency symbols and amounts 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"
    work_text = str(text)
    numbers: list[str] = re.findall("[0-9]+", work_text)
    replacement = f" {mask} "
    for raw_number in sorted(numbers, key=len, reverse=True):
        number = str(raw_number)
        work_text = re.sub(re.escape(number), replacement, work_text)
    return work_text

archipy.helpers.utils.string_utils.StringUtils.is_string_none_or_empty classmethod

is_string_none_or_empty(text: str) -> bool

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

Parameters:

Name Type Description Default
text str

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) -> bool:
    """Checks if a string is `None` or empty (after stripping whitespace).

    Args:
        text (str): 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,
) -> 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

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,
) -> 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`.

    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)
    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.title() 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\ufee7\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\u0603\u06a3\u06a2", "\u0641"),
    )

    # replace "؋|ڧ|ڨ|ﻕ|ﻖ|ﻗ|ﻘ" with "ق"
    alphabet_ghaf_translate_table = str.maketrans(dict.fromkeys("\ufed5\ufed6\ufed7\u06a7\u06a8\u060b\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\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 '،'
    punctuation_translate_table3 = str.maketrans(dict.fromkeys("\u002c\u066c\u066b\u201a\uff0c", "\u060c"))

    # replace 'ʕ | ? | ⁉ | � ' with '؟'
    punctuation_translate_table4 = str.maketrans(dict.fromkeys("\u0295\u003f\u2049\ufffd", "\u061f"))

    # replace '‼ | ❕ ' with '!'
    punctuation_translate_table5 = str.maketrans(dict.fromkeys("\u203c\u2755", "\u0021"))

    # replace '_ ' with 'ـ'
    punctuation_translate_table6 = str.maketrans(dict.fromkeys("\u005f", "\u0640"))

    # 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"))

    # replace ';' with '؛'
    punctuation_translate_table10 = str.maketrans(dict.fromkeys("\u003b", "\u061b"))

    # replace '%' with '٪'
    punctuation_translate_table11 = str.maketrans(dict.fromkeys("\u0025", "\u066a"))

    # 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 = 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"),
            (
                f"([{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",
            "\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("ﻒﻑﻔﻓڡڥڦڤ\u0603ڣڢ", "ف")
)

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_table6 class-attribute instance-attribute

punctuation_translate_table6 = 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.punctuation_translate_table10 class-attribute instance-attribute

punctuation_translate_table10 = str.maketrans(
    dict.fromkeys(";", "؛")
)

archipy.helpers.utils.string_utils_constants.StringUtilsConstants.punctuation_translate_table11 class-attribute instance-attribute

punctuation_translate_table11 = str.maketrans(
    dict.fromkeys("%", "٪")
)

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 = 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"([{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()
        return ext in allowed_extensions and bool(ext)
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()
    return ext in allowed_extensions and bool(ext)

options: show_root_toc_entry: false heading_level: 3

Error Utils

Utilities for error formatting, context enrichment, and error chain inspection.

Error handling utility helpers.

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

logger = logging.getLogger(__name__)

archipy.helpers.utils.error_utils.HTTP_AVAILABLE module-attribute

HTTP_AVAILABLE = True

archipy.helpers.utils.error_utils.GRPC_AVAILABLE module-attribute

GRPC_AVAILABLE = True

archipy.helpers.utils.error_utils.RequestProtocol

Bases: Protocol

Protocol for FastAPI Request objects.

Source code in archipy/helpers/utils/error_utils.py
class RequestProtocol(Protocol):
    """Protocol for FastAPI Request objects."""

archipy.helpers.utils.error_utils.JSONResponseProtocol

Bases: Protocol

Protocol for FastAPI JSONResponse objects.

Source code in archipy/helpers/utils/error_utils.py
class JSONResponseProtocol(Protocol):
    """Protocol for FastAPI JSONResponse objects."""

archipy.helpers.utils.error_utils.ErrorUtils

A utility class for handling errors, including capturing, reporting, and generating responses.

Source code in archipy/helpers/utils/error_utils.py
class ErrorUtils:
    """A utility class for handling errors, including capturing, reporting, and generating responses."""

    @staticmethod
    def format_validation_errors(
        validation_error: ValidationError,
        *,
        include_type: bool = False,
    ) -> list[dict[str, str]]:
        """Formats Pydantic validation errors into a structured format.

        Args:
            validation_error (ValidationError): The validation error to format.
            include_type (bool): Whether to include the error type in the output. Defaults to False.

        Returns:
            list[dict[str, str]]: A list of formatted validation error details.
        """
        formatted_errors = []
        for error in validation_error.errors():
            error_dict = {
                "field": ".".join(str(x) for x in error["loc"]),
                "message": error["msg"],
                "value": str(error.get("input", "")),
            }
            if include_type:
                error_dict["type"] = error["type"]
            formatted_errors.append(error_dict)

        return formatted_errors

    @staticmethod
    def capture_exception(exception: BaseException) -> None:
        """Captures an exception and records it on the current OpenTelemetry span.

        Always logs locally. When OTel is enabled and a recording span is active,
        records the exception event and sets span status via ``OtelUtils.status_for_exception``.

        Args:
            exception (BaseException): The exception to capture and report.
        """
        # Always log the exception locally
        logger.error(
            "An exception occurred",
            exc_info=(type(exception), exception, exception.__traceback__),
        )
        config: Any = BaseConfig.global_config()

        if not config.OTEL.IS_ENABLED or not config.OTEL.TRACES_ENABLED:
            return

        try:
            from opentelemetry import trace

            from archipy.helpers.utils.otel_utils import OtelUtils

            span = trace.get_current_span()
            if span.is_recording():
                span.record_exception(exception)
                status = OtelUtils.status_for_exception(exception)
                if status is not None:
                    span.set_status(status)
        except ImportError:
            logger.debug("opentelemetry is not installed, cannot record exception on span.")

    @staticmethod
    async def async_handle_fastapi_exception(_request: RequestProtocol, exception: BaseError) -> JSONResponseProtocol:
        """Handles a FastAPI exception and returns a JSON response.

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

        Returns:
            JSONResponse: A JSON response containing the exception details.

        Raises:
            NotImplementedError: If FastAPI is not available.
        """
        if not HTTP_AVAILABLE:
            raise NotImplementedError
        return JSONResponse(
            status_code=exception.http_status or HTTPStatus.INTERNAL_SERVER_ERROR,
            content=exception.to_dict(),
        )

    @staticmethod
    def handle_grpc_exception(exception: BaseError) -> tuple[int, str]:
        """Handles a gRPC exception and returns a tuple of status code and message.

        Args:
            exception (BaseError): The exception to handle.

        Returns:
            tuple[int, str]: A tuple containing the gRPC status code and error message.

        Raises:
            NotImplementedError: If gRPC is not available.
        """
        if not GRPC_AVAILABLE:
            raise NotImplementedError
        return exception.grpc_status or StatusCode.UNKNOWN.value[0], exception.get_message()

    @staticmethod
    def get_fastapi_exception_responses(exceptions: list[type[BaseError]]) -> dict[int, dict[str, Any]]:
        """Generates OpenAPI response documentation for the given errors.

        This method creates OpenAPI-compatible response schemas for FastAPI errors,
        including validation errors and custom errors.

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

        Returns:
            dict[int, dict[str, Any]]: A dictionary mapping HTTP status codes to their corresponding response schemas.
        """
        responses: dict[int, dict[str, Any]] = {}

        # Add validation error response by default
        validation_error_response = ValidationErrorResponseDTO()
        if validation_error_response.status_code is not None:
            responses[validation_error_response.status_code] = validation_error_response.model

        exception_schemas = {
            "InvalidPhoneNumberError": {
                "phone_number": {"type": "string", "example": "1234567890", "description": "The invalid phone number"},
            },
            "InvalidLandlineNumberError": {
                "landline_number": {
                    "type": "string",
                    "example": "02112345678",
                    "description": "The invalid landline number",
                },
            },
            "NotFoundError": {
                "resource_type": {
                    "type": "string",
                    "example": "user",
                    "description": "Type of resource that was not found",
                },
            },
            "AlreadyExistsError": {
                "resource_type": {
                    "type": "string",
                    "example": "user",
                    "description": "Type of resource that was not found",
                },
            },
            "InvalidNationalCodeError": {
                "national_code": {
                    "type": "string",
                    "example": "1234567890",
                    "description": "The invalid national code",
                },
            },
            "InvalidArgumentError": {
                "argument": {
                    "type": "string",
                    "example": "mobile_number",
                    "description": "Argument that was invalid",
                },
            },
        }

        for exc in exceptions:
            # Use exception class directly (error details are now class attributes)
            if exc.http_status:
                additional_properties = exception_schemas.get(exc.__name__)
                response = FastAPIErrorResponseDTO(exc, additional_properties)
                if response.status_code is not None:
                    responses[response.status_code] = response.model

        return responses

archipy.helpers.utils.error_utils.ErrorUtils.format_validation_errors staticmethod

format_validation_errors(
    validation_error: ValidationError,
    *,
    include_type: bool = False,
) -> list[dict[str, str]]

Formats Pydantic validation errors into a structured format.

Parameters:

Name Type Description Default
validation_error ValidationError

The validation error to format.

required
include_type bool

Whether to include the error type in the output. Defaults to False.

False

Returns:

Type Description
list[dict[str, str]]

list[dict[str, str]]: A list of formatted validation error details.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
def format_validation_errors(
    validation_error: ValidationError,
    *,
    include_type: bool = False,
) -> list[dict[str, str]]:
    """Formats Pydantic validation errors into a structured format.

    Args:
        validation_error (ValidationError): The validation error to format.
        include_type (bool): Whether to include the error type in the output. Defaults to False.

    Returns:
        list[dict[str, str]]: A list of formatted validation error details.
    """
    formatted_errors = []
    for error in validation_error.errors():
        error_dict = {
            "field": ".".join(str(x) for x in error["loc"]),
            "message": error["msg"],
            "value": str(error.get("input", "")),
        }
        if include_type:
            error_dict["type"] = error["type"]
        formatted_errors.append(error_dict)

    return formatted_errors

archipy.helpers.utils.error_utils.ErrorUtils.capture_exception staticmethod

capture_exception(exception: BaseException) -> None

Captures an exception and records it on the current OpenTelemetry span.

Always logs locally. When OTel is enabled and a recording span is active, records the exception event and sets span status via OtelUtils.status_for_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:
    """Captures an exception and records it on the current OpenTelemetry span.

    Always logs locally. When OTel is enabled and a recording span is active,
    records the exception event and sets span status via ``OtelUtils.status_for_exception``.

    Args:
        exception (BaseException): The exception to capture and report.
    """
    # Always log the exception locally
    logger.error(
        "An exception occurred",
        exc_info=(type(exception), exception, exception.__traceback__),
    )
    config: Any = BaseConfig.global_config()

    if not config.OTEL.IS_ENABLED or not config.OTEL.TRACES_ENABLED:
        return

    try:
        from opentelemetry import trace

        from archipy.helpers.utils.otel_utils import OtelUtils

        span = trace.get_current_span()
        if span.is_recording():
            span.record_exception(exception)
            status = OtelUtils.status_for_exception(exception)
            if status is not None:
                span.set_status(status)
    except ImportError:
        logger.debug("opentelemetry is not installed, cannot record exception on span.")

archipy.helpers.utils.error_utils.ErrorUtils.async_handle_fastapi_exception async staticmethod

async_handle_fastapi_exception(
    _request: RequestProtocol, exception: BaseError
) -> JSONResponseProtocol

Handles a FastAPI exception and returns a JSON response.

Parameters:

Name Type Description Default
_request Request

The incoming FastAPI request.

required
exception BaseError

The exception to handle.

required

Returns:

Name Type Description
JSONResponse JSONResponseProtocol

A JSON response containing the exception details.

Raises:

Type Description
NotImplementedError

If FastAPI is not available.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
async def async_handle_fastapi_exception(_request: RequestProtocol, exception: BaseError) -> JSONResponseProtocol:
    """Handles a FastAPI exception and returns a JSON response.

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

    Returns:
        JSONResponse: A JSON response containing the exception details.

    Raises:
        NotImplementedError: If FastAPI is not available.
    """
    if not HTTP_AVAILABLE:
        raise NotImplementedError
    return JSONResponse(
        status_code=exception.http_status or HTTPStatus.INTERNAL_SERVER_ERROR,
        content=exception.to_dict(),
    )

archipy.helpers.utils.error_utils.ErrorUtils.handle_grpc_exception staticmethod

handle_grpc_exception(
    exception: BaseError,
) -> tuple[int, str]

Handles a gRPC exception and returns a tuple of status code and message.

Parameters:

Name Type Description Default
exception BaseError

The exception to handle.

required

Returns:

Type Description
tuple[int, str]

tuple[int, str]: A tuple containing the gRPC status code and error message.

Raises:

Type Description
NotImplementedError

If gRPC is not available.

Source code in archipy/helpers/utils/error_utils.py
@staticmethod
def handle_grpc_exception(exception: BaseError) -> tuple[int, str]:
    """Handles a gRPC exception and returns a tuple of status code and message.

    Args:
        exception (BaseError): The exception to handle.

    Returns:
        tuple[int, str]: A tuple containing the gRPC status code and error message.

    Raises:
        NotImplementedError: If gRPC is not available.
    """
    if not GRPC_AVAILABLE:
        raise NotImplementedError
    return exception.grpc_status or StatusCode.UNKNOWN.value[0], exception.get_message()

archipy.helpers.utils.error_utils.ErrorUtils.get_fastapi_exception_responses staticmethod

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

Generates OpenAPI response documentation for the given errors.

This method creates OpenAPI-compatible response schemas for FastAPI errors, including validation errors and custom errors.

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, Any]]

dict[int, dict[str, Any]]: 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, Any]]:
    """Generates OpenAPI response documentation for the given errors.

    This method creates OpenAPI-compatible response schemas for FastAPI errors,
    including validation errors and custom errors.

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

    Returns:
        dict[int, dict[str, Any]]: A dictionary mapping HTTP status codes to their corresponding response schemas.
    """
    responses: dict[int, dict[str, Any]] = {}

    # Add validation error response by default
    validation_error_response = ValidationErrorResponseDTO()
    if validation_error_response.status_code is not None:
        responses[validation_error_response.status_code] = validation_error_response.model

    exception_schemas = {
        "InvalidPhoneNumberError": {
            "phone_number": {"type": "string", "example": "1234567890", "description": "The invalid phone number"},
        },
        "InvalidLandlineNumberError": {
            "landline_number": {
                "type": "string",
                "example": "02112345678",
                "description": "The invalid landline number",
            },
        },
        "NotFoundError": {
            "resource_type": {
                "type": "string",
                "example": "user",
                "description": "Type of resource that was not found",
            },
        },
        "AlreadyExistsError": {
            "resource_type": {
                "type": "string",
                "example": "user",
                "description": "Type of resource that was not found",
            },
        },
        "InvalidNationalCodeError": {
            "national_code": {
                "type": "string",
                "example": "1234567890",
                "description": "The invalid national code",
            },
        },
        "InvalidArgumentError": {
            "argument": {
                "type": "string",
                "example": "mobile_number",
                "description": "Argument that was invalid",
            },
        },
    }

    for exc in exceptions:
        # Use exception class directly (error details are now class attributes)
        if exc.http_status:
            additional_properties = exception_schemas.get(exc.__name__)
            response = FastAPIErrorResponseDTO(exc, additional_properties)
            if response.status_code is not None:
                responses[response.status_code] = response.model

    return responses

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: dict[str, Any],
        expires_in: int,
        additional_claims: dict[str, Any] | None = None,
        auth_config: AuthConfig | None = None,
    ) -> str:
        """Creates a JWT token with enhanced security features.

        Args:
            data (dict[str, Any]): Base claims data to include in the token.
            expires_in (int): Token expiration time in seconds.
            additional_claims (dict[str, Any] | 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 JWT token.

        Raises:
            ValueError: If data is empty or expiration is invalid
        """
        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)

        to_encode = data.copy()
        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
        secret_key = configs.SECRET_KEY
        if secret_key is None:
            raise InvalidArgumentError("SECRET_KEY")
        return jwt.encode(to_encode, secret_key.get_secret_value(), algorithm=configs.HASH_ALGORITHM)

    @classmethod
    def create_access_token(
        cls,
        user_uuid: UUID,
        additional_claims: dict[str, Any] | 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 (dict[str, Any] | 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: dict[str, Any] | 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 (dict[str, Any] | 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, Any]:
        """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, Any]: 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,
            InvalidAudienceError,
            InvalidIssuerError,
            InvalidSignatureError,
            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
            secret_key = configs.SECRET_KEY
            if secret_key is None:
                raise InvalidArgumentError("SECRET_KEY")

            payload = jwt.decode(
                token,
                secret_key.get_secret_value(),
                algorithms=[configs.HASH_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,
            )

            # Verify token type
            if verify_type 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, Any] as declared
            return dict(payload)

        except ExpiredSignatureError as exception:
            raise TokenExpiredError from exception
        except InvalidSignatureError as exception:
            raise InvalidTokenError from exception
        except InvalidAudienceError as exception:
            raise InvalidTokenError from exception
        except InvalidIssuerError as exception:
            raise InvalidTokenError from exception
        except JWTInvalidTokenError as exception:
            raise InvalidTokenError from exception

    @classmethod
    def verify_access_token(cls, token: str, auth_config: AuthConfig | None = None) -> dict[str, Any]:
        """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, Any]: The decoded access token payload.

        Raises:
            InvalidTokenException: If the token is invalid or not an access token.
            TokenExpiredException: 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, Any]:
        """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, Any]: The decoded refresh token payload.

        Raises:
            InvalidTokenException: If the token is invalid or not a refresh token.
            TokenExpiredException: 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: dict[str, Any]) -> UUID:
        """Extracts the user UUID from the token payload.

        Args:
            payload (dict[str, Any]): The decoded token payload.

        Returns:
            UUID: The user's UUID.

        Raises:
            InvalidTokenException: If the user identifier is invalid or missing.
        """
        try:
            return UUID(payload["sub"])
        except (KeyError, 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:
            InvalidTokenException: If the token is invalid.
        """
        payload = cls.decode_token(token, auth_config=auth_config)
        return int(payload["exp"])

archipy.helpers.utils.jwt_utils.JWTUtils.create_token classmethod

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

Creates a JWT token with enhanced security features.

Parameters:

Name Type Description Default
data dict[str, Any]

Base claims data to include in the token.

required
expires_in int

Token expiration time in seconds.

required
additional_claims dict[str, Any] | 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 JWT token.

Raises:

Type Description
ValueError

If data is empty or expiration is invalid

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

    Args:
        data (dict[str, Any]): Base claims data to include in the token.
        expires_in (int): Token expiration time in seconds.
        additional_claims (dict[str, Any] | 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 JWT token.

    Raises:
        ValueError: If data is empty or expiration is invalid
    """
    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)

    to_encode = data.copy()
    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
    secret_key = configs.SECRET_KEY
    if secret_key is None:
        raise InvalidArgumentError("SECRET_KEY")
    return jwt.encode(to_encode, secret_key.get_secret_value(), algorithm=configs.HASH_ALGORITHM)

archipy.helpers.utils.jwt_utils.JWTUtils.create_access_token classmethod

create_access_token(
    user_uuid: UUID,
    additional_claims: dict[str, Any] | 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 dict[str, Any] | 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: dict[str, Any] | 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 (dict[str, Any] | 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: dict[str, Any] | 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 dict[str, Any] | 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: dict[str, Any] | 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 (dict[str, Any] | 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, Any]

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, Any]

dict[str, Any]: 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, Any]:
    """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, Any]: 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,
        InvalidAudienceError,
        InvalidIssuerError,
        InvalidSignatureError,
        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
        secret_key = configs.SECRET_KEY
        if secret_key is None:
            raise InvalidArgumentError("SECRET_KEY")

        payload = jwt.decode(
            token,
            secret_key.get_secret_value(),
            algorithms=[configs.HASH_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,
        )

        # Verify token type
        if verify_type 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, Any] as declared
        return dict(payload)

    except ExpiredSignatureError as exception:
        raise TokenExpiredError from exception
    except InvalidSignatureError as exception:
        raise InvalidTokenError from exception
    except InvalidAudienceError as exception:
        raise InvalidTokenError from exception
    except InvalidIssuerError as exception:
        raise InvalidTokenError from exception
    except JWTInvalidTokenError as exception:
        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, Any]

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, Any]

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

Raises:

Type Description
InvalidTokenException

If the token is invalid or not an access token.

TokenExpiredException

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, Any]:
    """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, Any]: The decoded access token payload.

    Raises:
        InvalidTokenException: If the token is invalid or not an access token.
        TokenExpiredException: 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, Any]

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, Any]

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

Raises:

Type Description
InvalidTokenException

If the token is invalid or not a refresh token.

TokenExpiredException

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, Any]:
    """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, Any]: The decoded refresh token payload.

    Raises:
        InvalidTokenException: If the token is invalid or not a refresh token.
        TokenExpiredException: 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: dict[str, Any]) -> UUID

Extracts the user UUID from the token payload.

Parameters:

Name Type Description Default
payload dict[str, Any]

The decoded token payload.

required

Returns:

Name Type Description
UUID UUID

The user's UUID.

Raises:

Type Description
InvalidTokenException

If the user identifier is invalid or missing.

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

    Args:
        payload (dict[str, Any]): The decoded token payload.

    Returns:
        UUID: The user's UUID.

    Raises:
        InvalidTokenException: If the user identifier is invalid or missing.
    """
    try:
        return UUID(payload["sub"])
    except (KeyError, 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
InvalidTokenException

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:
        InvalidTokenException: If the token is invalid.
    """
    payload = cls.decode_token(token, auth_config=auth_config)
    return int(payload["exp"])

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 base64-encoded string containing the salt and hash in the format "salt:hash".
        """
        configs = auth_config or BaseConfig.global_config().AUTH
        salt = os.urandom(configs.SALT_LENGTH)
        pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, configs.HASH_ITERATIONS)

        # Combine salt and hash, encode in base64
        return b64encode(salt + 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.

        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

            # Decode the stored password
            decoded = b64decode(stored_password.encode("utf-8"))
            salt = decoded[: configs.SALT_LENGTH]
            stored_hash = decoded[configs.SALT_LENGTH :]

            # Hash the provided password with the same salt
            pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, configs.HASH_ITERATIONS)

            # Compare in constant time to prevent timing attacks
            return hmac.compare_digest(pw_hash, stored_hash)
        except ValueError, TypeError, IndexError:
            # Catch specific exceptions that could occur during decoding or comparison
            return False

    @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
        if any(
            cls.verify_password(new_password, old_password, configs)
            for old_password in password_history[-configs.PASSWORD_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 base64-encoded string containing the salt and hash in the format "salt:hash".

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 base64-encoded string containing the salt and hash in the format "salt:hash".
    """
    configs = auth_config or BaseConfig.global_config().AUTH
    salt = os.urandom(configs.SALT_LENGTH)
    pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, configs.HASH_ITERATIONS)

    # Combine salt and hash, encode in base64
    return b64encode(salt + 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.

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.

    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

        # Decode the stored password
        decoded = b64decode(stored_password.encode("utf-8"))
        salt = decoded[: configs.SALT_LENGTH]
        stored_hash = decoded[configs.SALT_LENGTH :]

        # Hash the provided password with the same salt
        pw_hash = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, configs.HASH_ITERATIONS)

        # Compare in constant time to prevent timing attacks
        return hmac.compare_digest(pw_hash, stored_hash)
    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
    if any(
        cls.verify_password(new_password, old_password, configs)
        for old_password in password_history[-configs.PASSWORD_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.

        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

        # Convert secret to bytes if it's UUID
        if isinstance(secret, UUID):
            secret = str(secret)

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

        # Generate HMAC hash
        secret_bytes = str(secret).encode("utf-8")
        time_bytes = struct.pack(">Q", time_step_counter)

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

        hmac_obj = hmac.new(secret_bytes, time_bytes, hash_algo)
        hmac_result = hmac_obj.digest()

        # Get offset and truncate
        offset = hmac_result[-1] & 0xF
        truncated_hash = (
            ((hmac_result[offset] & 0x7F) << 24)
            | ((hmac_result[offset + 1] & 0xFF) << 16)
            | ((hmac_result[offset + 2] & 0xFF) << 8)
            | (hmac_result[offset + 3] & 0xFF)
        )

        # Generate TOTP code
        totp_code = str(truncated_hash % (10**configs.TOTP_LENGTH)).zfill(configs.TOTP_LENGTH)

        # Calculate expiration time
        expires_in = DatetimeUtils.get_datetime_after_given_datetime_or_now(seconds=configs.TOTP_EXPIRES_IN)

        return totp_code, expires_in

    @classmethod
    def verify_totp(cls, secret: str | UUID, totp_code: str, auth_config: AuthConfig | None = None) -> bool:
        """Verifies a TOTP code against the provided secret.

        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.

        Returns:
            `True` if the TOTP code is valid, `False` otherwise.

        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.isdigit():
            raise InvalidTokenError

        configs = auth_config or BaseConfig.global_config().AUTH

        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")

        # Check codes within verification window
        for i in range(-configs.TOTP_VERIFICATION_WINDOW, configs.TOTP_VERIFICATION_WINDOW + 1):
            time_step_counter = int(current_time / configs.TOTP_TIME_STEP) + i

            secret_bytes = str(secret).encode("utf-8")
            time_bytes = struct.pack(">Q", time_step_counter)
            hmac_obj = hmac.new(secret_bytes, time_bytes, hash_algo)
            hmac_result = hmac_obj.digest()

            offset = hmac_result[-1] & 0xF
            truncated_hash = (
                ((hmac_result[offset] & 0x7F) << 24)
                | ((hmac_result[offset + 1] & 0xFF) << 16)
                | ((hmac_result[offset + 2] & 0xFF) << 8)
                | (hmac_result[offset + 3] & 0xFF)
            )

            computed_totp = str(truncated_hash % (10 ** len(totp_code))).zfill(len(totp_code))

            if hmac.compare_digest(totp_code, computed_totp):
                return True

        return False

    @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 Exception as e:
            # Convert any errors to our custom errors
            raise InternalError() 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
tuple[str, datetime]

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

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.

    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

    # Convert secret to bytes if it's UUID
    if isinstance(secret, UUID):
        secret = str(secret)

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

    # Generate HMAC hash
    secret_bytes = str(secret).encode("utf-8")
    time_bytes = struct.pack(">Q", time_step_counter)

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

    hmac_obj = hmac.new(secret_bytes, time_bytes, hash_algo)
    hmac_result = hmac_obj.digest()

    # Get offset and truncate
    offset = hmac_result[-1] & 0xF
    truncated_hash = (
        ((hmac_result[offset] & 0x7F) << 24)
        | ((hmac_result[offset + 1] & 0xFF) << 16)
        | ((hmac_result[offset + 2] & 0xFF) << 8)
        | (hmac_result[offset + 3] & 0xFF)
    )

    # Generate TOTP code
    totp_code = str(truncated_hash % (10**configs.TOTP_LENGTH)).zfill(configs.TOTP_LENGTH)

    # Calculate expiration time
    expires_in = DatetimeUtils.get_datetime_after_given_datetime_or_now(seconds=configs.TOTP_EXPIRES_IN)

    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,
) -> bool

Verifies a TOTP code against the provided secret.

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

Returns:

Type Description
bool

True if the TOTP code is valid, False otherwise.

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) -> bool:
    """Verifies a TOTP code against the provided secret.

    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.

    Returns:
        `True` if the TOTP code is valid, `False` otherwise.

    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.isdigit():
        raise InvalidTokenError

    configs = auth_config or BaseConfig.global_config().AUTH

    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")

    # Check codes within verification window
    for i in range(-configs.TOTP_VERIFICATION_WINDOW, configs.TOTP_VERIFICATION_WINDOW + 1):
        time_step_counter = int(current_time / configs.TOTP_TIME_STEP) + i

        secret_bytes = str(secret).encode("utf-8")
        time_bytes = struct.pack(">Q", time_step_counter)
        hmac_obj = hmac.new(secret_bytes, time_bytes, hash_algo)
        hmac_result = hmac_obj.digest()

        offset = hmac_result[-1] & 0xF
        truncated_hash = (
            ((hmac_result[offset] & 0x7F) << 24)
            | ((hmac_result[offset + 1] & 0xFF) << 16)
            | ((hmac_result[offset + 2] & 0xFF) << 8)
            | (hmac_result[offset + 3] & 0xFF)
        )

        computed_totp = str(truncated_hash % (10 ** len(totp_code))).zfill(len(totp_code))

        if hmac.compare_digest(totp_code, computed_totp):
            return True

    return False

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 Exception as e:
        # Convert any errors to our custom errors
        raise InternalError() from e

options: show_root_toc_entry: false heading_level: 3

Keycloak Utils

Utilities for Keycloak token acquisition, validation, user info retrieval, and role checking.

Keycloak utility helpers.

archipy.helpers.utils.keycloak_utils.security module-attribute

security = HTTPBearer(
    scheme_name="OAuth2",
    description="OAuth2 Access Token",
    auto_error=False,
)

archipy.helpers.utils.keycloak_utils.DEFAULT_LANG module-attribute

DEFAULT_LANG = LanguageType.FA

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

logger = logging.getLogger(__name__)

archipy.helpers.utils.keycloak_utils.AuthContext

Bases: BaseModel

Authentication context passed to business logic.

Source code in archipy/helpers/utils/keycloak_utils.py
class AuthContext(BaseModel):
    """Authentication context passed to business logic."""

    user_id: str
    username: str
    email: str
    roles: list[str]
    token: str
    raw_user_info: dict[str, Any]

archipy.helpers.utils.keycloak_utils.AuthContext.user_id instance-attribute

user_id: str

archipy.helpers.utils.keycloak_utils.AuthContext.username instance-attribute

username: str

archipy.helpers.utils.keycloak_utils.AuthContext.email instance-attribute

email: str

archipy.helpers.utils.keycloak_utils.AuthContext.roles instance-attribute

roles: list[str]

archipy.helpers.utils.keycloak_utils.AuthContext.token instance-attribute

token: str

archipy.helpers.utils.keycloak_utils.AuthContext.raw_user_info instance-attribute

raw_user_info: dict[str, Any]

archipy.helpers.utils.keycloak_utils.AuthContextManager

Manager for handling auth context in gRPC services.

Source code in archipy/helpers/utils/keycloak_utils.py
class AuthContextManager:
    """Manager for handling auth context in gRPC services."""

    @staticmethod
    def set_auth_context(auth_context: AuthContext) -> None:
        """Set the auth context for the current request."""
        _auth_context_var.set(auth_context)

    @staticmethod
    def get_auth_context() -> AuthContext | None:
        """Get the auth context for the current request."""
        return _auth_context_var.get()

    @staticmethod
    def clear_auth_context() -> None:
        """Clear the auth context for the current request."""
        _auth_context_var.set(None)

archipy.helpers.utils.keycloak_utils.AuthContextManager.set_auth_context staticmethod

set_auth_context(auth_context: AuthContext) -> None

Set the auth context for the current request.

Source code in archipy/helpers/utils/keycloak_utils.py
@staticmethod
def set_auth_context(auth_context: AuthContext) -> None:
    """Set the auth context for the current request."""
    _auth_context_var.set(auth_context)

archipy.helpers.utils.keycloak_utils.AuthContextManager.get_auth_context staticmethod

get_auth_context() -> AuthContext | None

Get the auth context for the current request.

Source code in archipy/helpers/utils/keycloak_utils.py
@staticmethod
def get_auth_context() -> AuthContext | None:
    """Get the auth context for the current request."""
    return _auth_context_var.get()

archipy.helpers.utils.keycloak_utils.AuthContextManager.clear_auth_context staticmethod

clear_auth_context() -> None

Clear the auth context for the current request.

Source code in archipy/helpers/utils/keycloak_utils.py
@staticmethod
def clear_auth_context() -> None:
    """Clear the auth context for the current request."""
    _auth_context_var.set(None)

archipy.helpers.utils.keycloak_utils.KeycloakUtils

Utility class for Keycloak authentication and authorization in FastAPI applications.

Source code in archipy/helpers/utils/keycloak_utils.py
class KeycloakUtils:
    """Utility class for Keycloak authentication and authorization in FastAPI applications."""

    @staticmethod
    def _get_keycloak_adapter() -> KeycloakAdapter:
        return _shared_sync_adapter()

    @staticmethod
    def _get_async_keycloak_adapter() -> AsyncKeycloakAdapter:
        return _shared_async_adapter()

    @classmethod
    # Synchronous decorator
    def fastapi_auth(
        cls,
        resource_type_param: str | None = None,
        resource_type: str | None = None,
        required_roles: frozenset[str] | None = None,
        all_roles_required: bool = False,
        required_permissions: tuple[tuple[str, str], ...] | None = None,
        admin_roles: frozenset[str] | None = None,
        lang: LanguageType = DEFAULT_LANG,
    ) -> Callable:
        """FastAPI decorator for Keycloak authentication and resource-based authorization.

        Args:
            resource_type_param: The parameter name in the path (e.g., 'user_uuid', 'employee_uuid')
            resource_type: The type of resource being accessed (e.g., 'users', 'employees')
            required_roles: Set of role names that the user must have
            all_roles_required: If True, user must have all specified roles; if False, any role is sufficient
            required_permissions: List of (resource, scope) tuples to check
            admin_roles: Set of roles that grant administrative access to all resources
            lang: Language for error messages
        Raises:
            UnauthenticatedError: If no valid Authorization header is provided
            InvalidTokenError: If token is invalid
            TokenExpiredError: If token is expired
            PermissionDeniedError: If user lacks required roles, permissions, or resource access
            InvalidArgumentError: If resource_type_param is missing when resource_type is provided
        """

        def dependency(
            request: Request,
            token: HTTPAuthorizationCredentials = Security(security),
            keycloak: KeycloakAdapter = Depends(cls._get_keycloak_adapter),
        ) -> dict[str, Any]:
            if token is None:
                raise UnauthenticatedError(lang=lang)
            token_str = token.credentials

            resource_uuid: str | None = None
            if resource_type and resource_type_param:
                resource_uuid = request.path_params.get(resource_type_param)
                if not resource_uuid:
                    raise InvalidArgumentError(argument_name=resource_type_param, lang=lang)

            user_info, token_info, user_roles = _authorize_sync(
                keycloak,
                token_str,
                resource_uuid,
                required_roles,
                all_roles_required,
                required_permissions,
                admin_roles,
                lang,
                resource_type,
            )

            request.state.user_info = user_info
            request.state.token_info = token_info
            request.state.user_roles = user_roles
            return user_info

        return dependency

    @classmethod
    def async_fastapi_auth(
        cls,
        resource_type_param: str | None = None,
        resource_type: str | None = None,
        required_roles: frozenset[str] | None = None,
        all_roles_required: bool = False,
        required_permissions: tuple[tuple[str, str], ...] | None = None,
        admin_roles: frozenset[str] | None = None,
        lang: LanguageType = DEFAULT_LANG,
    ) -> Callable:
        """FastAPI async decorator for Keycloak authentication and resource-based authorization.

        Args:
            resource_type_param: The parameter name in the path (e.g., 'user_uuid', 'employee_uuid')
            resource_type: The type of resource being accessed (e.g., 'users', 'employees')
            required_roles: Set of role names that the user must have
            all_roles_required: If True, user must have all specified roles; if False, any role is sufficient
            required_permissions: List of (resource, scope) tuples to check
            admin_roles: Set of roles that grant administrative access to all resources
            lang: Language for error messages
        Raises:
            UnauthenticatedError: If no valid Authorization header is provided
            InvalidTokenError: If token is invalid
            TokenExpiredError: If token is expired
            PermissionDeniedError: If user lacks required roles, permissions, or resource access
            InvalidArgumentError: If resource_type_param is missing when resource_type is provided
        """

        async def dependency(
            request: Request,
            token: HTTPAuthorizationCredentials = Security(security),
            keycloak: AsyncKeycloakAdapter = Depends(cls._get_async_keycloak_adapter),
        ) -> dict[str, Any]:
            if token is None:
                raise UnauthenticatedError(lang=lang)
            token_str = token.credentials

            resource_uuid: str | None = None
            if resource_type and resource_type_param:
                resource_uuid = request.path_params.get(resource_type_param)
                if not resource_uuid:
                    raise InvalidArgumentError(argument_name=resource_type_param, lang=lang)

            user_info, token_info, user_roles = await _authorize_async(
                keycloak,
                token_str,
                resource_uuid,
                required_roles,
                all_roles_required,
                required_permissions,
                admin_roles,
                lang,
                resource_type,
            )

            request.state.user_info = user_info
            request.state.token_info = token_info
            request.state.user_roles = user_roles
            return user_info

        return dependency

    @staticmethod
    def _extract_token_from_metadata(context: object) -> str | None:
        """Extract Bearer token from gRPC metadata."""
        get_metadata = getattr(context, "invocation_metadata", None)
        if get_metadata is None or not callable(get_metadata):
            return None
        invocation_metadata_result = get_metadata()
        if invocation_metadata_result is None:
            return None
        # Convert metadata tuples to dict, handling both str and bytes keys
        # invocation_metadata_result is an iterable of tuples at runtime
        metadata: dict[str, str] = {}
        try:
            for key, value in invocation_metadata_result:
                # Normalize key to string
                key_str = key.decode("utf-8") if isinstance(key, bytes) else str(key)
                # Normalize value to string
                value_str = value.decode("utf-8") if isinstance(value, bytes) else str(value)
                metadata[key_str] = value_str
        except TypeError, ValueError:
            # If iteration fails, return None
            return None

        auth_keys = ["authorization", "Authorization", "auth", "token"]

        for key in auth_keys:
            if key in metadata:
                auth_value = metadata[key]
                # Handle both bytes and string values
                auth_value_str = auth_value.decode("utf-8") if isinstance(auth_value, bytes) else str(auth_value)

                if auth_value_str.startswith(("Bearer ", "bearer ")):
                    return auth_value_str[7:]
                return auth_value_str

        return None

    @classmethod
    def grpc_auth(
        cls,
        required_roles: frozenset[str] | None = None,
        all_roles_required: bool = False,
        required_permissions: tuple[tuple[str, str], ...] | None = None,
        resource_attribute_name: str | None = None,
        admin_roles: frozenset[str] | None = None,
        lang: LanguageType = DEFAULT_LANG,
    ) -> Callable[[Callable], Callable]:
        """Synchronous gRPC decorator for authentication and authorization.

        This decorator handles:
        1. Token validation
        2. Role/permission checking
        3. Passing auth context to business logic

        Resource ownership is handled in the business logic layer.

        Args:
            required_roles: Set of roles, user must have at least one (or all if all_roles_required=True)
            all_roles_required: If True, user must have all required_roles; if False, any one role is sufficient
            required_permissions: Tuple of (resource, scope) pairs that must be satisfied
            resource_attribute_name: Attribute name to extract resource UUID from context for ownership checking
            admin_roles: Set of admin roles that bypass resource ownership checks
            lang: Language for error messages

        Returns:
            Decorated function with authentication and authorization
        """

        def decorator(func: Callable) -> Callable:
            @functools.wraps(func)
            def wrapper(self: object, request: object, context: object) -> object:
                try:
                    token_str = cls._extract_token_from_metadata(context)
                    if not token_str:
                        _raise_unauthenticated(lang)

                    keycloak = cls._get_keycloak_adapter()

                    resource_uuid: str | None = None
                    if resource_attribute_name:
                        resource_uuid = getattr(request, resource_attribute_name, None)
                        if not resource_uuid:
                            _raise_invalid_argument(resource_attribute_name, lang)

                    user_info, _token_info, user_roles = _authorize_sync(
                        keycloak,
                        token_str,
                        resource_uuid,
                        required_roles,
                        all_roles_required,
                        required_permissions,
                        admin_roles,
                        lang,
                    )

                    auth_context = _build_auth_context(user_info, token_str, user_roles)
                    AuthContextManager.set_auth_context(auth_context)

                    return func(self, request, context)

                except Exception as e:
                    if isinstance(e, BaseError):
                        _abort_grpc_sync_if_servicer_context(e, context)
                        raise
                    raise InternalError(
                        lang=lang,
                        additional_data={"original_error": str(e), "error_type": type(e).__name__},
                    ) from e

                finally:
                    AuthContextManager.clear_auth_context()

            return wrapper

        return decorator

    @classmethod
    def async_grpc_auth(
        cls,
        required_roles: frozenset[str] | None = None,
        all_roles_required: bool = False,
        required_permissions: tuple[tuple[str, str], ...] | None = None,
        resource_attribute_name: str | None = None,
        admin_roles: frozenset[str] | None = None,
        lang: LanguageType = DEFAULT_LANG,
    ) -> Callable[[Callable], Callable]:
        """Simplified gRPC decorator for authentication and authorization.

        This decorator handles:
        1. Token validation
        2. Role/permission checking
        3. Passing auth context to business logic

        Resource ownership is handled in the business logic layer.

        Args:
            required_roles: Set of roles, user must have at least one (or all if all_roles_required=True)
            all_roles_required: If True, user must have all required_roles; if False, any one role is sufficient
            required_permissions: Tuple of (resource, scope) pairs that must be satisfied
            resource_attribute_name: Attribute name to extract resource UUID from context for ownership checking
            admin_roles: Set of admin roles that bypass resource ownership checks
            lang: Language for error messages

        Returns:
            Decorated function with authentication and authorization
        """

        def decorator(func: Callable) -> Callable:
            @functools.wraps(func)
            async def wrapper(self: object, request: object, context: object) -> object:
                try:
                    token_str = cls._extract_token_from_metadata(context)
                    if not token_str:
                        _raise_unauthenticated(lang)

                    keycloak = cls._get_async_keycloak_adapter()

                    resource_uuid: str | None = None
                    if resource_attribute_name:
                        resource_uuid = getattr(request, resource_attribute_name, None)
                        if not resource_uuid:
                            _raise_invalid_argument(resource_attribute_name, lang)

                    user_info, _token_info, user_roles = await _authorize_async(
                        keycloak,
                        token_str,
                        resource_uuid,
                        required_roles,
                        all_roles_required,
                        required_permissions,
                        admin_roles,
                        lang,
                    )

                    auth_context = _build_auth_context(user_info, token_str, user_roles)
                    AuthContextManager.set_auth_context(auth_context)

                    return await func(self, request, context)

                except Exception as e:
                    grpc_ctx = context
                    if grpc_ctx is None:
                        raise
                    if isinstance(e, BaseError) and GRPC_AVAILABLE and isinstance(grpc_ctx, AsyncServicerContext):
                        await _abort_grpc_async_if_servicer_context(e, grpc_ctx)
                        return None  # abort_grpc_async will terminate, but satisfy type checker
                    if GRPC_AVAILABLE and isinstance(grpc_ctx, AsyncServicerContext):
                        error_instance = InternalError(
                            lang=lang,
                            additional_data={"original_error": str(e), "error_type": type(e).__name__},
                        )
                        await _abort_grpc_async_if_servicer_context(error_instance, grpc_ctx)
                        return None  # abort_grpc_async will terminate, but satisfy type checker
                    raise

                finally:
                    AuthContextManager.clear_auth_context()

            return wrapper

        return decorator

archipy.helpers.utils.keycloak_utils.KeycloakUtils.fastapi_auth classmethod

fastapi_auth(
    resource_type_param: str | None = None,
    resource_type: str | None = None,
    required_roles: frozenset[str] | None = None,
    all_roles_required: bool = False,
    required_permissions: tuple[tuple[str, str], ...]
    | None = None,
    admin_roles: frozenset[str] | None = None,
    lang: LanguageType = DEFAULT_LANG,
) -> Callable

FastAPI decorator for Keycloak authentication and resource-based authorization.

Parameters:

Name Type Description Default
resource_type_param str | None

The parameter name in the path (e.g., 'user_uuid', 'employee_uuid')

None
resource_type str | None

The type of resource being accessed (e.g., 'users', 'employees')

None
required_roles frozenset[str] | None

Set of role names that the user must have

None
all_roles_required bool

If True, user must have all specified roles; if False, any role is sufficient

False
required_permissions tuple[tuple[str, str], ...] | None

List of (resource, scope) tuples to check

None
admin_roles frozenset[str] | None

Set of roles that grant administrative access to all resources

None
lang LanguageType

Language for error messages

DEFAULT_LANG

Raises: UnauthenticatedError: If no valid Authorization header is provided InvalidTokenError: If token is invalid TokenExpiredError: If token is expired PermissionDeniedError: If user lacks required roles, permissions, or resource access InvalidArgumentError: If resource_type_param is missing when resource_type is provided

Source code in archipy/helpers/utils/keycloak_utils.py
@classmethod
# Synchronous decorator
def fastapi_auth(
    cls,
    resource_type_param: str | None = None,
    resource_type: str | None = None,
    required_roles: frozenset[str] | None = None,
    all_roles_required: bool = False,
    required_permissions: tuple[tuple[str, str], ...] | None = None,
    admin_roles: frozenset[str] | None = None,
    lang: LanguageType = DEFAULT_LANG,
) -> Callable:
    """FastAPI decorator for Keycloak authentication and resource-based authorization.

    Args:
        resource_type_param: The parameter name in the path (e.g., 'user_uuid', 'employee_uuid')
        resource_type: The type of resource being accessed (e.g., 'users', 'employees')
        required_roles: Set of role names that the user must have
        all_roles_required: If True, user must have all specified roles; if False, any role is sufficient
        required_permissions: List of (resource, scope) tuples to check
        admin_roles: Set of roles that grant administrative access to all resources
        lang: Language for error messages
    Raises:
        UnauthenticatedError: If no valid Authorization header is provided
        InvalidTokenError: If token is invalid
        TokenExpiredError: If token is expired
        PermissionDeniedError: If user lacks required roles, permissions, or resource access
        InvalidArgumentError: If resource_type_param is missing when resource_type is provided
    """

    def dependency(
        request: Request,
        token: HTTPAuthorizationCredentials = Security(security),
        keycloak: KeycloakAdapter = Depends(cls._get_keycloak_adapter),
    ) -> dict[str, Any]:
        if token is None:
            raise UnauthenticatedError(lang=lang)
        token_str = token.credentials

        resource_uuid: str | None = None
        if resource_type and resource_type_param:
            resource_uuid = request.path_params.get(resource_type_param)
            if not resource_uuid:
                raise InvalidArgumentError(argument_name=resource_type_param, lang=lang)

        user_info, token_info, user_roles = _authorize_sync(
            keycloak,
            token_str,
            resource_uuid,
            required_roles,
            all_roles_required,
            required_permissions,
            admin_roles,
            lang,
            resource_type,
        )

        request.state.user_info = user_info
        request.state.token_info = token_info
        request.state.user_roles = user_roles
        return user_info

    return dependency

archipy.helpers.utils.keycloak_utils.KeycloakUtils.async_fastapi_auth classmethod

async_fastapi_auth(
    resource_type_param: str | None = None,
    resource_type: str | None = None,
    required_roles: frozenset[str] | None = None,
    all_roles_required: bool = False,
    required_permissions: tuple[tuple[str, str], ...]
    | None = None,
    admin_roles: frozenset[str] | None = None,
    lang: LanguageType = DEFAULT_LANG,
) -> Callable

FastAPI async decorator for Keycloak authentication and resource-based authorization.

Parameters:

Name Type Description Default
resource_type_param str | None

The parameter name in the path (e.g., 'user_uuid', 'employee_uuid')

None
resource_type str | None

The type of resource being accessed (e.g., 'users', 'employees')

None
required_roles frozenset[str] | None

Set of role names that the user must have

None
all_roles_required bool

If True, user must have all specified roles; if False, any role is sufficient

False
required_permissions tuple[tuple[str, str], ...] | None

List of (resource, scope) tuples to check

None
admin_roles frozenset[str] | None

Set of roles that grant administrative access to all resources

None
lang LanguageType

Language for error messages

DEFAULT_LANG

Raises: UnauthenticatedError: If no valid Authorization header is provided InvalidTokenError: If token is invalid TokenExpiredError: If token is expired PermissionDeniedError: If user lacks required roles, permissions, or resource access InvalidArgumentError: If resource_type_param is missing when resource_type is provided

Source code in archipy/helpers/utils/keycloak_utils.py
@classmethod
def async_fastapi_auth(
    cls,
    resource_type_param: str | None = None,
    resource_type: str | None = None,
    required_roles: frozenset[str] | None = None,
    all_roles_required: bool = False,
    required_permissions: tuple[tuple[str, str], ...] | None = None,
    admin_roles: frozenset[str] | None = None,
    lang: LanguageType = DEFAULT_LANG,
) -> Callable:
    """FastAPI async decorator for Keycloak authentication and resource-based authorization.

    Args:
        resource_type_param: The parameter name in the path (e.g., 'user_uuid', 'employee_uuid')
        resource_type: The type of resource being accessed (e.g., 'users', 'employees')
        required_roles: Set of role names that the user must have
        all_roles_required: If True, user must have all specified roles; if False, any role is sufficient
        required_permissions: List of (resource, scope) tuples to check
        admin_roles: Set of roles that grant administrative access to all resources
        lang: Language for error messages
    Raises:
        UnauthenticatedError: If no valid Authorization header is provided
        InvalidTokenError: If token is invalid
        TokenExpiredError: If token is expired
        PermissionDeniedError: If user lacks required roles, permissions, or resource access
        InvalidArgumentError: If resource_type_param is missing when resource_type is provided
    """

    async def dependency(
        request: Request,
        token: HTTPAuthorizationCredentials = Security(security),
        keycloak: AsyncKeycloakAdapter = Depends(cls._get_async_keycloak_adapter),
    ) -> dict[str, Any]:
        if token is None:
            raise UnauthenticatedError(lang=lang)
        token_str = token.credentials

        resource_uuid: str | None = None
        if resource_type and resource_type_param:
            resource_uuid = request.path_params.get(resource_type_param)
            if not resource_uuid:
                raise InvalidArgumentError(argument_name=resource_type_param, lang=lang)

        user_info, token_info, user_roles = await _authorize_async(
            keycloak,
            token_str,
            resource_uuid,
            required_roles,
            all_roles_required,
            required_permissions,
            admin_roles,
            lang,
            resource_type,
        )

        request.state.user_info = user_info
        request.state.token_info = token_info
        request.state.user_roles = user_roles
        return user_info

    return dependency

archipy.helpers.utils.keycloak_utils.KeycloakUtils.grpc_auth classmethod

grpc_auth(
    required_roles: frozenset[str] | None = None,
    all_roles_required: bool = False,
    required_permissions: tuple[tuple[str, str], ...]
    | None = None,
    resource_attribute_name: str | None = None,
    admin_roles: frozenset[str] | None = None,
    lang: LanguageType = DEFAULT_LANG,
) -> Callable[[Callable], Callable]

Synchronous gRPC decorator for authentication and authorization.

This decorator handles: 1. Token validation 2. Role/permission checking 3. Passing auth context to business logic

Resource ownership is handled in the business logic layer.

Parameters:

Name Type Description Default
required_roles frozenset[str] | None

Set of roles, user must have at least one (or all if all_roles_required=True)

None
all_roles_required bool

If True, user must have all required_roles; if False, any one role is sufficient

False
required_permissions tuple[tuple[str, str], ...] | None

Tuple of (resource, scope) pairs that must be satisfied

None
resource_attribute_name str | None

Attribute name to extract resource UUID from context for ownership checking

None
admin_roles frozenset[str] | None

Set of admin roles that bypass resource ownership checks

None
lang LanguageType

Language for error messages

DEFAULT_LANG

Returns:

Type Description
Callable[[Callable], Callable]

Decorated function with authentication and authorization

Source code in archipy/helpers/utils/keycloak_utils.py
@classmethod
def grpc_auth(
    cls,
    required_roles: frozenset[str] | None = None,
    all_roles_required: bool = False,
    required_permissions: tuple[tuple[str, str], ...] | None = None,
    resource_attribute_name: str | None = None,
    admin_roles: frozenset[str] | None = None,
    lang: LanguageType = DEFAULT_LANG,
) -> Callable[[Callable], Callable]:
    """Synchronous gRPC decorator for authentication and authorization.

    This decorator handles:
    1. Token validation
    2. Role/permission checking
    3. Passing auth context to business logic

    Resource ownership is handled in the business logic layer.

    Args:
        required_roles: Set of roles, user must have at least one (or all if all_roles_required=True)
        all_roles_required: If True, user must have all required_roles; if False, any one role is sufficient
        required_permissions: Tuple of (resource, scope) pairs that must be satisfied
        resource_attribute_name: Attribute name to extract resource UUID from context for ownership checking
        admin_roles: Set of admin roles that bypass resource ownership checks
        lang: Language for error messages

    Returns:
        Decorated function with authentication and authorization
    """

    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        def wrapper(self: object, request: object, context: object) -> object:
            try:
                token_str = cls._extract_token_from_metadata(context)
                if not token_str:
                    _raise_unauthenticated(lang)

                keycloak = cls._get_keycloak_adapter()

                resource_uuid: str | None = None
                if resource_attribute_name:
                    resource_uuid = getattr(request, resource_attribute_name, None)
                    if not resource_uuid:
                        _raise_invalid_argument(resource_attribute_name, lang)

                user_info, _token_info, user_roles = _authorize_sync(
                    keycloak,
                    token_str,
                    resource_uuid,
                    required_roles,
                    all_roles_required,
                    required_permissions,
                    admin_roles,
                    lang,
                )

                auth_context = _build_auth_context(user_info, token_str, user_roles)
                AuthContextManager.set_auth_context(auth_context)

                return func(self, request, context)

            except Exception as e:
                if isinstance(e, BaseError):
                    _abort_grpc_sync_if_servicer_context(e, context)
                    raise
                raise InternalError(
                    lang=lang,
                    additional_data={"original_error": str(e), "error_type": type(e).__name__},
                ) from e

            finally:
                AuthContextManager.clear_auth_context()

        return wrapper

    return decorator

archipy.helpers.utils.keycloak_utils.KeycloakUtils.async_grpc_auth classmethod

async_grpc_auth(
    required_roles: frozenset[str] | None = None,
    all_roles_required: bool = False,
    required_permissions: tuple[tuple[str, str], ...]
    | None = None,
    resource_attribute_name: str | None = None,
    admin_roles: frozenset[str] | None = None,
    lang: LanguageType = DEFAULT_LANG,
) -> Callable[[Callable], Callable]

Simplified gRPC decorator for authentication and authorization.

This decorator handles: 1. Token validation 2. Role/permission checking 3. Passing auth context to business logic

Resource ownership is handled in the business logic layer.

Parameters:

Name Type Description Default
required_roles frozenset[str] | None

Set of roles, user must have at least one (or all if all_roles_required=True)

None
all_roles_required bool

If True, user must have all required_roles; if False, any one role is sufficient

False
required_permissions tuple[tuple[str, str], ...] | None

Tuple of (resource, scope) pairs that must be satisfied

None
resource_attribute_name str | None

Attribute name to extract resource UUID from context for ownership checking

None
admin_roles frozenset[str] | None

Set of admin roles that bypass resource ownership checks

None
lang LanguageType

Language for error messages

DEFAULT_LANG

Returns:

Type Description
Callable[[Callable], Callable]

Decorated function with authentication and authorization

Source code in archipy/helpers/utils/keycloak_utils.py
@classmethod
def async_grpc_auth(
    cls,
    required_roles: frozenset[str] | None = None,
    all_roles_required: bool = False,
    required_permissions: tuple[tuple[str, str], ...] | None = None,
    resource_attribute_name: str | None = None,
    admin_roles: frozenset[str] | None = None,
    lang: LanguageType = DEFAULT_LANG,
) -> Callable[[Callable], Callable]:
    """Simplified gRPC decorator for authentication and authorization.

    This decorator handles:
    1. Token validation
    2. Role/permission checking
    3. Passing auth context to business logic

    Resource ownership is handled in the business logic layer.

    Args:
        required_roles: Set of roles, user must have at least one (or all if all_roles_required=True)
        all_roles_required: If True, user must have all required_roles; if False, any one role is sufficient
        required_permissions: Tuple of (resource, scope) pairs that must be satisfied
        resource_attribute_name: Attribute name to extract resource UUID from context for ownership checking
        admin_roles: Set of admin roles that bypass resource ownership checks
        lang: Language for error messages

    Returns:
        Decorated function with authentication and authorization
    """

    def decorator(func: Callable) -> Callable:
        @functools.wraps(func)
        async def wrapper(self: object, request: object, context: object) -> object:
            try:
                token_str = cls._extract_token_from_metadata(context)
                if not token_str:
                    _raise_unauthenticated(lang)

                keycloak = cls._get_async_keycloak_adapter()

                resource_uuid: str | None = None
                if resource_attribute_name:
                    resource_uuid = getattr(request, resource_attribute_name, None)
                    if not resource_uuid:
                        _raise_invalid_argument(resource_attribute_name, lang)

                user_info, _token_info, user_roles = await _authorize_async(
                    keycloak,
                    token_str,
                    resource_uuid,
                    required_roles,
                    all_roles_required,
                    required_permissions,
                    admin_roles,
                    lang,
                )

                auth_context = _build_auth_context(user_info, token_str, user_roles)
                AuthContextManager.set_auth_context(auth_context)

                return await func(self, request, context)

            except Exception as e:
                grpc_ctx = context
                if grpc_ctx is None:
                    raise
                if isinstance(e, BaseError) and GRPC_AVAILABLE and isinstance(grpc_ctx, AsyncServicerContext):
                    await _abort_grpc_async_if_servicer_context(e, grpc_ctx)
                    return None  # abort_grpc_async will terminate, but satisfy type checker
                if GRPC_AVAILABLE and isinstance(grpc_ctx, AsyncServicerContext):
                    error_instance = InternalError(
                        lang=lang,
                        additional_data={"original_error": str(e), "error_type": type(e).__name__},
                    )
                    await _abort_grpc_async_if_servicer_context(error_instance, grpc_ctx)
                    return None  # abort_grpc_async will terminate, but satisfy type checker
                raise

            finally:
                AuthContextManager.clear_auth_context()

        return wrapper

    return decorator

options: show_root_toc_entry: false heading_level: 3

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.logger module-attribute

logger = logging.getLogger(__name__)

archipy.helpers.utils.otel_utils.HTTP_SERVER_ERROR_MIN module-attribute

HTTP_SERVER_ERROR_MIN = 500

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.OtelUtils

Idempotent OpenTelemetry provider management and helpers.

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 only — OTEL_* environment-variable autoconfiguration is not used. On init, ArchiPy installs a W3C TraceContext + Baggage composite textmap propagator (idempotent) so cross-service traceparent / baggage carriers stay locked to the intended default.

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
 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
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
class OtelUtils:
    """Idempotent OpenTelemetry provider management and helpers.

    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`` only —
    ``OTEL_*`` environment-variable autoconfiguration is not used. On init,
    ArchiPy installs a W3C TraceContext + Baggage composite textmap propagator
    (idempotent) so cross-service ``traceparent`` / ``baggage`` carriers stay
    locked to the intended default.

    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.
    """

    _lock = threading.Lock()
    _initialized: bool = False
    _logging_handler_attached: bool = False
    _atexit_registered: bool = False
    _import_failed: bool = False
    _instrumented_libraries: ClassVar[set[str]] = set()

    _tracer_provider: Any | None = None
    _meter_provider: Any | None = None
    _logger_provider: Any | None = None
    _globals_set: bool = False

    _owns_tracer: bool = False
    _owns_meter: bool = False
    _owns_logger: bool = False
    _logging_handler: Any | None = None
    _init_pid: int | None = None
    _shutdown_provider_ids: ClassVar[set[int]] = set()
    _metrics_pull_registry: Any | None = None
    _metrics_pull_httpd: Any | None = None
    _metrics_pull_thread: Any | None = None
    _owns_metrics_pull_scrape: bool = False
    _metrics_pushgateway_stop: threading.Event | None = None
    _metrics_pushgateway_thread: threading.Thread | None = None
    _metrics_pushgateway_config: dict[str, Any] | None = None
    _owns_metrics_pushgateway: bool = False
    _textmap_propagator_installed: bool = False

    @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)

    @classmethod
    def import_failed(cls) -> bool:
        """Return True when OTel initialization failed due to missing packages."""
        return cls._import_failed

    @classmethod
    def get_tracer(cls, name: str) -> Any:
        """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 cls._tracer_provider is not None:
            return cls._tracer_provider.get_tracer(name)
        return trace.get_tracer(name)

    @classmethod
    def get_meter(cls, name: str) -> Any:
        """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 cls._meter_provider is not None:
            return cls._meter_provider.get_meter(name)
        return metrics.get_meter(name)

    @classmethod
    def tracer_provider(cls) -> Any | None:
        """Return the owned or adopted tracer provider, if initialized."""
        return cls._tracer_provider

    @classmethod
    def meter_provider(cls) -> Any | None:
        """Return the owned or adopted meter provider, if initialized."""
        return cls._meter_provider

    @classmethod
    def logger_provider(cls) -> Any | None:
        """Return the owned or adopted logger provider, if initialized."""
        return cls._logger_provider

    @classmethod
    def metrics_registry(cls) -> Any | 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 cls._metrics_pull_registry

    @staticmethod
    def _truncate_status_description(exception: BaseException) -> str:
        """Return a bounded status description for span status."""
        text = str(exception)
        if len(text) > _STATUS_DESC_MAX_LEN:
            return text[:_STATUS_DESC_MAX_LEN]
        return text

    @staticmethod
    def status_for_exception(exception: BaseException) -> Any | None:
        """Map an exception to an OpenTelemetry ``Status``, or ``None`` for UNSET.

        ``BaseError`` with ``http_status`` below 500 leaves status UNSET (handled
        client error — OTel spec recommends not forcing OK). 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.
        """
        from opentelemetry.trace import Status, StatusCode

        from archipy.models.errors.base_error import BaseError

        if isinstance(exception, BaseError) and exception.http_status < HTTP_SERVER_ERROR_MIN:
            return None
        return Status(StatusCode.ERROR, description=OtelUtils._truncate_status_description(exception))

    @staticmethod
    def metric_status_for_exception(exception: BaseException) -> str:
        """Map an exception to a metric ``status`` attribute value.

        Aligns with ``status_for_exception``: handled client ``BaseError`` values
        (HTTP status below 500) record as ``ok``; other exceptions as ``error``.
        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"``.
        """
        if OtelUtils.status_for_exception(exception) is None:
            return "ok"
        return "error"

    @classmethod
    def status_for_cancellation(cls) -> Any:
        """Return an ERROR status for asyncio task cancellation.

        Returns:
            An OpenTelemetry ``Status`` with ``StatusCode.ERROR``.
        """
        from opentelemetry.trace import Status, StatusCode

        return Status(StatusCode.ERROR, description="cancelled")

    @classmethod
    def init_otel_if_needed(cls, config: BaseConfig) -> None:
        """Initialize OTel providers once (idempotent, thread-safe, fork-aware).

        Args:
            config: Application configuration.
        """
        if not config.OTEL.IS_ENABLED or cls._import_failed:
            return

        current_pid = os.getpid()
        if cls._initialized and cls._init_pid == current_pid:
            return

        with cls._lock:
            if not config.OTEL.IS_ENABLED or cls._import_failed:
                return
            if cls._initialized and cls._init_pid == current_pid:
                return
            if cls._initialized and cls._init_pid is not None and cls._init_pid != current_pid:
                cls._reset_after_fork()
            try:
                cls._install_textmap_propagator_unlocked()
                cls._build_providers(config)
                cls._instrument_installed_libraries(config)
                cls._register_atexit()
                cls._initialized = True
                cls._init_pid = current_pid
            except ImportError:
                cls._import_failed = True
                logger.warning(
                    "OTEL.IS_ENABLED is True but OpenTelemetry is not installed; telemetry disabled. %s",
                    _OTEL_INSTALL_HINT,
                )
            except Exception:
                cls._stop_metrics_pushgateway_unlocked()
                cls._stop_metrics_pull_scrape_unlocked()
                logger.exception("Failed to initialize OpenTelemetry")

    @classmethod
    def force_flush(cls, 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).
        """
        ok = True
        with cls._lock:
            for provider in (cls._tracer_provider, cls._meter_provider, cls._logger_provider):
                if provider is None or not hasattr(provider, "force_flush"):
                    continue
                try:
                    result = provider.force_flush(timeout_millis)
                    if result is False:
                        ok = False
                except Exception:
                    logger.debug("Error during OTel force_flush", exc_info=True)
                    ok = False
        return ok

    @classmethod
    def shutdown(cls) -> None:
        """Flush and shut down ArchiPy-owned providers; detach logging handler.

        Adopted (borrowed) providers are left running. Idempotent.
        """
        with cls._lock:
            cls._force_flush_unlocked(_DEFAULT_FLUSH_TIMEOUT_MS)
            cls._detach_logging_handler_unlocked()
            cls._stop_metrics_pushgateway_unlocked()
            cls._stop_metrics_pull_scrape_unlocked()
            cls._shutdown_owned_providers_unlocked()
            cls._tracer_provider = None
            cls._meter_provider = None
            cls._logger_provider = None
            cls._owns_tracer = False
            cls._owns_meter = False
            cls._owns_logger = False
            cls._initialized = False
            cls._init_pid = None
            cls._instrumented_libraries.clear()
            cls._clear_metric_instrument_caches()

    @classmethod
    def configure_for_testing(
        cls,
        span_exporter: Any | None = None,
        metric_reader: Any | None = None,
        log_exporter: Any | 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 import TracerProvider
        from opentelemetry.sdk.trace.export import SimpleSpanProcessor

        with cls._lock:
            cls._install_textmap_propagator_unlocked()
            resource = Resource.create({"service.name": service_name})
            tracer_provider = TracerProvider(resource=resource)
            if span_exporter is not None:
                tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
            cls._tracer_provider = tracer_provider
            cls._owns_tracer = True
            if not cls._globals_set:
                trace.set_tracer_provider(tracer_provider)

            if metric_reader is not None:
                from opentelemetry.sdk.metrics import MeterProvider

                meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
                cls._meter_provider = meter_provider
                cls._owns_meter = True
                if not cls._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

                logger_provider = LoggerProvider(resource=resource)
                logger_provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter))
                set_logger_provider(logger_provider)
                cls._logger_provider = logger_provider
                cls._owns_logger = True
                cls._attach_logging_handler_unlocked(logging.INFO)

            cls._globals_set = True
            cls._initialized = True
            cls._init_pid = os.getpid()
            cls._import_failed = False

    @classmethod
    def reset_for_testing(cls) -> 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 cls._lock:
            cls._detach_logging_handler_unlocked()
            cls._stop_metrics_pushgateway_unlocked()
            cls._stop_metrics_pull_scrape_unlocked()
            cls._shutdown_owned_providers_unlocked()
            cls._tracer_provider = None
            cls._meter_provider = None
            cls._logger_provider = None
            cls._owns_tracer = False
            cls._owns_meter = False
            cls._owns_logger = False
            cls._initialized = False
            cls._import_failed = False
            cls._init_pid = None
            cls._instrumented_libraries.clear()
            cls._clear_metric_instrument_caches()
            # Keep global textmap propagator; first-call-wins across scenarios.

    @classmethod
    def _install_textmap_propagator_unlocked(cls) -> None:
        """Install W3C TraceContext + Baggage as the global textmap propagator.

        Idempotent. Locks ArchiPy's intended carriers (``traceparent``,
        ``tracestate``, ``baggage``) so another library cannot silently replace
        the default composite before ``init_otel_if_needed``.
        """
        if cls._textmap_propagator_installed:
            return
        from opentelemetry.baggage.propagation import W3CBaggagePropagator
        from opentelemetry.propagate import set_global_textmap
        from opentelemetry.propagators.composite import CompositePropagator
        from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator

        set_global_textmap(
            CompositePropagator(
                [
                    TraceContextTextMapPropagator(),
                    W3CBaggagePropagator(),
                ],
            ),
        )
        cls._textmap_propagator_installed = True

    @classmethod
    def grpc_client_interceptors(cls) -> list[Any]:
        """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=cls._tracer_provider)]

    @classmethod
    def async_grpc_client_interceptors(cls) -> list[Any]:
        """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=cls._tracer_provider))

    @classmethod
    def _reset_after_fork(cls) -> 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.",
            cls._init_pid,
            os.getpid(),
        )
        cls._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.
        cls._metrics_pull_httpd = None
        cls._metrics_pull_thread = None
        cls._metrics_pull_registry = None
        cls._owns_metrics_pull_scrape = False
        cls._metrics_pushgateway_stop = None
        cls._metrics_pushgateway_thread = None
        cls._metrics_pushgateway_config = None
        cls._owns_metrics_pushgateway = False
        cls._tracer_provider = None
        cls._meter_provider = None
        cls._logger_provider = None
        cls._owns_tracer = False
        cls._owns_meter = False
        cls._owns_logger = False
        cls._initialized = False
        cls._globals_set = False
        cls._init_pid = None
        cls._instrumented_libraries.clear()
        cls._clear_metric_instrument_caches()

    @classmethod
    def _clear_metric_instrument_caches(cls) -> 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 (
            _RpcDurationHistogram,
        )

        clear_instrument_caches()
        _RpcDurationHistogram.clear()

    @classmethod
    def _force_flush_unlocked(cls, timeout_millis: int) -> bool:
        ok = True
        for provider in (cls._tracer_provider, cls._meter_provider, cls._logger_provider):
            if provider is None or not hasattr(provider, "force_flush"):
                continue
            try:
                result = provider.force_flush(timeout_millis)
                if result is False:
                    ok = False
            except Exception:
                logger.debug("Error during OTel force_flush", exc_info=True)
                ok = False
        return ok

    @classmethod
    def _shutdown_owned_providers_unlocked(cls) -> None:
        owned: list[tuple[Any | None, bool]] = [
            (cls._tracer_provider, cls._owns_tracer),
            (cls._meter_provider, cls._owns_meter),
            (cls._logger_provider, cls._owns_logger),
        ]
        for provider, owns in owned:
            if not owns or provider is None:
                continue
            try:
                provider.shutdown()
            except Exception:
                logger.debug("Error shutting down OTel provider", exc_info=True)
            cls._mark_provider_shutdown(provider)

    @classmethod
    def _shutdown_partial(
        cls,
        tracer: Any | None,
        owns_tracer: bool,
        meter: Any | None,
        owns_meter: bool,
        log_provider: Any | None,
        owns_logger: bool,
    ) -> None:
        """Shut down providers built during a failed initialization attempt."""
        for provider, owns in (
            (tracer, owns_tracer),
            (meter, owns_meter),
            (log_provider, owns_logger),
        ):
            if not owns or provider is None:
                continue
            try:
                provider.shutdown()
            except Exception:
                logger.debug("Error shutting down partial OTel provider", exc_info=True)
            cls._mark_provider_shutdown(provider)

    @staticmethod
    def _is_concrete_provider(provider: Any, sdk_type: type) -> bool:
        return isinstance(provider, sdk_type)

    @classmethod
    def _is_usable_provider(cls, provider: Any) -> bool:
        """Return False for providers that were shut down or are otherwise unusable."""
        if provider is None or id(provider) in cls._shutdown_provider_ids:
            return False
        return not getattr(provider, "_shutdown", False)

    @classmethod
    def _mark_provider_shutdown(cls, provider: Any | None) -> None:
        if provider is not None:
            cls._shutdown_provider_ids.add(id(provider))

    @classmethod
    def _existing_concrete_tracer_provider(cls) -> Any | None:
        from opentelemetry import trace
        from opentelemetry.sdk.trace import TracerProvider

        current = trace.get_tracer_provider()
        if cls._is_concrete_provider(current, TracerProvider) and cls._is_usable_provider(current):
            return current
        return None

    @classmethod
    def _existing_concrete_meter_provider(cls) -> Any | None:
        from opentelemetry import metrics
        from opentelemetry.sdk.metrics import MeterProvider

        current = metrics.get_meter_provider()
        if cls._is_concrete_provider(current, MeterProvider) and cls._is_usable_provider(current):
            return current
        return None

    @classmethod
    def _existing_concrete_logger_provider(cls) -> Any | None:
        from opentelemetry._logs import get_logger_provider
        from opentelemetry.sdk._logs import LoggerProvider

        current = get_logger_provider()
        if cls._is_concrete_provider(current, LoggerProvider) and cls._is_usable_provider(current):
            return current
        return None

    @classmethod
    def _create_resource(cls, otel: Any) -> Any:
        from opentelemetry.sdk.resources import Resource

        resource_attrs: dict[str, Any] = dict(otel.RESOURCE_ATTRIBUTES)
        if otel.SERVICE_NAME:
            resource_attrs["service.name"] = otel.SERVICE_NAME
        if otel.ENVIRONMENT is not None:
            resource_attrs["deployment.environment.name"] = str(otel.ENVIRONMENT)
        return Resource.create(resource_attrs)

    @classmethod
    def _acquire_provider(
        cls,
        *,
        signal_name: str,
        existing: Any | None,
        builder: Any,
    ) -> tuple[Any, bool]:
        """Adopt an existing provider or build a new owned one.

        Args:
            signal_name: Signal label for warning messages (e.g. ``"trace"``).
            existing: Concrete global provider to adopt, or None.
            builder: Zero-arg callable that builds a new provider.

        Returns:
            Tuple of ``(provider, owns_provider)``.
        """
        if existing is not None:
            logger.warning(
                "Adopting existing %s provider; ArchiPy OTEL %s exporter configuration is ignored for this process",
                signal_name,
                signal_name,
            )
            return existing, False
        return builder(), True

    @classmethod
    def _publish_trace_global(cls, provider: Any, owns: bool) -> tuple[Any, bool]:
        from opentelemetry import trace

        if not owns or provider is None or cls._globals_set:
            return provider, owns
        trace.set_tracer_provider(provider)
        after = cls._existing_concrete_tracer_provider()
        if after is not None and after is not provider:
            cls._shutdown_partial(provider, True, None, False, None, False)
            logger.warning("Global TracerProvider was set by another library; adopting it")
            return after, False
        return provider, True

    @classmethod
    def _publish_metric_global(cls, provider: Any, owns: bool) -> tuple[Any, bool]:
        from opentelemetry import metrics

        if not owns or provider is None or cls._globals_set:
            return provider, owns
        metrics.set_meter_provider(provider)
        after = cls._existing_concrete_meter_provider()
        if after is not None and after is not provider:
            cls._shutdown_partial(None, False, provider, True, None, False)
            logger.warning("Global MeterProvider was set by another library; adopting it")
            return after, False
        return provider, True

    @classmethod
    def _build_providers(cls, config: BaseConfig) -> None:
        """Build providers transactionally and publish only on full success."""
        from opentelemetry._logs import set_logger_provider

        otel = config.OTEL
        resource = cls._create_resource(otel)

        new_tracer: Any | None = None
        new_meter: Any | None = None
        new_logger: Any | None = None
        owns_tracer = False
        owns_meter = False
        owns_logger = False

        try:
            if otel.TRACES_ENABLED:
                new_tracer, owns_tracer = cls._acquire_provider(
                    signal_name="trace",
                    existing=cls._existing_concrete_tracer_provider(),
                    builder=lambda: cls._build_tracer_provider(otel, resource),
                )
            if otel.METRICS_ENABLED:
                new_meter, owns_meter = cls._acquire_provider(
                    signal_name="metric",
                    existing=cls._existing_concrete_meter_provider(),
                    builder=lambda: cls._build_meter_provider(otel, resource),
                )
            if otel.LOGS_ENABLED:
                new_logger, owns_logger = cls._acquire_provider(
                    signal_name="log",
                    existing=cls._existing_concrete_logger_provider(),
                    builder=lambda: cls._build_logger_provider(otel, resource),
                )
        except Exception:
            cls._shutdown_partial(new_tracer, owns_tracer, new_meter, owns_meter, new_logger, owns_logger)
            raise

        new_tracer, owns_tracer = cls._publish_trace_global(new_tracer, owns_tracer)
        new_meter, owns_meter = cls._publish_metric_global(new_meter, owns_meter)
        if owns_logger and new_logger is not None:
            set_logger_provider(new_logger)

        cls._tracer_provider = new_tracer
        cls._meter_provider = new_meter
        cls._logger_provider = new_logger
        cls._owns_tracer = owns_tracer
        cls._owns_meter = owns_meter
        cls._owns_logger = owns_logger
        if new_tracer is not None or new_meter is not None or new_logger is not None:
            cls._globals_set = True

        cls._maybe_start_metrics_pull_scrape_unlocked(
            otel,
            owns_meter=owns_meter,
            new_meter=new_meter,
        )
        cls._maybe_start_metrics_pushgateway_unlocked(
            otel,
            owns_meter=owns_meter,
            new_meter=new_meter,
        )

        if otel.LOGS_ENABLED and new_logger is not None:
            cls._attach_logging_handler_unlocked(getattr(logging, otel.LOGS_LEVEL.upper(), logging.INFO))

    @classmethod
    def _maybe_start_metrics_pull_scrape_unlocked(
        cls,
        otel: Any,
        *,
        owns_meter: bool,
        new_meter: Any | None,
    ) -> None:
        """Start pull scrape when owned; leave providers intact on bind failure."""
        if not (otel.METRICS_ENABLED and otel.METRICS_EXPORTER == "pull"):
            return
        if not (owns_meter and new_meter is not None):
            logger.warning(
                "METRICS_EXPORTER=pull but the MeterProvider was adopted; "
                "ArchiPy metrics pull scrape server is not started for this process",
            )
            return
        try:
            cls._start_metrics_pull_scrape_unlocked(otel)
        except Exception:
            logger.warning(
                "Metrics pull scrape server failed to start on %s:%s; "
                "continuing without scrape (traces/logs/metrics providers remain active)",
                otel.METRICS_PULL_HOST,
                otel.METRICS_PULL_PORT,
                exc_info=True,
            )
            cls._metrics_pull_httpd = None
            cls._metrics_pull_thread = None
            cls._owns_metrics_pull_scrape = False

    @classmethod
    def _maybe_start_metrics_pushgateway_unlocked(
        cls,
        otel: Any,
        *,
        owns_meter: bool,
        new_meter: Any | None,
    ) -> None:
        """Start Pushgateway push loop when owned; never raise into the app."""
        if not (otel.METRICS_ENABLED and otel.METRICS_EXPORTER == "pushgateway"):
            return
        if not (owns_meter and new_meter is not None):
            logger.warning(
                "METRICS_EXPORTER=pushgateway but the MeterProvider was adopted; "
                "ArchiPy Pushgateway push loop is not started for this process",
            )
            return
        try:
            cls._start_metrics_pushgateway_unlocked(otel)
        except Exception:
            logger.warning(
                "Metrics Pushgateway push loop failed to start; "
                "continuing without push (traces/logs/metrics providers remain active)",
                exc_info=True,
            )
            cls._stop_metrics_pushgateway_unlocked(delete_group=False)

    @classmethod
    def _build_tracer_provider(cls, otel: Any, resource: Any) -> Any:
        from opentelemetry.sdk.trace import TracerProvider
        from opentelemetry.sdk.trace.export import BatchSpanProcessor
        from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio

        sampler = ParentBasedTraceIdRatio(otel.TRACES_SAMPLE_RATIO)
        provider = TracerProvider(resource=resource, sampler=sampler)
        exporter = cls._create_span_exporter(otel)
        provider.add_span_processor(BatchSpanProcessor(exporter))
        return provider

    @classmethod
    def _build_meter_provider(cls, otel: Any, resource: Any) -> Any:
        from opentelemetry.sdk.metrics import MeterProvider

        from archipy.configs.config_template import OtelMetricsExporter

        readers: list[Any] = []
        if otel.METRICS_EXPORTER == OtelMetricsExporter.OTLP:
            from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader

            exporter = cls._create_metric_exporter(otel)
            readers.append(
                PeriodicExportingMetricReader(
                    exporter,
                    export_interval_millis=otel.METRIC_EXPORT_INTERVAL_MS,
                ),
            )
        elif otel.METRICS_EXPORTER in {OtelMetricsExporter.PULL, OtelMetricsExporter.PUSHGATEWAY}:
            from opentelemetry.exporter.prometheus import PrometheusMetricReader
            from prometheus_client import CollectorRegistry

            registry = CollectorRegistry(auto_describe=True)
            cls._metrics_pull_registry = registry
            readers.append(PrometheusMetricReader(registry=registry))

        return MeterProvider(resource=resource, metric_readers=readers)

    @classmethod
    def _start_metrics_pull_scrape_unlocked(cls, otel: Any) -> None:
        """Start the metrics pull scrape HTTP server if not already running."""
        if cls._metrics_pull_httpd is not None:
            return
        from prometheus_client import start_http_server

        registry = cls._metrics_pull_registry
        if registry is None:
            msg = "Metrics pull registry missing before scrape server start"
            raise RuntimeError(msg)

        httpd, thread = start_http_server(
            int(otel.METRICS_PULL_PORT),
            addr=str(otel.METRICS_PULL_HOST),
            registry=registry,
        )
        cls._metrics_pull_httpd = httpd
        cls._metrics_pull_thread = thread
        cls._owns_metrics_pull_scrape = True
        logger.info(
            "Metrics pull scrape server listening on http://%s:%s/metrics",
            otel.METRICS_PULL_HOST,
            otel.METRICS_PULL_PORT,
        )

    @classmethod
    def _stop_metrics_pull_scrape_unlocked(cls) -> None:
        """Stop the ArchiPy-owned metrics pull scrape HTTP server."""
        if not cls._owns_metrics_pull_scrape:
            cls._metrics_pull_httpd = None
            cls._metrics_pull_thread = None
            # Keep registry when pushgateway (or another owner) still needs it.
            if not cls._owns_metrics_pushgateway:
                cls._metrics_pull_registry = None
            return
        httpd = cls._metrics_pull_httpd
        if httpd is not None:
            try:
                httpd.shutdown()
            except Exception:
                logger.debug("Error shutting down metrics pull scrape server", exc_info=True)
            try:
                httpd.server_close()
            except Exception:
                logger.debug("Error closing metrics pull scrape server socket", exc_info=True)
        cls._metrics_pull_httpd = None
        cls._metrics_pull_thread = None
        if not cls._owns_metrics_pushgateway:
            cls._metrics_pull_registry = None
        cls._owns_metrics_pull_scrape = False

    @classmethod
    def _start_metrics_pushgateway_unlocked(cls, otel: Any) -> None:
        """Start the daemon thread that pushes metrics to Prometheus Pushgateway."""
        if cls._metrics_pushgateway_thread is not None and cls._metrics_pushgateway_thread.is_alive():
            return
        registry = cls._metrics_pull_registry
        if registry is None:
            msg = "Metrics registry missing before Pushgateway start"
            raise RuntimeError(msg)

        gateway_url = str(otel.METRICS_PUSHGATEWAY_URL or "").strip()
        if not gateway_url:
            msg = "METRICS_PUSHGATEWAY_URL is required when METRICS_EXPORTER=pushgateway"
            raise RuntimeError(msg)

        job = str(otel.METRICS_PUSHGATEWAY_JOB or otel.SERVICE_NAME or "archipy").strip() or "archipy"
        grouping_key = dict(otel.METRICS_PUSHGATEWAY_GROUPING_KEY or {})
        interval = int(otel.METRICS_PUSHGATEWAY_INTERVAL_SECONDS)
        timeout = float(otel.METRICS_PUSHGATEWAY_TIMEOUT_SECONDS)
        delete_on_shutdown = bool(otel.METRICS_PUSHGATEWAY_DELETE_ON_SHUTDOWN)

        stop_event = threading.Event()
        cls._metrics_pushgateway_stop = stop_event
        cls._metrics_pushgateway_config = {
            "gateway": gateway_url,
            "job": job,
            "grouping_key": grouping_key,
            "timeout": timeout,
            "delete_on_shutdown": delete_on_shutdown,
        }

        def _push_loop() -> None:
            from prometheus_client import push_to_gateway

            while True:
                try:
                    push_to_gateway(
                        gateway=gateway_url,
                        job=job,
                        registry=registry,
                        grouping_key=grouping_key or None,
                        timeout=timeout,
                    )
                except Exception:
                    logger.warning(
                        "Failed to push metrics to Pushgateway %s (job=%s)",
                        gateway_url,
                        job,
                        exc_info=True,
                    )
                if stop_event.wait(interval):
                    break

        thread = threading.Thread(
            target=_push_loop,
            name="archipy-metrics-pushgateway",
            daemon=True,
        )
        thread.start()
        cls._metrics_pushgateway_thread = thread
        cls._owns_metrics_pushgateway = True
        logger.info(
            "Metrics Pushgateway push loop started (gateway=%s job=%s interval=%ss)",
            gateway_url,
            job,
            interval,
        )

    @classmethod
    def _stop_metrics_pushgateway_unlocked(cls, *, delete_group: bool = True) -> None:
        """Stop the Pushgateway push loop and optionally delete the grouping key."""
        stop_event = cls._metrics_pushgateway_stop
        thread = cls._metrics_pushgateway_thread
        config = cls._metrics_pushgateway_config
        registry = cls._metrics_pull_registry
        owned = cls._owns_metrics_pushgateway

        if stop_event is not None:
            stop_event.set()
        if thread is not None and thread.is_alive() and thread is not threading.current_thread():
            thread.join(timeout=2.0)

        should_delete = (
            delete_group
            and owned
            and config is not None
            and bool(config.get("delete_on_shutdown"))
            and registry is not None
        )
        if should_delete:
            try:
                from prometheus_client import delete_from_gateway

                delete_from_gateway(
                    gateway=str(config["gateway"]),
                    job=str(config["job"]),
                    grouping_key=config.get("grouping_key") or None,
                    timeout=float(config.get("timeout") or 10.0),
                )
            except Exception:
                logger.warning("Failed to delete Pushgateway metrics group on shutdown", exc_info=True)

        cls._metrics_pushgateway_stop = None
        cls._metrics_pushgateway_thread = None
        cls._metrics_pushgateway_config = None
        cls._owns_metrics_pushgateway = False
        if owned and not cls._owns_metrics_pull_scrape:
            cls._metrics_pull_registry = None

    @classmethod
    def _build_logger_provider(cls, otel: Any, resource: Any) -> Any:
        from opentelemetry.sdk._logs import LoggerProvider
        from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, SimpleLogRecordProcessor

        from archipy.configs.config_template import OtelLogsExporter

        provider = LoggerProvider(resource=resource)
        if otel.LOGS_EXPORTER == OtelLogsExporter.CONSOLE:
            provider.add_log_record_processor(SimpleLogRecordProcessor(cls._create_console_log_exporter()))
        else:
            exporter = cls._create_otlp_log_exporter(otel)
            provider.add_log_record_processor(BatchLogRecordProcessor(exporter))
        return provider

    @staticmethod
    def _create_console_log_exporter() -> Any:
        """Return a console exporter that splits INFO/DEBUG to stdout and WARNING+ to stderr."""
        import sys

        from opentelemetry._logs.severity import SeverityNumber
        from opentelemetry.sdk._logs.export import ConsoleLogRecordExporter, LogRecordExportResult

        stdout_exporter = ConsoleLogRecordExporter(out=sys.stdout)
        stderr_exporter = ConsoleLogRecordExporter(out=sys.stderr)
        warn_min = SeverityNumber.WARN

        class _StdoutStderrLogExporter:
            """Route log records to stdout or stderr by severity."""

            def export(self, batch: Sequence[Any]) -> Any:
                low: list[Any] = []
                high: list[Any] = []
                for record in batch:
                    severity = getattr(record.log_record, "severity_number", None) or SeverityNumber.UNSPECIFIED
                    severity_value = severity.value if isinstance(severity, SeverityNumber) else int(severity)
                    if severity_value >= warn_min.value:
                        high.append(record)
                    else:
                        low.append(record)
                results: list[Any] = []
                if low:
                    results.append(stdout_exporter.export(low))
                if high:
                    results.append(stderr_exporter.export(high))
                if any(result == LogRecordExportResult.FAILURE for result in results):
                    return LogRecordExportResult.FAILURE
                return LogRecordExportResult.SUCCESS

            def shutdown(self) -> None:
                stdout_exporter.shutdown()
                stderr_exporter.shutdown()

            def force_flush(self, timeout_millis: int = 30_000) -> bool:
                _ = timeout_millis
                return True

        return _StdoutStderrLogExporter()

    @staticmethod
    def _resolve_otlp_endpoint(
        otel: Any,
        signal: str,
        override: Any | 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.
        """
        from urllib.parse import urlparse, urlunparse

        if override:
            return str(override).rstrip("/")
        base = str(otel.OTLP_ENDPOINT).rstrip("/")
        if otel.PROTOCOL != "http/protobuf":
            return base
        parsed = urlparse(base)
        path = (parsed.path or "").rstrip("/")
        if path:
            return base
        return urlunparse(parsed._replace(path=f"/v1/{signal}"))

    @classmethod
    def resolve_metrics_endpoint(cls, otel: Any) -> str:
        """Return the resolved OTLP metrics endpoint for Temporal / callers.

        Args:
            otel: OpenTelemetry config section.

        Returns:
            The resolved metrics endpoint URL.
        """
        return cls._resolve_otlp_endpoint(otel, "metrics", getattr(otel, "METRICS_ENDPOINT", None))

    @classmethod
    def _create_span_exporter(cls, otel: Any) -> Any:
        headers = dict(otel.OTLP_HEADERS) or None
        endpoint = cls._resolve_otlp_endpoint(otel, "traces", getattr(otel, "TRACES_ENDPOINT", None))
        if otel.PROTOCOL == "http/protobuf":
            from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
                OTLPSpanExporter,
            )

            return OTLPSpanExporter(endpoint=endpoint, headers=headers, timeout=otel.TIMEOUT)
        from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
            OTLPSpanExporter,
        )

        return OTLPSpanExporter(endpoint=endpoint, headers=headers, timeout=otel.TIMEOUT)

    @classmethod
    def _create_metric_exporter(cls, otel: Any) -> Any:
        headers = dict(otel.OTLP_HEADERS) or None
        endpoint = cls.resolve_metrics_endpoint(otel)
        if otel.PROTOCOL == "http/protobuf":
            from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
                OTLPMetricExporter,
            )

            return OTLPMetricExporter(endpoint=endpoint, headers=headers, timeout=otel.TIMEOUT)
        from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
            OTLPMetricExporter,
        )

        return OTLPMetricExporter(endpoint=endpoint, headers=headers, timeout=otel.TIMEOUT)

    @classmethod
    def _create_otlp_log_exporter(cls, otel: Any) -> Any:
        headers = dict(otel.OTLP_HEADERS) or None
        endpoint = cls._resolve_otlp_endpoint(otel, "logs", getattr(otel, "LOGS_ENDPOINT", None))
        if otel.PROTOCOL == "http/protobuf":
            from opentelemetry.exporter.otlp.proto.http._log_exporter import (
                OTLPLogExporter,
            )

            return OTLPLogExporter(endpoint=endpoint, headers=headers, timeout=otel.TIMEOUT)
        from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
            OTLPLogExporter,
        )

        return OTLPLogExporter(endpoint=endpoint, headers=headers, timeout=otel.TIMEOUT)

    @classmethod
    def _attach_logging_handler_unlocked(cls, level: int) -> None:
        if cls._logging_handler_attached or cls._logger_provider is None:
            return
        from opentelemetry.sdk._logs import LoggingHandler

        class _DropOtelInternalLogs(logging.Filter):
            """Drop records from ``opentelemetry.*`` to avoid export feedback loops."""

            def filter(self, record: logging.LogRecord) -> bool:
                return not record.name.startswith("opentelemetry")

        handler = LoggingHandler(level=level, logger_provider=cls._logger_provider)
        handler.addFilter(_DropOtelInternalLogs())
        logging.getLogger().addHandler(handler)
        cls._logging_handler = handler
        cls._logging_handler_attached = True

    @classmethod
    def _detach_logging_handler_unlocked(cls) -> None:
        if not cls._logging_handler_attached or cls._logging_handler is None:
            cls._logging_handler_attached = False
            cls._logging_handler = None
            return
        root = logging.getLogger()
        try:
            root.removeHandler(cls._logging_handler)
        except Exception:
            logger.debug("Error removing OTel logging handler", exc_info=True)
        try:
            cls._logging_handler.close()
        except Exception:
            logger.debug("Error closing OTel logging handler", exc_info=True)
        cls._logging_handler = None
        cls._logging_handler_attached = False

    @classmethod
    def _instrument_installed_libraries(cls, config: BaseConfig) -> None:
        """Best-effort auto-instrumentation of installed contrib packages.

        Each entry is ``(cache_key, module, class_name)``. Missing packages are
        skipped via ``ImportError`` — install the matching ``archipy[otel-*]``
        extra (or the contrib package directly) to enable them.

        Driver-level DB instrumentors (psycopg/pymysql/sqlite3) are omitted:
        ArchiPy goes through SQLAlchemy — use ``archipy[otel-sqlalchemy]``.
        """
        instrumentors: Sequence[tuple[str, str, str]] = (
            ("threading", "opentelemetry.instrumentation.threading", "ThreadingInstrumentor"),
            ("system_metrics", "opentelemetry.instrumentation.system_metrics", "SystemMetricsInstrumentor"),
            ("sqlalchemy", "opentelemetry.instrumentation.sqlalchemy", "SQLAlchemyInstrumentor"),
            ("redis", "opentelemetry.instrumentation.redis", "RedisInstrumentor"),
            ("elasticsearch", "opentelemetry.instrumentation.elasticsearch", "ElasticsearchInstrumentor"),
            ("cassandra", "opentelemetry.instrumentation.cassandra", "CassandraInstrumentor"),
            ("confluent_kafka", "opentelemetry.instrumentation.confluent_kafka", "ConfluentKafkaInstrumentor"),
            ("botocore", "opentelemetry.instrumentation.botocore", "BotocoreInstrumentor"),
            ("httpx", "opentelemetry.instrumentation.httpx", "HTTPXClientInstrumentor"),
            ("requests", "opentelemetry.instrumentation.requests", "RequestsInstrumentor"),
        )
        otel = config.OTEL
        for key, module_name, class_name in instrumentors:
            if key in cls._instrumented_libraries:
                continue
            if key == "system_metrics" and not (
                otel.METRICS_ENABLED and otel.SYSTEM_METRICS_ENABLED and cls._meter_provider is not None
            ):
                logger.debug("Skipping system metrics instrumentation (disabled by config)")
                continue
            try:
                module = __import__(module_name, fromlist=[class_name])
                instrumentor_cls = getattr(module, class_name)
                instrumentor = instrumentor_cls()
                if getattr(instrumentor, "is_instrumented_by_opentelemetry", False):
                    cls._instrumented_libraries.add(key)
                    logger.debug("Skipping already-instrumented library: %s", key)
                    continue
                kwargs: dict[str, Any] = {}
                if cls._tracer_provider is not None:
                    kwargs["tracer_provider"] = cls._tracer_provider
                if cls._meter_provider is not None:
                    kwargs["meter_provider"] = cls._meter_provider
                try:
                    instrumentor.instrument(**kwargs)
                except TypeError:
                    kwargs.pop("meter_provider", None)
                    instrumentor.instrument(**kwargs)
                cls._instrumented_libraries.add(key)
                logger.debug("Instrumented library: %s", key)
            except ImportError:
                logger.debug("Skipping OTel instrumentation for %s (package not installed)", key)
            except Exception:
                logger.debug("Failed to instrument %s", key, exc_info=True)

    @classmethod
    def _register_atexit(cls) -> None:
        if cls._atexit_registered:
            return

        def _shutdown() -> None:
            try:
                cls.shutdown()
            except Exception:
                logger.debug("Error during OTel atexit shutdown", exc_info=True)

        atexit.register(_shutdown)
        cls._atexit_registered = True

archipy.helpers.utils.otel_utils.OtelUtils.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_utils.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.OtelUtils.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_utils.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.OtelUtils.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_utils.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.OtelUtils.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_utils.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.OtelUtils.import_failed classmethod

import_failed() -> bool

Return True when OTel initialization failed due to missing packages.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def import_failed(cls) -> bool:
    """Return True when OTel initialization failed due to missing packages."""
    return cls._import_failed

archipy.helpers.utils.otel_utils.OtelUtils.get_tracer classmethod

get_tracer(name: str) -> Any

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
Any

An OpenTelemetry Tracer instance, or a no-op stub when the

Any

opentelemetry package is not installed.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def get_tracer(cls, name: str) -> Any:
    """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 cls._tracer_provider is not None:
        return cls._tracer_provider.get_tracer(name)
    return trace.get_tracer(name)

archipy.helpers.utils.otel_utils.OtelUtils.get_meter classmethod

get_meter(name: str) -> Any

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
Any

An OpenTelemetry Meter instance, or a no-op stub when the

Any

opentelemetry package is not installed.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def get_meter(cls, name: str) -> Any:
    """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 cls._meter_provider is not None:
        return cls._meter_provider.get_meter(name)
    return metrics.get_meter(name)

archipy.helpers.utils.otel_utils.OtelUtils.tracer_provider classmethod

tracer_provider() -> Any | None

Return the owned or adopted tracer provider, if initialized.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def tracer_provider(cls) -> Any | None:
    """Return the owned or adopted tracer provider, if initialized."""
    return cls._tracer_provider

archipy.helpers.utils.otel_utils.OtelUtils.meter_provider classmethod

meter_provider() -> Any | None

Return the owned or adopted meter provider, if initialized.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def meter_provider(cls) -> Any | None:
    """Return the owned or adopted meter provider, if initialized."""
    return cls._meter_provider

archipy.helpers.utils.otel_utils.OtelUtils.logger_provider classmethod

logger_provider() -> Any | None

Return the owned or adopted logger provider, if initialized.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def logger_provider(cls) -> Any | None:
    """Return the owned or adopted logger provider, if initialized."""
    return cls._logger_provider

archipy.helpers.utils.otel_utils.OtelUtils.metrics_registry classmethod

metrics_registry() -> Any | None

Return the Prometheus CollectorRegistry for pull/pushgateway metrics.

Returns:

Type Description
Any | None

The registry when METRICS_EXPORTER is pull or pushgateway

Any | None

and providers were built, otherwise None.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def metrics_registry(cls) -> Any | 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 cls._metrics_pull_registry

archipy.helpers.utils.otel_utils.OtelUtils.status_for_exception staticmethod

status_for_exception(
    exception: BaseException,
) -> Any | None

Map an exception to an OpenTelemetry Status, or None for UNSET.

BaseError with http_status below 500 leaves status UNSET (handled client error — OTel spec recommends not forcing OK). All other exceptions become StatusCode.ERROR.

Parameters:

Name Type Description Default
exception BaseException

The exception raised during a span.

required

Returns:

Type Description
Any | None

An OpenTelemetry Status instance, or None to leave status UNSET.

Source code in archipy/helpers/utils/otel_utils.py
@staticmethod
def status_for_exception(exception: BaseException) -> Any | None:
    """Map an exception to an OpenTelemetry ``Status``, or ``None`` for UNSET.

    ``BaseError`` with ``http_status`` below 500 leaves status UNSET (handled
    client error — OTel spec recommends not forcing OK). 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.
    """
    from opentelemetry.trace import Status, StatusCode

    from archipy.models.errors.base_error import BaseError

    if isinstance(exception, BaseError) and exception.http_status < HTTP_SERVER_ERROR_MIN:
        return None
    return Status(StatusCode.ERROR, description=OtelUtils._truncate_status_description(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.

Aligns with status_for_exception: handled client BaseError values (HTTP status below 500) record as ok; other exceptions as error. 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_utils.py
@staticmethod
def metric_status_for_exception(exception: BaseException) -> str:
    """Map an exception to a metric ``status`` attribute value.

    Aligns with ``status_for_exception``: handled client ``BaseError`` values
    (HTTP status below 500) record as ``ok``; other exceptions as ``error``.
    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"``.
    """
    if OtelUtils.status_for_exception(exception) is None:
        return "ok"
    return "error"

archipy.helpers.utils.otel_utils.OtelUtils.status_for_cancellation classmethod

status_for_cancellation() -> Any

Return an ERROR status for asyncio task cancellation.

Returns:

Type Description
Any

An OpenTelemetry Status with StatusCode.ERROR.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def status_for_cancellation(cls) -> Any:
    """Return an ERROR status for asyncio task cancellation.

    Returns:
        An OpenTelemetry ``Status`` with ``StatusCode.ERROR``.
    """
    from opentelemetry.trace import Status, StatusCode

    return Status(StatusCode.ERROR, description="cancelled")

archipy.helpers.utils.otel_utils.OtelUtils.init_otel_if_needed classmethod

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_utils.py
@classmethod
def init_otel_if_needed(cls, config: BaseConfig) -> None:
    """Initialize OTel providers once (idempotent, thread-safe, fork-aware).

    Args:
        config: Application configuration.
    """
    if not config.OTEL.IS_ENABLED or cls._import_failed:
        return

    current_pid = os.getpid()
    if cls._initialized and cls._init_pid == current_pid:
        return

    with cls._lock:
        if not config.OTEL.IS_ENABLED or cls._import_failed:
            return
        if cls._initialized and cls._init_pid == current_pid:
            return
        if cls._initialized and cls._init_pid is not None and cls._init_pid != current_pid:
            cls._reset_after_fork()
        try:
            cls._install_textmap_propagator_unlocked()
            cls._build_providers(config)
            cls._instrument_installed_libraries(config)
            cls._register_atexit()
            cls._initialized = True
            cls._init_pid = current_pid
        except ImportError:
            cls._import_failed = True
            logger.warning(
                "OTEL.IS_ENABLED is True but OpenTelemetry is not installed; telemetry disabled. %s",
                _OTEL_INSTALL_HINT,
            )
        except Exception:
            cls._stop_metrics_pushgateway_unlocked()
            cls._stop_metrics_pull_scrape_unlocked()
            logger.exception("Failed to initialize OpenTelemetry")

archipy.helpers.utils.otel_utils.OtelUtils.force_flush classmethod

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_utils.py
@classmethod
def force_flush(cls, 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).
    """
    ok = True
    with cls._lock:
        for provider in (cls._tracer_provider, cls._meter_provider, cls._logger_provider):
            if provider is None or not hasattr(provider, "force_flush"):
                continue
            try:
                result = provider.force_flush(timeout_millis)
                if result is False:
                    ok = False
            except Exception:
                logger.debug("Error during OTel force_flush", exc_info=True)
                ok = False
    return ok

archipy.helpers.utils.otel_utils.OtelUtils.shutdown classmethod

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_utils.py
@classmethod
def shutdown(cls) -> None:
    """Flush and shut down ArchiPy-owned providers; detach logging handler.

    Adopted (borrowed) providers are left running. Idempotent.
    """
    with cls._lock:
        cls._force_flush_unlocked(_DEFAULT_FLUSH_TIMEOUT_MS)
        cls._detach_logging_handler_unlocked()
        cls._stop_metrics_pushgateway_unlocked()
        cls._stop_metrics_pull_scrape_unlocked()
        cls._shutdown_owned_providers_unlocked()
        cls._tracer_provider = None
        cls._meter_provider = None
        cls._logger_provider = None
        cls._owns_tracer = False
        cls._owns_meter = False
        cls._owns_logger = False
        cls._initialized = False
        cls._init_pid = None
        cls._instrumented_libraries.clear()
        cls._clear_metric_instrument_caches()

archipy.helpers.utils.otel_utils.OtelUtils.configure_for_testing classmethod

configure_for_testing(
    span_exporter: Any | None = None,
    metric_reader: Any | None = None,
    log_exporter: Any | None = None,
    *,
    service_name: str = "archipy-test",
) -> None

Swap in in-memory providers for BDD / unit tests.

Parameters:

Name Type Description Default
span_exporter Any | None

Optional span exporter (e.g. InMemorySpanExporter).

None
metric_reader Any | None

Optional metric reader (e.g. InMemoryMetricReader).

None
log_exporter Any | 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_utils.py
@classmethod
def configure_for_testing(
    cls,
    span_exporter: Any | None = None,
    metric_reader: Any | None = None,
    log_exporter: Any | 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 import TracerProvider
    from opentelemetry.sdk.trace.export import SimpleSpanProcessor

    with cls._lock:
        cls._install_textmap_propagator_unlocked()
        resource = Resource.create({"service.name": service_name})
        tracer_provider = TracerProvider(resource=resource)
        if span_exporter is not None:
            tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
        cls._tracer_provider = tracer_provider
        cls._owns_tracer = True
        if not cls._globals_set:
            trace.set_tracer_provider(tracer_provider)

        if metric_reader is not None:
            from opentelemetry.sdk.metrics import MeterProvider

            meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
            cls._meter_provider = meter_provider
            cls._owns_meter = True
            if not cls._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

            logger_provider = LoggerProvider(resource=resource)
            logger_provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter))
            set_logger_provider(logger_provider)
            cls._logger_provider = logger_provider
            cls._owns_logger = True
            cls._attach_logging_handler_unlocked(logging.INFO)

        cls._globals_set = True
        cls._initialized = True
        cls._init_pid = os.getpid()
        cls._import_failed = False

archipy.helpers.utils.otel_utils.OtelUtils.reset_for_testing classmethod

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_utils.py
@classmethod
def reset_for_testing(cls) -> 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 cls._lock:
        cls._detach_logging_handler_unlocked()
        cls._stop_metrics_pushgateway_unlocked()
        cls._stop_metrics_pull_scrape_unlocked()
        cls._shutdown_owned_providers_unlocked()
        cls._tracer_provider = None
        cls._meter_provider = None
        cls._logger_provider = None
        cls._owns_tracer = False
        cls._owns_meter = False
        cls._owns_logger = False
        cls._initialized = False
        cls._import_failed = False
        cls._init_pid = None
        cls._instrumented_libraries.clear()
        cls._clear_metric_instrument_caches()

archipy.helpers.utils.otel_utils.OtelUtils.grpc_client_interceptors classmethod

grpc_client_interceptors() -> list[Any]

Return sync gRPC client interceptors for outbound trace propagation.

Returns:

Type Description
list[Any]

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_utils.py
@classmethod
def grpc_client_interceptors(cls) -> list[Any]:
    """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=cls._tracer_provider)]

archipy.helpers.utils.otel_utils.OtelUtils.async_grpc_client_interceptors classmethod

async_grpc_client_interceptors() -> list[Any]

Return async gRPC client interceptors for outbound trace propagation.

Returns:

Type Description
list[Any]

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_utils.py
@classmethod
def async_grpc_client_interceptors(cls) -> list[Any]:
    """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=cls._tracer_provider))

archipy.helpers.utils.otel_utils.OtelUtils.resolve_metrics_endpoint classmethod

resolve_metrics_endpoint(otel: Any) -> str

Return the resolved OTLP metrics endpoint for Temporal / callers.

Parameters:

Name Type Description Default
otel Any

OpenTelemetry config section.

required

Returns:

Type Description
str

The resolved metrics endpoint URL.

Source code in archipy/helpers/utils/otel_utils.py
@classmethod
def resolve_metrics_endpoint(cls, otel: Any) -> str:
    """Return the resolved OTLP metrics endpoint for Temporal / callers.

    Args:
        otel: OpenTelemetry config section.

    Returns:
        The resolved metrics endpoint URL.
    """
    return cls._resolve_otlp_endpoint(otel, "metrics", getattr(otel, "METRICS_ENDPOINT", None))

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 or the window is zero.
        """
        if calls_count < 1:
            raise InvalidArgumentError(additional_data={"detail": "calls_count must be at least 1"})

        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 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 or the window is zero.
    """
    if calls_count < 1:
        raise InvalidArgumentError(additional_data={"detail": "calls_count must be at least 1"})

    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