networktocode/diffsync

Create an IGNORE_CASE flag to prevent case-sensitive mismatches

Opened this issue · 1 comments

Environment

  • DiffSync version: 1.4.1

Proposed Functionality

Implement either a global or model flag (or both) called IGNORE_CASE, that will tell DiffSync to ignore case-sensitive mismatches.

Example for Global Flags:

from diffsync.enum import DiffSyncFlags
flags = DiffSyncFlags.IGNORE_CASE
diff = nautobot.diff_from(local, flags=flags)

Example for Model Flags:

from diffsync import DiffSync
from diffsync.enum import DiffSyncModelFlags
from model import MyDeviceModel

class MyAdapter(DiffSync):

    device = MyDeviceModel

    def load(self, data):
        """Load all devices into the adapter and add the flag IGNORE to all firewall devices."""
        for device in data.get("devices"):
            obj = self.device(name=device["name"])
            if "firewall" in device["name"]:
                obj.model_flags = DiffSyncModelFlags.IGNORE_CASE
            self.add(obj)

Use Case

Currently, if we are trying to sync the same object from different backends that have the same name but without the same case (i.e.: "my-device" & "My-Device"), they will be marked as different, thus deleting the first device to replace it with the new one.

Below is an example to show the current limitations of not having such flag. As you can see from the DATA_BACKEND_A and DATA_BACKEND_B variables, the values are the same, but the first is in all caps, whereas the second is all lowercase.

from diffsync.logging import enable_console_logging
from diffsync import DiffSync
from diffsync import DiffSyncModel


class Site(DiffSyncModel):
    _modelname = "site"
    _identifiers = ("name",)

    name: str

    @classmethod
    def create(cls, diffsync, ids, attrs):
        print(f"Create {cls._modelname}")
        return super().create(ids=ids, diffsync=diffsync, attrs=attrs)

    def update(self, attrs):
        print(f"Update {self._modelname}")
        return super().update(attrs)

    def delete(self):
        print(f"Delete {self._modelname}")
        super().delete()
        return self


DATA_BACKEND_A = ["SITE-A"]
DATA_BACKEND_B = ["site-A"]


class BackendA(DiffSync):
    site = Site

    top_level = ["site"]

    def load(self):
        for site_name in DATA_BACKEND_A:
            site = self.site(name=site_name)
            self.add(site)


class BackendB(DiffSync):
    site = Site

    top_level = ["site"]

    def load(self):
        for site_name in DATA_BACKEND_B:
            site = self.site(name=site_name)
            self.add(site)


def main():
    enable_console_logging(verbosity=0)

    backend_a = BackendA(name="Backend-A")
    backend_a.load()

    backend_b = BackendB(name="Backend-B")
    backend_b.load()

    backend_a.sync_to(backend_b)


if __name__ == "__main__":
    main()

Upon executing this script, the output is:

Create site
Delete site

So we are replacing an object that could potentially be the same.

The implementation of this flag could help mitigate unexpected results when the user knows he might have case-insensitive data from both backends, and remove the need to use functions such as .lower() or .casefold() each time he creates a new object.

Hi @antoinedelia, thank you for this proposal, personally I like the idea
A global flag should be "easy" to implement, for the model flag that might be more complicated but why not.

@glennmatthews what do you think ?