Skip to content

Commit

Permalink
Improved OpenAPI docs (#8)
Browse files Browse the repository at this point in the history
* Redirect / to /docs
* Add title/description/version to app
* Add top-level dials_rest.__version__ attribute
* Add examples to the export_bitmaps endpoint
* Add examples to the find_spots endpoint
* Add tags_metadata to app
  • Loading branch information
rjgildea authored May 24, 2023
1 parent 78be3b9 commit 2bb07b7
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 20 deletions.
7 changes: 7 additions & 0 deletions src/dials_rest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from importlib.metadata import PackageNotFoundError, version

try:
__version__ = version("dials_rest")
except PackageNotFoundError:
# package is not installed
__version__ = None
58 changes: 54 additions & 4 deletions src/dials_rest/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,64 @@
"""
A RESTful API to a (limited) subset of DIALS functionality.
Authorization is currently handled through simple [JSON Web
Tokens](https://jwt.io/).
## Usage Example: Python/Requests
Given an authentication token you can use this API from python as:
```python
import pathlib
import requests
AUTHENTICATION_TOKEN = pathlib.Path("/path/to/token").read_text().strip()
auth_header = {"Authorization": f"Bearer {AUTHENTICATION_TOKEN}"}
base_url = "https://example-dials-rest.com"
# e.g. creating data collection
response = requests.post(
f"{base_url}/export_bitmaps",
headers=auth_header,
json={
# ... body schema, see API below ...
},
)
response.raise_for_status() # Check if the call was successful
```
"""

import logging

from fastapi import FastAPI
from fastapi.responses import RedirectResponse

from . import __version__
from .routers import find_spots, image
from .settings import Settings

logging.basicConfig(level=logging.INFO)


tags_metadata = [
{
"name": "images",
"description": "Generate bitmaps of diffraction images",
},
{
"name": "spotfinding",
"description": "Run spotfinding on a diffraction image and report summary statistics",
},
]


settings = Settings()
app = FastAPI()
app = FastAPI(
title="DIALS REST API",
description=__doc__,
openapi_tags=tags_metadata,
version=__version__,
)

app.include_router(find_spots.router)
app.include_router(image.router)
Expand All @@ -23,6 +73,6 @@
instrumentator.expose(app)


@app.get("/")
async def root():
return {"message": "Welcome to the DIALS REST API!"}
@app.get("/", include_in_schema=False)
def get_root():
return RedirectResponse("/docs")
67 changes: 55 additions & 12 deletions src/dials_rest/routers/find_spots.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import time
from enum import Enum
from pathlib import Path
from typing import Optional
from typing import Annotated

import pydantic
from cctbx import uctbx
Expand All @@ -11,7 +11,7 @@
from dials.command_line.find_spots import phil_scope as find_spots_phil_scope
from dials.util import phil
from dxtbx.model.experiment_list import ExperimentListFactory
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Body, Depends

from ..auth import JWTBearer

Expand All @@ -20,7 +20,7 @@

router = APIRouter(
prefix="/find_spots",
tags=["find_spots"],
tags=["spotfinding"],
dependencies=[Depends(JWTBearer())],
responses={404: {"description": "Not found"}},
)
Expand All @@ -32,11 +32,11 @@ class ThresholdAlgorithm(Enum):

class PerImageAnalysisParameters(pydantic.BaseModel):
filename: Path
d_min: Optional[pydantic.PositiveFloat] = None
d_max: Optional[pydantic.PositiveFloat] = 40
d_min: pydantic.PositiveFloat | None = None
d_max: pydantic.PositiveFloat | None = 40
threshold_algorithm: ThresholdAlgorithm = ThresholdAlgorithm.DISPERSION
disable_parallax_correction: bool = True
scan_range: Optional[tuple[int, int]] = None
scan_range: tuple[int, int] | None = None
filter_ice: bool = True
ice_rings_width: pydantic.NonNegativeFloat = 0.004

Expand All @@ -54,15 +54,58 @@ class PerImageAnalysisResults(pydantic.BaseModel):
n_spots_no_ice: pydantic.NonNegativeInt
n_spots_total: pydantic.NonNegativeInt
total_intensity: pydantic.NonNegativeFloat
d_min_distl_method_1: Optional[float] = None
d_min_distl_method_2: Optional[float] = None
estimated_d_min: Optional[float] = None
noisiness_method_1: Optional[float] = None
noisiness_method_2: Optional[float] = None
d_min_distl_method_1: float | None = None
d_min_distl_method_2: float | None = None
estimated_d_min: float | None = None
noisiness_method_1: float | None = None
noisiness_method_2: float | None = None

class Config:
schema_extra = {
"example": {
"n_spots_4A": 36,
"n_spots_no_ice": 44,
"n_spots_total": 49,
"total_intensity": 56848.0,
"d_min_distl_method_1": 4.234420130210043,
"d_min_distl_method_2": 4.053322019536269,
"estimated_d_min": 3.517157644985513,
"noisiness_method_1": 0.15019762845849802,
"noisiness_method_2": 0.46842105263157896,
}
}


find_spots_examples = {
"Single image example": {
"description": "Perform spotfinding on a single image with a high resolution cutoff of 3.5 Å",
"value": {
"filename": "/path/to/image_00001.cbf",
"d_min": 3.5,
},
},
"Image template example": {
"description": "Perform spotfinding on the second image matching the given filename template",
"value": {
"filename": "/path/to/image_#####.cbf",
"scan_range": [1, 1],
},
},
"Multi-image format example": {
"description": "Perform spotfinding on the fifth image of a NeXus file, filtering out spots at ice ring resolutions",
"value": {
"filename": "/path/to/master.h5",
"scan_range": [4, 4],
"filter_ice": True,
},
},
}


@router.post("/")
async def find_spots(params: PerImageAnalysisParameters) -> PerImageAnalysisResults:
async def find_spots(
params: Annotated[PerImageAnalysisParameters, Body(examples=find_spots_examples)]
) -> PerImageAnalysisResults:
if "#" in params.filename.stem:
experiments = ExperimentListFactory.from_templates([params.filename])
else:
Expand Down
52 changes: 48 additions & 4 deletions src/dials_rest/routers/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import time
from enum import Enum
from pathlib import Path
from typing import Annotated

import pydantic
from dials.command_line import export_bitmaps
from dxtbx.model.experiment_list import ExperimentListFactory
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Body, Depends
from fastapi.responses import FileResponse

from ..auth import JWTBearer
Expand All @@ -15,7 +16,7 @@

router = APIRouter(
prefix="/export_bitmap",
tags=["image"],
tags=["images"],
dependencies=[Depends(JWTBearer())],
responses={404: {"description": "Not found"}},
)
Expand Down Expand Up @@ -62,8 +63,51 @@ class ExportBitmapParams(pydantic.BaseModel):
resolution_rings: ResolutionRingsParams = ResolutionRingsParams()


@router.post("/")
async def image_as_bitmap(params: ExportBitmapParams):
image_as_bitmap_examples = {
"Single image example": {
"description": "Convert a cbf image to a png with binning of pixel to reduce overall image size",
"value": {
"filename": "/path/to/image_00001.cbf",
"binning": 4,
},
},
"Image template example": {
"description": "Generate a png for the second image matching the given filename template",
"value": {
"filename": "/path/to/image_#####.cbf",
"image_index": 2,
"binning": 4,
},
},
"Multi-image format example": {
"description": "Generate a png for the fifth image of a NeXus file, modifying the default colour scheme",
"value": {
"filename": "/path/to/master.h5",
"image_index": 5,
"binning": 4,
"colour_scheme": "inverse_greyscale",
},
},
"Resolution rings": {
"description": "Generate a png with resolution ring overlays",
"value": {
"filename": "/path/to/image_00001.cbf",
"binning": 2,
"resolution_rings": {"show": True, "number": 10},
},
},
}


@router.post(
"/",
status_code=200,
response_class=FileResponse,
responses={200: {"description": "Asynchronously streams the file as the response"}},
)
async def image_as_bitmap(
params: Annotated[ExportBitmapParams, Body(examples=image_as_bitmap_examples)]
) -> FileResponse:
if "#" in params.filename.stem:
# A filename template e.g. image_#####.cbf
expts = ExperimentListFactory.from_templates([params.filename])
Expand Down

0 comments on commit 2bb07b7

Please sign in to comment.