Skip to content

Commit f17b2f5

Browse files
authored
Merge pull request #71 from networktocode/dga-update-doc
Add core engine section in docs and rename example directories
2 parents e47dd69 + 8d937cc commit f17b2f5

26 files changed

+223
-44
lines changed

docs/source/core_engine/01-flags.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
2+
# Global and Model Flags
3+
4+
These flags offer a powerful way to instruct the core engine how to handle some specific situation without changing the data. One way to think of the flags is to represent them as configuration for the core engine. Currently 2 sets of flags are supported:
5+
- **global flags**: applicable to all data.
6+
- **model flags**: applicable to a specific model or to individual instances of a model.
7+
8+
> *The flags are stored in binary format which allows storing multiple flags in a single variable. See the section below [Working with flags](#working-with-flags) to learn how to manage them.*
9+
10+
The list of supported flags is expected to grow over time as more use cases are identified. If you think some additional flags should be supported, please reach out via Github to start a discussion.
11+
12+
## Global flags
13+
14+
Global flags can be defined at runtime when calling one of these functions : `diff_to` ,`diff_from`, `sync_to` or `sync_from`
15+
16+
```python
17+
from diffsync.enum import DiffSyncFlags
18+
flags = DiffSyncFlags.SKIP_UNMATCHED_DST
19+
diff = nautobot.diff_from(local, flags=flags)
20+
```
21+
22+
### Supported Global Flags
23+
24+
| Name | Description | Binary Value |
25+
|---|---|---|
26+
| CONTINUE_ON_FAILURE | Continue synchronizing even if failures are encountered when syncing individual models. | 0b1 |
27+
| SKIP_UNMATCHED_SRC | Ignore objects that only exist in the source/"from" DiffSync when determining diffs and syncing. If this flag is set, no new objects will be created in the target/"to" DiffSync. | 0b10 |
28+
| SKIP_UNMATCHED_DST | Ignore objects that only exist in the target/"to" DiffSync when determining diffs and syncing. If this flag is set, no objects will be deleted from the target/"to" DiffSync. | 0b100 |
29+
| SKIP_UNMATCHED_BOTH | Convenience value combining both SKIP_UNMATCHED_SRC and SKIP_UNMATCHED_DST into a single flag | 0b110 |
30+
| LOG_UNCHANGED_RECORDS | If this flag is set, a log message will be generated during synchronization for each model, even unchanged ones. | 0b1000 |
31+
32+
## Model flags
33+
34+
Model flags are stored in the attribute `model_flags` of each model and are usually set when the data is being loaded into the adapter.
35+
36+
```python
37+
from diffsync import DiffSync
38+
from diffsync.enum import DiffSyncModelFlags
39+
from model import MyDeviceModel
40+
41+
class MyAdapter(DiffSync):
42+
43+
device = MyDeviceModel
44+
45+
def load(self, data):
46+
"""Load all devices into the adapter and add the flag IGNORE to all firewall devices."""
47+
for device in data.get("devices"):
48+
obj = self.device(name=device["name"])
49+
if "firewall" in device["name"]:
50+
obj.model_flags = DiffSyncModelFlags.IGNORE
51+
self.add(obj)
52+
```
53+
54+
### Supported Model Flags
55+
56+
| Name | Description | Binary Value |
57+
|---|---|---|
58+
| IGNORE | Do not render diffs containing this model; do not make any changes to this model when synchronizing. Can be used to indicate a model instance that exists but should not be changed by DiffSync. | 0b1 |
59+
| SKIP_CHILDREN_ON_DELETE | When deleting this model, do not recursively delete its children. Can be used for the case where deletion of a model results in the automatic deletion of all its children. | 0b10 |
60+
61+
## Working with flags
62+
63+
Flags are stored in binary format. In binary format, each bit of a variable represents 1 flag which allow us to have up to many flags stored in a single variable. Using binary flags provides more flexibility to add support for more flags in the future without redefining the current interfaces and the current DiffSync API.
64+
65+
### Enable a flag (Bitwise OR)
66+
67+
Enabling a flag is possible with the bitwise OR operator `|=`. It's important to use the bitwise operator OR when enabling a flags to ensure that the value of other flags remains unchanged.
68+
69+
```python
70+
>>> from diffsync.enum import DiffSyncFlags
71+
>>> flags = DiffSyncFlags.CONTINUE_ON_FAILURE
72+
>>> flags
73+
<DiffSyncFlags.CONTINUE_ON_FAILURE: 1>
74+
>>> bin(flags.value)
75+
'0b1'
76+
>>> flags |= DiffSyncFlags.SKIP_UNMATCHED_DST
77+
>>> flags
78+
<DiffSyncFlags.SKIP_UNMATCHED_DST|CONTINUE_ON_FAILURE: 5>
79+
>>> bin(flags.value)
80+
'0b101'
81+
```
82+
83+
### Checking the value of a specific flag (bitwise AND)
84+
85+
Validating if a flag is enabled is possible with the bitwise operator AND: `&`. The AND operator will return 0 if the flag is not set and the binary value of the flag if it's enabled. To convert the result of the test into a proper conditional it's possible to wrap the bitwise AND operator into a `bool` function.
86+
87+
```python
88+
>>> from diffsync.enum import DiffSyncFlags
89+
>>> flags = DiffSyncFlags.NONE
90+
>>> bool(flags & DiffSyncFlags.CONTINUE_ON_FAILURE)
91+
False
92+
>>> flags |= DiffSyncFlags.CONTINUE_ON_FAILURE
93+
>>> bool(flags & DiffSyncFlags.CONTINUE_ON_FAILURE)
94+
True
95+
```
96+
97+
### Disable a flag (bitwise NOT)
98+
99+
After a flag has been enabled, it's possible to disable it with a bitwise AND NOT operator : `&= ~`
100+
101+
```python
102+
>>> from diffsync.enum import DiffSyncFlags
103+
>>> flags = DiffSyncFlags.NONE
104+
# Setting the flags SKIP_UNMATCHED_DST and CONTINUE_ON_FAILURE
105+
>>> flags |= DiffSyncFlags.SKIP_UNMATCHED_DST | DiffSyncFlags.CONTINUE_ON_FAILURE
106+
>>> flags
107+
<DiffSyncFlags.SKIP_UNMATCHED_DST|CONTINUE_ON_FAILURE: 5>
108+
>>> bool(flags & DiffSyncFlags.SKIP_UNMATCHED_DST)
109+
True
110+
# Unsetting the flag SKIP_UNMATCHED_DST; CONTINUE_ON_FAILURE remains set
111+
>>> flags &= ~DiffSyncFlags.SKIP_UNMATCHED_DST
112+
>>> flags
113+
<DiffSyncFlags.CONTINUE_ON_FAILURE: 1>
114+
>>> bool(flags & DiffSyncFlags.SKIP_UNMATCHED_DST)
115+
False
116+
```
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
# Custom Diff Class
3+
4+
When performing a diff or a sync operation, a diff object is generated. A diff object is itself composed of DiffElement objects representing the different elements of the original datasets with their differences.
5+
6+
The diff object helps to access all the DiffElements. It's possible to provide your own Diff class in order to customize some of its capabilities the main one being the order in which the elements are processed.
7+
8+
## Using your own Diff class
9+
10+
To use your own diff class, you need to provide it at runtime when calling one of these functions : `diff_to`, `diff_from`, `sync_to` or `sync_from`.
11+
12+
```python
13+
>>> from diffsync.enum import DiffSyncFlags
14+
>>> from diff import AlphabeticalOrderDiff
15+
>>> diff = remote_adapter.diff_from(local_adapter, diff_class=AlphabeticalOrderDiff)
16+
>>> type(diff)
17+
<class 'AlphabeticalOrderDiff'>
18+
```
19+
20+
## Change the order in which the element are being processed
21+
22+
By default, all objects of the same type will be stored in a dictionary and as such the order in which they will be processed during a diff or a sync operation is not guaranteed (although in most cases, it will match the order in which they were initially loaded and added to the adapter). When the order in which a given group of object should be processed is important, it's possible to define your own ordering inside a custom Diff class.
23+
24+
When iterating over a list of objects, either at the top level or as a group of children of a given object, the core engine is looking for a function named after the type of the object `order_children_<type>` and if none is found it will rely on the default function `order_children_default`. Either function need to be present and need to return an Iterator of DiffElement.
25+
26+
In the example below, by default all devices will be sorted per type of CRUD operations (`order_children_device`) while all other objects will be sorted alphabetically (`order_children_default`)
27+
28+
```python
29+
class MixedOrderingDiff(Diff):
30+
"""Alternate diff class to list children in alphabetical order, except devices to be ordered by CRUD action."""
31+
32+
@classmethod
33+
def order_children_default(cls, children):
34+
"""Simple diff to return all children in alphabetical order."""
35+
for child_name, child in sorted(children.items()):
36+
yield children[child_name]
37+
38+
@classmethod
39+
def order_children_device(cls, children):
40+
"""Return a list of device sorted by CRUD action and alphabetically."""
41+
children_by_type = defaultdict(list)
42+
43+
# Organize the children's name by action create, update or delete
44+
for child_name, child in children.items():
45+
action = child.action or "skip"
46+
children_by_type[action].append(child_name)
47+
48+
# Create a global list, organized per action
49+
sorted_children = sorted(children_by_type["create"])
50+
sorted_children += sorted(children_by_type["update"])
51+
sorted_children += sorted(children_by_type["delete"])
52+
sorted_children += sorted(children_by_type["skip"])
53+
54+
for name in sorted_children:
55+
yield children[name]
56+
```
57+

docs/source/core_engine/index.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Core Engine
2+
===========
3+
4+
The core engine of DiffSync is meant to be transparent for most users but in some cases it's important to have the ability to change its behavior to adjust to some specific use cases. For these use cases, there are several ways to customize its behavior:
5+
6+
- Global and Model Flags
7+
- Diff class
8+
9+
.. mdinclude:: 01-flags.md
10+
.. mdinclude:: 02-customize-diff-class.md
11+

docs/source/examples/01-multiple-data-sources.rst

Lines changed: 0 additions & 7 deletions
This file was deleted.

docs/source/examples/02-callback-function.rst

Lines changed: 0 additions & 7 deletions
This file was deleted.

docs/source/examples/index.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
Examples
33
############
44

5-
.. toctree::
6-
:maxdepth: 2
5+
For each example, the complete source code is `available in Github <https://github.com/networktocode/diffsync/tree/main/examples>`_ in the `examples` directory
76

8-
01-multiple-data-sources
9-
02-callback-function
7+
.. mdinclude:: ../../../examples/01-multiple-data-sources/README.md
8+
.. mdinclude:: ../../../examples/02-callback-function/README.md
9+
.. mdinclude:: ../../../examples/03-remote-system/README.md

docs/source/getting_started/01-getting-started.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11

2-
# Getting started
32

43
To be able to properly compare different datasets, DiffSync relies on a shared data model that both systems must use.
54
Specifically, each system or dataset must provide a `DiffSync` "adapter" subclass, which in turn represents its dataset as instances of one or more `DiffSyncModel` data model classes.
65

76
When comparing two systems, DiffSync detects the intersection between the two systems (which data models they have in common, and which attributes are shared between each pair of data models) and uses this intersection to compare and/or synchronize the data.
87

9-
## Define your model with DiffSyncModel
8+
# Define your model with DiffSyncModel
109

1110
`DiffSyncModel` is based on [Pydantic](https://pydantic-docs.helpmanual.io/) and is using Python typing to define the format of each attribute.
1211
Each `DiffSyncModel` subclass supports the following class-level attributes:
@@ -37,11 +36,11 @@ class Site(DiffSyncModel):
3736
database_pk: Optional[int] # not listed in _identifiers/_attributes/_children as it's only locally significant
3837
```
3938

40-
### Relationship between models
39+
## Relationship between models
4140

4241
Currently the relationships between models are very loose by design. Instead of storing an object, it's recommended to store the unique id of an object and retrieve it from the store as needed. The `add_child()` API of `DiffSyncModel` provides this behavior as a default.
4342

44-
## Define your system adapter with DiffSync
43+
# Define your system adapter with DiffSync
4544

4645
A `DiffSync` "adapter" subclass must reference each model available at the top of the object by its modelname and must have a `top_level` attribute defined to indicate how the diff and the synchronization should be done. In the example below, `"site"` is the only top level object so the synchronization engine will only check all known `Site` instances and all children of each Site. In this case, as shown in the code above, `Device`s are children of `Site`s, so this is exactly the intended logic.
4746

@@ -58,7 +57,7 @@ class BackendA(DiffSync):
5857

5958
It's up to the implementer to populate the `DiffSync`'s internal cache with the appropriate data. In the example below we are using the `load()` method to populate the cache but it's not mandatory, it could be done differently.
6059

61-
## Store data in a `DiffSync` object
60+
# Store data in a `DiffSync` object
6261

6362
To add a site to the local cache/store, you need to pass a valid `DiffSyncModel` object to the `add()` function.
6463

@@ -77,13 +76,13 @@ class BackendA(DiffSync):
7776
site.add_child(device)
7877
```
7978

80-
## Update remote system on sync
79+
# Update remote system on sync
8180

8281
When data synchronization is performed via `sync_from()` or `sync_to()`, DiffSync automatically updates the in-memory
8382
`DiffSyncModel` objects of the receiving adapter. The implementer of this class is responsible for ensuring that any remote system or data store is updated correspondingly. There are two usual ways to do this, depending on whether it's more
8483
convenient to manage individual records (as in a database) or modify the entire data store in one pass (as in a file-based data store).
8584

86-
### Manage individual records
85+
## Manage individual records
8786

8887
To update individual records in a remote system, you need to extend your `DiffSyncModel` class(es) to define your own `create`, `update` and/or `delete` methods for each model.
8988
A `DiffSyncModel` instance stores a reference to its parent `DiffSync` adapter instance in case you need to use it to look up other model instances from the `DiffSync`'s cache.
@@ -110,7 +109,7 @@ class Device(DiffSyncModel):
110109
return self
111110
```
112111

113-
### Bulk/batch modifications
112+
## Bulk/batch modifications
114113

115114
If you prefer to update the entire remote system with the final state after performing all individual create/update/delete operations (as might be the case if your "remote system" is a single YAML or JSON file), the easiest place to implement this logic is in the `sync_complete()` callback method that is automatically invoked by DiffSync upon completion of a sync operation.
116115

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Welcome to DiffSync's documentation!
77

88
overview/index
99
getting_started/index
10+
core_engine/index
1011
examples/index
1112
api/diffsync
1213
license/index

examples/example1/README.md renamed to examples/01-multiple-data-sources/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
# Example 1
1+
# Example 1 - Multiple Data Sources
22

33
This is a simple example to show how DiffSync can be used to compare and synchronize multiple data sources.
44

55
For this example, we have a shared model for Device and Interface defined in `models.py`
66
And we have 3 instances of DiffSync based on the same model but with different values (BackendA, BackendB & BackendC).
77

8+
> The source code for this example is in Github in the [examples/01-multiple-data-sources/](https://github.com/networktocode/diffsync/tree/main/examples/01-multiple-data-sources) directory.
9+
810
First create and populate all 3 objects:
911

1012
```python
File renamed without changes.

examples/example2/README.md renamed to examples/02-callback-function/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,29 @@
22

33
This example shows how you can set up DiffSync to invoke a callback function to update its status as a sync proceeds. This could be used to, for example, update a status bar (such as with the [tqdm](https://github.com/tqdm/tqdm) library), although here for simplicity we'll just have the callback print directly to the console.
44

5+
> The source code for this example is in Github in the [examples/02-callback-function/](https://github.com/networktocode/diffsync/tree/main/examples/02-callback-function) directory.
6+
7+
58
```python
69
from diffsync.logging import enable_console_logging
7-
from example2 import DiffSync1, DiffSync2, print_callback
10+
from main import DiffSync1, DiffSync2, print_callback
811

912
enable_console_logging(verbosity=0) # Show WARNING and ERROR logs only
1013

1114
# Create a DiffSync1 instance and populate it with records numbered 1-100
1215
ds1 = DiffSync1()
13-
ds1.populate(count=100)
16+
ds1.load(count=100)
1417

1518
# Create a DiffSync2 instance and populate it with 100 random records in the range 1-200
1619
ds2 = DiffSync2()
17-
ds2.populate(count=100)
20+
ds2.load(count=100)
1821

1922
# Identify and attempt to resolve the differences between the two,
2023
# periodically invoking print_callback() as DiffSync progresses
2124
ds1.sync_to(ds2, callback=print_callback)
2225
```
2326

2427
You should see output similar to the following:
25-
2628
```
2729
diff: Processed 1/200 records.
2830
diff: Processed 3/200 records.

examples/example2/example2.py renamed to examples/02-callback-function/main.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class DiffSync1(DiffSync):
3737

3838
top_level = ["number"]
3939

40-
def populate(self, count):
40+
def load(self, count): # pylint: disable=arguments-differ
4141
"""Construct Numbers from 1 to count."""
4242
for i in range(count):
4343
self.add(Number(number=(i + 1)))
@@ -50,7 +50,7 @@ class DiffSync2(DiffSync):
5050

5151
top_level = ["number"]
5252

53-
def populate(self, count):
53+
def load(self, count): # pylint: disable=arguments-differ
5454
"""Construct count numbers in the range (1 - 2*count)."""
5555
prev = 0
5656
for i in range(count): # pylint: disable=unused-variable
@@ -68,13 +68,13 @@ def main():
6868
"""Create instances of DiffSync1 and DiffSync2 and sync them with a progress-reporting callback function."""
6969
enable_console_logging(verbosity=0) # Show WARNING and ERROR logs only
7070

71-
# Create a DiffSync1 instance and populate it with records numbered 1-100
71+
# Create a DiffSync1 instance and load it with records numbered 1-100
7272
ds1 = DiffSync1()
73-
ds1.populate(count=100)
73+
ds1.load(count=100)
7474

75-
# Create a DiffSync2 instance and populate it with 100 random records in the range 1-200
75+
# Create a DiffSync2 instance and load it with 100 random records in the range 1-200
7676
ds2 = DiffSync2()
77-
ds2.populate(count=100)
77+
ds2.load(count=100)
7878

7979
# Identify and attempt to resolve the differences between the two,
8080
# periodically invoking print_callback() as DiffSync progresses

examples/example3/README.md renamed to examples/03-remote-system/README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
# Example 3
2+
# Example 3 - Work with a remote system
33

44
This is a simple example to show how DiffSync can be used to compare and synchronize data with a remote system like [Nautobot](https://nautobot.readthedocs.io) via a REST API.
55

@@ -8,6 +8,11 @@ A country must be part of a region and has an attribute to capture its populatio
88

99
The comparison and synchronization of dataset is done between a local JSON file and the [public instance of Nautobot](https://demo.nautobot.com).
1010

11+
Also, this example is showing :
12+
- How to set a Global Flags to ignore object that are not matching
13+
- How to provide a custom Diff class to change the ordering of a group of object
14+
15+
> The source code for this example is in Github in the [examples/03-remote-system/](https://github.com/networktocode/diffsync/tree/main/examples/03-remote-system) directory.
1116
1217
## Install the requirements
1318

File renamed without changes.

0 commit comments

Comments
 (0)