Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raw Data API Improvement #238

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
96dc935
Remove back slash
KafayatYusuf Mar 22, 2024
f42a967
Removed backslash
KafayatYusuf Mar 22, 2024
12b3bee
Removed trailing slash at the Auth endpoint
KafayatYusuf Mar 22, 2024
fa4d70d
Added google docstrings to Auth endpoint
KafayatYusuf Mar 22, 2024
ab6232b
Corrected grammatical errors and punctuations
KafayatYusuf Mar 22, 2024
0411b3d
Corrected spelling errors and punctuation
KafayatYusuf Mar 22, 2024
896c6e7
Added status response codes
KafayatYusuf Mar 22, 2024
dee822a
Deleted an import function
KafayatYusuf Mar 22, 2024
8f9a8f3
Removed my client ID and secret
KafayatYusuf Mar 22, 2024
8311cdc
Updated the README Markdown
KafayatYusuf Mar 23, 2024
4961a38
Updated the google docstrings used in classes and functions
KafayatYusuf Mar 23, 2024
38c365d
Updated the manual markdown
KafayatYusuf Mar 23, 2024
1d12047
Fixed indentation error after else
KafayatYusuf Mar 23, 2024
0a82ea4
Created a directory called "error_module" in src folder to import Err…
KafayatYusuf Mar 23, 2024
fd95ee8
Imported ErrorMessage from src
KafayatYusuf Mar 23, 2024
b525565
Imported some libraries in the terminal
KafayatYusuf Mar 23, 2024
bac89c5
Added google docstring to function
KafayatYusuf Mar 23, 2024
979962e
Added docstrings to login endpoint
KafayatYusuf Mar 23, 2024
ebf4cdf
Remove trailing slashes from extract endpoint
KafayatYusuf Mar 23, 2024
db61bfb
Removed trailing slashes in tasks endpoint
KafayatYusuf Mar 23, 2024
20a1697
Removed trailing slash from stats endpoint
KafayatYusuf Mar 23, 2024
43409bb
Removed the trailing slashes that were not taken out from tasks endpo…
KafayatYusuf Mar 23, 2024
193f4f3
Updated status response codes on auth endpoint
KafayatYusuf Mar 23, 2024
1ff73e4
Added docker-compose-config.txt to gitignore so as not make changes t…
KafayatYusuf Mar 24, 2024
905d81e
Cleared osm client id and secret as it was showing from the previous …
KafayatYusuf Mar 24, 2024
1312f95
Removed docker-compose-config from it as it didn't reflect the change…
KafayatYusuf Mar 24, 2024
d25b30f
Changed 200 status response code from 'OK' to 'Successful Response'
KafayatYusuf Mar 24, 2024
685116c
Changed auth login response code from 'OK' to 'Successful Response'
KafayatYusuf Mar 24, 2024
edff361
Removed trailing slash from custom exports endpoint
KafayatYusuf Mar 24, 2024
0ac9718
Removed trailing slash hdx endpoint
KafayatYusuf Mar 24, 2024
e6a776f
Added API description to the app
KafayatYusuf Mar 25, 2024
9bf9417
Added security feature to endpoints
KafayatYusuf Mar 26, 2024
e7a23c9
Updated the security feature
KafayatYusuf Mar 27, 2024
462144c
Corrected manual markdown grammatical speling error
KafayatYusuf Mar 27, 2024
8fdf9a8
Updated json _schema_extra
KafayatYusuf Mar 27, 2024
f0aff07
Updated schema example
KafayatYusuf Mar 27, 2024
3090b5e
Added a non-empty "tags" array
KafayatYusuf Mar 27, 2024
8fb9fea
Shortened operationId to be less than 50 characters
KafayatYusuf Mar 27, 2024
1f41db8
Updated status response codes
KafayatYusuf Mar 28, 2024
7278a4b
I imported the request library and wrote a display message
KafayatYusuf Mar 29, 2024
b5ccf6d
Updated the welcome message
KafayatYusuf Mar 29, 2024
5bb48ea
Added color compose
KafayatYusuf Mar 29, 2024
ab14d53
Changed download_url to downloadUrl
KafayatYusuf Mar 30, 2024
d912924
Included downloadUrl within "result" object
KafayatYusuf Mar 31, 2024
e90512e
Fixed a syntax error
KafayatYusuf Mar 31, 2024
700634b
Added the other keywords to the required property
KafayatYusuf Mar 31, 2024
e027067
Added the missing responses to the responses object
KafayatYusuf Apr 1, 2024
d544ee2
Added an item to the "tags" array
KafayatYusuf Apr 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ Pipfile.lock
#backend
backend/data
backend/.env

6 changes: 3 additions & 3 deletions API/api_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,14 @@ def process_raw_data(self, params, user=None):
if iso3countrycode:
upload_name = f"HDX/{iso3countrycode.upper()}/{exportname}"

download_url = file_transfer_obj.upload(
downloadUrl = file_transfer_obj.upload(
upload_file_path,
upload_name,
file_suffix="zip" if bind_zip else params.output_type.lower(),
)
else:
# give the static file download url back to user served from fastapi static export path
download_url = str(upload_file_path)
downloadUrl = str(upload_file_path)

# getting file size of zip , units are in bytes converted to mb in response
zip_file_size = os.path.getsize(upload_file_path)
Expand All @@ -258,7 +258,7 @@ def process_raw_data(self, params, user=None):
f"Done Export : {exportname} of {round(inside_file_size/1000000)} MB / {geom_area} sqkm in {response_time_str}"
)
final_response = {
"download_url": download_url,
"downloadUrl": downloadUrl,
"file_name": params.file_name,
"process_time": response_time_str,
"query_area": f"{round(geom_area,2)} Sq Km",
Expand Down
18 changes: 17 additions & 1 deletion API/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@
from typing import Union

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

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

API_access_token = APIKeyHeader(name="access_token", description="user_authentication")


class UserRole(Enum):
"""Assigning user roles as integer"""

ADMIN = 1
STAFF = 2
GUEST = 3


class AuthUser(BaseModel):
"""Defining fields as attributes"""

id: int
username: str
img_url: Union[str, None]
Expand All @@ -26,12 +33,14 @@ class AuthUser(BaseModel):


def get_user_from_db(osm_id: int):
"""Get user's information (osm_id)"""
auth = Users()
user = auth.read_user(osm_id)
return user


def get_osm_auth_user(access_token):
"""Get user's access token"""
try:
user = AuthUser(**osm_auth.deserialize_access_token(access_token))
except Exception as ex:
Expand All @@ -43,11 +52,16 @@ def get_osm_auth_user(access_token):
return user


def login_required(access_token: str = Header(...)):
def login_required(access_token: str = Depends(API_access_token)):
"""Get user's login details"""
if access_token != "my_token":
raise HTTPException(status_code=401, detail="Invalid API Key")

return get_osm_auth_user(access_token)


def get_optional_user(access_token: str = Header(default=None)) -> AuthUser:
"""Get user's access token which is optional"""
if access_token:
return get_osm_auth_user(access_token)
else:
Expand All @@ -56,13 +70,15 @@ def get_optional_user(access_token: str = Header(default=None)) -> AuthUser:


def admin_required(user: AuthUser = Depends(login_required)):
"""Get admin login details"""
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")
return user


def staff_required(user: AuthUser = Depends(login_required)):
"""Get staff login details"""
db_user = get_user_from_db(user.id)

# admin is staff too
Expand Down
90 changes: 75 additions & 15 deletions API/auth/routers.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,62 @@
import json


from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel

from src.app import Users
from src.error_module import ErrorMessage, common_responses, login_responses

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

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


@router.get("/login/")
class ErrorMessage(BaseModel):
detail: str


responses = {
200: {
"description": "Successful Response",
"content": {"application/json": {"example": {"detail": "Successful"}}},
},
400: {
"description": "Bad Request",
"content": {"application/json": {"example": {"detail": "Bad Request"}}},
},
401: {
"description": "Unauthorized",
"content": {"application/json": {"example": {"detail": "Unauthorized"}}},
},
403: {
"description": "Forbidden",
"content": {"application/json": {"example": {"detail": "Forbidden"}}},
},
408: {
"description": "Request Timeout",
"content": {"application/json": {"example": {"detail": "Request Timeout"}}},
},
422: {
"description": "Validation Error",
"content": {"application/json": {"example": {"detail": "Validation Error"}}},
},
500: {
"description": "Internal Server Error",
"content": {
"application/json": {"example": {"detail": "Internal Server Error"}}
},
},
}


@router.get(
"/login",
responses={
500: {"model": ErrorMessage},
200: {"content": {"application/json": {"example": {"loginUrl": "Successful"}}}},
},
)
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.
Expand All @@ -25,7 +71,7 @@ def login_url(request: Request):
return login_url


@router.get("/callback/")
@router.get("/callback", responses={500: {"model": ErrorMessage}})
def callback(request: Request):
"""Performs token exchange between OpenStreetMap and Raw Data API

Expand All @@ -38,14 +84,12 @@ def callback(request: Request):
- access_token (string)
"""
access_token = osm_auth.callback(str(request.url))

return access_token


@router.get("/me/", response_model=AuthUser)
@router.get("/me", response_model=AuthUser, responses={**responses})
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 .
"""Read the access token and provide user details from OSM user's API endpoint, also integrated with underpass.

Parameters:None

Expand All @@ -59,12 +103,14 @@ def my_data(user_data: AuthUser = Depends(login_required)):


class User(BaseModel):
"""Model representing a user"""

osm_id: int
role: int


# Create user
@router.post("/users/", response_model=dict)
@router.post("/users", response_model=dict, responses={**responses})
async def create_user(params: User, user_data: AuthUser = Depends(admin_required)):
"""
Creates a new user and returns the user's information.
Expand All @@ -80,14 +126,15 @@ 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 has unauthorized access
- HTTPException 500: If the access is denied 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)
@router.get("/users{osm_id}", response_model=dict, responses={**responses})
async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)):
"""
Retrieves user information based on the given osm_id.
Expand All @@ -103,15 +150,20 @@ 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 unauthorized access
- HTTPException 500: If the access is denied due to internal server error
"""
auth = Users()

return auth.read_user(osm_id)


# Update user by osm_id
@router.put("/users/{osm_id}", response_model=dict)
@router.put(
"/users{osm_id}",
response_model=dict,
responses={**responses, 403: {"model": ErrorMessage}},
)
async def update_user(
osm_id: int, update_data: User, user_data: AuthUser = Depends(admin_required)
):
Expand All @@ -129,14 +181,22 @@ 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 unauthorized access.
- HTTPException 404: If the user cannot be found.
- HTTPException 408: If the access is denied due to request timeout
- HTTPException 500: If access is denied 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)
@router.delete(
"/users{osm_id}",
response_model=dict,
responses={**responses, 404: {"model": ErrorMessage}},
)
async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required)):
"""
Deletes a user based on the given osm_id.
Expand All @@ -148,14 +208,14 @@ 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.
"""
auth = Users()
return auth.delete_user(osm_id)


# Get all users
@router.get("/users/", response_model=list)
@router.get("/users", response_model=list, responses={**responses})
async def read_users(
skip: int = 0, limit: int = 10, user_data: AuthUser = Depends(staff_required)
):
Expand Down
2 changes: 1 addition & 1 deletion API/custom_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion API/hdx.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async def read_hdx_list(
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(
Expand Down
21 changes: 20 additions & 1 deletion API/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@
# Humanitarian OpenStreetmap Team
# 1100 13th Street NW Suite 800 Washington, D.C. 20005
# <[email protected]>

from pip._vendor import requests

welcome_msg = (
welcome_msg
) = """Welcome to the HOTOSM raw data API!
Your one-stop API which is a set of high-performant APIs (Application Programming Interfaces) for transforming and exporting OpenStreetMap (OSM) data in different GIS file formats."""


class ConsoleColors:
BOLD = "\033[1m"
RESET = "\033[0m"
GREEN = "\033[92m"


import time

import psycopg2
Expand Down Expand Up @@ -75,7 +90,11 @@

os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"

app = FastAPI(title="Raw Data API ", swagger_ui_parameters={"syntaxHighlight": False})
app = FastAPI(
title="Raw Data API ",
description="RAW Data API (Application Programming Interface) helps 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)
Expand Down
10 changes: 5 additions & 5 deletions API/raw_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@
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"""
result = RawData().check_status()
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(
Expand Down Expand Up @@ -464,7 +464,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,
Expand Down Expand Up @@ -496,14 +496,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)
Loading