GeoPakTile 2.1.0__tar.gz → 2.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GeoPakTile
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: Create a Geopackage XYZ Tile layer.
5
5
  Keywords: GIS,Geopackage,Tiles,XYZ,gpkg
6
6
  Author: apwebber
@@ -1,50 +1,50 @@
1
- [project]
2
- name = "GeoPakTile"
3
- version = "2.1.0"
4
- description = "Create a Geopackage XYZ Tile layer."
5
- readme = "README.md"
6
- authors = [
7
- { name = "apwebber", email = "39490780+apwebber@users.noreply.github.com" }
8
- ]
9
- license = "MIT"
10
- license-files = ["LICENSE.md"]
11
- keywords = ["GIS", "Geopackage", "Tiles", "XYZ", "gpkg"]
12
- requires-python = ">=3.10"
13
- classifiers = [
14
- "Development Status :: 4 - Beta",
15
-
16
- # Specify the Python versions you support here.
17
- "Programming Language :: Python :: 3.10",
18
- "Programming Language :: Python :: 3.11",
19
- "Programming Language :: Python :: 3.12",
20
- "Programming Language :: Python :: 3.13",
21
- "Programming Language :: Python :: 3.14",
22
- ]
23
- dependencies = [
24
- "typing_extensions>=4.0; python_version < '3.11'",
25
- "morecantile>=6.2.0",
26
- "numpy>=2.2.6",
27
- "requests>=2.34.2",
28
- "pillow>=12.3.0",
29
- ]
30
-
31
- [project.urls]
32
- Homepage = "https://github.com/apwebber/GeoPakTile"
33
- Documentation = "https://github.com/apwebber/GeoPakTile"
34
- Repository = "https://github.com/apwebber/GeoPakTile.git"
35
- Issues = "https://github.com/apwebber/GeoPakTile/issues"
36
-
37
- [project.scripts]
38
- geopaktile = "geopaktile:main"
39
-
40
- [build-system]
41
- requires = ["uv_build>=0.11.2,<0.12.0"]
42
- build-backend = "uv_build"
43
-
44
- [dependency-groups]
45
- dev = [
46
- "pytest",
47
- "diskcache",
48
- "rasterio",
49
- "rio-cogeo>=6.0.0",
50
- ]
1
+ [project]
2
+ name = "GeoPakTile"
3
+ version = "2.1.2"
4
+ description = "Create a Geopackage XYZ Tile layer."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "apwebber", email = "39490780+apwebber@users.noreply.github.com" }
8
+ ]
9
+ license = "MIT"
10
+ license-files = ["LICENSE.md"]
11
+ keywords = ["GIS", "Geopackage", "Tiles", "XYZ", "gpkg"]
12
+ requires-python = ">=3.10"
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+
16
+ # Specify the Python versions you support here.
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ ]
23
+ dependencies = [
24
+ "typing_extensions>=4.0; python_version < '3.11'",
25
+ "morecantile>=6.2.0",
26
+ "numpy>=2.2.6",
27
+ "requests>=2.34.2",
28
+ "pillow>=12.3.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/apwebber/GeoPakTile"
33
+ Documentation = "https://github.com/apwebber/GeoPakTile"
34
+ Repository = "https://github.com/apwebber/GeoPakTile.git"
35
+ Issues = "https://github.com/apwebber/GeoPakTile/issues"
36
+
37
+ [project.scripts]
38
+ geopaktile = "geopaktile:main"
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.11.2,<0.12.0"]
42
+ build-backend = "uv_build"
43
+
44
+ [dependency-groups]
45
+ dev = [
46
+ "pytest",
47
+ "diskcache",
48
+ "rasterio",
49
+ "rio-cogeo>=6.0.0",
50
+ ]
@@ -159,6 +159,7 @@ class GeoPakTile:
159
159
  )
160
160
  VALUES (?, ?, ?, ?)
161
161
  """
162
+ tiles = list(tiles)
162
163
 
163
164
  zs = {t[0] for t in tiles}
164
165
  for z in zs:
@@ -170,9 +171,6 @@ class GeoPakTile:
170
171
  x = int(x)
171
172
  y = int(y)
172
173
 
173
- if self.has_tile(z, x, y):
174
- continue
175
-
176
174
  batch.append((z, x, y, sqlite3.Binary(tile_bytes)))
177
175
  if len(batch) >= batch_size:
178
176
  self._cursor.executemany(insert_sql, batch)
@@ -68,6 +68,35 @@ class _Plan:
68
68
  resampled: bool
69
69
 
70
70
 
71
+ def _connect_readonly(path: str | Path) -> sqlite3.Connection:
72
+ r"""Open a GeoPackage read-only, trying resolve() first, then absolute().
73
+
74
+ resolve() can fail on network drives: it may raise, or turn a mapped
75
+ drive letter into a UNC path (\\server\share) whose file:// URI SQLite
76
+ rejects. absolute() keeps the path as given, which often works instead.
77
+ """
78
+ path = Path(path)
79
+ errors: list[str] = []
80
+ tried: set[str] = set()
81
+ for method in (Path.resolve, Path.absolute):
82
+ con = None
83
+ try:
84
+ uri = method(path).as_uri() + "?mode=ro"
85
+ if uri in tried:
86
+ continue
87
+ tried.add(uri)
88
+ con = sqlite3.connect(uri, uri=True)
89
+ con.execute("PRAGMA schema_version") # make SQLite actually open the file
90
+ return con
91
+ except (OSError, RuntimeError, ValueError, sqlite3.Error) as exc:
92
+ if con is not None:
93
+ con.close()
94
+ errors.append(f"{method.__name__}(): {exc}")
95
+ raise sqlite3.OperationalError(
96
+ f"Could not open GeoPackage {path}: " + "; ".join(errors)
97
+ )
98
+
99
+
71
100
  def _quote_ident(name: str) -> str:
72
101
  return '"' + name.replace('"', '""') + '"'
73
102
 
@@ -84,7 +113,7 @@ def _as_int(value: float, what: str) -> int:
84
113
  "Zoom levels may not be exact multiples of each other.",
85
114
  stacklevel=3,
86
115
  )
87
- return round(value)
116
+ return int(round(value))
88
117
 
89
118
 
90
119
  def gpkg_tiles_to_cog(
@@ -102,6 +131,7 @@ def gpkg_tiles_to_cog(
102
131
  compress: str = "DEFLATE",
103
132
  blocksize: int | None = None,
104
133
  overwrite: bool = False,
134
+ tmp_dir: str | Path | None = None,
105
135
  ) -> Path:
106
136
  """Convert a GeoPackage tile layer to a COG, using zoom levels as overviews.
107
137
 
@@ -128,6 +158,9 @@ def gpkg_tiles_to_cog(
128
158
  compress: COG compression, e.g. "DEFLATE", "ZSTD", "LZW", "WEBP", "JPEG".
129
159
  blocksize: COG internal tile size. Defaults to the GeoPackage tile width.
130
160
  overwrite: Replace an existing output file.
161
+ tmp_dir: Directory for the temporary per-zoom mosaics, which can need
162
+ about as much space as the output. Defaults to the output file's
163
+ directory, so they go on the same drive as the COG.
131
164
 
132
165
  Returns:
133
166
  Path to the written COG.
@@ -146,8 +179,7 @@ def gpkg_tiles_to_cog(
146
179
  if cog_path.exists() and not overwrite:
147
180
  raise FileExistsError(f"{cog_path} exists; pass overwrite=True")
148
181
 
149
- uri = Path(gpkg_path).resolve().as_uri() + "?mode=ro"
150
- con = sqlite3.connect(uri, uri=True)
182
+ con = _connect_readonly(gpkg_path)
151
183
  try:
152
184
  # --- Tile table, tile matrix set and CRS ----------------------------
153
185
  tables = [
@@ -293,7 +325,9 @@ def gpkg_tiles_to_cog(
293
325
  if blocksize is None:
294
326
  blocksize = base.tile_width
295
327
 
296
- with tempfile.TemporaryDirectory(prefix="gpkg2cog_") as tmp:
328
+ if tmp_dir is None:
329
+ tmp_dir = cog_path.absolute().parent # absolute(): resolve() can fail on network drives
330
+ with tempfile.TemporaryDirectory(prefix="gpkg2cog_", dir=tmp_dir) as tmp:
297
331
  tmpdir = Path(tmp)
298
332
  level_files: list[Path] = []
299
333
 
@@ -505,4 +539,41 @@ def _pyramid_vrt(
505
539
  ]
506
540
  parts.append(" </VRTRasterBand>")
507
541
  parts.append("</VRTDataset>")
508
- return "\n".join(parts)
542
+ return "\n".join(parts)
543
+
544
+
545
+ if __name__ == "__main__":
546
+ import argparse
547
+
548
+ parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
549
+ parser.add_argument("gpkg")
550
+ parser.add_argument("cog")
551
+ parser.add_argument("--table")
552
+ parser.add_argument(
553
+ "--bbox", type=float, nargs=4, metavar=("MINX", "MINY", "MAXX", "MAXY")
554
+ )
555
+ parser.add_argument("--bbox-crs")
556
+ parser.add_argument("--min-zoom", type=int)
557
+ parser.add_argument("--max-zoom", type=int)
558
+ parser.add_argument("--resample-unaligned", action="store_true")
559
+ parser.add_argument("--resampling", default="bilinear")
560
+ parser.add_argument("--bands", default="RGBA", choices=sorted(SUPPORTED_BANDS))
561
+ parser.add_argument("--compress", default="DEFLATE")
562
+ parser.add_argument("--overwrite", action="store_true")
563
+ parser.add_argument("--tmp-dir", help="directory for temporary files")
564
+ args = parser.parse_args()
565
+ gpkg_tiles_to_cog(
566
+ args.gpkg,
567
+ args.cog,
568
+ args.table,
569
+ bbox=args.bbox,
570
+ bbox_crs=args.bbox_crs,
571
+ min_zoom=args.min_zoom,
572
+ max_zoom=args.max_zoom,
573
+ resample_unaligned=args.resample_unaligned,
574
+ resampling=args.resampling,
575
+ bands=args.bands,
576
+ compress=args.compress,
577
+ overwrite=args.overwrite,
578
+ tmp_dir=args.tmp_dir,
579
+ )
File without changes
File without changes