build backend in bin. choose several maps for action.

This commit is contained in:
parent ed272c5973
commit f8ae8d7d54
22 changed files with 1750 additions and 329 deletions

46
Docs/nuitka.md Normal file
View file

@ -0,0 +1,46 @@
## Nuitka build (backend)
### Важно
- `gdal2tiles.py` и GDAL — это системные бинарники/библиотеки, Nuitka их не “впаяет” в один файл. На целевой машине GDAL всё равно должен быть установлен, если ты используешь `/tiles/slice` (нарезку).
- Для onefile-режима бинарник распаковывается во временную папку, поэтому проектные папки внутри пакета не должны использоваться для записи. В коде это решено через `GNSS_DATA_DIR`.
### Где лежат данные
По умолчанию:
- в обычном режиме (запуск из исходников) данные лежат рядом с пакетом (`gnss/`).
- в “замороженном” режиме (`nuitka --onefile`) данные идут в `~/.local/share/gnss`.
Можно переопределить:
- `GNSS_DATA_DIR=/path/to/data`
Структура:
- `$GNSS_DATA_DIR/tilesets/<tileset>/...`
- `$GNSS_DATA_DIR/uploads/...`
- `$GNSS_DATA_DIR/tiles/...` (опционально)
### Запуск без Nuitka (для проверки)
`poetry run gnss-backend --host 0.0.0.0 --port 8000`
### Установка Nuitka (в Poetry)
Рекомендовано держать Nuitka как dev-зависимость:
`poetry add --group dev nuitka`
### Сборка (standalone + onefile)
Пример:
`poetry run python -m nuitka --onefile --standalone --follow-imports --assume-yes-for-downloads --output-dir=dist --output-filename=gnss-backend gnss/__main__.py`
После сборки:
- бинарник будет в `dist/` (например `dist/gnss-backend.bin`)
### Запуск бинарника
`GNSS_DATA_DIR=./data ./dist/gnss-backend.bin --host 0.0.0.0 --port 8000`
Положи tileset’ы сюда:
- `./data/tilesets/<name>/...`

View file

@ -218,6 +218,14 @@ body, #root {
color: var(--muted);
}
.empty code {
color: #e2e8f0;
background: rgba(255, 255, 255, 0.06);
padding: 2px 6px;
border-radius: 8px;
word-break: break-word;
}
.leaflet-control-container {
filter: none;
}

File diff suppressed because it is too large Load diff

34
gnss/__main__.py Normal file
View file

@ -0,0 +1,34 @@
import argparse
import os
from typing import Optional
import uvicorn
from gnss.main import app
def _env(name: str, default: str) -> str:
v = os.getenv(name)
return v if v is not None and v != "" else default
def main(argv: Optional[list[str]] = None) -> int:
parser = argparse.ArgumentParser(prog="gnss", description="GNSS Backend (FastAPI)")
parser.add_argument("--host", default=_env("GNSS_HOST", "0.0.0.0"))
parser.add_argument("--port", type=int, default=int(_env("GNSS_PORT", "8000")))
parser.add_argument("--log-level", default=_env("GNSS_LOG_LEVEL", "info"))
args = parser.parse_args(argv)
uvicorn.run(
app,
host=args.host,
port=args.port,
log_level=args.log_level,
access_log=True,
workers=1,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

Binary file not shown.

View file

@ -1,3 +1,15 @@
from .state import get_gnss_service, get_log_service, get_tile_service, get_vector_mbtiles_service
from .state import (
get_gnss_service,
get_log_service,
get_raster_mbtiles_service,
get_tile_service,
get_vector_mbtiles_service,
)
__all__ = ["get_gnss_service", "get_tile_service", "get_log_service", "get_vector_mbtiles_service"]
__all__ = [
"get_gnss_service",
"get_tile_service",
"get_log_service",
"get_raster_mbtiles_service",
"get_vector_mbtiles_service",
]

View file

@ -12,6 +12,8 @@ async def get_config() -> dict:
"tiles_url_tms": "/tiles/tms/{z}/{x}/{y}.png",
"vector_tiles_url": "/tiles/vector/{z}/{x}/{y}.pbf",
"vector_metadata_url": "/tiles/vector/metadata",
"vector_tileset_url": "/tilesets/active/vector/{z}/{x}/{y}.pbf",
"vector_tileset_metadata_url": "/tilesets/active/vector/metadata",
"tilesets_url": "/tilesets",
"tilesets_activate_url": "/tilesets/activate",
"ws_url": "/ws/track",

61
gnss/api/routes/maps.py Normal file
View file

@ -0,0 +1,61 @@
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from ..dependencies import get_raster_mbtiles_service, get_tile_service, get_vector_mbtiles_service
from ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService
from ...services.tiles import TileCutError, TileService
from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService
router = APIRouter(tags=["maps"])
class MapIngestRequest(BaseModel):
path: str
tileset: str | None = None
min_zoom: int = Field(default=10, ge=0)
max_zoom: int = Field(default=16, ge=0)
@router.post("/maps/ingest")
async def ingest_map(
payload: MapIngestRequest,
tile_service: TileService = Depends(get_tile_service),
raster_mbtiles: RasterMBTilesService = Depends(get_raster_mbtiles_service),
vector_mbtiles: VectorMBTilesService = Depends(get_vector_mbtiles_service),
) -> dict:
p = Path(payload.path).expanduser().resolve()
if not p.exists():
raise HTTPException(status_code=400, detail=f"path not found: {p}")
if p.suffix.lower() == ".mbtiles":
try:
info = vector_mbtiles.activate(p)
return {"status": "ok", "kind": "vector", "driver": "mbtiles", "path": str(info.path), "format": info.metadata.get("format")}
except VectorMBTilesError:
pass
try:
info = raster_mbtiles.activate(p)
except RasterMBTilesError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {"status": "ok", "kind": "raster", "driver": "mbtiles", "path": str(info.path), "format": info.metadata.get("format")}
if p.suffix.lower() in {".tif", ".tiff", ".vrt"}:
if payload.max_zoom < payload.min_zoom:
raise HTTPException(status_code=400, detail="max_zoom must be >= min_zoom")
try:
tile_service.slice_to_tiles(
source=p,
destination=tile_service.tiles_dir,
min_zoom=payload.min_zoom,
max_zoom=payload.max_zoom,
scheme="xyz",
)
except TileCutError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {"status": "ok", "kind": "raster", "driver": "gdal2tiles", "path": str(p), "tiles_dir": str(tile_service.tiles_dir)}
raise HTTPException(status_code=400, detail="unsupported input; expected .mbtiles or GeoTIFF/VRT")

View file

@ -0,0 +1,51 @@
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Response
from pydantic import BaseModel
from ..dependencies import get_raster_mbtiles_service
from ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService
router = APIRouter(tags=["raster-mbtiles"])
class ActivateRasterMBTiles(BaseModel):
path: str
@router.post("/tiles/raster/activate")
async def activate_raster_mbtiles(
payload: ActivateRasterMBTiles,
service: RasterMBTilesService = Depends(get_raster_mbtiles_service),
):
try:
info = service.activate(Path(payload.path))
except RasterMBTilesError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {"status": "ok", "path": str(info.path), "format": info.metadata.get("format")}
@router.get("/tiles/raster/metadata")
async def raster_metadata(service: RasterMBTilesService = Depends(get_raster_mbtiles_service)):
try:
return service.get_metadata()
except RasterMBTilesError as exc:
raise HTTPException(status_code=400, detail=str(exc))
@router.get("/tiles/raster/{z}/{x}/{y}")
async def raster_tile(
z: int,
x: int,
y: int,
service: RasterMBTilesService = Depends(get_raster_mbtiles_service),
):
try:
data, media_type = service.get_tile(z=z, x=x, y_xyz=y)
except KeyError:
raise HTTPException(status_code=404, detail="tile not found")
except RasterMBTilesError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return Response(content=data, media_type=media_type)

View file

@ -3,23 +3,53 @@ from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, UploadFile
from fastapi.responses import JSONResponse
from ...api.dependencies import get_tile_service
import json
from ...api.dependencies import get_raster_mbtiles_service, get_tile_service
from ...models.tiles import TileSliceRequest
from ...services.tiles import TileCutError, TileService
from ... import config
from ...services.tilesets import TilesetService, TilesetError
from ...services.raster_mbtiles import RasterMBTilesService
router = APIRouter(tags=["tiles"])
@router.get("/tiles/{zoom}/{x}/{y}.png")
async def serve_tile(zoom: int, x: int, y: int, tile_service: TileService = Depends(get_tile_service)):
async def serve_tile(
zoom: int,
x: int,
y: int,
tile_service: TileService = Depends(get_tile_service),
raster_mbtiles: RasterMBTilesService = Depends(get_raster_mbtiles_service),
):
hit = tile_service._tiles_dir_tile_path(zoom, x, y)
if hit:
return tile_service.get_tile_response(zoom, x, y)
try:
data, media_type = raster_mbtiles.get_tile(z=zoom, x=x, y_xyz=y)
except Exception:
return tile_service.get_tile_response(zoom, x, y)
return tile_service.bytes_response(data=data, media_type=media_type)
@router.get("/tiles/tms/{zoom}/{x}/{y}.png")
async def serve_tile_tms(zoom: int, x: int, y: int, tile_service: TileService = Depends(get_tile_service)):
async def serve_tile_tms(
zoom: int,
x: int,
y: int,
tile_service: TileService = Depends(get_tile_service),
raster_mbtiles: RasterMBTilesService = Depends(get_raster_mbtiles_service),
):
y_converted = (2**zoom - 1) - y
hit = tile_service._tiles_dir_tile_path(zoom, x, y_converted)
if hit:
return tile_service.get_tile_response(zoom, x, y_converted)
try:
data, media_type = raster_mbtiles.get_tile(z=zoom, x=x, y_xyz=y_converted)
except Exception:
return tile_service.get_tile_response_tms(zoom, x, y)
return tile_service.bytes_response(data=data, media_type=media_type)
@router.post("/tiles/slice")
@ -53,12 +83,35 @@ async def slice_tiles(payload: TileSliceRequest, tile_service: TileService = Dep
)
except TileCutError as exc:
raise HTTPException(status_code=400, detail=str(exc))
active_tileset = tileset_name or tileset_svc.get_active()
try:
tileset_root = tileset_svc.resolve(active_tileset)
(tileset_root / "gnss_tileset.json").write_text(
json.dumps(
{
"tileset": active_tileset,
"scheme": payload.scheme,
"source_format": payload.source_format,
"tile_format": payload.tile_format,
"min_zoom": payload.min_zoom,
"max_zoom": payload.max_zoom,
"source": str(source),
"destination": str(destination),
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
except Exception:
pass
return {
"status": "ok",
"tiles_dir": str(destination),
"source": str(source),
"scheme": payload.scheme,
"tileset": tileset_name or tileset_svc.get_active(),
"tileset": active_tileset,
}

View file

@ -1,4 +1,8 @@
from fastapi import APIRouter, Depends, HTTPException
import json
import math
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Response
from pydantic import BaseModel
from ..dependencies import get_tile_service
@ -22,10 +26,185 @@ class ActivateTileset(BaseModel):
name: str
def _is_gzipped(data: bytes) -> bool:
return len(data) >= 2 and data[0] == 0x1F and data[1] == 0x8B
def _detect_kind(path: Path) -> str:
meta = path / "metadata.json"
if meta.is_file():
try:
parsed = json.loads(meta.read_text(encoding="utf-8"))
fmt = str(parsed.get("format") or "").lower()
if fmt in {"pbf", "mvt"}:
return "vector-dir"
except Exception:
pass
if any(path.rglob("*.png")) or any(path.rglob("*.jpg")) or any(path.rglob("*.jpeg")) or any(path.rglob("*.webp")):
return "raster"
if any(path.rglob("*.pbf")):
return "vector-dir"
return "unknown"
def _tile_y_to_lat(y: float, z: int) -> float:
n = math.pi - (2.0 * math.pi * y) / (2**z)
return math.degrees(math.atan(math.sinh(n)))
def _tile_x_to_lon(x: float, z: int) -> float:
return (x / (2**z)) * 360.0 - 180.0
def _scan_xyz_tile_matrix_extent(path: Path, z_value: int) -> tuple[int, int, int, int] | None:
z_dir = path / str(z_value)
if not z_dir.is_dir():
return None
min_x = None
max_x = None
min_y = None
max_y = None
for xdir in z_dir.iterdir():
if not xdir.is_dir():
continue
try:
x = int(xdir.name)
except Exception:
continue
min_x = x if min_x is None else min(min_x, x)
max_x = x if max_x is None else max(max_x, x)
for f in xdir.iterdir():
if not f.is_file():
continue
if f.suffix.lower() not in {".png", ".jpg", ".jpeg", ".webp"}:
continue
try:
y = int(f.stem)
except Exception:
continue
min_y = y if min_y is None else min(min_y, y)
max_y = y if max_y is None else max(max_y, y)
if min_x is None or max_x is None or min_y is None or max_y is None:
return None
return min_x, min_y, max_x, max_y
def _tile_matrix_to_bounds(x_min: int, y_min: int, x_max: int, y_max: int, z: int) -> tuple[float, float, float, float]:
west = _tile_x_to_lon(x_min, z)
east = _tile_x_to_lon(x_max + 1, z)
north = _tile_y_to_lat(y_min, z)
south = _tile_y_to_lat(y_max + 1, z)
return west, south, east, north
def _parse_mapml_extent(path: Path) -> dict | None:
mapml = path / "mapml.mapml"
if not mapml.is_file():
return None
text = mapml.read_text(encoding="utf-8", errors="ignore")
import re
z_tag = re.search(r'<input[^>]*name="z"[^>]*>', text)
x_tag = re.search(r'<input[^>]*name="x"[^>]*>', text)
y_tag = re.search(r'<input[^>]*name="y"[^>]*>', text)
if not z_tag or not x_tag or not y_tag:
return None
def _attrs(tag: str) -> dict[str, str]:
return {m.group(1): m.group(2) for m in re.finditer(r'(\w+)="([^"]+)"', tag)}
z_attrs = _attrs(z_tag.group(0))
x_attrs = _attrs(x_tag.group(0))
y_attrs = _attrs(y_tag.group(0))
try:
z_value = int(z_attrs.get("value", z_attrs.get("max", "0")))
z_min = int(z_attrs.get("min", "0"))
z_max = int(z_attrs.get("max", str(z_value)))
x_min = int(x_attrs["min"])
x_max = int(x_attrs["max"])
y_min = int(y_attrs["min"])
y_max = int(y_attrs["max"])
except Exception:
return None
scanned = _scan_xyz_tile_matrix_extent(path, z_value)
if scanned:
x_min, y_min, x_max, y_max = scanned
west, south, east, north = _tile_matrix_to_bounds(x_min, y_min, x_max, y_max, z_value)
return {
"scheme": "xyz",
"z_value": z_value,
"minzoom": z_min,
"maxzoom": z_max,
"bounds": [west, south, east, north],
"center": [(west + east) / 2.0, (south + north) / 2.0],
"tile_matrix": {"z": z_value, "x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max},
}
def _parse_leaflet_bounds(path: Path) -> dict | None:
leaflet = path / "leaflet.html"
if not leaflet.is_file():
return None
text = leaflet.read_text(encoding="utf-8", errors="ignore")
import re
m = re.search(
r"map\\.fitBounds\\(\\[\\[([0-9.+-]+),\\s*([0-9.+-]+)\\],\\s*\\[([0-9.+-]+),\\s*([0-9.+-]+)\\]\\]\\)",
text,
)
if not m:
return None
lat1, lon1, lat2, lon2 = map(float, m.groups())
south = min(lat1, lat2)
north = max(lat1, lat2)
west = min(lon1, lon2)
east = max(lon1, lon2)
mzoom = re.search(r"minZoom:\\s*(\\d+)", text)
mxzoom = re.search(r"maxZoom:\\s*(\\d+)", text)
mcenter = re.search(r"center:\\s*\\[([0-9.+-]+),\\s*([0-9.+-]+)\\]", text)
minzoom = int(mzoom.group(1)) if mzoom else None
maxzoom = int(mxzoom.group(1)) if mxzoom else None
center = (
[float(mcenter.group(2)), float(mcenter.group(1))]
if mcenter
else [(west + east) / 2.0, (south + north) / 2.0]
)
return {
"scheme": "xyz",
"bounds": [west, south, east, north],
"center": center,
"minzoom": minzoom,
"maxzoom": maxzoom,
}
@router.get("/tilesets")
async def list_tilesets() -> dict:
svc = _tileset_service()
return {"tilesets": svc.list_tilesets(), "active": svc.get_active()}
tilesets = svc.list_tilesets()
kinds = {name: _detect_kind(svc.resolve(name)) for name in tilesets}
extents = {}
for name in tilesets:
p = svc.resolve(name)
e = _parse_mapml_extent(p) or _parse_leaflet_bounds(p)
if e:
extents[name] = e
return {"tilesets": tilesets, "active": svc.get_active(), "kinds": kinds, "extents": extents}
@router.get("/tilesets/active/extent")
async def active_tileset_extent() -> dict:
svc = _tileset_service()
tileset_dir = svc.resolve(None)
extent = _parse_mapml_extent(tileset_dir) or _parse_leaflet_bounds(tileset_dir)
if not extent:
raise HTTPException(status_code=404, detail="no extent info for active tileset")
extent["tileset"] = svc.get_active()
return extent
@router.post("/tilesets")
@ -47,3 +226,33 @@ async def activate_tileset(payload: ActivateTileset, tile_service: TileService =
raise HTTPException(status_code=400, detail=str(exc))
tile_service.set_tiles_dir(svc.resolve(active))
return {"status": "ok", "active": active}
@router.get("/tilesets/active/vector/metadata")
async def active_vector_metadata() -> dict:
svc = _tileset_service()
tileset_dir = svc.resolve(None)
meta_path = tileset_dir / "metadata.json"
if not meta_path.is_file():
raise HTTPException(status_code=400, detail="active tileset has no metadata.json (not a MapTiler vector folder)")
try:
parsed = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception as exc:
raise HTTPException(status_code=400, detail=f"failed to parse metadata.json: {exc}")
parsed["tileset"] = svc.get_active()
return parsed
@router.get("/tilesets/active/vector/{z}/{x}/{y}.pbf")
async def active_vector_tile(z: int, x: int, y: int) -> Response:
svc = _tileset_service()
tileset_dir = svc.resolve(None)
tile_path = tileset_dir / str(z) / str(x) / f"{y}.pbf"
if not tile_path.is_file():
raise HTTPException(status_code=404, detail="tile not found")
data = tile_path.read_bytes()
headers = {}
if _is_gzipped(data):
headers["Content-Encoding"] = "gzip"
return Response(content=data, media_type="application/x-protobuf", headers=headers)

View file

@ -3,21 +3,28 @@ from typing import Optional
from ..services.gnss import GNSSService
from ..services.logs import LogService
from ..services.tiles import TileService
from ..services.raster_mbtiles import RasterMBTilesService
from ..services.vector_mbtiles import VectorMBTilesService
_gnss_service: Optional[GNSSService] = None
_tile_service: Optional[TileService] = None
_log_service: Optional[LogService] = None
_raster_mbtiles_service: Optional[RasterMBTilesService] = None
_vector_mbtiles_service: Optional[VectorMBTilesService] = None
def set_services(
gnss: GNSSService, tiles: TileService, logger: LogService, vector_mbtiles: VectorMBTilesService
gnss: GNSSService,
tiles: TileService,
logger: LogService,
raster_mbtiles: RasterMBTilesService,
vector_mbtiles: VectorMBTilesService,
) -> None:
global _gnss_service, _tile_service, _log_service, _vector_mbtiles_service
global _gnss_service, _tile_service, _log_service, _raster_mbtiles_service, _vector_mbtiles_service
_gnss_service = gnss
_tile_service = tiles
_log_service = logger
_raster_mbtiles_service = raster_mbtiles
_vector_mbtiles_service = vector_mbtiles
@ -43,3 +50,9 @@ def get_vector_mbtiles_service() -> VectorMBTilesService:
if _vector_mbtiles_service is None:
raise RuntimeError("Vector MBTiles service is not initialized")
return _vector_mbtiles_service
def get_raster_mbtiles_service() -> RasterMBTilesService:
if _raster_mbtiles_service is None:
raise RuntimeError("Raster MBTiles service is not initialized")
return _raster_mbtiles_service

View file

@ -1,16 +1,28 @@
import os
import sys
from pathlib import Path
BASE_DIR = Path(__file__).parent
TILES_DIR = BASE_DIR / "tiles"
TILESSETS_DIR = BASE_DIR / "tilesets"
def _default_data_dir() -> Path:
if getattr(sys, "frozen", False):
return Path.home() / ".local" / "share" / "gnss"
return BASE_DIR
DATA_DIR = Path(os.getenv("GNSS_DATA_DIR", str(_default_data_dir()))).expanduser()
TILES_DIR = DATA_DIR / "tiles"
TILESSETS_DIR = DATA_DIR / "tilesets"
DEFAULT_TILESET = "default"
TILE_PLACEHOLDER = BASE_DIR / "tiles" / "placeholder.png"
UPLOAD_DIR = BASE_DIR / "uploads"
TILE_PLACEHOLDER = TILES_DIR / "placeholder.png"
UPLOAD_DIR = DATA_DIR / "uploads"
GNSS_SOURCE = "simulated"
GNSS_PORT = "/dev/ttyAMA0"
GNSS_BAUD = 9600
GNSS_TIMEOUT = 1.0
SIM_START_POINT = (55.300, 61.350)
SIM_START_POINT = (55.1644, 61.4368)
SIM_PATH = "circle" # circle | rectangle
# Для симуляции движения по прямоугольнику раскомментируй строку ниже:
# SIM_PATH = "rectangle"

View file

@ -6,10 +6,11 @@ from fastapi.middleware.cors import CORSMiddleware
from . import config
from .api import state
from .api.routes import config_route, health, logs, tiles, tilesets, vector_tiles, ws
from .api.routes import config_route, health, logs, maps, raster_mbtiles, tiles, tilesets, vector_tiles, ws
from .services.gnss import GNSSService, RectangularPathSource, SerialNMEASource, SimulatedPathSource
from .services.logs import LogService
from .services.tiles import TileService
from .services.raster_mbtiles import RasterMBTilesService
from .services.vector_mbtiles import VectorMBTilesService
from .services.tilesets import TilesetService
@ -52,9 +53,16 @@ tiles_dir = tileset_service.resolve(None)
placeholder_tile = Path(config.TILE_PLACEHOLDER)
tile_service = TileService(tiles_dir=tiles_dir, placeholder=placeholder_tile, upload_dir=Path(config.UPLOAD_DIR))
log_service = LogService(base_dir=Path("logs"))
raster_mbtiles_service = RasterMBTilesService()
vector_mbtiles_service = VectorMBTilesService()
gnss_service = create_gnss_service()
state.set_services(gnss=gnss_service, tiles=tile_service, logger=log_service, vector_mbtiles=vector_mbtiles_service)
state.set_services(
gnss=gnss_service,
tiles=tile_service,
logger=log_service,
raster_mbtiles=raster_mbtiles_service,
vector_mbtiles=vector_mbtiles_service,
)
app = FastAPI(title="GNSS Backend", version="0.2.0")
@ -76,6 +84,8 @@ app.include_router(config_route.router)
app.include_router(tiles.router)
app.include_router(ws.router)
app.include_router(logs.router)
app.include_router(maps.router)
app.include_router(raster_mbtiles.router)
app.include_router(vector_tiles.router)
app.include_router(tilesets.router)

View file

@ -0,0 +1,121 @@
import sqlite3
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional
class RasterMBTilesError(RuntimeError):
pass
@dataclass(frozen=True)
class RasterMBTilesInfo:
path: Path
metadata: dict[str, Any]
def _read_metadata(conn: sqlite3.Connection) -> dict[str, str]:
cur = conn.execute("SELECT name, value FROM metadata")
return {name: value for name, value in cur.fetchall()}
def _has_table(conn: sqlite3.Connection, table: str) -> bool:
cur = conn.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
(table,),
)
return cur.fetchone() is not None
def _format_to_media_type(fmt: str) -> str:
f = (fmt or "").lower()
if f in {"jpg", "jpeg"}:
return "image/jpeg"
if f == "png":
return "image/png"
if f == "webp":
return "image/webp"
return "application/octet-stream"
class RasterMBTilesService:
def __init__(self):
self._lock = threading.Lock()
self._active: Optional[RasterMBTilesInfo] = None
def activate(self, path: Path) -> RasterMBTilesInfo:
p = Path(path).expanduser().resolve()
if not p.exists():
raise RasterMBTilesError(f"mbtiles not found: {p}")
if p.suffix.lower() != ".mbtiles":
raise RasterMBTilesError("expected .mbtiles file")
conn = sqlite3.connect(f"file:{p}?mode=ro", uri=True)
try:
metadata = _read_metadata(conn)
finally:
conn.close()
fmt = (metadata.get("format") or "").lower()
if fmt in {"pbf", "mvt"}:
raise RasterMBTilesError("this is a vector mbtiles; use /tiles/vector/activate")
if fmt not in {"png", "jpg", "jpeg", "webp"}:
raise RasterMBTilesError(f"mbtiles format is '{fmt or 'unknown'}'; expected raster png/jpg/webp")
info = RasterMBTilesInfo(path=p, metadata=metadata)
with self._lock:
self._active = info
return info
def get_active(self) -> RasterMBTilesInfo:
with self._lock:
if self._active is None:
raise RasterMBTilesError("no active raster mbtiles; upload/activate first")
return self._active
def get_metadata(self) -> dict[str, Any]:
info = self.get_active()
meta = dict(info.metadata)
meta["path"] = str(info.path)
meta["media_type"] = _format_to_media_type(str(info.metadata.get("format") or ""))
return meta
def get_tile(self, z: int, x: int, y_xyz: int) -> tuple[bytes, str]:
info = self.get_active()
if z < 0:
raise RasterMBTilesError("invalid z")
max_index = (2**z) - 1
if x < 0 or x > max_index or y_xyz < 0 or y_xyz > max_index:
raise RasterMBTilesError("invalid x/y for z")
y_tms = max_index - y_xyz
conn = sqlite3.connect(f"file:{info.path}?mode=ro", uri=True)
try:
if _has_table(conn, "tiles"):
cur = conn.execute(
"SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=? LIMIT 1",
(z, x, y_tms),
)
row = cur.fetchone()
if row is None:
raise KeyError("tile not found")
data = row[0]
elif _has_table(conn, "map") and _has_table(conn, "images"):
cur = conn.execute(
"SELECT images.tile_data FROM map JOIN images ON map.tile_id = images.tile_id "
"WHERE map.zoom_level=? AND map.tile_column=? AND map.tile_row=? LIMIT 1",
(z, x, y_tms),
)
row = cur.fetchone()
if row is None:
raise KeyError("tile not found")
data = row[0]
else:
raise RasterMBTilesError("unsupported mbtiles schema (no tiles table and no map/images tables)")
finally:
conn.close()
fmt = str(info.metadata.get("format") or "").lower()
return data, _format_to_media_type(fmt)

View file

@ -79,14 +79,31 @@ class TileService:
raise TileCutError(f"gdal2tiles failed: {result.stderr or result.stdout}")
return destination
def _tiles_dir_tile_path(self, zoom: int, x: int, y: int) -> tuple[Path, str] | None:
base = self.tiles_dir / str(zoom) / str(x) / f"{y}"
for ext, media_type in (
(".png", "image/png"),
(".jpg", "image/jpeg"),
(".jpeg", "image/jpeg"),
(".webp", "image/webp"),
):
candidate = base.with_suffix(ext)
if candidate.is_file():
return candidate, media_type
return None
def get_tile_response(self, zoom: int, x: int, y: int):
tile_path = self.tiles_dir / str(zoom) / str(x) / f"{y}.png"
if tile_path.is_file():
return FileResponse(tile_path, media_type="image/png")
hit = self._tiles_dir_tile_path(zoom, x, y)
if hit:
tile_path, media_type = hit
return FileResponse(tile_path, media_type=media_type)
if self.placeholder.is_file():
return FileResponse(self.placeholder, media_type="image/png")
return Response(content=DEFAULT_PLACEHOLDER_PNG, media_type="image/png")
def bytes_response(self, data: bytes, media_type: str):
return Response(content=data, media_type=media_type)
def get_tile_response_tms(self, zoom: int, x: int, y: int):
y_converted = (2**zoom - 1) - y
return self.get_tile_response(zoom, x, y_converted)

149
poetry.lock generated
View file

@ -194,6 +194,43 @@ files = [
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "nuitka"
version = "2.8.9"
description = "Python compiler with full language support and CPython compatibility"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "nuitka-2.8.9.tar.gz", hash = "sha256:b178cd437f2110c46943b368db51d20d57d586a13f8f6323ab1be4e51e2fabf8"},
]
[package.dependencies]
ordered-set = ">=4.1.0"
zstandard = ">=0.15"
[package.extras]
all = ["imageio", "setuptools (>=42)", "toml", "zstandard (>=0.15)"]
app = ["zstandard (>=0.15)"]
build-wheel = ["setuptools (>=42)", "toml", "wheel"]
icon-conversion = ["imageio"]
onefile = ["zstandard (>=0.15)"]
[[package]]
name = "ordered-set"
version = "4.1.0"
description = "An OrderedSet is a custom MutableSet that remembers its order, so that every"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"},
{file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"},
]
[package.extras]
dev = ["black", "mypy", "pytest"]
[[package]]
name = "pydantic"
version = "2.12.5"
@ -852,6 +889,118 @@ files = [
{file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"},
]
[[package]]
name = "zstandard"
version = "0.25.0"
description = "Zstandard bindings for Python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd"},
{file = "zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74"},
{file = "zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa"},
{file = "zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e"},
{file = "zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c"},
{file = "zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7"},
{file = "zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4"},
{file = "zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2"},
{file = "zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137"},
{file = "zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b"},
{file = "zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa"},
{file = "zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd"},
{file = "zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01"},
{file = "zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9"},
{file = "zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94"},
{file = "zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf"},
{file = "zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09"},
{file = "zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5"},
{file = "zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049"},
{file = "zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3"},
{file = "zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088"},
{file = "zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12"},
{file = "zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2"},
{file = "zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d"},
{file = "zstandard-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0"},
{file = "zstandard-0.25.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27"},
{file = "zstandard-0.25.0-cp39-cp39-win32.whl", hash = "sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649"},
{file = "zstandard-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860"},
{file = "zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b"},
]
[package.extras]
cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and python_version < \"3.14\"", "cffi (>=2.0.0b) ; platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4.0"

View file

@ -16,6 +16,14 @@ uvicorn = {extras = ["standard"], version = ">=0.30.0,<0.31.0"}
python-multipart = ">=0.0.21,<0.0.22"
gdal2tiles = "^0.1.9"
[tool.poetry.scripts]
gnss-backend = "gnss.__main__:main"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[dependency-groups]
dev = [
"nuitka (>=2.8.9,<3.0.0)"
]