From 0c7864c9311dd31413f25759f07bb99acfa312e4 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:26:31 +0100 Subject: [PATCH 01/17] adds API description --- API/main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/API/main.py b/API/main.py index f887895a..56d775a6 100644 --- a/API/main.py +++ b/API/main.py @@ -75,7 +75,12 @@ os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" -app = FastAPI(title="Raw Data API ", swagger_ui_parameters={"syntaxHighlight": False}) +app = FastAPI( + title="Raw Data API ", + description="""The Raw Data API allows you to transform + and export OpenStreetMap (OSM) data in different GIS file formats""", + swagger_ui_parameters={"syntaxHighlight": False}, +) app.include_router(auth_router) app.include_router(raw_data_router) app.include_router(tasks_router) From 575e62899e9c1200302a6c0e982ec542b418b310 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:27:07 +0100 Subject: [PATCH 02/17] removes back slashes --- API/auth/routers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/API/auth/routers.py b/API/auth/routers.py index 438a28e4..20f8900c 100644 --- a/API/auth/routers.py +++ b/API/auth/routers.py @@ -10,7 +10,7 @@ router = APIRouter(prefix="/auth", tags=["Auth"]) -@router.get("/login/") +@router.get("/login") def login_url(request: Request): """Generate Login URL for authentication using OAuth2 Application registered with OpenStreetMap. Click on the download url returned to get access_token. @@ -25,7 +25,7 @@ def login_url(request: Request): return login_url -@router.get("/callback/") +@router.get("/callback") def callback(request: Request): """Performs token exchange between OpenStreetMap and Raw Data API @@ -42,7 +42,7 @@ def callback(request: Request): return access_token -@router.get("/me/", response_model=AuthUser) +@router.get("/me", response_model=AuthUser) 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 . @@ -64,7 +64,7 @@ class User(BaseModel): # Create user -@router.post("/users/", response_model=dict) +@router.post("/users", response_model=dict) async def create_user(params: User, user_data: AuthUser = Depends(admin_required)): """ Creates a new user and returns the user's information. @@ -155,7 +155,7 @@ async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required) # Get all users -@router.get("/users/", response_model=list) +@router.get("/users", response_model=list) async def read_users( skip: int = 0, limit: int = 10, user_data: AuthUser = Depends(staff_required) ): From 0ae90bffc8cb7c36c7c262c1a2185a1af185ce16 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:32:52 +0100 Subject: [PATCH 03/17] added top-level authorization --- API/auth/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/API/auth/__init__.py b/API/auth/__init__.py index 0ff71d79..eea09889 100644 --- a/API/auth/__init__.py +++ b/API/auth/__init__.py @@ -1,6 +1,7 @@ from enum import Enum from typing import Union +from fastapi.security import APIKeyHeader from fastapi import Depends, Header, HTTPException from osm_login_python.core import Auth from pydantic import BaseModel, Field @@ -9,6 +10,11 @@ from src.config import get_oauth_credentials +Raw_Data_Access_Token = APIKeyHeader( + name="Access_Token", description="Access Token to Authorize User" +) + + class UserRole(Enum): ADMIN = 1 STAFF = 2 @@ -43,11 +49,15 @@ def get_osm_auth_user(access_token): return user -def login_required(access_token: str = Header(...)): +def login_required(access_token: str = Depends(Raw_Data_Access_Token)): return get_osm_auth_user(access_token) -def get_optional_user(access_token: str = Header(default=None)) -> AuthUser: +def get_optional_user( + access_token: str = Header( + default=None, description="Access Token to Authorize User" + ) +) -> AuthUser: if access_token: return get_osm_auth_user(access_token) else: From 5672a09128c2c223a56cf0f91ed2b85db0b3862b Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:37:14 +0100 Subject: [PATCH 04/17] added AuthUser example --- API/auth/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/API/auth/__init__.py b/API/auth/__init__.py index eea09889..1aa4e11d 100644 --- a/API/auth/__init__.py +++ b/API/auth/__init__.py @@ -27,6 +27,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://hotteamimage.com", + "role": UserRole.GUEST.value, + } + } + osm_auth = Auth(*get_oauth_credentials()) From 6aec11d87eabba9e360522859ac4f0d25cd267e5 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:40:56 +0100 Subject: [PATCH 05/17] updated HTTPException response to match error model --- API/auth/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/API/auth/__init__.py b/API/auth/__init__.py index 1aa4e11d..6428d559 100644 --- a/API/auth/__init__.py +++ b/API/auth/__init__.py @@ -78,7 +78,7 @@ def get_optional_user( def admin_required(user: AuthUser = Depends(login_required)): db_user = get_user_from_db(user.id) if not db_user["role"] is UserRole.ADMIN.value: - raise HTTPException(status_code=403, detail="User is not an admin") + raise HTTPException(status_code=403, detail=[{"msg": "User is not an admin"}]) return user @@ -90,5 +90,5 @@ def staff_required(user: AuthUser = Depends(login_required)): db_user["role"] is UserRole.STAFF.value or db_user["role"] is UserRole.ADMIN.value ): - raise HTTPException(status_code=403, detail="User is not a staff") + raise HTTPException(status_code=403, detail=[{"msg": "User is not a staff"}]) return user From 869b03464631850611ecfadcfe82fccf9ce6db34 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:57:21 +0100 Subject: [PATCH 06/17] added error model and common response examples --- src/validation/models.py | 129 ++++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 34 deletions(-) diff --git a/src/validation/models.py b/src/validation/models.py index 0e3a3a63..eb477cc8 100644 --- a/src/validation/models.py +++ b/src/validation/models.py @@ -244,8 +244,8 @@ class SnapshotResponse(BaseModel): class Config: json_schema_extra = { "example": { - "task_id": "aa539af6-83d4-4aa3-879e-abf14fffa03f", - "track_link": "/tasks/status/aa539af6-83d4-4aa3-879e-abf14fffa03f/", + "taskId": "aa539af6-83d4-4aa3-879e-abf14fffa03f", + "trackLink": "/tasks/status/aa539af6-83d4-4aa3-879e-abf14fffa03f/", } } @@ -288,6 +288,67 @@ class Config: json_schema_extra = {"example": {"lastUpdated": "2022-06-27 19:59:24+05:45"}} +class ErrorDetail(BaseModel): + msg: str + + +class ErrorMessage(BaseModel): + detail: List[ErrorDetail] + + +common_responses = { + 401: { + "model": ErrorMessage, + "content": { + "application/json": { + "example": {"detail": [{"msg": "OSM Authentication failed"}]} + } + }, + }, + 403: { + "model": ErrorMessage, + "content": { + "application/json": { + "example": {"detail": [{"msg": "OSM Authentication failed"}]} + } + }, + }, + 500: {"model": ErrorMessage}, +} + +stats_response = { + "200": { + "content": { + "application/json": { + "example": { + "summary": {"buildings": "", "roads": ""}, + "raw": { + "population": 0, + "populatedAreaKm2": 0, + "averageEditTime": 0, + "lastEditTime": 0, + "osmUsersCount": 0, + "osmBuildingCompletenessPercentage": 0, + "osmRoadsCompletenessPercentage": 0, + "osmBuildingsCount": 0, + "osmHighwayLengthKm": 0, + "aiBuildingsCountEstimation": 0, + "aiRoadCountEstimationKm": 0, + "buildingCount6Months": 0, + "highwayLength6MonthsKm": 0, + }, + "meta": { + "indicators": "https://github.com/hotosm/raw-data-api/tree/develop/docs/src/stats/indicators.md", + "metrics": "https://github.com/hotosm/raw-data-api/tree/develop/docs/src/stats/metrics.md", + }, + } + } + } + }, + "500": {"model": ErrorMessage}, +} + + class StatsRequestParams(BaseModel, GeometryValidatorMixin): iso3: Optional[str] = Field( default=None, @@ -296,22 +357,22 @@ class StatsRequestParams(BaseModel, GeometryValidatorMixin): max_length=3, example="NPL", ) - geometry: Optional[ - Union[Polygon, MultiPolygon, Feature, FeatureCollection] - ] = Field( - default=None, - example={ - "type": "Polygon", - "coordinates": [ - [ - [83.96919250488281, 28.194446860487773], - [83.99751663208006, 28.194446860487773], - [83.99751663208006, 28.214869548073377], - [83.96919250488281, 28.214869548073377], - [83.96919250488281, 28.194446860487773], - ] - ], - }, + geometry: Optional[Union[Polygon, MultiPolygon, Feature, FeatureCollection]] = ( + Field( + default=None, + example={ + "type": "Polygon", + "coordinates": [ + [ + [83.96919250488281, 28.194446860487773], + [83.99751663208006, 28.194446860487773], + [83.99751663208006, 28.214869548073377], + [83.96919250488281, 28.214869548073377], + [83.96919250488281, 28.194446860487773], + ] + ], + }, + ) ) @validator("geometry", pre=True, always=True) @@ -608,22 +669,22 @@ class DynamicCategoriesModel(BaseModel, GeometryValidatorMixin): } ], ) - geometry: Optional[ - Union[Polygon, MultiPolygon, Feature, FeatureCollection] - ] = Field( - default=None, - example={ - "type": "Polygon", - "coordinates": [ - [ - [83.96919250488281, 28.194446860487773], - [83.99751663208006, 28.194446860487773], - [83.99751663208006, 28.214869548073377], - [83.96919250488281, 28.214869548073377], - [83.96919250488281, 28.194446860487773], - ] - ], - }, + geometry: Optional[Union[Polygon, MultiPolygon, Feature, FeatureCollection]] = ( + Field( + default=None, + example={ + "type": "Polygon", + "coordinates": [ + [ + [83.96919250488281, 28.194446860487773], + [83.99751663208006, 28.194446860487773], + [83.99751663208006, 28.214869548073377], + [83.96919250488281, 28.214869548073377], + [83.96919250488281, 28.194446860487773], + ] + ], + }, + ) ) @validator("geometry", pre=True, always=True) From 7ffebec2847c9791737e29d17a606866707c5121 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:22:55 +0100 Subject: [PATCH 07/17] added responses and descriptions --- API/auth/routers.py | 119 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 18 deletions(-) diff --git a/API/auth/routers.py b/API/auth/routers.py index 20f8900c..a5170883 100644 --- a/API/auth/routers.py +++ b/API/auth/routers.py @@ -1,16 +1,32 @@ import json -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, Request, Query, Path 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. Click on the download url returned to get access_token. @@ -25,7 +41,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 @@ -42,18 +58,27 @@ def callback(request: Request): return access_token -@router.get("/me", response_model=AuthUser) +@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 . Parameters:None - Returns: user_data + Returns: user_data\n User Role : ADMIN = 1 STAFF = 2 GUEST = 3 + + Raises: + - HTTPException 403: Due to authentication error(Wrong access token). + - HTTPException 500: Internal server error. """ return user_data @@ -64,7 +89,14 @@ class User(BaseModel): # 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. @@ -80,15 +112,26 @@ async def create_user(params: User, user_data: AuthUser = Depends(admin_required - Dict[str, Any]: A dictionary containing the osm_id of the newly created user. Raises: - - HTTPException: If the user creation fails. + - HTTPException 403: If the user creation fails due to insufficient permission. + - HTTPException 500: If the user creation fails due to internal server error. """ auth = Users() return auth.create_user(params.osm_id, params.role) # Read user by osm_id -@router.get("/users/{osm_id}", response_model=dict) -async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): +@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), +): """ Retrieves user information based on the given osm_id. User Role : @@ -103,7 +146,9 @@ async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): - Dict[str, Any]: A dictionary containing user information. Raises: - - HTTPException: If the user with the given osm_id is not found. + - HTTPException 403: If the user has insufficient permission. + - HTTPException 404: If the user with the given osm_id is not found. + - HTTPException 500: If it fails due to internal server error. """ auth = Users() @@ -111,9 +156,18 @@ async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): # 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( - osm_id: int, update_data: User, user_data: AuthUser = Depends(admin_required) + update_data: User, + user_data: AuthUser = Depends(admin_required), + osm_id: int = Path(description="The OSM ID of the User to Update"), ): """ Updates user information based on the given osm_id. @@ -129,15 +183,27 @@ async def update_user( - Dict[str, Any]: A dictionary containing the updated user information. Raises: - - HTTPException: If the user with the given osm_id is not found. + - HTTPException 403: If the user has insufficient permission. + - HTTPException 404: If the user with the given osm_id is not found. + - HTTPException 500: If it fails due to internal server error. """ auth = Users() return auth.update_user(osm_id, update_data) # Delete user by osm_id -@router.delete("/users/{osm_id}", response_model=dict) -async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required)): +@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"), +): """ Deletes a user based on the given osm_id. @@ -148,16 +214,29 @@ async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required) - Dict[str, Any]: A dictionary containing the deleted user information. Raises: - - HTTPException: If the user with the given osm_id is not found. + - HTTPException 403: If the user has insufficient permission. + - HTTPException 404: If the user with the given osm_id is not found. + - HTTPException 500: If it fails due to internal server error. """ auth = Users() return auth.delete_user(osm_id) # 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 = 0, limit: int = 10, user_data: AuthUser = Depends(staff_required) + skip: int = Query(0, description="The Number of Users to Skip"), + limit: int = Query(10, description="The Maximum Number of Users to Retrieve"), + user_data: AuthUser = Depends(staff_required), ): """ Retrieves a list of users with optional pagination. @@ -168,6 +247,10 @@ async def read_users( Returns: - List[Dict[str, Any]]: A list of dictionaries containing user information. + + Raises: + - HTTPException 403: If it fails due to insufficient permission. + - HTTPException 500: If it fails due to internal server error. """ auth = Users() return auth.read_users(skip, limit) From cadfa793858f6e10f23c2cb7e02d198c9dca913b Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:23:58 +0100 Subject: [PATCH 08/17] added User exanmple --- API/auth/routers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/API/auth/routers.py b/API/auth/routers.py index a5170883..817125c6 100644 --- a/API/auth/routers.py +++ b/API/auth/routers.py @@ -87,6 +87,9 @@ class User(BaseModel): osm_id: int role: int + class Config: + json_schema_extra = {"example": {"osm_id": 123, "role": 1}} + # Create user @router.post( From 1cd9a68889da3e8bb136a2fcb992436ca585a596 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:10:42 +0100 Subject: [PATCH 09/17] removed trailing slashes --- API/raw_data.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/API/raw_data.py b/API/raw_data.py index 46c8e068..0577d59e 100644 --- a/API/raw_data.py +++ b/API/raw_data.py @@ -51,7 +51,7 @@ redis_client = redis.StrictRedis.from_url(CELERY_BROKER_URL) -@router.get("/status/", response_model=StatusResponse) +@router.get("/status", response_model=StatusResponse) @version(1) def check_database_last_updated(): """Gives status about how recent the osm data is , it will give the last time that database was updated completely""" @@ -59,7 +59,7 @@ def check_database_last_updated(): return {"last_updated": result} -@router.post("/snapshot/", response_model=SnapshotResponse) +@router.post("/snapshot", response_model=SnapshotResponse) @limiter.limit(f"{export_rate_limit}/minute") @version(1) def get_osm_current_snapshot_as_file( @@ -462,7 +462,7 @@ def get_osm_current_snapshot_as_file( ) -@router.post("/snapshot/plain/") +@router.post("/snapshot/plain") @version(1) def get_osm_current_snapshot_as_plain_geojson( request: Request, @@ -494,14 +494,14 @@ def get_osm_current_snapshot_as_plain_geojson( return result -@router.get("/countries/") +@router.get("/countries") @version(1) def get_countries(q: str = ""): result = RawData().get_countries_list(q) return result -@router.get("/osm_id/") +@router.get("/osm_id") @version(1) def get_osm_feature(osm_id: int): return RawData().get_osm_feature(osm_id) From c96003c706cf43fb5009b449e94f5156cf40b3d6 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:22:01 +0100 Subject: [PATCH 10/17] added error response and parameter descriptions --- API/raw_data.py | 62 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/API/raw_data.py b/API/raw_data.py index 0577d59e..f1976e1f 100644 --- a/API/raw_data.py +++ b/API/raw_data.py @@ -23,7 +23,7 @@ import redis from area import area -from fastapi import APIRouter, Body, Depends, HTTPException, Request +from fastapi import APIRouter, Body, Depends, HTTPException, Request, Path, Query from fastapi.responses import JSONResponse from fastapi_versioning import version @@ -41,6 +41,8 @@ RawDataCurrentParamsBase, SnapshotResponse, StatusResponse, + ErrorMessage, + common_responses, ) from .api_worker import process_raw_data @@ -51,15 +53,26 @@ redis_client = redis.StrictRedis.from_url(CELERY_BROKER_URL) -@router.get("/status", response_model=StatusResponse) +@router.get( + "/status", response_model=StatusResponse, responses={"500": {"model": ErrorMessage}} +) @version(1) def check_database_last_updated(): - """Gives status about how recent the osm data is , it will give the last time that database was updated completely""" + """Gives status about how recent the osm data is. It will give the last time that database was updated completely""" + result = RawData().check_status() return {"last_updated": result} -@router.post("/snapshot", response_model=SnapshotResponse) +@router.post( + "/snapshot", + response_model=SnapshotResponse, + responses={ + **common_responses, + 404: {"model": ErrorMessage}, + 429: {"model": ErrorMessage}, + }, +) @limiter.limit(f"{export_rate_limit}/minute") @version(1) def get_osm_current_snapshot_as_file( @@ -411,7 +424,7 @@ def get_osm_current_snapshot_as_file( status_code=403, detail=[ { - "msg": "Insufficient Permission to use folder structure exports , Remove / from filename or get access" + "msg": "Insufficient Permission to use folder structure exports, Remove / from filename or get access" } ], ) @@ -462,7 +475,9 @@ def get_osm_current_snapshot_as_file( ) -@router.post("/snapshot/plain") +@router.post( + "/snapshot/plain", responses={**common_responses, 404: {"model": ErrorMessage}} +) @version(1) def get_osm_current_snapshot_as_plain_geojson( request: Request, @@ -473,7 +488,7 @@ def get_osm_current_snapshot_as_plain_geojson( Args: request (Request): _description_ - params (RawDataCurrentParamsBase): Same as /snapshot excpet multiple output format options and configurations + params (RawDataCurrentParamsBase): Same as /snapshot except multiple output format options and configurations Returns: Featurecollection: Geojson @@ -494,14 +509,39 @@ def get_osm_current_snapshot_as_plain_geojson( return result -@router.get("/countries") +@router.get("/countries", responses={"500": {"model": ErrorMessage}}) @version(1) -def get_countries(q: str = ""): +def get_countries( + q: str = Query("", description="Query parameter for filtering countries") +): + """ + Gets Countries list from the database + + Args: + q (str): query parameter for filtering countries + + Returns: + featurecollection: geojson of country + """ + result = RawData().get_countries_list(q) return result -@router.get("/osm_id") +@router.get( + "/osm_id", + responses={"404": {"model": ErrorMessage}, "500": {"model": ErrorMessage}}, +) @version(1) -def get_osm_feature(osm_id: int): +def get_osm_feature(osm_id: int = Path(description="The OSM ID of feature")): + """ + Gets geometry of osm_id in geojson + + Args: + osm_id (int): osm_id of feature + + Returns: + featurecollection: Geojson + """ + return RawData().get_osm_feature(osm_id) From c7c146bec01221227f84dd1498980057fb6a53e4 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:24:28 +0100 Subject: [PATCH 11/17] removed trailing slashes --- API/tasks.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/API/tasks.py b/API/tasks.py index bca37813..2a58cdff 100644 --- a/API/tasks.py +++ b/API/tasks.py @@ -16,7 +16,7 @@ router = APIRouter(prefix="/tasks", tags=["Tasks"]) -@router.get("/status/{task_id}/", response_model=SnapshotTaskResponse) +@router.get("/status/{task_id}", response_model=SnapshotTaskResponse) @version(1) def get_task_status( task_id, @@ -78,7 +78,7 @@ def get_task_status( return JSONResponse(result) -@router.get("/revoke/{task_id}/") +@router.get("/revoke/{task_id}") @version(1) def revoke_task(task_id, user: AuthUser = Depends(staff_required)): """Revokes task , Terminates if it is executing @@ -93,7 +93,7 @@ def revoke_task(task_id, user: AuthUser = Depends(staff_required)): return JSONResponse({"id": task_id}) -@router.get("/inspect/") +@router.get("/inspect") @version(1) def inspect_workers( request: Request, @@ -134,7 +134,7 @@ def inspect_workers( return JSONResponse(content=response_data) -@router.get("/ping/") +@router.get("/ping") @version(1) def ping_workers(): """Pings available workers @@ -145,7 +145,7 @@ def ping_workers(): return JSONResponse(inspected_ping) -@router.get("/purge/") +@router.get("/purge") @version(1) def discard_all_waiting_tasks(user: AuthUser = Depends(admin_required)): """ @@ -159,7 +159,7 @@ def discard_all_waiting_tasks(user: AuthUser = Depends(admin_required)): queues = [DEFAULT_QUEUE_NAME, DAEMON_QUEUE_NAME] -@router.get("/queue/") +@router.get("/queue") @version(1) def get_queue_info(): queue_info = {} @@ -176,7 +176,7 @@ def get_queue_info(): return JSONResponse(content=queue_info) -@router.get("/queue/details/{queue_name}/") +@router.get("/queue/details/{queue_name}") @version(1) def get_list_details( queue_name: str, From 3b8c7124201cf977c7376bda4133f9d6e962a4c5 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:44:27 +0100 Subject: [PATCH 12/17] added response, examples and parameter descriptions --- API/tasks.py | 106 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 13 deletions(-) diff --git a/API/tasks.py b/API/tasks.py index 2a58cdff..ef9c849e 100644 --- a/API/tasks.py +++ b/API/tasks.py @@ -3,12 +3,12 @@ import redis from celery.result import AsyncResult -from fastapi import APIRouter, Depends, HTTPException, Query, Request +from fastapi import APIRouter, Depends, HTTPException, Query, Request, Path from fastapi.responses import JSONResponse from fastapi_versioning import version from src.config import CELERY_BROKER_URL, DAEMON_QUEUE_NAME, DEFAULT_QUEUE_NAME -from src.validation.models import SnapshotTaskResponse +from src.validation.models import SnapshotTaskResponse, ErrorMessage, common_responses from .api_worker import celery from .auth import AuthUser, admin_required, login_required, staff_required @@ -16,10 +16,14 @@ router = APIRouter(prefix="/tasks", tags=["Tasks"]) -@router.get("/status/{task_id}", response_model=SnapshotTaskResponse) +@router.get( + "/status/{task_id}", + response_model=SnapshotTaskResponse, + responses={"404": {"model": ErrorMessage}, "500": {"model": ErrorMessage}}, +) @version(1) def get_task_status( - task_id, + task_id=Path(description="Unique id provided on response from */snapshot/*"), only_args: bool = Query( default=False, description="Fetches arguments of task", @@ -78,10 +82,26 @@ def get_task_status( return JSONResponse(result) -@router.get("/revoke/{task_id}") +@router.get( + "/revoke/{task_id}", + responses={ + **common_responses, + "404": {"model": ErrorMessage}, + "200": { + "content": { + "application/json": { + "example": {"id": "aa539af6-83d4-4aa3-879e-abf14fffa03f"} + } + } + }, + }, +) @version(1) -def revoke_task(task_id, user: AuthUser = Depends(staff_required)): - """Revokes task , Terminates if it is executing +def revoke_task( + task_id=Path(description="Unique id provided on response from */snapshot*"), + user: AuthUser = Depends(staff_required), +): + """Revokes task, Terminates if it is executing Args: task_id (_type_): task id of raw data task @@ -93,7 +113,19 @@ def revoke_task(task_id, user: AuthUser = Depends(staff_required)): return JSONResponse({"id": task_id}) -@router.get("/inspect") +@router.get( + "/inspect", + responses={ + "500": {"model": ErrorMessage}, + "200": { + "content": { + "application/json": { + "example": {"active": [{"celery@default_worker": {}}]} + } + } + }, + }, +) @version(1) def inspect_workers( request: Request, @@ -134,7 +166,19 @@ def inspect_workers( return JSONResponse(content=response_data) -@router.get("/ping") +@router.get( + "/ping", + responses={ + "500": {"model": ErrorMessage}, + "200": { + "content": { + "application/json": { + "example": {"celery@default_worker": {"ok": "pong"}} + } + } + }, + }, +) @version(1) def ping_workers(): """Pings available workers @@ -145,12 +189,22 @@ def ping_workers(): return JSONResponse(inspected_ping) -@router.get("/purge") +@router.get( + "/purge", + responses={ + **common_responses, + "200": {"content": {"application/json": {"example": {"tasks_discarded": 0}}}}, + }, +) @version(1) def discard_all_waiting_tasks(user: AuthUser = Depends(admin_required)): """ Discards all waiting tasks from the queue + Returns : Number of tasks discarded + + Raises: + - HTTPException 403: If purge fails due to insufficient permission. """ purged = celery.control.purge() return JSONResponse({"tasks_discarded": purged}) @@ -159,9 +213,23 @@ def discard_all_waiting_tasks(user: AuthUser = Depends(admin_required)): queues = [DEFAULT_QUEUE_NAME, DAEMON_QUEUE_NAME] -@router.get("/queue") +@router.get( + "/queue", + responses={ + "500": {"model": ErrorMessage}, + "200": { + "content": {"application/json": {"example": {"raw_daemon": {"length": 0}}}} + }, + }, +) @version(1) def get_queue_info(): + """ + Get all the queues + + Returns : The queues names and their lengths + """ + queue_info = {} redis_client = redis.StrictRedis.from_url(CELERY_BROKER_URL) @@ -176,15 +244,27 @@ def get_queue_info(): return JSONResponse(content=queue_info) -@router.get("/queue/details/{queue_name}") +@router.get( + "/queue/details/{queue_name}", + responses={**common_responses, "404": {"model": ErrorMessage}}, +) @version(1) def get_list_details( - queue_name: str, + queue_name=Path(description="Name of queue to retrieve"), args: bool = Query( default=False, description="Includes arguments of task", ), ): + """ + Retrieves queue information based on the given queue name + + Args: + - queue_name (str): The name of the queue to retrieve. + + Returns : The queue details + """ + if queue_name not in queues: raise HTTPException(status_code=404, detail=f"Queue '{queue_name}' not found") redis_client = redis.StrictRedis.from_url(CELERY_BROKER_URL) From 3912931fcf313aa93dc5706f9fc26640fc9a2492 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:47:15 +0100 Subject: [PATCH 13/17] updated HTTPException to match error model --- API/tasks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/API/tasks.py b/API/tasks.py index ef9c849e..914a1c4b 100644 --- a/API/tasks.py +++ b/API/tasks.py @@ -266,7 +266,9 @@ def get_list_details( """ if queue_name not in queues: - raise HTTPException(status_code=404, detail=f"Queue '{queue_name}' not found") + raise HTTPException( + status_code=404, detail=[{"msg": f"Queue '{queue_name}' not found"}] + ) redis_client = redis.StrictRedis.from_url(CELERY_BROKER_URL) list_items = redis_client.lrange(queue_name, 0, -1) From fb5f13debfd6570c1d3ac9e91979f2793d4009b1 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 22:03:52 +0100 Subject: [PATCH 14/17] removed trailing back slash --- API/custom_exports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/custom_exports.py b/API/custom_exports.py index da4bcea1..1a41054d 100644 --- a/API/custom_exports.py +++ b/API/custom_exports.py @@ -13,7 +13,7 @@ router = APIRouter(prefix="/custom", tags=["Custom Exports"]) -@router.post("/snapshot/") +@router.post("/snapshot") @limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute") @version(1) async def process_custom_requests( From 5a997d07dcdebb790c0cfd07d6451baee0e3407c Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Wed, 3 Apr 2024 22:12:43 +0100 Subject: [PATCH 15/17] added responses and examples --- API/custom_exports.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/API/custom_exports.py b/API/custom_exports.py index 1a41054d..8d5c5d67 100644 --- a/API/custom_exports.py +++ b/API/custom_exports.py @@ -5,7 +5,7 @@ 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 DynamicCategoriesModel +from src.validation.models import DynamicCategoriesModel, ErrorMessage, common_responses from .api_worker import process_custom_request from .auth import AuthUser, UserRole, staff_required @@ -13,7 +13,23 @@ router = APIRouter(prefix="/custom", tags=["Custom Exports"]) -@router.post("/snapshot") +@router.post( + "/snapshot", + responses={ + **common_responses, + "429": {"model": ErrorMessage}, + "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( From cbb152274c1b02299407ae61b1c2f0247a5de765 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Thu, 4 Apr 2024 06:26:22 +0100 Subject: [PATCH 16/17] removed trailing back slash --- API/stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/stats.py b/API/stats.py index b2ca5414..c8ebd6ee 100644 --- a/API/stats.py +++ b/API/stats.py @@ -11,7 +11,7 @@ router = APIRouter(prefix="/stats", tags=["Stats"]) -@router.post("/polygon/") +@router.post("/polygon") @limiter.limit(f"{POLYGON_STATISTICS_API_RATE_LIMIT}/minute") @version(1) async def get_polygon_stats( From 960dc4b10c7ff569302b9561f581ce2b43934a31 Mon Sep 17 00:00:00 2001 From: Oluwanifemi Daramola <76186151+nifedara@users.noreply.github.com> Date: Thu, 4 Apr 2024 06:30:25 +0100 Subject: [PATCH 17/17] added responses and example --- API/stats.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/API/stats.py b/API/stats.py index c8ebd6ee..49a12bb2 100644 --- a/API/stats.py +++ b/API/stats.py @@ -6,12 +6,15 @@ from src.app import PolygonStats from src.config import LIMITER as limiter from src.config import POLYGON_STATISTICS_API_RATE_LIMIT -from src.validation.models import StatsRequestParams +from src.validation.models import StatsRequestParams, stats_response router = APIRouter(prefix="/stats", tags=["Stats"]) -@router.post("/polygon") +@router.post( + "/polygon", + responses={**stats_response}, +) @limiter.limit(f"{POLYGON_STATISTICS_API_RATE_LIMIT}/minute") @version(1) async def get_polygon_stats(