Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 31 additions & 53 deletions diffsync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@
limitations under the License.
"""
from inspect import isclass
from typing import Callable, ClassVar, Dict, List, Mapping, Optional, Text, Tuple, Type, Union
from typing import Callable, ClassVar, Dict, List, Mapping, Optional, Text, Tuple, Type, Union, get_type_hints, get_args

from pydantic import BaseModel, PrivateAttr
import structlog # type: ignore

from diffsync.diff import Diff
from diffsync.enum import DiffSyncModelFlags, DiffSyncFlags, DiffSyncStatus
from diffsync.exceptions import DiffClassMismatch, ObjectAlreadyExists, ObjectStoreWrongType, ObjectNotFound
from diffsync.enum import DiffSyncModelFlags, DiffSyncFlags, DiffSyncStatus, DiffSyncFieldType
from diffsync.exceptions import (
DiffClassMismatch,
ObjectAlreadyExists,
ObjectStoreWrongType,
ObjectNotFound,
)
from diffsync.helpers import DiffSyncDiffer, DiffSyncSyncer
from diffsync.store import BaseStore
from diffsync.store.local import LocalStore
Expand All @@ -37,9 +42,6 @@ class DiffSyncModel(BaseModel):
as model attributes and we want to avoid any ambiguity or collisions.

This class has several underscore-prefixed class variables that subclasses should set as desired; see below.

NOTE: The groupings _identifiers, _attributes, and _children are mutually exclusive; any given field name can
be included in **at most** one of these three tuples.
"""

_modelname: ClassVar[str] = "diffsyncmodel"
Expand All @@ -48,38 +50,13 @@ class DiffSyncModel(BaseModel):
Lowercase by convention; typically corresponds to the class name, but that is not enforced.
"""

_identifiers: ClassVar[Tuple[str, ...]] = ()
"""List of model fields which together uniquely identify an instance of this model.

This identifier MUST be globally unique among all instances of this class.
"""

_shortname: ClassVar[Tuple[str, ...]] = ()
"""Optional: list of model fields that together form a shorter identifier of an instance.

This MUST be locally unique (e.g., interface shortnames MUST be unique among all interfaces on a given device),
but does not need to be guaranteed to be globally unique among all instances.
"""

_attributes: ClassVar[Tuple[str, ...]] = ()
"""Optional: list of additional model fields (beyond those in `_identifiers`) that are relevant to this model.

Only the fields in `_attributes` (as well as any `_children` fields, see below) will be considered
for the purposes of Diff calculation.
A model may define additional fields (not included in `_attributes`) for its internal use;
a common example would be a locally significant database primary key or id value.

Note: inclusion in `_attributes` is mutually exclusive from inclusion in `_identifiers`; a field cannot be in both!
"""

_children: ClassVar[Mapping[str, str]] = {}
"""Optional: dict of `{_modelname: field_name}` entries describing how to store "child" models in this model.

When calculating a Diff or performing a sync, DiffSync will automatically recurse into these child models.

Note: inclusion in `_children` is mutually exclusive from inclusion in `_identifiers` or `_attributes`.
"""

model_flags: DiffSyncModelFlags = DiffSyncModelFlags.NONE
"""Optional: any non-default behavioral flags for this DiffSyncModel.

Expand All @@ -106,31 +83,32 @@ def __init_subclass__(cls):

Called automatically on subclass declaration.
"""
variables = cls.__fields__.keys()
type_hints = get_type_hints(cls, include_extras=True)
field_name_type_mapping = {field_type.value: [] for field_type in DiffSyncFieldType}
children = {}
for key, value in type_hints.items():
try:
field_type = value.__metadata__[0]
except AttributeError:
# In this case we aren't dealing with an actual payload field
continue

if field_type in [DiffSyncFieldType.IDENTIFIER, DiffSyncFieldType.ATTRIBUTE]:
field_name_type_mapping[field_type.value].append(key)
elif field_type == DiffSyncFieldType.CHILDREN:
actual_type, _, child_modelname = get_args(value)
children[child_modelname] = key

cls._identifiers = field_name_type_mapping[DiffSyncFieldType.IDENTIFIER.value]
cls._attributes = field_name_type_mapping[DiffSyncFieldType.ATTRIBUTE.value]
cls._children = children

all_field_names = cls._identifiers + cls._attributes + list(cls._children.keys())

# Make sure that any field referenced by name actually exists on the model
for attr in cls._identifiers:
if attr not in variables and not hasattr(cls, attr):
raise AttributeError(f"_identifiers {cls._identifiers} references missing or un-annotated attr {attr}")
for attr in cls._shortname:
if attr not in variables:
if attr not in all_field_names:
raise AttributeError(f"_shortname {cls._shortname} references missing or un-annotated attr {attr}")
for attr in cls._attributes:
if attr not in variables:
raise AttributeError(f"_attributes {cls._attributes} references missing or un-annotated attr {attr}")
for attr in cls._children.values():
if attr not in variables:
raise AttributeError(f"_children {cls._children} references missing or un-annotated attr {attr}")

# Any given field can only be in one of (_identifiers, _attributes, _children)
id_attr_overlap = set(cls._identifiers).intersection(cls._attributes)
if id_attr_overlap:
raise AttributeError(f"Fields {id_attr_overlap} are included in both _identifiers and _attributes.")
id_child_overlap = set(cls._identifiers).intersection(cls._children.values())
if id_child_overlap:
raise AttributeError(f"Fields {id_child_overlap} are included in both _identifiers and _children.")
attr_child_overlap = set(cls._attributes).intersection(cls._children.values())
if attr_child_overlap:
raise AttributeError(f"Fields {attr_child_overlap} are included in both _attributes and _children.")

def __repr__(self):
return f'{self.get_type()} "{self.get_unique_id()}"'
Expand Down
7 changes: 7 additions & 0 deletions diffsync/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,10 @@ class DiffSyncActions: # pylint: disable=too-few-public-methods
DELETE = "delete"
SKIP = "skip"
NO_CHANGE = None


class DiffSyncFieldType(enum.Enum):
"""Enum that details which type of field a mode field is."""
ATTRIBUTE = "attribute"
IDENTIFIER = "identifier"
CHILDREN = "children"
30 changes: 11 additions & 19 deletions examples/01-multiple-data-sources/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,38 @@
See the License for the specific language governing permissions and
limitations under the License.
"""
from typing import List, Optional
from diffsync import DiffSyncModel
from typing import List, Optional, Annotated
from diffsync import DiffSyncModel, DiffSyncFieldType


class Site(DiffSyncModel):
"""Example model of a geographic Site."""

_modelname = "site"
_identifiers = ("name",)
_shortname = ()
_attributes = ()
_children = {"device": "devices"}

name: str
devices: List = []
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
devices: Annotated[List, DiffSyncFieldType.CHILDREN, "device"] = []


class Device(DiffSyncModel):
"""Example model of a network Device."""

_modelname = "device"
_identifiers = ("name",)
_attributes = ()
_children = {"interface": "interfaces"}

name: str
site_name: Optional[str] # note that this attribute is NOT included in _attributes
role: Optional[str] # note that this attribute is NOT included in _attributes
interfaces: List = []
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
site_name: Optional[str] # note that this attribute is NOT annotated
role: Optional[str] # note that this attribute is NOT annotated
interfaces: Annotated[List, DiffSyncFieldType.CHILDREN, "interface"] = []


class Interface(DiffSyncModel):
"""Example model of a network Interface."""

_modelname = "interface"
_identifiers = ("device_name", "name")
_shortname = ("name",)
_attributes = ("description",)

name: str
device_name: str
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
device_name: Annotated[str, DiffSyncFieldType.IDENTIFIER]

description: Optional[str]
description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
6 changes: 3 additions & 3 deletions examples/02-callback-function/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@
limitations under the License.
"""
import random
from typing import Annotated

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


class Number(DiffSyncModel):
"""Simple model that consists only of a number."""

_modelname = "number"
_identifiers = ("number",)

number: int
number: Annotated[int, DiffSyncFieldType.IDENTIFIER]


class DiffSync1(DiffSync):
Expand Down
27 changes: 11 additions & 16 deletions examples/03-remote-system/models.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
"""Main DiffSync models for example3."""
from typing import List, Optional
from diffsync import DiffSyncModel
from typing import List, Optional, Annotated
from diffsync import DiffSyncModel, DiffSyncFieldType


class Region(DiffSyncModel):
"""Example model of a geographic region."""

_modelname = "region"
_identifiers = ("slug",)
_attributes = ("name",)

# By listing country as a child to Region
# DiffSync will be able to recursively compare all regions including all their children
_children = {"country": "countries"}
slug: Annotated[str, DiffSyncFieldType.IDENTIFIER]
name: Annotated[str, DiffSyncFieldType.ATTRIBUTE]

slug: str
name: str
countries: List[str] = []
# By annotating country as a child to Region
# DiffSync will be able to recursively compare all regions including all their children
countries: Annotated[List[str], DiffSyncFieldType.CHILDREN, "country"] = []


class Country(DiffSyncModel):
Expand All @@ -26,10 +23,8 @@ class Country(DiffSyncModel):
"""

_modelname = "country"
_identifiers = ("slug",)
_attributes = ("name", "region", "population")

slug: str
name: str
region: str
population: Optional[int]
slug: Annotated[str, DiffSyncFieldType.IDENTIFIER]
name: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
region: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
population: Annotated[Optional[int], DiffSyncFieldType.ATTRIBUTE]
29 changes: 11 additions & 18 deletions examples/04-get-update-instantiate/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,38 @@
See the License for the specific language governing permissions and
limitations under the License.
"""
from typing import List, Optional
from diffsync import DiffSyncModel
from typing import List, Optional, Annotated
from diffsync import DiffSyncModel, DiffSyncFieldType


class Site(DiffSyncModel):
"""Example model of a geographic Site."""

_modelname = "site"
_identifiers = ("name",)
_shortname = ()
_attributes = ()

name: str
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]


class Device(DiffSyncModel):
"""Example model of a network Device."""

_modelname = "device"
_identifiers = ("name",)
_attributes = ()
_children = {"interface": "interfaces", "site": "sites"}

name: str
site_name: Optional[str] # note that this attribute is NOT included in _attributes
role: Optional[str] # note that this attribute is NOT included in _attributes
interfaces: List = []
sites: List = []
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
site_name: Optional[str] # note that this attribute is NOT annotated
role: Optional[str] # note that this attribute is NOT annotated
interfaces: Annotated[List[str], DiffSyncFieldType.CHILDREN, "interface"] = []
sites: Annotated[List[str], DiffSyncFieldType.CHILDREN, "site"] = []


class Interface(DiffSyncModel):
"""Example model of a network Interface."""

_modelname = "interface"
_identifiers = ("device_name", "name")
_shortname = ("name",)
_attributes = ("description",)

name: str
device_name: str
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
device_name: Annotated[str, DiffSyncFieldType.IDENTIFIER]

description: Optional[str]
description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
49 changes: 16 additions & 33 deletions examples/05-nautobot-peeringdb/models.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
"""DiffSyncModel subclasses for Nautobot-PeeringDB data sync."""
from typing import Optional, Union, List
from typing import Optional, Union, List, Annotated
from uuid import UUID

from diffsync import DiffSyncModel
from diffsync import DiffSyncModel, DiffSyncFieldType


class RegionModel(DiffSyncModel):
"""Shared data model representing a Region."""

# Metadata about this model
_modelname = "region"
_identifiers = ("name",)
_attributes = (
"slug",
"description",
"parent_name",
)
_children = {"site": "sites"}

# Data type declarations for all identifiers and attributes
name: str
slug: str
description: Optional[str]
parent_name: Optional[str] # may be None
sites: List = []
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
slug: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
parent_name: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
sites: Annotated[List, DiffSyncFieldType.CHILDREN, "sites"] = []

# Not in _attributes or _identifiers, hence not included in diff calculations
# Not annotated, hence not included in diff calculations
pk: Optional[UUID]


Expand All @@ -34,25 +27,15 @@ class SiteModel(DiffSyncModel):

# Metadata about this model
_modelname = "site"
_identifiers = ("name",)
# To keep this example simple, we don't include **all** attributes of a Site here. But you could!
_attributes = (
"slug",
"status_slug",
"region_name",
"description",
"latitude",
"longitude",
)

# Data type declarations for all identifiers and attributes
name: str
slug: str
status_slug: str
region_name: Optional[str] # may be None
description: Optional[str]
latitude: Optional[float]
longitude: Optional[float]
# To keep this example simple, we don't include **all** attributes of a Site here. But you could!
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
slug: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
status_slug: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
region_name: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
latitude: Annotated[Optional[float], DiffSyncFieldType.ATTRIBUTE]
longitude: Annotated[Optional[float], DiffSyncFieldType.ATTRIBUTE]

# Not in _attributes or _identifiers, hence not included in diff calculations
pk: Optional[Union[UUID, int]]
Loading