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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.mypy_cache

env2/
PIP_EXTRA_INDEX_URL
!tests/resources/*.jpg
**.pyc
Expand Down Expand Up @@ -131,4 +131,4 @@ docs/api/*
venv

# IDE
.vscode
.vscode
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Updated

* Api functionality updated to work with stacapi v6.0.0 release


## [Unreleased]

As a part of this release, this repository was extracted from the main
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.8-slim as base
FROM python:3.13-slim as base

# Any python libraries that require system libraries to be installed will likely
# need the following packages in order to build
Expand All @@ -16,4 +16,5 @@ WORKDIR /app

COPY . /app

RUN pip install pip --upgrade
RUN pip install -e .[dev,server]
4 changes: 2 additions & 2 deletions Dockerfile.docs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM python:3.8-slim
FROM python:3.11-slim

# build-essential is required to build a wheel for ciso8601
RUN apt update && apt install -y build-essential

RUN python -m pip install --upgrade pip
RUN python -m pip install mkdocs mkdocs-material pdocs
RUN python -m pip install "numpy<2" mkdocs mkdocs-material pdocs pystac

COPY . /opt/src

Expand Down
2 changes: 0 additions & 2 deletions docker-compose.docs.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3'

services:
docs:
container_name: stac-fastapi-docs-dev
Expand Down
1 change: 0 additions & 1 deletion docker-compose.nginx.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3'
services:
nginx:
image: nginx
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3'
services:
app:
container_name: stac-fastapi-sqlalchemy
Expand All @@ -22,6 +21,8 @@ services:
volumes:
- ./stac_fastapi:/app/stac_fastapi
- ./scripts:/app/scripts
- ./tests:/app/tests
- ./test_data:/app/test_data
depends_on:
- database
command: bash -c "./scripts/wait-for-it.sh database:5432 && python -m stac_fastapi.sqlalchemy.app"
Expand Down
17 changes: 9 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@

install_requires = [
"attrs",
"pydantic[dotenv]",
"stac_pydantic>=2.0.3",
"stac-fastapi.types",
"stac-fastapi.api",
"stac-fastapi.extensions",
"pydantic",
"stac_pydantic",
"stac-fastapi.types==6.0.0",
"stac-fastapi.api==6.0.0",
"stac-fastapi.extensions==6.0.0",
"sqlakeyset",
"geoalchemy2<0.14.0",
"geoalchemy2",
"sqlalchemy==1.3.23",
"shapely",
"psycopg2-binary",
"alembic",
"fastapi-utils",
"typing-inspect",
]

extra_reqs = {
Expand All @@ -34,7 +35,7 @@
"wheel",
],
"docs": ["mkdocs", "mkdocs-material", "pdocs"],
"server": ["uvicorn[standard]==0.19.0"],
"server": ["uvicorn[standard]==0.35.0"],
}


Expand All @@ -48,7 +49,7 @@
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: Science/Research",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: MIT License",
],
keywords="STAC FastAPI COG",
Expand Down
7 changes: 4 additions & 3 deletions stac_fastapi/sqlalchemy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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,
#ContextExtension,
FieldsExtension,
SortExtension,
TokenPaginationExtension,
Expand All @@ -30,7 +30,7 @@
QueryExtension(),
SortExtension(),
TokenPaginationExtension(),
ContextExtension(),
#ContextExtension(),
]

post_request_model = create_post_request_model(extensions)
Expand All @@ -39,11 +39,12 @@
settings=settings,
extensions=extensions,
client=CoreCrudClient(
session=session, extensions=extensions, post_request_model=post_request_model
session=session, extensions=extensions , post_request_model=post_request_model
),
search_get_request_model=create_get_request_model(extensions),
search_post_request_model=post_request_model,
)

app = api.app


Expand Down
86 changes: 68 additions & 18 deletions stac_fastapi/sqlalchemy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from sqlakeyset import get_page
from sqlalchemy import func
from sqlalchemy.orm import Session as SqlSession
from stac_fastapi.api.models import create_post_request_model
from stac_fastapi.types.config import Settings
from stac_fastapi.types.core import BaseCoreClient
from stac_fastapi.types.errors import NotFoundError
Expand Down Expand Up @@ -50,6 +51,12 @@ class CoreCrudClient(PaginationTokenClient, BaseCoreClient):
default=serializers.CollectionSerializer
)

#added attribute post_request_model to the class core crud client
post_request_model: type = attr.ib(factory=lambda: create_post_request_model([]))




@staticmethod
def _lookup_id(
id: str, table: Type[database.BaseModel], session: SqlSession
Expand Down Expand Up @@ -129,7 +136,16 @@ def item_collection(
bbox_2d = [bbox[0], bbox[1], bbox[3], bbox[4]]
geom = ShapelyPolygon.from_bounds(*bbox_2d)
if geom:
filter_geom = ga.shape.from_shape(geom, srid=4326)
# Ensure `geom` is a Shapely geometry
if not hasattr(geom, "wkt"):
geom = shape(geom)

#convert to WKT
wkt = geom.wkt

"""use shapelys shape method, geoalchemy's shape attribute has been removed"""
filter_geom = func.ST_GeomFromText(wkt, 4326)
#filter_geom = from_shape(geom, srid=4326)
query = query.filter(
ga.func.ST_Intersects(self.item_table.geometry, filter_geom)
)
Expand Down Expand Up @@ -262,9 +278,10 @@ def get_search(
"bbox": bbox,
"limit": limit,
"token": token,
"fields": fields,
"query": json.loads(unquote_plus(query)) if query else query,
}

#print(f"\n--------------------------------Parsed base_args---------------\n\n{base_args}")
if datetime:
base_args["datetime"] = datetime

Expand Down Expand Up @@ -294,13 +311,16 @@ def get_search(
else:
includes.add(field)
base_args["fields"] = {"include": includes, "exclude": excludes}
#print(f'-----------------------base args: {base_args["fields"]}--------------------------')

# Do the request
try:
search_request = self.post_request_model(**base_args)
#print(f"\n------------------------------Validated search_request------\n\n", search_request)
except ValidationError:
raise HTTPException(status_code=400, detail="Invalid parameters provided")
resp = self.post_search(search_request, request=kwargs["request"])
#print(f'\n------------------search response before pagination links----------------------\n\n{resp}\n\n{type(resp)}')

# Pagination
page_links = []
Expand All @@ -317,6 +337,7 @@ def get_search(
else:
page_links.append(link)
resp["links"] = page_links

return resp

def post_search(
Expand Down Expand Up @@ -397,12 +418,26 @@ def post_search(
]
geom = ShapelyPolygon.from_bounds(*bbox_2d)

# if geom:
# filter_geom = ga.shape(geom, srid=4326)
# query = query.filter(
# ga.func.ST_Intersects(self.item_table.geometry, filter_geom)
# )
"""geoalchemy has removed the shape attribute, we default to shapely"""
if geom:
filter_geom = ga.shape.from_shape(geom, srid=4326)
# Ensure `geom` is a Shapely geometry
if not hasattr(geom, "wkt"):
geom = shape(geom)

# Convert to WKT
wkt = geom.wkt

filter_geom = func.ST_GeomFromText(wkt, 4326)
query = query.filter(
ga.func.ST_Intersects(self.item_table.geometry, filter_geom)
func.ST_Intersects(self.item_table.geometry, filter_geom)
)


# Temporal query
if search_request.datetime:
# Two tailed query (between)
Expand Down Expand Up @@ -481,9 +516,16 @@ def post_search(
response_features.append(
self.item_serializer.db_to_stac(item, base_url=base_url)
)
#for i in response_features:
##print(f'----------------response item(db_to_stac) --------------\n\n{i}')

# Use pydantic includes/excludes syntax to implement fields extension
#apply the fields extension logic
if self.extension_is_enabled("FieldsExtension"):

include = getattr(search_request.fields, "include", set()) or set()
exclude = getattr(search_request.fields, "exclude", set()) or set()

#dynamically include query fields
if search_request.query is not None:
query_include: Set[str] = set(
[
Expand All @@ -493,18 +535,26 @@ def post_search(
for k in search_request.query.keys()
]
)
if not search_request.fields.include:
search_request.fields.include = query_include
else:
search_request.fields.include.union(query_include)

filter_kwargs = search_request.fields.filter_fields
# Need to pass through `.json()` for proper serialization
# of datetime
response_features = [
json.loads(stac_pydantic.Item(**feat).json(**filter_kwargs))
for feat in response_features
]

# Only pass if non-empty
if include and len(include) > 0:
response_features = [
json.loads(stac_pydantic.Item(**feat).model_dump_json(include=include))
for feat in response_features
]
#print(f'---------------------------------fields extension response included------------------------\n\n{response_features}')
elif exclude and len(exclude) > 0:
response_features = [
json.loads(stac_pydantic.Item(**feat).model_dump_json(exclude=exclude))
for feat in response_features
]
#print(f'---------------------------------fields extension response excluded------------------------\n\n{response_features}')

else:
response_features = [
json.loads(stac_pydantic.Item(**feat).model_dump_json())
for feat in response_features
]

context_obj = None
if self.extension_is_enabled("ContextExtension"):
Expand All @@ -519,4 +569,4 @@ def post_search(
features=response_features,
links=links,
context=context_obj,
)
)
21 changes: 11 additions & 10 deletions stac_fastapi/sqlalchemy/extensions/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
from typing import Any, Callable, Dict, Optional, Union

import sqlalchemy as sa
from pydantic import BaseModel, ValidationError, root_validator
from pydantic.error_wrappers import ErrorWrapper
from pydantic import BaseModel, ValidationError, root_validator, model_validator
#from pydantic.error_wrappers import ErrorWrapper
from stac_fastapi.extensions.core.query import QueryExtension as QueryExtensionBase
from stac_pydantic.utils import AutoValueEnum

from stac_fastapi.types.search import BaseSearchPostRequest
logger = logging.getLogger("uvicorn")
logger.setLevel(logging.INFO)
# Be careful: https://github.com/samuelcolvin/pydantic/issues/1423#issuecomment-642797287
Expand Down Expand Up @@ -99,10 +99,11 @@ class QueryExtensionPostRequest(BaseModel):
Add queryables validation to the POST request
to raise errors for unsupported querys.
"""
#added `= None` to make it fully optional
query: Optional[Dict[Queryables, Dict[Operator, Any]]] = None

query: Optional[Dict[Queryables, Dict[Operator, Any]]]

@root_validator(pre=True)
@model_validator(mode="before")
#@root_validator(pre=True)
def validate_query_fields(cls, values: Dict) -> Dict:
"""Validate query fields."""
logger.debug(f"Validating SQLAlchemySTACSearch {cls} {values}")
Expand All @@ -112,10 +113,10 @@ def validate_query_fields(cls, values: Dict) -> Dict:
if field_name not in queryable_fields:
raise ValidationError(
[
ErrorWrapper(
ValueError(f"Cannot search on field: {field_name}"),
"STACSearch",
)
{
'loc': ('query', field_name),
'msg': f"Cannot search on field: {field_name}", 'type': 'value_error'
}
],
QueryExtensionPostRequest,
)
Expand Down
Loading
Loading