Skip to content

Commit

Permalink
update for latest commit in tipg
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentsarago committed Oct 30, 2023
1 parent 0b00a9c commit 5c162e1
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 127 deletions.
34 changes: 21 additions & 13 deletions tipgstac/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import datetime
import json
import re
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, TypedDict
from urllib.parse import unquote_plus

from buildpg import asyncpg, render
Expand All @@ -16,7 +16,7 @@
from pygeofilter.ast import AstType
from pygeofilter.backends.cql2_json import to_cql2

from tipg.collections import Catalog, Collection, Column, FeatureCollection, Parameter
from tipg.collections import Collection, Column, ItemList, Parameter
from tipg.errors import InvalidDatetime, InvalidLimit
from tipg.model import Extent
from tipg.settings import FeaturesSettings
Expand Down Expand Up @@ -91,7 +91,8 @@ async def features( # noqa: C901
bbox_only: Optional[bool] = None, # Not Available
simplify: Optional[float] = None, # Not Available
geom_as_wkt: bool = False, # Not Available
) -> Tuple[FeatureCollection, Optional[int], Optional[str], Optional[str],]:
**kwargs: Any, # Not Used
) -> ItemList:
"""Build and run PgSTAC query."""
if limit and limit > features_settings.max_features_per_query:
raise InvalidLimit(
Expand Down Expand Up @@ -180,20 +181,18 @@ async def features( # noqa: C901
) from e
fc = {}

count = None
matched = None
if context := fc.get("context"):
count = context.get("matched")
matched = context.get("matched")

next_token = fc.get("next")
prev_token = fc.get("prev")

return (
FeatureCollection(
type="FeatureCollection", features=fc.get("features", [])
),
count,
next_token,
prev_token,
return ItemList( # type: ignore
items=fc.get("features", []),
matched=matched,
next=next_token,
prev=prev_token,
)

async def get_tile(
Expand All @@ -206,8 +205,17 @@ async def get_tile(
raise NotImplementedError


class PgSTACCatalog(Catalog):
class PgSTACCatalog(TypedDict):
"""Collection Catalog."""

collections: Dict[str, PgSTACCollection]
last_updated: datetime.datetime


class CollectionList(TypedDict):
"""Collections."""

collections: List[PgSTACCollection]
matched: Optional[int]
next: Optional[int]
prev: Optional[int]
35 changes: 17 additions & 18 deletions tipgstac/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
"""tipgstac dependencies."""

import datetime
from typing import Dict, Optional
from typing import List, Optional

from aiocache import cached
from buildpg import render
from fastapi import HTTPException, Path, Query
from starlette.requests import Request
from typing_extensions import Annotated

from tipgstac.collections import PgSTACCatalog, PgSTACCollection
from tipgstac.collections import CollectionList, PgSTACCollection
from tipgstac.settings import CacheSettings

cache_config = CacheSettings()
Expand Down Expand Up @@ -39,12 +38,12 @@ async def CatalogParams(
description="Starts the response at an offset.",
),
] = None,
) -> PgSTACCatalog:
) -> CollectionList:
"""Return Collections Catalog."""
limit = limit or 10 # add collection limit settings
offset = offset or 0

collections: Dict[str, PgSTACCollection] = {}
collections: List[PgSTACCollection] = []

async with request.app.state.pool.acquire() as conn:
matched = await conn.fetchval("SELECT count(*) FROM pgstac.collections;")
Expand All @@ -59,24 +58,24 @@ async def CatalogParams(
if not collection:
continue

collection_id = collection["id"]
collections[collection_id] = PgSTACCollection(
type="Collection",
id=collection_id,
table="collections",
schema="pgstac",
extent=collection.get("extent"),
description=collection.get("description", None),
id_column="id",
stac_version=collection.get("stac_version"),
stac_extensions=collection.get("stac_extensions", []),
collections.append(
PgSTACCollection(
type="Collection",
id=collection["id"],
table="collections",
schema="pgstac",
extent=collection.get("extent"),
description=collection.get("description", None),
id_column="id",
stac_version=collection.get("stac_version"),
stac_extensions=collection.get("stac_extensions", []),
),
)

returned = len(collections)

return PgSTACCatalog(
return CollectionList(
collections=collections,
last_updated=datetime.datetime.now(),
matched=matched,
next=offset + returned if matched - returned > offset else None,
prev=max(offset - returned, 0) if offset else None,
Expand Down
134 changes: 45 additions & 89 deletions tipgstac/factory.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,10 @@
"""Custom Factory.
PgSTAC uses `token: str` instead of `offset: int` which means we have to overwrite the /items endpoint.
PgSTAC returns `prev/next` token automatically so we forward them through the `PgSTACCollection.features` method, meaning we also had to update the `/item` endpoint
```python
# tipg
items, matched_items = await collection.features(
pool=request.app.state.pool,
bbox_only=bbox_only,
simplify=simplify,
ids_filter=[itemId],
properties=properties,
geom_as_wkt=geom_as_wkt,
)
# tipgstac
items, matched_items, next_token, prev_token = await collection.features(
pool=request.app.state.pool,
bbox_only=bbox_only,
simplify=simplify,
ids_filter=[itemId],
properties=properties,
geom_as_wkt=geom_as_wkt,
)
```
"""

from dataclasses import dataclass
from typing import Dict, List, Optional
from typing import Callable, Dict, List, Optional

import orjson
from fastapi import Depends, Path, Query
Expand All @@ -41,7 +16,6 @@
from typing_extensions import Annotated

from tipg import factory, model
from tipg.collections import Collection
from tipg.dependencies import (
ItemsOutputType,
bbox_query,
Expand All @@ -55,6 +29,8 @@
from tipg.resources.enums import MediaType
from tipg.resources.response import GeoJSONResponse
from tipg.settings import FeaturesSettings
from tipgstac.collections import CollectionList, PgSTACCollection
from tipgstac.dependencies import CatalogParams, CollectionParams

features_settings = FeaturesSettings()

Expand All @@ -63,6 +39,9 @@
class OGCFeaturesFactory(factory.OGCFeaturesFactory):
"""Override /items and /item endpoints."""

collection_dependency: Callable[..., PgSTACCollection] = CollectionParams
catalog_dependency: Callable[..., CollectionList] = CatalogParams

def _items_route(self): # noqa: C901
@self.router.get(
"/collections/{collectionId}/items",
Expand All @@ -83,7 +62,9 @@ def _items_route(self): # noqa: C901
)
async def items( # noqa: C901
request: Request,
collection: Annotated[Collection, Depends(self.collection_dependency)],
collection: Annotated[
PgSTACCollection, Depends(self.collection_dependency)
],
ids_filter: Annotated[Optional[List[str]], Depends(ids_query)],
bbox_filter: Annotated[Optional[List[float]], Depends(bbox_query)],
datetime_filter: Annotated[Optional[List[str]], Depends(datetime_query)],
Expand Down Expand Up @@ -134,7 +115,7 @@ async def items( # noqa: C901
MediaType.html,
]

items, matched_items, next_token, prev_token = await collection.features(
item_list = await collection.features(
request.app.state.pool,
ids_filter=ids_filter,
bbox_filter=bbox_filter,
Expand All @@ -155,29 +136,19 @@ async def items( # noqa: C901
MediaType.json,
MediaType.ndjson,
):
if (
items["features"]
and items["features"][0].get("geometry") is not None
):
rows = (
{
"collectionId": collection.id,
"itemId": f.get("id"),
**f.get("properties", {}),
"geometry": f["geometry"],
}
for f in items["features"]
)

else:
rows = (
{
rows = (
{
k: v
for k, v in {
"collectionId": collection.id,
"itemId": f.get("id"),
**f.get("properties", {}),
}
for f in items["features"]
)
"geometry": f.get("geometry", None),
}.items()
if v is not None
}
for f in item_list["items"]
)

# CSV Response
if output_type == MediaType.csv:
Expand Down Expand Up @@ -222,9 +193,7 @@ async def items( # noqa: C901
},
]

items_returned = len(items["features"])

if next_token:
if next_token := item_list["next"]:
query_params = QueryParams(
{**request.query_params, "offset": next_token}
)
Expand All @@ -241,14 +210,14 @@ async def items( # noqa: C901
},
)

if prev_token:
query_params = QueryParams(
{**request.query_params, "offset": prev_token},
)
url = (
self.url_for(request, "items", collectionId=collection.id)
+ f"?{query_params}"
)
if item_list["prev"] is not None:
prev_token = item_list["prev"]
qp = dict(request.query_params)
qp.pop("offset")
query_params = QueryParams({**qp, "offset": prev_token})
url = self.url_for(request, "items", collectionId=collection.id)
if qp:
url += f"?{query_params}"

links.append(
{
Expand All @@ -266,8 +235,8 @@ async def items( # noqa: C901
"description": collection.description
or collection.title
or collection.id,
"numberMatched": matched_items,
"numberReturned": items_returned,
"numberMatched": item_list["matched"],
"numberReturned": len(item_list["items"]),
"links": links,
"features": [
{
Expand Down Expand Up @@ -296,7 +265,7 @@ async def items( # noqa: C901
},
],
}
for feature in items["features"]
for feature in item_list["items"]
],
}

Expand Down Expand Up @@ -339,7 +308,9 @@ def _item_route(self):
)
async def item(
request: Request,
collection: Annotated[Collection, Depends(self.collection_dependency)],
collection: Annotated[
PgSTACCollection, Depends(self.collection_dependency)
],
itemId: Annotated[str, Path(description="Item identifier")],
bbox_only: Annotated[
Optional[bool],
Expand Down Expand Up @@ -369,7 +340,7 @@ async def item(
MediaType.html,
]

items, _, _, _ = await collection.features(
item_list = await collection.features(
pool=request.app.state.pool,
bbox_only=bbox_only,
simplify=simplify,
Expand All @@ -378,41 +349,26 @@ async def item(
geom_as_wkt=geom_as_wkt,
)

features = items.get("features", [])
if not features:
if not item_list["items"]:
raise NotFound(
f"Item {itemId} in Collection {collection.id} does not exist."
)

feature = features[0]
feature = item_list["items"][0]

if output_type in (
MediaType.csv,
MediaType.json,
MediaType.ndjson,
):
row = {
"collectionId": collection.id,
"itemId": feature.get("id"),
**feature.get("properties", {}),
}
if feature.get("geometry") is not None:
rows = iter(
[
{
"collectionId": collection.id,
"itemId": feature.get("id"),
**feature.get("properties", {}),
"geometry": feature["geometry"],
},
]
)

else:
rows = iter(
[
{
"collectionId": collection.id,
"itemId": feature.get("id"),
**feature.get("properties", {}),
},
]
)
row["geometry"] = (feature["geometry"],)
rows = iter([row])

# CSV Response
if output_type == MediaType.csv:
Expand Down
Loading

0 comments on commit 5c162e1

Please sign in to comment.