3
3
import re
4
4
from datetime import datetime as datetime_type
5
5
from datetime import timezone
6
+ from enum import Enum
6
7
from typing import Any , Dict , List , Optional , Set , Type , Union
7
8
from urllib .parse import unquote_plus , urljoin
8
9
14
15
from pydantic import ValidationError
15
16
from pygeofilter .backends .cql2_json import to_cql2
16
17
from pygeofilter .parsers .cql2_text import parse as parse_cql2_text
18
+ from stac_pydantic import Collection , Item , ItemCollection
17
19
from stac_pydantic .links import Relations
18
20
from stac_pydantic .shared import BBox , MimeTypes
19
21
from stac_pydantic .version import STAC_VERSION
25
27
from stac_fastapi .core .session import Session
26
28
from stac_fastapi .core .types .core import (
27
29
AsyncBaseCoreClient ,
28
- AsyncBaseFiltersClient ,
29
30
AsyncBaseTransactionsClient ,
30
31
)
31
32
from stac_fastapi .extensions .third_party .bulk_transactions import (
36
37
from stac_fastapi .types import stac as stac_types
37
38
from stac_fastapi .types .config import Settings
38
39
from stac_fastapi .types .conformance import BASE_CONFORMANCE_CLASSES
40
+ from stac_fastapi .types .core import AsyncBaseFiltersClient
39
41
from stac_fastapi .types .extension import ApiExtension
40
42
from stac_fastapi .types .requests import get_base_url
41
43
from stac_fastapi .types .rfc3339 import DateTimeType
42
44
from stac_fastapi .types .search import BaseSearchPostRequest
43
- from stac_fastapi .types .stac import Collection , Collections , Item , ItemCollection
44
45
45
46
logger = logging .getLogger (__name__ )
46
47
@@ -189,7 +190,7 @@ async def landing_page(self, **kwargs) -> stac_types.LandingPage:
189
190
190
191
return landing_page
191
192
192
- async def all_collections (self , ** kwargs ) -> Collections :
193
+ async def all_collections (self , ** kwargs ) -> stac_types . Collections :
193
194
"""Read all collections from the database.
194
195
195
196
Args:
@@ -221,9 +222,11 @@ async def all_collections(self, **kwargs) -> Collections:
221
222
next_link = PagingLinks (next = next_token , request = request ).link_next ()
222
223
links .append (next_link )
223
224
224
- return Collections (collections = collections , links = links )
225
+ return stac_types . Collections (collections = collections , links = links )
225
226
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 :
227
230
"""Get a collection from the database by its id.
228
231
229
232
Args:
@@ -250,7 +253,7 @@ async def item_collection(
250
253
limit : int = 10 ,
251
254
token : str = None ,
252
255
** kwargs ,
253
- ) -> ItemCollection :
256
+ ) -> stac_types . ItemCollection :
254
257
"""Read items from a specific collection in the database.
255
258
256
259
Args:
@@ -320,14 +323,16 @@ async def item_collection(
320
323
321
324
links = await PagingLinks (request = request , next = next_token ).get_links ()
322
325
323
- return ItemCollection (
326
+ return stac_types . ItemCollection (
324
327
type = "FeatureCollection" ,
325
328
features = items ,
326
329
links = links ,
327
330
context = context_obj ,
328
331
)
329
332
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 :
331
336
"""Get an item from the database based on its id and collection id.
332
337
333
338
Args:
@@ -399,6 +404,24 @@ def _return_date(
399
404
400
405
return result
401
406
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
+
402
425
async def get_search (
403
426
self ,
404
427
request : Request ,
@@ -415,7 +438,7 @@ async def get_search(
415
438
filter : Optional [str ] = None ,
416
439
filter_lang : Optional [str ] = None ,
417
440
** kwargs ,
418
- ) -> ItemCollection :
441
+ ) -> stac_types . ItemCollection :
419
442
"""Get search results from the database.
420
443
421
444
Args:
@@ -455,7 +478,7 @@ async def get_search(
455
478
filter_lang = match .group (1 )
456
479
457
480
if datetime :
458
- base_args ["datetime" ] = datetime
481
+ base_args ["datetime" ] = self . _format_datetime_range ( datetime )
459
482
460
483
if intersects :
461
484
base_args ["intersects" ] = orjson .loads (unquote_plus (intersects ))
@@ -502,7 +525,7 @@ async def get_search(
502
525
503
526
async def post_search (
504
527
self , search_request : BaseSearchPostRequest , request : Request
505
- ) -> ItemCollection :
528
+ ) -> stac_types . ItemCollection :
506
529
"""
507
530
Perform a POST search on the catalog.
508
531
@@ -552,8 +575,10 @@ async def post_search(
552
575
for field_name , expr in search_request .query .items ():
553
576
field = "properties__" + field_name
554
577
for op , value in expr .items ():
578
+ # Convert enum to string
579
+ operator = op .value if isinstance (op , Enum ) else op
555
580
search = self .database .apply_stacql_filter (
556
- search = search , op = op , field = field , value = value
581
+ search = search , op = operator , field = field , value = value
557
582
)
558
583
559
584
# only cql2_json is supported here
@@ -619,7 +644,7 @@ async def post_search(
619
644
620
645
links = await PagingLinks (request = request , next = next_token ).get_links ()
621
646
622
- return ItemCollection (
647
+ return stac_types . ItemCollection (
623
648
type = "FeatureCollection" ,
624
649
features = items ,
625
650
links = links ,
@@ -637,7 +662,7 @@ class TransactionsClient(AsyncBaseTransactionsClient):
637
662
638
663
@overrides
639
664
async def create_item (
640
- self , collection_id : str , item : stac_types . Item , ** kwargs
665
+ self , collection_id : str , item : Union [ Item , ItemCollection ] , ** kwargs
641
666
) -> Optional [stac_types .Item ]:
642
667
"""Create an item in the collection.
643
668
@@ -654,6 +679,7 @@ async def create_item(
654
679
ConflictError: If the item in the specified collection already exists.
655
680
656
681
"""
682
+ item = item .model_dump (mode = "json" )
657
683
base_url = str (kwargs ["request" ].base_url )
658
684
659
685
# If a feature collection is posted
@@ -677,7 +703,7 @@ async def create_item(
677
703
678
704
@overrides
679
705
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
681
707
) -> stac_types .Item :
682
708
"""Update an item in the collection.
683
709
@@ -694,13 +720,14 @@ async def update_item(
694
720
NotFound: If the specified collection is not found in the database.
695
721
696
722
"""
723
+ item = item .model_dump (mode = "json" )
697
724
base_url = str (kwargs ["request" ].base_url )
698
725
now = datetime_type .now (timezone .utc ).isoformat ().replace ("+00:00" , "Z" )
699
726
item ["properties" ]["updated" ] = now
700
727
701
728
await self .database .check_collection_exists (collection_id )
702
729
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 )
704
731
705
732
return ItemSerializer .db_to_stac (item , base_url )
706
733
@@ -722,7 +749,7 @@ async def delete_item(
722
749
723
750
@overrides
724
751
async def create_collection (
725
- self , collection : stac_types . Collection , ** kwargs
752
+ self , collection : Collection , ** kwargs
726
753
) -> stac_types .Collection :
727
754
"""Create a new collection in the database.
728
755
@@ -736,17 +763,17 @@ async def create_collection(
736
763
Raises:
737
764
ConflictError: If the collection already exists.
738
765
"""
766
+ collection = collection .model_dump (mode = "json" )
739
767
base_url = str (kwargs ["request" ].base_url )
740
768
collection = self .database .collection_serializer .stac_to_db (
741
769
collection , base_url
742
770
)
743
771
await self .database .create_collection (collection = collection )
744
-
745
772
return CollectionSerializer .db_to_stac (collection , base_url )
746
773
747
774
@overrides
748
775
async def update_collection (
749
- self , collection : stac_types . Collection , ** kwargs
776
+ self , collection : Collection , ** kwargs
750
777
) -> stac_types .Collection :
751
778
"""
752
779
Update a collection.
@@ -766,6 +793,8 @@ async def update_collection(
766
793
A STAC collection that has been updated in the database.
767
794
768
795
"""
796
+ collection = collection .model_dump (mode = "json" )
797
+
769
798
base_url = str (kwargs ["request" ].base_url )
770
799
771
800
collection_id = kwargs ["request" ].query_params .get (
0 commit comments