diffsync 1.9.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diffsync-1.9.0 → diffsync-2.0.0}/CHANGELOG.md +37 -16
- {diffsync-1.9.0 → diffsync-2.0.0}/PKG-INFO +6 -5
- {diffsync-1.9.0 → diffsync-2.0.0}/README.md +1 -1
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/__init__.py +99 -49
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/helpers.py +9 -7
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/store/__init__.py +7 -7
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/store/local.py +2 -2
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/store/redis.py +3 -3
- {diffsync-1.9.0 → diffsync-2.0.0}/pyproject.toml +5 -3
- {diffsync-1.9.0 → diffsync-2.0.0}/LICENSE +0 -0
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/diff.py +0 -0
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/enum.py +0 -0
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/exceptions.py +0 -0
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/logging.py +0 -0
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/py.typed +0 -0
- {diffsync-1.9.0 → diffsync-2.0.0}/diffsync/utils.py +0 -0
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
|
+
|
|
7
|
+
## [2.0.0]
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **BREAKING CHANGE** #236/240 - Upgrade to Pydantic v2.
|
|
12
|
+
|
|
13
|
+
## [1.10.0] - 2023-11-16
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- #249 - Fixes natural deletion order flag
|
|
18
|
+
- #247 - Fixes underspecified typing_extensions dependency
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- #247 - Deprecates Python 3.7
|
|
23
|
+
|
|
24
|
+
## [1.9.0] - 2023-10-16
|
|
4
25
|
|
|
5
26
|
### Added
|
|
6
27
|
|
|
@@ -10,7 +31,7 @@
|
|
|
10
31
|
|
|
11
32
|
- #219 - Type hinting overhaul
|
|
12
33
|
|
|
13
|
-
##
|
|
34
|
+
## [1.8.0] - 2023-04-18
|
|
14
35
|
|
|
15
36
|
### Added
|
|
16
37
|
|
|
@@ -23,7 +44,7 @@
|
|
|
23
44
|
- #77/#188 - `sync_from()` and `sync_to()` now return the `Diff` that was applied.
|
|
24
45
|
- #211 - Loosened `packaging` and `structlog` library dependency constraints for broader compatibility.
|
|
25
46
|
|
|
26
|
-
##
|
|
47
|
+
## [1.7.0] - 2022-11-03
|
|
27
48
|
|
|
28
49
|
### Changed
|
|
29
50
|
|
|
@@ -41,15 +62,15 @@
|
|
|
41
62
|
|
|
42
63
|
### Fixed
|
|
43
64
|
|
|
44
|
-
- #149 Limit redundant CI concurrency
|
|
65
|
+
- #149 - Limit redundant CI concurrency
|
|
45
66
|
|
|
46
|
-
##
|
|
67
|
+
## [1.6.0] - 2022-07-09
|
|
47
68
|
|
|
48
69
|
### Changed
|
|
49
70
|
|
|
50
71
|
- #120 - Dropped support for Python 3.6, new minimum is Python 3.7
|
|
51
72
|
|
|
52
|
-
##
|
|
73
|
+
## [1.5.1] - 2022-06-30
|
|
53
74
|
|
|
54
75
|
### Added
|
|
55
76
|
|
|
@@ -64,13 +85,13 @@
|
|
|
64
85
|
- #115 - Fixed ReadTheDocs rendering pipeline
|
|
65
86
|
- #118 - Fixed a regression in `DiffSync.get(modelname, identifiers)` introduced in 1.5.0
|
|
66
87
|
|
|
67
|
-
##
|
|
88
|
+
## [1.5.0] - 2022-06-07
|
|
68
89
|
|
|
69
90
|
### Added
|
|
70
91
|
|
|
71
92
|
- #106 - Add a new, optional, backend store based in Redis
|
|
72
93
|
|
|
73
|
-
##
|
|
94
|
+
## [1.4.3] - 2022-03-03
|
|
74
95
|
|
|
75
96
|
### Fixed
|
|
76
97
|
|
|
@@ -80,9 +101,9 @@
|
|
|
80
101
|
|
|
81
102
|
### Changed
|
|
82
103
|
|
|
83
|
-
- #103 Update development dependencies
|
|
104
|
+
- #103 - Update development dependencies
|
|
84
105
|
|
|
85
|
-
##
|
|
106
|
+
## [1.4.2] - 2022-02-28
|
|
86
107
|
|
|
87
108
|
**WARNING** - #90 inadvertently introduced a breaking API change in DiffSync 1.4.0 through 1.4.2 (#101); this change was reverted in #102 for DiffSync 1.4.3 and later. We recommend not using this release, and moving to 1.4.3 instead.
|
|
88
109
|
|
|
@@ -90,7 +111,7 @@
|
|
|
90
111
|
|
|
91
112
|
- #100 - Added explicit dependency on `packaging`.
|
|
92
113
|
|
|
93
|
-
##
|
|
114
|
+
## [1.4.1] - 2022-01-26
|
|
94
115
|
|
|
95
116
|
**WARNING** - #90 inadvertently introduced a breaking API change in DiffSync 1.4.0 through 1.4.2 (#101); this change was reverted in #102 for DiffSync 1.4.3 and later. We recommend not using this release, and moving to 1.4.3 instead.
|
|
96
117
|
|
|
@@ -98,7 +119,7 @@
|
|
|
98
119
|
|
|
99
120
|
- #95 - Removed optional dependencies on `sphinx`, `m2r2`, `sphinx-rtd-theme`, `toml`.
|
|
100
121
|
|
|
101
|
-
##
|
|
122
|
+
## [1.4.0] - 2022-01-24
|
|
102
123
|
|
|
103
124
|
**WARNING** - #90 inadvertently introduced a breaking API change in DiffSync 1.4.0 through 1.4.2 (#101); this change was reverted in #102 for DiffSync 1.4.3 and later. We recommend not using this release, and moving to 1.4.3 instead.
|
|
104
125
|
|
|
@@ -127,19 +148,19 @@
|
|
|
127
148
|
- #51 - Update minimum Pydantic version due to security advisory GHSA-5jqp-qgf6-3pvh
|
|
128
149
|
- #63 - Fix type in Readme
|
|
129
150
|
|
|
130
|
-
##
|
|
151
|
+
## [1.3.0] - 2021-04-07
|
|
131
152
|
|
|
132
153
|
### Added
|
|
133
154
|
|
|
134
155
|
- #48 - added optional `callback` argument to `diff_from`/`diff_to`/`sync_from`/`sync_to` for use with progress reporting.
|
|
135
156
|
|
|
136
|
-
##
|
|
157
|
+
## [1.2.0] - 2020-12-08
|
|
137
158
|
|
|
138
159
|
### Added
|
|
139
160
|
|
|
140
161
|
- #45 - minimum Python version lowered from 3.7 to 3.6, also now tested against Python 3.9.
|
|
141
162
|
|
|
142
|
-
##
|
|
163
|
+
## [1.1.0] - 2020-12-01
|
|
143
164
|
|
|
144
165
|
### Added
|
|
145
166
|
|
|
@@ -157,6 +178,6 @@
|
|
|
157
178
|
|
|
158
179
|
- #44 - On CRUD failure, do not generate an extraneous "success" log message in addition to the "failed" message
|
|
159
180
|
|
|
160
|
-
##
|
|
181
|
+
## [1.0.0] - 2020-10-23
|
|
161
182
|
|
|
162
183
|
Initial release
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: diffsync
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Library to easily sync/diff/update 2 different data sources
|
|
5
5
|
Home-page: https://diffsync.readthedocs.io
|
|
6
6
|
License: Apache-2.0
|
|
7
7
|
Keywords: source-of-truth,synchronization
|
|
8
8
|
Author: Network to Code, LLC
|
|
9
9
|
Author-email: info@networktocode.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.8,<4.0
|
|
11
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.8
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.9
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Provides-Extra: redis
|
|
19
19
|
Requires-Dist: colorama (>=0.4.3,<0.5.0)
|
|
20
20
|
Requires-Dist: packaging (>=21.3,<24.0)
|
|
21
|
-
Requires-Dist: pydantic (>=
|
|
21
|
+
Requires-Dist: pydantic (>=2.0.0,<3.0.0)
|
|
22
22
|
Requires-Dist: redis (>=4.3,<5.0) ; extra == "redis"
|
|
23
23
|
Requires-Dist: structlog (>=20.1.0,<23.0.0)
|
|
24
|
+
Requires-Dist: typing-extensions (>=4.0.1) ; python_version < "3.11"
|
|
24
25
|
Project-URL: Documentation, https://diffsync.readthedocs.io
|
|
25
26
|
Project-URL: Repository, https://github.com/networktocode/diffsync
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
@@ -42,7 +43,7 @@ DiffSync is at its most useful when you have multiple sources or sets of data to
|
|
|
42
43
|
|
|
43
44
|
# Overview of DiffSync
|
|
44
45
|
|
|
45
|
-
DiffSync acts as an intermediate translation layer between all of the data sets you are diffing and/or syncing. In practical terms, this means that to use DiffSync, you will define a set of data models as well as the “adapters” needed to translate between each base data source and the data model. In Python terms, the adapters will be subclasses of the `
|
|
46
|
+
DiffSync acts as an intermediate translation layer between all of the data sets you are diffing and/or syncing. In practical terms, this means that to use DiffSync, you will define a set of data models as well as the “adapters” needed to translate between each base data source and the data model. In Python terms, the adapters will be subclasses of the `Adapter` class, and each data model class will be a subclass of the `DiffSyncModel` class.
|
|
46
47
|
|
|
47
48
|

|
|
48
49
|
|
|
@@ -15,7 +15,7 @@ DiffSync is at its most useful when you have multiple sources or sets of data to
|
|
|
15
15
|
|
|
16
16
|
# Overview of DiffSync
|
|
17
17
|
|
|
18
|
-
DiffSync acts as an intermediate translation layer between all of the data sets you are diffing and/or syncing. In practical terms, this means that to use DiffSync, you will define a set of data models as well as the “adapters” needed to translate between each base data source and the data model. In Python terms, the adapters will be subclasses of the `
|
|
18
|
+
DiffSync acts as an intermediate translation layer between all of the data sets you are diffing and/or syncing. In practical terms, this means that to use DiffSync, you will define a set of data models as well as the “adapters” needed to translate between each base data source and the data model. In Python terms, the adapters will be subclasses of the `Adapter` class, and each data model class will be a subclass of the `DiffSyncModel` class.
|
|
19
19
|
|
|
20
20
|

|
|
21
21
|
|
|
@@ -14,21 +14,43 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
14
14
|
See the License for the specific language governing permissions and
|
|
15
15
|
limitations under the License.
|
|
16
16
|
"""
|
|
17
|
+
import sys
|
|
17
18
|
from inspect import isclass
|
|
18
|
-
from typing import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
from typing import (
|
|
20
|
+
Callable,
|
|
21
|
+
ClassVar,
|
|
22
|
+
Dict,
|
|
23
|
+
List,
|
|
24
|
+
Optional,
|
|
25
|
+
Tuple,
|
|
26
|
+
Type,
|
|
27
|
+
Union,
|
|
28
|
+
Any,
|
|
29
|
+
Set,
|
|
30
|
+
)
|
|
31
|
+
import warnings
|
|
32
|
+
|
|
33
|
+
from pydantic import ConfigDict, BaseModel, PrivateAttr
|
|
22
34
|
import structlog # type: ignore
|
|
23
35
|
|
|
24
36
|
from diffsync.diff import Diff
|
|
25
37
|
from diffsync.enum import DiffSyncModelFlags, DiffSyncFlags, DiffSyncStatus
|
|
26
|
-
from diffsync.exceptions import
|
|
38
|
+
from diffsync.exceptions import (
|
|
39
|
+
DiffClassMismatch,
|
|
40
|
+
ObjectAlreadyExists,
|
|
41
|
+
ObjectStoreWrongType,
|
|
42
|
+
ObjectNotFound,
|
|
43
|
+
)
|
|
27
44
|
from diffsync.helpers import DiffSyncDiffer, DiffSyncSyncer
|
|
28
45
|
from diffsync.store import BaseStore
|
|
29
46
|
from diffsync.store.local import LocalStore
|
|
30
47
|
from diffsync.utils import get_path, set_key, tree_string
|
|
31
48
|
|
|
49
|
+
if sys.version_info >= (3, 11):
|
|
50
|
+
from typing import Self
|
|
51
|
+
else:
|
|
52
|
+
from typing_extensions import Self
|
|
53
|
+
|
|
32
54
|
# This workaround is used because we are defining a method called `str` in our class definition, which therefore renders
|
|
33
55
|
# the builtin `str` type unusable.
|
|
34
56
|
StrType = str
|
|
@@ -91,8 +113,8 @@ class DiffSyncModel(BaseModel):
|
|
|
91
113
|
Can be set as a class attribute or an instance attribute as needed.
|
|
92
114
|
"""
|
|
93
115
|
|
|
94
|
-
|
|
95
|
-
"""Optional: the
|
|
116
|
+
adapter: Optional["Adapter"] = None
|
|
117
|
+
"""Optional: the Adapter instance that owns this model instance."""
|
|
96
118
|
|
|
97
119
|
_status: DiffSyncStatus = PrivateAttr(DiffSyncStatus.SUCCESS)
|
|
98
120
|
"""Status of the last attempt at creating/updating/deleting this model."""
|
|
@@ -100,30 +122,27 @@ class DiffSyncModel(BaseModel):
|
|
|
100
122
|
_status_message: str = PrivateAttr("")
|
|
101
123
|
"""Message, if any, associated with the create/update/delete status value."""
|
|
102
124
|
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
126
|
+
"""Pydantic-specific configuration to allow arbitrary types on this class."""
|
|
105
127
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def __init_subclass__(cls) -> None:
|
|
128
|
+
@classmethod
|
|
129
|
+
def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
|
|
110
130
|
"""Validate that the various class attribute declarations correspond to actual instance fields.
|
|
111
131
|
|
|
112
132
|
Called automatically on subclass declaration.
|
|
113
133
|
"""
|
|
114
|
-
variables = cls.__fields__.keys()
|
|
115
134
|
# Make sure that any field referenced by name actually exists on the model
|
|
116
135
|
for attr in cls._identifiers:
|
|
117
|
-
if attr not in
|
|
136
|
+
if attr not in cls.model_fields and not hasattr(cls, attr):
|
|
118
137
|
raise AttributeError(f"_identifiers {cls._identifiers} references missing or un-annotated attr {attr}")
|
|
119
138
|
for attr in cls._shortname:
|
|
120
|
-
if attr not in
|
|
139
|
+
if attr not in cls.model_fields:
|
|
121
140
|
raise AttributeError(f"_shortname {cls._shortname} references missing or un-annotated attr {attr}")
|
|
122
141
|
for attr in cls._attributes:
|
|
123
|
-
if attr not in
|
|
142
|
+
if attr not in cls.model_fields:
|
|
124
143
|
raise AttributeError(f"_attributes {cls._attributes} references missing or un-annotated attr {attr}")
|
|
125
144
|
for attr in cls._children.values():
|
|
126
|
-
if attr not in
|
|
145
|
+
if attr not in cls.model_fields:
|
|
127
146
|
raise AttributeError(f"_children {cls._children} references missing or un-annotated attr {attr}")
|
|
128
147
|
|
|
129
148
|
# Any given field can only be in one of (_identifiers, _attributes, _children)
|
|
@@ -144,18 +163,18 @@ class DiffSyncModel(BaseModel):
|
|
|
144
163
|
return self.get_unique_id()
|
|
145
164
|
|
|
146
165
|
def dict(self, **kwargs: Any) -> Dict:
|
|
147
|
-
"""Convert this DiffSyncModel to a dict, excluding the
|
|
166
|
+
"""Convert this DiffSyncModel to a dict, excluding the adapter field by default as it is not serializable."""
|
|
148
167
|
if "exclude" not in kwargs:
|
|
149
|
-
kwargs["exclude"] = {"
|
|
150
|
-
return super().
|
|
168
|
+
kwargs["exclude"] = {"adapter"}
|
|
169
|
+
return super().model_dump(**kwargs)
|
|
151
170
|
|
|
152
171
|
def json(self, **kwargs: Any) -> StrType:
|
|
153
|
-
"""Convert this DiffSyncModel to a JSON string, excluding the
|
|
172
|
+
"""Convert this DiffSyncModel to a JSON string, excluding the adapter field by default as it is not serializable."""
|
|
154
173
|
if "exclude" not in kwargs:
|
|
155
|
-
kwargs["exclude"] = {"
|
|
174
|
+
kwargs["exclude"] = {"adapter"}
|
|
156
175
|
if "exclude_defaults" not in kwargs:
|
|
157
176
|
kwargs["exclude_defaults"] = True
|
|
158
|
-
return super().
|
|
177
|
+
return super().model_dump_json(**kwargs)
|
|
159
178
|
|
|
160
179
|
def str(self, include_children: bool = True, indent: int = 0) -> StrType:
|
|
161
180
|
"""Build a detailed string representation of this DiffSyncModel and optionally its children."""
|
|
@@ -166,12 +185,12 @@ class DiffSyncModel(BaseModel):
|
|
|
166
185
|
child_ids = getattr(self, fieldname)
|
|
167
186
|
if not child_ids:
|
|
168
187
|
output += ": []"
|
|
169
|
-
elif not self.
|
|
188
|
+
elif not self.adapter or not include_children:
|
|
170
189
|
output += f": {child_ids}"
|
|
171
190
|
else:
|
|
172
191
|
for child_id in child_ids:
|
|
173
192
|
try:
|
|
174
|
-
child = self.
|
|
193
|
+
child = self.adapter.get(modelname, child_id)
|
|
175
194
|
output += "\n" + child.str(include_children=include_children, indent=indent + 4)
|
|
176
195
|
except ObjectNotFound:
|
|
177
196
|
output += f"\n{margin} {child_id} (ERROR: details unavailable)"
|
|
@@ -183,32 +202,32 @@ class DiffSyncModel(BaseModel):
|
|
|
183
202
|
self._status_message = message
|
|
184
203
|
|
|
185
204
|
@classmethod
|
|
186
|
-
def create_base(cls,
|
|
205
|
+
def create_base(cls, adapter: "Adapter", ids: Dict, attrs: Dict) -> Optional[Self]:
|
|
187
206
|
"""Instantiate this class, along with any platform-specific data creation.
|
|
188
207
|
|
|
189
208
|
This method is not meant to be subclassed, users should redefine create() instead.
|
|
190
209
|
|
|
191
210
|
Args:
|
|
192
|
-
|
|
211
|
+
adapter: The master data store for other DiffSyncModel instances that we might need to reference
|
|
193
212
|
ids: Dictionary of unique-identifiers needed to create the new object
|
|
194
213
|
attrs: Dictionary of additional attributes to set on the new object
|
|
195
214
|
|
|
196
215
|
Returns:
|
|
197
216
|
DiffSyncModel: instance of this class.
|
|
198
217
|
"""
|
|
199
|
-
model = cls(**ids,
|
|
218
|
+
model = cls(**ids, adapter=adapter, **attrs)
|
|
200
219
|
model.set_status(DiffSyncStatus.SUCCESS, "Created successfully")
|
|
201
220
|
return model
|
|
202
221
|
|
|
203
222
|
@classmethod
|
|
204
|
-
def create(cls,
|
|
223
|
+
def create(cls, adapter: "Adapter", ids: Dict, attrs: Dict) -> Optional[Self]:
|
|
205
224
|
"""Instantiate this class, along with any platform-specific data creation.
|
|
206
225
|
|
|
207
226
|
Subclasses must call `super().create()` or `self.create_base()`; they may wish to then override the default status information
|
|
208
227
|
by calling `set_status()` to provide more context (such as details of any interactions with underlying systems).
|
|
209
228
|
|
|
210
229
|
Args:
|
|
211
|
-
|
|
230
|
+
adapter: The master data store for other DiffSyncModel instances that we might need to reference
|
|
212
231
|
ids: Dictionary of unique-identifiers needed to create the new object
|
|
213
232
|
attrs: Dictionary of additional attributes to set on the new object
|
|
214
233
|
|
|
@@ -219,7 +238,7 @@ class DiffSyncModel(BaseModel):
|
|
|
219
238
|
Raises:
|
|
220
239
|
ObjectNotCreated: if an error occurred.
|
|
221
240
|
"""
|
|
222
|
-
return cls.create_base(
|
|
241
|
+
return cls.create_base(adapter=adapter, ids=ids, attrs=attrs)
|
|
223
242
|
|
|
224
243
|
def update_base(self, attrs: Dict) -> Optional[Self]:
|
|
225
244
|
"""Base Update method to update the attributes of this instance, along with any platform-specific data updates.
|
|
@@ -375,7 +394,10 @@ class DiffSyncModel(BaseModel):
|
|
|
375
394
|
attr_name = self._children[child_type]
|
|
376
395
|
childs = getattr(self, attr_name)
|
|
377
396
|
if child.get_unique_id() in childs:
|
|
378
|
-
raise ObjectAlreadyExists(
|
|
397
|
+
raise ObjectAlreadyExists(
|
|
398
|
+
f"Already storing a {child_type} with unique_id {child.get_unique_id()}",
|
|
399
|
+
child,
|
|
400
|
+
)
|
|
379
401
|
childs.append(child.get_unique_id())
|
|
380
402
|
|
|
381
403
|
def remove_child(self, child: "DiffSyncModel") -> None:
|
|
@@ -402,7 +424,7 @@ class DiffSyncModel(BaseModel):
|
|
|
402
424
|
childs.remove(child.get_unique_id())
|
|
403
425
|
|
|
404
426
|
|
|
405
|
-
class
|
|
427
|
+
class Adapter: # pylint: disable=too-many-public-methods
|
|
406
428
|
"""Class for storing a group of DiffSyncModel instances and diffing/synchronizing to another DiffSync instance."""
|
|
407
429
|
|
|
408
430
|
# In any subclass, you would add mapping of names to specific model classes here:
|
|
@@ -416,7 +438,9 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
416
438
|
"""List of top-level modelnames to begin from when diffing or synchronizing."""
|
|
417
439
|
|
|
418
440
|
def __init__(
|
|
419
|
-
self,
|
|
441
|
+
self,
|
|
442
|
+
name: Optional[str] = None,
|
|
443
|
+
internal_storage_engine: Union[Type[BaseStore], BaseStore] = LocalStore,
|
|
420
444
|
) -> None:
|
|
421
445
|
"""Generic initialization function.
|
|
422
446
|
|
|
@@ -425,9 +449,9 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
425
449
|
|
|
426
450
|
if isinstance(internal_storage_engine, BaseStore):
|
|
427
451
|
self.store = internal_storage_engine
|
|
428
|
-
self.store.
|
|
452
|
+
self.store.adapter = self
|
|
429
453
|
else:
|
|
430
|
-
self.store = internal_storage_engine(
|
|
454
|
+
self.store = internal_storage_engine(adapter=self)
|
|
431
455
|
|
|
432
456
|
# If the type is not defined, use the name of the class as the default value
|
|
433
457
|
if self.type is None:
|
|
@@ -535,7 +559,7 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
535
559
|
|
|
536
560
|
def sync_from( # pylint: disable=too-many-arguments
|
|
537
561
|
self,
|
|
538
|
-
source: "
|
|
562
|
+
source: "Adapter",
|
|
539
563
|
diff_class: Type[Diff] = Diff,
|
|
540
564
|
flags: DiffSyncFlags = DiffSyncFlags.NONE,
|
|
541
565
|
callback: Optional[Callable[[StrType, int, int], None]] = None,
|
|
@@ -564,7 +588,13 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
564
588
|
# Generate the diff if an existing diff was not provided
|
|
565
589
|
if not diff:
|
|
566
590
|
diff = self.diff_from(source, diff_class=diff_class, flags=flags, callback=callback)
|
|
567
|
-
syncer = DiffSyncSyncer(
|
|
591
|
+
syncer = DiffSyncSyncer(
|
|
592
|
+
diff=diff,
|
|
593
|
+
src_diffsync=source,
|
|
594
|
+
dst_diffsync=self,
|
|
595
|
+
flags=flags,
|
|
596
|
+
callback=callback,
|
|
597
|
+
)
|
|
568
598
|
result = syncer.perform_sync()
|
|
569
599
|
if result:
|
|
570
600
|
self.sync_complete(source, diff, flags, syncer.base_logger)
|
|
@@ -573,7 +603,7 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
573
603
|
|
|
574
604
|
def sync_to( # pylint: disable=too-many-arguments
|
|
575
605
|
self,
|
|
576
|
-
target: "
|
|
606
|
+
target: "Adapter",
|
|
577
607
|
diff_class: Type[Diff] = Diff,
|
|
578
608
|
flags: DiffSyncFlags = DiffSyncFlags.NONE,
|
|
579
609
|
callback: Optional[Callable[[StrType, int, int], None]] = None,
|
|
@@ -597,7 +627,7 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
597
627
|
|
|
598
628
|
def sync_complete(
|
|
599
629
|
self,
|
|
600
|
-
source: "
|
|
630
|
+
source: "Adapter",
|
|
601
631
|
diff: Diff,
|
|
602
632
|
flags: DiffSyncFlags = DiffSyncFlags.NONE,
|
|
603
633
|
logger: Optional[structlog.BoundLogger] = None,
|
|
@@ -623,7 +653,7 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
623
653
|
|
|
624
654
|
def diff_from(
|
|
625
655
|
self,
|
|
626
|
-
source: "
|
|
656
|
+
source: "Adapter",
|
|
627
657
|
diff_class: Type[Diff] = Diff,
|
|
628
658
|
flags: DiffSyncFlags = DiffSyncFlags.NONE,
|
|
629
659
|
callback: Optional[Callable[[StrType, int, int], None]] = None,
|
|
@@ -638,13 +668,17 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
638
668
|
calculation of the diff proceeds.
|
|
639
669
|
"""
|
|
640
670
|
differ = DiffSyncDiffer(
|
|
641
|
-
src_diffsync=source,
|
|
671
|
+
src_diffsync=source,
|
|
672
|
+
dst_diffsync=self,
|
|
673
|
+
flags=flags,
|
|
674
|
+
diff_class=diff_class,
|
|
675
|
+
callback=callback,
|
|
642
676
|
)
|
|
643
677
|
return differ.calculate_diffs()
|
|
644
678
|
|
|
645
679
|
def diff_to(
|
|
646
680
|
self,
|
|
647
|
-
target: "
|
|
681
|
+
target: "Adapter",
|
|
648
682
|
diff_class: Type[Diff] = Diff,
|
|
649
683
|
flags: DiffSyncFlags = DiffSyncFlags.NONE,
|
|
650
684
|
callback: Optional[Callable[[StrType, int, int], None]] = None,
|
|
@@ -673,7 +707,9 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
673
707
|
return self.store.get_all_model_names()
|
|
674
708
|
|
|
675
709
|
def get(
|
|
676
|
-
self,
|
|
710
|
+
self,
|
|
711
|
+
obj: Union[StrType, DiffSyncModel, Type[DiffSyncModel]],
|
|
712
|
+
identifier: Union[StrType, Dict],
|
|
677
713
|
) -> DiffSyncModel:
|
|
678
714
|
"""Get one object from the data store based on its unique id.
|
|
679
715
|
|
|
@@ -688,7 +724,9 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
688
724
|
return self.store.get(model=obj, identifier=identifier)
|
|
689
725
|
|
|
690
726
|
def get_or_none(
|
|
691
|
-
self,
|
|
727
|
+
self,
|
|
728
|
+
obj: Union[StrType, DiffSyncModel, Type[DiffSyncModel]],
|
|
729
|
+
identifier: Union[StrType, Dict],
|
|
692
730
|
) -> Optional[DiffSyncModel]:
|
|
693
731
|
"""Get one object from the data store based on its unique id or get a None
|
|
694
732
|
|
|
@@ -719,7 +757,9 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
719
757
|
return self.store.get_all(model=obj)
|
|
720
758
|
|
|
721
759
|
def get_by_uids(
|
|
722
|
-
self,
|
|
760
|
+
self,
|
|
761
|
+
uids: List[StrType],
|
|
762
|
+
obj: Union[StrType, DiffSyncModel, Type[DiffSyncModel]],
|
|
723
763
|
) -> List[DiffSyncModel]:
|
|
724
764
|
"""Get multiple objects from the store by their unique IDs/Keys and type.
|
|
725
765
|
|
|
@@ -854,5 +894,15 @@ class DiffSync: # pylint: disable=too-many-public-methods
|
|
|
854
894
|
return self.store.count(model=model)
|
|
855
895
|
|
|
856
896
|
|
|
857
|
-
|
|
858
|
-
|
|
897
|
+
def DiffSync(*args: Any, **kwargs: Any) -> Adapter: # noqa pylint: disable=invalid-name
|
|
898
|
+
"""For backwards-compatibility, keep around the old name."""
|
|
899
|
+
|
|
900
|
+
warnings.warn(
|
|
901
|
+
"'diffsync.DiffSync' is deprecated and will be removed with 2.1, use 'diffsync.Adapter' instead.",
|
|
902
|
+
DeprecationWarning,
|
|
903
|
+
)
|
|
904
|
+
return Adapter(*args, **kwargs)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
# DiffSyncModel references Adapter and Adapter references DiffSyncModel. Break the typing loop:
|
|
908
|
+
DiffSyncModel.model_rebuild()
|
|
@@ -26,7 +26,7 @@ from .utils import intersection, symmetric_difference
|
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING: # pragma: no cover
|
|
28
28
|
# For type annotation purposes, we have a circular import loop between __init__.py and this file.
|
|
29
|
-
from . import
|
|
29
|
+
from . import Adapter, DiffSyncModel # pylint: disable=cyclic-import
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class DiffSyncDiffer: # pylint: disable=too-many-instance-attributes
|
|
@@ -37,8 +37,8 @@ class DiffSyncDiffer: # pylint: disable=too-many-instance-attributes
|
|
|
37
37
|
|
|
38
38
|
def __init__( # pylint: disable=too-many-arguments
|
|
39
39
|
self,
|
|
40
|
-
src_diffsync: "
|
|
41
|
-
dst_diffsync: "
|
|
40
|
+
src_diffsync: "Adapter",
|
|
41
|
+
dst_diffsync: "Adapter",
|
|
42
42
|
flags: DiffSyncFlags,
|
|
43
43
|
diff_class: Type[Diff] = Diff,
|
|
44
44
|
callback: Optional[Callable[[str, int, int], None]] = None,
|
|
@@ -288,8 +288,8 @@ class DiffSyncSyncer: # pylint: disable=too-many-instance-attributes
|
|
|
288
288
|
def __init__( # pylint: disable=too-many-arguments
|
|
289
289
|
self,
|
|
290
290
|
diff: Diff,
|
|
291
|
-
src_diffsync: "
|
|
292
|
-
dst_diffsync: "
|
|
291
|
+
src_diffsync: "Adapter",
|
|
292
|
+
dst_diffsync: "Adapter",
|
|
293
293
|
flags: DiffSyncFlags,
|
|
294
294
|
callback: Optional[Callable[[str, int, int], None]] = None,
|
|
295
295
|
):
|
|
@@ -369,11 +369,13 @@ class DiffSyncSyncer: # pylint: disable=too-many-instance-attributes
|
|
|
369
369
|
natural_deletion_order = bool(dst_model.model_flags & DiffSyncModelFlags.NATURAL_DELETION_ORDER)
|
|
370
370
|
skip_children = bool(dst_model.model_flags & DiffSyncModelFlags.SKIP_CHILDREN_ON_DELETE)
|
|
371
371
|
|
|
372
|
+
# Recurse through children to delete if we are supposed to delete the current diff element
|
|
372
373
|
changed = False
|
|
373
374
|
if natural_deletion_order and self.action == DiffSyncActions.DELETE and not skip_children:
|
|
374
375
|
for child in element.get_children():
|
|
375
376
|
changed |= self.sync_diff_element(child, parent_model=dst_model)
|
|
376
377
|
|
|
378
|
+
# Sync the current model - this will delete the current model if self.action is DELETE
|
|
377
379
|
changed, modified_model = self.sync_model(src_model=src_model, dst_model=dst_model, ids=ids, attrs=attrs)
|
|
378
380
|
dst_model = modified_model or dst_model
|
|
379
381
|
|
|
@@ -396,7 +398,7 @@ class DiffSyncSyncer: # pylint: disable=too-many-instance-attributes
|
|
|
396
398
|
|
|
397
399
|
self.incr_elements_processed()
|
|
398
400
|
|
|
399
|
-
if not natural_deletion_order:
|
|
401
|
+
if not natural_deletion_order or self.action is not DiffSyncActions.DELETE:
|
|
400
402
|
for child in element.get_children():
|
|
401
403
|
changed |= self.sync_diff_element(child, parent_model=dst_model)
|
|
402
404
|
|
|
@@ -423,7 +425,7 @@ class DiffSyncSyncer: # pylint: disable=too-many-instance-attributes
|
|
|
423
425
|
if self.action == DiffSyncActions.CREATE:
|
|
424
426
|
if dst_model is not None:
|
|
425
427
|
raise ObjectNotCreated(f"Failed to create {self.model_class.get_type()} {ids} - it already exists!")
|
|
426
|
-
dst_model = self.model_class.create(
|
|
428
|
+
dst_model = self.model_class.create(adapter=self.dst_diffsync, ids=ids, attrs=attrs)
|
|
427
429
|
elif self.action == DiffSyncActions.UPDATE:
|
|
428
430
|
if dst_model is None:
|
|
429
431
|
raise ObjectNotUpdated(f"Failed to update {self.model_class.get_type()} {ids} - not found!")
|
|
@@ -6,7 +6,7 @@ from diffsync.exceptions import ObjectNotFound
|
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from diffsync import DiffSyncModel
|
|
9
|
-
from diffsync import
|
|
9
|
+
from diffsync import Adapter
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class BaseStore:
|
|
@@ -15,12 +15,12 @@ class BaseStore:
|
|
|
15
15
|
def __init__(
|
|
16
16
|
self, # pylint: disable=unused-argument
|
|
17
17
|
*args: Any, # pylint: disable=unused-argument
|
|
18
|
-
|
|
18
|
+
adapter: Optional["Adapter"] = None,
|
|
19
19
|
name: str = "",
|
|
20
20
|
**kwargs: Any, # pylint: disable=unused-argument
|
|
21
21
|
) -> None:
|
|
22
22
|
"""Init method for BaseStore."""
|
|
23
|
-
self.
|
|
23
|
+
self.adapter = adapter
|
|
24
24
|
self.name = name or self.__class__.__name__
|
|
25
25
|
self._log = structlog.get_logger().new(store=self)
|
|
26
26
|
|
|
@@ -95,8 +95,8 @@ class BaseStore:
|
|
|
95
95
|
|
|
96
96
|
self.remove_item(modelname, uid)
|
|
97
97
|
|
|
98
|
-
if obj.
|
|
99
|
-
obj.
|
|
98
|
+
if obj.adapter:
|
|
99
|
+
obj.adapter = None
|
|
100
100
|
|
|
101
101
|
if remove_children:
|
|
102
102
|
for child_type, child_fieldname in obj.get_children_mapping().items():
|
|
@@ -243,9 +243,9 @@ class BaseStore:
|
|
|
243
243
|
"""Get object class and model name for a model."""
|
|
244
244
|
if isinstance(model, str):
|
|
245
245
|
modelname = model
|
|
246
|
-
if not hasattr(self.
|
|
246
|
+
if not hasattr(self.adapter, model):
|
|
247
247
|
return None, modelname
|
|
248
|
-
object_class = getattr(self.
|
|
248
|
+
object_class = getattr(self.adapter, model)
|
|
249
249
|
else:
|
|
250
250
|
object_class = model
|
|
251
251
|
modelname = model.get_type()
|
|
@@ -108,8 +108,8 @@ class LocalStore(BaseStore):
|
|
|
108
108
|
# Return so we don't have to change anything on the existing object and underlying data
|
|
109
109
|
return
|
|
110
110
|
|
|
111
|
-
if not obj.
|
|
112
|
-
obj.
|
|
111
|
+
if not obj.adapter:
|
|
112
|
+
obj.adapter = self.adapter
|
|
113
113
|
|
|
114
114
|
self._data[modelname][uid] = obj
|
|
115
115
|
|
|
@@ -65,7 +65,7 @@ class RedisStore(BaseStore):
|
|
|
65
65
|
pickled_object = self._store.get(key)
|
|
66
66
|
if pickled_object:
|
|
67
67
|
obj_result = loads(pickled_object) # nosec
|
|
68
|
-
obj_result.
|
|
68
|
+
obj_result.adapter = self.adapter
|
|
69
69
|
return obj_result
|
|
70
70
|
raise ObjectNotFound(f"{key} not present in Cache")
|
|
71
71
|
|
|
@@ -178,7 +178,7 @@ class RedisStore(BaseStore):
|
|
|
178
178
|
|
|
179
179
|
# Remove the diffsync object before sending to Redis
|
|
180
180
|
obj_copy = copy.copy(obj)
|
|
181
|
-
obj_copy.
|
|
181
|
+
obj_copy.adapter = None
|
|
182
182
|
|
|
183
183
|
self._store.set(object_key, dumps(obj_copy))
|
|
184
184
|
|
|
@@ -193,7 +193,7 @@ class RedisStore(BaseStore):
|
|
|
193
193
|
|
|
194
194
|
object_key = self._get_key_for_object(modelname, uid)
|
|
195
195
|
obj_copy = copy.copy(obj)
|
|
196
|
-
obj_copy.
|
|
196
|
+
obj_copy.adapter = None
|
|
197
197
|
|
|
198
198
|
self._store.set(object_key, dumps(obj_copy))
|
|
199
199
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "diffsync"
|
|
3
|
-
version = "
|
|
3
|
+
version = "v2.0.0"
|
|
4
4
|
description = "Library to easily sync/diff/update 2 different data sources"
|
|
5
5
|
authors = ["Network to Code, LLC <info@networktocode.com>"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -16,12 +16,14 @@ include = [
|
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[tool.poetry.dependencies]
|
|
19
|
-
python = "
|
|
20
|
-
pydantic = "^
|
|
19
|
+
python = ">=3.8,<4.0"
|
|
20
|
+
pydantic = "^2.0.0"
|
|
21
21
|
structlog = ">= 20.1.0, < 23.0.0"
|
|
22
22
|
packaging = ">= 21.3, < 24.0"
|
|
23
23
|
colorama = {version = "^0.4.3", optional = true}
|
|
24
24
|
redis = {version = "^4.3", optional = true}
|
|
25
|
+
# typing.Self introduced in 3.11
|
|
26
|
+
typing-extensions = { version = ">=4.0.1", python = "<3.11" }
|
|
25
27
|
|
|
26
28
|
[tool.poetry.extras]
|
|
27
29
|
redis = ["redis"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|