Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ jobs:
run: |
uv sync

- name: Run pre-commit
- name: Run pre-commit and Mypy
if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
run: |
uv run pre-commit run --all-files
uv run --with mypy mypy -p tipg --ignore-missing-imports

- name: Run tests
run: uv run pytest --cov tipg --cov-report xml --cov-report term-missing --asyncio-mode=strict
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies = [
"fastapi>=0.100.0",
"starlette>=1.0",
"jinja2>=2.11.2,<4.0.0",
"morecantile>=5.0,<7.0",
"morecantile>=7.0,<8.0",
"pydantic>=2.4,<3.0",
"pydantic-settings~=2.0",
"geojson-pydantic>=1.0,<3.0",
Expand Down
33 changes: 24 additions & 9 deletions tests/test_factories.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
"""test endpoint factories."""

from fastapi import FastAPI
from fastapi.routing import APIRouter

from starlette.testclient import TestClient


def _count_routes(router: APIRouter) -> int:
"""Recursively count leaf routes.

Starting with FastAPI 0.141, `include_router` no longer copies the
included router's routes into `router.routes` — it appends a single
`_IncludedRouter` wrapper instead, so `len(router.routes)` undercounts.
"""
total = 0
for route in router.routes:
original_router = getattr(route, "original_router", None)
total += _count_routes(original_router) if original_router is not None else 1
return total


def test_features_factory():
"""test OGC Feature Factory."""

Expand All @@ -15,7 +30,7 @@ def test_features_factory():
endpoints = OGCFeaturesFactory()
assert endpoints.with_common
assert endpoints.title == "OGC API"
assert len(endpoints.router.routes) == 7
assert _count_routes(endpoints.router) == 7
assert len(endpoints.conforms_to) == 6

app = FastAPI()
Expand Down Expand Up @@ -56,7 +71,7 @@ def test_features_factory():
assert endpoints.router_prefix == "/features"
assert endpoints.with_common
assert endpoints.title == "OGC Features API"
assert len(endpoints.router.routes) == 7
assert _count_routes(endpoints.router) == 7

app = FastAPI()
app.include_router(endpoints.router, prefix="/features")
Expand Down Expand Up @@ -93,7 +108,7 @@ def test_features_factory():
endpoints = OGCFeaturesFactory(title="OGC Features API", with_common=False)
assert not endpoints.with_common
assert endpoints.title == "OGC Features API"
assert len(endpoints.router.routes) == 5
assert _count_routes(endpoints.router) == 5
assert len(endpoints.conforms_to) == 6

app = FastAPI()
Expand All @@ -116,7 +131,7 @@ def test_tiles_factory():
endpoints = OGCTilesFactory()
assert endpoints.with_common
assert endpoints.title == "OGC API"
assert len(endpoints.router.routes) == 10
assert _count_routes(endpoints.router) == 10
assert len(endpoints.conforms_to) == 5

app = FastAPI()
Expand Down Expand Up @@ -150,7 +165,7 @@ def test_tiles_factory():
assert endpoints.router_prefix == "/map"
assert endpoints.with_common
assert endpoints.title == "OGC Tiles API"
assert len(endpoints.router.routes) == 10
assert _count_routes(endpoints.router) == 10

app = FastAPI()
app.include_router(endpoints.router, prefix="/map")
Expand Down Expand Up @@ -180,7 +195,7 @@ def test_tiles_factory():
endpoints = OGCTilesFactory(title="OGC Tiles API", with_common=False)
assert not endpoints.with_common
assert endpoints.title == "OGC Tiles API"
assert len(endpoints.router.routes) == 8
assert _count_routes(endpoints.router) == 8
assert len(endpoints.conforms_to) == 5

app = FastAPI()
Expand All @@ -203,7 +218,7 @@ def test_endpoints_factory():
endpoints = Endpoints()
assert endpoints.with_common
assert endpoints.title == "OGC API"
assert len(endpoints.router.routes) == 15
assert _count_routes(endpoints.router) == 15
assert len(endpoints.conforms_to) == 11 # 5 from tiles + 6 from features

app = FastAPI()
Expand Down Expand Up @@ -244,7 +259,7 @@ def test_endpoints_factory():
assert endpoints.router_prefix == "/ogc"
assert endpoints.with_common
assert endpoints.title == "OGC Full API"
assert len(endpoints.router.routes) == 15
assert _count_routes(endpoints.router) == 15
assert not endpoints.ogc_features.with_common
assert endpoints.ogc_features.router_prefix == "/ogc"
assert not endpoints.ogc_tiles.with_common
Expand Down Expand Up @@ -288,7 +303,7 @@ def test_endpoints_factory():
endpoints = Endpoints(title="Tiles and Features API", with_common=False)
assert not endpoints.with_common
assert endpoints.title == "Tiles and Features API"
assert len(endpoints.router.routes) == 13 # 8 from tiles + 5 from features
assert _count_routes(endpoints.router) == 13 # 8 from tiles + 5 from features
assert len(endpoints.conforms_to) == 11 # 4 from tiles + 6 from features

app = FastAPI()
Expand Down
4 changes: 2 additions & 2 deletions tests/test_sql_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,15 @@ def test_tiles_functions(app_functions):
)
assert response.status_code == 200
decoded = mapbox_vector_tile.decode(response.content)
assert len(decoded["default"]["features"]) == 25
assert len(decoded["default"]["features"]) == 30

# Check default's function are used
response = app_functions.get(
"/collections/pg_temp.squares/tiles/WebMercatorQuad/3/3/3?size=2"
)
assert response.status_code == 200
decoded = mapbox_vector_tile.decode(response.content)
assert len(decoded["default"]["features"]) == 483
assert len(decoded["default"]["features"]) == 504

# Check any geometry input column will work
response = app_functions.get(
Expand Down
4 changes: 2 additions & 2 deletions tipg/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from fastapi import FastAPI

mvt_settings = MVTSettings()
features_settings = FeaturesSettings()
features_settings = FeaturesSettings() # type: ignore [call-arg]

TransformerFromCRS = lru_cache(Transformer.from_crs)

Expand Down Expand Up @@ -731,7 +731,7 @@ def _select_mvt(
# (WorldCRS84Quad → OGC:CRS84), 4326 is a safe fallback since the
# coordinate values are identical (only axis order differs, and
# PostGIS treats 4326 as lon/lat).
if not tms.is_valid(tile):
if not tms.is_valid(tile, strict=False):
west, south, east, north = tms.bbox
geo_srid = tms.geographic_crs.to_epsg() or 4326
geom = (
Expand Down
2 changes: 1 addition & 1 deletion tipg/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ def function_parameters_query( # noqa: C901
"tileMatrixSetId", tms_settings.default_tms
)
tms = default_tms.get(tms_id)
left, bottom, right, top = tms.bounds(x, y, z)
left, bottom, right, top = tms.bounds(Tile(x, y, z))

function_parameters[col_param.name] = (
"srid=4326;"
Expand Down
6 changes: 3 additions & 3 deletions tipg/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@

tms_settings = TMSSettings()
mvt_settings = MVTSettings()
features_settings = FeaturesSettings()
features_settings = FeaturesSettings() # type: ignore [call-arg]


jinja2_env = jinja2.Environment(
Expand Down Expand Up @@ -1679,7 +1679,7 @@ async def collection_get_tile(
tms = self.supported_tms.get(tileMatrixSetId)

async with request.app.state.pool.acquire() as conn:
tile = await collection.get_tile(
t = await collection.get_tile(
conn,
tms=tms,
tile=tile,
Expand All @@ -1697,7 +1697,7 @@ async def collection_get_tile(
simplify=simplify,
)

return Response(tile, media_type=MediaType.mvt.value)
return Response(t, media_type=MediaType.mvt.value)

def _tilejson_routes(self):
############################################################################
Expand Down
4 changes: 2 additions & 2 deletions tipg/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ async def lifespan(app: FastAPI):

if settings.catalog_ttl:
app.add_middleware(
CatalogUpdateMiddleware,
func=register_collection_catalog,
CatalogUpdateMiddleware, # type: ignore[arg-type]
func=register_collection_catalog, # type: ignore[arg-type]
ttl=settings.catalog_ttl,
db_settings=db_settings,
)
Expand Down
36 changes: 16 additions & 20 deletions tipg/middleware.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
"""tipg middlewares."""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any, Optional, Protocol, Set
from typing import Any, Optional, Protocol

from tipg.collections import Catalog
from tipg.errors import MissingCollectionCatalog
Expand All @@ -14,28 +17,21 @@
from starlette.types import ASGIApp, Message, Receive, Scope, Send


@dataclass(frozen=True)
class CacheControlMiddleware:
"""MiddleWare to add CacheControl in response headers."""
"""MiddleWare to add CacheControl in response headers.

def __init__(
self,
app: ASGIApp,
cachecontrol: Optional[str] = None,
cachecontrol_max_http_code: Optional[int] = 500,
exclude_path: Optional[Set[str]] = None,
) -> None:
"""Init Middleware.
Args:
app (ASGIApp): starlette/FastAPI application.
cachecontrol (str): Cache-Control string to add to the response.
exclude_path (set): Set of regex expression to use to filter the path.

Args:
app (ASGIApp): starlette/FastAPI application.
cachecontrol (str): Cache-Control string to add to the response.
exclude_path (set): Set of regex expression to use to filter the path.
"""

"""
self.app = app
self.cachecontrol = cachecontrol
self.cachecontrol_max_http_code = cachecontrol_max_http_code
self.exclude_path = exclude_path or set()
app: ASGIApp
cachecontrol: str | None = None
cachecontrol_max_http_code: int = 500
exclude_path: set[str] = field(default_factory=set)

async def __call__(self, scope: Scope, receive: Receive, send: Send):
"""Handle call."""
Expand Down Expand Up @@ -65,7 +61,7 @@ async def send_wrapper(message: Message):
class CatalogUpdateFunc(Protocol):
"""Catalog update function protocol."""

def __call__(self, app: ASGIApp, **kwargs: Any) -> None:
async def __call__(self, app: ASGIApp, **kwargs: Any) -> None:
"""define input/output for the function."""
...

Expand Down
5 changes: 4 additions & 1 deletion tipg/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ def _update_openapi(app: FastAPI) -> FastAPI:
SOFTWARE.
"""
# Find the route for the openapi_url in the app
# TODO: Type info is Route, while it shoukd maybe be APIRoute? Check FastAPI source.
openapi_route: Route = next(
route for route in app.router.routes if route.path == app.openapi_url
route
for route in app.router.routes
if route.path == app.openapi_url # type: ignore
)
# Store the old endpoint function so we can call it from the patched function
old_endpoint = openapi_route.endpoint
Expand Down
Loading
Loading