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

Fix: use async by default, with occasional def threading #1015

Merged
merged 20 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
90e578d
build: add asgiref async_to_sync, plus pytest-asyncio
spwoodcock Nov 22, 2023
2743671
build: install script login linger after docker install
spwoodcock Nov 27, 2023
39b7d34
docs: update instructions to automount s3fs
spwoodcock Nov 27, 2023
6e8cd0d
fix: add user role to user schema, remove appuser logic
spwoodcock Nov 28, 2023
dc27d3f
refactor: async initial load read_xlsforms
spwoodcock Nov 28, 2023
91d2cb5
refactor: update refs to form --> category in project creation
spwoodcock Nov 28, 2023
0570c35
refactor: remove redundant projects.utils (replaced)
spwoodcock Nov 28, 2023
e8cd301
refactor: rename add_features --> upload_custom_extract
spwoodcock Nov 28, 2023
918ed71
fix: update all to async, except BackgroundTasks defs
spwoodcock Nov 28, 2023
bb1e995
fix: update long running central operations to run_in_threadpool
spwoodcock Nov 28, 2023
208a35f
test: update tests to be async + threadpool
spwoodcock Nov 28, 2023
be772cf
fix: update expensive generate_task_files to use ThreadPoolExecutor
spwoodcock Nov 28, 2023
f4880dc
fix: use threadpool for generate_task_files
spwoodcock Nov 28, 2023
de08ec3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 28, 2023
fb69c17
refactor: move logic for task schemas into models
spwoodcock Nov 28, 2023
cd4e426
Merge branch 'fix/sync-project-create' of https://github.com/hotosm/f…
spwoodcock Nov 28, 2023
ce61879
refactor: remove final ref to convert_app_tasks
spwoodcock Nov 28, 2023
b404da5
refactor: fix loading project summaries without db_obj
spwoodcock Nov 28, 2023
079617e
fix: add outline_centroid to task_schemas
spwoodcock Nov 29, 2023
fbb8b84
refactor: add task details to project schema output
spwoodcock Nov 29, 2023
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
2 changes: 1 addition & 1 deletion docs/User-Manual-For-Project-Managers.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Click on "Next" to proceed.
![WhatsApp Image 2023-06-23 at 1 17 37 PM](https://github.com/hotosm/fmtm/assets/97789856/f53d76b4-e6cc-44a4-8c7c-00082eb72693)

14. Select Form . Select the form category you want to use for the field mapping, such as "Data Extract" or any other relevant category.
Choose a specific form from the existing forms or upload a custom form if needed.
Choose a specific form from the existing categories or upload a custom form if needed.
Click on "Submit" to proceed.

![WhatsApp Image 2023-06-23 at 1 37 19 PM](https://github.com/hotosm/fmtm/assets/97789856/f9a4bed7-d1a9-44dd-b2d4-b55f428f9416)
Expand Down
6 changes: 4 additions & 2 deletions docs/dev/Backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,12 @@ Access the files like a directory under: `/mnt/fmtm/local`.

To mount permanently, add the following to `/etc/fstab`:

`fmtm-local /mnt/fmtm/local fuse.s3fs _netdev,allow_other,\
use_path_request_style,passwd_file=/home/$(whoami)/s3-creds/fmtm-local,\
`fmtm-data /mnt/fmtm/local fuse.s3fs _netdev,allow_other,\
use_path_request_style,passwd_file=/home/USERNAME/s3-creds/fmtm-local,\
url=http://s3.fmtm.localhost:7050 0 0`

> Note: you should replace USERNAME with your linux username.

## Running JOSM in the dev stack

- Run JOSM with FMTM:
Expand Down
13 changes: 8 additions & 5 deletions src/backend/app/auth/auth_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@


@router.get("/osm_login/")
def login_url(request: Request, osm_auth=Depends(init_osm_auth)):
async def login_url(request: Request, osm_auth=Depends(init_osm_auth)):
"""Get Login URL for OSM Oauth Application.

The application must be registered on openstreetmap.org.
Expand All @@ -56,7 +56,7 @@ def login_url(request: Request, osm_auth=Depends(init_osm_auth)):


@router.get("/callback/")
def callback(request: Request, osm_auth=Depends(init_osm_auth)):
async def callback(request: Request, osm_auth=Depends(init_osm_auth)):
"""Performs token exchange between OpenStreetMap and Export tool API.

Core will use Oauth secret key from configuration while deserializing token,
Expand All @@ -81,7 +81,7 @@ def callback(request: Request, osm_auth=Depends(init_osm_auth)):


@router.get("/me/", response_model=AuthUser)
def my_data(
async def my_data(
db: Session = Depends(database.get_db),
user_data: AuthUser = Depends(login_required),
):
Expand All @@ -95,9 +95,11 @@ def my_data(
user_data(dict): The dict of user data.
"""
# Save user info in User table
user = user_crud.get_user_by_id(db, user_data["id"])
user = await user_crud.get_user_by_id(db, user_data["id"])
if not user:
user_by_username = user_crud.get_user_by_username(db, user_data["username"])
user_by_username = await user_crud.get_user_by_username(
db, user_data["username"]
)
if user_by_username:
raise HTTPException(
status_code=400,
Expand All @@ -107,6 +109,7 @@ def my_data(
),
)

# Add user to database
db_user = DbUser(id=user_data["id"], username=user_data["username"])
db.add(db_user)
db.commit()
Expand Down
10 changes: 7 additions & 3 deletions src/backend/app/central/central_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def get_odk_project(odk_central: project_schemas.ODKCentral = None):
log.debug(f"Connecting to ODKCentral: url={url} user={user}")
project = OdkProject(url, user, pw)
except Exception as e:
log.error(e)
log.exception(e)
raise HTTPException(
status_code=500, detail=f"Error creating project on ODK Central: {e}"
) from e
Expand Down Expand Up @@ -144,7 +144,9 @@ def create_odk_project(name: str, odk_central: project_schemas.ODKCentral = None
) from e


def delete_odk_project(project_id: int, odk_central: project_schemas.ODKCentral = None):
async def delete_odk_project(
project_id: int, odk_central: project_schemas.ODKCentral = None
):
"""Delete a project from a remote ODK Server."""
# FIXME: when a project is deleted from Central, we have to update the
# odkid in the projects table
Expand Down Expand Up @@ -537,7 +539,9 @@ def generate_updated_xform(
return outfile


def create_qrcode(project_id: int, token: str, name: str, odk_central_url: str = None):
async def create_qrcode(
project_id: int, token: str, name: str, odk_central_url: str = None
):
"""Create the QR Code for an app-user."""
if not odk_central_url:
log.debug("ODKCentral connection variables not set in function")
Expand Down
12 changes: 8 additions & 4 deletions src/backend/app/central/central_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import json

from fastapi import APIRouter, Depends, HTTPException
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import JSONResponse
from loguru import logger as log
from sqlalchemy import (
Expand All @@ -44,7 +45,8 @@
async def list_projects():
"""List projects in Central."""
# TODO update for option to pass credentials by user
projects = central_crud.list_odk_projects()
# NOTE runs in separate thread using run_in_threadpool
projects = await run_in_threadpool(lambda: central_crud.list_odk_projects())
if projects is None:
return {"message": "No projects found"}
return JSONResponse(content={"projects": projects})
Expand All @@ -58,8 +60,7 @@ async def create_appuser(
):
"""Create an appuser in Central."""
appuser = central_crud.create_appuser(project_id, name=name)
# tasks = tasks_crud.update_qrcode(db, task_id, qrcode['id'])
return project_crud.create_qrcode(db, project_id, appuser.get("token"), name)
return await project_crud.create_qrcode(db, project_id, appuser.get("token"), name)


# @router.get("/list_submissions")
Expand All @@ -86,7 +87,8 @@ async def get_form_lists(
Returns:
A list of dictionary containing the id and title of each XForm record retrieved from the database.
"""
forms = central_crud.get_form_list(db, skip, limit)
# NOTE runs in separate thread using run_in_threadpool
forms = await run_in_threadpool(lambda: central_crud.get_form_list(db, skip, limit))
return forms


Expand Down Expand Up @@ -116,6 +118,8 @@ async def download_submissions(
xforms = central_crud.list_odk_xforms(first.odkid)
submissions = list()
for xform in xforms:
# FIXME this should be optimised via async or threadpool
# FIXME very expensive opteration to run blocking in parallel
data = central_crud.download_submissions(first.odkid, xform["xmlFormId"])
# An empty submissions only has the CSV headers
# headers = data[0]
Expand Down
13 changes: 11 additions & 2 deletions src/backend/app/db/postgis_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with FMTM. If not, see <https:#www.gnu.org/licenses/>.
#
"""PostGIS and geometry handling helper funcs."""

import datetime

Expand All @@ -25,11 +26,15 @@


def timestamp():
"""Used in SQL Alchemy models to ensure we refresh timestamp when new models initialised."""
"""Get the current time.

Used to insert a current timestamp into Pydantic models.
"""
return datetime.datetime.utcnow()


def geometry_to_geojson(geometry: Geometry, properties: str = {}, id: int = None):
"""Convert SQLAlchemy geometry to GeoJSON."""
if geometry:
shape = to_shape(geometry)
geojson = {
Expand All @@ -40,15 +45,19 @@ def geometry_to_geojson(geometry: Geometry, properties: str = {}, id: int = None
# "bbox": shape.bounds,
}
return Feature(**geojson)
return {}


def get_centroid(geometry: Geometry, properties: str = {}):
def get_centroid(geometry: Geometry, properties: str = {}, id: int = None):
"""Convert SQLAlchemy geometry to Centroid GeoJSON."""
if geometry:
shape = to_shape(geometry)
point = shape.centroid
geojson = {
"type": "Feature",
"geometry": mapping(point),
"properties": properties,
"id": id,
}
return Feature(**geojson)
return {}
2 changes: 1 addition & 1 deletion src/backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async def lifespan(app: FastAPI):
# Startup events
log.debug("Starting up FastAPI server.")
log.debug("Reading XLSForms from DB.")
read_xlsforms(next(get_db()), xlsforms_path)
await read_xlsforms(next(get_db()), xlsforms_path)

yield

Expand Down
Loading