Skip to content

Commit

Permalink
Updates from feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
JbannisterScottLogic committed May 3, 2024
1 parent 4648ae2 commit 266174d
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 221 deletions.
4 changes: 2 additions & 2 deletions application/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
fact,
guidance_,
about_,
tiles_,
tiles,
osMapOAuth,
)
from application.settings import get_settings
Expand Down Expand Up @@ -270,7 +270,7 @@ def add_routers(app):
app.include_router(map_.router, prefix="/map", include_in_schema=False)
app.include_router(guidance_.router, prefix="/guidance", include_in_schema=False)
app.include_router(about_.router, prefix="/about", include_in_schema=False)
app.include_router(tiles_.router, prefix="/tiles", include_in_schema=False)
app.include_router(tiles.router, prefix="/tiles", include_in_schema=False)


def add_static(app):
Expand Down
85 changes: 85 additions & 0 deletions application/routers/tiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import logging
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from io import BytesIO

from db.models import EntityOrm
from db.session import get_session

router = APIRouter()
logger = logging.getLogger(__name__)

# ============================================================
# Helper Funcs
# ============================================================


# Validate tile x/y coordinates at the given zoom level
def tile_is_valid(tile):
size = 2 ** tile["zoom"]
return (
0 <= tile["x"] < size
and 0 <= tile["y"] < size
and tile["format"] in ["pbf", "mvt"]
)


# Build the database query using SQLAlchemy ORM
def build_db_query(tile, session: Session):
envelope = func.ST_TileEnvelope(tile["zoom"], tile["x"], tile["y"])
bounds = func.ST_MakeEnvelope(-180, -85.0511287798066, 180, 85.0511287798066, 4326)

geometries = (
session.query(
EntityOrm.entity,
EntityOrm.name,
EntityOrm.reference,
func.ST_AsMVTGeom(EntityOrm.geometry, envelope),
)
.filter(
EntityOrm.dataset == tile["dataset"],
EntityOrm.geometry.ST_Intersects(envelope),
)
.subquery()
)

# Build vector tile
tile_data = session.query(func.ST_AsMVT(geometries, tile["dataset"])).scalar()

return tile_data


# ============================================================
# API Endpoints
# ============================================================


@router.get("/tiles/{dataset}/{z}/{x}/{y}.{fmt}")
async def read_tiles_from_postgres(
dataset: str,
z: int,
x: int,
y: int,
fmt: str,
session: Session = Depends(get_session),
):

tile = {"dataset": dataset, "zoom": z, "x": x, "y": y, "format": fmt}
if not tile_is_valid(tile):
raise HTTPException(status_code=400, detail=f"Invalid tile path: {tile}")

tile_data = build_db_query(tile, session)
if not tile_data:
raise HTTPException(status_code=404, detail="Tile data not found")

pbf_buffer = BytesIO(tile_data)
resp_headers = {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/vnd.mapbox-vector-tile",
}

return StreamingResponse(
pbf_buffer, media_type="vnd.mapbox-vector-tile", headers=resp_headers
)
161 changes: 0 additions & 161 deletions application/routers/tiles_.py

This file was deleted.

93 changes: 35 additions & 58 deletions tests/unit/routers/test_tiles.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from unittest.mock import MagicMock, patch
import pytest
from unittest.mock import patch, AsyncMock
from fastapi import HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy.future import select

from application.routers.tiles_ import (
read_tiles_from_postgres,
tile_is_valid,
build_db_query,
sql_to_pbf,
)
from application.routers.tiles import read_tiles_from_postgres, tile_is_valid
from application.db.models import EntityOrm
from application.db.session import get_session

# Constants for Testing
VALID_TILE_INFO = {
Expand Down Expand Up @@ -38,15 +37,9 @@ def invalid_tile():


@pytest.fixture
def mock_build_db_query():
with patch("application.routers.tiles_.build_db_query") as mock:
yield mock


@pytest.fixture
def mock_sql_to_pbf():
with patch("application.routers.tiles_.sql_to_pbf") as mock:
mock.return_value = b"sample_pbf_data"
def mock_session_maker():
with patch("db.session._get_fastapi_sessionmaker") as mock:
mock.return_value = AsyncMock(get_db=AsyncMock())
yield mock


Expand All @@ -60,60 +53,44 @@ def test_tile_is_invalid(invalid_tile):
), "Tile should be invalid with incorrect parameters"


def test_build_db_query(valid_tile):
query = build_db_query(valid_tile)
assert (
"SELECT" in query and "FROM" in query
), "SQL query should be properly formed with SELECT and FROM clauses"


@patch("application.routers.tiles_.psycopg2.connect")
def test_sql_to_pbf(mock_connect, valid_tile):
mock_conn = MagicMock()
mock_cursor = MagicMock()
mock_connect.return_value = mock_conn
mock_conn.cursor.return_value.__enter__.return_value = mock_cursor
mock_cursor.fetchone.return_value = [b"test_pbf_data"]

sql = build_db_query(valid_tile)
pbf_data = sql_to_pbf(sql)

assert pbf_data == b"test_pbf_data", "Should return binary PBF data"
mock_cursor.execute.assert_called_with(sql)
mock_cursor.fetchone.assert_called_once()


@pytest.mark.asyncio
async def test_read_tiles_from_postgres_invalid_tile(invalid_tile):
with pytest.raises(HTTPException) as excinfo:
await read_tiles_from_postgres(
invalid_tile["dataset"],
invalid_tile["zoom"],
invalid_tile["x"],
invalid_tile["y"],
invalid_tile["format"],
)
assert (
excinfo.value.status_code == 400
), "Should raise HTTP 400 for invalid tile parameters"


@pytest.mark.asyncio
@patch("application.routers.tiles.build_db_query", return_value=b"sample_pbf_data")
async def test_read_tiles_from_postgres_valid_tile(
mock_build_db_query, mock_sql_to_pbf, valid_tile
mock_build_db_query, valid_tile, mock_session_maker
):
mock_build_db_query.return_value = "SELECT * FROM tiles"
session = (
mock_session_maker.return_value.get_db.return_value.__aenter__.return_value
)
response = await read_tiles_from_postgres(
valid_tile["dataset"],
valid_tile["zoom"],
valid_tile["x"],
valid_tile["y"],
valid_tile["format"],
session,
)

assert isinstance(response, StreamingResponse), "Should return a StreamingResponse"
assert (
response.status_code == 200
), "Response status should be 200 for valid requests"
mock_build_db_query.assert_called_once_with(valid_tile)
mock_sql_to_pbf.assert_called_once_with("SELECT * FROM tiles")
mock_build_db_query.assert_called_once_with(valid_tile, session)


@pytest.mark.asyncio
async def test_read_tiles_from_postgres_invalid_tile(invalid_tile, mock_session_maker):
session = (
mock_session_maker.return_value.get_db.return_value.__aenter__.return_value
)
with pytest.raises(HTTPException) as excinfo:
await read_tiles_from_postgres(
invalid_tile["dataset"],
invalid_tile["zoom"],
invalid_tile["x"],
invalid_tile["y"],
invalid_tile["format"],
session,
)
assert (
excinfo.value.status_code == 400
), "Should raise HTTP 400 for invalid tile parameters"

0 comments on commit 266174d

Please sign in to comment.