33import re
44from datetime import datetime as datetime_type
55from datetime import timezone
6+ from enum import Enum
67from typing import Any , Dict , List , Optional , Set , Type , Union
78from urllib .parse import unquote_plus , urljoin
89
1415from pydantic import ValidationError
1516from pygeofilter .backends .cql2_json import to_cql2
1617from pygeofilter .parsers .cql2_text import parse as parse_cql2_text
18+ from stac_pydantic import Collection , Item , ItemCollection
1719from stac_pydantic .links import Relations
1820from stac_pydantic .shared import BBox , MimeTypes
1921from stac_pydantic .version import STAC_VERSION
2527from stac_fastapi .core .session import Session
2628from stac_fastapi .core .types .core import (
2729 AsyncBaseCoreClient ,
28- AsyncBaseFiltersClient ,
2930 AsyncBaseTransactionsClient ,
3031)
3132from stac_fastapi .extensions .third_party .bulk_transactions import (
3637from stac_fastapi .types import stac as stac_types
3738from stac_fastapi .types .config import Settings
3839from stac_fastapi .types .conformance import BASE_CONFORMANCE_CLASSES
40+ from stac_fastapi .types .core import AsyncBaseFiltersClient
3941from stac_fastapi .types .extension import ApiExtension
4042from stac_fastapi .types .requests import get_base_url
4143from stac_fastapi .types .rfc3339 import DateTimeType
4244from stac_fastapi .types .search import BaseSearchPostRequest
43- from stac_fastapi .types .stac import Collection , Collections , Item , ItemCollection
4445
4546logger = logging .getLogger (__name__ )
4647
@@ -189,7 +190,7 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
189190
190191 return landing_page
191192
192- async def all_collections (self , ** kwargs ) -> Collections :
193+ async def all_collections (self , ** kwargs ) -> stac_types . Collections :
193194 """Read all collections from the database.
194195
195196 Args:
@@ -221,9 +222,11 @@ async def all_collections(self, **kwargs) -> Collections:
221222 next_link = PagingLinks (next = next_token , request = request ).link_next ()
222223 links .append (next_link )
223224
224- return Collections (collections = collections , links = links )
225+ return stac_types . Collections (collections = collections , links = links )
225226
226- async def get_collection (self , collection_id : str , ** kwargs ) -> Collection :
227+ async def get_collection (
228+ self , collection_id : str , ** kwargs
229+ ) -> stac_types .Collection :
227230 """Get a collection from the database by its id.
228231
229232 Args:
@@ -250,7 +253,7 @@ async def item_collection(
250253 limit : int = 10 ,
251254 token : str = None ,
252255 ** kwargs ,
253- ) -> ItemCollection :
256+ ) -> stac_types . ItemCollection :
254257 """Read items from a specific collection in the database.
255258
256259 Args:
@@ -320,14 +323,16 @@ async def item_collection(
320323
321324 links = await PagingLinks (request = request , next = next_token ).get_links ()
322325
323- return ItemCollection (
326+ return stac_types . ItemCollection (
324327 type = "FeatureCollection" ,
325328 features = items ,
326329 links = links ,
327330 context = context_obj ,
328331 )
329332
330- async def get_item (self , item_id : str , collection_id : str , ** kwargs ) -> Item :
333+ async def get_item (
334+ self , item_id : str , collection_id : str , ** kwargs
335+ ) -> stac_types .Item :
331336 """Get an item from the database based on its id and collection id.
332337
333338 Args:
@@ -399,6 +404,24 @@ def _return_date(
399404
400405 return result
401406
407+ def _format_datetime_range (self , date_tuple : DateTimeType ) -> str :
408+ """
409+ Convert a tuple of datetime objects or None into a formatted string for API requests.
410+
411+ Args:
412+ date_tuple (tuple): A tuple containing two elements, each can be a datetime object or None.
413+
414+ Returns:
415+ str: A string formatted as 'YYYY-MM-DDTHH:MM:SS.sssZ/YYYY-MM-DDTHH:MM:SS.sssZ', with '..' used if any element is None.
416+ """
417+
418+ def format_datetime (dt ):
419+ """Format a single datetime object to the ISO8601 extended format with 'Z'."""
420+ return dt .strftime ("%Y-%m-%dT%H:%M:%S.%f" )[:- 3 ] + "Z" if dt else ".."
421+
422+ start , end = date_tuple
423+ return f"{ format_datetime (start )} /{ format_datetime (end )} "
424+
402425 async def get_search (
403426 self ,
404427 request : Request ,
@@ -415,7 +438,7 @@ async def get_search(
415438 filter : Optional [str ] = None ,
416439 filter_lang : Optional [str ] = None ,
417440 ** kwargs ,
418- ) -> ItemCollection :
441+ ) -> stac_types . ItemCollection :
419442 """Get search results from the database.
420443
421444 Args:
@@ -455,7 +478,7 @@ async def get_search(
455478 filter_lang = match .group (1 )
456479
457480 if datetime :
458- base_args ["datetime" ] = datetime
481+ base_args ["datetime" ] = self . _format_datetime_range ( datetime )
459482
460483 if intersects :
461484 base_args ["intersects" ] = orjson .loads (unquote_plus (intersects ))
@@ -502,7 +525,7 @@ async def get_search(
502525
503526 async def post_search (
504527 self , search_request : BaseSearchPostRequest , request : Request
505- ) -> ItemCollection :
528+ ) -> stac_types . ItemCollection :
506529 """
507530 Perform a POST search on the catalog.
508531
@@ -552,8 +575,10 @@ async def post_search(
552575 for field_name , expr in search_request .query .items ():
553576 field = "properties__" + field_name
554577 for op , value in expr .items ():
578+ # Convert enum to string
579+ operator = op .value if isinstance (op , Enum ) else op
555580 search = self .database .apply_stacql_filter (
556- search = search , op = op , field = field , value = value
581+ search = search , op = operator , field = field , value = value
557582 )
558583
559584 # only cql2_json is supported here
@@ -619,7 +644,7 @@ async def post_search(
619644
620645 links = await PagingLinks (request = request , next = next_token ).get_links ()
621646
622- return ItemCollection (
647+ return stac_types . ItemCollection (
623648 type = "FeatureCollection" ,
624649 features = items ,
625650 links = links ,
@@ -637,7 +662,7 @@ class TransactionsClient(AsyncBaseTransactionsClient):
637662
638663 @overrides
639664 async def create_item (
640- self , collection_id : str , item : stac_types . Item , ** kwargs
665+ self , collection_id : str , item : Union [ Item , ItemCollection ] , ** kwargs
641666 ) -> Optional [stac_types .Item ]:
642667 """Create an item in the collection.
643668
@@ -654,6 +679,7 @@ async def create_item(
654679 ConflictError: If the item in the specified collection already exists.
655680
656681 """
682+ item = item .model_dump (mode = "json" )
657683 base_url = str (kwargs ["request" ].base_url )
658684
659685 # If a feature collection is posted
@@ -677,7 +703,7 @@ async def create_item(
677703
678704 @overrides
679705 async def update_item (
680- self , collection_id : str , item_id : str , item : stac_types . Item , ** kwargs
706+ self , collection_id : str , item_id : str , item : Item , ** kwargs
681707 ) -> stac_types .Item :
682708 """Update an item in the collection.
683709
@@ -694,13 +720,14 @@ async def update_item(
694720 NotFound: If the specified collection is not found in the database.
695721
696722 """
723+ item = item .model_dump (mode = "json" )
697724 base_url = str (kwargs ["request" ].base_url )
698725 now = datetime_type .now (timezone .utc ).isoformat ().replace ("+00:00" , "Z" )
699726 item ["properties" ]["updated" ] = now
700727
701728 await self .database .check_collection_exists (collection_id )
702729 await self .delete_item (item_id = item_id , collection_id = collection_id )
703- await self .create_item (collection_id = collection_id , item = item , ** kwargs )
730+ await self .create_item (collection_id = collection_id , item = Item ( ** item ) , ** kwargs )
704731
705732 return ItemSerializer .db_to_stac (item , base_url )
706733
@@ -722,7 +749,7 @@ async def delete_item(
722749
723750 @overrides
724751 async def create_collection (
725- self , collection : stac_types . Collection , ** kwargs
752+ self , collection : Collection , ** kwargs
726753 ) -> stac_types .Collection :
727754 """Create a new collection in the database.
728755
@@ -736,17 +763,17 @@ async def create_collection(
736763 Raises:
737764 ConflictError: If the collection already exists.
738765 """
766+ collection = collection .model_dump (mode = "json" )
739767 base_url = str (kwargs ["request" ].base_url )
740768 collection = self .database .collection_serializer .stac_to_db (
741769 collection , base_url
742770 )
743771 await self .database .create_collection (collection = collection )
744-
745772 return CollectionSerializer .db_to_stac (collection , base_url )
746773
747774 @overrides
748775 async def update_collection (
749- self , collection : stac_types . Collection , ** kwargs
776+ self , collection : Collection , ** kwargs
750777 ) -> stac_types .Collection :
751778 """
752779 Update a collection.
@@ -766,6 +793,8 @@ async def update_collection(
766793 A STAC collection that has been updated in the database.
767794
768795 """
796+ collection = collection .model_dump (mode = "json" )
797+
769798 base_url = str (kwargs ["request" ].base_url )
770799
771800 collection_id = kwargs ["request" ].query_params .get (
0 commit comments