content: import logging
  from datetime import timedelta
  from decimal import Decimal
  from functools import lru_cache
  from types import MappingProxyType
  from typing import Any, Dict, List, Optional, Tuple
  
  import orjson
  from django.core.serializers import serialize
  from django.core.serializers.json import DjangoJSONEncoder
  from django.db.models import (
      DateField,
      DateTimeField,
      DurationField,
      Model,
      QuerySet,
      TimeField,
  )
  from django.utils.dateparse import (
      parse_date,
      parse_datetime,
      parse_duration,
      parse_time,
  )
  from django.utils.duration import duration_string
  
  from django_unicorn.utils import is_int, is_non_string_sequence
  
  try:
      from pydantic import BaseModel as PydanticBaseModel
  except ImportError:
      PydanticBaseModel = None # type: ignore
  
  
  logger = logging.getLogger(__name__)
  
  django_json_encoder = DjangoJSONEncoder()
  
  
  class JSONDecodeError(Exception):
      pass
  
  
  class InvalidFieldNameError(Exception):
      def __init__(self, field_name: str, data: Optional[Dict] = None):
          message = f"Cannot resolve '{field_name}'."
  
          if data:
              available = ", ".join(data.keys())
              message = f"{message} Choices are: {available}"
  
          super().__init__(message)
  
  
  class InvalidFieldAttributeError(Exception):
      def __init__(self, field_name: str, field_attr: str, data: Optional[Dict] = None):
          message = f"Cannot resolve '{field_attr}'."
  
          if data:
              available = ", ".join(data[field_name].keys())
              message = f"{message} Choices on '{field_name}' are: {available}"
  
          super().__init__(message)
  
  
  def _parse_field_values_from_string(model: Model) -> None:
      """
      Convert the model fields' value to match the field type if appropriate.
  
      This is mostly to deal with field string values that will get saved as a date-related field.
      """
  
      for field in model._meta.fields:
          val = getattr(model, field.attname)
  
          if not isinstance(val, str):
              continue
  
          if isinstance(field, DateTimeField):
              setattr(model, field.attname, parse_datetime(val))
          elif isinstance(field, TimeField):
              setattr(model, field.attname, parse_time(val))
          elif isinstance(field, DateField):
              setattr(model, field.attname, parse_date(val))
          elif isinstance(field, DurationField):
              setattr(model, field.attname, parse_duration(val))
  
  
  def _get_many_to_many_field_related_names(model: Model) -> List[str]:
      """
      Get the many-to-many fields for a particular model. Returns either the automatically
      defined field name (i.e. something_set) or the related name.
      """
  
      # Use this internal method so that the fields can be cached
      @lru_cache(maxsize=128, typed=True)
      def _get_many_to_many_field_related_names_from_meta(meta):
          names = []
  
          for field in meta.get_fields():
              if field.is_relation and field.many_to_many:
                  related_name = field.name
  
                  if field.auto_created:
                      related_name = field.related_name or f"{field.name}_set"
  
                  names.append(related_name)
  
          return names
  
      return _get_many_to_many_field_related_names_from_meta(model._meta)
  
  
  def _get_m2m_field_serialized(model: Model, field_name) -> List:
      pks = []
  
      try:
          related_descriptor = getattr(model, field_name)
  
          # Get `pk` from `all` because it will re-use the cached data if the m-2-m field is prefetched
          # Using `values_list("pk", flat=True)` or `only()` won't use the cached prefetched values
          pks = [m.pk for m in related_descriptor.all()]
      except ValueError:
          # ValueError is thrown when the model doesn't have an id already set
          pass
  
      return pks
  
  
  def _handle_inherited_models(model: Model, model_json: Dict):
      """
      Handle if the model has a parent (i.e. the model is a subclass of another model).
  
      Subclassed model's fields don't get serialized
      (https://docs.djangoproject.com/en/stable/topics/serialization/#inherited-models)
      so those fields need to be retrieved manually.
      """
  
      if model._meta.get_parent_list():
          for field in model._meta.get_fields():
              if field.name not in model_json and hasattr(field, "primary_key") and not field.primary_key:
                  if field.is_relation:
                      # We already serialized the m2m fields above, so we can skip them, but need to handle FKs
                      if not field.many_to_many:
                          foreign_key_field = getattr(model, field.name)
                          foreign_key_field_pk = getattr(
                              foreign_key_field,
                              "pk",
                              getattr(foreign_key_field, "id", None),
                          )
                          model_json[field.name] = foreign_key_field_pk
                  else:
                      value = getattr(model, field.name)
  
                      # Explicitly handle `timedelta`, but use the DjangoJSONEncoder for everything else
                      if isinstance(value, timedelta):
                          value = duration_string(value)
                      else:
                          # Make sure the value is properly serialized
                          value = django_json_encoder.encode(value)
  
                          # The DjangoJSONEncoder has extra double-quotes for strings so remove them
                          if isinstance(value, str) and value.startswith('"') and value.endswith('"'):
                              value = value[1:-1]
  
                      model_json[field.name] = value
  
  
  def _get_model_dict(model: Model) -> dict:
      """
      Serializes Django models. Uses the built-in Django JSON serializer, but moves the data around to
      remove some unnecessary information and make the structure more compact.
      """
  
      _parse_field_values_from_string(model)
  
      # Django's `serialize` method always returns a string of an array,
      # so remove the brackets from the resulting string
      serialized_model = serialize("json", [model])[1:-1]
  
      # Convert the string into a dictionary and grab the `pk`
      model_json = orjson.loads(serialized_model)
      model_pk = model_json.get("pk")
  
      # Shuffle around the serialized pieces to condense the size of the payload
      model_json = model_json.get("fields")
      model_json["pk"] = model_pk
  
      # Set `pk` for models that subclass another model which only have `id` set
      if not model_pk:
          model_json["pk"] = model.pk or model.id #type: ignore
  
      # Add in m2m fields
      m2m_field_names = _get_many_to_many_field_related_names(model)
  
      for m2m_field_name in m2m_field_names:
          model_json[m2m_field_name] = _get_m2m_field_serialized(model, m2m_field_name)
  
      _handle_inherited_models(model, model_json)
  
      return model_json
  
  
  def _json_serializer(obj):
      """
      Handle the objects that the `orjson` deserializer can't handle automatically.
  
      The types handled by `orjson` by default: dataclass, datetime, enum, float, int, numpy, str, uuid.
      The types handled in this class: Django Model, Django QuerySet, Decimal, or any object with `to_json` method.
  
      TODO: Investigate other ways to serialize objects automatically.
      e.g. Using DRF serializer: https://www.django-rest-framework.org/api-guide/serializers/#serializing-objects
      """
      from django_unicorn.components import UnicornView
  
      try:
          if isinstance(obj, UnicornView):
              return {
                  "name": obj.component_name,
                  "id": obj.component_id,
                  "key": obj.component_key,
              }
          elif isinstance(obj, Model):
              return _get_model_dict(obj)
          elif isinstance(obj, QuerySet):
              queryset_json = []
  
              for model in obj:
                  if obj.query.values_select and isinstance(model, dict):
                      # If the queryset was created with values it's already a dictionary
                      model_json = model
                  else:
                      model_json = _get_model_dict(model)
  
                  queryset_json.append(model_json)
  
              return queryset_json
          elif PydanticBaseModel and isinstance(obj, PydanticBaseModel): #type: ignore
              return obj.dict()
          elif isinstance(obj, Decimal):
              return str(obj)
          elif isinstance(obj, MappingProxyType):
              # Return a regular dict for `mappingproxy`
              return obj.copy()
          elif hasattr(obj, "to_json"):
              return obj.to_json()
      except Exception as e:
          # Log this because the `TypeError` and resulting stacktrace lacks context
          logger.exception(e)
  
      raise TypeError
  
  
  def _fix_floats(current: Dict, data: Optional[Dict] = None, paths: Optional[List] = None) -> None:
      """
      Recursively change any Python floats to a string so that JavaScript
      won't convert the float to an integer when deserializing.
  
      Params:
          current: Dictionary in which to check for and fix floats.
      """
  
      if data is None:
          data = current
  
      if paths is None:
          paths = []
  
      if isinstance(current, dict):
          for key, val in current.items():
              paths.append(key)
              _fix_floats(val, data, paths=paths)
              paths.pop()
      elif isinstance(current, list):
          for idx, item in enumerate(current):
              paths.append(idx)
              _fix_floats(item, data, paths=paths)
              paths.pop()
      elif isinstance(current, float):
          _piece = data
  
          for idx, path in enumerate(paths):
              if idx == len(paths) - 1:
                  # `path` can be a dictionary key or list index,
                  # but in either instance it is set the same way
                  _piece[path] = str(current)
              else:
                  _piece = _piece[path]
  
  
  def _sort_dict(data: Dict) -> Dict:
      """
      Recursively sort the dictionary keys so that JavaScript won't change the order
      and change the generated checksum.
  
      Params:
          data: Dictionary to sort.
      """
  
      if not isinstance(data, dict):
          return data
  
      items = [
          [k, v]
          for k, v in sorted(
              data.items(),
              key=lambda item: item[0] if not is_int(item[0]) else int(item[0]),
          )
      ]
  
      for item in items:
          if isinstance(item[1], dict):
              item[1] = _sort_dict(item[1])
  
      return dict(items)
  
  
  def _exclude_field_attributes(dict_data: Dict[Any, Any], exclude_field_attributes: Optional[Tuple[str]] = None) -> None:
      """
      Remove the field attribute from `dict_data`. Handles nested attributes with a dot.
  
      Example:
      _exclude_field_attributes({"1": {"2": {"3": "4"}}}, ("1.2.3",)) == {"1": {"2": {}}}
      """
  
      if exclude_field_attributes:
          for field in exclude_field_attributes:
              field_splits = field.split(".")
              nested_attribute_split_count = 2
  
              if len(field_splits) > nested_attribute_split_count:
                  next_attribute_index = field.index(".") + 1
                  remaining_field_attributes = field[next_attribute_index:]
                  remaining_dict_data = dict_data[field_splits[0]]
  
                  return _exclude_field_attributes(remaining_dict_data, (remaining_field_attributes,))
              elif len(field_splits) == nested_attribute_split_count:
                  (field_name, field_attr) = field_splits
  
                  if field_name not in dict_data:
                      raise InvalidFieldNameError(field_name=field_name, data=dict_data)
  
                  if dict_data[field_name] is not None:
                      if field_attr not in dict_data[field_name]:
                          raise InvalidFieldAttributeError(field_name=field_name, field_attr=field_attr, data=dict_data)
  
                      del dict_data[field_name][field_attr]
  
  
  @lru_cache(maxsize=128, typed=True)
  def _dumps(
      serialized_data: bytes,
      *,
      fix_floats: bool = True,
      exclude_field_attributes: Optional[Tuple[str]] = None,
      sort_dict: bool = True,
  ) -> Dict:
      """
      Dump serialized data with custom massaging.
  
      Features:
         - fix floats
         - remove specific keys as needed
         - sort dictionary
      """
  
      data = orjson.loads(serialized_data)
  
      if fix_floats:
          _fix_floats(data)
  
      if exclude_field_attributes:
          # Excluding field attributes needs to de-serialize and then serialize again to
          # handle complex objects
          _exclude_field_attributes(data, exclude_field_attributes)
  
      if sort_dict:
          # Sort dictionary manually because stringified integers don't get sorted
          # correctly with `orjson.OPT_SORT_KEYS` and JavaScript will sort the keys
          # as if they are integers
          data = _sort_dict(data)
  
      return data
  
  
  def dumps(
      data: Dict,
      *,
      fix_floats: bool = True,
      exclude_field_attributes: Optional[Tuple[str, ...]] = None,
      sort_dict: bool = True,
  ) -> str:
      """
      Converts the passed-in dictionary to a string representation.
  
      Handles the following objects: dataclass, datetime, enum, float, int, numpy, str, uuid,
      Django Model, Django QuerySet, Pydantic models (`PydanticBaseModel`), any object with `to_json` method.
  
      Args:
          param fix_floats: Whether any floats should be converted to strings. Defaults to `True`,
              but will be faster without it.
          param exclude_field_attributes: Tuple of strings with field attributes to remove, i.e. "1.2"
              to remove the key `2` from `{"1": {"2": "3"}}`
          param sort_dict: Whether the `dict` should be sorted. Defaults to `True`, but
              will be faster without it.
  
      Returns a `str` instead of `bytes` (which deviates from `orjson.dumps`), but seems more useful.
      """
  
      if exclude_field_attributes is not None and not is_non_string_sequence(exclude_field_attributes):
          raise AssertionError("exclude_field_attributes type needs to be a sequence")
  
      # Call `dumps` to make sure that complex objects are serialized correctly
      serialized_data = orjson.dumps(data, default=_json_serializer)
  
      data = _dumps(
          serialized_data,
          fix_floats=fix_floats,
          exclude_field_attributes=exclude_field_attributes,
          sort_dict=sort_dict,
      )
  
      serialized_data = orjson.dumps(data)
  
      return serialized_data.decode("utf-8")
  
  
  def loads(string: str) -> dict:
      """
      Converts a string representation to dictionary.
      """
  
      try:
          return orjson.loads(string)
      except orjson.JSONDecodeError as e:
          raise JSONDecodeError from e
  
  
  def model_value(model: Model, *fields: str):
      """
      Serializes a model into a dictionary with the fields as specified in the `fields` argument.
      """
  
      model_data = {}
      model_dict = _get_model_dict(model)
  
      if not fields:
          return model_dict
  
      for field in fields:
          if field in model_dict:
              model_data[field] = model_dict[field]
  
      return model_data
  