Skip to content

Commit 410e178

Browse files
Merge pull request #226 from networktocode/release-1.8.0
Release 1.8.0
2 parents 1a982a3 + bd6cb38 commit 410e178

File tree

14 files changed

+1270
-807
lines changed

14 files changed

+1270
-807
lines changed

.github/workflows/ci.yml

+46-10
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: "Check out repository code"
3030
uses: "actions/checkout@v2"
3131
- name: "Setup environment"
32-
uses: "networktocode/gh-action-setup-poetry-environment@v1"
32+
uses: "networktocode/gh-action-setup-poetry-environment@v5"
3333
- name: "Linting: black"
3434
run: "poetry run invoke black"
3535
bandit:
@@ -40,7 +40,7 @@ jobs:
4040
- name: "Check out repository code"
4141
uses: "actions/checkout@v2"
4242
- name: "Setup environment"
43-
uses: "networktocode/gh-action-setup-poetry-environment@v1"
43+
uses: "networktocode/gh-action-setup-poetry-environment@v5"
4444
- name: "Linting: bandit"
4545
run: "poetry run invoke bandit"
4646
needs:
@@ -53,7 +53,7 @@ jobs:
5353
- name: "Check out repository code"
5454
uses: "actions/checkout@v2"
5555
- name: "Setup environment"
56-
uses: "networktocode/gh-action-setup-poetry-environment@v1"
56+
uses: "networktocode/gh-action-setup-poetry-environment@v5"
5757
- name: "Linting: pydocstyle"
5858
run: "poetry run invoke pydocstyle"
5959
needs:
@@ -66,7 +66,7 @@ jobs:
6666
- name: "Check out repository code"
6767
uses: "actions/checkout@v2"
6868
- name: "Setup environment"
69-
uses: "networktocode/gh-action-setup-poetry-environment@v1"
69+
uses: "networktocode/gh-action-setup-poetry-environment@v5"
7070
- name: "Linting: flake8"
7171
run: "poetry run invoke flake8"
7272
needs:
@@ -79,8 +79,8 @@ jobs:
7979
- name: "Check out repository code"
8080
uses: "actions/checkout@v2"
8181
- name: "Setup environment"
82-
uses: "networktocode/gh-action-setup-poetry-environment@v1"
83-
- name: "Linting: flake8"
82+
uses: "networktocode/gh-action-setup-poetry-environment@v5"
83+
- name: "Linting: mypy"
8484
run: "poetry run invoke mypy"
8585
needs:
8686
- "black"
@@ -92,7 +92,7 @@ jobs:
9292
- name: "Check out repository code"
9393
uses: "actions/checkout@v2"
9494
- name: "Setup environment"
95-
uses: "networktocode/gh-action-setup-poetry-environment@v1"
95+
uses: "networktocode/gh-action-setup-poetry-environment@v5"
9696
- name: "Linting: yamllint"
9797
run: "poetry run invoke yamllint"
9898
needs:
@@ -103,7 +103,7 @@ jobs:
103103
- name: "Check out repository code"
104104
uses: "actions/checkout@v2"
105105
- name: "Setup environment"
106-
uses: "networktocode/gh-action-setup-poetry-environment@v1"
106+
uses: "networktocode/gh-action-setup-poetry-environment@v5"
107107
- name: "Build Container"
108108
run: "poetry run invoke build"
109109
needs:
@@ -118,7 +118,7 @@ jobs:
118118
- name: "Check out repository code"
119119
uses: "actions/checkout@v2"
120120
- name: "Setup environment"
121-
uses: "networktocode/gh-action-setup-poetry-environment@v1"
121+
uses: "networktocode/gh-action-setup-poetry-environment@v5"
122122
- name: "Build Container"
123123
run: "poetry run invoke build"
124124
- name: "Linting: Pylint"
@@ -137,7 +137,7 @@ jobs:
137137
- name: "Check out repository code"
138138
uses: "actions/checkout@v2"
139139
- name: "Setup environment"
140-
uses: "networktocode/gh-action-setup-poetry-environment@v2"
140+
uses: "networktocode/gh-action-setup-poetry-environment@v5"
141141
with:
142142
python-version: "${{ matrix.python-version }}"
143143
- name: "Install redis"
@@ -203,3 +203,39 @@ jobs:
203203
password: "${{ secrets.PYPI_API_TOKEN }}"
204204
needs:
205205
- "unittest"
206+
slack-notify:
207+
needs:
208+
- "publish_gh"
209+
- "publish_pypi"
210+
name: "Send notification to the Slack"
211+
runs-on: "ubuntu-20.04"
212+
env:
213+
SLACK_WEBHOOK_URL: "${{ secrets.SLACK_WEBHOOK_URL }}"
214+
SLACK_MESSAGE: >-
215+
*NOTIFICATION: NEW-RELEASE-PUBLISHED*\n
216+
Repository: <${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>\n
217+
Release: <${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}|${{ github.ref_name }}>\n
218+
Published by: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}>
219+
steps:
220+
- name: "Send a notification to Slack"
221+
# ENVs cannot be used directly in job.if. This is a workaround to check
222+
# if SLACK_WEBHOOK_URL is present.
223+
if: "${{ env.SLACK_WEBHOOK_URL != '' }}"
224+
uses: "slackapi/[email protected]"
225+
with:
226+
payload: |
227+
{
228+
"text": "${{ env.SLACK_MESSAGE }}",
229+
"blocks": [
230+
{
231+
"type": "section",
232+
"text": {
233+
"type": "mrkdwn",
234+
"text": "${{ env.SLACK_MESSAGE }}"
235+
}
236+
}
237+
]
238+
}
239+
env:
240+
SLACK_WEBHOOK_URL: "${{ secrets.SLACK_WEBHOOK_URL }}"
241+
SLACK_WEBHOOK_TYPE: "INCOMING_WEBHOOK"

CHANGELOG.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## v1.8.0 - 2023-04-18
4+
5+
### Added
6+
7+
- #182 - Added `get_or_add_model_instance()` and `update_or_add_model_instance()` APIs.
8+
- #189 - Added note in `README.md` about running `invoke tests`.
9+
- #190 - Added note in `README.md` about running `invoke build`.
10+
11+
### Changed
12+
13+
- #77/#188 - `sync_from()` and `sync_to()` now return the `Diff` that was applied.
14+
- #211 - Loosened `packaging` and `structlog` library dependency constraints for broader compatibility.
15+
316
## v1.7.0 - 2022-11-03
417

518
### Changed
@@ -10,7 +23,7 @@
1023
### Added
1124

1225
- #174 - Add methods to load data from dictionary and enable tree traversal
13-
- #174 - Add a get_or_none method to the DiffSync class
26+
- #174 - Add a `get_or_none` method to the DiffSync class
1427
- #168 - Add 'skip' counter to diff.summary()
1528
- #169/#170 - Add documentation about model processing order
1629
- #121/#140 - Add and configure renovate

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright 2020 Network to Code <[email protected]>
1+
Copyright 2020-2023 Network to Code <[email protected]>
22
Network to Code, LLC
33

44
Licensed under the Apache License, Version 2.0 (the "License");

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,8 @@ The project is following Network to Code software development guidelines and are
7575
- Black, Pylint, Bandit, flake8, and pydocstyle, mypy for Python linting, formatting and type hint checking.
7676
- pytest, coverage, and unittest for unit tests.
7777

78+
You can ensure your contribution adheres to these checks by running `invoke tests` from the CLI.
79+
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.
80+
7881
# Questions
7982
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/)

diffsync/__init__.py

+41-9
Original file line numberDiff line numberDiff line change
@@ -526,14 +526,14 @@ def load_from_dict(self, data: Dict):
526526
# Synchronization between DiffSync instances
527527
# ------------------------------------------------------------------------------
528528

529-
def sync_from(
529+
def sync_from( # pylint: disable=too-many-arguments
530530
self,
531531
source: "DiffSync",
532532
diff_class: Type[Diff] = Diff,
533533
flags: DiffSyncFlags = DiffSyncFlags.NONE,
534534
callback: Optional[Callable[[Text, int, int], None]] = None,
535535
diff: Optional[Diff] = None,
536-
): # pylint: disable=too-many-arguments:
536+
) -> Diff:
537537
"""Synchronize data from the given source DiffSync object into the current DiffSync object.
538538
539539
Args:
@@ -543,6 +543,10 @@ def sync_from(
543543
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
544544
calculation of the diff and subsequent sync proceed.
545545
diff (Diff): An existing diff to be used rather than generating a completely new diff.
546+
Returns:
547+
Diff: Diff between origin object and source
548+
Raises:
549+
DiffClassMismatch: The provided diff's class does not match the diff_class
546550
"""
547551
if diff_class and diff:
548552
if not isinstance(diff, diff_class):
@@ -558,14 +562,16 @@ def sync_from(
558562
if result:
559563
self.sync_complete(source, diff, flags, syncer.base_logger)
560564

561-
def sync_to(
565+
return diff
566+
567+
def sync_to( # pylint: disable=too-many-arguments
562568
self,
563569
target: "DiffSync",
564570
diff_class: Type[Diff] = Diff,
565571
flags: DiffSyncFlags = DiffSyncFlags.NONE,
566572
callback: Optional[Callable[[Text, int, int], None]] = None,
567573
diff: Optional[Diff] = None,
568-
): # pylint: disable=too-many-arguments
574+
) -> Diff:
569575
"""Synchronize data from the current DiffSync object into the given target DiffSync object.
570576
571577
Args:
@@ -575,15 +581,19 @@ def sync_to(
575581
callback (function): Function with parameters (stage, current, total), to be called at intervals as the
576582
calculation of the diff and subsequent sync proceed.
577583
diff (Diff): An existing diff that will be used when determining what needs to be synced.
584+
Returns:
585+
Diff: Diff between origin object and target
586+
Raises:
587+
DiffClassMismatch: The provided diff's class does not match the diff_class
578588
"""
579-
target.sync_from(self, diff_class=diff_class, flags=flags, callback=callback, diff=diff)
589+
return target.sync_from(self, diff_class=diff_class, flags=flags, callback=callback, diff=diff)
580590

581591
def sync_complete(
582592
self,
583593
source: "DiffSync",
584594
diff: Diff,
585595
flags: DiffSyncFlags = DiffSyncFlags.NONE,
586-
logger: structlog.BoundLogger = None,
596+
logger: Optional[structlog.BoundLogger] = None,
587597
):
588598
"""Callback triggered after a `sync_from` operation has completed and updated the model data of this instance.
589599
@@ -776,7 +786,7 @@ def remove(self, obj: DiffSyncModel, remove_children: bool = False):
776786
return self.store.remove(obj=obj, remove_children=remove_children)
777787

778788
def get_or_instantiate(
779-
self, model: Type[DiffSyncModel], ids: Dict, attrs: Dict = None
789+
self, model: Type[DiffSyncModel], ids: Dict, attrs: Optional[Dict] = None
780790
) -> Tuple[DiffSyncModel, bool]:
781791
"""Attempt to get the object with provided identifiers or instantiate it with provided identifiers and attrs.
782792
@@ -790,19 +800,41 @@ def get_or_instantiate(
790800
"""
791801
return self.store.get_or_instantiate(model=model, ids=ids, attrs=attrs)
792802

803+
def get_or_add_model_instance(self, obj: DiffSyncModel) -> Tuple[DiffSyncModel, bool]:
804+
"""Attempt to get the object with provided obj identifiers or instantiate obj.
805+
806+
Args:
807+
obj: An obj of the DiffSyncModel to get or add.
808+
809+
Returns:
810+
Provides the existing or new object and whether it was created or not.
811+
"""
812+
return self.store.get_or_add_model_instance(obj=obj)
813+
793814
def update_or_instantiate(self, model: Type[DiffSyncModel], ids: Dict, attrs: Dict) -> Tuple[DiffSyncModel, bool]:
794815
"""Attempt to update an existing object with provided ids/attrs or instantiate it with provided identifiers and attrs.
795816
796817
Args:
797-
model (DiffSyncModel): The DiffSyncModel to get or create.
798-
ids (Dict): Identifiers for the DiffSyncModel to get or create with.
818+
model (DiffSyncModel): The DiffSyncModel to update or create.
819+
ids (Dict): Identifiers for the DiffSyncModel to update or create with.
799820
attrs (Dict): Attributes when creating/updating an object if it doesn't exist. Pass in empty dict, if no specific attrs.
800821
801822
Returns:
802823
Tuple[DiffSyncModel, bool]: Provides the existing or new object and whether it was created or not.
803824
"""
804825
return self.store.update_or_instantiate(model=model, ids=ids, attrs=attrs)
805826

827+
def update_or_add_model_instance(self, obj: DiffSyncModel) -> Tuple[DiffSyncModel, bool]:
828+
"""Attempt to update an existing object with provided obj ids/attrs or instantiate obj.
829+
830+
Args:
831+
instance: An instance of the DiffSyncModel to update or create.
832+
833+
Returns:
834+
Provides the existing or new object and whether it was created or not.
835+
"""
836+
return self.store.update_or_add_model_instance(obj=obj)
837+
806838
def count(self, model: Union[Text, "DiffSyncModel", Type["DiffSyncModel"], None] = None):
807839
"""Count how many objects of one model type exist in the backend store.
808840

diffsync/helpers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ def perform_sync(self) -> bool:
328328
self.base_logger.info("Sync complete")
329329
return changed
330330

331-
def sync_diff_element(self, element: DiffElement, parent_model: "DiffSyncModel" = None) -> bool:
331+
def sync_diff_element(self, element: DiffElement, parent_model: Optional["DiffSyncModel"] = None) -> bool:
332332
"""Recursively synchronize the given DiffElement and its children, if any, into the dst_diffsync.
333333
334334
Helper method to `perform_sync`.

diffsync/store/__init__.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def count(self, *, model: Union[Text, "DiffSyncModel", Type["DiffSyncModel"], No
134134
raise NotImplementedError
135135

136136
def get_or_instantiate(
137-
self, *, model: Type["DiffSyncModel"], ids: Dict, attrs: Dict = None
137+
self, *, model: Type["DiffSyncModel"], ids: Dict, attrs: Optional[Dict] = None
138138
) -> Tuple["DiffSyncModel", bool]:
139139
"""Attempt to get the object with provided identifiers or instantiate it with provided identifiers and attrs.
140140
@@ -159,6 +159,24 @@ def get_or_instantiate(
159159

160160
return obj, created
161161

162+
def get_or_add_model_instance(self, obj: "DiffSyncModel") -> Tuple["DiffSyncModel", bool]:
163+
"""Attempt to get the object with provided obj identifiers or instantiate obj.
164+
165+
Args:
166+
obj: An obj of the DiffSyncModel to get or add.
167+
168+
Returns:
169+
Provides the existing or new object and whether it was added or not.
170+
"""
171+
model = obj.get_type()
172+
ids = obj.get_unique_id()
173+
174+
try:
175+
return self.get(model=model, identifier=ids), False
176+
except ObjectNotFound:
177+
self.add(obj=obj)
178+
return obj, True
179+
162180
def update_or_instantiate(
163181
self, *, model: Type["DiffSyncModel"], ids: Dict, attrs: Dict
164182
) -> Tuple["DiffSyncModel", bool]:
@@ -188,6 +206,33 @@ def update_or_instantiate(
188206

189207
return obj, created
190208

209+
def update_or_add_model_instance(self, obj: "DiffSyncModel") -> Tuple["DiffSyncModel", bool]:
210+
"""Attempt to update an existing object with provided ids/attrs or instantiate obj.
211+
212+
Args:
213+
instance: An instance of the DiffSyncModel to update or create.
214+
215+
Returns:
216+
Provides the existing or new object and whether it was added or not.
217+
"""
218+
model = obj.get_type()
219+
ids = obj.get_unique_id()
220+
attrs = obj.get_attrs()
221+
222+
added = False
223+
try:
224+
obj = self.get(model=model, identifier=ids)
225+
except ObjectNotFound:
226+
# Add the object to the diffsync instance
227+
self.add(obj=obj)
228+
added = True
229+
230+
# Update existing obj with attrs
231+
for attr, value in attrs.items():
232+
setattr(obj, attr, value)
233+
234+
return obj, added
235+
191236
def _get_object_class_and_model(
192237
self, model: Union[Text, "DiffSyncModel", Type["DiffSyncModel"]]
193238
) -> Tuple[Union["DiffSyncModel", Type["DiffSyncModel"], None], str]:

examples/03-remote-system/diff.py

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ class AlphabeticalOrderDiff(Diff):
99
def order_children_default(cls, children):
1010
"""Simple diff to return all children in alphabetical order."""
1111
for child in sorted(children.values()):
12-
1312
# it's possible to access additional information about the object
1413
# like child.action can be "update", "create" or "delete"
1514

examples/03-remote-system/local_adapter.py

-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ def load(self, filename=COUNTRIES_FILE): # pylint: disable=arguments-differ
4343
# A Country object will be created for each country, it will be stored inside the adapter with self.add(),
4444
# and it will be linked to its parent with parent.add_child(item)
4545
for country in countries:
46-
4746
# Retrive the parent region object from the internal cache.
4847
region = self.get(obj=self.region, identifier=slugify(country.get("region")))
4948

0 commit comments

Comments
 (0)