diff --git a/.flake8 b/.flake8 index 9ab1801..0e2e870 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,2 @@ [flake8] max-line-length=100 - diff --git a/README.md b/README.md index b9ddc31..0a78dde 100644 --- a/README.md +++ b/README.md @@ -120,15 +120,19 @@ Changes between datasets are read from and written to a [binary changeset format # Building geodiff +## Deps Install postgresql client and sqlite3 library, e.g. for Linux ```bash sudo apt-get install libsqlite3-dev libpq-dev ``` -or MacOS (using SQLite from [QGIS deps](https://qgis.org/downloads/macos/deps/)) by defining SQLite variables in -a cmake configuration as following: -```bash -SQLite3_INCLUDE_DIR=/opt/QGIS/qgis-deps-${QGIS_DEPS_VERSION}/stage/include -SQLite3_LIBRARY=/opt/QGIS/qgis-deps-${QGIS_DEPS_VERSION}/stage/lib/libsqlite3.dylib + +alternatively you can build libraries yourself: +for sqlite +- download [autoconf version](https://www.sqlite.org/download.html) +- extract +``` + ./configure --enable-dynamic-extensions + make ``` Compile geodiff: @@ -136,10 +140,19 @@ Compile geodiff: cd geodiff mkdir build cd build -cmake .. -DWITH_POSTGRESQL=TRUE + +cmake \ + -DWITH_POSTGRESQL=TRUE \ + -DSQLite3_INCLUDE_DIR=../../sqlite-autoconf-3450000 \ + -DSQLite3_LIBRARY=../../sqlite-autoconf-3450000/.libs/libsqlite3.a \ +../geodiff + make ``` +if you get ```error: use of undeclared identifier 'sqlite3_enable_load_extension'``` make sure +you use sqlite with enabled dynamic extension. + ## Development of geodiff ### Running tests @@ -148,7 +161,7 @@ C++ tests: run `make test` or `ctest` to run all tests. Alternatively run just a Python tests: you need to setup GEODIFFLIB with path to .so/.dylib from build step ```bash -GEODIFFLIB=`pwd`/../build/libgeodiff.dylib GEODIFFCLI=`pwd`/build/geodiff pytest +GEODIFFLIB=`pwd`/build/libgeodiff.dylib GEODIFFCLI=`pwd`/build/geodiff pytest ``` ### Releasing new version diff --git a/geodiff/src/geodiff.cpp b/geodiff/src/geodiff.cpp index 393b5dc..505a289 100644 --- a/geodiff/src/geodiff.cpp +++ b/geodiff/src/geodiff.cpp @@ -54,7 +54,7 @@ static int handleException( Context *context, const GeoDiffException &exc ) // use scripts/update_version.py to update the version here and in other places at once const char *GEODIFF_version() { - return "2.0.4"; + return "2.1.0"; } int GEODIFF_driverCount( GEODIFF_ContextH /*contextHandle*/ ) diff --git a/pygeodiff/__about__.py b/pygeodiff/__about__.py index 30f0079..608e134 100644 --- a/pygeodiff/__about__.py +++ b/pygeodiff/__about__.py @@ -2,7 +2,7 @@ __description__ = "Diff tool for geo-spatial data" __url__ = "https://github.com/MerginMaps/geodiff" # use scripts/update_version.py to update the version here and in other places at once -__version__ = "2.0.4" +__version__ = "2.1.0" __author__ = "Lutra Consulting Ltd." __author_email__ = "info@merginmaps.com" __maintainer__ = "Lutra Consulting Ltd." diff --git a/pygeodiff/geodifflib.py b/pygeodiff/geodifflib.py index 761b606..da80d02 100644 --- a/pygeodiff/geodifflib.py +++ b/pygeodiff/geodifflib.py @@ -40,7 +40,6 @@ class GeoDiffLibVersionError(GeoDiffLibError): class GeoDiffLib: def __init__(self, name): - self.context = None if name is None: self.libname = self.package_libname() if not os.path.exists(self.libname): @@ -62,20 +61,20 @@ def __init__(self, name): raise GeoDiffLibVersionError( "Unable to load geodiff library " + self.libname ) - self.context = self.init() - self.callbackLogger = None - if self.context is None: - raise GeoDiffLibVersionError("Unable to create GeoDiff context") self.check_version() self._register_functions() def __del__(self): - if self.context is not None: - func = self.lib.GEODIFF_CX_destroy - func.argtypes = [ctypes.c_void_p] - func(self.context) - self.context = None + self.shutdown() + + def shutdown(self): + if self.lib is not None: + if platform.system() == "Windows": + from _ctypes import FreeLibrary + + FreeLibrary(self.lib._handle) + self.lib = None def _register_functions(self): self._readChangeset = self.lib.GEODIFF_readChangeset @@ -158,14 +157,14 @@ def _register_functions(self): self._V_destroy = self.lib.GEODIFF_V_destroy self._V_destroy.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - def _parse_return_code(self, rc, ctx): + def _parse_return_code(self, context, rc, ctx): if rc == SUCCESS: return get_error = self.lib.GEODIFF_CX_lastError get_error.restype = ctypes.c_char_p get_error.argtypes = [ctypes.c_void_p] - err = get_error(self.context).decode("utf-8") + err = get_error(context).decode("utf-8") msg = "Error in " + ctx + ":\n" + err if rc == ERROR: @@ -198,35 +197,45 @@ def package_libname(self): dir_path = os.path.dirname(os.path.realpath(__file__)) return os.path.join(dir_path, whl_lib) - def init(self): + def create_context(self): func = self.lib.GEODIFF_createContext func.restype = ctypes.c_void_p - return func() + context = func() + if context is None: + raise GeoDiffLibVersionError("Unable to create GeoDiff context") + return context - def set_logger_callback(self, callback): + def destroy_context(self, context): + if context is not None: + func = self.lib.GEODIFF_CX_destroy + func.argtypes = [ctypes.c_void_p] + func(context) + + def set_logger_callback(self, context, callback): func = self.lib.GEODIFF_CX_setLoggerCallback cFuncType = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p) func.argtypes = [ctypes.c_void_p, cFuncType] if callback: # do not remove self, callback needs to be member - self.callbackLogger = cFuncType(callback) + callbackLogger = cFuncType(callback) else: - self.callbackLogger = cFuncType() - func(self.context, self.callbackLogger) + callbackLogger = cFuncType() + func(context, callbackLogger) + return callbackLogger - def set_maximum_logger_level(self, maxLevel): + def set_maximum_logger_level(self, context, maxLevel): func = self.lib.GEODIFF_CX_setMaximumLoggerLevel func.argtypes = [ctypes.c_void_p, ctypes.c_int] - func(self.context, maxLevel) + func(context, maxLevel) - def set_tables_to_skip(self, tables): + def set_tables_to_skip(self, context, tables): # make array of char* with utf-8 encoding from python list of strings arr = (ctypes.c_char_p * len(tables))() for i in range(len(tables)): arr[i] = tables[i].encode("utf-8") self.lib.GEODIFF_CX_setTablesToSkip( - ctypes.c_void_p(self.context), ctypes.c_int(len(tables)), arr + ctypes.c_void_p(context), ctypes.c_int(len(tables)), arr ) def version(self): @@ -243,7 +252,7 @@ def check_version(self): "version mismatch ({} C vs {} PY)".format(cversion, pyversion) ) - def drivers(self): + def drivers(self, context): _driver_count_f = self.lib.GEODIFF_driverCount _driver_count_f.argtypes = [ctypes.c_void_p] _driver_count_f.restype = ctypes.c_int @@ -257,26 +266,26 @@ def drivers(self): _driver_name_from_index_f.restype = ctypes.c_int drivers_list = [] - driversCount = _driver_count_f(self.context) + driversCount = _driver_count_f(context) for index in range(driversCount): name_raw = 256 * "" b_string1 = name_raw.encode("utf-8") - res = _driver_name_from_index_f(self.context, index, b_string1) - self._parse_return_code(res, "drivers") + res = _driver_name_from_index_f(context, index, b_string1) + self._parse_return_code(context, res, "drivers") name = b_string1.decode("utf-8") drivers_list.append(name) return drivers_list - def driver_is_registered(self, name): + def driver_is_registered(self, context, name): func = self.lib.GEODIFF_driverIsRegistered func.argtypes = [ctypes.c_void_p, ctypes.c_char_p] func.restype = ctypes.c_bool b_string1 = name.encode("utf-8") - return func(self.context, b_string1) + return func(context, b_string1) - def create_changeset(self, base, modified, changeset): + def create_changeset(self, context, base, modified, changeset): func = self.lib.GEODIFF_createChangeset func.argtypes = [ ctypes.c_void_p, @@ -291,10 +300,10 @@ def create_changeset(self, base, modified, changeset): b_string2 = modified.encode("utf-8") b_string3 = changeset.encode("utf-8") - res = func(self.context, b_string1, b_string2, b_string3) - self._parse_return_code(res, "createChangeset") + res = func(context, b_string1, b_string2, b_string3) + self._parse_return_code(context, res, "createChangeset") - def invert_changeset(self, changeset, changeset_inv): + def invert_changeset(self, context, changeset, changeset_inv): func = self.lib.GEODIFF_invertChangeset func.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] func.restype = ctypes.c_int @@ -303,11 +312,11 @@ def invert_changeset(self, changeset, changeset_inv): b_string1 = changeset.encode("utf-8") b_string2 = changeset_inv.encode("utf-8") - res = func(self.context, b_string1, b_string2) - self._parse_return_code(res, "invert_changeset") + res = func(context, b_string1, b_string2) + self._parse_return_code(context, res, "invert_changeset") def create_rebased_changeset( - self, base, modified, changeset_their, changeset, conflict + self, context, base, modified, changeset_their, changeset, conflict ): func = self.lib.GEODIFF_createRebasedChangeset func.argtypes = [ @@ -327,10 +336,10 @@ def create_rebased_changeset( b_string4 = changeset.encode("utf-8") b_string5 = conflict.encode("utf-8") - res = func(self.context, b_string1, b_string2, b_string3, b_string4, b_string5) - self._parse_return_code(res, "createRebasedChangeset") + res = func(context, b_string1, b_string2, b_string3, b_string4, b_string5) + self._parse_return_code(context, res, "createRebasedChangeset") - def rebase(self, base, modified_their, modified, conflict): + def rebase(self, context, base, modified_their, modified, conflict): func = self.lib.GEODIFF_rebase func.argtypes = [ ctypes.c_void_p, @@ -346,10 +355,10 @@ def rebase(self, base, modified_their, modified, conflict): b_string2 = modified_their.encode("utf-8") b_string3 = modified.encode("utf-8") b_string4 = conflict.encode("utf-8") - res = func(self.context, b_string1, b_string2, b_string3, b_string4) - self._parse_return_code(res, "rebase") + res = func(context, b_string1, b_string2, b_string3, b_string4) + self._parse_return_code(context, res, "rebase") - def apply_changeset(self, base, changeset): + def apply_changeset(self, context, base, changeset): func = self.lib.GEODIFF_applyChangeset func.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] func.restype = ctypes.c_int @@ -358,10 +367,10 @@ def apply_changeset(self, base, changeset): b_string1 = base.encode("utf-8") b_string2 = changeset.encode("utf-8") - res = func(self.context, b_string1, b_string2) - self._parse_return_code(res, "apply_changeset") + res = func(context, b_string1, b_string2) + self._parse_return_code(context, res, "apply_changeset") - def list_changes(self, changeset, result): + def list_changes(self, context, changeset, result): func = self.lib.GEODIFF_listChanges func.argtypes = [ctypes.c_void_p, ctypes.c_char_p] func.restype = ctypes.c_int @@ -369,10 +378,10 @@ def list_changes(self, changeset, result): # create byte objects from the strings b_string1 = changeset.encode("utf-8") b_string2 = result.encode("utf-8") - res = func(self.context, b_string1, b_string2) - self._parse_return_code(res, "list_changes") + res = func(context, b_string1, b_string2) + self._parse_return_code(context, res, "list_changes") - def list_changes_summary(self, changeset, result): + def list_changes_summary(self, context, changeset, result): func = self.lib.GEODIFF_listChangesSummary func.argtypes = [ctypes.c_void_p, ctypes.c_char_p] func.restype = ctypes.c_int @@ -380,10 +389,10 @@ def list_changes_summary(self, changeset, result): # create byte objects from the strings b_string1 = changeset.encode("utf-8") b_string2 = result.encode("utf-8") - res = func(self.context, b_string1, b_string2) - self._parse_return_code(res, "list_changes_summary") + res = func(context, b_string1, b_string2) + self._parse_return_code(context, res, "list_changes_summary") - def has_changes(self, changeset): + def has_changes(self, context, changeset): func = self.lib.GEODIFF_hasChanges func.argtypes = [ctypes.c_void_p, ctypes.c_char_p] func.restype = ctypes.c_int @@ -391,12 +400,12 @@ def has_changes(self, changeset): # create byte objects from the strings b_string1 = changeset.encode("utf-8") - nchanges = func(self.context, b_string1) + nchanges = func(context, b_string1) if nchanges < 0: raise GeoDiffLibError("has_changes") return nchanges == 1 - def changes_count(self, changeset): + def changes_count(self, context, changeset): func = self.lib.GEODIFF_changesCount func.argtypes = [ctypes.c_void_p, ctypes.c_char_p] func.restype = ctypes.c_int @@ -404,30 +413,37 @@ def changes_count(self, changeset): # create byte objects from the strings b_string1 = changeset.encode("utf-8") - nchanges = func(self.context, b_string1) + nchanges = func(context, b_string1) if nchanges < 0: raise GeoDiffLibError("changes_count") return nchanges - def concat_changes(self, list_changesets, output_changeset): + def concat_changes(self, context, list_changesets, output_changeset): # make array of char* with utf-8 encoding from python list of strings arr = (ctypes.c_char_p * len(list_changesets))() for i in range(len(list_changesets)): arr[i] = list_changesets[i].encode("utf-8") res = self.lib.GEODIFF_concatChanges( - ctypes.c_void_p(self.context), + ctypes.c_void_p(context), ctypes.c_int(len(list_changesets)), arr, ctypes.c_char_p(output_changeset.encode("utf-8")), ) - self._parse_return_code(res, "concat_changes") + self._parse_return_code(context, res, "concat_changes") def make_copy( - self, driver_src, driver_src_info, src, driver_dst, driver_dst_info, dst + self, + context, + driver_src, + driver_src_info, + src, + driver_dst, + driver_dst_info, + dst, ): res = self.lib.GEODIFF_makeCopy( - ctypes.c_void_p(self.context), + ctypes.c_void_p(context), ctypes.c_char_p(driver_src.encode("utf-8")), ctypes.c_char_p(driver_src_info.encode("utf-8")), ctypes.c_char_p(src.encode("utf-8")), @@ -435,29 +451,32 @@ def make_copy( ctypes.c_char_p(driver_dst_info.encode("utf-8")), ctypes.c_char_p(dst.encode("utf-8")), ) - self._parse_return_code(res, "make_copy") + self._parse_return_code(context, res, "make_copy") - def make_copy_sqlite(self, src, dst): + def make_copy_sqlite(self, context, src, dst): res = self.lib.GEODIFF_makeCopySqlite( - ctypes.c_void_p(self.context), + ctypes.c_void_p(context), ctypes.c_char_p(src.encode("utf-8")), ctypes.c_char_p(dst.encode("utf-8")), ) - self._parse_return_code(res, "make_copy_sqlite") + self._parse_return_code(context, res, "make_copy_sqlite") - def create_changeset_ex(self, driver, driver_info, base, modified, changeset): + def create_changeset_ex( + self, context, driver, driver_info, base, modified, changeset + ): res = self.lib.GEODIFF_createChangesetEx( - ctypes.c_void_p(self.context), + ctypes.c_void_p(context), ctypes.c_char_p(driver.encode("utf-8")), ctypes.c_char_p(driver_info.encode("utf-8")), ctypes.c_char_p(base.encode("utf-8")), ctypes.c_char_p(modified.encode("utf-8")), ctypes.c_char_p(changeset.encode("utf-8")), ) - self._parse_return_code(res, "create_changeset_ex") + self._parse_return_code(context, res, "create_changeset_ex") def create_changeset_dr( self, + context, driver_src, driver_src_info, src, @@ -488,7 +507,7 @@ def create_changeset_dr( b_string7 = changeset.encode("utf-8") res = func( - self.context, + context, b_string1, b_string2, b_string3, @@ -497,20 +516,21 @@ def create_changeset_dr( b_string6, b_string7, ) - self._parse_return_code(res, "CreateChangesetDr") + self._parse_return_code(context, res, "CreateChangesetDr") - def apply_changeset_ex(self, driver, driver_info, base, changeset): + def apply_changeset_ex(self, context, driver, driver_info, base, changeset): res = self.lib.GEODIFF_applyChangesetEx( - ctypes.c_void_p(self.context), + ctypes.c_void_p(context), ctypes.c_char_p(driver.encode("utf-8")), ctypes.c_char_p(driver_info.encode("utf-8")), ctypes.c_char_p(base.encode("utf-8")), ctypes.c_char_p(changeset.encode("utf-8")), ) - self._parse_return_code(res, "apply_changeset_ex") + self._parse_return_code(context, res, "apply_changeset_ex") def create_rebased_changeset_ex( self, + context, driver, driver_info, base, @@ -520,7 +540,7 @@ def create_rebased_changeset_ex( conflict_file, ): res = self.lib.GEODIFF_createRebasedChangesetEx( - ctypes.c_void_p(self.context), + ctypes.c_void_p(context), ctypes.c_char_p(driver.encode("utf-8")), ctypes.c_char_p(driver_info.encode("utf-8")), ctypes.c_char_p(base.encode("utf-8")), @@ -529,11 +549,13 @@ def create_rebased_changeset_ex( ctypes.c_char_p(rebased.encode("utf-8")), ctypes.c_char_p(conflict_file.encode("utf-8")), ) - self._parse_return_code(res, "create_rebased_changeset_ex") + self._parse_return_code(context, res, "create_rebased_changeset_ex") - def rebase_ex(self, driver, driver_info, base, modified, base2their, conflict_file): + def rebase_ex( + self, context, driver, driver_info, base, modified, base2their, conflict_file + ): res = self.lib.GEODIFF_rebaseEx( - ctypes.c_void_p(self.context), + ctypes.c_void_p(context), ctypes.c_char_p(driver.encode("utf-8")), ctypes.c_char_p(driver_info.encode("utf-8")), ctypes.c_char_p(base.encode("utf-8")), @@ -541,38 +563,37 @@ def rebase_ex(self, driver, driver_info, base, modified, base2their, conflict_fi ctypes.c_char_p(base2their.encode("utf-8")), ctypes.c_char_p(conflict_file.encode("utf-8")), ) - self._parse_return_code(res, "rebase_ex") + self._parse_return_code(context, res, "rebase_ex") - def dump_data(self, driver, driver_info, src, changeset): + def dump_data(self, context, driver, driver_info, src, changeset): res = self.lib.GEODIFF_dumpData( - ctypes.c_void_p(self.context), + ctypes.c_void_p(context), ctypes.c_char_p(driver.encode("utf-8")), ctypes.c_char_p(driver_info.encode("utf-8")), ctypes.c_char_p(src.encode("utf-8")), ctypes.c_char_p(changeset.encode("utf-8")), ) - self._parse_return_code(res, "dump_data") + self._parse_return_code(context, res, "dump_data") - def schema(self, driver, driver_info, src, json): + def schema(self, context, driver, driver_info, src, json): res = self.lib.GEODIFF_schema( - ctypes.c_void_p(self.context), + ctypes.c_void_p(context), ctypes.c_char_p(driver.encode("utf-8")), ctypes.c_char_p(driver_info.encode("utf-8")), ctypes.c_char_p(src.encode("utf-8")), ctypes.c_char_p(json.encode("utf-8")), ) - self._parse_return_code(res, "schema") - - def read_changeset(self, changeset): + self._parse_return_code(context, res, "schema") + def read_changeset(self, context, changeset): b_string1 = changeset.encode("utf-8") - reader_ptr = self._readChangeset(self.context, b_string1) + reader_ptr = self._readChangeset(context, b_string1) if reader_ptr is None: raise GeoDiffLibError("Unable to open reader for: " + changeset) - return ChangesetReader(self, reader_ptr) + return ChangesetReader(self, context, reader_ptr) - def create_wkb_from_gpkg_header(self, geometry): + def create_wkb_from_gpkg_header(self, context, geometry): func = self.lib.GEODIFF_createWkbFromGpkgHeader func.argtypes = [ ctypes.c_void_p, @@ -586,13 +607,13 @@ def create_wkb_from_gpkg_header(self, geometry): out = ctypes.POINTER(ctypes.c_char)() out_size = ctypes.c_size_t(len(geometry)) res = func( - self.context, + context, geometry, ctypes.c_size_t(len(geometry)), ctypes.byref(out), ctypes.byref(out_size), ) - self._parse_return_code(res, "create_wkb_from_gpkg_header") + self._parse_return_code(context, res, "create_wkb_from_gpkg_header") wkb = copy.deepcopy(out[: out_size.value]) return wkb @@ -600,22 +621,23 @@ def create_wkb_from_gpkg_header(self, geometry): class ChangesetReader(object): """Wrapper around GEODIFF_CR_* functions from C API""" - def __init__(self, geodiff, reader_ptr): + def __init__(self, geodiff, context, reader_ptr): self.geodiff = geodiff self.reader_ptr = reader_ptr + self.context = context def __del__(self): - self.geodiff._CR_destroy(self.geodiff.context, self.reader_ptr) + self.geodiff._CR_destroy(self.context, self.reader_ptr) def next_entry(self): ok = ctypes.c_bool() entry_ptr = self.geodiff._CR_nextEntry( - self.geodiff.context, self.reader_ptr, ctypes.byref(ok) + self.context, self.reader_ptr, ctypes.byref(ok) ) if not ok: raise GeoDiffLibError("Failed to read entry!") if entry_ptr is not None: - return ChangesetEntry(self.geodiff, entry_ptr) + return ChangesetEntry(self.geodiff, self.context, entry_ptr) else: return None @@ -640,49 +662,44 @@ class ChangesetEntry(object): OP_UPDATE = 23 OP_DELETE = 9 - def __init__(self, geodiff, entry_ptr): + def __init__(self, geodiff, context, entry_ptr): self.geodiff = geodiff self.entry_ptr = entry_ptr + self.context = context - self.operation = self.geodiff._CE_operation( - self.geodiff.context, self.entry_ptr - ) - self.values_count = self.geodiff._CE_count(self.geodiff.context, self.entry_ptr) + self.operation = self.geodiff._CE_operation(self.context, self.entry_ptr) + self.values_count = self.geodiff._CE_count(self.context, self.entry_ptr) if self.operation == self.OP_DELETE or self.operation == self.OP_UPDATE: self.old_values = [] for i in range(self.values_count): - v_ptr = self.geodiff._CE_old_value( - self.geodiff.context, self.entry_ptr, i - ) + v_ptr = self.geodiff._CE_old_value(self.context, self.entry_ptr, i) self.old_values.append(self._convert_value(v_ptr)) if self.operation == self.OP_INSERT or self.operation == self.OP_UPDATE: self.new_values = [] for i in range(self.values_count): - v_ptr = self.geodiff._CE_new_value( - self.geodiff.context, self.entry_ptr, i - ) + v_ptr = self.geodiff._CE_new_value(self.context, self.entry_ptr, i) self.new_values.append(self._convert_value(v_ptr)) - table = self.geodiff._CE_table(self.geodiff.context, entry_ptr) - self.table = ChangesetTable(geodiff, table) + table = self.geodiff._CE_table(self.context, entry_ptr) + self.table = ChangesetTable(geodiff, self.context, table) def __del__(self): - self.geodiff._CE_destroy(self.geodiff.context, self.entry_ptr) + self.geodiff._CE_destroy(self.context, self.entry_ptr) def _convert_value(self, v_ptr): - v_type = self.geodiff._V_type(self.geodiff.context, v_ptr) + v_type = self.geodiff._V_type(self.context, v_ptr) if v_type == 0: v_val = UndefinedValue() elif v_type == 1: - v_val = self.geodiff._V_get_int(self.geodiff.context, v_ptr) + v_val = self.geodiff._V_get_int(self.context, v_ptr) elif v_type == 2: - v_val = self.geodiff._V_get_double(self.geodiff.context, v_ptr) + v_val = self.geodiff._V_get_double(self.context, v_ptr) elif v_type == 3 or v_type == 4: # 3==text, 4==blob - size = self.geodiff._V_get_data_size(self.geodiff.context, v_ptr) + size = self.geodiff._V_get_data_size(self.context, v_ptr) buffer = ctypes.create_string_buffer(size) - self.geodiff._V_get_data(self.geodiff.context, v_ptr, buffer) + self.geodiff._V_get_data(self.context, v_ptr, buffer) v_val = buffer.raw if v_type == 3: v_val = v_val.decode("utf-8") @@ -690,27 +707,24 @@ def _convert_value(self, v_ptr): v_val = None else: raise GeoDiffLibError("unknown value type {}".format(v_type)) - self.geodiff._V_destroy(self.geodiff.context, v_ptr) + self.geodiff._V_destroy(self.context, v_ptr) return v_val class ChangesetTable(object): """Wrapper around GEODIFF_CT_* functions from C API""" - def __init__(self, geodiff, table_ptr): + def __init__(self, geodiff, context, table_ptr): self.geodiff = geodiff self.table_ptr = table_ptr + self.context = context - self.name = self.geodiff._CT_name(self.geodiff.context, table_ptr).decode( - "utf-8" - ) - self.column_count = self.geodiff._CT_column_count( - self.geodiff.context, table_ptr - ) + self.name = self.geodiff._CT_name(self.context, table_ptr).decode("utf-8") + self.column_count = self.geodiff._CT_column_count(self.context, table_ptr) self.column_is_pkey = [] for i in range(self.column_count): self.column_is_pkey.append( - self.geodiff._CT_column_is_pkey(self.geodiff.context, table_ptr, i) + self.geodiff._CT_column_is_pkey(self.context, table_ptr, i) ) diff --git a/pygeodiff/main.py b/pygeodiff/main.py index 9bd6f0e..489b3a8 100644 --- a/pygeodiff/main.py +++ b/pygeodiff/main.py @@ -7,6 +7,7 @@ :license: MIT, see LICENSE for more details. """ +import weakref from .geodifflib import GeoDiffLib @@ -15,14 +16,44 @@ class GeoDiff: geodiff is a module to create and apply changesets to GIS files (geopackage) """ + # Dictionary of libname to instance of GeoDiffLib + _clib_cache = weakref.WeakValueDictionary() + def __init__(self, libname=None): """ if libname is None, it tries to import c-extension from wheel - messages are shown in stdout/stderr. + messages are shown in stdout/stderr. C-Library and context is lazy-loaded. + Use environment variable GEODIFF_LOGGER_LEVEL 0(Nothing)-4(Debug) to set level (Errors by default) """ - self.clib = GeoDiffLib(libname) + self.libname = libname + self.clib = None + self.context = None + self.callbackLogger = None + + def __del__(self): + self.shutdown() + + def _lazy_load(self): + if self.clib is None: + clib = GeoDiff._clib_cache.get(self.libname) + if clib: + self.clib = clib + else: + self.clib = GeoDiffLib(self.libname) + GeoDiff._clib_cache[self.libname] = self.clib + + if self.context is None: + self.context = self.clib.create_context() + + def shutdown(self): + if self.context is not None: + self.clib.destroy_context(self.context) + self.context = None + + self.clib = None + self.callbackLogger = None def set_logger_callback(self, callback): """ @@ -31,7 +62,9 @@ def set_logger_callback(self, callback): When callback is None, no output is produced at all Callback function has 2 arguments: (int) errorCode, (string) msg """ - return self.clib.set_logger_callback(callback) + self._lazy_load() + self.callbackLogger = self.clib.set_logger_callback(self.context, callback) + return None def set_tables_to_skip(self, tables): """ @@ -42,7 +75,8 @@ def set_tables_to_skip(self, tables): If empty list is passed, skip tables list will be reset. """ - return self.clib.set_tables_to_skip(tables) + self._lazy_load() + return self.clib.set_tables_to_skip(self.context, tables) LevelError = 1 LevelWarning = 2 @@ -59,7 +93,8 @@ def set_maximum_logger_level(self, maxLevel): maxLogLevel = 3 errors, warnings and infos are passed to logger callback maxLogLevel = 4 errors, warnings, infos, debug messages are passed to logger callback """ - return self.clib.set_maximum_logger_level(maxLevel) + self._lazy_load() + return self.clib.set_maximum_logger_level(self.context, maxLevel) def drivers(self): """ @@ -67,13 +102,15 @@ def drivers(self): :raises GeoDiffLibError: raised on error """ - return self.clib.drivers() + self._lazy_load() + return self.clib.drivers(self.context) def driver_is_registered(self, name): """ Returns whether dataset with given name is registered (e.g. "sqlite" or "postgresql") """ - return self.clib.driver_is_registered(name) + self._lazy_load() + return self.clib.driver_is_registered(self.context, name) def create_changeset(self, base, modified, changeset): """ @@ -89,7 +126,8 @@ def create_changeset(self, base, modified, changeset): :raises GeoDiffLibError: raised on error """ - return self.clib.create_changeset(base, modified, changeset) + self._lazy_load() + return self.clib.create_changeset(self.context, base, modified, changeset) def invert_changeset(self, changeset, changeset_inv): """ @@ -104,7 +142,8 @@ def invert_changeset(self, changeset, changeset_inv): :raises GeoDiffLibError: raised on error """ - return self.clib.invert_changeset(changeset, changeset_inv) + self._lazy_load() + return self.clib.invert_changeset(self.context, changeset, changeset_inv) def rebase(self, base, modified_their, modified, conflict): """ @@ -130,7 +169,8 @@ def rebase(self, base, modified_their, modified, conflict): :raises GeoDiffLibError: raised on error """ - return self.clib.rebase(base, modified_their, modified, conflict) + self._lazy_load() + return self.clib.rebase(self.context, base, modified_their, modified, conflict) def create_rebased_changeset( self, base, modified, changeset_their, changeset, conflict @@ -154,8 +194,9 @@ def create_rebased_changeset( :raises GeoDiffLibError: raised on error """ + self._lazy_load() return self.clib.create_rebased_changeset( - base, modified, changeset_their, changeset, conflict + self.context, base, modified, changeset_their, changeset, conflict ) def apply_changeset(self, base, changeset): @@ -167,7 +208,8 @@ def apply_changeset(self, base, changeset): :returns: number of conflicts :raises GeoDiffLibError: raised on error """ - return self.clib.apply_changeset(base, changeset) + self._lazy_load() + return self.clib.apply_changeset(self.context, base, changeset) def list_changes(self, changeset, json): """ @@ -177,7 +219,8 @@ def list_changes(self, changeset, json): :returns: number of changes :raises GeoDiffLibError: raised on error """ - return self.clib.list_changes(changeset, json) + self._lazy_load() + return self.clib.list_changes(self.context, changeset, json) def list_changes_summary(self, changeset, json): """ @@ -188,21 +231,24 @@ def list_changes_summary(self, changeset, json): :returns: number of changes :raises GeoDiffLibError: raised on error """ - return self.clib.list_changes_summary(changeset, json) + self._lazy_load() + return self.clib.list_changes_summary(self.context, changeset, json) def has_changes(self, changeset): """ :returns: whether changeset contains at least one change :raises GeoDiffLibError: raised on error """ - return self.clib.has_changes(changeset) + self._lazy_load() + return self.clib.has_changes(self.context, changeset) def changes_count(self, changeset): """ :returns: number of changes :raises GeoDiffLibError: raised on error """ - return self.clib.changes_count(changeset) + self._lazy_load() + return self.clib.changes_count(self.context, changeset) def concat_changes(self, list_changesets, output_changeset): """ @@ -215,7 +261,8 @@ def concat_changes(self, list_changesets, output_changeset): :raises GeoDiffLibError: raised on error """ - return self.clib.concat_changes(list_changesets, output_changeset) + self._lazy_load() + return self.clib.concat_changes(self.context, list_changesets, output_changeset) def make_copy( self, driver_src, driver_src_info, src, driver_dst, driver_dst_info, dst @@ -238,8 +285,15 @@ def make_copy( :raises GeoDiffLibError: raised on error """ + self._lazy_load() return self.clib.make_copy( - driver_src, driver_src_info, src, driver_dst, driver_dst_info, dst + self.context, + driver_src, + driver_src_info, + src, + driver_dst, + driver_dst_info, + dst, ) def make_copy_sqlite(self, src, dst): @@ -253,7 +307,8 @@ def make_copy_sqlite(self, src, dst): :raises GeoDiffLibError: raised on error """ - return self.clib.make_copy_sqlite(src, dst) + self._lazy_load() + return self.clib.make_copy_sqlite(self.context, src, dst) def create_changeset_ex(self, driver, driver_info, base, modified, changeset): """ @@ -265,8 +320,9 @@ def create_changeset_ex(self, driver, driver_info, base, modified, changeset): :raises GeoDiffLibError: raised on error """ + self._lazy_load() return self.clib.create_changeset_ex( - driver, driver_info, base, modified, changeset + self.context, driver, driver_info, base, modified, changeset ) def create_changeset_dr( @@ -298,7 +354,9 @@ def create_changeset_dr( :param changeset: [output] changeset between SRC -> DST :raises GeoDiffLibError: raised on error """ + self._lazy_load() return self.clib.create_changeset_dr( + self.context, driver_src, driver_src_info, src, @@ -318,7 +376,10 @@ def apply_changeset_ex(self, driver, driver_info, base, changeset): :raises GeoDiffLibError: raised on error """ - return self.clib.apply_changeset_ex(driver, driver_info, base, changeset) + self._lazy_load() + return self.clib.apply_changeset_ex( + self.context, driver, driver_info, base, changeset + ) def create_rebased_changeset_ex( self, @@ -336,8 +397,16 @@ def create_rebased_changeset_ex( :raises GeoDiffLibError: raised on error """ + self._lazy_load() return self.clib.create_rebased_changeset_ex( - driver, driver_info, base, base2modified, base2their, rebased, conflict_file + self.context, + driver, + driver_info, + base, + base2modified, + base2their, + rebased, + conflict_file, ) def rebase_ex(self, driver, driver_info, base, modified, base2their, conflict_file): @@ -347,8 +416,9 @@ def rebase_ex(self, driver, driver_info, base, modified, base2their, conflict_fi :raises GeoDiffLibError: raised on error """ + self._lazy_load() return self.clib.rebase_ex( - driver, driver_info, base, modified, base2their, conflict_file + self.context, driver, driver_info, base, modified, base2their, conflict_file ) def dump_data(self, driver, driver_info, src, changeset): @@ -357,7 +427,8 @@ def dump_data(self, driver, driver_info, src, changeset): :raises GeoDiffLibError: raised on error """ - return self.clib.dump_data(driver, driver_info, src, changeset) + self._lazy_load() + return self.clib.dump_data(self.context, driver, driver_info, src, changeset) def schema(self, driver, driver_info, src, json): """ @@ -365,7 +436,8 @@ def schema(self, driver, driver_info, src, json): :raises GeoDiffLibError: raised on error """ - return self.clib.schema(driver, driver_info, src, json) + self._lazy_load() + return self.clib.schema(self.context, driver, driver_info, src, json) def read_changeset(self, changeset): """ @@ -374,19 +446,22 @@ def read_changeset(self, changeset): :returns: reader object :raises GeoDiffLibError: raised on error """ - return self.clib.read_changeset(changeset) + self._lazy_load() + return self.clib.read_changeset(self.context, changeset) def version(self): """ geodiff version """ + self._lazy_load() return self.clib.version() def create_wkb_from_gpkg_header(self, geometry): """ Extracts geometry in WKB format from the geometry encoded according to GeoPackage spec. """ - return self.clib.create_wkb_from_gpkg_header(geometry) + self._lazy_load() + return self.clib.create_wkb_from_gpkg_header(self.context, geometry) def main(): diff --git a/setup.py b/setup.py index dc2ef20..a1a987f 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import platform # use scripts/update_version.py to update the version here and in other places at once -VERSION = "2.0.4" +VERSION = "2.1.0" cmake_args = [ '-DENABLE_TESTS:BOOL=OFF',