{"action":"created","comment":{"url":"https://api.github.com/repos/mapproxy/mapproxy/pulls/comments/70447462","id":70447462,"diff_hunk":"@@ -0,0 +1,657 @@\n+# This file is part of the MapProxy project.\n+# Copyright (C) 2011-2013 Omniscale <http://omniscale.de>\n+\n+# Licensed under the Apache License, Version 2.0 (the \"License\");\n+# you may not use this file except in compliance with the License.\n+# You may obtain a copy of the License at\n+#\n+#    http://www.apache.org/licenses/LICENSE-2.0\n+#\n+# Unless required by applicable law or agreed to in writing, software\n+# distributed under the License is distributed on an \"AS IS\" BASIS,\n+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+# See the License for the specific language governing permissions and\n+# limitations under the License.\n+\n+from __future__ import with_statement\n+import hashlib\n+import os\n+import sqlite3\n+import threading\n+\n+from mapproxy.image import ImageSource\n+from mapproxy.cache.base import TileCacheBase, tile_buffer\n+from mapproxy.util.fs import ensure_directory\n+from mapproxy.util.lock import FileLock\n+from mapproxy.compat import BytesIO, PY2\n+from mapproxy.srs import get_epsg_num\n+\n+import logging\n+\n+log = logging.getLogger(__name__)\n+\n+class GeopackageCache(TileCacheBase):\n+    supports_timestamp = False\n+\n+    def __init__(self, geopackage_file, grid_conf, table_name, with_timestamps=False):\n+        self.grid_conf = grid_conf\n+        self.table_name = self._check_table_name(table_name)\n+        self.lock_cache_id = 'gpkg' + hashlib.md5(geopackage_file.encode('utf-8')).hexdigest()\n+        self.geopackage_file = geopackage_file\n+        self.supports_timestamp = with_timestamps\n+        self.ensure_gpkg()\n+        self._db_conn_cache = threading.local()\n+\n+    @property\n+    def db(self):\n+        if not getattr(self._db_conn_cache, 'db', None):\n+            self.ensure_gpkg()\n+            self._db_conn_cache.db = sqlite3.connect(self.geopackage_file)\n+        return self._db_conn_cache.db\n+\n+    def cleanup(self):\n+        \"\"\"\n+        Close all open connection and remove them from cache.\n+        \"\"\"\n+        if getattr(self._db_conn_cache, 'db', None):\n+            self._db_conn_cache.db.close()\n+        self._db_conn_cache.db = None\n+\n+    @staticmethod\n+    def _check_table_name(table_name):\n+        \"\"\"\n+        >>> GeopackageCache._check_table_name(\"test\")\n+        'test'\n+        >>> GeopackageCache._check_table_name(\"test_2\")\n+        'test_2'\n+        >>> GeopackageCache._check_table_name(\"test3;\")\n+        Traceback (most recent call last):\n+        ...\n+        ValueError: The table_name test3; contains unsupported characters.\n+        >>> GeopackageCache._check_table_name(\"table name\")\n+        Traceback (most recent call last):\n+        ...\n+        ValueError: The table_name table name contains unsupported characters.\n+\n+        @param table_name: A desired name for an geopackage table table.\n+        @return: The name of the table if it is good, other wise and exception.\n+        \"\"\"\n+\n+        if is_alnum(table_name):\n+            return table_name\n+        else:\n+            log.info(\"The table name may only contain alphanumeric characters or an underscore.\")\n+            raise ValueError(\"The table_name {0} contains unsupported characters.\".format(table_name))\n+\n+    def ensure_gpkg(self):\n+        if not os.path.isfile(self.geopackage_file):\n+            with FileLock(os.path.join(os.path.dirname(self.geopackage_file), 'init.lck'),\n+                          remove_on_unlock=True):\n+                ensure_directory(self.geopackage_file)\n+                self._initialize_gpkg()\n+        else:\n+            if not self.check_gpkg():\n+                ensure_directory(self.geopackage_file)\n+                self._initialize_gpkg()\n+\n+    def check_gpkg(self):\n+        if not self._verify_table():\n+            print(\"table invalid\")\n+            return False\n+        if not self._verify_gpkg_contents():\n+            print(\"gpkg_contents invalid\")\n+            return False\n+        if not self._verify_tile_size():\n+            print(\"tile_size invalid\")\n+            return False\n+        return True\n+\n+    def _verify_table(self):\n+        with sqlite3.connect(self.geopackage_file) as db:\n+            cur = db.execute(\"\"\"SELECT name FROM sqlite_master WHERE type='table' AND name=?\"\"\",\n+                             (self.table_name,))\n+            content = cur.fetchone()\n+            if not content:\n+                # Table doesn't exist _initialize_gpkg will create a new one.\n+                return False\n+            return True\n+\n+    def _verify_gpkg_contents(self):\n+        with sqlite3.connect(self.geopackage_file) as db:\n+            try:\n+                cur = db.execute(\"\"\"SELECT table_name, data_type, srs_id FROM gpkg_contents WHERE table_name = ?\"\"\"\n+                                 , (self.table_name,))\n+            except sqlite3.OperationalError as oe:\n+                if 'no such column' in str(oe):\n+                    return False\n+                else:\n+                    raise\n+\n+        results = cur.fetchone()\n+        if not results:\n+            # Table doesn't exist in gpkg_contents _initialize_gpkg will add it.\n+            return False\n+        gpkg_table_name, gpkg_data_type, gpkg_srs_id = results\n+        try:\n+            cur = db.execute(\"\"\"SELECT organization_coordsys_id FROM gpkg_spatial_ref_sys WHERE srs_id = ?\"\"\"\n+                             , (gpkg_srs_id,))\n+        except sqlite3.OperationalError as oe:\n+            if 'no such column' in str(oe):\n+                return False\n+            else:\n+                raise\n+\n+        gpkg_coordsys_id = cur.fetchone()[0]\n+        if gpkg_data_type.lower() != \"tiles\":\n+            log.info(\"The geopackage table name already exists for a data type other than tiles.\")\n+            raise ValueError(\"table_name is improperly configured.\")\n+        if gpkg_coordsys_id != get_epsg_num(self.grid_conf.conf.get('srs')):\n+            log.info(\n+                \"The geopackage {0} table name {1} already exists and has an SRS of {2}, which does not match the configured\" \\\n+                \" Mapproxy SRS of {3}.\".format(self.geopackage_file, self.table_name, gpkg_coordsys_id,\n+                                              get_epsg_num(self.grid_conf.conf.get('srs'))))\n+            raise ValueError(\"srs is improperly configured.\")\n+        return True\n+\n+    def _verify_tile_size(self):\n+        with sqlite3.connect(self.geopackage_file) as db:\n+            try:\n+                cur = db.execute(\n+                    \"\"\"SELECT table_name, zoom_level, tile_width, tile_height, pixel_x_size, pixel_y_size\\\n+                      FROM gpkg_tile_matrix WHERE table_name = ?\"\"\",\n+                    (self.table_name,))\n+            except sqlite3.OperationalError as oe:\n+                if 'no such column' in str(oe):\n+                    return False\n+                else:\n+                    raise\n+\n+        results = cur.fetchall()\n+        count = len(results)\n+        results = results[0]\n+        tile_size = self.grid_conf.tile_grid().tile_size\n+\n+        if not results:\n+            # There is no tile conflict. Return to allow the creation of new tiles.\n+            return True\n+\n+        gpkg_table_name, gpkg_zoom_level, gpkg_tile_width, gpkg_tile_height, gpkg_pixel_x_size, gpkg_pixel_y_size = results\n+        resolution = self.grid_conf.tile_grid().resolution(gpkg_zoom_level)\n+        if gpkg_tile_width != tile_size[0] or gpkg_tile_height != tile_size[1]:\n+            log.info(\n+                \"The geopackage {0} table name {1} already exists and has tile sizes of ({2},{3})\"\n+                \" which is different than the configure tile sizes of ({4},{5}).\".format(self.geopackage_file,\n+                                                                                       self.table_name,\n+                                                                                       gpkg_tile_width,\n+                                                                                       gpkg_tile_height,\n+                                                                                       tile_size[0],\n+                                                                                       tile_size[1]))\n+            log.info(\"The current mapproxy configuration is invalid for this geopackage.\")\n+            raise ValueError(\"tile_size is improperly configured.\")\n+        if not isclose(gpkg_pixel_x_size, resolution) or not isclose(gpkg_pixel_y_size, resolution):\n+            log.info(\n+                \"The geopackage {0} table name {1} already exists and level {2} a resolution of ({3:.13f},{4:.13f})\"\n+                \" which is different than the configured resolution of ({5:.13f},{6:.13f}).\".format(self.geopackage_file,\n+                                                                                                  self.table_name,\n+                                                                                                  gpkg_zoom_level,\n+                                                                                                  gpkg_pixel_x_size,\n+                                                                                                  gpkg_pixel_y_size,\n+                                                                                                  resolution,\n+                                                                                                  resolution))\n+            log.info(\"The current mapproxy configuration is invalid for this geopackage.\")\n+            raise ValueError(\"res is improperly configured.\")\n+        return True\n+\n+    def _initialize_gpkg(self):\n+        log.info('initializing Geopackage file %s', self.geopackage_file)\n+        db = sqlite3.connect(self.geopackage_file)\n+        self.tile_grid = self.grid_conf.tile_grid()\n+        proj = get_epsg_num(self.grid_conf.conf.get('srs'))\n+        stmts = [\"\"\"\n+                CREATE TABLE IF NOT EXISTS gpkg_contents\n+                    (table_name  TEXT     NOT NULL PRIMARY KEY,                                    -- The name of the tiles, or feature table\n+                     data_type   TEXT     NOT NULL,                                                -- Type of data stored in the table: \"features\" per clause Features (http://www.geopackage.org/spec/#features), \"tiles\" per clause Tiles (http://www.geopackage.org/spec/#tiles), or an implementer-defined value for other data tables per clause in an Extended GeoPackage\n+                     identifier  TEXT     UNIQUE,                                                  -- A human-readable identifier (e.g. short name) for the table_name content\n+                     description TEXT     DEFAULT '',                                              -- A human-readable description for the table_name content\n+                     last_change DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), -- Timestamp value in ISO 8601 format as defined by the strftime function %Y-%m-%dT%H:%M:%fZ format string applied to the current time\n+                     min_x       DOUBLE,                                                           -- Bounding box minimum easting or longitude for all content in table_name\n+                     min_y       DOUBLE,                                                           -- Bounding box minimum northing or latitude for all content in table_name\n+                     max_x       DOUBLE,                                                           -- Bounding box maximum easting or longitude for all content in table_name\n+                     max_y       DOUBLE,                                                           -- Bounding box maximum northing or latitude for all content in table_name\n+                     srs_id      INTEGER,                                                          -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id; when data_type is features, SHALL also match gpkg_geometry_columns.srs_id; When data_type is tiles, SHALL also match gpkg_tile_matrix_set.srs.id\n+                     CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id))\n+                \"\"\",\n+                 \"\"\"\n+                 CREATE TABLE IF NOT EXISTS gpkg_spatial_ref_sys\n+                     (srs_name                 TEXT    NOT NULL,             -- Human readable name of this SRS (Spatial Reference System)\n+                      srs_id                   INTEGER NOT NULL PRIMARY KEY, -- Unique identifier for each Spatial Reference System within a GeoPackage\n+                      organization             TEXT    NOT NULL,             -- Case-insensitive name of the defining organization e.g. EPSG or epsg\n+                      organization_coordsys_id INTEGER NOT NULL,             -- Numeric ID of the Spatial Reference System assigned by the organization\n+                      definition               TEXT    NOT NULL,             -- Well-known Text representation of the Spatial Reference System\n+                      description              TEXT)\n+                  \"\"\",\n+                 \"\"\"\n+                 CREATE TABLE IF NOT EXISTS gpkg_tile_matrix\n+                     (table_name    TEXT    NOT NULL, -- Tile Pyramid User Data Table Name\n+                      zoom_level    INTEGER NOT NULL, -- 0 <= zoom_level <= max_level for table_name\n+                      matrix_width  INTEGER NOT NULL, -- Number of columns (>= 1) in tile matrix at this zoom level\n+                      matrix_height INTEGER NOT NULL, -- Number of rows (>= 1) in tile matrix at this zoom level\n+                      tile_width    INTEGER NOT NULL, -- Tile width in pixels (>= 1) for this zoom level\n+                      tile_height   INTEGER NOT NULL, -- Tile height in pixels (>= 1) for this zoom level\n+                      pixel_x_size  DOUBLE  NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0)\n+                      pixel_y_size  DOUBLE  NOT NULL, -- In t_table_name srid units or default meters for srid 0 (>0)\n+                      CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level), CONSTRAINT fk_tmm_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name))\n+                  \"\"\",\n+                 \"\"\"\n+                         CREATE TABLE IF NOT EXISTS gpkg_tile_matrix_set\n+                 (table_name TEXT    NOT NULL PRIMARY KEY, -- Tile Pyramid User Data Table Name\n+                  srs_id     INTEGER NOT NULL,             -- Spatial Reference System ID: gpkg_spatial_ref_sys.srs_id\n+                  min_x      DOUBLE  NOT NULL,             -- Bounding box minimum easting or longitude for all content in table_name\n+                  min_y      DOUBLE  NOT NULL,             -- Bounding box minimum northing or latitude for all content in table_name\n+                  max_x      DOUBLE  NOT NULL,             -- Bounding box maximum easting or longitude for all content in table_name\n+                  max_y      DOUBLE  NOT NULL,             -- Bounding box maximum northing or latitude for all content in table_name\n+                  CONSTRAINT fk_gtms_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name), CONSTRAINT fk_gtms_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys (srs_id))\n+                  \"\"\",\n+                 \"\"\"\n+                 CREATE TABLE IF NOT EXISTS {0}\n+                    (id          INTEGER PRIMARY KEY AUTOINCREMENT, -- Autoincrement primary key\n+                     zoom_level  INTEGER NOT NULL,                  -- min(zoom_level) <= zoom_level <= max(zoom_level) for t_table_name\n+                     tile_column INTEGER NOT NULL,                  -- 0 to tile_matrix matrix_width - 1\n+                     tile_row    INTEGER NOT NULL,                  -- 0 to tile_matrix matrix_height - 1\n+                     tile_data   BLOB    NOT NULL,                  -- Of an image MIME type specified in clauses Tile Encoding PNG, Tile Encoding JPEG, Tile Encoding WEBP\n+                     UNIQUE (zoom_level, tile_column, tile_row))\n+                  \"\"\".format(self.table_name)\n+                 ]\n+\n+        for stmt in stmts:\n+            db.execute(stmt)\n+\n+        db.execute(\"PRAGMA foreign_keys = 1;\")\n+\n+        # List of WKT execute statements and data.\n+        wkts = [(\"\"\"\n+                INSERT OR REPLACE INTO gpkg_spatial_ref_sys (\n+                    srs_id,\n+                    organization,\n+                    organization_coordsys_id,\n+                    srs_name,\n+                    definition)\n+                VALUES (3857, 'epsg', 3857, 'WGS 84 / Pseudo-Mercator', ?)\n+            \"\"\",\n+                 \"\"\"\n+ PROJCS[\"WGS 84 / Pseudo-Mercator\",GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,\\\n+ AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],\\\n+ UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"9122\"]]AUTHORITY[\"EPSG\",\"4326\"]],\\\n+ PROJECTION[\"Mercator_1SP\"],PARAMETER[\"central_meridian\",0],PARAMETER[\"scale_factor\",1],PARAMETER[\"false_easting\",0],\\\n+ PARAMETER[\"false_northing\",0],UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],AXIS[\"X\",EAST],AXIS[\"Y\",NORTH]\\\n+                 \"\"\"\n+                 ),\n+                (\n+                    \"\"\"\n+                INSERT OR REPLACE INTO gpkg_spatial_ref_sys (\n+                        srs_id,\n+                        organization,\n+                        organization_coordsys_id,\n+                        srs_name,\n+                        definition)\n+                    VALUES (4326, 'epsg', 4326, 'WGS 84', ?)\n+                    \"\"\",\n+                    \"\"\"\n+GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],\\\n+AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433,\\\n+AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]]\\\n+                    \"\"\"\n+                ), (\n+                    \"\"\"\n+                    INSERT OR REPLACE INTO gpkg_spatial_ref_sys (\n+                        srs_id,\n+                        organization,\n+                        organization_coordsys_id,\n+                        srs_name,\n+                        definition)\n+                    VALUES (-1, 'NONE', -1, ' ', ?)\n+                    \"\"\",\n+                    \"undefined\"\n+                ), (\n+                    \"\"\"\n+                    INSERT OR REPLACE INTO gpkg_spatial_ref_sys (\n+                        srs_id,\n+                        organization,\n+                        organization_coordsys_id,\n+                        srs_name,\n+                        definition)\n+                    VALUES (0, 'NONE', 0, ' ', ?)\n+                    \"\"\"\n+                    ,\n+                    \"undefined\"\n+                )\n+                ]\n+\n+        if get_epsg_num(self.grid_conf.conf.get('srs')) not in [4326, 3857]:\n+            wkts.append((\"\"\"\n+                    INSERT OR REPLACE INTO gpkg_spatial_ref_sys (\n+                        srs_id,\n+                        organization,\n+                        organization_coordsys_id,\n+                        srs_name,\n+                        definition)\n+                    VALUES ({0}, 'epsg', {1}, 'Not provided', ?)\n+                \"\"\".format(proj, proj),\n+                         \"Added via Mapproxy.\"))\n+        db.commit()\n+\n+        # Add geopackage version to the header (1.0)\n+        db.execute(\"PRAGMA application_id = 1196437808;\")\n+        db.commit()\n+\n+        for wkt in wkts:\n+            try:\n+                db.execute(wkt[0], (wkt[1],))\n+            except sqlite3.IntegrityError:\n+                log.info(\"srs_id already exists.\".format(wkt[0]))\n+        db.commit()\n+\n+        # Ensure that tile table exists here, don't overwrite a valid entry.\n+        try:\n+            db.execute(\"\"\"\n+                        INSERT INTO gpkg_contents (\n+                            table_name,\n+                            data_type,\n+                            identifier,\n+                            description,\n+                            min_x,\n+                            max_x,\n+                            min_y,\n+                            max_y,\n+                            srs_id)\n+                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);\n+                        \"\"\", (self.table_name,\n+                              \"tiles\",\n+                              self.table_name,\n+                              \"Created with Mapproxy.\",\n+                              self.tile_grid.bbox[0],\n+                              self.tile_grid.bbox[2],\n+                              self.tile_grid.bbox[1],\n+                              self.tile_grid.bbox[3],\n+                              proj))\n+        except sqlite3.IntegrityError:\n+            pass\n+        db.commit()\n+\n+        # Ensure that tile set exists here, don't overwrite a valid entry.\n+        try:\n+            db.execute(\"\"\"\n+                INSERT INTO gpkg_tile_matrix_set (\n+                    table_name,\n+                    srs_id,\n+                    min_x,\n+                    max_x,\n+                    min_y,\n+                    max_y)\n+                VALUES (\n+                    ?,\n+                    ?,\n+                    ?,\n+                    ?,\n+                    ?,\n+                    ?);\n+            \"\"\", (\n+                self.table_name, proj, self.tile_grid.bbox[0], self.tile_grid.bbox[2], self.tile_grid.bbox[1],\n+                self.tile_grid.bbox[3]))\n+        except sqlite3.IntegrityError:\n+            pass\n+        db.commit()\n+\n+        tile_size = self.grid_conf.tile_grid().tile_size\n+        for grid, resolution, level in zip(self.grid_conf.tile_grid().grid_sizes,\n+                                           self.grid_conf.tile_grid().resolutions, range(20)):\n+            db.execute(\"\"\"INSERT OR REPLACE INTO gpkg_tile_matrix\n+                              (table_name, zoom_level, matrix_width, matrix_height, tile_width, tile_height, pixel_x_size, pixel_y_size)\n+                              VALUES(?, ?, ?, ?, ?, ?, ?, ?)\n+                              \"\"\",\n+                       (self.table_name, level, grid[0], grid[1], tile_size[0], tile_size[1], resolution, resolution))\n+        db.commit()\n+        db.close()\n+\n+    def is_cached(self, tile):\n+        if tile.coord is None:\n+            return True\n+        if tile.source:\n+            return True\n+\n+        return self.load_tile(tile)\n+\n+    def store_tile(self, tile, grid=None):\n+\n+        if tile.stored:\n+            return True\n+\n+        with tile_buffer(tile) as buf:\n+            if PY2:\n+                content = buffer(buf.read())\n+            else:\n+                content = buf.read()\n+            x, y, level = tile.coord\n+            cursor = self.db.cursor()\n+            try:\n+                stmt = \"INSERT OR REPLACE INTO {0} (zoom_level, tile_column, tile_row, tile_data) VALUES (?,?,?,?)\".format(\n+                    self.table_name)\n+                cursor.execute(stmt, (level, x, y, content))\n+                self.db.commit()\n+            except sqlite3.OperationalError as ex:\n+                log.warn('unable to store tile: %s', ex)\n+                return False\n+            return True\n+\n+    def load_tile(self, tile, with_metadata=False):\n+        if tile.source or tile.coord is None:\n+            return True\n+\n+        cur = self.db.cursor()\n+        cur.execute(\"\"\"SELECT tile_data FROM {0}\n+                WHERE tile_column = ? AND\n+                      tile_row = ? AND\n+                      zoom_level = ?\"\"\".format(self.table_name), tile.coord)\n+\n+        content = cur.fetchone()\n+        if content:\n+            tile.source = ImageSource(BytesIO(content[0]))\n+            return True\n+        else:\n+            return False\n+\n+    def load_tiles(self, tiles, with_metadata=False):\n+        # associate the right tiles with the cursor\n+        tile_dict = {}\n+        coords = []\n+        for tile in tiles:\n+            if tile.source or tile.coord is None:\n+                continue\n+            x, y, level = tile.coord\n+            coords.append(x)\n+            coords.append(y)\n+            coords.append(level)\n+            tile_dict[(x, y)] = tile\n+\n+        if not tile_dict:\n+            # all tiles loaded or coords are None\n+            return True\n+\n+        stmt_base = \"SELECT tile_column, tile_row, tile_data FROM {0} WHERE \".format(self.table_name)\n+\n+        loaded_tiles = 0\n+\n+        # SQLite is limited to 1000 args -> split into multiple requests if more arguments are needed\n+        while coords:\n+            cur_coords = coords[:999]\n+\n+            stmt = stmt_base + ' OR '.join(\n+                ['(tile_column = ? AND tile_row = ? AND zoom_level = ?)'] * (len(cur_coords) // 3))\n+\n+            cursor = self.db.cursor()\n+            cursor.execute(stmt, cur_coords)\n+\n+            for row in cursor:\n+                loaded_tiles += 1\n+                tile = tile_dict[(row[0], row[1])]\n+                data = row[2]\n+                tile.size = len(data)\n+                tile.source = ImageSource(BytesIO(data))\n+            cursor.close()\n+\n+            coords = coords[999:]\n+\n+        return loaded_tiles == len(tile_dict)\n+\n+    def remove_tile(self, tile):\n+        cursor = self.db.cursor()\n+        cursor.execute(\n+            \"DELETE FROM {0} WHERE (tile_column = ? AND tile_row = ? AND zoom_level = ?)\".format(self.table_name),\n+            tile.coord)\n+        self.db.commit()\n+        if cursor.rowcount:\n+            return True\n+        return False\n+\n+    def remove_level_tiles_before(self, level, timestamp):\n+        if timestamp == 0:\n+            cursor = self.db.cursor()\n+            cursor.execute(\n+                \"DELETE FROM {0} WHERE (zoom_level = ?)\".format(self.table_name), (level,))\n+            self.db.commit()\n+            log.info(\"Cursor rowcount = {0}\".format(cursor.rowcount))\n+            if cursor.rowcount:\n+                return True\n+            return False\n+\n+    def load_tile_metadata(self, tile):\n+        self.load_tile(tile)\n+\n+\n+class GeopackageLevelCache(TileCacheBase):","path":"mapproxy/cache/geopackage.py","position":531,"original_position":531,"commit_id":"6cd6e5d19cd7c313b0392335b6d2562295d9358e","original_commit_id":"6cd6e5d19cd7c313b0392335b6d2562295d9358e","user":{"login":"olt","id":235174,"avatar_url":"https://avatars.githubusercontent.com/u/235174?v=3","gravatar_id":"","url":"https://api.github.com/users/olt","html_url":"https://github.com/olt","followers_url":"https://api.github.com/users/olt/followers","following_url":"https://api.github.com/users/olt/following{/other_user}","gists_url":"https://api.github.com/users/olt/gists{/gist_id}","starred_url":"https://api.github.com/users/olt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/olt/subscriptions","organizations_url":"https://api.github.com/users/olt/orgs","repos_url":"https://api.github.com/users/olt/repos","events_url":"https://api.github.com/users/olt/events{/privacy}","received_events_url":"https://api.github.com/users/olt/received_events","type":"User","site_admin":false},"body":"GeopackageLevelCache is only used in the tests?","created_at":"2016-07-12T14:19:45Z","updated_at":"2016-07-12T14:19:45Z","html_url":"https://github.com/mapproxy/mapproxy/pull/232#discussion_r70447462","pull_request_url":"https://api.github.com/repos/mapproxy/mapproxy/pulls/232","_links":{"self":{"href":"https://api.github.com/repos/mapproxy/mapproxy/pulls/comments/70447462"},"html":{"href":"https://github.com/mapproxy/mapproxy/pull/232#discussion_r70447462"},"pull_request":{"href":"https://api.github.com/repos/mapproxy/mapproxy/pulls/232"}}},"pull_request":{"url":"https://api.github.com/repos/mapproxy/mapproxy/pulls/232","id":73626763,"html_url":"https://github.com/mapproxy/mapproxy/pull/232","diff_url":"https://github.com/mapproxy/mapproxy/pull/232.diff","patch_url":"https://github.com/mapproxy/mapproxy/pull/232.patch","issue_url":"https://api.github.com/repos/mapproxy/mapproxy/issues/232","number":232,"state":"open","locked":false,"title":"Add geopackage cache","user":{"login":"jsvrcek","id":6437951,"avatar_url":"https://avatars.githubusercontent.com/u/6437951?v=3","gravatar_id":"","url":"https://api.github.com/users/jsvrcek","html_url":"https://github.com/jsvrcek","followers_url":"https://api.github.com/users/jsvrcek/followers","following_url":"https://api.github.com/users/jsvrcek/following{/other_user}","gists_url":"https://api.github.com/users/jsvrcek/gists{/gist_id}","starred_url":"https://api.github.com/users/jsvrcek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jsvrcek/subscriptions","organizations_url":"https://api.github.com/users/jsvrcek/orgs","repos_url":"https://api.github.com/users/jsvrcek/repos","events_url":"https://api.github.com/users/jsvrcek/events{/privacy}","received_events_url":"https://api.github.com/users/jsvrcek/received_events","type":"User","site_admin":false},"body":"Added a geopackage cache backend, referring to some work S. Lander did.  It uses the MBTiles as a basis, and then just adds the schemas and some checks to verify the datasets.\r\n\r\nI also updated the documentation and added a configuration example.\r\n\r\nLet me know what you think, or if I am missing something. Thanks.","created_at":"2016-06-13T19:07:46Z","updated_at":"2016-07-12T14:19:45Z","closed_at":null,"merged_at":null,"merge_commit_sha":"6c9345a396f2d2c569cd3214659f7376949b02fb","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/mapproxy/mapproxy/pulls/232/commits","review_comments_url":"https://api.github.com/repos/mapproxy/mapproxy/pulls/232/comments","review_comment_url":"https://api.github.com/repos/mapproxy/mapproxy/pulls/comments{/number}","comments_url":"https://api.github.com/repos/mapproxy/mapproxy/issues/232/comments","statuses_url":"https://api.github.com/repos/mapproxy/mapproxy/statuses/6cd6e5d19cd7c313b0392335b6d2562295d9358e","head":{"label":"terranodo:addGeopackageCache","ref":"addGeopackageCache","sha":"6cd6e5d19cd7c313b0392335b6d2562295d9358e","user":{"login":"terranodo","id":12091882,"avatar_url":"https://avatars.githubusercontent.com/u/12091882?v=3","gravatar_id":"","url":"https://api.github.com/users/terranodo","html_url":"https://github.com/terranodo","followers_url":"https://api.github.com/users/terranodo/followers","following_url":"https://api.github.com/users/terranodo/following{/other_user}","gists_url":"https://api.github.com/users/terranodo/gists{/gist_id}","starred_url":"https://api.github.com/users/terranodo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/terranodo/subscriptions","organizations_url":"https://api.github.com/users/terranodo/orgs","repos_url":"https://api.github.com/users/terranodo/repos","events_url":"https://api.github.com/users/terranodo/events{/privacy}","received_events_url":"https://api.github.com/users/terranodo/received_events","type":"Organization","site_admin":false},"repo":{"id":60358042,"name":"mapproxy","full_name":"terranodo/mapproxy","owner":{"login":"terranodo","id":12091882,"avatar_url":"https://avatars.githubusercontent.com/u/12091882?v=3","gravatar_id":"","url":"https://api.github.com/users/terranodo","html_url":"https://github.com/terranodo","followers_url":"https://api.github.com/users/terranodo/followers","following_url":"https://api.github.com/users/terranodo/following{/other_user}","gists_url":"https://api.github.com/users/terranodo/gists{/gist_id}","starred_url":"https://api.github.com/users/terranodo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/terranodo/subscriptions","organizations_url":"https://api.github.com/users/terranodo/orgs","repos_url":"https://api.github.com/users/terranodo/repos","events_url":"https://api.github.com/users/terranodo/events{/privacy}","received_events_url":"https://api.github.com/users/terranodo/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/terranodo/mapproxy","description":"MapProxy is a tile cache and WMS proxy","fork":true,"url":"https://api.github.com/repos/terranodo/mapproxy","forks_url":"https://api.github.com/repos/terranodo/mapproxy/forks","keys_url":"https://api.github.com/repos/terranodo/mapproxy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/terranodo/mapproxy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/terranodo/mapproxy/teams","hooks_url":"https://api.github.com/repos/terranodo/mapproxy/hooks","issue_events_url":"https://api.github.com/repos/terranodo/mapproxy/issues/events{/number}","events_url":"https://api.github.com/repos/terranodo/mapproxy/events","assignees_url":"https://api.github.com/repos/terranodo/mapproxy/assignees{/user}","branches_url":"https://api.github.com/repos/terranodo/mapproxy/branches{/branch}","tags_url":"https://api.github.com/repos/terranodo/mapproxy/tags","blobs_url":"https://api.github.com/repos/terranodo/mapproxy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/terranodo/mapproxy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/terranodo/mapproxy/git/refs{/sha}","trees_url":"https://api.github.com/repos/terranodo/mapproxy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/terranodo/mapproxy/statuses/{sha}","languages_url":"https://api.github.com/repos/terranodo/mapproxy/languages","stargazers_url":"https://api.github.com/repos/terranodo/mapproxy/stargazers","contributors_url":"https://api.github.com/repos/terranodo/mapproxy/contributors","subscribers_url":"https://api.github.com/repos/terranodo/mapproxy/subscribers","subscription_url":"https://api.github.com/repos/terranodo/mapproxy/subscription","commits_url":"https://api.github.com/repos/terranodo/mapproxy/commits{/sha}","git_commits_url":"https://api.github.com/repos/terranodo/mapproxy/git/commits{/sha}","comments_url":"https://api.github.com/repos/terranodo/mapproxy/comments{/number}","issue_comment_url":"https://api.github.com/repos/terranodo/mapproxy/issues/comments{/number}","contents_url":"https://api.github.com/repos/terranodo/mapproxy/contents/{+path}","compare_url":"https://api.github.com/repos/terranodo/mapproxy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/terranodo/mapproxy/merges","archive_url":"https://api.github.com/repos/terranodo/mapproxy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/terranodo/mapproxy/downloads","issues_url":"https://api.github.com/repos/terranodo/mapproxy/issues{/number}","pulls_url":"https://api.github.com/repos/terranodo/mapproxy/pulls{/number}","milestones_url":"https://api.github.com/repos/terranodo/mapproxy/milestones{/number}","notifications_url":"https://api.github.com/repos/terranodo/mapproxy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/terranodo/mapproxy/labels{/name}","releases_url":"https://api.github.com/repos/terranodo/mapproxy/releases{/id}","deployments_url":"https://api.github.com/repos/terranodo/mapproxy/deployments","created_at":"2016-06-03T15:25:39Z","updated_at":"2016-06-03T15:25:41Z","pushed_at":"2016-07-05T02:03:06Z","git_url":"git://github.com/terranodo/mapproxy.git","ssh_url":"git@github.com:terranodo/mapproxy.git","clone_url":"https://github.com/terranodo/mapproxy.git","svn_url":"https://github.com/terranodo/mapproxy","homepage":"http://mapproxy.org","size":8938,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"mapproxy:master","ref":"master","sha":"312ba32f50d99eb3e22e9038d4c63c7da8e99720","user":{"login":"mapproxy","id":1352978,"avatar_url":"https://avatars.githubusercontent.com/u/1352978?v=3","gravatar_id":"","url":"https://api.github.com/users/mapproxy","html_url":"https://github.com/mapproxy","followers_url":"https://api.github.com/users/mapproxy/followers","following_url":"https://api.github.com/users/mapproxy/following{/other_user}","gists_url":"https://api.github.com/users/mapproxy/gists{/gist_id}","starred_url":"https://api.github.com/users/mapproxy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mapproxy/subscriptions","organizations_url":"https://api.github.com/users/mapproxy/orgs","repos_url":"https://api.github.com/users/mapproxy/repos","events_url":"https://api.github.com/users/mapproxy/events{/privacy}","received_events_url":"https://api.github.com/users/mapproxy/received_events","type":"Organization","site_admin":false},"repo":{"id":4110611,"name":"mapproxy","full_name":"mapproxy/mapproxy","owner":{"login":"mapproxy","id":1352978,"avatar_url":"https://avatars.githubusercontent.com/u/1352978?v=3","gravatar_id":"","url":"https://api.github.com/users/mapproxy","html_url":"https://github.com/mapproxy","followers_url":"https://api.github.com/users/mapproxy/followers","following_url":"https://api.github.com/users/mapproxy/following{/other_user}","gists_url":"https://api.github.com/users/mapproxy/gists{/gist_id}","starred_url":"https://api.github.com/users/mapproxy/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mapproxy/subscriptions","organizations_url":"https://api.github.com/users/mapproxy/orgs","repos_url":"https://api.github.com/users/mapproxy/repos","events_url":"https://api.github.com/users/mapproxy/events{/privacy}","received_events_url":"https://api.github.com/users/mapproxy/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/mapproxy/mapproxy","description":"MapProxy is a tile cache and WMS proxy","fork":false,"url":"https://api.github.com/repos/mapproxy/mapproxy","forks_url":"https://api.github.com/repos/mapproxy/mapproxy/forks","keys_url":"https://api.github.com/repos/mapproxy/mapproxy/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mapproxy/mapproxy/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mapproxy/mapproxy/teams","hooks_url":"https://api.github.com/repos/mapproxy/mapproxy/hooks","issue_events_url":"https://api.github.com/repos/mapproxy/mapproxy/issues/events{/number}","events_url":"https://api.github.com/repos/mapproxy/mapproxy/events","assignees_url":"https://api.github.com/repos/mapproxy/mapproxy/assignees{/user}","branches_url":"https://api.github.com/repos/mapproxy/mapproxy/branches{/branch}","tags_url":"https://api.github.com/repos/mapproxy/mapproxy/tags","blobs_url":"https://api.github.com/repos/mapproxy/mapproxy/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mapproxy/mapproxy/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mapproxy/mapproxy/git/refs{/sha}","trees_url":"https://api.github.com/repos/mapproxy/mapproxy/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mapproxy/mapproxy/statuses/{sha}","languages_url":"https://api.github.com/repos/mapproxy/mapproxy/languages","stargazers_url":"https://api.github.com/repos/mapproxy/mapproxy/stargazers","contributors_url":"https://api.github.com/repos/mapproxy/mapproxy/contributors","subscribers_url":"https://api.github.com/repos/mapproxy/mapproxy/subscribers","subscription_url":"https://api.github.com/repos/mapproxy/mapproxy/subscription","commits_url":"https://api.github.com/repos/mapproxy/mapproxy/commits{/sha}","git_commits_url":"https://api.github.com/repos/mapproxy/mapproxy/git/commits{/sha}","comments_url":"https://api.github.com/repos/mapproxy/mapproxy/comments{/number}","issue_comment_url":"https://api.github.com/repos/mapproxy/mapproxy/issues/comments{/number}","contents_url":"https://api.github.com/repos/mapproxy/mapproxy/contents/{+path}","compare_url":"https://api.github.com/repos/mapproxy/mapproxy/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mapproxy/mapproxy/merges","archive_url":"https://api.github.com/repos/mapproxy/mapproxy/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mapproxy/mapproxy/downloads","issues_url":"https://api.github.com/repos/mapproxy/mapproxy/issues{/number}","pulls_url":"https://api.github.com/repos/mapproxy/mapproxy/pulls{/number}","milestones_url":"https://api.github.com/repos/mapproxy/mapproxy/milestones{/number}","notifications_url":"https://api.github.com/repos/mapproxy/mapproxy/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mapproxy/mapproxy/labels{/name}","releases_url":"https://api.github.com/repos/mapproxy/mapproxy/releases{/id}","deployments_url":"https://api.github.com/repos/mapproxy/mapproxy/deployments","created_at":"2012-04-23T07:36:34Z","updated_at":"2016-06-14T16:27:21Z","pushed_at":"2016-07-05T02:03:06Z","git_url":"git://github.com/mapproxy/mapproxy.git","ssh_url":"git@github.com:mapproxy/mapproxy.git","clone_url":"https://github.com/mapproxy/mapproxy.git","svn_url":"https://github.com/mapproxy/mapproxy","homepage":"http://mapproxy.org","size":10201,"stargazers_count":125,"watchers_count":125,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":71,"mirror_url":null,"open_issues_count":40,"forks":71,"open_issues":40,"watchers":125,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/mapproxy/mapproxy/pulls/232"},"html":{"href":"https://github.com/mapproxy/mapproxy/pull/232"},"issue":{"href":"https://api.github.com/repos/mapproxy/mapproxy/issues/232"},"comments":{"href":"https://api.github.com/repos/mapproxy/mapproxy/issues/232/comments"},"review_comments":{"href":"https://api.github.com/repos/mapproxy/mapproxy/pulls/232/comments"},"review_comment":{"href":"https://api.github.com/repos/mapproxy/mapproxy/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/mapproxy/mapproxy/pulls/232/commits"},"statuses":{"href":"https://api.github.com/repos/mapproxy/mapproxy/statuses/6cd6e5d19cd7c313b0392335b6d2562295d9358e"}}}}
{"actor":{"display_login":"olt"}}