Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
9e71008
Configure Renovate (#121)
ms-gh-admin Jul 21, 2022
434a9e9
Update renovate.json (#140)
Kircheneer Jul 25, 2022
7a9a182
Update renovate.json
Kircheneer Jul 26, 2022
86a0cc7
Update renovate.json
Kircheneer Aug 29, 2022
7ea9cca
Limit redundant CI concurrency (#149)
Kircheneer Sep 6, 2022
6fc4012
Merge pull request #141 from networktocode/lk-fix-renovate
glennmatthews Sep 14, 2022
0603123
Update dependency colorama to v0.4.5 (#150)
renovate[bot] Sep 15, 2022
e6f0bea
Update dependency yamllint to v1.28.0 (#151)
renovate[bot] Sep 27, 2022
a672325
Update dependency bandit to v1.7.4 (#152)
renovate[bot] Sep 27, 2022
82b3257
Update dependency types-toml to v0.10.8 (#155)
renovate[bot] Sep 27, 2022
7f24f77
Update dependency types-redis to v4.3.21 (#159)
renovate[bot] Sep 27, 2022
7d39a68
Update dependency pytest to v7.1.3 (#158)
renovate[bot] Sep 27, 2022
3990502
Update dependency mypy to v0.971 (#157)
renovate[bot] Sep 27, 2022
5e3f27b
Update dependency black to v22.8.0 (#156)
renovate[bot] Sep 27, 2022
8855dfe
Add diagram from Glenn's blog
itdependsnetworks Oct 14, 2022
84cd190
Update dependency mypy to v0.982 (#160)
renovate[bot] Oct 17, 2022
280c73d
Update dependency pytest-cov to v4 (#161)
renovate[bot] Oct 17, 2022
f89466a
Update dependency invoke to v1.7.3 (#162)
renovate[bot] Oct 17, 2022
3d606e4
Update dependency black to v22.10.0 (#163)
renovate[bot] Oct 17, 2022
3048fd5
Update dependency pylint to v2.15.4 (#165)
renovate[bot] Oct 17, 2022
68597b0
Update dependency m2r2 to v0.3.3 (#164)
renovate[bot] Oct 17, 2022
55ffcf0
Merge pull request #167 from itdependsnetworks/diags
glennmatthews Oct 17, 2022
ea88dce
Diags dev (#169)
itdependsnetworks Oct 18, 2022
538b661
Add 'skip' counter to diff.summary() (#168)
ubaumann Oct 18, 2022
b2c57e2
Add documentation around ordering (#170)
itdependsnetworks Oct 21, 2022
e9d9457
Update dependency colorama to v0.4.6 (#172)
renovate[bot] Oct 25, 2022
f9e4cb5
Update dependency pytest to v7.2.0 (#173)
renovate[bot] Nov 1, 2022
6f022dd
Update docker, add methods to load from dictionary, get tree traversa…
itdependsnetworks Nov 2, 2022
b106171
Remove pytest-redislite. (#176)
Kircheneer Nov 2, 2022
5e0e99d
Add a Slack notify step in CI
chadell Nov 8, 2022
cbeef38
Merge pull request #184 from networktocode/main
Kircheneer Nov 8, 2022
7c7a8b9
Merge pull request #185 from networktocode/slack-notify
chadell Nov 8, 2022
f6afd05
Add methods for get or add/update model (#182)
itdependsnetworks Nov 28, 2022
c91f2c7
Feature/dakonr dx sync to (#188)
dakonr Nov 29, 2022
2029a68
Update README.md (#189)
Kircheneer Nov 29, 2022
748f136
Contributor Ramp up (#190)
dakonr Dec 6, 2022
b9b3b7a
Bump setuptools from 65.5.0 to 65.5.1 (#196)
dependabot[bot] Jan 3, 2023
36c858d
Update CI. (#212)
Kircheneer Mar 3, 2023
95fb491
Upgrade structlog, packaging dependencies (#211)
jamesharr Mar 3, 2023
b3ea16e
Version bump.
Kircheneer Mar 3, 2023
bd6cb38
Update CHANGELOG and LICENSE, bump version to 1.8.0
glennmatthews Apr 18, 2023
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
56 changes: 46 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v1"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
- name: "Linting: black"
run: "poetry run invoke black"
bandit:
Expand All @@ -40,7 +40,7 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v1"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
- name: "Linting: bandit"
run: "poetry run invoke bandit"
needs:
Expand All @@ -53,7 +53,7 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v1"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
- name: "Linting: pydocstyle"
run: "poetry run invoke pydocstyle"
needs:
Expand All @@ -66,7 +66,7 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v1"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
- name: "Linting: flake8"
run: "poetry run invoke flake8"
needs:
Expand All @@ -79,8 +79,8 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v1"
- name: "Linting: flake8"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
- name: "Linting: mypy"
run: "poetry run invoke mypy"
needs:
- "black"
Expand All @@ -92,7 +92,7 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v1"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
- name: "Linting: yamllint"
run: "poetry run invoke yamllint"
needs:
Expand All @@ -103,7 +103,7 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v1"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
- name: "Build Container"
run: "poetry run invoke build"
needs:
Expand All @@ -118,7 +118,7 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v1"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
- name: "Build Container"
run: "poetry run invoke build"
- name: "Linting: Pylint"
Expand All @@ -137,7 +137,7 @@ jobs:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v2"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
with:
python-version: "${{ matrix.python-version }}"
- name: "Install redis"
Expand Down Expand Up @@ -203,3 +203,39 @@ jobs:
password: "${{ secrets.PYPI_API_TOKEN }}"
needs:
- "unittest"
slack-notify:
needs:
- "publish_gh"
- "publish_pypi"
name: "Send notification to the Slack"
runs-on: "ubuntu-20.04"
env:
SLACK_WEBHOOK_URL: "${{ secrets.SLACK_WEBHOOK_URL }}"
SLACK_MESSAGE: >-
*NOTIFICATION: NEW-RELEASE-PUBLISHED*\n
Repository: <${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>\n
Release: <${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}|${{ github.ref_name }}>\n
Published by: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}>
steps:
- name: "Send a notification to Slack"
# ENVs cannot be used directly in job.if. This is a workaround to check
# if SLACK_WEBHOOK_URL is present.
if: "${{ env.SLACK_WEBHOOK_URL != '' }}"
uses: "slackapi/slack-github-action@v1.23.0"
with:
payload: |
{
"text": "${{ env.SLACK_MESSAGE }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ env.SLACK_MESSAGE }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: "${{ secrets.SLACK_WEBHOOK_URL }}"
SLACK_WEBHOOK_TYPE: "INCOMING_WEBHOOK"
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## v1.8.0 - 2023-04-18

### Added

- #182 - Added `get_or_add_model_instance()` and `update_or_add_model_instance()` APIs.
- #189 - Added note in `README.md` about running `invoke tests`.
- #190 - Added note in `README.md` about running `invoke build`.

### Changed

- #77/#188 - `sync_from()` and `sync_to()` now return the `Diff` that was applied.
- #211 - Loosened `packaging` and `structlog` library dependency constraints for broader compatibility.

## v1.7.0 - 2022-11-03

### Changed
Expand All @@ -10,7 +23,7 @@
### Added

- #174 - Add methods to load data from dictionary and enable tree traversal
- #174 - Add a get_or_none method to the DiffSync class
- #174 - Add a `get_or_none` method to the DiffSync class
- #168 - Add 'skip' counter to diff.summary()
- #169/#170 - Add documentation about model processing order
- #121/#140 - Add and configure renovate
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2020 Network to Code <info@networktocode.com>
Copyright 2020-2023 Network to Code <info@networktocode.com>
Network to Code, LLC

Licensed under the Apache License, Version 2.0 (the "License");
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,8 @@ The project is following Network to Code software development guidelines and are
- Black, Pylint, Bandit, flake8, and pydocstyle, mypy for Python linting, formatting and type hint checking.
- pytest, coverage, and unittest for unit tests.

You can ensure your contribution adheres to these checks by running `invoke tests` from the CLI.
The command `invoke build` builds a docker container with all the necessary dependencies (including the redis backend) locally to facilitate the execution of these tests.

# Questions
Please see the [documentation](https://diffsync.readthedocs.io/en/latest/index.html) for detailed documentation on how to use `diffsync`. For any additional questions or comments, feel free to swing by the [Network to Code slack channel](https://networktocode.slack.com/) (channel #networktocode). Sign up [here](http://slack.networktocode.com/)
50 changes: 41 additions & 9 deletions diffsync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,14 +526,14 @@ def load_from_dict(self, data: Dict):
# Synchronization between DiffSync instances
# ------------------------------------------------------------------------------

def sync_from(
def sync_from( # pylint: disable=too-many-arguments
self,
source: "DiffSync",
diff_class: Type[Diff] = Diff,
flags: DiffSyncFlags = DiffSyncFlags.NONE,
callback: Optional[Callable[[Text, int, int], None]] = None,
diff: Optional[Diff] = None,
): # pylint: disable=too-many-arguments:
) -> Diff:
"""Synchronize data from the given source DiffSync object into the current DiffSync object.

Args:
Expand All @@ -543,6 +543,10 @@ def sync_from(
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
calculation of the diff and subsequent sync proceed.
diff (Diff): An existing diff to be used rather than generating a completely new diff.
Returns:
Diff: Diff between origin object and source
Raises:
DiffClassMismatch: The provided diff's class does not match the diff_class
"""
if diff_class and diff:
if not isinstance(diff, diff_class):
Expand All @@ -558,14 +562,16 @@ def sync_from(
if result:
self.sync_complete(source, diff, flags, syncer.base_logger)

def sync_to(
return diff

def sync_to( # pylint: disable=too-many-arguments
self,
target: "DiffSync",
diff_class: Type[Diff] = Diff,
flags: DiffSyncFlags = DiffSyncFlags.NONE,
callback: Optional[Callable[[Text, int, int], None]] = None,
diff: Optional[Diff] = None,
): # pylint: disable=too-many-arguments
) -> Diff:
"""Synchronize data from the current DiffSync object into the given target DiffSync object.

Args:
Expand All @@ -575,15 +581,19 @@ def sync_to(
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
calculation of the diff and subsequent sync proceed.
diff (Diff): An existing diff that will be used when determining what needs to be synced.
Returns:
Diff: Diff between origin object and target
Raises:
DiffClassMismatch: The provided diff's class does not match the diff_class
"""
target.sync_from(self, diff_class=diff_class, flags=flags, callback=callback, diff=diff)
return target.sync_from(self, diff_class=diff_class, flags=flags, callback=callback, diff=diff)

def sync_complete(
self,
source: "DiffSync",
diff: Diff,
flags: DiffSyncFlags = DiffSyncFlags.NONE,
logger: structlog.BoundLogger = None,
logger: Optional[structlog.BoundLogger] = None,
):
"""Callback triggered after a `sync_from` operation has completed and updated the model data of this instance.

Expand Down Expand Up @@ -776,7 +786,7 @@ def remove(self, obj: DiffSyncModel, remove_children: bool = False):
return self.store.remove(obj=obj, remove_children=remove_children)

def get_or_instantiate(
self, model: Type[DiffSyncModel], ids: Dict, attrs: Dict = None
self, model: Type[DiffSyncModel], ids: Dict, attrs: Optional[Dict] = None
) -> Tuple[DiffSyncModel, bool]:
"""Attempt to get the object with provided identifiers or instantiate it with provided identifiers and attrs.

Expand All @@ -790,19 +800,41 @@ def get_or_instantiate(
"""
return self.store.get_or_instantiate(model=model, ids=ids, attrs=attrs)

def get_or_add_model_instance(self, obj: DiffSyncModel) -> Tuple[DiffSyncModel, bool]:
"""Attempt to get the object with provided obj identifiers or instantiate obj.

Args:
obj: An obj of the DiffSyncModel to get or add.

Returns:
Provides the existing or new object and whether it was created or not.
"""
return self.store.get_or_add_model_instance(obj=obj)

def update_or_instantiate(self, model: Type[DiffSyncModel], ids: Dict, attrs: Dict) -> Tuple[DiffSyncModel, bool]:
"""Attempt to update an existing object with provided ids/attrs or instantiate it with provided identifiers and attrs.

Args:
model (DiffSyncModel): The DiffSyncModel to get or create.
ids (Dict): Identifiers for the DiffSyncModel to get or create with.
model (DiffSyncModel): The DiffSyncModel to update or create.
ids (Dict): Identifiers for the DiffSyncModel to update or create with.
attrs (Dict): Attributes when creating/updating an object if it doesn't exist. Pass in empty dict, if no specific attrs.

Returns:
Tuple[DiffSyncModel, bool]: Provides the existing or new object and whether it was created or not.
"""
return self.store.update_or_instantiate(model=model, ids=ids, attrs=attrs)

def update_or_add_model_instance(self, obj: DiffSyncModel) -> Tuple[DiffSyncModel, bool]:
"""Attempt to update an existing object with provided obj ids/attrs or instantiate obj.

Args:
instance: An instance of the DiffSyncModel to update or create.

Returns:
Provides the existing or new object and whether it was created or not.
"""
return self.store.update_or_add_model_instance(obj=obj)

def count(self, model: Union[Text, "DiffSyncModel", Type["DiffSyncModel"], None] = None):
"""Count how many objects of one model type exist in the backend store.

Expand Down
2 changes: 1 addition & 1 deletion diffsync/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ def perform_sync(self) -> bool:
self.base_logger.info("Sync complete")
return changed

def sync_diff_element(self, element: DiffElement, parent_model: "DiffSyncModel" = None) -> bool:
def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSyncModel"] = None) -> bool:
"""Recursively synchronize the given DiffElement and its children, if any, into the dst_diffsync.

Helper method to `perform_sync`.
Expand Down
47 changes: 46 additions & 1 deletion diffsync/store/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def count(self, *, model: Union[Text, "DiffSyncModel", Type["DiffSyncModel"], No
raise NotImplementedError

def get_or_instantiate(
self, *, model: Type["DiffSyncModel"], ids: Dict, attrs: Dict = None
self, *, model: Type["DiffSyncModel"], ids: Dict, attrs: Optional[Dict] = None
) -> Tuple["DiffSyncModel", bool]:
"""Attempt to get the object with provided identifiers or instantiate it with provided identifiers and attrs.

Expand All @@ -159,6 +159,24 @@ def get_or_instantiate(

return obj, created

def get_or_add_model_instance(self, obj: "DiffSyncModel") -> Tuple["DiffSyncModel", bool]:
"""Attempt to get the object with provided obj identifiers or instantiate obj.

Args:
obj: An obj of the DiffSyncModel to get or add.

Returns:
Provides the existing or new object and whether it was added or not.
"""
model = obj.get_type()
ids = obj.get_unique_id()

try:
return self.get(model=model, identifier=ids), False
except ObjectNotFound:
self.add(obj=obj)
return obj, True

def update_or_instantiate(
self, *, model: Type["DiffSyncModel"], ids: Dict, attrs: Dict
) -> Tuple["DiffSyncModel", bool]:
Expand Down Expand Up @@ -188,6 +206,33 @@ def update_or_instantiate(

return obj, created

def update_or_add_model_instance(self, obj: "DiffSyncModel") -> Tuple["DiffSyncModel", bool]:
"""Attempt to update an existing object with provided ids/attrs or instantiate obj.

Args:
instance: An instance of the DiffSyncModel to update or create.

Returns:
Provides the existing or new object and whether it was added or not.
"""
model = obj.get_type()
ids = obj.get_unique_id()
attrs = obj.get_attrs()

added = False
try:
obj = self.get(model=model, identifier=ids)
except ObjectNotFound:
# Add the object to the diffsync instance
self.add(obj=obj)
added = True

# Update existing obj with attrs
for attr, value in attrs.items():
setattr(obj, attr, value)

return obj, added

def _get_object_class_and_model(
self, model: Union[Text, "DiffSyncModel", Type["DiffSyncModel"]]
) -> Tuple[Union["DiffSyncModel", Type["DiffSyncModel"], None], str]:
Expand Down
1 change: 0 additions & 1 deletion examples/03-remote-system/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ class AlphabeticalOrderDiff(Diff):
def order_children_default(cls, children):
"""Simple diff to return all children in alphabetical order."""
for child in sorted(children.values()):

# it's possible to access additional information about the object
# like child.action can be "update", "create" or "delete"

Expand Down
1 change: 0 additions & 1 deletion examples/03-remote-system/local_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ def load(self, filename=COUNTRIES_FILE): # pylint: disable=arguments-differ
# A Country object will be created for each country, it will be stored inside the adapter with self.add(),
# and it will be linked to its parent with parent.add_child(item)
for country in countries:

# Retrive the parent region object from the internal cache.
region = self.get(obj=self.region, identifier=slugify(country.get("region")))

Expand Down
Loading