networktocode / diffsync

A utility library for comparing and synchronizing different datasets.
https://diffsync.readthedocs.io/
Other
152 stars 27 forks source link

Return sync result from create/update/delete methods instead of returning the #254

Open jamesharr opened 11 months ago

jamesharr commented 11 months ago

Environment

Proposed Functionality

In 1.9.0, the results of a sync are stored as a field in DiffSyncModel, and for most users this status is automatically set when calling the super() of the create()/update()/delete() methods.

There are a few things that are awkward about this API:

At a conceptual level, it's also a bit weird to have the status stored on an object. In particular, how do you report the result of a delete()? If that object is no longer in the destination backend, then how can I read the result of that after a sync process? I think this is ultimately because the result is not a property of an object, it's the result of a process.

This Feature Request proposes:

Misc notes

Use Case

# DiffSync code
class SyncResult(pydantic.BaseModel):
    status: DiffSyncStatus
    message: str
    skip_children: bool = False  # Set to True to skip updating children

# User code example 1 - providing custom values
class MyModelA(DiffSyncModel):
    @classmethod
    def create(cls, diffsync:DiffSync, ids:Dict[str, any], attrs:Dict[str, any]) -> Tuple[Self, SyncResult]:
        return cls(**ids, **atts), SyncResult(DiffSyncStatus.SUCCESS, "Sync successful")

    def update(self, attrs:Dict[str,any]) -> SyncResult:
        ... # perform update
        super().update(attrs) # Returns a successful SyncResult, but we can craft our own
        return SyncResult(DiffSyncStatus.SUCCESS, "with immense difficulty")

    def delete(self) -> SyncResult:
        return SyncResult(DiffSyncStatus.ERROR, "Delete is not implemented")

# User code example 2 - simple user experience
class MyModelB(DiffSyncModel):
    @classmethod
    def create(cls, diffsync:DiffSync, ids:Dict[str, any], attrs:Dict[str, any]) -> Tuple[Self, SyncResult]:
        return cls(**ids, **atts)
        # I'm struggling to figure out an elegant way to provide the user experience that exists today,
        # which is to make it easy to report success. This isn't ideal, but one thought is:
        # - If a single-value is returned, the returned value is the object created and success is assumed
        # - If a 2-tuple is returned: interpret the first value as the object created, and the second value is the SyncResult

    def update(self, attrs:Dict[str,any]) -> SyncResult:
        ...
        return super().update(**attrs)  # DiffSyncModel.update() returns a success object by default

    def delete(self) -> SyncResult:
        return super().delete()  # DiffSyncModel.delete() returns a success object by default.

# User code example 3 - augmented result to add additional fields
class CustomSyncResult(diffsync.SyncResult):
    nso_dry_run_result: str

class MyModelC(DiffSyncModel):
    @classmethod
    def create(cls, diffsync:DiffSync, ids:Dict[str, any], attrs:Dict[str, any]) -> Tuple[Self, SyncResult]:
        dry_run_text = nso.dry_run(...)
        status = CustomSyncResult(DiffSyncStatus.SUCCESS, "Sync successful", nso_dry_run_result=dry_run_text)
        obj = cls(**ids, **atts)
        return obj, status

    def update(self, attrs:Dict[str,any]) -> SyncResult:
        dry_run_text = nso.dry_run(...)
        super().update(attrs)
        return CustomSyncResult(DiffSyncStatus.SUCCESS, "Sync successful", nso_dry_run_result=dry_run_text)

    def delete(self) -> SyncResult:
        return CustomSyncResult(DiffSyncStatus.SUCCESS, "Sync successful", nso_dry_run_result=dry_run_text)

The most straight forward way for a user to read the result of a status is to embed that status object into the DiffElement.

class DiffElement:
    status: SyncStatus  # possibly | None
Kircheneer commented 9 months ago

I support the idea of returning a sync result rather then the object itself, I think this would make error handling more straight-forward. @glennmatthews/@Dav-C - any opinion here?

glennmatthews commented 9 months ago

I haven't thought it through in depth, but conceptually it feels reasonable to me. :)

Dav-C commented 9 months ago

Is anything lost by not returning the object? I think the current implementation loosely follows the REST paradigm where the object is returned for immediate use and the lack of a returned object is an error that should be handled. In any case, I think the return from all the methods should be consistent to avoid confusion.

Kircheneer commented 9 months ago

When calling update or delete, you are calling on the class instance itself, so you have the instance available in 100% of cases. The proposal for create returns a tuple with the class and the SyncResult, so that's covered as well