Skip to content

Commit e65d6fa

Browse files
committed
add function to auto_update stac api item
1 parent 732b8e0 commit e65d6fa

File tree

4 files changed

+265
-2
lines changed

4 files changed

+265
-2
lines changed

external_caller.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
warnings.filterwarnings("ignore")
1212

13+
1314
from papipyplug import parse_input, plugin_logger, print_results
1415

1516
PLUGIN_PARAMS = {"required": ["in_prefix", "crs", "out_prefix"], "optional": []}

ras_stac/ras1d/converter.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pystac.item import Item
1313
from shapely import to_geojson
1414

15+
from ras_stac.ras1d.update_stac_api import post_item
1516
from ras_stac.ras1d.utils.classes import (
1617
GenericAsset,
1718
GeometryAsset,
@@ -365,6 +366,7 @@ def append_geopackage(in_prefix: str, crs: str, out_prefix: str):
365366
stac_item["assets"]["GeoPackage_file"] = gpkg_asset.to_stac().to_dict()
366367
out_obj = json.dumps(stac_item).encode()
367368
save_bytes_s3(out_obj, stac_path)
369+
post_item(stac_item)
368370
return {"in_path": in_prefix, "crs": crs, "thumb_path": None, "stac_path": stac_path}
369371

370372

@@ -375,5 +377,5 @@ def append_geopackage(in_prefix: str, crs: str, out_prefix: str):
375377
crs = None
376378
out_dir = ras_dir.replace("source_models", "stac_items")
377379
# process_in_place_s3(ras_dir, crs, out_dir)
378-
ras_to_stac(ras_dir, crs)
379-
# append_geopackage(ras_dir, crs, out_dir)
380+
# ras_to_stac(ras_dir, crs)
381+
append_geopackage(ras_dir, crs, out_dir)

ras_stac/ras1d/headers.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Manage Headers."""
2+
3+
import json
4+
import logging
5+
import os
6+
7+
import requests
8+
from dotenv import load_dotenv
9+
10+
11+
def get_auth_header():
12+
load_dotenv()
13+
"""Get auth header for a given user."""
14+
auth_server = os.getenv("AUTH_ISSUER")
15+
client_id = os.getenv("AUTH_ID")
16+
client_secret = os.getenv("AUTH_SECRET")
17+
18+
username = os.getenv("AUTH_USER")
19+
password = os.getenv("AUTH_USER_PASSWORD")
20+
21+
auth_payload = f"username={username}&password={password}&client_id={client_id}&grant_type=password&client_secret={client_secret}"
22+
headers = {
23+
"Content-Type": "application/x-www-form-urlencoded",
24+
"Authorization": "Bearer null",
25+
}
26+
27+
auth_response = requests.request("POST", auth_server, headers=headers, data=auth_payload)
28+
29+
try:
30+
token = json.loads(auth_response.text)["access_token"]
31+
except KeyError:
32+
logging.debug(auth_response.text)
33+
raise KeyError
34+
35+
headers = {
36+
"Authorization": f"Bearer {token}",
37+
"Content-Type": "application/json",
38+
"X-ProcessAPI-User-Email": username,
39+
}
40+
41+
return headers

ras_stac/ras1d/update_stac_api.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import json
2+
import logging
3+
import re
4+
5+
import pystac_client
6+
import requests
7+
from pystac import TemporalExtent
8+
9+
from ras_stac.ras1d.headers import get_auth_header
10+
11+
12+
class DewStacClient:
13+
"""pystac_client with some Dewberry-specific features"""
14+
15+
def __init__(self, stac_endpoint: str = "https://stac2.dewberryanalytics.com") -> None:
16+
self.stac_endpoint = stac_endpoint
17+
self.stac_client = pystac_client.Client.open(self.stac_endpoint)
18+
19+
self.collection_url = "{}/collections/{}"
20+
self.item_url = "{}/collections/{}/items/{}"
21+
22+
def __getattr__(self, name):
23+
"""Delegate undefined methods to the underlying stac_client"""
24+
return getattr(self.stac_client, name)
25+
26+
def add_collection_summary(self, stac_collection_id: str) -> None:
27+
"""Update STAC collections to include item summaries."""
28+
29+
version_summary = {}
30+
coverage_summary = {
31+
"1D_HEC-RAS_models": 0,
32+
"1D_HEC-RAS_river_miles": 0,
33+
}
34+
collection = self.get_collection(stac_collection_id)
35+
daterange = []
36+
for item in collection.get_all_items():
37+
if item.properties["river_miles"] == "None":
38+
river_miles = 0
39+
else:
40+
river_miles = float(item.properties["river_miles"])
41+
coverage_summary["1D_HEC-RAS_river_miles"] += river_miles
42+
coverage_summary["1D_HEC-RAS_models"] += 1
43+
44+
ras_version = item.properties.get("ras_version", None)
45+
46+
if ras_version not in version_summary:
47+
version_summary[ras_version] = {"river_miles": 0, "ras_models": 0}
48+
49+
version_summary[ras_version]["river_miles"] += river_miles
50+
version_summary[ras_version]["ras_models"] += 1
51+
if item.properties["datetime_source"] == "model_geometry":
52+
daterange.append(item.datetime)
53+
54+
collection.extent.spatial = collection.extent.from_items(collection.get_all_items()).spatial
55+
if len(daterange) > 0:
56+
collection.extent.temporal = TemporalExtent(intervals=[[min(daterange), max(daterange)]])
57+
temp_notes = {"Temporal extent": "Timestamps reflect data scraped from HEC-RAS model files"}
58+
else:
59+
temp_notes = {
60+
"Temporal extent": "Timestamps not available in HEC-RAS model files, temporal extent is limited to data upload / processing date"
61+
}
62+
63+
coverage_summary["1D_HEC-RAS_river_miles"] = int(coverage_summary["1D_HEC-RAS_river_miles"])
64+
65+
for ras_version in version_summary.keys():
66+
version_summary[ras_version]["river_miles"] = int(version_summary[ras_version]["river_miles"])
67+
68+
collection.summaries.add("coverage", coverage_summary)
69+
collection.summaries.add("model-versions", version_summary)
70+
collection.summaries.add("notes", temp_notes)
71+
72+
header = get_auth_header()
73+
response = requests.put(
74+
self.collection_url.format(self.stac_endpoint, stac_collection_id),
75+
json=collection.to_dict(),
76+
headers=header,
77+
)
78+
response.raise_for_status
79+
80+
def add_all_summaries(self, overwrite: bool = False, re_filter: re.Pattern = re.compile("(.*?)")) -> list:
81+
"""Add summaries to all collections of the stac endpoint"""
82+
collections = self.stac_client.get_collections()
83+
collections = [c for c in collections if re_filter.fullmatch(c.id) is not None]
84+
85+
for c in collections:
86+
collection = self.get_collection(c.id)
87+
summary = collection.summaries
88+
89+
# If schema exists and not overwriting, go to next
90+
if len(summary.schemas) > 0 and not overwrite:
91+
continue
92+
# Otherwise, add summary
93+
try:
94+
print(f"Found missing summary for {c.id}")
95+
self.add_collection_summary(c.id)
96+
except requests.exceptions.HTTPError as e:
97+
print(f"Error uploading summary for {c.id}")
98+
print(e)
99+
100+
def export_to_json(self, out_path: str, catalog_filter: re.Pattern = re.compile("(.*?)")):
101+
"""Save all catalogs and items to a local json"""
102+
collections = self.stac_client.get_collections()
103+
collections = [c for c in collections if catalog_filter.fullmatch(c.id) is not None]
104+
out_dict = {}
105+
106+
for c in collections:
107+
collection = self.get_collection(c.id)
108+
out_dict[c.id] = [i.id for i in collection.get_all_items()]
109+
110+
with open(out_path, "w") as out_file:
111+
json.dump(out_dict, out_file, indent=4)
112+
113+
def add_collection(self, collection_id: str) -> None:
114+
"""Create new collection at endpoint"""
115+
header = get_auth_header()
116+
# check if exists first
117+
response = requests.get(self.collection_url.format(self.stac_endpoint, collection_id))
118+
if response.status_code != 404:
119+
return
120+
121+
response = requests.post(
122+
self.collection_url.format(self.stac_endpoint, ""),
123+
json={
124+
"id": collection_id,
125+
"description": f"HEC-RAS models for {collection_id}",
126+
"stac_version": "1.0.0",
127+
"links": [],
128+
"title": collection_id,
129+
"type": "Collection",
130+
"license": "proprietary",
131+
"extent": {
132+
"spatial": {"bbox": [[-180.0, -90.0, 180.0, 90.0]]},
133+
"temporal": {"interval": [[None, None]]},
134+
},
135+
},
136+
headers=header,
137+
)
138+
response.raise_for_status()
139+
140+
def add_collection_item(self, stac_collection_id: str, item_id: str, item: dict) -> None:
141+
header = get_auth_header()
142+
response = requests.post(
143+
self.item_url.format(self.stac_endpoint, stac_collection_id, ""),
144+
json=item,
145+
headers=header,
146+
)
147+
response.raise_for_status()
148+
149+
def remove_collection_item(self, stac_collection_id: str, item_id: str) -> None:
150+
header = get_auth_header()
151+
response = requests.delete(
152+
self.item_url.format(self.stac_endpoint, stac_collection_id, item_id),
153+
headers=header,
154+
)
155+
response.raise_for_status()
156+
157+
def remove_collection(self, stac_collection_id: str) -> None:
158+
header = get_auth_header()
159+
response = requests.delete(
160+
self.collection_url.format(self.stac_endpoint, stac_collection_id),
161+
headers=header,
162+
)
163+
response.raise_for_status()
164+
165+
def update_item(self, collection_id, item_id, stac):
166+
header = get_auth_header()
167+
response = requests.put(
168+
self.item_url.format(self.stac_endpoint, collection_id, item_id), headers=header, json=stac
169+
)
170+
response.raise_for_status()
171+
172+
173+
def post_item(stac):
174+
if "proj:wkt2" not in stac["properties"]: # no CRS
175+
if not stac["properties"]["has_1d"]: # 2D
176+
collection = "mip_2D_only"
177+
else:
178+
collection = "mip_no_crs"
179+
else:
180+
collection = "mip_" + stac["properties"]["assigned_HUC8"]
181+
182+
client = DewStacClient()
183+
collection_ref = client.get_collection(collection)
184+
update_stac_item(stac, collection_ref, client)
185+
186+
187+
def update_stac_item(stac, collection, client):
188+
# Check if item already exists
189+
stac["id"] = stac["id"].replace("(", "_").replace(")", "_")
190+
stac_id = stac["id"]
191+
api_item = collection.get_item(stac_id)
192+
193+
ind = 0
194+
searching = True
195+
candidate_hash_set = {
196+
f'{stac["assets"][i]["title"]} {stac["assets"][i]["e_tag"]}'
197+
for i in stac["assets"]
198+
if "thumbnail" not in stac["assets"][i]["roles"] and "ras-geometry-gpkg" not in stac["assets"][i]["roles"]
199+
}
200+
while searching:
201+
api_hash_set = {
202+
f'{api_item.assets[i].title} {api_item.assets[i].extra_fields["e_tag"]}'
203+
for i in api_item.assets
204+
if "thumbnail" not in api_item.assets[i].roles and "ras-geometry-gpkg" not in api_item.assets[i].roles
205+
}
206+
if candidate_hash_set == api_hash_set:
207+
searching = False
208+
client.update_item(collection.id, stac["id"], stac)
209+
logging.info(f"updated STAC item {stac["id"]} in collection {collection.id}")
210+
continue
211+
212+
ind += 1
213+
stac["id"] = stac_id + f"_{ind}"
214+
api_item = collection.get_item(stac["id"])
215+
if api_item is None:
216+
searching = False
217+
logging.warning("No STAC item found in API to update")
218+
219+
return stac_id

0 commit comments

Comments
 (0)