Repository: camptocamp/tilecloud

Issue:
Errors in README.md
The  Getting started section leaves out the actual install of tilecloud. "pip install tilecloud" should be added someplace.  

This repo does not seem to include a setup.py file, or anything to install.  Nor does the repo contain run-in-place python  code. 

All of the references to "./tc-viewer" and "./tc-info" do not work.  The files in the git repo can't be run directly, end in .py and have _ where the - is.   After a "pip install tilecloud" the commands do work but only without the preceding "./".

Now I'm also having trouble with the suggested demos.  But I'll report back on that once I have spent more time to see if the installation is actually ok.



Discussion:

Patch:
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index a86b15ad..7d3ae39e 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -87,10 +87,10 @@
       groupName: 'dpkg',
       automerge: true,
     },
-    /** Disable types-request update on version <= 1.11 */
+    /** Disable types-request update on version <= 1.10 */
     {
       matchPackageNames: ['types-requests'],
-      matchBaseBranches: ['1.6', '1.8', '1.10', '1.11'],
+      matchBaseBranches: ['1.6', '1.8', '1.10'],
       enabled: false,
     },
     /** Disable update of Python version in pyproject.toml */
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2819774c..7a7d6bcf 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -44,7 +44,7 @@ repos:
     hooks:
       - id: git-check
   - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.27.3
+    rev: 0.27.2
     hooks:
       - id: check-github-actions
       - id: check-github-workflows
@@ -58,7 +58,7 @@ repos:
           - github-workflows-require-timeout
       - id: check-renovate
         additional_dependencies:
-          - pyjson5==1.6.5 # pypi
+          - pyjson5==1.6.4 # pypi
   - repo: https://github.com/sirwart/ripsecrets
     rev: v0.1.7
     hooks:
diff --git a/Dockerfile b/Dockerfile
index 9dc5d22f..94140b08 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM ghcr.io/osgeo/gdal:ubuntu-small-3.8.3 as base-all
+FROM ghcr.io/osgeo/gdal:ubuntu-small-3.8.1 as base-all
 LABEL maintainer Camptocamp "info@camptocamp.com"
 SHELL ["/bin/bash", "-o", "pipefail", "-cux"]
 
diff --git a/README.md b/README.md
index c470de50..f3e6acc0 100644
--- a/README.md
+++ b/README.md
@@ -46,57 +46,51 @@ Example tasks that TileCloud makes easy include:
 
 TileCloud depends on some Python modules. It's easiest to install them with `pip` in a `virtualenv`:
 
-    $ python3 -m venv .venv
-    $ . .venv/bin/activate
-    $ pip install -r requirements.txt
+    $ pip install poetry
+    $ poetry install
 
 For a quick demo, run
 
-    $ ./tc-viewer --root=3/4/2 'http://gsp2.apple.com/tile?api=1&style=slideshow&layers=default&lang=en_GB&z=%(z)d&x=%(x)d&y=%(y)d&v=9'
+    $ poetry run tc-viewer --root=3/4/2 'http://gsp2.apple.com/tile?api=1&style=slideshow&layers=default&lang=en_GB&z=%(z)d&x=%(x)d&y=%(y)d&v=9'
 
 and point your browser at <http://localhost:8080/>. Type `Ctrl-C` to terminate `tc-viewer`.
 
-Next, download an example MBTiles file from [MapBox](http://mapbox.com/), such as [Geography
-Class](http://tiles.mapbox.com/mapbox/map/geography-class). We can quickly find out more about this tile set
+Next, download an example MBTiles file from [MapBox](http://mapbox.com/), such as [OSM map](https://wiki.openstreetmap.org/wiki/MBTiles#Offline-Karten). We can quickly find out more about this tile set
 with the `tc-info` command. For example, to count the number of tiles:
 
-    $ ./tc-info -t count geography-class.mbtiles
-    87381
+    $ poetry run tc-info -t count OSM-OpenCPN2-MagellanStrait.mbtiles
+    51672
 
 To calculate the bounding pyramid:
 
-    $ ./tc-info -t bounding-pyramid -r geography-class.mbtiles
-    0/0/0:+1/+1
-    1/0/0:+2/+2
-    2/0/0:+4/+4
-    3/0/0:+8/+8
-    4/0/0:+16/+16
-    5/0/0:+32/+32
-    6/0/0:+64/+64
-    7/0/0:+128/+128
-    8/0/0:+256/+256
+    $ poetry run tc-info -t bounding-pyramid -r OSM-OpenCPN2-MagellanStrait.mbtiles
+    8/72/168:+16/+16
+    10/296/680:+40/+32
+    12/1184/2728:+144/+104
+    14/4760/10935:+536/+393
+    16/19048/43760:+2112/+1552
 
 To check for missing tiles against a bounding pyramid:
 
-    $ ./tc-info -b 0/0/0:8/*/* -t completion geography-class.mbtiles
-    0 1/1 (100%)
-    1 4/4 (100%)
-    2 16/16 (100%)
-    3 64/64 (100%)
-    4 256/256 (100%)
-    5 1024/1024 (100%)
-    6 4096/4096 (100%)
-    7 16384/16384 (100%)
-    8 65536/65536 (100%)
+    $ poetry run tc-info -b 8/72/168:16/*/* -t completion OSM-OpenCPN2-MagellanStrait.mbtiles
+    8 256/65536 (0%)
+    9 0/262144 (0%)
+    10 832/1048576 (0%)
+    11 0/4194304 (0%)
+    12 4392/16777216 (0%)
+    13 0/67108864 (0%)
+    14 21408/268435456 (0%)
+    15 0/1073741824 (0%)
+    16 24784/4294967296 (0%)
 
 This shows, for each zoom level, the number of tiles at that zoom level, the total number of tiles expected at
-that zoom level for the specified bounding pyramid (`0/0/0:8/*/*` means all tiles from level 0 to level 8),
+that zoom level for the specified bounding pyramid (`8/0/0:16/*/*` means all tiles from level 8 to level 16),
 and a percentage completion. This can be useful for checking that a tile set is complete.
 
 Now, display this MBTiles tile set on top of the [OpenStreetMap](http://www.openstreetmap.org/) tiles and a
 debug tile layer:
 
-    $ ./tc-viewer tiles.openstreetmap_org geography-class.mbtiles tiles.debug.black
+    $ poetry run tc-viewer tiles.openstreetmap_org OSM-OpenCPN2-MagellanStrait.mbtiles tiles.debug.black
 
 You'll need to point your browser at <http://localhost:8080/> and choose your favorite library.
 
@@ -105,7 +99,7 @@ that connect the TileCloud's modules to perform the action that you want.
 
 As a first example, run the following:
 
-    $ PYTHONPATH=. examples/download.py
+    $ poetry run examples/download.py
 
 This will download a few tiles from [OpenStreetMap](http://www.openstreetmap.org/) and save them in a local
 MBTiles file called `local.mbtiles`. Look at the source code to `examples/download.py` to see how it works. If
@@ -114,7 +108,7 @@ automatically resume where it left off.
 
 Once you have downloaded a few tiles, you can view them directly with `tc-viewer`:
 
-    $ ./tc-viewer --root=4/8/5 local.mbtiles tiles.debug.black
+    $ poetry run tc-viewer --root=4/8/5 local.mbtiles tiles.debug.black
 
 Point your browser at <http://localhost:8080> as usual. The `--root` option to `tc-viewer` instructs the
 viewer to start at a defined tile, rather than at 0/0/0, so you don't have to zoom in to find the tiles that
@@ -166,7 +160,7 @@ effect of generating tiles, then it functions as a quick tile generation utility
 To convert from one tile format to another, just copy from source to destination. For example, to convert an
 MBTiles file in to a ZIP file, just run:
 
-    $ ./tc-copy geography-class.mbtiles geography-class.zip
+    $ poetry run tc-copy OSM-OpenCPN2-MagellanStrait.mbtiles geography-class.zip
 
 You can check this worked with `unzip`:
 
@@ -174,7 +168,7 @@ You can check this worked with `unzip`:
 
 Equally, `tc-copy` can be used to download multiple tiles:
 
-    $ ./tc-copy --bounding-pyramid 4/0/0:0/16/16 tiles.openstreetmap_org osm-up-to-z4.mbtiles
+    $ poetry run tc-copy --bounding-pyramid 4/0/0:0/16/16 tiles.openstreetmap_org osm-up-to-z4.mbtiles
 
 Here we downloaded zoom levels 0 to 4 of the OpenStreetMap tiles into a local MBTiles file. The
 `--bounding-pyramid` option is required because otherwise we would download _all_ OpenStreetMap tiles -- which
@@ -186,7 +180,7 @@ destination then pass the `--overwrite` option to `tc-copy`.
 In the same way, `tc-copy` can also be used to upload tiles. For example, to upload an MBTiles file to S3,
 just use:
 
-    $ ./tc-copy osm-up-to-z4.mbtiles s3://bucket/prefix/%(z)d/%(x)d/%(y)d.jpg
+    $ poetry run tc-copy osm-up-to-z4.mbtiles s3://bucket/prefix/%(z)d/%(x)d/%(y)d.jpg
 
 `bucket` is the name of your S3 bucket. You'll need to have set the `AWS_ACCESS_KEY_ID` and
 `AWS_SECRET_ACCESS_KEY` environment variables to have permission to upload to S3. The rest of the destination
@@ -196,7 +190,7 @@ format string: `%(x)d` means substitute the tile's `x` coordinate as a decimal i
 You can pass the same `s3://` URL to `tc-viewer`. This allows you to visualize your tiles stored in S3 with
 your favorite mapping library. For example:
 
-    $ ./tc-viewer s3://bucket/prefix/%(z)d/%(x)d/%(y)d.jpg
+    $ poetry run tc-viewer s3://bucket/prefix/%(z)d/%(x)d/%(y)d.jpg
 
 Here, `tc-viewer` is acting as a proxy, serving tiles stored in S3 over HTTP, bypassing any caches or access
 controls (assuming you have the correct credentials, of course). This allows you to visualize the exact tiles
@@ -208,12 +202,12 @@ At [FOSS4G-NA](http://foss4g-na.org/), [MapBox](http://mapbox.com/) presented an
 [rendering the world](http://mapbox.com/blog/rendering-the-world/). TileCloud supports the subdivision
 strategy. To run the demo, execute:
 
-    $ python examples/renderingtheworld.py
+    $ poetry run examples/renderingtheworld.py
 
 This will generate tiles from a WMTS tile server and save them in a local MBTiles tiles. When the above
 command is complete, you can see the bounding pyramid of the generated tiles:
 
-    $ ./tc-info -t bounding-pyramid -r medford_buildings.mbtiles
+    $ poetry run tc-info -t bounding-pyramid -r medford_buildings.mbtiles
     0/0/0:+1/+1
     1/0/0:+1/+1
     2/0/1:+1/+1
@@ -245,7 +239,7 @@ off-line demos. The TileStores passed as arguments to `tc-viewer` are available
 where `{index}` is the index of the TileStore on the command line (starting from 0 for the first tile store),
 and `{z}`, `{x}` and `{y}` are the components of the tile coordinate. The second `tiles` in the URL is present
 to work around assumptions made by OpenWebGlobe. This layout is directly usable by most mapping libraries, see
-the code in `views/*.tpl` for examples. The host and port can be set with the `--host` and `--port` command
+the code in `tilecloud/views/*.tpl` for examples. The host and port can be set with the `--host` and `--port` command
 line options, respectively.
 
 Note that there is no file extension. `tc-viewer` will automatically set the correct content type and content
diff --git a/ci/config.yaml b/ci/config.yaml
index cca0cd28..f4280018 100644
--- a/ci/config.yaml
+++ b/ci/config.yaml
@@ -1,4 +1,4 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/camptocamp/c2cciutils/1.6.14/c2cciutils/schema.json
+# yaml-language-server: $schema=https://raw.githubusercontent.com/camptocamp/c2cciutils/1.6.12/c2cciutils/schema.json
 
 publish:
   docker:
diff --git a/ci/requirements.txt b/ci/requirements.txt
index fdae50e3..8d126dac 100644
--- a/ci/requirements.txt
+++ b/ci/requirements.txt
@@ -1,4 +1,4 @@
-c2cciutils[checks,publish]==1.6.14
+c2cciutils[checks,publish]==1.6.12
 pre-commit==3.6.0
 poetry-dynamic-versioning==1.2.0
 poetry-plugin-export==1.6.0
diff --git a/examples/renderingtheworld.py b/examples/renderingtheworld.py
index 6c72a8e3..3e640277 100755
--- a/examples/renderingtheworld.py
+++ b/examples/renderingtheworld.py
@@ -37,7 +37,7 @@ def main() -> None:
         url="http://v2.suite.opengeo.org/geoserver/gwc/service/wmts/",
         layer="medford:buildings",
         style="_null",
-        format="image/png",
+        format_pattern="image/png",
         tile_matrix_set="EPSG:900913",
         tile_matrix=lambda z: f"EPSG:900913:{z:d}",
     )
diff --git a/poetry.lock b/poetry.lock
index 22ed69d9..f3312e96 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -111,17 +111,17 @@ yaml = ["PyYAML"]
 
 [[package]]
 name = "boto3"
-version = "1.34.15"
+version = "1.34.11"
 description = "The AWS SDK for Python"
 optional = true
 python-versions = ">= 3.8"
 files = [
-    {file = "boto3-1.34.15-py3-none-any.whl", hash = "sha256:f8f16c2d0ec1dca291857f1c138d5c30e01e40f653443cc2679e2f6ae71b05a6"},
-    {file = "boto3-1.34.15.tar.gz", hash = "sha256:2b74c58f475ff0dcf2f3637da9367a9465d29fad971ff5d8dc54ac39554e9022"},
+    {file = "boto3-1.34.11-py3-none-any.whl", hash = "sha256:1af021e0c6e3040e8de66d403e963566476235bb70f9a8e3f6784813ac2d8026"},
+    {file = "boto3-1.34.11.tar.gz", hash = "sha256:31c130a40ec0631059b77d7e87f67ad03ff1685a5b37638ac0c4687026a3259d"},
 ]
 
 [package.dependencies]
-botocore = ">=1.34.15,<1.35.0"
+botocore = ">=1.34.11,<1.35.0"
 jmespath = ">=0.7.1,<2.0.0"
 s3transfer = ">=0.10.0,<0.11.0"
 
@@ -130,13 +130,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
 
 [[package]]
 name = "botocore"
-version = "1.34.15"
+version = "1.34.11"
 description = "Low-level, data-driven core of boto 3."
 optional = true
 python-versions = ">= 3.8"
 files = [
-    {file = "botocore-1.34.15-py3-none-any.whl", hash = "sha256:16bcf871e67ef0177593f06e9e5bae4db51c9a9a2e953cb14feeb42d53441a85"},
-    {file = "botocore-1.34.15.tar.gz", hash = "sha256:c3c3404962a6d9d5e1634bd70ed53b8eff1ff17ee9d7a6240e9e8c94db48ad6f"},
+    {file = "botocore-1.34.11-py3-none-any.whl", hash = "sha256:1ff1398b6ea670e1c01ac67a33af3da854f8e700d3528289c04f319c330d8250"},
+    {file = "botocore-1.34.11.tar.gz", hash = "sha256:51905c3d623c60df5dc5794387de7caf886d350180a01a3dfa762e903edb45a9"},
 ]
 
 [package.dependencies]
@@ -1504,13 +1504,13 @@ test = ["setuptools (>=60)", "zest.releaser[recommended]"]
 
 [[package]]
 name = "pytest"
-version = "7.4.4"
+version = "7.4.3"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
-    {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+    {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"},
+    {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"},
 ]
 
 [package.dependencies]
@@ -1959,13 +1959,13 @@ cryptography = ">=35.0.0"
 
 [[package]]
 name = "types-redis"
-version = "4.6.0.20240106"
+version = "4.6.0.11"
 description = "Typing stubs for redis"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.7"
 files = [
-    {file = "types-redis-4.6.0.20240106.tar.gz", hash = "sha256:2b2fa3a78f84559616242d23f86de5f4130dfd6c3b83fb2d8ce3329e503f756e"},
-    {file = "types_redis-4.6.0.20240106-py3-none-any.whl", hash = "sha256:912de6507b631934bd225cdac310b04a58def94391003ba83939e5a10e99568d"},
+    {file = "types-redis-4.6.0.11.tar.gz", hash = "sha256:c8cfc84635183deca2db4a528966c5566445fd3713983f0034fb0f5a09e0890d"},
+    {file = "types_redis-4.6.0.11-py3-none-any.whl", hash = "sha256:94fc61118601fb4f79206b33b9f4344acff7ca1d7bba67834987fb0efcf6a770"},
 ]
 
 [package.dependencies]
@@ -2230,4 +2230,4 @@ wsgi = ["pyramid"]
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.9,<3.12"
-content-hash = "74f9ab283c1998e8823c02946b6c290e09a120d641790488fd3e8f969b33a43a"
+content-hash = "aafc66f0dae02d9b541d9b4418c3727a95be099ec240bb0a7dabca57b34724e9"
diff --git a/pyproject.toml b/pyproject.toml
index 7b56757c..a80de387 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,7 +52,7 @@ tc-viewer = "tilecloud.scripts.tc_viewer:main"
 python = ">=3.9,<3.12"
 azure-storage-blob = { version = "12.19.0", optional = true }
 azure-identity = { version = "1.15.0", optional = true }
-boto3 = { version = "1.34.15", optional = true }
+boto3 = { version = "1.34.11", optional = true }
 bottle = "0.12.25"
 prometheus_client = { version = "0.19.0", optional = true }
 Pillow = "10.1.0"
@@ -73,10 +73,10 @@ all = ["azure-storage-blob", "azure-identity", "boto3", "pyramid", "redis", "pro
 prospector = { version = "1.10.3", extras = ["with_bandit", "with_mypy", "with_pyroma"] }
 prospector-profile-duplicated = "0.5.1"
 prospector-profile-utils = "1.6.0"
-pytest = "7.4.4"
+pytest = "7.4.3"
 pytest-cov = "4.1.0"
 types-boto = "2.49.18.9"
-types-redis = "4.6.0.20240106"
+types-redis = "4.6.0.11"
 types-requests = "2.31.0.2"
 pycairo = "1.25.1"
 Shapely = "2.0.2"
diff --git a/requirements.txt b/requirements.txt
index 168bd7fa..496e9e46 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,4 +2,3 @@ poetry==1.7.1
 poetry-plugin-export==1.6.0
 poetry-plugin-tweak-dependencies-version==1.5.2
 poetry-dynamic-versioning==1.2.0
-pip==23.3.2
diff --git a/tilecloud/lib/sqlite3_.py b/tilecloud/lib/sqlite3_.py
index 3f9bd03d..0e08aba5 100644
--- a/tilecloud/lib/sqlite3_.py
+++ b/tilecloud/lib/sqlite3_.py
@@ -12,7 +12,7 @@ def query(connection: Connection, *args: Any) -> Cursor:
 
 
 if TYPE_CHECKING:
-    Base = MutableMapping[Union[str, TileCoord], bytes]
+    Base = MutableMapping[Union[str, TileCoord], Optional[bytes]]
 else:
     Base = MutableMapping
 
@@ -44,10 +44,10 @@ class SQLiteDict(Base):
         if self.commit:
             self.connection.commit()
 
-    def __getitem__(self, key: Union[str, TileCoord]) -> bytes:
+    def __getitem__(self, key: Union[str, TileCoord]) -> Optional[bytes]:
         row = query(self.connection, self.GETITEM_SQL, self._packkey(key)).fetchone()  # type: ignore
         if row is None:
-            raise KeyError(key)
+            return None
         return self._unpackvalue(row)
 
     def __iter__(self) -> Iterator[str]:
diff --git a/tilecloud/scripts/tc_viewer.py b/tilecloud/scripts/tc_viewer.py
index d0da957e..1c5e6056 100755
--- a/tilecloud/scripts/tc_viewer.py
+++ b/tilecloud/scripts/tc_viewer.py
@@ -10,13 +10,15 @@ import bottle
 from tilecloud import BoundingPyramid, Bounds, Tile, TileCoord, TileStore
 from tilecloud.filter.contenttype import ContentTypeAdder
 
+_root = None
+
 
 @bottle.route("/tiles/<index:int>/tiles/<z:int>/<x:int>/<y:int><ext:re:.*>")
 def tile(index, z, x, y, ext):
     # FIXME check ext
     if len(tilestores) < index:
         bottle.abort(404)
-    tilecoord = TileCoord(z + root.z, x + root.x * (1 << z), y + root.y * (1 << z))
+    tilecoord = TileCoord(z + _root.z, x + _root.x * (1 << z), y + _root.y * (1 << z))
     if cache is not None and (index, z, x, y) in cache:
         tile = cache[(index, z, x, y)]
     else:
@@ -125,6 +127,8 @@ def index():
 
 
 def main() -> None:
+    global _root
+
     option_parser = OptionParser()
     option_parser.add_option("--cache", action="store_true")
     option_parser.add_option("--debug", action="store_true", default=False)
@@ -141,9 +145,9 @@ def main() -> None:
         bottle.DEBUG = True
     if options.root:
         match = re.match(r"(\d+)/(\d+)/(\d+)\Z", options.root)
-        TileCoord(*map(int, match.groups()))
+        _root = TileCoord(*map(int, match.groups()))
     else:
-        TileCoord(0, 0, 0)
+        _root = TileCoord(0, 0, 0)
     if options.server is None:
         try:
             import tornado.httpserver
@@ -162,6 +166,7 @@ def main() -> None:
     tilestores = [(os.path.basename(arg), TileStore.load(arg)) for arg in args]
     content_type_adder = ContentTypeAdder()
 
+    bottle.TEMPLATE_PATH.append(os.path.join(os.path.dirname(os.path.dirname(__file__)), "views"))
     bottle.run(
         host=options.host,
         port=options.port,
diff --git a/tilecloud/store/debug.py b/tilecloud/store/debug.py
index 8ea8c04e..fc4bd5ba 100644
--- a/tilecloud/store/debug.py
+++ b/tilecloud/store/debug.py
@@ -20,12 +20,15 @@ class DebugTileStore(TileStore):
         draw.line([(0, 255), (0, 0), (255, 0)], fill=self.color)
         text = str(tile.tilecoord)
         font = PIL.ImageFont.load_default()
-        width, height = font.getsize(text)
+        bbox = font.getbbox(text)
+        width = bbox[2] - bbox[0]
+        height = bbox[3] - bbox[1]
         draw.text((127 - width / 2, 127 - height / 2), text, fill=self.color, font=font)
         bytes_io = BytesIO()
         assert self.content_type is not None
         image.save(bytes_io, FORMAT_BY_CONTENT_TYPE[self.content_type])
         tile.data = bytes_io.getvalue()
+        tile.content_type = self.content_type
         return tile
 
     def put_one(self, tile: Tile) -> Tile:
diff --git a/views/googlemaps.tpl b/tilecloud/views/googlemaps.tpl
similarity index 100%
rename from views/googlemaps.tpl
rename to tilecloud/views/googlemaps.tpl
diff --git a/views/index.tpl b/tilecloud/views/index.tpl
similarity index 100%
rename from views/index.tpl
rename to tilecloud/views/index.tpl
diff --git a/views/jquerygeo.tpl b/tilecloud/views/jquerygeo.tpl
similarity index 100%
rename from views/jquerygeo.tpl
rename to tilecloud/views/jquerygeo.tpl
diff --git a/views/leaflet.tpl b/tilecloud/views/leaflet.tpl
similarity index 100%
rename from views/leaflet.tpl
rename to tilecloud/views/leaflet.tpl
diff --git a/views/modestmaps.tpl b/tilecloud/views/modestmaps.tpl
similarity index 100%
rename from views/modestmaps.tpl
rename to tilecloud/views/modestmaps.tpl
diff --git a/tilecloud/views/openlayers.tpl b/tilecloud/views/openlayers.tpl
new file mode 100644
index 00000000..3b92f387
--- /dev/null
+++ b/tilecloud/views/openlayers.tpl
@@ -0,0 +1,77 @@
+<!doctype html>
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+        <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
+
+        <link
+          rel="stylesheet"
+          href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.9.0/css/ol.css"
+          crossorigin="anonymous"
+          referrerpolicy="no-referrer"
+        >
+        <link
+          rel="stylesheet"
+          href="https://unpkg.com/ol-layerswitcher@3.8.3/dist/ol-layerswitcher.css"
+          crossorigin="anonymous"
+          referrerpolicy="no-referrer"
+        />
+        <script
+          type="text/javascript"
+          src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.9.0/build/ol.js"
+          crossorigin="anonymous"
+          referrerpolicy="no-referrer"
+        ></script>
+        <script
+          type="text/javascript"
+          src="https://unpkg.com/ol-layerswitcher@3.8.3"
+          crossorigin="anonymous"
+          referrerpolicy="no-referrer"
+        ></script>
+        <style type="text/css">
+            html, body, #map {
+                margin: 0;
+                padding: 0;
+                height: 100%;
+            }
+        </style>
+        <title>OpenLayers - TileCloud</title>
+    </head>
+    <body>
+        <div id="map" style="width: 100%; height: 100%;">
+        </div>
+        <script type="text/javascript">
+            var layers = [];
+%for index, (name, tilestore) in enumerate(tilestores):
+%if tilestore.content_type is None or tilestore.content_type.startswith('image/'):
+            layers.push(new ol.layer.Tile({
+                source: new ol.source.XYZ({
+                    url: '/tiles/{{index}}/tiles/{z}/{x}/{y}',
+                }),
+                title: '{{name}}',
+%if getattr(tilestore, 'attribution', None):
+                attribution: '{{!tilestore.attribution}}',
+%end
+            }));
+%end
+%end
+            var map = new ol.Map({
+                target: "map",
+                layers: layers,
+                view: new ol.View({
+                    center: [0, 0],
+                    zoom: 2,
+%if resolutions:
+                    resolutions: [{{resolutions}}],
+%end
+                }),
+            });
+%if len(tilestores) > 1:
+            map.addControl(new ol.control.LayerSwitcher({
+              activationMode: 'click',
+              startActive: true
+            }));
+%end
+        </script>
+    </body>
+</html>
diff --git a/views/openwebglobe.tpl b/tilecloud/views/openwebglobe.tpl
similarity index 100%
rename from views/openwebglobe.tpl
rename to tilecloud/views/openwebglobe.tpl
diff --git a/views/polymaps.tpl b/tilecloud/views/polymaps.tpl
similarity index 100%
rename from views/polymaps.tpl
rename to tilecloud/views/polymaps.tpl
diff --git a/tiles/openstreetmap_org.py b/tiles/openstreetmap_org.py
index 84a66cea..5661f3e5 100644
--- a/tiles/openstreetmap_org.py
+++ b/tiles/openstreetmap_org.py
@@ -3,9 +3,10 @@ from tilecloud.store.url import URLTileStore
 
 tilestore = URLTileStore(
     (
-        TemplateTileLayout(f"http://{server!s}.tile.openstreetmap.org/%(z)d/%(x)d/%(y)d.png")
+        TemplateTileLayout(f"https://{server!s}.tile.openstreetmap.org/%(z)d/%(x)d/%(y)d.png")
         for server in "abc"
     ),
     attribution="&copy; OpenStreetMap contributors, CC-BY-SA",
     content_type="image/png",
+    headers={"User-Agent": "TileCloud"},
 )
diff --git a/views/openlayers.tpl b/views/openlayers.tpl
deleted file mode 100644
index 5c83d28b..00000000
--- a/views/openlayers.tpl
+++ /dev/null
@@ -1,53 +0,0 @@
-<!doctype html>
-<html>
-    <head>
-        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-        <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
-        <script type="text/javascript" src="http://www.openlayers.org/api/OpenLayers.js"></script>
-        <style type="text/css">
-            html, body, #map {
-                margin: 0;
-                padding: 0;
-                height: 100%;
-            }
-        </style>
-        <title>OpenLayers - TileCloud</title>
-    </head>
-    <body>
-        <div id="map" style="width: 100%; height: 100%;">
-        </div>
-        <script type="text/javascript">
-            var map = new OpenLayers.Map({
-                div: "map",
-%if max_extent:
-                maxExtent: new OpenLayers.Bounds({{max_extent}}),
-%end
-%if resolutions:
-                resolutions: [{{resolutions}}],
-                maxResolution: 'auto',
-%end
-                allOverlays: true
-            });
-%for index, (name, tilestore) in enumerate(tilestores):
-%if tilestore.content_type is None or tilestore.content_type.startswith('image/'):
-            map.addLayer(new OpenLayers.Layer.XYZ('{{name}}', '/tiles/{{index}}/tiles/${z}/${x}/${y}', {
-%if getattr(tilestore, 'attribution', None):
-                attribution: '{{!tilestore.attribution}}',
-%end
-                transitionEffect: 'resize',
-%if max_extent or resolutions:
-%pass
-%else:
-                numZoomLevels: 32,
-                sphericalMercator: true
-%end
-            }));
-%end
-%end
-%if len(tilestores) > 1:
-            map.addControl(new OpenLayers.Control.LayerSwitcher());
-%end
-            map.zoomToMaxExtent();
-        </script>
-    </body>
-</html>


Test patch:
diff --git a/tilecloud/tests/test_tilestore.py b/tilecloud/tests/test_tilestore.py
index fc869dd0..8a1a2311 100644
--- a/tilecloud/tests/test_tilestore.py
+++ b/tilecloud/tests/test_tilestore.py
@@ -156,7 +156,7 @@ class TestMBTilesTileStore(unittest.TestCase):
         connection = sqlite3.connect(":memory:")
         tilestore = MBTilesTileStore(connection)
         self.assertEqual(len(tilestore), 0)
-        self.assertEqual(tilestore.get_one(Tile(TileCoord(0, 0, 0))), None)
+        self.assertEqual(tilestore.get_one(Tile(TileCoord(0, 0, 0))), Tile(TileCoord(0, 0, 0)))
 
 
 class TestNullTileStore(unittest.TestCase):