def validate(self, raise_error: bool = True) -> (bool, dict):
        """
        Call all the validation methods of the fields defined on the Model subclass and return (True, {}) if they
        are all valid. Otherwise, raises an error (see raise_error) or returns (False, description_of_errors<dict>)

        :param raise_error: whether or not validation errors of Model fields will raise an exception
        :return (bool, dict): whether or not there was an error and a dict describing the errors

        Example:

        .. code-block:: python

            class User(BaseModel):
                username = Field(required=True)

            user = User()
            user.validate(raise_error=False) # returns (False, description_of_errors<dict>) because username is required
        """
        error_map = {}
        for field_name in self._field_names.union(self._model_field_names):
            # The `Field` version of the field is now private
            field_obj = getattr(self, f'_{field_name}')
            field_value = getattr(self, field_name)
            is_valid, validation_error = field_obj.validate(
                field_value, raise_error)
            if not is_valid:
                error_map[field_name] = validation_error
        # whether all fields passed validation, and if not, why not
        model_invalid = bool(error_map)
        if raise_error and model_invalid:
            raise ValidationError(error_map)
        return not model_invalid, error_map
    def __init__(self,
                 required: bool = False,
                 default=None,
                 validation: Optional[Callable[..., Tuple[bool,
                                                          dict]]] = None):
        """
        :param required: Whether or not the field is required
        :param default: What the field defaults to if no value is set
        :param validation: return true if the value is valid, otherwise return false
        """
        self.required = required

        # make sure default is always a callable
        if callable(default):
            self._default = default
        else:
            self._default = lambda *args, **kwargs: default

        # validation should either be None or a callable
        if validation is not None:
            if callable(validation):
                self.validation = validation
            else:
                raise ValidationError(
                    f'validation must be a callable, cannot be {validation}')
        else:
            # always passes if validation is None
            self.validation = lambda x: (True, {})
    def validate(self,
                 model_instance,
                 raise_error: bool = True) -> (bool, dict):
        """
        Check to see that the passed model instance is a subclass of `model` parameter passed into ModelField.__init__,
        then validate the fields of that model as usual. Parallels the validate method of the Field class

        :param model_instance: instance of model to validate (parallels `value` in validate method of Field class)
        :param raise_error: whether or not an exception is raised on validation error
        :return (bool, dict): whether or not there was an error and a dict describing the errors
        """
        is_valid_model, is_valid_field, model_errors, field_errors = True, {}, True, {}
        # check that the passed model_instance is a subclass of the prescribed model from __init__
        if isinstance(model_instance, self.field_model):
            is_valid_model, model_errors = model_instance.validate(raise_error)
        # the model instance is not None, this will emit an error. Otherwise, we check the
        # field validation logic to determine whether this is a required field.
        elif model_instance is not None:
            message = f'{self.name} field failed validation. {model_instance} is {model_instance.__class__.__name__}, must be {self.field_model_name}'
            if raise_error:
                raise ValidationError(message)
            else:
                return False, {'error': message}
        if is_valid_model:
            is_valid_field, field_errors = super(ModelField, self).validate(
                model_instance, raise_error)
        if is_valid_field and is_valid_model:
            return True, {}
        return False, {**model_errors, **field_errors}
 def validate(self, value, raise_error: bool = True):
     if value is not None and not isinstance(value, str):
         message = f'{self.model_name} value of {self.name} failed validation; {self.name} must be str instance, not {value.__class__}.'
         if raise_error:
             raise ValidationError(message)
         else:
             return False, message
     return super(IDField, self).validate(value, raise_error=raise_error)
    def validate(self, value, raise_error: bool = True) -> (bool, dict):
        """
        Check that the passed value is not None if the Field instance is required, and calls the `validation`
        function passed via Field.__init___. Raises an error if raise_error is `True` (default).

        :param value: value to validate against the Field specifications
        :param raise_error: whether or not to raise a ValidationError when an error is encountered.
        :return (bool, dict): whether or not there was an error and a dict describing the errors

        Example:

        .. code-block:: python

            def validate_date_created(date_created_value):
                is_valid, error = isint(date_created_value), {}
                if not is_valid:
                    error = {'error': 'value of date_created must be an integer number.'}
                return is_valid, error

            date_created = Field(required=True, default=time.time, validation=validate_date_created)

            date_created.validate(time.time()) # returns (True, {})
        """
        if self.required and not value:
            message = f'{self.model_name} field {self.name} is required but received no default and no value.'
            if raise_error:
                raise ValidationError(message)
            else:
                return False, {'error': message}

        validation_passed, errors = self.validation(value)

        if raise_error:
            if not validation_passed:
                raise ValidationError(
                    f'{self.model_name} value of {self.name} failed validation.'
                )

        # whether or not the validation passed and useful error information
        return validation_passed, {}
    def delete(self) -> dict:
        """
        Deletes the firestore record corresponding to the id defined on the instance

        :return: dictionary describing the result of the delete operation from firestore
        """

        id_as_str = None if self.id is None else str(
            self.id)  # just to be sure that id is a str
        if not id_as_str:
            raise ValidationError(
                f'Cannot call delete for {self._collection} document; no id specified.'
            )
        document_ref = self.collection.document(str(id_as_str))
        return {'result': document_ref.delete()}
    def retrieve(self, overwrite_local: bool = False) -> dict:
        """
        Retrieve the record corresponding to the id defined on the instance. If overwrite_local is True, the instance
        field values are overwritten with the firestore record values.

        :param overwrite_local: whether or not to overwrite instance field values with firestore field values
        :return:
        """
        id_as_str = None if self.id is None else str(
            self.id)  # just to be sure that id is a str
        if not id_as_str:
            raise ValidationError(
                f'Cannot retrieve document for {self._collection}; no id specified.'
            )
        document_ref = self.collection.document(id_as_str)
        document_dict = self.collection.document(id_as_str).get().to_dict()
        if document_dict is None:
            return {}
        full_subcollection = {}
        for subcollection in document_ref.collections():
            subcollection_name = subcollection._path[-1]
            subcollection_document_list = []
            full_subcollection[
                subcollection_name] = subcollection_document_list
            for subcollection_document_ref in subcollection.list_documents():
                subcollection_document_list.append(
                    subcollection_document_ref.get().to_dict())
            if len(subcollection_document_list) == 1:
                full_subcollection[
                    subcollection_name] = subcollection_document_list[0]

        document_dict.update(full_subcollection)

        if overwrite_local:
            # use from_dict on self, and then use from_dict on each of the related model fields
            self.from_dict(document_dict)
            for related_model_field_name in self._get_model_fields():
                related_model_name = related_model_field_name[
                    1:]  # remove leading underscore
                related_model = getattr(self, related_model_name)
                # if there is not an instance of the related model, we want to create one!
                if related_model is None:
                    related_model = getattr(
                        self, related_model_field_name).field_model()
                related_model.from_dict(
                    document_dict.get(related_model_name, {}))
                setattr(self, related_model_name, related_model)
        return document_dict