Source code for django_typer.parsers.model

import typing as t
from datetime import date, datetime, time
from enum import Enum
from uuid import UUID

from click import Context, Parameter, ParamType
from django.core.management import CommandError
from django.db import models

from django_typer.completers.model import ModelObjectCompleter


[docs] class ReturnType(Enum): MODEL_INSTANCE = 0 """Return the model instance with the matching field value.""" FIELD_VALUE = 1 """Return the value of the field that was matched.""" QUERY_SET = 2 """Return a queryset of model instances that match the field value."""
[docs] class ModelObjectParser(ParamType): """ A parser that will turn strings into model object instances based on the configured lookup field and model class. .. code-block:: python from django_typer.parsers.model import ModelObjectParser class Command(TyperCommand): def handle( self, django_apps: Annotated[ t.List[MyModel], typer.Argument( parser=ModelObjectParser(MyModel, lookup_field="name"), help=_("One or more application labels."), ), ], ): .. note:: Typer_ does not respect the shell_complete functions on ParamTypes passed as parsers. To add shell_completion see :class:`~django_typer.completers.ModelObjectCompleter` or the :func:`~django_typer.utils.model_parser_completer` convenience function. :param model_cls: The model class to use for lookup. :param lookup_field: The field to use for lookup. Defaults to 'pk'. :param on_error: A callable that will be called if the lookup fails. The callable should accept three arguments: the model class, the value that failed to lookup, and the exception that was raised. If not provided, a CommandError will be raised. :param return_type: The model object parser can return types other than the model instance (default) - use the ReturnType enumeration to return other types from the parser including QuerySets or the primitive values of the model fields. """ error_handler = t.Callable[[t.Type[models.Model], str, Exception], None] model_cls: t.Type[models.Model] lookup_field: str case_insensitive: bool = False on_error: t.Optional[error_handler] = None return_type: ReturnType = ReturnType.MODEL_INSTANCE _lookup: str = "" _field: models.Field _completer: ModelObjectCompleter __name__: str = "MODEL" # typer internals expect this def _get_metavar(self) -> str: if isinstance(self._field, models.IntegerField): return "INT" elif isinstance(self._field, models.EmailField): return "EMAIL" elif isinstance(self._field, models.URLField): return "URL" elif isinstance(self._field, models.GenericIPAddressField): return "[IPv4|IPv6]" elif isinstance(self._field, models.UUIDField): return "UUID" elif isinstance(self._field, (models.FloatField, models.DecimalField)): return "FLOAT" elif isinstance(self._field, (models.FileField, models.FilePathField)): return "PATH" elif isinstance(self._field, models.DateTimeField): return "ISO 8601" elif isinstance(self._field, models.DateField): return "YYYY-MM-DD" elif isinstance(self._field, models.TimeField): return "HH:MM:SS.sss" elif isinstance(self._field, models.DurationField): return "ISO 8601" return "TXT"
[docs] def __init__( self, model_cls: t.Type[models.Model], lookup_field: t.Optional[str] = None, case_insensitive: bool = case_insensitive, on_error: t.Optional[error_handler] = on_error, return_type: ReturnType = return_type, ): from django.contrib.contenttypes.fields import GenericForeignKey self.model_cls = model_cls self.lookup_field = str( lookup_field or getattr(self.model_cls._meta.pk, "name", "id") ) self.on_error = on_error self.return_type = return_type self.case_insensitive = case_insensitive field = self.model_cls._meta.get_field(self.lookup_field) assert not isinstance(field, (models.ForeignObjectRel, GenericForeignKey)), ( "{cls} is not a supported lookup field." ).format(cls=self._field.__class__.__name__) self._field = field if self.case_insensitive and "iexact" in self._field.get_lookups(): self._lookup = "__iexact" self.__name__ = self._get_metavar()
[docs] def convert( self, value: t.Any, param: t.Optional[Parameter], ctx: t.Optional[Context] ): """ Invoke the parsing action on the given string. If the value is already a model instance of the expected type the value will be returned. Otherwise the value will be treated as a value to query against the lookup_field. If no model object is found the error handler is invoked if one was provided. :param value: The value to parse. :param param: The parameter that the value is associated with. :param ctx: The context of the command. :raises CommandError: If the lookup fails and no error handler is provided. """ original = value try: if not isinstance(value, str): return value elif isinstance(self._field, models.UUIDField): uuid = "" for char in value: if char.isalnum(): uuid += char value = UUID(uuid) elif isinstance(self._field, models.DateTimeField): value = datetime.fromisoformat(value) elif isinstance(self._field, models.DateField): value = date.fromisoformat(value) elif isinstance(self._field, models.TimeField): value = time.fromisoformat(value) elif isinstance(self._field, models.DurationField): from django_typer.utils import parse_iso_duration parsed, ambiguous = parse_iso_duration(value) if ambiguous: raise ValueError(f"Invalid duration: {value}") value = parsed if self.return_type is ReturnType.QUERY_SET: return self.model_cls.objects.filter( **{f"{self.lookup_field}{self._lookup}": value} ) elif self.return_type is ReturnType.FIELD_VALUE: return value return self.model_cls.objects.get( **{f"{self.lookup_field}{self._lookup}": value} ) except ValueError as err: if self.on_error: return self.on_error(self.model_cls, original, err) raise CommandError( f"{original} is not a valid {self._field.__class__.__name__}" ) from err except self.model_cls.DoesNotExist as err: if self.on_error: return self.on_error(self.model_cls, original, err) raise CommandError( f"{self.model_cls.__name__}.{self.lookup_field}='{original}' does not " "exist!" ) from err