Skip to content

WIP: odata implementation #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/eodm/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pystac_client
from pystac import Collection, Item

from .odata import ODataClient, ODataCollection, ODataProduct, ODataQuery
from .opensearch import OpenSearchClient, OpenSearchFeature


Expand Down Expand Up @@ -67,7 +68,7 @@ def extract_opensearch_features(

Args:
url (str): Link to OpenSearch API endpoint
productTypes (list[str]): List of productTypes to search for
product_types (list[str]): List of productTypes to search for

Yields:
Iterator[OpenSearchFeature]: OpenSearch Features
Expand All @@ -83,3 +84,20 @@ def extract_opensearch_features(
if limit and i >= limit:
break
yield feature


def extract_odata_products(
url: str,
collections: list[ODataCollection],
) -> Iterator[ODataProduct]:
"""Extracts OData Products from an OData API

Args:
url (str): Link to OData API endpoint
collections (list[ODataCollection]): List of collections to search for
"""
client = ODataClient(url)
for collection in collections:
query = ODataQuery(collection=collection.value)
for product in client.search(query):
yield product
113 changes: 113 additions & 0 deletions src/eodm/odata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from datetime import datetime
from enum import Enum
from typing import Annotated, Iterator

import httpx
from geojson_pydantic.geometries import Geometry
from pydantic import BaseModel, Field


class ODataCollection(Enum):
SENTINEL_1 = "SENTINEL-1"
SENTINEL_2 = "SENTINEL-2"
SENTINEL_3 = "SENTINEL-3"
SENTINEL_5P = "SENTINEL-5P"
SENTINEL_6 = "SENTINEL-6"
SENTINEL_1_RTC = "SENTINEL-1-RTC"
GLOBAL_MOSAICS = "GLOBAL-MOSAICS"
SMOS = "SMOS"
ENVISAT = "ENVISAT"
LANDSAT_5 = "LANDSAT-5"
LANDSAT_7 = "LANDSAT-7"
LANDSAT_8 = "LANDSAT-8"
COP_DEM = "COP-DEM"
TERRAAQUA = "TERRAAQUA"
S2GLC = "S2GLC"
CCM = "CCM"


class ODataChecksum(BaseModel):
value: Annotated[str, Field(alias="Value")]
algorithm: Annotated[str, Field(alias="Algorithm")]
checksum_date: Annotated[str, Field(alias="ChecksumDate")]


class ODataContentDate(BaseModel):
start: Annotated[str, Field(alias="Start")]
end: Annotated[str, Field(alias="End")]


class ODataProduct(BaseModel):
media_content_type: Annotated[str, Field(alias="@odata.mediaContentType")]
id: Annotated[str, Field(alias="Id")]
name: Annotated[str, Field(alias="Name")]
content_type: Annotated[str, Field(alias="ContentType")]
content_length: Annotated[int, Field(alias="ContentLength")]
origin_date: Annotated[str, Field(alias="OriginDate")]
publication_date: Annotated[str, Field(alias="PublicationDate")]
modification_date: Annotated[str, Field(alias="ModificationDate")]
online: Annotated[bool, Field(alias="Online")]
eviction_date: Annotated[str, Field(alias="EvictionDate")]
s3_path: Annotated[str, Field(alias="S3Path")]
checksum: Annotated[list[ODataChecksum], Field(alias="Checksum")]
footprint: Annotated[Geometry | None, Field(alias="Footprint")]
geo_footprint: Annotated[Geometry | None, Field(alias="GeoFootprint")]


class ODataResult(BaseModel):
odata_context: Annotated[str, Field(alias="@odata.context")]
value: list[ODataProduct]
odata_nextlink: Annotated[str, Field(alias="@odata.nextLink")]


class ODataQuery:
def __init__(
self,
collection: str | None = None,
name: str | None = None,
top: int = 20,
publication_date: tuple[datetime, datetime] | None = None,
sensing_date: tuple[datetime, datetime] | None = None,
intersect_geometry: Geometry | None = None,
):
self.collection = collection
self.name = name
self.top = top
self.publication_date = publication_date
self.sensing_date = sensing_date
self.intersect_geometry = intersect_geometry

def to_params(self) -> dict:
query = []
if self.collection:
query.append(f"Collection/Name eq '{self.collection}'")
if self.name:
query.append("Name eq '{self.name}'")
if self.publication_date:
query.append(
f"PublicationDate ge {self.publication_date[0].isoformat()} and PublicationDate le {self.publication_date[1].isoformat()}"
)
if self.sensing_date:
query.append(
f"ContentDate/Start ge {self.sensing_date[0].isoformat()} and ContentDate/Start le {self.sensing_date[1].isoformat()}"
)
if self.intersect_geometry:
query.append(
f"OData.CSC.Intersects(area=geography'SRID=4326;{self.intersect_geometry.wkt})"
)

return {
"$filter": " and ".join(query),
"$top": self.top,
}


class ODataClient:
def __init__(self, url: str):
self.url = url

def search(self, query: ODataQuery) -> Iterator[ODataProduct]:
response = httpx.get(self.url, params=query.to_params())

product_collection = ODataResult.model_validate(response.json())
yield from product_collection.value
Loading