Source code for salespyforce.api

# -*- coding: utf-8 -*-
"""
:Module:            salespyforce.api
:Synopsis:          Defines the basic functions associated with the Salesforce API
:Created By:        Jeff Shurtliff
:Last Modified:     Jeff Shurtliff
:Modified Date:     03 Feb 2026
"""

from __future__ import annotations

from typing import Optional

import requests

from . import errors
from .utils import core_utils, log_utils

# Define constants
DEFAULT_API_REQUEST_TIMEOUT = 30

# Initialize logging
logger = log_utils.initialize_logging(__name__)


[docs] def get( sfdc_object, endpoint: str, params: Optional[dict] = None, headers: Optional[dict] = None, timeout: Optional[int] = None, show_full_error: bool = True, return_json: bool = True, ): """This method performs a GET request against the Salesforce instance. (`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_) .. versionchanged:: 1.4.0 The full URL for the API call is now constructed prior to making the call. The provided URL is also now evaluated to ensure it is a valid Salesforce URL. Additionally, a global constant is now leveraged for the API timeout value instead of hardcoding the value. (Timeout is still **30** seconds in this version) :param sfdc_object: The instantiated SalesPyForce object :type sfdc_object: class[salespyforce.Salesforce] :param endpoint: The API endpoint to query :type endpoint: str :param params: The query parameters (where applicable) :type params: dict, None :param headers: Specific API headers to use when performing the API call :type headers: dict, None :param timeout: The timeout period in seconds (defaults to ``30``) :type timeout: int, None :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``) :type show_full_error: bool :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``) :returns: The API response in JSON format or as a ``requests`` object :raises: :py:exc:`TypeError`, :py:exc:`RuntimeError`, :py:exc:`salespyforce.errors.exceptions.InvalidURLError` """ # Define the parameters as an empty dictionary if none are provided params = {} if params is None else params # Define the headers default_headers = _get_headers(sfdc_object.access_token) headers = default_headers if not headers else headers # Construct the request URL url = _construct_full_query_url(endpoint, sfdc_object.instance_url) # Define the API request timeout (using default value if not explicitly defined with parameter) timeout = DEFAULT_API_REQUEST_TIMEOUT if not timeout else timeout # Perform the API call response = requests.get(url, headers=headers, params=params, timeout=timeout) if response.status_code >= 300: # TODO: Functionalize this segment and figure out how to improve on the approach somehow if show_full_error: raise RuntimeError(f'The GET request failed with a {response.status_code} status code.\n' f'{response.text}') else: raise RuntimeError(f'The GET request failed with a {response.status_code} status code.') # TODO: Leverage private function for this section across all API call functions (see TODO in api_call_with_payload) if return_json: response = response.json() return response
[docs] def api_call_with_payload( sfdc_object, method: str, endpoint: str, payload: dict, params: Optional[dict] = None, headers: Optional[dict] = None, timeout: Optional[int] = None, show_full_error: bool = True, return_json: bool = True, ): """This method performs a POST call against the Salesforce instance. (`Reference <https://jereze.com/code/authentification-salesforce-rest-api-python/>`_) .. versionchanged:: 1.4.0 The full URL for the API call is now constructed prior to making the call. The provided URL is also now evaluated to ensure it is a valid Salesforce URL. Additionally, a global constant is now leveraged for the API timeout value instead of hardcoding the value. (Timeout is still **30** seconds in this version) :param sfdc_object: The instantiated SalesPyForce object :type sfdc_object: class[salespyforce.Salesforce] :param method: The API method (``post``, ``put``, or ``patch``) :type method: str :param endpoint: The API endpoint to query :type endpoint: str :param payload: The payload to leverage in the API call :type payload: dict :param params: The query parameters (where applicable) :type params: dict, None :param headers: Specific API headers to use when performing the API call :type headers: dict, None :param timeout: The timeout period in seconds (defaults to ``30``) :type timeout: int, None :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``) :type show_full_error: bool :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``) :returns: The API response in JSON format or as a ``requests`` object :raises: :py:exc:`TypeError`, :py:exc:`RuntimeError`, :py:exc:`ValueError`, :py:exc:`salespyforce.errors.exceptions.InvalidURLError` """ # Define the parameters as an empty dictionary if none are provided params = {} if params is None else params # Define the headers default_headers = _get_headers(sfdc_object.access_token) headers = default_headers if not headers else headers # Construct the request URL url = _construct_full_query_url(endpoint, sfdc_object.instance_url) # Define the API request timeout (using default value if not explicitly defined with parameter) timeout = DEFAULT_API_REQUEST_TIMEOUT if not timeout else timeout # Perform the API call if method.lower() == 'post': response = requests.post(url, json=payload, headers=headers, params=params, timeout=timeout) elif method.lower() == 'patch': response = requests.patch(url, json=payload, headers=headers, params=params, timeout=timeout) elif method.lower() == 'put': response = requests.put(url, json=payload, headers=headers, params=params, timeout=timeout) else: raise ValueError('The API call method (POST or PATCH or PUT) must be defined') # Examine the result if response.status_code >= 300: if show_full_error: # TODO: Functionalize this segment and figure out how to improve on the approach somehow raise RuntimeError(f'The POST request failed with a {response.status_code} status code.\n' f'{response.text}') else: raise RuntimeError(f'The POST request failed with a {response.status_code} status code.') # TODO: Break this out into a separate private function so it can be reused and standardized if return_json: try: response = response.json() except Exception as exc: # TODO: log the exception rather than using a print statement print(f'Failed to convert the API response to JSON format due to the following exception: {exc}') return response
[docs] def delete( sfdc_object, endpoint: str, params: Optional[dict] = None, headers: Optional[dict] = None, timeout: Optional[int] = None, show_full_error: bool = True, return_json: bool = True): """This method performs a DELETE request against the Salesforce instance. .. versionadded:: 1.4.0 :param sfdc_object: The instantiated SalesPyForce object :type sfdc_object: class[salespyforce.Salesforce] :param endpoint: The API endpoint to query :type endpoint: str :param params: The query parameters (where applicable) :type params: dict, None :param headers: Specific API headers to use when performing the API call :type headers: dict, None :param timeout: The timeout period in seconds (defaults to ``30``) :type timeout: int, None :param show_full_error: Determines if the full error message should be displayed (defaults to ``True``) :type show_full_error: bool :param return_json: Determines if the response should be returned in JSON format (defaults to ``True``) :returns: The API response in JSON format or as a ``requests`` object :raises: :py:exc:`TypeError`, :py:exc:`RuntimeError`, :py:exc:`salespyforce.errors.exceptions.InvalidURLError` """ # Define the parameters as an empty dictionary if none are provided params = {} if params is None else params # Define the headers default_headers = _get_headers(sfdc_object.access_token) headers = default_headers if not headers else headers # Construct the request URL url = _construct_full_query_url(endpoint, sfdc_object.instance_url) # Define the API request timeout (using default value if not explicitly defined with parameter) timeout = DEFAULT_API_REQUEST_TIMEOUT if not timeout else timeout # Perform the API call response = requests.delete(url, headers=headers, params=params, timeout=timeout) if response.status_code >= 300: if show_full_error: # TODO: Functionalize this segment and figure out how to improve on the approach somehow raise RuntimeError(f'The DELETE request failed with a {response.status_code} status code.\n' f'{response.text}') else: raise RuntimeError(f'The DELETE request failed with a {response.status_code} status code.') # TODO: Leverage private function for this section across all API call functions (see TODO in api_call_with_payload) if return_json: response = response.json() return response
def _get_headers(_access_token: str, _header_type: str = 'default') -> dict: """This function returns the appropriate HTTP headers to use for different types of API calls.""" headers = { 'content-type': 'application/json', 'accept-encoding': 'gzip', 'authorization': f'Bearer {_access_token}' } if _header_type == 'articles': headers['accept-language'] = 'en-US' return headers def _construct_full_query_url(_endpoint: str, _instance_url: str) -> str: """This function constructs the URL to use in an API call to the Salesforce REST API. .. versionadded:: 1.4.0 :param _endpoint: The endpoint provided when calling an API call method or function :type _endpoint: str :param _instance_url: The Salesforce instance URL defined when the core object was instantiated :type _instance_url: str :returns: The fully qualified URL :raises: :py:exc:`TypeError`, :py:exc:`salespyforce.errors.exceptions.InvalidURLError` """ # Raise an exception if the endpoint is not a string if not isinstance(_endpoint, str): _exc_msg = 'The provided URL must be a string and a valid Salesforce URL' logger.critical(_exc_msg) raise TypeError(_exc_msg) # Construct the URL as needed by prepending the instance URL if _endpoint.startswith('https://'): # Only permit valid Salesforce URLs if not core_utils.is_valid_salesforce_url(_endpoint): raise errors.exceptions.InvalidURLError(url=_endpoint) _url = _endpoint else: _endpoint = f'/{_endpoint}' if not _endpoint.startswith('/') else _endpoint _url = f'{_instance_url}{_endpoint}' # Return the constructed URL return _url