def tag(
        self,
        tags: Iterable[str],
        clear: bool = False,
        normalized: bool = True,
        expiration: datetime.timedelta | None = None,
    ):
        """Adds or updates tags on an object.

        Includes options for normalization, expiration, and clearing existing tags.

        Args:
            tags: A list or iterable of tag names to be added or updated.
                If a single string is provided, it will be converted to a list.
            clear: If True, clears all existing tags before adding the new ones.
                Defaults to False.
            normalized: If True, normalizes the tag names using `normalize_name`.
                Defaults to True.
            expiration: Specifies the expiration time for the tags.
                If not provided, the default expiration for each tag is used.

        Raises:
            RuntimeError: If an empty tag name is encountered after normalization.

        Returns:
            self: The updated object with the new or modified tags.
        """
        from core.schemas import tag

        if isinstance(tags, str):  # common strings are also technically iterables.
            tags = [tags]

        old_tags = [tag.name for tag in self.tags]
        actual_tags = []
        for tag_name in tags:
            new_tag_name = tag_name.strip()
            if normalized:
                new_tag_name = normalize_name(new_tag_name)
            if not new_tag_name:
                raise RuntimeError(
                    f"Cannot tag object with empty tag: '{new_tag_name}' -> '{tag_name}'"
                )
            actual_tags.append(new_tag_name)

        if clear:
            self.tags = []

        extra_tags = set()
        for tag_name in actual_tags:
            replacements, _ = tag.Tag.filter({"in__replaces": [tag_name]}, count=1)

            if replacements:
                db_tag = replacements[0]
            # Attempt to find actual tag
            else:
                db_tag = tag.Tag.find(name=tag_name)
                if not db_tag:
                    db_tag = tag.Tag(name=tag_name).save()

            action = (
                message.EventType.new
                if db_tag.name not in self.tags
                else message.EventType.update
            )

            expiration = expiration or db_tag.default_expiration
            now = datetime.datetime.now(tz=datetime.timezone.utc)
            for i, tag_instance in enumerate(self.tags):
                if tag_instance.name == db_tag.name:
                    self.tags[i].last_seen = now
                    self.tags[i].expires = now + expiration
                    self.tags[i].fresh = True
                    action = message.EventType.update
                    break
            else:
                self.tags.append(
                    YetiTagInstance(
                        name=db_tag.name,
                        last_seen=now,
                        expires=now + expiration,
                        fresh=True,
                    )
                )
                action = message.EventType.new
                db_tag.count += 1
                db_tag.save()

            producer.publish_event(
                message.TagEvent(
                    type=action,
                    tagged_object=self,
                    tag_object=db_tag,
                )
            )

            extra_tags |= set(db_tag.produces)

        extra_tags = extra_tags - set(tags) - set(tag.name for tag in self.tags)
        if extra_tags:
            self.tag(list(extra_tags), clear=False)

        removed_tags = set(old_tags) - set(actual_tags)
        if clear:
            for tag_name in removed_tags:
                removed_tag = tag.Tag.find(name=tag_name)
                removed_tag.count -= 1
                removed_tag.save()

                producer.publish_event(
                    message.TagEvent(
                        type=message.EventType.delete,
                        tagged_object=self,
                        tag_object=removed_tag,
                    )
                )

        self.save()
        return self
Adds or updates tags on an object.

        Includes options for normalization, expiration, and clearing existing tags.

        Args:
            tags: A list or iterable of tag names to be added or updated.
                If a single string is provided, it will be converted to a list.
            clear: If True, clears all existing tags before adding the new ones.
                Defaults to False.
            normalized: If True, normalizes the tag names using `normalize_name`.
                Defaults to True.
            expiration: Specifies the expiration time for the tags.
                If not provided, the default expiration for each tag is used.

        Raises:
            RuntimeError: If an empty tag name is encountered after normalization.

        Returns:
            self: The updated object with the new or modified tags.
        
core/schemas/model.py