Source code for salespyforce.utils.core_utils

# -*- coding: utf-8 -*-
"""
:Module:            salespyforce.utils.core_utils
:Synopsis:          Collection of supporting utilities and functions to complement the primary modules
:Usage:             ``from salespyforce.utils import core_utils``
:Example:           ``encoded_string = core_utils.encode_url(decoded_string)``
:Created By:        Jeff Shurtliff
:Last Modified:     Jeff Shurtliff
:Modified Date:     02 Feb 2026
"""

import re
import random
import string
import os.path
import warnings
import urllib.parse

import requests

from . import log_utils
from .. import errors
from ..decorators import deprecated

# Initialize the logger for this module
logger = log_utils.initialize_logging(__name__)

# Define constants
SALESFORCE_ID_SUFFIX_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'
VALID_SALESFORCE_URL_PATTERN = r'^https://[a-zA-Z0-9._-]+\.salesforce\.com(/|$)'


[docs] def url_encode(raw_string): """This function encodes a string for use in URLs. :param raw_string: The raw string to be encoded :type raw_string: str :returns: The encoded string """ return urllib.parse.quote_plus(raw_string)
[docs] def url_decode(encoded_string): """This function decodes a url-encoded string. :param encoded_string: The url-encoded string :type encoded_string: str :returns: The unencoded string """ return urllib.parse.unquote_plus(encoded_string)
[docs] @deprecated(since='1.4.0', replacement='salespyforce.errors.handlers.display_warning', removal='2.0.0') def display_warning(warn_msg): """This function displays a :py:exc:`UserWarning` message via the :py:mod:`warnings` module. .. deprecated:: 1.4.0 Use :py:func:`salespyforce.errors.handlers.display_warning` instead. :param warn_msg: The message to be displayed :type warn_msg: str :returns: None """ warnings.warn(warn_msg, UserWarning)
[docs] def get_file_type(file_path): """This function attempts to identify if a given file path is for a YAML or JSON file. :param file_path: The full path to the file :type file_path: str :returns: The file type in string format (e.g. ``yaml`` or ``json``) :raises: :py:exc:`FileNotFoundError`, :py:exc:`salespyforce.errors.exceptions.UnknownFileTypeError` """ file_type = 'unknown' if os.path.isfile(file_path): if file_path.endswith('.json'): file_type = 'json' elif file_path.endswith('.yml') or file_path.endswith('.yaml'): file_type = 'yaml' else: display_warning(f"Unable to recognize the file type of '{file_path}' by its extension.") with open(file_path) as cfg_file: for line in cfg_file: if line.startswith('#'): continue else: if '{' in line: file_type = 'json' break if file_type == 'unknown': raise errors.exceptions.UnknownFileTypeError(file=file_path) else: raise FileNotFoundError(f"Unable to locate the following file: {file_path}") return file_type
[docs] def get_random_string(length=32, prefix_string=""): """This function returns a random alphanumeric string. :param length: The length of the string (``32`` by default) :type length: int :param prefix_string: A string to which the random string should be appended (optional) :type prefix_string: str :returns: The alphanumeric string """ return f"{prefix_string}{''.join([random.choice(string.ascii_letters + string.digits) for _ in range(length)])}"
[docs] def get_18_char_id(record_id: str) -> str: """This function converts a 15-character Salesforce record ID to its 18-character case-insensitive form. .. versionadded:: 1.4.0 :param record_id: The Salesforce record ID to convert (or return unchanged if already 18 characters) :type record_id: str :returns: The 18-character Salesforce record ID :raises: :py:exc:`ValueError` """ # Ensure the provided record ID is a string if not isinstance(record_id, str): raise ValueError("Salesforce ID must be a string") # Return the record ID unchanged if it is already 18 characters in length if len(record_id) == 18: return record_id # Ensure the record ID is a valid 15-character value if len(record_id) != 15: raise ValueError("Salesforce ID must be 15 or 18 characters long") # Define the checksum suffix (additional 3 characters) suffix = "" for i in range(0, 15, 5): chunk = record_id[i:i + 5] bitmask = 0 for index, char in enumerate(chunk): if "A" <= char <= "Z": bitmask |= 1 << index suffix += SALESFORCE_ID_SUFFIX_ALPHABET[bitmask] # Return the 18-character ID value return record_id + suffix
[docs] def matches_regex_pattern(pattern: str, text: str, full_match: bool = False, must_start_with: bool = False) -> bool: """This function compares a text string against a regex pattern and determines whether they match. .. versionadded:: 1.4.0 :param pattern: The regex pattern that should match :type pattern: str :param text: The text string to evaluate :type text: str :param full_match: Determines if the entire string should be validated :type full_match: bool :param must_start_with: Determines if the pattern must be at the beginning of the string :returns: True if the regex pattern matches anywhere in the text string :raises: :py:exc:`TypeError` """ if full_match: return bool(re.fullmatch(pattern, text)) elif must_start_with: return bool(re.match(pattern, text)) else: return bool(re.search(pattern, text))
[docs] def is_valid_salesforce_url(url: str) -> bool: """This function evaluates a URL to determine if it is a valid Salesforce URL. .. versionadded:: 1.4.0 :param url: The URL to evaluate :type url: str :returns: Boolean value depending on whether the URL meets the criteria """ return True if isinstance(url, str) and matches_regex_pattern(VALID_SALESFORCE_URL_PATTERN, url) else False
[docs] def get_image_ref_id(image_url): """This function parses an image URL to identify the reference ID (refid) value. (`Reference 1 <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_rich_text_image_retrieve.htm>`_, `Reference 2 <https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_rich_text_image_retrieve.htm>`_) :param image_url: The URL of an image from within Salesforce :type image_url: str :returns: The reference ID (``refid``) value """ query_params = urllib.parse.parse_qs(urllib.parse.urlparse(image_url).query) # noinspection PyTypeChecker ref_id = query_params.get('refid') ref_id = ref_id[0] if not isinstance(ref_id, str) else ref_id return ref_id
[docs] def download_image(image_url=None, file_name=None, file_path=None, response=None, extension='jpeg'): """This function downloads an image and saves it to a specified directory. :param image_url: The absolute URL to the image :type image_url: str :param file_name: The file name including extension as which to save the file :type file_name: str :param file_path: File path where the image file should be saved (default: ``var/images/``) :type file_path: str, None :param response: The response of the previously performed API call :param extension: The file extension to use if a file name with extension is not provided :type extension: str :returns: The full path to the downloaded image :raises: :py:exc:`RuntimeError` """ if not image_url and not response: raise RuntimeError('An image URL or an API response must be provided to download an image.') # Define an appropriate file path file_path = './' if not file_path else file_path file_path = f'{file_path}/' if not any((file_path.endswith('/'), file_path.endswith('\\'))) else file_path # Define a file name if not provided if not file_name: file_name = get_random_string(10, 'image_') file_name += extension # Perform the API call if not supplied if not response: response = requests.get(image_url) if response.status_code != 200: raise RuntimeError(f'The image failed to download with a {response.status_code} status code.') # Export the response data as an image file with open(f'{file_path}{file_name}', 'wb') as file: file.write(response.content) return f'{file_path}{file_name}'