Skip to content

Commit d69369b

Browse files
authored
refactor: adapt code to collection and provider object (#80)
- In eodag a `collection` is now an object and there is also a `CollectionsList` and `CollectionsDict` class. This PR updates the collections initialisation and search to the new objects. - For providers there is a `Provider` and `ProvidersDict` class. The method `available_providers` for the `EODataAccessGateway` is deprecated and the `providers` property is used instead.
1 parent 1157af8 commit d69369b

10 files changed

Lines changed: 206 additions & 139 deletions

File tree

stac_fastapi/eodag/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ async def landing_page(self, **kwargs) -> stac.LandingPage:
4949
link["title"] = "Collections"
5050

5151
# add federation backends infos
52-
federation_backends = request.app.state.dag.available_providers()
52+
federation_backends = request.app.state.dag.providers.names
5353
federation_dict = {fb: get_federation_backend_dict(request, fb) for fb in federation_backends}
5454
landing_page["federation"] = federation_dict
5555

stac_fastapi/eodag/core.py

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@
3838
from stac_fastapi.types.rfc3339 import str_to_interval
3939
from stac_fastapi.types.search import BaseSearchPostRequest
4040
from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection
41-
from stac_pydantic.links import Relations
41+
from stac_pydantic.links import Links, Relations
4242
from stac_pydantic.shared import MimeTypes
4343

4444
from eodag import EOProduct, SearchResult
45+
from eodag.api.collection import Collection as EodagCollection
46+
from eodag.api.collection import CollectionsList
4547
from eodag.plugins.search.build_search_result import ECMWFSearch
4648
from eodag.utils import deepcopy, get_geometry_from_various
4749
from eodag.utils.exceptions import NoMatchingCollection as EodagNoMatchingCollection
@@ -92,19 +94,19 @@ class EodagCoreClient(CustomCoreClient):
9294
post_request_model: type[BaseModel] = attr.ib(default=BaseSearchPostRequest)
9395
stac_metadata_model: type[CommonStacMetadata] = attr.ib(default=CommonStacMetadata)
9496

95-
def _get_collection(self, collection: dict[str, Any], request: Request) -> Collection:
97+
def _get_collection(self, collection: EodagCollection, request: Request) -> Collection:
9698
"""Convert a EODAG produt type to a STAC collection."""
9799

98100
# extend collection with external stac collection if any
99-
extended_collection = Collection(deepcopy(request.app.state.ext_stac_collections.get(collection["ID"], {})))
101+
extended_collection = Collection(deepcopy(request.app.state.ext_stac_collections.get(collection.id, {})))
100102
extended_collection["type"] = "Collection"
101103

102-
platform_value = [p for p in (collection.get("platform") or "").split(",") if p]
103-
constellation = [c for c in (collection.get("constellation") or "").split(",") if c]
104-
processing_level = [pl for pl in (collection.get("processing:level") or "").split(",") if pl]
105-
instruments = collection.get("instruments") or []
104+
platform_value = [p for p in (collection.platform or "").split(",") if p]
105+
constellation = [c for c in (collection.constellation or "").split(",") if c]
106+
processing_level = [pl for pl in (collection.processing_level or "").split(",") if pl]
107+
instruments = collection.instruments or []
106108

107-
federation_backends = request.app.state.dag.available_providers(collection["_id"])
109+
federation_backends = request.app.state.dag.providers.filter(collection._id).names
108110

109111
summaries: dict[str, Any] = {
110112
"platform": platform_value,
@@ -114,31 +116,31 @@ def _get_collection(self, collection: dict[str, Any], request: Request) -> Colle
114116
"federation:backends": federation_backends,
115117
}
116118
extended_collection["summaries"] = {
117-
**collection.get("summaries", {}),
119+
**(getattr(collection, "summaries", {}) or {}),
118120
**{k: v for k, v in summaries.items() if v},
119121
}
120122

121123
extended_collection["extent"] = {
122124
"spatial": extended_collection.get("extent", {}).get("spatial")
123-
or collection.get("extent", {}).get("spatial")
125+
or collection.extent.spatial.to_dict()
124126
or {"bbox": [[-180.0, -90.0, 180.0, 90.0]]},
125127
"temporal": extended_collection.get("extent", {}).get("temporal")
126-
or collection.get("extent", {}).get("temporal")
128+
or collection.extent.temporal.to_dict()
127129
or {"interval": [[None, None]]},
128130
}
129131

130132
for key in ["license", "description", "title"]:
131-
if value := collection.get(key):
133+
if value := getattr(collection, key):
132134
extended_collection[key] = value
133135

134-
keywords = collection.get("keywords", [])
136+
keywords = collection.keywords or []
135137
keywords = keywords.split(",") if isinstance(keywords, str) else keywords
136138
try:
137139
extended_collection["keywords"] = list(set(keywords + extended_collection.get("keywords", [])))
138140
except TypeError as e:
139-
logger.warning("Could not merge keywords from external collection for %s: %s", collection["ID"], str(e))
141+
logger.warning("Could not merge keywords from external collection for %s: %s", collection.id, str(e))
140142

141-
extended_collection["id"] = collection["ID"]
143+
extended_collection["id"] = collection.id
142144

143145
# keep only federation backends which allow order mechanism
144146
# to create "retrieve" collection links from them
@@ -155,14 +157,14 @@ def has_ecmwf_search_plugin(federation_backends, request):
155157
):
156158
extension_names.remove("CollectionOrderExtension")
157159

160+
extra_links = (collection.links or Links([])).root
161+
extended_coll_links = Links(extended_collection.get("links", [])).root
158162
extended_collection["links"] = CollectionLinks(
159163
collection_id=extended_collection["id"],
160164
request=request,
161-
).get_links(
162-
extensions=extension_names, extra_links=collection.get("links", []) + extended_collection.get("links", [])
163-
)
165+
).get_links(extensions=extension_names, extra_links=extra_links + extended_coll_links)
164166

165-
return extended_collection
167+
return Collection(**extended_collection)
166168

167169
def _search_base(self, search_request: BaseSearchPostRequest, request: Request) -> ItemCollection:
168170
eodag_args = prepare_search_base_args(search_request=search_request, model=self.stac_metadata_model)
@@ -175,11 +177,12 @@ def _search_base(self, search_request: BaseSearchPostRequest, request: Request)
175177

176178
# check if the collection exists
177179
if collection := eodag_args.get("collection"):
178-
all_pt = request.app.state.dag.list_collections(fetch_providers=False)
180+
all_coll = request.app.state.dag.list_collections(fetch_providers=False)
179181
# only check the first collection (EODAG search only support a single collection)
180-
existing_pt = [pt for pt in all_pt if pt["ID"] == collection]
181-
if not existing_pt:
182+
existing_coll = [coll for coll in all_coll if coll.id == collection]
183+
if not existing_coll:
182184
raise NoMatchingCollection(f"Collection {collection} does not exist.")
185+
eodag_args["collection"] = existing_coll[0].id
183186
else:
184187
raise HTTPException(status_code=400, detail="A collection is required")
185188

@@ -265,7 +268,7 @@ async def all_collections(
265268
provider = parsed_query.get("federation:backends")
266269
provider = provider[0] if isinstance(provider, list) else provider
267270

268-
all_pt = request.app.state.dag.list_collections(provider=provider, fetch_providers=False)
271+
all_colls = request.app.state.dag.list_collections(provider=provider, fetch_providers=False)
269272

270273
# datetime & free-text-search filters
271274
if any((q, datetime)):
@@ -279,31 +282,32 @@ async def all_collections(
279282
guessed_collections = request.app.state.dag.guess_collection(
280283
free_text=free_text, start_date=start, end_date=end
281284
)
285+
guessed_collections_ids = [coll.id for coll in guessed_collections]
282286
except EodagNoMatchingCollection:
283-
collections = []
287+
collections = CollectionsList([])
284288
else:
285-
collections = [pt for pt in all_pt if pt["ID"] in guessed_collections]
289+
collections = CollectionsList([coll for coll in all_colls if coll.id in guessed_collections_ids])
286290
else:
287-
collections = all_pt
291+
collections = all_colls
288292

289-
collections = [self._get_collection(pt, request) for pt in collections]
293+
formatted_collections = [self._get_collection(coll, request) for coll in collections]
290294

291295
# bbox filter
292296
if bbox:
293297
bbox_geom = get_geometry_from_various(geometry=bbox)
294298

295299
default_extent = [[-180.0, -90.0, 180.0, 90.0]]
296-
collections = [
300+
formatted_collections = [
297301
c
298-
for c in collections
302+
for c in formatted_collections
299303
if check_poly_is_point(
300304
get_geometry_from_various( # type: ignore
301305
geometry=c.get("extent", {}).get("spatial", {}).get("bbox", default_extent)[0]
302306
)
303307
).intersection(bbox_geom)
304308
]
305309

306-
total = len(collections)
310+
total = len(formatted_collections)
307311

308312
links = [
309313
{
@@ -318,7 +322,7 @@ async def all_collections(
318322
limit = limit if limit is not None else 10
319323
offset = offset if offset is not None else 0
320324

321-
collections = collections[offset : offset + limit]
325+
formatted_collections = formatted_collections[offset : offset + limit]
322326

323327
if offset + limit < total:
324328
next_link = {"body": {"limit": limit, "offset": offset + limit}}
@@ -337,10 +341,10 @@ async def all_collections(
337341
links.extend(paging_links)
338342

339343
return Collections(
340-
collections=collections,
344+
collections=formatted_collections,
341345
links=links,
342346
numberMatched=total,
343-
numberReturned=len(collections),
347+
numberReturned=len(formatted_collections),
344348
)
345349

346350
async def get_collection(self, collection_id: str, request: Request, **kwargs: Any) -> Collection:
@@ -356,7 +360,7 @@ async def get_collection(self, collection_id: str, request: Request, **kwargs: A
356360
:raises NotFoundError: If the collection does not exist.
357361
"""
358362
collection = next(
359-
(pt for pt in request.app.state.dag.list_collections(fetch_providers=False) if pt["ID"] == collection_id),
363+
(c for c in request.app.state.dag.list_collections(fetch_providers=False) if c.id == collection_id),
360364
None,
361365
)
362366
if collection is None:
@@ -422,7 +426,7 @@ async def item_collection(
422426
item_collection = self._search_base(search_request, request)
423427
extension_names = [type(ext).__name__ for ext in self.extensions]
424428
links = ItemCollectionLinks(collection_id=collection_id, request=request).get_links(
425-
extensions=extension_names, extra_links=item_collection["links"]
429+
extensions=extension_names, extra_links=Links(item_collection["links"]).root
426430
)
427431
item_collection["links"] = links
428432
return item_collection

stac_fastapi/eodag/dag.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
import logging
2323
from typing import TYPE_CHECKING
2424

25+
from stac_pydantic.collection import Extent, SpatialExtent, TimeInterval
26+
2527
from eodag import EODataAccessGateway
28+
from eodag.api.collection import CollectionsList
2629
from eodag.utils.exceptions import (
2730
RequestError,
2831
TimeOutError,
@@ -39,7 +42,7 @@
3942

4043

4144
def fetch_external_stac_collections(
42-
collections: list[dict[str, Any]],
45+
collections: CollectionsList,
4346
) -> dict[str, dict[str, Any]]:
4447
"""Load external STAC collections
4548
@@ -49,10 +52,10 @@ def fetch_external_stac_collections(
4952
ext_stac_collections: dict[str, dict[str, Any]] = {}
5053

5154
for collection in collections:
52-
file_path = collection.get("stacCollection")
55+
file_path = getattr(collection, "eodag_stac_collection", None)
5356
if not file_path:
5457
continue
55-
logger.info(f"Fetching external STAC collection for {collection['ID']}")
58+
logger.info(f"Fetching external STAC collection for {collection.id}")
5659

5760
try:
5861
ext_stac_collection = fetch_json(file_path)
@@ -63,7 +66,7 @@ def fetch_external_stac_collections(
6366
)
6467
ext_stac_collection = {}
6568

66-
ext_stac_collections[collection["ID"]] = ext_stac_collection
69+
ext_stac_collections[collection.id] = ext_stac_collection
6770
return ext_stac_collections
6871

6972

@@ -80,8 +83,8 @@ def init_dag(app: FastAPI) -> None:
8083
app.state.ext_stac_collections = ext_stac_collections
8184

8285
# update eodag collections config form external stac collections
83-
for p, p_f in dag.collections_config.source.items():
84-
for key in (p, p_f.get("alias")):
86+
for c, c_f in dag.collections_config.items():
87+
for key in (c, getattr(c_f, "alias", None)):
8588
if key is None:
8689
continue
8790
ext_col = ext_stac_collections.get(key)
@@ -98,23 +101,27 @@ def init_dag(app: FastAPI) -> None:
98101
constellation = ",".join(constellation)
99102
if isinstance(processing_level, list):
100103
processing_level = ",".join(processing_level)
104+
ext_extent = ext_col["extent"]
105+
temporal_ext = TimeInterval(**ext_extent.get("temporal", [[None, None]]))
106+
spatial_ext = SpatialExtent(**ext_extent.get("spatial", {"bbox": [[-180.0, -90.0, 180.0, 90.0]]}))
101107

102108
update_fields: dict[str, Any] = {
103-
"title": p_f.get("title") or ext_col.get("title"),
104-
"description": p_f.get("description") or ext_col["description"],
109+
"title": c_f.title or ext_col.get("title"),
110+
"description": c_f.description or ext_col["description"],
105111
"keywords": ext_col.get("keywords"),
106-
"instruments": p_f.get("instruments") or instruments,
107-
"platform": p_f.get("platform") or platform,
108-
"constellation": p_f.get("constellation") or constellation,
109-
"processing:level": p_f.get("processing:level") or processing_level,
112+
"instruments": c_f.instruments or instruments,
113+
"platform": c_f.platform or platform,
114+
"constellation": c_f.constellation or constellation,
115+
"processing_level": c_f.processing_level or processing_level,
110116
"license": ext_col["license"],
111-
"extent": ext_col["extent"],
117+
"extent": Extent(temporal=temporal_ext, spatial=spatial_ext),
112118
}
113119
clean = {k: v for k, v in update_fields.items() if v is not None}
114-
p_f.update(clean)
120+
for field, value in clean.items():
121+
setattr(c_f, field, value)
115122

116123
# pre-build search plugins
117-
for provider in dag.available_providers():
124+
for provider in dag.providers:
118125
next(dag._plugins_manager.get_search_plugins(provider=provider))
119126

120127
app.state.dag = dag

stac_fastapi/eodag/models/links.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import attr
2424
import geojson
2525
from stac_fastapi.types.requests import get_base_url
26-
from stac_pydantic.links import Relations
26+
from stac_pydantic.links import Link, Relations
2727
from stac_pydantic.shared import MimeTypes
2828
from starlette.requests import Request
2929

@@ -105,7 +105,7 @@ def create_links(self, extensions: list[str]) -> list[dict[str, Any]]:
105105
def get_links(
106106
self,
107107
extensions: list[str],
108-
extra_links: Optional[list[dict[str, Any]]] = None,
108+
extra_links: Optional[list[Link]] = None,
109109
request_json: Optional[dict[str, Any]] = None,
110110
) -> list[dict[str, Any]]:
111111
"""
@@ -129,11 +129,9 @@ def get_links(
129129
# to be stored in pgstac and for the hrefs in the
130130
# links of response STAC objects to be resolved
131131
# to the request url.
132-
links += [
133-
{**link, "href": self.resolve(link["href"])}
134-
for link in extra_links
135-
if link["rel"] not in INFERRED_LINK_RELS
136-
]
132+
for link in extra_links:
133+
link.href = self.resolve(link.href)
134+
links += [link.model_dump() for link in extra_links if link.rel not in INFERRED_LINK_RELS]
137135

138136
return links
139137

stac_fastapi/eodag/models/stac_metadata.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -210,20 +210,20 @@ def create_stac_metadata_model(
210210
return model
211211

212212

213-
def get_federation_backend_dict(request: Request, provider: str) -> dict[str, Any]:
213+
def get_federation_backend_dict(request: Request, provider_name: str) -> dict[str, Any]:
214214
"""Generate Federation backend dict
215215
216216
:param request: FastAPI request
217-
:param provider: provider name
217+
:param provider_name: provider name
218218
:return: Federation backend dictionary
219219
"""
220-
provider_config = next(
221-
p for p in request.app.state.dag.providers_config.values() if provider in [p.name, getattr(p, "group", None)]
220+
provider = next(
221+
p for p in request.app.state.dag.providers.values() if provider_name in [p.name, getattr(p, "group", None)]
222222
)
223223
return {
224-
"title": getattr(provider_config, "group", provider_config.name),
225-
"description": getattr(provider_config, "description", None),
226-
"url": getattr(provider_config, "url", None),
224+
"title": provider.group or provider.name,
225+
"description": provider.title,
226+
"url": provider.url,
227227
}
228228

229229

@@ -260,9 +260,8 @@ def create_stac_item(
260260

261261
settings: Settings = get_settings()
262262

263-
collection = request.app.state.dag.collections_config.source.get(product.collection, {}).get(
264-
"alias", product.collection
265-
)
263+
collection_obj = request.app.state.dag.collections_config.get(product.collection)
264+
collection = collection_obj.id if collection_obj else product.collection
266265

267266
feature = Item(
268267
type="Feature",

0 commit comments

Comments
 (0)