Skip to content

Commit

Permalink
FastAPI app (#27)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
chkim-usgs authored Apr 29, 2024
1 parent 2dfba0c commit 10bd3f3
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 0 deletions.
5 changes: 5 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ dependencies:
- jsonschema
- cereal
- spdlog
# API dependencies
- fastapi
- pydantic==2.6.3
- uvicorn
- numpy
- pip:
- mkdocs
- mkdocs-swagger-ui-tag
Expand Down
26 changes: 26 additions & 0 deletions fastapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 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.
Empty file added fastapi/app/__init__.py
Empty file.
239 changes: 239 additions & 0 deletions fastapi/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
"""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)

0 comments on commit 10bd3f3

Please sign in to comment.