Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ cli = [
stac_server = [
"fastapi[standard]",
"Jinja2",
"earthkit-data",
"polytope-client",
]

docs = [
Expand Down
47 changes: 47 additions & 0 deletions stac_server/POLYTOPE_AUTH_TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Polytope Authentication

The STAC server's Polytope integration collects authentication credentials directly from users through the web interface and queries the **Destination Earth** Polytope service.

## User Flow

1. Navigate through the STAC catalogue and select your data
2. At the end of the catalogue, you'll see the "Query Data with Polytope" section
3. Enter your Destination Earth Polytope credentials:
- **Email Address**: Your email address registered with Destination Earth
- **API Key**: Your Polytope API key
4. Click "Query Polytope Service" to submit your data extraction requests

## Getting Your Polytope API Key

1. Visit [https://polytope.lumi.apps.dte.destination-earth.eu](https://polytope.lumi.apps.dte.destination-earth.eu)
2. Log in with your Destination Earth credentials
3. Navigate to your profile or settings
4. Generate or copy your API key
Comment on lines +14 to +19

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading instructions, at least currently


## Technical Details

The service connects to the Destination Earth Polytope instance:
- **Address**: `polytope.lumi.apps.dte.destination-earth.eu`
- **Collection**: `destination-earth`

## Security Notes

- Credentials are sent securely with each request
- Credentials are not stored on the server
- Each user provides their own authentication
- Credentials are only logged in a masked format for debugging

## For Developers

The credentials are sent in the request body as:
```json
{
"requests": [...],
"credentials": {
"user_email": "user@ecmwf.int",
"user_key": "your_api_key"
}
}
```

The backend passes these credentials directly to `earthkit.data.from_source()` when querying Polytope.
123 changes: 123 additions & 0 deletions stac_server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ async def deprecated():

@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
index_config = {
"title": os.environ.get("TITLE", "Qubed Catalogue Browser"),
}

return templates.TemplateResponse(request, "landing.html", index_config)


@app.get("/browse", response_class=HTMLResponse)
async def browse_catalogue(request: Request):
index_config = {
"api_url": os.environ.get("API_URL", "/api/v2/"),
"title": os.environ.get("TITLE", "Qubed Catalogue Browser"),
Expand Down Expand Up @@ -147,6 +156,120 @@ async def union(
return qube.to_json()


@app.post("/api/v2/polytope/query")
async def query_polytope(
body_json=Depends(get_body_json),
):
"""
Query the Destination Earth Polytope data extraction service with MARS requests.
Expects a JSON body with:
- 'requests': array of MARS request objects
- 'credentials': object with 'user_email' and 'user_key' fields

Connects to: polytope.lumi.apps.dte.destination-earth.eu
Collection: destination-earth
"""
try:
import earthkit.data

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use the polytope-client rather than earthkit.data unless we're using some speficic functionality of it

except ImportError:
raise HTTPException(
status_code=500,
detail="earthkit.data is not installed. Please install it with 'pip install earthkit-data'",
)

requests = body_json.get("requests", [])
if not requests:
raise HTTPException(status_code=400, detail="No requests provided")

# Get credentials from request body
credentials = body_json.get("credentials", {})
user_email = credentials.get("user_email")
user_key = credentials.get("user_key")

if not user_email or not user_key:
raise HTTPException(
status_code=400,
detail="Credentials required: provide user_email and user_key",
)

# Prepare kwargs for polytope connection
polytope_kwargs = {
"stream": False,
"address": "polytope.lumi.apps.dte.destination-earth.eu",
"user_email": user_email,
"user_key": user_key,
}

logger.info(f"Querying Polytope with user email: {user_email}")

results = []
successful = 0
failed = 0

for idx, mars_request in enumerate(requests):
try:
logger.info(f"Querying Polytope for request {idx + 1}/{len(requests)}")
logger.debug(f"Request: {mars_request}")

# Query Polytope service
ds = earthkit.data.from_source(
"polytope", "destination-earth", mars_request, **polytope_kwargs
)

# Get JSON representation of the data
try:
ds_json = ds._json()
logger.info(f"Successfully extracted JSON from request {idx + 1}")
except Exception as json_error:
logger.warning(
f"Could not extract JSON from request {idx + 1}: {json_error}"
)
ds_json = None

# Get some basic info about the result
data_info = (
f"Retrieved {len(ds)} fields"
if hasattr(ds, "__len__")
else "Data retrieved"
)

result_entry = {
"success": True,
"request_index": idx,
"message": data_info,
"data_size": str(len(ds)) if hasattr(ds, "__len__") else None,
"mars_request": mars_request,
}

# Add JSON data if available
if ds_json is not None:
result_entry["json_data"] = ds_json

results.append(result_entry)
successful += 1
logger.info(f"Request {idx + 1} successful: {data_info}")

except Exception as e:
error_msg = str(e)
logger.error(f"Request {idx + 1} failed: {error_msg}")
results.append(
{
"success": False,
"request_index": idx,
"error": error_msg,
"mars_request": mars_request,
}
)
failed += 1

return {
"total": len(requests),
"successful": successful,
"failed": failed,
"results": results,
}


def follow_query(request: dict[str, str | list[str]], qube: Qube):
rel_qube = qube.select(request, consume=False)

Expand Down
Loading