Skip to content

Commit

Permalink
Merge pull request #161 from developmentseed/patch/parse-sub-model-se…
Browse files Browse the repository at this point in the history
…ttings

parse sub-model for table_settings
  • Loading branch information
vincentsarago authored Jan 9, 2024
2 parents 37fd514 + 04bca7e commit fa17fb8
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 48 deletions.
4 changes: 3 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).

Note: Minor version `0.X.0` update might break the API, It's recommended to pin `tipg` to minor version: `tipg>=0.1,<0.2`

## [unreleased]
## [0.6.0] - 2024-01-09

- update FastAPI version lower limit to `>0.107.0` and adapt for new starlette version
- fix invalid streaming response formatting
- refactor internal table properties handling
- fix sub-model Table settings (https://github.com/developmentseed/tipg/issues/154)

## [0.5.7] - 2024-01-08

Expand Down
22 changes: 22 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,3 +558,25 @@ def app_functions(database_url, monkeypatch):

with TestClient(app) as client:
yield client


@pytest.fixture(autouse=True)
def app_public_table(database_url, monkeypatch):
"""Create app with connection to the pytest database."""
monkeypatch.setenv("TIPG_TABLE_CONFIG__public_landsat_wrs__properties", '["pr"]')

postgres_settings = PostgresSettings(database_url=database_url)
db_settings = DatabaseSettings(
schemas=["public"],
functions=[],
)
sql_settings = CustomSQLSettings(custom_sql_directory=None)

app = create_tipg_app(
postgres_settings=postgres_settings,
db_settings=db_settings,
sql_settings=sql_settings,
)

with TestClient(app) as client:
yield client
13 changes: 13 additions & 0 deletions tests/routes/test_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,16 @@ def test_item(app):
# not found
response = app.get("/collections/public.landsat_wrs/items/50000")
assert response.status_code == 404


def test_item_with_property_config(app_public_table):
"""Test /items/{item id} endpoint."""
response = app_public_table.get("/collections/public.landsat_wrs/items/1")
assert response.status_code == 200
assert response.headers["content-type"] == "application/geo+json"
body = response.json()
assert body["type"] == "Feature"
assert body["id"] == 1
assert body["links"]
assert list(body["properties"]) == ["pr"]
Item.model_validate(body)
69 changes: 29 additions & 40 deletions tipg/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from tipg.filter.filters import bbox_to_wkt
from tipg.logger import logger
from tipg.model import Extent
from tipg.settings import FeaturesSettings, MVTSettings, TableSettings
from tipg.settings import FeaturesSettings, MVTSettings, TableConfig, TableSettings

from fastapi import FastAPI

Expand Down Expand Up @@ -163,8 +163,9 @@ class Collection(BaseModel):
dbschema: str = Field(..., alias="schema")
title: Optional[str] = None
description: Optional[str] = None
table_columns: List[Column] = []
properties: List[Column] = []
id_column: Optional[str] = None
id_column: Optional[Column] = None
geometry_column: Optional[Column] = None
datetime_column: Optional[Column] = None
parameters: List[Parameter] = []
Expand Down Expand Up @@ -237,12 +238,12 @@ def crs(self):
@property
def geometry_columns(self) -> List[Column]:
"""Return geometry columns."""
return [c for c in self.properties if c.is_geometry]
return [c for c in self.table_columns if c.is_geometry]

@property
def datetime_columns(self) -> List[Column]:
"""Return datetime columns."""
return [c for c in self.properties if c.is_datetime]
return [c for c in self.table_columns if c.is_datetime]

def get_geometry_column(self, name: Optional[str] = None) -> Optional[Column]:
"""Return the name of the first geometry column."""
Expand Down Expand Up @@ -272,13 +273,6 @@ def get_datetime_column(self, name: Optional[str] = None) -> Optional[Column]:

return None

@property
def id_column_info(self) -> Column: # type: ignore
"""Return Column for a unique identifier."""
for col in self.properties:
if col.name == self.id_column:
return col

def columns(self, properties: Optional[List[str]] = None) -> List[str]:
"""Return table columns optionally filtered to only include columns from properties."""
if properties in [[], [""]]:
Expand Down Expand Up @@ -311,7 +305,7 @@ def _select_no_geo(self, properties: Optional[List[str]], addid: bool = True):

if addid:
if self.id_column:
id_clause = logic.V(self.id_column).as_("tipg_id")
id_clause = logic.V(self.id_column.name).as_("tipg_id")
else:
id_clause = raw(" ROW_NUMBER () OVER () AS tipg_id ")
if nocomma:
Expand Down Expand Up @@ -480,18 +474,14 @@ def _where( # noqa: C901
if ids is not None:
if len(ids) == 1:
wheres.append(
logic.V(self.id_column)
== pg_funcs.cast(
pg_funcs.cast(ids[0], "text"), self.id_column_info.type
)
logic.V(self.id_column.name)
== pg_funcs.cast(pg_funcs.cast(ids[0], "text"), self.id_column.type)
)
else:
w = [
logic.V(self.id_column)
logic.V(self.id_column.name)
== logic.S(
pg_funcs.cast(
pg_funcs.cast(i, "text"), self.id_column_info.type
)
pg_funcs.cast(pg_funcs.cast(i, "text"), self.id_column.type)
)
for i in ids
]
Expand Down Expand Up @@ -626,7 +616,7 @@ def _sortby(self, sortby: Optional[str]):

else:
if self.id_column is not None:
sorts.append(logic.V(self.id_column))
sorts.append(logic.V(self.id_column.name))
else:
sorts.append(logic.V(self.properties[0].name))

Expand Down Expand Up @@ -958,38 +948,36 @@ async def get_collection_index( # noqa: C901
if table_id == "pg_temp.tipg_catalog":
continue

table_conf = table_confs.get(confid, {})
table_conf = table_confs.get(confid, TableConfig())

# Make sure that any properties set in conf exist in table
properties = sorted(table.get("properties", []), key=lambda d: d["name"])
properties_setting = table_conf.get("properties", [])
if properties_setting:
properties = [p for p in properties if p["name"] in properties_setting]
columns = sorted(table.get("properties", []), key=lambda d: d["name"])
properties_setting = table_conf.properties or [c["name"] for c in columns]

# ID Column
id_column = table_conf.get("pk") or table.get("pk")
if not id_column and fallback_key_names:
for p in properties:
id_column = None
if id_name := table_conf.pk or table.get("pk"):
for p in columns:
if id_name == p["name"]:
id_column = p
break

if id_column is None and fallback_key_names:
for p in columns:
if p["name"] in fallback_key_names:
id_column = p["name"]
id_column = p
break

datetime_column = None
geometry_column = None

for c in properties:
for c in columns:
if c.get("type") in ("timestamp", "timestamptz", "date"):
if (
table_conf.get("datetimecol") == c["name"]
or datetime_column is None
):
if table_conf.datetimecol == c["name"] or datetime_column is None:
datetime_column = c

if c.get("type") in ("geometry", "geography"):
if (
table_conf.get("geomcol") == c["name"]
or geometry_column is None
):
if table_conf.geomcol == c["name"] or geometry_column is None:
geometry_column = c

catalog[table_id] = Collection(
Expand All @@ -998,8 +986,9 @@ async def get_collection_index( # noqa: C901
table=table["name"],
schema=table["schema"],
description=table.get("description", None),
table_columns=columns,
properties=[p for p in columns if p["name"] in properties_setting],
id_column=id_column,
properties=properties,
datetime_column=datetime_column,
geometry_column=geometry_column,
parameters=table.get("parameters") or [],
Expand Down
25 changes: 18 additions & 7 deletions tipg/settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""tipg config."""

import json
import pathlib
from typing import Dict, List, Optional
from typing import Any, Dict, List, Optional

from pydantic import (
BaseModel,
DirectoryPath,
Field,
PostgresDsn,
Expand All @@ -12,7 +14,6 @@
model_validator,
)
from pydantic_settings import BaseSettings
from typing_extensions import TypedDict


class APISettings(BaseSettings):
Expand All @@ -36,13 +37,23 @@ def parse_cors_origin(cls, v):
return [origin.strip() for origin in v.split(",")]


class TableConfig(TypedDict, total=False):
class TableConfig(BaseModel):
"""Configuration to add table options with env variables."""

geomcol: Optional[str]
datetimecol: Optional[str]
pk: Optional[str]
properties: Optional[List[str]]
geomcol: Optional[str] = None
datetimecol: Optional[str] = None
pk: Optional[str] = None
properties: Optional[List[str]] = None

model_config = {"extra": "ignore"}

@field_validator("properties", mode="before")
def _properties(cls, v: Any) -> Any:
"""set geometry from geo interface or input"""
if isinstance(v, str):
return json.loads(v)
else:
return v


class TableSettings(BaseSettings):
Expand Down

0 comments on commit fa17fb8

Please sign in to comment.