Source code for smarter.apps.prompt.views.detailviews.prompt_config_view

# pylint: disable=W0613,C0302
"""PromptConfigView is a Django class-based view responsible for providing configuration data to the ReactJS prompt UI component in the Smarter web application."""

from typing import Any, Optional, Union

from django.http import (
    HttpRequest,
    HttpResponse,
    HttpResponseNotAllowed,
    JsonResponse,
)
from pydantic import ValidationError

from smarter.apps.account.models import get_resolved_user
from smarter.apps.llmclient.models import (
    LLMClient,
    LLMClientFunctions,
    LLMClientHelper,
    LLMClientPlugin,
    LLMClientRequests,
    get_cached_llmclient_by_request,
)
from smarter.apps.llmclient.serializers import (
    LLMClientConfigSerializer,
)
from smarter.apps.plugin.models import (
    PluginSelectorHistory,
)
from smarter.apps.prompt.contract.builders import (
    PromptChatApiError,
    PromptChatApiErrorForbidden,
    PromptChatApiErrorInternal,
    PromptChatApiErrorNotFound,
    chat_error_response,
    requested_contract,
)
from smarter.apps.prompt.contract.const import CHAT_CONTRACT_V1, CHAT_CONTRACT_V2
from smarter.apps.prompt.contract.models import (
    PromptChatApiConfig,
    PromptChatApiFunctionList,
    PromptChatApiHistory,
    PromptChatApiListMetaData,
    PromptChatApiLLMClient,
    PromptChatApiLLMClientFunction,
    PromptChatApiLLMClientPlugin,
    PromptChatApiLLMClientRequest,
    PromptChatApiMetaData,
    PromptChatApiPluginList,
    PromptChatApiPluginSelection,
    PromptChatApiPromptPluginUsage,
    PromptChatApiPromptToolCall,
)
from smarter.apps.prompt.signals import prompt_config_invoked
from smarter.apps.prompt.views.detailviews.prompt_workbench_view import (
    SmarterPromptSession,
)
from smarter.common.conf import smarter_settings
from smarter.common.const import (
    SMARTER_CHAT_SESSION_KEY_NAME,
    SmarterHttpMethods,
)
from smarter.common.helpers.url_helpers import clean_url
from smarter.common.utils import is_authenticated_request
from smarter.lib import logging
from smarter.lib.django import waffle
from smarter.lib.django.validators import SmarterValidator
from smarter.lib.django.views import (
    SmarterOptionallyAuthenticatedApiView,
)
from smarter.lib.django.waffle import SmarterWaffleSwitches
from smarter.lib.drf.views.helpers import UnauthenticatedPermissionClass
from smarter.lib.journal.enum import SmarterJournalCliCommands, SmarterJournalThings
from smarter.lib.journal.http import (
    SmarterJournaledJsonErrorResponse,
    SmarterJournaledJsonResponse,
)
from smarter.lib.logging import WaffleSwitchedLoggerWrapper
from smarter.lib.typing import SmarterDictType

MAX_RETURNED_PLUGINS = 10


[docs] def should_log(level): """Check if logging should be done based on the waffle switch.""" return waffle.switch_is_active(SmarterWaffleSwitches.PROMPT_LOGGING)
base_logger = logging.getLogger(__name__) logger = WaffleSwitchedLoggerWrapper(base_logger, should_log)
[docs] def should_log_verbose(level): """Check if logging should be done based on the waffle switch.""" return smarter_settings.verbose_logging
verbose_logger = WaffleSwitchedLoggerWrapper(base_logger, should_log_verbose)
[docs] class PromptConfigView(SmarterOptionallyAuthenticatedApiView): """ Prompt configuration view for the Smarter web application. This view is responsible for providing all configuration information required by the ReactJS prompt UI component. It is designed to be performant and efficient, as it is invoked on every user-facing browser load and refresh. **Key Features:** - **Django Template-Based:** This view uses Django's template system for rendering and does not rely on Django REST Framework (DRF) serializers for its main response. The configuration data is returned as a JSON response, but the view itself is structured as a Django class-based view. - **CSRF Exempt:** The view is decorated with `@csrf_exempt` because it is read-only and does not modify server-side data. This exemption is safe in this context and avoids unnecessary CSRF validation for GET and POST requests that only retrieve configuration data. - **ReactJS UI Integration:** The endpoint provides all necessary configuration and context information for the ReactJS prompt component. This includes llmclient metadata, plugin information, session keys, and historical data relevant to the prompt session. The React app consumes this configuration to initialize and render the prompt UI in the user's browser. - **Session-Based Architecture:** Each prompt session is uniquely identified and managed by a globally unique 64-character session key. The session key is generated by RequestHelper.generate_session_key() and is persisted by the browser in a domain and path-specific cookie, meaning that this design supports an unlimited number of concurrent sessions per user, one per device per llmclient. - **Performance Considerations:** Since this endpoint is called on every browser load and refresh, performance is a primary concern. The view is optimized to minimize database queries and serialization overhead. Only a limited number of plugins (as defined by `MAX_RETURNED_PLUGINS`) are returned to avoid excessive payload sizes. Caching and efficient queryset usage are employed where possible to ensure fast response times for end users. **Example Usage:** https://smarter.3141-5926-5359.alpha.api.example.com/config/ **Returns:** JSON response containing: - `session_key`: Unique identifier for the prompt session. - `sandbox_mode`: Boolean indicating if the llmclient is running in sandbox mode. - `debug_mode`: Boolean indicating if debug mode is enabled. - `llmclient`: Serialized llmclient configuration. - `history`: Prompt and plugin selector history for the session. - `meta_data`: Additional metadata for the llmclient. - `plugins`: Plugin metadata and a limited list of plugins. **Security:** - The view is protected and requires the user to be authenticated, unless the llmclient is configured to allow unauthenticated access. - If authentication is required and the user is not authenticated, a 403 Forbidden response is returned. **See Also:** - `SmarterPromptSession`: Helper class for managing prompt sessions. - `LLMClientConfigSerializer`: Serializer for the llmclient's data. - `LLMClientHelper`: Helper for llmclient-related operations. """ authentication_classes = None permission_classes = (UnauthenticatedPermissionClass,) thing: Optional[SmarterJournalThings] = None command: Optional[SmarterJournalCliCommands] = None session: Optional[SmarterPromptSession] = None legacy_keys: bool = True """Rename keys for older versions of the React app ('llmclient' to 'chatbot'), in version 1 of the contract. The cli passes as_view(legacy_keys=False). """ include_diagnostics: bool = False """Return the complete configuration, whatever url resolved the LLMClient. The cli passes as_view(include_diagnostics=True). See :meth:`full_config`. """ contract: int = CHAT_CONTRACT_V1 """The version of the contract that the client asked for. See :func:`~smarter.apps.prompt.contract.builders.requested_contract`. """ resolved_by_id: bool = False """True if the LLMClient was found by its id or hashed id, which also checks that the user may read it.""" llmclient_name: Optional[str] = None _llmclient_helper: Optional[LLMClientHelper] = None _llmclient: Optional[LLMClient] = None @property def formatted_class_name(self) -> str: """Returns a formatted string of the class name for logging purposes.""" class_name = f"{__name__}.{PromptConfigView.__name__}[{id(self)}]" return self.formatted_text(class_name) @property def llmclient(self) -> Optional[LLMClient]: return self._llmclient @property def llmclient_helper(self) -> Optional[LLMClientHelper]: if self._llmclient_helper: return self._llmclient_helper if self.llmclient: # throw everything but the kitchen sink at the LLMClientHelper self._llmclient_helper = LLMClientHelper( request=self.smarter_request, session_key=self.session.session_key if self.session else None, name=self._llmclient.name if self._llmclient else self.llmclient_name, llmclient_id=self._llmclient.id if self._llmclient else None, # type: ignore[union-attr] account=self.account, user=self.user, user_profile=self.user_profile, ) else: self._llmclient_helper = LLMClientHelper( request=self.smarter_request, name=self.llmclient_name or self.smarter_request_llmclient_name, session_key=self.session.session_key if self.session else None, account=self.account, user=self.user, user_profile=self.user_profile, ) if self._llmclient_helper: logger.debug( "%s.llmclient_helper property - instantiated LLMClientHelper with llmclient=%s, name=%s", self.formatted_class_name, self._llmclient_helper.llmclient, self._llmclient_helper.name, ) else: logger.warning( "%s.llmclient_helper property - could not instantiate LLMClientHelper", self.formatted_class_name ) return self._llmclient_helper @llmclient_helper.setter def llmclient_helper(self, value: Optional[LLMClientHelper]): self._llmclient_helper = value if self._llmclient_helper and self._llmclient_helper.llmclient: self._llmclient = self._llmclient_helper.llmclient self.account = self._llmclient_helper.account self.user = self._llmclient_helper.user self.user_profile = self._llmclient_helper.user_profile verbose_logger.debug( "%s - llmclient_helper() setter llmclient=%s", self.formatted_class_name, self.llmclient ) else: self._llmclient = None verbose_logger.debug("%s - llmclient_helper() setter llmclient is unset", self.formatted_class_name)
[docs] def clean_url(self, url: str) -> str: """Clean the url of any query strings and trailing '/config/' strings.""" retval = clean_url(url) if retval.endswith("/config/"): retval = retval[:-8] return retval
@property def full_config(self) -> bool: """ Whether the client receives the complete configuration. The complete configuration includes the LLMClient's every field, e.g. its owner's profile, the session's diagnostic histories, and the lists of plugins and functions. It is returned when the LLMClient was found by its id or hashed id, i.e. by a workbench or platform api url, which checks that the user may read it, and to the cli. A deployed LLMClient's own host, which pages that embed Smarter Chat call, receives :func:`~smarter.apps.prompt.contract.builders.public_config`. :returns: True for the complete configuration. :rtype: bool """ return self.resolved_by_id or self.include_diagnostics
[docs] def prompt_url(self, url_llmclient: Optional[str]) -> Optional[str]: """ The url of the LLMClient's prompt api, on the host that served the configuration. A deployed LLMClient's host, or its custom domain, also serves its prompt api, at ``/prompt/``, so that a page that embeds Smarter Chat needs access to one host only. A workbench or platform api url keeps the platform's prompt api url. :param url_llmclient: The LLMClient's prompt api url on the platform, ``LLMClient.url_llmclient``. :type url_llmclient: Optional[str] :returns: The prompt api url to return to the client. :rtype: Optional[str] """ if self.resolved_by_id or not self.smarter_request: return url_llmclient host = self.smarter_request.get_host() return SmarterValidator.urlify(f"{host}/prompt/", environment=smarter_settings.environment)
[docs] def error_response(self, request: HttpRequest, error: PromptChatApiError) -> SmarterJournaledJsonErrorResponse: """ The configuration api's error response, the same shape for every error. See :func:`~smarter.apps.prompt.contract.builders.chat_error_response`. :param request: The request. :type request: django.http.HttpRequest :param error: The error. :type error: PromptChatApiError :returns: The error response, without a stack trace. :rtype: SmarterJournaledJsonErrorResponse """ return chat_error_response( request, error, thing=SmarterJournalThings(SmarterJournalThings.PROMPT_CONFIG), command=SmarterJournalCliCommands(SmarterJournalCliCommands.PROMPT_CONFIG), )
[docs] def chat_config(self) -> Optional[PromptChatApiConfig]: """ Assemble the configuration required by the ReactJS prompt UI component, as the contract's model. This method gathers all relevant context and configuration data for the prompt session and llmclient, and returns it as a :class:`~smarter.apps.prompt.contract.models.PromptChatApiConfig`, the Pydantic model of version 2 of the Smarter Chat api contract. :meth:`config` derives the response from it: the complete or public configuration, in version 2 or 1 of the contract. **Performance:** Since this endpoint is called on every browser load and refresh, the logic is optimized to minimize database queries and serialization overhead. Only a limited number of plugins (see ``MAX_RETURNED_PLUGINS``) are returned to keep payloads small and response times fast. :returns: The configuration, or None if the session or the LLMClient helper is not set. Its fields: ``session_key``, ``sandbox_mode``, ``debug_mode``, ``llmclient`` (serialized by ``LLMClientConfigSerializer``, with ``url_llmclient`` from :meth:`prompt_url`), ``history`` (the session's messages in ``chat_history``, and its diagnostic histories), ``meta_data`` (``LLMClientHelper.to_json()``), ``plugins`` and ``functions``. :rtype: Optional[PromptChatApiConfig] :raises pydantic.ValidationError: If the data breaks the contract. See Also -------- SmarterPromptSession : Helper class for managing prompt sessions. LLMClientConfigSerializer : Serializer for the llmclient's data. LLMClientHelper : Helper for llmclient-related operations. """ # add llmclient_request_history and plugin_selector_history to history # these have to be added here due to circular import issues. if self.session is None: logger.error( "%s.config() - session is None. Cannot retrieve llmclient request history.", self.formatted_class_name ) return None if self.llmclient_helper is None: logger.error( "%s.config() - LLMClientHelper is None. Cannot retrieve llmclient request history.", self.formatted_class_name, ) return None llmclient_serializer = ( LLMClientConfigSerializer(self.llmclient, context={"request": self.smarter_request}) if self.llmclient else None ) # plugins context. the main thing we need here is to constrain the number of plugins # returned to some reasonable number, since we'll probaably have cases where # the llmclient has a lot of plugins (hundreds, thousands...). llmclient_plugins_count = LLMClientPlugin.objects.filter(llmclient=self.llmclient).count() llmclient_plugins = ( LLMClientPlugin.objects.filter(llmclient=self.llmclient) .select_related("plugin_meta") .prefetch_related("plugin_meta__tags") .order_by("-pk")[:MAX_RETURNED_PLUGINS] ) llmclient_functions_count = LLMClientFunctions.objects.filter(llmclient=self.llmclient).count() llmclient_functions = LLMClientFunctions.objects.filter(llmclient=self.llmclient).order_by("-pk")[ :MAX_RETURNED_PLUGINS ] # the session's history, from its rows. The messages are "prompt_history" in PromptHelper. chat_helper = self.session.chat_helper session_key = self.session.session_key history = PromptChatApiHistory( chat_history=(chat_helper.prompt_history if chat_helper else None) or [], prompt_tool_call_history=[ PromptChatApiPromptToolCall.from_model(row) for row in (chat_helper.prompt_tool_call if chat_helper else []) ], prompt_plugin_usage_history=[ PromptChatApiPromptPluginUsage.from_model(row) for row in (chat_helper.prompt_plugin_usage if chat_helper else []) ], plugin_selector_history=[ PromptChatApiPluginSelection.from_model(row) for row in PluginSelectorHistory.objects.filter(session_key=session_key).order_by("-id") ], llmclient_request_history=[ PromptChatApiLLMClientRequest.from_model(row) for row in LLMClientRequests.objects.filter(session_key=session_key).order_by("-id") ], ) llmclient = self.to_snake_case(llmclient_serializer.data) if llmclient_serializer else {} llmclient["url_llmclient"] = self.prompt_url(llmclient.get("url_llmclient")) return PromptChatApiConfig( contract=CHAT_CONTRACT_V2, session_key=self.session.session_key, sandbox_mode=self.llmclient_helper.is_llmclient_sandbox_url, debug_mode=waffle.switch_is_active(SmarterWaffleSwitches.ENABLE_REACTAPP_DEBUG_MODE), llmclient=PromptChatApiLLMClient(**llmclient), history=history, meta_data=PromptChatApiMetaData(**self.to_snake_case(self.llmclient_helper.to_json())), plugins=PromptChatApiPluginList( meta_data=PromptChatApiListMetaData( total_plugins=llmclient_plugins_count, plugins_returned=len(llmclient_plugins) ), plugins=[PromptChatApiLLMClientPlugin.from_model(plugin) for plugin in llmclient_plugins], ), functions=PromptChatApiFunctionList( meta_data=PromptChatApiListMetaData( total_functions=llmclient_functions_count, functions_returned=len(llmclient_functions) ), functions=[PromptChatApiLLMClientFunction.from_model(function) for function in llmclient_functions], ), )
[docs] def config(self) -> SmarterDictType: """ The configuration api's response body: :meth:`chat_config`, in the form that the client receives. - The complete configuration, or :meth:`PromptChatApiConfig.public() <smarter.apps.prompt.contract.models.PromptChatApiConfig.public>` unless :attr:`full_config`. - Version 2 of the contract to a client that asks for it, and to the cli (``legacy_keys=False``), or :meth:`PromptChatApiConfig.to_v1() <smarter.apps.prompt.contract.models.PromptChatApiConfig.to_v1>`. :returns: ``{"data": <the configuration>}``, or ``{}`` without a session or LLMClient helper. :rtype: SmarterDictType :raises pydantic.ValidationError: If the configuration breaks the contract. """ chat_config = self.chat_config() if chat_config is None: return {} if not self.full_config: chat_config = chat_config.public() if self.contract == CHAT_CONTRACT_V2 or not self.legacy_keys: data = chat_config.model_dump(mode="json", exclude_unset=True) else: # version 1, for older versions of the React app, which expect 'chatbot' instead of 'llmclient'. data = chat_config.to_v1().model_dump(mode="json", exclude_unset=True) retval = {"data": data} prompt_config_invoked.send(sender=self.__class__, instance=self, request=self.smarter_request, data=retval) return retval
[docs] def dispatch( self, request: HttpRequest, *args, **kwargs ) -> Union[JsonResponse, HttpResponse, SmarterJournaledJsonErrorResponse]: """ Handles incoming HTTP requests for the prompt configuration endpoint. This method is responsible for orchestrating the retrieval and assembly of all configuration data required by the ReactJS prompt UI component. It is invoked on every user-facing browser load and refresh, making performance a critical concern. **Key Details:** - **Django Template-Based:** This view uses Django's class-based view and template system, not Django REST Framework (DRF). The response is a JSON object, but the view logic is not DRF-based. - **CSRF Exempt:** The view is decorated with ``@csrf_exempt`` because it is strictly read-only and does not modify server-side data. This avoids unnecessary CSRF validation for GET and POST requests that only retrieve configuration data. - **ReactJS UI Integration:** The endpoint provides all configuration and context information needed by the ReactJS prompt component, including llmclient metadata, plugin information, session keys, and prompt history. - **Session-Based:** Sessions are managed by the :class:`SmarterPromptSession` helper, which uniquely defines a prompt session using a combination of the user's IP address and device-identifying information. This ensures each device/browser instance receives a unique session key, which is used to track prompt history and plugin usage. - **Performance:** Since this endpoint is called on every browser load and refresh, it is optimized to minimize database queries and serialization overhead. Only a limited number of plugins are returned (see ``MAX_RETURNED_PLUGINS``) to keep payloads small and response times fast. Parameters ---------- request : HttpRequest The incoming HTTP request object. llmclient_id : Optional[int], default=None The ID of the llmclient to retrieve configuration for, if specified. *args Additional positional arguments. **kwargs Additional keyword arguments. Returns ------- Union[JsonResponse, HttpResponse, SmarterJournaledJsonErrorResponse] A JSON response containing all configuration data required by the ReactJS prompt UI component, or an error response if the request is invalid or unauthorized. See Also -------- SmarterPromptSession : Helper class for managing prompt sessions. LLMClientConfigSerializer : Serializer for the llmclient's data. LLMClientHelper : Helper for llmclient-related operations. """ self.thing = SmarterJournalThings(SmarterJournalThings.PROMPT_CONFIG) self.command = SmarterJournalCliCommands(SmarterJournalCliCommands.PROMPT_CONFIG) self.contract = requested_contract(request) self.resolved_by_id = False hashed_id = kwargs.pop("hashed_id", None) if hashed_id: llmclient_id = LLMClient.id_from_hashed_id(hashed_id) else: llmclient_id = kwargs.pop("llmclient_id", None) if llmclient_id is not None: try: self._llmclient = LLMClient.get_cached_object(pk=llmclient_id) # a 404 rather than a 403, so that the ids of other accounts' LLMClients can't be discovered. # A url with an id is a workbench or platform api url, which requires a user who may read it. user = get_resolved_user(request.user) if not self.is_authenticated: raise LLMClient.DoesNotExist(f"LLMClient {llmclient_id} is not readable by an anonymous user") if not LLMClient.objects.with_read_permission_for(user).filter(pk=llmclient_id).exists(): # type: ignore[arg-type] raise LLMClient.DoesNotExist(f"LLMClient {llmclient_id} is not readable by {user}") self.resolved_by_id = True self.llmclient_name = self._llmclient.name verbose_logger.debug( "%s.dispatch() - set llmclient=%s from llmclient_id=%s", self.formatted_class_name, self._llmclient, llmclient_id, ) except LLMClient.DoesNotExist: verbose_logger.error( "%s.dispatch() - LLMClient with id=%s does not exist. Returning 404.", self.formatted_class_name, llmclient_id, ) return self.error_response(request, PromptChatApiErrorNotFound("LLMClient not found.")) else: self.llmclient_name = kwargs.get("name") try: self._llmclient = get_cached_llmclient_by_request(request=request) if not self._llmclient: verbose_logger.debug( "%s.dispatch() - get_cached_llmclient_by_request() returned None. Attempting to instantiate LLMClientHelper with additional info", self.formatted_class_name, ) self.llmclient_helper = LLMClientHelper( request=self.smarter_request, session_key=self.session_key, llmclient_id=llmclient_id, name=self.llmclient_name, account=self.account, user=self.user, user_profile=self.user_profile, ) except LLMClient.DoesNotExist: return self.error_response(request, PromptChatApiErrorNotFound("LLMClient not found.")) # Initialize the prompt session for this request. session_key is generated # and managed by the /config/ endpoint for the llmclient # # examples: # - https://customer-support.3141-5926-5359.api.example.com/workbench/config/?session_key=123456 # - http://localhost:9357/api/v1/llm-clients/1556/prompt/config/?session_key=0733c7b3a33e7eb733c95f6d7fc88d671b6c957e6857a04d05b3f5905167116f # # The React app calls this endpoint at app initialization to get a # json dict that includes, among other pertinent info, this session_key # which uniquely identifies the device and the individual llmclient session # for the device. if not self.llmclient: # before the session, which requires an LLMClient. return self.error_response(request, PromptChatApiErrorNotFound("LLMClient not found.")) self.session = SmarterPromptSession(request, session_key=self.session_key, llmclient=self.llmclient) # "is not None", not truthiness: LLMClientHelper is a SmarterRequestMixin, whose __bool__ is its # readiness, so a helper that isn't ready would skip this check. if ( self.llmclient_helper is not None and self.llmclient_helper.is_authentication_required and not is_authenticated_request(request) ): return self.error_response( request, PromptChatApiErrorForbidden( "Authentication failed. This LLMClient requires a Smarter api key, or a logged-in user." ), ) verbose_logger.debug( "%s - llmclient=%s - llmclient_helper=%s", self.formatted_class_name, self.llmclient, self.llmclient_helper, ) if not (self.resolved_by_id or self.include_diagnostics) and not self.llmclient.deployed: # a host that isn't a workbench or platform api url serves deployed LLMClients only. return self.error_response(request, PromptChatApiErrorNotFound("LLMClient not found.")) verbose_logger.debug( "%s.dispatch() completed with llmclient=%s, session_key=%s", self.formatted_class_name, self.llmclient, self.session.session_key if self.session else "(Missing session)", ) return super().dispatch(request, *args, **kwargs) # type: ignore[return-value]
def __str__(self) -> str: return str(self.llmclient) if self.llmclient else "PromptConfigView" # pylint: disable=unused-argument
[docs] def post( self, request: HttpRequest, *args, **kwargs ) -> Union[SmarterJournaledJsonResponse, SmarterJournaledJsonErrorResponse]: """Get the llmclient configuration.""" logger.debug("%s - post()", self.formatted_class_name) return self.config_response(request)
[docs] def config_response( self, request: HttpRequest ) -> Union[SmarterJournaledJsonResponse, SmarterJournaledJsonErrorResponse]: """ The configuration, as the response to the client. :param request: The request. :type request: django.http.HttpRequest :returns: The configuration, or an error response if it breaks the contract. :rtype: Union[SmarterJournaledJsonResponse, SmarterJournaledJsonErrorResponse] """ try: data = self.config() except ValidationError as e: logger.error("%s.config_response() the configuration breaks the contract: %s", self.formatted_class_name, e) return self.error_response( request, PromptChatApiErrorInternal("The LLMClient's configuration could not be built.") ) return SmarterJournaledJsonResponse(request=request, data=data, thing=self.thing, command=self.command)
# pylint: disable=unused-argument
[docs] def get( self, request: HttpRequest, *args, **kwargs ) -> Union[SmarterJournaledJsonResponse, SmarterJournaledJsonErrorResponse, HttpResponseNotAllowed]: """Get the llmclient configuration.""" logger.warning( "%s - get() %s should be invoked via POST instead of GET.", self.formatted_class_name, request.path ) if not waffle.switch_is_active(SmarterWaffleSwitches.ALLOW_API_GET): logger.error( "%s.get() %s is not allowed because %s switch is inactive.", self.formatted_class_name, request.path, SmarterWaffleSwitches.ALLOW_API_GET, ) return HttpResponseNotAllowed(permitted_methods=[SmarterHttpMethods.POST]) return self.config_response(request)