11"""titiler.cmr FastAPI dependencies."""
22
3+ import re
34from collections .abc import Callable
45from dataclasses import dataclass , field
56from datetime import datetime
67from typing import Annotated , List , Optional
78
89from fastapi import Depends , HTTPException , Query , Request
910from httpx import Client
10- from titiler .core .dependencies import DefaultDependency , ExpressionParams
11+ from pydantic import AfterValidator
12+ from titiler .core .dependencies import (
13+ AssetsExprParams ,
14+ DefaultDependency ,
15+ ExpressionParams ,
16+ _parse_asset ,
17+ )
1118from titiler .xarray .dependencies import SelDimStr , XarrayIOParams
1219
1320from titiler .cmr .models import (
@@ -58,7 +65,11 @@ def GranuleSearchParams(
5865
5966@dataclass (init = False )
6067class BackendParams (DefaultDependency ):
61- """backend parameters."""
68+ """Reader backend parameters sourced from application state.
69+
70+ Reads the HTTP client, Earthdata auth token, S3 access flag, and S3
71+ credential provider from the FastAPI app state on each request.
72+ """
6273
6374 client : Client = field (init = False )
6475 auth_token : str | None = field (init = False )
@@ -76,7 +87,7 @@ def __init__(self, request: Request):
7687
7788@dataclass
7889class GranuleSearchBackendParams (DefaultDependency ):
79- """PgSTAC parameters."""
90+ """Backend parameters controlling granule search coverage behaviour ."""
8091
8192 items_limit : Annotated [
8293 int | None ,
@@ -118,6 +129,72 @@ def __post_init__(self):
118129 self .bands_regex = None
119130
120131
132+ def _translate_legacy_expr (expression : str , names : list [str ]) -> str :
133+ """Translate legacy name-based expression to positional bN format.
134+
135+ If the expression contains identifiers from `names` (not already bN-style,
136+ not function calls), replaces them with b1, b2, ... based on their position
137+ in `names`.
138+ """
139+ identifiers = re .findall (r"\b([a-zA-Z_]\w*)\b(?!\s*\()" , expression )
140+ new_style = re .compile (r"^b[1-9][0-9]*$" , re .IGNORECASE )
141+ legacy = list (dict .fromkeys (n for n in identifiers if not new_style .match (n )))
142+ if not legacy :
143+ return expression
144+ mapping = {name : f"b{ i + 1 } " for i , name in enumerate (names )}
145+ expr = expression
146+ for name , band_ref in mapping .items ():
147+ expr = re .sub (r"\b" + re .escape (name ) + r"\b" , band_ref , expr )
148+ return expr
149+
150+
151+ @dataclass
152+ class CMRAssetsExprParams (AssetsExprParams ):
153+ """AssetsExprParams with backwards-compatible legacy expression translation.
154+
155+ Detects legacy expressions that reference asset names directly (e.g. B04, NIR)
156+ and translates them to the new rio-tiler 9.0 positional band format (b1, b2, ...).
157+ """
158+
159+ assets : Annotated [
160+ list [str ] | None ,
161+ AfterValidator (_parse_asset ),
162+ Query (
163+ title = "Asset names" ,
164+ description = "Asset's names." ,
165+ ),
166+ ] = None
167+
168+ def __post_init__ (self ):
169+ """Translate legacy asset-name expressions to positional bN format.
170+
171+ If the expression already uses bN references (e.g. b1-b2), it is left
172+ unchanged. Otherwise, identifiers are matched against the provided or
173+ auto-detected asset list and substituted with b1, b2, ... in order.
174+ """
175+ if not self .expression :
176+ return
177+
178+ identifiers = re .findall (r"\b([a-zA-Z_]\w*)\b(?!\s*\()" , self .expression )
179+ new_style_pattern = re .compile (r"^b[1-9][0-9]*$" , re .IGNORECASE )
180+ asset_names = list (
181+ dict .fromkeys (
182+ name for name in identifiers if not new_style_pattern .match (name )
183+ )
184+ )
185+
186+ if not asset_names :
187+ return
188+
189+ if self .assets :
190+ ordered_assets = list (self .assets )
191+ else :
192+ ordered_assets = asset_names
193+ self .assets = ordered_assets
194+
195+ self .expression = _translate_legacy_expr (self .expression , ordered_assets )
196+
197+
121198@dataclass
122199class XarrayDsParams (DefaultDependency ):
123200 """Xarray Dataset Options."""
@@ -152,12 +229,29 @@ class InterpolatedXarrayParams(XarrayParams):
152229 ] = None
153230
154231
232+ @dataclass
233+ class CMRXarrayExprParams (InterpolatedXarrayParams ):
234+ """InterpolatedXarrayParams with legacy variable-name expression translation.
235+
236+ Translates expressions like `temperature/pressure` to `b1/b2` based on the
237+ order of `variables`.
238+ """
239+
240+ def __post_init__ (self ):
241+ """Translate legacy variable-name expressions to positional bN format.
242+
243+ Skipped when expression is already new-style (contains only bN refs) or
244+ when no expression is provided. Safe to call more than once — already-
245+ translated expressions are returned unchanged.
246+ """
247+ if self .expression and self .variables :
248+ self .expression = _translate_legacy_expr (self .expression , self .variables )
249+
250+
155251def interpolated_xarray_ds_params (
156- xarray_params : Annotated [
157- InterpolatedXarrayParams , Depends (InterpolatedXarrayParams )
158- ],
252+ xarray_params : Annotated [CMRXarrayExprParams , Depends (CMRXarrayExprParams )],
159253 granule_search : Annotated [GranuleSearch , Depends (GranuleSearchParams )],
160- ) -> InterpolatedXarrayParams :
254+ ) -> CMRXarrayExprParams :
161255 """
162256 Xarray parameters with string interpolation support for the sel parameter.
163257
@@ -184,9 +278,10 @@ def interpolated_xarray_ds_params(
184278 else :
185279 interpolated_sel .append (sel_item )
186280
187- return InterpolatedXarrayParams (
281+ return CMRXarrayExprParams (
188282 variables = xarray_params .variables ,
189283 group = xarray_params .group ,
190284 sel = interpolated_sel ,
191285 decode_times = xarray_params .decode_times ,
286+ expression = xarray_params .expression ,
192287 )
0 commit comments