Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update stac-fastapi to next major version #108

Merged
merged 11 commits into from
May 7, 2024
Merged
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ repos:
additional_dependencies:
- types-requests
- types-attrs
- pydantic~=1.10
- pydantic~=2.0
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

### Changed

- update pgstac version to `0.8.x`
- Update stac-fastapi libraries to v3.0.0a0 ([#108](https://github.com/stac-utils/stac-fastapi-pgstac/pull/108))
- Update pgstac version to `0.8.x`

## [2.5.0] - 2024-04-25

Expand Down
10 changes: 5 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
install_requires = [
"attrs",
"orjson",
"pydantic[dotenv]>=1.10.8", # https://github.com/pydantic/pydantic/issues/5821
"stac_pydantic==2.0.*",
"stac-fastapi.types~=2.5.5.post1",
"stac-fastapi.api~=2.5.5.post1",
"stac-fastapi.extensions~=2.5.5.post1",
"pydantic",
"stac_pydantic==3.0.*",
"stac-fastapi.api~=3.0.0a0",
"stac-fastapi.extensions~=3.0.0a0",
"stac-fastapi.types~=3.0.0a0",
"asyncpg",
"buildpg",
"brotli_asgi",
Expand Down
2 changes: 0 additions & 2 deletions stac_fastapi/pgstac/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from stac_fastapi.api.app import StacApi
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
from stac_fastapi.extensions.core import (
ContextExtension,
FieldsExtension,
FilterExtension,
SortExtension,
Expand Down Expand Up @@ -39,7 +38,6 @@
"sort": SortExtension(),
"fields": FieldsExtension(),
"pagination": TokenPaginationExtension(),
"context": ContextExtension(),
"filter": FilterExtension(client=FiltersClient()),
"bulk_transactions": BulkTransactionExtension(client=BulkTransactionsClient()),
}
Expand Down
16 changes: 9 additions & 7 deletions stac_fastapi/pgstac/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import List, Type
from urllib.parse import quote

from pydantic import BaseModel, Extra
from pydantic import BaseModel
from pydantic_settings import SettingsConfigDict
from stac_fastapi.types.config import ApiSettings

from stac_fastapi.pgstac.types.base_item_cache import (
Expand Down Expand Up @@ -33,12 +34,14 @@
]


class ServerSettings(BaseModel, extra=Extra.allow):
class ServerSettings(BaseModel):
"""Server runtime parameters."""

search_path: str = "pgstac,public"
application_name: str = "pgstac"

model_config = SettingsConfigDict(extra="allow")


class Settings(ApiSettings):
"""Postgres-specific API settings.
Expand All @@ -58,7 +61,7 @@ class Settings(ApiSettings):
postgres_pass: str
postgres_host_reader: str
postgres_host_writer: str
postgres_port: str
postgres_port: int
postgres_dbname: str

db_min_conn_size: int = 10
Expand Down Expand Up @@ -89,7 +92,6 @@ def testing_connection_string(self):
"""Create testing psql connection string."""
return f"postgresql://{self.postgres_user}:{quote(self.postgres_pass)}@{self.postgres_host_writer}:{self.postgres_port}/pgstactestdb"

class Config(ApiSettings.Config):
"""Model config."""

env_nested_delimiter = "__"
model_config = SettingsConfigDict(
**{**ApiSettings.model_config, **{"env_nested_delimiter": "__"}}
)
9 changes: 7 additions & 2 deletions stac_fastapi/pgstac/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ async def _search_base( # noqa: C901
search_request.conf = search_request.conf or {}
search_request.conf["nohydrate"] = settings.use_api_hydrate

search_request_json = search_request.json(exclude_none=True, by_alias=True)
search_request_json = search_request.model_dump_json(
exclude_none=True, by_alias=True
)

try:
async with request.app.state.get_connection(request, "r") as conn:
Expand Down Expand Up @@ -277,6 +279,9 @@ async def item_collection(
# If collection does not exist, NotFoundError wil be raised
await self.get_collection(collection_id, request=request)

if datetime:
datetime = format_datetime_range(datetime)

base_args = {
"collections": [collection_id],
"bbox": bbox,
Expand Down Expand Up @@ -393,7 +398,7 @@ async def get_search( # noqa: C901
base_args["filter-lang"] = "cql2-json"

if datetime:
base_args["datetime"] = datetime
base_args["datetime"] = format_datetime_range(datetime)

if intersects:
base_args["intersects"] = orjson.loads(unquote_plus(intersects))
Expand Down
2 changes: 1 addition & 1 deletion stac_fastapi/pgstac/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,6 @@ async def create_pool(self, connection_string: str, settings):
max_queries=settings.db_max_queries,
max_inactive_connection_lifetime=settings.db_max_inactive_conn_lifetime,
init=con_init,
server_settings=settings.server_settings.dict(),
server_settings=settings.server_settings.model_dump(),
)
return pool
13 changes: 7 additions & 6 deletions stac_fastapi/pgstac/extensions/filter.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""Get Queryables."""

from typing import Any, Optional
from typing import Any, Dict, Optional

from buildpg import render
from fastapi import Request
from fastapi.responses import JSONResponse
from stac_fastapi.types.core import AsyncBaseFiltersClient
from stac_fastapi.types.errors import NotFoundError

Expand All @@ -13,8 +12,11 @@ class FiltersClient(AsyncBaseFiltersClient):
"""Defines a pattern for implementing the STAC filter extension."""

async def get_queryables(
self, request: Request, collection_id: Optional[str] = None, **kwargs: Any
) -> JSONResponse:
self,
request: Request,
collection_id: Optional[str] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""Get the queryables available for the given collection_id.

If collection_id is None, returns the intersection of all
Expand All @@ -37,5 +39,4 @@ async def get_queryables(
raise NotFoundError(f"Collection {collection_id} not found")

queryables["$id"] = str(request.url)
headers = {"Content-Type": "application/schema+json"}
return JSONResponse(queryables, headers=headers)
return queryables
vincentsarago marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion stac_fastapi/pgstac/extensions/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def operator(self) -> Callable[[Any, Any], bool]:
class QueryExtensionPostRequest(BaseModel):
"""Query Extension POST request model."""

query: Optional[Dict[str, Dict[Operator, Any]]]
query: Optional[Dict[str, Dict[Operator, Any]]] = None


class QueryExtension(QueryExtensionBase):
Expand Down
49 changes: 38 additions & 11 deletions stac_fastapi/pgstac/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from stac_fastapi.types import stac as stac_types
from stac_fastapi.types.core import AsyncBaseTransactionsClient
from stac_pydantic import Collection, Item, ItemCollection
from starlette.responses import JSONResponse, Response

from stac_fastapi.pgstac.config import Settings
Expand Down Expand Up @@ -69,11 +70,13 @@ def _validate_item(
async def create_item(
self,
collection_id: str,
item: Union[stac_types.Item, stac_types.ItemCollection],
item: Union[Item, ItemCollection],
request: Request,
**kwargs,
) -> Optional[Union[stac_types.Item, Response]]:
"""Create item."""
item = item.model_dump(mode="json")

if item["type"] == "FeatureCollection":
valid_items = []
for item in item["features"]: # noqa: B020
Expand All @@ -100,6 +103,7 @@ async def create_item(
).get_links(extra_links=item.get("links"))

return stac_types.Item(**item)

else:
raise HTTPException(
status_code=400,
Expand All @@ -111,10 +115,12 @@ async def update_item(
request: Request,
collection_id: str,
item_id: str,
item: stac_types.Item,
item: Item,
**kwargs,
) -> Optional[Union[stac_types.Item, Response]]:
"""Update item."""
item = item.model_dump(mode="json")

self._validate_item(request, item, collection_id, item_id)
item["collection"] = collection_id

Expand All @@ -130,31 +136,50 @@ async def update_item(
return stac_types.Item(**item)

async def create_collection(
self, collection: stac_types.Collection, request: Request, **kwargs
self,
collection: Collection,
request: Request,
**kwargs,
) -> Optional[Union[stac_types.Collection, Response]]:
"""Create collection."""
collection = collection.model_dump(mode="json")

self._validate_collection(request, collection)

async with request.app.state.get_connection(request, "w") as conn:
await dbfunc(conn, "create_collection", collection)

collection["links"] = await CollectionLinks(
collection_id=collection["id"], request=request
).get_links(extra_links=collection.get("links"))
).get_links(extra_links=collection["links"])

return stac_types.Collection(**collection)

async def update_collection(
self, collection: stac_types.Collection, request: Request, **kwargs
self,
collection: Collection,
request: Request,
**kwargs,
) -> Optional[Union[stac_types.Collection, Response]]:
"""Update collection."""

col = collection.model_dump(mode="json")

async with request.app.state.get_connection(request, "w") as conn:
await dbfunc(conn, "update_collection", collection)
collection["links"] = await CollectionLinks(
collection_id=collection["id"], request=request
).get_links(extra_links=collection.get("links"))
return stac_types.Collection(**collection)
await dbfunc(conn, "update_collection", col)

col["links"] = await CollectionLinks(
collection_id=col["id"], request=request
).get_links(extra_links=col.get("links"))

return stac_types.Collection(**col)

async def delete_item(
self, item_id: str, collection_id: str, request: Request, **kwargs
self,
item_id: str,
collection_id: str,
request: Request,
**kwargs,
) -> Optional[Union[stac_types.Item, Response]]:
"""Delete item."""
q, p = render(
Expand All @@ -164,6 +189,7 @@ async def delete_item(
)
async with request.app.state.get_connection(request, "w") as conn:
await conn.fetchval(q, *p)

return JSONResponse({"deleted item": item_id})

async def delete_collection(
Expand All @@ -172,6 +198,7 @@ async def delete_collection(
"""Delete collection."""
async with request.app.state.get_connection(request, "w") as conn:
await dbfunc(conn, "delete_collection", collection_id)

return JSONResponse({"deleted collection": collection_id})


Expand Down
9 changes: 5 additions & 4 deletions stac_fastapi/pgstac/types/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Dict, Optional

from pydantic import validator
from pydantic import ValidationInfo, field_validator
from stac_fastapi.types.search import BaseSearchPostRequest


Expand All @@ -14,10 +14,11 @@ class PgstacSearch(BaseSearchPostRequest):

conf: Optional[Dict] = None

@validator("filter_lang", pre=False, check_fields=False, always=True)
def validate_query_uses_cql(cls, v, values):
@field_validator("filter_lang", check_fields=False)
@classmethod
def validate_query_uses_cql(cls, v: str, info: ValidationInfo):
"""Use of Query Extension is not allowed with cql2."""
if values.get("query", None) is not None and v != "cql-json":
if info.data.get("query", None) is not None and v != "cql-json":
raise ValueError(
"Query extension is not available when using pgstac with cql2"
)
Expand Down
4 changes: 2 additions & 2 deletions stac_fastapi/pgstac/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def dict_deep_update(merge_to: Dict[str, Any], merge_from: Dict[str, Any]) -> No
merge_to[k] = v


def format_datetime_range(dt_range: DateTimeType) -> Union[str, Any]:
def format_datetime_range(dt_range: Union[DateTimeType, str]) -> str:
"""
Convert a datetime object or a tuple of datetime objects to a formatted string for datetime ranges.

Expand All @@ -132,7 +132,7 @@ def format_datetime_range(dt_range: DateTimeType) -> Union[str, Any]:
return dt_range.isoformat().replace("+00:00", "Z")

# Handle a tuple containing datetime objects or None
if isinstance(dt_range, tuple):
elif isinstance(dt_range, tuple):
start, end = dt_range

# Convert start datetime to string if not None, otherwise use ".."
Expand Down
Loading
Loading