Skip to content

Commit

Permalink
docs(Http-Responses-and-examples): adds examples for 200, 401, 403, 4…
Browse files Browse the repository at this point in the history
…04, 429 & 500 responses to endpoints
  • Loading branch information
nifedara committed Aug 21, 2024
1 parent 612426a commit 194e694
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 48 deletions.
20 changes: 13 additions & 7 deletions API/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
from typing import Union

from fastapi.security import APIKeyHeader
from fastapi import Depends, Header, HTTPException
from fastapi import Depends, HTTPException
from osm_login_python.core import Auth
from pydantic import BaseModel, Field

from src.app import Users
from src.config import get_oauth_credentials

API_Access_Token = APIKeyHeader(
name="Access_Token", description="Access Token to Authorize User"
name="Access_Token", description="Access Token to Authorize User", auto_error=False
)


Expand All @@ -26,6 +26,16 @@ class AuthUser(BaseModel):
img_url: Union[str, None]
role: UserRole = Field(default=UserRole.GUEST.value)

class Config:
json_schema_extra = {
"example": {
"id": "123",
"username": "HOT Team",
"img_url": "https://hotosm/image.jpg",
"role": UserRole.GUEST.value,
}
}


osm_auth = Auth(*get_oauth_credentials())

Expand All @@ -52,11 +62,7 @@ def login_required(access_token: str = Depends(API_Access_Token)):
return get_osm_auth_user(access_token)


def get_optional_user(
access_token: str = Header(
default=None, description="Access Token to Authorize User"
),
) -> AuthUser:
def get_optional_user(access_token: str | None = Depends(API_Access_Token)) -> AuthUser:
if access_token:
return get_osm_auth_user(access_token)
else:
Expand Down
77 changes: 69 additions & 8 deletions API/auth/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,29 @@
from pydantic import BaseModel

from src.app import Users
from src.validation.models import ErrorMessage, common_responses

from . import AuthUser, admin_required, login_required, osm_auth, staff_required

router = APIRouter(prefix="/auth", tags=["Auth"])


@router.get("/login")
@router.get(
"/login",
responses={
200: {
"description": "A Login URL",
"content": {
"application/json": {
"example": {
"login_url": "https://www.openstreetmap.org/oauth2/authorize/..."
}
}
},
},
500: {"model": ErrorMessage},
},
)
def login_url(request: Request):
"""
Generate Login URL for authentication using OAuth2 Application registered with OpenStreetMap.
Expand All @@ -24,7 +40,7 @@ def login_url(request: Request):
return login_url


@router.get("/callback")
@router.get("/callback", responses={500: {"model": ErrorMessage}})
def callback(request: Request):
"""Performs token exchange between OpenStreetMap and Raw Data API
Expand All @@ -41,7 +57,12 @@ def callback(request: Request):
return access_token


@router.get("/me", response_model=AuthUser, response_description="User's Information")
@router.get(
"/me",
response_model=AuthUser,
responses={**common_responses},
response_description="User Information",
)
def my_data(user_data: AuthUser = Depends(login_required)):
"""Read the access token and provide user details from OSM user's API endpoint,
also integrated with underpass .
Expand All @@ -65,9 +86,19 @@ class User(BaseModel):
osm_id: int
role: int

class Config:
json_schema_extra = {"example": {"osm_id": 123, "role": 1}}


# Create user
@router.post("/users", response_model=dict)
@router.post(
"/users",
response_model=dict,
responses={
**common_responses,
"200": {"content": {"application/json": {"example": {"osm_id": 123}}}},
},
)
async def create_user(params: User, user_data: AuthUser = Depends(admin_required)):
"""
Creates a new user and returns the user's information.
Expand All @@ -91,7 +122,14 @@ async def create_user(params: User, user_data: AuthUser = Depends(admin_required


# Read user by osm_id
@router.get("/users/{osm_id}", response_model=dict)
@router.get(
"/users/{osm_id}",
responses={
**common_responses,
"200": {"content": {"application/json": {"example": {"osm_id": 1, "role": 2}}}},
"404": {"model": ErrorMessage},
},
)
async def read_user(
osm_id: int = Path(description="The OSM ID of the User to Retrieve"),
user_data: AuthUser = Depends(staff_required),
Expand Down Expand Up @@ -120,7 +158,14 @@ async def read_user(


# Update user by osm_id
@router.put("/users/{osm_id}", response_model=dict)
@router.put(
"/users/{osm_id}",
responses={
**common_responses,
"200": {"content": {"application/json": {"example": {"osm_id": 1, "role": 1}}}},
"404": {"model": ErrorMessage},
},
)
async def update_user(
update_data: User,
user_data: AuthUser = Depends(admin_required),
Expand Down Expand Up @@ -149,7 +194,14 @@ async def update_user(


# Delete user by osm_id
@router.delete("/users/{osm_id}", response_model=dict)
@router.delete(
"/users/{osm_id}",
responses={
**common_responses,
"200": {"content": {"application/json": {"example": {"osm_id": 1, "role": 1}}}},
"404": {"model": ErrorMessage},
},
)
async def delete_user(
user_data: AuthUser = Depends(admin_required),
osm_id: int = Path(description="The OSM ID of the User to Delete"),
Expand All @@ -173,7 +225,16 @@ async def delete_user(


# Get all users
@router.get("/users", response_model=list)
@router.get(
"/users",
response_model=list,
responses={
**common_responses,
"200": {
"content": {"application/json": {"example": [{"osm_id": 1, "role": 2}]}}
},
},
)
async def read_users(
skip: int = Query(0, description="The Number of Users to Skip"),
limit: int = Query(10, description="The Maximum Number of Users to Retrieve"),
Expand Down
29 changes: 25 additions & 4 deletions API/custom_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,34 @@
from src.config import DEFAULT_QUEUE_NAME
from src.config import LIMITER as limiter
from src.config import RATE_LIMIT_PER_MIN
from src.validation.models import CustomRequestsYaml, DynamicCategoriesModel
from src.validation.models import (
CustomRequestsYaml,
DynamicCategoriesModel,
common_responses,
)

from .api_worker import process_custom_request
from .auth import AuthUser, UserRole, staff_required

router = APIRouter(prefix="/custom", tags=["Custom Exports"])


@router.post("/snapshot")
@router.post(
"/snapshot",
responses={
**common_responses,
"200": {
"content": {
"application/json": {
"example": {
"task_id": "3fded368-456f-4ef4-a1b8-c099a7f77ca4",
"track_link": "/tasks/status/3fded368-456f-4ef4-a1b8-c099a7f77ca4/",
}
}
}
},
},
)
@limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute")
@version(1)
async def process_custom_requests(
Expand Down Expand Up @@ -845,11 +864,13 @@ async def process_custom_requests_yaml(
try:
data = yaml.safe_load(raw_body)
except yaml.YAMLError:
raise HTTPException(status_code=422, detail="Invalid YAML")
raise HTTPException(status_code=422, detail=[{"msg": "Invalid YAML"}])
try:
validated_data = DynamicCategoriesModel.model_validate(data)
except ValidationError as e:
raise HTTPException(status_code=422, detail=e.errors(include_url=False))
raise HTTPException(
status_code=422, detail=[{"msg": e.errors(include_url=False)}]
)

queue_name = validated_data.queue
if validated_data.queue != DEFAULT_QUEUE_NAME and user.role != UserRole.ADMIN.value:
Expand Down
43 changes: 35 additions & 8 deletions API/hdx.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@

from .auth import AuthUser, admin_required, staff_required

# from src.validation.models import DynamicCategoriesModel
from src.validation.models import ErrorMessage, common_responses


router = APIRouter(prefix="/hdx", tags=["HDX"])


@router.post("", response_model=dict)
@router.post(
"",
response_model=dict,
responses={
"200": {"content": {"application/json": {"example": {"create": True}}}},
**common_responses,
},
)
@limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute")
@version(1)
async def create_hdx(
Expand All @@ -36,7 +43,7 @@ async def create_hdx(
return hdx_instance.create_hdx(hdx_data)


@router.get("", response_model=List[dict])
@router.get("", response_model=List[dict], responses={"500": {"model": ErrorMessage}})
@limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute")
@version(1)
async def read_hdx_list(
Expand Down Expand Up @@ -70,7 +77,11 @@ async def read_hdx_list(
return hdx_list


@router.get("/search", response_model=List[dict])
@router.get(
"/search",
response_model=List[dict],
responses={"404": {"model": ErrorMessage}, "500": {"model": ErrorMessage}},
)
@limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute")
@version(1)
async def search_hdx(
Expand Down Expand Up @@ -98,7 +109,11 @@ async def search_hdx(
return hdx_list


@router.get("/{hdx_id}", response_model=dict)
@router.get(
"/{hdx_id}",
response_model=dict,
responses={"404": {"model": ErrorMessage}, "500": {"model": ErrorMessage}},
)
@limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute")
@version(1)
async def read_hdx(
Expand All @@ -124,7 +139,11 @@ async def read_hdx(
raise HTTPException(status_code=404, detail=[{"msg": "HDX not found"}])


@router.put("/{hdx_id}", response_model=dict)
@router.put(
"/{hdx_id}",
response_model=dict,
responses={**common_responses, "404": {"model": ErrorMessage}},
)
@limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute")
@version(1)
async def update_hdx(
Expand Down Expand Up @@ -156,7 +175,11 @@ async def update_hdx(
return hdx_instance_update.update_hdx(hdx_id, hdx_data)


@router.patch("/{hdx_id}", response_model=Dict)
@router.patch(
"/{hdx_id}",
response_model=Dict,
responses={**common_responses, "404": {"model": ErrorMessage}},
)
@limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute")
@version(1)
async def patch_hdx(
Expand Down Expand Up @@ -188,7 +211,11 @@ async def patch_hdx(
return patch_instance.patch_hdx(hdx_id, hdx_data)


@router.delete("/{hdx_id}", response_model=dict)
@router.delete(
"/{hdx_id}",
response_model=dict,
responses={**common_responses, "404": {"model": ErrorMessage}},
)
@limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute")
@version(1)
async def delete_hdx(
Expand Down
Loading

0 comments on commit 194e694

Please sign in to comment.