Skip to content

Commit

Permalink
Merge branch 'main' into disable-multi-platform-build
Browse files Browse the repository at this point in the history
  • Loading branch information
mindflayer authored Apr 18, 2024
2 parents 160139e + 4300355 commit 3eea854
Show file tree
Hide file tree
Showing 20 changed files with 68,920 additions and 29 deletions.
27 changes: 14 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,35 @@ jobs:
packages: write
contents: read
steps:
-
name: Checkout
- name: Checkout
uses: actions/checkout@v4
-
name: Set up Docker Buildx
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to GitHub Container Registry
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Login to ECR
- name: Login to ECR
uses: docker/login-action@v3
with:
registry: ${{ vars.AWS_ECR_URI }}
username: ${{ vars.AWS_ACCESS_KEY_ID }}
password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-
name: Build and push
- name: Set up Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/stat-utils/stat-fastapi
${{ vars.AWS_ECR_URI }}/stat-utils/stat-fastapi
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/stat-utils/stat-fastapi:latest
${{ vars.AWS_ECR_URI }}/stat-utils/stat-fastapi:latest
build-args:
BUILDKIT_MULTI_PLATFORM=0
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
251 changes: 250 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ python = ">=3.10"
fastapi = "^0.110.0"
pydantic = "^2.6.4"
geojson-pydantic = "^1.0.2"
pygeofilter = "^0.2.1"


[tool.poetry.group.dev.dependencies]
Expand All @@ -32,6 +33,7 @@ mangum = "^0.17.0"

[tool.poetry.scripts]
dev = "stat_fastapi.__dev__:cli"
landsat = "stat_fastapi_landsat.__dev__:cli"

[tool.ruff]
line-length = 88
Expand Down
6 changes: 3 additions & 3 deletions stat_fastapi/models/opportunity.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
from typing import Literal
from typing import Literal, Optional

from geojson_pydantic import Feature, FeatureCollection
from geojson_pydantic.geometries import Geometry
from pydantic import BaseModel

from stat_fastapi.models.constraints import Constraints
from stat_fastapi.types.datetime_interval import DatetimeInterval
from stat_fastapi.types.filter import CQL2Filter


# Copied and modified from stack_pydantic.item.ItemProperties
class OpportunityProperties(BaseModel):
datetime: DatetimeInterval
product_id: str
constraints: Constraints
filter: Optional[CQL2Filter] = None


class OpportunitySearch(OpportunityProperties):
Expand Down
Empty file added stat_fastapi/py.typed
Empty file.
22 changes: 22 additions & 0 deletions stat_fastapi/types/filter.py
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),
],
)
34 changes: 34 additions & 0 deletions stat_fastapi_landsat/__dev__.py
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,
)
3 changes: 3 additions & 0 deletions stat_fastapi_landsat/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from stat_fastapi_landsat.backend import StatLandsatBackend

__all__ = ["StatLandsatBackend"]
171 changes: 171 additions & 0 deletions stat_fastapi_landsat/backend.py
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)
9 changes: 9 additions & 0 deletions stat_fastapi_landsat/files/README.md
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/`
Loading

0 comments on commit 3eea854

Please sign in to comment.