From ebd093f730addea9fb77c620a79aec37d2835b1f Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 14:39:00 +0300 Subject: [PATCH 01/28] Added description that Provides a brief overview of the API's functionality for documentation purposes --- API/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/API/main.py b/API/main.py index f887895a..23493309 100644 --- a/API/main.py +++ b/API/main.py @@ -74,8 +74,13 @@ import os os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" - -app = FastAPI(title="Raw Data API ", swagger_ui_parameters={"syntaxHighlight": False}) +# CHANGES ADDING DESCRIPTION +# Provides a brief overview of the API's functionality for documentation purposes. +app = FastAPI( + title="Raw Data API", + description="This API provides access to raw data.", + swagger_ui_parameters={"syntaxHighlight": False} +) app.include_router(auth_router) app.include_router(raw_data_router) app.include_router(tasks_router) From 89c648acacee10b59a08d439bd61839b0f2ddd44 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 14:46:10 +0300 Subject: [PATCH 02/28] Added description that Provides a brief overview of the API's functionality for documentation purposes --- API/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/main.py b/API/main.py index 23493309..4800a016 100644 --- a/API/main.py +++ b/API/main.py @@ -74,7 +74,7 @@ import os os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" -# CHANGES ADDING DESCRIPTION + # Provides a brief overview of the API's functionality for documentation purposes. app = FastAPI( title="Raw Data API", From d78246f3df8cdefdc83b806fe77543cb81b2a1fb Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 14:48:11 +0300 Subject: [PATCH 03/28] Removing trailing slash on Auth APIs --- API/auth/routers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/API/auth/routers.py b/API/auth/routers.py index 438a28e4..7fa9a14a 100644 --- a/API/auth/routers.py +++ b/API/auth/routers.py @@ -9,8 +9,8 @@ router = APIRouter(prefix="/auth", tags=["Auth"]) - -@router.get("/login/") +#CHANGES: Path must not end with a slash: Removing Trailing slashes +@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 bc48838e03a8ef6b190730c1c5e591108197d8f4 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 14:49:39 +0300 Subject: [PATCH 04/28] Removing trailing slash on Custom Exports APIs --- API/custom_exports.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/API/custom_exports.py b/API/custom_exports.py index da4bcea1..75127ab5 100644 --- a/API/custom_exports.py +++ b/API/custom_exports.py @@ -12,8 +12,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 dd678c80ab647b68c92de9bd4e084a679beb327a Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 14:50:46 +0300 Subject: [PATCH 05/28] Removing trailing slash on HDX APIs --- API/hdx.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/API/hdx.py b/API/hdx.py index b5d89a20..8b3b1445 100644 --- a/API/hdx.py +++ b/API/hdx.py @@ -69,8 +69,7 @@ async def read_hdx_list( raise HTTPException(status_code=422, detail="Couldn't process query") return hdx_list - -@router.get("/search/", response_model=List[dict]) +@router.get("/search", response_model=List[dict]) @limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute") @version(1) async def search_hdx( From 4a67deb219764b5919c59852393f14be61c98996 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 14:55:05 +0300 Subject: [PATCH 06/28] Removing trailing slash on Raw data APIs --- API/raw_data.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/API/raw_data.py b/API/raw_data.py index 46c8e068..aac5e6bc 100644 --- a/API/raw_data.py +++ b/API/raw_data.py @@ -50,8 +50,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 +58,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 +461,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 +493,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 5a1bb3c0cf51bb963e33275d69caec897223fb91 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 14:56:52 +0300 Subject: [PATCH 07/28] Removing trailing slash on statistics for the specified polygon APIs --- API/stats.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/API/stats.py b/API/stats.py index b2ca5414..8185fc4a 100644 --- a/API/stats.py +++ b/API/stats.py @@ -10,8 +10,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 8954737d08adc1ea704a3bacc0cc765072880525 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 14:57:53 +0300 Subject: [PATCH 08/28] Removing trailing slash on tasks APIs --- API/tasks.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/API/tasks.py b/API/tasks.py index bca37813..6ed1fc26 100644 --- a/API/tasks.py +++ b/API/tasks.py @@ -15,8 +15,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 +77,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 +92,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 +133,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 +144,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 +158,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 +175,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 c328827fa7312c05604206be30f0f56ec63cf3c2 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 14:59:54 +0300 Subject: [PATCH 09/28] Removing trailing slash on S3 APIs --- API/s3.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/API/s3.py b/API/s3.py index 767f2952..d74e43e8 100644 --- a/API/s3.py +++ b/API/s3.py @@ -31,8 +31,7 @@ ) paginator = s3.get_paginator("list_objects_v2") - -@router.get("/files/") +@router.get("/files") @limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute") @version(1) async def list_s3_files( From b4f59a7060a163cde42e885eebe9988cd505adfd Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Mon, 18 Mar 2024 15:04:54 +0300 Subject: [PATCH 10/28] Added description that Provides a brief overview of the API's functionality for documentation purposes --- API/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/main.py b/API/main.py index 4800a016..496ace96 100644 --- a/API/main.py +++ b/API/main.py @@ -78,7 +78,7 @@ # Provides a brief overview of the API's functionality for documentation purposes. app = FastAPI( title="Raw Data API", - description="This API provides access to raw data.", + description="Raw Data API is a set of high-performant APIs for transforming and exporting OpenStreetMap (OSM) data in different GIS file formats.", swagger_ui_parameters={"syntaxHighlight": False} ) app.include_router(auth_router) From 0b7ef7223dc46f20c845131eb10c3483fde97ae8 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Wed, 20 Mar 2024 12:34:29 +0300 Subject: [PATCH 11/28] Documenting 403, 404 and 500 Responses on Auth APIs --- API/auth/routers.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/API/auth/routers.py b/API/auth/routers.py index 7fa9a14a..e4c8cdd2 100644 --- a/API/auth/routers.py +++ b/API/auth/routers.py @@ -1,6 +1,6 @@ import json -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, Request, HTTPException from pydantic import BaseModel from src.app import Users @@ -9,7 +9,12 @@ router = APIRouter(prefix="/auth", tags=["Auth"]) -#CHANGES: Path must not end with a slash: Removing Trailing slashes +error_responses = { + 403: {"description": "Forbidden"}, + 404: {"description": "User with that osm_id is not found"}, + 500: {"description": "Internal Server Error"} +} + @router.get("/login") def login_url(request: Request): """Generate Login URL for authentication using OAuth2 Application registered with OpenStreetMap. @@ -25,7 +30,7 @@ def login_url(request: Request): return login_url -@router.get("/callback") +@router.get("/callback", responses={500: {"description": "Internal Server Error"}}) def callback(request: Request): """Performs token exchange between OpenStreetMap and Raw Data API @@ -37,7 +42,13 @@ def callback(request: Request): Returns: - access_token (string) """ - access_token = osm_auth.callback(str(request.url)) + try: + access_token = osm_auth.callback(str(request.url)) + except Exception as ex: + raise HTTPException( + status_code=500, + detail="Internal Server Error occurred while performing token exchange between OpenStreetMap and Raw Data API" + ) return access_token @@ -64,7 +75,9 @@ class User(BaseModel): # Create user -@router.post("/users", response_model=dict) +@router.post("/users", response_model=dict, + responses={403: {"description": "Forbidden"}} + ) async def create_user(params: User, user_data: AuthUser = Depends(admin_required)): """ Creates a new user and returns the user's information. @@ -87,7 +100,7 @@ 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}", response_model=dict, responses=error_responses) async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): """ Retrieves user information based on the given osm_id. @@ -103,7 +116,8 @@ 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 404: If the user with the given osm_id is not found. + - HTTPException 403: If the user is not a staff. """ auth = Users() @@ -111,7 +125,7 @@ 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}", response_model=dict, responses=error_responses) async def update_user( osm_id: int, update_data: User, user_data: AuthUser = Depends(admin_required) ): @@ -129,14 +143,16 @@ 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 is not an Admin. + - HTTPException 404: If the user with the given osm_id is not found. """ auth = Users() return auth.update_user(osm_id, update_data) # Delete user by osm_id -@router.delete("/users/{osm_id}", response_model=dict) +@router.delete("/users/{osm_id}", response_model=dict, + responses=error_responses) async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required)): """ Deletes a user based on the given osm_id. @@ -148,14 +164,15 @@ 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 404: If the user with the given osm_id is not found. + - HTTPException 403: If the user is not an Admin. """ auth = Users() return auth.delete_user(osm_id) # Get all users -@router.get("/users", response_model=list) +@router.get("/users", response_model=list, responses={403: {"description": "Forbidden"}}) async def read_users( skip: int = 0, limit: int = 10, user_data: AuthUser = Depends(staff_required) ): From 3bd6f7c492c0bdbc71deb54ba193983e181511e5 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Wed, 20 Mar 2024 12:36:08 +0300 Subject: [PATCH 12/28] Add changes that when the User is not found in the Database 404 Error is raised --- API/auth/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/API/auth/__init__.py b/API/auth/__init__.py index 0ff71d79..1cfc1417 100644 --- a/API/auth/__init__.py +++ b/API/auth/__init__.py @@ -24,10 +24,12 @@ class AuthUser(BaseModel): osm_auth = Auth(*get_oauth_credentials()) - def get_user_from_db(osm_id: int): auth = Users() user = auth.read_user(osm_id) + # Add changes that when the User is not found in the Database 404 Error is raised + if not user: + raise HTTPException(status_code=404, detail="User not Found in the Database") return user From 6f32ae28bf419c99c9234adcf5464678adcd1f2f Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Wed, 20 Mar 2024 13:31:58 +0300 Subject: [PATCH 13/28] A File that contains all responses a user might get while sending a request to the server --- API/auth/responses.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 API/auth/responses.py diff --git a/API/auth/responses.py b/API/auth/responses.py new file mode 100644 index 00000000..c5450e50 --- /dev/null +++ b/API/auth/responses.py @@ -0,0 +1,24 @@ +from fastapi import HTTPException +from pydantic import BaseModel + +# Define common error responses +common_error_responses = { + 403: {"description": "Forbidden", "model": HTTPException}, + 404: {"description": "Not Found", "model": HTTPException}, + 500: {"description": "Internal Server Error", "model": HTTPException}, +} + +# Define shared error response models +class ErrorResponse(BaseModel): + detail: str + +# Add a default response model for error responses +for status_code in common_error_responses: + if "model" not in common_error_responses[status_code]: + common_error_responses[status_code]["model"] = ErrorResponse + +error_responses_with_examples = { + 403: {"content": {"application/json": {"example": {"message": "Access forbidden"}}}}, + 404: {"content": {"application/json": {"example": {"error": "Not Found"}}}}, + 500: {"content": {"application/json": {"example": {"error": "Internal Server Error"}}}}, +} From 99129081f9c8440b62c72e0067e6b7de87c91370 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Wed, 20 Mar 2024 13:33:16 +0300 Subject: [PATCH 14/28] Updated the Responses using a DRY Approach --- API/auth/routers.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/API/auth/routers.py b/API/auth/routers.py index e4c8cdd2..7bb1be30 100644 --- a/API/auth/routers.py +++ b/API/auth/routers.py @@ -6,15 +6,10 @@ from src.app import Users from . import AuthUser, admin_required, login_required, osm_auth, staff_required +from .responses import common_error_responses, error_responses_with_examples router = APIRouter(prefix="/auth", tags=["Auth"]) -error_responses = { - 403: {"description": "Forbidden"}, - 404: {"description": "User with that osm_id is not found"}, - 500: {"description": "Internal Server Error"} -} - @router.get("/login") def login_url(request: Request): """Generate Login URL for authentication using OAuth2 Application registered with OpenStreetMap. @@ -30,7 +25,10 @@ def login_url(request: Request): return login_url -@router.get("/callback", responses={500: {"description": "Internal Server Error"}}) +@router.get("/callback", responses= + { + **common_error_responses, **error_responses_with_examples + }) def callback(request: Request): """Performs token exchange between OpenStreetMap and Raw Data API @@ -76,7 +74,7 @@ class User(BaseModel): # Create user @router.post("/users", response_model=dict, - responses={403: {"description": "Forbidden"}} + responses={**common_error_responses, **error_responses_with_examples} ) async def create_user(params: User, user_data: AuthUser = Depends(admin_required)): """ @@ -100,7 +98,10 @@ 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, responses=error_responses) +@router.get("/users/{osm_id}", response_model=dict, + responses={ + **common_error_responses, **error_responses_with_examples + }) async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): """ Retrieves user information based on the given osm_id. @@ -125,7 +126,10 @@ 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, responses=error_responses) +@router.put("/users/{osm_id}", response_model=dict, + responses= { + **common_error_responses, **error_responses_with_examples + }) async def update_user( osm_id: int, update_data: User, user_data: AuthUser = Depends(admin_required) ): @@ -152,7 +156,9 @@ async def update_user( # Delete user by osm_id @router.delete("/users/{osm_id}", response_model=dict, - responses=error_responses) + responses={ + **common_error_responses, **error_responses_with_examples + }) async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required)): """ Deletes a user based on the given osm_id. @@ -172,7 +178,9 @@ async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required) # Get all users -@router.get("/users", response_model=list, responses={403: {"description": "Forbidden"}}) +@router.get("/users", response_model=list, + responses={**common_error_responses, **error_responses_with_examples} + ) async def read_users( skip: int = 0, limit: int = 10, user_data: AuthUser = Depends(staff_required) ): @@ -185,6 +193,9 @@ async def read_users( Returns: - List[Dict[str, Any]]: A list of dictionaries containing user information. + + Raises: + - HTTPException 403: If the user is not a Staff. """ auth = Users() return auth.read_users(skip, limit) From fc6a1175adae43d7ba7aa97078684ae9395d3b91 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Wed, 20 Mar 2024 13:34:13 +0300 Subject: [PATCH 15/28] Updated Tasks 404, 403, 500 Responses using a DRY Approach --- API/tasks.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/API/tasks.py b/API/tasks.py index 6ed1fc26..85ac11e6 100644 --- a/API/tasks.py +++ b/API/tasks.py @@ -12,6 +12,8 @@ from .api_worker import celery from .auth import AuthUser, admin_required, login_required, staff_required +from .auth.responses import common_error_responses, error_responses_with_examples + router = APIRouter(prefix="/tasks", tags=["Tasks"]) @@ -77,7 +79,7 @@ def get_task_status( return JSONResponse(result) -@router.get("/revoke/{task_id}") +@router.get("/revoke/{task_id}", responses={**common_error_responses, **error_responses_with_examples}) @version(1) def revoke_task(task_id, user: AuthUser = Depends(staff_required)): """Revokes task , Terminates if it is executing @@ -87,6 +89,8 @@ def revoke_task(task_id, user: AuthUser = Depends(staff_required)): Returns: id: id of revoked task + Raises: + - HTTPException 403: If the user is not an Staff. """ celery.control.revoke(task_id=task_id, terminate=True) return JSONResponse({"id": task_id}) @@ -144,12 +148,16 @@ def ping_workers(): return JSONResponse(inspected_ping) -@router.get("/purge") +@router.get("/purge", responses={403: {"description": "Forbidden", "content": {"application/json": {"example": {"message": "Access forbidden"}}}}}) @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 the user is not an Admin. + """ purged = celery.control.purge() return JSONResponse({"tasks_discarded": purged}) From 293c87bd69c379901aa989e5ae2a6d1c520ebb88 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 26 Mar 2024 21:41:59 +0300 Subject: [PATCH 16/28] Added Docstrings to explain test functions, added exception 403 for authentication --- tests/test_API.py | 305 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 290 insertions(+), 15 deletions(-) diff --git a/tests/test_API.py b/tests/test_API.py index 163cbc00..f7b60c54 100644 --- a/tests/test_API.py +++ b/tests/test_API.py @@ -1,3 +1,51 @@ +""" +FastAPI test client for the Raw Data API +======================================== + +This script contains test functions for the Raw Data API, which provides +various functionalities such as status checks, login, country information, +snapshot generation, and task management. + +To run the tests, simply execute this script using a Python interpreter. + +Note: An access token is required to run most of the tests. + +Prerequisites +============ + +- FastAPI test client +- Access token for Raw Data API + +Test Functions +============= + +The following test functions are available: + +- `test_status()`: Checks the status of the Raw Data API +- `test_login_url()`: Checks the login URL +- `test_login_auth_me()`: Checks the authentication status +- `test_countries_endpoint()`: Queries country information for Nepal +- `test_osm_id_endpoint()`: Queries OSM ID information +- `test_snapshot()`: Generates a snapshot of a polygon +- `test_snapshot_featurecollection()`: Generates a snapshot of a feature collection +- `test_snapshot_feature()`: Generates a snapshot of a feature +- `test_snapshot_feature_fgb()`: Generates a snapshot of a feature in FGB format +- `test_snapshot_feature_fgb_wrap_geom()`: Generates a snapshot of a feature in FGB format with wrapped geometry +- `test_snapshot_feature_shp()`: Generates a snapshot of a feature in SHP format +- `test_snapshot_feature_gpkg()`: Generates a snapshot of a feature in GPKG format +- `test_snapshot_feature_kml()`: Generates a snapshot of a feature in KML format +- `test_snapshot_feature_sql()`: Generates a snapshot of a feature in SQL format +- `test_snapshot_feature_csv()`: Generates a snapshot of a feature in CSV format +- `test_snapshot_centroid()`: Generates a snapshot of a polygon centroid +- `test_snapshot_filters()`: Generates a snapshot of a custom polygon with filters +- `test_snapshot_filters_and_filter()`: Generates a snapshot of a custom polygon with filters and AND operator +- `test_snapshot_and_filter()`: Generates a snapshot of a custom polygon with filters and AND/OR operators +- `test_snapshot_authentication_uuid()`: Generates a snapshot with UUID authentication +- `test_snapshot_bind_zip()`: Generates a snapshot with ZIP binding +- `test_worker_connection()`: Checks the connection to the worker + +""" + import os import time @@ -39,26 +87,40 @@ def wait_for_task_completion(track_link, max_attempts=12, interval_seconds=5): def test_status(): + """ + Checks the status of the Raw Data API + """ response = client.get("/v1/status/") assert response.status_code == 200 ## Login def test_login_url(): + """ + Checks the login URL + """ response = client.get("/v1/auth/login/") assert response.status_code == 200 def test_login_auth_me(): - headers = {"access-token": access_token} - response = client.get("/v1/auth/me/", headers=headers) - assert response.status_code == 200 - + """ + Checks the authentication status + """ + if access_token is None: + raise Exception("Access token is not available. Cannot execute tests.") + else: + headers = {"access-token": access_token} + response = client.get("/v1/auth/me/", headers=headers) + assert response.status_code == 200 ## Countries def test_countries_endpoint(): + """ + Queries country information for Nepal + """ response = client.get("/v1/countries/?q=nepal") assert response.status_code == 200 @@ -67,12 +129,18 @@ def test_countries_endpoint(): def test_osm_id_endpoint(): + """ + Queries OSM ID information + """ response = client.get("/v1/osm_id/?osm_id=421498318") assert response.status_code == 200 ## Snapshot def test_snapshot(): + """ + Generates a snapshot of a polygon + """ response = client.post( "/v1/snapshot/", json={ @@ -97,6 +165,10 @@ def test_snapshot(): def test_snapshot_featurecollection(): + """ + Test creating a snapshot of a specific geographic area. + Assert 200 status code, retrieve 'track_link' and wait for task completion. + """ response = client.post( "/v1/snapshot/", json={ @@ -130,6 +202,12 @@ def test_snapshot_featurecollection(): def test_snapshot_feature(): + """ + Test creating a snapshot of a geographic area. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -158,6 +236,12 @@ def test_snapshot_feature(): def test_snapshot_feature_fgb(): + """ + Test creating a snapshot of a geographic area with output type 'fgb'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -187,6 +271,12 @@ def test_snapshot_feature_fgb(): def test_snapshot_feature_fgb_wrap_geom(): + """ + Test creating a snapshot of a geographic area with output type 'fgb'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -217,6 +307,12 @@ def test_snapshot_feature_fgb_wrap_geom(): def test_snapshot_feature_shp(): + """ + Test creating a snapshot of a geographic area with output type 'shp'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -246,6 +342,12 @@ def test_snapshot_feature_shp(): def test_snapshot_feature_gpkg(): + """ + Test creating a snapshot of a geographic area with output type 'gpkg'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -275,6 +377,12 @@ def test_snapshot_feature_gpkg(): def test_snapshot_feature_kml(): + """ + Test creating a snapshot of a geographic area with output type 'kml'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -304,6 +412,19 @@ def test_snapshot_feature_kml(): def test_snapshot_feature_sql(): + """ + Test creating an SQL formatted snapshot for a GeoJSON Feature object. + + Send a POST request to the "/v1/snapshot/" endpoint, + including the target output format and a GeoJSON Feature + object in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ response = client.post( "/v1/snapshot/", json={ @@ -333,6 +454,12 @@ def test_snapshot_feature_sql(): def test_snapshot_feature_csv(): + """ + Test creating a snapshot of a geographic area with output type 'sql'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -362,6 +489,12 @@ def test_snapshot_feature_csv(): def test_snapshot_centroid(): + """ + Test creating a snapshot of a geographic area with centroid calculation. + Send a POST request with a Polygon payload and centroid=True, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -387,6 +520,14 @@ def test_snapshot_centroid(): def test_snapshot_filters(): + """ + Test creating a snapshot with tags filter and specific attributes. + Send a POST request with a Polygon payload, + including tags for points, lines and polygons, + and a list of requested attributes. + Expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -613,6 +754,14 @@ def test_snapshot_filters(): def test_snapshot_and_filter(): + """ + Test creating a snapshot with filters for geometries and specific attributes. + Send a POST request with a Polygon payload, + including a filter for geometry type and + specific tags and attributes. + Expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -664,6 +813,14 @@ def test_snapshot_and_filter(): def test_snapshot_authentication_uuid(): + """ + Test creating a snapshot with a valid access token and 'uuid' as False. + Send a POST request with a Polygon payload + and headers including access token. + Expect a status code of 200 or 403. + If 200, wait for the task to complete. + If 403, raise an exception. + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -683,13 +840,24 @@ def test_snapshot_authentication_uuid(): response = client.post("/v1/snapshot/", json=payload, headers=headers) - assert response.status_code == 200 - res = response.json() - track_link = res["track_link"] - wait_for_task_completion(track_link) + assert response.status_code in [200, 403], f"Unexpected status code: {response.status_code}" + if response.status_code == 200: + res = response.json() + track_link = res["track_link"] + wait_for_task_completion(track_link) + elif response.status_code == 403: + # Handle the 403 response accordingly + raise Exception("Access Forbidden: You may not have permission to access this resource.") def test_snapshot_bind_zip(): + """ + Test creating a snapshot with a valid access token and 'bindZip' as False. + Send a POST request with a Polygon payload, + and headers including access token and 'bindZip' parameter. + Expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -719,6 +887,12 @@ def test_snapshot_bind_zip(): def test_snapshot_plain(): + """ + Test creating a snapshot of a Polygon geometry. + Send a POST request with a Polygon payload and + don't include any headers or authentication. + Expect a 200 status code indicating a successful snapshot creation. + """ response = client.post( "/v1/snapshot/plain/", json={ @@ -743,6 +917,17 @@ def test_snapshot_plain(): def test_stats_endpoint_custom_polygon(): + """ + Test obtaining statistics for a custom polygon with valid access + token. + + Send a POST request to the "/v1/stats/polygon/" endpoint, + including "access-token" as a header and a Polygon payload. + + Assert that: + - the status code is 200 + - the response contains the correct indicators property + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -768,8 +953,18 @@ def test_stats_endpoint_custom_polygon(): == "https://github.com/hotosm/raw-data-api/tree/develop/docs/src/stats/indicators.md" ) - def test_stats_endpoint_iso3(): + """ + Test obtaining statistics for an ISO 3166-1 3-letter code (npl) + + Send a POST request to the "/v1/stats/polygon/" endpoint, + including "access-token" as a header and an ISO 3166-1 3-letter + country code ("npl") in the request body. + + Assert that: + - the status code is 200 + - the response contains the correct indicators property + """ headers = {"access-token": access_token} payload = {"iso3": "npl"} @@ -782,11 +977,23 @@ def test_stats_endpoint_iso3(): == "https://github.com/hotosm/raw-data-api/tree/develop/docs/src/stats/indicators.md" ) - # HDX def test_hdx_submit_normal_iso3(): + """ + Test submitting an HDX custom snapshot request for ISO3 code. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "iso3": "NPL", @@ -814,8 +1021,21 @@ def test_hdx_submit_normal_iso3(): track_link = res["track_link"] wait_for_task_completion(track_link) - def test_hdx_submit_normal_iso3_multiple_format(): + """ + Test submitting an HDX custom snapshot request for ISO3 code + with multiple data formats. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "iso3": "NPL", @@ -843,8 +1063,20 @@ def test_hdx_submit_normal_iso3_multiple_format(): track_link = res["track_link"] wait_for_task_completion(track_link) - def test_hdx_submit_normal_custom_polygon(): + """ + Test submitting an HDX custom snapshot request for a custom polygon. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -891,6 +1123,20 @@ def test_hdx_submit_normal_custom_polygon(): def test_custom_submit_normal_custom_polygon_TM_project(): + """ + Test uploading a custom snapshot for a Tasking Manager Project + on a custom polygon. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -982,8 +1228,21 @@ def test_custom_submit_normal_custom_polygon_TM_project(): track_link = res["track_link"] wait_for_task_completion(track_link) - def test_hdx_submit_normal_custom_polygon_upload(): + """ + Test submitting an HDX custom snapshot request with custom polygon + and upload option enabled. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -1029,8 +1288,21 @@ def test_hdx_submit_normal_custom_polygon_upload(): track_link = res["track_link"] wait_for_task_completion(track_link) - def test_full_hdx_set_iso(): + """ + Test uploading a full HDX dataset for an ISO 3166-1 alpha-3 code + and its geodata. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "iso3": "NPL", @@ -1314,10 +1586,13 @@ def test_full_hdx_set_iso(): track_link = res["track_link"] wait_for_task_completion(track_link) - # ## Tasks connection def test_worker_connection(): + """ + Tests the connection between the API and the worker. + This test sends a request to the API's ping endpoint. If the API is connected to the worker, it will return a 200 status code. + """ response = client.get("/v1/tasks/ping/") assert response.status_code == 200 From 1a2f40ab3328b8cbca6d4c504d485bd002760df8 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 26 Mar 2024 21:57:14 +0300 Subject: [PATCH 17/28] Added docstrings to describe test_app.py test functions --- tests/test_app.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_app.py b/tests/test_app.py index 087609f7..0b0bd530 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -22,6 +22,13 @@ def test_rawdata_current_snapshot_geometry_query(): + """ + Test the raw data current snapshot query on a Polygon geometry with specific + filters for tags and attributes, using ST_Intersects as the geometry + comparison method. + + This test covers a specific scenario with a handcrafted query result. + """ test_param = { "geometry": { "type": "Polygon", @@ -72,6 +79,13 @@ def test_rawdata_current_snapshot_geometry_query(): def test_rawdata_current_snapshot_normal_query(): + """ + Test the raw data current snapshot query on a Polygon geometry without + specific filters for tags and attributes, using ST_Intersects as the + geometry comparison method. + + This test covers a basic scenario with a handcrafted query result. + """ test_param = { "geometry": { "type": "Polygon", @@ -117,6 +131,14 @@ def test_rawdata_current_snapshot_normal_query(): def test_rawdata_current_snapshot_normal_query_ST_within(): + """ + Test the raw data current snapshot query on a Polygon geometry without + specific filters for tags and attributes, using ST_Within as the + geometry comparison method. + + This test covers a scenario similar to the other normal query tests with + ST_Within instead of ST_Intersects. + """ test_param = { "geometry": { "type": "Polygon", @@ -161,6 +183,14 @@ def test_rawdata_current_snapshot_normal_query_ST_within(): def test_attribute_filter_rawdata(): + """ + Test the raw data current snapshot query on a Polygon geometry with + specific attributes filter and tags filter with ST_Intersects and + ST_Union, and using a specific grid ID list. + + This test covers a complex scenario with multiple conditions and + a specific grid ID list. + """ test_param = { "geometry": { "type": "Polygon", @@ -212,6 +242,14 @@ def test_attribute_filter_rawdata(): def test_and_filters(): + """ + Test the raw data current snapshot query on a Polygon geometry with + complex conditions for tags filter using multiple join_and, + ST_Intersects and ST_Union. + + This test covers a specific scenario with multiple join_and clauses + for joined tags. + """ test_param = { "fileName": "Destroyed_Buildings_Turkey", "geometry": { From 742cac6f55334c2b3b8892cff790f8844693d91a Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 26 Mar 2024 23:26:25 +0300 Subject: [PATCH 18/28] Add documentation on how to set env variables in docker and how to run tests in docker --- docs/src/installation/docker.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/src/installation/docker.md b/docs/src/installation/docker.md index 29cb0308..61a0c4fe 100644 --- a/docs/src/installation/docker.md +++ b/docs/src/installation/docker.md @@ -45,6 +45,27 @@ You can either use full composed docker-compose directly or you can build docker docker-compose up -d --build ``` +### Setting Environment Variables in Docker Container + +To configure the environment variables `PYTHONPATH` and `ACCESS_TOKEN` in your Docker container, follow the steps below: + +#### PYTHONPATH + +- The `PYTHONPATH` environment variable specifies the directories where Python looks for modules and packages. +- In the context of your Docker container, setting `PYTHONPATH` to the present working directory (`pwd`) means that Python will search for modules and packages in the current directory. +- This is particularly useful when you have custom modules or packages in your project that you want Python to recognize and import. + +#### ACCESS_TOKEN + +- `ACCESS_TOKEN` is an environment variable used for authentication purposes. +- To obtain the `ACCESS_TOKEN`, you need to generate a login URL for authentication using an OAuth2 application registered with OpenStreetMap. +- Click on the generated URL, which will redirect you to the OpenStreetMap authentication page. +- After logging in and authorizing the OAuth2 application, OpenStreetMap will provide an `ACCESS_TOKEN`. +- Set the obtained `ACCESS_TOKEN` as an environment variable in the Docker container. +- This allows your application to use the token for making authenticated requests to OpenStreetMap APIs. + +In summary, configuring the `PYTHONPATH` to the present working directory enables Python to find modules and packages in your project, while obtaining the `ACCESS_TOKEN` involves generating a login URL for OAuth2 authentication with OpenStreetMap and setting the resulting token as an environment variable in the Docker container for authentication purposes. + OR ### Run Docker without docker compose for development From 29765a86679221ad38512f007ebf04fbf5144edc Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 26 Mar 2024 23:27:31 +0300 Subject: [PATCH 19/28] How to run Tests in Docker Container --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index b97ae50e..460a98d2 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,38 @@ py.test -v -s py.test -k test function name ``` +### Running Tests in Docker Container + +To run tests in Docker locally, follow these steps: + +1. **Update .dockerignore File:** + - Open the `.dockerignore` file in your project. + - Comment out the line that excludes the `tests` directory. This allows Docker to include the test files in the container. + +2. **Modify Dockerfile:** + - Navigate to your Dockerfile. + - Add the following line to copy the tests directory into the Docker container: + ``` + COPY tests/ ./tests/ + ``` + +3. **Spin up the Containers:** + - Use Docker Compose to manage the containers. + - Run the following command to build and start the containers in detached mode: + ``` + docker-compose up -d --build + ``` +4. **Run a Bash Shell Inside the Container:** + + - To access a Bash shell inside a Docker container, use the following command: + +```docker exec -it CONTAINER_NAME /bin/bash``` +Then Run the above commands to run tests + +By following these steps, you'll be able to run your tests inside a Docker container locally. Make sure to check the test results to ensure everything is working as expected. + +**Note:** Make sure you have exported the `PYTHONPATH` and `ACCESS_TOKEN` environment variables in your terminal before running the tests. For detailed instructions on installation using Docker, refer to the [Docker Installation Guide: Setting Environment Variables in Docker Container](https://github.com/hotosm/raw-data-api/blob/develop/docs/src/installation/docker.md). + ## Contribution & Development Learn about current priorities and work going through Roadmap & see here [CONTRIBUTING](./docs/src/contributing.md) From e3b6c81a88a8d17ac17f8f16b07d2c7678f27282 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Fri, 29 Mar 2024 11:04:03 +0300 Subject: [PATCH 20/28] Added 401 and 500 responses and content --- API/tasks.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/API/tasks.py b/API/tasks.py index 85ac11e6..16c4d298 100644 --- a/API/tasks.py +++ b/API/tasks.py @@ -17,7 +17,10 @@ router = APIRouter(prefix="/tasks", tags=["Tasks"]) -@router.get("/status/{task_id}", response_model=SnapshotTaskResponse) +@router.get("/status/{task_id}", response_model=SnapshotTaskResponse, responses= + { + **common_error_responses, **error_responses_with_examples + }) @version(1) def get_task_status( task_id, @@ -96,7 +99,10 @@ def revoke_task(task_id, user: AuthUser = Depends(staff_required)): return JSONResponse({"id": task_id}) -@router.get("/inspect") +@router.get("/inspect", responses= + { + **common_error_responses, **error_responses_with_examples + }) @version(1) def inspect_workers( request: Request, @@ -137,7 +143,10 @@ def inspect_workers( return JSONResponse(content=response_data) -@router.get("/ping") +@router.get("/ping", responses= + { + **common_error_responses, **error_responses_with_examples + }) @version(1) def ping_workers(): """Pings available workers @@ -148,7 +157,10 @@ def ping_workers(): return JSONResponse(inspected_ping) -@router.get("/purge", responses={403: {"description": "Forbidden", "content": {"application/json": {"example": {"message": "Access forbidden"}}}}}) +@router.get("/purge", responses= + { + **common_error_responses, **error_responses_with_examples + }) @version(1) def discard_all_waiting_tasks(user: AuthUser = Depends(admin_required)): """ @@ -166,7 +178,10 @@ def discard_all_waiting_tasks(user: AuthUser = Depends(admin_required)): queues = [DEFAULT_QUEUE_NAME, DAEMON_QUEUE_NAME] -@router.get("/queue") +@router.get("/queue", responses= + { + **common_error_responses, **error_responses_with_examples + }) @version(1) def get_queue_info(): queue_info = {} @@ -183,7 +198,10 @@ def get_queue_info(): return JSONResponse(content=queue_info) -@router.get("/queue/details/{queue_name}") +@router.get("/queue/details/{queue_name}", responses= + { + **common_error_responses, **error_responses_with_examples + }) @version(1) def get_list_details( queue_name: str, From 12073baaa8f9a366b1cee2bc95007d247525dd63 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Fri, 29 Mar 2024 11:04:46 +0300 Subject: [PATCH 21/28] Added 401 and 500 responses and content --- API/raw_data.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/API/raw_data.py b/API/raw_data.py index aac5e6bc..0fee5410 100644 --- a/API/raw_data.py +++ b/API/raw_data.py @@ -26,6 +26,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Request from fastapi.responses import JSONResponse from fastapi_versioning import version +from .auth.responses import common_error_responses, error_responses_with_examples from src.app import RawData from src.config import ( @@ -50,7 +51,10 @@ redis_client = redis.StrictRedis.from_url(CELERY_BROKER_URL) -@router.get("/status", response_model=StatusResponse) +@router.get("/status", response_model=StatusResponse, responses= + { + **common_error_responses, **error_responses_with_examples + }) @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""" @@ -58,7 +62,10 @@ def check_database_last_updated(): return {"last_updated": result} -@router.post("/snapshot", response_model=SnapshotResponse) +@router.post("/snapshot", response_model=SnapshotResponse, responses= + { + **common_error_responses, **error_responses_with_examples + }) @limiter.limit(f"{export_rate_limit}/minute") @version(1) def get_osm_current_snapshot_as_file( @@ -461,7 +468,10 @@ def get_osm_current_snapshot_as_file( ) -@router.post("/snapshot/plain") +@router.post("/snapshot/plain", responses= + { + **common_error_responses, **error_responses_with_examples + }) @version(1) def get_osm_current_snapshot_as_plain_geojson( request: Request, @@ -493,14 +503,20 @@ def get_osm_current_snapshot_as_plain_geojson( return result -@router.get("/countries") +@router.get("/countries", responses= + { + **common_error_responses, **error_responses_with_examples + }) @version(1) def get_countries(q: str = ""): result = RawData().get_countries_list(q) return result -@router.get("/osm_id") +@router.get("/osm_id", responses= + { + **common_error_responses, **error_responses_with_examples + }) @version(1) def get_osm_feature(osm_id: int): return RawData().get_osm_feature(osm_id) From 1de495b418d232c0b08498e304fefb741f07b1e4 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Fri, 29 Mar 2024 11:05:52 +0300 Subject: [PATCH 22/28] Added 401 and 500 responses and content in routers.py --- API/auth/routers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/API/auth/routers.py b/API/auth/routers.py index 7bb1be30..d1f63158 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", responses={**common_error_responses, **error_responses_with_examples}) 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. @@ -51,7 +51,10 @@ def callback(request: Request): return access_token -@router.get("/me", response_model=AuthUser) +@router.get("/me", response_model=AuthUser, responses= + { + **common_error_responses, **error_responses_with_examples + }) 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 . From 3d72f98ac45a081beb89d914287c4961e245e079 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 2 Apr 2024 12:41:41 +0300 Subject: [PATCH 23/28] HDX file in black format --- API/hdx.py | 1 + 1 file changed, 1 insertion(+) diff --git a/API/hdx.py b/API/hdx.py index 8b3b1445..36618d51 100644 --- a/API/hdx.py +++ b/API/hdx.py @@ -69,6 +69,7 @@ async def read_hdx_list( raise HTTPException(status_code=422, detail="Couldn't process query") return hdx_list + @router.get("/search", response_model=List[dict]) @limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute") @version(1) From 5d50d4bbc150f0397ebcd48d9cc1fce0dc83bc4b Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 2 Apr 2024 12:42:58 +0300 Subject: [PATCH 24/28] tasks file in black format --- API/tasks.py | 55 ++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/API/tasks.py b/API/tasks.py index 16c4d298..6ec90e57 100644 --- a/API/tasks.py +++ b/API/tasks.py @@ -17,10 +17,12 @@ router = APIRouter(prefix="/tasks", tags=["Tasks"]) -@router.get("/status/{task_id}", response_model=SnapshotTaskResponse, responses= - { - **common_error_responses, **error_responses_with_examples - }) + +@router.get( + "/status/{task_id}", + response_model=SnapshotTaskResponse, + responses={**common_error_responses, **error_responses_with_examples}, +) @version(1) def get_task_status( task_id, @@ -82,7 +84,10 @@ def get_task_status( return JSONResponse(result) -@router.get("/revoke/{task_id}", responses={**common_error_responses, **error_responses_with_examples}) +@router.get( + "/revoke/{task_id}", + responses={**common_error_responses, **error_responses_with_examples}, +) @version(1) def revoke_task(task_id, user: AuthUser = Depends(staff_required)): """Revokes task , Terminates if it is executing @@ -99,10 +104,9 @@ def revoke_task(task_id, user: AuthUser = Depends(staff_required)): return JSONResponse({"id": task_id}) -@router.get("/inspect", responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/inspect", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def inspect_workers( request: Request, @@ -143,10 +147,9 @@ def inspect_workers( return JSONResponse(content=response_data) -@router.get("/ping", responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/ping", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def ping_workers(): """Pings available workers @@ -157,19 +160,18 @@ def ping_workers(): return JSONResponse(inspected_ping) -@router.get("/purge", responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/purge", responses={**common_error_responses, **error_responses_with_examples} +) @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 the user is not an Admin. - + """ purged = celery.control.purge() return JSONResponse({"tasks_discarded": purged}) @@ -178,10 +180,9 @@ def discard_all_waiting_tasks(user: AuthUser = Depends(admin_required)): queues = [DEFAULT_QUEUE_NAME, DAEMON_QUEUE_NAME] -@router.get("/queue", responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/queue", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def get_queue_info(): queue_info = {} @@ -198,10 +199,10 @@ def get_queue_info(): return JSONResponse(content=queue_info) -@router.get("/queue/details/{queue_name}", responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/queue/details/{queue_name}", + responses={**common_error_responses, **error_responses_with_examples}, +) @version(1) def get_list_details( queue_name: str, From 8e3a666673f45f01035f629c876ac67269bf20d1 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 2 Apr 2024 12:44:15 +0300 Subject: [PATCH 25/28] S3 file in black format --- API/s3.py | 1 + 1 file changed, 1 insertion(+) diff --git a/API/s3.py b/API/s3.py index d74e43e8..708cc52d 100644 --- a/API/s3.py +++ b/API/s3.py @@ -31,6 +31,7 @@ ) paginator = s3.get_paginator("list_objects_v2") + @router.get("/files") @limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute") @version(1) From 556f36e66f1bb80b241a0ee941a6c5e9d38f0be8 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 2 Apr 2024 12:45:36 +0300 Subject: [PATCH 26/28] stats file in black format --- API/stats.py | 1 + 1 file changed, 1 insertion(+) diff --git a/API/stats.py b/API/stats.py index 8185fc4a..c8ebd6ee 100644 --- a/API/stats.py +++ b/API/stats.py @@ -10,6 +10,7 @@ router = APIRouter(prefix="/stats", tags=["Stats"]) + @router.post("/polygon") @limiter.limit(f"{POLYGON_STATISTICS_API_RATE_LIMIT}/minute") @version(1) From 2a302fb40a9bc1142757c070ec34d18eef6426fb Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 2 Apr 2024 12:46:53 +0300 Subject: [PATCH 27/28] routers file in black format --- API/auth/routers.py | 70 ++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/API/auth/routers.py b/API/auth/routers.py index d1f63158..05e18476 100644 --- a/API/auth/routers.py +++ b/API/auth/routers.py @@ -10,7 +10,10 @@ router = APIRouter(prefix="/auth", tags=["Auth"]) -@router.get("/login", responses={**common_error_responses, **error_responses_with_examples}) + +@router.get( + "/login", responses={**common_error_responses, **error_responses_with_examples} +) 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,10 +28,9 @@ def login_url(request: Request): return login_url -@router.get("/callback", responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/callback", responses={**common_error_responses, **error_responses_with_examples} +) def callback(request: Request): """Performs token exchange between OpenStreetMap and Raw Data API @@ -44,17 +46,18 @@ def callback(request: Request): access_token = osm_auth.callback(str(request.url)) except Exception as ex: raise HTTPException( - status_code=500, - detail="Internal Server Error occurred while performing token exchange between OpenStreetMap and Raw Data API" + status_code=500, + detail="Internal Server Error occurred while performing token exchange between OpenStreetMap and Raw Data API", ) return access_token -@router.get("/me", response_model=AuthUser, responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/me", + response_model=AuthUser, + responses={**common_error_responses, **error_responses_with_examples}, +) 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 . @@ -76,9 +79,11 @@ class User(BaseModel): # Create user -@router.post("/users", response_model=dict, - responses={**common_error_responses, **error_responses_with_examples} - ) +@router.post( + "/users", + response_model=dict, + responses={**common_error_responses, **error_responses_with_examples}, +) async def create_user(params: User, user_data: AuthUser = Depends(admin_required)): """ Creates a new user and returns the user's information. @@ -101,10 +106,11 @@ 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, - responses={ - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/users/{osm_id}", + response_model=dict, + responses={**common_error_responses, **error_responses_with_examples}, +) async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): """ Retrieves user information based on the given osm_id. @@ -129,10 +135,11 @@ 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, - responses= { - **common_error_responses, **error_responses_with_examples - }) +@router.put( + "/users/{osm_id}", + response_model=dict, + responses={**common_error_responses, **error_responses_with_examples}, +) async def update_user( osm_id: int, update_data: User, user_data: AuthUser = Depends(admin_required) ): @@ -158,10 +165,11 @@ async def update_user( # Delete user by osm_id -@router.delete("/users/{osm_id}", response_model=dict, - responses={ - **common_error_responses, **error_responses_with_examples - }) +@router.delete( + "/users/{osm_id}", + response_model=dict, + responses={**common_error_responses, **error_responses_with_examples}, +) async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required)): """ Deletes a user based on the given osm_id. @@ -181,9 +189,11 @@ async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required) # Get all users -@router.get("/users", response_model=list, - responses={**common_error_responses, **error_responses_with_examples} - ) +@router.get( + "/users", + response_model=list, + responses={**common_error_responses, **error_responses_with_examples}, +) async def read_users( skip: int = 0, limit: int = 10, user_data: AuthUser = Depends(staff_required) ): @@ -196,7 +206,7 @@ async def read_users( Returns: - List[Dict[str, Any]]: A list of dictionaries containing user information. - + Raises: - HTTPException 403: If the user is not a Staff. """ From bf222dda7f594bb98704346db1907cb03a0584c1 Mon Sep 17 00:00:00 2001 From: Jemimah Mmboga Date: Tue, 2 Apr 2024 12:50:56 +0300 Subject: [PATCH 28/28] raw_data file in black format --- API/raw_data.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/API/raw_data.py b/API/raw_data.py index 0fee5410..612c2e92 100644 --- a/API/raw_data.py +++ b/API/raw_data.py @@ -51,10 +51,12 @@ redis_client = redis.StrictRedis.from_url(CELERY_BROKER_URL) -@router.get("/status", response_model=StatusResponse, responses= - { - **common_error_responses, **error_responses_with_examples - }) + +@router.get( + "/status", + response_model=StatusResponse, + responses={**common_error_responses, **error_responses_with_examples}, +) @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""" @@ -62,10 +64,11 @@ def check_database_last_updated(): return {"last_updated": result} -@router.post("/snapshot", response_model=SnapshotResponse, responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.post( + "/snapshot", + response_model=SnapshotResponse, + responses={**common_error_responses, **error_responses_with_examples}, +) @limiter.limit(f"{export_rate_limit}/minute") @version(1) def get_osm_current_snapshot_as_file( @@ -468,10 +471,10 @@ def get_osm_current_snapshot_as_file( ) -@router.post("/snapshot/plain", responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.post( + "/snapshot/plain", + responses={**common_error_responses, **error_responses_with_examples}, +) @version(1) def get_osm_current_snapshot_as_plain_geojson( request: Request, @@ -503,20 +506,18 @@ def get_osm_current_snapshot_as_plain_geojson( return result -@router.get("/countries", responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/countries", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def get_countries(q: str = ""): result = RawData().get_countries_list(q) return result -@router.get("/osm_id", responses= - { - **common_error_responses, **error_responses_with_examples - }) +@router.get( + "/osm_id", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def get_osm_feature(osm_id: int): return RawData().get_osm_feature(osm_id)