tilepack 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tilepack-0.1.0/.github/workflows/ci.yml +70 -0
- tilepack-0.1.0/.github/workflows/release.yml +27 -0
- tilepack-0.1.0/.gitignore +32 -0
- tilepack-0.1.0/LICENSE +21 -0
- tilepack-0.1.0/PKG-INFO +120 -0
- tilepack-0.1.0/README.md +99 -0
- tilepack-0.1.0/pyproject.toml +43 -0
- tilepack-0.1.0/tests/__init__.py +0 -0
- tilepack-0.1.0/tests/conftest.py +92 -0
- tilepack-0.1.0/tests/test_cli.py +46 -0
- tilepack-0.1.0/tests/test_convert.py +108 -0
- tilepack-0.1.0/tests/test_serve.py +84 -0
- tilepack-0.1.0/tests/test_tms_utils.py +193 -0
- tilepack-0.1.0/tests/test_wmts_utils.py +65 -0
- tilepack-0.1.0/tilepack/__init__.py +0 -0
- tilepack-0.1.0/tilepack/__main__.py +5 -0
- tilepack-0.1.0/tilepack/cli.py +58 -0
- tilepack-0.1.0/tilepack/convert.py +188 -0
- tilepack-0.1.0/tilepack/selftest.py +101 -0
- tilepack-0.1.0/tilepack/serve.py +211 -0
- tilepack-0.1.0/tilepack/tms_utils.py +220 -0
- tilepack-0.1.0/tilepack/verify.py +70 -0
- tilepack-0.1.0/tilepack/wmts_utils.py +159 -0
- tilepack-0.1.0/uv.lock +725 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
defaults:
|
|
10
|
+
run:
|
|
11
|
+
shell: bash
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
lint:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.13"
|
|
21
|
+
enable-cache: true
|
|
22
|
+
cache-dependency-glob: |
|
|
23
|
+
pyproject.toml
|
|
24
|
+
uv.lock
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync --group dev
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: uv run ruff check tilepack/ tests/
|
|
29
|
+
- name: Format check
|
|
30
|
+
run: uv run ruff format --check tilepack/ tests/
|
|
31
|
+
|
|
32
|
+
test:
|
|
33
|
+
runs-on: ${{ matrix.os }}
|
|
34
|
+
strategy:
|
|
35
|
+
matrix:
|
|
36
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
37
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
- uses: astral-sh/setup-uv@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: ${{ matrix.python-version }}
|
|
43
|
+
enable-cache: true
|
|
44
|
+
cache-dependency-glob: |
|
|
45
|
+
pyproject.toml
|
|
46
|
+
uv.lock
|
|
47
|
+
- name: Install dependencies
|
|
48
|
+
run: uv sync --group dev
|
|
49
|
+
- name: Tests
|
|
50
|
+
run: uv run pytest tests/ -v
|
|
51
|
+
|
|
52
|
+
minimum:
|
|
53
|
+
name: test (minimum versions)
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
- uses: astral-sh/setup-uv@v5
|
|
58
|
+
with:
|
|
59
|
+
python-version: "3.11"
|
|
60
|
+
enable-cache: true
|
|
61
|
+
cache-dependency-glob: |
|
|
62
|
+
pyproject.toml
|
|
63
|
+
- name: Install minimum dependencies
|
|
64
|
+
run: uv sync --group dev --resolution lowest-direct
|
|
65
|
+
- name: Tests
|
|
66
|
+
run: uv run pytest tests/ -v
|
|
67
|
+
|
|
68
|
+
concurrency:
|
|
69
|
+
group: ${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}
|
|
70
|
+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
run: uv python install 3.13
|
|
22
|
+
|
|
23
|
+
- name: Build
|
|
24
|
+
run: uv build
|
|
25
|
+
|
|
26
|
+
- name: Publish to PyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.whl
|
|
9
|
+
|
|
10
|
+
# Test data
|
|
11
|
+
*_TMS*/
|
|
12
|
+
*.zip
|
|
13
|
+
|
|
14
|
+
# Generated archives
|
|
15
|
+
*.mbtiles
|
|
16
|
+
*.pmtiles
|
|
17
|
+
|
|
18
|
+
# Tile images
|
|
19
|
+
*.png
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.vscode/
|
|
23
|
+
.idea/
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
Thumbs.db
|
|
28
|
+
|
|
29
|
+
# Env
|
|
30
|
+
.env
|
|
31
|
+
.venv/
|
|
32
|
+
|
tilepack-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ashwin Nair
|
|
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.
|
tilepack-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tilepack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert TMS tile folders to MBTiles/PMTiles and serve them as TMS/WMTS endpoints
|
|
5
|
+
Author-email: Ashwin Nair <ashnair0007@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: click>=8.0
|
|
16
|
+
Requires-Dist: fastapi>=0.110
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: pmtiles>=3.4
|
|
19
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# tilepack
|
|
23
|
+
|
|
24
|
+
[](https://github.com/ashnair1/tilepack/actions/workflows/ci.yml)
|
|
25
|
+
[](https://pypi.org/project/tilepack/)
|
|
26
|
+
[](https://pypi.org/project/tilepack/)
|
|
27
|
+
[](LICENSE)
|
|
28
|
+
|
|
29
|
+
Pack raster tile folders (TMS or XYZ) into single-file archives (MBTiles / PMTiles) and serve them as TMS and WMTS endpoints over HTTP.
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
Raster tile folders contain thousands of small PNG files in deeply nested `z/x/y` directories. This makes them slow to copy, hard to manage, and fragile to transfer. Tilepack solves this by packing tiles into a single archive file while still exposing standard TMS and WMTS HTTP endpoints that clients like QGIS and CesiumForUnreal can consume directly.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
Requires Python 3.11+.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install tilepack
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For development:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/ashnair1/tilepack.git
|
|
47
|
+
cd tilepack
|
|
48
|
+
uv sync --group dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
### Verify a tile folder
|
|
54
|
+
|
|
55
|
+
Scan a tile folder and report zoom levels, tile counts, format, and detected Y-axis scheme (TMS vs XYZ).
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
tilepack verify ./path/to/tiles
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Convert to archive
|
|
62
|
+
|
|
63
|
+
The output format is inferred from the file extension. The input tile scheme (TMS or XYZ) is auto-detected, or can be specified explicitly.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Auto-detect input scheme
|
|
67
|
+
tilepack convert ./path/to/tiles output.mbtiles
|
|
68
|
+
tilepack convert ./path/to/tiles output.pmtiles
|
|
69
|
+
|
|
70
|
+
# Specify input scheme explicitly
|
|
71
|
+
tilepack convert ./path/to/tiles output.mbtiles --scheme xyz
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Serve as TMS + WMTS endpoint
|
|
75
|
+
|
|
76
|
+
Start a local HTTP server exposing both TMS and OGC WMTS 1.0.0 endpoints from an archive file.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
tilepack serve output.mbtiles --port 8000
|
|
80
|
+
tilepack serve output.pmtiles --port 8000
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**TMS endpoints:**
|
|
84
|
+
- `http://localhost:8000/tilemapresource.xml`
|
|
85
|
+
- `http://localhost:8000/{z}/{x}/{y}.png`
|
|
86
|
+
|
|
87
|
+
**WMTS endpoints:**
|
|
88
|
+
- `http://localhost:8000/WMTSCapabilities.xml` — GetCapabilities
|
|
89
|
+
- `http://localhost:8000/wmts/{Layer}/{TileMatrixSet}/{z}/{row}/{col}.png` — RESTful tiles
|
|
90
|
+
- `http://localhost:8000/wmts?Service=WMTS&Request=GetTile&...` — KVP tiles
|
|
91
|
+
|
|
92
|
+
To load in QGIS: **Layer > Add WMS/WMTS Layer > New**, set URL to `http://localhost:8000/WMTSCapabilities.xml`, then Connect and Add.
|
|
93
|
+
|
|
94
|
+
### Validate correctness
|
|
95
|
+
|
|
96
|
+
Randomly sample tiles from the original folder, fetch them from the running server, and verify byte-exact matches.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Start the server in one terminal, then in another:
|
|
100
|
+
tilepack selftest ./path/to/tiles --base-url http://127.0.0.1:8000 --samples 200
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## MBTiles vs PMTiles
|
|
104
|
+
|
|
105
|
+
| | MBTiles | PMTiles |
|
|
106
|
+
|---|---------|---------|
|
|
107
|
+
| Format | SQLite database | Cloud-optimised archive (Hilbert-curve index) |
|
|
108
|
+
| File count | 1 | 1 |
|
|
109
|
+
| Needs a tile server | Yes | No (supports HTTP range requests) |
|
|
110
|
+
| Best for | Local / on-prem serving | Cloud storage (S3, Azure Blob, GCS) |
|
|
111
|
+
|
|
112
|
+
**Use MBTiles** for local or on-prem serving (e.g. feeding CesiumForUnreal on the same machine or network). It's a SQLite file with fast tile lookups and no coordinate flipping at read time.
|
|
113
|
+
|
|
114
|
+
**Use PMTiles** if you plan to host tiles in cloud storage. PMTiles can be served directly from a bucket via HTTP range requests with no tile server needed. However, TMS clients like CesiumForUnreal cannot consume PMTiles directly — they still need a server translating to TMS endpoints.
|
|
115
|
+
|
|
116
|
+
**Either format** works identically when served through `tilepack serve` — clients see the same TMS and WMTS endpoints regardless of the backing archive.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
tilepack-0.1.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# tilepack
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ashnair1/tilepack/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/tilepack/)
|
|
5
|
+
[](https://pypi.org/project/tilepack/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Pack raster tile folders (TMS or XYZ) into single-file archives (MBTiles / PMTiles) and serve them as TMS and WMTS endpoints over HTTP.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
Raster tile folders contain thousands of small PNG files in deeply nested `z/x/y` directories. This makes them slow to copy, hard to manage, and fragile to transfer. Tilepack solves this by packing tiles into a single archive file while still exposing standard TMS and WMTS HTTP endpoints that clients like QGIS and CesiumForUnreal can consume directly.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Requires Python 3.11+.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install tilepack
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For development:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/ashnair1/tilepack.git
|
|
26
|
+
cd tilepack
|
|
27
|
+
uv sync --group dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Verify a tile folder
|
|
33
|
+
|
|
34
|
+
Scan a tile folder and report zoom levels, tile counts, format, and detected Y-axis scheme (TMS vs XYZ).
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
tilepack verify ./path/to/tiles
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Convert to archive
|
|
41
|
+
|
|
42
|
+
The output format is inferred from the file extension. The input tile scheme (TMS or XYZ) is auto-detected, or can be specified explicitly.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Auto-detect input scheme
|
|
46
|
+
tilepack convert ./path/to/tiles output.mbtiles
|
|
47
|
+
tilepack convert ./path/to/tiles output.pmtiles
|
|
48
|
+
|
|
49
|
+
# Specify input scheme explicitly
|
|
50
|
+
tilepack convert ./path/to/tiles output.mbtiles --scheme xyz
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Serve as TMS + WMTS endpoint
|
|
54
|
+
|
|
55
|
+
Start a local HTTP server exposing both TMS and OGC WMTS 1.0.0 endpoints from an archive file.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
tilepack serve output.mbtiles --port 8000
|
|
59
|
+
tilepack serve output.pmtiles --port 8000
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**TMS endpoints:**
|
|
63
|
+
- `http://localhost:8000/tilemapresource.xml`
|
|
64
|
+
- `http://localhost:8000/{z}/{x}/{y}.png`
|
|
65
|
+
|
|
66
|
+
**WMTS endpoints:**
|
|
67
|
+
- `http://localhost:8000/WMTSCapabilities.xml` — GetCapabilities
|
|
68
|
+
- `http://localhost:8000/wmts/{Layer}/{TileMatrixSet}/{z}/{row}/{col}.png` — RESTful tiles
|
|
69
|
+
- `http://localhost:8000/wmts?Service=WMTS&Request=GetTile&...` — KVP tiles
|
|
70
|
+
|
|
71
|
+
To load in QGIS: **Layer > Add WMS/WMTS Layer > New**, set URL to `http://localhost:8000/WMTSCapabilities.xml`, then Connect and Add.
|
|
72
|
+
|
|
73
|
+
### Validate correctness
|
|
74
|
+
|
|
75
|
+
Randomly sample tiles from the original folder, fetch them from the running server, and verify byte-exact matches.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Start the server in one terminal, then in another:
|
|
79
|
+
tilepack selftest ./path/to/tiles --base-url http://127.0.0.1:8000 --samples 200
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## MBTiles vs PMTiles
|
|
83
|
+
|
|
84
|
+
| | MBTiles | PMTiles |
|
|
85
|
+
|---|---------|---------|
|
|
86
|
+
| Format | SQLite database | Cloud-optimised archive (Hilbert-curve index) |
|
|
87
|
+
| File count | 1 | 1 |
|
|
88
|
+
| Needs a tile server | Yes | No (supports HTTP range requests) |
|
|
89
|
+
| Best for | Local / on-prem serving | Cloud storage (S3, Azure Blob, GCS) |
|
|
90
|
+
|
|
91
|
+
**Use MBTiles** for local or on-prem serving (e.g. feeding CesiumForUnreal on the same machine or network). It's a SQLite file with fast tile lookups and no coordinate flipping at read time.
|
|
92
|
+
|
|
93
|
+
**Use PMTiles** if you plan to host tiles in cloud storage. PMTiles can be served directly from a bucket via HTTP range requests with no tile server needed. However, TMS clients like CesiumForUnreal cannot consume PMTiles directly — they still need a server translating to TMS endpoints.
|
|
94
|
+
|
|
95
|
+
**Either format** works identically when served through `tilepack serve` — clients see the same TMS and WMTS endpoints regardless of the backing archive.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tilepack"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Convert TMS tile folders to MBTiles/PMTiles and serve them as TMS/WMTS endpoints"
|
|
5
|
+
authors = [{ name = "Ashwin Nair", email = "ashnair0007@gmail.com" }]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3.11",
|
|
12
|
+
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Programming Language :: Python :: 3.13",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"click>=8.0",
|
|
19
|
+
"fastapi>=0.110",
|
|
20
|
+
"uvicorn[standard]>=0.29",
|
|
21
|
+
"pmtiles>=3.4",
|
|
22
|
+
"httpx>=0.27",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
tilepack = "tilepack.cli:cli"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = ["ruff>=0.9", "ty>=0.0.2", "pytest>=8.0"]
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatchling"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
target-version = "py311"
|
|
37
|
+
line-length = 100
|
|
38
|
+
|
|
39
|
+
[tool.ruff.lint]
|
|
40
|
+
select = ["E", "F", "I", "UP"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint.per-file-ignores]
|
|
43
|
+
"tilepack/tms_utils.py" = ["E501"] # XML template strings
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Shared test fixtures for tilepack tests."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from tilepack.convert import run_convert
|
|
8
|
+
from tilepack.tms_utils import PNG_SIGNATURE
|
|
9
|
+
|
|
10
|
+
# Minimal valid PNG: 1x1 pixel, 8-bit RGBA
|
|
11
|
+
# PNG signature + IHDR + IDAT + IEND (the smallest valid PNG possible)
|
|
12
|
+
_MINIMAL_PNG = (
|
|
13
|
+
PNG_SIGNATURE
|
|
14
|
+
+ b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde"
|
|
15
|
+
+ b"\x00\x00\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05\x18\xd8N"
|
|
16
|
+
+ b"\x00\x00\x00\x00IEND\xaeB`\x82"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _write_tile(root: Path, z: int, x: int, y: int) -> Path:
|
|
21
|
+
"""Write a minimal PNG tile at root/z/x/y.png."""
|
|
22
|
+
tile_dir = root / str(z) / str(x)
|
|
23
|
+
tile_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
tile_path = tile_dir / f"{y}.png"
|
|
25
|
+
tile_path.write_bytes(_MINIMAL_PNG)
|
|
26
|
+
return tile_path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture()
|
|
30
|
+
def tiny_tms_dir(tmp_path: Path) -> Path:
|
|
31
|
+
"""Create a minimal TMS tile directory (y=0 at south).
|
|
32
|
+
|
|
33
|
+
Uses z=1 with a tile in the northern hemisphere:
|
|
34
|
+
- TMS y=1 (north half), x=0 (western half)
|
|
35
|
+
- TMS y=1, x=1 (eastern half)
|
|
36
|
+
|
|
37
|
+
And z=2 with a couple tiles:
|
|
38
|
+
- TMS y=2, x=0
|
|
39
|
+
- TMS y=3, x=1
|
|
40
|
+
"""
|
|
41
|
+
root = tmp_path / "tms_tiles"
|
|
42
|
+
root.mkdir()
|
|
43
|
+
|
|
44
|
+
# z=1: 2x2 grid. TMS y=1 = northern hemisphere
|
|
45
|
+
_write_tile(root, 1, 0, 1)
|
|
46
|
+
_write_tile(root, 1, 1, 1)
|
|
47
|
+
|
|
48
|
+
# z=2: 4x4 grid. TMS y=2,3 = northern hemisphere
|
|
49
|
+
_write_tile(root, 2, 0, 2)
|
|
50
|
+
_write_tile(root, 2, 1, 3)
|
|
51
|
+
|
|
52
|
+
return root
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture()
|
|
56
|
+
def tiny_xyz_dir(tmp_path: Path) -> Path:
|
|
57
|
+
"""Create a minimal XYZ tile directory (y=0 at north).
|
|
58
|
+
|
|
59
|
+
Equivalent geographic area to tiny_tms_dir but with flipped y values.
|
|
60
|
+
z=1: XYZ y=0 (north half) = TMS y=1
|
|
61
|
+
z=2: XYZ y=1 (TMS y=2), XYZ y=0 (TMS y=3)
|
|
62
|
+
"""
|
|
63
|
+
root = tmp_path / "xyz_tiles"
|
|
64
|
+
root.mkdir()
|
|
65
|
+
|
|
66
|
+
# z=1: XYZ y=0 = TMS y=1 (northern hemisphere)
|
|
67
|
+
_write_tile(root, 1, 0, 0)
|
|
68
|
+
_write_tile(root, 1, 1, 0)
|
|
69
|
+
|
|
70
|
+
# z=2: XYZ y=1 = TMS y=2, XYZ y=0 = TMS y=3
|
|
71
|
+
_write_tile(root, 2, 0, 1)
|
|
72
|
+
_write_tile(root, 2, 1, 0)
|
|
73
|
+
|
|
74
|
+
return root
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.fixture()
|
|
78
|
+
def tiny_mbtiles(tmp_path: Path, tiny_tms_dir: Path) -> Path:
|
|
79
|
+
"""Convert tiny_tms_dir to a small .mbtiles file."""
|
|
80
|
+
|
|
81
|
+
out = tmp_path / "test.mbtiles"
|
|
82
|
+
run_convert(str(tiny_tms_dir), str(out), scheme="tms")
|
|
83
|
+
return out
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.fixture()
|
|
87
|
+
def tiny_pmtiles(tmp_path: Path, tiny_tms_dir: Path) -> Path:
|
|
88
|
+
"""Convert tiny_tms_dir to a small .pmtiles file."""
|
|
89
|
+
|
|
90
|
+
out = tmp_path / "test.pmtiles"
|
|
91
|
+
run_convert(str(tiny_tms_dir), str(out), scheme="tms")
|
|
92
|
+
return out
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Tests for tilepack.cli."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from click.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from tilepack.cli import cli
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestVerifyCommand:
|
|
11
|
+
def test_verify_tms_dir(self, tiny_tms_dir: Path):
|
|
12
|
+
runner = CliRunner()
|
|
13
|
+
result = runner.invoke(cli, ["verify", str(tiny_tms_dir)])
|
|
14
|
+
assert result.exit_code == 0
|
|
15
|
+
assert "Zoom range" in result.output
|
|
16
|
+
assert "Detected scheme: TMS" in result.output
|
|
17
|
+
|
|
18
|
+
def test_verify_empty_dir(self, tmp_path: Path):
|
|
19
|
+
empty = tmp_path / "empty"
|
|
20
|
+
empty.mkdir()
|
|
21
|
+
runner = CliRunner()
|
|
22
|
+
result = runner.invoke(cli, ["verify", str(empty)])
|
|
23
|
+
assert result.exit_code == 1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestConvertCommand:
|
|
27
|
+
def test_convert_to_mbtiles(self, tmp_path: Path, tiny_tms_dir: Path):
|
|
28
|
+
out = tmp_path / "output.mbtiles"
|
|
29
|
+
runner = CliRunner()
|
|
30
|
+
result = runner.invoke(cli, ["convert", str(tiny_tms_dir), str(out)])
|
|
31
|
+
assert result.exit_code == 0
|
|
32
|
+
assert out.exists()
|
|
33
|
+
|
|
34
|
+
def test_convert_with_scheme_flag(self, tmp_path: Path, tiny_xyz_dir: Path):
|
|
35
|
+
out = tmp_path / "output.mbtiles"
|
|
36
|
+
runner = CliRunner()
|
|
37
|
+
result = runner.invoke(cli, ["convert", str(tiny_xyz_dir), str(out), "--scheme", "xyz"])
|
|
38
|
+
assert result.exit_code == 0
|
|
39
|
+
assert "Using specified scheme: XYZ" in result.output
|
|
40
|
+
|
|
41
|
+
def test_convert_auto_detect(self, tmp_path: Path, tiny_tms_dir: Path):
|
|
42
|
+
out = tmp_path / "output.mbtiles"
|
|
43
|
+
runner = CliRunner()
|
|
44
|
+
result = runner.invoke(cli, ["convert", str(tiny_tms_dir), str(out)])
|
|
45
|
+
assert result.exit_code == 0
|
|
46
|
+
assert "Detected input scheme:" in result.output
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Tests for tilepack.convert."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from tilepack.convert import run_convert
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestConvertToMbtiles:
|
|
10
|
+
def test_creates_mbtiles_file(self, tmp_path: Path, tiny_tms_dir: Path):
|
|
11
|
+
out = tmp_path / "output.mbtiles"
|
|
12
|
+
run_convert(str(tiny_tms_dir), str(out), scheme="tms")
|
|
13
|
+
assert out.exists()
|
|
14
|
+
assert out.stat().st_size > 0
|
|
15
|
+
|
|
16
|
+
def test_mbtiles_tile_count(self, tmp_path: Path, tiny_tms_dir: Path):
|
|
17
|
+
out = tmp_path / "output.mbtiles"
|
|
18
|
+
run_convert(str(tiny_tms_dir), str(out), scheme="tms")
|
|
19
|
+
conn = sqlite3.connect(str(out))
|
|
20
|
+
count = conn.execute("SELECT COUNT(*) FROM tiles").fetchone()[0]
|
|
21
|
+
conn.close()
|
|
22
|
+
assert count == 4
|
|
23
|
+
|
|
24
|
+
def test_mbtiles_metadata(self, tmp_path: Path, tiny_tms_dir: Path):
|
|
25
|
+
out = tmp_path / "output.mbtiles"
|
|
26
|
+
run_convert(str(tiny_tms_dir), str(out), scheme="tms")
|
|
27
|
+
conn = sqlite3.connect(str(out))
|
|
28
|
+
meta = dict(conn.execute("SELECT name, value FROM metadata").fetchall())
|
|
29
|
+
conn.close()
|
|
30
|
+
assert meta["format"] == "png"
|
|
31
|
+
assert "minzoom" in meta
|
|
32
|
+
assert "maxzoom" in meta
|
|
33
|
+
assert meta["minzoom"] == "1"
|
|
34
|
+
assert meta["maxzoom"] == "2"
|
|
35
|
+
|
|
36
|
+
def test_xyz_input_flips_y(self, tmp_path: Path, tiny_xyz_dir: Path):
|
|
37
|
+
out = tmp_path / "output.mbtiles"
|
|
38
|
+
run_convert(str(tiny_xyz_dir), str(out), scheme="xyz")
|
|
39
|
+
conn = sqlite3.connect(str(out))
|
|
40
|
+
# z=1, x=0, XYZ y=0 should become TMS y=1
|
|
41
|
+
row = conn.execute(
|
|
42
|
+
"SELECT tile_data FROM tiles WHERE zoom_level=1 AND tile_column=0 AND tile_row=1"
|
|
43
|
+
).fetchone()
|
|
44
|
+
conn.close()
|
|
45
|
+
assert row is not None, "Expected XYZ y=0 to be stored as TMS y=1"
|
|
46
|
+
|
|
47
|
+
def test_auto_detect_scheme(self, tmp_path: Path, tiny_tms_dir: Path):
|
|
48
|
+
out = tmp_path / "output.mbtiles"
|
|
49
|
+
# No scheme specified — should auto-detect as TMS
|
|
50
|
+
run_convert(str(tiny_tms_dir), str(out))
|
|
51
|
+
assert out.exists()
|
|
52
|
+
conn = sqlite3.connect(str(out))
|
|
53
|
+
count = conn.execute("SELECT COUNT(*) FROM tiles").fetchone()[0]
|
|
54
|
+
conn.close()
|
|
55
|
+
assert count == 4
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestConvertToPmtiles:
|
|
59
|
+
def test_creates_pmtiles_file(self, tmp_path: Path, tiny_tms_dir: Path):
|
|
60
|
+
out = tmp_path / "output.pmtiles"
|
|
61
|
+
run_convert(str(tiny_tms_dir), str(out), scheme="tms")
|
|
62
|
+
assert out.exists()
|
|
63
|
+
assert out.stat().st_size > 0
|
|
64
|
+
|
|
65
|
+
def test_pmtiles_tiles_readable(self, tmp_path: Path, tiny_tms_dir: Path):
|
|
66
|
+
out = tmp_path / "output.pmtiles"
|
|
67
|
+
run_convert(str(tiny_tms_dir), str(out), scheme="tms")
|
|
68
|
+
|
|
69
|
+
from pmtiles.reader import MmapSource
|
|
70
|
+
from pmtiles.reader import Reader as PMTilesReader
|
|
71
|
+
|
|
72
|
+
source = MmapSource(open(out, "rb"))
|
|
73
|
+
reader = PMTilesReader(source)
|
|
74
|
+
header = reader.header()
|
|
75
|
+
assert header["min_zoom"] == 1
|
|
76
|
+
assert header["max_zoom"] == 2
|
|
77
|
+
|
|
78
|
+
def test_xyz_input_to_pmtiles(self, tmp_path: Path, tiny_xyz_dir: Path):
|
|
79
|
+
out = tmp_path / "output.pmtiles"
|
|
80
|
+
run_convert(str(tiny_xyz_dir), str(out), scheme="xyz")
|
|
81
|
+
assert out.exists()
|
|
82
|
+
|
|
83
|
+
from pmtiles.reader import MmapSource
|
|
84
|
+
from pmtiles.reader import Reader as PMTilesReader
|
|
85
|
+
|
|
86
|
+
source = MmapSource(open(out, "rb"))
|
|
87
|
+
reader = PMTilesReader(source)
|
|
88
|
+
# z=1, x=0, XYZ y=0 — should be stored directly as XYZ (no flip)
|
|
89
|
+
data = reader.get(1, 0, 0)
|
|
90
|
+
assert data is not None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TestConvertErrors:
|
|
94
|
+
def test_unknown_format(self, tmp_path: Path, tiny_tms_dir: Path):
|
|
95
|
+
out = tmp_path / "output.unknown"
|
|
96
|
+
try:
|
|
97
|
+
run_convert(str(tiny_tms_dir), str(out))
|
|
98
|
+
except SystemExit as e:
|
|
99
|
+
assert e.code == 1
|
|
100
|
+
|
|
101
|
+
def test_empty_dir(self, tmp_path: Path):
|
|
102
|
+
empty = tmp_path / "empty"
|
|
103
|
+
empty.mkdir()
|
|
104
|
+
out = tmp_path / "output.mbtiles"
|
|
105
|
+
try:
|
|
106
|
+
run_convert(str(empty), str(out))
|
|
107
|
+
except SystemExit as e:
|
|
108
|
+
assert e.code == 1
|