2626logger = logging .getLogger (__name__ )
2727
2828
29+ def _robust_urljoin (base : str , path : str ) -> str :
30+ """Join URL parts robustly, ensuring base is treated as a directory."""
31+ # Ensure base ends with slash so it's treated as a directory
32+ if not base .endswith ("/" ):
33+ base += "/"
34+ return urljoin (base , path )
35+
36+
2937COLLECTION_SEARCH_CONFORMANCE_CLASSES = [
3038 STACConformanceClasses .CORE ,
3139 STACConformanceClasses .COLLECTIONS ,
@@ -40,6 +48,7 @@ class UpstreamApiStatus(BaseModel):
4048 """Status information for an upstream API."""
4149
4250 healthy : bool
51+ collection_search_conformance : list [str ]
4352
4453
4554class LifespanStatus (BaseModel ):
@@ -112,7 +121,9 @@ def _get_search_state(
112121 logger .info ("Continuing collection search with token pagination" )
113122 else :
114123 search_state = {
115- "current" : {api : f"{ api } /collections?{ param_str } " for api in apis },
124+ "current" : {
125+ api : _robust_urljoin (api , f"collections?{ param_str } " ) for api in apis
126+ },
116127 "is_first_page" : True ,
117128 }
118129 logger .info (f"Starting new collection search across { len (apis )} APIs" )
@@ -139,7 +150,9 @@ def _build_pagination_links(
139150 links .append (
140151 {
141152 "rel" : "previous" ,
142- "href" : f"{ request .base_url } collections?token={ prev_token } " ,
153+ "href" : _robust_urljoin (
154+ str (request .base_url ), f"collections?token={ prev_token } "
155+ ),
143156 }
144157 )
145158
@@ -152,7 +165,9 @@ def _build_pagination_links(
152165 links .append (
153166 {
154167 "rel" : "next" ,
155- "href" : f"{ request .base_url } collections?token={ next_token } " ,
168+ "href" : _robust_urljoin (
169+ str (request .base_url ), f"collections?token={ next_token } "
170+ ),
156171 }
157172 )
158173
@@ -164,7 +179,7 @@ async def _fetch_api_conformance(
164179 """Fetch conformance classes from a single API."""
165180 async with semaphore :
166181 try :
167- api_response = await client .get (f" { api } /conformance" )
182+ api_response = await client .get (api , follow_redirects = True )
168183 if api_response .status_code == 200 :
169184 conformance_data = api_response .json ()
170185 return api , set (conformance_data .get ("conformsTo" , []))
@@ -507,12 +522,38 @@ async def health_check(
507522 async def check_api_health (client , api : str ) -> tuple [str , UpstreamApiStatus ]:
508523 async with semaphore :
509524 try :
510- # Check landing page health
511- api_response = await client .get (api )
512- return api , UpstreamApiStatus (healthy = api_response .status_code == 200 )
525+ # Check landing page health - follow redirects automatically
526+ api_response = await client .get (api , follow_redirects = True )
527+ # Consider 2xx and 3xx status codes as healthy
528+ is_healthy = 200 <= api_response .status_code < 400
529+
530+ collection_search_conformance = []
531+ if is_healthy :
532+ try :
533+ response_data = api_response .json ()
534+ conforms_to = response_data .get ("conformsTo" , [])
535+ # Extract collection-search conformance classes with suffixes
536+ for cls in conforms_to :
537+ if "collection-search" in cls :
538+ # Extract from "collection-search" onwards
539+ collection_search_part = cls [
540+ cls .find ("collection-search" ) :
541+ ]
542+ collection_search_conformance .append (
543+ collection_search_part
544+ )
545+ except Exception as e :
546+ logger .warning (f"Failed to parse conformance from { api } : { e } " )
547+
548+ return api , UpstreamApiStatus (
549+ healthy = is_healthy ,
550+ collection_search_conformance = collection_search_conformance ,
551+ )
513552
514553 except Exception :
515- return api , UpstreamApiStatus (healthy = False )
554+ return api , UpstreamApiStatus (
555+ healthy = False , collection_search_conformance = []
556+ )
516557
517558 async with AsyncClient (timeout = HTTPX_TIMEOUT ) as client :
518559 tasks = [check_api_health (client , api ) for api in apis ]
0 commit comments