Skip to content

Commit

Permalink
Merge pull request #6654 from hotosm/fastapi-refactor
Browse files Browse the repository at this point in the history
* Asyncio cron job scheduler set up. * Auto unlock task refactored. * Auto unlock task cron job. * Task geojson export.
  • Loading branch information
prabinoid authored Dec 11, 2024
2 parents 8c536f5 + 3d91d8e commit 883f0bb
Show file tree
Hide file tree
Showing 8 changed files with 314 additions and 125 deletions.
12 changes: 7 additions & 5 deletions backend/api/projects/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ async def get_project(
finally:
# this will try to unlock tasks that have been locked too long
try:
# TODO
await ProjectService.auto_unlock_tasks(project_id, db)
except Exception as e:
logger.critical(str(e))
Expand Down Expand Up @@ -293,10 +292,13 @@ def head(request: Request, project_id):
request.user.display_name, project_id
)
except ValueError:
return {
"Error": "User is not a manager of the project",
"SubCode": "UserPermissionError",
}, 403
return JSONResponse(
content={
"Error": "User is not a manager of the project",
"SubCode": "UserPermissionError",
},
status_code=403,
)

project_dto = ProjectAdminService.get_project_dto_for_admin(project_id)
return project_dto.model_dump(by_alias=True), 200
Expand Down
2 changes: 1 addition & 1 deletion backend/api/tasks/actions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from databases import Database
from fastapi import APIRouter, Depends, Query, Request, BackgroundTasks
from fastapi import APIRouter, BackgroundTasks, Depends, Query, Request
from fastapi.responses import JSONResponse
from loguru import logger

Expand Down
21 changes: 13 additions & 8 deletions backend/api/tasks/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from databases import Database
from fastapi import APIRouter, Depends, Request
from fastapi.responses import Response, StreamingResponse, JSONResponse
from fastapi.responses import Response, JSONResponse, StreamingResponse
from loguru import logger
from starlette.authentication import requires

Expand All @@ -17,6 +17,7 @@
from backend.services.users.authentication_service import tm
from backend.services.users.user_service import UserService
from backend.services.validator_service import ValidatorService
import json

router = APIRouter(
prefix="/projects",
Expand Down Expand Up @@ -119,14 +120,18 @@ async def get(request: Request, project_id: int, db: Database = Depends(get_db))
)

tasks_json = await ProjectService.get_project_tasks(db, int(project_id), tasks)

if as_file:
tasks_json = str(tasks_json).encode("utf-8")
return send_file(
io.BytesIO(tasks_json),
mimetype="application/json",
as_attachment=True,
download_name=f"{str(project_id)}-tasks.geojson",
tasks_json = json.dumps(tasks_json, indent=4) # Pretty-printed JSON
file_bytes = io.BytesIO(tasks_json.encode("utf-8"))
file_bytes.seek(0) # Reset stream position

# Return the file response for download
return StreamingResponse(
file_bytes,
media_type="application/json",
headers={
"Content-Disposition": f'attachment; filename="{project_id}-tasks.json"'
},
)
return tasks_json
except ProjectServiceError as e:
Expand Down
7 changes: 4 additions & 3 deletions backend/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import json
import logging
import os
from functools import lru_cache
from typing import Optional

from dotenv import load_dotenv
from pydantic import PostgresDsn, ValidationInfo, field_validator
from pydantic_settings import BaseSettings
from typing import Optional
import json
from pydantic import PostgresDsn, field_validator, ValidationInfo


class Settings(BaseSettings):
Expand Down
48 changes: 48 additions & 0 deletions backend/cron.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import asyncio
import atexit
import datetime

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger

from backend.db import db_connection
from backend.models.postgis.task import Task


async def auto_unlock_tasks():
async with db_connection.database.connection() as conn:
# Identify distinct project IDs that were touched in the last 2 hours
two_hours_ago = datetime.datetime.utcnow() - datetime.timedelta(minutes=120)
# Query to fetch distinct project IDs with recent task history
projects_query = """
SELECT DISTINCT project_id
FROM task_history
WHERE action_date > :two_hours_ago
"""
projects = await conn.fetch_all(
query=projects_query, values={"two_hours_ago": two_hours_ago}
)
# For each project, update task history for tasks that were not manually unlocked
for project in projects:
project_id = project["project_id"]
await Task.auto_unlock_tasks(project_id, conn)


# Setup scheduler with asyncio support
def setup_cron_jobs():
scheduler = AsyncIOScheduler()

# Add the job to run every minute
scheduler.add_job(
auto_unlock_tasks,
IntervalTrigger(minutes=120),
id="auto_unlock_tasks",
replace_existing=True,
)

# Start the scheduler
scheduler.start()
print("Scheduler initialized: auto_unlock_tasks runs every 2 hours.")

# Ensure scheduler stops gracefully on app shutdown
atexit.register(lambda: asyncio.run(scheduler.shutdown(wait=False)))
3 changes: 2 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from starlette.middleware.authentication import AuthenticationMiddleware

from backend.config import settings
from backend.cron import setup_cron_jobs
from backend.db import db_connection
from backend.routes import add_api_end_points
from backend.services.users.authentication_service import TokenAuthBackend
Expand Down Expand Up @@ -87,7 +88,7 @@ async def pyinstrument_middleware(request, call_next):
_app.add_middleware(
AuthenticationMiddleware, backend=TokenAuthBackend(), on_error=None
)

setup_cron_jobs()
add_api_end_points(_app)
return _app

Expand Down
Loading

0 comments on commit 883f0bb

Please sign in to comment.