-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into disable-multi-platform-build
- Loading branch information
Showing
20 changed files
with
68,920 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
from typing import Annotated, TypeAliasType | ||
|
||
from pydantic import BeforeValidator | ||
from pygeofilter.parsers import cql2_json | ||
|
||
|
||
def validate(v: dict): | ||
if v: | ||
try: | ||
cql2_json.parse({"filter": v}) | ||
except Exception as e: | ||
raise ValueError("Filter is not valid cql2-json") from e | ||
return v | ||
|
||
|
||
CQL2Filter = TypeAliasType( | ||
"CQL2Filter", | ||
Annotated[ | ||
dict, | ||
BeforeValidator(validate), | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
#!/usr/bin/env python3 | ||
|
||
from sys import stderr | ||
|
||
from fastapi import FastAPI | ||
|
||
try: | ||
from pydantic_settings import BaseSettings | ||
from uvicorn.main import run | ||
except ImportError: | ||
print("install uvicorn and pydantic-settings to use the dev server", file=stderr) | ||
exit(1) | ||
|
||
from stat_fastapi.api import StatApiRouter | ||
from stat_fastapi_landsat import StatLandsatBackend | ||
|
||
|
||
class DevSettings(BaseSettings): | ||
port: int = 8000 | ||
host: str = "127.0.0.1" | ||
|
||
|
||
app = FastAPI(debug=True) | ||
app.include_router(StatApiRouter(backend=StatLandsatBackend()).router) | ||
|
||
|
||
def cli(): | ||
settings = DevSettings() | ||
run( | ||
"stat_fastapi_landsat.__dev__:app", | ||
reload=True, | ||
host=settings.host, | ||
port=settings.port, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from stat_fastapi_landsat.backend import StatLandsatBackend | ||
|
||
__all__ = ["StatLandsatBackend"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
import json | ||
from datetime import datetime | ||
from typing import cast | ||
|
||
import pytz | ||
from fastapi import Request | ||
from pydantic import ( | ||
BaseModel, | ||
ConfigDict, | ||
) | ||
from shapely.geometry import shape | ||
|
||
from stat_fastapi.exceptions import NotFoundException | ||
from stat_fastapi.models.opportunity import ( | ||
Opportunity, | ||
OpportunityProperties, | ||
OpportunitySearch, | ||
) | ||
from stat_fastapi.models.order import Order | ||
from stat_fastapi.models.product import Product, Provider, ProviderRole | ||
|
||
|
||
class Constraints(BaseModel): | ||
model_config = ConfigDict(extra="forbid") | ||
|
||
|
||
PRODUCTS = [ | ||
Product( | ||
id="landsat:8", | ||
description="Landsat 8", | ||
license="CC0-1.0", | ||
providers=[ | ||
Provider( | ||
name="ACME", | ||
roles=[ | ||
ProviderRole.licensor, | ||
ProviderRole.producer, | ||
ProviderRole.processor, | ||
ProviderRole.host, | ||
], | ||
url="http://acme.example.com", | ||
) | ||
], | ||
constraints=Constraints, | ||
links=[], | ||
), | ||
Product( | ||
id="landsat:9", | ||
description="Landsat 9", | ||
license="CC0-1.0", | ||
providers=[ | ||
Provider( | ||
name="ACME", | ||
roles=[ | ||
ProviderRole.licensor, | ||
ProviderRole.producer, | ||
ProviderRole.processor, | ||
ProviderRole.host, | ||
], | ||
url="http://acme.example.com", | ||
) | ||
], | ||
constraints=Constraints, | ||
links=[], | ||
), | ||
] | ||
|
||
|
||
class StatLandsatBackend: | ||
def __init__(self): | ||
self.wrs = { | ||
"ascending": self._load_json( | ||
"stat_fastapi_landsat/files/wrs2ascending.geojson" | ||
), | ||
"descending": self._load_json( | ||
"stat_fastapi_landsat/files/wrs2descending.geojson" | ||
), | ||
} | ||
self.satellite = self._load_json("stat_fastapi_landsat/files/satellites.json") | ||
|
||
def products(self, request: Request) -> list[Product]: | ||
""" | ||
Return a list of supported products. | ||
""" | ||
return PRODUCTS | ||
|
||
def product(self, product_id: str, request: Request) -> Product | None: | ||
""" | ||
Return the product identified by `product_id` or `None` if it isn't | ||
supported. | ||
""" | ||
try: | ||
return next((product for product in PRODUCTS if product.id == product_id)) | ||
except StopIteration as exc: | ||
raise NotFoundException() from exc | ||
|
||
async def search_opportunities( | ||
self, search: OpportunitySearch, request: Request | ||
) -> list[Opportunity]: | ||
""" | ||
Search for ordering opportunities for the given search parameters. | ||
""" | ||
opportunities = [] | ||
|
||
dt_start = cast(tuple, search.datetime)[0] | ||
dt_end = cast(tuple, search.datetime)[1] | ||
|
||
poi = shape(search.geometry) | ||
cell_numbers = set(self._find_wrs_cell(poi, self.wrs["ascending"])).union( | ||
set(self._find_wrs_cell(poi, self.wrs["descending"])) | ||
) | ||
|
||
if search.product_id == "landsat:8": | ||
satellite_id = "landsat_8" | ||
elif search.product_id == "landsat:9": | ||
satellite_id = "landsat_9" | ||
else: | ||
raise NotFoundException() | ||
|
||
if satellite_id in self.satellite: | ||
for date_str in self.satellite[satellite_id]: | ||
current_date = datetime.strptime(date_str, "%m/%d/%Y") | ||
current_date = current_date.astimezone(pytz.utc) | ||
|
||
if dt_start <= current_date <= dt_end: | ||
path_list = [ | ||
int(path) | ||
for path in self.satellite[satellite_id][date_str][ | ||
"path" | ||
].split(",") | ||
] | ||
|
||
# Check if any path from the satellite data matches the paths from the WRS-2 files | ||
for path_of_interest, row, geometry in cell_numbers: | ||
if path_of_interest in path_list: | ||
opportunities.append( | ||
Opportunity( | ||
geometry=geometry, | ||
properties=OpportunityProperties( | ||
product_id=search.product_id, | ||
datetime=[current_date, current_date], | ||
filter=search.filter, | ||
), | ||
) | ||
) | ||
|
||
return opportunities | ||
|
||
async def create_order(self, search: OpportunitySearch, request: Request) -> Order: | ||
""" | ||
Create a new order. | ||
""" | ||
raise NotImplementedError() | ||
|
||
async def get_order(self, order_id: str, request: Request): | ||
""" | ||
Show details for order with `order_id`. | ||
""" | ||
raise NotImplementedError() | ||
|
||
def _load_json(self, file_path): | ||
with open(file_path) as f: | ||
return json.load(f) | ||
|
||
def _find_wrs_cell(self, poi, geojson_data): | ||
for feature in geojson_data["features"]: | ||
if shape(feature["geometry"]).contains(poi): | ||
path = feature["properties"]["PATH"] | ||
row = feature["properties"]["ROW"] | ||
geometry = shape(feature["geometry"]) | ||
yield (path, row, geometry) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Provided data Landsat | ||
|
||
### Landsat Acquisition | ||
|
||
In `satellites.json` (from `https://landsat.usgs.gov/landsat_acq`). Information about when and where Landsat 8 and 9 satellites will acquire data. The area is specified by path and row numbers that correspond to the Landsat World Reference System (WRS2) | ||
|
||
### Landsat World Reference System (WRS2) | ||
|
||
In `wrs2Pascending.geojson` and `wrs2Pdescending.geojson` (from `https://www.usgs.gov/landsat-missions/landsat-shapefiles-and-kml-files`). The WRS2 describes the coordinates of each of the areas from the WRS2. More information: `https://landsat.gsfc.nasa.gov/about/the-worldwide-reference-system/` |
Oops, something went wrong.