Skip to content

Commit

Permalink
Merge pull request #6 from lapig-ufg/email
Browse files Browse the repository at this point in the history
file upload
  • Loading branch information
jairomr authored Oct 3, 2024
2 parents 9a6d927 + 7973545 commit 4e7687c
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 32 deletions.
83 changes: 52 additions & 31 deletions app/api/task.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,51 @@

from pathlib import Path
import shutil
import tempfile
from typing import List
from fastapi.responses import JSONResponse
import geopandas as gpd
from pydantic import EmailStr


from app.models.oauth2 import UserInfo
from app.oauth2 import has_role
from app.utils.file import check_geofiles
from worker import gee_get_index_pasture
from app.models.payload import PayloadSaveGeojson
from app.models.payload import PayloadSaveGeojson, User
import os

from fastapi import APIRouter, Depends, HTTPException, Request, Query
from fastapi import APIRouter, Depends, File, HTTPException, Request, Query, UploadFile
from app.config import logger
from celery.result import AsyncResult

router = APIRouter()


@router.post("/savegeom" )
async def savegeom(
@router.post("/savegeom/geojson" )
async def savegeom_geojson(
payload: PayloadSaveGeojson,
request: Request,
crs: int = Query(4326, description="EPSG code for the geometry"),
user_data: UserInfo = Depends(has_role(['savegeom'])) # This should be a function that retrieves user data from the request.
):

MAXHECTARES = os.environ.get('MAXHECTARES',40_000)
geojson = payload.dict().get('geojson',gpd.GeoDataFrame())
user = payload.dict().get('user')
logger.info(f"Received payload: {geojson}")
try:
gdf = gpd.GeoDataFrame.from_features(geojson, crs=crs)
if gdf.empty or len(gdf) > 1:
return HTTPException(status_code=400, detail="Empty GeoDataFrame or more than one feature.")
if gdf.geometry.type[0] != "Polygon":
return HTTPException(status_code=400, detail="Geometry must be a Polygon.")
if gdf.to_crs(5880).area.iloc[0] / 10_000 > MAXHECTARES:
return HTTPException(status_code=400, detail=f"Geometry area must be less than {MAXHECTARES} hectares.")
logger.info('Geometry is valid')
logger.info(user_data)
try:
dict_payload = {
**payload.dict(),
'request_user': user_data
}
task = gee_get_index_pasture.delay(dict_payload)
except Exception as e:
logger.exception(f"Failed to create task: {e}")
raise HTTPException(status_code=400, detail="Failed to create task.")
except Exception as e:
logger.error(f"{e}")
raise HTTPException(status_code=400, detail=e)
gdf = gpd.GeoDataFrame.from_features(geojson, crs=crs)
return __savegeom__(gdf, user, user_data)

@router.post("/savegeom/file" )
async def savegeom_gdf(
name: str,
email:EmailStr,
files: List[UploadFile] = File(...),
user_data: UserInfo = Depends(has_role(['savegeom'])) # This should be a function that retrieves user data from the request.
):
return __savegeom__(check_geofiles(files), {'name':name,'email':email}, user_data)




return JSONResponse({"task_id": task.id})

@router.get("/status/{task_id}")
def get_status(task_id):
Expand All @@ -60,4 +55,30 @@ def get_status(task_id):
"task_status": task_result.status,
"task_result": task_result.result
}
return JSONResponse(result)
return JSONResponse(result)


def __checkgeom__(gdf: gpd.GeoDataFrame):
MAXHECTARES = os.environ.get('MAXHECTARES',40_000)
if gdf.empty:
raise HTTPException(status_code=400, detail="Empty GeoDataFrame.")
if len(gdf) > 1:
raise HTTPException(status_code=400, detail="GeoDataFrame must have only one geometry.")
if gdf.geometry.type[0] != "Polygon":
raise HTTPException(status_code=400, detail="Geometry must be a Polygon.")
if gdf.to_crs(5880).area.iloc[0] / 10_000 > MAXHECTARES:
raise HTTPException(status_code=400, detail=f"Geometry area must be less than {MAXHECTARES} hectares.")
logger.info('Geometry is valid')
return gdf.to_crs(4326).to_geo_dict()



def __savegeom__(gdf: gpd.GeoDataFrame, user: User,user_data: UserInfo):
dict_payload = {
'user':user,
'geojson':__checkgeom__(gdf),
'request_user': user_data
}
logger.info(f"Starting task with payload: {dict_payload}")
task = gee_get_index_pasture.delay(dict_payload)
return JSONResponse({"task_id": task.id})
1 change: 0 additions & 1 deletion app/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ async def get_idp_public_key():
# Get the payload/token from keycloak
async def get_payload(token: str = Security(oauth2_scheme)) -> dict:
key= await get_idp_public_key()
logger.info(f"token {token}")
try:

return keycloak_openid.decode_token(
Expand Down
113 changes: 113 additions & 0 deletions app/utils/file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import shutil
import tempfile
from zipfile import ZipFile
from pathlib import Path
from fastapi import HTTPException
import geopandas as gpd
from app.config import logger

def valid_file_geo(content_type):

if content_type in [
'application/text',
'application/xml',
'text/plain',
'application/geo+json',
'application/geopackage+sqlite3',
'application/octet-stream',
'application/vnd.google-earth.kml+xml',
'application/vnd.google-earth.kmz',
'application/x-dbf',
'application/x-esri-crs',
'application/x-esri-shape',
'application/zip'
]:
return True
raise HTTPException(status_code=415, detail=f'Invalid file type: {content_type}')

def valid_extension_shp(file):
if file.suffix in [
".shp", # Geometria dos vetores
".shx", # Índice de geometria
".dbf", # Dados tabulares
".prj", # Sistema de coordenadas e projeção
".cpg", # Codificação de caracteres
".sbn", # Índice espacial
".sbx", # Arquivo auxiliar para índice espacial
".xml", # Metadados em formato XML
".qix", # Índice espacial (gerado por software)
".aih", # Índice de atributos para .dbf
".ain", # Arquivo auxiliar para índice de atributos
".qmd" # Extensão adicional (se aplicável)
]:
return True
raise HTTPException(status_code=415, detail=f'Invalid file type: {file.suffix}')


def check_geofiles(files):
with tempfile.TemporaryDirectory() as tmpdirname:
logger.info(f"Saving files to {tmpdirname}")
logger.info(files)
if len(files) == 1:
valid_file_geo(files[0].content_type)
file = files[0]
else:
extensions = []
for f in files:
valid_file_geo(f.content_type)
valid_extension_shp(Path(f.filename))
extensions.append(Path(f.filename).suffix)
with open(f'{tmpdirname}/{f.filename}', 'wb') as buffer:
shutil.copyfileobj(f.file, buffer)
minal_shape =set(['.shp', '.shx', '.dbf', '.prj']) - set(extensions)
if len(minal_shape) > 0:
raise HTTPException(status_code=400, detail=f'Missing files: {minal_shape}')
file = str(get_geofile(tmpdirname))
return read_file(file)


def read_kml(file):
import fiona
try:
return gpd.read_file(file, driver='KML')
except Exception as e:
gpd.io.file.fiona.drvsupport.supported_drivers['KML'] = 'rw'
return gpd.read_file(file, driver='KML')


def read_kmz(file):
import fiona
with tempfile.TemporaryDirectory() as tmpdirname:
kmz = ZipFile(file, 'r')
kmz.extract('doc.kml',tmpdirname)
try:
gdf = gpd.read_file(f'{tmpdirname}/doc.kml')
except Exception as e:
gpd.io.file.fiona.drvsupport.supported_drivers['KML'] = 'rw'
gdf = gpd.read_file(f'{tmpdirname}/doc.kml')

return gdf

def read_gpd(file):
return gpd.read_file(file)


def get_geofile(dirname):
for file in Path(dirname).glob('*'):
if file.suffix in ['.shp']:
return file


def read_file(file):
if isinstance(file, str):
gdf = gpd.read_file(file)
else:
match Path(file.filename).suffix.capitalize():
case '.kml':
gdf = read_kml(file.file)
case '.kmz':
gdf = read_kmz(file.file)
case _:
gdf = read_gpd(file.file)

return gdf
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies = [
"earthengine-api>=1.1.2",
"fastapi-keycloak-middleware>=1.1.0",
"fastapi>=0.115.0",
"fiona>=1.10.1",
"flower>=2.0.1",
"geemap>=0.34.5",
"geopandas>=1.0.1",
Expand Down
46 changes: 46 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4e7687c

Please sign in to comment.