Configuration
=============

We use ``DMR_SETTINGS`` dictionary object to store all the configuration.
All keys are typed with :class:`~dmr.settings.Settings` enum keys
which can be used to both set and get settings.

.. note::

  Remember, that ``django-modern-rest`` settings
  are cached after the first access.
  If you need to modify settings dynamically
  in runtime use :func:`~dmr.settings.clear_settings_cache`.
  You can modify the size of cache with adjusting
  :envvar:`DMR_MAX_CACHE_SIZE` value.

Here are all keys and values that can be set.
As usual, all settings go to ``settings.py`` file in your Django project.

.. seealso::

  - Official Django settings docs:
    https://docs.djangoproject.com/en/5.2/topics/settings
  - ``django-split-settings`` configuration helper
    https://github.com/wemake-services/django-split-settings


We also validate defined settings in import time.
See :ref:`settings_validation` for more details.


Settings
--------

Class that can be used to properly type settings in user's code:

.. autoclass:: dmr.settings.SettingsDict
  :members:


Class with all possible setting keys as enum:

.. autoclass:: dmr.settings.Settings
  :show-inheritance:

  To get settings use
  :func:`~dmr.settings.resolve_setting` function
  together with ``Settings`` keys:

  .. code:: python

    >>> from dmr.settings import Settings, resolve_setting

    >>> resolve_setting(Settings.responses)
    []

  To set settings use:

  .. code:: python

    >>> DMR_SETTINGS = {Settings.responses: []}


Content negotiation
-------------------

.. note::

  It is recommended to always install ``msgspec``
  with ``'django-modern-rest[msgspec]'`` extra for better performance.

.. data:: dmr.settings.Settings.parsers

  Default: :class:`dmr.parsers.JsonParser` or
  :class:`dmr.plugins.msgspec.MsgspecJsonParser` if installed.

  A list of instances of subtypes
  of :class:`~dmr.parsers.Parser`
  to serialize data from the requested text format,
  like json or xml, into python object.

  Custom configuration example, let's say you want to always use ``ujson``:

  .. code-block:: python
    :caption: settings.py

    >>> from dmr.parsers import JsonParser
    >>> DMR_SETTINGS = {Settings.parsers: [JsonParser()]}

.. data:: dmr.settings.Settings.renderers

  Default: :class:`dmr.renderers.JsonRenderer` or
  :class:`dmr.plugins.msgspec.MsgspecJsonRenderer` if installed.

  A list of instances of subtypes
  of :class:`~dmr.renderers.Renderer`
  to serialize python objects to the requested text format, like json or xml.

  Custom configuration example, let's say you want to always use ``ujson``:

  .. code-block:: python
    :caption: settings.py

    >>> from dmr.renderers import JsonRenderer
    >>> DMR_SETTINGS = {Settings.renderers: [JsonRenderer()]}

.. data:: dmr.settings.Settings.validate_negotiation

  Default: ``None``

  Should we validate content negotiation?
  Meaning: ``django-modern-rest`` finds which parser and which renderer
  to use based on ``Content-Type`` and ``Accept`` headers.
  However, by mistake people can return responses with wrong ``Content-Type``
  if they construct responses manually.
  See :doc:`negotiation` for more info.

  Defaults to the value set in :data:`~dmr.settings.Settings.validate_responses`
  for convenience if this value is ``None``.

  To disable the content negotiation validation globally, use:

  .. code-block:: python
    :caption: settings.py

    >>> DMR_SETTINGS = {
    ...     Settings.validate_negotiation: False,
    ... }


Response handling
-----------------

.. data:: dmr.settings.Settings.responses

  Default: ``[]``

  The list of global :class:`~dmr.metadata.ResponseSpec`
  object that will be added to all endpoints' metadata
  as a possible response schema.

  Use it to set global responses' status codes like ``500``:

  .. code-block:: python
    :caption: settings.py

    >>> from http import HTTPStatus
    >>> from typing_extensions import TypedDict
    >>> from dmr import ResponseSpec

    >>> class Error(TypedDict):
    ...     detail: str

    >>> # If our API can always return a 500 response with `{"detail": str}`
    >>> # error message:
    >>> DMR_SETTINGS = {
    ...     Settings.responses: [
    ...         ResponseSpec(
    ...             Error,
    ...             status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
    ...         ),
    ...     ],
    ... }

.. data:: dmr.settings.Settings.validate_responses

  Default: ``True``

  When some endpoint returns any data, we by default validate
  that this data matches the endpoint schema.

  So, this code will produce not only static typing error,
  but also a runtime error:

  .. code:: python

    >>> from dmr import Controller
    >>> from dmr.plugins.pydantic import PydanticSerializer

    >>> class MyController(Controller[PydanticSerializer]):
    ...     def get(self) -> list[str]:
    ...         return [1, 2]  # <- both static typing and runtime error

  But, there's a runtime cost to this. It is recommended to switch
  this validation off for production:

  .. code-block:: python
    :caption: settings.py

    >>> DMR_SETTINGS = {Settings.validate_responses: False}

  .. note::

    You can also switch off this validation per-controller
    with :attr:`~dmr.controller.Controller.validate_responses`
    and per-endpoint with ``validate_responses`` argument
    to :func:`~dmr.endpoint.modify`
    and :func:`~dmr.endpoint.validate`.

.. data:: dmr.settings.Settings.semantic_responses

  Default: ``True``

  When ``True``, parsers, renderers, and authentication classes
  automatically inject their semantic response specs
  (e.g. ``422 Unprocessable Entity``, ``406 Not Acceptable``)
  into every endpoint's metadata.
  These responses are then visible in the generated OpenAPI spec.

  Set to ``False`` to disable this auto-injection globally.
  User-defined responses (via ``@modify``, ``@validate``,
  controller ``responses``, or ``Settings.responses``)
  are not affected by this flag.
  Runtime response validation still works as configured
  by ``Settings.validate_responses``.

  .. code-block:: python
    :caption: settings.py

    >>> DMR_SETTINGS = {Settings.semantic_responses: False}

.. data:: dmr.settings.Settings.exclude_semantic_responses

  Default: ``frozenset()``

  Pass any status code, that you wanna exclude from semantic responses.

  .. code-block:: python
    :caption: settings.py

    >>> from http import HTTPStatus

    >> DMR_SETTINGS = {
    ...    Settings.exclude_semantic_responses: {
    ...        HTTPStatus.CONFLICT,
    ...    },
    ... }

  When this value is set to ``None`` at any level,
  this means that the value is reset.
  For example, setting ``exclude_semantic_responses=None`` on endpoint level
  will cancel all controller and settings level values
  and enable all responses back again.


Error handling
--------------

.. data:: dmr.settings.Settings.global_error_handler

  Default: ``'dmr.errors.global_error_handler'``

  Globally handle all errors in the application.
  You can use real object or string path for the object to be imported.
  Here's our error handling hierarchy:

  1. Per-endpoint with
     :meth:`~dmr.endpoint.Endpoint.handle_error`
     and :meth:`~dmr.endpoint.Endpoint.handle_async_error`
  2. Per-controller with
     :meth:`~dmr.controller.Controller.handle_error`
     or :meth:`~dmr.controller.Controller.handle_async_error`
  3. If nothing helped, ``'global_error_handler'`` is called

  See :func:`~dmr.errors.global_error_handler`
  for the callback type.

  .. code-block:: python
    :caption: settings.py

    >>> DMR_SETTINGS = {Settings.global_error_handler: 'path.to.your.handler'}


Authentication
--------------

.. data:: dmr.settings.Settings.auth

  Default: ``[]``

  Configure authentication rules for the whole API.

  To enable auth for all endpoints you can use:

  .. code-block:: python
    :caption: settings.py

    >>> from dmr.security.django_session import DjangoSessionSyncAuth

    >>> DMR_SETTINGS = {
    ...     Settings.auth: [
    ...         DjangoSessionSyncAuth(),
    ...     ],
    ... }

  If you use both sync and async controllers in your app,
  consider using :class:`~dmr.security.SyncOrAsyncAuth` for settings.
  All auth types must be importable in settings.


Throttling
----------

.. data:: dmr.settings.Settings.throttling

  Default: ``[]``

  Configure throttling rules for the whole API.

  To enable throttling for all endpoints you can use:

  .. code-block:: python
    :caption: settings.py

    >>> from dmr.settings import Settings
    >>> from dmr.throttling import SyncThrottle, Rate

    >>> DMR_SETTINGS = {
    ...     Settings.throttling: [SyncThrottle(10, Rate.second)],
    ... }

  If you use both sync and async controllers in your app,
  consider using :class:`~dmr.throttling.SyncOrAsyncThrottle` for settings.
  All throttle types must be importable in settings.


.. data:: dmr.settings.Settings.throttling_allow_unsafe_cache

  Default: ``True``

  By default we emit
  :class:`~dmr.throttling.backends.django_cache.UnsafeCacheBackendWarning`
  at startup if an unsafe cache backend is detected
  for throttling backend instance (``LocMemCache``, ``DummyCache``).
  These backends do not share state between processes,
  so throttling counters are not consistent in multi-process deployments.

  When set to ``False``,
  we raise :exc:`~django.core.exceptions.ImproperlyConfigured` instead.

  Set to ``None`` to completely disable this check:
  no warnings or errors will be produced.

  .. code-block:: python
    :caption: settings.py

    >>> from dmr.settings import Settings

    >>> DMR_SETTINGS = {
    ...     Settings.throttling_allow_unsafe_cache: False,
    ... }


HTTP Spec validation
--------------------

.. data:: dmr.settings.Settings.no_validate_http_spec

  Default: ``frozenset()``

  A set of unique :class:`~dmr.settings.HttpSpec` codes
  to be globally disabled.

  We don't recommend disabling any of these checks globally.

  .. code-block:: python
    :caption: settings.py

    >>> from dmr.settings import HttpSpec

    >>> DMR_SETTINGS = {
    ...     Settings.no_validate_http_spec: {
    ...         HttpSpec.empty_request_body,
    ...     },
    ... }

  When this value is set to ``None`` at any level,
  this means that the value is reset.
  For example, setting ``no_validate_http_spec=None`` on endpoint level
  will cancel all controller and settings level values
  and enable all validation back again.


.. autoclass:: dmr.settings.HttpSpec
  :show-inheritance:
  :members:


Streaming
---------

.. data:: dmr.settings.Settings.validate_events

  Default: ``None``

  Should we validate the events in all streams?
  Defaults to the value set in :data:`~dmr.settings.Settings.validate_responses`
  for convenience if this value is ``None``.

  To disable the event validation globally, use:

  .. code-block:: python
    :caption: settings.py

    >>> DMR_SETTINGS = {
    ...     Settings.validate_events: False,
    ... }


OpenAPI
-------

.. data:: dmr.settings.Settings.openapi_config

  Default: ``OpenAPIConfig(title='Your Awesome Project', version='0.1.0')``

  Metadata to be used in the OpenAPI schema.

  See :class:`~dmr.openapi.OpenAPIConfig`
  for the available fields and their description.
  It can also be used to change the default OpenAPI spec version.

  .. code-block:: python
    :caption: settings.py

    >>> from dmr.openapi.config import OpenAPIConfig

    >>> DMR_SETTINGS = {
    ...     Settings.openapi_config: OpenAPIConfig(
    ...         title='My Amazing App',
    ...         version='13.22.3',
    ...         openapi_version='3.2.0',
    ...     ),
    ... }

.. data:: dmr.settings.Settings.openapi_examples_seed

  Default: ``None``

  Random seed to use when generating missing examples in the OpenAPI spec.

  If set to ``None``, no examples are generated.
  Existing examples won't be overridden.

  It only works if ``'django-modern-rest[openapi]'`` extra is installed.
  If `polyfactory <https://github.com/litestar-org/polyfactory>`_
  package is missing, no examples are generated.

  .. code-block:: python
    :caption: settings.py

    >>> from dmr.settings import HttpSpec
    >>> from dmr.openapi.config import OpenAPIConfig

    >>> DMR_SETTINGS = {
    ...     Settings.openapi_examples_seed: 10,
    ... }

.. data:: dmr.settings.Settings.openapi_static_cdn

  Default: ``{}``

  Optional mapping to switch OpenAPI renderers to CDN resources.
  Only renderers explicitly listed in this mapping will use CDN,
  the rest will use local bundled static files served by Django.

  Supported keys are:

  - ``swagger``: base URL to ``swagger-ui-dist`` (without file name)
  - ``redoc``: full URL to ``redoc.standalone.js``
  - ``scalar``: full URL to ``@scalar/api-reference`` standalone bundle
  - ``stoplight``: base URL to ``@stoplight/elements`` (without file name)

  You can also modify the exact versions that we use for each tool this way.

  .. code-block:: python
    :caption: settings.py

    >>> DMR_SETTINGS = {
    ...     Settings.openapi_static_cdn: {
    ...         'swagger': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.1',
    ...         'redoc': 'https://cdn.redoc.ly/redoc/2.5.2/bundles/redoc.standalone.js',
    ...         'scalar': 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.49.2/dist/browser/standalone.js',
    ...         'stoplight': 'https://unpkg.com/@stoplight/elements@9.0.16',
    ...     },
    ... }


Hacks
-----

.. data:: dmr.settings.Settings.django_treat_as_post

  Default: ``frozenset({'PUT', 'PATCH'})``

  By default Django only populates
  :attr:`django.http.HttpRequest.POST`
  and :attr:`django.http.HttpRequest.FILES`
  for ``POST`` requests.

  Some parsers like :class:`~dmr.parsers.MultiPartParser`
  require ``.POST`` and ``.FILES`` to be set to work with
  :data:`~dmr.components.Body`
  and :data:`~dmr.components.FileMetadata`.

  However, we build REST APIs where more methods are in use, not just ``POST``.
  So, we use this setting to populate ``.POST`` and ``.FILES``
  when parser is a subtype of either
  :class:`~dmr.parsers.SupportsFileParsing`
  or :class:`~dmr.parsers.SupportsDjangoDefaultParsing`
  and we work with components that require request body.

  .. note::

    This is a really advanced setting. If you are not creating
    your own components or your own parsers - do not change it.


Environment variables
---------------------

.. envvar:: DMR_MAX_CACHE_SIZE

  Default: ``256``

  We use :func:`functools.lru_cache` in many places internally.
  For example:

  - To create json encoders and decoders only once
  - To create type validation objects
    in :class:`~dmr.serializer.BaseEndpointOptimizer`

  You can control the size / memory usage with this setting.

  Increase if you have a lot of different return types.

.. envvar:: DMR_USE_COMPILED

  Default: ``1``

  We compile some modules to C-extensions with :ref:`mypyc`.
  If you want to disable the extensions and fallback to pure Python
  implementation, set this variable to ``0``.

  It is only recommended for debugging.
  It should be set to ``1`` in production for maximum speed.


API Reference
-------------

.. autofunction:: dmr.settings.resolve_setting

.. autofunction:: dmr.settings.clear_settings_cache
