Unverified Commit 10bd3f3e authored by Christine Kim's avatar Christine Kim Committed by GitHub
Browse files

FastAPI app (#27)

* Added fastapi app files

* Refactor for less complicated model structure

* Added redirect from root endpoint to docs endpoint

* Add api deps to main env.yml

* Hide root endpt from schema & set searchKernels to always true

* Remove spiceql dep

* Update readme

* Update getTargetOrientations and getTargetStates endpoints
parent 2dfba0c7
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -19,6 +19,11 @@ dependencies:
  - jsonschema
  - cereal
  - spdlog
  # API dependencies
  - fastapi
  - pydantic==2.6.3
  - uvicorn
  - numpy
  - pip:
    - mkdocs 
    - mkdocs-swagger-ui-tag

fastapi/README.md

0 → 100644
+26 −0
Original line number Diff line number Diff line
# SpiceQL FastAPI App

## Create local instance

### 1. Create conda environment
Create the conda environment to run your local instance in:
```
conda env update -n spiceql-api -f environment.yaml
```

### 2. Set environment variables
Similarly to your SpiceQL conda environment, set `SPICEROOT` or `ISISDATA` to your ISIS data area. You may also need to set `SSPICE_DEBUG` to any value, like `True`.

To set an environment variable within the scope of your conda environment:
```
conda activate spiceql-api
conda env config vars set SPICEROOT=/path/to/isis_data
```

### 3. Run the app
Within the `fastapi/` dir but outside the `app/` dir, run the following command:
```
uvicorn app.main:app --reload --port 8080
```

You can access the Swagger UI of all the endpoints at http://127.0.0.1:8080/docs.
+0 −0

Empty file added.

fastapi/app/main.py

0 → 100644
+239 −0
Original line number Diff line number Diff line
"""Module providing SpiceQL endpoints"""

from ast import literal_eval
from typing import Annotated, Any
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
from starlette.responses import RedirectResponse
import numpy as np
import pyspiceql

SEARCH_KERNELS_BOOL = True

# Models
class MessageItem(BaseModel):
    message: str

class ResultModel(BaseModel):
    result: Any = Field(serialization_alias='return')

class ErrorModel(BaseModel):
    error: str

class ResponseModel(BaseModel):
    statusCode: int
    body: ResultModel | ErrorModel

# Create FastAPI instance
app = FastAPI()

# General endpoints
@app.get("/", include_in_schema=False)
async def root():
    return RedirectResponse(url="/docs")

@app.post("/customMessage")
async def message(
    message_item: MessageItem
    ):
    return {"message": message_item.message}


# SpiceQL endpoints
@app.get("/getTargetStates")
async def getTargetStates(
    target: str,
    observer: str,
    frame: str,
    abcorr: str,
    mission: str,
    ets: Annotated[list[float], Query()] | str | None = None,
    startEts: float | None = None,
    exposureDuration: float | None = None,
    numOfExposures: int | None = None,
    ckQuality: str = "",
    spkQuality: str = ""):
    try:
        if ets is not None:
            if isinstance(ets, str):
                ets = literal_eval(ets)
        else:
            if all(v is not None for v in [startEts, exposureDuration, numOfExposures]):
                stopEts = (exposureDuration * numOfExposures) + startEts
                etsNpArray = np.arange(startEts, stopEts, exposureDuration)
                ets = list(etsNpArray)
            else:
                raise Exception("Verify that a startEts, exposureDuration, and numOfExposures are being passed correctly.")
        result = pyspiceql.getTargetStates(ets, target, observer, frame, abcorr, mission, ckQuality, spkQuality, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)
    
@app.get("/getTargetOrientations")
async def getTargetOrientations(
    toFrame: int,
    refFrame: int,
    mission: str,
    ets: Annotated[list[float], Query()] | str | None = None,
    startEts: float | None = None,
    exposureDuration: float | None = None,
    numOfExposures: int | None = None,
    ckQuality: str = ""):
    try:
        if ets is not None:
            if isinstance(ets, str):
                ets = literal_eval(ets)
        else:
            if all(v is not None for v in [startEts, exposureDuration, numOfExposures]):
                stopEts = (exposureDuration * numOfExposures) + startEts
                etsNpArray = np.arange(startEts, stopEts, exposureDuration)
                ets = list(etsNpArray)
            else:
                raise Exception("Verify that a startEts, exposureDuration, and numOfExposures are being passed correctly.")
        result = pyspiceql.getTargetOrientations(ets, toFrame, refFrame, mission, ckQuality, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)  
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/strSclkToEt")
async def strSclkToEt(
    frameCode: int,
    sclk: str,
    mission: str):
    try:
        result = pyspiceql.strSclkToEt(frameCode, sclk, mission, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/doubleSclkToEt")
async def doubleSclkToEt(
    frameCode: int,
    sclk: float,
    mission: str):
    try:
        result = pyspiceql.doubleSclkToEt(frameCode, sclk, mission, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/utcToEt")
async def utcToEt(
    utc: str):
    try:
        result = pyspiceql.utcToEt(utc, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/translateNameToCode")
async def translateNameToCode(
    frame: str,
    mission: str):
    try:
        result = pyspiceql.translateNameToCode(frame, mission, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/translateCodeToName")
async def translateCodeToName(
    frame: int,
    mission: str):
    try:
        result = pyspiceql.translateCodeToName(frame, mission, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/getFrameInfo")
async def getFrameInfo(
    frame: int,
    mission: str):
    try:
        result = pyspiceql.getFrameInfo(frame, mission, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/getTargetFrameInfo")
async def getTargetFrameInfo(
    targetId: int,
    mission: str):
    try:
        result = pyspiceql.getTargetFrameInfo(targetId, mission, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/findMissionKeywords")
async def findMissionKeywords(
    key: str,
    mission: str):
    try:
        result = pyspiceql.findMissionKeywords(key, mission, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/findTargetKeywords")
async def findTargetKeywords(
    key: str,
    mission: str):
    try:
        result = pyspiceql.findTargetKeywords(key, mission, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)

@app.get("/frameTrace")
async def frameTrace(
    et: float,
    initialFrame: int,
    mission: str,
    ckQuality: str = ""):
    try:
        result = pyspiceql.frameTrace(et, initialFrame, mission, ckQuality, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)
    
@app.get("/extractExactCkTimes")
async def extractExactCkTimes(
    observStart: float,
    observEnd: float,
    targetFrame: int,
    mission: str,
    ckQuality: str = ""):
    try:
        result = pyspiceql.extractExactCkTimes(observStart, observEnd, targetFrame, mission, ckQuality, SEARCH_KERNELS_BOOL)
        body = ResultModel(result=result)
        return ResponseModel(statusCode=200, body=body)
    except Exception as e:
        body = ErrorModel(error=str(e))
        return ResponseModel(statusCode=500, body=body)
    
 No newline at end of file