Skip to content

Commit

Permalink
WIP routing
Browse files Browse the repository at this point in the history
  • Loading branch information
tijsziere committed Sep 27, 2024
1 parent 5f4651f commit b668530
Show file tree
Hide file tree
Showing 9 changed files with 891 additions and 1,063 deletions.
1,072 changes: 9 additions & 1,063 deletions main.py

Large diffs are not rendered by default.

413 changes: 413 additions & 0 deletions routes/121_routes.py

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions routes/espo_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from fastapi import APIRouter, Request, Depends, HTTPException
from fastapi.responses import JSONResponse
from utils.kobo_utils import (
add_submission,
clean_kobo_data,
get_attachment_dict,
update_submission_status,
espo_request,
required_headers_espocrm,
required_headers_121,
login121,
)
from utils.espo_utils import espo_request, required_headers_espocrm
import logging
import os

router = APIRouter()

@router.post("/kobo-to-espocrm")
async def kobo_to_espocrm(
request: Request, dependencies=Depends(required_headers_espocrm)
):
"""Send a Kobo submission to EspoCRM."""
# ... existing code ...
return JSONResponse(status_code=200, content=target_response)

@router.post("/kobo-to-121")
async def kobo_to_121(request: Request, dependencies=Depends(required_headers_121)):
"""Send a Kobo submission to 121."""
# ... existing code ...
return JSONResponse(status_code=import_response.status_code, content=import_response_message)
39 changes: 39 additions & 0 deletions routes/generic_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@router.post("/kobo-to-generic")
async def kobo_to_generic(request: Request):
"""Send a Kobo submission to a generic API.
API Key is passed as 'x-api-key' in headers."""

kobo_data = await request.json()
kobo_data = clean_kobo_data(kobo_data)
attachments = get_attachment_dict(kobo_data)

# Create API payload body
payload = {}
for kobo_field, target_field in request.headers.items():
if kobo_field in kobo_data.keys():
kobo_value = kobo_data[kobo_field].replace(" ", "_")
if kobo_value not in attachments.keys():
payload[target_field] = kobo_value
else:
file_url = attachments[kobo_value]["url"]
if "kobotoken" not in request.headers.keys():
raise HTTPException(
status_code=400,
detail=f"'kobotoken' needs to be specified in headers to upload attachments",
)
# encode attachment in base64
file = get_kobo_attachment(file_url, request.headers["kobotoken"])
file_b64 = base64.b64encode(file).decode("utf8")
payload[target_field] = (
f"data:{attachments[kobo_value]['mimetype']};base64,{file_b64}"
)

# POST to target API
response = requests.post(
request.headers["targeturl"],
headers={"x-api-key": request.headers["targetkey"]},
data=payload,
)
target_response = response.content.decode("utf-8")

return JSONResponse(status_code=200, content=target_response)
190 changes: 190 additions & 0 deletions routes/kobo_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
@router.post("/update-kobo-csv")
async def prepare_kobo_validation(request: Request, programId: int, kobousername: str, dependencies=Depends(required_headers_121_kobo)):
"""
Prepare Kobo validation by fetching data from 121 platform,
converting it to CSV, and uploading to Kobo.
"""
# get access token from cookie
body = {'username': request.headers['username121'], 'password': request.headers['password121']}
url = f"{request.headers['url121']}/api/users/login"
login = requests.post(url, data=body)
if login.status_code >= 400:
raise HTTPException(
status_code=login.status_code,
detail=login.content.decode("utf-8")
)
access_token = login.json()['access_token_general']

# Fetch data from 121 platform
response = requests.get(
f"{request.headers['url121']}/api/programs/{programId}/metrics/export-list/all-people-affected",
headers={'Cookie': f"access_token_general={access_token}"}
)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail="Failed to fetch data from 121 platform")

data = response.json()

# Convert JSON to CSV
output = io.StringIO()
writer = csv.writer(output)

# Ensure we have data to process
if data and 'data' in data and len(data['data']) > 0:
# Get the keys (column names) from the first row
fieldnames = list(data['data'][0].keys())

# Write header
writer.writerow(fieldnames)

# Write rows
for row in data['data']:
# Create a list of values in the same order as fieldnames
row_data = [row.get(field, '') for field in fieldnames]
writer.writerow(row_data)

csv_content = output.getvalue().encode('utf-8')

# Prepare the payload for Kobo
base64_encoded_csv = base64.b64encode(csv_content).decode('utf-8')
metadata = json.dumps({"filename": "ValidationDataFrom121.csv"})

payload = {
"description": "default",
"file_type": "form_media",
"metadata": metadata,
"base64Encoded": f"data:text/csv;base64,{base64_encoded_csv}"
}

# Kobo headers
headers = {
"Authorization": f"Token {request.headers['kobotoken']}",
"Content-Type": "application/x-www-form-urlencoded"
}
#If exists, remove existing ValidationDataFrom121.csv
media_response = requests.get(
f"https://kobo.ifrc.org/api/v2/assets/{request.headers['koboasset']}/files/",
headers=headers
)
if media_response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail="Failed to fetch media from kobo")

media = media_response.json()

# Check if ValidationDataFrom121.csv exists and get its uid
existing_file_uid = None
for file in media.get('results', []):
if file.get('metadata', {}).get('filename') == "ValidationDataFrom121.csv":
existing_file_uid = file.get('uid')
break

# If the file exists, delete it
if existing_file_uid:
delete_response = requests.delete(
f"https://kobo.ifrc.org/api/v2/assets/{request.headers['koboasset']}/files/{existing_file_uid}/",
headers={"Authorization": f"Token {request.headers['kobotoken']}"}
)
if delete_response.status_code != 204:
raise HTTPException(status_code=delete_response.status_code, detail="Failed to delete existing file from Kobo")


upload_response = requests.post(
f"https://kobo.ifrc.org/api/v2/assets/{request.headers['koboasset']}/files/",
headers=headers,
data=payload
)

if upload_response.status_code != 201:
raise HTTPException(status_code=upload_response.status_code, detail="Failed to upload file to Kobo")

# Redeploy the Kobo form
redeploy_url = f"https://kobo.ifrc.org/api/v2/assets/{request.headers['koboasset']}/deployment/"
redeploy_payload = {"active": True}

redeploy_response = requests.patch(
redeploy_url,
headers=headers,
json=redeploy_payload
)

if redeploy_response.status_code != 200:
raise HTTPException(status_code=redeploy_response.status_code, detail="Failed to redeploy Kobo form")


return {"message": "Validation data prepared and uploaded successfully", "kobo_response": upload_response.json()}


###############

class system(str, Enum):
system_generic = "generic"
system_espo = "espocrm"
system_121 = "121"


@router.post("/create-kobo-headers")
async def create_kobo_headers(
json_data: dict,
system: system,
koboassetId: str,
kobotoken: str,
hookId: str = None,
):
"""Utility endpoint to automatically create the necessary headers in Kobo. \n
Does only support the IFRC server kobo.ifrc.org \n
***NB: if you want to duplicate an endpoint, please also use the Hook ID query param***
"""

if json_data is None:
raise HTTPException(status_code=400, detail="JSON data is required")

target_url = f"https://kobo.ifrc.org/api/v2/assets/{koboassetId}/hooks/"
koboheaders = {"Authorization": f"Token {kobotoken}"}

if hookId is None:
payload = {
"name": "koboconnect",
"endpoint": f"https://kobo-connect.azurewebsites.net/kobo-to-{system}",
"active": True,
"subset_fields": [],
"email_notification": True,
"export_type": "json",
"auth_level": "no_auth",
"settings": {"custom_headers": {}},
"payload_template": "",
}

payload["settings"]["custom_headers"] = json_data
else:
get_url = f"https://kobo.ifrc.org/api/v2/assets/{koboassetId}/hooks/{hookId}"
hook = requests.get(get_url, headers=koboheaders)
hook = hook.json()
hook["name"] = "Duplicate of " + hook["name"]

def remove_keys(data, keys_to_remove):
for key in keys_to_remove:
if key in data:
del data[key]
return data

keys_to_remove = [
"url",
"logs_url",
"asset",
"uid",
"success_count",
"failed_count",
"pending_count",
"date_modified",
]
payload = remove_keys(hook, keys_to_remove)

response = requests.post(target_url, headers=koboheaders, json=payload)

if response.status_code == 200 or 201:
return JSONResponse(content={"message": "Sucess"})
else:
return JSONResponse(
content={"message": "Failed to post data to the target endpoint"},
status_code=response.status_code,
)
67 changes: 67 additions & 0 deletions utils/121_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
def clean_text(text):
# Normalize text to remove accents
normalized_text = unicodedata.normalize("NFD", text)
# Remove accents and convert to lowercase
cleaned_text = "".join(
c for c in normalized_text if not unicodedata.combining(c)
).lower()
return cleaned_text


def required_headers_121(
url121: str = Header(), username121: str = Header(), password121: str = Header()
):
return url121, username121, password121

# Dictionary to store cookies, credentials, and expiration times
cookie121 = {}

def login121(url121, username, password):
# Check if URL exists in the dictionary
if url121 in cookie121:
cookie_data = cookie121[url121]
# Check if the stored username and password match
if cookie_data['username'] == username and cookie_data['password'] == password:
cookie_expiry = cookie_data['expiry']
current_time = datetime.utcnow()

# Check if the cookie is valid for at least 24 more hours
if (cookie_expiry - current_time) >= timedelta(hours=24):
logger.info(f"Using cached cookie for {url121}")
return cookie_data['cookie']
else:
logger.info(f"Cookie for {url121} is valid for less than 24 hours, refreshing cookie...")

# Otherwise, request a new cookie
body = {'username': username, 'password': password}
url = f'{url121}/api/users/login'

try:
login_response = requests.post(url, data=body)
login_response.raise_for_status()
except requests.RequestException as e:
error_message = str(e)
logger.error(
f"Failed: 121 login returned {login_response.status_code} {error_message}",
extra=None,
)
raise HTTPException(
status_code=login_response.status_code, detail=error_message
)

# Parse the response
response_data = login_response.json()
cookie = response_data['access_token_general']

# Store the new cookie, username, password, and expiration time in the dictionary
expiry_datetime = datetime.fromisoformat(response_data['expires'].replace("Z", ""))

cookie121[url121] = {
'username': username,
'password': password,
'cookie': cookie,
'expiry': expiry_datetime
}

logger.info(f"New cookie stored for {url121} with credentials.")
return cookie
21 changes: 21 additions & 0 deletions utils/espo_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from clients.espo_api_client import EspoAPI
import requests
import time
import logger
from fastapi import HTTPException
from datetime import datetime, timedelta
from azure.cosmos.exceptions import CosmosResourceExistsError

def espo_request(submission, espo_client, method, entity, params=None, logs=None):
"""Make a request to EspoCRM. If the request fails, update submission status in CosmosDB."""
try:
response = espo_client.request(method, entity, params)
return response
except HTTPException as e:
detail = e.detail if "Unknown Error" not in e.detail else ""
logger.error(f"Failed: EspoCRM returned {e.status_code} {detail}", extra=logs)
update_submission_status(submission, "failed", e.detail)


def required_headers_espocrm(targeturl: str = Header(), targetkey: str = Header()):
return targeturl, targetkey
Loading

0 comments on commit b668530

Please sign in to comment.