pygeodiff 2.0.4__cp39-cp39-macosx_10_9_x86_64.whl
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.
- pygeodiff/.dylibs/libsqlite3.0.dylib +0 -0
- pygeodiff/__about__.py +10 -0
- pygeodiff/__init__.py +19 -0
- pygeodiff/geodifflib.py +718 -0
- pygeodiff/libpygeodiff-2.0.4-python.dylib +0 -0
- pygeodiff/main.py +390 -0
- pygeodiff-2.0.4.dist-info/LICENSE +21 -0
- pygeodiff-2.0.4.dist-info/METADATA +14 -0
- pygeodiff-2.0.4.dist-info/RECORD +12 -0
- pygeodiff-2.0.4.dist-info/WHEEL +5 -0
- pygeodiff-2.0.4.dist-info/entry_points.txt +2 -0
- pygeodiff-2.0.4.dist-info/top_level.txt +1 -0
|
Binary file
|
pygeodiff/__about__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
__title__ = "PyGeoDiff"
|
|
2
|
+
__description__ = "Diff tool for geo-spatial data"
|
|
3
|
+
__url__ = "https://github.com/MerginMaps/geodiff"
|
|
4
|
+
# use scripts/update_version.py to update the version here and in other places at once
|
|
5
|
+
__version__ = "2.0.4"
|
|
6
|
+
__author__ = "Lutra Consulting Ltd."
|
|
7
|
+
__author_email__ = "info@merginmaps.com"
|
|
8
|
+
__maintainer__ = "Lutra Consulting Ltd."
|
|
9
|
+
__license__ = "MIT"
|
|
10
|
+
__copyright__ = "(c) 2019-2022 Lutra Consulting Ltd."
|
pygeodiff/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
pygeodiff
|
|
4
|
+
-----------
|
|
5
|
+
This module provides tools for create diffs of geospatial data formats
|
|
6
|
+
:copyright: (c) 2019-2022 Lutra Consulting Ltd.
|
|
7
|
+
:license: MIT, see LICENSE for more details.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .main import GeoDiff
|
|
11
|
+
from .geodifflib import (
|
|
12
|
+
GeoDiffLibError,
|
|
13
|
+
GeoDiffLibConflictError,
|
|
14
|
+
GeoDiffLibUnsupportedChangeError,
|
|
15
|
+
GeoDiffLibVersionError,
|
|
16
|
+
ChangesetEntry,
|
|
17
|
+
ChangesetReader,
|
|
18
|
+
UndefinedValue,
|
|
19
|
+
)
|
pygeodiff/geodifflib.py
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
pygeodiff.geodifflib
|
|
4
|
+
--------------------
|
|
5
|
+
This module provides wrapper of geodiff C library
|
|
6
|
+
:copyright: (c) 2019-2022 Lutra Consulting Ltd.
|
|
7
|
+
:license: MIT, see LICENSE for more details.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ctypes
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
from ctypes.util import find_library
|
|
14
|
+
from .__about__ import __version__
|
|
15
|
+
import copy
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GeoDiffLibError(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GeoDiffLibConflictError(GeoDiffLibError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GeoDiffLibUnsupportedChangeError(GeoDiffLibError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GeoDiffLibVersionError(GeoDiffLibError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# keep in sync with c-library
|
|
35
|
+
SUCCESS = 0
|
|
36
|
+
ERROR = 1
|
|
37
|
+
CONFLICT = 2
|
|
38
|
+
UNSUPPORTED_CHANGE = 3
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_return_code(rc, msg):
|
|
42
|
+
if rc == SUCCESS:
|
|
43
|
+
return
|
|
44
|
+
elif rc == ERROR:
|
|
45
|
+
raise GeoDiffLibError(msg)
|
|
46
|
+
elif rc == CONFLICT:
|
|
47
|
+
raise GeoDiffLibConflictError(msg)
|
|
48
|
+
elif rc == UNSUPPORTED_CHANGE:
|
|
49
|
+
raise GeoDiffLibUnsupportedChangeError(msg)
|
|
50
|
+
else:
|
|
51
|
+
raise GeoDiffLibVersionError(
|
|
52
|
+
"Internal error (enum " + str(rc) + " not handled)"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GeoDiffLib:
|
|
57
|
+
def __init__(self, name):
|
|
58
|
+
self.context = None
|
|
59
|
+
if name is None:
|
|
60
|
+
self.libname = self.package_libname()
|
|
61
|
+
if not os.path.exists(self.libname):
|
|
62
|
+
# not found, try system library
|
|
63
|
+
self.libname = find_library("geodiff")
|
|
64
|
+
else:
|
|
65
|
+
self.libname = name
|
|
66
|
+
|
|
67
|
+
if self.libname is None:
|
|
68
|
+
raise GeoDiffLibVersionError(
|
|
69
|
+
"Unable to locate GeoDiff library, tried "
|
|
70
|
+
+ self.package_libname()
|
|
71
|
+
+ " and geodiff on system."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
self.lib = ctypes.CDLL(self.libname, use_errno=True)
|
|
76
|
+
except OSError:
|
|
77
|
+
raise GeoDiffLibVersionError(
|
|
78
|
+
"Unable to load geodiff library " + self.libname
|
|
79
|
+
)
|
|
80
|
+
self.context = self.init()
|
|
81
|
+
self.callbackLogger = None
|
|
82
|
+
if self.context is None:
|
|
83
|
+
raise GeoDiffLibVersionError("Unable to create GeoDiff context")
|
|
84
|
+
|
|
85
|
+
self.check_version()
|
|
86
|
+
self._register_functions()
|
|
87
|
+
|
|
88
|
+
def __del__(self):
|
|
89
|
+
if self.context is not None:
|
|
90
|
+
func = self.lib.GEODIFF_CX_destroy
|
|
91
|
+
func.argtypes = [ctypes.c_void_p]
|
|
92
|
+
func(self.context)
|
|
93
|
+
self.context = None
|
|
94
|
+
|
|
95
|
+
def _register_functions(self):
|
|
96
|
+
self._readChangeset = self.lib.GEODIFF_readChangeset
|
|
97
|
+
self._readChangeset.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
98
|
+
self._readChangeset.restype = ctypes.c_void_p
|
|
99
|
+
|
|
100
|
+
# ChangesetReader
|
|
101
|
+
self._CR_nextEntry = self.lib.GEODIFF_CR_nextEntry
|
|
102
|
+
self._CR_nextEntry.argtypes = [
|
|
103
|
+
ctypes.c_void_p,
|
|
104
|
+
ctypes.c_void_p,
|
|
105
|
+
ctypes.c_void_p,
|
|
106
|
+
]
|
|
107
|
+
self._CR_nextEntry.restype = ctypes.c_void_p
|
|
108
|
+
|
|
109
|
+
self._CR_destroy = self.lib.GEODIFF_CR_destroy
|
|
110
|
+
self._CR_destroy.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
111
|
+
|
|
112
|
+
# ChangesetEntry
|
|
113
|
+
self._CE_operation = self.lib.GEODIFF_CE_operation
|
|
114
|
+
self._CE_operation.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
115
|
+
self._CE_operation.restype = ctypes.c_int
|
|
116
|
+
|
|
117
|
+
self._CE_table = self.lib.GEODIFF_CE_table
|
|
118
|
+
self._CE_table.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
119
|
+
self._CE_table.restype = ctypes.c_void_p
|
|
120
|
+
|
|
121
|
+
self._CE_count = self.lib.GEODIFF_CE_countValues
|
|
122
|
+
self._CE_count.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
123
|
+
self._CE_count.restype = ctypes.c_int
|
|
124
|
+
|
|
125
|
+
self._CE_old_value = self.lib.GEODIFF_CE_oldValue
|
|
126
|
+
self._CE_old_value.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int]
|
|
127
|
+
self._CE_old_value.restype = ctypes.c_void_p
|
|
128
|
+
|
|
129
|
+
self._CE_new_value = self.lib.GEODIFF_CE_newValue
|
|
130
|
+
self._CE_new_value.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int]
|
|
131
|
+
self._CE_new_value.restype = ctypes.c_void_p
|
|
132
|
+
|
|
133
|
+
self._CE_destroy = self.lib.GEODIFF_CE_destroy
|
|
134
|
+
self._CE_destroy.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
135
|
+
|
|
136
|
+
# ChangesetTable
|
|
137
|
+
self._CT_name = self.lib.GEODIFF_CT_name
|
|
138
|
+
self._CT_name.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
139
|
+
self._CT_name.restype = ctypes.c_char_p
|
|
140
|
+
|
|
141
|
+
self._CT_column_count = self.lib.GEODIFF_CT_columnCount
|
|
142
|
+
self._CT_column_count.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
143
|
+
self._CT_column_count.restype = ctypes.c_int
|
|
144
|
+
|
|
145
|
+
self._CT_column_is_pkey = self.lib.GEODIFF_CT_columnIsPkey
|
|
146
|
+
self._CT_column_is_pkey.argtypes = [
|
|
147
|
+
ctypes.c_void_p,
|
|
148
|
+
ctypes.c_void_p,
|
|
149
|
+
ctypes.c_int,
|
|
150
|
+
]
|
|
151
|
+
self._CT_column_is_pkey.restype = ctypes.c_bool
|
|
152
|
+
|
|
153
|
+
# Value
|
|
154
|
+
self._V_type = self.lib.GEODIFF_V_type
|
|
155
|
+
self._V_type.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
156
|
+
self._V_type.restype = ctypes.c_int
|
|
157
|
+
|
|
158
|
+
self._V_get_int = self.lib.GEODIFF_V_getInt
|
|
159
|
+
self._V_get_int.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
160
|
+
self._V_get_int.restype = ctypes.c_int
|
|
161
|
+
|
|
162
|
+
self._V_get_double = self.lib.GEODIFF_V_getDouble
|
|
163
|
+
self._V_get_double.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
164
|
+
self._V_get_double.restype = ctypes.c_double
|
|
165
|
+
|
|
166
|
+
self._V_get_data_size = self.lib.GEODIFF_V_getDataSize
|
|
167
|
+
self._V_get_data_size.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
168
|
+
self._V_get_data_size.restype = ctypes.c_int
|
|
169
|
+
|
|
170
|
+
self._V_get_data = self.lib.GEODIFF_V_getData
|
|
171
|
+
self._V_get_data.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p]
|
|
172
|
+
|
|
173
|
+
self._V_destroy = self.lib.GEODIFF_V_destroy
|
|
174
|
+
self._V_destroy.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
175
|
+
|
|
176
|
+
def package_libname(self):
|
|
177
|
+
# assume that the package is installed through PIP
|
|
178
|
+
if platform.system() == "Windows":
|
|
179
|
+
prefix = ""
|
|
180
|
+
arch = platform.architecture()[0] # 64bit or 32bit
|
|
181
|
+
if "32" in arch:
|
|
182
|
+
suffix = "-win32.pyd"
|
|
183
|
+
else:
|
|
184
|
+
suffix = ".pyd"
|
|
185
|
+
elif platform.system() == "Darwin":
|
|
186
|
+
prefix = "lib"
|
|
187
|
+
suffix = ".dylib"
|
|
188
|
+
else:
|
|
189
|
+
prefix = "lib"
|
|
190
|
+
suffix = ".so"
|
|
191
|
+
whl_lib = prefix + "pygeodiff-" + __version__ + "-python" + suffix
|
|
192
|
+
dir_path = os.path.dirname(os.path.realpath(__file__))
|
|
193
|
+
return os.path.join(dir_path, whl_lib)
|
|
194
|
+
|
|
195
|
+
def init(self):
|
|
196
|
+
func = self.lib.GEODIFF_createContext
|
|
197
|
+
func.restype = ctypes.c_void_p
|
|
198
|
+
return func()
|
|
199
|
+
|
|
200
|
+
def set_logger_callback(self, callback):
|
|
201
|
+
func = self.lib.GEODIFF_CX_setLoggerCallback
|
|
202
|
+
cFuncType = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
|
|
203
|
+
func.argtypes = [ctypes.c_void_p, cFuncType]
|
|
204
|
+
if callback:
|
|
205
|
+
# do not remove self, callback needs to be member
|
|
206
|
+
self.callbackLogger = cFuncType(callback)
|
|
207
|
+
else:
|
|
208
|
+
self.callbackLogger = cFuncType()
|
|
209
|
+
func(self.context, self.callbackLogger)
|
|
210
|
+
|
|
211
|
+
def set_maximum_logger_level(self, maxLevel):
|
|
212
|
+
func = self.lib.GEODIFF_CX_setMaximumLoggerLevel
|
|
213
|
+
func.argtypes = [ctypes.c_void_p, ctypes.c_int]
|
|
214
|
+
func(self.context, maxLevel)
|
|
215
|
+
|
|
216
|
+
def set_tables_to_skip(self, tables):
|
|
217
|
+
# make array of char* with utf-8 encoding from python list of strings
|
|
218
|
+
arr = (ctypes.c_char_p * len(tables))()
|
|
219
|
+
for i in range(len(tables)):
|
|
220
|
+
arr[i] = tables[i].encode("utf-8")
|
|
221
|
+
|
|
222
|
+
self.lib.GEODIFF_CX_setTablesToSkip(
|
|
223
|
+
ctypes.c_void_p(self.context), ctypes.c_int(len(tables)), arr
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def version(self):
|
|
227
|
+
func = self.lib.GEODIFF_version
|
|
228
|
+
func.restype = ctypes.c_char_p
|
|
229
|
+
ver = func()
|
|
230
|
+
return ver.decode("utf-8")
|
|
231
|
+
|
|
232
|
+
def check_version(self):
|
|
233
|
+
cversion = self.version()
|
|
234
|
+
pyversion = __version__
|
|
235
|
+
if cversion != pyversion:
|
|
236
|
+
raise GeoDiffLibVersionError(
|
|
237
|
+
"version mismatch ({} C vs {} PY)".format(cversion, pyversion)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def drivers(self):
|
|
241
|
+
_driver_count_f = self.lib.GEODIFF_driverCount
|
|
242
|
+
_driver_count_f.argtypes = [ctypes.c_void_p]
|
|
243
|
+
_driver_count_f.restype = ctypes.c_int
|
|
244
|
+
|
|
245
|
+
_driver_name_from_index_f = self.lib.GEODIFF_driverNameFromIndex
|
|
246
|
+
_driver_name_from_index_f.argtypes = [
|
|
247
|
+
ctypes.c_void_p,
|
|
248
|
+
ctypes.c_int,
|
|
249
|
+
ctypes.c_char_p,
|
|
250
|
+
]
|
|
251
|
+
_driver_name_from_index_f.restype = ctypes.c_int
|
|
252
|
+
|
|
253
|
+
drivers_list = []
|
|
254
|
+
driversCount = _driver_count_f(self.context)
|
|
255
|
+
for index in range(driversCount):
|
|
256
|
+
name_raw = 256 * ""
|
|
257
|
+
b_string1 = name_raw.encode("utf-8")
|
|
258
|
+
res = _driver_name_from_index_f(self.context, index, b_string1)
|
|
259
|
+
_parse_return_code(res, "drivers")
|
|
260
|
+
name = b_string1.decode("utf-8")
|
|
261
|
+
drivers_list.append(name)
|
|
262
|
+
|
|
263
|
+
return drivers_list
|
|
264
|
+
|
|
265
|
+
def driver_is_registered(self, name):
|
|
266
|
+
func = self.lib.GEODIFF_driverIsRegistered
|
|
267
|
+
func.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
268
|
+
func.restype = ctypes.c_bool
|
|
269
|
+
|
|
270
|
+
b_string1 = name.encode("utf-8")
|
|
271
|
+
return func(self.context, b_string1)
|
|
272
|
+
|
|
273
|
+
def create_changeset(self, base, modified, changeset):
|
|
274
|
+
func = self.lib.GEODIFF_createChangeset
|
|
275
|
+
func.argtypes = [
|
|
276
|
+
ctypes.c_void_p,
|
|
277
|
+
ctypes.c_char_p,
|
|
278
|
+
ctypes.c_char_p,
|
|
279
|
+
ctypes.c_char_p,
|
|
280
|
+
]
|
|
281
|
+
func.restype = ctypes.c_int
|
|
282
|
+
|
|
283
|
+
# create byte objects from the strings
|
|
284
|
+
b_string1 = base.encode("utf-8")
|
|
285
|
+
b_string2 = modified.encode("utf-8")
|
|
286
|
+
b_string3 = changeset.encode("utf-8")
|
|
287
|
+
|
|
288
|
+
res = func(self.context, b_string1, b_string2, b_string3)
|
|
289
|
+
_parse_return_code(res, "createChangeset")
|
|
290
|
+
|
|
291
|
+
def invert_changeset(self, changeset, changeset_inv):
|
|
292
|
+
func = self.lib.GEODIFF_invertChangeset
|
|
293
|
+
func.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
|
|
294
|
+
func.restype = ctypes.c_int
|
|
295
|
+
|
|
296
|
+
# create byte objects from the strings
|
|
297
|
+
b_string1 = changeset.encode("utf-8")
|
|
298
|
+
b_string2 = changeset_inv.encode("utf-8")
|
|
299
|
+
|
|
300
|
+
res = func(self.context, b_string1, b_string2)
|
|
301
|
+
_parse_return_code(res, "invert_changeset")
|
|
302
|
+
|
|
303
|
+
def create_rebased_changeset(
|
|
304
|
+
self, base, modified, changeset_their, changeset, conflict
|
|
305
|
+
):
|
|
306
|
+
func = self.lib.GEODIFF_createRebasedChangeset
|
|
307
|
+
func.argtypes = [
|
|
308
|
+
ctypes.c_void_p,
|
|
309
|
+
ctypes.c_char_p,
|
|
310
|
+
ctypes.c_char_p,
|
|
311
|
+
ctypes.c_char_p,
|
|
312
|
+
ctypes.c_char_p,
|
|
313
|
+
ctypes.c_char_p,
|
|
314
|
+
]
|
|
315
|
+
func.restype = ctypes.c_int
|
|
316
|
+
|
|
317
|
+
# create byte objects from the strings
|
|
318
|
+
b_string1 = base.encode("utf-8")
|
|
319
|
+
b_string2 = modified.encode("utf-8")
|
|
320
|
+
b_string3 = changeset_their.encode("utf-8")
|
|
321
|
+
b_string4 = changeset.encode("utf-8")
|
|
322
|
+
b_string5 = conflict.encode("utf-8")
|
|
323
|
+
|
|
324
|
+
res = func(self.context, b_string1, b_string2, b_string3, b_string4, b_string5)
|
|
325
|
+
_parse_return_code(res, "createRebasedChangeset")
|
|
326
|
+
|
|
327
|
+
def rebase(self, base, modified_their, modified, conflict):
|
|
328
|
+
func = self.lib.GEODIFF_rebase
|
|
329
|
+
func.argtypes = [
|
|
330
|
+
ctypes.c_void_p,
|
|
331
|
+
ctypes.c_char_p,
|
|
332
|
+
ctypes.c_char_p,
|
|
333
|
+
ctypes.c_char_p,
|
|
334
|
+
ctypes.c_char_p,
|
|
335
|
+
]
|
|
336
|
+
func.restype = ctypes.c_int
|
|
337
|
+
|
|
338
|
+
# create byte objects from the strings
|
|
339
|
+
b_string1 = base.encode("utf-8")
|
|
340
|
+
b_string2 = modified_their.encode("utf-8")
|
|
341
|
+
b_string3 = modified.encode("utf-8")
|
|
342
|
+
b_string4 = conflict.encode("utf-8")
|
|
343
|
+
res = func(self.context, b_string1, b_string2, b_string3, b_string4)
|
|
344
|
+
_parse_return_code(res, "rebase")
|
|
345
|
+
|
|
346
|
+
def apply_changeset(self, base, changeset):
|
|
347
|
+
func = self.lib.GEODIFF_applyChangeset
|
|
348
|
+
func.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
|
|
349
|
+
func.restype = ctypes.c_int
|
|
350
|
+
|
|
351
|
+
# create byte objects from the strings
|
|
352
|
+
b_string1 = base.encode("utf-8")
|
|
353
|
+
b_string2 = changeset.encode("utf-8")
|
|
354
|
+
|
|
355
|
+
res = func(self.context, b_string1, b_string2)
|
|
356
|
+
_parse_return_code(res, "apply_changeset")
|
|
357
|
+
|
|
358
|
+
def list_changes(self, changeset, result):
|
|
359
|
+
func = self.lib.GEODIFF_listChanges
|
|
360
|
+
func.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
361
|
+
func.restype = ctypes.c_int
|
|
362
|
+
|
|
363
|
+
# create byte objects from the strings
|
|
364
|
+
b_string1 = changeset.encode("utf-8")
|
|
365
|
+
b_string2 = result.encode("utf-8")
|
|
366
|
+
res = func(self.context, b_string1, b_string2)
|
|
367
|
+
_parse_return_code(res, "list_changes")
|
|
368
|
+
|
|
369
|
+
def list_changes_summary(self, changeset, result):
|
|
370
|
+
func = self.lib.GEODIFF_listChangesSummary
|
|
371
|
+
func.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
372
|
+
func.restype = ctypes.c_int
|
|
373
|
+
|
|
374
|
+
# create byte objects from the strings
|
|
375
|
+
b_string1 = changeset.encode("utf-8")
|
|
376
|
+
b_string2 = result.encode("utf-8")
|
|
377
|
+
res = func(self.context, b_string1, b_string2)
|
|
378
|
+
_parse_return_code(res, "list_changes_summary")
|
|
379
|
+
|
|
380
|
+
def has_changes(self, changeset):
|
|
381
|
+
func = self.lib.GEODIFF_hasChanges
|
|
382
|
+
func.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
383
|
+
func.restype = ctypes.c_int
|
|
384
|
+
|
|
385
|
+
# create byte objects from the strings
|
|
386
|
+
b_string1 = changeset.encode("utf-8")
|
|
387
|
+
|
|
388
|
+
nchanges = func(self.context, b_string1)
|
|
389
|
+
if nchanges < 0:
|
|
390
|
+
raise GeoDiffLibError("has_changes")
|
|
391
|
+
return nchanges == 1
|
|
392
|
+
|
|
393
|
+
def changes_count(self, changeset):
|
|
394
|
+
func = self.lib.GEODIFF_changesCount
|
|
395
|
+
func.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
|
396
|
+
func.restype = ctypes.c_int
|
|
397
|
+
|
|
398
|
+
# create byte objects from the strings
|
|
399
|
+
b_string1 = changeset.encode("utf-8")
|
|
400
|
+
|
|
401
|
+
nchanges = func(self.context, b_string1)
|
|
402
|
+
if nchanges < 0:
|
|
403
|
+
raise GeoDiffLibError("changes_count")
|
|
404
|
+
return nchanges
|
|
405
|
+
|
|
406
|
+
def concat_changes(self, list_changesets, output_changeset):
|
|
407
|
+
# make array of char* with utf-8 encoding from python list of strings
|
|
408
|
+
arr = (ctypes.c_char_p * len(list_changesets))()
|
|
409
|
+
for i in range(len(list_changesets)):
|
|
410
|
+
arr[i] = list_changesets[i].encode("utf-8")
|
|
411
|
+
|
|
412
|
+
res = self.lib.GEODIFF_concatChanges(
|
|
413
|
+
ctypes.c_void_p(self.context),
|
|
414
|
+
ctypes.c_int(len(list_changesets)),
|
|
415
|
+
arr,
|
|
416
|
+
ctypes.c_char_p(output_changeset.encode("utf-8")),
|
|
417
|
+
)
|
|
418
|
+
_parse_return_code(res, "concat_changes")
|
|
419
|
+
|
|
420
|
+
def make_copy(
|
|
421
|
+
self, driver_src, driver_src_info, src, driver_dst, driver_dst_info, dst
|
|
422
|
+
):
|
|
423
|
+
res = self.lib.GEODIFF_makeCopy(
|
|
424
|
+
ctypes.c_void_p(self.context),
|
|
425
|
+
ctypes.c_char_p(driver_src.encode("utf-8")),
|
|
426
|
+
ctypes.c_char_p(driver_src_info.encode("utf-8")),
|
|
427
|
+
ctypes.c_char_p(src.encode("utf-8")),
|
|
428
|
+
ctypes.c_char_p(driver_dst.encode("utf-8")),
|
|
429
|
+
ctypes.c_char_p(driver_dst_info.encode("utf-8")),
|
|
430
|
+
ctypes.c_char_p(dst.encode("utf-8")),
|
|
431
|
+
)
|
|
432
|
+
_parse_return_code(res, "make_copy")
|
|
433
|
+
|
|
434
|
+
def make_copy_sqlite(self, src, dst):
|
|
435
|
+
res = self.lib.GEODIFF_makeCopySqlite(
|
|
436
|
+
ctypes.c_void_p(self.context),
|
|
437
|
+
ctypes.c_char_p(src.encode("utf-8")),
|
|
438
|
+
ctypes.c_char_p(dst.encode("utf-8")),
|
|
439
|
+
)
|
|
440
|
+
_parse_return_code(res, "make_copy_sqlite")
|
|
441
|
+
|
|
442
|
+
def create_changeset_ex(self, driver, driver_info, base, modified, changeset):
|
|
443
|
+
res = self.lib.GEODIFF_createChangesetEx(
|
|
444
|
+
ctypes.c_void_p(self.context),
|
|
445
|
+
ctypes.c_char_p(driver.encode("utf-8")),
|
|
446
|
+
ctypes.c_char_p(driver_info.encode("utf-8")),
|
|
447
|
+
ctypes.c_char_p(base.encode("utf-8")),
|
|
448
|
+
ctypes.c_char_p(modified.encode("utf-8")),
|
|
449
|
+
ctypes.c_char_p(changeset.encode("utf-8")),
|
|
450
|
+
)
|
|
451
|
+
_parse_return_code(res, "create_changeset_ex")
|
|
452
|
+
|
|
453
|
+
def create_changeset_dr(
|
|
454
|
+
self,
|
|
455
|
+
driver_src,
|
|
456
|
+
driver_src_info,
|
|
457
|
+
src,
|
|
458
|
+
driver_dst,
|
|
459
|
+
driver_dst_info,
|
|
460
|
+
dst,
|
|
461
|
+
changeset,
|
|
462
|
+
):
|
|
463
|
+
func = self.lib.GEODIFF_createChangesetDr
|
|
464
|
+
func.argtypes = [
|
|
465
|
+
ctypes.c_void_p,
|
|
466
|
+
ctypes.c_char_p,
|
|
467
|
+
ctypes.c_char_p,
|
|
468
|
+
ctypes.c_char_p,
|
|
469
|
+
ctypes.c_char_p,
|
|
470
|
+
ctypes.c_char_p,
|
|
471
|
+
ctypes.c_char_p,
|
|
472
|
+
ctypes.c_char_p,
|
|
473
|
+
]
|
|
474
|
+
func.restype = ctypes.c_int
|
|
475
|
+
|
|
476
|
+
b_string1 = driver_src.encode("utf-8")
|
|
477
|
+
b_string2 = driver_src_info.encode("utf-8")
|
|
478
|
+
b_string3 = src.encode("utf-8")
|
|
479
|
+
b_string4 = driver_dst.encode("utf-8")
|
|
480
|
+
b_string5 = driver_dst_info.encode("utf-8")
|
|
481
|
+
b_string6 = dst.encode("utf-8")
|
|
482
|
+
b_string7 = changeset.encode("utf-8")
|
|
483
|
+
|
|
484
|
+
res = func(
|
|
485
|
+
self.context,
|
|
486
|
+
b_string1,
|
|
487
|
+
b_string2,
|
|
488
|
+
b_string3,
|
|
489
|
+
b_string4,
|
|
490
|
+
b_string5,
|
|
491
|
+
b_string6,
|
|
492
|
+
b_string7,
|
|
493
|
+
)
|
|
494
|
+
_parse_return_code(res, "CreateChangesetDr")
|
|
495
|
+
|
|
496
|
+
def apply_changeset_ex(self, driver, driver_info, base, changeset):
|
|
497
|
+
res = self.lib.GEODIFF_applyChangesetEx(
|
|
498
|
+
ctypes.c_void_p(self.context),
|
|
499
|
+
ctypes.c_char_p(driver.encode("utf-8")),
|
|
500
|
+
ctypes.c_char_p(driver_info.encode("utf-8")),
|
|
501
|
+
ctypes.c_char_p(base.encode("utf-8")),
|
|
502
|
+
ctypes.c_char_p(changeset.encode("utf-8")),
|
|
503
|
+
)
|
|
504
|
+
_parse_return_code(res, "apply_changeset_ex")
|
|
505
|
+
|
|
506
|
+
def create_rebased_changeset_ex(
|
|
507
|
+
self,
|
|
508
|
+
driver,
|
|
509
|
+
driver_info,
|
|
510
|
+
base,
|
|
511
|
+
base2modified,
|
|
512
|
+
base2their,
|
|
513
|
+
rebased,
|
|
514
|
+
conflict_file,
|
|
515
|
+
):
|
|
516
|
+
res = self.lib.GEODIFF_createRebasedChangesetEx(
|
|
517
|
+
ctypes.c_void_p(self.context),
|
|
518
|
+
ctypes.c_char_p(driver.encode("utf-8")),
|
|
519
|
+
ctypes.c_char_p(driver_info.encode("utf-8")),
|
|
520
|
+
ctypes.c_char_p(base.encode("utf-8")),
|
|
521
|
+
ctypes.c_char_p(base2modified.encode("utf-8")),
|
|
522
|
+
ctypes.c_char_p(base2their.encode("utf-8")),
|
|
523
|
+
ctypes.c_char_p(rebased.encode("utf-8")),
|
|
524
|
+
ctypes.c_char_p(conflict_file.encode("utf-8")),
|
|
525
|
+
)
|
|
526
|
+
_parse_return_code(res, "create_rebased_changeset_ex")
|
|
527
|
+
|
|
528
|
+
def rebase_ex(self, driver, driver_info, base, modified, base2their, conflict_file):
|
|
529
|
+
res = self.lib.GEODIFF_rebaseEx(
|
|
530
|
+
ctypes.c_void_p(self.context),
|
|
531
|
+
ctypes.c_char_p(driver.encode("utf-8")),
|
|
532
|
+
ctypes.c_char_p(driver_info.encode("utf-8")),
|
|
533
|
+
ctypes.c_char_p(base.encode("utf-8")),
|
|
534
|
+
ctypes.c_char_p(modified.encode("utf-8")),
|
|
535
|
+
ctypes.c_char_p(base2their.encode("utf-8")),
|
|
536
|
+
ctypes.c_char_p(conflict_file.encode("utf-8")),
|
|
537
|
+
)
|
|
538
|
+
_parse_return_code(res, "rebase_ex")
|
|
539
|
+
|
|
540
|
+
def dump_data(self, driver, driver_info, src, changeset):
|
|
541
|
+
res = self.lib.GEODIFF_dumpData(
|
|
542
|
+
ctypes.c_void_p(self.context),
|
|
543
|
+
ctypes.c_char_p(driver.encode("utf-8")),
|
|
544
|
+
ctypes.c_char_p(driver_info.encode("utf-8")),
|
|
545
|
+
ctypes.c_char_p(src.encode("utf-8")),
|
|
546
|
+
ctypes.c_char_p(changeset.encode("utf-8")),
|
|
547
|
+
)
|
|
548
|
+
_parse_return_code(res, "dump_data")
|
|
549
|
+
|
|
550
|
+
def schema(self, driver, driver_info, src, json):
|
|
551
|
+
res = self.lib.GEODIFF_schema(
|
|
552
|
+
ctypes.c_void_p(self.context),
|
|
553
|
+
ctypes.c_char_p(driver.encode("utf-8")),
|
|
554
|
+
ctypes.c_char_p(driver_info.encode("utf-8")),
|
|
555
|
+
ctypes.c_char_p(src.encode("utf-8")),
|
|
556
|
+
ctypes.c_char_p(json.encode("utf-8")),
|
|
557
|
+
)
|
|
558
|
+
_parse_return_code(res, "schema")
|
|
559
|
+
|
|
560
|
+
def read_changeset(self, changeset):
|
|
561
|
+
|
|
562
|
+
b_string1 = changeset.encode("utf-8")
|
|
563
|
+
|
|
564
|
+
reader_ptr = self._readChangeset(self.context, b_string1)
|
|
565
|
+
if reader_ptr is None:
|
|
566
|
+
raise GeoDiffLibError("Unable to open reader for: " + changeset)
|
|
567
|
+
return ChangesetReader(self, reader_ptr)
|
|
568
|
+
|
|
569
|
+
def create_wkb_from_gpkg_header(self, geometry):
|
|
570
|
+
func = self.lib.GEODIFF_createWkbFromGpkgHeader
|
|
571
|
+
func.argtypes = [
|
|
572
|
+
ctypes.c_void_p,
|
|
573
|
+
ctypes.POINTER(ctypes.c_char),
|
|
574
|
+
ctypes.c_size_t,
|
|
575
|
+
ctypes.POINTER(ctypes.POINTER(ctypes.c_char)),
|
|
576
|
+
ctypes.POINTER(ctypes.c_size_t),
|
|
577
|
+
]
|
|
578
|
+
func.restype = ctypes.c_int
|
|
579
|
+
|
|
580
|
+
out = ctypes.POINTER(ctypes.c_char)()
|
|
581
|
+
out_size = ctypes.c_size_t(len(geometry))
|
|
582
|
+
res = func(
|
|
583
|
+
self.context,
|
|
584
|
+
geometry,
|
|
585
|
+
ctypes.c_size_t(len(geometry)),
|
|
586
|
+
ctypes.byref(out),
|
|
587
|
+
ctypes.byref(out_size),
|
|
588
|
+
)
|
|
589
|
+
_parse_return_code(res, "create_wkb_from_gpkg_header")
|
|
590
|
+
wkb = copy.deepcopy(out[: out_size.value])
|
|
591
|
+
return wkb
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class ChangesetReader(object):
|
|
595
|
+
"""Wrapper around GEODIFF_CR_* functions from C API"""
|
|
596
|
+
|
|
597
|
+
def __init__(self, geodiff, reader_ptr):
|
|
598
|
+
self.geodiff = geodiff
|
|
599
|
+
self.reader_ptr = reader_ptr
|
|
600
|
+
|
|
601
|
+
def __del__(self):
|
|
602
|
+
self.geodiff._CR_destroy(self.geodiff.context, self.reader_ptr)
|
|
603
|
+
|
|
604
|
+
def next_entry(self):
|
|
605
|
+
ok = ctypes.c_bool()
|
|
606
|
+
entry_ptr = self.geodiff._CR_nextEntry(
|
|
607
|
+
self.geodiff.context, self.reader_ptr, ctypes.byref(ok)
|
|
608
|
+
)
|
|
609
|
+
if not ok:
|
|
610
|
+
raise GeoDiffLibError("Failed to read entry!")
|
|
611
|
+
if entry_ptr is not None:
|
|
612
|
+
return ChangesetEntry(self.geodiff, entry_ptr)
|
|
613
|
+
else:
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
def __iter__(self):
|
|
617
|
+
return self
|
|
618
|
+
|
|
619
|
+
def __next__(self):
|
|
620
|
+
entry = self.next_entry()
|
|
621
|
+
if entry is not None:
|
|
622
|
+
return entry
|
|
623
|
+
else:
|
|
624
|
+
raise StopIteration
|
|
625
|
+
|
|
626
|
+
next = __next__ # python 2.x compatibility (requires next())
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class ChangesetEntry(object):
|
|
630
|
+
"""Wrapper around GEODIFF_CE_* functions from C API"""
|
|
631
|
+
|
|
632
|
+
# constants as defined in ChangesetEntry::OperationType enum
|
|
633
|
+
OP_INSERT = 18
|
|
634
|
+
OP_UPDATE = 23
|
|
635
|
+
OP_DELETE = 9
|
|
636
|
+
|
|
637
|
+
def __init__(self, geodiff, entry_ptr):
|
|
638
|
+
self.geodiff = geodiff
|
|
639
|
+
self.entry_ptr = entry_ptr
|
|
640
|
+
|
|
641
|
+
self.operation = self.geodiff._CE_operation(
|
|
642
|
+
self.geodiff.context, self.entry_ptr
|
|
643
|
+
)
|
|
644
|
+
self.values_count = self.geodiff._CE_count(self.geodiff.context, self.entry_ptr)
|
|
645
|
+
|
|
646
|
+
if self.operation == self.OP_DELETE or self.operation == self.OP_UPDATE:
|
|
647
|
+
self.old_values = []
|
|
648
|
+
for i in range(self.values_count):
|
|
649
|
+
v_ptr = self.geodiff._CE_old_value(
|
|
650
|
+
self.geodiff.context, self.entry_ptr, i
|
|
651
|
+
)
|
|
652
|
+
self.old_values.append(self._convert_value(v_ptr))
|
|
653
|
+
|
|
654
|
+
if self.operation == self.OP_INSERT or self.operation == self.OP_UPDATE:
|
|
655
|
+
self.new_values = []
|
|
656
|
+
for i in range(self.values_count):
|
|
657
|
+
v_ptr = self.geodiff._CE_new_value(
|
|
658
|
+
self.geodiff.context, self.entry_ptr, i
|
|
659
|
+
)
|
|
660
|
+
self.new_values.append(self._convert_value(v_ptr))
|
|
661
|
+
|
|
662
|
+
table = self.geodiff._CE_table(self.geodiff.context, entry_ptr)
|
|
663
|
+
self.table = ChangesetTable(geodiff, table)
|
|
664
|
+
|
|
665
|
+
def __del__(self):
|
|
666
|
+
self.geodiff._CE_destroy(self.geodiff.context, self.entry_ptr)
|
|
667
|
+
|
|
668
|
+
def _convert_value(self, v_ptr):
|
|
669
|
+
v_type = self.geodiff._V_type(self.geodiff.context, v_ptr)
|
|
670
|
+
if v_type == 0:
|
|
671
|
+
v_val = UndefinedValue()
|
|
672
|
+
elif v_type == 1:
|
|
673
|
+
v_val = self.geodiff._V_get_int(self.geodiff.context, v_ptr)
|
|
674
|
+
elif v_type == 2:
|
|
675
|
+
v_val = self.geodiff._V_get_double(self.geodiff.context, v_ptr)
|
|
676
|
+
elif v_type == 3 or v_type == 4: # 3==text, 4==blob
|
|
677
|
+
size = self.geodiff._V_get_data_size(self.geodiff.context, v_ptr)
|
|
678
|
+
buffer = ctypes.create_string_buffer(size)
|
|
679
|
+
self.geodiff._V_get_data(self.geodiff.context, v_ptr, buffer)
|
|
680
|
+
v_val = buffer.raw
|
|
681
|
+
if v_type == 3:
|
|
682
|
+
v_val = v_val.decode("utf-8")
|
|
683
|
+
elif v_type == 5:
|
|
684
|
+
v_val = None
|
|
685
|
+
else:
|
|
686
|
+
raise GeoDiffLibError("unknown value type {}".format(v_type))
|
|
687
|
+
self.geodiff._V_destroy(self.geodiff.context, v_ptr)
|
|
688
|
+
return v_val
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class ChangesetTable(object):
|
|
692
|
+
"""Wrapper around GEODIFF_CT_* functions from C API"""
|
|
693
|
+
|
|
694
|
+
def __init__(self, geodiff, table_ptr):
|
|
695
|
+
self.geodiff = geodiff
|
|
696
|
+
self.table_ptr = table_ptr
|
|
697
|
+
|
|
698
|
+
self.name = self.geodiff._CT_name(self.geodiff.context, table_ptr).decode(
|
|
699
|
+
"utf-8"
|
|
700
|
+
)
|
|
701
|
+
self.column_count = self.geodiff._CT_column_count(
|
|
702
|
+
self.geodiff.context, table_ptr
|
|
703
|
+
)
|
|
704
|
+
self.column_is_pkey = []
|
|
705
|
+
for i in range(self.column_count):
|
|
706
|
+
self.column_is_pkey.append(
|
|
707
|
+
self.geodiff._CT_column_is_pkey(self.geodiff.context, table_ptr, i)
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
class UndefinedValue(object):
|
|
712
|
+
"""Marker that a value in changeset is undefined. This is different
|
|
713
|
+
from NULL value (which is represented as None). Undefined values are
|
|
714
|
+
used for example as values of columns in UPDATE operation that did
|
|
715
|
+
not get modified."""
|
|
716
|
+
|
|
717
|
+
def __repr__(self):
|
|
718
|
+
return "<N/A>"
|
|
Binary file
|
pygeodiff/main.py
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
pygeodiff.main
|
|
4
|
+
--------------
|
|
5
|
+
Main entry of the library
|
|
6
|
+
:copyright: (c) 2019-2022 Lutra Consulting Ltd.
|
|
7
|
+
:license: MIT, see LICENSE for more details.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .geodifflib import GeoDiffLib
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GeoDiff:
|
|
14
|
+
"""
|
|
15
|
+
geodiff is a module to create and apply changesets to GIS files (geopackage)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, libname=None):
|
|
19
|
+
"""
|
|
20
|
+
if libname is None, it tries to import c-extension from wheel
|
|
21
|
+
messages are shown in stdout/stderr.
|
|
22
|
+
Use environment variable GEODIFF_LOGGER_LEVEL 0(Nothing)-4(Debug) to
|
|
23
|
+
set level (Errors by default)
|
|
24
|
+
"""
|
|
25
|
+
self.clib = GeoDiffLib(libname)
|
|
26
|
+
|
|
27
|
+
def set_logger_callback(self, callback):
|
|
28
|
+
"""
|
|
29
|
+
Assign custom logger
|
|
30
|
+
Replace default stdout/stderr logger with custom.
|
|
31
|
+
When callback is None, no output is produced at all
|
|
32
|
+
Callback function has 2 arguments: (int) errorCode, (string) msg
|
|
33
|
+
"""
|
|
34
|
+
return self.clib.set_logger_callback(callback)
|
|
35
|
+
|
|
36
|
+
def set_tables_to_skip(self, tables):
|
|
37
|
+
"""
|
|
38
|
+
Set list of tables to exclude from geodiff operations. Once defined, these
|
|
39
|
+
tables will be excluded from the following operations: create changeset,
|
|
40
|
+
apply changeset, rebase, get database schema, dump database contents, copy
|
|
41
|
+
database between different drivers.
|
|
42
|
+
|
|
43
|
+
If empty list is passed, skip tables list will be reset.
|
|
44
|
+
"""
|
|
45
|
+
return self.clib.set_tables_to_skip(tables)
|
|
46
|
+
|
|
47
|
+
LevelError = 1
|
|
48
|
+
LevelWarning = 2
|
|
49
|
+
LevelInfo = 3
|
|
50
|
+
LevelDebug = 4
|
|
51
|
+
|
|
52
|
+
def set_maximum_logger_level(self, maxLevel):
|
|
53
|
+
"""
|
|
54
|
+
Assign maximum level of messages that are passed to logger callbac
|
|
55
|
+
Based on maxLogLevel, the messages are filtered by level:
|
|
56
|
+
maxLogLevel = 0 nothing is passed to logger callback
|
|
57
|
+
maxLogLevel = 1 errors are passed to logger callback
|
|
58
|
+
maxLogLevel = 2 errors and warnings are passed to logger callback
|
|
59
|
+
maxLogLevel = 3 errors, warnings and infos are passed to logger callback
|
|
60
|
+
maxLogLevel = 4 errors, warnings, infos, debug messages are passed to logger callback
|
|
61
|
+
"""
|
|
62
|
+
return self.clib.set_maximum_logger_level(maxLevel)
|
|
63
|
+
|
|
64
|
+
def drivers(self):
|
|
65
|
+
"""
|
|
66
|
+
Returns list of registered drivers (e.g. ["sqlite", "postgresql"])
|
|
67
|
+
|
|
68
|
+
raises GeoDiffLibError on error
|
|
69
|
+
"""
|
|
70
|
+
return self.clib.drivers()
|
|
71
|
+
|
|
72
|
+
def driver_is_registered(self, name):
|
|
73
|
+
"""
|
|
74
|
+
Returns whether dataset with given name is registered (e.g. "sqlite" or "postgresql")
|
|
75
|
+
"""
|
|
76
|
+
return self.clib.driver_is_registered(name)
|
|
77
|
+
|
|
78
|
+
def create_changeset(self, base, modified, changeset):
|
|
79
|
+
"""
|
|
80
|
+
Creates changeset file (binary) in such way that
|
|
81
|
+
if CHANGESET is applied to BASE by applyChangeset,
|
|
82
|
+
MODIFIED will be created
|
|
83
|
+
|
|
84
|
+
BASE --- CHANGESET ---> MODIFIED
|
|
85
|
+
|
|
86
|
+
\param base [input] BASE sqlite3/geopackage file
|
|
87
|
+
\param modified [input] MODIFIED sqlite3/geopackage file
|
|
88
|
+
\param changeset [output] changeset between BASE -> MODIFIED
|
|
89
|
+
|
|
90
|
+
raises GeoDiffLibError on error
|
|
91
|
+
"""
|
|
92
|
+
return self.clib.create_changeset(base, modified, changeset)
|
|
93
|
+
|
|
94
|
+
def invert_changeset(self, changeset, changeset_inv):
|
|
95
|
+
"""
|
|
96
|
+
Inverts changeset file (binary) in such way that
|
|
97
|
+
if CHANGESET_INV is applied to MODIFIED by applyChangeset,
|
|
98
|
+
BASE will be created
|
|
99
|
+
|
|
100
|
+
\param changeset [input] changeset between BASE -> MODIFIED
|
|
101
|
+
\param changeset_inv [output] changeset between MODIFIED -> BASE
|
|
102
|
+
|
|
103
|
+
\returns number of conflics
|
|
104
|
+
|
|
105
|
+
raises GeoDiffLibError on error
|
|
106
|
+
"""
|
|
107
|
+
return self.clib.invert_changeset(changeset, changeset_inv)
|
|
108
|
+
|
|
109
|
+
def rebase(self, base, modified_their, modified, conflict):
|
|
110
|
+
"""
|
|
111
|
+
Rebases local modified version from base to modified_their version
|
|
112
|
+
|
|
113
|
+
--- > MODIFIED_THEIR
|
|
114
|
+
BASE -|
|
|
115
|
+
----> MODIFIED (local) ---> MODIFIED_THEIR_PLUS_MINE
|
|
116
|
+
|
|
117
|
+
Steps performed on MODIFIED (local) file:
|
|
118
|
+
1. undo local changes MODIFIED -> BASE
|
|
119
|
+
2. apply changes from MODIFIED_THEIR
|
|
120
|
+
3. apply rebased local changes and create MODIFIED_THEIR_PLUS_MINE
|
|
121
|
+
|
|
122
|
+
Note, when rebase is not successfull, modified could be in random state.
|
|
123
|
+
This works in general, even when base==modified, or base==modified_theirs
|
|
124
|
+
|
|
125
|
+
\param base [input] BASE sqlite3/geopackage file
|
|
126
|
+
\param modified_their [input] MODIFIED sqlite3/geopackage file
|
|
127
|
+
\param modified [input] local copy of the changes to be rebased
|
|
128
|
+
\param conflict [output] file where the automatically resolved conflicts are stored. If there are no conflicts, file is not created
|
|
129
|
+
|
|
130
|
+
raises GeoDiffLibError on error
|
|
131
|
+
"""
|
|
132
|
+
return self.clib.rebase(base, modified_their, modified, conflict)
|
|
133
|
+
|
|
134
|
+
def create_rebased_changeset(
|
|
135
|
+
self, base, modified, changeset_their, changeset, conflict
|
|
136
|
+
):
|
|
137
|
+
"""
|
|
138
|
+
Creates changeset file (binary) in such way that
|
|
139
|
+
if CHANGESET is applied to MODIFIED_THEIR by
|
|
140
|
+
applyChangeset, the new state will contain all
|
|
141
|
+
changes from MODIFIED and MODIFIED_THEIR.
|
|
142
|
+
|
|
143
|
+
--- CHANGESET_THEIR ---> MODIFIED_THEIR --- CHANGESET ---> MODIFIED_THEIR_PLUS_MINE
|
|
144
|
+
BASE -|
|
|
145
|
+
-----------------------> MODIFIED
|
|
146
|
+
|
|
147
|
+
\param base [input] BASE sqlite3/geopackage file
|
|
148
|
+
\param modified [input] MODIFIED sqlite3/geopackage file
|
|
149
|
+
\param changeset_their [input] changeset between BASE -> MODIFIED_THEIR
|
|
150
|
+
\param changeset [output] changeset between MODIFIED_THEIR -> MODIFIED_THEIR_PLUS_MINE
|
|
151
|
+
\param conflict [output] file where the automatically resolved conflicts are stored. If there are no conflicts, file is not created
|
|
152
|
+
|
|
153
|
+
raises GeoDiffLibError on error
|
|
154
|
+
"""
|
|
155
|
+
return self.clib.create_rebased_changeset(
|
|
156
|
+
base, modified, changeset_their, changeset, conflict
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def apply_changeset(self, base, changeset):
|
|
160
|
+
"""
|
|
161
|
+
Applies changeset file (binary) to BASE
|
|
162
|
+
|
|
163
|
+
\param base [input/output] BASE sqlite3/geopackage file
|
|
164
|
+
\param changeset [input] changeset to apply to BASE
|
|
165
|
+
\returns number of conflicts
|
|
166
|
+
|
|
167
|
+
raises GeoDiffLibError on error
|
|
168
|
+
"""
|
|
169
|
+
return self.clib.apply_changeset(base, changeset)
|
|
170
|
+
|
|
171
|
+
def list_changes(self, changeset, json):
|
|
172
|
+
"""
|
|
173
|
+
Lists changeset content JSON file
|
|
174
|
+
JSON contains all changes in human/machine readable name
|
|
175
|
+
\returns number of changes
|
|
176
|
+
|
|
177
|
+
raises GeoDiffLibError on error
|
|
178
|
+
"""
|
|
179
|
+
return self.clib.list_changes(changeset, json)
|
|
180
|
+
|
|
181
|
+
def list_changes_summary(self, changeset, json):
|
|
182
|
+
"""
|
|
183
|
+
Lists changeset summary content JSON file
|
|
184
|
+
JSON contains a list of how many inserts/edits/deletes is contained in changeset for each table
|
|
185
|
+
\returns number of changes
|
|
186
|
+
|
|
187
|
+
raises GeoDiffLibError on error
|
|
188
|
+
"""
|
|
189
|
+
return self.clib.list_changes_summary(changeset, json)
|
|
190
|
+
|
|
191
|
+
def has_changes(self, changeset):
|
|
192
|
+
"""
|
|
193
|
+
\returns whether changeset contains at least one change
|
|
194
|
+
|
|
195
|
+
raises GeoDiffLibError on error
|
|
196
|
+
"""
|
|
197
|
+
return self.clib.has_changes(changeset)
|
|
198
|
+
|
|
199
|
+
def changes_count(self, changeset):
|
|
200
|
+
"""
|
|
201
|
+
\returns number of changes
|
|
202
|
+
|
|
203
|
+
raises GeoDiffLibError on error
|
|
204
|
+
"""
|
|
205
|
+
return self.clib.changes_count(changeset)
|
|
206
|
+
|
|
207
|
+
def concat_changes(self, list_changesets, output_changeset):
|
|
208
|
+
"""
|
|
209
|
+
Combine multiple changeset files into a single changeset file. When the output changeset
|
|
210
|
+
is applied to a database, the result should be the same as if the input changesets were applied
|
|
211
|
+
one by one. The order of input files is important. At least two input files need to be
|
|
212
|
+
provided.
|
|
213
|
+
|
|
214
|
+
Incompatible changes (which would cause conflicts when applied) will be discarded.
|
|
215
|
+
|
|
216
|
+
raises GeoDiffLibError on error
|
|
217
|
+
"""
|
|
218
|
+
return self.clib.concat_changes(list_changesets, output_changeset)
|
|
219
|
+
|
|
220
|
+
def make_copy(
|
|
221
|
+
self, driver_src, driver_src_info, src, driver_dst, driver_dst_info, dst
|
|
222
|
+
):
|
|
223
|
+
"""
|
|
224
|
+
Makes a copy of the source dataset (a collection of tables) to the specified destination.
|
|
225
|
+
|
|
226
|
+
This will open the source dataset, get list of tables, their structure, dump data
|
|
227
|
+
to a temporary changeset file. Then it will create the destination dataset, create tables
|
|
228
|
+
and insert data from changeset file.
|
|
229
|
+
|
|
230
|
+
Supported drivers:
|
|
231
|
+
|
|
232
|
+
- "sqlite" - does not need extra connection info (may be null). A dataset is a single Sqlite3
|
|
233
|
+
database (a GeoPackage) - a path to a local file is expected.
|
|
234
|
+
|
|
235
|
+
- "postgres" - only available if compiled with postgres support. Needs extra connection info
|
|
236
|
+
argument which is passed to libpq's PQconnectdb(), see PostgreSQL docs for syntax.
|
|
237
|
+
A datasource identifies a PostgreSQL schema name (namespace) within the current database.
|
|
238
|
+
|
|
239
|
+
raises GeoDiffLibError on error
|
|
240
|
+
"""
|
|
241
|
+
return self.clib.make_copy(
|
|
242
|
+
driver_src, driver_src_info, src, driver_dst, driver_dst_info, dst
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def make_copy_sqlite(self, src, dst):
|
|
246
|
+
"""
|
|
247
|
+
Makes a copy of a SQLite database. If the destination database file exists, it will be overwritten.
|
|
248
|
+
|
|
249
|
+
This is the preferred way of copying SQLite/GeoPackage files compared to just using raw copying
|
|
250
|
+
of files on the file system: it will take into account other readers/writers and WAL file,
|
|
251
|
+
so we should never end up with a corrupt copy.
|
|
252
|
+
|
|
253
|
+
raises GeoDiffLibError on error
|
|
254
|
+
"""
|
|
255
|
+
return self.clib.make_copy_sqlite(src, dst)
|
|
256
|
+
|
|
257
|
+
def create_changeset_ex(self, driver, driver_info, base, modified, changeset):
|
|
258
|
+
"""
|
|
259
|
+
This is an extended version of create_changeset() which also allows specification
|
|
260
|
+
of the driver and its extra connection info. The original create_changeset() function
|
|
261
|
+
only supports Sqlite driver.
|
|
262
|
+
|
|
263
|
+
See documentation of make_copy() for details about supported drivers.
|
|
264
|
+
|
|
265
|
+
raises GeoDiffLibError on error
|
|
266
|
+
"""
|
|
267
|
+
return self.clib.create_changeset_ex(
|
|
268
|
+
driver, driver_info, base, modified, changeset
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def create_changeset_dr(
|
|
272
|
+
self,
|
|
273
|
+
driver_src,
|
|
274
|
+
driver_src_info,
|
|
275
|
+
src,
|
|
276
|
+
driver_dst,
|
|
277
|
+
driver_dst_info,
|
|
278
|
+
dst,
|
|
279
|
+
changeset,
|
|
280
|
+
):
|
|
281
|
+
"""
|
|
282
|
+
Creates changeset file (binary) between src and dest for different drivers.
|
|
283
|
+
Currently supported drivers:
|
|
284
|
+
- sqlite
|
|
285
|
+
- postgres
|
|
286
|
+
|
|
287
|
+
See documentation of create_changeset for more information about changeset.
|
|
288
|
+
|
|
289
|
+
\param driver_src [input] driver of base src
|
|
290
|
+
\param driver_src_info [input] connection string, leave empty for sqlite, for postgres pass a string of format:
|
|
291
|
+
"host=<host> port=<port> user=<user> password=<password> dbname=<database name>"
|
|
292
|
+
\param src [input] BASE sqlite3/geopackage file for sqlite and schema name for postgres
|
|
293
|
+
\param driver_dst [input] driver of modified dst
|
|
294
|
+
\param driver_dst_info [input] connection string for destination driver
|
|
295
|
+
\param dst [input] MODIFIED sqlite3/geopackage file for sqlite and schema name for postgres
|
|
296
|
+
\param changeset [output] changeset between SRC -> DST
|
|
297
|
+
|
|
298
|
+
raises GeoDiffLibError on error
|
|
299
|
+
"""
|
|
300
|
+
return self.clib.create_changeset_dr(
|
|
301
|
+
driver_src,
|
|
302
|
+
driver_src_info,
|
|
303
|
+
src,
|
|
304
|
+
driver_dst,
|
|
305
|
+
driver_dst_info,
|
|
306
|
+
dst,
|
|
307
|
+
changeset,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def apply_changeset_ex(self, driver, driver_info, base, changeset):
|
|
311
|
+
"""
|
|
312
|
+
This is an extended version of apply_changeset() which also allows specification
|
|
313
|
+
of the driver and its extra connection info. The original apply_changeset() function
|
|
314
|
+
only supports Sqlite driver.
|
|
315
|
+
|
|
316
|
+
See documentation of make_copy() for details about supported drivers.
|
|
317
|
+
|
|
318
|
+
raises GeoDiffLibError on error
|
|
319
|
+
"""
|
|
320
|
+
return self.clib.apply_changeset_ex(driver, driver_info, base, changeset)
|
|
321
|
+
|
|
322
|
+
def create_rebased_changeset_ex(
|
|
323
|
+
self,
|
|
324
|
+
driver,
|
|
325
|
+
driver_info,
|
|
326
|
+
base,
|
|
327
|
+
base2modified,
|
|
328
|
+
base2their,
|
|
329
|
+
rebased,
|
|
330
|
+
conflict_file,
|
|
331
|
+
):
|
|
332
|
+
"""
|
|
333
|
+
This function takes an existing changeset "base2modified" and rebases it on top of changes in
|
|
334
|
+
"base2their" and writes output to a new changeset "rebased"
|
|
335
|
+
|
|
336
|
+
raises GeoDiffLibError on error
|
|
337
|
+
"""
|
|
338
|
+
return self.clib.create_rebased_changeset_ex(
|
|
339
|
+
driver, driver_info, base, base2modified, base2their, rebased, conflict_file
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def rebase_ex(self, driver, driver_info, base, modified, base2their, conflict_file):
|
|
343
|
+
"""
|
|
344
|
+
This function takes care of updating "modified" dataset by taking any changes between "base"
|
|
345
|
+
and "modified" datasets and rebasing them on top of base2their changeset.
|
|
346
|
+
|
|
347
|
+
raises GeoDiffLibError on error
|
|
348
|
+
"""
|
|
349
|
+
return self.clib.rebase_ex(
|
|
350
|
+
driver, driver_info, base, modified, base2their, conflict_file
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def dump_data(self, driver, driver_info, src, changeset):
|
|
354
|
+
"""
|
|
355
|
+
Dumps all data from the data source as INSERT statements to a new changeset file.
|
|
356
|
+
|
|
357
|
+
raises GeoDiffLibError on error
|
|
358
|
+
"""
|
|
359
|
+
return self.clib.dump_data(driver, driver_info, src, changeset)
|
|
360
|
+
|
|
361
|
+
def schema(self, driver, driver_info, src, json):
|
|
362
|
+
"""
|
|
363
|
+
Writes a JSON file containing database schema of tables as understood by geodiff.
|
|
364
|
+
|
|
365
|
+
raises GeoDiffLibError on error
|
|
366
|
+
"""
|
|
367
|
+
return self.clib.schema(driver, driver_info, src, json)
|
|
368
|
+
|
|
369
|
+
def read_changeset(self, changeset):
|
|
370
|
+
"""
|
|
371
|
+
Opens a changeset file and returns reader object or raises GeoDiffLibError on error.
|
|
372
|
+
"""
|
|
373
|
+
return self.clib.read_changeset(changeset)
|
|
374
|
+
|
|
375
|
+
def version(self):
|
|
376
|
+
"""
|
|
377
|
+
geodiff version
|
|
378
|
+
"""
|
|
379
|
+
return self.clib.version()
|
|
380
|
+
|
|
381
|
+
def create_wkb_from_gpkg_header(self, geometry):
|
|
382
|
+
"""
|
|
383
|
+
Extracts geometry in WKB format from the geometry encoded according to GeoPackage spec
|
|
384
|
+
"""
|
|
385
|
+
return self.clib.create_wkb_from_gpkg_header(geometry)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def main():
|
|
389
|
+
diff_lib = GeoDiff()
|
|
390
|
+
print("pygeodiff " + diff_lib.version())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Lutra Consulting Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pygeodiff
|
|
3
|
+
Version: 2.0.4
|
|
4
|
+
Summary: Python wrapper around GeoDiff library
|
|
5
|
+
Home-page: https://github.com/MerginMaps/geodiff
|
|
6
|
+
Author: Lutra Consulting Ltd.
|
|
7
|
+
Author-email: info@merginmaps.com
|
|
8
|
+
License: License :: OSI Approved :: MIT License
|
|
9
|
+
Keywords: diff,gis,geo,geopackage,merge
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
|
|
14
|
+
Python wrapper around GeoDiff library
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pygeodiff/__init__.py,sha256=tNR2dfUOhbLnHY9BFg3SXsFqT-MVjU5WlSjePwFFaas,470
|
|
2
|
+
pygeodiff/libpygeodiff-2.0.4-python.dylib,sha256=dxMQX0bKeCJ7_xR2bpiQvAOumQvX3IgHNaIPS6sZ3bg,528848
|
|
3
|
+
pygeodiff/__about__.py,sha256=XRBs6-H8pn1zorWiCWgu5u1cBQwk1SCWmgaNiNEC-NI,427
|
|
4
|
+
pygeodiff/geodifflib.py,sha256=BfAK0K4t7rMgJ13yiH1Zl1OPdUATu5JBzHTcn05NZ2E,25086
|
|
5
|
+
pygeodiff/main.py,sha256=PE0dFuDfsMC2gVEjd5lEqjDdf7uHLh5DTdVIJzstgeQ,14423
|
|
6
|
+
pygeodiff/.dylibs/libsqlite3.0.dylib,sha256=EuFr3TE5NVNAn3v_oZGQf5egds6bmegFQ_Y-FW0nrwo,1793696
|
|
7
|
+
pygeodiff-2.0.4.dist-info/RECORD,,
|
|
8
|
+
pygeodiff-2.0.4.dist-info/LICENSE,sha256=pPekwnp737i2BfjOCth9EjHA8ijKqgUqnZvXhSiy7rk,1078
|
|
9
|
+
pygeodiff-2.0.4.dist-info/WHEEL,sha256=vmRTWwSr0k67XZG4u7Ipy0i2UTgz-efRH2vDqHyId_k,103
|
|
10
|
+
pygeodiff-2.0.4.dist-info/entry_points.txt,sha256=RT77c6_7NNim8cyF-SS9JxyG_28rsFv-Me24fU2co7o,50
|
|
11
|
+
pygeodiff-2.0.4.dist-info/top_level.txt,sha256=ndjfGeE8jGvELbnBQHaHjf13GDrUcOGA7V64GnQvFgc,10
|
|
12
|
+
pygeodiff-2.0.4.dist-info/METADATA,sha256=JGHSMWwWACuAbCoMFBTzEhOc70wEBBGVrpgOkOhgtkk,433
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pygeodiff
|