diff --git a/stac_server/main.py b/stac_server/main.py index 89f4225..f4e29c7 100644 --- a/stac_server/main.py +++ b/stac_server/main.py @@ -11,6 +11,10 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from frozendict import frozendict from qubed import Qube from qubed.tree_formatters import node_tree_to_html +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from fastapi.responses import HTMLResponse +import json app = FastAPI() security = HTTPBearer() @@ -22,6 +26,9 @@ app.add_middleware( allow_headers=["*"], ) +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") + qubes: dict[str, Qube] = {} # print("Getting climate and extremes dt data from github") # try: @@ -46,10 +53,32 @@ qubes["extremes-dt"] = Qube.empty() mars_language = {} if "LOCAL_CACHE" in os.environ: - base = Path(os.environ["LOCAL_CACHE"]) + print("Getting climate and extremes dt data from local files") + with open("../tests/example_qubes/climate_dt.json") as f: + qubes["climate-dt"] = Qube.from_json(json.load(f)) - with open(base / "language.yaml", "r") as f: + with open("../tests/example_qubes/extremes_dt.json") as f: + qubes["climate-dt"] = qubes["climate-dt"] | Qube.from_json(json.load(f)) + + with open("../config/climate-dt/language.yaml", "r") as f: mars_language = yaml.safe_load(f)["_field"] +else: + print("Getting climate and extremes dt data from github") + qubes["climate-dt"] = Qube.from_json( + requests.get( + "https://github.com/ecmwf/qubed/raw/refs/heads/main/tests/example_qubes/climate_dt.json", + timeout=1).json() + ) + qubes["extremes-dt"] = Qube.from_json( + requests.get( + "https://github.com/ecmwf/qubed/raw/refs/heads/main/tests/example_qubes/extremes_dt.json", + timeout=1).json() + ) + mars_language = yaml.safe_load( + requests.get( + "https://github.com/ecmwf/qubed/raw/refs/heads/main/config/climate-dt/language.yaml", + timeout=1).content + ) if "API_KEY" in os.environ: api_key = os.environ["API_KEY"] @@ -91,6 +120,15 @@ def validate_api_key(credentials: HTTPAuthorizationCredentials = Depends(securit async def favicon(): return FileResponse("favicon.ico") +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + return templates.TemplateResponse("index.html", {"request": request, "config": { + "message": "Hello from the dev server!", + }, + "api_url": "/api/v1/stac/climate-dt", + "request" : request, + }) + @app.get("/api/v1/keys/") async def keys(): @@ -137,6 +175,13 @@ def follow_query(request: dict[str, str | list[str]], qube: Qube): for key, v in by_path.items() ] +@app.get("/api/v1/select/{key}/") +async def get( + key: str = Depends(validate_key), + request: dict[str, str | list[str]] = Depends(parse_request), +): + q = qubes[key].select(request) + return q.to_json() @app.get("/api/v1/query/{key}") async def query( diff --git a/stac_server/run.sh b/stac_server/run.sh index 734e0ec..05d670a 100755 --- a/stac_server/run.sh +++ b/stac_server/run.sh @@ -1,3 +1,3 @@ parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) cd "$parent_path" -LOCAL_CACHE=../config/climate-dt fastapi dev ./main.py --port 8124 --reload +LOCAL_CACHE=True fastapi dev ./main.py --port 8124 --reload diff --git a/stac_server/run_prod.sh b/stac_server/run_prod.sh new file mode 100755 index 0000000..e91f4b8 --- /dev/null +++ b/stac_server/run_prod.sh @@ -0,0 +1,3 @@ +parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) +cd "$parent_path" +sudo LOCAL_CACHE=True ../../.venv/bin/fastapi dev ./main.py --port 80 --host=0.0.0.0 --reload diff --git a/stac_server/static/app.js b/stac_server/static/app.js new file mode 100644 index 0000000..87758f8 --- /dev/null +++ b/stac_server/static/app.js @@ -0,0 +1,315 @@ +// app.js + +// Take the query string and stick it on the API URL +function getSTACUrlFromQuery() { + const params = new URLSearchParams(window.location.search); + + // get current window url and remove path part + if (window.API_URL.startsWith("http")) { + // Absolute URL: Use it directly + api_url = new URL(window.API_URL); + } else { + // Relative URL: Combine with the current window's location + api_url = new URL(window.location.href); + api_url.pathname = window.API_URL; + } + + for (const [key, value] of params.entries()) { + api_url.searchParams.set(key, value); + } + + console.log(api_url.toString()); + return api_url.toString(); +} + +function get_request_from_url() { + // Extract the query params in order and split any with a , delimiter + // request is an ordered array of [key, [value1, value2, value3, ...]] + const url = new URL(window.location.href); + const params = new URLSearchParams(url.search); + const request = []; + for (const [key, value] of params.entries()) { + request.push([key, value.split(",")]); + } + return request; +} + +function make_url_from_request(request) { + const url = new URL(window.location.href); + url.search = ""; // Clear existing params + const params = new URLSearchParams(); + + for (const [key, values] of request) { + params.set(key, values.join(",")); + } + url.search = params.toString(); + + return url.toString().replace(/%2C/g, ","); +} + +function goToPreviousUrl() { + let request = get_request_from_url(); + request.pop(); + console.log("Request:", request); + const url = make_url_from_request(request); + console.log("URL:", url); + window.location.href = make_url_from_request(request); +} + +// Function to generate a new STAC URL based on current selection +function goToNextUrl() { + const request = get_request_from_url(); + + // Get the currently selected key = value,value2,value3 pairs + const items = Array.from(document.querySelectorAll("div#items > div")); + + let any_new_keys = false; + const new_keys = items.map((item) => { + const key = item.dataset.key; + const key_type = item.dataset.keyType; + let values = []; + + const datePicker = item.querySelector("input[type='date']"); + if (datePicker) { + values.push(datePicker.value.replace(/-/g, "")); + } + + const timePicker = item.querySelector("input[type='time']"); + if (timePicker) { + values.push(timePicker.value.replace(":", "")); + } + + const enum_checkboxes = item.querySelectorAll( + "input[type='checkbox']:checked" + ); + if (enum_checkboxes.length > 0) { + values.push( + ...Array.from(enum_checkboxes).map((checkbox) => checkbox.value) + ); + } + + const any = item.querySelector("input[type='text']"); + if (any && any.value !== "") { + values.push(any.value); + } + + // Keep track of whether any new keys are selected + if (values.length > 0) { + any_new_keys = true; + } + + return { key, values }; + }); + + // if not new keys are selected, do nothing + if (!any_new_keys) { + return; + } + + // Update the request with the new keys + for (const { key, values } of new_keys) { + // Find the index of the existing key in the request array + const existingIndex = request.findIndex( + ([existingKey, existingValues]) => existingKey === key + ); + + if (existingIndex !== -1) { + // If the key already exists, + // and the values aren't already in there, + // append the values + request[existingIndex][1] = [...request[existingIndex][1], ...values]; + } else { + // If the key doesn't exist, add a new entry + request.push([key, values]); + } + } + + const url = make_url_from_request(request); + window.location.href = url; +} + +async function createCatalogItem(link, itemsContainer) { + const itemDiv = document.createElement("div"); + itemDiv.className = "item loading"; + itemDiv.textContent = "Loading..."; + itemsContainer.appendChild(itemDiv); + + try { + // Update the item div with real content + itemDiv.classList.remove("loading"); + + const variables = link["variables"]; + const key = Object.keys(variables)[0]; + const variable = variables[key]; + + // add data-key attribute to the itemDiv + itemDiv.dataset.key = link.title; + itemDiv.dataset.keyType = variable.type; + + itemDiv.innerHTML = ` +
Key Type: ${itemDiv.dataset.keyType || "Unknown"}
+${ + variable.description ? variable.description.slice(0, 100) : "" + }
+ `; + + if (variable.enum && variable.enum.length > 0) { + const listContainer = renderCheckboxList(link); + itemDiv.appendChild(listContainer); + } else { + const any = ``; + const anyNode = document.createRange().createContextualFragment(any); + itemDiv.appendChild(anyNode); + } + } catch (error) { + console.error("Error loading item data:", error); + itemDiv.innerHTML = `Error loading item details: ${error}
`; + } +} + +function renderCheckboxList(link) { + const variables = link["variables"]; + const key = Object.keys(variables)[0]; + const variable = variables[key]; + const value_descriptions = variable.value_descriptions || []; + + const listContainerHTML = ` +{{ config.get('message', '')}}
+Select one or multiple items and then click next to iteratively build up a full request.
+Last database update:
+ + +
+{
+}
+
+
+
+
+# pip install qubed requests
+import requests
+from qubed import Qube
+qube = Qube.from_json(requests.get("http://stac-catalogs.ecmwf-development.f.ewcloud.host/api/v1/select/climate-dt?{{request.url.query}}").json())
+qube.print()
+
+
+